@miragon/wardley-dsl 0.5.3 → 0.7.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
@@ -357,6 +357,10 @@ var UniqueNames = class {
357
357
  this.used.add(name);
358
358
  return name;
359
359
  }
360
+ /** Takes a name as it is, even when it is already taken (a name the source text already uses). */
361
+ reserve(name) {
362
+ this.used.add(name);
363
+ }
360
364
  };
361
365
 
362
366
  // src/parser.ts
@@ -464,7 +468,7 @@ function unknownStatementMessage(line) {
464
468
  if (flowValue) return `Flow ${flowValue[0]} has no direction \u2014 add ">", "<" or "<>"`;
465
469
  return `Unknown statement "${line.split(/\s/)[0]}"`;
466
470
  }
467
- function buildEdges(links, idOfName, unresolved, labelOf) {
471
+ function buildEdges(links, idOfName, unresolved, labelOf, built) {
468
472
  let dependencies = 0;
469
473
  let flows = 0;
470
474
  const edges = [];
@@ -476,29 +480,29 @@ function buildEdges(links, idOfName, unresolved, labelOf) {
476
480
  continue;
477
481
  }
478
482
  if (link.kind === "dependency") {
479
- edges.push(
480
- compact({
481
- id: `dep_${++dependencies}`,
482
- edgeType: "dependency",
483
- from: fromId,
484
- to: toId,
485
- label: labelOf(link)
486
- })
487
- );
483
+ const edge2 = compact({
484
+ id: `dep_${++dependencies}`,
485
+ edgeType: "dependency",
486
+ from: fromId,
487
+ to: toId,
488
+ label: labelOf(link)
489
+ });
490
+ edges.push(edge2);
491
+ built(edge2, link);
488
492
  continue;
489
493
  }
490
494
  const [from, to] = link.reverse ? [toId, fromId] : [fromId, toId];
491
- edges.push(
492
- compact({
493
- id: `flow_${++flows}`,
494
- edgeType: "flow",
495
- from,
496
- to,
497
- bidirectional: link.bidirectional ? true : void 0,
498
- flowValue: link.flowValue,
499
- label: labelOf(link)
500
- })
501
- );
495
+ const edge = compact({
496
+ id: `flow_${++flows}`,
497
+ edgeType: "flow",
498
+ from,
499
+ to,
500
+ bidirectional: link.bidirectional ? true : void 0,
501
+ flowValue: link.flowValue,
502
+ label: labelOf(link)
503
+ });
504
+ edges.push(edge);
505
+ built(edge, link);
502
506
  }
503
507
  return edges;
504
508
  }
@@ -517,11 +521,18 @@ function parseDSL(text) {
517
521
  return parseDSLWithDiagnostics(text).map;
518
522
  }
519
523
  function parseDSLWithDiagnostics(text) {
524
+ return readMap(text).result;
525
+ }
526
+ function parseDSLWithSourceMap(text) {
527
+ const reading = readMap(text);
528
+ return { ...reading.result, sourceMap: reading.sourceMap };
529
+ }
530
+ function readMap(text) {
520
531
  const reading = new MapReader(/* @__PURE__ */ new Set()).read(text);
521
532
  const needsDeclaredNames = reading.keywordLinks.some(
522
533
  (link) => leftNameIsDeclared(link, reading.declaredNames)
523
534
  );
524
- return needsDeclaredNames ? new MapReader(reading.declaredNames).read(text).result : reading.result;
535
+ return needsDeclaredNames ? new MapReader(reading.declaredNames).read(text) : reading;
525
536
  }
526
537
  var MapReader = class {
527
538
  constructor(linkNames) {
@@ -541,7 +552,7 @@ var MapReader = class {
541
552
  pendingEvolve = [];
542
553
  pendingPipeline = [];
543
554
  pendingMethod = [];
544
- config = { title: "Untitled Map" };
555
+ config = { title: wardleySchemaModel.DEFAULT_MAP_TITLE };
545
556
  lineThatSetConfig = /* @__PURE__ */ new Map();
546
557
  unusedConfigLines = [];
547
558
  annotationCounter = 0;
@@ -554,6 +565,22 @@ var MapReader = class {
554
565
  blockOpening = { line: 0, text: "" };
555
566
  lineNo = 0;
556
567
  currentLine = "";
568
+ declarationLineById = /* @__PURE__ */ new Map();
569
+ configLinesByKeyword = /* @__PURE__ */ new Map();
570
+ evolveLinesById = /* @__PURE__ */ new Map();
571
+ methodLinesById = /* @__PURE__ */ new Map();
572
+ blockLines = /* @__PURE__ */ new Map();
573
+ pipelineIds = /* @__PURE__ */ new Map();
574
+ rangeFromChildren = /* @__PURE__ */ new Set();
575
+ addElement(element, line = this.lineNo) {
576
+ this.elements.push(element);
577
+ this.declarationLineById.set(element.id, line);
578
+ }
579
+ recordBlockLine(pipeline, end) {
580
+ const lines = this.blockLines.get(pipeline) ?? {};
581
+ lines[end] = this.lineNo;
582
+ this.blockLines.set(pipeline, lines);
583
+ }
557
584
  report = (message) => this.diag(message);
558
585
  reportAt = (message, line, text) => this.diag(message, line, text);
559
586
  diag(message, atLine = this.lineNo, text = this.currentLine) {
@@ -568,7 +595,7 @@ var MapReader = class {
568
595
  }
569
596
  /** For a line that looks like a known construct but cannot be parsed. */
570
597
  failed(line) {
571
- this.keep(line, "Line could not be interpreted (kept losslessly in rawPassthrough)");
598
+ this.keep(line, "Line not understood \u2014 kept as written");
572
599
  }
573
600
  // A reference binds to the name as written first, so `X 2` stays bound to an explicit "X 2" even
574
601
  // when a duplicate "X" was renamed to "X 2"; the serializer then writes that reference by the
@@ -586,6 +613,9 @@ var MapReader = class {
586
613
  this.diag(`Repeated "${keyword}" \u2014 replaces the one on line ${earlier}`);
587
614
  }
588
615
  this.lineThatSetConfig.set(keyword, this.lineNo);
616
+ const configLines2 = this.configLinesByKeyword.get(keyword) ?? [];
617
+ configLines2.push(this.lineNo);
618
+ this.configLinesByKeyword.set(keyword, configLines2);
589
619
  this.config = update;
590
620
  }
591
621
  keepUnusedConfigLine(keyword, statement, reason) {
@@ -670,13 +700,16 @@ var MapReader = class {
670
700
  openBlock() {
671
701
  if (this.lastPipeline) {
672
702
  this.blockPipeline = this.lastPipeline;
703
+ this.recordBlockLine(this.blockPipeline, "open");
673
704
  this.blockOpening = { line: this.lineNo, text: this.currentLine };
674
705
  } else this.diag('Ignored "{" \u2014 a block must directly follow a pipeline line');
675
706
  this.lastPipeline = null;
676
707
  }
677
708
  closeBlock() {
678
- if (this.blockPipeline) this.blockPipeline = null;
679
- else this.diag('Ignored "}" \u2014 no pipeline block is open');
709
+ if (this.blockPipeline) {
710
+ this.recordBlockLine(this.blockPipeline, "close");
711
+ this.blockPipeline = null;
712
+ } else this.diag('Ignored "}" \u2014 no pipeline block is open');
680
713
  this.lastPipeline = null;
681
714
  }
682
715
  endUnclosedBlock() {
@@ -745,7 +778,10 @@ var MapReader = class {
745
778
  const closesBlock = this.blockPipeline !== null && statement.endsWith("}");
746
779
  if (closesBlock) statement = statement.slice(0, -1).trim();
747
780
  if (statement) this.readStatement(statement);
748
- if (closesBlock) this.blockPipeline = null;
781
+ if (closesBlock && this.blockPipeline) {
782
+ this.recordBlockLine(this.blockPipeline, "close");
783
+ this.blockPipeline = null;
784
+ }
749
785
  }
750
786
  readStatement(line) {
751
787
  const kw = keywordOf(line);
@@ -838,7 +874,7 @@ var MapReader = class {
838
874
  labelOffset: node.labelOffset,
839
875
  color: node.color
840
876
  });
841
- this.elements.push(anchor);
877
+ this.addElement(anchor);
842
878
  this.register(node.name, label, id);
843
879
  }
844
880
  /** `component`/`market`/`ecosystem` and `build`/`buy`/`outsource` lines with coordinates. */
@@ -858,7 +894,7 @@ var MapReader = class {
858
894
  decorators: Object.keys(decorators).length ? decorators : void 0,
859
895
  color: node.color
860
896
  });
861
- this.elements.push(component);
897
+ this.addElement(component);
862
898
  if (node.urlRef) this.addUrlRef(node.urlRef);
863
899
  this.register(node.name, label, id);
864
900
  return true;
@@ -880,7 +916,7 @@ var MapReader = class {
880
916
  position: this.position(split.coords.a, split.coords.b),
881
917
  color
882
918
  });
883
- this.elements.push(note);
919
+ this.addElement(note);
884
920
  }
885
921
  /** `pipeline X [s, e]`, `pipeline X` (block form, range from children) — optionally with `{` at the end. */
886
922
  readPipeline(line, after) {
@@ -918,6 +954,7 @@ var MapReader = class {
918
954
  this.pendingPipeline.push(pending);
919
955
  if (opensBlock) {
920
956
  this.blockPipeline = pending;
957
+ this.recordBlockLine(pending, "open");
921
958
  this.blockOpening = { line: this.lineNo, text: this.currentLine };
922
959
  } else this.lastPipeline = pending;
923
960
  }
@@ -950,6 +987,13 @@ var MapReader = class {
950
987
  this.reportTextBefore(split.name);
951
988
  readSuffix(split.suffix, NO_SUFFIXES, this.report);
952
989
  this.setConfig("size", { ...this.config, size: { width: coords.a, height: coords.b } });
990
+ const problem = sizeOutsideLimits(coords.a, coords.b);
991
+ if (problem) this.diag(problem);
992
+ if (!Number.isInteger(coords.a) || !Number.isInteger(coords.b)) {
993
+ this.diag(
994
+ `Map size ${coords.a}\xD7${coords.b} is not in whole pixels \u2014 OWM only reads whole-pixel sizes`
995
+ );
996
+ }
953
997
  }
954
998
  readEvolution(line, after) {
955
999
  const parts = after.split("->").map((s) => s.trim());
@@ -1022,7 +1066,7 @@ var MapReader = class {
1022
1066
  text,
1023
1067
  ...color ? { color } : {}
1024
1068
  };
1025
- this.elements.push(annotation);
1069
+ this.addElement(annotation);
1026
1070
  }
1027
1071
  /** Project extension (freeform drawing): `line [[v,e], [v,e], …] (closed) (dashed) (color x)`. */
1028
1072
  readDrawing(line, after) {
@@ -1042,7 +1086,7 @@ var MapReader = class {
1042
1086
  ...suffix.strokeStyle ? { strokeStyle: suffix.strokeStyle } : {},
1043
1087
  ...suffix.color ? { color: suffix.color } : {}
1044
1088
  };
1045
- this.elements.push(drawing);
1089
+ this.addElement(drawing);
1046
1090
  }
1047
1091
  /** Canonical OWM form: `<kind> [vis1, mat1, vis2, mat2]` (two corners, normalized). */
1048
1092
  readAttitude(line, after, kind) {
@@ -1052,7 +1096,7 @@ var MapReader = class {
1052
1096
  const { color } = readSuffix(split.after, ATTITUDE_SUFFIXES, this.report);
1053
1097
  const { a, b, c, d } = split.coords;
1054
1098
  const corners = [this.clamp(a), this.clamp(b), this.clamp(c), this.clamp(d)];
1055
- this.elements.push(makeAttitude(this.ids, kind, corners, color));
1099
+ this.addElement(makeAttitude(this.ids, kind, corners, color));
1056
1100
  }
1057
1101
  /** OWM: `url Name [https://…]` — definition, referenced via `url(Name)` on elements. */
1058
1102
  readUrlDefinition(line, after) {
@@ -1088,7 +1132,7 @@ var MapReader = class {
1088
1132
  position: this.position(node.coords.a, node.coords.b),
1089
1133
  ...node.color ? { color: node.color } : {}
1090
1134
  };
1091
- this.elements.push(accelerator);
1135
+ this.addElement(accelerator);
1092
1136
  this.register(node.name, label, id);
1093
1137
  }
1094
1138
  readSubmap(line, after) {
@@ -1103,7 +1147,7 @@ var MapReader = class {
1103
1147
  position: this.position(node.coords.a, node.coords.b),
1104
1148
  ...node.color ? { color: node.color } : {}
1105
1149
  };
1106
- this.elements.push(submap);
1150
+ this.addElement(submap);
1107
1151
  if (node.urlRef) this.addUrlRef(node.urlRef);
1108
1152
  this.register(node.name, label, id);
1109
1153
  }
@@ -1117,11 +1161,20 @@ var MapReader = class {
1117
1161
  const indexById = new Map(this.elements.map((el, index) => [el.id, index]));
1118
1162
  this.resolveEvolves(indexById);
1119
1163
  this.resolveMethods(indexById);
1164
+ const urlReferences = /* @__PURE__ */ new Map();
1165
+ const usedDefinitions = /* @__PURE__ */ new Set();
1120
1166
  const keptDefinitions = resolveUrlReferences(
1121
1167
  this.elements,
1122
1168
  this.pendingUrlRefs,
1123
1169
  this.urlDefinitions,
1124
- this.reportAt
1170
+ this.reportAt,
1171
+ (element, definition) => {
1172
+ if (definition) usedDefinitions.add(definition);
1173
+ urlReferences.set(
1174
+ element.id,
1175
+ definition ? { kind: "definition", name: definition.name } : { kind: "embedded" }
1176
+ );
1177
+ }
1125
1178
  );
1126
1179
  for (const definition of keptDefinitions) {
1127
1180
  this.keepLine(
@@ -1131,6 +1184,7 @@ var MapReader = class {
1131
1184
  );
1132
1185
  }
1133
1186
  const declaredNames = /* @__PURE__ */ new Set([...this.idBySourceName.keys(), ...this.idByElementName.keys()]);
1187
+ const edgeLineById = /* @__PURE__ */ new Map();
1134
1188
  const edges = buildEdges(
1135
1189
  this.pendingLinks,
1136
1190
  (name) => this.idOfName(name),
@@ -1138,10 +1192,18 @@ var MapReader = class {
1138
1192
  this.keepLine(link.raw, link.lineNo, link.text);
1139
1193
  this.diag(`Link: "${missing}" not found`, link.lineNo, link.text);
1140
1194
  },
1141
- (link) => link.label && this.readText(link.label, link.lineNo, link.text)
1195
+ (link) => link.label && this.readText(link.label, link.lineNo, link.text),
1196
+ (edge, link) => edgeLineById.set(edge.id, link.lineNo)
1142
1197
  );
1143
1198
  const kept = this.passthrough.filter((_, index) => !removedFromPassthrough.has(index));
1144
1199
  const rawPassthrough = withoutClosersOfKeptComments(kept, this.reportAt);
1200
+ const sourceMap = this.sourceMap({
1201
+ removedFromPassthrough,
1202
+ usedDefinitions,
1203
+ urlReferences,
1204
+ edgeLineById,
1205
+ kept
1206
+ });
1145
1207
  const map = compact({
1146
1208
  schemaVersion: wardleySchemaModel.CURRENT_SCHEMA_VERSION,
1147
1209
  config: this.config,
@@ -1151,10 +1213,58 @@ var MapReader = class {
1151
1213
  });
1152
1214
  return {
1153
1215
  result: { map: wardleySchemaModel.validateMap(map), diagnostics: this.diagnostics },
1216
+ sourceMap,
1154
1217
  declaredNames,
1155
1218
  keywordLinks: this.keywordLinks
1156
1219
  };
1157
1220
  }
1221
+ sourceMap(parts) {
1222
+ const primaryLineOf = /* @__PURE__ */ new Map();
1223
+ const secondaryLinesOf = /* @__PURE__ */ new Map();
1224
+ const addSecondary = (key, line) => {
1225
+ const lines = secondaryLinesOf.get(key) ?? [];
1226
+ lines.push(line);
1227
+ secondaryLinesOf.set(key, lines);
1228
+ };
1229
+ const addRepeated = (key, lines) => {
1230
+ if (!lines?.length) return;
1231
+ primaryLineOf.set(key, lines[lines.length - 1]);
1232
+ for (const line of lines.slice(0, -1)) addSecondary(key, line);
1233
+ };
1234
+ for (const [keyword, lines] of this.configLinesByKeyword) {
1235
+ addRepeated(`config:${keyword}`, lines);
1236
+ }
1237
+ for (const unused of this.unusedConfigLines) {
1238
+ if (parts.removedFromPassthrough.has(unused.index)) {
1239
+ addSecondary(`config:${unused.keyword}`, unused.line);
1240
+ }
1241
+ }
1242
+ for (const definition of parts.usedDefinitions) {
1243
+ primaryLineOf.set(`url:${definition.name}`, definition.lineNo);
1244
+ }
1245
+ for (const el of this.elements) {
1246
+ const isChild = el.elementType === "component" && el.pipelineId !== void 0;
1247
+ const key = `${isChild ? "child" : "element"}:${el.id}`;
1248
+ const line = this.declarationLineById.get(el.id);
1249
+ if (line !== void 0) primaryLineOf.set(key, line);
1250
+ for (const methodLine of this.methodLinesById.get(el.id) ?? []) addSecondary(key, methodLine);
1251
+ addRepeated(`evolve:${el.id}`, this.evolveLinesById.get(el.id));
1252
+ }
1253
+ for (const [id, line] of parts.edgeLineById) primaryLineOf.set(`edge:${id}`, line);
1254
+ const blocks = /* @__PURE__ */ new Map();
1255
+ for (const [pipeline, lines] of this.blockLines) {
1256
+ const id = this.pipelineIds.get(pipeline);
1257
+ if (id !== void 0) blocks.set(id, lines);
1258
+ }
1259
+ return {
1260
+ primaryLineOf,
1261
+ secondaryLinesOf,
1262
+ passthrough: parts.kept.map((entry) => ({ line: entry.lineNo, isComment: entry.isComment })),
1263
+ blocks,
1264
+ rangeFromChildren: this.rangeFromChildren,
1265
+ urlReferences: parts.urlReferences
1266
+ };
1267
+ }
1158
1268
  /**
1159
1269
  * A pipeline pairs with the component written under its name: it takes that component's height,
1160
1270
  * as the serializer assumes when it leaves out `(y …)`. Block children are not such components.
@@ -1192,6 +1302,8 @@ var MapReader = class {
1192
1302
  }
1193
1303
  const visibility = p.visibility ?? component?.position.visibility ?? 0.5;
1194
1304
  const id = this.ids.alloc("pipeline", label);
1305
+ this.pipelineIds.set(p, id);
1306
+ if (writtenRange.fromChildren) this.rangeFromChildren.add(id);
1195
1307
  if (this.idOfName(p.name) === void 0 && this.idOfName(label) === void 0) {
1196
1308
  this.register(p.name, label, id);
1197
1309
  }
@@ -1206,9 +1318,9 @@ var MapReader = class {
1206
1318
  this.ids,
1207
1319
  childNames
1208
1320
  );
1209
- this.elements.push(pipeline);
1321
+ this.addElement(pipeline, p.lineNo);
1210
1322
  children.forEach((child, index) => {
1211
- this.elements.push(child);
1323
+ this.addElement(child, p.children[index].lineNo);
1212
1324
  this.register(p.children[index].name, child.label, child.id);
1213
1325
  });
1214
1326
  }
@@ -1236,6 +1348,9 @@ var MapReader = class {
1236
1348
  this.diag(message, ev.lineNo, ev.text);
1237
1349
  }
1238
1350
  evolveLineOf.set(component.id, ev.lineNo);
1351
+ const evolveLines = this.evolveLinesById.get(component.id) ?? [];
1352
+ evolveLines.push(ev.lineNo);
1353
+ this.evolveLinesById.set(component.id, evolveLines);
1239
1354
  const movement = compact({
1240
1355
  targetEvolution: ev.target,
1241
1356
  newLabel: ev.newLabel && this.readText(ev.newLabel, ev.lineNo, ev.text),
@@ -1261,6 +1376,9 @@ var MapReader = class {
1261
1376
  this.diag(message, pm.lineNo, pm.text);
1262
1377
  }
1263
1378
  this.elements[index] = { ...el, decorators: { ...el.decorators, method: pm.method } };
1379
+ const methodLines = this.methodLinesById.get(el.id) ?? [];
1380
+ methodLines.push(pm.lineNo);
1381
+ this.methodLinesById.set(el.id, methodLines);
1264
1382
  }
1265
1383
  }
1266
1384
  };
@@ -1302,7 +1420,7 @@ function replacedConfigLines(unusedLines, config, report) {
1302
1420
  }
1303
1421
  return replacedIndexes;
1304
1422
  }
1305
- function resolveUrlReferences(elements, refs, definitions, report) {
1423
+ function resolveUrlReferences(elements, refs, definitions, report, resolved) {
1306
1424
  const definitionByName = /* @__PURE__ */ new Map();
1307
1425
  const replaced = /* @__PURE__ */ new Set();
1308
1426
  for (const definition of definitions) {
@@ -1329,6 +1447,7 @@ function resolveUrlReferences(elements, refs, definitions, report) {
1329
1447
  const el = elements[index];
1330
1448
  if (el.elementType === "component") elements[index] = { ...el, url: address };
1331
1449
  else if (el.elementType === "submap") elements[index] = { ...el, urlRef: address };
1450
+ resolved(el, definition);
1332
1451
  }
1333
1452
  return definitions.filter((definition) => replaced.has(definition) || !used.has(definition));
1334
1453
  }
@@ -1579,6 +1698,17 @@ function parseEvolve(after, report) {
1579
1698
  labelOffset: lo.labelOffset ?? void 0
1580
1699
  });
1581
1700
  }
1701
+ function sizeOutsideLimits(width, height) {
1702
+ const below = width < wardleySchemaModel.PLOT_MIN.width || height < wardleySchemaModel.PLOT_MIN.height;
1703
+ const above = width > wardleySchemaModel.PLOT_MAX.width || height > wardleySchemaModel.PLOT_MAX.height;
1704
+ if (!below && !above) return null;
1705
+ const drawnWidth = wardleySchemaModel.drawnPlotLength(width, "width");
1706
+ const drawnHeight = wardleySchemaModel.drawnPlotLength(height, "height");
1707
+ const minimum = `${wardleySchemaModel.PLOT_MIN.width}\xD7${wardleySchemaModel.PLOT_MIN.height}`;
1708
+ const maximum = `${wardleySchemaModel.PLOT_MAX.width}\xD7${wardleySchemaModel.PLOT_MAX.height}`;
1709
+ const limit = below && above ? `outside ${minimum} to ${maximum}` : below ? `below the minimum ${minimum}` : `above the maximum ${maximum}`;
1710
+ return `Map size ${width}\xD7${height} is ${limit} \u2014 drawn at ${drawnWidth}\xD7${drawnHeight}`;
1711
+ }
1582
1712
 
1583
1713
  // src/serializer.ts
1584
1714
  function urlOf(el) {
@@ -1601,16 +1731,44 @@ function uniqueNames(map) {
1601
1731
  }
1602
1732
  return byId;
1603
1733
  }
1604
- function urlDefinitionNames(map, nameOf, keptDefinitionNames) {
1734
+ var EMBEDDABLE_ADDRESS_RE = /^[a-z][\w+.-]*:\/\/[^\s()]*$/i;
1735
+ function planUrls(map, nameOf, keptDefinitionNames, hints) {
1605
1736
  const namesInUse = new UniqueNames();
1606
1737
  for (const name of keptDefinitionNames) namesInUse.claim(name);
1607
- const byId = /* @__PURE__ */ new Map();
1738
+ const addressByHintedName = /* @__PURE__ */ new Map();
1739
+ const definitionNameById = /* @__PURE__ */ new Map();
1740
+ const referenceById = /* @__PURE__ */ new Map();
1741
+ for (const el of map.elements) {
1742
+ const address = urlOf(el);
1743
+ const hint = hints?.get(el.id);
1744
+ if (!address || !hint) continue;
1745
+ if (hint.kind === "embedded") {
1746
+ if (EMBEDDABLE_ADDRESS_RE.test(address)) referenceById.set(el.id, address);
1747
+ continue;
1748
+ }
1749
+ const hintedAddress = addressByHintedName.get(hint.name);
1750
+ if (hintedAddress === void 0) {
1751
+ addressByHintedName.set(hint.name, address);
1752
+ namesInUse.reserve(hint.name);
1753
+ } else if (hintedAddress !== address) continue;
1754
+ definitionNameById.set(el.id, hint.name);
1755
+ }
1756
+ const definitions = [];
1757
+ const written = /* @__PURE__ */ new Set();
1608
1758
  for (const el of map.elements) {
1609
- if (!urlOf(el)) continue;
1610
- const readable = nameOf(el).replace(/[()[\]<>]/g, " ").replace(/\s+/g, " ").trim();
1611
- byId.set(el.id, namesInUse.claim(`${readable} URL`.trim()));
1759
+ const address = urlOf(el);
1760
+ if (!address || referenceById.has(el.id)) continue;
1761
+ let name = definitionNameById.get(el.id);
1762
+ if (name === void 0) {
1763
+ const readable = nameOf(el).replace(/[()[\]<>]/g, " ").replace(/\s+/g, " ").trim();
1764
+ name = namesInUse.claim(`${readable} URL`.trim());
1765
+ }
1766
+ referenceById.set(el.id, name);
1767
+ if (written.has(name)) continue;
1768
+ written.add(name);
1769
+ definitions.push({ name, address });
1612
1770
  }
1613
- return byId;
1771
+ return { definitions, referenceById };
1614
1772
  }
1615
1773
  function offsetSuffix(lo) {
1616
1774
  return lo ? ` label [${r(lo.dx)}, ${r(lo.dy)}]` : "";
@@ -1630,20 +1788,21 @@ function decoratorSuffix(dec) {
1630
1788
  return out;
1631
1789
  }
1632
1790
  function configLines(config) {
1633
- const lines = [`title ${writtenText(config.title)}`];
1634
- if (config.style) lines.push(`style ${config.style}`);
1635
- if (config.size) lines.push(`size [${r(config.size.width)}, ${r(config.size.height)}]`);
1791
+ const lines = [{ key: "config:title", text: `title ${writtenText(config.title)}` }];
1792
+ const add = (keyword, text) => lines.push({ key: `config:${keyword}`, text });
1793
+ if (config.style) add("style", `style ${config.style}`);
1794
+ if (config.size) add("size", `size [${r(config.size.width)}, ${r(config.size.height)}]`);
1636
1795
  if (config.evolutionLabels) {
1637
- lines.push(`evolution ${config.evolutionLabels.map(writtenText).join("->")}`);
1796
+ add("evolution", `evolution ${config.evolutionLabels.map(writtenText).join("->")}`);
1638
1797
  }
1639
1798
  if (config.yAxisLabel) {
1640
1799
  const ends = config.yAxisEndLabels;
1641
1800
  const endLabels = ends ? `->${writtenText(ends[0])}->${writtenText(ends[1])}` : "";
1642
- lines.push(`y-axis ${writtenText(config.yAxisLabel)}${endLabels}`);
1801
+ add("y-axis", `y-axis ${writtenText(config.yAxisLabel)}${endLabels}`);
1643
1802
  }
1644
1803
  if (config.annotationsBoxPosition) {
1645
1804
  const b = config.annotationsBoxPosition;
1646
- lines.push(`annotations [${r(b.visibility)}, ${r(b.evolution)}]`);
1805
+ add("annotations", `annotations [${r(b.visibility)}, ${r(b.evolution)}]`);
1647
1806
  }
1648
1807
  return lines;
1649
1808
  }
@@ -1653,16 +1812,16 @@ function passthroughLines(map) {
1653
1812
  if (map.config.yAxisLabel) emitted.add("y-axis");
1654
1813
  const comments = new CommentStripper();
1655
1814
  const lines = [];
1656
- const urlDefinitionNames2 = [];
1657
- for (const raw of map.rawPassthrough ?? []) {
1815
+ const urlDefinitionNames = [];
1816
+ (map.rawPassthrough ?? []).forEach((raw, index) => {
1658
1817
  const code = comments.strip(raw).code;
1659
1818
  const keyword = keywordOf(code);
1660
- if (emitted.has(keyword)) continue;
1661
- lines.push(raw);
1819
+ if (emitted.has(keyword)) return;
1820
+ lines.push({ key: `raw:${index}`, text: raw });
1662
1821
  const definition = keyword === "url" ? readUrlDefinition(code.trim().slice(3)) : null;
1663
- if (definition) urlDefinitionNames2.push(definition.name);
1664
- }
1665
- return { lines, urlDefinitionNames: urlDefinitionNames2 };
1822
+ if (definition) urlDefinitionNames.push(definition.name);
1823
+ });
1824
+ return { lines, urlDefinitionNames };
1666
1825
  }
1667
1826
  function pipelineHeights(map, pipelineIds, nameOf) {
1668
1827
  const componentVisibilityByName = /* @__PURE__ */ new Map();
@@ -1684,24 +1843,27 @@ function edgeLines(map, nameOf) {
1684
1843
  const from = namesById.get(edge.from) ?? edge.from;
1685
1844
  const to = namesById.get(edge.to) ?? edge.to;
1686
1845
  const annotation = edge.label ? `; ${writtenText(edge.label)}` : "";
1687
- if (edge.edgeType === "dependency") return `${from} -> ${to}${annotation}`;
1846
+ const key = `edge:${edge.id}`;
1847
+ if (edge.edgeType === "dependency") return { key, text: `${from} -> ${to}${annotation}` };
1688
1848
  const op = edge.flowValue ? `+'${edge.flowValue}'${edge.bidirectional ? "<>" : ">"}` : edge.bidirectional ? "+<>" : "+>";
1689
- return `${from} ${op} ${to}${annotation}`;
1849
+ return { key, text: `${from} ${op} ${to}${annotation}` };
1690
1850
  });
1691
1851
  }
1692
1852
  function serializeDSL(map) {
1853
+ return [...serializeKeyedLines(map).map((line) => line.text), ""].join("\n");
1854
+ }
1855
+ function serializeKeyedLines(map, hints = {}) {
1693
1856
  const names = uniqueNames(map);
1694
1857
  const nameOf = (el) => names.get(el.id) ?? writtenText(el.label);
1695
1858
  const passthrough = passthroughLines(map);
1696
1859
  const lines = configLines(map.config);
1697
- const urlDefNames = urlDefinitionNames(map, nameOf, passthrough.urlDefinitionNames);
1698
- for (const el of map.elements) {
1699
- const defName = urlDefNames.get(el.id);
1700
- if (defName) lines.push(`url ${defName} [${urlOf(el)}]`);
1860
+ const urls = planUrls(map, nameOf, passthrough.urlDefinitionNames, hints.urlReferences);
1861
+ for (const { name, address } of urls.definitions) {
1862
+ lines.push({ key: `url:${name}`, text: `url ${name} [${address}]` });
1701
1863
  }
1702
1864
  const urlSuffix = (el) => {
1703
- const def = urlDefNames.get(el.id);
1704
- return def ? ` url(${def})` : "";
1865
+ const reference = urls.referenceById.get(el.id);
1866
+ return reference ? ` url(${reference})` : "";
1705
1867
  };
1706
1868
  const pipelineIds = new Set(
1707
1869
  map.elements.filter((e) => e.elementType === "pipeline").map((e) => e.id)
@@ -1719,24 +1881,28 @@ function serializeDSL(map) {
1719
1881
  const evolveLines = [];
1720
1882
  for (const el of map.elements) {
1721
1883
  if (el.elementType === "component" && el.movement) {
1722
- evolveLines.push(evolveLine(el, nameOf(el)));
1884
+ evolveLines.push({ key: `evolve:${el.id}`, text: evolveLine(el, nameOf(el)) });
1723
1885
  }
1724
1886
  if (el.elementType === "component" && el.pipelineId && pipelineIds.has(el.pipelineId)) {
1725
1887
  continue;
1726
1888
  }
1727
- lines.push(elementLine(el, nameOf(el)) + pipelineHeight(el) + urlSuffix(el));
1889
+ lines.push({
1890
+ key: `element:${el.id}`,
1891
+ text: elementLine(el, nameOf(el)) + pipelineHeight(el) + urlSuffix(el)
1892
+ });
1728
1893
  const kids = childrenByPipeline.get(el.id) ?? [];
1729
1894
  if (kids.length) {
1730
- lines.push("{");
1895
+ lines.push({ key: `block-open:${el.id}`, text: "{" });
1731
1896
  for (const k of kids) {
1732
- lines.push(
1733
- ` component ${nameOf(k)} [${r(k.position.evolution)}]${decoratorSuffix(k.decorators)}${colorSuffix(k)}${offsetSuffix(k.labelOffset)}`
1734
- );
1897
+ lines.push({
1898
+ key: `child:${k.id}`,
1899
+ text: ` component ${nameOf(k)} [${r(k.position.evolution)}]${decoratorSuffix(k.decorators)}${colorSuffix(k)}${offsetSuffix(k.labelOffset)}`
1900
+ });
1735
1901
  }
1736
- lines.push("}");
1902
+ lines.push({ key: `block-close:${el.id}`, text: "}" });
1737
1903
  }
1738
1904
  }
1739
- return [...lines, ...evolveLines, ...edgeLines(map, nameOf), ...passthrough.lines, ""].join("\n");
1905
+ return [...lines, ...evolveLines, ...edgeLines(map, nameOf), ...passthrough.lines];
1740
1906
  }
1741
1907
  function elementLine(el, name) {
1742
1908
  const p = el.position;
@@ -1774,6 +1940,593 @@ function evolveLine(el, name) {
1774
1940
  return `evolve ${label} ${r(mv.targetEvolution)}${method}${offsetSuffix(mv.labelOffset)}`;
1775
1941
  }
1776
1942
 
1943
+ // src/patch.ts
1944
+ function patchDSL(originalText, map, options = {}) {
1945
+ return patchDSLWithOutcome(originalText, map, options).text;
1946
+ }
1947
+ function patchDSLWithOutcome(originalText, map, options = {}) {
1948
+ const original = typeof originalText === "string" ? originalText : "";
1949
+ let canonical;
1950
+ try {
1951
+ canonical = serializeDSL(map);
1952
+ } catch {
1953
+ return { text: original, outcome: "unchanged" };
1954
+ }
1955
+ try {
1956
+ const expected = fingerprint(parseDSL(canonical));
1957
+ const source = readSource(original);
1958
+ const edited = alignToSource(source.map, map, options.previous);
1959
+ for (const mode of ["minimal", "rewrite"]) {
1960
+ const text = patchLines(source, edited, mode);
1961
+ if (text !== null && fingerprint(parseDSL(text)) === expected) {
1962
+ return { text, outcome: mode === "minimal" ? "patched" : "rewritten" };
1963
+ }
1964
+ }
1965
+ return { text: withLineBreaksOf(source, canonical), outcome: "serialized" };
1966
+ } catch {
1967
+ return { text: canonical, outcome: "serialized" };
1968
+ }
1969
+ }
1970
+ function readSource(text) {
1971
+ const lines = splitLines(text);
1972
+ const parsed = parseDSLWithSourceMap(text);
1973
+ const crlf = (text.match(/\r\n/g) ?? []).length;
1974
+ const lf = (text.match(/\n/g) ?? []).length - crlf;
1975
+ return {
1976
+ lines,
1977
+ shapes: shapeLines(lines),
1978
+ map: parsed.map,
1979
+ sourceMap: parsed.sourceMap,
1980
+ lineBreak: crlf > lf ? "\r\n" : "\n",
1981
+ endsWithLineBreak: text === "" || text.endsWith("\n")
1982
+ };
1983
+ }
1984
+ function splitLines(text) {
1985
+ const lines = [];
1986
+ const lineBreaks = /\r?\n/g;
1987
+ let start = 0;
1988
+ for (let match = lineBreaks.exec(text); match; match = lineBreaks.exec(text)) {
1989
+ lines.push({ text: text.slice(start, match.index), lineBreak: match[0] });
1990
+ start = match.index + match[0].length;
1991
+ }
1992
+ if (start < text.length) lines.push({ text: text.slice(start), lineBreak: "" });
1993
+ return lines;
1994
+ }
1995
+ function withoutTrailingCarriageReturns2(text) {
1996
+ let end = text.length;
1997
+ while (end > 0 && text.charCodeAt(end - 1) === 13) end--;
1998
+ return text.slice(0, end);
1999
+ }
2000
+ function shapeLines(lines) {
2001
+ const comments = new CommentStripper();
2002
+ return lines.map(({ text }) => {
2003
+ const raw = withoutTrailingCarriageReturns2(text);
2004
+ const startsInBlockComment = comments.insideBlockComment;
2005
+ let fragments = comments.strip(raw).comments;
2006
+ let rest = raw;
2007
+ let commentHead = "";
2008
+ let commentTail = "";
2009
+ let editable = true;
2010
+ if (startsInBlockComment) {
2011
+ const head = fragments[0];
2012
+ if (head?.endsWith("*/") && raw.startsWith(head)) {
2013
+ commentHead = head;
2014
+ rest = raw.slice(head.length);
2015
+ fragments = fragments.slice(1);
2016
+ } else {
2017
+ editable = false;
2018
+ }
2019
+ }
2020
+ let codePart = rest;
2021
+ if (editable && fragments.length) {
2022
+ const comment = fragments[0];
2023
+ if (fragments.length > 1 || !rest.endsWith(comment)) {
2024
+ editable = false;
2025
+ } else {
2026
+ const beforeComment = rest.slice(0, rest.length - comment.length);
2027
+ codePart = beforeComment.trimEnd();
2028
+ commentTail = beforeComment.slice(codePart.length) + comment;
2029
+ }
2030
+ }
2031
+ return {
2032
+ indent: /^\s*/.exec(codePart)[0],
2033
+ code: codePart.trim(),
2034
+ commentHead,
2035
+ commentTail,
2036
+ editable,
2037
+ endsInBlockComment: comments.insideBlockComment
2038
+ };
2039
+ });
2040
+ }
2041
+ function withLineBreaksOf(source, text) {
2042
+ if (source.lines.length === 0) return text;
2043
+ const withBreaks = source.lineBreak === "\n" ? text : text.replace(/\n/g, source.lineBreak);
2044
+ return source.endsWithLineBreak ? withBreaks : withBreaks.replace(/\r?\n$/, "");
2045
+ }
2046
+ function identityOf(el) {
2047
+ switch (el.elementType) {
2048
+ case "annotation":
2049
+ return `annotation\0${el.number}`;
2050
+ case "attitude":
2051
+ return `attitude\0${el.kind}`;
2052
+ default:
2053
+ return `${el.elementType}\0${el.label}`;
2054
+ }
2055
+ }
2056
+ function edgeIdentityOf(edge) {
2057
+ return `${edge.edgeType}\0${edge.from}\0${edge.to}`;
2058
+ }
2059
+ function matchElements(source, edited, hint = /* @__PURE__ */ new Map()) {
2060
+ const unmatchedSource = new Map(source.elements.map((el) => [el.id, el]));
2061
+ const idByEditedId = /* @__PURE__ */ new Map();
2062
+ const claim = (editedId, sourceId) => {
2063
+ idByEditedId.set(editedId, sourceId);
2064
+ unmatchedSource.delete(sourceId);
2065
+ };
2066
+ const unclaimed = () => edited.elements.filter((el) => !idByEditedId.has(el.id));
2067
+ for (const el of edited.elements) {
2068
+ const same = unmatchedSource.get(el.id);
2069
+ if (same && identityOf(same) === identityOf(el)) claim(el.id, el.id);
2070
+ }
2071
+ for (const el of unclaimed()) {
2072
+ const hinted = hint.get(el.id);
2073
+ const candidate = hinted === void 0 ? void 0 : unmatchedSource.get(hinted);
2074
+ if (candidate?.elementType === el.elementType) claim(el.id, candidate.id);
2075
+ }
2076
+ const sourceIdsByIdentity = /* @__PURE__ */ new Map();
2077
+ for (const el of unmatchedSource.values()) {
2078
+ const ids = sourceIdsByIdentity.get(identityOf(el)) ?? [];
2079
+ ids.push(el.id);
2080
+ sourceIdsByIdentity.set(identityOf(el), ids);
2081
+ }
2082
+ for (const el of unclaimed()) {
2083
+ const match = sourceIdsByIdentity.get(identityOf(el))?.shift();
2084
+ if (match !== void 0) claim(el.id, match);
2085
+ }
2086
+ for (const el of unclaimed()) {
2087
+ if (unmatchedSource.get(el.id)?.elementType === el.elementType) claim(el.id, el.id);
2088
+ }
2089
+ return idByEditedId;
2090
+ }
2091
+ function alignToSource(source, edited, previous) {
2092
+ const hint = previous ? matchElements(source, previous) : void 0;
2093
+ const idByEditedId = matchElements(source, edited, hint);
2094
+ const usedIds = /* @__PURE__ */ new Set([...source.elements.map((el) => el.id), ...idByEditedId.values()]);
2095
+ const freshId = (id) => {
2096
+ let candidate = id;
2097
+ for (let suffix = 2; usedIds.has(candidate); suffix++) candidate = `${id}~${suffix}`;
2098
+ usedIds.add(candidate);
2099
+ return candidate;
2100
+ };
2101
+ for (const el of edited.elements) {
2102
+ if (!idByEditedId.has(el.id)) idByEditedId.set(el.id, freshId(el.id));
2103
+ }
2104
+ const rename = (id) => idByEditedId.get(id) ?? id;
2105
+ const elements = edited.elements.map((el) => {
2106
+ const renamed = { ...el, id: rename(el.id) };
2107
+ if (renamed.elementType === "component" && renamed.pipelineId !== void 0) {
2108
+ return { ...renamed, pipelineId: rename(renamed.pipelineId) };
2109
+ }
2110
+ if (renamed.elementType === "pipeline") {
2111
+ return { ...renamed, childIds: renamed.childIds.map(rename) };
2112
+ }
2113
+ return renamed;
2114
+ });
2115
+ const edges = alignEdges(
2116
+ source.edges,
2117
+ edited.edges.map((edge) => ({ ...edge, from: rename(edge.from), to: rename(edge.to) }))
2118
+ );
2119
+ return {
2120
+ ...edited,
2121
+ elements: inSourceOrder(elements, source.elements),
2122
+ edges: inSourceOrder(edges, source.edges)
2123
+ };
2124
+ }
2125
+ function alignEdges(sourceEdges, editedEdges) {
2126
+ const unmatchedSource = new Map(sourceEdges.map((edge) => [edge.id, edge]));
2127
+ const idByIndex = /* @__PURE__ */ new Map();
2128
+ const claim = (index, id) => {
2129
+ idByIndex.set(index, id);
2130
+ unmatchedSource.delete(id);
2131
+ };
2132
+ editedEdges.forEach((edge, index) => {
2133
+ const same = unmatchedSource.get(edge.id);
2134
+ if (same && edgeIdentityOf(same) === edgeIdentityOf(edge)) claim(index, edge.id);
2135
+ });
2136
+ editedEdges.forEach((edge, index) => {
2137
+ if (idByIndex.has(index)) return;
2138
+ const match = [...unmatchedSource.values()].find(
2139
+ (candidate) => edgeIdentityOf(candidate) === edgeIdentityOf(edge)
2140
+ );
2141
+ if (match) claim(index, match.id);
2142
+ });
2143
+ editedEdges.forEach((edge, index) => {
2144
+ if (idByIndex.has(index)) return;
2145
+ if (unmatchedSource.get(edge.id)?.edgeType === edge.edgeType) claim(index, edge.id);
2146
+ });
2147
+ const usedIds = /* @__PURE__ */ new Set([...sourceEdges.map((edge) => edge.id), ...idByIndex.values()]);
2148
+ return editedEdges.map((edge, index) => {
2149
+ let id = idByIndex.get(index);
2150
+ if (id === void 0) {
2151
+ id = edge.id;
2152
+ for (let suffix = 2; usedIds.has(id); suffix++) id = `${edge.id}~${suffix}`;
2153
+ usedIds.add(id);
2154
+ }
2155
+ return { ...edge, id };
2156
+ });
2157
+ }
2158
+ function inSourceOrder(items, sourceItems) {
2159
+ const sourceIndex = new Map(sourceItems.map((item, index) => [item.id, index]));
2160
+ const position = (item) => sourceIndex.get(item.id) ?? Number.POSITIVE_INFINITY;
2161
+ return items.map((item, index) => ({ item, index })).sort((a, b) => position(a.item) - position(b.item) || a.index - b.index).map(({ item }) => item);
2162
+ }
2163
+ var isRawKey = (key) => key.startsWith("raw:");
2164
+ function keyedTexts(lines) {
2165
+ return new Map(lines.filter((line) => !isRawKey(line.key)).map((line) => [line.key, line.text]));
2166
+ }
2167
+ function listsByLine(lineCount) {
2168
+ return Array.from({ length: lineCount }, () => []);
2169
+ }
2170
+ function diffPassthrough(before, after) {
2171
+ const remainingByText = /* @__PURE__ */ new Map();
2172
+ for (const line of before.filter((entry) => isRawKey(entry.key))) {
2173
+ const indexes = remainingByText.get(line.text) ?? [];
2174
+ indexes.push(Number(line.key.slice("raw:".length)));
2175
+ remainingByText.set(line.text, indexes);
2176
+ }
2177
+ const added = [];
2178
+ for (const line of after.filter((entry) => isRawKey(entry.key))) {
2179
+ if (remainingByText.get(line.text)?.shift() === void 0) added.push(line);
2180
+ }
2181
+ return { removed: new Set([...remainingByText.values()].flat()), added };
2182
+ }
2183
+ function pipelineOfEachChild(map) {
2184
+ const pipelineById = /* @__PURE__ */ new Map();
2185
+ for (const el of map.elements) {
2186
+ if (el.elementType === "component" && el.pipelineId !== void 0) {
2187
+ pipelineById.set(el.id, el.pipelineId);
2188
+ }
2189
+ }
2190
+ return pipelineById;
2191
+ }
2192
+ function reparentedChildKeys(source, edited) {
2193
+ const pipelineBefore = pipelineOfEachChild(source);
2194
+ const keys = /* @__PURE__ */ new Set();
2195
+ for (const [id, pipelineId] of pipelineOfEachChild(edited)) {
2196
+ const previous = pipelineBefore.get(id);
2197
+ if (previous !== void 0 && previous !== pipelineId) keys.add(`child:${id}`);
2198
+ }
2199
+ return keys;
2200
+ }
2201
+ function pipelinesNoLongerSpannedByChildren(edited, rangeFromChildren) {
2202
+ const keys = /* @__PURE__ */ new Set();
2203
+ if (rangeFromChildren.size === 0) return keys;
2204
+ const evolutionsByPipeline = /* @__PURE__ */ new Map();
2205
+ for (const el of edited.elements) {
2206
+ if (el.elementType !== "component" || el.pipelineId === void 0) continue;
2207
+ const evolutions = evolutionsByPipeline.get(el.pipelineId) ?? [];
2208
+ evolutions.push(roundAsWritten(el.position.evolution));
2209
+ evolutionsByPipeline.set(el.pipelineId, evolutions);
2210
+ }
2211
+ for (const el of edited.elements) {
2212
+ if (el.elementType !== "pipeline" || !rangeFromChildren.has(el.id)) continue;
2213
+ const evolutions = evolutionsByPipeline.get(el.id) ?? [];
2214
+ const read = evolutions.length > 0 ? widenEmptyRange({ start: Math.min(...evolutions), end: Math.max(...evolutions) }) : void 0;
2215
+ const spanned = read !== void 0 && read.start === roundAsWritten(el.evolutionStart) && read.end === roundAsWritten(el.evolutionEnd);
2216
+ if (!spanned) keys.add(`element:${el.id}`);
2217
+ }
2218
+ return keys;
2219
+ }
2220
+ function patchLines(source, edited, mode) {
2221
+ const { lines, shapes, sourceMap } = source;
2222
+ const hints = { urlReferences: sourceMap.urlReferences };
2223
+ const before = serializeKeyedLines(source.map, hints);
2224
+ const after = serializeKeyedLines(edited, hints);
2225
+ const textBefore = keyedTexts(before);
2226
+ const textAfter = keyedTexts(after);
2227
+ const passthrough = diffPassthrough(before, after);
2228
+ const reparented = reparentedChildKeys(source.map, edited);
2229
+ const unspanned = pipelinesNoLongerSpannedByChildren(edited, sourceMap.rangeFromChildren);
2230
+ const statusOf = (key) => {
2231
+ const written = textAfter.get(key);
2232
+ if (written === void 0 || reparented.has(key)) return "deleted";
2233
+ if (mode === "rewrite" || written !== textBefore.get(key) || unspanned.has(key)) {
2234
+ return "changed";
2235
+ }
2236
+ return "same";
2237
+ };
2238
+ const primaryKeysByLine = listsByLine(lines.length);
2239
+ for (const [key, line] of sourceMap.primaryLineOf) {
2240
+ if (textBefore.has(key)) primaryKeysByLine[line - 1]?.push(key);
2241
+ }
2242
+ const secondaryKeysByLine = listsByLine(lines.length);
2243
+ for (const [key, secondaryLines] of sourceMap.secondaryLinesOf) {
2244
+ if (!textBefore.has(key)) continue;
2245
+ for (const line of secondaryLines) secondaryKeysByLine[line - 1]?.push(key);
2246
+ }
2247
+ const passthroughByLine = listsByLine(lines.length);
2248
+ sourceMap.passthrough.forEach((entry, index) => passthroughByLine[entry.line - 1]?.push(index));
2249
+ const bracesByLine = listsByLine(lines.length);
2250
+ for (const [pipelineId, block] of sourceMap.blocks) {
2251
+ if (block.open !== void 0) {
2252
+ bracesByLine[block.open - 1]?.push({ pipelineId, end: "open" });
2253
+ }
2254
+ if (block.close !== void 0) {
2255
+ bracesByLine[block.close - 1]?.push({ pipelineId, end: "close" });
2256
+ }
2257
+ }
2258
+ const braceRemoved = (pipelineId) => !textAfter.has(`block-open:${pipelineId}`) && (textBefore.has(`block-open:${pipelineId}`) || !textAfter.has(`element:${pipelineId}`));
2259
+ const result = [];
2260
+ for (let index = 0; index < lines.length; index++) {
2261
+ const patched = patchLine(index);
2262
+ if (patched === void 0) return null;
2263
+ result.push(patched);
2264
+ }
2265
+ function patchLine(index) {
2266
+ const line = lines[index];
2267
+ const shape = shapes[index];
2268
+ const primaries = primaryKeysByLine[index];
2269
+ const removedPassthrough = passthroughByLine[index].filter(
2270
+ (entry) => passthrough.removed.has(entry)
2271
+ );
2272
+ const braces = bracesByLine[index];
2273
+ const changed = primaries.some((key) => statusOf(key) !== "same") || secondaryKeysByLine[index].some((key) => statusOf(key) !== "same") || removedPassthrough.length > 0 || braces.some((brace) => braceRemoved(brace.pipelineId));
2274
+ if (!changed) return line.text;
2275
+ if (!shape.editable || primaries.length > 1) return void 0;
2276
+ if (removedPassthrough.some((entry) => sourceMap.passthrough[entry].isComment)) {
2277
+ return void 0;
2278
+ }
2279
+ let statement = null;
2280
+ const primary = primaries[0];
2281
+ if (primary !== void 0) {
2282
+ const status = statusOf(primary);
2283
+ if (status === "changed") statement = textAfter.get(primary).trim();
2284
+ else if (status === "deleted") statement = "";
2285
+ } else if (secondaryKeysByLine[index].length || removedPassthrough.length) {
2286
+ statement = "";
2287
+ }
2288
+ const open = braces.find((brace) => brace.end === "open");
2289
+ const close = braces.find((brace) => brace.end === "close");
2290
+ let rest = shape.code;
2291
+ let prefix = "";
2292
+ let suffix = "";
2293
+ if (open && primary !== `element:${open.pipelineId}` && rest.startsWith("{")) {
2294
+ prefix = braceRemoved(open.pipelineId) ? "" : "{";
2295
+ rest = rest.slice(1).trimStart();
2296
+ } else if (open && rest.endsWith("{")) {
2297
+ suffix = braceRemoved(open.pipelineId) ? "" : "{";
2298
+ rest = rest.slice(0, -1).trimEnd();
2299
+ } else if (open) return void 0;
2300
+ if (close && rest.endsWith("}") && !(open && suffix)) {
2301
+ suffix = braceRemoved(close.pipelineId) ? "" : "}";
2302
+ rest = rest.slice(0, -1).trimEnd();
2303
+ } else if (close) return void 0;
2304
+ const assembled = [prefix, statement ?? rest, suffix].filter(Boolean).join(" ");
2305
+ if (assembled) return shape.commentHead + shape.indent + assembled + shape.commentTail;
2306
+ if (shape.commentHead) return shape.commentHead + shape.commentTail;
2307
+ return shape.commentTail ? shape.indent + shape.commentTail.trimStart() : null;
2308
+ }
2309
+ const insertions = planInsertions(source, edited, after, textBefore, textAfter, reparented);
2310
+ if (!insertions) return null;
2311
+ passthrough.added.forEach((line, index) => {
2312
+ addInsertion(insertions, lines.length - 1, line.text, after.length + index);
2313
+ });
2314
+ return assemble(source, result, insertions);
2315
+ }
2316
+ function addInsertion(insertions, afterLine, text, order) {
2317
+ const list = insertions.get(afterLine) ?? [];
2318
+ list.push({ text, order });
2319
+ insertions.set(afterLine, list);
2320
+ }
2321
+ function planInsertions(source, edited, after, textBefore, textAfter, reparented) {
2322
+ const { lines, shapes, sourceMap } = source;
2323
+ const lastLine = lines.length - 1;
2324
+ const lineOf = (key) => {
2325
+ const line = sourceMap.primaryLineOf.get(key);
2326
+ return line === void 0 ? void 0 : line - 1;
2327
+ };
2328
+ const pipelineLineOf = (pipelineId) => lineOf(`element:${pipelineId}`);
2329
+ const childLinesOf = (pipelineId) => source.map.elements.flatMap((el) => {
2330
+ if (el.elementType !== "component" || el.pipelineId !== pipelineId) return [];
2331
+ const line = lineOf(`child:${el.id}`);
2332
+ return line === void 0 ? [] : [line];
2333
+ });
2334
+ const blockEndOf = (pipelineId) => {
2335
+ const block = sourceMap.blocks.get(pipelineId);
2336
+ if (block?.open === void 0 || pipelineLineOf(pipelineId) === void 0) return void 0;
2337
+ if (block.close !== void 0) return block.close - 1;
2338
+ return Math.max(block.open - 1, ...childLinesOf(pipelineId));
2339
+ };
2340
+ const anchorInBlock = (pipelineId) => {
2341
+ const block = sourceMap.blocks.get(pipelineId);
2342
+ let anchor;
2343
+ if (block.close === void 0) anchor = blockEndOf(pipelineId);
2344
+ else if (block.close !== block.open && shapes[block.close - 1].code === "}") {
2345
+ anchor = block.close - 2;
2346
+ } else return null;
2347
+ return shapes[anchor]?.endsInBlockComment ? null : anchor;
2348
+ };
2349
+ const hasSourceBlock = (pipelineId) => blockEndOf(pipelineId) !== void 0;
2350
+ const endOf = (key) => {
2351
+ const line = lineOf(key);
2352
+ if (line === void 0 || !key.startsWith("element:")) return line;
2353
+ return Math.max(line, blockEndOf(key.slice("element:".length)) ?? line);
2354
+ };
2355
+ const sourceElementTypes = new Map(source.map.elements.map((el) => [el.id, el.elementType]));
2356
+ const editedElementById = new Map(edited.elements.map((el) => [el.id, el]));
2357
+ const lastLineOf = (accepts) => {
2358
+ let last;
2359
+ for (const key of sourceMap.primaryLineOf.keys()) {
2360
+ if (!accepts(key) || !textBefore.has(key)) continue;
2361
+ const end = endOf(key);
2362
+ if (end !== void 0 && (last === void 0 || end > last)) last = end;
2363
+ }
2364
+ return last;
2365
+ };
2366
+ const isElementOfType = (type) => (key) => key.startsWith("element:") && sourceElementTypes.get(key.slice("element:".length)) === type;
2367
+ const startsWith = (prefix) => (key) => key.startsWith(prefix);
2368
+ const settle = (line) => {
2369
+ if (line < 0) return -1;
2370
+ let settled = line;
2371
+ for (const pipelineId of sourceMap.blocks.keys()) {
2372
+ const start = pipelineLineOf(pipelineId);
2373
+ const end = blockEndOf(pipelineId);
2374
+ if (start === void 0 || end === void 0) continue;
2375
+ if (settled >= start && settled < end) settled = end;
2376
+ }
2377
+ while (settled < lastLine && shapes[settled].endsInBlockComment) settled++;
2378
+ return settled;
2379
+ };
2380
+ const firstOf = (...candidates) => {
2381
+ for (const candidate of candidates) if (candidate !== void 0) return candidate;
2382
+ return lastLine;
2383
+ };
2384
+ const needsInsertion = (key) => reparented.has(key) || !sourceMap.primaryLineOf.has(key) && textBefore.get(key) !== textAfter.get(key);
2385
+ const anchorOfElement = (id) => {
2386
+ const type = editedElementById.get(id)?.elementType ?? "";
2387
+ return settle(
2388
+ firstOf(
2389
+ lastLineOf(isElementOfType(type)),
2390
+ lastLineOf(startsWith("element:")),
2391
+ lastLineOf(startsWith("url:")),
2392
+ lastLineOf(startsWith("config:"))
2393
+ )
2394
+ );
2395
+ };
2396
+ const anchorOfNewBlock = (pipelineId) => pipelineLineOf(pipelineId) ?? anchorOfElement(pipelineId);
2397
+ const insertions = /* @__PURE__ */ new Map();
2398
+ for (let order = 0; order < after.length; order++) {
2399
+ const { key, text } = after[order];
2400
+ if (isRawKey(key)) continue;
2401
+ const separator = key.indexOf(":");
2402
+ const kind = key.slice(0, separator);
2403
+ const id = key.slice(separator + 1);
2404
+ let anchor;
2405
+ if (kind === "block-open" || kind === "block-close") {
2406
+ if (textBefore.has(`block-open:${id}`)) continue;
2407
+ if (hasSourceBlock(id)) {
2408
+ if (anchorInBlock(id) === null) return null;
2409
+ continue;
2410
+ }
2411
+ anchor = anchorOfNewBlock(id);
2412
+ } else if (!needsInsertion(key)) {
2413
+ continue;
2414
+ } else if (kind === "config") {
2415
+ anchor = settle(firstOf(lastLineOf(startsWith("config:")), -1));
2416
+ } else if (kind === "url") {
2417
+ const lastUrl = lastLineOf(startsWith("url:"));
2418
+ anchor = settle(firstOf(lastUrl, lastLineOf(startsWith("config:")), -1));
2419
+ } else if (kind === "element") {
2420
+ anchor = anchorOfElement(id);
2421
+ } else if (kind === "child") {
2422
+ const child = editedElementById.get(id);
2423
+ const pipelineId = child?.elementType === "component" ? child.pipelineId : void 0;
2424
+ if (pipelineId === void 0) return null;
2425
+ anchor = hasSourceBlock(pipelineId) ? anchorInBlock(pipelineId) : anchorOfNewBlock(pipelineId);
2426
+ } else if (kind === "evolve") {
2427
+ anchor = settle(firstOf(lastLineOf(startsWith("evolve:")), endOf(`element:${id}`)));
2428
+ } else if (kind === "edge") {
2429
+ anchor = settle(firstOf(lastLineOf(startsWith("edge:"))));
2430
+ } else return null;
2431
+ if (anchor === null) return null;
2432
+ addInsertion(insertions, anchor, text, order);
2433
+ }
2434
+ return insertions;
2435
+ }
2436
+ function assemble(source, result, insertions) {
2437
+ const output = [];
2438
+ const insertAfter = (line) => {
2439
+ const list = [...insertions.get(line) ?? []].sort((a, b) => a.order - b.order);
2440
+ for (const { text } of list) output.push({ text, lineBreak: "" });
2441
+ };
2442
+ insertAfter(-1);
2443
+ result.forEach((text, index) => {
2444
+ if (text !== null) output.push({ text, lineBreak: source.lines[index].lineBreak });
2445
+ insertAfter(index);
2446
+ });
2447
+ return output.map(({ text, lineBreak }, index) => {
2448
+ const isLast = index === output.length - 1;
2449
+ if (isLast && !source.endsWithLineBreak) return text;
2450
+ return text + (lineBreak || source.lineBreak);
2451
+ }).join("");
2452
+ }
2453
+ function normalized(value) {
2454
+ if (typeof value === "number") return roundAsWritten(value);
2455
+ if (Array.isArray(value)) return value.map(normalized);
2456
+ if (value !== null && typeof value === "object") {
2457
+ return Object.fromEntries(
2458
+ Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, entry]) => [key, normalized(entry)])
2459
+ );
2460
+ }
2461
+ return value;
2462
+ }
2463
+ function fingerprint(map) {
2464
+ const elementById = new Map(map.elements.map((el) => [el.id, el]));
2465
+ const reference = (id) => {
2466
+ const el = elementById.get(id);
2467
+ return el ? `${el.elementType}:${el.label}` : `?${id}`;
2468
+ };
2469
+ const entries = [`config ${JSON.stringify(normalized(map.config))}`];
2470
+ for (const el of map.elements) {
2471
+ const { id: _id, ...rest } = el;
2472
+ const described = { ...rest };
2473
+ if (el.elementType === "component" && el.pipelineId !== void 0) {
2474
+ described["pipelineId"] = reference(el.pipelineId);
2475
+ }
2476
+ if (el.elementType === "pipeline") described["childIds"] = el.childIds.map(reference).sort();
2477
+ entries.push(`element ${JSON.stringify(normalized(described))}`);
2478
+ }
2479
+ for (const edge of map.edges) {
2480
+ const { id: _id, ...rest } = edge;
2481
+ const described = { ...rest, from: reference(edge.from), to: reference(edge.to) };
2482
+ entries.push(`edge ${JSON.stringify(normalized(described))}`);
2483
+ }
2484
+ for (const raw of map.rawPassthrough ?? []) entries.push(`raw ${raw}`);
2485
+ return entries.sort().join("\n");
2486
+ }
2487
+
2488
+ // src/read-back-ids.ts
2489
+ var ELEMENT_KEY = /^(?:element|child):(.*)$/s;
2490
+ function elementIdOf(key) {
2491
+ return ELEMENT_KEY.exec(key)?.[1];
2492
+ }
2493
+ function readBackIds(map) {
2494
+ const lines = serializeKeyedLines(map);
2495
+ const { sourceMap } = parseDSLWithSourceMap([...lines.map((line) => line.text), ""].join("\n"));
2496
+ const readIdByLine = /* @__PURE__ */ new Map();
2497
+ for (const [key, line] of sourceMap.primaryLineOf) {
2498
+ const id = elementIdOf(key);
2499
+ if (id !== void 0) readIdByLine.set(line, id);
2500
+ }
2501
+ const ids = /* @__PURE__ */ new Map();
2502
+ lines.forEach((line, index) => {
2503
+ const id = elementIdOf(line.key);
2504
+ const readId = readIdByLine.get(index + 1);
2505
+ if (id !== void 0 && readId !== void 0) ids.set(id, readId);
2506
+ });
2507
+ return ids;
2508
+ }
2509
+
2510
+ // src/raw-passthrough.ts
2511
+ function withoutRawStatements(rawPassthrough, keyword) {
2512
+ if (!rawPassthrough) return rawPassthrough;
2513
+ const wanted = keyword.toLowerCase();
2514
+ const comments = new CommentStripper();
2515
+ let changed = false;
2516
+ const kept = [];
2517
+ for (const raw of rawPassthrough) {
2518
+ const stripped = comments.strip(raw);
2519
+ if (keywordOf(stripped.code) !== wanted) {
2520
+ kept.push(raw);
2521
+ continue;
2522
+ }
2523
+ changed = true;
2524
+ if (stripped.comments.length) kept.push(stripped.comments.join(" "));
2525
+ }
2526
+ if (!changed) return rawPassthrough;
2527
+ return kept.length ? kept : void 0;
2528
+ }
2529
+
1777
2530
  Object.defineProperty(exports, "loadMap", {
1778
2531
  enumerable: true,
1779
2532
  get: function () { return wardleySchemaModel.loadMap; }
@@ -1792,7 +2545,10 @@ exports.parseDSL = parseDSL;
1792
2545
  exports.parseDSLWithDiagnostics = parseDSLWithDiagnostics;
1793
2546
  exports.parseDecorators = parseDecorators;
1794
2547
  exports.parseLabelOffset = parseLabelOffset;
2548
+ exports.patchDSL = patchDSL;
2549
+ exports.readBackIds = readBackIds;
1795
2550
  exports.serializeDSL = serializeDSL;
1796
2551
  exports.slug = slug;
2552
+ exports.withoutRawStatements = withoutRawStatements;
1797
2553
  //# sourceMappingURL=index.cjs.map
1798
2554
  //# sourceMappingURL=index.cjs.map