mermaid-formatter 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 chenyanchen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # mermaid-formatter
2
+
3
+ A formatter for [Mermaid](https://mermaid.js.org/) diagram syntax.
4
+
5
+ ## Features
6
+
7
+ - Format all Mermaid diagram types (sequenceDiagram, flowchart, classDiagram, etc.)
8
+ - Normalize indentation and whitespace
9
+ - Format Mermaid code blocks in Markdown
10
+ - CLI tool and programmatic API
11
+ - Zero dependencies (lightweight)
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install mermaid-formatter
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### CLI
22
+
23
+ ```bash
24
+ # Format file to stdout
25
+ npx mmdfmt diagram.mmd
26
+
27
+ # Format file in-place
28
+ npx mmdfmt -w diagram.mmd
29
+
30
+ # Format from stdin
31
+ echo "sequenceDiagram
32
+ A->>B: hello" | npx mmdfmt
33
+
34
+ # Custom indent (default: 4 spaces)
35
+ npx mmdfmt --indent 2 diagram.mmd
36
+
37
+ # Use tabs instead of spaces
38
+ npx mmdfmt --tabs diagram.mmd
39
+ ```
40
+
41
+ ### Programmatic API
42
+
43
+ ```typescript
44
+ import { formatMermaid, formatMarkdownMermaidBlocks } from 'mermaid-formatter';
45
+
46
+ // Format Mermaid code
47
+ const input = `sequenceDiagram
48
+ participant A
49
+ A->>B: Hello`;
50
+
51
+ const formatted = formatMermaid(input);
52
+ // Output:
53
+ // sequenceDiagram
54
+ // participant A
55
+ // A->>B: Hello
56
+
57
+ // With options
58
+ const formatted2 = formatMermaid(input, { indentSize: 2, useTabs: false });
59
+
60
+ // Format Mermaid blocks in Markdown
61
+ const markdown = `
62
+ # My Document
63
+
64
+ \`\`\`mermaid
65
+ sequenceDiagram
66
+ A->>B: hello
67
+ \`\`\`
68
+ `;
69
+
70
+ const formattedMarkdown = formatMarkdownMermaidBlocks(markdown);
71
+ ```
72
+
73
+ ### API Reference
74
+
75
+ #### `formatMermaid(input: string, options?: FormatOptions): string`
76
+
77
+ Format Mermaid diagram source code.
78
+
79
+ **Options:**
80
+
81
+ - `indentSize` (number, default: 4) - Number of spaces for indentation
82
+ - `useTabs` (boolean, default: false) - Use tabs instead of spaces
83
+
84
+ #### `formatMarkdownMermaidBlocks(markdown: string, options?: FormatOptions): string`
85
+
86
+ Format all Mermaid code blocks in a Markdown document.
87
+
88
+ #### `parse(input: string): Diagram`
89
+
90
+ Parse Mermaid source into an AST.
91
+
92
+ #### `detectDiagramType(input: string): DiagramType`
93
+
94
+ Detect the diagram type from source code.
95
+
96
+ ## Formatting Rules
97
+
98
+ - Diagram declaration at column 0
99
+ - Block keywords (`critical`, `alt`, `loop`, `par`, `opt`, `break`, `rect`, `subgraph`, `end`) at column 0
100
+ - Content inside blocks indented by configured amount
101
+ - Consecutive blank lines collapsed to single blank line
102
+ - Blank line inserted before block starts
103
+ - Whitespace normalized (multiple spaces → single, bracket padding removed)
104
+
105
+ ## Supported Diagram Types
106
+
107
+ - sequenceDiagram
108
+ - flowchart / graph
109
+ - classDiagram
110
+ - stateDiagram / stateDiagram-v2
111
+ - erDiagram
112
+ - journey
113
+ - gantt
114
+ - pie
115
+ - quadrantChart
116
+ - requirementDiagram
117
+ - gitGraph
118
+ - mindmap
119
+ - timeline
120
+ - sankey-beta
121
+ - xychart-beta
122
+ - block-beta
123
+ - architecture-beta
124
+
125
+ ## License
126
+
127
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
package/dist/cli.js ADDED
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync } from 'node:fs';
3
+ import { formatMermaid } from './index.js';
4
+ function parseArgs(args) {
5
+ const result = {
6
+ write: false,
7
+ indent: 4,
8
+ tabs: false,
9
+ help: false,
10
+ version: false,
11
+ };
12
+ for (let i = 0; i < args.length; i++) {
13
+ const arg = args[i];
14
+ if (arg === '-h' || arg === '--help') {
15
+ result.help = true;
16
+ }
17
+ else if (arg === '-v' || arg === '--version') {
18
+ result.version = true;
19
+ }
20
+ else if (arg === '-w' || arg === '--write') {
21
+ result.write = true;
22
+ }
23
+ else if (arg === '--tabs') {
24
+ result.tabs = true;
25
+ }
26
+ else if (arg === '--indent') {
27
+ const next = args[++i];
28
+ const parsed = parseInt(next, 10);
29
+ result.indent = Number.isNaN(parsed) ? 4 : parsed;
30
+ }
31
+ else if (arg.startsWith('--indent=')) {
32
+ const parsed = parseInt(arg.slice(9), 10);
33
+ result.indent = Number.isNaN(parsed) ? 4 : parsed;
34
+ }
35
+ else if (!arg.startsWith('-')) {
36
+ result.file = arg;
37
+ }
38
+ }
39
+ return result;
40
+ }
41
+ function printHelp() {
42
+ console.log(`
43
+ mmdfmt - Mermaid diagram formatter
44
+
45
+ USAGE:
46
+ mmdfmt [OPTIONS] [FILE]
47
+
48
+ ARGS:
49
+ <FILE> Input file (reads from stdin if not provided)
50
+
51
+ OPTIONS:
52
+ -w, --write Write result to source file instead of stdout
53
+ --indent <N> Number of spaces for indentation (default: 4)
54
+ --tabs Use tabs instead of spaces
55
+ -h, --help Print help information
56
+ -v, --version Print version information
57
+
58
+ EXAMPLES:
59
+ # Format file to stdout
60
+ mmdfmt diagram.mmd
61
+
62
+ # Format file in-place
63
+ mmdfmt -w diagram.mmd
64
+
65
+ # Format from stdin
66
+ echo "sequenceDiagram" | mmdfmt
67
+
68
+ # Custom indent
69
+ mmdfmt --indent 2 diagram.mmd
70
+ mmdfmt --tabs diagram.mmd
71
+ `);
72
+ }
73
+ function printVersion() {
74
+ // Read version from package.json
75
+ try {
76
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
77
+ console.log(`mmdfmt ${pkg.version}`);
78
+ }
79
+ catch {
80
+ console.log('mmdfmt 0.1.0');
81
+ }
82
+ }
83
+ async function readStdin() {
84
+ const chunks = [];
85
+ return new Promise((resolve, reject) => {
86
+ process.stdin.on('data', (chunk) => chunks.push(chunk));
87
+ process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
88
+ process.stdin.on('error', reject);
89
+ });
90
+ }
91
+ async function main() {
92
+ const args = parseArgs(process.argv.slice(2));
93
+ if (args.help) {
94
+ printHelp();
95
+ process.exit(0);
96
+ }
97
+ if (args.version) {
98
+ printVersion();
99
+ process.exit(0);
100
+ }
101
+ const options = {
102
+ indentSize: args.indent,
103
+ useTabs: args.tabs,
104
+ };
105
+ let input;
106
+ if (args.file) {
107
+ try {
108
+ input = readFileSync(args.file, 'utf-8');
109
+ }
110
+ catch (_err) {
111
+ console.error(`Error reading file: ${args.file}`);
112
+ process.exit(1);
113
+ }
114
+ }
115
+ else {
116
+ // Check if stdin is a TTY (no piped input)
117
+ if (process.stdin.isTTY) {
118
+ printHelp();
119
+ process.exit(0);
120
+ }
121
+ input = await readStdin();
122
+ }
123
+ try {
124
+ const formatted = formatMermaid(input, options);
125
+ if (args.write && args.file) {
126
+ writeFileSync(args.file, formatted, 'utf-8');
127
+ }
128
+ else {
129
+ process.stdout.write(formatted);
130
+ }
131
+ }
132
+ catch (err) {
133
+ console.error(`Error formatting: ${err instanceof Error ? err.message : err}`);
134
+ process.exit(1);
135
+ }
136
+ }
137
+ main().catch((err) => {
138
+ console.error(err);
139
+ process.exit(1);
140
+ });
141
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Formatter for Mermaid diagram AST.
3
+ * Converts AST back to formatted source code.
4
+ */
5
+ import type { Diagram, FormatOptions } from './types.js';
6
+ /**
7
+ * Format a parsed diagram AST back to string.
8
+ */
9
+ export declare function format(diagram: Diagram, options?: FormatOptions): string;
10
+ //# sourceMappingURL=formatter.d.ts.map
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Formatter for Mermaid diagram AST.
3
+ * Converts AST back to formatted source code.
4
+ */
5
+ // ============================================================================
6
+ // Configuration
7
+ // ============================================================================
8
+ const DEFAULT_OPTIONS = {
9
+ indentSize: 4,
10
+ useTabs: false,
11
+ };
12
+ const STATEMENT_FORMATTERS = {
13
+ 'block-start': (stmt) => {
14
+ const s = stmt;
15
+ return s.label ? `${s.blockKind} ${s.label}` : s.blockKind;
16
+ },
17
+ 'brace-block-start': (stmt) => {
18
+ const s = stmt;
19
+ return `${s.blockKind} ${s.name} {`;
20
+ },
21
+ 'block-option': (stmt) => {
22
+ const label = stmt.label;
23
+ return label ? `option ${label}` : 'option';
24
+ },
25
+ 'block-else': (stmt) => {
26
+ const label = stmt.label;
27
+ return label ? `else ${label}` : 'else';
28
+ },
29
+ };
30
+ // Statements that need content normalization
31
+ const NORMALIZABLE_TYPES = [
32
+ 'generic-line',
33
+ 'participant',
34
+ 'note',
35
+ ];
36
+ const CONTENT_NORMALIZERS = [
37
+ // Collapse multiple spaces to single space
38
+ (content) => content.replace(/ +/g, ' '),
39
+ // Normalize spaces after colon (A: B -> A: B)
40
+ (content) => content.replace(/:\s{2,}/g, ': '),
41
+ // Normalize bracket padding: [ text ] -> [text]
42
+ (content) => normalizeBracketPair(content, '[', ']'),
43
+ // Normalize brace padding: { text } -> {text}
44
+ (content) => normalizeBracketPair(content, '{', '}'),
45
+ // Normalize paren padding: ( text ) -> (text)
46
+ (content) => normalizeBracketPair(content, '(', ')'),
47
+ // Normalize pipe labels: | text | -> |text|
48
+ (content) => content.replace(/\|\s+([^|]*?)\s*\|/g, (_, inner) => `|${inner.trim()}|`),
49
+ ];
50
+ function normalizeContent(content) {
51
+ return CONTENT_NORMALIZERS.reduce((acc, fn) => fn(acc), content);
52
+ }
53
+ // ============================================================================
54
+ // Indentation Rules
55
+ // ============================================================================
56
+ /** Statements that always appear at column 0 (relative to brace depth) */
57
+ const COLUMN_ZERO_TYPES = [
58
+ 'diagram-decl',
59
+ 'directive',
60
+ 'block-start',
61
+ 'block-option',
62
+ 'block-else',
63
+ 'block-end',
64
+ ];
65
+ function getIndentDepth(stmt, seenDiagramDecl, braceBlockDepth) {
66
+ // Diagram declaration and directives: always at column 0
67
+ if (stmt.type === 'diagram-decl' || stmt.type === 'directive') {
68
+ return 0;
69
+ }
70
+ // Block control statements: at column 0 (relative to brace depth)
71
+ if (COLUMN_ZERO_TYPES.includes(stmt.type)) {
72
+ return braceBlockDepth;
73
+ }
74
+ // Brace block start/end: at brace depth level
75
+ if (stmt.type === 'brace-block-start' || stmt.type === 'brace-block-end') {
76
+ return braceBlockDepth;
77
+ }
78
+ // Inside brace blocks: use brace depth directly
79
+ if (braceBlockDepth > 0) {
80
+ return braceBlockDepth;
81
+ }
82
+ // At top level (after diagram decl): indent by 1
83
+ if (seenDiagramDecl) {
84
+ return 1;
85
+ }
86
+ return 0;
87
+ }
88
+ // ============================================================================
89
+ // Blank Line Rules
90
+ // ============================================================================
91
+ /** Types that trigger blank line insertion before block starts */
92
+ const BLANK_BEFORE_BLOCK_TYPES = [
93
+ 'diagram-decl',
94
+ 'generic-line',
95
+ 'participant',
96
+ 'note',
97
+ 'block-end',
98
+ 'brace-block-end',
99
+ ];
100
+ function shouldInsertBlankBefore(stmt, lastNonBlankType) {
101
+ if (!lastNonBlankType)
102
+ return false;
103
+ // Insert blank before block-start or brace-block-start
104
+ if (stmt.type === 'block-start' || stmt.type === 'brace-block-start') {
105
+ return BLANK_BEFORE_BLOCK_TYPES.includes(lastNonBlankType);
106
+ }
107
+ return false;
108
+ }
109
+ // ============================================================================
110
+ // Main Format Function
111
+ // ============================================================================
112
+ /**
113
+ * Format a parsed diagram AST back to string.
114
+ */
115
+ export function format(diagram, options = {}) {
116
+ const opts = { ...DEFAULT_OPTIONS, ...options };
117
+ const indentStr = opts.useTabs ? '\t' : ' '.repeat(opts.indentSize);
118
+ const lines = [];
119
+ let braceBlockDepth = 0;
120
+ let seenDiagramDecl = false;
121
+ let lastNonBlankType = null;
122
+ for (let i = 0; i < diagram.statements.length; i++) {
123
+ const stmt = diagram.statements[i];
124
+ // Handle blank lines: collapse consecutive blanks, skip trailing
125
+ if (stmt.type === 'blank-line') {
126
+ if (lastNonBlankType === 'blank-line' ||
127
+ i === diagram.statements.length - 1) {
128
+ continue;
129
+ }
130
+ if (lines.length === 0) {
131
+ continue;
132
+ }
133
+ lines.push('');
134
+ lastNonBlankType = 'blank-line';
135
+ continue;
136
+ }
137
+ // Insert blank line before block-start if needed
138
+ if (shouldInsertBlankBefore(stmt, lastNonBlankType)) {
139
+ if (lines.length > 0 && lines[lines.length - 1] !== '') {
140
+ lines.push('');
141
+ }
142
+ }
143
+ // Decrement brace depth before formatting brace-block-end
144
+ if (stmt.type === 'brace-block-end' && braceBlockDepth > 0) {
145
+ braceBlockDepth--;
146
+ }
147
+ // Calculate indentation depth
148
+ const depth = getIndentDepth(stmt, seenDiagramDecl, braceBlockDepth);
149
+ // Format the statement
150
+ const content = formatStatement(stmt);
151
+ const formatted = depth > 0 ? indentStr.repeat(depth) + content : content;
152
+ lines.push(formatted);
153
+ // Update state
154
+ if (stmt.type === 'diagram-decl') {
155
+ seenDiagramDecl = true;
156
+ }
157
+ if (stmt.type === 'brace-block-start') {
158
+ braceBlockDepth++;
159
+ }
160
+ lastNonBlankType = stmt.type;
161
+ }
162
+ // Ensure single trailing newline
163
+ return lines.join('\n') + '\n';
164
+ }
165
+ /**
166
+ * Format a single statement's content.
167
+ */
168
+ function formatStatement(stmt) {
169
+ // Use custom formatter if available
170
+ const formatter = STATEMENT_FORMATTERS[stmt.type];
171
+ if (formatter) {
172
+ return formatter(stmt);
173
+ }
174
+ // Normalize content for specific types
175
+ if (NORMALIZABLE_TYPES.includes(stmt.type)) {
176
+ return normalizeContent(stmt.content);
177
+ }
178
+ // Default: return content as-is
179
+ return stmt.content;
180
+ }
181
+ // ============================================================================
182
+ // Helper Functions
183
+ // ============================================================================
184
+ /**
185
+ * Normalize padding inside bracket pairs.
186
+ * Only normalizes when there's space after opening bracket.
187
+ */
188
+ function normalizeBracketPair(content, open, close) {
189
+ const chars = [...content];
190
+ const result = [];
191
+ let i = 0;
192
+ while (i < chars.length) {
193
+ if (chars[i] === open && chars[i + 1] === ' ') {
194
+ // Find matching close bracket
195
+ let depth = 1;
196
+ let j = i + 1;
197
+ while (j < chars.length && depth > 0) {
198
+ if (chars[j] === open)
199
+ depth++;
200
+ else if (chars[j] === close)
201
+ depth--;
202
+ j++;
203
+ }
204
+ if (depth === 0) {
205
+ // Extract inner content (excluding brackets)
206
+ const inner = chars
207
+ .slice(i + 1, j - 1)
208
+ .join('')
209
+ .trim();
210
+ result.push(open, inner, close);
211
+ i = j;
212
+ continue;
213
+ }
214
+ }
215
+ result.push(chars[i]);
216
+ i++;
217
+ }
218
+ return result.join('');
219
+ }
220
+ //# sourceMappingURL=formatter.js.map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Mermaid Formatter - Public API
3
+ *
4
+ * Format Mermaid diagram syntax for consistent code style.
5
+ */
6
+ export type { FormatOptions, DiagramType, Statement, Diagram, StatementType, BlockKind, BraceBlockKind, } from './types.js';
7
+ export { parse, detectDiagramType } from './parser.js';
8
+ export { format } from './formatter.js';
9
+ export { isIndentSensitive, INDENT_SENSITIVE_DIAGRAMS } from './rules.js';
10
+ import type { FormatOptions } from './types.js';
11
+ /**
12
+ * Format Mermaid diagram source code.
13
+ *
14
+ * @param input - Mermaid diagram source code
15
+ * @param options - Formatting options
16
+ * @returns Formatted Mermaid code
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { formatMermaid } from 'mermaid-formatter';
21
+ *
22
+ * const formatted = formatMermaid(`
23
+ * sequenceDiagram
24
+ * participant A
25
+ * A->>B: Hello
26
+ * `);
27
+ * ```
28
+ */
29
+ export declare function formatMermaid(input: string, options?: FormatOptions): string;
30
+ /**
31
+ * Format Mermaid code blocks in Markdown.
32
+ *
33
+ * Preserves indentation for nested code blocks (in lists, blockquotes, etc.)
34
+ *
35
+ * @param markdown - Markdown content
36
+ * @param options - Formatting options
37
+ * @returns Markdown with formatted Mermaid blocks
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * import { formatMarkdownMermaidBlocks } from 'mermaid-formatter';
42
+ *
43
+ * const formatted = formatMarkdownMermaidBlocks(`
44
+ * # My Document
45
+ *
46
+ * \`\`\`mermaid
47
+ * sequenceDiagram
48
+ * A->>B: Hello
49
+ * \`\`\`
50
+ * `);
51
+ * ```
52
+ */
53
+ export declare function formatMarkdownMermaidBlocks(markdown: string, options?: FormatOptions): string;
54
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Mermaid Formatter - Public API
3
+ *
4
+ * Format Mermaid diagram syntax for consistent code style.
5
+ */
6
+ // Function exports
7
+ export { parse, detectDiagramType } from './parser.js';
8
+ export { format } from './formatter.js';
9
+ export { isIndentSensitive, INDENT_SENSITIVE_DIAGRAMS } from './rules.js';
10
+ // Internal imports
11
+ import { parse, detectDiagramType } from './parser.js';
12
+ import { format } from './formatter.js';
13
+ import { isIndentSensitive } from './rules.js';
14
+ // ============================================================================
15
+ // Main API
16
+ // ============================================================================
17
+ /**
18
+ * Format Mermaid diagram source code.
19
+ *
20
+ * @param input - Mermaid diagram source code
21
+ * @param options - Formatting options
22
+ * @returns Formatted Mermaid code
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * import { formatMermaid } from 'mermaid-formatter';
27
+ *
28
+ * const formatted = formatMermaid(`
29
+ * sequenceDiagram
30
+ * participant A
31
+ * A->>B: Hello
32
+ * `);
33
+ * ```
34
+ */
35
+ export function formatMermaid(input, options) {
36
+ // Pipeline: detect -> check policy -> parse -> format
37
+ const diagramType = detectDiagramType(input);
38
+ // Policy: skip formatting for indent-sensitive diagrams
39
+ if (isIndentSensitive(diagramType)) {
40
+ return ensureTrailingNewline(input);
41
+ }
42
+ const diagram = parse(input);
43
+ return format(diagram, options);
44
+ }
45
+ /**
46
+ * Format Mermaid code blocks in Markdown.
47
+ *
48
+ * Preserves indentation for nested code blocks (in lists, blockquotes, etc.)
49
+ *
50
+ * @param markdown - Markdown content
51
+ * @param options - Formatting options
52
+ * @returns Markdown with formatted Mermaid blocks
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * import { formatMarkdownMermaidBlocks } from 'mermaid-formatter';
57
+ *
58
+ * const formatted = formatMarkdownMermaidBlocks(`
59
+ * # My Document
60
+ *
61
+ * \`\`\`mermaid
62
+ * sequenceDiagram
63
+ * A->>B: Hello
64
+ * \`\`\`
65
+ * `);
66
+ * ```
67
+ */
68
+ export function formatMarkdownMermaidBlocks(markdown, options) {
69
+ // Pattern captures:
70
+ // 1. Leading indentation (spaces/tabs before ```)
71
+ // 2. Code content between fences
72
+ // Supports both LF and CRLF line endings
73
+ return markdown.replace(/^([ \t]*)```mermaid\r?\n([\s\S]*?)```/gm, (_, indent, code) => {
74
+ const formatted = formatMermaid(code, options);
75
+ const indentedCode = applyIndent(formatted, indent);
76
+ return `${indent}\`\`\`mermaid\n${indentedCode}${indent}\`\`\``;
77
+ });
78
+ }
79
+ // ============================================================================
80
+ // Helper Functions
81
+ // ============================================================================
82
+ /**
83
+ * Ensure string ends with exactly one newline.
84
+ */
85
+ function ensureTrailingNewline(input) {
86
+ return input.endsWith('\n') ? input : input + '\n';
87
+ }
88
+ /**
89
+ * Apply indentation to each non-empty line.
90
+ */
91
+ function applyIndent(content, indent) {
92
+ if (!indent)
93
+ return content;
94
+ return content
95
+ .split('\n')
96
+ .map((line) => (line ? indent + line : line))
97
+ .join('\n');
98
+ }
99
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Lightweight parser for Mermaid diagram syntax.
3
+ * Converts source code into an AST for formatting.
4
+ */
5
+ import type { Diagram, DiagramType } from './types.js';
6
+ /**
7
+ * Parse Mermaid diagram source into AST.
8
+ */
9
+ export declare function parse(input: string): Diagram;
10
+ /**
11
+ * Detect diagram type from source code.
12
+ */
13
+ export declare function detectDiagramType(input: string): DiagramType;
14
+ //# sourceMappingURL=parser.d.ts.map
package/dist/parser.js ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Lightweight parser for Mermaid diagram syntax.
3
+ * Converts source code into an AST for formatting.
4
+ */
5
+ import { matchDiagramType, matchBlockKeyword, matchBraceBlockStart, } from './rules.js';
6
+ /**
7
+ * Parse Mermaid diagram source into AST.
8
+ */
9
+ export function parse(input) {
10
+ const lines = input.split('\n');
11
+ const statements = [];
12
+ let diagramType = 'unknown';
13
+ for (const line of lines) {
14
+ const trimmed = line.trim();
15
+ const statement = parseLine(trimmed, diagramType);
16
+ if (statement) {
17
+ // Track diagram type from first declaration
18
+ if (statement.type === 'diagram-decl' && diagramType === 'unknown') {
19
+ diagramType = statement.diagramType;
20
+ }
21
+ statements.push(statement);
22
+ }
23
+ }
24
+ return { type: diagramType, statements };
25
+ }
26
+ /**
27
+ * Parse a single line into a statement.
28
+ */
29
+ function parseLine(trimmed, currentDiagramType) {
30
+ // Blank line
31
+ if (trimmed === '') {
32
+ return { type: 'blank-line', content: '' };
33
+ }
34
+ // Comment (but not directive)
35
+ if (trimmed.startsWith('%%') && !trimmed.startsWith('%%{')) {
36
+ return { type: 'comment', content: trimmed };
37
+ }
38
+ // Directive %%{ ... }%%
39
+ if (trimmed.startsWith('%%{')) {
40
+ return { type: 'directive', content: trimmed };
41
+ }
42
+ // Diagram declaration
43
+ const detectedType = matchDiagramType(trimmed);
44
+ if (detectedType && currentDiagramType === 'unknown') {
45
+ return {
46
+ type: 'diagram-decl',
47
+ diagramType: detectedType,
48
+ content: trimmed,
49
+ };
50
+ }
51
+ // Block end: 'end'
52
+ if (trimmed === 'end') {
53
+ return { type: 'block-end', content: 'end' };
54
+ }
55
+ // Brace block end: '}'
56
+ if (trimmed === '}') {
57
+ return { type: 'brace-block-end', content: '}' };
58
+ }
59
+ // Block option (sequence diagram)
60
+ if (/^option\b/.test(trimmed)) {
61
+ const label = trimmed.slice(6).trim() || undefined;
62
+ return { type: 'block-option', label, content: trimmed };
63
+ }
64
+ // Block else (sequence diagram)
65
+ if (/^else\b/.test(trimmed)) {
66
+ const label = trimmed.slice(4).trim() || undefined;
67
+ return { type: 'block-else', label, content: trimmed };
68
+ }
69
+ // Block start with 'end' keyword
70
+ const blockKind = matchBlockKeyword(trimmed);
71
+ if (blockKind) {
72
+ const label = trimmed.slice(blockKind.length).trim() || undefined;
73
+ return {
74
+ type: 'block-start',
75
+ blockKind,
76
+ label,
77
+ content: trimmed,
78
+ };
79
+ }
80
+ // Brace block start (state/class/namespace with {)
81
+ const braceBlock = matchBraceBlockStart(trimmed);
82
+ if (braceBlock) {
83
+ return {
84
+ type: 'brace-block-start',
85
+ blockKind: braceBlock.kind,
86
+ name: braceBlock.name,
87
+ content: trimmed,
88
+ };
89
+ }
90
+ // Participant declaration (sequence diagram)
91
+ if (/^participant\b/.test(trimmed) || /^actor\b/.test(trimmed)) {
92
+ return { type: 'participant', content: trimmed };
93
+ }
94
+ // Note
95
+ if (/^note\b/i.test(trimmed)) {
96
+ return { type: 'note', content: trimmed };
97
+ }
98
+ // Generic line (arrows, relationships, nodes, etc.)
99
+ return { type: 'generic-line', content: trimmed };
100
+ }
101
+ /**
102
+ * Detect diagram type from source code.
103
+ */
104
+ export function detectDiagramType(input) {
105
+ const lines = input.split('\n');
106
+ for (const line of lines) {
107
+ const trimmed = line.trim();
108
+ if (trimmed === '' || trimmed.startsWith('%%'))
109
+ continue;
110
+ const type = matchDiagramType(trimmed);
111
+ if (type)
112
+ return type;
113
+ }
114
+ return 'unknown';
115
+ }
116
+ //# sourceMappingURL=parser.js.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Centralized grammar rules and patterns for Mermaid diagrams.
3
+ * All syntax definitions in one place for easy maintenance.
4
+ */
5
+ import type { DiagramType, BlockKind, BraceBlockKind } from './types.js';
6
+ /**
7
+ * Patterns for detecting diagram types.
8
+ * Order matters: more specific patterns should come first.
9
+ */
10
+ export declare const DIAGRAM_PATTERNS: [RegExp, DiagramType][];
11
+ /**
12
+ * Block keywords that close with 'end' keyword.
13
+ * Used in sequence diagrams, flowcharts, etc.
14
+ */
15
+ export declare const BLOCK_KEYWORDS: BlockKind[];
16
+ /**
17
+ * Block keywords that close with '}'.
18
+ * Used in class diagrams, state diagrams, etc.
19
+ */
20
+ export declare const BRACE_BLOCK_KEYWORDS: BraceBlockKind[];
21
+ /**
22
+ * Diagram types where indentation represents hierarchy.
23
+ * These should NOT be reformatted as it would change semantics.
24
+ */
25
+ export declare const INDENT_SENSITIVE_DIAGRAMS: DiagramType[];
26
+ /**
27
+ * Check if a diagram type is indent-sensitive.
28
+ */
29
+ export declare function isIndentSensitive(diagramType: DiagramType): boolean;
30
+ /**
31
+ * Match diagram type from a line of text.
32
+ */
33
+ export declare function matchDiagramType(line: string): DiagramType | null;
34
+ /**
35
+ * Match block start keyword (critical, alt, loop, etc.)
36
+ */
37
+ export declare function matchBlockKeyword(line: string): BlockKind | null;
38
+ /**
39
+ * Match brace block start (state Name {, class Name {, namespace Name {)
40
+ */
41
+ export declare function matchBraceBlockStart(line: string): {
42
+ kind: BraceBlockKind;
43
+ name: string;
44
+ } | null;
45
+ //# sourceMappingURL=rules.d.ts.map
package/dist/rules.js ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Centralized grammar rules and patterns for Mermaid diagrams.
3
+ * All syntax definitions in one place for easy maintenance.
4
+ */
5
+ /**
6
+ * Patterns for detecting diagram types.
7
+ * Order matters: more specific patterns should come first.
8
+ */
9
+ export const DIAGRAM_PATTERNS = [
10
+ [/^sequenceDiagram\b/, 'sequenceDiagram'],
11
+ [/^flowchart(\s+(TD|TB|BT|LR|RL))?\b/, 'flowchart'],
12
+ [/^graph(\s+(TD|TB|BT|LR|RL))?\b/, 'graph'],
13
+ [/^classDiagram\b/, 'classDiagram'],
14
+ [/^stateDiagram-v2\b/, 'stateDiagram-v2'],
15
+ [/^stateDiagram\b/, 'stateDiagram'],
16
+ [/^erDiagram\b/, 'erDiagram'],
17
+ [/^journey\b/, 'journey'],
18
+ [/^gantt\b/, 'gantt'],
19
+ [/^pie(\s+showData)?\b/, 'pie'],
20
+ [/^quadrantChart\b/, 'quadrantChart'],
21
+ [/^requirementDiagram\b/, 'requirementDiagram'],
22
+ [/^gitGraph\b/, 'gitGraph'],
23
+ [/^mindmap\b/, 'mindmap'],
24
+ [/^timeline\b/, 'timeline'],
25
+ [/^sankey-beta\b/, 'sankey-beta'],
26
+ [/^xychart-beta\b/, 'xychart-beta'],
27
+ [/^block-beta\b/, 'block-beta'],
28
+ [/^architecture-beta\b/, 'architecture-beta'],
29
+ ];
30
+ /**
31
+ * Block keywords that close with 'end' keyword.
32
+ * Used in sequence diagrams, flowcharts, etc.
33
+ */
34
+ export const BLOCK_KEYWORDS = [
35
+ 'critical',
36
+ 'alt',
37
+ 'loop',
38
+ 'par',
39
+ 'opt',
40
+ 'break',
41
+ 'rect',
42
+ 'subgraph',
43
+ ];
44
+ /**
45
+ * Block keywords that close with '}'.
46
+ * Used in class diagrams, state diagrams, etc.
47
+ */
48
+ export const BRACE_BLOCK_KEYWORDS = [
49
+ 'state',
50
+ 'class',
51
+ 'namespace',
52
+ ];
53
+ /**
54
+ * Diagram types where indentation represents hierarchy.
55
+ * These should NOT be reformatted as it would change semantics.
56
+ */
57
+ export const INDENT_SENSITIVE_DIAGRAMS = ['mindmap', 'timeline'];
58
+ /**
59
+ * Check if a diagram type is indent-sensitive.
60
+ */
61
+ export function isIndentSensitive(diagramType) {
62
+ return INDENT_SENSITIVE_DIAGRAMS.includes(diagramType);
63
+ }
64
+ /**
65
+ * Match diagram type from a line of text.
66
+ */
67
+ export function matchDiagramType(line) {
68
+ for (const [pattern, type] of DIAGRAM_PATTERNS) {
69
+ if (pattern.test(line)) {
70
+ return type;
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+ /**
76
+ * Match block start keyword (critical, alt, loop, etc.)
77
+ */
78
+ export function matchBlockKeyword(line) {
79
+ for (const keyword of BLOCK_KEYWORDS) {
80
+ const pattern = new RegExp(`^${keyword}\\b`);
81
+ if (pattern.test(line)) {
82
+ return keyword;
83
+ }
84
+ }
85
+ return null;
86
+ }
87
+ /**
88
+ * Match brace block start (state Name {, class Name {, namespace Name {)
89
+ */
90
+ export function matchBraceBlockStart(line) {
91
+ for (const keyword of BRACE_BLOCK_KEYWORDS) {
92
+ const pattern = new RegExp(`^${keyword}\\s+(.+?)\\s*\\{\\s*$`);
93
+ const match = line.match(pattern);
94
+ if (match) {
95
+ return { kind: keyword, name: match[1].trim() };
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+ //# sourceMappingURL=rules.js.map
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Type definitions for Mermaid formatter AST and options.
3
+ */
4
+ /**
5
+ * Formatting configuration options.
6
+ */
7
+ export interface FormatOptions {
8
+ /** Number of spaces for indentation (default: 4) */
9
+ indentSize?: number;
10
+ /** Use tabs instead of spaces (default: false) */
11
+ useTabs?: boolean;
12
+ }
13
+ /**
14
+ * Supported Mermaid diagram types.
15
+ */
16
+ export type DiagramType = 'sequenceDiagram' | 'flowchart' | 'graph' | 'classDiagram' | 'stateDiagram' | 'stateDiagram-v2' | 'erDiagram' | 'journey' | 'gantt' | 'pie' | 'quadrantChart' | 'requirementDiagram' | 'gitGraph' | 'mindmap' | 'timeline' | 'sankey-beta' | 'xychart-beta' | 'block-beta' | 'architecture-beta' | 'unknown';
17
+ /**
18
+ * Block types that close with 'end' keyword.
19
+ */
20
+ export type BlockKind = 'critical' | 'alt' | 'loop' | 'par' | 'opt' | 'break' | 'rect' | 'subgraph';
21
+ /**
22
+ * Block types that close with '}'.
23
+ */
24
+ export type BraceBlockKind = 'state' | 'class' | 'namespace';
25
+ /** Base interface for all statement nodes */
26
+ interface StatementBase {
27
+ type: string;
28
+ }
29
+ /** Diagram type declaration (e.g., "sequenceDiagram", "flowchart TD") */
30
+ export interface DiagramDeclStatement extends StatementBase {
31
+ type: 'diagram-decl';
32
+ diagramType: DiagramType;
33
+ content: string;
34
+ }
35
+ /** Directive (e.g., "%%{init: {...}}%%") */
36
+ export interface DirectiveStatement extends StatementBase {
37
+ type: 'directive';
38
+ content: string;
39
+ }
40
+ /** Participant declaration (sequence diagram) */
41
+ export interface ParticipantStatement extends StatementBase {
42
+ type: 'participant';
43
+ content: string;
44
+ }
45
+ /** Block start with 'end' keyword (critical, alt, loop, etc.) */
46
+ export interface BlockStartStatement extends StatementBase {
47
+ type: 'block-start';
48
+ blockKind: BlockKind;
49
+ label?: string;
50
+ content: string;
51
+ }
52
+ /** Brace block start (state {, class {, namespace {) */
53
+ export interface BraceBlockStartStatement extends StatementBase {
54
+ type: 'brace-block-start';
55
+ blockKind: BraceBlockKind;
56
+ name: string;
57
+ content: string;
58
+ }
59
+ /** Block option (sequence diagram) */
60
+ export interface BlockOptionStatement extends StatementBase {
61
+ type: 'block-option';
62
+ label?: string;
63
+ content: string;
64
+ }
65
+ /** Block else (sequence diagram) */
66
+ export interface BlockElseStatement extends StatementBase {
67
+ type: 'block-else';
68
+ label?: string;
69
+ content: string;
70
+ }
71
+ /** Block end ('end' keyword) */
72
+ export interface BlockEndStatement extends StatementBase {
73
+ type: 'block-end';
74
+ content: string;
75
+ }
76
+ /** Brace block end ('}') */
77
+ export interface BraceBlockEndStatement extends StatementBase {
78
+ type: 'brace-block-end';
79
+ content: string;
80
+ }
81
+ /** Note statement */
82
+ export interface NoteStatement extends StatementBase {
83
+ type: 'note';
84
+ content: string;
85
+ }
86
+ /** Comment (e.g., "%% comment") */
87
+ export interface CommentStatement extends StatementBase {
88
+ type: 'comment';
89
+ content: string;
90
+ }
91
+ /** Generic line (arrows, relationships, nodes, etc.) */
92
+ export interface GenericLineStatement extends StatementBase {
93
+ type: 'generic-line';
94
+ content: string;
95
+ }
96
+ /** Blank line */
97
+ export interface BlankLineStatement extends StatementBase {
98
+ type: 'blank-line';
99
+ content: '';
100
+ }
101
+ /**
102
+ * Union of all statement types.
103
+ */
104
+ export type Statement = DiagramDeclStatement | DirectiveStatement | ParticipantStatement | BlockStartStatement | BraceBlockStartStatement | BlockOptionStatement | BlockElseStatement | BlockEndStatement | BraceBlockEndStatement | NoteStatement | CommentStatement | GenericLineStatement | BlankLineStatement;
105
+ /**
106
+ * Statement type discriminator.
107
+ */
108
+ export type StatementType = Statement['type'];
109
+ /**
110
+ * Parsed diagram AST.
111
+ */
112
+ export interface Diagram {
113
+ type: DiagramType;
114
+ statements: Statement[];
115
+ }
116
+ export { INDENT_SENSITIVE_DIAGRAMS } from './rules.js';
117
+ //# sourceMappingURL=types.d.ts.map
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Type definitions for Mermaid formatter AST and options.
3
+ */
4
+ // Re-export from rules for backwards compatibility
5
+ export { INDENT_SENSITIVE_DIAGRAMS } from './rules.js';
6
+ //# sourceMappingURL=types.js.map
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "mermaid-formatter",
3
+ "version": "0.1.0",
4
+ "description": "A formatter for Mermaid diagram syntax",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "bin": {
17
+ "mmdfmt": "dist/cli.js"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "!dist/**/*.map"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest",
27
+ "lint": "eslint .",
28
+ "lint:fix": "eslint . --fix",
29
+ "format": "prettier --write .",
30
+ "format:check": "prettier --check .",
31
+ "prepublishOnly": "npm run lint && npm run test && npm run build",
32
+ "clean": "rm -rf dist"
33
+ },
34
+ "keywords": [
35
+ "mermaid",
36
+ "formatter",
37
+ "diagram",
38
+ "markdown",
39
+ "sequence-diagram",
40
+ "flowchart",
41
+ "ast"
42
+ ],
43
+ "author": "chenyanchen",
44
+ "license": "MIT",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/chenyanchen/mermaid-formatter.git"
48
+ },
49
+ "bugs": {
50
+ "url": "https://github.com/chenyanchen/mermaid-formatter/issues"
51
+ },
52
+ "homepage": "https://github.com/chenyanchen/mermaid-formatter#readme",
53
+ "engines": {
54
+ "node": ">=18"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "^22.0.0",
58
+ "eslint": "^9.0.0",
59
+ "prettier": "^3.0.0",
60
+ "typescript": "^5.0.0",
61
+ "typescript-eslint": "^8.0.0",
62
+ "vitest": "^2.0.0"
63
+ }
64
+ }