ink-markdown-es 1.1.0 → 1.3.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
@@ -77,6 +77,7 @@ const TestApp = () => {
77
77
  return (
78
78
  <Markdown
79
79
  showSharp
80
+ theme="dracula"
80
81
  renderers={{
81
82
  h1: (text) => (
82
83
  <Box padding={1} borderStyle="round" borderDimColor>
@@ -105,6 +106,7 @@ render(<TestApp />);
105
106
  - `styles` (BlockStyles, optional): Custom styles for markdown blocks.
106
107
  - `renderers` (BlockRenderers, optional): Custom renderers for markdown blocks.
107
108
  - `showSharp` (boolean, optional): Whether to show sharp signs for headings. Default is `false`.
109
+ - `theme` (string, optional): The theme for syntax highlighting. Default is `github-dark`. Check out [shiki](https://shiki.style/themes) for more themes.
108
110
 
109
111
  ## Contributing
110
112
 
@@ -1,2 +1,2 @@
1
1
  import type { ReactNode } from 'react';
2
- export declare function highlightCode(code: string, language?: string): ReactNode[] | null;
2
+ export declare function highlightCodeAsync(code: string, language?: string, theme?: string): Promise<ReactNode[] | null>;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /** biome-ignore-all lint/suspicious/noArrayIndexKey: <empty> */
2
2
  import { type Token, type Tokens } from 'marked';
3
3
  import type { MarkdownProps } from './types';
4
- declare function MarkdownComponent({ children, id, styles, renderers, showSharp, highlight, }: MarkdownProps): import("react/jsx-runtime").JSX.Element;
4
+ declare function MarkdownComponent({ children, id, styles, renderers, showSharp, highlight, theme, }: MarkdownProps): import("react/jsx-runtime").JSX.Element;
5
5
  declare const Markdown: import("react").MemoExoticComponent<typeof MarkdownComponent>;
6
6
  export default Markdown;
7
7
  export type { Token, Tokens };
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  import { marked } from "marked";
4
- import { memo, useId, useMemo } from "react";
5
- import highlight_0 from "highlight.js";
4
+ import { memo, useEffect, useId, useMemo, useState } from "react";
5
+ import { codeToTokens } from "shiki";
6
6
  const DEFAULT_STYLES = {
7
7
  h1: {
8
8
  bold: true,
@@ -121,98 +121,31 @@ const BOX_STYLE_KEYS = [
121
121
  'borderLeftDimColor',
122
122
  'borderRightDimColor'
123
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) {
124
+ async function highlightCodeAsync(code, language, theme = 'github-dark') {
205
125
  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);
126
+ const result = await codeToTokens(code, {
127
+ lang: language || 'text',
128
+ theme: theme
129
+ });
130
+ const nodes = [];
131
+ let index = 0;
132
+ for(let lineIndex = 0; lineIndex < result.tokens.length; lineIndex++){
133
+ const line = result.tokens[lineIndex];
134
+ if (line) {
135
+ for (const token of line){
136
+ const color = token.color || void 0;
137
+ nodes.push(/*#__PURE__*/ jsx(Text, {
138
+ color: color,
139
+ children: token.content
140
+ }, `token-${index}`));
141
+ index++;
142
+ }
143
+ if (lineIndex < result.tokens.length - 1) nodes.push(/*#__PURE__*/ jsx(Text, {
144
+ children: '\n'
145
+ }, `nl-${lineIndex}`));
146
+ }
213
147
  }
214
- else highlighted = highlight_0.highlightAuto(code);
215
- return parseHighlightedCode(highlighted.value);
148
+ return nodes;
216
149
  } catch {
217
150
  return null;
218
151
  }
@@ -238,6 +171,51 @@ function mergeStyles(defaultStyle, userStyle) {
238
171
  ...userStyle
239
172
  };
240
173
  }
174
+ function CodeBlock({ code, language, style, theme }) {
175
+ const [highlightedCode, setHighlightedCode] = useState(null);
176
+ const [isLoading, setIsLoading] = useState(true);
177
+ useEffect(()=>{
178
+ let cancelled = false;
179
+ async function highlight() {
180
+ try {
181
+ const result = await highlightCodeAsync(code, language, theme);
182
+ if (!cancelled) {
183
+ setHighlightedCode(result);
184
+ setIsLoading(false);
185
+ }
186
+ } catch {
187
+ if (!cancelled) {
188
+ setHighlightedCode(null);
189
+ setIsLoading(false);
190
+ }
191
+ }
192
+ }
193
+ highlight();
194
+ return ()=>{
195
+ cancelled = true;
196
+ };
197
+ }, [
198
+ code,
199
+ language,
200
+ theme
201
+ ]);
202
+ if (isLoading || !highlightedCode) return /*#__PURE__*/ jsx(Box, {
203
+ ...extractBoxProps(style),
204
+ children: /*#__PURE__*/ jsx(Text, {
205
+ ...extractTextProps(style),
206
+ children: code
207
+ })
208
+ });
209
+ return /*#__PURE__*/ jsx(Box, {
210
+ paddingX: 2,
211
+ paddingY: 1,
212
+ ...extractBoxProps(style),
213
+ children: /*#__PURE__*/ jsx(Text, {
214
+ ...extractTextProps(style),
215
+ children: highlightedCode
216
+ })
217
+ });
218
+ }
241
219
  function renderInlineTokens(tokens, styles, renderers) {
242
220
  if (!tokens || 0 === tokens.length) return null;
243
221
  return tokens.map((token, index)=>{
@@ -345,7 +323,7 @@ function renderInlineTokens(tokens, styles, renderers) {
345
323
  }
346
324
  });
347
325
  }
348
- function renderBlockToken(token, styles, renderers, showSharp, highlight) {
326
+ function renderBlockToken(token, styles, renderers, showSharp, highlight, theme = 'github-dark') {
349
327
  switch(token.type){
350
328
  case 'heading':
351
329
  {
@@ -386,19 +364,12 @@ function renderBlockToken(token, styles, renderers, showSharp, highlight) {
386
364
  const codeToken = token;
387
365
  const codeStyle = mergeStyles(DEFAULT_STYLES.code, styles.code);
388
366
  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
- }
367
+ if (highlight) return /*#__PURE__*/ jsx(CodeBlock, {
368
+ code: codeToken.text,
369
+ language: codeToken.lang,
370
+ style: codeStyle,
371
+ theme: theme
372
+ });
402
373
  return /*#__PURE__*/ jsx(Box, {
403
374
  ...extractBoxProps(codeStyle),
404
375
  children: /*#__PURE__*/ jsx(Text, {
@@ -412,7 +383,7 @@ function renderBlockToken(token, styles, renderers, showSharp, highlight) {
412
383
  const blockquoteToken = token;
413
384
  const blockquoteStyle = mergeStyles(DEFAULT_STYLES.blockquote, styles.blockquote);
414
385
  const content = blockquoteToken.tokens.map((t, i)=>/*#__PURE__*/ jsx(Box, {
415
- children: renderBlockToken(t, styles, renderers, showSharp, highlight)
386
+ children: renderBlockToken(t, styles, renderers, showSharp, highlight, theme)
416
387
  }, `bq-${i}`));
417
388
  if (renderers.blockquote) return renderers.blockquote(content, blockquoteToken);
418
389
  return /*#__PURE__*/ jsx(Box, {
@@ -420,7 +391,7 @@ function renderBlockToken(token, styles, renderers, showSharp, highlight) {
420
391
  children: /*#__PURE__*/ jsx(Box, {
421
392
  flexDirection: "column",
422
393
  children: blockquoteToken.tokens.map((t, i)=>{
423
- const rendered = renderBlockToken(t, styles, renderers, showSharp, highlight);
394
+ const rendered = renderBlockToken(t, styles, renderers, showSharp, highlight, theme);
424
395
  if (rendered) return /*#__PURE__*/ jsx(Text, {
425
396
  ...extractTextProps(blockquoteStyle),
426
397
  children: 'paragraph' === t.type ? renderInlineTokens(t.tokens, styles, renderers) : t.text || ''
@@ -482,40 +453,119 @@ function renderBlockToken(token, styles, renderers, showSharp, highlight) {
482
453
  const tableToken = token;
483
454
  const tableStyle = mergeStyles(DEFAULT_STYLES.table, styles.table);
484
455
  const cellStyle = mergeStyles(DEFAULT_STYLES.tableCell, styles.tableCell);
485
- const headerCells = tableToken.header.map((cell, i)=>/*#__PURE__*/ jsx(Box, {
486
- ...extractBoxProps(cellStyle),
487
- children: /*#__PURE__*/ jsx(Text, {
488
- bold: true,
489
- ...extractTextProps(cellStyle),
490
- children: renderInlineTokens(cell.tokens, styles, renderers)
456
+ const getCellText = (tokens)=>tokens.map((t)=>{
457
+ if ('text' in t) return t.text;
458
+ return '';
459
+ }).join('');
460
+ const columnWidths = tableToken.header.map((cell, colIndex)=>{
461
+ const headerWidth = getCellText(cell.tokens).length;
462
+ const bodyWidth = Math.max(...tableToken.rows.map((row)=>{
463
+ const cellTokens = row[colIndex]?.tokens;
464
+ return cellTokens ? getCellText(cellTokens).length : 0;
465
+ }), 0);
466
+ return Math.max(headerWidth, bodyWidth, 3);
467
+ });
468
+ const topBorder = /*#__PURE__*/ jsxs(Text, {
469
+ dimColor: true,
470
+ children: [
471
+ "┌─",
472
+ columnWidths.map((w)=>'─'.repeat(w)).join('─┬─'),
473
+ "─┐"
474
+ ]
475
+ });
476
+ const headerRow = /*#__PURE__*/ jsxs(Text, {
477
+ children: [
478
+ /*#__PURE__*/ jsx(Text, {
479
+ dimColor: true,
480
+ children: "│ "
481
+ }),
482
+ tableToken.header.map((cell, i)=>{
483
+ const content = renderInlineTokens(cell.tokens, styles, renderers);
484
+ const cellText = getCellText(cell.tokens);
485
+ return /*#__PURE__*/ jsxs(Text, {
486
+ children: [
487
+ /*#__PURE__*/ jsxs(Text, {
488
+ bold: true,
489
+ ...extractTextProps(cellStyle),
490
+ children: [
491
+ content,
492
+ ' '.repeat(Math.max(0, (columnWidths[i] || 3) - cellText.length))
493
+ ]
494
+ }),
495
+ /*#__PURE__*/ jsxs(Text, {
496
+ dimColor: true,
497
+ children: [
498
+ " │",
499
+ i < tableToken.header.length - 1 ? ' ' : ''
500
+ ]
501
+ })
502
+ ]
503
+ }, `th-${i}`);
491
504
  })
492
- }, `th-${i}`));
493
- const header = /*#__PURE__*/ jsx(Box, {
494
- flexDirection: "row",
495
- children: headerCells
505
+ ]
506
+ });
507
+ const headerSeparator = /*#__PURE__*/ jsxs(Text, {
508
+ dimColor: true,
509
+ children: [
510
+ "├─",
511
+ columnWidths.map((w)=>'─'.repeat(w)).join('─┼─'),
512
+ "─┤"
513
+ ]
496
514
  });
497
- const bodyRows = tableToken.rows.map((row, rowIndex)=>/*#__PURE__*/ jsx(Box, {
498
- flexDirection: "row",
499
- children: row.map((cell, cellIndex)=>/*#__PURE__*/ jsx(Box, {
500
- ...extractBoxProps(cellStyle),
501
- children: /*#__PURE__*/ jsx(Text, {
502
- ...extractTextProps(cellStyle),
503
- children: renderInlineTokens(cell.tokens, styles, renderers)
504
- })
505
- }, `td-${cellIndex}`))
515
+ const bodyRows = tableToken.rows.map((row, rowIndex)=>/*#__PURE__*/ jsxs(Text, {
516
+ children: [
517
+ /*#__PURE__*/ jsx(Text, {
518
+ dimColor: true,
519
+ children: "│ "
520
+ }),
521
+ row.map((cell, cellIndex)=>{
522
+ const content = renderInlineTokens(cell.tokens, styles, renderers);
523
+ const cellText = getCellText(cell.tokens);
524
+ const colWidth = columnWidths[cellIndex] || 3;
525
+ return /*#__PURE__*/ jsxs(Text, {
526
+ children: [
527
+ /*#__PURE__*/ jsxs(Text, {
528
+ ...extractTextProps(cellStyle),
529
+ children: [
530
+ content,
531
+ ' '.repeat(Math.max(0, colWidth - cellText.length))
532
+ ]
533
+ }),
534
+ /*#__PURE__*/ jsxs(Text, {
535
+ dimColor: true,
536
+ children: [
537
+ " │",
538
+ cellIndex < row.length - 1 ? ' ' : ''
539
+ ]
540
+ })
541
+ ]
542
+ }, `td-${rowIndex}-${cellIndex}`);
543
+ })
544
+ ]
506
545
  }, `tr-${rowIndex}`));
507
- const body = /*#__PURE__*/ jsx(Fragment, {
508
- children: bodyRows
546
+ const bottomBorder = /*#__PURE__*/ jsxs(Text, {
547
+ dimColor: true,
548
+ children: [
549
+ "└─",
550
+ columnWidths.map((w)=>'─'.repeat(w)).join('─┴─'),
551
+ "─┘"
552
+ ]
509
553
  });
510
- if (renderers.table) return renderers.table(header, body, tableToken);
511
- return /*#__PURE__*/ jsxs(Box, {
512
- flexDirection: "column",
513
- ...extractBoxProps(tableStyle),
554
+ const tableContent = /*#__PURE__*/ jsxs(Fragment, {
514
555
  children: [
515
- header,
516
- body
556
+ topBorder,
557
+ headerRow,
558
+ headerSeparator,
559
+ bodyRows,
560
+ bottomBorder
517
561
  ]
518
562
  });
563
+ if (renderers.table) return renderers.table(headerRow, bodyRows, tableToken);
564
+ return /*#__PURE__*/ jsx(Box, {
565
+ flexDirection: "column",
566
+ ...extractBoxProps(tableStyle),
567
+ children: tableContent
568
+ });
519
569
  }
520
570
  case 'space':
521
571
  return /*#__PURE__*/ jsx(Text, {
@@ -536,16 +586,19 @@ function renderBlockToken(token, styles, renderers, showSharp, highlight) {
536
586
  return null;
537
587
  }
538
588
  }
539
- const src_MemoizedBlock = /*#__PURE__*/ memo(function({ token, styles, renderers, showSharp, highlight }) {
589
+ const src_MemoizedBlock = /*#__PURE__*/ memo(function({ token, styles, renderers, showSharp, highlight, theme }) {
540
590
  return /*#__PURE__*/ jsx(Fragment, {
541
- children: renderBlockToken(token, styles, renderers, showSharp, highlight)
591
+ children: renderBlockToken(token, styles, renderers, showSharp, highlight, theme)
542
592
  });
543
593
  }, (prevProps, nextProps)=>prevProps.token === nextProps.token);
544
594
  src_MemoizedBlock.displayName = 'MemoizedBlock';
545
- function MarkdownComponent({ children, id, styles = {}, renderers = {}, showSharp = false, highlight = true }) {
595
+ function MarkdownComponent({ children, id, styles = {}, renderers = {}, showSharp = false, highlight = true, theme = 'github-dark' }) {
546
596
  const generatedId = useId();
547
597
  const key = id || generatedId;
548
- const tokens = useMemo(()=>marked.lexer(children), [
598
+ const tokens = useMemo(()=>marked.lexer(children, {
599
+ silent: true,
600
+ gfm: true
601
+ }), [
549
602
  children
550
603
  ]);
551
604
  return /*#__PURE__*/ jsx(Box, {
@@ -555,7 +608,8 @@ function MarkdownComponent({ children, id, styles = {}, renderers = {}, showShar
555
608
  styles: styles,
556
609
  renderers: renderers,
557
610
  showSharp: showSharp,
558
- highlight: highlight
611
+ highlight: highlight,
612
+ theme: theme
559
613
  }, `${key}-block-${index}`))
560
614
  });
561
615
  }
package/dist/types.d.ts CHANGED
@@ -67,6 +67,13 @@ export type MarkdownProps = {
67
67
  * @default true
68
68
  */
69
69
  highlight?: boolean;
70
+ /**
71
+ * Shiki theme name for syntax highlighting
72
+ * @default 'github-dark'
73
+ * @example 'github-dark', 'github-light', 'nord', 'dracula', etc.
74
+ * @see https://shiki.style/themes
75
+ */
76
+ theme?: string;
70
77
  };
71
78
  export type MemoizedBlockProps = {
72
79
  token: Token;
@@ -74,4 +81,5 @@ export type MemoizedBlockProps = {
74
81
  renderers: BlockRenderers;
75
82
  showSharp: boolean;
76
83
  highlight: boolean;
84
+ theme: string;
77
85
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ink-markdown-es",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "A modern performance markdown renderer for ink",
5
5
  "keywords": [
6
6
  "markdown",
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "dedent": "^1.7.1",
54
- "highlight.js": "^11.11.1",
55
- "marked": "^16.4.2"
54
+ "marked": "^16.4.2",
55
+ "shiki": "^3.22.0"
56
56
  }
57
57
  }