rn-shiki 0.0.37-2 → 0.0.37-4

Sign up to get free protection for your applications and to get access to all the features.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rn-shiki",
3
3
  "type": "module",
4
- "version": "0.0.37-2",
4
+ "version": "0.0.37-4",
5
5
  "description": "Shiki syntax highlighter for React Native.",
6
6
  "author": "Ryan Skinner <hello@ryanskinner.com>",
7
7
  "license": "MIT",
@@ -1,9 +1,7 @@
1
- import type { SyntaxHighlighterProps } from 'src/types/shiki'
2
1
  import React from 'react'
3
- import { Platform, ScrollView, StyleSheet, Text, View } from 'react-native'
2
+ import { Platform, ScrollView, type ScrollViewProps, StyleSheet, Text, type TextStyle, View } from 'react-native'
4
3
  import { useSyntaxHighlighter } from '../../hooks/useSyntaxHighlighter'
5
4
  import { convertShikiTheme } from '../../utils/style-transformer'
6
- import SyntaxLine from './SyntaxLine'
7
5
 
8
6
  const monospaceFont = Platform.select({
9
7
  ios: 'Menlo',
@@ -11,83 +9,72 @@ const monospaceFont = Platform.select({
11
9
  default: 'monospace',
12
10
  })
13
11
 
12
+ interface SyntaxHighlighterProps {
13
+ text: string
14
+ language: any
15
+ languageId: string
16
+ theme: any
17
+ textStyle?: TextStyle
18
+ scrollViewProps?: ScrollViewProps
19
+ }
20
+
14
21
  const styles = StyleSheet.create({
15
- container: {
22
+ scrollView: {
16
23
  flex: 1,
17
24
  minHeight: 20,
18
25
  },
19
- scrollContent: {
20
- flexGrow: 1,
21
- padding: 8,
22
- },
23
26
  contentContainer: {
24
- minWidth: '100%',
25
- },
26
- errorText: {
27
- padding: 8,
28
- color: '#ff6b6b',
27
+ flexGrow: 1,
29
28
  },
30
- loadingText: {
31
- padding: 8,
32
- color: '#666666',
29
+ lineContainer: {
30
+ flexDirection: 'row',
31
+ flexWrap: 'wrap',
33
32
  },
34
33
  })
35
34
 
36
- function SyntaxHighlighter({ text, language, languageId, theme, fontSize = 14 }: SyntaxHighlighterProps) {
37
- // Convert theme on mount
38
- const themeStyles = React.useMemo(() => convertShikiTheme(theme ?? {}), [theme])
39
- const { tokens, error, isLoading } = useSyntaxHighlighter({
35
+ const SyntaxHighlighter: React.FC<SyntaxHighlighterProps> = ({ text, language, languageId, theme, textStyle, scrollViewProps }) => {
36
+ const themeStyles = React.useMemo(() => convertShikiTheme(theme), [theme])
37
+ const { tokens } = useSyntaxHighlighter({
40
38
  text,
41
39
  language,
42
40
  languageId,
43
41
  theme,
44
42
  })
45
43
 
46
- if (error) {
47
- return (
48
- <Text
49
- style={[
50
- styles.errorText,
51
- {
52
- fontFamily: monospaceFont,
53
- fontSize,
54
- },
55
- ]}
56
- >
57
- Error:
58
- {' '}
59
- {error}
60
- </Text>
61
- )
62
- }
44
+ const renderToken = (token: any, index: number, lineIndex: number) => {
45
+ const tokenStyle: TextStyle = {
46
+ ...themeStyles.token,
47
+ fontFamily: monospaceFont,
48
+ ...(token.color && { color: token.color }),
49
+ ...(token.fontStyle === 'italic' && themeStyles.italic),
50
+ ...(token.fontWeight === 'bold' && themeStyles.bold),
51
+ ...textStyle,
52
+ }
63
53
 
64
- if (isLoading) {
65
54
  return (
66
- <Text
67
- style={[
68
- styles.loadingText,
69
- {
70
- fontFamily: monospaceFont,
71
- fontSize,
72
- },
73
- ]}
74
- >
75
- Loading...
55
+ <Text key={`${lineIndex}-${index}`} style={tokenStyle}>
56
+ {token.content.replace(/ /g, '\u00A0')}
76
57
  </Text>
77
58
  )
78
59
  }
79
60
 
80
- return (
81
- <View style={[styles.container, { backgroundColor: themeStyles.backgroundColor }]}>
82
- <ScrollView horizontal showsHorizontalScrollIndicator={Platform.OS !== 'web'} contentContainerStyle={{ flexGrow: 1 }}>
83
- <View style={styles.contentContainer}>
84
- {tokens.map((line, lineIndex) => (
85
- <SyntaxLine key={`line-${lineIndex}`} line={line} fontSize={fontSize} isLastLine={lineIndex === tokens.length - 1} themeStyles={themeStyles} />
86
- ))}
87
- </View>
88
- </ScrollView>
61
+ const renderLine = (line: any[], lineIndex: number) => (
62
+ <View key={lineIndex} style={styles.lineContainer}>
63
+ {line.map((token, tokenIndex) => renderToken(token, tokenIndex, lineIndex))}
89
64
  </View>
90
65
  )
66
+
67
+ return (
68
+ <ScrollView
69
+ horizontal
70
+ showsHorizontalScrollIndicator={Platform.OS !== 'web'}
71
+ style={styles.scrollView}
72
+ contentContainerStyle={[styles.contentContainer, themeStyles.container, scrollViewProps?.contentContainerStyle]}
73
+ {...scrollViewProps}
74
+ >
75
+ <View onStartShouldSetResponder={() => true}>{tokens.map((line, index) => renderLine(line, index))}</View>
76
+ </ScrollView>
77
+ )
91
78
  }
92
79
 
93
80
  export default SyntaxHighlighter
@@ -1,4 +1,5 @@
1
- import type { SyntaxHighlighterProps, ThemedToken, TokensResult } from '../types/shiki'
1
+ import type { ThemedToken } from 'shiki'
2
+ import type { SyntaxHighlighterProps, TokensResult } from '../types/shiki'
2
3
  import { useEffect, useState } from 'react'
3
4
  import { createHighlighter } from '../syntax'
4
5
 
@@ -19,14 +20,22 @@ export function useSyntaxHighlighter({ text, language, languageId, theme }: Synt
19
20
  }
20
21
 
21
22
  try {
23
+ // First create the highlighter
22
24
  const highlighter = await createHighlighter({
23
25
  langs: [language],
24
- themes: theme ? [theme] : [],
26
+ themes: [theme],
25
27
  })
26
28
 
29
+ // Get resolved theme name
30
+ const themeResolved = highlighter.getLoadedThemes()?.[0]
31
+ if (!themeResolved) {
32
+ throw new Error('Failed to resolve theme')
33
+ }
34
+
35
+ // Use resolved theme for tokenization
27
36
  const result = highlighter.codeToTokens(text, {
28
37
  lang: languageId,
29
- theme: theme || 'none',
38
+ theme: themeResolved,
30
39
  }) as TokensResult
31
40
 
32
41
  if (mounted && result.tokens) {
@@ -3,17 +3,30 @@ import type { TokenType } from '../../types'
3
3
  import { createHighlighterCore } from 'shiki/dist/core.mjs'
4
4
  import { createJavaScriptRegexEngine } from 'shiki/dist/engine-javascript.mjs'
5
5
 
6
- async function initializeHighlighter(langs: LanguageInput[], themes: ThemeInput[]): Promise<Awaited<ReturnType<typeof createHighlighterCore>>> {
7
- return createHighlighterCore({
8
- engine: createJavaScriptRegexEngine(),
9
- langs,
10
- themes,
11
- })
6
+ const engine = createJavaScriptRegexEngine({
7
+ forgiving: true,
8
+ simulation: true,
9
+ cache: new Map(),
10
+ })
11
+
12
+ async function initializeHighlighter(langs: LanguageInput[], themes: ThemeInput[]) {
13
+ try {
14
+ // Initialize the core highlighter with our configured engine
15
+ return await createHighlighterCore({
16
+ langs,
17
+ themes,
18
+ engine, // Pass the configured engine
19
+ })
20
+ }
21
+ catch (err) {
22
+ console.error('Error initializing highlighter:', err)
23
+ throw err
24
+ }
12
25
  }
13
26
 
14
- export async function createHighlighter({ langs, themes }: { langs: LanguageInput[], themes: ThemeInput[] }): Promise<Awaited<ReturnType<typeof createHighlighterCore>>> {
15
- if (!langs?.length || !themes?.length) {
16
- throw new Error('Please provide both `langs` and `themes` when creating a highlighter.')
27
+ export async function createHighlighter({ langs, themes }: { langs: LanguageInput[], themes: ThemeInput[] }) {
28
+ if (!langs?.length) {
29
+ throw new Error('Please provide languages when creating a highlighter.')
17
30
  }
18
31
 
19
32
  try {
@@ -1,20 +1,19 @@
1
- import type { LanguageInput, ThemeRegistrationAny } from 'shiki'
1
+ import type { LanguageInput, ThemedToken, ThemeInput } from 'shiki'
2
2
 
3
- export interface ThemedToken {
3
+ export interface TokenType {
4
4
  content: string
5
5
  color?: string
6
6
  fontStyle?: string
7
- fontWeight?: string
8
- }
9
-
10
- export interface TokensResult {
11
- tokens: ThemedToken[][]
12
7
  }
13
8
 
14
9
  export interface SyntaxHighlighterProps {
15
10
  text: string
16
11
  language: LanguageInput
17
12
  languageId: string
18
- theme?: ThemeRegistrationAny
13
+ theme: ThemeInput
19
14
  fontSize?: number
20
15
  }
16
+
17
+ export interface TokensResult {
18
+ tokens: ThemedToken[][]
19
+ }
@@ -1,71 +1,108 @@
1
- import type { TextStyle } from 'react-native'
2
- import type { ThemeRegistrationAny } from 'shiki'
1
+ import type { RawThemeSetting, ThemeRegistrationAny } from 'shiki'
2
+ import { StyleSheet, type TextStyle } from 'react-native'
3
3
 
4
- export interface RNTokenStyle {
5
- color?: string
6
- backgroundColor?: string
7
- fontWeight?: TextStyle['fontWeight']
8
- fontStyle?: TextStyle['fontStyle']
4
+ export interface HighlighterStyleSheet {
5
+ container: TextStyle
6
+ token: TextStyle
7
+ selection: TextStyle
8
+ styles: Record<string, TextStyle>
9
+ [key: string]: TextStyle | Record<string, TextStyle>
9
10
  }
10
11
 
11
- const ALLOWED_STYLE_PROPERTIES = {
12
- 'color': true,
13
- 'background': true,
14
- 'background-color': true,
15
- 'font-weight': true,
16
- 'font-style': true,
17
- } as const
18
-
19
- function transformValue(key: string, value: string): any {
20
- switch (key) {
21
- case 'background':
22
- case 'background-color':
23
- return ['backgroundColor', value]
24
- case 'font-weight':
25
- return ['fontWeight', value === 'bold' ? 'bold' : 'normal']
26
- case 'font-style':
27
- return ['fontStyle', value === 'italic' ? 'italic' : 'normal']
28
- default:
29
- return [key, value]
30
- }
12
+ interface TokenStyle {
13
+ color?: string
14
+ fontStyle?: 'normal' | 'italic'
15
+ fontWeight?: 'normal' | 'bold'
31
16
  }
32
17
 
33
- export function convertShikiTheme(theme: ThemeRegistrationAny): {
34
- backgroundColor: string
35
- defaultColor: string
36
- styles: Record<string, RNTokenStyle>
37
- } {
38
- // Extract theme colors
39
- const backgroundColor = theme.bg || '#1e1e1e'
40
- const defaultColor = theme.fg || '#d4d4d4'
18
+ function processSettings(settings: RawThemeSetting[]): Record<string, TokenStyle> {
19
+ const tokenStyles: Record<string, TokenStyle> = {}
41
20
 
42
- // Convert token colors
43
- const styles: Record<string, RNTokenStyle> = {}
44
-
45
- theme.settings?.forEach((setting) => {
21
+ settings.forEach((setting) => {
46
22
  if (!setting.scope || !setting.settings)
47
23
  return
48
24
 
49
- const scopes = Array.isArray(setting.scope) ? setting.scope : [setting.scope]
50
- const style: RNTokenStyle = {}
25
+ const style: TokenStyle = {}
26
+ const { foreground, fontStyle } = setting.settings
27
+
28
+ if (foreground) {
29
+ style.color = foreground
30
+ }
51
31
 
52
- // Convert CSS-style properties to RN style properties
53
- Object.entries(setting.settings).forEach(([key, value]) => {
54
- if (ALLOWED_STYLE_PROPERTIES[key as keyof typeof ALLOWED_STYLE_PROPERTIES]) {
55
- const [rnKey, rnValue] = transformValue(key, value as string)
56
- style[rnKey as keyof RNTokenStyle] = rnValue
32
+ if (fontStyle) {
33
+ if (fontStyle.includes('italic')) {
34
+ style.fontStyle = 'italic'
57
35
  }
58
- })
36
+ if (fontStyle.includes('bold')) {
37
+ style.fontWeight = 'bold'
38
+ }
39
+ }
59
40
 
60
- // Apply style to each scope
61
- scopes.forEach((scope) => {
62
- styles[scope] = style
63
- })
41
+ // Handle single scope
42
+ if (typeof setting.scope === 'string') {
43
+ tokenStyles[setting.scope] = style
44
+ }
45
+ // Handle multiple scopes
46
+ else if (Array.isArray(setting.scope)) {
47
+ setting.scope.forEach((scope) => {
48
+ tokenStyles[scope] = style
49
+ })
50
+ }
64
51
  })
65
52
 
53
+ return tokenStyles
54
+ }
55
+
56
+ export function convertShikiTheme(theme: ThemeRegistrationAny): ReturnType<typeof StyleSheet.create> {
57
+ // Get all styles from both settings and tokenColors
58
+ const settings = [...(theme.settings || []), ...(theme.tokenColors || [])]
59
+
60
+ // Process basic colors from the theme
61
+ const backgroundColor = theme.colors?.['editor.background'] || theme.bg || '#1e1e1e'
62
+ const defaultColor = theme.colors?.['editor.foreground'] || theme.fg || '#d4d4d4'
63
+ const selectionColor = theme.colors?.['editor.selectionBackground'] || 'rgba(255, 255, 255, 0.1)'
64
+
65
+ // Process token styles
66
+ const tokenStyles = processSettings(settings)
67
+
68
+ // Convert token styles to RN styles
69
+ const tokenStylesMap = Object.entries(tokenStyles).reduce<Record<string, TextStyle>>((acc, [scope, style]) => {
70
+ acc[`token-${scope.replace(/\./g, '-')}`] = {
71
+ color: style.color,
72
+ ...(style.fontStyle && { fontStyle: style.fontStyle }),
73
+ ...(style.fontWeight && { fontWeight: style.fontWeight }),
74
+ }
75
+ return acc
76
+ }, {})
77
+
78
+ // Create the complete styles object
79
+ const completeStyles: HighlighterStyleSheet = {
80
+ container: {
81
+ backgroundColor,
82
+ flex: 1,
83
+ padding: 16,
84
+ },
85
+ token: {
86
+ color: defaultColor,
87
+ fontSize: 14,
88
+ lineHeight: 21,
89
+ },
90
+ selection: {
91
+ backgroundColor: selectionColor,
92
+ },
93
+ styles: tokenStylesMap,
94
+ }
95
+
96
+ return StyleSheet.create(completeStyles)
97
+ }
98
+
99
+ export function getTokenStyle(theme: HighlighterStyleSheet, tokenTypes: string[], defaultColor: string): TextStyle {
100
+ const matchingStyles = tokenTypes.map(type => theme.styles[`token-${type.replace(/\./g, '-')}`]).filter(Boolean)
101
+
66
102
  return {
67
- backgroundColor,
68
- defaultColor,
69
- styles,
103
+ ...theme.token,
104
+ color: matchingStyles[0]?.color || defaultColor,
105
+ ...(matchingStyles.some(s => s?.fontStyle === 'italic') && { fontStyle: 'italic' }),
106
+ ...(matchingStyles.some(s => s?.fontWeight === 'bold') && { fontWeight: 'bold' }),
70
107
  }
71
108
  }