ink-markdown-es 1.1.0 → 1.2.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,
@@ -122,97 +122,114 @@ const BOX_STYLE_KEYS = [
122
122
  'borderRightDimColor'
123
123
  ];
124
124
  const TERMINAL_COLORS = {
125
+ comment: 'gray',
126
+ 'comment.block': 'gray',
127
+ 'comment.line': 'gray',
125
128
  keyword: 'magenta',
126
129
  'keyword.control': 'magenta',
127
- 'keyword.operator': 'cyan',
130
+ 'keyword.other': 'magenta',
128
131
  string: 'green',
129
- 'string.special': 'greenBright',
130
- comment: 'gray',
131
- function: 'blue',
132
- 'function.call': 'blueBright',
133
- 'function.builtin': 'blueBright',
132
+ 'string.quoted': 'green',
133
+ 'string.regexp': 'red',
134
+ 'entity.name.function': 'blue',
135
+ 'support.function': 'blueBright',
136
+ 'meta.function-call': 'blueBright',
134
137
  variable: 'white',
135
- 'variable.builtin': 'yellow',
136
138
  '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',
139
+ 'variable.other': 'white',
140
+ 'entity.name.type': 'cyan',
141
+ 'entity.name.class': 'yellowBright',
142
+ 'support.type': 'cyanBright',
143
+ 'support.class': 'yellowBright',
144
+ 'constant.numeric': 'yellow',
145
+ 'constant.language': 'yellow',
144
146
  operator: 'cyan',
145
147
  punctuation: 'white',
146
- property: 'blue',
147
- attribute: 'blue',
148
- tag: 'red',
149
- 'tag.builtin': 'redBright',
150
- boolean: 'yellow',
151
- null: 'yellow',
152
- regexp: 'red',
148
+ 'variable.other.property': 'blue',
149
+ 'entity.other.attribute-name': 'blue',
150
+ 'entity.name.tag': 'red',
151
+ 'meta.tag': 'red',
152
+ 'constant.language.boolean': 'yellow',
153
+ 'constant.language.null': 'yellow',
153
154
  meta: 'gray',
154
- title: 'yellowBright',
155
- section: 'yellowBright'
155
+ storage: 'magenta',
156
+ 'storage.type': 'magenta'
156
157
  };
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;': ' '
158
+ function hexToTerminalColor(hex) {
159
+ const colorMap = {
160
+ '#6a737d': 'gray',
161
+ '#8b949e': 'gray',
162
+ '#6e7681': 'gray',
163
+ '#d73a49': 'magenta',
164
+ '#f97583': 'magenta',
165
+ '#ff7b72': 'red',
166
+ '#032f62': 'green',
167
+ '#22863a': 'green',
168
+ '#7ee787': 'green',
169
+ '#005cc5': 'blue',
170
+ '#0366d6': 'blue',
171
+ '#79c0ff': 'cyan',
172
+ '#58a6ff': 'blueBright',
173
+ '#a5d6ff': 'cyan',
174
+ '#e36209': 'yellow',
175
+ '#d29922': 'yellow',
176
+ '#ffa657': 'yellow',
177
+ '#0550ae': 'cyan'
179
178
  };
180
- return text.replace(/&[a-z]+;|&#\d+;/gi, (match)=>entities[match] || match);
179
+ const normalized = hex.toLowerCase();
180
+ if (colorMap[normalized]) return colorMap[normalized];
181
+ const r = Number.parseInt(normalized.slice(1, 3), 16);
182
+ const g = Number.parseInt(normalized.slice(3, 5), 16);
183
+ const b = Number.parseInt(normalized.slice(5, 7), 16);
184
+ const brightness = (299 * r + 587 * g + 114 * b) / 1000;
185
+ if (brightness < 80) return 'gray';
186
+ if (r > g && r > b) return 'red';
187
+ if (g > r && g > b) return 'green';
188
+ if (b > r && b > g) return 'blue';
189
+ if (r > 150 && g > 150) return 'yellow';
190
+ if (g > 150 && b > 150) return 'cyan';
191
+ if (r > 150 && b > 150) return 'magenta';
181
192
  }
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);
193
+ function tokenScopeToColor(scopes) {
194
+ for (const scope of scopes){
195
+ if (TERMINAL_COLORS[scope]) return TERMINAL_COLORS[scope];
196
+ const parts = scope.split('.');
197
+ for(let i = parts.length; i > 0; i--){
198
+ const key = parts.slice(0, i).join('.');
199
+ if (TERMINAL_COLORS[key]) return TERMINAL_COLORS[key];
200
+ }
201
201
  }
202
- return result;
203
202
  }
204
- function highlightCode(code, language) {
203
+ async function highlightCodeAsync(code, language, theme = 'github-dark') {
205
204
  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);
205
+ const result = await codeToTokens(code, {
206
+ lang: language || 'text',
207
+ theme: theme
208
+ });
209
+ const nodes = [];
210
+ let index = 0;
211
+ for(let lineIndex = 0; lineIndex < result.tokens.length; lineIndex++){
212
+ const line = result.tokens[lineIndex];
213
+ if (line) {
214
+ for (const token of line){
215
+ let color;
216
+ if (token.color) color = hexToTerminalColor(token.color);
217
+ if (!color && token.explanation) {
218
+ const scopes = token.explanation.map((e)=>e.scopes?.[0]?.scopeName || '');
219
+ color = tokenScopeToColor(scopes);
220
+ }
221
+ nodes.push(/*#__PURE__*/ jsx(Text, {
222
+ color: color,
223
+ children: token.content
224
+ }, `token-${index}`));
225
+ index++;
226
+ }
227
+ if (lineIndex < result.tokens.length - 1) nodes.push(/*#__PURE__*/ jsx(Text, {
228
+ children: '\n'
229
+ }, `nl-${lineIndex}`));
230
+ }
213
231
  }
214
- else highlighted = highlight_0.highlightAuto(code);
215
- return parseHighlightedCode(highlighted.value);
232
+ return nodes;
216
233
  } catch {
217
234
  return null;
218
235
  }
@@ -238,6 +255,51 @@ function mergeStyles(defaultStyle, userStyle) {
238
255
  ...userStyle
239
256
  };
240
257
  }
258
+ function CodeBlock({ code, language, style, theme }) {
259
+ const [highlightedCode, setHighlightedCode] = useState(null);
260
+ const [isLoading, setIsLoading] = useState(true);
261
+ useEffect(()=>{
262
+ let cancelled = false;
263
+ async function highlight() {
264
+ try {
265
+ const result = await highlightCodeAsync(code, language, theme);
266
+ if (!cancelled) {
267
+ setHighlightedCode(result);
268
+ setIsLoading(false);
269
+ }
270
+ } catch {
271
+ if (!cancelled) {
272
+ setHighlightedCode(null);
273
+ setIsLoading(false);
274
+ }
275
+ }
276
+ }
277
+ highlight();
278
+ return ()=>{
279
+ cancelled = true;
280
+ };
281
+ }, [
282
+ code,
283
+ language,
284
+ theme
285
+ ]);
286
+ if (isLoading || !highlightedCode) return /*#__PURE__*/ jsx(Box, {
287
+ ...extractBoxProps(style),
288
+ children: /*#__PURE__*/ jsx(Text, {
289
+ ...extractTextProps(style),
290
+ children: code
291
+ })
292
+ });
293
+ return /*#__PURE__*/ jsx(Box, {
294
+ paddingX: 2,
295
+ paddingY: 1,
296
+ ...extractBoxProps(style),
297
+ children: /*#__PURE__*/ jsx(Text, {
298
+ ...extractTextProps(style),
299
+ children: highlightedCode
300
+ })
301
+ });
302
+ }
241
303
  function renderInlineTokens(tokens, styles, renderers) {
242
304
  if (!tokens || 0 === tokens.length) return null;
243
305
  return tokens.map((token, index)=>{
@@ -345,7 +407,7 @@ function renderInlineTokens(tokens, styles, renderers) {
345
407
  }
346
408
  });
347
409
  }
348
- function renderBlockToken(token, styles, renderers, showSharp, highlight) {
410
+ function renderBlockToken(token, styles, renderers, showSharp, highlight, theme = 'github-dark') {
349
411
  switch(token.type){
350
412
  case 'heading':
351
413
  {
@@ -386,19 +448,12 @@ function renderBlockToken(token, styles, renderers, showSharp, highlight) {
386
448
  const codeToken = token;
387
449
  const codeStyle = mergeStyles(DEFAULT_STYLES.code, styles.code);
388
450
  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
- }
451
+ if (highlight) return /*#__PURE__*/ jsx(CodeBlock, {
452
+ code: codeToken.text,
453
+ language: codeToken.lang,
454
+ style: codeStyle,
455
+ theme: theme
456
+ });
402
457
  return /*#__PURE__*/ jsx(Box, {
403
458
  ...extractBoxProps(codeStyle),
404
459
  children: /*#__PURE__*/ jsx(Text, {
@@ -412,7 +467,7 @@ function renderBlockToken(token, styles, renderers, showSharp, highlight) {
412
467
  const blockquoteToken = token;
413
468
  const blockquoteStyle = mergeStyles(DEFAULT_STYLES.blockquote, styles.blockquote);
414
469
  const content = blockquoteToken.tokens.map((t, i)=>/*#__PURE__*/ jsx(Box, {
415
- children: renderBlockToken(t, styles, renderers, showSharp, highlight)
470
+ children: renderBlockToken(t, styles, renderers, showSharp, highlight, theme)
416
471
  }, `bq-${i}`));
417
472
  if (renderers.blockquote) return renderers.blockquote(content, blockquoteToken);
418
473
  return /*#__PURE__*/ jsx(Box, {
@@ -420,7 +475,7 @@ function renderBlockToken(token, styles, renderers, showSharp, highlight) {
420
475
  children: /*#__PURE__*/ jsx(Box, {
421
476
  flexDirection: "column",
422
477
  children: blockquoteToken.tokens.map((t, i)=>{
423
- const rendered = renderBlockToken(t, styles, renderers, showSharp, highlight);
478
+ const rendered = renderBlockToken(t, styles, renderers, showSharp, highlight, theme);
424
479
  if (rendered) return /*#__PURE__*/ jsx(Text, {
425
480
  ...extractTextProps(blockquoteStyle),
426
481
  children: 'paragraph' === t.type ? renderInlineTokens(t.tokens, styles, renderers) : t.text || ''
@@ -482,40 +537,119 @@ function renderBlockToken(token, styles, renderers, showSharp, highlight) {
482
537
  const tableToken = token;
483
538
  const tableStyle = mergeStyles(DEFAULT_STYLES.table, styles.table);
484
539
  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)
540
+ const getCellText = (tokens)=>tokens.map((t)=>{
541
+ if ('text' in t) return t.text;
542
+ return '';
543
+ }).join('');
544
+ const columnWidths = tableToken.header.map((cell, colIndex)=>{
545
+ const headerWidth = getCellText(cell.tokens).length;
546
+ const bodyWidth = Math.max(...tableToken.rows.map((row)=>{
547
+ const cellTokens = row[colIndex]?.tokens;
548
+ return cellTokens ? getCellText(cellTokens).length : 0;
549
+ }), 0);
550
+ return Math.max(headerWidth, bodyWidth, 3);
551
+ });
552
+ const topBorder = /*#__PURE__*/ jsxs(Text, {
553
+ dimColor: true,
554
+ children: [
555
+ "┌─",
556
+ columnWidths.map((w)=>'─'.repeat(w)).join('─┬─'),
557
+ "─┐"
558
+ ]
559
+ });
560
+ const headerRow = /*#__PURE__*/ jsxs(Text, {
561
+ children: [
562
+ /*#__PURE__*/ jsx(Text, {
563
+ dimColor: true,
564
+ children: "│ "
565
+ }),
566
+ tableToken.header.map((cell, i)=>{
567
+ const content = renderInlineTokens(cell.tokens, styles, renderers);
568
+ const cellText = getCellText(cell.tokens);
569
+ return /*#__PURE__*/ jsxs(Text, {
570
+ children: [
571
+ /*#__PURE__*/ jsxs(Text, {
572
+ bold: true,
573
+ ...extractTextProps(cellStyle),
574
+ children: [
575
+ content,
576
+ ' '.repeat(Math.max(0, (columnWidths[i] || 3) - cellText.length))
577
+ ]
578
+ }),
579
+ /*#__PURE__*/ jsxs(Text, {
580
+ dimColor: true,
581
+ children: [
582
+ " │",
583
+ i < tableToken.header.length - 1 ? ' ' : ''
584
+ ]
585
+ })
586
+ ]
587
+ }, `th-${i}`);
491
588
  })
492
- }, `th-${i}`));
493
- const header = /*#__PURE__*/ jsx(Box, {
494
- flexDirection: "row",
495
- children: headerCells
589
+ ]
590
+ });
591
+ const headerSeparator = /*#__PURE__*/ jsxs(Text, {
592
+ dimColor: true,
593
+ children: [
594
+ "├─",
595
+ columnWidths.map((w)=>'─'.repeat(w)).join('─┼─'),
596
+ "─┤"
597
+ ]
496
598
  });
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}`))
599
+ const bodyRows = tableToken.rows.map((row, rowIndex)=>/*#__PURE__*/ jsxs(Text, {
600
+ children: [
601
+ /*#__PURE__*/ jsx(Text, {
602
+ dimColor: true,
603
+ children: "│ "
604
+ }),
605
+ row.map((cell, cellIndex)=>{
606
+ const content = renderInlineTokens(cell.tokens, styles, renderers);
607
+ const cellText = getCellText(cell.tokens);
608
+ const colWidth = columnWidths[cellIndex] || 3;
609
+ return /*#__PURE__*/ jsxs(Text, {
610
+ children: [
611
+ /*#__PURE__*/ jsxs(Text, {
612
+ ...extractTextProps(cellStyle),
613
+ children: [
614
+ content,
615
+ ' '.repeat(Math.max(0, colWidth - cellText.length))
616
+ ]
617
+ }),
618
+ /*#__PURE__*/ jsxs(Text, {
619
+ dimColor: true,
620
+ children: [
621
+ " │",
622
+ cellIndex < row.length - 1 ? ' ' : ''
623
+ ]
624
+ })
625
+ ]
626
+ }, `td-${rowIndex}-${cellIndex}`);
627
+ })
628
+ ]
506
629
  }, `tr-${rowIndex}`));
507
- const body = /*#__PURE__*/ jsx(Fragment, {
508
- children: bodyRows
630
+ const bottomBorder = /*#__PURE__*/ jsxs(Text, {
631
+ dimColor: true,
632
+ children: [
633
+ "└─",
634
+ columnWidths.map((w)=>'─'.repeat(w)).join('─┴─'),
635
+ "─┘"
636
+ ]
509
637
  });
510
- if (renderers.table) return renderers.table(header, body, tableToken);
511
- return /*#__PURE__*/ jsxs(Box, {
512
- flexDirection: "column",
513
- ...extractBoxProps(tableStyle),
638
+ const tableContent = /*#__PURE__*/ jsxs(Fragment, {
514
639
  children: [
515
- header,
516
- body
640
+ topBorder,
641
+ headerRow,
642
+ headerSeparator,
643
+ bodyRows,
644
+ bottomBorder
517
645
  ]
518
646
  });
647
+ if (renderers.table) return renderers.table(headerRow, bodyRows, tableToken);
648
+ return /*#__PURE__*/ jsx(Box, {
649
+ flexDirection: "column",
650
+ ...extractBoxProps(tableStyle),
651
+ children: tableContent
652
+ });
519
653
  }
520
654
  case 'space':
521
655
  return /*#__PURE__*/ jsx(Text, {
@@ -536,16 +670,19 @@ function renderBlockToken(token, styles, renderers, showSharp, highlight) {
536
670
  return null;
537
671
  }
538
672
  }
539
- const src_MemoizedBlock = /*#__PURE__*/ memo(function({ token, styles, renderers, showSharp, highlight }) {
673
+ const src_MemoizedBlock = /*#__PURE__*/ memo(function({ token, styles, renderers, showSharp, highlight, theme }) {
540
674
  return /*#__PURE__*/ jsx(Fragment, {
541
- children: renderBlockToken(token, styles, renderers, showSharp, highlight)
675
+ children: renderBlockToken(token, styles, renderers, showSharp, highlight, theme)
542
676
  });
543
677
  }, (prevProps, nextProps)=>prevProps.token === nextProps.token);
544
678
  src_MemoizedBlock.displayName = 'MemoizedBlock';
545
- function MarkdownComponent({ children, id, styles = {}, renderers = {}, showSharp = false, highlight = true }) {
679
+ function MarkdownComponent({ children, id, styles = {}, renderers = {}, showSharp = false, highlight = true, theme = 'github-dark' }) {
546
680
  const generatedId = useId();
547
681
  const key = id || generatedId;
548
- const tokens = useMemo(()=>marked.lexer(children), [
682
+ const tokens = useMemo(()=>marked.lexer(children, {
683
+ silent: true,
684
+ gfm: true
685
+ }), [
549
686
  children
550
687
  ]);
551
688
  return /*#__PURE__*/ jsx(Box, {
@@ -555,7 +692,8 @@ function MarkdownComponent({ children, id, styles = {}, renderers = {}, showShar
555
692
  styles: styles,
556
693
  renderers: renderers,
557
694
  showSharp: showSharp,
558
- highlight: highlight
695
+ highlight: highlight,
696
+ theme: theme
559
697
  }, `${key}-block-${index}`))
560
698
  });
561
699
  }
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.2.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
  }