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