網頁閱讀模式

在任何需要的頁面中開啟閱讀模式,自動或手動選擇正文區域。

目前為 2017-01-20 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name                Page Read Mode
// @name:zh-CN          网页阅读模式
// @name:zh-TW          網頁閱讀模式
// @description         Content reader on any page, selecting the text area automatically or manually.
// @description:zh-CN   在任何需要的页面中开启阅读模式,自动或手动选择正文区域。
// @description:zh-TW   在任何需要的頁面中開啟閱讀模式,自動或手動選擇正文區域。

// @authuer             Moshel
// @namespace           https://hzy.pw
// @@homepageURL         https://hzy.pw/p/1364
// @supportURL          https://github.com/h2y/link-fix
// @icon                https://wiki.greasespot.net/images/f/f3/Book.png
// @license             GPL-3.0

// @include             *
// @grant               GM_setClipboard
// @@run-at              context-menu
// @require             https://cdn.staticfile.org/keymaster/1.6.1/keymaster.min.js

// @date                12/17/2015
// @modified            01/20/2016
// @version             1.0.0
// ==/UserScript==


/*
    global var
 */
let mode = 0,        //状态标记
    topNode = null,  //顶层节点
    styleNode = null,
    butNodes = null,
    zoomLevel = 1;


/*
    Tool functions
 */
function isNodeShow(node) {
    const styles = window.getComputedStyle(node);

    if(styles.display=='none' || styles.visibility=='hidden')
        return false;

    if(!parseInt(styles.height) || !parseInt(styles.height))
        return false;

    return true;
}


function nodeStyleInline(node) {
    let styleStr = '',
        styles = window.getComputedStyle(node);

    let keys = Object.keys(styles);
    for(let key of keys) {
        //if(key==='cssText')     continue;
        //if(parseInt(key)==key)  continue;
        /*if(/^(webkit|moz|ms)/.test(key))
            continue;

        if(styles[key]=='')     continue;*/

        let value = styles[key];
        key = changeStrStyle(key.replace('webkit','-webkit'));

        styleStr += key + ':' + value + ';';
    }

    node.className = '';
    node.id = '';
    node.style = styleStr;

    //child
    if(node.childElementCount)
        for(let child of node.children)
            nodeStyleInline(child);
}


// textAlign -> text-align
function changeStrStyle(str) {
    let chars = str.split('');

    for(let i=chars.length-1; i>=0; i--) {
        let ascii = chars[i].charCodeAt(0);
        if(ascii>=65 && ascii<91) {
            //A-Z
            chars[i] = '-' + String.fromCharCode(ascii+32);
        }
    }

    return chars.join('');
}


/*
    main functions
 */
function enterCliping(e) {
    mode = 1;
    e.preventDefault();

    //add style
    if(!styleNode) {
        styleNode = document.createElement('style');
        styleNode.innerHTML = `.cliper-top-node {
            box-shadow: 0 0 20px #777 !important;
            border:     3px solid red !important;
        } .read-mode-reading {
            position:   fixed   !important;
            z-index:    999970  !important;
            top:        0       !important;
            left:       0       !important;
            width:      100%    !important;
            height:     100%    !important;
            background-color: white   !important;
            overflow:         scroll  !important;
            padding:          0       !important;
            border:           0       !important;
            margin:           0       !important;
        } .read-mode-buts {
            position:   fixed;
            z-index:    999985;
            top: 2rem;  right: 1rem;
        } .read-mode-button {
            width:      54px;
            height:     54px;
            margin:     0 .5rem;
            padding:    10px 15px;
            color:      #fff;
            opacity:    .5;
            transition: 500ms;
            border-radius:      5px;
            background-color:   black;
        } .read-mode-button:hover {
            background-color:   white;
            border-radius:      0;
            box-shadow:         0 0 10px #000;
            color:              #000;
        }`;
        styleNode.id = 'read_mode';
        document.body.appendChild(styleNode);
    }

    //choose the init node
    topNode = document.body;
    let preNode = null;

    do {
        preNode = topNode;
        onDown(e);
    }while(preNode!=topNode && preNode.clientHeight*0.9 < topNode.clientHeight);
}

function quitCliping(e) {
    mode = 0;
    e.preventDefault();

    topNode.style.zoom = '';

    changeTopNode(null);

    if(butNodes)
        butNodes.style.display = 'none';

    topNode.classList.remove('read-mode-reading');
}


function buildButNodes() {
    butNodes = document.createElement('div');
    butNodes.className = 'read-mode-buts';

    let buts = [
        {
            text:    "Exit read mode",
            handler: quitCliping,
            icon:    'x'
        }, {
            text:    "Enlarge",
            handler: onEnlarge,
            icon:    '+'
        }, {
            text:    "Shrink",
            handler: onShrink,
            icon:    '-'
        }, {
            text:    "Save HTML data",
            handler: onSaveHTML,
            icon:    '↓'
        }
    ];

    for(let but of buts) {
        let newBut = document.createElement('a');
        newBut.className = 'read-mode-button';
        newBut.innerHTML = but.icon;
        newBut.title     = but.text;
        newBut.onclick   = but.handler;
        butNodes.appendChild(newBut);
    }

    document.body.appendChild(butNodes);
}


function changeTopNode(newNode) {
    if(topNode)
        topNode.classList.remove('cliper-top-node');

    if(newNode)
        newNode.classList.add('cliper-top-node');
    else
        return;

    topNode = newNode;

    //scroll
    var winH = window.screen.availHeight,
        winY = window.scrollY,
        domH = topNode.clientHeight,
        domY = topNode.getBoundingClientRect().top + winY;

    if(domH>winH)
        document.body.scrollTop = domY - 50;
    else
        document.body.scrollTop = domY - (winH-domH)/2;
}


/*
    Event handler
 */
function onEnlarge(e) {
    zoomLevel += .1;
    topNode.style.zoom = zoomLevel;
}
function onShrink(e) {
   zoomLevel -= .1;
   topNode.style.zoom = zoomLevel;
}


function onSaveHTML(e) {
    let htmlStr = '';

    let styleNodes = document.querySelectorAll('style, link[rel=stylesheet]');
    for(let node of styleNodes) {
        if(node.id == 'read_mode')
            continue;

        if(node.nodeName=="LINK")
            htmlStr += `<link rel="stylesheet" href="${node.href}">`;
        else
            htmlStr += node.outerHTML;
    }

    topNode.style.zoom = '';

    //TODO: node filter
    htmlStr += topNode.outerHTML
        .replace(/<style[^>]*>.*?<\/style>/ig, '')
        .replace(/<script[^>]*>.*?<\/script>/ig, '');

    window.GM_setClipboard(htmlStr);

    topNode.style.zoom = zoomLevel;
    alert('Copied into clipboard.');
}


function onUp(e) {
    if(!mode) return;
    e.preventDefault();

    if(topNode.parentElement)
        changeTopNode(topNode.parentNode);
}

function onDown(e) {
    if(!mode) return;
    e.preventDefault();

    if(!topNode.childElementCount)
        return;

    var scanNodes = topNode.children,
        maxNode = null;
    var maxHeight = -1;

    for(let node of scanNodes)
        if(isNodeShow(node) && node.clientHeight > maxHeight) {
            maxHeight = node.clientHeight;
            maxNode = node;
        }

    if(maxNode)
        changeTopNode(maxNode);
}

function onLeft(e) {
    if(!mode) return;
    e.preventDefault();

    let nowNode = topNode;
    for(let node=nowNode; node.previousElementSibling;) {
        node = node.previousElementSibling;
        if(isNodeShow(node)) {
            nowNode = node;
            break;
        }
    }

    if(nowNode!=topNode)
        changeTopNode(nowNode);
    //else: up
    else if (topNode.parentNode) {
        let bakNode = nowNode = topNode;

        onUp(e);
        nowNode = topNode;

        onLeft(e);
        if(nowNode==topNode)
            changeTopNode(bakNode);
        else
            onDown(e);
    }
}

function onRight(e) {
    if(!mode) return;
    e.preventDefault();

    let nowNode = topNode;
    for(let node=nowNode; node.nextElementSibling;) {
        node = node.nextElementSibling;
        if(isNodeShow(node)) {
            nowNode = node;
            break;
        }
    }

    if(nowNode!=topNode)
        changeTopNode(nowNode);
    //else: up
    else if (topNode.parentNode) {
        let bakNode = nowNode = topNode;

        onUp(e);
        nowNode = topNode;

        onRight(e);
        if(nowNode==topNode)
            changeTopNode(bakNode);
        else
            onDown(e);
    }
}


function onEnter(e) {
    if(!mode) return;
    e.preventDefault();

    quitCliping(e);

    topNode.classList.add('read-mode-reading');

    topNode.style.zoom = 1.2;
    zoomLevel = 1.2;

    //buttons
    if(butNodes)
        butNodes.style.display = '';
    else
        buildButNodes();
}


/*
    Main
 */
console.log(window.key);
window.key('alt+r', function(){
	console.log('reading');
	if(mode)      
		quitCliping(new MouseEvent("main"));
	else         
		enterCliping(new MouseEvent("main"));	
});


/*
    bind action
 */
window.key('up', onUp);
window.key('down', onDown);
window.key('left', onLeft);
window.key('right', onRight);

window.key('enter', onEnter);
window.key('esc', quitCliping);