tree view for qwerty

あやしいわーるど@みさおの投稿をツリーで表示できます。スタック表示の方にもいくつか機能を追加できます。

当前为 2017-10-08 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        tree view for qwerty
// @name:ja     くわツリービュー
// @namespace   strangeworld
// @description あやしいわーるど@みさおの投稿をツリーで表示できます。スタック表示の方にもいくつか機能を追加できます。
// @match       http://misao.on.arena.ne.jp/cgi-bin/bbs.cgi*
// @match       http://misao.mixh.jp/cgi-bin/bbs.cgi*
// @grant       GM_setValue
// @grant       GM.setValue
// @grant       GM_getValue
// @grant       GM.getValue
// @grant       GM_deleteValue
// @grant       GM.deleteValue
// @grant       GM_listValues
// @grant       GM.listValues
// @grant       GM_xmlhttpRequest
// @grant       GM.xmlHttpRequest
// @grant       GM_openInTab
// @grant       GM.openInTab
// @grant       window.close
// @version     10.12
// @run-at      document-start
// @connect     misao.on.arena.ne.jp
// @connect     misao.mixh.jp
// ==/UserScript==
// zousan - A Lightning Fast, Yet Very Small Promise A+ Compliant Implementation
// https://github.com/bluejava/zousan
// Author: Glenn Crownover <[email protected]> (http://www.bluejava.com)
// Version 2.3.3
// License: MIT

/* jshint asi: true, browser: true */
/* global setImmediate, console */

(function(global){

		"use strict";

		var
			STATE_PENDING,					// These are the three possible states (PENDING remains undefined - as intended)
			STATE_FULFILLED = "fulfilled",		// a promise can be in.  The state is stored
			STATE_REJECTED = "rejected",		// in this.state as read-only

			_undefined,						// let the obfiscator compress these down
			_undefinedString = "undefined";		// by assigning them to variables (debatable "optimization")

		// See http://www.bluejava.com/4NS/Speed-up-your-Websites-with-a-Faster-setTimeout-using-soon
		// This is a very fast "asynchronous" flow control - i.e. it yields the thread and executes later,
		// but not much later. It is far faster and lighter than using setTimeout(fn,0) for yielding threads.
		// Its also faster than other setImmediate shims, as it uses Mutation Observer and "mainlines" successive
		// calls internally.
		// WARNING: This does not yield to the browser UI loop, so by using this repeatedly
		// 		you can starve the UI and be unresponsive to the user.
		// This is an even FASTER version of https://gist.github.com/bluejava/9b9542d1da2a164d0456 that gives up
		// passing context and arguments, in exchange for a 25x speed increase. (Use anon function to pass context/args)
		var soon = (function() {

				var	fq = [], // function queue;
					fqStart = 0, // avoid using shift() by maintaining a start pointer - and remove items in chunks of 1024 (bufferSize)
					bufferSize = 1024

				function callQueue()
				{
					while(fq.length - fqStart) // this approach allows new yields to pile on during the execution of these
					{
						try { fq[fqStart]() } // no context or args..
						catch(err) { if(global.console) global.console.error(err) }
						fq[fqStart++] = _undefined	// increase start pointer and dereference function just called
						if(fqStart == bufferSize)
						{
							fq.splice(0,bufferSize);
							fqStart = 0;
						}
					}
				}

				// run the callQueue function asyncrhonously, as fast as possible
				var cqYield = (function() {

						// This is the fastest way browsers have to yield processing
						if(typeof MutationObserver !== _undefinedString)
						{
							// first, create a div not attached to DOM to "observe"
							var dd = document.createElement("div");
							var mo = new MutationObserver(callQueue);
							mo.observe(dd, { attributes: true });

							return function() { dd.setAttribute("a",0); } // trigger callback to
						}

						// if No MutationObserver - this is the next best thing - handles Node and MSIE
						if(typeof setImmediate !== _undefinedString)
							return function() { setImmediate(callQueue) }

						// final fallback - shouldn't be used for much except very old browsers
						return function() { setTimeout(callQueue,0) }
					})();

				// this is the function that will be assigned to soon
				// it takes the function to call and examines all arguments
				return function(fn) {

						// push the function and any remaining arguments along with context
						fq.push(fn);

						if((fq.length - fqStart) == 1) // upon adding our first entry, kick off the callback
							cqYield();
					};

			})();

		// -------- BEGIN our main "class" definition here -------------

		function Zousan(func)
		{
			//  this.state = STATE_PENDING;	// Inital state (PENDING is undefined, so no need to actually have this assignment)
			//this.c = [];			// clients added while pending.   <Since 1.0.2 this is lazy instantiation>

			// If a function was specified, call it back with the resolve/reject functions bound to this context
			if(func)
			{
				var me = this;
				try
				{
				func(
					function(arg) { me.resolve(arg) },	// the resolve function bound to this context.
					function(arg) { me.reject(arg) })	// the reject function bound to this context
				} catch(err) { me.reject(err) }
			}
		}

		Zousan.prototype = {	// Add 6 functions to our prototype: "resolve", "reject", "then", "catch", "finally" and "timeout"

				resolve: function(value)
				{
					if(this.state !== STATE_PENDING)
						return;

					if(value === this)
						return this.reject(new TypeError("Attempt to resolve promise with self"));

					var me = this; // preserve this

					if(value && (typeof value === "function" || typeof value === "object"))
					{
						try
						{
							var first = true; // first time through?
							var then = value.then;
							if(typeof then === "function")
							{
								// and call the value.then (which is now in "then") with value as the context and the resolve/reject functions per thenable spec
								then.call(value,
									function(ra) { if(first) { first=false; me.resolve(ra);}  },
									function(rr) { if(first) { first=false; me.reject(rr); } });
								return;
							}
						}
						catch(e)
						{
							if(first)
								this.reject(e);
							return;
						}
					}

					this.state = STATE_FULFILLED;
					this.v = value;

					if(me.c)
						soon(function() {
								for(var n=0, l=me.c.length;n<l;n++)
									resolveClient(me.c[n],value);
							});
				},

				reject: function(reason)
				{
					if(this.state !== STATE_PENDING)
						return;

					this.state = STATE_REJECTED;
					this.v = reason;

					var clients = this.c;
					if(clients)
						soon(function() {
								for(var n=0, l=clients.length;n<l;n++)
									rejectClient(clients[n],reason);
							});
					else
						if(!Zousan.suppressUncaughtRejectionError && global.console)
							global.console.log("You upset Zousan. Please catch rejections: ", reason,reason ? reason.stack : null)
				},

				then: function(onF,onR)
				{
					var p = new Zousan();
					var client = {y:onF,n:onR,p:p};

					if(this.state === STATE_PENDING)
					{
						 // we are pending, so client must wait - so push client to end of this.c array (create if necessary for efficiency)
						if(this.c)
							this.c.push(client);
						else
							this.c = [client];
					}
					else // if state was NOT pending, then we can just immediately (soon) call the resolve/reject handler
					{
						var s = this.state, a = this.v;
						soon(function() { // we are not pending, so yield script and resolve/reject as needed
								if(s === STATE_FULFILLED)
									resolveClient(client,a);
								else
									rejectClient(client,a);
							});
					}

					return p;
				},

				"catch": function(cfn) { return this.then(null,cfn); }, // convenience method
				"finally": function(cfn) { return this.then(cfn,cfn); }, // convenience method

				// new for 1.2  - this returns a new promise that times out if original promise does not resolve/reject before the time specified.
				// Note: this has no effect on the original promise - which may still resolve/reject at a later time.
				"timeout" : function(ms,timeoutMsg)
				{
					timeoutMsg = timeoutMsg || "Timeout"
					var me = this;
					return new Zousan(function(resolve,reject) {

							setTimeout(function() {
									reject(Error(timeoutMsg));	// This will fail silently if promise already resolved or rejected
								}, ms);

							me.then(function(v) { resolve(v) },		// This will fail silently if promise already timed out
									function(er) { reject(er) });		// This will fail silently if promise already timed out

						})
				}

			}; // END of prototype function list

		function resolveClient(c,arg)
		{
			if(typeof c.y === "function")
			{
				try {
						var yret = c.y.call(_undefined,arg);
						c.p.resolve(yret);
					}
				catch(err) { c.p.reject(err) }
			}
			else
				c.p.resolve(arg); // pass this along...
		}

		function rejectClient(c,reason)
		{
			if(typeof c.n === "function")
			{
				try
				{
					var yret = c.n.call(_undefined,reason);
					c.p.resolve(yret);
				}
				catch(err) { c.p.reject(err) }
			}
			else
				c.p.reject(reason); // pass this along...
		}

		// "Class" functions follow (utility functions that live on the Zousan function object itself)

		Zousan.resolve = function(val) { var z = new Zousan(); z.resolve(val); return z; }

		Zousan.reject = function(err) { var z = new Zousan(); z.reject(err); return z; }

		Zousan.all = function(pa)
		{
			var results = [ ], rc = 0, retP = new Zousan(); // results and resolved count

			function rp(p,i)
			{
				if(!p || typeof p.then !== "function")
					p = Zousan.resolve(p);
				p.then(
						function(yv) { results[i] = yv; rc++; if(rc == pa.length) retP.resolve(results); },
						function(nv) { retP.reject(nv); }
					);
			}

			for(var x=0;x<pa.length;x++)
				rp(pa[x],x);

			// For zero length arrays, resolve immediately
			if(!pa.length)
				retP.resolve(results);

			return retP;
		}

		// If this appears to be a commonJS environment, assign Zousan as the module export
		if(typeof module != _undefinedString && module.exports)		// jshint ignore:line
			module.exports = Zousan;	// jshint ignore:line

		// If this appears to be an AMD environment, define Zousan as the module export
		if(global.define && global.define.amd)
			global.define([], function() { return Zousan });

		// Make Zousan a global variable in all environments
		global.Zousan = Zousan;

		// make soon accessable from Zousan
		Zousan.soon = soon;

	})(typeof global != "undefined" ? global : this);	// jshint ignore:line
(function () {
'use strict';

if (!window.Promise && typeof it == "undefined") {
	window.Promise = window.Zousan;

	if (!Promise.race) {
		Promise.race = function(promises) {
			return new Promise(function(resolve, reject) {
				promises.forEach(function(promise) {
					promise = promise.then ? promise : Promise.resolve(promise);
					promise.then(resolve).catch(reject);
				});
			});
		};
	}
}

if (!Object.assign) {
	Object.assign = function assign(target, _source) {
		var arguments$1 = arguments;

		for (var index = 1, key, src; index < arguments.length; ++index) {
			src = arguments$1[index];

			for (key in src) {
				if (Object.prototype.hasOwnProperty.call(src, key)) {
					target[key] = src[key];
				}
			}
		}

		return target;
	};
}

if (!Object.values) {
	Object.values = function values(object) {
		var values = [];

		for (var key in object) {
			if (Object.prototype.hasOwnProperty.call(object, key)) {
				values.push(object[key]);
			}
		}

		return values;
	};
}

if (!String.prototype.startsWith) {
	String.prototype.startsWith = function(start) {
		return this.lastIndexOf(start, 0) === 0;
	};
}
if (!String.prototype.endsWith) {
	Object.defineProperty(String.prototype, "endsWith", {
		value: function(searchString, position) {
			var subjectString = this.toString();
			if (position === undefined || position > subjectString.length) {
				position = subjectString.length;
			}
			position -= searchString.length;
			var lastIndex = subjectString.indexOf(searchString, position);
			return lastIndex !== -1 && lastIndex === position;
		},
	});
}
if (!String.prototype.includes) {
	String.prototype.includes = function() {
		return String.prototype.indexOf.apply(this, arguments) !== -1;
	};
}
if (!String.prototype.trimRight) {
	String.prototype.trimRight = function() {
		return this.replace(/\s+$/, "");
	};
}

// element-closest | CC0-1.0 | github.com/jonathantneal/closest

if (typeof Element.prototype.matches !== "function") {
	Element.prototype.matches =
		Element.prototype.msMatchesSelector ||
		Element.prototype.mozMatchesSelector ||
		Element.prototype.webkitMatchesSelector ||
		Element.prototype.oMatchesSelector ||
		function matches(selector) {
			var element = this;
			var elements = (element.document || element.ownerDocument)
				.querySelectorAll(selector);
			var index = 0;

			while (elements[index] && elements[index] !== element) {
				++index;
			}

			return Boolean(elements[index]);
		};
}

if (typeof Element.prototype.closest !== "function") {
	Element.prototype.closest = function closest(selector) {
		var element = this;

		while (element && element.nodeType === 1) {
			if (element.matches(selector)) {
				return element;
			}

			element = element.parentNode;
		}

		return null;
	};
}

if (typeof requestAnimationFrame !== "function") {
	window.requestAnimationFrame = function(callback) {
		setTimeout(callback, 16);
	};
}

var Stash = function Stash() {
	var area = (this.area = document.createElement("div"));
	area.id = "qtv-stash-area";
	area.hidden = true;
};
Stash.prototype.stash = function stash (buffer) {
	this.area.appendChild(buffer);
};
Stash.prototype.restore = function restore () {
	this.area.parentNode.removeChild(this.area);

	var range = document.createRange();
	range.selectNodeContents(this.area);
	return range.extractContents();
};
Stash.prototype.appendTo = function appendTo (node) {
	node.appendChild(this.area);
};

function Buffer(range) {
	var this$1 = this;
	if ( range === void 0 ) range = document.createRange();

	var buffer = (this.buffer = document.createDocumentFragment());

	this.marker = document.createComment("qtv-main-started");
	this.view = null;

	this.onProgress = function (lastChild) {
		if (lastChild !== this$1.marker) {
			range.setEndAfter(lastChild);
			buffer.appendChild(range.extractContents());
		}

		this$1.render();
	};

	/**
	 * @param {HTMLHRElement} hr
	 */
	this.onHr = function (hr) {
		hr.parentNode.insertBefore(this$1.marker, hr.nextSibling);

		range.setStartAfter(this$1.marker);
	};

	this.onLoaded = function () {
		this$1.wasLoaded = true;

		if (this$1.view) {
			this$1.render();
			this$1.finish();
		} else {
			this$1.flush();
		}
	};
}
Buffer.prototype.setView = function(view) {
	this.view = view;

	if (this.wasLoaded) {
		this.rewind();
	}
};
Buffer.prototype.rewind = function() {
	this.buffer = this.stash.restore();

	this.render();
	this.finish();
};
Buffer.prototype.render = function() {
	if (this.view && "render" in this.view) {
		this.view.render(this.buffer);
	}
};
Buffer.prototype.finish = function() {
	this.view.finish(this.buffer);
};
Buffer.prototype.flush = function() {
	if (!this.marker.parentNode) {
		return;
	}

	this.stash = new Stash();
	this.stash.stash(this.buffer);
	this.stash.appendTo(this.marker.parentNode);
};
Buffer.prototype.insertBefore = function(node) {
	this.marker.parentNode.insertBefore(node, this.marker);
};

function getBody() {
	return document.body;
}

var delayPromise = function (ms) { return new Promise(function (resolve) { return setTimeout(resolve, ms); }); };

var createDelayNotice = function (config, timeout) {
	if ( timeout === void 0 ) timeout = 700;

	var message = "設定読込待ち";
	var configIsLoaded = false;
	var notice = null;

	config.then(function () { return (configIsLoaded = true); });

	var popup = function() {
		if (configIsLoaded) {
			return;
		}

		var notice = document.createElement("aside");
		notice.id = "qtv-status";
		notice.style.cssText =
			"position:fixed;top:0px;left:0px;background-color:black;color:white;z-index:1";
		notice.textContent = message;

		var body = getBody();

		body.insertBefore(notice, body.firstChild);

		var removeNotice = function () { return body.removeChild(notice); };

		config.then(removeNotice, removeNotice);
	};

	return {
		onHr: function () { return delayPromise(timeout).then(popup); },
		onLoaded: function () {
			message = "設定読込待ちかレンダリング中";
			if (notice) {
				notice.textContent = message;
			}
		},
	};
};

function doNothing() {}

var find = Array.prototype.find;
var isHR = function (node) { return node.nodeName === "HR"; };

var findHr = function (mutations) {
	for (var i = 0; i < mutations.length; i++) {
		var mutation = mutations[i];
		if (mutation.target.nodeName === "BODY") {
			var element = find.call(mutation.addedNodes, isHR);
			if (element) {
				return element;
			}
		}
	}
};

function ready(ref) {
	if ( ref === void 0 ) ref = {};
	var doc = ref.doc; if ( doc === void 0 ) doc = document;
	var capture = ref.capture; if ( capture === void 0 ) capture = false;

	return new Promise(function(resolve) {
		var readyState = doc.readyState;
		if (
			readyState === "complete" ||
			(readyState !== "loading" && !doc.documentElement.doScroll)
		) {
			resolve();
		} else {
			doc.addEventListener("DOMContentLoaded", resolve, {
				capture: capture,
				once: true,
			});
		}
	});
}

var e$1;
function handleError(error) {
	if (e$1) {
		return;
	}

	e$1 = error;
	ready()
		.then(getBody)
		.then(doHandle);
}

function doHandle(body) {
	var lineNumber = e$1.lineNumber || 0;

	var pre = document.createElement("pre");
	pre.innerHTML =
		'くわツリービューの処理を中断しました。表示されていない投稿があります。<a href="javascript:;">スタックトレースを表示する</a>';

	var dStackTrace = document.createElement("p");
	dStackTrace.style.display = "none";

	var stackTrace = "";
	if (typeof GM_info !== "undefined") {
		stackTrace += GM_info.version + "+" + GM_info.script.version + "\n";
	} else if (typeof GM !== "undefined") {
		if (GM.info) {
			stackTrace += GM.info.version + "+" + GM.info.script.version + "\n";
		} else {
			stackTrace += "GM4\n";
		}
	}
	var stack = e$1.stackTrace || e$1.stack || "";
	stackTrace += e$1.name + ": " + e$1.stackTrace + ":" + lineNumber + "\n" + stack;

	dStackTrace.textContent = stackTrace;

	pre.appendChild(dStackTrace);
	pre.addEventListener("click", showStackTrace);

	body.insertBefore(pre, body.firstChild);
}
function showStackTrace(e) {
	e.target.parentNode.querySelector("p").style.display = null;
}

function Observer(loaded, doc) {
	var this$1 = this;
	if ( doc === void 0 ) doc = document;

	this.listeners = [];
	this.doc = doc;

	this.hr = null;

	var fireEvent = function (event, arg) {
		try {
			for (var i = 0; i < this$1.listeners.length; i++) {
				var handler = this$1.listeners[i][event];
				if (handler) {
					handler(arg);
				}
			}
		} catch (e) {
			handleError(e);
			this$1.observer.disconnect();
			this$1.observer.observe = doNothing;

			throw e;
		}
	};

	this.processRecords = function (mutations, observer) {
		observer.disconnect();

		if (!this$1.hr) {
			this$1.hr = findHr(mutations);

			if (this$1.hr) {
				fireEvent("onHr", this$1.hr);
			}
		}

		if (this$1.hr) {
			fireEvent("onProgress", doc.body.lastChild);
		}

		this$1.observe();
	};

	this.observer = this.makeMutationObserver(this.processRecords);

	if (doc.body) {
		this.first = function () {
			this$1.hr = doc.body.querySelector("body > hr");

			if (this$1.hr) {
				fireEvent("onHr", this$1.hr);
				fireEvent("onProgress", doc.body.lastChild);
			}

			this$1.first = null;
		};
	}
	loaded.then(function () {
		this$1.observer.observe = doNothing;
		var records = this$1.observer.takeRecords();
		if (records.length) {
			console.error(records.length);
			this$1.processRecords(records, this$1.observer);
		}
		this$1.observer.disconnect();
		fireEvent("onLoaded");
	});
}
Observer.prototype.observe = function() {
	if (this.doc.body) {
		if (this.first) {
			this.first();
		}

		this.observer.observe(this.doc.body, {childList: true});
	} else {
		this.observer.observe(this.doc.documentElement, {
			childList: true,
			subtree: true,
		});
	}
};
Observer.prototype.addListener = function(listener) {
	this.listeners.push(listener);
};

Observer.prototype.makeMutationObserver = function(callback) {
	return new MutationObserver(callback);
};

var waitForDomContentLoaded = function () { return ready({capture: true}); };

function ajax(options) {
	options = options || {};
	var type = options.type || "GET";
	var url = options.url || location.href;
	var data = options.data || {};

	url = url.replace(/#.*$/, "");

	for (var key in data) {
		url += "&" + encodeURIComponent(key) + "=" + encodeURIComponent(data[key]);
	}

	url = url.replace(/[&?]{1,2}/, "?");

	return new Promise(function(resolve, reject) {
		var xhr = new XMLHttpRequest();

		xhr.open(type, url);
		xhr.overrideMimeType("text/html; charset=windows-31j");
		xhr.onload = function() {
			if (xhr.status === 200) {
				resolve(xhr.response);
			} else {
				reject(new Error(xhr.statusText));
			}
		};
		xhr.onerror = function() {
			reject(new Error("Network Error"));
		};
		xhr.send();
	});
}

var fetch = function (options) { return ajax(options).then(wrapWithDiv).catch(wrapErrorWithDiv); };

function wrapWithDiv(html) {
	var div = document.createElement("div");
	div.innerHTML = html;
	return div;
}

function wrapErrorWithDiv(error) {
	var div = document.createElement("div");
	div.textContent = error;
	return div;
}

var AfterFetch = {
	hasOP: function() {
		return true;
	},
	run: function(_contaienr) {
		var after = this.concurrent();

		return after.then(function(afters) {
			return {afters: afters, befores: []};
		});
	},
};

var BothFetch = {
	hasOP: function(container) {
		return this.q.hasOP(container);
	},
	run: function(container) {
		var after = this.concurrent();
		var before = this.sequence(container);

		return Promise.all([after, before]).then(function(args) {
			return {afters: args[0], befores: args[1]};
		});
	},
};

/**
 * @param {Query}
 * @param {number}
 */
function Fetch(q, now) {
	this.q = q;
	this.now = now || Date.now();

	var extend;
	if (q.get("sv")) {
		extend = AfterFetch;
	} else {
		extend = BothFetch;
	}

	Object.assign(this, extend);
}
Fetch.prototype.getAfterDates = function() {
	return this.getThese7Dates().filter(function isAfterTodate(date) {
		return date > this.q.getDayAsNumber();
	}, this);
};
Fetch.prototype.getBeforeDates = function() {
	return this.getThese7Dates().filter(function isBeforeTodate(date) {
		return date < this.q.getDayAsNumber();
	}, this);
};
Fetch.prototype.getThese7Dates = function() {
	var this$1 = this;

	var dates = [];
	var ONE_DAY = 24 * 60 * 60 * 1000;
	var fill = function(n) {
		return n < 10 ? "0" + n : n;
	};

	for (var i = 0; i < 7; i++) {
		var back = new Date(this$1.now - ONE_DAY * i);
		var year = back.getFullYear();
		var month = fill(back.getMonth() + 1);
		var date = fill(back.getDate());

		dates.push("" + year + month + date);
	}

	return dates;
};
Fetch.prototype.fetch = function(date) {
	var ff = date + ".dat";

	return fetch({url: "bbs.cgi", data: this.q.queryFor(ff)}).then(function (div) {
		div.ff = ff;
		return div;
	});
};
Fetch.prototype.sequence = function(container) {
	var divs = [];
	var fetch$$1 = this.fetch.bind(this);
	var hasOP = this.hasOP.bind(this);
	var sequence = this.getBeforeDates().reduce(function(sequence, date) {
		return sequence.then(function(done) {
			if (done) {
				return done;
			}

			return fetch$$1(date).then(function(div) {
				divs.push(div);

				return hasOP(div);
			});
		});
	}, Promise.resolve(hasOP(container)));

	return sequence.then(function() {
		return divs;
	});
};
Fetch.prototype.concurrent = function() {
	return Promise.all(this.getAfterDates().map(this.fetch.bind(this)));
};

function Info() {
	var el = document.createElement("span");
	el.id = "info";

	return el;
}

function StackLog(config, q, body, view) {
	this.config = config;
	this.q = q;
	this.body = body;
	this.view = view;
}
StackLog.prototype = {
	container: function() {
		if (!document.body) {
			throw new Error("no body");
		}

		var el = document.createElement("div");
		el.id = "container";
		var info = new Info();
		el.appendChild(info);

		return {container: el, info: info};
	},
	shouldComplement: function() {
		return this.q.shouldComplement(this.body);
	},
	complement: function() {
		if (this.shouldComplement()) {
			var gui = this.container();
			var container = gui.container;
			var info = gui.info;

			info.innerHTML = "<strong>" + this.q.ff + "以外の過去ログを検索中...</strong>";
			this.body.insertBefore(container, this.body.firstChild);

			return this.makeFetch()
				.run(this.body)
				.then(this.addExtraLog.bind(this, container))
				.then(function() {
					info.textContent = "";
				});
		}
	},
	makeFetch: function() {
		return new Fetch(this.q);
	},
	addExtraLog: function(container, doms) {
		var view = this.view;
		var wrap = (function() {
			var wrap = view.wrapOne.bind(view);
			return function(f) {
				Array.prototype.forEach.call(f.querySelectorAll("a[name]"), wrap);
				return f;
			};
		})();
		var f = document.createDocumentFragment();
		function append(html) {
			f.insertAdjacentHTML("beforeend", html);
		}
		function format(f, div) {
			var numberOfPosts = div.querySelectorAll("a[name]").length;

			append("<h1>" + div.ff + "</h1>");

			if (numberOfPosts) {
				f.appendChild(wrap(div));
				append("<h3>" + numberOfPosts + "件見つかりました。</h3>");
			} else {
				append("<hr>");
				append("<h3>指定されたスレッドは見つかりませんでした。</h3><hr>");
			}

			return f;
		}

		if (doms.befores.length) {
			append("<hr>");
		}

		f = doms.befores.reduceRight(format, f);

		append("<hr>");
		append("<h1>" + this.q.ff + "</h1>");

		this.body.insertBefore(f, container.nextSibling);

		f = doms.afters.reduceRight(format, f);
		this.body.appendChild(f);
	},
};

var next = function (type) { return function (nodeName) { return function (node) {
	while ((node = node[type])) {
		if (node.nodeName === nodeName) {
			return node;
		}
	}
}; }; };

var nextElement = next("nextElementSibling");

function Post(id) {
	this.id = id;

	this.parent = null; // {Post}
	this.child = null; // {Post}
	this.next = null; // {Post}
	this.isNG = null;
}
Post.collectEssestialParts = function() {
	var nextFont = nextElement("FONT");
	var nextB = nextElement("B");
	var nextBlockquote = nextElement("BLOCKQUOTE");

	return function collectElements(a) {
		var header = nextFont(a);
		var name = nextB(header);
		var info = nextFont(name);
		var blockquote = nextBlockquote(info);
		var pre = blockquote.firstElementChild;
		var title = header.firstChild;
		var resButton = info.firstElementChild;
		var threadButton = info.lastElementChild;
		var threadUrl = threadButton.href;

		return {
			el: {
				anchor: a,
				blockquote: blockquote,
				pre: pre,
				title: title,
				name: name,
				info: info,
				resButton: resButton,
				posterButton: resButton.nextElementSibling,
				threadButton: threadButton,
			},
			name: name.innerHTML,
			title: title.innerHTML,
			text: pre.innerHTML,
			threadUrl: threadUrl,
			threadId: /&s=([^&]+)/.exec(threadUrl)[1],
		};
	};
};

Post.breakdownPre = function(html, id, hostname) {
	if ( hostname === void 0 ) hostname = location.hostname;

	var candidate, parentId, parentDate;
	var text = html
		.replace(/<\/?font[^>]*>/gi, "")
		.replace(/\r\n?/g, "\n")
		.slice(0, -1);

	if (text.includes("&lt;A")) {
		text = text.replace(
			//        "       </A>
			//firefox %22    %3C\/A%3E
			//chrome  &quot; &lt;\/A&gt;
			//opera   &quot; <\/A>
			/&lt;A href="<a href="(.*)(?:%22|&quot;)"( target="link"(?: rel="noreferrer noopener")?)>\1"<\/a>\2&gt;<a href="\1(?:%3C\/A%3E|&lt;\/A&gt;|<\/A>)"\2>\1&lt;\/A&gt;<\/a>/g,
			'<a href="$1"$2>$1</a>'
		);
	}

	candidate = text;

	var reference = /\n\n<a href="h[^"]+&amp;s=((?!0)\d+)&amp;r=[^"]+">参考:([^<]+)<\/a>$/.exec(
		text
	);
	if (!reference) {
		reference = /\n\n<a href="#((?!0)\d+)">参考:([^<]+)<\/a>$/.exec(text);
	}

	if (reference) {
		var assign;
		(assign = reference, parentId = assign[1], parentDate = assign[2]);
		if (+id <= parentId) {
			parentId = null;
		}
		text = text.slice(0, reference.index);
	}

	var url = /\n\n<[^<]+<\/a>$/.exec(text);
	if (url) {
		text = text.slice(0, url.index);
	}
	if (!text.includes("<") && text.includes(":")) {
		candidate =
			Post.relinkify(
				text,
				/misao/.test(hostname) ? ' rel="noreferrer noopener"' : ""
			) +
			(url ? url[0] : "") +
			(reference ? reference[0] : "");
	}

	return {
		text: candidate,
		parentId: parentId,
		parentDate: parentDate,
	};
};
Post.makePosts = function(context) {
	var posts = [];
	var as = context.querySelectorAll("a[name]");
	var font = nextElement("FONT");
	var b = nextElement("B");
	var blockquote = nextElement("BLOCKQUOTE");

	for (var i = 0, len = as.length; i < len; i++) {
		var a = as[i];
		var post = new Post(a.name);
		posts.push(post);

		var header = font(a);

		post.title = header.firstChild.innerHTML;
		var named = b(header);
		post.name = named.innerHTML;

		var info = font(named);
		post.date = info.firstChild.nodeValue.trim().slice(4); //「投稿日:」削除
		post.resUrl = info.firstElementChild.href;
		post.threadUrl = info.lastElementChild.href;
		post.threadId = /&s=([^&]+)/.exec(post.threadUrl)[1];
		if (info.childElementCount === 3) {
			post.posterUrl = info.firstElementChild.nextElementSibling.href;
		} else {
			post.posterUrl = null;
		}

		var body = blockquote(info);
		var pre = body.firstElementChild;
		var env = font(pre);

		if (env) {
			post.env = env.firstChild.innerHTML; // font > i > env
		}

		var ref = Post.breakdownPre(
			pre.innerHTML,
			post.id
		);
		var text = ref.text;
		var parentId = ref.parentId;
		var parentDate = ref.parentDate;

		post.text = text;
		if (parentId) {
			post.parentId = parentId;
			post.parentDate = parentDate;
		}
	}

	Post.sortByTime(posts);

	return posts;
};
// 新しいのが先
Post.sortByTime = function(posts) {
	if (posts.length >= 2 && +posts[0].id < +posts[1].id) {
		posts.reverse();
	}
};
Post.byID = function(l, r) {
	return +l.id - +r.id;
};
Post.relinkify1stMatching = function(_, rel, url) {
	if ( rel === void 0 ) rel = "";

	return Post.relinkify(url, rel);
};
Post.relinkify = function(url, rel) {
	return url.replace(
		/(?:https?|ftp|gopher|telnet|whois|news):\/\/[\x21-\x7e]+/gi,
		("<a href=\"$&\" target=\"link\"" + rel + ">$&</a>")
	);
};
Post.checkNG = function(ng, post) {
	var isNG = false;

	if (ng.word) {
		isNG = ng.word.test(post.text);
	}
	if (!isNG && ng.handle) {
		isNG = isNG || ng.handle.test(post.name);
		isNG = isNG || ng.handle.test(post.title);
	}

	post.isNG = isNG;

	return post;
};
Post.wantsParent = function(post) {
	return post.parentId;
};
Post.isOrphan = function(post) {
	return post.parent === null && post.parentId;
};
Post.isRootCandidate = function(post) {
	return post.parent === null;
};
Post.mayHaveParent = function(post) {
	return post.mayHaveParent();
};
Post.isClean = function(post) {
	return !post.rejectLevel;
};
Post.prototype = {
	id: "", // {string} /^\d+$/
	title: " ", // {string}
	name: " ", // {string}
	date: "", // {string}
	resUrl: "", // {string}
	threadUrl: "", // {string}
	threadId: "", // {string}
	posterUrl: "", // {string}
	// null: 親なし
	// undefined: 不明
	// string: ID 0から始まらない数字の文字列
	/** @type {(undefined|?string)} */
	parentId: null,
	parentDate: "", // {string}
	text: "", // {string}

	showAsIs: false, // {boolean}
	rejectLevel: 0, // {number}
	isRead: false, // {boolean}

	isOP: function() {
		return this.id === this.threadId;
	},
	getText: function() {
		if (this.hasSameDate()) {
			return this.text.slice(0, this.text.lastIndexOf("\n\n")); //参考と空行を除去
		}

		return this.text;
	},
	hasSameDate: function() {
		return this.parent && this.parent.date === this.parentDate;
	},
	computeQuotedText: function() {
		var lines = this.text
			.replace(/&gt; &gt;.*\n/g, "")
			//target属性がないのは参考リンクのみ
			.replace(/<a href="[^"]+">参考:.*<\/a>/i, "")
			.replace(
				/<a href="[^"]+" target="link"( rel="noreferrer noopener")?>([^<]+)<\/a>/gi, //<A href=¥S+ target=¥"link¥">(¥S+)<¥/A>
				Post.relinkify1stMatching
			)
			.replace(/\n/g, "\n&gt; ");
		lines = ("&gt; " + lines + "\n")
			.replace(/\n&gt;[ \n\r\f\t]+\n/g, "\n")
			.replace(/\n&gt;[ \n\r\f\t]+\n$/, "\n");
		return lines;
	},
	textCandidate: function() {
		var text = this.text
			.replace(/^&gt; (.*\n?)|^.*\n?/gm, "$1")
			.replace(/\n$/, "")
			.replace(/^[ \n\r\f\t]*$/gm, "$&\n$&");

		//TODO 引用と本文の間に一行開ける
		//text = text.replace(/((?:&gt; .*\n)+)(.+)/, "$1\n$2"); //replace(/^(?!&gt; )/m, "\n$&");

		return text; // + "\n\n";
	},
	textCandidateLooksValid: function() {
		return (
			this.getText()
				.replace(/^&gt; .*/gm, "")
				.trim() !== ""
		);
	},
	textBonus: 2,
	dateCandidate: function() {
		return this.parentDate;
	},
	dateCandidateLooksValid: function(candidate) {
		return /^\d{4}\/\d{2}\/\d{2}\(.\)\d{2}時\d{2}分\d{2}秒$/.test(candidate);
	},
	dateBonus: 100,
	hasQuote: function() {
		return /^&gt; /m.test(this.text);
	},
	mayHaveParent: function() {
		return this.isRead && !this.isOP() && this.hasQuote();
	},
	adoptAsEldestChild: function(childToBeAdopted) {
		var child = this.child;

		if (child) {
			childToBeAdopted.next = child;
		}

		this.child = childToBeAdopted;
		childToBeAdopted.parent = this;
	},
	getKeyForOwnParent: function() {
		return this.parentId;
	},
};

function Popup(config, image, body) {
	body = body || document.body;
	this.waitingMetadata = null;

	this.handleEvent = function(e) {
		var type = e.type;

		if (
			type === "keydown" &&
			!/^Esc(?:ape)?$/.test(e.key) &&
			e.keyIdentifier !== "U+001B"
		) {
			// not ESC
			return;
		}
		if (type === "mouseout" && e.relatedTarget.closest(".popup")) {
			return;
		}

		this.doHandleEvent();
	};

	this.doHandleEvent = function() {
		var popup = document.getElementById("image-view");
		if (popup) {
			popup.parentNode.removeChild(popup);
		}

		Array.prototype.slice
			.call(document.getElementsByClassName("popup"))
			.forEach(function(el) {
				el.classList.remove("popup");
			});

		this.removeEventListeners(body);

		if (this.waitingMetadata) {
			clearTimeout(this.waitingMetadata);
		}
	};

	this.addEventListeners = function() {
		this.toggleEventListeners("add");
	};
	this.removeEventListeners = function() {
		this.toggleEventListeners("remove");
	};
	this.toggleEventListeners = function(toggle) {
		["click", "keydown", "mouseout"].forEach(function(type) {
			body[toggle + "EventListener"](type, this);
		}, this);
	};

	function getRatio(natural, max) {
		if (/^\d+$/.test(max) && natural > max) {
			return +max / natural;
		} else {
			return 1;
		}
	}

	this.popup = function() {
		var isBestFit = config.popupBestFit;
		var viewport = document.compatMode === "BackCompat"
			? document.body
			: document.documentElement;
		var windowHeight = viewport.clientHeight;
		var windowWidth = viewport.clientWidth;
		var imageView = document.createElement("figure");
		imageView.id = "image-view";
		imageView.classList.add("popup");
		imageView.style.visibility = "hidden";
		imageView.innerHTML =
			'<figcaption><span id="percentage"></span>%</figcaption>';

		// bodyに追加することでimage-orientationが適用され
		// natural(Width|Height)以外の.*{[wW]idth|[hH]eight)が
		// EXIFのorientationが適用された値になる
		imageView.appendChild(image);
		body.appendChild(imageView);

		var width = image.offsetWidth;
		var height = image.offsetHeight;
		var marginHeight =
			Math.round(imageView.getBoundingClientRect().height) - height;
		var maxWidth = config.popupMaxWidth || (isBestFit ? windowWidth : width);
		var maxHeight =
			config.popupMaxHeight ||
			(isBestFit ? windowHeight - marginHeight : height);
		var ratio = Math.min(
			getRatio(width, maxWidth),
			getRatio(height, maxHeight)
		);
		var percentage = Math.floor(ratio * 100);
		var bgcolor = ratio < 0.5 ? "red" : ratio < 0.9 ? "blue" : "green";
		// 丸めないと画像が表示されないことがある
		var imageHeight = Math.floor(height * ratio) || 1;
		var imageWidth = Math.floor(width * ratio) || 1;

		imageView.style.display = "none";
		image.height = imageHeight;
		image.width = imageWidth;

		imageView.querySelector("#percentage").textContent = percentage;

		imageView.style.cssText = "background-color: " + bgcolor;
	};

	this.waitAndOpen = function() {
		if (
			!image.complete &&
			image.naturalWidth === 0 &&
			image.naturalHeight === 0
		) {
			this.waitingMetadata = setTimeout(this.waitAndOpen.bind(this), 50);
		} else {
			this.waitingMetadata = null;
			this.popup();
		}
	};
}

function Preload(head) {
	this.preloads = Object.create(null);
	this.head = head || document.head;

	var DOMTokenListSupports = function(tokenList, token) {
		if (!tokenList || !tokenList.supports) {
			return;
		}
		try {
			return tokenList.supports(token);
		} catch (e) {
			if (e instanceof TypeError) {
				console.log("The DOMTokenList doesn't have a supported tokens list");
			} else {
				console.error("That shouldn't have happened");
			}
		}
	};
	this.isSupported = DOMTokenListSupports(
		document.createElement("link").relList,
		"preload"
	);
}
Preload.prototype.fetch = function(url) {
	if (!this.isSupported || this.isFetched(url)) {
		return;
	}

	var link = document.createElement("link");
	link.rel = "preload";
	link.as = "image";
	link.href = url;

	this.head.appendChild(link);
	this.preloads[url] = true;
};
Preload.prototype.isFetched = function(url) {
	return this.preloads[url];
};

function memoize(fn) {
	var cache = {};
	return function(arg) {
		if (!cache.hasOwnProperty(arg)) {
			cache[arg] = fn(arg);
		}
		return cache[arg];
	};
}

var IS_FIREFOX = typeof InstallTrigger !== "undefined";
var IS_GM = typeof GM_setValue === "function";
var IS_GM4 = typeof GM !== "undefined";
var IS_EXTENSION = !IS_GM && !IS_GM4;

var GM_xmlhttpRequest$1 = function (options) { return GM_xmlhttpRequest(options); };

var GM$1 = {
	xmlHttpRequest: function (options) { return GM.xmlHttpRequest(options); },
};

var checkAnimation = function (imgURL) { return new Promise(function (resolve) {
		var options = {
			url: imgURL.replace(/\w+$/, "pch"),
			type: "HEAD",
			method: "HEAD",
			onload: function (response) { return resolve(response.status === 200); },
		};

		if (IS_GM4) {
			GM$1.xmlHttpRequest(options);
		} else if (IS_GM) {
			GM_xmlhttpRequest$1(options);
		} else if (IS_EXTENSION) {
			ajax(options).then(function () { return resolve(true); }, function () { return resolve(false); });
		}
	}); };

var mayHaveSmallerImage = /^http:\/\/misao\.(?:mixh|on\.arena\.ne)\.jp\/c\/up\/misao\d+\.\w+$/;
var small = function(href) {
	return mayHaveSmallerImage.test(href)
		? href.replace(/up\//, "up/pixy_")
		: href;
};
var animation = function(href) {
	var mayHaveAnimation = /^(http:\/\/misao\.(?:mixh|on\.arena\.ne)\.jp\/c\/)up\/(misao0*\d+)\.(?:png|jpg)$/;
	var ref = mayHaveAnimation.exec(href) || [];
	var directory = ref[1];
	var id = ref[2];

	if (id) {
		var animationURL =
			directory + "upload.cgi?m=A&id=" + id.replace(/misao0*/, "");

		return {id: id, href: animationURL};
	}
};

var sites = {
	sw: [
		{
			name: "misao",
			prefix: "http://misao.mixh.jp/c/",
			urls: function urls(href) {
				return {
					small: this.small(href),
					animation: this.animation(href),
				};
			},
			small: small,
			animation: animation,
		},
		{
			name: "misao-arena",
			prefix: "http://misao.on.arena.ne.jp/c/",
			urls: function urls(href) {
				return {
					small: this.small(href),
					animation: this.animation(href),
				};
			},
			small: small,
			animation: animation,
		},
		{
			name: "betanya",
			prefix: "http://komachi.betanya.com/uploader/stored/",
			urls: function (href) { return ({
				small: href,
			}); },
		} ],

	otherSites: [
		{
			name: "imgur",
			prefix: /^https?:\/\/(?:i\.)?imgur\.com\/[^/]+$/,
			urls: function urls(href) {
				var original = href.replace(/^https?:\/\/(?:i\.)?/, "https:/i.");
				var thumbnail = original.replace(/\.\w+$/, "t$&");

				return {
					small: thumbnail,
				};
			},
		},
		{
			name: "twimg",
			prefix: /^https?:\/\/pbs\.twimg\.com\/media\/[\w_-]+\.\w+/,
			suffix: /(?::(?:orig|large|medium|small|thumb))?$/,
			urls: function urls(href) {
				var ref = this.prefix.exec(href) || [];
				var hrefWithoutTag = ref[0];

				if (!hrefWithoutTag) {
					return;
				}

				return {
					original: hrefWithoutTag + ":orig",
					small: hrefWithoutTag + ":thumb",
				};
			},
		},
		{
			name: "any",
			suffix: /^[^?#]+\.(?:jpe?g|png|gif|bmp)(?:[?#]|$)/i,
			urls: function (original) { return ({original: original}); },
		} ],
};

function Thumbnail(config) {
	this.config = config;
	this.preload = new Preload();

	var animationChecker = memoize(checkAnimation);

	// ポップアップを消した時、カーソルがサムネイルの上にある
	this.isClosedAboveThumbnail = function(e) {
		var relatedTarget = e.relatedTarget;

		//firefox:
		if (relatedTarget === null) {
			return true;
		}

		//opera12
		if (relatedTarget instanceof HTMLBodyElement) {
			return true;
		}

		//chrome
		if (
			relatedTarget.closest("#image-view") &&
			!document.getElementById("image-view")
		) {
			return true;
		}
	};

	function setNote(a, text) {
		var note = a.nextElementSibling;
		// span.noteがない
		if (!note || !note.classList.contains("note")) {
			note = document.createElement("span");
			note.className = "note";

			a.parentNode.insertBefore(note, a.nextSibling);
		}

		note.textContent = text;
	}

	this.downloading = function(image, a) {
		var pending = true;
		var complete = function(success) {
			pending = false;
			if (success) {
				var note = a.nextElementSibling;
				if (note && note.classList.contains("note")) {
					note.parentNode.removeChild(note);
				}
			} else {
				setNote(a, "404?画像ではない?");
			}
		};

		image.addEventListener("load", complete.bind(null, true));
		image.addEventListener("error", complete.bind(null, false));

		setTimeout(function() {
			if (pending) {
				setNote(a, "ダウンロード中");
			}
		}, 100);
	};

	this.handleEvent = function(e) {
		if (this.isClosedAboveThumbnail(e)) {
			return;
		}

		var a = e.currentTarget;

		// ポップアップからサムネイルに帰ってきた
		if (a.classList.contains("popup")) {
			return;
		}

		var image = new Image();
		image.referrerPolicy = "no-referrer";

		this.downloading(image, a);

		image.classList.add("image-view-img");
		image.src = a.href;

		a.classList.add("popup");

		var popup = new Popup(config, image);
		popup.addEventListeners();
		popup.waitAndOpen();
	};

	this.thumbnailLink = function(href, container) {
		var site = getProperSite(href);
		if (!site) {
			return;
		}

		var ref = site.urls(href);
		var original = ref.original; if ( original === void 0 ) original = href;
		var small = ref.small;
		var animation = ref.animation;
		var thumbnailSrc = this.small(original, small);

		var thumbnail = this.thumbnail(original, thumbnailSrc);

		if (animation && config.linkAnimation) {
			this.checkAnimation(href, animation.id, container);

			thumbnail += animationHTML(animation);
		}

		if (config.shouki) {
			thumbnail += shouki(original);
		}

		return thumbnail;
	};

	var getProperSite = function (href) {
		var site;
		if (/\.(?:jpe?g|png|gif|bmp)$/i.test(href)) {
			site = pickProperSite({
				sites: sites.sw,
				href: href,
				testPrefix: startsWith,
			});
		}

		if (!site && config.popupAny) {
			site = pickProperSite({
				sites: sites.otherSites,
				href: href,
				testPrefix: test,
				testSuffix: test,
			});
		}

		return site;
	};

	var pickProperSite = function (ref) {
			var sites$$1 = ref.sites;
			var href = ref.href;
			var testPrefix = ref.testPrefix;
			var testSuffix = ref.testSuffix; if ( testSuffix === void 0 ) testSuffix = pass;

			return sites$$1.find(
			function (ref) {
				var prefix = ref.prefix;
				var suffix = ref.suffix;

				return testPrefix(href, prefix) && testSuffix(href, suffix);
			}
		);
	};

	var pass = function () { return true; };
	var startsWith = function (href, prefix) { return !prefix || href.startsWith(prefix); };
	var test = function (href, test) { return !test || test.test(href); };

	this.small = function(original, small) {
		if (!small) {
			return undefined;
		}

		if (original === small) {
			return small;
		}

		if (!config.thumbnailPopup) {
			return small;
		}

		this.preload.fetch(original);

		if (this.preload.isFetched(original)) {
			return small;
		} else {
			return original;
		}
	};

	var a = function (href, content) { return ("<a href=\"" + href + "\" target=\"link\" class=\"thumbnail\">" + content + "</a>"); };

	this.thumbnail = function(original, small) {
		if (small) {
			return a(original, thumbnailHTML(small));
		} else {
			return "[" + a(original, "■") + "]";
		}
	};

	var thumbnailHTML = function (src) { return ("<img referrerpolicy=\"no-referrer\" class=\"thumbnail-img\" src=\"" + src + "\">"); };

	var animationHTML = function (ref) {
			var id = ref.id;
			var href = ref.href;

			return "<span class=\"animation " + id + "\">" +
		"[<a href=\"" + href + "\" target=\"link\">A</a><span class=\"unsure\">?</span>]" +
		"</span>";
	};

	var shouki = function (href) { return ("[<a href=\"http://images.google.com/searchbyimage?image_url=" + href + "\" target=\"link\">詳</a>]"); };

	this.checkAnimation = function (href, id, container) { return animationChecker(href)
			.then(function (isAnimation) { return (isAnimation ? ("." + id + " .unsure") : ("." + id)); })
			.then(function (selector) { return container.querySelector(selector); })
			.then(function (e) { return e.parentNode.removeChild(e); }); };

	this.register = function(container) {
		var this$1 = this;

		var as = container.querySelectorAll("a[target]");
		var has = false;
		var i;
		for (i = as.length - 1; i >= 0; i--) {
			var a = as[i];
			var thumbnail = this$1.thumbnailLink(a.href, container);
			if (thumbnail) {
				a.insertAdjacentHTML("beforebegin", thumbnail);
				has = true;
			}
		}
		if (has && config.thumbnailPopup) {
			var thumbs = container.getElementsByClassName("thumbnail");
			for (i = thumbs.length - 1; i >= 0; i--) {
				thumbs[i].addEventListener("mouseover", this$1, false);
			}
		}
	};
}

function identity(x) {
	return x;
}

var Posts = {
	checkCharacterEntity: function(config, data) {
		var state = data.state;
		var post = data.post;

		state.hasCharacterEntity = /&amp;#(?:\d+|x[\da-fA-F]+);/.test(data.value);
		state.expandCharacterEntity =
			state.hasCharacterEntity &&
			(post.hasOwnProperty("characterEntity")
				? post.characterEntity
				: config.characterEntity);

		return data;
	},
	characterEntity: function(data) {
		var state = data.state;

		if (state.expandCharacterEntity) {
			var iter = document.createNodeIterator(
				data.value,
				NodeFilter.SHOW_TEXT,
				null,
				false
			); //operaは省略可能な第3,4引数も渡さないとエラーを吐く
			var node;
			while ((node = iter.nextNode())) {
				node.data = node.data.replace(
					/&#(\d+|x[0-9a-fA-F]+);/g,
					Posts.replaceCharacterEntity
				);
			}
		}

		return data;
	},
	replaceCharacterEntity: function(str, p1) {
		return String.fromCharCode(p1[0] === "x" ? parseInt(p1.slice(1), 16) : p1);
	},
	makeText: function(data) {
		//終わりの空行引用は消してレスする人がいる
		//引用の各行に空白を追加する人がいる
		var post = data.post;
		var text = post.getText();
		var parent = post.parent ? post.parent.computeQuotedText() : "";

		if (post.showAsIs || post.isNG) {
			text = Posts.markQuote(text, parent);
		} else {
			if (text.startsWith(parent)) {
				text = text.slice(parent.length);
			} else {
				//整形して
				parent = Posts.trimRights(parent);
				text = Posts.trimRights(text);

				//もう一度
				if (text.startsWith(parent)) {
					text = text.slice(parent.length);
				} else {
					//深海式レスのチェック
					var parent2 = parent
						.split("\n")
						.filter(function(line) {
							return !line.startsWith("&gt; &gt; ");
						})
						.join("\n");
					if (text.startsWith(parent2)) {
						text = text.slice(parent2.length);
					} else {
						text = Posts.markQuote(text, parent);
					}
				}
			}

			//全角空白も\sになる
			//空白のみの投稿が空投稿になる
			text = text.trimRight().replace(/^\s*\n/, "");

			if (text.length === 0) {
				text = '<span class="note">(空投稿)</span>';
			}
		}

		data.value = text;

		return data;
	},
	checkThumbnails: function(data) {
		data.state.mayHaveThumbnails = data.value.includes("<a");

		return data;
	},
	putThumbnails: function(config) {
		if (!config.thumbnail) {
			return identity;
		}

		var thumbnail = new Thumbnail(config);
		return function(data) {
			if (data.state.mayHaveThumbnails) {
				thumbnail.register(data.value);
			}

			return data;
		};
	},
	checkNGIfRead: function(ng) {
		if (!ng.isEnabled) {
			return identity;
		}

		return function(data) {
			var post = data.post;

			if (post.isRead) {
				Post.checkNG(ng, post);
			}

			return data;
		};
	},
	markNG: function(reg) {
		if (!reg) {
			return identity;
		}
		if (!reg.global) {
			throw new Error();
		}

		return function(data) {
			if (reg && data.post.isNG) {
				data.value = data.value.replace(
					reg,
					"<mark class='NGWordHighlight'>$&</mark>"
				);
			}

			return data;
		};
	},
	markNGHeader: function(reg) {
		if (reg && !reg.global) {
			throw new Error();
		}

		return function(value) {
			return value.replace(reg, "<mark class='NGWordHighlight'>$&</mark>");
		};
	},
	markQuote: function(text, parent) {
		var parentLines = parent.split("\n");
		parentLines.pop();
		var lines = text.split("\n");
		var i = Math.min(parentLines.length, lines.length);

		while (i--) {
			lines[i] =
				'<span class="quote' +
				(parentLines[i] === lines[i] ? "" : " modified") +
				'">' +
				lines[i] +
				"</span>";
		}

		return lines.join("\n");
	},
	trimRights: function(string) {
		return string.replace(/^.+$/gm, function(str) {
			return str.trimRight();
		});
	},
	truncate: function(config, data) {
		var post = data.post;

		if (!config.maxLine || post.showAsIs) {
			return data;
		}

		var text = data.value;
		var maxLine = +config.maxLine;
		var lines = text.split("\n");
		var length = lines.length;

		if (length > maxLine) {
			var truncation = post.hasOwnProperty("truncation")
				? post.truncation
				: true;
			var label;

			if (truncation) {
				lines[maxLine] = '<span class="truncation">' + lines[maxLine];
				text = lines.join("\n") + "\n</span>";
				label = "以下" + (length - maxLine) + "行省略";
			} else {
				text += "\n";
				label = "省略する";
			}

			text +=
				'(<a href="javascript:;" class="toggleTruncation note">' +
				label +
				"</a>)";
		}

		data.value = text;

		return data;
	},
	prependExtension: function(data) {
		if (data.state.extension) {
			return data.state.extension.text(data);
		} else {
			return data;
		}
	},
	createDText: function(treeMode) {
		var classes = "text text_" + treeMode;
		return function(data) {
			var post = data.post;

			var dText = document.createElement("div");
			dText.className = classes + (post.isRead ? " read" : "");
			dText.innerHTML = data.value;

			data.value = dText;

			return data;
		};
	},
	unfoldButton: function(data) {
		var rejectLevel = data.post.rejectLevel;
		var reasons = [];

		if (rejectLevel) {
			reasons.push([null, "孫", "子", "個"][rejectLevel]);
		}

		if (data.post.isNG) {
			reasons.push("NG");
		}

		return (
			'<a class="showMessageButton" href="javascript:;">' +
			reasons.join(",") +
			"</a>"
		);
	},
	hide: function(config) {
		var notCheckMode = !config.NGCheckMode;

		return function(data) {
			var post = data.post;

			data.state.hide = (post.isNG && notCheckMode) || post.rejectLevel;

			return data;
		};
	},
	headerContents: function(state, config, post, name, title) {
		var resUrl = post.resUrl ? 'href="' + post.resUrl + '" ' : "";
		var vanish;
		if (post.rejectLevel === 3) {
			vanish =
				' <a href="javascript:;" class="cancelVanishedMessage">非表示を解除</a>';
		} else if (config.useVanishMessage) {
			vanish = ' <a href="javascript:;" class="toggleMessage">消</a>';
		} else {
			vanish = "";
		}
		//prettier-ignore
		var header = '<a ' + resUrl + 'class="res" target="link">■</a>'
			+ '<span class="message-info">'
			+ ((title === '> ' || title === ' ') && name === ' '
				? ""
				: '<strong>' + title + '</strong> : <strong>' + name + '</strong> #'
			)
			+ post.date + '</span>'
			+ (resUrl && ' <a ' + resUrl + ' target="link">■</a>')
			+ vanish
			+ (state.hide ? ' <a href="javascript:;" class="fold">畳む</a>' : "")
			+ (post.posterUrl ? ' <a href="' + post.posterUrl + '" target="link">★</a>' : '')
			+ (state.hasCharacterEntity ? ' <a href="javascript:;" class="characterEntity' + (state.expandCharacterEntity ? ' characterEntityOn' : '' ) + '">文字参照</a>' : "")
			+ ' <a href="'
			+ post.threadUrl
			+ '" target="link">◆</a>';

		return header;
	},
};

var nextSibling = next("nextSibling");

function StackView(config) {
	this.range = document.createRange();
	this.original = document.createElement("div");
	this.original.className = "message original";
	this.thumbnail = new Thumbnail(config);

	this.showButtons = document.createElement("span");
	this.showButtons.className = "showOriginalButtons";

	this.range.selectNodeContents(this.original); // 引数は何でもいいが何かで上書きしないとopera12で<html>...</html>が返る
	this.vanishButton = this.range.createContextualFragment(
		'<a href="javascript:;" class="vanish">消</a>  '
	);
	this.showNGButton = this.range.createContextualFragment(
		'<a href="javascript:;" class="showNG">NG</a> '
	);
	this.showThreadButton = this.range.createContextualFragment(
		'<a href="javascript:;" class="showThread">非表示解除</a> '
	);

	this.needToWrap =
		config.useVanishThread ||
		config.keyboardNavigation ||
		(window.Intl && Intl.v8BreakIterator); // or blink
	this.useThumbnail = config.thumbnail;
	this.utterlyVanishNGThread = config.utterlyVanishNGThread;
	this.utterlyVanishNGStack = config.utterlyVanishNGStack;
	this.nextComment = nextSibling("#comment");
	this.makePost = Post.collectEssestialParts();
	this.config = config;
	this.ng = config.ng;
	this.markNG = this.createMarkNG(config.ng);
}
StackView.prototype = {
	setRange: function(start, end) {
		this.range.setStartBefore(start);
		this.range.setEndAfter(end);
	},

	deleteMessage: function(post) {
		var el = post.el;
		var end = this.nextComment(el.blockquote);
		this.setRange(el.anchor, end);
		this.range.deleteContents();
	},

	wrapMessage: function(post) {
		var el = post.el;
		var wrapper = this.original.cloneNode(false);

		this.setRange(el.anchor, el.blockquote);
		this.range.surroundContents(wrapper);

		if (this.config.useVanishThread) {
			var thread = el.threadButton;
			thread.parentNode.insertBefore(this.vanishButton.cloneNode(true), thread);
			wrapper.dataset.threadId = post.threadId;
		}

		return wrapper;
	},

	createMarkNG: function(ng) {
		var word = ng.wordg;
		var handle = ng.handleg;
		var markNG = Posts.markNG(word);
		var markNGHeader = Posts.markNGHeader(handle);

		return function(post) {
			var el = post.el;
			if (word) {
				var data = {
					value: post.text,
					post: post,
				};

				markNG(data);

				el.pre.innerHTML = data.value;
			}

			if (handle) {
				el.name.innerHTML = markNGHeader(post.name);
				el.title.innerHTML = markNGHeader(post.title);
			}
		};
	},

	wrapOne: function(a) {
		var post = this.makePost(a);
		var buttons = [];

		if (this.vanish(post, buttons) === false) {
			return;
		}

		if (this.vanishByNG(post, buttons) === false) {
			return;
		}

		this.buildMessage(post, buttons);
		this.registerThumbnail(post);
	},

	buildMessage: function(post, buttons) {
		if (this.needToWrap || buttons.length) {
			var wrapper = this.wrapMessage(post);

			if (buttons.length) {
				wrapper.classList.add("hidden");
				var showButtons = wrapper.parentNode.insertBefore(
					this.showButtons.cloneNode(false),
					wrapper
				);

				buttons.forEach(function(button) {
					showButtons.appendChild(button.cloneNode(true));
				});
			}
		}
	},
	vanish: function(post, buttons) {
		if (this.config.useVanishThread) {
			if (this.config.vanishedThreadIDs.indexOf(post.threadId) !== -1) {
				if (this.utterlyVanishNGThread) {
					this.deleteMessage(post);
					return false;
				} else {
					buttons.push(this.showThreadButton);
				}
			}
		}
	},

	vanishByNG: function(post, buttons) {
		var ng = this.ng;
		if (ng.isEnabled) {
			Post.checkNG(ng, post);

			if (post.isNG) {
				if (this.utterlyVanishNGStack) {
					this.deleteMessage(post);
					return false;
				} else if (this.config.NGCheckMode) {
					this.markNG(post);
				} else {
					buttons.push(this.showNGButton);
				}
			}
		}
	},

	registerThumbnail: function(post) {
		if (this.useThumbnail) {
			this.thumbnail.register(post.el.pre);
		}
	},
};

var Range = (function () {
	function anonymous() {
		this.range = this.makeRange();
	}
	anonymous.prototype.makeRange = function makeRange () {
		return document.createRange();
	};
	anonymous.prototype.setEndAfter = function setEndAfter (node) {
		this.range.setEndAfter(node);
	};
	anonymous.prototype.setStartAfter = function setStartAfter (node) {
		this.range.setStartAfter(node);
	};
	anonymous.prototype.setStartBefore = function setStartBefore (node) {
		this.range.setStartBefore(node);
	};
	anonymous.prototype.extractContents = function extractContents () {
		return this.range.extractContents();
	};
	anonymous.prototype.createContextualFragment = function createContextualFragment (html) {
		return this.range.createContextualFragment(html);
	};

	return anonymous;
}());

var clearVanishedIds = function(config, method, button) {
	config[method]();
	button.firstElementChild.innerHTML = "0";
};

var loop = function (func, array) { return new Promise(function (resolve, reject) {
		var i = 0;
		var length = array.length;

		(function loop() {
			var t = Date.now();
			do {
				if (i === length) {
					resolve();
					return;
				}

				try {
					func(array[i++]);
				} catch (e) {
					reject(e);
					return;
				}
			} while (Date.now() - t < 20);
			setTimeout(loop, 0);
		})();
	}); };

var on = function (el, event, selector, callback) {
	el.addEventListener(event, function (e) {
		if (e.target.closest(selector)) {
			if (callback.handleEvent) {
				callback.handleEvent(e);
			} else {
				callback(e);
			}
		}
	});
};

var Stack = {
	common: function(config, body) {
		Stack.addEventListener(config, body);
		Stack.configButton(config, body);
		Stack.accesskey(config, body);
	},
	accesskey: function(config, body) {
		var midoku = body.querySelector('input[name="midokureload"]');
		if (midoku) {
			midoku.accessKey = config.accesskeyReload;
		}
	},
	addEventListener: function(config, body) {
		on(body, "click", ".showNG", this.showNG);
		on(body, "click", ".showThread", this.showThread.bind(this, config));
		on(
			body,
			"click",
			".clearVanishedThreadIDs",
			this.clearVanishedThreadIDs.bind(this, config)
		);
		on(body, "click", ".vanish", this.vanish.bind(this, config));
	},
	showNG: function(e) {
		Stack.removeButtons(e.target.parentNode.nextElementSibling);
	},
	showThread: function(config, e) {
		e.preventDefault();

		var buttons = e.target.parentNode;
		var thisMessage = buttons.nextElementSibling;
		var id = thisMessage.dataset.threadId;
		var restore = Stack.savePosition(buttons);

		config.removeVanishedThread(id);

		Array.prototype.filter
			.call(document.querySelectorAll(".original"), function(message) {
				return message.dataset.threadId === id;
			})
			.forEach(function(message) {
				if (message === thisMessage) {
					restore();
				}

				Stack.removeButtons(message);
			});
	},
	clearVanishedThreadIDs: function(config, e) {
		e.preventDefault();
		clearVanishedIds(config, "clearVanishedThreadIDs", e.target);
	},
	removeButtons: function(message) {
		var buttons = message.previousElementSibling;
		message.classList.remove("hidden");
		buttons.parentNode.removeChild(buttons);
	},
	vanish: function(config, e) {
		e.preventDefault();

		var message = e.target.closest(".original");
		var id = message.dataset.threadId;
		var data = e.target.classList.contains("revert")
			? Stack.doRevertVanish()
			: Stack.doVanish();
		var restore = Stack.savePosition(message);

		config[data.type + "VanishedThread"](id);

		Array.prototype.filter
			.call(document.querySelectorAll(".original"), function(message) {
				return message.dataset.threadId === id;
			})
			.forEach(function(message) {
				message.classList.toggle("message");
				message.querySelector("blockquote").classList.toggle("hidden");

				var button = message.querySelector(".vanish");
				button.classList.toggle("revert");
				button.textContent = data.text;
			});

		restore();
	},
	doVanish: function() {
		return {
			text: "戻",
			type: "add",
		};
	},
	doRevertVanish: function() {
		return {
			text: "消",
			type: "remove",
		};
	},
	savePosition: function(element) {
		var top = element.getBoundingClientRect().top;
		return function restorePosition() {
			window.scrollTo(
				window.pageXOffset,
				window.pageYOffset + element.getBoundingClientRect().top - top
			);
		};
	},
	configButton: function(config, body) {
		var setup = body.querySelector('input[name="setup"]');
		if (setup) {
			var button = ' <a href="javascript:;" id="openConfig">★くわツリービューの設定★</a>';

			if (config.vanishedThreadIDs.length) {
				button +=
					' 非表示解除(<a class="clearVanishedThreadIDs" href="javascript:;"><span class="length">' +
					config.vanishedThreadIDs.length +
					"</span>スレッド</a>)";
			}

			setup.insertAdjacentHTML("afterend", button);
		}
	},
	render: function(config, body, view) {
		if (
			config.keyboardNavigation ||
			config.thumbnail ||
			config.ng.isEnabled ||
			config.useVanishThread
		) {
			var anchors = body.querySelectorAll("body > a[name]");
			var wrap = view.wrapOne.bind(view);

			if (IS_FIREFOX) {
				try {
					var html = body.parentNode;

					html.removeChild(body);

					anchors.forEach(wrap);
				} finally {
					html.appendChild(body);
				}
			} else {
				return loop(wrap, anchors);
			}
		}
	},
	tweakFooter: function(config, container, opt_done) {
		if (this.needsToTweakFooter(config)) {
			var insertFooter = this.doTweakFooter(container);

			return Promise.resolve(opt_done).then(insertFooter);
		}
	},
	needsToTweakFooter: function(config) {
		return (
			(config.ng.isEnabled && config.utterlyVanishNGStack) ||
			(config.useVanishThread && config.utterlyVanishNGThread)
		);
	},
	doTweakFooter: function(container) {
		var i = container.querySelector("p i");

		if (!i) {
			return doNothing;
		}

		var numPostsInfo = i.parentNode;

		var hr = nextElement("HR")(numPostsInfo);

		var insertionPoint = hr.nextSibling;

		var range = document.createRange();
		range.setStartBefore(numPostsInfo);
		range.setEndAfter(hr);

		var footer = range.extractContents();

		return function insertBack() {
			if (!footer.querySelector('table input[name="pnext"]')) {
				return;
			}

			footer.removeChild(numPostsInfo);
			insertionPoint.parentNode.insertBefore(footer, insertionPoint);
		};
	},
};

var StreamStackView = function StreamStackView(args) {
	Object.assign(this, args);

	this.range = new Range();
	this.main = document.createElement("main");
	this.main.id = "qtv-stack";
};
StreamStackView.prototype.init = function init () {
	Stack.common(this.config, this.body);

	this.buffer.insertBefore(this.main);
};
StreamStackView.prototype.finish = function finish (buffer) {
	Stack.tweakFooter(this.config, buffer);

	this.body.appendChild(buffer);

	return Promise.resolve(this.log.complement()).then(this.done);
};
StreamStackView.prototype.render = function render (buffer) {
	var ref = this;
		var range = ref.range;
		var view = ref.view;
		var main = ref.main;
		var firstComment = ref.firstComment;
	var comment;

	while ((comment = firstComment(buffer))) {
		range.setStartBefore(buffer.firstChild);
		range.setEndAfter(comment);
		// 以下のように一つずつやるとO(n)
		// 一気に全部やるとO(n^2)
		// chrome57の時点で一気にやってもO(n)になってる
		view.wrapOne(buffer.querySelector("a[name]"));
		main.appendChild(range.extractContents());
	}
};
StreamStackView.prototype.firstComment = function firstComment (buffer) {
	var first = buffer.firstChild;
	while (first) {
		if (first.nodeType === Node.COMMENT_NODE && first.nodeValue === " ") {
			return first;
		}
		first = first.nextSibling;
	}

	return null;
};

var ToggleOriginal = function ToggleOriginal(original) {
	this.toggle = document.createElement("div");
	this.button = this.createToggleButton();
	this.stack = this.createStackArea(original);
	this.toggle.appendChild(this.button);
	this.toggle.appendChild(this.stack);
};
ToggleOriginal.prototype.getUI = function getUI () {
	return this.toggle;
};
ToggleOriginal.prototype.createStackArea = function createStackArea (original) {
	var stack = document.createElement("div");
	stack.id = "qtv-stack";
	stack.hidden = true;
	stack.appendChild(original);

	return stack;
};
ToggleOriginal.prototype.createToggleButton = function createToggleButton () {
		var this$1 = this;

	var range = new Range();
	var fragment = range.createContextualFragment(
		'<div style="text-align:center"><a class="toggleOriginal" href="javascript:;">元の投稿の表示する(時間がかかることがあります)</a></div><hr>'
	);
	fragment
		.querySelector("a")
		.addEventListener("click", function (e) { return this$1.toggleOriginal(e); });

	return fragment;
};
ToggleOriginal.prototype.toggleOriginal = function toggleOriginal (e, win) {
		if ( win === void 0 ) win = window;

	e.preventDefault();
	e.stopPropagation();
	this.stack.hidden = !this.stack.hidden;
	win.scrollTo(
		win.pageXOffset,
		e.target.getBoundingClientRect().top + win.pageYOffset
	);
};

var storageIsAvailable = function (type, win) {
	if ( win === void 0 ) win = window;

	// https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API#Feature-detecting_localStorage
	try {
		var storage = win[type],
			x = "__storage_test__";
		storage.setItem(x, x);
		storage.removeItem(x);
		return true;
	} catch (e) {
		return false;
	}
};

var getStorage = function (config) {
	if (config.useVanishMessage && storageIsAvailable("localStorage")) {
		return localStorage;
	}

	if (storageIsAvailable("sessionStorage")) {
		return sessionStorage;
	}

	return nullStorage;
};
var nullStorage = {
	getItem: function getItem() {
		return null;
	},
	setItem: doNothing,
};

function createPostParent(config) {
	var storage;
	var data;
	var saveAsyncIfNeeded = function (posts) {
		if (!posts.length) {
			return;
		}

		storage = storage || getStorage(config);

		load();

		var changed;

		for (var i = 0; i < posts.length; i++) {
			var ref = posts[i];
			var id = ref.id;
			var parentId = ref.parentId;

			if (data.hasOwnProperty(id)) {
				break;
			}

			if (parentId && parentId.length > 20) {
				parentId = null;
			}

			data[id] = parentId;

			changed = true;
		}

		if (changed) {
			saveAsync(data);
		}
	};
	var load = function () {
		data = data || JSON.parse(storage.getItem("postParent")) || {};
	};
	var saveAsync = function (data) { return setTimeout(save, 0, data); };
	var save = function (data) { return storage.setItem("postParent", JSON.stringify(data)); };

	var TEN_SECONDS_LATER = 10 * 1000;
	var cleanUpLater = function () { return setTimeout(cleanUp, TEN_SECONDS_LATER, data); };
	var cleanUp = function (data) {
		if (!data) {
			return;
		}
		var ids = Object.keys(data);
		var limits = getLimits();
		if (ids.length <= limits.upper) {
			return;
		}

		ids = ids.map(function (id) { return +id; }).sort(function (l, r) { return r - l; });

		if (data[ids[0]] === false) {
			ids.shift();
		}

		var saveData = {};
		var i = limits.lower;
		while (i--) {
			saveData[ids[i]] = data[ids[i]];
		}
		saveAsync(saveData);
	};
	var getLimits = function () {
		if (config.vanishMessageAggressive && config.useVanishMessage) {
			return {upper: 3500, lower: 3300};
		}
		if (config.useVanishMessage) {
			return {upper: 1500, lower: 1300};
		}
		return {upper: 500, lower: 300};
	};

	var get = function (id) { return data[id]; };

	/**
	 * GhostPostが自身のIDを得るために子のMergedPostを渡す
	 */
	var findAsync = function (ref) {
		var id = ref.id;
		var threadId = ref.threadId;

		if (shouldFetch(id, threadId)) {
			return updateThread(threadId).then(function () { return get(id); });
		} else {
			return Promise.resolve(get(id));
		}
	};
	var isValidIds = function (childId, threadId) { return /^(?!0)\d+$/.test(threadId) && +threadId <= +childId; };
	var isActualStorage = function (storage) { return storage.removeItem; };
	var shouldFetch = function (childId, threadId) { return typeof data[childId] === "undefined" &&
		isActualStorage(storage) &&
		isValidIds(childId, threadId); };
	var updateThread = memoize(function (threadId) { return fetch({data: {m: "t", s: threadId}})
			.then(Post.makePosts)
			.then(saveAsyncIfNeeded); }
	);

	return {
		saveAsyncIfNeeded: saveAsyncIfNeeded,
		get: get,
		findAsync: findAsync,
		cleanUpLater: cleanUpLater,
	};
}

var div_ = document.createElement("div");

function DOM(html) {
	var div = div_.cloneNode(false);
	div.innerHTML = html;
	return div.firstChild;
}

var compose = function () {
	var fns = [], len = arguments.length;
	while ( len-- ) fns[ len ] = arguments[ len ];

	return function (x) { return fns.reduceRight(function (acc, fn) { return fn(acc); }, x); };
};

function AbstractPosts() {}
AbstractPosts.prototype = {
	init: function(item, el) {
		this.el = el || document.createElement("div");
		this.el.className = "messages";
		this.item = item;
	},
	getContainer: function() {
		return this.el;
	},
	render: function(config) {
		var this$1 = this;

		if (this.pre) {
			this.pre();
		}

		var roots = this.item;
		var maker = this.messageMaker(config);

		for (var i = 0, length = roots.length; i < length; i++) {
			this$1.doShowPosts(config, maker, roots[i], 1);
		}
		return this.el;
	},
	doShowPosts: function(config, maker, post, depth) {
		var dm = maker(post, depth);
		var dc = this.getContainer(post, depth);
		dc.appendChild(dm);

		if (post.child) {
			this.doShowPosts(config, maker, post.child, depth + 1);
		}
		if (post.next) {
			this.doShowPosts(config, maker, post.next, depth);
		}
	},
	checker: function(config) {
		var functions = [Posts.hide(config), Posts.checkNGIfRead(config.ng)];

		return compose.apply(null, functions);
	},
	text: function(config) {
		var markNG = Posts.markNG(config.ng.wordg);
		var putThumbnails = Posts.putThumbnails(config);
		var truncate = Posts.truncate.bind(Posts, config);
		var checkCharacterEntity = Posts.checkCharacterEntity.bind(Posts, config);

		return compose(
			putThumbnails,
			Posts.characterEntity,
			Posts.createDText(this.mode),
			Posts.prependExtension,
			truncate,
			markNG,
			checkCharacterEntity,
			Posts.checkThumbnails,
			Posts.makeText
		);
	},
	unfoldButton: Posts.unfoldButton,
	headerContents: Posts.headerContents,
	div: function(clazz, content) {
		var el = document.createElement("div");

		el.className = clazz;
		el.innerHTML = content;

		return el;
	},
	header: function(config) {
		var ng = config.ng;
		var markNGHeader = ng.handleg ? Posts.markNGHeader(ng.handleg) : identity;
		var classes = "message-header message-header_" + this.mode;

		return function(data) {
			var post = data.post;
			var state = data.state;
			var title = post.title;
			var name = post.name;

			if (post.isNG) {
				title = markNGHeader(title);
				name = markNGHeader(name);
			}

			var header = this.headerContents(state, config, post, name, title);

			return this.div(classes, header);
		}.bind(this);
	},
	env: function(data) {
		if (!data.post.env) {
			return null;
		}

		var env =
			'<span class="env">(' + data.post.env.replace(/<br>/, "/") + ")</span>";

		return this.div("extra extra_" + this.mode, this.doEnv(env, data));
	},
	doEnv: identity,
	message: function(header, text, env) {
		var el = document.createElement("div");

		el.appendChild(header);
		el.appendChild(text);

		if (env) {
			el.appendChild(env);
		}

		el.className = "message message_" + this.mode;

		return el;
	},
	messageMaker: function(config) {
		var checker = this.checker(config);
		var text = this.text(config);
		var header = this.header(config);

		return function(post, depth) {
			var dMessage;

			var data = checker({
				post: post,
				value: null,
				state: {
					depth: depth,
				},
			});

			var state = data.state;

			if (state.hide && !post.show) {
				dMessage = this.div(
					"showMessage showMessage_" + this.mode,
					this.unfoldButton(data)
				);
			} else {
				data = text(data);
				var dText = data.value;
				var dHeader = header(data);
				var dEnv = this.env(data);

				dMessage = this.message(dHeader, dText, dEnv);
			}

			if (config.spacingBetweenMessages) {
				this.setSpacer(dMessage, state.extension);
			}

			if (this.setMargin) {
				this.setMargin(dMessage, state.depth);
			}

			dMessage.id = post.id;
			dMessage.dataset.id = post.id;
			dMessage.post = post;

			return dMessage;
		}.bind(this);
	},
};

function CSSView() {
	this.mode = "tree-mode-css";
	this.containers = null;
	this.pre = function() {
		this.containers = [{dcontainer: this.el}];
	};

	this.border = function(depth) {
		var left = depth + 0.5;

		//prettier-ignore
		return DOM('<div class="border outer" style="left:' + left + 'rem">' +
			'<div class="border inner" style="left:-' + left + 'rem">' +
			'</div></div>');
	};

	this.getContainer = function(post, depth) {
		var containers = this.containers;
		var container = containers[containers.length - 1];

		if ("lastChildID" in container && container.lastChildID === post.id) {
			containers.pop();
			container = containers[containers.length - 1];
		}

		var child = post.child;
		if (child && child.next) {
			var lastChild = child;
			do {
				lastChild = lastChild.next;
			} while (lastChild.next);

			var dout = this.border(depth);
			container.dcontainer.appendChild(dout);
			container = {lastChildID: lastChild.id, dcontainer: dout.firstChild};
			containers.push(container);
		}

		return container.dcontainer;
	};

	this.setSpacer = function(el) {
		el.classList.add("spacing");
	};

	this.setMargin = function(el, depth) {
		el.style.marginLeft = depth + "rem";
	};
}
CSSView.prototype = Object.create(AbstractPosts.prototype);

function ASCIIView() {
	this.mode = "tree-mode-ascii";

	function wrapTree(tag, tree) {
		return "<" + tag + ' class="a-tree">' + tree + "</" + tag + ">";
	}

	function computeExtension(config, post) {
		var forHeader, forText, init;
		var utterlyVanishMessage = config.utterlyVanishMessage;
		var hasNext = post.next;
		var tree = [];
		var parent = post;

		while ((parent = parent.parent)) {
			if (utterlyVanishMessage && parent.rejectLevel) {
				break;
			}
			tree.push(parent.next ? "|" : " ");
		}
		init = tree.reverse().join("");

		if (post.isOP()) {
			forHeader = " ";
		} else {
			forHeader = init + (hasNext ? "├" : "└");
		}
		forText = init + (hasNext ? "|" : " ") + (post.child ? "|" : " ");

		return {header: forHeader, text: forText};
	}

	this.extension = function(config, data) {
		var extension = computeExtension(config, data.post);

		data.state.extension = {
			text: function(data) {
				data.value = data.value.replace(
					/^/gm,
					wrapTree("span", extension.text)
				);

				return data;
			},
			header: function(header) {
				return wrapTree("span", extension.header) + header;
			},
			env: function(env) {
				return wrapTree("span", extension.text) + env;
			},
			spacer: function() {
				return wrapTree("div", extension.text);
			},
		};

		return data;
	};

	this.checker = function(config) {
		var checker = AbstractPosts.prototype.checker.apply(this, arguments);

		return compose(this.extension.bind(this, config), checker);
	};

	this.setSpacer = function(el, extension) {
		el.insertAdjacentHTML("beforeend", extension.spacer());
	};

	var headerContents = AbstractPosts.prototype.headerContents;
	var unfoldButton = AbstractPosts.prototype.unfoldButton;

	this.headerContents = function(state) {
		return state.extension.header(headerContents.apply(null, arguments));
	};

	this.unfoldButton = function(data) {
		return data.state.extension.header(unfoldButton(data));
	};

	this.doEnv = function(env, data) {
		return data.state.extension.env(env);
	};
}
ASCIIView.prototype = Object.create(AbstractPosts.prototype);

var createView$1 = function (mode) { return new {
		"tree-mode-css": CSSView,
		"tree-mode-ascii": ASCIIView,
	}[mode](); };

function showThread(
	ref,
	view
) {
	var config = ref.config;
	var el = ref.el;
	if ( view === void 0 ) view = createView$1(config.treeMode);

	var mode = config.treeMode;
	var toggleTreeMode =
		mode === "tree-mode-css" && config.toggleTreeMode
			? ' <a href="javascript:;" class="toggleTreeMode">●</a>'
			: "";

	var vanishButtons = config.useVanishThread
		? {
				// class に revert がないが、分岐に使っているのは .NGThread なので気にしないでいい
				true: ' <a href="javascript:;" class="vanish">戻</a>',
				false: ' <a href="javascript:;" class="vanish">消</a>',
			}
		: {true: "", false: ""};

	function template(ref) {
		var thread = ref.thread;
		var classExtra = ref.classExtra;
		var date = ref.date;
		var contents = ref.contents;
		var headerExtra = ref.headerExtra; if ( headerExtra === void 0 ) headerExtra = "";

		var url = "<a href=\"" + (thread.getURL()) + "\" target=\"link\">◆</a>";
		return (
			"<pre data-thread-id=\"" + (thread.getID()) + "\" class=\"thread " + mode + " " + classExtra + "\">" +
			"<div class=\"thread-header\">" + url + " 更新日:" + date + headerExtra + " " + url + "</div>" +
			contents + "</pre>"
		);
	}
	var makeThreadHtml = function (ref) {
			var thread = ref.thread;
			var number = ref.number;
			var isVanished = ref.isVanished;

			return template({
			thread: thread,
			classExtra: isVanished ? "NGThread" : "",
			date: thread.getAppropriateDate(),
			headerExtra:
				" 記事数:" + number + toggleTreeMode + vanishButtons[isVanished],
			contents: '<span class="messages"></span>',
		});
	};

	function build(ref) {
		var thread = ref.thread;
		var isVanished = ref.isVanished;
		var roots = ref.roots;

		var number = thread.getNumber();

		if (!number) {
			return document.createTextNode("");
		}

		var dthread = DOM(makeThreadHtml({thread: thread, number: number, isVanished: isVanished}));

		view.init(roots, dthread.lastChild);
		view.render(config);
		dthread.roots = roots;

		return dthread;
	}

	return function doShowThread(thread) {
		var isVanished = thread.isVanished();

		if (isVanished && config.utterlyVanishNGThread) {
			return;
		}

		var roots = thread.computeRoots();
		return el.appendChild(build({thread: thread, isVanished: isVanished, roots: roots}));
	};
}

var showThreads = function(config, gui, threads) {
	gui.setInfo(" - スレッド構築中");
	var el = gui.getContent();

	return loop(showThread({config: config, el: el}), threads);
};

function shouldMakeUrlsSearchLog(q, posts) {
	if (!q.shouldMakeUrlsSearchLog()) {
		return posts;
	}

	posts.forEach(function (post) {
		var ref = post.date.match(/\d+/g) || [];
		var year = ref[0];
		var month = ref[1];
		var day = ref[2];
		var ff = "&ff=" + year + month + day + ".dat";
		post.threadUrl += ff; //post.threadUrl.replace(/&ac=1$/, "")必要?
		if (post.resUrl) {
			post.resUrl += ff;
		}
		if (post.posterUrl) {
			post.posterUrl += ff;
		}
	});

	return posts;
}

function checkNG(ng, posts) {
	for (var i = 0; i < posts.length; ++i) {
		Post.checkNG(ng, posts[i]);
	}
}

var excludeNg = function (posts) { return posts.filter(function (post) { return !post.isNG; }); };

var shouldExclude = function (config) { return !config.autovanishThread && config.utterlyVanishNGStack; };

function processNg(config, posts) {
	if (!config.ng.isEnabled) {
		return posts;
	}

	checkNG(config.ng, posts);

	if (shouldExclude(config)) {
		return excludeNg(posts);
	}

	return posts;
}

var fetch$1 = function (q, container) { return new Fetch(q).run(container); };

function complementMissingPostsFromLog(ref) {
	var q = ref.q;
	var gui = ref.gui;
	var container = ref.container;
	var posts = ref.posts;

	if (!q.shouldFetch()) {
		return Promise.resolve(posts);
	}

	gui.setInfoHtml(("<strong>" + (q.getLogName()) + "以外の過去ログを検索中...</strong>"));

	var makePostsAndConcat = function (posts, div) { return posts.concat( Post.makePosts(div)); };

	return fetch$1(q, container).then(function (ref) {
		var afters = ref.afters;
		var befores = ref.befores;

		return afters.reduce(makePostsAndConcat, []).concat( posts,
		befores.reduce(makePostsAndConcat, []) );
	});
}

var makePosts = function(ref) {
	var config = ref.config;
	var q = ref.q;
	var gui = ref.gui;
	var container = ref.container;

	var originalPosts = Post.makePosts(container);

	return complementMissingPostsFromLog({
		q: q,
		gui: gui,
		container: container,
		posts: originalPosts,
	})
		.then(function (posts) { return processNg(config, posts); })
		.then(function (posts) { return shouldMakeUrlsSearchLog(q, posts); });
};

var ImaginaryPostPrototype = {
	__proto__: Post.prototype,
	/**
	 * @param {Post} child
	 */
	setFields: function(child) {
		this.id = child.parentId;

		this.parent = null;
		this.next = null;
		this.isNG = null;

		this.threadId = child.threadId;
		this.threadUrl = child.threadUrl;

		this.parentId = this.isOP() ? null : undefined;

		if (this.id) {
			this.setResUrl(child);
		}
	},
	calculate: function(property) {
		var value,
			child = this.child;
		var getCandidate = property + "Candidate";

		if (child.next) {
			var rank = Object.create(null),
				max = 0,
				candidate;
			var validates = getCandidate + "LooksValid";
			var bonus = this[property + "Bonus"];

			do {
				candidate = child[getCandidate]();
				rank[candidate] = ++rank[candidate] || 1;
				if (child[validates](candidate)) {
					rank[candidate] += bonus;
				}
			} while ((child = child.next));

			for (candidate in rank) {
				var number = rank[candidate];
				if (max < number) {
					max = +number;
					value = candidate;
				}
			}
		} else {
			value = child[getCandidate]();
		}

		return Object.defineProperty(this, property, {value: value})[property];
	},
	getText: function() {
		return this.text;
	},
	isRead: true,
	setResUrl: function(child) {
		this.resUrl = child.resUrl.replace(/(\?|&)s=\d+/, "$1s=" + this.id);
	},
	getKeyForOwnParent: function() {
		return this.parentId ? this.parentId : "parent of " + this.id;
	},
};
Object.defineProperty(ImaginaryPostPrototype, "text", {
	get: function() {
		return this.calculate("text");
	},
});

function GhostPost(child) {
	this.setFields(child);
}
GhostPost.prototype = Object.create(ImaginaryPostPrototype);
GhostPost.prototype.date = "?";
GhostPost.prototype.getIdForcibly = function(postParent) {
	return postParent.findAsync(this.child);
};

function MergedPost(child) {
	this.setFields(child);
	this.name = child.title.replace(/^>/, "");
}
MergedPost.prototype = Object.create(ImaginaryPostPrototype, {
	date: {
		get: function() {
			return this.calculate("date");
		},
	},
});

function makeParent(post) {
	if (post instanceof MergedPost) {
		return new GhostPost(post);
	} else if (post instanceof Post) {
		return new MergedPost(post);
	} else {
		throw new Error("should not be called");
	}
}

function Thread(config, postParent) {
	this.config = config;
	this.postParent = postParent;
	this.posts = [];
	this.isNG = false;
	this.allPosts = Object.create(null);
}
Thread.computeRejectLevelForRoot = function(
	vanishedMessageIDs,
	postParent,
	id,
	level
) {
	if (!id || level === 0) {
		return 0;
	}

	if (vanishedMessageIDs.indexOf(id) > -1) {
		return level;
	}

	return Thread.computeRejectLevelForRoot(
		vanishedMessageIDs,
		postParent,
		postParent.get(id),
		level - 1
	);
};
Thread.inheritRejectLevel = function(vanishedMessageIDs, post, generation) {
	if (!post) {
		return;
	}

	var rejectLevel = 0;

	if (vanishedMessageIDs.indexOf(post.id) > -1) {
		rejectLevel = 3;
	} else if (generation > 0) {
		rejectLevel = generation;
	}

	post.rejectLevel = rejectLevel;

	Thread.inheritRejectLevel(vanishedMessageIDs, post.child, rejectLevel - 1);
	Thread.inheritRejectLevel(vanishedMessageIDs, post.next, generation);
};

Thread.prototype = {
	addPost: function(post) {
		this.posts.push(post);
		this.allPosts[post.id] = post;

		if (post.isNG) {
			this.isNG = true;
		}
	},
	computeRoots: function() {
		var roots = this.computeRoots2();

		if (!this.config.useVanishMessage) {
			return roots;
		}

		if (!this.shouldSetRejectLevel()) {
			return roots;
		}

		this.setRejectLevel(roots);

		if (!this.config.utterlyVanishMessage) {
			return roots;
		}

		return this.dropRejectedPosts(roots);
	},
	computeRoots2: function() {
		return this.computeRoots2ndPass(this.computeRoots1stPass());
	},
	computeRoots1stPass: function() {
		this.makeFamilyTree();

		var orphans = this.posts.filter(Post.isOrphan);

		this.connect(orphans);

		return this.getRootCandidates().sort(Post.byID);
	},
	makeFamilyTree: function() {
		this.posts.filter(Post.wantsParent).forEach(this.adopt, this);
	},
	connect: function(orphans) {
		orphans.forEach(this.makeParent, this);
		orphans.forEach(this.adopt, this);
	},
	getRootCandidates: function() {
		return Object.values(this.allPosts).filter(Post.isRootCandidate);
	},
	computeRoots2ndPass: function(roots) {
		var orphans = roots.filter(Post.mayHaveParent);

		orphans.forEach(this.readParentId, this);

		this.connect(orphans);

		return this.getRootCandidates().sort(this.byID);
	},
	readParentId: function(post) {
		post.parentId = this.postParent.get(post.id);
	},
	makeParent: function(orphan) {
		var key = orphan.getKeyForOwnParent();
		this.allPosts[key] = this.allPosts[key] || makeParent(orphan);
	},
	byID: function(l, r) {
		var lid = l.id ? l.id : l.child.id;
		var rid = r.id ? r.id : r.child.id;

		return lid - rid;
	},
	adopt: function(post) {
		var parent = this.allPosts[post.getKeyForOwnParent()];
		if (!parent) {
			return;
		}

		parent.adoptAsEldestChild(post);
	},
	shouldSetRejectLevel: function() {
		return this.getSmallestMessageID() <= this.getThreshold();
	},
	getThreshold: function() {
		return +this.config.vanishedMessageIDs[0];
	},
	getSmallestMessageID: function() {
		return Object.keys(this.allPosts).sort(this.byNumber)[0];
	},
	byNumber: function(l, r) {
		return l - r;
	},

	setRejectLevel: function(roots) {
		var vanishedMessageIDs = this.config.vanishedMessageIDs;
		var computeRejectLevelForRoot = Thread.computeRejectLevelForRoot;
		var postParent = this.postParent;

		for (var i = roots.length - 1; i >= 0; i--) {
			var root = roots[i];
			var child = root.child;
			var id = root.id;

			if (id) {
				root.rejectLevel = computeRejectLevelForRoot(
					vanishedMessageIDs,
					postParent,
					id,
					3
				);
			}

			if (child) {
				Thread.inheritRejectLevel(
					vanishedMessageIDs,
					child,
					root.rejectLevel - 1
				);
			}
		}

		return roots;
	},
	dropRejectedPosts: function(roots) {
		var newRoots = [];
		function drop(post, isRoot) {
			if (!post) {
				return null;
			}

			var child = drop(post.child, false);
			var next = drop(post.next, false);

			var isRead = post.isRead;
			if (!child && isRead) {
				return next;
			}

			post.child = child;
			post.next = next;

			var rejectLevel = post.rejectLevel;
			if (isRoot && rejectLevel === 0) {
				newRoots.push(post);
			} else if (rejectLevel === 1 && child) {
				newRoots.push(child);
			}

			return rejectLevel === 3 ? next : post;
		}

		for (var i = roots.length - 1; i >= 0; i--) {
			drop(roots[i], true);
		}

		return newRoots.sort(Post.byID);
	},
	getDate: function() {
		return this.posts[0].date;
	},
	getAppropriateDate: function() {
		if (this.config.utterlyVanishMessage) {
			return this.posts.filter(Post.isClean)[0].date;
		} else {
			return this.getDate();
		}
	},
	getNumber: function() {
		return this.posts.filter(Post.isClean).length;
	},
	getID: function() {
		return this.posts[0].threadId;
	},
	getURL: function() {
		return this.posts[0].threadUrl;
	},
	isVanished: function isVanished() {
		return this.config.isVanishedThread(this.getID());
	},
};

function makeThreads(config, postParent, posts) {
	var allThreads = Object.create(null);
	var threads = [];

	posts.forEach(function(post) {
		var id = post.threadId;
		var thread = allThreads[id];
		if (!thread) {
			thread = allThreads[id] = new Thread(config, postParent);
			threads.push(thread);
		}

		thread.addPost(post);
	});

	return threads;
}

function sortThreads(config, threads) {
	if (config.threadOrder === "ascending") {
		threads.reverse();
	}
}

var transformToThreads = function(ref) {
	var config = ref.config;
	var postParent = ref.postParent;
	var posts = ref.posts;

	var threads = makeThreads(config, postParent, posts);
	sortThreads(config, threads);

	return threads;
};

var deleteFooter = function(container, howManyPosts) {
	var i = container.querySelector("p i");
	if (!i) {
		return;
	}

	// <P><I><FONT size="-1">ここまでは、現在登録されている新着順1番目から1番目までの記事っぽい!</FONT></I></P>
	var numPostsInfo = i.parentNode; // === <P>
	var buttons = nextElement("TABLE")(numPostsInfo);
	var end;

	if (buttons && howManyPosts) {
		// ボタンを残す
		end = numPostsInfo;
	} else {
		// ボタンはないか、あるが0件の振りをするため消す
		end = nextElement("HR")(numPostsInfo);
	}

	deleteBetween(numPostsInfo, end);
};

function deleteBetween(start, end) {
	var range = document.createRange();

	range.setStartBefore(start);
	range.setEndAfter(end);

	range.deleteContents();
}

function suggestLinkToLog(ref) {
	var q = ref.q;
	var gui = ref.gui;
	var posts = ref.posts;
	var href = ref.href; if ( href === void 0 ) href = location.href;

	if (!posts) {
		throw new Error("no posts");
	}

	if (q.shouldSuggestLinkToLog(posts)) {
		var fill = function (n) { return (n < 10 ? "0" + n : n); };
		var today = new Date();
		var year = today.getFullYear();
		var month = fill(today.getMonth() + 1);
		var date = fill(today.getDate());
		var url = href + "&ff=" + year + month + date + ".dat";

		gui.appendExtraInfoHtml((" <a id=\"hint\" href=\"" + url + "\">過去ログを検索する</a>"));
	}
}

function setPostCount(setPostCount, postLength) {
	var message;
	if (postLength) {
		message = postLength + "件取得";
	} else {
		message = "未読メッセージはありません。";
	}

	setPostCount(message);
}

function autovanishThread(config, gui, threads) {
	if (!config.autovanishThread) {
		return;
	}

	var ids = threads.filter(function (thread) { return thread.isNG; }).map(function (thread) { return thread.getID(); });

	if (!ids.length) {
		return;
	}

	return gui.showSaving(function () { return config.addVanishedThread(ids); });
}

var buildAndShowThreads = function(ref) {
	var config = ref.config;
	var q = ref.q;
	var gui = ref.gui;
	var container = ref.container;
	var postParent = ref.postParent; if ( postParent === void 0 ) postParent = createPostParent(config);

	var mPosts = makePosts({config: config, q: q, gui: gui, container: container});

	var gotAllowedToTweakContainer = mPosts.then(function (posts) { return deleteFooter(container, posts.length); }
	);

	var gotDone = mPosts.then(function (posts) {
		suggestLinkToLog({q: q, gui: gui, posts: posts});
		setPostCount(gui.setPostCount, posts.length);

		postParent.saveAsyncIfNeeded(posts);

		var threads = transformToThreads({config: config, postParent: postParent, posts: posts});

		autovanishThread(config, gui, threads);

		gui.addEventListeners(config, postParent);

		var done = showThreads(config, gui, threads);

		done.then(function (done) { return postParent.cleanUpLater(done); });

		done.then(function (done) { return gui.clearInfo(done); });

		return done.then(function () { return posts; });
	});
	return {gotDone: gotDone, gotAllowedToTweakContainer: gotAllowedToTweakContainer};
};

var createReload = function (config) {
	var reload = '<input type="button" value="リロード" class="mattari">';

	if (!config.zero) {
		reload = reload.replace("mattari", "reload");
		reload += '<input type="button" value="未読" class="mattari">';
	}

	return reload;
};

var focusV = function () {
	setTimeout(function() {
		document.getElementsByName("v")[0].focus();
	}, 50);
};

var getAccesskey = function(config) {
	var accesskey = config.accesskeyReload;
	return /^\w$/.test(accesskey) ? accesskey : "R";
};

var getViewsAndViewing = function(body) {
	var hr = body.getElementsByTagName("hr")[0];
	if (hr) {
		var font = hr.previousElementSibling;
		if (font && font.tagName === "FONT") {
			// eslint-disable-next-line
			// 2005/03/01 から views(こわれにくさレベル4) 現在の参加者 : viewing名 (300秒以内)
			var ref = font.textContent.match(/\d+/g) || [];
			var views = ref[3];
			var viewing = ref[5];
			return (views + " / " + viewing + " 名");
		}
	}

	return "";
};

var midokureload = function() {
	var midoku = document.querySelector('#form input[name="midokureload"]');
	if (midoku) {
		midoku.click();
	} else {
		location.reload();
	}
};

var reload = function() {
	var form = document.getElementById("form");
	if (!form) {
		location.reload();
		return;
	}

	var reload = document.getElementById("qtv-reload");
	if (!reload) {
		form.insertAdjacentHTML(
			"beforeend",
			'<input type="submit" id="qtv-reload" name="reload" value="1" style="display:none;">'
		);
	}

	document.getElementById("qtv-reload").click();
};

var createTreeGuiContainer = function (config, body) {
	var el = document.createElement("div");
	el.id = "container";
	el.innerHTML =
		headerTemplate(config, body) +
		'<div id="content"></div><hr>' +
		footerTemplate(config);

	//event
	var click = on.bind(null, el, "click");
	click(".reload", reload);
	click(".mattari", midokureload);
	click(".goToForm", focusV);
	addClearVanishEvent(config, click);

	var header = el.firstElementChild;
	var firstChildOfHeader = header.firstElementChild;
	var info = firstChildOfHeader.lastElementChild;
	var postcount = info.previousElementSibling;

	return {
		container: el,
		info: info,
		postcount: postcount,
		content: header.nextSibling,
		footer: el.lastChild,
	};
};

var addClearVanishEvent = function (config, click) {
	["Message", "Thread"].forEach(function (type) {
		var id = "clearVanished" + type + "IDs";
		click("#" + id, function (e) {
			e.preventDefault();
			clearVanishedIds(config, id, e.target);
		});
	});
};

function headerTemplate(config, body) {
	var reload$$1 = createReload(config);
	var accesskey = getAccesskey(config);
	var viewsAndViewing = getViewsAndViewing(body);

	return ("\n\t\t<header id=\"header\">\n\t\t\t<span class=\"left\">\n\t\t\t\t" + (reload$$1.replace('class="mattari"', ("$& accesskey=\"" + accesskey + "\""))) + "\n\t\t\t\t" + viewsAndViewing + "\n\t\t\t\t<span id=\"postcount\"></span>\n\t\t\t\t<span id=\"info\">ダウンロード中...</span>\n\t\t\t</span>\n\t\t\t<span>\n\t\t\t\t<a href=\"javascript:;\" id=\"openConfig\">設定</a>\n\t\t\t\t<a href=\"#link\">link</a>\n\t\t\t\t<a href=\"#form\" class=\"goToForm\">投稿フォーム</a>\n\t\t\t\t" + reload$$1 + "\n\t\t\t</span>\n\t\t</header>");
}

function footerTemplate(config) {
	var reload$$1 = createReload(config);
	var length = {
		Thread: config.vanishedThreadIDs.length,
		Message: config.vanishedMessageIDs.length,
	};
	var hidden = length.Thread || length.Message ? "" : "hidden";

	var count = function (type, text) { return ("<a id=\"clearVanished" + type + "IDs\" href=\"javascript:;\"><span class=\"count\">" + (length[
			type
		]) + "</span>" + text + "</a>"); };

	return ("\n\t\t<footer id=\"footer\">\n\t\t\t<span class=\"left\">\n\t\t\t\t" + reload$$1 + "\n\t\t\t</span>\n\t\t\t<span>\n\t\t\t\t<span class=\"clearVanishedButtons " + hidden + "\">\n\t\t\t\t\t非表示解除(" + (count("Thread", "スレッド")) + "/" + (count("Message", "投稿")) + ")\n\t\t\t\t</span>\n\t\t\t\t" + reload$$1 + "\n\t\t\t</span>\n\t\t</footer>");
}

var setText = function (node) { return function (text) {
	node.textContent = text;
}; };

var setHtml = function (element) { return function (html) {
	element.innerHTML = html;
}; };

var appendHtmlAfter = function (node) { return function (html) {
	node.insertAdjacentHTML("afterend", html);
}; };

var showSaving = function (config, footer) { return function (execute) {
	var buttons = footer.querySelector(".clearVanishedButtons");
	buttons.insertAdjacentHTML(
		"beforebegin",
		'<span class="savingVanishedThreadIDs">非表示スレッド保存中</span>'
	);

	return execute.then(function showSaved() {
		var saving = buttons.previousElementSibling;
		saving.parentNode.removeChild(saving);

		var threadLength = config.vanishedThreadIDs.length;

		if (threadLength) {
			buttons.querySelector(
				"#clearVanishedThreadIDs .count"
			).textContent = threadLength;
			buttons.classList.remove("hidden");
		}
	});
}; };

var HideMessage = {
	changeTextState: function() {
		this.text.style.display = "none";
	},
	changeButtonText: function() {
		this.button.textContent = "戻";
	},
	save: function() {
		this.config.addVanishedMessage(this.post.id);
	},
	setRejectLevel: function() {
		var post = this.post;
		post.previousRejectLevel = post.rejectLevel;
		post.rejectLevel = 3;
	},
	shouldProcess: function(post, rejectLevel) {
		return post.rejectLevel < rejectLevel;
	},
	setChildRejectLevel: function(post, rejectLevel) {
		post.rejectLevel = rejectLevel;
	},
	processMarking: function(message) {
		if (!message.querySelector(".chainingHidden")) {
			message.firstElementChild.classList.add("chainingHidden");
		}
	},
};

var ShowMessage = {
	changeTextState: function() {
		this.text.style.display = null;
	},
	changeButtonText: function() {
		this.button.textContent = "消";
	},
	save: function() {
		this.config.removeVanishedMessage(this.post.id);
	},
	setRejectLevel: function() {
		var post = this.post;
		post.rejectLevel = post.previousRejectLevel;
	},
	shouldProcess: function(post, rejectLevel) {
		return post.rejectLevel <= rejectLevel;
	},
	setChildRejectLevel: function(post, _rejectLevel) {
		post.rejectLevel = 0;
	},
	processMarking: function(message) {
		var mark = message.querySelector(".chainingHidden");
		if (mark) {
			mark.classList.remove("chainingHidden");
		}
	},
};

function ToggleMessage(config, postParent) {
	this.config = config;
	this.postParent = postParent;
}
ToggleMessage.prototype = {
	handleEvent: function(e) {
		this.button = e.target;
		this.message = this.button.closest(".message");
		this.messages = this.message.closest(".messages");
		this.text = this.message.querySelector(".text");
		this.post = this.message.post;

		return this.execute();
	},
	execute: function() {
		return this.setIDToPost()
			.then(this.toggle.bind(this))
			.catch(this.error.bind(this));
	},
	toggle: function() {
		this.setRejectLevel();

		this.save();

		this.changeTextState();
		this.changeButtonState();

		this.setChildrensRejectLevel(this.post.child, 2);
	},
	changeButtonState: function() {
		this.toggleButtonState();
		this.changeButtonText();
	},
	toggleButtonState: function() {
		this.button.classList.toggle("revert");
	},
	isRevertButton: function() {
		return this.button.classList.contains("revert");
	},
	error: function(error) {
		this.button.parentNode.replaceChild(
			document.createTextNode(error.message),
			this.button
		);
	},
	setIDToPost: function() {
		var this$1 = this;

		return this.findPostID().then(function (id) {
			if (!id) {
				return Promise.reject(
					new Error("最新1000件以内に存在しないため投稿番号が取得できませんでした。過去ログからなら消せるかもしれません")
				);
			}

			if (id.length > 100) {
				return Promise.reject(new Error("この投稿は実在しないようです"));
			}

			this$1.post.id = id;
		});
	},
	findPostID: function() {
		var post = this.post;
		var id = post.id;
		if (id === undefined) {
			id = post.getIdForcibly(this.postParent);
		}

		return Promise.resolve(id);
	},
	setChildrensRejectLevel: function(post, rejectLevel) {
		if (post === null || rejectLevel === 0) {
			return;
		}

		if (this.shouldProcess(post, rejectLevel)) {
			this.setChildRejectLevel(post, rejectLevel);

			var message = this.getTargetMessage(post);
			if (message) {
				this.processMarking(message);
			}
		}

		this.setChildrensRejectLevel(post.child, rejectLevel - 1);
		this.setChildrensRejectLevel(post.next, rejectLevel);
	},
	getTargetMessage: function(post) {
		return this.messages.querySelector('[data-id="' + post.id + '"]');
	},
};

function ToggleMessageDispatcher(config, postParent) {
	this.config = config;
	this.postParent = postParent;
}
ToggleMessageDispatcher.prototype.handleEvent = function(e) {
	e.preventDefault();

	var handler = this.makeHandler(e);

	return handler.handleEvent(e);
};
ToggleMessageDispatcher.prototype.makeHandler = function(e) {
	var handler = new ToggleMessage(this.config, this.postParent);

	if (e.target.classList.contains("revert")) {
		Object.assign(handler, ShowMessage);
	} else {
		Object.assign(handler, HideMessage);
	}

	return handler;
};

var getTreeMode = function (node) {
	return node.closest(".tree-mode-css") ? "tree-mode-css" : "tree-mode-ascii";
};

var replace = function (config, change) { return function (e) {
	e.preventDefault();

	var message = e.target.closest(".message, .showMessage");
	var parent = message.parentNode;
	var post = message.post;
	var mode = getTreeMode(message);
	var view = createView$1(mode);
	var maker = view.messageMaker(config);
	var depth = parseInt(message.style.marginLeft, 10);

	change(post);

	var newMessage = maker(post, depth);

	parent.replaceChild(newMessage, message);
}; };

var showAsIs = function (config) { return function (e) {
	function callback(post) {
		post.showAsIs = !post.showAsIs;
	}

	var target = e.target;
	var id = setTimeout(replace(config, callback), 500, e);
	var cancel = function() {
		clearTimeout(id);
		target.removeEventListener("mouseup", cancel);
		target.removeEventListener("mousemove", cancel);
	};

	target.addEventListener("mouseup", cancel);
	target.addEventListener("mousemove", cancel);
}; };

var toggleTreeMode = function (config) { return function (e) {
	e.preventDefault();

	var button = e.target;
	var thread = button.closest(".thread");

	thread.classList.toggle("tree-mode-css");
	thread.classList.toggle("tree-mode-ascii");

	var view = createView$1(getTreeMode(thread));
	view.init(thread.roots);
	var newMessages = view.render(config);

	thread.replaceChild(newMessages, thread.querySelector(".messages"));
}; };

var toggleThread = function (config) { return function (e) {
	var button = e.target;
	var thread = button.closest(".thread");
	var id = thread.dataset.threadId;
	var type, text;

	if (thread.classList.contains("NGThread")) {
		type = "remove";
		text = "消";
	} else {
		type = "add";
		text = "戻";
	}
	type += "VanishedThread";

	config[type](id);
	thread.classList.toggle("NGThread");
	button.textContent = text;
}; };

var addEventListeners = function(ref) {
	var config = ref.config;
	var postParent = ref.postParent;
	var el = ref.el;

	function click(selector, callback) {
		on(el, "click", selector, replace(config, callback));
	}

	click(".characterEntity", function(post) {
		post.characterEntity = !(post.hasOwnProperty("characterEntity")
			? post.characterEntity
			: config.characterEntity);
	});

	click(".showMessageButton", function(post) {
		post.show = true;
	});

	click(".cancelVanishedMessage", function(post) {
		config.removeVanishedMessage(post.id);
		delete post.rejectLevel;
	});

	click(".fold", function(post) {
		post.show = false;
	});

	on(el, "mousedown", ".message", showAsIs(config));

	click(".toggleTruncation", function(post) {
		post.truncation = post.hasOwnProperty("truncation")
			? !post.truncation
			: false;
	});

	if (config.useVanishMessage) {
		on(
			el,
			"click",
			".toggleMessage",
			new ToggleMessageDispatcher(config, postParent)
		);
	}

	on(el, "click", ".vanish", toggleThread(config));

	on(el, "click", ".toggleTreeMode", toggleTreeMode(config));
};

var createGui = function (config, body) {
	if ( body === void 0 ) body = document.body;

	var ref = createTreeGuiContainer(
		config,
		body
	);
	var container = ref.container;
	var info = ref.info;
	var postcount = ref.postcount;
	var content = ref.content;
	var footer = ref.footer;

	return {
		setInfo: setText(info),
		setInfoHtml: setHtml(info),
		clearInfo: function () { return setText(info)(""); },
		appendExtraInfoHtml: appendHtmlAfter(info),
		setPostCount: setText(postcount),
		getContent: function () { return content; },
		addEventListeners: function (config, postParent) { return addEventListeners({config: config, postParent: postParent, el: content}); },
		showSaving: showSaving(config, footer),
		prependToBody: function prependToBody() {
			body.insertBefore(container, body.firstChild);
		},
	};
};

var originalRange = function(container, range) {
	if ( range === void 0 ) range = document.createRange();

	var firstAnchor = container.querySelector("a[name]");
	if (!firstAnchor) {
		return range;
	}

	var end = kuzuhaEnd(container);
	if (!end) {
		return range;
	}

	var start = startNode(container, firstAnchor);

	range.setStartBefore(start);
	range.setEndAfter(end);

	return range;
};

function startNode(container, firstAnchor) {
	var h1 = container.querySelector("h1");
	if (
		h1 &&
		h1.compareDocumentPosition(firstAnchor) & Node.DOCUMENT_POSITION_FOLLOWING
	) {
		return h1;
	} else {
		return firstAnchor;
	}
}

function kuzuhaEnd(container) {
	var last = container.lastChild;
	while (last) {
		var type = last.nodeType;
		if (
			(type === Node.COMMENT_NODE && last.nodeValue === " ") ||
			(type === Node.ELEMENT_NODE && last.nodeName === "H3")
		) {
			return last;
		}

		last = last.previousSibling;
	}

	return null;
}

var StreamTreeView = function StreamTreeView(args) {
	Object.assign(this, args);
	this.gui = createGui(this.config, this.body);
};
StreamTreeView.prototype.init = function init () {
	this.gui.prependToBody();
};
StreamTreeView.prototype.finish = function finish (buffer) {
		var this$1 = this;

	var ref = this;
		var config = ref.config;
		var gui = ref.gui;
		var q = ref.q;

	var ref$1 = buildAndShowThreads({
		config: config,
		q: q,
		gui: gui,
		container: buffer,
	});
		var gotDone = ref$1.gotDone;
		var gotAllowedToTweakContainer = ref$1.gotAllowedToTweakContainer;

	this.prepareToggleOriginal(buffer, gotDone);

	gotAllowedToTweakContainer.then(function () { return this$1.appendLeftovers(buffer); });

	return gotDone.then(this.done);
};
StreamTreeView.prototype.appendLeftovers = function appendLeftovers (buffer) {
	this.body.appendChild(buffer);
};
StreamTreeView.prototype.prepareToggleOriginal = function prepareToggleOriginal (buffer, done) {
	var range = originalRange(buffer);

	if (this.config.deleteOriginal) {
		range.deleteContents();
	} else {
		var original = range.extractContents();
		return Promise.all([original, done]).then(
			this.appendToggleOriginal.bind(this)
		);
	}
};
StreamTreeView.prototype.appendToggleOriginal = function appendToggleOriginal (ref) {
		var original = ref[0];
		var posts = ref[1];

	if (!original || !posts.length) {
		return;
	}

	var toggle = new ToggleOriginal(original);
	this.buffer.insertBefore(toggle.getUI());
};

var createView = function(ref) {
	var config = ref.config;
	var q = ref.q;
	var buffer = ref.buffer;
	var body = ref.body; if ( body === void 0 ) body = document.body;
	var done = ref.done;

	if (config.isTreeView()) {
		return new StreamTreeView({config: config, body: body, q: q, buffer: buffer, done: done});
	} else {
		var view = new StackView(config);
		var log = new StackLog(config, q, body, view);

		return new StreamStackView({config: config, body: body, buffer: buffer, view: view, log: log, done: done});
	}
};

var initView = function (ref) {
	var config = ref.config;
	var q = ref.q;
	var buffer = ref.buffer;
	var done = ref.done;

	var view = createView({config: config, q: q, buffer: buffer, done: done});
	view.init();
	buffer.setView(view);

	return view.done;
};

function getTitle() {
	return document.title;
}

function shouldCloseWindow(config, title) {
	return config.closeResWindow && title.endsWith(" 書き込み完了");
}

function sendMessageToRuntime(message) {
	chrome.runtime.sendMessage(message);
}

function closeResWindow() {
	if (IS_EXTENSION) {
		sendMessageToRuntime({type: "closeTab"});
	} else {
		window.open("", "_parent");
		window.close();
	}
}

var closeWindowIfNeeded = function (gotConfig) { return gotConfig.then(function (config) {
		var title = getTitle();
		if (shouldCloseWindow(config, title)) {
			closeResWindow();
		}
	}); };

var streamMain = function (gotConfig, q, execute) {
	var loaded = waitForDomContentLoaded();
	var observer = new Observer(loaded);
	var notice = createDelayNotice(gotConfig);
	var buffer = new Buffer();

	observer.addListener({
		onHr: function () { return execute(function (config, done) { return initView({config: config, q: q, buffer: buffer, done: done}); }
			).catch(function (e) {
				handleError(e);
				throw e;
			}); },
		onLoaded: function () { return closeWindowIfNeeded(gotConfig); },
	});

	observer.addListener(notice);
	observer.addListener(buffer);

	observer.observe();
};

var deleteOriginal = function(config, body) {
	if (config.deleteOriginal) {
		originalRange(body).deleteContents();
	}
};

var detachBody = function () {
	var body = document.body;
	if (IS_FIREFOX) {
		document.documentElement.removeChild(body);
	}
	return body;
};

var attachToDocumentElement = function (body) {
	if (IS_FIREFOX) {
		document.documentElement.appendChild(body);
	}
};

var tree = function(config, q) {
	var body = detachBody();
	try {
		var gui = createGui(config, body);

		var ref = buildAndShowThreads({
			config: config,
			q: q,
			gui: gui,
			container: body,
		});
		var gotDone = ref.gotDone;
		var gotAllowedToTweakContainer = ref.gotAllowedToTweakContainer;

		gotAllowedToTweakContainer.then(function () { return deleteOriginal(config, body); });

		gui.prependToBody();

		return gotDone;
	} finally {
		attachToDocumentElement(body);
	}
};

function stack(config, q, body) {
	if ( body === void 0 ) body = document.body;

	Stack.common(config, body);

	var view = new StackView(config);
	var log = new StackLog(config, q, body, view);

	var complement = log.complement();
	var render = Stack.render(config, body, view);
	var tweakFooter = Stack.tweakFooter(config, body, render);

	return Promise.all([complement, render, tweakFooter]);
}

var runProperView = function (config, q) { return (config.isTreeView() ? tree : stack)(config, q); };

var endMain = function (gotConfig, q, execute) { return ready().then(function () {
		closeWindowIfNeeded(gotConfig);

		return execute(function (config, done) { return runProperView(config, q).then(done); });
	}); };

function NG(config) {
	var word = config.NGWord;
	var handle = config.NGHandle;

	if (config.useNG) {
		if (handle) {
			this.handle = new RegExp(handle);
			this.handleg = new RegExp(handle, "g");
		}
		if (word) {
			this.word = new RegExp(word);
			this.wordg = new RegExp(word, "g");
		}
	}

	this.isEnabled = !!(this.word || this.handle);
}

var ChromeStorage = {
	load: function(_defaults) {
		var this$1 = this;

		return new Promise(function (resolve) {
			this$1.storage().get(null, resolve);
		});
	},
	remove: function(key) {
		this.storage().remove(key);
	},
	set: function(key, value, callback) {
		var item = {};
		item[key] = value;
		this.storage().set(item, callback);
	},
	setAll: function(items) {
		var this$1 = this;

		return new Promise(function (resolve) { return this$1.storage().set(items, resolve); });
	},
	clear: function() {
		var this$1 = this;

		return new Promise(function (resolve) {
			this$1.storage().clear(resolve);
		});
	},
	get: function(key, fun) {
		this.storage().get(key, function(item) {
			fun(item[key]);
		});
	},
	storage: function() {
		return chrome.storage.local;
	},
};

var GMStorage = {
	load: function(defaults) {
		return new Promise(function (resolve) {
			var config = Object.create(null);
			var keys = Object.keys(defaults);
			var i = keys.length;
			var key, value;
			while (i--) {
				key = keys[i];
				if (typeof defaults[key] === "function") {
					continue;
				}
				value = GM_getValue(key);
				if (value != null) {
					config[key] = JSON.parse(value);
				}
			}

			resolve(config);
		});
	},
	remove: function(key) {
		GM_deleteValue(key);
	},
	set: function(key, value, callback) {
		GM_setValue(key, JSON.stringify(value));

		if (callback) {
			callback();
		}
	},
	setAll: function(items) {
		var this$1 = this;

		for (var key in items) {
			this$1.set(key, items[key]);
		}
		return Promise.resolve();
	},
	clear: function() {
		GM_listValues().forEach(GM_deleteValue);
		return Promise.resolve();
	},
	get: function(key, fun) {
		fun(JSON.parse(GM_getValue(key, "null")));
	},
};

var GM4Storage = {
	load: function(defaults) {
		var this$1 = this;

		return new Promise(function (resolve) {
			var keys = Object.keys(defaults).filter(
				function (key) { return typeof defaults[key] !== "function"; }
			);

			Promise.all(keys.map(function (key) { return this$1.storage().getValue(key); }))
				.then(function (values) { return values.reduce(function (config, value, i) {
						if (value != null) {
							config[keys[i]] = JSON.parse(value);
						}

						return config;
					}, Object.create(null)); }
				)
				.then(resolve);
		});
	},
	remove: function(key) {
		this.storage().deleteValue(key);
	},
	set: function(key, value, callback) {
		if ( callback === void 0 ) callback = function () {};

		return this.storage()
			.setValue(key, JSON.stringify(value))
			.then(callback);
	},
	setAll: function(items) {
		var this$1 = this;

		var promises = [];
		for (var key in items) {
			promises.push(this$1.set(key, items[key]));
		}
		return Promise.all(promises);
	},
	clear: function() {
		var storage = this.storage();
		return storage.listValues().then(function (keys) { return keys.forEach(storage.deleteValue); });
	},
	get: function(key, callback) {
		return this.storage()
			.getValue(key, "null")
			.then(JSON.parse)
			.then(callback);
	},
	storage: function() {
		return GM;
	},
};

function Config(config, storage) {
	Object.assign(this, config);
	this._storage = storage;
	this.init();
}

Config.prototype = {
	treeMode: "tree-mode-ascii",
	toggleTreeMode: false,
	thumbnail: true,
	thumbnailPopup: true,
	popupAny: false,
	popupMaxWidth: "",
	popupMaxHeight: "",
	popupBestFit: true,
	threadOrder: "ascending",
	NGHandle: "",
	NGWord: "",
	useNG: true,
	NGCheckMode: false,
	spacingBetweenMessages: false,
	useVanishThread: true,
	vanishedThreadIDs: [], //扱い注意
	autovanishThread: false,
	utterlyVanishNGThread: false,
	useVanishMessage: false,
	vanishedMessageIDs: [],
	vanishMessageAggressive: false,
	utterlyVanishMessage: false,
	utterlyVanishNGStack: false,
	deleteOriginal: true,
	zero: true,
	accesskeyReload: "R",
	accesskeyV: "",
	keyboardNavigation: false,
	keyboardNavigationOffsetTop: "200",
	viewMode: "t",
	css: "",
	linkAnimation: true,
	shouki: true,
	closeResWindow: false,
	maxLine: "",
	openLinkInNewTab: false,
	characterEntity: true,
};

function init() {
	this.ng = new NG(this);
}

function setStorage(storage) {
	this._storage = storage;
}

var addID = function(type, id_or_ids, callback) {
	var this$1 = this;

	var ids = Array.isArray(id_or_ids) ? id_or_ids : [id_or_ids];
	var target = "vanished" + type + "IDs";

	this[target] = ids.concat(this[target]);

	this._storage.get(target, function (IDs) {
		IDs = Array.isArray(IDs) ? IDs : [];

		ids = ids.filter(function(id) {
			return IDs.indexOf(id) === -1;
		});

		IDs = IDs.concat(ids).sort(function(l, r) {
			return +r - l;
		});

		this$1[target] = IDs;
		this$1._storage.set(target, IDs, callback);
	});
};
var removeID = function(type, id) {
	var this$1 = this;

	var target = "vanished" + type + "IDs";
	this._storage.get(target, function (ids) {
		ids = Array.isArray(ids) ? ids : [];
		var index = ids.indexOf(id);
		if (index !== -1) {
			ids.splice(index, 1);
			this$1[target] = ids;
			if (ids.length) {
				this$1._storage.set(target, ids);
			} else {
				this$1._storage.remove(target);
			}
		}
	});
};
var clearIDs = function(type) {
	var target = "vanished" + type + "IDs";
	this._storage.remove(target);
	this[target] = [];
};

/** @param {String} id */
var addVanishedMessage = function(id) {
	this.addID("Message", id);
};
var removeVanishedMessage = function(id) {
	this.removeID("Message", id);
};
var clearVanishedMessageIDs = function() {
	this.clearIDs("Message");
};

/** @param {String} id */
var addVanishedThread = function(id) {
	var this$1 = this;

	return new Promise(function (resolve) {
		this$1.addID("Thread", id, resolve);
	});
};
var removeVanishedThread = function(id) {
	this.removeID("Thread", id);
};
var clearVanishedThreadIDs = function() {
	this.clearIDs("Thread");
};
var clearVanish = function() {
	clearVanishedMessageIDs();
	clearVanishedThreadIDs();
};
var clear = function() {
	var this$1 = this;

	return this._storage.clear().then(function () {
		Object.assign(this$1, Config.prototype);
	});
};
var update = function(items) {
	var this$1 = this;

	Object.keys(items)
		.filter(function (key) { return typeof Config.prototype[key] === "undefined"; })
		.forEach(function (key) { return delete items[key]; });

	return this._storage.setAll(items).then(function () {
		Object.assign(this$1, items);
	});
};

var isTreeView = function() {
	return this.viewMode === "t";
};

var isVanishedThread = function(id) {
	return this.useVanishThread && this.vanishedThreadIDs.indexOf(id) > -1;
};

Object.assign(Config.prototype, {
	init: init,
	setStorage: setStorage,

	addVanishedMessage: addVanishedMessage,
	removeVanishedMessage: removeVanishedMessage,
	clearVanishedMessageIDs: clearVanishedMessageIDs,
	addVanishedThread: addVanishedThread,
	removeVanishedThread: removeVanishedThread,
	clearVanishedThreadIDs: clearVanishedThreadIDs,
	clearVanish: clearVanish,
	clear: clear,
	update: update,

	isTreeView: isTreeView,
	isVanishedThread: isVanishedThread,

	addID: addID,
	removeID: removeID,
	clearIDs: clearIDs,
});

Config.load = function(storage) {
	storage = storage || Config.whichStorageToUse();

	return storage
		.load(Config.prototype)
		.then(function (config) { return new Config(config, storage); });
};

Config.whichStorageToUse = function() {
	return IS_GM ? GMStorage : IS_GM4 ? GM4Storage : ChromeStorage;
};

var BothWaysLogSearch = {
	getLogParameterName: function(query) {
		return query.get("ff");
	},
	getDayAsNumber: function(query) {
		return +this.getLogParameterName(query).match(/^(\d{8})\.dat$/)[1];
	},
	queryFor: function(query, ff) {
		var data = query.copy();
		data.ff = ff;
		return data;
	},
};

var FutureLogSearch = {
	getLogParameterName: function(query) {
		return Object.keys(query.q).find(function(key) {
			return /^chk\d+\.dat$/.test(key);
		});
	},
	getDayAsNumber: function(query) {
		return +this.getLogParameterName(query).match(/\d+/)[0];
	},
	queryFor: function(query, ff) {
		var data = query.copy();
		delete data[query.getLogParameterName()];
		data["chk" + ff] = "checked";

		return data;
	},
};

var Query = function Query(search, hostname) {
	if ( search === void 0 ) search = location.search;
	if ( hostname === void 0 ) hostname = location.hostname;

	this.q = typeof search === "object" ? search : Query.parse(search);
	this.hostname = hostname;
};
Query.parse = function parse (search) {
	var obj = {},
		kvs = search.substring(1).split("&");
	kvs.forEach(function(kv) {
		obj[kv.split("=")[0]] = kv.split("=")[1];
	});
	return obj;
};
Query.prototype.get = function get (key) {
	return this.q[key];
};
Query.prototype.set = function set (key, value) {
	this.q[key] = value;
};

Query.prototype.shouldHaveValidPosts = function shouldHaveValidPosts () {
	return this.q.sv || (this.q.e && this.isAtMisao());
};
Query.prototype.isAtMisao = function isAtMisao () {
	return (
		this.hostname === "misao.on.arena.ne.jp" ||
		this.hostname === "misao.mixh.jp"
	);
};

Query.prototype.isNormalMode = function isNormalMode () {
	return !this.q.m;
};

Query.prototype.shouldMakeUrlsSearchLog = function shouldMakeUrlsSearchLog () {
	return this.isThreadSearchWithin1000() || this.isPosterSearchInLog();
};
//通常モードからスレッドボタンを押した場合
Query.prototype.isThreadSearchWithin1000 = function isThreadSearchWithin1000 () {
	return this.q.m === "t" && !this.q.ff && /^\d+$/.test(this.q.s);
};
//検索窓→投稿者検索→★の結果の場合
Query.prototype.isPosterSearchInLog = function isPosterSearchInLog () {
	return this.q.s && this.q.ff && this.q.m === "s";
};

//ツリーでログ補完するべきか
Query.prototype.shouldFetch = function shouldFetch () {
	return this.shouldSearchLog() || this.isFromKomachi();
};
Query.prototype.shouldSearchLog = function shouldSearchLog () {
	return (
		this.q.m === "t" && /^\d+\.dat$/.test(this.q.ff) && /^\d+$/.test(this.q.s)
	);
};
Query.prototype.isFromKomachi = function isFromKomachi (referrer, search) {
		if ( referrer === void 0 ) referrer = document.referrer;
		if ( search === void 0 ) search = location.search;

	return (
		/^http:\/\/misao\.(?:mixh|on\.arena\.ne)\.jp\/c\/upload\.cgi/.test(
			referrer
		) &&
		/^\?chk\d+\.dat=checked&kwd=http:\/\/misao\.(?:mixh|on\.arena\.ne)\.jp\/c\/up\/misao\d+\.\w+&s1=0&e1=0&s2=24&e2=0&ao=a&tt=a&alp=checked&btn=checked(?:&g=checked)?&m=g&k=%82%A0&sv=on$/.test(
			search
		)
	);
};

Query.prototype.shouldSuggestLinkToLog = function shouldSuggestLinkToLog (posts) {
	return (
		this.isThreadSearchWithin1000() &&
		posts.every(function(post) {
			return !post.isOP();
		})
	);
};

//スタックでログ補完するべきか
Query.prototype.shouldComplement = function shouldComplement (body) {
	return this.shouldSearchLog() && !this.hasOP(body);
};
Query.prototype.selectorForOP = function selectorForOP () {
	return 'a[name="' + this.q.s + '"]';
};
Query.prototype.hasOP = function hasOP (body) {
	return body.querySelector(this.selectorForOP());
};

Query.prototype.getLogMode = function getLogMode () {
	return this.q.sv ? FutureLogSearch : BothWaysLogSearch;
};
// isLogMode() {
// return this.q.m === "g";
// }
Query.prototype.getLogParameterName = function getLogParameterName () {
	return this.getLogMode().getLogParameterName(this);
};
Query.prototype.getDayAsNumber = function getDayAsNumber () {
	return this.getLogMode().getDayAsNumber(this);
};
Query.prototype.copy = function copy () {
	return Object.assign({}, this.q);
};
Query.prototype.queryFor = function queryFor (ff) {
	return this.getLogMode().queryFor(this, ff);
};
Query.prototype.getLogName = function getLogName () {
	return this.getDayAsNumber() + ".dat";
};

var canProcessStreamingly = function (win) {
	if ( win === void 0 ) win = window;

	return !!win.MutationObserver;
};

var tweak = function(body) {
	var v = body.querySelector("textarea");
	if (v) {
		v.focus(); // Firefox needs focus before setSelectionRange.
		v.scrollIntoView();
		// 内容を下までスクロール firefox, opera12
		v.setSelectionRange(v.textLength, v.textLength);
		// 内容を下までスクロール chrome
		v.scrollTop = v.scrollHeight;
	}
};

var tweakResWindow = function () { return ready().then(getBody).then(tweak); };

var css = "\n.text {\n\twhite-space: pre-wrap;\n}\n.text, .extra {\n\tmin-width: 20rem;\n}\n.text_tree-mode-css, .extra_tree-mode-css {\n\tmargin-left: 1rem;\n}\n.env {\n\tfont-family: initial;\n\tfont-size: smaller;\n}\n.message_tree-mode-css, .border, .showMessage_tree-mode-css {\n\tposition: relative;\n}\n\n.thread-header {\n\tbackground: #447733 none repeat scroll 0 0;\n\tborder-color: #669955 #225533 #225533 #669955;\n\tborder-style: solid;\n\tborder-width: 1px 2px 2px 1px;\n\tfont-size: 0.8rem;\n\tfont-family: normal;\n\tmargin-top: 0.8rem;\n\tpadding: 0;\n\twidth: 100%;\n}\n\n.message-header {\n\twhite-space: nowrap;\n}\n.message-header_tree-mode-css {\n\tfont-size: 0.85rem;\n\tfont-family: normal;\n}\n.message-info {\n\tfont-family: monospace;\n\tcolor: #87CE99;\n}\n\n.read, .quote {\n\tcolor: #CCB;\n}\nheader, footer {\n\tdisplay: flex;\n\tfont-size: 0.9rem;\n}\nheader .left, footer .left {\n\tmargin-right: auto;\n}\n.thread {\n\tmargin-bottom: 1rem;\n}\n.modified {\n\tcolor: #FBB\n}\n.note, .characterEntityOn, .env {\n\tfont-style: italic;\n}\n.chainingHidden::after {\n\tcontent: \"この投稿も非表示になります\";\n\tfont-weight: bold;\n\tfont-style: italic;\n\tcolor: red;\n}\n.a-tree {\n\tfont-style: initial;\n}\n\n.inner {\n/*\t\t\tborder: 2px solid yellow; */\n\ttop: -1rem;\n}\n.outer {\n\tborder-left: 1px solid #ADB;\n\ttop: 1rem;\n}\n.thumbnail-img {\n\twidth: 80px;\n\tmax-height: 400px;\n\timage-orientation: from-image;\n}\n#image-view {\n\tposition: fixed;\n\ttop: 50%;\n\tleft: 50%;\n\ttransform: translate(-50%, -50%);\n\tbackground: #004040;\n\tcolor: white;\n\tfont-weight: bold;\n\tfont-style: italic;\n\tmargin: 0;\n\timage-orientation: from-image;\n}\n.image-view-img {\n\tbackground-color: white;\n}\n\n.focused {\n\tbox-shadow: 0px 0px 0px 2px yellow;\n}\n.truncation, .NGThread .messages, .hidden {\n\tdisplay: none;\n}\n.spacing {\n\tpadding-bottom: 1rem;\n}\n";

var applyCss = function(config) {
	document.head.insertAdjacentHTML(
		"beforeend",
		("<style>" + (css + config.css) + "</style>")
	);
};

function zero(config) {
	if (config.zero) {
		var d = document.getElementsByName("d")[0];
		if (d && d.value !== "0") {
			d.value = "0";
		}
	}
}

var id;
function progress(after, controller, fun) {
	clearTimeout(id);
	var info = controller.$("#configInfo");
	info.textContent = "保存中";
	setTimeout(function () {
		fun().then(function () {
			info.textContent = after;
			id = setTimeout(function () {
				info.innerHTML = "";
			}, 5000);
		});
	});
}

function ConfigController(item) {
	var this$1 = this;

	this.item = item;
	var el = document.createElement("form");
	el.id = "config";
	this.el = el;

	var events = [
		"save",
		"clear",
		"close",
		"clearVanishThread",
		"clearVanishMessage",
		"addToNGWord" ];
	for (var i = events.length - 1; i >= 0; i--) {
		var event = events[i];
		on(el, "click", "#" + event, this$1[event].bind(this$1));
	}

	on(el, "keyup", "#quote-input", this.quotemeta.bind(this));

	this.render();
}
ConfigController.prototype = {
	$: function(selector) {
		return this.el.querySelector(selector);
	},
	$$: function(selector) {
		return Array.prototype.slice.call(this.el.querySelectorAll(selector));
	},
	render: function() {
		this.el.innerHTML = this.template();
		if (IS_EXTENSION) {
			var close = this.$("#close");
			close.parentNode.removeChild(close);
		}
		this.restore();
	},
	template: function() {
		return '<style type="text/css">\
			<!--\
				li {\
					list-style-type: none;\
				}\
				#configInfo {\
					font-weight: bold;\
					font-style: italic;\
				}\
				legend + ul {\
					margin: 0 0 0 0;\
				}\
			-->\
			</style>\
			<fieldset>\
				<legend>設定</legend>\
				<fieldset>\
					<legend>表示</legend>\
					<ul>\
						<li><label><input type="radio" name="viewMode" value="t">ツリー表示</label></li>\
						<li><label><input type="radio" name="viewMode" value="s">スタック表示</label></li>\
					</ul>\
				</fieldset>\
				<fieldset>\
					<legend>共通</legend>\
					<ul>\
						<li><label><input type="checkbox" name="zero">常に0件リロード</label><em>(チェックを外しても「表示件数」は0のままなので手動で直してね)</em></li>\
						<li><label>未読リロードに使うアクセスキー<input type="text" name="accesskeyReload" size="1"></label></li>\
						<li><label>内容欄へのアクセスキー<input type="text" name="accesskeyV" size="1"></label></li>\
						<li><label><input type="checkbox" name="keyboardNavigation">jkで移動、rでレス窓開く</label><em><a href="@GF@#keyboardNavigation">chrome以外の人は説明を読む</a></em></li>\
						<ul>\
							<li><label>上から<input type="text" name="keyboardNavigationOffsetTop" size="4">pxの位置に合わせる</label></li>\
						</ul>\
						<li><label><input type="checkbox" name="closeResWindow">書き込み完了した窓を閉じる</label> <em><a href="@GF@#close-tab-in-firefox">firefoxは説明を読むこと</a></em><li>\
						<li><label><input type="checkbox" name="openLinkInNewTab">target属性の付いたリンクを常に新しいタブで開く</label></li>\
					</ul>\
				</fieldset>\
				<fieldset>\
					<legend>ツリーのみ</legend>\
					<ul style="display:inline-block">\
						<li><label><input type="checkbox" name="deleteOriginal">元の投稿を非表示にする</label>(高速化)</li>\
						<li>スレッドの表示順\
							<ul>\
								<li><label><input type="radio" name="threadOrder" value="ascending">古→新</label></li>\
								<li><label><input type="radio" name="threadOrder" value="descending">新→古</label></li>\
							</ul>\
						</li>\
						<li>ツリーの表示に使うのは\
							<ul>\
								<li><label><input type="radio" name="treeMode" value="tree-mode-css">CSS</label></li>\
								<li><label><input type="radio" name="treeMode" value="tree-mode-ascii">文字</label></li>\
							</ul>\
						</li>\
						<li><label><input type="checkbox" name="spacingBetweenMessages">記事の間隔を開ける</label></li>\
						<li><label><input type="text" name="maxLine" size="2">行以上は省略する</label></li>\
						<li><label><input type="checkbox" name="characterEntity">数値文字参照を展開</label> <em>(&#数字;が置き換わる)</em></li>\
						<li><label><input type="checkbox" name="toggleTreeMode">CSSツリー時にスレッド毎に一時的な文字/CSSの切り替えが出来るようにする</label></li>\
					</ul>\
					<fieldset style="display:inline-block">\
						<legend>投稿非表示設定</legend>\
						<ul>\
							<li><label><input type="checkbox" name="useVanishMessage">投稿非表示機能を使う</label> <em>使う前に<a href="@GF@#vanish">投稿非表示機能の注意点</a>を読むこと。</em><li>\
							<ul>\
								<li><span id="vanishedMessageIDs"></span>個の投稿を非表示中<input type="button" value="クリア" id="clearVanishMessage"></li>\
								<li><label><input type="checkbox" name="utterlyVanishMessage">完全に非表示</label></li>\
								<li><label><input type="checkbox" name="vanishMessageAggressive">パラノイア</label></li>\
							<ul>\
						</ul>\
					</fieldset>\
				</fieldset>\
				<fieldset>\
					<legend>スレッド非表示設定</legend>\
					<ul>\
						<li><label><input type="checkbox" name="useVanishThread">スレッド非表示機能を使う</label><li>\
						<ul>\
							<li><span id="vanishedThreadIDs"></span>個のスレッドを非表示中<input type="button" value="クリア" id="clearVanishThread"></li>\
							<li><label><input type="checkbox" name="utterlyVanishNGThread">完全に非表示</label></li>\
							<li><label><input type="checkbox" name="autovanishThread">NGワードを含む投稿があったら、そのスレッドを自動的に非表示に追加する(ツリーのみ)</label></li>\
						</ul>\
					</ul>\
				</fieldset>\
				<fieldset>\
					<legend>画像</legend>\
					<ul>\
						<li>\
							<label><input type="checkbox" name="thumbnail">小町と退避の画像のサムネイルを表示</label>\
							<ul>\
								<li>\
									<label><input type="checkbox" name="thumbnailPopup">ポップアップ表示</label>\
									<ul>\
										<li><label><input type="checkbox" name="popupBestFit">画面サイズに合わせる</label></li>\
										<li><label>最大幅:<input type="text" name="popupMaxWidth" size="5">px </label><label>最大高:<input type="text" name="popupMaxHeight" size="5">px <em>画面サイズに合わせない時の設定。空欄で原寸表示</em></label></li>\
									</ul>\
								</li>\
								<li><label><input type="checkbox" name="linkAnimation">描画アニメがある場合にリンクする</label></li>\
								<li><label><input type="checkbox" name="shouki">詳希(;゚Д゚)</label></li>\
							</ul>\
						</li>\
						<li><label><input type="checkbox" name="popupAny">小町と退避以外の画像も対象にする</label></li>\
					</ul>\
				</fieldset>\
				<fieldset>\
					<legend>NGワード</legend>\
					<ul>\
						<li><label><input type="checkbox" name="useNG">NGワードを使う</label>\
						<p>指定には正規表現を使う。以下簡易説明。複数指定するには|(縦棒)で"区切る"(先頭や末尾につけてはいけない)。()?*+[]{}^$.の前には\\を付ける。</p>\
						<li><table>\
							<tr>\
								<td><label for="NGHandle">ハンドル</label>\
								<td><input id="NGHandle" type="text" name="NGHandle" size="30"><em>投稿者とメールと題名</em>\
							<tr>\
								<td><label for="NGWord">本文</label>\
								<td><input id="NGWord" type="text" name="NGWord" size="30">\
							<tr><td><td><input id="quote-input" type="text" size="15" value=""> よく分からん人はここにNGワードを一つづつ入力して追加ボタンだ\
							<tr><td><td><input id="quote-output" type="text" size="15" readonly><input type="button" id="addToNGWord" value="本文に追加">\
						</table>\
						<li><label><input type="checkbox" name="NGCheckMode">NGワードを含む投稿を畳まず、NGワードをハイライトする</label>\
						<li><label><input type="checkbox" name="utterlyVanishNGStack">完全非表示</label>\
					</ul>\
				</fieldset>\
				<p>\
					<label>追加CSS<br><textarea name="css" cols="70" rows="5"></textarea></label>\
				</p>\
				<p>\
					<input type="submit" id="save" accesskey="s" value="保存(s)">\
					<input type="button" id="clear" style="float:right" value="デフォルトに戻す">\
					<input type="button" id="close" accesskey="c" value="閉じる(c)">\
					<span id="configInfo"></span>\
				</p>\
			</fieldset>'.replace(
			/@GF@/g,
			"https://greasyfork.org/scripts/1971-tree-view-for-qwerty"
		);
	},
	quotemeta: function() {
		var output = this.$("#quote-output");
		var input = this.$("#quote-input");
		output.value = ConfigController.quotemeta(input.value);
	},
	addToNGWord: function() {
		var output = this.$("#quote-output").value;
		if (!output.length) {
			return;
		}
		var word = this.$("#NGWord").value;
		if (word.length) {
			output = word + "|" + output;
		}
		this.$("#NGWord").value = output;
		this.$$("#quote-output, #quote-input").forEach(function(el) {
			el.value = "";
		});
	},
	save: function(e) {
		var this$1 = this;

		e.preventDefault();

		var items = {};
		this.$$("input, select, textarea").forEach(function(el) {
			var k = el.name;
			var v = null;

			if (!k) {
				return;
			}

			switch (el.type) {
				case "radio":
					if (el.checked) {
						v = el.value;
					}
					break;
				case "text":
				case "textarea":
					v = el.value;
					break;
				case "checkbox":
					v = el.checked;
					break;
			}

			if (v !== null) {
				items[k] = v;
			}
		});

		progress("保存しました。", this, function () { return this$1.item.update(items); });
	},

	clear: function() {
		var this$1 = this;

		progress("デフォルトに戻しました。", this, function () { return this$1.item.clear().then(function () { return this$1.restore(); }); }
		);
	},

	close: function() {
		this.el.parentNode.removeChild(this.el);
		window.scrollTo(0, 0);
	},

	clearVanishThread: function() {
		this.item.clearVanishedThreadIDs();
		this.$("#vanishedThreadIDs").textContent = "0";
		this.info("非表示に設定されていたスレッドを解除しました。");
	},

	clearVanishMessage: function() {
		this.item.clearVanishedMessageIDs();
		this.$("#vanishedMessageIDs").textContent = "0";
		this.info("非表示に設定されていた投稿を解除しました。");
	},

	info: function(text) {
		clearTimeout(this.id);
		var info = this.$("#configInfo");
		info.textContent = text;
		this.id = setTimeout(function() {
			info.innerHTML = "";
		}, 5000);
	},

	restore: function restore() {
		var config = this.item;
		this.$("#vanishedThreadIDs").textContent = config.vanishedThreadIDs.length;
		this.$("#vanishedMessageIDs").textContent =
			config.vanishedMessageIDs.length;

		this.$$("input, select, textarea").forEach(function(el) {
			var name = el.name;
			if (!name) {
				return;
			}
			switch (el.type) {
				case "radio":
					el.checked = config[name] === el.value;
					break;
				case "text":
				case "textarea":
					el.value = config[name];
					break;
				case "checkbox":
					el.checked = config[name];
					break;
			}
		});
	},
};
ConfigController.quotemeta = function(str) {
	return (str + "").replace(/([()[\]{}|*+.^$?\\])/g, "\\$1");
};

var Body = (function () {
	function anonymous() {
		this.body = document.body;
	}
	anonymous.prototype.prepend = function prepend (el) {
		this.body.insertBefore(el, this.body.firstChild);
	};

	return anonymous;
}());

var openConfig = function(config) {
	if (IS_EXTENSION) {
		sendMessageToRuntime({type: "openConfig"});
	} else if (!document.getElementById("config")) {
		new Body().prepend(new ConfigController(config).el);
		window.scrollTo(0, 0);
	}
};

var tweakLink = function(config, a) {
	if (config.openLinkInNewTab && a.target === "link") {
		a.target = "_blank";
	}

	if (a.target) {
		a.rel += " noreferrer noopener";
	}
};

function addCommonEvents(config) {
	var body = getBody();
	on(body, "click", "#openConfig", function (e) {
		e.preventDefault();
		openConfig(config);
	});
	on(body, "click", "a", function (e) {
		tweakLink(config, e.target);
	});
}

function setAccesskeyToV(config) {
	var accessKey = config.accesskeyV;
	if (accessKey.length === 1) {
		var v = document.getElementsByName("v")[0];
		if (v) {
			v.accessKey = accessKey;
		}
	}
}

function KeyboardNavigation(config, window) {
	if (!window) {
		throw new Error("missing window");
	}

	//同じキーでもkeypressとkeydownでe.whichの値が違うので注意
	var messages = document.getElementsByClassName("message");
	var focusedIndex = -1;

	var done = -1;

	this.isReloadableNow = function() {
		done = Date.now();
	};

	this.isValid = function(index) {
		return !!messages[index];
	};

	// jQuery 2系 jQuery.expr.filters.visibleより
	function isVisible(elem) {
		return (
			elem.offsetWidth > 0 ||
			elem.offsetHeight > 0 ||
			elem.getClientRects().length > 0
		);
	}
	function isHidden(elem) {
		return !isVisible(elem);
	}

	this.indexOfNextVisible = function(index, dir) {
		var el = messages[index];
		if (el && isHidden(el)) {
			return this.indexOfNextVisible(index + dir, dir);
		}
		return index;
	};

	var isUpdateScheduled = false;
	this.updateIfNeeded = function() {
		if (isUpdateScheduled) {
			return;
		}

		isUpdateScheduled = true;

		requestAnimationFrame(this.changeFocusedMessage);
	};
	this.changeFocusedMessage = function() {
		var m = messages[focusedIndex];
		var top = m.getBoundingClientRect().top;
		var x = window.pageXOffset;
		var y = window.pageYOffset;

		window.scrollTo(x, top + y - config.keyboardNavigationOffsetTop);

		var focused = document.getElementsByClassName("focused")[0];
		if (focused) {
			focused.classList.remove("focused");
		}
		m.classList.add("focused");

		isUpdateScheduled = false;
	};

	this.focus = function(dir) {
		var index = this.indexOfNextVisible(focusedIndex + dir, dir);
		if (this.isValid(index)) {
			focusedIndex = index;
			this.updateIfNeeded();
		} else if (dir === 1) {
			var now = Date.now();
			if (done >= 0 && now - done >= 500) {
				done = now;
				midokureload();
			}
		}
	};

	this.res = function() {
		var focused = document.querySelector(".focused");
		if (!focused) {
			return;
		}

		var selector;
		if (focused.classList.contains("original")) {
			selector = "font > a:first-child";
		} else {
			selector = ".res";
		}

		var res = focused.querySelector(selector);
		if (res) {
			if (typeof GM_openInTab === "function") {
				GM_openInTab(res.href, false);
			} else if (typeof GM === "object" && GM.openInTab) {
				GM.openInTab(res.href, false);
			} else {
				window.open(res.href);
			}
		}
	};

	this.handleEvent = function(e) {
		switch (e.type) {
			case "keypress":
				this.move(e);
				break;
			case "view is done":
				this.isReloadableNow();
				break;
			default:
				throw new Error("should not reach here: " + e.type);
		}
	};

	this.move = function(e) {
		var target = e.target;

		if (
			/^(?:INPUT|SELECT|TEXTAREA)$/.test(target.nodeName) ||
			target.isContentEditable
		) {
			return;
		}

		switch (e.which) {
			case 106: //j
				this.focus(1);
				break;
			case 107: //k
				this.focus(-1);
				break;
			case 114: //r
				this.res();
				break;
			default:
		}
	};
}
KeyboardNavigation.prototype.registerToDocument = function(doc) {
	if ( doc === void 0 ) doc = document;

	doc.addEventListener("keypress", this, false);
	doc.addEventListener("view is done", this, false);
};

function registerKeyboardNavigation(config) {
	if (config.keyboardNavigation) {
		var keyboardNavigation = new KeyboardNavigation(config, window);
		keyboardNavigation.registerToDocument();
	}
}

function setID() {
	var form = document.forms[0];
	if (form) {
		form.id = "form";
		var fonts = form.getElementsByTagName("font");
		var link = fonts[fonts.length - 3];
		if (link) {
			link.id = "link";
		}
	}
}

function setup(config) {
	applyCss(config);
	zero(config);
	addCommonEvents(config);
	setAccesskeyToV(config);
	setID();

	registerKeyboardNavigation(config);
}

var shouldQuitHere = function (title) {
	if ( title === void 0 ) title = getTitle();

	return title.endsWith(" 個人用環境設定");
};

var quitOrExecute = function (gotConfig) { return function (execute) { return gotConfig.then(function (config) {
		if (shouldQuitHere()) {
			return;
		}

		setup(config);

		return execute(config, function () { return document.dispatchEvent(new Event("view is done")); }
		);
	}); }; };

function main(location) {
	if ( location === void 0 ) location = window.location;

	var q = new Query(location.search, location.hostname);
	switch (q.get("m")) {
		case "f": //レス窓
			tweakResWindow();
			return;
		case "l": //トピック一覧
		case "c": //個人用設定
			return;
		case "g": //過去ログ
			if (!q.shouldHaveValidPosts()) {
				return;
			}
	}

	var gotConfig = Config.load();
	var execute = quitOrExecute(gotConfig);

	var main = canProcessStreamingly() ? streamMain : endMain;
	main(gotConfig, q, execute);
}

main();

}());