@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/README.md +41 -0
- package/dist/cli.cjs +5250 -0
- package/dist/index.cjs +214 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -2
- package/dist/index.d.ts +15 -2
- package/dist/index.js +213 -37
- package/dist/index.js.map +1 -1
- package/package.json +8 -2
- package/src/cli.ts +254 -0
- package/src/index.ts +7 -1
- package/src/sequence/parser.ts +7 -0
- package/src/sequence/renderer.ts +339 -99
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diagrammo/dgmo",
|
|
3
|
-
"version": "0.
|
|
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
|
package/src/sequence/parser.ts
CHANGED
|
@@ -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 = {
|