ink-markdown-es 1.0.2 → 1.1.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 CHANGED
@@ -21,9 +21,6 @@ bun add ink-markdown-es # bun
21
21
  ```
22
22
 
23
23
  ```tsx
24
- import Markdown from "ink-markdown-es";
25
- import { render } from "ink";
26
-
27
24
  const text = `# Hello World
28
25
 
29
26
  This is a show case.
@@ -34,12 +31,27 @@ It's very fast!
34
31
  - Support custom renderers
35
32
  - **Bold text** and *italic text*
36
33
  - Inline \`code\` support
37
-
38
- ### Code Block
39
-
40
- \`\`\`javascript
41
- const hello = "world";
42
- console.log(hello);
34
+ - **Syntax highlighting** for code blocks powered by highlight.js
35
+
36
+ ### Code Block with Syntax Highlighting
37
+
38
+ \`\`\`typescript
39
+ interface User {
40
+ id: number;
41
+ name: string;
42
+ email: string;
43
+ }
44
+
45
+ const user: User = {
46
+ id: 1,
47
+ name: "Alice",
48
+ email: "alice@example.com"
49
+ };
50
+
51
+ async function fetchUser(id: number): Promise<User> {
52
+ const response = await fetch(\`/api/users/\${id}\`);
53
+ return response.json();
54
+ }
43
55
  \`\`\`
44
56
 
45
57
  > This is a blockquote
@@ -59,25 +71,32 @@ Check out [this link](https://example.com) for more info.
59
71
  | Bob | 30 |
60
72
  `;
61
73
 
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
- );
74
+ const TestApp = () => {
75
+ useInput(() => {});
76
+
77
+ return (
78
+ <Markdown
79
+ showSharp
80
+ renderers={{
81
+ h1: (text) => (
82
+ <Box padding={1} borderStyle="round" borderDimColor>
83
+ <Text bold color="greenBright">
84
+ {text}
85
+ </Text>
86
+ </Box>
87
+ ),
88
+ }}
89
+ >
90
+ {text}
91
+ </Markdown>
92
+ );
93
+ };
94
+
95
+ render(<TestApp />);
96
+
78
97
  ```
79
98
 
80
- <img width="1919" height="689" alt="image" src="https://github.com/user-attachments/assets/d7cc741d-4c52-4b27-b183-ca8cce13007b" />
99
+ <img width="1904" height="964" alt="image" src="https://github.com/user-attachments/assets/bed0c942-6d08-4f24-a42a-4999bdf1fc85" />
81
100
 
82
101
  ## Props
83
102
 
@@ -0,0 +1,2 @@
1
+ import type { ReactNode } from 'react';
2
+ export declare function highlightCode(code: string, language?: string): ReactNode[] | null;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
+ /** biome-ignore-all lint/suspicious/noArrayIndexKey: <empty> */
1
2
  import { type Token, type Tokens } from 'marked';
2
3
  import type { MarkdownProps } from './types';
3
- declare function MarkdownComponent({ children, id, styles, renderers, showSharp, }: MarkdownProps): import("react/jsx-runtime").JSX.Element;
4
+ declare function MarkdownComponent({ children, id, styles, renderers, showSharp, highlight, }: MarkdownProps): import("react/jsx-runtime").JSX.Element;
4
5
  declare const Markdown: import("react").MemoExoticComponent<typeof MarkdownComponent>;
5
6
  export default Markdown;
6
7
  export type { Token, Tokens };
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { memo, useId, useMemo } from "react";
3
- import { marked } from "marked";
4
2
  import { Box, Text } from "ink";
3
+ import { marked } from "marked";
4
+ import { memo, useId, useMemo } from "react";
5
+ import highlight_0 from "highlight.js";
5
6
  const DEFAULT_STYLES = {
6
7
  h1: {
7
8
  bold: true,
@@ -120,6 +121,102 @@ const BOX_STYLE_KEYS = [
120
121
  'borderLeftDimColor',
121
122
  'borderRightDimColor'
122
123
  ];
124
+ const TERMINAL_COLORS = {
125
+ keyword: 'magenta',
126
+ 'keyword.control': 'magenta',
127
+ 'keyword.operator': 'cyan',
128
+ string: 'green',
129
+ 'string.special': 'greenBright',
130
+ comment: 'gray',
131
+ function: 'blue',
132
+ 'function.call': 'blueBright',
133
+ 'function.builtin': 'blueBright',
134
+ variable: 'white',
135
+ 'variable.builtin': 'yellow',
136
+ 'variable.parameter': 'yellowBright',
137
+ type: 'cyan',
138
+ 'type.builtin': 'cyanBright',
139
+ class: 'yellowBright',
140
+ number: 'yellow',
141
+ 'number.float': 'yellow',
142
+ constant: 'yellow',
143
+ 'constant.builtin': 'yellowBright',
144
+ operator: 'cyan',
145
+ punctuation: 'white',
146
+ property: 'blue',
147
+ attribute: 'blue',
148
+ tag: 'red',
149
+ 'tag.builtin': 'redBright',
150
+ boolean: 'yellow',
151
+ null: 'yellow',
152
+ regexp: 'red',
153
+ meta: 'gray',
154
+ title: 'yellowBright',
155
+ section: 'yellowBright'
156
+ };
157
+ function classToColor(className) {
158
+ if (TERMINAL_COLORS[className]) return TERMINAL_COLORS[className];
159
+ const parts = className.replace(/^hljs-/, '').split('.');
160
+ for(let i = parts.length; i > 0; i--){
161
+ const key = parts.slice(0, i).join('.');
162
+ if (TERMINAL_COLORS[key]) return TERMINAL_COLORS[key];
163
+ }
164
+ }
165
+ function decodeHtmlEntities(text) {
166
+ const entities = {
167
+ '&quot;': '"',
168
+ '&#34;': '"',
169
+ '&apos;': "'",
170
+ '&#39;': "'",
171
+ '&lt;': '<',
172
+ '&#60;': '<',
173
+ '&gt;': '>',
174
+ '&#62;': '>',
175
+ '&amp;': '&',
176
+ '&#38;': '&',
177
+ '&nbsp;': ' ',
178
+ '&#160;': ' '
179
+ };
180
+ return text.replace(/&[a-z]+;|&#\d+;/gi, (match)=>entities[match] || match);
181
+ }
182
+ function parseHighlightedCode(html) {
183
+ const result = [];
184
+ const regex = /<span class="([^"]+)">([^<]*)<\/span>|([^<]+)/g;
185
+ let index = 0;
186
+ let match = regex.exec(html);
187
+ while(null !== match){
188
+ if (match[1] && void 0 !== match[2]) {
189
+ const className = match[1];
190
+ const text = decodeHtmlEntities(match[2]);
191
+ const color = classToColor(className);
192
+ result.push(/*#__PURE__*/ jsx(Text, {
193
+ color: color,
194
+ children: text
195
+ }, index));
196
+ } else if (match[3]) result.push(/*#__PURE__*/ jsx(Text, {
197
+ children: decodeHtmlEntities(match[3])
198
+ }, index));
199
+ index++;
200
+ match = regex.exec(html);
201
+ }
202
+ return result;
203
+ }
204
+ function highlightCode(code, language) {
205
+ try {
206
+ let highlighted;
207
+ if (language) try {
208
+ highlighted = highlight_0.highlight(code, {
209
+ language
210
+ });
211
+ } catch {
212
+ highlighted = highlight_0.highlightAuto(code);
213
+ }
214
+ else highlighted = highlight_0.highlightAuto(code);
215
+ return parseHighlightedCode(highlighted.value);
216
+ } catch {
217
+ return null;
218
+ }
219
+ }
123
220
  function extractTextProps(style) {
124
221
  if (!style) return {};
125
222
  const result = {};
@@ -248,7 +345,7 @@ function renderInlineTokens(tokens, styles, renderers) {
248
345
  }
249
346
  });
250
347
  }
251
- function renderBlockToken(token, styles, renderers, showSharp) {
348
+ function renderBlockToken(token, styles, renderers, showSharp, highlight) {
252
349
  switch(token.type){
253
350
  case 'heading':
254
351
  {
@@ -289,6 +386,19 @@ function renderBlockToken(token, styles, renderers, showSharp) {
289
386
  const codeToken = token;
290
387
  const codeStyle = mergeStyles(DEFAULT_STYLES.code, styles.code);
291
388
  if (renderers.code) return renderers.code(codeToken.text, codeToken.lang, codeToken);
389
+ if (highlight) {
390
+ const highlightedCode = highlightCode(codeToken.text, codeToken.lang);
391
+ if (highlightedCode) return /*#__PURE__*/ jsx(Box, {
392
+ backgroundColor: "gray",
393
+ paddingX: 2,
394
+ paddingY: 1,
395
+ ...extractBoxProps(codeStyle),
396
+ children: /*#__PURE__*/ jsx(Text, {
397
+ ...extractTextProps(codeStyle),
398
+ children: highlightedCode
399
+ })
400
+ });
401
+ }
292
402
  return /*#__PURE__*/ jsx(Box, {
293
403
  ...extractBoxProps(codeStyle),
294
404
  children: /*#__PURE__*/ jsx(Text, {
@@ -302,17 +412,15 @@ function renderBlockToken(token, styles, renderers, showSharp) {
302
412
  const blockquoteToken = token;
303
413
  const blockquoteStyle = mergeStyles(DEFAULT_STYLES.blockquote, styles.blockquote);
304
414
  const content = blockquoteToken.tokens.map((t, i)=>/*#__PURE__*/ jsx(Box, {
305
- children: renderBlockToken(t, styles, renderers)
415
+ children: renderBlockToken(t, styles, renderers, showSharp, highlight)
306
416
  }, `bq-${i}`));
307
- if (renderers.blockquote) return renderers.blockquote(/*#__PURE__*/ jsx(Fragment, {
308
- children: content
309
- }), blockquoteToken);
417
+ if (renderers.blockquote) return renderers.blockquote(content, blockquoteToken);
310
418
  return /*#__PURE__*/ jsx(Box, {
311
419
  ...extractBoxProps(blockquoteStyle),
312
420
  children: /*#__PURE__*/ jsx(Box, {
313
421
  flexDirection: "column",
314
422
  children: blockquoteToken.tokens.map((t, i)=>{
315
- const rendered = renderBlockToken(t, styles, renderers);
423
+ const rendered = renderBlockToken(t, styles, renderers, showSharp, highlight);
316
424
  if (rendered) return /*#__PURE__*/ jsx(Text, {
317
425
  ...extractTextProps(blockquoteStyle),
318
426
  children: 'paragraph' === t.type ? renderInlineTokens(t.tokens, styles, renderers) : t.text || ''
@@ -428,13 +536,13 @@ function renderBlockToken(token, styles, renderers, showSharp) {
428
536
  return null;
429
537
  }
430
538
  }
431
- const src_MemoizedBlock = /*#__PURE__*/ memo(function({ token, styles, renderers, showSharp }) {
539
+ const src_MemoizedBlock = /*#__PURE__*/ memo(function({ token, styles, renderers, showSharp, highlight }) {
432
540
  return /*#__PURE__*/ jsx(Fragment, {
433
- children: renderBlockToken(token, styles, renderers, showSharp)
541
+ children: renderBlockToken(token, styles, renderers, showSharp, highlight)
434
542
  });
435
543
  }, (prevProps, nextProps)=>prevProps.token === nextProps.token);
436
544
  src_MemoizedBlock.displayName = 'MemoizedBlock';
437
- function MarkdownComponent({ children, id, styles = {}, renderers = {}, showSharp = false }) {
545
+ function MarkdownComponent({ children, id, styles = {}, renderers = {}, showSharp = false, highlight = true }) {
438
546
  const generatedId = useId();
439
547
  const key = id || generatedId;
440
548
  const tokens = useMemo(()=>marked.lexer(children), [
@@ -446,7 +554,8 @@ function MarkdownComponent({ children, id, styles = {}, renderers = {}, showShar
446
554
  token: token,
447
555
  styles: styles,
448
556
  renderers: renderers,
449
- showSharp: showSharp
557
+ showSharp: showSharp,
558
+ highlight: highlight
450
559
  }, `${key}-block-${index}`))
451
560
  });
452
561
  }
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { ReactNode, ComponentProps } from 'react';
1
+ import type { Box, Text } from 'ink';
2
2
  import type { Token, Tokens } from 'marked';
3
- import type { Text, Box } from 'ink';
3
+ import type { ComponentProps, ReactNode } from 'react';
4
4
  export type TextStyleProps = Pick<ComponentProps<typeof Text>, 'color' | 'backgroundColor' | 'dimColor' | 'bold' | 'italic' | 'underline' | 'strikethrough' | 'inverse' | 'wrap'>;
5
5
  export type BoxStyleProps = ComponentProps<typeof Box>;
6
6
  export type HeadingStyleProps = TextStyleProps & BoxStyleProps & {
@@ -62,10 +62,16 @@ export type MarkdownProps = {
62
62
  styles?: BlockStyles;
63
63
  renderers?: BlockRenderers;
64
64
  showSharp?: boolean;
65
+ /**
66
+ * Enable syntax highlighting for code blocks
67
+ * @default true
68
+ */
69
+ highlight?: boolean;
65
70
  };
66
71
  export type MemoizedBlockProps = {
67
72
  token: Token;
68
73
  styles: BlockStyles;
69
74
  renderers: BlockRenderers;
70
75
  showSharp: boolean;
76
+ highlight: boolean;
71
77
  };
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { TextStyleProps, BoxStyleProps } from './types';
1
+ import type { BoxStyleProps, TextStyleProps } from './types';
2
2
  export declare function extractTextProps(style: (TextStyleProps & BoxStyleProps) | undefined): TextStyleProps;
3
3
  export declare function extractBoxProps(style: (TextStyleProps & BoxStyleProps) | undefined): BoxStyleProps;
4
4
  export declare function mergeStyles<T>(defaultStyle: T | undefined, userStyle: T | undefined): T;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ink-markdown-es",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "A modern performance markdown renderer for ink",
5
5
  "keywords": [
6
6
  "markdown",
@@ -51,6 +51,7 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "dedent": "^1.7.1",
54
+ "highlight.js": "^11.11.1",
54
55
  "marked": "^16.4.2"
55
56
  }
56
57
  }