@diagrammo/dgmo 0.8.1 → 0.8.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/c4/parser.ts CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  measureIndent,
12
12
  extractColor,
13
13
  parsePipeMetadata,
14
- MULTIPLE_PIPE_WARNING,
14
+ MULTIPLE_PIPE_ERROR,
15
15
  parseFirstLine,
16
16
  OPTION_NOCOLON_RE,
17
17
  } from '../utils/parsing';
@@ -56,8 +56,8 @@ const SECTION_HEADER_RE = /^(containers|components|deployment)\s*$/i;
56
56
  /** Matches `container X` references inside deployment nodes */
57
57
  const CONTAINER_REF_RE = /^container\s+(.+)$/i;
58
58
 
59
- /** Matches indented metadata: `key value` (space-separated, no colon) */
60
- const METADATA_RE = /^([a-z][a-z0-9-]*)\s+(.+)$/i;
59
+ /** Matches indented metadata: `key: value` (colon-separated) */
60
+ const METADATA_RE = /^([a-z][a-z0-9-]*):\s+(.+)$/i;
61
61
 
62
62
  // ============================================================
63
63
  // Helpers
@@ -83,7 +83,11 @@ const VALID_SHAPES = new Set<string>([
83
83
  /** Known top-level option keys for C4 diagrams. */
84
84
  const KNOWN_C4_OPTIONS = new Set<string>([
85
85
  'layout',
86
- 'direction',
86
+ ]);
87
+
88
+ /** Known C4 boolean options (bare keyword = on). */
89
+ const KNOWN_C4_BOOLEANS = new Set<string>([
90
+ 'direction-tb',
87
91
  ]);
88
92
 
89
93
  const ALL_CHART_TYPES = [
@@ -286,10 +290,6 @@ export function parseC4(
286
290
  pushError(lineNumber, 'Tag groups must appear before content');
287
291
  continue;
288
292
  }
289
- if (tagBlockMatch.deprecated) {
290
- pushError(lineNumber, `'## ${tagBlockMatch.name}' is no longer supported — use 'tag: ${tagBlockMatch.name}' instead`);
291
- continue;
292
- }
293
293
  currentTagGroup = {
294
294
  name: tagBlockMatch.name,
295
295
  alias: tagBlockMatch.alias,
@@ -303,8 +303,14 @@ export function parseC4(
303
303
  continue;
304
304
  }
305
305
 
306
- // Generic header options (space-separated: `key value`)
306
+ // Generic header options (space-separated: `key value` or bare boolean)
307
307
  if (!contentStarted && !currentTagGroup && measureIndent(line) === 0) {
308
+ // Bare boolean options
309
+ if (KNOWN_C4_BOOLEANS.has(trimmed.toLowerCase())) {
310
+ result.options[trimmed.toLowerCase()] = 'on';
311
+ continue;
312
+ }
313
+
308
314
  const optMatch = trimmed.match(OPTION_NOCOLON_RE);
309
315
  if (optMatch) {
310
316
  const key = optMatch[1].trim().toLowerCase();
@@ -382,7 +388,7 @@ export function parseC4(
382
388
  // Otherwise it's a deployment node (possibly with pipe metadata)
383
389
  const segments = trimmed.split('|').map((s) => s.trim());
384
390
  const nodeName = segments[0];
385
- const metadata = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber, MULTIPLE_PIPE_WARNING, 'warning'));
391
+ const metadata = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber, MULTIPLE_PIPE_ERROR));
386
392
  const shape = inferC4Shape(nodeName, metadata.tech ?? metadata.technology);
387
393
 
388
394
  const dNode: C4DeploymentNode = {
@@ -641,7 +647,7 @@ export function parseC4(
641
647
  namePart = namePart.substring(0, nameIsAMatch.index!).trim();
642
648
  }
643
649
 
644
- const metadata = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber, MULTIPLE_PIPE_WARNING, 'warning'));
650
+ const metadata = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber, MULTIPLE_PIPE_ERROR));
645
651
 
646
652
  const shape =
647
653
  explicitShape ??
@@ -705,7 +711,7 @@ export function parseC4(
705
711
  `'${elementMatch[1]} ${namePart}' prefix syntax is no longer supported — use '${namePart} is a ${elementType}' instead`,
706
712
  );
707
713
 
708
- const metadata = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber, MULTIPLE_PIPE_WARNING, 'warning'));
714
+ const metadata = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber, MULTIPLE_PIPE_ERROR));
709
715
 
710
716
  // Determine shape: explicit > inference
711
717
  const shape =
@@ -747,7 +753,7 @@ export function parseC4(
747
753
  if (parentEntry) {
748
754
  const rawKey = metadataMatch[1].trim().toLowerCase();
749
755
 
750
- // Special case: `import file.dgmo`
756
+ // Special case: `import: file.dgmo`
751
757
  if (rawKey === 'import') {
752
758
  parentEntry.element.importPath = metadataMatch[2].trim();
753
759
  continue;
package/src/chart.ts CHANGED
@@ -46,7 +46,9 @@ export interface ParsedChart {
46
46
  orientation?: 'horizontal' | 'vertical';
47
47
  color?: string;
48
48
  label?: string;
49
- labels?: 'name' | 'value' | 'percent' | 'full';
49
+ noLabelName?: boolean;
50
+ noLabelValue?: boolean;
51
+ noLabelPercent?: boolean;
50
52
  data: ChartDataPoint[];
51
53
  eras?: ChartEra[];
52
54
  diagnostics: DgmoError[];
@@ -60,7 +62,7 @@ export interface ParsedChart {
60
62
  import { resolveColor } from './colors';
61
63
  import type { PaletteColors } from './palettes';
62
64
  import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
63
- import { extractColor, normalizeDirection, normalizeGroupedNumber, parseFirstLine, parseSeriesNames } from './utils/parsing';
65
+ import { extractColor, normalizeGroupedNumber, parseFirstLine, parseSeriesNames } from './utils/parsing';
64
66
 
65
67
  // ============================================================
66
68
  // Parser
@@ -83,8 +85,14 @@ const TYPE_ALIASES: Record<string, ChartType> = {
83
85
 
84
86
  /** Known option keywords for the simple chart parser. */
85
87
  const KNOWN_OPTIONS = new Set([
86
- 'chart', 'title', 'series', 'xlabel', 'ylabel', 'label', 'labels',
87
- 'orientation', 'direction', 'color',
88
+ 'chart', 'title', 'series', 'xlabel', 'ylabel', 'label',
89
+ 'no-label-name', 'no-label-value', 'no-label-percent',
90
+ 'color',
91
+ ]);
92
+
93
+ /** Known boolean options for the simple chart parser. */
94
+ const KNOWN_BOOLEANS = new Set([
95
+ 'orientation-horizontal',
88
96
  ]);
89
97
 
90
98
  /**
@@ -193,6 +201,14 @@ export function parseChart(
193
201
  const spaceIdx = trimmed.indexOf(' ');
194
202
  const firstToken = (spaceIdx >= 0 ? trimmed.substring(0, spaceIdx) : trimmed).toLowerCase();
195
203
 
204
+ // Bare boolean options (e.g. orientation-horizontal)
205
+ if (KNOWN_BOOLEANS.has(firstToken) && spaceIdx < 0) {
206
+ if (firstToken === 'orientation-horizontal') {
207
+ result.orientation = 'horizontal';
208
+ }
209
+ continue;
210
+ }
211
+
196
212
  // Known option with a value
197
213
  if (KNOWN_OPTIONS.has(firstToken) && spaceIdx >= 0) {
198
214
  const value = trimmed.substring(spaceIdx + 1).trim();
@@ -234,29 +250,6 @@ export function parseChart(
234
250
  continue;
235
251
  }
236
252
 
237
- if (firstToken === 'labels') {
238
- const v = value.toLowerCase();
239
- if (v === 'name' || v === 'value' || v === 'percent' || v === 'full') {
240
- result.labels = v;
241
- }
242
- continue;
243
- }
244
-
245
- if (firstToken === 'orientation' || firstToken === 'direction') {
246
- // Only bar and bar-stacked support orientation (axis swapping)
247
- if (result.type === 'bar' || result.type === 'bar-stacked') {
248
- const vLower = value.toLowerCase();
249
- if (vLower === 'horizontal' || vLower === 'vertical') {
250
- result.orientation = vLower;
251
- } else {
252
- const dir = normalizeDirection(value);
253
- if (dir === 'LR') result.orientation = 'horizontal';
254
- else if (dir === 'TB') result.orientation = 'vertical';
255
- }
256
- }
257
- continue;
258
- }
259
-
260
253
  if (firstToken === 'color') {
261
254
  result.color = resolveColor(value.trim(), palette) ?? undefined;
262
255
  continue;
@@ -276,6 +269,11 @@ export function parseChart(
276
269
  }
277
270
  }
278
271
 
272
+ // Bare boolean options: no-label-name, no-label-value, no-label-percent
273
+ if (firstToken === 'no-label-name') { result.noLabelName = true; continue; }
274
+ if (firstToken === 'no-label-value') { result.noLabelValue = true; continue; }
275
+ if (firstToken === 'no-label-percent') { result.noLabelPercent = true; continue; }
276
+
279
277
  // Bare "series" keyword with no value — collect indented names
280
278
  if (firstToken === 'series' && spaceIdx === -1) {
281
279
  const parsed = parseSeriesNames('', lines, i, palette);
@@ -292,8 +290,10 @@ export function parseChart(
292
290
 
293
291
  // Data row: parse from the right — rightmost numeric token(s) = value(s), everything left = label
294
292
  // Supports comma-separated multi-values: "Jan 100, 200, 300"
293
+ // Supports space-separated multi-values when series are defined: "Jan 100 200 300"
295
294
  // Supports comma-grouped numbers: "Revenue 1,200, 1,500" → [1200, 1500]
296
- const dataValues = parseDataRowValues(trimmed);
295
+ const multiValue = (result.seriesNames?.length ?? 0) >= 2;
296
+ const dataValues = parseDataRowValues(trimmed, { multiValue });
297
297
  if (dataValues) {
298
298
  const { label: rawLabel, color: pointColor } = extractColor(dataValues.label, palette);
299
299
  const [first, ...rest] = dataValues.values;
@@ -361,7 +361,7 @@ export function parseChart(
361
361
  for (const dp of result.data) {
362
362
  const actualCount = 1 + (dp.extraValues?.length ?? 0);
363
363
  if (actualCount !== expectedCount) {
364
- warn(dp.lineNumber, `Data point "${dp.label}" has ${actualCount} value(s), but ${expectedCount} series defined. Each row must have ${expectedCount} comma-separated values.`);
364
+ warn(dp.lineNumber, `Data point "${dp.label}" has ${actualCount} value(s), but ${expectedCount} series defined. Each row must have ${expectedCount} values.`);
365
365
  }
366
366
  }
367
367
  // Filter out mismatched data points so renderers get clean data
@@ -380,20 +380,22 @@ export function parseChart(
380
380
 
381
381
  /**
382
382
  * Parse a data row line: everything before the last numeric token(s) is the label,
383
- * numeric tokens at the end are the values. Supports comma-separated multi-values
384
- * and comma-grouped numbers (e.g., "1,087").
383
+ * numeric tokens at the end are the values. Supports comma-separated multi-values,
384
+ * space-separated multi-values, and comma-grouped numbers (e.g., "1,087").
385
385
  *
386
386
  * Examples:
387
- * "Jan 120" → { label: "Jan", values: [120] }
388
- * "North America 250" → { label: "North America", values: [250] }
389
- * "Region 5 300" → { label: "Region 5", values: [300] }
390
- * "Q1 10, 20, 30" → { label: "Q1", values: [10, 20, 30] }
391
- * "Revenue 1,200" → { label: "Revenue", values: [1200] }
387
+ * "Jan 120" → { label: "Jan", values: [120] }
388
+ * "North America 250" → { label: "North America", values: [250] }
389
+ * "Q1 10, 20, 30" → { label: "Q1", values: [10, 20, 30] }
390
+ * "Q1 10 20 30" → { label: "Q1", values: [10, 20, 30] }
391
+ * "Revenue 1,200" → { label: "Revenue", values: [1200] }
392
+ * "Revenue 3,984,078.65"→ { label: "Revenue", values: [3984078.65] }
392
393
  *
393
394
  * Returns null if the line has no numeric value at the end.
394
395
  */
395
396
  export function parseDataRowValues(
396
397
  line: string,
398
+ options?: { multiValue?: boolean },
397
399
  ): { label: string; values: number[] } | null {
398
400
  // First, normalize comma-grouped numbers: replace patterns like "1,087" with "1087"
399
401
  // We need to be careful: commas also separate multi-values.
@@ -407,11 +409,12 @@ export function parseDataRowValues(
407
409
  const normalized: string[] = [];
408
410
  for (let i = 0; i < segments.length; i++) {
409
411
  const seg = segments[i].trim();
410
- // Check if this segment is a continuation of a grouped number
411
- // A continuation looks like exactly 3 digits and follows a segment ending in digits.
412
+ // Check if this segment is a continuation of a grouped number.
413
+ // A continuation starts with exactly 3 digits (possibly followed by a decimal like ".65")
414
+ // and follows a segment ending in digits.
412
415
  // Grouped numbers have NO space around the comma (e.g., "1,087"), so skip if
413
416
  // the raw segment has leading whitespace (e.g., ", 350" is a value separator).
414
- if (i > 0 && /^\d{3}$/.test(seg) && !/^\s/.test(segments[i])) {
417
+ if (i > 0 && /^\d{3}(\.\d+)?$/.test(seg) && !/^\s/.test(segments[i])) {
415
418
  const prevSeg = normalized[normalized.length - 1].trimEnd();
416
419
  // Check if previous segment ends with a number (1-3 digits at the end of the last token)
417
420
  if (/\d{1,3}$/.test(prevSeg)) {
@@ -476,16 +479,35 @@ export function parseDataRowValues(
476
479
  }
477
480
  }
478
481
 
479
- // No commas or comma parsing didn't work — split by spaces from right
480
- // Last space-separated token that is numeric = the value
481
- const lastSpaceIdx = rebuilt.lastIndexOf(' ');
482
- if (lastSpaceIdx < 0) return null;
482
+ // No commas or comma parsing didn't work — split by spaces from right.
483
+ // When multiValue is enabled, walk backward collecting consecutive numeric tokens.
484
+ // Otherwise (default), take only the last token — preserving labels that contain
485
+ // numbers (e.g., "Region 5 300" → label "Region 5", value 300).
486
+ const tokens = rebuilt.split(/\s+/);
487
+ if (tokens.length < 2) return null;
488
+
489
+ if (options?.multiValue) {
490
+ const values: number[] = [];
491
+ let idx = tokens.length - 1;
492
+ while (idx >= 1) {
493
+ const tok = tokens[idx];
494
+ const num = parseFloat(tok);
495
+ if (isNaN(num) || !isFinite(Number(tok))) break;
496
+ values.unshift(num);
497
+ idx--;
498
+ }
499
+ if (values.length === 0) return null;
500
+ const label = tokens.slice(0, idx + 1).join(' ');
501
+ if (!label) return null;
502
+ return { label, values };
503
+ }
483
504
 
484
- const possibleValue = rebuilt.substring(lastSpaceIdx + 1).trim();
485
- const num = parseFloat(possibleValue);
486
- if (isNaN(num) || !isFinite(Number(possibleValue))) return null;
505
+ // Single-value mode: only the last space-separated token
506
+ const lastToken = tokens[tokens.length - 1];
507
+ const num = parseFloat(lastToken);
508
+ if (isNaN(num) || !isFinite(Number(lastToken))) return null;
487
509
 
488
- const label = rebuilt.substring(0, lastSpaceIdx).trim();
510
+ const label = tokens.slice(0, -1).join(' ');
489
511
  if (!label) return null;
490
512
 
491
513
  return { label, values: [num] };
@@ -30,10 +30,14 @@ function classId(name: string): string {
30
30
  const CLASS_DECL_RE =
31
31
  /^(?:(abstract|interface|enum)\s+)?([A-Z][A-Za-z0-9_]*)(?:\s+(extends|implements)\s+([A-Z][A-Za-z0-9_]*))?(?:\s+\[(abstract|interface|enum)\])?(?:\s+\(([^)]+)\))?\s*$/;
32
32
 
33
- // Relationship — arrow syntax:
34
- // ClassName --|> TargetClass label (new: space-separated)
35
- // ClassName --|> TargetClass : label (old: colon-separated, kept for transition)
33
+ // Relationship — arrow syntax (indented under source class):
34
+ // --|> TargetClass label (space-separated)
35
+ // --|> TargetClass : label (colon-separated, kept for transition)
36
36
  // Arrows: --|> ..|> *-- o-- ..> ->
37
+ const INDENT_REL_ARROW_RE =
38
+ /^(--\|>|\.\.\|>|\*--|o--|\.\.\>|->)\s*([A-Z][A-Za-z0-9_]*)(?:\s+:?\s*(.+))?$/;
39
+
40
+ // Legacy top-level relationship regex (used only for detection/rejection)
37
41
  const REL_ARROW_RE =
38
42
  /^([A-Z][A-Za-z0-9_]*)\s*(--\|>|\.\.\|>|\*--|o--|\.\.\>|->)\s*([A-Z][A-Za-z0-9_]*)(?:\s+:?\s*(.+))?$/;
39
43
 
@@ -211,9 +215,14 @@ export function parseClassDiagram(
211
215
  }
212
216
  }
213
217
 
214
- // Space-separated options before content (new syntax): `color off`
218
+ // Space-separated options before content (new syntax): `no-auto-color`
215
219
  // Only match lines starting with a lowercase token (options), not uppercase (class names)
216
220
  if (!contentStarted && indent === 0 && /^[a-z]/.test(trimmed)) {
221
+ // Bare boolean option (single keyword, no value)
222
+ if (trimmed.toLowerCase() === 'no-auto-color') {
223
+ result.options['no-auto-color'] = 'on';
224
+ continue;
225
+ }
217
226
  const optMatch = trimmed.match(OPTION_NOCOLON_RE);
218
227
  if (optMatch) {
219
228
  const key = optMatch[1].toLowerCase();
@@ -226,8 +235,27 @@ export function parseClassDiagram(
226
235
  }
227
236
  }
228
237
 
229
- // Indented lines = members of current class
238
+ // Indented lines = relationships or members of current class
230
239
  if (indent > 0 && currentClass) {
240
+ // Try indented relationship arrow: --|> TargetClass [label]
241
+ const indentRel = trimmed.match(INDENT_REL_ARROW_RE);
242
+ if (indentRel) {
243
+ const arrow = indentRel[1];
244
+ const targetName = indentRel[2];
245
+ const label = indentRel[3]?.trim();
246
+
247
+ getOrCreateClass(targetName, lineNumber);
248
+
249
+ result.relationships.push({
250
+ source: currentClass.id,
251
+ target: classId(targetName),
252
+ type: ARROW_TO_TYPE[arrow],
253
+ ...(label && { label }),
254
+ lineNumber,
255
+ });
256
+ continue;
257
+ }
258
+
231
259
  const member = parseMember(
232
260
  trimmed,
233
261
  lineNumber,
@@ -243,25 +271,19 @@ export function parseClassDiagram(
243
271
  currentClass = null;
244
272
  contentStarted = true;
245
273
 
246
- // Try relationship — arrow syntax
274
+ // Reject top-level relationship arrows must be indented under source class
247
275
  const relArrow = trimmed.match(REL_ARROW_RE);
248
276
  if (relArrow) {
249
277
  const sourceName = relArrow[1];
250
278
  const arrow = relArrow[2];
251
279
  const targetName = relArrow[3];
252
- const label = relArrow[4]?.trim();
253
-
254
- // Ensure both classes exist
255
- getOrCreateClass(sourceName, lineNumber);
256
- getOrCreateClass(targetName, lineNumber);
257
-
258
- result.relationships.push({
259
- source: classId(sourceName),
260
- target: classId(targetName),
261
- type: ARROW_TO_TYPE[arrow],
262
- ...(label && { label }),
263
- lineNumber,
264
- });
280
+ result.diagnostics.push(
281
+ makeDgmoError(
282
+ lineNumber,
283
+ `Relationship "${sourceName} ${arrow} ${targetName}" must be indented under the source class "${sourceName}"`,
284
+ 'warning',
285
+ ),
286
+ );
265
287
  continue;
266
288
  }
267
289
 
@@ -380,6 +402,10 @@ export function looksLikeClassDiagram(content: string): boolean {
380
402
  if (/^[+\-#]?\s*\w+.*[:(]/.test(trimmed)) {
381
403
  hasIndentedMember = true;
382
404
  }
405
+ // Indented relationship arrows
406
+ if (INDENT_REL_ARROW_RE.test(trimmed)) {
407
+ hasRelationship = true;
408
+ }
383
409
  }
384
410
  }
385
411
 
@@ -410,6 +436,7 @@ export function extractSymbols(docText: string): DiagramSymbols {
410
436
  const line = rawLine.trim();
411
437
  // Skip old-style colon metadata and new-style first line / space-separated options
412
438
  if (inMetadata && (/^[a-z-]+\s*:/i.test(line) || /^class(\s|$)/i.test(line))) continue;
439
+ if (inMetadata && line.toLowerCase() === 'no-auto-color') continue;
413
440
  if (inMetadata && /^[a-z]/.test(line) && OPTION_NOCOLON_RE.test(line)) {
414
441
  const key = line.match(OPTION_NOCOLON_RE)![1].toLowerCase();
415
442
  if (key !== 'abstract' && key !== 'interface' && key !== 'enum') continue;
@@ -82,7 +82,7 @@ const CLASS_TYPE_MAP: Record<string, ClassLegendEntry> = {
82
82
  const CLASS_TYPE_ORDER = ['class', 'abstract', 'interface', 'enum'];
83
83
 
84
84
  function collectClassTypes(parsed: ParsedClassDiagram): ClassLegendEntry[] {
85
- if (parsed.options?.color === 'off') return [];
85
+ if (parsed.options?.['no-auto-color']) return [];
86
86
 
87
87
  const present = new Set<string>();
88
88
  for (const c of parsed.classes) {
@@ -500,7 +500,7 @@ export function renderClassDiagram(
500
500
 
501
501
  const w = node.width;
502
502
  const h = node.height;
503
- const colorOff = parsed.options?.color === 'off';
503
+ const colorOff = !!parsed.options?.['no-auto-color'];
504
504
  // When legend is collapsed, use neutral color for nodes without explicit color
505
505
  const neutralize = hasLegend && !isLegendExpanded && !node.color;
506
506
  const effectiveColor = neutralize ? palette.primary : node.color;
package/src/cli.ts CHANGED
@@ -199,7 +199,7 @@ Key options:
199
199
  ### Common to all diagrams
200
200
 
201
201
  \`\`\`
202
- chart: sequence // explicit type (optional — auto-detected)
202
+ sequence // explicit type (optional — auto-detected)
203
203
  title: My Diagram
204
204
  palette: catppuccin // override palette
205
205
 
@@ -372,8 +372,7 @@ When the \`dgmo\` MCP server is configured, use these tools directly:
372
372
 
373
373
  ### Sequence diagram
374
374
  \`\`\`
375
- chart: sequence
376
- title: Auth Flow
375
+ sequence Auth Flow
377
376
 
378
377
  User -Login-> API
379
378
  API -Find user-> DB
@@ -386,8 +385,7 @@ DB -user-> API
386
385
 
387
386
  ### Flowchart
388
387
  \`\`\`
389
- chart: flowchart
390
- title: Process
388
+ flowchart Process
391
389
 
392
390
  (Start) -> <Valid?>
393
391
  -yes-> [Process] -> (Done)
@@ -396,8 +394,7 @@ title: Process
396
394
 
397
395
  ### Bar chart
398
396
  \`\`\`
399
- chart: bar
400
- title: Revenue
397
+ bar Revenue
401
398
  series: USD
402
399
 
403
400
  North: 850
@@ -407,8 +404,7 @@ East: 1100
407
404
 
408
405
  ### ER diagram
409
406
  \`\`\`
410
- chart: er
411
- title: Schema
407
+ er Schema
412
408
 
413
409
  users
414
410
  id: int [pk]
@@ -423,7 +419,7 @@ users 1--* posts : writes
423
419
 
424
420
  ### Org chart
425
421
  \`\`\`
426
- chart: org
422
+ org
427
423
 
428
424
  CEO
429
425
  VP Engineering
@@ -434,8 +430,7 @@ CEO
434
430
 
435
431
  ### Infra chart
436
432
  \`\`\`
437
- chart: infra
438
- direction: LR
433
+ infra
439
434
 
440
435
  edge
441
436
  rps: 10000
@@ -461,7 +456,7 @@ bar, line, multi-line, area, pie, doughnut, radar, polar-area, bar-stacked, scat
461
456
 
462
457
  ## Common patterns
463
458
 
464
- - \`chart: type\` explicit chart type (auto-detected if unambiguous)
459
+ - First line: chart type keyword (e.g. \`sequence\`, \`flowchart\`, \`bar\`) auto-detected if unambiguous
465
460
  - \`title: text\` — diagram title
466
461
  - \`// comment\` — only \`//\` comments (not \`#\`)
467
462
  - \`(colorname)\` — inline colors: \`Label(red): 100\`
@@ -480,7 +475,7 @@ dgmo file.dgmo --json # structured JSON output
480
475
  - Don't use \`#\` for comments — use \`//\`
481
476
  - Don't use \`end\` to close sequence blocks — indentation closes them
482
477
  - Don't use hex colors in section headers — use named colors
483
- - Don't forget \`chart:\` directive when content is ambiguous
478
+ - Start the file with the chart type keyword when content is ambiguous
484
479
  - Sequence arrows: \`->\` (sync), \`~>\` (async) — always left-to-right
485
480
 
486
481
  Full reference: call \`get_language_reference\` MCP tool or visit diagrammo.app/docs
@@ -686,8 +681,8 @@ function noInput(): never {
686
681
  writeFileSync(
687
682
  samplePath,
688
683
  [
689
- 'chart: sequence',
690
- 'activations: off',
684
+ 'sequence',
685
+ 'activations off',
691
686
  '',
692
687
  'Client -POST /login-> API',
693
688
  ' API -validate credentials-> Auth',