@diagrammo/dgmo 0.30.0 → 0.32.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 (85) hide show
  1. package/.cursorrules +4 -1
  2. package/.github/copilot-instructions.md +4 -1
  3. package/.windsurfrules +4 -1
  4. package/README.md +21 -3
  5. package/SKILL.md +4 -1
  6. package/dist/advanced.cjs +1853 -623
  7. package/dist/advanced.d.cts +143 -16
  8. package/dist/advanced.d.ts +143 -16
  9. package/dist/advanced.js +1846 -623
  10. package/dist/auto.cjs +1640 -581
  11. package/dist/auto.js +99 -99
  12. package/dist/auto.mjs +1640 -581
  13. package/dist/cli.cjs +148 -147
  14. package/dist/index.cjs +1643 -662
  15. package/dist/index.js +1643 -662
  16. package/docs/ai-integration.md +4 -1
  17. package/docs/language-reference.md +282 -27
  18. package/gallery/fixtures/boxes-and-lines.dgmo +2 -2
  19. package/gallery/fixtures/c4-full.dgmo +4 -5
  20. package/gallery/fixtures/c4.dgmo +2 -3
  21. package/package.json +7 -1
  22. package/src/advanced.ts +10 -0
  23. package/src/boxes-and-lines/focus.ts +257 -0
  24. package/src/boxes-and-lines/layout-search.ts +345 -65
  25. package/src/boxes-and-lines/layout.ts +11 -1
  26. package/src/boxes-and-lines/parser.ts +97 -4
  27. package/src/boxes-and-lines/renderer.ts +111 -8
  28. package/src/boxes-and-lines/types.ts +9 -0
  29. package/src/c4/parser.ts +8 -7
  30. package/src/c4/renderer.ts +7 -5
  31. package/src/chart-type-registry.ts +129 -4
  32. package/src/chart-types.ts +3 -3
  33. package/src/chart.ts +18 -1
  34. package/src/class/renderer.ts +4 -2
  35. package/src/cli-banner.ts +107 -0
  36. package/src/cli.ts +13 -0
  37. package/src/colors.ts +247 -2
  38. package/src/cycle/parser.ts +2 -7
  39. package/src/d3.ts +67 -54
  40. package/src/diagnostics.ts +17 -0
  41. package/src/dimensions.ts +9 -13
  42. package/src/echarts.ts +42 -14
  43. package/src/er/parser.ts +6 -1
  44. package/src/er/renderer.ts +4 -2
  45. package/src/gantt/parser.ts +44 -7
  46. package/src/graph/flowchart-parser.ts +77 -3
  47. package/src/graph/flowchart-renderer.ts +4 -2
  48. package/src/graph/state-renderer.ts +6 -4
  49. package/src/infra/parser.ts +80 -0
  50. package/src/infra/renderer.ts +8 -4
  51. package/src/journey-map/parser.ts +23 -8
  52. package/src/journey-map/renderer.ts +1 -1
  53. package/src/kanban/parser.ts +8 -7
  54. package/src/kanban/renderer.ts +1 -1
  55. package/src/map/context-labels.ts +134 -27
  56. package/src/map/geo.ts +10 -2
  57. package/src/map/layout.ts +259 -4
  58. package/src/map/parser.ts +2 -0
  59. package/src/map/renderer.ts +49 -25
  60. package/src/map/resolver.ts +68 -19
  61. package/src/mindmap/parser.ts +15 -7
  62. package/src/mindmap/renderer.ts +55 -15
  63. package/src/org/parser.ts +8 -7
  64. package/src/org/renderer.ts +89 -127
  65. package/src/palettes/color-utils.ts +19 -4
  66. package/src/palettes/index.ts +1 -0
  67. package/src/pert/renderer.ts +15 -10
  68. package/src/pyramid/parser.ts +2 -7
  69. package/src/quadrant/renderer.ts +2 -2
  70. package/src/raci/parser.ts +2 -7
  71. package/src/raci/renderer.ts +5 -5
  72. package/src/ring/parser.ts +2 -7
  73. package/src/sequence/parser.ts +18 -7
  74. package/src/sequence/renderer.ts +4 -4
  75. package/src/sitemap/parser.ts +8 -7
  76. package/src/sitemap/renderer.ts +37 -39
  77. package/src/tech-radar/parser.ts +2 -7
  78. package/src/timeline/renderer.ts +15 -5
  79. package/src/utils/card.ts +183 -0
  80. package/src/utils/parsing.ts +13 -1
  81. package/src/utils/scaling.ts +38 -81
  82. package/src/utils/tag-groups.ts +48 -10
  83. package/src/utils/visual-conventions.ts +61 -0
  84. package/src/visualizations/parse.ts +6 -1
  85. package/src/wireframe/parser.ts +6 -1
package/src/d3.ts CHANGED
@@ -83,6 +83,8 @@ interface ExportContext {
83
83
  viewState: import('./sharing').CompactViewState | undefined;
84
84
  options: RenderForExportOptions | undefined;
85
85
  exportMode: boolean;
86
+ /** Whether the theme is dark, resolved once in renderForExport (Story 111.2). */
87
+ isDark: boolean;
86
88
  }
87
89
 
88
90
  type DiagramExportHandler = (ctx: ExportContext) => Promise<string>;
@@ -145,6 +147,7 @@ export async function renderForExport(
145
147
  viewState,
146
148
  options,
147
149
  exportMode,
150
+ isDark: theme === 'dark',
148
151
  };
149
152
  // Generic dispatch: every structured diagram AND every D3 visualization now
150
153
  // resolves through the handler table. Only `sequence` — which has no chart
@@ -155,14 +158,24 @@ export async function renderForExport(
155
158
  return (handler ?? exportVisualization)(ctx);
156
159
  }
157
160
 
161
+ /**
162
+ * The tag-group override threaded into every handler: an explicit viewState tag
163
+ * (app toggle / share link) wins, else the options.tagGroup fallback. Resolved
164
+ * from ctx so handlers stop repeating the viewState/options fallback
165
+ * shape (Story 111.2).
166
+ */
167
+ function ctxTagOverride(ctx: ExportContext): string | undefined {
168
+ return ctx.viewState?.tag ?? ctx.options?.tagGroup;
169
+ }
170
+
158
171
  async function exportOrg(ctx: ExportContext): Promise<string> {
159
- const { content, theme, palette, viewState, options, exportMode } = ctx;
172
+ const { content, theme, palette, viewState, exportMode } = ctx;
160
173
  const { parseOrg } = await import('./org/parser');
161
174
  const { layoutOrg } = await import('./org/layout');
162
175
  const { collapseOrgTree } = await import('./org/collapse');
163
176
  const { renderOrg } = await import('./org/renderer');
164
177
 
165
- const isDark = theme === 'dark';
178
+ const isDark = ctx.isDark;
166
179
  const effectivePalette = await resolveExportPalette(theme, palette);
167
180
 
168
181
  const orgParsed = parseOrg(content, effectivePalette);
@@ -173,7 +186,7 @@ async function exportOrg(ctx: ExportContext): Promise<string> {
173
186
  const activeTagGroup = resolveActiveTagGroup(
174
187
  orgParsed.tagGroups,
175
188
  orgParsed.options['active-tag'],
176
- viewState?.tag ?? options?.tagGroup
189
+ ctxTagOverride(ctx)
177
190
  );
178
191
  const hiddenAttributes = viewState?.ha ? new Set(viewState.ha) : undefined;
179
192
 
@@ -213,13 +226,13 @@ async function exportOrg(ctx: ExportContext): Promise<string> {
213
226
  }
214
227
 
215
228
  async function exportSitemap(ctx: ExportContext): Promise<string> {
216
- const { content, theme, palette, viewState, options, exportMode } = ctx;
229
+ const { content, theme, palette, viewState, exportMode } = ctx;
217
230
  const { parseSitemap } = await import('./sitemap/parser');
218
231
  const { layoutSitemap } = await import('./sitemap/layout');
219
232
  const { collapseSitemapTree } = await import('./sitemap/collapse');
220
233
  const { renderSitemap } = await import('./sitemap/renderer');
221
234
 
222
- const isDark = theme === 'dark';
235
+ const isDark = ctx.isDark;
223
236
  const effectivePalette = await resolveExportPalette(theme, palette);
224
237
 
225
238
  const sitemapParsed = parseSitemap(content, effectivePalette);
@@ -230,7 +243,7 @@ async function exportSitemap(ctx: ExportContext): Promise<string> {
230
243
  const activeTagGroup = resolveActiveTagGroup(
231
244
  sitemapParsed.tagGroups,
232
245
  sitemapParsed.options['active-tag'],
233
- viewState?.tag ?? options?.tagGroup
246
+ ctxTagOverride(ctx)
234
247
  );
235
248
  const hiddenAttributes = viewState?.ha ? new Set(viewState.ha) : undefined;
236
249
 
@@ -269,7 +282,7 @@ async function exportSitemap(ctx: ExportContext): Promise<string> {
269
282
  }
270
283
 
271
284
  async function exportKanban(ctx: ExportContext): Promise<string> {
272
- const { content, theme, palette, viewState, options, exportMode } = ctx;
285
+ const { content, theme, palette, viewState, exportMode } = ctx;
273
286
  const { parseKanban } = await import('./kanban/parser');
274
287
  const { renderKanban } = await import('./kanban/renderer');
275
288
 
@@ -289,11 +302,11 @@ async function exportKanban(ctx: ExportContext): Promise<string> {
289
302
  const kanbanCollapsedColumns = viewState?.cc
290
303
  ? new Set(viewState.cc)
291
304
  : undefined;
292
- renderKanban(container, kanbanParsed, effectivePalette, theme === 'dark', {
305
+ renderKanban(container, kanbanParsed, effectivePalette, ctx.isDark, {
293
306
  activeTagGroup: resolveActiveTagGroup(
294
307
  kanbanParsed.tagGroups,
295
308
  kanbanParsed.options['active-tag'],
296
- viewState?.tag ?? options?.tagGroup
309
+ ctxTagOverride(ctx)
297
310
  ),
298
311
  currentSwimlaneGroup: viewState?.swim ?? null,
299
312
  ...(kanbanCollapsedLanes !== undefined && {
@@ -330,7 +343,7 @@ async function exportClass(ctx: ExportContext): Promise<string> {
330
343
  classParsed,
331
344
  classLayout,
332
345
  effectivePalette,
333
- theme === 'dark',
346
+ ctx.isDark,
334
347
  undefined,
335
348
  { width: exportWidth, height: exportHeight },
336
349
  undefined,
@@ -340,7 +353,7 @@ async function exportClass(ctx: ExportContext): Promise<string> {
340
353
  }
341
354
 
342
355
  async function exportEr(ctx: ExportContext): Promise<string> {
343
- const { content, theme, palette, viewState, options, exportMode } = ctx;
356
+ const { content, theme, palette, viewState, exportMode } = ctx;
344
357
  const { parseERDiagram } = await import('./er/parser');
345
358
  const { layoutERDiagram } = await import('./er/layout');
346
359
  const { renderERDiagram } = await import('./er/renderer');
@@ -361,13 +374,13 @@ async function exportEr(ctx: ExportContext): Promise<string> {
361
374
  erParsed,
362
375
  erLayout,
363
376
  effectivePalette,
364
- theme === 'dark',
377
+ ctx.isDark,
365
378
  undefined,
366
379
  { width: exportWidth, height: exportHeight },
367
380
  resolveActiveTagGroup(
368
381
  erParsed.tagGroups,
369
382
  erParsed.options['active-tag'],
370
- viewState?.tag ?? options?.tagGroup
383
+ ctxTagOverride(ctx)
371
384
  ),
372
385
  viewState?.sem,
373
386
  exportMode
@@ -376,7 +389,7 @@ async function exportEr(ctx: ExportContext): Promise<string> {
376
389
  }
377
390
 
378
391
  async function exportBoxesAndLines(ctx: ExportContext): Promise<string> {
379
- const { content, theme, palette, viewState, options, exportMode } = ctx;
392
+ const { content, theme, palette, viewState, exportMode } = ctx;
380
393
  const { parseBoxesAndLines } = await import('./boxes-and-lines/parser');
381
394
  const effectivePalette = await resolveExportPalette(theme, palette);
382
395
  const blParsed = parseBoxesAndLines(content, effectivePalette);
@@ -401,13 +414,13 @@ async function exportBoxesAndLines(ctx: ExportContext): Promise<string> {
401
414
  const exportHeight = blLayout.height + PADDING * 2 + titleOffset;
402
415
  const container = createExportContainer(exportWidth, exportHeight);
403
416
 
404
- const blActiveTagGroup = viewState?.tag ?? options?.tagGroup;
417
+ const blActiveTagGroup = ctxTagOverride(ctx);
405
418
  renderBoxesAndLinesForExport(
406
419
  container,
407
420
  blParsed,
408
421
  blLayout,
409
422
  effectivePalette,
410
- theme === 'dark',
423
+ ctx.isDark,
411
424
  {
412
425
  exportDims: { width: exportWidth, height: exportHeight },
413
426
  ...(blActiveTagGroup !== undefined && {
@@ -423,13 +436,13 @@ async function exportBoxesAndLines(ctx: ExportContext): Promise<string> {
423
436
  }
424
437
 
425
438
  async function exportMindmap(ctx: ExportContext): Promise<string> {
426
- const { content, theme, palette, viewState, options, exportMode } = ctx;
439
+ const { content, theme, palette, viewState, exportMode } = ctx;
427
440
  const { parseMindmap } = await import('./mindmap/parser');
428
441
  const { layoutMindmap } = await import('./mindmap/layout');
429
442
  const { collapseMindmapTree } = await import('./mindmap/collapse');
430
443
  const { renderMindmap } = await import('./mindmap/renderer');
431
444
 
432
- const isDark = theme === 'dark';
445
+ const isDark = ctx.isDark;
433
446
  const effectivePalette = await resolveExportPalette(theme, palette);
434
447
 
435
448
  const mmParsed = parseMindmap(content, effectivePalette);
@@ -439,7 +452,7 @@ async function exportMindmap(ctx: ExportContext): Promise<string> {
439
452
  const activeTagGroup = resolveActiveTagGroup(
440
453
  mmParsed.tagGroups,
441
454
  mmParsed.options['active-tag'],
442
- viewState?.tag ?? options?.tagGroup
455
+ ctxTagOverride(ctx)
443
456
  );
444
457
  const hideDescriptions =
445
458
  mmParsed.options['no-descriptions'] === 'true' || viewState?.hd === true;
@@ -507,7 +520,7 @@ async function exportWireframe(ctx: ExportContext): Promise<string> {
507
520
  wireframeParsed,
508
521
  wireframeLayout,
509
522
  effectivePalette,
510
- theme === 'dark',
523
+ ctx.isDark,
511
524
  undefined,
512
525
  { width: exportWidth, height: exportHeight },
513
526
  theme
@@ -516,7 +529,7 @@ async function exportWireframe(ctx: ExportContext): Promise<string> {
516
529
  }
517
530
 
518
531
  async function exportC4(ctx: ExportContext): Promise<string> {
519
- const { content, theme, palette, viewState, options, exportMode } = ctx;
532
+ const { content, theme, palette, viewState, exportMode } = ctx;
520
533
  const { parseC4 } = await import('./c4/parser');
521
534
  const {
522
535
  layoutC4Context,
@@ -532,7 +545,7 @@ async function exportC4(ctx: ExportContext): Promise<string> {
532
545
 
533
546
  // Container/component-level rendering (viewState fallback for share links)
534
547
  const c4Level =
535
- options?.c4Level ??
548
+ ctx.options?.c4Level ??
536
549
  (viewState?.c4l as
537
550
  | 'context'
538
551
  | 'containers'
@@ -540,8 +553,8 @@ async function exportC4(ctx: ExportContext): Promise<string> {
540
553
  | 'deployment'
541
554
  | undefined) ??
542
555
  'context';
543
- const c4System = options?.c4System ?? viewState?.c4s;
544
- const c4Container = options?.c4Container ?? viewState?.c4c;
556
+ const c4System = ctx.options?.c4System ?? viewState?.c4s;
557
+ const c4Container = ctx.options?.c4Container ?? viewState?.c4c;
545
558
 
546
559
  const c4Layout =
547
560
  c4Level === 'deployment'
@@ -572,13 +585,13 @@ async function exportC4(ctx: ExportContext): Promise<string> {
572
585
  c4Parsed,
573
586
  c4Layout,
574
587
  effectivePalette,
575
- theme === 'dark',
588
+ ctx.isDark,
576
589
  undefined,
577
590
  { width: exportWidth, height: exportHeight },
578
591
  resolveActiveTagGroup(
579
592
  c4Parsed.tagGroups,
580
593
  c4Parsed.options['active-tag'],
581
- viewState?.tag ?? options?.tagGroup
594
+ ctxTagOverride(ctx)
582
595
  ),
583
596
  exportMode
584
597
  );
@@ -603,7 +616,7 @@ async function exportFlowchart(ctx: ExportContext): Promise<string> {
603
616
  fcParsed,
604
617
  layout,
605
618
  effectivePalette,
606
- theme === 'dark',
619
+ ctx.isDark,
607
620
  undefined,
608
621
  { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
609
622
  );
@@ -611,7 +624,7 @@ async function exportFlowchart(ctx: ExportContext): Promise<string> {
611
624
  }
612
625
 
613
626
  async function exportInfra(ctx: ExportContext): Promise<string> {
614
- const { content, theme, palette, viewState, options } = ctx;
627
+ const { content, theme, palette, viewState } = ctx;
615
628
  const { parseInfra } = await import('./infra/parser');
616
629
  const { computeInfra } = await import('./infra/compute');
617
630
  const { layoutInfra } = await import('./infra/layout');
@@ -627,7 +640,7 @@ async function exportInfra(ctx: ExportContext): Promise<string> {
627
640
  const activeTagGroup = resolveActiveTagGroup(
628
641
  infraParsed.tagGroups,
629
642
  infraParsed.options['active-tag'],
630
- viewState?.tag ?? options?.tagGroup
643
+ ctxTagOverride(ctx)
631
644
  );
632
645
 
633
646
  const showInfraTitle =
@@ -648,7 +661,7 @@ async function exportInfra(ctx: ExportContext): Promise<string> {
648
661
  container,
649
662
  infraLayout,
650
663
  effectivePalette,
651
- theme === 'dark',
664
+ ctx.isDark,
652
665
  showInfraTitle ? infraParsed.title : null,
653
666
  showInfraTitle ? infraParsed.titleLineNumber : null,
654
667
  infraTagGroups,
@@ -713,7 +726,7 @@ async function exportPert(ctx: ExportContext): Promise<string> {
713
726
  pertResolved,
714
727
  pertLayout,
715
728
  effectivePalette,
716
- theme === 'dark',
729
+ ctx.isDark,
717
730
  {
718
731
  title: pertParsed.title,
719
732
  exportDims: { width: exportW, height: exportH },
@@ -727,7 +740,7 @@ async function exportPert(ctx: ExportContext): Promise<string> {
727
740
  }
728
741
 
729
742
  async function exportGantt(ctx: ExportContext): Promise<string> {
730
- const { content, theme, palette, viewState, options, exportMode } = ctx;
743
+ const { content, theme, palette, viewState, exportMode } = ctx;
731
744
  const { parseGantt } = await import('./gantt/parser');
732
745
  const { calculateSchedule } = await import('./gantt/calculator');
733
746
  const { renderGantt } = await import('./gantt/renderer');
@@ -750,7 +763,7 @@ async function exportGantt(ctx: ExportContext): Promise<string> {
750
763
  container,
751
764
  resolved,
752
765
  effectivePalette,
753
- theme === 'dark',
766
+ ctx.isDark,
754
767
  {
755
768
  ...(ganttCollapsedGroups !== undefined && {
756
769
  collapsedGroups: ganttCollapsedGroups,
@@ -764,7 +777,7 @@ async function exportGantt(ctx: ExportContext): Promise<string> {
764
777
  currentActiveGroup: resolveActiveTagGroup(
765
778
  resolved.tagGroups,
766
779
  resolved.options.activeTag ?? undefined,
767
- viewState?.tag ?? options?.tagGroup
780
+ ctxTagOverride(ctx)
768
781
  ),
769
782
  exportMode,
770
783
  },
@@ -791,7 +804,7 @@ async function exportState(ctx: ExportContext): Promise<string> {
791
804
  stateParsed,
792
805
  layout,
793
806
  effectivePalette,
794
- theme === 'dark',
807
+ ctx.isDark,
795
808
  undefined,
796
809
  { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
797
810
  );
@@ -814,7 +827,7 @@ async function exportTechRadar(ctx: ExportContext): Promise<string> {
814
827
  container,
815
828
  radarParsed,
816
829
  effectivePalette,
817
- theme === 'dark',
830
+ ctx.isDark,
818
831
  { width: RADAR_EXPORT_W, height: RADAR_EXPORT_H },
819
832
  viewState,
820
833
  exportMode
@@ -837,13 +850,13 @@ async function exportJourneyMap(ctx: ExportContext): Promise<string> {
837
850
  return '';
838
851
 
839
852
  const jmLayout = layoutJourneyMap(jmParsed, effectivePalette, {
840
- isDark: theme === 'dark',
853
+ isDark: ctx.isDark,
841
854
  });
842
855
  const container = createExportContainer(
843
856
  jmLayout.totalWidth,
844
857
  jmLayout.totalHeight
845
858
  );
846
- renderJourneyMap(container, jmParsed, effectivePalette, theme === 'dark', {
859
+ renderJourneyMap(container, jmParsed, effectivePalette, ctx.isDark, {
847
860
  exportDims: { width: jmLayout.totalWidth, height: jmLayout.totalHeight },
848
861
  exportMode,
849
862
  });
@@ -864,7 +877,7 @@ async function exportCycle(ctx: ExportContext): Promise<string> {
864
877
  container,
865
878
  cycleParsed,
866
879
  effectivePalette,
867
- theme === 'dark',
880
+ ctx.isDark,
868
881
  { width: EXPORT_WIDTH, height: EXPORT_HEIGHT },
869
882
  viewState,
870
883
  exportMode
@@ -913,7 +926,7 @@ async function exportMap(ctx: ExportContext): Promise<string> {
913
926
  mapResolved,
914
927
  mapData,
915
928
  effectivePalette,
916
- theme === 'dark',
929
+ ctx.isDark,
917
930
  dims
918
931
  );
919
932
  return finalizeSvgExport(container, theme, effectivePalette);
@@ -933,7 +946,7 @@ async function exportPyramid(ctx: ExportContext): Promise<string> {
933
946
  container,
934
947
  pyramidParsed,
935
948
  effectivePalette,
936
- theme === 'dark',
949
+ ctx.isDark,
937
950
  { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
938
951
  );
939
952
  return finalizeSvgExport(container, theme, effectivePalette);
@@ -953,7 +966,7 @@ async function exportRing(ctx: ExportContext): Promise<string> {
953
966
  container,
954
967
  ringParsed,
955
968
  effectivePalette,
956
- theme === 'dark',
969
+ ctx.isDark,
957
970
  { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
958
971
  );
959
972
  return finalizeSvgExport(container, theme, effectivePalette);
@@ -973,7 +986,7 @@ async function exportRaci(ctx: ExportContext): Promise<string> {
973
986
  container,
974
987
  raciParsed,
975
988
  effectivePalette,
976
- theme === 'dark',
989
+ ctx.isDark,
977
990
  { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
978
991
  );
979
992
  return finalizeSvgExport(container, theme, effectivePalette);
@@ -1006,7 +1019,7 @@ async function exportSlope(ctx: ExportContext): Promise<string> {
1006
1019
  container,
1007
1020
  parsed,
1008
1021
  effectivePalette,
1009
- theme === 'dark',
1022
+ ctx.isDark,
1010
1023
  undefined,
1011
1024
  dims
1012
1025
  );
@@ -1023,7 +1036,7 @@ async function exportArc(ctx: ExportContext): Promise<string> {
1023
1036
  container,
1024
1037
  parsed,
1025
1038
  effectivePalette,
1026
- theme === 'dark',
1039
+ ctx.isDark,
1027
1040
  undefined,
1028
1041
  dims
1029
1042
  );
@@ -1031,7 +1044,7 @@ async function exportArc(ctx: ExportContext): Promise<string> {
1031
1044
  }
1032
1045
 
1033
1046
  async function exportTimeline(ctx: ExportContext): Promise<string> {
1034
- const { content, theme, palette, viewState, options, exportMode } = ctx;
1047
+ const { content, theme, palette, viewState, exportMode } = ctx;
1035
1048
  const parsed = parseTimeline(content, palette);
1036
1049
  if (parsed.error || parsed.timelineEvents.length === 0) return '';
1037
1050
  const effectivePalette = await resolveExportPalette(theme, palette);
@@ -1040,13 +1053,13 @@ async function exportTimeline(ctx: ExportContext): Promise<string> {
1040
1053
  container,
1041
1054
  parsed,
1042
1055
  effectivePalette,
1043
- theme === 'dark',
1056
+ ctx.isDark,
1044
1057
  undefined,
1045
1058
  dims,
1046
1059
  resolveActiveTagGroup(
1047
1060
  parsed.timelineTagGroups,
1048
1061
  parsed.timelineActiveTag,
1049
- viewState?.tag ?? options?.tagGroup
1062
+ ctxTagOverride(ctx)
1050
1063
  ),
1051
1064
  viewState?.swim,
1052
1065
  undefined,
@@ -1066,7 +1079,7 @@ async function exportWordcloud(ctx: ExportContext): Promise<string> {
1066
1079
  container,
1067
1080
  parsed,
1068
1081
  effectivePalette,
1069
- theme === 'dark',
1082
+ ctx.isDark,
1070
1083
  dims
1071
1084
  );
1072
1085
  return finalizeSvgExport(container, theme, effectivePalette);
@@ -1082,7 +1095,7 @@ async function exportVenn(ctx: ExportContext): Promise<string> {
1082
1095
  container,
1083
1096
  parsed,
1084
1097
  effectivePalette,
1085
- theme === 'dark',
1098
+ ctx.isDark,
1086
1099
  undefined,
1087
1100
  dims
1088
1101
  );
@@ -1099,7 +1112,7 @@ async function exportQuadrant(ctx: ExportContext): Promise<string> {
1099
1112
  container,
1100
1113
  parsed,
1101
1114
  effectivePalette,
1102
- theme === 'dark',
1115
+ ctx.isDark,
1103
1116
  undefined,
1104
1117
  dims
1105
1118
  );
@@ -1112,7 +1125,7 @@ async function exportQuadrant(ctx: ExportContext): Promise<string> {
1112
1125
  * D3 visualizations now have their own handler in DIAGRAM_EXPORT_HANDLERS.
1113
1126
  */
1114
1127
  async function exportVisualization(ctx: ExportContext): Promise<string> {
1115
- const { content, theme, palette, viewState, options } = ctx;
1128
+ const { content, theme, palette, viewState } = ctx;
1116
1129
  const parsed = parseVisualization(content, palette);
1117
1130
  // Allow sequence diagrams through even if parseVisualization errors —
1118
1131
  // sequence is parsed by its own dedicated parser (parseSequenceDgmo)
@@ -1127,7 +1140,7 @@ async function exportVisualization(ctx: ExportContext): Promise<string> {
1127
1140
  }
1128
1141
 
1129
1142
  const effectivePalette = await resolveExportPalette(theme, palette);
1130
- const isDark = theme === 'dark';
1143
+ const isDark = ctx.isDark;
1131
1144
  const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
1132
1145
 
1133
1146
  const { parseSequenceDgmo } = await import('./sequence/parser');
@@ -1141,7 +1154,7 @@ async function exportVisualization(ctx: ExportContext): Promise<string> {
1141
1154
  const collapsedGroups = viewState?.cg
1142
1155
  ? new Set(viewState.cg.map(Number).filter((n) => Number.isFinite(n)))
1143
1156
  : undefined;
1144
- const seqActiveTagGroup = viewState?.tag ?? options?.tagGroup;
1157
+ const seqActiveTagGroup = ctxTagOverride(ctx);
1145
1158
  renderSequenceDiagram(
1146
1159
  container,
1147
1160
  seqParsed,
@@ -32,6 +32,23 @@ export function formatDgmoError(err: DgmoError): string {
32
32
  return err.line > 0 ? `Line ${err.line}: ${err.message}` : err.message;
33
33
  }
34
34
 
35
+ /**
36
+ * The fatal-error accumulator every structured parser re-declared identically
37
+ * (Story 111.4): push a fresh error diagnostic, set `result.error`, and return
38
+ * the partial result so callers can `return fail(line, msg)`. Generic over any
39
+ * result carrying `diagnostics` + `error`.
40
+ */
41
+ export function makeFail<
42
+ T extends { diagnostics: DgmoError[]; error?: string | null },
43
+ >(result: T): (line: number, message: string) => T {
44
+ return (line: number, message: string): T => {
45
+ const diag = makeDgmoError(line, message);
46
+ result.diagnostics.push(diag);
47
+ result.error = formatDgmoError(diag);
48
+ return result;
49
+ };
50
+ }
51
+
35
52
  // ============================================================
36
53
  // "Did you mean?" Suggestions
37
54
  // ============================================================
package/src/dimensions.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { parseDgmo } from './dgmo-router';
2
- import { computeMinDimensions, type ContentCounts } from './utils/scaling';
2
+ import type { ContentCounts } from './utils/scaling';
3
3
  import { REGISTRY_BY_ID } from './chart-type-registry';
4
4
 
5
5
  export function getMinDimensions(content: string): {
@@ -9,16 +9,12 @@ export function getMinDimensions(content: string): {
9
9
  const { chartType } = parseDgmo(content);
10
10
  if (!chartType) return { width: 300, height: 200 };
11
11
 
12
- const counts = extractContentCounts(content, chartType);
13
- return computeMinDimensions(chartType, counts);
14
- }
15
-
16
- // Content-count extraction is owned by each chart type's descriptor
17
- // (`measure`) in chart-type-registry.ts. Types without a meaningful count omit
18
- // `measure` and fall back to `{}` — the previous silent `default:` switch arm.
19
- function extractContentCounts(
20
- content: string,
21
- chartType: string
22
- ): ContentCounts {
23
- return REGISTRY_BY_ID.get(chartType)?.measure?.(content) ?? {};
12
+ // Both halves of sizing are owned by the chart type's descriptor in
13
+ // chart-type-registry.ts: `measure` (content counts) and `minDims`
14
+ // (counts → min size). One lookup serves both. Types without a `minDims`
15
+ // fall back to {300,200} — the previous silent `default:` arm of
16
+ // computeMinDimensions; `measure`-less types contribute `{}` counts.
17
+ const descriptor = REGISTRY_BY_ID.get(chartType);
18
+ const counts: ContentCounts = descriptor?.measure?.(content) ?? {};
19
+ return descriptor?.minDims?.(counts) ?? { width: 300, height: 200 };
24
20
  }
package/src/echarts.ts CHANGED
@@ -217,6 +217,7 @@ import {
217
217
  shapeFill,
218
218
  hexToHSL,
219
219
  hslToHex,
220
+ themeBaseBg,
220
221
  } from './palettes/color-utils';
221
222
  import { parseChart } from './chart';
222
223
  import type { ParsedChart, ChartEra } from './chart';
@@ -330,7 +331,9 @@ function parseScatterRow(
330
331
  if (!dataRow || dataRow.values.length < 2) return null;
331
332
  const { label: rawLabel, color: pointColor } = extractColor(
332
333
  dataRow.label,
333
- palette
334
+ palette,
335
+ diagnostics,
336
+ lineNumber
334
337
  );
335
338
  return {
336
339
  name: rawLabel,
@@ -577,11 +580,15 @@ function parseExtendedChartFull(
577
580
  const targetResolved = resolveSlot(rawTarget!);
578
581
  const { label: source, color: sourceColor } = extractColor(
579
582
  sourceResolved,
580
- palette
583
+ palette,
584
+ result.diagnostics,
585
+ lineNumber
581
586
  );
582
587
  const { label: target, color: targetColor } = extractColor(
583
588
  targetResolved,
584
- palette
589
+ palette,
590
+ result.diagnostics,
591
+ lineNumber
585
592
  );
586
593
  if (sourceColor || targetColor) {
587
594
  if (!result.nodeColors) result.nodeColors = {};
@@ -653,7 +660,9 @@ function parseExtendedChartFull(
653
660
  const targetResolved = resolveSlot(dataRow.label);
654
661
  const { label: target, color: targetColor } = extractColor(
655
662
  targetResolved,
656
- palette
663
+ palette,
664
+ result.diagnostics,
665
+ lineNumber
657
666
  );
658
667
  if (targetColor) {
659
668
  if (!result.nodeColors) result.nodeColors = {};
@@ -695,7 +704,9 @@ function parseExtendedChartFull(
695
704
  const trimmedResolved = resolveSlot(trimmed);
696
705
  const { label: nodeName, color: nodeColor } = extractColor(
697
706
  trimmedResolved,
698
- palette
707
+ palette,
708
+ result.diagnostics,
709
+ lineNumber
699
710
  );
700
711
  if (nodeColor) {
701
712
  if (!result.nodeColors) result.nodeColors = {};
@@ -802,8 +813,15 @@ function parseExtendedChartFull(
802
813
  min: parseFloat(rangeMatch[1]!),
803
814
  max: parseFloat(rangeMatch[2]!),
804
815
  };
816
+ continue;
817
+ }
818
+ // The `x` keyword owns ONLY the `x <min> to <max>` range form. A
819
+ // function curve can legitimately start with `x` (e.g. `x / 2: x / 2`);
820
+ // such a colon-bearing line must fall through to the function-curve
821
+ // handler below instead of being silently swallowed here.
822
+ if (!(result.type === 'function' && trimmed.includes(':'))) {
823
+ continue;
805
824
  }
806
- continue;
807
825
  }
808
826
  }
809
827
 
@@ -873,7 +891,9 @@ function parseExtendedChartFull(
873
891
  if (colonIndex >= 0) {
874
892
  const { label: fnName, color: fnColor } = extractColor(
875
893
  trimmed.substring(0, colonIndex).trim(),
876
- palette
894
+ palette,
895
+ result.diagnostics,
896
+ lineNumber
877
897
  );
878
898
  const fnValue = trimmed.substring(colonIndex + 1).trim();
879
899
  if (!result.functions) result.functions = [];
@@ -928,7 +948,9 @@ function parseExtendedChartFull(
928
948
  if (dataRow?.values.length === 1) {
929
949
  const { label: rawLabel, color: pointColor } = extractColor(
930
950
  dataRow.label,
931
- palette
951
+ palette,
952
+ result.diagnostics,
953
+ lineNumber
932
954
  );
933
955
  result.data.push({
934
956
  label: rawLabel,
@@ -1064,13 +1086,13 @@ export function buildExtendedChartOption(
1064
1086
 
1065
1087
  // Sankey chart has different structure
1066
1088
  if (parsed.type === 'sankey') {
1067
- const bg = isDark ? palette.surface : palette.bg;
1089
+ const bg = themeBaseBg(palette, isDark);
1068
1090
  return buildSankeyOption(parsed, textColor, colors, bg, titleConfig, sc);
1069
1091
  }
1070
1092
 
1071
1093
  // Chord diagram
1072
1094
  if (parsed.type === 'chord') {
1073
- const bg = isDark ? palette.surface : palette.bg;
1095
+ const bg = themeBaseBg(palette, isDark);
1074
1096
  return buildChordOption(
1075
1097
  parsed,
1076
1098
  palette,
@@ -1119,7 +1141,7 @@ export function buildExtendedChartOption(
1119
1141
 
1120
1142
  // Funnel chart
1121
1143
  if (parsed.type === 'funnel') {
1122
- const bg = isDark ? palette.surface : palette.bg;
1144
+ const bg = themeBaseBg(palette, isDark);
1123
1145
  return buildFunnelOption(
1124
1146
  parsed,
1125
1147
  palette,
@@ -1960,7 +1982,13 @@ function buildScatterOption(
1960
1982
  const gridLeft = parsed.ylabel ? 12 : 3;
1961
1983
  const gridRight = 4;
1962
1984
  const gridBottom = hasCategories ? 15 : parsed.xlabel ? 10 : 3;
1963
- const gridTop = parsed.title && !parsed.noTitle ? 15 : 5;
1985
+ // The categorized legend is hoisted to the TOP in both the app preview and
1986
+ // the static export (the `bottom` legend config below is stripped by both
1987
+ // render paths). So the top inset must reserve room for it — matching the
1988
+ // shared makeChartGrid rule — otherwise the topmost point labels collide
1989
+ // with the legend when there is no title to absorb the space.
1990
+ const hasTitle = !!(parsed.title && !parsed.noTitle);
1991
+ const gridTop = hasTitle ? (hasCategories ? 22 : 15) : hasCategories ? 12 : 5;
1964
1992
 
1965
1993
  // Compute custom label graphics for SSR when labels are enabled
1966
1994
  let graphic: Record<string, unknown>[] | undefined;
@@ -2137,7 +2165,7 @@ function buildHeatmapOption(
2137
2165
  ctx?: ScaleContext
2138
2166
  ): EChartsOption {
2139
2167
  const sc = ctx ?? ScaleContext.identity();
2140
- const bg = isDark ? palette.surface : palette.bg;
2168
+ const bg = themeBaseBg(palette, isDark);
2141
2169
  const heatmapRows = parsed.heatmapRows ?? [];
2142
2170
  const columns = parsed.columns ?? [];
2143
2171
  const rowLabels = heatmapRows.map((r) => r.label);
@@ -2566,7 +2594,7 @@ export function buildSimpleChartOption(
2566
2594
  colors,
2567
2595
  titleConfig,
2568
2596
  } = buildChartCommons(parsed, palette, isDark, sc);
2569
- const bg = isDark ? palette.surface : palette.bg;
2597
+ const bg = themeBaseBg(palette, isDark);
2570
2598
 
2571
2599
  switch (parsed.type) {
2572
2600
  case 'bar':