@ptengine/lp-editor-core 1.0.0-alpha.2 → 1.0.0-alpha.3

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/README.md CHANGED
@@ -43,15 +43,39 @@ const core = createCore({ DOMParser: new JSDOM('').window.DOMParser });
43
43
  const doc = core.parse(sourceHtml);
44
44
  core.normalize(doc); // 唯一分配元素 id 的地方
45
45
 
46
- const { ok, issues } = core.validate(doc, { profile: core.KIND_PROFILES.page });
46
+ const { ok, issues } = core.validate(doc); // 可选开关都不传 = 最宽松
47
47
 
48
- const result = core.apply(doc, ops, { profile: core.KIND_PROFILES.page });
48
+ const result = core.apply(doc, ops);
49
49
  if (result.ok) {
50
50
  const html = core.serialize(doc); // 规范序列化,两侧同字节
51
51
  const undo = result.inverse; // 逆操作,喂给撤销栈
52
52
  }
53
53
  ```
54
54
 
55
+ **core 不感知宿主**:它只认一份 v4 文档、一套三层树、一套 EditOp,没有「落地页 / 弹窗」
56
+ 这类档位入参。宿主差异(运行时契约标记、样式作用域与单位换算、可用能力开关)由调用方在
57
+ 进出边界上处理。少数可选规则以**扁平开关**的形式提供:
58
+
59
+ ```ts
60
+ core.validate(doc, {
61
+ allowVhUnits: false, // 文档被装在比视口小的框里时:vh / h-screen 按整个视口算,不是那个框
62
+ singleSection: true, // 限定整份文档只有一个 section
63
+ canCompileClass // 注入「这个 class 在当前 @theme 下能不能编译」的判定
64
+ });
65
+ ```
66
+
67
+ 持有 HTML **片段**(而不是完整文档)的调用方,在读出与写回的那一刻各调一次包裹 / 剥离:
68
+
69
+ ```ts
70
+ const doc = core.wrap(fragmentHtml); // 片段 → 合规的三层树文档,编辑器内部没有「片段」这回事
71
+ // …编辑…
72
+ const html = core.serialize(core.unwrap(doc)); // 无损剥回原来的形态
73
+ ```
74
+
75
+ 包裹会把片段的根元素**认领**成文档的 `<body>`(不改名、不另起一层),并合成一个 `section`
76
+ + 一个 `container`(都带 `data-pt-wrap` 标记)。往返无损由 golden 锁住。遇到不合约定的输入
77
+ **一律抛错**,不自行修复或改名。
78
+
55
79
  ### 主要导出
56
80
 
57
81
  | 导出 | 作用 |
@@ -64,7 +88,8 @@ if (result.ok) {
64
88
  | `compile.{incremental,fresh,warm}` | Tailwind 编译。`fresh` 的输出只取决于内容 |
65
89
  | `collectClasses` | 收集文档用到的 class |
66
90
  | `guards` | AI 护栏:id 命中、片段 id 齐全、禁脚本与事件属性 |
67
- | `rules` / `KIND_PROFILES` | 规则数据表与两种文档类型的能力档 |
91
+ | `wrap` / `unwrap` | 片段型来源的规范化包裹与无损剥离 |
92
+ | `rules` | 规则数据表(层级、变体允许集、保留字面量、结构开关…) |
68
93
  | `VERSION` / `COMPILER_VERSION` | 包版本 / **编译产物版本**(两者独立,见下) |
69
94
 
70
95
  ### 两个版本号
@@ -22,10 +22,15 @@
22
22
 
23
23
  两条与 golden 直接相关的性质:
24
24
 
25
- - **弹窗是 fragment,不是完整文档**(`KIND_PROFILES.popup.root === 'fragment'`),
26
- 所以这些文件没有 `<html>` / `<head>`,第一个节点就是主题块。
27
- - **`form` / `countdown` 在弹窗里是 `engage-sdk` 运行时**:源里只有声明式空壳,
28
- 行为归 Engage SDK。core 给弹窗表单出脚本会和 SDK 抢同一张表单。
25
+ - **弹窗是片段型来源,不是完整文档**:这些文件没有 `<html>` / `<head>`,第一个节点
26
+ 就是主题块。进编辑器前由 `wrap()` 规范化成合规的三层树文档(根 div 认领成
27
+ `<body>`,合成 `section` + `container` 两层),保存时 `unwrap()` 剥回来。
28
+ core 里因此**没有「弹窗档」这回事** —— 包裹之后它与落地页走同一套规则。
29
+ 往返由 `popup-wrapped/` 下的 golden 与随包发布的复算脚本一起锁住。
30
+ - **组件在源里只有声明式空壳**:`data-pt-component` + `data-pt-component-settings`,
31
+ 没有一个子元素。core 的渲染器只出语义化 markup + settings、一行脚本都不出,
32
+ **行为归宿主的运行时** —— 谁提供它(落地页的 lp-sdk / 弹窗宿主的 SDK)是宿主
33
+ 的事,core 不认识它们。
29
34
 
30
35
  每份各带自己的 `@theme`(品牌色差异很大),这同时把「按主题 hash 分缓存条目」
31
36
  与「tailwind-merge 按 @theme 派生」两条路径都压上了真实数据。
@@ -193,6 +193,120 @@
193
193
  "input": "fixtures/popup/19-jp-clinic-coupon-placeholder.html",
194
194
  "sha256": "6b56860a22d361f1cca4419ce5ff0d974fbe08cd00b92ab6c1d48e190ca4c3ef"
195
195
  },
196
+ {
197
+ "fixture": "popup-wrapped/01-jp-coupon-line-tall.txt",
198
+ "kind": "wrap",
199
+ "input": "fixtures/popup/01-jp-coupon-line-tall.html",
200
+ "sha256": "143edd4f764be4246a8349c41328c36043de5a0a4ad3cf713a0f91388d923e76"
201
+ },
202
+ {
203
+ "fixture": "popup-wrapped/02-jp-webinar-doctor.txt",
204
+ "kind": "wrap",
205
+ "input": "fixtures/popup/02-jp-webinar-doctor.html",
206
+ "sha256": "855804cd54a8ef127c05ebbbb91eb06155f07fbb86047dc29b3f9dee8dfe94cd"
207
+ },
208
+ {
209
+ "fixture": "popup-wrapped/03-jp-model-recruit.txt",
210
+ "kind": "wrap",
211
+ "input": "fixtures/popup/03-jp-model-recruit.html",
212
+ "sha256": "374800322e7423ea3e8efc37f098e839a3dfba5ceaec6c110b10d53e7599b630"
213
+ },
214
+ {
215
+ "fixture": "popup-wrapped/04-jp-shinpi-campaign.txt",
216
+ "kind": "wrap",
217
+ "input": "fixtures/popup/04-jp-shinpi-campaign.html",
218
+ "sha256": "e2904cd41c320966d58811cef77e575d5fe68c02f42d1bbe0ba14afe6d37e1f4"
219
+ },
220
+ {
221
+ "fixture": "popup-wrapped/05-jp-jal-course.txt",
222
+ "kind": "wrap",
223
+ "input": "fixtures/popup/05-jp-jal-course.html",
224
+ "sha256": "cb981b5c82094235d92080e9e551a4199765e85f43a8d1b354e873d808fb4185"
225
+ },
226
+ {
227
+ "fixture": "popup-wrapped/06-jp-training-two-cta.txt",
228
+ "kind": "wrap",
229
+ "input": "fixtures/popup/06-jp-training-two-cta.html",
230
+ "sha256": "e8366c1ad2214d60bba105a6280f59f1e7496368d3f4e74f1223eca2a8a8e9a4"
231
+ },
232
+ {
233
+ "fixture": "popup-wrapped/07-vivaia-lightweight-shoes.txt",
234
+ "kind": "wrap",
235
+ "input": "fixtures/popup/07-vivaia-lightweight-shoes.html",
236
+ "sha256": "bfaaa574c18bf400ac606b0bc75020666cc5d6ef36ce8ec3509d09be0517dce1"
237
+ },
238
+ {
239
+ "fixture": "popup-wrapped/08-sticky-black-friday-countdown.txt",
240
+ "kind": "wrap",
241
+ "input": "fixtures/popup/08-sticky-black-friday-countdown.html",
242
+ "sha256": "44dd0e8f5572e6058f8bcb6384de9dd0adb1ab1850551ccb0d55ca92c129c862"
243
+ },
244
+ {
245
+ "fixture": "popup-wrapped/09-sticky-line-coupon-countdown.txt",
246
+ "kind": "wrap",
247
+ "input": "fixtures/popup/09-sticky-line-coupon-countdown.html",
248
+ "sha256": "3f9029cc7178d259458501cf13ef3345a97bdd16c9d615887a6cda63ec5405c8"
249
+ },
250
+ {
251
+ "fixture": "popup-wrapped/10-sticky-coupon-code-countdown.txt",
252
+ "kind": "wrap",
253
+ "input": "fixtures/popup/10-sticky-coupon-code-countdown.html",
254
+ "sha256": "18fc1c6c14b9e95a544f13866f9f015079a7d8a5dfc10be65fb93318d8e1d70d"
255
+ },
256
+ {
257
+ "fixture": "popup-wrapped/11-sticky-anniversary-countdown.txt",
258
+ "kind": "wrap",
259
+ "input": "fixtures/popup/11-sticky-anniversary-countdown.html",
260
+ "sha256": "fdb886c729550a56afadf922bb3000d077a93c16829327b523cd1012df3e27d6"
261
+ },
262
+ {
263
+ "fixture": "popup-wrapped/12-vivaia-product-grid.txt",
264
+ "kind": "wrap",
265
+ "input": "fixtures/popup/12-vivaia-product-grid.html",
266
+ "sha256": "03a4229d80cc65ad2436763cc255a9919192fbbc6b01c5cde6d66515b85ad506"
267
+ },
268
+ {
269
+ "fixture": "popup-wrapped/13-vivaia-restock-split.txt",
270
+ "kind": "wrap",
271
+ "input": "fixtures/popup/13-vivaia-restock-split.html",
272
+ "sha256": "045428b2d8fb67ccce2a6f8aa19c997328cd28d3160c22a6af9a44461f5d0765"
273
+ },
274
+ {
275
+ "fixture": "popup-wrapped/14-back-to-school-email-form.txt",
276
+ "kind": "wrap",
277
+ "input": "fixtures/popup/14-back-to-school-email-form.html",
278
+ "sha256": "848803e8547c3cc4602f19f838bd1b8436efe36cbd54a84d39078173aadc0c3d"
279
+ },
280
+ {
281
+ "fixture": "popup-wrapped/15-xtool-ebook-split-form.txt",
282
+ "kind": "wrap",
283
+ "input": "fixtures/popup/15-xtool-ebook-split-form.html",
284
+ "sha256": "aebaa9a6a663507d366e05a4af0e80c4016ec7a8d5ab214b75a36da5e0e032bb"
285
+ },
286
+ {
287
+ "fixture": "popup-wrapped/16-880-off-gradient-form.txt",
288
+ "kind": "wrap",
289
+ "input": "fixtures/popup/16-880-off-gradient-form.html",
290
+ "sha256": "f1b5e87503d289edde5d4a86b481b6cef109c7c21d8a09a47f34e942f5f70fe0"
291
+ },
292
+ {
293
+ "fixture": "popup-wrapped/17-holiday-sale-price-swatch.txt",
294
+ "kind": "wrap",
295
+ "input": "fixtures/popup/17-holiday-sale-price-swatch.html",
296
+ "sha256": "f9e120f0c80988b5949f2d104bfd59537c25d848a47b0a8e86bf1754560845bf"
297
+ },
298
+ {
299
+ "fixture": "popup-wrapped/18-solar-battery-dark-specs.txt",
300
+ "kind": "wrap",
301
+ "input": "fixtures/popup/18-solar-battery-dark-specs.html",
302
+ "sha256": "83b2b13027990b25d044e71e8b63e1bfd5e79731d4a6357a6209a0c31dce45ce"
303
+ },
304
+ {
305
+ "fixture": "popup-wrapped/19-jp-clinic-coupon-placeholder.txt",
306
+ "kind": "wrap",
307
+ "input": "fixtures/popup/19-jp-clinic-coupon-placeholder.html",
308
+ "sha256": "ede1722bd495e83076c7bda4766b4a96edd9d61565d7cf5e280051b9b9ee846e"
309
+ },
196
310
  {
197
311
  "fixture": "page-canonical/01-etvos-article-lp.txt",
198
312
  "kind": "document",
@@ -41,11 +41,16 @@ const opsData = JSON.parse(readFileSync(join(here, 'ops-cases.json'), 'utf8'));
41
41
  const opsByName = new Map(opsData.cases.map(c => [c.name, c]));
42
42
 
43
43
  const DOMParser = new JSDOM('').window.DOMParser;
44
- const { parse } = core.createCore({ DOMParser });
44
+ const { parse, wrap } = core.createCore({ DOMParser });
45
45
  const hostDoc = parse('<!DOCTYPE html><body>');
46
46
 
47
47
  const sha = s => createHash('sha256').update(s, 'utf8').digest('hex');
48
48
 
49
+ /** 同一份输入的片段条目哈希,供 wrap 分支内联断言往返一致。 */
50
+ const fragmentSha = new Map(
51
+ manifest.fixtures.filter(f => f.kind === 'fragment').map(f => [f.input, f.sha256])
52
+ );
53
+
49
54
  function compute(entry) {
50
55
  if (entry.kind === 'document') {
51
56
  return core.serialize(parse(readFileSync(join(here, entry.input), 'utf8')));
@@ -55,13 +60,30 @@ function compute(entry) {
55
60
  core.parseFragment(hostDoc, readFileSync(join(here, entry.input), 'utf8'))
56
61
  );
57
62
  }
63
+ if (entry.kind === 'wrap') {
64
+ const html = readFileSync(join(here, entry.input), 'utf8');
65
+ const doc = wrap(html);
66
+ // 往返:剥回来的片段,规范序列化后必须与同名 fragment 条目逐字节相同。
67
+ // 这条不单列清单条目,就地断言 —— 包裹有损的话症状滞后数天,必须在这里挡住。
68
+ const expected = fragmentSha.get(entry.input);
69
+ if (expected === undefined) {
70
+ throw new Error(`清单里缺少 ${entry.input} 的 fragment 条目,往返无从比对`);
71
+ }
72
+ const roundTrip = sha(core.serialize(core.unwrap(doc)));
73
+ if (roundTrip !== expected) {
74
+ throw new Error(
75
+ `wrap/unwrap 往返不一致\n 期望 ${expected}\n 实际 ${roundTrip}`
76
+ );
77
+ }
78
+ return core.serialize(doc);
79
+ }
58
80
  if (entry.kind === 'apply') {
59
81
  const name = entry.input.split('#')[1];
60
82
  const c = opsByName.get(name);
61
83
  if (!c) throw new Error(`ops-cases.json 里找不到用例 ${name}`);
62
84
  const html = opsData.wrapper.replace('{{HEAD_EXTRA}}', '').replace('{{BODY}}', c.body);
63
85
  const doc = parse(html);
64
- const r = core.apply(doc, c.ops, { profile: core.KIND_PROFILES.page });
86
+ const r = core.apply(doc, c.ops);
65
87
  if (!r.ok) throw new Error(`apply 失败 ${r.error.code} @${r.failedIndex}`);
66
88
  return core.serialize(doc);
67
89
  }