🔗 链接助手

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

当前为 2023-05-10 提交的版本,查看 最新版本

  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.3.2
  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
  9. // @description:zh-CN 支持所有网站在新标签页中打开第三方网站链接(外链),在新标签页中打开符合指定规则的本站链接,解析文本链接为超链接,微信公众号文本转可点击的超链接,图片链接转图片标签,解析 Markdown 格式链接与图片标签
  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_addElement
  17. // @grant GM_addStyle
  18. // @grant GM.registerMenuCommand
  19. // @grant GM.getValue
  20. // @grant GM.setValue
  21. // @grant GM_addValueChangeListener
  22. // @grant GM_removeValueChangeListener
  23. // ==/UserScript==
  24. //
  25. //// Recent Updates
  26. //// - 0.3.2 2023.05.10
  27. //// - Parse Markdown style links and image tags
  28. //// - 0.3.0 2023.05.10
  29. //// - Convert image links to image tags
  30. //// - 0.2.0 2023.05.09
  31. //// - Convert text to hyperlinks
  32. //// - Fix opening internal links in a new tab in SPA apps
  33. //// - 0.1.3 2023.05.08
  34. //// - Fix compatibility issues on Violentmonkey, Greasemonkey(Firefox), Userscripts(Safari)
  35. //// - 0.1.1 2023.04.23
  36. //// - Change to run_at: document_start
  37. //// - 0.1.0 2023.04.23
  38. //// - Setting for url rules, open links matching the specified rules in a new tab
  39. //// - 0.0.2 2023.04.22
  40. //// - Add settings menu
  41. //// - Enable/Disable userscript
  42. //// - Enable/Disable current site
  43. ////
  44. ;(() => {
  45. "use strict"
  46. var doc = document
  47. var $ = (selectors, element) => (element || doc).querySelector(selectors)
  48. var $$ = (selectors, element) => [
  49. ...(element || doc).querySelectorAll(selectors),
  50. ]
  51. var createElement = (tagName, attributes) =>
  52. setAttributes(doc.createElement(tagName), attributes)
  53. var addElement = (parentNode, tagName, attributes) => {
  54. if (!parentNode) {
  55. return
  56. }
  57. if (typeof parentNode === "string") {
  58. attributes = tagName
  59. tagName = parentNode
  60. parentNode = doc.head
  61. }
  62. if (typeof tagName === "string") {
  63. const element = createElement(tagName, attributes)
  64. parentNode.append(element)
  65. return element
  66. }
  67. setAttributes(tagName, attributes)
  68. parentNode.append(tagName)
  69. return tagName
  70. }
  71. var addStyle = (styleText) => {
  72. const element = createElement("style", { textContent: styleText })
  73. doc.head.append(element)
  74. return element
  75. }
  76. var addEventListener = (element, type, listener, options) => {
  77. if (!element) {
  78. return
  79. }
  80. if (typeof type === "object") {
  81. for (const type1 in type) {
  82. if (Object.hasOwn(type, type1)) {
  83. element.addEventListener(type1, type[type1])
  84. }
  85. }
  86. } else if (typeof type === "string" && typeof listener === "function") {
  87. element.addEventListener(type, listener, options)
  88. }
  89. }
  90. var removeEventListener = (element, type, listener, options) => {
  91. if (!element) {
  92. return
  93. }
  94. if (typeof type === "object") {
  95. for (const type1 in type) {
  96. if (Object.hasOwn(type, type1)) {
  97. element.removeEventListener(type1, type[type1])
  98. }
  99. }
  100. } else if (typeof type === "string" && typeof listener === "function") {
  101. element.removeEventListener(type, listener, options)
  102. }
  103. }
  104. var getAttribute = (element, name) =>
  105. element ? element.getAttribute(name) : null
  106. var setAttribute = (element, name, value) =>
  107. element ? element.setAttribute(name, value) : void 0
  108. var setAttributes = (element, attributes) => {
  109. if (element && attributes) {
  110. for (const name in attributes) {
  111. if (Object.hasOwn(attributes, name)) {
  112. const value = attributes[name]
  113. if (value === void 0) {
  114. continue
  115. }
  116. if (/^(value|textContent|innerText|innerHTML)$/.test(name)) {
  117. element[name] = value
  118. } else if (name === "style") {
  119. setStyle(element, value, true)
  120. } else if (/on\w+/.test(name)) {
  121. const type = name.slice(2)
  122. addEventListener(element, type, value)
  123. } else {
  124. setAttribute(element, name, value)
  125. }
  126. }
  127. }
  128. }
  129. return element
  130. }
  131. var addAttribute = (element, name, value) => {
  132. const orgValue = getAttribute(element, name)
  133. if (!orgValue) {
  134. setAttribute(element, name, value)
  135. } else if (!orgValue.includes(value)) {
  136. setAttribute(element, name, orgValue + " " + value)
  137. }
  138. }
  139. var setStyle = (element, values, overwrite) => {
  140. if (!element) {
  141. return
  142. }
  143. const style = element.style
  144. if (typeof values === "string") {
  145. style.cssText = overwrite ? values : style.cssText + ";" + values
  146. return
  147. }
  148. if (overwrite) {
  149. style.cssText = ""
  150. }
  151. for (const key in values) {
  152. if (Object.hasOwn(values, key)) {
  153. style[key] = values[key].replace("!important", "")
  154. }
  155. }
  156. }
  157. var throttle = (func, interval) => {
  158. let timeoutId = null
  159. let next = false
  160. const handler = (...args) => {
  161. if (timeoutId) {
  162. next = true
  163. } else {
  164. func.apply(void 0, args)
  165. timeoutId = setTimeout(() => {
  166. timeoutId = null
  167. if (next) {
  168. next = false
  169. handler()
  170. }
  171. }, interval)
  172. }
  173. }
  174. return handler
  175. }
  176. if (typeof Object.hasOwn !== "function") {
  177. Object.hasOwn = (instance, prop) =>
  178. Object.prototype.hasOwnProperty.call(instance, prop)
  179. }
  180. var addElement2 =
  181. typeof GM_addElement === "function"
  182. ? (parentNode, tagName, attributes) => {
  183. if (!parentNode) {
  184. return
  185. }
  186. if (typeof parentNode === "string") {
  187. attributes = tagName
  188. tagName = parentNode
  189. parentNode = doc.head
  190. }
  191. if (typeof tagName === "string") {
  192. const element = GM_addElement(tagName)
  193. setAttributes(element, attributes)
  194. parentNode.append(element)
  195. return element
  196. }
  197. setAttributes(tagName, attributes)
  198. parentNode.append(tagName)
  199. return tagName
  200. }
  201. : addElement
  202. var addStyle2 =
  203. typeof GM_addStyle === "function"
  204. ? (styleText) => GM_addStyle(styleText)
  205. : addStyle
  206. var registerMenuCommand = (name, callback, accessKey) => {
  207. if (window !== top) {
  208. return
  209. }
  210. if (typeof GM.registerMenuCommand !== "function") {
  211. console.warn("Do not support GM.registerMenuCommand!")
  212. return
  213. }
  214. GM.registerMenuCommand(name, callback, accessKey)
  215. }
  216. var getValue = async (key) => {
  217. const value = await GM.getValue(key)
  218. return value && value !== "undefined" ? JSON.parse(value) : void 0
  219. }
  220. var setValue = async (key, value) => {
  221. if (value !== void 0) GM.setValue(key, JSON.stringify(value))
  222. }
  223. var addValueChangeListener = (key, func) => {
  224. if (typeof GM_addValueChangeListener !== "function") {
  225. console.warn("Do not support GM_addValueChangeListener!")
  226. return () => void 0
  227. }
  228. const listenerId = GM_addValueChangeListener(key, func)
  229. return () => {
  230. GM_removeValueChangeListener(listenerId)
  231. }
  232. }
  233. var style_default =
  234. "#browser_extension_settings{--browser-extension-settings-background-color: #f3f3f3;--browser-extension-settings-text-color: #444444;position:fixed;top:10px;right:30px;min-width:250px;max-height:90%;overflow-y:auto;overflow-x:hidden;display:none;box-sizing:border-box;padding:10px 15px;background-color:var(--browser-extension-settings-background-color);color:var(--browser-extension-settings-text-color);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 h2{text-align:center;margin:5px 0 0;font-size:18px;font-weight:600;border:none}#browser_extension_settings 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 footer a{color:#217dfc;text-decoration:none;padding:0}#browser_extension_settings footer p{text-align:center;padding:0;margin:2px;line-height:13px}#browser_extension_settings .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 .option_groups .action{font-size:14px;border-top:1px solid #ccc;padding:6px 0 6px 0;color:#217dfc;cursor:pointer}#browser_extension_settings .option_groups textarea{margin:10px 0 10px 0;height:100px;width:100%;border:1px solid #a9a9a9;border-radius:4px;box-sizing:border-box}#browser_extension_settings .switch_option{display:flex;justify-content:space-between;align-items:center;border-top:1px solid #ccc;padding:6px 0 6px 0;font-size:14px}#browser_extension_settings .switch_option:first-of-type,#browser_extension_settings .option_groups .action:first-of-type{border-top:none}#browser_extension_settings .switch_option>span{margin-right:10px}#browser_extension_settings .option_groups .tip{position:relative;margin:0;padding:0 15px 0 0;border:none;max-width:none;font-size:14px}#browser_extension_settings .option_groups .tip .tip_anchor{cursor:help;text-decoration:underline}#browser_extension_settings .option_groups .tip .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 .option_groups .tip .tip_anchor:hover+.tip_content,#browser_extension_settings .option_groups .tip .tip_content:hover{display:block}#browser_extension_settings .option_groups .tip p,#browser_extension_settings .option_groups .tip pre{margin:revert;padding:revert}#browser_extension_settings .option_groups .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 .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}#browser_extension_settings input[type=checkbox]{opacity:0;width:0;height:0;position:absolute}#browser_extension_settings .switch{width:100%;height:100%;display:block;background-color:var(--color-off);border-radius:calc(var(--button-height)/2);cursor:pointer;transition:all .2s ease-out}#browser_extension_settings .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 input[type=checkbox]:checked+.switch{background-color:var(--color-on)}#browser_extension_settings input[type=checkbox]:checked+.switch .slider{left:calc(var(--button-width) - var(--toggle-diameter) - 2px)}"
  235. function createSwitch(options = {}) {
  236. const container = createElement("label", { class: "container" })
  237. const checkbox = createElement(
  238. "input",
  239. options.checked ? { type: "checkbox", checked: "" } : { type: "checkbox" }
  240. )
  241. addElement2(container, checkbox)
  242. const switchElm = createElement("span", { class: "switch" })
  243. addElement2(switchElm, "span", { class: "slider" })
  244. addElement2(container, switchElm)
  245. if (options.onchange) {
  246. addEventListener(checkbox, "change", options.onchange)
  247. }
  248. return container
  249. }
  250. function createSwitchOption(text, options) {
  251. const div = createElement("div", { class: "switch_option" })
  252. addElement2(div, "span", { textContent: text })
  253. div.append(createSwitch(options))
  254. return div
  255. }
  256. var settingsElementId =
  257. "browser_extension_settings_" + String(Math.round(Math.random() * 1e4))
  258. var getSettingsElement = () => $("#" + settingsElementId)
  259. var getSettingsStyle = () =>
  260. style_default.replace(/browser_extension_settings/gm, settingsElementId)
  261. var storageKey = "settings"
  262. var settingsOptions = {}
  263. var settingsTable = {}
  264. var settings = {}
  265. async function getSettings() {
  266. var _a
  267. return (_a = await getValue(storageKey)) != null ? _a : {}
  268. }
  269. async function saveSattingsValue(key, value) {
  270. const settings2 = await getSettings()
  271. settings2[key] =
  272. settingsTable[key] && settingsTable[key].defaultValue === value
  273. ? void 0
  274. : value
  275. await setValue(storageKey, settings2)
  276. }
  277. function getSettingsValue(key) {
  278. var _a
  279. return Object.hasOwn(settings, key)
  280. ? settings[key]
  281. : (_a = settingsTable[key]) == null
  282. ? void 0
  283. : _a.defaultValue
  284. }
  285. var modalHandler = (event) => {
  286. let target = event.target
  287. const settingsLayer = getSettingsElement()
  288. if (settingsLayer) {
  289. while (target !== settingsLayer && target) {
  290. target = target.parentNode
  291. }
  292. if (target === settingsLayer) {
  293. return
  294. }
  295. settingsLayer.style.display = "none"
  296. }
  297. removeEventListener(document, "click", modalHandler)
  298. }
  299. async function updateOptions() {
  300. if (!getSettingsElement()) {
  301. return
  302. }
  303. for (const key in settingsTable) {
  304. if (Object.hasOwn(settingsTable, key)) {
  305. const checkbox = $(
  306. `#${settingsElementId} .option_groups .switch_option[data-key="${key}"] input`
  307. )
  308. if (checkbox) {
  309. checkbox.checked = getSettingsValue(key)
  310. }
  311. }
  312. }
  313. const host2 = location.host
  314. const group2 = $(`#${settingsElementId} .option_groups:nth-of-type(2)`)
  315. if (group2) {
  316. group2.style.display = getSettingsValue(
  317. `enableCustomRulesForCurrentSite_${host2}`
  318. )
  319. ? "block"
  320. : "none"
  321. }
  322. const customStyleValue = $(`#${settingsElementId} .option_groups textarea`)
  323. if (customStyleValue) {
  324. customStyleValue.value =
  325. settings[`customRulesForCurrentSite_${host2}`] || ""
  326. }
  327. }
  328. function createSettingsElement() {
  329. let settingsLayer = getSettingsElement()
  330. if (!settingsLayer) {
  331. addStyle2(getSettingsStyle())
  332. settingsLayer = addElement2(document.body, "div", {
  333. id: settingsElementId,
  334. })
  335. if (settingsOptions.title) {
  336. addElement2(settingsLayer, "h2", { textContent: settingsOptions.title })
  337. }
  338. const options = addElement2(settingsLayer, "div", {
  339. class: "option_groups",
  340. })
  341. for (const key in settingsTable) {
  342. if (Object.hasOwn(settingsTable, key)) {
  343. const item = settingsTable[key]
  344. if (!item.type || item.type === "switch") {
  345. const switchOption = createSwitchOption(item.title, {
  346. async onchange(event) {
  347. await saveSattingsValue(key, event.target.checked)
  348. },
  349. })
  350. switchOption.dataset.key = key
  351. addElement2(options, switchOption)
  352. }
  353. }
  354. }
  355. const options2 = addElement2(settingsLayer, "div", {
  356. class: "option_groups",
  357. })
  358. let timeoutId
  359. addElement2(options2, "textarea", {
  360. placeholder: `/* Custom rules for internal URLs, matching URLs will be opened in new tabs */`,
  361. onkeyup(event) {
  362. if (timeoutId) {
  363. clearTimeout(timeoutId)
  364. timeoutId = null
  365. }
  366. timeoutId = setTimeout(async () => {
  367. const host2 = location.host
  368. await saveSattingsValue(
  369. `customRulesForCurrentSite_${host2}`,
  370. event.target.value.trim()
  371. )
  372. }, 100)
  373. },
  374. })
  375. const tip = addElement2(options2, "div", {
  376. class: "tip",
  377. })
  378. addElement2(tip, "a", {
  379. class: "tip_anchor",
  380. textContent: "Examples",
  381. })
  382. const tipContent = addElement2(tip, "div", {
  383. class: "tip_content",
  384. innerHTML: `<p>Custom rules for internal URLs, matching URLs will be opened in new tabs</p>
  385. <p>
  386. - One line per url pattern<br>
  387. - All URLs contains '/posts' or '/users/'<br>
  388. <pre>/posts/
  389. /users/</pre>
  390. - Regex is supported<br>
  391. <pre>^/(posts|members)/d+</pre>
  392. - '*' for all URLs
  393. </p>`,
  394. })
  395. if (settingsOptions.footer) {
  396. const footer = addElement2(settingsLayer, "footer")
  397. footer.innerHTML =
  398. typeof settingsOptions.footer === "string"
  399. ? settingsOptions.footer
  400. : `<p>Made with \u2764\uFE0F by
  401. <a href="https://www.pipecraft.net/" target="_blank">
  402. Pipecraft
  403. </a></p>`
  404. }
  405. }
  406. return settingsLayer
  407. }
  408. async function showSettings() {
  409. const settingsLayer = createSettingsElement()
  410. await updateOptions()
  411. settingsLayer.style.display = "block"
  412. addEventListener(document, "click", modalHandler)
  413. }
  414. var initSettings = async (options) => {
  415. settingsOptions = options
  416. settingsTable = options.settingsTable || {}
  417. addValueChangeListener(storageKey, async () => {
  418. settings = await getSettings()
  419. await updateOptions()
  420. })
  421. settings = await getSettings()
  422. }
  423. var image_url_default =
  424. '{\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'
  425. var rules = JSON.parse(image_url_default)
  426. var cachedRules = {}
  427. var getHostname = (url) => (/https?:\/\/([^/]+)/.exec(url) || [])[1]
  428. var processRule = (rule, href) => {
  429. var _a
  430. let pattern
  431. let replacement
  432. const cachedRule = cachedRules[rule]
  433. try {
  434. if (cachedRule) {
  435. pattern = cachedRule.pattern
  436. replacement = cachedRule.replacement
  437. } else {
  438. const result = rule.replace(/ #.*/, "").split("->")
  439. const patternString = result[0].trim()
  440. pattern = new RegExp(
  441. patternString.startsWith("http")
  442. ? "^" + patternString
  443. : patternString,
  444. "i"
  445. )
  446. replacement = (_a = result[1]) == null ? void 0 : _a.trim()
  447. cachedRules[rule] = { pattern, replacement }
  448. }
  449. if (pattern.test(href)) {
  450. return replacement ? href.replace(pattern, replacement) : href
  451. }
  452. } catch (error) {
  453. console.error(error)
  454. }
  455. }
  456. var convertImgUrl = (href) => {
  457. if (!href) {
  458. return
  459. }
  460. const hostname = getHostname(href)
  461. if (Object.hasOwn(rules, hostname)) {
  462. for (const rule of rules[hostname]) {
  463. const newHref = processRule(rule, href)
  464. if (newHref) {
  465. return newHref
  466. }
  467. }
  468. }
  469. }
  470. var createImgTagString = (src, text) =>
  471. `<img src="${src}" title="${text || "image"}" alt="${
  472. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
  473. text || "image"
  474. }" role="img" style="max-width: 100% !important; vertical-align: bottom;" loading="lazy" referrerpolicy="no-referrer"/>`
  475. var anchorElementToImgElement = (anchor, href, text) => {
  476. anchor.innerHTML = createImgTagString(href, text)
  477. setAttribute(anchor, "target", "_blank")
  478. addAttribute(anchor, "rel", "noopener")
  479. addAttribute(anchor, "rel", "noreferrer")
  480. setAttributes(anchor.childNodes[0], {
  481. onerror(event) {
  482. const img = event.srcElement
  483. img.outerHTML =
  484. text + '<i class="lh_img_load_failed"> (failed to load)</i>'
  485. },
  486. })
  487. }
  488. var linkToImg = (anchor) => {
  489. if (
  490. !anchor ||
  491. anchor.childElementCount !== 0 ||
  492. anchor.childNodes[0].nodeType !== 3
  493. ) {
  494. return
  495. }
  496. const href = anchor.href
  497. const text = anchor.textContent
  498. const newHref = convertImgUrl(href)
  499. if (newHref) {
  500. anchorElementToImgElement(anchor, newHref, text)
  501. } else if (
  502. /^https:[^?]+\.(?:jpg|jpeg|jpe|bmp|png|gif|webp|ico|svg)/i.test(href)
  503. ) {
  504. anchorElementToImgElement(anchor, href, text)
  505. }
  506. }
  507. var ignoredTags = /* @__PURE__ */ new Set([
  508. "A",
  509. "BUTTON",
  510. "SVG",
  511. "PATH",
  512. "G",
  513. "SCRIPT",
  514. "STYLE",
  515. "TEXTAREA",
  516. "CODE",
  517. "PRE",
  518. "TEMPLATE",
  519. "NOSCRIPT",
  520. "TITLE",
  521. ])
  522. var urlPattern =
  523. "\\b((?:https?:\\/\\/(?:[\\w-.]+\\.[a-z]{2,15}|localhost|(?:\\d{1,3}\\.){3}\\d{1,3}))(?::\\d+)?(?:\\/[\\w-/%.~+:!@=&?#]*)?)"
  524. var linkPattern1 = new RegExp(
  525. `!\\[([^\\[\\]]*)\\]\\(\\s*${urlPattern}\\)`,
  526. "gim"
  527. )
  528. var linkPattern2 = new RegExp(
  529. `\\[([^\\[\\]]*)\\]\\(\\s*${urlPattern}\\)`,
  530. "gim"
  531. )
  532. var linkPattern3 = new RegExp(urlPattern, "gim")
  533. var replaceMarkdownImgLinks = (text) => {
  534. if (text.search(linkPattern1) >= 0) {
  535. text = text.replace(linkPattern1, (m, p1, p2) => {
  536. return createImgTagString(convertImgUrl(p2) || p2, p1)
  537. })
  538. }
  539. return text
  540. }
  541. var replaceMarkdownLinks = (text) => {
  542. if (text.search(linkPattern2) >= 0) {
  543. text = text.replace(linkPattern2, (m, p1, p2) => {
  544. return `<a href='${p2}'>${p1}</a>`
  545. })
  546. }
  547. return text
  548. }
  549. var replaceTextLinks = (text) => {
  550. if (text.search(linkPattern3) >= 0) {
  551. text = text.replace(linkPattern3, (m, p1) => {
  552. return `<a href='${p1}'>${p1}</a>`
  553. })
  554. }
  555. return text
  556. }
  557. var textToLink = (textNode) => {
  558. const textContent = textNode.textContent
  559. if (
  560. textNode.nodeName !== "#text" ||
  561. !textContent ||
  562. textContent.trim().length < 3
  563. ) {
  564. return
  565. }
  566. if (textContent.includes("://")) {
  567. const original = textContent
  568. let newContent = original
  569. if (/\[.*]\(/.test(original)) {
  570. newContent = replaceMarkdownImgLinks(newContent)
  571. newContent = replaceMarkdownLinks(newContent)
  572. }
  573. if (newContent === original) {
  574. newContent = replaceTextLinks(original)
  575. }
  576. if (newContent === original) {
  577. console.error(newContent)
  578. } else {
  579. const span = createElement("span")
  580. span.innerHTML = newContent
  581. textNode.after(span)
  582. textNode.remove()
  583. return true
  584. }
  585. }
  586. const parentNode = textNode.parentNode
  587. if (
  588. /\[.*]\(/.test(textContent) &&
  589. parentNode &&
  590. parentNode.textContent &&
  591. parentNode.textContent.search(linkPattern2) >= 0
  592. ) {
  593. const original = parentNode.innerHTML
  594. const newContent = original.replace(
  595. /\(\s*<a[^<>]*\shref=['"](http[^'"]+)['"]\s[^<>]*>\1<\/a>\)/gim,
  596. "($1)"
  597. )
  598. if (newContent !== original) {
  599. let newContent2 = replaceMarkdownImgLinks(newContent)
  600. newContent2 = replaceMarkdownLinks(newContent2)
  601. if (newContent2 !== newContent) {
  602. parentNode.innerHTML = newContent2
  603. return true
  604. }
  605. }
  606. }
  607. }
  608. var scanAndConvertChildNodes = (parentNode) => {
  609. if (
  610. !parentNode ||
  611. parentNode.nodeType === 8 ||
  612. !parentNode.tagName ||
  613. ignoredTags.has(parentNode.tagName.toUpperCase())
  614. ) {
  615. return
  616. }
  617. for (const child of parentNode.childNodes) {
  618. if (child.nodeName === "#text") {
  619. if (textToLink(child)) {
  620. scanAndConvertChildNodes(parentNode)
  621. break
  622. }
  623. } else {
  624. scanAndConvertChildNodes(child)
  625. }
  626. }
  627. }
  628. var origin = location.origin
  629. var host = location.host
  630. var config = {
  631. run_at: "document_start",
  632. }
  633. var settingsTable2 = {
  634. enable: {
  635. title: "Enable",
  636. defaultValue: true,
  637. },
  638. [`enableCurrentSite_${host}`]: {
  639. title: "Enable current site",
  640. defaultValue: true,
  641. },
  642. [`enableCustomRulesForCurrentSite_${host}`]: {
  643. title: "Enable custom rules for current site",
  644. defaultValue: false,
  645. },
  646. [`customRulesForCurrentSite_${host}`]: {
  647. title: "Enable custom rules for current site",
  648. defaultValue: "",
  649. type: "textarea",
  650. },
  651. }
  652. function registerMenuCommands() {
  653. registerMenuCommand("\u2699\uFE0F \u8BBE\u7F6E", showSettings, "o")
  654. }
  655. var getWithoutOrigin = (url) => url.replace(/(^https?:\/\/[^/]+)/, "")
  656. var shouldOpenInNewTab = (element) => {
  657. var _a
  658. const url = element.href
  659. if (
  660. !url ||
  661. !/^https?:\/\//.test(url) ||
  662. ((_a = element.getAttribute("href")) == null
  663. ? void 0
  664. : _a.startsWith("#"))
  665. ) {
  666. return false
  667. }
  668. if (element.origin !== origin) {
  669. return true
  670. }
  671. if (getSettingsValue(`enableCustomRulesForCurrentSite_${host}`)) {
  672. const rules2 = (
  673. getSettingsValue(`customRulesForCurrentSite_${host}`) || ""
  674. ).split("\n")
  675. if (rules2.includes("*")) {
  676. return true
  677. }
  678. const hrefWithoutOrigin = getWithoutOrigin(url)
  679. for (let rule of rules2) {
  680. rule = rule.trim()
  681. if (rule.length === 0) {
  682. continue
  683. }
  684. try {
  685. const regexp = new RegExp(rule)
  686. if (regexp.test(hrefWithoutOrigin)) {
  687. return true
  688. }
  689. } catch (error) {
  690. console.log(error.message)
  691. if (hrefWithoutOrigin.includes(rule)) {
  692. return true
  693. }
  694. }
  695. }
  696. }
  697. }
  698. var setAttributeAsOpenInNewTab = (element) => {
  699. if (shouldOpenInNewTab(element)) {
  700. setAttribute(element, "target", "_blank")
  701. addAttribute(element, "rel", "noopener")
  702. }
  703. }
  704. async function main() {
  705. await initSettings({
  706. title: "\u{1F517} Links Helper",
  707. footer: `
  708. <p>After change settings, reload the page to take effect</p>
  709. <p>
  710. <a href="https://github.com/utags/links-helper/issues" target="_blank">
  711. Report and Issue...
  712. </a></p>
  713. <p>Made with \u2764\uFE0F by
  714. <a href="https://www.pipecraft.net/" target="_blank">
  715. Pipecraft
  716. </a></p>`,
  717. settingsTable: settingsTable2,
  718. })
  719. registerMenuCommands()
  720. if (
  721. !getSettingsValue("enable") ||
  722. !getSettingsValue(`enableCurrentSite_${host}`)
  723. ) {
  724. return
  725. }
  726. addEventListener(
  727. document,
  728. "click",
  729. (event) => {
  730. let anchorElement = event.target
  731. while (anchorElement && anchorElement.tagName !== "A") {
  732. anchorElement = anchorElement.parentNode
  733. }
  734. if (anchorElement) {
  735. setAttributeAsOpenInNewTab(anchorElement)
  736. if (getAttribute(anchorElement, "target") === "_blank") {
  737. event.stopImmediatePropagation()
  738. event.stopPropagation()
  739. }
  740. }
  741. },
  742. true
  743. )
  744. const scanAnchors = () => {
  745. for (const element of $$("a")) {
  746. if (element.__links_helper_scaned) {
  747. continue
  748. }
  749. element.__links_helper_scaned = 1
  750. setAttributeAsOpenInNewTab(element)
  751. linkToImg(element)
  752. }
  753. }
  754. const scanNodes = throttle(() => {
  755. scanAndConvertChildNodes(doc.body)
  756. scanAnchors()
  757. }, 500)
  758. const observer = new MutationObserver(() => {
  759. scanNodes()
  760. })
  761. const startObserver = () => {
  762. observer.observe(doc.body, {
  763. childList: true,
  764. subtree: true,
  765. characterData: true,
  766. })
  767. }
  768. if (doc.body) {
  769. startObserver()
  770. scanAndConvertChildNodes(doc.body)
  771. } else {
  772. const intervalId = setInterval(() => {
  773. if (doc.body) {
  774. clearInterval(intervalId)
  775. startObserver()
  776. scanAndConvertChildNodes(doc.body)
  777. }
  778. }, 100)
  779. }
  780. scanAnchors()
  781. }
  782. main()
  783. })()