Linkify Plus Plus

Based on Linkify Plus. Turn plain text URLs into links.

As of 20.02.2025. See апошняя версія.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name Linkify Plus Plus
// @version 11.0.0
// @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();
}

const _module_ = {exports: {}};
(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);
var Events = _module_.exports;

function createPref(DEFAULT, sep = "/") {
  let storage;
  let currentScope = "global";
  let scopeList = ["global"];
  const events = new Events;
  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 result = await _confirm(_("deleteScopeConfirm", scopeName));

      if (result) {
        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 Events;
  
  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,
	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,
	dabur: true,
	dad: true,
	dance: true,
	date: true,
	dating: true,
	day: true,
	de: 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,
	dnp: 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,
	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,
	lipsy: true,
	live: true,
	living: true,
	lk: true,
	llc: true,
	loan: true,
	loans: 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,
	ni: true,
	nico: true,
	nike: true,
	ninja: true,
	nissan: true,
	nl: true,
	no: true,
	nokia: 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,
	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--30rr7y": 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--55qw42g": 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--clchc0ea0b2g2a9gcd": true,
	"xn--czr694b": true,
	"xn--czrs0t": true,
	"xn--czru2d": true,
	"xn--d1acj3b": true,
	"xn--d1alf": true,
	"xn--e1a4c": true,
	"xn--efvy88h": 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,
	"닷넷": true,
	"コム": true,
	"游戏": true,
	"vermögensberatung": true,
	"企业": true,
	"信息": true,
	"مصر": true,
	"قطر": true,
	"广东": true,
	"இலங்கை": true,
	"இந்தியா": true,
	"հայ": true,
	"新加坡": true,
	"فلسطين": true,
	"政务": 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 result = {
        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 (result.custom) {
        result.start = urlMatch.index;
        result.end = url.lastIndex;
        result.text = result.url = urlMatch[0];
			} else {
        
        result.start = urlMatch.index + result.prefix.length;
        result.end = url.lastIndex - result.suffix.length;
			}
			
			if (mustacheRange && mustacheRange.end <= result.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 && result.start < mustacheRange.end && result.end >= mustacheRange.start) {
				continue;
			}
			
			if (!result.custom) {
				// adjust path and suffix
				if (result.path) {
					// Strip BBCode
					pathStrip(result, /\[\/?(b|i|u|url|img|quote|code|size|color)\].*/i, "");
					
					// Strip braces
					pathStripBrace(result, "(", ")");
					pathStripBrace(result, "[", "]");
					pathStripBrace(result, "{", "}");
					
					// Strip quotes
					pathStripQuote(result, "'");
					pathStripQuote(result, '"');
					
					// Remove trailing ".,?"
					pathStrip(result, /(^|[^-_])[.,?]+$/, "$1");
				}
				
				// check suffix
				if (invalidSuffix && invalidSuffix.test(result.suffix)) {
					if (/\s$/.test(result.suffix)) {
						url.lastIndex--;
					}
					continue;
				}
        
        // ignore fuzzy ip
				if (!fuzzyIp && isIP(result.domain) &&
            !result.protocol && !result.auth && !result.path) {
          continue;
        }
        
				// mailto protocol
				if (!result.protocol && result.auth) {
					var matchMail = result.auth.match(/^mailto:(.+)/);
					if (matchMail) {
						result.protocol = "mailto:";
						result.auth = matchMail[1];
					}
				}

				// http alias
				if (result.protocol && result.protocol.match(/^(hxxp|h\*\*p|ttp)/)) {
					result.protocol = "http://";
				}

				// guess protocol
				if (!result.protocol) {
					var domainMatch;
					if ((domainMatch = result.domain.match(/^(ftp|irc)/))) {
						result.protocol = domainMatch[0] + "://";
					} else if (result.domain.match(/^(www|web)/)) {
						result.protocol = "http://";
					} else if (result.auth && result.auth.indexOf(":") < 0 && !result.path) {
						result.protocol = "mailto:";
					} else {
						result.protocol = "http://";
					}
				}
        
        // ignore mail
        if (!mail && result.protocol === "mailto:") {
          continue;
        }
        
				// verify domain
        if (!isIP(result.domain)) {
          if (/^(http|https|mailto)/.test(result.protocol) && !inTLDS(result.domain)) {
            continue;
          }
          
          const invalidLabel = getInvalidLabel(result.domain);
          if (invalidLabel) {
            url.lastIndex = urlMatch.index + invalidLabel.index + 1;
            continue;
          }
        }

				// Create URL
				result.url = result.protocol + (result.auth && result.auth + "@") + result.domain + result.port + result.path;
				result.text = text.slice(result.start, result.end);
			}
			
			// since regex is shared with other parse generators, cache lastIndex position and restore later
			mustacheLastIndex = mustache.lastIndex;
			urlLastIndex = url.lastIndex;
			
			yield result;
			
			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 Events {
	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 result of matcher.match(text)) {
				if (!frag) {
					frag = document.createDocumentFragment();
					pos = new Pos(range.startContainer, range.startOffset);
					textRange = range.cloneRange();
				}
				// clone text
				pos.moveTo(result.start);
				textRange.setEnd(pos.container, pos.offset);
				frag.appendChild(cloneContents(textRange));
				
				// clone link
				textRange.collapse();
				pos.moveTo(result.end);
				textRange.setEnd(pos.container, pos.offset);
				
				var content = cloneContents(textRange),
					link = this.buildLink(result, content);

				textRange.collapse();

				frag.appendChild(link);
				this.emit("link", {link, range, result, content});
			}
			if (pos) {
				pos.moveTo(text.length);
				textRange.setEnd(pos.container, pos.offset);
				frag.appendChild(cloneContents(textRange));
				
				range.deleteContents();
				range.insertNode(frag);
			}
			yield;
		}
	}
	buildLink(result, content) {
		var {newTab, embedImage, noOpener} = this.options;
		var link = document.createElement("a");
		link.href = result.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(result.url)) {
			child = new Image;
			child.src = result.url;
			child.alt = result.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;
});