@boba-cli/markdown 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 +41 -0
- package/dist/index.cjs +200 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +146 -0
- package/dist/index.d.ts +146 -0
- package/dist/index.js +195 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @boba-cli/markdown
|
|
2
|
+
|
|
3
|
+
Markdown viewer component for Boba terminal UIs.
|
|
4
|
+
|
|
5
|
+
<img src="../../examples/markdown-demo.gif" width="950" alt="Markdown component demo" />
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Renders markdown with beautiful terminal styling
|
|
10
|
+
- Scrollable viewport for long documents
|
|
11
|
+
- Automatic light/dark theme detection
|
|
12
|
+
- Support for headers, code blocks, lists, links, and more
|
|
13
|
+
- Word wrapping at viewport width
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add @boba-cli/markdown
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { MarkdownModel } from '@boba-cli/markdown'
|
|
25
|
+
import { Program } from '@boba-cli/tea'
|
|
26
|
+
|
|
27
|
+
let model = MarkdownModel.new({ active: true })
|
|
28
|
+
const [updatedModel, cmd] = model.setFileName('README.md')
|
|
29
|
+
model = updatedModel
|
|
30
|
+
|
|
31
|
+
// Handle resize
|
|
32
|
+
const [resizedModel] = model.setSize(width, height)
|
|
33
|
+
model = resizedModel
|
|
34
|
+
|
|
35
|
+
// Render
|
|
36
|
+
const view = model.view()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
See the [API documentation](../../docs/markdown.md) for complete details.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
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 marked = require('marked');
|
|
7
|
+
var markedTerminal = require('marked-terminal');
|
|
8
|
+
var machine = require('@boba-cli/machine');
|
|
9
|
+
|
|
10
|
+
// src/model.ts
|
|
11
|
+
|
|
12
|
+
// src/messages.ts
|
|
13
|
+
var RenderMarkdownMsg = class {
|
|
14
|
+
constructor(content) {
|
|
15
|
+
this.content = content;
|
|
16
|
+
}
|
|
17
|
+
_tag = "markdown-render";
|
|
18
|
+
};
|
|
19
|
+
var ErrorMsg = class {
|
|
20
|
+
constructor(error) {
|
|
21
|
+
this.error = error;
|
|
22
|
+
}
|
|
23
|
+
_tag = "markdown-error";
|
|
24
|
+
};
|
|
25
|
+
function renderMarkdown(content, options = {}) {
|
|
26
|
+
const width = options.width ?? 80;
|
|
27
|
+
const background = options.background ?? options.env?.getTerminalBackground() ?? "dark";
|
|
28
|
+
const isDark = background !== "light";
|
|
29
|
+
const style = machine.createAlwaysEnabledStyle();
|
|
30
|
+
const marked$1 = new marked.Marked(
|
|
31
|
+
markedTerminal.markedTerminal({
|
|
32
|
+
// Wrap text at specified width
|
|
33
|
+
width,
|
|
34
|
+
reflowText: true,
|
|
35
|
+
// Headings - brighter on dark backgrounds
|
|
36
|
+
firstHeading: isDark ? style.cyan.bold : style.blue.bold,
|
|
37
|
+
heading: isDark ? style.cyan.bold : style.blue.bold,
|
|
38
|
+
// Code blocks
|
|
39
|
+
code: isDark ? style.white : style.gray,
|
|
40
|
+
blockquote: isDark ? style.white : style.gray,
|
|
41
|
+
// Emphasis
|
|
42
|
+
strong: style.bold,
|
|
43
|
+
em: style.italic,
|
|
44
|
+
// Lists
|
|
45
|
+
listitem: style,
|
|
46
|
+
// Links
|
|
47
|
+
link: isDark ? style.blueBright : style.blue,
|
|
48
|
+
// Other elements
|
|
49
|
+
hr: style.gray,
|
|
50
|
+
paragraph: style
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
try {
|
|
54
|
+
const rendered = marked$1.parse(content);
|
|
55
|
+
return rendered.trim();
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Failed to render markdown: ${error instanceof Error ? error.message : String(error)}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/model.ts
|
|
64
|
+
var MarkdownModel = class _MarkdownModel {
|
|
65
|
+
viewport;
|
|
66
|
+
active;
|
|
67
|
+
fileName;
|
|
68
|
+
filesystem;
|
|
69
|
+
constructor(options) {
|
|
70
|
+
this.viewport = options.viewport;
|
|
71
|
+
this.active = options.active;
|
|
72
|
+
this.fileName = options.fileName;
|
|
73
|
+
this.filesystem = options.filesystem;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Create a new markdown model.
|
|
77
|
+
* @param options - Configuration options
|
|
78
|
+
*/
|
|
79
|
+
static new(options) {
|
|
80
|
+
const viewport$1 = viewport.ViewportModel.new({
|
|
81
|
+
width: options.width ?? 0,
|
|
82
|
+
height: options.height ?? 0,
|
|
83
|
+
style: options.style
|
|
84
|
+
});
|
|
85
|
+
return new _MarkdownModel({
|
|
86
|
+
viewport: viewport$1,
|
|
87
|
+
active: options.active ?? true,
|
|
88
|
+
fileName: "",
|
|
89
|
+
filesystem: options.filesystem
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Tea init hook (no-op).
|
|
94
|
+
*/
|
|
95
|
+
init() {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Set the filename to render. Returns a command that will read and render the file.
|
|
100
|
+
* @param fileName - Path to the markdown file
|
|
101
|
+
*/
|
|
102
|
+
setFileName(fileName) {
|
|
103
|
+
const updated = this.with({ fileName });
|
|
104
|
+
const cmd = renderMarkdownCmd(this.filesystem, this.viewport.width, fileName);
|
|
105
|
+
return [updated, cmd];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Set the size of the viewport and re-render if a file is set.
|
|
109
|
+
* @param width - New width
|
|
110
|
+
* @param height - New height
|
|
111
|
+
*/
|
|
112
|
+
setSize(width, height) {
|
|
113
|
+
const updatedViewport = this.viewport.setWidth(width).setHeight(height);
|
|
114
|
+
const updated = this.with({ viewport: updatedViewport });
|
|
115
|
+
if (this.fileName !== "") {
|
|
116
|
+
const cmd = renderMarkdownCmd(this.filesystem, width, this.fileName);
|
|
117
|
+
return [updated, cmd];
|
|
118
|
+
}
|
|
119
|
+
return [updated, null];
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Set whether the component is active and should handle input.
|
|
123
|
+
* @param active - Active state
|
|
124
|
+
*/
|
|
125
|
+
setIsActive(active) {
|
|
126
|
+
if (active === this.active) return this;
|
|
127
|
+
return this.with({ active });
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Scroll to the top of the viewport.
|
|
131
|
+
*/
|
|
132
|
+
gotoTop() {
|
|
133
|
+
const updatedViewport = this.viewport.scrollToTop();
|
|
134
|
+
if (updatedViewport === this.viewport) return this;
|
|
135
|
+
return this.with({ viewport: updatedViewport });
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Handle messages. Processes viewport scrolling and markdown rendering.
|
|
139
|
+
* @param msg - The message to handle
|
|
140
|
+
*/
|
|
141
|
+
update(msg) {
|
|
142
|
+
if (msg instanceof RenderMarkdownMsg) {
|
|
143
|
+
const styled = new chapstick.Style().width(this.viewport.width).alignHorizontal("left").render(msg.content);
|
|
144
|
+
const updatedViewport = this.viewport.setContent(styled);
|
|
145
|
+
return [this.with({ viewport: updatedViewport }), null];
|
|
146
|
+
}
|
|
147
|
+
if (msg instanceof ErrorMsg) {
|
|
148
|
+
const errorContent = msg.error.message;
|
|
149
|
+
const updatedViewport = this.viewport.setContent(errorContent);
|
|
150
|
+
return [
|
|
151
|
+
this.with({
|
|
152
|
+
fileName: "",
|
|
153
|
+
viewport: updatedViewport
|
|
154
|
+
}),
|
|
155
|
+
null
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
if (this.active) {
|
|
159
|
+
const [updatedViewport, cmd] = this.viewport.update(msg);
|
|
160
|
+
if (updatedViewport !== this.viewport) {
|
|
161
|
+
return [this.with({ viewport: updatedViewport }), cmd];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return [this, null];
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Render the markdown viewport.
|
|
168
|
+
*/
|
|
169
|
+
view() {
|
|
170
|
+
return this.viewport.view();
|
|
171
|
+
}
|
|
172
|
+
with(patch) {
|
|
173
|
+
return new _MarkdownModel({
|
|
174
|
+
viewport: patch.viewport ?? this.viewport,
|
|
175
|
+
active: patch.active ?? this.active,
|
|
176
|
+
fileName: patch.fileName ?? this.fileName,
|
|
177
|
+
filesystem: patch.filesystem ?? this.filesystem
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
function renderMarkdownCmd(fs, width, fileName) {
|
|
182
|
+
return async () => {
|
|
183
|
+
try {
|
|
184
|
+
const content = await filesystem.readFileContent(fs, fileName);
|
|
185
|
+
const rendered = renderMarkdown(content, { width });
|
|
186
|
+
return new RenderMarkdownMsg(rendered);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
return new ErrorMsg(
|
|
189
|
+
error instanceof Error ? error : new Error(String(error))
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
exports.ErrorMsg = ErrorMsg;
|
|
196
|
+
exports.MarkdownModel = MarkdownModel;
|
|
197
|
+
exports.RenderMarkdownMsg = RenderMarkdownMsg;
|
|
198
|
+
exports.renderMarkdown = renderMarkdown;
|
|
199
|
+
//# sourceMappingURL=index.cjs.map
|
|
200
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/messages.ts","../src/renderer.ts","../src/model.ts"],"names":["createAlwaysEnabledStyle","marked","Marked","markedTerminal","viewport","ViewportModel","Style","readFileContent"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Message types for the markdown component.\n */\n\n/**\n * Message containing rendered markdown content.\n * @public\n */\nexport class RenderMarkdownMsg {\n readonly _tag = 'markdown-render'\n\n constructor(public readonly content: string) {}\n}\n\n/**\n * Message containing an error from file reading or rendering.\n * @public\n */\nexport class ErrorMsg {\n readonly _tag = 'markdown-error'\n\n constructor(public readonly error: Error) {}\n}\n","/**\n * Markdown rendering utilities.\n */\n\nimport { Marked } from 'marked'\nimport { markedTerminal } from 'marked-terminal'\nimport type { EnvironmentAdapter, TerminalBackground } from '@boba-cli/machine'\nimport { createAlwaysEnabledStyle } from '@boba-cli/machine'\n\n/**\n * Options for rendering markdown.\n * @public\n */\nexport interface RenderMarkdownOptions {\n /**\n * Width for word wrapping. Defaults to 80.\n */\n width?: number\n /**\n * Terminal background mode. Defaults to 'dark'.\n */\n background?: TerminalBackground\n /**\n * Environment adapter for detecting terminal capabilities.\n */\n env?: EnvironmentAdapter\n}\n\n/**\n * Renders markdown content with terminal styling.\n * Detects terminal background (light/dark) and applies appropriate styling.\n *\n * @param content - The markdown string to render\n * @param options - Rendering options\n * @returns The styled markdown output\n * @public\n */\nexport function renderMarkdown(\n content: string,\n options: RenderMarkdownOptions = {},\n): string {\n const width = options.width ?? 80\n const background = options.background ?? options.env?.getTerminalBackground() ?? 'dark'\n\n // Use appropriate colors for terminal background\n const isDark = background !== 'light'\n\n // Create a style function with full color support for markdown rendering\n const style = createAlwaysEnabledStyle()\n\n // Create marked instance with terminal renderer\n const marked = new Marked(\n markedTerminal({\n // Wrap text at specified width\n width,\n reflowText: true,\n // Headings - brighter on dark backgrounds\n firstHeading: isDark ? style.cyan.bold : style.blue.bold,\n heading: isDark ? style.cyan.bold : style.blue.bold,\n // Code blocks\n code: isDark ? style.white : style.gray,\n blockquote: isDark ? style.white : style.gray,\n // Emphasis\n strong: style.bold,\n em: style.italic,\n // Lists\n listitem: style,\n // Links\n link: isDark ? style.blueBright : style.blue,\n // Other elements\n hr: style.gray,\n paragraph: style,\n }),\n )\n\n try {\n const rendered = marked.parse(content) as string\n return rendered.trim()\n } catch (error) {\n throw new Error(\n `Failed to render markdown: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n}\n","/**\n * Markdown viewer component.\n */\n\nimport { Style } from '@boba-cli/chapstick'\nimport { readFileContent } from '@boba-cli/filesystem'\nimport { ViewportModel } from '@boba-cli/viewport'\nimport type { Cmd, Msg } from '@boba-cli/tea'\nimport type { FileSystemAdapter } from '@boba-cli/machine'\nimport { RenderMarkdownMsg, ErrorMsg } from './messages.js'\nimport { renderMarkdown } from './renderer.js'\n\n/**\n * Options for creating a markdown model.\n * @public\n */\nexport interface MarkdownOptions {\n /**\n * Filesystem adapter for file operations.\n */\n filesystem: FileSystemAdapter\n /**\n * Whether the component is active and should handle input.\n * Defaults to true.\n */\n active?: boolean\n /**\n * Initial width for the viewport.\n * Defaults to 0.\n */\n width?: number\n /**\n * Initial height for the viewport.\n * Defaults to 0.\n */\n height?: number\n /**\n * Style for the viewport.\n */\n style?: Style\n}\n\n/**\n * Markdown viewer model that renders markdown files with terminal styling\n * in a scrollable viewport.\n * @public\n */\nexport class MarkdownModel {\n readonly viewport: ViewportModel\n readonly active: boolean\n readonly fileName: string\n readonly filesystem: FileSystemAdapter\n\n private constructor(options: {\n viewport: ViewportModel\n active: boolean\n fileName: string\n filesystem: FileSystemAdapter\n }) {\n this.viewport = options.viewport\n this.active = options.active\n this.fileName = options.fileName\n this.filesystem = options.filesystem\n }\n\n /**\n * Create a new markdown model.\n * @param options - Configuration options\n */\n static new(options: MarkdownOptions): MarkdownModel {\n const viewport = ViewportModel.new({\n width: options.width ?? 0,\n height: options.height ?? 0,\n style: options.style,\n })\n\n return new MarkdownModel({\n viewport,\n active: options.active ?? true,\n fileName: '',\n filesystem: options.filesystem,\n })\n }\n\n /**\n * Tea init hook (no-op).\n */\n init(): Cmd<Msg> {\n return null\n }\n\n /**\n * Set the filename to render. Returns a command that will read and render the file.\n * @param fileName - Path to the markdown file\n */\n setFileName(fileName: string): [MarkdownModel, Cmd<Msg>] {\n const updated = this.with({ fileName })\n const cmd = renderMarkdownCmd(this.filesystem, this.viewport.width, fileName)\n return [updated, cmd]\n }\n\n /**\n * Set the size of the viewport and re-render if a file is set.\n * @param width - New width\n * @param height - New height\n */\n setSize(width: number, height: number): [MarkdownModel, Cmd<Msg>] {\n const updatedViewport = this.viewport.setWidth(width).setHeight(height)\n const updated = this.with({ viewport: updatedViewport })\n\n if (this.fileName !== '') {\n const cmd = renderMarkdownCmd(this.filesystem, width, this.fileName)\n return [updated, cmd]\n }\n\n return [updated, null]\n }\n\n /**\n * Set whether the component is active and should handle input.\n * @param active - Active state\n */\n setIsActive(active: boolean): MarkdownModel {\n if (active === this.active) return this\n return this.with({ active })\n }\n\n /**\n * Scroll to the top of the viewport.\n */\n gotoTop(): MarkdownModel {\n const updatedViewport = this.viewport.scrollToTop()\n if (updatedViewport === this.viewport) return this\n return this.with({ viewport: updatedViewport })\n }\n\n /**\n * Handle messages. Processes viewport scrolling and markdown rendering.\n * @param msg - The message to handle\n */\n update(msg: Msg): [MarkdownModel, Cmd<Msg>] {\n // Handle markdown rendering\n if (msg instanceof RenderMarkdownMsg) {\n // Apply width for word wrapping and left-align to pad lines to consistent width\n // Viewport handles height/scrolling\n const styled = new Style()\n .width(this.viewport.width)\n .alignHorizontal('left')\n .render(msg.content)\n\n const updatedViewport = this.viewport.setContent(styled)\n return [this.with({ viewport: updatedViewport }), null]\n }\n\n // Handle errors\n if (msg instanceof ErrorMsg) {\n const errorContent = msg.error.message\n const updatedViewport = this.viewport.setContent(errorContent)\n return [\n this.with({\n fileName: '',\n viewport: updatedViewport,\n }),\n null,\n ]\n }\n\n // Handle viewport updates if active\n if (this.active) {\n const [updatedViewport, cmd] = this.viewport.update(msg)\n if (updatedViewport !== this.viewport) {\n return [this.with({ viewport: updatedViewport }), cmd]\n }\n }\n\n return [this, null]\n }\n\n /**\n * Render the markdown viewport.\n */\n view(): string {\n return this.viewport.view()\n }\n\n private with(patch: Partial<MarkdownModel>): MarkdownModel {\n return new MarkdownModel({\n viewport: patch.viewport ?? this.viewport,\n active: patch.active ?? this.active,\n fileName: patch.fileName ?? this.fileName,\n filesystem: patch.filesystem ?? this.filesystem,\n })\n }\n}\n\n/**\n * Command to read and render a markdown file.\n */\nfunction renderMarkdownCmd(\n fs: FileSystemAdapter,\n width: number,\n fileName: string,\n): Cmd<Msg> {\n return async () => {\n try {\n const content = await readFileContent(fs, fileName)\n const rendered = renderMarkdown(content, { width })\n return new RenderMarkdownMsg(rendered)\n } catch (error) {\n return new ErrorMsg(\n error instanceof Error ? error : new Error(String(error)),\n )\n }\n }\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Style } from '@boba-cli/chapstick';
|
|
2
|
+
import { ViewportModel } from '@boba-cli/viewport';
|
|
3
|
+
import { Cmd, Msg } from '@boba-cli/tea';
|
|
4
|
+
import { FileSystemAdapter, TerminalBackground, EnvironmentAdapter } from '@boba-cli/machine';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Markdown viewer component.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Options for creating a markdown model.
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
interface MarkdownOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Filesystem adapter for file operations.
|
|
17
|
+
*/
|
|
18
|
+
filesystem: FileSystemAdapter;
|
|
19
|
+
/**
|
|
20
|
+
* Whether the component is active and should handle input.
|
|
21
|
+
* Defaults to true.
|
|
22
|
+
*/
|
|
23
|
+
active?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Initial width for the viewport.
|
|
26
|
+
* Defaults to 0.
|
|
27
|
+
*/
|
|
28
|
+
width?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Initial height for the viewport.
|
|
31
|
+
* Defaults to 0.
|
|
32
|
+
*/
|
|
33
|
+
height?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Style for the viewport.
|
|
36
|
+
*/
|
|
37
|
+
style?: Style;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Markdown viewer model that renders markdown files with terminal styling
|
|
41
|
+
* in a scrollable viewport.
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
declare class MarkdownModel {
|
|
45
|
+
readonly viewport: ViewportModel;
|
|
46
|
+
readonly active: boolean;
|
|
47
|
+
readonly fileName: string;
|
|
48
|
+
readonly filesystem: FileSystemAdapter;
|
|
49
|
+
private constructor();
|
|
50
|
+
/**
|
|
51
|
+
* Create a new markdown model.
|
|
52
|
+
* @param options - Configuration options
|
|
53
|
+
*/
|
|
54
|
+
static new(options: MarkdownOptions): MarkdownModel;
|
|
55
|
+
/**
|
|
56
|
+
* Tea init hook (no-op).
|
|
57
|
+
*/
|
|
58
|
+
init(): Cmd<Msg>;
|
|
59
|
+
/**
|
|
60
|
+
* Set the filename to render. Returns a command that will read and render the file.
|
|
61
|
+
* @param fileName - Path to the markdown file
|
|
62
|
+
*/
|
|
63
|
+
setFileName(fileName: string): [MarkdownModel, Cmd<Msg>];
|
|
64
|
+
/**
|
|
65
|
+
* Set the size of the viewport and re-render if a file is set.
|
|
66
|
+
* @param width - New width
|
|
67
|
+
* @param height - New height
|
|
68
|
+
*/
|
|
69
|
+
setSize(width: number, height: number): [MarkdownModel, Cmd<Msg>];
|
|
70
|
+
/**
|
|
71
|
+
* Set whether the component is active and should handle input.
|
|
72
|
+
* @param active - Active state
|
|
73
|
+
*/
|
|
74
|
+
setIsActive(active: boolean): MarkdownModel;
|
|
75
|
+
/**
|
|
76
|
+
* Scroll to the top of the viewport.
|
|
77
|
+
*/
|
|
78
|
+
gotoTop(): MarkdownModel;
|
|
79
|
+
/**
|
|
80
|
+
* Handle messages. Processes viewport scrolling and markdown rendering.
|
|
81
|
+
* @param msg - The message to handle
|
|
82
|
+
*/
|
|
83
|
+
update(msg: Msg): [MarkdownModel, Cmd<Msg>];
|
|
84
|
+
/**
|
|
85
|
+
* Render the markdown viewport.
|
|
86
|
+
*/
|
|
87
|
+
view(): string;
|
|
88
|
+
private with;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Message types for the markdown component.
|
|
93
|
+
*/
|
|
94
|
+
/**
|
|
95
|
+
* Message containing rendered markdown content.
|
|
96
|
+
* @public
|
|
97
|
+
*/
|
|
98
|
+
declare class RenderMarkdownMsg {
|
|
99
|
+
readonly content: string;
|
|
100
|
+
readonly _tag = "markdown-render";
|
|
101
|
+
constructor(content: string);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Message containing an error from file reading or rendering.
|
|
105
|
+
* @public
|
|
106
|
+
*/
|
|
107
|
+
declare class ErrorMsg {
|
|
108
|
+
readonly error: Error;
|
|
109
|
+
readonly _tag = "markdown-error";
|
|
110
|
+
constructor(error: Error);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Markdown rendering utilities.
|
|
115
|
+
*/
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Options for rendering markdown.
|
|
119
|
+
* @public
|
|
120
|
+
*/
|
|
121
|
+
interface RenderMarkdownOptions {
|
|
122
|
+
/**
|
|
123
|
+
* Width for word wrapping. Defaults to 80.
|
|
124
|
+
*/
|
|
125
|
+
width?: number;
|
|
126
|
+
/**
|
|
127
|
+
* Terminal background mode. Defaults to 'dark'.
|
|
128
|
+
*/
|
|
129
|
+
background?: TerminalBackground;
|
|
130
|
+
/**
|
|
131
|
+
* Environment adapter for detecting terminal capabilities.
|
|
132
|
+
*/
|
|
133
|
+
env?: EnvironmentAdapter;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Renders markdown content with terminal styling.
|
|
137
|
+
* Detects terminal background (light/dark) and applies appropriate styling.
|
|
138
|
+
*
|
|
139
|
+
* @param content - The markdown string to render
|
|
140
|
+
* @param options - Rendering options
|
|
141
|
+
* @returns The styled markdown output
|
|
142
|
+
* @public
|
|
143
|
+
*/
|
|
144
|
+
declare function renderMarkdown(content: string, options?: RenderMarkdownOptions): string;
|
|
145
|
+
|
|
146
|
+
export { ErrorMsg, MarkdownModel, type MarkdownOptions, RenderMarkdownMsg, type RenderMarkdownOptions, renderMarkdown };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Style } from '@boba-cli/chapstick';
|
|
2
|
+
import { ViewportModel } from '@boba-cli/viewport';
|
|
3
|
+
import { Cmd, Msg } from '@boba-cli/tea';
|
|
4
|
+
import { FileSystemAdapter, TerminalBackground, EnvironmentAdapter } from '@boba-cli/machine';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Markdown viewer component.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Options for creating a markdown model.
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
interface MarkdownOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Filesystem adapter for file operations.
|
|
17
|
+
*/
|
|
18
|
+
filesystem: FileSystemAdapter;
|
|
19
|
+
/**
|
|
20
|
+
* Whether the component is active and should handle input.
|
|
21
|
+
* Defaults to true.
|
|
22
|
+
*/
|
|
23
|
+
active?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Initial width for the viewport.
|
|
26
|
+
* Defaults to 0.
|
|
27
|
+
*/
|
|
28
|
+
width?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Initial height for the viewport.
|
|
31
|
+
* Defaults to 0.
|
|
32
|
+
*/
|
|
33
|
+
height?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Style for the viewport.
|
|
36
|
+
*/
|
|
37
|
+
style?: Style;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Markdown viewer model that renders markdown files with terminal styling
|
|
41
|
+
* in a scrollable viewport.
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
declare class MarkdownModel {
|
|
45
|
+
readonly viewport: ViewportModel;
|
|
46
|
+
readonly active: boolean;
|
|
47
|
+
readonly fileName: string;
|
|
48
|
+
readonly filesystem: FileSystemAdapter;
|
|
49
|
+
private constructor();
|
|
50
|
+
/**
|
|
51
|
+
* Create a new markdown model.
|
|
52
|
+
* @param options - Configuration options
|
|
53
|
+
*/
|
|
54
|
+
static new(options: MarkdownOptions): MarkdownModel;
|
|
55
|
+
/**
|
|
56
|
+
* Tea init hook (no-op).
|
|
57
|
+
*/
|
|
58
|
+
init(): Cmd<Msg>;
|
|
59
|
+
/**
|
|
60
|
+
* Set the filename to render. Returns a command that will read and render the file.
|
|
61
|
+
* @param fileName - Path to the markdown file
|
|
62
|
+
*/
|
|
63
|
+
setFileName(fileName: string): [MarkdownModel, Cmd<Msg>];
|
|
64
|
+
/**
|
|
65
|
+
* Set the size of the viewport and re-render if a file is set.
|
|
66
|
+
* @param width - New width
|
|
67
|
+
* @param height - New height
|
|
68
|
+
*/
|
|
69
|
+
setSize(width: number, height: number): [MarkdownModel, Cmd<Msg>];
|
|
70
|
+
/**
|
|
71
|
+
* Set whether the component is active and should handle input.
|
|
72
|
+
* @param active - Active state
|
|
73
|
+
*/
|
|
74
|
+
setIsActive(active: boolean): MarkdownModel;
|
|
75
|
+
/**
|
|
76
|
+
* Scroll to the top of the viewport.
|
|
77
|
+
*/
|
|
78
|
+
gotoTop(): MarkdownModel;
|
|
79
|
+
/**
|
|
80
|
+
* Handle messages. Processes viewport scrolling and markdown rendering.
|
|
81
|
+
* @param msg - The message to handle
|
|
82
|
+
*/
|
|
83
|
+
update(msg: Msg): [MarkdownModel, Cmd<Msg>];
|
|
84
|
+
/**
|
|
85
|
+
* Render the markdown viewport.
|
|
86
|
+
*/
|
|
87
|
+
view(): string;
|
|
88
|
+
private with;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Message types for the markdown component.
|
|
93
|
+
*/
|
|
94
|
+
/**
|
|
95
|
+
* Message containing rendered markdown content.
|
|
96
|
+
* @public
|
|
97
|
+
*/
|
|
98
|
+
declare class RenderMarkdownMsg {
|
|
99
|
+
readonly content: string;
|
|
100
|
+
readonly _tag = "markdown-render";
|
|
101
|
+
constructor(content: string);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Message containing an error from file reading or rendering.
|
|
105
|
+
* @public
|
|
106
|
+
*/
|
|
107
|
+
declare class ErrorMsg {
|
|
108
|
+
readonly error: Error;
|
|
109
|
+
readonly _tag = "markdown-error";
|
|
110
|
+
constructor(error: Error);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Markdown rendering utilities.
|
|
115
|
+
*/
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Options for rendering markdown.
|
|
119
|
+
* @public
|
|
120
|
+
*/
|
|
121
|
+
interface RenderMarkdownOptions {
|
|
122
|
+
/**
|
|
123
|
+
* Width for word wrapping. Defaults to 80.
|
|
124
|
+
*/
|
|
125
|
+
width?: number;
|
|
126
|
+
/**
|
|
127
|
+
* Terminal background mode. Defaults to 'dark'.
|
|
128
|
+
*/
|
|
129
|
+
background?: TerminalBackground;
|
|
130
|
+
/**
|
|
131
|
+
* Environment adapter for detecting terminal capabilities.
|
|
132
|
+
*/
|
|
133
|
+
env?: EnvironmentAdapter;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Renders markdown content with terminal styling.
|
|
137
|
+
* Detects terminal background (light/dark) and applies appropriate styling.
|
|
138
|
+
*
|
|
139
|
+
* @param content - The markdown string to render
|
|
140
|
+
* @param options - Rendering options
|
|
141
|
+
* @returns The styled markdown output
|
|
142
|
+
* @public
|
|
143
|
+
*/
|
|
144
|
+
declare function renderMarkdown(content: string, options?: RenderMarkdownOptions): string;
|
|
145
|
+
|
|
146
|
+
export { ErrorMsg, MarkdownModel, type MarkdownOptions, RenderMarkdownMsg, type RenderMarkdownOptions, renderMarkdown };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { Style } from '@boba-cli/chapstick';
|
|
2
|
+
import { readFileContent } from '@boba-cli/filesystem';
|
|
3
|
+
import { ViewportModel } from '@boba-cli/viewport';
|
|
4
|
+
import { Marked } from 'marked';
|
|
5
|
+
import { markedTerminal } from 'marked-terminal';
|
|
6
|
+
import { createAlwaysEnabledStyle } from '@boba-cli/machine';
|
|
7
|
+
|
|
8
|
+
// src/model.ts
|
|
9
|
+
|
|
10
|
+
// src/messages.ts
|
|
11
|
+
var RenderMarkdownMsg = class {
|
|
12
|
+
constructor(content) {
|
|
13
|
+
this.content = content;
|
|
14
|
+
}
|
|
15
|
+
_tag = "markdown-render";
|
|
16
|
+
};
|
|
17
|
+
var ErrorMsg = class {
|
|
18
|
+
constructor(error) {
|
|
19
|
+
this.error = error;
|
|
20
|
+
}
|
|
21
|
+
_tag = "markdown-error";
|
|
22
|
+
};
|
|
23
|
+
function renderMarkdown(content, options = {}) {
|
|
24
|
+
const width = options.width ?? 80;
|
|
25
|
+
const background = options.background ?? options.env?.getTerminalBackground() ?? "dark";
|
|
26
|
+
const isDark = background !== "light";
|
|
27
|
+
const style = createAlwaysEnabledStyle();
|
|
28
|
+
const marked = new Marked(
|
|
29
|
+
markedTerminal({
|
|
30
|
+
// Wrap text at specified width
|
|
31
|
+
width,
|
|
32
|
+
reflowText: true,
|
|
33
|
+
// Headings - brighter on dark backgrounds
|
|
34
|
+
firstHeading: isDark ? style.cyan.bold : style.blue.bold,
|
|
35
|
+
heading: isDark ? style.cyan.bold : style.blue.bold,
|
|
36
|
+
// Code blocks
|
|
37
|
+
code: isDark ? style.white : style.gray,
|
|
38
|
+
blockquote: isDark ? style.white : style.gray,
|
|
39
|
+
// Emphasis
|
|
40
|
+
strong: style.bold,
|
|
41
|
+
em: style.italic,
|
|
42
|
+
// Lists
|
|
43
|
+
listitem: style,
|
|
44
|
+
// Links
|
|
45
|
+
link: isDark ? style.blueBright : style.blue,
|
|
46
|
+
// Other elements
|
|
47
|
+
hr: style.gray,
|
|
48
|
+
paragraph: style
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
try {
|
|
52
|
+
const rendered = marked.parse(content);
|
|
53
|
+
return rendered.trim();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Failed to render markdown: ${error instanceof Error ? error.message : String(error)}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/model.ts
|
|
62
|
+
var MarkdownModel = class _MarkdownModel {
|
|
63
|
+
viewport;
|
|
64
|
+
active;
|
|
65
|
+
fileName;
|
|
66
|
+
filesystem;
|
|
67
|
+
constructor(options) {
|
|
68
|
+
this.viewport = options.viewport;
|
|
69
|
+
this.active = options.active;
|
|
70
|
+
this.fileName = options.fileName;
|
|
71
|
+
this.filesystem = options.filesystem;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Create a new markdown model.
|
|
75
|
+
* @param options - Configuration options
|
|
76
|
+
*/
|
|
77
|
+
static new(options) {
|
|
78
|
+
const viewport = ViewportModel.new({
|
|
79
|
+
width: options.width ?? 0,
|
|
80
|
+
height: options.height ?? 0,
|
|
81
|
+
style: options.style
|
|
82
|
+
});
|
|
83
|
+
return new _MarkdownModel({
|
|
84
|
+
viewport,
|
|
85
|
+
active: options.active ?? true,
|
|
86
|
+
fileName: "",
|
|
87
|
+
filesystem: options.filesystem
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Tea init hook (no-op).
|
|
92
|
+
*/
|
|
93
|
+
init() {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Set the filename to render. Returns a command that will read and render the file.
|
|
98
|
+
* @param fileName - Path to the markdown file
|
|
99
|
+
*/
|
|
100
|
+
setFileName(fileName) {
|
|
101
|
+
const updated = this.with({ fileName });
|
|
102
|
+
const cmd = renderMarkdownCmd(this.filesystem, this.viewport.width, fileName);
|
|
103
|
+
return [updated, cmd];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Set the size of the viewport and re-render if a file is set.
|
|
107
|
+
* @param width - New width
|
|
108
|
+
* @param height - New height
|
|
109
|
+
*/
|
|
110
|
+
setSize(width, height) {
|
|
111
|
+
const updatedViewport = this.viewport.setWidth(width).setHeight(height);
|
|
112
|
+
const updated = this.with({ viewport: updatedViewport });
|
|
113
|
+
if (this.fileName !== "") {
|
|
114
|
+
const cmd = renderMarkdownCmd(this.filesystem, width, this.fileName);
|
|
115
|
+
return [updated, cmd];
|
|
116
|
+
}
|
|
117
|
+
return [updated, null];
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Set whether the component is active and should handle input.
|
|
121
|
+
* @param active - Active state
|
|
122
|
+
*/
|
|
123
|
+
setIsActive(active) {
|
|
124
|
+
if (active === this.active) return this;
|
|
125
|
+
return this.with({ active });
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Scroll to the top of the viewport.
|
|
129
|
+
*/
|
|
130
|
+
gotoTop() {
|
|
131
|
+
const updatedViewport = this.viewport.scrollToTop();
|
|
132
|
+
if (updatedViewport === this.viewport) return this;
|
|
133
|
+
return this.with({ viewport: updatedViewport });
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Handle messages. Processes viewport scrolling and markdown rendering.
|
|
137
|
+
* @param msg - The message to handle
|
|
138
|
+
*/
|
|
139
|
+
update(msg) {
|
|
140
|
+
if (msg instanceof RenderMarkdownMsg) {
|
|
141
|
+
const styled = new Style().width(this.viewport.width).alignHorizontal("left").render(msg.content);
|
|
142
|
+
const updatedViewport = this.viewport.setContent(styled);
|
|
143
|
+
return [this.with({ viewport: updatedViewport }), null];
|
|
144
|
+
}
|
|
145
|
+
if (msg instanceof ErrorMsg) {
|
|
146
|
+
const errorContent = msg.error.message;
|
|
147
|
+
const updatedViewport = this.viewport.setContent(errorContent);
|
|
148
|
+
return [
|
|
149
|
+
this.with({
|
|
150
|
+
fileName: "",
|
|
151
|
+
viewport: updatedViewport
|
|
152
|
+
}),
|
|
153
|
+
null
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
if (this.active) {
|
|
157
|
+
const [updatedViewport, cmd] = this.viewport.update(msg);
|
|
158
|
+
if (updatedViewport !== this.viewport) {
|
|
159
|
+
return [this.with({ viewport: updatedViewport }), cmd];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return [this, null];
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Render the markdown viewport.
|
|
166
|
+
*/
|
|
167
|
+
view() {
|
|
168
|
+
return this.viewport.view();
|
|
169
|
+
}
|
|
170
|
+
with(patch) {
|
|
171
|
+
return new _MarkdownModel({
|
|
172
|
+
viewport: patch.viewport ?? this.viewport,
|
|
173
|
+
active: patch.active ?? this.active,
|
|
174
|
+
fileName: patch.fileName ?? this.fileName,
|
|
175
|
+
filesystem: patch.filesystem ?? this.filesystem
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
function renderMarkdownCmd(fs, width, fileName) {
|
|
180
|
+
return async () => {
|
|
181
|
+
try {
|
|
182
|
+
const content = await readFileContent(fs, fileName);
|
|
183
|
+
const rendered = renderMarkdown(content, { width });
|
|
184
|
+
return new RenderMarkdownMsg(rendered);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return new ErrorMsg(
|
|
187
|
+
error instanceof Error ? error : new Error(String(error))
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export { ErrorMsg, MarkdownModel, RenderMarkdownMsg, renderMarkdown };
|
|
194
|
+
//# sourceMappingURL=index.js.map
|
|
195
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/messages.ts","../src/renderer.ts","../src/model.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Message types for the markdown component.\n */\n\n/**\n * Message containing rendered markdown content.\n * @public\n */\nexport class RenderMarkdownMsg {\n readonly _tag = 'markdown-render'\n\n constructor(public readonly content: string) {}\n}\n\n/**\n * Message containing an error from file reading or rendering.\n * @public\n */\nexport class ErrorMsg {\n readonly _tag = 'markdown-error'\n\n constructor(public readonly error: Error) {}\n}\n","/**\n * Markdown rendering utilities.\n */\n\nimport { Marked } from 'marked'\nimport { markedTerminal } from 'marked-terminal'\nimport type { EnvironmentAdapter, TerminalBackground } from '@boba-cli/machine'\nimport { createAlwaysEnabledStyle } from '@boba-cli/machine'\n\n/**\n * Options for rendering markdown.\n * @public\n */\nexport interface RenderMarkdownOptions {\n /**\n * Width for word wrapping. Defaults to 80.\n */\n width?: number\n /**\n * Terminal background mode. Defaults to 'dark'.\n */\n background?: TerminalBackground\n /**\n * Environment adapter for detecting terminal capabilities.\n */\n env?: EnvironmentAdapter\n}\n\n/**\n * Renders markdown content with terminal styling.\n * Detects terminal background (light/dark) and applies appropriate styling.\n *\n * @param content - The markdown string to render\n * @param options - Rendering options\n * @returns The styled markdown output\n * @public\n */\nexport function renderMarkdown(\n content: string,\n options: RenderMarkdownOptions = {},\n): string {\n const width = options.width ?? 80\n const background = options.background ?? options.env?.getTerminalBackground() ?? 'dark'\n\n // Use appropriate colors for terminal background\n const isDark = background !== 'light'\n\n // Create a style function with full color support for markdown rendering\n const style = createAlwaysEnabledStyle()\n\n // Create marked instance with terminal renderer\n const marked = new Marked(\n markedTerminal({\n // Wrap text at specified width\n width,\n reflowText: true,\n // Headings - brighter on dark backgrounds\n firstHeading: isDark ? style.cyan.bold : style.blue.bold,\n heading: isDark ? style.cyan.bold : style.blue.bold,\n // Code blocks\n code: isDark ? style.white : style.gray,\n blockquote: isDark ? style.white : style.gray,\n // Emphasis\n strong: style.bold,\n em: style.italic,\n // Lists\n listitem: style,\n // Links\n link: isDark ? style.blueBright : style.blue,\n // Other elements\n hr: style.gray,\n paragraph: style,\n }),\n )\n\n try {\n const rendered = marked.parse(content) as string\n return rendered.trim()\n } catch (error) {\n throw new Error(\n `Failed to render markdown: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n}\n","/**\n * Markdown viewer component.\n */\n\nimport { Style } from '@boba-cli/chapstick'\nimport { readFileContent } from '@boba-cli/filesystem'\nimport { ViewportModel } from '@boba-cli/viewport'\nimport type { Cmd, Msg } from '@boba-cli/tea'\nimport type { FileSystemAdapter } from '@boba-cli/machine'\nimport { RenderMarkdownMsg, ErrorMsg } from './messages.js'\nimport { renderMarkdown } from './renderer.js'\n\n/**\n * Options for creating a markdown model.\n * @public\n */\nexport interface MarkdownOptions {\n /**\n * Filesystem adapter for file operations.\n */\n filesystem: FileSystemAdapter\n /**\n * Whether the component is active and should handle input.\n * Defaults to true.\n */\n active?: boolean\n /**\n * Initial width for the viewport.\n * Defaults to 0.\n */\n width?: number\n /**\n * Initial height for the viewport.\n * Defaults to 0.\n */\n height?: number\n /**\n * Style for the viewport.\n */\n style?: Style\n}\n\n/**\n * Markdown viewer model that renders markdown files with terminal styling\n * in a scrollable viewport.\n * @public\n */\nexport class MarkdownModel {\n readonly viewport: ViewportModel\n readonly active: boolean\n readonly fileName: string\n readonly filesystem: FileSystemAdapter\n\n private constructor(options: {\n viewport: ViewportModel\n active: boolean\n fileName: string\n filesystem: FileSystemAdapter\n }) {\n this.viewport = options.viewport\n this.active = options.active\n this.fileName = options.fileName\n this.filesystem = options.filesystem\n }\n\n /**\n * Create a new markdown model.\n * @param options - Configuration options\n */\n static new(options: MarkdownOptions): MarkdownModel {\n const viewport = ViewportModel.new({\n width: options.width ?? 0,\n height: options.height ?? 0,\n style: options.style,\n })\n\n return new MarkdownModel({\n viewport,\n active: options.active ?? true,\n fileName: '',\n filesystem: options.filesystem,\n })\n }\n\n /**\n * Tea init hook (no-op).\n */\n init(): Cmd<Msg> {\n return null\n }\n\n /**\n * Set the filename to render. Returns a command that will read and render the file.\n * @param fileName - Path to the markdown file\n */\n setFileName(fileName: string): [MarkdownModel, Cmd<Msg>] {\n const updated = this.with({ fileName })\n const cmd = renderMarkdownCmd(this.filesystem, this.viewport.width, fileName)\n return [updated, cmd]\n }\n\n /**\n * Set the size of the viewport and re-render if a file is set.\n * @param width - New width\n * @param height - New height\n */\n setSize(width: number, height: number): [MarkdownModel, Cmd<Msg>] {\n const updatedViewport = this.viewport.setWidth(width).setHeight(height)\n const updated = this.with({ viewport: updatedViewport })\n\n if (this.fileName !== '') {\n const cmd = renderMarkdownCmd(this.filesystem, width, this.fileName)\n return [updated, cmd]\n }\n\n return [updated, null]\n }\n\n /**\n * Set whether the component is active and should handle input.\n * @param active - Active state\n */\n setIsActive(active: boolean): MarkdownModel {\n if (active === this.active) return this\n return this.with({ active })\n }\n\n /**\n * Scroll to the top of the viewport.\n */\n gotoTop(): MarkdownModel {\n const updatedViewport = this.viewport.scrollToTop()\n if (updatedViewport === this.viewport) return this\n return this.with({ viewport: updatedViewport })\n }\n\n /**\n * Handle messages. Processes viewport scrolling and markdown rendering.\n * @param msg - The message to handle\n */\n update(msg: Msg): [MarkdownModel, Cmd<Msg>] {\n // Handle markdown rendering\n if (msg instanceof RenderMarkdownMsg) {\n // Apply width for word wrapping and left-align to pad lines to consistent width\n // Viewport handles height/scrolling\n const styled = new Style()\n .width(this.viewport.width)\n .alignHorizontal('left')\n .render(msg.content)\n\n const updatedViewport = this.viewport.setContent(styled)\n return [this.with({ viewport: updatedViewport }), null]\n }\n\n // Handle errors\n if (msg instanceof ErrorMsg) {\n const errorContent = msg.error.message\n const updatedViewport = this.viewport.setContent(errorContent)\n return [\n this.with({\n fileName: '',\n viewport: updatedViewport,\n }),\n null,\n ]\n }\n\n // Handle viewport updates if active\n if (this.active) {\n const [updatedViewport, cmd] = this.viewport.update(msg)\n if (updatedViewport !== this.viewport) {\n return [this.with({ viewport: updatedViewport }), cmd]\n }\n }\n\n return [this, null]\n }\n\n /**\n * Render the markdown viewport.\n */\n view(): string {\n return this.viewport.view()\n }\n\n private with(patch: Partial<MarkdownModel>): MarkdownModel {\n return new MarkdownModel({\n viewport: patch.viewport ?? this.viewport,\n active: patch.active ?? this.active,\n fileName: patch.fileName ?? this.fileName,\n filesystem: patch.filesystem ?? this.filesystem,\n })\n }\n}\n\n/**\n * Command to read and render a markdown file.\n */\nfunction renderMarkdownCmd(\n fs: FileSystemAdapter,\n width: number,\n fileName: string,\n): Cmd<Msg> {\n return async () => {\n try {\n const content = await readFileContent(fs, fileName)\n const rendered = renderMarkdown(content, { width })\n return new RenderMarkdownMsg(rendered)\n } catch (error) {\n return new ErrorMsg(\n error instanceof Error ? error : new Error(String(error)),\n )\n }\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@boba-cli/markdown",
|
|
3
|
+
"description": "Markdown viewer component for Boba terminal UIs",
|
|
4
|
+
"version": "0.1.0-alpha.2",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"marked": "^15.0.0",
|
|
7
|
+
"marked-terminal": "^7.3.0",
|
|
8
|
+
"@boba-cli/chapstick": "0.1.0-alpha.2",
|
|
9
|
+
"@boba-cli/filesystem": "0.1.0-alpha.2",
|
|
10
|
+
"@boba-cli/machine": "0.1.0-alpha.1",
|
|
11
|
+
"@boba-cli/tea": "0.1.0-alpha.1",
|
|
12
|
+
"@boba-cli/viewport": "0.1.0-alpha.2"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
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
|
+
}
|