ink-markdown-es 1.0.0

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,95 @@
1
+ # ink-markdown-es
2
+
3
+ A modern performance markdown renderer for [ink](https://github.com/vadimdemedes/ink) using [marked](https://github.com/markedjs/marked).
4
+
5
+ Inspired by [ink-markdown](https://github.com/vadimdemedes/ink-markdown) and [prompt-kit](https://github.com/ibelick/prompt-kit).
6
+
7
+ Compare with [ink-markdown](https://github.com/vadimdemedes/ink-markdown):
8
+
9
+ - **ES module** support & only
10
+ - Use memo & useMemo to improve performance
11
+ - More flexible configuration (`renderers` prop)
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ npm install ink-markdown-es # npm
17
+
18
+ pnpm add ink-markdown-es # pnpm
19
+
20
+ bun add ink-markdown-es # bun
21
+ ```
22
+
23
+ ```tsx
24
+ import Markdown from "ink-markdown-es";
25
+ import { render } from "ink";
26
+
27
+ const text = `# Hello World
28
+
29
+ This is a show case.
30
+ It's very fast!
31
+
32
+ ## Features
33
+ - Render markdown in ink
34
+ - Support custom renderers
35
+ - **Bold text** and *italic text*
36
+ - Inline \`code\` support
37
+
38
+ ### Code Block
39
+
40
+ \`\`\`javascript
41
+ const hello = "world";
42
+ console.log(hello);
43
+ \`\`\`
44
+
45
+ > This is a blockquote
46
+ > with multiple lines
47
+
48
+ ---
49
+
50
+ Check out [this link](https://example.com) for more info.
51
+
52
+ 1. First item
53
+ 2. Second item
54
+ 3. Third item
55
+
56
+ | Name | Age |
57
+ |------|-----|
58
+ | Alice | 25 |
59
+ | Bob | 30 |
60
+ `;
61
+
62
+ render(
63
+ <Markdown
64
+ showSharp
65
+ renderers={{
66
+ h1: (text) => (
67
+ <Box padding={1} borderStyle="round" borderDimColor>
68
+ <Text bold color="greenBright">
69
+ {text}
70
+ </Text>
71
+ </Box>
72
+ ),
73
+ }}
74
+ >
75
+ {text}
76
+ </Markdown>
77
+ );
78
+ ```
79
+
80
+ <img width="1919" height="689" alt="image" src="https://github.com/user-attachments/assets/d7cc741d-4c52-4b27-b183-ca8cce13007b" />
81
+
82
+
83
+ ## Contributing
84
+
85
+ To install dependencies:
86
+
87
+ ```bash
88
+ bun install
89
+ ```
90
+
91
+ To run:
92
+
93
+ ```bash
94
+ bun run dev
95
+ ```
@@ -0,0 +1,4 @@
1
+ import type { BlockStyles, BoxStyleProps, TextStyleProps } from './types';
2
+ export declare const DEFAULT_STYLES: BlockStyles;
3
+ export declare const TEXT_STYLE_KEYS: (keyof TextStyleProps)[];
4
+ export declare const BOX_STYLE_KEYS: (keyof BoxStyleProps)[];
@@ -0,0 +1,6 @@
1
+ import { type Token, type Tokens } from 'marked';
2
+ import type { MarkdownProps } from './types';
3
+ declare function MarkdownComponent({ children, styles, renderers, showSharp, }: MarkdownProps): import("react/jsx-runtime").JSX.Element;
4
+ declare const Markdown: import("react").MemoExoticComponent<typeof MarkdownComponent>;
5
+ export default Markdown;
6
+ export type { Token, Tokens };
package/dist/index.js ADDED
@@ -0,0 +1,389 @@
1
+ import { memo, useId, useMemo } from "react";
2
+ import { marked } from "marked";
3
+ import { Box, Text } from "ink";
4
+ const DEFAULT_STYLES = {
5
+ h1: {
6
+ bold: true,
7
+ marginBottom: 1,
8
+ color: '#f74cc7ff'
9
+ },
10
+ h2: {
11
+ bold: true,
12
+ marginBottom: 1,
13
+ color: '#326cfcff'
14
+ },
15
+ h3: {
16
+ bold: true,
17
+ color: '#24ffedff'
18
+ },
19
+ h4: {
20
+ bold: true,
21
+ color: '#e29418ff'
22
+ },
23
+ h5: {
24
+ bold: true,
25
+ color: '#9fc214ff'
26
+ },
27
+ h6: {
28
+ bold: true,
29
+ dimColor: true,
30
+ color: '#88eb2bff'
31
+ },
32
+ paragraph: {},
33
+ blockquote: {
34
+ paddingLeft: 1,
35
+ dimColor: true,
36
+ borderTop: false,
37
+ borderBottom: false,
38
+ borderRight: false,
39
+ borderStyle: 'single',
40
+ borderDimColor: true
41
+ },
42
+ code: {
43
+ marginTop: 1
44
+ },
45
+ codespan: {
46
+ dimColor: true
47
+ },
48
+ list: {},
49
+ listItem: {
50
+ bullet: '●',
51
+ paddingLeft: 2
52
+ },
53
+ hr: {
54
+ char: '─',
55
+ width: 40,
56
+ dimColor: true
57
+ },
58
+ link: {
59
+ color: '#343afcff',
60
+ underline: true
61
+ },
62
+ strong: {
63
+ bold: true
64
+ },
65
+ em: {
66
+ italic: true
67
+ },
68
+ del: {
69
+ strikethrough: true,
70
+ dimColor: true
71
+ },
72
+ image: {
73
+ color: 'blue'
74
+ },
75
+ table: {},
76
+ tableCell: {
77
+ paddingRight: 2
78
+ }
79
+ };
80
+ const TEXT_STYLE_KEYS = [
81
+ 'color',
82
+ 'backgroundColor',
83
+ 'dimColor',
84
+ 'bold',
85
+ 'italic',
86
+ 'underline',
87
+ 'strikethrough',
88
+ 'inverse',
89
+ 'wrap'
90
+ ];
91
+ const BOX_STYLE_KEYS = [
92
+ 'paddingLeft',
93
+ 'paddingRight',
94
+ 'paddingTop',
95
+ 'paddingBottom',
96
+ 'padding',
97
+ 'paddingX',
98
+ 'paddingY',
99
+ 'marginLeft',
100
+ 'marginRight',
101
+ 'marginTop',
102
+ 'marginBottom',
103
+ 'margin',
104
+ 'marginX',
105
+ 'marginY',
106
+ 'borderStyle',
107
+ 'borderColor',
108
+ 'borderTop',
109
+ 'borderBottom',
110
+ 'borderLeft',
111
+ 'borderRight',
112
+ 'borderDimColor',
113
+ 'borderTopColor',
114
+ 'borderBottomColor',
115
+ 'borderLeftColor',
116
+ 'borderRightColor',
117
+ 'borderTopDimColor',
118
+ 'borderBottomDimColor',
119
+ 'borderLeftDimColor',
120
+ 'borderRightDimColor'
121
+ ];
122
+ function extractTextProps(style) {
123
+ if (!style) return {};
124
+ const result = {};
125
+ for (const key of TEXT_STYLE_KEYS)if (key in style) result[key] = style[key];
126
+ return result;
127
+ }
128
+ function extractBoxProps(style) {
129
+ if (!style) return {};
130
+ const result = {};
131
+ for (const key of BOX_STYLE_KEYS)if (key in style) result[key] = style[key];
132
+ return result;
133
+ }
134
+ function mergeStyles(defaultStyle, userStyle) {
135
+ if (!defaultStyle && !userStyle) return {};
136
+ if (!defaultStyle) return userStyle;
137
+ if (!userStyle) return defaultStyle;
138
+ return {
139
+ ...defaultStyle,
140
+ ...userStyle
141
+ };
142
+ }
143
+ function renderInlineTokens(tokens, styles, renderers) {
144
+ if (!tokens || 0 === tokens.length) return null;
145
+ return tokens.map((token, index)=>{
146
+ const key = `inline-${index}`;
147
+ switch(token.type){
148
+ case 'text':
149
+ case 'escape':
150
+ {
151
+ const textToken = token;
152
+ if ('tokens' in textToken && textToken.tokens) return /*#__PURE__*/ React.createElement(Text, {
153
+ key: key
154
+ }, renderInlineTokens(textToken.tokens, styles, renderers));
155
+ return /*#__PURE__*/ React.createElement(Text, {
156
+ key: key
157
+ }, textToken.text);
158
+ }
159
+ case 'strong':
160
+ {
161
+ const strongToken = token;
162
+ const strongStyle = mergeStyles(DEFAULT_STYLES.strong, styles.strong);
163
+ if (renderers.strong) return /*#__PURE__*/ React.createElement(Text, {
164
+ key: key
165
+ }, renderers.strong(renderInlineTokens(strongToken.tokens, styles, renderers), strongToken));
166
+ return /*#__PURE__*/ React.createElement(Text, {
167
+ key: key,
168
+ ...extractTextProps(strongStyle)
169
+ }, renderInlineTokens(strongToken.tokens, styles, renderers));
170
+ }
171
+ case 'em':
172
+ {
173
+ const emToken = token;
174
+ const emStyle = mergeStyles(DEFAULT_STYLES.em, styles.em);
175
+ if (renderers.em) return /*#__PURE__*/ React.createElement(Text, {
176
+ key: key
177
+ }, renderers.em(renderInlineTokens(emToken.tokens, styles, renderers), emToken));
178
+ return /*#__PURE__*/ React.createElement(Text, {
179
+ key: key,
180
+ ...extractTextProps(emStyle)
181
+ }, renderInlineTokens(emToken.tokens, styles, renderers));
182
+ }
183
+ case 'del':
184
+ {
185
+ const delToken = token;
186
+ const delStyle = mergeStyles(DEFAULT_STYLES.del, styles.del);
187
+ if (renderers.del) return /*#__PURE__*/ React.createElement(Text, {
188
+ key: key
189
+ }, renderers.del(renderInlineTokens(delToken.tokens, styles, renderers), delToken));
190
+ return /*#__PURE__*/ React.createElement(Text, {
191
+ key: key,
192
+ ...extractTextProps(delStyle)
193
+ }, renderInlineTokens(delToken.tokens, styles, renderers));
194
+ }
195
+ case 'codespan':
196
+ {
197
+ const codespanToken = token;
198
+ const codespanStyle = mergeStyles(DEFAULT_STYLES.codespan, styles.codespan);
199
+ if (renderers.codespan) return /*#__PURE__*/ React.createElement(Text, {
200
+ key: key
201
+ }, renderers.codespan(codespanToken.text, codespanToken));
202
+ return /*#__PURE__*/ React.createElement(Text, {
203
+ key: key,
204
+ ...extractTextProps(codespanStyle)
205
+ }, codespanToken.text);
206
+ }
207
+ case 'link':
208
+ {
209
+ const linkToken = token;
210
+ const linkStyle = mergeStyles(DEFAULT_STYLES.link, styles.link);
211
+ if (renderers.link) return /*#__PURE__*/ React.createElement(Text, {
212
+ key: key
213
+ }, renderers.link(linkToken.text, linkToken.href, linkToken.title, linkToken));
214
+ return /*#__PURE__*/ React.createElement(Text, {
215
+ key: key,
216
+ ...extractTextProps(linkStyle)
217
+ }, linkToken.text, linkToken.href && ` (${linkToken.href})`);
218
+ }
219
+ case 'image':
220
+ {
221
+ const imageToken = token;
222
+ const imageStyle = mergeStyles(DEFAULT_STYLES.image, styles.image);
223
+ if (renderers.image) return /*#__PURE__*/ React.createElement(Text, {
224
+ key: key
225
+ }, renderers.image(imageToken.text, imageToken.href, imageToken.title, imageToken));
226
+ return /*#__PURE__*/ React.createElement(Text, {
227
+ key: key,
228
+ ...extractTextProps(imageStyle)
229
+ }, "[Image: ", imageToken.text || imageToken.href, "]");
230
+ }
231
+ case 'br':
232
+ return /*#__PURE__*/ React.createElement(Text, {
233
+ key: key
234
+ }, '\n');
235
+ default:
236
+ if ('text' in token) return /*#__PURE__*/ React.createElement(Text, {
237
+ key: key
238
+ }, token.text);
239
+ return null;
240
+ }
241
+ });
242
+ }
243
+ function renderBlockToken(token, styles, renderers, showSharp) {
244
+ switch(token.type){
245
+ case 'heading':
246
+ {
247
+ const headingToken = token;
248
+ const headingKey = `h${headingToken.depth}`;
249
+ const styleKey = `h${headingToken.depth}`;
250
+ const renderer = renderers[headingKey];
251
+ if (renderer) return renderer(headingToken.text, headingToken);
252
+ const headingStyle = mergeStyles(DEFAULT_STYLES[styleKey], styles[styleKey]);
253
+ const mergedShowSharp = 'boolean' == typeof headingStyle.showSharp ? headingStyle.showSharp : Boolean(showSharp);
254
+ return /*#__PURE__*/ React.createElement(Box, extractBoxProps(headingStyle), /*#__PURE__*/ React.createElement(Text, extractTextProps(headingStyle), mergedShowSharp && `${'#'.repeat(headingToken.depth)} `, renderInlineTokens(headingToken.tokens, styles, renderers)));
255
+ }
256
+ case 'paragraph':
257
+ {
258
+ const paragraphToken = token;
259
+ const paragraphStyle = mergeStyles(DEFAULT_STYLES.paragraph, styles.paragraph);
260
+ const content = renderInlineTokens(paragraphToken.tokens, styles, renderers);
261
+ if (renderers.paragraph) return renderers.paragraph(content, paragraphToken);
262
+ return /*#__PURE__*/ React.createElement(Box, extractBoxProps(paragraphStyle), /*#__PURE__*/ React.createElement(Text, extractTextProps(paragraphStyle), content));
263
+ }
264
+ case 'code':
265
+ {
266
+ const codeToken = token;
267
+ const codeStyle = mergeStyles(DEFAULT_STYLES.code, styles.code);
268
+ if (renderers.code) return renderers.code(codeToken.text, codeToken.lang, codeToken);
269
+ return /*#__PURE__*/ React.createElement(Box, extractBoxProps(codeStyle), /*#__PURE__*/ React.createElement(Text, extractTextProps(codeStyle), codeToken.text));
270
+ }
271
+ case 'blockquote':
272
+ {
273
+ const blockquoteToken = token;
274
+ const blockquoteStyle = mergeStyles(DEFAULT_STYLES.blockquote, styles.blockquote);
275
+ const content = blockquoteToken.tokens.map((t, i)=>/*#__PURE__*/ React.createElement(Box, {
276
+ key: `bq-${i}`
277
+ }, renderBlockToken(t, styles, renderers)));
278
+ if (renderers.blockquote) return renderers.blockquote(/*#__PURE__*/ React.createElement(React.Fragment, null, content), blockquoteToken);
279
+ return /*#__PURE__*/ React.createElement(Box, extractBoxProps(blockquoteStyle), /*#__PURE__*/ React.createElement(Box, {
280
+ flexDirection: "column"
281
+ }, blockquoteToken.tokens.map((t, i)=>{
282
+ const rendered = renderBlockToken(t, styles, renderers);
283
+ if (rendered) return /*#__PURE__*/ React.createElement(Text, {
284
+ key: `bq-text-${i}`,
285
+ ...extractTextProps(blockquoteStyle)
286
+ }, 'paragraph' === t.type ? renderInlineTokens(t.tokens, styles, renderers) : t.text || '');
287
+ return null;
288
+ })));
289
+ }
290
+ case 'list':
291
+ {
292
+ const listToken = token;
293
+ const listStyle = mergeStyles(DEFAULT_STYLES.list, styles.list);
294
+ const listItemStyle = mergeStyles(DEFAULT_STYLES.listItem, styles.listItem);
295
+ const bullet = listItemStyle.bullet || '●';
296
+ const items = listToken.items.map((item, index)=>{
297
+ const start = 'number' == typeof listToken.start ? listToken.start : 1;
298
+ const prefix = listToken.ordered ? `${start + index}. ` : `${bullet} `;
299
+ const itemContent = renderInlineTokens(item.tokens, styles, renderers);
300
+ if (renderers.listItem) return /*#__PURE__*/ React.createElement(Box, {
301
+ key: `li-${index}`,
302
+ ...extractBoxProps(listItemStyle)
303
+ }, renderers.listItem(itemContent, item));
304
+ return /*#__PURE__*/ React.createElement(Box, {
305
+ key: `li-${index}`,
306
+ ...extractBoxProps(listItemStyle)
307
+ }, /*#__PURE__*/ React.createElement(Text, extractTextProps(listItemStyle), prefix, itemContent));
308
+ });
309
+ if (renderers.list) return renderers.list(items, listToken.ordered, listToken);
310
+ return /*#__PURE__*/ React.createElement(Box, {
311
+ flexDirection: "column",
312
+ ...extractBoxProps(listStyle)
313
+ }, items);
314
+ }
315
+ case 'hr':
316
+ {
317
+ const hrToken = token;
318
+ const hrStyle = mergeStyles(DEFAULT_STYLES.hr, styles.hr);
319
+ const char = hrStyle.char || '─';
320
+ const width = hrStyle.width || 40;
321
+ if (renderers.hr) return renderers.hr(hrToken);
322
+ return /*#__PURE__*/ React.createElement(Box, extractBoxProps(hrStyle), /*#__PURE__*/ React.createElement(Text, extractTextProps(hrStyle), char.repeat(width)));
323
+ }
324
+ case 'table':
325
+ {
326
+ const tableToken = token;
327
+ const tableStyle = mergeStyles(DEFAULT_STYLES.table, styles.table);
328
+ const cellStyle = mergeStyles(DEFAULT_STYLES.tableCell, styles.tableCell);
329
+ const headerCells = tableToken.header.map((cell, i)=>/*#__PURE__*/ React.createElement(Box, {
330
+ key: `th-${i}`,
331
+ ...extractBoxProps(cellStyle)
332
+ }, /*#__PURE__*/ React.createElement(Text, {
333
+ bold: true,
334
+ ...extractTextProps(cellStyle)
335
+ }, renderInlineTokens(cell.tokens, styles, renderers))));
336
+ const header = /*#__PURE__*/ React.createElement(Box, {
337
+ flexDirection: "row"
338
+ }, headerCells);
339
+ const bodyRows = tableToken.rows.map((row, rowIndex)=>/*#__PURE__*/ React.createElement(Box, {
340
+ key: `tr-${rowIndex}`,
341
+ flexDirection: "row"
342
+ }, row.map((cell, cellIndex)=>/*#__PURE__*/ React.createElement(Box, {
343
+ key: `td-${cellIndex}`,
344
+ ...extractBoxProps(cellStyle)
345
+ }, /*#__PURE__*/ React.createElement(Text, extractTextProps(cellStyle), renderInlineTokens(cell.tokens, styles, renderers))))));
346
+ const body = /*#__PURE__*/ React.createElement(React.Fragment, null, bodyRows);
347
+ if (renderers.table) return renderers.table(header, body, tableToken);
348
+ return /*#__PURE__*/ React.createElement(Box, {
349
+ flexDirection: "column",
350
+ ...extractBoxProps(tableStyle)
351
+ }, header, body);
352
+ }
353
+ case 'space':
354
+ return /*#__PURE__*/ React.createElement(Text, null, '\n');
355
+ case 'html':
356
+ {
357
+ const htmlToken = token;
358
+ return /*#__PURE__*/ React.createElement(Text, {
359
+ dimColor: true
360
+ }, htmlToken.text);
361
+ }
362
+ default:
363
+ if ('text' in token) return /*#__PURE__*/ React.createElement(Text, null, token.text);
364
+ return null;
365
+ }
366
+ }
367
+ const src_MemoizedBlock = /*#__PURE__*/ memo(function({ token, styles, renderers, showSharp }) {
368
+ return /*#__PURE__*/ React.createElement(React.Fragment, null, renderBlockToken(token, styles, renderers, showSharp));
369
+ }, (prevProps, nextProps)=>prevProps.token === nextProps.token);
370
+ src_MemoizedBlock.displayName = 'MemoizedBlock';
371
+ function MarkdownComponent({ children, styles = {}, renderers = {}, showSharp = false }) {
372
+ const generatedId = useId();
373
+ const tokens = useMemo(()=>marked.lexer(children), [
374
+ children
375
+ ]);
376
+ return /*#__PURE__*/ React.createElement(Box, {
377
+ flexDirection: "column"
378
+ }, tokens.map((token, index)=>/*#__PURE__*/ React.createElement(src_MemoizedBlock, {
379
+ key: `${generatedId}-block-${index}`,
380
+ token: token,
381
+ styles: styles,
382
+ renderers: renderers,
383
+ showSharp: showSharp
384
+ })));
385
+ }
386
+ const Markdown = /*#__PURE__*/ memo(MarkdownComponent);
387
+ Markdown.displayName = 'Markdown';
388
+ const src = Markdown;
389
+ export default src;
@@ -0,0 +1,4 @@
1
+ import type { TextStyleProps, BoxStyleProps } from './types';
2
+ export declare function extractTextProps(style: (TextStyleProps & BoxStyleProps) | undefined): TextStyleProps;
3
+ export declare function extractBoxProps(style: (TextStyleProps & BoxStyleProps) | undefined): BoxStyleProps;
4
+ export declare function mergeStyles<T>(defaultStyle: T | undefined, userStyle: T | undefined): T;
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "ink-markdown-es",
3
+ "version": "1.0.0",
4
+ "description": "A modern performance markdown renderer for ink",
5
+ "keywords": [
6
+ "markdown",
7
+ "ink",
8
+ "react",
9
+ "esm",
10
+ "terminal"
11
+ ],
12
+ "author": "Mio <miownag@gmail.com>",
13
+ "repository": "https://github.com/miownag/ink-markdown-es",
14
+ "license": "MIT",
15
+ "type": "module",
16
+ "module": "dist/index.js",
17
+ "types": "dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "package.json",
27
+ "README.md"
28
+ ],
29
+ "scripts": {
30
+ "build": "rslib build",
31
+ "check": "biome check --write",
32
+ "dev": "bun --watch run examples/index.tsx",
33
+ "format": "biome format --write",
34
+ "prepublishOnly": "bun run build"
35
+ },
36
+ "devDependencies": {
37
+ "@rslib/core": "^0.19.2",
38
+ "@types/bun": "latest",
39
+ "@types/dedent": "^0.7.2",
40
+ "@types/react": "^19",
41
+ "@biomejs/biome": "2.3.8",
42
+ "ink": "^6",
43
+ "react": "^19",
44
+ "react-devtools-core": "^6",
45
+ "typescript": "^5.9.3"
46
+ },
47
+ "peerDependencies": {
48
+ "ink": "^6",
49
+ "react": "^19"
50
+ },
51
+ "dependencies": {
52
+ "dedent": "^1.7.1",
53
+ "marked": "^16.4.2"
54
+ }
55
+ }