@lobehub/chat 1.82.0 → 1.82.1
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/.cursor/rules/desktop-local-tools-implement.mdc +80 -0
- package/.env.desktop +2 -1
- package/.github/scripts/pr-comment.js +4 -9
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/locales/ar/electron.json +38 -2
- package/locales/ar/plugin.json +31 -31
- package/locales/bg-BG/electron.json +38 -2
- package/locales/bg-BG/plugin.json +31 -31
- package/locales/de-DE/electron.json +38 -2
- package/locales/de-DE/plugin.json +3 -8
- package/locales/en-US/electron.json +38 -2
- package/locales/en-US/plugin.json +3 -8
- package/locales/es-ES/electron.json +38 -2
- package/locales/es-ES/plugin.json +31 -31
- package/locales/fa-IR/electron.json +38 -2
- package/locales/fa-IR/plugin.json +31 -31
- package/locales/fr-FR/electron.json +38 -2
- package/locales/fr-FR/plugin.json +31 -31
- package/locales/it-IT/electron.json +38 -2
- package/locales/it-IT/plugin.json +31 -31
- package/locales/ja-JP/electron.json +38 -2
- package/locales/ja-JP/plugin.json +31 -31
- package/locales/ko-KR/electron.json +38 -2
- package/locales/ko-KR/plugin.json +3 -8
- package/locales/nl-NL/electron.json +38 -2
- package/locales/nl-NL/plugin.json +31 -31
- package/locales/pl-PL/electron.json +38 -2
- package/locales/pl-PL/plugin.json +3 -8
- package/locales/pt-BR/electron.json +38 -2
- package/locales/pt-BR/plugin.json +31 -31
- package/locales/ru-RU/electron.json +38 -2
- package/locales/ru-RU/plugin.json +31 -31
- package/locales/tr-TR/electron.json +38 -2
- package/locales/tr-TR/plugin.json +31 -31
- package/locales/vi-VN/electron.json +38 -2
- package/locales/vi-VN/plugin.json +3 -8
- package/locales/zh-CN/electron.json +38 -2
- package/locales/zh-CN/plugin.json +14 -9
- package/locales/zh-TW/electron.json +38 -2
- package/locales/zh-TW/plugin.json +31 -31
- package/package.json +1 -1
- package/packages/electron-client-ipc/src/events/update.ts +3 -3
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Mode.tsx +222 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Option.tsx +104 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Sync.tsx +42 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Waiting.tsx +203 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/index.tsx +57 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/UpdateModal.tsx +242 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/UpdateNotification.tsx +193 -0
- package/src/app/[variants]/(main)/_layout/Desktop/{Titlebar.tsx → ElectronTitlebar/index.tsx} +15 -1
- package/src/app/[variants]/(main)/_layout/Desktop/SideBar/BottomActions.tsx +3 -2
- package/src/app/[variants]/(main)/_layout/Desktop/index.tsx +1 -1
- package/src/app/[variants]/layout.tsx +2 -1
- package/src/features/Conversation/components/MarkdownElements/LocalFile/Render/LocalFile.tsx +65 -0
- package/src/features/Conversation/components/MarkdownElements/LocalFile/Render/index.tsx +29 -0
- package/src/features/Conversation/components/MarkdownElements/LocalFile/index.ts +16 -0
- package/src/features/Conversation/components/MarkdownElements/index.ts +7 -1
- package/src/features/Conversation/components/MarkdownElements/remarkPlugins/__snapshots__/createRemarkSelfClosingTagPlugin.test.ts.snap +260 -0
- package/src/features/Conversation/components/MarkdownElements/remarkPlugins/createRemarkSelfClosingTagPlugin.test.ts +204 -0
- package/src/features/Conversation/components/MarkdownElements/remarkPlugins/createRemarkSelfClosingTagPlugin.ts +133 -0
- package/src/features/Conversation/components/MarkdownElements/type.ts +5 -1
- package/src/features/PluginDevModal/MCPManifestForm/ArgsInput.tsx +20 -0
- package/src/features/PluginDevModal/MCPManifestForm/MCPTypeSelect.tsx +176 -0
- package/src/features/PluginDevModal/{MCPManifestForm.tsx → MCPManifestForm/index.tsx} +92 -30
- package/src/libs/mcp/__tests__/__snapshots__/index.test.ts.snap +0 -56
- package/src/locales/default/electron.ts +38 -2
- package/src/locales/default/plugin.ts +14 -7
- package/src/server/modules/ElectronIPCClient/index.ts +36 -0
- package/src/server/routers/lambda/session.ts +2 -6
- package/src/server/routers/tools/mcp.ts +6 -0
- package/src/server/services/file/impls/index.ts +9 -1
- package/src/server/services/file/impls/local.test.ts +299 -0
- package/src/server/services/file/impls/local.ts +183 -0
- package/src/server/services/mcp/index.ts +19 -0
- package/src/services/aiModel/index.ts +5 -1
- package/src/services/aiProvider/index.ts +5 -1
- package/src/services/electron/autoUpdate.ts +4 -0
- package/src/services/file/index.ts +5 -1
- package/src/services/mcp.ts +13 -2
- package/src/services/message/index.ts +5 -1
- package/src/services/plugin/index.ts +5 -1
- package/src/services/session/index.ts +5 -1
- package/src/services/tableViewer/desktop.ts +15 -0
- package/src/services/tableViewer/index.ts +4 -1
- package/src/services/thread/index.ts +5 -1
- package/src/services/topic/index.ts +5 -1
- package/src/services/user/index.ts +5 -1
- package/src/store/electron/actions/app.ts +59 -0
- package/src/store/electron/actions/sync.ts +5 -1
- package/src/store/electron/initialState.ts +3 -1
- package/src/store/electron/store.ts +6 -1
- package/src/store/tool/slices/customPlugin/action.ts +16 -4
- package/src/utils/client/GlobalAgentContextManager.ts +85 -0
- package/src/utils/promptTemplate.test.ts +78 -0
- package/src/utils/promptTemplate.ts +17 -0
@@ -0,0 +1,260 @@
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
2
|
+
|
3
|
+
exports[`createRemarkSelfClosingTagPlugin > should handle tag within a list item and generate snapshot 1`] = `
|
4
|
+
{
|
5
|
+
"children": [
|
6
|
+
{
|
7
|
+
"children": [
|
8
|
+
{
|
9
|
+
"checked": null,
|
10
|
+
"children": [
|
11
|
+
{
|
12
|
+
"children": [
|
13
|
+
{
|
14
|
+
"position": {
|
15
|
+
"end": {
|
16
|
+
"column": 26,
|
17
|
+
"line": 2,
|
18
|
+
"offset": 26,
|
19
|
+
},
|
20
|
+
"start": {
|
21
|
+
"column": 4,
|
22
|
+
"line": 2,
|
23
|
+
"offset": 4,
|
24
|
+
},
|
25
|
+
},
|
26
|
+
"type": "text",
|
27
|
+
"value": "文件名:飞机全书 一部明晰可见的历史.pdf",
|
28
|
+
},
|
29
|
+
],
|
30
|
+
"position": {
|
31
|
+
"end": {
|
32
|
+
"column": 26,
|
33
|
+
"line": 2,
|
34
|
+
"offset": 26,
|
35
|
+
},
|
36
|
+
"start": {
|
37
|
+
"column": 4,
|
38
|
+
"line": 2,
|
39
|
+
"offset": 4,
|
40
|
+
},
|
41
|
+
},
|
42
|
+
"type": "paragraph",
|
43
|
+
},
|
44
|
+
{
|
45
|
+
"children": [
|
46
|
+
{
|
47
|
+
"checked": null,
|
48
|
+
"children": [
|
49
|
+
{
|
50
|
+
"children": [
|
51
|
+
{
|
52
|
+
"position": {
|
53
|
+
"end": {
|
54
|
+
"column": 10,
|
55
|
+
"line": 3,
|
56
|
+
"offset": 36,
|
57
|
+
},
|
58
|
+
"start": {
|
59
|
+
"column": 6,
|
60
|
+
"line": 3,
|
61
|
+
"offset": 32,
|
62
|
+
},
|
63
|
+
},
|
64
|
+
"type": "text",
|
65
|
+
"value": "路径1:",
|
66
|
+
},
|
67
|
+
{
|
68
|
+
"data": {
|
69
|
+
"hName": "localFile",
|
70
|
+
"hProperties": {
|
71
|
+
"name": "飞机全书 一部明晰可见的历史.pdf",
|
72
|
+
"path": "/Users/abc/Zotero/storage/ASBMAURK/飞机全书 一部明晰可见的历史.pdf",
|
73
|
+
},
|
74
|
+
},
|
75
|
+
"type": "localFile",
|
76
|
+
},
|
77
|
+
],
|
78
|
+
"position": {
|
79
|
+
"end": {
|
80
|
+
"column": 110,
|
81
|
+
"line": 3,
|
82
|
+
"offset": 136,
|
83
|
+
},
|
84
|
+
"start": {
|
85
|
+
"column": 6,
|
86
|
+
"line": 3,
|
87
|
+
"offset": 32,
|
88
|
+
},
|
89
|
+
},
|
90
|
+
"type": "paragraph",
|
91
|
+
},
|
92
|
+
],
|
93
|
+
"position": {
|
94
|
+
"end": {
|
95
|
+
"column": 110,
|
96
|
+
"line": 3,
|
97
|
+
"offset": 136,
|
98
|
+
},
|
99
|
+
"start": {
|
100
|
+
"column": 4,
|
101
|
+
"line": 3,
|
102
|
+
"offset": 30,
|
103
|
+
},
|
104
|
+
},
|
105
|
+
"spread": false,
|
106
|
+
"type": "listItem",
|
107
|
+
},
|
108
|
+
{
|
109
|
+
"checked": null,
|
110
|
+
"children": [
|
111
|
+
{
|
112
|
+
"children": [
|
113
|
+
{
|
114
|
+
"position": {
|
115
|
+
"end": {
|
116
|
+
"column": 56,
|
117
|
+
"line": 4,
|
118
|
+
"offset": 192,
|
119
|
+
},
|
120
|
+
"start": {
|
121
|
+
"column": 6,
|
122
|
+
"line": 4,
|
123
|
+
"offset": 142,
|
124
|
+
},
|
125
|
+
},
|
126
|
+
"type": "text",
|
127
|
+
"value": "路径2:/Users/abc/Downloads/测试 PDF/飞机全书 一部明晰可见的历史.pdf",
|
128
|
+
},
|
129
|
+
],
|
130
|
+
"position": {
|
131
|
+
"end": {
|
132
|
+
"column": 56,
|
133
|
+
"line": 4,
|
134
|
+
"offset": 192,
|
135
|
+
},
|
136
|
+
"start": {
|
137
|
+
"column": 6,
|
138
|
+
"line": 4,
|
139
|
+
"offset": 142,
|
140
|
+
},
|
141
|
+
},
|
142
|
+
"type": "paragraph",
|
143
|
+
},
|
144
|
+
],
|
145
|
+
"position": {
|
146
|
+
"end": {
|
147
|
+
"column": 56,
|
148
|
+
"line": 4,
|
149
|
+
"offset": 192,
|
150
|
+
},
|
151
|
+
"start": {
|
152
|
+
"column": 4,
|
153
|
+
"line": 4,
|
154
|
+
"offset": 140,
|
155
|
+
},
|
156
|
+
},
|
157
|
+
"spread": false,
|
158
|
+
"type": "listItem",
|
159
|
+
},
|
160
|
+
],
|
161
|
+
"ordered": false,
|
162
|
+
"position": {
|
163
|
+
"end": {
|
164
|
+
"column": 56,
|
165
|
+
"line": 4,
|
166
|
+
"offset": 192,
|
167
|
+
},
|
168
|
+
"start": {
|
169
|
+
"column": 4,
|
170
|
+
"line": 3,
|
171
|
+
"offset": 30,
|
172
|
+
},
|
173
|
+
},
|
174
|
+
"spread": false,
|
175
|
+
"start": null,
|
176
|
+
"type": "list",
|
177
|
+
},
|
178
|
+
],
|
179
|
+
"position": {
|
180
|
+
"end": {
|
181
|
+
"column": 56,
|
182
|
+
"line": 4,
|
183
|
+
"offset": 192,
|
184
|
+
},
|
185
|
+
"start": {
|
186
|
+
"column": 1,
|
187
|
+
"line": 2,
|
188
|
+
"offset": 1,
|
189
|
+
},
|
190
|
+
},
|
191
|
+
"spread": false,
|
192
|
+
"type": "listItem",
|
193
|
+
},
|
194
|
+
],
|
195
|
+
"ordered": true,
|
196
|
+
"position": {
|
197
|
+
"end": {
|
198
|
+
"column": 56,
|
199
|
+
"line": 4,
|
200
|
+
"offset": 192,
|
201
|
+
},
|
202
|
+
"start": {
|
203
|
+
"column": 1,
|
204
|
+
"line": 2,
|
205
|
+
"offset": 1,
|
206
|
+
},
|
207
|
+
},
|
208
|
+
"spread": false,
|
209
|
+
"start": 1,
|
210
|
+
"type": "list",
|
211
|
+
},
|
212
|
+
{
|
213
|
+
"children": [
|
214
|
+
{
|
215
|
+
"position": {
|
216
|
+
"end": {
|
217
|
+
"column": 75,
|
218
|
+
"line": 6,
|
219
|
+
"offset": 268,
|
220
|
+
},
|
221
|
+
"start": {
|
222
|
+
"column": 1,
|
223
|
+
"line": 6,
|
224
|
+
"offset": 194,
|
225
|
+
},
|
226
|
+
},
|
227
|
+
"type": "text",
|
228
|
+
"value": "这是一本 PDF 格式的书,并且在你的 Zotero 和 Downloads 文件夹里都能找到。如果需要进一步操作,比如阅读或者提取内容,可以告诉我",
|
229
|
+
},
|
230
|
+
],
|
231
|
+
"position": {
|
232
|
+
"end": {
|
233
|
+
"column": 75,
|
234
|
+
"line": 6,
|
235
|
+
"offset": 268,
|
236
|
+
},
|
237
|
+
"start": {
|
238
|
+
"column": 1,
|
239
|
+
"line": 6,
|
240
|
+
"offset": 194,
|
241
|
+
},
|
242
|
+
},
|
243
|
+
"type": "paragraph",
|
244
|
+
},
|
245
|
+
],
|
246
|
+
"position": {
|
247
|
+
"end": {
|
248
|
+
"column": 1,
|
249
|
+
"line": 7,
|
250
|
+
"offset": 269,
|
251
|
+
},
|
252
|
+
"start": {
|
253
|
+
"column": 1,
|
254
|
+
"line": 1,
|
255
|
+
"offset": 0,
|
256
|
+
},
|
257
|
+
},
|
258
|
+
"type": "root",
|
259
|
+
}
|
260
|
+
`;
|
@@ -0,0 +1,204 @@
|
|
1
|
+
import remarkParse from 'remark-parse';
|
2
|
+
import { unified } from 'unified';
|
3
|
+
import { describe, expect, it } from 'vitest';
|
4
|
+
|
5
|
+
import { createRemarkSelfClosingTagPlugin } from './createRemarkSelfClosingTagPlugin';
|
6
|
+
|
7
|
+
// Helper function to process markdown and get the resulting tree
|
8
|
+
const processMarkdown = (markdown: string, tagName: string) => {
|
9
|
+
const processor = unified().use(remarkParse).use(createRemarkSelfClosingTagPlugin(tagName));
|
10
|
+
|
11
|
+
const tree = processor.parse(markdown);
|
12
|
+
return processor.runSync(tree);
|
13
|
+
};
|
14
|
+
|
15
|
+
describe('createRemarkSelfClosingTagPlugin', () => {
|
16
|
+
const tagName = 'localFile';
|
17
|
+
|
18
|
+
it('should replace a single self-closing tag (parsed as HTML) with a custom node', () => {
|
19
|
+
const markdown = `<${tagName} name="test.txt" path="/path/to/test.txt" />`;
|
20
|
+
const tree = processMarkdown(markdown, tagName);
|
21
|
+
|
22
|
+
expect(tree.children).toHaveLength(1);
|
23
|
+
const node = tree.children[0];
|
24
|
+
expect(node.type).toBe(tagName);
|
25
|
+
expect(node.data?.hProperties).toEqual({
|
26
|
+
name: 'test.txt',
|
27
|
+
path: '/path/to/test.txt',
|
28
|
+
});
|
29
|
+
expect(node.data?.hName).toBe(tagName);
|
30
|
+
});
|
31
|
+
|
32
|
+
it('should handle boolean attributes in a standalone tag', () => {
|
33
|
+
const markdown = `<${tagName} name="docs" path="/path/to/docs" isDirectory />`;
|
34
|
+
const tree = processMarkdown(markdown, tagName);
|
35
|
+
|
36
|
+
expect(tree.children).toHaveLength(1);
|
37
|
+
const node = tree.children[0];
|
38
|
+
expect(node.type).toBe(tagName);
|
39
|
+
expect(node.data?.hProperties).toEqual({
|
40
|
+
name: 'docs',
|
41
|
+
path: '/path/to/docs',
|
42
|
+
isDirectory: true,
|
43
|
+
});
|
44
|
+
expect(node.data?.hName).toBe(tagName);
|
45
|
+
});
|
46
|
+
|
47
|
+
it('should handle tags surrounded by text (parsed within paragraph)', () => {
|
48
|
+
const markdown = `Here is a file: <${tagName} name="report.pdf" path="report.pdf" /> Please review.`;
|
49
|
+
const tree = processMarkdown(markdown, tagName);
|
50
|
+
|
51
|
+
expect(tree.children).toHaveLength(1);
|
52
|
+
expect(tree.children[0].type).toBe('paragraph');
|
53
|
+
|
54
|
+
const paragraphChildren = tree.children[0].children;
|
55
|
+
expect(paragraphChildren).toHaveLength(3);
|
56
|
+
|
57
|
+
expect(paragraphChildren[0].type).toBe('text');
|
58
|
+
expect(paragraphChildren[0].value).toBe('Here is a file: ');
|
59
|
+
|
60
|
+
const tagNode = paragraphChildren[1];
|
61
|
+
expect(tagNode.type).toBe(tagName);
|
62
|
+
expect(tagNode.data?.hProperties).toEqual({
|
63
|
+
name: 'report.pdf',
|
64
|
+
path: 'report.pdf',
|
65
|
+
});
|
66
|
+
expect(tagNode.data?.hName).toBe(tagName);
|
67
|
+
|
68
|
+
expect(paragraphChildren[2].type).toBe('text');
|
69
|
+
expect(paragraphChildren[2].value).toBe(' Please review.');
|
70
|
+
});
|
71
|
+
|
72
|
+
it('should handle multiple tags within the same text block', () => {
|
73
|
+
const markdown = `File 1: <${tagName} name="a.txt" path="a" /> and File 2: <${tagName} name="b.txt" path="b" isDirectory />`;
|
74
|
+
const tree = processMarkdown(markdown, tagName);
|
75
|
+
|
76
|
+
expect(tree.children).toHaveLength(1);
|
77
|
+
expect(tree.children[0].type).toBe('paragraph');
|
78
|
+
|
79
|
+
const paragraphChildren = tree.children[0].children;
|
80
|
+
expect(paragraphChildren).toHaveLength(4);
|
81
|
+
|
82
|
+
expect(paragraphChildren[0].value).toBe('File 1: ');
|
83
|
+
|
84
|
+
const tagNode1 = paragraphChildren[1];
|
85
|
+
expect(tagNode1.type).toBe(tagName);
|
86
|
+
expect(tagNode1.data?.hProperties).toEqual({ name: 'a.txt', path: 'a' });
|
87
|
+
expect(tagNode1.data?.hName).toBe(tagName);
|
88
|
+
|
89
|
+
expect(paragraphChildren[2].value).toBe(' and File 2: ');
|
90
|
+
|
91
|
+
const tagNode2 = paragraphChildren[3];
|
92
|
+
expect(tagNode2.type).toBe(tagName);
|
93
|
+
expect(tagNode2.data?.hProperties).toEqual({
|
94
|
+
name: 'b.txt',
|
95
|
+
path: 'b',
|
96
|
+
isDirectory: true,
|
97
|
+
});
|
98
|
+
expect(tagNode2.data?.hName).toBe(tagName);
|
99
|
+
});
|
100
|
+
|
101
|
+
it('should handle standalone tags with no attributes', () => {
|
102
|
+
const markdown = `<${tagName} />`;
|
103
|
+
const tree = processMarkdown(markdown, tagName);
|
104
|
+
|
105
|
+
expect(tree.children).toHaveLength(1);
|
106
|
+
const node = tree.children[0];
|
107
|
+
expect(node.type).toBe(tagName);
|
108
|
+
expect(node.data?.hProperties).toEqual({});
|
109
|
+
expect(node.data?.hName).toBe(tagName);
|
110
|
+
});
|
111
|
+
|
112
|
+
it('should ignore tags with different names (parsed within a paragraph)', () => {
|
113
|
+
const markdown = `<other_tag name="ignore_me" /> <${tagName} name="process_me" path="/p" />`;
|
114
|
+
const tree = processMarkdown(markdown, tagName);
|
115
|
+
|
116
|
+
expect(tree.children).toHaveLength(1);
|
117
|
+
expect(tree.children[0].type).toBe('paragraph');
|
118
|
+
|
119
|
+
const paragraphChildren = tree.children[0].children;
|
120
|
+
expect(paragraphChildren).toHaveLength(2);
|
121
|
+
|
122
|
+
expect(paragraphChildren[0].type).toBe('text');
|
123
|
+
expect(paragraphChildren[0].value).toBe('<other_tag name="ignore_me" /> ');
|
124
|
+
|
125
|
+
const tagNode = paragraphChildren[1];
|
126
|
+
expect(tagNode.type).toBe(tagName);
|
127
|
+
expect(tagNode.data?.hProperties).toEqual({ name: 'process_me', path: '/p' });
|
128
|
+
expect(tagNode.data?.hName).toBe(tagName);
|
129
|
+
});
|
130
|
+
|
131
|
+
it('should not modify markdown without the target tag', () => {
|
132
|
+
const markdown = 'This is just regular text.';
|
133
|
+
const tree = processMarkdown(markdown, tagName);
|
134
|
+
const originalTree = unified().use(remarkParse).parse(markdown);
|
135
|
+
|
136
|
+
expect(tree).toEqual(originalTree);
|
137
|
+
});
|
138
|
+
|
139
|
+
it('should work with a different tag name provided to the creator', () => {
|
140
|
+
const otherTagName = 'customData';
|
141
|
+
const markdown = `Data: <${otherTagName} id="123" value="abc" active />`;
|
142
|
+
const tree = processMarkdown(markdown, otherTagName);
|
143
|
+
|
144
|
+
expect(tree.children).toHaveLength(1);
|
145
|
+
expect(tree.children[0].type).toBe('paragraph');
|
146
|
+
const paragraphChildren = tree.children[0].children;
|
147
|
+
expect(paragraphChildren).toHaveLength(2);
|
148
|
+
|
149
|
+
expect(paragraphChildren[0].value).toBe('Data: ');
|
150
|
+
|
151
|
+
const tagNode = paragraphChildren[1];
|
152
|
+
expect(tagNode.type).toBe(otherTagName);
|
153
|
+
expect(tagNode.data?.hProperties).toEqual({ id: '123', value: 'abc', active: true });
|
154
|
+
expect(tagNode.data?.hName).toBe(otherTagName);
|
155
|
+
});
|
156
|
+
|
157
|
+
it('should handle tag at the beginning of the text', () => {
|
158
|
+
const markdown = `<${tagName} name="start.log" path="/logs/start.log" /> Log started.`;
|
159
|
+
const tree = processMarkdown(markdown, tagName);
|
160
|
+
|
161
|
+
expect(tree.children).toHaveLength(1);
|
162
|
+
expect(tree.children[0].type).toBe('paragraph');
|
163
|
+
const paragraphChildren = tree.children[0].children;
|
164
|
+
expect(paragraphChildren).toHaveLength(2);
|
165
|
+
|
166
|
+
const tagNode = paragraphChildren[0];
|
167
|
+
expect(tagNode.type).toBe(tagName);
|
168
|
+
expect(tagNode.data?.hProperties).toEqual({ name: 'start.log', path: '/logs/start.log' });
|
169
|
+
expect(tagNode.data?.hName).toBe(tagName);
|
170
|
+
|
171
|
+
expect(paragraphChildren[1].type).toBe('text');
|
172
|
+
expect(paragraphChildren[1].value).toBe(' Log started.');
|
173
|
+
});
|
174
|
+
|
175
|
+
it('should handle tag at the end of the text', () => {
|
176
|
+
const markdown = `Log ended: <${tagName} name="end.log" path="/logs/end.log" />`;
|
177
|
+
const tree = processMarkdown(markdown, tagName);
|
178
|
+
|
179
|
+
expect(tree.children).toHaveLength(1);
|
180
|
+
expect(tree.children[0].type).toBe('paragraph');
|
181
|
+
const paragraphChildren = tree.children[0].children;
|
182
|
+
expect(paragraphChildren).toHaveLength(2);
|
183
|
+
|
184
|
+
expect(paragraphChildren[0].type).toBe('text');
|
185
|
+
expect(paragraphChildren[0].value).toBe('Log ended: ');
|
186
|
+
|
187
|
+
const tagNode = paragraphChildren[1];
|
188
|
+
expect(tagNode.type).toBe(tagName);
|
189
|
+
expect(tagNode.data?.hProperties).toEqual({ name: 'end.log', path: '/logs/end.log' });
|
190
|
+
expect(tagNode.data?.hName).toBe(tagName);
|
191
|
+
});
|
192
|
+
|
193
|
+
it('should handle tag within a list item and generate snapshot', () => {
|
194
|
+
const markdown = `
|
195
|
+
1. 文件名:飞机全书 一部明晰可见的历史.pdf
|
196
|
+
- 路径1:<${tagName} name="飞机全书 一部明晰可见的历史.pdf" path="/Users/abc/Zotero/storage/ASBMAURK/飞机全书 一部明晰可见的历史.pdf" />
|
197
|
+
- 路径2:/Users/abc/Downloads/测试 PDF/飞机全书 一部明晰可见的历史.pdf
|
198
|
+
|
199
|
+
这是一本 PDF 格式的书,并且在你的 Zotero 和 Downloads 文件夹里都能找到。如果需要进一步操作,比如阅读或者提取内容,可以告诉我
|
200
|
+
`;
|
201
|
+
const tree = processMarkdown(markdown, tagName);
|
202
|
+
expect(tree).toMatchSnapshot();
|
203
|
+
});
|
204
|
+
});
|
@@ -0,0 +1,133 @@
|
|
1
|
+
import debug from 'debug';
|
2
|
+
import type { Plugin } from 'unified';
|
3
|
+
import { SKIP, visit } from 'unist-util-visit';
|
4
|
+
|
5
|
+
// 创建 debugger 实例
|
6
|
+
const log = debug('lobe-markdown:remark-plugin:self-closing');
|
7
|
+
|
8
|
+
// Regex to parse attributes from a string
|
9
|
+
// Handles keys, keys with quoted values (double or single), and boolean keys
|
10
|
+
const attributeRegex = /([\w-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
11
|
+
|
12
|
+
// Helper function to parse the attribute string into an object
|
13
|
+
const parseAttributes = (attributeString: string): Record<string, string | boolean> => {
|
14
|
+
const attributes: Record<string, string | boolean> = {};
|
15
|
+
let match;
|
16
|
+
while ((match = attributeRegex.exec(attributeString)) !== null) {
|
17
|
+
const [, key, valueDouble, valueSingle, valueUnquoted] = match;
|
18
|
+
// If any value group is captured, use it, otherwise treat as boolean true
|
19
|
+
attributes[key] = valueDouble ?? valueSingle ?? valueUnquoted ?? true;
|
20
|
+
}
|
21
|
+
return attributes;
|
22
|
+
};
|
23
|
+
|
24
|
+
export const createRemarkSelfClosingTagPlugin =
|
25
|
+
(tagName: string): Plugin<[], any> =>
|
26
|
+
() => {
|
27
|
+
// Regex for the specific tag, ensure it matches the entire string for HTML check
|
28
|
+
const exactTagRegex = new RegExp(`^<${tagName}(\\s+[^>]*?)?\\s*\\/>$`);
|
29
|
+
// Regex for finding tags within text
|
30
|
+
const textTagRegex = new RegExp(`<${tagName}(\\s+[^>]*?)?\\s*\\/>`, 'g');
|
31
|
+
|
32
|
+
return (tree) => {
|
33
|
+
// --- DEBUG LOG START (Before Visit) ---
|
34
|
+
log('Plugin execution start for tag: %s', tagName);
|
35
|
+
log('Tree: %o', tree);
|
36
|
+
log('Tree type: %s', tree?.type);
|
37
|
+
log('Tree children count: %d', tree?.children?.length);
|
38
|
+
if (!tree || !Array.isArray(tree.children)) {
|
39
|
+
log('ERROR: Invalid Tree Structure Detected Before Visit! %o', tree);
|
40
|
+
} else {
|
41
|
+
const hasUndefinedChild = tree.children.includes(undefined);
|
42
|
+
if (hasUndefinedChild) {
|
43
|
+
log('ERROR: Tree contains undefined children Before Visit!');
|
44
|
+
log(
|
45
|
+
'Children types: %o',
|
46
|
+
tree.children.map((c: any) => c?.type),
|
47
|
+
);
|
48
|
+
}
|
49
|
+
}
|
50
|
+
log('---------------------------------------------------');
|
51
|
+
// --- DEBUG LOG END (Before Visit) ---
|
52
|
+
|
53
|
+
// 1. Visit HTML nodes first for exact matches
|
54
|
+
// @ts-ignore
|
55
|
+
visit(tree, 'html', (node, index: number, parent) => {
|
56
|
+
log('>>> Visiting HTML node: %s', node.value);
|
57
|
+
const match = node.value.match(exactTagRegex);
|
58
|
+
|
59
|
+
if (match && parent && typeof index === 'number') {
|
60
|
+
const [, attributesString] = match;
|
61
|
+
const properties = attributesString ? parseAttributes(attributesString.trim()) : {};
|
62
|
+
|
63
|
+
const newNode = {
|
64
|
+
data: {
|
65
|
+
hName: tagName,
|
66
|
+
hProperties: properties,
|
67
|
+
},
|
68
|
+
type: tagName,
|
69
|
+
};
|
70
|
+
|
71
|
+
log('Replacing HTML node at index %d with %s node: %o', index, tagName, newNode);
|
72
|
+
parent.children.splice(index, 1, newNode);
|
73
|
+
return [SKIP, index + 1]; // Skip the node we just inserted
|
74
|
+
}
|
75
|
+
});
|
76
|
+
|
77
|
+
// 2. Visit Text nodes for inline matches
|
78
|
+
// @ts-ignore
|
79
|
+
visit(tree, 'text', (node: any, index: number, parent) => {
|
80
|
+
log('>>> Visiting Text node: "%s"', node.value);
|
81
|
+
|
82
|
+
if (!parent || typeof index !== 'number' || !node.value?.includes(`<${tagName}`)) {
|
83
|
+
return; // Quick exit if tag isn't possibly present
|
84
|
+
}
|
85
|
+
|
86
|
+
const text = node.value;
|
87
|
+
let lastIndex = 0;
|
88
|
+
const newChildren = [];
|
89
|
+
let match;
|
90
|
+
|
91
|
+
textTagRegex.lastIndex = 0; // Reset regex state
|
92
|
+
|
93
|
+
while ((match = textTagRegex.exec(text)) !== null) {
|
94
|
+
const [fullMatch, attributesString] = match;
|
95
|
+
const matchIndex = match.index;
|
96
|
+
|
97
|
+
// Add text before the match
|
98
|
+
if (matchIndex > lastIndex) {
|
99
|
+
newChildren.push({ type: 'text', value: text.slice(lastIndex, matchIndex) });
|
100
|
+
}
|
101
|
+
|
102
|
+
// Parse attributes and create the new node
|
103
|
+
const properties = attributesString ? parseAttributes(attributesString.trim()) : {};
|
104
|
+
newChildren.push({
|
105
|
+
data: {
|
106
|
+
hName: tagName,
|
107
|
+
hProperties: properties,
|
108
|
+
},
|
109
|
+
type: tagName,
|
110
|
+
});
|
111
|
+
|
112
|
+
lastIndex = matchIndex + fullMatch.length;
|
113
|
+
}
|
114
|
+
|
115
|
+
// If matches were found, replace the original text node
|
116
|
+
if (newChildren.length > 0) {
|
117
|
+
// Add any remaining text after the last match
|
118
|
+
if (lastIndex < text.length) {
|
119
|
+
newChildren.push({ type: 'text', value: text.slice(lastIndex) });
|
120
|
+
}
|
121
|
+
|
122
|
+
// --- DEBUG LOG START (Before Splice - Text Node) ---
|
123
|
+
log('--- Replacing Text Node Content ---');
|
124
|
+
log('Original text node index: %d', index);
|
125
|
+
log('-----------------------------------');
|
126
|
+
// --- DEBUG LOG END (Before Splice - Text Node) ---
|
127
|
+
|
128
|
+
parent.children.splice(index, 1, ...newChildren);
|
129
|
+
return [SKIP, index + newChildren.length]; // Skip new nodes
|
130
|
+
}
|
131
|
+
});
|
132
|
+
};
|
133
|
+
};
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import { Input, InputProps } from 'antd';
|
2
|
+
import { memo } from 'react';
|
3
|
+
|
4
|
+
interface ArgsInputProps extends Omit<InputProps, 'value' | 'onChange'> {
|
5
|
+
onChange?: (value: string[]) => void;
|
6
|
+
value?: string[];
|
7
|
+
}
|
8
|
+
|
9
|
+
const ArgsInput = memo<ArgsInputProps>(({ value, onChange, ...res }) => {
|
10
|
+
return (
|
11
|
+
<Input
|
12
|
+
onChange={(e) => {
|
13
|
+
onChange?.([e.target.value]);
|
14
|
+
}}
|
15
|
+
value={value?.[0]}
|
16
|
+
{...res}
|
17
|
+
/>
|
18
|
+
);
|
19
|
+
});
|
20
|
+
export default ArgsInput;
|