🔗 链接助手

支持所有网站在新标签页中打开第三方网站链接(外链),在新标签页中打开符合指定规则的本站链接,解析文本链接为超链接,微信公众号文本转可点击的超链接,图片链接转图片标签,解析 Markdown 格式链接与图片标签,解析 BBCode 格式链接与图片标签

当前为 2023-07-24 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name 🔗 Links Helper
  3. // @name:zh-CN 🔗 链接助手
  4. // @namespace https://github.com/utags/links-helper
  5. // @homepageURL https://github.com/utags/links-helper#readme
  6. // @supportURL https://github.com/utags/links-helper/issues
  7. // @version 0.5.1
  8. // @description Open external links in a new tab, open internal links matching the specified rules in a new tab, convert text to hyperlinks, convert image links to image tags(<img>), parse Markdown style links and image tags, parse BBCode style links and image tags
  9. // @description:zh-CN 支持所有网站在新标签页中打开第三方网站链接(外链),在新标签页中打开符合指定规则的本站链接,解析文本链接为超链接,微信公众号文本转可点击的超链接,图片链接转图片标签,解析 Markdown 格式链接与图片标签,解析 BBCode 格式链接与图片标签
  10. // @icon data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMTUnIGhlaWdodD0nMTUnIHZpZXdCb3g9JzAgMCAxNSAxNScgZmlsbD0nbm9uZScgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJz48cGF0aCBkPSdNMyAyQzIuNDQ3NzIgMiAyIDIuNDQ3NzIgMiAzVjEyQzIgMTIuNTUyMyAyLjQ0NzcyIDEzIDMgMTNIMTJDMTIuNTUyMyAxMyAxMyAxMi41NTIzIDEzIDEyVjguNUMxMyA4LjIyMzg2IDEyLjc3NjEgOCAxMi41IDhDMTIuMjIzOSA4IDEyIDguMjIzODYgMTIgOC41VjEySDNWM0w2LjUgM0M2Ljc3NjE0IDMgNyAyLjc3NjE0IDcgMi41QzcgMi4yMjM4NiA2Ljc3NjE0IDIgNi41IDJIM1pNMTIuODUzNiAyLjE0NjQ1QzEyLjkwMTUgMi4xOTQzOSAxMi45Mzc3IDIuMjQ5NjQgMTIuOTYyMSAyLjMwODYxQzEyLjk4NjEgMi4zNjY2OSAxMi45OTk2IDIuNDMwMyAxMyAyLjQ5N0wxMyAyLjVWMi41MDA0OVY1LjVDMTMgNS43NzYxNCAxMi43NzYxIDYgMTIuNSA2QzEyLjIyMzkgNiAxMiA1Ljc3NjE0IDEyIDUuNVYzLjcwNzExTDYuODUzNTUgOC44NTM1NUM2LjY1ODI5IDkuMDQ4ODIgNi4zNDE3MSA5LjA0ODgyIDYuMTQ2NDUgOC44NTM1NUM1Ljk1MTE4IDguNjU4MjkgNS45NTExOCA4LjM0MTcxIDYuMTQ2NDUgOC4xNDY0NUwxMS4yOTI5IDNIOS41QzkuMjIzODYgMyA5IDIuNzc2MTQgOSAyLjVDOSAyLjIyMzg2IDkuMjIzODYgMiA5LjUgMkgxMi40OTk5SDEyLjVDMTIuNTY3OCAyIDEyLjYzMjQgMi4wMTM0OSAxMi42OTE0IDIuMDM3OTRDMTIuNzUwNCAyLjA2MjM0IDEyLjgwNTYgMi4wOTg1MSAxMi44NTM2IDIuMTQ2NDVaJyBmaWxsPSdjdXJyZW50Q29sb3InIGZpbGwtcnVsZT0nZXZlbm9kZCcgY2xpcC1ydWxlPSdldmVub2RkJz48L3BhdGg+PC9zdmc+
  11. // @author Pipecraft
  12. // @license MIT
  13. // @match https://*/*
  14. // @match http://*/*
  15. // @run-at document-start
  16. // @grant GM.getValue
  17. // @grant GM.setValue
  18. // @grant GM_addValueChangeListener
  19. // @grant GM_removeValueChangeListener
  20. // @grant GM_addElement
  21. // @grant GM_addStyle
  22. // @grant GM.registerMenuCommand
  23. // ==/UserScript==
  24. //
  25. ;(() => {
  26. "use strict"
  27. var listeners = {}
  28. var getValue = async (key) => {
  29. const value = await GM.getValue(key)
  30. return value && value !== "undefined" ? JSON.parse(value) : void 0
  31. }
  32. var setValue = async (key, value) => {
  33. if (value !== void 0) {
  34. const newValue = JSON.stringify(value)
  35. if (listeners[key]) {
  36. const oldValue = await GM.getValue(key)
  37. await GM.setValue(key, newValue)
  38. if (newValue !== oldValue) {
  39. for (const func of listeners[key]) {
  40. func(key, oldValue, newValue)
  41. }
  42. }
  43. } else {
  44. await GM.setValue(key, newValue)
  45. }
  46. }
  47. }
  48. var _addValueChangeListener = (key, func) => {
  49. listeners[key] = listeners[key] || []
  50. listeners[key].push(func)
  51. return () => {
  52. if (listeners[key] && listeners[key].length > 0) {
  53. for (let i = listeners[key].length - 1; i >= 0; i--) {
  54. if (listeners[key][i] === func) {
  55. listeners[key].splice(i, 1)
  56. }
  57. }
  58. }
  59. }
  60. }
  61. var addValueChangeListener = (key, func) => {
  62. if (typeof GM_addValueChangeListener !== "function") {
  63. console.warn("Do not support GM_addValueChangeListener!")
  64. return _addValueChangeListener(key, func)
  65. }
  66. const listenerId = GM_addValueChangeListener(key, func)
  67. return () => {
  68. GM_removeValueChangeListener(listenerId)
  69. }
  70. }
  71. var doc = document
  72. var $ = (selectors, element) => (element || doc).querySelector(selectors)
  73. var $$ = (selectors, element) => [
  74. ...(element || doc).querySelectorAll(selectors),
  75. ]
  76. var createElement = (tagName, attributes) =>
  77. setAttributes(doc.createElement(tagName), attributes)
  78. var addElement = (parentNode, tagName, attributes) => {
  79. if (!parentNode) {
  80. return
  81. }
  82. if (typeof parentNode === "string") {
  83. attributes = tagName
  84. tagName = parentNode
  85. parentNode = doc.head
  86. }
  87. if (typeof tagName === "string") {
  88. const element = createElement(tagName, attributes)
  89. parentNode.append(element)
  90. return element
  91. }
  92. setAttributes(tagName, attributes)
  93. parentNode.append(tagName)
  94. return tagName
  95. }
  96. var addStyle = (styleText) => {
  97. const element = createElement("style", { textContent: styleText })
  98. doc.head.append(element)
  99. return element
  100. }
  101. var addEventListener = (element, type, listener, options) => {
  102. if (!element) {
  103. return
  104. }
  105. if (typeof type === "object") {
  106. for (const type1 in type) {
  107. if (Object.hasOwn(type, type1)) {
  108. element.addEventListener(type1, type[type1])
  109. }
  110. }
  111. } else if (typeof type === "string" && typeof listener === "function") {
  112. element.addEventListener(type, listener, options)
  113. }
  114. }
  115. var removeEventListener = (element, type, listener, options) => {
  116. if (!element) {
  117. return
  118. }
  119. if (typeof type === "object") {
  120. for (const type1 in type) {
  121. if (Object.hasOwn(type, type1)) {
  122. element.removeEventListener(type1, type[type1])
  123. }
  124. }
  125. } else if (typeof type === "string" && typeof listener === "function") {
  126. element.removeEventListener(type, listener, options)
  127. }
  128. }
  129. var getAttribute = (element, name) =>
  130. element ? element.getAttribute(name) : null
  131. var setAttribute = (element, name, value) =>
  132. element ? element.setAttribute(name, value) : void 0
  133. var setAttributes = (element, attributes) => {
  134. if (element && attributes) {
  135. for (const name in attributes) {
  136. if (Object.hasOwn(attributes, name)) {
  137. const value = attributes[name]
  138. if (value === void 0) {
  139. continue
  140. }
  141. if (/^(value|textContent|innerText)$/.test(name)) {
  142. element[name] = value
  143. } else if (/^(innerHTML)$/.test(name)) {
  144. element[name] = createHTML(value)
  145. } else if (name === "style") {
  146. setStyle(element, value, true)
  147. } else if (/on\w+/.test(name)) {
  148. const type = name.slice(2)
  149. addEventListener(element, type, value)
  150. } else {
  151. setAttribute(element, name, value)
  152. }
  153. }
  154. }
  155. }
  156. return element
  157. }
  158. var addAttribute = (element, name, value) => {
  159. const orgValue = getAttribute(element, name)
  160. if (!orgValue) {
  161. setAttribute(element, name, value)
  162. } else if (!orgValue.includes(value)) {
  163. setAttribute(element, name, orgValue + " " + value)
  164. }
  165. }
  166. var addClass = (element, className) => {
  167. if (!element || !element.classList) {
  168. return
  169. }
  170. element.classList.add(className)
  171. }
  172. var removeClass = (element, className) => {
  173. if (!element || !element.classList) {
  174. return
  175. }
  176. element.classList.remove(className)
  177. }
  178. var setStyle = (element, values, overwrite) => {
  179. if (!element) {
  180. return
  181. }
  182. const style = element.style
  183. if (typeof values === "string") {
  184. style.cssText = overwrite ? values : style.cssText + ";" + values
  185. return
  186. }
  187. if (overwrite) {
  188. style.cssText = ""
  189. }
  190. for (const key in values) {
  191. if (Object.hasOwn(values, key)) {
  192. style[key] = values[key].replace("!important", "")
  193. }
  194. }
  195. }
  196. var throttle = (func, interval) => {
  197. let timeoutId = null
  198. let next = false
  199. const handler = (...args) => {
  200. if (timeoutId) {
  201. next = true
  202. } else {
  203. func.apply(void 0, args)
  204. timeoutId = setTimeout(() => {
  205. timeoutId = null
  206. if (next) {
  207. next = false
  208. handler()
  209. }
  210. }, interval)
  211. }
  212. }
  213. return handler
  214. }
  215. if (typeof Object.hasOwn !== "function") {
  216. Object.hasOwn = (instance, prop) =>
  217. Object.prototype.hasOwnProperty.call(instance, prop)
  218. }
  219. var parseInt10 = (number, defaultValue) => {
  220. if (typeof number === "number" && !Number.isNaN(number)) {
  221. return number
  222. }
  223. if (typeof defaultValue !== "number") {
  224. defaultValue = Number.NaN
  225. }
  226. if (!number) {
  227. return defaultValue
  228. }
  229. const result = Number.parseInt(number, 10)
  230. return Number.isNaN(result) ? defaultValue : result
  231. }
  232. var headFuncArray = []
  233. var bodyFuncArray = []
  234. var headBodyObserver
  235. var startObserveHeadBodyExists = () => {
  236. if (headBodyObserver) {
  237. return
  238. }
  239. headBodyObserver = new MutationObserver(() => {
  240. if (doc.head && doc.body) {
  241. headBodyObserver.disconnect()
  242. }
  243. if (doc.head && headFuncArray.length > 0) {
  244. for (const func of headFuncArray) {
  245. func()
  246. }
  247. headFuncArray.length = 0
  248. }
  249. if (doc.body && bodyFuncArray.length > 0) {
  250. for (const func of bodyFuncArray) {
  251. func()
  252. }
  253. bodyFuncArray.length = 0
  254. }
  255. })
  256. headBodyObserver.observe(doc, {
  257. childList: true,
  258. subtree: true,
  259. })
  260. }
  261. var runWhenBodyExists = (func) => {
  262. if (!doc.body) {
  263. bodyFuncArray.push(func)
  264. startObserveHeadBodyExists()
  265. return
  266. }
  267. func()
  268. }
  269. var escapeHTMLPolicy =
  270. typeof trustedTypes !== "undefined" &&
  271. typeof trustedTypes.createPolicy === "function"
  272. ? trustedTypes.createPolicy("beuEscapePolicy", {
  273. createHTML: (string) => string,
  274. })
  275. : void 0
  276. var createHTML = (html) => {
  277. return escapeHTMLPolicy ? escapeHTMLPolicy.createHTML(html) : html
  278. }
  279. var addElement2 =
  280. typeof GM_addElement === "function"
  281. ? (parentNode, tagName, attributes) => {
  282. if (!parentNode) {
  283. return
  284. }
  285. if (typeof parentNode === "string") {
  286. attributes = tagName
  287. tagName = parentNode
  288. parentNode = doc.head
  289. }
  290. if (typeof tagName === "string") {
  291. const element = GM_addElement(tagName)
  292. setAttributes(element, attributes)
  293. parentNode.append(element)
  294. return element
  295. }
  296. setAttributes(tagName, attributes)
  297. parentNode.append(tagName)
  298. return tagName
  299. }
  300. : addElement
  301. var addStyle2 =
  302. typeof GM_addStyle === "function"
  303. ? (styleText) => GM_addStyle(styleText)
  304. : addStyle
  305. var registerMenuCommand = (name, callback, accessKey) => {
  306. if (window !== top) {
  307. return
  308. }
  309. if (typeof GM.registerMenuCommand !== "function") {
  310. console.warn("Do not support GM.registerMenuCommand!")
  311. return
  312. }
  313. GM.registerMenuCommand(name, callback, accessKey)
  314. }
  315. var style_default =
  316. "#browser_extension_settings_container{--browser-extension-settings-background-color: #f2f2f7;--browser-extension-settings-text-color: #444444;--browser-extension-settings-link-color: #217dfc;--sb-track-color: #00000000;--sb-thumb-color: #33334480;--sb-size: 2px;position:fixed;top:10px;right:30px;max-height:90%;height:600px;overflow:hidden;display:none;z-index:100000;border-radius:5px;-webkit-box-shadow:0px 10px 39px 10px rgba(62,66,66,.22);-moz-box-shadow:0px 10px 39px 10px rgba(62,66,66,.22);box-shadow:0px 10px 39px 10px rgba(62,66,66,.22) !important}#browser_extension_settings_container .browser_extension_settings_wrapper{display:flex;height:100%;overflow:hidden;background-color:var(--browser-extension-settings-background-color)}#browser_extension_settings_container .browser_extension_settings_wrapper h1{font-size:26px;font-weight:800;border:none}#browser_extension_settings_container .browser_extension_settings_wrapper h2{font-size:18px;font-weight:600;border:none}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container{overflow-x:auto;box-sizing:border-box;padding:10px 15px;background-color:var(--browser-extension-settings-background-color);color:var(--browser-extension-settings-text-color)}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div{background-color:#fff;font-size:14px;border-top:1px solid #ccc;padding:6px 15px 6px 15px}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div a,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div a:visited,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div a,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div a:visited{display:flex;justify-content:space-between;align-items:center;cursor:pointer;text-decoration:none;color:var(--browser-extension-settings-text-color)}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div a:hover,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div a:visited:hover,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div a:hover,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div a:visited:hover{text-decoration:none;color:var(--browser-extension-settings-text-color)}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div a span,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div a:visited span,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div a span,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div a:visited span{margin-right:10px;line-height:24px}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div.active,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div:hover,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div.active,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div:hover{background-color:#e4e4e6}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div.active a,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div.active a{cursor:default}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div:first-of-type,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div:first-of-type{border-top:none;border-top-right-radius:10px;border-top-left-radius:10px}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div:last-of-type,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div:last-of-type{border-bottom-right-radius:10px;border-bottom-left-radius:10px}#browser_extension_settings_container .thin_scrollbar{scrollbar-color:var(--sb-thumb-color) var(--sb-track-color);scrollbar-width:thin}#browser_extension_settings_container .thin_scrollbar::-webkit-scrollbar{width:var(--sb-size)}#browser_extension_settings_container .thin_scrollbar::-webkit-scrollbar-track{background:var(--sb-track-color);border-radius:10px}#browser_extension_settings_container .thin_scrollbar::-webkit-scrollbar-thumb{background:var(--sb-thumb-color);border-radius:10px}#browser_extension_settings_main{min-width:250px;overflow-y:auto;overflow-x:hidden;box-sizing:border-box;padding:10px 15px;background-color:var(--browser-extension-settings-background-color);color:var(--browser-extension-settings-text-color)}#browser_extension_settings_main h2{text-align:center;margin:5px 0 0;font-size:18px;font-weight:600;border:none}#browser_extension_settings_main footer{display:flex;justify-content:center;flex-direction:column;font-size:11px;margin:10px auto 0px;background-color:var(--browser-extension-settings-background-color);color:var(--browser-extension-settings-text-color)}#browser_extension_settings_main footer a{color:var(--browser-extension-settings-link-color) !important;text-decoration:none;padding:0}#browser_extension_settings_main footer p{text-align:center;padding:0;margin:2px;line-height:13px}#browser_extension_settings_main a.navigation_go_previous{color:var(--browser-extension-settings-link-color);cursor:pointer;display:none}#browser_extension_settings_main a.navigation_go_previous::before{content:\"< \"}#browser_extension_settings_main .option_groups{background-color:#fff;padding:6px 15px 6px 15px;border-radius:10px;display:flex;flex-direction:column;margin:10px 0 0}#browser_extension_settings_main .option_groups .action{font-size:14px;padding:6px 0 6px 0;color:var(--browser-extension-settings-link-color);cursor:pointer}#browser_extension_settings_main .bes_external_link{font-size:14px;padding:6px 0 6px 0}#browser_extension_settings_main .bes_external_link a,#browser_extension_settings_main .bes_external_link a:visited,#browser_extension_settings_main .bes_external_link a:hover{color:var(--browser-extension-settings-link-color);text-decoration:none;cursor:pointer}#browser_extension_settings_main .option_groups textarea{font-size:12px;margin:10px 0 10px 0;height:100px;width:100%;border:1px solid #a9a9a9;border-radius:4px;box-sizing:border-box}#browser_extension_settings_main .switch_option{display:flex;justify-content:space-between;align-items:center;padding:6px 0 6px 0;font-size:14px}#browser_extension_settings_main .option_groups>*{border-top:1px solid #ccc}#browser_extension_settings_main .option_groups>*:first-child{border-top:none}#browser_extension_settings_main .switch_option>span{margin-right:10px}#browser_extension_settings_main .option_groups .bes_tip{position:relative;margin:0;padding:0 15px 0 0;border:none;max-width:none;font-size:14px}#browser_extension_settings_main .option_groups .bes_tip .bes_tip_anchor{cursor:help;text-decoration:underline}#browser_extension_settings_main .option_groups .bes_tip .bes_tip_content{position:absolute;bottom:15px;left:0;background-color:#fff;color:var(--browser-extension-settings-text-color);text-align:left;padding:10px;display:none;border-radius:5px;-webkit-box-shadow:0px 10px 39px 10px rgba(62,66,66,.22);-moz-box-shadow:0px 10px 39px 10px rgba(62,66,66,.22);box-shadow:0px 10px 39px 10px rgba(62,66,66,.22) !important}#browser_extension_settings_main .option_groups .bes_tip .bes_tip_anchor:hover+.bes_tip_content,#browser_extension_settings_main .option_groups .bes_tip .bes_tip_content:hover{display:block}#browser_extension_settings_main .option_groups .bes_tip p,#browser_extension_settings_main .option_groups .bes_tip pre{margin:revert;padding:revert}#browser_extension_settings_main .option_groups .bes_tip pre{font-family:Consolas,panic sans,bitstream vera sans mono,Menlo,microsoft yahei,monospace;font-size:13px;letter-spacing:.015em;line-height:120%;white-space:pre;overflow:auto;background-color:#f5f5f5;word-break:normal;overflow-wrap:normal;padding:.5em;border:none}#browser_extension_settings_main .container{--button-width: 51px;--button-height: 24px;--toggle-diameter: 20px;--color-off: #e9e9eb;--color-on: #34c759;width:var(--button-width);height:var(--button-height);position:relative;padding:0;margin:0;flex:none;user-select:none}#browser_extension_settings_main input[type=checkbox]{opacity:0;width:0;height:0;position:absolute}#browser_extension_settings_main .switch{width:100%;height:100%;display:block;background-color:var(--color-off);border-radius:calc(var(--button-height)/2);border:none;cursor:pointer;transition:all .2s ease-out}#browser_extension_settings_main .switch::before{display:none}#browser_extension_settings_main .slider{width:var(--toggle-diameter);height:var(--toggle-diameter);position:absolute;left:2px;top:calc(50% - var(--toggle-diameter)/2);border-radius:50%;background:#fff;box-shadow:0px 3px 8px rgba(0,0,0,.15),0px 3px 1px rgba(0,0,0,.06);transition:all .2s ease-out;cursor:pointer}#browser_extension_settings_main input[type=checkbox]:checked+.switch{background-color:var(--color-on)}#browser_extension_settings_main input[type=checkbox]:checked+.switch .slider{left:calc(var(--button-width) - var(--toggle-diameter) - 2px)}#browser_extension_side_menu{min-height:100px;width:40px;opacity:0;position:fixed;top:80px;right:0;padding-top:20px;z-index:10000}#browser_extension_side_menu:hover{opacity:1}#browser_extension_side_menu button{cursor:pointer;width:24px;height:24px;border:none;background-color:rgba(0,0,0,0);background-image:url(\"data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M12.0002 16C14.2094 16 16.0002 14.2091 16.0002 12C16.0002 9.79086 14.2094 8 12.0002 8C9.79109 8 8.00023 9.79086 8.00023 12C8.00023 14.2091 9.79109 16 12.0002 16ZM12.0002 14C13.1048 14 14.0002 13.1046 14.0002 12C14.0002 10.8954 13.1048 10 12.0002 10C10.8957 10 10.0002 10.8954 10.0002 12C10.0002 13.1046 10.8957 14 12.0002 14Z' fill='black'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M15.1182 1.86489L15.5203 4.81406C15.8475 4.97464 16.1621 5.1569 16.4623 5.35898L19.2185 4.23223C19.6814 4.043 20.2129 4.2248 20.463 4.65787L22.5901 8.34213C22.8401 8.77521 22.7318 9.3264 22.3365 9.63266L19.9821 11.4566C19.9941 11.6362 20.0002 11.8174 20.0002 12C20.0002 12.1826 19.9941 12.3638 19.9821 12.5434L22.3365 14.3673C22.7318 14.6736 22.8401 15.2248 22.5901 15.6579L20.463 19.3421C20.2129 19.7752 19.6814 19.957 19.2185 19.7678L16.4623 18.641C16.1621 18.8431 15.8475 19.0254 15.5203 19.1859L15.1182 22.1351C15.0506 22.6306 14.6274 23 14.1273 23H9.87313C9.37306 23 8.94987 22.6306 8.8823 22.1351L8.48014 19.1859C8.15296 19.0254 7.83835 18.8431 7.53818 18.641L4.78195 19.7678C4.31907 19.957 3.78756 19.7752 3.53752 19.3421L1.41042 15.6579C1.16038 15.2248 1.26869 14.6736 1.66401 14.3673L4.01841 12.5434C4.00636 12.3638 4.00025 12.1826 4.00025 12C4.00025 11.8174 4.00636 11.6362 4.01841 11.4566L1.66401 9.63266C1.26869 9.3264 1.16038 8.77521 1.41041 8.34213L3.53752 4.65787C3.78755 4.2248 4.31906 4.043 4.78195 4.23223L7.53818 5.35898C7.83835 5.1569 8.15296 4.97464 8.48014 4.81406L8.8823 1.86489C8.94987 1.3694 9.37306 1 9.87313 1H14.1273C14.6274 1 15.0506 1.3694 15.1182 1.86489ZM13.6826 6.14004L14.6392 6.60948C14.8842 6.72975 15.1201 6.86639 15.3454 7.01805L16.231 7.61423L19.1674 6.41382L20.4216 8.58619L17.9153 10.5278L17.9866 11.5905C17.9956 11.7255 18.0002 11.8621 18.0002 12C18.0002 12.1379 17.9956 12.2745 17.9866 12.4095L17.9153 13.4722L20.4216 15.4138L19.1674 17.5862L16.231 16.3858L15.3454 16.982C15.1201 17.1336 14.8842 17.2702 14.6392 17.3905L13.6826 17.86L13.2545 21H10.746L10.3178 17.86L9.36131 17.3905C9.11626 17.2702 8.88037 17.1336 8.6551 16.982L7.76954 16.3858L4.83313 17.5862L3.57891 15.4138L6.0852 13.4722L6.01392 12.4095C6.00487 12.2745 6.00024 12.1379 6.00024 12C6.00024 11.8621 6.00487 11.7255 6.01392 11.5905L6.0852 10.5278L3.57891 8.58619L4.83312 6.41382L7.76953 7.61423L8.6551 7.01805C8.88037 6.86639 9.11625 6.72976 9.36131 6.60949L10.3178 6.14004L10.746 3H13.2545L13.6826 6.14004Z' fill='black'/%3E%3C/svg%3E\")}#browser_extension_side_menu button:hover{opacity:70%}#browser_extension_side_menu button:active{opacity:100%}@media(max-width: 500px){#browser_extension_settings_container{right:10px}#browser_extension_settings_container .extension_list_container{display:none}#browser_extension_settings_container .extension_list_container.bes_active{display:block}#browser_extension_settings_container .extension_list_container.bes_active+div{display:none}#browser_extension_settings_main a.navigation_go_previous{display:block}}"
  317. function createSwitch(options = {}) {
  318. const container = createElement("label", { class: "container" })
  319. const checkbox = createElement(
  320. "input",
  321. options.checked ? { type: "checkbox", checked: "" } : { type: "checkbox" }
  322. )
  323. addElement2(container, checkbox)
  324. const switchElm = createElement("span", { class: "switch" })
  325. addElement2(switchElm, "span", { class: "slider" })
  326. addElement2(container, switchElm)
  327. if (options.onchange) {
  328. addEventListener(checkbox, "change", options.onchange)
  329. }
  330. return container
  331. }
  332. function createSwitchOption(text, options) {
  333. const div = createElement("div", { class: "switch_option" })
  334. addElement2(div, "span", { textContent: text })
  335. div.append(createSwitch(options))
  336. return div
  337. }
  338. var besVersion = 17
  339. var openButton =
  340. '<svg viewBox="0 0 60.2601318359375 84.8134765625" version="1.1" xmlns="http://www.w3.org/2000/svg" class=" glyph-box" style="height: 9.62969px; width: 6.84191px;"><g transform="matrix(1 0 0 1 -6.194965820312518 77.63671875)"><path d="M66.4551-35.2539C66.4551-36.4746 65.9668-37.5977 65.0391-38.4766L26.3672-76.3672C25.4883-77.1973 24.4141-77.6367 23.1445-77.6367C20.6543-77.6367 18.7012-75.7324 18.7012-73.1934C18.7012-71.9727 19.1895-70.8496 19.9707-70.0195L55.5176-35.2539L19.9707-0.488281C19.1895 0.341797 18.7012 1.41602 18.7012 2.68555C18.7012 5.22461 20.6543 7.12891 23.1445 7.12891C24.4141 7.12891 25.4883 6.68945 26.3672 5.81055L65.0391-32.0312C65.9668-32.959 66.4551-34.0332 66.4551-35.2539Z"></path></g></svg>'
  341. var openInNewTabButton =
  342. '<svg viewBox="0 0 72.127685546875 72.2177734375" version="1.1" xmlns="http://www.w3.org/2000/svg" class=" glyph-box" style="height: 8.19958px; width: 8.18935px;"><g transform="matrix(1 0 0 1 -12.451127929687573 71.3388671875)"><path d="M84.5703-17.334L84.5215-66.4551C84.5215-69.2383 82.7148-71.1914 79.7852-71.1914L30.6641-71.1914C27.9297-71.1914 26.0742-69.0918 26.0742-66.748C26.0742-64.4043 28.1738-62.4023 30.4688-62.4023L47.4609-62.4023L71.2891-63.1836L62.207-55.2246L13.8184-6.73828C12.9395-5.85938 12.4512-4.73633 12.4512-3.66211C12.4512-1.31836 14.5508 0.878906 16.9922 0.878906C18.1152 0.878906 19.1895 0.488281 20.0684-0.439453L68.5547-48.877L76.6113-58.0078L75.7324-35.2051L75.7324-17.1387C75.7324-14.8438 77.7344-12.6953 80.127-12.6953C82.4707-12.6953 84.5703-14.6973 84.5703-17.334Z"></path></g></svg>'
  343. var relatedExtensions = [
  344. {
  345. id: "utags",
  346. title: "\u{1F3F7}\uFE0F UTags - Add usertags to links",
  347. url: "https://greasyfork.org/scripts/460718",
  348. },
  349. {
  350. id: "links-helper",
  351. title: "\u{1F517} \u94FE\u63A5\u52A9\u624B",
  352. description:
  353. "\u5728\u65B0\u6807\u7B7E\u9875\u4E2D\u6253\u5F00\u7B2C\u4E09\u65B9\u7F51\u7AD9\u94FE\u63A5\uFF0C\u56FE\u7247\u94FE\u63A5\u8F6C\u56FE\u7247\u6807\u7B7E\u7B49",
  354. url: "https://greasyfork.org/scripts/464541",
  355. },
  356. {
  357. id: "v2ex.rep",
  358. title:
  359. "V2EX.REP - \u4E13\u6CE8\u63D0\u5347 V2EX \u4E3B\u9898\u56DE\u590D\u6D4F\u89C8\u4F53\u9A8C",
  360. url: "https://greasyfork.org/scripts/466589",
  361. },
  362. {
  363. id: "v2ex.min",
  364. title: "v2ex.min - V2EX \u6781\u7B80\u98CE\u683C",
  365. url: "https://greasyfork.org/scripts/463552",
  366. },
  367. ]
  368. var getInstalledExtesionList = () => {
  369. return $(".extension_list_container .installed_extension_list")
  370. }
  371. var getRelatedExtesionList = () => {
  372. return $(".extension_list_container .related_extension_list")
  373. }
  374. var isInstalledExtension = (id) => {
  375. const list = getInstalledExtesionList()
  376. if (!list) {
  377. return false
  378. }
  379. const installed = $('[data-extension-id="'.concat(id, '"]'), list)
  380. return Boolean(installed)
  381. }
  382. var addCurrentExtension = (extension) => {
  383. const list = getInstalledExtesionList()
  384. if (!list) {
  385. return
  386. }
  387. if (isInstalledExtension(extension.id)) {
  388. return
  389. }
  390. const element = createInstalledExtension(extension)
  391. list.append(element)
  392. const list2 = getRelatedExtesionList()
  393. if (list2) {
  394. updateRelatedExtensions(list2)
  395. }
  396. }
  397. var activeExtension = (id) => {
  398. const list = getInstalledExtesionList()
  399. if (!list) {
  400. return false
  401. }
  402. for (const element of $$(".active", list)) {
  403. removeClass(element, "active")
  404. }
  405. const installed = $('[data-extension-id="'.concat(id, '"]'), list)
  406. if (installed) {
  407. addClass(installed, "active")
  408. }
  409. }
  410. var activeExtensionList = () => {
  411. const extensionListContainer = $(".extension_list_container")
  412. if (extensionListContainer) {
  413. addClass(extensionListContainer, "bes_active")
  414. }
  415. }
  416. var deactiveExtensionList = () => {
  417. const extensionListContainer = $(".extension_list_container")
  418. if (extensionListContainer) {
  419. removeClass(extensionListContainer, "bes_active")
  420. }
  421. }
  422. var createInstalledExtension = (installedExtension) => {
  423. const div = createElement("div", {
  424. class: "installed_extension",
  425. "data-extension-id": installedExtension.id,
  426. })
  427. const a = addElement2(div, "a", {
  428. onclick: installedExtension.onclick,
  429. })
  430. addElement2(a, "span", {
  431. textContent: installedExtension.title,
  432. })
  433. const svg = addElement2(a, "svg")
  434. svg.outerHTML = createHTML(openButton)
  435. return div
  436. }
  437. var updateRelatedExtensions = (container) => {
  438. container.innerHTML = createHTML("")
  439. for (const relatedExtension of relatedExtensions) {
  440. if (isInstalledExtension(relatedExtension.id)) {
  441. continue
  442. }
  443. const div4 = addElement2(container, "div", {
  444. class: "related_extension",
  445. })
  446. const a = addElement2(div4, "a", {
  447. href: relatedExtension.url,
  448. target: "_blank",
  449. })
  450. addElement2(a, "span", {
  451. textContent: relatedExtension.title,
  452. })
  453. const svg = addElement2(a, "svg")
  454. svg.outerHTML = createHTML(openInNewTabButton)
  455. }
  456. }
  457. function createExtensionList(installedExtensions) {
  458. const div = createElement("div", {
  459. class: "extension_list_container thin_scrollbar",
  460. })
  461. addElement2(div, "h1", { textContent: "Settings" })
  462. const div2 = addElement2(div, "div", {
  463. class: "installed_extension_list",
  464. })
  465. for (const installedExtension of installedExtensions) {
  466. if (isInstalledExtension(installedExtension.id)) {
  467. continue
  468. }
  469. const element = createInstalledExtension(installedExtension)
  470. div2.append(element)
  471. }
  472. addElement2(div, "h2", { textContent: "Other Extensions" })
  473. const div3 = addElement2(div, "div", {
  474. class: "related_extension_list",
  475. })
  476. updateRelatedExtensions(div3)
  477. return div
  478. }
  479. var prefix = "browser_extension_settings_"
  480. var randomId = String(Math.round(Math.random() * 1e4))
  481. var settingsContainerId = prefix + "container_" + randomId
  482. var settingsElementId = prefix + "main_" + randomId
  483. var getSettingsElement = () => $("#" + settingsElementId)
  484. var getSettingsStyle = () =>
  485. style_default
  486. .replaceAll(/browser_extension_settings_container/gm, settingsContainerId)
  487. .replaceAll(/browser_extension_settings_main/gm, settingsElementId)
  488. var storageKey = "settings"
  489. var settingsOptions
  490. var settingsTable = {}
  491. var settings = {}
  492. async function getSettings() {
  493. var _a
  494. return (_a = await getValue(storageKey)) != null ? _a : {}
  495. }
  496. async function saveSattingsValue(key, value) {
  497. const settings2 = await getSettings()
  498. settings2[key] =
  499. settingsTable[key] && settingsTable[key].defaultValue === value
  500. ? void 0
  501. : value
  502. await setValue(storageKey, settings2)
  503. }
  504. function getSettingsValue(key) {
  505. var _a
  506. return Object.hasOwn(settings, key)
  507. ? settings[key]
  508. : (_a = settingsTable[key]) == null
  509. ? void 0
  510. : _a.defaultValue
  511. }
  512. var closeModal = () => {
  513. const settingsContainer = getSettingsContainer()
  514. if (settingsContainer) {
  515. settingsContainer.style.display = "none"
  516. }
  517. removeEventListener(document, "click", onDocumentClick, true)
  518. removeEventListener(document, "keydown", onDocumentKeyDown, true)
  519. }
  520. function hideSettings() {
  521. closeModal()
  522. }
  523. var onDocumentClick = (event) => {
  524. const target = event.target
  525. if (
  526. target == null ? void 0 : target.closest(".".concat(prefix, "container"))
  527. ) {
  528. return
  529. }
  530. closeModal()
  531. }
  532. var onDocumentKeyDown = (event) => {
  533. if (event.defaultPrevented) {
  534. return
  535. }
  536. if (event.key === "Escape") {
  537. closeModal()
  538. event.preventDefault()
  539. }
  540. }
  541. async function updateOptions() {
  542. if (!getSettingsElement()) {
  543. return
  544. }
  545. for (const key in settingsTable) {
  546. if (Object.hasOwn(settingsTable, key)) {
  547. const item = settingsTable[key]
  548. const type = item.type || "switch"
  549. switch (type) {
  550. case "switch": {
  551. const checkbox = $(
  552. "#"
  553. .concat(
  554. settingsElementId,
  555. ' .option_groups .switch_option[data-key="'
  556. )
  557. .concat(key, '"] input')
  558. )
  559. if (checkbox) {
  560. checkbox.checked = getSettingsValue(key)
  561. }
  562. break
  563. }
  564. case "textarea": {
  565. const textArea = $(
  566. "#"
  567. .concat(
  568. settingsElementId,
  569. ' .option_groups textarea[data-key="'
  570. )
  571. .concat(key, '"]')
  572. )
  573. if (textArea) {
  574. textArea.value = getSettingsValue(key)
  575. }
  576. break
  577. }
  578. default: {
  579. break
  580. }
  581. }
  582. }
  583. }
  584. if (typeof settingsOptions.onViewUpdate === "function") {
  585. const settingsMain = createSettingsElement()
  586. settingsOptions.onViewUpdate(settingsMain)
  587. }
  588. }
  589. function getSettingsContainer() {
  590. const container = $(".".concat(prefix, "container"))
  591. if (container) {
  592. const theVersion = parseInt10(container.dataset.besVersion, 0)
  593. if (theVersion < besVersion) {
  594. container.id = settingsContainerId
  595. container.dataset.besVersion = String(besVersion)
  596. }
  597. return container
  598. }
  599. return addElement2(doc.body, "div", {
  600. id: settingsContainerId,
  601. class: "".concat(prefix, "container"),
  602. "data-bes-version": besVersion,
  603. style: "display: none;",
  604. })
  605. }
  606. function getSettingsWrapper() {
  607. const container = getSettingsContainer()
  608. return (
  609. $(".".concat(prefix, "wrapper"), container) ||
  610. addElement2(container, "div", {
  611. class: "".concat(prefix, "wrapper"),
  612. })
  613. )
  614. }
  615. function initExtensionList() {
  616. const wrapper = getSettingsWrapper()
  617. if (!$(".extension_list_container", wrapper)) {
  618. const list = createExtensionList([])
  619. wrapper.append(list)
  620. }
  621. addCurrentExtension({
  622. id: settingsOptions.id,
  623. title: settingsOptions.title,
  624. onclick: showSettings,
  625. })
  626. }
  627. function createSettingsElement() {
  628. let settingsMain = getSettingsElement()
  629. if (!settingsMain) {
  630. const wrapper = getSettingsWrapper()
  631. for (const element of $$(".".concat(prefix, "main"))) {
  632. element.remove()
  633. }
  634. settingsMain = addElement2(wrapper, "div", {
  635. id: settingsElementId,
  636. class: "".concat(prefix, "main thin_scrollbar"),
  637. })
  638. addElement2(settingsMain, "a", {
  639. textContent: "Settings",
  640. class: "navigation_go_previous",
  641. onclick() {
  642. activeExtensionList()
  643. },
  644. })
  645. if (settingsOptions.title) {
  646. addElement2(settingsMain, "h2", { textContent: settingsOptions.title })
  647. }
  648. const optionGroups = []
  649. const getOptionGroup = (index) => {
  650. if (index > optionGroups.length) {
  651. for (let i = optionGroups.length; i < index; i++) {
  652. optionGroups.push(
  653. addElement2(settingsMain, "div", {
  654. class: "option_groups",
  655. })
  656. )
  657. }
  658. }
  659. return optionGroups[index - 1]
  660. }
  661. for (const key in settingsTable) {
  662. if (Object.hasOwn(settingsTable, key)) {
  663. const item = settingsTable[key]
  664. const type = item.type || "switch"
  665. const group = item.group || 1
  666. const optionGroup = getOptionGroup(group)
  667. switch (type) {
  668. case "switch": {
  669. const switchOption = createSwitchOption(item.title, {
  670. async onchange(event) {
  671. const checkbox = event.target
  672. if (checkbox) {
  673. await saveSattingsValue(key, checkbox.checked)
  674. }
  675. },
  676. })
  677. switchOption.dataset.key = key
  678. addElement2(optionGroup, switchOption)
  679. break
  680. }
  681. case "textarea": {
  682. let timeoutId
  683. const div = addElement2(optionGroup, "div", {
  684. class: "bes_textarea",
  685. })
  686. addElement2(div, "textarea", {
  687. "data-key": key,
  688. placeholder: item.placeholder || "",
  689. onkeyup(event) {
  690. const textArea = event.target
  691. if (timeoutId) {
  692. clearTimeout(timeoutId)
  693. timeoutId = void 0
  694. }
  695. timeoutId = setTimeout(async () => {
  696. if (textArea) {
  697. await saveSattingsValue(key, textArea.value.trim())
  698. }
  699. }, 100)
  700. },
  701. })
  702. break
  703. }
  704. case "action": {
  705. addElement2(optionGroup, "a", {
  706. class: "action",
  707. textContent: item.title,
  708. onclick: item.onclick,
  709. })
  710. break
  711. }
  712. case "externalLink": {
  713. const div4 = addElement2(optionGroup, "div", {
  714. class: "bes_external_link",
  715. })
  716. addElement2(div4, "a", {
  717. textContent: item.title,
  718. href: item.url,
  719. target: "_blank",
  720. })
  721. break
  722. }
  723. case "tip": {
  724. const tip = addElement2(optionGroup, "div", {
  725. class: "bes_tip",
  726. })
  727. addElement2(tip, "a", {
  728. class: "bes_tip_anchor",
  729. textContent: item.title,
  730. })
  731. const tipContent = addElement2(tip, "div", {
  732. class: "bes_tip_content",
  733. innerHTML: createHTML(item.tipContent),
  734. })
  735. break
  736. }
  737. }
  738. }
  739. }
  740. if (settingsOptions.footer) {
  741. const footer = addElement2(settingsMain, "footer")
  742. footer.innerHTML = createHTML(
  743. typeof settingsOptions.footer === "string"
  744. ? settingsOptions.footer
  745. : '<p>Made with \u2764\uFE0F by\n <a href="https://www.pipecraft.net/" target="_blank">\n Pipecraft\n </a></p>'
  746. )
  747. }
  748. }
  749. return settingsMain
  750. }
  751. function addSideMenu() {
  752. if (!getSettingsValue("displaySettingsButtonInSideMenu")) {
  753. return
  754. }
  755. const menu =
  756. $("#browser_extension_side_menu") ||
  757. addElement2(doc.body, "div", {
  758. id: "browser_extension_side_menu",
  759. "data-bes-version": besVersion,
  760. })
  761. const button = $("button[data-bes-version]", menu)
  762. if (button) {
  763. const theVersion = parseInt10(button.dataset.besVersion, 0)
  764. if (theVersion >= besVersion) {
  765. return
  766. }
  767. button.remove()
  768. }
  769. addElement2(menu, "button", {
  770. type: "button",
  771. "data-bes-version": besVersion,
  772. title: "\u8BBE\u7F6E",
  773. onclick() {
  774. setTimeout(showSettings, 1)
  775. },
  776. })
  777. }
  778. function addCommonSettings(settingsTable3) {
  779. let maxGroup = 0
  780. for (const key in settingsTable3) {
  781. if (Object.hasOwn(settingsTable3, key)) {
  782. const item = settingsTable3[key]
  783. const group = item.group || 1
  784. if (group > maxGroup) {
  785. maxGroup = group
  786. }
  787. }
  788. }
  789. settingsTable3.displaySettingsButtonInSideMenu = {
  790. title: "Display Settings Button in Side Menu",
  791. defaultValue: !(
  792. typeof GM === "object" && typeof GM.registerMenuCommand === "function"
  793. ),
  794. group: maxGroup + 1,
  795. }
  796. }
  797. function handleShowSettingsUrl() {
  798. if (location.hash === "#bes-show-settings") {
  799. setTimeout(showSettings, 100)
  800. }
  801. }
  802. async function showSettings() {
  803. const settingsContainer = getSettingsContainer()
  804. const settingsMain = createSettingsElement()
  805. await updateOptions()
  806. settingsContainer.style.display = "block"
  807. addEventListener(document, "click", onDocumentClick, true)
  808. addEventListener(document, "keydown", onDocumentKeyDown, true)
  809. activeExtension(settingsOptions.id)
  810. deactiveExtensionList()
  811. }
  812. var initSettings = async (options) => {
  813. settingsOptions = options
  814. settingsTable = options.settingsTable || {}
  815. addCommonSettings(settingsTable)
  816. addValueChangeListener(storageKey, async () => {
  817. settings = await getSettings()
  818. await updateOptions()
  819. addSideMenu()
  820. if (typeof options.onValueChange === "function") {
  821. options.onValueChange()
  822. }
  823. })
  824. settings = await getSettings()
  825. addStyle2(getSettingsStyle())
  826. runWhenBodyExists(() => {
  827. initExtensionList()
  828. addSideMenu()
  829. })
  830. handleShowSettingsUrl()
  831. }
  832. var content_default =
  833. ".lh_selected_element{border:solid 1px red;cursor:not-allowed}a[data-lh-erased-href],a[data-lh-erased-href]:hover{cursor:default;pointer-events:none;text-decoration:none}"
  834. var lastTarget
  835. var handleMouseOver = (event) => {
  836. const target = event.target
  837. if (!target || target === lastTarget) {
  838. return
  839. }
  840. if (lastTarget) {
  841. removeClass(lastTarget, "lh_selected_element")
  842. }
  843. lastTarget = target
  844. while (lastTarget && !$("a", lastTarget)) {
  845. lastTarget = lastTarget.parentElement
  846. }
  847. if (lastTarget) {
  848. addClass(lastTarget, "lh_selected_element")
  849. }
  850. }
  851. var handleMouseClick = (event) => {
  852. event.preventDefault()
  853. event.stopPropagation()
  854. event.stopImmediatePropagation()
  855. if (lastTarget) {
  856. for (const element of $$("a[href]", lastTarget)) {
  857. const href = getAttribute(element, "href")
  858. if (href) {
  859. setAttribute(element, "data-lh-erased-href", href)
  860. element.removeAttribute("href")
  861. }
  862. }
  863. removeClass(lastTarget, "lh_selected_element")
  864. }
  865. removeEventListener(doc, "mouseover", handleMouseOver, true)
  866. removeEventListener(doc, "click", handleMouseClick, true)
  867. return false
  868. }
  869. function eraseLinks() {
  870. addEventListener(doc, "mouseover", handleMouseOver, true)
  871. addEventListener(doc, "click", handleMouseClick, true)
  872. }
  873. function restoreLinks() {
  874. for (const element of $$("a[data-lh-erased-href]")) {
  875. const href = getAttribute(element, "data-lh-erased-href")
  876. setAttribute(element, "href", href)
  877. delete element.dataset.lhErasedHref
  878. }
  879. }
  880. var image_url_default =
  881. '{\n "imgur.com": [\n "https?://imgur.com/(\\\\w+)($|\\\\?) -> https://i.imgur.com/$1.png # ex: https://imgur.com/gi2b1rj",\n "https?://imgur.com/(\\\\w+)\\\\.(\\\\w+) -> https://i.imgur.com/$1.$2 # ex: https://imgur.com/gi2b1rj.png"\n ],\n "imgur.io": [\n "https?://imgur.io/(\\\\w+)($|\\\\?) -> https://i.imgur.com/$1.png # ex: https://imgur.io/gi2b1rj",\n "https?://imgur.io/(\\\\w+)\\\\.(\\\\w+) -> https://i.imgur.com/$1.$2 # ex: https://imgur.io/gi2b1rj.png"\n ],\n "i.imgur.com": [\n "https?://i.imgur.com/(\\\\w+)($|\\\\?) -> https://i.imgur.com/$1.png"\n ],\n "camo.githubusercontent.com": [\n "https://camo.githubusercontent.com/.* # This is a img url, no need to replace value"\n ]\n}\n'
  882. var rules = JSON.parse(image_url_default)
  883. var cachedRules = {}
  884. var getHostname = (url) => (/https?:\/\/([^/]+)/.exec(url) || [])[1]
  885. var processRule = (rule, href) => {
  886. var _a
  887. let pattern
  888. let replacement
  889. const cachedRule = cachedRules[rule]
  890. try {
  891. if (cachedRule) {
  892. pattern = cachedRule.pattern
  893. replacement = cachedRule.replacement
  894. } else {
  895. const result = rule.replace(/ #.*/, "").split("->")
  896. const patternString = result[0].trim()
  897. pattern = new RegExp(
  898. patternString.startsWith("http")
  899. ? "^" + patternString
  900. : patternString,
  901. "i"
  902. )
  903. replacement = (_a = result[1]) == null ? void 0 : _a.trim()
  904. cachedRules[rule] = { pattern, replacement }
  905. }
  906. if (pattern.test(href)) {
  907. return replacement ? href.replace(pattern, replacement) : href
  908. }
  909. } catch (error) {
  910. console.error(error)
  911. }
  912. }
  913. var convertImgUrl = (href) => {
  914. if (!href) {
  915. return
  916. }
  917. const hostname = getHostname(href)
  918. if (Object.hasOwn(rules, hostname)) {
  919. for (const rule of rules[hostname]) {
  920. const newHref = processRule(rule, href)
  921. if (newHref) {
  922. return newHref
  923. }
  924. }
  925. }
  926. }
  927. var createImgTagString = (src, text) =>
  928. '<img src="'
  929. .concat(src, '" title="')
  930. .concat(text || "image", '" alt="')
  931. .concat(
  932. text || "image",
  933. '" role="img" style="max-width: 100% !important; vertical-align: bottom;" loading="lazy" referrerpolicy="no-referrer" rel="noreferrer" data-lh-status="1"/>'
  934. )
  935. var bindOnError = () => {
  936. for (const element of $$('img[data-lh-status="1"]')) {
  937. setAttribute(element, "data-lh-status", "2")
  938. addEventListener(element, "error", (event) => {
  939. const img = event.target
  940. const anchor = img.parentElement
  941. img.outerHTML = createHTML(getAttribute(img, "src"))
  942. if ((anchor == null ? void 0 : anchor.tagName) === "A") {
  943. setStyle(anchor, "opacity: 50%;")
  944. setAttribute(anchor, "data-message", "failed to load image")
  945. }
  946. })
  947. }
  948. }
  949. var anchorElementToImgElement = (anchor, href, text) => {
  950. anchor.innerHTML = createHTML(createImgTagString(href, text))
  951. setAttribute(anchor, "target", "_blank")
  952. addAttribute(anchor, "rel", "noopener")
  953. addAttribute(anchor, "rel", "noreferrer")
  954. }
  955. var linkToImg = (anchor) => {
  956. if (
  957. !anchor ||
  958. anchor.childElementCount !== 0 ||
  959. (anchor.childNodes[0] && anchor.childNodes[0].nodeType !== 3)
  960. ) {
  961. return
  962. }
  963. const href = anchor.href
  964. const text = anchor.textContent || href
  965. const newHref = convertImgUrl(href)
  966. if (newHref) {
  967. anchorElementToImgElement(anchor, newHref, text)
  968. } else if (
  969. /^https:[^?]+\.(?:jpg|jpeg|jpe|bmp|png|gif|webp|ico|svg)/i.test(href)
  970. ) {
  971. anchorElementToImgElement(anchor, href, text)
  972. }
  973. }
  974. var ignoredTags = /* @__PURE__ */ new Set([
  975. "A",
  976. "BUTTON",
  977. "SVG",
  978. "PATH",
  979. "G",
  980. "SCRIPT",
  981. "STYLE",
  982. "TEXTAREA",
  983. "CODE",
  984. "PRE",
  985. "TEMPLATE",
  986. "NOSCRIPT",
  987. "TITLE",
  988. ])
  989. var urlPattern =
  990. "\\b((?:https?:\\/\\/(?:[\\w-.]+\\.[a-z]{2,15}|localhost|(?:\\d{1,3}\\.){3}\\d{1,3}))(?::\\d+)?(?:\\/[\\w-/%.~+:;!@=&?#]*)?)"
  991. var linkPattern1 = new RegExp(
  992. "!\\[([^\\[\\]]*)\\]\\((?:\\s|<br/?>)*".concat(
  993. urlPattern,
  994. "(?:\\s|<br/?>)*\\)"
  995. ),
  996. "gim"
  997. )
  998. var linkPattern2 = new RegExp(
  999. "\\[([^\\[\\]]*)\\]\\((?:\\s|<br/?>)*".concat(
  1000. urlPattern,
  1001. "(?:\\s|<br/?>)*\\)"
  1002. ),
  1003. "gim"
  1004. )
  1005. var linkPattern3 = new RegExp(urlPattern, "gim")
  1006. var linkPattern4 = new RegExp(
  1007. "\\[img\\](?:\\s|<br/?>)*".concat(urlPattern, "(?:\\s|<br/?>)*\\[/img\\]"),
  1008. "gim"
  1009. )
  1010. var linkPattern5 = new RegExp(
  1011. "\\[url\\](?:\\s|<br/?>)*".concat(urlPattern, "(?:\\s|<br/?>)*\\[/url\\]"),
  1012. "gim"
  1013. )
  1014. var linkPattern6 = new RegExp(
  1015. "\\[url=".concat(urlPattern, "\\]([^\\[\\]]+)\\[/url\\]"),
  1016. "gim"
  1017. )
  1018. var replaceMarkdownImgLinks = (text) => {
  1019. if (text.search(linkPattern1) >= 0) {
  1020. text = text.replaceAll(linkPattern1, (m, p1, p2) => {
  1021. return createImgTagString(convertImgUrl(p2) || p2, p1)
  1022. })
  1023. }
  1024. return text
  1025. }
  1026. var replaceMarkdownLinks = (text) => {
  1027. if (text.search(linkPattern2) >= 0) {
  1028. text = text.replaceAll(linkPattern2, (m, p1, p2) => {
  1029. return '<a href="'
  1030. .concat(p2, '">')
  1031. .concat(p1.replaceAll(/<br>$/gi, ""), "</a>")
  1032. })
  1033. }
  1034. return text
  1035. }
  1036. var replaceTextLinks = (text) => {
  1037. if (text.search(linkPattern3) >= 0) {
  1038. text = text.replaceAll(linkPattern3, (m, p1) => {
  1039. return '<a href="'.concat(p1, '">').concat(p1, "</a>")
  1040. })
  1041. }
  1042. return text
  1043. }
  1044. var replaceBBCodeImgLinks = (text) => {
  1045. if (text.search(linkPattern4) >= 0) {
  1046. text = text.replaceAll(linkPattern4, (m, p1) => {
  1047. return createImgTagString(convertImgUrl(p1) || p1, p1)
  1048. })
  1049. }
  1050. return text
  1051. }
  1052. var replaceBBCodeLinks = (text) => {
  1053. if (text.search(linkPattern5) >= 0) {
  1054. text = text.replaceAll(linkPattern5, (m, p1) => {
  1055. return '<a href="'.concat(p1, '">').concat(p1, "</a>")
  1056. })
  1057. }
  1058. if (text.search(linkPattern6) >= 0) {
  1059. text = text.replaceAll(linkPattern6, (m, p1, p2) => {
  1060. return '<a href="'.concat(p1, '">').concat(p2, "</a>")
  1061. })
  1062. }
  1063. return text
  1064. }
  1065. var textToLink = (textNode, previousText) => {
  1066. var _a, _b
  1067. const textContent = (_a = textNode.textContent) != null ? _a : ""
  1068. const parentNode = textNode.parentNode
  1069. const mergedText = previousText + textContent
  1070. if (
  1071. !parentNode ||
  1072. textNode.nodeName !== "#text" ||
  1073. textContent.trim().length === 0 ||
  1074. mergedText.trim().length < 3
  1075. ) {
  1076. return
  1077. }
  1078. if (textContent.includes("://")) {
  1079. const original = textContent
  1080. let newContent = original
  1081. if (new RegExp("\\[.*]\\(", "ms").test(original)) {
  1082. newContent = replaceMarkdownImgLinks(newContent)
  1083. newContent = replaceMarkdownLinks(newContent)
  1084. }
  1085. if (/\[(img|url)]|\[url=/.test(textContent)) {
  1086. newContent = replaceBBCodeImgLinks(newContent)
  1087. newContent = replaceBBCodeLinks(newContent)
  1088. }
  1089. if (newContent === original) {
  1090. newContent = replaceTextLinks(original)
  1091. } else {
  1092. newContent = newContent.replaceAll(
  1093. new RegExp(
  1094. "(<a(?:\\s[^<>]*)?>.*?<\\/a>)|(<img(?:\\s[^<>]*)?\\/?>)|(.+?(?=(?:<a|<img))|.+$)",
  1095. "gims"
  1096. ),
  1097. (m, p1, p2) => (p1 || p2 ? m : replaceTextLinks(m))
  1098. )
  1099. }
  1100. if (newContent !== original) {
  1101. const span = createElement("span")
  1102. span.innerHTML = createHTML(newContent)
  1103. textNode.after(span)
  1104. textNode.remove()
  1105. return true
  1106. }
  1107. }
  1108. const parentTextContent = (_b = parentNode.textContent) != null ? _b : ""
  1109. if (
  1110. new RegExp("\\[.*]\\(", "ms").test(mergedText) &&
  1111. (parentTextContent.search(linkPattern2) >= 0 ||
  1112. $$("img", parentNode).length > 0)
  1113. ) {
  1114. const original = parentNode.innerHTML
  1115. const newContent = original
  1116. .replaceAll(new RegExp("\\[.*]\\([^[\\]()]+?\\)", "gims"), (m) =>
  1117. m
  1118. .replaceAll(
  1119. /<img[^<>]*\ssrc=['"]?(http[^'"]+)['"]?(\s[^<>]*)?>/gim,
  1120. "$1"
  1121. )
  1122. .replaceAll(
  1123. /\((?:\s|<br\/?>)*<a[^<>]*\shref=['"]?(http[^'"]+)['"]?(\s[^<>]*)?>\1<\/a>(?:\s|<br\/?>)*\)/gim,
  1124. "($1)"
  1125. )
  1126. )
  1127. .replaceAll(
  1128. new RegExp("\\[!\\[.*]\\([^()]+\\)]\\([^[\\]()]+?\\)", "gims"),
  1129. (m) =>
  1130. m
  1131. .replaceAll(
  1132. /<img[^<>]*\ssrc=['"]?(http[^'"]+)['"]?(\s[^<>]*)?>/gim,
  1133. "$1"
  1134. )
  1135. .replaceAll(
  1136. /\((?:\s|<br\/?>)*<a[^<>]*\shref=['"]?(http[^'"]+)['"]?(\s[^<>]*)?>\1<\/a>(?:\s|<br\/?>)*\)/gim,
  1137. "($1)"
  1138. )
  1139. )
  1140. if (newContent !== original) {
  1141. let newContent2 = replaceMarkdownImgLinks(newContent)
  1142. newContent2 = replaceMarkdownLinks(newContent2)
  1143. if (newContent2 !== newContent) {
  1144. parentNode.innerHTML = createHTML(newContent2)
  1145. return true
  1146. }
  1147. }
  1148. }
  1149. if (
  1150. /\[(img|url)]|\[url=/.test(textContent) &&
  1151. parentTextContent.search(/\[(img|url)[^\]]*]([^[\]]*?)\[\/\1]/) >= 0
  1152. ) {
  1153. const original = parentNode.innerHTML
  1154. let before = ""
  1155. let after = original
  1156. let count = 0
  1157. while (before !== after && count < 5) {
  1158. count++
  1159. before = after
  1160. after = before.replaceAll(
  1161. /\[(img|url)[^\]]*]([^[\]]+?)\[\/\1]/gim,
  1162. (m, p1) => {
  1163. let tagsRemoved
  1164. let converted
  1165. if (p1 === "img") {
  1166. tagsRemoved = m
  1167. .replaceAll(
  1168. /<img[^<>]*\ssrc=['"]?(http[^'"]+)['"]?(\s[^<>]*)?>/gim,
  1169. "$1"
  1170. )
  1171. .replaceAll(
  1172. /\[img](?:\s|<br\/?>)*<a[^<>]*\shref=['"]?(http[^'"]+)['"]?(\s[^<>]*)?>\1<\/a>(?:\s|<br\/?>)*\[\/img]/gim,
  1173. "[img]$1[/img]"
  1174. )
  1175. converted = replaceBBCodeImgLinks(tagsRemoved)
  1176. } else {
  1177. tagsRemoved = m
  1178. .replaceAll(
  1179. /\[url](?:\s|<br\/?>)*<a[^<>]*\shref=['"]?(http[^'"]+)['"]?(\s[^<>]*)?>\1<\/a>(?:\s|<br\/?>)*\[\/url]/gim,
  1180. "[url]$1[/url]"
  1181. )
  1182. .replaceAll(
  1183. /\[url=<a[^<>]*\shref=['"]?(http[^'"]+)['"]?(\s[^<>]*)?>\1<\/a>]/gim,
  1184. "[url=$1]"
  1185. )
  1186. converted = replaceBBCodeLinks(tagsRemoved)
  1187. }
  1188. return converted === tagsRemoved ? m : converted
  1189. }
  1190. )
  1191. }
  1192. const newContent = after
  1193. if (newContent !== original) {
  1194. parentNode.innerHTML = createHTML(newContent)
  1195. return true
  1196. }
  1197. }
  1198. }
  1199. var fixAnchorTag = (anchorElement) => {
  1200. var _a
  1201. const href = anchorElement.href
  1202. const textContent = (_a = anchorElement.textContent) != null ? _a : ""
  1203. const nextSibling = anchorElement.nextSibling
  1204. if (
  1205. anchorElement.childElementCount === 0 &&
  1206. href.includes(")") &&
  1207. textContent.includes(")")
  1208. ) {
  1209. const index = textContent.indexOf(")")
  1210. const removed = textContent.slice(Math.max(0, index))
  1211. anchorElement.textContent = textContent.slice(0, Math.max(0, index))
  1212. anchorElement.href = anchorElement.href.slice(
  1213. 0,
  1214. Math.max(0, href.indexOf(")"))
  1215. )
  1216. if (nextSibling && nextSibling.nodeType === 3) {
  1217. nextSibling.textContent = removed + nextSibling.textContent
  1218. } else {
  1219. anchorElement.after(doc.createTextNode(removed))
  1220. }
  1221. }
  1222. }
  1223. var scanAndConvertChildNodes = (parentNode) => {
  1224. if (
  1225. !parentNode ||
  1226. parentNode.nodeType === 8 ||
  1227. !parentNode.tagName ||
  1228. ignoredTags.has(parentNode.tagName.toUpperCase())
  1229. ) {
  1230. if (parentNode.tagName === "A") {
  1231. fixAnchorTag(parentNode)
  1232. }
  1233. return
  1234. }
  1235. let previousText = ""
  1236. for (const child of parentNode.childNodes) {
  1237. try {
  1238. if (child.nodeName === "#text") {
  1239. if (textToLink(child, previousText)) {
  1240. scanAndConvertChildNodes(parentNode)
  1241. break
  1242. }
  1243. previousText += child.textContent
  1244. } else if (child.nodeName === "BR") {
  1245. previousText += "\n"
  1246. } else {
  1247. previousText = ""
  1248. scanAndConvertChildNodes(child)
  1249. }
  1250. } catch (error) {
  1251. console.error(error)
  1252. }
  1253. }
  1254. }
  1255. var origin = location.origin
  1256. var host = location.host
  1257. var config = {
  1258. run_at: "document_start",
  1259. }
  1260. var settingsTable2 = {
  1261. enable: {
  1262. title: "Enable",
  1263. defaultValue: true,
  1264. },
  1265. ["enableCurrentSite_".concat(host)]: {
  1266. title: "Enable current site",
  1267. defaultValue: true,
  1268. },
  1269. ["enableCustomRulesForCurrentSite_".concat(host)]: {
  1270. title: "Enable custom rules for the current site",
  1271. defaultValue: false,
  1272. },
  1273. ["customRulesForCurrentSite_".concat(host)]: {
  1274. title: "Enable custom rules for the current site",
  1275. defaultValue: "",
  1276. placeholder:
  1277. "/* Custom rules for internal URLs, matching URLs will be opened in new tabs */",
  1278. type: "textarea",
  1279. group: 2,
  1280. },
  1281. customRulesTip: {
  1282. title: "Examples",
  1283. type: "tip",
  1284. tipContent:
  1285. "<p>Custom rules for internal URLs, matching URLs will be opened in new tabs</p>\n <p>\n - One line per url pattern<br>\n - All URLs contains '/posts' or '/users/'<br>\n <pre>/posts/\n/users/</pre>\n\n - Regex is supported<br>\n <pre>^/(posts|members)/d+</pre>\n\n - '*' for all URLs\n </p>",
  1286. group: 2,
  1287. },
  1288. ["enableLinkToImgForCurrentSite_".concat(host)]: {
  1289. title: "Enable converting image links to image tags for the current site",
  1290. defaultValue: Boolean(/v2ex\.com|localhost/.test(host)),
  1291. group: 3,
  1292. },
  1293. eraseLinks: {
  1294. title: "Erase Links",
  1295. type: "action",
  1296. async onclick() {
  1297. hideSettings()
  1298. eraseLinks()
  1299. },
  1300. group: 4,
  1301. },
  1302. restoreLinks: {
  1303. title: "Restore Links",
  1304. type: "action",
  1305. async onclick() {
  1306. hideSettings()
  1307. restoreLinks()
  1308. },
  1309. group: 4,
  1310. },
  1311. }
  1312. function registerMenuCommands() {
  1313. registerMenuCommand("\u2699\uFE0F \u8BBE\u7F6E", showSettings, "o")
  1314. }
  1315. var getWithoutOrigin = (url) => url.replace(/(^https?:\/\/[^/]+)/, "")
  1316. var shouldOpenInNewTab = (element) => {
  1317. var _a
  1318. const url = element.href
  1319. if (
  1320. !url ||
  1321. !/^https?:\/\//.test(url) ||
  1322. ((_a = element.getAttribute("href")) == null
  1323. ? void 0
  1324. : _a.startsWith("#"))
  1325. ) {
  1326. return false
  1327. }
  1328. if (element.origin !== origin) {
  1329. return true
  1330. }
  1331. if (getSettingsValue("enableCustomRulesForCurrentSite_".concat(host))) {
  1332. const rules2 = (
  1333. getSettingsValue("customRulesForCurrentSite_".concat(host)) || ""
  1334. ).split("\n")
  1335. if (rules2.includes("*")) {
  1336. return true
  1337. }
  1338. const hrefWithoutOrigin = getWithoutOrigin(url)
  1339. for (let rule of rules2) {
  1340. rule = rule.trim()
  1341. if (rule.length === 0) {
  1342. continue
  1343. }
  1344. try {
  1345. const regexp = new RegExp(rule)
  1346. if (regexp.test(hrefWithoutOrigin)) {
  1347. return true
  1348. }
  1349. } catch (error) {
  1350. console.log(error.message)
  1351. if (hrefWithoutOrigin.includes(rule)) {
  1352. return true
  1353. }
  1354. }
  1355. }
  1356. }
  1357. }
  1358. var setAttributeAsOpenInNewTab = (element) => {
  1359. if (shouldOpenInNewTab(element)) {
  1360. setAttribute(element, "target", "_blank")
  1361. addAttribute(element, "rel", "noopener")
  1362. }
  1363. }
  1364. async function main() {
  1365. await initSettings({
  1366. id: "links-helper",
  1367. title: "\u{1F517} Links Helper",
  1368. footer:
  1369. '\n <p>After change settings, reload the page to take effect</p>\n <p>\n <a href="https://github.com/utags/links-helper/issues" target="_blank">\n Report and Issue...\n </a></p>\n <p>Made with \u2764\uFE0F by\n <a href="https://www.pipecraft.net/" target="_blank">\n Pipecraft\n </a></p>',
  1370. settingsTable: settingsTable2,
  1371. onViewUpdate(settingsMainView) {
  1372. const group2 = $(".option_groups:nth-of-type(2)", settingsMainView)
  1373. if (group2) {
  1374. group2.style.display = getSettingsValue(
  1375. "enableCustomRulesForCurrentSite_".concat(host)
  1376. )
  1377. ? "block"
  1378. : "none"
  1379. }
  1380. },
  1381. })
  1382. registerMenuCommands()
  1383. if (
  1384. !getSettingsValue("enable") ||
  1385. !getSettingsValue("enableCurrentSite_".concat(host))
  1386. ) {
  1387. return
  1388. }
  1389. addStyle2(content_default)
  1390. addEventListener(
  1391. doc,
  1392. "click",
  1393. (event) => {
  1394. let anchorElement = event.target
  1395. while (anchorElement && anchorElement.tagName !== "A") {
  1396. anchorElement = anchorElement.parentNode
  1397. }
  1398. if (anchorElement) {
  1399. setAttributeAsOpenInNewTab(anchorElement)
  1400. if (getAttribute(anchorElement, "target") === "_blank") {
  1401. event.stopImmediatePropagation()
  1402. event.stopPropagation()
  1403. }
  1404. }
  1405. },
  1406. true
  1407. )
  1408. const scanAnchors = () => {
  1409. for (const element of $$("a")) {
  1410. if (element.__links_helper_scaned) {
  1411. continue
  1412. }
  1413. element.__links_helper_scaned = 1
  1414. try {
  1415. setAttributeAsOpenInNewTab(element)
  1416. } catch (error) {
  1417. console.error(error)
  1418. }
  1419. if (getSettingsValue("enableLinkToImgForCurrentSite_".concat(host))) {
  1420. try {
  1421. linkToImg(element)
  1422. } catch (error) {
  1423. console.error(error)
  1424. }
  1425. }
  1426. }
  1427. }
  1428. const scanNodes = throttle(() => {
  1429. scanAndConvertChildNodes(doc.body)
  1430. scanAnchors()
  1431. bindOnError()
  1432. }, 500)
  1433. const observer = new MutationObserver((mutationsList) => {
  1434. scanNodes()
  1435. })
  1436. const startObserver = () => {
  1437. observer.observe(doc.body, {
  1438. childList: true,
  1439. subtree: true,
  1440. characterData: true,
  1441. })
  1442. }
  1443. runWhenBodyExists(() => {
  1444. startObserver()
  1445. scanAndConvertChildNodes(doc.body)
  1446. })
  1447. scanAnchors()
  1448. }
  1449. main()
  1450. })()