4393 1 0 0
// ==UserScript==
// @name Linkify Plus Plus
// @version 1
// @run-at 

// ==/UserScript==

/**
 * 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,
    maxRunTime: 100,
    customRules: "",
  };
}

var prefBody = getMessage => {
  return [
    {
      key: "fuzzyIp",
      type: "checkbox",
      label: getMessage("optionsFuzzyIpLabel")
    },
    {
      key: "ignoreMustache",
      type: "checkbox",
      label: getMessage("optionsIgnoreMustacheLabel")
    },
    {
      key: "embedImage",
      type: "checkbox",
      label: getMessage("optionsEmbedImageLabel"),
      children: [
        {
          key: "embedImageExcludeElement",
          type: "textarea",
          label: getMessage("optionsEmbedImageExcludeElementLabel"),
          validate: validateSelector
        }
      ]
    },
    {
      key: "unicode",
      type: "checkbox",
      label: getMessage("optionsUnicodeLabel")
    },
    {
      key: "mail",
      type: "checkbox",
      label: getMessage("optionsMailLabel")
    },
    {
      key: "newTab",
      type: "checkbox",
      label: getMessage("optionsNewTabLabel")
    },
    {
      key: "standalone",
      type: "checkbox",
      label: getMessage("optionsStandaloneLabel"),
      children: [
        {
          key: "boundaryLeft",
          type: "text",
          label: getMessage("optionsBoundaryLeftLabel")
        },
        {
          key: "boundaryRight",
          type: "text",
          label: getMessage("optionsBoundaryRightLabel")
        }
      ]
    },
    {
      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")
    },
    {
      key: "customRules",
      type: "textarea",
      label: getMessage("optionsCustomRulesLabel")
    }
  ];
  
  function validateSelector(value) {
    if (value) {
      document.documentElement.matches(value);
    }
  }
};

var maxLength = 22;
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,
	accenture: true,
	accountant: true,
	accountants: true,
	aco: true,
	actor: true,
	ad: true,
	adac: true,
	adult: true,
	ae: true,
	aeg: true,
	aero: true,
	aetna: true,
	af: true,
	afamilycompany: true,
	afl: true,
	africa: true,
	ag: true,
	agency: true,
	ai: true,
	aig: true,
	airbus: true,
	airforce: true,
	al: true,
	allfinanz: true,
	allstate: true,
	alsace: true,
	am: true,
	amazon: true,
	americanexpress: true,
	amex: true,
	amfam: true,
	amica: true,
	amsterdam: true,
	analytics: true,
	anz: true,
	ao: true,
	aol: true,
	apartments: true,
	app: true,
	apple: true,
	aq: true,
	aquarelle: true,
	ar: true,
	aramco: 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,
	barefoot: true,
	bargains: true,
	basketball: true,
	bauhaus: true,
	bayern: true,
	bb: true,
	bbc: true,
	bbt: 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,
	bms: true,
	bmw: true,
	bn: true,
	bnpparibas: true,
	bo: true,
	boats: true,
	bofa: true,
	bond: true,
	bosch: true,
	bostik: true,
	boston: true,
	bot: true,
	boutique: true,
	br: true,
	bradesco: true,
	bridgestone: true,
	broadway: true,
	broker: true,
	brother: true,
	brussels: true,
	bs: true,
	bt: true,
	bugatti: 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,
	cancerresearch: true,
	canon: true,
	capetown: true,
	capital: true,
	car: true,
	cards: true,
	care: true,
	career: true,
	careers: true,
	cars: true,
	casa: true,
	cash: true,
	casino: true,
	cat: true,
	catering: true,
	catholic: true,
	cba: true,
	cbn: true,
	cbs: true,
	cc: true,
	cd: true,
	center: true,
	ceo: true,
	cern: true,
	cf: true,
	cfa: true,
	cfd: true,
	cg: true,
	ch: true,
	chanel: 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,
	cookingchannel: 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,
	csc: true,
	cu: true,
	cuisinella: true,
	cv: true,
	cw: true,
	cx: true,
	cy: true,
	cymru: true,
	cyou: true,
	cz: true,
	dabur: true,
	dance: true,
	date: true,
	dating: 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,
	duck: 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,
	epson: true,
	equipment: true,
	er: true,
	ericsson: true,
	erni: true,
	es: 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,
	fidelity: true,
	film: true,
	finance: true,
	financial: true,
	firmdale: true,
	fish: true,
	fishing: true,
	fit: true,
	fitness: true,
	fj: true,
	fk: true,
	flights: true,
	florist: true,
	flowers: true,
	fm: true,
	fo: true,
	foo: true,
	food: true,
	foodnetwork: true,
	football: true,
	ford: true,
	forex: true,
	forsale: true,
	forum: true,
	foundation: true,
	fox: true,
	fr: true,
	fresenius: true,
	frl: true,
	frogans: true,
	frontdoor: true,
	fujitsu: true,
	fujixerox: true,
	fun: true,
	fund: true,
	furniture: true,
	futbol: true,
	fyi: true,
	ga: true,
	gal: true,
	gallery: true,
	gallo: 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,
	gl: true,
	glade: 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,
	guardian: 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,
	hgtv: 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,
	hoteles: true,
	hotmail: true,
	house: true,
	how: true,
	hr: true,
	hsbc: true,
	ht: true,
	hu: true,
	hyatt: true,
	ice: true,
	icu: true,
	id: true,
	ie: true,
	ieee: true,
	ifm: true,
	ikano: true,
	il: true,
	im: true,
	immo: true,
	immobilien: true,
	"in": true,
	inc: true,
	industries: true,
	info: 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,
	juniper: true,
	kaufen: true,
	ke: true,
	kerryhotels: true,
	kerrylogistics: true,
	kerryproperties: true,
	kfh: true,
	kg: true,
	kh: true,
	ki: true,
	kia: true,
	kim: true,
	kinder: 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,
	kuokgroup: 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,
	lefrak: true,
	legal: true,
	lego: true,
	lexus: true,
	lgbt: true,
	li: true,
	lidl: true,
	life: true,
	lighting: true,
	lilly: true,
	limited: true,
	limo: true,
	linde: true,
	link: true,
	lipsy: true,
	live: 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,
	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,
	movie: true,
	mp: true,
	mq: true,
	mr: true,
	ms: true,
	mt: true,
	mtn: true,
	mtr: true,
	mu: true,
	museum: true,
	mutual: true,
	mv: true,
	mw: true,
	mx: true,
	my: true,
	mz: true,
	na: true,
	nab: true,
	nagoya: true,
	name: true,
	nationwide: true,
	natura: true,
	navy: true,
	nc: true,
	ne: true,
	nec: true,
	net: true,
	netbank: true,
	network: true,
	neustar: true,
	"new": true,
	news: true,
	next: true,
	nextdirect: true,
	nf: true,
	ng: true,
	ngo: true,
	ni: true,
	nico: true,
	nikon: true,
	ninja: true,
	nissan: true,
	nissay: true,
	nl: true,
	no: true,
	nokia: true,
	northwesternmutual: true,
	norton: true,
	nowruz: true,
	np: true,
	nr: true,
	nra: true,
	nrw: true,
	ntt: true,
	nu: true,
	nyc: true,
	nz: true,
	obi: true,
	observer: true,
	off: true,
	office: true,
	okinawa: true,
	om: true,
	omega: true,
	one: true,
	ong: true,
	onl: true,
	online: true,
	onyourside: true,
	ooo: true,
	oracle: true,
	orange: true,
	org: true,
	organic: true,
	osaka: true,
	otsuka: true,
	ovh: true,
	pa: true,
	page: true,
	paris: true,
	partners: true,
	parts: true,
	party: true,
	pe: true,
	pet: true,
	pf: true,
	pfizer: true,
	pg: true,
	ph: true,
	pharmacy: 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,
	pro: true,
	productions: true,
	promo: true,
	properties: true,
	property: true,
	protection: true,
	pru: true,
	prudential: true,
	ps: true,
	pt: true,
	pub: true,
	pw: true,
	py: true,
	qa: true,
	qpon: true,
	quebec: true,
	quest: true,
	racing: true,
	radio: true,
	raid: 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,
	rmit: true,
	ro: true,
	rocks: true,
	rodeo: true,
	rs: true,
	ru: true,
	rugby: true,
	ruhr: true,
	run: true,
	rw: true,
	rwe: 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,
	sc: true,
	sca: true,
	scb: true,
	schaeffler: true,
	schmidt: true,
	school: true,
	schule: true,
	schwarz: true,
	science: true,
	scjohnson: true,
	scot: true,
	sd: true,
	se: true,
	seat: true,
	security: true,
	select: true,
	sener: true,
	services: true,
	ses: true,
	seven: true,
	sew: true,
	sex: true,
	sexy: true,
	sfr: true,
	sg: true,
	sh: true,
	shangrila: 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,
	spreadbetting: 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,
	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,
	tiaa: true,
	tickets: true,
	tienda: true,
	tiffany: 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,
	travelchannel: true,
	travelers: true,
	trust: true,
	tt: true,
	tube: true,
	tui: true,
	tv: true,
	tw: true,
	tz: true,
	ua: true,
	ubank: true,
	ubs: true,
	ug: true,
	uk: true,
	university: true,
	uno: true,
	uol: true,
	us: true,
	uy: true,
	uz: true,
	va: true,
	vacations: 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,
	visa: true,
	vision: true,
	vivo: true,
	vlaanderen: true,
	vn: true,
	vodka: true,
	volkswagen: true,
	volvo: true,
	vote: true,
	voting: true,
	voto: true,
	voyage: true,
	vu: true,
	wales: true,
	walter: true,
	wang: true,
	watch: 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,
	xerox: 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--5su34j936bgsg": true,
	"xn--5tzm5g": true,
	"xn--6frz82g": true,
	"xn--6qq986b3xl": true,
	"xn--80adxhks": true,
	"xn--80ao21a": true,
	"xn--80asehdb": true,
	"xn--80aswg": true,
	"xn--90a3ac": true,
	"xn--90ae": true,
	"xn--90ais": true,
	"xn--9dbq2a": true,
	"xn--bck1b9a5dre4c": true,
	"xn--c1avg": true,
	"xn--cck2b3b": true,
	"xn--cckwcxetd": 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--jlq480n2rg": 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--mgbbh1a71e": 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--vhquv": true,
	"xn--vuq861b": true,
	"xn--w4r85el8fhu5dnra": true,
	"xn--w4rs40l": 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,
	yandex: true,
	ye: true,
	yoga: true,
	yokohama: true,
	youtube: true,
	yt: true,
	za: true,
	zara: 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,
	"닷넷": true,
	"コム": true,
	"游戏": true,
	"企业": true,
	"信息": 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
};

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} = 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 NodeFilter.FILTER_SKIP;
			}
		};
		// 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();
	});
}

// Valid root node before linkifing
function validRoot(node, validator) {
  // Cache valid state in node.VALID
  if (node.VALID !== undefined) {
    return node.VALID;
  }

  // Loop through ancestor
  var cache = [], isValid;
  while (node != document.documentElement) {
    cache.push(node);

    // It is invalid if it has invalid ancestor
    if (!validator(node) || INVALID_TAGS[node.localName]) {
      isValid = false;
      break;
    }

    // The node was removed from DOM tree
    if (!node.parentNode) {
      return false;
    }

    node = node.parentNode;

    if (node.VALID !== undefined) {
      isValid = node.VALID;
      break;
    }
  }

  // All ancestors are fine
  if (isValid === undefined) {
    isValid = true;
  }

  // Cache the result
  var i;
  for (i = 0; i < cache.length; i++) {
    cache[i].VALID = isValid;
  }

  return isValid;
}

function createValidator({includeElement, excludeElement}) {
  return function(node) {
    if (node.isContentEditable) {
      return false;
    }
    if (node.matches) {
      if (includeElement && node.matches(includeElement)) {
        return true;
      }
      if (excludeElement && node.matches(excludeElement)) {
        return false;
      }
    }
    return true;
  };
}

function createBuffer(size) {
  const set = new Set;
  const buff = Array(size);
  const eventBus = document.createElement("span");
  let start = 0;
  let end = 0;
  return {push, eventBus, shift};
  
  function push(item) {
    if (set.has(item)) {
      return;
    }
    if (set.size && start === end) {
      // overflow
      eventBus.dispatchEvent(new CustomEvent("overflow"));
      set.clear();
      return;
    }
    set.add(item);
    buff[end] = item;
    end = (end + 1) % size;
    eventBus.dispatchEvent(new CustomEvent("add"));
  }
  
  function shift() {
    if (!set.size) {
      return;
    }
    const item = buff[start];
    set.delete(item);
    buff[start] = null;
    start = (start + 1) % size;
    return item;
  }
}

function createLinkifyProcess({options, bufferSize}) {
  const buffer = createBuffer(bufferSize);
  let overflowed = false;
  let started = false;
  buffer.eventBus.addEventListener("add", start);
  buffer.eventBus.addEventListener("overflow", () => overflowed = true);
  return {process};
  
  function process(root) {
    if (overflowed) {
      return false
    }
    if (validRoot(root, options.validator)) {
      buffer.push(root);
    }
    return true;
  }
  
  function start() {
    if (started) {
      return;
    }
    started = true;
    deque();
  }
  
  function deque() {
    let root;
    if (overflowed) {
      root = document.body;
      overflowed = false;
    } else {
      root = buffer.shift();
    }
    if (!root) {
      started = false;
      return;
    }
    
    linkify(root, options)
      .then(() => {
        var p = Promise.resolve();
        if (options.includeElement) {
          for (var node of root.querySelectorAll(options.includeElement)) {
            p = p.then(linkify.bind(null, node, options));
          }
        }
        return p;
      })
      .catch(err => {
        console.error(err);
      })
      .then(deque);
  }
}

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);
    if (changes.includeElement != null || changes.excludeElement != null) {
      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 linkifyProcess = createLinkifyProcess({
    options: createOptions(pref),
    bufferSize: 100
  });  
  const 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) {
      if (record.addedNodes.length) {
        if (!linkifyProcess.process(record.target)) {
          // it's full
          break;
        }
      }
    }
  });
  await prepareDocument();
  observer.observe(document.body, {
    childList: true,
    subtree: true
  });
  linkifyProcess.process(document.body);    
}

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});
    });
  }
}

/* global $inline */

function getMessageFactory() {
  const translate = {
    "optionsFuzzyIpLabel": "Match IP with only 4 digits.",
    "optionsIgnoreMustacheLabel": "Ignore URLs inside mustaches e.g. {{ ... }}.",
    "optionsEmbedImageLabel": "Embed images.",
    "optionsEmbedImageExcludeElementLabel": "Exclude following elements. (CSS selector)",
    "optionsUnicodeLabel": "Match unicode characters.",
    "optionsMailLabel": "Match email address.",
    "optionsNewTabLabel": "Open links in new tabs.",
    "optionsStandaloneLabel": "The link must be surrounded by whitespaces.",
    "optionsBoundaryLeftLabel": "Allowed characters between the whitespace and the link. (left side)",
    "optionsBoundaryRightLabel": "Allowed characters between the whitespace and the link. (right side)",
    "optionsExcludeElementLabel": "Do not linkify following elements. (CSS selector)",
    "optionsIncludeElementLabel": "Always linkify following elements. Override above. (CSS selector)",
    "optionsTimeoutLabel": "Max executation time. (ms)",
    "optionsTimeoutHelp": "The script will terminate if it takes too long to convert the entire page.",
    "optionsMaxRunTimeLabel": "Max script run time. (ms)",
    "optionsMaxRunTimeHelp": "Split the process into small chunks to avoid freezing the browser.",
    "optionsCustomRulesLabel": "Custom rules. (RegExp per line)",
    "currentScopeLabel": "Current domain",
    "addScopeLabel": "Add new domain",
    "addScopePrompt": "Add new domain",
    "deleteScopeLabel": "Delete current domain",
    "deleteScopeConfirm": "Delete domain $1?",
    "learnMoreButton": "Learn more",
    "importButton": "Import",
    "importPrompt": "Paste settings",
    "exportButton": "Export",
    "exportPrompt": "Copy settings"
  };
  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;
});
最新回复 (1)
  • 0 引用
    这个需要自己创建脚本或者前往油猴网站上搜索linkify plus plus安装https://greasyfork.org/cs/scripts/4255-linkify-plus-plus?locale_override=9%27a%3D0
    效果就是网页上的文本网址都可以点击跳转
    2022-6-11 只看Ta 回复
    • 轻创社区 - 有源软件体验中心
      3
        登录 注册
返回