Based on Linkify Plus. Turn plain text URLs into links.
// ==UserScript== // @name Linkify Plus Plus // @version 12.0.1 // @description Based on Linkify Plus. Turn plain text URLs into links. // @license BSD-3-Clause // @author eight04 <[email protected]> // @homepageURL https://github.com/eight04/linkify-plus-plus // @supportURL https://github.com/eight04/linkify-plus-plus/issues // @namespace eight04.blogspot.com // @include * // @exclude https://www.google.*/search* // @exclude https://www.google.*/webhp* // @exclude https://music.google.com/* // @exclude https://mail.google.com/* // @exclude https://docs.google.com/* // @exclude https://encrypted.google.com/* // @exclude https://*101weiqi.com/* // @exclude https://w3c*.github.io/* // @exclude https://www.paypal.com/* // @exclude https://term.ptt.cc/* // @exclude https://mastodon.social/* // @grant GM.getValue // @grant GM.setValue // @grant GM.deleteValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_addValueChangeListener // @grant unsafeWindow // @compatible firefox Tampermonkey latest // @compatible chrome Tampermonkey latest // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDE2IDE2Ij4gPHBhdGggZmlsbD0iIzRjNGM0ZCIgZD0iTTMuNSAxYS41LjUgMCAxIDAgMCAxSDR2OWgtLjVhLjUuNSAwIDAgMCAwIDFoNy44NTVhLjUuNSAwIDAgMCAuNDc1LS4xODQuNS41IDAgMCAwIC4xMDYtLjM5OFYxMC41YS41LjUgMCAxIDAtMSAwdi41SDZWMmguNWEuNS41IDAgMSAwIDAtMWgtM3oiLz4gPHBhdGggZmlsbD0iIzQ1YTFmZiIgZD0iTTIuNSAxNGExIDEgMCAxIDAgMCAyaDExYTEgMSAwIDEgMCAwLTJoLTExeiIvPiA8L3N2Zz4= // ==/UserScript== var optionsFuzzyIpLabel = "Match ambiguous IP addresses."; var optionsIgnoreMustacheLabel = "Ignore URLs inside mustaches e.g. {{ ... }}."; var optionsEmbedImageLabel = "Create an image element if the URL looks like an image file."; var optionsEmbedImageExcludeElementLabel = "Exclude following elements. (CSS selector)"; var optionsUnicodeLabel = "Match unicode characters."; var optionsMailLabel = "Match email address."; var optionsNewTabLabel = "Open links in new tabs."; var optionsStandaloneLabel = "The URL must be surrounded by whitespaces."; var optionsLinkifierLabel = "Linkifier"; var optionsTriggerByPageLoadLabel = "Trigger on page load."; var optionsTriggerByNewNodeLabel = "Trigger on dynamically created elements."; var optionsTriggerByHoverLabel = "Trigger on mouse over."; var optionsTriggerByClickLabel = "Trigger on mouse click."; var optionsBoundaryLeftLabel = "Allowed characters between the whitespace and the link. (left side)"; var optionsBoundaryRightLabel = "Allowed characters between the whitespace and the link. (right side)"; var optionsExcludeElementLabel = "Do not linkify following elements. (CSS selector)"; var optionsIncludeElementLabel = "Always linkify following elements. Override above. (CSS selector)"; var optionsTimeoutLabel = "Max execution time. (ms)"; var optionsTimeoutHelp = "The script will terminate if it takes too long to convert the entire page."; var optionsMaxRunTimeLabel = "Max script run time. (ms)"; var optionsMaxRunTimeHelp = "If the script takes too long to run, the process would be splitted into small chunks to avoid browser freeze."; var optionsUrlMatcherLabel = "URL matcher"; var optionsCustomRulesLabel = "Custom rules. (RegExp per line)"; var currentScopeLabel = "Current domain"; var addScopeLabel = "Add new domain"; var addScopePrompt = "Add new domain"; var deleteScopeLabel = "Delete current domain"; var deleteScopeConfirm = "Delete domain $1?"; var learnMoreButton = "Learn more"; var importButton = "Import"; var importPrompt = "Paste settings"; var exportButton = "Export"; var exportPrompt = "Copy settings"; var translate = { optionsFuzzyIpLabel: optionsFuzzyIpLabel, optionsIgnoreMustacheLabel: optionsIgnoreMustacheLabel, optionsEmbedImageLabel: optionsEmbedImageLabel, optionsEmbedImageExcludeElementLabel: optionsEmbedImageExcludeElementLabel, optionsUnicodeLabel: optionsUnicodeLabel, optionsMailLabel: optionsMailLabel, optionsNewTabLabel: optionsNewTabLabel, optionsStandaloneLabel: optionsStandaloneLabel, optionsLinkifierLabel: optionsLinkifierLabel, optionsTriggerByPageLoadLabel: optionsTriggerByPageLoadLabel, optionsTriggerByNewNodeLabel: optionsTriggerByNewNodeLabel, optionsTriggerByHoverLabel: optionsTriggerByHoverLabel, optionsTriggerByClickLabel: optionsTriggerByClickLabel, optionsBoundaryLeftLabel: optionsBoundaryLeftLabel, optionsBoundaryRightLabel: optionsBoundaryRightLabel, optionsExcludeElementLabel: optionsExcludeElementLabel, optionsIncludeElementLabel: optionsIncludeElementLabel, optionsTimeoutLabel: optionsTimeoutLabel, optionsTimeoutHelp: optionsTimeoutHelp, optionsMaxRunTimeLabel: optionsMaxRunTimeLabel, optionsMaxRunTimeHelp: optionsMaxRunTimeHelp, optionsUrlMatcherLabel: optionsUrlMatcherLabel, optionsCustomRulesLabel: optionsCustomRulesLabel, currentScopeLabel: currentScopeLabel, addScopeLabel: addScopeLabel, addScopePrompt: addScopePrompt, deleteScopeLabel: deleteScopeLabel, deleteScopeConfirm: deleteScopeConfirm, learnMoreButton: learnMoreButton, importButton: importButton, importPrompt: importPrompt, exportButton: exportButton, exportPrompt: exportPrompt }; /** * event-lite.js - Light-weight EventEmitter (less than 1KB when gzipped) * * @copyright Yusuke Kawasaki * @license MIT * @constructor * @see https://github.com/kawanet/event-lite * @see http://kawanet.github.io/event-lite/EventLite.html * @example * var EventLite = require("event-lite"); * * function MyClass() {...} // your class * * EventLite.mixin(MyClass.prototype); // import event methods * * var obj = new MyClass(); * obj.on("foo", function() {...}); // add event listener * obj.once("bar", function() {...}); // add one-time event listener * obj.emit("foo"); // dispatch event * obj.emit("bar"); // dispatch another event * obj.off("foo"); // remove event listener */ function EventLite() { if (!(this instanceof EventLite)) return new EventLite(); } // (function(EventLite) { // export the class for node.js // if ("undefined" !== typeof module) module.exports = EventLite; // property name to hold listeners var LISTENERS = "listeners"; // methods to export var methods = { on: on, once: once, off: off, emit: emit }; // mixin to self mixin(EventLite.prototype); // export mixin function EventLite.mixin = mixin; /** * Import on(), once(), off() and emit() methods into target object. * * @function EventLite.mixin * @param target {Prototype} */ function mixin(target) { for (var key in methods) { target[key] = methods[key]; } return target; } /** * Add an event listener. * * @function EventLite.prototype.on * @param type {string} * @param func {Function} * @returns {EventLite} Self for method chaining */ function on(type, func) { getListeners(this, type).push(func); return this; } /** * Add one-time event listener. * * @function EventLite.prototype.once * @param type {string} * @param func {Function} * @returns {EventLite} Self for method chaining */ function once(type, func) { var that = this; wrap.originalListener = func; getListeners(that, type).push(wrap); return that; function wrap() { off.call(that, type, wrap); func.apply(this, arguments); } } /** * Remove an event listener. * * @function EventLite.prototype.off * @param [type] {string} * @param [func] {Function} * @returns {EventLite} Self for method chaining */ function off(type, func) { var that = this; var listners; if (!arguments.length) { delete that[LISTENERS]; } else if (!func) { listners = that[LISTENERS]; if (listners) { delete listners[type]; if (!Object.keys(listners).length) return off.call(that); } } else { listners = getListeners(that, type, true); if (listners) { listners = listners.filter(ne); if (!listners.length) return off.call(that, type); that[LISTENERS][type] = listners; } } return that; function ne(test) { return test !== func && test.originalListener !== func; } } /** * Dispatch (trigger) an event. * * @function EventLite.prototype.emit * @param type {string} * @param [value] {*} * @returns {boolean} True when a listener received the event */ function emit(type, value) { var that = this; var listeners = getListeners(that, type, true); if (!listeners) return false; var arglen = arguments.length; if (arglen === 1) { listeners.forEach(zeroarg); } else if (arglen === 2) { listeners.forEach(onearg); } else { var args = Array.prototype.slice.call(arguments, 1); listeners.forEach(moreargs); } return !!listeners.length; function zeroarg(func) { func.call(that); } function onearg(func) { func.call(that, value); } function moreargs(func) { func.apply(that, args); } } /** * @ignore */ function getListeners(that, type, readonly) { if (readonly && !that[LISTENERS]) return; var listeners = that[LISTENERS] || (that[LISTENERS] = {}); return listeners[type] || (listeners[type] = []); } // })(EventLite); function createPref(DEFAULT, sep = "/") { let storage; let currentScope = "global"; let scopeList = ["global"]; const events = new EventLite; const globalCache = {}; let scopedCache = {}; let currentCache = Object.assign({}, DEFAULT); let initializing; return Object.assign(events, { // storage, // ready, connect, disconnect, get, getAll, set, getCurrentScope, setCurrentScope, addScope, deleteScope, getScopeList, import: import_, export: export_, has }); function import_(input) { const newScopeList = input.scopeList || scopeList.slice(); const scopes = new Set(newScopeList); if (!scopes.has("global")) { throw new Error("invalid scopeList"); } const changes = { scopeList: newScopeList }; for (const [scopeName, scope] of Object.entries(input.scopes)) { if (!scopes.has(scopeName)) { continue; } for (const [key, value] of Object.entries(scope)) { if (DEFAULT[key] == undefined) { continue; } changes[`${scopeName}${sep}${key}`] = value; } } return storage.setMany(changes); } function export_() { const keys = []; for (const scope of scopeList) { keys.push(...Object.keys(DEFAULT).map(k => `${scope}${sep}${k}`)); } keys.push("scopeList"); return storage.getMany(keys) .then(changes => { const _scopeList = changes.scopeList || scopeList.slice(); const scopes = new Set(_scopeList); const output = { scopeList: _scopeList, scopes: {} }; for (const [key, value] of Object.entries(changes)) { const sepIndex = key.indexOf(sep); if (sepIndex < 0) { continue; } const scope = key.slice(0, sepIndex); const realKey = key.slice(sepIndex + sep.length); if (!scopes.has(scope)) { continue; } if (DEFAULT[realKey] == undefined) { continue; } if (!output.scopes[scope]) { output.scopes[scope] = {}; } output.scopes[scope][realKey] = value; } return output; }); } function connect(_storage) { storage = _storage; initializing = storage.getMany( Object.keys(DEFAULT).map(k => `global${sep}${k}`).concat(["scopeList"]) ) .then(updateCache); storage.on("change", updateCache); return initializing; } function disconnect() { storage.off("change", updateCache); storage = null; } function updateCache(changes, rebuildCache = false) { if (changes.scopeList) { scopeList = changes.scopeList; events.emit("scopeListChange", scopeList); if (!scopeList.includes(currentScope)) { return setCurrentScope("global"); } } const changedKeys = new Set; for (const [key, value] of Object.entries(changes)) { const [scope, realKey] = key.startsWith(`global${sep}`) ? ["global", key.slice(6 + sep.length)] : key.startsWith(`${currentScope}${sep}`) ? [currentScope, key.slice(currentScope.length + sep.length)] : [null, null]; if (!scope || DEFAULT[realKey] == null) { continue; } if (scope === "global") { changedKeys.add(realKey); globalCache[realKey] = value; } if (scope === currentScope) { changedKeys.add(realKey); scopedCache[realKey] = value; } } if (rebuildCache) { Object.keys(DEFAULT).forEach(k => changedKeys.add(k)); } const realChanges = {}; let isChanged = false; for (const key of changedKeys) { const value = scopedCache[key] != null ? scopedCache[key] : globalCache[key] != null ? globalCache[key] : DEFAULT[key]; if (currentCache[key] !== value) { realChanges[key] = value; currentCache[key] = value; isChanged = true; } } if (isChanged) { events.emit("change", realChanges); } } function has(key) { return currentCache.hasOwnProperty(key); } function get(key) { return currentCache[key]; } function getAll() { return Object.assign({}, currentCache); } function set(key, value) { return storage.setMany({ [`${currentScope}${sep}${key}`]: value }); } function getCurrentScope() { return currentScope; } function setCurrentScope(newScope) { if (currentScope === newScope) { return Promise.resolve(true); } if (!scopeList.includes(newScope)) { return Promise.resolve(false); } return storage.getMany(Object.keys(DEFAULT).map(k => `${newScope}${sep}${k}`)) .then(changes => { currentScope = newScope; scopedCache = {}; events.emit("scopeChange", currentScope); updateCache(changes, true); return true; }); } function addScope(scope) { if (scopeList.includes(scope)) { return Promise.reject(new Error(`${scope} already exists`)); } if (scope.includes(sep)) { return Promise.reject(new Error(`invalid word: ${sep}`)); } return storage.setMany({ scopeList: scopeList.concat([scope]) }); } function deleteScope(scope) { if (scope === "global") { return Promise.reject(new Error(`cannot delete global`)); } return Promise.all([ storage.setMany({ scopeList: scopeList.filter(s => s != scope) }), storage.deleteMany(Object.keys(DEFAULT).map(k => `${scope}${sep}${k}`)) ]); } function getScopeList() { return scopeList; } } const keys = Object.keys; function isBoolean(val) { return typeof val === "boolean" } function isElement(val) { return val && typeof val.nodeType === "number" } function isString(val) { return typeof val === "string" } function isNumber(val) { return typeof val === "number" } function isObject(val) { return typeof val === "object" ? val !== null : isFunction(val) } function isFunction(val) { return typeof val === "function" } function isArrayLike(obj) { return isObject(obj) && typeof obj.length === "number" && typeof obj.nodeType !== "number" } function forEach(value, fn) { if (!value) return for (const key of keys(value)) { fn(value[key], key); } } function isRef(maybeRef) { return isObject(maybeRef) && "current" in maybeRef } const isUnitlessNumber = { animationIterationCount: 0, borderImageOutset: 0, borderImageSlice: 0, borderImageWidth: 0, boxFlex: 0, boxFlexGroup: 0, boxOrdinalGroup: 0, columnCount: 0, columns: 0, flex: 0, flexGrow: 0, flexPositive: 0, flexShrink: 0, flexNegative: 0, flexOrder: 0, gridArea: 0, gridRow: 0, gridRowEnd: 0, gridRowSpan: 0, gridRowStart: 0, gridColumn: 0, gridColumnEnd: 0, gridColumnSpan: 0, gridColumnStart: 0, fontWeight: 0, lineClamp: 0, lineHeight: 0, opacity: 0, order: 0, orphans: 0, tabSize: 0, widows: 0, zIndex: 0, zoom: 0, fillOpacity: 0, floodOpacity: 0, stopOpacity: 0, strokeDasharray: 0, strokeDashoffset: 0, strokeMiterlimit: 0, strokeOpacity: 0, strokeWidth: 0, }; function prefixKey(prefix, key) { return prefix + key.charAt(0).toUpperCase() + key.substring(1) } const prefixes = ["Webkit", "ms", "Moz", "O"]; keys(isUnitlessNumber).forEach((prop) => { prefixes.forEach((prefix) => { isUnitlessNumber[prefixKey(prefix, prop)] = 0; }); }); const SVGNamespace = "http://www.w3.org/2000/svg"; const XLinkNamespace = "http://www.w3.org/1999/xlink"; const XMLNamespace = "http://www.w3.org/XML/1998/namespace"; function isVisibleChild(value) { return !isBoolean(value) && value != null } function className(value) { if (Array.isArray(value)) { return value.map(className).filter(Boolean).join(" ") } else if (isObject(value)) { return keys(value) .filter((k) => value[k]) .join(" ") } else if (isVisibleChild(value)) { return "" + value } else { return "" } } const svg = { animate: 0, circle: 0, clipPath: 0, defs: 0, desc: 0, ellipse: 0, feBlend: 0, feColorMatrix: 0, feComponentTransfer: 0, feComposite: 0, feConvolveMatrix: 0, feDiffuseLighting: 0, feDisplacementMap: 0, feDistantLight: 0, feFlood: 0, feFuncA: 0, feFuncB: 0, feFuncG: 0, feFuncR: 0, feGaussianBlur: 0, feImage: 0, feMerge: 0, feMergeNode: 0, feMorphology: 0, feOffset: 0, fePointLight: 0, feSpecularLighting: 0, feSpotLight: 0, feTile: 0, feTurbulence: 0, filter: 0, foreignObject: 0, g: 0, image: 0, line: 0, linearGradient: 0, marker: 0, mask: 0, metadata: 0, path: 0, pattern: 0, polygon: 0, polyline: 0, radialGradient: 0, rect: 0, stop: 0, svg: 0, switch: 0, symbol: 0, text: 0, textPath: 0, tspan: 0, use: 0, view: 0, }; function createElement(tag, attr, ...children) { if (isString(attr) || Array.isArray(attr)) { children.unshift(attr); attr = {}; } attr = attr || {}; if (!attr.namespaceURI && svg[tag] === 0) { attr = { ...attr, namespaceURI: SVGNamespace }; } if (attr.children != null && !children.length) { ({ children, ...attr } = attr); } let node; if (isString(tag)) { node = attr.namespaceURI ? document.createElementNS(attr.namespaceURI, tag) : document.createElement(tag); attributes(attr, node); appendChild(children, node); } else if (isFunction(tag)) { if (isObject(tag.defaultProps)) { attr = { ...tag.defaultProps, ...attr }; } node = tag({ ...attr, children }); } if (isRef(attr.ref)) { attr.ref.current = node; } else if (isFunction(attr.ref)) { attr.ref(node); } return node } function appendChild(child, node) { if (isArrayLike(child)) { appendChildren(child, node); } else if (isString(child) || isNumber(child)) { appendChildToNode(document.createTextNode(child), node); } else if (child === null) { appendChildToNode(document.createComment(""), node); } else if (isElement(child)) { appendChildToNode(child, node); } } function appendChildren(children, node) { for (const child of children) { appendChild(child, node); } return node } function appendChildToNode(child, node) { if (node instanceof window.HTMLTemplateElement) { node.content.appendChild(child); } else { node.appendChild(child); } } function normalizeAttribute(s) { return s.replace(/[A-Z\d]/g, (match) => ":" + match.toLowerCase()) } function attribute(key, value, node) { switch (key) { case "xlinkActuate": case "xlinkArcrole": case "xlinkHref": case "xlinkRole": case "xlinkShow": case "xlinkTitle": case "xlinkType": attrNS(node, XLinkNamespace, normalizeAttribute(key), value); return case "xmlnsXlink": attr(node, normalizeAttribute(key), value); return case "xmlBase": case "xmlLang": case "xmlSpace": attrNS(node, XMLNamespace, normalizeAttribute(key), value); return } switch (key) { case "htmlFor": attr(node, "for", value); return case "dataset": forEach(value, (dataValue, dataKey) => { if (dataValue != null) { node.dataset[dataKey] = dataValue; } }); return case "innerHTML": case "innerText": case "textContent": node[key] = value; return case "spellCheck": node.spellcheck = value; return case "class": case "className": if (isFunction(value)) { value(node); } else { attr(node, "class", className(value)); } return case "ref": case "namespaceURI": return case "style": if (isObject(value)) { forEach(value, (val, key) => { if (isNumber(val) && isUnitlessNumber[key] !== 0) { node.style[key] = val + "px"; } else { node.style[key] = val; } }); return } } if (isFunction(value)) { if (key[0] === "o" && key[1] === "n") { const attribute = key.toLowerCase(); if (node[attribute] == null) { node[attribute] = value; } else { node.addEventListener(key, value); } } } else if (value === true) { attr(node, key, ""); } else if (value !== false && value != null) { attr(node, key, value); } } function attr(node, key, value) { node.setAttribute(key, value); } function attrNS(node, namespace, key, value) { node.setAttributeNS(namespace, key, value); } function attributes(attr, node) { for (const key of keys(attr)) { attribute(key, attr[key], node); } return node } function messageGetter({ getMessage, DEFAULT }) { return (key, params) => { const message = getMessage(key, params); if (message) return message; const defaultMessage = DEFAULT[key]; if (!defaultMessage) return ""; if (!params) return defaultMessage; if (!Array.isArray(params)) { params = [params]; } return defaultMessage.replace(/\$(\d+)/g, (m, n) => params[n - 1]); }; } function fallback(getMessage) { return messageGetter({ getMessage, DEFAULT: { currentScopeLabel: "Current scope", addScopeLabel: "Add new scope", deleteScopeLabel: "Delete current scope", learnMoreButton: "Learn more", importButton: "Import", exportButton: "Export", addScopePrompt: "Add new scope", deleteScopeConfirm: "Delete scope $1?", importPrompt: "Paste settings", exportPrompt: "Copy settings" } }); } const VALID_CONTROL = new Set(["import", "export", "scope-list", "add-scope", "delete-scope"]); class DefaultMap extends Map { constructor(getDefault) { super(); this.getDefault = getDefault; } get(key) { let item = super.get(key); if (!item) { item = this.getDefault(); super.set(key, item); } return item; } } function bindInputs(pref, inputs) { const bounds = []; const onPrefChange = change => { for (const key in change) { if (!inputs.has(key)) { continue; } for (const input of inputs.get(key)) { updateInput(input, change[key]); } } }; pref.on("change", onPrefChange); bounds.push(() => pref.off("change", onPrefChange)); for (const [key, list] of inputs.entries()) { for (const input of list) { const evt = input.hasAttribute("realtime") ? "input" : "change"; const onChange = () => updatePref(key, input); input.addEventListener(evt, onChange); bounds.push(() => input.removeEventListener(evt, onChange)); } } onPrefChange(pref.getAll()); return () => { for (const unbind of bounds) { unbind(); } }; function updatePref(key, input) { if (!input.checkValidity()) { return; } if (input.type === "checkbox") { pref.set(key, input.checked); return; } if (input.type === "radio") { if (input.checked) { pref.set(key, input.value); } return; } if (input.nodeName === "SELECT" && input.multiple) { pref.set(key, [...input.options].filter(o => o.selected).map(o => o.value)); return; } if (input.type === "number" || input.type === "range") { pref.set(key, Number(input.value)); return; } pref.set(key, input.value); } function updateInput(input, value) { if (input.nodeName === "INPUT" && input.type === "radio") { input.checked = input.value === value; return; } if (input.type === "checkbox") { input.checked = value; return; } if (input.nodeName === "SELECT" && input.multiple) { const checked = new Set(value); for (const option of input.options) { option.selected = checked.has(option.value); } return; } input.value = value; } } function bindFields(pref, fields) { const onPrefChange = change => { for (const key in change) { if (!fields.has(key)) { continue; } for (const field of fields.get(key)) { field.disabled = field.dataset.bindToValue ? field.dataset.bindToValue !== change[key] : !change[key]; } } }; pref.on("change", onPrefChange); onPrefChange(pref.getAll()); return () => pref.off("change", onPrefChange); } function bindControls({ pref, controls, alert: _alert = alert, confirm: _confirm = confirm, prompt: _prompt = prompt, getMessage = () => {}, getNewScope = () => "" }) { const CONTROL_METHODS = { "import": ["click", doImport], "export": ["click", doExport], "scope-list": ["change", updateCurrentScope], "add-scope": ["click", addScope], "delete-scope": ["click", deleteScope] }; for (const type in CONTROL_METHODS) { for (const el of controls.get(type)) { el.addEventListener(CONTROL_METHODS[type][0], CONTROL_METHODS[type][1]); } } pref.on("scopeChange", updateCurrentScopeEl); pref.on("scopeListChange", updateScopeList); updateScopeList(); updateCurrentScopeEl(); const _ = fallback(getMessage); return unbind; function unbind() { pref.off("scopeChange", updateCurrentScopeEl); pref.off("scopeListChange", updateScopeList); for (const type in CONTROL_METHODS) { for (const el of controls.get(type)) { el.removeEventListener(CONTROL_METHODS[type][0], CONTROL_METHODS[type][1]); } } } async function doImport() { try { const input = await _prompt(_("importPrompt")); if (input == null) { return; } const settings = JSON.parse(input); return pref.import(settings); } catch (err) { await _alert(err.message); } } async function doExport() { try { const settings = await pref.export(); await _prompt(_("exportPrompt"), JSON.stringify(settings)); } catch (err) { await _alert(err.message); } } function updateCurrentScope(e) { pref.setCurrentScope(e.target.value); } async function addScope() { try { let scopeName = await _prompt(_("addScopePrompt"), getNewScope()); if (scopeName == null) { return; } scopeName = scopeName.trim(); if (!scopeName) { throw new Error("the value is empty"); } await pref.addScope(scopeName); pref.setCurrentScope(scopeName); } catch (err) { await _alert(err.message); } } async function deleteScope() { try { const scopeName = pref.getCurrentScope(); const r###lt = await _confirm(_("deleteScopeConfirm", scopeName)); if (r###lt) { return pref.deleteScope(scopeName); } } catch (err) { await _alert(err.message); } } function updateCurrentScopeEl() { const scopeName = pref.getCurrentScope(); for (const el of controls.get("scope-list")) { el.value = scopeName; } } function updateScopeList() { const scopeList = pref.getScopeList(); for (const el of controls.get("scope-list")) { el.innerHTML = ""; el.append(...scopeList.map(scope => { const option = document.createElement("option"); option.value = scope; option.textContent = scope; return option; })); } } } function createBinding({ pref, root, elements = root.querySelectorAll("input, textarea, select, fieldset, button"), keyPrefix = "pref-", controlPrefix = "webext-pref-", alert, confirm, prompt, getMessage, getNewScope }) { const inputs = new DefaultMap(() => []); const fields = new DefaultMap(() => []); const controls = new DefaultMap(() => []); for (const element of elements) { const id = element.id && stripPrefix(element.id, keyPrefix); if (id && pref.has(id)) { inputs.get(id).push(element); continue; } if (element.nodeName === "INPUT" && element.type === "radio") { const name = element.name && stripPrefix(element.name, keyPrefix); if (name && pref.has(name)) { inputs.get(name).push(element); continue; } } if (element.nodeName === "FIELDSET" && element.dataset.bindTo) { fields.get(element.dataset.bindTo).push(element); continue; } const controlType = findControlType(element.classList); if (controlType) { controls.get(controlType).push(element); } } const bounds = [bindInputs(pref, inputs), bindFields(pref, fields), bindControls({ pref, controls, alert, confirm, prompt, getMessage, getNewScope })]; return () => { for (const unbind of bounds) { unbind(); } }; function stripPrefix(id, prefix) { if (!prefix) { return id; } return id.startsWith(prefix) ? id.slice(prefix.length) : ""; } function findControlType(list) { for (const name of list) { const controlType = stripPrefix(name, controlPrefix); if (VALID_CONTROL.has(controlType)) { return controlType; } } } } function createUI({ body, getMessage = () => {}, toolbar = true, navbar = true, keyPrefix = "pref-", controlPrefix = "webext-pref-" }) { const root = document.createDocumentFragment(); const _ = fallback(getMessage); if (toolbar) { root.append(createToolbar()); } if (navbar) { root.append(createNavbar()); } root.append( /*#__PURE__*/createElement("div", { class: controlPrefix + "body" }, body.map(item => { if (!item.hLevel) { item.hLevel = 3; } return createItem(item); }))); return root; function createToolbar() { return /*#__PURE__*/createElement("div", { class: controlPrefix + "toolbar" }, /*#__PURE__*/createElement("button", { type: "button", class: [controlPrefix + "import", "browser-style"] }, _("importButton")), /*#__PURE__*/createElement("button", { type: "button", class: [controlPrefix + "export", "browser-style"] }, _("exportButton"))); } function createNavbar() { return /*#__PURE__*/createElement("div", { class: controlPrefix + "nav" }, /*#__PURE__*/createElement("select", { class: [controlPrefix + "scope-list", "browser-style"], title: _("currentScopeLabel") }), /*#__PURE__*/createElement("button", { type: "button", class: [controlPrefix + "delete-scope", "browser-style"], title: _("deleteScopeLabel") }, "\xD7"), /*#__PURE__*/createElement("button", { type: "button", class: [controlPrefix + "add-scope", "browser-style"], title: _("addScopeLabel") }, "+")); } function createItem(p) { if (p.type === "section") { return createSection(p); } if (p.type === "checkbox") { return createCheckbox(p); } if (p.type === "radiogroup") { return createRadioGroup(p); } return createInput(p); } function createInput(p) { const key = keyPrefix + p.key; let input; const onChange = p.validate ? e => { try { p.validate(e.target.value); e.target.setCustomValidity(""); } catch (err) { e.target.setCustomValidity(err.message || String(err)); } } : null; if (p.type === "select") { input = /*#__PURE__*/createElement("select", { multiple: p.multiple, class: "browser-style", id: key, onChange: onChange }, Object.entries(p.options).map(([value, label]) => /*#__PURE__*/createElement("option", { value: value }, label))); } else if (p.type === "textarea") { input = /*#__PURE__*/createElement("textarea", { rows: "8", class: "browser-style", id: key, onChange: onChange }); } else { input = /*#__PURE__*/createElement("input", { type: p.type, id: key, onChange: onChange }); } return /*#__PURE__*/createElement("div", { class: [`${controlPrefix}${p.type}`, "browser-style", p.className] }, /*#__PURE__*/createElement("label", { htmlFor: key }, p.label), p.learnMore && /*#__PURE__*/createElement(LearnMore, { url: p.learnMore }), input, p.help && /*#__PURE__*/createElement(Help, { content: p.help })); } function createRadioGroup(p) { return /*#__PURE__*/createElement("div", { class: [`${controlPrefix}${p.type}`, "browser-style", p.className] }, /*#__PURE__*/createElement("div", { class: controlPrefix + "radio-title" }, p.label), p.learnMore && /*#__PURE__*/createElement(LearnMore, { url: p.learnMore }), p.help && /*#__PURE__*/createElement(Help, { content: p.help }), p.children.map(c => { c.parentKey = p.key; return createCheckbox(inheritProp(p, c)); })); } function Help({ content }) { return /*#__PURE__*/createElement("p", { class: controlPrefix + "help" }, content); } function LearnMore({ url }) { return /*#__PURE__*/createElement("a", { href: url, class: controlPrefix + "learn-more", target: "_blank", rel: "noopener noreferrer" }, _("learnMoreButton")); } function createCheckbox(p) { const id = p.parentKey ? `${keyPrefix}${p.parentKey}-${p.value}` : keyPrefix + p.key; return /*#__PURE__*/createElement("div", { class: [`${controlPrefix}${p.type}`, "browser-style", p.className] }, /*#__PURE__*/createElement("input", { type: p.type, id: id, name: p.parentKey ? keyPrefix + p.parentKey : null, value: p.value }), /*#__PURE__*/createElement("label", { htmlFor: id }, p.label), p.learnMore && /*#__PURE__*/createElement(LearnMore, { url: p.learnMore }), p.help && /*#__PURE__*/createElement(Help, { content: p.help }), p.children && /*#__PURE__*/createElement("fieldset", { class: controlPrefix + "checkbox-children", dataset: { bindTo: p.parentKey || p.key, bindToValue: p.value } }, p.children.map(c => createItem(inheritProp(p, c))))); } function createSection(p) { const Header = `h${p.hLevel}`; p.hLevel++; return ( /*#__PURE__*/ // FIXME: do we need browser-style for section? createElement("div", { class: [controlPrefix + p.type, p.className] }, /*#__PURE__*/createElement(Header, { class: controlPrefix + "header" }, p.label), p.help && /*#__PURE__*/createElement(Help, { content: p.help }), p.children && p.children.map(c => createItem(inheritProp(p, c)))) ); } function inheritProp(parent, child) { child.hLevel = parent.hLevel; return child; } } /* eslint-env greasemonkey */ function createGMStorage() { const setValue = typeof GM_setValue === "function" ? promisify(GM_setValue) : GM.setValue.bind(GM); const getValue = typeof GM_getValue === "function" ? promisify(GM_getValue) : GM.getValue.bind(GM); const deleteValue = typeof GM_deleteValue === "function" ? promisify(GM_deleteValue) : GM.deleteValue.bind(GM); const events = new EventLite; if (typeof GM_addValueChangeListener === "function") { GM_addValueChangeListener("webext-pref-message", (name, oldValue, newValue) => { const changes = JSON.parse(newValue); for (const key of Object.keys(changes)) { if (typeof changes[key] === "object" && changes[key].$undefined) { changes[key] = undefined; } } events.emit("change", changes); }); } return Object.assign(events, {getMany, setMany, deleteMany}); function getMany(keys) { return Promise.all(keys.map(k => getValue(`webext-pref/${k}`) .then(value => [k, typeof value === "string" ? JSON.parse(value) : value]) )) .then(entries => { const output = {}; for (const [key, value] of entries) { output[key] = value; } return output; }); } function setMany(changes) { return Promise.all(Object.entries(changes).map(([key, value]) => setValue(`webext-pref/${key}`, JSON.stringify(value)) )) .then(() => { if (typeof GM_addValueChangeListener === "function") { return setValue("webext-pref-message", JSON.stringify(changes)); } events.emit("change", changes); }); } function deleteMany(keys) { return Promise.all(keys.map(k => deleteValue(`webext-pref/${k}`))) .then(() => { if (typeof GM_addValueChangeListener === "function") { const changes = {}; for (const key of keys) { changes[key] = { $undefined: true }; } return setValue("webext-pref-message", JSON.stringify(changes)); } const changes = {}; for (const key of keys) { changes[key] = undefined; } events.emit("change", changes); }); } function promisify(fn) { return (...args) => { try { return Promise.resolve(fn(...args)); } catch (err) { return Promise.reject(err); } }; } } /* eslint-env greasemonkey */ function GM_webextPref({ default: default_, separator, css = "", ...options }) { const pref = createPref(default_, separator); const initializing = pref.connect(createGMStorage()); let isOpen = false; const registerMenu = typeof GM_registerMenuCommand === "function" ? GM_registerMenuCommand : typeof GM !== "undefined" && GM && GM.registerMenuCommand ? GM.registerMenuCommand.bind(GM) : undefined; if (registerMenu) { registerMenu(`${getTitle()} - Configure`, openDialog); } return Object.assign(pref, { ready: () => initializing, openDialog }); function openDialog() { if (isOpen) { return; } isOpen = true; let destroyView; const modal = document.createElement("div"); modal.className = "webext-pref-modal"; modal.onclick = () => { modal.classList.remove("webext-pref-modal-open"); modal.addEventListener("transitionend", () => { if (destroyView) { destroyView(); } modal.remove(); isOpen = false; }); }; const style = document.createElement("style"); style.textContent = "body{overflow:hidden}.webext-pref-modal{position:fixed;top:0;right:0;bottom:0;left:0;background:rgba(0,0,0,.5);overflow:auto;z-index:999999;opacity:0;transition:opacity .2s linear;display:flex}.webext-pref-modal-open{opacity:1}.webext-pref-modal::after,.webext-pref-modal::before{content:\"\";display:block;height:30px;visibility:hidden}.webext-pref-iframe-wrap{margin:auto}.webext-pref-iframe{margin:30px 0;display:inline-block;width:100%;max-width:100%;background:#fff;border-width:0;box-shadow:0 0 30px #000;transform:translateY(-20px);transition:transform .2s linear}.webext-pref-modal-open .webext-pref-iframe{transform:none}" + ` body { padding-right: ${window.innerWidth - document.documentElement.offsetWidth}px; } `; const iframe = document.createElement("iframe"); iframe.className = "webext-pref-iframe"; iframe.srcdoc = ` <html> <head> <style class="dialog-style"></style> </head> <body> <div class="dialog-body"></div> </body> </html> `; const wrap = document.createElement("div"); wrap.className = "webext-pref-iframe-wrap"; wrap.append(iframe); modal.append(style, wrap); document.body.appendChild(modal); iframe.onload = () => { iframe.onload = null; iframe.contentDocument.querySelector(".dialog-style").textContent = "body{display:inline-block;font-size:16px;font-family:sans-serif;white-space:nowrap;overflow:hidden;margin:0;color:#3d3d3d;line-height:1}input[type=number],input[type=text],select,textarea{display:block;width:100%;box-sizing:border-box;height:2em;font:inherit;padding:0 .3em;border:1px solid #9e9e9e;cursor:pointer}select[multiple],textarea{height:6em}input[type=number]:hover,input[type=text]:hover,select:hover,textarea:hover{border-color:#d5d5d5}input[type=number]:focus,input[type=text]:focus,select:focus,textarea:focus{cursor:auto;border-color:#3a93ee}textarea{line-height:1.5}input[type=checkbox],input[type=radio]{display:inline-block;width:1em;height:1em;font:inherit;margin:0}button{box-sizing:border-box;height:2em;font:inherit;border:1px solid #9e9e9e;cursor:pointer;background:0 0}button:hover{border-color:#d5d5d5}button:focus{border-color:#3a93ee}.dialog-body{margin:2em}.webext-pref-toolbar{display:flex;align-items:center;margin-bottom:1em}.dialog-title{font-size:1.34em;margin:0 2em 0 0;flex-grow:1}.webext-pref-toolbar button{font-size:.7em;margin-left:.5em}.webext-pref-nav{display:flex;margin-bottom:1em}.webext-pref-nav select{text-align:center;text-align-last:center}.webext-pref-nav button{width:2em}.webext-pref-number,.webext-pref-radiogroup,.webext-pref-select,.webext-pref-text,.webext-pref-textarea{margin:1em 0}.webext-pref-body>:first-child{margin-top:0}.webext-pref-body>:last-child{margin-bottom:0}.webext-pref-number>input,.webext-pref-select>select,.webext-pref-text>input,.webext-pref-textarea>textarea{margin:.3em 0}.webext-pref-checkbox,.webext-pref-radio{margin:.5em 0;padding-left:1.5em}.webext-pref-checkbox>input,.webext-pref-radio>input{margin-left:-1.5em;margin-right:.5em;vertical-align:middle}.webext-pref-checkbox>label,.webext-pref-radio>label{cursor:pointer;vertical-align:middle}.webext-pref-checkbox>label:hover,.webext-pref-radio>label:hover{color:#707070}.webext-pref-checkbox-children,.webext-pref-radio-children{margin:.7em 0 0;padding:0;border-width:0}.webext-pref-checkbox-children[disabled],.webext-pref-radio-children[disabled]{opacity:.5}.webext-pref-checkbox-children>:first-child,.webext-pref-radio-children>:first-child{margin-top:0}.webext-pref-checkbox-children>:last-child,.webext-pref-radio-children>:last-child{margin-bottom:0}.webext-pref-checkbox-children>:last-child>:last-child,.webext-pref-radio-children>:last-child>:last-child{margin-bottom:0}.webext-pref-help{color:#969696}.responsive{white-space:normal}.responsive .dialog-body{margin:1em}.responsive .webext-pref-toolbar{display:block}.responsive .dialog-title{margin:0 0 1em 0}.responsive .webext-pref-toolbar button{font-size:1em}.responsive .webext-pref-nav{display:block}" + css; const root = iframe.contentDocument.querySelector(".dialog-body"); root.append(createUI(options)); destroyView = createBinding({ pref, root, ...options }); const title = document.createElement("h2"); title.className = "dialog-title"; title.textContent = getTitle(); iframe.contentDocument.querySelector(".webext-pref-toolbar").prepend(title); if (iframe.contentDocument.body.offsetWidth > modal.offsetWidth) { iframe.contentDocument.body.classList.add("responsive"); } // calc iframe size iframe.style = ` width: ${iframe.contentDocument.body.offsetWidth}px; height: ${iframe.contentDocument.body.scrollHeight}px; `; modal.classList.add("webext-pref-modal-open"); }; } function getTitle() { return typeof GM_info === "object" ? GM_info.script.name : GM.info.script.name; } } function prefDefault() { return { fuzzyIp: true, embedImage: true, embedImageExcludeElement: ".hljs, .highlight, .brush\\:", ignoreMustache: false, unicode: false, mail: true, newTab: false, standalone: false, boundaryLeft: "{[(\"'", boundaryRight: "'\")]},.;?!", excludeElement: ".highlight, .editbox, .brush\\:, .bdsug, .spreadsheetinfo", includeElement: "", timeout: 10000, triggerByPageLoad: false, triggerByNewNode: false, triggerByHover: true, triggerByClick: !supportHover(), maxRunTime: 100, customRules: "", }; } function supportHover() { return window.matchMedia("(hover)").matches; } var prefBody = getMessage => { return [ { type: "section", label: getMessage("optionsUrlMatcherLabel"), children: [ { key: "fuzzyIp", type: "checkbox", label: getMessage("optionsFuzzyIpLabel") }, { key: "ignoreMustache", type: "checkbox", label: getMessage("optionsIgnoreMustacheLabel") }, { key: "unicode", type: "checkbox", label: getMessage("optionsUnicodeLabel") }, { key: "mail", type: "checkbox", label: getMessage("optionsMailLabel") }, { key: "standalone", type: "checkbox", label: getMessage("optionsStandaloneLabel"), children: [ { key: "boundaryLeft", type: "text", label: getMessage("optionsBoundaryLeftLabel") }, { key: "boundaryRight", type: "text", label: getMessage("optionsBoundaryRightLabel") } ] }, { key: "customRules", type: "textarea", label: getMessage("optionsCustomRulesLabel"), learnMore: "https://github.com/eight04/linkify-plus-plus?tab=readme-ov-file#custom-rules" }, ] }, { type: "section", label: getMessage("optionsLinkifierLabel"), children: [ { key: "triggerByPageLoad", type: "checkbox", label: getMessage("optionsTriggerByPageLoadLabel") }, { key: "triggerByNewNode", type: "checkbox", label: getMessage("optionsTriggerByNewNodeLabel") }, { key: "triggerByHover", type: "checkbox", label: getMessage("optionsTriggerByHoverLabel") }, { key: "triggerByClick", type: "checkbox", label: getMessage("optionsTriggerByClickLabel") }, { key: "embedImage", type: "checkbox", label: getMessage("optionsEmbedImageLabel"), children: [ { key: "embedImageExcludeElement", type: "textarea", label: getMessage("optionsEmbedImageExcludeElementLabel"), validate: validateSelector } ] }, { key: "newTab", type: "checkbox", label: getMessage("optionsNewTabLabel") }, { key: "excludeElement", type: "textarea", label: getMessage("optionsExcludeElementLabel"), validate: validateSelector }, { key: "includeElement", type: "textarea", label: getMessage("optionsIncludeElementLabel"), validate: validateSelector }, { key: "timeout", type: "number", label: getMessage("optionsTimeoutLabel"), help: getMessage("optionsTimeoutHelp") }, { key: "maxRunTime", type: "number", label: getMessage("optionsMaxRunTimeLabel"), help: getMessage("optionsMaxRunTimeHelp") }, ] }, ]; function validateSelector(value) { if (value) { document.documentElement.matches(value); } } }; var maxLength = 24; var chars = "セール佛山ಭಾರತ集团在线한국ଭାରତভাৰতর八卦ישראלموقعবংল公司网站移动我#你москвақзнлйт联通рбгеקוםファッションストアசிங்கபூர商标店城дию家電中文信国國娱乐భారత్ලංකා购物クラウドભારતभारतम्ोसंगठन餐厅络у##食品飞利浦##灣手机الجزئرنیتبيپکسدغظحةڀ#門닷컴شكგე构健康ไทย招聘фລາວみんなευλ世界書籍ഭാരതംਭਾਰਤ址넷コム游戏ö企业息صط广东இலைநதயாհայ新加坡ف#务"; var table = { aaa: true, aarp: true, abb: true, abbott: true, abbvie: true, abc: true, able: true, abogado: true, abudhabi: true, ac: true, academy: true, accountant: true, accountants: true, aco: true, actor: true, ad: true, adult: true, ae: true, aeg: true, aero: true, aetna: true, af: true, afl: true, africa: true, ag: true, agency: true, ai: true, aig: true, airbus: true, airforce: true, akdn: true, al: true, allfinanz: true, allstate: true, ally: true, alsace: true, alstom: true, am: true, amazon: true, americanexpress: true, amex: true, amfam: true, amica: true, amsterdam: true, analytics: true, android: true, anz: true, ao: true, apartments: true, app: true, apple: true, aq: true, aquarelle: true, ar: true, archi: true, army: true, arpa: true, art: true, arte: true, as: true, asia: true, associates: true, at: true, attorney: true, au: true, auction: true, audi: true, audio: true, auspost: true, auto: true, autos: true, aw: true, aws: true, ax: true, axa: true, az: true, azure: true, ba: true, baby: true, band: true, bank: true, bar: true, barcelona: true, barclaycard: true, barclays: true, bargains: true, basketball: true, bauhaus: true, bayern: true, bb: true, bbc: true, bbva: true, bcn: true, bd: true, be: true, beauty: true, beer: true, bentley: true, berlin: true, best: true, bet: true, bf: true, bg: true, bh: true, bi: true, bible: true, bid: true, bike: true, bing: true, bingo: true, bio: true, biz: true, bj: true, black: true, blackfriday: true, blog: true, bloomberg: true, blue: true, bm: true, bmw: true, bn: true, bnpparibas: true, bo: true, boats: true, bond: true, boo: true, bostik: true, boston: true, bot: true, boutique: true, box: true, br: true, bradesco: true, bridgestone: true, broadway: true, broker: true, brother: true, brussels: true, bs: true, bt: true, build: true, builders: true, business: true, buzz: true, bw: true, by: true, bz: true, bzh: true, ca: true, cab: true, cafe: true, cam: true, camera: true, camp: true, canon: true, capetown: true, capital: true, car: true, cards: true, care: true, career: true, careers: true, cars: true, casa: true, "case": true, cash: true, casino: true, cat: true, catering: true, catholic: true, cba: true, cbn: true, cc: true, cd: true, center: true, ceo: true, cern: true, cf: true, cfa: true, cfd: true, cg: true, ch: true, chanel: true, channel: true, charity: true, chase: true, chat: true, cheap: true, chintai: true, christmas: true, church: true, ci: true, cisco: true, citi: true, citic: true, city: true, ck: true, cl: true, claims: true, cleaning: true, click: true, clinic: true, clothing: true, cloud: true, club: true, clubmed: true, cm: true, cn: true, co: true, coach: true, codes: true, coffee: true, college: true, cologne: true, com: true, commbank: true, community: true, company: true, compare: true, computer: true, condos: true, construction: true, consulting: true, contact: true, contractors: true, cooking: true, cool: true, coop: true, corsica: true, country: true, coupons: true, courses: true, cpa: true, cr: true, credit: true, creditcard: true, creditunion: true, cricket: true, crown: true, crs: true, cruises: true, cu: true, cuisinella: true, cv: true, cw: true, cx: true, cy: true, cymru: true, cyou: true, cz: true, dad: true, dance: true, date: true, dating: true, day: true, de: true, deal: true, dealer: true, deals: true, degree: true, delivery: true, dell: true, deloitte: true, democrat: true, dental: true, dentist: true, desi: true, design: true, dev: true, dhl: true, diamonds: true, diet: true, digital: true, direct: true, directory: true, discount: true, discover: true, diy: true, dj: true, dk: true, dm: true, "do": true, doctor: true, dog: true, domains: true, download: true, dubai: true, dupont: true, durban: true, dvag: true, dz: true, earth: true, ec: true, eco: true, edeka: true, edu: true, education: true, ee: true, eg: true, email: true, emerck: true, energy: true, engineer: true, engineering: true, enterprises: true, equipment: true, er: true, ericsson: true, erni: true, es: true, esq: true, estate: true, et: true, eu: true, eurovision: true, eus: true, events: true, exchange: true, expert: true, exposed: true, express: true, extraspace: true, fage: true, fail: true, fairwinds: true, faith: true, family: true, fan: true, fans: true, farm: true, fashion: true, feedback: true, ferrero: true, fi: true, film: true, finance: true, financial: true, firmdale: true, fish: true, fishing: true, fit: true, fitness: true, fj: true, fk: true, flickr: true, flights: true, flir: true, florist: true, flowers: true, fm: true, fo: true, foo: true, food: true, football: true, ford: true, forex: true, forsale: true, forum: true, foundation: true, fox: true, fr: true, fresenius: true, frl: true, frogans: true, fujitsu: true, fun: true, fund: true, furniture: true, futbol: true, fyi: true, ga: true, gal: true, gallery: true, game: true, games: true, garden: true, gay: true, gd: true, gdn: true, ge: true, gea: true, gent: true, genting: true, gf: true, gg: true, gh: true, gi: true, gift: true, gifts: true, gives: true, giving: true, gl: true, glass: true, gle: true, global: true, globo: true, gm: true, gmail: true, gmbh: true, gmo: true, gmx: true, gn: true, godaddy: true, gold: true, golf: true, goog: true, google: true, gop: true, gov: true, gp: true, gq: true, gr: true, grainger: true, graphics: true, gratis: true, green: true, gripe: true, group: true, gs: true, gt: true, gu: true, gucci: true, guide: true, guitars: true, guru: true, gw: true, gy: true, hair: true, hamburg: true, haus: true, health: true, healthcare: true, help: true, helsinki: true, here: true, hermes: true, hiphop: true, hisamitsu: true, hitachi: true, hiv: true, hk: true, hm: true, hn: true, hockey: true, holdings: true, holiday: true, homes: true, honda: true, horse: true, hospital: true, host: true, hosting: true, hotmail: true, house: true, how: true, hr: true, hsbc: true, ht: true, hu: true, hyatt: true, hyundai: true, ice: true, icu: true, id: true, ie: true, ieee: true, ifm: true, ikano: true, il: true, im: true, imamat: true, immo: true, immobilien: true, "in": true, inc: true, industries: true, info: true, ing: true, ink: true, institute: true, insurance: true, insure: true, int: true, international: true, investments: true, io: true, ipiranga: true, iq: true, ir: true, irish: true, is: true, ismaili: true, ist: true, istanbul: true, it: true, itau: true, itv: true, jaguar: true, java: true, jcb: true, je: true, jetzt: true, jewelry: true, jio: true, jll: true, jm: true, jmp: true, jnj: true, jo: true, jobs: true, joburg: true, jp: true, jpmorgan: true, jprs: true, juegos: true, kaufen: true, ke: true, kfh: true, kg: true, kh: true, ki: true, kia: true, kids: true, kim: true, kitchen: true, kiwi: true, km: true, kn: true, koeln: true, komatsu: true, kp: true, kpmg: true, kpn: true, kr: true, krd: true, kred: true, kw: true, ky: true, kyoto: true, kz: true, la: true, lamborghini: true, lancaster: true, land: true, landrover: true, lanxess: true, lat: true, latrobe: true, law: true, lawyer: true, lb: true, lc: true, lease: true, leclerc: true, legal: true, lexus: true, lgbt: true, li: true, lidl: true, life: true, lifestyle: true, lighting: true, lilly: true, limited: true, limo: true, lincoln: true, link: true, live: true, living: true, lk: true, llc: true, loan: true, loans: true, locker: true, locus: true, lol: true, london: true, lotto: true, love: true, lr: true, ls: true, lt: true, ltd: true, ltda: true, lu: true, lundbeck: true, luxe: true, luxury: true, lv: true, ly: true, ma: true, madrid: true, maif: true, maison: true, makeup: true, man: true, management: true, mango: true, market: true, marketing: true, markets: true, marriott: true, mattel: true, mba: true, mc: true, md: true, me: true, med: true, media: true, meet: true, melbourne: true, meme: true, memorial: true, men: true, menu: true, mg: true, mh: true, miami: true, microsoft: true, mil: true, mini: true, mit: true, mk: true, ml: true, mlb: true, mm: true, mma: true, mn: true, mo: true, mobi: true, moda: true, moe: true, moi: true, mom: true, monash: true, money: true, monster: true, mortgage: true, moscow: true, motorcycles: true, mov: true, movie: true, mp: true, mq: true, mr: true, ms: true, mt: true, mtn: true, mtr: true, mu: true, museum: true, music: true, mv: true, mw: true, mx: true, my: true, mz: true, na: true, nab: true, nagoya: true, name: true, navy: true, nc: true, ne: true, nec: true, net: true, netbank: true, network: true, neustar: true, "new": true, news: true, next: true, nexus: true, nf: true, ng: true, ngo: true, nhk: true, ni: true, nico: true, nike: true, ninja: true, nissan: true, nl: true, no: true, nokia: true, now: true, nowruz: true, np: true, nr: true, nra: true, nrw: true, ntt: true, nu: true, nyc: true, nz: true, observer: true, office: true, okinawa: true, om: true, omega: true, one: true, ong: true, onl: true, online: true, ooo: true, oracle: true, orange: true, org: true, organic: true, osaka: true, otsuka: true, ovh: true, pa: true, page: true, panasonic: true, paris: true, partners: true, parts: true, party: true, pe: true, pet: true, pf: true, pfizer: true, pg: true, ph: true, pharmacy: true, phd: true, philips: true, photo: true, photography: true, photos: true, physio: true, pics: true, pictet: true, pictures: true, ping: true, pink: true, pioneer: true, pizza: true, pk: true, pl: true, place: true, play: true, plumbing: true, plus: true, pm: true, pn: true, pohl: true, poker: true, politie: true, porn: true, post: true, pr: true, praxi: true, press: true, prime: true, pro: true, productions: true, prof: true, promo: true, properties: true, property: true, protection: true, pru: true, prudential: true, ps: true, pt: true, pub: true, pw: true, pwc: true, py: true, qa: true, qpon: true, quebec: true, quest: true, racing: true, radio: true, re: true, realestate: true, realtor: true, realty: true, recipes: true, red: true, redstone: true, rehab: true, reise: true, reisen: true, reit: true, ren: true, rent: true, rentals: true, repair: true, report: true, republican: true, rest: true, restaurant: true, review: true, reviews: true, rexroth: true, rich: true, ricoh: true, rio: true, rip: true, ro: true, rocks: true, rodeo: true, rogers: true, rs: true, rsvp: true, ru: true, rugby: true, ruhr: true, run: true, rw: true, ryukyu: true, sa: true, saarland: true, sale: true, salon: true, samsung: true, sandvik: true, sandvikcoromant: true, sanofi: true, sap: true, sarl: true, saxo: true, sb: true, sbi: true, sbs: true, sc: true, scb: true, schaeffler: true, schmidt: true, school: true, schule: true, schwarz: true, science: true, scot: true, sd: true, se: true, seat: true, security: true, select: true, sener: true, services: true, seven: true, sew: true, sex: true, sexy: true, sfr: true, sg: true, sh: true, sharp: true, shell: true, shiksha: true, shoes: true, shop: true, shopping: true, show: true, si: true, singles: true, site: true, sk: true, ski: true, skin: true, sky: true, skype: true, sl: true, sm: true, smart: true, sn: true, sncf: true, so: true, soccer: true, social: true, softbank: true, software: true, sohu: true, solar: true, solutions: true, sony: true, soy: true, spa: true, space: true, sport: true, sr: true, srl: true, ss: true, st: true, stada: true, statebank: true, statefarm: true, stc: true, stockholm: true, storage: true, store: true, stream: true, studio: true, study: true, style: true, su: true, sucks: true, supplies: true, supply: true, support: true, surf: true, surgery: true, suzuki: true, sv: true, swatch: true, swiss: true, sx: true, sy: true, sydney: true, systems: true, sz: true, taipei: true, target: true, tatamotors: true, tatar: true, tattoo: true, tax: true, taxi: true, tc: true, td: true, team: true, tech: true, technology: true, tel: true, temasek: true, tennis: true, teva: true, tf: true, tg: true, th: true, theater: true, theatre: true, tickets: true, tienda: true, tips: true, tires: true, tirol: true, tj: true, tk: true, tl: true, tm: true, tn: true, to: true, today: true, tokyo: true, tools: true, top: true, toray: true, toshiba: true, total: true, tours: true, town: true, toyota: true, toys: true, tr: true, trade: true, trading: true, training: true, travel: true, travelers: true, trust: true, tt: true, tube: true, tui: true, tv: true, tvs: true, tw: true, tz: true, ua: true, ug: true, uk: true, unicom: true, university: true, uno: true, uol: true, us: true, uy: true, uz: true, va: true, vacations: true, vana: true, vanguard: true, vc: true, ve: true, vegas: true, ventures: true, versicherung: true, vet: true, vg: true, vi: true, viajes: true, video: true, vig: true, villas: true, vin: true, vip: true, vision: true, vivo: true, vlaanderen: true, vn: true, vodka: true, vote: true, voting: true, voto: true, voyage: true, vu: true, wales: true, walter: true, wang: true, watch: true, watches: true, webcam: true, weber: true, website: true, wed: true, wedding: true, weir: true, wf: true, whoswho: true, wien: true, wiki: true, williamhill: true, win: true, windows: true, wine: true, wme: true, woodside: true, work: true, works: true, world: true, ws: true, wtf: true, xbox: true, xin: true, "xn--1ck2e1b": true, "xn--1qqw23a": true, "xn--2scrj9c": true, "xn--3bst00m": true, "xn--3ds443g": true, "xn--3e0b707e": true, "xn--3hcrj9c": true, "xn--45br5cyl": true, "xn--45brj9c": true, "xn--45q11c": true, "xn--4dbrk0ce": true, "xn--4gbrim": true, "xn--54b7fta0cc": true, "xn--55qx5d": true, "xn--5tzm5g": true, "xn--6frz82g": true, "xn--6qq986b3xl": true, "xn--80adxhks": true, "xn--80ao21a": true, "xn--80asehdb": true, "xn--80aswg": true, "xn--8y0a063a": true, "xn--90a3ac": true, "xn--90ae": true, "xn--90ais": true, "xn--9dbq2a": true, "xn--bck1b9a5dre4c": true, "xn--c1avg": true, "xn--cck2b3b": true, "xn--clchc0ea0b2g2a9###": true, "xn--czr694b": true, "xn--czrs0t": true, "xn--czru2d": true, "xn--d1acj3b": true, "xn--d1alf": true, "xn--e1a4c": true, "xn--fct429k": true, "xn--fiq228c5hs": true, "xn--fiq64b": true, "xn--fiqs8s": true, "xn--fiqz9s": true, "xn--fjq720a": true, "xn--fpcrj9c3d": true, "xn--fzc2c9e2c": true, "xn--g2xx48c": true, "xn--gckr3f0f": true, "xn--gecrj9c": true, "xn--h2breg3eve": true, "xn--h2brj9c": true, "xn--h2brj9c8c": true, "xn--hxt814e": true, "xn--i1b6b1a6a2e": true, "xn--imr513n": true, "xn--io0a7i": true, "xn--j1amh": true, "xn--j6w193g": true, "xn--jvr189m": true, "xn--kcrx77d1x4a": true, "xn--kprw13d": true, "xn--kpry57d": true, "xn--kput3i": true, "xn--l1acc": true, "xn--lgbbat1ad8j": true, "xn--mgb9awbf": true, "xn--mgba3a4f16a": true, "xn--mgbaam7a8h": true, "xn--mgbab2bd": true, "xn--mgbah1a3hjkrd": true, "xn--mgbai9azgqp6j": true, "xn--mgbayh7gpa": true, "xn--mgbbh1a": true, "xn--mgbc0a9azcg": true, "xn--mgbca7dzdo": true, "xn--mgbcpq6gpa1a": true, "xn--mgberp4a5d4ar": true, "xn--mgbgu82a": true, "xn--mgbpl2fh": true, "xn--mgbtx2b": true, "xn--mix891f": true, "xn--mk1bu44c": true, "xn--ngbc5azd": true, "xn--ngbe9e0a": true, "xn--node": true, "xn--nqv7f": true, "xn--nyqy26a": true, "xn--o3cw4h": true, "xn--ogbpf8fl": true, "xn--otu796d": true, "xn--p1acf": true, "xn--p1ai": true, "xn--pgbs0dh": true, "xn--q7ce6a": true, "xn--q9jyb4c": true, "xn--qxa6a": true, "xn--qxam": true, "xn--rhqv96g": true, "xn--rovu88b": true, "xn--rvc1e0am3e": true, "xn--s9brj9c": true, "xn--ses554g": true, "xn--t60b56a": true, "xn--tckwe": true, "xn--unup4y": true, "xn--vermgensberatung-pwb": true, "xn--vhquv": true, "xn--vuq861b": true, "xn--wgbh1c": true, "xn--wgbl6a": true, "xn--xhq521b": true, "xn--xkc2al3hye2a": true, "xn--xkc2dl3a5ee0h": true, "xn--y9a3aq": true, "xn--yfro4i67o": true, "xn--ygbi2ammx": true, "xn--zfr164b": true, xxx: true, xyz: true, yachts: true, yahoo: true, yandex: true, ye: true, yodobashi: true, yoga: true, yokohama: true, youtube: true, yt: true, za: true, zappos: true, zara: true, zip: true, zm: true, zone: true, zuerich: true, zw: true, "セール": true, "佛山": true, "ಭಾರತ": true, "集团": true, "在线": true, "한국": true, "ଭାରତ": true, "ভাৰত": true, "ভারত": true, "八卦": true, "ישראל": true, "موقع": true, "বাংলা": true, "公司": true, "网站": true, "移动": true, "我#你": true, "москва": true, "қаз": true, "онлайн": true, "сайт": true, "联通": true, "срб": true, "бг": true, "бел": true, "קום": true, "ファッション": true, "орг": true, "ストア": true, "சிங்கப்பூர்": true, "商标": true, "商店": true, "商城": true, "дети": true, "мкд": true, "ею": true, "家電": true, "中文网": true, "中信": true, "##": true, "##": true, "娱乐": true, "భారత్": true, "ලංකා": true, "购物": true, "クラウド": true, "ભારત": true, "भारतम्": true, "भारत": true, "भारोत": true, "网店": true, "संगठन": true, "餐厅": true, "网络": true, "укр": true, "##": true, "食品": true, "飞利浦": true, "##": true, "##": true, "手机": true, "мон": true, "الجزائر": true, "عمان": true, "ایران": true, "امارات": true, "بازار": true, "موريتانيا": true, "پاکستان": true, "الاردن": true, "بارت": true, "المغرب": true, "ابوظبي": true, "البحرين": true, "السعودية": true, "ڀارت": true, "سودان": true, "عراق": true, "#門": true, "닷컴": true, "شبكة": true, "بيتك": true, "გე": true, "机构": true, "健康": true, "ไทย": true, "سورية": true, "招聘": true, "рус": true, "рф": true, "تونس": true, "ລາວ": true, "みんな": true, "ευ": true, "ελ": true, "世界": true, "書籍": true, "ഭാരതം": true, "ਭਾਰਤ": true, "网址": true, "닷넷": true, "コム": true, "游戏": true, "vermögensberatung": true, "企业": true, "信息": true, "مصر": true, "قطر": true, "广东": true, "இலங்கை": true, "இந்தியா": true, "հայ": true, "新加坡": true, "فلسطين": true, "#务": true, onion: true }; var RE = { PROTOCOL: "([a-z][-a-z*]+://)?", USER: "(?:([\\w:.+-]+)@)?", DOMAIN_UNI: `([a-z0-9-.\\u00A0-\\uFFFF]+\\.[a-z0-9-${chars}]{1,${maxLength}})`, DOMAIN: `([a-z0-9-.]+\\.[a-z0-9-]{1,${maxLength}})`, PORT: "(:\\d+\\b)?", PATH_UNI: "([/?#]\\S*)?", PATH: "([/?#][\\w-.~!$&*+;=:@%/?#(),'\\[\\]]*)?" }, TLD_TABLE = table; function regexEscape(text) { return text.replace(/[[\]\\^-]/g, "\\$&"); } function buildRegex({ unicode = false, customRules = [], standalone = false, boundaryLeft, boundaryRight }) { var pattern = RE.PROTOCOL + RE.USER; if (unicode) { pattern += RE.DOMAIN_UNI + RE.PORT + RE.PATH_UNI; } else { pattern += RE.DOMAIN + RE.PORT + RE.PATH; } if (customRules.length) { pattern = "(?:(" + customRules.join("|") + ")|" + pattern + ")"; } else { pattern = "()" + pattern; } var prefix, suffix, invalidSuffix; if (standalone) { if (boundaryLeft) { prefix = "((?:^|\\s)[" + regexEscape(boundaryLeft) + "]*?)"; } else { prefix = "(^|\\s)"; } if (boundaryRight) { suffix = "([" + regexEscape(boundaryRight) + "]*(?:$|\\s))"; } else { suffix = "($|\\s)"; } invalidSuffix = "[^\\s" + regexEscape(boundaryRight) + "]"; } else { prefix = "(^|\\b|_)"; suffix = "()"; } pattern = prefix + pattern + suffix; return { url: new RegExp(pattern, "igm"), invalidSuffix: invalidSuffix && new RegExp(invalidSuffix), mustache: /\{\{[\s\S]+?\}\}/g }; } function pathStrip(m, re, repl) { var s = m.path.replace(re, repl); if (s == m.path) return; m.end -= m.path.length - s.length; m.suffix = m.path.slice(s.length) + m.suffix; m.path = s; } function pathStripQuote(m, c) { var i = 0, s = m.path, end, pos = 0; if (!s.endsWith(c)) return; while ((pos = s.indexOf(c, pos)) >= 0) { if (i % 2) { end = null; } else { end = pos; } pos++; i++; } if (!end) return; m.end -= s.length - end; m.path = s.slice(0, end); m.suffix = s.slice(end) + m.suffix; } function pathStripBrace(m, left, right) { var str = m.path, re = new RegExp("[\\" + left + "\\" + right + "]", "g"), match, count = 0, end; // Match loop while ((match = re.exec(str))) { if (count % 2 == 0) { end = match.index; if (match[0] == right) { break; } } else { if (match[0] == left) { break; } } count++; } if (!match && count % 2 == 0) { return; } m.end -= m.path.length - end; m.path = str.slice(0, end); m.suffix = str.slice(end) + m.suffix; } function isIP(s) { var m, i; if (!(m = s.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/))) { return false; } for (i = 1; i < m.length; i++) { if (+m[i] > 255 || (m[i].length > 1 && m[i][0] == "0")) { return false; } } return true; } function inTLDS(domain) { var match = domain.match(/\.([^.]+)$/); if (!match) { return false; } var key = match[1].toLowerCase(); // eslint-disable-next-line no-prototype-builtins return TLD_TABLE.hasOwnProperty(key); } class UrlMatcher { constructor(options = {}) { this.options = options; this.regex = buildRegex(options); } *match(text) { var { fuzzyIp = true, ignoreMustache = false, mail = true } = this.options, { url, invalidSuffix, mustache } = this.regex, urlLastIndex, mustacheLastIndex; mustache.lastIndex = 0; url.lastIndex = 0; var mustacheMatch, mustacheRange; if (ignoreMustache) { mustacheMatch = mustache.exec(text); if (mustacheMatch) { mustacheRange = { start: mustacheMatch.index, end: mustache.lastIndex }; } } var urlMatch; while ((urlMatch = url.exec(text))) { const r###lt = { start: 0, end: 0, text: "", url: "", prefix: urlMatch[1], custom: urlMatch[2], protocol: urlMatch[3], auth: urlMatch[4] || "", domain: urlMatch[5], port: urlMatch[6] || "", path: urlMatch[7] || "", suffix: urlMatch[8] }; if (r###lt.custom) { r###lt.start = urlMatch.index; r###lt.end = url.lastIndex; r###lt.text = r###lt.url = urlMatch[0]; } else { r###lt.start = urlMatch.index + r###lt.prefix.length; r###lt.end = url.lastIndex - r###lt.suffix.length; } if (mustacheRange && mustacheRange.end <= r###lt.start) { mustacheMatch = mustache.exec(text); if (mustacheMatch) { mustacheRange.start = mustacheMatch.index; mustacheRange.end = mustache.lastIndex; } else { mustacheRange = null; } } // ignore urls inside mustache pair if (mustacheRange && r###lt.start < mustacheRange.end && r###lt.end >= mustacheRange.start) { continue; } if (!r###lt.custom) { // adjust path and suffix if (r###lt.path) { // Strip BBCode pathStrip(r###lt, /\[\/?(b|i|u|url|img|quote|code|size|color)\].*/i, ""); // Strip braces pathStripBrace(r###lt, "(", ")"); pathStripBrace(r###lt, "[", "]"); pathStripBrace(r###lt, "{", "}"); // Strip quotes pathStripQuote(r###lt, "'"); pathStripQuote(r###lt, '"'); // Remove trailing ".,?" pathStrip(r###lt, /(^|[^-_])[.,?]+$/, "$1"); } // check suffix if (invalidSuffix && invalidSuffix.test(r###lt.suffix)) { if (/\s$/.test(r###lt.suffix)) { url.lastIndex--; } continue; } // ignore fuzzy ip if (!fuzzyIp && isIP(r###lt.domain) && !r###lt.protocol && !r###lt.auth && !r###lt.path) { continue; } // mailto protocol if (!r###lt.protocol && r###lt.auth) { var matchMail = r###lt.auth.match(/^mailto:(.+)/); if (matchMail) { r###lt.protocol = "mailto:"; r###lt.auth = matchMail[1]; } } // http alias if (r###lt.protocol && r###lt.protocol.match(/^(hxxp|h\*\*p|ttp)/)) { r###lt.protocol = "http://"; } // guess protocol if (!r###lt.protocol) { var domainMatch; if ((domainMatch = r###lt.domain.match(/^(ftp|irc)/))) { r###lt.protocol = domainMatch[0] + "://"; } else if (r###lt.domain.match(/^(www|web)/)) { r###lt.protocol = "http://"; } else if (r###lt.auth && r###lt.auth.indexOf(":") < 0 && !r###lt.path) { r###lt.protocol = "mailto:"; } else { r###lt.protocol = "http://"; } } // ignore mail if (!mail && r###lt.protocol === "mailto:") { continue; } // verify domain if (!isIP(r###lt.domain)) { if (/^(http|https|mailto)/.test(r###lt.protocol) && !inTLDS(r###lt.domain)) { continue; } const invalidLabel = getInvalidLabel(r###lt.domain); if (invalidLabel) { url.lastIndex = urlMatch.index + invalidLabel.index + 1; continue; } } // Create URL r###lt.url = r###lt.protocol + (r###lt.auth && r###lt.auth + "@") + r###lt.domain + r###lt.port + r###lt.path; r###lt.text = text.slice(r###lt.start, r###lt.end); } // since regex is shared with other parse generators, cache lastIndex position and restore later mustacheLastIndex = mustache.lastIndex; urlLastIndex = url.lastIndex; yield r###lt; url.lastIndex = urlLastIndex; mustache.lastIndex = mustacheLastIndex; } } } function getInvalidLabel(domain) { // https://tools.ietf.org/html/rfc1035 // https://serverfault.com/questions/638260/is-it-valid-for-a-hostname-to-start-with-a-digit let index = 0; const parts = domain.split("."); for (const part of parts) { if ( !part || part.startsWith("-") || part.endsWith("-") ) { return { index, value: part }; } index += part.length + 1; } } /* eslint-env browser */ var INVALID_TAGS = { a: true, noscript: true, option: true, script: true, style: true, textarea: true, svg: true, canvas: true, button: true, select: true, template: true, meter: true, progress: true, math: true, time: true }; class Pos { constructor(container, offset, i = 0) { this.container = container; this.offset = offset; this.i = i; } add(change) { var cont = this.container, offset = this.offset; this.i += change; // If the container is #text.parentNode if (cont.childNodes.length) { cont = cont.childNodes[offset]; offset = 0; } // If the container is #text while (cont) { if (cont.nodeType == 3) { if (!cont.LEN) { cont.LEN = cont.nodeValue.length; } if (offset + change <= cont.LEN) { this.container = cont; this.offset = offset + change; return; } change = offset + change - cont.LEN; offset = 0; } cont = cont.nextSibling; } } moveTo(offset) { this.add(offset - this.i); } } function cloneContents(range) { if (range.startContainer == range.endContainer) { return document.createTextNode(range.toString()); } return range.cloneContents(); } var DEFAULT_OPTIONS = { maxRunTime: 100, timeout: 10000, newTab: true, noOpener: true, embedImage: true, recursive: true, }; class Linkifier extends EventLite { constructor(root, options = {}) { super(); if (!(root instanceof Node)) { options = root; root = options.root; } this.root = root; this.options = Object.assign({}, DEFAULT_OPTIONS, options); this.aborted = false; } start() { var time = Date.now, startTime = time(), chunks = this.generateChunks(); var next = () => { if (this.aborted) { this.emit("error", new Error("Aborted")); return; } var chunkStart = time(), now; do { if (chunks.next().done) { this.emit("complete", time() - startTime); return; } } while ((now = time()) - chunkStart < this.options.maxRunTime); if (now - startTime > this.options.timeout) { this.emit("error", new Error(`max execution time exceeded: ${now - startTime}, on ${this.root}`)); return; } setTimeout(next); }; setTimeout(next); } abort() { this.aborted = true; } *generateRanges() { var {validator, recursive} = this.options; var filter = { acceptNode: function(node) { if (validator && !validator(node)) { return NodeFilter.FILTER_REJECT; } if (INVALID_TAGS[node.localName]) { return NodeFilter.FILTER_REJECT; } if (node.localName == "wbr") { return NodeFilter.FILTER_ACCEPT; } if (node.nodeType == 3) { return NodeFilter.FILTER_ACCEPT; } return recursive ? NodeFilter.FILTER_SKIP : NodeFilter.FILTER_REJECT; } }; // Generate linkified ranges. var walker = document.createTreeWalker( this.root, NodeFilter.SHOW_TEXT + NodeFilter.SHOW_ELEMENT, filter ), start, end, current, range; end = start = walker.nextNode(); if (!start) { return; } range = document.createRange(); range.setStartBefore(start); while ((current = walker.nextNode())) { if (end.nextSibling == current) { end = current; continue; } range.setEndAfter(end); yield range; end = start = current; range.setStartBefore(start); } range.setEndAfter(end); yield range; } *generateChunks() { var {matcher} = this.options; for (var range of this.generateRanges()) { var frag = null, pos = null, text = range.toString(), textRange = null; for (var r###lt of matcher.match(text)) { if (!frag) { frag = document.createDocumentFragment(); pos = new Pos(range.startContainer, range.startOffset); textRange = range.cloneRange(); } // clone text pos.moveTo(r###lt.start); textRange.setEnd(pos.container, pos.offset); frag.appendChild(cloneContents(textRange)); // clone link textRange.collapse(); pos.moveTo(r###lt.end); textRange.setEnd(pos.container, pos.offset); var content = cloneContents(textRange), link = this.buildLink(r###lt, content); textRange.collapse(); frag.appendChild(link); this.emit("link", {link, range, r###lt, content}); } if (pos) { pos.moveTo(text.length); textRange.setEnd(pos.container, pos.offset); frag.appendChild(cloneContents(textRange)); range.deleteContents(); range.insertNode(frag); } yield; } } buildLink(r###lt, content) { var {newTab, embedImage, noOpener} = this.options; var link = document.createElement("a"); link.href = r###lt.url; link.title = "Linkify Plus Plus"; link.className = "linkifyplus"; if (newTab) { link.target = "_blank"; } if (noOpener) { link.rel = "noopener"; } var child; if (embedImage && /^[^?#]+\.(?:jpg|jpeg|png|apng|gif|svg|webp)(?:$|[?#])/i.test(r###lt.url)) { child = new Image; child.src = r###lt.url; child.alt = r###lt.text; } else { child = content; } link.appendChild(child); return link; } } function linkify(...args) { return new Promise((resolve, reject) => { var linkifier = new Linkifier(...args); linkifier.on("error", reject); linkifier.on("complete", resolve); for (var key of Object.keys(linkifier.options)) { if (key.startsWith("on")) { linkifier.on(key.slice(2), linkifier.options[key]); } } linkifier.start(); }); } const processedNodes = new WeakSet; const nodeValidationCache = new WeakMap; // Node -> boolean async function linkifyRoot(root, options, useIncludeElement = true) { if (validRoot(root, options.validator)) { processedNodes.add(root); await linkify({...options, root, recursive: true}); } if (options.includeElement && useIncludeElement) { for (const el of root.querySelectorAll(options.includeElement)) { await linkifyRoot(el, options, false); } } } function validRoot(node, validator) { if (processedNodes.has(node)) { return false; } return getValidation(node); function getValidation(p) { if (!p.parentNode) { return false; } let r = nodeValidationCache.get(p); if (r === undefined) { if (validator.isIncluded(p)) { r = true; } else if (validator.isExcluded(p)) { r = false; } else if (p.parentNode != document.documentElement) { r = getValidation(p.parentNode); } else { r = true; } nodeValidationCache.set(p, r); } return r; } } function prepareDocument() { // wait till everything is ready return prepareBody().then(prepareApp); function prepareApp() { const appRoot = document.querySelector("[data-server-rendered]"); if (!appRoot) { return; } return new Promise(resolve => { const onChange = () => { if (!appRoot.hasAttribute("data-server-rendered")) { resolve(); observer.disconnect(); } }; const observer = new MutationObserver(onChange); observer.observe(appRoot, {attributes: true}); }); } function prepareBody() { if (document.readyState !== "loading") { return Promise.resolve(); } return new Promise(resolve => { // https://github.com/Tampermonkey/tampermonkey/issues/485 document.addEventListener("DOMContentLoaded", resolve, {once: true}); }); } } // import {processedNodes} from "./cache.mjs"; var load = { key: "triggerByPageLoad", enable: async options => { await prepareDocument(); await linkifyRoot(document.body, options); }, disable: () => {} }; let options$1; const EVENTS$1 = [ ["click", handle$1, {passive: true}], ]; function handle$1(e) { const el = e.target; if (validRoot(el, options$1.validator)) { processedNodes.add(el); linkify({...options$1, root: el, recursive: false}); } } function enable$2(_options) { options$1 = _options; for (const [event, handler, options] of EVENTS$1) { document.addEventListener(event, handler, options); } } function disable$2() { for (const [event, handler, options] of EVENTS$1) { document.removeEventListener(event, handler, options); } } var click = { key: "triggerByClick", enable: enable$2, disable: disable$2 }; let options; const EVENTS = [ // catch the first mousemove event since mouseover doesn't fire at page refresh ["mousemove", handle, {passive: true, once: true}], ["mouseover", handle, {passive: true}] ]; function handle(e) { const el = e.target; if (validRoot(el, options.validator)) { processedNodes.add(el); linkify({...options, root: el, recursive: false}); } } function enable$1(_options) { options = _options; for (const [event, handler, options] of EVENTS) { document.addEventListener(event, handler, options); } } function disable$1() { for (const [event, handler, options] of EVENTS) { document.removeEventListener(event, handler, options); } } var hover = { key: "triggerByHover", enable: enable$1, disable: disable$1 }; const MAX_PROCESSES = 100; let processes = 0; let observer; async function enable(options) { await prepareDocument(); observer = new MutationObserver(function(mutations){ // Filter out mutations generated by LPP var lastRecord = mutations[mutations.length - 1], nodes = lastRecord.addedNodes, i; if (nodes.length >= 2) { for (i = 0; i < 2; i++) { if (nodes[i].className == "linkifyplus") { return; } } } for (var record of mutations) { for (const node of record.addedNodes) { if (node.nodeType === 1 && !processedNodes.has(node)) { if (processes >= MAX_PROCESSES) { throw new Error("Too many processes"); } processes++; linkifyRoot(node, options) .finally(() => { processes--; }); } } } }); observer.observe(document.body, { childList: true, subtree: true }); } async function disable() { await prepareDocument(); observer && observer.disconnect(); } var mutation = { key: "triggerByNewNode", enable, disable }; var triggers = [load, click, hover, mutation]; function createValidator({includeElement, excludeElement}) { const f = function(node) { if (processedNodes.has(node)) { return false; } if (node.isContentEditable) { return false; } if (node.matches) { if (includeElement && node.matches(includeElement)) { return true; } if (excludeElement && node.matches(excludeElement)) { return false; } } return true; }; f.isIncluded = node => { return includeElement && node.matches(includeElement); }; f.isExcluded = node => { if (INVALID_TAGS[node.localName]) { return true; } return excludeElement && node.matches(excludeElement); }; return f; } function stringToList(value) { value = value.trim(); if (!value) { return []; } return value.split(/\s*\n\s*/g); } function createOptions(pref) { const options = {}; pref.on("change", update); update(pref.getAll()); return options; function update(changes) { Object.assign(options, changes); options.validator = createValidator(options); if (typeof options.customRules === "string") { options.customRules = stringToList(options.customRules); } options.matcher = new UrlMatcher(options); options.onlink = options.embedImageExcludeElement ? onlink : null; } function onlink({link, range, content}) { if (link.childNodes[0].localName !== "img" || !options.embedImageExcludeElement) { return; } var parent = range.startContainer; // it might be a text node if (!parent.closest) { parent = parent.parentNode; } if (!parent.closest(options.embedImageExcludeElement)) return; // remove image link.innerHTML = ""; link.appendChild(content); } } async function startLinkifyPlusPlus(getPref) { // Limit contentType to specific content type if ( document.contentType && !["text/plain", "text/html", "application/xhtml+xml"].includes(document.contentType) ) { return; } const pref = await getPref(); const options = createOptions(pref); for (const trigger of triggers) { if (pref.get(trigger.key)) { trigger.enable(options); } } pref.on("change", changes => { for (const trigger of triggers) { if (changes[trigger.key] === true) { trigger.enable(options); } if (changes[trigger.key] === false) { trigger.disable(); } } }); } function getMessageFactory() { return (key, params) => { if (!params) { return translate[key]; } if (!Array.isArray(params)) { params = [params]; } return translate[key].replace(/\$\d/g, m => { const index = Number(m.slice(1)); return params[index - 1]; }); }; } startLinkifyPlusPlus(async () => { const getMessage = getMessageFactory(); const pref = GM_webextPref({ default: prefDefault(), body: prefBody(getMessage), getMessage, getNewScope: () => location.hostname }); await pref.ready(); await pref.setCurrentScope(location.hostname); return pref; });