准备
本文功能只提取h[1,2,3,4,5,6]标签, 把这些标签当做目录, 并根据不同的等级生成目录树
最大等级以第一个h标签
为准
提取html内容目录
假如我们有一篇文章, 内容包含h标签
:
<body>
<h1>第一个标题</h1>
<div>....</div>
<h2>第二个标题</h2>
<div>....</div>
<h2>第三个标题</h2>
<div>....</div>
<h1>一级标题</h1>
<div>....</div>
<h1>一级标题</h1>
<div>....</div>
<h2>二级标题</h2>
<div>....</div>
<h3>三级标题</h3>
<div>....</div>
</body>
我们希望通过文章内容生成一个如下结构的目录:
第一个标题
第二个标题
第三个标题
一级标题
一级标题
二级标题
三级标题
分析
- 首先要正则提取出
h标签
- 对提取出的标签进行处理, 生成一个简易的数组
- 对数组进行处理, 得到树结构的数组
- 通过树结构数组渲染出html
- 为文章内容的h标签加上锚点
正则提取出 h标签
// $content = 上面的html
$pattern = '#<h(\d)(.*?)>(.*?)</h\d>#im';
preg_match_all($pattern, $content, $match);
print_r($match);
// 打印匹配结果如下
/**
Array
(
[0] => Array
(
[0] => <h1>第一个标题</h1>
[1] => <h2>第二个标题</h2>
[2] => <h3>第三个标题</h3>
[3] => <h1>一级标题</h1>
[4] => <h1>一级标题</h1>
[5] => <h2>二级标题</h2>
[6] => <h3>三级标题</h3>
)
[1] => Array
(
[0] => 1
[1] => 2
[2] => 3
[3] => 1
[4] => 1
[5] => 2
[6] => 3
)
[2] => Array
(
[0] =>
[1] =>
[2] =>
[3] =>
[4] =>
[5] =>
[6] =>
)
[3] => Array
(
[0] => 第一个标题
[1] => 第二个标题
[2] => 第三个标题
[3] => 一级标题
[4] => 一级标题
[5] => 二级标题
[6] => 三级标题
)
)
*/
对提取出的标签进行处理, 生成一个简易的数组
$treeList = [];
$id = 1; // 对匹配到的每一项定义一个ID, 从1开始
foreach ($match[1] as $i => $level) {
// id => item
$treeList[$id] = [
'id' => $id,
'parent_id' => 0, // 上下级关系, 默认都为一级目录
'level' => $level, // 等级, 对应 h [1,2,3,4,5,6]
'name' => trim(strip_tags($match[3][$i])), // 标题名字
];
$id ++;
}
/**
* 按顺序处理目录对应关系, 第一个元素默认一级目录, 第二个元素开始遍历
* 每个元素和上一个元素的level进行比较, 分三种情况
* case 1: 当 当前的level == 上个元素的level, 说明该元素和上个元素同级, 将当前元素的parent_id=上一个元素的parent_id
* case 2: 当 当前的level > 上个元素的level, 说明该元素属于上个元素, 将当前元素的parent_id指向上一个元素的id
* case 3: 当 当前的level < 上个元素的level, 则往上查找上个元素, 直到找到的上个元素的level等于当前的level时, parent_id=找到的元素的parent_id
* 如果往上没有找到, 则给予默认的一级目录
*
*/
for ($i=2; $i<=count($treeList); $i++) {
$item = $treeList[$i];
$prevItem = $treeList[$i-1];
// case 1
if ($item['level'] == $prevItem['level']) {
$treeList[$i]['parent_id'] = $prevItem['parent_id'];
continue;
}
// case 2
if ($item['level'] > $prevItem['level']) {
$treeList[$i]['parent_id'] = $prevItem['id'];
continue;
}
$parentId = 0;
// case 3
while ($item['level'] <= $prevItem['level']) {
$parentId = $prevItem['parent_id'];
if (!isset($treeList[($prevItem['id'] - 1)])) {
break;
}
$prevItem = $treeList[($prevItem['id'] - 1)];
}
$treeList[$i]['parent_id'] = $parentId;
}
print_r($treeList);
/**
Array
(
[1] => Array
(
[id] => 1
[parent_id] => 0
[level] => 1
[name] => 第一个标题
)
[2] => Array
(
[id] => 2
[parent_id] => 1
[level] => 2
[name] => 第二个标题
)
[3] => Array
(
[id] => 3
[parent_id] => 2
[level] => 3
[name] => 第三个标题
)
[4] => Array
(
[id] => 4
[parent_id] => 0
[level] => 1
[name] => 一级标题
)
[5] => Array
(
[id] => 5
[parent_id] => 0
[level] => 1
[name] => 一级标题
)
[6] => Array
(
[id] => 6
[parent_id] => 5
[level] => 2
[name] => 二级标题
)
[7] => Array
(
[id] => 7
[parent_id] => 6
[level] => 3
[name] => 三级标题
)
)
*/
对数组进行处理, 得到树结构的数组
到这一步其实就已经完成了, 接下来就是渲染html了
/**
* @Desc: 获取分类树
* @param array $list 列表数据 id => data
* @return array
*/
function getTree($list)
{
$tree = []; //格式化好的树 id => array
foreach ($list as $item) {
// 不是一级分类,父级分类不存在,跳过
if ($item[ 'parent_id' ] != 0 && !isset($list[ $item[ 'parent_id' ] ])) {
continue;
}
if (isset($list[$item[ 'parent_id' ]])) {
$list[$item[ 'parent_id' ]][ 'children' ][] = &$list[$item['id']];
} else {
$tree[] = &$list[$item['id']];
}
}
return $tree;
}
$treeList = getTree($treeList);
print_r($treeList);
/**
Array
(
[0] => Array
(
[id] => 1
[parent_id] => 0
[level] => 1
[name] => 第一个标题
[children] => Array
(
[0] => Array
(
[id] => 2
[parent_id] => 1
[level] => 2
[name] => 第二个标题
[children] => Array
(
[0] => Array
(
[id] => 3
[parent_id] => 2
[level] => 3
[name] => 第三个标题
)
)
)
)
)
[1] => Array
(
[id] => 4
[parent_id] => 0
[level] => 1
[name] => 一级标题
)
[2] => Array
(
[id] => 5
[parent_id] => 0
[level] => 1
[name] => 一级标题
[children] => Array
(
[0] => Array
(
[id] => 6
[parent_id] => 5
[level] => 2
[name] => 二级标题
[children] => Array
(
[0] => Array
(
[id] => 7
[parent_id] => 6
[level] => 3
[name] => 三级标题
)
)
)
)
)
)
*/
通过树结构数组渲染出html
这里提供一个我自己用的方法
function renderArticleDirectory($tree)
{
$htmlStr = '<ul class="article-directory">';
foreach ($tree as $item) {
$htmlStr .= sprintf('<li><a href="#%s">%s</a></li>', $item['name'], $item['name']);
if (isset($item['children']) && count($item['children']) > 0) {
$htmlStr .= self::renderArticleDirectory($item['children']);
}
}
$htmlStr .= '</ul>';
return $htmlStr;
}
$directoryHtml = renderArticleDirectory($treeList);
为文章内容的h标签加上锚点
这里其实就是正则替换html,为h标签加上id和name, 做一个锚点, 使用php的 preg_replace
即可完成
这里是把name和id都赋值成了标题名称, 如果有其他要求的话, 修改干替换规则即可
$content = preg_replace($pattern, '<h${1} name="${3}" id="${3}">${3}</h${1}>', $content);
推荐使用
推荐使用回调函数的方式, 可以去除锚点的标签
虽然preg_replace
可以加上/e
的修饰符来达到同样的作用, 但是会造成漏洞, 见: https://www.php.net/manual/zh/reference.pcre.pattern.modifiers.php
如果想使用回调函数来控制的话, 可以使用preg_replace_callback
$content = preg_replace_callback(
$pattern,
function ($m) {
$name = trim(strip_tags($m[3]));
return "<h$m[1] name='$name' id='$name'>" . $m[3] . "</h$m[1]>";
},
$content
);
提取markdown内容目录
参考html的流程, 只要修改一下正则规则$pattern
和最后的替换规则即可