Feedly NG Filter

ルールにマッチするアイテムを既読にして取り除きます。ルールは正規表現で記述でき、複数のルールをツリー状に組み合わせることができます。

Per 13-02-2019. Zie de nieuwste versie.

// ==UserScript==
// @name           Feedly NG Filter
// @namespace      https://github.com/matzkoh
// @version        1.0.0
// @description    ルールにマッチするアイテムを既読にして取り除きます。ルールは正規表現で記述でき、複数のルールをツリー状に組み合わせることができます。
// @author         matzkoh
// @include        https://feedly.com/*
// @icon           https://raw.githubusercontent.com/matzkoh/userscripts/master/packages/feedly-ng-filter/images/icon.png
// @screenshot     https://raw.githubusercontent.com/matzkoh/userscripts/master/packages/feedly-ng-filter/images/screenshot.png
// @run-at         document-start
// @grant          GM_getValue
// @grant          GM_setValue
// @grant          GM_addStyle
// @grant          GM_xmlhttpRequest
// @grant          GM_registerMenuCommand
// @grant          GM_unregisterMenuCommand
// @grant          GM_log
// ==/UserScript==

;(function() {
  // ASSET: index.js
  var $Focm$exports = function() {
    var exports = this
    var module = {
      exports: this,
    }

    const fs = {}
    const notificationDefaults = {
      title: 'Feedly NG Filter',
      icon: GM_info.script.icon,
      tag: 'feedly-ng-filter',
      autoClose: 5000,
    }
    const CSS_STYLE_TEXT =
      ".fngf-row {\n  display: flex;\n  flex-direction: row;\n}\n\n.fngf-column {\n  display: flex;\n  flex-direction: column;\n}\n\n.fngf-align-center {\n  align-items: center;\n}\n\n.fngf-grow {\n  flex-grow: 1;\n}\n\n.fngf-badge {\n  padding: 0 0.5em;\n  margin: 0 0.5em;\n  color: #fff;\n  background-color: #999;\n  border-radius: 50%;\n}\n\n.fngf-btn {\n  padding: 5px 10px;\n  font: inherit;\n  font-weight: bold;\n  color: #333;\n  background-color: #eee;\n  border: none;\n  outline: none;\n}\n\n.fngf-menu-btn > .fngf-btn:not(:last-child) {\n  margin-right: -1px;\n}\n\n.fngf-btn[disabled] {\n  color: #ccc;\n  background-color: transparent;\n  box-shadow: 0 0 0 1px #eee inset;\n}\n\n.fngf-btn:not([disabled]):active,\n.fngf-btn:not([disabled]).active,\n.fngf-checkbox > :checked + .fngf-btn {\n  background-color: #ccc;\n}\n\n.fngf-btn:not([disabled]):hover,\n.fngf-menu-btn:hover > .fngf-btn:not([disabled]) {\n  box-shadow: 0 0 0 1px #ccc inset;\n}\n\n.fngf-dropdown {\n  position: relative;\n  display: flex;\n  align-items: center;\n  padding-right: 5px;\n  padding-left: 5px;\n}\n\n.fngf-dropdown::before {\n  display: block;\n  content: '';\n  border-top: 5px solid #333;\n  border-right: 3px solid transparent;\n  border-left: 3px solid transparent;\n}\n\n.fngf-dropdown-menu {\n  position: absolute;\n  top: 100%;\n  right: 0;\n  z-index: 1;\n  min-width: 100px;\n  background-color: #fff;\n  box-shadow: 1px 2px 5px #0008;\n}\n\n.fngf-dropdown:not(.active) > .fngf-dropdown-menu {\n  display: none;\n}\n\n.fngf-dropdown-menu-item {\n  padding: 10px;\n}\n\n.fngf-dropdown-menu-item:hover {\n  background-color: #eee;\n}\n\n.fngf-checkbox > input[type='checkbox'] {\n  display: none;\n}\n\n.fngf-only:not(:only-child) {\n  display: none;\n}\n" +
      "@keyframes error {\n  from {\n    background-color: #ff0;\n    border-color: #f00;\n  }\n}\n\n.fngf-panel {\n  position: fixed;\n  z-index: 2147483646;\n  display: grid;\n  grid-gap: 10px;\n  min-width: 320px;\n  padding: 10px;\n  font-size: 12px;\n  color: #333;\n  cursor: default;\n  user-select: none;\n  background-color: #fffe;\n  box-shadow: 1px 2px 5px #0008;\n}\n\n.fngf-panel-body {\n  display: grid;\n  grid-gap: 10px;\n}\n\n.fngf-panel input[type='text'] {\n  padding: 4px;\n  font: inherit;\n  border: 1px solid #999;\n}\n\n.fngf-panel input[type='text']:focus {\n  box-shadow: 0 0 0 1px #999 inset;\n}\n\n.fngf-panel-terms {\n  display: grid;\n  grid-template-columns: auto 1fr auto;\n  grid-gap: 5px;\n  align-items: center;\n  width: 400px;\n  padding: 10px;\n  white-space: nowrap;\n  border: 1px solid #999;\n}\n\n.fngf-panel.root .fngf-panel-name,\n.fngf-panel.root .fngf-panel-terms {\n  display: none;\n}\n\n.fngf-panel-terms-textbox.error {\n  animation: error 1s;\n}\n\n.fngf-panel-rules {\n  padding: 10px;\n  border: 1px solid #999;\n}\n\n.fngf-panel fieldset {\n  padding: 10px;\n  margin: 0;\n}\n\n.fngf-panel-rule-name {\n  flex-grow: 1;\n}\n\n.fngf-panel-buttons {\n  justify-content: space-between;\n}\n\n.fngf-panel-buttons > .fngf-btn-group:not(:first-child) {\n  margin-left: 10px;\n}\n"

    function __(strings, ...values) {
      let key = values.map((v, i) => `${strings[i]}{${i}}`).join('') + strings[strings.length - 1]

      if (!(key in __.data)) {
        throw new Error(`localized string not found: ${key}`)
      }

      return __.data[key].replace(/\{(\d+)\}/g, (_, cap) => values[cap])
    }

    Object.defineProperties(__, {
      config: {
        configurable: true,
        writable: true,
        value: {
          defaultLocale: 'en-US',
        },
      },
      locales: {
        configurable: true,
        writable: true,
        value: {},
      },
      data: {
        configurable: true,

        get() {
          return this.locales[this.config.locale]
        },
      },
      languages: {
        configurable: true,

        get() {
          return Object.keys(this.locales)
        },
      },
      add: {
        configurable: true,
        writable: true,
        value: function add({ locale, data }) {
          if (locale in this.locales) {
            throw new Error(`failed to add existing locale: ${locale}`)
          }

          this.locales[locale] = data
        },
      },
      use: {
        configurable: true,
        writable: true,
        value: function use(locale) {
          if (locale in this.locales) {
            this.config.locale = locale
          } else if (this.config.defaultLocale) {
            this.config.locale = this.config.defaultLocale
          } else {
            throw new Error(`unknown locale: ${locale}`)
          }
        },
      },
    })

    __.add({
      locale: 'en-US',
      data: {
        'Feedly NG Filter': 'Feedly NG Filter',
        OK: 'OK',
        Cancel: 'Cancel',
        Add: 'Add',
        Copy: 'Copy',
        Paste: 'Paste',
        'New Filter': 'New Filter',
        'Rule Name': 'Rule Name',
        'No Rules': 'No Rules',
        Title: 'Title',
        URL: 'URL',
        'Feed Title': 'Feed Title',
        'Feed URL': 'Feed URL',
        Author: 'Author',
        Keywords: 'Keywords',
        Contents: 'Contents',
        'Ignore Case': 'Ignore Case',
        Edit: 'Edit',
        Delete: 'Delete',
        'Hit Count:\t{0}': 'Hit Count:\t{0}',
        'Last Hit:\t{0}': 'Last Hit:\t{0}',
        'NG Setting': 'NG Setting',
        Setting: 'Setting',
        'Import Configuration': 'Import Configuration',
        'Preferences were successfully imported.': 'Preferences were successfully imported.',
        'Export Configuration': 'Export Configuration',
        Language: 'Language',
        'NG Settings were modified.\nNew filters take effect after next refresh.':
          'NG Settings were modified.\nNew filters take effect after next refresh.',
      },
    })

    __.add({
      locale: 'ja',
      data: {
        'Feedly NG Filter': 'Feedly NG Filter',
        OK: 'OK',
        Cancel: 'キャンセル',
        Add: '追加',
        Copy: 'コピー',
        Paste: '貼り付け',
        'New Filter': '新しいフィルタ',
        'Rule Name': 'ルール名',
        'No Rules': 'ルールはありません',
        Title: 'タイトル',
        URL: 'URL',
        'Feed Title': 'フィードのタイトル',
        'Feed URL': 'フィードの URL',
        Author: '著者',
        Keywords: 'キーワード',
        Contents: '本文',
        'Ignore Case': '大/小文字を区別しない',
        Edit: '編集',
        Delete: '削除',
        'Hit Count:\t{0}': 'ヒット数:\t{0}',
        'Last Hit:\t{0}': '最終ヒット:\t{0}',
        'NG Setting': 'NG 設定',
        Setting: '設定',
        'Import Configuration': '設定をインポート',
        'Preferences were successfully imported.': '設定をインポートしました',
        'Export Configuration': '設定をエクスポート',
        Language: '言語',
        'NG Settings were modified.\nNew filters take effect after next refresh.':
          'NG 設定を更新しました。\n新しいフィルタは次回読み込み時から有効になります。',
      },
    })

    __.use(navigator.language)

    class Serializer {
      static stringify(value, space) {
        return JSON.stringify(
          value,
          (key, value) => {
            if (value instanceof RegExp) {
              return {
                __serialized__: true,
                class: 'RegExp',
                args: [value.source, value.flags],
              }
            }

            return value
          },
          space,
        )
      }

      static parse(text) {
        return JSON.parse(text, (key, value) => {
          if (value === null || value === void 0 ? void 0 : value.__serialized__) {
            switch (value.class) {
              case 'RegExp':
                return new RegExp(...value.args)
            }
          }

          return value
        })
      }
    }

    class EventEmitter {
      constructor() {
        this.listeners = {}
      }

      on(type, listener) {
        if (type.trim().includes(' ')) {
          type.match(/\S+/g).forEach(t => this.on(t, listener))
          return
        }

        if (!(type in this.listeners)) {
          this.listeners[type] = new Set()
        }

        const set = this.listeners[type]

        for (const fn of set.values()) {
          if (EventEmitter.compareListener(fn, listener)) {
            return
          }
        }

        set.add(listener)
      }

      async once(type, listener) {
        return new Promise((resolve, reject) => {
          function wrapper(event) {
            this.off(wrapper)

            try {
              EventEmitter.applyListener(this, listener, event)
              resolve(event)
            } catch (e) {
              reject(e)
            }
          }

          wrapper[EventEmitter.original] = listener
          this.on(type, wrapper)
        })
      }

      off(type, listener) {
        if (!listener || !(type in this.listeners)) {
          return
        }

        const set = this.listeners[type]

        for (const fn of set.values()) {
          if (EventEmitter.compareListener(fn, listener)) {
            set.delete(fn)
          }
        }
      }

      removeAllListeners(type) {
        delete this.listeners[type]
      }

      dispatchEvent(event) {
        event.timestamp = Date.now()

        if (event.type in this.listeners) {
          this.listeners[event.type].forEach(listener => {
            try {
              EventEmitter.applyListener(this, listener, event)
            } catch (e) {
              setTimeout(
                () =>
                  (function(e) {
                    throw e
                  })(e),
                0,
              )
            }
          })
        }

        return !event.canceled
      }

      emit(type, data) {
        const event = this.createEvent(type)
        Object.assign(event, data)
        return this.dispatchEvent(event)
      }

      createEvent(type) {
        return new Event(type, this)
      }

      static compareListener(a, b) {
        return a === b || a === b[EventEmitter.original] || a[EventEmitter.original] === b
      }

      static applyListener(target, listener, ...args) {
        if (typeof listener === 'function') {
          listener.apply(target, args)
        } else {
          listener.handleEvent(...args)
        }
      }
    }

    EventEmitter.original = Symbol('fngf.original')

    class Event {
      constructor(type, target) {
        this.type = type
        this.target = target
        this.canceled = false
        this.timestamp = 0
      }

      preventDefault() {
        this.canceled = true
      }
    }

    class DataTransfer extends EventEmitter {
      set(type, data) {
        this.purge()
        this.type = type
        this.data = data
        this.emit(type, {
          data,
        })
      }

      purge() {
        this.emit('purge', {
          data: this.data,
        })
        delete this.data
      }

      cut(data) {
        this.set('cut', data)
      }

      copy(data) {
        this.set('copy', data)
      }

      receive() {
        const data = this.data

        if (this.type === 'cut') {
          this.purge()
        }

        return data
      }
    }

    class MenuCommand {
      constructor(label, oncommand) {
        this.label = label
        this.oncommand = oncommand
      }

      register() {
        if (typeof GM_registerMenuCommand === 'function') {
          this.uuid = GM_registerMenuCommand(`${__`Feedly NG Filter`} - ${this.label}`, this.oncommand)
        }

        if (MenuCommand.contextmenu) {
          this.menuitem = $el`<menuitem label="${this.label}" @click="${this.oncommand}">`.first
          MenuCommand.contextmenu.appendChild(this.menuitem)
        }
      }

      unregister() {
        if (typeof GM_unregisterMenuCommand === 'function') {
          GM_unregisterMenuCommand(this.uuid)
        }

        delete this.uuid
        document.adoptNode(this.menuitem)
      }

      static register(...args) {
        const c = new MenuCommand(...args)
        c.register()
        return c
      }
    }

    MenuCommand.contextmenu = null

    class Preference extends EventEmitter {
      constructor() {
        super()

        if (Preference._instance) {
          return Preference._instance
        }

        Preference._instance = this
        this.dict = {}
      }

      has(key) {
        return key in this.dict
      }

      get(key, def) {
        return this.has(key) ? this.dict[key] : def
      }

      set(key, newValue) {
        const prevValue = this.dict[key]

        if (newValue !== prevValue) {
          this.dict[key] = newValue
          this.emit('change', {
            key,
            prevValue,
            newValue,
          })
        }

        return newValue
      }

      del(key) {
        if (!this.has(key)) {
          return
        }

        const prevValue = this.dict[key]
        delete this.dict[key]
        this.emit('delete', {
          key,
          prevValue,
        })
      }

      load(str) {
        str || (str = GM_getValue(Preference.prefName, Preference.defaultPref || '({})'))
        let obj

        try {
          obj = Serializer.parse(str)
        } catch (e) {
          if (e instanceof SyntaxError) {
            obj = eval(`(${str})`)
          }
        }

        if (!obj || typeof obj !== 'object') {
          return
        }

        this.dict = {}

        for (const key in obj) {
          this.set(key, obj[key])
        }

        this.emit('load')
      }

      write() {
        var _this$dict, _ref

        this.dict.__version__ = GM_info.script.version
        ;(_ref = ((_this$dict = this.dict), Serializer.stringify.bind(Serializer)(_this$dict))),
          GM_setValue(Preference.prefName, _ref)
      }

      autosave() {
        if (this.autosaveReserved) {
          return
        }

        window.addEventListener('unload', this.write.bind(this), false)
        this.autosaveReserved = true
      }

      exportToFile() {
        const blob = new Blob([this.serialize()], {
          type: 'application/octet-stream',
        })
        const url = URL.createObjectURL(blob)
        location.assign(url)
        URL.revokeObjectURL(url)
      }

      importFromString(str) {
        try {
          this.load(str)
        } catch (e) {
          if (!(e instanceof SyntaxError)) {
            throw e
          }

          notify(e)
          return false
        }

        notify(__`Preferences were successfully imported.`)
        return true
      }

      importFromFile() {
        openFilePicker().then(([file]) => {
          const reader = new FileReader()
          reader.addEventListener('load', () => this.importFromString(reader.result), false)
          reader.readAsText(file)
        })
      }

      toString() {
        return '[object Preference]'
      }

      serialize() {
        return Serializer.stringify(this.dict)
      }
    }

    Preference.prefName = 'settings'

    class Draggable {
      constructor(element, ignore = 'select, button, input, textarea, [tabindex]') {
        this.element = element
        this.ignore = ignore
        this.attach()
      }

      isDraggableTarget(target) {
        if (!target) {
          return false
        }

        if (target === this.element) {
          return true
        }

        return !target.matches(`${this.ignore}, :-webkit-any(${this.ignore}) *`)
      }

      attach() {
        this.element.addEventListener('mousedown', this, false, false)
      }

      detach() {
        this.element.removeEventListener('mousedown', this, false)
      }

      handleEvent(event) {
        const name = `on${event.type}`

        if (name in this) {
          this[name](event)
        }
      }

      onmousedown(event) {
        var _this$element$querySe

        if (event.button !== 0) {
          return
        }

        if (!this.isDraggableTarget(event.target)) {
          return
        }

        event.preventDefault()
        ;(_this$element$querySe = this.element.querySelector(':focus')) === null || _this$element$querySe === void 0
          ? void 0
          : _this$element$querySe.blur()
        this.offsetX = event.pageX - this.element.offsetLeft
        this.offsetY = event.pageY - this.element.offsetTop
        document.addEventListener('mousemove', this, true, false)
        document.addEventListener('mouseup', this, true, false)
      }

      onmousemove(event) {
        event.preventDefault()
        this.element.style.left = `${event.pageX - this.offsetX}px`
        this.element.style.top = `${event.pageY - this.offsetY}px`
      }

      onmouseup(event) {
        if (event.button === 0) {
          event.preventDefault()
          document.removeEventListener('mousemove', this, true)
          document.removeEventListener('mouseup', this, true)
        }
      }
    }

    class Filter {
      constructor(filter = {}) {
        var _filter$children

        this.name = filter.name || ''
        this.regexp = { ...filter.regexp }
        this.children =
          ((_filter$children = filter.children) === null || _filter$children === void 0
            ? void 0
            : _filter$children.map(f => new Filter(f))) || []
        this.hitcount = filter.hitcount || 0
        this.lasthit = filter.lasthit || 0
      }

      test(entry) {
        let name

        for (name in this.regexp) {
          if (!this.regexp[name].test(entry[name] || '')) {
            return false
          }
        }

        const hit = this.children.length ? this.children.some(filter => filter.test(entry)) : !!name

        if (hit && entry.unread) {
          this.hitcount++
          this.lasthit = Date.now()
        }

        return hit
      }

      appendChild(filter) {
        if (!(filter instanceof Filter)) {
          return null
        }

        this.removeChild(filter)
        this.children.push(filter)
        this.sortChildren()
        return filter
      }

      removeChild(filter) {
        if (!(filter instanceof Filter)) {
          return null
        }

        const index = this.children.indexOf(filter)

        if (index !== -1) {
          this.children.splice(index, 1)
        }

        return filter
      }

      sortChildren() {
        return this.children.sort((a, b) => b.name < a.name)
      }
    }

    class Entry {
      constructor(data) {
        this.data = data
      }

      get title() {
        const value = $el`<div>${this.data.title || ''}`.first.textContent
        Object.defineProperty(this, 'title', {
          configurable: true,
          value,
        })
        return value
      }

      get id() {
        return this.data.id
      }

      get url() {
        var _this$data$alternate, _this$data$alternate$

        return (_this$data$alternate = this.data.alternate) === null || _this$data$alternate === void 0
          ? void 0
          : (_this$data$alternate$ = _this$data$alternate[0]) === null || _this$data$alternate$ === void 0
          ? void 0
          : _this$data$alternate$.href
      }

      get sourceTitle() {
        return this.data.origin.title
      }

      get sourceURL() {
        return this.data.origin.streamId.replace(/^[^/]+\//, '')
      }

      get body() {
        var _ref2

        return (_ref2 = this.data.content || this.data.summary) === null || _ref2 === void 0 ? void 0 : _ref2.content
      }

      get author() {
        return this.data.author
      }

      get recrawled() {
        return this.data.recrawled
      }

      get published() {
        return this.data.published
      }

      get updated() {
        return this.data.updated
      }

      get keywords() {
        var _this$data$keywords

        return (
          ((_this$data$keywords = this.data.keywords) === null || _this$data$keywords === void 0
            ? void 0
            : _this$data$keywords.join(',')) || ''
        )
      }

      get unread() {
        return this.data.unread
      }

      get tags() {
        return this.data.tags.map(tag => tag.label)
      }
    }

    class Panel extends EventEmitter {
      constructor() {
        super()
        this.opened = false

        const onSubmit = event => {
          event.preventDefault()
          event.stopPropagation()
          this.apply()
        }

        const onKeyPress = event => {
          if (event.keyCode === KeyboardEvent.DOM_VK_ESCAPE) {
            this.emit('escape')
          }
        }

        const { element, body, buttons } = $el`
      <form class="fngf-panel" @submit="${onSubmit}" @keydown="${onKeyPress}" ref="element">
        <input type="submit" style="display: none;">
        <div class="fngf-panel-body fngf-column" ref="body"></div>
        <div class="fngf-panel-buttons fngf-row" ref="buttons">
          <div class="fngf-btn-group fngf-row">
            <button type="button" class="fngf-btn" @click="${this.apply.bind(this)}">${__`OK`}</button>
            <button type="button" class="fngf-btn" @click="${this.close.bind(this)}">${__`Cancel`}</button>
          </div>
        </div>
      </form>
    `
        new Draggable(element)
        this.dom = {
          element,
          body,
          buttons,
        }
      }

      open(anchorElement) {
        var _anchorElement, _document$querySelect

        if (this.opened) {
          return
        }

        if (!this.emit('showing')) {
          return
        }

        if (
          ((_anchorElement = anchorElement) === null || _anchorElement === void 0
            ? void 0
            : _anchorElement.nodeType) !== 1
        ) {
          anchorElement = null
        }

        document.body.appendChild(this.dom.element)
        this.opened = true
        this.snapTo(anchorElement)

        if (anchorElement) {
          const onWindowResize = () => this.snapTo(anchorElement)

          window.addEventListener('resize', onWindowResize, false)
          this.on('hidden', () => window.removeEventListener('resize', onWindowResize, false))
        }

        ;(_document$querySelect = document.querySelector(':focus')) === null || _document$querySelect === void 0
          ? void 0
          : _document$querySelect.blur()
        const selector = ':not(.feedlyng-panel) > :-webkit-any(button, input, select, textarea, [tabindex])'
        const ctrl = Array.from(this.dom.element.querySelectorAll(selector)).sort(
          (a, b) => (b.tabIndex || 0) < (a.tabIndex || 0),
        )[0]

        if (ctrl) {
          ctrl.focus()

          if (ctrl.select) {
            ctrl.select()
          }
        }

        this.emit('shown')
      }

      apply() {
        if (this.emit('apply')) {
          this.close()
        }
      }

      close() {
        if (!this.opened) {
          return
        }

        if (!this.emit('hiding')) {
          return
        }

        document.adoptNode(this.dom.element)
        this.opened = false
        this.emit('hidden')
      }

      toggle(anchorElement) {
        if (this.opened) {
          this.close()
        } else {
          this.open(anchorElement)
        }
      }

      moveTo(x, y) {
        this.dom.element.style.left = `${x}px`
        this.dom.element.style.top = `${y}px`
      }

      snapTo(anchorElement) {
        const pad = 5
        let x = pad
        let y = pad

        if (anchorElement) {
          let { left, bottom: top } = anchorElement.getBoundingClientRect()
          left += pad
          top += pad
          const { width, height } = this.dom.element.getBoundingClientRect()
          const right = left + width + pad
          const bottom = top + height + pad
          const { innerWidth, innerHeight } = window

          if (innerWidth < right) {
            left -= right - innerWidth
          }

          if (innerHeight < bottom) {
            top -= bottom - innerHeight
          }

          x = Math.max(x, left)
          y = Math.max(y, top)
        }

        this.moveTo(x, y)
      }

      getFormData(asElement) {
        const data = {}
        const elements = this.dom.body.querySelectorAll('[name]')

        function getValue(el) {
          if (el.localName === 'input' && (el.type === 'checkbox' || el.type === 'radio')) {
            return el.checked
          }

          return 'value' in el ? el.value : el.getAttribute('value')
        }

        for (const el of elements) {
          const value = asElement ? el : getValue(el)
          const path = el.name.split('.')
          let leaf = path.pop()
          const cd = path.reduce((parent, key) => {
            if (!(key in parent)) {
              parent[key] = {}
            }

            return parent[key]
          }, data)

          if (leaf.endsWith('[]')) {
            leaf = leaf.slice(0, -2)

            if (!(leaf in cd)) {
              cd[leaf] = []
            }

            cd[leaf].push(value)
          } else {
            cd[leaf] = value
          }
        }

        return data
      }

      appendContent(element) {
        if (element instanceof Array) {
          return element.map(el => this.appendContent(el))
        }

        return this.dom.body.appendChild(element)
      }

      removeContents() {
        this.dom.body.innerHTML = ''
      }
    }

    class FilterListPanel extends Panel {
      constructor(filter, isRoot) {
        super()
        this.filter = filter

        if (isRoot) {
          this.dom.element.classList.add('root')
        }

        const onAdd = () => {
          const filter = new Filter()
          filter.name = __`New Filter`
          this.on('apply', () => this.filter.appendChild(filter))
          this.appendFilter(filter)
        }

        const onPaste = () => {
          if (!clipboard.data) {
            return
          }

          const filter = new Filter(clipboard.receive())
          this.on('apply', () => this.filter.appendChild(filter))
          this.appendFilter(filter)
        }

        const { buttons, paste } = $el`
      <div class="fngf-btn-group fngf-row" ref="buttons">
        <button type="button" class="fngf-btn" @click="${onAdd}">${__`Add`}</button>
        <button type="button" class="fngf-btn" @click="${onPaste}" ref="paste" disabled>${__`Paste`}</button>
      </div>
    `

        function pasteState() {
          paste.disabled = !clipboard.data
        }

        clipboard.on('copy', pasteState)
        clipboard.on('purge', pasteState)
        pasteState()
        this.dom.buttons.insertBefore(buttons, this.dom.buttons.firstChild)
        this.on('escape', this.close.bind(this))
        this.on('showing', this.initContents)
        this.on('apply', this)
        this.on('hidden', () => {
          clipboard.off('copy', pasteState)
          clipboard.off('purge', pasteState)
        })
      }

      initContents() {
        const filter = this.filter
        const { name, terms, rules } = $el`
      <div class="fngf-panel-name fngf-row fngf-align-center" ref="name">
        ${__`Rule Name`}&nbsp;
        <input type="text" value="${filter.name}" autocomplete="off" name="name" class="fngf-grow">
      </div>
      <div class="fngf-panel-terms" ref="terms"></div>
      <div class="fngf-panel-rules fngf-column" ref="rules">
        <div class="fngf-panel-rule fngf-row fngf-align-center fngf-only">${__`No Rules`}</div>
      </div>
    `
        const labels = [
          ['title', __`Title`],
          ['url', __`URL`],
          ['sourceTitle', __`Feed Title`],
          ['sourceURL', __`Feed URL`],
          ['author', __`Author`],
          ['keywords', __`Keywords`],
          ['body', __`Contents`],
        ]

        for (const [type, labelText] of labels) {
          const randomId = `id-${Math.random().toFixed(8)}`
          const reg = filter.regexp[type]
          const sourceValue = reg ? reg.source.replace(/((?:^|[^\\])(?:\\\\)*)\\(?=\/)/g, '$1') : ''
          terms.appendChild($el`
        <label for="${randomId}">${labelText}</label>
        <input type="text" class="fngf-panel-terms-textbox" id="${randomId}" autocomplete="off" name="regexp.${type}.source" value="${sourceValue}">
        <label class="fngf-checkbox fngf-row" title="${__`Ignore Case`}">
          <input type="checkbox" name="regexp.${type}.ignoreCase" bool:checked="${
            reg === null || reg === void 0 ? void 0 : reg.ignoreCase
          }">
          <span class="fngf-btn" tabindex="0">i</span>
        </label>
      `)
        }

        this.appendContent([name, terms, rules])
        this.dom.rules = rules
        filter.children.forEach(this.appendFilter, this)
      }

      appendFilter(filter) {
        let panel

        const updateRow = () => {
          let title = __`Hit Count:\t${filter.hitcount}`

          if (filter.lasthit) {
            title += '\n'
            title += __`Last Hit:\t${new Date(filter.lasthit).toLocaleString()}`
          }

          rule.title = title
          name.textContent = filter.name
          count.textContent = filter.children.length || ''
        }

        const onEdit = () => {
          if (panel) {
            panel.close()
            return
          }

          panel = new FilterListPanel(filter)
          panel.on('shown', () => btnEdit.classList.add('active'))
          panel.on('hidden', () => {
            btnEdit.classList.remove('active')
            panel = null
          })
          panel.on('apply', () => setTimeout(updateRow, 0))
          panel.open(btnEdit)
        }

        const onCopy = () => clipboard.copy(filter)

        const onDelete = () => {
          document.adoptNode(rule)
          this.on('apply', () => this.filter.removeChild(filter))
        }

        const { rule, name, count, btnEdit } = $el`
      <div class="fngf-panel-rule fngf-row fngf-align-center" ref="rule">
        <div class="fngf-panel-rule-name" @dblclick="${onEdit}" ref="name"></div>
        <div class="fngf-panel-rule-count fngf-badge" ref="count"></div>
        <div class="fngf-panel-rule-actions fngf-btn-group fngf-menu-btn fngf-row" ref="buttons">
          <button type="button" class="fngf-btn" @click="${onEdit}" ref="btnEdit">${__`Edit`}</button>
          <div class="fngf-dropdown fngf-btn" tabindex="0">
            <div class="fngf-dropdown-menu fngf-column">
              <div class="fngf-dropdown-menu-item" @click="${onCopy}">${__`Copy`}</div>
              <div class="fngf-dropdown-menu-item" @click="${onDelete}">${__`Delete`}</div>
            </div>
          </div>
        </div>
      </div>
    `
        updateRow()
        this.dom.rules.appendChild(rule)
      }

      handleEvent(event) {
        if (event.type !== 'apply') {
          return
        }

        const data = this.getFormData(true)
        const filter = this.filter
        const regexp = {}
        let hasError = false

        for (const type in data.regexp) {
          const { source, ignoreCase } = data.regexp[type]

          if (!source.value) {
            continue
          }

          try {
            regexp[type] = new RegExp(source.value, ignoreCase.checked ? 'i' : '')
          } catch (e) {
            if (!(e instanceof SyntaxError)) {
              throw e
            }

            hasError = true
            event.preventDefault()
            source.classList.remove('error')
            source.offsetWidth.valueOf()
            source.classList.add('error')
          }
        }

        if (hasError) {
          return
        }

        const prevSource = Serializer.stringify(filter)
        filter.name = data.name.value
        filter.regexp = regexp

        if (Serializer.stringify(filter) !== prevSource) {
          filter.hitcount = 0
          filter.lasthit = 0
        }

        filter.sortChildren()
      }
    }

    Preference.defaultPref = Serializer.stringify({
      filter: {
        name: '',
        regexp: {},
        children: [
          {
            name: 'AD',
            regexp: {
              title: /^\W?(?:ADV?|PR)\b/,
            },
            children: [],
          },
        ],
      },
    })
    evalInContent(() => {
      const XHR = XMLHttpRequest
      let uniqueId = 0

      window.XMLHttpRequest = function XMLHttpRequest() {
        const req = new XHR()
        req.open = open
        req.setRequestHeader = setRequestHeader
        req.addEventListener('readystatechange', onReadyStateChange, false)
        return req
      }

      function open(method, url, ...args) {
        this.__url__ = url
        return XHR.prototype.open.call(this, method, url, ...args)
      }

      function setRequestHeader(header, value) {
        if (header === 'Authorization') {
          this.__auth__ = value
        }

        return XHR.prototype.setRequestHeader.call(this, header, value)
      }

      function onReadyStateChange() {
        if (this.readyState < 4 || this.status !== 200) {
          return
        }

        if (!/^(?:https?:)?\/\/(?:cloud\.)?feedly\.com\/v3\/streams\/contents\b/.test(this.__url__)) {
          return
        }

        const pongEventType = 'streamcontentloaded_callback' + uniqueId++
        const data = JSON.stringify({
          type: pongEventType,
          auth: this.__auth__,
          text: this.responseText,
        })
        const event = new MessageEvent('streamcontentloaded', {
          bubbles: true,
          cancelable: false,
          data: data,
          origin: location.href,
          source: null,
        })

        const onPong = ({ data }) =>
          Object.defineProperty(this, 'responseText', {
            configurable: true,
            value: data,
          })

        document.addEventListener(pongEventType, onPong, false)
        document.dispatchEvent(event)
        document.removeEventListener(pongEventType, onPong, false)
      }
    })
    const clipboard = new DataTransfer()
    const pref = new Preference()
    let rootFilterPanel
    let { contextmenu } = $el`
  <menu type="context" id="feedlyng-contextmenu">
    <menu type="context" label="${__`Feedly NG Filter`}" ref="contextmenu"></menu>
  </menu>
`
    MenuCommand.contextmenu = contextmenu
    pref.on('change', function({ key, newValue }) {
      switch (key) {
        case 'filter':
          if (!(newValue instanceof Filter)) {
            this.set('filter', new Filter(newValue))
          }

          break

        case 'language':
          __.use(newValue)

          break
      }
    })
    document.addEventListener(
      'streamcontentloaded',
      event => {
        const logging = pref.get('logging', true)
        const filter = pref.get('filter')
        const filteredEntryIds = []
        const { type: pongEventType, auth, text } = JSON.parse(event.data)
        const data = JSON.parse(text)
        let hasUnread = false
        data.items = data.items.filter(item => {
          const entry = new Entry(item)

          if (!filter.test(entry)) {
            return true
          }

          if (logging) {
            GM_log(`filtered: "${entry.title || ''}" ${entry.url}`)
          }

          filteredEntryIds.push(entry.id)

          if (entry.unread) {
            hasUnread = true
          }

          return false
        })

        if (!filteredEntryIds.length) {
          return
        }

        let ev = new MessageEvent(pongEventType, {
          bubbles: true,
          cancelable: false,
          data: JSON.stringify(data),
          origin: location.href,
          source: unsafeWindow,
        })
        document.dispatchEvent(ev)

        if (!hasUnread) {
          return
        }

        sendJSON({
          url: '/v3/markers',
          headers: {
            Authorization: auth,
          },
          data: {
            action: 'markAsRead',
            entryIds: filteredEntryIds,
            type: 'entries',
          },
        })
      },
      false,
    )
    document.addEventListener(
      'DOMContentLoaded',
      () => {
        GM_addStyle(CSS_STYLE_TEXT)
        pref.load()
        pref.autosave()
        registerMenuCommands()
        addSettingsMenuItem()
      },
      false,
    )
    document.addEventListener(
      'mousedown',
      ({ target }) => {
        if (target.matches('.fngf-dropdown')) {
          target.classList.toggle('active')
        }

        if (!target.closest('.fngf-dropdown')) {
          var _document$querySelect2

          ;(_document$querySelect2 = document.querySelector('.fngf-dropdown.active')) === null ||
          _document$querySelect2 === void 0
            ? void 0
            : _document$querySelect2.classList.remove('active')
        }
      },
      true,
    )
    document.addEventListener(
      'click',
      ({ target }) => {
        if (target.closest('.fngf-dropdown-menu-item')) {
          var _target$closest

          ;(_target$closest = target.closest('.fngf-dropdown')) === null || _target$closest === void 0
            ? void 0
            : _target$closest.classList.remove('active')
        }
      },
      true,
    )

    function $el(strings, ...values) {
      let html = ''

      if (typeof strings === 'string') {
        html = strings
      } else {
        values.forEach((v, i) => {
          html += strings[i]

          if (v === null || v === undefined) {
            return
          }

          if (v instanceof Node || v instanceof NodeList || v instanceof HTMLCollection || v instanceof Array) {
            html += `<!--${$el.dataPrefix}${i}-->`

            if (v instanceof Node) {
              return
            }

            values[i] = document.createDocumentFragment()

            for (const item of v) {
              values[i].appendChild(item)
            }

            return
          }

          html += v instanceof Object ? i : v
        })
        html += strings[strings.length - 1]
      }

      const renderer = document.createElement('template')
      const container = document.createElement('body')
      const refs = document.createDocumentFragment()
      renderer.innerHTML = html
      container.appendChild(renderer.content)
      refs.first = container.firstElementChild
      refs.last = container.lastElementChild
      const exp = `
    .//*[@ref or @*[starts-with(name(), "@") or contains(name(), ":")]] |
    .//comment()[starts-with(., "${$el.dataPrefix}")]
  `
      const xpath = document.evaluate(exp, container, null, 7, null)

      for (let i = 0; i < xpath.snapshotLength; i++) {
        const el = xpath.snapshotItem(i)

        if (el.nodeType === document.COMMENT_NODE) {
          const index = el.data.substring($el.dataPrefix.length)
          el.parentNode.replaceChild(values[index], el)
          continue
        }

        for (const { name, value } of Array.from(el.attributes)) {
          const data = values[value]

          if (name === 'ref') {
            refs[value] = el
          } else if (name.startsWith('@')) {
            $el.func(el, name.substring(1), data)
          } else if (name === ':class') {
            for (const k of Object.keys(data)) {
              el.classList.toggle(k, data[k])
            }
          } else if (name.startsWith('bool:')) {
            el[name.substring(5)] = data
          } else {
            continue
          }

          el.removeAttribute(name)
        }
      }

      Array.from(container.childNodes).forEach(node => refs.appendChild(node))
      return refs
    }

    $el.dataPrefix = '$el.data:'

    $el.func = (el, type, fn) => {
      if (type) {
        el.addEventListener(type, fn, false)
      } else {
        try {
          fn.call(el, el)
        } catch (e) {
          console.error(e)
        }
      }
    }

    function xhr(details) {
      const opt = { ...details }
      const { data } = opt
      opt.method || (opt.method = data ? 'POST' : 'GET')

      if (data instanceof Object) {
        opt.headers || (opt.headers = {})
        opt.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'
        opt.data = Object.entries(data)
          .map(kv => kv.map(encodeURIComponent).join('='))
          .join('&')
      }

      setTimeout(() => GM_xmlhttpRequest(opt), 0)
    }

    function registerMenuCommands() {
      MenuCommand.register(`${__`Setting`}...`, togglePrefPanel)
      MenuCommand.register(`${__`Language`}...`, () => {
        const { langField, select } = $el(`
      <fieldset ref="langField">
        <legend>${__`Language`}</legend>
        <select ref="select"></select>
      </fieldset>
    `)

        __.languages.forEach(lang => {
          const option = $el(`<option value="${lang}">${lang}</option>`).first

          if (lang === __.config.locale) {
            option.selected = true
          }

          select.appendChild(option)
        })

        const panel = new Panel()
        panel.appendContent(langField)
        panel.on('apply', () => pref.set('language', select.value))
        panel.open()
      })
      MenuCommand.register(`${__`Import Configuration`}...`, pref.importFromFile.bind(pref))
      MenuCommand.register(__`Export Configuration`, pref.exportToFile.bind(pref))
    }

    function sendJSON(details) {
      const opt = { ...details }
      const { data } = opt
      opt.headers || (opt.headers = {})
      opt.method = 'POST'
      opt.headers['Content-Type'] = 'application/json; charset=utf-8'
      opt.data = JSON.stringify(data)
      return xhr(opt)
    }

    function evalInContent(code) {
      const script = document.createElement('script')
      script.textContent = typeof code === 'function' ? `(${code})()` : code
      document.documentElement.appendChild(script)
      document.adoptNode(script)
    }

    function togglePrefPanel(anchorElement) {
      if (rootFilterPanel) {
        rootFilterPanel.close()
        return
      }

      rootFilterPanel = new FilterListPanel(pref.get('filter'), true)
      rootFilterPanel.on('apply', () =>
        notify(__`NG Settings were modified.\nNew filters take effect after next refresh.`),
      )
      rootFilterPanel.on('hidden', () => {
        clipboard.purge()
        rootFilterPanel = null
      })
      rootFilterPanel.open(anchorElement)
    }

    function onNGSettingCommand({ target }) {
      togglePrefPanel(target)
    }

    function addSettingsMenuItem() {
      if (!document.getElementById('filtertab')) {
        setTimeout(addSettingsMenuItem, 100)
        return
      }

      let prefListener

      function onMutation() {
        if (document.getElementById('feedly-ng-filter-setting')) {
          return
        }

        const nativeFilterItem = document.getElementById('filtertab')

        if (!nativeFilterItem) {
          return
        }

        if (prefListener) {
          pref.off('change', prefListener)
        }

        const { tab, label } = $el`
      <div class="tab" contextmenu="${MenuCommand.contextmenu.parentNode.id}" @click="${onNGSettingCommand}" ref="tab">
        <div class="header target">
          <img class="icon" src="${GM_info.script.icon}" style="cursor: pointer;">
          <div class="label nonEmpty" id="feedly-ng-filter-setting" ref="label"></div>
        </div>
      </div>
    `
        label.textContent = __`NG Setting`
        nativeFilterItem.parentNode.insertBefore(tab, nativeFilterItem.nextSibling)
        document.body.appendChild(contextmenu.parentNode)

        prefListener = ({ key }) => {
          if (key === 'language') {
            label.textContent = __`NG Setting`
          }
        }

        pref.on('change', prefListener)
      }

      new MutationObserver(onMutation).observe(document.getElementById('feedlyTabs'), {
        childList: true,
        subtree: true,
      })
      onMutation()
    }

    async function openFilePicker(multiple) {
      return new Promise(resolve => {
        const input = $el`<input type="file" @change="${() => {
          var _input$files, _ref3

          return (_ref3 = ((_input$files = input.files), Array.from(_input$files))), resolve(_ref3)
        }}">`.first
        input.multiple = multiple
        input.click()
      })
    }

    async function notify(body, options) {
      options = {
        body,
        ...notificationDefaults,
        ...options,
      }
      return new Promise((resolve, reject) => {
        Notification.requestPermission(status => {
          if (status !== 'granted') {
            reject(status)
            return
          }

          const n = new Notification(options.title, options)

          if (options.autoClose) {
            setTimeout(n.close.bind(n), options.autoClose)
          }

          resolve(n)
        })
      })
    }

    return module.exports
  }.call({})

  if (typeof exports === 'object' && typeof module !== 'undefined') {
    // CommonJS
    module.exports = $Focm$exports
  } else if (typeof define === 'function' && define.amd) {
    // RequireJS
    define(function() {
      return $Focm$exports
    })
  }
})()