marimo 中的mo.outline:为 Notebook 自动生成可点击的 Markdown 目录大纲
【免费下载链接】marimoA reactive notebook for Python — run reproducible experiments, query with SQL, execute as a script, deploy as an app, and version with git. Stored as pure Python. All in a modern, AI-native editor.项目地址: https://gitcode.com/GitHub_Trending/ma/marimo
导读
mo.outline是 marimo 提供的无状态布局函数,用于在 Notebook 中渲染一个"目录大纲"组件:它会自动提取已执行单元格中所有 Markdown 标题(h1–h6),按层级结构展示并支持点击跳转、滚动高亮。本文基于 docs/api/layouts/outline.md 展开,结合后端输出实现与前端渲染源码,讲解它的用法、参数、工作原理与适用场景,读完你可以直接在 Notebook 中复现一个带目录的大纲组件,并理解它与编辑器侧边栏大纲、悬浮大纲之间的协作关系。
一、它是什么:无状态布局函数家族的一员
在 marimo 中,marimo.outline属于"无状态(Stateless)"的布局函数。与marimo.ui下带有交互值(如tabs记录选中标签、table记录选中行)的元素不同,无状态布局函数不携带任何值,只负责以特定方式渲染内容。docs/api/layouts/index.md 中将其描述为:"Display table of contents outline"(展示目录大纲),与accordion(折叠区)、carousel(轮播)、sidebar(侧边栏)、tree(树形结构)等函数并列。
这一点在实现层面也有印证:后端 marimo/_output/outline.py 通过build_stateless_plugin(component_name="marimo-outline", ...)构建组件,返回的是Html对象而非带值的 UI 元素;前端 frontend/src/plugins/layout/OutlinePlugin.tsx 中的OutlinePlugin也实现了IStatelessPlugin<Data>接口。因此大纲组件只输出视图,不产生任何可供程序读取的值。
二、最小可用示例:三行代码得到一份目录
outline的官方文档给出了一个非常简洁的完整示例(见 docs/api/layouts/outline.md)。在 Notebook 中创建三个单元格:
# 单元格 1:定义一个一级标题 import marimo as mo @app.cell def __(): mo.md("# Header 1") return # 单元格 2:定义一个二级标题 @app.cell def __(): mo.md("## Header 2") return # 单元格 3:渲染目录大纲 @app.cell def __(): mo.outline(label="Table of Contents") return运行全部单元格后,mo.outline的位置会渲染出一个带标题栏(内容为label参数传入的 "Table of Contents")的目录列表,其中包含# Header 1与## Header 2两个条目,并呈现层级缩进。点击任意条目,页面会平滑滚动到对应的标题位置。
需要说明的是,上面示例中的@app.cell是 marimo 脚本/导出场景下的装饰器写法(每个单元格一个函数);在日常交互式 Notebook 中,你只需要直接依次输入mo.md("# Header 1")、mo.md("## Header 2")和mo.outline(label="Table of Contents")三个单元格即可,效果完全相同。
三、参数说明:label
outline是纯关键字参数函数,签名如下(见 marimo/_output/outline.py):
def outline(*, label: str = "") -> Html| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
label | str | "" | 大纲组件顶部显示的描述性标题文本。传入后会在目录列表上方渲染一个标题栏;不传则没有标题栏 |
label只是"给组件本身命名"的说明性文本,与大纲的内容无关。官方示例中将其设为"Table of Contents"(目录),你也可以按需设置,例如mo.outline(label="📖 本章导航")或mo.outline(label="Sections")。
前端渲染时,label会显示在组件顶部:见 frontend/src/plugins/layout/OutlinePlugin.tsx 中OutlineContent的实现——当label非空时,会在带边框的容器顶部渲染一个px-4 py-2 border-b font-medium text-sm样式的标题栏。
四、大纲从哪里来:自动提取已执行单元格中的 Markdown 标题
mo.outline的内容并非手动传入,而是自动扫描整个 Notebook 中已执行单元格的 Markdown 标题聚合而成。其解析逻辑位于前端 frontend/src/core/dom/outline.ts:
- 对每个输出的 HTML 字符串使用
DOMParser解析,并通过querySelectorAll("h1, h2, h3, h4, h5, h6")收集全部标题(注释中说明此前只支持到 h3,因用户请求扩展到了 h6); - 每个标题生成一个
OutlineItem,包含name(纯文本标题)、level(1–6 级)与定位信息by(优先使用id,无 id 时退化为 XPath 定位),类型定义见 frontend/src/core/cells/outline.ts; - 若标题内部 HTML 与纯文本不一致(例如包含 LaTeX 公式、行内样式),会额外保留
html字段,大纲渲染该富文本而非纯文本,保证数学公式等内容在大纲中原样呈现; - 被
marimo-carousel、marimo-tabs、marimo-accordion、marimo-sidebar等组件包裹的标题会被显式排除(excludedTags),避免轮播、页签等嵌套区域内的标题污染顶层目录。
对 LaTeX 标题的支持在仓库中有专门的冒烟测试验证:marimo/_smoke_tests/markdown/latex_outline.py 构造了包含$E = mc^2$、行内公式、展示公式与混合文本标题的 Notebook,用于测试这些标题在大纲面板中的渲染正确性。前端另有 frontend/src/core/dom/tests/outline.test.ts 对标题提取、排除规则与折叠范围计算进行单元测试。
还有一个关键前提:大纲只来自"已执行"的单元格。后端 docstring 明确写道 "The outline automatically extracts all markdown headers fromexecuted cells"(见 marimo/_output/outline.py),未运行过的单元格中的标题不会出现在大纲中。因此,若要生成完整目录,请确保所有含标题的 Markdown 单元格都已执行。
五、交互行为:点击跳转、滚动高亮与空状态
大纲组件的交互由 frontend/src/components/editor/chrome/panels/outline/useActiveOutline.tsx 与 frontend/src/components/editor/chrome/panels/outline/floating-outline.tsx 中的OutlineList共同实现:
- 点击跳转:
scrollToOutlineItem通过scrollIntoView({ behavior: "smooth", block: "start" })平滑滚动到目标标题,并给该标题添加 3 秒的outline-item-highlight高亮样式,帮助读者定位; - 当前章节高亮:
useActiveOutline使用IntersectionObserver监听各标题在视口内的可见性,始终将"最靠上的可见标题"标记为活动项,使大纲能跟随阅读进度实时高亮; - 空状态:当 Notebook 中没有可提取的标题时(
items.length === 0),组件渲染一条虚线边框的提示文案 "No outline found. Add markdown headings to your notebook to create an outline.",引导用户先添加 Markdown 标题(见 OutlinePlugin.tsx); - 重复标题处理:定位信息相同的标题(如同名标题出现多次)会按出现次序区分(
occurrences),保证点击与高亮都能命中正确的那个。
六、与编辑器侧边栏大纲 / 悬浮大纲的关系
mo.outline组件与 marimo 编辑器的两个内置大纲能力共享同一套数据源与解析逻辑,但定位不同:
- 侧边栏大纲面板:编辑器左侧/右侧的开发面板(Developer Panel)中自带大纲视图(对应 frontend/src/components/editor/chrome/panels/outline-panel.tsx),面向"编辑 Notebook"场景;
- 悬浮大纲 + 迷你地图:
FloatingOutline(见 floating-outline.tsx)在屏幕右侧提供一个悬浮的目录入口:当大纲条目少于 2 个时不显示,鼠标悬停时展开一个 300px 宽的目录面板,旁边还附带一条按标题层级缩进的小刻度组成的MiniMap(迷你地图),点击刻度即可跳转;它在小屏(md以下)与打印场景(print:hidden)下自动隐藏; mo.outline组件:是"用户显式放入 Notebook 单元格"的可见目录,适合演示文稿、导出的应用(App)或长文阅读场景——尤其当你把 Notebook 部署为 App 时,只有mo.outline这种显式写入单元格的组件会出现在最终页面中。
三者读取的都是同一个notebookOutline状态(见 frontend/src/core/cells/cells.ts),因此无论你通过哪种方式查看,得到的目录结构与高亮行为都是一致的。
七、典型使用建议
- 在长篇教程、研究报告或演示型 Notebook 的顶部放置
mo.outline(label="Table of Contents"),为读者提供全局导航; - 与
mo.md标题配合使用时,注意保持标题层级语义正确(h1 → h2 → h3 逐级递减),因为大纲的缩进与折叠判断依赖level值; - 大纲只汇总"已执行"单元格中的标题,演示前务必全部运行一遍;
- 若希望排除某些区域(如页签、轮播、折叠区、侧边栏内部的标题),无需额外配置——解析层已默认排除这些容器内的标题。
参考实现路径速查
| 关注点 | 仓库路径 |
|---|---|
| 官方 API 文档 | docs/api/layouts/outline.md |
| 无状态布局函数总览 | docs/api/layouts/index.md |
后端实现(outline函数) | marimo/_output/outline.py |
公开导出(__all__) | marimo/init.py |
| 前端组件插件 | frontend/src/plugins/layout/OutlinePlugin.tsx |
| 标题提取与排除规则 | frontend/src/core/dom/outline.ts |
| 大纲条目类型定义 | frontend/src/core/cells/outline.ts |
| 高亮 / 跳转 / 悬浮大纲 / 迷你地图 | frontend/src/components/editor/chrome/panels/outline/useActiveOutline.tsx、floating-outline.tsx |
| LaTeX 标题冒烟测试 | marimo/_smoke_tests/markdown/latex_outline.py |
| 标题解析单元测试 | frontend/src/core/dom/tests/outline.test.ts |
【免费下载链接】marimoA reactive notebook for Python — run reproducible experiments, query with SQL, execute as a script, deploy as an app, and version with git. Stored as pure Python. All in a modern, AI-native editor.项目地址: https://gitcode.com/GitHub_Trending/ma/marimo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考