rn-shiki 0.0.37-2 → 0.0.37-20

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-20",
5
5
  "description": "Shiki syntax highlighter for React Native.",
6
6
  "author": "Ryan Skinner <hello@ryanskinner.com>",
7
7
  "license": "MIT",
@@ -34,21 +34,17 @@
34
34
  "shiki": "^1.1.7"
35
35
  },
36
36
  "dependencies": {
37
- "esbuild": "^0.24.0",
37
+ "css-to-react-native": "^3.2.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",
44
42
  "@eslint-react/eslint-plugin": "^1.15.0",
45
43
  "@types/react": "^18.2.0",
46
44
  "@types/react-native": "^0.73.0",
47
- "babel-plugin-module-resolver": "^5.0.2",
48
45
  "eslint": "^9.13.0",
49
46
  "eslint-plugin-react-hooks": "^5.0.0",
50
47
  "eslint-plugin-react-refresh": "^0.4.13",
51
- "metro-react-native-babel-preset": "^0.77.0",
52
48
  "react": "^18.2.0",
53
49
  "react-native": "^0.73.0",
54
50
  "react-native-builder-bob": "^0.30.2",
@@ -1,9 +1,9 @@
1
1
  import type { SyntaxHighlighterProps } from 'src/types/shiki'
2
- import React from 'react'
2
+ import type { ReactStyle } from '../../utils/style-transformer'
3
+ import React, { useMemo } from 'react'
3
4
  import { Platform, ScrollView, StyleSheet, Text, View } from 'react-native'
4
5
  import { useSyntaxHighlighter } from '../../hooks/useSyntaxHighlighter'
5
- import { convertShikiTheme } from '../../utils/style-transformer'
6
- import SyntaxLine from './SyntaxLine'
6
+ import { getRNStylesFromShikiStyle } from '../../utils/style-transformer'
7
7
 
8
8
  const monospaceFont = Platform.select({
9
9
  ios: 'Menlo',
@@ -12,81 +12,68 @@ const monospaceFont = Platform.select({
12
12
  })
13
13
 
14
14
  const styles = StyleSheet.create({
15
- container: {
15
+ scrollView: {
16
16
  flex: 1,
17
17
  minHeight: 20,
18
18
  },
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',
19
+ lineContainer: {
20
+ flexDirection: 'row',
21
+ flexWrap: 'wrap',
33
22
  },
34
23
  })
35
24
 
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({
25
+ const SyntaxHighlighter: React.FC<SyntaxHighlighterProps> = ({ text, language, languageId, theme }) => {
26
+ console.log('theme:', theme)
27
+ const stylesheet = useMemo(() => {
28
+ const styles = getRNStylesFromShikiStyle(theme as ReactStyle)
29
+ console.log('Generated stylesheet:', styles)
30
+ return styles
31
+ }, [theme])
32
+
33
+ const { tokens } = useSyntaxHighlighter({
40
34
  text,
41
35
  language,
42
36
  languageId,
43
37
  theme,
44
38
  })
45
39
 
46
- if (error) {
40
+ const renderToken = (token: any, index: number, lineIndex: number) => {
41
+ // Ensure to collect all styles associated with the token's scopes
42
+ const tokenScopes = Array.isArray(token.scope) ? token.scope : [token.scope]
43
+ const tokenStylesArray = tokenScopes.map((scope: string) => stylesheet[scope] || {})
44
+ const combinedStyles = StyleSheet.flatten([
45
+ ...tokenStylesArray,
46
+ {
47
+ fontFamily: monospaceFont,
48
+ ...(token.color && { color: token.color }),
49
+ ...(token.fontStyle === 'italic' && stylesheet.italic),
50
+ ...(token.fontWeight === 'bold' && stylesheet.bold),
51
+ },
52
+ ])
53
+
54
+ console.log(`Rendering token: ${token.content}, styles:`, combinedStyles)
55
+
47
56
  return (
48
- <Text
49
- style={[
50
- styles.errorText,
51
- {
52
- fontFamily: monospaceFont,
53
- fontSize,
54
- },
55
- ]}
56
- >
57
- Error:
58
- {' '}
59
- {error}
57
+ <Text key={`${lineIndex}-${index}`} style={combinedStyles}>
58
+ {token.content.replace(/ /g, '\u00A0')}
60
59
  </Text>
61
60
  )
62
61
  }
63
62
 
64
- if (isLoading) {
63
+ // In the renderLine and renderToken flow
64
+ const renderLine = (line: any[], lineIndex: number) => {
65
+ console.log(`Rendering line ${lineIndex}:`, line)
65
66
  return (
66
- <Text
67
- style={[
68
- styles.loadingText,
69
- {
70
- fontFamily: monospaceFont,
71
- fontSize,
72
- },
73
- ]}
74
- >
75
- Loading...
76
- </Text>
67
+ <View key={lineIndex} style={styles.lineContainer}>
68
+ {line.map((token, tokenIndex) => renderToken(token, tokenIndex, lineIndex))}
69
+ </View>
77
70
  )
78
71
  }
79
72
 
80
73
  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>
89
- </View>
74
+ <ScrollView horizontal showsHorizontalScrollIndicator={Platform.OS !== 'web'} style={styles.scrollView}>
75
+ <View onStartShouldSetResponder={() => true}>{tokens.map((line, index) => renderLine(line, index))}</View>
76
+ </ScrollView>
90
77
  )
91
78
  }
92
79
 
@@ -1,6 +1,7 @@
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
- import { createHighlighter } from '../syntax'
4
+ import { createHighlighter } from '../syntax/highlighter'
4
5
 
5
6
  export function useSyntaxHighlighter({ text, language, languageId, theme }: SyntaxHighlighterProps) {
6
7
  const [tokens, setTokens] = useState<ThemedToken[][]>([])
@@ -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) {
package/src/index.ts CHANGED
@@ -1,6 +1,3 @@
1
1
  import SyntaxHighlighter from './components/syntax/SyntaxHighlighter'
2
- import SyntaxLine from './components/syntax/SyntaxLine'
3
2
 
4
- export * from './types/shiki'
5
-
6
- export { SyntaxHighlighter, SyntaxLine }
3
+ export { SyntaxHighlighter }
@@ -1,19 +1,31 @@
1
1
  import type { LanguageInput, ThemedToken, ThemeInput } from 'shiki'
2
- import type { TokenType } from '../../types'
2
+ import type { TokenType } from '../types/shiki'
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
+ return await createHighlighterCore({
15
+ langs,
16
+ themes,
17
+ engine,
18
+ })
19
+ }
20
+ catch (err) {
21
+ console.error('Error initializing highlighter:', err)
22
+ throw err
23
+ }
12
24
  }
13
25
 
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.')
26
+ export async function createHighlighter({ langs, themes }: { langs: LanguageInput[], themes: ThemeInput[] }) {
27
+ if (!langs?.length) {
28
+ throw new Error('Please provide languages when creating a highlighter.')
17
29
  }
18
30
 
19
31
  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,107 @@
1
1
  import type { TextStyle } from 'react-native'
2
- import type { ThemeRegistrationAny } from 'shiki'
2
+ import transform from 'css-to-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
+ [key: string]: TextStyle
9
6
  }
10
7
 
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]
8
+ export type ReactStyle = Record<string, TextStyle>
9
+
10
+ // Helper function to convert hex color to RGB
11
+ function hexToRgb(hex: string): { r: number, g: number, b: number } | null {
12
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
13
+ return result
14
+ ? {
15
+ r: Number.parseInt(result[1]!, 16),
16
+ g: Number.parseInt(result[2]!, 16),
17
+ b: Number.parseInt(result[3]!, 16),
18
+ }
19
+ : null
20
+ }
21
+
22
+ // Helper function to normalize color values
23
+ function normalizeColor(color: string | undefined): string {
24
+ if (!color)
25
+ return 'transparent'
26
+
27
+ // Type guard to ensure color is string before we call string methods
28
+ if (typeof color === 'string' && color.startsWith('#')) {
29
+ const rgb = hexToRgb(color)
30
+ if (rgb) {
31
+ return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`
32
+ }
30
33
  }
34
+ return color
31
35
  }
32
36
 
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'
37
+ export function convertTokenColorsToReactStyle(tokenColors: any[]): ReactStyle {
38
+ const styleMap: ReactStyle = {}
41
39
 
42
- // Convert token colors
43
- const styles: Record<string, RNTokenStyle> = {}
40
+ // Add default styles
41
+ styleMap.default = {
42
+ color: '#e1e4e8',
43
+ backgroundColor: 'transparent',
44
+ }
44
45
 
45
- theme.settings?.forEach((setting) => {
46
- if (!setting.scope || !setting.settings)
46
+ tokenColors.forEach((token) => {
47
+ if (!token.settings || !token.scope)
47
48
  return
48
49
 
49
- const scopes = Array.isArray(setting.scope) ? setting.scope : [setting.scope]
50
- const style: RNTokenStyle = {}
50
+ const scopes = Array.isArray(token.scope) ? token.scope : [token.scope]
51
+ const styleEntries: Array<[string, string]> = []
51
52
 
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
53
+ // Only add styles if they exist
54
+ if (token.settings.foreground) {
55
+ styleEntries.push(['color', normalizeColor(token.settings.foreground)])
56
+ }
57
+ if (token.settings.background) {
58
+ styleEntries.push(['backgroundColor', normalizeColor(token.settings.background)])
59
+ }
60
+ if (token.settings.fontStyle) {
61
+ if (token.settings.fontStyle.includes('italic')) {
62
+ styleEntries.push(['fontStyle', 'italic'])
57
63
  }
58
- })
64
+ if (token.settings.fontStyle.includes('bold')) {
65
+ styleEntries.push(['fontWeight', 'bold'])
66
+ }
67
+ }
68
+
69
+ if (styleEntries.length > 0) {
70
+ const transformedStyle = transform(styleEntries)
59
71
 
60
- // Apply style to each scope
61
- scopes.forEach((scope) => {
62
- styles[scope] = style
63
- })
72
+ scopes.forEach((scope: string) => {
73
+ if (typeof scope === 'string') {
74
+ styleMap[scope] = { ...(styleMap[scope] || {}), ...transformedStyle }
75
+ }
76
+ })
77
+ }
64
78
  })
65
79
 
66
- return {
67
- backgroundColor,
68
- defaultColor,
69
- styles,
80
+ return styleMap
81
+ }
82
+
83
+ export function getRNStylesFromShikiStyle(theme: any): HighlighterStyleSheet {
84
+ if (!theme || !theme.tokenColors) {
85
+ console.error('Provided theme does not contain \'tokenColors\'.')
86
+ return {}
87
+ }
88
+
89
+ try {
90
+ // Convert theme colors to React Native styles
91
+ const styles = convertTokenColorsToReactStyle(theme.tokenColors)
92
+
93
+ // Add theme-specific base styles if they exist
94
+ if (theme.colors) {
95
+ styles.base = {
96
+ color: normalizeColor(theme.colors['editor.foreground']),
97
+ backgroundColor: normalizeColor(theme.colors['editor.background']),
98
+ }
99
+ }
100
+
101
+ return styles
102
+ }
103
+ catch {
104
+ console.error('Error converting theme styles')
105
+ return {}
70
106
  }
71
107
  }
@@ -1,81 +0,0 @@
1
- import type { TokenType } from '../../types'
2
- import type { RNTokenStyle } from '../../utils/style-transformer'
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
@@ -1,2 +0,0 @@
1
- export { default as SyntaxHighlighter } from './SyntaxHighlighter'
2
- export { default as SyntaxLine } from './SyntaxLine'
@@ -1 +0,0 @@
1
- export { createHighlighter, processTokens } from './highlighter'
@@ -1,6 +0,0 @@
1
- export interface TokenType {
2
- content: string
3
- color?: string
4
- fontStyle?: string
5
- fontWeight?: string
6
- }
@@ -1 +0,0 @@
1
- export * from './string'