基于 PHP Markdown 的 Blog 目录(TOC)功能

起源

现在大家可以看到文章已经启用了目录功能,自动提取文章中的小标题,并能够链接到指定文章位置,功能的实现如下文所示。

TOC 的全称是 Table of contents

Markdown 的标题分析

Markdown 转 HTML 后可以使用 PHP 完善的 Dom 功能,所以首先转HTML,然后获取对应的小标题。

<?php
function parseToc($html, &$toc)
{
    $encoding = '<?xml encoding="utf-8" ?'.'>';
    $doc = new \DOMDocument();
    $doc->loadHTML($encoding.$html);
    $xml = simplexml_import_dom($doc);
    $headings = $xml->xpath('//h1|//h2|//h3|//h4|//h5|//h6');
    $toc = [];
    foreach($headings as $i=>$heading) {
        $level = $heading->getName();
        $id = "toc-{$i}";
        $heading->addAttribute('id', $id);
        $toc[] = [
            'id'=>$id,
            'level'=>$level,
            'title'=>strval($heading)
        ];
    }
    $resultHtml = '';
    foreach($xml->body->children() as $child) {
        $resultHtml .= $child->asXML();
    }
    return $resultHtml;
}

参数

第一个 $html 是文章的 HTML
第二个参数 $toc 是传引用的目录数据,它是一个数组结构,每个元素包含了:

传引用的原理和 preg_match 的 $result 参数类似。函数调用完成时,$result 就包含了结果数据。

返回值

修改添加过 id 的文章数据

前端部分

HTML 结构:模板输出目录

<?php if(!empty($toc)): ?>
<aside id="toc" class="toc">
    <h3>目录</h3>
    <?php foreach($toc as $item): ?>
    <a href="#<?=$item['id']?>" class="level-<?=$item['level']?>"><?=$item['title']?></a>
    <?php endforeach; ?>
</aside>
<?php endif; ?>

CSS 部分

aside{
    position: fixed;
    top:0;
    right: 0;
}
aside a{
    display: block;
    font-size: 14px;
    padding:5px;
    text-decoration: none;
}
aside {
    position: fixed;
    width: 340px;
    top: 0;
    right: 0;
    overflow: auto;
    height: 100%;
    border-left: 1px solid #e1e4e8;
    box-sizing: border-box;
    z-index: 99;
    background-color: #fff;
}
.level-h1{
    padding-left: .5em;
}
.level-h2{
    padding-left: 2em;
}
.level-h3{
    padding-left: 3em;
}
.level-h4{
    padding-left: 4em;
}
.level-h5{
    padding-left: 5em;
}
.level-h6{
    padding-left: 6em;
}
.toc a{
    display: block;
    border-left:solid transparent 2px;
}
.toc h3{
    font-weight: 300;
    margin: .4em 0;
    padding-left: 1em;
}
.toc a.active {
    background-color: #fafafa;
    color: #e1830a;
    border-left-color:#fec323;
}

JavaScript:实时高亮当前段落(需要 jQuery库)

$(function(){
    var timer;
    var headings = $('#app > article').find('h1,h2,h3,h4,h5,h6');
    // 自动更新当前网址
    var updateLocation = function(id) {
        history.replaceState(null, null, "#"+id);
    };
      //添加链接的高亮效果
    var updateCurrentToc = function(matchedId) {
        $('#toc a').removeClass('active');
        $('#toc a[href="#'+matchedId+'"]').addClass('active');
    };
    // 搜索当前的段落
    var searchCurrentToc = function() {
        if(headings.length == 0) {
            return;
        }
        var top = $(document).scrollTop();
        var lastMatchId;
        var relativeHeight = 50;
        for(var i=0;i<headings.length;i++) {
            var heading = $(headings[i]);
            var offset = heading.offset();
               // 这里的 i > 0 使得至少匹配了第一个段落
            if(i > 0 && (offset['top'] - relativeHeight) >= top) {
                break;
            } else {
                lastMatchId = heading.attr('id');
            }
        }
        return lastMatchId;
    };
    // 打开网页就有高亮效果
    var initToc = function() {
        $(window).scroll();
    };
    $(window).scroll(function(){
        //这里使用定时器可以减少 searchCurrentToc 的调用次数,更低碳环保
        if(timer) {
            clearTimeout(timer);
        }
        timer = setTimeout(function(){
            var matchedId = searchCurrentToc();
            if(!matchedId) {
                return;
            }
            updateCurrentToc(matchedId);
            updateLocation(matchedId);
        }, 100);
    });
    initToc();
});

总结

目录效果除了体验好,对搜索引擎的优化亦有帮助。但在移动端还有改进空间。

参考资料

文章评论:做一头严肃的大叫驴

根据过去的经验得出,大多数评论是毫无意义的灌水,还有一小部分内容是针对文章的补充和纠错。如果你有建议请邮件联系,我将视反馈的重要程度和自己当月的经济水平发红包奖励。当然,也不一定会回复,这取决于邮件的内容质量。