💡WebPreview - 信息直达

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

  1. // ==UserScript==
  2. // @name 💡WebPreview - 信息直达
  3. // @namespace https://ez118.github.io/
  4. // @version 1.7.2
  5. // @description 支持快速预览搜索引擎结果。点击旁边的预览按钮,可在速览窗中查看目标网站的图片、链接、标题和文本。
  6. // @author ZZY_WISU
  7. // @match *://*/*
  8. // @connect *
  9. // @license GNU GPLv3
  10. // @icon data:image/webp;base64,UklGRpIAAABXRUJQVlA4WAoAAAAwAAAAIgAAIgAAVlA4THMAAAAvIoAIEA8wdtMxwfMf8HBT27ac7ISpY0UAgwXK3yVSkPBLOpBGhY4nIRrIHyxE9F9t2zaMNjs9Z3Ciy+S0SIRMFiRLzBPrG68Vl+/A+bPjaPffdtt35+i+8zsVbKJIKRoVS1brpkDONc8HYi4oREE3pXwJAA==
  11. // @run-at document-end
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM_addStyle
  14. // @grant window.onurlchange
  15. // @require https://update.greasyfork.org/scripts/503290/1426017/ultra-slim-jquery.js
  16. // @require https://unpkg.com/turndown@7.2.0/dist/turndown.js
  17. // @require https://unpkg.com/marked@14.0.0/marked.min.js
  18. // ==/UserScript==
  19.  
  20. const contentEleSelList = {
  21. "blog.csdn.net": "#article_content",
  22. "zhuanlan.zhihu.com": ".Post-RichTextContainer",
  23. "jingyan.baidu.com": "#format-exp",
  24. "www.bilibili.com": "#article-content",
  25. "zhidao.baidu.com": "#qb-content",
  26. "www.cnblogs.com": "#topics",
  27. "www.sohu.com": "#mp-editor"
  28. }; /* 储存特定网站内容优化数据(文章主体的父元素) */
  29.  
  30. const mediaPrevSupport = [
  31. {
  32. "site": "https://v.youku.com/v_show/*.html",
  33. "player": "https://player.youku.com/embed/*",
  34. "type": "video"
  35. },
  36. {
  37. "site": "https://v.qq.com/x/page/*.html",
  38. "player": "https://v.qq.com/txp/iframe/player.html?vid=*",
  39. "type": "video"
  40. },
  41. {
  42. "site": "https://www.bilibili.com/video/BV*/",
  43. "player": "https://www.bilibili.com/blackboard/html5mobileplayer.html?bvid=*",
  44. "type": "video"
  45. },
  46. {
  47. "site": "https://www.bilibili.com/video/av*/",
  48. "player": "https://www.bilibili.com/blackboard/html5mobileplayer.html?aid=*",
  49. "type": "video"
  50. },
  51. {
  52. "site": "https://www.youtube.com/watch?v=*",
  53. "player": "https://www.youtube.com/embed/*",
  54. "type": "video"
  55. },
  56. {
  57. "site": "https://music.163.com/#/song?id=*",
  58. "player": "https://music.163.com/outchain/player?type=2&id=*&auto=0&height=66",
  59. "type": "music"
  60. },
  61. {
  62. "site": "https://music.163.com/song?id=*",
  63. "player": "https://music.163.com/outchain/player?type=2&id=*&auto=0&height=66",
  64. "type": "music"
  65. },
  66. {
  67. "site": "https://open.spotify.com/track/*",
  68. "player": "https://open.spotify.com/embed/track/*",
  69. "type": "music"
  70. },
  71. {
  72. "site": "https://music.apple.com/cn/song/*",
  73. "player": "https://embed.music.apple.com/cn/album/*",
  74. "type": "music"
  75. },
  76. {
  77. "site": "https://music.youtube.com/watch?v=*",
  78. "player": "https://www.youtube.com/embed/*",
  79. "type": "music"
  80. }
  81. ]; /* 储存支持预览播放视频/预览试听音乐的网站及其嵌入播放器链接 */
  82.  
  83.  
  84. function runAsync(url, sendType, data) {
  85. return new Promise((resolve, reject) => {
  86. GM_xmlhttpRequest({
  87. method: sendType,
  88. url: url,
  89. headers: {
  90. "Content-Type": "application/x-www-form-urlencoded;charset=utf-8"
  91. },
  92. data: data,
  93. onload: (response) => resolve(response.responseText),
  94. onerror: () => reject("[WebPrvw] 请求失败")
  95. });
  96. });
  97. }
  98.  
  99.  
  100. function judgeMediaSupport(url){
  101. var jflag = null;
  102. mediaPrevSupport.forEach(function(item, index) {
  103. if (url.includes(item.site.split("*")[0])) {
  104. jflag = { "state": true, "data": item };
  105. }
  106. });
  107. return jflag || { "state": false, "data": null };
  108. }
  109.  
  110. function getOutline(markdown) {
  111. /* 文章大纲提取 */
  112.  
  113. var lines = markdown.split('\n');
  114. var titleElementArr = [];
  115. var preTitleElement = null;
  116.  
  117. lines.forEach(line => {
  118. const match = line.match(/^(#{1,6})\s+(.*)/);
  119. if (match) {
  120. var id = Math.random().toString(36).substr(2, 7);
  121. var level = 1;
  122. var tag = match[1].length;
  123. var title = match[2];
  124.  
  125. if (preTitleElement != null) {
  126. var tagPre = preTitleElement.tag;
  127. var levelPre = preTitleElement.level;
  128.  
  129. if (tagPre > tag) { level = levelPre - (tagPre - tag); }
  130. else if (tagPre < tag) { level = levelPre + 1; }
  131. else { level = levelPre; }
  132. }
  133.  
  134. if (title.trim().length > 0) {
  135. var titleElement = {
  136. tag: tag,
  137. title: title,
  138. level: level,
  139. id: id
  140. };
  141. titleElementArr.push(titleElement);
  142. preTitleElement = titleElement;
  143. }
  144. }
  145. });
  146.  
  147. return titleElementArr;
  148. }
  149.  
  150. function getWebContents(txt) {
  151. var links = [];
  152. var images = [];
  153. var content = "";
  154. var outline = [];
  155.  
  156. /* 获取所有链接 */
  157. txt.replace(/<a [^>]*href=['"]([^'"]+)['"][^>]*>/g, function(match, capture){
  158. links.push(capture);
  159. });
  160.  
  161. /* 获取所有图片 */
  162. txt.replace(/<img [^>]*src=['"]([^'"]+)['"][^>]*>/g, function(match, capture){
  163. images.push(capture);
  164. });
  165.  
  166. /* 去掉影响转换的标签 */
  167. var markdown = txt.replace(/<script.*?>.*?<\/script>/gis, "")
  168. .replace(/<style.*?>.*?<\/style>/gis, "")
  169. .replace(/<nav.*?>.*?<\/nav>/gis, "");
  170.  
  171.  
  172. /* html转markdown */
  173. const turndownService = new TurndownService();
  174. markdown = turndownService.turndown(markdown);
  175.  
  176. /* markdown转html */
  177. content = marked.parse(markdown);
  178.  
  179. /* 获取大纲信息 */
  180. try{ outline = getOutline(markdown);}
  181. catch{ console.log("[WebPrvw] 无法解析大纲") }
  182.  
  183. var final_data = {"link": links, "image": images, "content": content, "outline": outline};
  184.  
  185. return final_data;
  186. }
  187.  
  188. function openReader(url) {
  189. /* 打开阅读器 */
  190.  
  191. /* 阅读器加载提示 */
  192. var closeBtn = $("#userscript-closeBtn");
  193. var previewReader = $("#userscript-webPreviewReader");
  194. previewReader.html("<p style='font-size:22px;margin-top:33%;' align='center'>正在载入...<br/><span>" + url + "</span></p>");
  195.  
  196. previewReader.show();
  197. closeBtn.show();
  198.  
  199. /* 判断当前链接是支持预览的视频网站,并作出对应处理 */
  200. var showMedia = judgeMediaSupport(url);
  201. if(showMedia.state){
  202. /* 被支持的视频网站的处理 */
  203. var origUrl = url;
  204. var frameUrl = "";
  205. var mediaType = (showMedia.data.type == "video") ? "视频" : "音乐";
  206.  
  207. /* 将链接参数与嵌入式播放器链接拼接 */
  208. url = url.replace(showMedia.data.site.split("*")[0], "");
  209. url = url + "?#";
  210. url = url.split("#")[0].split("?")[0];
  211. url = url.replace(showMedia.data.site.split("*")[1], "");
  212.  
  213. frameUrl = showMedia.data.player.replace("*", url);
  214.  
  215.  
  216. previewReader.html(`
  217. <div id="FadeInContainer" style="display:none;">
  218. <div style="height:48px; overflow:hidden;">
  219. <p style="margin:16px 10px; font-size:medium; ">` + mediaType + `预览</p>
  220. </div>
  221. <iframe id="videoFrame" style="min-height:300px;" src="` + frameUrl + `"></iframe>
  222. <br>
  223.  
  224. <a href="` + origUrl + `" class="link" id="GoToLink" target="_blank">在原网站中继续 &nbsp; </a><br/>
  225. <a href="` + frameUrl + `" class="link" id="GoToLink" target="_blank">在播放器中继续 &nbsp; </a>
  226. </div>
  227. `);
  228.  
  229. $("#FadeInContainer").show();
  230. } else {
  231. /* 普通网站的处理 */
  232. runAsync(url, "GET", "").then((result)=>{ return result; }).then(function(result){
  233. /* 源数据处理(csdn存在利用img的onerror属性注入xss脚本的行为) */
  234. result = result.replace(/<img\s+[^>]*src\s*=\s*["']{2}[^>]*>/gi, ''); /* 删除src为空的标签 */
  235. result = result.replace(/<img([^>]*)onerror\s*=\s*(['"]?[^'">]*['"]?)([^>]*)>/gi, '<img$1$3>'); /* 删除所有img标签的onerror属性 */
  236.  
  237. /* 对指定网站进行内容过滤,指定元素获取 */
  238. let orig_result_backup = result;
  239. const domain = url.split("/")[2];
  240. if (contentEleSelList[domain]) {
  241. try {
  242. const selector = contentEleSelList[domain];
  243. result = $(result).find(selector).html();
  244. } catch (e) { console.log("[WebPrvw] 无法对特定网站进行内容优化") }
  245. }
  246. if (!result) { result = orig_result_backup; }
  247.  
  248. /* 调用解析网页 */
  249. let reslist = getWebContents(result);
  250. let linkhtml = "", imghtml = "", outlinehtml = "";
  251.  
  252. /* 处理链接列表 */
  253. reslist.link.forEach(link_tmp => {
  254. if(link_tmp.includes("//")){
  255. linkhtml += "<a class='link' target='_blank' href='" + link_tmp + "'> 🔗&nbsp;" + link_tmp + " </a><br>";
  256. }
  257. });
  258.  
  259. /* 处理图片列表 */
  260. reslist.image.forEach(image => {
  261. imghtml += "<a href='" + image + "' target='_blank'><img class='image' src='" + image + "' onerror='this.remove()'/></a>";
  262. });
  263.  
  264. /* 处理大纲 */
  265. reslist.outline.forEach(outlineItem => {
  266. let space = "";
  267. for(let j = 1; j < outlineItem.level; j++){
  268. space += "&emsp;&emsp;";
  269. }
  270. outlinehtml += space + "+&nbsp;" + outlineItem.title + "<br/>";
  271. });
  272.  
  273.  
  274. /* 将所有结果添加进阅读器,并显示 */
  275. previewReader.html(`
  276. <div id="FadeInContainer" style="display:none;">
  277. <div style="height:48px;"></div>
  278. <div class="ImageList" style="max-height:103px;">
  279. <p class='ShowList' align='right' style='' onclick='this.parentNode.setAttribute("style", "");'>展开</p>
  280. ` + imghtml + `
  281. </div>
  282.  
  283. <div class="LinkList" style="max-height:286px;">
  284. <p class='ShowList' align='right' style='' onclick='this.parentNode.setAttribute("style", "");'>展开</p>
  285. ` + linkhtml + `
  286. </div>
  287.  
  288. <div class="OutlineShow">
  289. <b>大纲: </b><br/>
  290. ` + outlinehtml + `
  291. </div>
  292.  
  293. <div class="ContentShow">
  294. <b>文本: </b><br/>
  295. ` + reslist.content + `
  296. </div>
  297. </div>
  298. `);
  299.  
  300. /* 隐藏不存在的项 */
  301. if(reslist.image.length == 0) { $(".ImageList").hide(); }
  302. if(reslist.link.length == 0) { $(".LinkList").hide(); }
  303. if(reslist.outline.length == 0) { $(".OutlineShow").hide(); }
  304.  
  305. $("#FadeInContainer").show();
  306. });
  307. }
  308. /* 执行结束 */
  309. }
  310.  
  311.  
  312.  
  313.  
  314. /* ======[ 搜索结果分析 ]===== */
  315. /*
  316. * 判断当前元素是否包含搜索结果
  317. * 解释:检查当前元素的子元素中是否有某个类名出现超过5次。
  318. * 如果存在,则认为该元素含有搜索结果。
  319. */
  320. function checkSearchResults(parentElement) {
  321. var classList = [];
  322. var countList = [];
  323. for(let i = 0; i < parentElement.children.length; i ++) {
  324. var child = parentElement.children[i];
  325. var childClass = child.classList;
  326. for(let j = 0; j < childClass.length; j ++) {
  327. if(classList.indexOf(childClass[j]) !== -1) {
  328. /* 对列表中的class出现次数进行计数 */
  329. var p = classList.indexOf(childClass[j]);
  330. countList[p] += 1;
  331. } else {
  332. /* 对列表中未出现的class,插入列表 */
  333. classList.push(childClass[j]);
  334. countList.push(0);
  335. }
  336. }
  337. }
  338. var countMax = Math.max.apply(null, countList);
  339. return (countMax >= 5);
  340. }
  341.  
  342. /* 遍历元素 */
  343. function traverseElements(element, callback) {
  344. if (!element || !element.children || element.children.length === 0) { return; }
  345.  
  346. var returnCode = callback(element);
  347. if (returnCode) { return; }
  348. /* 如果返回值为true,则代表该元素已包含搜索结果,无需继续遍历 */
  349.  
  350. for (let i = 0; i < element.children.length; i++) {
  351. traverseElements(element.children[i], callback);
  352. }
  353. }
  354.  
  355. /*
  356. * 在满足条件的搜索结果旁插入“速览按钮”
  357. * 解释:遍历 DOM 查找搜索列表,并在符合条件的结果旁添加按钮。
  358. * 这是程序分析部分的入口函数。
  359. */
  360. function initAnalyze() {
  361. traverseElements(document.body, function(element) {
  362. var status = checkSearchResults(element);
  363. if(status) {
  364. console.log("[WebPrvw] 存在搜索结果:", status);
  365. let resultItems = element.children;
  366. for(let i = 0; i < resultItems.length; i ++) {
  367. try {
  368. let resultItemLink = resultItems[i].getElementsByTagName("a")[0].href;
  369. let resultItemTitleEle = resultItems[i].getElementsByTagName("a")[0].parentNode;
  370. let resultItemText = resultItems[i].getElementsByTagName("a")[0].innerText;
  371.  
  372. if(resultItemText.length <= 5 || !resultItemLink){ continue; }
  373. if(resultItemLink.includes("javascript:") && resultItemLink[0] == "j") { continue; }
  374.  
  375. /* 向每一个搜索结果的标题部分添加按钮 */
  376. let previewBtn = document.createElement("button");
  377. previewBtn.setAttribute("class", "userscript-webPreviewBtn");
  378. previewBtn.setAttribute("link-data", resultItemLink);
  379. previewBtn.innerText = "预览";
  380. resultItemTitleEle.appendChild(previewBtn);
  381.  
  382. previewBtn.addEventListener("click", function(evt){
  383. let linkData = previewBtn.getAttribute("link-data");
  384. openReader(linkData);
  385. }, true);
  386. } catch(e) {
  387. //console.log("[WebPrvw] 列表元素读取失败 ELE(" + i + ") \n" + e);
  388. }
  389. }
  390.  
  391. return true;
  392. } else {
  393. return false;
  394. }
  395. });
  396. }
  397. /* =========================== */
  398.  
  399.  
  400. function init(){
  401. /* 初始化 */
  402.  
  403. /* 插入样式 */
  404. GM_addStyle(`
  405. :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}
  406. @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}}
  407.  
  408. .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}
  409. .userscript-webPreviewBtn:hover{background:var(--hover-bg-color)}
  410. .userscript-webPreviewBtn:active{background:var(--active-bg-color);color:var(--close-btn-text)}
  411. .userscript-webPreviewBtn img{height:16px}
  412. .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}
  413. .userscript-closeBtn:hover{background:var(--link-hover)}
  414. .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}
  415. .userscript-webPreviewReader .ShowList{margin:0;padding:0;width:100%;cursor:pointer;color:var(--link-color);user-select:none}
  416. .userscript-webPreviewReader .image{height:85px;margin-bottom:8px;margin-right:5px;border-radius:15px;object-fit:contain;max-width:calc(100% - 20px)}
  417. .userscript-webPreviewReader .link{text-decoration:none;color:var(--link-color) !important;margin-left:5px}
  418. .userscript-webPreviewReader .link:hover{text-decoration:underline}
  419. .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)}
  420. .ContentShow img{max-width:92% !important;max-height:85vh !important;position:relative !important;top:0 !important;left:0 !important;border-radius:10px}
  421. .ContentShow a{color:var(--link-color);text-decoration:underline 1px solid var(--link-hover);margin:0px 3px}
  422. .ContentShow code{font-family:Consolas,Courier,Courier New,monospace}
  423. .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}
  424. .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)}
  425. .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)}
  426. .userscript-webPreviewReader #FadeInContainer{overflow-y:scroll;overflow-x:hidden;border-radius:15px 15px 0px 0px;width:100%;height:100%}
  427. `);
  428.  
  429.  
  430. /* 页面加载时插入DOM */
  431. /* 阅读器 */
  432. if( $("#userscript-webPreviewReader").length == 0 ){
  433. var $previewReader = $('<div>', {
  434. class: 'userscript-webPreviewReader',
  435. id: 'userscript-webPreviewReader'
  436. }).appendTo('body');
  437.  
  438. var $closeBtn = $('<button>', {
  439. text: '关闭',
  440. class: 'userscript-closeBtn',
  441. id: 'userscript-closeBtn',
  442. }).appendTo('body');
  443.  
  444. $closeBtn.on('click', function() {
  445. $previewReader.hide();
  446. $closeBtn.hide();
  447. });
  448. }
  449.  
  450. /* 隐藏阅读器 */
  451. $("#userscript-webPreviewReader").hide();
  452. $("#userscript-closeBtn").hide();
  453.  
  454. /* 自动匹配搜索结果并插入按钮 */
  455. initAnalyze();
  456.  
  457. return;
  458. }
  459.  
  460. (function() {
  461. 'use strict';
  462.  
  463. init();
  464.  
  465. window.addEventListener('urlchange', (info) => {
  466. if($("#userscript-webPreviewReader").length > 0 && $(".userscript-webPreviewBtn").length > 1) { return; }
  467. setTimeout(function(){
  468. init();
  469. }, 1600)
  470. });
  471. })();