文章内容目录(Table of Contents)就是点开这篇文章看到的这个有序列表,索引了这篇文章的标题以及体现层级关系。
Typecho 原生的 Markdown 解析器是不支持 [toc] 的,试了一些插件觉得效果不大理想。所以参考 ContentIndex 插件,自己尝试简易实现了一下。
基本思路与实现
原理很简单,查找所有 <h1>
到 <h6>
的标签建立锚。根据层级关系建立列表和链接,在文章开头输出链接列表。
函数定义
在主题的 functions.php
里定义一个 exContent()
函数,负责对文章的输出内容进行加工。
把 post.php
和 page.php
里输出内容的语句 <?php $this->content(); ?>
改成 <?php echo exContent($this->content); ?>
。就可以让这个函数加工文章的输出内容。
正则表达式匹配
正则表达式太难了,我也不会。不过查找 h
标签是有这样一个现成的表达式的:
preg_match_all('/<h(\d)>(.*)<\/h\d>/isU', $content, $outarr)
$content
就是要查找的对象,$outarr
是以数组形式返回的结果。根据尝试发现 根据 preg_match_all()
这个函数的用法,outarr[0]
存储完整的匹配(包含标签和内容),outarr[1]
存储标签的层级号,outarr[2]
存储标签中的内容。
例如,如果文章的第一个小标题是 <h2>hello</h2>
,则 outarr[0][0]
为 <h2>hello</h2>
,outarr[1][0]
为 2,outarr[2][0]
为 hello
。
添加锚
检索 $content
,对每个标题依次添加 id
为 toc_titleX
,X 表示第 i 个小标题。
标题原本是 <h1>content</h1>
,要变成 <h1 id="toc_title0">content</h1>
。代码如下:
$level = $outarr[1][$key];
$content = substr($ta, 0, $tb). "<h{$level} id=\"toc_title{$key}\">{$outarr[2][$key]}</h{$level}>". substr($ta, strlen($outarr[0][$key])+$tb);
建立层级关系
这个实现起来比较简单,像是一个树状结构,记录当前 level
每次比较即可。
层级深入就一直嵌套 <ol>
(或者 <ul>
),层级出就加结束标签。
由于我个人的习惯是用 <h2>
作为最大的小标题(因为文章的大标题是 <h1>
,混用就会比较混乱……),所以最开始要检索一个 $minlevel
,将 $minlevel
作为根深度而非 0,否则如果只用 <h2>
小标题的话就会默认外面套一层 <ol/ul>
。
平滑滚动
默认是点击链接直接跳转。如何让页面优雅地滑过去呢?
不需要 jQuery,CSS 提供了新的 scroll-behavior 属性:
html{
scroll-behavior: smooth;
}
加上这个就可以让链接锚平滑跳转。参见文档。
遗憾的是,Safari 不支持。想不到啊 😮💨
伪锚点实现链接偏移
这样实现还有一个问题,那就是我的网站是有一个 sticky-top
的 navbar 的。跳转之后目标位置会在页面顶端,会被 navbar 完美挡住。我们需要让跳转的位置向上偏移。
不需要 jQuery,可以用伪锚点来实现。不对标题标签设置锚点,而是在每个标题标签之前添加一个不可见的伪锚点并设置 position:relative; top:-50px
。
$content = substr($ta, 0, $tb). "<a id=\"toc_title{$key}\" style=\"position:relative; top:-50px\"></a>". substr($ta, $tb);
这个 Safari 还是不支持。想不到吧 😮💨
代码实现
完整的函数实现如下:
if (preg_match_all('/<h(\d)>(.*)<\/h\d>/isU', $content, $outarr)){
$toc_out = "";
$minlevel = 6;
for ($key=0; $key<count($outarr[2]); $key++) $minlevel = min($minlevel, $outarr[1][$key]);
$curlevel = $minlevel-1;
for ($key=0; $key<count($outarr[2]); $key++) {
$ta = $content;
$tb = strpos($ta, $outarr[0][$key]);
$level = $outarr[1][$key];
$content = substr($ta, 0, $tb). "<a id=\"toc_title{$key}\" style=\"position:relative; top:-50px\"></a>". substr($ta, $tb);
if ($level > $curlevel) $toc_out.=str_repeat("<ol>\n", $level-$curlevel);
elseif ($level < $curlevel) $toc_out.=str_repeat("</ol>\n", $curlevel-$level);
$curlevel = $level;
$toc_out .= "<li><a href=\"#toc_title{$key}\">{$outarr[2][$key]}</a></li>\n";
}
$content = "<div id=\"tableOfContents\">{$toc_out}</div>". $content;
}