diff options
Diffstat (limited to 'packages/cli/src/ui/themes')
| -rw-r--r-- | packages/cli/src/ui/themes/theme-manager.ts | 29 | ||||
| -rw-r--r-- | packages/cli/src/ui/themes/theme.ts | 278 | ||||
| -rw-r--r-- | packages/cli/src/ui/themes/vs2015.ts | 144 |
3 files changed, 451 insertions, 0 deletions
diff --git a/packages/cli/src/ui/themes/theme-manager.ts b/packages/cli/src/ui/themes/theme-manager.ts new file mode 100644 index 00000000..e083575c --- /dev/null +++ b/packages/cli/src/ui/themes/theme-manager.ts @@ -0,0 +1,29 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { VS2015 } from './vs2015.js'; +import { Theme } from './theme.js'; + +class ThemeManager { + private static readonly DEFAULT_THEME: Theme = VS2015; + private readonly availableThemes: Theme[]; + private activeTheme: Theme; + + constructor() { + this.availableThemes = [VS2015]; + this.activeTheme = ThemeManager.DEFAULT_THEME; + } + + /** + * Returns the currently active theme object. + */ + getActiveTheme(): Theme { + return this.activeTheme; + } +} + +// Export an instance of the ThemeManager +export const themeManager = new ThemeManager(); diff --git a/packages/cli/src/ui/themes/theme.ts b/packages/cli/src/ui/themes/theme.ts new file mode 100644 index 00000000..f7bd1cd0 --- /dev/null +++ b/packages/cli/src/ui/themes/theme.ts @@ -0,0 +1,278 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { CSSProperties } from 'react'; + +export class Theme { + /** + * The user-facing name of the theme. + */ + readonly name: string; + + /** + * The default foreground color for text when no specific highlight rule applies. + * This is an Ink-compatible color string (hex or name). + */ + readonly defaultColor: string; + + /** + * Stores the mapping from highlight.js class names (e.g., 'hljs-keyword') + * to Ink-compatible color strings (hex or name). + */ + protected readonly _colorMap: Readonly<Record<string, string>>; + + // --- Static Helper Data --- + + // Mapping from common CSS color names (lowercase) to hex codes (lowercase) + // Excludes names directly supported by Ink + private static readonly cssNameToHexMap: Readonly<Record<string, string>> = { + aliceblue: '#f0f8ff', + antiquewhite: '#faebd7', + aqua: '#00ffff', + aquamarine: '#7fffd4', + azure: '#f0ffff', + beige: '#f5f5dc', + bisque: '#ffe4c4', + blanchedalmond: '#ffebcd', + blueviolet: '#8a2be2', + brown: '#a52a2a', + burlywood: '#deb887', + cadetblue: '#5f9ea0', + chartreuse: '#7fff00', + chocolate: '#d2691e', + coral: '#ff7f50', + cornflowerblue: '#6495ed', + cornsilk: '#fff8dc', + crimson: '#dc143c', + darkblue: '#00008b', + darkcyan: '#008b8b', + darkgoldenrod: '#b8860b', + darkgray: '#a9a9a9', + darkgrey: '#a9a9a9', + darkgreen: '#006400', + darkkhaki: '#bdb76b', + darkmagenta: '#8b008b', + darkolivegreen: '#556b2f', + darkorange: '#ff8c00', + darkorchid: '#9932cc', + darkred: '#8b0000', + darksalmon: '#e9967a', + darkseagreen: '#8fbc8f', + darkslateblue: '#483d8b', + darkslategray: '#2f4f4f', + darkslategrey: '#2f4f4f', + darkturquoise: '#00ced1', + darkviolet: '#9400d3', + deeppink: '#ff1493', + deepskyblue: '#00bfff', + dimgray: '#696969', + dimgrey: '#696969', + dodgerblue: '#1e90ff', + firebrick: '#b22222', + floralwhite: '#fffaf0', + forestgreen: '#228b22', + fuchsia: '#ff00ff', + gainsboro: '#dcdcdc', + ghostwhite: '#f8f8ff', + gold: '#ffd700', + goldenrod: '#daa520', + greenyellow: '#adff2f', + honeydew: '#f0fff0', + hotpink: '#ff69b4', + indianred: '#cd5c5c', + indigo: '#4b0082', + ivory: '#fffff0', + khaki: '#f0e68c', + lavender: '#e6e6fa', + lavenderblush: '#fff0f5', + lawngreen: '#7cfc00', + lemonchiffon: '#fffacd', + lightblue: '#add8e6', + lightcoral: '#f08080', + lightcyan: '#e0ffff', + lightgoldenrodyellow: '#fafad2', + lightgray: '#d3d3d3', + lightgrey: '#d3d3d3', + lightgreen: '#90ee90', + lightpink: '#ffb6c1', + lightsalmon: '#ffa07a', + lightseagreen: '#20b2aa', + lightskyblue: '#87cefa', + lightslategray: '#778899', + lightslategrey: '#778899', + lightsteelblue: '#b0c4de', + lightyellow: '#ffffe0', + lime: '#00ff00', + limegreen: '#32cd32', + linen: '#faf0e6', + maroon: '#800000', + mediumaquamarine: '#66cdaa', + mediumblue: '#0000cd', + mediumorchid: '#ba55d3', + mediumpurple: '#9370db', + mediumseagreen: '#3cb371', + mediumslateblue: '#7b68ee', + mediumspringgreen: '#00fa9a', + mediumturquoise: '#48d1cc', + mediumvioletred: '#c71585', + midnightblue: '#191970', + mintcream: '#f5fffa', + mistyrose: '#ffe4e1', + moccasin: '#ffe4b5', + navajowhite: '#ffdead', + navy: '#000080', + oldlace: '#fdf5e6', + olive: '#808000', + olivedrab: '#6b8e23', + orange: '#ffa500', + orangered: '#ff4500', + orchid: '#da70d6', + palegoldenrod: '#eee8aa', + palegreen: '#98fb98', + paleturquoise: '#afeeee', + palevioletred: '#db7093', + papayawhip: '#ffefd5', + peachpuff: '#ffdab9', + peru: '#cd853f', + pink: '#ffc0cb', + plum: '#dda0dd', + powderblue: '#b0e0e6', + purple: '#800080', + rebeccapurple: '#663399', + rosybrown: '#bc8f8f', + royalblue: '#4169e1', + saddlebrown: '#8b4513', + salmon: '#fa8072', + sandybrown: '#f4a460', + seagreen: '#2e8b57', + seashell: '#fff5ee', + sienna: '#a0522d', + silver: '#c0c0c0', + skyblue: '#87ceeb', + slateblue: '#6a5acd', + slategray: '#708090', + slategrey: '#708090', + snow: '#fffafa', + springgreen: '#00ff7f', + steelblue: '#4682b4', + tan: '#d2b48c', + teal: '#008080', + thistle: '#d8bfd8', + tomato: '#ff6347', + turquoise: '#40e0d0', + violet: '#ee82ee', + wheat: '#f5deb3', + whitesmoke: '#f5f5f5', + yellowgreen: '#9acd32', + }; + + // Define the set of Ink's named colors for quick lookup + private static readonly inkSupportedNames = new Set([ + 'black', + 'red', + 'green', + 'yellow', + 'blue', + 'cyan', + 'magenta', + 'white', + 'gray', + 'grey', + 'blackbright', + 'redbright', + 'greenbright', + 'yellowbright', + 'bluebright', + 'cyanbright', + 'magentabright', + 'whitebright', + ]); + + /** + * Creates a new Theme instance. + * @param name The name of the theme. + * @param rawMappings The raw CSSProperties mappings from a react-syntax-highlighter theme object. + */ + constructor(name: string, rawMappings: Record<string, CSSProperties>) { + this.name = name; + this._colorMap = Object.freeze(this._buildColorMap(rawMappings)); // Build and freeze the map + + // Determine the default foreground color + const rawDefaultColor = rawMappings['hljs']?.color; + this.defaultColor = + (rawDefaultColor ? Theme._resolveColor(rawDefaultColor) : undefined) ?? + ''; // Default to empty string if not found or resolvable + } + + /** + * Gets the Ink-compatible color string for a given highlight.js class name. + * @param hljsClass The highlight.js class name (e.g., 'hljs-keyword', 'hljs-string'). + * @returns The corresponding Ink color string (hex or name) if it exists. + */ + getInkColor(hljsClass: string): string | undefined { + return this._colorMap[hljsClass]; + } + + /** + * Resolves a CSS color value (name or hex) into an Ink-compatible color string. + * @param colorValue The raw color string (e.g., 'blue', '#ff0000', 'darkkhaki'). + * @returns An Ink-compatible color string (hex or name), or undefined if not resolvable. + */ + private static _resolveColor(colorValue: string): string | undefined { + const lowerColor = colorValue.toLowerCase(); + + // 1. Check if it's already a hex code + if (lowerColor.startsWith('#')) { + return lowerColor; // Use hex directly + } + // 2. Check if it's an Ink supported name (lowercase) + else if (Theme.inkSupportedNames.has(lowerColor)) { + return lowerColor; // Use Ink name directly + } + // 3. Check if it's a known CSS name we can map to hex + else if (Theme.cssNameToHexMap[lowerColor]) { + return Theme.cssNameToHexMap[lowerColor]; // Use mapped hex + } + + // 4. Could not resolve + console.warn( + `[Theme] Could not resolve color "${colorValue}" to an Ink-compatible format.`, + ); + return undefined; + } + + /** + * Builds the internal map from highlight.js class names to Ink-compatible color strings. + * This method is protected and primarily intended for use by the constructor. + * @param hljsTheme The raw CSSProperties mappings from a react-syntax-highlighter theme object. + * @returns An Ink-compatible theme map (Record<string, string>). + */ + protected _buildColorMap( + hljsTheme: Record<string, CSSProperties>, + ): Record<string, string> { + const inkTheme: Record<string, string> = {}; + for (const key in hljsTheme) { + // Ensure the key starts with 'hljs-' or is 'hljs' for the base style + if (!key.startsWith('hljs-') && key !== 'hljs') { + continue; // Skip keys not related to highlighting classes + } + + const style = hljsTheme[key]; + if (style?.color) { + const resolvedColor = Theme._resolveColor(style.color); + if (resolvedColor !== undefined) { + // Use the original key from the hljsTheme (e.g., 'hljs-keyword') + inkTheme[key] = resolvedColor; + } + // If color is not resolvable, it's omitted from the map, + // allowing fallback to the default foreground color. + } + // We currently only care about the 'color' property for Ink rendering. + // Other properties like background, fontStyle, etc., are ignored. + } + return inkTheme; + } +} diff --git a/packages/cli/src/ui/themes/vs2015.ts b/packages/cli/src/ui/themes/vs2015.ts new file mode 100644 index 00000000..fa4ec1ee --- /dev/null +++ b/packages/cli/src/ui/themes/vs2015.ts @@ -0,0 +1,144 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Theme } from './theme.js'; + +export const VS2015: Theme = new Theme('VS2015', { + hljs: { + display: 'block', + overflowX: 'auto', + padding: '0.5em', + background: '#1E1E1E', + color: '#DCDCDC', + }, + 'hljs-keyword': { + color: '#569CD6', + }, + 'hljs-literal': { + color: '#569CD6', + }, + 'hljs-symbol': { + color: '#569CD6', + }, + 'hljs-name': { + color: '#569CD6', + }, + 'hljs-link': { + color: '#569CD6', + textDecoration: 'underline', + }, + 'hljs-built_in': { + color: '#4EC9B0', + }, + 'hljs-type': { + color: '#4EC9B0', + }, + 'hljs-number': { + color: '#B8D7A3', + }, + 'hljs-class': { + color: '#B8D7A3', + }, + 'hljs-string': { + color: '#D69D85', + }, + 'hljs-meta-string': { + color: '#D69D85', + }, + 'hljs-regexp': { + color: '#9A5334', + }, + 'hljs-template-tag': { + color: '#9A5334', + }, + 'hljs-subst': { + color: '#DCDCDC', + }, + 'hljs-function': { + color: '#DCDCDC', + }, + 'hljs-title': { + color: '#DCDCDC', + }, + 'hljs-params': { + color: '#DCDCDC', + }, + 'hljs-formula': { + color: '#DCDCDC', + }, + 'hljs-comment': { + color: '#57A64A', + fontStyle: 'italic', + }, + 'hljs-quote': { + color: '#57A64A', + fontStyle: 'italic', + }, + 'hljs-doctag': { + color: '#608B4E', + }, + 'hljs-meta': { + color: '#9B9B9B', + }, + 'hljs-meta-keyword': { + color: '#9B9B9B', + }, + 'hljs-tag': { + color: '#9B9B9B', + }, + 'hljs-variable': { + color: '#BD63C5', + }, + 'hljs-template-variable': { + color: '#BD63C5', + }, + 'hljs-attr': { + color: '#9CDCFE', + }, + 'hljs-attribute': { + color: '#9CDCFE', + }, + 'hljs-builtin-name': { + color: '#9CDCFE', + }, + 'hljs-section': { + color: 'gold', + }, + 'hljs-emphasis': { + fontStyle: 'italic', + }, + 'hljs-strong': { + fontWeight: 'bold', + }, + 'hljs-bullet': { + color: '#D7BA7D', + }, + 'hljs-selector-tag': { + color: '#D7BA7D', + }, + 'hljs-selector-id': { + color: '#D7BA7D', + }, + 'hljs-selector-class': { + color: '#D7BA7D', + }, + 'hljs-selector-attr': { + color: '#D7BA7D', + }, + 'hljs-selector-pseudo': { + color: '#D7BA7D', + }, + 'hljs-addition': { + backgroundColor: '#144212', + display: 'inline-block', + width: '100%', + }, + 'hljs-deletion': { + backgroundColor: '#600', + display: 'inline-block', + width: '100%', + }, +}); |
