@xiongtao.xt/tau-plugins 0.31.1 → 0.33.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +2 -2
  3. package/client/{chunk-ECEBBIHP.js → chunk-SVY5LBAT.js} +44 -0
  4. package/docs/README.md +43 -0
  5. package/docs/ai.md +169 -0
  6. package/docs/backend.md +168 -0
  7. package/docs/editor.md +590 -0
  8. package/docs/excerpts.md +126 -0
  9. package/docs/flashcards.md +127 -0
  10. package/docs/getting-started.md +134 -0
  11. package/docs/mcp.md +173 -0
  12. package/docs/plugin-basics.md +209 -0
  13. package/docs/providers/README.md +108 -0
  14. package/docs/providers/images.md +65 -0
  15. package/docs/providers/publish.md +144 -0
  16. package/docs/providers/sync.md +75 -0
  17. package/docs/providers/tts.md +62 -0
  18. package/docs/providers/upload.md +77 -0
  19. package/docs/providers/video.md +118 -0
  20. package/docs/release.md +130 -0
  21. package/docs/services.md +159 -0
  22. package/docs/site-data.md +90 -0
  23. package/docs/skills.md +129 -0
  24. package/docs/storage-and-network.md +62 -0
  25. package/docs/ui.md +336 -0
  26. package/docs/user-data.md +389 -0
  27. package/docs/whiteboard.md +290 -0
  28. package/package.js +1 -1
  29. package/package.json +1 -1
  30. package/publish-artifacts.js +38 -1
  31. package/types/packages/db-schema/content-resources.d.ts +76 -0
  32. package/types/packages/editor/contracts.d.ts +1 -1
  33. package/types/packages/editor/source-contracts.d.ts +41 -1
  34. package/types/packages/editor/src/core/api/extension.d.ts +15 -0
  35. package/types/packages/editor/src/core/api/index.d.ts +5 -1
  36. package/types/packages/editor/src/core/api/resources.d.ts +6 -0
  37. package/types/packages/editor/src/core/components/ElementFrame/index.d.ts +25 -0
  38. package/types/packages/editor/src/core/utils/blockKeyTarget.d.ts +9 -0
  39. package/types/packages/plugins/src/categories.d.ts +3 -0
  40. package/types/packages/plugins/src/contract.d.ts +0 -2
  41. package/types/packages/plugins/src/editor.d.ts +1 -0
  42. package/types/packages/plugins/src/element.d.ts +11 -0
  43. package/types/packages/plugins/src/index.d.ts +4 -1
  44. package/types/packages/plugins/src/media.d.ts +69 -0
  45. package/types/packages/plugins/src/package.d.ts +2 -0
  46. package/types/packages/ui/src/index.d.ts +1 -0
  47. package/types/packages/ui/src/primitives/Sortable.d.ts +32 -0
  48. package/types/packages/ui/src/primitives/Tabs.d.ts +1 -3
  49. package/AI.md +0 -116
  50. package/API.md +0 -881
  51. package/BACKEND.md +0 -68
  52. package/DEVELOPMENT.md +0 -173
  53. package/EDITOR.md +0 -192
  54. package/FLASHCARDS.md +0 -92
  55. package/IMAGES.md +0 -16
  56. package/MCP.md +0 -118
  57. package/NOTIFICATIONS.md +0 -48
  58. package/PUBLISH-DATA.md +0 -48
  59. package/PUBLISH.md +0 -50
  60. package/SERVICES.md +0 -119
  61. package/SYNC.md +0 -37
  62. package/TTS.md +0 -20
  63. package/UPLOAD.md +0 -35
  64. package/VIDEO.md +0 -65
  65. package/WHITEBOARD.md +0 -115
  66. package/types/packages/plugins/src/skills.d.ts +0 -8
package/docs/editor.md ADDED
@@ -0,0 +1,590 @@
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
+ | `resources` | 可选。节点里哪些字段是图片、视频、音频或文件,见[声明资源字段](#声明资源字段) |
138
+
139
+ ### 渲染组件
140
+
141
+ `render` 组件收到三个属性:`attributes`、`element`、`children`。
142
+
143
+ ```tsx
144
+ render: ({ attributes, element, children }) => (
145
+ <div {...attributes}>
146
+ <MyWidget data={element} />
147
+ {children}
148
+ </div>
149
+ );
150
+ ```
151
+
152
+ - `attributes` 必须展开到最外层元素上,`children` 必须渲染出来。即使是 `isVoid` 节点也一样,这是编辑器追踪光标所需要的。
153
+ - 组件里不可编辑的部分加上 `contentEditable={false}`。
154
+ - 组件里的按钮在 `onMouseDown` 中调用 `event.preventDefault()`,避免点击时编辑器失去光标。
155
+
156
+ ### 修改节点字段
157
+
158
+ 组件里需要修改节点数据(例如保存用户调整后的参数)时,使用 `useElement` 返回的 `update`:
159
+
160
+ ```tsx
161
+ import { useElement } from "@tau/plugins";
162
+
163
+ function Counter({ element }: { element: CounterElement }) {
164
+ const { update } = useElement(element);
165
+ return (
166
+ <button
167
+ onMouseDown={event => event.preventDefault()}
168
+ onClick={() => update({ initial: element.initial + 1 })}
169
+ >
170
+ 计数:{element.initial}
171
+ </button>
172
+ );
173
+ }
174
+ ```
175
+
176
+ `update` 把传入的字段合并进节点,这次修改会进入撤销记录并同步给协作者。
177
+
178
+ `useElement(element)` 返回:
179
+
180
+ | 字段 | 说明 |
181
+ | -------------------- | ------------------------------------------------------------------ |
182
+ | `readOnly` | 编辑器是否只读。只读时 `update`、`remove` 不会写入 |
183
+ | `update(properties)` | 把字段合并进节点 |
184
+ | `remove()` | 删除节点 |
185
+ | `selected` | 声明了[外框](#块的外框)的块是否被点选 |
186
+ | `select()` | 选中外框里的块 |
187
+ | `openMenu(position)` | 在指定位置(视口坐标)打开块菜单,条目与右键、块手柄相同;需要外框 |
188
+
189
+ 请始终传入当前渲染拿到的 `element`。节点被替换或组件卸载后,旧对象上的写入会被忽略,不会改到别的块。
190
+
191
+ ### 块的外框
192
+
193
+ 图表、画廊这类整块由组件渲染的节点(`isVoid: true` 的块),在节点声明上加 `frame` 即可得到 Tau 块的公共交互:点击选中并描边、点到别处取消、右键打开块菜单、选中时 Delete / Backspace 删除块并留下空段落、Esc 取消选中,以及块末尾的「添加段落」。
194
+
195
+ ```tsx
196
+ defineEditorElement({
197
+ type: "yourname.chart",
198
+ kind: "block",
199
+ isVoid: true,
200
+ frame: { keys: { Enter: "yourname.chart.edit" } },
201
+ render: Chart,
202
+ });
203
+ ```
204
+
205
+ | 字段 | 说明 |
206
+ | ----------------- | ------------------------------------------------------------------- |
207
+ | `keys` | 可选。选中时的按键对应的命令 id,命令收到这个节点(同菜单里的命令) |
208
+ | `appendParagraph` | 可选。为 `false` 时不显示末尾的「添加段落」 |
209
+
210
+ 声明外框后,`render` 只渲染内容:收到的 `attributes` 是空对象、`children` 为空,交给外框处理。组件里用 `useElement(element).selected` 决定是否显示属性条。
211
+
212
+ ### 块的属性条
213
+
214
+ 选中块后浮在块上方的属性条,用 `BlockToolbar`、`BlockToolbarButton`、`BlockToolbarGap` 组成,外观和 Tau 自带的图片块一致。上方放不下时自动翻到下方,点击按钮不会让编辑器失去光标。
215
+
216
+ ```tsx
217
+ import { BlockToolbar, BlockToolbarButton, BlockToolbarGap, useElement } from "@tau/plugins";
218
+
219
+ function Chart({ element }: { element: ChartElement }) {
220
+ const { selected, readOnly, update, openMenu } = useElement(element);
221
+ return (
222
+ <div className="relative">
223
+ {selected && !readOnly && (
224
+ <BlockToolbar label="图表属性">
225
+ <BlockToolbarButton
226
+ title="柱状图"
227
+ active={element.kind === "bar"}
228
+ icon={<BarChart3 size={15} />}
229
+ onClick={() => update({ kind: "bar" })}
230
+ />
231
+ <BlockToolbarGap />
232
+ <BlockToolbarButton
233
+ title="更多"
234
+ icon={<MoreHorizontal size={15} />}
235
+ onClick={event => {
236
+ const rect = event.currentTarget.getBoundingClientRect();
237
+ openMenu({ x: rect.left, y: rect.bottom + 6 });
238
+ }}
239
+ />
240
+ </BlockToolbar>
241
+ )}
242
+ <ChartView data={element.data} />
243
+ </div>
244
+ );
245
+ }
246
+ ```
247
+
248
+ `BlockToolbarButton` 的 `title` 是按钮名称(纯图标按钮悬停时显示),`label` 是显示在图标旁的文字,`shortcut` 在提示里显示快捷键,`active` 表示当前选中的选项。
249
+
250
+ ## 扩展能声明的内容
251
+
252
+ `defineEditorExtension` 接收:
253
+
254
+ | 字段 | 说明 |
255
+ | ----------------- | ----------------------------------------------------------------------- |
256
+ | `id` | 扩展 ID,全局唯一 |
257
+ | `elements` | 节点声明,至少一个 |
258
+ | `blockPanel` | 块菜单(`/` 菜单)中的条目 |
259
+ | `commands` | 编辑器命令 |
260
+ | `hotkeys` | 快捷键,绑定到命令 |
261
+ | `menu` | 块的手柄菜单和右键菜单中的条目 |
262
+ | `behaviorsFor` | 挂在其他节点类型上的行为,用来实现快捷语法 |
263
+ | `styleSources` | 节点里的 Tailwind 源码字段,见[动态 Tailwind 样式](#动态-tailwind-样式) |
264
+ | `htmlSerializers` | 静态 HTML 导出,见 [HTML 导出](#html-导出) |
265
+
266
+ ### 块菜单
267
+
268
+ `blockPanel` 的每一项会出现在用户输入 `/` 后弹出的菜单里:
269
+
270
+ | 字段 | 说明 |
271
+ | ------------- | -------------------------------------------- |
272
+ | `icon` | 图标组件,接收 `className` |
273
+ | `title` | 标题 |
274
+ | `description` | 说明 |
275
+ | `keywords` | 搜索关键词,用户输入这些词时会匹配到这一项 |
276
+ | `onClick` | 选中时调用,收到当前编辑器,通常用来插入节点 |
277
+ | `children` | 可选。子菜单 |
278
+
279
+ `onClick` 收到的 `editor` 提供 `insertNode`、`setNodes`、`removeNodes` 等编辑器方法。插入后需要立即编辑的源码类块,用 `insertSourceBlock`,见[源码预览块](#源码预览块)。
280
+
281
+ `blockPanel` 也可以是一个函数 `(editor) => items`,每次打开菜单时求值。
282
+
283
+ ### 命令与快捷键
284
+
285
+ 命令是编辑器里的动作,快捷键和菜单都通过命令 ID 触发它:
286
+
287
+ ```tsx
288
+ defineEditorExtension({
289
+ id: "yourname.counter",
290
+ elements: [/* ... */],
291
+ commands: [
292
+ {
293
+ id: "yourname.counter.reset",
294
+ title: "重置计数",
295
+ run: (editor, { path }) => {
296
+ if (path) editor.setNodes({ initial: 0 }, { at: path });
297
+ },
298
+ },
299
+ ],
300
+ hotkeys: [{ key: "mod+shift+0", command: "yourname.counter.reset" }],
301
+ menu: [{ id: "yourname.counter.reset", title: "重置计数", command: "yourname.counter.reset" }],
302
+ });
303
+ ```
304
+
305
+ | 命令字段 | 说明 |
306
+ | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
307
+ | `id` | 命令 ID,全局唯一,建议以扩展 ID 作前缀 |
308
+ | `title` | 可选。显示名称 |
309
+ | `when` | 可选。`(editor) => boolean`,返回 `false` 时快捷键跳过、菜单项变灰 |
310
+ | `run` | `(editor, context) => void`。`context` 包含触发它的 `event`、调用方的 `payload`,从菜单触发时还有目标节点 `element` 和它的 `path` |
311
+
312
+ | 快捷键字段 | 说明 |
313
+ | ---------- | -------------------------------------------------------------------------------- |
314
+ | `key` | 按键组合,如 `"mod+shift+k"`,`mod` 在 macOS 上是 ⌘,其他系统是 Ctrl;可以传数组 |
315
+ | `command` | 要执行的命令 ID |
316
+ | `fallback` | 可选。为 `true` 时只在其他扩展的快捷键都没有处理时才尝试 |
317
+
318
+ 命令执行出错时,Tau 会隔离错误,不影响编辑器的其他功能。异步命令返回 Promise 即可。
319
+
320
+ ### 菜单
321
+
322
+ `menu` 中的条目出现在你的块的手柄菜单和右键菜单里:
323
+
324
+ | 字段 | 说明 |
325
+ | ---------------- | ----------------------------------------------------- |
326
+ | `id` | 条目 ID,同一份菜单内唯一 |
327
+ | `title` | 标题,可以是函数 `(ctx) => string`,打开菜单时求值 |
328
+ | `icon` | 可选。图标,同样可以是函数 |
329
+ | `command` | 点击时执行的命令 ID;只有子菜单的父条目可以省略 |
330
+ | `payload` | 可选。传给命令的参数,命令在 `context.payload` 中读取 |
331
+ | `when` | 可选。返回 `false` 时不显示这一项 |
332
+ | `disabled` | 可选。返回 `true` 时这一项变灰 |
333
+ | `group`、`order` | 可选。分组与排序 |
334
+ | `danger` | 可选。危险操作,以警示色显示 |
335
+ | `children` | 可选。子菜单 |
336
+
337
+ 函数形式的字段收到 `{ editor, element, path }`,即菜单针对的块。
338
+
339
+ ### 行为
340
+
341
+ `behaviors` 写在节点声明里,在光标位于该节点内时接管按键:
342
+
343
+ | 行为 | 时机 |
344
+ | -------------------------------------------- | ------------------ |
345
+ | `insertBreak(editor, [node, path])` | 按回车 |
346
+ | `deleteBackward(editor, [node, path], unit)` | 按退格 |
347
+ | `insertText(editor, [node, path], text)` | 输入文字 |
348
+ | `normalize(editor, [node, path])` | 编辑器规范化节点时 |
349
+
350
+ 每个函数返回 `true` 表示已经处理,返回 `false` 交给外层节点和默认行为。
351
+
352
+ `behaviorsFor` 写在扩展上,把行为挂到**其他**节点类型上。典型用途是快捷语法:用户在段落里输入某个标记后回车,把段落替换成你的块。
353
+
354
+ ```ts
355
+ behaviorsFor: {
356
+ paragraph: {
357
+ insertBreak: (editor, [node, path]) => {
358
+ // 检查段落文字,满足条件时替换成自己的节点并返回 true
359
+ return false;
360
+ },
361
+ },
362
+ },
363
+ ```
364
+
365
+ ## 块会在哪里显示
366
+
367
+ 你的块会出现在两个地方:
368
+
369
+ - **Tau 桌面端**的编辑器。
370
+ - **用户发布的博客**。博客使用同一个 `render` 组件,你不需要另写一份。
371
+
372
+ 移动端和浏览器扩展不运行插件,遇到你的块时保留数据,并显示一个通用的占位。
373
+
374
+ 因为同一份代码要在博客里运行,渲染相关的代码需要满足:
375
+
376
+ - 模块顶层和渲染组件只使用浏览器能力,不导入 Node.js 模块。
377
+ - 不依赖 `activate` 里才赋值的变量,也不使用 `context`(存储、页面、服务等只在桌面端的插件运行时里存在)。需要编译服务时,用 [`usePluginServices`](services.md#在编辑器块中使用服务),并处理拿不到服务的情况。
378
+ - JS 和 CSS 自包含:React、`@tau/plugins`、`@tau/ui` 由 Tau 提供,其他依赖会被打进 `main.js`;图片等资源内联。
379
+ - 样式用插件自己的类名前缀,不修改 `body` 或 Tau 自身元素的样式。
380
+
381
+ 博客里的文档是只读的,但组件自身的交互(按钮、缩放图表)照常可用;在只读文档里修改节点字段不会被保存。
382
+
383
+ ## 停用与卸载
384
+
385
+ - 插件停用后,文档里已有的块会保留,显示为「不可用」的占位,不会被删除;重新启用后恢复显示。
386
+ - 卸载正在使用的编辑器插件前,需要先停用并重启 Tau。
387
+
388
+ ## Markdown 导入导出
389
+
390
+ 在插件定义上提供 `editorCodecs`,你的块就能参与 Markdown 导入导出、复制为 Markdown 和搜索摘要。没有 codec 的块在导出 Markdown 时会以 Tau 的扩展格式保留,其他工具未必认识。
391
+
392
+ ```ts
393
+ export default definePlugin({
394
+ ...__PLUGIN_META__,
395
+ editorExtensions: [counterExtension],
396
+ editorCodecs: [
397
+ {
398
+ type: "yourname.counter",
399
+ isVoid: true,
400
+ // 导出:节点 → Markdown
401
+ serializer: { toMarkdown: element => `::yourname-counter{initial=${element.initial}}` },
402
+ // 导入:Markdown → 节点
403
+ directives: {
404
+ "yourname-counter": attributes => ({
405
+ type: "yourname.counter",
406
+ initial: Number(attributes.initial) || 0,
407
+ children: [{ type: "formatted", text: "" }],
408
+ }),
409
+ },
410
+ },
411
+ ],
412
+ });
413
+ ```
414
+
415
+ | 字段 | 说明 |
416
+ | ------------ | ------------------------------------------------------------------------------------------------------- |
417
+ | `type` | 对应的节点类型,只能是本插件声明的类型 |
418
+ | `serializer` | 导出。`toMarkdown(element, childrenMarkdown)` 返回 Markdown 字符串 |
419
+ | `directives` | 导入。按 directive 名(`::name{...}`、`:::name`、`:name[...]`)解析成节点,返回 `null` 表示丢弃 |
420
+ | `fence` | 导入。把指定语言的代码围栏(如 `mermaid` 代码块)认领为节点,源码放进 `property` 字段(默认 `content`) |
421
+ | `isVoid` | 节点是 void 时设为 `true` |
422
+
423
+ - 导入和导出要成对设计:只写 `serializer` 不会自动获得导入能力。
424
+ - codec 会在没有界面的环境中单独加载,必须是纯函数。模块顶层不要访问 `window`、`document`。
425
+
426
+ ## 源码预览块
427
+
428
+ 很多块的模式是"上面编辑源码,下面显示预览",比如图表、公式、组件演示。`SourceBlockEditor` 提供了 Tau 统一的源码预览外壳:源码编辑、全屏、错误提示、删除,以及与相邻块之间的光标移动。
429
+
430
+ ```tsx
431
+ import type { ReactNode } from "react";
432
+ import { SourceBlockEditor, useSourceBlock } from "@tau/plugins";
433
+ import { Code } from "lucide-react";
434
+
435
+ interface DiagramElement {
436
+ type: "yourname.diagram";
437
+ content: string;
438
+ children: { type: "formatted"; text: string }[];
439
+ }
440
+
441
+ function DiagramBlock({ element, children }: { element: DiagramElement; children: ReactNode }) {
442
+ const { update } = useSourceBlock(element);
443
+ return (
444
+ <SourceBlockEditor
445
+ element={element}
446
+ mode="jsx"
447
+ title="图表"
448
+ icon={Code}
449
+ initValue={element.content}
450
+ onChange={content => update({ content })}
451
+ >
452
+ <DiagramPreview source={element.content} />
453
+ {children}
454
+ </SourceBlockEditor>
455
+ );
456
+ }
457
+ ```
458
+
459
+ 在节点的 `render` 里使用 `DiagramBlock`。`icon` 需要一个 lucide-react 图标,先 `pnpm add lucide-react`。
460
+
461
+ | `SourceBlockEditor` 属性 | 说明 |
462
+ | ------------------------ | ---------------------------------------- |
463
+ | `element` | 当前节点 |
464
+ | `mode` | 源码编辑器的语言模式,如 `jsx`、`html` |
465
+ | `title`、`icon` | 块的标题和图标 |
466
+ | `initValue` | 源码初始值 |
467
+ | `onChange` | 源码变化时调用,通常用 `update` 写回节点 |
468
+ | `center`、`extend` | 可选。预览居中、预览区域扩展到更宽 |
469
+ | `children` | 预览内容 |
470
+
471
+ 相关工具:
472
+
473
+ | 接口 | 用途 |
474
+ | ------------------------------------------- | -------------------------------------------------------------------------------------------- |
475
+ | `useSourceBlock(element)` | 返回 `update`(修改节点字段)、`theme`(当前主题)和 `compiler`(Tailwind 编译器,可能为空) |
476
+ | `useReportPreviewError(message, { stale })` | 预览出错时上报错误,传 `null` 清除;`stale` 表示是否保留了上一次成功的预览 |
477
+ | `insertSourceBlock(editor, element, line?)` | 插入节点并把光标放进源码编辑器,适合在块菜单的 `onClick` 中使用 |
478
+ | `sourceBlockParagraphBehavior(type)` | 放进 `behaviorsFor.paragraph`,让用户在段首按退格时进入上方的源码块 |
479
+
480
+ `theme.isDark` 表示当前是否为暗色主题,`theme.token(word)` 读取主题色值,预览应随主题变化。`compiler` 只在桌面编辑器中可用,在博客里为空,此时配合下文的 `styleSources` 使用预先生成的样式。
481
+
482
+ 不要在插件里打包自己的编辑器、Slate 或源码编辑器,直接使用这些组件。
483
+
484
+ ## 行内源码
485
+
486
+ 行内的源码节点(如行内公式)使用另一组接口:
487
+
488
+ | 接口 | 用途 |
489
+ | ------------------------------------- | ------------------------------------------------------------- |
490
+ | `useInlineSource(element)` | 返回 `readOnly`、`compiler`、`update(properties)`、`remove()` |
491
+ | `insertInlineSource(editor, element)` | 插入行内节点,适合在菜单中使用 |
492
+ | `InlineSourceBoundary` | 渲染在行内节点组件里,处理光标进出节点的边界 |
493
+
494
+ 行内的源码输入弹层由你用 `@tau/ui` 组件实现。只读时只显示预览,不打开编辑弹层;只读状态下 `update` 和 `remove` 不会写入文档。
495
+
496
+ ## 图片、视频与音频
497
+
498
+ 节点里存的图片、视频、音频地址可能是本地路径、远程地址或共享文档里的资源,不能直接交给 `<img>`。用宿主的媒体组件显示,解析方式与 Tau 自带的图片块相同:本地路径转成可加载的地址,远程资源按用户的缓存设置下载,共享文档里的资源经协作服务解析。
499
+
500
+ ```tsx
501
+ import { MediaImage, useMediaAppearance } from "@tau/plugins";
502
+
503
+ function Photo({ element }: { element: PhotoElement }) {
504
+ const appearance = useMediaAppearance();
505
+ return (
506
+ <figure contentEditable={false} data-radius={element.borderRadius ?? appearance?.borderRadius}>
507
+ <MediaImage url={element.url} alt={element.caption} />
508
+ </figure>
509
+ );
510
+ }
511
+ ```
512
+
513
+ | 接口 | 用途 |
514
+ | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
515
+ | `MediaImage` | 显示图片,参数为 `url`、`alt`、`className`、`style`、`loading`、`onLoadError` 等 |
516
+ | `MediaVideo` / `MediaAudio` | 宿主的视频、音频播放器,参数为 `src` 与原生媒体属性 |
517
+ | `useResource(url, { kind })` | 自己绘制时使用:返回 `src`、`status`(`loading` / `ready` / `error`)与 `retry()`,元素加载失败时调用 `retry` 重新解析 |
518
+ | `useResourceUpload()` | 返回上传函数,把用户选的文件存进当前的资源存储并返回可写入节点的地址;只读编辑器和博客里为 `undefined` |
519
+ | `useMediaAppearance()` | 用户设置的媒体默认圆角与阴影(`none` / `sm` / `md` / `lg`);博客里取发布设置。节点自己有设置时以节点为准 |
520
+
521
+ `kind` 取 `image`、`video`、`audio`、`file`,决定远程资源的缓存方式。上传的文件不需要自己判断本地还是远端存储,也不要把文件内容写进节点。
522
+
523
+ ### 查看图片
524
+
525
+ `useMediaViewer(element)` 返回一个函数,调用后打开 Tau 的图片查看器,浏览文档里的全部图片,并定位到这个节点的第 `index` 张(按[声明的资源字段](#声明资源字段)计,从 0 开始)。
526
+
527
+ ```tsx
528
+ const openViewer = useMediaViewer(element);
529
+ <MediaImage url={item.url} onDoubleClick={() => openViewer(index)} />;
530
+ ```
531
+
532
+ ### 声明资源字段
533
+
534
+ 节点里存了图片、视频、音频或文件地址时,在节点声明上用 `resources` 写明是哪些字段。Tau 只按这份声明找资源:导出打包、共享给协作者时上传本地文件、灯箱里浏览图片、EPUB 取图,都靠它。没有声明的字段按普通文字原样保留,本地图片不会随内容导出,也不会上传给协作者。
535
+
536
+ ```tsx
537
+ defineEditorElement({
538
+ type: "yourname.gallery",
539
+ kind: "block",
540
+ isVoid: true,
541
+ render: Gallery,
542
+ resources: [{ path: "images[].url", kind: "image", alt: "desc", caption: "desc" }],
543
+ });
544
+ ```
545
+
546
+ | 字段 | 说明 |
547
+ | --------- | ---------------------------------------------------------------------------------------- |
548
+ | `path` | 资源字段的路径。`url` 是节点上的字段,`images[].url` 表示 `images` 数组每一项的 `url` |
549
+ | `kind` | `image`、`video`、`audio` 或 `file` |
550
+ | `preview` | 可选。同一资源的显示版本(如裁剪结果)所在的字段,与 `path` 末段同层,同样会被导出和上传 |
551
+ | `alt` | 可选。替代文字所在的字段,与 `path` 末段同层,灯箱里显示 |
552
+ | `caption` | 可选。说明文字所在的字段,与 `path` 末段同层,灯箱里显示 |
553
+
554
+ 声明会随笔记库保存,插件停用或在没装插件的设备上,这些资源照样能导出和共享。已经有文档用了的字段不要改名;新增资源字段时补进声明即可。
555
+
556
+ ## 动态 Tailwind 样式
557
+
558
+ 如果节点的源码里写了 Tailwind 类名(比如用户写的组件演示),在扩展上声明哪个字段是源码:
559
+
560
+ ```ts
561
+ defineEditorExtension({
562
+ id: "yourname.demo",
563
+ elements: [/* ... */],
564
+ styleSources: [{ type: "yourname.demo", field: "content", language: "tsx" }],
565
+ });
566
+ ```
567
+
568
+ 在博客等不能实时编译样式的地方,Tau 会读取这些字段,提前生成对应的样式。
569
+
570
+ - 字段的值必须是字符串,`language` 为 `html` 或 `tsx`,只能登记本插件的节点类型。
571
+ - 类名要完整地写在源码里。拼接出来的类名(如 `` `bg-${color}` ``)扫描不到。
572
+
573
+ ## HTML 导出
574
+
575
+ 用户把文档导出为 HTML、EPUB 或复制为 HTML 时,你的块默认按未知节点处理,交互组件不会执行。需要更好的导出效果时,提供 `htmlSerializers`:
576
+
577
+ ```ts
578
+ defineEditorExtension({
579
+ id: "yourname.counter",
580
+ elements: [/* ... */],
581
+ htmlSerializers: [
582
+ {
583
+ type: "yourname.counter",
584
+ toHtml: element => `<p>计数:${Number(element.initial)}</p>`,
585
+ },
586
+ ],
587
+ });
588
+ ```
589
+
590
+ `toHtml` 返回 HTML 字符串,文本内容要自己转义。它只用于静态导出,博客里的显示仍然使用 `render`。