@diagrammo/dgmo 0.34.0 → 0.36.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 +41 -43
  2. package/.github/copilot-instructions.md +1 -1
  3. package/README.md +7 -1
  4. package/SKILL.md +12 -7
  5. package/dist/advanced.cjs +460 -463
  6. package/dist/advanced.d.cts +2 -51
  7. package/dist/advanced.d.ts +2 -51
  8. package/dist/advanced.js +460 -464
  9. package/dist/auto.cjs +461 -140
  10. package/dist/auto.js +91 -91
  11. package/dist/auto.mjs +461 -140
  12. package/dist/cli.cjs +143 -326
  13. package/dist/index.cjs +460 -139
  14. package/dist/index.js +460 -139
  15. package/docs/ai-integration.md +47 -25
  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.cjs CHANGED
@@ -37,6 +37,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
37
37
  function makeDgmoError(line11, message, severity = "error", code) {
38
38
  return code !== void 0 ? { line: line11, message, severity, code } : { line: line11, message, severity };
39
39
  }
40
+ function dedupeDiagnostics(diagnostics) {
41
+ const seen = /* @__PURE__ */ new Set();
42
+ const out = [];
43
+ for (const d of diagnostics) {
44
+ const key = `${d.line}\0${d.column ?? ""}\0${d.severity}\0${d.code ?? ""}\0${d.message}`;
45
+ if (seen.has(key)) continue;
46
+ seen.add(key);
47
+ out.push(d);
48
+ }
49
+ return out;
50
+ }
40
51
  function formatDgmoError(err) {
41
52
  return err.line > 0 ? `Line ${err.line}: ${err.message}` : err.message;
42
53
  }
@@ -94,7 +105,7 @@ function vennAliasKeywordRemovedMessage(args) {
94
105
  return `Venn 'alias' keyword was removed. Use 'as' instead \u2014 '${args.name} as ${args.alias}'.`;
95
106
  }
96
107
  function pipeOperatorRemovedMessage() {
97
- return `'|' removed \u2014 'Node | c: red' \u2192 'Node c: red'. Run 'dgmo migrate'`;
108
+ return `'|' removed \u2014 'Node | c: red' \u2192 'Node c: red'`;
98
109
  }
99
110
  function ganttBarePercentRemovedMessage(percent) {
100
111
  return `Bare-percent progress shorthand '| ${percent}' was removed. Use 'progress: ${percent.replace(/%$/, "")}' (integer 0\u2013100, no '%').`;
@@ -1247,7 +1258,7 @@ var init_parsing = __esm({
1247
1258
  "no-title",
1248
1259
  "no-notes"
1249
1260
  ]);
1250
- MULTIPLE_PIPE_ERROR = 'Use a single "|" to start metadata, then separate items with commas.';
1261
+ 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.";
1251
1262
  RESERVED_KEY_RE = /^[A-Za-z][A-Za-z0-9_-]*$/;
1252
1263
  AS_ALIAS_RE = /\s+as\s+([A-Za-z][A-Za-z0-9_]{0,11})$/;
1253
1264
  POTENTIAL_META_RE = /\b([\w-]+)\s*:/;
@@ -1428,12 +1439,10 @@ function validateTagValues(entities, tagGroups, pushWarning, suggestFn) {
1428
1439
  );
1429
1440
  if (!isPrefix) {
1430
1441
  const defined = group.entries.map((e) => e.value);
1431
- let msg = `Unknown value '${value}' for tag group '${group.name}'`;
1442
+ let msg = `Unknown value '${value}' for tag group '${group.name}' \u2014 use one of: ${defined.join(", ")}`;
1432
1443
  const hint = suggestFn?.(value, defined);
1433
1444
  if (hint) {
1434
1445
  msg += `. ${hint}`;
1435
- } else {
1436
- msg += ` \u2014 defined values: ${defined.join(", ")}`;
1437
1446
  }
1438
1447
  pushWarning(entity.lineNumber, msg);
1439
1448
  }
@@ -2209,7 +2218,10 @@ function parseVisualizationFull(content, palette) {
2209
2218
  lineNumber
2210
2219
  });
2211
2220
  } else {
2212
- warn(lineNumber, `Unrecognized era entry: "${line11}"`);
2221
+ warn(
2222
+ lineNumber,
2223
+ `Unrecognized era entry: "${line11}" \u2014 use '<start> -> <end> Label [color]' (e.g. '1716 -> 1717 Reign').`
2224
+ );
2213
2225
  }
2214
2226
  continue;
2215
2227
  }
@@ -2237,7 +2249,10 @@ function parseVisualizationFull(content, palette) {
2237
2249
  lineNumber
2238
2250
  });
2239
2251
  } else {
2240
- warn(lineNumber, `Unrecognized marker entry: "${line11}"`);
2252
+ warn(
2253
+ lineNumber,
2254
+ `Unrecognized marker entry: "${line11}" \u2014 use '<date> Label [color]' (e.g. '1776 Independence').`
2255
+ );
2241
2256
  }
2242
2257
  continue;
2243
2258
  }
@@ -2915,7 +2930,7 @@ function parseVisualizationFull(content, palette) {
2915
2930
  result.diagnostics.push(
2916
2931
  makeDgmoError(
2917
2932
  ov.lineNumber,
2918
- `Intersection references unknown set or alias "${ref}"`
2933
+ `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)`
2919
2934
  )
2920
2935
  );
2921
2936
  if (!result.error)
@@ -6298,10 +6313,10 @@ function parseSequenceDgmo(content, palette) {
6298
6313
  const rawInside = fallbackMatch[1];
6299
6314
  const pipeIdx = rawInside.indexOf("|");
6300
6315
  const cleanName = rawInside.substring(0, pipeIdx).trim().replace(/\([^)]*\)$/, "").trim();
6301
- const metaPart = rawInside.substring(pipeIdx).trim();
6316
+ const metaPart = rawInside.substring(pipeIdx).replace(/^\|\s*/, "").trim();
6302
6317
  pushError(
6303
6318
  lineNumber,
6304
- `Pipe metadata must go outside brackets \u2014 use '[${cleanName}] ${metaPart}' instead of '[${rawInside.trim()}]'`
6319
+ `Metadata goes after the bracket with no pipe \u2014 use '[${cleanName}] ${metaPart}' (e.g. '[Backend] t: Product'), not '[${rawInside.trim()}]'`
6305
6320
  );
6306
6321
  continue;
6307
6322
  }
@@ -6911,7 +6926,7 @@ function parseSequenceDgmo(content, palette) {
6911
6926
  }
6912
6927
  pushWarning(
6913
6928
  lineNumber,
6914
- `Unexpected line: '${trimmed}'. Expected a message (A -> B), participant, section (== Name ==), or block keyword (if/loop/parallel).`
6929
+ `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>').`
6915
6930
  );
6916
6931
  }
6917
6932
  if (!hasExplicitChart && result.messages.length === 0) {
@@ -8073,7 +8088,11 @@ function parseFlowchart(content, palette) {
8073
8088
  const value = optMatch[2].trim();
8074
8089
  if (key === "no-color") {
8075
8090
  result.diagnostics.push(
8076
- makeDgmoError(lineNumber, '"no-color" has been removed.', "warning")
8091
+ makeDgmoError(
8092
+ lineNumber,
8093
+ '"no-color" was removed \u2014 node colors are assigned automatically by shape; delete this line.',
8094
+ "warning"
8095
+ )
8077
8096
  );
8078
8097
  continue;
8079
8098
  }
@@ -8402,7 +8421,11 @@ function parseState(content, palette) {
8402
8421
  const value = optMatch[2].trim();
8403
8422
  if (key === "no-color") {
8404
8423
  result.diagnostics.push(
8405
- makeDgmoError(lineNumber, '"no-color" has been removed.', "warning")
8424
+ makeDgmoError(
8425
+ lineNumber,
8426
+ '"no-color" was removed \u2014 state colors are assigned automatically; delete this line.',
8427
+ "warning"
8428
+ )
8406
8429
  );
8407
8430
  continue;
8408
8431
  }
@@ -8717,7 +8740,7 @@ function parseClassDiagram(content, palette) {
8717
8740
  result.diagnostics.push(
8718
8741
  makeDgmoError(
8719
8742
  lineNumber,
8720
- '"no-auto-color" has been removed.',
8743
+ '"no-auto-color" was removed \u2014 colors are always auto-assigned from the palette; delete this line.',
8721
8744
  "warning"
8722
8745
  )
8723
8746
  );
@@ -8832,7 +8855,11 @@ function parseClassDiagram(content, palette) {
8832
8855
  continue;
8833
8856
  }
8834
8857
  result.diagnostics.push(
8835
- makeDgmoError(lineNumber, `Unexpected line: '${trimmed}'.`, "warning")
8858
+ makeDgmoError(
8859
+ lineNumber,
8860
+ `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)`,
8861
+ "warning"
8862
+ )
8836
8863
  );
8837
8864
  }
8838
8865
  if (notes.length > 0) {
@@ -9310,7 +9337,11 @@ function parseERDiagram(content, palette) {
9310
9337
  continue;
9311
9338
  }
9312
9339
  result.diagnostics.push(
9313
- makeDgmoError(lineNumber, `Unexpected line: '${trimmed}'.`, "warning")
9340
+ makeDgmoError(
9341
+ lineNumber,
9342
+ `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)`,
9343
+ "warning"
9344
+ )
9314
9345
  );
9315
9346
  }
9316
9347
  if (result.tables.length === 0 && !result.error) {
@@ -9654,7 +9685,10 @@ function parseOrg(content, palette) {
9654
9685
  const value = metadataMatch[2].trim();
9655
9686
  const parent = findMetadataParent(indent, indentStack);
9656
9687
  if (!parent) {
9657
- pushError(lineNumber, "Metadata has no parent node");
9688
+ pushError(
9689
+ lineNumber,
9690
+ "Metadata 'key: value' must be indented under a node \u2014 no node precedes it."
9691
+ );
9658
9692
  } else {
9659
9693
  parent.metadata = { ...parent.metadata, [key]: value };
9660
9694
  }
@@ -9672,7 +9706,10 @@ function parseOrg(content, palette) {
9672
9706
  );
9673
9707
  attachNode(node, indent, indentStack, result);
9674
9708
  } else {
9675
- pushError(lineNumber, "Metadata has no parent node");
9709
+ pushError(
9710
+ lineNumber,
9711
+ "Metadata 'key: value' must be indented under a node \u2014 no node precedes it."
9712
+ );
9676
9713
  }
9677
9714
  } else {
9678
9715
  const node = parseNodeLabel(
@@ -10021,7 +10058,10 @@ function parseSitemap(content, palette) {
10021
10058
  if (arrowInfo) {
10022
10059
  const source = findParentNode(indent, indentStack);
10023
10060
  if (!source) {
10024
- pushError(lineNumber, "Arrow has no source node");
10061
+ pushError(
10062
+ lineNumber,
10063
+ "Arrow '-> target' must be indented under its source page \u2014 no page precedes it."
10064
+ );
10025
10065
  } else {
10026
10066
  deferredArrows.push({
10027
10067
  sourceNode: source,
@@ -10091,7 +10131,10 @@ function parseSitemap(content, palette) {
10091
10131
  const value = metadataMatch[2].trim();
10092
10132
  const parent = findParentNode(indent, indentStack);
10093
10133
  if (!parent) {
10094
- pushError(lineNumber, "Metadata has no parent node");
10134
+ pushError(
10135
+ lineNumber,
10136
+ "Metadata 'key: value' must be indented under a page \u2014 no page precedes it."
10137
+ );
10095
10138
  } else {
10096
10139
  parent.metadata = { ...parent.metadata, [key]: value };
10097
10140
  }
@@ -10111,7 +10154,10 @@ function parseSitemap(content, palette) {
10111
10154
  const key = normalizeName(node.label);
10112
10155
  labelToNode.set(key, node);
10113
10156
  } else {
10114
- pushError(lineNumber, "Metadata has no parent node");
10157
+ pushError(
10158
+ lineNumber,
10159
+ "Metadata 'key: value' must be indented under a page \u2014 no page precedes it."
10160
+ );
10115
10161
  }
10116
10162
  } else {
10117
10163
  const descResult = tryStripDescriptionKeyword(trimmed);
@@ -11929,7 +11975,7 @@ function parseChart(content, palette) {
11929
11975
  result.diagnostics.push(
11930
11976
  makeDgmoError(
11931
11977
  lineNumber,
11932
- `'${trimmed}' \u2014 ## syntax is no longer supported. Use [Group] containers instead`
11978
+ `'${trimmed}' removed \u2014 use a bracket container '[${trimmed.replace(/^#+\s*/, "")}]' instead (## headers are no longer supported).`
11933
11979
  )
11934
11980
  );
11935
11981
  continue;
@@ -12899,7 +12945,11 @@ function parseExtendedChartFull(content, palette) {
12899
12945
  continue;
12900
12946
  }
12901
12947
  result.diagnostics.push(
12902
- makeDgmoError(lineNumber, `Unexpected line: '${trimmed}'.`, "warning")
12948
+ makeDgmoError(
12949
+ lineNumber,
12950
+ `Unexpected line: '${trimmed}'. Expected a data row ('Label value', space-separated) or a known option.`,
12951
+ "warning"
12952
+ )
12903
12953
  );
12904
12954
  }
12905
12955
  const warn = (line11, message) => {
@@ -15261,7 +15311,10 @@ function parseKanban(content, palette) {
15261
15311
  continue;
15262
15312
  }
15263
15313
  if (!currentColumn) {
15264
- warn(lineNumber, "Card line found before any column");
15314
+ warn(
15315
+ lineNumber,
15316
+ "Card line found before any column. Declare a column first with '[Column Name]', then indent cards beneath it. (\xA711)"
15317
+ );
15265
15318
  continue;
15266
15319
  }
15267
15320
  if (currentCard && indent > cardBaseIndent) {
@@ -15400,7 +15453,7 @@ var init_parser13 = __esm({
15400
15453
  KNOWN_OPTIONS4 = /* @__PURE__ */ new Set(["hide", "active-tag"]);
15401
15454
  KNOWN_BOOLEANS3 = /* @__PURE__ */ new Set(["solid-fill", "no-title"]);
15402
15455
  REMOVED_BOOLEANS = {
15403
- "no-auto-color": '"no-auto-color" has been removed.'
15456
+ "no-auto-color": '"no-auto-color" was removed \u2014 colors are always auto-assigned from the palette; delete this line.'
15404
15457
  };
15405
15458
  }
15406
15459
  });
@@ -15637,7 +15690,7 @@ function parseC4(content, palette) {
15637
15690
  } else {
15638
15691
  pushError(
15639
15692
  lineNumber,
15640
- `"container ${refName}" must be inside a deployment node`
15693
+ `"container ${refName}" must be indented under a deployment node (e.g. under 'Prod is a deployment-node')`
15641
15694
  );
15642
15695
  }
15643
15696
  continue;
@@ -15692,7 +15745,10 @@ function parseC4(content, palette) {
15692
15745
  indent
15693
15746
  });
15694
15747
  } else {
15695
- pushError(lineNumber, `"${sectionType}" must be inside an element`);
15748
+ pushError(
15749
+ lineNumber,
15750
+ `"${sectionType}" must be indented under an element (e.g. under 'Web App is a container')`
15751
+ );
15696
15752
  }
15697
15753
  continue;
15698
15754
  }
@@ -15719,7 +15775,10 @@ function parseC4(content, palette) {
15719
15775
  indent
15720
15776
  });
15721
15777
  } else {
15722
- pushError(lineNumber, `Group [${groupName}] must be inside an element`);
15778
+ pushError(
15779
+ lineNumber,
15780
+ `Group [${groupName}] must be indented under an element (e.g. under 'Web App is a container')`
15781
+ );
15723
15782
  }
15724
15783
  continue;
15725
15784
  }
@@ -16049,7 +16108,10 @@ function parseC4(content, palette) {
16049
16108
  }
16050
16109
  desc.push(descResult.text);
16051
16110
  } else {
16052
- pushError(lineNumber, `Unexpected content: "${trimmed}"`);
16111
+ pushError(
16112
+ lineNumber,
16113
+ `Unexpected content: "${trimmed}". Expected an element ('Web App is a container'), a relationship ('-Uses-> Database'), indented metadata ('tech: React'), or a group ('[Backend]'). (\xA78)`
16114
+ );
16053
16115
  }
16054
16116
  }
16055
16117
  finalizeAutoTagColors(result.tagGroups, palette);
@@ -16111,7 +16173,7 @@ function validateRelationshipTargets(result, knownNames, pushWarning) {
16111
16173
  for (const el of elements) {
16112
16174
  for (const rel of el.relationships) {
16113
16175
  if (!knownNames.has(normalizeName(rel.target))) {
16114
- let msg = `Relationship target "${rel.target}" not found`;
16176
+ 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`;
16115
16177
  const hint = suggest(normalizeName(rel.target), allNames);
16116
16178
  if (hint) msg += `. ${hint}`;
16117
16179
  pushWarning(rel.lineNumber, msg, "warning");
@@ -16139,7 +16201,7 @@ function validateDeploymentRefs(result, knownNames, pushWarning) {
16139
16201
  for (const node of nodes) {
16140
16202
  for (const ref of node.containerRefs) {
16141
16203
  if (!knownNames.has(normalizeName(ref))) {
16142
- let msg = `Deployment reference "container ${ref}" not found`;
16204
+ let msg = `Deployment reference "container ${ref}" not found \u2014 declare the container element first, then reference it under the deployment node`;
16143
16205
  const hint = suggest(normalizeName(ref), allNames);
16144
16206
  if (hint) msg += `. ${hint}`;
16145
16207
  pushWarning(node.lineNumber, msg, "warning");
@@ -16858,7 +16920,7 @@ function parseInfra(content) {
16858
16920
  }
16859
16921
  warn(
16860
16922
  lineNumber,
16861
- `Invalid tag value '${trimmed}' in tag group '${currentTagGroup.name}'.`
16923
+ `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 '|'.`
16862
16924
  );
16863
16925
  continue;
16864
16926
  }
@@ -16931,7 +16993,7 @@ function parseInfra(content) {
16931
16993
  const n = deprecatedFanout[1];
16932
16994
  setError(
16933
16995
  lineNumber,
16934
- `'x${n}' fanout syntax is no longer supported \u2014 use '| fanout: ${n}' instead`
16996
+ `'x${n}' fanout syntax is no longer supported \u2014 use 'fanout: ${n}' instead`
16935
16997
  );
16936
16998
  continue;
16937
16999
  }
@@ -17105,8 +17167,10 @@ function parseInfra(content) {
17105
17167
  const hint = suggest(key, allKeys);
17106
17168
  const valueLooksNumeric = /^[\d.]+%?$/.test(rawVal);
17107
17169
  if (hint || valueLooksNumeric) {
17108
- let msg = `Unknown property '${key}'.`;
17170
+ let msg = `Unknown infra property '${key}'.`;
17109
17171
  if (hint) msg += ` ${hint}`;
17172
+ else
17173
+ msg += ` Properties are colon-keyed 'key: value' from the known schema (e.g. latency-ms, max-rps, cache-hit, instances, concurrency).`;
17110
17174
  warn(lineNumber, msg);
17111
17175
  } else if (!currentNode.isEdge) {
17112
17176
  pushDescription(currentNode, trimmed);
@@ -17117,7 +17181,7 @@ function parseInfra(content) {
17117
17181
  if (EDGE_ONLY_KEYS.has(key) && !currentNode.isEdge) {
17118
17182
  warn(
17119
17183
  lineNumber,
17120
- `Property '${key}' is only valid on the entry point (Edge/Internet).`
17184
+ `Property '${key}' belongs on the entry point \u2014 declare it indented under an 'internet' or 'edge' node, not on '${currentNode.label}'.`
17121
17185
  );
17122
17186
  }
17123
17187
  const value = parsePropertyValue(rawVal);
@@ -17224,7 +17288,7 @@ function parseInfra(content) {
17224
17288
  }
17225
17289
  warn(
17226
17290
  lineNumber,
17227
- currentGroup ? `Unexpected line: '${trimmed}'. Expected an indented component name.` : `Unexpected line: '${trimmed}'. Expected a component name, [Group Name], tag group, or option.`
17291
+ 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'.`
17228
17292
  );
17229
17293
  }
17230
17294
  finishCurrentNode();
@@ -17600,7 +17664,10 @@ function parseGantt(content, palette) {
17600
17664
  continue;
17601
17665
  }
17602
17666
  if (COMMENT_RE.test(line11)) continue;
17603
- warn(lineNumber, `Unrecognized holiday entry: "${line11}"`);
17667
+ warn(
17668
+ lineNumber,
17669
+ `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)`
17670
+ );
17604
17671
  continue;
17605
17672
  }
17606
17673
  }
@@ -17626,7 +17693,10 @@ function parseGantt(content, palette) {
17626
17693
  lineNumber
17627
17694
  });
17628
17695
  } else {
17629
- warn(lineNumber, `Unrecognized era entry: "${line11}"`);
17696
+ warn(
17697
+ lineNumber,
17698
+ `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)`
17699
+ );
17630
17700
  }
17631
17701
  continue;
17632
17702
  }
@@ -17652,7 +17722,10 @@ function parseGantt(content, palette) {
17652
17722
  lineNumber
17653
17723
  });
17654
17724
  } else {
17655
- warn(lineNumber, `Unrecognized marker entry: "${line11}"`);
17725
+ warn(
17726
+ lineNumber,
17727
+ `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)`
17728
+ );
17656
17729
  }
17657
17730
  continue;
17658
17731
  }
@@ -17818,7 +17891,7 @@ function parseGantt(content, palette) {
17818
17891
  } else {
17819
17892
  softError(
17820
17893
  lineNumber,
17821
- `Arrow reference "${content2.name}" has no parent task context.`
17894
+ `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)`
17822
17895
  );
17823
17896
  }
17824
17897
  }
@@ -19038,7 +19111,10 @@ function parseMap(content, palette) {
19038
19111
  function handleTag(trimmed, line11) {
19039
19112
  const m = matchTagBlockHeading(trimmed);
19040
19113
  if (!m) {
19041
- pushError(line11, `Malformed tag declaration: "${trimmed}".`);
19114
+ pushError(
19115
+ line11,
19116
+ `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.`
19117
+ );
19042
19118
  return;
19043
19119
  }
19044
19120
  emitTagLegacyDiagnostic(m, line11, diagnostics);
@@ -19150,7 +19226,7 @@ function parseMap(content, palette) {
19150
19226
  if (!pos || pos.kind === "name" && !pos.name) {
19151
19227
  pushError(
19152
19228
  line11,
19153
- "route requires an origin: `route <origin> [style: arc]`."
19229
+ "route needs an origin place \u2014 `route <origin>` then indented legs (`-> dest`); origin may be a city name, IATA code, or `<lat> <lon>`. (\xA724B)"
19154
19230
  );
19155
19231
  return;
19156
19232
  }
@@ -19213,7 +19289,10 @@ function parseMap(content, palette) {
19213
19289
  function handleEdges(trimmed, line11) {
19214
19290
  const parts = trimmed.split(ARROW_SPLIT);
19215
19291
  if (parts.length < 3) {
19216
- pushError(line11, `Malformed edge: "${trimmed}".`);
19292
+ pushError(
19293
+ line11,
19294
+ `Malformed edge: "${trimmed}" \u2014 an edge is 'A -> B' (or 'A -label-> B', '~>' for arc, '--' undirected), both endpoints non-empty. (\xA724B)`
19295
+ );
19217
19296
  return;
19218
19297
  }
19219
19298
  const endpoints = [];
@@ -19236,7 +19315,10 @@ function parseMap(content, palette) {
19236
19315
  const from = endpoints[k];
19237
19316
  const to = endpoints[k + 1];
19238
19317
  if (!from || !to) {
19239
- pushError(line11, `Edge has an empty endpoint: "${trimmed}".`);
19318
+ pushError(
19319
+ line11,
19320
+ `Edge has an empty endpoint: "${trimmed}" \u2014 both sides of the arrow need a place, e.g. 'Paris -> Berlin'. (\xA724B)`
19321
+ );
19240
19322
  continue;
19241
19323
  }
19242
19324
  const isLast = k === links.length - 1;
@@ -21355,7 +21437,10 @@ function parseTechRadar(content) {
21355
21437
  }
21356
21438
  if (indent === 0 && trimmed.toLowerCase() === "rings") {
21357
21439
  if (result.rings.length > 0) {
21358
- warn(lineNumber, 'Duplicate "rings" block \u2014 using last one');
21440
+ warn(
21441
+ lineNumber,
21442
+ 'Duplicate "rings" block \u2014 only the last is kept; merge all ring names into one "rings" block.'
21443
+ );
21359
21444
  result.rings = [];
21360
21445
  }
21361
21446
  inRingsBlock = true;
@@ -21574,7 +21659,7 @@ function parseTechRadar(content) {
21574
21659
  result.diagnostics.push(
21575
21660
  makeDgmoError(
21576
21661
  lineNumber,
21577
- `Blip "${trimmed}" has no ring assignment. Place it under a ring section header or use: ${trimmed} | ring: RingName`
21662
+ `Blip "${trimmed}" has no ring. Put it under a ring section header, or add a colon-keyed ring \u2014 '${trimmed} ring: RingName' (per \xA720).`
21578
21663
  )
21579
21664
  );
21580
21665
  continue;
@@ -21583,7 +21668,7 @@ function parseTechRadar(content) {
21583
21668
  if (!trimmed.includes("|") && headerParsed && result.rings.length > 0) {
21584
21669
  warn(
21585
21670
  lineNumber,
21586
- `Unrecognized line "${trimmed}". Quadrant headers require pipe metadata: ${trimmed} | quadrant: top-left`
21671
+ `Unrecognized line "${trimmed}". A quadrant header is 'Name quadrant: <top-left|top-right|bottom-left|bottom-right>' (per \xA720).`
21587
21672
  );
21588
21673
  }
21589
21674
  }
@@ -21876,7 +21961,10 @@ function parseCycle(content) {
21876
21961
  currentNode.description.push(descLine);
21877
21962
  continue;
21878
21963
  }
21879
- warn(lineNum, `Unexpected indented line: "${trimmed}".`);
21964
+ warn(
21965
+ lineNum,
21966
+ `Indented line "${trimmed}" has no step above it \u2014 add a step on a non-indented line first, then indent its details. (\xA721)`
21967
+ );
21880
21968
  continue;
21881
21969
  }
21882
21970
  }
@@ -22179,7 +22267,7 @@ function parseJourneyMap(content, palette) {
22179
22267
  if (trimmed.includes("|") || !PHASE_RE.test(trimmed)) {
22180
22268
  warn(
22181
22269
  lineNumber,
22182
- "Steps outside phases will be ignored when phases are present"
22270
+ `Step "${trimmed}" sits outside any phase and is ignored \u2014 indent it under a '[Phase]' header (per \xA722).`
22183
22271
  );
22184
22272
  continue;
22185
22273
  }
@@ -22236,7 +22324,10 @@ function parseJourneyMap(content, palette) {
22236
22324
  continue;
22237
22325
  }
22238
22326
  if (indent > 0 && !currentPhase && hasPhases) {
22239
- warn(lineNumber, `Unexpected indented line outside of a phase`);
22327
+ warn(
22328
+ lineNumber,
22329
+ `Indented line outside any phase \u2014 add a '[Phase]' header above it, or unindent it to a top-level step. (\xA722)`
22330
+ );
22240
22331
  }
22241
22332
  }
22242
22333
  if (currentStep) {
@@ -22582,7 +22673,10 @@ function parsePyramid(content) {
22582
22673
  continue;
22583
22674
  }
22584
22675
  if (!currentLayer) {
22585
- warn(lineNum, `Unexpected indented line: "${trimmed}".`);
22676
+ warn(
22677
+ lineNum,
22678
+ `Indented description "${trimmed}" has no layer above it \u2014 add a layer name on a non-indented line first. (\xA723)`
22679
+ );
22586
22680
  continue;
22587
22681
  }
22588
22682
  const descLine = trimmed.startsWith("- ") ? `\u2022 ${trimmed.substring(2)}` : trimmed;
@@ -22756,7 +22850,10 @@ function parseRing(content) {
22756
22850
  continue;
22757
22851
  }
22758
22852
  if (!currentLayer) {
22759
- warn(lineNum, `Unexpected indented line: "${trimmed}".`);
22853
+ warn(
22854
+ lineNum,
22855
+ `Indented description "${trimmed}" has no layer above it \u2014 add a layer name on a non-indented line first. (\xA724)`
22856
+ );
22760
22857
  continue;
22761
22858
  }
22762
22859
  const descLine = trimmed.startsWith("- ") ? `\u2022 ${trimmed.substring(2)}` : trimmed;
@@ -23482,7 +23579,10 @@ function parseRaci(content, palette) {
23482
23579
  continue;
23483
23580
  }
23484
23581
  if (bodyStarted) {
23485
- errorAt(lineNumber, `Unexpected line: '${trimmed}'.`);
23582
+ errorAt(
23583
+ lineNumber,
23584
+ `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)`
23585
+ );
23486
23586
  } else {
23487
23587
  warn(lineNumber, `Unrecognized directive: '${trimmed}'.`);
23488
23588
  }
@@ -24029,6 +24129,13 @@ function getAllChartTypes() {
24029
24129
  return chartTypes.map((c) => c.id);
24030
24130
  }
24031
24131
  function parseDgmo(content) {
24132
+ const result = parseDgmoUndeduped(content);
24133
+ return {
24134
+ diagnostics: dedupeDiagnostics(result.diagnostics),
24135
+ chartType: result.chartType
24136
+ };
24137
+ }
24138
+ function parseDgmoUndeduped(content) {
24032
24139
  const chartType = parseDgmoChartType(content);
24033
24140
  if (!chartType) {
24034
24141
  const colonDiag = detectColonChartType(content);
@@ -51669,7 +51776,7 @@ function resolveMap(parsed, data) {
51669
51776
  const hint = suggest(r.name, allNames);
51670
51777
  err(
51671
51778
  r.lineNumber,
51672
- `Unknown subdivision "${r.name}".${hint ? " " + hint : ""}`,
51779
+ `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.`,
51673
51780
  "E_MAP_UNKNOWN_SUBDIVISION"
51674
51781
  );
51675
51782
  continue;
@@ -51749,7 +51856,7 @@ function resolveMap(parsed, data) {
51749
51856
  const hint = suggest(name, cityNames);
51750
51857
  err(
51751
51858
  line11,
51752
- `Unknown place "${name}" (not in the gazetteer; use coordinates).${hint ? " " + hint : ""}`,
51859
+ `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>\`.`,
51753
51860
  "E_MAP_UNKNOWN_PLACE"
51754
51861
  );
51755
51862
  return { kind: "miss" };
@@ -52291,6 +52398,21 @@ function mapLegendBand(legend, opts) {
52291
52398
  if (height <= 0) return 0;
52292
52399
  return mapLegendTop(opts.hasTitle, opts.hasSubtitle) + height + LEGEND_BOTTOM_GAP2;
52293
52400
  }
52401
+ function mapLegendBox(legend, opts) {
52402
+ if (!legend) return null;
52403
+ const groups = mapLegendGroups(legend);
52404
+ if (groups.length === 0) return null;
52405
+ const config = mapLegendConfig(groups, opts.mode);
52406
+ const state = { activeGroup: legend.activeGroup };
52407
+ const { width, height } = computeLegendLayout(config, state, opts.width);
52408
+ if (height <= 0 || width <= 0) return null;
52409
+ return {
52410
+ x: (opts.width - width) / 2,
52411
+ y: mapLegendTop(opts.hasTitle, opts.hasSubtitle),
52412
+ width,
52413
+ height
52414
+ };
52415
+ }
52294
52416
  var LEGEND_TOP_GAP2, LEGEND_BOTTOM_GAP2;
52295
52417
  var init_legend_band = __esm({
52296
52418
  "src/map/legend-band.ts"() {
@@ -52415,6 +52537,8 @@ function placeContextLabels(args) {
52415
52537
  waterBodies,
52416
52538
  countries,
52417
52539
  palette,
52540
+ countryTone,
52541
+ waterTone,
52418
52542
  project,
52419
52543
  collides,
52420
52544
  contentPoints,
@@ -52424,8 +52548,8 @@ function placeContextLabels(args) {
52424
52548
  const band = tierBand(Math.max(dLonSpan, dLatSpan));
52425
52549
  const budget = labelBudget(width, height, band);
52426
52550
  if (budget <= 0) return [];
52427
- const waterColor = mix(palette.colors.blue, palette.textMuted, 50);
52428
- const countryColor = palette.textMuted;
52551
+ const waterColor = waterTone ?? mix(palette.colors.blue, palette.textMuted, 50);
52552
+ const countryColor = countryTone ?? palette.textMuted;
52429
52553
  const haloColor = palette.bg;
52430
52554
  const candidates = [];
52431
52555
  const center = [width / 2, height / 2];
@@ -52514,8 +52638,7 @@ function placeContextLabels(args) {
52514
52638
  )
52515
52639
  );
52516
52640
  const fontSize = Math.round(FONT + t * (COUNTRY_FONT_MAX - FONT));
52517
- const fade = Math.round(t * COUNTRY_FADE_MAX);
52518
- const color = fade > 0 ? mix(countryColor, palette.bg, 100 - fade) : countryColor;
52641
+ const color = countryColor;
52519
52642
  const text = c.name;
52520
52643
  const tw = labelWidth(text, 0, fontSize);
52521
52644
  if (tw > w || fontSize + 2 * PADY > h) continue;
@@ -52609,7 +52732,7 @@ function placeContextLabels(args) {
52609
52732
  }
52610
52733
  return placed;
52611
52734
  }
52612
- 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;
52735
+ 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;
52613
52736
  var init_context_labels = __esm({
52614
52737
  "src/map/context-labels.ts"() {
52615
52738
  "use strict";
@@ -52626,7 +52749,6 @@ var init_context_labels = __esm({
52626
52749
  COUNTRY_FONT_MAX = 22;
52627
52750
  COUNTRY_SIZE_FRAC_MIN = 0.06;
52628
52751
  COUNTRY_SIZE_FRAC_MAX = 0.32;
52629
- COUNTRY_FADE_MAX = 45;
52630
52752
  MAX_COUNTRY_POSITIONS = 4;
52631
52753
  COUNTRY_POS_GRID = 5;
52632
52754
  COUNTRY_POS_TOPN_MARGIN = 3;
@@ -52753,36 +52875,23 @@ function countryLabelPositions(args) {
52753
52875
  return n;
52754
52876
  };
52755
52877
  const dist2ToCentre = (c) => (c.sx - cx) ** 2 + (c.sy - cy) ** 2;
52756
- const pool = [...kept];
52757
- pool.sort((a, b) => {
52758
- const d = interiorness(b) - interiorness(a);
52759
- return d !== 0 ? d : dist2ToCentre(a) - dist2ToCentre(b);
52760
- });
52761
- const ordered = [pool.shift()];
52762
- while (ordered.length < MAX_COUNTRY_POSITIONS && pool.length) {
52763
- let bestIdx = 0;
52764
- let bestMin = -1;
52765
- for (let k = 0; k < pool.length; k++) {
52766
- const c = pool[k];
52767
- let minD = Infinity;
52768
- for (const o of ordered) {
52769
- const d = (c.sx - o.sx) ** 2 + (c.sy - o.sy) ** 2;
52770
- if (d < minD) minD = d;
52771
- }
52772
- if (minD > bestMin) {
52773
- bestMin = minD;
52774
- bestIdx = k;
52775
- }
52878
+ const centreLon = kept.reduce((s, c) => s + c.lon, 0) / kept.length;
52879
+ const centreLat = kept.reduce((s, c) => s + c.lat, 0) / kept.length;
52880
+ const centreScreen = project(centreLon, centreLat);
52881
+ 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 ? [
52882
+ {
52883
+ lonLat: [centreLon, centreLat],
52884
+ screen: [centreScreen[0], centreScreen[1]]
52776
52885
  }
52777
- ordered.push(pool.splice(bestIdx, 1)[0]);
52778
- }
52779
- const grid = ordered.map((c) => ({
52886
+ ] : [];
52887
+ const grid = [...kept].sort(
52888
+ (a, b) => interiorness(b) - interiorness(a) || dist2ToCentre(a) - dist2ToCentre(b)
52889
+ ).map((c) => ({
52780
52890
  lonLat: [c.lon, c.lat],
52781
52891
  screen: [c.sx, c.sy]
52782
52892
  }));
52783
52893
  const curatedPos = curated ? mkCurated(curated, project) : [];
52784
- const out = [...curatedPos, ...grid].slice(0, MAX_COUNTRY_POSITIONS);
52785
- return out;
52894
+ return [...curatedPos, ...lead, ...grid].slice(0, MAX_COUNTRY_POSITIONS);
52786
52895
  }
52787
52896
  function mkCurated(curated, project) {
52788
52897
  if (!curated) return [];
@@ -53048,6 +53157,25 @@ function layoutMap(resolvedIn, data, size, opts) {
53048
53157
  palette.bg,
53049
53158
  mutedBasemap ? isDark ? MUTED_FOREIGN_DARK : MUTED_FOREIGN_LIGHT : isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
53050
53159
  );
53160
+ const contrastFloorTone = (tone, ...substrates) => {
53161
+ const clears = (t) => substrates.every((s) => contrastRatio(t, s) >= REGION_LABEL_HALO_RATIO);
53162
+ if (clears(tone)) return tone;
53163
+ for (let s = 10; s <= 100; s += 10) {
53164
+ const t = mix(palette.text, tone, s);
53165
+ if (clears(t)) return t;
53166
+ }
53167
+ return palette.text;
53168
+ };
53169
+ const backdropLandTone = contrastFloorTone(
53170
+ palette.textMuted,
53171
+ neutralFill,
53172
+ water
53173
+ );
53174
+ const backdropWaterTone = contrastFloorTone(
53175
+ mix(palette.colors.blue, palette.textMuted, 50),
53176
+ water,
53177
+ neutralFill
53178
+ );
53051
53179
  const hasDirectColor = resolved.regions.some((r) => r.color !== void 0);
53052
53180
  const colorizeActive = resolved.directives.noColorize !== true && !hasRamp && !hasDirectColor && fillGroupNames.size === 0;
53053
53181
  const colorByIso = /* @__PURE__ */ new Map();
@@ -53162,6 +53290,33 @@ function layoutMap(resolvedIn, data, size, opts) {
53162
53290
  hasSubtitle: Boolean(resolved.subtitle)
53163
53291
  });
53164
53292
  if (legendBand > topPad) topPad = legendBand;
53293
+ const topReserved = [];
53294
+ if (shownTitle) {
53295
+ const lines = resolved.subtitle ? 2 : 1;
53296
+ const tw = Math.max(
53297
+ measureLegendText(shownTitle, TITLE_FONT_SIZE),
53298
+ resolved.subtitle ? measureLegendText(resolved.subtitle, TITLE_FONT_SIZE) : 0
53299
+ ) + 2 * TITLE_FONT_SIZE;
53300
+ topReserved.push({
53301
+ x: width / 2 - tw / 2,
53302
+ y: TITLE_Y - TITLE_FONT_SIZE,
53303
+ w: tw,
53304
+ h: TITLE_FONT_SIZE * (lines + 0.5)
53305
+ });
53306
+ }
53307
+ const legendBox = mapLegendBox(legend, {
53308
+ width,
53309
+ mode: opts.legendMode ?? "preview",
53310
+ hasTitle: Boolean(shownTitle),
53311
+ hasSubtitle: Boolean(resolved.subtitle)
53312
+ });
53313
+ if (legendBox)
53314
+ topReserved.push({
53315
+ x: legendBox.x,
53316
+ y: legendBox.y,
53317
+ w: legendBox.width,
53318
+ h: legendBox.height
53319
+ });
53165
53320
  const reserve = opts._calloutReserve;
53166
53321
  const fitLeft = FIT_PAD + (reserve?.left ?? 0);
53167
53322
  const fitRight = width - FIT_PAD - (reserve?.right ?? 0);
@@ -53891,6 +54046,19 @@ function layoutMap(resolvedIn, data, size, opts) {
53891
54046
  const by = b.cy - (b.cy - py) / tb * trimB;
53892
54047
  return `M${ax},${ay}Q${px},${py} ${bx},${by}`;
53893
54048
  };
54049
+ const legLabelPoint = (a, b, curved, offset) => {
54050
+ const mx = (a.cx + b.cx) / 2;
54051
+ const my = (a.cy + b.cy) / 2;
54052
+ if (!curved && offset === 0) return { x: mx, y: my - 4 };
54053
+ const dx = b.cx - a.cx;
54054
+ const dy = b.cy - a.cy;
54055
+ const len = Math.hypot(dx, dy) || 1;
54056
+ const nx = -dy / len;
54057
+ const ny = dx / len;
54058
+ const bow = offset !== 0 ? offset : len * ARC_CURVE_FRAC;
54059
+ const off = bow * 0.5 + Math.sign(bow || 1) * 8;
54060
+ return { x: mx + nx * off, y: my + ny * off };
54061
+ };
53894
54062
  const routeLegVals = resolved.routes.flatMap((rt) => rt.legs).map((l) => Number(l.value)).filter((n) => Number.isFinite(n) && n > 0);
53895
54063
  const rlMin = routeLegVals.length ? Math.min(...routeLegVals) : 0;
53896
54064
  const rlMax = routeLegVals.length ? Math.max(...routeLegVals) : 0;
@@ -53904,13 +54072,12 @@ function layoutMap(resolvedIn, data, size, opts) {
53904
54072
  const a = poiScreen.get(leg.fromId);
53905
54073
  const b = poiScreen.get(leg.toId);
53906
54074
  if (!a || !b) continue;
53907
- const mx = (a.cx + b.cx) / 2;
53908
- const my = (a.cy + b.cy) / 2;
54075
+ const lp = legLabelPoint(a, b, leg.style === "arc", 0);
53909
54076
  const bow = {
53910
54077
  curved: leg.style === "arc",
53911
54078
  offset: 0,
53912
- labelX: mx,
53913
- labelY: my - 4
54079
+ labelX: lp.x,
54080
+ labelY: lp.y
53914
54081
  };
53915
54082
  const routeLabelStyle = leg.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
53916
54083
  legs.push({
@@ -53957,13 +54124,13 @@ function layoutMap(resolvedIn, data, size, opts) {
53957
54124
  const b = poiScreen.get(e.toId);
53958
54125
  if (!a || !b) return;
53959
54126
  const fanOffset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
53960
- const mx = (a.cx + b.cx) / 2;
53961
- const my = (a.cy + b.cy) / 2;
54127
+ const curved = e.style === "arc" || n > 1;
54128
+ const lp = legLabelPoint(a, b, curved, fanOffset);
53962
54129
  const bow = {
53963
- curved: e.style === "arc" || n > 1,
54130
+ curved,
53964
54131
  offset: fanOffset,
53965
- labelX: mx,
53966
- labelY: my - 4
54132
+ labelX: lp.x,
54133
+ labelY: lp.y
53967
54134
  };
53968
54135
  const edgeLabelStyle = e.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
53969
54136
  legs.push({
@@ -53987,7 +54154,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53987
54154
  });
53988
54155
  }
53989
54156
  const labels = [];
53990
- const obstacles = [];
54157
+ const obstacles = [...topReserved];
53991
54158
  const regionLabelGuards = [];
53992
54159
  const markers = pois.map((p) => ({
53993
54160
  cx: p.cx,
@@ -54025,6 +54192,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54025
54192
  }
54026
54193
  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));
54027
54194
  const showRegionLabels = resolved.directives.noRegionLabels !== true;
54195
+ const labeledRegionIds = /* @__PURE__ */ new Set();
54028
54196
  const showRegionValues = resolved.directives.noRegionValue !== true && activeIsScore;
54029
54197
  const regionValueStr = (value) => showRegionValues && value !== void 0 ? compactNumber(value) : void 0;
54030
54198
  const isCompact = width < COMPACT_WIDTH_PX;
@@ -54045,7 +54213,9 @@ function layoutMap(resolvedIn, data, size, opts) {
54045
54213
  const REGION_FONT_MAX_ORIENT = 22;
54046
54214
  const REGION_SIZE_FRAC_MIN = 0.06;
54047
54215
  const REGION_SIZE_FRAC_MAX = 0.32;
54048
- const REGION_FADE_ORIENT = 45;
54216
+ const SUBJECT_FONT_MAX = 18;
54217
+ const SUBJECT_MIN_PROMINENCE = 13;
54218
+ const SUBJECT_LEADER_FONT = 15;
54049
54219
  const canvasLinear = Math.sqrt(Math.max(1, width * height));
54050
54220
  const sizeT = (boxW, boxH) => {
54051
54221
  const frac = Math.sqrt(Math.max(0, boxW * boxH)) / canvasLinear;
@@ -54057,9 +54227,9 @@ function layoutMap(resolvedIn, data, size, opts) {
54057
54227
  )
54058
54228
  );
54059
54229
  };
54060
- const pushRegionLabel = (x, y, text, fill2, lineNumber, valueLine, fontSize = FONT2, fade = 0) => {
54230
+ const pushRegionLabel = (x, y, text, fill2, lineNumber, valueLine, fontSize = FONT2, colorOverride) => {
54061
54231
  const { color: baseColor, haloColor } = labelOnFill(fill2);
54062
- const color = fade > 0 ? mix(baseColor, palette.bg, fade) : baseColor;
54232
+ const color = colorOverride ?? baseColor;
54063
54233
  const vf = Math.round(fontSize * 0.82);
54064
54234
  const halfW = Math.max(
54065
54235
  measureLegendText(text, fontSize),
@@ -54074,7 +54244,11 @@ function layoutMap(resolvedIn, data, size, opts) {
54074
54244
  text,
54075
54245
  anchor: "middle",
54076
54246
  color,
54077
- halo: overflows,
54247
+ // A backdrop label (colorOverride) NEVER haloes: the placement gate above
54248
+ // guarantees it sits on a floored-readable substrate (own fill, neutral
54249
+ // land, foreign land, or water), so a halo is pure noise. Data labels keep
54250
+ // the containment-gated halo until the role-tier pass (Phase 3).
54251
+ halo: colorOverride ? false : overflows,
54078
54252
  haloColor,
54079
54253
  ...fontSize !== FONT2 && { fontSize },
54080
54254
  ...valueLine !== void 0 && { valueLine },
@@ -54110,7 +54284,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54110
54284
  if (!c || !Number.isFinite(c[0])) return null;
54111
54285
  return { r, c, boxW, boxH, area: boxW * boxH, candidates };
54112
54286
  }).filter((e) => e !== null).sort((a, b) => b.area - a.area || a.r.lineNumber - b.r.lineNumber);
54113
- const placedRegionRects = [];
54287
+ const placedRegionRects = [...topReserved];
54114
54288
  const placedLeaders = [];
54115
54289
  const isEmptyFill = (f) => f === water || f === neutralFill || f === foreignFill;
54116
54290
  const POI_LABEL_PAD = 14;
@@ -54123,9 +54297,9 @@ function layoutMap(resolvedIn, data, size, opts) {
54123
54297
  const SHORT_HOP_MAX = Math.max(46, Math.min(width, height) * 0.11);
54124
54298
  const STEP = 4;
54125
54299
  const HOP_GAP = 9;
54126
- const tryShortHopCallout = (cx, cy, fill2, name, value) => {
54127
- const chipW = stackW(name, value);
54128
- const chipH = stackH(value !== void 0);
54300
+ const tryShortHopCallout = (cx, cy, fill2, name, value, font = FONT2, gap = HOP_GAP, maxLen = SHORT_HOP_MAX) => {
54301
+ const chipW = stackW(name, value, font);
54302
+ const chipH = stackH(value !== void 0, font);
54129
54303
  const dirs = [
54130
54304
  [0, -1],
54131
54305
  // north (over Canada)
@@ -54141,7 +54315,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54141
54315
  let ex = cx;
54142
54316
  let ey = cy;
54143
54317
  let steps = 0;
54144
- const maxSteps = Math.ceil(SHORT_HOP_MAX / STEP) + 1;
54318
+ const maxSteps = Math.ceil(maxLen / STEP) + 1;
54145
54319
  while (steps < maxSteps && fillAt(ex, ey) === fill2) {
54146
54320
  ex += dx * STEP;
54147
54321
  ey += dy * STEP;
@@ -54149,8 +54323,8 @@ function layoutMap(resolvedIn, data, size, opts) {
54149
54323
  }
54150
54324
  if (fillAt(ex, ey) === fill2) continue;
54151
54325
  const halfAlong = (Math.abs(dx) * chipW + Math.abs(dy) * chipH) / 2;
54152
- const ccx = ex + dx * (HOP_GAP + halfAlong);
54153
- const ccy = ey + dy * (HOP_GAP + halfAlong);
54326
+ const ccx = ex + dx * (gap + halfAlong);
54327
+ const ccy = ey + dy * (gap + halfAlong);
54154
54328
  const rect = {
54155
54329
  x: ccx - chipW / 2,
54156
54330
  y: ccy - chipH / 2,
@@ -54175,7 +54349,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54175
54349
  const innerX = ccx - dx * (chipW / 2);
54176
54350
  const innerY = ccy - dy * (chipH / 2);
54177
54351
  const len = Math.hypot(innerX - cx, innerY - cy);
54178
- if (len > SHORT_HOP_MAX) continue;
54352
+ if (len > maxLen) continue;
54179
54353
  if (placedLeaders.some(
54180
54354
  (l) => segmentsCross(cx, cy, innerX, innerY, l[0], l[1], l[2], l[3])
54181
54355
  ))
@@ -54199,6 +54373,78 @@ function layoutMap(resolvedIn, data, size, opts) {
54199
54373
  const valStr = regionValueStr(r.value);
54200
54374
  const fitsRegions = (rect) => !placedRegionRects.some((p) => rectsOverlap(rect, p));
54201
54375
  const fitsPois = (rect) => !poiObstacles.some((o) => rectsOverlap(rect, o));
54376
+ const isSubject = regionById.has(r.id);
54377
+ const extremesOnFill = (ax, ay, text) => {
54378
+ const halfW = measureLegendText(text, FONT2) / 2;
54379
+ return fillAt(ax - halfW, ay) === r.fill && fillAt(ax + halfW, ay) === r.fill;
54380
+ };
54381
+ if (isSubject && valStr === void 0 && r.label !== void 0) {
54382
+ const name = candidates[0];
54383
+ let best = -1;
54384
+ for (let f = SUBJECT_FONT_MAX; f >= FONT2; f--) {
54385
+ if (measureLegendText(name, f) > boxW || f + 2 * LABEL_PADY > boxH)
54386
+ continue;
54387
+ const rect = regionLabelRect(c[0], c[1], name, void 0, f);
54388
+ if (!fitsRegions(rect) || !fitsPois(rect) || collides(rect)) continue;
54389
+ const halfW = measureLegendText(name, f) / 2;
54390
+ if (fillAt(c[0] - halfW, c[1]) !== r.fill || fillAt(c[0] + halfW, c[1]) !== r.fill)
54391
+ continue;
54392
+ best = f;
54393
+ break;
54394
+ }
54395
+ if (best >= SUBJECT_MIN_PROMINENCE) {
54396
+ const rect = regionLabelRect(c[0], c[1], name, void 0, best);
54397
+ placedRegionRects.push(rect);
54398
+ pushRegionLabel(
54399
+ c[0],
54400
+ c[1],
54401
+ name,
54402
+ r.fill,
54403
+ r.lineNumber,
54404
+ void 0,
54405
+ best
54406
+ );
54407
+ labeledRegionIds.add(r.id);
54408
+ regionLabelGuards.push({ label: labels[labels.length - 1], rect });
54409
+ } else {
54410
+ const hop = tryShortHopCallout(
54411
+ c[0],
54412
+ c[1],
54413
+ r.fill,
54414
+ r.label,
54415
+ void 0,
54416
+ SUBJECT_LEADER_FONT,
54417
+ HOP_GAP + SUBJECT_LEADER_FONT,
54418
+ SHORT_HOP_MAX * 2.2
54419
+ );
54420
+ if (hop) {
54421
+ placedRegionRects.push(hop.rect);
54422
+ placedLeaders.push(hop.leader);
54423
+ const dark = mix(r.fill, palette.text, 60);
54424
+ labels.push({
54425
+ x: hop.x,
54426
+ y: hop.y,
54427
+ text: r.label,
54428
+ anchor: "middle",
54429
+ color: palette.text,
54430
+ halo: false,
54431
+ haloColor: palette.bg,
54432
+ fontSize: SUBJECT_LEADER_FONT,
54433
+ leader: {
54434
+ x1: hop.leader[0],
54435
+ y1: hop.leader[1],
54436
+ x2: hop.leader[2],
54437
+ y2: hop.leader[3]
54438
+ },
54439
+ leaderColor: dark,
54440
+ calloutDot: { x: c[0], y: c[1], color: dark },
54441
+ lineNumber: r.lineNumber
54442
+ });
54443
+ labeledRegionIds.add(r.id);
54444
+ }
54445
+ }
54446
+ continue;
54447
+ }
54202
54448
  const seekAnchors = [
54203
54449
  { x: c[0], y: c[1], guard: false }
54204
54450
  ];
@@ -54221,13 +54467,13 @@ function layoutMap(resolvedIn, data, size, opts) {
54221
54467
  const nameRect = regionLabelRect(a.x, a.y, t);
54222
54468
  if (valStr && stackW(t, valStr) <= boxW && stackH(true) <= boxH) {
54223
54469
  const stackRect = regionLabelRect(a.x, a.y, t, valStr);
54224
- if (fitsRegions(stackRect) && fitsPois(nameRect)) {
54470
+ if (fitsRegions(stackRect) && fitsPois(nameRect) && extremesOnFill(a.x, a.y, t)) {
54225
54471
  chosen = { text: t, valueLine: valStr, ax: a.x, ay: a.y };
54226
54472
  break;
54227
54473
  }
54228
54474
  }
54229
54475
  if (labelW(t) <= boxW && labelH <= boxH) {
54230
- if (fitsRegions(nameRect) && fitsPois(nameRect)) {
54476
+ if (fitsRegions(nameRect) && fitsPois(nameRect) && (valStr === void 0 || extremesOnFill(a.x, a.y, t))) {
54231
54477
  chosen = { text: t, ax: a.x, ay: a.y };
54232
54478
  break;
54233
54479
  }
@@ -54235,7 +54481,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54235
54481
  }
54236
54482
  if (chosen) break;
54237
54483
  }
54238
- if (chosen === void 0 && valStr && r.label !== void 0) {
54484
+ if (chosen === void 0 && r.label !== void 0 && isSubject) {
54239
54485
  const hop = tryShortHopCallout(c[0], c[1], r.fill, r.label, valStr);
54240
54486
  if (hop) {
54241
54487
  placedRegionRects.push(hop.rect);
@@ -54247,9 +54493,9 @@ function layoutMap(resolvedIn, data, size, opts) {
54247
54493
  text: r.label,
54248
54494
  anchor: "middle",
54249
54495
  color: palette.text,
54250
- halo: true,
54496
+ halo: false,
54251
54497
  haloColor: palette.bg,
54252
- valueLine: valStr,
54498
+ ...valStr !== void 0 && { valueLine: valStr },
54253
54499
  leader: {
54254
54500
  x1: hop.leader[0],
54255
54501
  y1: hop.leader[1],
@@ -54260,20 +54506,28 @@ function layoutMap(resolvedIn, data, size, opts) {
54260
54506
  calloutDot: { x: c[0], y: c[1], color: dark },
54261
54507
  lineNumber: r.lineNumber
54262
54508
  });
54509
+ labeledRegionIds.add(r.id);
54263
54510
  }
54264
54511
  continue;
54265
54512
  }
54266
54513
  if (chosen === void 0) continue;
54267
54514
  const isOrient = r.value === void 0 && r.layer === "base";
54268
54515
  let font = FONT2;
54269
- let fade = 0;
54270
54516
  if (isOrient) {
54271
54517
  const growT = sizeT(boxW, boxH);
54272
54518
  const desiredFont = Math.round(
54273
54519
  FONT2 + growT * (REGION_FONT_MAX_ORIENT - FONT2)
54274
54520
  );
54275
54521
  const hasVal = chosen.valueLine !== void 0;
54276
- for (let f = desiredFont; f > FONT2; f--) {
54522
+ const extremesClean = (f) => {
54523
+ const halfW = measureLegendText(chosen.text, f) / 2;
54524
+ return [chosen.ax - halfW, chosen.ax + halfW].every((sx) => {
54525
+ const fAt = fillAt(sx, chosen.ay);
54526
+ return fAt === r.fill || isEmptyFill(fAt);
54527
+ });
54528
+ };
54529
+ let fit = -1;
54530
+ for (let f = desiredFont; f >= FONT2; f--) {
54277
54531
  if (stackW(chosen.text, chosen.valueLine, f) > boxW || stackH(hasVal, f) > boxH)
54278
54532
  continue;
54279
54533
  const gRect = regionLabelRect(
@@ -54290,14 +54544,18 @@ function layoutMap(resolvedIn, data, size, opts) {
54290
54544
  void 0,
54291
54545
  f
54292
54546
  );
54293
- if (!fitsRegions(gRect) || !fitsPois(gName)) continue;
54294
- const halfW = measureLegendText(chosen.text, f) / 2;
54295
- if (fillAt(chosen.ax - halfW, chosen.ay) !== r.fill || fillAt(chosen.ax + halfW, chosen.ay) !== r.fill)
54547
+ if (!fitsRegions(gRect) || !fitsPois(gName) || collides(gName))
54296
54548
  continue;
54297
- font = f;
54549
+ if (!extremesClean(f)) continue;
54550
+ fit = f;
54298
54551
  break;
54299
54552
  }
54300
- fade = font > FONT2 ? Math.round(growT * REGION_FADE_ORIENT) : 0;
54553
+ if (fit < 0) {
54554
+ if (!frameContainers.has(r.id)) continue;
54555
+ font = FONT2;
54556
+ } else {
54557
+ font = fit;
54558
+ }
54301
54559
  }
54302
54560
  const rRect = regionLabelRect(
54303
54561
  chosen.ax,
@@ -54315,8 +54573,9 @@ function layoutMap(resolvedIn, data, size, opts) {
54315
54573
  r.lineNumber,
54316
54574
  chosen.valueLine,
54317
54575
  font,
54318
- fade
54576
+ isOrient ? backdropLandTone : void 0
54319
54577
  );
54578
+ labeledRegionIds.add(r.id);
54320
54579
  regionLabelGuards.push({
54321
54580
  label: labels[labels.length - 1],
54322
54581
  rect: rRect
@@ -54334,6 +54593,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54334
54593
  seed.lineNumber,
54335
54594
  valStr
54336
54595
  );
54596
+ labeledRegionIds.add(seed.iso);
54337
54597
  regionLabelGuards.push({
54338
54598
  label: labels[labels.length - 1],
54339
54599
  rect: regionLabelRect(seed.x, seed.y, text, valStr)
@@ -54765,13 +55025,30 @@ function layoutMap(resolvedIn, data, size, opts) {
54765
55025
  const nx = -dy / len;
54766
55026
  const ny = dx / len;
54767
55027
  const perp = labelH + 2;
55028
+ const pm = /^M(-?[\d.]+),(-?[\d.]+)(?:L(-?[\d.]+),(-?[\d.]+)|Q(-?[\d.]+),(-?[\d.]+) (-?[\d.]+),(-?[\d.]+))$/.exec(
55029
+ lg.d
55030
+ );
55031
+ const pointAt = (t) => {
55032
+ if (pm?.[5] !== void 0) {
55033
+ const u = 1 - t;
55034
+ return [
55035
+ u * u * +pm[1] + 2 * u * t * +pm[5] + t * t * +pm[7],
55036
+ u * u * +pm[2] + 2 * u * t * +pm[6] + t * t * +pm[8]
55037
+ ];
55038
+ }
55039
+ if (pm?.[3] !== void 0)
55040
+ return [
55041
+ +pm[1] + (+pm[3] - +pm[1]) * t,
55042
+ +pm[2] + (+pm[4] - +pm[2]) * t
55043
+ ];
55044
+ return [a.cx + dx * t, a.cy + dy * t];
55045
+ };
54768
55046
  const candidates = [];
54769
55047
  for (const off of [0, perp, -perp, 2 * perp, -2 * perp])
54770
- for (const t of T_LIST)
54771
- candidates.push([
54772
- a.cx + dx * t + nx * off,
54773
- a.cy + dy * t + ny * off - 4
54774
- ]);
55048
+ for (const t of T_LIST) {
55049
+ const [bx, by] = pointAt(t);
55050
+ candidates.push([bx + nx * off, by + ny * off - 4]);
55051
+ }
54775
55052
  const clean = ([cx2, cy2]) => {
54776
55053
  const rect2 = { x: cx2 - w / 2, y: cy2 - labelH / 2, w, h: labelH };
54777
55054
  if (rect2.x < 0 || rect2.x + w > width || rect2.y < 0 || rect2.y + rect2.h > height)
@@ -54781,10 +55058,8 @@ function layoutMap(resolvedIn, data, size, opts) {
54781
55058
  if (placedEdge.some((o) => rectsOverlap(o, rect2))) return false;
54782
55059
  return true;
54783
55060
  };
54784
- const [cx, cy] = candidates.find(clean) ?? [
54785
- a.cx + dx * 0.5,
54786
- a.cy + dy * 0.5 - 4
54787
- ];
55061
+ const mid = pointAt(0.5);
55062
+ const [cx, cy] = candidates.find(clean) ?? [mid[0], mid[1] - 4];
54788
55063
  const style = labelOnFill(fillAt(cx, cy));
54789
55064
  legs[i] = {
54790
55065
  ...lg,
@@ -54812,7 +55087,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54812
55087
  const rawCountries = [];
54813
55088
  for (const f of worldLayer.values()) {
54814
55089
  const iso = typeof f.id === "string" ? f.id : String(f.id ?? "");
54815
- if (!iso || regionById.has(iso)) continue;
55090
+ if (!iso || regionById.has(iso) || labeledRegionIds.has(iso)) continue;
54816
55091
  let hasReferencedSub = false;
54817
55092
  for (const k of regionById.keys())
54818
55093
  if (k.startsWith(iso + "-")) {
@@ -54886,7 +55161,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54886
55161
  );
54887
55162
  if (usLayer && framedStateContainers) {
54888
55163
  for (const [iso, f] of usLayer) {
54889
- if (regionById.has(iso)) continue;
55164
+ if (regionById.has(iso) || labeledRegionIds.has(iso)) continue;
54890
55165
  const viewF = cullFeatureToView(f);
54891
55166
  if (!viewF) continue;
54892
55167
  const b = path.bounds(viewF);
@@ -54910,6 +55185,11 @@ function layoutMap(resolvedIn, data, size, opts) {
54910
55185
  waterBodies: data.waterBodies,
54911
55186
  countries: countryCandidates,
54912
55187
  palette,
55188
+ // One muted, contrast-floored tone shared with the orientation backdrop
55189
+ // (region pass) so a context country (Canada) and an orientation region
55190
+ // (Minnesota) read identically — never below the WCAG-AA floor, no fade.
55191
+ countryTone: backdropLandTone,
55192
+ waterTone: backdropWaterTone,
54913
55193
  project,
54914
55194
  collides,
54915
55195
  contentPoints,
@@ -55464,19 +55744,60 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
55464
55744
  sel.style("cursor", "pointer").on("click", () => onClickItem(lineNumber));
55465
55745
  };
55466
55746
  const gLegs = svg.append("g").attr("class", "dgmo-map-legs").attr("fill", "none");
55467
- layout.legs.forEach((leg, i) => {
55468
- 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);
55747
+ const legEnd = (d) => {
55748
+ const m = /^M(-?[\d.]+),(-?[\d.]+)(?:L(-?[\d.]+),(-?[\d.]+)|Q(-?[\d.]+),(-?[\d.]+) (-?[\d.]+),(-?[\d.]+))$/.exec(
55749
+ d
55750
+ );
55751
+ if (!m) return null;
55752
+ const x0 = +m[1];
55753
+ const y0 = +m[2];
55754
+ let ex;
55755
+ let ey;
55756
+ let fromX;
55757
+ let fromY;
55758
+ if (m[3] !== void 0) {
55759
+ ex = +m[3];
55760
+ ey = +m[4];
55761
+ fromX = x0;
55762
+ fromY = y0;
55763
+ } else {
55764
+ ex = +m[7];
55765
+ ey = +m[8];
55766
+ fromX = +m[5];
55767
+ fromY = +m[6];
55768
+ }
55769
+ const dx = ex - fromX;
55770
+ const dy = ey - fromY;
55771
+ const len = Math.hypot(dx, dy);
55772
+ if (!(len > 0)) return null;
55773
+ return { ex, ey, tx: dx / len, ty: dy / len };
55774
+ };
55775
+ layout.legs.forEach((leg) => {
55776
+ const end = leg.arrow ? legEnd(leg.d) : null;
55777
+ let drawD = leg.d;
55778
+ let arrowTri = null;
55779
+ if (end) {
55780
+ const aLen = arrowSize(leg.width);
55781
+ const aHalf = Math.max(aLen * 0.5, leg.width * 0.6 + 1);
55782
+ const bx = end.ex - end.tx * aLen;
55783
+ const by = end.ey - end.ty * aLen;
55784
+ const px = -end.ty;
55785
+ const py = end.tx;
55786
+ arrowTri = `M${bx + px * aHalf},${by + py * aHalf}L${end.ex},${end.ey}L${bx - px * aHalf},${by - py * aHalf}z`;
55787
+ drawD = leg.d.replace(
55788
+ /(-?[\d.]+),(-?[\d.]+)$/,
55789
+ `${bx.toFixed(2)},${by.toFixed(2)}`
55790
+ );
55791
+ }
55792
+ 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);
55469
55793
  if (leg.tags) {
55470
55794
  for (const [group, value] of Object.entries(leg.tags)) {
55471
55795
  p.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
55472
55796
  }
55473
55797
  }
55474
55798
  wireSync(p, leg.lineNumber);
55475
- if (leg.arrow) {
55476
- const id = nid(`dgmo-map-arrow-${i}`);
55477
- const s = arrowSize(leg.width);
55478
- 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);
55479
- p.attr("marker-end", `url(#${id})`);
55799
+ if (arrowTri) {
55800
+ 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);
55480
55801
  }
55481
55802
  if (leg.label !== void 0 && leg.labelX !== void 0) {
55482
55803
  const lt = emitText(
@@ -59632,12 +59953,10 @@ __export(advanced_exports, {
59632
59953
  extractFlowchartSymbols: () => extractSymbols,
59633
59954
  extractInfraSymbols: () => extractSymbols4,
59634
59955
  extractPertSymbols: () => extractPertSymbols,
59635
- findUnsafePipePositions: () => findUnsafePipePositions,
59636
59956
  focusBoxesAndLines: () => focusBoxesAndLines,
59637
59957
  focusOrgTree: () => focusOrgTree,
59638
59958
  formatDateLabel: () => formatDateLabel,
59639
59959
  formatDgmoError: () => formatDgmoError,
59640
- formatLineDiff: () => formatLineDiff,
59641
59960
  getAllChartTypes: () => getAllChartTypes,
59642
59961
  getAvailablePalettes: () => getAvailablePalettes,
59643
59962
  getExtendedChartLegendGroups: () => getExtendedChartLegendGroups,
@@ -59658,7 +59977,6 @@ __export(advanced_exports, {
59658
59977
  isArchiveColumn: () => isArchiveColumn,
59659
59978
  isExtendedChartType: () => isExtendedChartType,
59660
59979
  isInvalidColorToken: () => isInvalidColorToken,
59661
- isLegacyMetadataLine: () => isLegacyMetadataLine,
59662
59980
  isRecognizedColorName: () => isRecognizedColorName,
59663
59981
  isSequenceBlock: () => isSequenceBlock,
59664
59982
  isSequenceNote: () => isSequenceNote,
@@ -59695,7 +60013,6 @@ __export(advanced_exports, {
59695
60013
  mapExportDimensions: () => mapExportDimensions,
59696
60014
  mapNeutralLandColor: () => mapNeutralLandColor,
59697
60015
  measurePertAnalysisBlock: () => measurePertAnalysisBlock,
59698
- migrateContent: () => migrateContent,
59699
60016
  mix: () => mix,
59700
60017
  mulberry32: () => mulberry32,
59701
60018
  nearestNamedColor: () => nearestNamedColor,
@@ -59823,7 +60140,6 @@ __export(advanced_exports, {
59823
60140
  tidewaterPalette: () => tidewaterPalette,
59824
60141
  tint: () => tint,
59825
60142
  tokyoNightPalette: () => tokyoNightPalette,
59826
- transformLine: () => transformLine,
59827
60143
  truncateBareUrl: () => truncateBareUrl,
59828
60144
  validate: () => parseDgmo,
59829
60145
  validateComputed: () => validateComputed,
@@ -65661,320 +65977,6 @@ var themes = {
65661
65977
 
65662
65978
  // src/advanced.ts
65663
65979
  init_dgmo_router();
65664
-
65665
- // src/migrate/index.ts
65666
- var import_node_fs = require("fs");
65667
- var import_node_path = require("path");
65668
- init_dgmo_router();
65669
-
65670
- // src/migrate/line-classifier.ts
65671
- function findUnsafePipePositions(line11) {
65672
- const positions = [];
65673
- let i = 0;
65674
- let inDouble = false;
65675
- let inSingle = false;
65676
- let braceDepth = 0;
65677
- let inArrow = false;
65678
- let arrowOpenChar = null;
65679
- while (i < line11.length) {
65680
- const ch = line11[i];
65681
- const ch2 = i + 1 < line11.length ? line11[i + 1] : "";
65682
- if (inDouble) {
65683
- if (ch === '"') inDouble = false;
65684
- i++;
65685
- continue;
65686
- }
65687
- if (inSingle) {
65688
- if (ch === "'") inSingle = false;
65689
- i++;
65690
- continue;
65691
- }
65692
- if (inArrow) {
65693
- if (arrowOpenChar === "-" && ch === "-" && ch2 === ">" || arrowOpenChar === "~" && ch === "~" && ch2 === ">") {
65694
- inArrow = false;
65695
- arrowOpenChar = null;
65696
- i += 2;
65697
- continue;
65698
- }
65699
- i++;
65700
- continue;
65701
- }
65702
- if (ch === '"') {
65703
- inDouble = true;
65704
- i++;
65705
- continue;
65706
- }
65707
- if (ch === "'") {
65708
- inSingle = true;
65709
- i++;
65710
- continue;
65711
- }
65712
- if (ch === "{") {
65713
- braceDepth++;
65714
- i++;
65715
- continue;
65716
- }
65717
- if (ch === "}") {
65718
- if (braceDepth > 0) braceDepth--;
65719
- i++;
65720
- continue;
65721
- }
65722
- if ((ch === "-" || ch === "~") && i > 0) {
65723
- const prev = line11[i - 1];
65724
- const onBoundary = prev === " " || prev === " ";
65725
- const nextIsLabelChar = ch2 !== "" && ch2 !== ch && ch2 !== ">" && ch2 !== " " && ch2 !== " ";
65726
- if (onBoundary && nextIsLabelChar) {
65727
- const closer = ch + ">";
65728
- const closeIdx = line11.indexOf(closer, i + 1);
65729
- if (closeIdx > i) {
65730
- const labelRegion = line11.substring(i + 1, closeIdx);
65731
- if (!labelRegion.includes("->") && !labelRegion.includes("~>")) {
65732
- inArrow = true;
65733
- arrowOpenChar = ch;
65734
- i++;
65735
- continue;
65736
- }
65737
- }
65738
- }
65739
- }
65740
- if (ch === "|" && braceDepth === 0) {
65741
- positions.push(i);
65742
- }
65743
- i++;
65744
- }
65745
- return positions;
65746
- }
65747
- function isLegacyMetadataLine(line11) {
65748
- return findUnsafePipePositions(line11).length > 0;
65749
- }
65750
-
65751
- // src/migrate/line-transformer.ts
65752
- function replaceUnsafePipesWithCommas(s) {
65753
- const positions = findUnsafePipePositions(s);
65754
- if (positions.length === 0) return s;
65755
- const chars = s.split("");
65756
- for (const pos of positions) {
65757
- chars[pos] = ",";
65758
- }
65759
- return chars.join("");
65760
- }
65761
- var GANTT_PERCENT = {
65762
- match: (t) => /^\d+(?:\.\d+)?\s*%$/.test(t.trim()),
65763
- promote: (t) => `progress: ${t.trim().replace(/\s*%$/, "")}`
65764
- };
65765
- var JOURNEY_SCORE = {
65766
- match: (t) => /^\d+(?:\s+[A-Za-z][A-Za-z0-9_-]*)?$/.test(t.trim()),
65767
- promote: (t) => {
65768
- const m = t.trim().match(/^(\d+)(?:\s+([A-Za-z][A-Za-z0-9_-]*))?$/);
65769
- if (!m) return t;
65770
- return m[2] ? `score: ${m[1]}, emotion: ${m[2]}` : `score: ${m[1]}`;
65771
- }
65772
- };
65773
- var PROMOTIONS_BY_CHART = {
65774
- gantt: [GANTT_PERCENT],
65775
- "journey-map": [JOURNEY_SCORE]
65776
- };
65777
- var BARE_DESCRIPTION_CHARTS = /* @__PURE__ */ new Set(["pyramid", "ring"]);
65778
- function tokenizeMetaRegion(region) {
65779
- const tokens = [];
65780
- let i = 0;
65781
- let buf = "";
65782
- let inQuote = null;
65783
- while (i < region.length) {
65784
- const ch = region[i];
65785
- if (inQuote) {
65786
- buf += ch;
65787
- if (ch === inQuote) inQuote = null;
65788
- i++;
65789
- continue;
65790
- }
65791
- if (ch === '"' || ch === "'") {
65792
- inQuote = ch;
65793
- buf += ch;
65794
- i++;
65795
- continue;
65796
- }
65797
- if (ch === ",") {
65798
- const t = buf.trim();
65799
- if (t) tokens.push(t);
65800
- buf = "";
65801
- i++;
65802
- continue;
65803
- }
65804
- buf += ch;
65805
- i++;
65806
- }
65807
- const tail = buf.trim();
65808
- if (tail) tokens.push(tail);
65809
- return tokens;
65810
- }
65811
- function transformLine(line11, chartType) {
65812
- const indentMatch = line11.match(/^[ \t]*/);
65813
- const indent = indentMatch ? indentMatch[0] : "";
65814
- const body = line11.substring(indent.length);
65815
- const pipePositions = findUnsafePipePositions(body);
65816
- if (pipePositions.length === 0) {
65817
- return { line: line11, changed: false };
65818
- }
65819
- const cut = pipePositions[0];
65820
- const nameRegion = body.substring(0, cut).trimEnd();
65821
- let metaRegion = body.substring(cut + 1).trimStart();
65822
- if (pipePositions.length > 1) {
65823
- metaRegion = replaceUnsafePipesWithCommas(metaRegion);
65824
- }
65825
- if (chartType !== null && BARE_DESCRIPTION_CHARTS.has(chartType) && !metaRegion.includes(":")) {
65826
- const text = metaRegion.trim();
65827
- if (!text) {
65828
- return {
65829
- line: indent + nameRegion,
65830
- changed: true
65831
- };
65832
- }
65833
- const value = text.includes(",") ? `"${text}"` : text;
65834
- return {
65835
- line: `${indent}${nameRegion} description: ${value}`,
65836
- changed: true
65837
- };
65838
- }
65839
- const tokens = tokenizeMetaRegion(metaRegion);
65840
- const promotions = chartType ? PROMOTIONS_BY_CHART[chartType] ?? [] : [];
65841
- const emitted = [];
65842
- for (const raw of tokens) {
65843
- if (raw.includes(":")) {
65844
- emitted.push(raw);
65845
- continue;
65846
- }
65847
- const promo = promotions.find((p) => p.match(raw));
65848
- if (promo) {
65849
- emitted.push(promo.promote(raw));
65850
- continue;
65851
- }
65852
- emitted.push(raw);
65853
- }
65854
- if (emitted.length === 0) {
65855
- return {
65856
- line: indent + nameRegion,
65857
- changed: true
65858
- };
65859
- }
65860
- return {
65861
- line: `${indent}${nameRegion} ${emitted.join(", ")}`,
65862
- changed: true
65863
- };
65864
- }
65865
-
65866
- // src/migrate/data-comma.ts
65867
- init_colors();
65868
- var DATA_COMMA_CHART_TYPES = /* @__PURE__ */ new Set([
65869
- // Simple charts (chart.ts)
65870
- "bar",
65871
- "line",
65872
- "multi-line",
65873
- "pie",
65874
- "doughnut",
65875
- "area",
65876
- "polar-area",
65877
- "radar",
65878
- "bar-stacked",
65879
- // Extended charts (echarts.ts)
65880
- "scatter",
65881
- "heatmap",
65882
- "sankey",
65883
- "chord",
65884
- "funnel",
65885
- // Visualizations (d3 / visualizations/parse.ts)
65886
- "quadrant",
65887
- "slope",
65888
- "arc",
65889
- "wordcloud"
65890
- ]);
65891
- var COLOR_SET = new Set(RECOGNIZED_COLOR_NAMES);
65892
- var NUMERIC_VALUE_RE = /^-?\d[\d,_]*(?:\.\d+)?,?$/;
65893
- function isNumericValueToken(tok) {
65894
- if (!NUMERIC_VALUE_RE.test(tok)) return false;
65895
- return /\d/.test(tok);
65896
- }
65897
- function migrateDataRowCommaLine(line11) {
65898
- const indentMatch = line11.match(/^[ \t]*/);
65899
- const indent = indentMatch ? indentMatch[0] : "";
65900
- const body = line11.substring(indent.length);
65901
- if (!body || body.startsWith("//")) return { line: line11, changed: false };
65902
- const tokens = body.split(/\s+/);
65903
- if (tokens.length < 2) return { line: line11, changed: false };
65904
- let i = tokens.length - 1;
65905
- if (i >= 1 && COLOR_SET.has(tokens[i])) {
65906
- i--;
65907
- }
65908
- const valueStart = (() => {
65909
- let j = i;
65910
- while (j >= 1 && isNumericValueToken(tokens[j])) j--;
65911
- return j + 1;
65912
- })();
65913
- if (valueStart > i || valueStart < 1) return { line: line11, changed: false };
65914
- const valueTokens = tokens.slice(valueStart, i + 1);
65915
- const hasComma = valueTokens.some((t) => t.includes(","));
65916
- if (!hasComma) return { line: line11, changed: false };
65917
- const cleaned = valueTokens.map((t) => t.replace(/,/g, ""));
65918
- const labelPart = tokens.slice(0, valueStart);
65919
- const tail = tokens.slice(i + 1);
65920
- const rebuiltBody = [...labelPart, ...cleaned, ...tail].join(" ");
65921
- return { line: indent + rebuiltBody, changed: rebuiltBody !== body };
65922
- }
65923
-
65924
- // src/migrate/index.ts
65925
- function migrateContent(source) {
65926
- const chartType = parseDgmoChartType(source);
65927
- const lines = source.split("\n");
65928
- const out = [];
65929
- const changedLines = [];
65930
- let changed = false;
65931
- const migrateDataCommas = chartType !== null && DATA_COMMA_CHART_TYPES.has(chartType);
65932
- let firstContentSeen = false;
65933
- for (let i = 0; i < lines.length; i++) {
65934
- let line11 = lines[i];
65935
- const trimmed = line11.trim();
65936
- if (migrateDataCommas && firstContentSeen && trimmed && !trimmed.startsWith("//")) {
65937
- const dc = migrateDataRowCommaLine(line11);
65938
- if (dc.changed) {
65939
- line11 = dc.line;
65940
- changed = true;
65941
- changedLines.push(i + 1);
65942
- }
65943
- }
65944
- if (trimmed && !trimmed.startsWith("//")) firstContentSeen = true;
65945
- if (!isLegacyMetadataLine(line11)) {
65946
- out.push(line11);
65947
- continue;
65948
- }
65949
- const result = transformLine(line11, chartType);
65950
- if (result.changed) {
65951
- changed = true;
65952
- if (!changedLines.includes(i + 1)) changedLines.push(i + 1);
65953
- }
65954
- out.push(result.line);
65955
- }
65956
- return {
65957
- migrated: out.join("\n"),
65958
- changed,
65959
- chartType,
65960
- changedLines
65961
- };
65962
- }
65963
- function formatLineDiff(path, original, migrated) {
65964
- const origLines = original.split("\n");
65965
- const newLines = migrated.split("\n");
65966
- const lines = [`--- ${path}`, `+++ ${path}`];
65967
- const max = Math.max(origLines.length, newLines.length);
65968
- for (let i = 0; i < max; i++) {
65969
- const a = origLines[i];
65970
- const b = newLines[i];
65971
- if (a === b) continue;
65972
- if (a !== void 0) lines.push(`@@ line ${i + 1}
65973
- - ${a}`);
65974
- if (b !== void 0) lines.push(`+ ${b}`);
65975
- }
65976
- return lines.join("\n");
65977
- }
65978
65980
  // Annotate the CommonJS export names for ESM import in node:
65979
65981
  0 && (module.exports = {
65980
65982
  ALL_CHART_TYPES,
@@ -66044,12 +66046,10 @@ function formatLineDiff(path, original, migrated) {
66044
66046
  extractFlowchartSymbols,
66045
66047
  extractInfraSymbols,
66046
66048
  extractPertSymbols,
66047
- findUnsafePipePositions,
66048
66049
  focusBoxesAndLines,
66049
66050
  focusOrgTree,
66050
66051
  formatDateLabel,
66051
66052
  formatDgmoError,
66052
- formatLineDiff,
66053
66053
  getAllChartTypes,
66054
66054
  getAvailablePalettes,
66055
66055
  getExtendedChartLegendGroups,
@@ -66070,7 +66070,6 @@ function formatLineDiff(path, original, migrated) {
66070
66070
  isArchiveColumn,
66071
66071
  isExtendedChartType,
66072
66072
  isInvalidColorToken,
66073
- isLegacyMetadataLine,
66074
66073
  isRecognizedColorName,
66075
66074
  isSequenceBlock,
66076
66075
  isSequenceNote,
@@ -66107,7 +66106,6 @@ function formatLineDiff(path, original, migrated) {
66107
66106
  mapExportDimensions,
66108
66107
  mapNeutralLandColor,
66109
66108
  measurePertAnalysisBlock,
66110
- migrateContent,
66111
66109
  mix,
66112
66110
  mulberry32,
66113
66111
  nearestNamedColor,
@@ -66235,7 +66233,6 @@ function formatLineDiff(path, original, migrated) {
66235
66233
  tidewaterPalette,
66236
66234
  tint,
66237
66235
  tokyoNightPalette,
66238
- transformLine,
66239
66236
  truncateBareUrl,
66240
66237
  validate,
66241
66238
  validateComputed,