@diagrammo/dgmo 0.19.0 → 0.20.1

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 (48) hide show
  1. package/dist/advanced.cjs +948 -321
  2. package/dist/advanced.d.cts +148 -54
  3. package/dist/advanced.d.ts +148 -54
  4. package/dist/advanced.js +949 -321
  5. package/dist/auto.cjs +930 -317
  6. package/dist/auto.js +117 -117
  7. package/dist/auto.mjs +934 -318
  8. package/dist/cli.cjs +160 -160
  9. package/dist/index.cjs +929 -316
  10. package/dist/index.js +933 -317
  11. package/dist/internal.cjs +948 -321
  12. package/dist/internal.d.cts +148 -54
  13. package/dist/internal.d.ts +148 -54
  14. package/dist/internal.js +949 -321
  15. package/dist/map-data/PROVENANCE.json +1 -1
  16. package/dist/map-data/lakes.json +1 -0
  17. package/dist/map-data/na-lakes.json +1 -0
  18. package/dist/map-data/na-land.json +1 -0
  19. package/dist/map-data/rivers.json +1 -0
  20. package/docs/language-reference.md +12 -7
  21. package/gallery/fixtures/map-region-scope.dgmo +15 -0
  22. package/package.json +4 -4
  23. package/src/advanced.ts +7 -6
  24. package/src/c4/parser.ts +6 -6
  25. package/src/completion.ts +6 -2
  26. package/src/echarts.ts +1 -1
  27. package/src/infra/parser.ts +10 -10
  28. package/src/journey-map/parser.ts +1 -1
  29. package/src/label-layout.ts +36 -0
  30. package/src/map/data/PROVENANCE.json +1 -1
  31. package/src/map/data/README.md +2 -0
  32. package/src/map/data/lakes.json +1 -0
  33. package/src/map/data/na-lakes.json +1 -0
  34. package/src/map/data/na-land.json +1 -0
  35. package/src/map/data/rivers.json +1 -0
  36. package/src/map/layout.ts +1022 -205
  37. package/src/map/load-data.ts +73 -17
  38. package/src/map/parser.ts +22 -13
  39. package/src/map/renderer.ts +200 -219
  40. package/src/map/resolved-types.ts +18 -1
  41. package/src/map/resolver.ts +79 -7
  42. package/src/map/types.ts +4 -0
  43. package/src/mindmap/parser.ts +1 -1
  44. package/src/sitemap/parser.ts +1 -1
  45. package/src/utils/legend-d3.ts +42 -0
  46. package/src/utils/legend-layout.ts +83 -3
  47. package/src/utils/legend-svg.ts +1 -8
  48. package/src/utils/legend-types.ts +44 -1
@@ -10,16 +10,41 @@
10
10
  // build must inject `MapData` (or supply a fetch/bundle loader) — `resolveMap`
11
11
  // takes `MapData` by DI precisely so the browser path can differ. Do NOT assume
12
12
  // a green Node smoke test proves the browser load.
13
- import { readFile } from 'node:fs/promises';
14
- import { fileURLToPath } from 'node:url';
15
- import { dirname, resolve } from 'node:path';
13
+ //
14
+ // Node builtins (`fs/promises`, `url`, `path`) are imported LAZILY inside
15
+ // `loadMapData` never at module top level — so this file can be pulled into a
16
+ // browser bundle (Obsidian's esbuild `platform: browser`, the app's Vite/Rollup
17
+ // web build) without the bundler trying to resolve `node:*`. Mirrors the
18
+ // `await import('jsdom')` seam in render.ts. The web build injects `MapData` via
19
+ // DI and never calls `loadMapData`, so the dynamic import only runs in Node.
16
20
  import type { MapData } from './resolved-types';
17
21
  import type { BoundaryTopology, Gazetteer } from './data/types';
18
22
 
23
+ type NodeBuiltins = {
24
+ readFile: typeof import('node:fs/promises').readFile;
25
+ fileURLToPath: typeof import('node:url').fileURLToPath;
26
+ dirname: typeof import('node:path').dirname;
27
+ resolve: typeof import('node:path').resolve;
28
+ };
29
+
30
+ async function loadNodeBuiltins(): Promise<NodeBuiltins> {
31
+ const [{ readFile }, { fileURLToPath }, { dirname, resolve }] =
32
+ await Promise.all([
33
+ import('node:fs/promises'),
34
+ import('node:url'),
35
+ import('node:path'),
36
+ ]);
37
+ return { readFile, fileURLToPath, dirname, resolve };
38
+ }
39
+
19
40
  const FILES = {
20
41
  worldCoarse: 'world-coarse.json',
21
42
  worldDetail: 'world-detail.json',
22
43
  usStates: 'us-states.json',
44
+ lakes: 'lakes.json',
45
+ rivers: 'rivers.json',
46
+ naLand: 'na-land.json',
47
+ naLakes: 'na-lakes.json',
23
48
  gazetteer: 'gazetteer.json',
24
49
  } as const;
25
50
 
@@ -33,15 +58,22 @@ const CANDIDATE_DIRS = [
33
58
 
34
59
  let cache: Promise<MapData> | undefined;
35
60
 
36
- async function readJson<T>(dir: string, name: string): Promise<T> {
37
- return JSON.parse(await readFile(resolve(dir, name), 'utf8')) as T;
61
+ async function readJson<T>(
62
+ nb: NodeBuiltins,
63
+ dir: string,
64
+ name: string
65
+ ): Promise<T> {
66
+ return JSON.parse(await nb.readFile(nb.resolve(dir, name), 'utf8')) as T;
38
67
  }
39
68
 
40
- async function firstExistingDir(baseDir: string): Promise<string> {
69
+ async function firstExistingDir(
70
+ nb: NodeBuiltins,
71
+ baseDir: string
72
+ ): Promise<string> {
41
73
  for (const rel of CANDIDATE_DIRS) {
42
- const dir = resolve(baseDir, rel);
74
+ const dir = nb.resolve(baseDir, rel);
43
75
  try {
44
- await readFile(resolve(dir, FILES.gazetteer), 'utf8');
76
+ await nb.readFile(nb.resolve(dir, FILES.gazetteer), 'utf8');
45
77
  return dir;
46
78
  } catch {
47
79
  /* try next candidate */
@@ -74,10 +106,10 @@ function validate(data: MapData): MapData {
74
106
  * (dist/cli.cjs, where `import.meta.url` is `undefined` → `fileURLToPath`
75
107
  * throws). The `typeof __dirname` guard is safe in ESM (evaluates to
76
108
  * 'undefined' without a ReferenceError). */
77
- function moduleBaseDir(): string {
109
+ function moduleBaseDir(nb: NodeBuiltins): string {
78
110
  try {
79
111
  const url = import.meta.url;
80
- if (url) return dirname(fileURLToPath(url));
112
+ if (url) return nb.dirname(nb.fileURLToPath(url));
81
113
  } catch {
82
114
  /* CJS: import.meta unavailable — fall through */
83
115
  }
@@ -91,14 +123,38 @@ function moduleBaseDir(): string {
91
123
  * call can retry rather than inheriting a poisoned promise. */
92
124
  export function loadMapData(): Promise<MapData> {
93
125
  cache ??= (async (): Promise<MapData> => {
94
- const dir = await firstExistingDir(moduleBaseDir());
95
- const [worldCoarse, worldDetail, usStates, gazetteer] = await Promise.all([
96
- readJson<BoundaryTopology>(dir, FILES.worldCoarse),
97
- readJson<BoundaryTopology>(dir, FILES.worldDetail),
98
- readJson<BoundaryTopology>(dir, FILES.usStates),
99
- readJson<Gazetteer>(dir, FILES.gazetteer),
126
+ const nb = await loadNodeBuiltins();
127
+ const dir = await firstExistingDir(nb, moduleBaseDir(nb));
128
+ const [
129
+ worldCoarse,
130
+ worldDetail,
131
+ usStates,
132
+ lakes,
133
+ rivers,
134
+ naLand,
135
+ naLakes,
136
+ gazetteer,
137
+ ] = await Promise.all([
138
+ readJson<BoundaryTopology>(nb, dir, FILES.worldCoarse),
139
+ readJson<BoundaryTopology>(nb, dir, FILES.worldDetail),
140
+ readJson<BoundaryTopology>(nb, dir, FILES.usStates),
141
+ // Lakes/rivers/NA assets are optional — older bundles may predate them.
142
+ readJson<BoundaryTopology>(nb, dir, FILES.lakes).catch(() => undefined),
143
+ readJson<BoundaryTopology>(nb, dir, FILES.rivers).catch(() => undefined),
144
+ readJson<BoundaryTopology>(nb, dir, FILES.naLand).catch(() => undefined),
145
+ readJson<BoundaryTopology>(nb, dir, FILES.naLakes).catch(() => undefined),
146
+ readJson<Gazetteer>(nb, dir, FILES.gazetteer),
100
147
  ]);
101
- return validate({ worldCoarse, worldDetail, usStates, gazetteer });
148
+ return validate({
149
+ worldCoarse,
150
+ worldDetail,
151
+ usStates,
152
+ gazetteer,
153
+ ...(lakes && { lakes }),
154
+ ...(rivers && { rivers }),
155
+ ...(naLand && { naLand }),
156
+ ...(naLakes && { naLakes }),
157
+ });
102
158
  })().catch((e: unknown) => {
103
159
  cache = undefined; // don't poison future calls with a rejected promise
104
160
  throw e;
package/src/map/parser.ts CHANGED
@@ -105,11 +105,6 @@ export function parseMap(content: string): ParsedMap {
105
105
  const pushWarning = (line: number, message: string): void => {
106
106
  diagnostics.push(makeDgmoError(line, message, 'warning'));
107
107
  };
108
- // §24B calls the score+tag-coexistence note "info", but DgmoSeverity is only
109
- // error|warning — emit at warning (lowest available informational severity).
110
- const pushInfo = (line: number, message: string): void => {
111
- diagnostics.push(makeDgmoError(line, message, 'warning'));
112
- };
113
108
 
114
109
  const lines = content.split('\n');
115
110
 
@@ -289,11 +284,16 @@ export function parseMap(content: string): ParsedMap {
289
284
  dup(d.projection);
290
285
  if (
291
286
  value &&
292
- !['natural-earth', 'albers-usa', 'mercator'].includes(value)
287
+ ![
288
+ 'equirectangular',
289
+ 'natural-earth',
290
+ 'albers-usa',
291
+ 'mercator',
292
+ ].includes(value)
293
293
  )
294
294
  pushWarning(
295
295
  line,
296
- `Unknown projection "${value}" (expected natural-earth | albers-usa | mercator).`
296
+ `Unknown projection "${value}" (expected equirectangular | natural-earth | albers-usa | mercator).`
297
297
  );
298
298
  d.projection = value;
299
299
  break;
@@ -441,17 +441,26 @@ export function parseMap(content: string): ParsedMap {
441
441
  scoreNum = undefined;
442
442
  }
443
443
  }
444
- if (scoreNum !== undefined && Object.keys(tags).length)
445
- pushInfo(
446
- line,
447
- 'A region has both `score:` and a tag value v1 renders only the score (bivariate is a future seam).'
448
- );
444
+ // A region may carry BOTH a `score:` and a tag value — they are two
445
+ // selectable colouring dimensions (the legend flips between the score ramp
446
+ // and the tag group), so this is no longer warned (bivariate is handled).
447
+ // Peel a trailing ISO scope token (§24B.8)same qualifier POIs accept,
448
+ // so `Georgia US-GA` / `Georgia US` can force the country-vs-state pick.
449
+ let regionName = split.name;
450
+ let regionScope: string | undefined;
451
+ const rToks = regionName.split(/\s+/);
452
+ const rLast = rToks[rToks.length - 1]!;
453
+ if (rToks.length > 1 && SCOPE_RE.test(rLast)) {
454
+ regionName = rToks.slice(0, -1).join(' ');
455
+ regionScope = rLast;
456
+ }
449
457
  const region: Writable<MapRegion> = {
450
- name: split.name,
458
+ name: regionName,
451
459
  tags,
452
460
  meta,
453
461
  lineNumber: line,
454
462
  };
463
+ if (regionScope !== undefined) region.scope = regionScope;
455
464
  if (scoreNum !== undefined) region.score = scoreNum;
456
465
  regions.push(region);
457
466
  }