← 精品代码功能 · 可复用实现库

精品功能 03:零依赖 Markdown 渲染器(含"答案折叠")

支持一个独特扩展——把列表项后面的引用折叠成"看答案"按钮。

而引入 markdown 库会带来依赖和不可控的输出。

核心实现

渲染器是一个手写状态机,用"层栈"管理列表嵌套(第 112-137 行):

def render(md: str) -> str:
    """把 Markdown 正文渲染成 HTML 片段(不含 <html>/<body>)。

    列表用一个"层栈"来管:每层记 {indent, kind, li_open}。
    `<li>` 故意**不立刻闭合**,因为「课堂提问」的折叠答案要落在它里面;
    遇到下一个块级元素或列表层级变化时才补上 `</li>`。
    """
    stack: list[dict] = []   # [{indent, kind, li_open}]
    can_attach = False       # 刚输出过 <li>,下一条引用可以折叠进去

答案折叠的关键分支(第 197-213 行):

if s.startswith(">"):                       # 引用
    ...
    if can_attach and stack:
        # 「课堂提问」:答案折叠进上一个列表项,先自己想再看答案
        out.append('<details class="answer"><summary>看答案</summary>'
                   f'<div class="answer-body">{_loose(body)}</div></details>')
        close_li(stack[-1])                 # 现在才补 </li>
        can_attach = False
    else:
        out.append(f"<blockquote>{_loose(body)}</blockquote>")

还有一个容易被忽略但很真实的坑(第 143-156 行):
松散列表(列表项之间有空行)不能断开,否则会另起一个 <ol>、编号从 1 重来。

设计亮点

  1. 状态机 + 层栈:处理任意嵌套的列表,比"逐行 if"健壮得多
  2. 延迟闭合 <li>:为了实现"答案挂在列表项里"这个扩展,故意不立刻写 </li>
  3. 区分紧凑/松散列表:编号连续性这种细节,才是"能用的渲染器"和"玩具"的分界
  4. 零依赖:只有标准库 re + html,任何环境都能跑
  5. 有 197 行测试兜底(含"折叠答案必须落在 <li> 内""松散列表不能断成两个 <ol>")

可复用性评估

开源化建议

对照开源

相关课程

第 19 课(零依赖构建与交付验收)