docs(05): 锚点补记四十二 —— 行内 HTML 标签渲染(W67:b/strong、i/em、code、br;只认小写、未配对保持字面、code 内不解析、整段仅标记回落字面、流式扣留未闭合行内标签;不碰星号语义)+ 真机五组样本逐条验收 + 八条故意不做 + 给后端登记的一问

This commit is contained in:
AskaEth committed 2026-10-07 17:17:26 +08:00
1 parent 3d4a9f288a
commit 3fe55e8d0c
1 file changed
+70
+70
View File
@@ -2274,3 +2274,73 @@ infinity maximum height constraints, which is disallowed.
- HEAD = **`3b9936e`** = `origin/android`;worktree / 分支 / 构建锁**全清**;主仓唯一脏 = `devdocs/02`(后端在途 + 我的登记与四问)。
- 全量真跑:**125 suites / 1509 tests / 0 failures / 0 errors**。
- 模拟器装的**是最新包**(含 W58~W66);晚些已把 `tcp:80` 切回真后端。假后端 18080(带语音夹具 + `/__control/frame`)在后台任务里跑着。
## ★★ 锚点补记四十二 · 行内 HTML 标签渲染(W67)+ 真机验收(2026-10-07 下午)
### 1. 需求与**取证**(不凭猜设计范围)
主人原话:「我看到她在聊天消息中插入了 html/md 标签,能否增加对此类标签的基本渲染支持?」
**取证**(拉真后端最近 **100 条**,按一组模式扫):
- **命中只有 1 种**:`<b>` —— **2 条**。实例:`今天那儿是晴,13~25℃,日落 <b>18:35</b>,比上海晚了整整一小时呢。`
- **0 命中**:`<strong>`/`<i>`/`<em>`/`<code>`/`<br>`/`<a href>`/`<ul>/<ol>/<li>`/`<h1-6>`,以及 Markdown 的 `**`/`__`/`` ` ``/`#`/`-`/`[链接](url)`/`>`/围栏/表格。
⇒ **不做 Markdown 引擎**(尤其 🔴 **`*` 系列不能碰**:`ROLE_RE` 里全角 `*…*` 与半角 `*关键词…*` **已经是「旁白/动作」语义**,有既有测试钉着)。
### 2. 做了什么(`d7d93b4` → 合并 `3d4a9f2`)
| 标签 | 渲染 |
|---|---|
| `<b>` / `<strong>` | 粗体 |
| `<i>` / `<em>` | 斜体 |
| `<code>` | 等宽 + 浅底(内部**不再解析** ⇒ `<code><b>甲</b></code>` 显示字面的 `<b>甲</b>`) |
| `<br>` / `<br/>` / `<br />` | 换行(自闭合、不入栈) |
**侵入最小的做法**:**没动 `ContentPart`**(加变体或给 `Text` 加字段会波及 6+ 个调用点,其中 `MessageEditText` / `SessionSearchModel` / `ProactiveNotify` 读的是 `parts` **原文** —— 在那儿改就是静默改用户内容);只在 `ParsedBubble` 上加一个**带默认值**的字段 `runs: List<InlineRun>`(`InlineRun(text, styles: Set<InlineStyle>)`,相邻同样式已合并)⇒ `plainBubble`、`ShareConfirmPanel:700`(红线)、三个测试文件里的手搓气泡**一行都不用改**。解析只在 `toBubble` 加**一行调用**(段落定稿之后的最后一步,不碰 `tokenize` / `stripLooseTags` / `P_BLOCK_RE` / `ROLE_RE`)。UI 只在 `BubbleBody` 加一条分支:`runs` 为空走原来的 `Text(text)`(绝大多数消息**逐像素不变**)。
**口径(6 条边界,各有单测)**:
| 边界 | 口径 |
|---|---|
| 未配对 | **整项字面**(`<b>没闭合` 原样显示);配对成功的另一半照常生效 |
| 嵌套 | 严格嵌套;`<b>甲<i>乙</b>丙</i>` 里 `</b>` 关不掉 `i` ⇒ 它连同 `<b>` 字面 |
| 尖括号 | 只有「配成对 + 正确嵌套」才被吃;`1 < 2`、`a<b`、`a<b>c`、`<b`、`<p2>x</p2>`、`<B>`、`<b class="x">` **全部原样** |
| `<br>` 与空行 | 就是**一个 `\n`**,不与相邻 `\n` 去重、**不分气泡**、两端不留空行 |
| `code` 内部 | 不再解析 |
| HTML 实体 | **不做**(`&amp;`/`&lt;`/`&nbsp;` 全原样;0 命中 + 实体是开放清单,做一半比不做更糟) |
**大小写只认小写** —— 理由很硬:流式**扣留**侧的标签名扫描只认 `a..z`,认大小写会让「扣留」与「消费」两边**分叉**(中间态把 `<B>` 当正文吐上屏、收口时再吃掉 ⇒ **屏幕上显示过的字会改口**)。
**两条自定契约**:① **整段只有标记没有字**(`<b></b>` / `<br>`)⇒ **回落字面原文**(绝不把非空正文渲染成空);② **流式把未闭合的行内开标签同样入栈扣留**(`TAG_NAMES` 补词表,`<br>` 不入栈)。
**测试**:新增 `InlineMarkupTest` **36 条**(含**星号反证** `starsKeepTheirExistingMeaningInsteadOfBecomingMarkdown`、流式逐字符 `streamingWithholdsUnclosedInlineTags`)。全量 **126 suites / 1545 tests**。
### 3. 真机验收(`emulator-5554` + 假后端灌样本,**不污染主人真实会话**)
| 输入 | 屏幕 |
|---|---|
| `甲<b>粗</b>乙<i>斜</i>丙<code>码</code>丁<br>戊` | 「甲**粗**乙*斜*丙`码`丁」+ **换行**「戊」,**标签全部消失** |
| `1 < 2` / `a<b` / `<b>没闭合` / `<p2>x</p2>` | **全部原样** |
| `*弯起眼睛*` / `*内心:…*` | 旁白行灰斜体(**既有星号语义没被动**) |
| `**不该加粗**` | **保持字面**(md 没被误渲染) |
| `日落 <b>18:35</b>,…` | 「日落 **18:35**,…」粗体生效、无标签 |
### 4. 如实登记:本批**故意不做**的(等主人拍板)
1. **`<a href>`**(要点击行为 + 安全策略:只允许 http/https、禁 `intent://`)—— 0 命中;
2. **Markdown 全部语法**(与星号语义冲突 / 0 命中);
3. **HTML 实体解码**;
4. **旁白/动作行不做行内解析**(`<action>笑<b>了</b></action>` 里的 `<b>` 照原样)—— 因为那一行与分享预览**同源**(都读 `parts` 原文),只改一侧会让 W65 刚统一的「预览 == 气泡」在旁白上**重新裂开**;
5. **三处仍显示标签的地方**(都读 `parts` 原文,且都在红线文件里):搜索片段(搜索本身没问题)、**通知文案**(`ProactiveNotify` 是红线)、编辑框(**这个是对的** —— 编辑必须看到原文,否则保存会丢标记);
6. **分享预览**:正文跟着受益(不再露标签,因为走同一个 `parse`),但那一栏是红线、一字未改 ⇒ 预览里**没有**粗体/等宽底色,只有文字;
7. **`MessageEditText.KNOWN_TAG_RE` 没改**(口径「只认解析器真正认识的标签」现在与 `parse` 有了一点偏差);
8. **乐观气泡**(`PendingBubble` 走 `plainBubble`)原样显示,落库后才渲染。
### 5. 给后端登记了一问(`devdocs/02`,未提交)
**`<b>` 是产出侧有意的吗?契约要不要承认这一小撮行内标签?以及 Web / IM 出站是否也渲染它们** —— 特别是 **IM 出站**(`channelbridge/sanitize.go` 的 `splitBubbles`):若原样发到微信/QQ,对方看到的就是字面标签,**口径最好对齐**。
### 6. 环境
- HEAD = **`3d4a9f2`** = `origin/android`;worktree / 分支 / 构建锁**全清**;主仓唯一脏 = `devdocs/02`。
- 模拟器装最新包(含 W58~W67),`tcp:80` 已切回**真后端**。