// ==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()
})()