@miragon/wardley-dsl 0.0.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/dist/index.cjs ADDED
@@ -0,0 +1,593 @@
1
+ 'use strict';
2
+
3
+ var wardleySchemaModel = require('@miragon/wardley-schema-model');
4
+
5
+ // src/parser.ts
6
+
7
+ // src/lexer.ts
8
+ var COORDS_RE = /\[\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\]/;
9
+ function parseCoords(line) {
10
+ const m = COORDS_RE.exec(line);
11
+ if (!m) return null;
12
+ const a = Number(m[1]);
13
+ const b = Number(m[2]);
14
+ if (Number.isNaN(a) || Number.isNaN(b)) return null;
15
+ return { a, b };
16
+ }
17
+ function stripCoords(line) {
18
+ return line.replace(COORDS_RE, " ");
19
+ }
20
+ var PAREN_RE = /\(([^)]*)\)/g;
21
+ var METHODS = /* @__PURE__ */ new Set(["build", "buy", "outsource"]);
22
+ function parseDecorators(line) {
23
+ const dec = {};
24
+ let rest = line;
25
+ rest = rest.replace(PAREN_RE, (_full, group) => {
26
+ for (const rawToken of group.split(",")) {
27
+ const token = rawToken.trim().toLowerCase();
28
+ if (token === "market") dec.market = true;
29
+ else if (token === "ecosystem") dec.ecosystem = true;
30
+ else if (token === "inertia") dec.inertia = true;
31
+ else if (METHODS.has(token)) dec.method = token;
32
+ }
33
+ return " ";
34
+ });
35
+ rest = rest.replace(/\binertia\b/i, () => {
36
+ dec.inertia = true;
37
+ return " ";
38
+ });
39
+ return { decorators: dec, rest };
40
+ }
41
+ var COLOR_RE = /\(\s*color\s+(#[0-9a-fA-F]{3,8}|[a-zA-Z][\w-]*)\s*\)/i;
42
+ function parseColor(line) {
43
+ const m = COLOR_RE.exec(line);
44
+ if (!m || !m[1]) return { rest: line };
45
+ return { color: m[1], rest: line.replace(COLOR_RE, " ") };
46
+ }
47
+ var LABEL_OFFSET_RE = /\blabel\s*\[\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\]/i;
48
+ function parseLabelOffset(line) {
49
+ const m = LABEL_OFFSET_RE.exec(line);
50
+ if (!m) return { labelOffset: null, rest: line };
51
+ const dx = Number(m[1]);
52
+ const dy = Number(m[2]);
53
+ const rest = line.replace(LABEL_OFFSET_RE, " ");
54
+ if (Number.isNaN(dx) || Number.isNaN(dy)) return { labelOffset: null, rest };
55
+ return { labelOffset: { dx, dy }, rest };
56
+ }
57
+ function keywordOf(line) {
58
+ const m = /^\s*([A-Za-z][\w-]*)/.exec(line);
59
+ return m ? m[1].toLowerCase() : "";
60
+ }
61
+ function slug(label) {
62
+ return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "x";
63
+ }
64
+
65
+ // src/parser.ts
66
+ var DEP_RE = /^(.+?)\s*->\s*(.+)$/;
67
+ var FLOW_RE = /^(.+?)\s*\+(?:'([^']*)')?(<>|>|<)\s*(.+)$/;
68
+ var NON_LINK_KEYWORDS = /* @__PURE__ */ new Set([
69
+ "title",
70
+ "style",
71
+ "size",
72
+ "evolution",
73
+ "y-axis",
74
+ "annotations",
75
+ "annotation",
76
+ "evolve"
77
+ ]);
78
+ var KNOWN_STYLES = /* @__PURE__ */ new Set(["wardley", "handwritten", "colour", "dark"]);
79
+ function compact(obj) {
80
+ const out = {};
81
+ for (const [k, v] of Object.entries(obj)) if (v !== void 0) out[k] = v;
82
+ return out;
83
+ }
84
+ var IdAllocator = class {
85
+ used = /* @__PURE__ */ new Set();
86
+ alloc(prefix, label) {
87
+ const base = `${prefix}_${slug(label)}`;
88
+ let id = base;
89
+ let i = 2;
90
+ while (this.used.has(id)) id = `${base}_${i++}`;
91
+ this.used.add(id);
92
+ return id;
93
+ }
94
+ };
95
+ function parseDSL(text) {
96
+ const ids = new IdAllocator();
97
+ const nameToId = /* @__PURE__ */ new Map();
98
+ const elements = [];
99
+ const rawPassthrough = [];
100
+ const pendingLinks = [];
101
+ const pendingEvolve = [];
102
+ const pendingPipeline = [];
103
+ let config = { title: "Untitled Map" };
104
+ let annoCounter = 0;
105
+ const register = (name, id) => {
106
+ if (!nameToId.has(name)) nameToId.set(name, id);
107
+ };
108
+ const pushLink = (line, raw) => {
109
+ const semi = line.indexOf(";");
110
+ const core = semi >= 0 ? line.slice(0, semi).trim() : line;
111
+ const linkLabel = semi >= 0 ? line.slice(semi + 1).trim() : "";
112
+ const dep = DEP_RE.exec(core);
113
+ const flow = dep ? null : FLOW_RE.exec(core);
114
+ if (dep) {
115
+ pendingLinks.push({
116
+ left: dep[1].trim(),
117
+ right: dep[2].trim(),
118
+ kind: "dependency",
119
+ ...linkLabel ? { label: linkLabel } : {},
120
+ raw
121
+ });
122
+ return true;
123
+ }
124
+ if (flow) {
125
+ const op = flow[3];
126
+ pendingLinks.push({
127
+ left: flow[1].trim(),
128
+ right: flow[4].trim(),
129
+ kind: "flow",
130
+ bidirectional: op === "<>",
131
+ reverse: op === "<",
132
+ ...flow[2] ? { flowValue: flow[2] } : {},
133
+ ...linkLabel ? { label: linkLabel } : {},
134
+ raw
135
+ });
136
+ return true;
137
+ }
138
+ return false;
139
+ };
140
+ for (const raw of text.split(/\r?\n/)) {
141
+ const line = raw.trim();
142
+ if (!line) continue;
143
+ const kw = keywordOf(line);
144
+ const after = line.slice(kw.length).trim();
145
+ if (!NON_LINK_KEYWORDS.has(kw) && !parseCoords(line) && pushLink(line, raw)) {
146
+ continue;
147
+ }
148
+ switch (kw) {
149
+ case "title":
150
+ config = { ...config, title: after };
151
+ break;
152
+ case "anchor": {
153
+ const node = parseNode(after);
154
+ if (!node) {
155
+ rawPassthrough.push(raw);
156
+ break;
157
+ }
158
+ const id = ids.alloc("anchor", node.name);
159
+ const anchor = compact({
160
+ id,
161
+ elementType: "anchor",
162
+ label: node.name,
163
+ position: { visibility: node.coords.a, evolution: node.coords.b },
164
+ labelOffset: node.labelOffset
165
+ });
166
+ elements.push(anchor);
167
+ register(node.name, id);
168
+ break;
169
+ }
170
+ case "component":
171
+ case "market":
172
+ case "ecosystem": {
173
+ const node = parseNode(after);
174
+ if (!node) {
175
+ rawPassthrough.push(raw);
176
+ break;
177
+ }
178
+ const decorators = mergeLegacy(kw, node.decorators);
179
+ const id = ids.alloc("cmp", node.name);
180
+ const component = compact({
181
+ id,
182
+ elementType: "component",
183
+ label: node.name,
184
+ position: { visibility: node.coords.a, evolution: node.coords.b },
185
+ labelOffset: node.labelOffset,
186
+ decorators: Object.keys(decorators).length ? decorators : void 0
187
+ });
188
+ elements.push(component);
189
+ register(node.name, id);
190
+ break;
191
+ }
192
+ case "note": {
193
+ const { color, rest } = parseColor(after);
194
+ const coords = parseCoords(rest);
195
+ const textPart = stripCoords(rest).trim().replace(/\\n/g, "\n");
196
+ if (!coords) {
197
+ rawPassthrough.push(raw);
198
+ break;
199
+ }
200
+ const id = ids.alloc("note", textPart || "note");
201
+ const note = compact({
202
+ id,
203
+ elementType: "note",
204
+ label: textPart,
205
+ position: { visibility: coords.a, evolution: coords.b },
206
+ color
207
+ });
208
+ elements.push(note);
209
+ break;
210
+ }
211
+ case "pipeline": {
212
+ const coords = parseCoords(after);
213
+ const name = stripCoords(after).trim();
214
+ if (!coords || !name) {
215
+ rawPassthrough.push(raw);
216
+ break;
217
+ }
218
+ pendingPipeline.push({ name, start: coords.a, end: coords.b, raw });
219
+ break;
220
+ }
221
+ case "evolve": {
222
+ const ev = parseEvolve(after);
223
+ if (!ev) {
224
+ rawPassthrough.push(raw);
225
+ break;
226
+ }
227
+ pendingEvolve.push({ ...ev, raw });
228
+ break;
229
+ }
230
+ case "style": {
231
+ const s = after.toLowerCase();
232
+ if (KNOWN_STYLES.has(s)) config = { ...config, style: s };
233
+ else rawPassthrough.push(raw);
234
+ break;
235
+ }
236
+ case "size": {
237
+ const coords = parseCoords(after);
238
+ if (coords) config = { ...config, size: { width: coords.a, height: coords.b } };
239
+ else rawPassthrough.push(raw);
240
+ break;
241
+ }
242
+ case "evolution": {
243
+ const parts = after.split("->").map((s) => s.trim());
244
+ if (parts.length === 4) {
245
+ config = { ...config, evolutionLabels: [parts[0], parts[1], parts[2], parts[3]] };
246
+ } else rawPassthrough.push(raw);
247
+ break;
248
+ }
249
+ case "y-axis": {
250
+ const parts = after.split("->").map((s) => s.trim()).filter(Boolean);
251
+ if (parts.length) config = { ...config, yAxisLabel: parts[0] };
252
+ else rawPassthrough.push(raw);
253
+ break;
254
+ }
255
+ case "annotations": {
256
+ const coords = parseCoords(after);
257
+ if (coords) {
258
+ config = {
259
+ ...config,
260
+ annotationsBoxPosition: { visibility: coords.a, evolution: coords.b }
261
+ };
262
+ } else rawPassthrough.push(raw);
263
+ break;
264
+ }
265
+ case "annotation": {
266
+ const coords = parseCoords(after);
267
+ if (!coords) {
268
+ rawPassthrough.push(raw);
269
+ break;
270
+ }
271
+ const numMatch = /^\s*(\d+)/.exec(after);
272
+ const number = numMatch ? Number(numMatch[1]) : ++annoCounter;
273
+ const text2 = stripCoords(after.replace(/^\s*\d+\s*/, "")).trim();
274
+ const position = { visibility: coords.a, evolution: coords.b };
275
+ const annotation = {
276
+ id: ids.alloc("anno", String(number)),
277
+ elementType: "annotation",
278
+ label: text2,
279
+ position,
280
+ number,
281
+ positions: [position],
282
+ text: text2
283
+ };
284
+ elements.push(annotation);
285
+ break;
286
+ }
287
+ case "pioneers":
288
+ case "settlers":
289
+ case "townplanners": {
290
+ const coords = parseCoords(after);
291
+ const trailing = stripCoords(after).trim().split(/\s+/).filter(Boolean);
292
+ const width = Number(trailing[0]);
293
+ const height = Number(trailing[1]);
294
+ if (!coords || Number.isNaN(width) || Number.isNaN(height)) {
295
+ rawPassthrough.push(raw);
296
+ break;
297
+ }
298
+ const attitude = {
299
+ id: ids.alloc("attitude", kw),
300
+ elementType: "attitude",
301
+ kind: kw,
302
+ label: "",
303
+ position: { visibility: coords.a, evolution: coords.b },
304
+ width,
305
+ height
306
+ };
307
+ elements.push(attitude);
308
+ break;
309
+ }
310
+ case "accelerator":
311
+ case "deaccelerator": {
312
+ const node = parseNode(after);
313
+ if (!node) {
314
+ rawPassthrough.push(raw);
315
+ break;
316
+ }
317
+ const id = ids.alloc("accel", node.name);
318
+ const accelerator = {
319
+ id,
320
+ elementType: "accelerator",
321
+ direction: kw === "deaccelerator" ? "deaccelerate" : "accelerate",
322
+ label: node.name,
323
+ position: { visibility: node.coords.a, evolution: node.coords.b }
324
+ };
325
+ elements.push(accelerator);
326
+ register(node.name, id);
327
+ break;
328
+ }
329
+ case "submap": {
330
+ const node = parseNode(after);
331
+ if (!node) {
332
+ rawPassthrough.push(raw);
333
+ break;
334
+ }
335
+ const id = ids.alloc("submap", node.name);
336
+ const submap = {
337
+ id,
338
+ elementType: "submap",
339
+ label: node.name,
340
+ position: { visibility: node.coords.a, evolution: node.coords.b }
341
+ };
342
+ elements.push(submap);
343
+ register(node.name, id);
344
+ break;
345
+ }
346
+ default: {
347
+ if (!pushLink(line, raw)) rawPassthrough.push(raw);
348
+ }
349
+ }
350
+ }
351
+ for (const ev of pendingEvolve) {
352
+ const id = nameToId.get(ev.name);
353
+ const idx = elements.findIndex((e) => e.id === id);
354
+ if (idx < 0 || elements[idx].elementType !== "component") {
355
+ rawPassthrough.push(ev.raw);
356
+ continue;
357
+ }
358
+ const movement = compact({
359
+ targetEvolution: ev.target,
360
+ newLabel: ev.newLabel,
361
+ method: ev.method
362
+ });
363
+ elements[idx] = { ...elements[idx], movement };
364
+ }
365
+ for (const p of pendingPipeline) {
366
+ const refId = nameToId.get(p.name);
367
+ const ref = elements.find((e) => e.id === refId);
368
+ const visibility = ref ? ref.position.visibility : 0.5;
369
+ const id = ids.alloc("pipeline", p.name);
370
+ const pipeline = {
371
+ id,
372
+ elementType: "pipeline",
373
+ label: p.name,
374
+ position: { visibility, evolution: (p.start + p.end) / 2 },
375
+ evolutionStart: p.start,
376
+ evolutionEnd: p.end,
377
+ childIds: []
378
+ };
379
+ elements.push(pipeline);
380
+ }
381
+ let depN = 0;
382
+ let flowN = 0;
383
+ const edges = [];
384
+ for (const link of pendingLinks) {
385
+ const fromId = nameToId.get(link.left);
386
+ const toId = nameToId.get(link.right);
387
+ if (!fromId || !toId) {
388
+ rawPassthrough.push(link.raw);
389
+ continue;
390
+ }
391
+ if (link.kind === "dependency") {
392
+ edges.push(
393
+ compact({
394
+ id: `dep_${++depN}`,
395
+ edgeType: "dependency",
396
+ from: fromId,
397
+ to: toId,
398
+ label: link.label
399
+ })
400
+ );
401
+ } else {
402
+ const [f, t] = link.reverse ? [toId, fromId] : [fromId, toId];
403
+ edges.push(
404
+ compact({
405
+ id: `flow_${++flowN}`,
406
+ edgeType: "flow",
407
+ from: f,
408
+ to: t,
409
+ bidirectional: link.bidirectional ? true : void 0,
410
+ flowValue: link.flowValue,
411
+ label: link.label
412
+ })
413
+ );
414
+ }
415
+ }
416
+ const map = compact({
417
+ schemaVersion: wardleySchemaModel.CURRENT_SCHEMA_VERSION,
418
+ config,
419
+ elements,
420
+ edges,
421
+ rawPassthrough: rawPassthrough.length ? rawPassthrough : void 0
422
+ });
423
+ return wardleySchemaModel.validateMap(map);
424
+ }
425
+ function parseNode(after) {
426
+ const lo = parseLabelOffset(after);
427
+ const dec = parseDecorators(lo.rest);
428
+ const coords = parseCoords(dec.rest);
429
+ if (!coords) return null;
430
+ const name = stripCoords(dec.rest).trim();
431
+ if (!name) return null;
432
+ return compact({
433
+ name,
434
+ coords,
435
+ decorators: dec.decorators,
436
+ labelOffset: lo.labelOffset ?? void 0
437
+ });
438
+ }
439
+ function mergeLegacy(kw, dec) {
440
+ const merged = { ...dec };
441
+ if (kw === "market") merged["market"] = true;
442
+ if (kw === "ecosystem") merged["ecosystem"] = true;
443
+ return compact(merged);
444
+ }
445
+ function parseEvolve(after) {
446
+ const dec = parseDecorators(after);
447
+ const tokens = dec.rest.trim().split(/\s+/);
448
+ if (tokens.length < 2) return null;
449
+ const last = tokens[tokens.length - 1];
450
+ const target = Number(last);
451
+ if (Number.isNaN(target)) return null;
452
+ const namePart = tokens.slice(0, -1).join(" ").trim();
453
+ if (!namePart) return null;
454
+ const renameIdx = namePart.indexOf("->");
455
+ const name = renameIdx >= 0 ? namePart.slice(0, renameIdx).trim() : namePart;
456
+ const newLabel = renameIdx >= 0 ? namePart.slice(renameIdx + 2).trim() : void 0;
457
+ return compact({
458
+ name,
459
+ newLabel: newLabel || void 0,
460
+ target,
461
+ method: dec.decorators.method
462
+ });
463
+ }
464
+
465
+ // src/serializer.ts
466
+ function r(n) {
467
+ return String(Math.round(n * 1e3) / 1e3);
468
+ }
469
+ var NAMED_TYPES = /* @__PURE__ */ new Set(["anchor", "component", "accelerator", "submap"]);
470
+ function defaultName(type) {
471
+ return type === "anchor" ? "User" : type === "accelerator" ? "Accelerator" : type === "submap" ? "Submap" : "Component";
472
+ }
473
+ function uniqueNames(map) {
474
+ const used = /* @__PURE__ */ new Set();
475
+ const byId = /* @__PURE__ */ new Map();
476
+ for (const el of map.elements) {
477
+ if (!NAMED_TYPES.has(el.elementType)) continue;
478
+ const base = el.label.trim().replace(/->/g, "\u2192") || defaultName(el.elementType);
479
+ let name = base;
480
+ let i = 2;
481
+ while (used.has(name)) name = `${base} ${i++}`;
482
+ used.add(name);
483
+ byId.set(el.id, name);
484
+ }
485
+ return byId;
486
+ }
487
+ function offsetSuffix(lo) {
488
+ return lo ? ` label [${r(lo.dx)}, ${r(lo.dy)}]` : "";
489
+ }
490
+ function decoratorSuffix(dec) {
491
+ if (!dec) return "";
492
+ const paren = [];
493
+ if (dec.market) paren.push("market");
494
+ if (dec.ecosystem) paren.push("ecosystem");
495
+ if (dec.method) paren.push(dec.method);
496
+ let out = "";
497
+ if (paren.length) out += ` (${paren.join(", ")})`;
498
+ if (dec.inertia) out += " inertia";
499
+ return out;
500
+ }
501
+ function serializeDSL(map) {
502
+ const lines = [];
503
+ const names = uniqueNames(map);
504
+ const nameOf = (el) => names.get(el.id) ?? el.label;
505
+ lines.push(`title ${map.config.title}`);
506
+ if (map.config.style) lines.push(`style ${map.config.style}`);
507
+ if (map.config.size)
508
+ lines.push(`size [${r(map.config.size.width)}, ${r(map.config.size.height)}]`);
509
+ if (map.config.evolutionLabels) lines.push(`evolution ${map.config.evolutionLabels.join("->")}`);
510
+ if (map.config.yAxisLabel) lines.push(`y-axis ${map.config.yAxisLabel}`);
511
+ if (map.config.annotationsBoxPosition) {
512
+ const b = map.config.annotationsBoxPosition;
513
+ lines.push(`annotations [${r(b.visibility)}, ${r(b.evolution)}]`);
514
+ }
515
+ const evolveLines = [];
516
+ for (const el of map.elements) {
517
+ lines.push(elementLine(el, nameOf(el)));
518
+ if (el.elementType === "component" && el.movement) {
519
+ evolveLines.push(evolveLine(el, nameOf(el)));
520
+ }
521
+ }
522
+ for (const line of evolveLines) lines.push(line);
523
+ for (const edge of map.edges) {
524
+ const from = names.get(edge.from) ?? edge.from;
525
+ const to = names.get(edge.to) ?? edge.to;
526
+ const annotation = edge.label ? `; ${edge.label}` : "";
527
+ if (edge.edgeType === "dependency") {
528
+ lines.push(`${from} -> ${to}${annotation}`);
529
+ } else {
530
+ const op = edge.flowValue ? `+'${edge.flowValue}'${edge.bidirectional ? "<>" : ">"}` : edge.bidirectional ? "+<>" : "+>";
531
+ lines.push(`${from} ${op} ${to}${annotation}`);
532
+ }
533
+ }
534
+ if (map.rawPassthrough) {
535
+ const emitted = /* @__PURE__ */ new Set();
536
+ if (map.config.evolutionLabels) emitted.add("evolution");
537
+ if (map.config.yAxisLabel) emitted.add("y-axis");
538
+ for (const raw of map.rawPassthrough) {
539
+ if (emitted.has(raw.trim().split(/\s+/)[0])) continue;
540
+ lines.push(raw);
541
+ }
542
+ }
543
+ return lines.join("\n") + "\n";
544
+ }
545
+ function elementLine(el, name) {
546
+ const p = el.position;
547
+ switch (el.elementType) {
548
+ case "anchor":
549
+ return `anchor ${name} [${r(p.visibility)}, ${r(p.evolution)}]${offsetSuffix(el.labelOffset)}`;
550
+ case "component":
551
+ return `component ${name} [${r(p.visibility)}, ${r(p.evolution)}]${decoratorSuffix(el.decorators)}${offsetSuffix(el.labelOffset)}`;
552
+ case "note":
553
+ return `note ${name.replace(/\n/g, "\\n")} [${r(p.visibility)}, ${r(p.evolution)}]${el.color ? ` (color ${el.color})` : ""}`;
554
+ case "pipeline":
555
+ return `pipeline ${name} [${r(el.evolutionStart)}, ${r(el.evolutionEnd)}]`;
556
+ case "submap":
557
+ return `submap ${name} [${r(p.visibility)}, ${r(p.evolution)}]`;
558
+ case "annotation":
559
+ return `annotation ${el.number} [${r(p.visibility)}, ${r(p.evolution)}] ${el.text}`;
560
+ case "accelerator":
561
+ return `${el.direction === "deaccelerate" ? "deaccelerator" : "accelerator"} ${name} [${r(p.visibility)}, ${r(p.evolution)}]`;
562
+ case "attitude":
563
+ return `${el.kind} [${r(p.visibility)}, ${r(p.evolution)}] ${r(el.width)} ${r(el.height)}`;
564
+ }
565
+ }
566
+ function evolveLine(el, name) {
567
+ const mv = el.movement;
568
+ const label = mv.newLabel ? `${name}->${mv.newLabel}` : name;
569
+ const method = mv.method ? ` (${mv.method})` : "";
570
+ return `evolve ${label} ${r(mv.targetEvolution)}${method}`;
571
+ }
572
+
573
+ Object.defineProperty(exports, "loadMap", {
574
+ enumerable: true,
575
+ get: function () { return wardleySchemaModel.loadMap; }
576
+ });
577
+ Object.defineProperty(exports, "mapFromJSON", {
578
+ enumerable: true,
579
+ get: function () { return wardleySchemaModel.parseMapJSON; }
580
+ });
581
+ Object.defineProperty(exports, "mapToJSON", {
582
+ enumerable: true,
583
+ get: function () { return wardleySchemaModel.serializeMap; }
584
+ });
585
+ exports.keywordOf = keywordOf;
586
+ exports.parseCoords = parseCoords;
587
+ exports.parseDSL = parseDSL;
588
+ exports.parseDecorators = parseDecorators;
589
+ exports.parseLabelOffset = parseLabelOffset;
590
+ exports.serializeDSL = serializeDSL;
591
+ exports.slug = slug;
592
+ //# sourceMappingURL=index.cjs.map
593
+ //# sourceMappingURL=index.cjs.map