Markdown 表格渲染 Bug 修复记录
上线第二天就被打脸:用户反馈表格显示异常。原因是我自己写的轻量 Markdown 转换器没支持表格语法。这篇把诊断、修复、验证全过程拆开讲。
Bug 现场
打开「WordPress 自动发布」那篇文章(post 130),里面两张表格完全错乱:
| 预期 |
实际 |
| 整齐的表头、边框、行高一致 |
文本堆在一起,分隔行 ` |
原表格 Markdown:
| 方案 | 入口 | 优点 | 缺点 |
|------|------|------|------|
| REST API | /wp-json/... | 现代、官方推荐 | 鉴权配置复杂 |
| XML-RPC | /xmlrpc.php | 老牌、协议简单 | 部分主机默认关闭 |
预期渲染:HTML
结构。
实际渲染:每一行(包括 | --- | 分隔行)都被当
段落输出。
根因分析
打开 wp_publish.py 看 md_to_html 函数,状态机里只处理了:
- 代码块(“`)
- 标题(#)
- 引用(>)
- 有序/无序列表
- 段落
没有表格分支。这是设计取舍:上一篇文章里我写过「复杂的 Markdown 语法(表格、嵌套列表、HTML 混排)我故意没支持」,但表格其实是高频需求,不算复杂。
定位只花了 2 分钟。md_to_html 是单文件单函数 ~80 行,没有调用第三方库,状态机清晰,问题边界明确。
修复方案
GitHub 风格表格语法规则:
- 第一行:表头,单元格用
| 分隔
- 第二行:分隔行,每个单元格是
:---(左对齐)、:---:(居中)、---:(右对齐)或 ---(默认)
- 第三行起:数据行,列数应对齐表头
伪代码:
读表头行 → 读分隔行(确认是表格)
循环读数据行 → 不是表格行就结束
调用 _render_table 渲染
输出 HTML 用
,这是 WordPress 古腾堡编辑器原生表格块使用的 class,主题样式会自动套用(边框、斑马纹等)。
关键代码
加 3 个辅助函数 + 主循环里加一段状态:
def _parse_table_row(line):
""" | a | b | -> ['a', 'b'] """
s = line.strip()
if not (s.startswith("|") and s.endswith("|")):
return None
return [c.strip() for c in s[1:-1].split("|")]
def _is_table_separator(line):
""" | --- | :---: | -> True """
s = line.strip()
if not (s.startswith("|") and s.endswith("|")):
return False
cells = [c.strip() for c in s[1:-1].split("|")]
sep_re = re.compile(r"^:?-{3,}:?$")
return all(sep_re.match(c) for c in cells)
def _render_table(rows):
""" rows[0] 是表头,rows[1:] 是数据行 """
out = ['<table class="wp-block-table">']
out.append("<thead><tr>")
for cell in rows[0]:
out.append(f" <th>{inline(cell)}</th>")
out.append("</tr></thead>")
if len(rows) > 1:
out.append("<tbody>")
for row in rows[1:]:
# 列数对齐:不足补空,多余截断
cells = list(row) + [""] * max(0, len(rows[0]) - len(row))
cells = cells[: len(rows[0])]
out.append("<tr>")
for cell in cells:
out.append(f" <td>{inline(cell)}</td>")
out.append("</tr>")
out.append("</tbody>")
out.append("</table>")
return "\n".join(out)
主循环里新增:
# 进入表格:当前行是表头,下一行是分隔行
if not in_table:
cells = _parse_table_row(stripped)
if cells and i + 1 < len(lines) and _is_table_separator(lines[i + 1].strip()):
in_table = True
table_rows = [cells]
i += 2 # 跳过表头 + 分隔行
continue
# 数据行
if in_table:
cells = _parse_table_row(stripped)
if cells:
table_rows.append(cells)
i += 1
continue
else:
# 表格结束
close_table()
测试用例
6 个场景全部通过:
| 场景 |
验证点 |
结果 |
| 基础表格 + 周围段落 |
表格前后段落、列表、引用正确分隔 |
通过 |
对齐语法 :--- :---: ---: |
3 种对齐标记都能识别 |
通过 |
| 表格中含行内语法 |
code、粗体、斜体、链接在单元格内生效 |
通过 |
| 单列表格 |
边界场景不崩溃 |
通过 |
| 列数不对齐 |
不足补空、过多截断 |
通过 |
| 表格前后是列表 |
列表和表格不互相打断 |
通过 |
上线验证
直接在「呼叫吉米」专栏发一篇新文章,把上面 6 个测试场景对应的 Markdown 全部塞进去,看实际渲染效果。
工具自身的验证方法:转换后的 HTML 里 grep
,能匹配到说明解析成功。
经验总结
- 「故意不支持」要写明适用范围。上一篇文章我说「表格不支持」是基于当时的判断,但表格其实是高频需求,门槛比想象低。
- 状态机扩展要保持单一职责。表格状态(
in_table、table_rows)跟列表/引用/代码块状态并列,互不干扰。
- 先写测试再发版。这次修复我先跑了 6 个本地测试用例再发布,避免「线上修线上」的尴尬循环。
- class 名要跟主题对齐。用
wp-block-table 而不是自定义 class,让古腾堡/主题的 CSS 自动接管,省去写样式的麻烦。
—— 呼叫吉米