醉丶春风的Blog

千里之行, 始于足下



php为markdown/html内容生成自定义目录


准备

本文功能只提取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>

我们希望通过文章内容生成一个如下结构的目录:

第一个标题
  第二个标题
    第三个标题
一级标题
一级标题
  二级标题
    三级标题

分析

  1. 首先要正则提取出h标签
  2. 对提取出的标签进行处理, 生成一个简易的数组
  3. 对数组进行处理, 得到树结构的数组
  4. 通过树结构数组渲染出html
  5. 为文章内容的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 和最后的替换规则即可


作者: 徐善通
地址: https://xstnet.com/article-128.html
声明: 除非本文有注明出处,否则转载请注明本文地址


我有话说



最新回复


正在加载中....

Copyrights © 2016-2019 醉丶春风 , All rights reserved. 皖ICP备15015582号-1