Greasy Fork

Greasy Fork is available in English.

💡WebPreview - 信息直达

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

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         💡WebPreview - 信息直达
// @namespace    https://ez118.github.io/
// @version      1.7.1
// @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.icu/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==

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 mediaPrevSupport = [
    {
        "site": "https://v.youku.com/v_show/*.html",
        "player": "https://player.youku.com/embed/*",
        "type": "video"
    },
    {
        "site": "https://v.qq.com/x/page/*.html",
        "player": "https://v.qq.com/txp/iframe/player.html?vid=*",
        "type": "video"
    },
    {
        "site": "https://www.bilibili.com/video/BV*/",
        "player": "https://www.bilibili.com/blackboard/html5mobileplayer.html?bvid=*",
        "type": "video"
    },
    {
        "site": "https://www.bilibili.com/video/av*/",
        "player": "https://www.bilibili.com/blackboard/html5mobileplayer.html?aid=*",
        "type": "video"
    },
    {
        "site": "https://www.youtube.com/watch?v=*",
        "player": "https://www.youtube.com/embed/*",
        "type": "video"
    },
    {
        "site": "https://music.163.com/#/song?id=*",
        "player": "https://music.163.com/outchain/player?type=2&id=*&auto=0&height=66",
        "type": "music"
    },
    {
        "site": "https://music.163.com/song?id=*",
        "player": "https://music.163.com/outchain/player?type=2&id=*&auto=0&height=66",
        "type": "music"
    },
    {
        "site": "https://open.spotify.com/track/*",
        "player": "https://open.spotify.com/embed/track/*",
        "type": "music"
    },
    {
        "site": "https://music.apple.com/cn/song/*",
        "player": "https://embed.music.apple.com/cn/album/*",
        "type": "music"
    },
    {
        "site": "https://music.youtube.com/watch?v=*",
        "player": "https://www.youtube.com/embed/*",
        "type": "music"
    }
]; /* 储存支持预览播放视频/预览试听音乐的网站及其嵌入播放器链接 */


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("[WebPrvw] 请求失败");}
        });
    });
    return p;
}

function judgeMediaSupport(url){
    var jflag = null;
    mediaPrevSupport.forEach(function(item, index) {
        if (url.includes(item.site.split("*")[0])) {
            jflag = { "state": true, "data": item };
        }
    });
    return jflag || { "state": false, "data": null };
}

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 = {
                    tag: tag,
                    title: title,
                    level: level,
                    id: id
                };
                titleElementArr.push(titleElement);
                preTitleElement = titleElement;
            }
        }
    });

    return titleElementArr;
}

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("[WebPrvw] 无法解析大纲") }

    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 showMedia = judgeMediaSupport(url);
    if(showMedia.state){
        /* 被支持的视频网站的处理 */
        var origUrl = url;
        var frameUrl = "";
        var mediaType = (showMedia.data.type == "video") ? "视频" : "音乐";

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

        frameUrl = showMedia.data.player.replace("*", url);


        previewReader.html(`
            <div id="FadeInContainer" style="display:none;">
                <div style="height:48px; overflow:hidden;">
                    <p style="margin:16px 10px; font-size:medium; ">` + mediaType + `预览</p>
                </div>
	        	<iframe id="videoFrame" style="min-height:300px;" src="` + frameUrl + `"></iframe>
	    	    <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("[WebPrvw] 无法对特定网站进行内容优化") }
            }
            if (!result) { result = orig_result_backup; }

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

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

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

            /* 处理大纲 */
            reslist.outline.forEach(outlineItem => {
                let space = "";
                for(let j = 1; j < outlineItem.level; j++){
                    space += "&emsp;&emsp;";
                }
                outlinehtml += space + "+&nbsp;" + outlineItem.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("[WebPrvw] 存在搜索结果:", 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");
                    previewBtn.setAttribute("class", "userscript-webPreviewBtn");
                    previewBtn.setAttribute("link-data", resultItemLink);
                    previewBtn.innerText = "预览";
                    resultItemTitleEle.appendChild(previewBtn);

                    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:var(--bg-color);color:var(--text-color);padding:1px 8px;font-size:12px;font-weight:normal;height:fit-content;margin-left:5px;border-radius:16px;border:1px solid var(--border-color);cursor:pointer}
        .userscript-webPreviewBtn:hover{background:var(--hover-bg-color)}
        .userscript-webPreviewBtn:active{background:var(--active-bg-color);color:var(--close-btn-text)}
        .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:92% !important;max-height:85vh !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% - 120px);background:var(--code-bg-color);border:none;border-radius:30px;margin:5px 5px;box-shadow:0 .5px 1.5px 0 rgba(0,0,0,.19),0 0 1px 0 rgba(0,0,0,.039)}
        .userscript-webPreviewReader #FadeInContainer{overflow-y:scroll;overflow-x:hidden;border-radius:15px 15px 0px 0px;width:100%;height:100%}
    `);


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

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

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

    /* 隐藏阅读器 */
    $("#userscript-webPreviewReader").hide();
    $("#userscript-closeBtn").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)
    });
})();