基于 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
是传引用的目录数据,它是一个数组结构,每个元素包含了:
- id:小标题的 id
- level:缩进级别,比如 h2
- title: 小标题的文字内容
传引用的原理和 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();
});
总结
目录效果除了体验好,对搜索引擎的优化亦有帮助。但在移动端还有改进空间。
文章评论:做一头严肃的大叫驴
根据过去的经验得出,大多数评论是毫无意义的灌水,还有一小部分内容是针对文章的补充和纠错。如果你有建议请邮件联系。