# Markdown 渲染容器不能加 whitespace-pre-wrap

**日期**：2026-05-17
**触发场景**：FFAI Agent 用户对话 markdown 列表渲染，每条之间多出 2-3 行白

## 现象

`AssistantBlock` 用作 assistant 消息容器，传入两种 children：
1. **streamingText**（纯文本 + 光标 span）—— 需要 `whitespace-pre-wrap` 保留换行
2. **MarkdownContent**（react-markdown 渲染的 HTML 树）—— **不需要** pre-wrap

之前为了支持 streaming，我给 AssistantBlock 容器加了 `whitespace-pre-wrap`：

```jsx
<div className="whitespace-pre-wrap text-sm leading-7 ...">
  {children}  // 既渲染 streamingText 也渲染 MarkdownContent
</div>
```

结果：MarkdownContent 渲染 `<p>...</p>\n<ul>...</ul>` 时，**两个 block 之间的换行字符 `\n` 在 pre-wrap 下被保留成可见空白行**。叠加 list 的 default margin，每条 list item 之间出现 2-3 行空白。

视觉测试中**单段文本看不出来**——只有嵌套列表或多 block 内容才暴露。

## 怎么定位

用 MCP 浏览器抓 DOM `outerHTML`：

```bash
# 看 markdown 渲染后的 HTML，确认是否在元素间有大量 \n + spaces
mcp__plugin_playwright__browser_evaluate '() => 
  document.querySelector("[data-testid=agent-msg-assistant_text]").outerHTML'
```

如果输出形如：
```html
<div class="whitespace-pre-wrap">
  <p>...</p>
  <ul>...</ul>
</div>
```

且渲染后元素间有 unexpected 空白行 → 大概率是 `whitespace-pre-wrap` 把元素间的 whitespace 渲染了。

## 修复

**让 markdown 容器不带 `whitespace-pre-wrap`**。如果同一个容器既要承载 markdown 又要承载纯文本：

```jsx
{markdownChild ? (
  <MarkdownContent ... />          // 自管换行，不需 pre-wrap
) : (
  <span className="whitespace-pre-wrap">{plainText}</span>  // 单独包
)}
```

## 项目侧 root cause

我们的 AssistantBlock 是通用容器，初始设计时只考虑 streaming text 场景，没考虑 markdown 渲染后会和 wrapper 的 whitespace 行为冲突。

## 元规则

> **`whitespace-pre-wrap` 只能加在叶节点文本上，不要加在容纳 HTML 子树的容器上**——任何
> 包含 block-level 子元素（p/ul/ol/heading 等）的容器都会因 element 间 `\n` 渲染成空白行
> 而塌房。

跟 [[html_regex_must_verify]]（先 curl 真实 HTML 再写正则）相关——都是"在简单 case 测试通过，复杂内容才暴露"的隐性 bug。
