Monorepo for Aesthetic.Computer
aesthetic.computer
1// 🌈 KidLisp Syntax Highlighting for VS Code
2// Extracted from shared/kidlisp-syntax.mjs for VS Code extension bundling
3// Provides Monaco-parity highlighting including:
4// - Rainbow and zebra color words
5// - Animated timing token blinking (1s, 2s..., 3f, etc.)
6// - Color codes (c0, c1, etc.) and CSS color names
7// - $code, #code, !code references
8// - fade: expressions with gradient coloring
9// - RGB channel highlighting
10// - Rainbow nested parentheses
11
12// CSS Colors (subset for syntax highlighting)
13export const cssColors: Record<string, [number, number, number]> = {
14 aliceblue: [240, 248, 255],
15 aqua: [0, 255, 255],
16 aquamarine: [127, 255, 212],
17 black: [0, 0, 0],
18 blue: [0, 0, 255],
19 blueviolet: [138, 43, 226],
20 brown: [165, 42, 42],
21 chartreuse: [127, 255, 0],
22 coral: [255, 127, 80],
23 crimson: [220, 20, 60],
24 cyan: [0, 255, 255],
25 darkblue: [0, 0, 139],
26 darkcyan: [0, 139, 139],
27 darkgray: [169, 169, 169],
28 darkgreen: [0, 100, 0],
29 darkmagenta: [139, 0, 139],
30 darkorange: [255, 140, 0],
31 darkred: [139, 0, 0],
32 darkviolet: [148, 0, 211],
33 deeppink: [255, 20, 147],
34 deepskyblue: [0, 191, 255],
35 dodgerblue: [30, 144, 255],
36 fuchsia: [255, 0, 255],
37 gold: [255, 215, 0],
38 gray: [128, 128, 128],
39 green: [0, 128, 0],
40 greenyellow: [173, 255, 47],
41 grey: [128, 128, 128],
42 hotpink: [255, 105, 180],
43 indigo: [75, 0, 130],
44 khaki: [240, 230, 140],
45 lavender: [230, 230, 250],
46 lawngreen: [124, 252, 0],
47 lightblue: [173, 216, 230],
48 lightcoral: [240, 128, 128],
49 lightcyan: [224, 255, 255],
50 lightgray: [211, 211, 211],
51 lightgreen: [144, 238, 144],
52 lightpink: [255, 182, 193],
53 lightyellow: [255, 255, 224],
54 lime: [0, 255, 0],
55 limegreen: [50, 205, 50],
56 magenta: [255, 0, 255],
57 maroon: [128, 0, 0],
58 mediumaquamarine: [102, 205, 170],
59 mediumblue: [0, 0, 205],
60 mediumseagreen: [60, 179, 113],
61 mediumspringgreen: [0, 250, 154],
62 midnightblue: [25, 25, 112],
63 navy: [0, 0, 128],
64 olive: [128, 128, 0],
65 orange: [255, 165, 0],
66 orangered: [255, 69, 0],
67 orchid: [218, 112, 214],
68 palegreen: [152, 251, 152],
69 pink: [255, 192, 203],
70 plum: [221, 160, 221],
71 purple: [128, 0, 128],
72 red: [255, 0, 0],
73 royalblue: [65, 105, 225],
74 salmon: [250, 128, 114],
75 seagreen: [46, 139, 87],
76 silver: [192, 192, 192],
77 skyblue: [135, 206, 235],
78 slateblue: [106, 90, 205],
79 springgreen: [0, 255, 127],
80 steelblue: [70, 130, 180],
81 tan: [210, 180, 140],
82 teal: [0, 128, 128],
83 tomato: [255, 99, 71],
84 turquoise: [64, 224, 208],
85 violet: [238, 130, 238],
86 wheat: [245, 222, 179],
87 white: [255, 255, 255],
88 yellow: [255, 255, 0],
89 yellowgreen: [154, 205, 50],
90};
91
92// Static color map (c0, c1, etc.)
93export const staticColorMap: Record<number, [number, number, number]> = {
94 0: [255, 0, 0], // c0 = red
95 1: [255, 165, 0], // c1 = orange
96 2: [255, 255, 0], // c2 = yellow
97 3: [0, 255, 0], // c3 = lime
98 4: [0, 0, 255], // c4 = blue
99 5: [75, 0, 130], // c5 = indigo
100 6: [238, 130, 238], // c6 = violet
101 7: [255, 192, 203], // c7 = pink
102 8: [0, 255, 255], // c8 = cyan
103 9: [255, 0, 255], // c9 = magenta
104 10: [128, 128, 128], // c10 = gray
105 11: [0, 0, 0], // c11 = black
106 12: [255, 255, 255], // c12 = white
107 13: [165, 42, 42], // c13 = brown
108 14: [0, 128, 0], // c14 = darkgreen
109 15: [128, 0, 0], // c15 = maroon
110};
111
112// Rainbow colors for character-by-character coloring
113export const RAINBOW_COLORS = ['#ff0000', '#ff7f00', '#ffff00', '#00ff00', '#0000ff', '#4b0082', '#9400d3'];
114export const RAINBOW_NAMES = ['red', 'orange', 'yellow', 'lime', 'blue', 'purple', 'magenta'];
115
116// Zebra colors
117export const ZEBRA_COLORS = ['#000000', '#ffffff'];
118
119// Rainbow colors for nested parentheses
120export const PAREN_RAINBOW_COLORS: [number, number, number][] = [
121 [255, 100, 100], // 0: red-ish
122 [255, 180, 100], // 1: orange-ish
123 [255, 255, 100], // 2: yellow-ish
124 [100, 255, 100], // 3: green-ish
125 [100, 200, 255], // 4: cyan-ish
126 [150, 150, 255], // 5: blue-ish
127 [200, 100, 255], // 6: purple-ish
128 [255, 100, 200], // 7: magenta-ish
129];
130
131// Known function categories
132export const FUNCTIONS = {
133 graphics: ['wipe', 'ink', 'line', 'box', 'flood', 'circle', 'write', 'paste', 'stamp', 'point', 'poly', 'embed', 'grid', 'noise', 'qr', 'tri', 'plot', 'draw', 'shape'],
134 control: ['def', 'now', 'later', 'repeat', 'bunch', 'once', 'bake', 'jump', 'page', 'tap', 'drag', 'lift', 'key', 'act', 'leave'],
135 math: ['abs', 'floor', 'ceil', 'round', 'sqrt', 'pow', 'sin', 'cos', 'tan', 'min', 'max', 'random', 'wiggle', 'lerp', 'map', 'clamp', 'mod'],
136 logic: ['if', 'cond', 'and', 'or', 'not', 'eq', 'neq', 'lt', 'gt', 'lte', 'gte', 'choose'],
137 list: ['list', 'first', 'rest', 'nth', 'len', 'push', 'pop', 'concat', 'reverse', 'sort', 'filter', 'range'],
138 sound: ['sound', 'synth', 'tone', 'play', 'stop', 'bpm', 'beat', 'note', 'chord', 'melody', 'sample', 'overtone', 'mic', 'amplitude', 'speaker'],
139 text: ['write', 'text', 'font', 'print', 'say'],
140 mode: ['fill', 'outline', 'stroke', 'nofill', 'nostroke'],
141 transform: ['pan', 'unpan', 'zoom', 'spin', 'scroll', 'resolution', 'blur', 'contrast', 'mask', 'unmask'],
142 utility: ['debug', 'log', 'die', 'steal', 'putback', 'label'],
143};
144
145// All known functions flattened
146export const ALL_FUNCTIONS = Object.values(FUNCTIONS).flat();
147
148// Math operators
149export const MATH_OPERATORS = ['+', '-', '*', '/', '%', 'mod', '=', '>', '<', '>=', '<='];
150
151// Special forms and control flow keywords
152export const SPECIAL_FORMS = ['def', 'if', 'cond', 'later', 'once', 'lambda', 'let', 'do'];
153
154// Token types for semantic highlighting
155export const TOKEN_TYPES = {
156 COMMENT: 'comment',
157 STRING: 'string',
158 NUMBER: 'number',
159 TIMING: 'timing',
160 TIMING_CYCLE: 'timing-cycle',
161 COLOR_NAME: 'color-name',
162 COLOR_CODE: 'color-code',
163 PATTERN_CODE: 'pattern-code',
164 RAINBOW: 'rainbow',
165 ZEBRA: 'zebra',
166 FADE: 'fade',
167 EMBEDDED_CODE: 'embedded-code',
168 PAINTING_REF: 'painting-ref',
169 TAPE_REF: 'tape-ref',
170 FUNCTION: 'function',
171 SPECIAL_FORM: 'special-form',
172 MATH_OPERATOR: 'math-operator',
173 PAREN_OPEN: 'paren-open',
174 PAREN_CLOSE: 'paren-close',
175 IDENTIFIER: 'identifier',
176 RGB_CHANNEL: 'rgb-channel',
177 UNKNOWN: 'unknown',
178} as const;
179
180export type TokenType = typeof TOKEN_TYPES[keyof typeof TOKEN_TYPES];
181
182/**
183 * Tokenize KidLisp source code
184 */
185export function tokenize(input: string): string[] {
186 const regex = /\s*(;.*|[(),]|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\s()";',]+)/g;
187 const tokens: string[] = [];
188 let match;
189 while ((match = regex.exec(input)) !== null) {
190 const token = match[1];
191 tokens.push(token);
192 }
193 return tokens;
194}
195
196/**
197 * Tokenize with position information for editor decorations
198 */
199export function tokenizeWithPositions(input: string): Array<{value: string, pos: number, line: number, col: number}> {
200 const regex = /\s*(;.*|[(),]|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\s()";',]+)/g;
201 const tokens: Array<{value: string, pos: number, line: number, col: number}> = [];
202 let match;
203
204 // Pre-compute line starts for position mapping
205 const lineStarts: number[] = [0];
206 for (let i = 0; i < input.length; i++) {
207 if (input[i] === '\n') {
208 lineStarts.push(i + 1);
209 }
210 }
211
212 while ((match = regex.exec(input)) !== null) {
213 const token = match[1];
214 const tokenStart = match.index + match[0].indexOf(token);
215
216 // Find line and column
217 let line = 0;
218 for (let i = lineStarts.length - 1; i >= 0; i--) {
219 if (tokenStart >= lineStarts[i]) {
220 line = i;
221 break;
222 }
223 }
224 const col = tokenStart - lineStarts[line];
225
226 tokens.push({ value: token, pos: tokenStart, line, col });
227 }
228 return tokens;
229}
230
231/**
232 * Get the token type for a given token
233 */
234export function getTokenType(token: string, tokens: string[], index: number): TokenType {
235 // Comments
236 if (token.startsWith(';')) return TOKEN_TYPES.COMMENT;
237
238 // Strings
239 if ((token.startsWith('"') && token.endsWith('"')) ||
240 (token.startsWith("'") && token.endsWith("'"))) {
241 return TOKEN_TYPES.STRING;
242 }
243
244 // Timing patterns - cycle (3s..., 5f...)
245 if (/^\d*\.?\d+[sf]\.\.\.?$/.test(token)) return TOKEN_TYPES.TIMING_CYCLE;
246
247 // Timing patterns - delay (1.25s, 0.5s, 3f, 1s!)
248 if (/^\d*\.?\d+[sf]!?$/.test(token)) return TOKEN_TYPES.TIMING;
249
250 // Rainbow keyword
251 if (token === 'rainbow') return TOKEN_TYPES.RAINBOW;
252
253 // Zebra keyword
254 if (token === 'zebra') return TOKEN_TYPES.ZEBRA;
255
256 // Fade expressions
257 if (token.startsWith('fade:')) return TOKEN_TYPES.FADE;
258
259 // $code references
260 if (token.startsWith('$') && token.length > 1 && /^[$][0-9A-Za-z]+$/.test(token)) {
261 return TOKEN_TYPES.EMBEDDED_CODE;
262 }
263
264 // #code painting references (not hex colors)
265 if (token.startsWith('#') && token.length > 1 && /^#[0-9A-Za-z]{1,8}$/.test(token)) {
266 const codepart = token.substring(1);
267 const isHexColor = /^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{4}$|^[0-9A-Fa-f]{6}$|^[0-9A-Fa-f]{8}$/.test(codepart);
268 if (!isHexColor) return TOKEN_TYPES.PAINTING_REF;
269 }
270
271 // !code tape references
272 if (token.startsWith('!') && token.length > 1 && /^![0-9A-Za-z]+$/.test(token)) {
273 return TOKEN_TYPES.TAPE_REF;
274 }
275
276 // Color codes (c0, c1, etc.)
277 if (/^c\d+$/i.test(token)) return TOKEN_TYPES.COLOR_CODE;
278
279 // Pattern codes (p0, p1, etc.)
280 if (/^p\d+$/i.test(token)) return TOKEN_TYPES.PATTERN_CODE;
281
282 // CSS color names
283 if (cssColors[token.toLowerCase()]) return TOKEN_TYPES.COLOR_NAME;
284
285 // Parentheses
286 if (token === '(') return TOKEN_TYPES.PAREN_OPEN;
287 if (token === ')') return TOKEN_TYPES.PAREN_CLOSE;
288
289 // Math operators
290 if (MATH_OPERATORS.includes(token)) return TOKEN_TYPES.MATH_OPERATOR;
291
292 // Check for function call (first token after opening paren)
293 if (index > 0 && tokens[index - 1] === '(') {
294 if (SPECIAL_FORMS.includes(token)) return TOKEN_TYPES.SPECIAL_FORM;
295 if (ALL_FUNCTIONS.includes(token)) return TOKEN_TYPES.FUNCTION;
296 return TOKEN_TYPES.FUNCTION;
297 }
298
299 // Numbers (check for RGB channel context)
300 if (/^-?\d+(\.\d+)?$/.test(token)) {
301 const isNumeric = (t: string | undefined) => t && /^-?\d+(\.\d+)?$/.test(t);
302 const prevToken = index > 0 ? tokens[index - 1] : undefined;
303 const nextToken = index < tokens.length - 1 ? tokens[index + 1] : undefined;
304 const next2Token = index < tokens.length - 2 ? tokens[index + 2] : undefined;
305 const prev2Token = index > 1 ? tokens[index - 2] : undefined;
306
307 if ((isNumeric(nextToken) && isNumeric(next2Token)) ||
308 (isNumeric(prevToken) && isNumeric(nextToken)) ||
309 (isNumeric(prev2Token) && isNumeric(prevToken))) {
310 return TOKEN_TYPES.RGB_CHANNEL;
311 }
312 return TOKEN_TYPES.NUMBER;
313 }
314
315 // Special forms at any position
316 if (SPECIAL_FORMS.includes(token)) return TOKEN_TYPES.SPECIAL_FORM;
317
318 // Known functions at any position
319 if (ALL_FUNCTIONS.includes(token)) return TOKEN_TYPES.FUNCTION;
320
321 return TOKEN_TYPES.IDENTIFIER;
322}
323
324export interface ColorOptions {
325 isEditMode?: boolean;
326 lightMode?: boolean;
327}
328
329/**
330 * Get the color for a token (returns CSS color string or special markers)
331 */
332export function getTokenColor(token: string, tokens: string[], index: number, options: ColorOptions = {}): string {
333 const { isEditMode = false } = options;
334 const tokenType = getTokenType(token, tokens, index);
335
336 switch (tokenType) {
337 case TOKEN_TYPES.COMMENT:
338 return 'gray';
339
340 case TOKEN_TYPES.STRING:
341 return 'yellow';
342
343 case TOKEN_TYPES.TIMING:
344 case TOKEN_TYPES.TIMING_CYCLE:
345 if (isEditMode) {
346 const match = token.match(/^(\d*\.?\d+)([sf])/);
347 if (match) {
348 const value = parseFloat(match[1]);
349 const unit = match[2];
350 const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
351 const blinkDuration = 200;
352
353 let duration: number;
354 if (unit === 's') {
355 duration = value * 1000;
356 } else {
357 duration = (value / 60) * 1000;
358 }
359
360 const cyclePhase = now % duration;
361
362 if (cyclePhase < blinkDuration) {
363 return tokenType === TOKEN_TYPES.TIMING_CYCLE ? 'lime' : 'red';
364 }
365 }
366 }
367 return 'yellow';
368
369 case TOKEN_TYPES.RAINBOW:
370 return 'RAINBOW';
371
372 case TOKEN_TYPES.ZEBRA:
373 return 'ZEBRA';
374
375 case TOKEN_TYPES.FADE:
376 return 'mediumseagreen';
377
378 case TOKEN_TYPES.EMBEDDED_CODE:
379 return 'COMPOUND:limegreen:lime';
380
381 case TOKEN_TYPES.PAINTING_REF:
382 return 'COMPOUND:magenta:orange';
383
384 case TOKEN_TYPES.TAPE_REF:
385 return 'COMPOUND:cyan:teal';
386
387 case TOKEN_TYPES.COLOR_CODE: {
388 const colorIndex = parseInt(token.substring(1));
389 if (staticColorMap[colorIndex]) {
390 const [r, g, b] = staticColorMap[colorIndex];
391 return `${r},${g},${b}`;
392 }
393 return 'orange';
394 }
395
396 case TOKEN_TYPES.PATTERN_CODE: {
397 const patternIndex = parseInt(token.substring(1));
398 if (patternIndex === 0) return 'RAINBOW';
399 if (patternIndex === 1) return 'ZEBRA';
400 return 'orange';
401 }
402
403 case TOKEN_TYPES.COLOR_NAME: {
404 const colorValue = cssColors[token.toLowerCase()];
405 if (colorValue) {
406 return `${colorValue[0]},${colorValue[1]},${colorValue[2]}`;
407 }
408 return 'orange';
409 }
410
411 case TOKEN_TYPES.PAREN_OPEN:
412 case TOKEN_TYPES.PAREN_CLOSE:
413 return getParenColor(tokens, index);
414
415 case TOKEN_TYPES.MATH_OPERATOR:
416 return 'lime';
417
418 case TOKEN_TYPES.SPECIAL_FORM:
419 return token === 'repeat' ? 'magenta' : 'purple';
420
421 case TOKEN_TYPES.FUNCTION:
422 return 'cyan';
423
424 case TOKEN_TYPES.RGB_CHANNEL: {
425 const value = Math.max(0, Math.min(255, parseFloat(token)));
426 const isNumeric = (t: string | undefined) => t && /^-?\d+(\.\d+)?$/.test(t);
427 const prevToken = index > 0 ? tokens[index - 1] : undefined;
428 const nextToken = index < tokens.length - 1 ? tokens[index + 1] : undefined;
429 const next2Token = index < tokens.length - 2 ? tokens[index + 2] : undefined;
430 const prev2Token = index > 1 ? tokens[index - 2] : undefined;
431
432 if (isNumeric(nextToken) && isNumeric(next2Token)) {
433 return `${value},0,0`;
434 } else if (isNumeric(prevToken) && isNumeric(nextToken)) {
435 return `0,${value},0`;
436 } else if (isNumeric(prev2Token) && isNumeric(prevToken)) {
437 const greenComponent = Math.round(value * 0.75);
438 return `0,${greenComponent},${value}`;
439 }
440 return 'pink';
441 }
442
443 case TOKEN_TYPES.NUMBER:
444 return 'pink';
445
446 case TOKEN_TYPES.IDENTIFIER:
447 default:
448 return 'orange';
449 }
450}
451
452/**
453 * Get rainbow color for parentheses based on nesting depth
454 */
455export function getParenColor(tokens: string[], index: number): string {
456 let depth = 0;
457 for (let i = 0; i < index; i++) {
458 if (tokens[i] === '(') depth++;
459 else if (tokens[i] === ')') depth--;
460 }
461
462 if (tokens[index] === ')') {
463 depth = Math.max(0, depth - 1);
464 }
465
466 const colorIndex = depth % PAREN_RAINBOW_COLORS.length;
467 const [r, g, b] = PAREN_RAINBOW_COLORS[colorIndex];
468 return `${r},${g},${b}`;
469}
470
471/**
472 * Convert color string to hex format for VS Code decorations
473 */
474export function colorToHex(color: string): string {
475 // Already hex
476 if (color.startsWith('#')) return color;
477
478 // Named color - look up or use as-is
479 if (!color.includes(',')) {
480 const mapped = cssColors[color.toLowerCase()];
481 if (mapped) {
482 const [r, g, b] = mapped;
483 return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
484 }
485 return color; // Return named color for CSS
486 }
487
488 // RGB string like "255,128,64"
489 const parts = color.split(',').map(n => parseInt(n.trim()));
490 if (parts.length >= 3) {
491 const [r, g, b] = parts;
492 return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
493 }
494
495 return color;
496}
497
498/**
499 * High contrast color map for light mode
500 */
501export const LIGHT_MODE_COLOR_MAP: Record<string, string> = {
502 'yellow': '#cc9900',
503 'cyan': '#0099cc',
504 'lime': '#00aa00',
505 'pink': '#cc0066',
506 'orange': '#cc6600',
507 'magenta': '#cc00cc',
508 'gray': '#666666',
509 'lightgray': '#999999',
510 'white': '#cccccc',
511 'mediumseagreen': '#2e8b57',
512};
513
514/**
515 * Get high-contrast version of a color for light mode
516 */
517export function getLightModeColor(color: string): string {
518 return LIGHT_MODE_COLOR_MAP[color.toLowerCase()] || color;
519}