rn-shiki 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. package/LICENCE +21 -0
  2. package/babel.cjs +150 -0
  3. package/babel.config.cjs +14 -0
  4. package/lib/commonjs/components/syntax/SyntaxHighlighter.js +73 -0
  5. package/lib/commonjs/components/syntax/SyntaxHighlighter.js.map +1 -0
  6. package/lib/commonjs/components/syntax/SyntaxLine.js +36 -0
  7. package/lib/commonjs/components/syntax/SyntaxLine.js.map +1 -0
  8. package/lib/commonjs/components/syntax/index.js +21 -0
  9. package/lib/commonjs/components/syntax/index.js.map +1 -0
  10. package/lib/commonjs/hooks/useSyntaxHighlighter.js +62 -0
  11. package/lib/commonjs/hooks/useSyntaxHighlighter.js.map +1 -0
  12. package/lib/commonjs/index.js +26 -0
  13. package/lib/commonjs/index.js.map +1 -0
  14. package/lib/commonjs/package.json +1 -0
  15. package/lib/commonjs/syntax/highlighter/index.js +128 -0
  16. package/lib/commonjs/syntax/highlighter/index.js.map +1 -0
  17. package/lib/commonjs/syntax/index.js +26 -0
  18. package/lib/commonjs/syntax/index.js.map +1 -0
  19. package/lib/commonjs/syntax/parser/index.js +26 -0
  20. package/lib/commonjs/syntax/parser/index.js.map +1 -0
  21. package/lib/commonjs/types/index.js +2 -0
  22. package/lib/commonjs/types/index.js.map +1 -0
  23. package/lib/commonjs/utils/index.js +17 -0
  24. package/lib/commonjs/utils/index.js.map +1 -0
  25. package/lib/commonjs/utils/string.js +28 -0
  26. package/lib/commonjs/utils/string.js.map +1 -0
  27. package/lib/module/components/syntax/SyntaxHighlighter.js +68 -0
  28. package/lib/module/components/syntax/SyntaxHighlighter.js.map +1 -0
  29. package/lib/module/components/syntax/SyntaxLine.js +31 -0
  30. package/lib/module/components/syntax/SyntaxLine.js.map +1 -0
  31. package/lib/module/components/syntax/index.js +5 -0
  32. package/lib/module/components/syntax/index.js.map +1 -0
  33. package/lib/module/hooks/useSyntaxHighlighter.js +58 -0
  34. package/lib/module/hooks/useSyntaxHighlighter.js.map +1 -0
  35. package/lib/module/index.js +5 -0
  36. package/lib/module/index.js.map +1 -0
  37. package/lib/module/package.json +1 -0
  38. package/lib/module/syntax/highlighter/index.js +121 -0
  39. package/lib/module/syntax/highlighter/index.js.map +1 -0
  40. package/lib/module/syntax/index.js +5 -0
  41. package/lib/module/syntax/index.js.map +1 -0
  42. package/lib/module/syntax/parser/index.js +22 -0
  43. package/lib/module/syntax/parser/index.js.map +1 -0
  44. package/lib/module/types/index.js +2 -0
  45. package/lib/module/types/index.js.map +1 -0
  46. package/lib/module/utils/index.js +4 -0
  47. package/lib/module/utils/index.js.map +1 -0
  48. package/lib/module/utils/string.js +22 -0
  49. package/lib/module/utils/string.js.map +1 -0
  50. package/lib/typescript/components/syntax/SyntaxHighlighter.d.ts +11 -0
  51. package/lib/typescript/components/syntax/SyntaxHighlighter.d.ts.map +1 -0
  52. package/lib/typescript/components/syntax/SyntaxLine.d.ts +9 -0
  53. package/lib/typescript/components/syntax/SyntaxLine.d.ts.map +1 -0
  54. package/lib/typescript/components/syntax/index.d.ts +3 -0
  55. package/lib/typescript/components/syntax/index.d.ts.map +1 -0
  56. package/lib/typescript/hooks/useSyntaxHighlighter.d.ts +11 -0
  57. package/lib/typescript/hooks/useSyntaxHighlighter.d.ts.map +1 -0
  58. package/lib/typescript/index.d.ts +4 -0
  59. package/lib/typescript/index.d.ts.map +1 -0
  60. package/lib/typescript/syntax/highlighter/index.d.ts +14 -0
  61. package/lib/typescript/syntax/highlighter/index.d.ts.map +1 -0
  62. package/lib/typescript/syntax/index.d.ts +4 -0
  63. package/lib/typescript/syntax/index.d.ts.map +1 -0
  64. package/lib/typescript/syntax/parser/index.d.ts +10 -0
  65. package/lib/typescript/syntax/parser/index.d.ts.map +1 -0
  66. package/lib/typescript/types/index.d.ts +7 -0
  67. package/lib/typescript/types/index.d.ts.map +1 -0
  68. package/lib/typescript/utils/index.d.ts +2 -0
  69. package/lib/typescript/utils/index.d.ts.map +1 -0
  70. package/lib/typescript/utils/string.d.ts +5 -0
  71. package/lib/typescript/utils/string.d.ts.map +1 -0
  72. package/package.json +78 -0
  73. package/src/components/syntax/SyntaxHighlighter.tsx +57 -0
  74. package/src/components/syntax/SyntaxLine.tsx +34 -0
  75. package/src/components/syntax/index.ts +2 -0
  76. package/src/hooks/useSyntaxHighlighter.ts +65 -0
  77. package/src/index.ts +3 -0
  78. package/src/syntax/highlighter/index.ts +139 -0
  79. package/src/syntax/index.ts +3 -0
  80. package/src/syntax/parser/index.ts +32 -0
  81. package/src/types/index.ts +6 -0
  82. package/src/utils/index.ts +1 -0
  83. package/src/utils/string.ts +37 -0
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "rn-shiki",
3
+ "type": "module",
4
+ "version": "0.0.1",
5
+ "description": "Shiki syntax highlighter for React Native.",
6
+ "author": "Ryan Skinner <hello@ryanskinner.com>",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "react-native",
10
+ "shiki",
11
+ "syntax-highlighting",
12
+ "ios",
13
+ "android"
14
+ ],
15
+ "exports": {
16
+ ".": {
17
+ "types": "./lib/typescript/index.d.ts",
18
+ "default": "./lib/commonjs/index.js"
19
+ },
20
+ "./babel": "./babel.cjs"
21
+ },
22
+ "main": "lib/commonjs/index",
23
+ "module": "lib/module/index",
24
+ "types": "lib/typescript/index.d.ts",
25
+ "react-native": "src/index",
26
+ "source": "src/index",
27
+ "files": [
28
+ "babel.cjs",
29
+ "babel.config.cjs",
30
+ "lib",
31
+ "src"
32
+ ],
33
+ "scripts": {
34
+ "build": "bob build",
35
+ "lint": "eslint .",
36
+ "lint:fix": "eslint . --fix",
37
+ "typescript": "tsc --noEmit"
38
+ },
39
+ "peerDependencies": {
40
+ "react": ">=18.0.0",
41
+ "react-native": ">=0.70.0",
42
+ "shiki": "^1.1.7"
43
+ },
44
+ "dependencies": {
45
+ "shiki": "^1.1.7"
46
+ },
47
+ "devDependencies": {
48
+ "@antfu/eslint-config": "^3.8.0",
49
+ "@babel/core": "^7.24.0",
50
+ "@babel/plugin-transform-modules-commonjs": "^7.25.9",
51
+ "@babel/preset-env": "^7.26.0",
52
+ "@eslint-react/eslint-plugin": "^1.15.0",
53
+ "@types/react": "^18.2.0",
54
+ "@types/react-native": "^0.73.0",
55
+ "eslint": "^9.13.0",
56
+ "eslint-plugin-react-hooks": "^5.0.0",
57
+ "eslint-plugin-react-refresh": "^0.4.13",
58
+ "metro-react-native-babel-preset": "^0.77.0",
59
+ "react": "^18.2.0",
60
+ "react-native": "^0.73.0",
61
+ "react-native-builder-bob": "^0.30.2",
62
+ "typescript": "^5.0.0"
63
+ },
64
+ "react-native-builder-bob": {
65
+ "source": "src",
66
+ "output": "lib",
67
+ "targets": [
68
+ "commonjs",
69
+ "module",
70
+ [
71
+ "typescript",
72
+ {
73
+ "project": "tsconfig.json"
74
+ }
75
+ ]
76
+ ]
77
+ }
78
+ }
@@ -0,0 +1,57 @@
1
+ import type { SupportedLanguage, SupportedTheme } from '../../syntax/highlighter'
2
+ import React from 'react'
3
+ import { Text, View } from 'react-native'
4
+ import { useSyntaxHighlighter } from '../../hooks/useSyntaxHighlighter'
5
+
6
+ interface SyntaxHighlighterProps {
7
+ text: string
8
+ lang: SupportedLanguage
9
+ theme?: SupportedTheme
10
+ fontSize?: number
11
+ }
12
+
13
+ function SyntaxHighlighter({ text, lang, theme = 'github-dark', fontSize }: SyntaxHighlighterProps) {
14
+ const { tokens, error, isLoading } = useSyntaxHighlighter({
15
+ text,
16
+ lang,
17
+ theme,
18
+ })
19
+
20
+ if (error) {
21
+ return (
22
+ <Text style={{ color: '#ff6b6b', fontFamily: 'monospace', fontSize }}>
23
+ Error:
24
+ {error}
25
+ </Text>
26
+ )
27
+ }
28
+
29
+ if (isLoading) {
30
+ return <Text style={{ color: '#f8f8f2', fontFamily: 'monospace', fontSize }}>Loading...</Text>
31
+ }
32
+
33
+ return (
34
+ <View>
35
+ {tokens.map((line, lineIndex) => (
36
+ <View key={`line-${lineIndex}`} style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
37
+ {line.map((token, tokenIndex) => (
38
+ <Text
39
+ key={`${lineIndex}-${tokenIndex}`}
40
+ style={{
41
+ color: token.color || '#FFFFFF',
42
+ fontFamily: 'monospace',
43
+ fontSize,
44
+ fontStyle: token.fontStyle as unknown as 'normal' | 'italic',
45
+ }}
46
+ >
47
+ {token.content.replace(/ /g, '\u00A0')}
48
+ </Text>
49
+ ))}
50
+ <Text style={{ color: '#FFFFFF', fontFamily: 'monospace', fontSize }}>{'\n'}</Text>
51
+ </View>
52
+ ))}
53
+ </View>
54
+ )
55
+ }
56
+
57
+ export default SyntaxHighlighter
@@ -0,0 +1,34 @@
1
+ import type { TokenType } from '../../types'
2
+ import React from 'react'
3
+ import { Text, View } from 'react-native'
4
+
5
+ interface SyntaxLineProps {
6
+ line: TokenType[]
7
+ fontSize?: number
8
+ }
9
+
10
+ function SyntaxLine({ line, fontSize }: SyntaxLineProps) {
11
+ return (
12
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
13
+ {line.map((token, tokenIndex) => {
14
+ // Preserve whitespace
15
+ const content = token.content.replace(/ /g, '\u00A0')
16
+ return (
17
+ <Text
18
+ key={`${content}-${tokenIndex}`}
19
+ style={{
20
+ color: token.color,
21
+ fontFamily: 'monospace',
22
+ fontSize,
23
+ fontStyle: token.fontStyle as 'normal' | 'italic',
24
+ }}
25
+ >
26
+ {content}
27
+ </Text>
28
+ )
29
+ })}
30
+ </View>
31
+ )
32
+ }
33
+
34
+ export default SyntaxLine
@@ -0,0 +1,2 @@
1
+ export { default as SyntaxHighlighter } from './SyntaxHighlighter'
2
+ export { default as SyntaxLine } from './SyntaxLine'
@@ -0,0 +1,65 @@
1
+ import type { BundledLanguage, BundledTheme, ThemedToken } from 'shiki'
2
+ import type { SupportedLanguage, SupportedTheme } from '../syntax/highlighter'
3
+ import { useEffect, useState } from 'react'
4
+ import { createHighlighter } from '../syntax'
5
+
6
+ interface ShikiResult {
7
+ tokens: ThemedToken[][]
8
+ bg: string
9
+ fg: string
10
+ }
11
+
12
+ export function useSyntaxHighlighter({ text, lang, theme }: { text: string, lang: BundledLanguage, theme: BundledTheme }) {
13
+ const [tokens, setTokens] = useState<ThemedToken[][]>([])
14
+ const [error, setError] = useState<string>()
15
+ const [isLoading, setIsLoading] = useState(true)
16
+
17
+ useEffect(() => {
18
+ let mounted = true
19
+ setIsLoading(true)
20
+
21
+ async function highlight() {
22
+ if (!text) {
23
+ setIsLoading(false)
24
+ setTokens([])
25
+ return
26
+ }
27
+
28
+ try {
29
+ const highlighter = await createHighlighter({
30
+ langs: [lang as SupportedLanguage],
31
+ themes: [theme as SupportedTheme],
32
+ })
33
+
34
+ const result = highlighter.codeToTokens(text, {
35
+ lang,
36
+ theme,
37
+ }) as ShikiResult
38
+
39
+ if (mounted && result.tokens) {
40
+ setTokens(result.tokens)
41
+ setError(undefined)
42
+ }
43
+ }
44
+ catch (err) {
45
+ console.error('Highlighting error:', err)
46
+ if (mounted) {
47
+ setError(err instanceof Error ? err.message : 'Syntax highlighting failed')
48
+ setTokens([])
49
+ }
50
+ }
51
+ finally {
52
+ if (mounted) {
53
+ setIsLoading(false)
54
+ }
55
+ }
56
+ }
57
+
58
+ highlight()
59
+ return () => {
60
+ mounted = false
61
+ }
62
+ }, [text, lang, theme])
63
+
64
+ return { tokens, error, isLoading }
65
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { SyntaxHighlighter, SyntaxLine } from './components/syntax'
2
+ export { parseCodeFence } from './syntax'
3
+ export type { ParsedCodeFence } from './syntax'
@@ -0,0 +1,139 @@
1
+ import type { LanguageInput, ThemedToken, ThemeInput } from 'shiki'
2
+ import type { TokenType } from '../../types'
3
+ import { createHighlighterCore } from 'shiki/core'
4
+ import { createJavaScriptRegexEngine } from 'shiki/engine-javascript.mjs'
5
+
6
+ // Import languages
7
+ import css from 'shiki/langs/css.mjs'
8
+ import html from 'shiki/langs/html.mjs'
9
+ import javascript from 'shiki/langs/javascript.mjs'
10
+ import json from 'shiki/langs/json.mjs'
11
+ import php from 'shiki/langs/php.mjs'
12
+ import typescript from 'shiki/langs/typescript.mjs'
13
+
14
+ // Import themes
15
+ import githubDark from 'shiki/themes/github-dark.mjs'
16
+ import githubLight from 'shiki/themes/github-light.mjs'
17
+
18
+ // Define supported languages and themes explicitly
19
+ export type SupportedLanguage = 'css' | 'html' | 'javascript' | 'json' | 'php' | 'typescript'
20
+ export type SupportedTheme = 'github-dark' | 'github-light'
21
+
22
+ // Bundled languages mapping - using the imported grammars directly
23
+ const LANGUAGE_IMPORTS: Record<SupportedLanguage, LanguageInput> = {
24
+ css,
25
+ html,
26
+ javascript,
27
+ json,
28
+ php,
29
+ typescript,
30
+ }
31
+
32
+ // Bundled themes mapping
33
+ const THEME_IMPORTS: Record<SupportedTheme, ThemeInput> = {
34
+ 'github-dark': githubDark,
35
+ 'github-light': githubLight,
36
+ }
37
+
38
+ // Singleton highlighter instance with tracking
39
+ interface HighlighterInstance {
40
+ highlighter: Awaited<ReturnType<typeof createHighlighterCore>>
41
+ loadedLanguages: Set<string>
42
+ loadedThemes: Set<string>
43
+ }
44
+
45
+ let highlighterInstance: HighlighterInstance | null = null
46
+
47
+ interface HighlighterOptions {
48
+ langs: SupportedLanguage[]
49
+ themes: SupportedTheme[]
50
+ }
51
+
52
+ export async function createHighlighter({ langs, themes }: HighlighterOptions) {
53
+ if (!langs || !themes) {
54
+ throw new Error('Please provide both `langs` and `themes` when creating a highlighter.')
55
+ }
56
+
57
+ // Return existing instance if already initialized
58
+ if (highlighterInstance?.highlighter) {
59
+ // Load any new languages that weren't previously loaded
60
+ for (const lang of langs) {
61
+ if (!highlighterInstance.loadedLanguages.has(lang)) {
62
+ const langData = LANGUAGE_IMPORTS[lang]
63
+ await highlighterInstance.highlighter.loadLanguage(langData)
64
+ highlighterInstance.loadedLanguages.add(lang)
65
+ }
66
+ }
67
+
68
+ // Load any new themes that weren't previously loaded
69
+ for (const theme of themes) {
70
+ if (!highlighterInstance.loadedThemes.has(theme)) {
71
+ const themeData = THEME_IMPORTS[theme]
72
+ await highlighterInstance.highlighter.loadTheme(themeData)
73
+ highlighterInstance.loadedThemes.add(theme)
74
+ }
75
+ }
76
+
77
+ return highlighterInstance.highlighter
78
+ }
79
+
80
+ try {
81
+ // Create highlighter with initial languages and themes
82
+ const highlighter = await createHighlighterCore({
83
+ engine: createJavaScriptRegexEngine(),
84
+ langs: langs.map(lang => LANGUAGE_IMPORTS[lang]),
85
+ themes: themes.map(theme => THEME_IMPORTS[theme]),
86
+ })
87
+
88
+ // Store the instance with tracking sets
89
+ highlighterInstance = {
90
+ highlighter,
91
+ loadedLanguages: new Set(langs),
92
+ loadedThemes: new Set(themes),
93
+ }
94
+
95
+ return highlighter
96
+ }
97
+ catch (error) {
98
+ console.error('Failed to create highlighter:', error)
99
+ throw error
100
+ }
101
+ }
102
+
103
+ // Function to ensure a theme is loaded
104
+ export async function loadTheme(theme: SupportedTheme) {
105
+ if (!highlighterInstance?.highlighter) {
106
+ throw new Error('Highlighter not initialized')
107
+ }
108
+
109
+ if (highlighterInstance.loadedThemes.has(theme)) {
110
+ return
111
+ }
112
+
113
+ const themeData = THEME_IMPORTS[theme]
114
+ await highlighterInstance.highlighter.loadTheme(themeData)
115
+ highlighterInstance.loadedThemes.add(theme)
116
+ }
117
+
118
+ // Function to ensure a language is loaded
119
+ export async function loadLanguage(lang: SupportedLanguage) {
120
+ if (!highlighterInstance?.highlighter) {
121
+ throw new Error('Highlighter not initialized')
122
+ }
123
+
124
+ if (highlighterInstance.loadedLanguages.has(lang)) {
125
+ return
126
+ }
127
+
128
+ const langData = LANGUAGE_IMPORTS[lang]
129
+ await highlighterInstance.highlighter.loadLanguage(langData)
130
+ highlighterInstance.loadedLanguages.add(lang)
131
+ }
132
+
133
+ export function processTokens(tokens: ThemedToken[]): TokenType[] {
134
+ return tokens.map(token => ({
135
+ content: token.content,
136
+ color: token.color || '#000000',
137
+ fontStyle: token.fontStyle?.toString(),
138
+ }))
139
+ }
@@ -0,0 +1,3 @@
1
+ export { createHighlighter, processTokens } from './highlighter'
2
+ export { parseCodeFence } from './parser'
3
+ export type { ParsedCodeFence } from './parser'
@@ -0,0 +1,32 @@
1
+ import type { BundledLanguage } from 'shiki/langs'
2
+ import type { SupportedLanguage } from '../../syntax/highlighter'
3
+ import { removeExtraIndentation } from '../../utils'
4
+
5
+ export interface ParsedCodeFence {
6
+ code: string
7
+ language: BundledLanguage
8
+ valid: boolean
9
+ outsideText: string[]
10
+ }
11
+
12
+ const CODE_BLOCK_REGEX = /```([a-z0-9]+)?\n([\s\S]*?)\n```/gi
13
+
14
+ export function parseCodeFence(text: string, defaultLang?: SupportedLanguage): ParsedCodeFence {
15
+ const matches = [...text.matchAll(CODE_BLOCK_REGEX)]
16
+ if (matches.length === 0) {
17
+ return {
18
+ code: text,
19
+ } as ParsedCodeFence
20
+ }
21
+
22
+ const code = matches[0]?.[2] || ''
23
+ const language = (matches[0]?.[1] as unknown as SupportedLanguage) || defaultLang
24
+ const outsideText = text.split(CODE_BLOCK_REGEX).filter(Boolean)
25
+
26
+ return {
27
+ code: removeExtraIndentation(code),
28
+ language,
29
+ valid: true,
30
+ outsideText,
31
+ }
32
+ }
@@ -0,0 +1,6 @@
1
+ export interface TokenType {
2
+ content: string
3
+ color: string
4
+ fontStyle?: string
5
+ fontWeight?: string
6
+ }
@@ -0,0 +1 @@
1
+ export * from './string'
@@ -0,0 +1,37 @@
1
+ import type { TokenType } from '../types'
2
+
3
+ export function createContentPreview(tokens: TokenType[], maxLength: number = 20): string {
4
+ return tokens
5
+ .slice(0, 3)
6
+ .map(token => token.content)
7
+ .join('')
8
+ .slice(0, maxLength)
9
+ }
10
+
11
+ export function createLineKey(line: TokenType[], index: number) {
12
+ const contentPreview = createContentPreview(line)
13
+ return `line-${index}-${contentPreview}`
14
+ }
15
+
16
+ export function removeExtraIndentation(text: string): string {
17
+ const preserved = text.replace(/\\n/g, '§NEWLINE§')
18
+ const lines = preserved.split('\n')
19
+ const minIndent = Math.min(
20
+ ...lines
21
+ .filter(line => line.trim().length > 0)
22
+ .map(line => line.match(/^\s*/)?.[0].length ?? 0),
23
+ )
24
+
25
+ const normalized = lines
26
+ .map((line) => {
27
+ if (line.trim().length === 0)
28
+ return ''
29
+
30
+ const indent = line.match(/^\s*/)?.[0] ?? ''
31
+ const relativeIndent = ' '.repeat(Math.max(0, indent.length - minIndent))
32
+ return relativeIndent + line.trim()
33
+ })
34
+ .join('\n')
35
+
36
+ return normalized.replace(/§NEWLINE§/g, '\\n')
37
+ }