@4399ywkf/editor 0.1.2 → 0.2.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.
@@ -0,0 +1,325 @@
1
+ import { JSONContent, Editor } from '@tiptap/core';
2
+ import { Node } from '@tiptap/pm/model';
3
+
4
+ /**
5
+ * LiteXML —— 给 AI 看的文档表示。
6
+ *
7
+ * 设计原则(来自实测与业界先例):
8
+ * 1. 比 ProseMirror JSON 省 token(Notion 换成 markdown、Tiptap 做 Shorthand,
9
+ * 理由都是这个)。实测 7 块中文文档:LiteXML 322 token / PM JSON minified 499
10
+ * (1.55x)/ pretty 898(2.79x)。1.55x 是下限 —— 中文正文占比高时对 JSON 最有利,
11
+ * 嵌套列表和表格会把差距成倍拉大;
12
+ * 2. 每个可寻址块带稳定 id,AI 只用 id 定位,绝不用 position;
13
+ * 3. **有损但不丢失**:表达不了的节点降级成不透明占位符 `<node type="..."/>`,
14
+ * 而不是抛错;表达不了的属性靠写入侧 attrs 合并保住(见 mergeParsedNode)。
15
+ *
16
+ * 本模块是**纯数据变换**:进 ProseMirror JSON 出字符串,或者反过来。
17
+ * 不碰 Editor 实例、不碰 DOM(XML 解析器由调用方注入,同 docx/parse.ts 的做法),
18
+ * 所以可以在 Node 里直接跑往返测试。
19
+ */
20
+
21
+ /**
22
+ * 节点类型 → 标签。不在表里的走 `<node type="..."/>` 兜底。
23
+ *
24
+ * 这张表镜像的是**本编辑器自己的 schema**(30 个节点类型)。它必须和
25
+ * components/tiptap-node/ 下的节点定义住在同一个包里 —— 跨仓之后每加一个节点
26
+ * 类型,序列化就静默降级成占位符,不报错、不丢数据、也不告诉你。
27
+ */
28
+ declare const NODE_TAG: Record<string, string>;
29
+ /** 叶子节点:自闭合,没有子内容。AI 不应往里写内容。 */
30
+ declare const VOID_TAGS: ReadonlySet<string>;
31
+ /** 只读节点:由扩展自动维护,AI 不许直接写。 */
32
+ declare const READONLY_TAGS: ReadonlySet<string>;
33
+ declare const MARK_TAG: Record<string, string>;
34
+ /** 可被 formatText 施加/移除的标记。 */
35
+ declare const FORMATTABLE: string[];
36
+ type FormattableMark = "bold" | "italic" | "underline" | "strike" | "code" | "superscript" | "subscript";
37
+ declare function nodeToXml(node: JSONContent, depth?: number): string;
38
+ /** 把一整份 ProseMirror doc JSON 序列化成 LiteXML。 */
39
+ declare function docToXml(doc: JSONContent): string;
40
+ /**
41
+ * XML 解析器由调用方注入 —— 浏览器传原生 `DOMParser`,Node 传 `@xmldom/xmldom` 的。
42
+ * 同 `docx/parse.ts` 的做法,目的一样:让这层能在 Node 里跑测试。
43
+ * 省略时自动取 `globalThis.DOMParser`,所以浏览器侧零配置。
44
+ */
45
+ interface LiteXmlParseOptions {
46
+ DOMParser?: {
47
+ new (): {
48
+ parseFromString(source: string, mimeType: string): Document;
49
+ };
50
+ };
51
+ }
52
+ declare function xmlElToPm(el: Element): JSONContent;
53
+ /** 把一段 LiteXML 片段(可以是多个平级块)解析成 ProseMirror JSON 数组。 */
54
+ declare function parseXmlFragment(xml: string, opts?: LiteXmlParseOptions): JSONContent[];
55
+ /**
56
+ * LiteXML 是**有损投影**。直接用解析结果整块替换会把它表达不了的属性静默抹掉
57
+ * (= 「AI 改一个词顺手毁排版」)。所以 modify 一律以原节点 attrs 为底做合并,
58
+ * 只让 XML 里真的出现过的键覆盖。
59
+ *
60
+ * 已验证的用例:对带 `lang="typescript"` 的代码块用不带 lang 的 XML 做 modify,
61
+ * 内容改了、lang 保住。
62
+ */
63
+ declare function mergeParsedNode(originalAttrs: Record<string, unknown>, parsed: JSONContent): JSONContent;
64
+
65
+ /**
66
+ * doc-runtime 的操作层:一组 `(editor, args) => result` 的函数。
67
+ *
68
+ * 纪律:
69
+ * - **寻址在执行的那一瞬间才解析**(每个函数自己现查 id → pos),所以没有
70
+ * 「写后索引失效」—— 调用方不需要倒序处理,也不需要每改一处就重读一遍。
71
+ * 这条同时写进了工具 description,因为模型带着 Google Docs / 腾讯文档那套
72
+ * 条件反射,不明说它会自作聪明。
73
+ * - 这层**不碰 DOM、不做高亮**。要闪烁提示的话,看返回值里的 id 自己去做
74
+ * (DocRuntime 就是这么干的)—— 操作层保持可测。
75
+ */
76
+
77
+ interface NodeHit {
78
+ node: Node;
79
+ pos: number;
80
+ }
81
+ declare function findById(editor: Editor, id: string): NodeHit | null;
82
+ type ModifyOperation = {
83
+ action: "insert";
84
+ afterId: string;
85
+ litexml: string;
86
+ beforeId?: never;
87
+ id?: never;
88
+ } | {
89
+ action: "insert";
90
+ beforeId: string;
91
+ litexml: string;
92
+ afterId?: never;
93
+ id?: never;
94
+ } | {
95
+ action: "remove";
96
+ id: string;
97
+ litexml?: never;
98
+ afterId?: never;
99
+ beforeId?: never;
100
+ } | {
101
+ action: "modify";
102
+ litexml: string | string[];
103
+ id?: never;
104
+ afterId?: never;
105
+ beforeId?: never;
106
+ };
107
+ interface ModifyResultItem {
108
+ action: string;
109
+ success: boolean;
110
+ error?: string;
111
+ id?: string;
112
+ anchor?: string;
113
+ count?: number;
114
+ modifiedIds?: string[];
115
+ }
116
+ interface ModifyNodesResult {
117
+ results: ModifyResultItem[];
118
+ successCount: number;
119
+ totalCount: number;
120
+ /** 本批实际改动到的块 id,调用方可拿去做高亮。 */
121
+ touchedIds: string[];
122
+ }
123
+ declare function applyOperations(editor: Editor, operations: ModifyOperation[], parseOpts?: LiteXmlParseOptions): ModifyNodesResult;
124
+ interface ReplaceTextArgs {
125
+ searchText: string;
126
+ newText: string;
127
+ /** 限定生效范围;省略则全文 */
128
+ nodeIds?: string[];
129
+ /** 默认 false,只替换第一处 */
130
+ replaceAll?: boolean;
131
+ }
132
+ interface ReplaceTextResult {
133
+ replacementCount: number;
134
+ modifiedNodeIds: string[];
135
+ }
136
+ declare function replaceText(editor: Editor, { searchText, newText, nodeIds, replaceAll }: ReplaceTextArgs): ReplaceTextResult;
137
+ interface FormatTextArgs {
138
+ nodeId: string;
139
+ searchText: string;
140
+ marks: FormattableMark[];
141
+ /** true 则移除这些标记而不是施加,默认 false */
142
+ remove?: boolean;
143
+ /** 块内第几处匹配,从 1 开始,默认 1 */
144
+ occurrence?: number;
145
+ }
146
+ declare function formatText(editor: Editor, { nodeId, searchText, marks, remove, occurrence }: FormatTextArgs): {
147
+ nodeId: string;
148
+ marks: FormattableMark[];
149
+ removed: boolean;
150
+ matched: string;
151
+ };
152
+ type TableAction = "set_cells" | "insert_row_after" | "insert_row_before" | "insert_col_after" | "insert_col_before" | "delete_row" | "delete_col" | "toggle_header_row" | "merge_cells" | "split_cell";
153
+ interface TableEditArgs {
154
+ tableId: string;
155
+ action: TableAction;
156
+ row?: number;
157
+ col?: number;
158
+ values?: {
159
+ row: number;
160
+ col: number;
161
+ text: string;
162
+ }[];
163
+ }
164
+ declare function tableEdit(editor: Editor, { tableId, action, row, col, values }: TableEditArgs): {
165
+ tableId: string;
166
+ updated: number;
167
+ action?: undefined;
168
+ ok?: undefined;
169
+ } | {
170
+ tableId: string;
171
+ action: "insert_row_after" | "insert_row_before" | "insert_col_after" | "insert_col_before" | "delete_row" | "delete_col" | "toggle_header_row" | "merge_cells" | "split_cell";
172
+ ok: boolean;
173
+ updated?: undefined;
174
+ };
175
+ interface FindHit {
176
+ nodeId: string | null;
177
+ nodeType: string;
178
+ contextBefore: string;
179
+ match: string;
180
+ contextAfter: string;
181
+ }
182
+ declare function findTextInDoc(editor: Editor, query: string, limit?: number): {
183
+ hits: FindHit[];
184
+ total: number;
185
+ };
186
+ declare function getSelection(editor: Editor): {
187
+ selection: null;
188
+ note: string;
189
+ } | {
190
+ selection: {
191
+ text: string;
192
+ blocks: {
193
+ id: string;
194
+ type: string;
195
+ }[];
196
+ };
197
+ note?: undefined;
198
+ };
199
+ /**
200
+ * 从**运行时 schema** 反射出来的能力清单,所以永远不会和实现漂移。
201
+ * 模型不确定某个结构能不能写、该用什么标签时先查它。
202
+ */
203
+ declare function docSchema(editor: Editor): {
204
+ nodeTypes: string[];
205
+ markTypes: string[];
206
+ litexmlTags: Record<string, string>;
207
+ readonlyTags: string[];
208
+ idBearingTypes: string[];
209
+ note: string;
210
+ };
211
+ /** 按节点类型点数,用来和导入源做对账。 */
212
+ declare function docStats(editor: Editor): {
213
+ nodes: Record<string, number>;
214
+ marks: Record<string, number>;
215
+ headings: Record<string, number>;
216
+ textLength: number;
217
+ topLevelBlocks: number;
218
+ };
219
+
220
+ type DocReadFormat = "xml" | "json" | "both";
221
+ interface GetPageContentArgs {
222
+ /**
223
+ * 默认 xml。json 是 ProseMirror 原生结构(无损但费 token,约 1.5-2.8 倍),
224
+ * 只在怀疑 LiteXML 丢了信息时才用。
225
+ */
226
+ format?: DocReadFormat;
227
+ }
228
+ interface DocRuntimeOptions {
229
+ editor?: Editor | null;
230
+ /**
231
+ * 写操作后对改动到的块做视觉提示。浏览器里默认闪一下(`.ai-flash`),
232
+ * 传 `false` 关掉。Node / 测试环境里自动 no-op。
233
+ */
234
+ highlight?: ((id: string) => void) | false;
235
+ /** XML 解析器注入。浏览器里省略即可,会走全局 `DOMParser`。 */
236
+ DOMParser?: LiteXmlParseOptions["DOMParser"];
237
+ }
238
+ declare class DocRuntime {
239
+ private editor;
240
+ private readonly highlight;
241
+ private readonly parseOpts;
242
+ constructor(options?: DocRuntimeOptions);
243
+ /** 编辑器实例就绪 / 卸载时调用。配合 `<NotionEditor onEditorReady>` 用。 */
244
+ setEditor(editor: Editor | null): void;
245
+ isReady(): boolean;
246
+ private use;
247
+ private flash;
248
+ /** 读全文。要编辑之前必须先调它拿 id。 */
249
+ getPageContent({ format }?: GetPageContentArgs): {
250
+ blockCount: number;
251
+ xml?: string;
252
+ json?: unknown;
253
+ };
254
+ /** 运行时反射出的节点·标记·标签映射·哪些类型带 id。永不与实现漂移。 */
255
+ getSchema(): {
256
+ nodeTypes: string[];
257
+ markTypes: string[];
258
+ litexmlTags: Record<string, string>;
259
+ readonlyTags: string[];
260
+ idBearingTypes: string[];
261
+ note: string;
262
+ };
263
+ /** 按节点类型点数,用来和导入源做对账。 */
264
+ getStats(): {
265
+ nodes: Record<string, number>;
266
+ marks: Record<string, number>;
267
+ headings: Record<string, number>;
268
+ textLength: number;
269
+ topLevelBlocks: number;
270
+ };
271
+ /** 检索,返回可直接回填给写工具的 nodeId。 */
272
+ find(args: {
273
+ query: string;
274
+ limit?: number;
275
+ }): {
276
+ hits: FindHit[];
277
+ total: number;
278
+ };
279
+ /** 用户此刻的选区;无选区时显式声明「不要沿用历史」。 */
280
+ getSelection(): {
281
+ selection: null;
282
+ note: string;
283
+ } | {
284
+ selection: {
285
+ text: string;
286
+ blocks: {
287
+ id: string;
288
+ type: string;
289
+ }[];
290
+ };
291
+ note?: undefined;
292
+ };
293
+ /** 块内文本替换(细粒度改动首选)。 */
294
+ replaceText(args: ReplaceTextArgs): ReplaceTextResult;
295
+ /** 对某段文字加/去 bold·italic·underline·strike·code·sup·sub(改格式首选)。 */
296
+ formatText(args: FormatTextArgs): {
297
+ nodeId: string;
298
+ marks: FormattableMark[];
299
+ removed: boolean;
300
+ matched: string;
301
+ };
302
+ /** 结构化 insert / remove / modify,一次提交多个操作。 */
303
+ modifyNodes(args: {
304
+ operations: ModifyOperation[];
305
+ }): ModifyNodesResult;
306
+ /** 表格:增删行列、合并拆分、批量写值。 */
307
+ tableEdit(args: TableEditArgs): {
308
+ tableId: string;
309
+ updated: number;
310
+ action?: undefined;
311
+ ok?: undefined;
312
+ } | {
313
+ tableId: string;
314
+ action: "insert_row_after" | "insert_row_before" | "insert_col_after" | "insert_col_before" | "delete_row" | "delete_col" | "toggle_header_row" | "merge_cells" | "split_cell";
315
+ ok: boolean;
316
+ updated?: undefined;
317
+ };
318
+ /**
319
+ * 工具名 → 方法。给传输层(MCP 桥、agent executor)用的单一分发口,
320
+ * 这样加一个工具只需要改这里,不需要每个传输各写一遍 switch。
321
+ */
322
+ call(tool: string, args?: Record<string, unknown>): Promise<unknown>;
323
+ }
324
+
325
+ export { type DocReadFormat, DocRuntime, type DocRuntimeOptions, FORMATTABLE, type FindHit, type FormatTextArgs, type FormattableMark, type GetPageContentArgs, type LiteXmlParseOptions, MARK_TAG, type ModifyNodesResult, type ModifyOperation, type ModifyResultItem, NODE_TAG, type NodeHit, READONLY_TAGS, type ReplaceTextArgs, type ReplaceTextResult, type TableAction, type TableEditArgs, VOID_TAGS, applyOperations, docSchema, docStats, docToXml, findById, findTextInDoc, formatText, getSelection, mergeParsedNode, nodeToXml, parseXmlFragment, replaceText, tableEdit, xmlElToPm };