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