Skip to content

Commit 36bdfe5

Browse files
committed
first draft
1 parent b388e15 commit 36bdfe5

File tree

8 files changed

+97
-17
lines changed

8 files changed

+97
-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: 36 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,33 @@ 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 = (
163+
(tokensOutput as TokensResult).tokens ?? tokensOutput
164+
) as ThemedToken[][];
165+
166+
setHighlightedCode(tokens);
167+
} else {
168+
const output =
169+
stableOpts.outputFormat === 'html'
170+
? highlighter.codeToHtml(code, finalOptions)
171+
: toJsxRuntime(highlighter.codeToHast(code, finalOptions), {
172+
jsx,
173+
jsxs,
174+
Fragment,
175+
});
176+
setHighlightedCode(output);
177+
}
154178
}
155179
};
156180

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 { ShikiHighlighter, createJavaScriptRegexEngine } from '../src/index';
45

56
// Test fixtures
@@ -191,6 +192,26 @@ describe('ShikiHighlighter Component', () => {
191192
expect(container.querySelector('code')).toBeInTheDocument();
192193
});
193194
});
195+
196+
test('throws an error when outputFormat is tokens', () => {
197+
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
198+
199+
expect(() =>
200+
render(
201+
<ShikiHighlighter
202+
language="javascript"
203+
theme="github-dark"
204+
outputFormat="tokens"
205+
>
206+
{codeSample}
207+
</ShikiHighlighter>
208+
)
209+
).toThrowError(
210+
'ShikiHighlighter component does not support outputFormat="tokens". Use the useShikiHighlighter hook to access raw tokens.'
211+
);
212+
213+
spy.mockRestore();
214+
});
194215
});
195216

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

package/tests/hook.test.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { render, waitFor } from '@testing-library/react';
1+
import { render, renderHook, waitFor } from '@testing-library/react';
22
import { vi } from 'vitest';
33
import { useShikiHighlighter, createJavaScriptRegexEngine } from '../src/index';
4-
import type { Language, Theme, Themes } from '../src/lib/types';
4+
import type { Language, Theme, Themes, ThemedToken } from '../src/lib/types';
55
import type { ShikiTransformer } from 'shiki';
66
import { throttleHighlighting } from '../src/lib/utils';
77

@@ -13,7 +13,7 @@ interface TestComponentProps {
1313
langAlias?: Record<string, string>;
1414
showLineNumbers?: boolean;
1515
startingLineNumber?: number;
16-
outputFormat?: 'react' | 'html';
16+
outputFormat?: 'react' | 'html' | 'tokens';
1717
defaultColor?: string;
1818
cssVariablePrefix?: string;
1919
mergeWhitespaces?: boolean;
@@ -439,6 +439,29 @@ describe('useShikiHighlighter Hook', () => {
439439
});
440440
});
441441

442+
describe('Token Output Format', () => {
443+
test('returns themed tokens when outputFormat is tokens', async () => {
444+
const code = 'console.log("token test");';
445+
446+
const { result } = renderHook(() =>
447+
useShikiHighlighter(code, 'javascript', 'github-dark', {
448+
outputFormat: 'tokens',
449+
})
450+
);
451+
452+
await waitFor(() => {
453+
expect(result.current).not.toBeNull();
454+
});
455+
456+
const tokens = result.current as ThemedToken[][];
457+
458+
expect(Array.isArray(tokens)).toBe(true);
459+
expect(tokens.length).toBeGreaterThan(0);
460+
expect(Array.isArray(tokens[0])).toBe(true);
461+
expect(tokens[0]?.[0]?.content).toBeDefined();
462+
});
463+
});
464+
442465
describe('Output Format', () => {
443466
const sampleCode = 'const x = 1;\nconst y = 2;';
444467

0 commit comments

Comments
 (0)