您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
あやしいわーるど@上海の投稿をツリーで表示できます。スタック表示の方にもいくつか機能を追加できます。
当前为
"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">\1"<\/a> target="link"><a href="\1(?:%3C\/A%3E|<\/A>|<\/A>)" target="link">\1<\/A><\/a>/g, '<a href="$1" target="link">$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) { return url.replace(/(?:https?|ftp|gopher|telnet|whois|news):\/\/[\x21-\x7e]+/ig, '<a href="$&" target="link">$&</a>'); }; Post.checkNG = function(ng, post) { var isNG = false; if (ng.word) { isNG = ng.word.test(post.text); } if (!isNG && ng.handle) { isNG = isNG || ng.handle.test(post.name); isNG = isNG || ng.handle.test(post.title); } post.isNG = isNG; return post; }; Post.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">([^<]+)<\/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; (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); button.classList.toggle("revert"); type += "VanishedMessage"; config[type](post.id); }).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.threadId, post.child.id, true); } 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")) { message.querySelector(".message-info").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 checkIfThreadIsVanished = config.useVanishThread || config.autovanishThread; 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 = checkIfThreadIsVanished && (thread.isNG || vanishedThreadIDs.indexOf(thread.getID()) > -1); if (isVanished && 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, opt_force) { if (!this.isNumber(childID)) { throw new TypeError('"' + childID + '"は自然数の文字列'); } if (typeof this.data[childID] !== "undefined") { return this.data[childID]; } if (opt_threadID && (this.tryHard || opt_force)) { 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(); 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 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(); if (q.g) { var chk = Object.keys(q).find(function(element) { return /^chk\d+\.dat/.test(element); }); 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); }; } } 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) { switch (q.m) { case "f": //レス窓 return ResWindow.ready.bind(ResWindow); case "l": //トピック一覧 case "c": //個人用設定 return doNothing; case 'g': //過去ログ if (!q.btn) { //スレッドボタン・レスボタンがなければ 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, loc) { loc = loc || location; return q.ff && q.s && q.m === 't' && /dat$/.test(loc.search); }, isFromKomachi: function(referrer, href) { return /^http:\/\/misao\.on\.arena\.ne\.jp\/c\/upload\.cgi/.test(referrer) && /^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; var messageLength = config.vanishedMessageIDs.length; if (threadLength || messageLength) { buttons.querySelector("#clearVanishedThreadIDs .count").textContent = threadLength; buttons.querySelector("#clearVanishedMessageIDs .count").textContent = messageLength; 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) { if (!document.body) { throw new Error("no 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 forms = document.forms; if (forms.length) { var fonts = forms[0].getElementsByTagName("font"); if (fonts.length >= 4) { var tmp = fonts[fonts.length - 4].textContent.match(/\d+/g) || []; views = tmp[3]; viewing = tmp[5]; } } 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 hidden">' + '非表示解除(' + '<a id="clearVanishedThreadIDs" href="javascript:;"><span class="count"></span>スレッド</a>/' + '<a id="clearVanishedMessageIDs" href="javascript:;"><span class="count"></span>投稿</a>' + ')' + '</span> ' + reload + '</span>' + '</footer>'; return containee; }, render: function(config) { 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); 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, origin) { gui.info.innerHTML = '<strong>' + q.ff + "以外の過去ログを検索中...</strong>"; var posts = Post.makePosts(origin); return fetcher[target](origin).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); 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(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); if (config.ng.isEnabled && config.utterlyVanishNGStack) { var insertFooter = Stack.tweakFooter(body); $.when(render).then(insertFooter); } 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(); } /*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 () { if (this.config.ng.isEnabled && this.config.utterlyVanishNGStack) { // Invoke right away Stack.tweakFooter(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.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://qwerty.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.8.1 // @run-at document-start // ==/UserScript== function parseQuery(search) { var obj = {}, kvs = search.substring(1).split("&"); kvs.forEach(function (kv) { obj[kv.split("=")[0]] = kv.split("=")[1]; }); return obj; } function main() { var q = parseQuery(location.search); var action = whatToDo(q); action(q); } main();