@vendure-io/ui 2.0.0-beta.7 → 2.0.0-beta.9
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/package.json +2 -2
- package/src/components/molecules/anonymized-token.tsx +2 -2
- package/src/components/molecules/code-block.tsx +19 -138
- package/src/components/molecules/copy-feedback-provider.tsx +33 -0
- package/src/components/molecules/copyable-text.tsx +16 -6
- package/src/components/molecules/id-chip.tsx +1 -1
- package/src/lib/highlight.ts +141 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vendure-io/ui",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.9",
|
|
4
4
|
"description": "React component library for Vendure, built on shadcn/ui and Tailwind v4",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"homepage": "https://github.com/vendurehq/design/tree/main/packages/ui",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@shikijs/themes": "^3.23.0",
|
|
50
50
|
"@shikijs/transformers": "^3.22.0",
|
|
51
51
|
"@tanstack/react-table": "^8.21.3",
|
|
52
|
-
"@vendure-io/design-tokens": "^2.0.0-beta.
|
|
52
|
+
"@vendure-io/design-tokens": "^2.0.0-beta.7",
|
|
53
53
|
"class-variance-authority": "^0.7.1",
|
|
54
54
|
"clsx": "^2.1.1",
|
|
55
55
|
"cmdk": "^1.1.1",
|
|
@@ -53,9 +53,9 @@ interface AnonymizedTokenProps extends Omit<React.ComponentProps<'span'>, 'child
|
|
|
53
53
|
copyable?: boolean;
|
|
54
54
|
/** How long the check-mark feedback stays visible, in ms. */
|
|
55
55
|
timeout?: CopyButtonProps['timeout'];
|
|
56
|
-
/** Called after a successful copy. Wire your toast here. */
|
|
56
|
+
/** Called after a successful copy. Wire your toast here. Falls back to `CopyFeedbackProvider`. */
|
|
57
57
|
onCopied?: CopyButtonProps['onCopied'];
|
|
58
|
-
/** Called when the clipboard write fails. */
|
|
58
|
+
/** Called when the clipboard write fails. Falls back to `CopyFeedbackProvider`. */
|
|
59
59
|
onCopyError?: CopyButtonProps['onCopyError'];
|
|
60
60
|
/** Accessible label before copying. @default "Copy token" */
|
|
61
61
|
copyLabel?: CopyButtonProps['copyLabel'];
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
transformerNotationDiff,
|
|
5
|
-
transformerNotationErrorLevel,
|
|
6
|
-
transformerNotationFocus,
|
|
7
|
-
transformerNotationHighlight,
|
|
8
|
-
transformerNotationWordHighlight,
|
|
9
|
-
} from '@shikijs/transformers';
|
|
10
3
|
import { Button } from '@vendure-io/ui/components/atoms/button';
|
|
11
4
|
import { ScrollArea, ScrollBar } from '@vendure-io/ui/components/atoms/scroll-area';
|
|
12
5
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@vendure-io/ui/components/atoms/tooltip';
|
|
6
|
+
import { useCopyFeedback } from '@vendure-io/ui/components/molecules/copy-feedback-provider';
|
|
13
7
|
import { useCopy } from '@vendure-io/ui/hooks/use-copy';
|
|
8
|
+
import {
|
|
9
|
+
highlightCode,
|
|
10
|
+
normalizeLanguage,
|
|
11
|
+
type SupportedLanguage,
|
|
12
|
+
} from '@vendure-io/ui/lib/highlight';
|
|
14
13
|
import { cn } from '@vendure-io/ui/lib/utils';
|
|
15
14
|
import {
|
|
16
15
|
CheckIcon,
|
|
@@ -32,8 +31,6 @@ import {
|
|
|
32
31
|
useRef,
|
|
33
32
|
useState,
|
|
34
33
|
} from 'react';
|
|
35
|
-
import { createHighlighterCore } from 'shiki/core';
|
|
36
|
-
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
|
|
37
34
|
|
|
38
35
|
// =============================================================================
|
|
39
36
|
// Types
|
|
@@ -56,9 +53,14 @@ type CodeBlockProps = PropsWithChildren<ComponentProps<'div'>> & {
|
|
|
56
53
|
packageManagerSwitcher?: boolean;
|
|
57
54
|
/** Extra toolbar actions rendered before the built-in copy button. Compose with CodeBlockAction. */
|
|
58
55
|
actions?: ReactNode;
|
|
59
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Called after a successful copy. Wire your toast here. The DS never toasts.
|
|
58
|
+
* Function props can't cross an RSC boundary — when rendering from a server
|
|
59
|
+
* component, omit this and mount `CopyFeedbackProvider` instead; it is the
|
|
60
|
+
* fallback when no prop is passed.
|
|
61
|
+
*/
|
|
60
62
|
onCopied?: () => void;
|
|
61
|
-
/** Called when the clipboard write fails. */
|
|
63
|
+
/** Called when the clipboard write fails. Falls back to `CopyFeedbackProvider`. */
|
|
62
64
|
onCopyError?: (error: Error) => void;
|
|
63
65
|
};
|
|
64
66
|
|
|
@@ -81,53 +83,6 @@ const STORAGE_EVENT = 'vendure-ui-package-manager-change';
|
|
|
81
83
|
const COLLAPSE_THRESHOLD = 40; // Lines needed to trigger collapse
|
|
82
84
|
const COLLAPSED_HEIGHT = 300; // px height when collapsed (roughly 15-20 lines)
|
|
83
85
|
|
|
84
|
-
const languageLoaders = {
|
|
85
|
-
bash: () => import('@shikijs/langs/bash').then((module) => module.default),
|
|
86
|
-
css: () => import('@shikijs/langs/css').then((module) => module.default),
|
|
87
|
-
dotenv: () => import('@shikijs/langs/dotenv').then((module) => module.default),
|
|
88
|
-
graphql: () => import('@shikijs/langs/graphql').then((module) => module.default),
|
|
89
|
-
html: () => import('@shikijs/langs/html').then((module) => module.default),
|
|
90
|
-
ini: () => import('@shikijs/langs/ini').then((module) => module.default),
|
|
91
|
-
javascript: () => import('@shikijs/langs/javascript').then((module) => module.default),
|
|
92
|
-
json: () => import('@shikijs/langs/json').then((module) => module.default),
|
|
93
|
-
jsonc: () => import('@shikijs/langs/jsonc').then((module) => module.default),
|
|
94
|
-
jsx: () => import('@shikijs/langs/jsx').then((module) => module.default),
|
|
95
|
-
markdown: () => import('@shikijs/langs/markdown').then((module) => module.default),
|
|
96
|
-
mdx: () => import('@shikijs/langs/mdx').then((module) => module.default),
|
|
97
|
-
python: () => import('@shikijs/langs/python').then((module) => module.default),
|
|
98
|
-
shellscript: () => import('@shikijs/langs/shellscript').then((module) => module.default),
|
|
99
|
-
sql: () => import('@shikijs/langs/sql').then((module) => module.default),
|
|
100
|
-
tsx: () => import('@shikijs/langs/tsx').then((module) => module.default),
|
|
101
|
-
typescript: () => import('@shikijs/langs/typescript').then((module) => module.default),
|
|
102
|
-
yaml: () => import('@shikijs/langs/yaml').then((module) => module.default),
|
|
103
|
-
} as const;
|
|
104
|
-
|
|
105
|
-
type SupportedLanguage = keyof typeof languageLoaders;
|
|
106
|
-
|
|
107
|
-
const themeLoaders = [
|
|
108
|
-
() => import('@shikijs/themes/github-light').then((module) => module.default),
|
|
109
|
-
() => import('@shikijs/themes/github-dark-default').then((module) => module.default),
|
|
110
|
-
] as const;
|
|
111
|
-
|
|
112
|
-
let highlighterPromise: ReturnType<typeof createHighlighterCore> | null = null;
|
|
113
|
-
|
|
114
|
-
function getHighlighter(): ReturnType<typeof createHighlighterCore> {
|
|
115
|
-
highlighterPromise ??= Promise.all([
|
|
116
|
-
Promise.all(themeLoaders.map((loadTheme) => loadTheme())),
|
|
117
|
-
Promise.all(Object.values(languageLoaders).map((loadLanguage) => loadLanguage())),
|
|
118
|
-
]).then(([themes, languages]) =>
|
|
119
|
-
createHighlighterCore({
|
|
120
|
-
themes,
|
|
121
|
-
langs: languages.flat(),
|
|
122
|
-
engine: createJavaScriptRegexEngine(),
|
|
123
|
-
}),
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
return highlighterPromise;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const highlightedCodeCache = new Map<string, Promise<string>>();
|
|
130
|
-
|
|
131
86
|
/**
|
|
132
87
|
* Get the stored package manager preference from localStorage
|
|
133
88
|
*/
|
|
@@ -445,83 +400,6 @@ function processCode(
|
|
|
445
400
|
};
|
|
446
401
|
}
|
|
447
402
|
|
|
448
|
-
// =============================================================================
|
|
449
|
-
// Shiki Highlighting
|
|
450
|
-
// =============================================================================
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Highlight code using Shiki with all transformers.
|
|
454
|
-
* Uses Shiki's native notation for highlighting:
|
|
455
|
-
* - // [!code highlight] - highlight a line (use language-appropriate comment)
|
|
456
|
-
* - // [!code ++] / // [!code --] - diff highlighting
|
|
457
|
-
* - // [!code focus] - focus mode (blur other lines)
|
|
458
|
-
* - // [!code word:myVar] - highlight specific word
|
|
459
|
-
* - // [!code error] / // [!code warning] - error levels
|
|
460
|
-
*/
|
|
461
|
-
async function highlightCode(code: string, language: SupportedLanguage): Promise<string> {
|
|
462
|
-
const cacheKey = `${language}:${code}`;
|
|
463
|
-
const cached = highlightedCodeCache.get(cacheKey);
|
|
464
|
-
if (cached) return cached;
|
|
465
|
-
|
|
466
|
-
const highlighted = getHighlighter().then((highlighter) =>
|
|
467
|
-
highlighter.codeToHtml(code, {
|
|
468
|
-
lang: language,
|
|
469
|
-
themes: {
|
|
470
|
-
light: 'github-light',
|
|
471
|
-
dark: 'github-dark-default',
|
|
472
|
-
},
|
|
473
|
-
transformers: [
|
|
474
|
-
transformerNotationDiff({ matchAlgorithm: 'v3' }),
|
|
475
|
-
transformerNotationHighlight({ matchAlgorithm: 'v3' }),
|
|
476
|
-
transformerNotationWordHighlight({ matchAlgorithm: 'v3' }),
|
|
477
|
-
transformerNotationFocus({ matchAlgorithm: 'v3' }),
|
|
478
|
-
transformerNotationErrorLevel({ matchAlgorithm: 'v3' }),
|
|
479
|
-
],
|
|
480
|
-
}),
|
|
481
|
-
);
|
|
482
|
-
highlightedCodeCache.set(cacheKey, highlighted);
|
|
483
|
-
return highlighted;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* Language aliases that map to Shiki's bundled language names
|
|
488
|
-
*/
|
|
489
|
-
const LANGUAGE_ALIASES: Record<string, SupportedLanguage> = {
|
|
490
|
-
env: 'dotenv',
|
|
491
|
-
js: 'javascript',
|
|
492
|
-
ts: 'typescript',
|
|
493
|
-
sh: 'bash',
|
|
494
|
-
shell: 'bash',
|
|
495
|
-
yml: 'yaml',
|
|
496
|
-
py: 'python',
|
|
497
|
-
md: 'markdown',
|
|
498
|
-
plaintext: 'ini',
|
|
499
|
-
text: 'ini',
|
|
500
|
-
chroma: 'ini',
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* Normalize language identifier for Shiki/BundledLanguage.
|
|
505
|
-
* Falls back to 'ini' for unsupported languages (minimal highlighting).
|
|
506
|
-
*/
|
|
507
|
-
function normalizeLanguage(lang?: string): SupportedLanguage {
|
|
508
|
-
const normalized = lang?.toLowerCase() || 'ini';
|
|
509
|
-
|
|
510
|
-
// Check for aliases first
|
|
511
|
-
const alias = LANGUAGE_ALIASES[normalized];
|
|
512
|
-
if (alias) {
|
|
513
|
-
return alias;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// Check if the language is one of the explicitly bundled grammars.
|
|
517
|
-
if (normalized in languageLoaders) {
|
|
518
|
-
return normalized as SupportedLanguage;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// Fallback to 'ini' for unsupported languages (has minimal highlighting)
|
|
522
|
-
return 'ini';
|
|
523
|
-
}
|
|
524
|
-
|
|
525
403
|
/**
|
|
526
404
|
* Escape HTML special characters for safe display (fallback)
|
|
527
405
|
*/
|
|
@@ -975,7 +853,9 @@ function SyntaxHighlightedContent({ code, language }: SyntaxHighlightedContentPr
|
|
|
975
853
|
* long snippets, and an optional npm→pnpm/yarn/bun package-manager switcher.
|
|
976
854
|
* Highlighting is lazy (a shared highlighter singleton + per-snippet cache), and
|
|
977
855
|
* line/diff/focus/word decorations use Shiki's `[!code ...]` notations. The DS
|
|
978
|
-
* never toasts. Wire `onCopied`/`onCopyError` to your own feedback
|
|
856
|
+
* never toasts. Wire `onCopied`/`onCopyError` to your own feedback — or, when
|
|
857
|
+
* rendering from server components (where function props can't be passed),
|
|
858
|
+
* mount `CopyFeedbackProvider` once and leave the props off.
|
|
979
859
|
*/
|
|
980
860
|
export function CodeBlock({
|
|
981
861
|
className,
|
|
@@ -990,6 +870,7 @@ export function CodeBlock({
|
|
|
990
870
|
...props
|
|
991
871
|
}: CodeBlockProps) {
|
|
992
872
|
const { copied, copy } = useCopy();
|
|
873
|
+
const copyFeedback = useCopyFeedback();
|
|
993
874
|
const [activePackageManager, setActivePackageManager] = usePackageManager();
|
|
994
875
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
995
876
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -1035,8 +916,8 @@ export function CodeBlock({
|
|
|
1035
916
|
|
|
1036
917
|
const handleCopy = async () => {
|
|
1037
918
|
const ok = await copy(displayCode);
|
|
1038
|
-
if (ok) onCopied?.();
|
|
1039
|
-
else onCopyError?.(new Error('Failed to copy to the clipboard'));
|
|
919
|
+
if (ok) (onCopied ?? copyFeedback.onCopied)?.();
|
|
920
|
+
else (onCopyError ?? copyFeedback.onCopyError)?.(new Error('Failed to copy to the clipboard'));
|
|
1040
921
|
};
|
|
1041
922
|
|
|
1042
923
|
const toolbar = <CodeBlockToolbar onCopy={handleCopy} isCopied={copied} actions={actions} />;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, type ReactNode, useContext, useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
// Function props can't cross a server→client boundary, so a copy surface
|
|
6
|
+
// rendered from RSC (e.g. MDX docs) could never receive an `onCopied` callback
|
|
7
|
+
// for toast wiring. This context is the RSC-safe alternative: mount
|
|
8
|
+
// CopyFeedbackProvider once in a client component (wire your toast there — the
|
|
9
|
+
// DS never toasts), and copy surfaces resolve their feedback in a fixed order —
|
|
10
|
+
// explicit prop → this context → nothing beyond the built-in copied icon.
|
|
11
|
+
interface CopyFeedbackContextValue {
|
|
12
|
+
/** Called after a successful copy. Wire your toast here — the DS never toasts. */
|
|
13
|
+
onCopied?: () => void;
|
|
14
|
+
/** Called when the clipboard write fails. */
|
|
15
|
+
onCopyError?: (error: Error) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CopyFeedbackContext = createContext<CopyFeedbackContextValue>({});
|
|
19
|
+
|
|
20
|
+
function CopyFeedbackProvider({
|
|
21
|
+
children,
|
|
22
|
+
onCopied,
|
|
23
|
+
onCopyError,
|
|
24
|
+
}: CopyFeedbackContextValue & { children: ReactNode }) {
|
|
25
|
+
const value = useMemo(() => ({ onCopied, onCopyError }), [onCopied, onCopyError]);
|
|
26
|
+
return <CopyFeedbackContext.Provider value={value}>{children}</CopyFeedbackContext.Provider>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function useCopyFeedback(): CopyFeedbackContextValue {
|
|
30
|
+
return useContext(CopyFeedbackContext);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { CopyFeedbackProvider, useCopyFeedback, type CopyFeedbackContextValue };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Button } from '@vendure-io/ui/components/atoms/button';
|
|
4
|
+
import { useCopyFeedback } from '@vendure-io/ui/components/molecules/copy-feedback-provider';
|
|
4
5
|
import { useCopy } from '@vendure-io/ui/hooks/use-copy';
|
|
5
6
|
import { cn } from '@vendure-io/ui/lib/utils';
|
|
6
7
|
import { CheckIcon, CopyIcon } from 'lucide-react';
|
|
@@ -11,9 +12,16 @@ interface CopyButtonProps extends Omit<React.ComponentProps<typeof Button>, 'val
|
|
|
11
12
|
value: string;
|
|
12
13
|
/** How long the check-mark feedback stays visible, in ms. @default 2000 */
|
|
13
14
|
timeout?: number;
|
|
14
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Called after a successful copy. Wire your toast here — the DS never toasts.
|
|
17
|
+
* Falls back to `CopyFeedbackProvider` when omitted (the RSC-safe path, since
|
|
18
|
+
* function props can't be passed from server components).
|
|
19
|
+
*/
|
|
15
20
|
onCopied?: () => void;
|
|
16
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Called when the clipboard write fails (e.g. permissions, insecure context).
|
|
23
|
+
* Falls back to `CopyFeedbackProvider` when omitted.
|
|
24
|
+
*/
|
|
17
25
|
onCopyError?: (error: Error) => void;
|
|
18
26
|
/** Accessible label before copying. @default "Copy" */
|
|
19
27
|
copyLabel?: string;
|
|
@@ -40,6 +48,7 @@ function CopyButton({
|
|
|
40
48
|
...props
|
|
41
49
|
}: CopyButtonProps) {
|
|
42
50
|
const { copied, copy } = useCopy({ timeout });
|
|
51
|
+
const copyFeedback = useCopyFeedback();
|
|
43
52
|
|
|
44
53
|
return (
|
|
45
54
|
<Button
|
|
@@ -53,8 +62,9 @@ function CopyButton({
|
|
|
53
62
|
onClick?.(event);
|
|
54
63
|
if (event.defaultPrevented) return;
|
|
55
64
|
const ok = await copy(value);
|
|
56
|
-
if (ok) onCopied?.();
|
|
57
|
-
else
|
|
65
|
+
if (ok) (onCopied ?? copyFeedback.onCopied)?.();
|
|
66
|
+
else
|
|
67
|
+
(onCopyError ?? copyFeedback.onCopyError)?.(new Error('Failed to copy to the clipboard'));
|
|
58
68
|
}}
|
|
59
69
|
{...props}
|
|
60
70
|
>
|
|
@@ -71,9 +81,9 @@ interface CopyableTextProps {
|
|
|
71
81
|
className?: string;
|
|
72
82
|
/** How long the check-mark feedback stays visible, in ms. @default 2000 */
|
|
73
83
|
timeout?: number;
|
|
74
|
-
/** Called after a successful copy. Wire your toast here — the DS never toasts. */
|
|
84
|
+
/** Called after a successful copy. Wire your toast here — the DS never toasts. Falls back to `CopyFeedbackProvider`. */
|
|
75
85
|
onCopied?: () => void;
|
|
76
|
-
/** Called when the clipboard write fails. */
|
|
86
|
+
/** Called when the clipboard write fails. Falls back to `CopyFeedbackProvider`. */
|
|
77
87
|
onCopyError?: (error: Error) => void;
|
|
78
88
|
}
|
|
79
89
|
|
|
@@ -40,7 +40,7 @@ interface IdChipProps {
|
|
|
40
40
|
/** Render the copy affordance. @default true */
|
|
41
41
|
copyable?: boolean;
|
|
42
42
|
className?: string;
|
|
43
|
-
/** Called after a successful copy. Wire your toast here — the DS never toasts. */
|
|
43
|
+
/** Called after a successful copy. Wire your toast here — the DS never toasts. Falls back to `CopyFeedbackProvider`. */
|
|
44
44
|
onCopied?: () => void;
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
transformerNotationDiff,
|
|
3
|
+
transformerNotationErrorLevel,
|
|
4
|
+
transformerNotationFocus,
|
|
5
|
+
transformerNotationHighlight,
|
|
6
|
+
transformerNotationWordHighlight,
|
|
7
|
+
} from '@shikijs/transformers';
|
|
8
|
+
import { createHighlighterCore } from 'shiki/core';
|
|
9
|
+
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The design system's Shiki setup: a lazy shared highlighter with the DS themes
|
|
13
|
+
* (github-light / github-dark-default), a fixed grammar set, and the `[!code ...]`
|
|
14
|
+
* notation transformers. Exported so consumers rendering highlighted HTML outside
|
|
15
|
+
* of `CodeBlock` (e.g. custom docs pipelines) reuse the exact same setup instead
|
|
16
|
+
* of duplicating it — and get the lazy per-language chunks instead of the full
|
|
17
|
+
* Shiki bundle.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const languageLoaders = {
|
|
21
|
+
bash: () => import('@shikijs/langs/bash').then((module) => module.default),
|
|
22
|
+
css: () => import('@shikijs/langs/css').then((module) => module.default),
|
|
23
|
+
dotenv: () => import('@shikijs/langs/dotenv').then((module) => module.default),
|
|
24
|
+
graphql: () => import('@shikijs/langs/graphql').then((module) => module.default),
|
|
25
|
+
html: () => import('@shikijs/langs/html').then((module) => module.default),
|
|
26
|
+
ini: () => import('@shikijs/langs/ini').then((module) => module.default),
|
|
27
|
+
javascript: () => import('@shikijs/langs/javascript').then((module) => module.default),
|
|
28
|
+
json: () => import('@shikijs/langs/json').then((module) => module.default),
|
|
29
|
+
jsonc: () => import('@shikijs/langs/jsonc').then((module) => module.default),
|
|
30
|
+
jsx: () => import('@shikijs/langs/jsx').then((module) => module.default),
|
|
31
|
+
markdown: () => import('@shikijs/langs/markdown').then((module) => module.default),
|
|
32
|
+
mdx: () => import('@shikijs/langs/mdx').then((module) => module.default),
|
|
33
|
+
python: () => import('@shikijs/langs/python').then((module) => module.default),
|
|
34
|
+
shellscript: () => import('@shikijs/langs/shellscript').then((module) => module.default),
|
|
35
|
+
sql: () => import('@shikijs/langs/sql').then((module) => module.default),
|
|
36
|
+
tsx: () => import('@shikijs/langs/tsx').then((module) => module.default),
|
|
37
|
+
typescript: () => import('@shikijs/langs/typescript').then((module) => module.default),
|
|
38
|
+
yaml: () => import('@shikijs/langs/yaml').then((module) => module.default),
|
|
39
|
+
} as const;
|
|
40
|
+
|
|
41
|
+
type SupportedLanguage = keyof typeof languageLoaders;
|
|
42
|
+
|
|
43
|
+
const themeLoaders = [
|
|
44
|
+
() => import('@shikijs/themes/github-light').then((module) => module.default),
|
|
45
|
+
() => import('@shikijs/themes/github-dark-default').then((module) => module.default),
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
let highlighterPromise: ReturnType<typeof createHighlighterCore> | null = null;
|
|
49
|
+
|
|
50
|
+
function getHighlighter(): ReturnType<typeof createHighlighterCore> {
|
|
51
|
+
highlighterPromise ??= Promise.all([
|
|
52
|
+
Promise.all(themeLoaders.map((loadTheme) => loadTheme())),
|
|
53
|
+
Promise.all(Object.values(languageLoaders).map((loadLanguage) => loadLanguage())),
|
|
54
|
+
]).then(([themes, languages]) =>
|
|
55
|
+
createHighlighterCore({
|
|
56
|
+
themes,
|
|
57
|
+
langs: languages.flat(),
|
|
58
|
+
engine: createJavaScriptRegexEngine(),
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return highlighterPromise;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const highlightedCodeCache = new Map<string, Promise<string>>();
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Highlight code using Shiki with all transformers.
|
|
69
|
+
* Uses Shiki's native notation for highlighting:
|
|
70
|
+
* - // [!code highlight] - highlight a line (use language-appropriate comment)
|
|
71
|
+
* - // [!code ++] / // [!code --] - diff highlighting
|
|
72
|
+
* - // [!code focus] - focus mode (blur other lines)
|
|
73
|
+
* - // [!code word:myVar] - highlight specific word
|
|
74
|
+
* - // [!code error] / // [!code warning] - error levels
|
|
75
|
+
*/
|
|
76
|
+
async function highlightCode(code: string, language: SupportedLanguage): Promise<string> {
|
|
77
|
+
const cacheKey = `${language}:${code}`;
|
|
78
|
+
const cached = highlightedCodeCache.get(cacheKey);
|
|
79
|
+
if (cached) return cached;
|
|
80
|
+
|
|
81
|
+
const highlighted = getHighlighter().then((highlighter) =>
|
|
82
|
+
highlighter.codeToHtml(code, {
|
|
83
|
+
lang: language,
|
|
84
|
+
themes: {
|
|
85
|
+
light: 'github-light',
|
|
86
|
+
dark: 'github-dark-default',
|
|
87
|
+
},
|
|
88
|
+
transformers: [
|
|
89
|
+
transformerNotationDiff({ matchAlgorithm: 'v3' }),
|
|
90
|
+
transformerNotationHighlight({ matchAlgorithm: 'v3' }),
|
|
91
|
+
transformerNotationWordHighlight({ matchAlgorithm: 'v3' }),
|
|
92
|
+
transformerNotationFocus({ matchAlgorithm: 'v3' }),
|
|
93
|
+
transformerNotationErrorLevel({ matchAlgorithm: 'v3' }),
|
|
94
|
+
],
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
highlightedCodeCache.set(cacheKey, highlighted);
|
|
98
|
+
return highlighted;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Language aliases that map to Shiki's bundled language names
|
|
103
|
+
*/
|
|
104
|
+
const LANGUAGE_ALIASES: Record<string, SupportedLanguage> = {
|
|
105
|
+
env: 'dotenv',
|
|
106
|
+
js: 'javascript',
|
|
107
|
+
ts: 'typescript',
|
|
108
|
+
sh: 'bash',
|
|
109
|
+
shell: 'bash',
|
|
110
|
+
yml: 'yaml',
|
|
111
|
+
py: 'python',
|
|
112
|
+
md: 'markdown',
|
|
113
|
+
plaintext: 'ini',
|
|
114
|
+
text: 'ini',
|
|
115
|
+
chroma: 'ini',
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Normalize language identifier for Shiki/BundledLanguage.
|
|
120
|
+
* Falls back to 'ini' for unsupported languages (minimal highlighting).
|
|
121
|
+
*/
|
|
122
|
+
function normalizeLanguage(lang?: string): SupportedLanguage {
|
|
123
|
+
const normalized = lang?.toLowerCase() || 'ini';
|
|
124
|
+
|
|
125
|
+
// Check for aliases first
|
|
126
|
+
const alias = LANGUAGE_ALIASES[normalized];
|
|
127
|
+
if (alias) {
|
|
128
|
+
return alias;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check if the language is one of the explicitly bundled grammars.
|
|
132
|
+
if (normalized in languageLoaders) {
|
|
133
|
+
return normalized as SupportedLanguage;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Fallback to 'ini' for unsupported languages (has minimal highlighting)
|
|
137
|
+
return 'ini';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export { getHighlighter, highlightCode, normalizeLanguage };
|
|
141
|
+
export type { SupportedLanguage };
|