Feedly NG Filter

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

Fra 28.01.2019. Se den seneste versjonen.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name           Feedly NG Filter
// @namespace      https://twitter.com/xulapp
// @version        1.0.0
// @description    ルールにマッチするアイテムを既読にして取り除きます。ルールは正規表現で記述でき、複数のルールをツリー状に組み合わせることができます。
// @author         matzkoh
// @include        https://feedly.com/*
// @icon           https://greatest.deepsurf.us/system/screenshots/screenshots/000/000/615/original/icon.png
// @screenshot     https://greatest.deepsurf.us/system/screenshots/screenshots/000/000/614/original/large.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
    })
  }
})()