如何用 @hyperframes/parsers 解析 HTML 合成并安全改写 GSAP 时间线脚本
【免费下载链接】hyperframesWrite HTML. Render video. Built for agents.项目地址: https://gitcode.com/GitHub_Trending/hy/hyperframes
当你在 hyperframes 组合(composition)之上构建工具——例如让 agent 自动调整动画节奏、批量移动时间线上的元素,或做一个“改完写回原文件”的自动化脚本——就需要先把 HTML 组合解析成结构化模型,再修改其中的 GSAP 时间线脚本,同时保证脚本里与目标动画无关的代码一行都不变。hyperframes 仓库里的@hyperframes/parsers包就是为这一层设计的:它没有@hyperframes/*运行时依赖,可以脱离框架其余部分单独使用。GSAP 改写的核心是“解析 → 变更 → 重新序列化”的往返(round-trip):写路径按字节区间做 splice,编辑区间之外的内容逐字节保留。
这条路径的完整操作顺序是:安装包 → 解析 HTML 组合取出 GSAP 脚本 → 把脚本解析成结构化动画模型 → 用 writer 助手函数改写 → 通过再解析与校验确认结果。
安装与导入入口
npm install @hyperframes/parsers官方文档明确提示:大多数用户不需要直接安装@hyperframes/parsers——@hyperframes/core已经重新导出了它需要的解析器 API,CLI 和 studio 也是传递依赖它。只有当你想要纯解析层、不引入 core 的 runtime / compiler / generators 时,才直接使用这个包。
包提供一组聚焦的 subpath 入口,本场景涉及的是:
| 导入路径 | 用途 |
|---|---|
@hyperframes/parsers | HTML 解析器、GSAP 序列化/校验助手、hf-ids、共享类型 |
@hyperframes/parsers/gsap-parser-acorn | Acorn 实现的 GSAP 解析器(读路径,browser-safe) |
@hyperframes/parsers/gsap-writer-acorn | Acorn 实现的 GSAP 写路径(mutation 助手函数) |
@hyperframes/parsers/gsap-constants | SUPPORTED_PROPS、SUPPORTED_EASES、属性分组 |
@hyperframes/parsers/hf-ids | 确定性的元素 id 打戳 |
两个实现要点:读路径基于 acorn + acorn-walk,无 Node 全局、无fs/require,可以打进浏览器 bundle;写路径基于 magic-string 做字节区间覆写,编辑区间外的每一个字节都原样保留,没有任何 pretty-printer 重排。@hyperframes/parsers/gsap-parser-recast是遗留的 recast 实现,新代码走 acorn 路径。
解析 HTML 组合,取出时间线脚本
把组合 HTML 文件读入字符串后交给parseHtml,它在 HTML 与结构化数据之间往返:
import { parseHtml, extractCompositionMetadata, validateCompositionHtml, } from '@hyperframes/parsers'; // 把 HTML 解析成结构化数据 const parsed = parseHtml(htmlString); // parsed.elements, parsed.gsapScript, parsed.styles, parsed.resolution, parsed.keyframes // 提取组合元数据(id、duration、尺寸、variables) const meta = extractCompositionMetadata(htmlString); // 校验 HTML 结构 const result = validateCompositionHtml(htmlString); // result.valid, result.errors如果validateCompositionHtml返回valid: false,先根据errors处理文档结构问题,再进入脚本改写环节。后续步骤的操作对象是parsed.gsapScript这个字符串。
如果工具需要稳定的元素标识来做 diff 与编辑,先用ensureHfIds给文档打戳:
import { ensureHfIds } from '@hyperframes/parsers/hf-ids'; // 给文档中每个可编辑元素盖上 hf-id 属性 const withIds = ensureHfIds(htmlString);hf-id是确定性的:同一份文档每次打戳结果相同,因此可以作为跨轮次 diff 和定位改写的锚点。只改时间线、不涉及元素身份识别的工具可以跳过这一步。
把时间线脚本解析成结构化模型
对parsed.gsapScript调用parseGsapScriptAcorn,得到ParsedGsap对象:
import { parseGsapScriptAcorn, extractGsapLabels } from '@hyperframes/parsers/gsap-parser-acorn'; const gsap = parseGsapScriptAcorn(scriptContent); // gsap.animations, gsap.timelineVar, gsap.preamble, gsap.postamble几个关键点:
gsap.timelineVar是时间线变量名(tl = gsap.timeline()中的tl),writer 生成和定位 tween 语句时依赖它;gsap.animations每一项是GsapAnimation:id、targetSelector、method(set/to/from/fromTo)、position、properties、duration、ease,以及可选的keyframes与extras(stagger、yoyo、repeat 这类不可编辑配置,往返时会被保留);id是基于内容生成的,不是基于位置——同一条 tween 无论排在脚本哪个位置,id 都稳定。所以“先按targetSelector或 id 找到动画,再拿 id 去改写”是安全的定位方式;extractGsapLabels(script)可以取出脚本中的 label 信息,供需要按 label 跳转的工具使用;ParsedGsap上还有multipleTimelines和unsupportedTimelinePattern两个可选标志,使用 writer 之前先确认脚本落在受支持的单一时间线形态内。
改动前,用常量表核对要写入的属性与缓动值是否在支持集合里:
import { SUPPORTED_PROPS, SUPPORTED_EASES } from '@hyperframes/parsers/gsap-constants';用 writer 助手函数改写脚本
所有 mutation 助手都直接操作脚本文本并返回新字符串,不相关的代码原样保留。主路径 API 如下(签名与仓库源码一致):
import { updateAnimationInScript, addAnimationToScript, removeAnimationFromScript, shiftPositionsInScript, scalePositionsInScript, } from '@hyperframes/parsers/gsap-writer-acorn'; // 更新单条动画(文档中的标准示例) const next = updateAnimationInScript(scriptContent, animationId, { duration: 2 }); // 新增动画,返回值包含新条目的内容式 id const { script: withNew, id: newId } = addAnimationToScript(scriptContent, newAnimation); // 按 id 移除一条动画 const cleaned = removeAnimationFromScript(scriptContent, animationId); // 把某个 selector 的全部 tween 平移 delta 秒(结果位置被钳制在 ≥ 0) const moved = shiftPositionsInScript(scriptContent, '#card', 0.5); // 把旧片段 [oldStart, oldDuration] 线性重映射到 [newStart, newDuration](position 与 duration 按比例换算) const resized = scalePositionsInScript(scriptContent, '#card', 0, 2, 0.5, 3);实现中内置了几条安全性质,对照 gsapWriterAcorn.ts 源码可以逐一确认:
- 失败即 no-op:writer 助手内部先执行
parseGsapScriptAcornForWrite,解析失败或animationId找不到时,原样返回输入脚本。也就是说“返回的字符串和输入完全相同”本身就是一次信号——改写没有生效,先回到解析结果排查,而不是以为改掉了; - 更新语义要分清:
updateAnimationInScript传properties是替换可编辑属性集合(不在更新里的可编辑 key 会被移除);只传duration/ease/extras是就地 upsert;传position会覆写 tween 调用的位置参数(调用原本没有显式位置参数时则补一个); - 每个元素只保留一个位置写入:
dedupePositionWritesInScript(script, selector, keepId?)会移除同一 selector 上其他纯位置写入(包括duration: 0的退化 tween),避免两处位置写入互相静默覆盖。提交位置类改动(拖拽、加 keyframe)之后应调用它维持该不变量; - keyframe 级编辑:对使用
keyframes: { ... }的脚本,另有updateKeyframeInScript/addKeyframeToScript/removeKeyframeFromScript等助手。
如果脚本里的值是运行时动态算出来的,注意可编辑性边界。解析器会给每条 tween 标注来源(provenance),并据此划分三种可编辑性:
direct—— 字面量 tween,可直接就地编辑;unroll—— helper/循环展开生成,需先展开成字面量 tween 再编辑;source—— 运行时动态值,静态编辑不适用,只能直接改源码。
所以改写前检查目标动画的hasUnresolvedSelector/hasUnresolvedKeyframes;值来自source的动画,AST 改写路径不适用,改代码本身。
端到端示例与结果验证
下面是一个可运行的最小端到端示例(脚本内容为示例值,API 调用可直接执行):
import { parseGsapScriptAcorn } from '@hyperframes/parsers/gsap-parser-acorn'; import { updateAnimationInScript, shiftPositionsInScript } from '@hyperframes/parsers/gsap-writer-acorn'; import { validateCompositionGsap } from '@hyperframes/parsers'; const script = [ 'const tl = gsap.timeline();', "tl.from('#title', { opacity: 0, y: 40, duration: 0.8, ease: 'power2.out' }, 0);", "tl.to('#card', { x: 120, duration: 1.2, ease: 'power1.inOut' }, 0.4);", ].join('\n'); // 1) 解析并定位动画 const before = parseGsapScriptAcorn(script); const target = before.animations.find((a) => a.targetSelector === '#card'); // 2) 改写:把 duration 改为 2s,再把该 selector 整体后移 0.5s let next = updateAnimationInScript(script, target.id, { duration: 2 }); next = shiftPositionsInScript(next, '#card', 0.5); // 3) 验证:重新解析,确认改动落地、条目数量未变 const after = parseGsapScriptAcorn(next); const edited = after.animations.find((a) => a.id === target.id); console.assert(edited.duration === 2, 'duration should be 2'); console.assert(edited.position === 0.9, 'position should be 0.4 + 0.5'); console.assert(after.animations.length === before.animations.length, 'animation count unchanged'); // 4) 脚本级校验 const check = validateCompositionGsap(next); console.assert(check.valid, check.errors.join('; '));验证逻辑逐条说明:
- 再解析 diff:
id基于内容稳定,shiftPositionsInScript只重写目标 selector 的位置参数,所以“同 id + 字段已更新 + 总数不变”三者同时成立,才说明改到了正确的那条动画且没伤及其他条目; - no-op 检查:如果
next === script,说明改写完全没有发生——脚本没解析成功或 id 不匹配,回到解析结果里核对; - 脚本级校验:
validateCompositionGsap返回valid与errors;也可以用getAnimationsForElementId拉取某元素的全部动画做交叉核对; - 仓库自身用 golden 文件锁定这套往返契约:goldens目录 提供 minimal、moderate、complex、fromto 四组
.parsed.json/.serialized.js样本对,gsapParser.golden.test.ts 用它们断言解析与序列化结果,gsapWriter.parity.test.ts 交叉核对 acorn 与 recast 两种写实现的输出。如果你的批量改写工具需要回归保障,可以照这个思路为自己的一组输入/输出样本建 golden 断言。
边界与不支持项
- 不要使用 recast 路径:
@hyperframes/parsers/gsap-parser-recast是遗留实现,源码注释明确读路径已切换到parseGsapScriptAcorn、写路径切换到 acorn writer;新代码一律用gsap-parser-acorn/gsap-writer-acorn。 asset-pathssubpath 仅限 Node:@hyperframes/parsers/asset-paths提供的资产路径改写助手是 Node-only 的;主入口与 acorn 解析/写路径是 browser-safe。工具若要在浏览器运行,不要引入该 subpath。- 单一时间线形态:编辑前确认
ParsedGsap的multipleTimelines/unsupportedTimelinePattern均未置位,否则不要对该脚本走 writer 路径。 - 与 core 的 API 不重复引入:如果你的工具已经依赖
@hyperframes/core,直接使用它重新导出的解析器 API 即可;@hyperframes/lint与@hyperframes/studio-server同样构建在 parsers 之上,写校验或预览类工具时可以先看这两个包的消费方式。
下一步
- 本包的完整入口清单与使用提示见 parsers 包文档;
- 改写后的脚本要过 lint 的话,见 lint 包文档;
- 元素身份打戳的规则见 hfIds.ts;HTML 解析与校验的实现见 htmlParser.ts。
【免费下载链接】hyperframesWrite HTML. Render video. Built for agents.项目地址: https://gitcode.com/GitHub_Trending/hy/hyperframes
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考