@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.
- package/.cursorrules +4 -1
- package/.github/copilot-instructions.md +4 -1
- package/.windsurfrules +4 -1
- package/README.md +21 -3
- package/SKILL.md +4 -1
- package/dist/advanced.cjs +1853 -623
- package/dist/advanced.d.cts +143 -16
- package/dist/advanced.d.ts +143 -16
- package/dist/advanced.js +1846 -623
- package/dist/auto.cjs +1640 -581
- package/dist/auto.js +99 -99
- package/dist/auto.mjs +1640 -581
- package/dist/cli.cjs +148 -147
- package/dist/index.cjs +1643 -662
- package/dist/index.js +1643 -662
- package/docs/ai-integration.md +4 -1
- package/docs/language-reference.md +282 -27
- package/gallery/fixtures/boxes-and-lines.dgmo +2 -2
- package/gallery/fixtures/c4-full.dgmo +4 -5
- package/gallery/fixtures/c4.dgmo +2 -3
- package/package.json +7 -1
- package/src/advanced.ts +10 -0
- package/src/boxes-and-lines/focus.ts +257 -0
- package/src/boxes-and-lines/layout-search.ts +345 -65
- package/src/boxes-and-lines/layout.ts +11 -1
- package/src/boxes-and-lines/parser.ts +97 -4
- package/src/boxes-and-lines/renderer.ts +111 -8
- package/src/boxes-and-lines/types.ts +9 -0
- package/src/c4/parser.ts +8 -7
- package/src/c4/renderer.ts +7 -5
- package/src/chart-type-registry.ts +129 -4
- package/src/chart-types.ts +3 -3
- package/src/chart.ts +18 -1
- package/src/class/renderer.ts +4 -2
- package/src/cli-banner.ts +107 -0
- package/src/cli.ts +13 -0
- package/src/colors.ts +247 -2
- package/src/cycle/parser.ts +2 -7
- package/src/d3.ts +67 -54
- package/src/diagnostics.ts +17 -0
- package/src/dimensions.ts +9 -13
- package/src/echarts.ts +42 -14
- package/src/er/parser.ts +6 -1
- package/src/er/renderer.ts +4 -2
- package/src/gantt/parser.ts +44 -7
- package/src/graph/flowchart-parser.ts +77 -3
- package/src/graph/flowchart-renderer.ts +4 -2
- package/src/graph/state-renderer.ts +6 -4
- package/src/infra/parser.ts +80 -0
- package/src/infra/renderer.ts +8 -4
- package/src/journey-map/parser.ts +23 -8
- package/src/journey-map/renderer.ts +1 -1
- package/src/kanban/parser.ts +8 -7
- package/src/kanban/renderer.ts +1 -1
- package/src/map/context-labels.ts +134 -27
- package/src/map/geo.ts +10 -2
- package/src/map/layout.ts +259 -4
- package/src/map/parser.ts +2 -0
- package/src/map/renderer.ts +49 -25
- package/src/map/resolver.ts +68 -19
- package/src/mindmap/parser.ts +15 -7
- package/src/mindmap/renderer.ts +55 -15
- package/src/org/parser.ts +8 -7
- package/src/org/renderer.ts +89 -127
- package/src/palettes/color-utils.ts +19 -4
- package/src/palettes/index.ts +1 -0
- package/src/pert/renderer.ts +15 -10
- package/src/pyramid/parser.ts +2 -7
- package/src/quadrant/renderer.ts +2 -2
- package/src/raci/parser.ts +2 -7
- package/src/raci/renderer.ts +5 -5
- package/src/ring/parser.ts +2 -7
- package/src/sequence/parser.ts +18 -7
- package/src/sequence/renderer.ts +4 -4
- package/src/sitemap/parser.ts +8 -7
- package/src/sitemap/renderer.ts +37 -39
- package/src/tech-radar/parser.ts +2 -7
- package/src/timeline/renderer.ts +15 -5
- package/src/utils/card.ts +183 -0
- package/src/utils/parsing.ts +13 -1
- package/src/utils/scaling.ts +38 -81
- package/src/utils/tag-groups.ts +48 -10
- package/src/utils/visual-conventions.ts +61 -0
- package/src/visualizations/parse.ts +6 -1
- package/src/wireframe/parser.ts +6 -1
package/src/chart.ts
CHANGED
|
@@ -405,7 +405,9 @@ export function parseChart(
|
|
|
405
405
|
if (dataValues) {
|
|
406
406
|
const { label: rawLabel, color: pointColor } = extractColor(
|
|
407
407
|
dataValues.label,
|
|
408
|
-
palette
|
|
408
|
+
palette,
|
|
409
|
+
result.diagnostics,
|
|
410
|
+
lineNumber
|
|
409
411
|
);
|
|
410
412
|
const [first, ...rest] = dataValues.values;
|
|
411
413
|
result.data.push({
|
|
@@ -485,6 +487,21 @@ export function parseChart(
|
|
|
485
487
|
);
|
|
486
488
|
}
|
|
487
489
|
|
|
490
|
+
// Plain "bar" renders a single series per row — extra series are dropped
|
|
491
|
+
// silently by the renderer. Surface that instead of losing data quietly so
|
|
492
|
+
// the author switches to a multi-series type. (multi-line/line parse to
|
|
493
|
+
// type "line" and DO render every series, so they are unaffected.)
|
|
494
|
+
if (
|
|
495
|
+
!result.error &&
|
|
496
|
+
result.type === 'bar' &&
|
|
497
|
+
(result.seriesNames?.length ?? 0) > 1
|
|
498
|
+
) {
|
|
499
|
+
warn(
|
|
500
|
+
result.seriesLineNumber ?? 1,
|
|
501
|
+
`Plain "bar" shows only the first series ("${result.seriesNames![0]}"); the other ${result.seriesNames!.length - 1} are dropped at render. Use "bar-stacked" for stacked bars or "multi-line" to plot every series.`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
488
505
|
if (!result.error && result.seriesNames) {
|
|
489
506
|
const expectedCount = result.seriesNames.length;
|
|
490
507
|
for (const dp of result.data) {
|
package/src/class/renderer.ts
CHANGED
|
@@ -47,8 +47,10 @@ const MAX_SCALE = 3;
|
|
|
47
47
|
const CLASS_FONT_SIZE = 13;
|
|
48
48
|
const MEMBER_FONT_SIZE = 11;
|
|
49
49
|
const EDGE_LABEL_FONT_SIZE = 11;
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
import {
|
|
51
|
+
EDGE_STROKE_WIDTH,
|
|
52
|
+
NODE_STROKE_WIDTH,
|
|
53
|
+
} from '../utils/visual-conventions'; // shared (Story 111.1)
|
|
52
54
|
const MEMBER_LINE_HEIGHT = 18;
|
|
53
55
|
const COMPARTMENT_PADDING_Y = 8;
|
|
54
56
|
const MEMBER_PADDING_X = 10;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// CLI Banner — colorful ASCII logo for `dgmo`
|
|
3
|
+
// ============================================================
|
|
4
|
+
//
|
|
5
|
+
// Rendered as the header of `dgmo --help` and at the end of the
|
|
6
|
+
// install/init flows. Uses a horizontal true-color (24-bit) gradient
|
|
7
|
+
// sampled from the default `slate` palette (corporate blue → teal →
|
|
8
|
+
// steel cyan) so the wordmark matches the brand.
|
|
9
|
+
//
|
|
10
|
+
// Honors the same guards as `dgmo cat`: color only when stdout is a TTY
|
|
11
|
+
// and NO_COLOR is unset; otherwise prints plain ASCII so pipes/CI stay
|
|
12
|
+
// clean.
|
|
13
|
+
|
|
14
|
+
import { getPalette } from './palettes';
|
|
15
|
+
|
|
16
|
+
// ANSI Shadow letterform for "dgmo" (6 rows) + tagline.
|
|
17
|
+
const ART = [
|
|
18
|
+
'██████╗ ██████╗ ███╗ ███╗ ██████╗ ',
|
|
19
|
+
'██╔══██╗██╔════╝ ████╗ ████║██╔═══██╗',
|
|
20
|
+
'██║ ██║██║ ███╗██╔████╔██║██║ ██║',
|
|
21
|
+
'██║ ██║██║ ██║██║╚██╔╝██║██║ ██║',
|
|
22
|
+
'██████╔╝╚██████╔╝██║ ╚═╝ ██║╚██████╔╝',
|
|
23
|
+
'╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const TAGLINE = 'diagrams as code';
|
|
27
|
+
|
|
28
|
+
// Gradient stops, pulled by name from the live slate palette (light) so the
|
|
29
|
+
// wordmark tracks any palette edit instead of drifting from a hardcoded copy.
|
|
30
|
+
// blue → teal → green → amber: a vivid sweep across the categorical hues.
|
|
31
|
+
const GRADIENT_COLOR_NAMES = ['blue', 'teal', 'green', 'orange'] as const;
|
|
32
|
+
|
|
33
|
+
function hexToRgb(hex: string): [number, number, number] {
|
|
34
|
+
const h = hex.replace('#', '');
|
|
35
|
+
return [
|
|
36
|
+
parseInt(h.slice(0, 2), 16),
|
|
37
|
+
parseInt(h.slice(2, 4), 16),
|
|
38
|
+
parseInt(h.slice(4, 6), 16),
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const STOPS: Array<[number, number, number]> = (() => {
|
|
43
|
+
const colors = getPalette('slate').light.colors;
|
|
44
|
+
return GRADIENT_COLOR_NAMES.map((name) => hexToRgb(colors[name]));
|
|
45
|
+
})();
|
|
46
|
+
|
|
47
|
+
function lerp(a: number, b: number, t: number): number {
|
|
48
|
+
return Math.round(a + (b - a) * t);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Interpolate the multi-stop gradient at position t ∈ [0, 1]. */
|
|
52
|
+
function gradientAt(t: number): [number, number, number] {
|
|
53
|
+
const clamped = Math.max(0, Math.min(1, t));
|
|
54
|
+
const span = STOPS.length - 1;
|
|
55
|
+
const scaled = clamped * span;
|
|
56
|
+
const i = Math.min(span - 1, Math.floor(scaled));
|
|
57
|
+
const local = scaled - i;
|
|
58
|
+
const [r1, g1, b1] = STOPS[i]!;
|
|
59
|
+
const [r2, g2, b2] = STOPS[i + 1]!;
|
|
60
|
+
return [lerp(r1, r2, local), lerp(g1, g2, local), lerp(b1, b2, local)];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function fg(r: number, g: number, b: number): string {
|
|
64
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const RESET = '\x1b[0m';
|
|
68
|
+
|
|
69
|
+
export interface BannerOptions {
|
|
70
|
+
/** Force-disable color regardless of TTY (default honors stdout TTY + NO_COLOR). */
|
|
71
|
+
color?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Build the dgmo banner string. Colorized with a horizontal gradient when
|
|
76
|
+
* `color` is true; otherwise returns plain ASCII.
|
|
77
|
+
*/
|
|
78
|
+
export function renderBanner(opts: BannerOptions = {}): string {
|
|
79
|
+
const useColor =
|
|
80
|
+
opts.color ?? (process.stdout.isTTY === true && !process.env['NO_COLOR']);
|
|
81
|
+
|
|
82
|
+
const width = Math.max(...ART.map((line) => line.length));
|
|
83
|
+
|
|
84
|
+
const lines = ART.map((line) => {
|
|
85
|
+
if (!useColor) return line;
|
|
86
|
+
let out = '';
|
|
87
|
+
for (let col = 0; col < line.length; col++) {
|
|
88
|
+
const ch = line[col];
|
|
89
|
+
// Don't paint spaces — keeps escape sequences minimal.
|
|
90
|
+
if (ch === ' ') {
|
|
91
|
+
out += ch;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const [r, g, b] = gradientAt(col / (width - 1));
|
|
95
|
+
out += fg(r, g, b) + ch;
|
|
96
|
+
}
|
|
97
|
+
return out + RESET;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Right-align the tagline under the wordmark, muted.
|
|
101
|
+
const pad = Math.max(0, width - TAGLINE.length);
|
|
102
|
+
const tagline = useColor
|
|
103
|
+
? `${' '.repeat(pad)}\x1b[2;3m${TAGLINE}${RESET}`
|
|
104
|
+
: `${' '.repeat(pad)}${TAGLINE}`;
|
|
105
|
+
|
|
106
|
+
return ['', ...lines, tagline, ''].join('\n');
|
|
107
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
migrateFile,
|
|
26
26
|
} from './migrate';
|
|
27
27
|
import { migrateEmbedded } from './migrate/embedded';
|
|
28
|
+
import { renderBanner } from './cli-banner';
|
|
28
29
|
|
|
29
30
|
// Derived from the palette registry so new palettes are auto-included.
|
|
30
31
|
const PALETTES = getAvailablePalettes().map((p) => p.id);
|
|
@@ -464,6 +465,7 @@ For architecture diagrams, sequence diagrams, flowcharts, and charts, use the \`
|
|
|
464
465
|
`;
|
|
465
466
|
|
|
466
467
|
function printHelp(): void {
|
|
468
|
+
console.log(renderBanner());
|
|
467
469
|
console.log(`Usage: dgmo <input> [options]
|
|
468
470
|
cat input.dgmo | dgmo [options]
|
|
469
471
|
dgmo cat <file> Display file with syntax highlighting
|
|
@@ -648,10 +650,12 @@ function svgToPng(svg: string, background?: string): Buffer {
|
|
|
648
650
|
}
|
|
649
651
|
|
|
650
652
|
function noInput(): never {
|
|
653
|
+
console.error(renderBanner());
|
|
651
654
|
const samplePath = resolve('sample.dgmo');
|
|
652
655
|
if (existsSync(samplePath)) {
|
|
653
656
|
console.error('Error: No input file specified');
|
|
654
657
|
console.error(`Try: dgmo ${basename(samplePath)}`);
|
|
658
|
+
console.error('Run dgmo --help for all options.');
|
|
655
659
|
process.exit(1);
|
|
656
660
|
}
|
|
657
661
|
writeFileSync(
|
|
@@ -925,6 +929,15 @@ async function main(): Promise<void> {
|
|
|
925
929
|
return;
|
|
926
930
|
}
|
|
927
931
|
|
|
932
|
+
if (
|
|
933
|
+
opts.installClaudeCodeIntegration ||
|
|
934
|
+
opts.installClaudeSkill ||
|
|
935
|
+
opts.installCodexIntegration ||
|
|
936
|
+
opts.installClaudeDesktopIntegration
|
|
937
|
+
) {
|
|
938
|
+
console.log(renderBanner());
|
|
939
|
+
}
|
|
940
|
+
|
|
928
941
|
if (opts.installClaudeCodeIntegration) {
|
|
929
942
|
const claudeDir = join(homedir(), '.claude');
|
|
930
943
|
if (!existsSync(claudeDir)) {
|
package/src/colors.ts
CHANGED
|
@@ -60,6 +60,28 @@ export const RECOGNIZED_COLOR_NAMES = Object.freeze([
|
|
|
60
60
|
'white',
|
|
61
61
|
] as const);
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The canonical order in which the categorical (non-neutral) color names are
|
|
65
|
+
* auto-assigned: tag/group swatches (`autoTagColorCycle`) and data-chart
|
|
66
|
+
* series colors (`getSeriesColors`) both derive their rotation from this list.
|
|
67
|
+
*
|
|
68
|
+
* Seeded with the RGB primaries (`red, green, blue`) for an unmistakable first
|
|
69
|
+
* three, then each subsequent hue is chosen to fill the widest remaining gap on
|
|
70
|
+
* the color wheel — maximizing contrast between adjacent swatches. Neutrals
|
|
71
|
+
* (`gray`/`black`/`white`) are intentionally excluded so auto-picked colors
|
|
72
|
+
* always read as distinct legend swatches.
|
|
73
|
+
*/
|
|
74
|
+
export const CATEGORICAL_COLOR_ORDER = Object.freeze([
|
|
75
|
+
'red',
|
|
76
|
+
'green',
|
|
77
|
+
'blue',
|
|
78
|
+
'yellow',
|
|
79
|
+
'teal',
|
|
80
|
+
'purple',
|
|
81
|
+
'orange',
|
|
82
|
+
'cyan',
|
|
83
|
+
] as const);
|
|
84
|
+
|
|
63
85
|
/**
|
|
64
86
|
* Returns true iff `name` is one of the 11 recognized DGMO color names.
|
|
65
87
|
*/
|
|
@@ -95,6 +117,211 @@ export function resolveColor(
|
|
|
95
117
|
import type { DgmoError } from './diagnostics';
|
|
96
118
|
import { makeDgmoError, suggest } from './diagnostics';
|
|
97
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Stable diagnostic code for "this token is not one of the 11 named palette
|
|
122
|
+
* colors" — covers both hex/CSS literals (emitted as `error`) and unrecognized
|
|
123
|
+
* bare words like `crimson` (emitted as `warning`). Consumers that want to
|
|
124
|
+
* HARD-BLOCK invalid colors regardless of severity (e.g. the MCP render gate)
|
|
125
|
+
* filter on this code rather than re-deriving the rule.
|
|
126
|
+
*/
|
|
127
|
+
export const INVALID_COLOR_CODE = 'E_INVALID_COLOR';
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* CSS / X11 color names that are NOT one of DGMO's 11 — mapped to their hex so
|
|
131
|
+
* a "nearest valid color" hint can be computed. This is the blocklist that lets
|
|
132
|
+
* the trailing-token rule tell an *intended-but-invalid* color (`pink`,
|
|
133
|
+
* `crimson`, `navy`) apart from an ordinary label word (`Zinfandel`, `Blanc`):
|
|
134
|
+
* a lowercase trailing token found here is flagged, anything else stays label
|
|
135
|
+
* text. Our 11 valid names are deliberately excluded. Extend freely — it only
|
|
136
|
+
* sharpens detection. (Case-sensitive lowercase, matching the §1.5 color rule.)
|
|
137
|
+
*/
|
|
138
|
+
export const INVALID_CSS_COLOR_HEX: Readonly<Record<string, string>> =
|
|
139
|
+
Object.freeze({
|
|
140
|
+
pink: '#ffc0cb',
|
|
141
|
+
hotpink: '#ff69b4',
|
|
142
|
+
deeppink: '#ff1493',
|
|
143
|
+
lightpink: '#ffb6c1',
|
|
144
|
+
palevioletred: '#db7093',
|
|
145
|
+
crimson: '#dc143c',
|
|
146
|
+
scarlet: '#ff2400',
|
|
147
|
+
firebrick: '#b22222',
|
|
148
|
+
darkred: '#8b0000',
|
|
149
|
+
maroon: '#800000',
|
|
150
|
+
salmon: '#fa8072',
|
|
151
|
+
lightsalmon: '#ffa07a',
|
|
152
|
+
darksalmon: '#e9967a',
|
|
153
|
+
coral: '#ff7f50',
|
|
154
|
+
lightcoral: '#f08080',
|
|
155
|
+
tomato: '#ff6347',
|
|
156
|
+
orangered: '#ff4500',
|
|
157
|
+
darkorange: '#ff8c00',
|
|
158
|
+
gold: '#ffd700',
|
|
159
|
+
goldenrod: '#daa520',
|
|
160
|
+
darkgoldenrod: '#b8860b',
|
|
161
|
+
khaki: '#f0e68c',
|
|
162
|
+
darkkhaki: '#bdb76b',
|
|
163
|
+
amber: '#ffbf00',
|
|
164
|
+
lavender: '#e6e6fa',
|
|
165
|
+
violet: '#ee82ee',
|
|
166
|
+
magenta: '#ff00ff',
|
|
167
|
+
fuchsia: '#ff00ff',
|
|
168
|
+
orchid: '#da70d6',
|
|
169
|
+
plum: '#dda0dd',
|
|
170
|
+
indigo: '#4b0082',
|
|
171
|
+
navy: '#000080',
|
|
172
|
+
midnightblue: '#191970',
|
|
173
|
+
darkblue: '#00008b',
|
|
174
|
+
mediumblue: '#0000cd',
|
|
175
|
+
royalblue: '#4169e1',
|
|
176
|
+
cornflowerblue: '#6495ed',
|
|
177
|
+
dodgerblue: '#1e90ff',
|
|
178
|
+
deepskyblue: '#00bfff',
|
|
179
|
+
skyblue: '#87ceeb',
|
|
180
|
+
lightskyblue: '#87cefa',
|
|
181
|
+
lightblue: '#add8e6',
|
|
182
|
+
powderblue: '#b0e0e6',
|
|
183
|
+
steelblue: '#4682b4',
|
|
184
|
+
slateblue: '#6a5acd',
|
|
185
|
+
cadetblue: '#5f9ea0',
|
|
186
|
+
turquoise: '#40e0d0',
|
|
187
|
+
aqua: '#00ffff',
|
|
188
|
+
aquamarine: '#7fffd4',
|
|
189
|
+
lime: '#00ff00',
|
|
190
|
+
limegreen: '#32cd32',
|
|
191
|
+
lightgreen: '#90ee90',
|
|
192
|
+
palegreen: '#98fb98',
|
|
193
|
+
seagreen: '#2e8b57',
|
|
194
|
+
mediumseagreen: '#3cb371',
|
|
195
|
+
forestgreen: '#228b22',
|
|
196
|
+
darkgreen: '#006400',
|
|
197
|
+
olive: '#808000',
|
|
198
|
+
olivedrab: '#6b8e23',
|
|
199
|
+
darkolivegreen: '#556b2f',
|
|
200
|
+
chartreuse: '#7fff00',
|
|
201
|
+
lawngreen: '#7cfc00',
|
|
202
|
+
springgreen: '#00ff7f',
|
|
203
|
+
greenyellow: '#adff2f',
|
|
204
|
+
brown: '#a52a2a',
|
|
205
|
+
sienna: '#a0522d',
|
|
206
|
+
chocolate: '#d2691e',
|
|
207
|
+
peru: '#cd853f',
|
|
208
|
+
tan: '#d2b48c',
|
|
209
|
+
beige: '#f5f5dc',
|
|
210
|
+
wheat: '#f5deb3',
|
|
211
|
+
ivory: '#fffff0',
|
|
212
|
+
silver: '#c0c0c0',
|
|
213
|
+
lightgray: '#d3d3d3',
|
|
214
|
+
lightgrey: '#d3d3d3',
|
|
215
|
+
darkgray: '#a9a9a9',
|
|
216
|
+
darkgrey: '#a9a9a9',
|
|
217
|
+
dimgray: '#696969',
|
|
218
|
+
dimgrey: '#696969',
|
|
219
|
+
slategray: '#708090',
|
|
220
|
+
slategrey: '#708090',
|
|
221
|
+
gainsboro: '#dcdcdc',
|
|
222
|
+
grey: '#808080',
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Best-effort nearest recognized color NAME for an unsupported hex value.
|
|
227
|
+
* Matches by HUE (with a low-saturation cutoff routing to black/white/gray),
|
|
228
|
+
* NOT raw RGB distance to the muted palette hexes — vivid LLM colors like a
|
|
229
|
+
* `#3cb44b` green would otherwise snap to `gray` against a desaturated sage.
|
|
230
|
+
* Returns null for non-hex input (CSS function/keyword colors, no RGB to read).
|
|
231
|
+
* Used ONLY to enrich a diagnostic — never to silently accept the value.
|
|
232
|
+
*/
|
|
233
|
+
export function nearestNamedColor(input: string): string | null {
|
|
234
|
+
// CSS color name → resolve to its hex first so hue-matching works on it too.
|
|
235
|
+
const cssHex = INVALID_CSS_COLOR_HEX[input.trim().toLowerCase()];
|
|
236
|
+
if (cssHex) input = cssHex;
|
|
237
|
+
const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(input.trim());
|
|
238
|
+
if (!m) return null;
|
|
239
|
+
let h = m[1]!.toLowerCase();
|
|
240
|
+
if (h.length === 3)
|
|
241
|
+
h = h
|
|
242
|
+
.split('')
|
|
243
|
+
.map((c) => c + c)
|
|
244
|
+
.join('');
|
|
245
|
+
const r = parseInt(h.slice(0, 2), 16) / 255;
|
|
246
|
+
const g = parseInt(h.slice(2, 4), 16) / 255;
|
|
247
|
+
const b = parseInt(h.slice(4, 6), 16) / 255;
|
|
248
|
+
const max = Math.max(r, g, b);
|
|
249
|
+
const min = Math.min(r, g, b);
|
|
250
|
+
const delta = max - min;
|
|
251
|
+
const l = (max + min) / 2;
|
|
252
|
+
const s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
|
|
253
|
+
// Low chroma → a neutral; pick by lightness.
|
|
254
|
+
if (s < 0.15) {
|
|
255
|
+
if (l < 0.2) return 'black';
|
|
256
|
+
if (l > 0.85) return 'white';
|
|
257
|
+
return 'gray';
|
|
258
|
+
}
|
|
259
|
+
let hue: number;
|
|
260
|
+
if (max === r) hue = 60 * (((g - b) / delta) % 6);
|
|
261
|
+
else if (max === g) hue = 60 * ((b - r) / delta + 2);
|
|
262
|
+
else hue = 60 * ((r - g) / delta + 4);
|
|
263
|
+
if (hue < 0) hue += 360;
|
|
264
|
+
// Canonical hue anchors for the 8 chromatic names (red appears at both 0 and
|
|
265
|
+
// 360 so wrap-around resolves correctly).
|
|
266
|
+
const anchors: readonly [string, number][] = [
|
|
267
|
+
['red', 0],
|
|
268
|
+
['orange', 30],
|
|
269
|
+
['yellow', 55],
|
|
270
|
+
['green', 120],
|
|
271
|
+
['teal', 170],
|
|
272
|
+
['cyan', 190],
|
|
273
|
+
['blue', 225],
|
|
274
|
+
['purple', 285],
|
|
275
|
+
['red', 360],
|
|
276
|
+
];
|
|
277
|
+
let best = 'red';
|
|
278
|
+
let bestD = Infinity;
|
|
279
|
+
for (const [name, deg] of anchors) {
|
|
280
|
+
const d = Math.abs(hue - deg);
|
|
281
|
+
if (d < bestD) {
|
|
282
|
+
bestD = d;
|
|
283
|
+
best = name;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return best;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* True iff `token` is an INTENDED-but-invalid color: a hex/`rgb()`/`hsl()`
|
|
291
|
+
* literal, or a known CSS color name that isn't one of DGMO's 11. Lets the
|
|
292
|
+
* trailing-token rule flag `Rosé pink` / `Foo #e6194b` while leaving genuine
|
|
293
|
+
* label words (`Zinfandel`) untouched.
|
|
294
|
+
*/
|
|
295
|
+
export function isInvalidColorToken(token: string): boolean {
|
|
296
|
+
if (/^(#|rgba?\(|hsla?\()/i.test(token)) return true;
|
|
297
|
+
const lower = token.toLowerCase();
|
|
298
|
+
return (
|
|
299
|
+
INVALID_CSS_COLOR_HEX[lower] !== undefined && !isRecognizedColorName(lower)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Build an `E_INVALID_COLOR` diagnostic for an intended-but-invalid trailing
|
|
305
|
+
* color token, or return null if `token` isn't color-like (so it stays label
|
|
306
|
+
* text). Severity is `warning` so the library degrades gracefully (the value
|
|
307
|
+
* just keeps the word); the MCP render gate blocks on the code regardless.
|
|
308
|
+
* Used by `extractColor` to close the trailing-token "silent swallow" gap.
|
|
309
|
+
*/
|
|
310
|
+
export function invalidColorDiagnostic(
|
|
311
|
+
token: string,
|
|
312
|
+
line: number
|
|
313
|
+
): DgmoError | null {
|
|
314
|
+
if (!isInvalidColorToken(token)) return null;
|
|
315
|
+
const nearest = nearestNamedColor(token);
|
|
316
|
+
const near = nearest ? ` Nearest: ${nearest}.` : '';
|
|
317
|
+
return makeDgmoError(
|
|
318
|
+
line,
|
|
319
|
+
`Color "${token}" is not a valid DGMO color — DGMO accepts only these 11 named colors: ${RECOGNIZED_COLOR_NAMES.join(', ')} (no hex, no CSS color names).${near}`,
|
|
320
|
+
'warning',
|
|
321
|
+
INVALID_COLOR_CODE
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
98
325
|
/**
|
|
99
326
|
* Resolves a color name and pushes a warning diagnostic on failure.
|
|
100
327
|
* Returns the hex string for valid names, or `undefined` for unknown
|
|
@@ -109,13 +336,31 @@ export function resolveColorWithDiagnostic(
|
|
|
109
336
|
): string | undefined {
|
|
110
337
|
const resolved = resolveColor(color, palette);
|
|
111
338
|
if (resolved !== null) return resolved;
|
|
339
|
+
// Literal color values (hex `#e6194b`, `rgb(...)`, `hsl(...)`) are NOT part
|
|
340
|
+
// of the DGMO language — only the 11 named palette colors are accepted, so
|
|
341
|
+
// that a single source recolors correctly across every palette and theme.
|
|
342
|
+
// Flag these with a precise error rather than a typo suggestion.
|
|
343
|
+
if (/^(#|rgba?\(|hsla?\()/i.test(color)) {
|
|
344
|
+
const nearest = nearestNamedColor(color);
|
|
345
|
+
const near = nearest ? ` Nearest: ${nearest}.` : '';
|
|
346
|
+
diagnostics.push(
|
|
347
|
+
makeDgmoError(
|
|
348
|
+
line,
|
|
349
|
+
`Color "${color}" is not supported — DGMO does not accept hex or CSS color values. Use a named palette color: ${RECOGNIZED_COLOR_NAMES.join(', ')}.${near}`,
|
|
350
|
+
'error',
|
|
351
|
+
INVALID_COLOR_CODE
|
|
352
|
+
)
|
|
353
|
+
);
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
112
356
|
const hint = suggest(color, RECOGNIZED_COLOR_NAMES as readonly string[]);
|
|
113
357
|
const suggestion = hint ? ` ${hint}` : '';
|
|
114
358
|
diagnostics.push(
|
|
115
359
|
makeDgmoError(
|
|
116
360
|
line,
|
|
117
|
-
`Unknown color "${color}".
|
|
118
|
-
'warning'
|
|
361
|
+
`Unknown color "${color}". DGMO accepts only these 11 named colors: ${RECOGNIZED_COLOR_NAMES.join(', ')} (no hex, no CSS color names).${suggestion}`,
|
|
362
|
+
'warning',
|
|
363
|
+
INVALID_COLOR_CODE
|
|
119
364
|
)
|
|
120
365
|
);
|
|
121
366
|
return undefined;
|
package/src/cycle/parser.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
-
formatDgmoError,
|
|
7
6
|
makeDgmoError,
|
|
7
|
+
makeFail,
|
|
8
8
|
METADATA_DIAGNOSTIC_CODES,
|
|
9
9
|
pipeOperatorRemovedMessage,
|
|
10
10
|
} from '../diagnostics';
|
|
@@ -64,12 +64,7 @@ export function parseCycle(content: string): ParsedCycle {
|
|
|
64
64
|
let currentEdge: Writable<CycleEdge> | null = null;
|
|
65
65
|
// nodeBaseIndent tracking removed — indent-based nesting not used in cycle
|
|
66
66
|
|
|
67
|
-
const fail = (
|
|
68
|
-
const diag = makeDgmoError(line, message);
|
|
69
|
-
result.diagnostics.push(diag);
|
|
70
|
-
result.error = formatDgmoError(diag);
|
|
71
|
-
return result;
|
|
72
|
-
};
|
|
67
|
+
const fail = makeFail(result);
|
|
73
68
|
|
|
74
69
|
const warn = (
|
|
75
70
|
line: number,
|