@amaster.ai/components-templates 1.5.0 → 1.8.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/README.md +12 -8
- package/components/ai-assistant/amaster.config.json +3 -0
- package/components/ai-assistant/package.json +3 -3
- package/components/ai-assistant/template/components/chat-assistant-message.tsx +1 -1
- package/components/ai-assistant/template/components/chat-banner.tsx +1 -1
- package/components/ai-assistant/template/components/chat-display-mode-switcher.tsx +6 -6
- package/components/ai-assistant/template/components/chat-floating-button.tsx +4 -5
- package/components/ai-assistant/template/components/chat-floating-card.tsx +3 -3
- package/components/ai-assistant/template/components/chat-header.tsx +5 -5
- package/components/ai-assistant/template/components/chat-input.tsx +36 -24
- package/components/ai-assistant/template/components/chat-messages.tsx +11 -3
- package/components/ai-assistant/template/components/chat-recommends.tsx +12 -14
- package/components/ai-assistant/template/components/chat-user-message.tsx +1 -1
- package/components/ai-assistant/template/components/ui-renderer.tsx +1 -1
- package/components/ai-assistant/template/components/voice-input.tsx +10 -2
- package/components/ai-assistant/template/hooks/useConversation.ts +0 -23
- package/components/ai-assistant/template/hooks/useVoiceInput.ts +18 -20
- package/components/ai-assistant/template/inline-ai-assistant.tsx +1 -1
- package/components/ai-assistant-taro/amaster.config.json +3 -0
- package/components/ai-assistant-taro/package.json +94 -0
- package/components/ai-assistant-taro/template/components/ChatAssistantMessage.tsx +176 -0
- package/components/ai-assistant-taro/template/components/ChatHeader.tsx +27 -0
- package/components/ai-assistant-taro/template/components/ChatInput.tsx +233 -0
- package/components/ai-assistant-taro/template/components/ChatMessages.tsx +126 -0
- package/components/ai-assistant-taro/template/components/ChatUserMessage.tsx +25 -0
- package/components/ai-assistant-taro/template/components/VoiceInput.tsx +169 -0
- package/components/ai-assistant-taro/template/components/markdown.tsx +362 -0
- package/components/ai-assistant-taro/template/hooks/useConversation.ts +905 -0
- package/components/ai-assistant-taro/template/hooks/useSafeArea.ts +20 -0
- package/components/ai-assistant-taro/template/hooks/useVoiceInput.ts +204 -0
- package/components/ai-assistant-taro/template/i18n.ts +157 -0
- package/components/ai-assistant-taro/template/index.config.ts +10 -0
- package/components/ai-assistant-taro/template/index.tsx +83 -0
- package/components/ai-assistant-taro/template/types.ts +74 -0
- package/package.json +5 -2
- package/packages/cli/dist/index.js +14 -3
- package/packages/cli/dist/index.js.map +1 -1
- package/packages/cli/package.json +1 -1
- package/components/ai-assistant/example.md +0 -34
- package/components/ai-assistant/others.md +0 -16
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Text, View } from "@tarojs/components";
|
|
2
|
+
import Taro from "@tarojs/taro";
|
|
3
|
+
import type React from "react";
|
|
4
|
+
import { useCallback, useRef, useState } from "react";
|
|
5
|
+
import { useVoiceInput, type VoiceInputStatus } from "../hooks/useVoiceInput";
|
|
6
|
+
import { useAiAssistantI18n } from "../i18n";
|
|
7
|
+
|
|
8
|
+
interface VoiceInputProps {
|
|
9
|
+
onResult: (text: string) => void;
|
|
10
|
+
onError?: (error: string) => void;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const getStatusIcon = (status: VoiceInputStatus): string => {
|
|
15
|
+
switch (status) {
|
|
16
|
+
case "recording":
|
|
17
|
+
return "i-lucide-mic";
|
|
18
|
+
case "recognizing":
|
|
19
|
+
return "i-lucide-brain";
|
|
20
|
+
default:
|
|
21
|
+
return "i-lucide-mic";
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const getStatusColor = (status: VoiceInputStatus): string => {
|
|
26
|
+
switch (status) {
|
|
27
|
+
case "recording":
|
|
28
|
+
return "bg-red-500";
|
|
29
|
+
case "recognizing":
|
|
30
|
+
return "bg-blue-500";
|
|
31
|
+
default:
|
|
32
|
+
return "bg-gray-500";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const VoiceInput: React.FC<VoiceInputProps> = ({
|
|
37
|
+
onResult,
|
|
38
|
+
disabled = false,
|
|
39
|
+
}) => {
|
|
40
|
+
const [showOverlay, setShowOverlay] = useState(false);
|
|
41
|
+
const shouldStopRef = useRef(false);
|
|
42
|
+
const { t } = useAiAssistantI18n();
|
|
43
|
+
|
|
44
|
+
const getStatusText = (status: VoiceInputStatus): string => {
|
|
45
|
+
switch (status) {
|
|
46
|
+
case "recording":
|
|
47
|
+
return t.VoiceInputStatus.recording;
|
|
48
|
+
case "recognizing":
|
|
49
|
+
return t.VoiceInputStatus.recognizing;
|
|
50
|
+
default:
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const handleStatusChange = useCallback((newStatus: VoiceInputStatus) => {
|
|
56
|
+
if (newStatus === "idle") {
|
|
57
|
+
setShowOverlay(false);
|
|
58
|
+
// setTimeout(() => {
|
|
59
|
+
// }, 500);
|
|
60
|
+
}
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
const handleResult = useCallback(
|
|
64
|
+
(text: string) => {
|
|
65
|
+
if (text) {
|
|
66
|
+
onResult(text);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
[onResult],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const { status, startRecording, stopRecording } = useVoiceInput({
|
|
73
|
+
onResult: handleResult,
|
|
74
|
+
onError: (error) => {
|
|
75
|
+
let errorType = error;
|
|
76
|
+
if (error.includes("deny")) {
|
|
77
|
+
errorType = "MIC_PERMISSION_DENIED";
|
|
78
|
+
}
|
|
79
|
+
Taro.showToast({
|
|
80
|
+
title: t.voiceInputError[errorType] || error,
|
|
81
|
+
icon: "none",
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
onStatusChange: handleStatusChange,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const handleTouchStart = () => {
|
|
88
|
+
if (disabled) return;
|
|
89
|
+
shouldStopRef.current = false;
|
|
90
|
+
setShowOverlay(true);
|
|
91
|
+
startRecording();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleTouchEnd = () => {
|
|
95
|
+
shouldStopRef.current = true;
|
|
96
|
+
stopRecording();
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<>
|
|
101
|
+
{showOverlay && (
|
|
102
|
+
<View
|
|
103
|
+
className="fixed top-0 left-0 right-0 bottom-0 w-screen h-screen z-[9999] flex items-center justify-center"
|
|
104
|
+
style={{ backgroundColor: "rgba(0, 0, 0, 0.6)" }}
|
|
105
|
+
catchMove
|
|
106
|
+
>
|
|
107
|
+
<View className="flex flex-col items-center gap-6 px-20 py-10 bg-black/80 rounded-3xl shadow-2xl relative">
|
|
108
|
+
<View
|
|
109
|
+
className={`size-20 rounded-full ${getStatusColor(status)} flex items-center justify-center shadow-lg ${status === "recording" ? "animate-pulse" : ""}`}
|
|
110
|
+
>
|
|
111
|
+
<View
|
|
112
|
+
className={`text-white text-5xl ${getStatusIcon(status)}`}
|
|
113
|
+
/>
|
|
114
|
+
</View>
|
|
115
|
+
|
|
116
|
+
<Text className="text-white text-center text-xl font-medium whitespace-pre-line leading-relaxed">
|
|
117
|
+
{getStatusText(status)}
|
|
118
|
+
</Text>
|
|
119
|
+
|
|
120
|
+
{(status === "recording" || status === "idle") && (
|
|
121
|
+
<View className="flex items-center gap-2 mt-2">
|
|
122
|
+
<View className="w-2 h-2 bg-red-400 rounded-full animate-pulse" />
|
|
123
|
+
<View
|
|
124
|
+
className="w-2 h-2 bg-red-400 rounded-full animate-pulse"
|
|
125
|
+
style={{ animationDelay: "150ms" }}
|
|
126
|
+
/>
|
|
127
|
+
<View
|
|
128
|
+
className="w-2 h-2 bg-red-400 rounded-full animate-pulse"
|
|
129
|
+
style={{ animationDelay: "300ms" }}
|
|
130
|
+
/>
|
|
131
|
+
</View>
|
|
132
|
+
)}
|
|
133
|
+
|
|
134
|
+
{/* 点击结束 */}
|
|
135
|
+
<View
|
|
136
|
+
className="w-6 h-6 i-lucide-x text-white rounded-full bg-gray-500 absolute top-2 right-2"
|
|
137
|
+
onClick={() => {
|
|
138
|
+
stopRecording();
|
|
139
|
+
}}
|
|
140
|
+
/>
|
|
141
|
+
</View>
|
|
142
|
+
</View>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
<View
|
|
146
|
+
onTouchStart={handleTouchStart}
|
|
147
|
+
onTouchEnd={handleTouchEnd}
|
|
148
|
+
onTouchCancel={handleTouchEnd}
|
|
149
|
+
onLongPress={handleTouchStart}
|
|
150
|
+
className={`flex items-center justify-center gap-1.5 px-4 py-2 bg-danger rounded-xl active:opacity-80 transition-all duration-200 shadow-sm ${
|
|
151
|
+
disabled
|
|
152
|
+
? "bg-gray-200 opacity-50"
|
|
153
|
+
: status !== "idle"
|
|
154
|
+
? "bg-red-500 shadow-md"
|
|
155
|
+
: "bg-gradient-to-r from-primary to-primary/80 active:opacity-80 shadow-md"
|
|
156
|
+
}`}
|
|
157
|
+
hoverClass={disabled ? "" : "opacity-90"}
|
|
158
|
+
>
|
|
159
|
+
<View
|
|
160
|
+
className={`i-lucide-mic text-lg ${
|
|
161
|
+
disabled ? "text-gray-400" : "text-white"
|
|
162
|
+
}`}
|
|
163
|
+
/>
|
|
164
|
+
</View>
|
|
165
|
+
</>
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export default VoiceInput;
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
// components/MarkdownLite.tsx
|
|
2
|
+
|
|
3
|
+
import { Image, Text, View } from "@tarojs/components";
|
|
4
|
+
import Taro from "@tarojs/taro";
|
|
5
|
+
import MarkdownIt from "markdown-it";
|
|
6
|
+
import { memo, useMemo } from "react";
|
|
7
|
+
|
|
8
|
+
const md = new MarkdownIt({
|
|
9
|
+
html: true,
|
|
10
|
+
breaks: true,
|
|
11
|
+
linkify: true,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
function buildTokenPairs(tokens: any[]): Map<number, number> {
|
|
15
|
+
const pairs = new Map<number, number>();
|
|
16
|
+
const stack: number[] = [];
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
19
|
+
const token = tokens[i];
|
|
20
|
+
if (token.nesting === 1) {
|
|
21
|
+
stack.push(i);
|
|
22
|
+
} else if (token.nesting === -1) {
|
|
23
|
+
const openIndex = stack.pop();
|
|
24
|
+
if (openIndex !== undefined) {
|
|
25
|
+
pairs.set(openIndex, i);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return pairs;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface RenderResult {
|
|
34
|
+
elements: JSX.Element[];
|
|
35
|
+
nextIndex: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderTokens(
|
|
39
|
+
tokens: any[],
|
|
40
|
+
pairs: Map<number, number>,
|
|
41
|
+
startIndex: number,
|
|
42
|
+
endIndex: number,
|
|
43
|
+
keyPrefix: string,
|
|
44
|
+
listContext?: { isOrdered: boolean; start: number },
|
|
45
|
+
): RenderResult {
|
|
46
|
+
const elements: JSX.Element[] = [];
|
|
47
|
+
let i = startIndex;
|
|
48
|
+
|
|
49
|
+
while (i < endIndex) {
|
|
50
|
+
const token = tokens[i];
|
|
51
|
+
const key = `${keyPrefix}-${i}`;
|
|
52
|
+
|
|
53
|
+
switch (token.type) {
|
|
54
|
+
case "heading_open": {
|
|
55
|
+
const level = token.markup.length;
|
|
56
|
+
const hClasses =
|
|
57
|
+
{
|
|
58
|
+
1: "text-4xl font-bold mt-4 mb-4 border-b border-gray-200 dark:border-gray-700",
|
|
59
|
+
2: "text-3xl font-bold mt-3 mb-3 border-l-4 border-blue-500",
|
|
60
|
+
3: "text-2xl font-bold mt-2 mb-2",
|
|
61
|
+
4: "text-xl font-bold mt-1 mb-1",
|
|
62
|
+
5: "text-lg font-bold",
|
|
63
|
+
6: "text-base font-bold",
|
|
64
|
+
}[level] || "text-lg font-bold mt-2 mb-2";
|
|
65
|
+
|
|
66
|
+
const closeIndex = pairs.get(i) ?? tokens.length;
|
|
67
|
+
const { elements: children } = renderTokens(
|
|
68
|
+
tokens,
|
|
69
|
+
pairs,
|
|
70
|
+
i + 1,
|
|
71
|
+
closeIndex,
|
|
72
|
+
key,
|
|
73
|
+
);
|
|
74
|
+
elements.push(
|
|
75
|
+
<View key={key} className={hClasses}>
|
|
76
|
+
{children}
|
|
77
|
+
</View>,
|
|
78
|
+
);
|
|
79
|
+
i = closeIndex + 1;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
case "paragraph_open": {
|
|
84
|
+
const closeIndex = pairs.get(i) ?? tokens.length;
|
|
85
|
+
const { elements: children } = renderTokens(
|
|
86
|
+
tokens,
|
|
87
|
+
pairs,
|
|
88
|
+
i + 1,
|
|
89
|
+
closeIndex,
|
|
90
|
+
key,
|
|
91
|
+
listContext,
|
|
92
|
+
);
|
|
93
|
+
elements.push(
|
|
94
|
+
<View key={key} className="mb-1 leading-relaxed text-base">
|
|
95
|
+
{children}
|
|
96
|
+
</View>,
|
|
97
|
+
);
|
|
98
|
+
i = closeIndex + 1;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case "bullet_list_open": {
|
|
103
|
+
const closeIndex = pairs.get(i) ?? tokens.length;
|
|
104
|
+
const { elements: children } = renderTokens(
|
|
105
|
+
tokens,
|
|
106
|
+
pairs,
|
|
107
|
+
i + 1,
|
|
108
|
+
closeIndex,
|
|
109
|
+
key,
|
|
110
|
+
{ isOrdered: false, start: 1 },
|
|
111
|
+
);
|
|
112
|
+
elements.push(
|
|
113
|
+
<View key={key} className="my-2 pl-2">
|
|
114
|
+
{children}
|
|
115
|
+
</View>,
|
|
116
|
+
);
|
|
117
|
+
i = closeIndex + 1;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case "ordered_list_open": {
|
|
122
|
+
const start = token.attrGet?.("start") || 1;
|
|
123
|
+
const closeIndex = pairs.get(i) ?? tokens.length;
|
|
124
|
+
const { elements: children } = renderTokens(
|
|
125
|
+
tokens,
|
|
126
|
+
pairs,
|
|
127
|
+
i + 1,
|
|
128
|
+
closeIndex,
|
|
129
|
+
key,
|
|
130
|
+
{ isOrdered: true, start },
|
|
131
|
+
);
|
|
132
|
+
elements.push(
|
|
133
|
+
<View key={key} className="my-2 pl-2">
|
|
134
|
+
{children}
|
|
135
|
+
</View>,
|
|
136
|
+
);
|
|
137
|
+
i = closeIndex + 1;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
case "list_item_open": {
|
|
142
|
+
const closeIndex = pairs.get(i) ?? tokens.length;
|
|
143
|
+
let itemIndex = 0;
|
|
144
|
+
for (let j = startIndex; j < i; j++) {
|
|
145
|
+
if (
|
|
146
|
+
tokens[j].type === "list_item_open" &&
|
|
147
|
+
tokens[j].level === token.level
|
|
148
|
+
) {
|
|
149
|
+
itemIndex++;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const itemNumber = (listContext?.start || 1) + itemIndex;
|
|
153
|
+
|
|
154
|
+
const { elements: children } = renderTokens(
|
|
155
|
+
tokens,
|
|
156
|
+
pairs,
|
|
157
|
+
i + 1,
|
|
158
|
+
closeIndex,
|
|
159
|
+
key,
|
|
160
|
+
listContext,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
elements.push(
|
|
164
|
+
<View key={key} className="flex flex-row items-start mb-1">
|
|
165
|
+
<Text className="mr-2 text-gray-700 dark:text-gray-300 w-5 text-right shrink-0">
|
|
166
|
+
{listContext?.isOrdered ? `${itemNumber}.` : "•"}
|
|
167
|
+
</Text>
|
|
168
|
+
<View className="flex-1 overflow-hidden">{children}</View>
|
|
169
|
+
</View>,
|
|
170
|
+
);
|
|
171
|
+
i = closeIndex + 1;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
case "strong_open": {
|
|
176
|
+
const closeIndex = pairs.get(i) ?? tokens.length;
|
|
177
|
+
const { elements: children } = renderTokens(
|
|
178
|
+
tokens,
|
|
179
|
+
pairs,
|
|
180
|
+
i + 1,
|
|
181
|
+
closeIndex,
|
|
182
|
+
key,
|
|
183
|
+
listContext,
|
|
184
|
+
);
|
|
185
|
+
elements.push(
|
|
186
|
+
<Text
|
|
187
|
+
key={key}
|
|
188
|
+
className="font-bold text-gray-900 dark:text-gray-100"
|
|
189
|
+
>
|
|
190
|
+
{children}
|
|
191
|
+
</Text>,
|
|
192
|
+
);
|
|
193
|
+
i = closeIndex + 1;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
case "em_open": {
|
|
198
|
+
const closeIndex = pairs.get(i) ?? tokens.length;
|
|
199
|
+
const { elements: children } = renderTokens(
|
|
200
|
+
tokens,
|
|
201
|
+
pairs,
|
|
202
|
+
i + 1,
|
|
203
|
+
closeIndex,
|
|
204
|
+
key,
|
|
205
|
+
listContext,
|
|
206
|
+
);
|
|
207
|
+
elements.push(
|
|
208
|
+
<Text key={key} className="italic text-gray-700 dark:text-gray-300">
|
|
209
|
+
{children}
|
|
210
|
+
</Text>,
|
|
211
|
+
);
|
|
212
|
+
i = closeIndex + 1;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case "link_open": {
|
|
217
|
+
const href =
|
|
218
|
+
token.attrs?.find((a: string[]) => a[0] === "href")?.[1] || "";
|
|
219
|
+
const closeIndex = pairs.get(i) ?? tokens.length;
|
|
220
|
+
const { elements: children } = renderTokens(
|
|
221
|
+
tokens,
|
|
222
|
+
pairs,
|
|
223
|
+
i + 1,
|
|
224
|
+
closeIndex,
|
|
225
|
+
key,
|
|
226
|
+
listContext,
|
|
227
|
+
);
|
|
228
|
+
elements.push(
|
|
229
|
+
<Text
|
|
230
|
+
key={key}
|
|
231
|
+
className="text-blue-600 dark:text-blue-400 no-underline hover:underline cursor-pointer"
|
|
232
|
+
onClick={() => {
|
|
233
|
+
if (href) {
|
|
234
|
+
Taro.setClipboardData({ data: href }).then(() =>
|
|
235
|
+
Taro.showToast({ title: "链接已复制", icon: "none" }),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}}
|
|
239
|
+
>
|
|
240
|
+
{children}
|
|
241
|
+
</Text>,
|
|
242
|
+
);
|
|
243
|
+
i = closeIndex + 1;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
case "code_inline":
|
|
248
|
+
elements.push(
|
|
249
|
+
<Text
|
|
250
|
+
key={key}
|
|
251
|
+
className="font-mono text-red-600 dark:text-red-400 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded"
|
|
252
|
+
>
|
|
253
|
+
{token.content}
|
|
254
|
+
</Text>,
|
|
255
|
+
);
|
|
256
|
+
i++;
|
|
257
|
+
break;
|
|
258
|
+
|
|
259
|
+
case "fence": {
|
|
260
|
+
const lang = token.info ? token.info.trim() : "";
|
|
261
|
+
elements.push(
|
|
262
|
+
<View
|
|
263
|
+
key={key}
|
|
264
|
+
className="my-2 bg-gray-900 text-gray-200 p-4 rounded-xl overflow-x-auto shadow-lg font-mono text-sm leading-6 relative"
|
|
265
|
+
>
|
|
266
|
+
<Text className="whitespace-pre">{token.content.trim()}</Text>
|
|
267
|
+
{lang && (
|
|
268
|
+
<View className="absolute top-2 right-3 text-xs text-gray-500 bg-gray-800 px-2 py-1 rounded">
|
|
269
|
+
{lang}
|
|
270
|
+
</View>
|
|
271
|
+
)}
|
|
272
|
+
</View>,
|
|
273
|
+
);
|
|
274
|
+
i++;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
case "image": {
|
|
279
|
+
const src =
|
|
280
|
+
token.attrs?.find((a: string[]) => a[0] === "src")?.[1] || "";
|
|
281
|
+
elements.push(
|
|
282
|
+
<Image
|
|
283
|
+
key={key}
|
|
284
|
+
src={src}
|
|
285
|
+
mode="widthFix"
|
|
286
|
+
lazyLoad
|
|
287
|
+
className="rounded-xl shadow-md my-2 max-w-full inline-block"
|
|
288
|
+
onClick={() => src && Taro.previewImage({ urls: [src] })}
|
|
289
|
+
/>,
|
|
290
|
+
);
|
|
291
|
+
i++;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
case "text":
|
|
296
|
+
elements.push(<Text key={key}>{token.content}</Text>);
|
|
297
|
+
i++;
|
|
298
|
+
break;
|
|
299
|
+
|
|
300
|
+
case "softbreak":
|
|
301
|
+
case "hardbreak":
|
|
302
|
+
elements.push(<Text key={key}> </Text>);
|
|
303
|
+
i++;
|
|
304
|
+
break;
|
|
305
|
+
|
|
306
|
+
case "heading_close":
|
|
307
|
+
case "paragraph_close":
|
|
308
|
+
case "strong_close":
|
|
309
|
+
case "em_close":
|
|
310
|
+
case "link_close":
|
|
311
|
+
case "bullet_list_close":
|
|
312
|
+
case "ordered_list_close":
|
|
313
|
+
case "list_item_close":
|
|
314
|
+
// 这些由对应的 open 处理,直接跳过
|
|
315
|
+
i++;
|
|
316
|
+
break;
|
|
317
|
+
|
|
318
|
+
default:
|
|
319
|
+
if (token.children && token.children.length > 0) {
|
|
320
|
+
const childPairs = buildTokenPairs(token.children);
|
|
321
|
+
const { elements: childElements } = renderTokens(
|
|
322
|
+
token.children,
|
|
323
|
+
childPairs,
|
|
324
|
+
0,
|
|
325
|
+
token.children.length,
|
|
326
|
+
key,
|
|
327
|
+
listContext,
|
|
328
|
+
);
|
|
329
|
+
elements.push(<Text key={key}>{childElements}</Text>);
|
|
330
|
+
}
|
|
331
|
+
i++;
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return { elements, nextIndex: i };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
interface MarkdownLiteProps {
|
|
340
|
+
content: string;
|
|
341
|
+
className?: string;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const MarkdownLite = memo(({ content, className }: MarkdownLiteProps) => {
|
|
345
|
+
const elements = useMemo(() => {
|
|
346
|
+
if (!content) return [];
|
|
347
|
+
const tokens = md.parse(content, {});
|
|
348
|
+
const pairs = buildTokenPairs(tokens);
|
|
349
|
+
const { elements: rendered } = renderTokens(
|
|
350
|
+
tokens,
|
|
351
|
+
pairs,
|
|
352
|
+
0,
|
|
353
|
+
tokens.length,
|
|
354
|
+
"root",
|
|
355
|
+
);
|
|
356
|
+
return rendered;
|
|
357
|
+
}, [content]);
|
|
358
|
+
|
|
359
|
+
return <View className={`text-lg ${className || ""}`}>{elements}</View>;
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
export default MarkdownLite;
|