rn-shiki 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- package/LICENCE +21 -0
- package/babel.cjs +150 -0
- package/babel.config.cjs +14 -0
- package/lib/commonjs/components/syntax/SyntaxHighlighter.js +73 -0
- package/lib/commonjs/components/syntax/SyntaxHighlighter.js.map +1 -0
- package/lib/commonjs/components/syntax/SyntaxLine.js +36 -0
- package/lib/commonjs/components/syntax/SyntaxLine.js.map +1 -0
- package/lib/commonjs/components/syntax/index.js +21 -0
- package/lib/commonjs/components/syntax/index.js.map +1 -0
- package/lib/commonjs/hooks/useSyntaxHighlighter.js +62 -0
- package/lib/commonjs/hooks/useSyntaxHighlighter.js.map +1 -0
- package/lib/commonjs/index.js +26 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/syntax/highlighter/index.js +128 -0
- package/lib/commonjs/syntax/highlighter/index.js.map +1 -0
- package/lib/commonjs/syntax/index.js +26 -0
- package/lib/commonjs/syntax/index.js.map +1 -0
- package/lib/commonjs/syntax/parser/index.js +26 -0
- package/lib/commonjs/syntax/parser/index.js.map +1 -0
- package/lib/commonjs/types/index.js +2 -0
- package/lib/commonjs/types/index.js.map +1 -0
- package/lib/commonjs/utils/index.js +17 -0
- package/lib/commonjs/utils/index.js.map +1 -0
- package/lib/commonjs/utils/string.js +28 -0
- package/lib/commonjs/utils/string.js.map +1 -0
- package/lib/module/components/syntax/SyntaxHighlighter.js +68 -0
- package/lib/module/components/syntax/SyntaxHighlighter.js.map +1 -0
- package/lib/module/components/syntax/SyntaxLine.js +31 -0
- package/lib/module/components/syntax/SyntaxLine.js.map +1 -0
- package/lib/module/components/syntax/index.js +5 -0
- package/lib/module/components/syntax/index.js.map +1 -0
- package/lib/module/hooks/useSyntaxHighlighter.js +58 -0
- package/lib/module/hooks/useSyntaxHighlighter.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/syntax/highlighter/index.js +121 -0
- package/lib/module/syntax/highlighter/index.js.map +1 -0
- package/lib/module/syntax/index.js +5 -0
- package/lib/module/syntax/index.js.map +1 -0
- package/lib/module/syntax/parser/index.js +22 -0
- package/lib/module/syntax/parser/index.js.map +1 -0
- package/lib/module/types/index.js +2 -0
- package/lib/module/types/index.js.map +1 -0
- package/lib/module/utils/index.js +4 -0
- package/lib/module/utils/index.js.map +1 -0
- package/lib/module/utils/string.js +22 -0
- package/lib/module/utils/string.js.map +1 -0
- package/lib/typescript/components/syntax/SyntaxHighlighter.d.ts +11 -0
- package/lib/typescript/components/syntax/SyntaxHighlighter.d.ts.map +1 -0
- package/lib/typescript/components/syntax/SyntaxLine.d.ts +9 -0
- package/lib/typescript/components/syntax/SyntaxLine.d.ts.map +1 -0
- package/lib/typescript/components/syntax/index.d.ts +3 -0
- package/lib/typescript/components/syntax/index.d.ts.map +1 -0
- package/lib/typescript/hooks/useSyntaxHighlighter.d.ts +11 -0
- package/lib/typescript/hooks/useSyntaxHighlighter.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +4 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/syntax/highlighter/index.d.ts +14 -0
- package/lib/typescript/syntax/highlighter/index.d.ts.map +1 -0
- package/lib/typescript/syntax/index.d.ts +4 -0
- package/lib/typescript/syntax/index.d.ts.map +1 -0
- package/lib/typescript/syntax/parser/index.d.ts +10 -0
- package/lib/typescript/syntax/parser/index.d.ts.map +1 -0
- package/lib/typescript/types/index.d.ts +7 -0
- package/lib/typescript/types/index.d.ts.map +1 -0
- package/lib/typescript/utils/index.d.ts +2 -0
- package/lib/typescript/utils/index.d.ts.map +1 -0
- package/lib/typescript/utils/string.d.ts +5 -0
- package/lib/typescript/utils/string.d.ts.map +1 -0
- package/package.json +78 -0
- package/src/components/syntax/SyntaxHighlighter.tsx +57 -0
- package/src/components/syntax/SyntaxLine.tsx +34 -0
- package/src/components/syntax/index.ts +2 -0
- package/src/hooks/useSyntaxHighlighter.ts +65 -0
- package/src/index.ts +3 -0
- package/src/syntax/highlighter/index.ts +139 -0
- package/src/syntax/index.ts +3 -0
- package/src/syntax/parser/index.ts +32 -0
- package/src/types/index.ts +6 -0
- package/src/utils/index.ts +1 -0
- 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,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,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,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 @@
|
|
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
|
+
}
|