summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/utils
diff options
context:
space:
mode:
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>
);
}