Greasy Fork is available in English.

通用阅读器

为所有站点增加进入阅读模式按钮,点击后如果匹配成功则自动转码成为通用的阅读器样式方便使用,并提供扩展语音阅读功能(需要浏览器支持,推荐最新版Firefox浏览器)

// ==UserScript==
// @name         通用阅读器
// @version      0.4.1
// @description  为所有站点增加进入阅读模式按钮,点击后如果匹配成功则自动转码成为通用的阅读器样式方便使用,并提供扩展语音阅读功能(需要浏览器支持,推荐最新版Firefox浏览器)
// @author       pppploi8
// @match        https://*/*
// @match        http://*/*
// @grant        none
// @namespace https://greasyfork.org/users/240492
// ==/UserScript==

(function() {
    var $ = function(selector){
        return document.querySelector(selector);
    }

    var blackList = {"www.baidu.com": true, "www.sogou.com": true, "www.so.com": true, "www.bing.com":true, "cn.bing.com": true, "www.google.com": true, "m.sm.cn": true};

    // 通用解析模板
    function parseContentAndTitle(){
        var mainDom = null;

        function findMainDom(doms){
            var docSize = document.body.scrollHeight * document.body.scrollWidth;
            for(var i=0;i<doms.length;i++){
                var dom = doms[i];
                // 计算dom尺寸百分比并输出
                var size = (dom.scrollWidth * dom.scrollHeight) / docSize;
                if (size > 0.3 && dom.nodeName !== "CODE" && dom.nodeName !== "PRE"){ // pre和code是代码常用元素块,忽略不认为是正文内容
                    // 如果innerText字数大于500,则视为正文区
                    var text = dom.innerText;
                    if (text.length >= 500){
                        mainDom = dom;
                    }
                }
                findMainDom(dom.children||[]);
            }
        }
        findMainDom(document.body.children);

        if (mainDom){
            // 在mainDom同层查找标题
            // var titleList = [];
            // findTitleDom(mainDom.parentNode.children, titleList);
            // 查找标题逻辑bug较多,暂时改为查找网页title作为标题,后续重写逻辑进行优化
            return {content: mainDom.innerText, title: document.title};
        }

        // function findTitleDom(doms, titleList){
        //     for(var i=0;i<doms.length;i++){
        //         var dom = doms[i];
        //         if (dom.children && dom.children.length !== 0){
        //             var title = findTitleDom(dom.children, titleList);
        //         }else{
        //             if (/\<h1|\<h2|title/.test(dom.outerHTML)){
        //                 var title = dom.innerText;
        //                 if (title && title.length >= 5 && title.length <= 200){
        //                     titleList.push(dom);
        //                 }
        //             }
        //         }
        //     }
        // }
    }

    function parsePageUp(){
        var as = document.querySelectorAll('a');
        var reg = /上一章|上一篇|上一页|navigation-prev/;
        for(var i=0;i<as.length;i++){
            var text = as[i].outerHTML;
            var href = (as[i].attributes.href && as[i].attributes.href.value) || (as[i].dataset && as[i].dataset.url);
            if (text && reg.test(text.trim()) && href && href != "#" && href.indexOf("javascript:") !== 0){
                return href;
            }
        }
    }

    function parsePageDown(){
        var as = document.querySelectorAll('a');
        var reg = /下一章|下一篇|下一页|navigation-next/;
        for(var i=0;i<as.length;i++){
            var text = as[i].outerHTML;
            var href = (as[i].attributes.href && as[i].attributes.href.value) || (as[i].dataset && as[i].dataset.url);
            if (text && reg.test(text.trim()) && href && href != "#" && href.indexOf("javascript:") !== 0){
                return href;
            }
        }
    }

    function parsePageIndex(){
        var as = document.querySelectorAll('a');
        var reg = /目录/;
        for(var i=0;i<as.length;i++){
            var text = as[i].innerText;
            var href = as[i].attributes.href && as[i].attributes.href.value;
            if (text && text.length <= 10 && reg.test(text.trim()) && href && href != "#" && href.indexOf("javascript:") !== 0){
                return href;
            }
        }
    }


    var fontsize = parseInt(localStorage["_er_fontsize"]||0);
    var autoplay = false;
    if (localStorage['_er-autoplay'] === 'true'){
        autoplay = true;
    }
    delete localStorage['_er-autoplay'];

    if (top.window !== window) return; // iframe内的网页不展示按钮,也不支持进入阅读模式
    if (localStorage['_er-enable'] === 'true'){
        localStorage['_er-enable'] = 'false';
        checkAndCreateReader(true);
    } else if (blackList[location.host] !== true){
        // 创建阅读模式悬浮按钮
        $('body').children[0].insertAdjacentHTML('beforeBegin', '<button id="_er-entryReadMode" style="' +
            '    position: fixed;' +
            '    right: 50px;' +
            '    bottom: 50px;' +
            '    background-color: white;' +
            '    border: 1px solid green;' +
            '    border-radius: 10px;' +
            '    padding: 0 5px;' +
            '    height: 50px;' +
            '    overflow: auto;' +
            '    background-color: white;' +
            '    z-index: 201901272210;">进入阅读模式</button>');
        // 配置阅读模式按钮自动淡出效果
        var i = 1;
        var interval = setInterval(function(){
            i -= 0.03;
            $('#_er-entryReadMode').style.opacity = i;
            if (i <= 0) {
                $('#_er-entryReadMode').remove();
                clearInterval(interval);
            }
        }, 100);
        $('#_er-entryReadMode').onclick = checkAndCreateReader;
        $('#_er-NotShowReadMode').onclick = function(){
            localStorage['_er-disabled'] = 'true';
            $('#_er-entryReadMode').remove();
            $('#_er-NotShowReadMode').remove();
        }
    }

    function checkAndCreateReader(notAlert){
        // 通过调用通用模板尝试是否能够成功匹配到阅读内容
        var content = parseContentAndTitle();
        if (content && content.content){
            content.pageup = parsePageUp();
            content.pagedown = parsePageDown();
            content.pageindex = parsePageIndex();
            createReader(content);
        }else{
            if (notAlert !== true){
                alert('当前页面解析失败,无法进入阅读模式!');
            }
        }
    }

    // 创建阅读器
    function createReader(content){
        $('#_er-entryReadMode') && $('#_er-entryReadMode').remove();
        $('#_er-NotShowReadMode') && $('#_er-NotShowReadMode').remove();
        addClassAndDom();
        if (window.SpeechSynthesisUtterance){
            $('#_er-tts').style.display = 'block';
        }
        $('._er-title').innerText = content.title;
        var contentArr = content.content.split('\n');
        var contentHtml = '';
        for(var i=0;i<contentArr.length;i++){
            var line = contentArr[i];
            if (line){
                contentHtml += '<span>' + line + '</span>';
            }
            contentHtml += '<br>';
        }
        $('._er-content').innerHTML = contentHtml;
        var spanNodes = document.querySelectorAll('._er-content span');
        for(var i=0;i<spanNodes.length;i++){
            spanNodes[i].onclick = function(){
                for(var j=0;j<spanNodes.length;j++){
                    spanNodes[j].classList.remove('_er-current');
                }
                this.classList.add('_er-current');
            }
        }
        // 挂接键盘事件,实现键盘上下左右切换阅读功能
        $('body').onkeydown = function(e){
            switch(e.keyCode){
                case 38: // up
                    toPrevReadPos();
                    updateReadPos();
                    break;
                case 40: // down
                    toNextReadPos();
                    updateReadPos();
                    break;
                case 37: // left
                    $('._er').scrollTop = $('._er').scrollTop - (document.documentElement.clientHeight - 24)
                    break;
                case 39: // right
                    $('._er').scrollTop = $('._er').scrollTop + (document.documentElement.clientHeight - 24);
                    break;
                default:
                    return true;
            }
            return false;
        };
        $('._er-content').onclick = function(e){ // 适用于墨水屏的左右点击无动画翻页
            var x = e.pageX;
            var width = document.documentElement.clientWidth;
            if (x <= width*0.1){ // 前翻一页
                $('._er').scrollTop = $('._er').scrollTop - (document.documentElement.clientHeight - 24)
            }else if(x >= width*0.9){ // 后翻一页
                $('._er').scrollTop = $('._er').scrollTop + (document.documentElement.clientHeight - 24);
            }
        }
        $('#_er-pageindex').onclick = function(){
            if (content.pageindex){
                location.href = content.pageindex;
            }else{
                alert('很抱歉,没有匹配到目录!');
            }
        };
        $('#_er-pageup').onclick = function(){
            if (content.pageup){
                localStorage['_er-enable'] = 'true';
                location.href = content.pageup;
            }else{
                alert('很抱歉,没有匹配到上一页!');
            }
        };
        $('#_er-pagedown').onclick = function(){
            if (content.pagedown){
                localStorage['_er-enable'] = 'true';
                location.href = content.pagedown;
            }else{
                alert('很抱歉,没有匹配到下一页!');
            }
        };
        $('#_er-pagedown').dataset['nexturl'] = content.pagedown;
        setFontSize();

        // 按钮事件处理
        $('#_er-close').onclick = removeDom;
        $('#_er-font-plus').onclick = function(){
            fontsize += 2;
            setFontSize();
        };
        $('#_er-font-minus').onclick = function(){
            fontsize -= 2;
            setFontSize();
        };

        $('#_er-tts').onclick = function(){
            if (this.dataset['pause'] === 'true'){
                // 开始播放
                this.innerText = '停止';
                this.dataset['pause'] = 'false';
                playNextText();
            }else{
                this.innerText = '听书';
                this.dataset['pause'] = 'true';
            }
        };

        if (autoplay){
            $('#_er-tts').innerText = '停止';
            $('#_er-tts').dataset['pause'] = 'false';
            playNextText();
        }else{
            $('#_er-tts').dataset['pause'] = 'true';
        }
    }

    // 听书功能
    function playNextText(){
        updateReadPos();
        var current = $('._er-current');
        var playText = '';
        if (current){
            playText = current.innerText;
        }else{
            playText = $('._er-title').innerText;
        }
        if (playText){
            var utterThis = new SpeechSynthesisUtterance();
            utterThis.text = playText;
            utterThis.onerror = function(){
                $('#_er-tts').dataset['pause'] = 'true';
                alert("TTS语音转换文字出现异常,听书已停止运行!");
            };
            utterThis.onend = function(){
                toNextReadPos();
                if (!$('._er-current')){
                    var nextUrl = $('#_er-pagedown').dataset['nexturl'];
                    console.log(nextUrl);
                    if (nextUrl){
                        localStorage['_er-autoplay'] = 'true';
                        localStorage['_er-enable'] = 'true';
                        location.href = nextUrl;
                    }
                    return;
                }
                if ($('#_er-tts').dataset['pause'] === 'false'){
                    playNextText();
                }
            };
            speechSynthesis.speak(utterThis);
        }else{
            toNextReadPos();
            playNextText();
        }
    }

    function toNextReadPos(){
        var current = $('._er-current');
        var nextSpan = null;
         if (current){
            nextSpan = current.nextElementSibling;
            while(nextSpan && nextSpan.nodeName !== 'SPAN'){
                nextSpan = nextSpan.nextElementSibling;
            }
        }else{
            nextSpan = $('._er-content span');
        }
        if (current) current.classList.remove('_er-current');
        if (nextSpan) nextSpan.classList.add('_er-current');
    }

    function toPrevReadPos(){
        var current = $('._er-current');
        var prevSpan = null;
        if (current){
            prevSpan = current.previousElementSibling;
            while(prevSpan && prevSpan.nodeName !== 'SPAN'){
                prevSpan = prevSpan.previousElementSibling;
            }
        }
        if (current) current.classList.remove('_er-current');
        if (prevSpan) prevSpan.classList.add('_er-current');
    }

    function updateReadPos(){
        if ($('._er-current'))
            $('._er').scrollTop =  $('._er-current').offsetTop - (document.documentElement.clientHeight / 2);
    }

    function setFontSize(){
        localStorage["_er_fontsize"] = fontsize;
        $('._er-title').style.fontSize = (20+fontsize) + 'px';
        $('._er-title').style.lineHeight = ((20+fontsize)*1.5) + 'px';
        $('._er-content').style.fontSize = (14+fontsize) + 'px';
        $('._er-content').style.lineHeight = ((14+fontsize)*1.5) + 'px';
    }

    var oldOverflow = '';
    var oldOnKeyDown = $('body').onkeydown;

    function removeDom(){
        $('._er').remove();
        $('body').style.overflow = oldOverflow;
        $('body').onkeydown = oldOnKeyDown;
    }

    function addClassAndDom(){
        oldOverflow = $('body').style.overflow;
        $('body').style.overflow = 'hidden';

        $('body').children[0].insertAdjacentHTML('beforeBegin',
            '<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no"><div class="_er">' +
            '    <div class="_er-tts">' +
            '        <button type="button" id="_er-tts">听书</button>' +
            '    </div>' +
            '    <div class="_er-tools">' +
            '        <button type="button" id="_er-pageindex">目录</button>' +
            '        <button type="button" id="_er-font-plus">字体+</button>' +
            '        <button type="button" id="_er-font-minus">字体-</button>' +
            '        <button type="button" id="_er-close">返回原网页</button>' +
            '    </div>' +
            '    <div class="_er-title"></div>' +
            '    <div class="_er-content">' +
            '    </div>' +
            '    <div class="_er-tools">' +
            '        <button type="button" id="_er-pageup">上一页</button>' +
            '        <button type="button" id="_er-pagedown">下一页</button>' +
            '    </div>' +
            '</div>');
        $('body').children[0].insertAdjacentHTML('beforeBegin',
            '<style>' +
            '._er{' +
            '    position: fixed;' +
            '    left: 0;' +
            '    right: 0;' +
            '    top: 0;' +
            '    bottom: 0;' +
            '    overflow: auto;' +
            '    background-color: white;' +
            '    z-index: 201901272211;' +
            '}' +
            '._er-title{' +
            '    text-align: center;' +
            '    font-size: 20px;' +
            '    line-height: 30px;' +
            '    font-weight: 900;' +
            '    padding: 10px 10%;' +
            '    color: black;' +
            '}' +
            '._er-content{' +
            '    padding: 10px 10%;' +
            '    font-size: 14px;' +
            '    line-height: 21px;' +
            '    color: black;' +
            '}' +
            '._er-tools{' +
            '    margin-top: 10px;' +
            '    margin-bottom: 10px;' +
            '    text-align: center;' +
            '}' +
            '._er-tools button{' +
            '    cursor: pointer;' +
            '    color: green;' +
            '    border: 1px solid black;' +
            '    padding: 5px;' +
            '    border-radius: 10px;' +
            '}' +
            '._er-tts button{' +
            '    width: 50px;' +
            '    height: 50px;' +
            '    position: fixed;' +
            '    right: 15px;' +
            '    bottom: 15px;' +
            '    z-index: 201901272212;' +
            '    color: green;' +
            '    border: 1px solid black;' +
            '    opacity: 0.5;' +
            '    cursor: pointer;' +
            '    border-radius: 25px;' +
            '    display: none;' +
            '}' +
            '._er-current{' +
            '    background-color: yellow;' +
            '}' +
            '</style>');
    }
})();