@diagrammo/dgmo 0.34.0 → 0.35.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.
Files changed (55) hide show
  1. package/.claude/commands/dgmo.md +17 -32
  2. package/.github/copilot-instructions.md +1 -1
  3. package/README.md +7 -1
  4. package/SKILL.md +1 -1
  5. package/dist/advanced.cjs +462 -465
  6. package/dist/advanced.d.cts +2 -51
  7. package/dist/advanced.d.ts +2 -51
  8. package/dist/advanced.js +462 -466
  9. package/dist/auto.cjs +463 -142
  10. package/dist/auto.js +91 -91
  11. package/dist/auto.mjs +463 -142
  12. package/dist/cli.cjs +143 -326
  13. package/dist/index.cjs +462 -141
  14. package/dist/index.js +462 -141
  15. package/docs/ai-integration.md +46 -26
  16. package/docs/language-reference.md +2 -2
  17. package/package.json +2 -2
  18. package/src/advanced.ts +0 -12
  19. package/src/c4/parser.ts +15 -6
  20. package/src/chart.ts +1 -1
  21. package/src/class/parser.ts +6 -2
  22. package/src/cli-banner.ts +2 -3
  23. package/src/cli.ts +539 -1223
  24. package/src/cycle/parser.ts +4 -1
  25. package/src/dgmo-router.ts +16 -2
  26. package/src/diagnostics.ts +0 -0
  27. package/src/echarts.ts +5 -1
  28. package/src/er/parser.ts +5 -1
  29. package/src/gantt/parser.ts +13 -4
  30. package/src/graph/flowchart-parser.ts +5 -1
  31. package/src/graph/state-parser.ts +5 -1
  32. package/src/infra/parser.ts +10 -6
  33. package/src/journey-map/parser.ts +5 -2
  34. package/src/kanban/parser.ts +6 -2
  35. package/src/map/context-labels.ts +29 -19
  36. package/src/map/layout.ts +403 -108
  37. package/src/map/legend-band.ts +28 -0
  38. package/src/map/parser.ts +13 -4
  39. package/src/map/renderer.ts +74 -23
  40. package/src/map/resolver.ts +2 -2
  41. package/src/org/parser.ts +8 -2
  42. package/src/pyramid/parser.ts +4 -1
  43. package/src/raci/parser.ts +4 -1
  44. package/src/ring/parser.ts +4 -1
  45. package/src/sequence/parser.ts +6 -3
  46. package/src/sitemap/parser.ts +12 -3
  47. package/src/tech-radar/parser.ts +6 -3
  48. package/src/utils/parsing.ts +2 -2
  49. package/src/utils/tag-groups.ts +3 -3
  50. package/src/visualizations/parse.ts +9 -3
  51. package/src/migrate/data-comma.ts +0 -117
  52. package/src/migrate/embedded.ts +0 -238
  53. package/src/migrate/index.ts +0 -295
  54. package/src/migrate/line-classifier.ts +0 -128
  55. package/src/migrate/line-transformer.ts +0 -218
package/dist/advanced.js CHANGED
@@ -35,6 +35,17 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
35
35
  function makeDgmoError(line11, message, severity = "error", code) {
36
36
  return code !== void 0 ? { line: line11, message, severity, code } : { line: line11, message, severity };
37
37
  }
38
+ function dedupeDiagnostics(diagnostics) {
39
+ const seen = /* @__PURE__ */ new Set();
40
+ const out = [];
41
+ for (const d of diagnostics) {
42
+ const key = `${d.line}\0${d.column ?? ""}\0${d.severity}\0${d.code ?? ""}\0${d.message}`;
43
+ if (seen.has(key)) continue;
44
+ seen.add(key);
45
+ out.push(d);
46
+ }
47
+ return out;
48
+ }
38
49
  function formatDgmoError(err) {
39
50
  return err.line > 0 ? `Line ${err.line}: ${err.message}` : err.message;
40
51
  }
@@ -92,7 +103,7 @@ function vennAliasKeywordRemovedMessage(args) {
92
103
  return `Venn 'alias' keyword was removed. Use 'as' instead \u2014 '${args.name} as ${args.alias}'.`;
93
104
  }
94
105
  function pipeOperatorRemovedMessage() {
95
- return `'|' removed \u2014 'Node | c: red' \u2192 'Node c: red'. Run 'dgmo migrate'`;
106
+ return `'|' removed \u2014 'Node | c: red' \u2192 'Node c: red'`;
96
107
  }
97
108
  function ganttBarePercentRemovedMessage(percent) {
98
109
  return `Bare-percent progress shorthand '| ${percent}' was removed. Use 'progress: ${percent.replace(/%$/, "")}' (integer 0\u2013100, no '%').`;
@@ -1245,7 +1256,7 @@ var init_parsing = __esm({
1245
1256
  "no-title",
1246
1257
  "no-notes"
1247
1258
  ]);
1248
- MULTIPLE_PIPE_ERROR = 'Use a single "|" to start metadata, then separate items with commas.';
1259
+ MULTIPLE_PIPE_ERROR = "'|' is no longer metadata syntax \u2014 put metadata on the same line as 'key: value, key2: value2' (no pipes), per \xA71.4.";
1249
1260
  RESERVED_KEY_RE = /^[A-Za-z][A-Za-z0-9_-]*$/;
1250
1261
  AS_ALIAS_RE = /\s+as\s+([A-Za-z][A-Za-z0-9_]{0,11})$/;
1251
1262
  POTENTIAL_META_RE = /\b([\w-]+)\s*:/;
@@ -1426,12 +1437,10 @@ function validateTagValues(entities, tagGroups, pushWarning, suggestFn) {
1426
1437
  );
1427
1438
  if (!isPrefix) {
1428
1439
  const defined = group.entries.map((e) => e.value);
1429
- let msg = `Unknown value '${value}' for tag group '${group.name}'`;
1440
+ let msg = `Unknown value '${value}' for tag group '${group.name}' \u2014 use one of: ${defined.join(", ")}`;
1430
1441
  const hint = suggestFn?.(value, defined);
1431
1442
  if (hint) {
1432
1443
  msg += `. ${hint}`;
1433
- } else {
1434
- msg += ` \u2014 defined values: ${defined.join(", ")}`;
1435
1444
  }
1436
1445
  pushWarning(entity.lineNumber, msg);
1437
1446
  }
@@ -2207,7 +2216,10 @@ function parseVisualizationFull(content, palette) {
2207
2216
  lineNumber
2208
2217
  });
2209
2218
  } else {
2210
- warn(lineNumber, `Unrecognized era entry: "${line11}"`);
2219
+ warn(
2220
+ lineNumber,
2221
+ `Unrecognized era entry: "${line11}" \u2014 use '<start> -> <end> Label [color]' (e.g. '1716 -> 1717 Reign').`
2222
+ );
2211
2223
  }
2212
2224
  continue;
2213
2225
  }
@@ -2235,7 +2247,10 @@ function parseVisualizationFull(content, palette) {
2235
2247
  lineNumber
2236
2248
  });
2237
2249
  } else {
2238
- warn(lineNumber, `Unrecognized marker entry: "${line11}"`);
2250
+ warn(
2251
+ lineNumber,
2252
+ `Unrecognized marker entry: "${line11}" \u2014 use '<date> Label [color]' (e.g. '1776 Independence').`
2253
+ );
2239
2254
  }
2240
2255
  continue;
2241
2256
  }
@@ -2913,7 +2928,7 @@ function parseVisualizationFull(content, palette) {
2913
2928
  result.diagnostics.push(
2914
2929
  makeDgmoError(
2915
2930
  ov.lineNumber,
2916
- `Intersection references unknown set or alias "${ref}"`
2931
+ `Intersection references unknown set or alias "${ref}" \u2014 declare the set first on its own line ('Name [as <alias>] [color]', e.g. 'Navigation as nav blue'), then reference it by that name or alias. (\xA716.4)`
2917
2932
  )
2918
2933
  );
2919
2934
  if (!result.error)
@@ -6296,10 +6311,10 @@ function parseSequenceDgmo(content, palette) {
6296
6311
  const rawInside = fallbackMatch[1];
6297
6312
  const pipeIdx = rawInside.indexOf("|");
6298
6313
  const cleanName = rawInside.substring(0, pipeIdx).trim().replace(/\([^)]*\)$/, "").trim();
6299
- const metaPart = rawInside.substring(pipeIdx).trim();
6314
+ const metaPart = rawInside.substring(pipeIdx).replace(/^\|\s*/, "").trim();
6300
6315
  pushError(
6301
6316
  lineNumber,
6302
- `Pipe metadata must go outside brackets \u2014 use '[${cleanName}] ${metaPart}' instead of '[${rawInside.trim()}]'`
6317
+ `Metadata goes after the bracket with no pipe \u2014 use '[${cleanName}] ${metaPart}' (e.g. '[Backend] t: Product'), not '[${rawInside.trim()}]'`
6303
6318
  );
6304
6319
  continue;
6305
6320
  }
@@ -6909,7 +6924,7 @@ function parseSequenceDgmo(content, palette) {
6909
6924
  }
6910
6925
  pushWarning(
6911
6926
  lineNumber,
6912
- `Unexpected line: '${trimmed}'. Expected a message (A -> B), participant, section (== Name ==), or block keyword (if/loop/parallel).`
6927
+ `Unexpected line: '${trimmed}'. Expected a message ('A -> B' or 'A -label-> B'), participant ('Name' or 'Name is a actor'), note ('note right of A Text'), section ('== Name =='), or block ('if/loop/parallel <label>').`
6913
6928
  );
6914
6929
  }
6915
6930
  if (!hasExplicitChart && result.messages.length === 0) {
@@ -8071,7 +8086,11 @@ function parseFlowchart(content, palette) {
8071
8086
  const value = optMatch[2].trim();
8072
8087
  if (key === "no-color") {
8073
8088
  result.diagnostics.push(
8074
- makeDgmoError(lineNumber, '"no-color" has been removed.', "warning")
8089
+ makeDgmoError(
8090
+ lineNumber,
8091
+ '"no-color" was removed \u2014 node colors are assigned automatically by shape; delete this line.',
8092
+ "warning"
8093
+ )
8075
8094
  );
8076
8095
  continue;
8077
8096
  }
@@ -8400,7 +8419,11 @@ function parseState(content, palette) {
8400
8419
  const value = optMatch[2].trim();
8401
8420
  if (key === "no-color") {
8402
8421
  result.diagnostics.push(
8403
- makeDgmoError(lineNumber, '"no-color" has been removed.', "warning")
8422
+ makeDgmoError(
8423
+ lineNumber,
8424
+ '"no-color" was removed \u2014 state colors are assigned automatically; delete this line.',
8425
+ "warning"
8426
+ )
8404
8427
  );
8405
8428
  continue;
8406
8429
  }
@@ -8715,7 +8738,7 @@ function parseClassDiagram(content, palette) {
8715
8738
  result.diagnostics.push(
8716
8739
  makeDgmoError(
8717
8740
  lineNumber,
8718
- '"no-auto-color" has been removed.',
8741
+ '"no-auto-color" was removed \u2014 colors are always auto-assigned from the palette; delete this line.',
8719
8742
  "warning"
8720
8743
  )
8721
8744
  );
@@ -8830,7 +8853,11 @@ function parseClassDiagram(content, palette) {
8830
8853
  continue;
8831
8854
  }
8832
8855
  result.diagnostics.push(
8833
- makeDgmoError(lineNumber, `Unexpected line: '${trimmed}'.`, "warning")
8856
+ makeDgmoError(
8857
+ lineNumber,
8858
+ `Unexpected line: '${trimmed}'. Classes start uppercase at indent 0 ('Ship'); members are indented and colon-typed ('+ name: string', '+ sail(): void'); relationships are indented arrows ('--|> Vessel'). (\xA710)`,
8859
+ "warning"
8860
+ )
8834
8861
  );
8835
8862
  }
8836
8863
  if (notes.length > 0) {
@@ -9308,7 +9335,11 @@ function parseERDiagram(content, palette) {
9308
9335
  continue;
9309
9336
  }
9310
9337
  result.diagnostics.push(
9311
- makeDgmoError(lineNumber, `Unexpected line: '${trimmed}'.`, "warning")
9338
+ makeDgmoError(
9339
+ lineNumber,
9340
+ `Unexpected line: '${trimmed}'. A table is a bare name at indent 0 ('users'); columns are indented and SPACE-separated, no colon ('id int pk'); relationships are indented ('1--* orders'). (\xA79)`,
9341
+ "warning"
9342
+ )
9312
9343
  );
9313
9344
  }
9314
9345
  if (result.tables.length === 0 && !result.error) {
@@ -9652,7 +9683,10 @@ function parseOrg(content, palette) {
9652
9683
  const value = metadataMatch[2].trim();
9653
9684
  const parent = findMetadataParent(indent, indentStack);
9654
9685
  if (!parent) {
9655
- pushError(lineNumber, "Metadata has no parent node");
9686
+ pushError(
9687
+ lineNumber,
9688
+ "Metadata 'key: value' must be indented under a node \u2014 no node precedes it."
9689
+ );
9656
9690
  } else {
9657
9691
  parent.metadata = { ...parent.metadata, [key]: value };
9658
9692
  }
@@ -9670,7 +9704,10 @@ function parseOrg(content, palette) {
9670
9704
  );
9671
9705
  attachNode(node, indent, indentStack, result);
9672
9706
  } else {
9673
- pushError(lineNumber, "Metadata has no parent node");
9707
+ pushError(
9708
+ lineNumber,
9709
+ "Metadata 'key: value' must be indented under a node \u2014 no node precedes it."
9710
+ );
9674
9711
  }
9675
9712
  } else {
9676
9713
  const node = parseNodeLabel(
@@ -10019,7 +10056,10 @@ function parseSitemap(content, palette) {
10019
10056
  if (arrowInfo) {
10020
10057
  const source = findParentNode(indent, indentStack);
10021
10058
  if (!source) {
10022
- pushError(lineNumber, "Arrow has no source node");
10059
+ pushError(
10060
+ lineNumber,
10061
+ "Arrow '-> target' must be indented under its source page \u2014 no page precedes it."
10062
+ );
10023
10063
  } else {
10024
10064
  deferredArrows.push({
10025
10065
  sourceNode: source,
@@ -10089,7 +10129,10 @@ function parseSitemap(content, palette) {
10089
10129
  const value = metadataMatch[2].trim();
10090
10130
  const parent = findParentNode(indent, indentStack);
10091
10131
  if (!parent) {
10092
- pushError(lineNumber, "Metadata has no parent node");
10132
+ pushError(
10133
+ lineNumber,
10134
+ "Metadata 'key: value' must be indented under a page \u2014 no page precedes it."
10135
+ );
10093
10136
  } else {
10094
10137
  parent.metadata = { ...parent.metadata, [key]: value };
10095
10138
  }
@@ -10109,7 +10152,10 @@ function parseSitemap(content, palette) {
10109
10152
  const key = normalizeName(node.label);
10110
10153
  labelToNode.set(key, node);
10111
10154
  } else {
10112
- pushError(lineNumber, "Metadata has no parent node");
10155
+ pushError(
10156
+ lineNumber,
10157
+ "Metadata 'key: value' must be indented under a page \u2014 no page precedes it."
10158
+ );
10113
10159
  }
10114
10160
  } else {
10115
10161
  const descResult = tryStripDescriptionKeyword(trimmed);
@@ -11927,7 +11973,7 @@ function parseChart(content, palette) {
11927
11973
  result.diagnostics.push(
11928
11974
  makeDgmoError(
11929
11975
  lineNumber,
11930
- `'${trimmed}' \u2014 ## syntax is no longer supported. Use [Group] containers instead`
11976
+ `'${trimmed}' removed \u2014 use a bracket container '[${trimmed.replace(/^#+\s*/, "")}]' instead (## headers are no longer supported).`
11931
11977
  )
11932
11978
  );
11933
11979
  continue;
@@ -12919,7 +12965,11 @@ function parseExtendedChartFull(content, palette) {
12919
12965
  continue;
12920
12966
  }
12921
12967
  result.diagnostics.push(
12922
- makeDgmoError(lineNumber, `Unexpected line: '${trimmed}'.`, "warning")
12968
+ makeDgmoError(
12969
+ lineNumber,
12970
+ `Unexpected line: '${trimmed}'. Expected a data row ('Label value', space-separated) or a known option.`,
12971
+ "warning"
12972
+ )
12923
12973
  );
12924
12974
  }
12925
12975
  const warn = (line11, message) => {
@@ -15277,7 +15327,10 @@ function parseKanban(content, palette) {
15277
15327
  continue;
15278
15328
  }
15279
15329
  if (!currentColumn) {
15280
- warn(lineNumber, "Card line found before any column");
15330
+ warn(
15331
+ lineNumber,
15332
+ "Card line found before any column. Declare a column first with '[Column Name]', then indent cards beneath it. (\xA711)"
15333
+ );
15281
15334
  continue;
15282
15335
  }
15283
15336
  if (currentCard && indent > cardBaseIndent) {
@@ -15416,7 +15469,7 @@ var init_parser13 = __esm({
15416
15469
  KNOWN_OPTIONS4 = /* @__PURE__ */ new Set(["hide", "active-tag"]);
15417
15470
  KNOWN_BOOLEANS3 = /* @__PURE__ */ new Set(["solid-fill", "no-title"]);
15418
15471
  REMOVED_BOOLEANS = {
15419
- "no-auto-color": '"no-auto-color" has been removed.'
15472
+ "no-auto-color": '"no-auto-color" was removed \u2014 colors are always auto-assigned from the palette; delete this line.'
15420
15473
  };
15421
15474
  }
15422
15475
  });
@@ -15653,7 +15706,7 @@ function parseC4(content, palette) {
15653
15706
  } else {
15654
15707
  pushError(
15655
15708
  lineNumber,
15656
- `"container ${refName}" must be inside a deployment node`
15709
+ `"container ${refName}" must be indented under a deployment node (e.g. under 'Prod is a deployment-node')`
15657
15710
  );
15658
15711
  }
15659
15712
  continue;
@@ -15708,7 +15761,10 @@ function parseC4(content, palette) {
15708
15761
  indent
15709
15762
  });
15710
15763
  } else {
15711
- pushError(lineNumber, `"${sectionType}" must be inside an element`);
15764
+ pushError(
15765
+ lineNumber,
15766
+ `"${sectionType}" must be indented under an element (e.g. under 'Web App is a container')`
15767
+ );
15712
15768
  }
15713
15769
  continue;
15714
15770
  }
@@ -15735,7 +15791,10 @@ function parseC4(content, palette) {
15735
15791
  indent
15736
15792
  });
15737
15793
  } else {
15738
- pushError(lineNumber, `Group [${groupName}] must be inside an element`);
15794
+ pushError(
15795
+ lineNumber,
15796
+ `Group [${groupName}] must be indented under an element (e.g. under 'Web App is a container')`
15797
+ );
15739
15798
  }
15740
15799
  continue;
15741
15800
  }
@@ -16065,7 +16124,10 @@ function parseC4(content, palette) {
16065
16124
  }
16066
16125
  desc.push(descResult.text);
16067
16126
  } else {
16068
- pushError(lineNumber, `Unexpected content: "${trimmed}"`);
16127
+ pushError(
16128
+ lineNumber,
16129
+ `Unexpected content: "${trimmed}". Expected an element ('Web App is a container'), a relationship ('-Uses-> Database'), indented metadata ('tech: React'), or a group ('[Backend]'). (\xA78)`
16130
+ );
16069
16131
  }
16070
16132
  }
16071
16133
  finalizeAutoTagColors(result.tagGroups, palette);
@@ -16127,7 +16189,7 @@ function validateRelationshipTargets(result, knownNames, pushWarning) {
16127
16189
  for (const el of elements) {
16128
16190
  for (const rel of el.relationships) {
16129
16191
  if (!knownNames.has(normalizeName(rel.target))) {
16130
- let msg = `Relationship target "${rel.target}" not found`;
16192
+ let msg = `Relationship target "${rel.target}" not found \u2014 declare it first (e.g. '${rel.target} is a container'), then reference it in the arrow`;
16131
16193
  const hint = suggest(normalizeName(rel.target), allNames);
16132
16194
  if (hint) msg += `. ${hint}`;
16133
16195
  pushWarning(rel.lineNumber, msg, "warning");
@@ -16155,7 +16217,7 @@ function validateDeploymentRefs(result, knownNames, pushWarning) {
16155
16217
  for (const node of nodes) {
16156
16218
  for (const ref of node.containerRefs) {
16157
16219
  if (!knownNames.has(normalizeName(ref))) {
16158
- let msg = `Deployment reference "container ${ref}" not found`;
16220
+ let msg = `Deployment reference "container ${ref}" not found \u2014 declare the container element first, then reference it under the deployment node`;
16159
16221
  const hint = suggest(normalizeName(ref), allNames);
16160
16222
  if (hint) msg += `. ${hint}`;
16161
16223
  pushWarning(node.lineNumber, msg, "warning");
@@ -16874,7 +16936,7 @@ function parseInfra(content) {
16874
16936
  }
16875
16937
  warn(
16876
16938
  lineNumber,
16877
- `Invalid tag value '${trimmed}' in tag group '${currentTagGroup.name}'.`
16939
+ `Invalid tag value '${trimmed}' in tag group '${currentTagGroup.name}' \u2014 a value is a bare word with an optional trailing color (e.g. 'Production red'); no ':' or '|'.`
16878
16940
  );
16879
16941
  continue;
16880
16942
  }
@@ -16947,7 +17009,7 @@ function parseInfra(content) {
16947
17009
  const n = deprecatedFanout[1];
16948
17010
  setError(
16949
17011
  lineNumber,
16950
- `'x${n}' fanout syntax is no longer supported \u2014 use '| fanout: ${n}' instead`
17012
+ `'x${n}' fanout syntax is no longer supported \u2014 use 'fanout: ${n}' instead`
16951
17013
  );
16952
17014
  continue;
16953
17015
  }
@@ -17121,8 +17183,10 @@ function parseInfra(content) {
17121
17183
  const hint = suggest(key, allKeys);
17122
17184
  const valueLooksNumeric = /^[\d.]+%?$/.test(rawVal);
17123
17185
  if (hint || valueLooksNumeric) {
17124
- let msg = `Unknown property '${key}'.`;
17186
+ let msg = `Unknown infra property '${key}'.`;
17125
17187
  if (hint) msg += ` ${hint}`;
17188
+ else
17189
+ msg += ` Properties are colon-keyed 'key: value' from the known schema (e.g. latency-ms, max-rps, cache-hit, instances, concurrency).`;
17126
17190
  warn(lineNumber, msg);
17127
17191
  } else if (!currentNode.isEdge) {
17128
17192
  pushDescription(currentNode, trimmed);
@@ -17133,7 +17197,7 @@ function parseInfra(content) {
17133
17197
  if (EDGE_ONLY_KEYS.has(key) && !currentNode.isEdge) {
17134
17198
  warn(
17135
17199
  lineNumber,
17136
- `Property '${key}' is only valid on the entry point (Edge/Internet).`
17200
+ `Property '${key}' belongs on the entry point \u2014 declare it indented under an 'internet' or 'edge' node, not on '${currentNode.label}'.`
17137
17201
  );
17138
17202
  }
17139
17203
  const value = parsePropertyValue(rawVal);
@@ -17240,7 +17304,7 @@ function parseInfra(content) {
17240
17304
  }
17241
17305
  warn(
17242
17306
  lineNumber,
17243
- currentGroup ? `Unexpected line: '${trimmed}'. Expected an indented component name.` : `Unexpected line: '${trimmed}'. Expected a component name, [Group Name], tag group, or option.`
17307
+ currentGroup ? `Unexpected line: '${trimmed}'. Inside a group, indent a component name under the '[Group]'.` : `Unexpected line: '${trimmed}'. Expected a node ('Api Gateway'), a group ('[Backend]'), a tag group ('tag Tier as t'), or an option ('default-rps 100'); node properties go indented under a node as 'key: value'.`
17244
17308
  );
17245
17309
  }
17246
17310
  finishCurrentNode();
@@ -17616,7 +17680,10 @@ function parseGantt(content, palette) {
17616
17680
  continue;
17617
17681
  }
17618
17682
  if (COMMENT_RE.test(line11)) continue;
17619
- warn(lineNumber, `Unrecognized holiday entry: "${line11}"`);
17683
+ warn(
17684
+ lineNumber,
17685
+ `Unrecognized holiday entry: "${line11}". Use a date '2024-02-19 Presidents Day' or a range '2024-05-27 -> 2024-05-29 Memorial Weekend'. (\xA713.3)`
17686
+ );
17620
17687
  continue;
17621
17688
  }
17622
17689
  }
@@ -17642,7 +17709,10 @@ function parseGantt(content, palette) {
17642
17709
  lineNumber
17643
17710
  });
17644
17711
  } else {
17645
- warn(lineNumber, `Unrecognized era entry: "${line11}"`);
17712
+ warn(
17713
+ lineNumber,
17714
+ `Unrecognized era entry: "${line11}". Use 'YYYY-MM-DD -> YYYY-MM-DD Label [color]' (e.g. '2026-04-06 -> 2026-04-10 Conference purple'). (\xA713.5)`
17715
+ );
17646
17716
  }
17647
17717
  continue;
17648
17718
  }
@@ -17668,7 +17738,10 @@ function parseGantt(content, palette) {
17668
17738
  lineNumber
17669
17739
  });
17670
17740
  } else {
17671
- warn(lineNumber, `Unrecognized marker entry: "${line11}"`);
17741
+ warn(
17742
+ lineNumber,
17743
+ `Unrecognized marker entry: "${line11}". Use 'YYYY-MM-DD Label [color]' (e.g. '2026-03-27 Board Review' or '2026-06-15 Release green'). (\xA713.6)`
17744
+ );
17672
17745
  }
17673
17746
  continue;
17674
17747
  }
@@ -17834,7 +17907,7 @@ function parseGantt(content, palette) {
17834
17907
  } else {
17835
17908
  softError(
17836
17909
  lineNumber,
17837
- `Arrow reference "${content2.name}" has no parent task context.`
17910
+ `Arrow '-> ${content2.name}' has no source task \u2014 indent it under the task it follows (e.g. 'Design 5bd' then indented '-> ${content2.name} 3bd'). (\xA713)`
17838
17911
  );
17839
17912
  }
17840
17913
  }
@@ -19054,7 +19127,10 @@ function parseMap(content, palette) {
19054
19127
  function handleTag(trimmed, line11) {
19055
19128
  const m = matchTagBlockHeading(trimmed);
19056
19129
  if (!m) {
19057
- pushError(line11, `Malformed tag declaration: "${trimmed}".`);
19130
+ pushError(
19131
+ line11,
19132
+ `Malformed tag declaration: "${trimmed}" \u2014 write 'tag <Group>' (optionally 'tag <Group> as <alias>'), e.g. 'tag Market as m', with indented 'Value color' entries below.`
19133
+ );
19058
19134
  return;
19059
19135
  }
19060
19136
  emitTagLegacyDiagnostic(m, line11, diagnostics);
@@ -19166,7 +19242,7 @@ function parseMap(content, palette) {
19166
19242
  if (!pos || pos.kind === "name" && !pos.name) {
19167
19243
  pushError(
19168
19244
  line11,
19169
- "route requires an origin: `route <origin> [style: arc]`."
19245
+ "route needs an origin place \u2014 `route <origin>` then indented legs (`-> dest`); origin may be a city name, IATA code, or `<lat> <lon>`. (\xA724B)"
19170
19246
  );
19171
19247
  return;
19172
19248
  }
@@ -19229,7 +19305,10 @@ function parseMap(content, palette) {
19229
19305
  function handleEdges(trimmed, line11) {
19230
19306
  const parts = trimmed.split(ARROW_SPLIT);
19231
19307
  if (parts.length < 3) {
19232
- pushError(line11, `Malformed edge: "${trimmed}".`);
19308
+ pushError(
19309
+ line11,
19310
+ `Malformed edge: "${trimmed}" \u2014 an edge is 'A -> B' (or 'A -label-> B', '~>' for arc, '--' undirected), both endpoints non-empty. (\xA724B)`
19311
+ );
19233
19312
  return;
19234
19313
  }
19235
19314
  const endpoints = [];
@@ -19252,7 +19331,10 @@ function parseMap(content, palette) {
19252
19331
  const from = endpoints[k];
19253
19332
  const to = endpoints[k + 1];
19254
19333
  if (!from || !to) {
19255
- pushError(line11, `Edge has an empty endpoint: "${trimmed}".`);
19334
+ pushError(
19335
+ line11,
19336
+ `Edge has an empty endpoint: "${trimmed}" \u2014 both sides of the arrow need a place, e.g. 'Paris -> Berlin'. (\xA724B)`
19337
+ );
19256
19338
  continue;
19257
19339
  }
19258
19340
  const isLast = k === links.length - 1;
@@ -21371,7 +21453,10 @@ function parseTechRadar(content) {
21371
21453
  }
21372
21454
  if (indent === 0 && trimmed.toLowerCase() === "rings") {
21373
21455
  if (result.rings.length > 0) {
21374
- warn(lineNumber, 'Duplicate "rings" block \u2014 using last one');
21456
+ warn(
21457
+ lineNumber,
21458
+ 'Duplicate "rings" block \u2014 only the last is kept; merge all ring names into one "rings" block.'
21459
+ );
21375
21460
  result.rings = [];
21376
21461
  }
21377
21462
  inRingsBlock = true;
@@ -21590,7 +21675,7 @@ function parseTechRadar(content) {
21590
21675
  result.diagnostics.push(
21591
21676
  makeDgmoError(
21592
21677
  lineNumber,
21593
- `Blip "${trimmed}" has no ring assignment. Place it under a ring section header or use: ${trimmed} | ring: RingName`
21678
+ `Blip "${trimmed}" has no ring. Put it under a ring section header, or add a colon-keyed ring \u2014 '${trimmed} ring: RingName' (per \xA720).`
21594
21679
  )
21595
21680
  );
21596
21681
  continue;
@@ -21599,7 +21684,7 @@ function parseTechRadar(content) {
21599
21684
  if (!trimmed.includes("|") && headerParsed && result.rings.length > 0) {
21600
21685
  warn(
21601
21686
  lineNumber,
21602
- `Unrecognized line "${trimmed}". Quadrant headers require pipe metadata: ${trimmed} | quadrant: top-left`
21687
+ `Unrecognized line "${trimmed}". A quadrant header is 'Name quadrant: <top-left|top-right|bottom-left|bottom-right>' (per \xA720).`
21603
21688
  );
21604
21689
  }
21605
21690
  }
@@ -21892,7 +21977,10 @@ function parseCycle(content) {
21892
21977
  currentNode.description.push(descLine);
21893
21978
  continue;
21894
21979
  }
21895
- warn(lineNum, `Unexpected indented line: "${trimmed}".`);
21980
+ warn(
21981
+ lineNum,
21982
+ `Indented line "${trimmed}" has no step above it \u2014 add a step on a non-indented line first, then indent its details. (\xA721)`
21983
+ );
21896
21984
  continue;
21897
21985
  }
21898
21986
  }
@@ -22195,7 +22283,7 @@ function parseJourneyMap(content, palette) {
22195
22283
  if (trimmed.includes("|") || !PHASE_RE.test(trimmed)) {
22196
22284
  warn(
22197
22285
  lineNumber,
22198
- "Steps outside phases will be ignored when phases are present"
22286
+ `Step "${trimmed}" sits outside any phase and is ignored \u2014 indent it under a '[Phase]' header (per \xA722).`
22199
22287
  );
22200
22288
  continue;
22201
22289
  }
@@ -22252,7 +22340,10 @@ function parseJourneyMap(content, palette) {
22252
22340
  continue;
22253
22341
  }
22254
22342
  if (indent > 0 && !currentPhase && hasPhases) {
22255
- warn(lineNumber, `Unexpected indented line outside of a phase`);
22343
+ warn(
22344
+ lineNumber,
22345
+ `Indented line outside any phase \u2014 add a '[Phase]' header above it, or unindent it to a top-level step. (\xA722)`
22346
+ );
22256
22347
  }
22257
22348
  }
22258
22349
  if (currentStep) {
@@ -22598,7 +22689,10 @@ function parsePyramid(content) {
22598
22689
  continue;
22599
22690
  }
22600
22691
  if (!currentLayer) {
22601
- warn(lineNum, `Unexpected indented line: "${trimmed}".`);
22692
+ warn(
22693
+ lineNum,
22694
+ `Indented description "${trimmed}" has no layer above it \u2014 add a layer name on a non-indented line first. (\xA723)`
22695
+ );
22602
22696
  continue;
22603
22697
  }
22604
22698
  const descLine = trimmed.startsWith("- ") ? `\u2022 ${trimmed.substring(2)}` : trimmed;
@@ -22772,7 +22866,10 @@ function parseRing(content) {
22772
22866
  continue;
22773
22867
  }
22774
22868
  if (!currentLayer) {
22775
- warn(lineNum, `Unexpected indented line: "${trimmed}".`);
22869
+ warn(
22870
+ lineNum,
22871
+ `Indented description "${trimmed}" has no layer above it \u2014 add a layer name on a non-indented line first. (\xA724)`
22872
+ );
22776
22873
  continue;
22777
22874
  }
22778
22875
  const descLine = trimmed.startsWith("- ") ? `\u2022 ${trimmed.substring(2)}` : trimmed;
@@ -23498,7 +23595,10 @@ function parseRaci(content, palette) {
23498
23595
  continue;
23499
23596
  }
23500
23597
  if (bodyStarted) {
23501
- errorAt(lineNumber, `Unexpected line: '${trimmed}'.`);
23598
+ errorAt(
23599
+ lineNumber,
23600
+ `Unexpected line: '${trimmed}'. In the body expect a '[Phase]' header, an indented task name, or an assignment 'Role: A R C I' under a task. (\xA724A)`
23601
+ );
23502
23602
  } else {
23503
23603
  warn(lineNumber, `Unrecognized directive: '${trimmed}'.`);
23504
23604
  }
@@ -24045,6 +24145,13 @@ function getAllChartTypes() {
24045
24145
  return chartTypes.map((c) => c.id);
24046
24146
  }
24047
24147
  function parseDgmo(content) {
24148
+ const result = parseDgmoUndeduped(content);
24149
+ return {
24150
+ diagnostics: dedupeDiagnostics(result.diagnostics),
24151
+ chartType: result.chartType
24152
+ };
24153
+ }
24154
+ function parseDgmoUndeduped(content) {
24048
24155
  const chartType = parseDgmoChartType(content);
24049
24156
  if (!chartType) {
24050
24157
  const colonDiag = detectColonChartType(content);
@@ -51685,7 +51792,7 @@ function resolveMap(parsed, data) {
51685
51792
  const hint = suggest(r.name, allNames);
51686
51793
  err(
51687
51794
  r.lineNumber,
51688
- `Unknown subdivision "${r.name}".${hint ? " " + hint : ""}`,
51795
+ `Unknown region "${r.name}" \u2014 not a known country or US state.${hint ? " " + hint : ""} Search the exact token with \`dgmo map-search "${r.name}"\` (or the lookup_map_location tool), use an ISO code (e.g. FR, US-CA), or coordinates.`,
51689
51796
  "E_MAP_UNKNOWN_SUBDIVISION"
51690
51797
  );
51691
51798
  continue;
@@ -51765,7 +51872,7 @@ function resolveMap(parsed, data) {
51765
51872
  const hint = suggest(name, cityNames);
51766
51873
  err(
51767
51874
  line11,
51768
- `Unknown place "${name}" (not in the gazetteer; use coordinates).${hint ? " " + hint : ""}`,
51875
+ `Unknown place "${name}" \u2014 not in the gazetteer.${hint ? " " + hint : ""} Search the exact token with \`dgmo map-search "${name}"\` (or the lookup_map_location tool), or use coordinates \`poi <lat> <lon>\`.`,
51769
51876
  "E_MAP_UNKNOWN_PLACE"
51770
51877
  );
51771
51878
  return { kind: "miss" };
@@ -52307,6 +52414,21 @@ function mapLegendBand(legend, opts) {
52307
52414
  if (height <= 0) return 0;
52308
52415
  return mapLegendTop(opts.hasTitle, opts.hasSubtitle) + height + LEGEND_BOTTOM_GAP2;
52309
52416
  }
52417
+ function mapLegendBox(legend, opts) {
52418
+ if (!legend) return null;
52419
+ const groups = mapLegendGroups(legend);
52420
+ if (groups.length === 0) return null;
52421
+ const config = mapLegendConfig(groups, opts.mode);
52422
+ const state = { activeGroup: legend.activeGroup };
52423
+ const { width, height } = computeLegendLayout(config, state, opts.width);
52424
+ if (height <= 0 || width <= 0) return null;
52425
+ return {
52426
+ x: (opts.width - width) / 2,
52427
+ y: mapLegendTop(opts.hasTitle, opts.hasSubtitle),
52428
+ width,
52429
+ height
52430
+ };
52431
+ }
52310
52432
  var LEGEND_TOP_GAP2, LEGEND_BOTTOM_GAP2;
52311
52433
  var init_legend_band = __esm({
52312
52434
  "src/map/legend-band.ts"() {
@@ -52431,6 +52553,8 @@ function placeContextLabels(args) {
52431
52553
  waterBodies,
52432
52554
  countries,
52433
52555
  palette,
52556
+ countryTone,
52557
+ waterTone,
52434
52558
  project,
52435
52559
  collides,
52436
52560
  contentPoints,
@@ -52440,8 +52564,8 @@ function placeContextLabels(args) {
52440
52564
  const band = tierBand(Math.max(dLonSpan, dLatSpan));
52441
52565
  const budget = labelBudget(width, height, band);
52442
52566
  if (budget <= 0) return [];
52443
- const waterColor = mix(palette.colors.blue, palette.textMuted, 50);
52444
- const countryColor = palette.textMuted;
52567
+ const waterColor = waterTone ?? mix(palette.colors.blue, palette.textMuted, 50);
52568
+ const countryColor = countryTone ?? palette.textMuted;
52445
52569
  const haloColor = palette.bg;
52446
52570
  const candidates = [];
52447
52571
  const center = [width / 2, height / 2];
@@ -52530,8 +52654,7 @@ function placeContextLabels(args) {
52530
52654
  )
52531
52655
  );
52532
52656
  const fontSize = Math.round(FONT + t * (COUNTRY_FONT_MAX - FONT));
52533
- const fade = Math.round(t * COUNTRY_FADE_MAX);
52534
- const color = fade > 0 ? mix(countryColor, palette.bg, 100 - fade) : countryColor;
52657
+ const color = countryColor;
52535
52658
  const text = c.name;
52536
52659
  const tw = labelWidth(text, 0, fontSize);
52537
52660
  if (tw > w || fontSize + 2 * PADY > h) continue;
@@ -52625,7 +52748,7 @@ function placeContextLabels(args) {
52625
52748
  }
52626
52749
  return placed;
52627
52750
  }
52628
- var FONT, LINE_HEIGHT, PADX, PADY, WATER_LETTER_SPACING, CONTEXT_PAD, EDGE_CLAMP_MARGIN, EDGE_CLAMP_OVERSHOOT, COUNTRY_FONT_MAX, COUNTRY_SIZE_FRAC_MIN, COUNTRY_SIZE_FRAC_MAX, COUNTRY_FADE_MAX, MAX_COUNTRY_POSITIONS, COUNTRY_POS_GRID, COUNTRY_POS_TOPN_MARGIN, KIND_ORDER;
52751
+ var FONT, LINE_HEIGHT, PADX, PADY, WATER_LETTER_SPACING, CONTEXT_PAD, EDGE_CLAMP_MARGIN, EDGE_CLAMP_OVERSHOOT, COUNTRY_FONT_MAX, COUNTRY_SIZE_FRAC_MIN, COUNTRY_SIZE_FRAC_MAX, MAX_COUNTRY_POSITIONS, COUNTRY_POS_GRID, COUNTRY_POS_TOPN_MARGIN, KIND_ORDER;
52629
52752
  var init_context_labels = __esm({
52630
52753
  "src/map/context-labels.ts"() {
52631
52754
  "use strict";
@@ -52642,7 +52765,6 @@ var init_context_labels = __esm({
52642
52765
  COUNTRY_FONT_MAX = 22;
52643
52766
  COUNTRY_SIZE_FRAC_MIN = 0.06;
52644
52767
  COUNTRY_SIZE_FRAC_MAX = 0.32;
52645
- COUNTRY_FADE_MAX = 45;
52646
52768
  MAX_COUNTRY_POSITIONS = 4;
52647
52769
  COUNTRY_POS_GRID = 5;
52648
52770
  COUNTRY_POS_TOPN_MARGIN = 3;
@@ -52780,36 +52902,23 @@ function countryLabelPositions(args) {
52780
52902
  return n;
52781
52903
  };
52782
52904
  const dist2ToCentre = (c) => (c.sx - cx) ** 2 + (c.sy - cy) ** 2;
52783
- const pool = [...kept];
52784
- pool.sort((a, b) => {
52785
- const d = interiorness(b) - interiorness(a);
52786
- return d !== 0 ? d : dist2ToCentre(a) - dist2ToCentre(b);
52787
- });
52788
- const ordered = [pool.shift()];
52789
- while (ordered.length < MAX_COUNTRY_POSITIONS && pool.length) {
52790
- let bestIdx = 0;
52791
- let bestMin = -1;
52792
- for (let k = 0; k < pool.length; k++) {
52793
- const c = pool[k];
52794
- let minD = Infinity;
52795
- for (const o of ordered) {
52796
- const d = (c.sx - o.sx) ** 2 + (c.sy - o.sy) ** 2;
52797
- if (d < minD) minD = d;
52798
- }
52799
- if (minD > bestMin) {
52800
- bestMin = minD;
52801
- bestIdx = k;
52802
- }
52905
+ const centreLon = kept.reduce((s, c) => s + c.lon, 0) / kept.length;
52906
+ const centreLat = kept.reduce((s, c) => s + c.lat, 0) / kept.length;
52907
+ const centreScreen = project(centreLon, centreLat);
52908
+ const lead = pointInGeometry(geometry, centreLon, centreLat) && centreScreen && Number.isFinite(centreScreen[0]) && Number.isFinite(centreScreen[1]) && centreScreen[0] >= 0 && centreScreen[0] <= width && centreScreen[1] >= 0 && centreScreen[1] <= height ? [
52909
+ {
52910
+ lonLat: [centreLon, centreLat],
52911
+ screen: [centreScreen[0], centreScreen[1]]
52803
52912
  }
52804
- ordered.push(pool.splice(bestIdx, 1)[0]);
52805
- }
52806
- const grid = ordered.map((c) => ({
52913
+ ] : [];
52914
+ const grid = [...kept].sort(
52915
+ (a, b) => interiorness(b) - interiorness(a) || dist2ToCentre(a) - dist2ToCentre(b)
52916
+ ).map((c) => ({
52807
52917
  lonLat: [c.lon, c.lat],
52808
52918
  screen: [c.sx, c.sy]
52809
52919
  }));
52810
52920
  const curatedPos = curated ? mkCurated(curated, project) : [];
52811
- const out = [...curatedPos, ...grid].slice(0, MAX_COUNTRY_POSITIONS);
52812
- return out;
52921
+ return [...curatedPos, ...lead, ...grid].slice(0, MAX_COUNTRY_POSITIONS);
52813
52922
  }
52814
52923
  function mkCurated(curated, project) {
52815
52924
  if (!curated) return [];
@@ -53075,6 +53184,25 @@ function layoutMap(resolvedIn, data, size, opts) {
53075
53184
  palette.bg,
53076
53185
  mutedBasemap ? isDark ? MUTED_FOREIGN_DARK : MUTED_FOREIGN_LIGHT : isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
53077
53186
  );
53187
+ const contrastFloorTone = (tone, ...substrates) => {
53188
+ const clears = (t) => substrates.every((s) => contrastRatio(t, s) >= REGION_LABEL_HALO_RATIO);
53189
+ if (clears(tone)) return tone;
53190
+ for (let s = 10; s <= 100; s += 10) {
53191
+ const t = mix(palette.text, tone, s);
53192
+ if (clears(t)) return t;
53193
+ }
53194
+ return palette.text;
53195
+ };
53196
+ const backdropLandTone = contrastFloorTone(
53197
+ palette.textMuted,
53198
+ neutralFill,
53199
+ water
53200
+ );
53201
+ const backdropWaterTone = contrastFloorTone(
53202
+ mix(palette.colors.blue, palette.textMuted, 50),
53203
+ water,
53204
+ neutralFill
53205
+ );
53078
53206
  const hasDirectColor = resolved.regions.some((r) => r.color !== void 0);
53079
53207
  const colorizeActive = resolved.directives.noColorize !== true && !hasRamp && !hasDirectColor && fillGroupNames.size === 0;
53080
53208
  const colorByIso = /* @__PURE__ */ new Map();
@@ -53189,6 +53317,33 @@ function layoutMap(resolvedIn, data, size, opts) {
53189
53317
  hasSubtitle: Boolean(resolved.subtitle)
53190
53318
  });
53191
53319
  if (legendBand > topPad) topPad = legendBand;
53320
+ const topReserved = [];
53321
+ if (shownTitle) {
53322
+ const lines = resolved.subtitle ? 2 : 1;
53323
+ const tw = Math.max(
53324
+ measureLegendText(shownTitle, TITLE_FONT_SIZE),
53325
+ resolved.subtitle ? measureLegendText(resolved.subtitle, TITLE_FONT_SIZE) : 0
53326
+ ) + 2 * TITLE_FONT_SIZE;
53327
+ topReserved.push({
53328
+ x: width / 2 - tw / 2,
53329
+ y: TITLE_Y - TITLE_FONT_SIZE,
53330
+ w: tw,
53331
+ h: TITLE_FONT_SIZE * (lines + 0.5)
53332
+ });
53333
+ }
53334
+ const legendBox = mapLegendBox(legend, {
53335
+ width,
53336
+ mode: opts.legendMode ?? "preview",
53337
+ hasTitle: Boolean(shownTitle),
53338
+ hasSubtitle: Boolean(resolved.subtitle)
53339
+ });
53340
+ if (legendBox)
53341
+ topReserved.push({
53342
+ x: legendBox.x,
53343
+ y: legendBox.y,
53344
+ w: legendBox.width,
53345
+ h: legendBox.height
53346
+ });
53192
53347
  const reserve = opts._calloutReserve;
53193
53348
  const fitLeft = FIT_PAD + (reserve?.left ?? 0);
53194
53349
  const fitRight = width - FIT_PAD - (reserve?.right ?? 0);
@@ -53918,6 +54073,19 @@ function layoutMap(resolvedIn, data, size, opts) {
53918
54073
  const by = b.cy - (b.cy - py) / tb * trimB;
53919
54074
  return `M${ax},${ay}Q${px},${py} ${bx},${by}`;
53920
54075
  };
54076
+ const legLabelPoint = (a, b, curved, offset) => {
54077
+ const mx = (a.cx + b.cx) / 2;
54078
+ const my = (a.cy + b.cy) / 2;
54079
+ if (!curved && offset === 0) return { x: mx, y: my - 4 };
54080
+ const dx = b.cx - a.cx;
54081
+ const dy = b.cy - a.cy;
54082
+ const len = Math.hypot(dx, dy) || 1;
54083
+ const nx = -dy / len;
54084
+ const ny = dx / len;
54085
+ const bow = offset !== 0 ? offset : len * ARC_CURVE_FRAC;
54086
+ const off = bow * 0.5 + Math.sign(bow || 1) * 8;
54087
+ return { x: mx + nx * off, y: my + ny * off };
54088
+ };
53921
54089
  const routeLegVals = resolved.routes.flatMap((rt) => rt.legs).map((l) => Number(l.value)).filter((n) => Number.isFinite(n) && n > 0);
53922
54090
  const rlMin = routeLegVals.length ? Math.min(...routeLegVals) : 0;
53923
54091
  const rlMax = routeLegVals.length ? Math.max(...routeLegVals) : 0;
@@ -53931,13 +54099,12 @@ function layoutMap(resolvedIn, data, size, opts) {
53931
54099
  const a = poiScreen.get(leg.fromId);
53932
54100
  const b = poiScreen.get(leg.toId);
53933
54101
  if (!a || !b) continue;
53934
- const mx = (a.cx + b.cx) / 2;
53935
- const my = (a.cy + b.cy) / 2;
54102
+ const lp = legLabelPoint(a, b, leg.style === "arc", 0);
53936
54103
  const bow = {
53937
54104
  curved: leg.style === "arc",
53938
54105
  offset: 0,
53939
- labelX: mx,
53940
- labelY: my - 4
54106
+ labelX: lp.x,
54107
+ labelY: lp.y
53941
54108
  };
53942
54109
  const routeLabelStyle = leg.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
53943
54110
  legs.push({
@@ -53984,13 +54151,13 @@ function layoutMap(resolvedIn, data, size, opts) {
53984
54151
  const b = poiScreen.get(e.toId);
53985
54152
  if (!a || !b) return;
53986
54153
  const fanOffset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
53987
- const mx = (a.cx + b.cx) / 2;
53988
- const my = (a.cy + b.cy) / 2;
54154
+ const curved = e.style === "arc" || n > 1;
54155
+ const lp = legLabelPoint(a, b, curved, fanOffset);
53989
54156
  const bow = {
53990
- curved: e.style === "arc" || n > 1,
54157
+ curved,
53991
54158
  offset: fanOffset,
53992
- labelX: mx,
53993
- labelY: my - 4
54159
+ labelX: lp.x,
54160
+ labelY: lp.y
53994
54161
  };
53995
54162
  const edgeLabelStyle = e.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
53996
54163
  legs.push({
@@ -54014,7 +54181,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54014
54181
  });
54015
54182
  }
54016
54183
  const labels = [];
54017
- const obstacles = [];
54184
+ const obstacles = [...topReserved];
54018
54185
  const regionLabelGuards = [];
54019
54186
  const markers = pois.map((p) => ({
54020
54187
  cx: p.cx,
@@ -54052,6 +54219,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54052
54219
  }
54053
54220
  const collides = (rect) => markers.some((m) => rectCircleOverlap(rect, m)) || obstacles.some((o) => rectsOverlap(rect, o)) || legSegments.some((s) => segmentRectOverlap(s[0], s[1], s[2], s[3], rect));
54054
54221
  const showRegionLabels = resolved.directives.noRegionLabels !== true;
54222
+ const labeledRegionIds = /* @__PURE__ */ new Set();
54055
54223
  const showRegionValues = resolved.directives.noRegionValue !== true && activeIsScore;
54056
54224
  const regionValueStr = (value) => showRegionValues && value !== void 0 ? compactNumber(value) : void 0;
54057
54225
  const isCompact = width < COMPACT_WIDTH_PX;
@@ -54072,7 +54240,9 @@ function layoutMap(resolvedIn, data, size, opts) {
54072
54240
  const REGION_FONT_MAX_ORIENT = 22;
54073
54241
  const REGION_SIZE_FRAC_MIN = 0.06;
54074
54242
  const REGION_SIZE_FRAC_MAX = 0.32;
54075
- const REGION_FADE_ORIENT = 45;
54243
+ const SUBJECT_FONT_MAX = 18;
54244
+ const SUBJECT_MIN_PROMINENCE = 13;
54245
+ const SUBJECT_LEADER_FONT = 15;
54076
54246
  const canvasLinear = Math.sqrt(Math.max(1, width * height));
54077
54247
  const sizeT = (boxW, boxH) => {
54078
54248
  const frac = Math.sqrt(Math.max(0, boxW * boxH)) / canvasLinear;
@@ -54084,9 +54254,9 @@ function layoutMap(resolvedIn, data, size, opts) {
54084
54254
  )
54085
54255
  );
54086
54256
  };
54087
- const pushRegionLabel = (x, y, text, fill2, lineNumber, valueLine, fontSize = FONT2, fade = 0) => {
54257
+ const pushRegionLabel = (x, y, text, fill2, lineNumber, valueLine, fontSize = FONT2, colorOverride) => {
54088
54258
  const { color: baseColor, haloColor } = labelOnFill(fill2);
54089
- const color = fade > 0 ? mix(baseColor, palette.bg, fade) : baseColor;
54259
+ const color = colorOverride ?? baseColor;
54090
54260
  const vf = Math.round(fontSize * 0.82);
54091
54261
  const halfW = Math.max(
54092
54262
  measureLegendText(text, fontSize),
@@ -54101,7 +54271,11 @@ function layoutMap(resolvedIn, data, size, opts) {
54101
54271
  text,
54102
54272
  anchor: "middle",
54103
54273
  color,
54104
- halo: overflows,
54274
+ // A backdrop label (colorOverride) NEVER haloes: the placement gate above
54275
+ // guarantees it sits on a floored-readable substrate (own fill, neutral
54276
+ // land, foreign land, or water), so a halo is pure noise. Data labels keep
54277
+ // the containment-gated halo until the role-tier pass (Phase 3).
54278
+ halo: colorOverride ? false : overflows,
54105
54279
  haloColor,
54106
54280
  ...fontSize !== FONT2 && { fontSize },
54107
54281
  ...valueLine !== void 0 && { valueLine },
@@ -54137,7 +54311,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54137
54311
  if (!c || !Number.isFinite(c[0])) return null;
54138
54312
  return { r, c, boxW, boxH, area: boxW * boxH, candidates };
54139
54313
  }).filter((e) => e !== null).sort((a, b) => b.area - a.area || a.r.lineNumber - b.r.lineNumber);
54140
- const placedRegionRects = [];
54314
+ const placedRegionRects = [...topReserved];
54141
54315
  const placedLeaders = [];
54142
54316
  const isEmptyFill = (f) => f === water || f === neutralFill || f === foreignFill;
54143
54317
  const POI_LABEL_PAD = 14;
@@ -54150,9 +54324,9 @@ function layoutMap(resolvedIn, data, size, opts) {
54150
54324
  const SHORT_HOP_MAX = Math.max(46, Math.min(width, height) * 0.11);
54151
54325
  const STEP = 4;
54152
54326
  const HOP_GAP = 9;
54153
- const tryShortHopCallout = (cx, cy, fill2, name, value) => {
54154
- const chipW = stackW(name, value);
54155
- const chipH = stackH(value !== void 0);
54327
+ const tryShortHopCallout = (cx, cy, fill2, name, value, font = FONT2, gap = HOP_GAP, maxLen = SHORT_HOP_MAX) => {
54328
+ const chipW = stackW(name, value, font);
54329
+ const chipH = stackH(value !== void 0, font);
54156
54330
  const dirs = [
54157
54331
  [0, -1],
54158
54332
  // north (over Canada)
@@ -54168,7 +54342,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54168
54342
  let ex = cx;
54169
54343
  let ey = cy;
54170
54344
  let steps = 0;
54171
- const maxSteps = Math.ceil(SHORT_HOP_MAX / STEP) + 1;
54345
+ const maxSteps = Math.ceil(maxLen / STEP) + 1;
54172
54346
  while (steps < maxSteps && fillAt(ex, ey) === fill2) {
54173
54347
  ex += dx * STEP;
54174
54348
  ey += dy * STEP;
@@ -54176,8 +54350,8 @@ function layoutMap(resolvedIn, data, size, opts) {
54176
54350
  }
54177
54351
  if (fillAt(ex, ey) === fill2) continue;
54178
54352
  const halfAlong = (Math.abs(dx) * chipW + Math.abs(dy) * chipH) / 2;
54179
- const ccx = ex + dx * (HOP_GAP + halfAlong);
54180
- const ccy = ey + dy * (HOP_GAP + halfAlong);
54353
+ const ccx = ex + dx * (gap + halfAlong);
54354
+ const ccy = ey + dy * (gap + halfAlong);
54181
54355
  const rect = {
54182
54356
  x: ccx - chipW / 2,
54183
54357
  y: ccy - chipH / 2,
@@ -54202,7 +54376,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54202
54376
  const innerX = ccx - dx * (chipW / 2);
54203
54377
  const innerY = ccy - dy * (chipH / 2);
54204
54378
  const len = Math.hypot(innerX - cx, innerY - cy);
54205
- if (len > SHORT_HOP_MAX) continue;
54379
+ if (len > maxLen) continue;
54206
54380
  if (placedLeaders.some(
54207
54381
  (l) => segmentsCross(cx, cy, innerX, innerY, l[0], l[1], l[2], l[3])
54208
54382
  ))
@@ -54226,6 +54400,78 @@ function layoutMap(resolvedIn, data, size, opts) {
54226
54400
  const valStr = regionValueStr(r.value);
54227
54401
  const fitsRegions = (rect) => !placedRegionRects.some((p) => rectsOverlap(rect, p));
54228
54402
  const fitsPois = (rect) => !poiObstacles.some((o) => rectsOverlap(rect, o));
54403
+ const isSubject = regionById.has(r.id);
54404
+ const extremesOnFill = (ax, ay, text) => {
54405
+ const halfW = measureLegendText(text, FONT2) / 2;
54406
+ return fillAt(ax - halfW, ay) === r.fill && fillAt(ax + halfW, ay) === r.fill;
54407
+ };
54408
+ if (isSubject && valStr === void 0 && r.label !== void 0) {
54409
+ const name = candidates[0];
54410
+ let best = -1;
54411
+ for (let f = SUBJECT_FONT_MAX; f >= FONT2; f--) {
54412
+ if (measureLegendText(name, f) > boxW || f + 2 * LABEL_PADY > boxH)
54413
+ continue;
54414
+ const rect = regionLabelRect(c[0], c[1], name, void 0, f);
54415
+ if (!fitsRegions(rect) || !fitsPois(rect) || collides(rect)) continue;
54416
+ const halfW = measureLegendText(name, f) / 2;
54417
+ if (fillAt(c[0] - halfW, c[1]) !== r.fill || fillAt(c[0] + halfW, c[1]) !== r.fill)
54418
+ continue;
54419
+ best = f;
54420
+ break;
54421
+ }
54422
+ if (best >= SUBJECT_MIN_PROMINENCE) {
54423
+ const rect = regionLabelRect(c[0], c[1], name, void 0, best);
54424
+ placedRegionRects.push(rect);
54425
+ pushRegionLabel(
54426
+ c[0],
54427
+ c[1],
54428
+ name,
54429
+ r.fill,
54430
+ r.lineNumber,
54431
+ void 0,
54432
+ best
54433
+ );
54434
+ labeledRegionIds.add(r.id);
54435
+ regionLabelGuards.push({ label: labels[labels.length - 1], rect });
54436
+ } else {
54437
+ const hop = tryShortHopCallout(
54438
+ c[0],
54439
+ c[1],
54440
+ r.fill,
54441
+ r.label,
54442
+ void 0,
54443
+ SUBJECT_LEADER_FONT,
54444
+ HOP_GAP + SUBJECT_LEADER_FONT,
54445
+ SHORT_HOP_MAX * 2.2
54446
+ );
54447
+ if (hop) {
54448
+ placedRegionRects.push(hop.rect);
54449
+ placedLeaders.push(hop.leader);
54450
+ const dark = mix(r.fill, palette.text, 60);
54451
+ labels.push({
54452
+ x: hop.x,
54453
+ y: hop.y,
54454
+ text: r.label,
54455
+ anchor: "middle",
54456
+ color: palette.text,
54457
+ halo: false,
54458
+ haloColor: palette.bg,
54459
+ fontSize: SUBJECT_LEADER_FONT,
54460
+ leader: {
54461
+ x1: hop.leader[0],
54462
+ y1: hop.leader[1],
54463
+ x2: hop.leader[2],
54464
+ y2: hop.leader[3]
54465
+ },
54466
+ leaderColor: dark,
54467
+ calloutDot: { x: c[0], y: c[1], color: dark },
54468
+ lineNumber: r.lineNumber
54469
+ });
54470
+ labeledRegionIds.add(r.id);
54471
+ }
54472
+ }
54473
+ continue;
54474
+ }
54229
54475
  const seekAnchors = [
54230
54476
  { x: c[0], y: c[1], guard: false }
54231
54477
  ];
@@ -54248,13 +54494,13 @@ function layoutMap(resolvedIn, data, size, opts) {
54248
54494
  const nameRect = regionLabelRect(a.x, a.y, t);
54249
54495
  if (valStr && stackW(t, valStr) <= boxW && stackH(true) <= boxH) {
54250
54496
  const stackRect = regionLabelRect(a.x, a.y, t, valStr);
54251
- if (fitsRegions(stackRect) && fitsPois(nameRect)) {
54497
+ if (fitsRegions(stackRect) && fitsPois(nameRect) && extremesOnFill(a.x, a.y, t)) {
54252
54498
  chosen = { text: t, valueLine: valStr, ax: a.x, ay: a.y };
54253
54499
  break;
54254
54500
  }
54255
54501
  }
54256
54502
  if (labelW(t) <= boxW && labelH <= boxH) {
54257
- if (fitsRegions(nameRect) && fitsPois(nameRect)) {
54503
+ if (fitsRegions(nameRect) && fitsPois(nameRect) && (valStr === void 0 || extremesOnFill(a.x, a.y, t))) {
54258
54504
  chosen = { text: t, ax: a.x, ay: a.y };
54259
54505
  break;
54260
54506
  }
@@ -54262,7 +54508,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54262
54508
  }
54263
54509
  if (chosen) break;
54264
54510
  }
54265
- if (chosen === void 0 && valStr && r.label !== void 0) {
54511
+ if (chosen === void 0 && r.label !== void 0 && isSubject) {
54266
54512
  const hop = tryShortHopCallout(c[0], c[1], r.fill, r.label, valStr);
54267
54513
  if (hop) {
54268
54514
  placedRegionRects.push(hop.rect);
@@ -54274,9 +54520,9 @@ function layoutMap(resolvedIn, data, size, opts) {
54274
54520
  text: r.label,
54275
54521
  anchor: "middle",
54276
54522
  color: palette.text,
54277
- halo: true,
54523
+ halo: false,
54278
54524
  haloColor: palette.bg,
54279
- valueLine: valStr,
54525
+ ...valStr !== void 0 && { valueLine: valStr },
54280
54526
  leader: {
54281
54527
  x1: hop.leader[0],
54282
54528
  y1: hop.leader[1],
@@ -54287,20 +54533,28 @@ function layoutMap(resolvedIn, data, size, opts) {
54287
54533
  calloutDot: { x: c[0], y: c[1], color: dark },
54288
54534
  lineNumber: r.lineNumber
54289
54535
  });
54536
+ labeledRegionIds.add(r.id);
54290
54537
  }
54291
54538
  continue;
54292
54539
  }
54293
54540
  if (chosen === void 0) continue;
54294
54541
  const isOrient = r.value === void 0 && r.layer === "base";
54295
54542
  let font = FONT2;
54296
- let fade = 0;
54297
54543
  if (isOrient) {
54298
54544
  const growT = sizeT(boxW, boxH);
54299
54545
  const desiredFont = Math.round(
54300
54546
  FONT2 + growT * (REGION_FONT_MAX_ORIENT - FONT2)
54301
54547
  );
54302
54548
  const hasVal = chosen.valueLine !== void 0;
54303
- for (let f = desiredFont; f > FONT2; f--) {
54549
+ const extremesClean = (f) => {
54550
+ const halfW = measureLegendText(chosen.text, f) / 2;
54551
+ return [chosen.ax - halfW, chosen.ax + halfW].every((sx) => {
54552
+ const fAt = fillAt(sx, chosen.ay);
54553
+ return fAt === r.fill || isEmptyFill(fAt);
54554
+ });
54555
+ };
54556
+ let fit = -1;
54557
+ for (let f = desiredFont; f >= FONT2; f--) {
54304
54558
  if (stackW(chosen.text, chosen.valueLine, f) > boxW || stackH(hasVal, f) > boxH)
54305
54559
  continue;
54306
54560
  const gRect = regionLabelRect(
@@ -54317,14 +54571,18 @@ function layoutMap(resolvedIn, data, size, opts) {
54317
54571
  void 0,
54318
54572
  f
54319
54573
  );
54320
- if (!fitsRegions(gRect) || !fitsPois(gName)) continue;
54321
- const halfW = measureLegendText(chosen.text, f) / 2;
54322
- if (fillAt(chosen.ax - halfW, chosen.ay) !== r.fill || fillAt(chosen.ax + halfW, chosen.ay) !== r.fill)
54574
+ if (!fitsRegions(gRect) || !fitsPois(gName) || collides(gName))
54323
54575
  continue;
54324
- font = f;
54576
+ if (!extremesClean(f)) continue;
54577
+ fit = f;
54325
54578
  break;
54326
54579
  }
54327
- fade = font > FONT2 ? Math.round(growT * REGION_FADE_ORIENT) : 0;
54580
+ if (fit < 0) {
54581
+ if (!frameContainers.has(r.id)) continue;
54582
+ font = FONT2;
54583
+ } else {
54584
+ font = fit;
54585
+ }
54328
54586
  }
54329
54587
  const rRect = regionLabelRect(
54330
54588
  chosen.ax,
@@ -54342,8 +54600,9 @@ function layoutMap(resolvedIn, data, size, opts) {
54342
54600
  r.lineNumber,
54343
54601
  chosen.valueLine,
54344
54602
  font,
54345
- fade
54603
+ isOrient ? backdropLandTone : void 0
54346
54604
  );
54605
+ labeledRegionIds.add(r.id);
54347
54606
  regionLabelGuards.push({
54348
54607
  label: labels[labels.length - 1],
54349
54608
  rect: rRect
@@ -54361,6 +54620,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54361
54620
  seed.lineNumber,
54362
54621
  valStr
54363
54622
  );
54623
+ labeledRegionIds.add(seed.iso);
54364
54624
  regionLabelGuards.push({
54365
54625
  label: labels[labels.length - 1],
54366
54626
  rect: regionLabelRect(seed.x, seed.y, text, valStr)
@@ -54792,13 +55052,30 @@ function layoutMap(resolvedIn, data, size, opts) {
54792
55052
  const nx = -dy / len;
54793
55053
  const ny = dx / len;
54794
55054
  const perp = labelH + 2;
55055
+ const pm = /^M(-?[\d.]+),(-?[\d.]+)(?:L(-?[\d.]+),(-?[\d.]+)|Q(-?[\d.]+),(-?[\d.]+) (-?[\d.]+),(-?[\d.]+))$/.exec(
55056
+ lg.d
55057
+ );
55058
+ const pointAt = (t) => {
55059
+ if (pm?.[5] !== void 0) {
55060
+ const u = 1 - t;
55061
+ return [
55062
+ u * u * +pm[1] + 2 * u * t * +pm[5] + t * t * +pm[7],
55063
+ u * u * +pm[2] + 2 * u * t * +pm[6] + t * t * +pm[8]
55064
+ ];
55065
+ }
55066
+ if (pm?.[3] !== void 0)
55067
+ return [
55068
+ +pm[1] + (+pm[3] - +pm[1]) * t,
55069
+ +pm[2] + (+pm[4] - +pm[2]) * t
55070
+ ];
55071
+ return [a.cx + dx * t, a.cy + dy * t];
55072
+ };
54795
55073
  const candidates = [];
54796
55074
  for (const off of [0, perp, -perp, 2 * perp, -2 * perp])
54797
- for (const t of T_LIST)
54798
- candidates.push([
54799
- a.cx + dx * t + nx * off,
54800
- a.cy + dy * t + ny * off - 4
54801
- ]);
55075
+ for (const t of T_LIST) {
55076
+ const [bx, by] = pointAt(t);
55077
+ candidates.push([bx + nx * off, by + ny * off - 4]);
55078
+ }
54802
55079
  const clean = ([cx2, cy2]) => {
54803
55080
  const rect2 = { x: cx2 - w / 2, y: cy2 - labelH / 2, w, h: labelH };
54804
55081
  if (rect2.x < 0 || rect2.x + w > width || rect2.y < 0 || rect2.y + rect2.h > height)
@@ -54808,10 +55085,8 @@ function layoutMap(resolvedIn, data, size, opts) {
54808
55085
  if (placedEdge.some((o) => rectsOverlap(o, rect2))) return false;
54809
55086
  return true;
54810
55087
  };
54811
- const [cx, cy] = candidates.find(clean) ?? [
54812
- a.cx + dx * 0.5,
54813
- a.cy + dy * 0.5 - 4
54814
- ];
55088
+ const mid = pointAt(0.5);
55089
+ const [cx, cy] = candidates.find(clean) ?? [mid[0], mid[1] - 4];
54815
55090
  const style = labelOnFill(fillAt(cx, cy));
54816
55091
  legs[i] = {
54817
55092
  ...lg,
@@ -54839,7 +55114,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54839
55114
  const rawCountries = [];
54840
55115
  for (const f of worldLayer.values()) {
54841
55116
  const iso = typeof f.id === "string" ? f.id : String(f.id ?? "");
54842
- if (!iso || regionById.has(iso)) continue;
55117
+ if (!iso || regionById.has(iso) || labeledRegionIds.has(iso)) continue;
54843
55118
  let hasReferencedSub = false;
54844
55119
  for (const k of regionById.keys())
54845
55120
  if (k.startsWith(iso + "-")) {
@@ -54913,7 +55188,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54913
55188
  );
54914
55189
  if (usLayer && framedStateContainers) {
54915
55190
  for (const [iso, f] of usLayer) {
54916
- if (regionById.has(iso)) continue;
55191
+ if (regionById.has(iso) || labeledRegionIds.has(iso)) continue;
54917
55192
  const viewF = cullFeatureToView(f);
54918
55193
  if (!viewF) continue;
54919
55194
  const b = path.bounds(viewF);
@@ -54937,6 +55212,11 @@ function layoutMap(resolvedIn, data, size, opts) {
54937
55212
  waterBodies: data.waterBodies,
54938
55213
  countries: countryCandidates,
54939
55214
  palette,
55215
+ // One muted, contrast-floored tone shared with the orientation backdrop
55216
+ // (region pass) so a context country (Canada) and an orientation region
55217
+ // (Minnesota) read identically — never below the WCAG-AA floor, no fade.
55218
+ countryTone: backdropLandTone,
55219
+ waterTone: backdropWaterTone,
54940
55220
  project,
54941
55221
  collides,
54942
55222
  contentPoints,
@@ -55490,19 +55770,60 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
55490
55770
  sel.style("cursor", "pointer").on("click", () => onClickItem(lineNumber));
55491
55771
  };
55492
55772
  const gLegs = svg.append("g").attr("class", "dgmo-map-legs").attr("fill", "none");
55493
- layout.legs.forEach((leg, i) => {
55494
- const p = gLegs.append("path").attr("d", leg.d).attr("stroke", leg.color).attr("stroke-width", leg.width).attr("stroke-linecap", "round").attr("data-from-id", leg.fromId).attr("data-to-id", leg.toId);
55773
+ const legEnd = (d) => {
55774
+ const m = /^M(-?[\d.]+),(-?[\d.]+)(?:L(-?[\d.]+),(-?[\d.]+)|Q(-?[\d.]+),(-?[\d.]+) (-?[\d.]+),(-?[\d.]+))$/.exec(
55775
+ d
55776
+ );
55777
+ if (!m) return null;
55778
+ const x0 = +m[1];
55779
+ const y0 = +m[2];
55780
+ let ex;
55781
+ let ey;
55782
+ let fromX;
55783
+ let fromY;
55784
+ if (m[3] !== void 0) {
55785
+ ex = +m[3];
55786
+ ey = +m[4];
55787
+ fromX = x0;
55788
+ fromY = y0;
55789
+ } else {
55790
+ ex = +m[7];
55791
+ ey = +m[8];
55792
+ fromX = +m[5];
55793
+ fromY = +m[6];
55794
+ }
55795
+ const dx = ex - fromX;
55796
+ const dy = ey - fromY;
55797
+ const len = Math.hypot(dx, dy);
55798
+ if (!(len > 0)) return null;
55799
+ return { ex, ey, tx: dx / len, ty: dy / len };
55800
+ };
55801
+ layout.legs.forEach((leg) => {
55802
+ const end = leg.arrow ? legEnd(leg.d) : null;
55803
+ let drawD = leg.d;
55804
+ let arrowTri = null;
55805
+ if (end) {
55806
+ const aLen = arrowSize(leg.width);
55807
+ const aHalf = Math.max(aLen * 0.5, leg.width * 0.6 + 1);
55808
+ const bx = end.ex - end.tx * aLen;
55809
+ const by = end.ey - end.ty * aLen;
55810
+ const px = -end.ty;
55811
+ const py = end.tx;
55812
+ arrowTri = `M${bx + px * aHalf},${by + py * aHalf}L${end.ex},${end.ey}L${bx - px * aHalf},${by - py * aHalf}z`;
55813
+ drawD = leg.d.replace(
55814
+ /(-?[\d.]+),(-?[\d.]+)$/,
55815
+ `${bx.toFixed(2)},${by.toFixed(2)}`
55816
+ );
55817
+ }
55818
+ const p = gLegs.append("path").attr("d", drawD).attr("stroke", leg.color).attr("stroke-width", leg.width).attr("stroke-linecap", "round").attr("data-from-id", leg.fromId).attr("data-to-id", leg.toId);
55495
55819
  if (leg.tags) {
55496
55820
  for (const [group, value] of Object.entries(leg.tags)) {
55497
55821
  p.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
55498
55822
  }
55499
55823
  }
55500
55824
  wireSync(p, leg.lineNumber);
55501
- if (leg.arrow) {
55502
- const id = nid(`dgmo-map-arrow-${i}`);
55503
- const s = arrowSize(leg.width);
55504
- defs.append("marker").attr("id", id).attr("viewBox", "0 0 10 10").attr("refX", 10).attr("refY", 5).attr("markerUnits", "userSpaceOnUse").attr("markerWidth", s).attr("markerHeight", s).attr("orient", "auto-start-reverse").append("path").attr("d", "M0,0L10,5L0,10z").attr("fill", leg.color);
55505
- p.attr("marker-end", `url(#${id})`);
55825
+ if (arrowTri) {
55826
+ gLegs.append("path").attr("d", arrowTri).attr("fill", leg.color).attr("stroke", leg.color).attr("stroke-width", 1).attr("stroke-linejoin", "round").attr("data-from-id", leg.fromId).attr("data-to-id", leg.toId);
55506
55827
  }
55507
55828
  if (leg.label !== void 0 && leg.labelX !== void 0) {
55508
55829
  const lt = emitText(
@@ -59129,9 +59450,9 @@ var init_renderer20 = __esm({
59129
59450
  }
59130
59451
  });
59131
59452
 
59132
- // node_modules/.pnpm/lz-string@1.5.0/node_modules/lz-string/libs/lz-string.js
59453
+ // ../../diagrammo/dgmo/node_modules/.pnpm/lz-string@1.5.0/node_modules/lz-string/libs/lz-string.js
59133
59454
  var require_lz_string = __commonJS({
59134
- "node_modules/.pnpm/lz-string@1.5.0/node_modules/lz-string/libs/lz-string.js"(exports, module) {
59455
+ "../../diagrammo/dgmo/node_modules/.pnpm/lz-string@1.5.0/node_modules/lz-string/libs/lz-string.js"(exports, module) {
59135
59456
  "use strict";
59136
59457
  var LZString = (function() {
59137
59458
  var f = String.fromCharCode;
@@ -65417,326 +65738,6 @@ var themes = {
65417
65738
 
65418
65739
  // src/advanced.ts
65419
65740
  init_dgmo_router();
65420
-
65421
- // src/migrate/index.ts
65422
- init_dgmo_router();
65423
- import {
65424
- readFileSync,
65425
- writeFileSync,
65426
- existsSync,
65427
- statSync,
65428
- readdirSync
65429
- } from "fs";
65430
- import { join, extname } from "path";
65431
-
65432
- // src/migrate/line-classifier.ts
65433
- function findUnsafePipePositions(line11) {
65434
- const positions = [];
65435
- let i = 0;
65436
- let inDouble = false;
65437
- let inSingle = false;
65438
- let braceDepth = 0;
65439
- let inArrow = false;
65440
- let arrowOpenChar = null;
65441
- while (i < line11.length) {
65442
- const ch = line11[i];
65443
- const ch2 = i + 1 < line11.length ? line11[i + 1] : "";
65444
- if (inDouble) {
65445
- if (ch === '"') inDouble = false;
65446
- i++;
65447
- continue;
65448
- }
65449
- if (inSingle) {
65450
- if (ch === "'") inSingle = false;
65451
- i++;
65452
- continue;
65453
- }
65454
- if (inArrow) {
65455
- if (arrowOpenChar === "-" && ch === "-" && ch2 === ">" || arrowOpenChar === "~" && ch === "~" && ch2 === ">") {
65456
- inArrow = false;
65457
- arrowOpenChar = null;
65458
- i += 2;
65459
- continue;
65460
- }
65461
- i++;
65462
- continue;
65463
- }
65464
- if (ch === '"') {
65465
- inDouble = true;
65466
- i++;
65467
- continue;
65468
- }
65469
- if (ch === "'") {
65470
- inSingle = true;
65471
- i++;
65472
- continue;
65473
- }
65474
- if (ch === "{") {
65475
- braceDepth++;
65476
- i++;
65477
- continue;
65478
- }
65479
- if (ch === "}") {
65480
- if (braceDepth > 0) braceDepth--;
65481
- i++;
65482
- continue;
65483
- }
65484
- if ((ch === "-" || ch === "~") && i > 0) {
65485
- const prev = line11[i - 1];
65486
- const onBoundary = prev === " " || prev === " ";
65487
- const nextIsLabelChar = ch2 !== "" && ch2 !== ch && ch2 !== ">" && ch2 !== " " && ch2 !== " ";
65488
- if (onBoundary && nextIsLabelChar) {
65489
- const closer = ch + ">";
65490
- const closeIdx = line11.indexOf(closer, i + 1);
65491
- if (closeIdx > i) {
65492
- const labelRegion = line11.substring(i + 1, closeIdx);
65493
- if (!labelRegion.includes("->") && !labelRegion.includes("~>")) {
65494
- inArrow = true;
65495
- arrowOpenChar = ch;
65496
- i++;
65497
- continue;
65498
- }
65499
- }
65500
- }
65501
- }
65502
- if (ch === "|" && braceDepth === 0) {
65503
- positions.push(i);
65504
- }
65505
- i++;
65506
- }
65507
- return positions;
65508
- }
65509
- function isLegacyMetadataLine(line11) {
65510
- return findUnsafePipePositions(line11).length > 0;
65511
- }
65512
-
65513
- // src/migrate/line-transformer.ts
65514
- function replaceUnsafePipesWithCommas(s) {
65515
- const positions = findUnsafePipePositions(s);
65516
- if (positions.length === 0) return s;
65517
- const chars = s.split("");
65518
- for (const pos of positions) {
65519
- chars[pos] = ",";
65520
- }
65521
- return chars.join("");
65522
- }
65523
- var GANTT_PERCENT = {
65524
- match: (t) => /^\d+(?:\.\d+)?\s*%$/.test(t.trim()),
65525
- promote: (t) => `progress: ${t.trim().replace(/\s*%$/, "")}`
65526
- };
65527
- var JOURNEY_SCORE = {
65528
- match: (t) => /^\d+(?:\s+[A-Za-z][A-Za-z0-9_-]*)?$/.test(t.trim()),
65529
- promote: (t) => {
65530
- const m = t.trim().match(/^(\d+)(?:\s+([A-Za-z][A-Za-z0-9_-]*))?$/);
65531
- if (!m) return t;
65532
- return m[2] ? `score: ${m[1]}, emotion: ${m[2]}` : `score: ${m[1]}`;
65533
- }
65534
- };
65535
- var PROMOTIONS_BY_CHART = {
65536
- gantt: [GANTT_PERCENT],
65537
- "journey-map": [JOURNEY_SCORE]
65538
- };
65539
- var BARE_DESCRIPTION_CHARTS = /* @__PURE__ */ new Set(["pyramid", "ring"]);
65540
- function tokenizeMetaRegion(region) {
65541
- const tokens = [];
65542
- let i = 0;
65543
- let buf = "";
65544
- let inQuote = null;
65545
- while (i < region.length) {
65546
- const ch = region[i];
65547
- if (inQuote) {
65548
- buf += ch;
65549
- if (ch === inQuote) inQuote = null;
65550
- i++;
65551
- continue;
65552
- }
65553
- if (ch === '"' || ch === "'") {
65554
- inQuote = ch;
65555
- buf += ch;
65556
- i++;
65557
- continue;
65558
- }
65559
- if (ch === ",") {
65560
- const t = buf.trim();
65561
- if (t) tokens.push(t);
65562
- buf = "";
65563
- i++;
65564
- continue;
65565
- }
65566
- buf += ch;
65567
- i++;
65568
- }
65569
- const tail = buf.trim();
65570
- if (tail) tokens.push(tail);
65571
- return tokens;
65572
- }
65573
- function transformLine(line11, chartType) {
65574
- const indentMatch = line11.match(/^[ \t]*/);
65575
- const indent = indentMatch ? indentMatch[0] : "";
65576
- const body = line11.substring(indent.length);
65577
- const pipePositions = findUnsafePipePositions(body);
65578
- if (pipePositions.length === 0) {
65579
- return { line: line11, changed: false };
65580
- }
65581
- const cut = pipePositions[0];
65582
- const nameRegion = body.substring(0, cut).trimEnd();
65583
- let metaRegion = body.substring(cut + 1).trimStart();
65584
- if (pipePositions.length > 1) {
65585
- metaRegion = replaceUnsafePipesWithCommas(metaRegion);
65586
- }
65587
- if (chartType !== null && BARE_DESCRIPTION_CHARTS.has(chartType) && !metaRegion.includes(":")) {
65588
- const text = metaRegion.trim();
65589
- if (!text) {
65590
- return {
65591
- line: indent + nameRegion,
65592
- changed: true
65593
- };
65594
- }
65595
- const value = text.includes(",") ? `"${text}"` : text;
65596
- return {
65597
- line: `${indent}${nameRegion} description: ${value}`,
65598
- changed: true
65599
- };
65600
- }
65601
- const tokens = tokenizeMetaRegion(metaRegion);
65602
- const promotions = chartType ? PROMOTIONS_BY_CHART[chartType] ?? [] : [];
65603
- const emitted = [];
65604
- for (const raw of tokens) {
65605
- if (raw.includes(":")) {
65606
- emitted.push(raw);
65607
- continue;
65608
- }
65609
- const promo = promotions.find((p) => p.match(raw));
65610
- if (promo) {
65611
- emitted.push(promo.promote(raw));
65612
- continue;
65613
- }
65614
- emitted.push(raw);
65615
- }
65616
- if (emitted.length === 0) {
65617
- return {
65618
- line: indent + nameRegion,
65619
- changed: true
65620
- };
65621
- }
65622
- return {
65623
- line: `${indent}${nameRegion} ${emitted.join(", ")}`,
65624
- changed: true
65625
- };
65626
- }
65627
-
65628
- // src/migrate/data-comma.ts
65629
- init_colors();
65630
- var DATA_COMMA_CHART_TYPES = /* @__PURE__ */ new Set([
65631
- // Simple charts (chart.ts)
65632
- "bar",
65633
- "line",
65634
- "multi-line",
65635
- "pie",
65636
- "doughnut",
65637
- "area",
65638
- "polar-area",
65639
- "radar",
65640
- "bar-stacked",
65641
- // Extended charts (echarts.ts)
65642
- "scatter",
65643
- "heatmap",
65644
- "sankey",
65645
- "chord",
65646
- "funnel",
65647
- // Visualizations (d3 / visualizations/parse.ts)
65648
- "quadrant",
65649
- "slope",
65650
- "arc",
65651
- "wordcloud"
65652
- ]);
65653
- var COLOR_SET = new Set(RECOGNIZED_COLOR_NAMES);
65654
- var NUMERIC_VALUE_RE = /^-?\d[\d,_]*(?:\.\d+)?,?$/;
65655
- function isNumericValueToken(tok) {
65656
- if (!NUMERIC_VALUE_RE.test(tok)) return false;
65657
- return /\d/.test(tok);
65658
- }
65659
- function migrateDataRowCommaLine(line11) {
65660
- const indentMatch = line11.match(/^[ \t]*/);
65661
- const indent = indentMatch ? indentMatch[0] : "";
65662
- const body = line11.substring(indent.length);
65663
- if (!body || body.startsWith("//")) return { line: line11, changed: false };
65664
- const tokens = body.split(/\s+/);
65665
- if (tokens.length < 2) return { line: line11, changed: false };
65666
- let i = tokens.length - 1;
65667
- if (i >= 1 && COLOR_SET.has(tokens[i])) {
65668
- i--;
65669
- }
65670
- const valueStart = (() => {
65671
- let j = i;
65672
- while (j >= 1 && isNumericValueToken(tokens[j])) j--;
65673
- return j + 1;
65674
- })();
65675
- if (valueStart > i || valueStart < 1) return { line: line11, changed: false };
65676
- const valueTokens = tokens.slice(valueStart, i + 1);
65677
- const hasComma = valueTokens.some((t) => t.includes(","));
65678
- if (!hasComma) return { line: line11, changed: false };
65679
- const cleaned = valueTokens.map((t) => t.replace(/,/g, ""));
65680
- const labelPart = tokens.slice(0, valueStart);
65681
- const tail = tokens.slice(i + 1);
65682
- const rebuiltBody = [...labelPart, ...cleaned, ...tail].join(" ");
65683
- return { line: indent + rebuiltBody, changed: rebuiltBody !== body };
65684
- }
65685
-
65686
- // src/migrate/index.ts
65687
- function migrateContent(source) {
65688
- const chartType = parseDgmoChartType(source);
65689
- const lines = source.split("\n");
65690
- const out = [];
65691
- const changedLines = [];
65692
- let changed = false;
65693
- const migrateDataCommas = chartType !== null && DATA_COMMA_CHART_TYPES.has(chartType);
65694
- let firstContentSeen = false;
65695
- for (let i = 0; i < lines.length; i++) {
65696
- let line11 = lines[i];
65697
- const trimmed = line11.trim();
65698
- if (migrateDataCommas && firstContentSeen && trimmed && !trimmed.startsWith("//")) {
65699
- const dc = migrateDataRowCommaLine(line11);
65700
- if (dc.changed) {
65701
- line11 = dc.line;
65702
- changed = true;
65703
- changedLines.push(i + 1);
65704
- }
65705
- }
65706
- if (trimmed && !trimmed.startsWith("//")) firstContentSeen = true;
65707
- if (!isLegacyMetadataLine(line11)) {
65708
- out.push(line11);
65709
- continue;
65710
- }
65711
- const result = transformLine(line11, chartType);
65712
- if (result.changed) {
65713
- changed = true;
65714
- if (!changedLines.includes(i + 1)) changedLines.push(i + 1);
65715
- }
65716
- out.push(result.line);
65717
- }
65718
- return {
65719
- migrated: out.join("\n"),
65720
- changed,
65721
- chartType,
65722
- changedLines
65723
- };
65724
- }
65725
- function formatLineDiff(path, original, migrated) {
65726
- const origLines = original.split("\n");
65727
- const newLines = migrated.split("\n");
65728
- const lines = [`--- ${path}`, `+++ ${path}`];
65729
- const max = Math.max(origLines.length, newLines.length);
65730
- for (let i = 0; i < max; i++) {
65731
- const a = origLines[i];
65732
- const b = newLines[i];
65733
- if (a === b) continue;
65734
- if (a !== void 0) lines.push(`@@ line ${i + 1}
65735
- - ${a}`);
65736
- if (b !== void 0) lines.push(`+ ${b}`);
65737
- }
65738
- return lines.join("\n");
65739
- }
65740
65741
  export {
65741
65742
  ALL_CHART_TYPES,
65742
65743
  ARROW_DIAGNOSTIC_CODES,
@@ -65805,12 +65806,10 @@ export {
65805
65806
  extractSymbols as extractFlowchartSymbols,
65806
65807
  extractSymbols4 as extractInfraSymbols,
65807
65808
  extractPertSymbols,
65808
- findUnsafePipePositions,
65809
65809
  focusBoxesAndLines,
65810
65810
  focusOrgTree,
65811
65811
  formatDateLabel,
65812
65812
  formatDgmoError,
65813
- formatLineDiff,
65814
65813
  getAllChartTypes,
65815
65814
  getAvailablePalettes,
65816
65815
  getExtendedChartLegendGroups,
@@ -65831,7 +65830,6 @@ export {
65831
65830
  isArchiveColumn,
65832
65831
  isExtendedChartType,
65833
65832
  isInvalidColorToken,
65834
- isLegacyMetadataLine,
65835
65833
  isRecognizedColorName,
65836
65834
  isSequenceBlock,
65837
65835
  isSequenceNote,
@@ -65868,7 +65866,6 @@ export {
65868
65866
  mapExportDimensions,
65869
65867
  mapNeutralLandColor,
65870
65868
  measurePertAnalysisBlock,
65871
- migrateContent,
65872
65869
  mix,
65873
65870
  mulberry32,
65874
65871
  nearestNamedColor,
@@ -65996,7 +65993,6 @@ export {
65996
65993
  tidewaterPalette,
65997
65994
  tint,
65998
65995
  tokyoNightPalette,
65999
- transformLine,
66000
65996
  truncateBareUrl,
66001
65997
  parseDgmo as validate,
66002
65998
  validateComputed,