"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("<A")) {
text = text.replace(
// " </A>
//firefox %22 %3C\/A%3E
//chrome " <\/A>
//opera " <\/A>
/<A href="<a href="(.*)(?:%22|")"( target="link"(?: rel="noreferrer noopener")?)>\1"<\/a>\2><a href="\1(?:%3C\/A%3E|<\/A>|<\/A>)"\2>\1<\/A><\/a>/g,
'<a href="$1"$2>$1</a>'
);
}
post.text = text;
var reference = /\n\n<a href="h[^"]+&s=((?!0)\d+)&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(/> >.*\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> ");
lines = ("> " + lines + "\n")
.replace(/\n>[ \n\r\f\t]+\n/g, "\n")
.replace(/\n>[ \n\r\f\t]+\n$/, "\n");
return lines;
},
textCandidate: function() {
var text = this.text
.replace(/^> (.*\n?)|^.*\n?/mg, "$1")
.replace(/\n$/, "")
.replace(/^[ \n\r\f\t]*$/mg, "$&\n$&");
//TODO 引用と本文の間に一行開ける
//text = text.replace(/((?:> .*\n)+)(.+)/, "$1\n$2"); //replace(/^(?!> )/m, "\n$&");
return text;// + "\n\n";
},
textCandidateLooksValid: function() {
return this.getText().replace(/^> .*/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 (/^> /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 = /&#(?:\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("> > ");
}).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();