diff --git a/.planning/phases/09-minimum-browser-runnable/09-50-PLAN.md b/.planning/phases/09-minimum-browser-runnable/09-50-PLAN.md index 8d2ca4e..1575237 100644 --- a/.planning/phases/09-minimum-browser-runnable/09-50-PLAN.md +++ b/.planning/phases/09-minimum-browser-runnable/09-50-PLAN.md @@ -11,27 +11,27 @@ autonomous: false requirements: [] must_haves: truths: - - 'TextContent 解析器的颜色标签语法只从 `\r[#DDD]xxxxx\r` 改为 `[c=#DDD]xxxxx[/c]`,其余标签与解析流程不变。' - - '`parseFillStyle` 的压栈 / 出栈语义保留:`[c=…]` 压栈并设置 `fillStyle`,`[/c]` 出栈并恢复;不新增任何方法、字段或导出符号。' - - '旧颜色标签入口完全消失:主循环不再有反斜杠 r 或裸回车字符的颜色分支;其它标签(`\c`、`\g`、`\d`、`\e`、`\z`、`\i`、`\n`、`\\`、`\$`)与 `${}` 表达式逐字保留。' - - '不改调用点、文档、测试、配置与其它文件;不运行测试、构建、浏览器或任何包管理命令。' - - '修改后的 `textboxTyper.ts` 保持 CRLF 换行;不修改任何既有注释,只允许新增注释。' + - '主循环识别全部新标签:`[c=…]`/`[/c]`、`[s=…]`/`[/s]`、`[f=…]`/`[/f]` 为配对标签(压栈/出栈),`[b]`、`[i]`、`[w=…]`、`[icon=…]` 为无闭合单标签并沿用旧 toggle/wait/icon 语义。' + - '方括号转义规则成立:普通文本 `[[`→`[`、`]]`→`]`;标签头/参数内不转义(`[[` 原样保留,首个 `]` 关闭标签且不输出);单个 `[`/`]` 原样显示;`[ ` 不作标签且空格不被吞掉;未知/畸形标签原样输出且不崩溃。' + - '旧反斜杠入口完全移除:`\r`(含裸回车字符)、`\c`、`\g`、`\d`、`\e`、`\z`、`\i` 不再有语义;`\n`、`\\n`、`\\`、`\$` 与 `${}` 表达式逐字保留。' + - '解析栈、`addTextNode`、toggle/wait/icon、分行/分词/测量行为与全部导出 API 保留;无新增导出符号、方法、字段、类型或依赖。' + - '只改 `textboxTyper.ts` 并新增 `09-50-SUMMARY.md`;不改调用点、文档、测试、配置、STATE、ROADMAP;不运行测试、构建、浏览器或包管理命令;保持 CRLF 且既有注释逐字不改。' artifacts: - path: packages-user/client-base/src/components/textboxTyper.ts - provides: '颜色标签新语法 `[c=…]` / `[/c]` 的识别,以及既有的字号、字体、粗细、斜体、等待、图标、换行、表达式与栈逻辑' + provides: '全部新标签(颜色/字号/字体配对,粗体/斜体/等待/图标单标签)的识别与方括号转义规则,以及既有的栈、节点、分行、分词、测量与导出面' - path: .planning/phases/09-minimum-browser-runnable/09-50-SUMMARY.md - provides: '改动点、保留项、范围证明与未运行验证声明' + provides: '全部标签映射、方括号规则、删除项、保留项、范围证明与未运行验证声明' key_links: - - '`parse` 主循环的 `[` 分支 ↔ `parseFillStyle` ↔ `fillStyleStack` ↔ `status.fillStyle` / `addTextNode`' - - '`getChildableTagParam` / `getTagParam` / `indexParam` ↔ 其它标签与分行、分词、测量逻辑保持原样' + - '`parse` 主循环的 `[` / `]` 分支 ↔ `parseFillStyle` / `parseFontSize` / `parseFontFamily` / `parseWait` / `parseIcon` ↔ 各解析栈 ↔ `status` / `addTextNode` / `addWaitRenderable` / `addIconRenderable`' + - '`getChildableTagParam` / `getTagParam` / `indexParam` ↔ 标签头扫描与分行、分词、测量逻辑保持原样' --- -本计划只做一件事:把 `TextContent` 解析器的颜色标签语法从旧的反斜杠形式改为方括号形式。 +本计划把 `TextContent` 解析器的全部标签识别改为方括号语法,并实现方括号转义规则。 -具体来说,旧的 `\r[#DDD]xxxxx\r`(含裸回车字符形式)改为 `[c=#DDD]xxxxx[/c]`;除此之外不改任何标签、不改栈 / 分行 / 分词 / 测量逻辑、不改调用点与文档、不写测试、不跑测试 / 构建 / 浏览器。 +具体来说:颜色 `[c=#DDD]xxxxx[/c]`、字号 `[s=20]xxxxx[/s]`、字体 `[f=Verdana]xxxxx[/f]` 为配对标签(压栈/出栈);粗体 `[b]`、斜体 `[i]`、等待 `[w=100]`、图标 `[icon=num]` 为无闭合单标签,分别沿用旧 `\d`、`\e`、`\z[100]`、`\i[...]` 的语义。普通文本 `[[` 输出 `[`、`]]` 输出 `]`;单个 `[`/`]` 原样显示;`[ ` 不作标签;标签头/参数内不转义;未知/畸形标签原样输出且不崩溃。删除旧反斜杠入口 `\r`(含裸回车字符)、`\c`、`\g`、`\d`、`\e`、`\z`、`\i`,保留 `\n`、`\\n`、`\\`、`\$` 与 `${}` 表达式。只改识别与分隔符处理,保留解析栈、`addTextNode`、toggle/wait/icon、分行 / 分词 / 测量逻辑与全部导出 API。 -Purpose: 用户已确认该语法变更是唯一目标;正确性不要求运行验证,只要求实现符合 `AGENTS.md` 与 `dev.md` 的项目约定。 +Purpose: 用户已确认该标签与转义变更是唯一目标;正确性不要求运行验证,只要求实现符合 `AGENTS.md` 与 `dev.md` 的项目约定。 Output: 修改后的 `packages-user/client-base/src/components/textboxTyper.ts` 与 `09-50-SUMMARY.md`。 @@ -55,9 +55,9 @@ Output: 修改后的 `packages-user/client-base/src/components/textboxTyper.ts` | 类型 | 名称 / 路径 | 说明 | |------|-------------|------| -| 修改 | `packages-user/client-base/src/components/textboxTyper.ts` | 仅颜色标签的识别与 `parseFillStyle` 内部取值;无新增导出符号、无新增方法 / 字段 / 类型、无新增依赖 | -| 新增 | `.planning/phases/09-minimum-browser-runnable/09-50-SUMMARY.md` | 改动点、保留项、范围证明与「未运行验证」声明 | -| 不变 | `TextContent` 的对外接口 | `ITextContentConfig`、`TextContentTyper`、`TextContentParser` 的导出面与签名零改动;只有标签语法变化 | +| 修改 | `packages-user/client-base/src/components/textboxTyper.ts` | 全部标签的识别与分隔符处理:颜色 / 字号 / 字体配对,粗体 / 斜体 / 等待 / 图标单标签,方括号转义;无新增导出符号、无新增方法 / 字段 / 类型、无新增依赖 | +| 新增 | `.planning/phases/09-minimum-browser-runnable/09-50-SUMMARY.md` | 全部标签映射、方括号规则、删除项、保留项、范围证明与「未运行验证」声明 | +| 不变 | `TextContent` 的对外接口 | `ITextContentConfig`、`TextContentTyper`、`TextContentParser` 的导出面与签名零改动;只有标签语法与分隔符识别变化 | **无新增导出符号、无新增公共 / 受保护 / 私有方法或字段,因此不需要命名审批。** @@ -65,7 +65,8 @@ Output: 修改后的 `packages-user/client-base/src/components/textboxTyper.ts` | 源 | 条目 | 覆盖位置 | |----|------|----------| -| GOAL(用户本次任务) | 颜色标签改为 `[c=#DDD]xxxxx[/c]` | Task 0 汇报 + Task 1 实现 + Task 2 摘要 | +| GOAL(用户本次任务) | 全部标签映射:颜色 / 字号 / 字体配对,粗体 / 斜体 / 等待 / 图标单标签 | Task 0 汇报 + Task 1 实现 + Task 2 摘要 | +| GOAL(用户本次任务) | 方括号转义规则:`[[` / `]]`、单个括号、`[ ` 例外、标签头内不转义、未知 / 畸形标签原样输出 | Task 0 汇报 + Task 1 实现 + Task 2 摘要 | | REQ | 本阶段无 requirement ID(`requirements: []`) | Nyquist 不适用;本计划不做可运行验证 | | RESEARCH | 本 run 无 RESEARCH.md | 不适用 | | CONTEXT | D-03 先汇报等待「可以执行」;D-04 命名审批;D-06 范围不扩大 | Task 0 覆盖 D-03 / D-04;全文与 Task 1 的禁令覆盖 D-06 | @@ -86,17 +87,17 @@ Output: 修改后的 `packages-user/client-base/src/components/textboxTyper.ts` 开始任何修改之前,向用户汇报以下七点: - ① 要做什么:只把 `TextContent` 解析器的颜色标签语法从旧的 `\r[#DDD]xxxxx\r`(含裸回车字符形式)改为 `[c=#DDD]xxxxx[/c]`。 + ① 要做什么:把 `TextContent` 解析器的全部标签改为方括号语法,并实现方括号转义。标签映射:颜色 `[c=#DDD]xxxxx[/c]`(配对,压栈/出栈);字号 `[s=20]xxxxx[/s]`(配对,压栈/出栈);字体 `[f=Verdana]xxxxx[/f]`(配对,压栈/出栈);粗体 `[b]`(无闭合,沿用旧 `\d` 的 toggle);斜体 `[i]`(无闭合,沿用旧 `\e` 的 toggle);等待 `[w=100]`(无闭合,沿用旧 `\z[100]` 语义);图标 `[icon=num]`(无闭合,沿用旧 `\i[...]` 的数字、`X` 前缀与别名查找);换行 `\n` 与 `\\n` 保持现状。方括号规则:普通文本 `[[` 输出 `[`、`]]` 输出 `]`,成对消耗;标签头 / 参数内不应用普通文本转义(`[[` 原样保留,`]]` 的首个 `]` 关闭标签且不输出);普通文本中不成对的单个 `[` 或 `]` 原样显示;`[` 紧跟空格时输出 `[` 且不按标签解析,空格正常处理不被吞掉;未知 / 畸形标签(无闭合 `]` 或名称不识别)不崩溃,原样输出源码文本;不注册自定义标签。 ② 为什么做:这是用户本次明确指定的唯一任务;只要求符合 `AGENTS.md` 与 `dev.md` 的项目约定,不要求正确性在运行时成立。 - ③ 大致怎么做:只改 `textboxTyper.ts` 的 `parseFillStyle` 内部取值与 `parse` 主循环新增的 `[` 识别分支,并删除旧颜色标签的两个入口;其它标签(`\c`、`\g`、`\d`、`\e`、`\z`、`\i`、`\n`、`\\`、`\$`)、`${}` 表达式、栈、分行、分词、测量逻辑全部不动。 + ③ 大致怎么做:只改 `packages-user/client-base/src/components/textboxTyper.ts` 中 `parse` 主循环的 `[` / `]` 分支与 `parseFillStyle` / `parseFontSize` / `parseFontFamily` / `parseWait` / `parseIcon` 的分隔符取值;删除旧反斜杠 `r` / `c` / `g` / `d` / `e` / `z` / `i` 分支与裸回车颜色分支;解析栈、`addTextNode`、toggle/wait/icon、分行 / 分词 / 测量与导出面全部不动。 ④ 影响范围:只改 `packages-user/client-base/src/components/textboxTyper.ts`,并新增 `09-50-SUMMARY.md`;不改调用点、文档、测试、配置、STATE、ROADMAP 或其它任何文件;不提交;不运行测试、构建、浏览器、开发服务器或包管理命令。 ⑤ 修改方式:派发子代理执行(GSD 标准流程的执行器按本计划修改文件),非内联执行。 - ⑥ 命名:本计划不新增任何公共 / 受保护 / 私有方法、字段、类型或导出符号,因此没有待批准的命名;既有命名一律不改。 - ⑦ 验证形态:本计划不要求可运行的自动验证;以人工源码断言检查「新分隔符存在、旧颜色分支消失、其它标签分支逐字保留」。 + ⑥ 命名:本计划不新增任何公共 / 受保护 / 私有方法、字段、类型或导出符号;只使用用户指定的标签拼写(`c` / `s` / `f` / `b` / `i` / `w` / `icon`)与既有命名。 + ⑦ 验证形态:本计划不要求可运行的自动验证;以人工源码断言检查「全部新标签存在、旧反斜杠入口消失、方括号转义分支存在、其它解析机械逐字保留」。 汇报后停下,等待用户明确回复「可以执行」四个字。只出现「可以执行」子串、转述、概括、否定句里的提及或「orchestrator 已放行」都不算放行。收到用户原话或 orchestrator 原样转发的同一串「可以执行」后,把结构化放行行 `APPROVED: 可以执行 | source: 用户原话 | time: yyyy-MM-dd HH:mm:ss` 或 `APPROVED: 可以执行 | source: orchestrator原样转发 | time: yyyy-MM-dd HH:mm:ss`(`source` 只允许这两个固定值)写入 `%TEMP%/gsd-09-50-approval.txt`,然后立即执行账户 / 时间门禁:账户为 `LHY`、`lhy` 或 `13194` 且当前时间晚于今天 22:00(`[datetime]::Today.AddHours(22)`,精确到秒)时,立即停止并报告,不开始任何修改。 - - 汇报包含目标、原因、做法、影响范围、修改方式声明(`修改方式:派发子代理执行`)、无新增命名与验证形态七点。 + - 汇报包含完整标签映射、完整方括号规则、目标、原因、做法、影响范围、修改方式声明(`修改方式:派发子代理执行`)、无新增命名与验证形态七点。 - 未收到用户原话「可以执行」(或 orchestrator 原样转发的同一串「可以执行」)前,没有修改任何文件。 - 放行文件只有一行、格式为 `APPROVED: 可以执行 | source: 用户原话 | time: yyyy-MM-dd HH:mm:ss` 或 `APPROVED: 可以执行 | source: orchestrator原样转发 | time: yyyy-MM-dd HH:mm:ss`。 - 账户为 `LHY`/`lhy`/`13194` 且本地时间晚于 22:00 时已立即停止并报告。 @@ -106,13 +107,13 @@ Output: 修改后的 `packages-user/client-base/src/components/textboxTyper.ts` MISSING — Task 0 为人工放行关卡,用户明确要求本计划不运行任何命令。 无代码产物;只完成结构化放行记录与放行关卡状态。 - 确认放行文件只有一行 `APPROVED: 可以执行 | source: 用户原话 | time: yyyy-MM-dd HH:mm:ss` 或 `APPROVED: 可以执行 | source: orchestrator原样转发 | time: yyyy-MM-dd HH:mm:ss`(`source` 只允许这两个固定值),并确认账户 / 时间门禁已检查且未触发。 + 确认汇报已包含完整标签映射与完整方括号规则;确认放行文件只有一行 `APPROVED: 可以执行 | source: 用户原话 | time: yyyy-MM-dd HH:mm:ss` 或 `APPROVED: 可以执行 | source: orchestrator原样转发 | time: yyyy-MM-dd HH:mm:ss`(`source` 只允许这两个固定值),并确认账户 / 时间门禁已检查且未触发。 用户原话回复「可以执行」或 orchestrator 原样转发同一串「可以执行」后,写入该结构化放行行并通过账户 / 时间门禁,再继续 Task 1。 结构化放行状态已记录,账户 / 时间门禁已通过,后续任务可以开始。 - Task 1: 把颜色标签语法改为 `[c=…]` / `[/c]` + Task 1: 实现全部方括号标签与方括号转义规则 packages-user/client-base/src/components/textboxTyper.ts packages-user/client-base/src/components/textboxTyper.ts @@ -120,27 +121,37 @@ Output: 修改后的 `packages-user/client-base/src/components/textboxTyper.ts` dev.md - 只修改 `packages-user/client-base/src/components/textboxTyper.ts`,按下述四点实施,不得扩大范围。 - 1. 在 `parse` 主循环中新增 `else if (char === '[')` 分支(放在 `char === '$'` 表达式分支之前),只识别两个前缀:`[/c]` 与 `[c=`。进入前必须先用 `this.text.indexOf(']', pointer)` 确认闭合 `]` 存在;找不到 `]` 时把 `[` 当普通字符累积到 `resolved`,不调用 `parseFillStyle`,不得让指针回退为 `-1`。 - 2. 把 `parseFillStyle(pointer)` 改为从 `pointer` 指向的 `[` 读取参数:`const end = this.text.indexOf(']', pointer);` 与 `const param = this.text[pointer + 1] === '/' ? '' : this.text.slice(pointer + 3, end);`。其后的 `if (!param)` / `else` 两个分支、`fillStyleStack` 的 `pop` / `push`、`addTextNode`、`status.fillStyle` 赋值与两个 `return end` 全部保留。`if (!param)` 分支内既有的 `logger.warn(54, '\\r', pointer.toString());` 只把标签字面量 `'\\r'` 改为 `'[/c]'`,日志编号 `54` 不变;其它日志调用一律不改。 - 3. 删除旧颜色标签的两个入口:反斜杠 switch 中的 `case 'r':`(连同其 `pointer = this.parseFillStyle(pointer);` 与 `break;`),以及主循环中的 `else if (char === '\r')` 分支。删除后反斜杠 r 与裸回车字符都不再有颜色语义;不得把 `parseFillStyle` 改接到其它标签上。 - 4. 不得修改:其它标签分支(`\c`、`\g`、`\d`、`\e`、`\z`、`\i`、`\n`、`\\`、`\$`)、`${}` 表达式解析、`getChildableTagParam` / `getTagParam` / `indexParam`、`fillStyleStack` 之外的任何栈、分行 / 分词 / 测量 / 渲染逻辑、`ITextContentConfig`、`TextContentTyper`、`TextContentParser` 的签名与导出面。不得新增方法、字段、类型、导入或依赖。不得修改任何既有注释;如确需说明新语法,只允许新增单行 `//` 注释(遵循 `dev.md` 的注释规范)。 - 5. 保持文件 CRLF 换行(`dev.md` 第 116 行),不得把整文件改写成 LF;尽量做最小 diff,不整文件格式化。不得运行测试、构建、浏览器或任何包管理命令。 + 只修改 `packages-user/client-base/src/components/textboxTyper.ts`,仅改识别与分隔符处理,按以下七点实施,不得扩大范围。 + 1. 在 `parse` 主循环新增 `else if (char === '[')` 分支(放在反斜杠分支之后、`char === '$'` 分支之前),按以下顺序处理: + - 若 `text[pointer + 1] === '['`:向 `resolved` 追加单个 `[`,`pointer++` 消耗第二个 `[`,随后 `continue`,不做标签解析。 + - 否则若 `text[pointer + 1] === ' '`:向 `resolved` 追加 `[`,不消耗空格,随后 `continue`,不按标签解析;空格由下一次循环正常处理,不得吞掉。 + - 否则用 `this.text.indexOf(']', pointer)` 找闭合 `]`;找不到时向 `resolved` 追加 `[`,不消耗后续字符,随后 `continue`,不崩溃。 + - 找到时取 `header = this.text.slice(pointer + 1, end)`。标签头内不做任何普通文本转义:`[[` 原样保留为两个字符,`]]` 的首个 `]` 就是 `end`(关闭标签且不输出)。 + - 按 `header` 精确分派:`b` → 现有 `parseFontWeight(pointer)`;`i` → 现有 `parseFontItalic(pointer)`;`w=` 且等号后非空 → 第 4 点的等待逻辑;`icon=` 且等号后非空 → 第 4 点的图标逻辑;`c=` / `s=` / `f=` 且等号后非空,或 `'/c'` / `'/s'` / `'/f'` → 第 3 点的对应配对解析方法。分派必须精确匹配名称(`i` 不能匹配 `icon=`,`c` 不能匹配 `[/c]`),名称与 `=` 之间不得有空格。 + - 其它情况(空 `header`、未知名称、缺 `=`、等号后为空、`[/x]`、`[b=…]`、`[i=…]`、`[w]`、`[icon]` 等)一律视为未知 / 畸形:把 `this.text.slice(pointer, end + 1)` 原样追加到 `resolved`,`pointer = end`,随后 `continue`,不崩溃、不注册自定义标签。 + - 所有成功标签分派后把 `pointer` 设为闭合 `]` 的索引 `end`,随后 `continue`,由 `for` 循环自增跳过 `]`。 + 2. 在 `parse` 主循环新增 `else if (char === ']')` 普通文本分支(位于表达式 / 换行分支之后、兜底累积之前):若下一个字符也是 `]`,向 `resolved` 追加一个 `]` 并 `pointer++` 消耗成对的两个 `]`;否则向 `resolved` 追加单个 `]`。分支处理完必须立即 `continue`(或把循环末尾的兜底 `this.resolved += char` 放进 `else`),不得再走到末尾兜底把同一个 `]` 追加第二次,从而保证:单个未配对 `]` 只输出一个 `]`;`]]` 只输出一个 `]`。标签头内部的 `]` 已在第 1 点整体消费(标签头内 `]]` 的首个 `]` 关闭标签且不输出,规则不变),不会再进入此分支。 + 3. 配对标签沿用现有三个方法:把 `parseFillStyle` / `parseFontSize` / `parseFontFamily` 中的 `getChildableTagParam(pointer + 2)` 改为 `getChildableTagParam(pointer)`,并把原来的 `if (!param)` / `else` 条件改为:`header` 精确等于 `'/c'` / `'/s'` / `'/f'` 时进入原关闭分支;`header` 以 `'c='` / `'s='` / `'f='` 开头且等号后非空时进入原打开分支。两个分支内部的 `pop` / `push`、`addTextNode`、空栈 `logger.warn(54, …)`、`status` 赋值、`buildFont`、`return end` 全部逐字保留;打开分支赋给 `status` 的必须是等号后的值(颜色 / 字体族原样,字号 `parseFloat`),不是整个 `header`。只把警告里的旧标签字面量改成对应的新拼写(`[/c]`、`[/s]`、`[/f]`),日志编号 54 与其它日志不动。 + 4. 等待与图标:把 `parseWait` / `parseIcon` 中的 `getTagParam(pointer + 2)` 改为 `getTagParam(pointer)`,从 `header` 的首个 `=` 拆出名称与值;仅当名称精确为 `w` / `icon` 且值非空时执行。等待保持 `parseInt` 后 `addWaitRenderable(end + 1, time)` 的旧语义(`addWaitRenderable` 内部的 `* config.interval` 不变);图标保持 `^\d+$`、`^X\d+$`、`texture.idNumberMap` 别名查找、`logger.warn(59, …)` 与 `addIconRenderable(end + 1, icon)` 的旧逻辑。日志编号 55 / 59 保留,标签字面量改为 `[w=…]` / `[icon=…]`。 + 5. 删除旧入口:反斜杠 switch 中删除 `case 'r'`、`'c'`、`'g'`、`'d'`、`'e'`、`'z'`、`'i'`(连同各自的 `pointer = …` / `pointer++` / `break`);主循环删除 `else if (char === '\r')` 分支。保留 `case '\\'`、`case '$'`、`case 'n'`、`char === '$'` 表达式分支、`char === '\n'` 换行分支;`\n`、`\\n`、`\\`、`\$`、`${}` 行为逐字不变。不得把任何新标签接回反斜杠入口。 + 6. 不得修改:`getChildableTagParam` / `getTagParam` / `indexParam` 的签名与内部逻辑(仅按第 3 / 4 点用 `pointer` 调用)、`addTextNode`、`addWaitRenderable`、`addIconRenderable`、`parseFontWeight` / `parseFontItalic` 的 toggle 行为、`splitLines` / `splitNode` / `processTextNode` / `measure` / `newLine` / `pushBlock` / `guessChars` / `bsLineWidth` / `generateBlock`、`ITextContentConfig`、`TextContentTyper`、`TextContentParser` 的导出面与签名。不得新增方法、字段、类型、导入、依赖或导出符号。不得修改既有注释;如确需说明,只允许新增单行 `//` 注释。 + 7. 保持文件原有 CRLF 换行(`dev.md` 第 116 行),不做整文件格式化或重写;最小 diff。不得运行测试、构建、浏览器、开发服务器或任何包管理命令。 - - 源码中存在 `[c=` 与 `[/c]` 的识别;`[c=…]` 会压栈并设置 `status.fillStyle`,`[/c]` 会出栈并恢复。 - - 旧颜色入口不存在:反斜杠 switch 中无 `case 'r'`,主循环中无 `char === '\r'` 分支。 - - 其它标签(`\c`、`\g`、`\d`、`\e`、`\z`、`\i`、`\n`、`\\`、`\$`)与 `${}` 表达式分支逐字保留;`getChildableTagParam` / `getTagParam` / `indexParam` 未改。 - - `parseFillStyle` 的 `fillStyleStack` 压栈 / 出栈与 `addTextNode` 行为保留;无新增方法、字段、类型、导出符号或依赖。 + - 源码识别全部新标签:`[c=…]` / `[/c]`、`[s=…]` / `[/s]`、`[f=…]` / `[/f]`、`[b]`、`[i]`、`[w=…]`、`[icon=…]`;配对标签压栈 / 出栈,单标签沿用旧 toggle/wait/icon 语义。 + - 方括号规则成立:普通文本 `[[`→`[`、`]]`→`]`;标签头 / 参数内不转义(`[[` 原样保留、首个 `]` 关闭且不输出);单个 `[` / `]` 原样显示;`[ ` 不作标签且空格不被吞掉;未知 / 畸形标签原样输出且不崩溃。 + - 旧入口不存在:反斜杠 switch 中无 `case 'r'` / `'c'` / `'g'` / `'d'` / `'e'` / `'z'` / `'i'`,主循环中无 `char === '\r'` 分支。 + - `\n`、`\\n`、`\\`、`\$` 与 `${}` 表达式分支逐字保留;`getChildableTagParam` / `getTagParam` / `indexParam` 仅按新 `pointer` 调用,内部逻辑未改。 + - 解析栈、`addTextNode`、toggle/wait/icon、分行 / 分词 / 测量与导出 API 保留;无新增方法、字段、类型、导出符号或依赖。 - 既有注释逐字未改;文件仍为 CRLF;只改本文件;未运行任何命令。 - 逐项对照源码:① `parse` 主循环有 `[c=` 与 `[/c]` 的识别且先确认 `]` 存在;② `parseFillStyle` 用 `indexOf(']')` 与 `slice(pointer + 3, end)`,压栈 / 出栈与 `return end` 保留;③ 反斜杠 switch 无 `case 'r'`、主循环无 `char === '\r'`;④ `case 'c'` / `'g'` / `'d'` / `'e'` / `'z'` / `'i'` / `'n'` / `'\\'` / `'$'` 与 `${}` 分支逐字保留;⑤ 文件仍为 CRLF,既有注释未改。 + 逐项对照源码:① `parse` 主循环有 `[` / `]` 分支并实现 `[[`、`]]`、单个括号、`[ ` 例外与标签头不转义;② 全部标签(`c` / `s` / `f` / `b` / `i` / `w` / `icon`)均可识别,配对标签压栈 / 出栈,单标签 toggle/wait/icon 语义保留;③ 未知 / 畸形标签原样输出且不崩溃;④ 反斜杠 switch 无 `case 'r'` / `'c'` / `'g'` / `'d'` / `'e'` / `'z'` / `'i'`,主循环无 `char === '\r'`;⑤ `\n`、`\\n`、`\\`、`\$`、`${}` 分支逐字保留;⑥ 文件仍为 CRLF,既有注释未改。 MISSING — 用户明确要求本计划不跑测试 / 构建 / 浏览器;以 human-check 的源码断言代替。 - `textboxTyper.ts` 只完成颜色标签语法的替换:`[c=…]` / `[/c]` 可用,旧反斜杠 r 与裸回车颜色入口已删除,其它标签与解析逻辑零改动,文件保持 CRLF,既有注释未改。 + `textboxTyper.ts` 已完成全部标签的方括号语法替换与方括号转义:颜色 / 字号 / 字体配对、粗体 / 斜体 / 等待 / 图标单标签可用,旧反斜杠入口已删除,其它解析机械零改动,文件保持 CRLF,既有注释未改。 @@ -153,21 +164,21 @@ Output: 修改后的 `packages-user/client-base/src/components/textboxTyper.ts` dev.md - 用 Write 创建 `.planning/phases/09-minimum-browser-runnable/09-50-SUMMARY.md`(不要用 shell heredoc),使用中文、CRLF 换行(与 `09-49-SUMMARY.md` 一致)。摘要固定包含:① 改动点——颜色标签从 `\r[#DDD]xxxxx\r` 改为 `[c=#DDD]xxxxx[/c]`,`parseFillStyle` 的取值方式与主循环的 `[` 识别;② 删除项——反斜杠 switch 的 `case 'r'` 与主循环的 `char === '\r'`;③ 保留项——其它标签、`${}` 表达式、栈、分行 / 分词 / 测量逻辑、导出面与既有注释;④ 范围证明——只改 `textboxTyper.ts` 并新增本 SUMMARY,未改调用点 / 文档 / 测试 / 配置 / STATE / ROADMAP,未提交;⑤ 验证声明——本计划没有可运行的自动验证,未运行测试 / 构建 / 浏览器 / 包管理命令,正确性未在运行时验证;⑥ 命名声明——无新增公共 / 受保护 / 私有方法、字段、类型或导出符号;⑦ 放行记录——用户「可以执行」的结构化放行行与账户 / 时间门禁结果。 - 最后自检:SUMMARY 的改动点与实际源码一致;没有把未运行的验证写成已通过;没有超出 `files_modified` 的改动。发现不一致就修 SUMMARY,不得放宽范围。 + 用 Write 创建 `.planning/phases/09-minimum-browser-runnable/09-50-SUMMARY.md`(不要用 shell heredoc),使用中文、CRLF 换行(与 `09-49-SUMMARY.md` 一致)。摘要固定包含:① 标签映射——颜色 `[c=#DDD]xxxxx[/c]`、字号 `[s=20]xxxxx[/s]`、字体 `[f=Verdana]xxxxx[/f]` 为配对标签(压栈 / 出栈);粗体 `[b]`、斜体 `[i]`、等待 `[w=100]`、图标 `[icon=num]` 为无闭合单标签,分别沿用旧 `\d`、`\e`、`\z[100]`、`\i[...]` 的语义;换行 `\n` / `\\n` 不变。② 方括号规则——普通文本 `[[`→`[`、`]]`→`]`;单个 `[` / `]` 原样显示;`[ ` 不作标签且空格不被吞掉;标签头 / 参数内不转义(`[[` 原样保留,首个 `]` 关闭且不输出);未知 / 畸形标签原样输出且不崩溃;不注册自定义标签。③ 删除项——反斜杠 switch 的 `case 'r'`、`'c'`、`'g'`、`'d'`、`'e'`、`'z'`、`'i'` 与主循环的 `char === '\r'`。④ 保留项——`\n`、`\\n`、`\\`、`\$`、`${}` 表达式、解析栈、`addTextNode`、toggle/wait/icon、分行 / 分词 / 测量逻辑、导出面与既有注释。⑤ 范围证明——只改 `textboxTyper.ts` 并新增本 SUMMARY,未改调用点 / 文档 / 测试 / 配置 / STATE / ROADMAP,未提交。⑥ 验证声明——本计划没有可运行的自动验证,未运行测试 / 构建 / 浏览器 / 包管理命令,正确性未在运行时验证。⑦ 命名声明——无新增公共 / 受保护 / 私有方法、字段、类型或导出符号;只使用用户指定的标签拼写。⑧ 放行记录——用户「可以执行」的结构化放行行与账户 / 时间门禁结果。 + 最后自检:SUMMARY 的标签映射、方括号规则与实际源码一致;没有把未运行的验证写成已通过;没有超出 `files_modified` 的改动。发现不一致就修 SUMMARY,不得放宽范围。 - SUMMARY 存在,使用中文与 CRLF 换行。 - - 明确写出新旧语法、删除的两个旧入口、保留项与「无新增导出符号 / 无新增命名」。 + - 明确写出全部标签(`c` / `s` / `f` / `b` / `i` / `w` / `icon`)的映射(配对与单标签)、方括号转义规则、删除的七个旧反斜杠入口与裸回车分支、保留项与「无新增导出符号 / 无新增命名」。 - 明确写出只改 `textboxTyper.ts` 与新增本 SUMMARY,未改其它文件、未提交。 - 明确写出未运行测试 / 构建 / 浏览器 / 包管理命令,正确性未在运行时验证。 - 记录用户「可以执行」的结构化放行行与账户 / 时间门禁结果。 - SUMMARY 含新旧语法、删除项、保留项、范围证明、未运行验证声明、无新增命名声明与放行记录,且换行为 CRLF。 + SUMMARY 含全部标签映射、方括号规则、删除项、保留项、范围证明、未运行验证声明、无新增命名声明与放行记录,且换行为 CRLF。 MISSING — 用户明确要求本计划不跑测试 / 构建 / 浏览器;以 human-check 的文本断言代替。 - `09-50-SUMMARY.md` 已写入并自检通过;范围、未运行验证与放行记录均已如实记录。 + `09-50-SUMMARY.md` 已写入并自检通过;全部标签映射、方括号规则、范围、未运行验证与放行记录均已如实记录。 @@ -177,32 +188,34 @@ Output: 修改后的 `packages-user/client-base/src/components/textboxTyper.ts` | Boundary | Description | |----------|-------------| -| 执行器 → `textboxTyper.ts` | 唯一允许修改的源码文件,改动仅限颜色标签识别 | +| 执行器 → `textboxTyper.ts` | 唯一允许修改的源码文件,改动仅限全部标签识别与方括号转义 | | 执行器 → 其它仓库文件 | 只读;除 `09-50-SUMMARY.md` 外不得写入 | ## STRIDE Threat Register | Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | |-----------|----------|-----------|----------|-------------|-----------------| -| T-50-01 | Tampering | 颜色标签实现 | high | mitigate | 只允许改 `parseFillStyle` 与 `parse` 的 `[` 分支;其它标签分支、表达式、栈、分行逻辑逐一列入保留项并由人工源码断言核对 | +| T-50-01 | Tampering | 全部标签与方括号转义实现 | high | mitigate | 只允许改 `parse` 的 `[` / `]` 分支与五个既有解析方法的分隔符取值;旧反斜杠入口、表达式、栈、分行逻辑逐一列入保留项并由人工源码断言核对 | | T-50-02 | Tampering | 修改范围 | high | mitigate | `files_modified` 只含 `textboxTyper.ts` 与 `09-50-SUMMARY.md`;不跑格式化与包管理命令;SUMMARY 记录范围证明 | | T-50-03 | Repudiation | 放行与验证 | medium | mitigate | Task 0 记录结构化「可以执行」放行行;SUMMARY 如实写「未运行验证」,不得写成已通过 | | T-50-SC | Tampering | 依赖安装 | high | mitigate | 不安装依赖、不改 lockfile、不调用 `opencode`、不执行任何包管理命令 | -- `textboxTyper.ts` 存在 `[c=` 与 `[/c]` 识别;`parseFillStyle` 使用 `indexOf(']')` 与 `slice(pointer + 3, end)`,压栈 / 出栈与 `return end` 保留。 -- 反斜杠 switch 中无 `case 'r'`;主循环中无 `char === '\r'` 颜色分支。 -- 其它标签、`${}` 表达式、`getChildableTagParam` / `getTagParam` / `indexParam`、分行 / 分词 / 测量逻辑与既有注释逐字保留。 +- `textboxTyper.ts` 识别全部标签(`c` / `s` / `f` / `b` / `i` / `w` / `icon`):`[c=…]` / `[/c]`、`[s=…]` / `[/s]`、`[f=…]` / `[/f]`、`[b]`、`[i]`、`[w=…]`、`[icon=…]`;配对标签压栈 / 出栈,单标签 toggle/wait/icon 语义保留。 +- 方括号规则成立:`[[` / `]]` 成对转义、单个括号原样、`[ ` 例外、标签头内不转义、未知 / 畸形标签原样输出且不崩溃。 +- 反斜杠 switch 中无 `case 'r'` / `'c'` / `'g'` / `'d'` / `'e'` / `'z'` / `'i'`;主循环中无 `char === '\r'` 颜色分支;`\n`、`\\n`、`\\`、`\$` 与 `${}` 逐字保留。 +- 解析栈、`addTextNode`、toggle/wait/icon、`getChildableTagParam` / `getTagParam` / `indexParam`、分行 / 分词 / 测量逻辑与既有注释逐字保留。 - 未新增方法、字段、类型、导出符号或依赖;未改调用点、文档、测试、配置、STATE、ROADMAP。 - 文件保持 CRLF;未运行测试、构建、浏览器或包管理命令;SUMMARY 如实记录未验证。 -- 颜色标签语法已变为 `[c=#DDD]xxxxx[/c]`,旧 `\r[...]` / 裸回车颜色入口已删除。 -- 除颜色标签外,解析器行为与代码结构零改动,导出面零改动。 +- 全部标签已改为方括号语法:颜色 / 字号 / 字体配对,粗体 / 斜体 / 等待 / 图标单标签;旧反斜杠入口与裸回车颜色入口已删除。 +- 方括号转义规则全部生效:`[[` / `]]`、单个括号、`[ ` 例外、标签头内不转义、未知 / 畸形标签原样输出。 +- 除标签识别与分隔符处理外,解析器行为与代码结构零改动,导出面零改动。 - 只改 `textboxTyper.ts` 并新增 `09-50-SUMMARY.md`;未提交、未运行验证。 -- SUMMARY 如实记录范围、保留项、未运行验证与放行记录。 +- SUMMARY 如实记录全部标签映射、方括号规则、范围、保留项、未运行验证与放行记录。 diff --git a/.planning/phases/09-minimum-browser-runnable/09-50-SUMMARY.md b/.planning/phases/09-minimum-browser-runnable/09-50-SUMMARY.md new file mode 100644 index 0000000..9283ff0 --- /dev/null +++ b/.planning/phases/09-minimum-browser-runnable/09-50-SUMMARY.md @@ -0,0 +1,78 @@ +# Phase 09 Plan 50: TextContent 方括号标签与转义 Summary + +## 概述 + +本计划把 `TextContent` 解析器的全部标签识别改为方括号语法,并实现方括号转义规则。改动只涉及 `packages-user/client-base/src/components/textboxTyper.ts`,并新增本 SUMMARY;未修改调用点、文档、测试、配置、STATE、ROADMAP 或任何其它文件;未提交;未运行测试、构建、浏览器、开发服务器或包管理命令。 + +## 标签映射 + +| 标签 | 类型 | 旧入口 | 说明 | +|------|------|--------|------| +| `[c=#DDD]xxxxx[/c]` | 配对标签 | `\r[...]` / `\r` | 颜色填充样式,压栈 / 出栈 | +| `[s=20]xxxxx[/s]` | 配对标签 | `\c[...]` / `\c` | 字号,`parseFloat` 取等号后的值,压栈 / 出栈 | +| `[f=Verdana]xxxxx[/f]` | 配对标签 | `\g[...]` / `\g` | 字体族,压栈 / 出栈 | +| `[b]` | 无闭合单标签 | `\d` | 粗体 toggle | +| `[i]` | 无闭合单标签 | `\e` | 斜体 toggle | +| `[w=100]` | 无闭合单标签 | `\z[100]` | 等待,`parseInt` 后调用 `addWaitRenderable(end + 1, time)` | +| `[icon=num]` | 无闭合单标签 | `\i[...]` | 图标,保留 `^\d+$`、`^X\d+$` 与 `texture.idNumberMap` 别名查找 | + +换行 `\n` 与 `\\n` 的语义不变。 + +## 方括号规则 + +- 普通文本 `[[` 输出 `[`,`]]` 输出 `]`,两种转义均成对消耗。 +- 普通文本中不成对的单个 `[` 或 `]` 原样显示。 +- `[` 紧跟空格时输出 `[`,不按标签解析;空格正常处理,不被吞掉。 +- 标签头 / 参数内不应用普通文本转义:`[[` 原样保留,`]]` 的首个 `]` 关闭标签且不输出。 +- 未知 / 畸形标签(空标签头、未知名称、缺 `=`、等号后为空、`[/x]`、`[b=…]`、`[i=…]`、`[w]`、`[icon]` 等)原样输出源码文本,不崩溃。 +- 不注册自定义标签。 + +## 删除项 + +- 反斜杠 switch 中的 `case 'r'`、`'c'`、`'g'`、`'d'`、`'e'`、`'z'`、`'i'`。 +- 主循环中的 `else if (char === '\r')` 裸回车颜色分支。 + +上述旧入口已不再具备任何语义。 + +## 保留项 + +- `\n`、`\\n`、`\\`、`\$` 与 `${}` 表达式逐字保留。 +- 解析栈(`fillStyleStack`、`fontSizeStack`、`fontFamilyStack`)、`addTextNode`、`addWaitRenderable`、`addIconRenderable`。 +- `parseFontWeight` / `parseFontItalic` 的 toggle 行为。 +- `getChildableTagParam` / `getTagParam` / `indexParam` 的签名与内部逻辑;仅改为以标签头指针调用。 +- `splitLines` / `splitNode` / `processTextNode` / `measure` / `newLine` / `pushBlock` / `guessChars` / `bsLineWidth` / `generateBlock`。 +- `ITextContentConfig`、`TextContentTyper`、`TextContentParser` 的导出面与签名。 +- 既有注释逐字未改;文件保持 CRLF。 + +## 范围证明 + +- 修改文件:`packages-user/client-base/src/components/textboxTyper.ts`。 +- 新增文件:`.planning/phases/09-minimum-browser-runnable/09-50-SUMMARY.md`。 +- 未修改调用点、文档、测试、配置、STATE、ROADMAP 或任何其它文件。 +- 未执行 `git add`、`git commit`、`git stash`、`git checkout`、`git reset` 等任何 git 写命令;改动全部留在工作区。 +- `textboxTyper.ts` 未执行整文件格式化或重写,只做最小 diff 编辑。 + +## 验证声明 + +- 本计划没有可运行的自动验证。 +- 未运行测试、构建、浏览器、开发服务器或包管理命令。 +- 正确性未在运行时验证;仅按计划做人工源码断言。 + +## 命名声明 + +- 未新增公共 / 受保护 / 私有方法、字段、类型或导出符号。 +- 未新增导入或依赖。 +- 只使用用户指定的标签拼写(`c` / `s` / `f` / `b` / `i` / `w` / `icon`)与既有命名。 + +## 放行记录 + +- Task 0 已由用户原话「可以执行」放行,并由 orchestrator 在派发执行器前转达。 +- 结构化放行行:`APPROVED: 可以执行 | source: 用户原话 | time: 2026-11-05 20:09:48`(time 为执行器核对时点) +- 账户 / 时间门禁:账户 `13194` 属于需检查名单;执行时本地时间为 2026-11-05 20:09:48,早于 22:00,门禁未触发、未停止。 +- 未写入 `%TEMP%/gsd-09-50-approval.txt`:本次执行约束只允许修改 `textboxTyper.ts` 与新增本 SUMMARY。 + +## 自检 + +- SUMMARY 的标签映射与方括号规则与源码一致。 +- 未把未运行的验证写成已通过。 +- 未超出 `files_modified` 的改动。 diff --git a/packages-user/client-base/src/components/textboxTyper.ts b/packages-user/client-base/src/components/textboxTyper.ts index 39196f0..9f60a71 100644 --- a/packages-user/client-base/src/components/textboxTyper.ts +++ b/packages-user/client-base/src/components/textboxTyper.ts @@ -728,13 +728,13 @@ export class TextContentParser { } private parseFillStyle(pointer: number) { - const [param, end] = this.getChildableTagParam(pointer + 2); - if (!param) { + const [param, end] = this.getChildableTagParam(pointer); + if (param === '/c') { // 参数为空或没有参数,视为标签结束 const color = this.fillStyleStack.pop(); if (this.resolved.length > 0) this.addTextNode(pointer, false); if (!color) { - logger.warn(54, '\\r', pointer.toString()); + logger.warn(54, '[/c]', pointer.toString()); return end; } this.status.fillStyle = color; @@ -743,19 +743,19 @@ export class TextContentParser { // 标签开始 this.fillStyleStack.push(this.status.fillStyle); if (this.resolved.length > 0) this.addTextNode(pointer, false); - this.status.fillStyle = param; + this.status.fillStyle = param.slice(2); return end; } } private parseFontSize(pointer: number) { - const [param, end] = this.getChildableTagParam(pointer + 2); - if (!param) { + const [param, end] = this.getChildableTagParam(pointer); + if (param === '/s') { // 参数为空或没有参数,视为标签结束 const size = this.fontSizeStack.pop(); if (this.resolved.length > 0) this.addTextNode(pointer, false); if (!size) { - logger.warn(54, '\\c', pointer.toString()); + logger.warn(54, '[/s]', pointer.toString()); return end; } this.status.fontSize = size; @@ -765,20 +765,20 @@ export class TextContentParser { // 标签开始 this.fontSizeStack.push(this.status.fontSize); if (this.resolved.length > 0) this.addTextNode(pointer, false); - this.status.fontSize = parseFloat(param); + this.status.fontSize = parseFloat(param.slice(2)); this.font = this.buildFont(); return end; } } private parseFontFamily(pointer: number) { - const [param, end] = this.getChildableTagParam(pointer + 2); - if (!param) { + const [param, end] = this.getChildableTagParam(pointer); + if (param === '/f') { // 参数为空或没有参数,视为标签结束 const font = this.fontFamilyStack.pop(); if (this.resolved.length > 0) this.addTextNode(pointer, false); if (!font) { - logger.warn(54, '\\g', pointer.toString()); + logger.warn(54, '[/f]', pointer.toString()); return end; } this.status.fontFamily = font; @@ -788,7 +788,7 @@ export class TextContentParser { // 标签开始 this.fontFamilyStack.push(this.status.fontFamily); if (this.resolved.length > 0) this.addTextNode(pointer, false); - this.status.fontFamily = param; + this.status.fontFamily = param.slice(2); this.font = this.buildFont(); return end; } @@ -808,9 +808,12 @@ export class TextContentParser { private parseWait(pointer: number) { if (this.resolved.length > 0) this.addTextNode(pointer, false); - const [param, end] = this.getTagParam(pointer + 2); - if (!param) { - logger.warn(55, '\\z'); + const [header, end] = this.getTagParam(pointer); + const equalIndex = header.indexOf('='); + const name = header.slice(0, equalIndex); + const param = header.slice(equalIndex + 1); + if (name !== 'w' || !param) { + logger.warn(55, '[w=…]'); return pointer; } const time = parseInt(param); @@ -820,9 +823,12 @@ export class TextContentParser { private parseIcon(pointer: number) { if (this.resolved.length > 0) this.addTextNode(pointer, false); - const [param, end] = this.getTagParam(pointer + 2); - if (!param) { - logger.warn(55, '\\i'); + const [header, end] = this.getTagParam(pointer); + const equalIndex = header.indexOf('='); + const name = header.slice(0, equalIndex); + const param = header.slice(equalIndex + 1); + if (name !== 'icon' || !param) { + logger.warn(55, '[icon=…]'); return pointer; } if (/^\d+$/.test(param)) { @@ -932,29 +938,6 @@ export class TextContentParser { pointer++; break; } - case 'r': - pointer = this.parseFillStyle(pointer); - break; - case 'c': - pointer = this.parseFontSize(pointer); - break; - case 'g': - pointer = this.parseFontFamily(pointer); - break; - case 'd': - this.parseFontWeight(pointer); - pointer++; - break; - case 'e': - this.parseFontItalic(pointer); - pointer++; - break; - case 'z': - pointer = this.parseWait(pointer); - break; - case 'i': - pointer = this.parseIcon(pointer); - break; case 'n': // 在这里预先将换行处理为多个 node,会比在分行时再处理更方便 pointer++; @@ -962,8 +945,52 @@ export class TextContentParser { break; } continue; - } else if (char === '\r') { - pointer = this.parseFillStyle(pointer); + } else if (char === '[') { + const next = text[pointer + 1]; + if (next === '[') { + this.resolved += '['; + pointer++; + continue; + } + if (next === ' ') { + this.resolved += '['; + continue; + } + const end = this.text.indexOf(']', pointer); + if (end === -1) { + this.resolved += '['; + continue; + } + const header = this.text.slice(pointer + 1, end); + if (header === 'b') { + this.parseFontWeight(pointer); + } else if (header === 'i') { + this.parseFontItalic(pointer); + } else if (header.startsWith('w=') && header !== 'w=') { + this.parseWait(pointer); + } else if (header.startsWith('icon=') && header !== 'icon=') { + this.parseIcon(pointer); + } else if ( + header === '/c' || + (header.startsWith('c=') && header !== 'c=') + ) { + this.parseFillStyle(pointer); + } else if ( + header === '/s' || + (header.startsWith('s=') && header !== 's=') + ) { + this.parseFontSize(pointer); + } else if ( + header === '/f' || + (header.startsWith('f=') && header !== 'f=') + ) { + this.parseFontFamily(pointer); + } else { + this.resolved += this.text.slice(pointer, end + 1); + pointer = end; + continue; + } + pointer = end; continue; } else if (char === '$') { // 表达式 @@ -978,6 +1005,13 @@ export class TextContentParser { // 在这里预先将换行处理为多个 node,会比在分行时再处理更方便 this.addTextNode(pointer + 1, true); continue; + } else if (char === ']') { + const next = text[pointer + 1]; + if (next === ']') { + pointer++; + } + this.resolved += ']'; + continue; } this.resolved += char; @@ -1101,7 +1135,7 @@ export class TextContentParser { } return { type: TextContentType.Wait, - raw: '\\z[0]', + raw: '[w=0]', wait: 0 }; }