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