@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 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"]}
@@ -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 };
@@ -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
+ }