tree view for qwerty

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

Od 30.12.2016.. Pogledajte najnovija verzija.

"use strict";
// Generated by CoffeeScript 1.10.0

var Zepto, $;
Zepto = $ = {};

(function() {
  var Deferred, PENDING, REJECTED, RESOLVED, VERSION, _when, after, execute, flatten, has, installInto, isArguments, isPromise, wrap,
    slice = [].slice;

  VERSION = '3.1.0';

  PENDING = "pending";

  RESOLVED = "resolved";

  REJECTED = "rejected";

  has = function(obj, prop) {
    return obj != null ? obj.hasOwnProperty(prop) : void 0;
  };

  isArguments = function(obj) {
    return has(obj, 'length') && has(obj, 'callee');
  };

  isPromise = function(obj) {
    return has(obj, 'promise') && typeof (obj != null ? obj.promise : void 0) === 'function';
  };

  flatten = function(array) {
    if (isArguments(array)) {
      return flatten(Array.prototype.slice.call(array));
    }
    if (!Array.isArray(array)) {
      return [array];
    }
    return array.reduce(function(memo, value) {
      if (Array.isArray(value)) {
        return memo.concat(flatten(value));
      }
      memo.push(value);
      return memo;
    }, []);
  };

  after = function(times, func) {
    if (times <= 0) {
      return func();
    }
    return function() {
      if (--times < 1) {
        return func.apply(this, arguments);
      }
    };
  };

  wrap = function(func, wrapper) {
    return function() {
      var args;
      args = [func].concat(Array.prototype.slice.call(arguments, 0));
      return wrapper.apply(this, args);
    };
  };

  execute = function(callbacks, args, context) {
    var callback, i, len, ref, results;
    ref = flatten(callbacks);
    results = [];
    for (i = 0, len = ref.length; i < len; i++) {
      callback = ref[i];
      results.push(callback.call.apply(callback, [context].concat(slice.call(args))));
    }
    return results;
  };

  Deferred = function() {
    var candidate, close, closingArguments, doneCallbacks, failCallbacks, progressCallbacks, state;
    state = PENDING;
    doneCallbacks = [];
    failCallbacks = [];
    progressCallbacks = [];
    closingArguments = {
      'resolved': {},
      'rejected': {},
      'pending': {}
    };
    this.promise = function(candidate) {
      var pipe, storeCallbacks;
      candidate = candidate || {};
      candidate.state = function() {
        return state;
      };
      storeCallbacks = function(shouldExecuteImmediately, holder, holderState) {
        return function() {
          if (state === PENDING) {
            holder.push.apply(holder, flatten(arguments));
          }
          if (shouldExecuteImmediately()) {
            execute(arguments, closingArguments[holderState]);
          }
          return candidate;
        };
      };
      candidate.done = storeCallbacks((function() {
        return state === RESOLVED;
      }), doneCallbacks, RESOLVED);
      candidate.fail = storeCallbacks((function() {
        return state === REJECTED;
      }), failCallbacks, REJECTED);
      candidate.progress = storeCallbacks((function() {
        return state !== PENDING;
      }), progressCallbacks, PENDING);
      candidate.always = function() {
        var ref;
        return (ref = candidate.done.apply(candidate, arguments)).fail.apply(ref, arguments);
      };
      pipe = function(doneFilter, failFilter, progressFilter) {
        var filter, master;
        master = new Deferred();
        filter = function(source, funnel, callback) {
          if (!callback) {
            return candidate[source](master[funnel]);
          }
          return candidate[source](function() {
            var args, value;
            args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
            value = callback.apply(null, args);
            if (isPromise(value)) {
              return value.done(master.resolve).fail(master.reject).progress(master.notify);
            } else {
              return master[funnel](value);
            }
          });
        };
        filter('done', 'resolve', doneFilter);
        filter('fail', 'reject', failFilter);
        filter('progress', 'notify', progressFilter);
        return master;
      };
      candidate.pipe = pipe;
      candidate.then = pipe;
      if (candidate.promise == null) {
        candidate.promise = function() {
          return candidate;
        };
      }
      return candidate;
    };
    this.promise(this);
    candidate = this;
    close = function(finalState, callbacks, context) {
      return function() {
        if (state === PENDING) {
          state = finalState;
          closingArguments[finalState] = arguments;
          execute(callbacks, closingArguments[finalState], context);
          return candidate;
        }
        return this;
      };
    };
    this.resolve = close(RESOLVED, doneCallbacks);
    this.reject = close(REJECTED, failCallbacks);
    this.notify = close(PENDING, progressCallbacks);
    this.resolveWith = function(context, args) {
      return close(RESOLVED, doneCallbacks, context).apply(null, args);
    };
    this.rejectWith = function(context, args) {
      return close(REJECTED, failCallbacks, context).apply(null, args);
    };
    this.notifyWith = function(context, args) {
      return close(PENDING, progressCallbacks, context).apply(null, args);
    };
    return this;
  };

  _when = function() {
    var def, defs, finish, i, len, resolutionArgs, trigger;
    defs = Array.prototype.slice.apply(arguments);
    if (defs.length === 1) {
      if (isPromise(defs[0])) {
        return defs[0];
      } else {
        return (new Deferred()).resolve(defs[0]).promise();
      }
    }
    trigger = new Deferred();
    if (!defs.length) {
      return trigger.resolve().promise();
    }
    resolutionArgs = [];
    finish = after(defs.length, function() {
      return trigger.resolve.apply(trigger, resolutionArgs);
    });
    defs.forEach(function(def, index) {
      if (isPromise(def)) {
        return def.done(function() {
          var args;
          args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
          resolutionArgs[index] = args.length > 1 ? args : args[0];
          return finish();
        });
      } else {
        resolutionArgs[index] = def;
        return finish();
      }
    });
    for (i = 0, len = defs.length; i < len; i++) {
      def = defs[i];
      isPromise(def) && def.fail(trigger.reject);
    }
    return trigger.promise();
  };

  installInto = function(fw) {
    fw.Deferred = function() {
      return new Deferred();
    };
    fw.ajax = wrap(fw.ajax, function(ajax, options) {
      var createWrapper, def, promise, xhr;
      if (options == null) {
        options = {};
      }
      def = new Deferred();
      createWrapper = function(wrapped, finisher) {
        return wrap(wrapped, function() {
          var args, func;
          func = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
          if (func) {
            func.apply(null, args);
          }
          return finisher.apply(null, args);
        });
      };
      options.success = createWrapper(options.success, def.resolve);
      options.error = createWrapper(options.error, def.reject);
      xhr = ajax(options);
      promise = def.promise();
      promise.abort = function() {
        return xhr.abort();
      };
      return promise;
    });
    return fw.when = _when;
  };

  if (typeof exports !== 'undefined') {
    exports.Deferred = function() {
      return new Deferred();
    };
    exports.when = _when;
    exports.installInto = installInto;
  } else if (typeof define === 'function' && define.amd) {
    define(function() {
      if (typeof Zepto !== 'undefined') {
        return installInto(Zepto);
      } else {
        Deferred.when = _when;
        Deferred.installInto = installInto;
        return Deferred;
      }
    });
  } else if (typeof Zepto !== 'undefined') {
    installInto(Zepto);
  } else {
    this.Deferred = function() {
      return new Deferred();
    };
    this.Deferred.when = _when;
    this.Deferred.installInto = installInto;
  }

}).call(this);

if (!Object.assign) {
	Object.assign = function assign(target, source) { // eslint-disable-line no-unused-vars
		for (var index = 1, key, src; index < arguments.length; ++index) {
			src = arguments[index];

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

		return target;
	};
}

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;
	};
}


/*exported on*/
function on(el, event, selector, callback) {
	el.addEventListener(event, function(e) {
		if (e.target.closest(selector)) {
			callback(e);
		}
	});
}

var Env = (function() {
	var IS_EXTENSION = typeof chrome === 'object';
	return {
		IS_EXTENSION: IS_EXTENSION,
		IS_GM: typeof GM_setValue === "function",
		IS_FIREFOX: typeof InstallTrigger !== 'undefined',
	};
})();

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 Config = {};

Config.methods = function(storage) {
	function init() {
		this.ng = new NG(this);
	}
	var addID = function(config, type, id_or_ids, callback) {
		var target = "vanished" + type + "IDs";
		storage.get(target, function(IDs) {
			IDs = Array.isArray(IDs) ? IDs : [];

			var IDsToAdd = Array.isArray(id_or_ids) ? id_or_ids : [id_or_ids];
			IDsToAdd = IDsToAdd.filter(function(id) {
				return IDs.indexOf(id) === -1;
			});

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

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

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

	/** @param {String} id */
	var addVanishedThread = function(id) {
		var dfd = $.Deferred();
		addID(this, "Thread", id, dfd.resolve.bind(dfd));
		return dfd.promise();
	};
	var removeVanishedThread = function(id) {
		removeID(this, "Thread", id);
	};
	var clearVanishedThreadIDs = function() {
		clearIDs(this, "Thread");
	};
	var clearVanish = function() {
		clearVanishedMessageIDs();
		clearVanishedThreadIDs();
	};
	var clear = function() {
		storage.clear();
		Object.assign(this, Config.defaults);
	};
	var update = function(items) {
		Object.keys(items).filter(function(key) {
			return typeof Config.defaults[key] === "undefined";
		}).forEach(function(key) {
			delete items[key];
		});
		storage.setAll(items);
		Object.assign(this, items);
	};

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

	return {
		init: init,
		addVanishedMessage: addVanishedMessage,
		removeVanishedMessage: removeVanishedMessage,
		clearVanishedMessageIDs: clearVanishedMessageIDs,
		addVanishedThread: addVanishedThread,
		removeVanishedThread: removeVanishedThread,
		clearVanishedThreadIDs: clearVanishedThreadIDs,
		clearVanish: clearVanish,
		clear: clear,
		update: update,
		isTreeView: isTreeView,
	};
};

Config.defaults = Object.seal({
	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,
});

Config.storage = {};
Config.storage.chrome = {
	load: function() {
		var that = this;
		//eslint-disable-next-line no-undef
		return new Promise(function(resolve) {
			that.storage().get(Config.defaults, 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) {
		this.storage().set(items);
	},
	clear: function() {
		this.storage().clear();
	},
	get: function(key, fun) {
		this.storage().get(key, function(item) {
			fun(item[key]);
		});
	},
	storage: function() {
		return chrome.storage.local;
	},
};
Config.storage.gm = {
	load: function() {
		var config = Object.create(Config.defaults);
		var keys = Object.keys(Config.defaults);
		var i = keys.length;
		var key, value;
		while (i--) {
			key = keys[i];
			value = GM_getValue(key);
			if (value != null) {
				config[key] = JSON.parse(value);
			}
		}

		return $.Deferred().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) {
		for (var key in items) {
			this.set(key, items[key]);
		}
	},
	clear: function() {
		GM_listValues().forEach(GM_deleteValue);
	},
	get: function(key, fun) {
		fun(JSON.parse(GM_getValue(key, "null")));
	},
};

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

	return storage.load().then(function init(config) {
		Object.assign(config, Config.methods(storage));
		config.init();
		return config;
	});
};

Config.whichStorageToUse = function() {
	return Env.IS_GM ? Config.storage.gm : Config.storage.chrome;
};

if (!window.__karma__) {
	Config.instance = Config.load();
}

function ConfigController(item) {
	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[event].bind(this));
	}

	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 (Env.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) {
		e.preventDefault();

		var items = {}, config = this.item;
		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;
			}
		});
		config.update(items);

		this.info("保存しました。");
	},

	clear: function() {
		this.item.clear();
		this.restore();
		this.info("デフォルトに戻しました。");
	},

	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");
};

function identity(x) {
	return x;
}

function compose() {
	return Array.prototype.reduce.call(arguments, function(comp, fn) {
		return function() {
			return comp(fn.apply(null, arguments));
		};
	});
}

function curry2(fn) {
	return function(first) {
		return function(second) {
			return fn(first, second);
		};
	};
}

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

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}/, "?");

	var dfd = $.Deferred();
	var xhr = new XMLHttpRequest();

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

	return dfd.promise();
}

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 = DOM.nextElement("FONT");
	var nextB = DOM.nextElement("B");
	var nextBlockquote = DOM.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.makePosts = function(context) {
	var posts = [];
	var as = context.querySelectorAll("a[name]");
	var font = DOM.nextElement("FONT");
	var b = DOM.nextElement("B");
	var blockquote = DOM.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 text = pre.innerHTML.replace(/<\/?font[^>]*>/ig, "")
			.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>'
			);
		}

		post.text = 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) {
			post.parentId = reference[1];
			post.parentDate = reference[2];
			text = text.slice(0, reference.index);
		} else {
			post.parentId = null;
			post.parentDate = null;
		}

		var url = /\n\n<[^<]+<\/a>$/.exec(text);
		if (url) {
			text = text.slice(0, url.index);
		}
		if (!text.includes("<") && text.includes(":")) {
			post.text = Post.relinkify(text) +
				(url ? url[0] : "") + (reference ? reference[0] : "");
		}
	}
	if (posts.length >= 2 && (+posts[0].id) < (+posts[1].id)) {
		posts.reverse();
	}

	return posts;
};
Post.byID = function(l, r) {
	return +l.id - +r.id;
};
Post.relinkify1stMatching = function(_, p1) {
	return Post.relinkify(p1);
};
Post.relinkify = function(url, hostname) {
	hostname = hostname || location.hostname;
	var rel = "misao.on.arena.ne.jp" === hostname ? ' rel="noreferrer noopener"' : "";
	var replacer = '<a href="$&" target="link"' + rel + '>$&</a>';

	return url.replace(/(?:https?|ftp|gopher|telnet|whois|news):\/\/[\x21-\x7e]+/ig, replacer);
};
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.prototype = {
	id: "", // {string} /^\d+$/
	title: " ", // {string}
	name: " ", // {string}
	date: "", // {string}
	resUrl: "", // {string}
	threadUrl: "", // {string}
	threadId: "", // {string}
	posterUrl: "", // {string}
	// null: 親なし
	// undefined: 不明
	// string: ID 0から始まらない数字の文字列
	parentId: null, // {(string|null|undefined}}
	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>/ig,       //<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?/mg, "$1")
			.replace(/\n$/, "")
			.replace(/^[ \n\r\f\t]*$/mg, "$&\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; .*/mg, "").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();
	},
};

var ImaginaryPostPrototype = {
	__proto__: Post.prototype,
	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() {
		this.resUrl = this.child.resUrl.replace(/(\?|&)s=\d+/, "$1s=" + this.id);
	},
};
Object.defineProperty(ImaginaryPostPrototype, "text", {
	get: function() {
		return this.calculate("text");
	},
});

function MergedPost(id, child) {
	this.id = id;
	this.name = child.title.replace(/^>/, "");
	this.threadUrl = child.threadUrl;
	this.threadId = child.threadId;
	this.parentId = this.isOP() ? null : undefined;
	this.child = child;
	this.next = null;
	this.parent = null;

	this.setResUrl();
}
MergedPost.prototype = Object.create(ImaginaryPostPrototype, {
	date: {
		get: function() {
			return this.calculate("date");
		},
	},
});

function GhostPost(id, child) {
	this.id = id;
	this.child = child;
	child.parent = this;
	this.threadId = child.threadId;
	this.threadUrl = child.threadUrl;

	if (id) {
		this.setResUrl();
	}
}
GhostPost.prototype = Object.create(ImaginaryPostPrototype);
GhostPost.prototype.date = "?";

function Thread(config, postParent, id) {
	this.config = config;
	this.postParent = postParent;
	this.posts = [];
	this.id = id;
	this.postCount = 0;
	this.isNG = false;
}
Thread.connect = function(allPosts) {
	var lastChild = Object.create(null);

	return function connect(roots, post) {
		allPosts[post.id] = post;
		var parentId = post.parentId;
		// parentIdは自然数の文字列かnull
		if (parentId) {
			var parent = allPosts[parentId];
			if (parent) {
				var child = lastChild[parentId];
				if (child) {
					child.next = post;
				} else {
					parent.child = post;
				}
			} else {
				parent = new MergedPost(parentId, post);
				allPosts[parentId] = parent;
				roots.push(parent);
			}
			post.parent = parent;
			lastChild[parentId] = post;
		} else {
			roots.push(post);
		}
		return roots;
	};
};

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.find(id), level - 1);
};
Thread.setRejectLevel = function(vanishedMessageIDs, post, generation) {
	var rejectLevel = 0;

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

	post.rejectLevel = rejectLevel;

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

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

	if (next) {
		Thread.setRejectLevel(vanishedMessageIDs, next, generation);
	}
};

Thread.prototype = {
	makeRoots: function(parentIDs, allPosts, roots) {
		return roots.reduce(function(roots, post) {
			var root = post;

			if (post.mayHaveParent()) {
				// parentID = 自然数の文字列 || null || undefined
				var parentID = parentIDs[post.id];
				var parent = allPosts[parentID];

				if (parent) {
					root = null;
					post.parentId = parentID;
					post.parent = parent;
					post.next = parent.child;
					parent.child = post;
				} else if (parentID !== null) { // string || undefined
					post.parentId = parentID;
					var ghost = new GhostPost(parentID, post);
					if (parentID) { // string
						allPosts[parentID] = ghost;
					}

					root = ghost;
				}
			}

			if (root) {
				roots.push(root);
			}

			return roots;
		}, []);
	},
	computeRoots: function(threshold) {
		var parentIDs = this.posts
		.filter(function(post) {
			return post.parentId !== null;
		}).map(function(post) {
			return post.parentId;
		});

		var pParentIDHash = this.postParent.findAll(parentIDs, this.id);

		if (pParentIDHash.then) {
			return pParentIDHash.then(this.doComputeRoots.bind(this, threshold));
		} else {
			return this.doComputeRoots(threshold, pParentIDHash);
		}
	},
	doComputeRoots: function(threshold, parentIDHash) {
		var allPosts = Object.create(null);
		var roots = this.posts.reduceRight(Thread.connect(allPosts), []);
		roots.sort(Post.byID);

		roots = this.makeRoots(parentIDHash, allPosts, roots);

		this.postCount = this.posts.length;

		if (this.config.useVanishMessage) {
			var smallestMessageID = Object.keys(allPosts).sort(Post.byID)[0];

			if (smallestMessageID <= threshold) {
				roots = this.processVanish(roots);
			}

			if (this.config.utterlyVanishMessage) {
				roots = this.processUtterlyVanish(roots);
			}
		}

		return roots;
	},
	processVanish: function(roots) {
		var vanishedMessageIDs = this.config.vanishedMessageIDs;
		var computeRejectLevelForRoot = Thread.computeRejectLevelForRoot;
		var setRejectLevel = Thread.setRejectLevel;
		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) {
				setRejectLevel(vanishedMessageIDs, child, root.rejectLevel - 1);
			}
		}

		return roots;
	},
	processUtterlyVanish: function(roots) {
		var newRoots = [];
		var vanished = 0;
		function drop(post, isRoot) {
			var child = post.child;
			var next = post.next;
			var rejectLevel = post.rejectLevel;
			var isRead = post.isRead;

			if (child) {
				child = drop(child, false);
			}

			if (next) {
				next = drop(next, false);
			}

			if (!child && isRead) {
				return next;
			}

			if (rejectLevel && !isRead) {
				vanished++;
			}

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

			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);
		}

		this.postCount -= vanished;

		return newRoots.sort(Post.byID);
	},
	getDate: function() {
		return this.posts[0].date;
	},
	getNumber: function() {
		return this.postCount;
	},
	getID: function() {
		return this.id;
	},
	getURL: function() {
		return this.posts[0].threadUrl;
	},
};

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 = "";
		}
		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;
	},
};

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) {
		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.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 = curry2(Posts.truncate)(config);
		var checkCharacterEntity = curry2(Posts.checkCharacterEntity)(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.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;

		return DOM('<div class="border outer" style="left:' + left + 'em">' +
			'<div class="border inner" style="left:-' + left + 'em">' +
			'</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 + 'em';
	};
}
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(curry2(this.extension)(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 View = {
	"tree-mode-css": CSSView,
	"tree-mode-ascii": ASCIIView,
};

function Threads() {
	var el = document.createElement("div");
	el.id = "content";
	return el;
}
Threads.addEventListeners = function(config, el, postParent) {
	function click(selector, callback) {
		on(el, "click", selector, Threads.replace.bind(null, 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", Threads.showAsIs.bind(Threads, config));

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

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

	on(el, "click", ".vanish", function(e) {
		var button = e.target;
		var thread = button.closest(".thread");
		var id = thread.dataset.id;
		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;
	});

	on(el, "click", ".toggleTreeMode", Threads.toggleTreeMode.bind(null, config));

};

Threads.getTreeMode = function(node) {
	return node.closest(".tree-mode-css") ? "tree-mode-css" : "tree-mode-ascii";
};
Threads.replace = function(config, change, e) {
	e.preventDefault();

	var message = e.target.closest(".message, .showMessage");
	var parent = message.parentNode;
	var post = message.post;
	var mode = Threads.getTreeMode(message);
	var view = new View[mode]();
	var maker = view.messageMaker(config);
	var depth = parseInt(message.style.marginLeft, 10);

	change(post);

	var newMessage = maker(post, depth);

	parent.insertBefore(newMessage, message);
	parent.removeChild(message);
};

Threads.toggleMessage = function(config, postParent, e) {
	e.preventDefault();

	var button = e.target;
	var message = button.closest(".message");
	var post = message.post;
	var pTmp;

	if (button.classList.contains("revert")) {
		pTmp = Threads.doToggleMessageRevert();
	} else {
		pTmp = Threads.doToggleMessage(post, postParent);
	}

	$.when(pTmp).then(function(tmp) {
		var label = tmp.label;
		var func = tmp.func;
		var type = tmp.type;

		if (button.classList.contains("revert")) {
			post.rejectLevel = post.previousRejectLevel;
		} else {
			post.previousRejectLevel = post.rejectLevel;
			post.rejectLevel = 3;
		}

		var text = message.querySelector(".text");

		if (text.ownerDocument.defaultView.getComputedStyle(text, null).display === 'none') {
			text.style.display = null;
		} else {
			text.style.display = 'none';
		}

		button.textContent = label;
		button.classList.toggle("revert");
		type += "VanishedMessage";
		config[type](post.id);

		(function prepareToBeVanished(post, rejectLevel) {
			if (post === null || rejectLevel === 0) {
				return;
			}
			func(post, rejectLevel);
			prepareToBeVanished(post.child, rejectLevel - 1);
			prepareToBeVanished(post.next, rejectLevel);
		})(post.child, 2);
	}).fail(function(error) {
		button.insertAdjacentHTML("beforebegin", error);
		button.parentNode.removeChild(button);
	});
};
Threads.doToggleMessage = function(post, postParent) {
	var pid = post.id;
	if (post.isRead) {
		pid = postParent.find(post.child.id, post.threadId);
	}

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

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

		return id;
	}).then(function(id) {
		post.id = id;
		var func = function(post, rejectLevel) {
			if (post.rejectLevel < rejectLevel) {
				post.rejectLevel = rejectLevel;
			}

			var message = document.getElementById(post.id);

			if (!message.querySelector("strong.note")) {
				var target = message.matches(".message") ? ".message-info" : ".showMessageButton";
				message.querySelector(target).insertAdjacentHTML("beforebegin",
					'<strong class="note" style="color:red">' +
					'この投稿も非表示になります</strong>'
				);
			}
		};

		return {
			type: "add",
			func: func,
			label: "戻",
		};
	});
};
Threads.doToggleMessageRevert = function() {
	var func = function(post, rejectLevel) {
		if (post.rejectLevel === rejectLevel) {
			post.rejectLevel = 0;
			var message = document.getElementById(post.id);
			var strong = message.querySelector("strong.note");
			if (strong) {
				strong.parentNode.removeChild(strong);
			}
		}
	};

	return {
		type: "remove",
		func: func,
		label: "消",
	};
};

Threads.toggleTreeMode = function(config, 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 = new View[Threads.getTreeMode(thread)]();
	var roots = thread.roots;
	var messages = thread.querySelector(".messages");

	view.init(roots);
	var newMessages = view.render(config);

	thread.insertBefore(newMessages, messages);
	thread.removeChild(messages);
};

Threads.showAsIs = function(config, e) {
	function callback(post) {
		post.showAsIs = !post.showAsIs;
	}

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

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

Threads.showThreads = function(config, el, threads) {
	var mode = config.treeMode;
	var view = new View[mode]();
	var utterlyVanishNGThread = config.utterlyVanishNGThread;
	var vanishedThreadIDs = config.vanishedThreadIDs;
	var threshold = +config.vanishedMessageIDs[0];
	var toggleTreeMode = mode === "tree-mode-css" && config.toggleTreeMode ? ' <a href="javascript:;" class="toggleTreeMode">●</a>' : '';
	var emptyVanishButtons = { true: "", false: "" };
	var vanishButtons = {
		true: ' <a href="javascript:;" class="vanish">戻</a>',
		false: ' <a href="javascript:;" class="vanish">消</a>',
	};

	function show(thread, pending, isVanished, roots) {
		var number = thread.getNumber();

		if (!number) {
			if (pending) {
				el.removeChild(pending);
			}

			return;
		}

		var vanish;
		if (config.useVanishThread || (isVanished && config.autovanishThread)) {
			vanish = vanishButtons;
		} else {
			vanish = emptyVanishButtons;
		}

		var url = '<a href="' + thread.getURL() + '" target="link">◆</a>';
		var html = '<pre data-id="' + thread.getID() + '" class="thread ' + mode + '">' +
			'<div class="thread-header">' +
			url +
			' 更新日:' + thread.getDate() + ' 記事数:' + number +
			toggleTreeMode +
			vanish[isVanished] +
			' ' + url +
			'</div><span class="messages"></span></pre>';
		var dthread = DOM(html);

		if (isVanished) {
			dthread.classList.add("NGThread");
		}

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

		if (pending) {
			el.replaceChild(dthread, pending);
		} else {
			el.appendChild(dthread);
		}
	}

	function showThread(thread) {
		var isVanished = config.useVanishThread && vanishedThreadIDs.indexOf(thread.getID()) > -1;
		var isToBeVanished = config.autovanishThread && thread.isNG;
		var vanish = isVanished || isToBeVanished;

		if (vanish && utterlyVanishNGThread) {
			return;
		}

		var pending;
		var pRoots = thread.computeRoots(threshold);

		if (pRoots.then) {
			var url = '<a href="' + thread.getURL() + '" target="link">◆</a>';
			var pendingHTML = '<pre class="pending thread "' + mode + '>' +
			'<div class="thread-header">' +
			url +
			' 更新日:' + thread.getDate() +
			' ' + url +
			'</div>親子関係取得中</pre>';
			pending = DOM(pendingHTML);
			el.appendChild(pending);

			return pRoots.then(show.bind(null, thread, pending, isVanished));
		} else {
			return show(thread, pending, isVanished, pRoots);
		}
	}
	return loop(showThread, threads);
};

function PostParent(config, q) {
	this.useStorage = this.isDOMStorageAvailable("localStorage");
	var tryHard = config.vanishMessageAggressive && !q.m && this.useStorage;

	if (tryHard) {
		var storage = this.sessionStorage();
		var first = !storage.getItem("qtv-session");
		if (first) {
			storage.setItem("qtv-session", true);
		}
		tryHard = tryHard && first;
	}

	this.config = config;
	this.tryHard = tryHard;
}

PostParent.prototype = {
	useStorage: false,
	nullStorage: function() {
		return {
			getItem: function() { return null; },
			setItem: doNothing,
		};
	},
	sessionStorage: function() {
		return sessionStorage;
	},
	getStorage: function() {
		if (this.useStorage) {
			return this.config.useVanishMessage ? localStorage : sessionStorage;
		} else {
			return this.nullStorage();
		}
	},
	load: function() {
		this.data = JSON.parse(this.getStorage().getItem("postParent")) || {};
	},
	save: function(data) {
		this.getStorage().setItem("postParent", JSON.stringify(data));
	},
	saveAsync: function(data) {
		setTimeout(this.save.bind(this), 0, data);
	},
	setWhenToCleanUp: function(view) {
		view.then(function() {
			setTimeout(this.cleanUp.bind(this), 10 * 1000);
		}.bind(this));
	},
	update: function(posts) {
		if (!posts.length) {
			return;
		}
		var changed = false;

		this.load();

		var data = this.data;

		for (var i = 0, len = posts.length; i < len; i++) {
			var post = posts[i];
			var id = post.id;
			var parentID = post.parentId;

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

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

			data[id] = parentID;
			changed = true;
		}
		if (changed) {
			this.saveAsync(data);
		}
	},

	limit: function() {
		if (this.config.useVanishMessage) {
			if (this.config.vanishMessageAggressive) {
				return { upper: 3500, lower: 3300 };
			} else {
				return { upper: 1500, lower: 1300 };
			}
		} else {
			return { upper: 500, lower: 300 };
		}
	},
	cleanUp: function() {
		if (!this.data) {
			return;
		}
		var ids = Object.keys(this.data);
		var length = ids.length;
		var limit = this.limit();
		if (length > limit.upper) {
			ids = ids.map(function(id) {
				return +id;
			}).sort(function(l, r) {
				return r - l;
			});

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

			var saveData = {};
			var i = limit.lower;
			while (i--) {
				saveData[ids[i]] = this.data[ids[i]];
			}
			this.saveAsync(saveData);
		}
	},
	isNumber: function(number) {
		return /^(?!0)\d+$/.test(number);
	},
	updateThread: function(threadID) {
		return ajax({data: { m: 't', s: threadID}})
			.then(DOM.wrapWithDiv)
			.then(Post.makePosts)
			.then(this.update.bind(this))
			.then(function() {
				return this.data;
			}.bind(this));
	},
	head: function(array) {
		return array[0];
	},
	find: function(childID, opt_threadID) {
		if (!this.isNumber(childID)) {
			throw new TypeError('"' + childID + '"は自然数の文字列');
		}

		if (opt_threadID && typeof this.data[childID] === "undefined") {
			return this.findAll([childID], opt_threadID, true)
				.then(this.head);
		}

		return this.data[childID];
	},

	notContainedIn: function(id) {
		return typeof this[id] === "undefined";
	},
	needsToFetch: function(childIDs, threadID, force) {
		return (this.tryHard || force) &&
			this.useStorage &&
			this.isNumber(threadID) && // 要らないかもしれない
			childIDs.some(this.notContainedIn, this.data);
	},
	from: function(p) { return this[p]; },
	collect: function(ids, id) {
		ids[id] = this.data[id];

		return ids;
	},
	findAll: function(childIDs, threadID, opt_force) {
		var hash = Object.create(null);

		if (!this.needsToFetch(childIDs, threadID, opt_force)) {
			return childIDs.reduce(this.collect.bind(this), hash);
		}

		if (!this.updateThreadMemoized) {
			this.updateThreadMemoized = memoize(this.updateThread.bind(this));
		}

		return this.updateThreadMemoized(threadID)
			.then(childIDs.map.bind(childIDs, this.from));
	},
	isDOMStorageAvailable: function(type, win) {
		win = 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;
		}
	},
};

function Thumbnail(config, head) {
	this.config = config;
	head = head || document.head;

	var animationChecker = memoize(Thumbnail.checkAnimation);

	this.preloads = [];
	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.linkSupportsPreload = DOMTokenListSupports(document.createElement("link").relList, "preload");

	// ポップアップを消した時、カーソルがサムネイルの上にある
	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 complete = $.Deferred();

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

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

		setTimeout(function() {
			if (complete.state() === "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, document.body, image);
		popup.addEventListeners();
		popup.waitAndOpen();
	};

	this.image = {
		sw: [{
			name: "misao",
			prefix: "http://misao.on.arena.ne.jp/c/",
			urls: function(href) {
				return {
					original: href,
					small: this.small(href),
					animation: this.animation(href),
				};
			},
			small: function(href) {
				var src = href;

				if (!/^http:\/\/misao\.on\.arena\.ne\.jp\/c\/up\/misao\d+\.\w+$/.test(href)) {
					return src;
				}

				return src.replace(/up\//, "up/pixy_");
			},
			animation: function(href) {
				if (!config.linkAnimation) {
					return;
				}

				var misao = /^http:\/\/misao\.on\.arena\.ne\.jp\/c\/up\/(misao0*\d+)\.(?:png|jpg)$/.exec(href);

				if (misao) {
					var misaoID = misao[1];
					var animationURL = 'http://misao.on.arena.ne.jp/c/upload.cgi?m=A&id=' + (/(?!0)\d+/).exec(misaoID)[0];

					animationChecker(href).then(function(isAnimation) {
						setTimeout(function() {
							if (!document.body) {
								throw new Error("no body");
							}
							var animations = document.getElementsByClassName(misaoID);

							Array.prototype.slice.call(animations).forEach(function(animation) {
								if (isAnimation) {
									var unsure = animation.getElementsByClassName("unsure")[0];
									if (unsure) {
										animation.removeChild(unsure);
									}
								} else {
									animation.parentNode.removeChild(animation);
								}
							});
						});
					});

					return {id: misaoID, href: animationURL};
				}
			},
		}, {
			name: "betanya",
			prefix: "http://komachi.betanya.com/uploader/stored/",
			urls: function(href) {
				return {
					original: href,
					small: href,
				};
			},
		}],

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

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

				href = parts[0];

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

	this.thumbnailLink = function(href) {
		var thumbnail;

		if (/\.(?:jpe?g|png|gif|bmp)$/i.test(href)) {
			thumbnail = this.loopSites(this.image.sw, href, startsWith, null);
		}

		if (!thumbnail && config.popupAny) {
			thumbnail = this.loopSites(this.image.otherSites, href, test, test);
		}

		return thumbnail;
	};

	this.loopSites = function(sites, href, testPrefix, testSuffix) {
		for (var i = 0; i < sites.length; ++i) {
			var thumbnail = this.thumbnailThis(sites[i], href, testPrefix, testSuffix);

			if (thumbnail) {
				return thumbnail;
			}
		}
	};

	this.thumbnailThis = function(site, href, testPrefix, testSuffix) {
		var suffix = site.suffix;
		var prefix = site.prefix;

		if (testSuffix && testSuffix(href, suffix)) {
			return;
		}

		if (testPrefix && testPrefix(href, prefix)) {
			return;
		}

		return this.construct(site.urls(href));
	};

	function startsWith(href, string) {
		return string && !href.startsWith(string);
	}

	function test(href, test) {
		return test && !test.test(href);
	}

	this.preload = function(original) {
		if (this.preloads.indexOf(original) !== -1) {
			return;
		}

		var link = document.createElement("link");
		link.rel = "preload";
		link.as = "image";
		link.href = original;
		head.appendChild(link);

		this.preloads.push(original);
	};

	this.small = function(original, small) {
		// if (!original) {
		// 	throw new Error();
		// }
		if (!small) {
			return small;
		}

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

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

		if (this.linkSupportsPreload) {
			this.preload(original);

			return small;
		}

		return original;
	};

	this.a = function(original) {
		return '<a href="' + original + '" target="link" class="thumbnail">';
	};

	this.thumbnail = function(original, small) {
		var a = this.a(original);

		if (small) {
			return a + '<img referrerpolicy="no-referrer" class="thumbnail-img" src="' + small + '"></a>';
		} else {
			return '[' + a + '■</a>]';
		}
	};

	this.construct = function(data) {
		var original = data.original;
		var small = this.small(original, data.small);

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

		var animation = data.animation;
		if (animation) {
			thumbnail += '<span class="animation ' + animation.id + '">[<a href="' + animation.href + '" target="link">A</a><span class="unsure">?</span>]</span>';
		}

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

		return thumbnail;
	};

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

	this.register = function(container) {
		var as = container.querySelectorAll('a[target]');
		var has = false;
		var i;
		for (i = as.length - 1; i >= 0; i--) {
			var a = as[i];
			var href = a.href;
			var thumbnail = this.thumbnailLink(href);
			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, false);
			}
		}
	};
}

Thumbnail.checkAnimation = function(imgURL) {
	var dfd = $.Deferred();
	var url = imgURL.replace(/\w+$/, "pch");
	if (Env.IS_GM) {
		GM_xmlhttpRequest({
			url: url,
			method: "HEAD",
			onload: function(response) {
				dfd.resolve(response.status === 200);
			},
		});
	} else if (Env.IS_EXTENSION) {
		ajax({
			url: url,
			type: "HEAD",
		}).then(function() {
			dfd.resolve(true);
		}, function() {
			dfd.resolve(false);
		});
	}

	return dfd.promise();
};

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

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

		if (type === "keydown" && !/^Esc(?:ape)?$/.test(e.key) && e.keyIdentifier !== "U+001B") { // 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 Fetch(q, now) {
	this.now = now || Date.now();

	var chk = this.getChk(q);

	if (chk) {
		this.today = chk.match(/\d+/)[0];
		this.hasOP = function() { return true; };

		this.data = function(ff) {
			var data = Object.assign({}, q);
			delete data[chk];
			data["chk" + ff] = "checked";

			return data;
		};
	} else {
		this.data = function(ff) {
			return {
				__proto__: q,
				ff: ff,
			};
		};
		this.today = +q.ff.match(/^(\d{8})\.dat$/)[1];
		var query = 'a[name="' + q.s + '"]';
		this.hasOP = function(container) {
			return container.querySelector(query);
		};
	}

	this.thisLog = this.today + ".dat";
}
Fetch.prototype.getChk = function(q) {
	return Object.keys(q).find(function(key) {
		return /^chk\d+\.dat$/.test(key);
	});
};
Fetch.prototype.dates = function() {
	var ONE_DAY = 24 * 60 * 60 * 1000;
	var afters = [];
	var befores = [];
	var fill = function(n) {
		return n < 10 ? "0" + n : n;
	};

	for (var i = 0; i < 7; i++) {
		var back = new Date(this.now - ONE_DAY * i);
		var year = back.getFullYear();
		var month = fill(back.getMonth() + 1);
		var date = fill(back.getDate());
		var day = "" + year + month + date;
		if (day > this.today) {
			afters.push(day);
		} else if (day < this.today) {
			befores.push(day);
		}
	}

	return {afters: afters, befores: befores};
};
Fetch.prototype.both = function(container) {
	var dates = this.dates();

	var after = this.concurrent(dates.afters);
	var before = this.sequence(dates.befores, container);

	return $.when(after, before).then(function(afters, befores) {
		return {afters: afters, befores: befores};
	});
};
Fetch.prototype.after = function() {
	var dates = this.dates();
	var after = this.concurrent(dates.afters);

	return after.then(function(afters) {
		return {afters: afters, befores: []};
	});
};
Fetch.prototype.fetch = function(date) {
	var ff = date + ".dat";

	return ajax({url: "bbs.cgi", data: this.data(ff)})
	.then(DOM.wrapWithDiv)
	.then(function(div) {
		div.ff = ff;
		return div;
	});
};
Fetch.prototype.sequence = function(dates, container) {
	var divs = [];
	var fetch = this.fetch.bind(this);
	var hasOP = this.hasOP;
	var sequence = dates.reduce(function(sequence, date) {
		return sequence.then(function(done) {
			if (done) {
				return done;
			}

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

				return hasOP(div);
			});
		});
	}, $.Deferred().resolve(hasOP(container)));

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

	return $.when.apply(null, all).then(function() {
		return Array.apply(null, arguments);
	});
};

function doNothing() {}

function ready(continuation, readyState) {
	readyState = readyState || document.readyState;
	if (/complete|loaded|interactive/.test(readyState) && document.body) {
		continuation();
	} else {
		document.addEventListener('DOMContentLoaded', continuation, {once: true});
	}
}

var ResWindow = {
	ready: function(readyState) {
		ready(this.tweak, readyState);
	},
	tweak: function() {
		var v = document.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 eventHandlers = {
	openConfig: function(config, body, e, chrome_) {
		e.preventDefault();

		chrome_ = chrome_ || (typeof chrome === "object" ? chrome : undefined);

		if (chrome_ && chrome_.runtime.id) {
			chrome_.runtime.sendMessage({type: "openConfig"});
		} else if (!document.getElementById("config")) {
			body.insertBefore(new ConfigController(config).el, body.firstChild);
			window.scrollTo(0, 0);
		}
	},
	tweakLink: function(config, e) {
		var a = e.target;

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

		if (a.target) {
			a.rel += " noreferrer noopener";
		}
	},
	reload: function(e, loc) {
		loc = loc || location;

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

		var reload = document.getElementById("qtv-reload");
		if (!reload) {
			reload = DOM('<input type="submit" id="qtv-reload" name="reload" value="1" style="display:none;">');
			document.forms[0].appendChild(reload);
		}

		reload.click();
	},
	midokureload: function(e, loc) {
		loc = loc || location;

		if (document.getElementById("form")) {
			document.getElementsByName("midokureload")[0].click();
		} else {
			loc.reload();
		}
	},
	clearVanishedIDs: function(config, method, e) {
		e.preventDefault();
		config[method]();
		e.target.firstElementChild.innerHTML = "0";
	},
};

var App = {
	gm: {
		main: function(q) {
			ready(function() {
				Config.instance.then(function(config) {
					App.execute(config, function() {
						App.gm.doMain(config, q, document.body);
					});
				});
			});
		},
		doMain: function(config, q, body) {
			var view = this.view(config);
			var done = view(config, q, body);

			App.common(config, body, done);
		},
		view: function(config) {
			return config.isTreeView() ? tree : stack;
		},
	},
	execute: function(config, execute) {
		if (App.checkResWindow(document)) {
			if (config.closeResWindow) {
				App.closeResWindow();
			}
		} else if (App.checkSetupWindow(document)) {
			// Do nothing
		} else {
			// opera12は実行中にも描画されるから先に挿入しておく
			// サポートをやめたらApp.commonに戻す
			App.injectCSS(config);
			execute();
		}
	},
	checkResWindow: function(document) {
		return document.title.endsWith(" 書き込み完了");
	},
	checkSetupWindow: function(document) {
		return document.title.endsWith(" 個人用環境設定");
	},
	closeResWindow: function() {
		if (Env.IS_EXTENSION) {
			chrome.runtime.sendMessage({type: "closeTab"});
		} else {
			window.open("", "_parent");
			window.close();
		}
	},
	common: function(config, body, view) {
		App.zero(config);
		App.addCommonEvents(config, body);
		App.setAccesskeyToV(config);
		App.keyboardNavigation(config, view);
		App.setID();
	},
	keyboardNavigation: function(config, view, KN) {
		KN = KN || KeyboardNavigation;
		if (config.keyboardNavigation) {
			document.addEventListener("keypress", new KN(config, view, window), false);
		}
	},
	zero: function(config) {
		if (config.zero) {
			var d = document.getElementsByName("d")[0];
			if (d && d.value !== "0") {
				d.value = "0";
			}
		}
	},
	addCommonEvents: function(config, body) {
		on(body, "click", "#openConfig", eventHandlers.openConfig.bind(eventHandlers, config, body));
		on(body, "click", "a", eventHandlers.tweakLink.bind(null, config));
	},
	setAccesskeyToV: function(config) {
		var accessKey = config.accesskeyV;
		if (accessKey.length === 1) {
			var v = document.getElementsByName("v")[0];
			if (v) {
				v.accessKey = accessKey;
			}
		}
	},
	setID: function() {
		var forms = document.forms;
		if (forms.length) {
			var form = forms[0];
			form.id = "form";
			var fonts = form.getElementsByTagName("font");
			if (fonts.length >= 3) {
				fonts[fonts.length - 3].id = "link";
			}
		}
	},
	// injectCSSは下の方で定義
};

/* exported whatToDo */
function whatToDo(q, hostname) {
	switch (q.m) {
		case "f": //レス窓
			return ResWindow.ready.bind(ResWindow);
		case "l": //トピック一覧
		case "c": //個人用設定
			return doNothing;
		case 'g': //過去ログ
			if (!q.sv && !(q.e && hostname === "misao.on.arena.ne.jp")) {
				return doNothing;
			}
	}

	return window.Promise && window.MutationObserver ? App.chrome.main : App.gm.main;
}

var Tree = {
	execute: function(config, q, gui, container) {
		return this.collect(config, q, gui, container).then(function(posts) {
			Tree.tweakFooter(container, posts);
			return Tree.show(config, q, gui, posts).then(function() {
				return posts;
			});
		});
	},
	collect: function(config, q, gui, container) {
		var ng = config.ng;
		var makePosts = this.howToMakePosts(q, gui);
		var posts = makePosts(container);
		if (!posts.then) {
			posts = $.Deferred().resolve(posts);
		}

		if (!ng.isEnabled) {
			return posts;
		}

		return posts.then(this.processNG.bind(this, config));
	},
	processNG: function(config, posts) {
		this.checkNG(config.ng, posts);

		if (!config.autovanishThread && config.utterlyVanishNGStack) {
			return this.excludeNG(posts);
		}

		return posts;
	},
	howToMakePosts: function(q, gui) {
		if (this.needsToSearchLog(q)) {
			return this.fetchFromRemote.bind(null, q, gui, new Fetch(q), "both");
		} else if (this.needsToTweakLink(q)) {
			return compose(this.tweakURL, Post.makePosts);
		} else if (this.isFromKomachi(document.referrer, this.href())) {
			return this.fetchFromRemote.bind(null, q, gui, new Fetch(q), "after");
		} else {
			return Post.makePosts;
		}
	},
	//通常モードからスレッドボタンを押した場合
	isThreadSearchWithin1000: function(q) {
		return q.m === 't' && !q.ff && /^\d+$/.test(q.s);
	},
	//検索窓→投稿者検索→★の結果の場合
	isPosterSearchInLog: function(q) {
		return q.s && q.ff && q.m === 's';
	},
	needsToTweakLink: function(q) {
		return this.isThreadSearchWithin1000(q) || this.isPosterSearchInLog(q);
	},
	needsToSearchLog: function(q) {
		return q.m === "t" && /^\d+\.dat$/.test(q.ff) && /^\d+$/.test(q.s);
	},
	isFromKomachi: function(referrer, href) {
		return /^http:\/\/misao\.on\.arena\.ne\.jp\/c\/upload\.cgi/.test(referrer) &&
		(/^http:\/\/misao\.on\.arena\.ne\.jp\/cgi-bin\/bbs\.cgi\?chk\d+\.dat=checked&kwd=http:\/\/misao\.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(href) ||
		/^http:\/\/qwerty\.on\.arena\.ne\.jp\/cgi-bin\/bbs\.cgi\?chk\d+\.dat=checked&kwd=http:\/\/misao\.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(href));
	},
	checkNG: function(ng, posts) {
		for (var i = 0; i < posts.length; ++i) {
			Post.checkNG(ng, posts[i]);
		}
	},
	excludeNG: function(posts) {
		return posts.filter(function(post) {
			return !post.isNG;
		});
	},
	show: function(config, q, gui, posts) {
		var postParent = Tree.makePostParent(config, q);

		gui.info.textContent = " - スレッド構築中";
		Threads.addEventListeners(config, gui.content, postParent);

		Tree.suggestLinkToLog(q, Tree.href(), gui.info, posts);
		Tree.setPostCount(gui.postcount, posts.length);

		postParent.update(posts);

		var threads = Tree.threads(config, postParent, posts);
		Tree.sortThreads(config, threads);

		this.autovanishThread(config, gui.footer, threads);

		var done = Threads.showThreads(config, gui.content, threads);
		done.then(this.clearInfo.bind(this, gui.info));

		postParent.setWhenToCleanUp(done);

		return done;
	},
	autovanishThread: function(config, footer, threads) {
		if (!config.autovanishThread) {
			return;
		}

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

		if (!ids.length) {
			return;
		}

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

		return config.addVanishedThread(ids).then(function() {
			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");
			}
		});
	},
	clearInfo: function(info) {
		info.textContent = "";
	},
	makePostParent: function(config, q) {
		return new PostParent(config, q);
	},
	href: function() {
		return location.href;
	},
	template: function(config, body) {
		var reload = '<input type="button" value="リロード" class="mattari">';
		if (!config.zero) {
			reload = reload.replace('mattari', 'reload');
			reload += '<input type="button" value="未読" class="mattari">';
		}

		var accesskey = config.accesskeyReload;
		if (!/^\w$/.test(accesskey)) {
			accesskey = "R";
		}
		var views = "";
		var viewing = "";
		var hr = body.getElementsByTagName("hr")[0];
		if (hr) {
			var font = hr.previousElementSibling;
			if (font && font.tagName === "FONT") {
				var tmp = font.textContent.match(/\d+/g) || [];
				views = tmp[3];
				viewing = tmp[5];
			}
		}

		var vanishedThreadIDLength = config.vanishedThreadIDs.length;
		var vanishedMessageIDLength = config.vanishedMessageIDs.length;
		var hasVanishings = vanishedThreadIDLength || vanishedMessageIDLength;

		var containee =
			'<header id="header">' +
				'<span class="left">' +
					reload.replace('class="mattari"', '$& accesskey="' + accesskey + '"') + ' ' +
					views + ' / ' + viewing + '名 ' +
					'<span id="postcount"></span>' +
				'</span>' +
				'<span>' +
					'<a href="javascript:;" id="openConfig">設定</a> ' +
					'<a href="#link">link</a> ' +
					'<a href="#form" class="goToForm">投稿フォーム</a> ' +
					reload +
				'</span>' +
			'</header>' +
			'<hr>' +
			'<footer id="footer">' +
				'<span class="left">' +
					reload +
				'</span>' +
				'<span>' +
					'<span class="clearVanishedButtons' + (hasVanishings ? '' : ' hidden') + '">' +
						'非表示解除(' +
							'<a id="clearVanishedThreadIDs" href="javascript:;"><span class="count">' + vanishedThreadIDLength + '</span>スレッド</a>/' +
							'<a id="clearVanishedMessageIDs" href="javascript:;"><span class="count">' + vanishedMessageIDLength + '</span>投稿</a>' +
						')' +
					'</span> ' +
					reload +
				'</span>' +
			'</footer>';
		return containee;
	},
	render: function(config, body) {
		var el = document.createElement("div");
		el.id = "container";
		var click = on.bind(null, el, "click");

		//event
		click(".reload", eventHandlers.reload);
		click(".mattari", eventHandlers.midokureload);
		click('.goToForm', Tree.focusV);
		['Message', 'Thread'].forEach(function(type) {
			var id = 'clearVanished' + type + 'IDs';
			click('#' + id, eventHandlers.clearVanishedIDs.bind(null, config, id));
		});

		el.innerHTML = Tree.template(config, body);

		var header = el.firstChild;
		var firstChildOfHeader = header.firstChild;
		var postcount = firstChildOfHeader.lastChild;

		var info = new Info();
		info.textContent = "ダウンロード中...";
		firstChildOfHeader.appendChild(info);

		var threads = new Threads();
		el.insertBefore(threads, header.nextSibling);

		return {
			container: el,
			info: info,
			content: threads,
			postcount: postcount,
			footer: el.lastChild,
		};
	},
	deleteOriginal: function(config, body) {
		if (config.deleteOriginal) {
			Tree.originalRange(body).deleteContents();
		}
	},
	originalRange: function(container) {
		function startNode(container, firstAnchor) {
			var h1 = container.querySelector("h1");
			if (h1 && h1.compareDocumentPosition(firstAnchor) & Node.DOCUMENT_POSITION_FOLLOWING) {
				return h1;
			} else {
				return firstAnchor;
			}
		}

		var range = document.createRange();

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

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

		var start = startNode(container, firstAnchor);

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

		return range;
	},
	kuzuhaEnd: function(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;
	},
	focusV: function() {
		setTimeout(function() {
			document.getElementsByName("v")[0].focus();
		}, 50);
	},
	tweakURL: function(posts) {
		posts.forEach(function(post) {
			var date = post.date.match(/\d+/g);
			var ff = '&ff=' + date[0] + date[1] + date[2] + '.dat';
			post.threadUrl += ff; //post.threadUrl.replace(/&ac=1$/, "")必要?
			if (post.resUrl) {
				post.resUrl += ff;
			}
			if (post.posterUrl) {
				post.posterUrl += ff;
			}
		});

		return posts;
	},
	fetchFromRemote: function(q, gui, fetcher, target, container) {
		gui.info.innerHTML = '<strong>' + fetcher.thisLog + "以外の過去ログを検索中...</strong>";
		var posts = Post.makePosts(container);
		return fetcher[target](container).then(function(doms) {
			var makeArray = function(posts, div) {
				var newPosts = Post.makePosts(div);
				return posts.concat(newPosts);
			};
			return [].concat(
				doms.afters.reduce(makeArray, []),
				posts,
				doms.befores.reduce(makeArray, [])
			);
		});
	},
	threads: function(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, id);
				threads.push(thread);
			}
			thread.posts.push(post);

			if (post.isNG) {
				thread.isNG = true;
			}
		});

		return threads;
	},
	sortThreads: function(config, threads) {
		if (config.threadOrder === "ascending") {
			threads.reverse();
		}
	},
	whenToSuggestLinkToLog: function(q, posts) {
		return q.m === 't' && !q.ff && /^\d+$/.test(q.s) && posts.every(function(post) {
			return !post.isOP();
		});
	},
	suggestLinkToLog: function(q, href, info, posts) {
		if (!posts) {
			throw new Error("no posts");
		}

		if (Tree.whenToSuggestLinkToLog(q, 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";
			info.insertAdjacentHTML("afterend", ' <a id="hint" href="' + url + '">過去ログを検索する</a>');
		}
	},
	setPostCount: function(postcount, postLength) {
		var message;
		if (postLength) {
			message = postLength + "件取得";
		} else {
			message = "未読メッセージはありません。";
		}
		postcount.textContent = message;
	},
	tweakFooter: function(container, posts) {
		var i = container.querySelector("p i");
		if (!i) {
			return;
		}
		var numPostsInfo = i.parentNode;
		var buttons = DOM.nextElement("TABLE")(numPostsInfo);
		var end;

		if (buttons && posts.length) {
			end = numPostsInfo;
		} else {
			end = DOM.nextElement("HR")(numPostsInfo);
		}

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

		range.deleteContents();
	},
};

function tree(config, q, body) {
	var gui = Tree.render(config, body);

	if (Env.IS_FIREFOX) {
		var html = body.parentNode;
		html.removeChild(body);
	}

	var done = Tree.execute(config, q, gui, body);

	Tree.deleteOriginal(config, body);

	body.insertBefore(gui.container, body.firstChild);

	if (Env.IS_FIREFOX) {
		html.appendChild(body);
	}

	return done;
}

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 = DOM.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 Stack = {
	common: function(config) {
		if (!document.body) {
			throw new Error("no body");
		}

		Stack.addEventListener(config);
		Stack.configButton(config);
		Stack.accesskey(config);
	},
	accesskey: function(config) {
		var midoku = document.getElementsByName("midokureload")[0];
		if (midoku) {
			midoku.accessKey = config.accesskeyReload;
		}
	},
	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};
	},
	addEventListener: function(config, body) {
		body = body || document.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) {
		eventHandlers.clearVanishedIDs(config, "clearVanishedThreadIDs", e);
	},
	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);
		};
	},
	whenToComplement: function(q) {
		return q.ff && q.m === 't' && /dat$/.test(location.search);
	},
	complementLog: function(config, q, body, Fetch_) {
		if (Stack.whenToComplement(q)) {
			var gui = Stack.container();
			gui.info.innerHTML = '<strong>' + q.ff + "以外の過去ログを検索中...</strong>";
			body.insertBefore(gui.container, body.firstChild);

			return new (Fetch_ || Fetch)(q).both(body)
			.then(Stack.addExtraLog.bind(null, config, q, gui))
			.then(function() {
				gui.info.textContent = "";
			});
		}
	},
	addExtraLog: function(config, q, gui, doms) {
		var wrap = (function() {
			var wrap = Stack.wrapA(config);
			return function(f) {
				Array.prototype.forEach.call(f.querySelectorAll("a[name]"), wrap);
				return f;
			};
		})();
		var f = document.createDocumentFragment();
		function format(f, div) {
			var numberOfPosts = div.querySelectorAll("a[name]").length;

			f.appendChild(DOM('<h1>' + div.ff + '</h1>'));

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

			return f;
		}

		if (doms.befores.length) {
			f.appendChild(DOM('<hr>'));
		}

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

		f.appendChild(DOM('<hr>'));
		f.appendChild(DOM('<h1>' + q.ff + '</h1>'));

		document.body.insertBefore(f, gui.container.nextSibling);

		f = doms.afters.reduceRight(format, f);
		document.body.appendChild(f);
	},
	configButton: function(config) {
		var setup = document.getElementsByName("setup")[0];
		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);
		}
	},
	wrapStack: function(config) {
		var view = new StackView(config);
		return view.wrapOne.bind(view);
	},
	render: function(config, body) {
		if (config.keyboardNavigation || config.thumbnail || config.ng.isEnabled || config.useVanishThread) {
			var anchors = body.querySelectorAll("body > a[name]");
			if (Env.IS_FIREFOX) {
				var html = body.parentNode;

				html.removeChild(body);

				anchors.forEach(Stack.wrapA(config));

				html.appendChild(body);
			} else {
				return loop(Stack.wrapA(config), anchors);
			}
		}
	},
	wrapA: function(config) {
		return Stack.wrapStack(config);
	},
	wrapOne: function(config) {
		var wrap = Stack.wrapStack(config);
		return function(f) {
			wrap(f.querySelector("a[name]"));
			return f;
		};
	},
	tweakFooter: function(config, container, opt_done) {
		if (this.needsToTweakFooter(config)) {
			var insertFooter = this.doTweakFooter(container);

			$.when(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 = DOM.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);
		};
	},
};

function stack(config, q, body) {
	Stack.common(config);

	var complement = Stack.complementLog(config, q, body);

	var render = Stack.render(config, body);

	Stack.tweakFooter(config, body, render);

	return $.when(complement, render);
}

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

	return el;
}

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

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

	var done = 0;
	view.then(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;

		var focused = document.getElementsByClassName("focused")[0];
		if (focused) {
			focused.classList.remove("focused");
		}
		m.classList.add("focused");
		window.scrollTo(x, top + y - config.keyboardNavigationOffsetTop);

		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 && now - done >= 500) {
				done = now;
				eventHandlers.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 {
				window.open(res.href);
			}
		}
	};

	this.handleEvent = 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:
		}
	};
}

///////////////////////////////////////////////////////////////////////////////

App.injectCSS = function(config) {
	var css = '\
		.text {\
			white-space: pre-wrap;\
		}\
		.text, .extra {\
			min-width: 20em;\
		}\
		.text_tree-mode-css, .extra_tree-mode-css {\
			margin-left: 1em;\
		}\
		.env {\
			font-family: initial;\
			font-size: smaller;\
		}\
		.message_tree-mode-css, .border, .showMessage_tree-mode-css {\
			position: relative;\
		}\
\
		.thread-header {\
			background: #447733 none repeat scroll 0 0;\
			border-color: #669955 #225533 #225533 #669955;\
			border-style: solid;\
			border-width: 1px 2px 2px 1px;\
			font-size: 80%;\
			font-family: normal;\
			margin-top: 0.8em;\
			padding: 0;\
			width: 100%;\
		}\
\
		.message-header {\
			white-space: nowrap;\
		}\
		.message-header_tree-mode-css {\
			font-size: 85%;\
			font-family: normal;\
		}\
		.message-info {\
			font-family: monospace;\
			color: #87CE99;\
		}\
\
		.read, .quote {\
			color: #CCB;\
		}\
		header, footer {\
			display: flex;\
			font-size: 90%;\
		}\
		header .left, footer .left {\
			margin-right: auto;\
		}\
		.thread {\
			margin-bottom: 1em;\
		}\
		.modified {\
			color: #FBB\
		}\
		.note, .characterEntityOn, .env {\
			font-style: italic;\
		}\
		.a-tree {\
			font-style: initial;\
		}\
\
		.inner {\
/*			border: 2px solid yellow; */\
			top: -1em;\
		}\
		.outer {\
			border-left: 1px solid #ADB;\
			top: 1em;\
		}\
		.thumbnail-img {\
			width: 80px;\
			max-height: 400px;\
			image-orientation: from-image;\
		}\
		#image-view {\
			position: fixed;\
			top: 50%;\
			left: 50%;\
			transform: translate(-50%, -50%);\
			background: #004040;\
			color: white;\
			font-weight: bold;\
			font-style: italic;\
			margin: 0;\
			image-orientation: from-image;\
		}\
		.image-view-img {\
			background-color: white;\
		}\
\
		.focused {\
			border: 2px solid yellow;\
		}\
		.truncation, .NGThread .messages, .hidden {\
			display: none;\
		}\
		.spacing {\
			padding-bottom: 1em;\
		}\
	';
	GM_addStyle(css + config.css);
};

function GM_addStyle(css) {
	var doc = document;
	var head = doc.getElementsByTagName("head")[0];
	var style = null;
	if (head) {
		style = doc.createElement("style");
		style.textContent = css;
		head.appendChild(style);
	}
}

var div_ = document.createElement("div");
function DOM(html) {
	var div = div_.cloneNode(false);
	div.innerHTML = html;
	return div.firstChild;
}
DOM._next = function(type) {
	type = "next" + type;
	return function(nodeName) {
		return function next(node) {
			node = node[type];

			while (node) {
				if (node.nodeName === nodeName) {
					return node;
				}

				node = node[type];
			}
		};
	};
};
DOM.nextElement = DOM._next("ElementSibling");
DOM.nextSibling = DOM._next("Sibling");
DOM.wrapWithDiv = function wrapWithDiv(html) {
	var div = document.createElement("div");
	div.innerHTML = html;
	return div;
};

function loop(func, array) {
	var i = 0, length = array.length, dfd = $.Deferred();
	var done = [];
	(function loop() {
		var t = Date.now();
		do {
			if (i === length) {
				$.when.apply(null, done).then(dfd.resolve.bind(dfd));
				return;
			}
			done.push(func(array[i++]));
		} while (Date.now() - t < 20);
		setTimeout(loop, 0);
	})();
	return dfd.promise();
}

/*exported parseQuery*/
function parseQuery(search) {
	var obj = {}, kvs = search.substring(1).split("&");
	kvs.forEach(function (kv) {
		obj[kv.split("=")[0]] = kv.split("=")[1];
	});
	return obj;
}

/*eslint-env es6 */
function delayPromise(ms) {
	return new Promise(function(resolve) {
		setTimeout(resolve, ms);
	});
}

function DelayNotice(config, loaded, body, timeout) {
	var this$1 = this;

	this.config = config;
	this.loaded = loaded;
	this.body = body;
	this.timeout = delayPromise(timeout || 700);

	config.then(function () {
		this$1.configIsLoaded = true;
	});
}
DelayNotice.prototype.start = function() {
	var this$1 = this;

	return Promise.race([this.timeout, this.loaded])
	.then(function () { return this$1.body; })
	.then(this.popup.bind(this));
};
DelayNotice.prototype.popup = function(body) {
	if (this.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 = '設定読込待ち';

	body.insertBefore(notice, body.firstChild);

	this.config.then(function() {
		body.removeChild(notice);
	});

	this.loaded.then(function() {
		notice.textContent = "設定読込待ちかレンダリング中";
	});
};

App.chrome = {
	main: function main(q) {
		var config = Config.instance;
		var body = App.chrome.waitFor.body(document);
		var loaded = App.chrome.waitFor.loaded(window);
		var observer = new Observer(document, loaded);
		var handler = new Handler(config, q, body, loaded);
		var notice = new DelayNotice(config, loaded, body, 700);

		handler.start();
		notice.start();

		observer.listener = handler;

		observer.observe();
	},
	waitFor: {
		body: function body(document) {
			return new Promise(function(resolve) {
				if (document.body) {
					resolve(document.body);
					return;
				}

				var observer = new MutationObserver(function(mutations) {
					mutations.forEach(function(mutation) {
						Array.prototype.forEach.call(mutation.addedNodes, function(node) {
							if (node.nodeName === "BODY") {
								observer.disconnect();
								resolve(node);
							}
						});
					});
				});

				observer.observe(document.documentElement, {childList: true});
			});
		},
		loaded: function loaded(window) {
			return new Promise(function (resolve) {
				window.addEventListener("DOMContentLoaded", function resolver(e) {
					window.removeEventListener(e.type, resolver, true);
					resolve();
				}, true);
			});
		},
	},
};

var AbstractStreamView = function AbstractStreamView(args) {
	Object.assign(this, args);
};
AbstractStreamView.prototype.init = function init () {
		var this$1 = this;

	this.done = this.loaded.then(function () { return this$1.finish(); });
};
var StreamStackView = (function (AbstractStreamView) {
	function StreamStackView(args) {
		AbstractStreamView.call(this, args);

		this.wrapper = Stack.wrapOne(this.config);
		this.r = document.createRange();
	}

	if ( AbstractStreamView ) StreamStackView.__proto__ = AbstractStreamView;
	StreamStackView.prototype = Object.create( AbstractStreamView && AbstractStreamView.prototype );
	StreamStackView.prototype.constructor = StreamStackView;
	StreamStackView.prototype.init = function init () {
		AbstractStreamView.prototype.init.call(this);
		var ms = this.ms;
		Stack.common(this.config);

		if (ms.hasChildNodes()) {
			var range = this.r;
			range.selectNodeContents(ms);
			this.buffer.appendChild(range.extractContents());
		}

		this.render();
		ms.hidden = false;
	};
	StreamStackView.prototype.finish = function finish () {
		Stack.tweakFooter(this.config, this.buffer);

		this.body.appendChild(this.buffer);

		return Stack.complementLog(this.config, this.q, this.body);
	};
	StreamStackView.prototype.render = function render () {
		var ref = this;
		var r = ref.r;
		var wrapper = ref.wrapper;
		var ms = ref.ms;
		var buffer = ref.buffer;
		var firstComment = ref.firstComment;
		var comment;

		while ((comment = firstComment(buffer))) {
			r.setStartBefore(buffer.firstChild);
			r.setEndAfter(comment);
			wrapper(buffer);
			ms.appendChild(r.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;
	};

	return StreamStackView;
}(AbstractStreamView));

var StreamTreeView = (function (AbstractStreamView) {
	function StreamTreeView () {
		AbstractStreamView.apply(this, arguments);
	}

	if ( AbstractStreamView ) StreamTreeView.__proto__ = AbstractStreamView;
	StreamTreeView.prototype = Object.create( AbstractStreamView && AbstractStreamView.prototype );
	StreamTreeView.prototype.constructor = StreamTreeView;

	StreamTreeView.prototype.init = function init () {
		AbstractStreamView.prototype.init.call(this);
		this.gui = Tree.render(this.config, this.body);
		this.body.insertBefore(this.gui.container, this.body.firstChild);
	};
	StreamTreeView.prototype.finish = function finish () {
		var ref = this;
		var config = ref.config;
		var gui = ref.gui;
		var buffer = ref.buffer;
		var q = ref.q;
		var ms = ref.ms;
		var container = buffer.hasChildNodes() ? buffer : ms;

		var mDone = Tree.execute(config, q, gui, container);

		this.prepareToggleOriginal(container, mDone);

		this.appendLeftovers(container);

		return mDone;
	};
	StreamTreeView.prototype.appendLeftovers = function appendLeftovers (container) {
		var leftovers;
		if (container === this.ms) {
			var r = document.createRange();
			r.selectNodeContents(container);
			leftovers = r.extractContents();
		} else if (container === this.buffer) {
			leftovers = container;
		}

		if (leftovers) {
			this.body.appendChild(leftovers);
		}
	};
	StreamTreeView.prototype.prepareToggleOriginal = function prepareToggleOriginal (container, done) {
		var range = Tree.originalRange(container);

		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;
		}

		this.appendToggleOriginalButton();
		this.putInOriginal(original);
	};
	StreamTreeView.prototype.putInOriginal = function putInOriginal (original) {
		this.ms.appendChild(original);
	};
	StreamTreeView.prototype.appendToggleOriginalButton = function appendToggleOriginalButton () {
		var ms = this.ms;
		var range = document.createRange();
		var fragment = range.createContextualFragment('<div style="text-align:center"><a class="toggleOriginal" href="javascript:;">元の投稿の表示する(時間がかかることがあります)</a></div><hr>');
		var button = fragment.firstChild.firstChild;
		button.addEventListener("click", {ms: ms, handleEvent: this.toggleOriginal});

		ms.parentNode.insertBefore(fragment, ms);
	};
	StreamTreeView.prototype.toggleOriginal = function toggleOriginal (e, win) {
		win = win || window;
		e.preventDefault();
		e.stopPropagation();
		this.ms.hidden = !this.ms.hidden;
		win.scrollTo(win.pageXOffset, e.target.getBoundingClientRect().top + win.pageYOffset);
	};

	return StreamTreeView;
}(AbstractStreamView));

function Handler(pConfig, q, pBody, pLoaded) {
	var this$1 = this;

	var ms = document.createElement("main");
	ms.id = "qtv-stack";
	ms.hidden = true;

	var buffer = document.createDocumentFragment();
	var bufferRange = document.createRange();

	var view;

	this.onProgress = function (lastChild) {
		if (lastChild === ms) {
			return;
		}

		bufferRange.setEndAfter(lastChild);
		buffer.appendChild(bufferRange.extractContents());

		if (view && "render" in view) {
			view.render();
		}
	};

	this.stash = function() {
		ms.appendChild(buffer);
	};

	this.onHR = function (hr) {
		bufferRange.setStartAfter(hr);
	};

	var pAnchor = new Promise(function (resolve) {
		this$1.onFirstAnchor = function(a) {
			resolve();

			a.parentNode.insertBefore(ms, a);

			bufferRange.setEndBefore(ms);
			ms.parentNode.insertBefore(bufferRange.extractContents(), ms);

			bufferRange.setStartAfter(ms);
		};
	});

	this.stashForNow = function (config) {
		if (!config) {
			this$1.stash();
		}
	};

	this.createView = function(config, body) {
		var args = {config: config, body: body, q: q, ms: ms, buffer: buffer, loaded: pLoaded};
		if (config.isTreeView()) {
			return new StreamTreeView(args);
		} else {
			return new StreamStackView(args);
		}
	};

	this.initView = function (config, body) {
		view = this$1.createView(config, body);

		view.init();

		App.common(config, body, view.done);
	};

	this.execute = function (ref) {
		var config = ref[0];
		var body = ref[1];

		App.execute(config, this$1.initView.bind(this$1, config, body));
	};

	this.start = function () {
		Promise.race([pConfig, pLoaded]).then(this$1.stashForNow);

		Promise.all([
			pConfig,
			pBody,
			Promise.race([pAnchor, pLoaded]) ]).then(this$1.execute);
	};
}

function Observer(htmlDocument, loaded) {
	var this$1 = this;

	this.listener = null;

	this.firstAnchor = null;
	this.hr = null;

	var find = Array.prototype.find;
	var fireEvent = function (event, arg) {
		return this$1.listener[event](arg);
	};
	var isAnchor = function(node) {
		return node.name &&
			node.nodeName === "A" &&
			node.attributes.length === 1 &&
			/^\d+$/.test(node.name) &&
			!node.textContent;
	};

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

	var findElement = function (name, predicate, mutation) {
		if (mutation.target.nodeName === "BODY") {
			var element = find.call(mutation.addedNodes, predicate);
			if (element) {
				this$1[name] = element;
				return element;
			}
		}
	};

	var findAnchor = findElement.bind(null, "firstAnchor", isAnchor);
	var findHR = findElement.bind(null, "hr", isHR);

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

		if (!this$1.hr) {
			mutations.some(findHR);

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

		if (!this$1.firstAnchor) {
			mutations.some(findAnchor);

			if (this$1.firstAnchor) {
				fireEvent("onFirstAnchor", this$1.firstAnchor);
			}
		}

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

		observer.start();
	};

	var observer = new MutationObserver(this.processRecords);

	observer.start = function() {
		if (htmlDocument.body) {
			this.observe(htmlDocument.body, { childList: true });
		} else {
			this.observe(htmlDocument.documentElement, { childList: true, subtree: true });
		}
	};

	loaded.then(function () {
		observer.start = doNothing;
		var records = observer.takeRecords();
		if (records.length) {
			console.error(records.length);
			this$1.processRecords(records, observer);
		}
		observer.disconnect();
	});

	this.observe = function () {
		observer.start();
	};
}


// ==UserScript==
// @name        tree view for qwerty
// @name:ja     くわツリービュー
// @namespace   strangeworld
// @description あやしいわーるど@上海の投稿をツリーで表示できます。スタック表示の方にもいくつか機能を追加できます。
// @match       http://misao.on.arena.ne.jp/cgi-bin/bbs.cgi*
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_deleteValue
// @grant       GM_listValues
// @grant       GM_xmlhttpRequest
// @grant       GM_openInTab
// @version     10.9.1
// @run-at      document-start
// ==/UserScript==

/*global parseQuery*/
function main() {
	var q = parseQuery(location.search);
	var action = whatToDo(q, location.hostname);

	action(q);
}

main();