rn-shiki 0.0.37-9 → 0.0.37

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-9",
4
+ "version": "0.0.37",
5
5
  "description": "Shiki syntax highlighter for React Native.",
6
6
  "author": "Ryan Skinner <hello@ryanskinner.com>",
7
7
  "license": "MIT",
@@ -34,17 +34,21 @@
34
34
  "shiki": "^1.1.7"
35
35
  },
36
36
  "dependencies": {
37
- "css-to-react-native": "^3.2.0",
37
+ "esbuild": "^0.24.0",
38
38
  "shiki": "^1.1.7"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@antfu/eslint-config": "^3.8.0",
42
+ "@babel/core": "^7.24.0",
43
+ "@babel/preset-env": "^7.26.0",
42
44
  "@eslint-react/eslint-plugin": "^1.15.0",
43
45
  "@types/react": "^18.2.0",
44
46
  "@types/react-native": "^0.73.0",
47
+ "babel-plugin-module-resolver": "^5.0.2",
45
48
  "eslint": "^9.13.0",
46
49
  "eslint-plugin-react-hooks": "^5.0.0",
47
50
  "eslint-plugin-react-refresh": "^0.4.13",
51
+ "metro-react-native-babel-preset": "^0.77.0",
48
52
  "react": "^18.2.0",
49
53
  "react-native": "^0.73.0",
50
54
  "react-native-builder-bob": "^0.30.2",
@@ -1,9 +1,9 @@
1
1
  import type { SyntaxHighlighterProps } from 'src/types/shiki'
2
- import type { ReactStyle } from '../../utils/style-transformer'
3
- import React, { useMemo } from 'react'
4
- import { Platform, ScrollView, StyleSheet, Text, type TextStyle, View } from 'react-native'
2
+ import React from 'react'
3
+ import { Platform, ScrollView, StyleSheet, Text, View } from 'react-native'
4
+ import { convertShikiTheme } from 'src/utils'
5
5
  import { useSyntaxHighlighter } from '../../hooks/useSyntaxHighlighter'
6
- import { getRNStylesFromShikiStyle } from '../../utils/style-transformer'
6
+ import SyntaxLine from './SyntaxLine'
7
7
 
8
8
  const monospaceFont = Platform.select({
9
9
  ios: 'Menlo',
@@ -12,53 +12,81 @@ const monospaceFont = Platform.select({
12
12
  })
13
13
 
14
14
  const styles = StyleSheet.create({
15
- scrollView: {
15
+ container: {
16
16
  flex: 1,
17
17
  minHeight: 20,
18
18
  },
19
- lineContainer: {
20
- flexDirection: 'row',
21
- flexWrap: 'wrap',
19
+ scrollContent: {
20
+ flexGrow: 1,
21
+ padding: 8,
22
+ },
23
+ contentContainer: {
24
+ minWidth: '100%',
25
+ },
26
+ errorText: {
27
+ padding: 8,
28
+ color: '#ff6b6b',
29
+ },
30
+ loadingText: {
31
+ padding: 8,
32
+ color: '#666666',
22
33
  },
23
34
  })
24
35
 
25
- const SyntaxHighlighter: React.FC<SyntaxHighlighterProps> = ({ text, language, languageId, theme }) => {
26
- const stylesheet = useMemo(() => getRNStylesFromShikiStyle(theme as ReactStyle), [theme])
27
- const { tokens } = useSyntaxHighlighter({
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({
28
40
  text,
29
41
  language,
30
42
  languageId,
31
43
  theme,
32
44
  })
33
45
 
34
- const renderToken = (token: any, index: number, lineIndex: number) => {
35
- const tokenStyles: TextStyle = StyleSheet.flatten([
36
- stylesheet.token,
37
- {
38
- fontFamily: monospaceFont,
39
- ...(token.color && { color: token.color }),
40
- ...(token.fontStyle === 'italic' && stylesheet.italic),
41
- ...(token.fontWeight === 'bold' && stylesheet.bold),
42
- },
43
- ])
44
-
46
+ if (error) {
45
47
  return (
46
- <Text key={`${lineIndex}-${index}`} style={tokenStyles}>
47
- {token.content.replace(/ /g, '\u00A0')}
48
+ <Text
49
+ style={[
50
+ styles.errorText,
51
+ {
52
+ fontFamily: monospaceFont,
53
+ fontSize,
54
+ },
55
+ ]}
56
+ >
57
+ Error:
58
+ {' '}
59
+ {error}
48
60
  </Text>
49
61
  )
50
62
  }
51
63
 
52
- const renderLine = (line: any[], lineIndex: number) => (
53
- <View key={lineIndex} style={styles.lineContainer}>
54
- {line.map((token, tokenIndex) => renderToken(token, tokenIndex, lineIndex))}
55
- </View>
56
- )
64
+ if (isLoading) {
65
+ return (
66
+ <Text
67
+ style={[
68
+ styles.loadingText,
69
+ {
70
+ fontFamily: monospaceFont,
71
+ fontSize,
72
+ },
73
+ ]}
74
+ >
75
+ Loading...
76
+ </Text>
77
+ )
78
+ }
57
79
 
58
80
  return (
59
- <ScrollView horizontal showsHorizontalScrollIndicator={Platform.OS !== 'web'} style={styles.scrollView}>
60
- <View onStartShouldSetResponder={() => true}>{tokens.map((line, index) => renderLine(line, index))}</View>
61
- </ScrollView>
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>
89
+ </View>
62
90
  )
63
91
  }
64
92
 
@@ -0,0 +1,81 @@
1
+ import type { RNTokenStyle } from 'src/utils/style-transformer'
2
+ import type { TokenType } from '../../types'
3
+ import React from 'react'
4
+ import { Platform, StyleSheet, Text, View } from 'react-native'
5
+
6
+ const monospaceFont = Platform.select({
7
+ ios: 'Menlo',
8
+ android: 'monospace',
9
+ default: 'monospace',
10
+ })
11
+
12
+ const styles = StyleSheet.create({
13
+ lineContainer: {
14
+ flexDirection: 'row',
15
+ flexWrap: 'wrap',
16
+ },
17
+ token: {
18
+ includeFontPadding: false,
19
+ },
20
+ })
21
+
22
+ interface SyntaxLineProps {
23
+ line: TokenType[]
24
+ fontSize?: number
25
+ isLastLine?: boolean
26
+ themeStyles: {
27
+ backgroundColor: string
28
+ defaultColor: string
29
+ styles: Record<string, RNTokenStyle>
30
+ }
31
+ }
32
+
33
+ function SyntaxLine({ line, fontSize = 14, isLastLine = false, themeStyles }: SyntaxLineProps) {
34
+ const lineHeight = Math.floor(fontSize * 1.5)
35
+
36
+ return (
37
+ <View
38
+ style={[
39
+ styles.lineContainer,
40
+ {
41
+ minHeight: lineHeight,
42
+ paddingBottom: !isLastLine ? 4 : 0,
43
+ },
44
+ ]}
45
+ >
46
+ {line.map((token, tokenIndex) => {
47
+ const content = token.content.replace(/ /g, '\u00A0')
48
+ // Use converted theme styles
49
+ const tokenStyle = token.color ? { color: token.color } : { color: themeStyles.defaultColor }
50
+
51
+ return (
52
+ <Text
53
+ key={`${tokenIndex}-${content.slice(0, 8)}`}
54
+ style={[
55
+ styles.token,
56
+ {
57
+ ...tokenStyle,
58
+ fontFamily: monospaceFont,
59
+ fontSize,
60
+ fontStyle: token.fontStyle as 'normal' | 'italic',
61
+ fontWeight: token.fontWeight as 'normal' | 'bold',
62
+ lineHeight,
63
+ height: lineHeight,
64
+ textAlignVertical: 'center',
65
+ ...(Platform.OS === 'ios'
66
+ ? {
67
+ paddingTop: 2,
68
+ }
69
+ : {}),
70
+ },
71
+ ]}
72
+ >
73
+ {content}
74
+ </Text>
75
+ )
76
+ })}
77
+ </View>
78
+ )
79
+ }
80
+
81
+ export default SyntaxLine
@@ -0,0 +1,2 @@
1
+ export { default as SyntaxHighlighter } from './SyntaxHighlighter'
2
+ export { default as SyntaxLine } from './SyntaxLine'
@@ -1,7 +1,6 @@
1
- import type { ThemedToken } from 'shiki'
2
- import type { SyntaxHighlighterProps, TokensResult } from '../types/shiki'
1
+ import type { SyntaxHighlighterProps, ThemedToken, TokensResult } from '../types/shiki'
3
2
  import { useEffect, useState } from 'react'
4
- import { createHighlighter } from '../syntax/highlighter'
3
+ import { createHighlighter } from '../syntax'
5
4
 
6
5
  export function useSyntaxHighlighter({ text, language, languageId, theme }: SyntaxHighlighterProps) {
7
6
  const [tokens, setTokens] = useState<ThemedToken[][]>([])
@@ -20,22 +19,14 @@ export function useSyntaxHighlighter({ text, language, languageId, theme }: Synt
20
19
  }
21
20
 
22
21
  try {
23
- // First create the highlighter
24
22
  const highlighter = await createHighlighter({
25
23
  langs: [language],
26
- themes: [theme],
24
+ themes: theme ? [theme] : [],
27
25
  })
28
26
 
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
36
27
  const result = highlighter.codeToTokens(text, {
37
28
  lang: languageId,
38
- theme: themeResolved,
29
+ theme: theme || 'none',
39
30
  }) as TokensResult
40
31
 
41
32
  if (mounted && result.tokens) {
package/src/index.ts CHANGED
@@ -1,3 +1,6 @@
1
1
  import SyntaxHighlighter from './components/syntax/SyntaxHighlighter'
2
+ import SyntaxLine from './components/syntax/SyntaxLine'
2
3
 
3
- export { SyntaxHighlighter }
4
+ export * from './types/shiki'
5
+
6
+ export { SyntaxHighlighter, SyntaxLine }
@@ -1,32 +1,19 @@
1
1
  import type { LanguageInput, ThemedToken, ThemeInput } from 'shiki'
2
- import type { TokenType } from '../types/shiki'
2
+ 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
- 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
- }
6
+ async function initializeHighlighter(langs: LanguageInput[], themes: ThemeInput[]): Promise<Awaited<ReturnType<typeof createHighlighterCore>>> {
7
+ return createHighlighterCore({
8
+ engine: createJavaScriptRegexEngine(),
9
+ langs,
10
+ themes,
11
+ })
25
12
  }
26
13
 
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.')
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.')
30
17
  }
31
18
 
32
19
  try {
@@ -0,0 +1 @@
1
+ export { createHighlighter, processTokens } from './highlighter'
@@ -0,0 +1,6 @@
1
+ export interface TokenType {
2
+ content: string
3
+ color?: string
4
+ fontStyle?: string
5
+ fontWeight?: string
6
+ }
@@ -1,19 +1,20 @@
1
- import type { LanguageInput, ThemedToken, ThemeInput } from 'shiki'
1
+ import type { LanguageInput, ThemeRegistrationAny } from 'shiki'
2
2
 
3
- export interface TokenType {
3
+ export interface ThemedToken {
4
4
  content: string
5
5
  color?: string
6
6
  fontStyle?: string
7
+ fontWeight?: string
8
+ }
9
+
10
+ export interface TokensResult {
11
+ tokens: ThemedToken[][]
7
12
  }
8
13
 
9
14
  export interface SyntaxHighlighterProps {
10
15
  text: string
11
16
  language: LanguageInput
12
17
  languageId: string
13
- theme: ThemeInput
18
+ theme?: ThemeRegistrationAny
14
19
  fontSize?: number
15
20
  }
16
-
17
- export interface TokensResult {
18
- tokens: ThemedToken[][]
19
- }
@@ -0,0 +1,2 @@
1
+ export * from './string'
2
+ export * from './style-transformer'
@@ -1,28 +1,71 @@
1
- import type { CSSProperties } from 'react'
2
1
  import type { TextStyle } from 'react-native'
3
- import transform, { type StyleTuple } from 'css-to-react-native'
2
+ import type { ThemeRegistrationAny } from 'shiki'
4
3
 
5
- export interface HighlighterStyleSheet {
6
- [key: string]: TextStyle
7
- }
8
- export type ReactStyle = Record<string, CSSProperties>
9
-
10
- const ALLOWED_STYLE_PROPERTIES: Record<string, boolean> = {
11
- color: true,
12
- background: true,
13
- backgroundColor: true,
14
- fontWeight: true,
15
- fontStyle: true,
4
+ export interface RNTokenStyle {
5
+ color?: string
6
+ backgroundColor?: string
7
+ fontWeight?: TextStyle['fontWeight']
8
+ fontStyle?: TextStyle['fontStyle']
16
9
  }
17
10
 
18
- export function getRNStylesFromShikiStyle(shikiStyle: ReactStyle): HighlighterStyleSheet {
19
- return Object.fromEntries(Object.entries(shikiStyle).map(([className, style]) => [className, cleanStyle(style)]))
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
+ }
20
31
  }
21
32
 
22
- export function cleanStyle(style: CSSProperties) {
23
- const styles = Object.entries(style)
24
- .filter(([key]) => ALLOWED_STYLE_PROPERTIES[key])
25
- .map<StyleTuple>(([key, value]) => [key, value])
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'
41
+
42
+ // Convert token colors
43
+ const styles: Record<string, RNTokenStyle> = {}
44
+
45
+ theme.settings?.forEach((setting) => {
46
+ if (!setting.scope || !setting.settings)
47
+ return
48
+
49
+ const scopes = Array.isArray(setting.scope) ? setting.scope : [setting.scope]
50
+ const style: RNTokenStyle = {}
51
+
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
57
+ }
58
+ })
59
+
60
+ // Apply style to each scope
61
+ scopes.forEach((scope) => {
62
+ styles[scope] = style
63
+ })
64
+ })
26
65
 
27
- return transform(styles)
66
+ return {
67
+ backgroundColor,
68
+ defaultColor,
69
+ styles,
70
+ }
28
71
  }