💡WebPreview - 信息直达

支持国内主流搜索引擎的搜索结果快速预览(直达网页大纲)。只需点击搜索结果旁的小灯泡按钮,即可在右侧速览窗中快速查看目标网站所含的图片、链接、标题大纲、文本。

目前为 2024-03-11 提交的版本。查看 最新版本

// ==UserScript==
// @name         💡WebPreview - 信息直达
// @namespace    https://ez118.github.io/
// @version      0.15
// @description  支持国内主流搜索引擎的搜索结果快速预览(直达网页大纲)。只需点击搜索结果旁的小灯泡按钮,即可在右侧速览窗中快速查看目标网站所含的图片、链接、标题大纲、文本。
// @author       ZZY_WISU
// @match        https://*.bing.com/*
// @match        https://www.google.com/*
// @match        https://www.baidu.com/*
// @match        https://www.so.com/*
// @match        https://www.sogou.com/*
// @connect      *
// @license      GNU GPLv3
// @icon         https://cn.bing.com/sa/simg/favicon-trans-bg-blue-mg.ico
// @run-at document-end
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @require https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js
// ==/UserScript==


/* ============================================= */

/**
 * 生成随机数
 * @param len 长度
 * @constructor
 */
function randomString(len) {
    len = len || 32;
    /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/
    var $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
    var maxPos = $chars.length;
    var pwd = '';
    for (let i = 0; i < len; i++) { pwd += $chars.charAt(Math.floor(Math.random() * maxPos)); }
    return pwd;
}
/**
 * 标题元素
 * @param tag 标签,h1 ==> 1 , h2 ==> 2
 * @param title 标题
 * @param level 层级
 * @param id 随机生成的id
 * @constructor
 */
function TitleElement(tag, title, level, id) {
    this.tag = tag;
    this.title = title;
    this.level = level;
    this.id = id;
}
/* 是否是标题元素 */
function isTitleTag(tag) {
    if (tag.is("h1")) { return true }
    if (tag.is("h2")) { return true }
    if (tag.is("h3")) { return true }
    if (tag.is("h4")) { return true }
    if (tag.is("h5")) { return true }
    if (tag.is("h6")) { return true }
    if (tag.is("h7")) { return true }
    return false
}
/**
 * 生成大纲
 * @param $articleContent 文章容器
 * @constructor
 */
function getOutline($articleContent) {
    /** 全部元素 */
    var $eles = $articleContent.find("*");
    /** 标题元素列表 */
    var titleElementArr = new Array();
    /** 上一个元素 */
    var preTitleElement = null;

    $.each($eles, function(index, item) {
        if (isTitleTag($(item))) {
            var id = randomString(20);
            var level = 1;
            var tag = parseInt($(item).get(0).tagName.replace('h', "").replace('H', ""));
            var title = $(item).text();

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

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

            }

            if (title.trim().length > 0) {
                $(item).attr("id", id);
                var titleElement = new TitleElement(tag, title, level, id);
                titleElementArr.push(titleElement);
                preTitleElement = titleElement;
            }
        }
    })
    console.log(titleElementArr);
    return titleElementArr;
}
/* ============================================= */



var ReaderFlag1 = false; /* 用于存储阅读器元素是否被创建 */
var ReaderFlag2 = false; /* 用于存储阅读器是否为开启状态 */
var VideoSupport = [
	["https://v.youku.com/v_show/*.html", "https://player.youku.com/embed/*"],
	["https://v.qq.com/x/page/*.html", "http://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 stringToHtml(str) {
	var parser = new DOMParser();
    var doc = parser.parseFromString(str, 'text/html');
    return doc;
}

function mobileDevice(){
    var info = navigator.userAgent;
    var isPhone = /mobile/i.test(info);
    return isPhone;
}

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 escapeHtml(str) {
    return str.replace(/[&<>"']/g, function(match) {
        switch (match) {
           case '&':
              //return '&amp;';
              return '&';
           case '<':
              return '&lt;';
           case '>':
              return '&gt;';
           case '"':
              return '&quot;';
           case '\'':
              return '&#39;';
           default:
              return '';
        }
    });
}

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;
    /* 获取所有链接 */
    /*links = txt.match(/href\=\"(https?|http|ftp|file):\/\/[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]/g);
    for(let i = 0; i < links.length; i ++) {
        links[i] = links[i].replace("href=\"", "");
    }*/
    links = [];
    txt.replace(/<a [^>]*href=['"]([^'"]+)[^>]*>/g,function(match, capture){
        links.push(capture);
    });
    //console.log(links);

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

    /* 获取文本 */
    content = txt.replace(/<\/div>/g, "</div>\n").replace(/<\/table>/g, "</table>\n")
    content = content.replace(/<\/h3>/g, "</h3>\n").replace(/<\/p>/g, "</p>\n")
    content = content.replace(/<\/li>/g, "</li>\n").replace(/<br>/g, "\n")
    content = content.replace(/<br\/>/g, "\n").replace(/<script(.*?)<\/script>/gis, "")
    content = content.replace(/<style(.*?)<\/style>/gis, "").replace(/<nav(.*?)<\/nav>/gis, "");

    // 现在代码:
    //content = content.replace(/<(?!\/?a\b)[^>]+>/g, ''); /* 删除除了a以外的标签 */
    content = content.replace(/<(?!\/?(a|img)\b)[^>]+>/g, ''); /* 删除除了a和img以外的标签 */

    // 原先代码:
    //content = content.replace(/<[^>]+>/g, ""); /* 删除所有标签 */
    //content = escapeHtml(content); /* 转义 */

    var content_len = content.length;
    content = content.substring(0, content_len);
    const search_txt = ["  ", "  ", "\n\n", "\r\r", "\t\t", "\n\r\n\r", "\r\n\r\n", "\t\r\n"];
    const replace_txt = ["", "", "", "", "", "", "", ""];
    for (let i = 0; i < search_txt.length; i++) {
        content = content.split(search_txt[i]).join(replace_txt[i]);
    }
    
    content = content.replace(/\r\n/g,"<br/>").replace(/\n/g,"<br/>").replace(/<br\/><br\/><br\/>/g, "");
    //console.log(content);

    outline = getOutline($(txt));

    return {"link":links,"image":images,"content":content,"outline":outline};
}

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

    /* 判断阅读器元素是否已经被创建过 */
    if (ReaderFlag1 != true) {
        /* 若元素不存在,则创建元素 */
        let previewReader = document.createElement("div");
        previewReader.setAttribute("class", "userscript-webPreviewReader"); previewReader.setAttribute("style", "display:block;"); previewReader.setAttribute("id", "userscript-webPreviewReader");
        document.body.appendChild(previewReader);
        ReaderFlag1 = true; ReaderFlag2 = true;
    } else {
        /* 若存在,则显示元素 */
        let previewReader = document.getElementById("userscript-webPreviewReader");
        previewReader.setAttribute("style", "display:block;");
        ReaderFlag2 = true;
    }

    /* 阅读器加载提示 */
    var previewReader = document.getElementById("userscript-webPreviewReader");
    previewReader.innerHTML = "<p style='font-size:22px;margin-top:33%;' align='center'>加载中...<br/><span>" + url + "</span></p>";

    /* 判断当前链接是支持预览的视频网站,并作出对应处理 */
    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.innerHTML = `
        <div id="FadeInContainer" style="display:none; height:100%;">
	    	<button class="CloseButton" style="margin:5px;" onclick='this.parentNode.parentNode.setAttribute("style", "display:none;");'>关闭</button>

	    	<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").fadeIn(700);
    } else {
        /* 普通网站的处理 */
        runAsync(url, "GET", "").then((result)=>{ return result; }).then(function(result){
            var rh;
            if (url.split("/")[2] == "blog.csdn.net"){
                try{ result = stringToHtml(result).getElementById("article_content").innerHTML; }catch(e){}
            }else if (url.split("/")[2] == "zhuanlan.zhihu.com"){
                try{ result = stringToHtml(result).getElementsByClass("Post-RichTextContainer")[0].innerHTML; }catch(e){}
            }else if (url.split("/")[2] == "jingyan.baidu.com"){
                try{ result = stringToHtml(result).getElementById("format-exp").innerHTML; }catch(e){}
            }else if (url.split("/")[2] == "www.bilibili.com"){
                try{ result = stringToHtml(result).getElementById("article-content").innerHTML; }catch(e){}
            }

            /* 用函数解析网页 */
            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("https://") || link_tmp.includes("http://")*/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.innerHTML = `
            <div id="FadeInContainer" style="display:none;">
                <button class="CloseButton" onclick='this.parentNode.parentNode.setAttribute("style", "display:none;");'>关闭</button>
                <br>
                <br>
                <div class="ImageList" style="max-height:103px;">
                    <p class='ShowImgList' align='right' style='' onclick='this.parentNode.setAttribute("style", "");'>所有图片</p>
                    ` + imghtml + `
	        	</div>

		        <div class="LinkList" style="max-height:286px;">
                    <p class='ShowImgList' align='right' style='' onclick='this.parentNode.setAttribute("style", "");'>所有链接</p>
                    ` + linkhtml + `
	        	</div>

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

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

            /* 淡入 */
            $("#FadeInContainer").fadeIn(250);
        });
    }
    /* 执行结束 */
}

(function() {
    'use strict';
    var url = window.location.href;
    var paths = url.split("/");

    GM_addStyle(`.userscript-webPreviewBtn{ background:#FFF; padding:3px 13px; margin-left:5px; border-radius:10px; border:2px solid #555; cursor:pointer; }
                 .userscript-webPreviewBtn:active{ background:#111; border:2px solid #AAA; }
                 .userscript-webPreviewReader{ position:fixed; top:8%; right:10px; bottom:0px; z-index:9999; width:35%; height:calc(100% - 8%); min-width:350px; background:#FFF; color:#333; overflow:hidden; box-shadow:0px 2px 5px #000; border-radius:15px 15px 0px 0px; }
                 .userscript-webPreviewReader img.error{ display:none; }
                 .ShowImgList{ margin:0;padding:0;width:100%;cursor:pointer;color:#138AF1;user-select:none; }
                 .image{ height:85px; margin-bottom:8px; margin-right:5px; border-radius:15px; object-fit:contain; max-width:calc(100% - 20px); }
                 .link{ text-decoration:none; color:#001ba0; margin-left: 5px; }
                 .link:hover{ text-decoration:underline; }
                 .ImageList{ padding:10px; margin:5px; border:2px solid #f4effb; background:#f4effb; border-radius:15px; overflow:hidden; }
                 .LinkList{ padding:10px; margin:5px; border:2px solid #f4f1f6; background:#f4f1f6; border-radius:15px; overflow:hidden; }
                 .ContentShow{ padding:10px; margin:5px; border:2px solid #f2f2f2; background:#f2f2f2; border-radius:15px; }
                 .ContentShow img{ max-width:90%!important; position:relative!important; top:0!important; left:0!important; }
                 .ContentShow img::after{ content: ""; display: block; clear: both; }
                 .CloseButton{ position:fixed; top:calc(8% + 5px); right:15px; background:#FFF; color:#000; padding:3px 13px; margin:5px 5px; margin-bottom:0px; border-radius:10px; border:2px solid #555; cursor:pointer; }
                 .CloseButton:hover{ background:#138AF1; border:2px solid #106ebe; color:#FFF; }
                 #videoFrame{ width: calc(100% - 10px); height: calc(100% - 0px); border:1px solid #CCC; border-radius:15px; }

                 #FadeInContainer { overflow-y:auto; overflow-x:hidden; border-radius:15px 15px 0px 0px; width:100%; height:100%; }
                 `);

    if (paths[2].includes("bing.com") && paths[3].includes("search")) {
        let resultItems = document.getElementsByClassName("b_algo");
        for(let i = 0; i < resultItems.length; i ++) {
            let resultItemLink = resultItems[i].getElementsByTagName("a")[0].href;
            let resultItemTitleEle = resultItems[i].getElementsByTagName("h2")[0];

            /* 向每一个搜索结果的标题部分添加按钮 */
            let previewBtn = document.createElement("button");
            let previewBtnTxt = document.createTextNode("💡");
            previewBtn.setAttribute("class", "userscript-webPreviewBtn");
            previewBtn.setAttribute("link-data", resultItemLink);
            previewBtn.appendChild(previewBtnTxt);
            /* 适配必应手机网页 */
            if(mobileDevice()){ resultItemTitleEle.parentNode.parentNode.appendChild(previewBtn); }
            else { resultItemTitleEle.appendChild(previewBtn); }

            previewBtn.addEventListener("click", function(evt){
                let linkData = previewBtn.getAttribute("link-data");
                openReader(linkData);
            }, true);
        }
    } else if (paths[2].includes("google.com")) {
        let resultItems = document.getElementsByClassName("MjjYud");
        for(let i = 0; i < resultItems.length; i ++) {
            try{
                let resultItemLink = resultItems[i].getElementsByTagName("a")[1].getAttribute("href");
                let resultItemTitleEle = resultItems[i].getElementsByTagName("h3")[0];

                if(resultItemLink == null || resultItemLink == "" || resultItemLink == undefined){ continue; }

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

                previewBtn.addEventListener("click", function(evt){
                    let linkData = previewBtn.getAttribute("link-data");
                    openReader(linkData);
                }, true);
            } catch(e) {
                console.log("[ERROR] ELE(" + i + ")");
            }
        }
    } else if (paths[2].includes("baidu.com") && paths[3].includes("s?")) {
        let resultItems = document.getElementsByClassName("c-container");
        //let resultItems = document.getElementsByClassName("c-container new-pmd");
        for(let i = 0; i < resultItems.length; i ++) {
            try{
                let resultItemLink = resultItems[i].getAttribute("mu");
                let resultItemTitleEle = resultItems[i].getElementsByTagName("h3")[0];

                if(resultItemLink == null || resultItemLink == "" || resultItemLink == undefined){ continue; }

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

                previewBtn.addEventListener("click", function(evt){
                    let linkData = previewBtn.getAttribute("link-data");
                    openReader(linkData);
                }, true);
            } catch(e) {
                console.log("[ERROR] ELE(" + i + ")");
            }
        }
    } else if (paths[2].includes("so.com") && paths[3].includes("s?")) {
        let resultItems = document.getElementsByClassName("res-list");
        for(let i = 0; i < resultItems.length; i ++) {
            try{
                let resultItemLink = resultItems[i].getElementsByTagName("a")[0].getAttribute("data-mdurl");
                let resultItemTitleEle = resultItems[i].getElementsByTagName("h3")[0];

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

                previewBtn.addEventListener("click", function(evt){
                    let linkData = previewBtn.getAttribute("link-data");
                    openReader(linkData);
                }, true);
            } catch(e) {
                console.log("[ERROR] ELE(" + i + ") \n" + e);
            }
        }
    } else if (paths[2].includes("sogou.com")) {
        let resultItems = document.getElementsByClassName("vrwrap");

        for(let i = 0; i < resultItems.length; i ++) {
            try{
                let resultItemLink = resultItems[i].getElementsByClassName("ext_query")[0].getAttribute("data-url");
                let resultItemTitleEle = resultItems[i].getElementsByTagName("h3")[0];

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

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