Skip to content

Commit 074f18b

Browse files
committed
first draft
1 parent e084633 commit 074f18b

File tree

8 files changed

+101
-17
lines changed

8 files changed

+101
-17
lines changed

package/src/core.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type {
1818
Themes,
1919
Element,
2020
HighlighterOptions,
21+
ThemedToken,
2122
} from './lib/types';
2223

2324
export { createHighlighterCore } from 'shiki/core';

package/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type {
1818
Themes,
1919
Element,
2020
HighlighterOptions,
21+
ThemedToken,
2122
} from './lib/types';
2223

2324
export {

package/src/lib/component.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ export const createShikiHighlighterComponent = (
136136
...shikiOptions,
137137
};
138138

139+
if (options.outputFormat === 'tokens') {
140+
throw new Error(
141+
'ShikiHighlighter component does not support outputFormat="tokens". Use the useShikiHighlighter hook to access raw tokens.'
142+
);
143+
}
144+
139145
// Use resolveLanguage to get displayLanguageId directly
140146
const { displayLanguageId } = resolveLanguage(
141147
language,

package/src/lib/hook.ts

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import type {
1818
Awaitable,
1919
RegexEngine,
2020
BundledTheme,
21+
CodeToTokensBaseOptions,
22+
CodeToTokensOptions,
23+
TokensResult,
24+
ThemedToken,
2125
} from 'shiki';
2226

2327
import type { ShikiLanguageRegistration } from './extended-types';
@@ -61,7 +65,7 @@ export const useShikiHighlighter = (
6165
) => Promise<Highlighter | HighlighterCore>
6266
) => {
6367
const [highlightedCode, setHighlightedCode] = useState<
64-
ReactNode | string | null
68+
ReactNode | string | ThemedToken[][] | null
6569
>(null);
6670

6771
// Stabilize options, language and theme inputs to prevent unnecessary
@@ -108,8 +112,10 @@ export const useShikiHighlighter = (
108112
theme: singleTheme || DEFAULT_THEMES.dark,
109113
} as CodeOptionsSingleTheme<BundledTheme>);
110114

111-
const transformers = restOptions.transformers || [];
112-
if (showLineNumbers) {
115+
const isTokensOutput = stableOpts.outputFormat === 'tokens';
116+
const transformers = [...(restOptions.transformers || [])];
117+
118+
if (showLineNumbers && !isTokensOutput) {
113119
transformers.push(lineNumbersTransformer(startingLineNumber));
114120
}
115121

@@ -142,15 +148,32 @@ export const useShikiHighlighter = (
142148
const finalOptions = { ...shikiOptions, lang: langToUse };
143149

144150
if (isMounted) {
145-
const output =
146-
stableOpts.outputFormat === 'html'
147-
? highlighter.codeToHtml(code, finalOptions)
148-
: toJsxRuntime(highlighter.codeToHast(code, finalOptions), {
149-
jsx,
150-
jsxs,
151-
Fragment,
152-
});
153-
setHighlightedCode(output);
151+
if (stableOpts.outputFormat === 'tokens') {
152+
const tokensOutput = isMultiTheme
153+
? highlighter.codeToTokens(
154+
code,
155+
finalOptions as CodeToTokensOptions
156+
)
157+
: highlighter.codeToTokensBase(
158+
code,
159+
finalOptions as CodeToTokensBaseOptions
160+
);
161+
162+
const tokens = ((tokensOutput as TokensResult).tokens ??
163+
tokensOutput) as ThemedToken[][];
164+
165+
setHighlightedCode(tokens);
166+
} else {
167+
const output =
168+
stableOpts.outputFormat === 'html'
169+
? highlighter.codeToHtml(code, finalOptions)
170+
: toJsxRuntime(highlighter.codeToHast(code, finalOptions), {
171+
jsx,
172+
jsxs,
173+
Fragment,
174+
});
175+
setHighlightedCode(output);
176+
}
154177
}
155178
};
156179

package/src/lib/types.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
BundledHighlighterOptions,
1212
Awaitable,
1313
RegexEngine,
14+
ThemedToken,
1415
} from 'shiki';
1516

1617
import type { ReactNode } from 'react';
@@ -78,9 +79,10 @@ interface ReactShikiOptions {
7879
* Output format for the highlighted code.
7980
* - 'react': Returns React nodes (default, safer)
8081
* - 'html': Returns HTML string (~15-45% faster, requires dangerouslySetInnerHTML)
82+
* - 'tokens': Returns raw Shiki tokens (array of themed tokens per line)
8183
* @default 'react'
8284
*/
83-
outputFormat?: 'react' | 'html';
85+
outputFormat?: 'react' | 'html' | 'tokens';
8486

8587
/**
8688
* Custom Shiki highlighter instance to use instead of the default one.
@@ -162,7 +164,7 @@ export type UseShikiHighlighter = (
162164
lang: Language,
163165
themeInput: Theme | Themes,
164166
options?: HighlighterOptions
165-
) => ReactNode | string | null;
167+
) => ReactNode | string | ThemedToken[][] | null;
166168

167169
export type {
168170
Language,
@@ -171,4 +173,5 @@ export type {
171173
Element,
172174
TimeoutState,
173175
HighlighterOptions,
176+
ThemedToken,
174177
};

package/src/web.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type {
1818
Themes,
1919
Element,
2020
HighlighterOptions,
21+
ThemedToken,
2122
} from './lib/types';
2223

2324
export {

package/tests/component.test.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useRef } from 'react';
22
import { render, waitFor } from '@testing-library/react';
3+
import { vi } from 'vitest';
34
import {
45
ShikiHighlighter,
56
createJavaScriptRegexEngine,
@@ -194,6 +195,26 @@ describe('ShikiHighlighter Component', () => {
194195
expect(container.querySelector('code')).toBeInTheDocument();
195196
});
196197
});
198+
199+
test('throws an error when outputFormat is tokens', () => {
200+
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
201+
202+
expect(() =>
203+
render(
204+
<ShikiHighlighter
205+
language="javascript"
206+
theme="github-dark"
207+
outputFormat="tokens"
208+
>
209+
{codeSample}
210+
</ShikiHighlighter>
211+
)
212+
).toThrowError(
213+
'ShikiHighlighter component does not support outputFormat="tokens". Use the useShikiHighlighter hook to access raw tokens.'
214+
);
215+
216+
spy.mockRestore();
217+
});
197218
});
198219

199220
describe('Ref Forwarding', () => {

package/tests/hook.test.tsx

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
import { render, waitFor } from '@testing-library/react';
1+
import { render, renderHook, waitFor } from '@testing-library/react';
22
import { vi } from 'vitest';
33
import {
44
useShikiHighlighter,
55
createJavaScriptRegexEngine,
66
} from '../src/index';
7-
import type { Language, Theme, Themes } from '../src/lib/types';
7+
import type {
8+
Language,
9+
Theme,
10+
Themes,
11+
ThemedToken,
12+
} from '../src/lib/types';
813
import type { ShikiTransformer } from 'shiki';
914
import { throttleHighlighting } from '../src/lib/utils';
1015

@@ -16,7 +21,7 @@ interface TestComponentProps {
1621
langAlias?: Record<string, string>;
1722
showLineNumbers?: boolean;
1823
startingLineNumber?: number;
19-
outputFormat?: 'react' | 'html';
24+
outputFormat?: 'react' | 'html' | 'tokens';
2025
defaultColor?: string;
2126
cssVariablePrefix?: string;
2227
mergeWhitespaces?: boolean;
@@ -460,6 +465,29 @@ describe('useShikiHighlighter Hook', () => {
460465
});
461466
});
462467

468+
describe('Token Output Format', () => {
469+
test('returns themed tokens when outputFormat is tokens', async () => {
470+
const code = 'console.log("token test");';
471+
472+
const { result } = renderHook(() =>
473+
useShikiHighlighter(code, 'javascript', 'github-dark', {
474+
outputFormat: 'tokens',
475+
})
476+
);
477+
478+
await waitFor(() => {
479+
expect(result.current).not.toBeNull();
480+
});
481+
482+
const tokens = result.current as ThemedToken[][];
483+
484+
expect(Array.isArray(tokens)).toBe(true);
485+
expect(tokens.length).toBeGreaterThan(0);
486+
expect(Array.isArray(tokens[0])).toBe(true);
487+
expect(tokens[0]?.[0]?.content).toBeDefined();
488+
});
489+
});
490+
463491
describe('Output Format', () => {
464492
const sampleCode = 'const x = 1;\nconst y = 2;';
465493

0 commit comments

Comments
 (0)