💡WebPreview - 信息直达

支持搜索引擎的搜索结果快速预览。点击搜索结果旁的小灯泡按钮,即可在速览窗中快速查看目标网站所含图片、链接、标题大纲、文本。

当前为 2024-08-11 提交的版本,查看 最新版本

// ==UserScript==
// @name         💡WebPreview - 信息直达
// @namespace    https://ez118.github.io/
// @version      1.6.2
// @description  支持搜索引擎的搜索结果快速预览。点击搜索结果旁的小灯泡按钮,即可在速览窗中快速查看目标网站所含图片、链接、标题大纲、文本。
// @author       ZZY_WISU
// @match        *://*/*
// @connect      *
// @license      GNU GPLv3
// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAYAAAAe2bNZAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAKVJREFUeJztlskNwCAMBGkkbaa/9JMi8orEg8P2+tgHlngRZgfEkdbAuu7n/RvKguvIlMhooVaZ7fdZSy7OiRZS86OEzNzVgB46axauuiQiKUddEpYipA0IE0LuEnchBOgqg87OdXU8QGLG7khGyQxzqWQsIA8ZCESxgXtg9tglkOLSs4Bp3qeSl7tUhE6mD9T2hcqU/eXRy2j7jszZMxQyMzGU9QGdyuqQn9XecwAAAABJRU5ErkJggg==
// @run-at       document-end
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        window.onurlchange
// @require      https://update.greasyfork.org/scripts/503290/1426017/ultra-slim-jquery.js
// @require      https://unpkg.com/[email protected]/dist/turndown.js
// @require      https://unpkg.com/[email protected]/marked.min.js
// ==/UserScript==

var iconImg = "data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjIwcHgiIGhlaWdodD0iMjBweCI+PHBhdGggZD0iTTkgMjFjMCAuNS40IDEgMSAxaDRjLjYgMCAxLS41IDEtMXYtMUg5em0zLTE5QzguMSAyIDUgNS4xIDUgOWMwIDIuNCAxLjIgNC41IDMgNS43VjE3YzAgLjUuNCAxIDEgMWg2Yy42IDAgMS0uNSAxLTF2LTIuM2MxLjgtMS4zIDMtMy40IDMtNS43IDAtMy45LTMuMS03LTctNyIgZmlsbD0iIzM4NmExZiI+PC9wYXRoPjwvc3ZnPg==";
/* 用于存储小灯泡按钮的图片数据 */

const contentEleSelList = {
    "blog.csdn.net": "#article_content",
    "zhuanlan.zhihu.com": ".Post-RichTextContainer",
    "jingyan.baidu.com": "#format-exp",
    "www.bilibili.com": "#article-content",
    "zhidao.baidu.com": "#qb-content",
    "www.cnblogs.com": "#topics",
    "www.sohu.com": "#mp-editor"
}; /* 储存特定网站内容优化数据(文章主体的父元素) */

const VideoSupport = [
	["https://v.youku.com/v_show/*.html", "https://player.youku.com/embed/*"],
	["https://v.qq.com/x/page/*.html", "https://v.qq.com/txp/iframe/player.html?vid=*"],
	["https://www.bilibili.com/video/BV*/", "https://www.bilibili.com/blackboard/html5mobileplayer.html?bvid=*"],
	["https://www.bilibili.com/video/av*/", "https://www.bilibili.com/blackboard/html5mobileplayer.html?aid=*"]
]; /* 储存支持预览播放视频的网站及其嵌入播放器链接 */



/* =====[ 文章大纲提取 ]====== */
/* 标题元素 */
function titleElements(tag, title, level, id) {
    this.tag = tag;
    this.title = title;
    this.level = level;
    this.id = id;
}

/* 生成大纲 */
function getOutline(markdown) {
    var lines = markdown.split('\n');
    var titleElementArr = [];
    var preTitleElement = null;

    lines.forEach(line => {
        const match = line.match(/^(#{1,6})\s+(.*)/);
        if (match) {
            var id = Math.random().toString(36).substr(2, 7);
            var level = 1;
            var tag = match[1].length;
            var title = match[2];

            if (preTitleElement != null) {
                var tagPre = preTitleElement.tag;
                var levelPre = preTitleElement.level;

                if (tagPre > tag) { level = levelPre - (tagPre - tag); }
                else if (tagPre < tag) { level = levelPre + 1; }
                else { level = levelPre; }
            }

            if (title.trim().length > 0) {
                var titleElement = new titleElements(tag, title, level, id);
                titleElementArr.push(titleElement);
                preTitleElement = titleElement;
            }
        }
    });

    return titleElementArr;
}
/* =========================== */



function runAsync(url,send_type,data_ry) {
    var p = new Promise((resolve, reject)=> {
        GM_xmlhttpRequest({
            method: send_type, url: url, headers: {"Content-Type": "application/x-www-form-urlencoded;charset=utf-8"}, data: data_ry,
            onload: function(response){resolve(response.responseText);}, onerror: function(response){reject("请求失败");}
        });
    });
    return p;
}

function JudgeVideoSupport(url) {
	/* 是否为支持预览视频的网站 */
	var previewFlag = 0;
	for(let i = 0; i < VideoSupport.length; i ++){
		if( url.includes( VideoSupport[i][0].split("*")[0] ) ){
			return { "state":true, "data":i };
			break;
		}
	}

	return { "state":false, "data":-1 };
}

function getWebContents(txt) {
    var links = [];
    var images = [];
    var content = "";
    var outline = [];

    /* 获取所有链接 */
    txt.replace(/<a [^>]*href=['"]([^'"]+)['"][^>]*>/g, function(match, capture){
        links.push(capture);
    });

    /* 获取所有图片 */
    txt.replace(/<img [^>]*src=['"]([^'"]+)['"][^>]*>/g, function(match, capture){
        images.push(capture);
    });

    /* 去掉影响转换的标签 */
    var markdown = txt.replace(/<script.*?>.*?<\/script>/gis, "")
        .replace(/<style.*?>.*?<\/style>/gis, "")
        .replace(/<nav.*?>.*?<\/nav>/gis, "");


    /* html转markdown */
    const turndownService = new TurndownService();
    markdown = turndownService.turndown(markdown);

    /* markdown转html */
    content = marked.parse(markdown);

    /* 获取大纲信息 */
    try{ outline = getOutline(markdown);}
    catch{ console.log("[ERROR] 大纲处理问题") }

    var final_data = {"link": links, "image": images, "content": content, "outline": outline};

    return final_data;
}

function openReader(url) {
    /* 打开阅读器 */

    /* 阅读器加载提示 */
    var closeBtn = $("#userscript-closeBtn");
    var previewReader = $("#userscript-webPreviewReader");
    previewReader.html("<p style='font-size:22px;margin-top:33%;' align='center'>正在载入...<br/><span>" + url + "</span></p>");

    previewReader.show();
    closeBtn.show();

    /* 判断当前链接是支持预览的视频网站,并作出对应处理 */
    var SoN = JudgeVideoSupport(url);
    if(SoN.state == true){
        /* 被支持的视频网站的处理 */
        var origUrl = url;
        var frameUrl = "";

        url = url.replace(VideoSupport[SoN.data][0].split("*")[0], "");
		url = url + "?#";
		url = url.split("#")[0].split("?")[0];
		url = url.replace(VideoSupport[SoN.data][0].split("*")[1], "");

        frameUrl = VideoSupport[SoN.data][1].replace("*", url);

        previewReader.html(`
            <div id="FadeInContainer" style="display:none;">
                <div style="height:48px;"></div>
	        	<center style="height: calc(100% - 120px)">
	        		<iframe id="videoFrame" style="min-height:300px;" src="` + frameUrl + `"></iframe>
	    	    </center>
	    	    <br>

	        	<a href="` + origUrl + `" class="link" id="GoToLink" target="_blank">在原网站中继续 &nbsp; ▶ </a><br/>
                <a href="` + frameUrl + `" class="link" id="GoToLink" target="_blank">在播放器中继续 &nbsp; ▶ </a>
            </div>
        `);

        $("#FadeInContainer").show();
    } else {
        /* 普通网站的处理 */
        runAsync(url, "GET", "").then((result)=>{ return result; }).then(function(result){
            /* 源数据处理(csdn存在利用img的onerror属性注入xss脚本的行为) */
            result = result.replace(/<img\s+[^>]*src\s*=\s*["']{2}[^>]*>/gi, ''); /* 删除src为空的标签 */
            result = result.replace(/<img([^>]*)onerror\s*=\s*(['"]?[^'">]*['"]?)([^>]*)>/gi, '<img$1$3>'); /* 删除所有img标签的onerror属性 */

            /* 对指定网站进行内容过滤,指定元素获取 */
            let orig_result_backup = result;
            const domain = url.split("/")[2];
            if (contentEleSelList[domain]) {
                try {
                    const selector = contentEleSelList[domain];
                    result = $(result).find(selector).html();
                } catch (e) { console.log("[ERROR] 特定网站处理问题") }
            }
            if (!result) { result = orig_result_backup; }

            /* 调用解析网页 */
            let reslist = getWebContents(result);
            let linkhtml = "", imghtml = "", outlinehtml = "";

            /* 处理链接列表 */
            for(let i = 0; i < reslist.link.length; i ++){
                let link_tmp = reslist.link[i];
                if(link_tmp.includes("//")){
                    linkhtml += "<a class='link' target='_blank' href='" + link_tmp + "'> 🔗&nbsp;" + link_tmp + " </a><br>";
                }
            }

            /* 处理图片列表 */
            for(let i = 0; i < reslist.image.length; i ++){
                imghtml += "<a href='" + reslist.image[i] + "' target='_blank'><img class='image' src='" + reslist.image[i] + "' onerror='this.remove()'/></a>";
            }

            /* 处理大纲 */
            for(let i = 0; i < reslist.outline.length; i ++){
                let space = "";
                for(let j = 1; j < reslist.outline[i].level; j ++){ space += "&emsp;&emsp;"; }
                outlinehtml += space + "+&nbsp;" + reslist.outline[i].title + "<br/>"
            }

            /* 将所有结果添加进阅读器,并显示 */
            previewReader.html(`
                <div id="FadeInContainer" style="display:none;">
                    <div style="height:48px;"></div>
                    <div class="ImageList" style="max-height:103px;">
                        <p class='ShowList' align='right' style='' onclick='this.parentNode.setAttribute("style", "");'>展开</p>
                        ` + imghtml + `
	            	</div>

	    	        <div class="LinkList" style="max-height:286px;">
                        <p class='ShowList' align='right' style='' onclick='this.parentNode.setAttribute("style", "");'>展开</p>
                        ` + linkhtml + `
	        	    </div>

                    <div class="OutlineShow">
	            		<b>大纲: </b><br/>
                        ` + outlinehtml + `
	        	    </div>

	            	<div class="ContentShow">
	            		<b>文本: </b><br/>
                        ` + reslist.content + `
	        	    </div>
                </div>
            `);

            /* 隐藏不存在的项 */
            if(reslist.image.length == 0) { $(".ImageList").hide(); }
            if(reslist.link.length == 0) { $(".LinkList").hide(); }
            if(reslist.outline.length == 0) { $(".OutlineShow").hide(); }

            $("#FadeInContainer").show();
        });
    }
    /* 执行结束 */
}




/* ======[ 搜索结果分析 ]===== */
/*
  * 自动判断当前元素下是否 具有搜索结果特征
  * 解释:判断一个父元素下存在 大于等于5个的 具有相同class的 子元素。
  *       该函数用于统计当前元素的子元素的各个class的数量,若其中存在一个class的数量大于5次,则判断为搜索结果
  */
function checkSearchResults(parentElement) {
    var classList = [];
    var countList = [];
    for(let i = 0; i < parentElement.children.length; i ++) {
        var child = parentElement.children[i];
        var childClass = child.classList;
        for(let j = 0; j < childClass.length; j ++) {
            if(classList.indexOf(childClass[j]) !== -1) {
                /* 对列表中的class出现次数进行计数 */
                var p = classList.indexOf(childClass[j]);
                countList[p] += 1;
            } else {
                /* 对列表中未出现的class,插入列表 */
                classList.push(childClass[j]);
                countList.push(0);
            }
        }
    }
    var countMax = Math.max.apply(null, countList);
    return (countMax >= 5);
}

/* 遍历元素 */
function traverseElements(element, callback) {
    if (!element || !element.children || element.children.length === 0) {
        return;
    }

    var returnCode = callback(element);
    if (returnCode) { return; }
    /* 如果返回值为true,则代表该元素已包含搜索结果,无需继续遍历 */

    for (let i = 0; i < element.children.length; i++) {
        traverseElements(element.children[i], callback);
    }
}

/*
 * 运用上述的 遍历函数 和 分析判断函数 实现在满足要求的搜索结果旁插入“速览按钮”
 * 解释:遍历DOM,获取搜索列表,插入按钮
 *       该函数是全程序 分析部分 的起始
 */
function initAnalyze() {
    traverseElements(document.body, function(element) {
        var status = checkSearchResults(element);
        if(status) {
            console.log("存在搜索结果:", status);
            let resultItems = element.children;
            for(let i = 0; i < resultItems.length; i ++) {
                try {
                    let resultItemLink = resultItems[i].getElementsByTagName("a")[0].href;
                    let resultItemTitleEle = resultItems[i].getElementsByTagName("a")[0].parentNode;
                    let resultItemText = resultItems[i].getElementsByTagName("a")[0].innerText;

                    if(resultItemText.length <= 5 || !resultItemLink){ continue; }
                    if(resultItemLink.includes("javascript:") && resultItemLink[0] == "j") { continue; }

                    /* 向每一个搜索结果的标题部分添加按钮 */
                    let previewBtn = document.createElement("button");
                    let previewBtnImg = document.createElement("img");
                    previewBtn.setAttribute("class", "userscript-webPreviewBtn");
                    previewBtn.setAttribute("link-data", resultItemLink);
                    previewBtnImg.setAttribute("src", iconImg);
                    resultItemTitleEle.appendChild(previewBtn);
                    previewBtn.appendChild(previewBtnImg);

                    previewBtn.addEventListener("click", function(evt){
                        let linkData = previewBtn.getAttribute("link-data");
                        openReader(linkData);
                    }, true);
                } catch(e) {
                    //console.log("[ERROR] ELE(" + i + ") \n" + e);
                }
            }

            return true;
        } else {
            return false;
        }
    });
}
/* =========================== */


function init(){
    /* 初始化 */

    /* 插入样式 */
    GM_addStyle(`
        :root{--bg-color:#FFFFFFAA;--text-color:#386a1f;--border-color:#285a0f;--hover-bg-color:#edf1e5;--active-bg-color:#d7e1cd;--close-btn-bg:#386a1f;--close-btn-text:#FFF;--reader-bg:#fdfdf6;--reader-text-color:#131f0d;--link-color:#386a1f;--link-hover:#487631;--pre-bg-color:#eeeee8;--pre-border-color:#dee5d8;--code-bg-color:#e2e3dd}
        @media (prefers-color-scheme:dark){:root{--bg-color:#00390a55;--text-color:#7edb7b;--border-color:#7edb7b;--hover-bg-color:#00390aAA;--active-bg-color:#7edb7b;--close-btn-bg:#7edb7b;--close-btn-text:#00390a;--reader-bg:#1a1c19;--reader-text-color:#e2e3dd;--link-color:#7edb7b;--link-hover:#76cd74;--pre-bg-color:#1e201d;--pre-border-color:#424940;--code-bg-color:#42494047}}

        .userscript-webPreviewBtn{user-select:none;background-color:var(--bg-color);color:var(--text-color);padding:6px 14px;font-weight:bold;line-height:16px;height:30px;margin-left:5px;border-radius:30px;border:1px solid var(--border-color);cursor:pointer}
        .userscript-webPreviewBtn:hover{background-color:var(--hover-bg-color)}
        .userscript-webPreviewBtn:active{background-color:var(--active-bg-color)}
        .userscript-webPreviewBtn img{height:16px}
        .userscript-closeBtn{position:fixed;top:calc(8% + 5px);right:26px;z-index:100000;background:var(--close-btn-bg);color:var(--close-btn-text);padding:8px 20px;margin:6px;border-radius:30px;font-weight:bold;border:0;border-bottom:1px solid var(--border-color);cursor:pointer}
        .userscript-closeBtn:hover{background:var(--link-hover)}
        .userscript-webPreviewReader{font-size:medium;text-align:left;position:fixed;top:8vh;right:10px;bottom:0px;z-index:99999;width:35%;height:calc(100vh - 8%);min-width:340px;background:var(--reader-bg);color:var(--reader-text-color);overflow:hidden;box-shadow:0 0 0 1px rgba(0,0,0,.1),0 2px 4px 1px rgba(0,0,0,.18);border-radius:28px 28px 0px 0px}
        .userscript-webPreviewReader .ShowList{margin:0;padding:0;width:100%;cursor:pointer;color:var(--link-color);user-select:none}
        .userscript-webPreviewReader .image{height:85px;margin-bottom:8px;margin-right:5px;border-radius:15px;object-fit:contain;max-width:calc(100% - 20px)}
        .userscript-webPreviewReader .link{text-decoration:none;color:var(--link-color) !important;margin-left:5px}
        .userscript-webPreviewReader .link:hover{text-decoration:underline}
        .ImageList,.LinkList,.OutlineShow,.ContentShow{padding:16px;margin:8px;background:var(--code-bg-color);border-radius:30px;overflow:hidden;color:var(--reader-text-color);box-shadow:0 .5px 1.5px 0 rgba(0,0,0,.19),0 0 1px 0 rgba(0,0,0,.039)}
        .ContentShow img{max-width:90% !important;position:relative !important;top:0 !important;left:0 !important;border-radius:10px}
        .ContentShow a{color:var(--link-color);text-decoration:underline 1px solid var(--link-hover);margin:0px 3px}
        .ContentShow code{font-family:Consolas,Courier,Courier New,monospace}
        .ContentShow pre{color:var(--reader-text-color);background:var(--pre-bg-color);width:90%;padding:5px;margin:5px 0px;overflow-y:auto;height:fit-content;border:1px solid var(--pre-border-color);border-radius:5px}
        .ContentShow code:not(pre code){color:var(--reader-text-color);background:var(--code-bg-color);border-radius:0.25rem;padding:.125rem .375rem;line-height:1.75;word-wrap:break-word;border:1px solid var(--pre-border-color)}
        .userscript-webPreviewReader #videoFrame{width:calc(100% - 10px);height:calc(100% - 0px);border:1px solid #CCC;border-radius:30px;margin:5px}
        .userscript-webPreviewReader #FadeInContainer{overflow-y:scroll;overflow-x:hidden;border-radius:15px 15px 0px 0px;width:100%;height:100%}
    `);


    /* 页面加载时插入DOM */
    /* 阅读器 */
    if( !$("#userscript-webPreviewReader").length ){
        var $previewReader = $('<div>', {
            class: 'userscript-webPreviewReader',
            style: 'display:none;',
            id: 'userscript-webPreviewReader'
        }).appendTo('body');

        var $closeBtn = $('<button>', {
            text: '关闭',
            class: 'userscript-closeBtn',
            id: 'userscript-closeBtn',
            style: 'display:none;'
        }).appendTo('body');

        $closeBtn.on('click', function() {
            $previewReader.hide(200);
            $closeBtn.hide();
        });
    }

    /* 隐藏阅读器 */
    $("#userscript-webPreviewReader").hide();

    /* 自动匹配搜索结果并插入按钮 */
    initAnalyze();

    return;
}


(function() {
    'use strict';

    init();

    window.addEventListener('urlchange', (info) => {
        if($("#userscript-webPreviewReader").length > 0 && $(".userscript-webPreviewBtn").length > 1) { return; }
        setTimeout(function(){
            init();
        }, 1600)
    });
})();