remnote-bridge 0.1.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/dist/cli/commands/connect.d.ts +12 -0
- package/dist/cli/commands/connect.js +124 -0
- package/dist/cli/commands/disconnect.d.ts +11 -0
- package/dist/cli/commands/disconnect.js +100 -0
- package/dist/cli/commands/edit-rem.d.ts +13 -0
- package/dist/cli/commands/edit-rem.js +83 -0
- package/dist/cli/commands/edit-tree.d.ts +14 -0
- package/dist/cli/commands/edit-tree.js +67 -0
- package/dist/cli/commands/health.d.ts +12 -0
- package/dist/cli/commands/health.js +100 -0
- package/dist/cli/commands/install-skill.d.ts +6 -0
- package/dist/cli/commands/install-skill.js +39 -0
- package/dist/cli/commands/read-context.d.ts +20 -0
- package/dist/cli/commands/read-context.js +77 -0
- package/dist/cli/commands/read-globe.d.ts +16 -0
- package/dist/cli/commands/read-globe.js +60 -0
- package/dist/cli/commands/read-rem.d.ts +16 -0
- package/dist/cli/commands/read-rem.js +80 -0
- package/dist/cli/commands/read-tree.d.ts +17 -0
- package/dist/cli/commands/read-tree.js +85 -0
- package/dist/cli/commands/search.d.ts +12 -0
- package/dist/cli/commands/search.js +65 -0
- package/dist/cli/config.d.ts +55 -0
- package/dist/cli/config.js +139 -0
- package/dist/cli/daemon/daemon.d.ts +11 -0
- package/dist/cli/daemon/daemon.js +186 -0
- package/dist/cli/daemon/dev-server.d.ts +26 -0
- package/dist/cli/daemon/dev-server.js +81 -0
- package/dist/cli/daemon/pid.d.ts +34 -0
- package/dist/cli/daemon/pid.js +67 -0
- package/dist/cli/daemon/send-request.d.ts +24 -0
- package/dist/cli/daemon/send-request.js +92 -0
- package/dist/cli/handlers/context-read-handler.d.ts +18 -0
- package/dist/cli/handlers/context-read-handler.js +24 -0
- package/dist/cli/handlers/edit-handler.d.ts +30 -0
- package/dist/cli/handlers/edit-handler.js +133 -0
- package/dist/cli/handlers/globe-read-handler.d.ts +17 -0
- package/dist/cli/handlers/globe-read-handler.js +23 -0
- package/dist/cli/handlers/read-handler.d.ts +16 -0
- package/dist/cli/handlers/read-handler.js +78 -0
- package/dist/cli/handlers/rem-cache.d.ts +19 -0
- package/dist/cli/handlers/rem-cache.js +63 -0
- package/dist/cli/handlers/tree-edit-handler.d.ts +30 -0
- package/dist/cli/handlers/tree-edit-handler.js +188 -0
- package/dist/cli/handlers/tree-parser.d.ts +95 -0
- package/dist/cli/handlers/tree-parser.js +506 -0
- package/dist/cli/handlers/tree-read-handler.d.ts +28 -0
- package/dist/cli/handlers/tree-read-handler.js +53 -0
- package/dist/cli/main.d.ts +7 -0
- package/dist/cli/main.js +300 -0
- package/dist/cli/protocol.d.ts +39 -0
- package/dist/cli/protocol.js +35 -0
- package/dist/cli/server/config-server.d.ts +26 -0
- package/dist/cli/server/config-server.js +363 -0
- package/dist/cli/server/ws-server.d.ts +68 -0
- package/dist/cli/server/ws-server.js +335 -0
- package/dist/cli/utils/output.d.ts +11 -0
- package/dist/cli/utils/output.js +13 -0
- package/dist/mcp/daemon-client.d.ts +31 -0
- package/dist/mcp/daemon-client.js +99 -0
- package/dist/mcp/index.d.ts +7 -0
- package/dist/mcp/index.js +68 -0
- package/dist/mcp/instructions.d.ts +1 -0
- package/dist/mcp/instructions.js +249 -0
- package/dist/mcp/resources/edit-tree-guide.d.ts +1 -0
- package/dist/mcp/resources/edit-tree-guide.js +197 -0
- package/dist/mcp/resources/error-reference.d.ts +1 -0
- package/dist/mcp/resources/error-reference.js +132 -0
- package/dist/mcp/resources/outline-format.d.ts +1 -0
- package/dist/mcp/resources/outline-format.js +104 -0
- package/dist/mcp/resources/rem-object-fields.d.ts +1 -0
- package/dist/mcp/resources/rem-object-fields.js +331 -0
- package/dist/mcp/resources/separator-flashcard.d.ts +1 -0
- package/dist/mcp/resources/separator-flashcard.js +120 -0
- package/dist/mcp/tools/edit-tools.d.ts +5 -0
- package/dist/mcp/tools/edit-tools.js +47 -0
- package/dist/mcp/tools/infra-tools.d.ts +5 -0
- package/dist/mcp/tools/infra-tools.js +43 -0
- package/dist/mcp/tools/read-tools.d.ts +5 -0
- package/dist/mcp/tools/read-tools.js +195 -0
- package/dist/mcp/types.d.ts +12 -0
- package/dist/mcp/types.js +4 -0
- package/docs/instruction/connect.md +158 -0
- package/docs/instruction/disconnect.md +146 -0
- package/docs/instruction/edit-rem.md +509 -0
- package/docs/instruction/edit-tree.md +419 -0
- package/docs/instruction/health.md +159 -0
- package/docs/instruction/overall.md +751 -0
- package/docs/instruction/read-context.md +353 -0
- package/docs/instruction/read-globe.md +206 -0
- package/docs/instruction/read-rem.md +476 -0
- package/docs/instruction/read-tree.md +428 -0
- package/docs/instruction/search.md +196 -0
- package/package.json +41 -0
- package/remnote-plugin/package.json +48 -0
- package/remnote-plugin/postcss.config.js +5 -0
- package/remnote-plugin/public/bridge-icon.svg +8 -0
- package/remnote-plugin/public/manifest.json +22 -0
- package/remnote-plugin/src/bridge/message-router.ts +57 -0
- package/remnote-plugin/src/bridge/websocket-client.ts +245 -0
- package/remnote-plugin/src/index.css +1 -0
- package/remnote-plugin/src/services/breadcrumb.ts +26 -0
- package/remnote-plugin/src/services/create-rem.ts +59 -0
- package/remnote-plugin/src/services/delete-rem.ts +29 -0
- package/remnote-plugin/src/services/index.ts +16 -0
- package/remnote-plugin/src/services/move-rem.ts +39 -0
- package/remnote-plugin/src/services/powerup-filter.ts +31 -0
- package/remnote-plugin/src/services/read-context.ts +368 -0
- package/remnote-plugin/src/services/read-globe.ts +197 -0
- package/remnote-plugin/src/services/read-rem.ts +284 -0
- package/remnote-plugin/src/services/read-tree.ts +222 -0
- package/remnote-plugin/src/services/rem-builder.ts +124 -0
- package/remnote-plugin/src/services/reorder-children.ts +61 -0
- package/remnote-plugin/src/services/search.ts +56 -0
- package/remnote-plugin/src/services/write-rem-fields.ts +254 -0
- package/remnote-plugin/src/settings.ts +12 -0
- package/remnote-plugin/src/style.css +45 -0
- package/remnote-plugin/src/test-scripts/AGENTS.md +46 -0
- package/remnote-plugin/src/test-scripts/test-actions.ts +230 -0
- package/remnote-plugin/src/test-scripts/test-powerup-rendering.ts +722 -0
- package/remnote-plugin/src/test-scripts/test-rem-type-mapping.ts +283 -0
- package/remnote-plugin/src/test-scripts/test-richtext-builder.ts +207 -0
- package/remnote-plugin/src/test-scripts/test-richtext-matrix.ts +332 -0
- package/remnote-plugin/src/test-scripts/test-richtext-remaining.ts +245 -0
- package/remnote-plugin/src/test-scripts/test-rw-fields.ts +399 -0
- package/remnote-plugin/src/types.ts +419 -0
- package/remnote-plugin/src/utils/elision.ts +45 -0
- package/remnote-plugin/src/utils/index.ts +10 -0
- package/remnote-plugin/src/utils/tree-serializer.ts +269 -0
- package/remnote-plugin/src/widgets/bridge_widget.tsx +170 -0
- package/remnote-plugin/src/widgets/index.tsx +82 -0
- package/remnote-plugin/tailwind.config.js +7 -0
- package/remnote-plugin/tsconfig.json +21 -0
- package/remnote-plugin/webpack.config.js +125 -0
- package/skill/SKILL.md +428 -0
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Powerup 渲染机制测试 — 每个 SDK 渲染方法的隐藏副作用探测
|
|
3
|
+
*
|
|
4
|
+
* 目的:搞清楚每个改变 Rem "整体渲染" 的 SDK 方法到底:
|
|
5
|
+
* 1. 是否在 tags 数组中注入了 Powerup Tag 引用?
|
|
6
|
+
* 2. 是否在 children 中生成了隐藏的 Powerup 子 Rem?
|
|
7
|
+
* 3. 是否改变了其他属性(hasPowerup 等)?
|
|
8
|
+
*
|
|
9
|
+
* 设计原则:一个 Rem 只用一种 SDK 方法,避免效果叠加。
|
|
10
|
+
*/
|
|
11
|
+
import type { ReactRNPlugin, PluginRem as Rem } from '@remnote/plugin-sdk';
|
|
12
|
+
|
|
13
|
+
async function delay(ms: number) {
|
|
14
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 单个测试用例的探测结果 */
|
|
18
|
+
interface ProbeResult {
|
|
19
|
+
label: string;
|
|
20
|
+
remId: string;
|
|
21
|
+
// SDK 方法调用前的快照
|
|
22
|
+
before: RemSnapshot;
|
|
23
|
+
// SDK 方法调用后的快照
|
|
24
|
+
after: RemSnapshot;
|
|
25
|
+
// 差异摘要
|
|
26
|
+
diff: DiffSummary;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface RemSnapshot {
|
|
30
|
+
tags: TagInfo[];
|
|
31
|
+
children: ChildInfo[];
|
|
32
|
+
hasPowerup: PowerupCheck[];
|
|
33
|
+
// 其他可能相关的属性
|
|
34
|
+
type: number | string | null;
|
|
35
|
+
isDocument: boolean;
|
|
36
|
+
fontSize: string | null;
|
|
37
|
+
highlightColor: string | null;
|
|
38
|
+
isTodo: boolean;
|
|
39
|
+
todoStatus: string | null;
|
|
40
|
+
isCode: boolean;
|
|
41
|
+
isQuote: boolean;
|
|
42
|
+
isListItem: boolean;
|
|
43
|
+
isCardItem: boolean;
|
|
44
|
+
isSlot: boolean;
|
|
45
|
+
isProperty: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface TagInfo {
|
|
49
|
+
id: string;
|
|
50
|
+
text: string;
|
|
51
|
+
isPowerup: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface ChildInfo {
|
|
55
|
+
id: string;
|
|
56
|
+
text: string;
|
|
57
|
+
backText: string | null;
|
|
58
|
+
type: number | string | null;
|
|
59
|
+
isPowerup: boolean;
|
|
60
|
+
isPowerupProperty: boolean;
|
|
61
|
+
children: ChildInfo[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface PowerupCheck {
|
|
65
|
+
code: string;
|
|
66
|
+
name: string;
|
|
67
|
+
hasPowerup: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface DiffSummary {
|
|
71
|
+
tagsAdded: TagInfo[];
|
|
72
|
+
tagsRemoved: TagInfo[];
|
|
73
|
+
childrenAdded: ChildInfo[];
|
|
74
|
+
childrenRemoved: ChildInfo[];
|
|
75
|
+
powerupsChanged: PowerupCheck[];
|
|
76
|
+
fieldsChanged: string[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 需要探测的内置 Powerup codes(只检测与渲染相关的)
|
|
80
|
+
const RENDERING_POWERUP_CODES: Array<{ code: string; name: string }> = [
|
|
81
|
+
{ code: 'r', name: 'Header' },
|
|
82
|
+
{ code: 'h', name: 'Highlight' },
|
|
83
|
+
{ code: 'cd', name: 'Code' },
|
|
84
|
+
{ code: 'qt', name: 'Quote' },
|
|
85
|
+
{ code: 'i', name: 'List' },
|
|
86
|
+
{ code: 't', name: 'Todo' },
|
|
87
|
+
{ code: 'w', name: 'MultiLineCard' },
|
|
88
|
+
{ code: 'o', name: 'Document' },
|
|
89
|
+
{ code: 'e', name: 'EditLater' },
|
|
90
|
+
{ code: 'dv', name: 'Divider' },
|
|
91
|
+
{ code: 'u', name: 'DisableCards' },
|
|
92
|
+
{ code: 'y', name: 'Slot' },
|
|
93
|
+
{ code: 'l', name: 'Aliases' },
|
|
94
|
+
{ code: 'b', name: 'Link' },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
/** 拍摄一个 Rem 的完整快照 */
|
|
98
|
+
async function takeSnapshot(
|
|
99
|
+
plugin: ReactRNPlugin,
|
|
100
|
+
rem: Rem,
|
|
101
|
+
): Promise<RemSnapshot> {
|
|
102
|
+
// 获取 tags
|
|
103
|
+
const tagRems = await rem.getTagRems();
|
|
104
|
+
const tags: TagInfo[] = [];
|
|
105
|
+
for (const t of tagRems) {
|
|
106
|
+
const text = t.text ? await plugin.richText.toString(t.text) : '';
|
|
107
|
+
const isPowerup = await t.isPowerup();
|
|
108
|
+
tags.push({ id: t._id, text, isPowerup });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 获取 children(递归一层即可)
|
|
112
|
+
const childRems = await rem.getChildrenRem();
|
|
113
|
+
const children: ChildInfo[] = [];
|
|
114
|
+
for (const c of childRems) {
|
|
115
|
+
children.push(await serializeChild(plugin, c, 0));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 检测各 Powerup 状态
|
|
119
|
+
const hasPowerup: PowerupCheck[] = [];
|
|
120
|
+
for (const { code, name } of RENDERING_POWERUP_CODES) {
|
|
121
|
+
try {
|
|
122
|
+
const has = await rem.hasPowerup(code);
|
|
123
|
+
hasPowerup.push({ code, name, hasPowerup: has });
|
|
124
|
+
} catch {
|
|
125
|
+
hasPowerup.push({ code, name, hasPowerup: false });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 获取渲染相关字段
|
|
130
|
+
const [
|
|
131
|
+
remType,
|
|
132
|
+
isDocument,
|
|
133
|
+
fontSize,
|
|
134
|
+
highlightColor,
|
|
135
|
+
isTodo,
|
|
136
|
+
todoStatus,
|
|
137
|
+
isCode,
|
|
138
|
+
isQuote,
|
|
139
|
+
isListItem,
|
|
140
|
+
isCardItem,
|
|
141
|
+
isSlot,
|
|
142
|
+
isProperty,
|
|
143
|
+
] = await Promise.all([
|
|
144
|
+
rem.getType(),
|
|
145
|
+
rem.isDocument(),
|
|
146
|
+
rem.getFontSize(),
|
|
147
|
+
rem.getHighlightColor(),
|
|
148
|
+
rem.isTodo(),
|
|
149
|
+
rem.getTodoStatus(),
|
|
150
|
+
rem.isCode(),
|
|
151
|
+
rem.isQuote(),
|
|
152
|
+
rem.isListItem(),
|
|
153
|
+
rem.isCardItem(),
|
|
154
|
+
rem.isSlot(),
|
|
155
|
+
rem.isProperty(),
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
tags,
|
|
160
|
+
children,
|
|
161
|
+
hasPowerup,
|
|
162
|
+
type: remType,
|
|
163
|
+
isDocument,
|
|
164
|
+
fontSize: (fontSize as string) ?? null,
|
|
165
|
+
highlightColor: (highlightColor as string) ?? null,
|
|
166
|
+
isTodo,
|
|
167
|
+
todoStatus: (todoStatus as string) ?? null,
|
|
168
|
+
isCode,
|
|
169
|
+
isQuote,
|
|
170
|
+
isListItem,
|
|
171
|
+
isCardItem,
|
|
172
|
+
isSlot,
|
|
173
|
+
isProperty,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** 递归序列化子 Rem(最深 2 层) */
|
|
178
|
+
async function serializeChild(
|
|
179
|
+
plugin: ReactRNPlugin,
|
|
180
|
+
rem: Rem,
|
|
181
|
+
depth: number,
|
|
182
|
+
): Promise<ChildInfo> {
|
|
183
|
+
const text = rem.text ? await plugin.richText.toString(rem.text) : '';
|
|
184
|
+
const backText = rem.backText ? await plugin.richText.toString(rem.backText) : null;
|
|
185
|
+
const isPowerup = await rem.isPowerup();
|
|
186
|
+
const isPowerupProperty = await rem.isPowerupProperty();
|
|
187
|
+
|
|
188
|
+
const children: ChildInfo[] = [];
|
|
189
|
+
if (depth < 2) {
|
|
190
|
+
const childRems = await rem.getChildrenRem();
|
|
191
|
+
for (const c of childRems) {
|
|
192
|
+
children.push(await serializeChild(plugin, c, depth + 1));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
id: rem._id,
|
|
198
|
+
text,
|
|
199
|
+
backText,
|
|
200
|
+
type: rem.type,
|
|
201
|
+
isPowerup,
|
|
202
|
+
isPowerupProperty,
|
|
203
|
+
children,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** 计算前后快照的差异 */
|
|
208
|
+
function computeDiff(before: RemSnapshot, after: RemSnapshot): DiffSummary {
|
|
209
|
+
const beforeTagIds = new Set(before.tags.map(t => t.id));
|
|
210
|
+
const afterTagIds = new Set(after.tags.map(t => t.id));
|
|
211
|
+
|
|
212
|
+
const tagsAdded = after.tags.filter(t => !beforeTagIds.has(t.id));
|
|
213
|
+
const tagsRemoved = before.tags.filter(t => !afterTagIds.has(t.id));
|
|
214
|
+
|
|
215
|
+
const beforeChildIds = new Set(before.children.map(c => c.id));
|
|
216
|
+
const afterChildIds = new Set(after.children.map(c => c.id));
|
|
217
|
+
|
|
218
|
+
const childrenAdded = after.children.filter(c => !beforeChildIds.has(c.id));
|
|
219
|
+
const childrenRemoved = before.children.filter(c => !afterChildIds.has(c.id));
|
|
220
|
+
|
|
221
|
+
const powerupsChanged = after.hasPowerup.filter(ap => {
|
|
222
|
+
const bp = before.hasPowerup.find(b => b.code === ap.code);
|
|
223
|
+
return !bp || bp.hasPowerup !== ap.hasPowerup;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const fieldsChanged: string[] = [];
|
|
227
|
+
const fieldKeys: (keyof RemSnapshot)[] = [
|
|
228
|
+
'type', 'isDocument', 'fontSize', 'highlightColor',
|
|
229
|
+
'isTodo', 'todoStatus', 'isCode', 'isQuote',
|
|
230
|
+
'isListItem', 'isCardItem', 'isSlot', 'isProperty',
|
|
231
|
+
];
|
|
232
|
+
for (const key of fieldKeys) {
|
|
233
|
+
if (JSON.stringify(before[key]) !== JSON.stringify(after[key])) {
|
|
234
|
+
fieldsChanged.push(`${key}: ${JSON.stringify(before[key])} -> ${JSON.stringify(after[key])}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { tagsAdded, tagsRemoved, childrenAdded, childrenRemoved, powerupsChanged, fieldsChanged };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** 格式化报告 */
|
|
242
|
+
function formatReport(results: ProbeResult[]): string {
|
|
243
|
+
const lines: string[] = [];
|
|
244
|
+
lines.push('='.repeat(80));
|
|
245
|
+
lines.push('Powerup 渲染机制探测报告');
|
|
246
|
+
lines.push(`测试时间: ${new Date().toISOString()}`);
|
|
247
|
+
lines.push(`测试数量: ${results.length}`);
|
|
248
|
+
lines.push('='.repeat(80));
|
|
249
|
+
lines.push('');
|
|
250
|
+
|
|
251
|
+
for (const r of results) {
|
|
252
|
+
lines.push('-'.repeat(60));
|
|
253
|
+
lines.push(`[${r.label}] remId: ${r.remId}`);
|
|
254
|
+
lines.push('-'.repeat(60));
|
|
255
|
+
|
|
256
|
+
// Tags 变化
|
|
257
|
+
if (r.diff.tagsAdded.length > 0) {
|
|
258
|
+
lines.push(' Tags 新增:');
|
|
259
|
+
for (const t of r.diff.tagsAdded) {
|
|
260
|
+
lines.push(` + "${t.text}" (${t.id}) isPowerup=${t.isPowerup}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (r.diff.tagsRemoved.length > 0) {
|
|
264
|
+
lines.push(' Tags 移除:');
|
|
265
|
+
for (const t of r.diff.tagsRemoved) {
|
|
266
|
+
lines.push(` - "${t.text}" (${t.id}) isPowerup=${t.isPowerup}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Children 变化
|
|
271
|
+
if (r.diff.childrenAdded.length > 0) {
|
|
272
|
+
lines.push(' Children 新增:');
|
|
273
|
+
for (const c of r.diff.childrenAdded) {
|
|
274
|
+
const back = c.backText ? ` ;; "${c.backText}"` : '';
|
|
275
|
+
lines.push(` + "${c.text}"${back} (${c.id}) type=${c.type} isPowerup=${c.isPowerup} isPowerupProp=${c.isPowerupProperty}`);
|
|
276
|
+
for (const gc of c.children) {
|
|
277
|
+
const gcBack = gc.backText ? ` ;; "${gc.backText}"` : '';
|
|
278
|
+
lines.push(` + "${gc.text}"${gcBack} (${gc.id}) type=${gc.type} isPowerup=${gc.isPowerup} isPowerupProp=${gc.isPowerupProperty}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (r.diff.childrenRemoved.length > 0) {
|
|
283
|
+
lines.push(' Children 移除:');
|
|
284
|
+
for (const c of r.diff.childrenRemoved) {
|
|
285
|
+
lines.push(` - "${c.text}" (${c.id})`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Powerup 状态变化
|
|
290
|
+
if (r.diff.powerupsChanged.length > 0) {
|
|
291
|
+
lines.push(' Powerup 状态变化:');
|
|
292
|
+
for (const p of r.diff.powerupsChanged) {
|
|
293
|
+
lines.push(` ${p.name} (${p.code}): ${p.hasPowerup}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 字段变化
|
|
298
|
+
if (r.diff.fieldsChanged.length > 0) {
|
|
299
|
+
lines.push(' 字段变化:');
|
|
300
|
+
for (const f of r.diff.fieldsChanged) {
|
|
301
|
+
lines.push(` ${f}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 无变化
|
|
306
|
+
if (
|
|
307
|
+
r.diff.tagsAdded.length === 0 &&
|
|
308
|
+
r.diff.tagsRemoved.length === 0 &&
|
|
309
|
+
r.diff.childrenAdded.length === 0 &&
|
|
310
|
+
r.diff.childrenRemoved.length === 0 &&
|
|
311
|
+
r.diff.powerupsChanged.length === 0 &&
|
|
312
|
+
r.diff.fieldsChanged.length === 0
|
|
313
|
+
) {
|
|
314
|
+
lines.push(' (无可检测的变化)');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 完整的 after 快照(供详细分析)
|
|
318
|
+
lines.push(' After 完整快照:');
|
|
319
|
+
lines.push(` tags (${r.after.tags.length}): ${JSON.stringify(r.after.tags.map(t => ({ text: t.text, isPowerup: t.isPowerup })))}`);
|
|
320
|
+
lines.push(` children (${r.after.children.length}): ${JSON.stringify(r.after.children.map(c => ({ text: c.text, backText: c.backText, isPowerup: c.isPowerup, isPowerupProp: c.isPowerupProperty, childCount: c.children.length })))}`);
|
|
321
|
+
lines.push(` hasPowerup (true only): ${JSON.stringify(r.after.hasPowerup.filter(p => p.hasPowerup).map(p => p.name))}`);
|
|
322
|
+
lines.push('');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
lines.push('='.repeat(80));
|
|
326
|
+
lines.push('报告结束');
|
|
327
|
+
lines.push('='.repeat(80));
|
|
328
|
+
|
|
329
|
+
return lines.join('\n');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export async function runPowerupRenderingTest(plugin: ReactRNPlugin): Promise<void> {
|
|
333
|
+
const alreadyRan = await plugin.storage.getSession('powerup-render-test-v2-ran');
|
|
334
|
+
if (alreadyRan) {
|
|
335
|
+
console.log('[POWERUP-RENDER] 已运行过,跳过');
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
await plugin.storage.setSession('powerup-render-test-v2-ran', true);
|
|
339
|
+
|
|
340
|
+
console.log('[POWERUP-RENDER] ========== 开始 Powerup 渲染机制探测 ==========');
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
// ── 找到 "mcp 测试" 文档 ──
|
|
344
|
+
const allRem = await plugin.rem.getAll();
|
|
345
|
+
let parentDoc: Rem | undefined;
|
|
346
|
+
for (const r of allRem) {
|
|
347
|
+
const t = r.text ? await plugin.richText.toString(r.text) : '';
|
|
348
|
+
if (t.trim() === 'mcp') { parentDoc = r; break; }
|
|
349
|
+
}
|
|
350
|
+
if (!parentDoc) { console.error('[POWERUP-RENDER] 找不到 "mcp"'); return; }
|
|
351
|
+
|
|
352
|
+
// ── 清理上次测试残留(以 "PU:" 开头的 Rem)──
|
|
353
|
+
const children = await parentDoc.getChildrenRem();
|
|
354
|
+
for (const c of children) {
|
|
355
|
+
const t = c.text ? await plugin.richText.toString(c.text) : '';
|
|
356
|
+
if (t.startsWith('PU:') || t.startsWith('=== Powerup')) {
|
|
357
|
+
await c.remove();
|
|
358
|
+
await delay(100);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
await delay(500);
|
|
362
|
+
|
|
363
|
+
let pos = 0;
|
|
364
|
+
const results: ProbeResult[] = [];
|
|
365
|
+
|
|
366
|
+
// ── 工具函数:创建空白 Rem 并拍快照 ──
|
|
367
|
+
async function createAndSnapshotBefore(label: string): Promise<{ rem: Rem; before: RemSnapshot } | null> {
|
|
368
|
+
const r = await plugin.rem.createRem();
|
|
369
|
+
if (!r) return null;
|
|
370
|
+
await r.setParent(parentDoc!._id, pos++);
|
|
371
|
+
await r.setText([`PU: ${label}`]);
|
|
372
|
+
await delay(500); // 等 SDK 稳定
|
|
373
|
+
const before = await takeSnapshot(plugin, r);
|
|
374
|
+
return { rem: r, before };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── 工具函数:执行 SDK 方法后拍快照并记录差异 ──
|
|
378
|
+
async function probeAfter(
|
|
379
|
+
label: string,
|
|
380
|
+
rem: Rem,
|
|
381
|
+
before: RemSnapshot,
|
|
382
|
+
): Promise<void> {
|
|
383
|
+
await delay(500); // 等 SDK 稳定
|
|
384
|
+
// 重新获取 Rem(某些操作可能需要刷新)
|
|
385
|
+
const fresh = await plugin.rem.findOne(rem._id);
|
|
386
|
+
if (!fresh) {
|
|
387
|
+
console.error(`[POWERUP-RENDER] Rem ${rem._id} 不存在了`);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const after = await takeSnapshot(plugin, fresh);
|
|
391
|
+
const diff = computeDiff(before, after);
|
|
392
|
+
results.push({ label, remId: rem._id, before, after, diff });
|
|
393
|
+
console.log(`[POWERUP-RENDER] ${label}: done`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ════════════════════════════════════════════════════════════
|
|
397
|
+
// 测试用例:每个 Rem 只用一种 SDK 方法
|
|
398
|
+
// ════════════════════════════════════════════════════════════
|
|
399
|
+
|
|
400
|
+
// 1. setFontSize('H1')
|
|
401
|
+
{
|
|
402
|
+
const ctx = await createAndSnapshotBefore('setFontSize(H1)');
|
|
403
|
+
if (ctx) {
|
|
404
|
+
await ctx.rem.setFontSize('H1');
|
|
405
|
+
await probeAfter('setFontSize(H1)', ctx.rem, ctx.before);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// 2. setFontSize('H2')
|
|
410
|
+
{
|
|
411
|
+
const ctx = await createAndSnapshotBefore('setFontSize(H2)');
|
|
412
|
+
if (ctx) {
|
|
413
|
+
await ctx.rem.setFontSize('H2');
|
|
414
|
+
await probeAfter('setFontSize(H2)', ctx.rem, ctx.before);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// 3. setFontSize('H3')
|
|
419
|
+
{
|
|
420
|
+
const ctx = await createAndSnapshotBefore('setFontSize(H3)');
|
|
421
|
+
if (ctx) {
|
|
422
|
+
await ctx.rem.setFontSize('H3');
|
|
423
|
+
await probeAfter('setFontSize(H3)', ctx.rem, ctx.before);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 4. setHighlightColor('Red')
|
|
428
|
+
{
|
|
429
|
+
const ctx = await createAndSnapshotBefore('setHighlightColor(Red)');
|
|
430
|
+
if (ctx) {
|
|
431
|
+
await ctx.rem.setHighlightColor('Red');
|
|
432
|
+
await probeAfter('setHighlightColor(Red)', ctx.rem, ctx.before);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// 5. setHighlightColor('Blue')
|
|
437
|
+
{
|
|
438
|
+
const ctx = await createAndSnapshotBefore('setHighlightColor(Blue)');
|
|
439
|
+
if (ctx) {
|
|
440
|
+
await ctx.rem.setHighlightColor('Blue');
|
|
441
|
+
await probeAfter('setHighlightColor(Blue)', ctx.rem, ctx.before);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// 6. setHighlightColor('Green')
|
|
446
|
+
{
|
|
447
|
+
const ctx = await createAndSnapshotBefore('setHighlightColor(Green)');
|
|
448
|
+
if (ctx) {
|
|
449
|
+
await ctx.rem.setHighlightColor('Green');
|
|
450
|
+
await probeAfter('setHighlightColor(Green)', ctx.rem, ctx.before);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// 7. setIsCode(true)
|
|
455
|
+
{
|
|
456
|
+
const ctx = await createAndSnapshotBefore('setIsCode(true)');
|
|
457
|
+
if (ctx) {
|
|
458
|
+
await ctx.rem.setIsCode(true);
|
|
459
|
+
await probeAfter('setIsCode(true)', ctx.rem, ctx.before);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// 8. setIsQuote(true)
|
|
464
|
+
{
|
|
465
|
+
const ctx = await createAndSnapshotBefore('setIsQuote(true)');
|
|
466
|
+
if (ctx) {
|
|
467
|
+
await ctx.rem.setIsQuote(true);
|
|
468
|
+
await probeAfter('setIsQuote(true)', ctx.rem, ctx.before);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// 9. setIsListItem(true)
|
|
473
|
+
{
|
|
474
|
+
const ctx = await createAndSnapshotBefore('setIsListItem(true)');
|
|
475
|
+
if (ctx) {
|
|
476
|
+
await ctx.rem.setIsListItem(true);
|
|
477
|
+
await probeAfter('setIsListItem(true)', ctx.rem, ctx.before);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// 10. setIsTodo(true)
|
|
482
|
+
{
|
|
483
|
+
const ctx = await createAndSnapshotBefore('setIsTodo(true)');
|
|
484
|
+
if (ctx) {
|
|
485
|
+
await ctx.rem.setIsTodo(true);
|
|
486
|
+
await probeAfter('setIsTodo(true)', ctx.rem, ctx.before);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// 11. setTodoStatus('Finished') — 需要先 setIsTodo
|
|
491
|
+
{
|
|
492
|
+
const ctx = await createAndSnapshotBefore('setTodoStatus(Finished)');
|
|
493
|
+
if (ctx) {
|
|
494
|
+
await ctx.rem.setIsTodo(true);
|
|
495
|
+
await delay(300);
|
|
496
|
+
// 重新拍 before(因为 setIsTodo 已经改变了状态)
|
|
497
|
+
const beforeTodoFinish = await takeSnapshot(plugin, ctx.rem);
|
|
498
|
+
await ctx.rem.setTodoStatus('Finished');
|
|
499
|
+
await probeAfter('setTodoStatus(Finished) [after setIsTodo]', ctx.rem, beforeTodoFinish);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// 12. setIsCardItem(true)
|
|
504
|
+
{
|
|
505
|
+
const ctx = await createAndSnapshotBefore('setIsCardItem(true)');
|
|
506
|
+
if (ctx) {
|
|
507
|
+
await ctx.rem.setIsCardItem(true);
|
|
508
|
+
await probeAfter('setIsCardItem(true)', ctx.rem, ctx.before);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// 13. setIsDocument(true)
|
|
513
|
+
{
|
|
514
|
+
const ctx = await createAndSnapshotBefore('setIsDocument(true)');
|
|
515
|
+
if (ctx) {
|
|
516
|
+
await ctx.rem.setIsDocument(true);
|
|
517
|
+
await probeAfter('setIsDocument(true)', ctx.rem, ctx.before);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// 14. setType(CONCEPT = 1)
|
|
522
|
+
{
|
|
523
|
+
const ctx = await createAndSnapshotBefore('setType(CONCEPT)');
|
|
524
|
+
if (ctx) {
|
|
525
|
+
await ctx.rem.setType(1 as any);
|
|
526
|
+
await probeAfter('setType(CONCEPT)', ctx.rem, ctx.before);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// 15. setType(DESCRIPTOR = 2)
|
|
531
|
+
{
|
|
532
|
+
const ctx = await createAndSnapshotBefore('setType(DESCRIPTOR)');
|
|
533
|
+
if (ctx) {
|
|
534
|
+
await ctx.rem.setType(2 as any);
|
|
535
|
+
await probeAfter('setType(DESCRIPTOR)', ctx.rem, ctx.before);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// 16. setIsSlot(true)
|
|
540
|
+
{
|
|
541
|
+
const ctx = await createAndSnapshotBefore('setIsSlot(true)');
|
|
542
|
+
if (ctx) {
|
|
543
|
+
await ctx.rem.setIsSlot(true);
|
|
544
|
+
await probeAfter('setIsSlot(true)', ctx.rem, ctx.before);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// 17. setIsProperty(true)
|
|
549
|
+
{
|
|
550
|
+
const ctx = await createAndSnapshotBefore('setIsProperty(true)');
|
|
551
|
+
if (ctx) {
|
|
552
|
+
await ctx.rem.setIsProperty(true);
|
|
553
|
+
await probeAfter('setIsProperty(true)', ctx.rem, ctx.before);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// 18. setBackText (产生正反面)
|
|
558
|
+
{
|
|
559
|
+
const ctx = await createAndSnapshotBefore('setBackText');
|
|
560
|
+
if (ctx) {
|
|
561
|
+
await ctx.rem.setBackText(['这是背面文本']);
|
|
562
|
+
await probeAfter('setBackText', ctx.rem, ctx.before);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// 19. addPowerup(Header) — 直接用 Powerup API
|
|
567
|
+
{
|
|
568
|
+
const ctx = await createAndSnapshotBefore('addPowerup(Header)');
|
|
569
|
+
if (ctx) {
|
|
570
|
+
await ctx.rem.addPowerup('r');
|
|
571
|
+
await probeAfter('addPowerup(Header)', ctx.rem, ctx.before);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// 20. addPowerup(Highlight) — 直接用 Powerup API
|
|
576
|
+
{
|
|
577
|
+
const ctx = await createAndSnapshotBefore('addPowerup(Highlight)');
|
|
578
|
+
if (ctx) {
|
|
579
|
+
await ctx.rem.addPowerup('h');
|
|
580
|
+
await probeAfter('addPowerup(Highlight)', ctx.rem, ctx.before);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// 21. addPowerup(Code) — 直接用 Powerup API
|
|
585
|
+
{
|
|
586
|
+
const ctx = await createAndSnapshotBefore('addPowerup(Code)');
|
|
587
|
+
if (ctx) {
|
|
588
|
+
await ctx.rem.addPowerup('cd');
|
|
589
|
+
await probeAfter('addPowerup(Code)', ctx.rem, ctx.before);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// 22. addPowerup(Quote) — 直接用 Powerup API
|
|
594
|
+
{
|
|
595
|
+
const ctx = await createAndSnapshotBefore('addPowerup(Quote)');
|
|
596
|
+
if (ctx) {
|
|
597
|
+
await ctx.rem.addPowerup('qt');
|
|
598
|
+
await probeAfter('addPowerup(Quote)', ctx.rem, ctx.before);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// 23. addPowerup(Todo) — 直接用 Powerup API
|
|
603
|
+
{
|
|
604
|
+
const ctx = await createAndSnapshotBefore('addPowerup(Todo)');
|
|
605
|
+
if (ctx) {
|
|
606
|
+
await ctx.rem.addPowerup('t');
|
|
607
|
+
await probeAfter('addPowerup(Todo)', ctx.rem, ctx.before);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// 24. addPowerup(List) — 直接用 Powerup API
|
|
612
|
+
{
|
|
613
|
+
const ctx = await createAndSnapshotBefore('addPowerup(List)');
|
|
614
|
+
if (ctx) {
|
|
615
|
+
await ctx.rem.addPowerup('i');
|
|
616
|
+
await probeAfter('addPowerup(List)', ctx.rem, ctx.before);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// 25. addPowerup(EditLater) — 直接用 Powerup API
|
|
621
|
+
{
|
|
622
|
+
const ctx = await createAndSnapshotBefore('addPowerup(EditLater)');
|
|
623
|
+
if (ctx) {
|
|
624
|
+
await ctx.rem.addPowerup('e');
|
|
625
|
+
await probeAfter('addPowerup(EditLater)', ctx.rem, ctx.before);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// 26. addPowerup(Document) — 直接用 Powerup API
|
|
630
|
+
{
|
|
631
|
+
const ctx = await createAndSnapshotBefore('addPowerup(Document)');
|
|
632
|
+
if (ctx) {
|
|
633
|
+
await ctx.rem.addPowerup('o');
|
|
634
|
+
await probeAfter('addPowerup(Document)', ctx.rem, ctx.before);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// 27. addPowerup(MultiLineCard) — 直接用 Powerup API
|
|
639
|
+
{
|
|
640
|
+
const ctx = await createAndSnapshotBefore('addPowerup(MultiLineCard)');
|
|
641
|
+
if (ctx) {
|
|
642
|
+
await ctx.rem.addPowerup('w');
|
|
643
|
+
await probeAfter('addPowerup(MultiLineCard)', ctx.rem, ctx.before);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// 28. addPowerup(Divider)
|
|
648
|
+
{
|
|
649
|
+
const ctx = await createAndSnapshotBefore('addPowerup(Divider)');
|
|
650
|
+
if (ctx) {
|
|
651
|
+
await ctx.rem.addPowerup('dv');
|
|
652
|
+
await probeAfter('addPowerup(Divider)', ctx.rem, ctx.before);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// 29. setPowerupProperty(Header, Size, H1) — 先 addPowerup 再设属性
|
|
657
|
+
{
|
|
658
|
+
const ctx = await createAndSnapshotBefore('setPowerupProperty(Header,Size,H1)');
|
|
659
|
+
if (ctx) {
|
|
660
|
+
await ctx.rem.addPowerup('r');
|
|
661
|
+
await delay(300);
|
|
662
|
+
// 重新拍 before(addPowerup 已改变状态)
|
|
663
|
+
const beforeSetProp = await takeSnapshot(plugin, ctx.rem);
|
|
664
|
+
await ctx.rem.setPowerupProperty('r', 'Size', ['H1']);
|
|
665
|
+
await probeAfter('setPowerupProperty(Header,Size,H1) [after addPowerup]', ctx.rem, beforeSetProp);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// 30. setPowerupProperty(Highlight, Color, Red) — 先 addPowerup 再设属性
|
|
670
|
+
{
|
|
671
|
+
const ctx = await createAndSnapshotBefore('setPowerupProperty(Highlight,Color,Red)');
|
|
672
|
+
if (ctx) {
|
|
673
|
+
await ctx.rem.addPowerup('h');
|
|
674
|
+
await delay(300);
|
|
675
|
+
const beforeSetProp = await takeSnapshot(plugin, ctx.rem);
|
|
676
|
+
await ctx.rem.setPowerupProperty('h', 'Color', ['Red']);
|
|
677
|
+
await probeAfter('setPowerupProperty(Highlight,Color,Red) [after addPowerup]', ctx.rem, beforeSetProp);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// 31. setEnablePractice(true)
|
|
682
|
+
{
|
|
683
|
+
const ctx = await createAndSnapshotBefore('setEnablePractice(true)');
|
|
684
|
+
if (ctx) {
|
|
685
|
+
await ctx.rem.setEnablePractice(true);
|
|
686
|
+
await probeAfter('setEnablePractice(true)', ctx.rem, ctx.before);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// 32. setPracticeDirection('forward')
|
|
691
|
+
{
|
|
692
|
+
const ctx = await createAndSnapshotBefore('setPracticeDirection(forward)');
|
|
693
|
+
if (ctx) {
|
|
694
|
+
await ctx.rem.setPracticeDirection('forward');
|
|
695
|
+
await probeAfter('setPracticeDirection(forward)', ctx.rem, ctx.before);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// ════════════════════════════════════════════════════════════
|
|
700
|
+
// 生成并输出报告
|
|
701
|
+
// ════════════════════════════════════════════════════════════
|
|
702
|
+
|
|
703
|
+
const report = formatReport(results);
|
|
704
|
+
console.log(report);
|
|
705
|
+
|
|
706
|
+
// 同时创建一个报告 Rem,方便在 RemNote 中查看
|
|
707
|
+
const reportRem = await plugin.rem.createRem();
|
|
708
|
+
if (reportRem) {
|
|
709
|
+
await reportRem.setParent(parentDoc._id, pos++);
|
|
710
|
+
await reportRem.setText([`=== Powerup 渲染探测完毕 (${results.length} 个测试) ===`]);
|
|
711
|
+
await reportRem.setHighlightColor('Green');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// 将完整 JSON 结果存到 session,方便通过 bridge 读取
|
|
715
|
+
await plugin.storage.setSession('powerup-render-results', results);
|
|
716
|
+
|
|
717
|
+
console.log('[POWERUP-RENDER] ========== 探测完毕 ==========');
|
|
718
|
+
|
|
719
|
+
} catch (err) {
|
|
720
|
+
console.error('[POWERUP-RENDER] 出错:', err);
|
|
721
|
+
}
|
|
722
|
+
}
|