我的搜索

打造订阅式搜索,让我的搜索,只搜精品!

  1. // ==UserScript==
  2. // @name 我的搜索
  3. // @namespace http://tampermonkey.net/
  4. // @version 7.5.0
  5. // @description 打造订阅式搜索,让我的搜索,只搜精品!
  6. // @license MIT
  7. // @author zhuangjie
  8. // @exclude http://127.0.0.1*
  9. // @exclude http://localhost*
  10. // @match *://*/*
  11. // @exclude http://192.168.*
  12. // @icon data:image/png;base64,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
  13. // @require https://cdn.jsdelivr.net/npm/jquery@3.6.2/dist/jquery.min.js
  14.  
  15. // @require https://cdn.jsdelivr.net/npm/showdown@2.1.0/dist/showdown.min.js
  16. // @resource markdown-css https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.8.1/github-markdown.min.css
  17.  
  18. // @require https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js
  19. // @resource code-css https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css
  20.  
  21. // @require https://update.greasyfork.org/scripts/530877/1560004/ms-pinyin.js
  22. // @require https://update.greasyfork.org/scripts/501646/1429885/string-overlap-matching-degree.js
  23. // @noframes
  24.  
  25. // @grant window.onurlchange
  26. // @grant GM_setValue
  27. // @grant GM_xmlhttpRequest
  28. // @connect *
  29. // @grant GM_getValue
  30. // @grant GM_addStyle
  31. // @grant GM_getResourceText
  32.  
  33. // @grant GM_getResourceURL
  34. // @grant GM_deleteValue
  35. // @grant GM_registerMenuCommand
  36. // @grant GM_info
  37.  
  38. // ==/UserScript==
  39.  
  40. // 参数tis: actualWindow 是真实的window,而如果在下面脚本内访问window则不是,是沙盒的XPCNativeWrapper对象,详情https://blog.rxliuli.com/p/e55a67646bf546b3900ce270a6fbc6ca/
  41. (function(actualWindow) {
  42. 'use strict';
  43.  
  44. // 模块一:快捷键触发某一事件 (属于触发策略组)
  45. // 模块二:搜索视图(显示与隐藏)(属于搜索视图组)
  46. // 模块三:触发策略组触发策略触发搜索视图组视图
  47. // 模块四:根据用户提供的策略(策略属于数据生成策略组)生成搜索项的数据库
  48. // 模块五:视图接入数据库
  49.  
  50. // 判断当前是否在iframe里面,
  51. function currentIsIframe() {
  52. if (self.frameElement && self.frameElement.tagName == "IFRAME") return true;
  53. if (window.frames.length != parent.frames.length) return true;
  54. if (self != top) return true;
  55. return false;
  56. }
  57.  
  58.  
  59. // 如果当前是ifrae,结束脚本执行
  60. let MY_SEARCH_SCRIPT_VIEW_SHOW_EVENT = "MY_SEARCH_SCRIPT_VIEW_SHOW_EVENT";
  61. if(currentIsIframe()) {
  62. // 虽然iframe不能初始化脚本,但可以作为父窗口的事件触发源
  63. triggerAndEvent("ctrl+alt+s", function () { // 通知主容器显示搜索框
  64. window.parent.postMessage(MY_SEARCH_SCRIPT_VIEW_SHOW_EVENT, '*');
  65. })
  66. // 结束脚本执行
  67. return;
  68. }
  69.  
  70. // css resource加载器
  71. function cssLoad(prefix,css = "",{isResourceName = false,replacePrefix}) {
  72. if(isResourceName) css = GM_getResourceText(css);
  73.  
  74. if(replacePrefix && prefix) {
  75. css = css.toReplaceAll(replacePrefix,prefix);
  76. }else if(prefix) {
  77. // 对css原始内容引入前缀
  78. css = `.${prefix} {
  79. ${css}
  80. }`;
  81. }
  82. GM_addStyle(css);
  83. return prefix;
  84. }
  85. // 正则捕获
  86. function captureRegEx(regex, text) {
  87. let m;
  88. let result = []; // 一组一组 [[],[],...]
  89. regex.lastIndex = 0; // 重置lastIndex
  90. while ((m = regex.exec(text)) !== null) {
  91. let group = [];
  92. group.push(...m);
  93. if(group.length != 0) result.push(group);
  94. }
  95. return result;
  96. }
  97. // 滚动到目标文本 可以指定容器
  98. function scrollToText(text, index = 0, container = document.body) {
  99. // 创建高亮样式(只需执行一次)
  100. if (!document.getElementById('highlight-style')) {
  101. const style = document.createElement('style');
  102. style.id = 'highlight-style';
  103. style.textContent = `.highlight-text { border-bottom: 2px solid red !important;color:red; }`;
  104. document.head.appendChild(style);
  105. }
  106.  
  107. // 处理容器参数
  108. const containerElement = typeof container === 'string'
  109. ? document.querySelector(container)
  110. : container;
  111.  
  112. if (!containerElement) {
  113. console.error('Container not found:', container);
  114. return;
  115. }
  116.  
  117. // 清理旧高亮
  118. Array.from(containerElement.getElementsByClassName('highlight-text'))
  119. .forEach(span => {
  120. const textNode = document.createTextNode(span.textContent);
  121. span.parentNode.replaceChild(textNode, span);
  122. });
  123.  
  124. if (!text) return;
  125.  
  126. // 获取所有文本节点
  127. const textNodes = [];
  128. const walker = document.createTreeWalker(
  129. containerElement,
  130. NodeFilter.SHOW_TEXT,
  131. { acceptNode: () => NodeFilter.FILTER_ACCEPT }
  132. );
  133. while (walker.nextNode()) textNodes.push(walker.currentNode);
  134.  
  135. // 查找所有匹配项
  136. const matches = [];
  137. const searchLen = text.length;
  138. for (const node of textNodes) {
  139. const nodeText = node.nodeValue;
  140. let pos = 0;
  141. while (pos <= nodeText.length - searchLen) {
  142. const idx = nodeText.indexOf(text, pos);
  143. if (idx === -1) break;
  144. matches.push({ node, start: idx, end: idx + searchLen });
  145. pos = idx + 1; // 允许重叠匹配
  146. }
  147. }
  148.  
  149. // 验证索引
  150. if (index < 0 || index >= matches.length) {
  151. console.error(`Index ${index} out of range (0-${matches.length - 1})`);
  152. return;
  153. }
  154.  
  155. // 处理目标匹配项
  156. const { node, start, end } = matches[index];
  157. const middle = node.splitText(start);
  158. const after = middle.splitText(end - start);
  159. const span = document.createElement('span');
  160. span.className = 'highlight-text';
  161. span.textContent = middle.nodeValue;
  162. middle.parentNode.replaceChild(span, middle);
  163.  
  164. // 滚动到目标位置
  165. span.scrollIntoView({
  166. behavior: 'smooth',
  167. block: 'center',
  168. inline: 'nearest'
  169. });
  170. }
  171. // 重写console.log方法
  172. let originalLog = console.log;
  173. console.logout = function() {
  174. const prefix = "[我的搜索log]>>> ";
  175. const args = [prefix].concat(Array.from(arguments));
  176. originalLog.apply(console, args);
  177. }
  178. // markdown转html 转换器 【1】
  179. // 更多配置项:https://github.com/showdownjs/showdown
  180. const converter = new showdown.Converter({
  181. // 将换行符解析为<br>
  182. simpleLineBreaks:true,
  183. // 新窗口打开链接
  184. openLinksInNewWindow: true,
  185. metadata:true,
  186. // 不允许下划线变斜体
  187. literalMidWordUnderscores: true,
  188. // 识别md表格
  189. tables: true,
  190. // www.baidu.com 会识别为链接
  191. simplifiedAutoLink: true
  192. });
  193.  
  194. function md2html(rawText) {
  195. // 方案二: return marked.parse(rawText); // @require https://cdnjs.cloudflare.com/ajax/libs/marked/9.0.2/marked.min.js
  196. return converter.makeHtml( rawText )
  197. }
  198.  
  199. // 提取URL根域名
  200. function getUrlRoot(url,isRemovePrefix = true,isRemoveSuffix = true) {
  201. if(! (typeof url == "string" || url.length >= 3)) return url;
  202. // 可处理
  203. // 判断是否有前缀
  204. let prefix = "";
  205. let root = "";
  206. let suffix = "";
  207. // 提取前缀
  208. if(url.indexOf("://") != -1) {
  209. // 存在前缀
  210. let prefixSplitArr = url.split("://")
  211. prefix = prefixSplitArr[0];
  212. url = prefixSplitArr[1];
  213. }
  214. // 提取root 和suffix
  215. if(url.indexOf("/") != -1) {
  216. let twoLevelIndex = url.indexOf("/")
  217. root = url.substr(0,twoLevelIndex);
  218. suffix = url.substr(twoLevelIndex,url.length-1);
  219. }else {
  220. root = url;
  221. suffix = "";
  222. }
  223. return ((!isRemovePrefix && prefix != "")?(prefix+"://"):"") + root + (isRemoveSuffix?"":suffix);
  224. }
  225. // 解析出http url 结构
  226. function parseUrl(url) {
  227. const regex = /(https?:|)\/\/([^\/]*|[^\/]*)(\/[^\s\?]*|)(\??[^\s]*|)/;
  228. const matches = regex.exec(url);
  229. if (matches) {
  230. const protocol = matches[1];
  231. const domain = matches[2];
  232. const path = matches[3];
  233. const params = matches[4];
  234. const rootUrl = protocol+"//"+domain
  235. const rawUrl = url;
  236. return {protocol,domain,path,params,rootUrl,rawUrl}
  237. }
  238. return null;
  239. }
  240. function isHttpUrl(url = "") {
  241. url = url.trim();
  242. return /^https?:\/\/.+/i.test(url)
  243. }
  244.  
  245. // 检查网站是否可用
  246. function checkUsability(templateUrl,isStopCheck = false) {
  247. return new Promise(function (resolve, reject) {
  248. // 判断是否要检查
  249. if(isStopCheck) {
  250. reject(null);
  251. return;
  252. }
  253. var img=document.createElement("img");
  254. img.src = templateUrl.fillByObj(parseUrl("https://www.baidu.com"));
  255. img.style= "display:none;";
  256. img.onerror = function(e) {
  257. setTimeout(function() {img.remove();},20)
  258. reject(null);
  259. }
  260. img.onload = function(e) {
  261. setTimeout(function() {img.remove();},20)
  262. resolve(templateUrl);
  263. }
  264. document.body.appendChild(img);
  265. });
  266. }
  267.  
  268.  
  269.  
  270. // 数据缓存器
  271. let cache = {
  272. prefix: "",
  273. get(key) {
  274. return GM_getValue(this.prefix+key);
  275. },
  276. set(key,value) {
  277. this.remove(this.prefix+key);
  278. GM_setValue(this.prefix+key,value);
  279. },
  280. jGet(key) {
  281. let value = GM_getValue(this.prefix+key);
  282. if( value == null) return value;
  283. return JSON.parse(value);
  284. },
  285. jSet(key,value) {
  286. value = JSON.stringify(value)
  287. GM_setValue(this.prefix+key,value);
  288. },
  289. remove(key) {
  290. GM_deleteValue(this.prefix+key);
  291. },
  292. cookieSet(cname,cvalue,exdays) {
  293. var d = new Date();
  294. d.setTime(d.getTime()+exdays);
  295. var expires = "expires="+d.toGMTString();
  296. document.cookie = cname + "=" + cvalue + "; " + expires;
  297. },
  298. cookieGet(cname) {
  299. var name = cname + "=";
  300. var ca = document.cookie.split(';');
  301. for(var i=0; i<ca.length; i++)
  302. {
  303. var c = ca[i].trim();
  304. if (c.indexOf(name)==0) return c.substring(name.length,c.length);
  305. }
  306. return "";
  307. }
  308. }
  309.  
  310. // 责任链对象工厂
  311. function getResponsibilityChain() {
  312. return {
  313. chains: [],
  314. add(chain = {weight:0,fun: (data,ref)=>data}) {
  315. if(chain == null ) throw new Error("[ERROR]责任链对象: 你添加了一个null Chain!")
  316. if(chain.weight == undefined || chain.fun == undefined) throw new Error("[ERROR]责任链对象: 你传入的Chain是无效的!")
  317. this.chains.push(chain)
  318. },
  319. trigger(baton) {
  320. // 排序,通过weight从高到低
  321. this.chains = this.chains.sort((a, b)=>b.weight - a.weight);
  322. // 开始执行
  323. let _baton = baton;
  324. let ref = {
  325. stop: false
  326. }
  327. for(let chain of this.chains) {
  328. if( ref.stop) {
  329. break;
  330. }
  331. _baton = chain.fun(_baton,ref)
  332.  
  333. }
  334. return _baton;
  335. }
  336. }
  337. }
  338. // 请求包装
  339. function request(type, url, { query, body,header = {},config ={}} = {}) {
  340. return new Promise(function(resolve, reject) {
  341. var formData = new FormData();
  342. var isFormData = false;
  343.  
  344. if (body) {
  345. for (var key in body) {
  346. if (body[key] instanceof File) {
  347. formData.append(key, body[key]);
  348. isFormData = true;
  349. } else {
  350. formData.append(key, JSON.stringify(body[key]));
  351. }
  352. }
  353. }
  354.  
  355. var ajaxOptions = {
  356. ...config,
  357. url: url + (query ? ("?" + $.param(query)) : ""),
  358. method: type,
  359. headers: header,
  360. success: function(response) {
  361. resolve(response);
  362. },
  363. error: function(jqXHR, textStatus, errorThrown) {
  364. reject(errorThrown);
  365. }
  366. };
  367.  
  368. if (isFormData) {
  369. ajaxOptions.data = formData;
  370. ajaxOptions.processData = false;
  371. ajaxOptions.contentType = false;
  372. } else {
  373. ajaxOptions.data = JSON.stringify(body);
  374. }
  375. config?.crossDomain
  376. // ajax兼容
  377. ? GM_xmlhttpRequest({
  378. ...ajaxOptions,
  379. onload: (xhr)=>{
  380. ajaxOptions.success(xhr.responseText,xhr)
  381. },
  382. onerror: ajaxOptions.error,
  383. ontimeout: ajaxOptions.error
  384. })
  385. : $.ajax(ajaxOptions);
  386. });
  387. }
  388. // 正则字符串匹配目录字符串-匹配工具
  389. function isMatch(regexStr, text) {
  390. // 创建正则表达式对象
  391. let regex = new RegExp(regexStr);
  392. // 使用 test 方法测试字符串是否匹配
  393. return regex.test(text);
  394. }
  395. // `视图渲染完成`后调用
  396. function waitViewRenderingComplete(callback) {
  397. // 这里模拟的是当下次渲染完成后执行
  398. setTimeout(callback,30)
  399. }
  400. // 结构化的css 转 平铺的css
  401. function flattenCSS(cssObject, parentSelector = '') {
  402. let result = '';
  403.  
  404. for (const [selector, rules] of Object.entries(cssObject)) {
  405. if (typeof rules === 'object') {
  406. // 如果 rules 是对象,说明有嵌套,需要递归处理
  407. const fullSelector = parentSelector ? `${parentSelector} ${selector}` : selector;
  408. result += flattenCSS(rules, fullSelector);
  409. } else {
  410. // 如果 rules 不是对象,说明是样式规则
  411. const fullSelector = parentSelector ? `${parentSelector} { ${selector}: ${rules}; }\n` : '';
  412. result += fullSelector;
  413. }
  414. }
  415.  
  416. return result;
  417. }
  418. // ref引用变量
  419. function ref(initValue = null) {
  420. return {value: initValue}
  421. }
  422. // ==偏业务工具函数==
  423. // 使用责任链模式——对pageText进行操作的工具
  424. class PageTextHandleChains {
  425. pageText = "";
  426. constructor(pageText = "") {
  427. this.pageText = pageText;
  428. }
  429. setPageText(newPageText) {
  430. this.pageText = newPageText;
  431. }
  432. getPageText() {
  433. return this.pageText;
  434. }
  435. // 解析双标签-获取指定标签下指定属性下的值
  436. parseDoubleTab(tabName,attrName) {
  437. // 返回指定标签下指定属性下的值
  438. const regex = RegExp(`<\\s*${tabName}[^<>]*\\s*${attrName}="([^<>]*)"\\s*>([\\s\\S]*?)<\/\\s*${tabName}\\s*>`,"gm");
  439. let m;
  440. let tabNameArr = [];
  441. let copyPageText = this.pageText;
  442. // 注意下面的 copyPageText 不能改变
  443. while ((m = regex.exec(copyPageText)) !== null) {
  444. // 这对于避免零宽度匹配的无限循环是必要的
  445. if (m.index === regex.lastIndex) {
  446. regex.lastIndex++;
  447. }
  448. tabNameArr.push({
  449. attrValue: m[1],
  450. tabValue: m[2]
  451. })
  452. const newPageText =this.pageText.replace(m[0], "");
  453. this.pageText = newPageText;
  454. }
  455. return tabNameArr;
  456. }
  457. // 解析双标签-只获取值
  458. parseDoubleTabValue(tabName) {
  459. // 返回指定标签下指定属性下的值
  460. const regex = RegExp(`<\\s*${tabName}[^<>]*\\s*>([\\s\\S]*?)<\/\\s*${tabName}\\s*>`,"gm");
  461. let m;
  462. let tabNameArr = [];
  463. let copyPageText = this.pageText;
  464. while ((m = regex.exec(copyPageText)) !== null) {
  465. // 这对于避免零宽度匹配的无限循环是必要的
  466. if (m.index === regex.lastIndex) {
  467. regex.lastIndex++;
  468. }
  469. tabNameArr.push({
  470. tabValue: m[1]
  471. })
  472. const newPageText =this.pageText.replace(m[0], "");
  473. this.pageText = newPageText;
  474. }
  475.  
  476. return tabNameArr;
  477. }
  478.  
  479. // 解析所有指定的单标签
  480. parseAllDesignatedSingTags(parseTabName) {
  481. // 匹配标签的正则表达式
  482. const regex = /<(\w+)::([\S]+)(.*?)\/>/g;
  483. // 匹配属性键值对的正则表达式,支持连字符
  484. const attributesRegex = /([\w-]+)="(.*?)"/g;
  485. let matches;
  486. const result = [];
  487. let modifiedString = this.pageText;
  488.  
  489. while ((matches = regex.exec(this.pageText)) !== null) {
  490. const tabName = matches[1];
  491. const tabValue = matches[2];
  492. const attributesString = matches[3];
  493.  
  494. console.log(tabName, parseTabName);
  495. if (tabName !== parseTabName) continue;
  496.  
  497. const attributes = {};
  498. let attrMatch;
  499. while ((attrMatch = attributesRegex.exec(attributesString)) !== null) {
  500. attributes[attrMatch[1]] = attrMatch[2];
  501. }
  502.  
  503. result.push({
  504. tabName,
  505. tabValue,
  506. ...attributes
  507. });
  508.  
  509. // 将匹配到的内容替换为空字符串
  510. modifiedString = modifiedString.replace(matches[0], '');
  511. }
  512.  
  513. // 更新 pageText
  514. this.pageText = modifiedString;
  515. return result;
  516. }
  517. // 根据单标签的元信息进行stringify
  518. rebuildTags(tagMetaArr = []) {
  519. return tagMetaArr.map(tag => {
  520. const { tabName, tabValue, ...attributes } = tag;
  521. const attributesString = Object.entries(attributes)
  522. .map(([key, value]) => `${key}="${value}"`)
  523. .join(' ');
  524. return `<${tabName}::${tabValue} ${attributesString} />`;
  525. }).join('\n');
  526. }
  527. }
  528.  
  529. // 根据反馈的错误项调整templates位置,使得错误的靠后
  530. function feedbackError(saveKey,currentErrorItem) {
  531. let items = cache.get(saveKey)??[];
  532. let foundIndex = -1; // -1-查找模式 , n-已找到 n是所在位置模式
  533. let foundValue = null;
  534. for(let i = 0; i < items.length; i++) {
  535. let item = items[i];
  536. if(foundIndex == -1 ) {
  537. if(item == currentErrorItem) {
  538. foundIndex = i;
  539. foundValue = items[i];
  540. }
  541. }else {
  542. items[i-1] = items[i];
  543. // 查看是否是最后一个
  544. if( i == items.length - 1 ) items[i] = foundValue;
  545. }
  546. }
  547. cache.set(saveKey,items);
  548. return items;
  549. }
  550. // 数据项选择记录器
  551. class SelectHistoryRecorder {
  552. static HISTORY_CACHE_KEY = "HISTORY_CACHE_KEY";
  553. static defaultIdFun = (item)=> JSON.stringify(item);
  554. static select(item,idFun = SelectHistoryRecorder.defaultIdFun) {
  555. // 记录到历史
  556. let key = idFun(item);
  557. let history = cache.get( SelectHistoryRecorder.HISTORY_CACHE_KEY )??[];
  558. history = history.filter(_item=>idFun(_item) != key) // 将原来的去除
  559. history.unshift({...item}) // 必须拷贝
  560. // 清理掉索引,索引只是本次数据加载有效的,而我们存储的历史数据是不随数据加载而改变的,也就是如果缓存索引会失效,没有索引它会自己找,当然我们会提供我们这里的数据给它找,如果在全局数据中匹配不到的话
  561. history.forEach(_item=>{
  562. delete _item.index;
  563. return _item;
  564. })
  565. // 缓存历史数据
  566. cache.set(SelectHistoryRecorder.HISTORY_CACHE_KEY,history);
  567. }
  568. static history(count) {
  569. let history = cache.get( SelectHistoryRecorder.HISTORY_CACHE_KEY )??[];
  570. if(count == null) return history; // 如果没有传入count,那就是全部
  571. let result = [];
  572. for(let i = 0; i < count && i+1 <= history.length; i++) result.push( history[i] ); // 将history前count个放在result数组中
  573. return result;
  574. }
  575.  
  576. }
  577. // 加分、“加分(取分)”
  578. class DataWeightScorer {
  579. static ITEM_WEIGHT_CACHE_KEY = "ITEM_WEIGHT_CACHE_KEY";
  580. static defaultIdFun = (item)=> JSON.stringify(item);
  581. static SCORE_RECORD_ATTR_KEY = "weight";
  582. static select(item,idFun = DataWeightScorer.defaultIdFun) {
  583. let ItemWeightData = cache.get( DataWeightScorer.ITEM_WEIGHT_CACHE_KEY )??{};
  584. let key = idFun(item);
  585. ItemWeightData[key] = (ItemWeightData[key]??0) + 1
  586. cache.set(DataWeightScorer.ITEM_WEIGHT_CACHE_KEY,ItemWeightData)
  587. }
  588. static assign(items=[],idFun = DataWeightScorer.defaultIdFun) {
  589. let ItemWeightData = cache.get( DataWeightScorer.ITEM_WEIGHT_CACHE_KEY)??{};
  590. items.forEach(item=>{
  591. let key = idFun(item);
  592. item[DataWeightScorer.SCORE_RECORD_ATTR_KEY] = ItemWeightData[key]??0;
  593. })
  594. return items;
  595. }
  596. static sort(items=[],idFun = DataWeightScorer.defaultIdFun) {
  597. // 将权重赋于
  598. DataWeightScorer.assign(items,idFun);
  599. // 根据权重排序(高->低)
  600. return items.sort((a, b) => b[DataWeightScorer.SCORE_RECORD_ATTR_KEY] - a[DataWeightScorer.SCORE_RECORD_ATTR_KEY]);
  601. }
  602. // 获取高频前count项
  603. static highFrequency(count) {
  604. let ItemWeightData = cache.get( DataWeightScorer.ITEM_WEIGHT_CACHE_KEY )??{};
  605. let orderKeys = Object.keys(ItemWeightData).sort((a, b) => ItemWeightData[b] - ItemWeightData[a]);
  606. if(count != null) orderKeys = orderKeys.slice(0, count);
  607. // keys转items
  608. return registry.searchData.matchItemsByKeys(orderKeys);
  609. }
  610. }
  611. // 将多个
  612. function parseTis(bodyText) {
  613. // 提取整个tis标签的正则
  614. const regex = /(<\s*tis::http[^<>]+\/\s*>)/gm;
  615. let raw = captureRegEx(regex,bodyText);
  616. if(raw != null) {
  617. return raw.map(item=>item[1])
  618. }
  619. return null;
  620. }
  621. let USER_GITHUB_TOKEN_CACHE_KEY = "USER_GITHUB_TOKEN_CACHE_KEY";
  622. let GithubAPI = {
  623. token: cache.get(USER_GITHUB_TOKEN_CACHE_KEY),
  624. defaultToken: '',
  625. setToken(token) {
  626. if(token != null) this.token = token;
  627. if(this.token == null) {
  628. token = prompt("请输入您的github Token (只缓存在你的本地):")
  629. // 获取的内容无效
  630. if(token == null || token == "") return this;
  631. // 内容有效-设置
  632. cache.set(USER_GITHUB_TOKEN_CACHE_KEY,this.token = token);
  633. }
  634. return this;
  635. },
  636. clearToken() {
  637. cache.remove(USER_GITHUB_TOKEN_CACHE_KEY)
  638. this.token = null;
  639. },
  640. getToken(isRequest = false) {
  641. if(this.token == null && isRequest) this.setToken();
  642. return this.token;
  643. },
  644. baseRequest(type,url,{query,body}={},header = {}) {
  645. query = {...query}
  646. return request(type, url, { query,body,header });
  647. },
  648. getUserInfo() {
  649. return this.baseRequest("GET","https://api.github.com/user")
  650. },
  651. commitIssues(body) {
  652. const header = {Authorization:`Bearer ${this.token}`};
  653. return this.baseRequest("POST","https://api.github.com/repos/My-Search/TisHub/issues",{ body, header })
  654. },
  655. // get issues不要加 Authorization 头,可能会出现401
  656. getTisForIssues({keyword,state} = {}) {
  657. let query = null;
  658. if(state != null) query = {state};
  659. let token = this.token;
  660. if(token == null) token = this.defaultToken;
  661. return keyword
  662. ? new Promise((resolve,reject)=>{
  663. // API兼容处理
  664. this.baseRequest("GET",`https://api.github.com/search/issues?q=repo:My-Search/TisHub+state:${state}+in:title+${keyword}`)
  665. .then(response=>resolve(response.items)).catch(error=>resolve([]));
  666. })
  667. : this.baseRequest("GET","https://api.github.com/repos/My-Search/TisHub/issues",{query})
  668. }
  669. }
  670.  
  671. // 从订阅标签中提取订阅链接
  672. let TisHub = {
  673. // 将第一个tis集与第二个tis集合并
  674. tisFilter(source,filterList) {
  675. if(typeof source == "string") source = parseTis(source);
  676. if(typeof filterList == "string") filterList = parseTis(filterList);
  677. for(let filterItem of filterList) {
  678. let pageTextHandler = new PageTextHandleChains(filterItem);
  679. let tabMetaInfos = pageTextHandler.parseAllDesignatedSingTags("tis");
  680. let subscribedLink = null;
  681. // 一个filterItem解析出元信息返回tabMetaInfos只能有一个元素,如果是多个,只取一个
  682. if(tabMetaInfos != null && tabMetaInfos.length > 0 ) subscribedLink = tabMetaInfos[0].tabValue;
  683. // 如果取不出来,说明tis无效,断言不需要解析filterItem就是subscribedLink
  684. if(subscribedLink == null) subscribedLink = filterItem;
  685. // subscribedLink 这里是filterItem用于filter source的元素实体,下面开始过滤
  686. source = source.filter(resultSubscribed=>! resultSubscribed.includes(subscribedLink));
  687. }
  688. return source;
  689. },
  690. getTisHubAllTis(filterList = []) {
  691. return new Promise((resolve,reject)=>{
  692. let openIssuesTisPromise = this.getOpenIssuesTis();
  693. let result = [];
  694. return Promise.all([ this.getOpenIssuesTis(), this.getClosedIssuesTis() ]).then(values=>{
  695. for(let value of values) {
  696. if(value == null ) continue;
  697. for(let tisListObj of value) {
  698. if(tisListObj != null ) result.push(...tisListObj.tisList)
  699. }
  700. }
  701. // 过滤并提交结果
  702. resolve(this.tisFilter(result,filterList));
  703. })
  704. })
  705. },
  706. // {keyword,state} .其中state {open, closed, all}
  707. getTisForIssues(params = {}) {
  708. return new Promise((resolve,reject)=>{
  709. GithubAPI.getTisForIssues(params).then(response=>{
  710. if(response != null && Array.isArray(response)) {
  711. resolve(response.map(obj=>{return {
  712. owner: obj.user.login,
  713. ownerProfile: obj.user.html_url,
  714. title: obj.title,
  715. tisList: parseTis(obj.body),
  716. status: obj.state
  717. }}))
  718. }
  719. }).catch(error=>resolve([]));
  720. })
  721. },
  722. getOpenIssuesTis(params = {}) {
  723. return this.getTisForIssues({state: "open",...params});
  724. },
  725. getClosedIssuesTis(params = {}) {
  726. return this.getTisForIssues({state: "closed",...params});
  727. },
  728. tisListToTisText(tisList) {
  729. let text = "";
  730. for(let tis of tisList) text += tis.tisList;
  731. return text;
  732. }
  733. }
  734.  
  735. // 全局注册表
  736. let ERROR = {
  737. tell(info) {
  738. console.error("ERROR " + info)
  739. }
  740. }
  741. let registry = {
  742. view: {
  743. viewVisibilityController: () => { ERROR.tell("视图未初始化,但你使用了它的未初始化的注册表信息!") },
  744. viewDocument: null, // 视图挂载后有值
  745. element: null, // 存放着视图的关键元素对象 视图挂载后有值
  746. tis: {
  747. beginTis(msg) {
  748. if(msg == null || msg.length === 0) return;
  749. const tisDocument = document.querySelector("#my_search_box > #tis");
  750. tisDocument.innerHTML = msg;
  751. tisDocument.display = "block";
  752. console.log("设置结束")
  753. return ()=>{
  754. tisDocument.innerHTML = ""; // 置空消息内容
  755. tisDocument.display = "none"; // 让tis不可见
  756. }
  757. }
  758. },
  759. setButtonVisibility: () => { ERROR.tell("按钮未初始化!") },
  760. titleTagHandler: {
  761. handlers: [],
  762. // 标题tag处理器
  763. execute: function (title) {
  764. // 去掉标题内容只剩下tags
  765. let arr = captureRegEx(/(\[.*\])/gm,title)
  766. if(arr == null || arr[0] == null || arr[0][0] == null) return "";
  767. let tagStr = arr[0][0];
  768. for(let titleTagHandler of this.handlers) {
  769. let result = titleTagHandler(tagStr.trim());
  770. if(result != -1) return result;
  771. }
  772. return tagStr;
  773. }
  774. },
  775. viewFirstShowEventListener: [],
  776. viewHideEventAfterListener: [],
  777. // 在查看详情内容时,返回后事件
  778. itemDetailBackAfterEventListener: [
  779. // 简讯内容隐藏-确定存在触发的事件 - 脚本环境变量置空
  780. () => registry.script.clearMSSE()
  781. ],
  782. // 视图延时隐藏时间,避免点击右边logo,还没显示就隐藏了
  783. delayedHideTime: 100,
  784. initialized: false,
  785. textView: {
  786. cssFillPrefix(css = "", prefix = "") {
  787. const cssBlocks = css.split('}');
  788. let outputCSS = '';
  789. for (let block of cssBlocks) {
  790. let blockLines = block.split('\n');
  791. let blockOutput = '';
  792.  
  793. for (let line of blockLines) {
  794. // 判断行末是否以 `{` 或 `,` 结尾且行首不能有空格
  795. if ((line.trim().endsWith('{') || line.trim().endsWith(','))
  796. && !line.startsWith(' ') && !line.trim().startsWith('@')) {
  797. blockOutput += `${prefix} ${line.trim()}`; // 在当前行前加上前缀
  798. } else {
  799. blockOutput += line; // 其他行保持原样
  800. }
  801. blockOutput += '\n'; // 添加换行符,用于分隔CSS内容的各行
  802. }
  803.  
  804. if (blockOutput.trim() !== '') {
  805. outputCSS += blockOutput;
  806. outputCSS += '}\n'; // 只有在当前块不为空时添加闭合大括号
  807. }
  808. }
  809. return outputCSS;
  810. },
  811. show(html,css = "",js = "") {
  812. const MS_BODY_ID = "ms-page-body";
  813. html = `<style type='text/css'>${this.cssFillPrefix(css,`#${registry.view.viewDocument.id} #${registry.view.element.textView.attr('id')} #${MS_BODY_ID}` )}</style>`
  814. + `<div id="${MS_BODY_ID}">${html}</div>`
  815. // 这里在函数内执行js是为了在同一页面未刷新下可多次执行该js不执行,也就是对变量/函数等进行隔离
  816. +`<script>(()=>{ ${js} })()</script>`
  817. let my_search_box = $(registry.view.viewDocument);
  818. // 视图还没有初始化
  819. if(my_search_box == null) return;
  820. let matchResult = registry.view.element.matchResult;
  821. let textView = registry.view.element.textView
  822. textView.html(html);
  823. /*使用code代码块样式*/
  824. document.querySelectorAll('#text_show pre code').forEach((el) => {
  825. // 这里没有错,发警告不用理
  826. hljs.highlightElement(el);
  827. });
  828. matchResult.css({
  829. "display": "none"
  830. })
  831. textView.css({
  832. "display":"block"
  833. })
  834. waitViewRenderingComplete(() => {
  835. const currentKey = registry.searchData.searchHistory.currentKeyword();
  836. if(currentKey.trim().length > 3) scrollToText(currentKey,0,"#ms-page-body");
  837. });
  838. }
  839. },
  840. // 搜索框logo控制
  841. logo: {
  842. // logo src值
  843. originalLogoImgSrc: null,
  844. // logo按钮是否按下状态
  845. isLogoButtonPressedRef: ref(false),
  846. getLogoImg: function () {
  847. let viewDocument = registry.view.viewDocument;
  848. if(viewDocument == null ) return null;
  849. let currentLogoImg = registry.view.element.logoButton.find("img");
  850. if(this.originalLogoImgSrc == null) this.originalLogoImgSrc = currentLogoImg.attr("src");
  851. return currentLogoImg;
  852. },
  853. change: function (imgResource) {
  854. let logoImg = this.getLogoImg();
  855. if(imgResource == null || logoImg == null ) return;
  856. logoImg.attr("src",imgResource)
  857. },
  858. reset: function() {
  859. let logoImg = this.getLogoImg();
  860. if (logoImg == null ) return;
  861. logoImg.attr("src",this.originalLogoImgSrc)
  862. }
  863. },
  864. modeEnum: {
  865. UN_INIT: -2, // 未初始化
  866. HIDE: -1, // 隐藏
  867. WAIT_SEARCH: 0, // 待搜索模式
  868. SHOW_RESULT: 1, // 结果显示模式
  869. SHOW_ITEM_DETAIL: 2 // 查看项详情 (简述内容查看/脚本页)
  870. },
  871. seeNowMode() {
  872. if(this.viewDocument == null) return this.modeEnum.UN_INIT;
  873. if(this.element.textView.css('display')!== "none") return this.modeEnum.SHOW_ITEM_DETAIL;
  874. if(this.element.matchResult.css('display') !== "none") return this.modeEnum.SHOW_RESULT;
  875. return this.viewDocument.style.display !== "none" ? this.modeEnum.WAIT_SEARCH : this.modeEnum.HIDE;
  876. }
  877. },
  878. other: {
  879. UPDATE_CDNS_CACHE_KEY: "UPDATE_CDNS_CACHE_KEY"
  880. },
  881. searchData: {
  882. // 处理的历史
  883. processHistory: [],
  884. // 用于数据显示后,数据又更新了
  885. version: 0,
  886. // 全局搜索数据
  887. data: [],
  888. // 数据更新后有效时长
  889. effectiveDuration: 1000*60*60*12,
  890. // 数据设置到全局中的时间
  891. dataMountTime: null,
  892. clearData() {
  893. this.data = [];
  894. this.dataMountTime = null;
  895. },
  896. setData(data) {
  897. if(data == null || data.length == 0) return;
  898. this.data = data;
  899. this.dataMountTime = Date.now();
  900. },
  901. getData() {
  902. let dataPackage = cache.get(registry.searchData.SEARCH_DATA_KEY);
  903. if(dataPackage == null || dataPackage.data == null) return this.data;
  904. // 缓存信息不为空,深入判断是否使用缓存的数据
  905. let updateDataTime = dataPackage.expire - this.effectiveDuration;
  906. // 如果数据在挂载后面已经更新了,重新加载数据到全局中
  907. // 全局data (即this.data)与dataMountTime是同时设置的,两者理论上是必须同时有值的
  908. if(this.data == null || updateDataTime > this.dataMountTime) {
  909. console.logout("== 数据未加载或已检查到在其它页面已重新更新数据 ==")
  910. this.setData(dataPackage.data);
  911. }
  912. return this.data;
  913. },
  914. // 根据keys(由idFun决定)从data中匹配items
  915. matchItemsByKeys: function (keys = []) {
  916. let that = this;
  917. if(keys.length == 0) return [];
  918. // 有keys转items
  919. let items = keys.map(key=>{
  920. for(let item of that.data) {
  921. if(that.idFun(item) == key) return item;
  922. }
  923. return null;
  924. })
  925. // 对数组keys去空,注意此时keys已经是items了
  926. return items.filter(item => item != null);
  927. },
  928. specialKeyword: { // 特殊的keyword
  929. new: "<new>",
  930. history: "<history>",
  931. highFrequency: "<highFrequency>"
  932. },
  933. // 决定着数据是否要再次初始化
  934. isDataInitialized: false,
  935. // 从可自定义搜索数据中根据title与desc进行数据匹配
  936. findSearchDataItem: function (title = "",desc = "",matchData) {
  937. if(matchData == null ) matchData = this.data;
  938. for(let item of matchData) {
  939. if( (item.title.includes(title) || title.includes(item.title) )
  940. && ( item.desc.includes(desc) || desc.includes(item.desc) )) return item;
  941. }
  942. return null;
  943. },
  944. // 数组差异-获取不同的元素比较的基值
  945. idFun(item) { // 自定义比较
  946. if(item == null || !( item instanceof Object && item.title != null)) return null;
  947. return item.title.replace(/\[.*\]/,"").trim()+(""+item.desc).trim();
  948. },
  949. // 旧的新数据缓存KEY
  950. OLD_SEARCH_DATA_KEY: "OLD_SEARCH_DATAS_KEY",
  951. // 标签数据缓存KEY
  952. DATA_ITEM_TAGS_CACHE_KEY: "DATA_ITEM_TAGS_CACHE_KEY",
  953. // 用户维护的不关注标签列表,缓存KEY
  954. USER_UNFOLLOW_LIST_CACHE_KEY: "USER_UNFOLLOW_LIST_CACHE_KEY",
  955. // 用户安装tishub订阅的缓存
  956. USE_INSTALL_TISHUB_CACHE_KEY: "USE_INSTALL_TISHUB_CACHE_KEY",
  957. // 默认用户不关注标签
  958. USER_DEFAULT_UNFOLLOW: ["成人内容","Adults only"],
  959. // 已经清理了用户不关注的与隐藏的标签,这是用户应真正搜索的数据
  960. CLEANED_SEARCH_DATA_CACHE_KEY: "CLEANED_SEARCH_DATA_CACHE_KEY",
  961. subscribeKey: "subscribeKey",
  962. showSize: 15,
  963. isSearchAll: false,
  964. searchEven: {
  965. event:{},
  966. // 搜索状态,失去焦点隐藏的一要素
  967. isSearching:false,
  968. async send(search,rawKeyword) {
  969. try {
  970. // 标记为搜索进行中
  971. this.isSearching = true;
  972. for(let subscriptionRegular of Object.keys(this.event)) {
  973. const regex = new RegExp(subscriptionRegular,"i"); // 将正则字符串转换为正则表达式对象
  974. if(regex.test(rawKeyword) && typeof this.event[subscriptionRegular] == "function" ) {
  975. return this.event[subscriptionRegular](search,rawKeyword);
  976. }
  977. }
  978. return await search(rawKeyword);
  979. }finally {
  980. // 标记为搜索结束
  981. this.isSearching = false;
  982. }
  983. }
  984. },
  985. // 新数据设置的过期天数
  986. NEW_DATA_EXPIRE_DAY_NUM:7,
  987. // 搜索逻辑,可用来手动触发搜索
  988. triggerSearchHandle: function (keyword){
  989. // 获取input元素
  990. const inputEl = registry.view.element?.input;
  991. // 如果视图还没有初始化,触发搜索无效
  992. if(! inputEl) return;
  993. if(keyword == null) {
  994. keyword = inputEl.val() ?? ""
  995. }else {
  996. inputEl.val(keyword);
  997. }
  998. // 手动触发input事件
  999. inputEl[0].dispatchEvent(new Event("input", { bubbles: true }));
  1000. // 维护全局搜索keyword
  1001. this.keyword = keyword;
  1002. },
  1003. // 数据改变事件
  1004. dataChangeEventListener: [],
  1005. // 缓存被删除事件
  1006. dataCacheRemoveEventListener:[],
  1007. onSearch: [],
  1008. // 新数据块处理完成事件
  1009. // 更新搜索数据的责任链
  1010. USDRC: getResponsibilityChain(),
  1011. onNewDataBlockHandleAfter: [],
  1012. // 新数据的tag
  1013. NEW_ITEMS_TAG: "[新]",
  1014. // 搜索的keyword
  1015. keyword: "",
  1016. // 搜索的附加文件
  1017. searchForFile: {
  1018. files: [],
  1019. refreshFileListView() {
  1020. registry.view.element.files.html('')
  1021. for(let [index,file] of this.files.entries()) {
  1022. const reader = new FileReader();
  1023. reader.onload = function (e) {
  1024. // 创建 img 元素并设置 src 为文件的 Base64 数据
  1025. registry.view.element.files.append(`
  1026. <div class="ms-input-file" key="${index}">
  1027. <img src="${e.target.result}" />
  1028. </div>
  1029. `);
  1030. };
  1031. reader.readAsDataURL(file);
  1032. }
  1033. },
  1034. push(file) {
  1035. this.files.push(file);
  1036. this.refreshFileListView();
  1037. },
  1038. delete() {
  1039. this.files.pop();
  1040. this.refreshFileListView();
  1041. },
  1042. clear() {
  1043. const trashCan = this.files;
  1044. this.files = [];
  1045. this.refreshFileListView();
  1046. return trashCan;
  1047. },
  1048. start() {
  1049. registry.view.element.input.on('paste', event => {
  1050. // 希望input外层元素无法感知到paste的动作
  1051. event.stopPropagation();
  1052. // 获取粘贴事件对象中的剪贴板数据
  1053. const clipboardData = event.originalEvent.clipboardData || window.clipboardData;
  1054. if (clipboardData) {
  1055. const items = clipboardData.items;
  1056. for (let i = 0; i < items.length; i++) {
  1057. const file = items[i].getAsFile();
  1058. if (! file.type.startsWith('text/')) this.push(file);
  1059. }
  1060. }
  1061. });
  1062. registry.view.viewHideEventAfterListener.push(() => this.clear());
  1063. }
  1064. },
  1065. // 持久化Key
  1066. SEARCH_DATA_KEY: "SEARCH_DATA_KEY",
  1067. SEARCH_NEW_ITEMS_KEY:"SEARCH_NEW_ITEMS_KEY",
  1068. // 搜索搜索出来的数据
  1069. searchData: [],
  1070. pos: 0,
  1071. clearUrlSearchTemplate(url) {
  1072. return url.replace(/\[\[[^\[\]]*\]\]/gm,"");
  1073. },
  1074. faviconSources: [ // favicon来源:https://api.cxr.cool/
  1075. // "https://favicon.yandex.net/favicon/${domain}", 淘汰原因:当获取不到favicon时不报错,而是显示空白图标
  1076. // "https://api.cxr.cool/ico/?url=${domain}", 淘汰原因:慢
  1077. // "https://api.vvhan.com/api/ico?url=${domain}", 淘汰原因:快,但存在很多网站的图标无法获取
  1078. // "https://statistical-apricot-seahorse.faviconkit.com/${domain}/32", 淘汰原因:有些图标无法获取,但会根据网站生成其网站单字符图标
  1079. // "https://favicons.fuzqing.workers.dev/api/getFavicon?url=${rootUrl}", 淘汰原因:快,但存在很多网站的图标无法获取
  1080. // "https://tools.ly522.com/ico/favicon.php?url=${rootUrl}", 淘汰原因:废的,10个8个获取不到
  1081. "https://api.iowen.cn/favicon/${domain}.png", // 神
  1082. "https://api.xinac.net/icon/?url=${rootUrl}", // 可选:但有点慢
  1083. // "https://favicon.qqsuu.cn/${rootUrl}", 淘汰原因:外国站点不行
  1084. // "https://api.uomg.com/api/get.favicon?url=${rootUrl}", 淘汰原因:外国站点不行
  1085. // "https://api.vvhan.com/api/ico?url=${domain}", // 淘汰原因:存在很多网站的图标无法获取
  1086. // "https://api.15777.cn/get.php?url=${rootUrl}",// 淘汰原因:存在很多网站的图标无法获取
  1087. "https://ico.txmulu.com/${domain}", // 可选,但有些站点不行
  1088. // "https://api.cxr.cool/ico/?url=${domain}", // 很多网站获取不到,国外站点不行
  1089. "${rootUrl}/favicon.ico", // 永久有效的兜底
  1090. ],
  1091. CACHE_FAVICON_SOURCE_KEY: "CACHE_FAVICON_SOURCE_KEY", // 可见远程源
  1092. CACHE_FAVICON_SOURCE_TIMEOUT: 1000*60*60*4, // 4个小时重新检测一下favicon源/过期时间,只会在呼出搜索框后检查
  1093. getFaviconAPI: (function(){
  1094. let defaultFaviconUrlTemplate = "${rootUrl}/favicon.ico";
  1095. let faviconUrlTemplate = defaultFaviconUrlTemplate;
  1096. let isRemoteTemplate = false;
  1097. // 查看是否已经检查模板
  1098. function checkTemplateAndUpdateTemplate() {
  1099. let faviconSourceCache = cache.get(registry.searchData.CACHE_FAVICON_SOURCE_KEY);
  1100. if( !isRemoteTemplate && faviconSourceCache != null && faviconSourceCache.sourceTemplate != null ) {
  1101. faviconUrlTemplate = faviconSourceCache.sourceTemplate;
  1102. // 设置已经是远程Favicon模板
  1103. isRemoteTemplate = true;
  1104. }
  1105. }
  1106. return function(url,isStandby = false) {
  1107. checkTemplateAndUpdateTemplate();
  1108. let useFaviconUrlTemplate = faviconUrlTemplate;
  1109. // 如果是要获取备用favicon,那直接使用上面定义的faviconUrlTemplate
  1110. if(isStandby) useFaviconUrlTemplate = defaultFaviconUrlTemplate;
  1111. // 去掉资源的“可搜索”模板,才是真正的URL
  1112. url = registry.searchData.clearUrlSearchTemplate(url);
  1113. // 将资源url放到获取favicon的源模板中
  1114. let urlObj = parseUrl(url)
  1115. return useFaviconUrlTemplate.fillByObj(urlObj);
  1116. }
  1117. })(),
  1118. tmpVar: null, // 用于防抖
  1119. searchPlaceholder(target = "SELECT",placeholder,duration = 1200) {
  1120. // 全部的输入提示
  1121. let inputDescs = ["我的搜索"];
  1122. // 当前应用“输入提示”
  1123. let inputDesc = inputDescs[Math.floor(Math.random()*inputDescs.length)];
  1124. if(target == "UPDATE") {
  1125. const inputEl = registry.view.element?.input;
  1126. // 如果视图还没挂载,无需显示
  1127. if(! inputEl) return;
  1128. if(this.tmpVar != null) {
  1129. clearTimeout(this.tmpVar);
  1130. }
  1131. this.tmpVar = setTimeout(()=>{
  1132. inputEl.attr("placeholder",this.searchPlaceholder());
  1133. },duration)
  1134. let updateResult = placeholder==null?`🔁 数据库更新到 ${this.data==null?0:this.data.length}条`:placeholder;
  1135. inputEl.attr("placeholder",updateResult);
  1136. return updateResult;
  1137. }
  1138. return inputDesc;
  1139.  
  1140. },
  1141. // 存储着text转pinyin的历史 registry.searchData.subSearch.isSubSearchMode
  1142. TEXT_PINYIN_KEY: "TEXT_PINYIN_MAP",
  1143. // 默认数据不应初始化,不然太占内存了,只用调用了toPinyin才会初始化 getGlobalTextPinyinMap()
  1144. getGlobalTextPinyinMap: (function() {
  1145. let textPinyinMap = null;
  1146. return function (){
  1147. if(textPinyinMap != null) return textPinyinMap;
  1148. return (textPinyinMap = cache.jGet("TEXT_PINYIN_MAP")??{});
  1149. }
  1150. })(),
  1151. subSearch: {
  1152. searchBoundary: " : ",
  1153. isEnteredSubSearchMode: false, // 是否已经进入子搜索模式
  1154. // 不传参数是看当前是否为子搜索模式 , [0] 是最近一个
  1155. isSubSearchMode(by = undefined) {
  1156. let byKeyword = typeof(by) === 'string'
  1157. ? by // by就是keyword
  1158. : (by === undefined ? registry.searchData.searchHistory.currentKeyword() : registry.searchData.searchHistory.history[by]); // by是index
  1159. return byKeyword && byKeyword.includes(this.searchBoundary);
  1160. },
  1161. // 获取父级(根keyword)
  1162. getParentKeyword(keyword) {
  1163. // 如果没有传入使用搜索框的value
  1164. if(! keyword) keyword = registry.searchData.searchHistory.currentKeyword();
  1165. return (keyword || "").split(this.searchBoundary)[0].trim()
  1166. },
  1167. // 获取子搜索keyword
  1168. getSubSearchKeyword(keyword) {
  1169. // 如果没有传入使用搜索框的value
  1170. if(! keyword) keyword = registry.searchData.searchHistory.currentKeyword();
  1171. let _arr = (keyword || "").split(this.searchBoundary);
  1172. if( _arr.length < 2 ) return undefined;
  1173. return _arr[1].trim();
  1174. }
  1175.  
  1176. },
  1177. searchHistory: {
  1178. history: [], // 新,旧...
  1179. add(keyword) {
  1180. if(! keyword) return;
  1181. // 维护isEnteredSubSearchMode变量状态(进入与退出)
  1182. const searchBoundary = registry.searchData.subSearch.searchBoundary
  1183. if(keyword !== searchBoundary && keyword.endsWith(searchBoundary)) {
  1184. registry.searchData.subSearch.isEnteredSubSearchMode = true;
  1185. console.logout("进入了子搜索")
  1186. }else if(registry.searchData.subSearch.isEnteredSubSearchMode && !keyword.includes(searchBoundary)){
  1187. registry.searchData.subSearch.isEnteredSubSearchMode = false;
  1188. console.logout("退出了子搜索")
  1189. }
  1190. // 加入到历史
  1191. const _history = this.history;
  1192. _history.unshift(keyword);
  1193. _history.slice(10); // 不能超过10个元素
  1194. },
  1195. currentKeyword() {
  1196. return registry.view.element.input.val()
  1197. },
  1198. // 当前keyword "123 : 哈哈" 与 最近"123 : 嘻嘻" 则返回true,即看左边
  1199. seeCurrentEqualsLastByRealKeyword() {
  1200. // 上一次真实搜索keyword === 当前真实搜索keyword
  1201. return registry.searchData.subSearch.getParentKeyword(this.history[0]) === registry.searchData.subSearch.getParentKeyword(this.currentKeyword());
  1202. }
  1203. },
  1204. searchProTag: "[可搜索]",
  1205. links: {
  1206. stringifyForSearch(links) {
  1207. if(links == null) return ''
  1208. links = links.filter(link => link != null);
  1209. return links.reduce((acc, cur) => acc + `${cur.text}${cur.url}${cur.title}`, '');
  1210. }
  1211. }
  1212. },
  1213. script: {
  1214. // MSSE默认值/模板
  1215. MS_SCRIPT_ENV_TEMPLATE: {
  1216. event: {
  1217. // 发送sub keyword事件
  1218. sendListener: [] // 脚本页监听IPush事件
  1219. },
  1220. // 让脚本页可使用cache,为避免冲突,加前缀于区别
  1221. cache: {...cache, prefix: "MS_SCRIPT_CACHE:"},
  1222. // 让脚本页面获取脚本项数据
  1223. getSearchDB() {
  1224. return [...registry.searchData.getData()]
  1225. },
  1226. // 让脚本页面获取选择的文本
  1227. getSelectedText,
  1228. // 挂载markdown
  1229. md2html,
  1230. // 挂载http request对象
  1231. request
  1232. },
  1233. // 当值为undefined时表示会话未开始
  1234. SESSION_MS_SCRIPT_ENV: undefined,
  1235. openSessionForMSSE() {
  1236. return (this.SESSION_MS_SCRIPT_ENV = (actualWindow.MS_SCRIPT_ENV = this.MS_SCRIPT_ENV_TEMPLATE));
  1237. },
  1238. clearMSSE() {
  1239. this.SESSION_MS_SCRIPT_ENV = undefined;
  1240. },
  1241. // 这个函数会在脚本页渲染完成后调用
  1242. tryRunTextViewHandler() {
  1243. const input = registry.view.element.input;
  1244. const rawKeyword = input.val();
  1245. // 如果还在显示搜索的数据项 执行失败返回false
  1246. if(registry.view.seeNowMode() === registry.view.modeEnum.SHOW_ITEM_DETAIL) {
  1247. // 当msg不为空发送msg消息到脚本
  1248. const subKeyword = registry.searchData.subSearch.getSubSearchKeyword()
  1249. if( subKeyword == undefined && registry.searchData.searchForFile.files.length === 0) return;
  1250. // 通知脚本回车send事件
  1251. const msg = subKeyword;
  1252. // actualWindow是页面真实windows对象
  1253. this.SESSION_MS_SCRIPT_ENV.event.sendListener.forEach(listener=>listener(msg,registry.searchData.searchForFile.clear()))
  1254. // clear : 清理掉send msg内容
  1255. input.val(rawKeyword.replace(msg,""))
  1256. return true;
  1257. }
  1258. return false;
  1259. }
  1260. }
  1261. }
  1262. let dao = {}
  1263.  
  1264. // registry.registry.viewDocument
  1265. // 页面文本选择器
  1266. function getSelectedText(tis = '请选择页面文本') {
  1267. function createTipElement() {
  1268. const tipElement = document.createElement('p');
  1269. tipElement.textContent = tis;
  1270. // 设置行内样式
  1271. tipElement.style.position = 'fixed';
  1272. tipElement.style.top = '0';
  1273. tipElement.style.left = '50%';
  1274. tipElement.style.transform = 'translateX(-50%)';
  1275. tipElement.style.backgroundColor = 'black';
  1276. tipElement.style.color = 'white';
  1277. tipElement.style.padding = '10px 20px';
  1278. tipElement.style.fontSize = '16px';
  1279. tipElement.style.zIndex = '9999';
  1280. tipElement.style.borderRadius = '5px';
  1281. document.body.appendChild(tipElement);
  1282. return tipElement;
  1283. }
  1284. const view = registry.view.viewDocument;
  1285. if(view == null) throw new Error('调用页面文本选择器异常,原因:视图隐藏不允许!')
  1286. let tipElement = createTipElement();
  1287. return new Promise((resolve) => {
  1288. // 先隐藏搜索视图
  1289. view.style.display = "none";
  1290. // 监听鼠标抬起事件(用户结束选择)
  1291. const onMouseUp = () => {
  1292. const selectedText = window.getSelection().toString().trim();
  1293. // 确保用户已选择文本
  1294. if (selectedText) {
  1295. tipElement?.remove();
  1296. view.style.display = "block";
  1297. resolve(selectedText);
  1298. document.removeEventListener('mouseup', onMouseUp);
  1299. }
  1300. };
  1301.  
  1302. // 监听鼠标抬起事件
  1303. document.addEventListener('mouseup', onMouseUp);
  1304. });
  1305. }
  1306. // 网页脚本自动执行函数
  1307. let autoRunStringScript = {
  1308. cacheKey : "autoRunStringScriptKey",
  1309. getData() {
  1310. let scripts = cache.get(this.cacheKey)??{};
  1311. let keys = Object.keys(scripts);
  1312. for(let key of keys) {
  1313. let time = scripts[key].timeout;
  1314. if(Date.now() > time) delete scripts[key];
  1315. }
  1316. cache.set(this.cacheKey,scripts);
  1317. return scripts;
  1318. },
  1319. add(target,funStr,effectiveTime = 5000) {
  1320. if(target == null || ! target.trim().startsWith("http")) return;
  1321. let data = this.getData();
  1322. data[target.trim()] = {
  1323. timeout: Date.now()+effectiveTime,
  1324. handle: funStr
  1325. }
  1326. cache.set(this.cacheKey,data);
  1327. },
  1328. run() {
  1329. let currentPageUrl = document.URL;
  1330. let data = this.getData();
  1331. let keys = Object.keys(data);
  1332. let targetObj = null;
  1333. for(let key of keys) {
  1334. if(key.startsWith(currentPageUrl) || currentPageUrl.startsWith(key)) targetObj = data[key];
  1335. }
  1336. if(targetObj != null) {
  1337. // 从data中失去,再执行
  1338. delete data[currentPageUrl];
  1339. let handle = targetObj.handle;
  1340. if(handle == null) return;
  1341. new Function('$',handle)($);
  1342. }
  1343. }
  1344. }
  1345. // 页面加载执行
  1346. autoRunStringScript.run();
  1347. // 添加页面模拟脚本
  1348. function addPageSimulatorScript(url,scriptStr) {
  1349. scriptStr = `function exector(handle) {
  1350. function selector(select, all = false) {
  1351. return all ? document.querySelectorAll(select) : document.querySelector(select);
  1352. }
  1353. function clicker(select) {
  1354. let element = selector(select);
  1355. if (element != null) element.click();
  1356. }
  1357. function scroller(selector = 'body', topOffset = null, timeConsuming = 2000) {
  1358. return new Promise((resolove,reject)=>{
  1359. var containerElement = $(selector);
  1360. if (containerElement.length > 0) {
  1361. if (topOffset !== null) {
  1362. $('html, body').animate({
  1363. scrollTop: containerElement.offset().top + topOffset
  1364. }, timeConsuming);
  1365. } else {
  1366. $('html, body').animate({
  1367. scrollTop: containerElement.offset().top
  1368. }, timeConsuming);
  1369. }
  1370. } else {
  1371. console.error('找不到指定的元素');
  1372. }
  1373. setTimeout(()=>{resolove(true)},timeConsuming)
  1374. })
  1375. }
  1376. function annotator(select, styleStr = "border:5px solid red;") {
  1377. let element = selector(select);
  1378. if (element == null) return;
  1379. element.style = styleStr;
  1380.  
  1381. }
  1382.  
  1383.  
  1384. handle({
  1385. click: clicker,
  1386. roll: scroller,
  1387. dimension: annotator
  1388. });
  1389. }
  1390. window.onload = function () {
  1391. exector(${scriptStr})
  1392. }`;
  1393. autoRunStringScript.add(url,scriptStr,6000);
  1394. }
  1395.  
  1396.  
  1397.  
  1398. // 判断是否只是url且不应该是URL文本 (用于查看类型)
  1399. function isUrl(resource) {
  1400. // 如果为空或不是字符串,就不是url
  1401. if(resource == null || typeof resource != "string" ) return false;
  1402. // resource是字符串类型
  1403. resource = resource.trim().split("#")[0];
  1404. // 不能存在换行符,如果存在不满足
  1405. if(resource.indexOf("\n") != -1 ) return false;
  1406. // 被“空白符”切割后只能有一个元素
  1407. if(resource.split(/\s+/).length != 1) return false;
  1408. // 如果不满足url,返回false
  1409. return isHttpUrl(resource);
  1410. }
  1411. /*cache.remove(registry.searchData.SEARCH_DATA_KEY);
  1412. cache.remove(registry.searchData.SEARCH_DATA_KEY+"2");
  1413. cache.remove(registry.searchData.SEARCH_NEW_ITEMS_KEY);
  1414. */
  1415. // 设置远程可用Favicon源
  1416. let setFaviconSource = function () {
  1417. function startTestFaviconSources(sources,pos,setFaviconUrlTemplate) {
  1418. if(pos > sources.length - 1) return;
  1419. console.logout(`${pos}/${sources.length-1}: 正在测试 `+sources[pos])
  1420. checkUsability(sources[pos]).then(function(result) {
  1421. console.logout("使用的源:"+ sources[pos])
  1422. setFaviconUrlTemplate(result);
  1423. }).catch(function() {
  1424. startTestFaviconSources(sources,++pos,setFaviconUrlTemplate)
  1425. });
  1426. }
  1427. let cacheFaviconSourceData = cache.get(registry.searchData.CACHE_FAVICON_SOURCE_KEY);
  1428. let currentTime = new Date().getTime();
  1429. let timeout = registry.searchData.CACHE_FAVICON_SOURCE_TIMEOUT;
  1430.  
  1431. let faviconSources = registry.searchData.faviconSources;
  1432. // 生成favicon源镜像
  1433. function currentSourceArraySnapshot() {
  1434. return JSON.stringify(faviconSources);
  1435. }
  1436. if(cacheFaviconSourceData == null || currentTime - cacheFaviconSourceData.updateTime > timeout || cacheFaviconSourceData.sourceArraySnapshot != currentSourceArraySnapshot()) {
  1437. if(cacheFaviconSourceData != null) {
  1438. console.logout(`==之前检查的已超时或源发现了修改,重新设置Favicon源==`);
  1439. }
  1440. let pos = 0;
  1441. let promise = null;
  1442. function setFaviconUrlTemplate(source = null) {
  1443. console.logout("Test compled, set source! "+source)
  1444. if(source != null) {
  1445. cache.set(registry.searchData.CACHE_FAVICON_SOURCE_KEY, {
  1446. updateTime: new Date().getTime(),
  1447. sourceTemplate: source,
  1448. sourceArraySnapshot: currentSourceArraySnapshot()
  1449. })
  1450. }
  1451. }
  1452. // 去测试index=0的源, 当失败,会向后继续测试
  1453. if(faviconSources.length < 1) return;
  1454. startTestFaviconSources(faviconSources,0,setFaviconUrlTemplate);
  1455.  
  1456. }else {
  1457.  
  1458. console.logout(`Favicon${(timeout - (currentTime - cacheFaviconSourceData.updateTime))/1000}s后测试`);
  1459. }
  1460. }
  1461. // 判断是否要执行设置源,如果之前没有设置过的话就要设置,而不是通过事件触发
  1462. if(cache.get(registry.searchData.CACHE_FAVICON_SOURCE_KEY) == null ) setTimeout(()=>{setFaviconSource();},2000);
  1463. // 添加事件(视图在页面中初次显示时)
  1464. registry.view.viewFirstShowEventListener.push(setFaviconSource);
  1465.  
  1466. // 【函数库】
  1467. // 加载样式
  1468. function loadStyleString(css) {
  1469. var style = document.createElement("style");
  1470. style.type = "text/css";
  1471. try {
  1472. style.appendChild(document.createTextNode(css));
  1473. } catch(ex) {
  1474. style.styleSheet.cssText = css;
  1475. }
  1476. var head = document.getElementsByTagName('head')[0];
  1477. head.appendChild(style);
  1478. return style;
  1479. }
  1480. // 加载html
  1481. function loadHtmlString(html) {
  1482. // 创建一个新的 div 元素
  1483. var newDiv = document.createElement("div");
  1484. // 设置新的 div 的内容为要追加的 HTML 字符串
  1485. newDiv.innerHTML = html;
  1486. // 将新的 div 追加到 body 的末尾
  1487. document.body.appendChild(newDiv);
  1488. return newDiv;
  1489. }
  1490. // Div方式的Page页(比如构建配置面板视图)
  1491. function DivPage(cssStr,htmlStr,handle) {
  1492. let style = loadStyleString(cssStr);
  1493. let div = loadHtmlString(htmlStr);
  1494. function selector(select,isAll = false) {
  1495. if(isAll) {
  1496. return div.querySelectorAll(select);
  1497. }else {
  1498. return div.querySelector(select);
  1499. }
  1500. }
  1501. function remove() {
  1502. div.remove();
  1503. style.remove();
  1504. }
  1505. handle(selector,remove);
  1506. }
  1507. // 异步函数
  1508. function asyncExecFun(fun,time = 20) {
  1509. setTimeout(()=>{
  1510. fun();
  1511. },time)
  1512. }
  1513. // 同步执行函数
  1514. let syncActuator = function () {
  1515. return (function () {
  1516. let queue = [];
  1517. let vote = 0;
  1518. let timer = null;
  1519. // 确保定时器已经在运行
  1520. function ensureTimerRuning() {
  1521. if (timer != null) return;
  1522. timer = setInterval(async () => {
  1523. let taskItem = queue.pop();
  1524. if (taskItem != null) {
  1525. taskItem.active = true;
  1526. await taskItem.task;
  1527. // 任务执行完,消耗一票
  1528. vote--;
  1529. if (vote <= 0) {
  1530. clearInterval(timer);
  1531. timer = null;
  1532. }
  1533. }
  1534. }, 100);
  1535. }
  1536. return function (handleFun, args, that) {
  1537. // 让票加一
  1538. vote++;
  1539. // 确保定时器运行
  1540. ensureTimerRuning();
  1541. let taskItem = {
  1542. active: false,
  1543. task: null
  1544. }
  1545. taskItem.task = new Promise((resolve, reject) => {
  1546. let timer = null;
  1547. timer = setInterval(async () => {
  1548. if (taskItem.active) {
  1549. await resolve(handleFun.apply(that ?? window, args));
  1550. clearInterval(timer);
  1551. }
  1552. }, 30)
  1553. })
  1554. queue.unshift(taskItem)
  1555. return taskItem.task;
  1556. }
  1557. })()
  1558. }
  1559. // 全页面“询问”函数
  1560. function askIsExpiredByTopic(topic,validTime=10*1000) {
  1561. let currentTime = new Date().getTime();
  1562. let lastTime = cache.get(topic);
  1563. let isExpired = lastTime == null || lastTime + validTime < currentTime;
  1564. if(isExpired) {
  1565. // 获取到资格,需要标记
  1566. cache.set(topic,currentTime);
  1567. }
  1568. return isExpired;
  1569. }
  1570. // 移除数组中重复元素的函数
  1571. function removeDuplicates(objs,selecter) {
  1572. let itemType = objs[0] == null?false:typeof objs[0];
  1573. // 比较两个属性相等
  1574. function compareObjects(obj1, obj2) {
  1575. if(selecter != null ) return selecter(obj1) == selecter(obj2);
  1576. if(itemType != "object" ) return obj1 == obj2;
  1577. // 如果是对象且selecter没有传入时,比较对象的全部属性
  1578. const keys1 = Object.keys(obj1);
  1579. const keys2 = Object.keys(obj2);
  1580.  
  1581. if (keys1.length !== keys2.length) {
  1582. return false;
  1583. }
  1584.  
  1585. for (let key of keys1) {
  1586. if (!(key in obj2) || obj1[key] !== obj2[key]) {
  1587. return false;
  1588. }
  1589. }
  1590. return true;
  1591. }
  1592. for(let i = 0; i< objs.length; i++ ) {
  1593. let item1 = objs[i];
  1594. for(let j = i+1; j< objs.length; j++ ) {
  1595. let item2 = objs[j];
  1596. if(item2 == null ) continue;
  1597. if( compareObjects(item1,item2) ) {
  1598. objs[i] = null;
  1599. break;
  1600. }
  1601. }
  1602. }
  1603. // 去掉无效新数据(item == null)-- 必须先去重
  1604. return objs.filter((item, index) => item != null);
  1605. }
  1606. // 【追加原型函数】
  1607. // 往字符原型中添加新的方法 matchFetch
  1608. String.prototype.matchFetch=function (regex,callback) {
  1609. let str = this;
  1610. // Alternative syntax using RegExp constructor
  1611. // const regex = new RegExp('\\[\\[[^\\[\\]]*\\]\\]', 'gm')
  1612. let m;
  1613. let length = 0;
  1614. while ((m = regex.exec(str)) !== null) {
  1615. // 这对于避免零宽度匹配的无限循环是必要的
  1616. if (m.index === regex.lastIndex) {
  1617. regex.lastIndex++;
  1618. }
  1619.  
  1620. // 结果可以通过`m变量`访问。
  1621. m.forEach((match, groupIndex) => {
  1622. length++;
  1623. callback(match, groupIndex);
  1624. });
  1625. }
  1626. return length;
  1627. };
  1628. // 往字符原型中添加新的方法 matchFetch
  1629. String.prototype.fillByObj=function (obj) {
  1630. if(obj == null ) return null;
  1631. let template = this;
  1632. let resultUrl = template;
  1633. for(let key of Object.keys(obj)) {
  1634. let regexStr = `\\$\\s*?{[^{}]*${key}[^{}]*}`;
  1635. resultUrl = resultUrl.replace(new RegExp(regexStr),obj[key]);
  1636. }
  1637. if(/\$.*?{.*?}/.test(resultUrl)) return null;
  1638. return resultUrl;
  1639. }
  1640. // 比较两个数组是否相等(顺序不相同不影响)
  1641. function isArraysEqual (arr1,arr2) {
  1642. if( arr2 == null || arr1.length != arr2.length ) return false;
  1643. for(let arr1Item of arr1) {
  1644. let f = false;
  1645. for(let arr2Item of arr2) {
  1646. if(arr1Item == arr2Item ) {
  1647. f = true;
  1648. break;
  1649. }
  1650. }
  1651. if(! f) return false;
  1652. }
  1653. return true;
  1654. }
  1655.  
  1656. function compareArrayDiff (arr1, arr2, idFun = () => null,diffRange = 3) { // diffRange值:“1”是左边多的,“2”是右边数组多的,3是左右合并,0是相同的部分,30是两个数组去重的
  1657. function hashString(obj) {
  1658. let str = JSON.stringify(obj);
  1659. let hash = 0;
  1660. [...str].forEach((char) => {
  1661. hash += char.charCodeAt(0);
  1662. });
  1663. return "" + hash;
  1664. }
  1665. if (arr2 == null || arr2.length == 0) return arr1;
  1666. // arr1与arr2都为数组对象
  1667. // 将arr1生成模板
  1668. let template = {};
  1669. for (let item of arr1) {
  1670. let itemHash = hashString(idFun(item) ?? item);
  1671.  
  1672. if (template[itemHash] == null) template[itemHash] = [];
  1673. template[itemHash].push(item);
  1674. }
  1675. let leftDiff = [];
  1676. let rightDiff = [];
  1677. let overlap = [];
  1678. // arr2根据arr1的模板进行比对
  1679. for (let item of arr2) {
  1680. let itemHash = hashString(idFun(item) ?? item);
  1681. let hitArr = template[itemHash];
  1682. let item2Json = idFun(item) ?? JSON.stringify(item);
  1683. if (hitArr != null) {
  1684. // 模板中存在
  1685. for (let hitIndex in hitArr) {
  1686. let hashItem = hitArr[hitIndex];
  1687. // 判断冲突是否真的相同
  1688. let item1Json = idFun(hashItem) ?? JSON.stringify(hashItem);
  1689. if (item1Json == item2Json) {
  1690. // 命中-将arr1命中的删除
  1691. delete hitArr.splice(hitIndex, 1);
  1692. overlap.push( {...item, ...hashItem} );
  1693. break;
  1694. }
  1695. }
  1696. } else {
  1697. // 模板不存在,是差异项
  1698. rightDiff.push(item);
  1699. }
  1700. }
  1701. // 将模板中未命中的收集
  1702. for (let templateKey in template) {
  1703. let templateValue = template[templateKey]; //templateValue 是数组
  1704. if (templateValue == null || !(templateValue instanceof Array)) continue;
  1705. for (let templateValueItem of templateValue) {
  1706. leftDiff.push(templateValueItem);
  1707. }
  1708. }
  1709. // 根据参数,返回指定的数据
  1710. switch (diffRange) {
  1711. case 0:
  1712. return overlap;
  1713. break;
  1714. case 1:
  1715. return leftDiff;
  1716. break;
  1717. case 2:
  1718. return rightDiff;
  1719. break;
  1720. case 3:
  1721. return [...leftDiff, ...rightDiff];
  1722. break;
  1723. case 30:
  1724. return [...leftDiff, ...rightDiff, ...overlap];
  1725. }
  1726. }
  1727. // 保证replaceAll方法替换后也可以正常
  1728. String.prototype.toReplaceAll = function(str1,str2) {
  1729. return this.split(str1).join(str2);
  1730. }
  1731. // 向原型中添加方法:文字转拼音
  1732. String.prototype.toPinyin = function (isOnlyFomCacheFind= false,options = { toneType: 'none', type: 'array' }) {
  1733. let textPinyinMap = registry.searchData.getGlobalTextPinyinMap();
  1734. // 查看字典中是否存在
  1735. if(textPinyinMap[this] != null) {
  1736. // console.logout("命中了")
  1737. return textPinyinMap[this];
  1738. }
  1739. // 如果 isOnlyFomCacheFind = true,那返回原数据
  1740. if(isOnlyFomCacheFind) return null;
  1741.  
  1742. // console.logout("字典没有,将进行转拼音",Object.keys(textPinyinMap).length)
  1743. let {pinyin} = pinyinPro;
  1744. let text = this;
  1745. let space = "<Space>"
  1746. let spaceChar = " ";
  1747. text = text.toReplaceAll(spaceChar,space)
  1748. let pinyinArr = pinyin(text,options);
  1749. // 保存到全局字典对象 ( 会话级别 )
  1750. textPinyinMap[this] = pinyinArr.join("").toReplaceAll(space,spaceChar).toUpperCase();
  1751. return textPinyinMap[this];
  1752. }
  1753. // 加载全局样式
  1754. loadStyleString(`
  1755. /*搜索视图样式*/
  1756. #searchBox {
  1757. height: 45px;
  1758. background: #ffffff;
  1759. padding: 0 10px;
  1760. box-sizing: border-box;
  1761. z-index: 10001;
  1762. position: relative;
  1763. display: flex;
  1764. align-items: center;
  1765. flex-wrap: nowrap;
  1766. }
  1767. #searchBox #ms-input-files {
  1768. display: flex;
  1769. flex-wrap: nowrap;
  1770. align-items: center;
  1771. height: 100%;
  1772. }
  1773. #ms-input-files .ms-input-file {
  1774. height: 70%;
  1775. display: flex;
  1776. align-items: center;
  1777. margin-right: 2px;
  1778. }
  1779. #ms-input-files .ms-input-file img {
  1780. height: 100%;
  1781. box-sizing: border-box;
  1782. padding: 3px;
  1783. background: #d3d3d3;
  1784. }
  1785.  
  1786. #my_search_input {
  1787. text-align: left;
  1788. width: 100%;
  1789. height: 100%;
  1790. border: none;
  1791. outline: none;
  1792. font-size: 15px;
  1793. background: #fff;
  1794. padding: 0px;
  1795. box-sizing: border-box;
  1796. color: rgba(0, 0, 0, .87);
  1797. font-weight: 400;
  1798. margin: 0px;
  1799. }
  1800.  
  1801. #matchResult {
  1802. display: none;
  1803. }
  1804.  
  1805. #matchResult > ol {
  1806. margin: 0px;
  1807. padding: 0px 15px 5px;
  1808. }
  1809.  
  1810. #text_show {
  1811. display: none;
  1812. width: 100%;
  1813. box-sizing: border-box;
  1814. padding: 5px 10px 7px;
  1815. font-size: 15px;
  1816. line-height: 25px;
  1817. max-height: 450px;
  1818. overflow: auto;
  1819. text-align: left;
  1820. color: #000000;
  1821. user-select: text !important; /* 允许用户选中复制 */
  1822. }
  1823. #text_show img {
  1824. width: 100%;
  1825. }
  1826. #text_show .copy-btn {
  1827. position: absolute;
  1828. top: 8px;
  1829. right: 8px;
  1830. background: #7f7f7fa1;
  1831. color: white;
  1832. border: none;
  1833. padding: 3px 10px;
  1834. font-size: 12px;
  1835. cursor: pointer;
  1836. border-radius: 3px;
  1837. opacity: 0.8;
  1838. transition: opacity 0.3s;
  1839. }
  1840.  
  1841. #text_show .copy-btn:hover {
  1842. opacity: 1;
  1843. }
  1844.  
  1845. /*定义字体*/
  1846. @font-face {
  1847. font-family: 'HarmonyOS';
  1848. src: url('https://s1.hdslb.com/bfs/static/jinkela/long/font/HarmonyOS_Medium.a1.woff2');
  1849. }
  1850. #my_search_view {
  1851. font-family: 'HarmonyOS', sans-serif !important;
  1852. }
  1853. .searchItem {
  1854. background-image: url(data:image/jpeg;base64,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);
  1855. background-size: 100% 100%;
  1856. background-clip: content-box;
  1857. background-origin: content-box;
  1858. }
  1859.  
  1860. #my_search_input {
  1861. animation-duration: 1s;
  1862. animation-name: my_search_view;
  1863. outline: none;
  1864. border: none;
  1865. box-shadow: none;
  1866. }
  1867. #my_search_input:focus{
  1868. outline: none;
  1869. border: none;
  1870. box-shadow: none;
  1871. }
  1872.  
  1873. .resultItem {
  1874. animation-duration: 0.5s;
  1875. animation-name: resultItem;
  1876. }
  1877. .resultItem .enter_main_link{
  1878. display: flex !important ;
  1879. justify-content: start;
  1880. align-items: center;
  1881. flex-grow:3;
  1882. }
  1883. /*关联图标样式*/
  1884. .resultItem .vassal {
  1885. /*对下面的svg位置进行调整*/
  1886. display: flex !important;
  1887. align-items: center;
  1888. flex-shrink:0;
  1889. margin-right:2px;
  1890. }
  1891. .related-links {
  1892. margin: 0 3px;
  1893. display: flex;
  1894. gap: 4px; /* 增加链接之间的间距 */
  1895. }
  1896.  
  1897. .related-links > a {
  1898. white-space: nowrap;
  1899. line-height: 16px;
  1900. font-size: 12px;
  1901. padding: 3px 10px;
  1902. background: #f0f4ff;
  1903. color: #3578FE;
  1904. text-decoration: none;
  1905. transition: all 0.3s ease;
  1906. }
  1907.  
  1908. .related-links > a:hover {
  1909. background: #e6f0ff; /* 悬停时背景颜色变浅 */
  1910. border-color: #cce1ff; /* 改变边框颜色 */
  1911. color: #255ec8; /* 深蓝色字体加深 */
  1912. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 增加阴影效果 */
  1913. }
  1914.  
  1915. .resultItem svg{
  1916. width: 16px;
  1917. height:16px;
  1918. }
  1919. @-webkit-keyframes my_search_view {
  1920.  
  1921. 0% {
  1922. width: 0px;
  1923. }
  1924.  
  1925. 50% {
  1926. width: 50%;
  1927. }
  1928.  
  1929. 100% {
  1930. width: 100%;
  1931. }
  1932. }
  1933.  
  1934. @-webkit-keyframes resultItem {
  1935.  
  1936. 0% {
  1937. opacity: 0;
  1938. }
  1939.  
  1940. 40% {
  1941. opacity: 0.6;
  1942. }
  1943.  
  1944. 50% {
  1945. opacity: 0.7;
  1946. }
  1947.  
  1948. 60% {
  1949. opacity: 0.8;
  1950. }
  1951.  
  1952. 100% {
  1953. opacity: 1;
  1954. }
  1955. }
  1956.  
  1957. /*简述超链接样式*/
  1958. #text_show a {
  1959. color: #1a0dab !important;
  1960. text-decoration:none;
  1961. }
  1962. /*自定义markdown的html样式*/
  1963. #text_show>p>code {
  1964. padding: 2px 0.4em;
  1965. font-size: 95%;
  1966. background-color: rgba(188, 188, 188, 0.2);
  1967. border-radius: 5px;
  1968. line-height: normal;
  1969. font-family: SFMono-Regular,Consolas,'Liberation Mono',Menlo,monospace;
  1970. color: #558eda;
  1971. }
  1972. #my_search_input::placeholder {
  1973. color: #757575;
  1974. }
  1975. /*简述文本颜色为统一*/
  1976. #text_show p {
  1977. color: #202122;
  1978. }
  1979. /*让简述内容的li标签不受页面样式影响*/
  1980. #text_show > ul > li {
  1981. list-style-type: disc !important;
  1982. }
  1983. #text_show > ul > li > ul > li {
  1984. list-style-type: circle !important;
  1985. }
  1986. #text_show > ol > li {
  1987. list-style-type: decimal !important;
  1988. }
  1989. /*当视图大于等于1400.1px时*/
  1990. @media (min-width: 1400.1px) {
  1991. #my_search_box {
  1992. left: 24%;
  1993. right:24%;
  1994. }
  1995. }
  1996. /*当视图小于等于1400px时*/
  1997. @media (max-width: 1400px) {
  1998. #my_search_box {
  1999. left: 20%;
  2000. right:20%;
  2001. }
  2002. }
  2003. /*当视图小于等于1200px时*/
  2004. @media (max-width: 1200px) {
  2005. #my_search_box {
  2006. left: 15%;
  2007. right:15%;
  2008. }
  2009. }
  2010. /*当视图小于等于800px时*/
  2011. @media (max-width: 800px) {
  2012. #my_search_box {
  2013. left: 10%;
  2014. right:10%;
  2015. }
  2016. }
  2017. /*输入框右边按钮*/
  2018. #logoButton {
  2019. position: absolute;
  2020. font-size: 12px;
  2021. right: 5px;
  2022. padding: 0px;
  2023. border: none;
  2024. display: block;
  2025. background: rgba(255, 255, 255, 0);
  2026. margin: 0px 7px 0px 0px;
  2027. cursor: pointer !important;
  2028. outline: none;
  2029. }
  2030. #logoButton:active {
  2031. opacity: 0.4;
  2032. }
  2033. #logoButton img {
  2034. display: block;
  2035. width: 25px;
  2036. }
  2037.  
  2038. /*代码颜色*/
  2039. #text_show code,#text_show pre{
  2040. color:#5f6368;
  2041. }
  2042.  
  2043.  
  2044. /* 滚动条整体宽度 */
  2045. #text_show::-webkit-scrollbar,
  2046. #text_show pre::-webkit-scrollbar {
  2047. -webkit-appearance: none;
  2048. width: 5px;
  2049. height: 5px;
  2050. }
  2051.  
  2052. /* 滚动条滑槽样式 */
  2053. #text_show::-webkit-scrollbar-track,
  2054. #text_show pre::-webkit-scrollbar-track {
  2055. background-color: #f1f1f1;
  2056. }
  2057.  
  2058. /* 滚动条样式 */
  2059. #text_show::-webkit-scrollbar-thumb,
  2060. #text_show pre::-webkit-scrollbar-thumb {
  2061. background-color: #c1c1c1;
  2062. }
  2063.  
  2064. #text_show::-webkit-scrollbar-thumb:hover,
  2065. #text_show pre::-webkit-scrollbar-thumb:hover {
  2066. background-color: #a8a8a8;
  2067. }
  2068.  
  2069. #text_show::-webkit-scrollbar-thumb:active,
  2070. #text_show pre::-webkit-scrollbar-thumb:active {
  2071. background-color: #a8a8a8;
  2072. }
  2073. /*结果项样式*/
  2074. #matchResult li {
  2075. line-height: 30.2px;
  2076. height: 30.2px;
  2077. color: #0088cc;
  2078. list-style: none;
  2079. width: 100%;
  2080. padding: 0.5px;
  2081. display: flex;
  2082. justify-content: space-between;
  2083. align-items: center;
  2084. margin: 0.5px 0 !important;
  2085. }
  2086.  
  2087. #matchResult li > a {
  2088. display: inline-block;
  2089. font-size: 15.5px;
  2090. text-decoration: none;
  2091. text-align: left;
  2092. cursor: pointer;
  2093. font-weight: 400;
  2094. background: rgb(255 255 255 / 0%);
  2095. overflow: hidden;
  2096. text-overflow: ellipsis;
  2097. white-space: nowrap;
  2098. margin: 0 3px;
  2099. }
  2100. #matchResult .flag {
  2101. color: #fff;
  2102. height: 21px;
  2103. line-height: 21px;
  2104. font-size: 10px;
  2105. padding: 0px 6px;
  2106. border-radius: 5px;
  2107. font-weight: 600;
  2108. box-sizing: border-box;
  2109. margin-right: 3.5px;
  2110. box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 0.5px;
  2111. }
  2112. #matchResult .item_title {
  2113. color: #1a0dab;
  2114. }
  2115. #matchResult .obsolete {
  2116. text-decoration:line-through;
  2117. color:#a8a8a8;
  2118. }
  2119. #matchResult .item_desc {
  2120. color: #474747;
  2121. overflow: hidden;
  2122. text-overflow: ellipsis;
  2123. white-space: nowrap;
  2124. }
  2125.  
  2126. #matchResult img {
  2127. display: inline-block;
  2128. width: 24px;
  2129. height: 24px;
  2130. margin: 0 6px 0 3px;
  2131. box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
  2132. border-radius: 30%;
  2133. box-sizing: border-box;
  2134. padding: 3px;
  2135. flex-shrink: 0; /* 当容量不够时,不压缩图片的大小 */
  2136. }
  2137. #my_search_box {
  2138. position: fixed;top:50px;
  2139. border:2px solid #cecece;z-index:2147383656;
  2140. background: #ffffff;
  2141. }
  2142. #my_search_box > #tis {
  2143. position: absolute;
  2144. left: 5px;
  2145. top: -20px;
  2146. font-size: 12px;
  2147. color: #d5a436;
  2148. font-weight: bold;
  2149. }
  2150.  
  2151. `)
  2152.  
  2153.  
  2154. //防抖函数模板
  2155. function debounce(fun, wait) {
  2156. let timer = null;
  2157. return function (...args) {
  2158. // 清除原来的定时器
  2159. if (timer) clearTimeout(timer)
  2160. // 开启一个新的定时器
  2161. timer = setTimeout(() => {
  2162. fun.apply(this, args)
  2163. }, wait)
  2164. }
  2165. }
  2166. // 判断是否为指定指令
  2167. function isInstructions(cmd) {
  2168. let searchInputDocument = registry.view.element.input;
  2169. if(searchInputDocument == null) return false;
  2170. let regexString = "^\\s*:" + cmd + "\\s*$";
  2171. let regex = new RegExp(regexString,"i");
  2172. return regex.test(searchInputDocument.val());
  2173. }
  2174.  
  2175.  
  2176.  
  2177. // 获取一个同步执行器实例
  2178. let pinyinActuator = syncActuator();
  2179. // 向数据项中加入拼音项 如:title加了titlePinyin, desc加了descPinyin
  2180. function genDataItemPinyin(threadHandleItems){
  2181. let textPinyinMap = registry.searchData.getGlobalTextPinyinMap();
  2182. // console.logout("分配的预热item:",threadHandleItems)
  2183. pinyinActuator(()=>{
  2184. if(threadHandleItems.length < 1) return;
  2185. for(let item of threadHandleItems) {
  2186. // 查看字典是否存在,只有没有预热过再预热
  2187. if( textPinyinMap[threadHandleItems.title] != null ) continue;
  2188. item.title.toPinyin();
  2189. item.desc.toPinyin();
  2190. }
  2191. // 持久化-textPinyinMap字典 (这里需要判断是否值已经被初始化)
  2192. if(textPinyinMap != null ) {
  2193. cache.jSet(registry.searchData.TEXT_PINYIN_KEY,textPinyinMap);
  2194. }
  2195. });
  2196. }
  2197. // 当页面加载完成时触发-转拼音库操作
  2198. const refresh = debounce(()=>{
  2199. console.logout("==pinyin word==")
  2200. let threadHandleItemSize = 100;
  2201. let threadHandleItems = [];
  2202. let currentSize = 0;
  2203. let data = registry.searchData.getData();
  2204. for(let item of data) {
  2205. // 加入处理容器中
  2206. threadHandleItems.push(item);
  2207. currentSize++;
  2208. // 判断是否已满
  2209. if(currentSize >= threadHandleItemSize || data[data.length-1] == item ) {
  2210. // 已满-去操作
  2211. genDataItemPinyin(threadHandleItems);
  2212. // 重置数据
  2213. currentSize = 0;
  2214. threadHandleItems = [];
  2215. }
  2216. }
  2217. }, 2000)
  2218. registry.searchData.dataChangeEventListener.push(refresh);
  2219.  
  2220. // 实现模块一:使用快捷键触发指定事件
  2221. function triggerAndEvent(goKeys = "ctrl+alt+s", fun, isKeyCode = false) {
  2222. // 监听键盘按下事件
  2223.  
  2224. let handle = function (event) {
  2225. let isCtrl = goKeys.indexOf("ctrl") >= 0;
  2226. let isAlt = goKeys.indexOf("alt") >= 0;
  2227. let lastKey = goKeys.replace("alt", "").replace("ctrl", "").replace(/\++/gm,"").trim();
  2228. // 判断 Ctrl+S
  2229. if (event.ctrlKey != isCtrl || event.altKey != isAlt) return;
  2230. if (!isKeyCode) {
  2231. // 查看 lastKey == 按下的key
  2232. if (lastKey.toUpperCase() == event.key.toUpperCase()) fun();
  2233. } else {
  2234. // 查看 lastKey == event.keyCode
  2235. if (lastKey == event.keyCode) fun();
  2236. }
  2237.  
  2238. }
  2239. // 如果使用 document.onkeydown 这种,只能有一个监听者
  2240. $(document).keyup(handle);
  2241. }
  2242. function codeCopyMount(elementSelector) {
  2243. document.querySelectorAll(`${elementSelector} pre code`).forEach((codeBlock) => {
  2244. // 创建复制按钮
  2245. const copyButton = document.createElement("button");
  2246. copyButton.innerText = "复制";
  2247. copyButton.classList.add("copy-btn");
  2248.  
  2249. // 复制代码逻辑
  2250. copyButton.addEventListener("click", () => {
  2251. const text = codeBlock.innerText || codeBlock.textContent;
  2252. navigator.clipboard.writeText(text).then(() => {
  2253. copyButton.innerText = "已复制";
  2254. setTimeout(() => (copyButton.innerText = "复制"), 2000);
  2255. }).catch(err => {
  2256. console.error("复制失败:", err);
  2257. });
  2258. });
  2259.  
  2260. // 让 <pre> 相对定位,以便按钮放在右上角
  2261. const pre = codeBlock.parentElement;
  2262. pre.style.position = "relative";
  2263.  
  2264. // 添加按钮到 <pre> 容器
  2265. pre.appendChild(copyButton);
  2266. });
  2267. }
  2268.  
  2269. // 【数据初始化】
  2270. // 获取存在的订阅信息
  2271. function getSubscribe() {
  2272. // 查看是否有订阅信息
  2273. let subscribeKey = registry.searchData.subscribeKey;
  2274. let subscribeInfo = cache.get(subscribeKey);
  2275. if(subscribeInfo == null ) {
  2276. // 初始化订阅信息(初次)
  2277. subscribeInfo = `
  2278. <tis::https://raw.githubusercontent.com/18476305640/xiaozhuang/dev/%E6%88%91%E7%9A%84%E6%90%9C%E7%B4%A2%E8%AE%A2%E9%98%85%E6%96%87%E4%BB%B6.txt />
  2279. `;
  2280. cache.set(subscribeKey,subscribeInfo);
  2281. }
  2282. return subscribeInfo;
  2283. }
  2284. function editSubscribe(subscribe) {
  2285. // 判断导入的订阅是否有效
  2286. // 获取订阅信息(得到的值肯定不会为空)
  2287. let pageTextHandleChainsY = new PageTextHandleChains(subscribe);
  2288. let tisArr = pageTextHandleChainsY.parseAllDesignatedSingTags("tis");
  2289. // 生成订阅信息存储
  2290. let subscribeText = "\n" + pageTextHandleChainsY.rebuildTags(tisArr) + "\n";
  2291. // 持久化
  2292. let newSubscribeInfo = subscribeText.replace(/\n+/gm,"\n\n");
  2293. cache.set(registry.searchData.subscribeKey,newSubscribeInfo);
  2294. return tisArr.length;
  2295. }
  2296. // 存储订阅信息,当指定 sLineFetchFun 时,表示将解析“直接页”的配置,如果没有指定 sLineFetchFun 时,只解析内容
  2297. // 在提取函数中 \n 要改写为 \\n
  2298. function getDataSources() {
  2299. let localDataSources = `
  2300. <fetchFun name="mLineFetchFun">
  2301. function(pageText) {
  2302. let type = "sketch"; // url sketch
  2303. let lines = pageText.split("\\n");
  2304. let search_data_lines = []; // 扫描的搜索数据 {},{}
  2305. let current_build_search_item = {};
  2306. let appendTarget = "resource"; // resource 或 vassal
  2307. let current_build_search_item_resource = ""; // 主要内容
  2308. let current_build_search_item_vassal = ""; // 附加内容
  2309. let current_build_search_item_links = []; // 快捷链接列表
  2310. let point = 0; // 指的是上面的 current_build_search_item
  2311. let default_desc = "--无描述--"
  2312. function extractLinkInfo(str) {
  2313. const regex = /\\[(.*?)\\]\\((https?:\\/\\/[^\\s]+)\\s*(?:\\s+"([^"]+)?")?\\s*\\)/;
  2314. const match = str.match(regex);
  2315. if (match) {
  2316. return {
  2317. text: match[1], // 链接文本
  2318. url: match[2], // URL
  2319. title: match[3] || '' // 标题,如果没有则为空字符串
  2320. };
  2321. } else {
  2322. return null; // 如果没有匹配到内容,返回 null
  2323. }
  2324. }
  2325. function isOnlyLinkLine(str) {
  2326. // 按行拆分,并检查每一行
  2327. return !str.split('\\n').some(line => line.trim() !== '' && !line.trim().startsWith('> '));
  2328. }
  2329. function getTitleLineData(titleLine) {
  2330. try {
  2331. const regex = /^#\\s*([^((]+)(?:[((](.*)[))])?\\s*$/;
  2332. let matchData = regex.exec(titleLine)
  2333. return {
  2334. title: matchData[1],
  2335. desc: ((matchData[2]==null || matchData[2] == "")?default_desc:matchData[2])
  2336. }
  2337. }catch(e) {
  2338. debugger
  2339. }
  2340. }
  2341. // 是否为空字符串,忽略空格/换行
  2342. function isBlank(str) {
  2343. const trimmedStr = str.replace(/\\s+/g, '').replace(/[\\n\\r]+/g, '');
  2344. return trimmedStr === '';
  2345. }
  2346. for (let i = 0; i < lines.length; i++) {
  2347. let line = lines[i];
  2348. if(line.indexOf("# ") == 0) {
  2349. // 当前新的开始工作
  2350. point++;
  2351. // 创建新的搜索项目容器
  2352. current_build_search_item = {...getTitleLineData(line)}
  2353. // 重置resource
  2354. current_build_search_item_resource = "";
  2355. continue;
  2356. }
  2357. // 如果是刚开始,没有标题的内容行,跳过
  2358. if(point == 0) continue;
  2359. // 判断是否开始为附加内容
  2360. if(/^\s*-{3,}\s*$/gm.test(line)) {
  2361. appendTarget = "vassal"
  2362. // 分割行不添加
  2363. continue
  2364. }
  2365.  
  2366. // 向当前搜索项目容器追加当前行
  2367. if(appendTarget == "resource") {
  2368. current_build_search_item_resource += (line+"\\n");
  2369. }else {
  2370. // 判断当前行是否为特殊行-快捷link行
  2371. if(isOnlyLinkLine(current_build_search_item_vassal) && (line.trim().length > 0 && isOnlyLinkLine(line)) ) {
  2372. current_build_search_item_links.push(extractLinkInfo(line))
  2373. }else {
  2374. current_build_search_item_vassal += (line+"\\n");
  2375. }
  2376. }
  2377.  
  2378. // 如果是最后一行,打包
  2379. let nextLine = lines[i+1];
  2380. if(i === lines.length-1 || ( nextLine != null && nextLine.indexOf("# ") == 0 )) {
  2381. // 加入resource,最后一项
  2382. current_build_search_item.resource = current_build_search_item_resource;
  2383. if(! isBlank(current_build_search_item_vassal)) {
  2384. current_build_search_item.vassal = current_build_search_item_vassal;
  2385. }
  2386. if(current_build_search_item_links.length > 0) {
  2387. current_build_search_item.links = current_build_search_item_links;
  2388. }
  2389. // 打包装箱
  2390. search_data_lines.push(current_build_search_item);
  2391. // 重置资源
  2392. appendTarget = "resource"
  2393. current_build_search_item_resource = "";
  2394. current_build_search_item_vassal = "";
  2395. current_build_search_item_links = [];
  2396. }
  2397. }
  2398. // 添加种类
  2399. for(let line of search_data_lines) {
  2400. line.type = type;
  2401. }
  2402. return search_data_lines;
  2403. }
  2404. </fetchFun>
  2405. <fetchFun name="sLineFetchFun">
  2406. function(pageText) {
  2407. let type = "url"; // url sketch
  2408. let lines = pageText.split("\\n");
  2409. let search_data_lines = []
  2410. for (let line of lines) {
  2411.  
  2412. let search_data_line = (function(line) {
  2413. const baseReg = /([^::\\n(())]+)[((]([^()()]*)[))]\\s*[::]\\s*(.+)/gm;
  2414. const ifNotDescMatchReg = /([^::]+)\\s*[::]\\s*(.*)/gm;
  2415. let title = "";
  2416. let desc = "";
  2417. let resource = "";
  2418.  
  2419. let captureResult = null;
  2420. if( !(/[()()]/.test(line))) {
  2421. // 兼容没有描述
  2422. captureResult = ifNotDescMatchReg.exec(line);
  2423. if(captureResult == null ) return;
  2424. title = captureResult[1];
  2425. desc = "-暂无描述信息-";
  2426. resource = captureResult[2];
  2427. }else {
  2428. // 正常语法
  2429. captureResult = baseReg.exec(line);
  2430. if(captureResult == null ) return;
  2431. title = captureResult[1];
  2432. desc = captureResult[2];
  2433. resource = captureResult[3];
  2434. }
  2435. return {
  2436. title: title,
  2437. desc: desc,
  2438. resource: resource
  2439. };
  2440. })(line);
  2441. if (search_data_line == null || search_data_line.title == null) continue;
  2442. search_data_lines.push(search_data_line)
  2443. }
  2444.  
  2445. for(let line of search_data_lines) {
  2446. line.type = type;
  2447. }
  2448. return search_data_lines;
  2449. }
  2450. </fetchFun>
  2451. ` + getSubscribe();
  2452. return new Promise(async (resolve,reject)=>{
  2453. // 这里请求tishub datasources
  2454. // [ {name: "官方订阅",body: "<tis::http... />",status: ""} ] // status: disable enable
  2455. const installHubTisList = cache.get(registry.searchData.USE_INSTALL_TISHUB_CACHE_KEY) || [];
  2456. const installDataSources = installHubTisList.map(installTis => `${installTis.body}`).join("\n");
  2457. resolve(installDataSources+localDataSources);
  2458. })
  2459. }
  2460.  
  2461.  
  2462. // 判断是否是github文件链接
  2463. let githubUrlTag = "raw.githubusercontent.com";
  2464. // cdn模板+数据=完整资源加速链接 -> 返回
  2465. function cdnTemplateWrapForUrl(cdnTemplate,initUrl) {
  2466. let result = parseUrl(initUrl)??{};
  2467. if(Object.keys(result) == 0 ) return null;
  2468. return cdnTemplate.fillByObj(result);
  2469. }
  2470. // github CDN加速包装器
  2471. // 根据传入的状态,返回适合的新状态(状态中包含资源加速下载链接|原始链接|null-表示不再试)
  2472. let cdnPack = (function () { // index = 1 用原始的(不加速链接), -2 表示原始链接打不开此时要退出
  2473.  
  2474. let cdnrs = cache.get(registry.other.UPDATE_CDNS_CACHE_KEY);
  2475. // 提供的加速模板(顺序会在后面的请求中进行重排序-请求错误反馈的使重排序)
  2476. // protocol、domain、path、params
  2477. let initCdnrs = ["https://ghproxy.net/${rootUrl}${path}","https://ghps.cc/${rootUrl}${path}","https://github.moeyy.xyz/${rootUrl}${path}"];
  2478. // 如果我们修改了最开始提供的加速模板,比如新添加/删除了一个会使用新的
  2479. if(cdnrs == null || ! isArraysEqual(initCdnrs,cdnrs) ) {
  2480. cdnrs = initCdnrs;
  2481. cache.set(registry.other.UPDATE_CDNS_CACHE_KEY,initCdnrs);
  2482. }
  2483. return function ({index,url,initUrl}) {
  2484.  
  2485. if( index <= -2 ) return null;
  2486. // 如果已经遍历完了 或 不满足github url 不使用加速
  2487. if(index == -1 || index > cdnrs.length -1 || (index == 0 && ! url.includes(githubUrlTag)) ) {
  2488. url = initUrl;
  2489. index--;
  2490. console.logout("无法加速,将使用原链接!")
  2491. return {index,url,initUrl};
  2492. }
  2493. let cdnTemplate = cdnrs[index++];
  2494. url = cdnTemplateWrapForUrl(cdnTemplate,initUrl);
  2495. if(index == cdnrs.length) index = -1;
  2496. return {index,url,initUrl};
  2497. }
  2498. })();
  2499.  
  2500. // 模块四:初始化数据源
  2501.  
  2502. // 从 订阅信息(或页) 中解析出配置(json)
  2503. function getConfigFromDataSource(pageText) {
  2504.  
  2505. let config = {
  2506. // {url、fetchFun属性}
  2507. tis: [],
  2508. // {name与fetchFun属性}
  2509. fetchFuns: []
  2510. }
  2511. // 从config中放在返回对象中
  2512. let pageTextHandleChainsX = new PageTextHandleChains(pageText);
  2513. let fetchFunTabDatas = pageTextHandleChainsX.parseDoubleTab("fetchFun","name");
  2514. for(let fetchFunTabData of fetchFunTabDatas) {
  2515. config.fetchFuns.push( { name:fetchFunTabData.attrValue,fetchFun:fetchFunTabData.tabValue } )
  2516. }
  2517. // 获取tis
  2518. let tisMetaInfos = pageTextHandleChainsX.parseAllDesignatedSingTags("tis");
  2519. config.tis.push( ...tisMetaInfos )
  2520. return config;
  2521.  
  2522. }
  2523. // 将url转为文本(url请求得到的就是文本),当下面的dataSourceUrl不是http的url时,就会直接返回,不作请求
  2524. function urlToText(dataSourceUrl) {
  2525. // dataSourceUrl 转text
  2526. return new Promise(function (resolve, reject) {
  2527. // 如果不是URL,那直接返回
  2528. if( ! isHttpUrl(dataSourceUrl) ) return resolve(dataSourceUrl) ;
  2529. let allCdns = cache.get(registry.other.UPDATE_CDNS_CACHE_KEY);
  2530. function rq( cdnRequestStatus ) {
  2531. let {index,url,initUrl} = cdnRequestStatus??{};
  2532. // -2 表示加速链接+原始链接都不会请求成功(异常) ,null表示index状态已经是-2了还去请求返回null
  2533. if(index == null || index < -2 ) return;
  2534. request("GET",url,{query: {time: new Date().getTime()} ,config : {timeout: 5000} }).then(resolve).catch(()=>{
  2535. console.log("CDN失败,不加速请求!");
  2536. // 反馈错误,调整请求顺序,避免错误还是访问
  2537. // 获取请求错误的根域名
  2538. let { domain } = parseUrl(url);
  2539. // 根据根域名从模板中找出完整域名
  2540. let templates = allCdns.filter(item=>item.includes(domain));
  2541. // 反馈
  2542. if(templates.length > 0 ) {
  2543. if(index > 0 || index <= cache.get(registry.other.UPDATE_CDNS_CACHE_KEY).length ) feedbackError(registry.other.UPDATE_CDNS_CACHE_KEY,templates[0]);
  2544. }
  2545. console.logout("反馈重调整后:",cache.get(registry.other.UPDATE_CDNS_CACHE_KEY)); // 反馈的结果只会在下次起作用
  2546. // 处理失败后的回调函数代码
  2547. rq(cdnPack({index,url,initUrl}));
  2548. })
  2549.  
  2550. }
  2551. rq(cdnPack({index:0,url:dataSourceUrl,initUrl:dataSourceUrl}));
  2552. });
  2553. }
  2554. // 下面的 dataSourceHandle 函数
  2555. let globalFetchFun = [];
  2556. // tis处理队列
  2557. let waitQueue = [];
  2558. // 缓存数据
  2559. function cacheSearchData(newSearchData) {
  2560. if(newSearchData == null) return;
  2561. console.logout("触发了缓存,当前数据",registry.searchData.data)
  2562. // 数据加载后缓存
  2563. cache.set( registry.searchData.SEARCH_DATA_KEY,{
  2564. data: newSearchData,
  2565. expire: new Date().getTime() + registry.searchData.effectiveDuration
  2566. })
  2567. }
  2568. // 更新历史数据
  2569. function compareAndPushDiffToHistory(items = [],isCompared = false) {
  2570. // 更新“旧全局数据”:searchData 追加-> oldSearchData
  2571. let oldSearchData = cache.get(registry.searchData.OLD_SEARCH_DATA_KEY)??[];
  2572. let newItemList = items;
  2573. if(! isCompared && oldSearchData.length != 0) {
  2574. // 比较后,差异项加入(取并集)
  2575. newItemList = compareArrayDiff(items,oldSearchData,registry.searchData.idFun,1) ;
  2576. }
  2577. oldSearchData.push(... newItemList)
  2578. console.log("旧数据缓存",oldSearchData)
  2579. cache.set(registry.searchData.OLD_SEARCH_DATA_KEY,oldSearchData);
  2580. if(! Array.isArray(newItemList)) newItemList = [];
  2581. return newItemList;
  2582. }
  2583. // 防抖函数->处理新数据
  2584. let blocks = [];
  2585. let processingBlock = [];
  2586. let triggerDataChageActuator = syncActuator();
  2587. let refreshNewData = debounce(()=>{
  2588. if(blocks.length == 0) return;
  2589. // 倒动作
  2590. processingBlock = blocks;
  2591. blocks = [];
  2592. // 将经过处理链得到的数据放到全局注册表中
  2593. let globalSearchData = registry.searchData.getData();
  2594. triggerDataChageActuator(()=>{
  2595. globalSearchData.push(... registry.searchData.USDRC.trigger(processingBlock))
  2596. // 数据版本改变
  2597. registry.searchData.version++;
  2598. // 更新视图显示条数
  2599. registry.searchData.searchPlaceholder("UPDATE")
  2600. // 触发搜索数据改变事件(做缓存等操作,观察者模式)
  2601. for(let fun of registry.searchData.dataChangeEventListener) fun(globalSearchData);
  2602. // 重新搜索
  2603. registry.searchData.triggerSearchHandle();
  2604. })
  2605. }, 200) // 积累时间
  2606. const triggerRefreshNewData = (block)=>{
  2607. // 块积累
  2608. blocks.push(...block);
  2609.  
  2610. // 开始去处理
  2611. refreshNewData();
  2612. }
  2613. // 转义与恢复,数据进行解析前进行转义,解析后恢复——比如文本中出现“/”,就会出现:SyntaxError: Octal escape sequences are not allowed in template strings.
  2614. function CallBeforeParse() {
  2615. this.obj = {
  2616. "`":"<反引号>",
  2617. "\\":"<转义>",
  2618. "$": "<美元符>"
  2619. }
  2620. this.escape = function(text) {
  2621. let obj = this.obj;
  2622. for (var key in obj) {
  2623. text = text.toReplaceAll(key,obj[key]);
  2624. }
  2625. return text;
  2626. }
  2627. this.recovery = function(text) {
  2628. let obj = this.obj;
  2629. for (var key in obj) {
  2630. text = text.toReplaceAll(obj[key],key);
  2631. }
  2632. return text;
  2633. }
  2634. }
  2635. let callBeforeParse = new CallBeforeParse();
  2636.  
  2637. // recovery作用:将之前修改为 <wrapLine> 改为真正的换行符 \n
  2638. function contentRecovery(item) {
  2639. item.title = callBeforeParse.recovery(item.title);
  2640. item.desc = callBeforeParse.recovery(item.desc);
  2641. item.resource = callBeforeParse.recovery(item.resource);
  2642. if(item.vassal != null ) item.vassal = callBeforeParse.recovery(item.vassal);
  2643. }
  2644. // 如果tisMetaInfo中有"default-tag"属性表示标签有这个属性,属性处理器在此
  2645. function defaultTagHandle(item,tisMetaInfo = {}) {
  2646. const defaultTag = tisMetaInfo['default-tag'];
  2647. if(!defaultTag) return;
  2648. // 假设defaultTag是 h'游戏' 那下面processedDefaultTag是 [h'游戏']
  2649. const processedDefaultTag = `[${defaultTag}]`
  2650. // defaultTagContent就是 游戏
  2651. const defaultTagContent = parseTag(processedDefaultTag)[0][3];
  2652. // 这里看item.title是否已经有 '游戏'] 或 [游戏] 如果都没有才加,也就是子数据项如果手动加就,default-tag就不会生效
  2653. if( !parseTag(item.title).some(captureMeta => captureMeta[3] === defaultTagContent) ) {
  2654. item.title = processedDefaultTag + item.title;
  2655. }
  2656. }
  2657. // baseUrl + relativePath(文件 ./文件 ../文件)= relativePath的绝对路径
  2658. function resolveUrl(baseUrl, relativePath) {
  2659. // 创建一个链接对象,方便解析路径
  2660. const base = new URL(baseUrl);
  2661. // 处理相对路径
  2662. const resolvedUrl = new URL(relativePath, base);
  2663. return resolvedUrl.href;
  2664. }
  2665. function dataSourceHandle(resourcePageUrl,tisMetaInfo = {}, parentResourcePageUrl) { //resourcePageUrl 可以是url也可以是已经url解析出来的资源
  2666. const tisTabFetchFunName = tisMetaInfo && tisMetaInfo.fetchFun;
  2667. if(! registry.searchData.isDataInitialized) {
  2668. registry.searchData.isDataInitialized = true;
  2669. registry.searchData.processHistory = []; // 清空处理历史
  2670. registry.searchData.clearData(); // 清理旧数据
  2671. }
  2672. let processHistory = registry.searchData.processHistory; // 处理过哪些链接需要记住,避免重复
  2673. if(processHistory.includes(resourcePageUrl)) return; // 判断
  2674. processHistory.push(resourcePageUrl); // 记录
  2675. // 如果不根,且不是resourcePageUrl不是httpUrl,需要将resourcePageUrl(相对路径)根据parentResourcePageUrl(绝对路径)转为http url
  2676. if( ! tisMetaInfo.root && !isHttpUrl(resourcePageUrl) ) {
  2677. // if(parentResourcePageUrl == null) throw new Error(`订阅异常,相对路径: ${resourcePageUrl},没有父绝对路径!`);
  2678. resourcePageUrl = resolveUrl(parentResourcePageUrl,resourcePageUrl);
  2679. }
  2680. urlToText(resourcePageUrl).then(text => {
  2681. if(tisTabFetchFunName == null) {
  2682. // --> 是配置 <--
  2683. let data = []
  2684. // 解析配置
  2685. let config = getConfigFromDataSource(text);
  2686. console.logout("解析的配置:",config)
  2687. // 解析FetchFun:将FetchFun放到全局解析器中
  2688. globalFetchFun.push(...config.fetchFuns);
  2689. // 解析订阅:将tis放到处理队列中
  2690. waitQueue.push(...config.tis);
  2691. let tis = null;
  2692. while((tis = waitQueue.pop()) != undefined) {
  2693. // tis第一个是url,第二是fetchFun
  2694. dataSourceHandle(tis.tabValue,tis, resourcePageUrl);
  2695. }
  2696. }else {
  2697. // --> 是内容 <--
  2698. // 解析内容
  2699. if(tisTabFetchFunName === "") return;
  2700. let fetchFunStr = getFetchFunGetByName(tisTabFetchFunName);
  2701.  
  2702. let searchDataItems = [];
  2703. try {
  2704. searchDataItems =(new Function('text', "return ( " + fetchFunStr + " )(`"+callBeforeParse.escape(text)+"`)"))();
  2705. }catch(e) {
  2706. throw new Error("我的搜索 run log: 由于页面站点限制,导致数据解析失败!",e)
  2707. }
  2708. // 处理并push到全局数据容器中
  2709. for(let item of searchDataItems) {
  2710. // 转义-恢复
  2711. contentRecovery(item);
  2712. // "default-tag"标签属性处理器
  2713. defaultTagHandle(item,tisMetaInfo)
  2714. }
  2715. // 加入到push到全局的搜索数据队列中,等待加入到全局数据容器中
  2716. triggerRefreshNewData(searchDataItems)
  2717. }
  2718. })
  2719.  
  2720.  
  2721. }
  2722. // 根据fetchFun名返回字符串函数
  2723. function getFetchFunGetByName(fetchFunName) {
  2724. for(let fetchFunData of globalFetchFun) {
  2725. if(fetchFunData.name == fetchFunName) {
  2726. return fetchFunData.fetchFun;
  2727. }
  2728. }
  2729. }
  2730. // 检查是否已经执行初始化
  2731. function checkIsInitializedAndSetInitialized(secondTime) {
  2732. let key = "DATA_INIT";
  2733. let value = cache.cookieGet(key);
  2734. if(value != null && value != "") return true;
  2735. cache.cookieSet(key,key,1000*secondTime);
  2736. return false;
  2737. }
  2738. // 【数据初始化主函数】
  2739. // 调用下面函数自动初始化数据,刚进来直接检查更新(如果数据已过期就更新数据)
  2740. function dataInitFun() {
  2741. // 从缓存中获取数据,判断是否还有效
  2742. // cache.remove(SEARCH_DATA_KEY)
  2743. let dataPackage = cache.get(registry.searchData.SEARCH_DATA_KEY);
  2744. if(dataPackage != null && dataPackage.data != null) {
  2745. // 缓存信息不为空,深入判断是否使用缓存的数据
  2746. let dataExpireTime = dataPackage.expire;
  2747. let currentTime = new Date().getTime();
  2748. // 判断是否有效,有效的话放到全局容器中
  2749. let isNotExpire = (dataExpireTime != null && dataExpireTime > currentTime && dataPackage.data != null && dataPackage.data.length > 0);
  2750. // 如果网站比较特殊,忽略数据过期时间
  2751. if( window.location.host.includes("github.com") ) isNotExpire = true;
  2752.  
  2753. if(isNotExpire) {
  2754. // 当视图已经初始化时-从缓存中将挂载数据挂载 (条件是视图已经初始化)
  2755. console.logout(`视图${registry.view.initialized?'已加载':'未加载'}:数据有效期还有${parseInt((dataExpireTime - currentTime)/1000/60)} 分钟!`,dataPackage.data);
  2756. if( registry.view.initialized ) registry.searchData.setData(dataPackage.data);
  2757. // 如果数据状态未过期(有效)不会去请求数据
  2758. return;
  2759. }
  2760.  
  2761. }
  2762. // 在去网络请求获取数据前-检查是否已经执行初始化-防止多页面同时加载导致的数据重复加载
  2763. if(! askIsExpiredByTopic("SEARCH_DATA_INIT",6*1000)) return;
  2764. // 清理掉当前缓存数据
  2765. cache.remove(registry.searchData.SEARCH_DATA_KEY);
  2766. registry.searchData.clearData();
  2767. // 重置数据初始化状态
  2768. registry.searchData.isDataInitialized = false;
  2769. // 持续执行
  2770. registry.searchData.searchPlaceholder("UPDATE","🔁 数据准备更新中...",5000)
  2771. // 内部将使用递归,解析出信息
  2772. getDataSources().then(dataSources=>{dataSourceHandle(dataSources,{ root: true})})
  2773. }
  2774. // 检查数据有效性,且只有数据无效时挂载到数据
  2775. dataInitFun();
  2776. // 当视图第一次显示时,再执行
  2777. registry.view.viewFirstShowEventListener.push(dataInitFun);
  2778.  
  2779. // 解析标签函数-core函数
  2780. function parseTag(title) {
  2781. return captureRegEx(/\[\s*(([^'\]\s]*)\s*')?\s*([^'\]]*)\s*'?\s*]/gm,title);
  2782. }
  2783. // 解析出传入的所有项标签数据
  2784. function parseTags(data = [],selecterFun = (_item)=>_item,tagsMap = {}) {
  2785. let isArray = Array.isArray(data);
  2786. let items = isArray?data:[data];
  2787. // 解析 item.name中包含的标签
  2788. items.forEach(function(item) {
  2789. let captureGroups = parseTag(selecterFun(item));
  2790. captureGroups.forEach(function(group) {
  2791. let params = group[2]??"";
  2792. let label = group[3];
  2793. // 判断是否已经存在
  2794. if(label != null && tagsMap[label] == null ) {
  2795. let currentHandleTagObj = tagsMap[label] = {
  2796. name: label,
  2797. status: 1, // 正常
  2798. //visible: params.includes("h"), // 参数中包含h字符表示可见
  2799. count: 1
  2800. //params: params
  2801. //items: [item]
  2802. }
  2803. // 如果传入的不是一个数组,那设置下面参数才有意义
  2804. if(! isArray) {
  2805. currentHandleTagObj.params = params;
  2806. }
  2807. }else {
  2808. if(tagsMap[label] != null) {
  2809. tagsMap[label].count++;
  2810. //tagsMap[label].items.push(item);
  2811. }
  2812.  
  2813. }
  2814. })
  2815. });
  2816. // 这里不能是不是数组(上面的isArray)都返回tag数组,因为一项也可能有多个标签
  2817. return Object.values(tagsMap);
  2818. }
  2819.  
  2820. let tagsMap = {}
  2821. const parseSearchItem = function (searchData){
  2822. console.log("==1:解析出数据标签==")
  2823. // 将现有的所有标签提取出来
  2824. // 解析
  2825. let dataItemTags = parseTags(searchData,(_item=>_item.title),tagsMap);
  2826. // 缓存
  2827. if(dataItemTags.length > 0) {
  2828. cache.set(registry.searchData.DATA_ITEM_TAGS_CACHE_KEY,dataItemTags)
  2829. }
  2830. return searchData;
  2831. }
  2832. // ################# 执行顺序从大到小 1000 -> 500
  2833. registry.searchData.USDRC.add({weight:600 ,fun:parseSearchItem});
  2834. // 解析script项的text
  2835. function scriptTextParser(text) {
  2836. if (text == null) return null;
  2837. let scriptLines = text.split("\n");
  2838. if (scriptLines != null && scriptLines.length != 0) {
  2839. // 可以解析
  2840. let result = {};
  2841. let key = null;
  2842. let value = null;
  2843. for (let i = 0; i < scriptLines.length; i++) {
  2844. let line = scriptLines[i];
  2845. // 判断是否为新的变量开始
  2846. let captureArr = captureRegEx(/^--\s*([^-\s]*)\s*--\s*$/gm, line);
  2847. let isStartNewVar = captureArr != null && captureArr[0] != null && captureArr[0].length >= 2;
  2848. let isLastLine = (i + 1 == scriptLines.length);
  2849. if(isStartNewVar) {
  2850. // 保存前面的
  2851. if (key != null) result[key] = value.trim();
  2852. // 开始新的
  2853. key = captureArr[0][1];
  2854. value = ""; // 重置value
  2855. }else {
  2856. value += ("\n" + line);
  2857. }
  2858.  
  2859. if ( isLastLine) {
  2860. // 保存前面的
  2861. if (key != null) result[key] = value.trim();
  2862. return result;
  2863. }
  2864. }
  2865. return result;
  2866. }
  2867.  
  2868. return null;
  2869. }
  2870. // 将形如“aa bb” 转为 {aa:"bb"} ,并且如果是布尔类型或数值字符串转为对应的类型
  2871. function extractVariables(varsString) {
  2872. const lines = varsString.split('\n');
  2873. const result = {};
  2874.  
  2875. for (const line of lines) {
  2876. const parts = line.trim().split(/\s+/);
  2877. if (parts.length === 2) {
  2878. const key = parts[0].trim();
  2879. const value = parts[1].trim();
  2880.  
  2881. // 检查是否为 true/false 字符串
  2882. if (value === 'true' || value === 'false') {
  2883. result[key] = value === 'true'; // 转换为布尔值
  2884. } else if (!isNaN(value)) { // 检查是否为数值字符串
  2885. result[key] = parseFloat(value); // 转换为数值类型
  2886. } else {
  2887. result[key] = value; // 保持原始字符串
  2888. }
  2889. }
  2890. }
  2891.  
  2892. return result;
  2893. }
  2894. const parseScriptItem = function (searchData){
  2895. console.log("==1:简述项解析出脚本项==")
  2896. for(let item of searchData) {
  2897. if((item == null || item.title == null) || item.type != "sketch" ) continue;
  2898. if( /\[\s*(.*')?\s*(脚本|script)\s*'?\s*\]/.test( item.title ) ) {
  2899. // 是脚本项
  2900. item.type = "script";
  2901. // 将resource解析为对象
  2902. item.resourceObj = scriptTextParser(item.resource);
  2903. item.resource = "--脚本项resource已解析到resourceObj--"
  2904. // 解析脚本中的env(环境变量)
  2905. if(item.resourceObj.env != null) {
  2906. item.resourceObj.env = extractVariables(item.resourceObj.env);
  2907. // 将提取的icon变量放到数据项根上,这样显示时,可读取作为icon
  2908. let customIcon = item.resourceObj.env._icon;
  2909. if( customIcon != null) item.icon = customIcon;
  2910. let vassal = item.resourceObj.vassal;
  2911. if(vassal != null) item.vassal = vassal;
  2912. }
  2913. }
  2914. }
  2915. return searchData;
  2916. }
  2917. // ################# 执行顺序从大到小 1000 -> 500
  2918. registry.searchData.USDRC.add({weight:599 ,fun:parseScriptItem});
  2919. // 监听缓存被清理,当被清理时,置空之前收集的标签数据
  2920. registry.searchData.dataCacheRemoveEventListener.push(()=>{tagsMap = {}})
  2921.  
  2922. const refreshTags = function (searchData){
  2923. // 在添加前,进行额外处理添加,如给有”{keyword}“的url搜索项添加”可搜索“标签
  2924. for(let searchItem of searchData) {
  2925. let resource = searchItem.resource;
  2926. let isHttpUrl =/^[^\n]*\.[^\n]*$/.test(`${resource}`.trim());
  2927. let isSearchable = /\[\[[^\[\]]+keyword[^\[\]]+\]\]/.test(resource);
  2928. // 判断是否为可搜索
  2929. if( resource == null || !isHttpUrl || !isSearchable ) continue;
  2930. if(! searchItem.title.includes(registry.searchData.searchProTag)) searchItem.title = registry.searchData.searchProTag+searchItem.title;
  2931. }
  2932. return searchData;
  2933. }
  2934.  
  2935. // ################# 执行顺序从大到小 1000 -> 500
  2936. registry.searchData.USDRC.add({weight:500 ,fun:refreshTags});
  2937. // 清理标签(参数中有h的)
  2938. function clearHideTag(data,get = (item)=>item.title,set = (item,cleaned)=>{item.title=cleaned}) {
  2939. let isArray = Array.isArray(data);
  2940. let items = isArray?data:[data];
  2941. for(let item of items) {
  2942. let target = get(item);
  2943. const regex = /\[\s*[^:\]]*h[^:\]]*\s*'\s*[^'\]]*\s*'\s*]/gm;
  2944. let cleanedTarget = target.replace(regex, '');
  2945. set(item,cleanedTarget);
  2946. }
  2947. return isArray?items:items[0];
  2948. }
  2949. // 给title清理掉“h”标签
  2950. function clearHideTagForTitle(rawTitle) {
  2951. const regex = /\[\s*[^:\]]*h[^:\]]*\s*'\s*[^'\]]*\s*'\s*]/gm;
  2952. return rawTitle.replace(regex, '');
  2953. }
  2954. // 解析出标题中的所有标签-返回string数组
  2955. function extractTagsAndCleanContent(inputString = "") {
  2956. // 使用正则表达式匹配所有方括号包围的内容
  2957. const regex = /\[.*?\]/g;
  2958. const tags = inputString.match(regex) || [];
  2959. // 清理掉标签的内容
  2960. const cleanedContent = inputString.replace(regex, '').trim();
  2961. return {
  2962. tags: tags,
  2963. cleaned: cleanedContent
  2964. };
  2965. }
  2966. const filterSearchData = function (searchData) {
  2967. const filterDataByUserUnfollowList = (itemsData,userUnfollowList = []) => {
  2968. var userUnfollowMap = userUnfollowList.reduce(function(result, item) {
  2969. result[item] = '';
  2970. return result;
  2971. }, {});
  2972. // 开始过滤
  2973. return itemsData.filter(item=>{
  2974. let tags = parseTags(item.title);
  2975. for(let tag of tags){
  2976. if(userUnfollowMap[tag.name] != null){
  2977. // 被过滤
  2978. return false;
  2979. }
  2980. }
  2981. return true;
  2982. })
  2983. }
  2984. console.log("==去除用户不关注的数据项==")
  2985. // 用户维护的取消关注标签列表
  2986. let userUnfollowList = cache.get(registry.searchData.USER_UNFOLLOW_LIST_CACHE_KEY)?? registry.searchData.USER_DEFAULT_UNFOLLOW;
  2987. // 利用用户维护的取消关注标签列表 过滤 搜索数据
  2988. let filteredSearchData = filterDataByUserUnfollowList(searchData,userUnfollowList);
  2989. // 去标签(参数h),清理每个item中title属性的tag , 下面注释掉是因为清理后置了仅在显示时不显示
  2990. // let clearedSearchData = clearHideTag(filteredSearchData);
  2991. return filteredSearchData;
  2992. }
  2993. // ############### 执行顺序从大到小 1000 -> 500
  2994. registry.searchData.USDRC.add({weight:400 ,fun:filterSearchData});
  2995. let isHasLaftData = true;
  2996. const compareBlocks = function (searchData = []) {
  2997. let oldSearchData = cache.get(registry.searchData.OLD_SEARCH_DATA_KEY)??[];
  2998. if(isHasLaftData) isHasLaftData = oldSearchData != null && oldSearchData.length > 0;
  2999. console.log("块数据与旧数据对比中>>")
  3000. // 新数据加载完成-进行数据对比
  3001. // 旧数据,也就是上一次数据,用于与本次比较,得出新添加数据
  3002. // 当前时间戳
  3003. let currentTime = new Date().getTime();
  3004. // 准备一个存储新数据项的容器
  3005. let newDataItems = compareAndPushDiffToHistory(searchData);
  3006. // 给新添加的过期时间(新数据有效期)
  3007. newDataItems.forEach(item=> {
  3008. // 添加过期时间
  3009. item.expires = (currentTime++) + ( 1000*60*60*24*registry.searchData.NEW_DATA_EXPIRE_DAY_NUM )
  3010. });
  3011. console.log("数据对比-新差异项:",[...newDataItems]);
  3012. // 过滤掉新数据中带有“带注释”的项
  3013. newDataItems = newDataItems.filter(item=> !item.title.startsWith("#"));
  3014. // 以前的新增数据
  3015. let oldNewItems = cache.get(registry.searchData.SEARCH_NEW_ITEMS_KEY);
  3016. // 如果第一次加载数据,那不要这次的最新数据
  3017. if(oldNewItems == null) {
  3018. cache.set(registry.searchData.SEARCH_NEW_ITEMS_KEY,[]);
  3019. return searchData;
  3020. }
  3021. // 如果还没有过期的,保留下来放在最新数据中
  3022. for(let item of oldNewItems) {
  3023. if(item != null && item.expires > currentTime) newDataItems.push(item);
  3024. }
  3025. console.log("数据对比-总新数据:",[...newDataItems])
  3026. // 总新增去重 (标记 - 过滤标记的 )
  3027. newDataItems = removeDuplicates(newDataItems,(item)=>item.title+item.desc);
  3028. // 当新数据项大于registry.searchData.showSize时,进行截取
  3029. if(! isHasLaftData) {
  3030. // 如何是第一次安装,那不应该有新数据
  3031. newDataItems = [];
  3032. }else if( newDataItems.length > registry.searchData.showSize ) {
  3033. // 如果新增超过指定数量 ,进行截取头部最新
  3034. // 先根据expires属性降序排序
  3035. newDataItems.sort((a, b) => b.expires - a.expires);
  3036. // 然后截取前15条记录
  3037. newDataItems = newDataItems.slice(0, registry.searchData.showSize );
  3038. }
  3039. // 重新缓存“New Data”
  3040. cache.set(registry.searchData.SEARCH_NEW_ITEMS_KEY,newDataItems);
  3041. // 为全局数据(注册表中)的新数据添加新数据标签
  3042. for(let nItem of newDataItems) {
  3043. for(let cItem of searchData) {
  3044. if(nItem.title === cItem.title && nItem.desc === cItem.desc) {
  3045. // 修改全局搜索数据中New Data数据添加“新数据”标签
  3046. if (! cItem.title.startsWith(registry.searchData.NEW_ITEMS_TAG)) {
  3047. cItem.title = registry.searchData.NEW_ITEMS_TAG+cItem.title;
  3048. }
  3049. break;
  3050. }
  3051. }
  3052. }
  3053. return searchData;
  3054. }
  3055. // ############ 使用用户操作的规则对加载出来的数据过滤:(责任链中的一块)
  3056. registry.searchData.USDRC.add({weight:300 ,fun:compareBlocks});
  3057.  
  3058. // 索引处理与缓存
  3059. const refreshIndex = function (globalSearchData) {
  3060. if(globalSearchData == null || globalSearchData.length == 0 ) return;
  3061. console.log("===刷新索引===")
  3062. // 当前最新数据,用于搜索
  3063. let newDataItems = cache.get(registry.searchData.SEARCH_NEW_ITEMS_KEY);
  3064. // 去重
  3065. globalSearchData = removeDuplicates(globalSearchData,(item)=>item.title+item.desc)
  3066. // 将 index 给 newDataItems ,不然new中的我们选择与实际选择的不一致问题 !
  3067. // 给全局数据创建索引
  3068. globalSearchData.forEach((item,index)=>{item.index=index});
  3069. // 给NEW建索引
  3070. newDataItems.forEach(NItem=>{
  3071. for(let CItem of globalSearchData) {
  3072. if( CItem.title.includes(NItem.title) && NItem.desc === CItem.desc) {
  3073. NItem.index = CItem.index;
  3074. break;
  3075. }
  3076. }
  3077. })
  3078. // 重新缓存“New Data”
  3079. cache.set(registry.searchData.SEARCH_NEW_ITEMS_KEY,newDataItems);
  3080. // 重新缓存全局数据
  3081. cacheSearchData(globalSearchData);
  3082. }
  3083. // 加入到数据改变后事件处理
  3084. registry.searchData.dataChangeEventListener.push(refreshIndex);
  3085.  
  3086. // 模块二
  3087. registry.view.viewVisibilityController = (function() {
  3088. // 整个视图对象
  3089. let viewDocument = null;
  3090. let initView = function () {
  3091. // 初始化视图
  3092. let view = document.createElement("div")
  3093. view.id = "my_search_box";
  3094. let menu_icon = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyB0PSIxNjc3MDgxNTk3NzA3IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjEzNDYxIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiPjxwYXRoIGQ9Ik0yMjQuMiA0NzIuM2MtMTMtNS43LTMuNy0yMy41IDguMi0xOSA5MSAzNCAxNDYuOCAxMDguNyAxODIuNCAxMzguNSA1LjYgNC43IDE0IDIuOSAxNy4zLTMuNSAxNi44LTMyIDQ1LjgtMTEzLjctNTcuMS0xNjguNi04Ny4zLTQ2LjUtMTg4LTUzLjYtMjQ3LjMtODIuMi0xNC41LTctMzEuMSA0LjYtMjkuOSAyMC43IDUgNjkuNyAyOC45IDEyNC43IDYyLjMgMTgxLjUgNjcuMyAxMTQuMyAxNDAuNiAxMzIuOSAyMTYuNiAxMDQgMi4yLTAuOSA0LjUtMS44IDctMyA3LTMuNCA4LjMtMTIuOSAyLjUtMTguMSAwLjEgMC00NS43LTY5LjMtMTYyLTE1MC4zeiIgZmlsbD0iI0ZGRDQwMSIgcC1pZD0iMTM0NjIiPjwvcGF0aD48cGF0aCBkPSJNMjgyLjcgODQ5LjljNzkuNS0xMzcgMTcyLjQtMjYzLjEgMzg1LjQtNDAxLjMgOS44LTYuNCAyLjEtMjEuNS04LjktMTcuNEM0OTcuNyA0OTIuOCA0MjkuNyA1ODUgMzczLjMgNjQwLjhjLTguNyA4LjctMjMuNCA2LjMtMjkuMS00LjYtMjcuMi01MS44LTY5LjUtMTc0LjEgOTcuMy0yNjMuMSAxNDcuNy03OC44IDMxOS45LTkxLjQgNDI5LjctOTMuMyAxOC45LTAuMyAzMS41IDE5LjQgMjMuMyAzNi40Qzg2My43IDM4MCA4NDIuNiA0NzggNzg5LjkgNTY3LjYgNjgwLjggNzUzLjEgNTQ1LjUgNzY2LjcgNDIyLjIgNzE5LjhjLTguOC0zLjQtMTguOC0wLjItMjQgNy43LTE2LjYgMjUuMi01MC4zIDgwLjEtNTguNyAxMjIuNC0xMS40IDU2LjgtODIuMiA0My45LTU2LjggMHoiIGZpbGw9IiM4QkMwM0MiIHAtaWQ9IjEzNDYzIj48L3BhdGg+PHBhdGggZD0iTTM3NSA0MTkuNmMtMzAuMSAyOC4yLTQ1LjggNTcuNy01Mi40IDg2LjEgNDAuNiAzMi40IDcwLjIgNjcuNyA5Mi4xIDg1LjkgMS4yIDEgMi41IDEuNiAzLjkgMi4xIDYuNS02LjcgMTMuMy0xMy43IDIwLjQtMjAuNyAxNS4yLTM3LjkgMjUuMy0xMDUuNy02NC0xNTMuNHpNMzE4LjggNTQ4LjJjMS42IDM2LjEgMTQuNyA2Ny42IDI1LjUgODguMSA1LjcgMTAuOSAyMC4zIDEzLjMgMjkuMSA0LjYgNC45LTQuOSAxMC0xMCAxNS4xLTE1LjQtMC42LTEtMS4zLTItMi4yLTIuOCAwLTAuMS0yMC4xLTMwLjUtNjcuNS03NC41eiIgZmlsbD0iIzhCQTAwMCIgcC1pZD0iMTM0NjQiPjwvcGF0aD48L3N2Zz4=";
  3095. const matchResultDocumentId = "matchResult", textViewDocumentId = "text_show",searchInputDocumentId = "my_search_input",matchItemsId = "matchItems",searchBoxId = "searchBox",logoButtonId = "logoButton",msInputFilesId = "ms-input-files";
  3096. view.innerHTML = (`
  3097. <div id="tis"></div>
  3098. <div id="my_search_view">
  3099. <div id="${searchBoxId}" >
  3100. <div id="${msInputFilesId}">
  3101. <!-- <div class="ms-input-file">
  3102. <img src="https://greasyfork.org/vite/assets/blacklogo96-CxYTSM_T.png" />
  3103. </div> -->
  3104. </div>
  3105. <input placeholder="${registry.searchData.searchPlaceholder()}" id="${searchInputDocumentId}" />
  3106. <button id="${logoButtonId}" >
  3107. <img src="${menu_icon}" draggable="false" />
  3108. </button>
  3109. </div>
  3110. <div id="${matchResultDocumentId}">
  3111. <ol id="${matchItemsId}">
  3112. </ol>
  3113. </div>
  3114. <!--加“markdown-body”是使用了github-markdown.css 样式!加在markdown文档父容器中-->
  3115. <div id="${textViewDocumentId}" class="${cssLoad("ms-markdown-body","markdown-css",{isResourceName:true, replacePrefix: 'markdown-body'})} ${cssLoad("ms-code-body","code-css",{isResourceName: true})}" style="min-height:auto !important;">
  3116. </div>
  3117. </div>
  3118. `)
  3119. // 挂载到文档中
  3120. document.body.appendChild(view)
  3121. // 整个视图对象放在组件全局中/注册表中
  3122. viewDocument = registry.view.viewDocument = view;
  3123. // 想要追加请看下面registry.view.element是否已经包含,没有在那下面追加即可~
  3124.  
  3125. // 搜索框对象
  3126. let searchInputDocument = $(document.getElementById(searchInputDocumentId)),
  3127. matchItems = $(document.getElementById(matchItemsId)),
  3128. searchBox = $(document.getElementById(searchBoxId)),
  3129. logoButton = $(document.getElementById(logoButtonId)),
  3130. textView = $(document.getElementById(textViewDocumentId)),
  3131. matchResult = $(document.getElementById(matchResultDocumentId)),
  3132. msInputFiles = $(document.getElementById(msInputFilesId));
  3133. // 将视图对象放到注册表中
  3134. registry.view.element = {
  3135. input: searchInputDocument,
  3136. logoButton,
  3137. matchItems,
  3138. searchBox,
  3139. matchResult,
  3140. textView,
  3141. files: msInputFiles
  3142. }
  3143. // 开启files 粘贴事件监听
  3144. registry.searchData.searchForFile.start();
  3145. // 菜单函数(点击输入框右边按钮时会调用)
  3146. function onClickLogo() {
  3147. // alert("小彩蛋:可以搜索一下“系统项”了解脚本基本使用哦~");
  3148. // 调用手动触发搜索函数,如果已经搜索过,搜索空串(清理)
  3149. let keyword = "[系统项]";
  3150. registry.searchData.triggerSearchHandle(searchInputDocument.val()==keyword?'':keyword);
  3151. // 重新聚焦搜索框
  3152. registry.view.element.input.focus()
  3153. }
  3154. const isLogoButtonPressedRef = registry.view.logo.isLogoButtonPressedRef;
  3155. // 按下按钮时设置变量为 true
  3156. logoButton.on('mousedown', function() {
  3157. isLogoButtonPressedRef.value = true;
  3158. });
  3159.  
  3160. // 按钮弹起时设置变量为 false,并让输入框聚焦
  3161. logoButton.on('mouseup', function() {
  3162. isLogoButtonPressedRef.value = false;
  3163. onClickLogo() // 触发logo点击事件
  3164. searchInputDocument.focus(); // 输入框聚焦
  3165. });
  3166.  
  3167. // 防止鼠标拖出按钮后弹起无法触发 mouseup
  3168. logoButton.on('mouseleave', function() {
  3169. if (isLogoButtonPressedRef.value) {
  3170. isLogoButtonPressedRef.value = false;
  3171. searchInputDocument.focus(); // 输入框聚焦
  3172. }
  3173. });
  3174.  
  3175. // 设置视图已经初始化
  3176. registry.view.initialized = true;
  3177.  
  3178. // 在搜索的结果集中上下选择移动然后回车(相当点击)
  3179. searchInputDocument.keyup(function(event){
  3180. let keyword = $(event.target).val().trim();
  3181. // 当不为空时,放到全局keyword中
  3182. if(keyword) {
  3183. registry.searchData.keyword = event.target.value;
  3184. }
  3185. // 处理keyword中的":"字符
  3186. if(keyword.endsWith("::") || keyword.endsWith("::")) {
  3187. keyword = keyword.replace(/::|::/,registry.searchData.subSearch.searchBoundary).replace(/\s+/," ");
  3188. // 每次要形成一个" : "的时候去掉重复的" : : " -> " : "
  3189. keyword = keyword.replace(/((\s{1,2}:)+ )/,registry.searchData.subSearch.searchBoundary);
  3190. $(event.target).val(keyword.toUpperCase());
  3191. }
  3192. });
  3193. // searchInputDocument.keydown:这个监听用来处理其它键(非上下选择)的。
  3194. searchInputDocument.keydown(function (event){
  3195. // 阻止键盘事件冒泡 | 阻止输入框外监听到按钮,应只作用于该输入框
  3196. event.stopPropagation();
  3197. // 判断一个输入框的东西,如果如果按下的是删除,判断一下是不是"搜索模式"
  3198. let keyword = $(event.target).val();
  3199. let input = event.target;
  3200. if(event.key == "Backspace" ) { // 按的是删除键-块删除
  3201. if(keyword.endsWith(registry.searchData.subSearch.searchBoundary)) {
  3202. // 取消默认事件-删除
  3203. event.preventDefault();
  3204. return;
  3205. }else if(/^\s*[\[<][^\[\]<>]*[\]>]\s*$/.test( keyword )) {
  3206. // 如果输入框只有[xxx]或<xxx>那就清空掉输入框
  3207. searchInputDocument.val('')
  3208. // keyword重置为空字符后触发搜索
  3209. registry.searchData.triggerSearchHandle();
  3210. event.preventDefault();
  3211. return;
  3212. }else if(keyword === ""){
  3213. registry.searchData.searchForFile.delete();
  3214. }
  3215. }else if ( ! event.shiftKey && event.keyCode === 9 ) { // Tab键
  3216. if(! registry.searchData.subSearch.isSubSearchMode()) {
  3217. // 转大写
  3218. event.target.value = event.target.value.toUpperCase()
  3219. // 添加搜索pro模式分隔符
  3220. event.target.value += registry.searchData.subSearch.searchBoundary
  3221. // 阻止默认行为,避免跳转到下一个元素
  3222. registry.searchData.triggerSearchHandle();
  3223. }
  3224. event.preventDefault();
  3225. }else if (event.shiftKey && event.keyCode === 9 ) { // 按下shift + tab键时取消搜索模式
  3226. if(registry.searchData.subSearch.isSubSearchMode()) {
  3227. // 在这里编写按下shift+tab键时要执行的代码
  3228. let input = event.target;
  3229. input.value = input.value.split(registry.searchData.subSearch.searchBoundary)[0]
  3230. event.target.value = event.target.value.toLowerCase();
  3231. // 手动触发输入事件
  3232. input.dispatchEvent(new Event("input", { bubbles: true }));
  3233. }
  3234. event.preventDefault();
  3235. }
  3236.  
  3237. })
  3238.  
  3239. // searchInputDocument.keydown:这个监听用来处理上下选择范围的操作
  3240. searchInputDocument.keydown(function (event){
  3241. let e = event || window.event;
  3242.  
  3243. if(e && e.keyCode!=38 && e.keyCode!=40 && e.keyCode!=13) return;
  3244. if(e && e.keyCode==38){ // 上
  3245. registry.searchData.pos --;
  3246.  
  3247. }
  3248. if(e && e.keyCode==40){ //下
  3249. registry.searchData.pos ++;
  3250. }
  3251. // 如果是回车 && registry.searchData.pos == 0 时,设置 registry.searchData.pos = 1 (这样是为了搜索后回车相当于点击第一个)
  3252. if(e && e.keyCode==13 && registry.searchData.pos == 0){ // 回车选择的元素
  3253. // 如果当前是在搜索中就忽略回车这个操作
  3254. if(registry.searchData.searchEven.isSearching) return;
  3255. registry.searchData.pos = 1;
  3256. }
  3257.  
  3258. // 当指针位置越出时,位置重定向
  3259. if(registry.searchData.pos < 1 || registry.searchData.pos > registry.searchData.searchData.length ) {
  3260. if(registry.searchData.pos < 1) {
  3261. // 回到最后一个
  3262. registry.searchData.pos = registry.searchData.searchData.length;
  3263. }else {
  3264. // 回到第一个
  3265. registry.searchData.pos = 1;
  3266. }
  3267. }
  3268. // 设置显示样式
  3269. let activeItem = $(registry.view.element.matchItems.find('li')[registry.searchData.pos-1]);
  3270. // 设置活跃背景颜色
  3271. let activeBackgroundColor = "#dee2e6";
  3272. activeItem.css({
  3273. "background":activeBackgroundColor
  3274. })
  3275.  
  3276. // 设置其它子元素背景为默认统一背景
  3277. activeItem.siblings().css({
  3278. "background":"#fff"
  3279. })
  3280.  
  3281. // 看是不是item detail内容显示中,如果是回车发送send事件,否则才是结果集显示的回车选择
  3282. if(e && e.keyCode==13 && activeItem.find("a").length > 0 && !registry.script.tryRunTextViewHandler()){ // 回车
  3283. // 点击当前活跃的项,点击
  3284. activeItem.find("a")[0].click();
  3285. }
  3286. // 取消冒泡
  3287. e.stopPropagation();
  3288. // 取消默认事件
  3289. e.preventDefault();
  3290.  
  3291. });
  3292. // 将输入框的控制按钮设置可见性函数公开放注册表中
  3293. registry.view.setButtonVisibility = function (buttonVisibility = false) {
  3294. // registry.view.setButtonVisibility
  3295. logoButton.css({
  3296. "display": buttonVisibility?"block":"none"
  3297. })
  3298. }
  3299. // 高权重项特殊搜索关键词直达
  3300. registry.searchData.searchEven.event[registry.searchData.specialKeyword.highFrequency] = function(search,rawKeyword) {
  3301. return DataWeightScorer.highFrequency(45);
  3302. }
  3303. // 历史记录特殊搜索关键词直达
  3304. registry.searchData.searchEven.event[registry.searchData.specialKeyword.history] = function(search,rawKeyword) {
  3305. return SelectHistoryRecorder.history(15);
  3306. }
  3307. // 向搜索事件(只会触发一个)中添加一个“NEW”搜索关键词
  3308. registry.searchData.searchEven.event["new|"+registry.searchData.specialKeyword.new] = function(search,rawKeyword) {
  3309. let showNewData = null;
  3310. let activeSearchData = registry.searchData.getData();
  3311. // 如果当前注册表中全局搜索数据为空,使用缓存的数据
  3312. if(activeSearchData == null ) {
  3313. let cacheAllSearchData = cache.get(registry.searchData.SEARCH_DATA_KEY);
  3314. if(cacheAllSearchData != null && cacheAllSearchData.data != null) activeSearchData = cacheAllSearchData.data;
  3315. }
  3316. // 如果最新数据都没有,使用旧数据(上一次)
  3317. if(activeSearchData == null ) {
  3318. let oldCacheAllSearchData = cache.get(registry.searchData.OLD_SEARCH_DATA_KEY);
  3319. if(oldCacheAllSearchData != null) activeSearchData = oldCacheAllSearchData;
  3320. }
  3321. // 只展示 newItems 数据中data也存在的项
  3322. let newItems = cache.get(registry.searchData.SEARCH_NEW_ITEMS_KEY)??[];
  3323. if(newItems.length > 0 && activeSearchData.length > 0) {
  3324. // 返回的showNewData是左边的(activeSearchData),而不是右边的(newItems),但newItems多出来 的属性也会合并到activeSearchData的item
  3325. showNewData = compareArrayDiff(activeSearchData,newItems,registry.searchData.idFun,0)
  3326. }
  3327. if(showNewData == null) return [];
  3328. // 对数据进行排序
  3329. showNewData.sort(function(item1, item2){return item2.expires - item1.expires});
  3330.  
  3331. showNewData.map((item,index)=>{
  3332. let dayNumber = registry.searchData.NEW_DATA_EXPIRE_DAY_NUM;
  3333. // 去掉[新] 再都加[新],使得就算没有也在显示时也是有新标签的
  3334. item.title = registry.searchData.NEW_ITEMS_TAG+item.title.toReplaceAll(registry.searchData.NEW_ITEMS_TAG,"")
  3335. // 添加“几天前”
  3336. item.title = item.title + " | " + Math.floor( (Date.now() - (item.expires - 1000*60*60*24*dayNumber) )/(1000*60*60*24) )+"天前"; //toDateString
  3337. return item;
  3338. })
  3339. // 将最新的一条由“新”改为“最新一条”
  3340. showNewData[0].title = showNewData[0].title.toReplaceAll(registry.searchData.NEW_ITEMS_TAG,"[最新一条]")
  3341. return showNewData;
  3342. }
  3343. // 可填充搜索模式优先路由(key是正则字符串,value为字符串类型是转发,如果是函数,是自定义搜索逻辑)
  3344. const searchableSpecialRouting = {
  3345. "^\\s*$": "问AI",
  3346. "^问AI$": async (search,rawKeyword,keywordForFill0)=>{
  3347. return await search(keywordForFill0,{isAccurateSearch : true});
  3348. }
  3349. }
  3350. // 返回undfind表示没有定义匹配对应的SpecialRouting,执行通用路由 | null表示跳过 | 返回数组表示SpecialRouting执行搜索得到的结果
  3351. const searchableSpecialRoutingHandler = async function(search,rawKeyword){
  3352. const keywordForFill0 = registry.searchData.subSearch.getParentKeyword(rawKeyword);
  3353. for(let key of Object.keys(searchableSpecialRouting)) {
  3354. if(isMatch(key,keywordForFill0)) {
  3355. const value = searchableSpecialRouting[key];
  3356. if(typeof value === "string") {
  3357. registry.searchData.triggerSearchHandle(value+registry.searchData.subSearch.searchBoundary)
  3358. return [];
  3359. }
  3360. if(typeof value === "function") return await value(search,rawKeyword,keywordForFill0);
  3361. }
  3362. }
  3363. // 表示没有匹配到SpecialRouting
  3364. return undefined;
  3365. }
  3366. registry.searchData.searchEven.event[".*"+registry.searchData.subSearch.searchBoundary+".*"] = async function(search,rawKeyword) {
  3367. const specialRoutinResult = await searchableSpecialRoutingHandler(search,rawKeyword)
  3368. // 当没有优先Result, 只搜索“可搜索”项
  3369. return Array.isArray(specialRoutinResult)
  3370. ? specialRoutinResult
  3371. : await search(`${registry.searchData.searchProTag} ${registry.searchData.subSearch.getParentKeyword()}`);
  3372. }
  3373. // 搜索AOP
  3374. async function searchAOP(search,rawKeyword) {
  3375. // 转发到对应的AOP处理器中(keyword规则订阅者)
  3376. let data = registry.searchData.getData();
  3377. console.log("搜索data:",data)
  3378. return await registry.searchData.searchEven.send(search,rawKeyword);
  3379. }
  3380. function searchUnitHandler(beforeData = [],keyword = "") {
  3381. // 触发搜索事件
  3382. for(let e of registry.searchData.onSearch) e(keyword);
  3383. // 如果没有搜索内容,返回空数据
  3384. keyword = keyword.trim().toUpperCase();
  3385. if(keyword == "" || registry.searchData.getData().length == 0 ) return [];
  3386. // 切割搜索内容以空格隔开,得到多个 keyword
  3387. let searchUnits = keyword.split(/\s+/);
  3388. // 弹出一个 keyword
  3389. keyword = searchUnits.pop();
  3390. // 本次搜索的总数据容器
  3391. let searchResultData = [];
  3392. let searchLevelData = [
  3393. [],[],[] // 分别是匹配标题/desc/url 的结果
  3394. ]
  3395. // 数据出来的总数据
  3396. //let searchData = []
  3397. // 前置处理函数,这里使用观察者模式
  3398. // searchPreFun(keyword);
  3399. // 搜索操作
  3400. // 为实现当关键词只有一位时,不使用转拼音搜索,后面搜索涉及到的转拼音操作要使用它,而不是直接调用toPinyin
  3401. function getPinyinByKeyword(str,isOnlyFomCacheFind=false) {
  3402. if(registry.searchData.keyword.length > 1 ) return str.toPinyin(isOnlyFomCacheFind)??"";
  3403. return str??"";
  3404. }
  3405. let pinyinKeyword = getPinyinByKeyword(keyword);
  3406. let searchBegin = Date.now()
  3407. for (let dataItem of beforeData) {
  3408. /* 取消注释会导致虽然是15条,但有些匹配度高的依然不能匹配
  3409. // 如果已达到搜索要显示的条数,则不再搜索 && 已经是本次最后一次过滤了 => 就不要扫描全部数据了,只搜出15条即可
  3410. let currentMeetConditionItemSize = searchLevelData[0].length + searchLevelData[1].length + searchLevelData[2].length;
  3411. if(currentMeetConditionItemSize >= registry.searchData.showSize && searchUnits.length == 0 && registry.searchData.subSearch.isSubSearchMode() ) break;
  3412. */
  3413. // 将数据放在指定搜索层级数据上DeepSeek
  3414.  
  3415. if(dataItem.title.includes("DeepSeek")) debugger;
  3416. if (
  3417. (( getPinyinByKeyword(dataItem.title,true).includes(pinyinKeyword) || dataItem.title.toUpperCase().includes(keyword) ) && searchLevelData[0].push(dataItem) )
  3418. || (( getPinyinByKeyword(dataItem.desc,true).includes(pinyinKeyword) || dataItem.desc.toUpperCase().includes(keyword)) && searchLevelData[1].push(dataItem) )
  3419. || ( `${registry.searchData.links.stringifyForSearch(dataItem.links)}${dataItem.resource}${dataItem.vassal}`.substring(0, 4096).toUpperCase().includes(keyword) && searchLevelData[2].push(dataItem) )
  3420. ) {} // 若满足条件的数据对象则会添加到对应的盒子中
  3421. }
  3422. let searchEnd = Date.now();
  3423. console.logout("常规搜索主逻辑耗时:"+(searchEnd - searchBegin ) +"ms");
  3424. // 将上面层级数据进行权重排序然后放在总容器中
  3425. searchResultData.push(...DataWeightScorer.sort(searchLevelData[0],registry.searchData.idFun));
  3426. searchResultData.push(...DataWeightScorer.sort(searchLevelData[1],registry.searchData.idFun));
  3427. searchResultData.push(...DataWeightScorer.sort(searchLevelData[2],registry.searchData.idFun));
  3428.  
  3429. if(searchUnits.length > 0 && searchUnits[searchUnits.length-1].trim() != registry.searchData.subSearch.searchBoundary.trim()) {
  3430. // 递归搜索
  3431. searchResultData = searchUnitHandler(searchResultData,searchUnits.join(" "));
  3432. }
  3433. return searchResultData;
  3434. }
  3435. // ==标题tag处理==
  3436. // 1、标题tag颜色选择器
  3437. function titleTagColorMatchHandler(tagValue) {
  3438. let vcObj = {
  3439. "系统项":"background:rgb(0,210,13);",
  3440. "非最佳":"background:#fbbc05;",
  3441. "推荐":"background:#ea4335;",
  3442. "装机必备":"background:#9933E5;",
  3443. "好物":"background:rgb(247,61,3);",
  3444. "安卓应用":"background:#73bb56;",
  3445. "Adults only": "background:rgb(244,201,13);",
  3446. "可搜索":"background:#4c89fb;border-radius:0px !important;",
  3447. "新":"background:#f70000;",
  3448. "最新一条":"background:#f70000;",
  3449. "精选好课":"background:#221109;color:#fccd64 !important;"
  3450. };
  3451. let resultTagColor = "background:#5eb95e;";
  3452. Object.getOwnPropertyNames(vcObj).forEach(function(key){
  3453. if(key == tagValue) {
  3454. resultTagColor = vcObj[key];
  3455. }
  3456. });
  3457. return resultTagColor;
  3458. }
  3459. // 2、标题内容处理程序
  3460. function titleTagHandler(title) {
  3461. if(!(/[\[]?/.test(title) && /[\]]?/.test(title))) return -1;
  3462. // 格式是:[tag]title 这种的
  3463. const regex = /(\[[^\[\]]*\])/gm;
  3464. let m;
  3465. let resultTitle = title;
  3466. while ((m = regex.exec(title)) !== null) {
  3467. // 这对于避免零宽度匹配的无限循环是必要的
  3468. if (m.index === regex.lastIndex) {
  3469. regex.lastIndex++;
  3470. }
  3471. let tag = m[0];
  3472. if(tag == null || tag.length == 0) return -1;
  3473. let tagCore = tag.substring(1,tag.length - 1);
  3474. // 正确提取
  3475. resultTitle = resultTitle.toReplaceAll(tag,`<span style="${titleTagColorMatchHandler(tagCore)}" class="flag">${tagCore}</span>`);
  3476. }
  3477. return resultTitle;
  3478. }
  3479. // 3、添加标题处理器 titleTagHandler
  3480. registry.view.titleTagHandler.handlers.push(titleTagHandler)
  3481. // 给输入框加事件
  3482. // 执行 debounce 函数返回新函数
  3483. let handler = async function (e) {
  3484. // 搜索使用的数据版本
  3485. let version = registry.searchData.version;
  3486. let rawKeyword = e.target.value;
  3487.  
  3488. // 在本次搜索加入到历史前检查(如果之前是子搜索模式且现在还是子搜索模式那就跳过搜索,因为内是子搜索内容被修改不进行搜索)
  3489. if(registry.searchData.subSearch.isEnteredSubSearchMode && registry.searchData.subSearch.isSubSearchMode()
  3490. && registry.searchData.searchHistory.seeCurrentEqualsLastByRealKeyword()) return;
  3491.  
  3492. // 添加到搜索历史(维护这个历史有用是为了子搜索模式的“进”-“出”)
  3493. registry.searchData.searchHistory.add(rawKeyword)
  3494.  
  3495. // 字符串重叠匹配度搜索(类AI搜索)
  3496. async function stringOverlapMatchingDegreeSearch(rawKeyword) {
  3497. const endTis = registry.view.tis.beginTis("(;`O´)o 匹配度模式搜索中...")
  3498. // 这里为什么要用异步,不果不会那上面设置的tis会得不到渲染,先保证上面已经渲染完成再执行下面函数
  3499. return await new Promise((resolve,reject)=>{
  3500. waitViewRenderingComplete(() => {
  3501. try {
  3502. // 搜索逻辑开始
  3503. // `registry.searchData.getData()`会被排序desc
  3504. // 为什么需要拷贝data,因为全局的搜索位置不能改变!!
  3505. const searchBegin = Date.now();
  3506. let searchResult = overlapMatchingDegreeForObjectArray(rawKeyword.toUpperCase(),[...registry.searchData.getData()], (item)=>{
  3507. const str2ScopeMap = {}
  3508. const { tags , cleaned } = extractTagsAndCleanContent(`${item.title}`);
  3509. str2ScopeMap[cleaned.toUpperCase()] = 4;
  3510. str2ScopeMap[`${item.describe}${tags.join()}`.toUpperCase()] = 2;
  3511. str2ScopeMap[`${item.links && registry.searchData.links.stringifyForSearch(item.links)}${item.resource}${item.vassal}`.substring(0, 4096).toUpperCase()] = 1;
  3512. return str2ScopeMap;
  3513. },"desc",{sort:"desc",onlyHasScope:true});
  3514. const searchEnd = Date.now();
  3515. console.log("启动类AI搜索结果 :",searchResult)
  3516. console.logout("类AI搜索主逻辑耗时:"+(searchEnd - searchBegin ) +"ms");
  3517. resolve(searchResult)
  3518. }catch (e) {
  3519. console.error("类AI搜索异常!",e)
  3520. resolve([])
  3521. }finally {
  3522. endTis()
  3523. }
  3524. })
  3525. })
  3526. }
  3527. // 常规方式搜索(搜索逻辑入口)
  3528. async function search(rawKeyword,{isAccurateSearch = false} = {}) {
  3529. let processedKeyword = rawKeyword.trim().split(/\s+/).reverse().join(" ");
  3530. version = registry.searchData.version;
  3531. // 常规搜索
  3532. let searchResult = searchUnitHandler(registry.searchData.getData(),processedKeyword);
  3533. // 如果常规搜索不到使用类AI搜索(不能是精确搜索 && 常规搜索没有结果 && 搜索keyword不为空串)
  3534. if(!isAccurateSearch && (searchResult == null || searchResult.length === 0) && `${rawKeyword}`.trim().length > 0 ) {
  3535. searchResult = await stringOverlapMatchingDegreeSearch(rawKeyword)
  3536. }
  3537. return searchResult;
  3538. }
  3539. // 搜索AOP或说搜索代理
  3540. // 递归搜索,根据空字符切换出来的多个keyword
  3541. // let searchResultData = searchUnitHandler(registry.searchData.data,key)
  3542. let searchResultData = await searchAOP(search,rawKeyword);
  3543. // 如果搜索的内容无效,跳过内容的显示
  3544. if(searchResultData == null) return;
  3545. // 放到视图上
  3546. // 置空内容
  3547. matchItems.html("")
  3548. // 最多显示条数
  3549. let show_item_number = registry.searchData.showSize ;
  3550. function getFaviconImgHtml(searchResultItem) {
  3551. if(searchResultItem == null) return null;
  3552. let resource = searchResultItem.resource.trim();
  3553. let customIcon = null;
  3554. if(searchResultItem.icon != null) {
  3555. customIcon = searchResultItem.icon;
  3556. }else {
  3557. let type = searchResultItem.type;
  3558. // 如果不是url,那其它类型就需要自定义图标
  3559. let typesAndImg = {
  3560. "sketch":"data:image/png;base64,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",
  3561. "script":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyB0PSIxNjkyMzU4NTM5NjI1IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9Ijc2MjUiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCI+PHBhdGggZD0iTTk1OC4xNyA0NDcuNEw3NjAuNjkgMjQ5LjkybC02NS44MiA2NS44MyAxOTcuNDcgMTk3LjQ3TDY5NC44NyA3MTAuN2w2NS44MiA2NS44MiAxOTcuNDgtMTk3LjQ3IDY1LjgzLTY1Ljgzek0yNjMuMyAyNDkuOTJMNjUuODIgNDQ3LjQgMCA1MTMuMjJsNjUuODIgNjUuODNMMjYzLjMgNzc2LjUybDY1LjgyLTY1LjgyLTE5Ny40Ny0xOTcuNDggMTk3LjQ3LTE5Ny40N3pNMzQzLjI0NyA5NDkuNDgzTDU5MC45NiA1Mi4xOWw4OS43MiAyNC43NjgtMjQ3LjcxMyA4OTcuMjk1eiIgZmlsbD0iIzIzMTgxNSIgcC1pZD0iNzYyNiI+PC9wYXRoPjwvc3ZnPg=="
  3562. }
  3563. // url与sketch类型可互转,主要看resource
  3564. type = (type == "url" || type == "sketch")?(isUrl(resource)?"url":"sketch"):type;
  3565. if(type != "url") customIcon = typesAndImg[type];
  3566. }
  3567. if(customIcon != null) {
  3568. return `<img src="${customIcon}" />`
  3569. }else {
  3570. return `<img src="${registry.searchData.getFaviconAPI(resource)}" standbyFavicon="${registry.searchData.getFaviconAPI(resource,true)}" class="searchItem" />`
  3571. }
  3572. }
  3573.  
  3574. // 标题内容处理器
  3575. function titleContentHandler(title) {
  3576. // 对标题去掉所有tag
  3577. const { cleaned } = extractTagsAndCleanContent(title)
  3578. title = cleaned
  3579. // 如果带#将加上删除线,通过加obsolete类名方式
  3580. return `<span class="item_title ${title.startsWith("#")?'obsolete':''}">${title.replace(/^#/,"")}</span>`;
  3581. }
  3582.  
  3583.  
  3584. let matchItemsHtml = "";
  3585. // 真正渲染到列表的数据项
  3586. let searchData = []
  3587. for(let searchResultItem of searchResultData ) {
  3588. // 限制条数
  3589. if(show_item_number-- <= 0 && !registry.searchData.isSearchAll) {
  3590. break;
  3591. }
  3592. // 显示时清理标签-虽然在加载数据时已经清理了,但这是后备方案
  3593. // clearHideTag(searchResultItem);
  3594. // 将数据放入局部容器中
  3595. searchData.push(searchResultItem)
  3596.  
  3597. let isSketch = !isUrl(searchResultItem.resource);// searchResultItem.resource.trim().toUpperCase().indexOf("HTTP") != 0;
  3598. let vassalSvg = `<svg t="1685187993813" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3692" width="200" height="200"><path d="M971.904 372.736L450.901333 887.338667a222.976 222.976 0 0 1-312.576 0 216.362667 216.362667 0 0 1 0-308.736l468.906667-463.232a148.736 148.736 0 0 1 208.469333 0 144.298667 144.298667 0 0 1 0 205.824L346.752 784.469333a74.325333 74.325333 0 0 1-104.192 0 72.106667 72.106667 0 0 1 0-102.912l416.853333-411.733333-52.181333-51.456-416.853333 411.733333a144.298667 144.298667 0 0 0 0 205.781334 148.650667 148.650667 0 0 0 208.426666 0l468.906667-463.146667a216.490667 216.490667 0 0 0 0-308.736 223.061333 223.061333 0 0 0-312.661333 0L60.16 552.832l1.792 1.792a288.384 288.384 0 0 0 24.277333 384.170667c106.24 104.917333 273.322667 112.768 388.906667 23.936l1.792 1.834666L1024 424.192l-52.096-51.456z" fill="#666666" p-id="3693"></path></svg>`;
  3599. // 构建快捷link html
  3600. function buildRelatedLinksHtml(links) {
  3601. if (links == null || links.length === 0) return '';
  3602. let html = `<div class="related-links">`;
  3603. // 遍历 links 数组,为每个链接生成对应的 <a> 标签
  3604. links.forEach(link => {
  3605. html += `<a href="${link.url}" target="_blank" title="${link.title}">${link.text}</a>`;
  3606. });
  3607. html += '</div>';
  3608. return html;
  3609. }
  3610. // 将符合的数据装载到视图
  3611. let item = `
  3612. <li class="resultItem">
  3613. <!--图标-->
  3614. ${getFaviconImgHtml(searchResultItem)}
  3615. <a href="${isSketch?'':searchResultItem.resource}" target="_blank" title="${searchResultItem.desc}" index="${searchResultItem.index}" version="${version}" class="enter_main_link">
  3616. <!--tag与标题-->
  3617. ${registry.view.titleTagHandler.execute(clearHideTagForTitle(searchResultItem.title))}${titleContentHandler(searchResultItem.title)}
  3618. <!--描述信息-->
  3619. <span class="item_desc">(${searchResultItem.desc})</span>
  3620. </a>
  3621. ${buildRelatedLinksHtml(searchResultItem.links)}
  3622. ${searchResultItem.vassal !=null?'<a index="'+searchResultItem.index+'" version="'+version+'" vassal="true" class="vassal" title="查看相关联/同类项内容" target="_blank">'+vassalSvg+'</a>':''}
  3623. </li>`
  3624. matchItemsHtml += item;
  3625. }
  3626. matchItems.html(matchItemsHtml);
  3627.  
  3628. let loadErrorTagIcon = "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDIwMCAyMDAiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj4KCTx0aXRsZT5hcDh6Yy12cm1kbzwvdGl0bGU+Cgk8ZGVmcz4KCQk8aW1hZ2UgIHdpZHRoPSIxOTQiIGhlaWdodD0iMTk0IiBpZD0iaW1nMSIgaHJlZj0iZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SdzBLR2dvQUFBQU5TVWhFVWdBQUFNSUFBQURDQ0FZQUFBQWI0UjB4QUFBQUFYTlNSMElCMmNrc2Z3QUFIWjFKUkVGVWVKenRYUXU0SkVWMXZzUmdpQ1lLdmpVKzhCRk5RREJDZ3ZMNTJnQVNnV0FJNXFyTDNlbnFxYTQ3M1QzTDFTV0o4YTJicUtnSmo1QVk4a0dJSmlDSjhqSVFKUVlSQ0JCV1dJS0NLQVo1aU1ndVlGWjJlY2d1N0M0NXA3cm0zcDY1M1QzZFBWVjlxbnU2disvLzdtVjFvYnZPK2F0T1ZaM3puNW1abE9lSko1NW9NUUc2M1lXWkR2ZG5HQTltbUVENE00NEhQejM0TS9rekgrVGZnYi92Q3Z3OWxQL3N3TC9UeFg4djc4OXdIczY0cmt2K3ZYVkg2a1A5WW5VQjUzeW0wdzNCV1FHOG1KUHJSeWpod3UvZHJqK3pkdTFhOHZHcEMxb2lGSVRyd293c0ltZHpTSjArRHlKaTRtclNCZlI2UGZMeHN4VXRFY1pnQmN5cUR1K0I0MVBQOXBxZ1FpeGN3YWpIMWlhMFJFakE3T3h4VVd6UEIrR0dCUTVzQ0xoYXVHNjdVclJFVU1CNE9wcjFHekx6bHlTRjU2MG10MFZMQkFJSUlhSlFvYVR6ZExzTHorNXcvN2RoOVhnSGhCMS93b1IvTXV3ZHpnUkNmUlgrOTNXQVd3RWJBVDlUMkFaNFF1SEJ3Wi9EMy9rUi9QMmJ3Qm12Z1BjNUgvNzVkSWNIZitIeVlKN3gvcUdjaDY5eVhYZTNTa2lCcXdTTXlZb3AybXhQTFJGY1VjejVPZWUvMnVtR2I0SFllalhqL21ud1o5Y29SMzZpWXR3SjRkcEZnT05ody83dWJ0ZmZDMWF6WHpCRkNvZUhzRXA0NVBacWlhRDVZMlg0STNLRUNXNndweVBDVmVCc2Z3ZXo4M2ZnejdZVE9IMU8rQS9BejR0aE5mbG9Wd1FIOVhxOXArZ2xSSFEwN016UGs5dXdKY0lFaU9ML1VHMStrd0Zod0M4NnZQZEdJTXBuNEordnAzZnVpZkFva1AwYkVHSzlIMWF3L2ZXU3dvZEp3aWUzYVV1RWdzQ2xQYzJvczdQSC9iS003WGx3THN4NkQxdmd3S1p3SzZ3V24zTGQzajVhU2RHZ2U0bkdFc0ZOQ1g4d25vWlovMjB3czUzRmFHSjhhbndQUXlqUFcvMFNMV1RnUVNQMkVJMGpRcmZiVDd6eEZVSThWNFlLWG5DN0JjNW9BM1pnK0FRaDR5eUdoWk1TQWsvZVptZG55ZTAvOVVUQTlJR2t5eTlYaEsrSG4rY0FIclBBK1d6RjNSQkNmZ2htOW1kTVNnamNRMUQ3d3RRU2dZMWNoTUd6QzRRL1IrS01aNEdUMVFpd1QrTCthYzc4L0NzbkpjVEttdTBmYWswRTExMHpsUEVaeGYvaE1SQzMza0x2VkxYRzQ3aUhjbDMvMXljaVJJM3ltV3BMaE5Fa09GamFENEdmTjFqZ1JFM0NEb1poWmEvMzh2SmtxTWZxVURzaVlCRktuQVN1Q0ZiQXovVVdPRTJUc1ExVzJWTW0yVU80d3U2OVE2MklnRm1TZzcxQXQ5dC9VWlRURSt5MHdGR21CVC9EazdmWjJka25seUtEeFNkTHRTRUNsanJpcVZDVVBoQWV6L0QybE40eHBoWGZnd25wNERKa1FEZ1doa3JXRTJIRllucTBQSnA3TStCL0xYQ0VGaEhPZ2ZqL1dlVUlZVmVvWkRVUlZzcTdBVXlBVzdPN3l2aHN3eUQ3c0ltSnNGZUdESGp4U2UxajFoTUI4MWdjR1ZkaU9vVE01NmMyZUlzc2lPQ0NVcXNERDZ6WU4xaEpCSWRqaHFPN204b0UzVUZ1NUJaNWNhOHIvTU1Ma3dIMmZxZ0cwaElodmhKd1dVTzdEOFNRTjF0ZzJCYkZzZFAxd2hQTG5DdzVEbDJacUZWRWtDZER3bC9aOEpUb2FjRjZwOWQ3Y1ZFeWNIN3NkQk1CTXlCVktFUnR3QmI2Y0gveFk5WndwaVBFZEJKQmJiS3V0TUJ3TGZUamNjY0xqczFIZ2lWVXJhWkJUZ1RNWTRHQitvRUZCbXRoRWp3NEJmWU5UeHBIZ0RpcURKTklpZUJ3LzBBbWwwOExETldpQW9UL2dXb2dTVTZmQmhSVGJqUVJaSzJ3RjJ5bE4wNkxpbkd0NDZ4K0ppdEFoaW8wVzBtSUVKME1ZYzQ3dVZGYTBPRDdFUGE4Z0JVZ2crbEx0OHFKSUpYYjJrdXlGbDU0UjBlSWx6SkwwakVxSllMTC9RWFc1Z3VaeGxad21xOHhFYndIa3hRZEovaTFZOEp3RHhRdjZIWjcrN2xld0pVazVRTVd2T3VkaGRRMGhEa3lWRVlFeHdzRmEwbGdFdmVnOWlyRTAwL1BOU21oamlxRXFFckZqL0s5YnlzU0ptRk5RMjJKQUJ2ak9kYUdRNmF3RlFqdzRZV0ZoVjhxNlFDN29MUWxvMDFzL0I2S0tlY2hBcFBIcXZySllKd0l3T0NqV0xzeE5vVnJPNkwvbTFyczVLN1pIVmFIZjJSMHEvWU51SnJsSVFMdUYrYm05QjZyR2lXQ085Ly9IWGp4Unl4d21NWUI0M3lVck5ROU14SVhQMTJlTjFsUGQ0aGtqQWl3ZkwwTVFxTDdxQjJtZ2RnT29WQ29td0J4Uk9Xdy9pY1lRVG1zV3BWeWJwNzFrY0VJRWFUYVFhc3RaQUpiWGVFZmJaSUVRNnVEbEhIQlhnd1ZmeWZzZWZLU1laWFh0NU1JTXAra1ZaZ3pBRlNoNng5YUZRbmlRUExCTzJ5bzhIdDNxcjFsWlRmUDJvbmc4T0RUOUU3VE1Jamc1OWl0aDRJRUErQmRSTVdiNlMzWUxpc1BFVndOOXd0YWllQUkvdzhxSEtocHdXT3dHaHhCU1lJNHF0eE1ZMWJ5M056QzAzS1JZY0pWUVJzUmxFN21GZ3NjcDBuWWpyTHQxTTQvaWs2bjgxUjR0MU9yR0FQNC9pL2xJUUlXOUt5ZG9QbWhGaUpBakxZcmJJNnZzOEJ4bW9UdFRJUmRhcWZQblB4UWNOa0xIakkrRmpBT2Vjamc4dkphU1ZxSWdHMkpMSENjSm1Henc4UERxQjA5RHpwZThHcDQzN3NNajhlRHE3eitLOGFUd1M5ZHZ6QXhFYkFCSDdPNjQyVHRjQnUyaktWMjhDSlkxZXM5bjVsWElsOHZJdzlEZHdzVEVTR0tGY003TEhDZXB1QlN2SU9oZHV3eWtPa1Joc05qN1AwMmxnaVlnbEdDREJNUkFmNmpKMW5nUEUzQUR0UUNRaVVQYW9lZUJFbzIva2FENDdUVjdmVitZendaaXU4VlNoUEI4WUlEV0JzU2FVQjRCL1VkZ1U1MGd1QTU4RjAvTkRoZVYyRm5wSEZrY0FwbXFKWWlndXJDMkhhbm1RdzdVZGdZaTltcG5WYzMxRVhZWmxOajUzSS9HRWNFUkpHOWJpa2lZQUdJQlk1VVkvZzNZNnNyYW9jMUNXYysrSDFtckFiRmZ5QlAvWUxEODk4NEZ5YUNXdnFNc2IyaHdNYm1HQzZjaHc0Q3c3c0x0YU5XQWJXNU5UT21Jdno3Y1VSQTVMMWtLMHdFMWFlQTJyRnN3K095NUZHRVowVEx0bit3SThKOXNWNjRiT1ZZRTRBSm1FQ0dLd3lOK1hZYzQzRkV5TnU3clJBUk9QZGZ3OW9OY2h6cjBQR3h1b3ZhNld3RjlycGoyRXpFeVBqN2w0MGpBc3NaSGhVaUFpcVZXZUI4MU1DSjRBdTZTaVNuQVNxTjI0Zzk4QVorSEJuY0hQY0t1WW1nYnBDcG5aQWExN0w1MVh0VE8xWWRBYUhqMllac2NqM3V1YkxKb0pFSThDKzgzQUpISkFOSzFzdVkxN0ltZUhXQmtubTgxNFJ0SEI3OFlUWVJzUGxNdHQxeUVVRjF0U2QzUmlMc1VPcDhha0R0YVlCWE55aTlXeE0yK3U2NFN6Wm56S1k1RnhFTTd2enRod2hYeHdlMENrSGFKb09KNE1zbTdLVDJJWm1yd3NKQ2VtYnFXQ0lvU1JaNmh5UWhRZkRGMGNHMG9RTmtuZUc2L2VjeEUvZFFVY0pmZG5qa3BhL21ZNGtBLzRKenlCMlNCZytwSkxLaHdleDBPdVRPVkhjd3ovOVRFelp6UmZpbUxDSmszVFJuRWtHcEZVL252WUVJejBnYXpHNVhqM3pJTkVQbXFvbmdKdjEyazlJejJXa1hUaklaTW9rQVM4bGZrenNrRVZ6UFB6SnBJQ0ZVSkhla0prQTFHZFJ0dDUycWRqNmREQ21LRjZrUHlna3lZemVDOXNOMWd6MFRpV0JJalhrYUFXVDRxbTY3T2NML3kwd2lwT3dUVWgvSEN4bTFNMUlpclN5d1NFWmppMnlvZW1mZG9mZjltTitWUllTa2pwMnBEL3lGZGRUT1NJbDBNZHIyUWswbmxHaVlWdHRCU1AvdUxDSWtkZDlKZkJSVHlaMlJFa3E3SjNFZ3V3WTd0MHdibkY3dnhUQ20yM1RhRGxidGIyb2hBdXkrajZkMlJHcW9yTW4wd1p5ZkozZWlwb0J4Ly9PYTdiY0RVK0JUN1Nmd1B1aTRzVVRBQktiYnFCMlJHdTU4LzdleWlCQWhyTFFwZGxPaFNqdjFWclBKSG5KWmwydkRJZTZ5WjZwdmt1TkU4TUxaOFVRWTNqc3d0ZXhpWGdzV2p6dndaM2pLaEduQWFlZlhMU0pBWFA5dm1tMTR6VGliWlJQQkMwK2tka0lib0ZTOUN4QWhQenJkTmw5cE9SSGt2WTFPRys0YzE3RXprd2pNcUJSSG5lRGZiSW9JQStES2dRKzFFOW9BcFl5aXRRY0RyTTdIWm8xL1BJRnllRFdJYnVVc2NFSTcwQlhCUWNiSm9NS3Fic0lHYnRxZ094cVIvYWF6OWdteDA3L2gxU0RhWUpBN29FVzQyNW1mZjZWcE1peUhML2NWMDVicGloMkJ0TnBQQkQ5WEdSTEprMUJNQkd4NFJlREIxeTF3UHR1d2hYbmhSNG8weFRaQkRCUzNiWG9ZRlRVeDFOdkFFUFllYjBzZFY1NndJc2hyYVdDUUJZNW5LN0FUME8xS09lRWNXSFpQeDd3V21GVSs1SGgrSDNzRllCcHc1dm0xRG9oUVNwUTBOWXlDYjd4U3M5MU9TaHZMK01YYTR1TncvMEFMbkswcEFNSUVwM2E2dmRkcGNmNUVRcWc0RjFhTEZSTjBpckVOS3YxZHB5MnV6UnJId1lRUzN4KzBNbzVtY0FQMmxwc1pxN1F3R1RBWjBIWHJueUtPSzZ6bThYOU1oVnlwSyt6d2l1Q0Y1MXZnTkkyRlZNTHovRS9nU1ZUV0JtNVNPRjV5TGsxZDRJaHdsZTZ4ZDBXd0ltMjhCdVdiOGZ1RGpkVE9Na1hBRGVIbFFJeVA0YjRpUGROMUFzQUt3UXRLbzlzQUpjMmlkYnpWS3BNeVZ2NFNFZFNKQ0xWelRETWVBVndDUnZrZ0xOWDc2dzJqL0ltNlRWWlBCS2xjcDNkOGVYQnU2dmp3R0JFNjg4SHZXZUFNTFphd1FRb0tDLzlvSFdGVWxucURiVEJSd29uOW10UEdabEJvcGU0UHd2ZFpZUHdXU1lpT3RNL0JYQnlWaGxDS0RERFprVHQ1SHFoemY5M2p1RDFyUWxra0FzdysvMHh1OEJaNWNBL01ZQjlYMmtDTlhCVVlEOTl1Wk93ZzVFd2JteVVpZU1IMUZoaTVSWDVzUTFGZHdHdlRqRHVLK2hBaGVLZUpNWVB2NzZTTkRXcFZEWWp3ZnhZWXQwVng3QURIK1JmT3c1ZWxHWGtSSmZzUFYwNkVTR0xUd0ZqNUgwc2JHOVNxbXNGbWRoWVl0TVZrMklhR3poTENyVU1sbmV1NmVJcnpPU05qRkpXREpxK1c4LzIyVUw5SmdPWC9uNUtOYmJmeVJzZUxhak1Va2U4eU1UYlpCZjMrektEN0lia1JXK2lCT2dvZlBpSzBqQWdRaGNqRVFUekRIM3BQNGIvTDROamNQam91QTJDK0ZzWmtQV3JqdGRBSEpkTTVaR2hxS1h2WFhTTm5mQ2I4bEJrNWlHUmRlSENmd2JGNU5PMi9qYmZ3V0tYekFXcmp0ZENIWlVUZ2RLdEJ0eHVKRjZRNjRHQkdkbnY3T0Y3d0k5TmprNnBWaFNXendJWVRxSTNYUWlORTJCMHlNdEZxZ0tkVWlVNDNSQUIzTjVpSTE4THZXNnNZbXpTdEtoZFhCQVBpU2kzb3NETmVTVWNoV0l6NS9Vbk94a1lJb0xSMWI2MXlmTkswcWlRUkRPakp0Q0NELysyNGdic1Z5OFpndHV0QTN5a0plRUtwMUtxSjdxMWtIbFBpSFF2K2NnbTlBVnZvZ09QNTc0MGJ1RW9TT1BQQk1oTGdIWlVyL01QVjNjQ2QxT09UMXZOaXNGbStndm9GVzJqQmc3MWU3K2xMeHExbWt4eGxKdmdxNUZtek8xYmp3ZWJ6WkJhbDdkalZiU25xNnBsd2ZCcXRDTmVRdjJBTERVYVdzKzZpY1dmNzVzczJjVC9neUJETTN3dWM2VXVzb2sxdmFRaC9KVXNnZ2hSYVkyM0NYUk93bmZWNkwyY1Zoa1dvNVlvaGhicUgwaXJyYmdxd0tYWlpBaEV3UlIxL3VaSDZCVnRNYU9Cb05vNFoxK3hwRVc3Q1ZYNy9GNmkvdmRBNHhSckh4eUZ6alpnSS9vZjZCVnRNQm5ETS9VYU5hMHIzQ05YaG9teFhlVUpGL3UyRk1IckhFcnRyd1YrbXVrVlVBN0JPYWh5SndWSGcwcEdnYnNsSXh2c3FOODEvd0lMdkxnNGV6TEhFZTQwMWtnaTZsY1dvc1JNekRWM3VMK0FGQ2pZRlBDWU05NWdUNG9WU0lRRnYwa1h3RGZqLzNXL0J1Mm93cnU4TmIyQm5wYml0TkxCQUkrdlpMMFNDQXY0SG1lNkdIaFVpcmVmRm9Dam5VdW9YMUlTTllQaFA0YVl4cjNHRkVNL2wzSDhOMXNsaUJSTktOenBlK0g3Wk9vc0hwNmdTMW90VkdHQ2ozTTBqcy8zK3J5U0dNUEVVaHdLNnFaaWdoNkVDa2dnM2tYaTBpUHNCcklpejRIc25nanphVFNDQ0pMcUpYcmVWZnB3WC9BQ2NWNkIycThrTjRvQTRRSklqd01uK3huQ21aRjVjbmhuS2pKeVZZM3dmbDRmRWs1OUJVaHltR1NRNWlkUnk1Y0YxRm56cnhFaEtVV2N4SXB4Ri9ZSWxjUU9HT2xqTVlab0FLVFBucnJDSzhDcXlKdE1SWHBTWENHV2dOdUZhbTNkUVFyVkZXelpCU0NLb3RGM3lsOHdMcVZIRGczZGl6RXBCZ0ZISUJMSklTZTBoZ3ZHNEY4T1dwUGRhaFNIT0JDU0Fiem9FZmo1SWJXK3RHTGxyWWQ1U1U4RVpWZFJNLzVJNWpJNGJROVQyb1hiK0ZNZDdQZ3pxWitBOWYxend1NUJBRzVRSTFmVUtWOHNOZlJ4ZWNHSHM5L1dMRXY2d2o0RU44cE9XclFZcG9VNHVSQTA3N0w0bExnRk1BVm4ycldJUUdobFREZENHeDNIamluazAxTTZlQnhpcXVXNndKODZvS25RNkZqZmdDSG1TNVlYSG9CQXd0dW5DMWFUc2Z3ZWRYNG5iM29yNVl2amZIQ0pDU1JKZ2tReUVYQTliWUhmdGZwUWtwVGxvSGorampwU29YeklONjJCanVpKzFjOXNNSEI4MVZsdnd3bWdRTW81emVOempZS2lBWk1KTkpQcUI4b1ZUQVk5WllIdmQySkEwRHAxQUVRRm1yQU1zZU1sUmJJZFo2Yzl0RFlOc0E0dmRpWUF6Znd0WG83ZzhKSjc4d0o4ZkJiOS9VclVIdTR2WmxobHFIdXVTaURBWVEzVWtTUDZTaTFDbk1HOFlmZEVXbVVUWWxEQ1d1TkZGNVlaV3ZNMUx5c2NhSVlLTW0renBuYllPbHFybnhGOTAyanBMRmdWdTBpMndtL1ZRQnhtWlJNQVRobHVvWHhSdmIrZm1GcDQyK3FLcmlLVkliSWNwcmRER1FRVGhNaUtNZHRXMG9XNVpiZm9TenJOYkltUVN3UXYrbHRwMmRVQlM4M2k4V1I4aUF2YjJJbjdSVFVra2tDOUxvTVJRSnpDWkMwWHZhTFpqTk9SbUkvdlBxSkdnV2FtOVBOaVNSb1RSRjI0eERCWDdranVhNWJoM21WK0pCQ0pnelNueGkyNUxKWUlJR3Q5eGZpSWlSRTFEa2s2TldpemgwbEcvR28wMDVLUE9uQitsZkZtOEtVMGpnOFBiVlNHVERNSi9LOE1iZUhxSHN4VW5qZnJVbkJETGlSRHRFOEtyS0Y5V0hRT21oRWUrc2RMRHBrQVYwZSswd09uc1EzU3lsaG9XalJBaCtDemx5enE4OThaMElrUk50T3ZVSnBVQ21OZlVraUhCdDdDbUl1NUxZcm5tMCtLanFuZm9YamJTd3N4T0NBT3NhTW1RQ1pmN0FhdHhPYVVCM0xuTWo3SldoSlc5M3JNWTdXenlEK09JTUFBMm1xQjJPSnVCR2E2czNUTkVFTUVYUi8wbktWdGg2SUZZL0diQ0YvNUo3bzd6WEY5UmVsT2h5REQxS3dPVzhjWjl4MGxwcWpoQ2hQQ3ZTRjhhVlNaeXJncURqOEtOOU56Y0FybmoyUWhXTXdFdUUwRDFrbEcvR1VzRWRRMU4rT0wrelZrZDBuT1JRKzBsVU8wTkZacXAyeWFSRWtFRUYxQTdJaWxFY05Pb2Y2VGRTUTA5RURzOW1SSFhxWUlqbjVuVkpyVWN3cG1GaGVsYU5Wd2VyaUYzUkdLb1hneER4L0JwNDdYc3NXRVd3VGFwOGNJU1hjamFaTE5Zc2J0VDg5TXAxR2xpMDFkNHM5eVBvbExXUmJ0NjN1b2lSSkRTMmVRZkFiaEVOVVBYUmdRWk5zMHZ5YVd2UkNHcmxHWjNxQUZFN2RCbHdIbjRxdHBLTXVyRnh0RnNoYXh4Vy9hZ2NocjhwVWNzK0JDOEVid09pOUoxa2lGT2l1ei9qMTI5aWZOQUdmNEdjcnRaQUNuQ0ZyZDN3aVZhSmhHaTA2UGdIT29QaVdGTDlGSCttNU1LZDB6QkdUT0QyQWhYK0VkYllDOWJzRmp1bXlmVVRYd3NIbEM4OEx1SG9jcWRGM3dOOXhLd0VmNElLalQzZXIybjZDUkNIVmNFR0pQVExiQ1JEYmhyNkU0cTRTWTVGeEZRUjVUVlR5MzZJYndIVWUrdVpVV29XNzIwRXYraXRvTUZrSVZtaTdic2REcmxpSUNQT25xeTRLTUs0K3JNVFhZays0Y0podXR4VlJsM09rWHQzSVdJMEJDeDNnbXh3L05XdjJUSmh2bFc5dFFIbGRoWVRUTVp3Y0cvRXhkOHhUd3FKVjZGWlkxRGFRY09Edy9MSWdLZUt2R2FuQ0NCMFMrakhuc0xjUEdTL2NKRkFhL1NSSWdHdHZhOUV6WW9uYVFNUWt2dDExeDdCdHNKQWU5NW5nVmpUb3A0RHdTM3dENHZrd2dxOTRmODQ0d09YTFRCekwxM2NIbitwaHNFUkpocVJRdWM5SVlWL3RJdjBBb1JRYVU2ZkovNkE4MUNOa29wdHBrV2R1WXd1U0w0QVAxNEVrSUU3eG5ZS0tubW9EUVI1Q3dUbFFEU2Y2UWg0Q3lTVlMrZENtNmZxQURqdmtNOW5vVFkxT2wwbmpxd1Q5R0phaXdSVUxxY1JYSVkxQjlxRXNlbE9ueFdtRFRtdHJKcU9DSmNaY0ZZRW1IcHlOVGx4ZTB5bGdoeXBvbTZLVnJ3c2Nhd0UwVmlvenlkQWtTdzdIZ1Z3b0dQV2pDV0JBZ2ZqZ3Q0bGJuL3lVVUVsWDlVdHd1Mk10aU9QZVdpaHRvNWlPRDJ0VGl3THNBN2ZkZUNNYXdjY1lGZmg1ZXpTUzRpeUdXWCszOU0vY0VWNGhITTU1OFpVenBLN2ZoRDlpRVdYeURFWnNkWi9VeW05bTFseHk4M0VWVGwyRDBXZkhoMUVNR1gwM0tZOGx6YlZ3VWx2SEEzK1hnUkFNTEJ0VXpkODB5aW5KNmJDQWdsRlVMKzhaV0NCLzgrdWpLNEtRWGdGRkJIM05NcUJQeFQ3SzBuYlRLaEdtSWhJc2pMaXFnT2xIb0FLb1dTVkZ4TXVhQjIvamlVcUJmNUdKSFlKWnFZcFYzU2ZOWUlFUkN3QkIxTVBRQ1ZEN2dYY0J6c0tDUFZIdWxKRmE1dW5PejdzSnJOL3dyOC9xK0FhMWw5RGtWdUhOei91TzdreDlpRmlhREk4QlVMQnFJcVBLb1VwN0dNbGR6NWgrd1E5VU11LzIzYy81emNBNDJFRmJLdkhnL2Z6cnp3K0NpUno3NTJzdzd2LzY2MFNZazdBMzFFd0ZSbWUvcXVtY2Fpa3JKdFlSRjJIcDNndXo2NytGMWowaEdpUzlYd0NGZ1J6MlkyaUlieDRGeW0rZVN1RkJFUTJFQ2JmRURNNDdiNHRUM3o3Q3JoaE5uNlA4dDlWM2pWVUZwSmdieWNxQ0c1YkU5TFpCUC9nWUZ5dXVkNTlFU0lzdno4YjF2Z3JLYXd3eFhobTlqSXNhbWoyUUNURVNINGFZbnZlckFqeEV2WjBDbFlNWEl6RWU1UFpSZVhCL1BTRHBwQ29vbUpFQnVRUm9yTlFoaDA0aWdKRnNrQU1YVzNTeXNZcHFxd1NuemJjQmtqSzdIS3FTTkxDcnY4MTR4c2g2dy9SSjJJQ05Hc2hCc3Flc2ZWaVNnamRiejBaS2RMdDJjb21XbjZhRkpUdmFKRWNIaDRDSUZkSHNLcVNWT2g2Y1JFZ05saFYzako5ZFRPcXhYQ1Aza2NDWllRWWcrNjZvbFFSbkludHNrY3hTb3ZmNDRPM3JoWGJoUHVlOHpnOGZYRVJKQXp4UHo4SzIwOFlpc0x6RVROVDRUQmhoTUpVVTBTbnRLbzNWTDR1NkxDblpSdnlFZm1UcmYzT2xiOXlkRjVqdUVrUnkxRWtHU0lkT2pKblZnVEhvZUJQNkF3R1ZBK3NvTDBDOVJ4S3ZWZHNRcXVKSXduSUlTTDFXY1czTzE1M2pQWWZQNnlTMUlpUkdRSXpyVEFpYlVBOXduU0FJWEpVT3c0c2d6S1htZ3VKYWlsSEFKa2tBRnZid2tFeExZNTNEK3dDaDFhclVUQVcwcVVVcUYyWW0xazRNRi9kN3NMenk1RGh0bSttV1ZjM3ZwNndXT2x2aWU2RU10ODc0RmlOQ3Ayb0Y2bzdFNkV2U1lvVm53UnJoNm5XV29sRWRRQVlsSEx6NmlkV0NOdTczVER0eFFsZ3FsYmFKZ2RQMVQyVzVTMFRXRlNLM24xYlZXT08wWVhvMDNCYTBVRWhOTG5iOWo5UW5oUnQrdnZWY1NCSmgzSFVjZ1luUWYzVGZJZHE3eitLNHA4ZzlvclZkMDhaajErYTFVa01FWUVTWVptMWk0OERzdjFHZDF1LzBWNW5NalZITnRpcURENU44ajY4N3duWVhoaHVxbktNY1pWQzVNY3F5U0JVU0pJd3hFM0p6U0g4R0hsbE9OblZLNUgvd2huU0JYYVRQcitOK1o3YnludTlsREZZN3VaemEvZXUyb1NHQ2VDcXA2eXFkZUNYb2pndzNtY2FwSVN3cVZKUlVwVGFubHZJTlJyMDk1VjNkNmVUVENlMi9ER21vSUV4b21BUUpsMmx3ZGZKM2RhTTNoRUtYeU1JWU0va2NTOHlpdlMyY1hvbW9GaU5CNXVPTUovRit3OVRvQU4vcmNZamZEemRuaUhQNklpUVNWRVFPQ3hxaXVDS3l4d1hPMVFqYjF6eGR3bERiUUwxazFUZjZkQjdIQzhrRkdTb0RJaUlMQm5nWnB4cUFkZU42NmVHU1A3TWtEUm9uOXNpY3NpRlQ3cWJ6U0ZuWTduOTZsSlVDa1JFQ3A5OTJvTERLQVZMZy9mbDRjSW1JSXhONWN2ZlJ2REtkWGpvZEx6KzRwSjhGNXFBcEFRQVlFeHRjT0RiMXBnQ0oxNHpPRzlOK1lodzdnMFl0ZGRJMjl6VldlZnB1cEliYmNoSENJbEFpTHFjOVk0QVlDTmd4TENjVUFOSHB6eE1VRVBrOGxrSGc5WC8reGgwUS9lVTRSM1dQQk5KckNOZW1Oc0RSRVFzbzZCKzUrM3dEQWE0VjlXU21JK3ZtTElsUGJnaC9UZllnU2JLWTlJclNUQ0FOZ2VsdFcwVjFzS1BsbWFDRkpDSmRoc3dUZVl3RjBkTDNnMXRjTmJTd1JKQmg3TXdVQnR0Y0JZT3JBOXFlZy9DeGdxcWk2bVRab1E0cmdldzBacVo3ZWVDQWh3bnRlenhnalpobmZNelMwOExSOFJwSEpnZytYYy9iUHdIb25hMFd0REJJUXFMTCtjM25pVFkxeVRRdGRkczN2RCt5SnZRMTFXYWdldkpSRVFVaStKQnlldytvY0pHMXdldUhMR3h5ek8rZFY3eXp3ZTdoK29Fdlp1dGVBZHpVQUVQNEdmYjZCMjdsb1RZUUFsR2RMVWMvVG1RZ1FYWU9NT2F2OXBEQkVRcWt6eVFuTGp0c2hEZ0ovYmRGUGNLQ0tvRjl4RnlmdzE5Vml4Q2JnUzd6K29mYVhSUkJoQVNiTTN0N2FobnRpTXF3RFduVkQ3eDlRUVlaRVF3ajlhYmNhb25XQ3E0WGpoK1p3Zit3SnFmNWhhSWlCVWZjUGFLZXJSWUE5NGNJdkR3OE9vZmFBbFFneHpRcnhRaVlyVi9haTFEdGlFWVJBZWIxUGJ2U1ZDQ3FRZVo3T3J1QWdSUG93TnZZOEp3ejJvN2R3U0lTZkFjRytJZW41Uk8wOGpzSlZ4L3pSVTFxTzJhMHVFMG9UQS9KMnliWldtSGRobE16eWVRbHVvSllJaE9DTGNWKzBoU21tRlRoazI0Z0VFNWtCUjI2MGxnaUhJVFRVUFBnN0cvckVGRG1jVGRzaVZrd2Z2d0VJcGFqdTFSS2dJV0QwVzlSWUlMMkxUdlVyY0F4UERwMUhUaU5vbXRtQnFIendGa1gzSW90T21oZ2tXSjJJVGhvbXU1eCtKUjZEVTQ5OCtGajR5ZlVPRVBjeWNaTlVyUHhzRE9QNFBzQitjSy95M3RzN2ZQb1VlN0UvV0ZjRkJlSGFPalVKWXZVcElOMkREUUJUTndyQ0hlaXpicDBHUDY3cTdLYzJpUDJPWTlNZURXNWdkb2RSbTJPdGNCVDlQZGIyZzB6cCsrMVQrWUxGOXQ5dmJ6eEhoS25VYTlRVjFrWGM3MDZwTTV6K2dXbkZkQ0FROHhlWGhHZ2h6RG5kNnZSZFRqMEg3dE0vWVIrcTh1c0dlc2tTVDl3OTF2WEJXQWtzMzVWNGtEbitsK3QrUGt1b1hzcVN6L3p3TTBhaS9ZNXFlL3dmaFNwMGVHdnoyYkFBQUFBQkpSVTVFcmtKZ2dnPT0iLz4KCTwvZGVmcz4KCTxzdHlsZT4KCTwvc3R5bGU+Cgk8dXNlIGlkPSJCYWNrZ3JvdW5kIiBocmVmPSIjaW1nMSIgeD0iNiIgeT0iMCIvPgo8L3N2Zz4=";
  3629.  
  3630. // 给刚才添加的img添加事件
  3631. for(let imgObj of registry.view.element.matchItems.find('img')) {
  3632. // 加载完成事件,去除加载背景
  3633. imgObj.onload = function(e) {
  3634. $(e.target).css({
  3635. "background": "#fff"
  3636. })
  3637. }
  3638. // 加载失败,设置自定义失败的本地图片
  3639. imgObj.onerror = function(e,a,b,c) {
  3640. let currentErrorImg = $(e.target);
  3641. let standbyFaviconAttr = "standbyFavicon";
  3642. let standbyFavicon = currentErrorImg.attr(standbyFaviconAttr);
  3643. if(standbyFavicon != null) {
  3644. // 如果备用favicon使用
  3645. currentErrorImg.attr("src",standbyFavicon)
  3646. currentErrorImg.removeAttr(standbyFaviconAttr)
  3647. }else {
  3648. // 如果备用favicon直接使用加载失败图标base64
  3649. currentErrorImg.attr("src",loadErrorTagIcon)
  3650. }
  3651. }
  3652. }
  3653.  
  3654. // 隐藏文本显示视图
  3655. textView.css({
  3656. "display":"none"
  3657. })
  3658. // 让搜索结果显示
  3659. let matchResultDisplay = "block";
  3660. if(searchResultData.length < 1) matchResultDisplay="none";
  3661. matchResult.css({
  3662. "display":matchResultDisplay,
  3663. "overflow":"hidden"
  3664. })
  3665. // 将显示搜索的数据放入全局容器中
  3666. registry.searchData.searchData = searchData;
  3667. // 指令归位(置零)
  3668. registry.searchData.pos = 0;
  3669. }
  3670.  
  3671. registry.view.element.matchItems.on("click","li > a",function(e) {
  3672. let targetObj = e.target;
  3673. // 如果当前标签是svg标签,那委托给父节点
  3674. while ( targetObj != null && !/^(a|A)$/.test(targetObj.tagName)) {
  3675. targetObj = targetObj.parentNode
  3676. }
  3677. // 取消默认事件,全部都是手动操作
  3678. e.preventDefault();
  3679. // 取消冒泡
  3680. window.event? window.event.cancelBubble = true : e.stopPropagation();
  3681. // 设置为阅读模式
  3682. // $("#my_search_input").val(":read");
  3683. // 获取当前结果在搜索数组中的索引
  3684. let dataIndex = parseInt($(targetObj).attr("index"));
  3685. let dataVersion = parseInt($(targetObj).attr("version"));
  3686. let currentSearchDataVersion = registry.searchData.version;
  3687. let itemData = registry.searchData.getData()[dataIndex];
  3688. if(itemData == null || dataVersion != currentSearchDataVersion ) {
  3689. console.log("后备方案(没有找到了?"+(itemData == null)+",数据版本改变了?"+(dataVersion != currentSearchDataVersion)+")")
  3690. // 索引出现问题-启动后备方案-全局搜索
  3691. let title = $(targetObj).parent().find(".item_title").text();
  3692. let desc = $(targetObj).parent().find(".item_desc").text();
  3693. // 从全局数据中根据title与desc进行匹配
  3694. itemData = registry.searchData.findSearchDataItem(title,desc)
  3695. // 从历史数据中找,根据title与desc进行匹配
  3696. if(itemData == null) itemData = registry.searchData.findSearchDataItem(title,desc,SelectHistoryRecorder.history)
  3697. }
  3698. // 给选择的item加分,便于后面调整排序 (这里的idFun使用注册表中已经有的,也是我们确认item唯一的函数)
  3699. if(itemData != null) DataWeightScorer.select(itemData,registry.searchData.idFun);
  3700. // 记录选择的item项
  3701. SelectHistoryRecorder.select(itemData,registry.searchData.idFun);
  3702. // === 如果是简述搜索信息,那就取消a标签的默认跳转事件===
  3703. let hasVassal = $(targetObj).attr("vassal") != null;
  3704. // 初始化textView注册表中的对象
  3705. function showTextPage(title,desc,body) {
  3706. registry.view.textView.show(`<span style='color:red'>标题</span>:${title}<br /><span style='color:red'>描述:</span>${desc}<br /><span style='color:red'>简述内容:</span><br />${md2html(body)} `)
  3707. }
  3708. if(hasVassal) {
  3709. showTextPage(itemData.title,"主项的相关/附加内容",itemData.vassal);
  3710. // 挂载一键code复制
  3711. codeCopyMount("#text_show");
  3712. return;
  3713. }else if(itemData.type == "script"){
  3714. // 是脚本,执行脚本
  3715. let callBeforeParse = new CallBeforeParse();
  3716. let jscript = ( itemData.resourceObj == null || itemData.resourceObj.script == null ) ?"function (obj) {alert('- _ - 脚本异常!')}":itemData.resourceObj.script;
  3717. // 调用里面的函数,传入注册表对象
  3718. // 打开网址函数
  3719.  
  3720. function open(url) {
  3721. let openUrl = url;
  3722. return {
  3723. simulator(operate = (click, roll, dimension) => {}) { // 模拟器
  3724. if(openUrl == null || operate == null || typeof operate != 'function') return;
  3725. let pageSimulatorScript = operate.toString();
  3726. addPageSimulatorScript(openUrl,pageSimulatorScript); // 保存模拟操作,模拟脚本将在指定时间内打开指定网址有效
  3727. window.open(openUrl); // 打开网址
  3728. return this;
  3729. }
  3730. }
  3731. }
  3732. let view = {
  3733. beforeCallback: null,
  3734. afterCallback: null,
  3735. mountBefore(handle) {
  3736. this.beforeCallback = handle;
  3737. return this;
  3738. },
  3739. mountAfter(handle) {
  3740. this.afterCallback = handle;
  3741. return this;
  3742. },
  3743. // mount是脚本项-脚本js调用
  3744. mount() {
  3745. // 看脚本js是否给beforeCallback ,如果有在此执行
  3746. if(this.beforeCallback != null) this.beforeCallback();
  3747. // 挂载MS_SCRIPT_ENV 实现系统脚本API到视图
  3748. registry.script.openSessionForMSSE();
  3749. // 挂载视图
  3750. let viewHtml = itemData.resourceObj['view:html'];
  3751. let viewCss = itemData.resourceObj['view:css'];
  3752. let viewJs = itemData.resourceObj['view:js'];
  3753. registry.view.textView.show(viewHtml,viewCss,viewJs);
  3754. // wait view complate alfter ...
  3755. waitViewRenderingComplete(()=>{
  3756. registry.script.tryRunTextViewHandler();
  3757. // 看脚本js是否给afterCallback ,如果有在此执行
  3758. if(this.afterCallback != null) this.afterCallback();
  3759. })
  3760. }
  3761. }
  3762. // 设置logo为运行图标
  3763. registry.view.logo.change("data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDM5IDM5IiB3aWR0aD0iMzkiIGhlaWdodD0iMzkiPgoJPHRpdGxlPuW+ruS/oeaIquWbvl8yMDIzMDgxODIzMDIxNzwvdGl0bGU+Cgk8ZGVmcz4KCQk8aW1hZ2UgIHdpZHRoPSIyMyIgaGVpZ2h0PSIyMyIgaWQ9ImltZzEiIGhyZWY9ImRhdGE6aW1hZ2UvcG5nO2Jhc2U2NCxpVkJPUncwS0dnb0FBQUFOU1VoRVVnQUFBQmNBQUFBWEFRTUFBQURhNUViakFBQUFBWE5TUjBJQjJja3Nmd0FBQUFOUVRGUkZ4MVJRaGlaMHV3QUFBQXhKUkVGVWVKeGpZS0FkQUFBQVhBQUJZNkd4Z1FBQUFBQkpSVTVFcmtKZ2dnPT0iLz4KCTwvZGVmcz4KCTxzdHlsZT4KCTwvc3R5bGU+Cgk8dXNlIGlkPSJCYWNrZ3JvdW5kIiBocmVmPSIjaW1nMSIgeD0iOCIgeT0iOCIvPgo8L3N2Zz4=")
  3764. try {
  3765. Function('obj',`(${jscript})(obj)`)({registry,cache,$,open,view})
  3766. } catch (error) {
  3767. setTimeout(()=>{alert("Ծ‸Ծ 你选择的是脚本项,而当前页面安全策略不允许此操作所依赖的函数!这种情况是极少数的,请换个页面试试!")},20)
  3768. console.logout("脚本执行失败!",error);
  3769. }
  3770. // logo图标还原
  3771. setTimeout(()=>{registry.view.logo.reset();},200)
  3772. return;
  3773. }else if(! isUrl(itemData.resource)) {
  3774. showTextPage(itemData.title,itemData.desc,itemData.resource)
  3775. return;
  3776. }
  3777. // 隐藏视图
  3778. registry.view.viewVisibilityController(false)
  3779.  
  3780. const initUrl = itemData.resource;//$(targetObj).attr("href"); // 不作改变的URL
  3781. let url = initUrl; // 进行修改,形成要跳转的真正url
  3782. let temNum = url.matchFetch(/\[\[[^\[\]]*\]\]/gm, function (matchStr,index) { // temNum是url中有几个 "[[...]]", 得到后,就已经得到解析了
  3783. let templateStr = matchStr;
  3784. // 使用全局的keyword, 构造出真正的keyword
  3785. let keyword = registry.searchData.keyword.split(":").reverse();
  3786. keyword.pop();
  3787. keyword = keyword.reverse().join(":").trim();
  3788.  
  3789. let parseAfterStr = matchStr.replace(/{keyword}/g,keyword).replace(/\[\[+|\]\]+/g,"");
  3790. url = url.replace(templateStr,parseAfterStr);
  3791. });
  3792. // 如果搜索的真正keyword为空字符串,则去掉模板跳转
  3793. if( registry.searchData.keyword.split(registry.searchData.subSearch.searchBoundary).length < 2
  3794. || registry.searchData.keyword.split(registry.searchData.subSearch.searchBoundary)[1].trim() == "" ) {
  3795. url = registry.searchData.clearUrlSearchTemplate(initUrl);
  3796. }
  3797. // 跳转(url如果有模板,可能已经去掉模板,取决于是“搜索模式”)
  3798. window.open(url);
  3799.  
  3800. })
  3801. //registry.searchData.searchHandle = handler;
  3802. const refresh = debounce(handler, 300)
  3803. // 第一次触发 scroll 执行一次 fn,后续只有在停止滑动 1 秒后才执行函数 fn
  3804. searchInputDocument.on('input', refresh)
  3805. }
  3806. function ensureViewHide() {
  3807. // 隐藏视图
  3808. // 如果视图还没有初始化,直接退出
  3809. if (! registry.view.initialized) return;
  3810. // 如果正在查看查看“简讯”,先退出简讯
  3811. const nowMode = registry.view.seeNowMode();
  3812. if(nowMode === registry.view.modeEnum.SHOW_ITEM_DETAIL) {
  3813. // 让简讯隐藏
  3814. registry.view.element.textView.css({"display":"none"})
  3815. // 让搜索结果显示
  3816. registry.view.element.matchResult.css({ display:"block",overflow: "hidden" })
  3817. // 通知简讯back事件
  3818. registry.view.itemDetailBackAfterEventListener.forEach(listener=>listener())
  3819. return;
  3820. }
  3821. // 让视图隐藏
  3822. viewDocument.style.display = "none";
  3823. // 将输入框内容置空,在置空前将值备份,好让未好得及的操作它
  3824. registry.view.element.input.val("")
  3825. // 将之前搜索结果置空
  3826. registry.view.element.matchItems.html("")
  3827. // 隐藏文本显示视图
  3828. registry.view.element.textView.css({
  3829. "display":"none"
  3830. })
  3831. // 让搜索结果隐藏
  3832. registry.view.element.matchResult.css({
  3833. "display":"none"
  3834. })
  3835. // 视图隐藏-清理旧数据
  3836. registry.searchData.clearData();
  3837. // 触发视图隐藏事件
  3838. registry.view.viewHideEventAfterListener.forEach(fun=>fun());
  3839. }
  3840. function showView() {
  3841. // 让视图可见
  3842. viewDocument.style.display = "block";
  3843. //聚焦
  3844. registry.view.element.input.focus()
  3845. // 当输入框失去焦点时,隐藏视图
  3846. registry.view.element.input.blur(function() {
  3847. const isLogoButtonPressedRef = registry.view.logo.isLogoButtonPressedRef
  3848. if(isLogoButtonPressedRef.value) {
  3849. console.logout("隐藏跳过,因为isLogoButtonPressedRef")
  3850. return
  3851. };
  3852. setTimeout(function(){
  3853. const isDebuging = isInstructions("debug");
  3854. const isSearching = registry.searchData.searchEven.isSearching;
  3855. // 当前视图是否在展示数据,如搜索结果,简述内容?如果在展示不隐藏
  3856. let isWaitSearch = registry.view.seeNowMode() === registry.view.modeEnum.WAIT_SEARCH;
  3857. if(isDebuging || isSearching || !isWaitSearch || isLogoButtonPressedRef.value) {
  3858. console.logout("隐藏跳过,条件列表不满足!")
  3859. return
  3860. };
  3861. registry.view.viewVisibilityController(false);
  3862. },registry.view.delayedHideTime)
  3863. });
  3864. }
  3865.  
  3866. // 返回给外界控制视图显示与隐藏
  3867. return function (isSetViewVisibility) {
  3868. if (isSetViewVisibility) {
  3869. // 让视图可见 >>>
  3870. // 如果还没初始化先初始化 // 初始化数据 initData();
  3871. if (! registry.view.initialized) {
  3872. // 初始化视图
  3873. initView();
  3874. // 初始化数据
  3875. // initData();
  3876. }
  3877. // 让视图可见
  3878. showView();
  3879. } else {
  3880. // 隐藏视图 >>>
  3881. ensureViewHide();
  3882. }
  3883. }
  3884. })();
  3885. // 触发策略——快捷键
  3886. let useKeyTrigger = function (viewVisibilityController) {
  3887. let isFirstShow = true;
  3888. // 将视图与触发策略绑定
  3889. function showFun() {
  3890. // 让视图可见
  3891. viewVisibilityController(true);
  3892. // 触发视图首次显示事件
  3893. if(isFirstShow) {
  3894. for(let e of registry.view.viewFirstShowEventListener) e();
  3895. isFirstShow = false;
  3896. }
  3897. }
  3898. window.addEventListener('message', event => {
  3899. // console.log("父容器接收到了信息~~")
  3900. if(event.data == MY_SEARCH_SCRIPT_VIEW_SHOW_EVENT) {
  3901. showFun() // 接收显示呼出搜索框
  3902. }
  3903. });
  3904. triggerAndEvent("ctrl+alt+s", showFun)
  3905. triggerAndEvent("Escape", function () {
  3906. // 如果视图还没有初始化,就跳过
  3907. if(registry.view.viewDocument == null ) return;
  3908. // 让视图不可见
  3909. viewVisibilityController(false);
  3910. })
  3911. }
  3912.  
  3913. // 触发策略组
  3914. let trigger_group = [useKeyTrigger];
  3915. // 初始化入选的触发策略
  3916. (function () {
  3917. for (let trigger of trigger_group) {
  3918. trigger(registry.view.viewVisibilityController);
  3919. }
  3920. })();
  3921.  
  3922. // 打开视图进行配置
  3923. // 显示配置视图
  3924. // 是否显示进度 - 进度控制
  3925. function clearCache() {
  3926. cache.remove(registry.searchData.SEARCH_DATA_KEY);
  3927. // 如果处于debug模式,也清理其它的
  3928. if(isInstructions("debug")) {
  3929. cache.remove(registry.searchData.CACHE_FAVICON_SOURCE_KEY);
  3930. }
  3931. // 触发缓存被清理事件
  3932. for(let fun of registry.searchData.dataCacheRemoveEventListener) fun();
  3933. }
  3934. GM_registerMenuCommand("订阅管理",function() {
  3935. showConfigView();
  3936. });
  3937. GM_registerMenuCommand("清理缓存",function() {
  3938. clearCache();
  3939. });
  3940.  
  3941. function giveTagsStatus(tagsOfData,userUnfollowList) {
  3942. // 赋予tags一个是否选中状态
  3943. // 将 userUnfollowList 转为以key为userUnfollowList的item.name值是Item的方便检索
  3944. let userUnfollowMap = userUnfollowList.reduce(function(result, item) {
  3945. result[item] = '';
  3946. return result;
  3947. }, {});
  3948. tagsOfData.forEach(item=>{
  3949. if(userUnfollowMap[item.name] != null ) {
  3950. // 默认都是选中状态,如果item在userUnfollowList上将此tag状态改为未选中状态
  3951. item.status = 0;
  3952. }
  3953. })
  3954. return tagsOfData;
  3955. }
  3956. function showConfigView() {
  3957. // 剃除已转关注的,添加新关注的
  3958. function reshapeUnfollowList(userUnfollowList,userFollowList,newUserUnfollowList) {
  3959. // 剃除已转关注的
  3960. userUnfollowList = userUnfollowList.filter(item => !userFollowList.includes(item));
  3961. // 添加新关注的
  3962. userUnfollowList = userUnfollowList.concat(newUserUnfollowList.filter(item => !userUnfollowList.includes(item)));
  3963. return userUnfollowList;
  3964. }
  3965.  
  3966. if($("#subscribe_save")[0] != null) return;
  3967. // 显示视图
  3968. // 用户维护的取消关注标签列表
  3969. let userUnfollowList = cache.get(registry.searchData.USER_UNFOLLOW_LIST_CACHE_KEY)?? registry.searchData.USER_DEFAULT_UNFOLLOW;
  3970. // 当前数据所有的标签
  3971. let tagsOfData = cache.get(registry.searchData.DATA_ITEM_TAGS_CACHE_KEY);
  3972. // 使用 userUnfollowList 给 tagsOfData中的标签一个是否选中状态,在userUnfollowList中不选中,不在选中,添加一个属性到tagsOfData用boolean表达
  3973. tagsOfData = giveTagsStatus(tagsOfData,userUnfollowList);
  3974. // 生成多选框html
  3975. let tagsCheckboxHtml = "";
  3976. tagsOfData.forEach(item=>{
  3977. tagsCheckboxHtml += `
  3978. <div>
  3979. <input type="checkbox" id="${item.name}" name="_tagsCheckBox" value="${item.name}" ${item.status==1?'checked':''} >
  3980. <label for="${item.name}">${item.name} ${item.count})</label>
  3981. </div>
  3982. `
  3983. })
  3984.  
  3985. DivPage(`
  3986. #my-search-view {
  3987. width: 500px;
  3988. max-height: 100%;
  3989. max-width: 100%;
  3990. background: pink;
  3991. position: fixed;
  3992. right: 0px;
  3993. top: 0px;
  3994. z-index: 2147383656;
  3995. padding: 20px;
  3996. box-sizing: border-box;
  3997. border-radius: 14px;
  3998. text-align: left;
  3999. button {
  4000. cursor: pointer;
  4001. }
  4002.  
  4003. ._topController {
  4004. width: 100%;
  4005. position: absolute;
  4006. top: 0px;
  4007. right: 0px;
  4008. text-align: right;
  4009. padding: 15px 15px 0px;
  4010. box-sizing: border-box;
  4011. * {
  4012. cursor: pointer;
  4013. }
  4014. #topController_close {
  4015. font-sise: 15px;
  4016. color: #e8221e;
  4017. }
  4018. }
  4019. .page {
  4020. .control_title {
  4021. margin: 10px 0px 5px;
  4022. font-size: 17px;
  4023. color: black;
  4024. }
  4025. }
  4026. .home {
  4027. .submitable {
  4028. color: #3CB371;
  4029. }
  4030. .tagsCheckBoxDiv > div {
  4031. width: 32%;
  4032. display: inline-block;
  4033. margin: 0px;
  4034. padding: 0px;
  4035. overflow: hidden;
  4036. text-overflow: ellipsis;
  4037. white-space: nowrap;
  4038. }
  4039. #all_subscribe {
  4040. width: 100%;
  4041. height: 150px;
  4042. box-sizing: border-box;
  4043. border: 4px solid #f5f5f5;
  4044. }
  4045. #subscribe_save {
  4046. margin-top: 20px;
  4047. border: none;
  4048. border-radius: 3px;
  4049. padding: 4px 17px;
  4050. cursor: pointer;
  4051. box-sizing: border-box;
  4052. background: #6161bb;
  4053. color: #fff;
  4054. }
  4055. .view-base-button {
  4056. background: #fff;
  4057. border: none;
  4058. font-size: 15px;
  4059. padding: 1px 10px;
  4060. cursor: pointer;
  4061. margin: 2px;
  4062. color: black;
  4063. }
  4064. ._topController span {
  4065. color: #3CB371;
  4066. }
  4067. .home label {
  4068. font-size: 13px;
  4069. }
  4070. }
  4071. .tis-hub {
  4072. .logo-search {
  4073. display: flex;
  4074. flex-direction: column;
  4075. align-items: center;
  4076. img {
  4077. display: block;
  4078. width: 40px;
  4079. height: 40px;
  4080. }
  4081. .keyword {
  4082. display: flex;
  4083. font-size: 12px;
  4084. width: 70%;
  4085. margin-top: 5px;
  4086. input {
  4087. border: none;
  4088. padding: 0 6px;
  4089. min-width: 100px;
  4090. line-height: 25px;
  4091. height: 25px;
  4092. flex-grow: 1;
  4093. }
  4094. button {
  4095. padding: 0 12px;
  4096. border: none;
  4097. background: #f0f0f0;
  4098. line-height: 25px;
  4099. height: 25px;
  4100. }
  4101. }
  4102. }
  4103. .search-type {
  4104. display: flex;
  4105. padding: 10px 0;
  4106. label {
  4107. display: flex;
  4108. align-items: center;
  4109. margin-right: 20px;
  4110. font-size: 14px;
  4111. input {
  4112. padding: 0;
  4113. margin: 0 3px 0 0;
  4114. }
  4115. }
  4116. }
  4117. .result-list {
  4118. min-height: 300px;
  4119. padding-top: 15px;
  4120. .hub-tis {
  4121. display: flex;
  4122. justify-content: space-between;
  4123. margin-bottom: 12px;
  4124. align-items: center;
  4125. button {
  4126. font-size: 10px;
  4127. line-height: 22px;
  4128. height: 22px;
  4129. padding: 0 15px;
  4130. border-radius: 3px;
  4131. border: none;
  4132. }
  4133. .tis-info {
  4134. display: flex;
  4135. flex-direction: column;
  4136. .title {
  4137. font-size: 14px;
  4138. font-weight: bold;
  4139. color: rgb(103, 0, 0);
  4140. }
  4141. .describe {
  4142. font-size: 12px;
  4143. font-weight: 400;
  4144. display: block;
  4145. font-size: smaller;
  4146. margin: 0.5em 0px;
  4147. color: #333333;
  4148. }
  4149.  
  4150. }
  4151. }
  4152. }
  4153. }
  4154. }
  4155. `,`
  4156. <div id="my-search-view">
  4157. <div class="_topController">
  4158. <span id="topController_close">X</span>
  4159. </div>
  4160. <div class="page home">
  4161. <div>
  4162. <p class="control_title">订阅总览:</p>
  4163. <textarea id="all_subscribe"></textarea>
  4164. </div>
  4165. <div>
  4166. <p class="control_title">公共仓库:</p>
  4167. <button id="pushTis" class="view-base-button">提交我的订阅到TisHub(<span class="submitable"> - </span>)</button>
  4168. <button id="openTisHub" class="view-base-button">Tis订阅市场</button>
  4169. <button id="clearToken" class="view-base-button" style="display:none;">清理Token (存在)</button>
  4170. </div>
  4171. <div>
  4172. <p class="control_title">关注标签:</p>
  4173. <div class="tagsCheckBoxDiv">
  4174. ${tagsCheckboxHtml}
  4175. </div>
  4176. </div>
  4177. <button id="subscribe_save">保存并应用</button>
  4178. </div>
  4179. <div class="page tis-hub">
  4180. <p class="control_title">订阅市场</p>
  4181. <div class="logo-search">
  4182. <a href="https://github.com/My-Search/TisHub" target="_blank">
  4183. <img src="https://cdn.jsdelivr.net/gh/My-Search/TisHub/favicon.ico" title="TisHub是一个GitHub仓库,订阅以Issues的方式存在!" />
  4184. </a>
  4185. <div class="keyword">
  4186. <input name="keyword" placeholder="请输入搜索关键字..." />
  4187. <button id="search-tishub">搜索</button>
  4188. </div>
  4189. </div>
  4190. <div class="search-type">
  4191. <label>
  4192. <input type="radio" name="search-type" value="installed" checked>
  4193. 已安装
  4194. </label><br>
  4195. <label>
  4196. <input type="radio" name="search-type" value="market">
  4197. 市场订阅
  4198. </label>
  4199. </div>
  4200. <div class="result-list">
  4201. <div class="list-rol">
  4202. </div>
  4203. </div>
  4204. </div>
  4205. </div>
  4206.  
  4207. `,function (selector,remove) {
  4208. let subscribe_text = selector("#all_subscribe");
  4209. let subscribe_save = selector("#subscribe_save");
  4210. let topController_close = selector("#topController_close");
  4211. let openTisHub = selector("#openTisHub");
  4212. let tisHubLink = "https://github.com/My-Search/TisHub/issues";
  4213. let pushTis = selector("#pushTis");
  4214. let commitableTisList = null;
  4215. let clearToken = selector("#clearToken");
  4216. let mySearchView = selector("#my-search-view");
  4217. let currentPage = setPage(); // 默认显示的是home页
  4218.  
  4219. // 刷新页
  4220. function setPage(page = "home") {
  4221. $(mySearchView).find('.page').hide().filter(`.${page}`).show();
  4222. }
  4223. setPage("home");
  4224. // 刷新视图状态
  4225. async function refreshViewState() {
  4226. // 更新token状态
  4227. $(clearToken).css({"display":GithubAPI.getToken() == null?"none":"inline-block"})
  4228. // 更新可提交数
  4229. let tisList = await TisHub.getTisHubAllTis();
  4230. if(tisList != null && tisList.length != 0) {
  4231. commitableTisList = TisHub.tisFilter(subscribe_text.value,tisList)??[]
  4232. $(pushTis).find("span").text(commitableTisList.length);
  4233. }
  4234. }
  4235. // 初始化subscribe_text的值
  4236. subscribe_text.value = getSubscribe();
  4237. // 初始化其它状态,通过调用refreshViewState()
  4238. refreshViewState();
  4239. // 当SubscribeText多行输入框内容发生改变时,刷新更新可提交数,通过调用refreshViewState()
  4240. let refreshSubscribeText = debounce(()=>{refreshViewState() }, 300)
  4241. subscribe_text.oninput = ()=>{refreshSubscribeText();}
  4242. // 保存
  4243. function configViewClose() {
  4244. remove();
  4245. }
  4246. // 点击保存时
  4247. subscribe_save.onclick=function() {
  4248. // 保存用户选择的关注标签(维护数据)
  4249. // 获取所有多选框元素
  4250. var checkboxes = selector(".tagsCheckBoxDiv input",true);
  4251. // 初始化已选中和未选中的数组
  4252. var userFollowList = [];
  4253. var newUserUnfollowList = [];
  4254. // 遍历多选框元素,将选中的元素的value值添加到checkedValues数组中,
  4255. // 未选中的元素的value值添加到uncheckedValues数组中
  4256. for (var i = 0; i < checkboxes.length; i++) {
  4257. if (checkboxes[i].checked) {
  4258. userFollowList.push(checkboxes[i].value);
  4259. } else {
  4260. newUserUnfollowList.push(checkboxes[i].value);
  4261. }
  4262. }
  4263. // 剃除已转关注的,添加新关注的
  4264. newUserUnfollowList = reshapeUnfollowList( userUnfollowList,userFollowList,newUserUnfollowList);
  4265. cache.set(registry.searchData.USER_UNFOLLOW_LIST_CACHE_KEY,newUserUnfollowList);
  4266.  
  4267. // 保存到对象
  4268. let allSubscribe = subscribe_text.value;
  4269. let validCount = editSubscribe(allSubscribe);
  4270. // 清除视图
  4271. configViewClose();
  4272. // 清理缓存,让数据重新加载
  4273. clearCache();
  4274. alert("保存配置成功!有效订阅数:"+validCount);
  4275.  
  4276. }
  4277. // 打开TitHub
  4278. openTisHub.onclick = function() {
  4279. // window.open(tisHubLink, "_blank");
  4280. setPage("tis-hub");
  4281. }
  4282. // push到TisHub公共仓库中
  4283. pushTis.onclick =async function () {
  4284. if(! confirm("是否确认要提交到TisHub公共仓库?")) return;
  4285. if(commitableTisList == null || commitableTisList.length == 0) {
  4286. alert("经过与TisHub中订阅的比较,本地没有可提交的订阅!")
  4287. return;
  4288. }
  4289. if(GithubAPI.getToken(true) == null) {
  4290. alert("获取token失败,无法继续!");
  4291. return;
  4292. }
  4293. // 组装提交的body
  4294. let body = (()=>{
  4295. let _body = "";
  4296. for(let tis of commitableTisList) _body+=tis;
  4297. return _body;
  4298. })();
  4299. if ( body == "") return;
  4300. let userInfo = await GithubAPI.setToken().getUserInfo();
  4301. if(userInfo == null) {
  4302. alert("提交异常,请检查网络或提交的Token信息!")
  4303. return;
  4304. }
  4305. GithubAPI.commitIssues({
  4306. "title": userInfo.name+"的订阅",
  4307. "body": body
  4308. }).then(response=>{
  4309. refreshViewState();
  4310. alert("提交成功(issues)!感谢您的参与,脚本因你而更加精彩。")
  4311. }).catch(error=>alert("提交失败~"))
  4312. }
  4313. // 清理token
  4314. clearToken.onclick = function(){
  4315. GithubAPI.clearToken(); // 清理token
  4316. refreshViewState(); // 刷新视图变量
  4317. };
  4318. // 关闭
  4319. $(topController_close).click(configViewClose)
  4320.  
  4321. // 点击搜索tis-hub
  4322. let installedList = cache.get(registry.searchData.USE_INSTALL_TISHUB_CACHE_KEY) || []; // [ {name: "官方订阅",describe: "这是官方订阅...", body: "<tis::http... />",status: ""} ] status: disable enable installable
  4323. let tisSearchInput = $(".tis-hub .keyword input");
  4324. let tisSearchBtn = $("#search-tishub");
  4325. let searchFun = async function() {
  4326. const keyword = tisSearchInput.val()?.trim() || '';
  4327. // 搜索类型(installed | market)
  4328. const searchType = $('.search-type input[name="search-type"]:checked').val();
  4329. let resultTisList = installedList.filter(item => keyword === "" || item.name.includes(keyword));
  4330. if(searchType === "market") {
  4331. let marketResult = await TisHub.getClosedIssuesTis({keyword})
  4332. marketResult = marketResult.map(hubTisInfo => {
  4333. return {
  4334. name: hubTisInfo.title,
  4335. describe: hubTisInfo.describe,
  4336. body: hubTisInfo.tisList.join('\n') || '',
  4337. state: "installable"
  4338. }
  4339. })
  4340. const installedMap = resultTisList.reduce((map, item) => {
  4341. map[item.name] = item;
  4342. return map;
  4343. }, {});
  4344.  
  4345. // 看本地是否已安装,如果已安装state就取已安装的项state
  4346. (resultTisList = marketResult).forEach(hubTis =>{
  4347. if(installedMap[hubTis.name]) hubTis.state = installedMap[hubTis.name].state;
  4348. });
  4349. }
  4350. // 列表渲染
  4351. const resultElement = $(".tis-hub .result-list > .list-rol");
  4352. resultElement.html('')
  4353. // 转状态名
  4354. function stateAsName(state) {
  4355. return (state === "disable" && "移除(未启用)") || (state === "enable" && "移除") || "安装";
  4356. }
  4357.  
  4358. for(let tis of resultTisList) {
  4359. // tis 有该订阅的名 tis.name
  4360. // tisMetaInfo 是tis.body 包含描述信息 tisMetaInfo.describe
  4361. const tisMetaInfo = new PageTextHandleChains(tis.body).parseAllDesignatedSingTags("tis")[0];
  4362. // 自己的搜索逻辑
  4363. if(! `${tis.name}`.includes(keyword) && (tisMetaInfo != null && ! `${tisMetaInfo.describe}`.includes(keyword))) return;
  4364. // 渲染到页面
  4365. resultElement.append(`
  4366. <div class="hub-tis">
  4367. <div class="tis-info">
  4368. <a class="title" href="${tisMetaInfo.tabValue}" target="_blank">${tis.name}</a>
  4369. <span class="describe">${tisMetaInfo.describe || '订阅没有描述信息,请确认订阅安全或信任后再安装!'}</span>
  4370. </div>
  4371. <button class="tis-button" tis-name="${tis.name}">${ stateAsName(tis.state) }</button>
  4372. </div>
  4373. `)
  4374. }
  4375. // 当点击tis-button按钮时
  4376. $(".hub-tis .tis-button").click(function() {
  4377. // 使用 $(this) 获取当前被点击的元素
  4378. const button = $(this);
  4379. const tisName = button.attr("tis-name");
  4380. let tis = installedList.find(item=>item.name === tisName);
  4381. if(tis != null) {
  4382. // 移除
  4383. installedList = installedList.filter(item => item.name !== tisName);
  4384. tis.state = "installable";
  4385. }else {
  4386. // 安装
  4387. const hubTis = resultTisList.find(item=>item.name === tisName);
  4388. hubTis.state = "enable";
  4389. installedList.unshift(tis = hubTis);
  4390. }
  4391. // 更新状态
  4392. button.html(stateAsName(tis.state));
  4393. // 保存
  4394. console.log("保存:",installedList)
  4395. cache.set(registry.searchData.USE_INSTALL_TISHUB_CACHE_KEY,installedList);
  4396. // 清理缓存
  4397. clearCache()
  4398. });
  4399. }
  4400. // 点击搜索
  4401. const searchButton = tisSearchBtn.click(searchFun).click();
  4402. // 回车键触发搜索
  4403. tisSearchInput.on("keydown", function(event) {
  4404. if (event.key === "Enter") {
  4405. searchFun(); // 调用搜索功能
  4406. }
  4407. });
  4408.  
  4409. // 单选框值改变时,搜索
  4410. const radioButtons = document.querySelectorAll('input[name="search-type"]');
  4411. radioButtons.forEach(radio => {
  4412. radio.addEventListener('change', function() {
  4413. if (this.checked) {
  4414. searchButton.click();
  4415. }
  4416. });
  4417. });
  4418.  
  4419. })
  4420. }
  4421.  
  4422. })(unsafeWindow);
  4423. // unsafeWindow是真实的window,作为参数传入