今年早些时候,我自出版了一本名为《理解 JavaScript Promises》的电子书 (免费下载)。尽管我无意将其制作成纸质书,但还是有很多人询问纸质版本,于是我决定也自出版纸质版本。我认为使用 HTML 和 CSS 生成 PDF 并发送给印刷商将是一项简单的任务。我当时没有意识到,我没有解决纸质书的一个重要部分:目录。
目录的构成
从本质上讲,目录非常简单。每一行代表书籍或网页的一部分,并指示您可以在哪里找到该内容。通常,这些行包含三个部分
- 章节或部分的标题
- 引导符(即那些点、破折号或线),它们在视觉上将标题连接到页码
- 页码
目录很容易在 Microsoft Word 或 Google Docs 等文字处理工具中生成,但由于我的内容是 Markdown 格式并转换成了 HTML,因此对我来说这不是一个好的选择。我想要一种自动化的方式,可以使用 HTML 生成适合打印的目录格式。我还希望每一行都是链接,以便它可以在网页和 PDF 中用于在文档中导航。我还希望标题和页码之间有引导符。
因此我开始研究。
我偶然发现了关于使用 HTML 和 CSS 创建目录的两个优秀博文。第一篇是 Julie Blanc 的 “从您的 HTML 中构建目录”。Julie 曾在 PagedJS 工作,PagedJS 是一个针对网页浏览器中缺少的分页媒体功能的 polyfill,它可以适当地格式化打印文档。我从 Julie 的示例开始,但发现它并不适合我。接下来,我找到了 Christoph Grabo 的 “使用 CSS 创建响应式目录引导符” 文章,这篇文章介绍了使用 CSS Grid(而不是 Julie 的基于浮动的方案)来简化对齐方式的概念。但同样,他的方案也不完全适合我的目的。
不过,阅读了这两篇文章后,我感觉自己对布局问题有了足够的了解,可以开始自己尝试了。我借鉴了这两篇博文的片段,并添加了一些新的 HTML 和 CSS 概念到方案中,最终得出了一个令我满意的结果。
选择正确的标记
在决定目录的正确标记时,我主要考虑了正确的语义。从根本上讲,目录是关于标题(章节或小节)与页码的绑定,几乎就像一个键值对。这让我想到了两种选择
- 一种选择是使用表格 (
<table>
),一列用于标题,一列用于页码。 - 然后是经常被遗忘的定义列表 (
<dl>
) 元素。它也充当键值映射。因此,标题和页码之间的关系将再次变得显而易见。
这两种方案似乎都是不错的选择,直到我意识到它们实际上只适用于单级目录,也就是说,如果我只想要一个包含章节名称的目录,那么它们就能发挥作用。但是,如果我想在目录中显示小节,那么我就没有好的选择。表格元素不适合分层数据,虽然定义列表在技术上可以嵌套,但语义似乎并不正确。因此,我重新开始思考。
我决定借鉴 Julie 的方案并使用列表;但是,我选择了有序列表 (<ol>
) 而不是无序列表 (<ul>
)。我认为有序列表在这种情况下更合适。目录代表章节和小标题的列表,按照它们在内容中出现的顺序排列。顺序很重要,不应该在标记中丢失。
不幸的是,使用有序列表意味着丢失了标题和页码之间的语义关系,因此我的下一步是重新建立列表项中的这种关系。解决此问题的最简单方法是在页码之前简单地插入“页”字。这样,即使没有其他视觉区分,数字相对于文本的关系也是明确的。
以下是一个简单的 HTML 骨架,它构成了我的标记的基础
<ol class="toc-list">
<li>
<a href="#link_to_heading">
<span class="title">Chapter or subsection title</span>
<span class="page">Page 1</span>
</a>
<ol>
<!-- subsection items -->
</ol>
</li>
</ol>
将样式应用于目录
确定了要使用的标记后,下一步是应用一些样式。
首先,我删除了自动生成的数字。如果您愿意,可以选择在自己的项目中保留自动生成的数字,但书籍通常会在章节列表中包含无编号的前言和后记,这使得自动生成的数字不正确。
出于我的目的,我会手动填写章节编号,然后调整布局,使顶级列表没有任何填充(因此与段落对齐),并且每个嵌套列表缩进两个空格。我选择使用 2ch
填充值,因为我还不确定要使用哪种字体。ch
长度单位允许填充相对于字符宽度进行调整——无论使用哪种字体——而不是可能导致外观不一致的绝对像素大小。
以下是我最终使用的 CSS 代码
.toc-list, .toc-list ol {
list-style-type: none;
}
.toc-list {
padding: 0;
}
.toc-list ol {
padding-inline-start: 2ch;
}
Sara Soueidan 指出,WebKit 浏览器在 list-style-type
为 none
时会删除列表语义,因此我需要在 HTML 中添加 role="list"
来保留它
<ol class="toc-list" role="list">
<li>
<a href="#link_to_heading">
<span class="title">Chapter or subsection title</span>
<span class="page">Page 1</span>
</a>
<ol role="list">
<!-- subsection items -->
</ol>
</li>
</ol>
设置标题和页码的样式
对列表进行样式设置后,是时候继续设置单个列表项的样式了。对于目录中的每个项目,标题和页码都必须在同一行上,标题位于左侧,页码与右侧对齐。
您可能会想,“没问题,这就是 flexbox 的作用!”您说得没错!Flexbox 确实可以实现正确的标题-页码对齐方式。但是,在添加引导符时,存在一些棘手的对齐问题,因此我选择使用 Christoph 的方案,使用网格,它也有助于处理多行标题。以下是对单个项目设置样式的 CSS 代码
.toc-list li > a {
text-decoration: none;
display: grid;
grid-template-columns: auto max-content;
align-items: end;
}
.toc-list li > a > .page {
text-align: right;
}
网格有两列,第一列是 auto
大小,用于填充容器的整个宽度,减去第二列,第二列的大小为 max-content
。页码与右侧对齐,这在目录中是传统的。
此时我做的唯一其他更改是隐藏“页”字。这对屏幕阅读器很有帮助,但在视觉上没有必要,因此我使用了一个 传统的 visually-hidden
类 来将其隐藏起来
.visually-hidden {
clip: rect(0 0 0 0);
clip-path: inset(100%);
height: 1px;
overflow: hidden;
position: absolute;
width: 1px;
white-space: nowrap;
}
当然,HTML 也需要更新以使用该类
<ol class="toc-list" role="list">
<li>
<a href="#link_to_heading">
<span class="title">Chapter or subsection title</span>
<span class="page"><span class="visually-hidden">Page</span> 1</span>
</a>
<ol role="list">
<!-- subsection items -->
</ol>
</li>
</ol>
有了这个基础,我继续处理标题和页码之间的引导符。
创建引导符
引导符在印刷媒体中如此常见,您可能会想,为什么 CSS 还不支持它?答案是:**支持,但不是完全支持。**
CSS 生成内容用于分页媒体规范中实际上定义了一个 leader()
函数。但是,与大多数分页媒体规范一样,此函数在任何浏览器中都没有实现,因此将其排除在选项之外(至少在我撰写本文时是如此)。它甚至没有在 caniuse.com 上列出,可能是因为还没有人实现它,并且没有计划或信号表明他们会实现它。
幸运的是,Julie 和 Christoph 已经在各自的文章中解决了这个问题。为了插入引导符,他们都使用了一个 ::after
伪元素,并将它的 content
属性设置为一个非常长的点字符串,例如
.toc-list li > a > .title {
position: relative;
overflow: hidden;
}
.toc-list li > a .title::after {
position: absolute;
padding-left: .25ch;
content: " . . . . . . . . . . . . . . . . . . . "
". . . . . . . . . . . . . . . . . . . . . . . "
". . . . . . . . . . . . . . . . . . . . . . . "
". . . . . . . . . . . . . . . . . . . . . . . "
". . . . . . . . . . . . . . . . . . . . . . . "
". . . . . . . . . . . . . . . . . . . . . . . "
". . . . . . . . . . . . . . . . . . . . . . . ";
text-align: right;
}
::after
伪元素被设置为绝对定位,以将其从页面流中取出并避免换行。文本对齐到右边,因为我们希望每行最后的点与行尾的数字对齐。(稍后将详细介绍此复杂性的更多内容。).title
元素被设置为相对定位,以便::after
伪元素不会超出其框。同时,overflow
被隐藏,所以所有额外的点都不可见。结果是一个带有点引导的漂亮目录。
但是,还需要考虑其他事项。
Sara还向我指出,所有这些点对屏幕阅读器来说都算作文本。那么你听到什么呢?“引言点点点……”直到所有点都被宣布。这对屏幕阅读器用户来说是一种糟糕的体验。
解决方案是插入一个额外的元素,将aria-hidden
设置为true
,然后使用该元素插入点。所以 HTML 代码变为
<ol class="toc-list" role="list">
<li>
<a href="#link_to_heading">
<span class="title">Chapter or subsection title<span class="leaders" aria-hidden="true"></span></span>
<span class="page"><span class="visually-hidden">Page</span> 1</span>
</a>
<ol role="list">
<!-- subsection items -->
</ol>
</li>
</ol>
而 CSS 代码变为
.toc-list li > a > .title {
position: relative;
overflow: hidden;
}
.toc-list li > a .leaders::after {
position: absolute;
padding-left: .25ch;
content: " . . . . . . . . . . . . . . . . . . . "
". . . . . . . . . . . . . . . . . . . . . . . "
". . . . . . . . . . . . . . . . . . . . . . . "
". . . . . . . . . . . . . . . . . . . . . . . "
". . . . . . . . . . . . . . . . . . . . . . . "
". . . . . . . . . . . . . . . . . . . . . . . "
". . . . . . . . . . . . . . . . . . . . . . . ";
text-align: right;
}
现在屏幕阅读器会忽略这些点,从而避免用户因听到多个点被宣布而感到沮丧。
收尾工作
此时,目录组件看起来已经相当不错,但还需要一些细微的调整。首先,大多数书籍在视觉上将章节标题与小节标题区分开来,因此我将顶级项目加粗,并引入了一个边距来将小节与后面的章节隔开。
.toc-list > li > a {
font-weight: bold;
margin-block-start: 1em;
}
接下来,我想清理一下页码的对齐方式。当我使用固定宽度的字体时,一切看起来都很好,但对于可变宽度的字体,引导点可能会形成锯齿状图案,因为它们会根据页码的宽度进行调整。例如,任何页码包含数字 1 的页码都将比其他页码窄,导致引导点与前一行或后一行的点不对齐。

为了解决这个问题,我将font-variant-numeric
设置为tabular-nums
,这样所有数字的宽度都相同。通过还将最小宽度设置为2ch
,我确保了所有包含一位或两位数字的数字都完美对齐。(如果您的项目有超过 100 页,您可能需要将其设置为3ch
。)以下是页码的最终 CSS 代码。
.toc-list li > a > .page {
min-width: 2ch;
font-variant-numeric: tabular-nums;
text-align: right;
}

就这样,目录就完成了!
结论
用纯 HTML 和 CSS 创建目录比我预期的更具挑战性,但我对结果非常满意。这种方法不仅足够灵活,可以容纳章节和小节,而且可以很好地处理子小节,而无需更新 CSS。总体方法适用于您想要链接到内容不同位置的网页,以及您想要目录链接到不同页面的 PDF 文件。当然,如果您想在手册或书籍中使用它,它在打印时也看起来很棒。
我要感谢 Julie Blanc 和 Christoph Grabo 他们撰写的关于创建目录的优秀博文,这两篇博文在我开始时都非常宝贵。我还要感谢 Sara Soueidan 在我进行此项目时提供的无障碍反馈。
我会使用伪元素来简化代码,以创建点。
这样可以避免添加额外的 span 元素。
演示:https://codepen.io/t_afif/pen/gOvXmyK
需要额外的元素,因为它包含了屏幕阅读器要排除的内容,而
.title
的内容仍然需要读出来。如果您想避免使用单独的元素,可以为屏幕阅读器指定单独的内容。
斜杠后面的字符串不会显示,但会被屏幕阅读器使用(而不是点)。理论上,您可以删除视觉上隐藏的“页面”元素并在 CSS 中指定它。
但是,在 CSS 中添加文本感觉不太干净,我不知道这在屏幕阅读器和其他工具(如谷歌翻译)中的支持情况如何。所以,我仍然会使用文章中的原始方法。
据我所知,伪元素的内容被排除在可访问性树之外,正是出于这个原因,我使用伪元素来避免使用额外的元素。
它只是为了样式目的,不属于内容。
您使用字符串来生成点是否有特殊原因?我相信您可以执行以下操作
浏览器倾向于以不同的方式呈现点状边框,所以这可能是一个原因,我猜想 :)
是的。使用字符串可以轻松地将点模式与字体匹配并正确对齐。我尝试过使用边框和图像,但结果总是不好。
@Sandro,添加斜杠到生成内容的方法(
content: "......" / ""
)在任何浏览器中都不受支持(https://adrianroselli.com/2020/10/alternative-text-for-css-generated-content.html),屏幕阅读器从浏览器获取提示。/
的存在将使整个声明失效,因此您可以在没有屏幕阅读器的情况下通过查看生成的内容是否显示来测试这一点。@Temani,CSS 生成的内容/伪元素自 2015 年以来一直暴露给屏幕阅读器。您可以在浏览器的可访问性检查器中看到它。
CSS 计数器可以用作一种技巧来生成点字符串
不幸的是,这目前在 Safari 中不起作用。
我想知道是否应该将所有这些内容都包装在一个
<nav>
标签中。我同意,由于数字与您的需求不同,因此失去了
<ol>
的语义性,这很可惜。如果不是因为引言,您本可以在<ol>
上使用start
属性,或者更准确地说是在<li>
上使用value
属性。我一直认为屏幕阅读器不会读出伪元素,这是不一致的行为吗?我认为额外的引导元素是不必要的。
不过 TOC 看起来很棒,干得好!
我也这么认为,但生成的内容绝对会被读出来。您也可以在开发者工具的可访问性树中看到这一点。
我认为这是一个比较新的规范,旧的屏幕阅读器不会读出它,所以现在有点不一致。
@Noam,@Benji,浏览器自 2015 年以来至少将 CSS 生成的内容/伪元素暴露在可访问性树中。屏幕阅读器从浏览器获取内容,所以这与屏幕阅读器支持无关。
在 25% 的缩放比例下会发生什么?
大家好!
您使用什么来生成 PDF 表单 HTML?您会推荐什么好的工具吗?
我编写了一个名为 PrintReady 的工具,我使用它来将 HTML 转换为 PDF。
请您把它做成 WordPress 插件吗?它可以带有不同的样式。
您是如何生成页码的?我希望我的每页都有一个标题……我不确定我应该去哪里查找实现这个方法。
我使用 target-counter() 来生成页码。它在浏览器中没有实现,但您可以在 PagedJS 中使用它。为了简单起见,我在示例中只是硬编码了数字。
感谢您的建议!我正在使用 Paged.js,页码可以正常工作,但当我尝试创建 TOC 时,页码总是为 0。
我在我的打印样式中设置了如下内容
在我的 cshtml 代码中,我有以下内容
目标子标题的 href # 和 id # 都是相同的……我不确定还能检查什么。有什么想法吗?
我不确定,但请仔细检查您的 ID 中是否包含任何特殊字符。早期我遇到过很多奇怪的行为,结果发现我的某些 ID 包含导致它们无效的字符。坚持使用字母和破折号,看看是否有帮助。
看来这就是诀窍,谢谢!
如果您使用的是针对打印输出而不是浏览器的 CSS 布局引擎,您会发现 leader() 非常有效 - target-counter() 函数也可以自动设置页码。
事实上,对于目录来说,通常只需要类似的东西
这不太可能出现在浏览器中,如果这是您必须使用的方法,您的方法是一个不错的技巧。
我对这篇文章很感兴趣,所以我尝试了一种略微不同的方法
它又快又脏,但似乎效果很好。
好处
1. 使用任何字体(不仅仅是等宽字体)都能获得不错的效果
2. 由于您不必硬编码点的数量,所以点之间不会出现明显的间隙,也不会出现在页码和最右边的点之间。
3. 在屏幕放大倍率发生较大变化时,也能很好地缩放。
4. 允许非常长的标题,并且页码在标题文本的底部对齐。
5. 伪元素不会被屏幕阅读器读取。
6. 项目编号无需手动输入。
注意事项
1. 目录依赖于有序列表的背景颜色为白色,以隐藏出现在标题和页码下的点。
这是一篇很棒的文章。
它勾起了我的回忆。
似乎事物变化越多,它们就越相似。
细节变了。挑战依然存在。
我太固执己见,当我读到“顺序很重要,不应该在标记中丢失”时,我做的第一件事就是寻找一个分享按钮。
顺便说一句(BTW),还有人用记事本或 Microsoft Word 来编写网页内容吗?Word 会自动生成列表编号,从您选择的数字开始。
我正在制作一份在线指南,我创建了一个图像来列出目录。您是否知道我可以将代码放入图像以使每个部分标题都可点击,这样一旦点击,它就会滚动到该部分?