您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
あやしいわーるど@みさおの投稿をツリーで表示できます。スタック表示の方にもいくつか機能を追加できます。
当前为
"use strict"; // zousan - A Lightning Fast, Yet Very Small Promise A+ Compliant Implementation // https://github.com/bluejava/zousan // Author: Glenn Crownover <[email protected]> (http://www.bluejava.com) // Version 2.3.3 // License: MIT /* jshint asi: true, browser: true */ /* global setImmediate, console */ (function(global){ "use strict"; var STATE_PENDING, // These are the three possible states (PENDING remains undefined - as intended) STATE_FULFILLED = "fulfilled", // a promise can be in. The state is stored STATE_REJECTED = "rejected", // in this.state as read-only _undefined, // let the obfiscator compress these down _undefinedString = "undefined"; // by assigning them to variables (debatable "optimization") // See http://www.bluejava.com/4NS/Speed-up-your-Websites-with-a-Faster-setTimeout-using-soon // This is a very fast "asynchronous" flow control - i.e. it yields the thread and executes later, // but not much later. It is far faster and lighter than using setTimeout(fn,0) for yielding threads. // Its also faster than other setImmediate shims, as it uses Mutation Observer and "mainlines" successive // calls internally. // WARNING: This does not yield to the browser UI loop, so by using this repeatedly // you can starve the UI and be unresponsive to the user. // This is an even FASTER version of https://gist.github.com/bluejava/9b9542d1da2a164d0456 that gives up // passing context and arguments, in exchange for a 25x speed increase. (Use anon function to pass context/args) var soon = (function() { var fq = [], // function queue; fqStart = 0, // avoid using shift() by maintaining a start pointer - and remove items in chunks of 1024 (bufferSize) bufferSize = 1024 function callQueue() { while(fq.length - fqStart) // this approach allows new yields to pile on during the execution of these { try { fq[fqStart]() } // no context or args.. catch(err) { if(global.console) global.console.error(err) } fq[fqStart++] = _undefined // increase start pointer and dereference function just called if(fqStart == bufferSize) { fq.splice(0,bufferSize); fqStart = 0; } } } // run the callQueue function asyncrhonously, as fast as possible var cqYield = (function() { // This is the fastest way browsers have to yield processing if(typeof MutationObserver !== _undefinedString) { // first, create a div not attached to DOM to "observe" var dd = document.createElement("div"); var mo = new MutationObserver(callQueue); mo.observe(dd, { attributes: true }); return function() { dd.setAttribute("a",0); } // trigger callback to } // if No MutationObserver - this is the next best thing - handles Node and MSIE if(typeof setImmediate !== _undefinedString) return function() { setImmediate(callQueue) } // final fallback - shouldn't be used for much except very old browsers return function() { setTimeout(callQueue,0) } })(); // this is the function that will be assigned to soon // it takes the function to call and examines all arguments return function(fn) { // push the function and any remaining arguments along with context fq.push(fn); if((fq.length - fqStart) == 1) // upon adding our first entry, kick off the callback cqYield(); }; })(); // -------- BEGIN our main "class" definition here ------------- function Zousan(func) { // this.state = STATE_PENDING; // Inital state (PENDING is undefined, so no need to actually have this assignment) //this.c = []; // clients added while pending. <Since 1.0.2 this is lazy instantiation> // If a function was specified, call it back with the resolve/reject functions bound to this context if(func) { var me = this; func( function(arg) { me.resolve(arg) }, // the resolve function bound to this context. function(arg) { me.reject(arg) }) // the reject function bound to this context } } Zousan.prototype = { // Add 6 functions to our prototype: "resolve", "reject", "then", "catch", "finally" and "timeout" resolve: function(value) { if(this.state !== STATE_PENDING) return; if(value === this) return this.reject(new TypeError("Attempt to resolve promise with self")); var me = this; // preserve this if(value && (typeof value === "function" || typeof value === "object")) { try { var first = true; // first time through? var then = value.then; if(typeof then === "function") { // and call the value.then (which is now in "then") with value as the context and the resolve/reject functions per thenable spec then.call(value, function(ra) { if(first) { first=false; me.resolve(ra);} }, function(rr) { if(first) { first=false; me.reject(rr); } }); return; } } catch(e) { if(first) this.reject(e); return; } } this.state = STATE_FULFILLED; this.v = value; if(me.c) soon(function() { for(var n=0, l=me.c.length;n<l;n++) resolveClient(me.c[n],value); }); }, reject: function(reason) { if(this.state !== STATE_PENDING) return; this.state = STATE_REJECTED; this.v = reason; var clients = this.c; if(clients) soon(function() { for(var n=0, l=clients.length;n<l;n++) rejectClient(clients[n],reason); }); else if(!Zousan.suppressUncaughtRejectionError && global.console) global.console.log("You upset Zousan. Please catch rejections: ", reason,reason ? reason.stack : null) }, then: function(onF,onR) { var p = new Zousan(); var client = {y:onF,n:onR,p:p}; if(this.state === STATE_PENDING) { // we are pending, so client must wait - so push client to end of this.c array (create if necessary for efficiency) if(this.c) this.c.push(client); else this.c = [client]; } else // if state was NOT pending, then we can just immediately (soon) call the resolve/reject handler { var s = this.state, a = this.v; soon(function() { // we are not pending, so yield script and resolve/reject as needed if(s === STATE_FULFILLED) resolveClient(client,a); else rejectClient(client,a); }); } return p; }, "catch": function(cfn) { return this.then(null,cfn); }, // convenience method "finally": function(cfn) { return this.then(cfn,cfn); }, // convenience method // new for 1.2 - this returns a new promise that times out if original promise does not resolve/reject before the time specified. // Note: this has no effect on the original promise - which may still resolve/reject at a later time. "timeout" : function(ms,timeoutMsg) { timeoutMsg = timeoutMsg || "Timeout" var me = this; return new Zousan(function(resolve,reject) { setTimeout(function() { reject(Error(timeoutMsg)); // This will fail silently if promise already resolved or rejected }, ms); me.then(function(v) { resolve(v) }, // This will fail silently if promise already timed out function(er) { reject(er) }); // This will fail silently if promise already timed out }) } }; // END of prototype function list function resolveClient(c,arg) { if(typeof c.y === "function") { try { var yret = c.y.call(_undefined,arg); c.p.resolve(yret); } catch(err) { c.p.reject(err) } } else c.p.resolve(arg); // pass this along... } function rejectClient(c,reason) { if(typeof c.n === "function") { try { var yret = c.n.call(_undefined,reason); c.p.resolve(yret); } catch(err) { c.p.reject(err) } } else c.p.reject(reason); // pass this along... } // "Class" functions follow (utility functions that live on the Zousan function object itself) Zousan.resolve = function(val) { var z = new Zousan(); z.resolve(val); return z; } Zousan.reject = function(err) { var z = new Zousan(); z.reject(err); return z; } Zousan.all = function(pa) { var results = [ ], rc = 0, retP = new Zousan(); // results and resolved count function rp(p,i) { if(!p || typeof p.then !== "function") p = Zousan.resolve(p); p.then( function(yv) { results[i] = yv; rc++; if(rc == pa.length) retP.resolve(results); }, function(nv) { retP.reject(nv); } ); } for(var x=0;x<pa.length;x++) rp(pa[x],x); // For zero length arrays, resolve immediately if(!pa.length) retP.resolve(results); return retP; } // If this appears to be a commonJS environment, assign Zousan as the module export if(typeof module != _undefinedString && module.exports) // jshint ignore:line module.exports = Zousan; // jshint ignore:line // If this appears to be an AMD environment, define Zousan as the module export if(global.define && global.define.amd) global.define([], function() { return Zousan }); // Make Zousan a global variable in all environments global.Zousan = Zousan; // make soon accessable from Zousan Zousan.soon = soon; })(typeof global != "undefined" ? global : this); // jshint ignore:line if (!window.Promise && typeof it == "undefined") { window.Promise = window.Zousan; if (!Promise.race) { Promise.race = function(promises) { return new Promise(function(resolve, reject) { promises.forEach(function(promise) { promise = promise.then ? promise : Promise.resolve(promise); promise.then(resolve).catch(reject); }); }); }; } } if (!Object.assign) { Object.assign = function assign(target, source) { // 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)) { if (callback.handleEvent) { callback.handleEvent.call(callback, e); } else { callback(e); } } }); } /*exported Env*/ var Env = (function() { var IS_EXTENSION = typeof chrome === 'object'; return { IS_EXTENSION: IS_EXTENSION, 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); } function 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 config = this; return new Promise(function(resolve) { addID(config, "Thread", id, resolve); }); }; 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.prototype); }; var update = function(items) { Object.keys(items).filter(function(key) { return typeof Config.prototype[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.prototype = { treeMode: "tree-mode-ascii", toggleTreeMode: false, thumbnail: true, thumbnailPopup: true, popupAny: false, popupMaxWidth: "", popupMaxHeight: "", popupBestFit: true, threadOrder: "ascending", NGHandle: "", NGWord: "", useNG: true, NGCheckMode: false, spacingBetweenMessages: false, useVanishThread: true, vanishedThreadIDs: [], //扱い注意 autovanishThread: false, utterlyVanishNGThread: false, useVanishMessage: false, vanishedMessageIDs: [], vanishMessageAggressive: false, utterlyVanishMessage: false, utterlyVanishNGStack: false, deleteOriginal: true, zero: true, accesskeyReload: "R", accesskeyV: "", keyboardNavigation: false, keyboardNavigationOffsetTop: "200", viewMode: "t", css: "", linkAnimation: true, shouki: true, closeResWindow: false, maxLine: "", openLinkInNewTab: false, characterEntity: true, }; 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.prototype, 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() { return new Promise(function(resolve) { var config = Object.create(Config.prototype); var keys = Object.keys(Config.prototype); var i = keys.length; var key, value; while (i--) { key = keys[i]; value = GM_getValue(key); if (value != null) { config[key] = JSON.parse(value); } } resolve(config); }); }, remove: function(key) { GM_deleteValue(key); }, set: function(key, value, callback) { GM_setValue(key, JSON.stringify(value)); if (callback) { callback(); } }, setAll: function(items) { 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 typeof GM_getValue === "function" ? Config.storage.gm : Config.storage.chrome; }; if (!window.__karma__) { Config.instance = Config.load(); } /*global on, Env*/ 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"); }; /*global on, Env */ 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 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}/, "?"); return new Promise(function(resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open(type, url); xhr.overrideMimeType('text/html; charset=windows-31j'); xhr.onload = function() { if (xhr.status === 200) { resolve(xhr.response); } else { reject(new Error(xhr.statusText)); } }; xhr.onerror = function() { reject(new Error("Network Error")); }; xhr.send(); }); } 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, hostname) { hostname = hostname || location.hostname; var posts = []; var as = context.querySelectorAll("a[name]"); var font = DOM.nextElement("FONT"); var b = DOM.nextElement("B"); var blockquote = DOM.nextElement("BLOCKQUOTE"); for (var i = 0, len = as.length; i < len; i++) { var a = as[i]; var post = new Post(a.name); posts.push(post); var header = font(a); post.title = header.firstChild.innerHTML; var named = b(header); post.name = named.innerHTML; var info = font(named); post.date = info.firstChild.nodeValue.trim().slice(4);//「投稿日:」削除 post.resUrl = info.firstElementChild.href; post.threadUrl = info.lastElementChild.href; post.threadId = /&s=([^&]+)/.exec(post.threadUrl)[1]; if (info.childElementCount === 3) { post.posterUrl = info.firstElementChild.nextElementSibling.href; } else { post.posterUrl = null; } var body = blockquote(info); var pre = body.firstElementChild; var env = font(pre); if (env) { post.env = env.firstChild.innerHTML; // font > i > env } var text = pre.innerHTML.replace(/<\/?font[^>]*>/ig, "") .replace(/\r\n?/g, "\n") .slice(0, -1); if (text.includes("<A")) { text = text.replace( // " </A> //firefox %22 %3C\/A%3E //chrome " <\/A> //opera " <\/A> /<A href="<a href="(.*)(?:%22|")"( target="link"(?: rel="noreferrer noopener")?)>\1"<\/a>\2><a href="\1(?:%3C\/A%3E|<\/A>|<\/A>)"\2>\1<\/A><\/a>/g, '<a href="$1"$2>$1</a>' ); } post.text = text; var reference = /\n\n<a href="h[^"]+&s=((?!0)\d+)&r=[^"]+">参考:([^<]+)<\/a>$/.exec(text); if (!reference) { reference = /\n\n<a href="#((?!0)\d+)">参考:([^<]+)<\/a>$/.exec(text); } if (reference) { post.setParentId(reference[1]); post.setParentDate(reference[2]); text = text.slice(0, reference.index); } var url = /\n\n<[^<]+<\/a>$/.exec(text); if (url) { text = text.slice(0, url.index); } if (!text.includes("<") && text.includes(":")) { post.text = Post.relinkify(text, /misao/.test(hostname) ? ' rel="noreferrer noopener"' : "") + (url ? url[0] : "") + (reference ? reference[0] : ""); } } Post.sortByTime(posts); return posts; }; // 新しいのが先 Post.sortByTime = function(posts) { if (posts.length >= 2 && (+posts[0].id) < (+posts[1].id)) { posts.reverse(); } }; Post.byID = function(l, r) { return +l.id - +r.id; }; Post.relinkify1stMatching = function(_, rel, url) { rel = rel || ""; return Post.relinkify(url, rel); }; Post.relinkify = function(url, rel) { var replacer = '<a href="$&" target="link"' + rel + '>$&</a>'; return url.replace(/(?:https?|ftp|gopher|telnet|whois|news):\/\/[\x21-\x7e]+/ig, replacer); }; Post.checkNG = function(ng, post) { var isNG = false; if (ng.word) { isNG = ng.word.test(post.text); } if (!isNG && ng.handle) { isNG = isNG || ng.handle.test(post.name); isNG = isNG || ng.handle.test(post.title); } post.isNG = isNG; return post; }; Post.prototype = { id: "", // {string} /^\d+$/ title: " ", // {string} name: " ", // {string} date: "", // {string} resUrl: "", // {string} threadUrl: "", // {string} threadId: "", // {string} posterUrl: "", // {string} // null: 親なし // undefined: 不明 // string: ID 0から始まらない数字の文字列 /** @type {(undefined|?string)} */ parentId: null, parentDate: "", // {string} text: "", // {string} showAsIs: false, // {boolean} rejectLevel: 0, // {number} isRead: false, // {boolean} isOP: function() { return this.id === this.threadId; }, getText: function() { if (this.hasSameDate()) { return this.text.slice(0, this.text.lastIndexOf("\n\n"));//参考と空行を除去 } return this.text; }, hasSameDate: function() { return this.parent && this.parent.date === this.parentDate; }, computeQuotedText: function() { var lines = this.text .replace(/> >.*\n/g, "") //target属性がないのは参考リンクのみ .replace(/<a href="[^"]+">参考:.*<\/a>/i, "") .replace( /<a href="[^"]+" target="link"( rel="noreferrer noopener")?>([^<]+)<\/a>/ig, //<A href=¥S+ target=¥"link¥">(¥S+)<¥/A> Post.relinkify1stMatching ) .replace(/\n/g, "\n> "); lines = ("> " + lines + "\n") .replace(/\n>[ \n\r\f\t]+\n/g, "\n") .replace(/\n>[ \n\r\f\t]+\n$/, "\n"); return lines; }, textCandidate: function() { var text = this.text .replace(/^> (.*\n?)|^.*\n?/mg, "$1") .replace(/\n$/, "") .replace(/^[ \n\r\f\t]*$/mg, "$&\n$&"); //TODO 引用と本文の間に一行開ける //text = text.replace(/((?:> .*\n)+)(.+)/, "$1\n$2"); //replace(/^(?!> )/m, "\n$&"); return text;// + "\n\n"; }, textCandidateLooksValid: function() { return this.getText().replace(/^> .*/mg, "").trim() !== ""; }, textBonus: 2, dateCandidate: function() { return this.parentDate; }, dateCandidateLooksValid: function(candidate) { return /^\d{4}\/\d{2}\/\d{2}\(.\)\d{2}時\d{2}分\d{2}秒$/.test(candidate); }, dateBonus: 100, hasQuote: function() { return (/^> /m).test(this.text); }, mayHaveParent: function() { return this.isRead && !this.isOP() && this.hasQuote(); }, setParentId: function(parentId) { if (+this.id > parentId) { this.parentId = parentId; } }, setParentDate: function(parentDate) { this.parentDate = parentDate; }, }; var ImaginaryPostPrototype = { __proto__: Post.prototype, /** * @param {Post} child */ setFields: function(child) { this.id = child.parentId; this.parent = null; this.child = child; this.next = null; this.isNG = null; this.threadId = child.threadId; this.threadUrl = child.threadUrl; this.parentId = this.isOP() ? null : undefined; if (this.id) { this.setResUrl(); } }, 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(child) { this.setFields(child); this.name = child.title.replace(/^>/, ""); } MergedPost.prototype = Object.create(ImaginaryPostPrototype, { date: { get: function() { return this.calculate("date"); }, }, }); function GhostPost(child) { this.setFields(child); } 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(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 = { addPost: function(post) { this.posts.push(post); if (post.isNG) { this.isNG = true; } }, 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(post); post.parent = ghost; 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 = this.getSmallestMessageID(allPosts); if (smallestMessageID <= threshold) { roots = this.processVanish(roots); } if (this.config.utterlyVanishMessage) { roots = this.processUtterlyVanish(roots); } } return roots; }, getSmallestMessageID: function(allPosts) { return Object.keys(allPosts).sort(this.byNumber)[0]; }, byNumber: function(l, r) { return l - r; }, 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 = Posts.truncate.bind(Posts, config); var checkCharacterEntity = Posts.checkCharacterEntity.bind(Posts, config); return compose( putThumbnails, Posts.characterEntity, Posts.createDText(this.mode), Posts.prependExtension, truncate, markNG, checkCharacterEntity, Posts.checkThumbnails, Posts.makeText ); }, unfoldButton: Posts.unfoldButton, headerContents: Posts.headerContents, div: function(clazz, content) { var el = document.createElement("div"); el.className = clazz; el.innerHTML = content; return el; }, header: function(config) { var ng = config.ng; var markNGHeader = ng.handleg ? Posts.markNGHeader(ng.handleg) : identity; var classes = "message-header message-header_" + this.mode; return function(data) { var post = data.post; var state = data.state; var title = post.title; var name = post.name; if (post.isNG) { title = markNGHeader(title); name = markNGHeader(name); } var header = this.headerContents(state, config, post, name, title); return this.div(classes, header); }.bind(this); }, env: function(data) { if (!data.post.env) { return null; } var env = '<span class="env">(' + data.post.env.replace(/<br>/, "/") + ')</span>'; return this.div("extra extra_" + this.mode, this.doEnv(env, data)); }, doEnv: identity, message: function(header, text, env) { var el = document.createElement("div"); el.appendChild(header); el.appendChild(text); if (env) { el.appendChild(env); } el.className = "message message_" + this.mode; return el; }, messageMaker: function(config) { var checker = this.checker(config); var text = this.text(config); var header = this.header(config); return function(post, depth) { var dMessage; var data = checker({ post: post, value: null, state: { depth: depth, }, }); var state = data.state; if (state.hide && !post.show) { dMessage = this.div("showMessage showMessage_" + this.mode, this.unfoldButton(data)); } else { data = text(data); var dText = data.value; var dHeader = header(data); var dEnv = this.env(data); dMessage = this.message(dHeader, dText, dEnv); } if (config.spacingBetweenMessages) { this.setSpacer(dMessage, state.extension); } if (this.setMargin) { this.setMargin(dMessage, state.depth); } dMessage.id = post.id; dMessage.dataset.id = post.id; dMessage.post = post; return dMessage; }.bind(this); }, }; function CSSView() { this.mode = "tree-mode-css"; this.containers = null; this.pre = function() { this.containers = [{dcontainer: this.el}]; }; this.border = function(depth) { var left = depth + 0.5; return DOM('<div class="border outer" style="left:' + left + 'rem">' + '<div class="border inner" style="left:-' + left + 'rem">' + '</div></div>'); }; this.getContainer = function(post, depth) { var containers = this.containers; var container = containers[containers.length - 1]; if ("lastChildID" in container && container.lastChildID === post.id) { containers.pop(); container = containers[containers.length - 1]; } var child = post.child; if (child && child.next) { var lastChild = child; do { lastChild = lastChild.next; } while (lastChild.next); var dout = this.border(depth); container.dcontainer.appendChild(dout); container = {lastChildID: lastChild.id, dcontainer: dout.firstChild}; containers.push(container); } return container.dcontainer; }; this.setSpacer = function(el) { el.classList.add("spacing"); }; this.setMargin = function(el, depth) { el.style.marginLeft = depth + 'rem'; }; } CSSView.prototype = Object.create(AbstractPosts.prototype); function ASCIIView() { this.mode = "tree-mode-ascii"; function wrapTree(tag, tree) { return '<' + tag + ' class="a-tree">' + tree + '</' + tag + '>'; } function computeExtension(config, post) { var forHeader, forText, init; var utterlyVanishMessage = config.utterlyVanishMessage; var hasNext = post.next; var tree = []; var parent = post; while ((parent = parent.parent)) { if (utterlyVanishMessage && parent.rejectLevel) { break; } tree.push(parent.next ? "|" : " "); } init = tree.reverse().join(""); if (post.isOP()) { forHeader = " "; } else { forHeader = init + (hasNext ? '├' : '└'); } forText = init + (hasNext ? '|' : ' ') + (post.child ? '|' : ' '); return {header: forHeader, text: forText}; } this.extension = function(config, data) { var extension = computeExtension(config, data.post); data.state.extension = { text: function(data) { data.value = data.value.replace(/^/gm, wrapTree("span", extension.text)); return data; }, header: function(header) { return wrapTree("span", extension.header) + header; }, env: function(env) { return wrapTree("span", extension.text) + env; }, spacer: function() { return wrapTree("div", extension.text); }, }; return data; }; this.checker = function(config) { var checker = AbstractPosts.prototype.checker.apply(this, arguments); return compose(this.extension.bind(this, config), checker); }; this.setSpacer = function(el, extension) { el.insertAdjacentHTML("beforeend", extension.spacer()); }; var headerContents = AbstractPosts.prototype.headerContents; var unfoldButton = AbstractPosts.prototype.unfoldButton; this.headerContents = function(state) { return state.extension.header(headerContents.apply(null, arguments)); }; this.unfoldButton = function(data) { return data.state.extension.header(unfoldButton(data)); }; this.doEnv = function(env, data) { return data.state.extension.env(env); }; } ASCIIView.prototype = Object.create(AbstractPosts.prototype); var View = { "tree-mode-css": CSSView, "tree-mode-ascii": ASCIIView, }; function ToggleMessage(config, postParent) { this.config = config; this.postParent = postParent; } ToggleMessage.prototype = { handleEvent: function(e) { e.preventDefault(); var toggleMessage = Object.create(this); toggleMessage.button = e.target; toggleMessage.message = toggleMessage.button.closest(".message"); toggleMessage.messages = toggleMessage.message.closest(".messages"); toggleMessage.text = toggleMessage.message.querySelector(".text"); toggleMessage.post = toggleMessage.message.post; return toggleMessage.execute(); }, execute: function() { return this.setIDToPost() .then(this.hideOrRevert.bind(this)) .catch(this.error.bind(this)); }, hideOrRevert: function() { if (this.isRevertButton()) { this.revert(); } else { this.hide(); } this.changeBetweenHideAndRevert(); }, isRevertButton: function() { return this.button.classList.contains("revert"); }, changeBetweenHideAndRevert: function() { this.button.classList.toggle("revert"); }, hide: function() { var post = this.post; post.previousRejectLevel = post.rejectLevel; post.rejectLevel = 3; this.text.style.display = 'none'; this.button.textContent = "戻"; this.config.addVanishedMessage(post.id); this.setRejectLevel(post.child, 2); }, revert: function() { var post = this.post; post.rejectLevel = post.previousRejectLevel; this.text.style.display = null; this.button.textContent = "消"; this.config.removeVanishedMessage(post.id); this.removeChainingHiddenMark(post.child, 2); }, error: function(error) { this.button.parentNode.replaceChild(document.createTextNode(error.message), this.button); }, setIDToPost: function() { return this.findPostID().then(function(id) { if (!id) { return Promise.reject(new Error( "最新1000件以内に存在しないため投稿番号が取得できませんでした。" + "過去ログからなら消せるかもしれません" )); } if (id.length > 100) { return Promise.reject(new Error("この投稿は実在しないようです")); } this.post.id = id; }.bind(this)); }, findPostID: function() { var post = this.post; var id = post.id; if (id === undefined) { id = this.postParent.find(post.child.id, post.threadId); } return Promise.resolve(id); }, setRejectLevel: function setRejectLevel(post, rejectLevel) { if (post === null || rejectLevel === 0) { return; } if (post.rejectLevel < rejectLevel) { post.rejectLevel = rejectLevel; var message = this.getTargetMessage(post); if (message && !message.querySelector(".chainingHidden")) { message.firstElementChild.classList.add("chainingHidden"); } } this.setRejectLevel(post.child, rejectLevel - 1); this.setRejectLevel(post.next, rejectLevel); }, removeChainingHiddenMark: function(post, rejectLevel) { if (post === null || rejectLevel === 0) { return; } if (post.rejectLevel <= rejectLevel) { post.rejectLevel = 0; var message = this.getTargetMessage(post); var mark = message.querySelector(".chainingHidden"); if (mark) { mark.classList.remove("chainingHidden"); } } this.removeChainingHiddenMark(post.child, rejectLevel - 1); this.removeChainingHiddenMark(post.next, rejectLevel); }, getTargetMessage: function(post) { return this.messages.querySelector('[data-id="' + post.id + '"]'); }, }; function Threads() { var el = document.createElement("div"); el.id = "content"; return el; } Threads.addEventListeners = function(config, el, postParent) { function click(selector, callback) { Threads.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; }); Threads.on(el, "mousedown", ".message", Threads.showAsIs.bind(Threads, config)); click(".toggleTruncation", function(post) { post.truncation = post.hasOwnProperty("truncation") ? !post.truncation : false; }); if (config.useVanishMessage) { Threads.on(el, "click", ".toggleMessage", new ToggleMessage(config, postParent)); } Threads.on(el, "click", ".vanish", function(e) { var button = e.target; var thread = button.closest(".thread"); var id = thread.dataset.threadId; var type, text; if (thread.classList.contains("NGThread")) { type = "remove"; text = "消"; } else { type = "add"; text = "戻"; } type += "VanishedThread"; config[type](id); thread.classList.toggle("NGThread"); button.textContent = text; }); Threads.on(el, "click", ".toggleTreeMode", Threads.toggleTreeMode.bind(null, config)); }; Threads.on = on; 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.toggleTreeMode = function(config, e) { e.preventDefault(); var button = e.target; var thread = button.closest(".thread"); thread.classList.toggle("tree-mode-css"); thread.classList.toggle("tree-mode-ascii"); var view = new View[Threads.getTreeMode(thread)](); var roots = thread.roots; var messages = thread.querySelector(".messages"); view.init(roots); var newMessages = view.render(config); thread.insertBefore(newMessages, messages); thread.removeChild(messages); }; Threads.showAsIs = function(config, e) { function callback(post) { post.showAsIs = !post.showAsIs; } var target = e.target; var id = setTimeout(Threads.replace.bind(Threads, config, callback, e), 500); var cancel = function() { clearTimeout(id); target.removeEventListener("mouseup", cancel); target.removeEventListener("mousemove", cancel); }; target.addEventListener("mouseup", cancel); target.addEventListener("mousemove", cancel); }; Threads.showThreads = function(config, el, threads) { var mode = config.treeMode; var view = new View[mode](); var utterlyVanishNGThread = config.utterlyVanishNGThread; var vanishedThreadIDs = config.vanishedThreadIDs; var threshold = +config.vanishedMessageIDs[0]; var toggleTreeMode = mode === "tree-mode-css" && config.toggleTreeMode ? ' <a href="javascript:;" class="toggleTreeMode">●</a>' : ''; var emptyVanishButtons = { true: "", false: "" }; var vanishButtons = { true: ' <a href="javascript:;" class="vanish">戻</a>', false: ' <a href="javascript:;" class="vanish">消</a>', }; function show(thread, pending, isVanished, roots) { var number = thread.getNumber(); if (!number) { if (pending) { el.removeChild(pending); } return; } var vanish; if (config.useVanishThread || (isVanished && config.autovanishThread)) { vanish = vanishButtons; } else { vanish = emptyVanishButtons; } var url = '<a href="' + thread.getURL() + '" target="link">◆</a>'; var html = '<pre data-thread-id="' + thread.getID() + '" class="thread ' + mode + '">' + '<div class="thread-header">' + url + ' 更新日:' + thread.getDate() + ' 記事数:' + number + toggleTreeMode + vanish[isVanished] + ' ' + url + '</div><span class="messages"></span></pre>'; var dthread = DOM(html); if (isVanished) { dthread.classList.add("NGThread"); } view.init(roots, dthread.lastChild); view.render(config); dthread.roots = roots; if (pending) { el.replaceChild(dthread, pending); } else { el.appendChild(dthread); } } function showThread(thread) { var isVanished = config.useVanishThread && vanishedThreadIDs.indexOf(thread.getID()) > -1; var isToBeVanished = config.autovanishThread && thread.isNG; var vanish = isVanished || isToBeVanished; if (vanish && utterlyVanishNGThread) { return; } var 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>'; el.insertAdjacentHTML("beforeend", pendingHTML); return pRoots.then(show.bind(null, thread, el.lastChild, isVanished)); } else { return show(thread, null, isVanished, pRoots); } } return loop(showThread, threads); }; function PostParent(config, q) { this.useStorage = this.storageIsAvailable("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 DOM.fetch({data: { m: 't', s: threadID}}) .then(Post.makePosts) .then(this.update.bind(this)) .then(function() { return this.data; }.bind(this)); }, head: function(array) { return array[0]; }, find: function(childID, opt_threadID) { if (!this.isNumber(childID)) { throw new TypeError('"' + childID + '"は自然数の文字列'); } if (opt_threadID && typeof this.data[childID] === "undefined") { return this.findAll([childID], opt_threadID, true) .then(this.head); } return this.data[childID]; }, notContainedIn: function(id) { return typeof this[id] === "undefined"; }, needsToFetch: function(childIDs, threadID, force) { return (this.tryHard || force) && this.useStorage && this.isNumber(threadID) && // 要らないかもしれない childIDs.some(this.notContainedIn, this.data); }, from: function(p) { return this[p]; }, collect: function(ids, id) { ids[id] = this.data[id]; return ids; }, findAll: function(childIDs, threadID, opt_force) { if (!this.needsToFetch(childIDs, threadID, opt_force)) { return childIDs.reduce(this.collect.bind(this), Object.create(null)); } if (!this.updateThreadMemoized) { this.updateThreadMemoized = memoize(this.updateThread.bind(this)); } return this.updateThreadMemoized(threadID) .then(childIDs.map.bind(childIDs, this.from)); }, storageIsAvailable: 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 Preload(head) { this.preloads = Object.create(null); this.head = head || document.head; var DOMTokenListSupports = function(tokenList, token) { if (!tokenList || !tokenList.supports) { return; } try { return tokenList.supports(token); } catch (e) { if (e instanceof TypeError) { console.log("The DOMTokenList doesn't have a supported tokens list"); } else { console.error("That shouldn't have happened"); } } }; this.isSupported = DOMTokenListSupports(document.createElement("link").relList, "preload"); } Preload.prototype.fetch = function(url) { if (!this.isSupported || this.isFetched(url)) { return; } var link = document.createElement("link"); link.rel = "preload"; link.as = "image"; link.href = url; this.head.appendChild(link); this.preloads[url] = true; }; Preload.prototype.isFetched = function(url) { return this.preloads[url]; }; function Thumbnail(config) { this.config = config; this.preload = new Preload(); var animationChecker = memoize(Thumbnail.checkAnimation); // ポップアップを消した時、カーソルがサムネイルの上にある this.isClosedAboveThumbnail = function(e) { var relatedTarget = e.relatedTarget; //firefox: if (relatedTarget === null) { return true; } //opera12 if (relatedTarget instanceof HTMLBodyElement) { return true; } //chrome if (relatedTarget.closest("#image-view") && !document.getElementById("image-view")) { return true; } }; function setNote(a, text) { var note = a.nextElementSibling; // span.noteがない if (!note || !note.classList.contains("note")) { note = document.createElement("span"); note.className = "note"; a.parentNode.insertBefore(note, a.nextSibling); } note.textContent = text; } this.downloading = function(image, a) { var pending = true; var complete = function(success) { pending = false; if (success) { var note = a.nextElementSibling; if (note && note.classList.contains("note")) { note.parentNode.removeChild(note); } } else { setNote(a, "404?画像ではない?"); } }; image.addEventListener("load", complete.bind(null, true)); image.addEventListener("error", complete.bind(null, false)); setTimeout(function() { if (pending) { setNote(a, "ダウンロード中"); } }, 100); }; this.handleEvent = function(e) { if (this.isClosedAboveThumbnail(e)) { return; } var a = e.currentTarget; // ポップアップからサムネイルに帰ってきた if (a.classList.contains("popup")) { return; } var image = new Image(); image.referrerPolicy = "no-referrer"; this.downloading(image, a); image.classList.add("image-view-img"); image.src = a.href; a.classList.add("popup"); var popup = new Popup(config, image); popup.addEventListeners(); popup.waitAndOpen(); }; var misaoSmall = function(href) { var src = href; if (!/^http:\/\/misao\.(?:mixh|on\.arena\.ne)\.jp\/c\/up\/misao\d+\.\w+$/.test(href)) { return src; } return src.replace(/up\//, "up/pixy_"); }; var misaoAnimation = function(href) { if (!config.linkAnimation) { return; } var misao = /^(http:\/\/misao\.(?:mixh|on\.arena\.ne)\.jp\/c\/)up\/(misao0*\d+)\.(?:png|jpg)$/.exec(href); if (misao) { var misaoID = misao[2]; var animationURL = misao[1] + '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}; } }; this.image = { sw: [{ name: "misao", prefix: "http://misao.mixh.jp/c/", urls: function(href) { return { original: href, small: this.small(href), animation: this.animation(href), }; }, small: misaoSmall, animation: misaoAnimation, }, { name: "misao-arena", prefix: "http://misao.on.arena.ne.jp/c/", urls: function(href) { return { original: href, small: this.small(href), animation: this.animation(href), }; }, small: misaoSmall, animation: misaoAnimation, }, { 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.small = function(original, small) { // if (!original) { // throw new Error(); // } if (!small) { return small; } if (original === small) { return small; } if (!config.thumbnailPopup) { return small; } this.preload.fetch(original); if (this.preload.isFetched(original)) { return small; } else { return original; } }; this.a = function(original) { return '<a href="' + original + '" target="link" class="thumbnail">'; }; this.thumbnail = function(original, small) { var a = this.a(original); if (small) { return a + '<img referrerpolicy="no-referrer" class="thumbnail-img" src="' + small + '"></a>'; } else { return '[' + a + '■</a>]'; } }; this.construct = function(data) { var original = data.original; var small = this.small(original, data.small); var thumbnail = this.thumbnail(original, small); var animation = data.animation; if (animation) { thumbnail += '<span class="animation ' + animation.id + '">[<a href="' + animation.href + '" target="link">A</a><span class="unsure">?</span>]</span>'; } if (config.shouki) { thumbnail += shouki(original); } return thumbnail; }; function shouki(href) { return '[<a href="http://images.google.com/searchbyimage?image_url=' + href + '" target="link">詳</a>]'; } this.register = function(container) { var as = container.querySelectorAll('a[target]'); var has = false; var i; for (i = as.length - 1; i >= 0; i--) { var a = as[i]; var href = a.href; var thumbnail = this.thumbnailLink(href); if (thumbnail) { a.insertAdjacentHTML('beforebegin', thumbnail); has = true; } } if (has && config.thumbnailPopup) { var thumbs = container.getElementsByClassName('thumbnail'); for (i = thumbs.length - 1; i >= 0; i--) { thumbs[i].addEventListener("mouseover", this, false); } } }; } Thumbnail.checkAnimation = function(imgURL) { return new Promise(function(resolve) { var url = imgURL.replace(/\w+$/, "pch"); if (typeof GM_xmlhttpRequest === "function") { GM_xmlhttpRequest({ url: url, method: "HEAD", onload: function(response) { resolve(response.status === 200); }, }); } else if (Env.IS_EXTENSION) { ajax({ url: url, type: "HEAD", }).then(function() { resolve(true); }, function() { resolve(false); }); } }); }; function Popup(config, image, body) { body = body || document.body; this.waitingMetadata = null; this.handleEvent = function(e) { var type = e.type; if (type === "keydown" && !/^Esc(?:ape)?$/.test(e.key) && e.keyIdentifier !== "U+001B") { // ESC return; } if (type === "mouseout" && e.relatedTarget.closest(".popup")) { return; } this.doHandleEvent(); }; this.doHandleEvent = function() { var popup = document.getElementById("image-view"); if (popup) { popup.parentNode.removeChild(popup); } Array.prototype.slice.call(document.getElementsByClassName("popup")).forEach(function(el) { el.classList.remove("popup"); }); this.removeEventListeners(body); if (this.waitingMetadata) { clearTimeout(this.waitingMetadata); } }; this.addEventListeners = function() { this.toggleEventListeners("add"); }; this.removeEventListeners = function() { this.toggleEventListeners("remove"); }; this.toggleEventListeners = function(toggle) { ["click", "keydown", "mouseout"].forEach(function(type) { body[toggle + "EventListener"](type, this); }, this); }; function getRatio(natural, max) { if (/^\d+$/.test(max) && natural > max) { return +max / natural; } else { return 1; } } this.popup = function() { var isBestFit = config.popupBestFit; var viewport = document.compatMode === "BackCompat" ? document.body : document.documentElement; var windowHeight = viewport.clientHeight; var windowWidth = viewport.clientWidth; var imageView = document.createElement("figure"); imageView.id = "image-view"; imageView.classList.add("popup"); imageView.style.visibility = "hidden"; imageView.innerHTML = '<figcaption><span id="percentage"></span>%</figcaption>'; // bodyに追加することでimage-orientationが適用され // natural(Width|Height)以外の.*{[wW]idth|[hH]eight)が // EXIFのorientationが適用された値になる imageView.appendChild(image); body.appendChild(imageView); var width = image.offsetWidth; var height = image.offsetHeight; var marginHeight = Math.round(imageView.getBoundingClientRect().height) - height; var maxWidth = config.popupMaxWidth || (isBestFit ? windowWidth : width); var maxHeight = config.popupMaxHeight || (isBestFit ? windowHeight - marginHeight : height); var ratio = Math.min(getRatio(width, maxWidth), getRatio(height, maxHeight)); var percentage = Math.floor(ratio * 100); var bgcolor = ratio < 0.5 ? "red" : ratio < 0.9 ? "blue" : "green"; // 丸めないと画像が表示されないことがある var imageHeight = Math.floor(height * ratio) || 1; var imageWidth = Math.floor(width * ratio) || 1; imageView.style.display = "none"; image.height = imageHeight; image.width = imageWidth; imageView.querySelector("#percentage").textContent = percentage; imageView.style.cssText = 'background-color: ' + bgcolor; }; this.waitAndOpen = function() { if (!image.complete && image.naturalWidth === 0 && image.naturalHeight === 0) { this.waitingMetadata = setTimeout(this.waitAndOpen.bind(this), 50); } else { this.waitingMetadata = null; this.popup(); } }; } function Fetch(q, now) { this.now = now || Date.now(); var chk = this.getChk(q); if (chk) { this.today = chk.match(/\d+/)[0]; this.hasOP = function() { return true; }; this.data = function(ff) { var data = Object.assign({}, q); delete data[chk]; data["chk" + ff] = "checked"; return data; }; } else { this.data = function(ff) { return { __proto__: q, ff: ff, }; }; this.today = +q.ff.match(/^(\d{8})\.dat$/)[1]; var query = 'a[name="' + q.s + '"]'; this.hasOP = function(container) { return container.querySelector(query); }; } this.thisLog = this.today + ".dat"; } Fetch.prototype.getChk = function(q) { return Object.keys(q).find(function(key) { return /^chk\d+\.dat$/.test(key); }); }; Fetch.prototype.dates = function() { var ONE_DAY = 24 * 60 * 60 * 1000; var afters = []; var befores = []; var fill = function(n) { return n < 10 ? "0" + n : n; }; for (var i = 0; i < 7; i++) { var back = new Date(this.now - ONE_DAY * i); var year = back.getFullYear(); var month = fill(back.getMonth() + 1); var date = fill(back.getDate()); var day = "" + year + month + date; if (day > this.today) { afters.push(day); } else if (day < this.today) { befores.push(day); } } return {afters: afters, befores: befores}; }; Fetch.prototype.both = function(container) { var dates = this.dates(); var after = this.concurrent(dates.afters); var before = this.sequence(dates.befores, container); return Promise.all([after, before]).then(function(args) { return {afters: args[0], befores: args[1]}; }); }; 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 DOM.fetch({url: "bbs.cgi", data: this.data(ff)}) .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); }); }); }, Promise.resolve(hasOP(container))); return sequence.then(function() { return divs; }); }; Fetch.prototype.concurrent = function(dates) { return Promise.all(dates.map(this.fetch.bind(this))); }; function doNothing() {} function ready(readyState) { return new Promise(function(resolve) { readyState = readyState || document.readyState; if (/complete|loaded|interactive/.test(readyState) && document.body) { resolve(document.body); } else { document.addEventListener('DOMContentLoaded', function(e) { resolve(e.target.body); }, {once: true}); } }); } var ResWindow = { ready: function(readyState) { return ready(readyState).then(this.tweak); }, tweak: function(body) { var v = body.querySelector("textarea"); if (v) { v.focus(); // Firefox needs focus before setSelectionRange. v.scrollIntoView(); // 内容を下までスクロール firefox, opera12 v.setSelectionRange(v.textLength, v.textLength); // 内容を下までスクロール chrome v.scrollTop = v.scrollHeight; } }, }; /*global ConfigController*/ 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 AppCommon = { execute: function(config, body) { this.injectCSS(config); this.zero(config); this.addCommonEvents(config, body); this.setAccesskeyToV(config); this.registerKeyboardNavigation(config); this.setID(); }, registerKeyboardNavigation: function(config, doc_) { if (config.keyboardNavigation) { this.keyboardNavigation = this.createKeyboardNavigation(config); (doc_ || document).addEventListener("keypress", this.keyboardNavigation, false); } }, setReloadable: function() { if (this.keyboardNavigation) { this.keyboardNavigation.setReloadable(); } }, createKeyboardNavigation: function(config) { return new KeyboardNavigation(config, window); }, 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は下の方で定義 }; var ErrorHandler = { handle: function(e, body) { if (ErrorHandler.error) { throw e; } ErrorHandler.error = e; body = body || ErrorHandler.getBody(); var lineNumber = e.lineNumber || 0; var pre = document.createElement("pre"); pre.innerHTML = 'くわツリービューの処理を中断しました。表示されていない投稿があります。<a href="javascript:;">スタックトレースを表示する</a>'; var dStackTrace = document.createElement("p"); dStackTrace.style.display = "none"; var stackTrace = ""; if (typeof GM_info !== "undefined") { stackTrace += GM_info.version + "+" + GM_info.script.version + "\n"; } var stack = (e.stackTrace || e.stack || ""); stackTrace += e.name + ": " + e.stackTrace + ":" + lineNumber + "\n" + stack; dStackTrace.textContent = stackTrace; pre.appendChild(dStackTrace); pre.addEventListener("click", ErrorHandler.showStackTrace); body.insertBefore(pre, body.firstChild); throw e; }, getBody: function() { return document.body; }, showStackTrace: function(e) { e.target.parentNode.querySelector("p").style.display = null; }, }; var App = { execute: function(config, q, container, execute) { if (App.checkResWindow(document)) { if (config.closeResWindow) { App.closeResWindow(); } } else if (App.checkSetupWindow(document)) { // Do nothing } else { var app = Object.create(AppCommon); app.execute(config, container); var done = execute(config, q, container); return Promise.resolve(done).then(function() { app.setReloadable(); }); } }, 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(); } }, }; var AppGM = { main: function(config, q) { return AppGM.doMain(config, q).catch(ErrorHandler.handle); }, doMain: function(config, q) { return Promise.all([config, ready()]).then(function(args) { var config = args[0]; var body = args[1]; return App.execute(config, q, body, AppGM.view(config)); }); }, view: function(config) { return config.isTreeView() ? tree : stack; }, }; /* global AppChrome */ /* exported whatToDo */ function whatToDo(q, hostname) { switch (q.m) { case "f": //レス窓 return ResWindow.ready.bind(ResWindow); case "l": //トピック一覧 case "c": //個人用設定 return doNothing; case 'g': //過去ログ if (!q.sv && !(q.e && /^misao\.(mixh|on\.arena\.ne)\.jp$/.test(hostname))) { return doNothing; } } return window.MutationObserver ? AppChrome.main : AppGM.main; } function TreeGUI(config, body) { this.config = config; this.body = body; } TreeGUI.prototype = { template: function() { var reload = this.createReload(); var accesskey = this.getAccesskey(); var viewsAndViewing = this.getViewsAndViewing(); var vanishedThreadIDLength = this.config.vanishedThreadIDs.length; var vanishedMessageIDLength = this.config.vanishedMessageIDs.length; var hidden = vanishedThreadIDLength || vanishedMessageIDLength ? "" : " hidden"; var containee = '<header id="header">' + '<span class="left">' + reload.replace('class="mattari"', '$& accesskey="' + accesskey + '"') + ' ' + viewsAndViewing + '<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">' + vanishedThreadIDLength + '</span>スレッド</a>/' + '<a id="clearVanishedMessageIDs" href="javascript:;"><span class="count">' + vanishedMessageIDLength + '</span>投稿</a>' + ')' + '</span> ' + reload + '</span>' + '</footer>'; return containee; }, createReload: function() { var reload = '<input type="button" value="リロード" class="mattari">'; if (!this.config.zero) { reload = reload.replace('mattari', 'reload'); reload += '<input type="button" value="未読" class="mattari">'; } return reload; }, getAccesskey: function() { var accesskey = this.config.accesskeyReload; return /^\w$/.test(accesskey) ? accesskey : "R"; }, getViewsAndViewing: function() { var hr = this.body.getElementsByTagName("hr")[0]; if (hr) { var font = hr.previousElementSibling; if (font && font.tagName === "FONT") { var tmp = font.textContent.match(/\d+/g) || []; var views = tmp[3]; var viewing = tmp[5]; return views + ' / ' + viewing + '名 '; } } return ""; }, render: function() { 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', this.focusV); ['Message', 'Thread'].forEach(function(type) { var id = 'clearVanished' + type + 'IDs'; click('#' + id, eventHandlers.clearVanishedIDs.bind(null, this.config, id)); }.bind(this)); el.innerHTML = this.template(); 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); this.gui = { container: el, info: info, content: threads, postcount: postcount, footer: el.lastChild, }; return this; }, prependToBody: function() { this.body.insertBefore(this.gui.container, this.body.firstChild); }, focusV: function() { setTimeout(function() { document.getElementsByName("v")[0].focus(); }, 50); }, setInfo: function(text) { this.gui.info.textContent = text; }, setInfoHTML: function(html) { this.gui.info.innerHTML = html; }, clearInfo: function() { this.gui.info.textContent = ""; }, appendExtraInfoHTML: function(html) { this.gui.info.insertAdjacentHTML("afterend", html); }, setPostCount: function(message) { this.gui.postcount.textContent = message; }, showSaving: function() { this.buttons = this.gui.footer.querySelector(".clearVanishedButtons"); this.buttons.insertAdjacentHTML("beforebegin", '<span class="savingVanishedThreadIDs">非表示スレッド保存中</span>'); }, showSaved: function() { var buttons = this.buttons; var saving = buttons.previousElementSibling; saving.parentNode.removeChild(saving); var threadLength = this.config.vanishedThreadIDs.length; if (threadLength) { buttons.querySelector("#clearVanishedThreadIDs .count").textContent = threadLength; buttons.classList.remove("hidden"); } }, }; var Tree = { execute: function(config, q, gui, container) { var posts = this.makePosts(config, container); this.tweakFooter(container, posts.length); this.tweakURL(q, posts); var mPosts = this.addExtraLog(config, q, gui, container, posts); return mPosts.then(this.show.bind(this, config, q, gui)) .then(function() { return mPosts; }); }, makePosts: function(config, container) { var posts = Post.makePosts(container); return this.processNG(config, posts); }, processNG: function(config, posts) { if (!config.ng.isEnabled) { return posts; } this.checkNG(config.ng, posts); if (!config.autovanishThread && config.utterlyVanishNGStack) { return this.excludeNG(posts); } return posts; }, addExtraLog: function(config, q, gui, container, posts) { var target; if (this.needsToSearchLog(q)) { target = "both"; } else if (this.isFromKomachi(document.referrer, this.href())) { target = "after"; } else { return Promise.resolve(posts); } return this.fetchFromRemote(config, gui, new Fetch(q), target, container, posts); }, //通常モードからスレッドボタンを押した場合 isThreadSearchWithin1000: function(q) { return q.m === 't' && !q.ff && /^\d+$/.test(q.s); }, //検索窓→投稿者検索→★の結果の場合 isPosterSearchInLog: function(q) { return q.s && q.ff && q.m === 's'; }, needsToTweakLink: function(q) { return this.isThreadSearchWithin1000(q) || this.isPosterSearchInLog(q); }, needsToSearchLog: function(q) { return q.m === "t" && /^\d+\.dat$/.test(q.ff) && /^\d+$/.test(q.s); }, isFromKomachi: function(referrer, href) { return /^http:\/\/misao\.(?:mixh|on\.arena\.ne)\.jp\/c\/upload\.cgi/.test(referrer) && /^http:\/\/misao\.(?:mixh|on\.arena\.ne)\.jp\/cgi-bin\/bbs\.cgi\?chk\d+\.dat=checked&kwd=http:\/\/misao\.(?:mixh|on\.arena\.ne)\.jp\/c\/up\/misao\d+\.\w+&s1=0&e1=0&s2=24&e2=0&ao=a&tt=a&alp=checked&btn=checked(?:&g=checked)?&m=g&k=%82%A0&sv=on$/.test(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.setInfo(" - スレッド構築中"); Threads.addEventListeners(config, gui.gui.content, postParent); Tree.suggestLinkToLog(q, Tree.href(), gui, posts); Tree.setPostCount(gui, posts.length); postParent.update(posts); var threads = Tree.threads(config, postParent, posts); Tree.sortThreads(config, threads); this.autovanishThread(config, gui, threads); var done = Threads.showThreads(config, gui.gui.content, threads); done.then(gui.clearInfo.bind(gui)); postParent.setWhenToCleanUp(done); return done; }, autovanishThread: function(config, gui, threads) { if (!config.autovanishThread) { return; } var ids = threads.filter(function(thread) { return thread.isNG; }).map(function(thread) { return thread.id; }); if (!ids.length) { return; } gui.showSaving(); return config.addVanishedThread(ids).then(gui.showSaved.bind(gui)); }, makePostParent: function(config, q) { return new PostParent(config, q); }, href: function() { return location.href; }, 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; }, tweakURL: function(q, posts) { if (!this.needsToTweakLink(q)) { return; } 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; } }); }, fetchFromRemote: function(config, gui, fetcher, target, container, posts) { gui.setInfoHTML('<strong>' + fetcher.thisLog + "以外の過去ログを検索中...</strong>"); var makeArray = function(posts, div) { var newPosts = this.makePosts(config, div); return posts.concat(newPosts); }.bind(this); return fetcher[target](container).then(function(doms) { 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.addPost(post); }); 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, gui, 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"; gui.appendExtraInfoHTML(' <a id="hint" href="' + url + '">過去ログを検索する</a>'); } }, setPostCount: function(gui, postLength) { var message; if (postLength) { message = postLength + "件取得"; } else { message = "未読メッセージはありません。"; } gui.setPostCount(message); }, tweakFooter: function(container, hasPost) { var i = container.querySelector("p i"); if (!i) { return; } var numPostsInfo = i.parentNode; var buttons = DOM.nextElement("TABLE")(numPostsInfo); var end; if (buttons && hasPost) { // ボタンを残す 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) { try { if (Env.IS_FIREFOX) { var html = body.parentNode; html.removeChild(body); } var gui = new TreeGUI(config, body); gui.render(); var done = Tree.execute(config, q, gui, body); Tree.deleteOriginal(config, body); gui.prependToBody(); return done; } finally { if (Env.IS_FIREFOX) { html.appendChild(body); } } } 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); } }, }; function StackLog(config, q, body, view) { this.config = config; this.q = q; this.body = body; this.view = view; } StackLog.prototype = { container: function() { if (!document.body) { throw new Error("no body"); } var el = document.createElement("div"); el.id = "container"; var info = new Info(); el.appendChild(info); return {container: el, info: info}; }, shouldComplement: function() { return this.q.m === "t" && /^\d+\.dat$/.test(this.q.ff) && /^\d+$/.test(this.q.s) && !this.body.querySelector('a[name="' + this.q.s + '"]'); }, complement: function() { if (this.shouldComplement()) { var gui = this.container(); var container = gui.container; var info = gui.info; info.innerHTML = '<strong>' + this.q.ff + "以外の過去ログを検索中...</strong>"; this.body.insertBefore(container, this.body.firstChild); return this.makeFetch().both(this.body) .then(this.addExtraLog.bind(this, container)) .then(function() { info.textContent = ""; }); } }, makeFetch: function() { return new Fetch(this.q); }, addExtraLog: function(container, doms) { var view = this.view; var wrap = (function() { var wrap = view.wrapOne.bind(view); return function(f) { Array.prototype.forEach.call(f.querySelectorAll("a[name]"), wrap); return f; }; })(); var f = document.createDocumentFragment(); function 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>' + this.q.ff + '</h1>')); this.body.insertBefore(f, container.nextSibling); f = doms.afters.reduceRight(format, f); this.body.appendChild(f); }, }; var Stack = { common: function(config, body) { Stack.addEventListener(config, body); Stack.configButton(config, body); Stack.accesskey(config, body); }, accesskey: function(config, body) { var midoku = body.querySelector('input[name="midokureload"]'); if (midoku) { midoku.accessKey = config.accesskeyReload; } }, addEventListener: function(config, body) { on(body, "click", ".showNG", this.showNG); on(body, "click", ".showThread", this.showThread.bind(this, config)); on(body, "click", ".clearVanishedThreadIDs", this.clearVanishedThreadIDs.bind(this, config)); on(body, "click", ".vanish", this.vanish.bind(this, config)); }, showNG: function(e) { Stack.removeButtons(e.target.parentNode.nextElementSibling); }, showThread: function(config, e) { e.preventDefault(); var buttons = e.target.parentNode; var thisMessage = buttons.nextElementSibling; var id = thisMessage.dataset.threadId; var restore = Stack.savePosition(buttons); config.removeVanishedThread(id); Array.prototype.filter.call(document.querySelectorAll('.original'), function(message) { return message.dataset.threadId === id; }).forEach(function(message) { if (message === thisMessage) { restore(); } Stack.removeButtons(message); }); }, clearVanishedThreadIDs: function(config, e) { 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); }; }, configButton: function(config, body) { var setup = body.querySelector('input[name="setup"]'); if (setup) { var button = ' <a href="javascript:;" id="openConfig">★くわツリービューの設定★</a>'; if (config.vanishedThreadIDs.length) { button += ' 非表示解除(<a class="clearVanishedThreadIDs" href="javascript:;"><span class="length">' + config.vanishedThreadIDs.length + '</span>スレッド</a>)'; } setup.insertAdjacentHTML("afterend", button); } }, render: function(config, body, view) { if (config.keyboardNavigation || config.thumbnail || config.ng.isEnabled || config.useVanishThread) { var anchors = body.querySelectorAll("body > a[name]"); var wrap = view.wrapOne.bind(view); if (Env.IS_FIREFOX) { try { var html = body.parentNode; html.removeChild(body); anchors.forEach(wrap); } finally { html.appendChild(body); } } else { return loop(wrap, anchors); } } }, tweakFooter: function(config, container, opt_done) { if (this.needsToTweakFooter(config)) { var insertFooter = this.doTweakFooter(container); return Promise.resolve(opt_done).then(insertFooter); } }, needsToTweakFooter: function(config) { return config.ng.isEnabled && config.utterlyVanishNGStack || config.useVanishThread && config.utterlyVanishNGThread; }, doTweakFooter: function(container) { var i = container.querySelector("p i"); if (!i) { return doNothing; } var numPostsInfo = i.parentNode; var hr = 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, body); var view = new StackView(config); var log = new StackLog(config, q, body, view); var complement = log.complement(); var render = Stack.render(config, body, view); var tweakFooter = Stack.tweakFooter(config, body, render); return Promise.all([complement, render, tweakFooter]); } function Info() { var el = document.createElement("span"); el.id = "info"; return el; } function KeyboardNavigation(config, window) { if (!window) { throw new Error("missing 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; this.setReloadable = 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: } }; } /////////////////////////////////////////////////////////////////////////////// AppCommon.injectCSS = function(config) { var css = '\ .text {\ white-space: pre-wrap;\ }\ .text, .extra {\ min-width: 20rem;\ }\ .text_tree-mode-css, .extra_tree-mode-css {\ margin-left: 1rem;\ }\ .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: 0.8rem;\ font-family: normal;\ margin-top: 0.8rem;\ padding: 0;\ width: 100%;\ }\ \ .message-header {\ white-space: nowrap;\ }\ .message-header_tree-mode-css {\ font-size: 0.85rem;\ font-family: normal;\ }\ .message-info {\ font-family: monospace;\ color: #87CE99;\ }\ \ .read, .quote {\ color: #CCB;\ }\ header, footer {\ display: flex;\ font-size: 0.9rem;\ }\ header .left, footer .left {\ margin-right: auto;\ }\ .thread {\ margin-bottom: 1rem;\ }\ .modified {\ color: #FBB\ }\ .note, .characterEntityOn, .env {\ font-style: italic;\ }\ .chainingHidden::after {\ content: "この投稿も非表示になります";\ font-weight: bold;\ font-style: italic;\ color: red;\ }\ .a-tree {\ font-style: initial;\ }\ \ .inner {\ /* border: 2px solid yellow; */\ top: -1rem;\ }\ .outer {\ border-left: 1px solid #ADB;\ top: 1rem;\ }\ .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: 1rem;\ }\ '; 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.fetch = function(options) { return ajax(options) .then(DOM.wrapWithDiv) .catch(DOM.wrapErrorWithDiv); }; DOM.wrapWithDiv = function wrapWithDiv(html) { var div = document.createElement("div"); div.innerHTML = html; return div; }; DOM.wrapErrorWithDiv = function(error) { var div = document.createElement("div"); div.textContent = error; return div; }; function loop(func, array) { return new Promise(function(resolve, reject) { var i = 0, length = array.length; var done = []; (function loop() { var t = Date.now(); do { if (i === length) { Promise.all(done).then(resolve); return; } try { done.push(func(array[i++])); } catch (e) { reject(e); return; } } while (Date.now() - t < 20); setTimeout(loop, 0); })(); }); } /*exported parseQuery*/ function parseQuery(search) { var obj = {}, kvs = search.substring(1).split("&"); kvs.forEach(function (kv) { obj[kv.split("=")[0]] = kv.split("=")[1]; }); return obj; } /*eslint-env es6 */ function delayPromise(ms) { return new Promise(function(resolve) { setTimeout(resolve, ms); }); } function DelayNotice(config, loaded, body, timeout) { var this$1 = this; this.config = config; this.loaded = loaded; this.body = body; this.timeout = delayPromise(timeout || 700); config.then(function () { this$1.configIsLoaded = true; }); } DelayNotice.prototype.start = function() { var this$1 = this; return Promise.race([this.timeout, this.loaded]) .then(function () { return this$1.body; }) .then(this.popup.bind(this)); }; DelayNotice.prototype.popup = function(body) { if (this.configIsLoaded) { return; } var notice = document.createElement("aside"); notice.id = "qtv-status"; notice.style.cssText = "position:fixed;top:0px;left:0px;background-color:black;color:white;z-index:1"; notice.textContent = '設定読込待ち'; body.insertBefore(notice, body.firstChild); var removeNotice = function() { body.removeChild(notice); }; this.config.then(removeNotice, removeNotice); this.loaded.then(function() { notice.textContent = "設定読込待ちかレンダリング中"; }); }; /*global App*/ /*exported AppChrome*/ var AppChrome = { main: function main(config, q) { var body = AppChrome.waitFor.body(document); var loaded = AppChrome.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); var done = handler.start(); notice.start(); observer.listener = handler; observer.observe(); return done; }, 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); }); }, }, }; /*global Stack*/ var StreamStackView = function StreamStackView(args) { Object.assign(this, args); this.r = document.createRange(); }; StreamStackView.prototype.init = function init () { var ms = this.ms; Stack.common(this.config, this.body); if (ms.hasChildNodes()) { var range = this.r; range.selectNodeContents(ms); this.buffer.appendChild(range.extractContents()); } this.render(); ms.hidden = false; }; StreamStackView.prototype.finish = function finish () { Stack.tweakFooter(this.config, this.buffer); this.body.appendChild(this.buffer); return this.log.complement(); }; StreamStackView.prototype.render = function render () { var ref = this; var r = ref.r; var view = ref.view; 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); // 以下のように一つずつやるとO(n) // 一気に全部やるとO(n^2) view.wrapOne(buffer.querySelector("a[name]")); 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; }; /*global Tree, TreeGUI */ var StreamTreeView = function StreamTreeView(args) { Object.assign(this, args); this.gui = new TreeGUI(this.config, this.body); }; StreamTreeView.prototype.init = function init () { this.gui.render(); this.gui.prependToBody(); }; 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); }; /*global ErrorHandler, StackView, StackLog */ 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}; if (config.isTreeView()) { return new StreamTreeView(args); } else { var view = new StackView(config); var log = new StackLog(config, q, body, view); return new StreamStackView(Object.assign(args, {view: view, log: log})); } }; this.initView = function (config, q, body) { view = this$1.createView(config, body); view.init(); return pLoaded.then(function () { return view.finish(); }); }; this.execute = function (ref) { var config = ref[0]; var body = ref[1]; return App.execute(config, q, body, this$1.initView); }; this.start = function () { Promise.race([pConfig, pLoaded]).then(this$1.stashForNow); return Promise.all([ pConfig, pBody, Promise.race([pAnchor, pLoaded]) ]).then(this$1.execute).catch(function (e) { return pLoaded.then(function () { this$1.onProgress = doNothing; ErrorHandler.handle(e); }); }); }; } /*global doNothing*/ function Observer(htmlDocument, loaded) { var this$1 = this; var observer; this.listener = null; this.firstAnchor = null; this.hr = null; var find = Array.prototype.find; var fireEvent = function (event, arg) { try { return this$1.listener[event](arg); } catch (e) { ErrorHandler.handle(e); observer.disconnect(); observer.start = doNothing; } }; 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(); }; observer = new MutationObserver(this.processRecords); observer.start = function() { if (htmlDocument.body) { this.observe(htmlDocument.body, { childList: true }); } else { this.observe(htmlDocument.documentElement, { childList: true, subtree: true }); } }; loaded.then(function () { observer.start = doNothing; var records = observer.takeRecords(); if (records.length) { console.error(records.length); this$1.processRecords(records, observer); } observer.disconnect(); }); this.observe = function () { observer.start(); }; } // ==UserScript== // @name tree view for qwerty // @name:ja くわツリービュー // @namespace strangeworld // @description あやしいわーるど@みさおの投稿をツリーで表示できます。スタック表示の方にもいくつか機能を追加できます。 // @match http://misao.on.arena.ne.jp/cgi-bin/bbs.cgi* // @match http://misao.mixh.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.11 // @run-at document-start // ==/UserScript== /*global parseQuery, whatToDo, Config*/ function main() { var q = parseQuery(location.search); var action = whatToDo(q, location.hostname); var config = Config.instance; action(config, q); } main();