@diagrammo/dgmo 0.0.1 → 0.1.1

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/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.0.1",
3
+ "version": "0.1.1",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
+ "bin": {
8
+ "dgmo": "./dist/cli.cjs"
9
+ },
7
10
  "main": "./dist/index.cjs",
8
11
  "module": "./dist/index.js",
9
12
  "types": "./dist/index.d.ts",
@@ -30,6 +33,7 @@
30
33
  "dev": "tsup --watch"
31
34
  },
32
35
  "dependencies": {
36
+ "@resvg/resvg-js": "^2.6.2",
33
37
  "chart.js": "^4.4.8",
34
38
  "chartjs-plugin-datalabels": "^2.2.0",
35
39
  "d3-array": "^3.2.4",
@@ -37,7 +41,8 @@
37
41
  "d3-scale": "^4.0.2",
38
42
  "d3-selection": "^3.0.0",
39
43
  "d3-shape": "^3.2.0",
40
- "echarts": "^5.6.0"
44
+ "echarts": "^5.6.0",
45
+ "jsdom": "^26.0.0"
41
46
  },
42
47
  "devDependencies": {
43
48
  "@types/d3-array": "^3.2.1",
@@ -45,6 +50,7 @@
45
50
  "@types/d3-scale": "^4.0.8",
46
51
  "@types/d3-selection": "^3.0.11",
47
52
  "@types/d3-shape": "^3.1.7",
53
+ "@types/jsdom": "^21.1.7",
48
54
  "tsup": "^8.5.1",
49
55
  "typescript": "^5.7.3"
50
56
  }
package/src/cli.ts ADDED
@@ -0,0 +1,254 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { resolve, basename, extname } from 'node:path';
3
+ import { JSDOM } from 'jsdom';
4
+ import { Resvg } from '@resvg/resvg-js';
5
+ import { renderD3ForExport } from './d3';
6
+ import { getPalette } from './palettes/registry';
7
+
8
+ const PALETTES = [
9
+ 'nord',
10
+ 'solarized',
11
+ 'catppuccin',
12
+ 'rose-pine',
13
+ 'gruvbox',
14
+ 'tokyo-night',
15
+ 'one-dark',
16
+ 'bold',
17
+ ];
18
+
19
+ const THEMES = ['light', 'dark', 'transparent'] as const;
20
+
21
+ function printHelp(): void {
22
+ console.log(`Usage: dgmo <input> [options]
23
+ cat input.dgmo | dgmo [options]
24
+
25
+ Render a .dgmo file to PNG (default) or SVG.
26
+
27
+ Options:
28
+ -o <file> Output file (default: <input>.png in cwd)
29
+ Format inferred from extension: .svg → SVG, else PNG
30
+ With stdin and no -o, PNG is written to stdout
31
+ --theme <theme> Theme: ${THEMES.join(', ')} (default: light)
32
+ --palette <name> Palette: ${PALETTES.join(', ')} (default: nord)
33
+ --help Show this help
34
+ --version Show version`);
35
+ }
36
+
37
+ function printVersion(): void {
38
+ const pkg = JSON.parse(
39
+ readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8')
40
+ );
41
+ console.log(pkg.version);
42
+ }
43
+
44
+ function parseArgs(argv: string[]): {
45
+ input: string | undefined;
46
+ output: string | undefined;
47
+ theme: (typeof THEMES)[number];
48
+ palette: string;
49
+ help: boolean;
50
+ version: boolean;
51
+ } {
52
+ const result = {
53
+ input: undefined as string | undefined,
54
+ output: undefined as string | undefined,
55
+ theme: 'light' as (typeof THEMES)[number],
56
+ palette: 'nord',
57
+ help: false,
58
+ version: false,
59
+ };
60
+
61
+ const args = argv.slice(2); // skip node + script
62
+ let i = 0;
63
+
64
+ while (i < args.length) {
65
+ const arg = args[i];
66
+
67
+ if (arg === '--help' || arg === '-h') {
68
+ result.help = true;
69
+ i++;
70
+ } else if (arg === '--version' || arg === '-v') {
71
+ result.version = true;
72
+ i++;
73
+ } else if (arg === '-o') {
74
+ result.output = args[++i];
75
+ i++;
76
+ } else if (arg === '--theme') {
77
+ const val = args[++i];
78
+ if (!THEMES.includes(val as (typeof THEMES)[number])) {
79
+ console.error(
80
+ `Error: Invalid theme "${val}". Valid themes: ${THEMES.join(', ')}`
81
+ );
82
+ process.exit(1);
83
+ }
84
+ result.theme = val as (typeof THEMES)[number];
85
+ i++;
86
+ } else if (arg === '--palette') {
87
+ const val = args[++i];
88
+ if (!PALETTES.includes(val)) {
89
+ console.error(
90
+ `Error: Unknown palette "${val}". Valid palettes: ${PALETTES.join(', ')}`
91
+ );
92
+ process.exit(1);
93
+ }
94
+ result.palette = val;
95
+ i++;
96
+ } else if (!result.input) {
97
+ result.input = arg;
98
+ i++;
99
+ } else {
100
+ console.error(`Error: Unexpected argument "${arg}"`);
101
+ process.exit(1);
102
+ }
103
+ }
104
+
105
+ return result;
106
+ }
107
+
108
+ function setupDom(): void {
109
+ const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
110
+ const win = dom.window;
111
+
112
+ // Expose DOM globals needed by d3-selection and renderers
113
+ Object.defineProperty(globalThis, 'document', { value: win.document, configurable: true });
114
+ Object.defineProperty(globalThis, 'window', { value: win, configurable: true });
115
+ Object.defineProperty(globalThis, 'navigator', { value: win.navigator, configurable: true });
116
+ Object.defineProperty(globalThis, 'HTMLElement', { value: win.HTMLElement, configurable: true });
117
+ Object.defineProperty(globalThis, 'SVGElement', { value: win.SVGElement, configurable: true });
118
+ }
119
+
120
+ function inferFormat(outputPath: string | undefined): 'svg' | 'png' {
121
+ if (outputPath && extname(outputPath).toLowerCase() === '.svg') {
122
+ return 'svg';
123
+ }
124
+ return 'png';
125
+ }
126
+
127
+ function svgToPng(svg: string): Buffer {
128
+ const resvg = new Resvg(svg, {
129
+ fitTo: { mode: 'zoom', value: 2 },
130
+ });
131
+ const rendered = resvg.render();
132
+ return rendered.asPng();
133
+ }
134
+
135
+ function noInput(): never {
136
+ const samplePath = resolve('sample.dgmo');
137
+ if (existsSync(samplePath)) {
138
+ console.error('Error: No input file specified');
139
+ console.error(`Try: dgmo ${basename(samplePath)}`);
140
+ process.exit(1);
141
+ }
142
+ writeFileSync(
143
+ samplePath,
144
+ [
145
+ 'chart: sequence',
146
+ 'activations: off',
147
+ '',
148
+ ' Client -> API: POST /login',
149
+ ' API -> Auth: validate credentials',
150
+ ' Auth -> DB: SELECT user',
151
+ ' DB -> Auth: user record',
152
+ ' Auth -> API: JWT token',
153
+ ' API -> Client: 200 OK { token }',
154
+ '',
155
+ ].join('\n'),
156
+ 'utf-8'
157
+ );
158
+ console.error(`Created ${samplePath}`);
159
+ console.error('');
160
+ console.error(' Render it: dgmo sample.dgmo');
161
+ console.error(' As SVG: dgmo sample.dgmo -o sample.svg');
162
+ console.error('');
163
+ console.error(
164
+ 'Edit sample.dgmo to make it your own, or run dgmo --help for all options.'
165
+ );
166
+ process.exit(0);
167
+ }
168
+
169
+ async function main(): Promise<void> {
170
+ const opts = parseArgs(process.argv);
171
+
172
+ if (opts.help) {
173
+ printHelp();
174
+ return;
175
+ }
176
+
177
+ if (opts.version) {
178
+ printVersion();
179
+ return;
180
+ }
181
+
182
+ // Determine input source
183
+ let content: string;
184
+ let inputBasename: string | undefined;
185
+ const stdinIsPiped = !process.stdin.isTTY;
186
+
187
+ if (opts.input) {
188
+ // File argument provided
189
+ const inputPath = resolve(opts.input);
190
+ try {
191
+ content = readFileSync(inputPath, 'utf-8');
192
+ } catch {
193
+ console.error(`Error: Cannot read file "${inputPath}"`);
194
+ process.exit(1);
195
+ }
196
+ // Strip extension for default output name
197
+ const name = basename(opts.input);
198
+ const ext = extname(name);
199
+ inputBasename = ext ? name.slice(0, -ext.length) : name;
200
+ } else if (stdinIsPiped) {
201
+ // Read from stdin
202
+ try {
203
+ content = readFileSync(0, 'utf-8');
204
+ } catch {
205
+ noInput();
206
+ }
207
+ } else {
208
+ noInput();
209
+ }
210
+
211
+ // Set up jsdom before any d3/renderer code runs
212
+ setupDom();
213
+
214
+ const isDark = opts.theme === 'dark';
215
+ const paletteColors = isDark
216
+ ? getPalette(opts.palette).dark
217
+ : getPalette(opts.palette).light;
218
+
219
+ const svg = await renderD3ForExport(content, opts.theme, paletteColors);
220
+
221
+ if (!svg) {
222
+ console.error(
223
+ 'Error: Failed to render diagram. The input may be empty, invalid, or use an unsupported chart type (e.g. Chart.js/ECharts charts require a browser).'
224
+ );
225
+ process.exit(1);
226
+ }
227
+
228
+ // Determine output format and destination
229
+ const format = inferFormat(opts.output);
230
+
231
+ if (opts.output) {
232
+ // Explicit output path
233
+ const outputPath = resolve(opts.output);
234
+ if (format === 'svg') {
235
+ writeFileSync(outputPath, svg, 'utf-8');
236
+ } else {
237
+ writeFileSync(outputPath, svgToPng(svg));
238
+ }
239
+ console.error(`Wrote ${outputPath}`);
240
+ } else if (inputBasename) {
241
+ // File input, no -o → write <basename>.png in cwd
242
+ const outputPath = resolve(`${inputBasename}.png`);
243
+ writeFileSync(outputPath, svgToPng(svg));
244
+ console.error(`Wrote ${outputPath}`);
245
+ } else {
246
+ // Stdin input, no -o → write PNG to stdout
247
+ process.stdout.write(svgToPng(svg));
248
+ }
249
+ }
250
+
251
+ main().catch((err: Error) => {
252
+ console.error(`Error: ${err.message}`);
253
+ process.exit(1);
254
+ });
package/src/index.ts CHANGED
@@ -85,8 +85,14 @@ export {
85
85
  computeActivations,
86
86
  applyPositionOverrides,
87
87
  applyGroupOrdering,
88
+ groupMessagesBySection,
89
+ } from './sequence/renderer';
90
+ export type {
91
+ RenderStep,
92
+ Activation,
93
+ SectionMessageGroup,
94
+ SequenceRenderOptions,
88
95
  } from './sequence/renderer';
89
- export type { RenderStep, Activation } from './sequence/renderer';
90
96
 
91
97
  // ============================================================
92
98
  // Colors & Palettes
@@ -247,8 +247,15 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
247
247
  if (trimmed.startsWith('#') || trimmed.startsWith('//')) continue;
248
248
 
249
249
  // Parse section dividers — "== Label ==" or "== Label(color) =="
250
+ // Close blocks first — sections at indent 0 should not nest inside blocks
250
251
  const sectionMatch = trimmed.match(SECTION_PATTERN);
251
252
  if (sectionMatch) {
253
+ const sectionIndent = measureIndent(raw);
254
+ while (blockStack.length > 0) {
255
+ const top = blockStack[blockStack.length - 1];
256
+ if (sectionIndent > top.indent) break;
257
+ blockStack.pop();
258
+ }
252
259
  const labelRaw = sectionMatch[1].trim();
253
260
  const colorMatch = labelRaw.match(/^(.+?)\((\w+)\)$/);
254
261
  const section: SequenceSection = {