Typora Markdown标题自动编号:CSS计数器、大纲与TOC目录实践 前阵子帮同事整理一份 Typora 上的技术手册四十多页标题层级深到四级。他发来的截图里大纲已经歪得不成样子一级标题有的带编号有的不带二级标题从 3.1 直接跳到 3.4。问题不在他写的 Markdown而在于他把三件不同的事混成了一件——侧边栏里的大纲、正文里的标题、以及用[TOC]生成的目录这三处在 Typora 里是三套独立的渲染区编号的来源、能不能自动、改了会不会生效全都不一样。正文标题的自动编号用 CSS 计数器counter就能稳定拿下大纲和目录能不能跟着编号得看你手上的版本和主题很多时候只能接受一个体面的妥协。这篇笔记把我在 Typora 里调侧边栏、调目录、给内容标题做自动编号以及改编号格式的整套做法写下来包括每一步为什么会这么写、哪里最容易翻车、翻车之后按什么顺序查。如果你只是想给正文加个1.1前缀第 3 章直接抄如果你要的是第 1 章 / 一/ 1.1这种混搭中文格式第 5 章有现成配方如果你改完 CSS 一点反应都没有第 7 章的排查顺序能省你半小时。1. 先把自动编号这件事拆开三个渲染区各管各的1.1 正文、侧边栏大纲、[TOC] 目录互不认识Typora 的正文区在 DOM 上的容器是#write你写的#、##最终会变成真正的h1、h2标签这也是为什么正文标题的自动编号可以靠纯 CSS 实现——有真实标签就有伪元素可以挂。侧边栏里的大纲是另一个渲染流程它读的是文档里的标题结构但输出成一套列表节点标题文字被当成纯文本塞进去所以正文里那些靠::before画出来的编号它一个字都看不到。[TOC]更特殊它是文档打开时动态生成的锚点列表同样只取标题的纯文本内容。理解这点之后很多玄学问题就有了答案。比如你在 CSS 里写了#write h2::before { content: 1.1 }正文显示正常大纲里干干净净——这不是没生效是大纲本来就不吃这套。反过来如果你为了省事直接在标题里手写## 1.1 环境准备那正文、大纲、目录三处会同时出现编号一致性最好代价是每次插入新章节都要手动重排后面所有编号。所以选方案之前先想清楚你到底要哪一处带编号。绝大多数人其实只需要正文和导出的 PDF 带编号大纲和目录保持纯文本反而更清爽点起来也更快。1.2 纯 CSS 能做到什么做不到什么我把能稳定实现和需要碰运气的部分列一下免得你在一个注定做不干净的方向上耗时间。需求纯 CSS 可行性说明正文 h1-h4 自动编号稳定#write下有真实标签计数器可靠导出 PDF/HTML 保留编号稳定导出复用同一套结构伪元素会一起渲染编号格式改成中文、罗马数字稳定用counter()的内置样式或counter-style侧边栏配色、字号、高亮稳定主题变量加少量选择器即可侧边栏大纲自动编号看版本DOM 结构不统一要按实际情况调整[TOC]目录自动编号看版本扁平列表做分级计数器容易错位源码模式下显示编号做不到源码模式换了容器#write规则不生效提示判断一条 CSS 能不能落地先问这段内容有没有真实的标签结构。没有标签的地方CSS 只能改外观改不了内容。1.3 动手前先确认版本和主题文件夹打开文件 → 偏好设置 → 外观点打开主题文件夹这是最省事的入口不用去背 Windows 或 macOS 上的具体路径。进去之后你会看到一堆官方主题的.css文件不要直接改官方文件Typora 升级时会覆盖它们你辛辛苦苦调的东西一夜回到解放前。我的做法是复制一份改造成自己的主题比如把github.css复制成github-mine.css所有改动都写在这个副本里。文件名建议用英文加连字符中文文件名在部分版本的菜单里排序和显示都不太正常。改完之后在主题菜单里切换一次到你的新主题Typora 有时不会实时刷新样式切走再切回来是最快的强制重载方式。版本差异这件事必须强调新版本和几年前的老版本在 DOM 结构和 CSS 支持度上差得很远网上抄来的选择器在你机器上不命中太正常了。解决方案不是硬猜而是拿到你本机真实的类名方法在第 2 章和第 7 章。2. 侧边栏宽度、字号与高亮先让工作区看着顺眼2.1 不用写 CSS 就能搞定的三件事在动样式之前先确认原生功能有没有被漏掉。侧边栏的面板切换在视图菜单里大纲、文件列表、文件树是并列的几项菜单路径比快捷键靠谱——快捷键在不同平台和版本上有差异背它不如记菜单位置。大纲面板顶部有过滤输入框长文档里输几个字就能把不相关的层级筛掉比滚动快得多。侧边栏和正文之间的分隔条一般可以拖拽调整宽度这个动作不需要任何配置。如果你觉得重启之后宽度偶尔会复位那就只能接受或者用下面提到的固定宽度写法兜一下——但要注意固定宽度在窄屏上会让正文区被挤得很难看属于配置一时爽换设备就翻车的类型。真正值得花时间的是大纲的层级和可读性。深到四五级的文档如果大纲里每一条都长一个样你根本分不清自己在哪一层这时候调字号递进和左侧缩进比调配色有用得多。2.2 用开发者工具认清你这一版的真实类名Typora 是 Electron 应用本质上是浏览器。如果你的版本在菜单里有开发者工具或检查元素之类的入口直接对着侧边栏右键或者用快捷键打开点一下大纲的某一条右边会告诉你它真实的标签和 class。如果你的版本没有这个入口有个更稳的替代方案导出为 HTML文件 → 导出 → HTML用浏览器打开导出的文件按 F12 看结构。导出的 HTML 里能看到#write下标题的真实层级关系也能看到[TOC]生成后的锚点列表长什么样。虽然侧边栏本身不在导出结果里但正文和目录这两块的信息足够你写选择器了剩下的侧边栏部分靠变量改色基本够用。2.3 用主题变量改色比硬写选择器稳得多Typora 的主题体系里有一批 CSS 变量主题作者靠它们定义全局配色。你自己改样式时优先改变量而不是覆盖具体元素因为变量是公开约定跨版本稳定性远高于内部类名。常用的几个变量名作用--bg-color主背景色--text-color正文文字色--side-bar-bg-color侧边栏底色--active-file-bg-color侧边栏当前项底色--active-file-text-color侧边栏当前项文字色--item-hover-bg-color侧边栏悬停底色--item-hover-text-color侧边栏悬停文字色--control-text-color面板里控件文字色一个能直接用的片段:root { --side-bar-bg-color: #fafbfc; --active-file-bg-color: #e8eefc; --active-file-text-color: #1a56db; --item-hover-bg-color: #eef2f8; --item-hover-text-color: #1f2d3d; } /* 侧边栏整体字号13px 在 1080p 上比较舒服 */ #typora-sidebar { font-size: 13px; } /* 大纲条目的呼吸感行高比 padding 更好调 */ .outline-item { line-height: 1.9; }这里有个很典型的坑网上不少侧边栏美化 CSS 是给 macOS 版写的依赖半透明背景之类的效果。搬到 Windows 版上就会出现侧边栏看着发黑、跟正文明显割裂的观感其实不是 CSS 写错了是底层效果本身不存在。遇到这种情况把侧边栏底色和正文底色调成同一个色系的差异值视觉上立刻统一。注意如果你改完侧边栏发现文字和背景糊在一起先在开发者工具里确认是不是主题把--text-color和--side-bar-bg-color设成了接近的色值。这类问题不要靠!important硬顶改变量就能解决。3. 正文标题自动编号计数器怎么写才不出错3.1 counter-reset 和 counter-increment 到底怎么跑CSS 计数器可以理解成一排计分板。counter-reset: h1表示在这个元素上把 h1 这块计分板清零counter-increment: h1表示走到这个元素h1 加一content: counter(h1)表示把当前 h1 的值印出来。真正需要理解的是作用域在一个元素上 reset 某个计数器会影响到这个元素之后的同级元素以及它们的后代这正是它能在连续的 h2 之间共享同一个父级计数的原因。把这个机制翻译成中文写作的直觉就是遇到 h1 就把 h2 的计分板清零遇到 h2 就把 h3 的清零这样1.1、1.2、2.1的编号才会正确地跟着章走。只写自增不写清零是新手最常犯的错——结果就是全文一路数下去第 5 章的二级标题跑到1.9。还有一个容易忽略的点计数发生在元素本身而不是伪元素上。所以counter-increment要写在h2上content写在h2::before上两个地方的分工不能搞反。3.2 一份可以抄的四级编号配置/* 1. 在最外层建立计数器 */ #write { counter-reset: h1; } /* 2. 逐级自增同时给下级清零 */ #write h1 { counter-reset: h2; counter-increment: h1; } #write h2 { counter-reset: h3; counter-increment: h2; } #write h3 { counter-reset: h4; counter-increment: h3; } #write h4 { counter-increment: h4; } /* 3. 用伪元素把编号画出来不改动 Markdown 原文 */ #write h1::before { content: counter(h1) . ; padding-right: 0.3em; } #write h2::before { content: counter(h1) . counter(h2) ; padding-right: 0.3em; } #write h3::before { content: counter(h1) . counter(h2) . counter(h3) ; padding-right: 0.3em; } #write h4::before { content: counter(h1) . counter(h2) . counter(h3) . counter(h4) ; padding-right: 0.3em; }两个细节值得说。第一编号和标题文字之间加padding-right而不是在content里塞空格因为连续空格在部分渲染场景下会被压缩伪元素上加内边距是确定生效的。第二如果你只想到三级把h4相关规则删掉就行但不要删掉 h3 上的counter-reset: h4如果保留 h4 规则的话否则四级标题会串到上一章的计数里。写完之后打开一份长文档看一眼一级标题是不是从 1 开始二级是不是在每个一级下重新数。如果看到0.1这种编号说明这段内容前没有出现更高级标题计数器还是初始值属于正常现象而不是 bug。3.3 编号为什么会带着颜色和源码模式不见了有些人加完编号发现编号颜色跟标题文字一样想让编号浅一点。这里只能用::before的独立样式#write h2::before { color: #9aa5b1; font-weight: 400; }至于源码模式Typora 切到源码模式后正文区换成了另一套容器你在#write下写的所有伪元素规则都不再命中编号自然消失。这不是故障是渲染路径不同。我一般把编号是否正常的判断标准定为实时预览模式下观察导出 PDF 之前再抽查一遍。提示写在代码块或者引用块里的#不会被算成标题也就不会参与计数。所以你在文档里放 Bash 注释不会把编号搞乱这一点可以放心。4. 让大纲也带编号两条路线和它们的翻车点4.1 路线一给大纲节点套计数器思路很简单——把#write换成大纲面板里的容器把h1/h2换成大纲节点的选择器。难点在于大纲的 DOM 有两种常见形态写法完全不同。嵌套形态每个子节点物理上包在父节点里这种情况最省事直接用counters()一把梭.outline-content { counter-reset: sec; } .outline-item { counter-increment: sec; } .outline-item::before { content: counters(sec, .) ; color: #9aa5b1; }counters(sec, .)会自动把各层级的当前值用点连起来一级出1二级出1.1非常优雅。扁平形态所有节点是平级排列只靠类名或属性区分层级。这时候要手工用reset 造作用域的技巧.outline-content { counter-reset: ol1; } .outline-item[data-level1] { counter-reset: ol2; counter-increment: ol1; } .outline-item[data-level2] { counter-increment: ol2; } .outline-item[data-level1] .outline-label::before { content: counter(ol1) . ; } .outline-item[data-level2] .outline-label::before { content: counter(ol1) . counter(ol2) ; }注意上面第二段代码里的>#write h1::before { content: counter(h1, upper-roman) . ; /* I. II. III. */ } #write h2::before { content: counter(h2, upper-alpha) . ; /* A. B. C. */ }可用的内置样式里写中文文档最常用的是cjk-ideographic它会输出一、二、三……十、十一这种形态比自己在 CSS 里拼符号表可靠得多。另外decimal-leading-zero能输出01、02这种补零格式做技术文档的章节号挺常见。要提醒的是cjk-ideographic在 10 以内和 10 以上的表现都符合直觉但如果你需要二十三十这类不带一十的写法它默认就是你想要的结果不用额外处理。真正需要额外处理的是百位以上的大数字不过一份文档很少写到一百章不用为它操心。5.2 用 counter-style 造自己的编号体系想同时控制数字形态、后缀和补零位数就得自定义计数器样式。最实用的是中文序号加顿号后缀counter-style cn-chapter { system: additive; additive-symbols: 1000 千, 100 百, 10 十, 9 九, 8 八, 7 七, 6 六, 5 五, 4 四, 3 三, 2 二, 1 一; suffix: 、; range: 1 infinite; } #write h1::before { content: counter(h1, cn-chapter); /* 一、 二、 三、 */ }additive是加法系统适合中文这类十位个位拼接的编号定义好之后counter(h1, cn-chapter)会输出一、十一、二十三、。另一个高频需求是补零到固定位数用pad描述符counter-style pad3 { system: numeric; symbols: 0 1 2 3 4 5 6 7 8 9; pad: 3 0; }配合content: counter(h1, pad3) 就能得到001、002。需要注意的是counter-style依赖浏览器内核对 CSS Counter Styles 的支持Typora 的新版本都没问题如果你用的是好几年前的旧版可能不认退路就是回到cjk-ideographic这类内置样式。5.3 常用格式配方表下面这张表是我自己反复用到的直接抄一列就能用只需要把#write h1::before这层壳套上去。想要的效果content 写法1.counter(h1) . 1.1counter(h1) . counter(h2) 第一章第 counter(h1, cjk-ideographic) 章 第 1 章第 counter(h1) 章 一 counter(h2, cjk-ideographic) 一、counter(h1, cn-chapter)需自定义样式I.counter(h1, upper-roman) . A.counter(h1, upper-alpha) . 01counter(h1, decimal-leading-zero)第1节第 counter(h3) 节 混搭格式完全可行比如一级用第 1 章、二级用一、三级用1.1.1只要各级的counter-reset关系没断CSS 不会觉得别扭。但格式换得越勤读者的心智负担越大我在正式文档里最多用两种形态章级用第 N 章其余全部用数字点分。注意如果你换的是第 X 章这类带固定文字的格式记得检查一下标题本身有没有已经手写了第 1 章。两边都写就会出现第 1 章 第 1 章 环境准备这个错我犯过一次排查了半天。5.4 有序列表的编号格式顺手一起改既然在改编号格式文档里的有序列表也可以统一。Typora 里有序列表的标记是::marker改起来比标题更直接#write ol li::marker { content: counter(list-item, cjk-ideographic) 、; color: #6b7280; }list-item是浏览器内置的列表计数器不需要自己 reset。改完之后正文里的1. 2. 3.会变成一、二、三、和标题的中文编号放一起风格就统一了。不过列表编号一般不出现在大纲和目录里改它属于纯视觉收益优先级可以放低。6. 目录 [TOC] 的编号、缩进与跳转问题6.1 [TOC] 生成机制决定了它取不到 CSS 编号在文档里单独写一行[TOC]Typora 会在那个位置渲染出一个可点击的目录列表。关键点是这个列表的内容来自标题的纯文本你在正文里用::before画出来的编号不属于标题文本所以目录里看不到。这一点和侧边栏大纲完全一致。知道这个机制之后策略就很清楚了。如果你要目录里也带编号只有两条路要么用 CSS 给目录的锚点条目单独做计数器要么在标题里手写编号。前者在扁平结构上很容易出现一级不重置、二级从上一章的末尾接着数的错位后者虽然笨但永远对。我一般在正式文档里用后者在随手写的笔记里用前者博个好看。6.2 给目录做计数器的一次尝试与它的边界如果你想试形状大致是这样.md-toc-content { counter-reset: toc1; } .md-toc-h1 { counter-reset: toc2; counter-increment: toc1; } .md-toc-h2 { counter-increment: toc2; } .md-toc-h1 .md-toc-inner::before { content: counter(toc1) . ; } .md-toc-h2 .md-toc-inner::before { content: counter(toc1) . counter(toc2) ; }写之前一定要先说清楚上面这些类名.md-toc-content、.md-toc-inner、.md-toc-h1这类在不同 Typora 版本里是否一致需要你自己确认。确认方式我推荐导出 HTML 之后在浏览器里翻目录渲染成什么结构一目了然比猜快得多。另外一个小坑[TOC]是文档打开时动态生成的改完 CSS 之后如果目录没变化先把文档关掉重新打开或者切换一次主题强制重载很多改了没反应其实是没刷新。6.3 导出场景下目录表现不一致的处理这里有个必须提前知道的现实同一份文档屏幕上看和导出 PDF 看目录的表现可能不一样。屏幕上的[TOC]是一套锚点链接导出 PDF 时它会被排版成带页码的目录页码是排版引擎算出来的跟你在 CSS 里设的缩进、颜色、编号都不完全是一回事。所以我给自己定了两条规矩。第一条导出之前先导一小份试水别等四十页导完了才发现目录页码对不上。第二条不要把编号的准确性押在目录上。目录的作用是让人快速跳转页码和层级清楚就够了到底这一节是 3.2 还是 3.3靠正文标题上的编号来确认。这两件事分开之后你对目录的期待会合理很多也就不用在它身上耗太多时间。7. 排查链路实录CSS 改了没反应时按这个顺序查7.1 先确认规则有没有被加载而不是先改代码CSS 没生效九成不是语法问题而是根本没被加载。按这个顺序走第一步回到偏好设置 → 外观 → 打开主题文件夹确认你的.css文件确实在这个目录里扩展名是.css而不是.css.txtWindows 上隐藏扩展名时这个错特别常见。第二步打开主题菜单确认你新建的主题名出现在列表里。如果没出现说明文件没被识别检查文件名和编码纯 ASCII 文件名加 UTF-8 编码最保险。第三步在主题菜单里切到别的主题再切回你的主题强制重载一次。第四步随便改一个极显眼的规则试水比如给#write h1加个color: red。如果连红色的标题都看不到那就不是编号的问题是加载的问题回到前三步。这四步走完绝大多数没反应都能定位。我最常遇到的是第二步和第三步——文件放对位置了但菜单里没刷新出来切一下就出现了。7.2 优先级冲突什么时候该用 !important规则加载正常但被覆盖表现是开发者工具里规则上有一条删除线。最常见的原因是主题里已经给标题写过::before比如某些论文风格的主题自带编号你再加一层就变成1.1 1.1 环境准备。处理办法有两个。一是提高选择器权重比如从#write h2::before升到#write.write h2::before前提是容器上同时有这两个类这比!important干净因为不会把其他规则的正常覆盖关系也一起破坏。二是干脆换主题切回内置的普通主题编号立刻干净说明冲突来源就是原主题。!important不是不能用但要限定范围。我一般只在确认是主题硬写、且我不想改主题文件的时候用一次用完在注释里写清楚为什么加免得三个月后自己看到都懵。7.3 升级或换主题之后编号乱掉的恢复办法Typora 升级会覆盖官方主题文件换主题则会换掉整套变量和选择器。这两件事发生后编号最容易出的问题是重复编号和层级错位。恢复流程我固定这么走先把自定义样式单独放在一个文件里不要混进复制来的主题文件里逐行改这样升级时只要重新挑一次主题、把样式文件的内容补进去就行。其次恢复之后立刻跑一遍第 4.3 节的五条自查清单重点看第二条二级是否重新从 1 开始和第三条插删标题后是否自动跟变。最后如果原主题确实做了编号而你又想保留主题的配色可以在自定义样式里先把它原有的::before规则覆盖掉再写自己的两段代码尽量挨着放方便以后一起看。8. 一套可以长期用的配置实践8.1 文件组织与备份策略我现在的工作方式是三份东西各就各位一份官方主题副本作为视觉底子一个单独的自定义样式文件放编号规则和侧边栏微调一份改动记录写在文件顶部的注释里记清楚每条规则是干什么的、哪天加的。目录结构大致是themes/ ├── github-mine.css # 从官方主题复制来的底子只改配色 └── my-numbering.css # 编号规则独立维护为什么不合成一个文件因为配色和编号的修改频率完全不同。配色可能半年不动编号规则经常会因为换了新主题而重写。分开之后重写编号不会碰到配色改动风险小很多。每次改完在注释里写一行2024-xx-xx改成四层编号二级用中文半年后回来还能看懂。8.2 我最终留在配置里的几段一段是正文四层编号用的是第 3 章那套点分格式一段是侧边栏变量微调用的是第 2 章那张表里的几个颜色变量一段是标题编号的浅色样式让编号比正文浅一档视觉上不抢戏#write h1::before, #write h2::before, #write h3::before, #write h4::before { color: #9aa5b1; font-weight: 400; }剩下的全删了。包括那些给大纲做编号、给[TOC]做编号的规则——我试过、用过、也删过。它们不是不能实现而是每次 Typora 更新都要重新验收益又只是看着更整齐。同样的时间花在给文档加一张能说明问题的表格上对读者的帮助更大。8.3 什么时候该放弃 CSS回到手写编号判断标准很简单看你的文档要不要离开 Typora。如果文档只在这个编辑器里看或者导出成 PDF 就完事那纯 CSS 方案是最优解Markdown 原文干净插入删除标题自动重排最省心。如果文档要复制到别的地方——比如贴进在线文档、发给同事用别的编辑器打开、发布到静态站点——那 CSS 编号会全部消失别人看到的是没有编号的一堆标题。这时候标题里手写编号反而是更负责的做法虽然改起来烦但它跟着文本走。我自己是把笔记类和交付类分开处理的笔记用 CSS 自动编号交付类的手写编号。混在一起才是真正的灾难因为你会记得自己用了自动编号却在交付之后才发现编号没跟过去。最后再分享一个小技巧如果你已经写了手写编号又想要 CSS 的美观样式可以在::before里用content: counter(h1) .之外的方式直接给标题加一点左边距和浅色分隔线来强化层级感。编号这件事本身做到读的人不会迷路就足够了没必要做到每一层都有编号。