Zulip 的 Markdown 标签页扩展:{start_tabs}语法如何把 API 文档渲染成分 Tab 的操作指南
【免费下载链接】zulipZulip server and web application. Open-source team chat that helps teams stay productive and focused.项目地址: https://gitcode.com/GitHub_Trending/zu/zulip
本篇指南解析 Zulip 服务端自带的 Tabbed Sections Markdown 扩展:从{start_tabs}/{tab|key}/{end_tabs}的书写语法,讲到zerver/lib/markdown/tabbed_sections.py中预处理器如何逐行解析、校验标签并拼装 HTML,再到 Django 模板过滤器render_markdown_path如何将其挂载进文档渲染管线。读完你可以掌握:如何为自己的文档撰写多平台/多语言分 Tab 内容、合法tab_key的完整清单、未知标签触发的报错机制,以及扩展在预处理器优先级体系中的执行位置。
语法速览:一个真实的测试文档
仓库中有一份专门验证该扩展语法的示例文件 test_tabbed_sections.md,它恰好覆盖了三种典型写法。以下按其原文结构讲解:
# Heading {start_tabs} {tab|ios} iOS instructions {tab|desktop-web} Desktop/browser instructions {end_tabs} ## Heading 2 {start_tabs} {tab|desktop-web} Desktop/browser instructions {tab|android} Android instructions {end_tabs} ## Heading 3 {start_tabs} Instructions for all platforms {end_tabs}三个片段分别演示了:
- 两个标签(
ios与desktop-web):每个{tab|key}声明一个 Tab,其后的内容属于该 Tab,直到下一个{tab|key}或{end_tabs}; - 无空行的紧凑写法:
{tab|desktop-web}与{tab|android}之间可以没有空行,解析器按行匹配标记,不依赖空行分隔; - 无 Tab 的"伪 Tab"段落:
{start_tabs}与{end_tabs}之间只写内容、不写任何{tab|...}行。此时扩展会把整段内容当作单一 Tab 处理,Tab 键固定为instructions-for-all-platforms。
需要牢记的三条硬性规则:
- 标记必须独占一行,且严格匹配(行首行尾都不能有多余字符);
- Tab 的键(
{tab|...}中竖线后面的部分)必须是 zerver/lib/markdown/tabbed_sections.py 中TAB_SECTION_LABELS字典里已注册的 key; - 一段
{start_tabs}...{end_tabs}只能出现在文档中的一层,同一文档内可以有多段(如上述示例的三个 H 段各有一段)。
实现剖析:预处理阶段的逐行解析
该扩展位于 zerver/lib/markdown/tabbed_sections.py,基于 Pythonmarkdown库的Extension机制实现,核心是一个Preprocessor(预处理器)——即在任何 HTML 生成之前先对纯文本行做改写。
三个锚定正则
START_TABBED_SECTION_REGEX = re.compile(r"^\{start_tabs\}$") END_TABBED_SECTION_REGEX = re.compile(r"^\{end_tabs\}$") TAB_CONTENT_REGEX = re.compile(r"^\{tab\|([^}]+)\}$")(tabbed_sections.py#L12-L14)
^...$的双端锚定解释了第一条硬性规则:{ start_tabs }、{tab|ios} 注释这类带额外字符的行都不会被识别为标记,会被当作普通文本原样输出。TAB_CONTENT_REGEX用捕获组([^}]+)提取 Tab 键,允许任意非}字符(所以 key 可以含连字符)。
parse_tabs:从行列表中定位一个 Tab 段
parse_tabs(lines)(tabbed_sections.py#L71-L88)从行列表顶部开始扫描,记录:
- 第一个
{start_tabs}的行号 →start_tabs_index; - 每个
{tab|key}的行号与键名 → 依次追加到tabs列表; - 遇到
{end_tabs}时记录end_tabs_index并break,返回该段的字典结构。
预处理器TabbedSectionsPreprocessor.run()(tabbed_sections.py#L126-L150)随后进入一个while循环:找到一段就渲染掉(把lines[start:end+1]整段替换为生成的 HTML 字符串,并打上markdown="1"属性以便内部内容继续参与 Markdown 渲染),再对剩余行重新调用parse_tabs,直到文档中不再存在{start_tabs}...{end_tabs}段为止。这就是为什么同一篇文档可以包含任意多个 Tab 段,且它们互不干扰。
has-tabs 与 no-tabs 两种形态
if "tabs" in tab_section: tab_class = "has-tabs" else: tab_class = "no-tabs" tab_section["tabs"] = [{"tab_key": "instructions-for-all-platforms", ...}](tabbed_sections.py#L129-L139)
对照示例文档的三个段落:前两段各有{tab|...}行,生成class="tabbed-section has-tabs";第三段没有任何 Tab 声明,被自动补成一个键为instructions-for-all-platforms的隐式 Tab,生成class="tabbed-section no-tabs"。no-tabs形态下导航栏仍会渲染出一个名为 "Instructions for all platforms" 的项(见测试期望 HTML 中的data-tab-key="instructions-for-all-platforms"),前端样式据此决定是否显示可点击的 Tab 头。
TAB_SECTION_LABELS:合法 Tab 键的权威清单
TAB_SECTION_LABELS(tabbed_sections.py#L43-L58)同时承担两个职责:key 是文档中允许使用的 Tab 标识符,value 是渲染到页面上的显示标签。当前清单:
| tab_key | 显示标签 |
|---|---|
desktop-web | Desktop/Web |
ios | iOS |
android | Android |
python | Python |
js | JavaScript |
curl | curl |
zulip-send | zulip-send |
instructions-for-all-platforms | Instructions for all platforms |
for-a-bot | For a bot |
for-yourself | For yourself |
grafana-latest | Grafana 8.3+ |
grafana-older-version | Grafana 8.2 and below |
send-channel-message | Send a channel message |
send-dm | Send a DM |
从这份 key 的命名可以推断出该扩展的主要服务对象:Zulip 的 API 文档(api_docs/ 下大量文件使用{start_tabs},按 Desktop/Web、iOS、Android 客户端或 curl/Python/zulip-send 等调用方式分 Tab),以及部分 Webhook/集成文档(zerver/webhooks/ 各服务的doc.md亦广泛使用,grafana-*两个 key 正是为 Grafana Webhook 文档的版本差异而设)。
源码中有一条值得注意的注释:新增 key 时"也请检查是否需要同步更新tabbed-instructions.js"(tabbed_sections.py#L41-L42)。从当前仓库结构看,前端对.tabbed-section样式的定义位于 web/styles/portico/markdown.css,即该机制主要服务于服务端渲染的文档页面(Portico 文档站)。
错误路径:未知 Tab 键触发 ValueError
仓库配有第二份测试文档 test_tabbed_sections_missing_tabs.md,其中声明了{tab|minix}——一个未在TAB_SECTION_LABELS中注册的键:
{start_tabs} {tab|ios} iOS instructions {tab|minix} Minix instructions. We expect an exception because the minix tab doesn't have a declared label. {end_tabs}对应的测试test_markdown_tabbed_sections_missing_tabs(zerver/tests/test_templates.py#L93-L100)断言渲染过程抛出ValueError,且消息精确匹配:
Tab 'minix' is not present in TAB_SECTION_LABELS in zerver/lib/markdown/tabbed_sections.py抛出点位于generate_nav_bar()(tabbed_sections.py#L152-L166):生成导航栏时需要把每个 key 映射为显示标签,查不到即报错。这种"快速失败"设计保证了文档作者在发布前就能发现拼写错误(比如把desktop-web写成desktop_web),而不是得到一个 Tab 名显示为 key 原文的页面。
渲染产物结构:由模板字符串拼出的 HTML
预处理器用三组模板字符串拼装最终 HTML(tabbed_sections.py#L16-L39):
TABBED_SECTION_TEMPLATE:外层<div class="tabbed-section has-tabs|no-tabs" markdown="1">+ 导航栏 +<div class="blocks">包裹的内容块;NAV_BAR_TEMPLATE:<ul class="nav">内逐 Tab 生成<li>;DIV_TAB_CONTENT_TEMPLATE:每个 Tab 一个<div class="tab-content [active]"><div class="tabbed-section has-tabs" markdown="1"> <ul class="nav"> <li class="active">zerver.lib.markdown.nested_code_blocks.makeExtension(), zerver.lib.markdown.tabbed_sections.makeExtension(), zerver.lib.markdown.help_settings_links.makeExtension(), ...测试正是经由 Django 模板
tests/test_markdown.html触发该过滤器:上下文变量markdown_test_file指向zerver/tests/markdown/test_tabbed_sections.md,template.render(context)即完成"Markdown 文件 → HTML"的全过程(test_templates.py#L21-L27)。注意render_markdown_path的 docstring 强调:渲染出的 HTML 被视为可信内容,该路径面向文档而非用户输入——{tab|minix}抛出的ValueError也因此发生在服务端渲染阶段,而非任何用户可见界面。执行时机由优先级决定
扩展注册时使用的优先级取自 zerver/lib/markdown/priorities.py:
PREPROCESSOR_PRIORITIES = { ... "fenced_code_block": 25, # "html_block": 20, "tabbed_sections": -500, "nested_code_blocks": -500, ... }注册代码见 tabbed_sections.py#L62-L68:
TabbedSectionsGenerator.extendMarkdown把预处理器以名称tabbed_sections、优先级-500挂入md.preprocessors。-500是刻意取的低值(注释标明 registry 中数值越大越先执行):Tab 段内部可能含有围栏代码块,必须先由fenced_code_block(优先级 25)等上游预处理器处理,Tabbed Sections 在接近最后阶段对整个段做整体替换,避免干扰其他扩展对段内行的解析。小结
Zulip 的 Tabbed Sections 扩展用不到两百行代码实现了一套完整的"多平台文档分栏"机制:三个行级正则锚定语法、
parse_tabs定位段落、TAB_SECTION_LABELS提供白名单式校验与显示文案、四组 HTML 模板负责拼装、markdown="1"属性让 Tab 内部 Markdown 得以二次渲染,而-500的预处理器优先级则保证了它与其他文档扩展(围栏代码、嵌套代码块等)的安全协作。如果你需要在 Zulip 的 API/集成/帮助文档中新增分 Tab 说明,只需遵循{start_tabs}→ 若干{tab|<合法key>}→{end_tabs}的独占行写法,把test_tabbed_sections.md与 test_templates.py 中的期望 HTML 当作可运行的验收标准即可;新增 Tab 键时,则必须同步在TAB_SECTION_LABELS中登记,否则渲染将以ValueError快速失败。【免费下载链接】zulipZulip server and web application. Open-source team chat that helps teams stay productive and focused.
项目地址: https://gitcode.com/GitHub_Trending/zu/zulip
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考