LouCipher666 / conduitjs

// ==UserScript==
// @author        badbrainz <wormboy.d@gmail.com> (https://github.com/badbrainz/conduit.js)
// @exclude       *
// ==UserLibrary==
// @name          conduitjs
// @description   Send DOM changes through a pipeline.
// @copyright     2019, badbrainz (https://github.com/badbrainz)
// @license       MIT; https://github.com/badbrainz/conduit.js/raw/master/LICENSE
// @version       1.0.0
// ==/UserScript==
// ==/UserLibrary==
// ==OpenUserJS==
// @author        wormboy
// ==/OpenUserJS==

var conduit = (function () {
  'use strict';

  const state = Symbol('ObserverState');

  function observer(element, delegate, options) {
    const observer = Object.create(Observer);
    const started = false;
    const elements = new Set();
    const elementObserver = new MutationObserver(Observer.processMutations.bind(observer));
    observer[state] = { started, elements, delegate, element, options, elementObserver };
    return observer
  }

  // Original source code: https://github.com/stimulusjs/stimulus/
  const Observer = {
    start() {
      const data = this[state], { elementObserver, options, started } = data;
      if (!started) {
        data.started = true;
        elementObserver.observe(this.element, options);
        this.refresh();
      }
    },

    stop() {
      const data = this[state], { elementObserver, options, started } = data;
      if (started) {
        elementObserver.takeRecords();
        elementObserver.disconnect();
        data.started = false;
      }
    },

    refresh() {
      const data = this[state], { elements, delegate, started } = data;
      if (started) {
        const matches = new Set(delegate.matchElementsInTree());
        for (const element of elements) {
          if (!matches.has(element)) {
            this.removeElement(element);
          }
        }
        for (const element of matches) {
          this.addElement(element);
        }
      }
    },

    get element() {
      return this[state].element
    },

    processMutations(mutations) {
      if (this[state].started) {
        for (const mutation of mutations) {
          this.processMutation(mutation);
        }
      }
    },

    processMutation(mutation) {
      if (mutation.type == 'attributes') {
        this.processAttributeChange(mutation.target, mutation.attributeName);
      } else if (mutation.type == 'childList') {
        this.processRemovedNodes(mutation.removedNodes);
        this.processAddedNodes(mutation.addedNodes);
      } else if (mutation.type == 'characterData') {
        this.processCharacterDataChange(mutation.target);
      }
    },

    processAttributeChange(element, attributeName) {
      const { elements, delegate } = this[state];
      if (elements.has(element)) {
        if (delegate.elementAttributeChanged && delegate.matchElement(element)) {
          delegate.elementAttributeChanged(element, attributeName);
        } else {
          this.removeElement(element);
        }
      } else if (delegate.matchElement(element)) {
        this.addElement(element);
      }
    },

    processCharacterDataChange(node) {
      const { elements, delegate } = this[state];
      if (elements.has(node)) {
        if (delegate.elementCharacterDataChanged && delegate.matchElement(node)) {
          delegate.elementCharacterDataChanged(node);
        } else {
          this.removeElement(node);
        }
      } else if (delegate.matchElement(node)) {
        this.addElement(node);
      }
    },

    processAddedNodes(nodes) {
      for (const node of nodes) {
        const element = this.elementFromNode(node);
        if (element && this.elementIsActive(element)) {
          this.processTree(element, this.addElement);
        }
      }
    },

    processRemovedNodes(nodes) {
      for (const node of nodes) {
        const element = this.elementFromNode(node);
        if (element) {
          this.processTree(element, this.removeElement);
        }
      }
    },

    processTree(tree, processor) {
      const { delegate } = this[state];
      for (const element of delegate.matchElementsInTree(tree)) {
        processor.call(this, element);
      }
    },

    elementFromNode(element) {
      if (element.nodeType == Node.ELEMENT_NODE) {
        return element
      }
    },

    elementIsActive(element) {
      if (element.isConnected != this.element.isConnected) {
        return false
      } else {
        return this.element.contains(element)
      }
    },

    addElement(element) {
      const { elements, delegate } = this[state];
      if (!elements.has(element)) {
        if (this.elementIsActive(element)) {
          elements.add(element);
          if (delegate.elementProcessed) {
            delegate.elementProcessed(element, { type: 'match' });
          }
        }
      }
    },

    removeElement(element) {
      const { elements, delegate } = this[state];
      if (elements.has(element)) {
        elements.delete(element);
        if (delegate.elementProcessed) {
          delegate.elementProcessed(element, { type: 'unmatch' });
        }
      }
    }
  };

  const state$1 = Symbol('ConduitState');
  const routes = {};

  function conduit(...routes) {
    if (routes.length < 2) throw Error('invalid arguments')
    return routes.reduce(conduit.join)
  }

  Object.defineProperties(conduit, {
    define: {
      value(name, ctor) {
        routes[name] = function(...args) {
          return conduit.join(this, ctor(...args))
        };
      }
    },
    observe: {
      value(element) {
        const input = conduit.junction();
        window.setTimeout(() => input.observe(element), 0);
        return input
      }
    },
    junction: {
      value(options = {}) {
        const src = Object.assign(Object.create(Base), routes, Self);
        if (typeof options == 'function') options = { observe: options };
        if (options.observe) src.observe = options.observe;
        if (options.disconnect) src.disconnect = options.disconnect;
        src[state$1] = { events: {} };
        return src
      }
    },
    join: {
      value(src, dest) {
        function ondata(...data) {
          dest.observe(...data);
        }

        function onend() {
          src.off('data', ondata);
          src.off('end', onend);
          dest.disconnect();
          dest.emit('end');
        }

        src.on('data', ondata);
        src.on('end', onend);

        return dest
      }
    },
    observer: {
      value: observer
    }
  });

  const Base = {
    on(evt, fn) {
      const { events } = this[state$1], { [evt]: e = [] } = events;
      events[evt] = e.concat(fn);
      return this
    },

    off(evt, fn) {
      const { events } = this[state$1], { [evt]: e } = events;
      if (e) events[evt] = e.filter(f => f !== fn);
      return this
    },

    emit(evt, ...args) {
      const { [evt]: e } = this[state$1].events;
      if (e) e.forEach(f => f.apply(this, args));
      return this
    }
  };

  {
    const reserved = { writable: false };
    for (const key of Object.keys(Base)) {
      Object.defineProperty(routes, key, reserved);
    }
  }

  const Self = {
    observe(...data) {
      this.matched(...data);
    },

    matched(...data) {
      this.emit('data', ...data);
    },

    disconnect() {
      this.emit('end');
    }
  };

  const state$2 = Symbol('AttributeState');

  function attribute(attributeName) {
    const src = conduit.junction(Route);
    src[state$2] = { attributeName, observers: new Map() };
    return src
  }

  const Route = {
    observe(element, details = {}) {
      const { observers } = this[state$2];
      if (observers.has(element)) {
        if (details.type === 'unmatch') {
          observers.get(element).stop();
          observers.delete(element);
        } else {
          observers.get(element).refresh();
        }
      } else {
        const observer = Filter(element, this);
        observers.set(element, observer);
        observer.start();
      }
    },

    disconnect() {
      const { observers } = this[state$2];
      observers.forEach(o => o.stop());
      observers.clear();
    }
  };

  const Filter = function(element, delegate) {
    const filter = Object.create(Observer$1);
    const observer = conduit.observer(element, filter, {
      attributeFilter: [delegate[state$2].attributeName]
    });
    return Object.assign(filter, { observer, delegate })
  };

  const Observer$1 = {
    start() {
      this.observer.start();
    },

    stop() {
      this.observer.stop();
    },

    refresh() {
      this.observer.refresh();
    },

    get element() {
      return this.observer.element
    },

    get attributeName() {
      return this.delegate[state$2].attributeName
    },

    matchElement(element) {
      return element.hasAttribute(this.attributeName)
    },

    matchElementsInTree() {
      const { element } = this;
      return this.matchElement(element) ? [element] : []
    },

    elementProcessed(element, details) {
      this.delegate.matched(element, details);
    },

    elementAttributeChanged(element, attributeName) {
      if (this.attributeName == attributeName) {
        this.delegate.matched(element, { type: 'change' });
      }
    }
  };

  function each(cb) {
    return conduit.junction(cb)
  }

  const state$3 = Symbol('FilterState');

  function filter(selector) {
    const src = conduit.junction(Route$1);
    src[state$3] = { selector, observers: new Map() };
    return src
  }

  const Route$1 = {
    observe(element, details = {}) {
      const { observers } = this[state$3];
      if (observers.has(element)) {
        if (details.type === 'unmatch') {
          observers.get(element).stop();
          observers.delete(element);
        } else {
          observers.get(element).refresh();
        }
      } else {
        const observer = Filter$1(element, this);
        observers.set(element, observer);
        observer.start();
      }
    },

    disconnect() {
      const { observers } = this[state$3];
      observers.forEach(o => o.stop());
      observers.clear();
    }
  };

  const Filter$1 = function(element, delegate) {
    const filter = Object.create(Observer$2);
    const observer = conduit.observer(element, filter, {
      childList: true,
      subtree: true
    });
    return Object.assign(filter, { observer, delegate })
  };

  const Observer$2 = {
    start() {
      this.observer.start();
    },

    stop() {
      this.observer.stop();
    },

    refresh() {
      this.observer.refresh();
    },

    get element() {
      return this.observer.element
    },

    get selector() {
      return this.delegate[state$3].selector
    },

    matchElement(element) {
      return element.matches(this.selector)
    },

    matchElementsInTree(tree) {
      if (tree) {
        const match = this.matchElement(tree) ? [tree] : [];
        const matches = Array.from(tree.querySelectorAll(this.selector));
        return match.concat(matches)
      } else {
        return Array.from(this.element.querySelectorAll(this.selector))
      }
    },

    elementProcessed(element, details) {
      this.delegate.matched(element, details);
    }
  };

  const state$4 = Symbol('FollowState');
  const delegates = new WeakMap();

  function follow(path) {
    const src = conduit.junction(Route$2);
    const observers = new Map();
    const delegate = Object.create(Delegate);
    delegates.set(delegate, src);
    src[state$4] = { path, observers, delegate };
    return src
  }

  const Delegate = {
    pathMatched(element, details) {
      delegates.get(this).matched(element, details);
    }
  };

  const Route$2 = {
    observe(element, details = {}) {
      const { observers, path, delegate } = this[state$4];
      if (observers.has(element)) {
        if (details.type === 'unmatch') {
          observers.get(element).stop();
          observers.delete(element);
        } else {
          observers.get(element).refresh();
        }
      } else {
        const observer = Filter$2(element, path, delegate);
        observers.set(element, observer);
        observer.start();
      }
    },

    disconnect() {
      const { observers } = this[state$4];
      observers.forEach(o => o.stop());
      observers.clear();
    }
  };

  const Filter$2 = function(element, selectors, delegate) {
    const filter = Object.create(Observer$3);
    const path = Array.isArray(selectors) ? Selector(selectors) : selectors;
    const observer = conduit.observer(element, filter, { childList: true });
    const children = new Map();
    return Object.assign(filter, { path, children, observer, delegate })
  };

  const Observer$3 = {
    start() {
      this.observer.start();
      this.children.forEach(o => o.start());
    },

    stop() {
      this.observer.stop();
      this.children.forEach(o => o.stop());
    },

    refresh() {
      this.observer.refresh();
      this.children.forEach(o => o.refresh());
    },

    matchElement(element) {
      return element.matches(this.path.string)
    },

    matchElementsInTree(tree) {
      const matches = tree ? [tree] : Array.from(this.observer.element.children);
      return matches.filter(this.matchElement, this)
    },

    elementProcessed(element, details) {
      if (details.type == 'match') {
        this.elementMatched(element, details);
      } else {
        this.elementUnmatched(element, details);
      }
    },

    elementMatched(element, details) {
      const selector = this.path.next;
      if (selector) {
        const child = Filter$2(element, selector, this);
        this.children.set(element, child);
        child.start();
      } else {
        this.pathMatched(element, details);
      }
    },

    elementUnmatched(element, details) {
      const child = this.children.get(element);
      if (child) {
        child.stop();
        this.children.delete(element);
      } else {
        this.pathMatched(element, details);
      }
    },

    pathMatched(element, details) {
      this.delegate.pathMatched(element, details);
    }
  };

  const Selector = function(path, start = 0) {
    const string = path[start], index = start + 1;
    const next = index < path.length ? Selector(path, index) : null;
    return { string, next }
  };

  const state$5 = Symbol('ListenerState');

  function listen(eventName, useCapture = false) {
    const src = conduit.junction(Route$3);
    src[state$5] = { useCapture, eventName, listeners: new Map() };
    return src
  }

  const Route$3 = {
    observe(element, details = {}) {
      const { listeners } = this[state$5];
      if (listeners.has(element)) {
        if (details.type === 'unmatch') {
          listeners.get(element).stop();
          listeners.delete(element);
        }
      } else {
        const listener = Listener(element, this);
        listeners.set(element, listener);
        listener.start();
      }
    },

    disconnect() {
      const { listeners } = this[state$5];
      listeners.forEach(l => l.stop());
      listeners.clear();
    }
  };

  const Listener = function(element, delegate) {
    const handler = Object.create(Observer$4);
    return Object.assign(handler, { element, delegate })
  };

  const Observer$4 = {
    start() {
      this.element.addEventListener(this.event, this, this.capture);
    },

    stop() {
      this.element.removeEventListener(this.event, this, this.capture);
    },

    handleEvent(event) {
      this.delegate.matched(this.element, event);
    },

    get event() {
      return this.delegate[state$5].eventName
    },

    get capture() {
      return this.delegate[state$5].useCapture
    }
  };

  const state$6 = Symbol('TextState');

  function text(query) {
    const src = conduit.junction(Route$4);
    src[state$6] = { query: new RegExp(query), observers: new Map() };
    return src
  }

  const Route$4 = {
    observe(element, details) {
      const { observers } = this[state$6];
      if (observers.has(element)) {
        if (details.type === 'unmatch') {
          observers.get(element).stop();
          observers.delete(element);
        } else {
          observers.get(element).refresh();
        }
      } else {
        const observer = Filter$3(element, this);
        observers.set(element, observer);
        observer.start();
      }
    },

    disconnect() {
      const { observers } = this[state$6];
      observers.forEach(o => o.stop());
      observers.clear();
    }
  };

  const Filter$3 = function(element, delegate) {
    const filter = Object.create(Observer$5);
    const observer = conduit.observer(element, filter, {
      characterData: true,
      subtree: true
    });
    return Object.assign(filter, { observer, delegate })
  };

  const Observer$5 = {
    start() {
      this.observer.start();
    },

    stop() {
      this.observer.stop();
    },

    refresh() {
      this.observer.refresh();
    },

    get query() {
      return this.delegate[state$6].query
    },

    matchElement(element) {
      return this.query.test(element.textContent)
    },

    matchElementsInTree(tree) {
      if (tree && tree.nodeType == Node.TEXT_NODE) {
        return this.matchElement(tree) ? [tree] : []
      } else {
        const element = tree || this.observer.element;
        const result = document.evaluate('.//text()', element, null,
          XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
        let node, matches = [];
        while (node = result.iterateNext()) {
          if (this.matchElement(node)) {
            matches.push(node);
          }
        }
        return matches
      }
    },

    elementProcessed(element, details) {
      this.delegate.matched(element, details);
    },

    elementCharacterDataChanged(node) {
      this.delegate.matched(node, { type: 'change' });
    }
  };

  conduit.define('attribute', attribute);
  conduit.define('each', each);
  conduit.define('filter', filter);
  conduit.define('follow', follow);
  conduit.define('listen', listen);
  conduit.define('text', text);

  return conduit;

}());