@diagrammo/dgmo 0.8.4 → 0.8.5

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.
@@ -1143,20 +1143,27 @@ Thousands commas supported.
1143
1143
 
1144
1144
  ## 16. Visualizations
1145
1145
 
1146
- ### 16.1 Slope Charts (Colon REQUIRED for data)
1146
+ ### 16.1 Slope Charts
1147
1147
 
1148
1148
  ```
1149
1149
  slope Fleet Strength
1150
1150
 
1151
- 1715 1725
1151
+ period 1715 1725
1152
1152
 
1153
- Blackbeard: 40 4
1154
- Roberts: 12 52
1153
+ Blackbeard 40 4
1154
+ Roberts 12 52
1155
1155
  ```
1156
1156
 
1157
- - Period labels on their own line, commas optional
1158
- - Data rows: `Label: value1 value2` — colon required, commas between values optional
1159
- - Thousands commas supported in values
1157
+ - Period directive required: `period Label1 Label2` (one-line) or indented block for multi-token labels:
1158
+ ```
1159
+ period
1160
+ Before COVID
1161
+ After COVID
1162
+ ```
1163
+ - Data rows: `Label value1 value2` — space-separated, no colons, no commas between values
1164
+ - Thousands commas within values supported (e.g., `1,000`)
1165
+ - Color annotations: `Label (color) value1 value2`
1166
+ - Minimum 2 periods required
1160
1167
 
1161
1168
  ### 16.2 Wordcloud
1162
1169
 
@@ -1239,7 +1246,6 @@ Navigator 0.85, 0.8
1239
1246
  | Class field types | class | `+ name: string` |
1240
1247
  | Class method returns | class | `+ sail(): void` |
1241
1248
  | Function expressions | function | `f(x): x^2 + 1` |
1242
- | Slope data rows | slope | `Blackbeard: 40 4` |
1243
1249
  | Hide tag values | initiative-status | `hide phase:Planning` |
1244
1250
 
1245
1251
  ### Colons OPTIONAL
@@ -1265,13 +1271,15 @@ Navigator 0.85, 0.8
1265
1271
  | Section dividers | sequence | `== Phase ==` |
1266
1272
  | Comments | all | `// comment` |
1267
1273
  | Wordcloud data | wordcloud | `swordsmanship 95` |
1274
+ | Slope data rows | slope | `Blackbeard 40 4` |
1275
+ | Slope period directive | slope | `period 1715 1725` |
1268
1276
  | Venn intersections | venn | `sw + nav Sea Raiders` |
1269
1277
 
1270
1278
  ### The Rule
1271
1279
 
1272
1280
  **Colons appear in two contexts:**
1273
1281
  1. **Value assignment** — `key: value` in pipe metadata, indented tag/metadata assignment (org, c4), and hide directives
1274
- 2. **Type/expression separation** — where labels can contain spaces and a delimiter is needed (function expressions, slope data, class members)
1282
+ 2. **Type/expression separation** — where labels can contain spaces and a delimiter is needed (function expressions, class members)
1275
1283
 
1276
1284
  **Exception**: Known-schema properties (infra node properties, ER columns) remain space-separated even though they are indented. The colon rule applies to open-ended metadata, not fixed property schemas.
1277
1285
 
@@ -1,8 +1,9 @@
1
1
  slope Programming Language Popularity
2
2
 
3
- 2020, 2022, 2025
4
- Python (blue) 3, 1, 1
5
- JavaScript (yellow) 1, 2, 2
6
- TypeScript (cyan) 7, 4, 3
7
- Rust (orange) 18, 12, 5
8
- Go (green) 10, 8, 7
3
+ period 2020 2022 2025
4
+
5
+ Python (blue) 3 1 1
6
+ JavaScript (yellow) 1 2 2
7
+ TypeScript (cyan) 7 4 3
8
+ Rust (orange) 18 12 5
9
+ Go (green) 10 8 7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,6 +30,16 @@
30
30
  "types": "./dist/editor.d.cts",
31
31
  "default": "./dist/editor.cjs"
32
32
  }
33
+ },
34
+ "./highlight": {
35
+ "import": {
36
+ "types": "./dist/highlight.d.ts",
37
+ "default": "./dist/highlight.js"
38
+ },
39
+ "require": {
40
+ "types": "./dist/highlight.d.cts",
41
+ "default": "./dist/highlight.cjs"
42
+ }
33
43
  }
34
44
  },
35
45
  "files": [
@@ -116,10 +126,20 @@
116
126
  "@lezer/lr": "^1.4.8"
117
127
  },
118
128
  "peerDependenciesMeta": {
119
- "@codemirror/language": { "optional": true },
120
- "@codemirror/state": { "optional": true },
121
- "@lezer/common": { "optional": true },
122
- "@lezer/highlight": { "optional": true },
123
- "@lezer/lr": { "optional": true }
129
+ "@codemirror/language": {
130
+ "optional": true
131
+ },
132
+ "@codemirror/state": {
133
+ "optional": true
134
+ },
135
+ "@lezer/common": {
136
+ "optional": true
137
+ },
138
+ "@lezer/highlight": {
139
+ "optional": true
140
+ },
141
+ "@lezer/lr": {
142
+ "optional": true
143
+ }
124
144
  }
125
145
  }
package/src/cli.ts CHANGED
@@ -486,6 +486,7 @@ Full reference: call \`get_language_reference\` MCP tool or visit diagrammo.app/
486
486
  function printHelp(): void {
487
487
  console.log(`Usage: dgmo <input> [options]
488
488
  cat input.dgmo | dgmo [options]
489
+ dgmo cat <file> Display file with syntax highlighting
489
490
 
490
491
  Render a .dgmo file to PNG (default) or SVG.
491
492
 
@@ -534,6 +535,8 @@ function parseArgs(argv: string[]): {
534
535
  copy: boolean;
535
536
  json: boolean;
536
537
  chartTypes: boolean;
538
+ cat: boolean;
539
+ noColor: boolean;
537
540
  installClaudeSkill: boolean;
538
541
  installClaudeCodeIntegration: boolean;
539
542
  installCodexIntegration: boolean;
@@ -553,6 +556,8 @@ function parseArgs(argv: string[]): {
553
556
  copy: false,
554
557
  json: false,
555
558
  chartTypes: false,
559
+ cat: false,
560
+ noColor: false,
556
561
  installClaudeSkill: false,
557
562
  installClaudeCodeIntegration: false,
558
563
  installCodexIntegration: false,
@@ -572,7 +577,13 @@ function parseArgs(argv: string[]): {
572
577
  while (i < args.length) {
573
578
  const arg = args[i];
574
579
 
575
- if (arg === '--help' || arg === '-h') {
580
+ if (arg === 'cat' && !result.cat && !result.input) {
581
+ result.cat = true;
582
+ i++;
583
+ } else if (arg === '--no-color') {
584
+ result.noColor = true;
585
+ i++;
586
+ } else if (arg === '--help' || arg === '-h') {
576
587
  result.help = true;
577
588
  i++;
578
589
  } else if (arg === '--version' || arg === '-v') {
@@ -746,6 +757,37 @@ async function main(): Promise<void> {
746
757
  return;
747
758
  }
748
759
 
760
+ if (opts.cat) {
761
+ const useColor =
762
+ !opts.noColor && !process.env.NO_COLOR && process.stdout.isTTY === true;
763
+
764
+ let catContent: string;
765
+ if (opts.input && opts.input !== '-') {
766
+ const inputPath = resolve(opts.input);
767
+ try {
768
+ catContent = readFileSync(inputPath, 'utf-8');
769
+ } catch {
770
+ console.error(`Error: Cannot read file "${inputPath}"`);
771
+ process.exit(1);
772
+ }
773
+ } else {
774
+ // Read from stdin
775
+ try {
776
+ catContent = readFileSync(0, 'utf-8');
777
+ } catch {
778
+ console.error('Error: No input file specified');
779
+ console.error('Usage: dgmo cat <file>');
780
+ process.exit(1);
781
+ }
782
+ }
783
+
784
+ const { highlightDgmo, renderAnsi } =
785
+ await import('./editor/highlight-api');
786
+ const tokens = highlightDgmo(catContent);
787
+ process.stdout.write(renderAnsi(tokens, useColor));
788
+ return;
789
+ }
790
+
749
791
  if (opts.installClaudeCodeIntegration) {
750
792
  const claudeDir = join(homedir(), '.claude');
751
793
  if (!existsSync(claudeDir)) {
package/src/d3.ts CHANGED
@@ -509,6 +509,7 @@ export function parseVisualization(
509
509
  let timelineEraBlockIndent = 0;
510
510
  let inTimelineMarkerBlock = false;
511
511
  let timelineMarkerBlockIndent = 0;
512
+ let inSlopePeriodBlock = false;
512
513
  const timelineAliasMap = new Map<string, string>();
513
514
  const VALID_D3_TYPES = new Set([
514
515
  'slope',
@@ -1099,6 +1100,164 @@ export function parseVisualization(
1099
1100
  }
1100
1101
  }
1101
1102
 
1103
+ // ── Slope chart: period directive + right-scan data rows ──
1104
+ if (result.type === 'slope') {
1105
+ // Period block: indented lines inside `period` block
1106
+ // (blank lines are pre-filtered at loop top, so only non-indented lines close the block)
1107
+ if (inSlopePeriodBlock) {
1108
+ if (indent > 0) {
1109
+ result.periods.push(line);
1110
+ continue;
1111
+ }
1112
+ // Non-indented line → close block, fall through to process normally
1113
+ inSlopePeriodBlock = false;
1114
+ }
1115
+
1116
+ // Period directive: `period Label1 Label2` or bare `period` (block open)
1117
+ // Only accept before data rows start (F4: prevent keyword shadowing labels)
1118
+ if (result.data.length === 0) {
1119
+ const periodMatch = line.match(/^period\b(.*)$/i);
1120
+ if (periodMatch) {
1121
+ if (result.periods.length > 0 && !inSlopePeriodBlock) {
1122
+ // F5: warn on duplicate period directives
1123
+ warn(
1124
+ lineNumber,
1125
+ `Duplicate 'period' directive — periods are already defined`
1126
+ );
1127
+ }
1128
+ const rest = periodMatch[1].trim();
1129
+ if (rest) {
1130
+ // One-line: `period 1715 1725`
1131
+ const periodLabels = rest.split(/\s+/);
1132
+ result.periods.push(...periodLabels);
1133
+ } else {
1134
+ // Block open: bare `period`
1135
+ inSlopePeriodBlock = true;
1136
+ }
1137
+ continue;
1138
+ }
1139
+ }
1140
+
1141
+ // Migration error: bare period line (old syntax — comma-separated, no keyword)
1142
+ // F1: Only fire when ALL comma-separated tokens are short (≤20 chars) and non-empty
1143
+ if (
1144
+ result.periods.length === 0 &&
1145
+ line.includes(',') &&
1146
+ !line.includes(':')
1147
+ ) {
1148
+ const tokens = line
1149
+ .split(',')
1150
+ .map((t) => t.trim())
1151
+ .filter(Boolean);
1152
+ const looksLikePeriods =
1153
+ tokens.length >= 2 && tokens.every((t) => t.length <= 20);
1154
+ if (looksLikePeriods) {
1155
+ return fail(
1156
+ lineNumber,
1157
+ `Period lines require the 'period' keyword — use 'period ${tokens.join(' ')}'`
1158
+ );
1159
+ }
1160
+ }
1161
+
1162
+ // Migration error: old colon syntax in data rows
1163
+ // F2: Only fire when content after colon is predominantly numeric (old "Label: val1, val2" pattern)
1164
+ if (line.includes(':')) {
1165
+ const colonPos = line.indexOf(':');
1166
+ const afterColon = line.substring(colonPos + 1).trim();
1167
+ const numericTokens = afterColon
1168
+ .split(/[,\s]+/)
1169
+ .filter((v) => /^-?\d/.test(v));
1170
+ // Only trigger if most tokens after the colon are numeric (old data pattern)
1171
+ if (numericTokens.length >= 1) {
1172
+ const allTokens = afterColon.split(/[,\s]+/).filter(Boolean);
1173
+ if (numericTokens.length >= allTokens.length * 0.5) {
1174
+ const label = line.substring(0, colonPos).trim();
1175
+ return fail(
1176
+ lineNumber,
1177
+ `Colons are no longer used in slope data rows — use '${label} ${numericTokens.join(' ')}'`
1178
+ );
1179
+ }
1180
+ }
1181
+ }
1182
+
1183
+ // Right-scan data row parsing (requires periods to be known)
1184
+ if (result.periods.length >= 2) {
1185
+ const P = result.periods.length;
1186
+ const tokens = line.split(/\s+/);
1187
+ const values: number[] = [];
1188
+
1189
+ // Scan from right, capped at P values
1190
+ let rightIdx = tokens.length - 1;
1191
+ while (rightIdx >= 0 && values.length < P) {
1192
+ const raw = tokens[rightIdx].replace(/,/g, '');
1193
+ const num = parseFloat(raw);
1194
+ if (!isNaN(num) && /^-?\d/.test(raw)) {
1195
+ values.unshift(num);
1196
+ rightIdx--;
1197
+ } else {
1198
+ break;
1199
+ }
1200
+ }
1201
+
1202
+ if (values.length < P) {
1203
+ warn(
1204
+ lineNumber,
1205
+ `Data row has ${values.length} numeric value(s) but ${P} period(s) are defined — expected ${P} values`
1206
+ );
1207
+ continue;
1208
+ }
1209
+
1210
+ // Remaining left tokens = label
1211
+ const labelTokens = tokens.slice(0, rightIdx + 1);
1212
+ const joinedLabel = labelTokens.join(' ');
1213
+
1214
+ if (!joinedLabel) {
1215
+ warn(
1216
+ lineNumber,
1217
+ `Data row has no label — add a label before the numeric values`
1218
+ );
1219
+ continue;
1220
+ }
1221
+
1222
+ // Color annotation: `Label (color)` → extract color
1223
+ const colorMatch = joinedLabel.match(/^(.+?)\(([^)]+)\)\s*$/);
1224
+ const labelPart = colorMatch ? colorMatch[1].trim() : joinedLabel;
1225
+ const colorPart = colorMatch
1226
+ ? resolveColor(colorMatch[2].trim(), palette)
1227
+ : null;
1228
+
1229
+ if (!labelPart) {
1230
+ warn(
1231
+ lineNumber,
1232
+ `Data row has no label — add a label before the numeric values`
1233
+ );
1234
+ continue;
1235
+ }
1236
+
1237
+ // F3: Warn on purely numeric labels — likely a mistake
1238
+ if (/^\d[\d,.]*$/.test(labelPart)) {
1239
+ warn(
1240
+ lineNumber,
1241
+ `Label '${labelPart}' looks numeric — this may indicate too many values or a missing label`
1242
+ );
1243
+ }
1244
+
1245
+ result.data.push({
1246
+ label: labelPart,
1247
+ values,
1248
+ color: colorPart,
1249
+ lineNumber,
1250
+ });
1251
+ continue;
1252
+ }
1253
+
1254
+ // If we get here in a slope chart, it's an unrecognized line
1255
+ if (firstLineParsed) {
1256
+ warn(lineNumber, `Unexpected line: '${line}'.`);
1257
+ }
1258
+ continue;
1259
+ }
1260
+
1102
1261
  // ── Colon-separated metadata / options (legacy + data lines) ──
1103
1262
  const colonIndex = line.indexOf(':');
1104
1263
 
@@ -1222,23 +1381,6 @@ export function parseVisualization(
1222
1381
  continue;
1223
1382
  }
1224
1383
 
1225
- // Period line: comma-separated labels with no colon before first comma
1226
- // e.g., "2020, 2024" or "Q1 2023, Q2 2023, Q3 2023"
1227
- if (
1228
- result.periods.length === 0 &&
1229
- line.includes(',') &&
1230
- !line.includes(':')
1231
- ) {
1232
- const periods = line
1233
- .split(',')
1234
- .map((p) => p.trim())
1235
- .filter(Boolean);
1236
- if (periods.length >= 2) {
1237
- result.periods = periods;
1238
- continue;
1239
- }
1240
- }
1241
-
1242
1384
  // Catch-all: nothing matched this line
1243
1385
  // Skip on first line — chart type suggestion is handled post-loop
1244
1386
  if (firstLineParsed) {
@@ -1408,14 +1550,14 @@ export function parseVisualization(
1408
1550
  if (result.periods.length < 2) {
1409
1551
  return fail(
1410
1552
  1,
1411
- 'Missing or invalid periods line. Provide at least 2 comma-separated period labels (e.g., "2020, 2024")'
1553
+ "Missing 'period' directive. Add 'period 2020 2024' before data rows (minimum 2 periods required)"
1412
1554
  );
1413
1555
  }
1414
1556
 
1415
1557
  if (result.data.length === 0) {
1416
1558
  warn(
1417
1559
  1,
1418
- 'No data lines found. Add data as "Label: value1, value2" (e.g., "Apple: 25, 35")'
1560
+ "No data lines found. Add data as 'Label value1 value2' (e.g., 'Blackbeard 40 4')"
1419
1561
  );
1420
1562
  }
1421
1563