@boba-cli/code 0.1.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -0
- package/dist/index.cjs +245 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +117 -0
- package/dist/index.d.ts +117 -0
- package/dist/index.js +240 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @boba-cli/code
|
|
2
|
+
|
|
3
|
+
Syntax-highlighted code viewer component for Boba terminal UIs.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Syntax highlighting with configurable themes
|
|
10
|
+
- Scrollable viewport for long code files
|
|
11
|
+
- Supports multiple programming languages via file extension detection
|
|
12
|
+
- Async file loading
|
|
13
|
+
- Keyboard navigation when active
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add @boba-cli/code
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { CodeModel } from '@boba-cli/code'
|
|
25
|
+
import { Program } from '@boba-cli/tea'
|
|
26
|
+
|
|
27
|
+
const codeModel = CodeModel.new({ active: true })
|
|
28
|
+
|
|
29
|
+
// In init
|
|
30
|
+
const cmd = codeModel.setFileName('example.ts')
|
|
31
|
+
|
|
32
|
+
// In update - handle window resize
|
|
33
|
+
if (msg instanceof WindowSizeMsg) {
|
|
34
|
+
codeModel = codeModel.setSize(msg.width, msg.height)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// In view
|
|
38
|
+
return codeModel.view()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## API
|
|
42
|
+
|
|
43
|
+
See [API documentation](../../docs/code.codemodel.md) for details.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chapstick = require('@boba-cli/chapstick');
|
|
4
|
+
var filesystem = require('@boba-cli/filesystem');
|
|
5
|
+
var viewport = require('@boba-cli/viewport');
|
|
6
|
+
var shiki = require('shiki');
|
|
7
|
+
|
|
8
|
+
// src/model.ts
|
|
9
|
+
|
|
10
|
+
// src/messages.ts
|
|
11
|
+
var SyntaxMsg = class {
|
|
12
|
+
constructor(content) {
|
|
13
|
+
this.content = content;
|
|
14
|
+
}
|
|
15
|
+
_tag = "code-syntax";
|
|
16
|
+
};
|
|
17
|
+
var ErrorMsg = class {
|
|
18
|
+
constructor(error) {
|
|
19
|
+
this.error = error;
|
|
20
|
+
}
|
|
21
|
+
_tag = "code-error";
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// src/model.ts
|
|
25
|
+
async function highlight(content, extension, theme = "dracula") {
|
|
26
|
+
try {
|
|
27
|
+
const highlighter = await shiki.getHighlighter({
|
|
28
|
+
themes: [theme],
|
|
29
|
+
langs: []
|
|
30
|
+
});
|
|
31
|
+
const ext = extension.startsWith(".") ? extension.slice(1) : extension;
|
|
32
|
+
let lang = ext || "text";
|
|
33
|
+
try {
|
|
34
|
+
await highlighter.loadLanguage(lang);
|
|
35
|
+
} catch {
|
|
36
|
+
lang = "text";
|
|
37
|
+
try {
|
|
38
|
+
await highlighter.loadLanguage("text");
|
|
39
|
+
} catch {
|
|
40
|
+
return content;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const tokens = highlighter.codeToTokens(content, {
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
|
|
45
|
+
lang,
|
|
46
|
+
theme
|
|
47
|
+
});
|
|
48
|
+
let result = "";
|
|
49
|
+
for (const line of tokens.tokens) {
|
|
50
|
+
for (const token of line) {
|
|
51
|
+
if (token.color) {
|
|
52
|
+
result += `\x1B[38;2;${hexToRgb(token.color)}m${token.content}\x1B[0m`;
|
|
53
|
+
} else {
|
|
54
|
+
result += token.content;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
result += "\n";
|
|
58
|
+
}
|
|
59
|
+
return result.trimEnd();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
62
|
+
throw new Error(`Syntax highlighting failed: ${errorMessage}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function hexToRgb(hex) {
|
|
66
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
67
|
+
if (!result) return "255;255;255";
|
|
68
|
+
return `${parseInt(result[1], 16)};${parseInt(result[2], 16)};${parseInt(result[3], 16)}`;
|
|
69
|
+
}
|
|
70
|
+
function readFileContentCmd(fsAdapter, pathAdapter, fileName, syntaxTheme) {
|
|
71
|
+
return async () => {
|
|
72
|
+
try {
|
|
73
|
+
const content = await filesystem.readFileContent(fsAdapter, fileName);
|
|
74
|
+
const extension = pathAdapter.extname(fileName);
|
|
75
|
+
const highlightedContent = await highlight(
|
|
76
|
+
content,
|
|
77
|
+
extension,
|
|
78
|
+
syntaxTheme
|
|
79
|
+
);
|
|
80
|
+
return new SyntaxMsg(highlightedContent);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return new ErrorMsg(
|
|
83
|
+
error instanceof Error ? error : new Error(String(error))
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
var CodeModel = class _CodeModel {
|
|
89
|
+
viewport;
|
|
90
|
+
active;
|
|
91
|
+
filename;
|
|
92
|
+
highlightedContent;
|
|
93
|
+
syntaxTheme;
|
|
94
|
+
filesystem;
|
|
95
|
+
path;
|
|
96
|
+
constructor(options) {
|
|
97
|
+
this.viewport = options.viewport;
|
|
98
|
+
this.active = options.active;
|
|
99
|
+
this.filename = options.filename;
|
|
100
|
+
this.highlightedContent = options.highlightedContent;
|
|
101
|
+
this.syntaxTheme = options.syntaxTheme;
|
|
102
|
+
this.filesystem = options.filesystem;
|
|
103
|
+
this.path = options.path;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Creates a new code model instance.
|
|
107
|
+
* @param options - Configuration options
|
|
108
|
+
* @returns A new CodeModel instance
|
|
109
|
+
*/
|
|
110
|
+
static new(options) {
|
|
111
|
+
return new _CodeModel({
|
|
112
|
+
viewport: viewport.ViewportModel.new({
|
|
113
|
+
width: options.width ?? 0,
|
|
114
|
+
height: options.height ?? 0
|
|
115
|
+
}),
|
|
116
|
+
active: options.active ?? false,
|
|
117
|
+
filename: "",
|
|
118
|
+
highlightedContent: "",
|
|
119
|
+
syntaxTheme: options.syntaxTheme ?? "dracula",
|
|
120
|
+
filesystem: options.filesystem,
|
|
121
|
+
path: options.path
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Create a copy with updated fields.
|
|
126
|
+
*/
|
|
127
|
+
with(partial) {
|
|
128
|
+
return new _CodeModel({
|
|
129
|
+
viewport: partial.viewport ?? this.viewport,
|
|
130
|
+
active: partial.active ?? this.active,
|
|
131
|
+
filename: partial.filename ?? this.filename,
|
|
132
|
+
highlightedContent: partial.highlightedContent ?? this.highlightedContent,
|
|
133
|
+
syntaxTheme: partial.syntaxTheme ?? this.syntaxTheme,
|
|
134
|
+
filesystem: partial.filesystem ?? this.filesystem,
|
|
135
|
+
path: partial.path ?? this.path
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Sets the file to display and triggers highlighting.
|
|
140
|
+
* @param filename - Path to the file to display
|
|
141
|
+
* @returns Command to read and highlight the file
|
|
142
|
+
*/
|
|
143
|
+
setFileName(filename) {
|
|
144
|
+
const updated = this.with({ filename });
|
|
145
|
+
return [updated, readFileContentCmd(this.filesystem, this.path, filename, this.syntaxTheme)];
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Sets whether the component is active (receives input).
|
|
149
|
+
* @param active - Whether the component should be active
|
|
150
|
+
* @returns Updated model
|
|
151
|
+
*/
|
|
152
|
+
setIsActive(active) {
|
|
153
|
+
if (this.active === active) return this;
|
|
154
|
+
return this.with({ active });
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Sets the syntax highlighting theme.
|
|
158
|
+
* @param theme - The theme name (e.g., "dracula", "monokai")
|
|
159
|
+
* @returns Updated model
|
|
160
|
+
*/
|
|
161
|
+
setSyntaxTheme(theme) {
|
|
162
|
+
if (this.syntaxTheme === theme) return this;
|
|
163
|
+
return this.with({ syntaxTheme: theme });
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Sets the viewport dimensions.
|
|
167
|
+
* @param width - Width in characters
|
|
168
|
+
* @param height - Height in lines
|
|
169
|
+
* @returns Updated model
|
|
170
|
+
*/
|
|
171
|
+
setSize(width, height) {
|
|
172
|
+
const nextViewport = this.viewport.setWidth(width).setHeight(height);
|
|
173
|
+
if (nextViewport === this.viewport) return this;
|
|
174
|
+
return this.with({ viewport: nextViewport });
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Scrolls to the top of the viewport.
|
|
178
|
+
* @returns Updated model
|
|
179
|
+
*/
|
|
180
|
+
gotoTop() {
|
|
181
|
+
const nextViewport = this.viewport.scrollToTop();
|
|
182
|
+
if (nextViewport === this.viewport) return this;
|
|
183
|
+
return this.with({ viewport: nextViewport });
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Tea init hook.
|
|
187
|
+
*/
|
|
188
|
+
init() {
|
|
189
|
+
return this.viewport.init();
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Handles messages (keyboard navigation when active).
|
|
193
|
+
* @param msg - The message to handle
|
|
194
|
+
* @returns Tuple of updated model and command
|
|
195
|
+
*/
|
|
196
|
+
update(msg) {
|
|
197
|
+
if (msg instanceof SyntaxMsg) {
|
|
198
|
+
const content = msg.content;
|
|
199
|
+
const style = new chapstick.Style().width(this.viewport.width).alignHorizontal("left");
|
|
200
|
+
const rendered = style.render(content);
|
|
201
|
+
const nextViewport = this.viewport.setContent(rendered);
|
|
202
|
+
return [
|
|
203
|
+
this.with({
|
|
204
|
+
highlightedContent: rendered,
|
|
205
|
+
viewport: nextViewport
|
|
206
|
+
}),
|
|
207
|
+
null
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
if (msg instanceof ErrorMsg) {
|
|
211
|
+
const errorContent = `Error: ${msg.error.message}`;
|
|
212
|
+
const style = new chapstick.Style().width(this.viewport.width).alignHorizontal("left");
|
|
213
|
+
const rendered = style.render(errorContent);
|
|
214
|
+
const nextViewport = this.viewport.setContent(rendered);
|
|
215
|
+
return [
|
|
216
|
+
this.with({
|
|
217
|
+
highlightedContent: rendered,
|
|
218
|
+
viewport: nextViewport
|
|
219
|
+
}),
|
|
220
|
+
null
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
if (this.active) {
|
|
224
|
+
const [nextViewport, cmd] = this.viewport.update(msg);
|
|
225
|
+
if (nextViewport !== this.viewport) {
|
|
226
|
+
return [this.with({ viewport: nextViewport }), cmd];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return [this, null];
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Returns the rendered string.
|
|
233
|
+
* @returns The viewport view
|
|
234
|
+
*/
|
|
235
|
+
view() {
|
|
236
|
+
return this.viewport.view();
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
exports.CodeModel = CodeModel;
|
|
241
|
+
exports.ErrorMsg = ErrorMsg;
|
|
242
|
+
exports.SyntaxMsg = SyntaxMsg;
|
|
243
|
+
exports.highlight = highlight;
|
|
244
|
+
//# sourceMappingURL=index.cjs.map
|
|
245
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/messages.ts","../src/model.ts"],"names":["getHighlighter","readFileContent","ViewportModel","Style"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Message containing syntax-highlighted content after async processing.\n * @public\n */\nexport class SyntaxMsg {\n readonly _tag = 'code-syntax'\n constructor(public readonly content: string) {}\n}\n\n/**\n * Message containing an error if file read or highlighting fails.\n * @public\n */\nexport class ErrorMsg {\n readonly _tag = 'code-error'\n constructor(public readonly error: Error) {}\n}\n","import { Style } from '@boba-cli/chapstick'\nimport { readFileContent } from '@boba-cli/filesystem'\nimport { type Cmd, type Msg } from '@boba-cli/tea'\nimport type { FileSystemAdapter, PathAdapter } from '@boba-cli/machine'\nimport { ViewportModel } from '@boba-cli/viewport'\nimport { getHighlighter } from 'shiki'\nimport { ErrorMsg, SyntaxMsg } from './messages.js'\n\n/**\n * Options for creating a code model.\n * @public\n */\nexport interface CodeOptions {\n /** Filesystem adapter for file operations */\n filesystem: FileSystemAdapter\n /** Path adapter for path operations */\n path: PathAdapter\n active?: boolean\n syntaxTheme?: string\n width?: number\n height?: number\n}\n\n/**\n * Highlight returns a syntax-highlighted string of text.\n * @param content - The source code content to highlight\n * @param extension - The file extension (e.g., \".ts\", \".go\")\n * @param theme - The syntax theme to use (default: \"dracula\")\n * @returns Promise resolving to the highlighted content string\n * @public\n */\nexport async function highlight(\n content: string,\n extension: string,\n theme: string = 'dracula',\n): Promise<string> {\n try {\n const highlighter = await getHighlighter({\n themes: [theme],\n langs: [],\n })\n\n // Map extension to language - use a safer approach\n const ext = extension.startsWith('.') ? extension.slice(1) : extension\n let lang = ext || 'text'\n\n // Load the language if needed, fallback to text\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument\n await highlighter.loadLanguage(lang as any)\n } catch {\n // If language loading fails, use 'text' as fallback\n lang = 'text'\n try {\n await highlighter.loadLanguage('text')\n } catch {\n // If even text fails, just return plain content\n return content\n }\n }\n\n // Get tokens from highlighter\n const tokens = highlighter.codeToTokens(content, {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment\n lang: lang as any,\n theme: theme,\n })\n\n // Convert tokens to ANSI colored text\n let result = ''\n for (const line of tokens.tokens) {\n for (const token of line) {\n if (token.color) {\n // Simple ANSI color conversion (this is a basic approach)\n result += `\\x1b[38;2;${hexToRgb(token.color)}m${token.content}\\x1b[0m`\n } else {\n result += token.content\n }\n }\n result += '\\n'\n }\n\n return result.trimEnd()\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n throw new Error(`Syntax highlighting failed: ${errorMessage}`)\n }\n}\n\n/**\n * Convert hex color to RGB values for ANSI\n */\nfunction hexToRgb(hex: string): string {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex)\n if (!result) return '255;255;255'\n return `${parseInt(result[1]!, 16)};${parseInt(result[2]!, 16)};${parseInt(result[3]!, 16)}`\n}\n\n/**\n * Read file content and highlight it asynchronously.\n */\nfunction readFileContentCmd(\n fsAdapter: FileSystemAdapter,\n pathAdapter: PathAdapter,\n fileName: string,\n syntaxTheme: string,\n): Cmd<Msg> {\n return async (): Promise<Msg> => {\n try {\n const content = await readFileContent(fsAdapter, fileName)\n const extension = pathAdapter.extname(fileName)\n const highlightedContent = await highlight(\n content,\n extension,\n syntaxTheme,\n )\n return new SyntaxMsg(highlightedContent)\n } catch (error) {\n return new ErrorMsg(\n error instanceof Error ? error : new Error(String(error)),\n )\n }\n }\n}\n\n/**\n * Model representing a syntax-highlighted code viewer.\n * @public\n */\nexport class CodeModel {\n readonly viewport: ViewportModel\n readonly active: boolean\n readonly filename: string\n readonly highlightedContent: string\n readonly syntaxTheme: string\n readonly filesystem: FileSystemAdapter\n readonly path: PathAdapter\n\n private constructor(options: {\n viewport: ViewportModel\n active: boolean\n filename: string\n highlightedContent: string\n syntaxTheme: string\n filesystem: FileSystemAdapter\n path: PathAdapter\n }) {\n this.viewport = options.viewport\n this.active = options.active\n this.filename = options.filename\n this.highlightedContent = options.highlightedContent\n this.syntaxTheme = options.syntaxTheme\n this.filesystem = options.filesystem\n this.path = options.path\n }\n\n /**\n * Creates a new code model instance.\n * @param options - Configuration options\n * @returns A new CodeModel instance\n */\n static new(options: CodeOptions): CodeModel {\n return new CodeModel({\n viewport: ViewportModel.new({\n width: options.width ?? 0,\n height: options.height ?? 0,\n }),\n active: options.active ?? false,\n filename: '',\n highlightedContent: '',\n syntaxTheme: options.syntaxTheme ?? 'dracula',\n filesystem: options.filesystem,\n path: options.path,\n })\n }\n\n /**\n * Create a copy with updated fields.\n */\n private with(\n partial: Partial<{\n viewport: ViewportModel\n active: boolean\n filename: string\n highlightedContent: string\n syntaxTheme: string\n filesystem: FileSystemAdapter\n path: PathAdapter\n }>,\n ): CodeModel {\n return new CodeModel({\n viewport: partial.viewport ?? this.viewport,\n active: partial.active ?? this.active,\n filename: partial.filename ?? this.filename,\n highlightedContent: partial.highlightedContent ?? this.highlightedContent,\n syntaxTheme: partial.syntaxTheme ?? this.syntaxTheme,\n filesystem: partial.filesystem ?? this.filesystem,\n path: partial.path ?? this.path,\n })\n }\n\n /**\n * Sets the file to display and triggers highlighting.\n * @param filename - Path to the file to display\n * @returns Command to read and highlight the file\n */\n setFileName(filename: string): [CodeModel, Cmd<Msg>] {\n const updated = this.with({ filename })\n return [updated, readFileContentCmd(this.filesystem, this.path, filename, this.syntaxTheme)]\n }\n\n /**\n * Sets whether the component is active (receives input).\n * @param active - Whether the component should be active\n * @returns Updated model\n */\n setIsActive(active: boolean): CodeModel {\n if (this.active === active) return this\n return this.with({ active })\n }\n\n /**\n * Sets the syntax highlighting theme.\n * @param theme - The theme name (e.g., \"dracula\", \"monokai\")\n * @returns Updated model\n */\n setSyntaxTheme(theme: string): CodeModel {\n if (this.syntaxTheme === theme) return this\n return this.with({ syntaxTheme: theme })\n }\n\n /**\n * Sets the viewport dimensions.\n * @param width - Width in characters\n * @param height - Height in lines\n * @returns Updated model\n */\n setSize(width: number, height: number): CodeModel {\n const nextViewport = this.viewport.setWidth(width).setHeight(height)\n if (nextViewport === this.viewport) return this\n return this.with({ viewport: nextViewport })\n }\n\n /**\n * Scrolls to the top of the viewport.\n * @returns Updated model\n */\n gotoTop(): CodeModel {\n const nextViewport = this.viewport.scrollToTop()\n if (nextViewport === this.viewport) return this\n return this.with({ viewport: nextViewport })\n }\n\n /**\n * Tea init hook.\n */\n init(): Cmd<Msg> {\n return this.viewport.init()\n }\n\n /**\n * Handles messages (keyboard navigation when active).\n * @param msg - The message to handle\n * @returns Tuple of updated model and command\n */\n update(msg: Msg): [CodeModel, Cmd<Msg>] {\n if (msg instanceof SyntaxMsg) {\n const content = msg.content\n // Apply width for consistent line lengths and left-align to pad lines\n // Viewport handles height/scrolling - don't use .height() here as it truncates\n const style = new Style()\n .width(this.viewport.width)\n .alignHorizontal('left')\n const rendered = style.render(content)\n const nextViewport = this.viewport.setContent(rendered)\n return [\n this.with({\n highlightedContent: rendered,\n viewport: nextViewport,\n }),\n null,\n ]\n }\n\n if (msg instanceof ErrorMsg) {\n const errorContent = `Error: ${msg.error.message}`\n const style = new Style()\n .width(this.viewport.width)\n .alignHorizontal('left')\n const rendered = style.render(errorContent)\n const nextViewport = this.viewport.setContent(rendered)\n return [\n this.with({\n highlightedContent: rendered,\n viewport: nextViewport,\n }),\n null,\n ]\n }\n\n if (this.active) {\n const [nextViewport, cmd] = this.viewport.update(msg)\n if (nextViewport !== this.viewport) {\n return [this.with({ viewport: nextViewport }), cmd]\n }\n }\n\n return [this, null]\n }\n\n /**\n * Returns the rendered string.\n * @returns The viewport view\n */\n view(): string {\n return this.viewport.view()\n }\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Cmd, Msg } from '@boba-cli/tea';
|
|
2
|
+
import { FileSystemAdapter, PathAdapter } from '@boba-cli/machine';
|
|
3
|
+
import { ViewportModel } from '@boba-cli/viewport';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Options for creating a code model.
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
interface CodeOptions {
|
|
10
|
+
/** Filesystem adapter for file operations */
|
|
11
|
+
filesystem: FileSystemAdapter;
|
|
12
|
+
/** Path adapter for path operations */
|
|
13
|
+
path: PathAdapter;
|
|
14
|
+
active?: boolean;
|
|
15
|
+
syntaxTheme?: string;
|
|
16
|
+
width?: number;
|
|
17
|
+
height?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Highlight returns a syntax-highlighted string of text.
|
|
21
|
+
* @param content - The source code content to highlight
|
|
22
|
+
* @param extension - The file extension (e.g., ".ts", ".go")
|
|
23
|
+
* @param theme - The syntax theme to use (default: "dracula")
|
|
24
|
+
* @returns Promise resolving to the highlighted content string
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
declare function highlight(content: string, extension: string, theme?: string): Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* Model representing a syntax-highlighted code viewer.
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
declare class CodeModel {
|
|
33
|
+
readonly viewport: ViewportModel;
|
|
34
|
+
readonly active: boolean;
|
|
35
|
+
readonly filename: string;
|
|
36
|
+
readonly highlightedContent: string;
|
|
37
|
+
readonly syntaxTheme: string;
|
|
38
|
+
readonly filesystem: FileSystemAdapter;
|
|
39
|
+
readonly path: PathAdapter;
|
|
40
|
+
private constructor();
|
|
41
|
+
/**
|
|
42
|
+
* Creates a new code model instance.
|
|
43
|
+
* @param options - Configuration options
|
|
44
|
+
* @returns A new CodeModel instance
|
|
45
|
+
*/
|
|
46
|
+
static new(options: CodeOptions): CodeModel;
|
|
47
|
+
/**
|
|
48
|
+
* Create a copy with updated fields.
|
|
49
|
+
*/
|
|
50
|
+
private with;
|
|
51
|
+
/**
|
|
52
|
+
* Sets the file to display and triggers highlighting.
|
|
53
|
+
* @param filename - Path to the file to display
|
|
54
|
+
* @returns Command to read and highlight the file
|
|
55
|
+
*/
|
|
56
|
+
setFileName(filename: string): [CodeModel, Cmd<Msg>];
|
|
57
|
+
/**
|
|
58
|
+
* Sets whether the component is active (receives input).
|
|
59
|
+
* @param active - Whether the component should be active
|
|
60
|
+
* @returns Updated model
|
|
61
|
+
*/
|
|
62
|
+
setIsActive(active: boolean): CodeModel;
|
|
63
|
+
/**
|
|
64
|
+
* Sets the syntax highlighting theme.
|
|
65
|
+
* @param theme - The theme name (e.g., "dracula", "monokai")
|
|
66
|
+
* @returns Updated model
|
|
67
|
+
*/
|
|
68
|
+
setSyntaxTheme(theme: string): CodeModel;
|
|
69
|
+
/**
|
|
70
|
+
* Sets the viewport dimensions.
|
|
71
|
+
* @param width - Width in characters
|
|
72
|
+
* @param height - Height in lines
|
|
73
|
+
* @returns Updated model
|
|
74
|
+
*/
|
|
75
|
+
setSize(width: number, height: number): CodeModel;
|
|
76
|
+
/**
|
|
77
|
+
* Scrolls to the top of the viewport.
|
|
78
|
+
* @returns Updated model
|
|
79
|
+
*/
|
|
80
|
+
gotoTop(): CodeModel;
|
|
81
|
+
/**
|
|
82
|
+
* Tea init hook.
|
|
83
|
+
*/
|
|
84
|
+
init(): Cmd<Msg>;
|
|
85
|
+
/**
|
|
86
|
+
* Handles messages (keyboard navigation when active).
|
|
87
|
+
* @param msg - The message to handle
|
|
88
|
+
* @returns Tuple of updated model and command
|
|
89
|
+
*/
|
|
90
|
+
update(msg: Msg): [CodeModel, Cmd<Msg>];
|
|
91
|
+
/**
|
|
92
|
+
* Returns the rendered string.
|
|
93
|
+
* @returns The viewport view
|
|
94
|
+
*/
|
|
95
|
+
view(): string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Message containing syntax-highlighted content after async processing.
|
|
100
|
+
* @public
|
|
101
|
+
*/
|
|
102
|
+
declare class SyntaxMsg {
|
|
103
|
+
readonly content: string;
|
|
104
|
+
readonly _tag = "code-syntax";
|
|
105
|
+
constructor(content: string);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Message containing an error if file read or highlighting fails.
|
|
109
|
+
* @public
|
|
110
|
+
*/
|
|
111
|
+
declare class ErrorMsg {
|
|
112
|
+
readonly error: Error;
|
|
113
|
+
readonly _tag = "code-error";
|
|
114
|
+
constructor(error: Error);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { CodeModel, type CodeOptions, ErrorMsg, SyntaxMsg, highlight };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Cmd, Msg } from '@boba-cli/tea';
|
|
2
|
+
import { FileSystemAdapter, PathAdapter } from '@boba-cli/machine';
|
|
3
|
+
import { ViewportModel } from '@boba-cli/viewport';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Options for creating a code model.
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
interface CodeOptions {
|
|
10
|
+
/** Filesystem adapter for file operations */
|
|
11
|
+
filesystem: FileSystemAdapter;
|
|
12
|
+
/** Path adapter for path operations */
|
|
13
|
+
path: PathAdapter;
|
|
14
|
+
active?: boolean;
|
|
15
|
+
syntaxTheme?: string;
|
|
16
|
+
width?: number;
|
|
17
|
+
height?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Highlight returns a syntax-highlighted string of text.
|
|
21
|
+
* @param content - The source code content to highlight
|
|
22
|
+
* @param extension - The file extension (e.g., ".ts", ".go")
|
|
23
|
+
* @param theme - The syntax theme to use (default: "dracula")
|
|
24
|
+
* @returns Promise resolving to the highlighted content string
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
declare function highlight(content: string, extension: string, theme?: string): Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* Model representing a syntax-highlighted code viewer.
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
declare class CodeModel {
|
|
33
|
+
readonly viewport: ViewportModel;
|
|
34
|
+
readonly active: boolean;
|
|
35
|
+
readonly filename: string;
|
|
36
|
+
readonly highlightedContent: string;
|
|
37
|
+
readonly syntaxTheme: string;
|
|
38
|
+
readonly filesystem: FileSystemAdapter;
|
|
39
|
+
readonly path: PathAdapter;
|
|
40
|
+
private constructor();
|
|
41
|
+
/**
|
|
42
|
+
* Creates a new code model instance.
|
|
43
|
+
* @param options - Configuration options
|
|
44
|
+
* @returns A new CodeModel instance
|
|
45
|
+
*/
|
|
46
|
+
static new(options: CodeOptions): CodeModel;
|
|
47
|
+
/**
|
|
48
|
+
* Create a copy with updated fields.
|
|
49
|
+
*/
|
|
50
|
+
private with;
|
|
51
|
+
/**
|
|
52
|
+
* Sets the file to display and triggers highlighting.
|
|
53
|
+
* @param filename - Path to the file to display
|
|
54
|
+
* @returns Command to read and highlight the file
|
|
55
|
+
*/
|
|
56
|
+
setFileName(filename: string): [CodeModel, Cmd<Msg>];
|
|
57
|
+
/**
|
|
58
|
+
* Sets whether the component is active (receives input).
|
|
59
|
+
* @param active - Whether the component should be active
|
|
60
|
+
* @returns Updated model
|
|
61
|
+
*/
|
|
62
|
+
setIsActive(active: boolean): CodeModel;
|
|
63
|
+
/**
|
|
64
|
+
* Sets the syntax highlighting theme.
|
|
65
|
+
* @param theme - The theme name (e.g., "dracula", "monokai")
|
|
66
|
+
* @returns Updated model
|
|
67
|
+
*/
|
|
68
|
+
setSyntaxTheme(theme: string): CodeModel;
|
|
69
|
+
/**
|
|
70
|
+
* Sets the viewport dimensions.
|
|
71
|
+
* @param width - Width in characters
|
|
72
|
+
* @param height - Height in lines
|
|
73
|
+
* @returns Updated model
|
|
74
|
+
*/
|
|
75
|
+
setSize(width: number, height: number): CodeModel;
|
|
76
|
+
/**
|
|
77
|
+
* Scrolls to the top of the viewport.
|
|
78
|
+
* @returns Updated model
|
|
79
|
+
*/
|
|
80
|
+
gotoTop(): CodeModel;
|
|
81
|
+
/**
|
|
82
|
+
* Tea init hook.
|
|
83
|
+
*/
|
|
84
|
+
init(): Cmd<Msg>;
|
|
85
|
+
/**
|
|
86
|
+
* Handles messages (keyboard navigation when active).
|
|
87
|
+
* @param msg - The message to handle
|
|
88
|
+
* @returns Tuple of updated model and command
|
|
89
|
+
*/
|
|
90
|
+
update(msg: Msg): [CodeModel, Cmd<Msg>];
|
|
91
|
+
/**
|
|
92
|
+
* Returns the rendered string.
|
|
93
|
+
* @returns The viewport view
|
|
94
|
+
*/
|
|
95
|
+
view(): string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Message containing syntax-highlighted content after async processing.
|
|
100
|
+
* @public
|
|
101
|
+
*/
|
|
102
|
+
declare class SyntaxMsg {
|
|
103
|
+
readonly content: string;
|
|
104
|
+
readonly _tag = "code-syntax";
|
|
105
|
+
constructor(content: string);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Message containing an error if file read or highlighting fails.
|
|
109
|
+
* @public
|
|
110
|
+
*/
|
|
111
|
+
declare class ErrorMsg {
|
|
112
|
+
readonly error: Error;
|
|
113
|
+
readonly _tag = "code-error";
|
|
114
|
+
constructor(error: Error);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { CodeModel, type CodeOptions, ErrorMsg, SyntaxMsg, highlight };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { Style } from '@boba-cli/chapstick';
|
|
2
|
+
import { readFileContent } from '@boba-cli/filesystem';
|
|
3
|
+
import { ViewportModel } from '@boba-cli/viewport';
|
|
4
|
+
import { getHighlighter } from 'shiki';
|
|
5
|
+
|
|
6
|
+
// src/model.ts
|
|
7
|
+
|
|
8
|
+
// src/messages.ts
|
|
9
|
+
var SyntaxMsg = class {
|
|
10
|
+
constructor(content) {
|
|
11
|
+
this.content = content;
|
|
12
|
+
}
|
|
13
|
+
_tag = "code-syntax";
|
|
14
|
+
};
|
|
15
|
+
var ErrorMsg = class {
|
|
16
|
+
constructor(error) {
|
|
17
|
+
this.error = error;
|
|
18
|
+
}
|
|
19
|
+
_tag = "code-error";
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/model.ts
|
|
23
|
+
async function highlight(content, extension, theme = "dracula") {
|
|
24
|
+
try {
|
|
25
|
+
const highlighter = await getHighlighter({
|
|
26
|
+
themes: [theme],
|
|
27
|
+
langs: []
|
|
28
|
+
});
|
|
29
|
+
const ext = extension.startsWith(".") ? extension.slice(1) : extension;
|
|
30
|
+
let lang = ext || "text";
|
|
31
|
+
try {
|
|
32
|
+
await highlighter.loadLanguage(lang);
|
|
33
|
+
} catch {
|
|
34
|
+
lang = "text";
|
|
35
|
+
try {
|
|
36
|
+
await highlighter.loadLanguage("text");
|
|
37
|
+
} catch {
|
|
38
|
+
return content;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const tokens = highlighter.codeToTokens(content, {
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
|
|
43
|
+
lang,
|
|
44
|
+
theme
|
|
45
|
+
});
|
|
46
|
+
let result = "";
|
|
47
|
+
for (const line of tokens.tokens) {
|
|
48
|
+
for (const token of line) {
|
|
49
|
+
if (token.color) {
|
|
50
|
+
result += `\x1B[38;2;${hexToRgb(token.color)}m${token.content}\x1B[0m`;
|
|
51
|
+
} else {
|
|
52
|
+
result += token.content;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
result += "\n";
|
|
56
|
+
}
|
|
57
|
+
return result.trimEnd();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
60
|
+
throw new Error(`Syntax highlighting failed: ${errorMessage}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function hexToRgb(hex) {
|
|
64
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
65
|
+
if (!result) return "255;255;255";
|
|
66
|
+
return `${parseInt(result[1], 16)};${parseInt(result[2], 16)};${parseInt(result[3], 16)}`;
|
|
67
|
+
}
|
|
68
|
+
function readFileContentCmd(fsAdapter, pathAdapter, fileName, syntaxTheme) {
|
|
69
|
+
return async () => {
|
|
70
|
+
try {
|
|
71
|
+
const content = await readFileContent(fsAdapter, fileName);
|
|
72
|
+
const extension = pathAdapter.extname(fileName);
|
|
73
|
+
const highlightedContent = await highlight(
|
|
74
|
+
content,
|
|
75
|
+
extension,
|
|
76
|
+
syntaxTheme
|
|
77
|
+
);
|
|
78
|
+
return new SyntaxMsg(highlightedContent);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return new ErrorMsg(
|
|
81
|
+
error instanceof Error ? error : new Error(String(error))
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
var CodeModel = class _CodeModel {
|
|
87
|
+
viewport;
|
|
88
|
+
active;
|
|
89
|
+
filename;
|
|
90
|
+
highlightedContent;
|
|
91
|
+
syntaxTheme;
|
|
92
|
+
filesystem;
|
|
93
|
+
path;
|
|
94
|
+
constructor(options) {
|
|
95
|
+
this.viewport = options.viewport;
|
|
96
|
+
this.active = options.active;
|
|
97
|
+
this.filename = options.filename;
|
|
98
|
+
this.highlightedContent = options.highlightedContent;
|
|
99
|
+
this.syntaxTheme = options.syntaxTheme;
|
|
100
|
+
this.filesystem = options.filesystem;
|
|
101
|
+
this.path = options.path;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Creates a new code model instance.
|
|
105
|
+
* @param options - Configuration options
|
|
106
|
+
* @returns A new CodeModel instance
|
|
107
|
+
*/
|
|
108
|
+
static new(options) {
|
|
109
|
+
return new _CodeModel({
|
|
110
|
+
viewport: ViewportModel.new({
|
|
111
|
+
width: options.width ?? 0,
|
|
112
|
+
height: options.height ?? 0
|
|
113
|
+
}),
|
|
114
|
+
active: options.active ?? false,
|
|
115
|
+
filename: "",
|
|
116
|
+
highlightedContent: "",
|
|
117
|
+
syntaxTheme: options.syntaxTheme ?? "dracula",
|
|
118
|
+
filesystem: options.filesystem,
|
|
119
|
+
path: options.path
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Create a copy with updated fields.
|
|
124
|
+
*/
|
|
125
|
+
with(partial) {
|
|
126
|
+
return new _CodeModel({
|
|
127
|
+
viewport: partial.viewport ?? this.viewport,
|
|
128
|
+
active: partial.active ?? this.active,
|
|
129
|
+
filename: partial.filename ?? this.filename,
|
|
130
|
+
highlightedContent: partial.highlightedContent ?? this.highlightedContent,
|
|
131
|
+
syntaxTheme: partial.syntaxTheme ?? this.syntaxTheme,
|
|
132
|
+
filesystem: partial.filesystem ?? this.filesystem,
|
|
133
|
+
path: partial.path ?? this.path
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Sets the file to display and triggers highlighting.
|
|
138
|
+
* @param filename - Path to the file to display
|
|
139
|
+
* @returns Command to read and highlight the file
|
|
140
|
+
*/
|
|
141
|
+
setFileName(filename) {
|
|
142
|
+
const updated = this.with({ filename });
|
|
143
|
+
return [updated, readFileContentCmd(this.filesystem, this.path, filename, this.syntaxTheme)];
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Sets whether the component is active (receives input).
|
|
147
|
+
* @param active - Whether the component should be active
|
|
148
|
+
* @returns Updated model
|
|
149
|
+
*/
|
|
150
|
+
setIsActive(active) {
|
|
151
|
+
if (this.active === active) return this;
|
|
152
|
+
return this.with({ active });
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Sets the syntax highlighting theme.
|
|
156
|
+
* @param theme - The theme name (e.g., "dracula", "monokai")
|
|
157
|
+
* @returns Updated model
|
|
158
|
+
*/
|
|
159
|
+
setSyntaxTheme(theme) {
|
|
160
|
+
if (this.syntaxTheme === theme) return this;
|
|
161
|
+
return this.with({ syntaxTheme: theme });
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Sets the viewport dimensions.
|
|
165
|
+
* @param width - Width in characters
|
|
166
|
+
* @param height - Height in lines
|
|
167
|
+
* @returns Updated model
|
|
168
|
+
*/
|
|
169
|
+
setSize(width, height) {
|
|
170
|
+
const nextViewport = this.viewport.setWidth(width).setHeight(height);
|
|
171
|
+
if (nextViewport === this.viewport) return this;
|
|
172
|
+
return this.with({ viewport: nextViewport });
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Scrolls to the top of the viewport.
|
|
176
|
+
* @returns Updated model
|
|
177
|
+
*/
|
|
178
|
+
gotoTop() {
|
|
179
|
+
const nextViewport = this.viewport.scrollToTop();
|
|
180
|
+
if (nextViewport === this.viewport) return this;
|
|
181
|
+
return this.with({ viewport: nextViewport });
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Tea init hook.
|
|
185
|
+
*/
|
|
186
|
+
init() {
|
|
187
|
+
return this.viewport.init();
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Handles messages (keyboard navigation when active).
|
|
191
|
+
* @param msg - The message to handle
|
|
192
|
+
* @returns Tuple of updated model and command
|
|
193
|
+
*/
|
|
194
|
+
update(msg) {
|
|
195
|
+
if (msg instanceof SyntaxMsg) {
|
|
196
|
+
const content = msg.content;
|
|
197
|
+
const style = new Style().width(this.viewport.width).alignHorizontal("left");
|
|
198
|
+
const rendered = style.render(content);
|
|
199
|
+
const nextViewport = this.viewport.setContent(rendered);
|
|
200
|
+
return [
|
|
201
|
+
this.with({
|
|
202
|
+
highlightedContent: rendered,
|
|
203
|
+
viewport: nextViewport
|
|
204
|
+
}),
|
|
205
|
+
null
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
if (msg instanceof ErrorMsg) {
|
|
209
|
+
const errorContent = `Error: ${msg.error.message}`;
|
|
210
|
+
const style = new Style().width(this.viewport.width).alignHorizontal("left");
|
|
211
|
+
const rendered = style.render(errorContent);
|
|
212
|
+
const nextViewport = this.viewport.setContent(rendered);
|
|
213
|
+
return [
|
|
214
|
+
this.with({
|
|
215
|
+
highlightedContent: rendered,
|
|
216
|
+
viewport: nextViewport
|
|
217
|
+
}),
|
|
218
|
+
null
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
if (this.active) {
|
|
222
|
+
const [nextViewport, cmd] = this.viewport.update(msg);
|
|
223
|
+
if (nextViewport !== this.viewport) {
|
|
224
|
+
return [this.with({ viewport: nextViewport }), cmd];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return [this, null];
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Returns the rendered string.
|
|
231
|
+
* @returns The viewport view
|
|
232
|
+
*/
|
|
233
|
+
view() {
|
|
234
|
+
return this.viewport.view();
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export { CodeModel, ErrorMsg, SyntaxMsg, highlight };
|
|
239
|
+
//# sourceMappingURL=index.js.map
|
|
240
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/messages.ts","../src/model.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Message containing syntax-highlighted content after async processing.\n * @public\n */\nexport class SyntaxMsg {\n readonly _tag = 'code-syntax'\n constructor(public readonly content: string) {}\n}\n\n/**\n * Message containing an error if file read or highlighting fails.\n * @public\n */\nexport class ErrorMsg {\n readonly _tag = 'code-error'\n constructor(public readonly error: Error) {}\n}\n","import { Style } from '@boba-cli/chapstick'\nimport { readFileContent } from '@boba-cli/filesystem'\nimport { type Cmd, type Msg } from '@boba-cli/tea'\nimport type { FileSystemAdapter, PathAdapter } from '@boba-cli/machine'\nimport { ViewportModel } from '@boba-cli/viewport'\nimport { getHighlighter } from 'shiki'\nimport { ErrorMsg, SyntaxMsg } from './messages.js'\n\n/**\n * Options for creating a code model.\n * @public\n */\nexport interface CodeOptions {\n /** Filesystem adapter for file operations */\n filesystem: FileSystemAdapter\n /** Path adapter for path operations */\n path: PathAdapter\n active?: boolean\n syntaxTheme?: string\n width?: number\n height?: number\n}\n\n/**\n * Highlight returns a syntax-highlighted string of text.\n * @param content - The source code content to highlight\n * @param extension - The file extension (e.g., \".ts\", \".go\")\n * @param theme - The syntax theme to use (default: \"dracula\")\n * @returns Promise resolving to the highlighted content string\n * @public\n */\nexport async function highlight(\n content: string,\n extension: string,\n theme: string = 'dracula',\n): Promise<string> {\n try {\n const highlighter = await getHighlighter({\n themes: [theme],\n langs: [],\n })\n\n // Map extension to language - use a safer approach\n const ext = extension.startsWith('.') ? extension.slice(1) : extension\n let lang = ext || 'text'\n\n // Load the language if needed, fallback to text\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument\n await highlighter.loadLanguage(lang as any)\n } catch {\n // If language loading fails, use 'text' as fallback\n lang = 'text'\n try {\n await highlighter.loadLanguage('text')\n } catch {\n // If even text fails, just return plain content\n return content\n }\n }\n\n // Get tokens from highlighter\n const tokens = highlighter.codeToTokens(content, {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment\n lang: lang as any,\n theme: theme,\n })\n\n // Convert tokens to ANSI colored text\n let result = ''\n for (const line of tokens.tokens) {\n for (const token of line) {\n if (token.color) {\n // Simple ANSI color conversion (this is a basic approach)\n result += `\\x1b[38;2;${hexToRgb(token.color)}m${token.content}\\x1b[0m`\n } else {\n result += token.content\n }\n }\n result += '\\n'\n }\n\n return result.trimEnd()\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n throw new Error(`Syntax highlighting failed: ${errorMessage}`)\n }\n}\n\n/**\n * Convert hex color to RGB values for ANSI\n */\nfunction hexToRgb(hex: string): string {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex)\n if (!result) return '255;255;255'\n return `${parseInt(result[1]!, 16)};${parseInt(result[2]!, 16)};${parseInt(result[3]!, 16)}`\n}\n\n/**\n * Read file content and highlight it asynchronously.\n */\nfunction readFileContentCmd(\n fsAdapter: FileSystemAdapter,\n pathAdapter: PathAdapter,\n fileName: string,\n syntaxTheme: string,\n): Cmd<Msg> {\n return async (): Promise<Msg> => {\n try {\n const content = await readFileContent(fsAdapter, fileName)\n const extension = pathAdapter.extname(fileName)\n const highlightedContent = await highlight(\n content,\n extension,\n syntaxTheme,\n )\n return new SyntaxMsg(highlightedContent)\n } catch (error) {\n return new ErrorMsg(\n error instanceof Error ? error : new Error(String(error)),\n )\n }\n }\n}\n\n/**\n * Model representing a syntax-highlighted code viewer.\n * @public\n */\nexport class CodeModel {\n readonly viewport: ViewportModel\n readonly active: boolean\n readonly filename: string\n readonly highlightedContent: string\n readonly syntaxTheme: string\n readonly filesystem: FileSystemAdapter\n readonly path: PathAdapter\n\n private constructor(options: {\n viewport: ViewportModel\n active: boolean\n filename: string\n highlightedContent: string\n syntaxTheme: string\n filesystem: FileSystemAdapter\n path: PathAdapter\n }) {\n this.viewport = options.viewport\n this.active = options.active\n this.filename = options.filename\n this.highlightedContent = options.highlightedContent\n this.syntaxTheme = options.syntaxTheme\n this.filesystem = options.filesystem\n this.path = options.path\n }\n\n /**\n * Creates a new code model instance.\n * @param options - Configuration options\n * @returns A new CodeModel instance\n */\n static new(options: CodeOptions): CodeModel {\n return new CodeModel({\n viewport: ViewportModel.new({\n width: options.width ?? 0,\n height: options.height ?? 0,\n }),\n active: options.active ?? false,\n filename: '',\n highlightedContent: '',\n syntaxTheme: options.syntaxTheme ?? 'dracula',\n filesystem: options.filesystem,\n path: options.path,\n })\n }\n\n /**\n * Create a copy with updated fields.\n */\n private with(\n partial: Partial<{\n viewport: ViewportModel\n active: boolean\n filename: string\n highlightedContent: string\n syntaxTheme: string\n filesystem: FileSystemAdapter\n path: PathAdapter\n }>,\n ): CodeModel {\n return new CodeModel({\n viewport: partial.viewport ?? this.viewport,\n active: partial.active ?? this.active,\n filename: partial.filename ?? this.filename,\n highlightedContent: partial.highlightedContent ?? this.highlightedContent,\n syntaxTheme: partial.syntaxTheme ?? this.syntaxTheme,\n filesystem: partial.filesystem ?? this.filesystem,\n path: partial.path ?? this.path,\n })\n }\n\n /**\n * Sets the file to display and triggers highlighting.\n * @param filename - Path to the file to display\n * @returns Command to read and highlight the file\n */\n setFileName(filename: string): [CodeModel, Cmd<Msg>] {\n const updated = this.with({ filename })\n return [updated, readFileContentCmd(this.filesystem, this.path, filename, this.syntaxTheme)]\n }\n\n /**\n * Sets whether the component is active (receives input).\n * @param active - Whether the component should be active\n * @returns Updated model\n */\n setIsActive(active: boolean): CodeModel {\n if (this.active === active) return this\n return this.with({ active })\n }\n\n /**\n * Sets the syntax highlighting theme.\n * @param theme - The theme name (e.g., \"dracula\", \"monokai\")\n * @returns Updated model\n */\n setSyntaxTheme(theme: string): CodeModel {\n if (this.syntaxTheme === theme) return this\n return this.with({ syntaxTheme: theme })\n }\n\n /**\n * Sets the viewport dimensions.\n * @param width - Width in characters\n * @param height - Height in lines\n * @returns Updated model\n */\n setSize(width: number, height: number): CodeModel {\n const nextViewport = this.viewport.setWidth(width).setHeight(height)\n if (nextViewport === this.viewport) return this\n return this.with({ viewport: nextViewport })\n }\n\n /**\n * Scrolls to the top of the viewport.\n * @returns Updated model\n */\n gotoTop(): CodeModel {\n const nextViewport = this.viewport.scrollToTop()\n if (nextViewport === this.viewport) return this\n return this.with({ viewport: nextViewport })\n }\n\n /**\n * Tea init hook.\n */\n init(): Cmd<Msg> {\n return this.viewport.init()\n }\n\n /**\n * Handles messages (keyboard navigation when active).\n * @param msg - The message to handle\n * @returns Tuple of updated model and command\n */\n update(msg: Msg): [CodeModel, Cmd<Msg>] {\n if (msg instanceof SyntaxMsg) {\n const content = msg.content\n // Apply width for consistent line lengths and left-align to pad lines\n // Viewport handles height/scrolling - don't use .height() here as it truncates\n const style = new Style()\n .width(this.viewport.width)\n .alignHorizontal('left')\n const rendered = style.render(content)\n const nextViewport = this.viewport.setContent(rendered)\n return [\n this.with({\n highlightedContent: rendered,\n viewport: nextViewport,\n }),\n null,\n ]\n }\n\n if (msg instanceof ErrorMsg) {\n const errorContent = `Error: ${msg.error.message}`\n const style = new Style()\n .width(this.viewport.width)\n .alignHorizontal('left')\n const rendered = style.render(errorContent)\n const nextViewport = this.viewport.setContent(rendered)\n return [\n this.with({\n highlightedContent: rendered,\n viewport: nextViewport,\n }),\n null,\n ]\n }\n\n if (this.active) {\n const [nextViewport, cmd] = this.viewport.update(msg)\n if (nextViewport !== this.viewport) {\n return [this.with({ viewport: nextViewport }), cmd]\n }\n }\n\n return [this, null]\n }\n\n /**\n * Returns the rendered string.\n * @returns The viewport view\n */\n view(): string {\n return this.viewport.view()\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@boba-cli/code",
|
|
3
|
+
"description": "Syntax-highlighted code viewer with scrollable viewport for Boba terminal UIs",
|
|
4
|
+
"version": "0.1.0-alpha.2",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"shiki": "^1.24.2",
|
|
7
|
+
"@boba-cli/chapstick": "0.1.0-alpha.2",
|
|
8
|
+
"@boba-cli/filesystem": "0.1.0-alpha.2",
|
|
9
|
+
"@boba-cli/machine": "0.1.0-alpha.1",
|
|
10
|
+
"@boba-cli/tea": "0.1.0-alpha.1",
|
|
11
|
+
"@boba-cli/viewport": "0.1.0-alpha.2"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/node": "^24.10.2",
|
|
15
|
+
"typescript": "5.8.2",
|
|
16
|
+
"vitest": "^4.0.16"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20.0.0"
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"import": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"require": {
|
|
28
|
+
"types": "./dist/index.d.cts",
|
|
29
|
+
"default": "./dist/index.cjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist"
|
|
36
|
+
],
|
|
37
|
+
"main": "./dist/index.cjs",
|
|
38
|
+
"module": "./dist/index.js",
|
|
39
|
+
"type": "module",
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsup",
|
|
43
|
+
"check:api-report": "pnpm run generate:api-report",
|
|
44
|
+
"check:eslint": "pnpm run lint",
|
|
45
|
+
"generate:api-report": "api-extractor run --local",
|
|
46
|
+
"lint": "eslint \"{src,test}/**/*.{ts,tsx}\"",
|
|
47
|
+
"test": "vitest run"
|
|
48
|
+
}
|
|
49
|
+
}
|