Github Markdown File Content Navigation

Provide directory navigation of the markdown file content of the github website.

// ==UserScript==
// @name         Github Markdown File Content Navigation
// @name:zh-CN   Github Markdown 文件内容导航
// @namespace    https://github.com/wang1212/github-markdown-file-content-navigation
// @version      0.1.1
// @description  Provide directory navigation of the markdown file content of the github website.
// @description:zh-cn 提供 github 网站 markdown 文件内容的目录导航。
// @author       wang1212
// @match        http*://github.com/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const log = console.log

    /* ------------------------------- Init ----------------------------- */

    const href = location.href
    const matchRepository = /https?:\/\/github.com\/.+\/.+/

    const isRepositoryPage = !!href.match(matchRepository)

    /* ------------------------------- Parse MarkDown file content navigation ----------------------------- */

    function updateMarkdownFileContentNavigation() {
        let navBarElem = document.querySelector('.wang1212_md-content-nav')

        // Remove existing
        navBarElem && navBarElem.remove()

        const rootElem = document.querySelector('.markdown-body')
        if (!isRepositoryPage || !rootElem) return

        // navBar button
        navBarElem = document.createElement('div')
        navBarElem.classList.add('wang1212_md-content-nav')
        navBarElem.title = 'Markdown 文件内容导航'

        navBarElem.innerText = 'N'

        // Panel
        const navBarPanelElem = document.createElement('div')
        navBarPanelElem.classList.add('wang1212_md-content-nav_panel')

        navBarPanelElem.innerHTML = ''

        // titles
        const titles = getMarkDownContentTitles(rootElem)
        if (!titles.length) return

        titles.forEach(title => {
            const level = +title.tagName.substr(-1)
            navBarPanelElem.innerHTML += `
<p class="wang1212_md-content-nav_to-anchor" style="font-size: ${1 - ((level - 1) * 0).toFixed(2)}rem; margin: 0; padding-left: ${((level - 1) * 0.5).toFixed(2)}rem" data-anchor="${title.anchorId}">
${title.text}
</p>
`
        })

        // --- CSS Style ---
        const styleElem = document.createElement('style')
        styleElem.type = 'text/css'
        styleElem.innerHTML = `
.wang1212_md-content-nav {
position: fixed;
right: 1rem;
bottom: 3.5rem;
z-index: 999;
width: 2rem;
height: 2rem;
color: white;
font-size: 1.5rem;
line-height: 2rem;
text-align: center;
background-color: rgb(36, 41, 46);
cursor: pointer;
}
.wang1212_md-content-nav_panel {
position: absolute;
right: 0;
bottom: 2rem;
display: block;
width: 20rem;
height: 75vh;
padding: 0.5rem;
overflow: auto;
color: #ddd;
text-align: left;
background: white;
box-shadow: rgba(0, 0, 0, 0.25) 0 0 0.5rem 0;
}
.wang1212_md-content-nav_to-anchor:hover {
	color: rgb(0, 0, 0);
}
`

        navBarElem.appendChild(navBarPanelElem)
        document.body.appendChild(navBarElem)
        document.head.appendChild(styleElem)

        // --- Event ---
        // Show/Hide
        navBarElem.addEventListener("click", (e) => {
            if (e.target !== navBarElem) return

            if (navBarPanelElem.style.display === 'none') {
                navBarPanelElem.style.display = 'block'
            } else {
                navBarPanelElem.style.display = 'none'
            }
        }, false)

        // fly to view
        navBarPanelElem.addEventListener("click", (e) => {
            if (!e.target.classList.contains('wang1212_md-content-nav_to-anchor')) return

            const anchorElem = document.getElementById(e.target.dataset.anchor)
            if (!anchorElem) return

            anchorElem.scrollIntoView({ behavior: "smooth" })
        }, false)
    }

    /* ------------------------------- To Top ----------------------------- */

    // to top button
    function updateGoToTopButton() {
        let toTopElem = document.querySelector('.wang1212_to-top')

        // Remove existing
        toTopElem && toTopElem.remove()

        // toTop button
        toTopElem = document.createElement('div')
        toTopElem.classList.add('wang1212_to-top')
        toTopElem.title = '回到顶部'

        toTopElem.innerText = '↑'


        // --- CSS Style ---
        const styleElem = document.createElement('style')
        styleElem.type = 'text/css'
        styleElem.innerHTML = `
.wang1212_to-top {
position: fixed;
right: 1rem;
bottom: 1rem;
z-index: 999;
width: 2rem;
height: 2rem;
color: white;
font-size: 1.5rem;
line-height: 2rem;
text-align: center;
background-color: rgb(36, 41, 46);
cursor: pointer;
}
`

        document.body.appendChild(toTopElem)
        document.head.appendChild(styleElem)

        // --- Event ---
        // fly to view
        toTopElem.addEventListener("click", () => {
            document.body.scrollIntoView({ behavior: "smooth" })
        }, false)
    }

    /* ------------------------------- Utils ----------------------------- */

    // parse titles
    function getMarkDownContentTitles(rootElem) {
        const anchors = rootElem.querySelectorAll('a.anchor')

        if (!anchors.length) return

        const titles = []

        anchors.forEach((elem) => {
            const parentElem = elem.parentElement

            titles.push({
                tagName: parentElem.tagName,
                text: parentElem.textContent,
                anchorId: elem.id,
            })
        })

        return titles
    }

    /* ------------------------------- Load ----------------------------- */

    function load() {
        updateMarkdownFileContentNavigation()
        updateGoToTopButton()
    }

    // Monitor page reload
    document.addEventListener("pjax:end", load, false)

    //
    load()

})();