@xiongtao.xt/tau-plugins 0.31.0 → 0.32.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/CHANGELOG.md +12 -0
- package/README.md +2 -2
- package/client/{chunk-ECEBBIHP.js → chunk-SVY5LBAT.js} +44 -0
- package/docs/README.md +43 -0
- package/docs/ai.md +169 -0
- package/docs/backend.md +168 -0
- package/docs/editor.md +457 -0
- package/docs/excerpts.md +126 -0
- package/docs/flashcards.md +127 -0
- package/docs/getting-started.md +134 -0
- package/docs/mcp.md +173 -0
- package/docs/plugin-basics.md +209 -0
- package/docs/providers/README.md +108 -0
- package/docs/providers/images.md +65 -0
- package/docs/providers/publish.md +144 -0
- package/docs/providers/sync.md +75 -0
- package/docs/providers/tts.md +62 -0
- package/docs/providers/upload.md +77 -0
- package/docs/providers/video.md +118 -0
- package/docs/release.md +130 -0
- package/docs/services.md +159 -0
- package/docs/site-data.md +90 -0
- package/docs/skills.md +129 -0
- package/docs/storage-and-network.md +62 -0
- package/docs/ui.md +335 -0
- package/docs/user-data.md +389 -0
- package/docs/whiteboard.md +290 -0
- package/package.js +1 -1
- package/package.json +1 -1
- package/publish-artifacts.js +1 -1
- package/publish-client.js +3 -1
- package/types/packages/plugins/src/categories.d.ts +3 -0
- package/types/packages/plugins/src/contract.d.ts +0 -2
- package/types/packages/plugins/src/index.d.ts +0 -1
- package/types/packages/plugins/src/package.d.ts +2 -0
- package/AI.md +0 -116
- package/API.md +0 -881
- package/BACKEND.md +0 -68
- package/DEVELOPMENT.md +0 -173
- package/EDITOR.md +0 -192
- package/FLASHCARDS.md +0 -92
- package/IMAGES.md +0 -16
- package/MCP.md +0 -118
- package/NOTIFICATIONS.md +0 -48
- package/PUBLISH-DATA.md +0 -48
- package/PUBLISH.md +0 -50
- package/SERVICES.md +0 -119
- package/SYNC.md +0 -37
- package/TTS.md +0 -20
- package/UPLOAD.md +0 -35
- package/VIDEO.md +0 -65
- package/WHITEBOARD.md +0 -115
- package/types/packages/plugins/src/skills.d.ts +0 -8
package/docs/editor.md
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
# 编辑器扩展
|
|
2
|
+
|
|
3
|
+
编辑器扩展让你在 Tau 的文档里添加自己的块:交互演示、图表、特殊格式的卡片等等。用户从块菜单(输入 `/`)插入你的块,它和 Tau 自带的块一样可以拖动、复制、保存,还会出现在用户发布的博客里。
|
|
4
|
+
|
|
5
|
+
这一篇先用一个完整的例子走一遍,再逐项介绍扩展能声明的内容。
|
|
6
|
+
|
|
7
|
+
## 第一个块:计数器
|
|
8
|
+
|
|
9
|
+
### 1. 在 manifest 中声明
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"editor": true
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
把这一行加进 `manifest.json`。没有它,Tau 不会加载插件里的编辑器扩展。
|
|
18
|
+
|
|
19
|
+
### 2. 定义节点和扩展
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { useState } from "react";
|
|
23
|
+
import { definePlugin, defineEditorElement, defineEditorExtension } from "@tau/plugins";
|
|
24
|
+
|
|
25
|
+
declare const __PLUGIN_META__: { id: string; name: string; version: string };
|
|
26
|
+
|
|
27
|
+
// 节点的数据结构:除了 type 和 children,其余字段都会随文档保存
|
|
28
|
+
interface CounterElement {
|
|
29
|
+
type: "yourname.counter";
|
|
30
|
+
initial: number;
|
|
31
|
+
children: { type: "formatted"; text: string }[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 登记节点类型,让 TypeScript 认识它
|
|
35
|
+
declare module "@tau/plugins" {
|
|
36
|
+
interface EditorElementMap {
|
|
37
|
+
"yourname.counter": CounterElement;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function Counter({ initial }: { initial: number }) {
|
|
42
|
+
const [count, setCount] = useState(initial);
|
|
43
|
+
return (
|
|
44
|
+
<div className="yourname-counter" contentEditable={false}>
|
|
45
|
+
<button onMouseDown={event => event.preventDefault()} onClick={() => setCount(n => n + 1)}>
|
|
46
|
+
计数:{count}
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function CounterIcon({ className }: { className?: string }) {
|
|
53
|
+
return (
|
|
54
|
+
<svg
|
|
55
|
+
className={className}
|
|
56
|
+
viewBox="0 0 24 24"
|
|
57
|
+
fill="none"
|
|
58
|
+
stroke="currentColor"
|
|
59
|
+
strokeWidth="1.5"
|
|
60
|
+
>
|
|
61
|
+
<rect x="3" y="3" width="18" height="18" rx="3" />
|
|
62
|
+
<path d="M7 12h10M12 7v10" />
|
|
63
|
+
</svg>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default definePlugin({
|
|
68
|
+
...__PLUGIN_META__,
|
|
69
|
+
editorExtensions: [
|
|
70
|
+
defineEditorExtension({
|
|
71
|
+
id: "yourname.counter",
|
|
72
|
+
elements: [
|
|
73
|
+
defineEditorElement<CounterElement>({
|
|
74
|
+
type: "yourname.counter",
|
|
75
|
+
kind: "block",
|
|
76
|
+
isVoid: true,
|
|
77
|
+
drag: true,
|
|
78
|
+
render: ({ attributes, element, children }) => (
|
|
79
|
+
<div {...attributes}>
|
|
80
|
+
<Counter initial={element.initial} />
|
|
81
|
+
{children}
|
|
82
|
+
</div>
|
|
83
|
+
),
|
|
84
|
+
}),
|
|
85
|
+
],
|
|
86
|
+
blockPanel: [
|
|
87
|
+
{
|
|
88
|
+
icon: CounterIcon,
|
|
89
|
+
title: "计数器",
|
|
90
|
+
keywords: ["counter", "计数"],
|
|
91
|
+
description: "一个可以点击计数的块",
|
|
92
|
+
onClick: editor =>
|
|
93
|
+
editor.insertNode({
|
|
94
|
+
type: "yourname.counter",
|
|
95
|
+
initial: 0,
|
|
96
|
+
children: [{ type: "formatted", text: "" }],
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
}),
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 3. 构建、安装、重启
|
|
106
|
+
|
|
107
|
+
`pnpm build` 并安装后,**完全退出并重新打开 Tau**。编辑器扩展在启动时装配,启用、停用和更新都要重启才生效。之后在文档里输入 `/计数器`,就能插入这个块。
|
|
108
|
+
|
|
109
|
+
这个只有编辑器扩展的插件没有写 `create`。如果插件还需要命令、页面或设置,照常提供 `create` 和 `activate`,两者可以共存。
|
|
110
|
+
|
|
111
|
+
## 节点
|
|
112
|
+
|
|
113
|
+
节点是文档里的一块数据。你定义它的结构,Tau 负责保存、复制、撤销和协作。
|
|
114
|
+
|
|
115
|
+
### 节点数据与组件状态
|
|
116
|
+
|
|
117
|
+
- **节点字段**(例子中的 `initial`)是文档内容,会随文档保存、同步、发布。
|
|
118
|
+
- **组件状态**(例子中的 `useState`)只是这一次显示的状态,刷新后就没了。
|
|
119
|
+
|
|
120
|
+
需要持久保存的东西放进节点字段。不要把文档内容放进插件的私有存储,那样复制、导出、发布时都会丢失。
|
|
121
|
+
|
|
122
|
+
### 节点类型的命名
|
|
123
|
+
|
|
124
|
+
节点 `type` 和扩展 `id` 在所有插件之间必须唯一,建议用你的插件 ID 作前缀,例如 `yourname.counter`、`yourname.chart`。一旦有文档用了这个类型,就不要再改名,否则已有的块会失效。
|
|
125
|
+
|
|
126
|
+
### 节点的声明
|
|
127
|
+
|
|
128
|
+
| 字段 | 说明 |
|
|
129
|
+
| ----------- | ----------------------------------------------------------------------------------------- |
|
|
130
|
+
| `type` | 节点类型 |
|
|
131
|
+
| `kind` | `"block"`(块级)或 `"inline"`(行内) |
|
|
132
|
+
| `render` | 渲染组件,见下文 |
|
|
133
|
+
| `isVoid` | 可选。为 `true` 时节点没有可编辑的文字,内容完全由组件渲染(如图表、演示) |
|
|
134
|
+
| `drag` | 可选。为 `true` 时显示拖动手柄,用户可以拖动这个块。不声明就不能拖动 |
|
|
135
|
+
| `isEmpty` | 可选。判断节点是否为空。内容保存在自定义字段(如源码)里时,用它告诉 Tau 什么情况下算空块 |
|
|
136
|
+
| `behaviors` | 可选。光标在节点内时,接管回车、退格、输入等行为,见[行为](#行为) |
|
|
137
|
+
|
|
138
|
+
### 渲染组件
|
|
139
|
+
|
|
140
|
+
`render` 组件收到三个属性:`attributes`、`element`、`children`。
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
render: ({ attributes, element, children }) => (
|
|
144
|
+
<div {...attributes}>
|
|
145
|
+
<MyWidget data={element} />
|
|
146
|
+
{children}
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- `attributes` 必须展开到最外层元素上,`children` 必须渲染出来。即使是 `isVoid` 节点也一样,这是编辑器追踪光标所需要的。
|
|
152
|
+
- 组件里不可编辑的部分加上 `contentEditable={false}`。
|
|
153
|
+
- 组件里的按钮在 `onMouseDown` 中调用 `event.preventDefault()`,避免点击时编辑器失去光标。
|
|
154
|
+
|
|
155
|
+
### 修改节点字段
|
|
156
|
+
|
|
157
|
+
组件里需要修改节点数据(例如保存用户调整后的参数)时,使用 `useSourceBlock` 返回的 `update`:
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
import { useSourceBlock } from "@tau/plugins";
|
|
161
|
+
|
|
162
|
+
function Counter({ element }: { element: CounterElement }) {
|
|
163
|
+
const { update } = useSourceBlock(element);
|
|
164
|
+
return (
|
|
165
|
+
<button
|
|
166
|
+
onMouseDown={event => event.preventDefault()}
|
|
167
|
+
onClick={() => update({ initial: element.initial + 1 })}
|
|
168
|
+
>
|
|
169
|
+
计数:{element.initial}
|
|
170
|
+
</button>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`update` 把传入的字段合并进节点,这次修改会进入撤销记录并同步给协作者。
|
|
176
|
+
|
|
177
|
+
## 扩展能声明的内容
|
|
178
|
+
|
|
179
|
+
`defineEditorExtension` 接收:
|
|
180
|
+
|
|
181
|
+
| 字段 | 说明 |
|
|
182
|
+
| ----------------- | ----------------------------------------------------------------------- |
|
|
183
|
+
| `id` | 扩展 ID,全局唯一 |
|
|
184
|
+
| `elements` | 节点声明,至少一个 |
|
|
185
|
+
| `blockPanel` | 块菜单(`/` 菜单)中的条目 |
|
|
186
|
+
| `commands` | 编辑器命令 |
|
|
187
|
+
| `hotkeys` | 快捷键,绑定到命令 |
|
|
188
|
+
| `menu` | 块的手柄菜单和右键菜单中的条目 |
|
|
189
|
+
| `behaviorsFor` | 挂在其他节点类型上的行为,用来实现快捷语法 |
|
|
190
|
+
| `styleSources` | 节点里的 Tailwind 源码字段,见[动态 Tailwind 样式](#动态-tailwind-样式) |
|
|
191
|
+
| `htmlSerializers` | 静态 HTML 导出,见 [HTML 导出](#html-导出) |
|
|
192
|
+
|
|
193
|
+
### 块菜单
|
|
194
|
+
|
|
195
|
+
`blockPanel` 的每一项会出现在用户输入 `/` 后弹出的菜单里:
|
|
196
|
+
|
|
197
|
+
| 字段 | 说明 |
|
|
198
|
+
| ------------- | -------------------------------------------- |
|
|
199
|
+
| `icon` | 图标组件,接收 `className` |
|
|
200
|
+
| `title` | 标题 |
|
|
201
|
+
| `description` | 说明 |
|
|
202
|
+
| `keywords` | 搜索关键词,用户输入这些词时会匹配到这一项 |
|
|
203
|
+
| `onClick` | 选中时调用,收到当前编辑器,通常用来插入节点 |
|
|
204
|
+
| `children` | 可选。子菜单 |
|
|
205
|
+
|
|
206
|
+
`onClick` 收到的 `editor` 提供 `insertNode`、`setNodes`、`removeNodes` 等编辑器方法。插入后需要立即编辑的源码类块,用 `insertSourceBlock`,见[源码预览块](#源码预览块)。
|
|
207
|
+
|
|
208
|
+
`blockPanel` 也可以是一个函数 `(editor) => items`,每次打开菜单时求值。
|
|
209
|
+
|
|
210
|
+
### 命令与快捷键
|
|
211
|
+
|
|
212
|
+
命令是编辑器里的动作,快捷键和菜单都通过命令 ID 触发它:
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
defineEditorExtension({
|
|
216
|
+
id: "yourname.counter",
|
|
217
|
+
elements: [/* ... */],
|
|
218
|
+
commands: [
|
|
219
|
+
{
|
|
220
|
+
id: "yourname.counter.reset",
|
|
221
|
+
title: "重置计数",
|
|
222
|
+
run: (editor, { path }) => {
|
|
223
|
+
if (path) editor.setNodes({ initial: 0 }, { at: path });
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
hotkeys: [{ key: "mod+shift+0", command: "yourname.counter.reset" }],
|
|
228
|
+
menu: [{ id: "yourname.counter.reset", title: "重置计数", command: "yourname.counter.reset" }],
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
| 命令字段 | 说明 |
|
|
233
|
+
| -------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
234
|
+
| `id` | 命令 ID,全局唯一,建议以扩展 ID 作前缀 |
|
|
235
|
+
| `title` | 可选。显示名称 |
|
|
236
|
+
| `when` | 可选。`(editor) => boolean`,返回 `false` 时快捷键跳过、菜单项变灰 |
|
|
237
|
+
| `run` | `(editor, context) => void`。`context` 包含触发它的 `event`、调用方的 `payload`,从菜单触发时还有目标节点 `element` 和它的 `path` |
|
|
238
|
+
|
|
239
|
+
| 快捷键字段 | 说明 |
|
|
240
|
+
| ---------- | -------------------------------------------------------------------------------- |
|
|
241
|
+
| `key` | 按键组合,如 `"mod+shift+k"`,`mod` 在 macOS 上是 ⌘,其他系统是 Ctrl;可以传数组 |
|
|
242
|
+
| `command` | 要执行的命令 ID |
|
|
243
|
+
| `fallback` | 可选。为 `true` 时只在其他扩展的快捷键都没有处理时才尝试 |
|
|
244
|
+
|
|
245
|
+
命令执行出错时,Tau 会隔离错误,不影响编辑器的其他功能。异步命令返回 Promise 即可。
|
|
246
|
+
|
|
247
|
+
### 菜单
|
|
248
|
+
|
|
249
|
+
`menu` 中的条目出现在你的块的手柄菜单和右键菜单里:
|
|
250
|
+
|
|
251
|
+
| 字段 | 说明 |
|
|
252
|
+
| ---------------- | ----------------------------------------------------- |
|
|
253
|
+
| `id` | 条目 ID,同一份菜单内唯一 |
|
|
254
|
+
| `title` | 标题,可以是函数 `(ctx) => string`,打开菜单时求值 |
|
|
255
|
+
| `icon` | 可选。图标,同样可以是函数 |
|
|
256
|
+
| `command` | 点击时执行的命令 ID;只有子菜单的父条目可以省略 |
|
|
257
|
+
| `payload` | 可选。传给命令的参数,命令在 `context.payload` 中读取 |
|
|
258
|
+
| `when` | 可选。返回 `false` 时不显示这一项 |
|
|
259
|
+
| `disabled` | 可选。返回 `true` 时这一项变灰 |
|
|
260
|
+
| `group`、`order` | 可选。分组与排序 |
|
|
261
|
+
| `danger` | 可选。危险操作,以警示色显示 |
|
|
262
|
+
| `children` | 可选。子菜单 |
|
|
263
|
+
|
|
264
|
+
函数形式的字段收到 `{ editor, element, path }`,即菜单针对的块。
|
|
265
|
+
|
|
266
|
+
### 行为
|
|
267
|
+
|
|
268
|
+
`behaviors` 写在节点声明里,在光标位于该节点内时接管按键:
|
|
269
|
+
|
|
270
|
+
| 行为 | 时机 |
|
|
271
|
+
| -------------------------------------------- | ------------------ |
|
|
272
|
+
| `insertBreak(editor, [node, path])` | 按回车 |
|
|
273
|
+
| `deleteBackward(editor, [node, path], unit)` | 按退格 |
|
|
274
|
+
| `insertText(editor, [node, path], text)` | 输入文字 |
|
|
275
|
+
| `normalize(editor, [node, path])` | 编辑器规范化节点时 |
|
|
276
|
+
|
|
277
|
+
每个函数返回 `true` 表示已经处理,返回 `false` 交给外层节点和默认行为。
|
|
278
|
+
|
|
279
|
+
`behaviorsFor` 写在扩展上,把行为挂到**其他**节点类型上。典型用途是快捷语法:用户在段落里输入某个标记后回车,把段落替换成你的块。
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
behaviorsFor: {
|
|
283
|
+
paragraph: {
|
|
284
|
+
insertBreak: (editor, [node, path]) => {
|
|
285
|
+
// 检查段落文字,满足条件时替换成自己的节点并返回 true
|
|
286
|
+
return false;
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## 块会在哪里显示
|
|
293
|
+
|
|
294
|
+
你的块会出现在两个地方:
|
|
295
|
+
|
|
296
|
+
- **Tau 桌面端**的编辑器。
|
|
297
|
+
- **用户发布的博客**。博客使用同一个 `render` 组件,你不需要另写一份。
|
|
298
|
+
|
|
299
|
+
移动端和浏览器扩展不运行插件,遇到你的块时保留数据,并显示一个通用的占位。
|
|
300
|
+
|
|
301
|
+
因为同一份代码要在博客里运行,渲染相关的代码需要满足:
|
|
302
|
+
|
|
303
|
+
- 模块顶层和渲染组件只使用浏览器能力,不导入 Node.js 模块。
|
|
304
|
+
- 不依赖 `activate` 里才赋值的变量,也不使用 `context`(存储、页面、服务等只在桌面端的插件运行时里存在)。需要编译服务时,用 [`usePluginServices`](services.md#在编辑器块中使用服务),并处理拿不到服务的情况。
|
|
305
|
+
- JS 和 CSS 自包含:React、`@tau/plugins`、`@tau/ui` 由 Tau 提供,其他依赖会被打进 `main.js`;图片等资源内联。
|
|
306
|
+
- 样式用插件自己的类名前缀,不修改 `body` 或 Tau 自身元素的样式。
|
|
307
|
+
|
|
308
|
+
博客里的文档是只读的,但组件自身的交互(按钮、缩放图表)照常可用;在只读文档里修改节点字段不会被保存。
|
|
309
|
+
|
|
310
|
+
## 停用与卸载
|
|
311
|
+
|
|
312
|
+
- 插件停用后,文档里已有的块会保留,显示为「不可用」的占位,不会被删除;重新启用后恢复显示。
|
|
313
|
+
- 卸载正在使用的编辑器插件前,需要先停用并重启 Tau。
|
|
314
|
+
|
|
315
|
+
## Markdown 导入导出
|
|
316
|
+
|
|
317
|
+
在插件定义上提供 `editorCodecs`,你的块就能参与 Markdown 导入导出、复制为 Markdown 和搜索摘要。没有 codec 的块在导出 Markdown 时会以 Tau 的扩展格式保留,其他工具未必认识。
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
export default definePlugin({
|
|
321
|
+
...__PLUGIN_META__,
|
|
322
|
+
editorExtensions: [counterExtension],
|
|
323
|
+
editorCodecs: [
|
|
324
|
+
{
|
|
325
|
+
type: "yourname.counter",
|
|
326
|
+
isVoid: true,
|
|
327
|
+
// 导出:节点 → Markdown
|
|
328
|
+
serializer: { toMarkdown: element => `::yourname-counter{initial=${element.initial}}` },
|
|
329
|
+
// 导入:Markdown → 节点
|
|
330
|
+
directives: {
|
|
331
|
+
"yourname-counter": attributes => ({
|
|
332
|
+
type: "yourname.counter",
|
|
333
|
+
initial: Number(attributes.initial) || 0,
|
|
334
|
+
children: [{ type: "formatted", text: "" }],
|
|
335
|
+
}),
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
});
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
| 字段 | 说明 |
|
|
343
|
+
| ------------ | ------------------------------------------------------------------------------------------------------- |
|
|
344
|
+
| `type` | 对应的节点类型,只能是本插件声明的类型 |
|
|
345
|
+
| `serializer` | 导出。`toMarkdown(element, childrenMarkdown)` 返回 Markdown 字符串 |
|
|
346
|
+
| `directives` | 导入。按 directive 名(`::name{...}`、`:::name`、`:name[...]`)解析成节点,返回 `null` 表示丢弃 |
|
|
347
|
+
| `fence` | 导入。把指定语言的代码围栏(如 `mermaid` 代码块)认领为节点,源码放进 `property` 字段(默认 `content`) |
|
|
348
|
+
| `isVoid` | 节点是 void 时设为 `true` |
|
|
349
|
+
|
|
350
|
+
- 导入和导出要成对设计:只写 `serializer` 不会自动获得导入能力。
|
|
351
|
+
- codec 会在没有界面的环境中单独加载,必须是纯函数。模块顶层不要访问 `window`、`document`。
|
|
352
|
+
|
|
353
|
+
## 源码预览块
|
|
354
|
+
|
|
355
|
+
很多块的模式是"上面编辑源码,下面显示预览",比如图表、公式、组件演示。`SourceBlockEditor` 提供了 Tau 统一的源码预览外壳:源码编辑、全屏、错误提示、删除,以及与相邻块之间的光标移动。
|
|
356
|
+
|
|
357
|
+
```tsx
|
|
358
|
+
import type { ReactNode } from "react";
|
|
359
|
+
import { SourceBlockEditor, useSourceBlock } from "@tau/plugins";
|
|
360
|
+
import { Code } from "lucide-react";
|
|
361
|
+
|
|
362
|
+
interface DiagramElement {
|
|
363
|
+
type: "yourname.diagram";
|
|
364
|
+
content: string;
|
|
365
|
+
children: { type: "formatted"; text: string }[];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function DiagramBlock({ element, children }: { element: DiagramElement; children: ReactNode }) {
|
|
369
|
+
const { update } = useSourceBlock(element);
|
|
370
|
+
return (
|
|
371
|
+
<SourceBlockEditor
|
|
372
|
+
element={element}
|
|
373
|
+
mode="jsx"
|
|
374
|
+
title="图表"
|
|
375
|
+
icon={Code}
|
|
376
|
+
initValue={element.content}
|
|
377
|
+
onChange={content => update({ content })}
|
|
378
|
+
>
|
|
379
|
+
<DiagramPreview source={element.content} />
|
|
380
|
+
{children}
|
|
381
|
+
</SourceBlockEditor>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
在节点的 `render` 里使用 `DiagramBlock`。`icon` 需要一个 lucide-react 图标,先 `pnpm add lucide-react`。
|
|
387
|
+
|
|
388
|
+
| `SourceBlockEditor` 属性 | 说明 |
|
|
389
|
+
| ------------------------ | ---------------------------------------- |
|
|
390
|
+
| `element` | 当前节点 |
|
|
391
|
+
| `mode` | 源码编辑器的语言模式,如 `jsx`、`html` |
|
|
392
|
+
| `title`、`icon` | 块的标题和图标 |
|
|
393
|
+
| `initValue` | 源码初始值 |
|
|
394
|
+
| `onChange` | 源码变化时调用,通常用 `update` 写回节点 |
|
|
395
|
+
| `center`、`extend` | 可选。预览居中、预览区域扩展到更宽 |
|
|
396
|
+
| `children` | 预览内容 |
|
|
397
|
+
|
|
398
|
+
相关工具:
|
|
399
|
+
|
|
400
|
+
| 接口 | 用途 |
|
|
401
|
+
| ------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
402
|
+
| `useSourceBlock(element)` | 返回 `update`(修改节点字段)、`theme`(当前主题)和 `compiler`(Tailwind 编译器,可能为空) |
|
|
403
|
+
| `useReportPreviewError(message, { stale })` | 预览出错时上报错误,传 `null` 清除;`stale` 表示是否保留了上一次成功的预览 |
|
|
404
|
+
| `insertSourceBlock(editor, element, line?)` | 插入节点并把光标放进源码编辑器,适合在块菜单的 `onClick` 中使用 |
|
|
405
|
+
| `sourceBlockParagraphBehavior(type)` | 放进 `behaviorsFor.paragraph`,让用户在段首按退格时进入上方的源码块 |
|
|
406
|
+
|
|
407
|
+
`theme.isDark` 表示当前是否为暗色主题,`theme.token(word)` 读取主题色值,预览应随主题变化。`compiler` 只在桌面编辑器中可用,在博客里为空,此时配合下文的 `styleSources` 使用预先生成的样式。
|
|
408
|
+
|
|
409
|
+
不要在插件里打包自己的编辑器、Slate 或源码编辑器,直接使用这些组件。
|
|
410
|
+
|
|
411
|
+
## 行内源码
|
|
412
|
+
|
|
413
|
+
行内的源码节点(如行内公式)使用另一组接口:
|
|
414
|
+
|
|
415
|
+
| 接口 | 用途 |
|
|
416
|
+
| ------------------------------------- | ------------------------------------------------------------- |
|
|
417
|
+
| `useInlineSource(element)` | 返回 `readOnly`、`compiler`、`update(properties)`、`remove()` |
|
|
418
|
+
| `insertInlineSource(editor, element)` | 插入行内节点,适合在菜单中使用 |
|
|
419
|
+
| `InlineSourceBoundary` | 渲染在行内节点组件里,处理光标进出节点的边界 |
|
|
420
|
+
|
|
421
|
+
行内的源码输入弹层由你用 `@tau/ui` 组件实现。只读时只显示预览,不打开编辑弹层;只读状态下 `update` 和 `remove` 不会写入文档。
|
|
422
|
+
|
|
423
|
+
## 动态 Tailwind 样式
|
|
424
|
+
|
|
425
|
+
如果节点的源码里写了 Tailwind 类名(比如用户写的组件演示),在扩展上声明哪个字段是源码:
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
defineEditorExtension({
|
|
429
|
+
id: "yourname.demo",
|
|
430
|
+
elements: [/* ... */],
|
|
431
|
+
styleSources: [{ type: "yourname.demo", field: "content", language: "tsx" }],
|
|
432
|
+
});
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
在博客等不能实时编译样式的地方,Tau 会读取这些字段,提前生成对应的样式。
|
|
436
|
+
|
|
437
|
+
- 字段的值必须是字符串,`language` 为 `html` 或 `tsx`,只能登记本插件的节点类型。
|
|
438
|
+
- 类名要完整地写在源码里。拼接出来的类名(如 `` `bg-${color}` ``)扫描不到。
|
|
439
|
+
|
|
440
|
+
## HTML 导出
|
|
441
|
+
|
|
442
|
+
用户把文档导出为 HTML、EPUB 或复制为 HTML 时,你的块默认按未知节点处理,交互组件不会执行。需要更好的导出效果时,提供 `htmlSerializers`:
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
defineEditorExtension({
|
|
446
|
+
id: "yourname.counter",
|
|
447
|
+
elements: [/* ... */],
|
|
448
|
+
htmlSerializers: [
|
|
449
|
+
{
|
|
450
|
+
type: "yourname.counter",
|
|
451
|
+
toHtml: element => `<p>计数:${Number(element.initial)}</p>`,
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
});
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
`toHtml` 返回 HTML 字符串,文本内容要自己转义。它只用于静态导出,博客里的显示仍然使用 `render`。
|
package/docs/excerpts.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# 摘录
|
|
2
|
+
|
|
3
|
+
如果你的插件从外部服务读取划线、批注或收藏(读书笔记、网页高亮、文献批注等),可以把它们写成 Tau 的**摘录**。写入后,用户能在全局搜索和 AI 对话中找到它们,在文档中引用,也能把它们转为卡片或问题。
|
|
4
|
+
|
|
5
|
+
摘录属于用户:插件停用或卸载后,已经写入的摘录和引用都会保留。
|
|
6
|
+
|
|
7
|
+
## 注册提供方
|
|
8
|
+
|
|
9
|
+
写入摘录之前,先在 `activate` 中注册一次提供方:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
const bookPage = context.pages.register({
|
|
13
|
+
id: "book",
|
|
14
|
+
title: "书籍",
|
|
15
|
+
component: BookPage,
|
|
16
|
+
match: "params",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
context.excerpts.register({
|
|
20
|
+
label: "我的读书笔记",
|
|
21
|
+
open: ({ sourceKey, externalId }) => bookPage.open({ bookId: sourceKey, noteId: externalId }),
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
| 字段 | 说明 |
|
|
26
|
+
| ------- | ------------------------------------------------------------------------------------------ |
|
|
27
|
+
| `label` | 摘录来源的显示名称,1 至 100 字。插件停用后仍用它显示来源 |
|
|
28
|
+
| `open` | 可选。用户点「打开原处」时调用,通常打开插件自己的页面并定位;省略时打开摘录或来源的 `url` |
|
|
29
|
+
|
|
30
|
+
每次启用只能注册一次,没有注册就写入会报错。
|
|
31
|
+
|
|
32
|
+
## 写入
|
|
33
|
+
|
|
34
|
+
摘录按**来源**分组写入,一个来源可以是一本书、一个网页或一篇文献。写入有两种方式:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
const source = { key: "book-42", title: "设计心理学", url: "https://example.com/books/42" };
|
|
38
|
+
|
|
39
|
+
// 全量:以这批条目为准,不在其中的已有条目会被移除
|
|
40
|
+
await context.excerpts.replace(source, [
|
|
41
|
+
{
|
|
42
|
+
externalId: "n1",
|
|
43
|
+
quote: "好的设计是让人一眼就知道怎么用。",
|
|
44
|
+
location: "第 1 章",
|
|
45
|
+
time: 1760000000000,
|
|
46
|
+
},
|
|
47
|
+
{ externalId: "n2", quote: "示能决定了可能的操作。", note: "和交互设计课的观点一致" },
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
// 增量:只新增、更新或移除指定条目
|
|
51
|
+
await context.excerpts.patch(source, {
|
|
52
|
+
upsert: [{ externalId: "n3", quote: "反馈要及时。" }],
|
|
53
|
+
remove: ["n1"],
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
外部服务能一次给出某个来源的全部条目时用 `replace`,只拿到变化时用 `patch`。两者都返回 `{ inserted, updated, deleted }`。`replace` 传入空数组表示整个来源已经不存在。
|
|
58
|
+
|
|
59
|
+
| 来源字段 | 说明 |
|
|
60
|
+
| -------- | ------------------------------------------------------- |
|
|
61
|
+
| `key` | 来源在本插件内的稳定标识,必填,最长 512 字 |
|
|
62
|
+
| `title` | 来源标题,最长 500 字 |
|
|
63
|
+
| `url` | 可选。http(s) 地址,条目没有自己的 `url` 时用它打开原处 |
|
|
64
|
+
|
|
65
|
+
| 条目字段 | 说明 |
|
|
66
|
+
| ------------ | ------------------------------------------------------------------------------- |
|
|
67
|
+
| `externalId` | 条目在本插件内的唯一标识,必填,最长 512 字 |
|
|
68
|
+
| `quote` | 摘录原文,必填,最长 20000 字 |
|
|
69
|
+
| `note` | 可选。用户写的想法或批注 |
|
|
70
|
+
| `location` | 可选。给人看的位置,如「第 3 章 · 标题」「第 12 页」,最长 500 字 |
|
|
71
|
+
| `url` | 可选。条目级的 http(s) 深链,优先于来源的 `url` |
|
|
72
|
+
| `anchor` | 可选。插件自己的定位数据,JSON 对象,序列化后不超过 8000 字;Tau 只保存,不解读 |
|
|
73
|
+
| `color` | 可选。划线颜色,最长 50 字 |
|
|
74
|
+
| `time` | 可选。在原服务中的创建时间(毫秒时间戳) |
|
|
75
|
+
|
|
76
|
+
### externalId 要稳定
|
|
77
|
+
|
|
78
|
+
Tau 中对一条摘录的引用,靠 `externalId` 找到它。只要 `externalId` 不变,引用就一直有效。
|
|
79
|
+
|
|
80
|
+
- 用外部数据里稳定的标识生成 `externalId`,例如原服务的笔记 ID。改变生成规则,会让旧摘录被移除、用户的引用失效。
|
|
81
|
+
- 被移除的摘录不会真正删除:原文仍然保留,引用仍然能显示。以后同一个 `externalId` 再次写入时,它会恢复。
|
|
82
|
+
|
|
83
|
+
### 限制
|
|
84
|
+
|
|
85
|
+
- 单次写入最多 20000 条。同一批中 `externalId` 不能重复,同一次 `patch` 不能同时写入和移除同一条。
|
|
86
|
+
- 任何一条不合法时,整批拒绝。
|
|
87
|
+
- 外部服务分页返回时,确认拿到完整列表后再调用 `replace`。用不完整的结果覆盖,会误移除条目。
|
|
88
|
+
|
|
89
|
+
## 读回
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const sources = await context.excerpts.sources(); // [{ key, title, count }]
|
|
93
|
+
const entries = await context.excerpts.items("book-42");
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- `sources()` 列出本插件写入过的来源,`count` 是未移除的条目数。
|
|
97
|
+
- `items(sourceKey)` 返回该来源的全部条目,包括已移除的(`deleted: true`),可以用来离线展示,或与外部数据比对。
|
|
98
|
+
- 每个条目带有 `ref`,用于下面的材料动作。
|
|
99
|
+
|
|
100
|
+
## 材料动作
|
|
101
|
+
|
|
102
|
+
摘录等内容在 Tau 中统称**材料**。`context.materials.actions(ref)` 返回 Tau 当前能对这条材料执行的操作,例如「转为卡片」「转为问题」,适合放在插件自己的列表或详情页里:
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
import { useEffect, useState } from "react";
|
|
106
|
+
import { Button } from "@tau/ui";
|
|
107
|
+
import type { PluginContext, PluginMaterialAction, PluginMaterialRef } from "@tau/plugins";
|
|
108
|
+
|
|
109
|
+
function NoteActions({ context, target }: { context: PluginContext; target: PluginMaterialRef }) {
|
|
110
|
+
const [actions, setActions] = useState<PluginMaterialAction[]>([]);
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
void context.materials.actions(target).then(list => {
|
|
113
|
+
if (!context.signal.aborted) setActions(list);
|
|
114
|
+
});
|
|
115
|
+
}, [context, target]);
|
|
116
|
+
return actions.map(action => (
|
|
117
|
+
<Button key={action.id} variant="ghost" onClick={() => void action.run()}>
|
|
118
|
+
{action.label}
|
|
119
|
+
</Button>
|
|
120
|
+
));
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- `ref` 来自 `excerpts.items()` 返回的条目,原样传回即可,不要自己构造或解析。
|
|
125
|
+
- 直接用 `label` 作为按钮文字,不要按 `id` 写死列表,Tau 以后可能增加新的动作。
|
|
126
|
+
- 动作执行后由 Tau 给出成功或失败的提示,插件不需要再提示。
|