summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/utils
diff options
context:
space:
mode:
authorTaylor Mullen <[email protected]>2025-04-22 18:37:58 -0700
committerN. Taylor Mullen <[email protected]>2025-04-22 18:57:27 -0700
commite163e024996ff8bb1152284322831c4f35696801 (patch)
treee8799f87ba6b234136e56a1581ddd106962aa196 /packages/cli/src/ui/utils
parentffe368afed853f43c9ab8851c1edc86160db8486 (diff)
Colorize code blocks.
- This changeset uses lowlight.js to parse the code in codeblocks to derive an AST, it then translates that into CSS themes that are widely known via highlight.js (things that GitHub use), finally I translate those css.color attributes into Ink colors and effectivel do <Text color={the color}>the text</Text>. - To do this I needed to build color mappings from css -> Ink - I introduced a new `Theme` type that will be used to represent many different color themes. It also enabled the color mappings to be seamless. - Added a theme manager that only has one theme for now (VS2015). The theme works very well with our colorization. - Some other bits was removal of borders around our codeblocks since they now have richer rendering. - Most complex bits of code in this PR is in the `CodeColorizer.tsx` Fixes https://b.corp.google.com/issues/412433479
Diffstat (limited to 'packages/cli/src/ui/utils')
-rw-r--r--packages/cli/src/ui/utils/CodeColorizer.tsx117
-rw-r--r--packages/cli/src/ui/utils/MarkdownRenderer.tsx20
2 files changed, 123 insertions, 14 deletions
diff --git a/packages/cli/src/ui/utils/CodeColorizer.tsx b/packages/cli/src/ui/utils/CodeColorizer.tsx
new file mode 100644
index 00000000..1916ff50
--- /dev/null
+++ b/packages/cli/src/ui/utils/CodeColorizer.tsx
@@ -0,0 +1,117 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Text } from 'ink';
+import { common, createLowlight } from 'lowlight';
+import type {
+ Root,
+ Element,
+ Text as HastText,
+ ElementContent,
+ RootContent,
+} from 'hast';
+import { themeManager } from '../themes/theme-manager.js';
+import { Theme } from '../themes/theme.js';
+
+// Configure themeing and parsing utilities.
+const lowlight = createLowlight(common);
+
+function renderHastNode(
+ node: Root | Element | HastText | RootContent,
+ theme: Theme,
+ inheritedColor: string | undefined,
+): React.ReactNode {
+ if (node.type === 'text') {
+ // Use the color passed down from parent element, if any
+ return <Text color={inheritedColor}>{node.value}</Text>;
+ }
+
+ // Handle Element Nodes: Determine color and pass it down, don't wrap
+ if (node.type === 'element') {
+ const nodeClasses: string[] =
+ (node.properties?.className as string[]) || [];
+ let elementColor: string | undefined = undefined;
+
+ // Find color defined specifically for this element's class
+ for (let i = nodeClasses.length - 1; i >= 0; i--) {
+ const color = theme.getInkColor(nodeClasses[i]);
+ if (color) {
+ elementColor = color;
+ break;
+ }
+ }
+
+ // Determine the color to pass down: Use this element's specific color
+ // if found, otherwise, continue passing down the already inherited color.
+ const colorToPassDown = elementColor || inheritedColor;
+
+ // Recursively render children, passing the determined color down
+ // Ensure child type matches expected HAST structure (ElementContent is common)
+ const children = node.children?.map(
+ (child: ElementContent, index: number) => (
+ <React.Fragment key={index}>
+ {renderHastNode(child, theme, colorToPassDown)}
+ </React.Fragment>
+ ),
+ );
+
+ // Element nodes now only group children; color is applied by Text nodes.
+ // Use a React Fragment to avoid adding unnecessary elements.
+ return <React.Fragment>{children}</React.Fragment>;
+ }
+
+ // Handle Root Node: Start recursion with initial inherited color
+ if (node.type === 'root') {
+ // Pass down the initial inheritedColor (likely undefined from the top call)
+ // Ensure child type matches expected HAST structure (RootContent is common)
+ return node.children?.map((child: RootContent, index: number) => (
+ <React.Fragment key={index}>
+ {renderHastNode(child, theme, inheritedColor)}
+ </React.Fragment>
+ ));
+ }
+
+ // Handle unknown or unsupported node types
+ return null;
+}
+
+/**
+ * Renders syntax-highlighted code for Ink applications using a selected theme.
+ *
+ * @param code The code string to highlight.
+ * @param language The language identifier (e.g., 'javascript', 'css', 'html')
+ * @returns A React.ReactNode containing Ink <Text> elements for the highlighted code.
+ */
+export function colorizeCode(
+ code: string,
+ language: string | null,
+): React.ReactNode {
+ const codeToHighlight = code.replace(/\n$/, '');
+ const activeTheme = themeManager.getActiveTheme();
+
+ try {
+ const hastTree =
+ !language || !lowlight.registered(language)
+ ? lowlight.highlightAuto(codeToHighlight)
+ : lowlight.highlight(language, codeToHighlight);
+
+ // Render the HAST tree using the adapted theme
+ // Apply the theme's default foreground color to the top-level Text element
+ return (
+ <Text color={activeTheme.defaultColor}>
+ {renderHastNode(hastTree, activeTheme, undefined)}
+ </Text>
+ );
+ } catch (error) {
+ console.error(
+ `[colorizeCode] Error highlighting code for language "${language}":`,
+ error,
+ );
+ // Fallback to plain text with default color on error
+ return <Text color={activeTheme.defaultColor}>{codeToHighlight}</Text>;
+ }
+}
diff --git a/packages/cli/src/ui/utils/MarkdownRenderer.tsx b/packages/cli/src/ui/utils/MarkdownRenderer.tsx
index 680d7407..c9053728 100644
--- a/packages/cli/src/ui/utils/MarkdownRenderer.tsx
+++ b/packages/cli/src/ui/utils/MarkdownRenderer.tsx
@@ -7,6 +7,7 @@
import React from 'react';
import { Text, Box } from 'ink';
import { Colors } from '../colors.js';
+import { colorizeCode } from './CodeColorizer.js';
/**
* A utility class to render a subset of Markdown into Ink components.
@@ -155,21 +156,12 @@ export class MarkdownRenderer {
content: string[],
lang: string | null,
): React.ReactNode {
- // Basic styling for code block
+ const fullContent = content.join('\n');
+ const colorizedCode = colorizeCode(fullContent, lang);
+
return (
- <Box
- key={key}
- borderStyle="round"
- borderColor={Colors.SubtleComment}
- borderLeft={false}
- borderRight={false}
- flexDirection="column"
- >
- {lang && <Text dimColor>{lang}</Text>}
- {/* Render each line preserving whitespace (within Text component) */}
- {content.map((line, idx) => (
- <Text key={idx}>{line}</Text>
- ))}
+ <Box key={key} flexDirection="column" padding={1}>
+ {colorizedCode}
</Box>
);
}