1

本人为Javascript菜鸟,有问题请指正...

TOC目录

最近用Flask搭建个人博客在制作文章显示页面时,需要在前端页面生成TOC。就打算自己写一个JavaScript脚本。

需要生成的目标HTML代码如下:

<ul>
    <li><a href="#TOC1">标题1</a></li>
    <li><a href="#TOC2">标题2</a>
        <ul>
            <li><a href="#TOC2.1">标题2.1</a></li>
            <li><a href="#TOC2.1">标题2.2</a>
                <ul>
                    <li><a href="#TOC2.2.1">标题2.2.1</a></li>
                </ul>
            </li>
        </ul>
    </li>
    ...
</ul>    

在看了JavaScript权威指南这本书后,发现其中虽然有例子生成TOC,但是并没有显示层级。不过,其中有一个记录TOC各个锚点序号的好方法:

# 使用一个数组报错<h1到h6标题出现的次数
var sectionNumbers = [0,0,0,0,0,0]
# 取各级标题的level,如<h1>的level为1
var level = parseInt(header.tagName.charAt(1));
# 当处理一个标签后,计算加1
sectionNumbers[level-1]++
# 然后去当前序号 比如序号为2.1.1
sectionNumber = sectionNumbers.slice(0,level).join('.')

分析算法逻辑

吹牛B之前需要打草稿,写代码之前也如此。

刚开始很纠结如何生成有层级结构的HTML代码,最后发现可以使用设计模式中的组合模式来完成这一功能。具体点就是将li节点看作叶子节点,ul节点看错作容器。那么就有一下限制:

  1. 只有ul节点可以插入li节点

  2. li节点需要插入子节点,就需要创建一个ul子节点,让子节点去插入li节点

  3. ul与它的li子节点level相同

如果当前节点需要处理一个header,就需要作出以下判断:

  1. 判断自己为ul节点还是li节点

  2. 如果为ul节点,且待处理的header的level与自己相同,那么就直接生成一个li节点并插入;如果待处理的header的level比自己大,那么就找到子节点,交给子节点去处理。

  3. 如果为li节点,且待处理的header的level比自己大,那么就取li节点的ul节点,交给ul节点去处理

  4. 一直向下去处理header,直到插入成功

代码实现

为了尽量减少代码的污染,使用Fucntion.call(args)方式了动态的给节点添加属性:

var sectionNumbers = [0, 0, 0, 0, 0, 0];

function Toc() {
    this.headers = $(this).find(':header');
    var ul = document.createElement('ul');
    this.toc = TocObj.call(ul, 1);
    for(var i = 0; i < this.headers.length; i++) {
        // if(i > 1) break;
        this.toc.add(this.headers[i]);
    }
    console.log(this.toc);
}

function TocObj(level) {
    this.level = level;
    this.num = 0;
    this.add = function(header) {
        var flag = this.tagName == 'UL'; // ul节点和li节点处理header的方式不通过
        var level = parseInt(header.tagName.charAt(1));
        if(flag) { // 只有ul节点才能插入li
            if(this.num == 0 || level == this.level) {
                var link = document.createElement('a');
                link.href = '';
                link.innerHTML = header.innerHTML;
                sectionNumbers[level-1]++;
                for(var i = level; i < sectionNumbers.length; i++) sectionNumbers[i] = 0;
                link.href = "#TOC" + sectionNumbers.slice(0, level).join('.');
                var li = document.createElement('li');
                li.insertBefore(link, li.firstChild);
                this.num++;
                this.appendChild(TocObj.call(li, level));
            } else if(level > this.level){
                if(this.num == 0) { 
                    throw new Error("level error 1");  
                }
                var lastChild = this.lastChild;
                lastChild.add(header); // 让ul节点的li节点去处理header
            }
        } else { // li节点让它的ul子节点去插入li
            if(level == this.level) {
                throw new Error("level error 2");
            } else if(level > this.level){ 
                if(this.num == 0) { // 没有ul子节点,就创建一个
                    var ul = document.createElement('ul');
                    this.appendChild(ul);
                    this.num++;
                    TocObj.call(ul, level).add(header); // 注意设置level
                } else { 
                    var lastChild = this.lastChild;
                    lastChild.add(header); //让ul节点去处理这个header
                }
            }
        }
    }
    return this;
}

var toc = document.getElementsByClassName('post')[0];
Toc.call(toc);

效果展示

对于有以下结构的HTML代码:

<div class="col-md-9 post">
    <h1>标题1</h1>
    <h2>标题1.1</h2>
    <h2>标题1.2</h2>
    <h1>标题2</h1>
    <h2>标题2.1</h2>
    <h3>标题2.1.1</h3>
    <h1>标题3</h1>
    <h1>标题4</h1>
    <h1>标题5</h1>
</div>

输出结构如下:

clipboard.png


孤独的自我
523 声望12 粉丝