Greasy Fork is available in English.

2ch Thread Viewer

2ちゃんねるのスレッドビューワ

Version vom 25.08.2015. Aktuellste Version

// ==UserScript==
// @name         2ch Thread Viewer
// @namespace    https://greasyfork.org/users/1009-kengo321
// @version      1
// @description  2ちゃんねるのスレッドビューワ
// @grant        GM_getValue
// @grant        GM_setValue
// @match        http://*.2ch.net/test/read.cgi/*
// @match        http://*.bbspink.com/test/read.cgi/*
// @license      MIT
// @noframes
// ==/UserScript==

;(function() {
  'use strict'

  var find = function(predicate, array) {
    for (var i = 0; i < array.length; i++) {
      var e = array[i]
      if (predicate(e)) return e
    }
  }
  var pushIfAbsent = function(array, value) {
    if (array.indexOf(value) === -1) array.push(value)
    return array
  }
  var not = function(fn) {
    return function() { return !fn.apply(this, arguments) }
  }
  var array = Function.prototype.call.bind([].slice)
  var curry = (function() {
    var applyOrRebind = function(func, arity, args) {
      var passed = args.concat(array(arguments, 3)).slice(0, arity)
      return arity === passed.length
             ? func.apply(this, passed)
             : applyOrRebind.bind(this, func, arity, passed)
    }
    return function(func) {
      return applyOrRebind.bind(this, func, func.length, [])
    }
  })()
  var invoke = curry(function(methodName, args, obj) {
    return obj[methodName].apply(obj, args)
  })
  var equalObj = curry(function(o1, o2) {
    return Object.keys(o1)
      .concat(Object.keys(o2))
      .reduce(pushIfAbsent, [])
      .every(function(key) { return o1[key] === o2[key] })
  })
  var prop = curry(function(propName, obj) {
    return obj[propName]
  })
  var listeners = {
    set: function(eventTypes, observer) {
      eventTypes.forEach(function(t) {
        observer[`_${t}Listener`] = observer[`_${t}`].bind(observer)
      })
    },
    add: function(eventTypes, observer, observable) {
      eventTypes.forEach(function(t) {
        observable.addEventListener(t, observer[`_${t}Listener`])
      })
    },
    remove: function(eventTypes, observer, observable) {
      eventTypes.forEach(function(t) {
        observable.removeEventListener(t, observer[`_${t}Listener`])
      })
    },
    addWithoutSet: function(eventTypes, observer, observable) {
      eventTypes.forEach(function(t) {
        observable.addEventListener(t, observer[`_${t}`].bind(observer))
      })
    },
  }

  var Observable = (function() {
    var Observable = function() {
      this._eventTypeToListeners = Object.create(null)
    }
    Observable.prototype.addEventListener = function(eventType, listener) {
      var m = this._eventTypeToListeners
      var v = m[eventType]
      if (v) v.push(listener); else m[eventType] = [listener]
    }
    Observable.prototype.removeEventListener = function(eventType, listener) {
      var v = this._eventTypeToListeners[eventType]
      if (!v) return
      var i = v.indexOf(listener)
      if (i >= 0) v.splice(i, 1)
    }
    Observable.prototype.getEventListeners = function(eventType) {
      return this._eventTypeToListeners[eventType] || []
    }
    Observable.prototype.fireEvent = function(eventType/*, ...args*/) {
      var v = this._eventTypeToListeners[eventType]
      ;(v || []).forEach(invoke('apply', [null, array(arguments, 1)]))
    }
    return Observable
  })()

  var Response = (function(_super) {
    var padZero = function(n) {
      return (n <= 9 ? '0' : '') + n
    }
    var Response = function(objParam) {
      _super.call(this)
      this.number = objParam.number
      this.name = objParam.name
      this.mail = objParam.mail
      this.jstTime = objParam.jstTime
      this.id = objParam.id
      this.content = objParam.content
      this.anchors = objParam.anchors
      this.children = []
      this.sameIdResponses = []
      this.ngId = false
      this.ngParent = false
      this.ngWord = false
    }
    Response.prototype = Object.create(_super.prototype)
    Response.prototype.getDateTimeString = function() {
      var d = new Date(this.jstTime)
      var y = d.getUTCFullYear()
      var mon = padZero(d.getUTCMonth() + 1)
      var date = padZero(d.getUTCDate())
      var h = padZero(d.getUTCHours())
      var min = padZero(d.getUTCMinutes())
      var s = padZero(d.getUTCSeconds())
      return `${y}-${mon}-${date} ${h}:${min}:${s}`
    }
    Response.prototype.getIndexOfSameIdResponses = function() {
      return this.sameIdResponses.indexOf(this)
    }
    Response.prototype.addChildren = function(children) {
      if (children.length === 0) return
      ;[].push.apply(this.children, children)
      children.forEach(invoke('setParent', [this]))
      this.fireEvent('childrenAdded', children)
    }
    Response.prototype.addSameIdResponses = function(sameIdResponses) {
      if (sameIdResponses.length === 0) return
      ;[].push.apply(this.sameIdResponses, sameIdResponses)
      this.fireEvent('sameIdResponsesAdded', sameIdResponses)
    }
    Response.prototype.getNoNgChildren = function() {
      return this.children.filter(not(invoke('isNg', [])))
    }
    Response.prototype.isNg = function() {
      return this.ngId || this.ngParent || this.ngWord
    }
    Response.prototype._setNg = function(propName, getNewVal) {
      var preNg = this.isNg()
      this[propName] = getNewVal.call(this)
      if (preNg !== this.isNg()) this.fireEvent('ngChanged', this.isNg())
    }
    Response.prototype.setNgIdIfMatchAny = function(ngIds) {
      this._setNg('ngId', function() {
        return ngIds.some(invoke('match', [this]))
      })
    }
    Response.prototype.setNgIdByAddedNgId = function(addedNgId) {
      if (!this.ngId) this.setNgIdIfMatchAny([addedNgId])
    }
    Response.prototype.setNgIdByRemovedNgId = function(removedNgId) {
      if (!this.ngId) return
      this._setNg('ngId', function() {
        return !removedNgId.match(this)
      })
    }
    Response.prototype.setNgParent = function(ngParent) {
      this._setNg('ngParent', function() { return ngParent })
    }
    Response.prototype.setNgWordIfInclude = function(ngWords) {
      this._setNg('ngWord', function() {
        return ngWords.some(function(ngWord) {
          return this.content.indexOf(ngWord) >= 0
        }, this)
      })
    }
    Response.prototype.setNgWordByAddedNgWord = function(addedNgWord) {
      if (!this.ngWord) this.setNgWordIfInclude([addedNgWord])
    }
    Response.prototype.setParent = function(parent) {
      this.setNgParent(parent.isNg())
      parent.addEventListener('ngChanged', this.setNgParent.bind(this))
    }
    return Response
  })(Observable)

  var Parser = (function() {
    var number = function(dt) {
      return parseInt(dt.firstChild.textContent.split(' ')[0])
    }
    var name = function(dt) {
      return dt.childNodes[1].textContent
    }
    var mail = function(dt) {
      var e = dt.childNodes[1]
      return e.tagName === 'FONT'
           ? ''
           : decodeURI(e.href.slice('mailto:'.length))
    }
    var jstTime = function(dt) {
      var t = dt.childNodes[2].textContent
      var datetime = /(\d{4})\/(\d{2})\/(\d{2})\(.\)/.exec(t)
      if (!datetime) return NaN
      var year = datetime[1]
      var month = datetime[2] - 1
      var date = datetime[3]
      var time = /(\d{2}):(\d{2}):(\d{2})/.exec(t)
      var hour = time ? time[1] : 0
      var minute = time ? time[2] : 0
      var seconds = time ? time[3] : 0
      return Date.UTC(year, month, date, hour, minute, seconds)
    }
    var id = function(dt) {
      var r = /ID:([\w+/]+)/.exec(dt.childNodes[2].textContent)
      return r ? r[1] : ''
    }
    var content = function(dd) {
      return [].map.call(dd.childNodes, function(n) {
        return n.tagName === 'BR' ? '\n' : n.textContent
      }).join('').replace(/\s+$/, '')
    }
    var anchors = function(dd, responseNumber) {
      return [].filter.call(dd.childNodes, function(n) {
        return n.tagName === 'A' && n.textContent.startsWith('>>')
      }).map(function(n) {
        return parseInt(n.textContent.slice('>>'.length))
      }).filter(function(num) {
        return num < responseNumber
      }).reduce(pushIfAbsent, [])
    }
    var createResponse = function(dt, dd) {
      var num = number(dt)
      return new Response({
        number: num,
        name: name(dt),
        mail: mail(dt),
        jstTime: jstTime(dt),
        id: id(dt),
        content: content(dd),
        anchors: anchors(dd, num),
      })
    }
    var responses = function(document) {
      var dl = document.querySelector('.thread')
      var dt = dl.getElementsByTagName('dt')
      var dd = dl.getElementsByTagName('dd')
      var result = []
      for (var i = 0; i < dt.length; i++) {
        result.push(createResponse(dt[i], dd[i]))
      }
      return result
    }
    var postedResShowElem = function(document) {
      return find(function(e) {
        return e.textContent === '新着レスの表示'
      }, document.getElementsByTagName('center'))
    }
    var hasThreadClosed = function(document) {
      return !postedResShowElem(document)
    }
    var ads = function(document) {
      return [
        '.ad--right',
        '.js--ad--top',
        '.js--ad--bottom',
      ].reduce(function(a, s) {
        ;[].push.apply(a, document.querySelectorAll(s))
        return a
      }, [])
    }
    var hrAbove = function(e) {
      var p = e.previousSibling
      if (!p) return null
      var hr = p.previousSibling
      return hr && hr.tagName === 'HR' ? hr : null
    }
    var pageSizeElem = function(document) {
      return find(function(e) {
        return e.textContent.endsWith('KB')
            && e.getAttribute('color') === 'red'
            && e.getAttribute('face') === 'Arial'
      }, document.getElementsByTagName('font'))
    }
    var pushIfTruthy = function(array, value) {
      if (value) array.push(value)
    }
    var elementsToRemove = function(document) {
      var result = []
      var e = postedResShowElem(document)
      if (e) {
        result.push(e)
        pushIfTruthy(result, hrAbove(e))
      }
      pushIfTruthy(result, pageSizeElem(document))
      return result
    }
    var postForm = function(document) {
      return find(function(f) {
        return f.getAttribute('action').startsWith('../test/bbs.cgi')
            && f.method.toUpperCase() === 'POST'
      }, document.querySelectorAll('form'))
    }
    var boardId = function(document) {
      var l = document.location
      if (!l) return ''
      var r = /\/test\/read.cgi\/([^/]+)/.exec(l.pathname)
      return r ? r[1] : ''
    }
    var threadNumber = function(document) {
      var l = document.location
      if (!l) return 0
      var r = /\/test\/read.cgi\/[^/]+\/(\d+)/.exec(l.pathname)
      return r ? parseInt(r[1]) : 0
    }
    var Parser = function() {}
    Parser.prototype.parse = function(document) {
      return {
        responses: responses(document),
        threadClosed: hasThreadClosed(document),
        ads: ads(document),
        elementsToRemove: elementsToRemove(document),
        threadRootElement: document.querySelector('.thread'),
        postForm: postForm(document),
        boardId: boardId(document),
        threadNumber: threadNumber(document),
        floatedSpan: document.querySelector('body > div > span'),
      }
    }
    return Parser
  })()

  var ResponseRequest = (function() {
    var HTTP_OK = 200
    var parseResponseText = function(responseText) {
      var d = new DOMParser().parseFromString(responseText, 'text/html')
      var r = new Parser().parse(d)
      return {
        responses: r.responses.slice(1),
        threadClosed: r.threadClosed,
      }
    }
    var onload = function(xhr, resolve, reject) {
      return function() {
        if (xhr.status === HTTP_OK) {
          try {
            resolve(parseResponseText(xhr.responseText))
          } catch (e) {
            reject(e)
          }
        } else {
          reject(new Error(xhr.status + ' ' + xhr.statusText))
        }
      }
    }
    var ResponseRequest = function() {}
    ResponseRequest.prototype.send = function(basePath, startResponseNumber) {
      return new Promise(function(resolve, reject) {
        var xhr = new XMLHttpRequest()
        xhr.timeout = 10000
        xhr.onload = onload(xhr, resolve, reject)
        xhr.onerror = function() { reject(new Error('エラー')) }
        xhr.ontimeout = function() { reject(new Error('時間切れ')) }
        xhr.open('GET', `${basePath}${startResponseNumber - 1}n-`)
        xhr.overrideMimeType('text/html; charset=shift_jis')
        xhr.send()
      })
    }
    return ResponseRequest
  })()

  var NgId = (function() {
    var msPerDay = 86400000
    var truncTime = function(dateTime) {
      return dateTime - dateTime % msPerDay
    }
    var NgId = function(boardId, jstTime, id) {
      this.boardId = boardId
      this.activeDate = truncTime(jstTime)
      this.id = id
    }
    NgId.prototype.match = function(response) {
      return this.id === response.id
          && this.activeDate === truncTime(response.jstTime)
    }
    NgId.prototype.getActiveDateString = function() {
      var d = new Date(this.activeDate)
      return `${d.getUTCFullYear()}-${d.getUTCMonth() + 1}-${d.getUTCDate()}`
    }
    return NgId
  })()

  var Config = (function(_super) {
    var ngWordObj = function(ngWord, boardId, threadNumber) {
      var result = {ngWord: ngWord}
      if (boardId) result.boardId = boardId
      if (threadNumber) result.threadNumber = threadNumber
      return result
    }
    var Config = function(getValue, setValue) {
      _super.call(this)
      this._getValue = getValue
      this._setValue = setValue
    }
    Config.prototype = Object.create(_super.prototype)
    Config.prototype.addNgWord = function(ngWord, boardId, threadNumber) {
      var o = ngWordObj(ngWord, boardId, threadNumber)
      this._setNgWords(this.getNgWords().concat(o))
      this.fireEvent('ngWordAdded', o)
    }
    Config.prototype.removeNgWord = function(ngWord, boardId, threadNumber) {
      if (threadNumber) threadNumber = Math.trunc(threadNumber)
      var o = ngWordObj(ngWord, boardId, threadNumber)
      this._setNgWords(this.getNgWords().filter(not(equalObj(o))))
      this.fireEvent('ngWordRemoved', o)
    }
    Config.prototype.removeAllNgWords = function() {
      this._setNgWords([])
      this.fireEvent('allNgWordsRemoved')
    }
    Config.prototype.getNgWords = function() {
      return JSON.parse(this._getValue('ngWords', '[]'))
    }
    Config.prototype._setNgWords = function(ngWords) {
      this._setValue('ngWords', JSON.stringify(ngWords))
    }
    Config.prototype.addNgId = function(ngId) {
      this._setNgIds(this.getNgIds().concat(ngId))
      this.fireEvent('ngIdAdded', ngId)
    }
    Config.prototype.removeNgId = function(ngId) {
      this._setNgIds(this.getNgIds().filter(not(equalObj(ngId))))
      this.fireEvent('ngIdRemoved', ngId)
    }
    Config.prototype.removeAllNgIds = function() {
      this._setNgIds([])
      this.fireEvent('allNgIdsRemoved')
    }
    Config.prototype.getNgIds = function() {
      return JSON.parse(this._getValue('ngIds', '[]')).map(function(o) {
        return new NgId(o.boardId, o.activeDate, o.id)
      })
    }
    Config.prototype._setNgIds = function(ngIds) {
      this._setValue('ngIds', JSON.stringify(ngIds))
    }
    return Config
  })(Observable)

  var Thread = (function(_super) {
    var putAsArray = function(obj, key, value) {
      var array = obj[key]
      if (array) array.push(value); else obj[key] = [value]
      return obj
    }
    var putResById = function(obj, res) {
      return res.id ? putAsArray(obj, res.id, res) : obj
    }
    var putResByPassedAnchor = curry(function(res, obj, anchor) {
      return putAsArray(obj, anchor, res)
    })
    var putResByAnchor = function(obj, res) {
      return res.anchors.reduce(putResByPassedAnchor(res), obj)
    }
    var putResByNumber = function(obj, res) {
      obj[res.number] = res
      return obj
    }
    var addNewChild = function(responses, addedResponses) {
      var all = responses.concat(addedResponses)
      var resNumToRes = all.reduce(putResByNumber, {})
      var addedAnchors = addedResponses.reduce(putResByAnchor, {})
      Object.keys(addedAnchors).forEach(function(anchor) {
        var r = resNumToRes[anchor]
        if (r) r.addChildren(addedAnchors[anchor])
      })
    }
    var addSameId = curry(function(idToRes, response) {
      var sameId = idToRes[response.id]
      if (sameId) response.addSameIdResponses(sameId)
    })
    var addNewSameId = function(responses, addedResponses) {
      responses.forEach(addSameId(addedResponses.reduce(putResById, {})))
      addedResponses.forEach(
        addSameId(responses.concat(addedResponses).reduce(putResById, {})))
    }
    var eventTypes = [
      'ngIdAdded',
      'ngIdRemoved',
      'allNgIdsRemoved',
      'ngWordAdded',
      'ngWordRemoved',
      'allNgWordsRemoved',
    ]
    var Thread = function(config, boardId, threadNumber) {
      _super.call(this)
      this._responses = []
      this._boardId = boardId
      this._threadNumber = threadNumber
      this.config = config
      listeners.addWithoutSet(eventTypes, this, config)
    }
    Thread.prototype = Object.create(_super.prototype)
    Thread.prototype.addResponses = function(responses) {
      responses.forEach(invoke('setNgIdIfMatchAny', [this._getNgIds()]))
      responses.forEach(invoke('setNgWordIfInclude', [this._getNgWords()]))
      addNewChild(this._responses, responses)
      addNewSameId(this._responses, responses)
      ;[].push.apply(this._responses, responses)
      this.fireEvent('responsesAdded', responses)
    }
    Thread.prototype._testNgWordForValid = function(ngWord) {
      var w = ngWord
      return (!w.boardId || w.boardId === this._boardId)
          && (!w.threadNumber || w.threadNumber === this._threadNumber)
    }
    Thread.prototype._getNgWords = function() {
      return this.config.getNgWords()
        .filter(this._testNgWordForValid.bind(this))
        .map(prop('ngWord'))
    }
    Thread.prototype._getNgIds = function() {
      return this.config.getNgIds().filter(function(ngId) {
        return this._boardId === ngId.boardId
      }, this)
    }
    Thread.prototype.getLastResponseNumber = function() {
      var r = this._responses
      var last = r[r.length - 1]
      return last ? last.number : -1
    }
    Thread.prototype.addNgId = function(jstTime, ngId) {
      this.config.addNgId(new NgId(this._boardId, jstTime, ngId))
    }
    Thread.prototype._ngIdAdded = function(addedNgId) {
      this._responses.forEach(invoke('setNgIdByAddedNgId', [addedNgId]))
    }
    Thread.prototype._ngIdRemoved = function(removedNgId) {
      if (this._boardId === removedNgId.boardId)
        this._responses.forEach(invoke('setNgIdByRemovedNgId', [removedNgId]))
    }
    Thread.prototype._allNgIdsRemoved = function() {
      this._responses.forEach(invoke('setNgIdIfMatchAny', [[]]))
    }
    Thread.prototype.addNgWordForBoard = function(ngWord) {
      this.config.addNgWord(ngWord, this._boardId)
    }
    Thread.prototype.addNgWordForThread = function(ngWord) {
      this.config.addNgWord(ngWord, this._boardId, this._threadNumber)
    }
    Thread.prototype._ngWordAdded = function(addedNgWord) {
      this._responses
        .forEach(invoke('setNgWordByAddedNgWord', [addedNgWord.ngWord]))
    }
    Thread.prototype._ngWordRemoved = function() {
      this._responses
        .forEach(invoke('setNgWordIfInclude', [this._getNgWords()]))
    }
    Thread.prototype._allNgWordsRemoved = function() {
      this._responses.forEach(invoke('setNgWordIfInclude', [[]]))
    }
    return Thread
  })(Observable)

  var ResponseView = (function() {
    var getResponseViewByChild = function(children, selector, elem) {
      for (var i = 0; i < children.length; i++) {
        var v = children[i]._getResponseViewBy(selector, elem)
        if (v) return v
      }
    }
    var eventTypes = ['childrenAdded', 'sameIdResponsesAdded', 'ngChanged']
    var ResponseView = function(document, response, root) {
      this._doc = document
      this._response = response
      this._factory = new ResponseView.Factory(document, response, root)
      this.rootElement = this._factory.createResponseElement()
      this._childResponseViews = []
      this._sameIdResponseViews = []
      listeners.set(eventTypes, this)
      listeners.add(eventTypes, this, this._response)
      this._childNgChangedListener = this._childNgChanged.bind(this)
      this._addListenersToChildren(response.children)
    }
    ResponseView.new = curry(function(document, response) {
      return new ResponseView(document, response)
    })
    ResponseView.prototype._childrenAdded = function(addedChildren) {
      this._addListenersToChildren(addedChildren)
      this._updateResNumElem()
      this._appendAddedChildren(addedChildren)
    }
    ResponseView.prototype._sameIdResponsesAdded = function(addedSameId) {
      this._updateIdElem()
      this._appendAddedSameId(addedSameId)
    }
    ResponseView.prototype._ngChanged = function(ng) {
      if (ng) this._destroyAllResponseViews()
      this._replaceRootWithNew()
    }
    ResponseView.prototype._isChildrenVisibleAndAllNg = function() {
      return Boolean(this.rootElement.querySelector('.children'))
          && this._response.getNoNgChildren().length === 0
    }
    ResponseView.prototype._childNgChanged = function() {
      this._updateResNumElem()
      if (this._isChildrenVisibleAndAllNg()) this._destroyChildren()
    }
    ResponseView.prototype._addListenersToChildren = function(children) {
      children.forEach(invoke('addEventListener'
                            , ['ngChanged', this._childNgChangedListener]))
    }
    ResponseView.prototype._removeListenersFromChildren = function() {
      this._response.children
        .forEach(invoke('removeEventListener'
                      , ['ngChanged', this._childNgChangedListener]))
    }
    ResponseView.prototype._removeListenersFromResponse = function() {
      listeners.remove(eventTypes, this, this._response)
    }
    ResponseView.prototype._updateResNumElem = function() {
      if (this._response.isNg()) return
      var numElem = this.rootElement.querySelector('header .number')
      if (numElem) this._factory.updateHeaderNumClass(numElem)
    }
    ResponseView.prototype._appendAdded = function(added, propName, selector) {
      var views = added.map(ResponseView.new(this._doc))
      ;[].push.apply(this[propName], views)
      var toggled = this.rootElement.querySelector(selector)
      views.map(prop('rootElement')).forEach(toggled.appendChild.bind(toggled))
    }
    ResponseView.prototype._appendAddedChildren = function(addedChildren) {
      if (this._childResponseViews.length) {
        this._appendAdded(addedChildren, '_childResponseViews', '.children')
      }
    }
    ResponseView.prototype._appendAddedSameId = function(addedSameId) {
      if (this._sameIdResponseViews.length) {
        this._appendAdded(addedSameId, '_sameIdResponseViews', '.sameId')
      }
    }
    ResponseView.prototype._getIdValElem = function() {
      return this.rootElement.querySelector('header .id .value')
    }
    ResponseView.prototype._updateIdValueElem = function() {
      this._factory.updateIdValClass(this._getIdValElem())
    }
    ResponseView.prototype._hasIdCountElem = function() {
      return Boolean(this.rootElement.querySelector('header .id .count'))
    }
    ResponseView.prototype._insertIdCountElem = function() {
      var e = this._getIdValElem()
      e.parentNode.insertBefore(this._factory.createIdCount(), e.nextSibling)
    }
    ResponseView.prototype._updateIdTotalElem = function() {
      var e = this.rootElement.querySelector('header .id .count .total')
      e.textContent = this._response.sameIdResponses.length
    }
    ResponseView.prototype._updateIdElem = function() {
      if (this._response.isNg()) return
      this._updateIdValueElem()
      if (this._hasIdCountElem()) {
        this._updateIdTotalElem()
      } else {
        this._insertIdCountElem()
      }
    }
    ResponseView.prototype._replaceRootWithNew = function() {
      var old = this.rootElement
      this.rootElement = this._factory.createResponseElement()
      var p = old.parentNode
      if (p) p.replaceChild(this.rootElement, old)
    }
    ResponseView.prototype._destroyResponseViews = function(propName) {
      this[propName].forEach(function(v) {
        v._removeListenersFromResponse()
        v._removeListenersFromChildren()
        v._destroyAllResponseViews()
      })
      this[propName] = []
    }
    ResponseView.prototype._destroyAllResponseViews = function() {
      this._destroyResponseViews('_childResponseViews')
      this._destroyResponseViews('_sameIdResponseViews')
    }
    ResponseView.prototype._newSubResponseViews = function(propName) {
      return this._response[propName].map(ResponseView.new(this._doc))
    }
    ResponseView.prototype._insertAfterContent = function(views, methodName) {
      var responseElems = views.map(prop('rootElement'))
      var toggledElem = this._factory[methodName](responseElems)
      var contentElem = this.rootElement.querySelector('.content')
      this.rootElement.insertBefore(toggledElem, contentElem.nextSibling)
    }
    ResponseView.prototype._destroyChildren = function() {
      this.rootElement.querySelector('.children').remove()
      this._destroyResponseViews('_childResponseViews')
    }
    ResponseView.prototype.toggleChildren = function() {
      if (this._response.children.length === 0) return
      var e = this.rootElement.querySelector('.children')
      if (e) {
        this._destroyChildren()
      } else {
        var views = this._newSubResponseViews('children')
        this._childResponseViews = views
        this._insertAfterContent(views, 'createChildrenElement')
      }
    }
    ResponseView.prototype.toggleSameId = function() {
      if (this._response.sameIdResponses.length < 2) return
      var e = this.rootElement.querySelector('.sameId')
      if (e) {
        e.remove()
        this._destroyResponseViews('_sameIdResponseViews')
      } else {
        var views = this._newSubResponseViews('sameIdResponses')
        this._sameIdResponseViews = views
        this._insertAfterContent(views, 'createSameIdElement')
      }
    }
    ResponseView.prototype._getResponseViewBy = function(selector, elem) {
      if (this.rootElement.querySelector(selector) === elem) return this
      var resViews = this._childResponseViews.concat(this._sameIdResponseViews)
      return getResponseViewByChild(resViews, selector, elem)
    }
    ResponseView.prototype.getResponseViewByNumElem = function(numElem) {
      return this._getResponseViewBy('header .number', numElem)
    }
    ResponseView.prototype.getResponseViewByIdValElem = function(idValElem) {
      return this._getResponseViewBy('header .id .value', idValElem)
    }
    return ResponseView
  })()

  ResponseView.Factory = (function() {
    var replaceMatchedByCreatedElem = function(textNode, regExp, createElem) {
      var document = textNode.ownerDocument
      var result = document.createDocumentFragment()
      var begin = 0
      var text = textNode.nodeValue
      for (var r; r = regExp.exec(text);) {
        result.appendChild(document.createTextNode(text.slice(begin, r.index)))
        result.appendChild(createElem(r[0]))
        begin = regExp.lastIndex
      }
      result.appendChild(document.createTextNode(text.slice(begin)))
      result.normalize()
      return result
    }
    var replaceTextNodeIfMatched = function(node, regExp, createElem) {
      ;[].filter.call(node.childNodes, function(child) {
        return child.nodeType === Node.TEXT_NODE
      }).forEach(function(textNode) {
        var newNode = replaceMatchedByCreatedElem(textNode, regExp, createElem)
        node.replaceChild(newNode, textNode)
      }, this)
      return node
    }
    var replaceHash = function(location, hash) {
      return '//' + location.host + location.pathname + location.search + hash
    }
    var createBR = function(document) {
      return function() {
        return document.createElement('br')
      }
    }
    var createAnchor = curry(function(document, responseNumber, matchedText) {
      var matchedNumber = parseInt(matchedText.slice(2))
      if (matchedNumber >= responseNumber) {
        return document.createTextNode(matchedText)
      }
      var result = document.createElement('a')
      result.href = replaceHash(document.location, '#res' + matchedNumber)
      result.textContent = matchedText
      return result
    })
    var createLink = curry(function(document, matchedText) {
      var result = document.createElement('a')
      result.href = matchedText[0] === 'h' ? matchedText : 'h' + matchedText
      result.target = '_blank'
      result.textContent = matchedText
      return result
    })
    var Factory = function(document, response, root) {
      this._doc = document
      this._response = response
      this._root = root
    }
    Factory.prototype._createIdTotal = function() {
      var result = this._doc.createElement('span')
      result.className = 'total'
      result.textContent = this._response.sameIdResponses.length
      return result
    }
    Factory.prototype._createIdIndex = function() {
      var i = this._response.getIndexOfSameIdResponses() + 1
      return this._doc.createTextNode('(' + i + '/')
    }
    Factory.prototype.updateIdValClass = function(idValElem) {
      var n = this._response.sameIdResponses.length
      var l = idValElem.classList
      if (n >= 2) l.add('sameIdExist')
      if (n >= 5) l.add('sameIdExist5')
    }
    Factory.prototype._createIdVal = function() {
      var result = this._doc.createElement('span')
      result.className = 'value'
      result.textContent = this._response.id
      this.updateIdValClass(result)
      return result
    }
    Factory.prototype._createIdNgButton = function() {
      var result = this._doc.createElement('span')
      result.className = 'ngButton'
      result.textContent = '[NGID]'
      result.dataset.id = this._response.id
      result.dataset.jstTime = this._response.jstTime
      return result
    }
    Factory.prototype.createIdCount = function() {
      var result = this._doc.createDocumentFragment()
      if (this._response.sameIdResponses.length >= 2) {
        var count = this._doc.createElement('span')
        count.className = 'count'
        count.appendChild(this._createIdIndex())
        count.appendChild(this._createIdTotal())
        count.appendChild(this._doc.createTextNode(')'))
        result.appendChild(count)
      }
      return result
    }
    Factory.prototype._createId = function() {
      var result = this._doc.createDocumentFragment()
      if (this._response.id) {
        var id = this._doc.createElement('span')
        id.className = 'id'
        id.appendChild(this._createIdVal())
        id.appendChild(this.createIdCount())
        id.appendChild(this._createIdNgButton())
        result.appendChild(id)
      }
      return result
    }
    Factory.prototype.updateHeaderNumClass = function(numElem) {
      var childNum = this._response.getNoNgChildren().length
      numElem.classList[childNum >= 1 ? 'add' : 'remove']('hasChild')
      numElem.classList[childNum >= 3 ? 'add' : 'remove']('hasChild3')
    }
    Factory.prototype._createHeaderNum = function() {
      var result = this._doc.createElement('span')
      result.className = 'number'
      this.updateHeaderNumClass(result)
      result.textContent = this._response.number
      return result
    }
    Factory.prototype._createHeaderName = function() {
      var result = this._doc.createElement('span')
      result.className = 'name'
      result.textContent = this._response.name
      return result
    }
    Factory.prototype._createHeaderMail = function() {
      var result = this._doc.createDocumentFragment()
      if (this._response.mail) {
        var e = this._doc.createElement('span')
        e.className = 'mail'
        e.textContent = '[' + this._response.mail + ']'
        result.appendChild(e)
      }
      return result
    }
    Factory.prototype._createHeaderTime = function() {
      var result = this._doc.createDocumentFragment()
      if (!Number.isNaN(this._response.jstTime)) {
        var datetime = this._doc.createElement('time')
        datetime.textContent = this._response.getDateTimeString()
        result.appendChild(datetime)
      }
      return result
    }
    Factory.prototype._createHeader = function() {
      var result = this._doc.createElement('header')
      result.appendChild(this._createHeaderNum())
      result.appendChild(this._createHeaderName())
      result.appendChild(this._createHeaderMail())
      result.appendChild(this._createHeaderTime())
      result.appendChild(this._createId())
      return result
    }
    Factory.prototype._createContent = function() {
      var f = this._doc.createDocumentFragment()
      f.appendChild(this._doc.createTextNode(this._response.content))
      replaceTextNodeIfMatched(f, /\n/g, createBR(this._doc))
      replaceTextNodeIfMatched(f
                             , />>\d+/g
                             , createAnchor(this._doc, this._response.number))
      replaceTextNodeIfMatched(f, /h?ttps?:\/\/[^\s]+/g, createLink(this._doc))
      var result = this._doc.createElement('div')
      result.className = 'content'
      result.appendChild(f)
      return result
    }
    Factory.prototype.createResponseElement = function() {
      var result = this._doc.createElement('article')
      if (this._response.isNg()) {
        result.classList.add('ng')
        result.appendChild(this._createNgResponse())
      } else {
        result.appendChild(this._createHeader())
        result.appendChild(this._createContent())
      }
      if (this._root) result.id = 'res' + this._response.number
      return result
    }
    Factory.prototype._createNgResponse = function() {
      var text = this._response.number + ' あぼーん'
      return this._doc.createTextNode(text)
    }
    Factory.prototype.createChildrenElement = function(responseElements) {
      var result = this._doc.createElement('div')
      result.className = 'children'
      responseElements.forEach(result.appendChild.bind(result))
      return result
    }
    Factory.prototype.createSameIdElement = function(responseElements) {
      var result = this._doc.createElement('div')
      result.className = 'sameId'
      responseElements.forEach(result.appendChild.bind(result))
      return result
    }
    return Factory
  })()

  var ConfigView = (function() {
    var addTH = function(doc, row) {
      return function(text) {
        var th = doc.createElement('th')
        th.textContent = text
        row.appendChild(th)
      }
    }
    var setTHead = function(tHead, texts) {
      var d = tHead.ownerDocument
      var row = tHead.insertRow()
      texts.forEach(addTH(d, row))
    }
    var addCell = function(row, text) {
      var result = row.insertCell()
      result.textContent = text
      return result
    }
    var addDelCell = function(row) {
      var result = addCell(row, '削除')
      result.className = 'removeButton'
      return result
    }
    var addNgIdDelCell = function(row, ngId) {
      var c = addDelCell(row)
      var s = c.dataset
      s.boardId = ngId.boardId
      s.activeDate = ngId.activeDate
      s.id = ngId.id
    }
    var newNgIdByDataset = function(dataset) {
      return new NgId(dataset.boardId, dataset.activeDate, dataset.id)
    }
    var getDelCell = function(row) {
      return row.cells[3]
    }
    var setNgIdRow = function(row, ngId) {
      addCell(row, ngId.boardId)
      addCell(row, ngId.getActiveDateString())
      addCell(row, ngId.id)
      addNgIdDelCell(row, ngId)
    }
    var setNgIdTBody = function(tBody, ngIds) {
      ngIds.forEach(function(ngId) {
        setNgIdRow(tBody.insertRow(), ngId)
      })
    }
    var newNgWordByDataset = function(dataset) {
      return {
        boardId: dataset.boardId,
        threadNumber: dataset.threadNumber
                      ? parseInt(dataset.threadNumber)
                      : undefined,
        ngWord: dataset.ngWord,
      }
    }
    var addNgWordDelCell = function(row, ngWord) {
      var c = addDelCell(row)
      var s = c.dataset
      s.ngWord = ngWord.ngWord
      if (ngWord.boardId) s.boardId = ngWord.boardId
      if (ngWord.threadNumber) s.threadNumber = ngWord.threadNumber
    }
    var setNgWordRow = function(row, ngWord) {
      addCell(row, ngWord.boardId)
      addCell(row, ngWord.threadNumber)
      addCell(row, ngWord.ngWord)
      addNgWordDelCell(row, ngWord)
    }
    var setNgWordTBody = function(tBody, ngWords) {
      ngWords.forEach(function(ngWord) {
        setNgWordRow(tBody.insertRow(), ngWord)
      })
    }
    var wrap = function(content) {
      var result = content.ownerDocument.createElement('div')
      result.className = 'wrap'
      result.appendChild(content)
      return result
    }
    var eventTypes = [
      'ngIdAdded',
      'ngIdRemoved',
      'allNgIdsRemoved',
      'ngWordAdded',
      'ngWordRemoved',
      'allNgWordsRemoved',
    ]
    var ConfigView = function(document, config) {
      this._doc = document
      this._config = config
      this.rootElement = this._createRootElem()
      listeners.set(eventTypes, this)
      listeners.add(eventTypes, this, config)
    }
    ConfigView.prototype._createOption = function(value, text) {
      var result = this._doc.createElement('option')
      result.value = value
      result.textContent = text
      return result
    }
    ConfigView.prototype._createTargetSelect = function() {
      var result = this._doc.createElement('select')
      result.appendChild(this._createOption('thread', 'このスレッド'))
      result.appendChild(this._createOption('board', 'この板'))
      result.appendChild(this._createOption('all', '全体'))
      return result
    }
    ConfigView.prototype._createNgWordInput = function() {
      var result = this._doc.createElement('input')
      result.className = 'ngWordInput'
      return result
    }
    ConfigView.prototype._createNgWordAddButton = function() {
      var result = this._doc.createElement('input')
      result.className = 'addButton'
      result.type = 'button'
      result.value = '追加'
      return result
    }
    ConfigView.prototype._createNgWordAddP = function() {
      var result = this._doc.createElement('p')
      result.className = 'add'
      result.appendChild(this._createTargetSelect())
      result.appendChild(this._createNgWordInput())
      result.appendChild(this._createNgWordAddButton())
      return result
    }
    ConfigView.prototype._createNgWordTable = function() {
      var result = this._doc.createElement('table')
      setTHead(result.createTHead(), ['板', 'スレッド', 'NGワード', ''])
      setNgWordTBody(result.createTBody(), this._config.getNgWords())
      return result
    }
    ConfigView.prototype._createRemoveAllButton = function() {
      var result = this._doc.createElement('span')
      result.className = 'removeAllButton'
      result.textContent = '[すべて削除]'
      return result
    }
    ConfigView.prototype._createSectionHeader = function(text) {
      var result = this._doc.createElement('h2')
      result.appendChild(this._doc.createTextNode(text))
      result.appendChild(this._createRemoveAllButton())
      return result
    }
    ConfigView.prototype._createNgWordSection = function() {
      var result = this._doc.createElement('section')
      result.className = 'ngWordSection'
      result.appendChild(this._createSectionHeader('NGワード'))
      result.appendChild(this._createNgWordAddP())
      result.appendChild(wrap(this._createNgWordTable()))
      return result
    }
    ConfigView.prototype._createNgIdTable = function() {
      var result = this._doc.createElement('table')
      setTHead(result.createTHead(), ['板', '有効日', 'ID', ''])
      setNgIdTBody(result.createTBody(), this._config.getNgIds())
      return result
    }
    ConfigView.prototype._createNgIdSection = function() {
      var result = this._doc.createElement('section')
      result.className = 'ngIdSection'
      result.appendChild(this._createSectionHeader('NGID'))
      result.appendChild(wrap(this._createNgIdTable()))
      return result
    }
    ConfigView.prototype._createRootElem = function() {
      var result = this._doc.createElement('div')
      result.className = 'config'
      result.appendChild(this._createNgWordSection())
      result.appendChild(this._createNgIdSection())
      return result
    }
    ConfigView.prototype._getNgWordTable = function() {
      return this.rootElement.querySelector('.ngWordSection table')
    }
    ConfigView.prototype._ngWordAdded = function(addedNgWord) {
      var r = this._getNgWordTable().tBodies[0].insertRow()
      setNgWordRow(r, addedNgWord)
    }
    ConfigView.prototype._ngWordRemoved = function(removedNgWord) {
      var rows = this._getNgWordTable().tBodies[0].rows
      ;[].filter.call(rows, function(row) {
        var ngWord = newNgWordByDataset(getDelCell(row).dataset)
        return equalObj(ngWord, removedNgWord)
      }).forEach(invoke('remove', []))
    }
    ConfigView.prototype._allNgWordsRemoved = function() {
      var newTable = this._createNgWordTable()
      var oldTable = this._getNgWordTable()
      oldTable.parentNode.replaceChild(newTable, oldTable)
    }
    ConfigView.prototype._getNgIdTable = function() {
      return this.rootElement.querySelector('.ngIdSection table')
    }
    ConfigView.prototype._ngIdAdded = function(addedNgId) {
      var r = this._getNgIdTable().tBodies[0].insertRow()
      setNgIdRow(r, addedNgId)
    }
    ConfigView.prototype._ngIdRemoved = function(removedNgId) {
      var rows = this._getNgIdTable().tBodies[0].rows
      ;[].filter.call(rows, function(row) {
        var ngId = newNgIdByDataset(getDelCell(row).dataset)
        return equalObj(ngId, removedNgId)
      }).forEach(invoke('remove', []))
    }
    ConfigView.prototype._allNgIdsRemoved = function() {
      var newTable = this._createNgIdTable()
      var oldTable = this._getNgIdTable()
      oldTable.parentNode.replaceChild(newTable, oldTable)
    }
    ConfigView.prototype.destroy = function() {
      this.rootElement.remove()
      listeners.remove(eventTypes, this, this._config)
    }
    ConfigView.prototype._getNgWordInput = function() {
      return this.rootElement.querySelector('.ngWordSection .add .ngWordInput')
    }
    ConfigView.prototype.getNgWordInputValue = function() {
      return this._getNgWordInput().value.trim()
    }
    ConfigView.prototype.clearNgWordInputValue = function() {
      this._getNgWordInput().value = ''
    }
    ConfigView.prototype.getNgWordAddTarget = function() {
      return this.rootElement.querySelector('.ngWordSection .add select').value
    }
    return ConfigView
  })()

  var ThreadView = (function() {
    var createTopBar = function(document) {
      var configToggle = document.createElement('span')
      configToggle.className = 'configToggle'
      configToggle.textContent = 'NG設定▼'
      var result = document.createElement('div')
      result.className = 'topBar'
      result.appendChild(configToggle)
      return result
    }
    var createBottomBar = function(document) {
      var reloadButton = document.createElement('input')
      reloadButton.type = 'button'
      reloadButton.value = '新着レスの取得'
      reloadButton.className = 'reloadButton'
      var reloadMessage = document.createElement('span')
      reloadMessage.className = 'reloadMessage'
      var result = document.createElement('div')
      result.className = 'bottomBar'
      result.appendChild(reloadButton)
      result.appendChild(reloadMessage)
      return result
    }
    var createRoot = function(document) {
      var main = document.createElement('div')
      main.className = 'main'
      var result = document.createElement('div')
      result.className = 'threadView'
      result.appendChild(createTopBar(document))
      result.appendChild(main)
      result.appendChild(createBottomBar(document))
      return result
    }
    var ThreadView = function(document, thread) {
      this.doc = document
      this._thread = thread
      this.rootElement = createRoot(document)
      this._responseViews = []
      this.configView = null
      this.responsePostForm = null
      thread.addEventListener('responsesAdded', this._responsesAdded.bind(this))
    }
    ThreadView.prototype.getReloadButton = function() {
      return this.rootElement.querySelector('.reloadButton')
    }
    ThreadView.prototype.getReloadMessageElement = function() {
      return this.rootElement.querySelector('.reloadMessage')
    }
    ThreadView.prototype._getTopBar = function() {
      return this.rootElement.querySelector('.topBar')
    }
    ThreadView.prototype._getConfigToggle = function() {
      return this._getTopBar().querySelector('.configToggle')
    }
    ThreadView.prototype.replace = function(threadRootElement) {
      var p = threadRootElement.parentNode
      p.replaceChild(this.rootElement, threadRootElement)
    }
    ThreadView.prototype.disableReload = function() {
      this.rootElement.querySelector('.bottomBar').remove()
    }
    ThreadView.prototype._createResponseViews = function(responses) {
      return responses.map(function(r) {
        return new ResponseView(this.doc, r, true)
      }, this)
    }
    ThreadView.prototype._getMainElement = function() {
      return this.rootElement.querySelector('.main')
    }
    ThreadView.prototype._addResponseViewsToMainElement = function(views) {
      var main = this._getMainElement()
      views.map(prop('rootElement')).forEach(main.appendChild.bind(main))
    }
    ThreadView.prototype._getNewResponseBar = function() {
      return this.rootElement.querySelector('#new')
    }
    ThreadView.prototype._removeNewResponseBar = function() {
      var e = this._getNewResponseBar()
      if (e) e.remove()
    }
    ThreadView.prototype._addNewResponseBarIfRequired = function(newResNum) {
      if (newResNum === 0) return
      var main = this._getMainElement()
      if (!main.hasChildNodes()) return
      var newResBar = this.doc.createElement('p')
      newResBar.id = 'new'
      newResBar.textContent = `${newResNum} 件の新着レス`
      main.appendChild(newResBar)
    }
    ThreadView.prototype._scrollToNewResponseBar = function() {
      if (!this._getNewResponseBar()) return
      this.doc.location.hash = ''
      this.doc.location.hash = '#new'
    }
    ThreadView.prototype._responsesAdded = function(addedResponses) {
      this._removeNewResponseBar()
      this._addNewResponseBarIfRequired(addedResponses.length)
      var views = this._createResponseViews(addedResponses)
      ;[].push.apply(this._responseViews, views)
      this._addResponseViewsToMainElement(views)
      this._scrollToNewResponseBar()
    }
    ThreadView.prototype._toggleSubView = function(getView, toggle) {
      for (var i = 0; i < this._responseViews.length; i++) {
        var v = getView(this._responseViews[i])
        if (v) {
          toggle(v)
          break
        }
      }
    }
    ThreadView.prototype.toggleResponseChildren = function(numElem) {
      this._toggleSubView(invoke('getResponseViewByNumElem', [numElem])
                        , invoke('toggleChildren', []))
    }
    ThreadView.prototype.toggleSameIdResponses = function(idValElem) {
      this._toggleSubView(invoke('getResponseViewByIdValElem', [idValElem])
                        , invoke('toggleSameId', []))
    }
    ThreadView.prototype.toggleConfigView = function() {
      if (this.configView) {
        this.configView.destroy()
        this.configView = null
        this._getConfigToggle().textContent = 'NG設定▼'
      } else {
        this.configView = new ConfigView(this.doc, this._thread.config)
        this._getTopBar().appendChild(this.configView.rootElement)
        this._getConfigToggle().textContent = 'NG設定▲'
      }
    }
    ThreadView.prototype.close = function() {
      if (this.responsePostForm) this.responsePostForm.remove()
      this.responsePostForm = null
      this.disableReload()
    }
    return ThreadView
  })()

  var ResponsePostForm = (function(_super) {
    var ResponsePostForm = function(form) {
      _super.call(this)
      this._form = this._initForm(form)
      this._progress = this._createProgress()
      this._target = null
    }
    ResponsePostForm.prototype = Object.create(_super.prototype)
    ResponsePostForm.prototype._initForm = function(form) {
      form.target = 'postTarget'
      form.addEventListener('submit', this._formSubmitted.bind(this))
      return form
    }
    ResponsePostForm.prototype._getDoc = function() {
      return this._form.ownerDocument
    }
    ResponsePostForm.prototype._createProgress = function() {
      var result = this._getDoc().createElement('p')
      result.textContent = '書き込み中...'
      return result
    }
    ResponsePostForm.prototype._insertProgress = function() {
      var f = this._form
      f.parentNode.insertBefore(this._progress, f.nextSibling)
    }
    ResponsePostForm.prototype._createTarget = function() {
      var result = this._getDoc().createElement('iframe')
      result.name = this._form.target
      result.className = 'postTarget loading'
      result.addEventListener('load', this._targetLoaded.bind(this))
      return result
    }
    ResponsePostForm.prototype._hideOrCreateTarget = function() {
      if (this._target) {
        this._target.classList.add('loading')
      } else {
        this._target = this._createTarget()
        var p = this._progress
        p.parentNode.insertBefore(this._target, p.nextSibling)
      }
    }
    ResponsePostForm.prototype._formSubmitted = function() {
      this._form.submit.disabled = true
      this._insertProgress()
      this._hideOrCreateTarget()
    }
    ResponsePostForm.prototype._getTargetLocation = function() {
      return this._target.contentDocument.location.toString()
    }
    ResponsePostForm.prototype._isPostDone = function() {
      return this._target.contentDocument.title.indexOf('書きこみました') >= 0
    }
    ResponsePostForm.prototype._targetLoaded = function() {
      if (this._getTargetLocation() === 'about:blank') return
      this._form.submit.disabled = false
      this._progress.remove()
      if (this._isPostDone()) {
        this._target.remove()
        this._target = null
        this._form.MESSAGE.value = ''
        this.fireEvent('postDone')
      } else {
        this._target.classList.remove('loading')
      }
    }
    ResponsePostForm.prototype.remove = function() {
      ;[this._form, this._progress, this._target]
        .filter(Boolean)
        .forEach(invoke('remove', []))
    }
    return ResponsePostForm
  })(Observable)

  var ThreadController = (function() {
    var ThreadController = function(thread, threadView) {
      this.thread = thread
      this.threadView = threadView
    }
    ThreadController.prototype.addCallback = function() {
      var r = this.threadView.rootElement
      r.addEventListener('click', this.callback.bind(this))
      r.addEventListener('keydown', this.keydownCallback.bind(this))
    }
    ThreadController.prototype.requestNewResponses = function() {
      this.threadView.getReloadButton().disabled = true
      this.threadView.getReloadMessageElement().textContent = ''
      var path = this.threadView.doc.location.pathname
      var _this = this
      new ResponseRequest()
        .send(path.slice(0, path.lastIndexOf('/') + 1)
            , this.thread.getLastResponseNumber() + 1)
        .then(function(result) {
          _this.thread.addResponses(result.responses)
          if (result.threadClosed) _this.threadView.close()
        })
        .catch(function(error) {
          _this.threadView.getReloadMessageElement().textContent = error
        })
        .then(function() {
          _this.threadView.getReloadButton().disabled = false
        })
    }
    ThreadController.prototype._addNgWord = function() {
      var view = this.threadView.configView
      var val = view.getNgWordInputValue()
      if (!val) return
      switch (view.getNgWordAddTarget()) {
      case 'all':
        this.thread.config.addNgWord(val)
        break
      case 'board':
        this.thread.addNgWordForBoard(val)
        break
      case 'thread':
        this.thread.addNgWordForThread(val)
        break
      default:
        throw new Error(view.getNgWordAddTarget())
      }
      view.clearNgWordInputValue()
    }
    ThreadController.prototype._removeNgWord = function(target) {
      var s = target.dataset
      this.thread.config.removeNgWord(s.ngWord, s.boardId, s.threadNumber)
    }
    ThreadController.prototype._addNgId = function(target) {
      this.thread.addNgId(target.dataset.jstTime, target.dataset.id)
    }
    ThreadController.prototype._removeNgId = function(target) {
      var s = target.dataset
      this.thread.config.removeNgId(new NgId(s.boardId, s.activeDate, s.id))
    }
    ThreadController.prototype._actionMap = function() {
      var view = this.threadView
      var cfg = this.thread.config
      var header = '.threadView .main article header'
      var topBar = '.threadView .topBar'
      var config = `${topBar} .config`
      var ngIdSection = `${config} .ngIdSection`
      var ngWordSection = `${config} .ngWordSection`
      return {
        [`${header} .number`]: view.toggleResponseChildren.bind(view),
        [`${header} .value`]: view.toggleSameIdResponses.bind(view),
        [`${header} .id .ngButton`]: this._addNgId.bind(this),
        '.threadView .bottomBar .reloadButton': this.requestNewResponses.bind(this),
        [`${topBar} .configToggle`]: view.toggleConfigView.bind(view),
        [`${ngIdSection} table .removeButton`]: this._removeNgId.bind(this),
        [`${ngIdSection} h2 .removeAllButton`]: cfg.removeAllNgIds.bind(cfg),
        [`${ngWordSection} .add .addButton`]: this._addNgWord.bind(this),
        [`${ngWordSection} table .removeButton`]: this._removeNgWord.bind(this),
        [`${ngWordSection} h2 .removeAllButton`]: cfg.removeAllNgWords.bind(cfg),
      }
    }
    ThreadController.prototype._getAction = function(target) {
      var map = this._actionMap()
      var selectors = Object.keys(map)
      for (var i = 0; i < selectors.length; i++) {
        var s = selectors[i]
        if (target.matches(s)) return map[s]
      }
    }
    ThreadController.prototype.callback = function(event) {
      var action = this._getAction(event.target)
      if (action) action(event.target)
    }
    ThreadController.prototype.keydownCallback = function(event) {
      var enterKeyCode = 13
      if (event.keyCode !== enterKeyCode) return
      var s = '.threadView .topBar .config .ngWordSection .add .ngWordInput'
      if (event.target.matches(s)) this._addNgWord()
    }
    return ThreadController
  })()

  var addStyle = function() {
    var style = document.createElement('style')
    style.textContent = `
html {
  max-width: 900px;
  margin: 0 auto;
}
.threadView {
  line-height: 1.5em;
}
.threadView .main article header .name {
  color: green;
  font-weight: bold;
}
.threadView .main article header .name,
.threadView .main article header time,
.threadView .main article header .id {
  margin-left: 0.5em;
}
.threadView .main article header .number.hasChild,
.threadView .main article header .id .value.sameIdExist,
.threadView .main article header .id .ngButton,
.threadView .topBar .configToggle,
.threadView .topBar .config h2 .removeAllButton,
.threadView .topBar .config table .removeButton {
  cursor: pointer;
  text-decoration: underline;
}
.threadView .main article header .number.hasChild3,
.threadView .main article header .id .value.sameIdExist5 {
  font-weight: bold;
  color: red;
}
.threadView .main article .content {
  margin: 0 0 1em 1em;
}
.threadView .main article .sameId,
.threadView .main article .children {
  border-top: solid black thin;
  border-left: solid black thin;
  padding: 5px 0 0 5px;
}
.threadView .main article .sameId > article > header .id .value {
  color: black;
  background-color: yellow;
}
.threadView .main article.ng,
.threadView .main article header .name,
.threadView .main article header .mail,
.threadView .main article header time,
.threadView .main article header .id,
.threadView .topBar .config h2 .removeAllButton {
  font-size: smaller;
}
.threadView .topBar .config {
  border: solid black thin;
  padding: 0 0.5em;
}
.threadView .topBar .config h2 {
  font-size: medium;
}
.threadView .topBar .config table {
  border-collapse: collapse;
}
.threadView .topBar .config .ngWordSection .wrap,
.threadView .topBar .config .ngIdSection .wrap {
  max-height: 10em;
  overflow: auto;
}
.threadView .topBar .config table th,
.threadView .topBar .config table td {
  border: solid thin black;
  line-height: 1.5em;
  padding: 0 0.5em;
}
.postTarget {
  width: 100%;
}
.postTarget.loading {
  display: none;
}
#new {
  background-color: lightblue;
  padding-left: 0.5em;
}
`
    document.head.appendChild(style)
  }
  var main = function() {
    addStyle()
    var parsed = new Parser().parse(document)
    parsed.ads.forEach(function(e) { e.style.display = 'none' })
    parsed.elementsToRemove.forEach(invoke('remove', []))
    if (parsed.floatedSpan) parsed.floatedSpan.style.cssFloat = ''
    var config = new Config(GM_getValue, GM_setValue)
    var thread = new Thread(config, parsed.boardId, parsed.threadNumber)
    var threadView = new ThreadView(document, thread)
    if (parsed.threadClosed) threadView.disableReload()
    thread.addResponses(parsed.responses)
    threadView.replace(parsed.threadRootElement)
    var ctrl = new ThreadController(thread, threadView)
    ctrl.addCallback()
    if (parsed.postForm) {
      var postForm = new ResponsePostForm(parsed.postForm)
      postForm.addEventListener('postDone'
                              , ctrl.requestNewResponses.bind(ctrl))
      threadView.responsePostForm = postForm
    }
  }

  main()
})()