@miragon/event-storming-dsl 0.2.2 → 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.js CHANGED
@@ -5,12 +5,19 @@ export { parseBoardJSON as boardFromJSON, serializeBoard as boardToJSON, loadBoa
5
5
 
6
6
  // src/lexer.ts
7
7
  var COORDS_RE = /\[\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\]/;
8
+ var LARGEST_DRAWABLE_NUMBER = 1e38;
9
+ function isDrawable(value) {
10
+ return Math.abs(value) <= LARGEST_DRAWABLE_NUMBER;
11
+ }
12
+ function drawablePair(a, b) {
13
+ return isDrawable(a) && isDrawable(b);
14
+ }
8
15
  function parseCoords(line) {
9
16
  const m = COORDS_RE.exec(line);
10
17
  if (!m) return null;
11
18
  const a = Number(m[1]);
12
19
  const b = Number(m[2]);
13
- if (Number.isNaN(a) || Number.isNaN(b)) return null;
20
+ if (!drawablePair(a, b)) return null;
14
21
  return { a, b };
15
22
  }
16
23
  function splitAtCoords(line) {
@@ -18,7 +25,7 @@ function splitAtCoords(line) {
18
25
  if (!m) return null;
19
26
  const a = Number(m[1]);
20
27
  const b = Number(m[2]);
21
- if (Number.isNaN(a) || Number.isNaN(b)) return null;
28
+ if (!drawablePair(a, b)) return null;
22
29
  return {
23
30
  name: line.slice(0, m.index).trim(),
24
31
  coords: { a, b },
@@ -35,7 +42,8 @@ function parseMultiCoords(line) {
35
42
  while (t = inner.exec(m[1])) {
36
43
  const a = Number(t[1]);
37
44
  const b = Number(t[2]);
38
- if (!Number.isNaN(a) && !Number.isNaN(b)) tuples.push({ a, b });
45
+ if (!drawablePair(a, b)) return null;
46
+ tuples.push({ a, b });
39
47
  }
40
48
  if (!tuples.length) return null;
41
49
  return { tuples, rest: line.replace(MULTI_COORDS_RE, " ") };
@@ -97,7 +105,7 @@ function parseSize(line) {
97
105
  if (m) {
98
106
  const width = Number(m[1]);
99
107
  const height = Number(m[2]);
100
- if (width > 0 && height > 0) {
108
+ if (width > 0 && height > 0 && isDrawable(width) && isDrawable(height)) {
101
109
  return { size: { width, height }, rest: line.replace(SIZE_RE, " ") };
102
110
  }
103
111
  }
@@ -122,6 +130,9 @@ function parseAlign(line) {
122
130
  if (!p) return { rest: line };
123
131
  return { invalid: p[0], rest: line.replace(ALIGN_PRESENT_RE, " ") };
124
132
  }
133
+ function decodeName(name) {
134
+ return name.replace(/\\n/g, "\n");
135
+ }
125
136
  function keywordOf(line) {
126
137
  const m = /^\s*([A-Za-z][\w-]*)/.exec(line);
127
138
  return m ? m[1].toLowerCase() : "";
@@ -130,6 +141,44 @@ function slug(label) {
130
141
  return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_/, "").replace(/_$/, "") || "x";
131
142
  }
132
143
 
144
+ // src/ids.ts
145
+ var STICKY_ID_PREFIXES = {
146
+ event: "event",
147
+ command: "cmd",
148
+ actor: "actor",
149
+ aggregate: "agg",
150
+ policy: "policy",
151
+ readmodel: "read",
152
+ external: "ext",
153
+ hotspot: "hot"
154
+ };
155
+ var ENGINE_ID_RE = /^(?:event-storming-root$|__implicitroot)/;
156
+ function isEngineId(id) {
157
+ return ENGINE_ID_RE.test(id);
158
+ }
159
+ var IdAllocator = class {
160
+ used = /* @__PURE__ */ new Set();
161
+ /** Explicit `(id …)` — false when the id is taken or reserved (caller reports + allocates). */
162
+ claim(id) {
163
+ if (this.used.has(id) || isEngineId(id)) return false;
164
+ this.used.add(id);
165
+ return true;
166
+ }
167
+ /** The id `alloc(prefix, label)` would return, without taking it. */
168
+ peek(prefix, label) {
169
+ const base = `${prefix}_${slug(label)}`;
170
+ let id = base;
171
+ let i = 2;
172
+ while (this.used.has(id)) id = `${base}_${i++}`;
173
+ return id;
174
+ }
175
+ alloc(prefix, label) {
176
+ const id = this.peek(prefix, label);
177
+ this.used.add(id);
178
+ return id;
179
+ }
180
+ };
181
+
133
182
  // src/parser.ts
134
183
  function splitArrow(core) {
135
184
  const arrow = core.indexOf("->");
@@ -142,19 +191,6 @@ var KNOWN_STYLES = /* @__PURE__ */ new Set(["classic", "dark"]);
142
191
  var ATTACHABLE_KINDS = new Set(ATTACHABLE_STICKY_KINDS);
143
192
  var HOST_KINDS = new Set(HOST_STICKY_KINDS);
144
193
  var KNOWN_LEVELS = /* @__PURE__ */ new Set(["big-picture", "process", "design"]);
145
- function decodeName(name) {
146
- return name.replace(/\\n/g, "\n");
147
- }
148
- var STICKY_ID_PREFIXES = {
149
- event: "event",
150
- command: "cmd",
151
- actor: "actor",
152
- aggregate: "agg",
153
- policy: "policy",
154
- readmodel: "read",
155
- external: "ext",
156
- hotspot: "hot"
157
- };
158
194
  var STICKY_KINDS = new Set(Object.keys(STICKY_ID_PREFIXES));
159
195
  function compact(obj) {
160
196
  const out = {};
@@ -169,23 +205,6 @@ function modelAlign(align) {
169
205
  });
170
206
  return Object.keys(out).length ? out : void 0;
171
207
  }
172
- var IdAllocator = class {
173
- used = /* @__PURE__ */ new Set();
174
- /** Explicit `(id …)` — false when the id is already taken (caller reports + allocates). */
175
- claim(id) {
176
- if (this.used.has(id)) return false;
177
- this.used.add(id);
178
- return true;
179
- }
180
- alloc(prefix, label) {
181
- const base = `${prefix}_${slug(label)}`;
182
- let id = base;
183
- let i = 2;
184
- while (this.used.has(id)) id = `${base}_${i++}`;
185
- this.used.add(id);
186
- return id;
187
- }
188
- };
189
208
  function parseDSL(text) {
190
209
  return parseDSLWithDiagnostics(text).board;
191
210
  }
@@ -222,12 +241,13 @@ function parseDSLWithDiagnostics(text) {
222
241
  const diag = (message, atLine = lineNo, text_ = currentLine) => diagnostics.push({ line: atLine, message, text: text_ });
223
242
  const failed = (l) => {
224
243
  rawPassthrough.push(l);
225
- diag("Line could not be interpreted (kept losslessly in rawPassthrough)");
244
+ diag("Couldn't read this line. It stays in the file unchanged.");
226
245
  };
227
246
  const pos = (x, y) => ({ x, y });
228
247
  const sourceLines = text.split(/\r?\n/);
229
248
  for (let i = 0; i < sourceLines.length; i++) {
230
249
  const raw = sourceLines[i];
250
+ if (raw === "" && i === sourceLines.length - 1) continue;
231
251
  lineNo = i + 1;
232
252
  currentLine = raw.trim();
233
253
  let working = raw;
@@ -301,12 +321,14 @@ function parseDSLWithDiagnostics(text) {
301
321
  }
302
322
  if (node.idInvalid) {
303
323
  diag(
304
- `Id: could not read "${node.idInvalid.trim()}" \u2014 expected (id <id>) with letters, digits, '_' or '-'`
324
+ `Couldn't read the id \u201C${node.idInvalid.trim()}\u201D. An id has letters, digits, \u201C_\u201D or \u201C-\u201D, as in (id order_1).`
305
325
  );
306
326
  }
307
327
  const explicit = node.id !== void 0 && ids.claim(node.id) ? node.id : void 0;
308
328
  if (node.id !== void 0 && explicit === void 0) {
309
- diag(`Id: "${node.id}" is already taken \u2014 the element got a fresh id`);
329
+ diag(
330
+ isEngineId(node.id) ? `The id \u201C${node.id}\u201D belongs to the board itself, so this sticky got a new id.` : `The id \u201C${node.id}\u201D is already taken, so this sticky got a new id.`
331
+ );
310
332
  }
311
333
  const id = explicit ?? ids.alloc(STICKY_ID_PREFIXES[kw], node.name);
312
334
  elements.push(
@@ -328,14 +350,14 @@ function parseDSLWithDiagnostics(text) {
328
350
  lineNo
329
351
  });
330
352
  } else {
331
- diag(`Attachment: a ${kw} cannot be pinned \u2014 only actor/hotspot/note support (on \u2026)`);
353
+ diag(`A ${kw} can't be pinned. Only an actor, a hotspot or a note takes (on \u2026).`);
332
354
  }
333
355
  }
334
356
  if (node.sizeSuffix) {
335
- diag(`Size: a ${kw} cannot be resized \u2014 only notes support (size \u2026)`);
357
+ diag(`A ${kw} can't be resized. Only a note takes (size \u2026).`);
336
358
  }
337
359
  if (node.alignSuffix) {
338
- diag(`Align: a ${kw} cannot be aligned \u2014 only notes support (align \u2026)`);
360
+ diag(`A ${kw} can't be aligned. Only a note takes (align \u2026).`);
339
361
  }
340
362
  break;
341
363
  }
@@ -352,16 +374,16 @@ function parseDSLWithDiagnostics(text) {
352
374
  const col = parseColor(idp.rest);
353
375
  if (al.invalid) {
354
376
  diag(
355
- `Align: could not read "${al.invalid.trim()}" \u2014 expected (align left|center|right top|middle|bottom)`
377
+ `Couldn't read the alignment \u201C${al.invalid.trim()}\u201D. Write (align left|center|right top|middle|bottom).`
356
378
  );
357
379
  }
358
380
  if (sz.invalid) {
359
381
  diag(
360
- `Size: could not read "${sz.invalid.trim()}" \u2014 expected (size <w>x<h>) with positive numbers`
382
+ `Couldn't read the size \u201C${sz.invalid.trim()}\u201D. Write (size <w>x<h>) with positive numbers.`
361
383
  );
362
384
  }
363
385
  if (idp.id !== void 0 || idp.invalid) {
364
- diag("Id: a note cannot be referenced \u2014 only sticky kinds support (id \u2026)");
386
+ diag("A note can't have an id. Only a sticky takes (id \u2026).");
365
387
  }
366
388
  const textPart = decodeName((split ? split.name : col.rest).trim());
367
389
  const id = ids.alloc("note", textPart || "note");
@@ -389,19 +411,19 @@ function parseDSLWithDiagnostics(text) {
389
411
  }
390
412
  const onDrawing = parseOn(multi.rest);
391
413
  if (onDrawing.host !== void 0) {
392
- diag("Attachment: a drawing cannot be pinned \u2014 only actor/hotspot/note support (on \u2026)");
414
+ diag("A drawing can't be pinned. Only an actor, a hotspot or a note takes (on \u2026).");
393
415
  }
394
416
  const szDrawing = parseSize(onDrawing.rest);
395
417
  if (szDrawing.size || szDrawing.invalid) {
396
- diag("Size: a drawing cannot be resized \u2014 only notes support (size \u2026)");
418
+ diag("A drawing can't be resized. Only a note takes (size \u2026).");
397
419
  }
398
420
  const alDrawing = parseAlign(onDrawing.rest);
399
421
  if (alDrawing.align || alDrawing.invalid) {
400
- diag("Align: a drawing cannot be aligned \u2014 only notes support (align \u2026)");
422
+ diag("A drawing can't be aligned. Only a note takes (align \u2026).");
401
423
  }
402
424
  const idDrawing = parseId(onDrawing.rest);
403
425
  if (idDrawing.id !== void 0 || idDrawing.invalid) {
404
- diag("Id: a drawing cannot be referenced \u2014 only sticky kinds support (id \u2026)");
426
+ diag("A drawing can't have an id. Only a sticky takes (id \u2026).");
405
427
  }
406
428
  const flags = onDrawing.rest.toLowerCase();
407
429
  const strokeStyle = flags.includes("(dashed)") ? "dashed" : flags.includes("(dotted)") ? "dotted" : void 0;
@@ -435,12 +457,12 @@ function parseDSLWithDiagnostics(text) {
435
457
  for (const att of pendingAttachments) {
436
458
  const host = resolve(att.host);
437
459
  if (!host) {
438
- diag(`Attachment: "${att.host}" not found`, att.lineNo, att.raw);
460
+ diag(`There's no \u201C${att.host}\u201D on the board to pin this to.`, att.lineNo, att.raw);
439
461
  continue;
440
462
  }
441
463
  if (!HOST_KINDS.has(host.elementType)) {
442
464
  diag(
443
- `Attachment: "${att.host}" is a ${host.elementType} \u2014 actors/hotspots/notes may only attach to host stickies`,
465
+ `\u201C${att.host}\u201D is a ${host.elementType}. An actor, a hotspot or a note is pinned to an event, a command, an aggregate, a policy, a read model or an external system.`,
444
466
  att.lineNo,
445
467
  att.raw
446
468
  );
@@ -462,7 +484,7 @@ function parseDSLWithDiagnostics(text) {
462
484
  if (!from || !to) {
463
485
  rawPassthrough.push(arrow.raw);
464
486
  diag(
465
- `Arrow: ${!from ? `"${arrow.left}"` : `"${arrow.right}"`} not found`,
487
+ !from ? `This arrow starts at \u201C${arrow.left}\u201D, which isn't on the board.` : `This arrow points to \u201C${arrow.right}\u201D, which isn't on the board.`,
466
488
  arrow.lineNo,
467
489
  arrow.raw
468
490
  );
@@ -472,7 +494,7 @@ function parseDSLWithDiagnostics(text) {
472
494
  if (nonSticky) {
473
495
  rawPassthrough.push(arrow.raw);
474
496
  diag(
475
- `Arrow: "${nonSticky[0]}" is a ${nonSticky[1].elementType} \u2014 arrows may only connect stickies`,
497
+ `\u201C${nonSticky[0]}\u201D is a ${nonSticky[1].elementType}, and an arrow only connects stickies.`,
476
498
  arrow.lineNo,
477
499
  arrow.raw
478
500
  );
@@ -593,21 +615,22 @@ function serializeDSL(board) {
593
615
  const nameOf = (el) => names.get(el.id) ?? el.label;
594
616
  const nameCount = /* @__PURE__ */ new Map();
595
617
  for (const name of names.values()) nameCount.set(name, (nameCount.get(name) ?? 0) + 1);
596
- const needsId = (id) => {
618
+ const referencedById = (id) => {
597
619
  const name = names.get(id);
598
620
  return name !== void 0 && (nameCount.get(name) > 1 || name.startsWith("#"));
599
621
  };
600
622
  const emitIds = /* @__PURE__ */ new Map();
601
623
  const taken = new Set(board.elements.map((e) => e.id));
602
624
  for (const el of board.elements) {
603
- if (!needsId(el.id) || ID_CHARSET_RE.test(el.id)) continue;
625
+ if (!referencedById(el.id) || ID_CHARSET_RE.test(el.id)) continue;
604
626
  let candidate = slug(el.id);
605
627
  for (let i = 2; taken.has(candidate); i++) candidate = `${slug(el.id)}_${i}`;
606
628
  taken.add(candidate);
607
629
  emitIds.set(el.id, candidate);
608
630
  }
609
631
  const idOf = (id) => emitIds.get(id) ?? id;
610
- const ref = (id) => needsId(id) ? `#${idOf(id)}` : names.get(id) ?? id;
632
+ const declaredIds = idsToDeclare(board, nameOf, referencedById, idOf);
633
+ const ref = (id) => referencedById(id) ? `#${idOf(id)}` : names.get(id) ?? id;
611
634
  lines.push(`title ${board.config.title}`);
612
635
  if (board.config.style) lines.push(`style ${board.config.style}`);
613
636
  if (board.config.level) lines.push(`level ${board.config.level}`);
@@ -616,7 +639,7 @@ function serializeDSL(board) {
616
639
  return hostId ? ` (on ${ref(hostId)})` : "";
617
640
  };
618
641
  for (const el of board.elements) {
619
- const idSuffix = needsId(el.id) ? ` (id ${idOf(el.id)})` : "";
642
+ const idSuffix = declaredIds.has(el.id) ? ` (id ${idOf(el.id)})` : "";
620
643
  lines.push(elementLine(el, nameOf(el), idSuffix, onSuffix(el)));
621
644
  }
622
645
  for (const edge of board.edges) {
@@ -634,6 +657,30 @@ function serializeDSL(board) {
634
657
  }
635
658
  return lines.join("\n") + "\n";
636
659
  }
660
+ function idsToDeclare(board, nameOf, referencedById, emittedId) {
661
+ const allocator = new IdAllocator();
662
+ const declared = /* @__PURE__ */ new Set();
663
+ for (const el of board.elements) {
664
+ const prefix = STICKY_ID_PREFIXES[el.elementType];
665
+ if (prefix === void 0) {
666
+ allocator.alloc(el.elementType === "note" ? "note" : "draw", noteOrDrawingName(el));
667
+ continue;
668
+ }
669
+ const parsedName = decodeName(nameOf(el));
670
+ const declare = referencedById(el.id) || ID_CHARSET_RE.test(el.id) && allocator.peek(prefix, parsedName) !== el.id;
671
+ if (declare) {
672
+ declared.add(el.id);
673
+ allocator.claim(emittedId(el.id));
674
+ } else {
675
+ allocator.alloc(prefix, parsedName);
676
+ }
677
+ }
678
+ return declared;
679
+ }
680
+ function noteOrDrawingName(el) {
681
+ if (el.elementType === "drawing") return "line";
682
+ return decodeName(escapeText(el.label)) || "note";
683
+ }
637
684
  function elementLine(el, name, idSuffix, attach) {
638
685
  const p = el.position;
639
686
  switch (el.elementType) {