@ptengine/lp-editor-core 1.0.0-alpha.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/LICENSE +3 -0
- package/README.md +84 -0
- package/dist/golden/manifest.json +173 -0
- package/dist/index.d.ts +678 -0
- package/dist/index.js +1426 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 片段解析:EditOp 的 `insert.html` / `replace.html` 从字符串变成节点的唯一入口。
|
|
3
|
+
*
|
|
4
|
+
* 不需要注入解析器 —— 用目标文档自己的 `<template>` 就够,而且这是**唯一**能
|
|
5
|
+
* 正确处理上下文相关元素的通用做法:HTML5 的 "in template" 插入模式对
|
|
6
|
+
* `<tr>` / `<td>` / `<col>` / `<li>` / `<option>` 会各自切到正确的表格 / 列表模式,
|
|
7
|
+
* 元素不会被丢弃。所以不需要额外的 contextEl 参数,一个容器覆盖全部上下文。
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ `template.content` 里的节点属于**模板内容所有者文档**,不是 `doc`。
|
|
10
|
+
* 插入时由 DOM 的 pre-insert 自动 adopt,所以最终没问题;但在插入**之前**
|
|
11
|
+
* `node.ownerDocument !== doc`,任何「同文档」断言都会误判。
|
|
12
|
+
*
|
|
13
|
+
* 这里是全包唯一允许写 innerHTML 的地方(见 eslint.config.js):写是解析,
|
|
14
|
+
* 与「禁读 outerHTML」要防的事情无关。
|
|
15
|
+
*/
|
|
16
|
+
declare function parseFragment(doc: Document, html: string): DocumentFragment;
|
|
17
|
+
/** 片段里的元素子节点(跳过纯空白文本),供单根校验使用。 */
|
|
18
|
+
declare function fragmentElementRoots(fragment: DocumentFragment): Element[];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 规范序列化(canonical serializer)。
|
|
22
|
+
*
|
|
23
|
+
* 目标:同一棵 DOM 树(由任何符合 HTML5 规范的树构造器产出),无论宿主是浏览器
|
|
24
|
+
* 原生 DOM 还是 jsdom,都序列化成**逐字节相同**的字符串。这是「编辑器 apply ==
|
|
25
|
+
* mastra apply」那条不变量的落点。
|
|
26
|
+
*
|
|
27
|
+
* 约束:
|
|
28
|
+
* - 纯函数,无副作用、无 I/O
|
|
29
|
+
* - 只依赖标准 DOM 读 API,同一份实现在浏览器与 Node 两侧跑
|
|
30
|
+
* - 绝不使用 innerHTML / outerHTML —— 它们本身就是分歧的来源
|
|
31
|
+
*
|
|
32
|
+
* 规范规则(全部写死,不提供选项):
|
|
33
|
+
* R1 标签名 HTML 命名空间元素小写;外来元素(SVG / MathML)保留
|
|
34
|
+
* 解析器调整后的 localName(foreignObject、clipPath…)
|
|
35
|
+
* R2 属性顺序 按限定名排序,UTF-16 码元序
|
|
36
|
+
* R3 属性引号 一律 name="value",双引号,永远带引号
|
|
37
|
+
* R4 文本转义 & < > -> & < >
|
|
38
|
+
* R5 属性值转义 & < > " -> & < > "
|
|
39
|
+
* R6 空元素 <br> —— 不带结尾斜杠、不带闭合标签
|
|
40
|
+
* R7 pre/textarea/listing 空白原样保留;紧随起始标签的换行重新发出
|
|
41
|
+
* (解析器会吃掉一个),保证再解析回同一棵树
|
|
42
|
+
* R8 注释 原样 <!--data-->
|
|
43
|
+
* R9 doctype 规范化;无 id 的 html -> <!DOCTYPE html>
|
|
44
|
+
* R10 布尔/空属性 统一一种形态:disabled=""、alt=""
|
|
45
|
+
* R11 raw text script/style/xmp/iframe/noembed/noframes/plaintext
|
|
46
|
+
* 的字符数据原样输出,不转义
|
|
47
|
+
* R12 template 经其 .content 片段序列化
|
|
48
|
+
* R13 回车 U+000D 在文本与属性值里输出为 ,使输出可往返
|
|
49
|
+
* R14 class 值 按空白切分后**去重**、单空格拼接,**不重排顺序**
|
|
50
|
+
*
|
|
51
|
+
* R13 与 R14 是相对 WHATWG 序列化器的**增补**,不是放宽:
|
|
52
|
+
* - R13:词法分析器在建树前把 CR / CRLF 归一成 LF,不转义的 CR 会在下一次解析时
|
|
53
|
+
* 静默变成 LF。原生 outerHTML 在所有引擎上都有这个毛病。
|
|
54
|
+
* - R14:两侧按同一组 op 应用会得到同样的插入顺序,所以顺序天然确定、不需要排序;
|
|
55
|
+
* 但源数据里的 `class="a b a"` 这类写法会让字节不同。去重 + 折叠空白把它收敛掉。
|
|
56
|
+
* **有意不排序** —— 排序会把作者写的顺序变成非作者的,而 `md:` 与基线的相对位置
|
|
57
|
+
* 在读源码时是有意义的。
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* R14:class 值去重 + 折叠空白,保持首次出现的顺序。
|
|
61
|
+
* 导出是因为 EditOp 的 class 增删也要用同一份规则,否则「写进去的」与
|
|
62
|
+
* 「序列化出来的」会是两套。
|
|
63
|
+
*/
|
|
64
|
+
declare function normalizeClassValue(value: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* 把 Document、DocumentFragment、Element 或 DocumentType 序列化成规范 HTML。
|
|
67
|
+
*
|
|
68
|
+
* 片段(DocumentFragment)与元素都走同一条路径 —— 片段序列化其全部子节点,
|
|
69
|
+
* 所以 `serialize(parseFragment(doc, html))` 是幂等可比对的。
|
|
70
|
+
*/
|
|
71
|
+
declare function serialize(node: Node): string;
|
|
72
|
+
|
|
73
|
+
interface NormalizeOptions {
|
|
74
|
+
/**
|
|
75
|
+
* id 前缀。默认 `e`。
|
|
76
|
+
* 有意不暴露「随机」开关:同一份输入必须得到同一份输出,否则 normalize 自己
|
|
77
|
+
* 就成了两侧分叉的来源。
|
|
78
|
+
*/
|
|
79
|
+
prefix?: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 按文档序补齐缺失的 id。返回被修改的元素个数(0 表示本来就规范)。
|
|
83
|
+
*/
|
|
84
|
+
declare function normalize(root: Document | DocumentFragment | Element, options?: NormalizeOptions): number;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 错误码与校验条目的形状。
|
|
88
|
+
*
|
|
89
|
+
* core **只出结构,不出文案**。文案由各消费端渲染:编辑器用 lp-editor 的三语字典,
|
|
90
|
+
* iam 发布拒绝按 `rule` 出后端文案,mastra 回给模型的结构化报错复用同一枚举。
|
|
91
|
+
* 三处各一份措辞,但没有两份真源 —— 这也是 core 能作为公开包发布的前提之一。
|
|
92
|
+
*/
|
|
93
|
+
declare const ERROR_CODES: readonly ["FORMAT_MISSING", "ID_DUPLICATE", "ID_CONFLICT", "ID_FORMAT", "ID_NOT_FOUND", "SCRIPT_FORBIDDEN", "INLINE_STYLE", "NATIVE_PALETTE", "CLASS_UNCOMPILABLE", "VARIANT_FORBIDDEN", "STRUCTURE", "MULTI_ROOT", "REPLACE_ID_CHANGED", "STALE_BASE_VERSION", "FRAGMENT_NOT_CANONICAL", "PLACEHOLDER_UNRESOLVED"];
|
|
94
|
+
type ErrorCode = (typeof ERROR_CODES)[number];
|
|
95
|
+
declare function isErrorCode(value: unknown): value is ErrorCode;
|
|
96
|
+
/** 校验条目。**不带 message** —— 文案归消费端。 */
|
|
97
|
+
interface Issue {
|
|
98
|
+
/** 相关元素的 data-pt-id;文档级问题(如缺格式标记)没有。 */
|
|
99
|
+
ptId?: string;
|
|
100
|
+
rule: ErrorCode;
|
|
101
|
+
/** 供消费端拼文案的结构化参数,如 { className: 'bg-red-500' }。 */
|
|
102
|
+
params?: Readonly<Record<string, string | number>>;
|
|
103
|
+
}
|
|
104
|
+
interface ValidateResult$1 {
|
|
105
|
+
ok: boolean;
|
|
106
|
+
issues: Issue[];
|
|
107
|
+
}
|
|
108
|
+
/** 阻塞发布的规则。预览放行,发布必须拦。 */
|
|
109
|
+
declare const PUBLISH_BLOCKING_RULES: ReadonlySet<ErrorCode>;
|
|
110
|
+
declare function isPublishBlocking(issue: Issue): boolean;
|
|
111
|
+
/** 编辑器 / mastra 抛出的错误。同样不带文案。 */
|
|
112
|
+
interface EditorError {
|
|
113
|
+
code: ErrorCode;
|
|
114
|
+
rule?: ErrorCode;
|
|
115
|
+
ptId?: string;
|
|
116
|
+
params?: Readonly<Record<string, string | number>>;
|
|
117
|
+
}
|
|
118
|
+
declare function editorError(code: ErrorCode, extra?: Omit<EditorError, 'code'>): EditorError;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 规则**数据表**。
|
|
122
|
+
*
|
|
123
|
+
* 这里只有数据,没有一句提示词措辞 —— 措辞在 mastra 的
|
|
124
|
+
* `page-designer/segments/v4.ts`,从这张表渲染。core 因此可以作为公开包发布,
|
|
125
|
+
* 措辞微调也不需要 core 发版。
|
|
126
|
+
*
|
|
127
|
+
* 代价是语义变更要两仓配对改。mastra 侧靠**双向**覆盖测试兜住:
|
|
128
|
+
* 正向:规则段必须提到这里的每一项(防 core 新增、mastra 没跟)
|
|
129
|
+
* 反向:规则段提到的每一项都能在这里找到(防 core 删除、mastra 还在讲)
|
|
130
|
+
* 只做正向的话,删一个开关后测试仍是绿的。
|
|
131
|
+
*/
|
|
132
|
+
/** 设备变体。`md:` 是 PC 覆盖,`max-md:` 是手机独占(两者无缝无重叠)。 */
|
|
133
|
+
declare const DEVICE_VARIANTS: readonly ["md", "max-md"];
|
|
134
|
+
/**
|
|
135
|
+
* 状态变体。
|
|
136
|
+
*
|
|
137
|
+
* 有意不含 `focus:` —— 统一用 `focus-visible:`。
|
|
138
|
+
* 有意不含 `group-*` / `peer-*`:它们把样式绑到**别的元素**的状态上,
|
|
139
|
+
* 面板表达不了、属性回显放不进(基线 / md / max-md)三槽。
|
|
140
|
+
* 这两类 Tailwind 都能编译,拒绝它们是产品决定,不是能力限制。
|
|
141
|
+
*/
|
|
142
|
+
declare const STATE_VARIANTS: readonly ["hover", "focus-visible", "active", "disabled"];
|
|
143
|
+
type DeviceVariant = (typeof DEVICE_VARIANTS)[number];
|
|
144
|
+
type StateVariant = (typeof STATE_VARIANTS)[number];
|
|
145
|
+
/** 属性回显的三个槽位。 */
|
|
146
|
+
declare const PROPERTY_SLOTS: readonly ["base", "md", "max-md"];
|
|
147
|
+
type PropertySlot = (typeof PROPERTY_SLOTS)[number];
|
|
148
|
+
/** 结构层级。弹窗没有 section 层。 */
|
|
149
|
+
declare const LAYER_NAMES: readonly ["section", "container", "leaf"];
|
|
150
|
+
type LayerName = (typeof LAYER_NAMES)[number];
|
|
151
|
+
/** leaf 的类型。 */
|
|
152
|
+
declare const LEAF_TYPES: readonly ["text", "media", "button", "shape"];
|
|
153
|
+
type LeafType = (typeof LEAF_TYPES)[number];
|
|
154
|
+
/** 不透明子树的两档。 */
|
|
155
|
+
declare const OPAQUE_MODES: readonly ["strict", "text"];
|
|
156
|
+
type OpaqueMode = (typeof OPAQUE_MODES)[number];
|
|
157
|
+
/**
|
|
158
|
+
* 整张表。mastra 的规则段渲染它,双向覆盖测试也比对它。
|
|
159
|
+
* 加一项就要同步 mastra —— 这正是双向断言要暴露的事。
|
|
160
|
+
*/
|
|
161
|
+
declare const rules: {
|
|
162
|
+
readonly deviceVariants: readonly ["md", "max-md"];
|
|
163
|
+
readonly stateVariants: readonly ["hover", "focus-visible", "active", "disabled"];
|
|
164
|
+
readonly propertySlots: readonly ["base", "md", "max-md"];
|
|
165
|
+
readonly layers: readonly ["section", "container", "leaf"];
|
|
166
|
+
readonly leafTypes: readonly ["text", "media", "button", "shape"];
|
|
167
|
+
readonly idPattern: "[a-z][a-z0-9_-]{1,31}";
|
|
168
|
+
readonly themePtId: "theme";
|
|
169
|
+
readonly rootPtId: "root";
|
|
170
|
+
readonly formatMetaName: "lpx-format";
|
|
171
|
+
readonly formatVersion: "4";
|
|
172
|
+
readonly switches: readonly [{
|
|
173
|
+
readonly key: "hide";
|
|
174
|
+
readonly attr: "data-pt-hide";
|
|
175
|
+
readonly values: readonly ["mobile", "pc"];
|
|
176
|
+
readonly summary: "一端隐藏";
|
|
177
|
+
}, {
|
|
178
|
+
readonly key: "layout-mobile";
|
|
179
|
+
readonly attr: "data-pt-layout-mobile";
|
|
180
|
+
readonly values: readonly ["stack", "scroll"];
|
|
181
|
+
readonly summary: "PC 轮播→手机纵向平铺 / PC 网格→手机横滑";
|
|
182
|
+
}, {
|
|
183
|
+
readonly key: "swap-image";
|
|
184
|
+
readonly attr: null;
|
|
185
|
+
readonly values: readonly [];
|
|
186
|
+
readonly summary: "<picture> 双 source,(min-width:768px) 分界";
|
|
187
|
+
}, {
|
|
188
|
+
readonly key: "direction";
|
|
189
|
+
readonly attr: null;
|
|
190
|
+
readonly values: readonly [];
|
|
191
|
+
readonly summary: "flex-col md:flex-row";
|
|
192
|
+
}, {
|
|
193
|
+
readonly key: "columns";
|
|
194
|
+
readonly attr: null;
|
|
195
|
+
readonly values: readonly [];
|
|
196
|
+
readonly summary: "grid-cols-1 md:grid-cols-{n}";
|
|
197
|
+
}, {
|
|
198
|
+
readonly key: "placeholder";
|
|
199
|
+
readonly attr: "data-pt-placeholder";
|
|
200
|
+
readonly values: readonly [];
|
|
201
|
+
readonly summary: "未落图占位;必带 aspect-*,src 为空";
|
|
202
|
+
}];
|
|
203
|
+
readonly crossDeviceConsistentProperties: readonly ["color", "font-weight", "border", "box-shadow", "font-family"];
|
|
204
|
+
readonly opaqueModes: readonly ["strict", "text"];
|
|
205
|
+
readonly opaqueTextEditableTags: readonly ["td", "th"];
|
|
206
|
+
readonly inlineNoIdTags: readonly ["a", "abbr", "b", "bdi", "bdo", "br", "cite", "code", "data", "dfn", "em", "i", "kbd", "mark", "q", "rp", "rt", "ruby", "s", "samp", "small", "span", "strong", "sub", "sup", "time", "u", "var", "wbr"];
|
|
207
|
+
readonly inlineStyleAllowedPrefix: "--pt-";
|
|
208
|
+
readonly editOpKinds: readonly ["set", "insert", "delete", "move", "replace"];
|
|
209
|
+
readonly arbitraryValueAllowedProperties: readonly ["width", "height", "aspect-ratio", "background-image", "max-width", "min-height"];
|
|
210
|
+
};
|
|
211
|
+
type Rules = typeof rules;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 两种文档类型的能力档。宿主把其中一档作为 `capabilities` 传给编辑器,
|
|
215
|
+
* mastra 也读同一份来判断结构规则。
|
|
216
|
+
*/
|
|
217
|
+
/** 组件运行时由谁提供。 */
|
|
218
|
+
type ComponentRuntime =
|
|
219
|
+
/** 渲染器自带脚本(落地页)。 */
|
|
220
|
+
'self'
|
|
221
|
+
/**
|
|
222
|
+
* 只出声明式标记、零脚本,行为归 Engage SDK(弹窗的表单与倒计时)。
|
|
223
|
+
* core 给弹窗表单出脚本会和 SDK 抢同一张表单。
|
|
224
|
+
*/
|
|
225
|
+
| 'engage-sdk';
|
|
226
|
+
interface KindProfile {
|
|
227
|
+
/** 文档形态:完整文档(落地页)还是片段(弹窗)。 */
|
|
228
|
+
root: 'document' | 'fragment';
|
|
229
|
+
/** 结构层级。弹窗没有 section 层。 */
|
|
230
|
+
layers: readonly LayerName[];
|
|
231
|
+
/** 编译目标:完整文档 CSS,还是 Shadow DOM 内的 px 化 CSS。 */
|
|
232
|
+
compile: 'document' | 'shadow-px';
|
|
233
|
+
leafTypes: readonly LeafType[];
|
|
234
|
+
/** 允许的内置组件 → 其运行时策略。 */
|
|
235
|
+
components: Readonly<Record<string, ComponentRuntime>>;
|
|
236
|
+
units: {
|
|
237
|
+
vh: boolean;
|
|
238
|
+
};
|
|
239
|
+
customCode: boolean;
|
|
240
|
+
}
|
|
241
|
+
declare const KIND_PROFILES: {
|
|
242
|
+
readonly page: {
|
|
243
|
+
readonly root: "document";
|
|
244
|
+
readonly layers: readonly ["section", "container", "leaf"];
|
|
245
|
+
readonly compile: "document";
|
|
246
|
+
readonly leafTypes: readonly ["text", "media", "button", "shape"];
|
|
247
|
+
readonly components: {
|
|
248
|
+
readonly carousel: "self";
|
|
249
|
+
readonly faq: "self";
|
|
250
|
+
readonly tabs: "self";
|
|
251
|
+
readonly 'sticky-cta': "self";
|
|
252
|
+
readonly form: "self";
|
|
253
|
+
};
|
|
254
|
+
readonly units: {
|
|
255
|
+
readonly vh: true;
|
|
256
|
+
};
|
|
257
|
+
readonly customCode: true;
|
|
258
|
+
};
|
|
259
|
+
readonly popup: {
|
|
260
|
+
readonly root: "fragment";
|
|
261
|
+
readonly layers: readonly ["container", "leaf"];
|
|
262
|
+
readonly compile: "shadow-px";
|
|
263
|
+
readonly leafTypes: readonly ["text", "media", "button", "shape"];
|
|
264
|
+
readonly components: {
|
|
265
|
+
readonly form: "engage-sdk";
|
|
266
|
+
readonly countdown: "engage-sdk";
|
|
267
|
+
readonly carousel: "self";
|
|
268
|
+
readonly faq: "self";
|
|
269
|
+
readonly tabs: "self";
|
|
270
|
+
};
|
|
271
|
+
readonly units: {
|
|
272
|
+
readonly vh: false;
|
|
273
|
+
};
|
|
274
|
+
readonly customCode: false;
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
type KindName = keyof typeof KIND_PROFILES;
|
|
278
|
+
|
|
279
|
+
declare function isNativePaletteClass(className: string): boolean;
|
|
280
|
+
|
|
281
|
+
interface ValidateOptions {
|
|
282
|
+
profile: KindProfile;
|
|
283
|
+
/**
|
|
284
|
+
* 判定单个 class 在当前 @theme 下是否能编译出 CSS。
|
|
285
|
+
* 由调用方注入(编译器在另一个模块,validate 不直接依赖它)。
|
|
286
|
+
* 不传则跳过这条规则 —— 用于只想跑结构 / 格式检查的场合。
|
|
287
|
+
*/
|
|
288
|
+
canCompileClass?: (className: string) => boolean;
|
|
289
|
+
}
|
|
290
|
+
interface ValidateResult {
|
|
291
|
+
ok: boolean;
|
|
292
|
+
issues: Issue[];
|
|
293
|
+
}
|
|
294
|
+
declare function validate(root: Document | DocumentFragment | Element, options: ValidateOptions): ValidateResult;
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* EditOp:人的面板操作、拖拽、富文本、AI 指令,最终都变成这五种操作之一,
|
|
298
|
+
* 作用在唯一的 Document 上。撤销、AI、序列化因此只面对一个对象。
|
|
299
|
+
*/
|
|
300
|
+
interface SetOp {
|
|
301
|
+
op: 'set';
|
|
302
|
+
id: string;
|
|
303
|
+
/** 值为 null 表示删除该属性。 */
|
|
304
|
+
attrs?: Record<string, string | null>;
|
|
305
|
+
classes?: {
|
|
306
|
+
add?: string[];
|
|
307
|
+
remove?: string[];
|
|
308
|
+
};
|
|
309
|
+
/** 元素的内容片段(富文本标记 / @theme 内容)。同样要过脚本护栏。 */
|
|
310
|
+
text?: string;
|
|
311
|
+
}
|
|
312
|
+
interface InsertOp {
|
|
313
|
+
op: 'insert';
|
|
314
|
+
parentId: string;
|
|
315
|
+
/** 在父元素的**元素子节点**中的下标。 */
|
|
316
|
+
index: number;
|
|
317
|
+
/** 必须是单根元素,且片段内结构节点自带 data-pt-id。 */
|
|
318
|
+
html: string;
|
|
319
|
+
/**
|
|
320
|
+
* 精确的 childNodes 下标。**只由 core 生成逆操作时填写**,构造方不要用。
|
|
321
|
+
*
|
|
322
|
+
* 为什么需要它:元素下标对人和 AI 才是可理解的,但删除一个元素后,
|
|
323
|
+
* 它前后的空白文本节点还在原地;只按元素下标插回去,空白会跑到另一侧,
|
|
324
|
+
* 往返就不再逐字节相同。逆操作记下精确偏移,往返才稳。
|
|
325
|
+
*/
|
|
326
|
+
nodeIndex?: number;
|
|
327
|
+
}
|
|
328
|
+
interface DeleteOp {
|
|
329
|
+
op: 'delete';
|
|
330
|
+
id: string;
|
|
331
|
+
}
|
|
332
|
+
interface MoveOp {
|
|
333
|
+
op: 'move';
|
|
334
|
+
id: string;
|
|
335
|
+
parentId: string;
|
|
336
|
+
index: number;
|
|
337
|
+
/** 同 InsertOp.nodeIndex,只由 core 生成逆操作时填写。 */
|
|
338
|
+
nodeIndex?: number;
|
|
339
|
+
}
|
|
340
|
+
interface ReplaceOp {
|
|
341
|
+
op: 'replace';
|
|
342
|
+
id: string;
|
|
343
|
+
/** 单根,且根元素 id 必须等于被替换元素的 id(埋点锚点不能断)。 */
|
|
344
|
+
html: string;
|
|
345
|
+
}
|
|
346
|
+
type EditOp = SetOp | InsertOp | DeleteOp | MoveOp | ReplaceOp;
|
|
347
|
+
interface Transaction {
|
|
348
|
+
/** 一次意图 = 一次撤销。进历史栏的那句话。 */
|
|
349
|
+
label: string;
|
|
350
|
+
ops: EditOp[];
|
|
351
|
+
source: 'user' | 'ai';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
interface ApplyOptions {
|
|
355
|
+
profile: KindProfile;
|
|
356
|
+
}
|
|
357
|
+
type ApplyResult = {
|
|
358
|
+
ok: true;
|
|
359
|
+
inverse: EditOp[];
|
|
360
|
+
} | {
|
|
361
|
+
ok: false;
|
|
362
|
+
error: EditorError;
|
|
363
|
+
failedIndex: number;
|
|
364
|
+
};
|
|
365
|
+
/**
|
|
366
|
+
* 应用一组 EditOp,并产出逆操作。
|
|
367
|
+
*
|
|
368
|
+
* **事务语义**:一组 op 顺序应用,每条的下标基于**前一条应用之后**的状态。
|
|
369
|
+
* 任何一条失败 → 整个事务不生效,文档一个字节都不变。
|
|
370
|
+
*
|
|
371
|
+
* 原子性的实现方式是「先在克隆体上试跑一遍,通过了再在真文档上跑」,而不是
|
|
372
|
+
* 「边跑边记逆操作、失败了回滚」。后者的正确性取决于逆操作自身没有 bug,
|
|
373
|
+
* 而失败时恰恰是最容易触发边角情况的时刻 —— 把原子性压在最脆弱的那条路径上
|
|
374
|
+
* 不是好交易。克隆一次的代价(原生 cloneNode)远小于一次编译。
|
|
375
|
+
*/
|
|
376
|
+
declare function apply(doc: Document, ops: EditOp[], options: ApplyOptions): ApplyResult;
|
|
377
|
+
|
|
378
|
+
/** 操作引用的 id 必须在文档里。用得最多的一条。 */
|
|
379
|
+
declare function requireIdHit(doc: Document | DocumentFragment, id: string): EditorError | null;
|
|
380
|
+
declare function findById(root: Document | DocumentFragment | Element, id: string): Element | null;
|
|
381
|
+
/** 片段与富文本里都不许有脚本与事件属性。 */
|
|
382
|
+
declare function rejectScriptsAndHandlers(node: Node): EditorError | null;
|
|
383
|
+
/**
|
|
384
|
+
* 片段内每个结构节点都要自带 id,格式合规,且不与目标文档冲突。
|
|
385
|
+
*
|
|
386
|
+
* apply **永不生成 id** —— id 在构造 op 时就要写进 payload。两侧各自生成必然分叉,
|
|
387
|
+
* 所以这里宁可整个事务拒绝,也不就地补一个。
|
|
388
|
+
*/
|
|
389
|
+
declare function requireFragmentIds(fragment: DocumentFragment, existing: ReadonlySet<string>): EditorError | null;
|
|
390
|
+
/** 片段必须是单根元素。 */
|
|
391
|
+
declare function requireSingleRoot(fragment: DocumentFragment): EditorError | null;
|
|
392
|
+
/**
|
|
393
|
+
* 片段 canonical 往返一致:`serialize(parse(html))` 再解析一次仍相同。
|
|
394
|
+
* 拦的是畸形 HTML —— 它在两侧的纠错结果可能不同。
|
|
395
|
+
*/
|
|
396
|
+
declare function requireCanonicalFragment(doc: Document, html: string): EditorError | null;
|
|
397
|
+
|
|
398
|
+
declare const guards_findById: typeof findById;
|
|
399
|
+
declare const guards_rejectScriptsAndHandlers: typeof rejectScriptsAndHandlers;
|
|
400
|
+
declare const guards_requireCanonicalFragment: typeof requireCanonicalFragment;
|
|
401
|
+
declare const guards_requireFragmentIds: typeof requireFragmentIds;
|
|
402
|
+
declare const guards_requireIdHit: typeof requireIdHit;
|
|
403
|
+
declare const guards_requireSingleRoot: typeof requireSingleRoot;
|
|
404
|
+
declare namespace guards {
|
|
405
|
+
export { guards_findById as findById, guards_rejectScriptsAndHandlers as rejectScriptsAndHandlers, guards_requireCanonicalFragment as requireCanonicalFragment, guards_requireFragmentIds as requireFragmentIds, guards_requireIdHit as requireIdHit, guards_requireSingleRoot as requireSingleRoot };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* 主题:`<style type="text/tailwindcss" data-pt-id="theme">` 里的 @theme 块内容。
|
|
410
|
+
*
|
|
411
|
+
* 它是缓存的键:一个 class 成不成立、tailwind-merge 认不认得某个 token,
|
|
412
|
+
* 都取决于当前主题。
|
|
413
|
+
*/
|
|
414
|
+
interface Theme {
|
|
415
|
+
/** @theme 块的原文(含 `@theme { … }` 外壳)。 */
|
|
416
|
+
source: string;
|
|
417
|
+
/** 内容 hash,作为缓存键。 */
|
|
418
|
+
hash: string;
|
|
419
|
+
}
|
|
420
|
+
declare const EMPTY_THEME: Theme;
|
|
421
|
+
/** 从文档里取出主题块。取不到就是空主题。 */
|
|
422
|
+
declare function themeOf(root: Document | DocumentFragment | Element): Theme;
|
|
423
|
+
declare function themeFromSource(source: string): Theme;
|
|
424
|
+
/**
|
|
425
|
+
* 从 @theme 原文里抽出各命名空间的 token 名。
|
|
426
|
+
* 供 tailwind-merge 的 class group 派生 —— token 名是每页自定义的,
|
|
427
|
+
* 模块级单例配置认不出它们。
|
|
428
|
+
*/
|
|
429
|
+
interface ThemeTokens {
|
|
430
|
+
color: string[];
|
|
431
|
+
text: string[];
|
|
432
|
+
spacing: string[];
|
|
433
|
+
radius: string[];
|
|
434
|
+
shadow: string[];
|
|
435
|
+
font: string[];
|
|
436
|
+
}
|
|
437
|
+
declare function themeTokens(theme: Theme): ThemeTokens;
|
|
438
|
+
|
|
439
|
+
/** 累积编译:返回迄今见过的所有候选的完整 CSS。画布用。 */
|
|
440
|
+
declare function incremental(candidates: Iterable<string>, theme?: Theme): Promise<string>;
|
|
441
|
+
/**
|
|
442
|
+
* 全新编译:每次新建引擎,候选**排序后**编译。
|
|
443
|
+
* 排序是为了让同一组 class 无论以什么顺序收集到,都产出同一串字节。
|
|
444
|
+
*/
|
|
445
|
+
declare function fresh(candidates: Iterable<string>, theme?: Theme): Promise<string>;
|
|
446
|
+
/** 把该主题的缓存条目建齐。`fresh` 按定义不受益。 */
|
|
447
|
+
declare function warm(theme?: Theme): Promise<void>;
|
|
448
|
+
/**
|
|
449
|
+
* 「class 可编译」判定。
|
|
450
|
+
*
|
|
451
|
+
* 用 Tailwind 自己的 design system:`candidatesToCss` 对解析不出的候选直接返回
|
|
452
|
+
* null。这是 Tailwind IntelliSense 与 prettier 插件用的同一个口子 —— 精确判定,
|
|
453
|
+
* 不是在 CSS 字串里找转义选择器的启发式。
|
|
454
|
+
*
|
|
455
|
+
* 一个 ops 块一次调用,不是逐 class 建引擎(逐个建引擎每次约 3ms,
|
|
456
|
+
* 20 个 class 的块冷缓存就要多花 60ms,加在每轮 AI 往返上)。
|
|
457
|
+
*
|
|
458
|
+
* ⚠️ 它挂着 `__unstable__` 前缀。我们本来就锁精确版本,所以现在是安全的;
|
|
459
|
+
* 万一哪个版本拿掉它,退回「在 fresh 输出里查转义后的选择器」那条路,
|
|
460
|
+
* 判定器同样只在这一处。
|
|
461
|
+
*/
|
|
462
|
+
declare function canCompile(classes: readonly string[], theme?: Theme): Promise<Map<string, boolean>>;
|
|
463
|
+
/** 同属性互斥:按当前主题派生的冲突表合并 class 列表。 */
|
|
464
|
+
declare function mergeClasses(classes: readonly string[], theme?: Theme): string;
|
|
465
|
+
/** 测试与宿主重载主题时用。 */
|
|
466
|
+
declare function clearCompileCache(): void;
|
|
467
|
+
|
|
468
|
+
declare const compile_EMPTY_THEME: typeof EMPTY_THEME;
|
|
469
|
+
type compile_Theme = Theme;
|
|
470
|
+
type compile_ThemeTokens = ThemeTokens;
|
|
471
|
+
declare const compile_canCompile: typeof canCompile;
|
|
472
|
+
declare const compile_clearCompileCache: typeof clearCompileCache;
|
|
473
|
+
declare const compile_fresh: typeof fresh;
|
|
474
|
+
declare const compile_incremental: typeof incremental;
|
|
475
|
+
declare const compile_mergeClasses: typeof mergeClasses;
|
|
476
|
+
declare const compile_themeFromSource: typeof themeFromSource;
|
|
477
|
+
declare const compile_themeOf: typeof themeOf;
|
|
478
|
+
declare const compile_themeTokens: typeof themeTokens;
|
|
479
|
+
declare const compile_warm: typeof warm;
|
|
480
|
+
declare namespace compile {
|
|
481
|
+
export { compile_EMPTY_THEME as EMPTY_THEME, type compile_Theme as Theme, type compile_ThemeTokens as ThemeTokens, compile_canCompile as canCompile, compile_clearCompileCache as clearCompileCache, compile_fresh as fresh, compile_incremental as incremental, compile_mergeClasses as mergeClasses, compile_themeFromSource as themeFromSource, compile_themeOf as themeOf, compile_themeTokens as themeTokens, compile_warm as warm };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* 收集文档里用到的全部 class。
|
|
486
|
+
*
|
|
487
|
+
* 接 `Document` 而不是 HTML 字串 —— 与其余 API 一致,也免得调用方为了收集 class
|
|
488
|
+
* 再解析一遍(浏览器侧先 parse 即可)。
|
|
489
|
+
*
|
|
490
|
+
* 注意它扫的是**源文档**:内置组件在源里只是一个空壳,展开后才出现的类名不在
|
|
491
|
+
* 这里。这正是「渲染器产出的 markup 不使用 Tailwind class」那条规则存在的原因 ——
|
|
492
|
+
* 组件自带 CSS,不走这条编译管线。
|
|
493
|
+
*/
|
|
494
|
+
declare function collectClasses(root: Document | DocumentFragment | Element): Set<string>;
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* 宿主注入的 DOM 契约。
|
|
498
|
+
*
|
|
499
|
+
* core 不读任何浏览器全局 —— 它要在 Node 里跑出与浏览器逐字节相同的结果,
|
|
500
|
+
* 任何隐式依赖都会让两侧漂掉。整文档解析所需的 DOMParser 由宿主在
|
|
501
|
+
* `createCore({ DOMParser })` 时注入一次:
|
|
502
|
+
*
|
|
503
|
+
* 浏览器 createCore({ DOMParser: window.DOMParser })
|
|
504
|
+
* Node createCore({ DOMParser: new JSDOM('').window.DOMParser })
|
|
505
|
+
*
|
|
506
|
+
* Node 侧用 jsdom window 上的构造器、而不是 `new JSDOM(html)`:两侧对称,
|
|
507
|
+
* 也避开 runScripts 选项对 <noscript> 解析模式的影响。
|
|
508
|
+
*/
|
|
509
|
+
interface DOMParserLike {
|
|
510
|
+
parseFromString(html: string, type: 'text/html'): Document;
|
|
511
|
+
}
|
|
512
|
+
interface DOMParserCtor {
|
|
513
|
+
new (): DOMParserLike;
|
|
514
|
+
}
|
|
515
|
+
interface CoreOptions {
|
|
516
|
+
/** 整文档解析用。片段解析不需要它(见 parseFragment)。 */
|
|
517
|
+
DOMParser: DOMParserCtor;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
declare function layerOf(el: Element, profile: KindProfile): LayerName | null;
|
|
521
|
+
type StructureRejection = 'section-not-body-child' | 'container-too-deep' | 'leaf-cannot-contain' | 'opaque-subtree' | 'unknown-layer';
|
|
522
|
+
/** 某个父节点能否接纳某一层的子节点。 */
|
|
523
|
+
declare function canContain(parent: Element, childLayer: LayerName, profile: KindProfile): StructureRejection | null;
|
|
524
|
+
/**
|
|
525
|
+
* 删除的级联语义。
|
|
526
|
+
*
|
|
527
|
+
* 删掉 container 的最后一个 leaf —— container 保留(用户还要往里放东西)。
|
|
528
|
+
* 删掉 section 的最后一个 container —— 连 section 一起删(空区块没有意义)。
|
|
529
|
+
*
|
|
530
|
+
* 返回**真正要从树上摘掉的那个节点**。
|
|
531
|
+
*/
|
|
532
|
+
declare function deleteTarget(el: Element, profile: KindProfile): Element;
|
|
533
|
+
|
|
534
|
+
interface VariantCheck {
|
|
535
|
+
ok: boolean;
|
|
536
|
+
/** 不合规时给出第一个出问题的前缀,供消费端拼文案。 */
|
|
537
|
+
offending?: string;
|
|
538
|
+
reason?: 'not-allowed' | 'order';
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* 变体前缀允许集与顺序。
|
|
542
|
+
*
|
|
543
|
+
* 允许:设备 md: / max-md:,状态 hover: / focus-visible: / active: / disabled:,
|
|
544
|
+
* 组合只允许**设备在前、状态在后**(md:hover:),不允许 hover:md:。
|
|
545
|
+
*
|
|
546
|
+
* 为什么固定顺序:属性回显要把值放进(基线 / md / max-md)三槽,
|
|
547
|
+
* 两种写法语义相同但会被当成两个不同的 class,同属性互斥就失效了。
|
|
548
|
+
*
|
|
549
|
+
* 注意 `group-*` / `peer-*` 与 `hover:md:` 这类 Tailwind **都能编译** ——
|
|
550
|
+
* 拒绝它们是产品决定,所以必须由这条独立规则拦,「class 可编译」判定拦不住。
|
|
551
|
+
*/
|
|
552
|
+
declare function checkVariants(className: string): VariantCheck;
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* 两个版本号,有意分家。
|
|
556
|
+
*
|
|
557
|
+
* `VERSION` 包版本,按 semver 走。改规则数据、改护栏逻辑、改注释都会动它。
|
|
558
|
+
* `COMPILER_VERSION` **只在编译 / 导出 / 组件渲染 / 规范序列化的输出可能变时**
|
|
559
|
+
* 手动递增。
|
|
560
|
+
*
|
|
561
|
+
* 为什么必须分开:iam 的 `compiler_version` 列存后者,保存去重也按后者。若用包版本,
|
|
562
|
+
* 改一条 rules 数据就会让全租户的去重失效、把字节完全相同的成品重算一遍。
|
|
563
|
+
*
|
|
564
|
+
* 护栏:export golden 断言「输出变了而 COMPILER_VERSION 没变就红」。
|
|
565
|
+
* 递增它是一次显式动作,不要顺手跟着包版本一起改。
|
|
566
|
+
*/
|
|
567
|
+
declare const VERSION = "1.0.0-alpha.0";
|
|
568
|
+
declare const COMPILER_VERSION = "1";
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* core 的入口。宿主注入 DOM 之后拿到一组行为。
|
|
572
|
+
*
|
|
573
|
+
* 只有 `parse` 真的需要注入(整文档解析)。其余能力都是纯函数,挂在这里只是给宿主
|
|
574
|
+
* 一个单一入口;它们同时也是具名导出 —— golden 测试直接对着纯函数比对最省事,
|
|
575
|
+
* 也省得两份设计文档里所有签名都写成 core.x(...)。
|
|
576
|
+
*/
|
|
577
|
+
declare function createCore(options: CoreOptions): {
|
|
578
|
+
parse: (html: string) => Document;
|
|
579
|
+
parseFragment: typeof parseFragment;
|
|
580
|
+
serialize: typeof serialize;
|
|
581
|
+
normalize: typeof normalize;
|
|
582
|
+
validate: typeof validate;
|
|
583
|
+
apply: typeof apply;
|
|
584
|
+
guards: typeof guards;
|
|
585
|
+
compile: typeof compile;
|
|
586
|
+
collectClasses: typeof collectClasses;
|
|
587
|
+
rules: {
|
|
588
|
+
readonly deviceVariants: readonly ["md", "max-md"];
|
|
589
|
+
readonly stateVariants: readonly ["hover", "focus-visible", "active", "disabled"];
|
|
590
|
+
readonly propertySlots: readonly ["base", "md", "max-md"];
|
|
591
|
+
readonly layers: readonly ["section", "container", "leaf"];
|
|
592
|
+
readonly leafTypes: readonly ["text", "media", "button", "shape"];
|
|
593
|
+
readonly idPattern: "[a-z][a-z0-9_-]{1,31}";
|
|
594
|
+
readonly themePtId: "theme";
|
|
595
|
+
readonly rootPtId: "root";
|
|
596
|
+
readonly formatMetaName: "lpx-format";
|
|
597
|
+
readonly formatVersion: "4";
|
|
598
|
+
readonly switches: readonly [{
|
|
599
|
+
readonly key: "hide";
|
|
600
|
+
readonly attr: "data-pt-hide";
|
|
601
|
+
readonly values: readonly ["mobile", "pc"];
|
|
602
|
+
readonly summary: "一端隐藏";
|
|
603
|
+
}, {
|
|
604
|
+
readonly key: "layout-mobile";
|
|
605
|
+
readonly attr: "data-pt-layout-mobile";
|
|
606
|
+
readonly values: readonly ["stack", "scroll"];
|
|
607
|
+
readonly summary: "PC 轮播→手机纵向平铺 / PC 网格→手机横滑";
|
|
608
|
+
}, {
|
|
609
|
+
readonly key: "swap-image";
|
|
610
|
+
readonly attr: null;
|
|
611
|
+
readonly values: readonly [];
|
|
612
|
+
readonly summary: "<picture> 双 source,(min-width:768px) 分界";
|
|
613
|
+
}, {
|
|
614
|
+
readonly key: "direction";
|
|
615
|
+
readonly attr: null;
|
|
616
|
+
readonly values: readonly [];
|
|
617
|
+
readonly summary: "flex-col md:flex-row";
|
|
618
|
+
}, {
|
|
619
|
+
readonly key: "columns";
|
|
620
|
+
readonly attr: null;
|
|
621
|
+
readonly values: readonly [];
|
|
622
|
+
readonly summary: "grid-cols-1 md:grid-cols-{n}";
|
|
623
|
+
}, {
|
|
624
|
+
readonly key: "placeholder";
|
|
625
|
+
readonly attr: "data-pt-placeholder";
|
|
626
|
+
readonly values: readonly [];
|
|
627
|
+
readonly summary: "未落图占位;必带 aspect-*,src 为空";
|
|
628
|
+
}];
|
|
629
|
+
readonly crossDeviceConsistentProperties: readonly ["color", "font-weight", "border", "box-shadow", "font-family"];
|
|
630
|
+
readonly opaqueModes: readonly ["strict", "text"];
|
|
631
|
+
readonly opaqueTextEditableTags: readonly ["td", "th"];
|
|
632
|
+
readonly inlineNoIdTags: readonly ["a", "abbr", "b", "bdi", "bdo", "br", "cite", "code", "data", "dfn", "em", "i", "kbd", "mark", "q", "rp", "rt", "ruby", "s", "samp", "small", "span", "strong", "sub", "sup", "time", "u", "var", "wbr"];
|
|
633
|
+
readonly inlineStyleAllowedPrefix: "--pt-";
|
|
634
|
+
readonly editOpKinds: readonly ["set", "insert", "delete", "move", "replace"];
|
|
635
|
+
readonly arbitraryValueAllowedProperties: readonly ["width", "height", "aspect-ratio", "background-image", "max-width", "min-height"];
|
|
636
|
+
};
|
|
637
|
+
KIND_PROFILES: {
|
|
638
|
+
readonly page: {
|
|
639
|
+
readonly root: "document";
|
|
640
|
+
readonly layers: readonly ["section", "container", "leaf"];
|
|
641
|
+
readonly compile: "document";
|
|
642
|
+
readonly leafTypes: readonly ["text", "media", "button", "shape"];
|
|
643
|
+
readonly components: {
|
|
644
|
+
readonly carousel: "self";
|
|
645
|
+
readonly faq: "self";
|
|
646
|
+
readonly tabs: "self";
|
|
647
|
+
readonly 'sticky-cta': "self";
|
|
648
|
+
readonly form: "self";
|
|
649
|
+
};
|
|
650
|
+
readonly units: {
|
|
651
|
+
readonly vh: true;
|
|
652
|
+
};
|
|
653
|
+
readonly customCode: true;
|
|
654
|
+
};
|
|
655
|
+
readonly popup: {
|
|
656
|
+
readonly root: "fragment";
|
|
657
|
+
readonly layers: readonly ["container", "leaf"];
|
|
658
|
+
readonly compile: "shadow-px";
|
|
659
|
+
readonly leafTypes: readonly ["text", "media", "button", "shape"];
|
|
660
|
+
readonly components: {
|
|
661
|
+
readonly form: "engage-sdk";
|
|
662
|
+
readonly countdown: "engage-sdk";
|
|
663
|
+
readonly carousel: "self";
|
|
664
|
+
readonly faq: "self";
|
|
665
|
+
readonly tabs: "self";
|
|
666
|
+
};
|
|
667
|
+
readonly units: {
|
|
668
|
+
readonly vh: false;
|
|
669
|
+
};
|
|
670
|
+
readonly customCode: false;
|
|
671
|
+
};
|
|
672
|
+
};
|
|
673
|
+
VERSION: string;
|
|
674
|
+
COMPILER_VERSION: string;
|
|
675
|
+
};
|
|
676
|
+
type Core = ReturnType<typeof createCore>;
|
|
677
|
+
|
|
678
|
+
export { type ApplyOptions, type ApplyResult, COMPILER_VERSION, type ComponentRuntime, type Core, type CoreOptions, type DOMParserCtor, type DOMParserLike, type DeleteOp, type DeviceVariant, EMPTY_THEME, ERROR_CODES, type EditOp, type EditorError, type ErrorCode, type InsertOp, type Issue, KIND_PROFILES, type KindName, type KindProfile, type LayerName, type LeafType, type MoveOp, type NormalizeOptions, type OpaqueMode, PUBLISH_BLOCKING_RULES, type PropertySlot, type ReplaceOp, type Rules, type SetOp, type StateVariant, type Theme, type ThemeTokens, type Transaction, VERSION, type ValidateOptions, type ValidateResult$1 as ValidateResult, apply, canContain, checkVariants, collectClasses, compile, createCore, deleteTarget, editorError, fragmentElementRoots, guards, isErrorCode, isNativePaletteClass, isPublishBlocking, layerOf, normalize, normalizeClassValue, parseFragment, rules, serialize, themeFromSource, themeOf, themeTokens, validate };
|