🔗 链接助手

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

目前为 2023-05-17 提交的版本。查看 最新版本

  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.5
  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_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.5 2023.05.17
  27. //// - Fix some edge cases
  28. //// - 0.3.4 2023.05.16
  29. //// - Parse BBCode style links and image tags
  30. //// - Update parsing links logic
  31. //// - 0.3.3 2023.05.11
  32. //// - Fix parse markdown style text
  33. //// - 0.3.2 2023.05.10
  34. //// - Parse Markdown style links and image tags
  35. //// - 0.3.0 2023.05.10
  36. //// - Convert image links to image tags
  37. //// - 0.2.0 2023.05.09
  38. //// - Convert text to hyperlinks
  39. //// - Fix opening internal links in a new tab in SPA apps
  40. //// - 0.1.3 2023.05.08
  41. //// - Fix compatibility issues on Violentmonkey, Greasemonkey(Firefox), Userscripts(Safari)
  42. //// - 0.1.1 2023.04.23
  43. //// - Change to run_at: document_start
  44. //// - 0.1.0 2023.04.23
  45. //// - Setting for url rules, open links matching the specified rules in a new tab
  46. //// - 0.0.2 2023.04.22
  47. //// - Add settings menu
  48. //// - Enable/Disable userscript
  49. //// - Enable/Disable current site
  50. ////
  51. ;(() => {
  52. "use strict"
  53. var doc = document
  54. var $ = (selectors, element) => (element || doc).querySelector(selectors)
  55. var $$ = (selectors, element) => [
  56. ...(element || doc).querySelectorAll(selectors),
  57. ]
  58. var createElement = (tagName, attributes) =>
  59. setAttributes(doc.createElement(tagName), attributes)
  60. var addElement = (parentNode, tagName, attributes) => {
  61. if (!parentNode) {
  62. return
  63. }
  64. if (typeof parentNode === "string") {
  65. attributes = tagName
  66. tagName = parentNode
  67. parentNode = doc.head
  68. }
  69. if (typeof tagName === "string") {
  70. const element = createElement(tagName, attributes)
  71. parentNode.append(element)
  72. return element
  73. }
  74. setAttributes(tagName, attributes)
  75. parentNode.append(tagName)
  76. return tagName
  77. }
  78. var addStyle = (styleText) => {
  79. const element = createElement("style", { textContent: styleText })
  80. doc.head.append(element)
  81. return element
  82. }
  83. var addEventListener = (element, type, listener, options) => {
  84. if (!element) {
  85. return
  86. }
  87. if (typeof type === "object") {
  88. for (const type1 in type) {
  89. if (Object.hasOwn(type, type1)) {
  90. element.addEventListener(type1, type[type1])
  91. }
  92. }
  93. } else if (typeof type === "string" && typeof listener === "function") {
  94. element.addEventListener(type, listener, options)
  95. }
  96. }
  97. var removeEventListener = (element, type, listener, options) => {
  98. if (!element) {
  99. return
  100. }
  101. if (typeof type === "object") {
  102. for (const type1 in type) {
  103. if (Object.hasOwn(type, type1)) {
  104. element.removeEventListener(type1, type[type1])
  105. }
  106. }
  107. } else if (typeof type === "string" && typeof listener === "function") {
  108. element.removeEventListener(type, listener, options)
  109. }
  110. }
  111. var getAttribute = (element, name) =>
  112. element ? element.getAttribute(name) : null
  113. var setAttribute = (element, name, value) =>
  114. element ? element.setAttribute(name, value) : void 0
  115. var setAttributes = (element, attributes) => {
  116. if (element && attributes) {
  117. for (const name in attributes) {
  118. if (Object.hasOwn(attributes, name)) {
  119. const value = attributes[name]
  120. if (value === void 0) {
  121. continue
  122. }
  123. if (/^(value|textContent|innerText|innerHTML)$/.test(name)) {
  124. element[name] = value
  125. } else if (name === "style") {
  126. setStyle(element, value, true)
  127. } else if (/on\w+/.test(name)) {
  128. const type = name.slice(2)
  129. addEventListener(element, type, value)
  130. } else {
  131. setAttribute(element, name, value)
  132. }
  133. }
  134. }
  135. }
  136. return element
  137. }
  138. var addAttribute = (element, name, value) => {
  139. const orgValue = getAttribute(element, name)
  140. if (!orgValue) {
  141. setAttribute(element, name, value)
  142. } else if (!orgValue.includes(value)) {
  143. setAttribute(element, name, orgValue + " " + value)
  144. }
  145. }
  146. var setStyle = (element, values, overwrite) => {
  147. if (!element) {
  148. return
  149. }
  150. const style = element.style
  151. if (typeof values === "string") {
  152. style.cssText = overwrite ? values : style.cssText + ";" + values
  153. return
  154. }
  155. if (overwrite) {
  156. style.cssText = ""
  157. }
  158. for (const key in values) {
  159. if (Object.hasOwn(values, key)) {
  160. style[key] = values[key].replace("!important", "")
  161. }
  162. }
  163. }
  164. var throttle = (func, interval) => {
  165. let timeoutId = null
  166. let next = false
  167. const handler = (...args) => {
  168. if (timeoutId) {
  169. next = true
  170. } else {
  171. func.apply(void 0, args)
  172. timeoutId = setTimeout(() => {
  173. timeoutId = null
  174. if (next) {
  175. next = false
  176. handler()
  177. }
  178. }, interval)
  179. }
  180. }
  181. return handler
  182. }
  183. if (typeof Object.hasOwn !== "function") {
  184. Object.hasOwn = (instance, prop) =>
  185. Object.prototype.hasOwnProperty.call(instance, prop)
  186. }
  187. var addElement2 =
  188. typeof GM_addElement === "function"
  189. ? (parentNode, tagName, attributes) => {
  190. if (!parentNode) {
  191. return
  192. }
  193. if (typeof parentNode === "string") {
  194. attributes = tagName
  195. tagName = parentNode
  196. parentNode = doc.head
  197. }
  198. if (typeof tagName === "string") {
  199. const element = GM_addElement(tagName)
  200. setAttributes(element, attributes)
  201. parentNode.append(element)
  202. return element
  203. }
  204. setAttributes(tagName, attributes)
  205. parentNode.append(tagName)
  206. return tagName
  207. }
  208. : addElement
  209. var addStyle2 =
  210. typeof GM_addStyle === "function"
  211. ? (styleText) => GM_addStyle(styleText)
  212. : addStyle
  213. var registerMenuCommand = (name, callback, accessKey) => {
  214. if (window !== top) {
  215. return
  216. }
  217. if (typeof GM.registerMenuCommand !== "function") {
  218. console.warn("Do not support GM.registerMenuCommand!")
  219. return
  220. }
  221. GM.registerMenuCommand(name, callback, accessKey)
  222. }
  223. var getValue = async (key) => {
  224. const value = await GM.getValue(key)
  225. return value && value !== "undefined" ? JSON.parse(value) : void 0
  226. }
  227. var setValue = async (key, value) => {
  228. if (value !== void 0) GM.setValue(key, JSON.stringify(value))
  229. }
  230. var addValueChangeListener = (key, func) => {
  231. if (typeof GM_addValueChangeListener !== "function") {
  232. console.warn("Do not support GM_addValueChangeListener!")
  233. return () => void 0
  234. }
  235. const listenerId = GM_addValueChangeListener(key, func)
  236. return () => {
  237. GM_removeValueChangeListener(listenerId)
  238. }
  239. }
  240. var style_default =
  241. "#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);border:none;cursor:pointer;transition:all .2s ease-out}#browser_extension_settings .switch::before{display:none}#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)}"
  242. function createSwitch(options = {}) {
  243. const container = createElement("label", { class: "container" })
  244. const checkbox = createElement(
  245. "input",
  246. options.checked ? { type: "checkbox", checked: "" } : { type: "checkbox" }
  247. )
  248. addElement2(container, checkbox)
  249. const switchElm = createElement("span", { class: "switch" })
  250. addElement2(switchElm, "span", { class: "slider" })
  251. addElement2(container, switchElm)
  252. if (options.onchange) {
  253. addEventListener(checkbox, "change", options.onchange)
  254. }
  255. return container
  256. }
  257. function createSwitchOption(text, options) {
  258. const div = createElement("div", { class: "switch_option" })
  259. addElement2(div, "span", { textContent: text })
  260. div.append(createSwitch(options))
  261. return div
  262. }
  263. var settingsElementId =
  264. "browser_extension_settings_" + String(Math.round(Math.random() * 1e4))
  265. var getSettingsElement = () => $("#" + settingsElementId)
  266. var getSettingsStyle = () =>
  267. style_default.replace(/browser_extension_settings/gm, settingsElementId)
  268. var storageKey = "settings"
  269. var settingsOptions = {}
  270. var settingsTable = {}
  271. var settings = {}
  272. async function getSettings() {
  273. var _a
  274. return (_a = await getValue(storageKey)) != null ? _a : {}
  275. }
  276. async function saveSattingsValue(key, value) {
  277. const settings2 = await getSettings()
  278. settings2[key] =
  279. settingsTable[key] && settingsTable[key].defaultValue === value
  280. ? void 0
  281. : value
  282. await setValue(storageKey, settings2)
  283. }
  284. function getSettingsValue(key) {
  285. var _a
  286. return Object.hasOwn(settings, key)
  287. ? settings[key]
  288. : (_a = settingsTable[key]) == null
  289. ? void 0
  290. : _a.defaultValue
  291. }
  292. var modalHandler = (event) => {
  293. let target = event.target
  294. const settingsLayer = getSettingsElement()
  295. if (settingsLayer) {
  296. while (target !== settingsLayer && target) {
  297. target = target.parentNode
  298. }
  299. if (target === settingsLayer) {
  300. return
  301. }
  302. settingsLayer.style.display = "none"
  303. }
  304. removeEventListener(document, "click", modalHandler)
  305. }
  306. async function updateOptions() {
  307. if (!getSettingsElement()) {
  308. return
  309. }
  310. for (const key in settingsTable) {
  311. if (Object.hasOwn(settingsTable, key)) {
  312. const checkbox = $(
  313. `#${settingsElementId} .option_groups .switch_option[data-key="${key}"] input`
  314. )
  315. if (checkbox) {
  316. checkbox.checked = getSettingsValue(key)
  317. }
  318. }
  319. }
  320. const host2 = location.host
  321. const group2 = $(`#${settingsElementId} .option_groups:nth-of-type(2)`)
  322. if (group2) {
  323. group2.style.display = getSettingsValue(
  324. `enableCustomRulesForCurrentSite_${host2}`
  325. )
  326. ? "block"
  327. : "none"
  328. }
  329. const customStyleValue = $(`#${settingsElementId} .option_groups textarea`)
  330. if (customStyleValue) {
  331. customStyleValue.value =
  332. settings[`customRulesForCurrentSite_${host2}`] || ""
  333. }
  334. }
  335. function createSettingsElement() {
  336. let settingsLayer = getSettingsElement()
  337. if (!settingsLayer) {
  338. addStyle2(getSettingsStyle())
  339. settingsLayer = addElement2(document.body, "div", {
  340. id: settingsElementId,
  341. })
  342. if (settingsOptions.title) {
  343. addElement2(settingsLayer, "h2", { textContent: settingsOptions.title })
  344. }
  345. const options = addElement2(settingsLayer, "div", {
  346. class: "option_groups",
  347. })
  348. for (const key in settingsTable) {
  349. if (Object.hasOwn(settingsTable, key)) {
  350. const item = settingsTable[key]
  351. if (!item.type || item.type === "switch") {
  352. const switchOption = createSwitchOption(item.title, {
  353. async onchange(event) {
  354. await saveSattingsValue(key, event.target.checked)
  355. },
  356. })
  357. switchOption.dataset.key = key
  358. addElement2(options, switchOption)
  359. }
  360. }
  361. }
  362. const options2 = addElement2(settingsLayer, "div", {
  363. class: "option_groups",
  364. })
  365. let timeoutId
  366. addElement2(options2, "textarea", {
  367. placeholder: `/* Custom rules for internal URLs, matching URLs will be opened in new tabs */`,
  368. onkeyup(event) {
  369. if (timeoutId) {
  370. clearTimeout(timeoutId)
  371. timeoutId = null
  372. }
  373. timeoutId = setTimeout(async () => {
  374. const host2 = location.host
  375. await saveSattingsValue(
  376. `customRulesForCurrentSite_${host2}`,
  377. event.target.value.trim()
  378. )
  379. }, 100)
  380. },
  381. })
  382. const tip = addElement2(options2, "div", {
  383. class: "tip",
  384. })
  385. addElement2(tip, "a", {
  386. class: "tip_anchor",
  387. textContent: "Examples",
  388. })
  389. const tipContent = addElement2(tip, "div", {
  390. class: "tip_content",
  391. innerHTML: `<p>Custom rules for internal URLs, matching URLs will be opened in new tabs</p>
  392. <p>
  393. - One line per url pattern<br>
  394. - All URLs contains '/posts' or '/users/'<br>
  395. <pre>/posts/
  396. /users/</pre>
  397. - Regex is supported<br>
  398. <pre>^/(posts|members)/d+</pre>
  399. - '*' for all URLs
  400. </p>`,
  401. })
  402. if (settingsOptions.footer) {
  403. const footer = addElement2(settingsLayer, "footer")
  404. footer.innerHTML =
  405. typeof settingsOptions.footer === "string"
  406. ? settingsOptions.footer
  407. : `<p>Made with \u2764\uFE0F by
  408. <a href="https://www.pipecraft.net/" target="_blank">
  409. Pipecraft
  410. </a></p>`
  411. }
  412. }
  413. return settingsLayer
  414. }
  415. async function showSettings() {
  416. const settingsLayer = createSettingsElement()
  417. await updateOptions()
  418. settingsLayer.style.display = "block"
  419. addEventListener(document, "click", modalHandler)
  420. }
  421. var initSettings = async (options) => {
  422. settingsOptions = options
  423. settingsTable = options.settingsTable || {}
  424. addValueChangeListener(storageKey, async () => {
  425. settings = await getSettings()
  426. await updateOptions()
  427. })
  428. settings = await getSettings()
  429. }
  430. var image_url_default =
  431. '{\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'
  432. var rules = JSON.parse(image_url_default)
  433. var cachedRules = {}
  434. var getHostname = (url) => (/https?:\/\/([^/]+)/.exec(url) || [])[1]
  435. var processRule = (rule, href) => {
  436. var _a
  437. let pattern
  438. let replacement
  439. const cachedRule = cachedRules[rule]
  440. try {
  441. if (cachedRule) {
  442. pattern = cachedRule.pattern
  443. replacement = cachedRule.replacement
  444. } else {
  445. const result = rule.replace(/ #.*/, "").split("->")
  446. const patternString = result[0].trim()
  447. pattern = new RegExp(
  448. patternString.startsWith("http")
  449. ? "^" + patternString
  450. : patternString,
  451. "i"
  452. )
  453. replacement = (_a = result[1]) == null ? void 0 : _a.trim()
  454. cachedRules[rule] = { pattern, replacement }
  455. }
  456. if (pattern.test(href)) {
  457. return replacement ? href.replace(pattern, replacement) : href
  458. }
  459. } catch (error) {
  460. console.error(error)
  461. }
  462. }
  463. var convertImgUrl = (href) => {
  464. if (!href) {
  465. return
  466. }
  467. const hostname = getHostname(href)
  468. if (Object.hasOwn(rules, hostname)) {
  469. for (const rule of rules[hostname]) {
  470. const newHref = processRule(rule, href)
  471. if (newHref) {
  472. return newHref
  473. }
  474. }
  475. }
  476. }
  477. var createImgTagString = (src, text) =>
  478. `<img src="${src}" title="${text || "image"}" alt="${
  479. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
  480. text || "image"
  481. }" role="img" style="max-width: 100% !important; vertical-align: bottom;" loading="lazy" referrerpolicy="no-referrer" rel="noreferrer" data-lh-status="1"/>`
  482. var bindOnError = () => {
  483. for (const element of $$('img[data-lh-status="1"]')) {
  484. setAttribute(element, "data-lh-status", "2")
  485. addEventListener(element, "error", (event) => {
  486. const img = event.target
  487. const anchor = img.parentElement
  488. img.outerHTML = getAttribute(img, "src")
  489. if ((anchor == null ? void 0 : anchor.tagName) === "A") {
  490. setStyle(anchor, "opacity: 50%;")
  491. setAttribute(anchor, "data-message", "failed to load image")
  492. }
  493. })
  494. }
  495. }
  496. var anchorElementToImgElement = (anchor, href, text) => {
  497. anchor.innerHTML = createImgTagString(href, text)
  498. setAttribute(anchor, "target", "_blank")
  499. addAttribute(anchor, "rel", "noopener")
  500. addAttribute(anchor, "rel", "noreferrer")
  501. }
  502. var linkToImg = (anchor) => {
  503. if (
  504. !anchor ||
  505. anchor.childElementCount !== 0 ||
  506. (anchor.childNodes[0] && anchor.childNodes[0].nodeType !== 3)
  507. ) {
  508. return
  509. }
  510. const href = anchor.href
  511. const text = anchor.textContent || href
  512. const newHref = convertImgUrl(href)
  513. if (newHref) {
  514. anchorElementToImgElement(anchor, newHref, text)
  515. } else if (
  516. /^https:[^?]+\.(?:jpg|jpeg|jpe|bmp|png|gif|webp|ico|svg)/i.test(href)
  517. ) {
  518. anchorElementToImgElement(anchor, href, text)
  519. }
  520. }
  521. var ignoredTags = /* @__PURE__ */ new Set([
  522. "A",
  523. "BUTTON",
  524. "SVG",
  525. "PATH",
  526. "G",
  527. "SCRIPT",
  528. "STYLE",
  529. "TEXTAREA",
  530. "CODE",
  531. "PRE",
  532. "TEMPLATE",
  533. "NOSCRIPT",
  534. "TITLE",
  535. ])
  536. var urlPattern =
  537. "\\b((?:https?:\\/\\/(?:[\\w-.]+\\.[a-z]{2,15}|localhost|(?:\\d{1,3}\\.){3}\\d{1,3}))(?::\\d+)?(?:\\/[\\w-/%.~+:;!@=&?#]*)?)"
  538. var linkPattern1 = new RegExp(
  539. `!\\[([^\\[\\]]*)\\]\\((?:\\s|<br/?>)*${urlPattern}(?:\\s|<br/?>)*\\)`,
  540. "gim"
  541. )
  542. var linkPattern2 = new RegExp(
  543. `\\[([^\\[\\]]*)\\]\\((?:\\s|<br/?>)*${urlPattern}(?:\\s|<br/?>)*\\)`,
  544. "gim"
  545. )
  546. var linkPattern3 = new RegExp(urlPattern, "gim")
  547. var linkPattern4 = new RegExp(
  548. `\\[img\\](?:\\s|<br/?>)*${urlPattern}(?:\\s|<br/?>)*\\[/img\\]`,
  549. "gim"
  550. )
  551. var linkPattern5 = new RegExp(
  552. `\\[url\\](?:\\s|<br/?>)*${urlPattern}(?:\\s|<br/?>)*\\[/url\\]`,
  553. "gim"
  554. )
  555. var linkPattern6 = new RegExp(
  556. `\\[url=${urlPattern}\\]([^\\[\\]]+)\\[/url\\]`,
  557. "gim"
  558. )
  559. var replaceMarkdownImgLinks = (text) => {
  560. if (text.search(linkPattern1) >= 0) {
  561. text = text.replace(linkPattern1, (m, p1, p2) => {
  562. return createImgTagString(convertImgUrl(p2) || p2, p1)
  563. })
  564. }
  565. return text
  566. }
  567. var replaceMarkdownLinks = (text) => {
  568. if (text.search(linkPattern2) >= 0) {
  569. text = text.replace(linkPattern2, (m, p1, p2) => {
  570. return `<a href="${p2}">${p1.replace(/<br>$/gi, "")}</a>`
  571. })
  572. }
  573. return text
  574. }
  575. var replaceTextLinks = (text) => {
  576. if (text.search(linkPattern3) >= 0) {
  577. text = text.replace(linkPattern3, (m, p1) => {
  578. return `<a href="${p1}">${p1}</a>`
  579. })
  580. }
  581. return text
  582. }
  583. var replaceBBCodeImgLinks = (text) => {
  584. if (text.search(linkPattern4) >= 0) {
  585. text = text.replace(linkPattern4, (m, p1) => {
  586. return createImgTagString(convertImgUrl(p1) || p1, p1)
  587. })
  588. }
  589. return text
  590. }
  591. var replaceBBCodeLinks = (text) => {
  592. if (text.search(linkPattern5) >= 0) {
  593. text = text.replace(linkPattern5, (m, p1) => {
  594. return `<a href="${p1}">${p1}</a>`
  595. })
  596. }
  597. if (text.search(linkPattern6) >= 0) {
  598. text = text.replace(linkPattern6, (m, p1, p2) => {
  599. return `<a href="${p1}">${p2}</a>`
  600. })
  601. }
  602. return text
  603. }
  604. var textToLink = (textNode, previousText) => {
  605. var _a, _b
  606. const textContent = (_a = textNode.textContent) != null ? _a : ""
  607. const parentNode = textNode.parentNode
  608. const mergedText = previousText + textContent
  609. if (
  610. !parentNode ||
  611. textNode.nodeName !== "#text" ||
  612. textContent.trim().length === 0 ||
  613. mergedText.trim().length < 3
  614. ) {
  615. return
  616. }
  617. if (textContent.includes("://")) {
  618. const original = textContent
  619. let newContent = original
  620. if (new RegExp("\\[.*]\\(", "ms").test(original)) {
  621. newContent = replaceMarkdownImgLinks(newContent)
  622. newContent = replaceMarkdownLinks(newContent)
  623. }
  624. if (/\[(img|url)]|\[url=/.test(textContent)) {
  625. newContent = replaceBBCodeImgLinks(newContent)
  626. newContent = replaceBBCodeLinks(newContent)
  627. }
  628. if (newContent === original) {
  629. newContent = replaceTextLinks(original)
  630. } else {
  631. newContent = newContent.replace(
  632. new RegExp(
  633. "(<a(?:\\s[^<>]*)?>.*?<\\/a>)|(<img(?:\\s[^<>]*)?\\/?>)|(.+?(?=(?:<a|<img))|.+$)",
  634. "gims"
  635. ),
  636. (m, p1, p2) => (p1 || p2 ? m : replaceTextLinks(m))
  637. )
  638. }
  639. if (newContent !== original) {
  640. const span = createElement("span")
  641. span.innerHTML = newContent
  642. textNode.after(span)
  643. textNode.remove()
  644. return true
  645. }
  646. }
  647. const parentTextContent = (_b = parentNode.textContent) != null ? _b : ""
  648. if (
  649. new RegExp("\\[.*]\\(", "ms").test(mergedText) &&
  650. (parentTextContent.search(linkPattern2) >= 0 ||
  651. $$("img", parentNode).length > 0)
  652. ) {
  653. const original = parentNode.innerHTML
  654. const newContent = original
  655. .replace(new RegExp("\\[.*]\\([^[\\]()]+?\\)", "gims"), (m) =>
  656. m
  657. .replace(
  658. /<img[^<>]*\ssrc=['"]?(http[^'"]+)['"]?(\s[^<>]*)?>/gim,
  659. "$1"
  660. )
  661. .replace(
  662. /\((?:\s|<br\/?>)*<a[^<>]*\shref=['"]?(http[^'"]+)['"]?(\s[^<>]*)?>\1<\/a>(?:\s|<br\/?>)*\)/gim,
  663. "($1)"
  664. )
  665. )
  666. .replace(
  667. new RegExp("\\[!\\[.*]\\([^()]+\\)]\\([^[\\]()]+?\\)", "gims"),
  668. (m) =>
  669. m
  670. .replace(
  671. /<img[^<>]*\ssrc=['"]?(http[^'"]+)['"]?(\s[^<>]*)?>/gim,
  672. "$1"
  673. )
  674. .replace(
  675. /\((?:\s|<br\/?>)*<a[^<>]*\shref=['"]?(http[^'"]+)['"]?(\s[^<>]*)?>\1<\/a>(?:\s|<br\/?>)*\)/gim,
  676. "($1)"
  677. )
  678. )
  679. if (newContent !== original) {
  680. let newContent2 = replaceMarkdownImgLinks(newContent)
  681. newContent2 = replaceMarkdownLinks(newContent2)
  682. if (newContent2 !== newContent) {
  683. parentNode.innerHTML = newContent2
  684. return true
  685. }
  686. }
  687. }
  688. if (
  689. /\[(img|url)]|\[url=/.test(textContent) &&
  690. parentTextContent.search(/\[(img|url)[^\]]*]([^[\]]*?)\[\/\1]/) >= 0
  691. ) {
  692. const original = parentNode.innerHTML
  693. let before = ""
  694. let after = original
  695. let count = 0
  696. while (before !== after && count < 5) {
  697. count++
  698. before = after
  699. after = before.replace(
  700. /\[(img|url)[^\]]*]([^[\]]+?)\[\/\1]/gim,
  701. (m, p1) => {
  702. let tagsRemoved
  703. let converted
  704. if (p1 === "img") {
  705. tagsRemoved = m
  706. .replace(
  707. /<img[^<>]*\ssrc=['"]?(http[^'"]+)['"]?(\s[^<>]*)?>/gim,
  708. "$1"
  709. )
  710. .replace(
  711. /\[img](?:\s|<br\/?>)*<a[^<>]*\shref=['"]?(http[^'"]+)['"]?(\s[^<>]*)?>\1<\/a>(?:\s|<br\/?>)*\[\/img]/gim,
  712. "[img]$1[/img]"
  713. )
  714. converted = replaceBBCodeImgLinks(tagsRemoved)
  715. } else {
  716. tagsRemoved = m
  717. .replace(
  718. /\[url](?:\s|<br\/?>)*<a[^<>]*\shref=['"]?(http[^'"]+)['"]?(\s[^<>]*)?>\1<\/a>(?:\s|<br\/?>)*\[\/url]/gim,
  719. "[url]$1[/url]"
  720. )
  721. .replace(
  722. /\[url=<a[^<>]*\shref=['"]?(http[^'"]+)['"]?(\s[^<>]*)?>\1<\/a>]/gim,
  723. "[url=$1]"
  724. )
  725. converted = replaceBBCodeLinks(tagsRemoved)
  726. }
  727. return converted === tagsRemoved ? m : converted
  728. }
  729. )
  730. }
  731. const newContent = after
  732. if (newContent !== original) {
  733. parentNode.innerHTML = newContent
  734. return true
  735. }
  736. }
  737. }
  738. var fixAnchorTag = (anchorElement) => {
  739. var _a
  740. const href = anchorElement.href
  741. const textContent = (_a = anchorElement.textContent) != null ? _a : ""
  742. const nextSibling = anchorElement.nextSibling
  743. if (
  744. anchorElement.childElementCount === 0 &&
  745. href.includes(")") &&
  746. textContent.includes(")")
  747. ) {
  748. const index = textContent.indexOf(")")
  749. const removed = textContent.slice(Math.max(0, index))
  750. anchorElement.textContent = textContent.slice(0, Math.max(0, index))
  751. anchorElement.href = anchorElement.href.slice(
  752. 0,
  753. Math.max(0, href.indexOf(")"))
  754. )
  755. if (nextSibling && nextSibling.nodeType === 3) {
  756. nextSibling.textContent = removed + nextSibling.textContent
  757. } else {
  758. anchorElement.after(doc.createTextNode(removed))
  759. }
  760. }
  761. }
  762. var scanAndConvertChildNodes = (parentNode) => {
  763. if (
  764. !parentNode ||
  765. parentNode.nodeType === 8 ||
  766. !parentNode.tagName ||
  767. ignoredTags.has(parentNode.tagName.toUpperCase())
  768. ) {
  769. if (parentNode.tagName === "A") {
  770. fixAnchorTag(parentNode)
  771. }
  772. return
  773. }
  774. let previousText = ""
  775. for (const child of parentNode.childNodes) {
  776. try {
  777. if (child.nodeName === "#text") {
  778. if (textToLink(child, previousText)) {
  779. scanAndConvertChildNodes(parentNode)
  780. break
  781. }
  782. previousText += child.textContent
  783. } else if (child.nodeName === "BR") {
  784. previousText += "\n"
  785. } else {
  786. previousText = ""
  787. scanAndConvertChildNodes(child)
  788. }
  789. } catch (error) {
  790. console.error(error)
  791. }
  792. }
  793. }
  794. var origin = location.origin
  795. var host = location.host
  796. var config = {
  797. run_at: "document_start",
  798. }
  799. var settingsTable2 = {
  800. enable: {
  801. title: "Enable",
  802. defaultValue: true,
  803. },
  804. [`enableCurrentSite_${host}`]: {
  805. title: "Enable current site",
  806. defaultValue: true,
  807. },
  808. [`enableCustomRulesForCurrentSite_${host}`]: {
  809. title: "Enable custom rules for current site",
  810. defaultValue: false,
  811. },
  812. [`customRulesForCurrentSite_${host}`]: {
  813. title: "Enable custom rules for current site",
  814. defaultValue: "",
  815. type: "textarea",
  816. },
  817. }
  818. function registerMenuCommands() {
  819. registerMenuCommand("\u2699\uFE0F \u8BBE\u7F6E", showSettings, "o")
  820. }
  821. var getWithoutOrigin = (url) => url.replace(/(^https?:\/\/[^/]+)/, "")
  822. var shouldOpenInNewTab = (element) => {
  823. var _a
  824. const url = element.href
  825. if (
  826. !url ||
  827. !/^https?:\/\//.test(url) ||
  828. ((_a = element.getAttribute("href")) == null
  829. ? void 0
  830. : _a.startsWith("#"))
  831. ) {
  832. return false
  833. }
  834. if (element.origin !== origin) {
  835. return true
  836. }
  837. if (getSettingsValue(`enableCustomRulesForCurrentSite_${host}`)) {
  838. const rules2 = (
  839. getSettingsValue(`customRulesForCurrentSite_${host}`) || ""
  840. ).split("\n")
  841. if (rules2.includes("*")) {
  842. return true
  843. }
  844. const hrefWithoutOrigin = getWithoutOrigin(url)
  845. for (let rule of rules2) {
  846. rule = rule.trim()
  847. if (rule.length === 0) {
  848. continue
  849. }
  850. try {
  851. const regexp = new RegExp(rule)
  852. if (regexp.test(hrefWithoutOrigin)) {
  853. return true
  854. }
  855. } catch (error) {
  856. console.log(error.message)
  857. if (hrefWithoutOrigin.includes(rule)) {
  858. return true
  859. }
  860. }
  861. }
  862. }
  863. }
  864. var setAttributeAsOpenInNewTab = (element) => {
  865. if (shouldOpenInNewTab(element)) {
  866. setAttribute(element, "target", "_blank")
  867. addAttribute(element, "rel", "noopener")
  868. }
  869. }
  870. async function main() {
  871. await initSettings({
  872. title: "\u{1F517} Links Helper",
  873. footer: `
  874. <p>After change settings, reload the page to take effect</p>
  875. <p>
  876. <a href="https://github.com/utags/links-helper/issues" target="_blank">
  877. Report and Issue...
  878. </a></p>
  879. <p>Made with \u2764\uFE0F by
  880. <a href="https://www.pipecraft.net/" target="_blank">
  881. Pipecraft
  882. </a></p>`,
  883. settingsTable: settingsTable2,
  884. })
  885. registerMenuCommands()
  886. if (
  887. !getSettingsValue("enable") ||
  888. !getSettingsValue(`enableCurrentSite_${host}`)
  889. ) {
  890. return
  891. }
  892. addEventListener(
  893. document,
  894. "click",
  895. (event) => {
  896. let anchorElement = event.target
  897. while (anchorElement && anchorElement.tagName !== "A") {
  898. anchorElement = anchorElement.parentNode
  899. }
  900. if (anchorElement) {
  901. setAttributeAsOpenInNewTab(anchorElement)
  902. if (getAttribute(anchorElement, "target") === "_blank") {
  903. event.stopImmediatePropagation()
  904. event.stopPropagation()
  905. }
  906. }
  907. },
  908. true
  909. )
  910. const scanAnchors = () => {
  911. for (const element of $$("a")) {
  912. if (element.__links_helper_scaned) {
  913. continue
  914. }
  915. element.__links_helper_scaned = 1
  916. try {
  917. setAttributeAsOpenInNewTab(element)
  918. } catch (error) {
  919. console.error(error)
  920. }
  921. try {
  922. linkToImg(element)
  923. } catch (error) {
  924. console.error(error)
  925. }
  926. }
  927. }
  928. const scanNodes = throttle(() => {
  929. scanAndConvertChildNodes(doc.body)
  930. scanAnchors()
  931. bindOnError()
  932. }, 500)
  933. const observer = new MutationObserver(() => {
  934. scanNodes()
  935. })
  936. const startObserver = () => {
  937. observer.observe(doc.body, {
  938. childList: true,
  939. subtree: true,
  940. characterData: true,
  941. })
  942. }
  943. if (doc.body) {
  944. startObserver()
  945. scanAndConvertChildNodes(doc.body)
  946. } else {
  947. const intervalId = setInterval(() => {
  948. if (doc.body) {
  949. clearInterval(intervalId)
  950. startObserver()
  951. scanAndConvertChildNodes(doc.body)
  952. }
  953. }, 100)
  954. }
  955. scanAnchors()
  956. }
  957. main()
  958. })()