Skip to content

Commit ab8d3ce

Browse files
fix: isTextmateTheme type guard for multi-theme validation
Extract isTextmateTheme as reusable type guard function and use it inside .some() callback to properly check each theme value. Previously the boolean was computed once on themeInput and incorrectly reused inside .some() where it should check each individual value. Amp-Thread-ID: https://ampcode.com/threads/T-019b0c55-150d-7167-815e-ed3140b50974 Co-authored-by: Amp <amp@ampcode.com>
1 parent c2d8962 commit ab8d3ce

File tree

2 files changed

+45
-7
lines changed

2 files changed

+45
-7
lines changed

package/src/lib/theme.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,31 +23,34 @@ export type ThemeShikiOptions =
2323
| CodeOptionsSingleTheme<BundledTheme>
2424
| CodeOptionsMultipleThemes<BundledTheme>;
2525

26+
const isTextmateTheme = (value: unknown): value is ThemeRegistrationAny =>
27+
typeof value === 'object' &&
28+
value !== null &&
29+
'tokenColors' in value &&
30+
Array.isArray((value as ThemeRegistrationAny).tokenColors);
31+
2632
export function resolveTheme(
2733
themeInput: Theme | Themes
2834
): ThemeResolution {
29-
const isTextmateTheme =
30-
typeof themeInput === 'object' &&
31-
'tokenColors' in themeInput &&
32-
Array.isArray(themeInput.tokenColors);
35+
const inputIsTextmateTheme = isTextmateTheme(themeInput);
3336

3437
// Non-textmate objects are assumed to be multi-theme configs
3538
const isMultiThemeConfig =
3639
typeof themeInput === 'object' &&
3740
themeInput !== null &&
38-
!isTextmateTheme;
41+
!inputIsTextmateTheme;
3942

4043
const validMultiThemeObj =
4144
typeof themeInput === 'object' &&
4245
themeInput !== null &&
43-
!isTextmateTheme &&
46+
!inputIsTextmateTheme &&
4447
Object.entries(themeInput).some(
4548
([key, value]) =>
4649
key &&
4750
value &&
4851
key.trim() !== '' &&
4952
value !== '' &&
50-
(typeof value === 'string' || isTextmateTheme)
53+
(typeof value === 'string' || isTextmateTheme(value))
5154
);
5255

5356
if (isMultiThemeConfig) {

package/tests/hook.test.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,41 @@ describe('useShikiHighlighter Hook', () => {
562562
// Should use custom prefix
563563
expect(styledToken?.htmlStyle?.['--theme-dark']).toBeDefined();
564564
});
565+
566+
test('multi-theme with two TextMate theme objects works', async () => {
567+
const code = 'const x = 1;';
568+
// Both values are TextMate theme objects (no strings)
569+
const lightTheme = {
570+
name: 'test-light',
571+
tokenColors: [
572+
{ scope: 'keyword', settings: { foreground: '#ff0000' } },
573+
],
574+
};
575+
const darkTheme = {
576+
name: 'test-dark',
577+
tokenColors: [
578+
{ scope: 'keyword', settings: { foreground: '#00ff00' } },
579+
],
580+
};
581+
const themes = { light: lightTheme, dark: darkTheme };
582+
583+
const { result } = renderHook(() =>
584+
useShikiHighlighter(code, 'javascript', themes, {
585+
outputFormat: 'tokens',
586+
})
587+
);
588+
589+
// Wait for highlighting to complete
590+
await waitFor(() => {
591+
const tokensResult = result.current as TokensResult;
592+
// Should have theme name from our custom theme, not fallback
593+
expect(tokensResult.themeName).not.toBe('');
594+
});
595+
596+
const tokensResult = result.current as TokensResult;
597+
// Verify it used our themes, not DEFAULT_THEMES fallback
598+
expect(tokensResult.tokens.length).toBeGreaterThan(0);
599+
});
565600
});
566601

567602
describe('TokenRenderer DOM Output', () => {

0 commit comments

Comments
 (0)