Project address https://github.com/liangxh0523/md_web_display

Let’s start with the results

Page layout

The web layout is divided into three parts, respectively

  • Header, fixed position
  • Aside,margin-top is the height of the header
  • Contain, static position, margin-top = header height, margin-left = aside width, router-view exit Divided into two parts:
    • Main content, display md converted HTML page, margin-right value is md directory width value
    • Extract markdown’s H1 and H2 directories for title navigation, fixed positioning

function

In order for MD to be well displayed on web pages, it should have the following functions:

  1. Click the menu on the left to get the corresponding MD content (string format), convert the MD content into HTML, and add anchor point ID for the first and second level titles
  2. To add md format to HTML, introduce a CSS, refer to the url
  3. Extract the first and second level titles in MD and display the article table of contents on the right
  4. Click on the right side of the article catalog, and the content on the left side can be located to the corresponding position, and the smooth scrolling process is also required to enhance the user experience. Ps: This function is mainly discussed in three ways
    • Native JS method, the advantage is good compatibility, almost all browsers support, the disadvantage is the code is not elegant
    • The scroll behavior method of CSS has the advantage of simple and beautiful code, but the disadvantage is that it does not support Safari (maybe more, only opera, Chrome, Firefox).
    • Js scrollIntoView method, the advantage is that there is no need for anchor location, simple code. The downside is also browser compatibility issues, not safari support
  5. As the content on the left scrolls, the activation items in the right directory change dynamically

Md into HTML

I used marked. Js to convert md to HTML

// install marked. Js to local NPM install marked --save'marked'; SetOptions ({renderer: rendererMD, GFM:true,
    tables: true,
    breaks: false,
    pedantic: false,
    sanitize: false,
    smartLists: true,
    smartypants: false}); / / instantiatelet rendererMD = new marked.Renderer();
watch: {
    '$route.path': {
        immediate:true,
        handler: function(newValue) {
            this.mode = newValue.slice(1);
            letindex = 0; Renderermd. heading has its own scope, not accessible by thisletMode = this.mode // The processing here is just to show more than one demo at a time, the three methods require different processing methods renderermd. heading =function(text, level) {
                if (level < 3) {
                    return `<h${level} id="${index++}" class="jump" >${text}</h${level}> `; }else {
                    return `<h${level}>${text}</h${level}>`;
                }
            };
            document.getElementsByTagName("html")[0].style['scroll-behavior'] = this.mode === 'css_demo' ? 'smooth' : ' '; this.compiledMarkdown = marked(str); }}}Copy the code

In HTML, you can bind the calculated property with V-HTML

<div class="markdown-body" ref="content" id="content" v-html="compiledMarkdown">
Copy the code

Extract the title

getTitle(content) {
    let nav = [];
    let navLevel = [1, 2];
    let tempArr = [];
    content
        .replace(/```/g, function(match) {
            return '\f'; }) .replace(/\f[^\f]*? \f/g,function(match) {
            return ' ';
        })
        .replace(/\r|\n+/g, function(match) {
            return '\n'; }) // At least one# Lazy match followed by any character other than a non-newline character, followed by a newline character
        .replace(/(#+)[^#][^\n]*? (? :\n)/g, function(match, m1) {
            let title = match.replace('\n'.' ');
            let level = m1.length;
            tempArr.push({
                title: title.replace(/^#+/, '').replace(/\([^)]*? \)/"),level: level, children: [] }); }); Nav = tempar.filter (_ => _. Level <= 2);letindex = 0; Nav = nav. Map (_ => {_. Index = index++;return _;
    });
    let retNavs = [];
    lettoAppendNavList; ToAppendNavList = this.find(nav, {level: level}); toAppendNavList = this.find(nav, {level: level});if(retnavs.length === 0) {// Handle the first level title retNavs = retnavs. concat(toAppendNavList); }elseToappendnavlist.foreach (_ => {_ = object.assign (_); toappendnavList.foreach (_ => {_ = object.assign (_));let parentNavIndex = this.getParentIndex(nav, _.index);
                returnthis.appendToParentNav(retNavs, parentNavIndex, _); }); }}); // retNavs are processed treesreturnretNavs; }, // Find (arr, condition) {return arr.filter(_ => {
        for (let key in condition) {
            if(condition.hasOwnProperty(key) && condition[key] ! == _[key]) {return false; }}return true; }); }, // Get the parent of this node getParentIndex(nav, endIndex) {// Start with the current index and find 1. 2. The lower the level, the higher the level.for (var i = endIndex - 1; i >= 0; i--) {
        if (nav[endIndex].level > nav[i].level) {
            returnnav[i].index; }}}, // find all children of the same parent node appendToParentNav(nav, parentIndex, newNav) {// Find the index value of the Fu title of each secondary titlelet index = this.findIndex(nav, {
        index: parentIndex
    });
    ifIf (index === -1) {// If (index == -1) {// If (index == -1) {// If (index == -1)let subNav;
        for(var i = 0; i < nav.length; SubNav = nav[I]; subNav = nav[I]; subNav.children.length && this.appendToParentNav(subNav.children, parentIndex, newNav); }}else{ 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]) {// no deep comparisonreturn false;
            }
        }
        ret = index;
    });
    return ret;
},
Copy the code

Md directory display and anchor location

<div id="menu">
    <ul class="nav-list">
        <li v-for="(nav, index) in contentMenu" :key="index">
            <a :href="'#' + nav.index" :class="{'active': highlightIndex === nav.index}" @click="handleHighlight(nav.index)" :key="nav.index">{{nav.title}}
            </a>
            <template v-if="nav.children.length > 0">
                <ul class="nav-list">
                    <li v-for="(item, index) in nav.children" :key="index">
                        <a :href="'#' + item.index" :class="{active: highlightIndex === item.index}" :key="item.index" @click="handleHighlight(item.index)">{{item.title}}
                        </a>
                    </li>
                </ul>
            </template>
        </li>
    </ul>
</div>
Copy the code

Smooth scrolling

There are three ways to achieve smooth scrolling

1. Js native method

When the scroll bar on the right is clicked, the click event is processed and the distance to scroll is determined according to the difference between the offsetTop and scrollTop values of the clicked element

handleHighlight(item) {
    this.highlightIndex = item;
    if (this.mode === 'raw_js_demo') {
        this.rawJsScroll(item);
    } else if (this.mode === 'scroll_into_view') {
        let jump = document.querySelectorAll('.jump');
        jump[item].scrollIntoView({
            block: 'start',
            behavior: "smooth"
        });
    }
},
rawJsScroll(item) {
    let jump = document.querySelectorAll('.jump'); // 60 is the height of the headerlet total = jump[item].offsetTop - 60;
    letdistance = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop; // Smooth roll, length 500ms, one hop every 10ms, total 50 hopslet step = total / 50;
    if (total > distance) {
        smoothDown();
    } else {
        let newTotal = distance - total;
        step = newTotal / 50;
        smoothUp();
    }
    function smoothDown() {
        if (distance < total) {
            distance += step;
            window.scrollTo(0, distance);
            setTimeout(smoothDown, 10);
        } else{ window.scrollTo(0, total); }}function smoothUp() {
        if (distance > total) {
            distance -= step;
            window.scrollTo(0, distance);
            setTimeout(smoothUp, 10);
        } else{ window.scrollTo(0, total); }}}Copy the code

2. CSS mode

If CSS is used, the scrollbar container’s Scroll behavior property is set to smooth

document.getElementsByTagName("html")[0].style['scroll-behavior'] = this.mode === 'css_demo' ? 'smooth' : ' ';
Copy the code

Since there is header, it needs to be offset when anchor point is positioned. Therefore, insert an element before h1 and H2 to be positioned, and bind anchor point value in the tag of this element. Its padding-top and margin-top are set as negative numbers, and the value is the height of the head. This completes the offset processing of the positioned element.

rendererMD.heading = function(text, level) {
    if (level < 3) {
        return <div class="jump offset" id="${index++}"></div><h${level}>${text}</h${level}> `; }else {
        return `<h${level}>${text}</h${level}>`;
    }
};

div .offset {
    position: relative;
    padding-top: 40px;
    margin-top: -40px;
    visibility: hidden;
}
Copy the code

3. JS scrollIntoView() method

If you use the scrollIntoView() method, there is no need to add anchor points for positioning. In the click event of the directory item, set the block and behavior values of the scrollIntoView() method using the currently selected element, and scroll smoothly to the specified position

rendererMD.heading = function(text, level) {
    if (level < 3) {
        return <div class="jump offset" id="${index++}"></div><h${level}>${text}</h${level}> `; }else {
        return `<h${level}>${text}</h${level}>`;
    }
};
handleHighlight(item) {
    let jump = document.querySelectorAll('.jump');
    jump[item].scrollIntoView({
        block: 'start',
        behavior: "smooth"
    });
}
Copy the code

The main contents scroll and the contents are highlighted

When reading the md content, the highlighted items in the directory change as the scrollbar changes. Note here:

  • Mounted Add scroll listening event to mounted hook function
  • Since scroll is global, this listener should be removed before leaving the current component
mounted() {
    this.$nextTick(function() {
        window.addEventListener('scroll', this.onScroll);
    });
},
beforeDestroy() {
    window.removeEventListener('scroll', this.onScroll);
},
methods: {
    onScroll() {
        let top = document.documentElement ? document.documentElement.scrollTop : document.body.scrollTop;
        let items = document.getElementById('content').getElementsByClassName('jump');
        let currentId = ' ';
        for (let i = 0; i < items.length; i++) {
            let _item = items[i];
                let_itemTop = _item.offsetTop; // The offset of the three smooth scroll processing methods is differentlet height = this.mode === 'raw_js_demo' ? 75 : (this.mode === 'css_demo'? 10:0);if (top > _itemTop - height) {
                    currentId = i;
                } else {
                    break; }}ifHighlightIndex = parseInt(currentId) {// currentOId is a string and must be converted to a number, otherwise the congruency of the highlighted item will not match this.highlightIndex = parseInt(currentId); }}}Copy the code

Summary

This is how to make md well displayed on the web page all the features, Google many times, thanks to share experience of Ariba Dei, open source long live ~!

Points to be improved include:

  • How do I avoid regular matches for non-heading #, for example// # this is not the titleFormatted content
  • The processing of H1 and H2 tags in MD, which allows HTML, now does not satisfy tags matching H1 and H2

Feel useful words please point praise ~ your praise is I update the biggest power ~!