@gitruck/cli 0.2.15 → 0.2.16
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 +3 -2
- package/contracts/gsap-emit-v1.md +44 -2
- package/dist/index.js +725 -0
- package/package.json +1 -1
- package/skills/gtrk-mg/SKILL.md +5 -1
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
| ⬆️ | `gtrk upgrade` | 升级 CLI 到最新版 + 刷新 skill(配置保留);`--check` 只查不装 |
|
|
32
32
|
| 🎞️ | `gtrk render` | 本地渲染 gtrk 工程(EDL)→ 成片 mp4(需 ffmpeg) |
|
|
33
33
|
| 🔎 | `gtrk matrix` | B-roll 检索+**候选铺轨**:消费 FILM_BROLL 派单 → 产候选清单 + 下载 preview 代理铺 N 条候选轨(`--lay N` 默认 1,opencut 打开即可用轨道小眼睛对比;`--lay 0` 只出清单);`matrix search "<词>"` 单条 ad-hoc |
|
|
34
|
-
| 🎨 | `gtrk mg` | MG 动态图颗粒铺轨:消费 MG 派单 → 把 html-particle 颗粒(透明叠加 / 满屏底层,由你栏目的 MG 生产 skill 所产)铺进 `.gtrk` 的 beat_track;`mg lint <颗粒.html>`
|
|
34
|
+
| 🎨 | `gtrk mg` | MG 动态图颗粒铺轨:消费 MG 派单 → 把 html-particle 颗粒(透明叠加 / 满屏底层,由你栏目的 MG 生产 skill 所产)铺进 `.gtrk` 的 beat_track;`mg lint <颗粒.html>` 铁律静态子集校验、`mg status --project <dir>` 编排看板(缺 HTML / 已产未铺 / 已铺);aux 叠层颗粒同段多铺(一 beat 派生主 + `-aux<n>`)。旧名 `gtrk rrv` 保留为弃用别名 |
|
|
35
35
|
| 🧰 | `gtrk tool <name>` | 单点工具族:图转运镜、图片/视频抠像、图片去黑边/比例转换/净化/转方图/LivePhoto、视频去黑边/比例转换/防抖/蒸汽波滤镜/机械·智能分镜/运镜高光/智能字幕、人声伴奏分离/说话人分轨/变调变速、钢琴转MIDI/修复、音视频降噪、静音移除、MAD 等;`gtrk tool list` 查全部输入/产物/实时价格/状态。单发单收、共享 runner,接新工具只加一个 descriptor |
|
|
36
36
|
| 🚧 | `struct` | (规划中)已有 gtrk 转三方工程 |
|
|
37
37
|
|
|
@@ -374,9 +374,10 @@ gtrk transcript "D:/素材/采访视频.mp4" --lang zh-CN --out "D:/文字稿/
|
|
|
374
374
|
- **素材表不囤积**:素材的剥离键按「**自产身份 × 零引用**」判(自产 = `mg-`/`rrv-` 前缀 **或** 落在 CLI 独占的 `assets/mg/` 下且文件名在自产登记里),**不认客户端可改写的 `html_material` 前缀**——所以在 opencut 里编辑过工程之后重铺,旧素材照样剥得掉,`mg-` 素材数**恒等于轨上颗粒数**,历史遗留的重复 / 孤儿条目一并清掉。**非自产素材零连带**(`broll-*` / `ex-solid-*` / 你自加的,哪怕零引用也不碰);仍被存活 clip 引用的自产素材也不剥(不会剥出失联 clip);盘上 `assets/mg/` 的 html 副本从不删。
|
|
375
375
|
- **「一条都没定位到」不是清空指令**:`--only` 打空、`dispatch.mg` 为空/缺失、或本次条目全被 skip,**而轨上已有已铺颗粒**时,同样拒绝写回(那是派单或选择器出问题的信号)。确要清空加 `--replace-all`。首次铺轨(轨上本就没有已铺条目)不受此限,照常走完报 `laid=0`。
|
|
376
376
|
- **槽位窗口现场重投影**:铺轨与 lint 之前先用「`transcript` × 当刻 `.gtrk`」重算每条队列条目的 `[track_st, track_ed]`,之后 lint 的坑位包络(铁律⑦)与落轨 clip 时长一律以重算值为准(`--only` 同守;aux 派生颗粒按**自己的** span 重投影,不与主 beat 窗口混同)。`dispatch.mg` 里的时码只是**投影时刻快照**,仅在重投影不可行时兜底——**改完口播轨直接铺即可,不必先重跑 `gtrk split`**。`--json` 恒出 `reprojection:{mode,degraded,reason?,drifted,max_offset,shrunk,dropped}`(`--lint-only` 也有)。重投影后**零存活**的条目 skip 并计入 `skipped`(不复制 HTML、不按快照铺回去);重投影不可行(transcript 缺失 / 工程定位不到 / 主轨查不到口播素材)→ 降级用快照 + 告警 + `--json` 标注,退出码不变;工程**非 v1** 的既有行为不变(铺轨路径版本门非 0 退出、`--lint-only` 照旧出报告)。铺轨成功会把本次时码来源(`timecode_source` / `reprojected_at`)纯追加登记进 `struct_meta.mg`。
|
|
377
|
-
- **lint**(`gtrk mg lint <颗粒.html> [--dispatch <path>]`):纯本地静态校验颗粒 HTML
|
|
377
|
+
- **lint**(`gtrk mg lint <颗粒.html> [--dispatch <path>]`):纯本地静态校验颗粒 HTML 的铁律机器可判定子集(`<template>` 包裹、`data-composition-id` + 1920×1080、`gsap.timeline({ paused: true })`、`window.__timelines` 注册、无 `Math.random` / `Date.now`、自包含无相对外链、根 `background` 与 `opaque` 自洽…);给 `--dispatch` 时校验 `composition_id` 命中派单。任一致命项非 0 退出并逐条报因。
|
|
378
378
|
- **期望 id 一致性**(`1-cid-expect`,**致命**):HTML 内 `data-composition-id` 必须等于期望 id(铺轨=该条派单的 `composition_id`;`mg lint`=文件名,仅当它命中派单或形如 `…-B<数字>[-aux<n>]` 时比对,`./tmp.html` 这类改过名的副本不比对)。防的是「复制 `<id>.html` 改名时漏改内部 id」——落轨会写出以文件名命名的 clip/material,而文件注册的是另一个 `__timelines` 键、还与同名颗粒抢同一个样式作用域。
|
|
379
379
|
- **铁律⑦ tl 总长估长**(`7-fill-slot` / `7-no-estimate` / `7-infinite-repeat`,**恒非致命、不拦铺轨**):已知坑位包络时(铺轨逐颗;`mg lint --dispatch` 命中派单条目)对 GSAP 时间线做**静态下界估算**——逐调用降级,能解析的计入(`duration×(repeat+1) + repeatDelay×repeat`,`yoyo` 不加时长),表达式 position / 非字面量 duration 那条**跳过不计**(忽略若干调用仍是合法下界)。估长 < 包络 → 告警;一条都算不出 → 显式提示「无法静态估长,铁律⑦未校验,须真引擎 seek 验收」(**不静默**,「算不出」与「算过且通过」在输出上可区分);含 `repeat:-1` → 告警「无限循环令总长 Infinity、铁律⑦不可静态验证,请改按坑位算死的有限 repeat」。真判据永远是渲染引擎逐帧,本项只做提醒层。
|
|
380
|
+
- **铁律⑧重复图元合并**(`8-primitive-merge`,**恒非致命、不拦铺轨**):识别「循环体内创建,或由循环调用具名工厂创建;落到同一父节点;且没有逐元素动画驱动」的可合并 `line` / `rect` / `path` / `polyline` / `polygon` 批次。同一父节点的纯数字循环 trip count 累加后 **≥ 8** 才报数;边界含 `.length` / 具名常量而算不出时仍报「条数未知」,不做常量折叠;逐元素 `gsap.set` / tween 或被 tween 首实参使用的元素数组会被排除。本项只提示「这里有一批可**无损**合并的重复图元,合并后画面逐像素不变」,**不是风险判定**:命中不代表该颗粒会复现缺陷,未命中也不代表安全,真判据仍是真渲染出片抽帧。
|
|
380
381
|
- **回调与 seek 语义**(`x-callback-driven` / `x-engine-api-override` / `x-raf-interval`,**恒非致命、不拦铺轨**):对齐契约同名一节(2026-07-26 增补)。GSAP `seek(t)` 默认抑制回调 → 补间属性照常插值、但 `onUpdate` 里的 DOM 写入不执行,翻车形态是**画面定在初始态而非黑屏**。契约把保证压在**引擎侧**(定帧 MUST 用 `seek(t,false)` / `time(t)` / `progress(p)`),故颗粒**用回调驱动画面是合规写法**;lint 这三项只是**哨兵**:`x-callback-driven` = 回调写 DOM 且无任何 seek 兜底(有兜底则沉默,避免重复提醒);`x-engine-api-override` = 颗粒运行时覆写 `tl.seek` 或把 `__timelines[…]` 换成包装对象(会推翻引擎显式传的 `seek(t,true)`,且引擎改走 `time()`/`progress()` 即失效,属过渡态);`x-raf-interval` = 含 `requestAnimationFrame(` / `setInterval(`(自有时钟不被 seek,等于冻结)。三项 MUST NOT 致命——「用回调驱动画面」不是违规。
|
|
381
382
|
- **status**(`gtrk mg status --project <dir>`):汇总 MG 流水线——`dispatch.mg` beat 总数 / 已产源 HTML 数 / 已铺进 `.gtrk` 数,并逐 beat 标注(缺 HTML / 已产未铺 / 已铺)。
|
|
382
383
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **契约版本**:gsap-emit v1(2026-07-10;2026-07-24 增补铁律 7「占满坑位 + 终态驻留」,主理人硬性规定;同日铁律 6 增补字体注册表命中规则,对齐 gitruck-infra change `align-render-font-contract`;**2026-07-26 增补「回调与 seek 语义」一节**——补此前留白,首次对**渲染引擎侧**提出 MUST 条款,对齐 change `define-seek-suppress-events-contract`;**同日该节核实状态转「已核实(行为层)」**——真渲染引擎(`@hyperframes/producer@0.6.101`)三帧实测回调可达、结论绑定该引擎版本;
|
|
4
4
|
> **2026-07-26 铁律 4 按真机实测改写**——实心底 MUST 下沉为根下第一个全幅子层、根元素 MUST 保持零视觉、overlay 颗粒 MUST 在根显式写 `background:transparent`,
|
|
5
|
-
> 附证据锚并消歧铁律编号,对齐 change `align-particle-solid-backdrop-contract`)。产 HTML 动画颗粒、经同合云渲染管线(html_animate_render)逐帧 seek 合成的 skill/工具,其产物 MUST 满足本契约。
|
|
5
|
+
> 附证据锚并消歧铁律编号,对齐 change `align-particle-solid-backdrop-contract`;**同日增补铁律 8「重复图元合并」**——把整组同步驱动的重复图元合并成单元素,以规避一类真渲染横带、纵向重复叠印与逐帧闪烁缺陷;触发轴未知,本条只按零成本写法成文、不设数字门槛,对齐 change `add-particle-primitive-merge-law`)。产 HTML 动画颗粒、经同合云渲染管线(html_animate_render)逐帧 seek 合成的 skill/工具,其产物 MUST 满足本契约。
|
|
6
6
|
> **边界**:本契约只约束**机器可判定的管线消费属性**(封装/注册/确定性/自包含/依赖可达/禁 var()/字体名命中注册表)。画面长什么样——颜色、字体取值、构图、节奏——**一律由调用方按其栏目自身规则决定**,本契约不点名任何具体字体/颜色;文中示例取值均为中性占位。
|
|
7
7
|
|
|
8
8
|
## 原理(为什么不能用 CSS animation)
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
|
|
84
84
|
**本节口径优先于任何栏目指南的招式建议。** 栏目指南若与本节冲突,以本节为准。(当前二者同向:栏目指南推荐的 `onUpdate` 驱动写法在本节下**合规**,作坊侧**无需修订**——本节不向作坊侧派发任何待办。此条照写不误,它防的是将来的分歧。)
|
|
85
85
|
|
|
86
|
-
##
|
|
86
|
+
## 八条铁律(违反任一条 → 整片渲染失败 / 颗粒冻结 / 全黑 / 坑位内突兀消失 / 画面被切成横带、内容纵向重复叠印并逐帧闪烁)
|
|
87
87
|
|
|
88
88
|
1. **`<template>` 包裹根元素**:`<template><div data-composition-id="<id>" data-width="1920" data-height="1080">…</div></template>`。编译器取的是 `<template>` 内容;裸 `<div>` 会被判 "empty or could not be parsed" 整片失败。(1920×1080 为**当前引擎版本约束**,分辨率参数化预留——升版时以本契约新版为准。)
|
|
89
89
|
2. **GSAP `paused` 时间线 + 注册**:`var tl = gsap.timeline({paused:true}); … window.__timelines = window.__timelines || {}; window.__timelines["<id>"] = tl;`。`<id>` 必须等于根的 `data-composition-id`。
|
|
@@ -96,6 +96,12 @@
|
|
|
96
96
|
5. **脚本用渲染机可达的 CDN(编译期内联)**:`<script src="https://lib.baomitu.com/gsap/3.13.0/gsap.min.js"></script>` 或由渲染管线 vendor 本地。⚠️ jsdelivr 在渲染服务器不稳(实测 compile 期 `fetch failed` → GSAP 未加载 → 整片全黑)。编译器**只内联 http(s) CDN、不内联相对本地路径**(写 `src="gsap.min.js"` 运行时 404)。
|
|
97
97
|
6. **颜色/字体用字面值,禁 CSS `var()` 自定义变量;字体名 MUST 命中服务端注册字体表**:编译器/挂载不可靠地解析 var()(字体映射把 `var(--font-body)` 当字面字体名;颜色 var() 不应用 → 整片全黑,实测)。直接写字面值(如 `#RRGGBB` / `'某字体名'`);SVG 属性里同样禁 var()。栏目级换色/换主题 = **生成期**替换字面值(查调用方自己的词表/token 注入),不是运行时变量。**字体名规则(2026-07-24 增补)**:font-family 的每个具名家族 MUST 逐字符命中渲染服务端注册字体表(gitruck-infra 仓 `utils/assets/text/classic_template/font_manifest.json`,中英别名等价),并 SHOULD 以 `sans-serif`/`serif` 通用族收尾兜底;表外名字渲染不失败但**字形不保证**(服务端 fail-open 系统回退,2026-07-24 真机实锤:错名导致宋体被渲成回退黑体)。具体选哪款仍由调用方栏目规则决定,本契约不点名。
|
|
98
98
|
7. **占满坑位 + 终态驻留(2026-07-24 主理人硬性规定;2026-07-26 增补无限循环定性)**:颗粒时间线总长 MUST ≥ 它在成片中的**坑位时长**(落轨 clip 的实际时长,通常 = 派单槽位包络 `track_ed − track_st`,**不是** `duration_hint`);动画主叙事播完后,颗粒 MUST 以「**定格保持**」或「**有限次循环**」驻留到坑位末尾。**禁用无限循环 `repeat:-1`(2026-07-26 增补,补此前留白)**:无限循环让时间线总长为 `Infinity`,本条「总长 ≥ 坑位」就**变得不可验证**(机器与人都无从判断它是否真按坑位算过),且掩盖「作者根本没算坑位」这件事。循环次数 MUST **按坑位算死**——`repeat = ceil((坑位时长 − 循环起点) / 单圈时长) − 1`,宁可多算一两圈(末尾被 clip 裁掉无害),也不许写 `-1` 蒙混——坑位内任意时刻(含最后一帧)核心内容必须可见。**禁止**「整体渐隐到空 / 全局退场 / 清空画面」类收尾:渐隐会与剪辑层转场冲突,淡出与否由剪辑/装配层决定,不在颗粒内做。局部元素可按叙事退场(黯淡/让位),但画面在坑位内不得归零;**定格不动是完全合法的终态**(不必为凑动作密度在尾段硬加动画)。违反表现 = 观感上「动画一过完整个颗粒突兀消失」(2026-07-24 回声定位真机实测)。
|
|
99
|
+
8. **重复图元合并(2026-07-26 增补 · 规避条款,根治在渲染侧)**:同一批**重复图元**——同色、同 `stroke` / `fill`、**整组同步驱动**(只有组级动画或完全静态)的网格、排线、刻度、点阵——**MUST 合并成单个元素**(SVG 用一条 `<path>` 的多子路径 `M…H…V…`;同色分档时可按档合成若干条),**MUST NOT** 用「每条线 / 每个格子一个 `<element>`」的批量构建,无论它写成字面标签还是由 `createElementNS` 循环生成。合并前后画面**逐像素相同**,故本条不涉任何视觉取值。
|
|
100
|
+
- **豁免**:需要**逐元素动画**(stagger / 逐条画入 / 逐个变色)的批次不在本条范围内——元素身份是该批动画所必需的。此类批次若同时是满幅大面积绘制,MUST 以真渲染出片抽帧验收;这是保守兜底,**不是**「满幅即危险」的风险判据,反向的「不满幅即安全」同样不成立。
|
|
101
|
+
- **违反表现**:画幅被切成等高横带、相邻带内容互相纵向重复叠印(同一元素在 `y` 与 `y±带高` 各画一次,或被顶部裁半)并逐帧闪烁;该表现与动画逻辑无关,同帧里零动画的静态元素也会重复。DOM 元素数与 `getBoundingClientRect` 均正常,本地播放器与客户端预览看不出。
|
|
102
|
+
- **这是规避,不是根治**:缺陷出在渲染侧已由真机行为确证——颗粒的 DOM 元素数与布局值正常,不是颗粒写错。机制层面**推断**为渲染侧按等高带做局部栅格刷新时失效区域算错,**属推断、未经渲染侧确认**;**真实触发轴至今未知**,「图元总数」「同页累积负载」「同父同类聚簇」均已被跨颗粒真渲反例否证。本条 MUST NOT 被读成「图元少 / 负载低就安全」。
|
|
103
|
+
- **根治登记**:根治方 = 渲染管线侧;当前状态 = **未立项(2026-07-26;决策角色 = 本 change 实施者,按 proposal 的跨仓授权边界不代提;是否立项仍待主理人另行授权)**。
|
|
104
|
+
- **根治后处置预案**:待渲染管线侧根治上线且经真机确认缺陷不再复现后,本条由 **MUST 降为 SHOULD**,并保留「本条曾于 2026-07-26、本证据锚所载引擎版本口径下作为规避所必需」的历史记录。
|
|
99
105
|
|
|
100
106
|
> **⚠️ 铁律编号的唯一定义在本文件。** 本契约的**铁律 7 = 「占满坑位 + 终态驻留」**,这是框架侧对该条号的**唯一**定义。
|
|
101
107
|
> 创作侧(栏目 skill / 作坊)的模板与 references **MUST NOT 另立同号铁律**——引用请写「gsap-emit v1 铁律 7」并指回本文件,
|
|
@@ -132,6 +138,41 @@
|
|
|
132
138
|
`gitruck-infra` / `add-html-animate-opaque-fullscreen-cover`(**另一个独立问题**:渲染侧要不要按 `opaque` 位替颗粒兜一层黑底)。
|
|
133
139
|
⚠️ 两者**互不依赖**:上表的观测正是在后者**未 apply** 时取得的——即**本条的修法(下沉子层)当场生效,不需要渲染侧任何配合**。
|
|
134
140
|
|
|
141
|
+
### 铁律 8「重复图元合并」的真机证据锚(2026-07-26)
|
|
142
|
+
|
|
143
|
+
本条断言的是一条**作者侧规避写法**及其在真渲染中的效果,不断言已知触发轴。证据锚如下:
|
|
144
|
+
|
|
145
|
+
- **结论**:同一故障样本把 37 条满幅 `<line>` 合为一条 `<path>` 多子路径后,目标画面逐像素不变,横带重复率由 **65%~68% 降至 2%**;规避当场有效,但合并同时改变了图元数、DOM 结构与栅格化路径,故本实验**不能证明是哪一个变量生效**。
|
|
146
|
+
- **实验日期 / 机器**:2026-07-26 · r69(真渲,非本地无头模拟)。
|
|
147
|
+
- **引擎版本口径**:渲染引擎 CLI 与 producer 同版 `0.6.101` · 无头浏览器内核 `131.x`。
|
|
148
|
+
- **二分设计**:以同一颗粒为母本逐项只改一个构建块,每个变体各做一次真渲;每格抽取 168 帧,按等高横带间的重复像素统计复现率。装配路径、分辨率、帧率、底轨与其余颗粒代码保持一致。
|
|
149
|
+
- **范围限定(硬约束)**:下表全部是**同一颗粒内部**的二分结果,**MUST NOT 跨颗粒外推**;它只能说明这些改动在该颗粒内与复现率同向变化,不能推出「元素数少就安全」或任何数字门槛。
|
|
150
|
+
|
|
151
|
+
| 同一颗粒内的变体 | 重复率 | 限定读法 |
|
|
152
|
+
|---|---:|---|
|
|
153
|
+
| 原件:背景网格为 37 条满幅 `<line>` | **65%~68%**(复跑一致) | 故障基线 |
|
|
154
|
+
| 37 条 `<line>` 合为一条 `<path>` 多子路径 | **2%** | 本条规避正例;目标画面逐像素不变 |
|
|
155
|
+
| 整块删除背景网格 | **2%** | 与合并同档;只说明该块在本颗粒内参与复现 |
|
|
156
|
+
| 删除两个静态术语文本块 | **0%~1%** | 删除任一块负载都退烧;作用域仅限本颗粒,机制未知 |
|
|
157
|
+
| 删除三行静态标签文本块 | **0%~1%** | 同上 |
|
|
158
|
+
| 换字体族 / 去斜体 / 关闭 3D 加速 / 改入场位移 / 把文字改为 SVG `<text>` / 给根加变换提示 / 把网格改静态 | **仍为 65%+** | 均无效 |
|
|
159
|
+
| 增加满屏失效脉冲层 | **14%** | 作者侧强制全幅失效仍不能根治 |
|
|
160
|
+
| 极简结构(20 个静态 `<div>` + 1 个网格) | **0%** | 空载变体不复现;不得外推为通用预算 |
|
|
161
|
+
|
|
162
|
+
- **阳性对照(排除平凡解)**:合并版仍完整绘制与原件相同的整张网格,逐像素对拍一致;它不是「把内容删没了所以不重复」。同一颗粒内另有 24 条局部短线在合并版中继续保留,故 24 与 37 也 **MUST NOT** 被写成安全 / 故障分界。
|
|
163
|
+
- **跨颗粒反证(同批全量体检)**:同装配、同引擎、同机完成 **21 颗粒 + 六 beat 同页聚合,共 24 次渲染 / 约 22,000 帧**。这些结果专门限制上表的外推范围:
|
|
164
|
+
|
|
165
|
+
| 样本 | 真渲结果 | 可否证的候选轴 |
|
|
166
|
+
|---|---:|---|
|
|
167
|
+
| 另一密阵样本:884 条短划、全颗共 921 个绘制图元 | **0.0% 脏帧** | 图元总数、同父同类聚簇 |
|
|
168
|
+
| 六 beat 同页聚合:约 1,400 个绘制图元 | **0.0% 脏帧** | 同页累积负载 |
|
|
169
|
+
| 当前修复版样本(回归 / 阴性对照) | **0.0% 脏帧** | 规避在整轨分片路径下仍稳 |
|
|
170
|
+
| 未修版样本(阳性对照) | **70.3% 脏帧** | 缺陷仍活且可稳定复现 |
|
|
171
|
+
|
|
172
|
+
**真实触发轴至今未知**;当前已否证的候选轴 = **图元总数 / 同页累积负载 / 同父同类聚簇**。上表中「921 个图元干净」与「较少图元的阳性对照中招」同时成立,因此数字方向不存在可据此写入契约的安全门槛。
|
|
173
|
+
- **复现方式**:从同一母本生成上述单变量变体 → 走生产同款整轨分片渲染 → 对每格 168 帧计算等高横带间的重复率并抽取高分帧肉眼复核;再以同一口径跑 21 颗全量语料、六 beat 同页聚合、修复版阴性对照与未修版阳性对照。复现件的本地路径与文件名不进入分发契约。
|
|
174
|
+
- **相关 change / 根治状态**:本仓 `add-particle-primitive-merge-law` 收录作者侧规避;渲染管线侧根治**未立项(2026-07-26,本 change 实施会话按非目标不代提;待主理人另行决定)**。根治上线并经真机确认后,按铁律 8 的预案把 MUST 降为 SHOULD。
|
|
175
|
+
|
|
135
176
|
## 颗粒骨架(中性模板)
|
|
136
177
|
|
|
137
178
|
```html
|
|
@@ -188,4 +229,5 @@
|
|
|
188
229
|
- **`<template>` 内的 `<script>` 默认不执行**——引擎把 template 内容克隆进文档后才执行;本地直接开浏览器不会跑,必须经引擎/player。
|
|
189
230
|
- **transform-origin(SVG)**:缩放 `<g>` 用 GSAP `svgOrigin:"x y"`(SVG 用户坐标),别用 CSS transform-origin。
|
|
190
231
|
- **时间线总长 ≥ 坑位时长**:颗粒 tl 总时长 ≥ 落轨 clip 时长(坑位包络),否则 seek 越界(已升格为铁律 7,含终态驻留要求)。
|
|
232
|
+
- **整组同步驱动的重复图元必须合并**:网格、排线、刻度、点阵用单元素表达,SVG 以一条 `<path>` 的多子路径合成;逐元素动画批次豁免。详见 gsap-emit v1 铁律 8。
|
|
191
233
|
- **别用 `requestAnimationFrame`/`setInterval` 驱动画面**——不被 seek,等于冻结。所有视觉变化必须挂在 tl 上。(与「回调与 seek 语义」一节同源:任何**不经 tl** 的自有时钟都不被定帧驱动;而挂在 tl 上的回调是否被触发,则由该节的引擎侧条款保证。`gtrk mg lint` 对本条给**非致命**项 `x-raf-interval`——静态正则分不清「驱动画面」与其它用途,故只提醒不拦。)
|
package/dist/index.js
CHANGED
|
@@ -9318,6 +9318,725 @@ function namedFnBody(html, name) {
|
|
|
9318
9318
|
return null;
|
|
9319
9319
|
}
|
|
9320
9320
|
var NOT_A_CALL = new Set(["if", "for", "while", "switch", "catch", "return", "function", "typeof", "new", "delete", "void", "in", "of", "do", "else"]);
|
|
9321
|
+
var MERGEABLE_PRIMITIVES = new Set(["line", "rect", "path", "polyline", "polygon"]);
|
|
9322
|
+
var PRIMITIVE_MERGE_MIN = 8;
|
|
9323
|
+
function maskHtmlComments(src) {
|
|
9324
|
+
const out = src.split("");
|
|
9325
|
+
for (let at = src.indexOf("<!--");at >= 0; at = src.indexOf("<!--", at)) {
|
|
9326
|
+
const close = src.indexOf("-->", at + 4);
|
|
9327
|
+
const end = close < 0 ? src.length : close + 3;
|
|
9328
|
+
for (let i = at;i < end; i++)
|
|
9329
|
+
if (out[i] !== "\r" && out[i] !== `
|
|
9330
|
+
`)
|
|
9331
|
+
out[i] = " ";
|
|
9332
|
+
at = end;
|
|
9333
|
+
}
|
|
9334
|
+
return out.join("");
|
|
9335
|
+
}
|
|
9336
|
+
function scriptBodiesOnly(src) {
|
|
9337
|
+
const openRe = /<script\b[^>]*>/gi;
|
|
9338
|
+
const first = openRe.exec(src);
|
|
9339
|
+
if (!first)
|
|
9340
|
+
return src;
|
|
9341
|
+
const out = src.split("").map((c3) => c3 === "\r" || c3 === `
|
|
9342
|
+
` ? c3 : " ");
|
|
9343
|
+
let open2 = first;
|
|
9344
|
+
while (open2) {
|
|
9345
|
+
const bodyStart = open2.index + open2[0].length;
|
|
9346
|
+
const closeRe = /<\/script\s*>/gi;
|
|
9347
|
+
closeRe.lastIndex = bodyStart;
|
|
9348
|
+
const close = closeRe.exec(src);
|
|
9349
|
+
const bodyEnd = close?.index ?? src.length;
|
|
9350
|
+
for (let i = bodyStart;i < bodyEnd; i++)
|
|
9351
|
+
out[i] = src[i];
|
|
9352
|
+
openRe.lastIndex = close ? close.index + close[0].length : src.length;
|
|
9353
|
+
open2 = openRe.exec(src);
|
|
9354
|
+
}
|
|
9355
|
+
return out.join("");
|
|
9356
|
+
}
|
|
9357
|
+
function maskJsComments(src) {
|
|
9358
|
+
const out = src.split("");
|
|
9359
|
+
const blank = (from, to) => {
|
|
9360
|
+
for (let i = from;i < to; i++)
|
|
9361
|
+
if (out[i] !== "\r" && out[i] !== `
|
|
9362
|
+
`)
|
|
9363
|
+
out[i] = " ";
|
|
9364
|
+
};
|
|
9365
|
+
const canStartRegexAfter = new Set(["(", "[", "{", "=", ":", ",", ";", "!", "?", "&", "|", "+", "-", "*", "%", "^", "~", "<", ">"]);
|
|
9366
|
+
const keywordBeforeRegex = new Set(["return", "case", "throw", "else", "do", "yield", "await"]);
|
|
9367
|
+
const regexStartsAt = (at) => {
|
|
9368
|
+
let prev = at - 1;
|
|
9369
|
+
while (prev >= 0 && /\s/.test(out[prev]))
|
|
9370
|
+
prev--;
|
|
9371
|
+
if (prev < 0 || canStartRegexAfter.has(out[prev]))
|
|
9372
|
+
return true;
|
|
9373
|
+
if (!/[\w$]/.test(out[prev]))
|
|
9374
|
+
return false;
|
|
9375
|
+
let begin = prev;
|
|
9376
|
+
while (begin > 0 && /[\w$]/.test(out[begin - 1]))
|
|
9377
|
+
begin--;
|
|
9378
|
+
return keywordBeforeRegex.has(out.slice(begin, prev + 1).join(""));
|
|
9379
|
+
};
|
|
9380
|
+
for (let i = 0;i < src.length; i++) {
|
|
9381
|
+
const c3 = src[i];
|
|
9382
|
+
if (c3 === '"' || c3 === "'" || c3 === "`") {
|
|
9383
|
+
i = skipString(src, i);
|
|
9384
|
+
continue;
|
|
9385
|
+
}
|
|
9386
|
+
if (src.startsWith("<!--", i)) {
|
|
9387
|
+
const end2 = src.indexOf("-->", i + 4);
|
|
9388
|
+
const to = end2 < 0 ? src.length : end2 + 3;
|
|
9389
|
+
blank(i, to);
|
|
9390
|
+
i = to - 1;
|
|
9391
|
+
continue;
|
|
9392
|
+
}
|
|
9393
|
+
if (c3 === "/" && src[i + 1] === "/") {
|
|
9394
|
+
const end2 = src.indexOf(`
|
|
9395
|
+
`, i + 2);
|
|
9396
|
+
const to = end2 < 0 ? src.length : end2;
|
|
9397
|
+
blank(i, to);
|
|
9398
|
+
i = to - 1;
|
|
9399
|
+
continue;
|
|
9400
|
+
}
|
|
9401
|
+
if (c3 === "/" && src[i + 1] === "*") {
|
|
9402
|
+
const end2 = src.indexOf("*/", i + 2);
|
|
9403
|
+
const to = end2 < 0 ? src.length : end2 + 2;
|
|
9404
|
+
blank(i, to);
|
|
9405
|
+
i = to - 1;
|
|
9406
|
+
continue;
|
|
9407
|
+
}
|
|
9408
|
+
if (c3 !== "/" || src[i + 1] === "=" || !regexStartsAt(i))
|
|
9409
|
+
continue;
|
|
9410
|
+
let inClass = false;
|
|
9411
|
+
let end = -1;
|
|
9412
|
+
for (let j = i + 1;j < src.length; j++) {
|
|
9413
|
+
if (src[j] === "\\") {
|
|
9414
|
+
j++;
|
|
9415
|
+
continue;
|
|
9416
|
+
}
|
|
9417
|
+
if (src[j] === "\r" || src[j] === `
|
|
9418
|
+
`)
|
|
9419
|
+
break;
|
|
9420
|
+
if (src[j] === "[")
|
|
9421
|
+
inClass = true;
|
|
9422
|
+
else if (src[j] === "]")
|
|
9423
|
+
inClass = false;
|
|
9424
|
+
else if (src[j] === "/" && !inClass) {
|
|
9425
|
+
end = j + 1;
|
|
9426
|
+
while (end < src.length && /[A-Za-z]/.test(src[end]))
|
|
9427
|
+
end++;
|
|
9428
|
+
break;
|
|
9429
|
+
}
|
|
9430
|
+
}
|
|
9431
|
+
if (end < 0)
|
|
9432
|
+
continue;
|
|
9433
|
+
blank(i, end);
|
|
9434
|
+
i = end - 1;
|
|
9435
|
+
}
|
|
9436
|
+
return out.join("");
|
|
9437
|
+
}
|
|
9438
|
+
function maskJsStrings(src) {
|
|
9439
|
+
const out = src.split("");
|
|
9440
|
+
for (let i = 0;i < src.length; i++) {
|
|
9441
|
+
const c3 = src[i];
|
|
9442
|
+
if (c3 !== '"' && c3 !== "'" && c3 !== "`")
|
|
9443
|
+
continue;
|
|
9444
|
+
const end = skipString(src, i);
|
|
9445
|
+
for (let j = i;j <= end && j < out.length; j++)
|
|
9446
|
+
if (out[j] !== "\r" && out[j] !== `
|
|
9447
|
+
`)
|
|
9448
|
+
out[j] = " ";
|
|
9449
|
+
i = end;
|
|
9450
|
+
}
|
|
9451
|
+
return out.join("");
|
|
9452
|
+
}
|
|
9453
|
+
function closeParen(src, open2) {
|
|
9454
|
+
let depth = 0;
|
|
9455
|
+
for (let i = open2;i < src.length; i++) {
|
|
9456
|
+
const c3 = src[i];
|
|
9457
|
+
if (c3 === '"' || c3 === "'" || c3 === "`") {
|
|
9458
|
+
i = skipString(src, i);
|
|
9459
|
+
continue;
|
|
9460
|
+
}
|
|
9461
|
+
if (c3 === "/" && src[i + 1] === "/") {
|
|
9462
|
+
const nl = src.indexOf(`
|
|
9463
|
+
`, i);
|
|
9464
|
+
i = nl < 0 ? src.length : nl;
|
|
9465
|
+
continue;
|
|
9466
|
+
}
|
|
9467
|
+
if (c3 === "/" && src[i + 1] === "*") {
|
|
9468
|
+
const e = src.indexOf("*/", i + 2);
|
|
9469
|
+
i = e < 0 ? src.length : e + 1;
|
|
9470
|
+
continue;
|
|
9471
|
+
}
|
|
9472
|
+
if (c3 === "(")
|
|
9473
|
+
depth++;
|
|
9474
|
+
else if (c3 === ")" && --depth === 0)
|
|
9475
|
+
return i;
|
|
9476
|
+
}
|
|
9477
|
+
return -1;
|
|
9478
|
+
}
|
|
9479
|
+
function skipSpace(src, at) {
|
|
9480
|
+
while (at < src.length && /\s/.test(src[at]))
|
|
9481
|
+
at++;
|
|
9482
|
+
return at;
|
|
9483
|
+
}
|
|
9484
|
+
function braceStatementEnd(src, open2) {
|
|
9485
|
+
const body = braceBlock(src, open2);
|
|
9486
|
+
const close = open2 + 1 + body.length;
|
|
9487
|
+
return src[close] === "}" ? close + 1 : src.length;
|
|
9488
|
+
}
|
|
9489
|
+
function statementEnd(src, start) {
|
|
9490
|
+
const at = skipSpace(src, start);
|
|
9491
|
+
if (src[at] === "{")
|
|
9492
|
+
return braceStatementEnd(src, at);
|
|
9493
|
+
const keyword = /^([A-Za-z_$][\w$]*)\b/.exec(src.slice(at))?.[1] ?? "";
|
|
9494
|
+
if (keyword === "if" || keyword === "for" || keyword === "while" || keyword === "with" || keyword === "switch") {
|
|
9495
|
+
const open2 = src.indexOf("(", at + keyword.length);
|
|
9496
|
+
const close = open2 < 0 ? -1 : closeParen(src, open2);
|
|
9497
|
+
if (close < 0)
|
|
9498
|
+
return src.length;
|
|
9499
|
+
if (keyword === "switch") {
|
|
9500
|
+
const block = skipSpace(src, close + 1);
|
|
9501
|
+
return src[block] === "{" ? braceStatementEnd(src, block) : statementEnd(src, block);
|
|
9502
|
+
}
|
|
9503
|
+
const bodyEnd = statementEnd(src, close + 1);
|
|
9504
|
+
if (keyword !== "if")
|
|
9505
|
+
return bodyEnd;
|
|
9506
|
+
const next = skipSpace(src, bodyEnd);
|
|
9507
|
+
return /^else\b/.test(src.slice(next)) ? statementEnd(src, next + 4) : bodyEnd;
|
|
9508
|
+
}
|
|
9509
|
+
if (keyword === "do") {
|
|
9510
|
+
const bodyEnd = statementEnd(src, at + 2);
|
|
9511
|
+
const trailer = skipSpace(src, bodyEnd);
|
|
9512
|
+
if (!/^while\b/.test(src.slice(trailer)))
|
|
9513
|
+
return bodyEnd;
|
|
9514
|
+
const open2 = src.indexOf("(", trailer + 5);
|
|
9515
|
+
const close = open2 < 0 ? -1 : closeParen(src, open2);
|
|
9516
|
+
if (close < 0)
|
|
9517
|
+
return src.length;
|
|
9518
|
+
const semi = skipSpace(src, close + 1);
|
|
9519
|
+
return src[semi] === ";" ? semi + 1 : close + 1;
|
|
9520
|
+
}
|
|
9521
|
+
if (keyword === "try") {
|
|
9522
|
+
let end = statementEnd(src, at + 3);
|
|
9523
|
+
for (;; ) {
|
|
9524
|
+
const next = skipSpace(src, end);
|
|
9525
|
+
if (/^catch\b/.test(src.slice(next))) {
|
|
9526
|
+
let body = skipSpace(src, next + 5);
|
|
9527
|
+
if (src[body] === "(") {
|
|
9528
|
+
const close = closeParen(src, body);
|
|
9529
|
+
if (close < 0)
|
|
9530
|
+
return src.length;
|
|
9531
|
+
body = skipSpace(src, close + 1);
|
|
9532
|
+
}
|
|
9533
|
+
end = statementEnd(src, body);
|
|
9534
|
+
continue;
|
|
9535
|
+
}
|
|
9536
|
+
if (/^finally\b/.test(src.slice(next))) {
|
|
9537
|
+
end = statementEnd(src, next + 7);
|
|
9538
|
+
continue;
|
|
9539
|
+
}
|
|
9540
|
+
return end;
|
|
9541
|
+
}
|
|
9542
|
+
}
|
|
9543
|
+
let parens = 0;
|
|
9544
|
+
let brackets = 0;
|
|
9545
|
+
let braces = 0;
|
|
9546
|
+
for (let i = at;i < src.length; i++) {
|
|
9547
|
+
const c3 = src[i];
|
|
9548
|
+
if (c3 === '"' || c3 === "'" || c3 === "`") {
|
|
9549
|
+
i = skipString(src, i);
|
|
9550
|
+
continue;
|
|
9551
|
+
}
|
|
9552
|
+
if (c3 === "(")
|
|
9553
|
+
parens++;
|
|
9554
|
+
else if (c3 === ")")
|
|
9555
|
+
parens = Math.max(0, parens - 1);
|
|
9556
|
+
else if (c3 === "[")
|
|
9557
|
+
brackets++;
|
|
9558
|
+
else if (c3 === "]")
|
|
9559
|
+
brackets = Math.max(0, brackets - 1);
|
|
9560
|
+
else if (c3 === "{")
|
|
9561
|
+
braces++;
|
|
9562
|
+
else if (c3 === "}") {
|
|
9563
|
+
if (braces === 0 && parens === 0 && brackets === 0)
|
|
9564
|
+
return i;
|
|
9565
|
+
braces = Math.max(0, braces - 1);
|
|
9566
|
+
} else if (c3 === ";" && parens === 0 && brackets === 0 && braces === 0)
|
|
9567
|
+
return i + 1;
|
|
9568
|
+
else if ((c3 === "\r" || c3 === `
|
|
9569
|
+
`) && parens === 0 && brackets === 0 && braces === 0) {
|
|
9570
|
+
let prev = i - 1;
|
|
9571
|
+
while (prev >= at && /\s/.test(src[prev]))
|
|
9572
|
+
prev--;
|
|
9573
|
+
let next = skipSpace(src, i + 1);
|
|
9574
|
+
if (prev >= at && /[\w$)\]'"`}]/.test(src[prev]) && next < src.length && !/[.(\[`?+\-*/%&|^<>=,:]/.test(src[next]))
|
|
9575
|
+
return i;
|
|
9576
|
+
}
|
|
9577
|
+
}
|
|
9578
|
+
return src.length;
|
|
9579
|
+
}
|
|
9580
|
+
function numericForCount(header) {
|
|
9581
|
+
const parts = header.split(";");
|
|
9582
|
+
if (parts.length !== 3)
|
|
9583
|
+
return null;
|
|
9584
|
+
const init = /^(?:(?:var|let|const)\s+)?([A-Za-z_$][\w$]*)\s*=\s*(-?\d+(?:\.\d+)?)\s*$/.exec(parts[0].trim());
|
|
9585
|
+
if (!init)
|
|
9586
|
+
return null;
|
|
9587
|
+
const name = init[1];
|
|
9588
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9589
|
+
const cond = new RegExp(`^${escaped}\\s*(<=|<|>=|>)\\s*(-?\\d+(?:\\.\\d+)?)\\s*$`).exec(parts[1].trim());
|
|
9590
|
+
if (!cond)
|
|
9591
|
+
return null;
|
|
9592
|
+
const updateRaw = parts[2].trim();
|
|
9593
|
+
let step = null;
|
|
9594
|
+
if (new RegExp(`^(?:${escaped}\\s*\\+\\+|\\+\\+\\s*${escaped})$`).test(updateRaw))
|
|
9595
|
+
step = 1;
|
|
9596
|
+
else if (new RegExp(`^(?:${escaped}\\s*--|--\\s*${escaped})$`).test(updateRaw))
|
|
9597
|
+
step = -1;
|
|
9598
|
+
else {
|
|
9599
|
+
const by = new RegExp(`^${escaped}\\s*([+-])=\\s*(-?\\d+(?:\\.\\d+)?)$`).exec(updateRaw);
|
|
9600
|
+
if (by)
|
|
9601
|
+
step = (by[1] === "+" ? 1 : -1) * Number(by[2]);
|
|
9602
|
+
}
|
|
9603
|
+
if (step === null || !Number.isFinite(step) || step === 0)
|
|
9604
|
+
return null;
|
|
9605
|
+
const from = Number(init[2]);
|
|
9606
|
+
const to = Number(cond[2]);
|
|
9607
|
+
const op = cond[1];
|
|
9608
|
+
if (!Number.isFinite(from) || !Number.isFinite(to))
|
|
9609
|
+
return null;
|
|
9610
|
+
const compare = (value2) => op === "<" ? value2 < to : op === "<=" ? value2 <= to : op === ">" ? value2 > to : value2 >= to;
|
|
9611
|
+
if (!compare(from))
|
|
9612
|
+
return 0;
|
|
9613
|
+
if (step > 0 && op.startsWith(">") || step < 0 && op.startsWith("<"))
|
|
9614
|
+
return null;
|
|
9615
|
+
if (Number.isSafeInteger(from) && Number.isSafeInteger(to) && Number.isSafeInteger(step)) {
|
|
9616
|
+
const distance = step > 0 ? to - from : from - to;
|
|
9617
|
+
if (!Number.isSafeInteger(distance) || distance < 0)
|
|
9618
|
+
return null;
|
|
9619
|
+
const stride = Math.abs(step);
|
|
9620
|
+
const count2 = op === "<" || op === ">" ? Math.ceil(distance / stride) : Math.floor(distance / stride) + 1;
|
|
9621
|
+
return Number.isSafeInteger(count2) ? count2 : null;
|
|
9622
|
+
}
|
|
9623
|
+
let value = from;
|
|
9624
|
+
let count = 0;
|
|
9625
|
+
while (compare(value)) {
|
|
9626
|
+
if (++count > 1e6) {
|
|
9627
|
+
const distance = step > 0 ? to - from : from - to;
|
|
9628
|
+
const stride = Math.abs(step);
|
|
9629
|
+
const estimated = op === "<" || op === ">" ? Math.ceil(distance / stride) : Math.floor(distance / stride) + 1;
|
|
9630
|
+
return Number.isSafeInteger(estimated) && estimated >= 0 ? estimated : null;
|
|
9631
|
+
}
|
|
9632
|
+
const next = value + step;
|
|
9633
|
+
if (Object.is(next, value) || !Number.isFinite(next))
|
|
9634
|
+
return null;
|
|
9635
|
+
value = next;
|
|
9636
|
+
}
|
|
9637
|
+
return count;
|
|
9638
|
+
}
|
|
9639
|
+
function loopSites(html) {
|
|
9640
|
+
const out = [];
|
|
9641
|
+
const doWhileTrailers = new Set;
|
|
9642
|
+
const doRe = /\bdo\b/g;
|
|
9643
|
+
let dm;
|
|
9644
|
+
while (dm = doRe.exec(html)) {
|
|
9645
|
+
const bodyEnd = statementEnd(html, dm.index + dm[0].length);
|
|
9646
|
+
const trailer = skipSpace(html, bodyEnd);
|
|
9647
|
+
if (/^while\b/.test(html.slice(trailer)))
|
|
9648
|
+
doWhileTrailers.add(trailer);
|
|
9649
|
+
}
|
|
9650
|
+
const re = /\b(for|while)\s*\(/g;
|
|
9651
|
+
let m;
|
|
9652
|
+
while (m = re.exec(html)) {
|
|
9653
|
+
if (m[1] === "while" && doWhileTrailers.has(m.index))
|
|
9654
|
+
continue;
|
|
9655
|
+
const open2 = m.index + m[0].lastIndexOf("(");
|
|
9656
|
+
const close = closeParen(html, open2);
|
|
9657
|
+
if (close < 0)
|
|
9658
|
+
continue;
|
|
9659
|
+
let at = close + 1;
|
|
9660
|
+
while (at < html.length && /\s/.test(html[at]))
|
|
9661
|
+
at++;
|
|
9662
|
+
let bodyStart = at;
|
|
9663
|
+
let bodyEnd;
|
|
9664
|
+
if (html[at] === "{") {
|
|
9665
|
+
const body = braceBlock(html, at);
|
|
9666
|
+
bodyStart = at + 1;
|
|
9667
|
+
bodyEnd = bodyStart + body.length;
|
|
9668
|
+
} else
|
|
9669
|
+
bodyEnd = statementEnd(html, at);
|
|
9670
|
+
const kind = m[1];
|
|
9671
|
+
const header = html.slice(open2 + 1, close);
|
|
9672
|
+
out.push({
|
|
9673
|
+
kind,
|
|
9674
|
+
bodyStart,
|
|
9675
|
+
bodyEnd,
|
|
9676
|
+
count: kind === "for" ? numericForCount(header) : null,
|
|
9677
|
+
header
|
|
9678
|
+
});
|
|
9679
|
+
}
|
|
9680
|
+
return out;
|
|
9681
|
+
}
|
|
9682
|
+
function namedFunctionSites(code) {
|
|
9683
|
+
const out = [];
|
|
9684
|
+
const invocationAfter = (declStart, bodyEnd, expression) => {
|
|
9685
|
+
let before = declStart - 1;
|
|
9686
|
+
while (before >= 0 && /\s/.test(code[before]))
|
|
9687
|
+
before--;
|
|
9688
|
+
const expressionContext = expression || before >= 0 && /[(!=,:+\-~?]/.test(code[before]);
|
|
9689
|
+
let after = bodyEnd + 1;
|
|
9690
|
+
while (after < code.length && /\s/.test(code[after]))
|
|
9691
|
+
after++;
|
|
9692
|
+
if (code[after] === "(")
|
|
9693
|
+
return expressionContext;
|
|
9694
|
+
if (code[before] !== "(")
|
|
9695
|
+
return false;
|
|
9696
|
+
let groupingCount = 0;
|
|
9697
|
+
let beforeOpen = before;
|
|
9698
|
+
while (beforeOpen >= 0 && code[beforeOpen] === "(") {
|
|
9699
|
+
groupingCount++;
|
|
9700
|
+
beforeOpen--;
|
|
9701
|
+
while (beforeOpen >= 0 && /\s/.test(code[beforeOpen]))
|
|
9702
|
+
beforeOpen--;
|
|
9703
|
+
}
|
|
9704
|
+
if (beforeOpen >= 0 && /[\w$)\]]/.test(code[beforeOpen]))
|
|
9705
|
+
return false;
|
|
9706
|
+
for (let i = 0;i < groupingCount; i++) {
|
|
9707
|
+
if (code[after] !== ")")
|
|
9708
|
+
return false;
|
|
9709
|
+
after = skipSpace(code, after + 1);
|
|
9710
|
+
}
|
|
9711
|
+
return expressionContext && code[after] === "(";
|
|
9712
|
+
};
|
|
9713
|
+
const patterns = [
|
|
9714
|
+
{
|
|
9715
|
+
re: /\b(?:var|let|const)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:(?:async\s+)?function\s*\*?\s*[\w$]*\s*\([^)]*\)|(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>)\s*\{/g,
|
|
9716
|
+
nameGroup: 1,
|
|
9717
|
+
expression: true
|
|
9718
|
+
},
|
|
9719
|
+
{
|
|
9720
|
+
re: /\b(?:async\s+)?function\s*\*?\s+([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*\{/g,
|
|
9721
|
+
nameGroup: 1,
|
|
9722
|
+
expression: false
|
|
9723
|
+
},
|
|
9724
|
+
{
|
|
9725
|
+
re: /\b(?:async\s+)?function\s*\*?\s*\([^)]*\)\s*\{/g,
|
|
9726
|
+
nameGroup: null,
|
|
9727
|
+
expression: true
|
|
9728
|
+
},
|
|
9729
|
+
{
|
|
9730
|
+
re: /(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>\s*\{/g,
|
|
9731
|
+
nameGroup: null,
|
|
9732
|
+
expression: true
|
|
9733
|
+
}
|
|
9734
|
+
];
|
|
9735
|
+
for (const { re, nameGroup, expression } of patterns) {
|
|
9736
|
+
let m;
|
|
9737
|
+
while (m = re.exec(code)) {
|
|
9738
|
+
const open2 = m.index + m[0].lastIndexOf("{");
|
|
9739
|
+
const paired = braceBlock(code, open2);
|
|
9740
|
+
const bodyStart = open2 + 1;
|
|
9741
|
+
const bodyEnd = bodyStart + paired.length;
|
|
9742
|
+
if (out.some((fn) => fn.bodyStart === bodyStart))
|
|
9743
|
+
continue;
|
|
9744
|
+
const name = nameGroup === null ? null : m[nameGroup];
|
|
9745
|
+
const signature = m[0].slice(0, m[0].lastIndexOf("{"));
|
|
9746
|
+
const parenGroups = [...signature.matchAll(/\(([^()]*)\)/g)];
|
|
9747
|
+
const singleArrow = /=\s*([A-Za-z_$][\w$]*)\s*=>\s*$/.exec(signature);
|
|
9748
|
+
const rawParams = parenGroups.at(-1)?.[1] ?? singleArrow?.[1] ?? "";
|
|
9749
|
+
const params = new Set(rawParams.split(",").map((part) => /^([A-Za-z_$][\w$]*)/.exec(part.trim())?.[1]).filter((name2) => Boolean(name2)));
|
|
9750
|
+
out.push({
|
|
9751
|
+
name,
|
|
9752
|
+
declStart: m.index,
|
|
9753
|
+
bodyStart,
|
|
9754
|
+
bodyEnd,
|
|
9755
|
+
body: paired,
|
|
9756
|
+
params,
|
|
9757
|
+
immediatelyInvoked: invocationAfter(m.index, bodyEnd, expression)
|
|
9758
|
+
});
|
|
9759
|
+
}
|
|
9760
|
+
}
|
|
9761
|
+
const functionRe = /\b(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)?\s*\(/g;
|
|
9762
|
+
let fm;
|
|
9763
|
+
while (fm = functionRe.exec(code)) {
|
|
9764
|
+
const open2 = fm.index + fm[0].lastIndexOf("(");
|
|
9765
|
+
const close = closeParen(code, open2);
|
|
9766
|
+
if (close < 0)
|
|
9767
|
+
continue;
|
|
9768
|
+
let brace = close + 1;
|
|
9769
|
+
while (brace < code.length && /\s/.test(code[brace]))
|
|
9770
|
+
brace++;
|
|
9771
|
+
if (code[brace] !== "{")
|
|
9772
|
+
continue;
|
|
9773
|
+
const paired = braceBlock(code, brace);
|
|
9774
|
+
const bodyStart = brace + 1;
|
|
9775
|
+
const bodyEnd = bodyStart + paired.length;
|
|
9776
|
+
if (out.some((fn) => fn.bodyStart === bodyStart))
|
|
9777
|
+
continue;
|
|
9778
|
+
const prefix = code.slice(Math.max(0, fm.index - 160), fm.index);
|
|
9779
|
+
const assigned = /\b(?:var|let|const)\s+([A-Za-z_$][\w$]*)\s*=\s*$/.exec(prefix);
|
|
9780
|
+
const name = assigned?.[1] ?? fm[1] ?? null;
|
|
9781
|
+
const params = new Set(code.slice(open2 + 1, close).split(",").map((part) => /^([A-Za-z_$][\w$]*)/.exec(part.trim())?.[1]).filter((param) => Boolean(param)));
|
|
9782
|
+
out.push({
|
|
9783
|
+
name,
|
|
9784
|
+
declStart: fm.index,
|
|
9785
|
+
bodyStart,
|
|
9786
|
+
bodyEnd,
|
|
9787
|
+
body: paired,
|
|
9788
|
+
params,
|
|
9789
|
+
immediatelyInvoked: invocationAfter(fm.index, bodyEnd, assigned !== null || fm[1] === undefined)
|
|
9790
|
+
});
|
|
9791
|
+
}
|
|
9792
|
+
const arrowRe = /\b(?:var|let|const)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?\(/g;
|
|
9793
|
+
let am;
|
|
9794
|
+
while (am = arrowRe.exec(code)) {
|
|
9795
|
+
const open2 = am.index + am[0].lastIndexOf("(");
|
|
9796
|
+
const close = closeParen(code, open2);
|
|
9797
|
+
if (close < 0)
|
|
9798
|
+
continue;
|
|
9799
|
+
let arrow = skipSpace(code, close + 1);
|
|
9800
|
+
if (!code.startsWith("=>", arrow))
|
|
9801
|
+
continue;
|
|
9802
|
+
const brace = skipSpace(code, arrow + 2);
|
|
9803
|
+
if (code[brace] !== "{")
|
|
9804
|
+
continue;
|
|
9805
|
+
const paired = braceBlock(code, brace);
|
|
9806
|
+
const bodyStart = brace + 1;
|
|
9807
|
+
const bodyEnd = bodyStart + paired.length;
|
|
9808
|
+
if (out.some((fn) => fn.bodyStart === bodyStart))
|
|
9809
|
+
continue;
|
|
9810
|
+
const params = new Set(code.slice(open2 + 1, close).split(",").map((part) => /^([A-Za-z_$][\w$]*)/.exec(part.trim())?.[1]).filter((param) => Boolean(param)));
|
|
9811
|
+
out.push({
|
|
9812
|
+
name: am[1],
|
|
9813
|
+
declStart: am.index,
|
|
9814
|
+
bodyStart,
|
|
9815
|
+
bodyEnd,
|
|
9816
|
+
body: paired,
|
|
9817
|
+
params,
|
|
9818
|
+
immediatelyInvoked: invocationAfter(am.index, bodyEnd, true)
|
|
9819
|
+
});
|
|
9820
|
+
}
|
|
9821
|
+
return out.sort((a, b) => a.declStart - b.declStart);
|
|
9822
|
+
}
|
|
9823
|
+
var CREATE_PRIMITIVE = /\b(?:(?:var|let|const)\s+)?([A-Za-z_$][\w$]*)\s*=(?!=)\s*document\s*\.\s*createElementNS\s*\(\s*[^,]+,\s*["']([A-Za-z][\w-]*)["']\s*\)/gi;
|
|
9824
|
+
function escapedIdent(name) {
|
|
9825
|
+
return name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9826
|
+
}
|
|
9827
|
+
function appendedParent(body, elVar, baseAt, sameExecutionScope) {
|
|
9828
|
+
const el = escapedIdent(elVar);
|
|
9829
|
+
const appendRe = new RegExp(`\\b([A-Za-z_$][\\w$]*)\\s*\\.\\s*appendChild\\s*\\(\\s*${el}\\s*\\)`, "g");
|
|
9830
|
+
let append;
|
|
9831
|
+
while ((append = appendRe.exec(body)) && !sameExecutionScope(baseAt + append.index))
|
|
9832
|
+
;
|
|
9833
|
+
if (!append)
|
|
9834
|
+
return null;
|
|
9835
|
+
const reassignRe = new RegExp(`\\b(?:(?:var|let|const)\\s+)?${el}\\s*=(?!=)`, "g");
|
|
9836
|
+
let reassign;
|
|
9837
|
+
while (reassign = reassignRe.exec(body)) {
|
|
9838
|
+
if (reassign.index >= append.index)
|
|
9839
|
+
break;
|
|
9840
|
+
if (body[reassign.index - 1] === ".")
|
|
9841
|
+
continue;
|
|
9842
|
+
if (sameExecutionScope(baseAt + reassign.index))
|
|
9843
|
+
return null;
|
|
9844
|
+
}
|
|
9845
|
+
return append[1];
|
|
9846
|
+
}
|
|
9847
|
+
function perElementDriven(html, body, elVar) {
|
|
9848
|
+
const el = escapedIdent(elVar);
|
|
9849
|
+
if (new RegExp(`\\bgsap\\s*\\.\\s*set\\s*\\(\\s*${el}\\b`).test(body))
|
|
9850
|
+
return true;
|
|
9851
|
+
if (new RegExp(`\\b[A-Za-z_$][\\w$]*\\s*\\.\\s*(?:to|from|fromTo)\\s*\\(\\s*${el}\\b`).test(body))
|
|
9852
|
+
return true;
|
|
9853
|
+
const pushes = new Set;
|
|
9854
|
+
for (const pm of body.matchAll(new RegExp(`\\b([A-Za-z_$][\\w$]*)(?:\\s*\\[[^\\]]+\\])?\\s*\\.\\s*push\\s*\\(\\s*${el}\\s*\\)`, "g")))
|
|
9855
|
+
pushes.add(pm[1]);
|
|
9856
|
+
for (const arr of pushes) {
|
|
9857
|
+
const a = escapedIdent(arr);
|
|
9858
|
+
if (new RegExp(`\\b[A-Za-z_$][\\w$]*\\s*\\.\\s*(?:to|from|fromTo)\\s*\\(\\s*${a}(?:\\s*\\[[^\\]]+\\])?\\s*,`).test(html))
|
|
9859
|
+
return true;
|
|
9860
|
+
}
|
|
9861
|
+
return false;
|
|
9862
|
+
}
|
|
9863
|
+
function detectPrimitiveLoops(html) {
|
|
9864
|
+
const source = maskJsComments(scriptBodiesOnly(maskHtmlComments(html)));
|
|
9865
|
+
const code = maskJsStrings(source);
|
|
9866
|
+
const loops = loopSites(code);
|
|
9867
|
+
const functions = namedFunctionSites(code);
|
|
9868
|
+
const containingFunction = (at) => functions.filter((fn) => at >= fn.bodyStart && at < fn.bodyEnd).sort((a, b) => a.bodyEnd - a.bodyStart - (b.bodyEnd - b.bodyStart))[0] ?? null;
|
|
9869
|
+
const functionOwner = (fn) => containingFunction(fn.declStart);
|
|
9870
|
+
const hasOwnVariableBinding = (scope, name) => {
|
|
9871
|
+
if (scope.params.has(name))
|
|
9872
|
+
return true;
|
|
9873
|
+
const re = new RegExp(`\\b(?:var|let|const|class)\\s+${escapedIdent(name)}\\b`, "g");
|
|
9874
|
+
re.lastIndex = scope.bodyStart;
|
|
9875
|
+
let binding;
|
|
9876
|
+
while ((binding = re.exec(code)) && binding.index < scope.bodyEnd)
|
|
9877
|
+
if (containingFunction(binding.index) === scope)
|
|
9878
|
+
return true;
|
|
9879
|
+
return false;
|
|
9880
|
+
};
|
|
9881
|
+
const resolveFunction = (name, at) => {
|
|
9882
|
+
let scope = containingFunction(at);
|
|
9883
|
+
while (scope) {
|
|
9884
|
+
const local = functions.filter((fn) => fn.name === name && functionOwner(fn) === scope).sort((a, b) => b.declStart - a.declStart)[0];
|
|
9885
|
+
if (local)
|
|
9886
|
+
return local;
|
|
9887
|
+
if (scope.name === name)
|
|
9888
|
+
return scope;
|
|
9889
|
+
if (hasOwnVariableBinding(scope, name))
|
|
9890
|
+
return null;
|
|
9891
|
+
scope = functionOwner(scope);
|
|
9892
|
+
}
|
|
9893
|
+
return functions.filter((fn) => fn.name === name && functionOwner(fn) === null).sort((a, b) => b.declStart - a.declStart)[0] ?? null;
|
|
9894
|
+
};
|
|
9895
|
+
const parentBindingScope = (fn, parent) => {
|
|
9896
|
+
let scope = fn;
|
|
9897
|
+
while (scope) {
|
|
9898
|
+
if (hasOwnVariableBinding(scope, parent))
|
|
9899
|
+
return scope;
|
|
9900
|
+
scope = functionOwner(scope);
|
|
9901
|
+
}
|
|
9902
|
+
return null;
|
|
9903
|
+
};
|
|
9904
|
+
const freshParentDeclaration = (parent, before, scope) => {
|
|
9905
|
+
const ident = escapedIdent(parent);
|
|
9906
|
+
const re = new RegExp(`\\b(?:var|let|const)\\s+${ident}\\s*=\\s*(?:document\\s*\\.\\s*createElement(?:NS)?\\s*\\(|new\\b)`, "g");
|
|
9907
|
+
re.lastIndex = scope?.bodyStart ?? 0;
|
|
9908
|
+
let found = null;
|
|
9909
|
+
let m;
|
|
9910
|
+
while ((m = re.exec(code)) && m.index < before && (!scope || m.index < scope.bodyEnd))
|
|
9911
|
+
if (containingFunction(m.index) === scope)
|
|
9912
|
+
found = m.index;
|
|
9913
|
+
return found;
|
|
9914
|
+
};
|
|
9915
|
+
const loopsForOneParent = (owned, freshAt) => freshAt === null ? owned : owned.filter((loop) => freshAt < loop.bodyStart || freshAt >= loop.bodyEnd);
|
|
9916
|
+
const executionLoopsAt = (at) => {
|
|
9917
|
+
const fn = containingFunction(at);
|
|
9918
|
+
return loops.filter((loop) => {
|
|
9919
|
+
if (at < loop.bodyStart || at >= loop.bodyEnd)
|
|
9920
|
+
return false;
|
|
9921
|
+
if (fn)
|
|
9922
|
+
return loop.bodyStart >= fn.bodyStart && loop.bodyEnd <= fn.bodyEnd;
|
|
9923
|
+
return containingFunction(loop.bodyStart) === null;
|
|
9924
|
+
});
|
|
9925
|
+
};
|
|
9926
|
+
const loopProduct = (owned) => {
|
|
9927
|
+
if (owned.some((loop) => loop.count === null))
|
|
9928
|
+
return null;
|
|
9929
|
+
let product = 1;
|
|
9930
|
+
for (const loop of owned) {
|
|
9931
|
+
product *= loop.count;
|
|
9932
|
+
if (!Number.isSafeInteger(product))
|
|
9933
|
+
return null;
|
|
9934
|
+
}
|
|
9935
|
+
return product;
|
|
9936
|
+
};
|
|
9937
|
+
const innermostLoopBody = (at, owned = executionLoopsAt(at)) => {
|
|
9938
|
+
const loop = owned.sort((a, b) => a.bodyEnd - a.bodyStart - (b.bodyEnd - b.bodyStart))[0];
|
|
9939
|
+
return loop ? code.slice(loop.bodyStart, loop.bodyEnd) : "";
|
|
9940
|
+
};
|
|
9941
|
+
const grouped = new Map;
|
|
9942
|
+
const add = (tag, parent, count, driven, site, scope) => {
|
|
9943
|
+
const key = `${scope}\x00${tag}\x00${parent}`;
|
|
9944
|
+
const batch = grouped.get(key) ?? { tag, parent, count: 0, unknown: false, perElementDriven: false, sites: new Set };
|
|
9945
|
+
if (count === null)
|
|
9946
|
+
batch.unknown = true;
|
|
9947
|
+
else {
|
|
9948
|
+
batch.count += count;
|
|
9949
|
+
if (!Number.isSafeInteger(batch.count))
|
|
9950
|
+
batch.unknown = true;
|
|
9951
|
+
}
|
|
9952
|
+
batch.perElementDriven ||= driven;
|
|
9953
|
+
batch.sites.add(site);
|
|
9954
|
+
grouped.set(key, batch);
|
|
9955
|
+
};
|
|
9956
|
+
const creates = [];
|
|
9957
|
+
CREATE_PRIMITIVE.lastIndex = 0;
|
|
9958
|
+
let cm;
|
|
9959
|
+
while (cm = CREATE_PRIMITIVE.exec(source)) {
|
|
9960
|
+
if (!code[cm.index] || /\s/.test(code[cm.index]))
|
|
9961
|
+
continue;
|
|
9962
|
+
const tag = cm[2].toLowerCase();
|
|
9963
|
+
if (!MERGEABLE_PRIMITIVES.has(tag))
|
|
9964
|
+
continue;
|
|
9965
|
+
const fn = containingFunction(cm.index);
|
|
9966
|
+
const owned = executionLoopsAt(cm.index);
|
|
9967
|
+
const nearest = owned.slice().sort((a, b) => a.bodyEnd - a.bodyStart - (b.bodyEnd - b.bodyStart))[0];
|
|
9968
|
+
const bodyStart = nearest?.bodyStart ?? fn?.bodyStart ?? cm.index;
|
|
9969
|
+
const body = nearest ? code.slice(nearest.bodyStart, nearest.bodyEnd) : fn?.body ?? "";
|
|
9970
|
+
const tailStart = Math.max(0, cm.index - bodyStart) + cm[0].length;
|
|
9971
|
+
const parent = appendedParent(body.slice(tailStart), cm[1], bodyStart + tailStart, (at) => containingFunction(at) === fn);
|
|
9972
|
+
if (!parent)
|
|
9973
|
+
continue;
|
|
9974
|
+
creates.push({ at: cm.index, tag, elVar: cm[1], parent, fn, loops: owned });
|
|
9975
|
+
}
|
|
9976
|
+
const callsByFunction = new Map;
|
|
9977
|
+
const pushCall = (fn, at) => {
|
|
9978
|
+
const list = callsByFunction.get(fn.declStart) ?? [];
|
|
9979
|
+
list.push({ at, loops: executionLoopsAt(at) });
|
|
9980
|
+
callsByFunction.set(fn.declStart, list);
|
|
9981
|
+
};
|
|
9982
|
+
const functionNames = new Set(functions.map((fn) => fn.name).filter((name) => name !== null));
|
|
9983
|
+
for (const call of code.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g)) {
|
|
9984
|
+
const name = call[1];
|
|
9985
|
+
const at = call.index;
|
|
9986
|
+
let before = at - 1;
|
|
9987
|
+
while (before >= 0 && /\s/.test(code[before]))
|
|
9988
|
+
before--;
|
|
9989
|
+
if (NOT_A_CALL.has(name) || !functionNames.has(name) || code[before] === ".")
|
|
9990
|
+
continue;
|
|
9991
|
+
if (functions.some((fn) => fn.name === name && at >= fn.declStart && at < fn.bodyStart))
|
|
9992
|
+
continue;
|
|
9993
|
+
const target = resolveFunction(name, at);
|
|
9994
|
+
if (target)
|
|
9995
|
+
pushCall(target, at);
|
|
9996
|
+
}
|
|
9997
|
+
for (const fn of functions)
|
|
9998
|
+
if (fn.immediatelyInvoked)
|
|
9999
|
+
pushCall(fn, fn.bodyEnd);
|
|
10000
|
+
for (const create of creates) {
|
|
10001
|
+
if (!create.fn) {
|
|
10002
|
+
if (create.loops.length === 0)
|
|
10003
|
+
continue;
|
|
10004
|
+
const nearest = create.loops.slice().sort((a, b) => a.bodyEnd - a.bodyStart - (b.bodyEnd - b.bodyStart))[0];
|
|
10005
|
+
const body = innermostLoopBody(create.at, create.loops);
|
|
10006
|
+
const freshAt = freshParentDeclaration(create.parent, create.at, null);
|
|
10007
|
+
add(create.tag, create.parent, loopProduct(loopsForOneParent(create.loops, freshAt)), perElementDriven(code, body, create.elVar), `${nearest.kind} (${nearest.header.trim()})`, freshAt === null ? "shared-parent" : `fresh-parent:${freshAt}`);
|
|
10008
|
+
continue;
|
|
10009
|
+
}
|
|
10010
|
+
const fnCalls = callsByFunction.get(create.fn.declStart) ?? [];
|
|
10011
|
+
if (fnCalls.length === 0 && create.loops.length > 0) {
|
|
10012
|
+
const nearest = create.loops.slice().sort((a, b) => a.bodyEnd - a.bodyStart - (b.bodyEnd - b.bodyStart))[0];
|
|
10013
|
+
const boundParent = parentBindingScope(create.fn, create.parent);
|
|
10014
|
+
const freshAt = freshParentDeclaration(create.parent, create.at, boundParent);
|
|
10015
|
+
add(create.tag, create.parent, loopProduct(loopsForOneParent(create.loops, freshAt)), perElementDriven(code, innermostLoopBody(create.at, create.loops), create.elVar), `${nearest.kind} (${nearest.header.trim()})`, freshAt !== null ? `fresh-parent:${freshAt}` : boundParent ? `function:${boundParent.declStart}` : "shared-parent");
|
|
10016
|
+
}
|
|
10017
|
+
for (const call of fnCalls) {
|
|
10018
|
+
if (create.loops.length === 0 && call.loops.length === 0)
|
|
10019
|
+
continue;
|
|
10020
|
+
const inside = loopProduct(create.loops);
|
|
10021
|
+
const callerBody = innermostLoopBody(call.at, call.loops);
|
|
10022
|
+
const boundParent = parentBindingScope(create.fn, create.parent);
|
|
10023
|
+
const freshAt = freshParentDeclaration(create.parent, create.at, boundParent);
|
|
10024
|
+
const oneParentInside = loopProduct(loopsForOneParent(create.loops, freshAt));
|
|
10025
|
+
const oneParentOutside = loopProduct(loopsForOneParent(call.loops, freshAt));
|
|
10026
|
+
const freshOwnedByFactory = freshAt !== null && boundParent === create.fn;
|
|
10027
|
+
const count = freshOwnedByFactory ? oneParentInside : oneParentInside === null || oneParentOutside === null ? null : oneParentInside * oneParentOutside;
|
|
10028
|
+
add(create.tag, create.parent, count, perElementDriven(code, `${create.fn.body}
|
|
10029
|
+
${callerBody}`, create.elVar), create.fn.name ? `factory ${create.fn.name}` : `IIFE @${create.fn.declStart}`, freshAt !== null ? `fresh-parent:${freshAt}` : boundParent ? `function:${boundParent.declStart}` : "shared-parent");
|
|
10030
|
+
}
|
|
10031
|
+
}
|
|
10032
|
+
return [...grouped.values()].map((batch) => ({
|
|
10033
|
+
tag: batch.tag,
|
|
10034
|
+
parent: batch.parent,
|
|
10035
|
+
count: batch.unknown ? null : batch.count,
|
|
10036
|
+
perElementDriven: batch.perElementDriven,
|
|
10037
|
+
site: [...batch.sites].join(" + ")
|
|
10038
|
+
}));
|
|
10039
|
+
}
|
|
9321
10040
|
function bodyWritesDom(html, body, hops, seen) {
|
|
9322
10041
|
if (DOM_WRITE.test(body))
|
|
9323
10042
|
return true;
|
|
@@ -9466,6 +10185,12 @@ function lintParticle(html, opts = {}) {
|
|
|
9466
10185
|
else if (Number.isFinite(est) && est < slot)
|
|
9467
10186
|
push("7-fill-slot", false, `时间线静态估长 ~${est}s,短于槽位包络 ${slot}s(铁律⑦:颗粒应占满坑位并终态驻留)——静态估算是**下界**(${skipped} 条调用因无法静态解析被跳过),仅供参考,最终以真渲染引擎逐帧为准`);
|
|
9468
10187
|
}
|
|
10188
|
+
for (const batch of detectPrimitiveLoops(html)) {
|
|
10189
|
+
if (batch.perElementDriven || batch.count !== null && batch.count < PRIMITIVE_MERGE_MIN)
|
|
10190
|
+
continue;
|
|
10191
|
+
const amount = batch.count === null ? "条数未知(循环边界非数字字面量)" : `静态估算 ${batch.count} 个`;
|
|
10192
|
+
push("8-primitive-merge", false, `${batch.site} 向同一父节点「${batch.parent}」循环生成 ${amount} <${batch.tag}>。` + "这里有一批可无损合并的重复图元:请按相同 stroke/fill 分档合并成单个 <path> 的多子路径;" + "合并后画面逐像素不变,属零成本改法。本项只提示写法形态,最终画面仍以真渲染出片为准");
|
|
10193
|
+
}
|
|
9469
10194
|
return { ok: !v.some((x) => x.fatal), violations: v, opaque, compositionId: cid };
|
|
9470
10195
|
}
|
|
9471
10196
|
|
package/package.json
CHANGED
package/skills/gtrk-mg/SKILL.md
CHANGED
|
@@ -60,7 +60,7 @@ gtrk mg status --project "<split产物目录>" --json
|
|
|
60
60
|
gtrk mg lint "<产物目录>/mg/<composition_id>.html" --dispatch "<产物目录>/split/dispatch.json"
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
- 纯本地静态校验颗粒 HTML
|
|
63
|
+
- 纯本地静态校验颗粒 HTML 的**铁律机器可判定子集**:`<template>` 包裹、`data-composition-id` + 1920×1080、`gsap.timeline({ paused: true })`、`window.__timelines` 注册、无 `Math.random` / `Date.now`(可逐帧 seek 的确定性)、自包含无相对外链、`background` 声明与 `opaque` 自洽…;给 `--dispatch` 会额外校验 `composition_id` 命中派单。
|
|
64
64
|
- **`1-cid-expect`(致命)——复制改名必查**:颗粒内部的 `data-composition-id` 必须等于期望 id(`--dispatch` 命中时 = 该条派单的 `composition_id`,否则 = 文件名)。**你从别的颗粒复制模板时最容易漏改内部 id**:那样铺出来的 clip 指向 A、文件注册的却是 `__timelines["B"]`,渲染必错,且两颗粒抢同一个 `[data-composition-id]` 样式作用域。改名的临时副本(`./tmp.html`)不比对,不会误伤。
|
|
65
65
|
- **铁律④「实心底与透明度」两项恒非致命**(正本条款 = `contracts/gsap-emit-v1.md` 铁律 4,含 2026-07-26 真机证据锚;**本处只说看到该码怎么办,元素位置以契约为准**):
|
|
66
66
|
- `4-bg-on-root`:这颗把**实心底写在了根元素的 `style` 上**。**根元素的绘制属性会在子合成挂载时被丢弃**——那层底在成片里**一个像素都不落地**:前景照常渲出、底没了、底轨透出来,观感是「浮空面板」;而**本地播放器与客户端预览都看不出**(预览会按登记的 `opaque` 位自己给根盒打底,对这个问题结构性失明)。**报了要回步骤 2 让生产 skill 把实心底下沉为子层**——改法是机械的(把那条 `background` 声明整体搬到根下第一个全幅子层,根上改写 `background:transparent`)。⚠️ 它**不影响退出码、不拦铺轨**(避免存量颗粒一夜之间铺不进去),但**不是可以放着不管的噪音**:不改就是出片丢底。
|
|
@@ -71,6 +71,10 @@ gtrk mg lint "<产物目录>/mg/<composition_id>.html" --dispatch "<产物目录
|
|
|
71
71
|
- `7-no-estimate`:一条时长调用都解析不到 → **明说「铁律⑦这颗没校验」**,不是通过。这时 tl 总长只能靠真引擎 seek 或人读代码确认,别当它过了。
|
|
72
72
|
- `7-infinite-repeat`:颗粒里写了 `repeat:-1` → 总长变 `Infinity`、铁律⑦不可验证(契约 2026-07-26 已明文禁)。让生产 skill 改成按坑位算死的有限次数:`repeat = ceil((坑位 − 循环起点) / 单圈) − 1`。
|
|
73
73
|
- 三项都**不影响退出码、不拦铺轨**——真判据是渲染引擎逐帧,静态估长只做提醒。**但「没报 7-fill-slot」≠「时长够了」**,要看是否同时出了 `7-no-estimate`。
|
|
74
|
+
- **铁律⑧「重复图元合并」为恒非致命提示,但看见必须处理**:
|
|
75
|
+
- `8-primitive-merge`:这颗有一批由循环直接创建、或由循环调用具名工厂创建的可合并重复图元;它们落到同一父节点,且没有逐元素动画驱动。纯数字循环可算且同父累计 **≥ 8** 时会报数;边界算不出时会报「条数未知」。逐元素 `gsap.set` / tween、或作为 tween 首实参使用的元素数组会被排除。
|
|
76
|
+
- 本项报的是「这批图元可**无损**合并,合并后画面逐像素不变」这一写法事实,**不是**「该颗粒有缺陷」或「超过某个数量会出问题」;真实触发轴未知。反过来,没报也**不代表安全**,真判据仍是真渲染出片抽帧。
|
|
77
|
+
- **处置**:回步骤 2,把原始提示完整交给栏目 MG 生产 skill,让它把整组同步驱动的网格 / 排线 / 刻度 / 点阵合成单元素(SVG 用一条 `<path>` 多子路径;同色分档可分档合成),然后重产、重 lint。**别硬铺,也别当没看见**;CLI 为兼容存量把它做成**非致命、不影响退出码、不拦铺轨**,不等于 agent 可以跳过修正。确需 stagger / 逐条画入 / 逐个变色的逐元素动画批次属于契约豁免,不要为了消项删掉动画身份。
|
|
74
78
|
- **「回调与 seek 语义」三项恒非致命**(契约同名一节,2026-07-26 增补;**报了不用改颗粒**):
|
|
75
79
|
- `x-callback-driven`:这颗的画面靠 `onUpdate` 等时间线回调写 DOM 驱动,且没带任何 seek 兜底(GSAP `seek(t)` 的 `suppressEvents` 缺省为 `true`,会吞掉回调)。**该写法合规**——回调可达性由契约压在**引擎侧**的 MUST 条款保证(引擎定帧 MUST 用 `seek(t,false)` / `time(t)` / `progress(p)`),**且该条款已于 2026-07-26 经真渲染引擎核实**(producer `0.6.101`,纯回调驱动无垫片颗粒三帧读数各不相同)。本项**核实之后照留**做**哨兵**:结论绑死引擎版本,引擎换实现或失守时补间属性照常插值、回调不跑 → 画面**静默定格在初始态**(不是黑屏,本地播放器和预览都看不出)。**别因为它去让生产 skill 改写法,更别让它自己加垫片。**
|
|
76
80
|
- `x-engine-api-override`:颗粒运行时覆写了 `tl.seek`(老颗粒常见的 `rr-seek-shim`)或把 `__timelines[…]` 换成了包装对象。它会推翻引擎显式传的 `seek(t, true)`,且引擎改走 `time()`/`progress()` 就完全失效。**既有垫片属过渡态**:2026-07-26 引擎侧结论已核实(引擎本就不抑制回调),垫片已无保护作用、也无害,**可择期清理**(删后须重跑 lint 并重渲复验),不清也不拦;新颗粒别再加。
|