// ==UserScript==
// @name 💡WebPreview - 信息直达
// @namespace https://ez118.github.io/
// @version 1.5.1
// @description 支持国内主流搜索引擎的搜索结果快速预览(直达网页大纲)。只需点击搜索结果旁的小灯泡按钮,即可在右侧速览窗中快速查看目标网站所含的图片、链接、标题大纲、文本。
// @author ZZY_WISU
// @match *://*/*
// @connect *
// @license GNU GPLv3
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAMAAAApB0NrAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAADZQTFRFAAAAn8LgEG6+SZDMg7HaZaDSgrHZkLrdO4jJZqHTgbLZHnbCdKnWLX/FV5jPLX7FSY/LdKnVpsJ+6QAAABJ0Uk5TACb//8j/yoP//8n/8f/////yflwd4QAAAORJREFUeJy1k9EagyAIhfVYGqnV3v9lp2kLjdrFvnGT4Q8coJTqTMP0rov9nRnYebTn2Z3HCSSGzvAsvwjNACuQIH1lGiRBNU+IALwpAXq4hCUCh0VR3y69ykrZbqCFdTHJUNLd5JRGGeH4q4fAdNkDwgWxWLqY6eSJKBcZYTtmz5tuSa1pGkZkUBrJT621WL/U0vW6+nyDkLRm3/YOqXfXeKP8SRGb0M0u+AV14poCZUlaFNMK3YQ9fGwtjLjPwzaYPPynUomp9shQHv4XZl/Oz8yCGGwwj4yKRbL08zGz48t1rjcZWATm0KKJdwAAAABJRU5ErkJggg==
// @run-at document-end
// @grant GM_xmlhttpRequest
// @grant GM_download
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant window.onurlchange
// @require https://update.greasyfork.org/scripts/499192/1402326/jquery_360.js
// ==/UserScript==
var iconImg = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAUCAMAAACK2/weAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAHtQTFRFAAAAOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGofOGoffJU8/QAAACl0Uk5TABWJvOy7hhRd8P/tT25TIfzzlXziyPju8c/NgHgY9OMRZvU2WkPQx8o0T8eSAAAAbUlEQVR4nI3OyQ6CQBBF0auASOOEgICoCKLw/19ImqG7TFx4V3UWLymA1dpxXW/jM7YN1Fi409or0wGOJ8vI56xEMYlkykUyI5cMKCSvlJI37g+r6gm1UfPSb7UL39PTC/nJz6SOv/qazuNeXwMWhAmHAXFPJgAAAABJRU5ErkJggg==";
/* 用于存储小灯泡按钮的图片数据 */
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 randomString(len) {
len = len || 32;
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;
}
/* 标题元素 */
function TitleElement(tag, title, level, id) {
this.tag = tag;
this.title = title;
this.level = level;
this.id = id;
}
/* 是否是标题元素 */
function isTitleTag(tag) {
return tag.is("h1, h2, h3, h4, h5, h6, h7");
}
/* 生成大纲 */
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;
}
}
});
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);
});
/* 删除多余空格和换行 */
content = txt.replace(/\s+/g, ' ').trim().replace(/(\r\n|\r|\n){2,}/g, '$1\n');
/* 获取文本,去掉特定标签 */
content = content.replace(/<\/div>/g, "</div>\n")
.replace(/<\/table>/g, "</table>\n")
.replace(/<\/h3>/g, "</h3>\n")
.replace(/<\/p>/g, "</p>\n")
.replace(/<\/li>/g, "</li>\n")
.replace(/<script.*?>.*?<\/script>/gis, "")
.replace(/<style.*?>.*?<\/style>/gis, "")
.replace(/<nav.*?>.*?<\/nav>/gis, "");
content = content.replace(/<(?!\/?(a|img|code|pre)\b)[^>]+>/g, ''); /* 删除除了a和img以外的标签 */
/* 将换行符变为换行,删除多余的br */
content = content.replace(/\n/g,"<br/>").replace(/(<br\/>\s*)+/g, '<br/>');
/* 获取大纲信息 */
try{
outline = getOutline(txt);
} catch(e){ 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.fadeIn(100);
closeBtn.fadeIn(100);
/* 判断当前链接是支持预览的视频网站,并作出对应处理 */
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">在原网站中继续 ▶ </a><br/>
<a href="` + frameUrl + `" class="link" id="GoToLink" target="_blank">在播放器中继续 🎦 </a>
</div>
`);
/* 淡入 */
$("#FadeInContainer").fadeIn(700);
} 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 + "'> 🔗 " + 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 += "  "; }
outlinehtml += space + "+ " + 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").fadeIn(250);
});
}
/* 执行结束 */
}
/* =============[ 搜索结果分析 ]============ */
/*
* 自动判断当前元素下是否 具有搜索结果特征
* 解释:判断一个父元素下存在 大于等于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(){
/* 适配黑暗模式 */
if(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
/* 黑暗模式下的样式 */
GM_addStyle(`
.userscript-webPreviewBtn{ user-select:none; background-color:#00390a55; color:#7edb7b; padding:6px 18px; font-weight:bold; line-height:16px; height:30px; margin-left:5px; border-radius:30px; border:1px solid #7edb7b; cursor:pointer; }
.userscript-webPreviewBtn:hover{ background-color:#00390aAA; }
.userscript-webPreviewBtn:active{ background-color:#7edb7b; }
.userscript-webPreviewBtn img{ height:16px; }
.userscript-closeBtn{ position:fixed; top:calc(8% + 5px); right:26px; z-index:100000; background:#7edb7b; color:#00390a; padding:8px 20px; margin:6px; border-radius:30px; font-weight:bold; border:0; border-bottom:1px solid #00390a; cursor:pointer; }
.userscript-closeBtn:hover{ background:#76cd74; }
.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:#1a1c19; border:1px solid #424940; color:#e2e3dd; 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 img.error{ display:none; }
.userscript-webPreviewReader .ShowList{ margin:0;padding:0;width:100%;cursor:pointer;color:#7edb7b; 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:#7edb7b!important; margin-left: 5px; }
.userscript-webPreviewReader .link:hover{ text-decoration:underline; }
.ImageList, .LinkList, .OutlineShow, .ContentShow{ padding:16px; margin:8px; background:#42494047; border-radius:30px; overflow:hidden; color:#d5e8cf; 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 img::after{ content: ""; display: block; clear: both; }
.ContentShow a{ color:#7edb7b; text-decoration:underline 1px solid #386a1f; margin:0px 3px; }
.ContentShow code { font-family: Consolas, Courier, Courier New, monospace; }
.ContentShow pre { color:#dcdcdc; background:#1e201d; width:90%; padding:5px; margin:5px 0px; overflow-y:auto; height:fit-content; border:1px solid #424940; border-radius:5px;}
.ContentShow code:not(pre code) { color:#e2e3dd; background:#42494047; border-radius:0.25rem; padding:.125rem .375rem; line-height:1.75; word-wrap:break-word; border:1px solid #424940; }
.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%; }
`);
} else {
/* 日间模式下的样式 */
GM_addStyle(`
.userscript-webPreviewBtn{ user-select:none; background-color:#FFFFFFAA; color:#386a1f; padding:6px 18px; font-weight:bold; line-height:16px; height:30px; margin-left:5px; border-radius:30px; border:1px solid #285a0f; cursor:pointer; }
.userscript-webPreviewBtn:hover{ background-color:#edf1e5; }
.userscript-webPreviewBtn:active{ background-color:#d7e1cd; }
.userscript-webPreviewBtn img{ height:16px; }
.userscript-closeBtn{ position:fixed; top:calc(8% + 5px); right:26px; z-index:100000; background:#386a1f; color:#FFF; padding:8px 20px; margin:6px; border-radius:30px; font-weight:bold; border:0; border-bottom:1px solid #20460e; cursor:pointer; }
.userscript-closeBtn:hover{ background:#487631; }
.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:#fdfdf6; color:#131f0d; 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 img.error{ display:none; }
.userscript-webPreviewReader .ShowList{ margin:0;padding:0;width:100%;cursor:pointer;color:#386a1f; 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:#386a1f!important; margin-left: 5px; }
.userscript-webPreviewReader .link:hover{ text-decoration:underline; }
.ImageList, .LinkList, .OutlineShow, .ContentShow{ padding:16px; margin:8px; background:rgb(216,231,203); border-radius:30px; overflow:hidden; color:#131f0d; 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 img::after{ content: ""; display: block; clear: both; }
.ContentShow a{ color:#386a1f; text-decoration:underline 1px solid #386a1f; margin:0px 3px; }
.ContentShow code { font-family: Consolas, Courier, Courier New, monospace; }
.ContentShow pre { color:#1a1c19; background:#eeeee8; width:90%; padding:5px; margin:5px 0px; overflow-y:auto; height:fit-content; border:1px solid #dee5d8; border-radius:5px;}
.ContentShow code:not(pre code) { color:#1a1c19; background:#e2e3dd; border-radius:0.25rem; padding:.125rem .375rem; line-height:1.75; word-wrap:break-word; border:1px solid #dee5d8; }
.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.fadeOut(200);
$closeBtn.hide();
});
}
/* 隐藏阅读器(原因:有些网站会使display:none;属性失效,需要再次隐藏) */
$("#userscript-webPreviewReader").hide();
/* 遍历DOM,获取搜索列表,插入按钮 */
initAnalyze();
return;
}
(function() {
'use strict';
init();
window.addEventListener('urlchange', (info) => {
setTimeout(function(){
init();
}, 1600)
});
})();