github、码云 md文件目录化

github、码云、npmjs项目README.md增加目录侧栏导航,悬浮按钮

当前为 2022-03-18 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         github、码云 md文件目录化
// @name:en      Github, code cloud md file directory
// @namespace    github、码云 md文件目录化
// @version      1.14
// @description  github、码云、npmjs项目README.md增加目录侧栏导航,悬浮按钮
// @description:en  Github,code cloud project README.md add directory sidebar navigation,Floating button
// @author       lecoler
// @supportURL   https://github.com/lecoler/md-list
// @icon         https://raw.githubusercontent.com/lecoler/readme.md-list/master/static/icon.png
// @match        *://gitee.com/*/*
// @match        *://www.gitee.com/*/*
// @match        *://github.com/*/*
// @match        *://www.github.com/*/*
// @match        *://npmjs.com/*/*
// @match        *://www.npmjs.com/*/*
// @include      *.md
// @note         2022.03.18-v1.14 降低悬浮球位置,修改样式
// @note         2021.01.09-v1.13 修复高亮bug
// @note         2021.01.09-v1.12 新增根据页面阅读进度高亮
// @note         2020.11.10-v1.11 修复标题显示标签化问题
// @note         2020.10.30-v1.10 Fix not find node
// @note         2020.09.15-V1.9  优化,移除计时器,改成用户触发加载检测,同时为检测失败添加‘移除目录’按钮(测试版)
// @note         2020.09.14-V1.8  新增支持全部网站 *.md(测试版)
// @note         2020.07.14-V1.7  新增当前页面有能解析的md才展示
// @note         2020.06.23-V1.6  css样式进行兼容处理
// @note         2020.05.22-V1.5  新增支持github wiki 页
// @note         2020.05.20-V1.4  拖动按钮坐标改用百分比,对窗口大小改变做相应适配
// @note         2020.02.10-V1.3  修改样式,整个按钮可点;新增支持 npmjs.com
// @note         2019.12.04-V1.2  新增容错
// @note         2019.10.31-V1.1  修改样式,新增鼠标右键返回顶部
// @note         2019.10.28-V1.0  优化逻辑,追加判断目录内容是否存在
// @note         2019.10.25-V0.9  重构项目,移除jq,改用原生开发,新增悬浮按钮
// @note         2019.10.14-V0.9  修复bug
// @note         2019.9.18-V0.8  修改样式,新增可手动拉伸
// @note         2019.9.11-V0.7  新增点击跳转前判断是否能跳,不能将回到主页执行跳转
// @note         2019.8.11-V0.6  优化代码,修改样式
// @note         2019.7.25-V0.5  美化界面
// @note         2019.7.25-V0.4  新增支持github
// @note         2019.7.25-V0.2 修复bug,优化运行速度,新增按序获取
// @home-url     https://greasyfork.org/zh-CN/scripts/387834
// @homepageURL  https://github.com/lecoler/md-list
// @run-at 		 document-end
// ==/UserScript==
(function () {
    'use strict';
    // 初始化
    let reload = false; // 是否需重载
    let $main = null;
    let $menu = null;
    let $button = null;
    let lastPathName = '';
    let moveStatus = false;
    let titleHeight = 0;

    // 初始化按钮
    function createDom() {
        // 往页面插入样式表
        style();
        // 创建主容器
        $main = document.createElement('div');
        // 创建按钮
        $button = document.createElement('div');
        // 创建菜单
        $menu = document.createElement('ul');
        // 按钮设置
        $button.innerHTML = `目录`;
        $button.title = '右键返回顶部(RM to Top)';
        // 添加点击事件
        $button.addEventListener('click', btnClick);
        // 添加右键点击事件
        $button.oncontextmenu = e => {
            // 回到顶部
            scrollTo(0, 0);
            return false;
        };
        // 往主容器添加dom
        $main.appendChild($button);
        $main.appendChild($menu);
        // 主容器设置样式
        $main.setAttribute('class', 'le-md');
        // 为按钮添加拖动
        dragEle($button);
        // 往页面添加主容器
        document.body.appendChild($main);
        // 监听窗口大小
        window.onresize = function () {
            // 隐藏列表
            if (!$menu.className.match(/hidden/)) {
                $menu.className += ' hidden';
            }
        };
    }

    // 按钮点击事件
    function btnClick(e) {
        //判断是否在移动
        if (moveStatus) {
            moveStatus = false;
            return false;
        }
        if ($menu.className.match(/hidden/)) {
            // 判断路径是否改变,menu是否重载
            if (lastPathName !== window.location.pathname || reload) {
                start(true);
            }
            // 判断menu位置
            const winWidth = document.documentElement.clientWidth;
            const winHeight = document.documentElement.clientHeight;
            const x = e.clientX;
            const y = e.clientY;
            const classname1 = winWidth / 2 - x > 0 ? 'le-md-right' : 'le-md-left';
            const classname2 = winHeight / 2 - y > 0 ? 'le-md-bottom' : 'le-md-top';
            $menu.className = `${classname1} ${classname2}`;
        } else {
            $menu.className += ' hidden';
        }
    }

    // 插入样式表
    function style() {
        const style = document.createElement('style');
        style.innerHTML = `
       .le-md {
            position: fixed;
            top: 16%;
            left: 90%;
            z-index: 999;
        }
        .le-md-btn {
            display: block;
            font-size: 14px;
            text-transform: uppercase;
            width: 60px;
            height: 60px;
            -webkit-box-sizing: border-box;
                    box-sizing: border-box;
            border-radius: 50%;
            color: #fff;
            text-shadow: -1px -1px 1px rgba(0, 0, 0, 0.8);
            border: 0;
            background: hsla(230, 50%, 50%, 0.6);
            -webkit-animation: pulse 1s infinite alternate;
                    animation: pulse 1s infinite alternate;
            -webkit-transition: background 0.4s, margin 0.2s;
            -o-transition: background 0.4s, margin 0.2s;
            transition: background 0.4s, margin 0.2s;
            text-align: center;
            line-height: 60px;
            -webkit-user-select: none;
               -moz-user-select: none;
                -ms-user-select: none;
                    user-select: none;
            cursor: move;
        }
        .le-md-btn:after {
            background: rgba(0, 0, 0, 0.05);
            border-radius: 50%;
            bottom: -22.5px;
            content: "";
            display: block;
            height: 0;
            margin: 0 auto;
            left: 0;
            position: absolute;
            right: 0;
            width: 40px;
            -webkit-transition: height 0.5s ease-in-out, width 0.5s ease-in-out;
            -o-transition: height 0.5s ease-in-out, width 0.5s ease-in-out;
            transition: height 0.5s ease-in-out, width 0.5s ease-in-out;
            -webkit-animation: shadow 1s infinite alternate;
                    animation: shadow 1s infinite alternate;
        }
        .le-md-btn:hover {
            background: hsla(220, 50%, 47%, 1);
            margin-top: -1px;
            -webkit-animation: none;
                    animation: none;
            -webkit-box-shadow: inset -5px -10px 1px hsla(220, 50%, 42%, 1);
                    box-shadow: inset -5px -10px 1px hsla(220, 50%, 42%, 1);
        }
        .le-md-btn:hover:after {
            -webkit-animation: none;
                    animation: none;
            height: 10px;
        }
        .le-md-btn-hidden{
            display: none;
             -webkit-animation: none;
                     animation: none;
        }
        .hidden {
            height: 0 !important;
            min-height: 0 !important;
            border: 0 !important;
        }
        .le-md-left {
            right: 0;
            margin-right: 100px;
        }
        .le-md-right {
            left: 0;
            margin-left: 100px;
        }
        .le-md-top {
            bottom: 0;
        }
        .le-md-bottom {
            top: 0;
        }
        .le-md > ul {
            width: 200px;
            min-width: 100px;
            max-width: 1000px;
            list-style: none;
            position: absolute;
            overflow: auto;
            -webkit-transition: min-height 0.4s;
            -o-transition: min-height 0.4s;
            transition: min-height 0.4s;
            min-height: 50px;
            height: auto;
            max-height: 700px;
            resize: both;
            padding-right: 10px;
        }
        .le-md > ul::-webkit-scrollbar {
            width: 8px;
            height: 1px;
        }
        .le-md > ul::-webkit-scrollbar-thumb {
            border-radius: 8px;
            -webkit-box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
                    box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
            background-color: #96C2F1;
            background-image: linear-gradient(
                45deg,
                rgba(255, 255, 255, 0.2) 25%,
                transparent 25%,
                transparent 50%,
                rgba(255, 255, 255, 0.2) 50%,
                rgba(255, 255, 255, 0.2) 75%,
                transparent 75%,
                transparent
            );
        }
        .le-md > ul::-webkit-scrollbar-track {
            -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.1);
                    box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.1);
            border-radius: 8px 8px 0 0;
            background: #EFF7FF;
        }
        .le-md > ul a:hover {
            background: #fff;
            border-left: 1em groove #0099CC !important;
        }
        .le-md > ul a {
            text-decoration: none;
            font-size: 1em;
            color: #909399;
            text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
            display: block;
            white-space: nowrap;
            -o-text-overflow: ellipsis;
               text-overflow: ellipsis;
            overflow: hidden;
            padding: 5px 10px;
            border-bottom: 0.5em solid #eee;
            -webkit-transition: 0.4s all;
            -o-transition: 0.4s all;
            transition: 0.4s all;
            border-left: 0.5em groove #e2e2e2;
            border-right: 1px solid #e2e2e2;
            border-top: 1px solid #e2e2e2;
            background: #f4f4f5;
            -webkit-box-sizing: border-box;
                    box-sizing: border-box;
            -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
                    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
            border-radius: 0 0 5px 5px;
        }
        @-webkit-keyframes pulse {
            0% {
                margin-top: 0;
            }
            100% {
                margin-top: 6px;
                -webkit-box-shadow: inset -5px -10px 1px hsla(230, 50%, 55%, 0.6), 0 0 25px hsla(230, 50%, 50%, 1);
                        box-shadow: inset -5px -10px 1px hsla(230, 50%, 55%, 0.6), 0 0 25px hsla(230, 50%, 50%, 1);
            }
        }
        @keyframes pulse {
            0% {
                margin-top: 0;
            }
            100% {
                margin-top: 6px;
                -webkit-box-shadow: inset -5px -10px 1px hsla(230, 50%, 55%, 0.6), 0 0 25px hsla(230, 50%, 50%, 1);
                        box-shadow: inset -5px -10px 1px hsla(230, 50%, 55%, 0.6), 0 0 25px hsla(230, 50%, 50%, 1);
            }
        }
        @-webkit-keyframes shadow {
            to {
                height: 16px;
            }
        }
        @keyframes shadow {
            to {
                height: 16px;
            }
        }
        .le-md li.le-md-title-active a{
            background: linear-gradient(-135deg, #ffcccc 0.6em, #fff 0);
        }
        .le-md li.le-md-title-active.le-md-title-active-first a{
            background: linear-gradient(-135deg, #ff9999 0.6em, #fff 0);
            color: #000;
            font-weight: 700;
        }
        `;
        document.head.appendChild(style);
    }

    // 拖动事件
    function dragEle(ele) {
        ele.onmousedown = event => {
            // 鼠标相对dom坐标
            let eleX = event.offsetX;
            let eleY = event.offsetY;
            let count = 0;
            window.document.onmousemove = e => {
                //防止误触移动
                if (count > 9) {
                    moveStatus = true;
                }
                // dom相对win坐标
                let winX = e.clientX;
                let winY = e.clientY;
                // 实际坐标
                let x = winX - eleX;
                let y = winY - eleY;
                // win长宽
                let winWidth = document.documentElement.clientWidth;
                let winHeight = document.documentElement.clientHeight;
                // 转化成百分比
                ele.parentNode.style.left = (x / winWidth).toFixed(3) * 100 + '%';
                ele.parentNode.style.top = (y / winHeight).toFixed(3) * 100 + '%';
                count++;
            };
        };
        ele.onmouseup = () => {
            window.document.onmousemove = null;
        };
        ele.onmouseout = () => {
            window.document.onmousemove = null;
        };
    }

    // 执行, flag 是否部分重载
    function start(flag) {
        // 初始化
        reload = false;
        // 获取链接
        const host = window.location.host;
        lastPathName = window.location.pathname;

        // 获取相应的容器dom
        let $content = null;
        let list = [];
        if (host === 'github.com') {
            //github home / wiki
            const $parent = document.getElementById('readme') || document.getElementById('wiki-body');
            $content = $parent && $parent.getElementsByClassName('markdown-body')[0];
            // 标题dom高度
            const $boxTitle = ($parent && $parent.parentElement) ? $parent.parentElement.getElementsByClassName('js-sticky')[0] : null;
            titleHeight = $boxTitle ? $boxTitle.offsetHeight + 2 : 0;
            // 监听github dom的变化
            !$menu && domChangeListener(document.getElementById('js-repo-pjax-container'), start);
        } else if (host === 'gitee.com') {
            //码云 home
            const $parent = document.getElementById('tree-content-holder');
            $content = $parent && $parent.getElementsByClassName('markdown-body')[0];
            // 监听gitee dom的变化
            !$menu && domChangeListener(document.getElementById('tree-holder'), start);
        } else if (host === 'www.npmjs.com') {
            // npmjs.com
            const $parent = document.getElementById('readme');
            $content = $parent ? $parent : null;
        } else {
            // 检测是否符合md格式
            $content = checkMd();
        }
        // 获取子级
        const $children = $content ? $content.children : [];
        for (let $dom of $children) {
            const tagName = $dom.tagName;
            const lastCharAt = +tagName.charAt(tagName.length - 1);
            // 获取Tag h0-h9
            if (tagName.length === 2 && tagName.startsWith('H') && !isNaN(lastCharAt)) {
                // 获取value
                const value = $dom.innerText.trim();
                // 新增容错率
                const $a = $dom.getElementsByTagName('a')[0];
                if ($a) {
                    // 获取锚点
                    const href = $a.getAttribute('href');
                    // 获取offsetTop
                    const offsetTop = getTop($a)
                    list.push({
                        type: lastCharAt,
                        value,
                        href,
                        offsetTop
                    });
                }
            }
        }
        // 清空容器,不存在则创建
        if ($menu) {
            const list = [...$menu.childNodes];
            list.forEach(i => $menu.removeChild(i));
        } else {
            createDom();
        }
        if (!$menu || !$button) {
            console.warn('md文件目录化 脚本初始化失败');
            return false;
        }
        // 隐藏菜单
        if (!flag) {
            $menu.className = 'hidden';
        }
        //是否存在
        if (list.length) {
            // 生成菜单
            for (let i of list) {
                const li = document.createElement('li');
                li.setAttribute('data-offsetTop', i.offsetTop)
                const a = document.createElement('a');
                a.href = i.href;
                a.title = i.value;
                a.style = `font-size: ${1.3 - i.type * 0.1}em;margin-left: ${i.type - 1}em;border-left: 0.5em groove hsla(200, 80%, ${45 + i.type * 10}%, 0.8);`;
                a.innerText = i.value;
                li.appendChild(a);
                $menu.appendChild(li);
                // 是否不符合规范
                if (!i.value) {
                    reload = true;
                }
            }
            // 提供关闭入口
            if (reload) {
                const li = document.createElement('li');
                li.innerHTML = `<a title="移除目录" style="font-size: 1.1em;margin-left: 0.1em;border-left: 0.5em groove hsla(0,80%,50%,0.8);">移除目录</a>`;
                // 添加事件
                li.onclick = function () {
                    $main.remove();
                };
                $menu.appendChild(li);
            }
            // 设置按钮样式
            $button.setAttribute('class', 'le-md-btn');
        } else {
            // 设置按钮样式
            $button.setAttribute('class', 'le-md-btn le-md-btn-hidden');
        }
    }

    /**
     * @Description 监听指定dom发现变化事件
     * @author lecoler
     * @date 2020/7/14
     * @param dom
     * @param fun 回调 (MutationRecord[],MutationObserver)
     * @param opt 额外参数
     * @return MutationObserver
     */
    function domChangeListener(dom, fun, opt = {}) {
        if (!dom) return null;
        const observe = new MutationObserver(fun);
        observe.observe(dom, Object.assign({
            childList: true,
            attributes: true,
        }, opt));
        return observe;
    }

    /**
     * @Description 判断是否符合格式的md
     * @author lecoler
     * @date 2020/9/14
     * @return DOM
     */
    function checkMd() {
        // 缓存
        let tmp = [];
        // 是否存在h1 h2 h3 h4 h5 ...标签,同时他们父级相同
        for (let i = 1; i < 7; i++) {
            let list = document.body.getElementsByTagName(`h${i}`);
            // 获取父级
            for (let i = 0; i < list.length; i++) {
                const parent = list[i].parentElement;
                const item = tmp.filter(j => j && j['ele'].isEqualNode(parent))[0];
                if (item) {
                    item.count += 1;
                } else {
                    tmp.push({
                        ele: parent,
                        count: 1,
                    });
                }
            }
        }
        // 排序
        tmp.sort((a, b) => b.count - a.count);
        // 获取出现次数最高父级 返回
        return tmp.length ? tmp[0]['ele'] : null;
    }

    // 监听Windows滚动事件
    function onScrollEvent() {
        const fun = debounce(updateTitleActive, 500)
        // 判断原页面是否存在滚动事件监听,存在则合并,否则新建
        const oldFun = window.onscroll
        // 存在
        if (oldFun && oldFun.constructor === Function) {
            window.onscroll = function () {
                // 触发原页面事件
                oldFun.call(this)
                // 刷新标题 active 状态
                fun()
            }
        } else {
            window.onscroll = function () {
                // 刷新标题 active 状态
                fun()
            }
        }
    }

    /**
     * @Description 防抖动
     * @author lecoler
     * @date 2020/7/1
     * @param func<Function>
     * @param time<number>
     * @return Function
     */
    function debounce(func, time) {
        let context, args, timeId, timestamp

        function timeout() {
            const now = Date.now() - timestamp
            if (now >= 0 && now < time) {
                timeId = setTimeout(timeout, time - now)
            } else {
                timeId = null
                func.apply(context, args)
            }
        }

        function action() {
            context = this
            args = arguments
            timestamp = Date.now()
            if (!timeId) timeId = setTimeout(timeout, time)
        }

        return action
    }

    // 更新标题active状态
    function updateTitleActive() {
        // 获取目前页面scrollTop
        const ScrollTop = document.documentElement.scrollTop || document.body.scrollTop || 0
        const scrollTop = ScrollTop + titleHeight
        const offsetHeight = document.documentElement.clientHeight || document.body.clientHeight || 0
        const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight || 0
        // 存在菜单
        if ($menu) {
            const list = $menu.children || []

            for (let i = 0; i < list.length; i++) {
                const val = list[i].getAttribute('data-offsetTop')
                const nextVal = list[i + 1] ? list[i + 1].getAttribute('data-offsetTop') : scrollHeight
                // 排他
                list[i].removeAttribute('class')

                // 肉眼可见部分,标题高亮
                if (scrollTop <= val && val <= offsetHeight + scrollTop) {
                    list[i].className = 'le-md-title-active'
                }
                // 正在阅读部分,标题高亮
                if (scrollTop >= val && nextVal > scrollTop) {
                    list[i].className = 'le-md-title-active le-md-title-active-first'
                }
            }
        }
    }

    /**
     * @describe 获取dom元素距离body的offsetTop
     * @author lecoler
     * @date 21-1-8
     * @param $dom<Node>
     * @return Number
     */
    function getTop($dom, val = 0) {
        if (!$dom) return val
        const offsetTop = $dom.offsetTop || 0
        return getTop($dom.offsetParent, offsetTop + val)
    }

    try {
        document.onreadystatechange = function () {
            if (document.readyState === 'complete') {
                start();
                // 监听滚动
                onScrollEvent()
            }
        };
    } catch (e) {
        console.error('github、码云 md文件目录化 脚本异常报错:');
        console.error(e);
        console.error('请联系作者修复解决,https://github.com/lecoler/md-list');
    }
})();