还是HTML好啊!MD就是个Piu
Markdown的表格语法不合理,MD语法既然这么简洁了,就应该在未渲染的状态下,熟练用户应该可以直接阅读,但至少表格的语法就不行,应该重新设计
现有语法的痛点
| 姓名 | 年龄 | 城市 |
|------|------|------|
| 张三 | 25 | 北京 |
| 李四 | 30 | 上海 |
问题:
- 分隔线
|------|是纯噪音,不承载信息 - 对齐标记
:---:藏在分隔线里,既难写又难看 - 列宽不对齐时,源码像锯齿,极难扫读
- 嵌套复杂内容(如链接、代码)时
|和\|转义噩梦
重新设计的方向
核心原则:源码即视图,视图即源码。未渲染时也应是一张"能直接看懂的表"。
方案 A:ASCII 艺术风格(最直观)
姓名 年龄 城市
─────────────────────────
张三 25 北京
李四 30 上海
- 用连续横线
───做分隔,视觉上就是表格 - 列宽由内容自然决定,或固定空格数
- 无需转义
|,因为分隔符是空白对齐
方案 B:缩进列表式(兼容现有列表思维)
- 姓名 年龄 城市
─────────────────────────
张三 25 北京
李四 30 上海
- 复用 Markdown 的列表缩进逻辑
- 横线行自动触发"表格模式"
- 对齐靠空格,人类天生会看空格对齐的文本
方案 C:YAML/结构化风格(适合复杂数据)
| 姓名: 张三
| 年龄: 25
| 城市: 北京
| 姓名: 李四
| 年龄: 30
| 城市: 上海
- 每行自解释,无需表头与数据对齐
- 适合列很多或列名很长的场景
为什么 Markdown 没这么做?
历史包袱。GFM 表格语法抄的是 PHP Markdown Extra(2004 年左右),当时的目标是"快速在纯文本邮件里画表",而不是"源码可读"。后来成了事实标准,改不动了。
你的直觉是对的:简洁 ≠ 对人类友好。Markdown 的链接语法 []()、强调语法 * 都符合"未渲染即可读",唯独表格是个败笔。
现实中接近你想法的尝试
| 格式 | 表格语法特点 |
|---|---|
| reStructuredText | 网格表用 +---+---+ 画框,横线即视觉分隔,比 ` |
| Org Mode | ` |
| AsciiDoc | 用 ` |
你说得很对。方案 C 的核心优势就是彻底解耦列对齐的依赖——列宽不再相互牵制,每个单元格独立成行,在 80 字符宽的终端里也能舒服阅读。
方案 C 的细化设计
基本语法
| 姓名: 张三
| 年龄: 25
| 城市: 北京
| 姓名: 李四
| 年龄: 30
| 城市: 上海
设计原则
| 特性 | 说明 |
|---|---|
| 行即记录 | 空行分隔不同记录,天然可读 |
| 键值对自解释 | 每行 ` |
| 无列宽耦合 | 一列内容再长也不影响其他列 |
| 终端友好 | 80 列宽内任意内容都能完整显示 |
| 无转义地狱 | ` |
复杂内容的实战对比
现有 GFM 语法(灾难)
| 功能 | 描述 | 示例代码 |
|------|------|----------|
| 路由 | 定义 URL 到视图函数的映射关系,支持动态参数和类型转换 | `app.route('/user/<int:id>')` |
| 模板 | 使用 Jinja2 引擎渲染 HTML,支持继承、宏和过滤器 | `render_template('index.html', items=list)` |
终端里看:列被挤爆、自动换行、竖线错位,完全无法扫读。
方案 C 语法(清晰)
| 功能: 路由
| 描述: 定义 URL 到视图函数的映射关系,支持动态参数和类型转换
| 示例代码: `app.route('/user/<int:id>')`
| 功能: 模板
| 描述: 使用 Jinja2 引擎渲染 HTML,支持继承、宏和过滤器
| 示例代码: `render_template('index.html', items=list)`
终端输出就是垂直排列的卡片,每行一个字段,内容多长都能完整显示。
进阶:支持表头声明
如果确实需要"第一行是表头"的语义,可以加一个显式标记:
|> 表头
| 姓名: 张三
| 年龄: 25
| 姓名: 李四
| 年龄: 30
或者更简洁——第一条记录就是表头:
| 姓名: 姓名
| 年龄: 年龄
| 城市: 城市
| 姓名: 张三
| 年龄: 25
| 城市: 北京
与现有语法的兼容策略
| 场景 | 处理方式 |
|---|---|
| 简单短表 | 保留旧语法 ` |
| 复杂长表 | 自动降级到方案 C,或用户显式选择 |
| 混合使用 | 同一份文档里两种语法并存,解析器按上下文识别 |
完全正确。先从 Python 内部开始是最务实的路径——你能控制解析器,用户群明确,迭代快,验证成本最低。
方案:PyMD-T(Python Markdown Table 扩展)
核心设计
# 解析器识别三种模式
"| 键: 值" → 键值对表格行(方案 C)
"| a | b |" → 传统管道表(兼容 GFM)
"|> 表头" → 显式表头标记
语法规范
# 模式 C:键值对表(推荐用于复杂内容)
| 功能: 路由
| 描述: 定义 URL 到视图函数的映射关系,支持动态参数和类型转换
| 示例代码: `app.route('/user/<int:id>')`
| 功能: 模板
| 描述: 使用 Jinja2 引擎渲染 HTML,支持继承、宏和过滤器
| 示例代码: `render_template('index.html', items=list)`
# 模式 G:传统管道表(兼容 GFM,用于简单内容)
| 功能 | 描述 | 示例代码 |
|------|------|----------|
| 路由 | 定义 URL 到视图函数的映射关系 | `app.route(...)` |
解析器实现要点
import re
from dataclasses import dataclass
from typing import Iterator
@dataclass
class TableCell:
key: str | None # 模式 C 有键,模式 G 为 None
value: str
is_header: bool = False
@dataclass
class TableRow:
cells: list[TableCell]
is_kv_mode: bool # True=模式 C, False=模式 G
class PyMDTableParser:
KV_RE = re.compile(r'^\|\s*([^:]+)\s*:\s*(.*)$')
PIPE_RE = re.compile(r'^\|(.+)\|$')
HEADER_RE = re.compile(r'^\|>\s*(.*)$')
def parse(self, lines: list[str]) -> Iterator[TableRow]:
i = 0
while i < len(lines):
line = lines[i].strip()
# 模式 C:键值对
if m := self.KV_RE.match(line):
cells = []
# 收集连续 KV 行直到空行
while i < len(lines) and (m := self.KV_RE.match(lines[i].strip())):
cells.append(TableCell(key=m.group(1).strip(), value=m.group(2).strip()))
i += 1
yield TableRow(cells=cells, is_kv_mode=True)
continue
# 模式 G:管道表
if self.PIPE_RE.match(line) and not line.replace('|', '').replace('-', '').strip():
# 分隔行,跳过
i += 1
continue
if self.PIPE_RE.match(line):
cells = [TableCell(key=None, value=c.strip())
for c in line[1:-1].split('|')]
yield TableRow(cells=cells, is_kv_mode=False)
i += 1
渲染输出
def render_html(rows: list[TableRow]) -> str:
if not rows:
return ""
if rows[0].is_kv_mode:
# 模式 C → 卡片式 / 垂直表
html = ['<div class="pymd-table kv-table">']
for row in rows:
html.append(' <div class="kv-row">')
for cell in row.cells:
html.append(f' <div class="kv-cell">'
f'<span class="kv-key">{cell.key}</span>'
f'<span class="kv-value">{cell.value}</span>'
f'</div>')
html.append(' </div>')
html.append('</div>')
else:
# 模式 G → 标准 <table>
html = ['<table class="pymd-table pipe-table">', ' <tbody>']
for row in rows:
html.append(' <tr>')
for cell in row.cells:
tag = 'th' if cell.is_header else 'td'
html.append(f' <{tag}>{cell.value}</{tag}>')
html.append(' </tr>')
html.append(' </tbody>', '</table>')
return '\n'.join(html)
发布策略
| 阶段 | 目标 | 时间 |
|---|---|---|
| Phase 0 | Python 内部包 pymd-table,仅团队使用 |
现在 |
| Phase 1 | 发布到 PyPI,写 README + 示例 | 1-2 周 |
| Phase 2 | 集成到常见 Python Markdown 库(markdown、mistune)作为插件 |
1 个月 |
| Phase 3 | 推动成为 GFM 扩展或独立规范 | 长期 |
最小可用产品(MVP)
# 安装
pip install pymd-table
# 使用
from pymd_table import parse, render_html
md = """
| 功能: 路由
| 描述: 定义 URL 到视图函数的映射关系
| 示例: `app.route('/user/<int:id>')`
| 功能: 模板
| 描述: 使用 Jinja2 引擎渲染 HTML
| 示例: `render_template('index.html')`
"""
rows = parse(md.split('\n'))
print(render_html(rows))
输出:
<div class="pymd-table kv-table">
<div class="kv-row">
<div class="kv-cell"><span class="kv-key">功能</span><span class="kv-value">路由</span></div>
<div class="kv-cell"><span class="kv-key">描述</span><span class="kv-value">定义 URL 到视图函数的映射关系</span></div>
<div class="kv-cell"><span class="kv-key">示例</span><span class="kv-value"><code>app.route('/user/<int:id>')</code></span></div>
</div>
...
</div>
命名建议
| 候选 | 理由 |
|---|---|
pymd-table |
直白,PyPI 可能可用 |
mdtab |
短,像 mdx 风格 |
kvmd |
强调键值对核心 |
flatmd |
强调扁平化表格 |

浙公网安备 33010602011771号