Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 519 lines 17 kB view raw
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}