@yivan-lab/pretty-please 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +380 -0
- package/bin/pls.js +681 -0
- package/bin/pls.tsx +541 -0
- package/dist/bin/pls.d.ts +2 -0
- package/dist/bin/pls.js +429 -0
- package/dist/src/ai.d.ts +48 -0
- package/dist/src/ai.js +295 -0
- package/dist/src/builtin-detector.d.ts +15 -0
- package/dist/src/builtin-detector.js +83 -0
- package/dist/src/chat-history.d.ts +26 -0
- package/dist/src/chat-history.js +81 -0
- package/dist/src/components/Chat.d.ts +13 -0
- package/dist/src/components/Chat.js +80 -0
- package/dist/src/components/ChatStatus.d.ts +9 -0
- package/dist/src/components/ChatStatus.js +34 -0
- package/dist/src/components/CodeColorizer.d.ts +12 -0
- package/dist/src/components/CodeColorizer.js +82 -0
- package/dist/src/components/CommandBox.d.ts +10 -0
- package/dist/src/components/CommandBox.js +45 -0
- package/dist/src/components/CommandGenerator.d.ts +20 -0
- package/dist/src/components/CommandGenerator.js +116 -0
- package/dist/src/components/ConfigDisplay.d.ts +9 -0
- package/dist/src/components/ConfigDisplay.js +42 -0
- package/dist/src/components/ConfigWizard.d.ts +9 -0
- package/dist/src/components/ConfigWizard.js +72 -0
- package/dist/src/components/ConfirmationPrompt.d.ts +12 -0
- package/dist/src/components/ConfirmationPrompt.js +26 -0
- package/dist/src/components/Duration.d.ts +9 -0
- package/dist/src/components/Duration.js +21 -0
- package/dist/src/components/HistoryDisplay.d.ts +9 -0
- package/dist/src/components/HistoryDisplay.js +51 -0
- package/dist/src/components/HookManager.d.ts +10 -0
- package/dist/src/components/HookManager.js +88 -0
- package/dist/src/components/InlineRenderer.d.ts +12 -0
- package/dist/src/components/InlineRenderer.js +75 -0
- package/dist/src/components/MarkdownDisplay.d.ts +13 -0
- package/dist/src/components/MarkdownDisplay.js +197 -0
- package/dist/src/components/MultiStepCommandGenerator.d.ts +25 -0
- package/dist/src/components/MultiStepCommandGenerator.js +142 -0
- package/dist/src/components/TableRenderer.d.ts +12 -0
- package/dist/src/components/TableRenderer.js +66 -0
- package/dist/src/config.d.ts +29 -0
- package/dist/src/config.js +203 -0
- package/dist/src/history.d.ts +20 -0
- package/dist/src/history.js +113 -0
- package/dist/src/mastra-agent.d.ts +7 -0
- package/dist/src/mastra-agent.js +31 -0
- package/dist/src/multi-step.d.ts +41 -0
- package/dist/src/multi-step.js +67 -0
- package/dist/src/shell-hook.d.ts +35 -0
- package/dist/src/shell-hook.js +348 -0
- package/dist/src/sysinfo.d.ts +15 -0
- package/dist/src/sysinfo.js +52 -0
- package/dist/src/ui/theme.d.ts +26 -0
- package/dist/src/ui/theme.js +31 -0
- package/dist/src/utils/console.d.ts +44 -0
- package/dist/src/utils/console.js +114 -0
- package/package.json +78 -0
- package/src/ai.js +324 -0
- package/src/builtin-detector.js +98 -0
- package/src/chat-history.js +94 -0
- package/src/components/Chat.tsx +122 -0
- package/src/components/ChatStatus.tsx +53 -0
- package/src/components/CodeColorizer.tsx +128 -0
- package/src/components/CommandBox.tsx +60 -0
- package/src/components/CommandGenerator.tsx +184 -0
- package/src/components/ConfigDisplay.tsx +64 -0
- package/src/components/ConfigWizard.tsx +101 -0
- package/src/components/ConfirmationPrompt.tsx +41 -0
- package/src/components/Duration.tsx +24 -0
- package/src/components/HistoryDisplay.tsx +69 -0
- package/src/components/HookManager.tsx +150 -0
- package/src/components/InlineRenderer.tsx +123 -0
- package/src/components/MarkdownDisplay.tsx +288 -0
- package/src/components/MultiStepCommandGenerator.tsx +229 -0
- package/src/components/TableRenderer.tsx +110 -0
- package/src/config.js +221 -0
- package/src/history.js +131 -0
- package/src/mastra-agent.ts +35 -0
- package/src/multi-step.ts +93 -0
- package/src/shell-hook.js +393 -0
- package/src/sysinfo.js +57 -0
- package/src/ui/theme.ts +37 -0
- package/src/utils/console.js +130 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import TextInput from 'ink-text-input'
|
|
4
|
+
import { getConfig, saveConfig, maskApiKey } from '../config.js'
|
|
5
|
+
import { theme } from '../ui/theme.js'
|
|
6
|
+
import path from 'path'
|
|
7
|
+
import os from 'os'
|
|
8
|
+
|
|
9
|
+
const CONFIG_FILE = path.join(os.homedir(), '.please', 'config.json')
|
|
10
|
+
|
|
11
|
+
interface ConfigWizardProps {
|
|
12
|
+
onComplete: () => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type Step = 'apiKey' | 'baseUrl' | 'model' | 'done'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* ConfigWizard 组件 - 交互式配置向导
|
|
19
|
+
*/
|
|
20
|
+
export const ConfigWizard: React.FC<ConfigWizardProps> = ({ onComplete }) => {
|
|
21
|
+
const config = getConfig()
|
|
22
|
+
const [step, setStep] = useState<Step>('apiKey')
|
|
23
|
+
const [apiKey, setApiKey] = useState(config.apiKey)
|
|
24
|
+
const [baseUrl, setBaseUrl] = useState(config.baseUrl)
|
|
25
|
+
const [model, setModel] = useState(config.model)
|
|
26
|
+
|
|
27
|
+
const handleApiKeySubmit = (value: string) => {
|
|
28
|
+
if (value.trim()) {
|
|
29
|
+
setApiKey(value.trim())
|
|
30
|
+
}
|
|
31
|
+
setStep('baseUrl')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const handleBaseUrlSubmit = (value: string) => {
|
|
35
|
+
if (value.trim()) {
|
|
36
|
+
setBaseUrl(value.trim())
|
|
37
|
+
}
|
|
38
|
+
setStep('model')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const handleModelSubmit = (value: string) => {
|
|
42
|
+
if (value.trim()) {
|
|
43
|
+
setModel(value.trim())
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 保存配置
|
|
47
|
+
saveConfig({
|
|
48
|
+
...config,
|
|
49
|
+
apiKey: apiKey || config.apiKey,
|
|
50
|
+
baseUrl: baseUrl || config.baseUrl,
|
|
51
|
+
model: model.trim() || config.model,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
setStep('done')
|
|
55
|
+
setTimeout(onComplete, 100)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Box flexDirection="column" marginY={1}>
|
|
60
|
+
<Text bold color={theme.accent}>
|
|
61
|
+
🔧 Pretty Please 配置向导
|
|
62
|
+
</Text>
|
|
63
|
+
<Text color={theme.text.secondary}>{'━'.repeat(40)}</Text>
|
|
64
|
+
|
|
65
|
+
{step === 'apiKey' && (
|
|
66
|
+
<Box marginTop={1}>
|
|
67
|
+
<Text color={theme.primary}>
|
|
68
|
+
请输入 API Key{config.apiKey ? ` (当前: ${maskApiKey(config.apiKey)})` : ''}:{' '}
|
|
69
|
+
</Text>
|
|
70
|
+
<TextInput value="" onChange={() => {}} onSubmit={handleApiKeySubmit} />
|
|
71
|
+
</Box>
|
|
72
|
+
)}
|
|
73
|
+
|
|
74
|
+
{step === 'baseUrl' && (
|
|
75
|
+
<Box marginTop={1}>
|
|
76
|
+
<Text color={theme.primary}>
|
|
77
|
+
请输入 API Base URL (回车使用 {baseUrl}):{' '}
|
|
78
|
+
</Text>
|
|
79
|
+
<TextInput value="" onChange={() => {}} onSubmit={handleBaseUrlSubmit} />
|
|
80
|
+
</Box>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
{step === 'model' && (
|
|
84
|
+
<Box marginTop={1}>
|
|
85
|
+
<Text color={theme.primary}>
|
|
86
|
+
请输入模型名称 (回车使用 {model}):{' '}
|
|
87
|
+
</Text>
|
|
88
|
+
<TextInput value="" onChange={() => {}} onSubmit={handleModelSubmit} />
|
|
89
|
+
</Box>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
{step === 'done' && (
|
|
93
|
+
<Box flexDirection="column" marginTop={1}>
|
|
94
|
+
<Text color={theme.text.secondary}>{'━'.repeat(40)}</Text>
|
|
95
|
+
<Text color={theme.success}>✅ 配置已保存到 </Text>
|
|
96
|
+
<Text color={theme.text.secondary}>{CONFIG_FILE}</Text>
|
|
97
|
+
</Box>
|
|
98
|
+
)}
|
|
99
|
+
</Box>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Text, useInput } from 'ink'
|
|
3
|
+
import { theme } from '../ui/theme.js'
|
|
4
|
+
|
|
5
|
+
interface ConfirmationPromptProps {
|
|
6
|
+
prompt: string
|
|
7
|
+
onConfirm: () => void
|
|
8
|
+
onCancel: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ConfirmationPrompt 组件 - 单键确认提示
|
|
13
|
+
* 回车 = 确认,Esc = 取消,Ctrl+C = 退出
|
|
14
|
+
*/
|
|
15
|
+
export const ConfirmationPrompt: React.FC<ConfirmationPromptProps> = ({
|
|
16
|
+
prompt,
|
|
17
|
+
onConfirm,
|
|
18
|
+
onCancel,
|
|
19
|
+
}) => {
|
|
20
|
+
useInput((input, key) => {
|
|
21
|
+
if (key.return) {
|
|
22
|
+
// 回车键
|
|
23
|
+
onConfirm()
|
|
24
|
+
} else if (key.escape) {
|
|
25
|
+
// Esc 键
|
|
26
|
+
onCancel()
|
|
27
|
+
} else if (key.ctrl && input === 'c') {
|
|
28
|
+
// Ctrl+C
|
|
29
|
+
process.exit(0)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Text>
|
|
35
|
+
<Text bold color={theme.warning}>
|
|
36
|
+
{prompt}
|
|
37
|
+
</Text>
|
|
38
|
+
<Text color={theme.text.secondary}> [回车执行 / Esc 取消] </Text>
|
|
39
|
+
</Text>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Text } from 'ink'
|
|
3
|
+
import { theme } from '../ui/theme.js'
|
|
4
|
+
|
|
5
|
+
interface DurationProps {
|
|
6
|
+
ms: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 格式化耗时
|
|
11
|
+
*/
|
|
12
|
+
function formatDuration(ms: number): string {
|
|
13
|
+
if (ms < 1000) {
|
|
14
|
+
return `${ms}ms`
|
|
15
|
+
}
|
|
16
|
+
return `${(ms / 1000).toFixed(2)}s`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Duration 组件 - 显示耗时
|
|
21
|
+
*/
|
|
22
|
+
export const Duration: React.FC<DurationProps> = ({ ms }) => {
|
|
23
|
+
return <Text color={theme.text.secondary}>({formatDuration(ms)})</Text>
|
|
24
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import { getHistory, getHistoryFilePath } from '../history.js'
|
|
4
|
+
import { theme } from '../ui/theme.js'
|
|
5
|
+
|
|
6
|
+
interface HistoryDisplayProps {
|
|
7
|
+
onComplete?: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* HistoryDisplay 组件 - 显示历史记录
|
|
12
|
+
*/
|
|
13
|
+
export const HistoryDisplay: React.FC<HistoryDisplayProps> = ({ onComplete }) => {
|
|
14
|
+
const history = getHistory()
|
|
15
|
+
|
|
16
|
+
React.useEffect(() => {
|
|
17
|
+
if (onComplete) {
|
|
18
|
+
setTimeout(onComplete, 100)
|
|
19
|
+
}
|
|
20
|
+
}, [onComplete])
|
|
21
|
+
|
|
22
|
+
if (history.length === 0) {
|
|
23
|
+
return (
|
|
24
|
+
<Box marginY={1}>
|
|
25
|
+
<Text color={theme.text.secondary}>暂无历史记录</Text>
|
|
26
|
+
</Box>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Box flexDirection="column" marginY={1}>
|
|
32
|
+
<Text bold>📜 命令历史:</Text>
|
|
33
|
+
<Text color={theme.text.secondary}>{'━'.repeat(50)}</Text>
|
|
34
|
+
|
|
35
|
+
{history.map((item, index) => {
|
|
36
|
+
const status = item.executed
|
|
37
|
+
? item.exitCode === 0
|
|
38
|
+
? '✓'
|
|
39
|
+
: `✗ 退出码:${item.exitCode}`
|
|
40
|
+
: '(未执行)'
|
|
41
|
+
|
|
42
|
+
const statusColor = item.executed
|
|
43
|
+
? item.exitCode === 0
|
|
44
|
+
? theme.success
|
|
45
|
+
: theme.error
|
|
46
|
+
: theme.text.secondary
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Box key={index} flexDirection="column" marginY={1}>
|
|
50
|
+
<Box>
|
|
51
|
+
<Text color={theme.text.secondary}>{index + 1}. </Text>
|
|
52
|
+
<Text color={theme.primary}>{item.userPrompt}</Text>
|
|
53
|
+
</Box>
|
|
54
|
+
<Box marginLeft={3}>
|
|
55
|
+
<Text dimColor>→ </Text>
|
|
56
|
+
<Text>{item.command} </Text>
|
|
57
|
+
<Text color={statusColor}>{status}</Text>
|
|
58
|
+
</Box>
|
|
59
|
+
<Box marginLeft={3}>
|
|
60
|
+
<Text color={theme.text.secondary}>{item.timestamp}</Text>
|
|
61
|
+
</Box>
|
|
62
|
+
</Box>
|
|
63
|
+
)
|
|
64
|
+
})}
|
|
65
|
+
|
|
66
|
+
<Text color={theme.text.secondary}>历史文件: {getHistoryFilePath()}</Text>
|
|
67
|
+
</Box>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import {
|
|
4
|
+
getHookStatus,
|
|
5
|
+
installShellHook,
|
|
6
|
+
uninstallShellHook,
|
|
7
|
+
detectShell,
|
|
8
|
+
getShellConfigPath,
|
|
9
|
+
} from '../shell-hook.js'
|
|
10
|
+
import { theme } from '../ui/theme.js'
|
|
11
|
+
|
|
12
|
+
interface HookManagerProps {
|
|
13
|
+
action: 'status' | 'install' | 'uninstall'
|
|
14
|
+
onComplete: () => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* HookManager 组件 - Hook 管理界面
|
|
19
|
+
*/
|
|
20
|
+
export const HookManager: React.FC<HookManagerProps> = ({ action, onComplete }) => {
|
|
21
|
+
const [status, setStatus] = useState(getHookStatus())
|
|
22
|
+
const [message, setMessage] = useState<string | null>(null)
|
|
23
|
+
const [isProcessing, setIsProcessing] = useState(false)
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const execute = async () => {
|
|
27
|
+
if (action === 'install') {
|
|
28
|
+
setIsProcessing(true)
|
|
29
|
+
const shellType = detectShell()
|
|
30
|
+
const configPath = getShellConfigPath(shellType)
|
|
31
|
+
|
|
32
|
+
if (shellType === 'unknown') {
|
|
33
|
+
setMessage('❌ 不支持的 shell 类型')
|
|
34
|
+
setIsProcessing(false)
|
|
35
|
+
setTimeout(onComplete, 2000)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const result = await installShellHook()
|
|
40
|
+
setStatus(getHookStatus())
|
|
41
|
+
setIsProcessing(false)
|
|
42
|
+
|
|
43
|
+
if (result) {
|
|
44
|
+
setMessage(
|
|
45
|
+
`✅ Shell hook 已安装\n⚠️ 请重启终端或执行: source ${configPath}`
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setTimeout(onComplete, 3000)
|
|
50
|
+
} else if (action === 'uninstall') {
|
|
51
|
+
setIsProcessing(true)
|
|
52
|
+
uninstallShellHook()
|
|
53
|
+
setStatus(getHookStatus())
|
|
54
|
+
setMessage('✅ Shell hook 已卸载\n⚠️ 请重启终端使其生效')
|
|
55
|
+
setIsProcessing(false)
|
|
56
|
+
setTimeout(onComplete, 3000)
|
|
57
|
+
} else {
|
|
58
|
+
// status
|
|
59
|
+
setTimeout(onComplete, 100)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
execute()
|
|
64
|
+
}, [action, onComplete])
|
|
65
|
+
|
|
66
|
+
if (action === 'install' || action === 'uninstall') {
|
|
67
|
+
return (
|
|
68
|
+
<Box flexDirection="column" marginY={1}>
|
|
69
|
+
<Text bold color={theme.accent}>
|
|
70
|
+
🔧 Shell Hook {action === 'install' ? '安装' : '卸载'}向导
|
|
71
|
+
</Text>
|
|
72
|
+
<Text color={theme.text.secondary}>{'━'.repeat(40)}</Text>
|
|
73
|
+
|
|
74
|
+
{isProcessing && <Text color={theme.info}>处理中...</Text>}
|
|
75
|
+
|
|
76
|
+
{message && (
|
|
77
|
+
<Box flexDirection="column" marginTop={1}>
|
|
78
|
+
{message.split('\n').map((line, i) => (
|
|
79
|
+
<Text
|
|
80
|
+
key={i}
|
|
81
|
+
color={
|
|
82
|
+
line.startsWith('✅')
|
|
83
|
+
? theme.success
|
|
84
|
+
: line.startsWith('⚠️')
|
|
85
|
+
? theme.warning
|
|
86
|
+
: line.startsWith('❌')
|
|
87
|
+
? theme.error
|
|
88
|
+
: theme.text.primary
|
|
89
|
+
}
|
|
90
|
+
>
|
|
91
|
+
{line}
|
|
92
|
+
</Text>
|
|
93
|
+
))}
|
|
94
|
+
</Box>
|
|
95
|
+
)}
|
|
96
|
+
</Box>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Status display
|
|
101
|
+
return (
|
|
102
|
+
<Box flexDirection="column" marginY={1}>
|
|
103
|
+
<Text bold>📊 Shell Hook 状态</Text>
|
|
104
|
+
<Text color={theme.text.secondary}>{'━'.repeat(40)}</Text>
|
|
105
|
+
|
|
106
|
+
<Box marginTop={1}>
|
|
107
|
+
<Text color={theme.primary}> Shell 类型: </Text>
|
|
108
|
+
<Text>{status.shellType}</Text>
|
|
109
|
+
</Box>
|
|
110
|
+
|
|
111
|
+
<Box>
|
|
112
|
+
<Text color={theme.primary}> 配置文件: </Text>
|
|
113
|
+
<Text>{status.configPath || '未知'}</Text>
|
|
114
|
+
</Box>
|
|
115
|
+
|
|
116
|
+
<Box>
|
|
117
|
+
<Text color={theme.primary}> 已安装: </Text>
|
|
118
|
+
{status.installed ? (
|
|
119
|
+
<Text color={theme.success}>是</Text>
|
|
120
|
+
) : (
|
|
121
|
+
<Text color={theme.text.secondary}>否</Text>
|
|
122
|
+
)}
|
|
123
|
+
</Box>
|
|
124
|
+
|
|
125
|
+
<Box>
|
|
126
|
+
<Text color={theme.primary}> 已启用: </Text>
|
|
127
|
+
{status.enabled ? (
|
|
128
|
+
<Text color={theme.success}>是</Text>
|
|
129
|
+
) : (
|
|
130
|
+
<Text color={theme.text.secondary}>否</Text>
|
|
131
|
+
)}
|
|
132
|
+
</Box>
|
|
133
|
+
|
|
134
|
+
<Box>
|
|
135
|
+
<Text color={theme.primary}> 历史文件: </Text>
|
|
136
|
+
<Text>{status.historyFile}</Text>
|
|
137
|
+
</Box>
|
|
138
|
+
|
|
139
|
+
<Text color={theme.text.secondary}>{'━'.repeat(40)}</Text>
|
|
140
|
+
|
|
141
|
+
{!status.installed && (
|
|
142
|
+
<Box marginTop={1}>
|
|
143
|
+
<Text color={theme.text.secondary}>
|
|
144
|
+
提示: 运行 <Text color={theme.primary}>pls hook install</Text> 安装 shell hook
|
|
145
|
+
</Text>
|
|
146
|
+
</Box>
|
|
147
|
+
)}
|
|
148
|
+
</Box>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Text } from 'ink'
|
|
3
|
+
import { theme } from '../ui/theme.js'
|
|
4
|
+
|
|
5
|
+
interface RenderInlineProps {
|
|
6
|
+
text: string
|
|
7
|
+
defaultColor?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 行内 Markdown 渲染器
|
|
12
|
+
* 处理 **粗体**、*斜体*、`代码`、~~删除线~~、<u>下划线</u>、链接
|
|
13
|
+
*/
|
|
14
|
+
function RenderInlineInternal({ text, defaultColor }: RenderInlineProps) {
|
|
15
|
+
const baseColor = defaultColor || theme.text.primary
|
|
16
|
+
|
|
17
|
+
// 快速路径:纯文本无 markdown
|
|
18
|
+
if (!/[*_~`<[https?:]/.test(text)) {
|
|
19
|
+
return <Text color={baseColor}>{text}</Text>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const nodes: React.ReactNode[] = []
|
|
23
|
+
let lastIndex = 0
|
|
24
|
+
|
|
25
|
+
// 匹配所有行内 markdown 语法(添加删除线、下划线、链接)
|
|
26
|
+
const inlineRegex = /(~~.+?~~|\*\*[^*]+?\*\*|\*[^*]+?\*|_[^_]+?_|`+[^`]+?`+|\[.*?\]\(.*?\)|<u>.*?<\/u>|https?:\/\/\S+)/g
|
|
27
|
+
let match
|
|
28
|
+
|
|
29
|
+
while ((match = inlineRegex.exec(text)) !== null) {
|
|
30
|
+
// 添加匹配之前的普通文本
|
|
31
|
+
if (match.index > lastIndex) {
|
|
32
|
+
nodes.push(
|
|
33
|
+
<Text key={`t-${lastIndex}`} color={baseColor}>
|
|
34
|
+
{text.slice(lastIndex, match.index)}
|
|
35
|
+
</Text>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const fullMatch = match[0]
|
|
40
|
+
let renderedNode: React.ReactNode = null
|
|
41
|
+
const key = `m-${match.index}`
|
|
42
|
+
|
|
43
|
+
// **粗体**
|
|
44
|
+
if (fullMatch.startsWith('**') && fullMatch.endsWith('**') && fullMatch.length > 4) {
|
|
45
|
+
renderedNode = (
|
|
46
|
+
<Text key={key} bold color={baseColor}>
|
|
47
|
+
{fullMatch.slice(2, -2)}
|
|
48
|
+
</Text>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
// ~~删除线~~
|
|
52
|
+
else if (fullMatch.startsWith('~~') && fullMatch.endsWith('~~') && fullMatch.length > 4) {
|
|
53
|
+
renderedNode = (
|
|
54
|
+
<Text key={key} strikethrough color={baseColor}>
|
|
55
|
+
{fullMatch.slice(2, -2)}
|
|
56
|
+
</Text>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
// *斜体*
|
|
60
|
+
else if (fullMatch.startsWith('*') && fullMatch.endsWith('*') && fullMatch.length > 2 && !fullMatch.startsWith('**')) {
|
|
61
|
+
renderedNode = (
|
|
62
|
+
<Text key={key} italic color={baseColor}>
|
|
63
|
+
{fullMatch.slice(1, -1)}
|
|
64
|
+
</Text>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
// `行内代码`
|
|
68
|
+
else if (fullMatch.startsWith('`') && fullMatch.endsWith('`')) {
|
|
69
|
+
const codeText = fullMatch.slice(1, -1)
|
|
70
|
+
renderedNode = (
|
|
71
|
+
<Text key={key} color={theme.primary}>
|
|
72
|
+
{codeText}
|
|
73
|
+
</Text>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
// [链接文本](URL)
|
|
77
|
+
else if (fullMatch.startsWith('[') && fullMatch.includes('](') && fullMatch.endsWith(')')) {
|
|
78
|
+
const linkMatch = fullMatch.match(/\[(.*?)\]\((.*?)\)/)
|
|
79
|
+
if (linkMatch) {
|
|
80
|
+
const linkText = linkMatch[1]
|
|
81
|
+
const url = linkMatch[2]
|
|
82
|
+
renderedNode = (
|
|
83
|
+
<Text key={key} color={baseColor}>
|
|
84
|
+
{linkText}
|
|
85
|
+
<Text color={theme.info}> ({url})</Text>
|
|
86
|
+
</Text>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// <u>下划线</u>
|
|
91
|
+
else if (fullMatch.startsWith('<u>') && fullMatch.endsWith('</u>') && fullMatch.length > 7) {
|
|
92
|
+
renderedNode = (
|
|
93
|
+
<Text key={key} underline color={baseColor}>
|
|
94
|
+
{fullMatch.slice(3, -4)}
|
|
95
|
+
</Text>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
// 裸 URL
|
|
99
|
+
else if (fullMatch.match(/^https?:\/\//)) {
|
|
100
|
+
renderedNode = (
|
|
101
|
+
<Text key={key} color={theme.info}>
|
|
102
|
+
{fullMatch}
|
|
103
|
+
</Text>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
nodes.push(renderedNode || <Text key={key} color={baseColor}>{fullMatch}</Text>)
|
|
108
|
+
lastIndex = inlineRegex.lastIndex
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 添加剩余的普通文本
|
|
112
|
+
if (lastIndex < text.length) {
|
|
113
|
+
nodes.push(
|
|
114
|
+
<Text key={`t-${lastIndex}`} color={baseColor}>
|
|
115
|
+
{text.slice(lastIndex)}
|
|
116
|
+
</Text>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return <>{nodes}</>
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const RenderInline = React.memo(RenderInlineInternal)
|