vue使用marked.js实现markdown转html并提取标题生成目录

html:

<template>
    <div class="wrapper">
        <div class="container">
            <div class="menu">
                <ul class="menu-list">
                    <li v-for="(nav, index) in navList" 
:key="index"
:class="{on: activeIndex === index}"
@click="currentClick(index)"> <a href="javascript:;" @click="pageJump(nav.index)">{{nav.title}}</a> <div v-if="nav.children.length > 0 && activeIndex === index"
class="menu-children-list"> <ul class="nav-list"> <li v-for="(item, idx) in nav.children"
:key="idx"
:class="{on: childrenActiveIndex === idx}"
@click.stop="childrenCurrentClick(idx)"> <a href="javascript:;" @click="pageJump(item.index)">
{{item.title}}
</a> </li> </ul> </div> </li> </ul> </div> <div class="help-center-content" v-html="compiledMarkdown"
       ref="helpDocs" @scroll="docsScroll"></div> </div> </div> </template>

js部分:

<script>

import marked from 'marked';

let rendererMD = new marked.Renderer();
marked.setOptions({
    renderer: rendererMD,
    gfm: true,
    tables: true,
    breaks: false,
    pedantic: false,
    sanitize: false,
    smartLists: true,
    smartypants: false
});

export default {
    props: ['mdContent'],
    data() {
        return {
            navList: [],
            activeIndex: 0,
            docsFirstLevels: [],
            docsSecondLevels: [],
            childrenActiveIndex: 0
        }
    },
    mounted() {
        this.navList = this.handleNavTree();
        this.getDocsFirstLevels(0);
    },
    methods: {
        childrenCurrentClick(index) {
            this.childrenActiveIndex = index
        },
        getDocsFirstLevels(times) {
            // 解决图片加载会影响高度问题
            setTimeout(() => {
                let firstLevels = [];
                Array.from(document.querySelectorAll('h1'), element => {
                    firstLevels.push(element.offsetTop - 60)
                })
                this.docsFirstLevels = firstLevels;

                if (times < 8) {
                    this.getDocsFirstLevels(times + 1);
                }
            }, 500);
        },
        getDocsSecondLevels(parentActiveIndex) {
            let idx = parentActiveIndex;
            let secondLevels = [];
            let navChildren = this.navList[idx].children

            if(navChildren.length > 0) {
                secondLevels = navChildren.map((item)=>{
                    return this.$el.querySelector(`#data-${item.index}`).offsetTop - 60
                })
                this.docsSecondLevels = secondLevels;
            }
        },
        docsScroll() {
            if (this.titleClickScroll) {
                return;
            }

            let scrollTop = this.$refs.helpDocs.scrollTop
            let firstLevelIndex = this.getLevelActiveIndex(scrollTop, this.docsFirstLevels)
            this.currentClick(firstLevelIndex)

            let secondLevelIndex = this.getLevelActiveIndex(scrollTop, this.docsSecondLevels)
            this.childrenCurrentClick(secondLevelIndex)
        },
        getLevelActiveIndex(scrollTop, docsLevels) {
            let currentIdx = null;
            let nowActive = docsLevels.some((currentValue, index) => {
                if(currentValue >= scrollTop) {
                    currentIdx = index
                    return true
                }
            })

            currentIdx = currentIdx - 1
            
            if (nowActive && currentIdx === -1) {
                currentIdx = 0
            } else if (!nowActive && currentIdx === -1) {
                currentIdx = docsLevels.length - 1
            }
            return currentIdx     
        },
        pageJump(id) {
            this.titleClickScroll = true;
            this.$refs.helpDocs.scrollTop = this.$el.querySelector(`#data-${id}`).offsetTop - 40;
            setTimeout(() => this.titleClickScroll = false, 100);
        },
        currentClick(index) {
           this.activeIndex = index
           this.getDocsSecondLevels(index)
        },
        getTitle(content) {
            let nav = [];

            let tempArr = [];
            content.replace(/(#+)[^#][^\n]*?(?:\n)/g, function(match, m1, m2) {
                    let title = match.replace('\n', '');
                    let level = m1.length;
                    tempArr.push({
                        title: title.replace(/^#+/, '').replace(/\([^)]*?\)/, ''),
                        level: level,
                        children: [],
                    });
                });

            // 只处理一级二级标题,以及添加与id对应的index值
            nav = tempArr.filter(item => item.level <= 2);
            let index = 0;
            return nav = nav.map(item => {
                item.index = index++;
                return item;
            });
        },
        // 将一级二级标题数据处理成树结构
        handleNavTree() {
            let navs = this.getTitle(this.content)
            let navLevel = [1, 2];
            let retNavs = [];
            let toAppendNavList;

            navLevel.forEach(level => {
                // 遍历一级二级标题,将同一级的标题组成新数组
                toAppendNavList = this.find(navs, {
                    level: level
                });
                
                if (retNavs.length === 0) {
                    // 处理一级标题                    
                    retNavs = retNavs.concat(toAppendNavList);
                } else {
                    // 处理二级标题,并将二级标题添加到对应的父级标题的children中    
                    toAppendNavList.forEach(item => {
                        item = Object.assign(item);
                        let parentNavIndex = this.getParentIndex(navs, item.index);
                        return this.appendToParentNav(retNavs, parentNavIndex, item);
                    });
                }
            });
            return retNavs;
        },
        find(arr, condition) {
            return arr.filter(item => {
                for (let key in condition) {
                    if (condition.hasOwnProperty(key) && condition[key] !== item[key]) {
                        return false;
                    }
                }
                return true;
            });
        },
        getParentIndex(nav, endIndex) {
            for (var i = endIndex - 1; i >= 0; i--) {
                if (nav[endIndex].level > nav[i].level) {
                    return nav[i].index;
                }
            }
        },
        appendToParentNav(nav, parentIndex, newNav) {
            let index = this.findIndex(nav, {
                index: parentIndex
            });
            nav[index].children = nav[index].children.concat(newNav);
        },
        findIndex(arr, condition) {
            let ret = -1;
            arr.forEach((item, index) => {
                for (var key in condition) {
                    if (condition.hasOwnProperty(key) && condition[key] !== item[key]) { 
                        return false;
                    }
                }
                ret = index;
            });
            return ret;
        },
    },
    computed: {
        content() {
            return this.mdContent
        },
        compiledMarkdown: function() {
            let index = 0;
            rendererMD.heading = function(text, level) {
                if (level <= 2) {
                    return `<h${level} >${text}</h${level}>`;
                } else {
                    return `<h${level}>${text}</h${level}>`;
                }
            };
            rendererMD.code = function(code, language) {  
                code = code.replace(/\r\n/g,"<br>")
                code = code.replace(/\n/g,"<br>");
                return `<div class="text">${code}</div>`;
            };
            return marked(this.content);
        }
    }
}
</script>

参考链接:

https://github.com/markedjs/marked

https://www.jianshu.com/p/d182ea991609

https://hk.saowen.com/a/bf975e4296e33a14e2d0ad50aa7cbf24fbfb4a9fb851de171b4c71da54eb95e5