文章内容目录(Table of Contents)就是点开这篇文章看到的这个有序列表,索引了这篇文章的标题以及体现层级关系。

Typecho 原生的 Markdown 解析器是不支持 [toc] 的,试了一些插件觉得效果不大理想。所以参考 ContentIndex 插件,自己尝试简易实现了一下。

基本思路与实现

原理很简单,查找所有 <h1><h6> 的标签建立锚。根据层级关系建立列表和链接,在文章开头输出链接列表。

函数定义

在主题的 functions.php 里定义一个 exContent() 函数,负责对文章的输出内容进行加工。
post.phppage.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,对每个标题依次添加 idtoc_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;
}