@miragon/wardley-dsl 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/dist/index.cjs CHANGED
@@ -17,7 +17,65 @@ function parseCoords(line) {
17
17
  function stripCoords(line) {
18
18
  return line.replace(COORDS_RE, " ");
19
19
  }
20
- var PAREN_RE = /\(([^)]*)\)/g;
20
+ function splitAtCoords(line) {
21
+ const m = COORDS_RE.exec(line);
22
+ if (!m) return null;
23
+ const a = Number(m[1]);
24
+ const b = Number(m[2]);
25
+ if (Number.isNaN(a) || Number.isNaN(b)) return null;
26
+ return {
27
+ name: line.slice(0, m.index).trim(),
28
+ coords: { a, b },
29
+ suffix: line.slice(m.index + m[0].length)
30
+ };
31
+ }
32
+ var COORDS4_RE = /\[\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\]/;
33
+ function parseCoords4(line) {
34
+ const m = COORDS4_RE.exec(line);
35
+ if (!m) return null;
36
+ const vals = [m[1], m[2], m[3], m[4]].map(Number);
37
+ if (vals.some(Number.isNaN)) return null;
38
+ return { a: vals[0], b: vals[1], c: vals[2], d: vals[3] };
39
+ }
40
+ var MULTI_COORDS_RE = /\[\s{0,8}(\[[^[\]]*\](?:\s{0,8},\s{0,8}\[[^[\]]*\])+)\s{0,8}\]/;
41
+ function parseMultiCoords(line) {
42
+ const m = MULTI_COORDS_RE.exec(line);
43
+ if (!m) return null;
44
+ const tuples = [];
45
+ const inner = /\[\s{0,8}([-\d.]+)\s{0,8},\s{0,8}([-\d.]+)\s{0,8}\]/g;
46
+ let t;
47
+ while (t = inner.exec(m[1])) {
48
+ const a = Number(t[1]);
49
+ const b = Number(t[2]);
50
+ if (!Number.isNaN(a) && !Number.isNaN(b)) tuples.push({ a, b });
51
+ }
52
+ if (!tuples.length) return null;
53
+ return { tuples, rest: line.replace(MULTI_COORDS_RE, " ") };
54
+ }
55
+ function splitLineComment(line) {
56
+ let inQuote = false;
57
+ for (let i = 0; i < line.length - 1; i++) {
58
+ const ch = line[i];
59
+ if (ch === "'") inQuote = !inQuote;
60
+ else if (!inQuote && ch === "/" && line[i + 1] === "/" && line[i - 1] !== ":") {
61
+ return { code: line.slice(0, i), comment: line.slice(i) };
62
+ }
63
+ }
64
+ return { code: line, comment: null };
65
+ }
66
+ function indexOfOutsideQuotes(line, needle) {
67
+ let inQuote = false;
68
+ for (let i = 0; i + needle.length <= line.length; i++) {
69
+ const ch = line[i];
70
+ if (ch === "'") {
71
+ inQuote = !inQuote;
72
+ continue;
73
+ }
74
+ if (!inQuote && line.startsWith(needle, i)) return i;
75
+ }
76
+ return -1;
77
+ }
78
+ var PAREN_RE = /\(([^()]*)\)/g;
21
79
  var METHODS = /* @__PURE__ */ new Set(["build", "buy", "outsource"]);
22
80
  function parseDecorators(line) {
23
81
  const dec = {};
@@ -54,17 +112,47 @@ function parseLabelOffset(line) {
54
112
  if (Number.isNaN(dx) || Number.isNaN(dy)) return { labelOffset: null, rest };
55
113
  return { labelOffset: { dx, dy }, rest };
56
114
  }
115
+ var URL_REF_RE = /\burl\s{0,8}\(([^()]*)\)/i;
116
+ function parseUrlRef(line) {
117
+ const m = URL_REF_RE.exec(line);
118
+ if (!m) return { urlRef: null, rest: line };
119
+ return { urlRef: m[1].trim() || null, rest: line.replace(URL_REF_RE, " ") };
120
+ }
57
121
  function keywordOf(line) {
58
122
  const m = /^\s*([A-Za-z][\w-]*)/.exec(line);
59
123
  return m ? m[1].toLowerCase() : "";
60
124
  }
61
125
  function slug(label) {
62
- return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "x";
126
+ return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_/, "").replace(/_$/, "") || "x";
63
127
  }
64
128
 
65
129
  // src/parser.ts
66
- var DEP_RE = /^(.+?)\s*->\s*(.+)$/;
67
- var FLOW_RE = /^(.+?)\s*\+(?:'([^']*)')?(<>|>|<)\s*(.+)$/;
130
+ function splitDependency(core) {
131
+ const arrow = core.indexOf("->");
132
+ if (arrow <= 0) return null;
133
+ const left = core.slice(0, arrow).trim();
134
+ const right = core.slice(arrow + 2).trim();
135
+ return left && right ? { left, right } : null;
136
+ }
137
+ function splitFlow(core) {
138
+ for (let i = 1; i < core.length; i++) {
139
+ if (core[i] !== "+") continue;
140
+ let j = i + 1;
141
+ let value;
142
+ if (core[j] === "'") {
143
+ const end = core.indexOf("'", j + 1);
144
+ if (end < 0) continue;
145
+ value = core.slice(j + 1, end);
146
+ j = end + 1;
147
+ }
148
+ const op = core.startsWith("<>", j) ? "<>" : core[j] === ">" || core[j] === "<" ? core[j] : null;
149
+ if (!op) continue;
150
+ const left = core.slice(0, i).trim();
151
+ const right = core.slice(j + op.length).trim();
152
+ if (left && right) return { left, right, op, ...value ? { value } : {} };
153
+ }
154
+ return null;
155
+ }
68
156
  var NON_LINK_KEYWORDS = /* @__PURE__ */ new Set([
69
157
  "title",
70
158
  "style",
@@ -73,7 +161,8 @@ var NON_LINK_KEYWORDS = /* @__PURE__ */ new Set([
73
161
  "y-axis",
74
162
  "annotations",
75
163
  "annotation",
76
- "evolve"
164
+ "evolve",
165
+ "line"
77
166
  ]);
78
167
  var KNOWN_STYLES = /* @__PURE__ */ new Set(["wardley", "handwritten", "colour", "dark"]);
79
168
  function compact(obj) {
@@ -93,6 +182,10 @@ var IdAllocator = class {
93
182
  }
94
183
  };
95
184
  function parseDSL(text) {
185
+ return parseDSLWithDiagnostics(text).map;
186
+ }
187
+ function parseDSLWithDiagnostics(text) {
188
+ const diagnostics = [];
96
189
  const ids = new IdAllocator();
97
190
  const nameToId = /* @__PURE__ */ new Map();
98
191
  const elements = [];
@@ -100,49 +193,130 @@ function parseDSL(text) {
100
193
  const pendingLinks = [];
101
194
  const pendingEvolve = [];
102
195
  const pendingPipeline = [];
196
+ const pendingMethod = [];
103
197
  let config = { title: "Untitled Map" };
104
198
  let annoCounter = 0;
199
+ let inBlockComment = false;
200
+ const urlDefs = /* @__PURE__ */ new Map();
201
+ const pendingUrlRefs = [];
202
+ let lastPipeline = null;
203
+ let blockPipeline = null;
105
204
  const register = (name, id) => {
106
205
  if (!nameToId.has(name)) nameToId.set(name, id);
107
206
  };
108
- const pushLink = (line, raw) => {
207
+ let lineNo = 0;
208
+ let currentLine = "";
209
+ const pushLink = (line) => {
109
210
  const semi = line.indexOf(";");
110
211
  const core = semi >= 0 ? line.slice(0, semi).trim() : line;
111
212
  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);
213
+ const dep = splitDependency(core);
214
+ const flow = dep ? null : splitFlow(core);
114
215
  if (dep) {
115
216
  pendingLinks.push({
116
- left: dep[1].trim(),
117
- right: dep[2].trim(),
217
+ left: dep.left,
218
+ right: dep.right,
118
219
  kind: "dependency",
119
220
  ...linkLabel ? { label: linkLabel } : {},
120
- raw
221
+ raw: line,
222
+ lineNo
121
223
  });
122
224
  return true;
123
225
  }
124
226
  if (flow) {
125
- const op = flow[3];
126
227
  pendingLinks.push({
127
- left: flow[1].trim(),
128
- right: flow[4].trim(),
228
+ left: flow.left,
229
+ right: flow.right,
129
230
  kind: "flow",
130
- bidirectional: op === "<>",
131
- reverse: op === "<",
132
- ...flow[2] ? { flowValue: flow[2] } : {},
231
+ bidirectional: flow.op === "<>",
232
+ reverse: flow.op === "<",
233
+ ...flow.value ? { flowValue: flow.value } : {},
133
234
  ...linkLabel ? { label: linkLabel } : {},
134
- raw
235
+ raw: line,
236
+ lineNo
135
237
  });
136
238
  return true;
137
239
  }
138
240
  return false;
139
241
  };
140
- for (const raw of text.split(/\r?\n/)) {
141
- const line = raw.trim();
242
+ const diag = (message, atLine = lineNo, text_ = currentLine) => diagnostics.push({ line: atLine, message, text: text_ });
243
+ const failed = (l) => {
244
+ rawPassthrough.push(l);
245
+ diag("Line could not be interpreted (kept losslessly in rawPassthrough)");
246
+ };
247
+ const clampDiag = (n) => {
248
+ if (n < 0 || n > 1) diag(`Coordinate ${n} is outside [0,1] and was clamped`);
249
+ return n < 0 ? 0 : n > 1 ? 1 : n;
250
+ };
251
+ const pos = (visibility, evolution) => ({
252
+ visibility: clampDiag(visibility),
253
+ evolution: clampDiag(evolution)
254
+ });
255
+ const sourceLines = text.split(/\r?\n/);
256
+ for (let i = 0; i < sourceLines.length; i++) {
257
+ const raw = sourceLines[i];
258
+ lineNo = i + 1;
259
+ currentLine = raw.trim();
260
+ let working = raw;
261
+ if (inBlockComment) {
262
+ const close = working.indexOf("*/");
263
+ if (close < 0) {
264
+ rawPassthrough.push(raw);
265
+ continue;
266
+ }
267
+ rawPassthrough.push(working.slice(0, close + 2));
268
+ working = working.slice(close + 2);
269
+ inBlockComment = false;
270
+ }
271
+ if (keywordOf(working) !== "url") {
272
+ let open = indexOfOutsideQuotes(working, "/*");
273
+ while (open >= 0) {
274
+ const close = working.indexOf("*/", open + 2);
275
+ if (close < 0) {
276
+ rawPassthrough.push(working.slice(open));
277
+ working = working.slice(0, open);
278
+ inBlockComment = true;
279
+ break;
280
+ }
281
+ rawPassthrough.push(working.slice(open, close + 2));
282
+ working = `${working.slice(0, open)} ${working.slice(close + 2)}`;
283
+ open = indexOfOutsideQuotes(working, "/*");
284
+ }
285
+ const { code, comment } = splitLineComment(working);
286
+ if (comment !== null) rawPassthrough.push(comment);
287
+ working = code;
288
+ }
289
+ const line = working.trim();
142
290
  if (!line) continue;
291
+ if (blockPipeline) {
292
+ if (line === "}") {
293
+ blockPipeline = null;
294
+ continue;
295
+ }
296
+ const childKw = keywordOf(line);
297
+ if (childKw === "component") {
298
+ const child = parseBlockChild(line.slice(childKw.length).trim());
299
+ if (child) {
300
+ blockPipeline.children.push(child);
301
+ continue;
302
+ }
303
+ }
304
+ rawPassthrough.push(line);
305
+ continue;
306
+ }
307
+ if (line.startsWith("{")) {
308
+ if (lastPipeline) {
309
+ blockPipeline = lastPipeline;
310
+ lastPipeline = null;
311
+ continue;
312
+ }
313
+ rawPassthrough.push(line);
314
+ continue;
315
+ }
143
316
  const kw = keywordOf(line);
144
317
  const after = line.slice(kw.length).trim();
145
- if (!NON_LINK_KEYWORDS.has(kw) && !parseCoords(line) && pushLink(line, raw)) {
318
+ if (kw !== "pipeline") lastPipeline = null;
319
+ if (!NON_LINK_KEYWORDS.has(kw) && !parseCoords(line) && pushLink(line)) {
146
320
  continue;
147
321
  }
148
322
  switch (kw) {
@@ -152,7 +326,7 @@ function parseDSL(text) {
152
326
  case "anchor": {
153
327
  const node = parseNode(after);
154
328
  if (!node) {
155
- rawPassthrough.push(raw);
329
+ failed(line);
156
330
  break;
157
331
  }
158
332
  const id = ids.alloc("anchor", node.name);
@@ -160,8 +334,9 @@ function parseDSL(text) {
160
334
  id,
161
335
  elementType: "anchor",
162
336
  label: node.name,
163
- position: { visibility: node.coords.a, evolution: node.coords.b },
164
- labelOffset: node.labelOffset
337
+ position: pos(node.coords.a, node.coords.b),
338
+ labelOffset: node.labelOffset,
339
+ color: node.color
165
340
  });
166
341
  elements.push(anchor);
167
342
  register(node.name, id);
@@ -172,7 +347,7 @@ function parseDSL(text) {
172
347
  case "ecosystem": {
173
348
  const node = parseNode(after);
174
349
  if (!node) {
175
- rawPassthrough.push(raw);
350
+ failed(line);
176
351
  break;
177
352
  }
178
353
  const decorators = mergeLegacy(kw, node.decorators);
@@ -181,11 +356,13 @@ function parseDSL(text) {
181
356
  id,
182
357
  elementType: "component",
183
358
  label: node.name,
184
- position: { visibility: node.coords.a, evolution: node.coords.b },
359
+ position: pos(node.coords.a, node.coords.b),
185
360
  labelOffset: node.labelOffset,
186
- decorators: Object.keys(decorators).length ? decorators : void 0
361
+ decorators: Object.keys(decorators).length ? decorators : void 0,
362
+ color: node.color
187
363
  });
188
364
  elements.push(component);
365
+ if (node.urlRef) pendingUrlRefs.push({ index: elements.length - 1, ref: node.urlRef });
189
366
  register(node.name, id);
190
367
  break;
191
368
  }
@@ -194,7 +371,7 @@ function parseDSL(text) {
194
371
  const coords = parseCoords(rest);
195
372
  const textPart = stripCoords(rest).trim().replace(/\\n/g, "\n");
196
373
  if (!coords) {
197
- rawPassthrough.push(raw);
374
+ failed(line);
198
375
  break;
199
376
  }
200
377
  const id = ids.alloc("note", textPart || "note");
@@ -202,41 +379,60 @@ function parseDSL(text) {
202
379
  id,
203
380
  elementType: "note",
204
381
  label: textPart,
205
- position: { visibility: coords.a, evolution: coords.b },
382
+ position: pos(coords.a, coords.b),
206
383
  color
207
384
  });
208
385
  elements.push(note);
209
386
  break;
210
387
  }
211
388
  case "pipeline": {
212
- const coords = parseCoords(after);
213
- const name = stripCoords(after).trim();
214
- if (!coords || !name) {
215
- rawPassthrough.push(raw);
389
+ let body = after;
390
+ let opensBlock = false;
391
+ if (body.endsWith("{")) {
392
+ opensBlock = true;
393
+ body = body.slice(0, -1).trim();
394
+ }
395
+ const col = parseColor(body);
396
+ const yMatch = /\(\s{0,8}y\s{1,8}([\d.]+)\s{0,8}\)/.exec(col.rest);
397
+ const bodyRest = yMatch ? col.rest.replace(yMatch[0], " ") : col.rest;
398
+ const coords = parseCoords(bodyRest);
399
+ const name = stripCoords(bodyRest).trim();
400
+ if (!name) {
401
+ failed(line);
216
402
  break;
217
403
  }
218
- pendingPipeline.push({ name, start: coords.a, end: coords.b, raw });
404
+ const pending = {
405
+ name,
406
+ ...coords ? { start: coords.a, end: coords.b } : {},
407
+ ...yMatch ? { visibility: clamp01(Number(yMatch[1])) } : {},
408
+ ...col.color ? { color: col.color } : {},
409
+ raw: line,
410
+ children: []
411
+ };
412
+ pendingPipeline.push(pending);
413
+ if (opensBlock) blockPipeline = pending;
414
+ else lastPipeline = pending;
219
415
  break;
220
416
  }
221
417
  case "evolve": {
222
418
  const ev = parseEvolve(after);
223
419
  if (!ev) {
224
- rawPassthrough.push(raw);
420
+ failed(line);
225
421
  break;
226
422
  }
227
- pendingEvolve.push({ ...ev, raw });
423
+ pendingEvolve.push({ ...ev, target: clampDiag(ev.target), raw: line, lineNo });
228
424
  break;
229
425
  }
230
426
  case "style": {
231
427
  const s = after.toLowerCase();
232
428
  if (KNOWN_STYLES.has(s)) config = { ...config, style: s };
233
- else rawPassthrough.push(raw);
429
+ else failed(line);
234
430
  break;
235
431
  }
236
432
  case "size": {
237
433
  const coords = parseCoords(after);
238
434
  if (coords) config = { ...config, size: { width: coords.a, height: coords.b } };
239
- else rawPassthrough.push(raw);
435
+ else failed(line);
240
436
  break;
241
437
  }
242
438
  case "evolution": {
@@ -248,8 +444,13 @@ function parseDSL(text) {
248
444
  }
249
445
  case "y-axis": {
250
446
  const parts = after.split("->").map((s) => s.trim()).filter(Boolean);
251
- if (parts.length) config = { ...config, yAxisLabel: parts[0] };
252
- else rawPassthrough.push(raw);
447
+ if (parts.length) {
448
+ config = {
449
+ ...config,
450
+ yAxisLabel: parts[0],
451
+ ...parts.length >= 3 ? { yAxisEndLabels: [parts[1], parts[2]] } : {}
452
+ };
453
+ } else failed(line);
253
454
  break;
254
455
  }
255
456
  case "annotations": {
@@ -257,61 +458,122 @@ function parseDSL(text) {
257
458
  if (coords) {
258
459
  config = {
259
460
  ...config,
260
- annotationsBoxPosition: { visibility: coords.a, evolution: coords.b }
461
+ annotationsBoxPosition: pos(coords.a, coords.b)
261
462
  };
262
463
  } else rawPassthrough.push(raw);
263
464
  break;
264
465
  }
265
466
  case "annotation": {
266
- const coords = parseCoords(after);
267
- if (!coords) {
268
- rawPassthrough.push(raw);
269
- break;
270
- }
271
467
  const numMatch = /^\s*(\d+)/.exec(after);
272
468
  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 };
469
+ const colA = parseColor(after.replace(/^\s*\d+\s*/, ""));
470
+ const afterNum = colA.rest;
471
+ const multi = parseMultiCoords(afterNum);
472
+ let positions;
473
+ let text2;
474
+ if (multi) {
475
+ positions = multi.tuples.map((t) => pos(t.a, t.b));
476
+ text2 = multi.rest.trim();
477
+ } else {
478
+ const coords = parseCoords(afterNum);
479
+ if (!coords) {
480
+ rawPassthrough.push(line);
481
+ break;
482
+ }
483
+ positions = [pos(coords.a, coords.b)];
484
+ text2 = stripCoords(afterNum).trim();
485
+ }
275
486
  const annotation = {
276
487
  id: ids.alloc("anno", String(number)),
277
488
  elementType: "annotation",
278
489
  label: text2,
279
- position,
490
+ position: positions[0],
280
491
  number,
281
- positions: [position],
282
- text: text2
492
+ positions,
493
+ text: text2,
494
+ ...colA.color ? { color: colA.color } : {}
283
495
  };
284
496
  elements.push(annotation);
285
497
  break;
286
498
  }
499
+ case "line": {
500
+ const col = parseColor(after);
501
+ const multi = parseMultiCoords(col.rest);
502
+ if (!multi || multi.tuples.length < 2) {
503
+ failed(line);
504
+ break;
505
+ }
506
+ const flags = multi.rest.toLowerCase();
507
+ const strokeStyle = flags.includes("(dashed)") ? "dashed" : flags.includes("(dotted)") ? "dotted" : void 0;
508
+ const points = multi.tuples.map((t) => pos(t.a, t.b));
509
+ const drawing = {
510
+ id: ids.alloc("draw", "line"),
511
+ elementType: "drawing",
512
+ label: "",
513
+ position: points[0],
514
+ points,
515
+ ...flags.includes("(closed)") ? { closed: true } : {},
516
+ ...strokeStyle ? { strokeStyle } : {},
517
+ ...col.color ? { color: col.color } : {}
518
+ };
519
+ elements.push(drawing);
520
+ break;
521
+ }
287
522
  case "pioneers":
288
523
  case "settlers":
289
524
  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);
525
+ const col = parseColor(after);
526
+ const four = parseCoords4(col.rest);
527
+ if (!four) {
528
+ failed(line);
296
529
  break;
297
530
  }
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);
531
+ elements.push(
532
+ makeAttitude(ids, kw, four.a, four.b, four.c, four.d, col.color)
533
+ );
534
+ break;
535
+ }
536
+ case "url": {
537
+ const open = after.indexOf("[");
538
+ const close = after.lastIndexOf("]");
539
+ const name = open > 0 ? after.slice(0, open).trim() : "";
540
+ const address = open > 0 && close > open ? after.slice(open + 1, close).trim() : "";
541
+ if (!name || !address || after.slice(close + 1).trim() !== "") {
542
+ failed(line);
543
+ break;
544
+ }
545
+ urlDefs.set(name, address);
546
+ break;
547
+ }
548
+ case "build":
549
+ case "buy":
550
+ case "outsource": {
551
+ const node = parseNode(after);
552
+ if (node) {
553
+ const id = ids.alloc("cmp", node.name);
554
+ const component = compact({
555
+ id,
556
+ elementType: "component",
557
+ label: node.name,
558
+ position: pos(node.coords.a, node.coords.b),
559
+ labelOffset: node.labelOffset,
560
+ decorators: { ...node.decorators, method: kw },
561
+ color: node.color
562
+ });
563
+ elements.push(component);
564
+ register(node.name, id);
565
+ } else if (after.trim()) {
566
+ pendingMethod.push({ name: after.trim(), method: kw, raw: line, lineNo });
567
+ } else {
568
+ rawPassthrough.push(line);
569
+ }
308
570
  break;
309
571
  }
310
572
  case "accelerator":
311
573
  case "deaccelerator": {
312
574
  const node = parseNode(after);
313
575
  if (!node) {
314
- rawPassthrough.push(raw);
576
+ failed(line);
315
577
  break;
316
578
  }
317
579
  const id = ids.alloc("accel", node.name);
@@ -320,7 +582,8 @@ function parseDSL(text) {
320
582
  elementType: "accelerator",
321
583
  direction: kw === "deaccelerator" ? "deaccelerate" : "accelerate",
322
584
  label: node.name,
323
- position: { visibility: node.coords.a, evolution: node.coords.b }
585
+ position: pos(node.coords.a, node.coords.b),
586
+ ...node.color ? { color: node.color } : {}
324
587
  };
325
588
  elements.push(accelerator);
326
589
  register(node.name, id);
@@ -329,7 +592,7 @@ function parseDSL(text) {
329
592
  case "submap": {
330
593
  const node = parseNode(after);
331
594
  if (!node) {
332
- rawPassthrough.push(raw);
595
+ failed(line);
333
596
  break;
334
597
  }
335
598
  const id = ids.alloc("submap", node.name);
@@ -337,46 +600,107 @@ function parseDSL(text) {
337
600
  id,
338
601
  elementType: "submap",
339
602
  label: node.name,
340
- position: { visibility: node.coords.a, evolution: node.coords.b }
603
+ position: pos(node.coords.a, node.coords.b),
604
+ ...node.color ? { color: node.color } : {}
341
605
  };
342
606
  elements.push(submap);
607
+ if (node.urlRef) pendingUrlRefs.push({ index: elements.length - 1, ref: node.urlRef });
343
608
  register(node.name, id);
344
609
  break;
345
610
  }
346
611
  default: {
347
- if (!pushLink(line, raw)) rawPassthrough.push(raw);
612
+ if (!pushLink(line)) rawPassthrough.push(line);
348
613
  }
349
614
  }
350
615
  }
616
+ for (const p of pendingPipeline) {
617
+ const refId = nameToId.get(p.name);
618
+ const ref = elements.find((e) => e.id === refId);
619
+ const visibility = p.visibility ?? (ref ? ref.position.visibility : 0.5);
620
+ const id = ids.alloc("pipeline", p.name);
621
+ register(p.name, id);
622
+ const childIds = [];
623
+ const childElements = [];
624
+ for (const c of p.children) {
625
+ const cid = ids.alloc("cmp", c.name);
626
+ childElements.push(
627
+ compact({
628
+ id: cid,
629
+ elementType: "component",
630
+ label: c.name,
631
+ position: { visibility, evolution: clamp01(c.maturity) },
632
+ labelOffset: c.labelOffset,
633
+ decorators: Object.keys(c.decorators).length ? c.decorators : void 0,
634
+ pipelineId: id,
635
+ color: c.color
636
+ })
637
+ );
638
+ register(c.name, cid);
639
+ childIds.push(cid);
640
+ }
641
+ let start = p.start ?? (p.children.length ? Math.min(...p.children.map((c) => c.maturity)) : NaN);
642
+ let end = p.end ?? (p.children.length ? Math.max(...p.children.map((c) => c.maturity)) : NaN);
643
+ if (Number.isNaN(start) || Number.isNaN(end)) {
644
+ rawPassthrough.push(p.raw);
645
+ continue;
646
+ }
647
+ start = clamp01(start);
648
+ end = clamp01(end);
649
+ if (end <= start) end = Math.min(1, start + 0.05);
650
+ const pipeline = {
651
+ id,
652
+ elementType: "pipeline",
653
+ label: p.name,
654
+ position: { visibility, evolution: (start + end) / 2 },
655
+ evolutionStart: start,
656
+ evolutionEnd: end,
657
+ childIds,
658
+ ...p.color ? { color: p.color } : {}
659
+ };
660
+ elements.push(pipeline, ...childElements);
661
+ }
351
662
  for (const ev of pendingEvolve) {
352
663
  const id = nameToId.get(ev.name);
353
664
  const idx = elements.findIndex((e) => e.id === id);
354
665
  if (idx < 0 || elements[idx].elementType !== "component") {
355
666
  rawPassthrough.push(ev.raw);
667
+ diag(`evolve: component "${ev.name}" not found`, ev.lineNo, ev.raw);
356
668
  continue;
357
669
  }
358
670
  const movement = compact({
359
671
  targetEvolution: ev.target,
360
672
  newLabel: ev.newLabel,
361
- method: ev.method
673
+ method: ev.method,
674
+ labelOffset: ev.labelOffset
362
675
  });
363
676
  elements[idx] = { ...elements[idx], movement };
364
677
  }
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);
678
+ for (const pm of pendingMethod) {
679
+ const id = nameToId.get(pm.name);
680
+ const idx = elements.findIndex((e) => e.id === id);
681
+ const el = idx >= 0 ? elements[idx] : void 0;
682
+ if (!el || el.elementType !== "component") {
683
+ rawPassthrough.push(pm.raw);
684
+ diag(`${pm.method}: component "${pm.name}" not found`, pm.lineNo, pm.raw);
685
+ continue;
686
+ }
687
+ elements[idx] = { ...el, decorators: { ...el.decorators, method: pm.method } };
688
+ }
689
+ const usedUrlDefs = /* @__PURE__ */ new Set();
690
+ for (const { index, ref } of pendingUrlRefs) {
691
+ const fromDef = urlDefs.get(ref);
692
+ if (fromDef) usedUrlDefs.add(ref);
693
+ const address = fromDef ?? (/^[a-z][\w+.-]*:\/\//i.test(ref) ? ref : void 0);
694
+ if (!address) continue;
695
+ const el = elements[index];
696
+ if (el.elementType === "component") {
697
+ elements[index] = { ...el, url: address };
698
+ } else if (el.elementType === "submap") {
699
+ elements[index] = { ...el, urlRef: address };
700
+ }
701
+ }
702
+ for (const [name, address] of urlDefs) {
703
+ if (!usedUrlDefs.has(name)) rawPassthrough.push(`url ${name} [${address}]`);
380
704
  }
381
705
  let depN = 0;
382
706
  let flowN = 0;
@@ -386,6 +710,11 @@ function parseDSL(text) {
386
710
  const toId = nameToId.get(link.right);
387
711
  if (!fromId || !toId) {
388
712
  rawPassthrough.push(link.raw);
713
+ diag(
714
+ `Link: ${!fromId ? `"${link.left}"` : `"${link.right}"`} not found`,
715
+ link.lineNo,
716
+ link.raw
717
+ );
389
718
  continue;
390
719
  }
391
720
  if (link.kind === "dependency") {
@@ -420,22 +749,64 @@ function parseDSL(text) {
420
749
  edges,
421
750
  rawPassthrough: rawPassthrough.length ? rawPassthrough : void 0
422
751
  });
423
- return wardleySchemaModel.validateMap(map);
752
+ return { map: wardleySchemaModel.validateMap(map), diagnostics };
424
753
  }
425
754
  function parseNode(after) {
426
- const lo = parseLabelOffset(after);
755
+ const split = splitAtCoords(after);
756
+ if (!split || !split.name) return null;
757
+ const col = parseColor(split.suffix);
758
+ const url = parseUrlRef(col.rest);
759
+ const lo = parseLabelOffset(url.rest);
427
760
  const dec = parseDecorators(lo.rest);
428
- const coords = parseCoords(dec.rest);
429
- if (!coords) return null;
430
- const name = stripCoords(dec.rest).trim();
761
+ return compact({
762
+ name: split.name,
763
+ coords: split.coords,
764
+ decorators: dec.decorators,
765
+ labelOffset: lo.labelOffset ?? void 0,
766
+ urlRef: url.urlRef ?? void 0,
767
+ color: col.color ?? void 0
768
+ });
769
+ }
770
+ function clamp01(n) {
771
+ return n < 0 ? 0 : n > 1 ? 1 : n;
772
+ }
773
+ var SINGLE_COORD_RE = /\[\s*([-\d.]+)\s*\]/;
774
+ function parseBlockChild(after) {
775
+ const m = SINGLE_COORD_RE.exec(after);
776
+ if (!m) return null;
777
+ const maturity = Number(m[1]);
778
+ if (Number.isNaN(maturity)) return null;
779
+ const name = after.slice(0, m.index).trim();
431
780
  if (!name) return null;
781
+ const suffix = after.slice(m.index + m[0].length);
782
+ const col = parseColor(suffix);
783
+ const lo = parseLabelOffset(col.rest);
784
+ const dec = parseDecorators(lo.rest);
432
785
  return compact({
433
786
  name,
434
- coords,
787
+ maturity,
435
788
  decorators: dec.decorators,
436
- labelOffset: lo.labelOffset ?? void 0
789
+ labelOffset: lo.labelOffset ?? void 0,
790
+ color: col.color ?? void 0
437
791
  });
438
792
  }
793
+ function makeAttitude(ids, kind, v1, m1, v2, m2, color) {
794
+ return {
795
+ id: ids.alloc("attitude", kind),
796
+ elementType: "attitude",
797
+ kind,
798
+ label: "",
799
+ ...color ? { color } : {},
800
+ position: {
801
+ visibility: clamp01(Math.max(v1, v2)),
802
+ evolution: clamp01(Math.min(m1, m2))
803
+ },
804
+ corner2: {
805
+ visibility: clamp01(Math.min(v1, v2)),
806
+ evolution: clamp01(Math.max(m1, m2))
807
+ }
808
+ };
809
+ }
439
810
  function mergeLegacy(kw, dec) {
440
811
  const merged = { ...dec };
441
812
  if (kw === "market") merged["market"] = true;
@@ -443,7 +814,8 @@ function mergeLegacy(kw, dec) {
443
814
  return compact(merged);
444
815
  }
445
816
  function parseEvolve(after) {
446
- const dec = parseDecorators(after);
817
+ const lo = parseLabelOffset(after);
818
+ const dec = parseDecorators(lo.rest);
447
819
  const tokens = dec.rest.trim().split(/\s+/);
448
820
  if (tokens.length < 2) return null;
449
821
  const last = tokens[tokens.length - 1];
@@ -458,11 +830,17 @@ function parseEvolve(after) {
458
830
  name,
459
831
  newLabel: newLabel || void 0,
460
832
  target,
461
- method: dec.decorators.method
833
+ method: dec.decorators.method,
834
+ labelOffset: lo.labelOffset ?? void 0
462
835
  });
463
836
  }
464
837
 
465
838
  // src/serializer.ts
839
+ function urlOf(el) {
840
+ if (el.elementType === "component") return el.url;
841
+ if (el.elementType === "submap") return el.urlRef;
842
+ return void 0;
843
+ }
466
844
  function r(n) {
467
845
  return String(Math.round(n * 1e3) / 1e3);
468
846
  }
@@ -487,6 +865,9 @@ function uniqueNames(map) {
487
865
  function offsetSuffix(lo) {
488
866
  return lo ? ` label [${r(lo.dx)}, ${r(lo.dy)}]` : "";
489
867
  }
868
+ function colorSuffix(el) {
869
+ return el.color ? ` (color ${el.color})` : "";
870
+ }
490
871
  function decoratorSuffix(dec) {
491
872
  if (!dec) return "";
492
873
  const paren = [];
@@ -507,22 +888,75 @@ function serializeDSL(map) {
507
888
  if (map.config.size)
508
889
  lines.push(`size [${r(map.config.size.width)}, ${r(map.config.size.height)}]`);
509
890
  if (map.config.evolutionLabels) lines.push(`evolution ${map.config.evolutionLabels.join("->")}`);
510
- if (map.config.yAxisLabel) lines.push(`y-axis ${map.config.yAxisLabel}`);
891
+ if (map.config.yAxisLabel) {
892
+ const ends = map.config.yAxisEndLabels;
893
+ lines.push(`y-axis ${map.config.yAxisLabel}${ends ? `->${ends[0]}->${ends[1]}` : ""}`);
894
+ }
511
895
  if (map.config.annotationsBoxPosition) {
512
896
  const b = map.config.annotationsBoxPosition;
513
897
  lines.push(`annotations [${r(b.visibility)}, ${r(b.evolution)}]`);
514
898
  }
899
+ const urlDefNames = /* @__PURE__ */ new Map();
900
+ for (const el of map.elements) {
901
+ const address = urlOf(el);
902
+ if (!address) continue;
903
+ const defName = `${nameOf(el)} URL`;
904
+ urlDefNames.set(el.id, defName);
905
+ lines.push(`url ${defName} [${address}]`);
906
+ }
907
+ const urlSuffix = (el) => {
908
+ const def = urlDefNames.get(el.id);
909
+ return def ? ` url(${def})` : "";
910
+ };
911
+ const pipelineIds = new Set(
912
+ map.elements.filter((e) => e.elementType === "pipeline").map((e) => e.id)
913
+ );
914
+ const childrenByPipeline = /* @__PURE__ */ new Map();
915
+ for (const el of map.elements) {
916
+ if (el.elementType !== "component" || !el.pipelineId || !pipelineIds.has(el.pipelineId)) {
917
+ continue;
918
+ }
919
+ const list = childrenByPipeline.get(el.pipelineId) ?? [];
920
+ list.push(el);
921
+ childrenByPipeline.set(el.pipelineId, list);
922
+ }
923
+ const isPipelineChild = (el) => el.elementType === "component" && !!el.pipelineId && pipelineIds.has(el.pipelineId);
515
924
  const evolveLines = [];
925
+ const componentVisByLabel = /* @__PURE__ */ new Map();
926
+ for (const el of map.elements) {
927
+ if (el.elementType === "component" && !componentVisByLabel.has(el.label)) {
928
+ componentVisByLabel.set(el.label, el.position.visibility);
929
+ }
930
+ }
931
+ const pipelineYSuffix = (el) => {
932
+ if (el.elementType !== "pipeline") return "";
933
+ const derived = componentVisByLabel.get(el.label) ?? 0.5;
934
+ return Math.abs(el.position.visibility - derived) > 5e-4 ? ` (y ${r(el.position.visibility)})` : "";
935
+ };
516
936
  for (const el of map.elements) {
517
- lines.push(elementLine(el, nameOf(el)));
518
937
  if (el.elementType === "component" && el.movement) {
519
938
  evolveLines.push(evolveLine(el, nameOf(el)));
520
939
  }
940
+ if (isPipelineChild(el)) continue;
941
+ lines.push(elementLine(el, nameOf(el)) + pipelineYSuffix(el) + urlSuffix(el));
942
+ if (el.elementType === "pipeline") {
943
+ const kids = childrenByPipeline.get(el.id) ?? [];
944
+ if (kids.length) {
945
+ lines.push("{");
946
+ for (const k of kids) {
947
+ lines.push(
948
+ ` component ${nameOf(k)} [${r(k.position.evolution)}]${decoratorSuffix(k.decorators)}${colorSuffix(k)}${offsetSuffix(k.labelOffset)}`
949
+ );
950
+ }
951
+ lines.push("}");
952
+ }
953
+ }
521
954
  }
522
955
  for (const line of evolveLines) lines.push(line);
956
+ const labelsById = new Map(map.elements.map((el) => [el.id, el.label]));
523
957
  for (const edge of map.edges) {
524
- const from = names.get(edge.from) ?? edge.from;
525
- const to = names.get(edge.to) ?? edge.to;
958
+ const from = names.get(edge.from) ?? labelsById.get(edge.from) ?? edge.from;
959
+ const to = names.get(edge.to) ?? labelsById.get(edge.to) ?? edge.to;
526
960
  const annotation = edge.label ? `; ${edge.label}` : "";
527
961
  if (edge.edgeType === "dependency") {
528
962
  lines.push(`${from} -> ${to}${annotation}`);
@@ -546,28 +980,36 @@ function elementLine(el, name) {
546
980
  const p = el.position;
547
981
  switch (el.elementType) {
548
982
  case "anchor":
549
- return `anchor ${name} [${r(p.visibility)}, ${r(p.evolution)}]${offsetSuffix(el.labelOffset)}`;
983
+ return `anchor ${name} [${r(p.visibility)}, ${r(p.evolution)}]${colorSuffix(el)}${offsetSuffix(el.labelOffset)}`;
550
984
  case "component":
551
- return `component ${name} [${r(p.visibility)}, ${r(p.evolution)}]${decoratorSuffix(el.decorators)}${offsetSuffix(el.labelOffset)}`;
985
+ return `component ${name} [${r(p.visibility)}, ${r(p.evolution)}]${decoratorSuffix(el.decorators)}${colorSuffix(el)}${offsetSuffix(el.labelOffset)}`;
552
986
  case "note":
553
- return `note ${name.replace(/\n/g, "\\n")} [${r(p.visibility)}, ${r(p.evolution)}]${el.color ? ` (color ${el.color})` : ""}`;
987
+ return `note ${name.replace(/\n/g, "\\n")} [${r(p.visibility)}, ${r(p.evolution)}]${colorSuffix(el)}`;
554
988
  case "pipeline":
555
- return `pipeline ${name} [${r(el.evolutionStart)}, ${r(el.evolutionEnd)}]`;
989
+ return `pipeline ${name} [${r(el.evolutionStart)}, ${r(el.evolutionEnd)}]${colorSuffix(el)}`;
556
990
  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}`;
991
+ return `submap ${name} [${r(p.visibility)}, ${r(p.evolution)}]${colorSuffix(el)}`;
992
+ case "annotation": {
993
+ const pos = el.positions.length > 1 ? `[${el.positions.map((q) => `[${r(q.visibility)}, ${r(q.evolution)}]`).join(", ")}]` : `[${r(p.visibility)}, ${r(p.evolution)}]`;
994
+ return `annotation ${el.number} ${pos}${colorSuffix(el)} ${el.text}`;
995
+ }
560
996
  case "accelerator":
561
- return `${el.direction === "deaccelerate" ? "deaccelerator" : "accelerator"} ${name} [${r(p.visibility)}, ${r(p.evolution)}]`;
997
+ return `${el.direction === "deaccelerate" ? "deaccelerator" : "accelerator"} ${name} [${r(p.visibility)}, ${r(p.evolution)}]${colorSuffix(el)}`;
562
998
  case "attitude":
563
- return `${el.kind} [${r(p.visibility)}, ${r(p.evolution)}] ${r(el.width)} ${r(el.height)}`;
999
+ return `${el.kind} [${r(p.visibility)}, ${r(p.evolution)}, ${r(el.corner2.visibility)}, ${r(el.corner2.evolution)}]${colorSuffix(el)}`;
1000
+ case "drawing": {
1001
+ const pts = el.points.map((q) => `[${r(q.visibility)}, ${r(q.evolution)}]`).join(", ");
1002
+ const closed = el.closed ? " (closed)" : "";
1003
+ const stroke = el.strokeStyle && el.strokeStyle !== "solid" ? ` (${el.strokeStyle})` : "";
1004
+ return `line [${pts}]${closed}${stroke}${colorSuffix(el)}`;
1005
+ }
564
1006
  }
565
1007
  }
566
1008
  function evolveLine(el, name) {
567
1009
  const mv = el.movement;
568
1010
  const label = mv.newLabel ? `${name}->${mv.newLabel}` : name;
569
1011
  const method = mv.method ? ` (${mv.method})` : "";
570
- return `evolve ${label} ${r(mv.targetEvolution)}${method}`;
1012
+ return `evolve ${label} ${r(mv.targetEvolution)}${method}${offsetSuffix(mv.labelOffset)}`;
571
1013
  }
572
1014
 
573
1015
  Object.defineProperty(exports, "loadMap", {
@@ -585,6 +1027,7 @@ Object.defineProperty(exports, "mapToJSON", {
585
1027
  exports.keywordOf = keywordOf;
586
1028
  exports.parseCoords = parseCoords;
587
1029
  exports.parseDSL = parseDSL;
1030
+ exports.parseDSLWithDiagnostics = parseDSLWithDiagnostics;
588
1031
  exports.parseDecorators = parseDecorators;
589
1032
  exports.parseLabelOffset = parseLabelOffset;
590
1033
  exports.serializeDSL = serializeDSL;