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