- // ==UserScript==
- // @name 💡WebPreview - 信息直达
- // @namespace https://ez118.github.io/
- // @version 1.7.2
- // @description 支持快速预览搜索引擎结果。点击旁边的预览按钮,可在速览窗中查看目标网站的图片、链接、标题和文本。
- // @author ZZY_WISU
- // @match *://*/*
- // @connect *
- // @license GNU GPLv3
- // @icon data:image/webp;base64,UklGRpIAAABXRUJQVlA4WAoAAAAwAAAAIgAAIgAAVlA4THMAAAAvIoAIEA8wdtMxwfMf8HBT27ac7ISpY0UAgwXK3yVSkPBLOpBGhY4nIRrIHyxE9F9t2zaMNjs9Z3Ciy+S0SIRMFiRLzBPrG68Vl+/A+bPjaPffdtt35+i+8zsVbKJIKRoVS1brpkDONc8HYi4oREE3pXwJAA==
- // @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/turndown@7.2.0/dist/turndown.js
- // @require https://unpkg.com/marked@14.0.0/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, sendType, data) {
- return new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: sendType,
- url: url,
- headers: {
- "Content-Type": "application/x-www-form-urlencoded;charset=utf-8"
- },
- data: data,
- onload: (response) => resolve(response.responseText),
- onerror: () => reject("[WebPrvw] 请求失败")
- });
- });
- }
-
-
- 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">在原网站中继续 ▶ </a><br/>
- <a href="` + frameUrl + `" class="link" id="GoToLink" target="_blank">在播放器中继续 ▶ </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 + "'> 🔗 " + 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 += "  ";
- }
- outlinehtml += space + "+ " + 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次。
- * 如果存在,则认为该元素含有搜索结果。
- */
- 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("[WebPrvw] 列表元素读取失败 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)
- });
- })();