@diagrammo/dgmo 0.21.1 → 0.22.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 (73) hide show
  1. package/README.md +16 -6
  2. package/dist/advanced.cjs +2003 -466
  3. package/dist/advanced.d.cts +5714 -0
  4. package/dist/advanced.d.ts +5714 -0
  5. package/dist/advanced.js +1999 -466
  6. package/dist/auto.cjs +2048 -449
  7. package/dist/auto.d.cts +39 -0
  8. package/dist/auto.d.ts +39 -0
  9. package/dist/auto.js +121 -121
  10. package/dist/auto.mjs +2050 -450
  11. package/dist/cli.cjs +170 -170
  12. package/dist/editor.cjs +13 -16
  13. package/dist/editor.js +13 -16
  14. package/dist/highlight.cjs +15 -13
  15. package/dist/highlight.js +15 -13
  16. package/dist/index.cjs +2032 -435
  17. package/dist/index.d.cts +339 -0
  18. package/dist/index.d.ts +339 -0
  19. package/dist/index.js +2034 -436
  20. package/dist/internal.cjs +2003 -466
  21. package/dist/internal.d.cts +5714 -0
  22. package/dist/internal.d.ts +5714 -0
  23. package/dist/internal.js +1999 -466
  24. package/dist/map-data/water-bodies.json +1 -0
  25. package/docs/language-reference.md +20 -9
  26. package/gallery/fixtures/map-categorical-world.dgmo +16 -0
  27. package/gallery/fixtures/map-categorical.dgmo +0 -1
  28. package/gallery/fixtures/map-choropleth.dgmo +0 -1
  29. package/gallery/fixtures/map-coastline.dgmo +7 -0
  30. package/gallery/fixtures/map-colorize.dgmo +11 -0
  31. package/gallery/fixtures/map-direct-color.dgmo +0 -1
  32. package/gallery/fixtures/map-reference-world.dgmo +11 -0
  33. package/gallery/fixtures/map-region-scope.dgmo +0 -3
  34. package/gallery/fixtures/map-route.dgmo +0 -1
  35. package/package.json +1 -1
  36. package/src/advanced.ts +12 -1
  37. package/src/boxes-and-lines/renderer.ts +39 -12
  38. package/src/cli.ts +1 -1
  39. package/src/completion.ts +32 -25
  40. package/src/cycle/renderer.ts +14 -1
  41. package/src/d3.ts +8 -2
  42. package/src/editor/highlight-api.ts +4 -0
  43. package/src/editor/keywords.ts +13 -16
  44. package/src/infra/renderer.ts +35 -7
  45. package/src/map/colorize.ts +54 -0
  46. package/src/map/context-labels.ts +429 -0
  47. package/src/map/data/types.ts +34 -0
  48. package/src/map/data/water-bodies.json +1 -0
  49. package/src/map/dimensions.ts +117 -0
  50. package/src/map/geo-query.ts +21 -3
  51. package/src/map/geo.ts +47 -1
  52. package/src/map/layout.ts +1300 -251
  53. package/src/map/load-data.ts +10 -2
  54. package/src/map/parser.ts +42 -116
  55. package/src/map/renderer.ts +512 -13
  56. package/src/map/resolved-types.ts +16 -2
  57. package/src/map/resolver.ts +208 -59
  58. package/src/map/types.ts +30 -32
  59. package/src/mindmap/renderer.ts +10 -1
  60. package/src/palettes/atlas.ts +77 -0
  61. package/src/palettes/blueprint.ts +73 -0
  62. package/src/palettes/color-utils.ts +58 -1
  63. package/src/palettes/index.ts +12 -3
  64. package/src/palettes/slate.ts +73 -0
  65. package/src/palettes/tidewater.ts +73 -0
  66. package/src/render.ts +8 -1
  67. package/src/tech-radar/renderer.ts +3 -0
  68. package/src/tech-radar/types.ts +3 -0
  69. package/src/utils/d3-types.ts +5 -0
  70. package/src/utils/legend-layout.ts +21 -4
  71. package/src/utils/legend-types.ts +7 -0
  72. package/src/utils/reserved-key-registry.ts +3 -0
  73. package/src/palettes/bold.ts +0 -67
@@ -18,7 +18,7 @@
18
18
  // `await import('jsdom')` seam in render.ts. The web build injects `MapData` via
19
19
  // DI and never calls `loadMapData`, so the dynamic import only runs in Node.
20
20
  import type { MapData } from './resolved-types';
21
- import type { BoundaryTopology, Gazetteer } from './data/types';
21
+ import type { BoundaryTopology, Gazetteer, WaterBodies } from './data/types';
22
22
 
23
23
  type NodeBuiltins = {
24
24
  readFile: typeof import('node:fs/promises').readFile;
@@ -46,6 +46,7 @@ const FILES = {
46
46
  mountainRanges: 'mountain-ranges.json',
47
47
  naLand: 'na-land.json',
48
48
  naLakes: 'na-lakes.json',
49
+ waterBodies: 'water-bodies.json',
49
50
  gazetteer: 'gazetteer.json',
50
51
  } as const;
51
52
 
@@ -135,12 +136,17 @@ export function loadMapData(): Promise<MapData> {
135
136
  mountainRanges,
136
137
  naLand,
137
138
  naLakes,
139
+ waterBodies,
138
140
  gazetteer,
139
141
  ] = await Promise.all([
142
+ // worldCoarse (110m) is LOAD-BEARING but NOT a render source: the world
143
+ // basemap renders from worldDetail (50m) at all scales (resolver pins
144
+ // basemaps.world = 'detail'). Coarse stays as the authoritative region
145
+ // name index + dominant-landmass bbox source in resolver.ts. Do not drop it.
140
146
  readJson<BoundaryTopology>(nb, dir, FILES.worldCoarse),
141
147
  readJson<BoundaryTopology>(nb, dir, FILES.worldDetail),
142
148
  readJson<BoundaryTopology>(nb, dir, FILES.usStates),
143
- // Lakes/rivers/mountain/NA assets are optional — older bundles may predate them.
149
+ // Lakes/rivers/mountain/NA/water assets are optional — older bundles may predate them.
144
150
  readJson<BoundaryTopology>(nb, dir, FILES.lakes).catch(() => undefined),
145
151
  readJson<BoundaryTopology>(nb, dir, FILES.rivers).catch(() => undefined),
146
152
  readJson<BoundaryTopology>(nb, dir, FILES.mountainRanges).catch(
@@ -148,6 +154,7 @@ export function loadMapData(): Promise<MapData> {
148
154
  ),
149
155
  readJson<BoundaryTopology>(nb, dir, FILES.naLand).catch(() => undefined),
150
156
  readJson<BoundaryTopology>(nb, dir, FILES.naLakes).catch(() => undefined),
157
+ readJson<WaterBodies>(nb, dir, FILES.waterBodies).catch(() => undefined),
151
158
  readJson<Gazetteer>(nb, dir, FILES.gazetteer),
152
159
  ]);
153
160
  return validate({
@@ -160,6 +167,7 @@ export function loadMapData(): Promise<MapData> {
160
167
  ...(mountainRanges && { mountainRanges }),
161
168
  ...(naLand && { naLand }),
162
169
  ...(naLakes && { naLakes }),
170
+ ...(waterBodies && { waterBodies }),
163
171
  });
164
172
  })().catch((e: unknown) => {
165
173
  cache = undefined; // don't poison future calls with a rejected promise
package/src/map/parser.ts CHANGED
@@ -32,7 +32,6 @@ import type {
32
32
  MapRouteLeg,
33
33
  MapEdge,
34
34
  PoiPos,
35
- MapScale,
36
35
  } from './types';
37
36
  import type { TagGroup, TagEntry } from '../utils/tag-groups';
38
37
 
@@ -47,23 +46,22 @@ const HUB_RE = /^(->|~>)\s+(.+)$/;
47
46
  const LEG_ARROW_RE = /^(-[^>]*?->|->|~[^>]*?~>|~>|--)\s+(.+)$/;
48
47
  const AT_RE = /(^|[\s,])at\s*:/i; // the removed `at:` coord form (§24B.9)
49
48
 
49
+ // Final 13 (§24B.2): 6 irreducible-intent directives + 7 `no-*` cosmetic
50
+ // opt-outs. Every cosmetic is on by default; its `no-*` flag is the only switch.
50
51
  const DIRECTIVE_SET: ReadonlySet<string> = new Set([
51
- 'region',
52
- 'projection',
53
52
  'region-metric',
54
53
  'poi-metric',
55
54
  'flow-metric',
56
- 'scale',
57
- 'region-labels',
58
- 'poi-labels',
59
- 'default-country',
60
- 'default-state',
55
+ 'locale',
61
56
  'active-tag',
62
- 'no-legend',
63
- 'no-insets',
64
- 'relief',
65
- 'subtitle',
66
57
  'caption',
58
+ 'no-legend',
59
+ 'no-coastline',
60
+ 'no-relief',
61
+ 'no-context-labels',
62
+ 'no-region-labels',
63
+ 'no-poi-labels',
64
+ 'no-colorize',
67
65
  ]);
68
66
 
69
67
  /** True when the first non-blank/non-comment line declares `map`. */
@@ -224,15 +222,6 @@ export function parseMap(content: string): ParsedMap {
224
222
  handleTag(trimmed, lineNumber);
225
223
  continue;
226
224
  }
227
- // Bare-flag directives (no value) — only when the line is exactly the flag,
228
- // so a region named e.g. "Natural Bridge" still parses as a region.
229
- if (
230
- (firstWord === 'muted' || firstWord === 'natural') &&
231
- trimmed === firstWord
232
- ) {
233
- handleDirective(firstWord, '', lineNumber);
234
- continue;
235
- }
236
225
  if (
237
226
  DIRECTIVE_SET.has(firstWord) &&
238
227
  !trimmed.slice(firstWord.length).trimStart().startsWith(':')
@@ -292,27 +281,6 @@ export function parseMap(content: string): ParsedMap {
292
281
  pushWarning(line, `Duplicate directive "${key}" — last value wins.`);
293
282
  };
294
283
  switch (key) {
295
- case 'region':
296
- dup(d.region);
297
- d.region = value;
298
- break;
299
- case 'projection':
300
- dup(d.projection);
301
- if (
302
- value &&
303
- ![
304
- 'equirectangular',
305
- 'natural-earth',
306
- 'albers-usa',
307
- 'mercator',
308
- ].includes(value)
309
- )
310
- pushWarning(
311
- line,
312
- `Unknown projection "${value}" (expected equirectangular | natural-earth | albers-usa | mercator).`
313
- );
314
- d.projection = value;
315
- break;
316
284
  case 'region-metric': {
317
285
  dup(d.regionMetric);
318
286
  // A trailing color names the choropleth ramp hue (§24B.3): the
@@ -331,94 +299,44 @@ export function parseMap(content: string): ParsedMap {
331
299
  dup(d.flowMetric);
332
300
  d.flowMetric = value;
333
301
  break;
334
- case 'scale':
335
- dup(d.scale);
336
- {
337
- const s = parseScale(value, line);
338
- if (s) d.scale = s;
339
- }
340
- break;
341
- case 'region-labels':
342
- dup(d.regionLabels);
343
- if (value && !['full', 'abbrev', 'off'].includes(value))
344
- pushWarning(
345
- line,
346
- `Unknown region-labels "${value}" (expected full | abbrev | off).`
347
- );
348
- d.regionLabels = value;
349
- break;
350
- case 'poi-labels':
351
- dup(d.poiLabels);
352
- if (value && !['off', 'auto', 'all'].includes(value))
353
- pushWarning(
354
- line,
355
- `Unknown poi-labels "${value}" (expected off | auto | all).`
356
- );
357
- d.poiLabels = value;
358
- break;
359
- case 'default-country':
360
- dup(d.defaultCountry);
361
- d.defaultCountry = value;
362
- break;
363
- case 'default-state':
364
- dup(d.defaultState);
365
- d.defaultState = value;
302
+ case 'locale':
303
+ dup(d.locale);
304
+ d.locale = value;
366
305
  break;
367
306
  case 'active-tag':
368
307
  dup(d.activeTag);
369
308
  d.activeTag = value;
370
309
  break;
310
+ case 'caption':
311
+ dup(d.caption);
312
+ d.caption = value;
313
+ break;
314
+ // ── Cosmetic `no-*` opt-outs: bare flags, idempotent (mirror `no-legend`,
315
+ // no dup warning); each defaults the feature ON when absent. ──
371
316
  case 'no-legend':
372
317
  d.noLegend = true;
373
318
  break;
374
- case 'no-insets':
375
- d.noInsets = true;
319
+ case 'no-coastline':
320
+ d.noCoastline = true;
376
321
  break;
377
- case 'relief':
378
- // Bare flag (idempotent like no-insets — `relief\nrelief` is no warning).
379
- d.relief = true;
322
+ case 'no-relief':
323
+ d.noRelief = true;
380
324
  break;
381
- case 'muted':
382
- case 'natural':
383
- if (d.basemapStyle !== undefined && d.basemapStyle !== key)
384
- pushWarning(
385
- line,
386
- `Conflicting basemap dress — "${d.basemapStyle}" then "${key}"; last wins.`
387
- );
388
- d.basemapStyle = key;
325
+ case 'no-context-labels':
326
+ d.noContextLabels = true;
389
327
  break;
390
- case 'subtitle':
391
- dup(d.subtitle);
392
- d.subtitle = value;
328
+ case 'no-region-labels':
329
+ d.noRegionLabels = true;
393
330
  break;
394
- case 'caption':
395
- dup(d.caption);
396
- d.caption = value;
331
+ case 'no-poi-labels':
332
+ d.noPoiLabels = true;
333
+ break;
334
+ case 'no-colorize':
335
+ d.noColorize = true;
397
336
  break;
398
337
  }
399
338
  }
400
339
 
401
- function parseScale(value: string, line: number): MapScale | null {
402
- const toks = value.split(/\s+/).filter(Boolean);
403
- const min = Number(toks[0]);
404
- const max = Number(toks[1]);
405
- if (!Number.isFinite(min) || !Number.isFinite(max)) {
406
- pushError(line, `scale requires numeric <min> <max> (got "${value}").`);
407
- return null;
408
- }
409
- const scale: Writable<MapScale> = { min, max };
410
- if (toks[2] === 'center') {
411
- const c = Number(toks[3]);
412
- if (Number.isFinite(c)) scale.center = c;
413
- else
414
- pushError(
415
- line,
416
- `scale center requires a number (got "${toks[3] ?? ''}").`
417
- );
418
- }
419
- return scale;
420
- }
421
-
422
340
  function handleTag(trimmed: string, line: number): void {
423
341
  const m = matchTagBlockHeading(trimmed);
424
342
  if (!m) {
@@ -559,6 +477,8 @@ export function parseMap(content: string): ParsedMap {
559
477
  const { tags, meta } = partitionMeta(split.meta, tagGroupNames());
560
478
  const originLabel = meta['label'];
561
479
  const originValue = meta['value'];
480
+ // Leg shape comes only from `style: arc` / arrow style (surface parsing
481
+ // removed — a leg no longer bows just because it crosses water).
562
482
  const style: 'straight' | 'arc' =
563
483
  meta['style'] === 'arc' ? 'arc' : 'straight';
564
484
  const route: Writable<MapRoute> = {
@@ -608,6 +528,8 @@ export function parseMap(content: string): ParsedMap {
608
528
  const { tags, meta } = partitionMeta(split.meta, tagGroupNames());
609
529
  const value = meta['value'];
610
530
  const destLabel = meta['label'];
531
+ // Leg shape comes only from the arrow style or the route header `style: arc`
532
+ // (surface parsing removed — no implied bow).
611
533
  const style: 'straight' | 'arc' =
612
534
  arrowStyle === 'arc' || headerStyle === 'arc' ? 'arc' : 'straight';
613
535
  return {
@@ -654,13 +576,17 @@ export function parseMap(content: string): ParsedMap {
654
576
  pushError(line, `Edge has an empty endpoint: "${trimmed}".`);
655
577
  continue;
656
578
  }
657
- const meta = k === links.length - 1 ? lastSplit.meta : {};
579
+ const isLast = k === links.length - 1;
580
+ const meta = isLast ? lastSplit.meta : {};
581
+ // Edge shape comes only from the arrow token (surface parsing removed).
582
+ const style: 'straight' | 'arc' =
583
+ links[k]!.style === 'arc' ? 'arc' : 'straight';
658
584
  edges.push({
659
585
  from,
660
586
  to,
661
587
  ...(links[k]!.label !== undefined && { label: links[k]!.label }),
662
588
  directed: links[k]!.directed,
663
- style: links[k]!.style,
589
+ style,
664
590
  meta,
665
591
  lineNumber: line,
666
592
  });