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