您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
あやしいわーるど@みさおの投稿をツリーで表示できます。スタック表示の方にもいくつか機能を追加できます。
当前为
// ==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* // @match http://usamin.elpod.org/cgi-bin/swlog.cgi?b=*&s=* // @grant GM_setValue // @grant GM.setValue // @grant GM_getValue // @grant GM.getValue // @grant GM_deleteValue // @grant GM.deleteValue // @grant GM_listValues // @grant GM.listValues // @grant GM_xmlhttpRequest // @grant GM.xmlHttpRequest // @grant GM_openInTab // @grant GM.openInTab // @grant window.close // @version 10.13 // @run-at document-start // @connect misao.on.arena.ne.jp // @connect misao.mixh.jp // ==/UserScript== // zousan - A Lightning Fast, Yet Very Small Promise A+ Compliant Implementation // https://github.com/bluejava/zousan // Author: Glenn Crownover <[email protected]> (http://www.bluejava.com) // Version 2.3.3 // License: MIT /* jshint asi: true, browser: true */ /* global setImmediate, console */ (function(global){ "use strict"; var STATE_PENDING, // These are the three possible states (PENDING remains undefined - as intended) STATE_FULFILLED = "fulfilled", // a promise can be in. The state is stored STATE_REJECTED = "rejected", // in this.state as read-only _undefined, // let the obfiscator compress these down _undefinedString = "undefined"; // by assigning them to variables (debatable "optimization") // See http://www.bluejava.com/4NS/Speed-up-your-Websites-with-a-Faster-setTimeout-using-soon // This is a very fast "asynchronous" flow control - i.e. it yields the thread and executes later, // but not much later. It is far faster and lighter than using setTimeout(fn,0) for yielding threads. // Its also faster than other setImmediate shims, as it uses Mutation Observer and "mainlines" successive // calls internally. // WARNING: This does not yield to the browser UI loop, so by using this repeatedly // you can starve the UI and be unresponsive to the user. // This is an even FASTER version of https://gist.github.com/bluejava/9b9542d1da2a164d0456 that gives up // passing context and arguments, in exchange for a 25x speed increase. (Use anon function to pass context/args) var soon = (function() { var fq = [], // function queue; fqStart = 0, // avoid using shift() by maintaining a start pointer - and remove items in chunks of 1024 (bufferSize) bufferSize = 1024 function callQueue() { while(fq.length - fqStart) // this approach allows new yields to pile on during the execution of these { try { fq[fqStart]() } // no context or args.. catch(err) { if(global.console) global.console.error(err) } fq[fqStart++] = _undefined // increase start pointer and dereference function just called if(fqStart == bufferSize) { fq.splice(0,bufferSize); fqStart = 0; } } } // run the callQueue function asyncrhonously, as fast as possible var cqYield = (function() { // This is the fastest way browsers have to yield processing if(typeof MutationObserver !== _undefinedString) { // first, create a div not attached to DOM to "observe" var dd = document.createElement("div"); var mo = new MutationObserver(callQueue); mo.observe(dd, { attributes: true }); return function() { dd.setAttribute("a",0); } // trigger callback to } // if No MutationObserver - this is the next best thing - handles Node and MSIE if(typeof setImmediate !== _undefinedString) return function() { setImmediate(callQueue) } // final fallback - shouldn't be used for much except very old browsers return function() { setTimeout(callQueue,0) } })(); // this is the function that will be assigned to soon // it takes the function to call and examines all arguments return function(fn) { // push the function and any remaining arguments along with context fq.push(fn); if((fq.length - fqStart) == 1) // upon adding our first entry, kick off the callback cqYield(); }; })(); // -------- BEGIN our main "class" definition here ------------- function Zousan(func) { // this.state = STATE_PENDING; // Inital state (PENDING is undefined, so no need to actually have this assignment) //this.c = []; // clients added while pending. <Since 1.0.2 this is lazy instantiation> // If a function was specified, call it back with the resolve/reject functions bound to this context if(func) { var me = this; try { func( function(arg) { me.resolve(arg) }, // the resolve function bound to this context. function(arg) { me.reject(arg) }) // the reject function bound to this context } catch(err) { me.reject(err) } } } Zousan.prototype = { // Add 6 functions to our prototype: "resolve", "reject", "then", "catch", "finally" and "timeout" resolve: function(value) { if(this.state !== STATE_PENDING) return; if(value === this) return this.reject(new TypeError("Attempt to resolve promise with self")); var me = this; // preserve this if(value && (typeof value === "function" || typeof value === "object")) { try { var first = true; // first time through? var then = value.then; if(typeof then === "function") { // and call the value.then (which is now in "then") with value as the context and the resolve/reject functions per thenable spec then.call(value, function(ra) { if(first) { first=false; me.resolve(ra);} }, function(rr) { if(first) { first=false; me.reject(rr); } }); return; } } catch(e) { if(first) this.reject(e); return; } } this.state = STATE_FULFILLED; this.v = value; if(me.c) soon(function() { for(var n=0, l=me.c.length;n<l;n++) resolveClient(me.c[n],value); }); }, reject: function(reason) { if(this.state !== STATE_PENDING) return; this.state = STATE_REJECTED; this.v = reason; var clients = this.c; if(clients) soon(function() { for(var n=0, l=clients.length;n<l;n++) rejectClient(clients[n],reason); }); else if(!Zousan.suppressUncaughtRejectionError && global.console) global.console.log("You upset Zousan. Please catch rejections: ", reason,reason ? reason.stack : null) }, then: function(onF,onR) { var p = new Zousan(); var client = {y:onF,n:onR,p:p}; if(this.state === STATE_PENDING) { // we are pending, so client must wait - so push client to end of this.c array (create if necessary for efficiency) if(this.c) this.c.push(client); else this.c = [client]; } else // if state was NOT pending, then we can just immediately (soon) call the resolve/reject handler { var s = this.state, a = this.v; soon(function() { // we are not pending, so yield script and resolve/reject as needed if(s === STATE_FULFILLED) resolveClient(client,a); else rejectClient(client,a); }); } return p; }, "catch": function(cfn) { return this.then(null,cfn); }, // convenience method "finally": function(cfn) { return this.then(cfn,cfn); }, // convenience method // new for 1.2 - this returns a new promise that times out if original promise does not resolve/reject before the time specified. // Note: this has no effect on the original promise - which may still resolve/reject at a later time. "timeout" : function(ms,timeoutMsg) { timeoutMsg = timeoutMsg || "Timeout" var me = this; return new Zousan(function(resolve,reject) { setTimeout(function() { reject(Error(timeoutMsg)); // This will fail silently if promise already resolved or rejected }, ms); me.then(function(v) { resolve(v) }, // This will fail silently if promise already timed out function(er) { reject(er) }); // This will fail silently if promise already timed out }) } }; // END of prototype function list function resolveClient(c,arg) { if(typeof c.y === "function") { try { var yret = c.y.call(_undefined,arg); c.p.resolve(yret); } catch(err) { c.p.reject(err) } } else c.p.resolve(arg); // pass this along... } function rejectClient(c,reason) { if(typeof c.n === "function") { try { var yret = c.n.call(_undefined,reason); c.p.resolve(yret); } catch(err) { c.p.reject(err) } } else c.p.reject(reason); // pass this along... } // "Class" functions follow (utility functions that live on the Zousan function object itself) Zousan.resolve = function(val) { var z = new Zousan(); z.resolve(val); return z; } Zousan.reject = function(err) { var z = new Zousan(); z.reject(err); return z; } Zousan.all = function(pa) { var results = [ ], rc = 0, retP = new Zousan(); // results and resolved count function rp(p,i) { if(!p || typeof p.then !== "function") p = Zousan.resolve(p); p.then( function(yv) { results[i] = yv; rc++; if(rc == pa.length) retP.resolve(results); }, function(nv) { retP.reject(nv); } ); } for(var x=0;x<pa.length;x++) rp(pa[x],x); // For zero length arrays, resolve immediately if(!pa.length) retP.resolve(results); return retP; } // If this appears to be a commonJS environment, assign Zousan as the module export if(typeof module != _undefinedString && module.exports) // jshint ignore:line module.exports = Zousan; // jshint ignore:line // If this appears to be an AMD environment, define Zousan as the module export if(global.define && global.define.amd) global.define([], function() { return Zousan }); // Make Zousan a global variable in all environments global.Zousan = Zousan; // make soon accessable from Zousan Zousan.soon = soon; })(typeof global != "undefined" ? global : this); // jshint ignore:line (function () { 'use strict'; if (!window.Promise && typeof it == "undefined") { window.Promise = window.Zousan; if (!Promise.race) { Promise.race = function(promises) { return new Promise(function(resolve, reject) { promises.forEach(function(promise) { promise = promise.then ? promise : Promise.resolve(promise); promise.then(resolve).catch(reject); }); }); }; } } if (!Object.assign) { Object.assign = function assign(target, _source) { var arguments$1 = arguments; for (var index = 1, key, src; index < arguments.length; ++index) { src = arguments$1[index]; for (key in src) { if (Object.prototype.hasOwnProperty.call(src, key)) { target[key] = src[key]; } } } return target; }; } if (!Object.values) { Object.values = function values(object) { var values = []; for (var key in object) { if (Object.prototype.hasOwnProperty.call(object, key)) { values.push(object[key]); } } return values; }; } if (!String.prototype.startsWith) { String.prototype.startsWith = function(start) { return this.lastIndexOf(start, 0) === 0; }; } if (!String.prototype.endsWith) { Object.defineProperty(String.prototype, "endsWith", { value: function(searchString, position) { var subjectString = this.toString(); if (position === undefined || position > subjectString.length) { position = subjectString.length; } position -= searchString.length; var lastIndex = subjectString.indexOf(searchString, position); return lastIndex !== -1 && lastIndex === position; }, }); } if (!String.prototype.includes) { String.prototype.includes = function() { return String.prototype.indexOf.apply(this, arguments) !== -1; }; } if (!String.prototype.trimRight) { String.prototype.trimRight = function() { return this.replace(/\s+$/, ""); }; } // element-closest | CC0-1.0 | github.com/jonathantneal/closest if (typeof Element.prototype.matches !== "function") { Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.webkitMatchesSelector || Element.prototype.oMatchesSelector || function matches(selector) { var element = this; var elements = (element.document || element.ownerDocument) .querySelectorAll(selector); var index = 0; while (elements[index] && elements[index] !== element) { ++index; } return Boolean(elements[index]); }; } if (typeof Element.prototype.closest !== "function") { Element.prototype.closest = function closest(selector) { var element = this; while (element && element.nodeType === 1) { if (element.matches(selector)) { return element; } element = element.parentNode; } return null; }; } if (typeof requestAnimationFrame !== "function") { window.requestAnimationFrame = function(callback) { setTimeout(callback, 16); }; } var Stash = function Stash() { var area = (this.area = document.createElement("div")); area.id = "qtv-stash-area"; area.hidden = true; }; Stash.prototype.stash = function stash (buffer) { this.area.appendChild(buffer); }; Stash.prototype.restore = function restore () { this.area.parentNode.removeChild(this.area); var range = document.createRange(); range.selectNodeContents(this.area); return range.extractContents(); }; Stash.prototype.appendTo = function appendTo (node) { node.appendChild(this.area); }; function Buffer(range) { var this$1 = this; if ( range === void 0 ) range = document.createRange(); var buffer = (this.buffer = document.createDocumentFragment()); this.marker = document.createComment("qtv-main-started"); this.view = null; this.onProgress = function (lastChild) { if (lastChild !== this$1.marker) { range.setEndAfter(lastChild); buffer.appendChild(range.extractContents()); } this$1.render(); }; /** * @param {HTMLHRElement} hr */ this.onHr = function (hr) { hr.parentNode.insertBefore(this$1.marker, hr.nextSibling); range.setStartAfter(this$1.marker); }; this.onLoaded = function () { this$1.wasLoaded = true; if (this$1.view) { this$1.render(); this$1.finish(); } else { this$1.flush(); } }; } Buffer.prototype.setView = function(view) { this.view = view; if (this.wasLoaded) { this.rewind(); } }; Buffer.prototype.rewind = function() { this.buffer = this.stash.restore(); this.render(); this.finish(); }; Buffer.prototype.render = function() { if (this.view && "render" in this.view) { this.view.render(this.buffer); } }; Buffer.prototype.finish = function() { this.view.finish(this.buffer); }; Buffer.prototype.flush = function() { if (!this.marker.parentNode) { return; } this.stash = new Stash(); this.stash.stash(this.buffer); this.stash.appendTo(this.marker.parentNode); }; Buffer.prototype.insertBefore = function(node) { this.marker.parentNode.insertBefore(node, this.marker); }; function getBody() { return document.body; } var delayPromise = function (ms) { return new Promise(function (resolve) { return setTimeout(resolve, ms); }); }; var createDelayNotice = function (config, timeout) { if ( timeout === void 0 ) timeout = 700; var message = "設定読込待ち"; var configIsLoaded = false; var notice = null; config.then(function () { return (configIsLoaded = true); }); var popup = function() { if (configIsLoaded) { return; } var notice = document.createElement("aside"); notice.id = "qtv-status"; notice.style.cssText = "position:fixed;top:0px;left:0px;background-color:black;color:white;z-index:1"; notice.textContent = message; var body = getBody(); body.insertBefore(notice, body.firstChild); var removeNotice = function () { return body.removeChild(notice); }; config.then(removeNotice, removeNotice); }; return { onHr: function () { return delayPromise(timeout).then(popup); }, onLoaded: function () { message = "設定読込待ちかレンダリング中"; if (notice) { notice.textContent = message; } }, }; }; function doNothing() {} var find = Array.prototype.find; var isHR = function (node) { return node.nodeName === "HR"; }; var findHr = function (mutations) { for (var i = 0; i < mutations.length; i++) { var mutation = mutations[i]; if (mutation.target.nodeName === "BODY") { var element = find.call(mutation.addedNodes, isHR); if (element) { return element; } } } }; function ready(ref) { if ( ref === void 0 ) ref = {}; var doc = ref.doc; if ( doc === void 0 ) doc = document; var capture = ref.capture; if ( capture === void 0 ) capture = false; return new Promise(function(resolve) { var readyState = doc.readyState; if ( readyState === "complete" || (readyState !== "loading" && !doc.documentElement.doScroll) ) { resolve(); } else { doc.addEventListener("DOMContentLoaded", resolve, { capture: capture, once: true, }); } }); } var e$1; function handleError(error) { if (e$1) { return; } e$1 = error; ready() .then(getBody) .then(doHandle); } function doHandle(body) { var lineNumber = e$1.lineNumber || 0; var pre = document.createElement("pre"); pre.innerHTML = 'くわツリービューの処理を中断しました。表示されていない投稿があります。<a href="javascript:;">スタックトレースを表示する</a>'; var dStackTrace = document.createElement("p"); dStackTrace.style.display = "none"; var stackTrace = ""; if (typeof GM_info !== "undefined") { stackTrace += GM_info.version + "+" + GM_info.script.version + "\n"; } else if (typeof GM !== "undefined") { if (GM.info) { stackTrace += GM.info.version + "+" + GM.info.script.version + "\n"; } else { stackTrace += "GM4\n"; } } var stack = e$1.stackTrace || e$1.stack || ""; stackTrace += e$1.name + ": " + e$1.stackTrace + ":" + lineNumber + "\n" + stack; dStackTrace.textContent = stackTrace; pre.appendChild(dStackTrace); pre.addEventListener("click", showStackTrace); body.insertBefore(pre, body.firstChild); } function showStackTrace(e) { e.target.parentNode.querySelector("p").style.display = null; } function Observer(loaded, doc) { var this$1 = this; if ( doc === void 0 ) doc = document; this.listeners = []; this.doc = doc; this.hr = null; var fireEvent = function (event, arg) { try { for (var i = 0; i < this$1.listeners.length; i++) { var handler = this$1.listeners[i][event]; if (handler) { handler(arg); } } } catch (e) { handleError(e); this$1.observer.disconnect(); this$1.observer.observe = doNothing; throw e; } }; this.processRecords = function (mutations, observer) { observer.disconnect(); if (!this$1.hr) { this$1.hr = findHr(mutations); if (this$1.hr) { fireEvent("onHr", this$1.hr); } } if (this$1.hr) { fireEvent("onProgress", doc.body.lastChild); } this$1.observe(); }; this.observer = this.makeMutationObserver(this.processRecords); if (doc.body) { this.first = function () { this$1.hr = doc.body.querySelector("body > hr"); if (this$1.hr) { fireEvent("onHr", this$1.hr); fireEvent("onProgress", doc.body.lastChild); } this$1.first = null; }; } loaded.then(function () { this$1.observer.observe = doNothing; var records = this$1.observer.takeRecords(); if (records.length) { console.error(records.length); this$1.processRecords(records, this$1.observer); } this$1.observer.disconnect(); fireEvent("onLoaded"); }); } Observer.prototype.observe = function() { if (this.doc.body) { if (this.first) { this.first(); } this.observer.observe(this.doc.body, {childList: true}); } else { this.observer.observe(this.doc.documentElement, { childList: true, subtree: true, }); } }; Observer.prototype.addListener = function(listener) { this.listeners.push(listener); }; Observer.prototype.makeMutationObserver = function(callback) { return new MutationObserver(callback); }; var waitForDomContentLoaded = function () { return ready({capture: true}); }; function ajax(options) { options = options || {}; var type = options.type || "GET"; var url = options.url || location.href; var data = options.data || {}; url = url.replace(/#.*$/, ""); for (var key in data) { url += "&" + encodeURIComponent(key) + "=" + encodeURIComponent(data[key]); } url = url.replace(/[&?]{1,2}/, "?"); return new Promise(function(resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open(type, url); xhr.overrideMimeType("text/html; charset=windows-31j"); xhr.onload = function() { if (xhr.status === 200) { resolve(xhr.response); } else { reject(new Error(xhr.statusText)); } }; xhr.onerror = function() { reject(new Error("Network Error")); }; xhr.send(); }); } var fetch = function (options) { return ajax(options).then(wrapWithDiv).catch(wrapErrorWithDiv); }; function wrapWithDiv(html) { var div = document.createElement("div"); div.innerHTML = html; return div; } function wrapErrorWithDiv(error) { var div = document.createElement("div"); div.textContent = error; return div; } var AfterFetch = { hasOP: function() { return true; }, run: function(_contaienr) { var after = this.concurrent(); return after.then(function(afters) { return {afters: afters, befores: []}; }); }, }; var BothFetch = { hasOP: function(container) { return this.q.hasOP(container); }, run: function(container) { var after = this.concurrent(); var before = this.sequence(container); return Promise.all([after, before]).then(function(args) { return {afters: args[0], befores: args[1]}; }); }, }; /** * @param {Query} * @param {number} */ function Fetch(q, now) { this.q = q; this.now = now || Date.now(); var extend; if (q.get("sv")) { extend = AfterFetch; } else { extend = BothFetch; } Object.assign(this, extend); } Fetch.prototype.getAfterDates = function() { return this.getThese7Dates().filter(function isAfterTodate(date) { return date > this.q.getDayAsNumber(); }, this); }; Fetch.prototype.getBeforeDates = function() { return this.getThese7Dates().filter(function isBeforeTodate(date) { return date < this.q.getDayAsNumber(); }, this); }; Fetch.prototype.getThese7Dates = function() { var this$1 = this; var dates = []; var ONE_DAY = 24 * 60 * 60 * 1000; var fill = function(n) { return n < 10 ? "0" + n : n; }; for (var i = 0; i < 7; i++) { var back = new Date(this$1.now - ONE_DAY * i); var year = back.getFullYear(); var month = fill(back.getMonth() + 1); var date = fill(back.getDate()); dates.push("" + year + month + date); } return dates; }; Fetch.prototype.fetch = function(date) { var ff = date + ".dat"; return fetch({url: "bbs.cgi", data: this.q.queryFor(ff)}).then(function (div) { div.ff = ff; return div; }); }; Fetch.prototype.sequence = function(container) { var divs = []; var fetch$$1 = this.fetch.bind(this); var hasOP = this.hasOP.bind(this); var sequence = this.getBeforeDates().reduce(function(sequence, date) { return sequence.then(function(done) { if (done) { return done; } return fetch$$1(date).then(function(div) { divs.push(div); return hasOP(div); }); }); }, Promise.resolve(hasOP(container))); return sequence.then(function() { return divs; }); }; Fetch.prototype.concurrent = function() { return Promise.all(this.getAfterDates().map(this.fetch.bind(this))); }; function Info() { var el = document.createElement("span"); el.id = "info"; return el; } function StackLog(config, q, body, view) { this.config = config; this.q = q; this.body = body; this.view = view; } StackLog.prototype = { container: function() { if (!document.body) { throw new Error("no body"); } var el = document.createElement("div"); el.id = "container"; var info = new Info(); el.appendChild(info); return {container: el, info: info}; }, shouldComplement: function() { return this.q.shouldComplement(this.body); }, complement: function() { if (this.shouldComplement()) { var gui = this.container(); var container = gui.container; var info = gui.info; info.innerHTML = "<strong>" + this.q.ff + "以外の過去ログを検索中...</strong>"; this.body.insertBefore(container, this.body.firstChild); return this.makeFetch() .run(this.body) .then(this.addExtraLog.bind(this, container)) .then(function() { info.textContent = ""; }); } }, makeFetch: function() { return new Fetch(this.q); }, addExtraLog: function(container, doms) { var view = this.view; var wrap = (function() { var wrap = view.wrapOne.bind(view); return function(f) { Array.prototype.forEach.call(f.querySelectorAll("a[name]"), wrap); return f; }; })(); var f = document.createDocumentFragment(); function append(html) { f.insertAdjacentHTML("beforeend", html); } function format(f, div) { var numberOfPosts = div.querySelectorAll("a[name]").length; append("<h1>" + div.ff + "</h1>"); if (numberOfPosts) { f.appendChild(wrap(div)); append("<h3>" + numberOfPosts + "件見つかりました。</h3>"); } else { append("<hr>"); append("<h3>指定されたスレッドは見つかりませんでした。</h3><hr>"); } return f; } if (doms.befores.length) { append("<hr>"); } f = doms.befores.reduceRight(format, f); append("<hr>"); append("<h1>" + this.q.ff + "</h1>"); this.body.insertBefore(f, container.nextSibling); f = doms.afters.reduceRight(format, f); this.body.appendChild(f); }, }; var next = function (type) { return function (nodeName) { return function (node) { while ((node = node[type])) { if (node.nodeName === nodeName) { return node; } } }; }; }; var nextElement = next("nextElementSibling"); var a = document.createElement("a"); a.href = ">"; var gt = a.outerHTML === '<a href=">"></a>'; var replacer = function (match) { var href = match.replace(/"/g, """); if (gt) { href = href.replace(/>/g, ">").replace(/</g, "<"); } return ("<a href=\"" + href + "\" target=\"link\" rel=\"noreferrer noopener\">" + match + "</a>"); }; var relinkify = function(url) { return url.replace( /(?:https?|ftp|gopher|telnet|whois|news):\/\/[\x21-\x7e]+/gi, replacer ); }; var IS_FIREFOX = typeof InstallTrigger !== "undefined"; var IS_GM = typeof GM_setValue === "function"; var IS_GM4 = typeof GM !== "undefined"; var IS_EXTENSION = !IS_GM && !IS_GM4; var IS_USAMIN = location.hostname === "usamin.elpod.org" || (location.protocol === "file:" && /usamin/.test(location.pathname)); function Post(id) { this.id = id; this.parent = null; // {Post} this.child = null; // {Post} this.next = null; // {Post} this.isNG = null; } Post.collectEssestialParts = function() { var nextFont = nextElement("FONT"); var nextB = nextElement("B"); var nextBlockquote = nextElement("BLOCKQUOTE"); return function collectElements(a) { var header = nextFont(a); var name = nextB(header); var info = nextFont(name); var blockquote = nextBlockquote(info); var pre = blockquote.firstElementChild; var title = header.firstChild; var resButton = info.firstElementChild; var threadButton = info.lastElementChild; var threadUrl = threadButton.href; return { el: { anchor: a, blockquote: blockquote, pre: pre, title: title, name: name, info: info, resButton: resButton, posterButton: resButton.nextElementSibling, threadButton: threadButton, }, name: name.innerHTML, title: title.innerHTML, text: pre.innerHTML, threadUrl: threadUrl, threadId: /&s=([^&]+)/.exec(threadUrl)[1], }; }; }; Post.breakdownPre = function(html, id) { var parentId, parentDate; var text = html .replace(/<\/?font[^>]*>/gi, "") .replace(/\r\n?/g, "\n") .replace(/\n$/, ""); 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" target="link">$1</a>' ); } var candidate = text; var reference = /\n\n<a href="h[^"]+&s=((?!0)\d+)&r=[^"]+">参考:([^<]+)<\/a>$/.exec( text ) || /\n\n<a href="#((?!0)\d+)">参考:([^<]+)<\/a>$/.exec(text); if (reference) { var assign; (assign = reference, parentId = assign[1], parentDate = assign[2]); if (+id <= parentId) { parentId = null; } text = text.slice(0, reference.index); } // リンク欄を使ったリンクを落とす var url = /\n\n<[^<]+<\/a>$/.exec(text); if (url) { text = text.slice(0, url.index); } // 自動リンクがオフかつURLみたいのがあったら if (!text.includes("<") && text.includes(":")) { // 自動リンクする candidate = relinkify(text) + (url ? url[0] : "") + (reference ? reference[0] : ""); } candidate = candidate.replace( /target="link">/g, 'target="link" rel="noreferrer noopener">' ); return { text: candidate, parentId: parentId, parentDate: parentDate, }; }; Post.makePosts = function(context) { return IS_USAMIN ? Post.makePostsUsamin(context) : Post.makePostsMain(context); }; Post.makePostsMain = function(context) { var posts = []; var as = context.querySelectorAll("a[name]"); var font = nextElement("FONT"); var b = nextElement("B"); var blockquote = nextElement("BLOCKQUOTE"); for (var i = 0, len = as.length; i < len; i++) { var a = as[i]; var post = new Post(a.name); posts.push(post); var header = font(a); post.title = header.firstChild.innerHTML; var named = b(header); post.name = named.innerHTML; var info = font(named); post.date = info.firstChild.nodeValue.trim().slice(4); //「投稿日:」削除 post.resUrl = info.firstElementChild.href; post.threadUrl = info.lastElementChild.href; post.threadId = /&s=([^&]+)/.exec(post.threadUrl)[1]; if (info.childElementCount === 3) { post.posterUrl = info.firstElementChild.nextElementSibling.href; } else { post.posterUrl = null; } var body = blockquote(info); var pre = body.firstElementChild; var env = font(pre); if (env) { post.env = env.firstChild.innerHTML; // font > i > env } var ref = Post.breakdownPre( pre.innerHTML, post.id ); var text = ref.text; var parentId = ref.parentId; var parentDate = ref.parentDate; post.text = text; if (parentId) { post.parentId = parentId; post.parentDate = parentDate; } } Post.sortByTime(posts); return posts; }; Post.makePostsUsamin = function(context) { var as = context.querySelectorAll("a[id]"); var nextPre = nextElement("PRE"); var nextFontOrB = function (node) { while ((node = node.nextElementSibling)) { var name = node.nodeName; if (name === "FONT" || name === "B") { return node; } } }; var posts = Array.prototype.map.call(as, function (a) { var post = new Post(a.id); var header = nextFontOrB(a); if (header.size === "+1") { post.title = header.firstChild.innerHTML; header = nextFontOrB(header); } if (header.tagName === "B") { post.name = header.innerHTML; header = nextFontOrB(header); } var info = header; post.date = info.firstChild.nodeValue.trim(); var ref = info.children; var thread = ref[0]; var zengo = ref[1]; var only = ref[2]; post.threadUrl = thread.href; post.zengo = zengo.href; post.only = only.href; post.site = info.lastChild.textContent; var pre = nextPre(info); var ref$1 = Post.breakdownPre( pre.innerHTML, post.id ); var text = ref$1.text; var parentId = ref$1.parentId; var parentDate = ref$1.parentDate; post.text = text; if (parentId) { post.parentId = parentId; post.parentDate = parentDate; } return post; }); Post.sortByTime(posts); // usaminではthreadIdがないのでつける if (posts.length) { var opId = posts[posts.length - 1].id; posts.forEach(function (post) { return (post.threadId = opId); }); } 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(_, url) { return relinkify(url); }; Post.checkNG = function(ng, post) { var isNG = false; if (ng.word) { isNG = ng.word.test(post.text); } if (!isNG && ng.handle) { isNG = isNG || ng.handle.test(post.name); isNG = isNG || ng.handle.test(post.title); } post.isNG = isNG; return post; }; Post.wantsParent = function(post) { return post.parentId; }; Post.isOrphan = function(post) { return post.parent === null && post.parentId; }; Post.isRootCandidate = function(post) { return post.parent === null; }; Post.mayHaveParent = function(post) { return post.mayHaveParent(); }; Post.isClean = function(post) { return !post.rejectLevel; }; Post.prototype = { id: "", // {string} /^\d+$/ title: " ", // {string} name: " ", // {string} date: "", // {string} resUrl: "", // {string} threadUrl: "", // {string} threadId: "", // {string} posterUrl: "", // {string} site: "", // {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.hasDefaultReference()) { return this.text.slice(0, this.text.lastIndexOf("\n\n")); //参考と空行を除去 } return this.text; }, hasDefaultReference: function() { var parent = this.parent; if (!parent) { return false; } if (parent.date === this.parentDate) { return true; } var ref = /^(\d+)\/(\d+)\/(\d+) \((月|火|水|木|金|土|日)\) (\d+):(\d+):(\d+)$/.exec( parent.date ) || []; var _ = ref[0]; var year = ref[1]; var month = ref[2]; var day = ref[3]; var dow = ref[4]; var hour = ref[5]; var minute = ref[6]; var second = ref[7]; return ( this.parentDate === (year + "/" + month + "/" + day + "(" + dow + ")" + hour + "時" + minute + "分" + second + "秒") ); }, computeQuotedText: function() { var lines = this.text .replace(/> >.*\n/g, "") //target属性がないのは参考リンクのみ .replace(/<a href="[^"]+">参考:.*<\/a>/i, "") // <A href=¥S+ target=¥"link¥">(¥S+)<¥/A> .replace( /<a href="[^"]+" target="link"(?: rel="noreferrer noopener")?>([^<]+)<\/a>/gi, 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?/gm, "$1") .replace(/\n$/, "") .replace(/^[ \n\r\f\t]*$/gm, "$&\n$&"); //TODO 引用と本文の間に一行開ける //text = text.replace(/((?:> .*\n)+)(.+)/, "$1\n$2"); //replace(/^(?!> )/m, "\n$&"); return text; // + "\n\n"; }, textCandidateLooksValid: function() { return ( this.getText() .replace(/^> .*/gm, "") .trim() !== "" ); }, textBonus: 2, dateCandidate: function() { return this.parentDate; }, dateCandidateLooksValid: function(candidate) { return /^\d{4}\/\d{2}\/\d{2}\(.\)\d{2}時\d{2}分\d{2}秒$/.test(candidate); }, dateBonus: 100, hasQuote: function() { return /^> /m.test(this.text); }, mayHaveParent: function() { return this.isRead && !this.isOP() && this.hasQuote(); }, adoptAsEldestChild: function(childToBeAdopted) { var child = this.child; if (child) { childToBeAdopted.next = child; } this.child = childToBeAdopted; childToBeAdopted.parent = this; }, getKeyForOwnParent: function() { return this.parentId; }, }; function Popup(config, image, body) { body = body || document.body; this.waitingMetadata = null; this.handleEvent = function(e) { var type = e.type; if ( type === "keydown" && !/^Esc(?:ape)?$/.test(e.key) && e.keyIdentifier !== "U+001B" ) { // not ESC return; } if (type === "mouseout" && e.relatedTarget.closest(".popup")) { return; } this.doHandleEvent(); }; this.doHandleEvent = function() { var popup = document.getElementById("image-view"); if (popup) { popup.parentNode.removeChild(popup); } Array.prototype.slice .call(document.getElementsByClassName("popup")) .forEach(function(el) { el.classList.remove("popup"); }); this.removeEventListeners(body); if (this.waitingMetadata) { clearTimeout(this.waitingMetadata); } }; this.addEventListeners = function() { this.toggleEventListeners("add"); }; this.removeEventListeners = function() { this.toggleEventListeners("remove"); }; this.toggleEventListeners = function(toggle) { ["click", "keydown", "mouseout"].forEach(function(type) { body[toggle + "EventListener"](type, this); }, this); }; function getRatio(natural, max) { if (/^\d+$/.test(max) && natural > max) { return +max / natural; } else { return 1; } } this.popup = function() { var isBestFit = config.popupBestFit; var viewport = document.compatMode === "BackCompat" ? document.body : document.documentElement; var windowHeight = viewport.clientHeight; var windowWidth = viewport.clientWidth; var imageView = document.createElement("figure"); imageView.id = "image-view"; imageView.classList.add("popup"); imageView.style.visibility = "hidden"; imageView.innerHTML = '<figcaption><span id="percentage"></span>%</figcaption>'; // bodyに追加することでimage-orientationが適用され // natural(Width|Height)以外の.*{[wW]idth|[hH]eight)が // EXIFのorientationが適用された値になる imageView.appendChild(image); body.appendChild(imageView); var width = image.offsetWidth; var height = image.offsetHeight; var marginHeight = Math.round(imageView.getBoundingClientRect().height) - height; var maxWidth = config.popupMaxWidth || (isBestFit ? windowWidth : width); var maxHeight = config.popupMaxHeight || (isBestFit ? windowHeight - marginHeight : height); var ratio = Math.min( getRatio(width, maxWidth), getRatio(height, maxHeight) ); var percentage = Math.floor(ratio * 100); var bgcolor = ratio < 0.5 ? "red" : ratio < 0.9 ? "blue" : "green"; // 丸めないと画像が表示されないことがある var imageHeight = Math.floor(height * ratio) || 1; var imageWidth = Math.floor(width * ratio) || 1; imageView.style.display = "none"; image.height = imageHeight; image.width = imageWidth; imageView.querySelector("#percentage").textContent = percentage; imageView.style.cssText = "background-color: " + bgcolor; }; this.waitAndOpen = function() { if ( !image.complete && image.naturalWidth === 0 && image.naturalHeight === 0 ) { this.waitingMetadata = setTimeout(this.waitAndOpen.bind(this), 50); } else { this.waitingMetadata = null; this.popup(); } }; } function Preload(head) { this.preloads = Object.create(null); this.head = head || document.head; var DOMTokenListSupports = function(tokenList, token) { if (!tokenList || !tokenList.supports) { return; } try { return tokenList.supports(token); } catch (e) { if (e instanceof TypeError) { console.log("The DOMTokenList doesn't have a supported tokens list"); } else { console.error("That shouldn't have happened"); } } }; this.isSupported = DOMTokenListSupports( document.createElement("link").relList, "preload" ); } Preload.prototype.fetch = function(url) { if (!this.isSupported || this.isFetched(url)) { return; } var link = document.createElement("link"); link.rel = "preload"; link.as = "image"; link.href = url; this.head.appendChild(link); this.preloads[url] = true; }; Preload.prototype.isFetched = function(url) { return this.preloads[url]; }; function memoize(fn) { var cache = {}; return function(arg) { if (!cache.hasOwnProperty(arg)) { cache[arg] = fn(arg); } return cache[arg]; }; } var GM_xmlhttpRequest$1 = function (options) { return GM_xmlhttpRequest(options); }; var GM$1 = { xmlHttpRequest: function (options) { return GM.xmlHttpRequest(options); }, }; var checkAnimation = function (imgURL) { return new Promise(function (resolve) { var options = { url: imgURL.replace(/\w+$/, "pch"), type: "HEAD", method: "HEAD", onload: function (response) { return resolve(response.status === 200); }, }; if (IS_GM4) { GM$1.xmlHttpRequest(options); } else if (IS_GM) { GM_xmlhttpRequest$1(options); } else if (IS_EXTENSION) { ajax(options).then(function () { return resolve(true); }, function () { return resolve(false); }); } }); }; var mayHaveSmallerImage = /^http:\/\/misao\.(?:mixh|on\.arena\.ne)\.jp\/c\/up\/misao\d+\.\w+$/; var small = function(href) { return mayHaveSmallerImage.test(href) ? href.replace(/up\//, "up/pixy_") : href; }; var animation = function(href) { var mayHaveAnimation = /^(http:\/\/misao\.(?:mixh|on\.arena\.ne)\.jp\/c\/)up\/(misao0*\d+)\.(?:png|jpg)$/; var ref = mayHaveAnimation.exec(href) || []; var directory = ref[1]; var id = ref[2]; if (id) { var animationURL = directory + "upload.cgi?m=A&id=" + id.replace(/misao0*/, ""); return {id: id, href: animationURL}; } }; var sites = { sw: [ { name: "misao", prefix: "http://misao.mixh.jp/c/", urls: function urls(href) { return { small: this.small(href), animation: this.animation(href), }; }, small: small, animation: animation, }, { name: "misao-arena", prefix: "http://misao.on.arena.ne.jp/c/", urls: function urls(href) { return { small: this.small(href), animation: this.animation(href), }; }, small: small, animation: animation, }, { name: "betanya", prefix: "http://komachi.betanya.com/uploader/stored/", urls: function (href) { return ({ small: href, }); }, } ], otherSites: [ { name: "imgur", prefix: /^https?:\/\/(?:i\.)?imgur\.com\/[^/]+$/, urls: function urls(href) { var original = href.replace(/^https?:\/\/(?:i\.)?/, "https:/i."); var thumbnail = original.replace(/\.\w+$/, "t$&"); return { small: thumbnail, }; }, }, { name: "twimg", prefix: /^https?:\/\/pbs\.twimg\.com\/media\/[\w_-]+\.\w+/, suffix: /(?::(?:orig|large|medium|small|thumb))?$/, urls: function urls(href) { var ref = this.prefix.exec(href) || []; var hrefWithoutTag = ref[0]; if (!hrefWithoutTag) { return; } return { original: hrefWithoutTag + ":orig", small: hrefWithoutTag + ":thumb", }; }, }, { name: "any", suffix: /^[^?#]+\.(?:jpe?g|png|gif|bmp)(?:[?#]|$)/i, urls: function (original) { return ({original: original}); }, } ], }; function Thumbnail(config) { this.config = config; this.preload = new Preload(); var animationChecker = memoize(checkAnimation); // ポップアップを消した時、カーソルがサムネイルの上にある this.isClosedAboveThumbnail = function(e) { var relatedTarget = e.relatedTarget; //firefox: if (relatedTarget === null) { return true; } //opera12 if (relatedTarget instanceof HTMLBodyElement) { return true; } //chrome if ( relatedTarget.closest("#image-view") && !document.getElementById("image-view") ) { return true; } }; function setNote(a, text) { var note = a.nextElementSibling; // span.noteがない if (!note || !note.classList.contains("note")) { note = document.createElement("span"); note.className = "note"; a.parentNode.insertBefore(note, a.nextSibling); } note.textContent = text; } this.downloading = function(image, a) { var pending = true; var complete = function(success) { pending = false; if (success) { var note = a.nextElementSibling; if (note && note.classList.contains("note")) { note.parentNode.removeChild(note); } } else { setNote(a, "404?画像ではない?"); } }; image.addEventListener("load", complete.bind(null, true)); image.addEventListener("error", complete.bind(null, false)); setTimeout(function() { if (pending) { setNote(a, "ダウンロード中"); } }, 100); }; this.handleEvent = function(e) { if (this.isClosedAboveThumbnail(e)) { return; } var a = e.currentTarget; // ポップアップからサムネイルに帰ってきた if (a.classList.contains("popup")) { return; } var image = new Image(); image.referrerPolicy = "no-referrer"; this.downloading(image, a); image.classList.add("image-view-img"); image.src = a.href; a.classList.add("popup"); var popup = new Popup(config, image); popup.addEventListeners(); popup.waitAndOpen(); }; this.thumbnailLink = function(href, container) { var site = getProperSite(href); if (!site) { return; } var ref = site.urls(href); var original = ref.original; if ( original === void 0 ) original = href; var small = ref.small; var animation = ref.animation; var thumbnailSrc = this.small(original, small); var thumbnail = this.thumbnail(original, thumbnailSrc); if (animation && config.linkAnimation) { this.checkAnimation(href, animation.id, container); thumbnail += animationHTML(animation); } if (config.shouki) { thumbnail += shouki(original); } return thumbnail; }; var getProperSite = function (href) { var site; if (/\.(?:jpe?g|png|gif|bmp)$/i.test(href)) { site = pickProperSite({ sites: sites.sw, href: href, testPrefix: startsWith, }); } if (!site && config.popupAny) { site = pickProperSite({ sites: sites.otherSites, href: href, testPrefix: test, testSuffix: test, }); } return site; }; var pickProperSite = function (ref) { var sites$$1 = ref.sites; var href = ref.href; var testPrefix = ref.testPrefix; var testSuffix = ref.testSuffix; if ( testSuffix === void 0 ) testSuffix = pass; return sites$$1.find( function (ref) { var prefix = ref.prefix; var suffix = ref.suffix; return testPrefix(href, prefix) && testSuffix(href, suffix); } ); }; var pass = function () { return true; }; var startsWith = function (href, prefix) { return !prefix || href.startsWith(prefix); }; var test = function (href, test) { return !test || test.test(href); }; this.small = function(original, small) { if (!small) { return undefined; } if (original === small) { return small; } if (!config.thumbnailPopup) { return small; } this.preload.fetch(original); if (this.preload.isFetched(original)) { return small; } else { return original; } }; var a = function (href, content) { return ("<a href=\"" + href + "\" target=\"link\" class=\"thumbnail\">" + content + "</a>"); }; this.thumbnail = function(original, small) { if (small) { return a(original, thumbnailHTML(small)); } else { return "[" + a(original, "■") + "]"; } }; var thumbnailHTML = function (src) { return ("<img referrerpolicy=\"no-referrer\" class=\"thumbnail-img\" src=\"" + src + "\">"); }; var animationHTML = function (ref) { var id = ref.id; var href = ref.href; return "<span class=\"animation " + id + "\">" + "[<a href=\"" + href + "\" target=\"link\">A</a><span class=\"unsure\">?</span>]" + "</span>"; }; var shouki = function (href) { return ("[<a href=\"http://images.google.com/searchbyimage?image_url=" + href + "\" target=\"link\">詳</a>]"); }; this.checkAnimation = function (href, id, container) { return animationChecker(href) .then(function (isAnimation) { return (isAnimation ? ("." + id + " .unsure") : ("." + id)); }) .then(function (selector) { return container.querySelector(selector); }) .then(function (e) { return e.parentNode.removeChild(e); }); }; this.register = function(container) { var this$1 = this; var as = container.querySelectorAll("a[target]"); var has = false; var i; for (i = as.length - 1; i >= 0; i--) { var a = as[i]; var thumbnail = this$1.thumbnailLink(a.href, container); if (thumbnail) { a.insertAdjacentHTML("beforebegin", thumbnail); has = true; } } if (has && config.thumbnailPopup) { var thumbs = container.getElementsByClassName("thumbnail"); for (i = thumbs.length - 1; i >= 0; i--) { thumbs[i].addEventListener("mouseover", this$1, false); } } }; } function identity(x) { return x; } var Posts = { checkCharacterEntity: function(config, data) { var state = data.state; var post = data.post; state.hasCharacterEntity = /&#(?:\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 = ""; } //prettier-ignore var header = '<a ' + resUrl + 'class="res" target="link">■</a>' + '<span class="message-info">' + ((title === '> ' || title === ' ') && name === ' ' ? "" : '<strong>' + title + '</strong> : <strong>' + name + '</strong> #' ) + post.date + '</span>' + (resUrl && ' <a ' + resUrl + ' target="link">■</a>') + vanish + (state.hide ? ' <a href="javascript:;" class="fold">畳む</a>' : "") + (post.posterUrl ? ' <a href="' + post.posterUrl + '" target="link">★</a>' : '') + (state.hasCharacterEntity ? ' <a href="javascript:;" class="characterEntity' + (state.expandCharacterEntity ? ' characterEntityOn' : '' ) + '">文字参照</a>' : "") + ' <a href="' + post.threadUrl + '" target="link">◆</a>'; return header; }, }; var nextSibling = next("nextSibling"); function StackView(config) { this.range = document.createRange(); this.original = document.createElement("div"); this.original.className = "message original"; this.thumbnail = new Thumbnail(config); this.showButtons = document.createElement("span"); this.showButtons.className = "showOriginalButtons"; this.range.selectNodeContents(this.original); // 引数は何でもいいが何かで上書きしないとopera12で<html>...</html>が返る this.vanishButton = this.range.createContextualFragment( '<a href="javascript:;" class="vanish">消</a> ' ); this.showNGButton = this.range.createContextualFragment( '<a href="javascript:;" class="showNG">NG</a> ' ); this.showThreadButton = this.range.createContextualFragment( '<a href="javascript:;" class="showThread">非表示解除</a> ' ); this.needToWrap = config.useVanishThread || config.keyboardNavigation || (window.Intl && Intl.v8BreakIterator); // or blink this.useThumbnail = config.thumbnail; this.utterlyVanishNGThread = config.utterlyVanishNGThread; this.utterlyVanishNGStack = config.utterlyVanishNGStack; this.nextComment = nextSibling("#comment"); this.makePost = Post.collectEssestialParts(); this.config = config; this.ng = config.ng; this.markNG = this.createMarkNG(config.ng); } StackView.prototype = { setRange: function(start, end) { this.range.setStartBefore(start); this.range.setEndAfter(end); }, deleteMessage: function(post) { var el = post.el; var end = this.nextComment(el.blockquote); this.setRange(el.anchor, end); this.range.deleteContents(); }, wrapMessage: function(post) { var el = post.el; var wrapper = this.original.cloneNode(false); this.setRange(el.anchor, el.blockquote); this.range.surroundContents(wrapper); if (this.config.useVanishThread) { var thread = el.threadButton; thread.parentNode.insertBefore(this.vanishButton.cloneNode(true), thread); wrapper.dataset.threadId = post.threadId; } return wrapper; }, createMarkNG: function(ng) { var word = ng.wordg; var handle = ng.handleg; var markNG = Posts.markNG(word); var markNGHeader = Posts.markNGHeader(handle); return function(post) { var el = post.el; if (word) { var data = { value: post.text, post: post, }; markNG(data); el.pre.innerHTML = data.value; } if (handle) { el.name.innerHTML = markNGHeader(post.name); el.title.innerHTML = markNGHeader(post.title); } }; }, wrapOne: function(a) { var post = this.makePost(a); var buttons = []; if (this.vanish(post, buttons) === false) { return; } if (this.vanishByNG(post, buttons) === false) { return; } this.buildMessage(post, buttons); this.registerThumbnail(post); }, buildMessage: function(post, buttons) { if (this.needToWrap || buttons.length) { var wrapper = this.wrapMessage(post); if (buttons.length) { wrapper.classList.add("hidden"); var showButtons = wrapper.parentNode.insertBefore( this.showButtons.cloneNode(false), wrapper ); buttons.forEach(function(button) { showButtons.appendChild(button.cloneNode(true)); }); } } }, vanish: function(post, buttons) { if (this.config.useVanishThread) { if (this.config.vanishedThreadIDs.indexOf(post.threadId) !== -1) { if (this.utterlyVanishNGThread) { this.deleteMessage(post); return false; } else { buttons.push(this.showThreadButton); } } } }, vanishByNG: function(post, buttons) { var ng = this.ng; if (ng.isEnabled) { Post.checkNG(ng, post); if (post.isNG) { if (this.utterlyVanishNGStack) { this.deleteMessage(post); return false; } else if (this.config.NGCheckMode) { this.markNG(post); } else { buttons.push(this.showNGButton); } } } }, registerThumbnail: function(post) { if (this.useThumbnail) { this.thumbnail.register(post.el.pre); } }, }; var Range = (function () { function anonymous() { this.range = this.makeRange(); } anonymous.prototype.makeRange = function makeRange () { return document.createRange(); }; anonymous.prototype.setEndAfter = function setEndAfter (node) { this.range.setEndAfter(node); }; anonymous.prototype.setStartAfter = function setStartAfter (node) { this.range.setStartAfter(node); }; anonymous.prototype.setStartBefore = function setStartBefore (node) { this.range.setStartBefore(node); }; anonymous.prototype.extractContents = function extractContents () { return this.range.extractContents(); }; anonymous.prototype.createContextualFragment = function createContextualFragment (html) { return this.range.createContextualFragment(html); }; return anonymous; }()); var clearVanishedIds = function(config, method, button) { config[method](); button.firstElementChild.innerHTML = "0"; }; var loop = function (func, array) { return new Promise(function (resolve, reject) { var i = 0; var length = array.length; (function loop() { var t = Date.now(); do { if (i === length) { resolve(); return; } try { func(array[i++]); } catch (e) { reject(e); return; } } while (Date.now() - t < 20); setTimeout(loop, 0); })(); }); }; var on = function (el, event, selector, callback) { el.addEventListener(event, function (e) { if (e.target.closest(selector)) { if (callback.handleEvent) { callback.handleEvent(e); } else { callback(e); } } }); }; var Stack = { common: function(config, body) { Stack.addEventListener(config, body); Stack.configButton(config, body); Stack.accesskey(config, body); }, accesskey: function(config, body) { var midoku = body.querySelector('input[name="midokureload"]'); if (midoku) { midoku.accessKey = config.accesskeyReload; } }, addEventListener: function(config, body) { on(body, "click", ".showNG", this.showNG); on(body, "click", ".showThread", this.showThread.bind(this, config)); on( body, "click", ".clearVanishedThreadIDs", this.clearVanishedThreadIDs.bind(this, config) ); on(body, "click", ".vanish", this.vanish.bind(this, config)); }, showNG: function(e) { Stack.removeButtons(e.target.parentNode.nextElementSibling); }, showThread: function(config, e) { e.preventDefault(); var buttons = e.target.parentNode; var thisMessage = buttons.nextElementSibling; var id = thisMessage.dataset.threadId; var restore = Stack.savePosition(buttons); config.removeVanishedThread(id); Array.prototype.filter .call(document.querySelectorAll(".original"), function(message) { return message.dataset.threadId === id; }) .forEach(function(message) { if (message === thisMessage) { restore(); } Stack.removeButtons(message); }); }, clearVanishedThreadIDs: function(config, e) { e.preventDefault(); clearVanishedIds(config, "clearVanishedThreadIDs", e.target); }, removeButtons: function(message) { var buttons = message.previousElementSibling; message.classList.remove("hidden"); buttons.parentNode.removeChild(buttons); }, vanish: function(config, e) { e.preventDefault(); var message = e.target.closest(".original"); var id = message.dataset.threadId; var data = e.target.classList.contains("revert") ? Stack.doRevertVanish() : Stack.doVanish(); var restore = Stack.savePosition(message); config[data.type + "VanishedThread"](id); Array.prototype.filter .call(document.querySelectorAll(".original"), function(message) { return message.dataset.threadId === id; }) .forEach(function(message) { message.classList.toggle("message"); message.querySelector("blockquote").classList.toggle("hidden"); var button = message.querySelector(".vanish"); button.classList.toggle("revert"); button.textContent = data.text; }); restore(); }, doVanish: function() { return { text: "戻", type: "add", }; }, doRevertVanish: function() { return { text: "消", type: "remove", }; }, savePosition: function(element) { var top = element.getBoundingClientRect().top; return function restorePosition() { window.scrollTo( window.pageXOffset, window.pageYOffset + element.getBoundingClientRect().top - top ); }; }, configButton: function(config, body) { var setup = body.querySelector('input[name="setup"]'); if (setup) { var button = ' <a href="javascript:;" id="openConfig">★くわツリービューの設定★</a>'; if (config.vanishedThreadIDs.length) { button += ' 非表示解除(<a class="clearVanishedThreadIDs" href="javascript:;"><span class="length">' + config.vanishedThreadIDs.length + "</span>スレッド</a>)"; } setup.insertAdjacentHTML("afterend", button); } }, render: function(config, body, view) { if ( config.keyboardNavigation || config.thumbnail || config.ng.isEnabled || config.useVanishThread ) { var anchors = body.querySelectorAll("body > a[name]"); var wrap = view.wrapOne.bind(view); if (IS_FIREFOX) { try { var html = body.parentNode; html.removeChild(body); anchors.forEach(wrap); } finally { html.appendChild(body); } } else { return loop(wrap, anchors); } } }, tweakFooter: function(config, container, opt_done) { if (this.needsToTweakFooter(config)) { var insertFooter = this.doTweakFooter(container); return Promise.resolve(opt_done).then(insertFooter); } }, needsToTweakFooter: function(config) { return ( (config.ng.isEnabled && config.utterlyVanishNGStack) || (config.useVanishThread && config.utterlyVanishNGThread) ); }, doTweakFooter: function(container) { var i = container.querySelector("p i"); if (!i) { return doNothing; } var numPostsInfo = i.parentNode; var hr = nextElement("HR")(numPostsInfo); var insertionPoint = hr.nextSibling; var range = document.createRange(); range.setStartBefore(numPostsInfo); range.setEndAfter(hr); var footer = range.extractContents(); return function insertBack() { if (!footer.querySelector('table input[name="pnext"]')) { return; } footer.removeChild(numPostsInfo); insertionPoint.parentNode.insertBefore(footer, insertionPoint); }; }, }; var StreamStackView = function StreamStackView(args) { Object.assign(this, args); this.range = new Range(); this.main = document.createElement("main"); this.main.id = "qtv-stack"; }; StreamStackView.prototype.init = function init () { Stack.common(this.config, this.body); this.buffer.insertBefore(this.main); }; StreamStackView.prototype.finish = function finish (buffer) { Stack.tweakFooter(this.config, buffer); this.body.appendChild(buffer); return Promise.resolve(this.log.complement()).then(this.done); }; StreamStackView.prototype.render = function render (buffer) { var ref = this; var range = ref.range; var view = ref.view; var main = ref.main; var firstComment = ref.firstComment; var comment; while ((comment = firstComment(buffer))) { range.setStartBefore(buffer.firstChild); range.setEndAfter(comment); // 以下のように一つずつやるとO(n) // 一気に全部やるとO(n^2) // chrome57の時点で一気にやってもO(n)になってる view.wrapOne(buffer.querySelector("a[name]")); main.appendChild(range.extractContents()); } }; StreamStackView.prototype.firstComment = function firstComment (buffer) { var first = buffer.firstChild; while (first) { if (first.nodeType === Node.COMMENT_NODE && first.nodeValue === " ") { return first; } first = first.nextSibling; } return null; }; var ToggleOriginal = function ToggleOriginal(original) { this.toggle = document.createElement("div"); this.button = this.createToggleButton(); this.stack = this.createStackArea(original); this.toggle.appendChild(this.button); this.toggle.appendChild(this.stack); }; ToggleOriginal.prototype.getUI = function getUI () { return this.toggle; }; ToggleOriginal.prototype.createStackArea = function createStackArea (original) { var stack = document.createElement("div"); stack.id = "qtv-stack"; stack.hidden = true; stack.appendChild(original); return stack; }; ToggleOriginal.prototype.createToggleButton = function createToggleButton () { var this$1 = this; var range = new Range(); var fragment = range.createContextualFragment( '<div style="text-align:center"><a class="toggleOriginal" href="javascript:;">元の投稿の表示する(時間がかかることがあります)</a></div><hr>' ); fragment .querySelector("a") .addEventListener("click", function (e) { return this$1.toggleOriginal(e); }); return fragment; }; ToggleOriginal.prototype.toggleOriginal = function toggleOriginal (e, win) { if ( win === void 0 ) win = window; e.preventDefault(); e.stopPropagation(); this.stack.hidden = !this.stack.hidden; win.scrollTo( win.pageXOffset, e.target.getBoundingClientRect().top + win.pageYOffset ); }; var storageIsAvailable = function (type, win) { if ( win === void 0 ) win = window; // https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API#Feature-detecting_localStorage try { var storage = win[type], x = "__storage_test__"; storage.setItem(x, x); storage.removeItem(x); return true; } catch (e) { return false; } }; var getStorage = function (config) { if (IS_USAMIN) { return nullStorage; } if (config.useVanishMessage && storageIsAvailable("localStorage")) { return localStorage; } if (storageIsAvailable("sessionStorage")) { return sessionStorage; } return nullStorage; }; var nullStorage = { getItem: function getItem() { return null; }, setItem: doNothing, }; function createPostParent(config) { var storage; var data; var saveAsyncIfNeeded = function (posts) { if (!posts.length) { return; } storage = storage || getStorage(config); load(); var changed; for (var i = 0; i < posts.length; i++) { var ref = posts[i]; var id = ref.id; var parentId = ref.parentId; if (data.hasOwnProperty(id)) { break; } if (parentId && parentId.length > 20) { parentId = null; } data[id] = parentId; changed = true; } if (changed) { saveAsync(data); } }; var load = function () { data = data || JSON.parse(storage.getItem("postParent")) || {}; }; var saveAsync = function (data) { return setTimeout(save, 0, data); }; var save = function (data) { return storage.setItem("postParent", JSON.stringify(data)); }; var TEN_SECONDS_LATER = 10 * 1000; var cleanUpLater = function () { return setTimeout(cleanUp, TEN_SECONDS_LATER, data); }; var cleanUp = function (data) { if (!data) { return; } var ids = Object.keys(data); var limits = getLimits(); if (ids.length <= limits.upper) { return; } ids = ids.map(function (id) { return +id; }).sort(function (l, r) { return r - l; }); if (data[ids[0]] === false) { ids.shift(); } var saveData = {}; var i = limits.lower; while (i--) { saveData[ids[i]] = data[ids[i]]; } saveAsync(saveData); }; var getLimits = function () { if (config.vanishMessageAggressive && config.useVanishMessage) { return {upper: 3500, lower: 3300}; } if (config.useVanishMessage) { return {upper: 1500, lower: 1300}; } return {upper: 500, lower: 300}; }; var get = function (id) { return data[id]; }; /** * GhostPostが自身のIDを得るために子のMergedPostを渡す */ var findAsync = function (ref) { var id = ref.id; var threadId = ref.threadId; if (shouldFetch(id, threadId)) { return updateThread(threadId).then(function () { return get(id); }); } else { return Promise.resolve(get(id)); } }; var isValidIds = function (childId, threadId) { return /^(?!0)\d+$/.test(threadId) && +threadId <= +childId; }; var isActualStorage = function (storage) { return storage.removeItem; }; var shouldFetch = function (childId, threadId) { return typeof data[childId] === "undefined" && isActualStorage(storage) && isValidIds(childId, threadId); }; var updateThread = memoize(function (threadId) { return fetch({data: {m: "t", s: threadId}}) .then(Post.makePosts) .then(saveAsyncIfNeeded); } ); return { saveAsyncIfNeeded: saveAsyncIfNeeded, get: get, findAsync: findAsync, cleanUpLater: cleanUpLater, }; } var div_ = document.createElement("div"); function DOM(html) { var div = div_.cloneNode(false); div.innerHTML = html; return div.firstChild; } var compose = function () { var fns = [], len = arguments.length; while ( len-- ) fns[ len ] = arguments[ len ]; return function (x) { return fns.reduceRight(function (acc, fn) { return fn(acc); }, x); }; }; function AbstractPosts() {} AbstractPosts.prototype = { init: function(item, el) { this.el = el || document.createElement("div"); this.el.className = "messages"; this.item = item; }, getContainer: function() { return this.el; }, render: function(config) { var this$1 = this; if (this.pre) { this.pre(); } var roots = this.item; var maker = this.messageMaker(config); for (var i = 0, length = roots.length; i < length; i++) { this$1.doShowPosts(config, maker, roots[i], 1); } return this.el; }, doShowPosts: function(config, maker, post, depth) { var dm = maker(post, depth); var dc = this.getContainer(post, depth); dc.appendChild(dm); if (post.child) { this.doShowPosts(config, maker, post.child, depth + 1); } if (post.next) { this.doShowPosts(config, maker, post.next, depth); } }, checker: function(config) { var functions = [Posts.hide(config), Posts.checkNGIfRead(config.ng)]; return compose.apply(null, functions); }, text: function(config) { var markNG = Posts.markNG(config.ng.wordg); var putThumbnails = Posts.putThumbnails(config); var truncate = Posts.truncate.bind(Posts, config); var checkCharacterEntity = Posts.checkCharacterEntity.bind(Posts, config); return compose( putThumbnails, Posts.characterEntity, Posts.createDText(this.mode), Posts.prependExtension, truncate, markNG, checkCharacterEntity, Posts.checkThumbnails, Posts.makeText ); }, unfoldButton: Posts.unfoldButton, headerContents: Posts.headerContents, div: function(clazz, content) { var el = document.createElement("div"); el.className = clazz; el.innerHTML = content; return el; }, header: function(config) { var ng = config.ng; var markNGHeader = ng.handleg ? Posts.markNGHeader(ng.handleg) : identity; var classes = "message-header message-header_" + this.mode; return function(data) { var post = data.post; var state = data.state; var title = post.title; var name = post.name; if (post.isNG) { title = markNGHeader(title); name = markNGHeader(name); } var header = this.headerContents(state, config, post, name, title); return this.div(classes, header); }.bind(this); }, env: function(data) { if (!data.post.env) { return null; } var env = '<span class="env">(' + data.post.env.replace(/<br>/, "/") + ")</span>"; return this.div("extra extra_" + this.mode, this.doEnv(env, data)); }, doEnv: identity, message: function(header, text, env) { var el = document.createElement("div"); el.appendChild(header); el.appendChild(text); if (env) { el.appendChild(env); } el.className = "message message_" + this.mode; return el; }, messageMaker: function(config) { var checker = this.checker(config); var text = this.text(config); var header = this.header(config); return function(post, depth) { var dMessage; var data = checker({ post: post, value: null, state: { depth: depth, }, }); var state = data.state; if (state.hide && !post.show) { dMessage = this.div( "showMessage showMessage_" + this.mode, this.unfoldButton(data) ); } else { data = text(data); var dText = data.value; var dHeader = header(data); var dEnv = this.env(data); dMessage = this.message(dHeader, dText, dEnv); } if (config.spacingBetweenMessages) { this.setSpacer(dMessage, state.extension); } if (this.setMargin) { this.setMargin(dMessage, state.depth); } dMessage.id = post.id; dMessage.dataset.id = post.id; dMessage.post = post; return dMessage; }.bind(this); }, }; function CSSView() { this.mode = "tree-mode-css"; this.containers = null; this.pre = function() { this.containers = [{dcontainer: this.el}]; }; this.border = function(depth) { var left = depth + 0.5; //prettier-ignore return DOM('<div class="border outer" style="left:' + left + 'rem">' + '<div class="border inner" style="left:-' + left + 'rem">' + '</div></div>'); }; this.getContainer = function(post, depth) { var containers = this.containers; var container = containers[containers.length - 1]; if ("lastChildID" in container && container.lastChildID === post.id) { containers.pop(); container = containers[containers.length - 1]; } var child = post.child; if (child && child.next) { var lastChild = child; do { lastChild = lastChild.next; } while (lastChild.next); var dout = this.border(depth); container.dcontainer.appendChild(dout); container = {lastChildID: lastChild.id, dcontainer: dout.firstChild}; containers.push(container); } return container.dcontainer; }; this.setSpacer = function(el) { el.classList.add("spacing"); }; this.setMargin = function(el, depth) { el.style.marginLeft = depth + "rem"; }; } CSSView.prototype = Object.create(AbstractPosts.prototype); function ASCIIView() { this.mode = "tree-mode-ascii"; function wrapTree(tag, tree) { return "<" + tag + ' class="a-tree">' + tree + "</" + tag + ">"; } function computeExtension(config, post) { var forHeader, forText, init; var utterlyVanishMessage = config.utterlyVanishMessage; var hasNext = post.next; var tree = []; var parent = post; while ((parent = parent.parent)) { if (utterlyVanishMessage && parent.rejectLevel) { break; } tree.push(parent.next ? "|" : " "); } init = tree.reverse().join(""); if (post.isOP()) { forHeader = " "; } else { forHeader = init + (hasNext ? "├" : "└"); } forText = init + (hasNext ? "|" : " ") + (post.child ? "|" : " "); return {header: forHeader, text: forText}; } this.extension = function(config, data) { var extension = computeExtension(config, data.post); data.state.extension = { text: function(data) { data.value = data.value.replace( /^/gm, wrapTree("span", extension.text) ); return data; }, header: function(header) { return wrapTree("span", extension.header) + header; }, env: function(env) { return wrapTree("span", extension.text) + env; }, spacer: function() { return wrapTree("div", extension.text); }, }; return data; }; this.checker = function(config) { var checker = AbstractPosts.prototype.checker.apply(this, arguments); return compose(this.extension.bind(this, config), checker); }; this.setSpacer = function(el, extension) { el.insertAdjacentHTML("beforeend", extension.spacer()); }; var headerContents = AbstractPosts.prototype.headerContents; var unfoldButton = AbstractPosts.prototype.unfoldButton; this.headerContents = function(state) { return state.extension.header(headerContents.apply(null, arguments)); }; this.unfoldButton = function(data) { return data.state.extension.header(unfoldButton(data)); }; this.doEnv = function(env, data) { return data.state.extension.env(env); }; } ASCIIView.prototype = Object.create(AbstractPosts.prototype); var createView$1 = function (mode) { return new { "tree-mode-css": CSSView, "tree-mode-ascii": ASCIIView, }[mode](); }; function showThread( ref, view ) { var config = ref.config; var el = ref.el; if ( view === void 0 ) view = createView$1(config.treeMode); var mode = config.treeMode; var toggleTreeMode = mode === "tree-mode-css" && config.toggleTreeMode ? ' <a href="javascript:;" class="toggleTreeMode">●</a>' : ""; var vanishButtons = config.useVanishThread ? { // class に revert がないが、分岐に使っているのは .NGThread なので気にしないでいい true: ' <a href="javascript:;" class="vanish">戻</a>', false: ' <a href="javascript:;" class="vanish">消</a>', } : {true: "", false: ""}; function template(ref) { var thread = ref.thread; var classExtra = ref.classExtra; var date = ref.date; var contents = ref.contents; var headerExtra = ref.headerExtra; if ( headerExtra === void 0 ) headerExtra = ""; var url = "<a href=\"" + (thread.getURL()) + "\" target=\"link\">◆</a>"; return ( "<pre data-thread-id=\"" + (thread.getID()) + "\" class=\"thread " + mode + " " + classExtra + "\">" + "<div class=\"thread-header\">" + url + " 更新日:" + date + headerExtra + " " + url + (thread.getSite()) + "</div>" + contents + "</pre>" ); } var makeThreadHtml = function (ref) { var thread = ref.thread; var number = ref.number; var isVanished = ref.isVanished; return template({ thread: thread, classExtra: isVanished ? "NGThread" : "", date: thread.getAppropriateDate(), headerExtra: " 記事数:" + number + toggleTreeMode + vanishButtons[isVanished], contents: '<span class="messages"></span>', }); }; return function doShowThread(thread) { var isVanished = thread.isVanished(); if (isVanished && config.utterlyVanishNGThread) { return; } var roots = thread.computeRoots(); var number = thread.getNumber(roots); if (!number) { return; } var dthread = DOM(makeThreadHtml({thread: thread, number: number, isVanished: isVanished})); view.init(roots, dthread.lastChild); view.render(config); dthread.roots = roots; el.appendChild(dthread); }; } var showThreads = function(config, gui, threads) { gui.setInfo(" - スレッド構築中"); var el = gui.getContent(); return loop(showThread({config: config, el: el}), threads); }; function shouldMakeUrlsSearchLog(q, posts) { if (!q.shouldMakeUrlsSearchLog()) { return posts; } posts.forEach(function (post) { var ref = post.date.match(/\d+/g) || []; var year = ref[0]; var month = ref[1]; var day = ref[2]; var ff = "&ff=" + year + month + day + ".dat"; post.threadUrl += ff; //post.threadUrl.replace(/&ac=1$/, "")必要? if (post.resUrl) { post.resUrl += ff; } if (post.posterUrl) { post.posterUrl += ff; } }); return posts; } function checkNG(ng, posts) { for (var i = 0; i < posts.length; ++i) { Post.checkNG(ng, posts[i]); } } var excludeNg = function (posts) { return posts.filter(function (post) { return !post.isNG; }); }; var shouldExclude = function (config) { return !config.autovanishThread && config.utterlyVanishNGStack; }; function processNg(config, posts) { if (!config.ng.isEnabled) { return posts; } checkNG(config.ng, posts); if (shouldExclude(config)) { return excludeNg(posts); } return posts; } var fetch$1 = function (q, container) { return new Fetch(q).run(container); }; function complementMissingPostsFromLog(ref) { var q = ref.q; var gui = ref.gui; var container = ref.container; var posts = ref.posts; if (!q.shouldFetch()) { return Promise.resolve(posts); } gui.setInfoHtml(("<strong>" + (q.getLogName()) + "以外の過去ログを検索中...</strong>")); var makePostsAndConcat = function (posts, div) { return posts.concat( Post.makePosts(div)); }; return fetch$1(q, container).then(function (ref) { var afters = ref.afters; var befores = ref.befores; return afters.reduce(makePostsAndConcat, []).concat( posts, befores.reduce(makePostsAndConcat, []) ); }); } var makePosts = function(ref) { var config = ref.config; var q = ref.q; var gui = ref.gui; var container = ref.container; var originalPosts = Post.makePosts(container); return complementMissingPostsFromLog({ q: q, gui: gui, container: container, posts: originalPosts, }) .then(function (posts) { return processNg(config, posts); }) .then(function (posts) { return shouldMakeUrlsSearchLog(q, posts); }); }; var ImaginaryPostPrototype = { __proto__: Post.prototype, /** * @param {Post} child */ setFields: function(child) { this.id = child.parentId; this.parent = null; this.next = null; this.isNG = null; this.threadId = child.threadId; this.threadUrl = child.threadUrl; this.parentId = this.isOP() ? null : undefined; if (this.id) { this.setResUrl(child); } }, calculate: function(property) { var value, child = this.child; var getCandidate = property + "Candidate"; if (child.next) { var rank = Object.create(null), max = 0, candidate; var validates = getCandidate + "LooksValid"; var bonus = this[property + "Bonus"]; do { candidate = child[getCandidate](); rank[candidate] = ++rank[candidate] || 1; if (child[validates](candidate)) { rank[candidate] += bonus; } } while ((child = child.next)); for (candidate in rank) { var number = rank[candidate]; if (max < number) { max = +number; value = candidate; } } } else { value = child[getCandidate](); } return Object.defineProperty(this, property, {value: value})[property]; }, getText: function() { return this.text; }, isRead: true, setResUrl: function(child) { this.resUrl = child.resUrl.replace(/(\?|&)s=\d+/, "$1s=" + this.id); }, getKeyForOwnParent: function() { return this.parentId ? this.parentId : "parent of " + this.id; }, }; Object.defineProperty(ImaginaryPostPrototype, "text", { get: function() { return this.calculate("text"); }, }); function GhostPost(child) { this.setFields(child); } GhostPost.prototype = Object.create(ImaginaryPostPrototype); GhostPost.prototype.date = "?"; GhostPost.prototype.getIdForcibly = function(postParent) { return postParent.findAsync(this.child); }; function MergedPost(child) { this.setFields(child); this.name = child.title.replace(/^>/, ""); } MergedPost.prototype = Object.create(ImaginaryPostPrototype, { date: { get: function() { return this.calculate("date"); }, }, }); function makeParent(post) { if (post instanceof MergedPost) { return new GhostPost(post); } else if (post instanceof Post) { return new MergedPost(post); } else { throw new Error("should not be called"); } } function Thread(config, postParent) { this.config = config; this.postParent = postParent; this.posts = []; this.isNG = false; this.allPosts = Object.create(null); } Thread.computeRejectLevelForRoot = function( vanishedMessageIDs, postParent, id, level ) { if (!id || level === 0) { return 0; } if (vanishedMessageIDs.indexOf(id) > -1) { return level; } return Thread.computeRejectLevelForRoot( vanishedMessageIDs, postParent, postParent.get(id), level - 1 ); }; Thread.inheritRejectLevel = function(vanishedMessageIDs, post, generation) { if (!post) { return; } var rejectLevel = 0; if (vanishedMessageIDs.indexOf(post.id) > -1) { rejectLevel = 3; } else if (generation > 0) { rejectLevel = generation; } post.rejectLevel = rejectLevel; Thread.inheritRejectLevel(vanishedMessageIDs, post.child, rejectLevel - 1); Thread.inheritRejectLevel(vanishedMessageIDs, post.next, generation); }; Thread.prototype = { addPost: function(post) { this.posts.push(post); this.allPosts[post.id] = post; if (post.isNG) { this.isNG = true; } }, computeRoots: function() { var roots = this.computeRoots2(); if (!this.config.useVanishMessage) { return roots; } if (!this.shouldSetRejectLevel()) { return roots; } this.setRejectLevel(roots); if (!this.config.utterlyVanishMessage) { return roots; } return this.dropRejectedPosts(roots); }, computeRoots2: function() { return this.computeRoots2ndPass(this.computeRoots1stPass()); }, computeRoots1stPass: function() { this.makeFamilyTree(); var orphans = this.posts.filter(Post.isOrphan); this.connect(orphans); return this.getRootCandidates().sort(Post.byID); }, makeFamilyTree: function() { this.posts.filter(Post.wantsParent).forEach(this.adopt, this); }, connect: function(orphans) { orphans.forEach(this.makeParent, this); orphans.forEach(this.adopt, this); }, getRootCandidates: function() { return Object.values(this.allPosts).filter(Post.isRootCandidate); }, computeRoots2ndPass: function(roots) { var orphans = roots.filter(Post.mayHaveParent); orphans.forEach(this.readParentId, this); this.connect(orphans); return this.getRootCandidates().sort(this.byID); }, readParentId: function(post) { post.parentId = this.postParent.get(post.id); }, makeParent: function(orphan) { var key = orphan.getKeyForOwnParent(); this.allPosts[key] = this.allPosts[key] || makeParent(orphan); }, byID: function(l, r) { var lid = l.id ? l.id : l.child.id; var rid = r.id ? r.id : r.child.id; return lid - rid; }, adopt: function(post) { var parent = this.allPosts[post.getKeyForOwnParent()]; if (!parent) { return; } parent.adoptAsEldestChild(post); }, shouldSetRejectLevel: function() { return this.getSmallestMessageID() <= this.getThreshold(); }, getThreshold: function() { return +this.config.vanishedMessageIDs[0]; }, getSmallestMessageID: function() { return Object.keys(this.allPosts).sort(this.byNumber)[0]; }, byNumber: function(l, r) { return l - r; }, setRejectLevel: function(roots) { var vanishedMessageIDs = this.config.vanishedMessageIDs; var computeRejectLevelForRoot = Thread.computeRejectLevelForRoot; var postParent = this.postParent; for (var i = roots.length - 1; i >= 0; i--) { var root = roots[i]; var child = root.child; var id = root.id; if (id) { root.rejectLevel = computeRejectLevelForRoot( vanishedMessageIDs, postParent, id, 3 ); } if (child) { Thread.inheritRejectLevel( vanishedMessageIDs, child, root.rejectLevel - 1 ); } } return roots; }, dropRejectedPosts: function(roots) { var newRoots = []; function drop(post, isRoot) { if (!post) { return null; } var child = drop(post.child, false); var next = drop(post.next, false); var isRead = post.isRead; if (!child && isRead) { return next; } post.child = child; post.next = next; var rejectLevel = post.rejectLevel; if (isRoot && rejectLevel === 0) { newRoots.push(post); } else if (rejectLevel === 1 && child) { newRoots.push(child); } return rejectLevel === 3 ? next : post; } for (var i = roots.length - 1; i >= 0; i--) { drop(roots[i], true); } return newRoots.sort(Post.byID); }, getDate: function() { return this.posts[0].date; }, getAppropriateDate: function() { if (this.config.utterlyVanishMessage) { return this.posts.filter(Post.isClean)[0].date; } else { return this.getDate(); } }, getNumber: function() { return this.posts.filter(Post.isClean).length; }, getID: function() { return this.posts[0].threadId; }, getURL: function() { return this.posts[0].threadUrl; }, getSite: function() { return this.posts[0].site; }, isVanished: function isVanished() { return this.config.isVanishedThread(this.getID()); }, }; function makeThreads(config, postParent, posts) { var allThreads = Object.create(null); var threads = []; posts.forEach(function(post) { var id = post.threadId; var thread = allThreads[id]; if (!thread) { thread = allThreads[id] = new Thread(config, postParent); threads.push(thread); } thread.addPost(post); }); return threads; } function sortThreads(config, threads) { if (config.threadOrder === "ascending") { threads.reverse(); } } var transformToThreads = function(ref) { var config = ref.config; var postParent = ref.postParent; var posts = ref.posts; var threads = makeThreads(config, postParent, posts); sortThreads(config, threads); return threads; }; var deleteFooter = function(container, howManyPosts) { var i = container.querySelector("p i"); if (!i) { return; } // <P><I><FONT size="-1">ここまでは、現在登録されている新着順1番目から1番目までの記事っぽい!</FONT></I></P> var numPostsInfo = i.parentNode; // === <P> var buttons = nextElement("TABLE")(numPostsInfo); var end; if (buttons && howManyPosts) { // ボタンを残す end = numPostsInfo; } else { // ボタンはないか、あるが0件の振りをするため消す end = nextElement("HR")(numPostsInfo); } deleteBetween(numPostsInfo, end); }; function deleteBetween(start, end) { var range = document.createRange(); range.setStartBefore(start); range.setEndAfter(end); range.deleteContents(); } function suggestLinkToLog(ref) { var q = ref.q; var gui = ref.gui; var posts = ref.posts; var href = ref.href; if ( href === void 0 ) href = location.href; if (!posts) { throw new Error("no posts"); } if (q.shouldSuggestLinkToLog(posts)) { var fill = function (n) { return (n < 10 ? "0" + n : n); }; var today = new Date(); var year = today.getFullYear(); var month = fill(today.getMonth() + 1); var date = fill(today.getDate()); var url = href + "&ff=" + year + month + date + ".dat"; gui.appendExtraInfoHtml((" <a id=\"hint\" href=\"" + url + "\">過去ログを検索する</a>")); } } function setPostCount(setPostCount, postLength) { var message; if (postLength) { message = postLength + "件取得"; } else { message = "未読メッセージはありません。"; } setPostCount(message); } function autovanishThread(config, gui, threads) { if (!config.autovanishThread) { return; } var ids = threads.filter(function (thread) { return thread.isNG; }).map(function (thread) { return thread.getID(); }); if (!ids.length) { return; } return gui.showSaving(function () { return config.addVanishedThread(ids); }); } var buildAndShowThreads = function(ref) { var config = ref.config; var q = ref.q; var gui = ref.gui; var container = ref.container; var postParent = ref.postParent; if ( postParent === void 0 ) postParent = createPostParent(config); var mPosts = makePosts({config: config, q: q, gui: gui, container: container}); var gotAllowedToTweakContainer = mPosts.then(function (posts) { return deleteFooter(container, posts.length); } ); var gotDone = mPosts.then(function (posts) { suggestLinkToLog({q: q, gui: gui, posts: posts}); setPostCount(gui.setPostCount, posts.length); postParent.saveAsyncIfNeeded(posts); var threads = transformToThreads({config: config, postParent: postParent, posts: posts}); autovanishThread(config, gui, threads); gui.addEventListeners(config, postParent); var done = showThreads(config, gui, threads); done.then(function (done) { return postParent.cleanUpLater(done); }); done.then(function (done) { return gui.clearInfo(done); }); return done.then(function () { return posts; }); }); return {gotDone: gotDone, gotAllowedToTweakContainer: gotAllowedToTweakContainer}; }; var createReload = function (config) { var reload = '<input type="button" value="リロード" class="mattari">'; if (!config.zero) { reload = reload.replace("mattari", "reload"); reload += '<input type="button" value="未読" class="mattari">'; } return reload; }; var focusV = function () { setTimeout(function() { document.getElementsByName("v")[0].focus(); }, 50); }; var getAccesskey = function(config) { var accesskey = config.accesskeyReload; return /^\w$/.test(accesskey) ? accesskey : "R"; }; var getViewsAndViewing = function(body) { var hr = body.getElementsByTagName("hr")[0]; if (hr) { var font = hr.previousElementSibling; if (font && font.tagName === "FONT") { // eslint-disable-next-line // 2005/03/01 から views(こわれにくさレベル4) 現在の参加者 : viewing名 (300秒以内) var ref = font.textContent.match(/\d+/g) || []; var views = ref[3]; var viewing = ref[5]; return (views + " / " + viewing + " 名"); } } return ""; }; var midokureload = function() { var midoku = document.querySelector('#form input[name="midokureload"]'); if (midoku) { midoku.click(); } else { location.reload(); } }; var reload = function() { var form = document.getElementById("form"); if (!form) { location.reload(); return; } var reload = document.getElementById("qtv-reload"); if (!reload) { form.insertAdjacentHTML( "beforeend", '<input type="submit" id="qtv-reload" name="reload" value="1" style="display:none;">' ); } document.getElementById("qtv-reload").click(); }; var createTreeGuiContainer = function (config, body) { var el = document.createElement("div"); el.id = "container"; el.innerHTML = headerTemplate(config, body) + '<div id="content"></div><hr>' + footerTemplate(config); //event var click = on.bind(null, el, "click"); click(".reload", reload); click(".mattari", midokureload); click(".goToForm", focusV); addClearVanishEvent(config, click); var header = el.firstElementChild; var firstChildOfHeader = header.firstElementChild; var info = firstChildOfHeader.lastElementChild; var postcount = info.previousElementSibling; return { container: el, info: info, postcount: postcount, content: header.nextSibling, footer: el.lastChild, }; }; var addClearVanishEvent = function (config, click) { ["Message", "Thread"].forEach(function (type) { var id = "clearVanished" + type + "IDs"; click("#" + id, function (e) { e.preventDefault(); clearVanishedIds(config, id, e.target); }); }); }; function headerTemplate(config, body) { var reload$$1 = createReload(config); var accesskey = getAccesskey(config); var viewsAndViewing = getViewsAndViewing(body); return ("\n\t\t<header id=\"header\">\n\t\t\t<span class=\"left\">\n\t\t\t\t" + (reload$$1.replace('class="mattari"', ("$& accesskey=\"" + accesskey + "\""))) + "\n\t\t\t\t" + viewsAndViewing + "\n\t\t\t\t<span id=\"postcount\"></span>\n\t\t\t\t<span id=\"info\">ダウンロード中...</span>\n\t\t\t</span>\n\t\t\t<span>\n\t\t\t\t<a href=\"javascript:;\" id=\"openConfig\">設定</a>\n\t\t\t\t<a href=\"#link\">link</a>\n\t\t\t\t<a href=\"#form\" class=\"goToForm\">投稿フォーム</a>\n\t\t\t\t" + reload$$1 + "\n\t\t\t</span>\n\t\t</header>"); } function footerTemplate(config) { var reload$$1 = createReload(config); var length = { Thread: config.vanishedThreadIDs.length, Message: config.vanishedMessageIDs.length, }; var hidden = length.Thread || length.Message ? "" : "hidden"; var count = function (type, text) { return ("<a id=\"clearVanished" + type + "IDs\" href=\"javascript:;\"><span class=\"count\">" + (length[ type ]) + "</span>" + text + "</a>"); }; return ("\n\t\t<footer id=\"footer\">\n\t\t\t<span class=\"left\">\n\t\t\t\t" + reload$$1 + "\n\t\t\t</span>\n\t\t\t<span>\n\t\t\t\t<span class=\"clearVanishedButtons " + hidden + "\">\n\t\t\t\t\t非表示解除(" + (count("Thread", "スレッド")) + "/" + (count("Message", "投稿")) + ")\n\t\t\t\t</span>\n\t\t\t\t" + reload$$1 + "\n\t\t\t</span>\n\t\t</footer>"); } var setText = function (node) { return function (text) { node.textContent = text; }; }; var setHtml = function (element) { return function (html) { element.innerHTML = html; }; }; var appendHtmlAfter = function (node) { return function (html) { node.insertAdjacentHTML("afterend", html); }; }; var showSaving = function (config, footer) { return function (execute) { var buttons = footer.querySelector(".clearVanishedButtons"); buttons.insertAdjacentHTML( "beforebegin", '<span class="savingVanishedThreadIDs">非表示スレッド保存中</span>' ); return execute().then(function showSaved() { var saving = buttons.previousElementSibling; saving.parentNode.removeChild(saving); var threadLength = config.vanishedThreadIDs.length; if (threadLength) { buttons.querySelector( "#clearVanishedThreadIDs .count" ).textContent = threadLength; buttons.classList.remove("hidden"); } }); }; }; var HideMessage = { changeTextState: function() { this.text.style.display = "none"; }, changeButtonText: function() { this.button.textContent = "戻"; }, save: function() { this.config.addVanishedMessage(this.post.id); }, setRejectLevel: function() { var post = this.post; post.previousRejectLevel = post.rejectLevel; post.rejectLevel = 3; }, shouldProcess: function(post, rejectLevel) { return post.rejectLevel < rejectLevel; }, setChildRejectLevel: function(post, rejectLevel) { post.rejectLevel = rejectLevel; }, processMarking: function(message) { if (!message.querySelector(".chainingHidden")) { message.firstElementChild.classList.add("chainingHidden"); } }, }; var ShowMessage = { changeTextState: function() { this.text.style.display = null; }, changeButtonText: function() { this.button.textContent = "消"; }, save: function() { this.config.removeVanishedMessage(this.post.id); }, setRejectLevel: function() { var post = this.post; post.rejectLevel = post.previousRejectLevel; }, shouldProcess: function(post, rejectLevel) { return post.rejectLevel <= rejectLevel; }, setChildRejectLevel: function(post, _rejectLevel) { post.rejectLevel = 0; }, processMarking: function(message) { var mark = message.querySelector(".chainingHidden"); if (mark) { mark.classList.remove("chainingHidden"); } }, }; function ToggleMessage(config, postParent) { this.config = config; this.postParent = postParent; } ToggleMessage.prototype = { handleEvent: function(e) { this.button = e.target; this.message = this.button.closest(".message"); this.messages = this.message.closest(".messages"); this.text = this.message.querySelector(".text"); this.post = this.message.post; return this.execute(); }, execute: function() { return this.setIDToPost() .then(this.toggle.bind(this)) .catch(this.error.bind(this)); }, toggle: function() { this.setRejectLevel(); this.save(); this.changeTextState(); this.changeButtonState(); this.setChildrensRejectLevel(this.post.child, 2); }, changeButtonState: function() { this.toggleButtonState(); this.changeButtonText(); }, toggleButtonState: function() { this.button.classList.toggle("revert"); }, isRevertButton: function() { return this.button.classList.contains("revert"); }, error: function(error) { this.button.parentNode.replaceChild( document.createTextNode(error.message), this.button ); }, setIDToPost: function() { var this$1 = this; return this.findPostID().then(function (id) { if (!id) { return Promise.reject( new Error("最新1000件以内に存在しないため投稿番号が取得できませんでした。過去ログからなら消せるかもしれません") ); } if (id.length > 100) { return Promise.reject(new Error("この投稿は実在しないようです")); } this$1.post.id = id; }); }, findPostID: function() { var post = this.post; var id = post.id; if (id === undefined) { id = post.getIdForcibly(this.postParent); } return Promise.resolve(id); }, setChildrensRejectLevel: function(post, rejectLevel) { if (post === null || rejectLevel === 0) { return; } if (this.shouldProcess(post, rejectLevel)) { this.setChildRejectLevel(post, rejectLevel); var message = this.getTargetMessage(post); if (message) { this.processMarking(message); } } this.setChildrensRejectLevel(post.child, rejectLevel - 1); this.setChildrensRejectLevel(post.next, rejectLevel); }, getTargetMessage: function(post) { return this.messages.querySelector('[data-id="' + post.id + '"]'); }, }; function ToggleMessageDispatcher(config, postParent) { this.config = config; this.postParent = postParent; } ToggleMessageDispatcher.prototype.handleEvent = function(e) { e.preventDefault(); var handler = this.makeHandler(e); return handler.handleEvent(e); }; ToggleMessageDispatcher.prototype.makeHandler = function(e) { var handler = new ToggleMessage(this.config, this.postParent); if (e.target.classList.contains("revert")) { Object.assign(handler, ShowMessage); } else { Object.assign(handler, HideMessage); } return handler; }; var getTreeMode = function (node) { return node.closest(".tree-mode-css") ? "tree-mode-css" : "tree-mode-ascii"; }; var replace = function (config, change) { return function (e) { e.preventDefault(); var message = e.target.closest(".message, .showMessage"); var parent = message.parentNode; var post = message.post; var mode = getTreeMode(message); var view = createView$1(mode); var maker = view.messageMaker(config); var depth = parseInt(message.style.marginLeft, 10); change(post); var newMessage = maker(post, depth); parent.replaceChild(newMessage, message); }; }; var showAsIs = function (config) { return function (e) { function callback(post) { post.showAsIs = !post.showAsIs; } var target = e.target; var id = setTimeout(replace(config, callback), 500, e); var cancel = function() { clearTimeout(id); target.removeEventListener("mouseup", cancel); target.removeEventListener("mousemove", cancel); }; target.addEventListener("mouseup", cancel); target.addEventListener("mousemove", cancel); }; }; var toggleTreeMode = function (config) { return function (e) { e.preventDefault(); var button = e.target; var thread = button.closest(".thread"); thread.classList.toggle("tree-mode-css"); thread.classList.toggle("tree-mode-ascii"); var view = createView$1(getTreeMode(thread)); view.init(thread.roots); var newMessages = view.render(config); thread.replaceChild(newMessages, thread.querySelector(".messages")); }; }; var toggleThread = function (config) { return function (e) { var button = e.target; var thread = button.closest(".thread"); var id = thread.dataset.threadId; var type, text; if (thread.classList.contains("NGThread")) { type = "remove"; text = "消"; } else { type = "add"; text = "戻"; } type += "VanishedThread"; config[type](id); thread.classList.toggle("NGThread"); button.textContent = text; }; }; var addEventListeners = function(ref) { var config = ref.config; var postParent = ref.postParent; var el = ref.el; function click(selector, callback) { on(el, "click", selector, replace(config, callback)); } click(".characterEntity", function(post) { post.characterEntity = !(post.hasOwnProperty("characterEntity") ? post.characterEntity : config.characterEntity); }); click(".showMessageButton", function(post) { post.show = true; }); click(".cancelVanishedMessage", function(post) { config.removeVanishedMessage(post.id); delete post.rejectLevel; }); click(".fold", function(post) { post.show = false; }); on(el, "mousedown", ".message", showAsIs(config)); click(".toggleTruncation", function(post) { post.truncation = post.hasOwnProperty("truncation") ? !post.truncation : false; }); if (config.useVanishMessage) { on( el, "click", ".toggleMessage", new ToggleMessageDispatcher(config, postParent) ); } on(el, "click", ".vanish", toggleThread(config)); on(el, "click", ".toggleTreeMode", toggleTreeMode(config)); }; var createGui = function (config, body) { if ( body === void 0 ) body = document.body; var ref = createTreeGuiContainer( config, body ); var container = ref.container; var info = ref.info; var postcount = ref.postcount; var content = ref.content; var footer = ref.footer; return { setInfo: setText(info), setInfoHtml: setHtml(info), clearInfo: function () { return setText(info)(""); }, appendExtraInfoHtml: appendHtmlAfter(info), setPostCount: setText(postcount), getContent: function () { return content; }, addEventListeners: function (config, postParent) { return addEventListeners({config: config, postParent: postParent, el: content}); }, showSaving: showSaving(config, footer), prependToBody: function prependToBody() { body.insertBefore(container, body.firstChild); }, }; }; var originalRange = function(container, range) { if ( range === void 0 ) range = document.createRange(); var firstAnchor = container.querySelector("a[name]"); if (!firstAnchor) { return range; } var end = kuzuhaEnd(container); if (!end) { return range; } var start = startNode(container, firstAnchor); range.setStartBefore(start); range.setEndAfter(end); return range; }; function startNode(container, firstAnchor) { var h1 = container.querySelector("h1"); if ( h1 && h1.compareDocumentPosition(firstAnchor) & Node.DOCUMENT_POSITION_FOLLOWING ) { return h1; } else { return firstAnchor; } } function kuzuhaEnd(container) { var last = container.lastChild; while (last) { var type = last.nodeType; if ( (type === Node.COMMENT_NODE && last.nodeValue === " ") || (type === Node.ELEMENT_NODE && last.nodeName === "H3") ) { return last; } last = last.previousSibling; } return null; } var StreamTreeView = function StreamTreeView(args) { Object.assign(this, args); this.gui = createGui(this.config, this.body); }; StreamTreeView.prototype.init = function init () { this.gui.prependToBody(); }; StreamTreeView.prototype.finish = function finish (buffer) { var this$1 = this; var ref = this; var config = ref.config; var gui = ref.gui; var q = ref.q; var ref$1 = buildAndShowThreads({ config: config, q: q, gui: gui, container: buffer, }); var gotDone = ref$1.gotDone; var gotAllowedToTweakContainer = ref$1.gotAllowedToTweakContainer; this.prepareToggleOriginal(buffer, gotDone); gotAllowedToTweakContainer.then(function () { return this$1.appendLeftovers(buffer); }); return gotDone.then(this.done); }; StreamTreeView.prototype.appendLeftovers = function appendLeftovers (buffer) { this.body.appendChild(buffer); }; StreamTreeView.prototype.prepareToggleOriginal = function prepareToggleOriginal (buffer, done) { var range = originalRange(buffer); if (this.config.deleteOriginal) { range.deleteContents(); } else { var original = range.extractContents(); return Promise.all([original, done]).then( this.appendToggleOriginal.bind(this) ); } }; StreamTreeView.prototype.appendToggleOriginal = function appendToggleOriginal (ref) { var original = ref[0]; var posts = ref[1]; if (!original || !posts.length) { return; } var toggle = new ToggleOriginal(original); this.buffer.insertBefore(toggle.getUI()); }; var createView = function(ref) { var config = ref.config; var q = ref.q; var buffer = ref.buffer; var body = ref.body; if ( body === void 0 ) body = document.body; var done = ref.done; if (config.isTreeView()) { return new StreamTreeView({config: config, body: body, q: q, buffer: buffer, done: done}); } else { var view = new StackView(config); var log = new StackLog(config, q, body, view); return new StreamStackView({config: config, body: body, buffer: buffer, view: view, log: log, done: done}); } }; var initView = function (ref) { var config = ref.config; var q = ref.q; var buffer = ref.buffer; var done = ref.done; var view = createView({config: config, q: q, buffer: buffer, done: done}); view.init(); buffer.setView(view); return view.done; }; function getTitle() { return document.title; } function shouldCloseWindow(config, title) { return config.closeResWindow && title.endsWith(" 書き込み完了"); } function sendMessageToRuntime(message) { chrome.runtime.sendMessage(message); } function closeResWindow() { if (IS_EXTENSION) { sendMessageToRuntime({type: "closeTab"}); } else { window.open("", "_parent"); window.close(); } } var closeWindowIfNeeded = function (gotConfig) { return gotConfig.then(function (config) { var title = getTitle(); if (shouldCloseWindow(config, title)) { closeResWindow(); } }); }; var streamMain = function (gotConfig, q, execute) { var loaded = waitForDomContentLoaded(); var observer = new Observer(loaded); var notice = createDelayNotice(gotConfig); var buffer = new Buffer(); observer.addListener({ onHr: function () { return execute(function (config, done) { return initView({config: config, q: q, buffer: buffer, done: done}); } ).catch(function (e) { handleError(e); throw e; }); }, onLoaded: function () { return closeWindowIfNeeded(gotConfig); }, }); observer.addListener(notice); observer.addListener(buffer); observer.observe(); }; var deleteOriginal = function(config, body) { if (config.deleteOriginal) { originalRange(body).deleteContents(); } }; var detachBody = function () { var body = document.body; if (IS_FIREFOX) { document.documentElement.removeChild(body); } return body; }; var attachToDocumentElement = function (body) { if (IS_FIREFOX) { document.documentElement.appendChild(body); } }; var tree = function(config, q) { var body = detachBody(); try { var gui = createGui(config, body); var ref = buildAndShowThreads({ config: config, q: q, gui: gui, container: body, }); var gotDone = ref.gotDone; var gotAllowedToTweakContainer = ref.gotAllowedToTweakContainer; gotAllowedToTweakContainer.then(function () { return deleteOriginal(config, body); }); gui.prependToBody(); return gotDone; } finally { attachToDocumentElement(body); } }; function stack(config, q, body) { if ( body === void 0 ) body = document.body; Stack.common(config, body); var view = new StackView(config); var log = new StackLog(config, q, body, view); var complement = log.complement(); var render = Stack.render(config, body, view); var tweakFooter = Stack.tweakFooter(config, body, render); return Promise.all([complement, render, tweakFooter]); } var runProperView = function (config, q) { return (config.isTreeView() ? tree : stack)(config, q); }; var endMain = function (gotConfig, q, execute) { return ready().then(function () { closeWindowIfNeeded(gotConfig); return execute(function (config, done) { return runProperView(config, q).then(done); }); }); }; function NG(config) { var word = config.NGWord; var handle = config.NGHandle; if (config.useNG) { if (handle) { this.handle = new RegExp(handle); this.handleg = new RegExp(handle, "g"); } if (word) { this.word = new RegExp(word); this.wordg = new RegExp(word, "g"); } } this.isEnabled = !!(this.word || this.handle); } var ChromeStorage = { load: function(_defaults) { var this$1 = this; return new Promise(function (resolve) { this$1.storage().get(null, resolve); }); }, remove: function(key) { this.storage().remove(key); }, set: function(key, value, callback) { var item = {}; item[key] = value; this.storage().set(item, callback); }, setAll: function(items) { var this$1 = this; return new Promise(function (resolve) { return this$1.storage().set(items, resolve); }); }, clear: function() { var this$1 = this; return new Promise(function (resolve) { this$1.storage().clear(resolve); }); }, get: function(key, fun) { this.storage().get(key, function(item) { fun(item[key]); }); }, storage: function() { return chrome.storage.local; }, }; var GMStorage = { load: function(defaults) { return new Promise(function (resolve) { var config = Object.create(null); var keys = Object.keys(defaults); var i = keys.length; var key, value; while (i--) { key = keys[i]; if (typeof defaults[key] === "function") { continue; } value = GM_getValue(key); if (value != null) { config[key] = JSON.parse(value); } } resolve(config); }); }, remove: function(key) { GM_deleteValue(key); }, set: function(key, value, callback) { GM_setValue(key, JSON.stringify(value)); if (callback) { callback(); } }, setAll: function(items) { var this$1 = this; for (var key in items) { this$1.set(key, items[key]); } return Promise.resolve(); }, clear: function() { GM_listValues().forEach(GM_deleteValue); return Promise.resolve(); }, get: function(key, fun) { fun(JSON.parse(GM_getValue(key, "null"))); }, }; var GM4Storage = { load: function(defaults) { var this$1 = this; return new Promise(function (resolve) { var keys = Object.keys(defaults).filter( function (key) { return typeof defaults[key] !== "function"; } ); Promise.all(keys.map(function (key) { return this$1.storage().getValue(key); })) .then(function (values) { return values.reduce(function (config, value, i) { if (value != null) { config[keys[i]] = JSON.parse(value); } return config; }, Object.create(null)); } ) .then(resolve); }); }, remove: function(key) { this.storage().deleteValue(key); }, set: function(key, value, callback) { if ( callback === void 0 ) callback = function () {}; return this.storage() .setValue(key, JSON.stringify(value)) .then(callback); }, setAll: function(items) { var this$1 = this; var promises = []; for (var key in items) { promises.push(this$1.set(key, items[key])); } return Promise.all(promises); }, clear: function() { var storage = this.storage(); return storage.listValues().then(function (keys) { return keys.forEach(storage.deleteValue); }); }, get: function(key, callback) { return this.storage() .getValue(key, "null") .then(JSON.parse) .then(callback); }, storage: function() { return GM; }, }; function Config(config, storage) { Object.assign(this, config); this._storage = storage; this.init(); } Config.prototype = { treeMode: "tree-mode-ascii", toggleTreeMode: false, thumbnail: true, thumbnailPopup: true, popupAny: false, popupMaxWidth: "", popupMaxHeight: "", popupBestFit: true, threadOrder: "ascending", NGHandle: "", NGWord: "", useNG: true, NGCheckMode: false, spacingBetweenMessages: false, useVanishThread: true, vanishedThreadIDs: [], //扱い注意 autovanishThread: false, utterlyVanishNGThread: false, useVanishMessage: false, vanishedMessageIDs: [], vanishMessageAggressive: false, utterlyVanishMessage: false, utterlyVanishNGStack: false, deleteOriginal: true, zero: true, accesskeyReload: "R", accesskeyV: "", keyboardNavigation: false, keyboardNavigationOffsetTop: "200", viewMode: "t", css: "", linkAnimation: true, shouki: true, closeResWindow: false, maxLine: "", openLinkInNewTab: false, characterEntity: true, }; function init() { this.ng = new NG(this); } function setStorage(storage) { this._storage = storage; } var addID = function(type, id_or_ids, callback) { var this$1 = this; var ids = Array.isArray(id_or_ids) ? id_or_ids : [id_or_ids]; var target = "vanished" + type + "IDs"; this[target] = ids.concat(this[target]); this._storage.get(target, function (IDs) { IDs = Array.isArray(IDs) ? IDs : []; ids = ids.filter(function(id) { return IDs.indexOf(id) === -1; }); IDs = IDs.concat(ids).sort(function(l, r) { return +r - l; }); this$1[target] = IDs; this$1._storage.set(target, IDs, callback); }); }; var removeID = function(type, id) { var this$1 = this; var target = "vanished" + type + "IDs"; this._storage.get(target, function (ids) { ids = Array.isArray(ids) ? ids : []; var index = ids.indexOf(id); if (index !== -1) { ids.splice(index, 1); this$1[target] = ids; if (ids.length) { this$1._storage.set(target, ids); } else { this$1._storage.remove(target); } } }); }; var clearIDs = function(type) { var target = "vanished" + type + "IDs"; this._storage.remove(target); this[target] = []; }; /** @param {String} id */ var addVanishedMessage = function(id) { this.addID("Message", id); }; var removeVanishedMessage = function(id) { this.removeID("Message", id); }; var clearVanishedMessageIDs = function() { this.clearIDs("Message"); }; /** @param {String} id */ var addVanishedThread = function(id) { var this$1 = this; return new Promise(function (resolve) { this$1.addID("Thread", id, resolve); }); }; var removeVanishedThread = function(id) { this.removeID("Thread", id); }; var clearVanishedThreadIDs = function() { this.clearIDs("Thread"); }; var clear = function() { var this$1 = this; return this._storage.clear().then(function () { Object.assign(this$1, Config.prototype); }); }; var update = function(items) { var this$1 = this; Object.keys(items) .filter(function (key) { return typeof Config.prototype[key] === "undefined"; }) .forEach(function (key) { return delete items[key]; }); return this._storage.setAll(items).then(function () { Object.assign(this$1, items); }); }; var isTreeView = function() { return this.viewMode === "t"; }; var isVanishedThread = function(id) { return this.useVanishThread && this.vanishedThreadIDs.indexOf(id) > -1; }; Object.assign(Config.prototype, { init: init, setStorage: setStorage, addVanishedMessage: addVanishedMessage, removeVanishedMessage: removeVanishedMessage, clearVanishedMessageIDs: clearVanishedMessageIDs, addVanishedThread: addVanishedThread, removeVanishedThread: removeVanishedThread, clearVanishedThreadIDs: clearVanishedThreadIDs, clear: clear, update: update, isTreeView: isTreeView, isVanishedThread: isVanishedThread, addID: addID, removeID: removeID, clearIDs: clearIDs, }); Config.load = function(storage) { storage = storage || Config.whichStorageToUse(); return storage .load(Config.prototype) .then(function (config) { return new Config(config, storage); }); }; Config.whichStorageToUse = function() { return IS_GM ? GMStorage : IS_GM4 ? GM4Storage : ChromeStorage; }; var BothWaysLogSearch = { getLogParameterName: function(query) { return query.get("ff"); }, getDayAsNumber: function(query) { return +this.getLogParameterName(query).match(/^(\d{8})\.dat$/)[1]; }, queryFor: function(query, ff) { var data = query.copy(); data.ff = ff; return data; }, }; var FutureLogSearch = { getLogParameterName: function(query) { return Object.keys(query.q).find(function(key) { return /^chk\d+\.dat$/.test(key); }); }, getDayAsNumber: function(query) { return +this.getLogParameterName(query).match(/\d+/)[0]; }, queryFor: function(query, ff) { var data = query.copy(); delete data[query.getLogParameterName()]; data["chk" + ff] = "checked"; return data; }, }; var Query = function Query(search, hostname) { if ( search === void 0 ) search = location.search; if ( hostname === void 0 ) hostname = location.hostname; this.q = typeof search === "object" ? search : Query.parse(search); this.hostname = hostname; }; Query.parse = function parse (search) { var obj = {}, kvs = search.substring(1).split("&"); kvs.forEach(function(kv) { obj[kv.split("=")[0]] = kv.split("=")[1]; }); return obj; }; Query.prototype.get = function get (key) { return this.q[key]; }; Query.prototype.set = function set (key, value) { this.q[key] = value; }; Query.prototype.shouldHaveValidPosts = function shouldHaveValidPosts () { return this.q.sv || (this.q.e && this.isAtMisao()); }; Query.prototype.isAtMisao = function isAtMisao () { return ( this.hostname === "misao.on.arena.ne.jp" || this.hostname === "misao.mixh.jp" ); }; Query.prototype.isNormalMode = function isNormalMode () { return !this.q.m; }; Query.prototype.shouldMakeUrlsSearchLog = function shouldMakeUrlsSearchLog () { return this.isThreadSearchWithin1000() || this.isPosterSearchInLog(); }; //通常モードからスレッドボタンを押した場合 Query.prototype.isThreadSearchWithin1000 = function isThreadSearchWithin1000 () { return this.q.m === "t" && !this.q.ff && /^\d+$/.test(this.q.s); }; //検索窓→投稿者検索→★の結果の場合 Query.prototype.isPosterSearchInLog = function isPosterSearchInLog () { return this.q.s && this.q.ff && this.q.m === "s"; }; //ツリーでログ補完するべきか Query.prototype.shouldFetch = function shouldFetch () { return this.shouldSearchLog() || this.isFromKomachi(); }; Query.prototype.shouldSearchLog = function shouldSearchLog () { return ( this.q.m === "t" && /^\d+\.dat$/.test(this.q.ff) && /^\d+$/.test(this.q.s) ); }; Query.prototype.isFromKomachi = function isFromKomachi (referrer, search) { if ( referrer === void 0 ) referrer = document.referrer; if ( search === void 0 ) search = location.search; return ( /^http:\/\/misao\.(?:mixh|on\.arena\.ne)\.jp\/c\/upload\.cgi/.test( referrer ) && /^\?chk\d+\.dat=checked&kwd=http:\/\/misao\.(?:mixh|on\.arena\.ne)\.jp\/c\/up\/misao\d+\.\w+&s1=0&e1=0&s2=24&e2=0&ao=a&tt=a&alp=checked&btn=checked(?:&g=checked)?&m=g&k=%82%A0&sv=on$/.test( search ) ); }; Query.prototype.shouldSuggestLinkToLog = function shouldSuggestLinkToLog (posts) { return ( this.isThreadSearchWithin1000() && posts.every(function(post) { return !post.isOP(); }) ); }; //スタックでログ補完するべきか Query.prototype.shouldComplement = function shouldComplement (body) { return this.shouldSearchLog() && !this.hasOP(body); }; Query.prototype.selectorForOP = function selectorForOP () { return 'a[name="' + this.q.s + '"]'; }; Query.prototype.hasOP = function hasOP (body) { return body.querySelector(this.selectorForOP()); }; Query.prototype.getLogMode = function getLogMode () { return this.q.sv ? FutureLogSearch : BothWaysLogSearch; }; // isLogMode() { // return this.q.m === "g"; // } Query.prototype.getLogParameterName = function getLogParameterName () { return this.getLogMode().getLogParameterName(this); }; Query.prototype.getDayAsNumber = function getDayAsNumber () { return this.getLogMode().getDayAsNumber(this); }; Query.prototype.copy = function copy () { return Object.assign({}, this.q); }; Query.prototype.queryFor = function queryFor (ff) { return this.getLogMode().queryFor(this, ff); }; Query.prototype.getLogName = function getLogName () { return this.getDayAsNumber() + ".dat"; }; var canProcessStreamingly = function (win) { if ( win === void 0 ) win = window; return !!win.MutationObserver; }; var tweak = function(body) { var v = body.querySelector("textarea"); if (v) { v.focus(); // Firefox needs focus before setSelectionRange. v.scrollIntoView(); // 内容を下までスクロール firefox, opera12 v.setSelectionRange(v.textLength, v.textLength); // 内容を下までスクロール chrome v.scrollTop = v.scrollHeight; } }; var tweakResWindow = function () { return ready().then(getBody).then(tweak); }; var css = "\n.text {\n\twhite-space: pre-wrap;\n}\n.text, .extra {\n\tmin-width: 20rem;\n}\n.text_tree-mode-css, .extra_tree-mode-css {\n\tmargin-left: 1rem;\n}\n.env {\n\tfont-family: initial;\n\tfont-size: smaller;\n}\n.message_tree-mode-css, .border, .showMessage_tree-mode-css {\n\tposition: relative;\n}\n\n.thread-header {\n\tbackground: #447733 none repeat scroll 0 0;\n\tborder-color: #669955 #225533 #225533 #669955;\n\tborder-style: solid;\n\tborder-width: 1px 2px 2px 1px;\n\tfont-size: 0.8rem;\n\tfont-family: normal;\n\tmargin-top: 0.8rem;\n\tpadding: 0;\n\twidth: 100%;\n}\n\n.message-header {\n\twhite-space: nowrap;\n}\n.message-header_tree-mode-css {\n\tfont-size: 0.85rem;\n\tfont-family: normal;\n}\n.message-info {\n\tfont-family: monospace;\n\tcolor: #87CE99;\n}\n\n.read, .quote {\n\tcolor: #CCB;\n}\nheader, footer {\n\tdisplay: flex;\n\tfont-size: 0.9rem;\n}\nheader .left, footer .left {\n\tmargin-right: auto;\n}\n.thread {\n\tmargin-bottom: 1rem;\n}\n.modified {\n\tcolor: #FBB\n}\n.note, .characterEntityOn, .env {\n\tfont-style: italic;\n}\n.chainingHidden::after {\n\tcontent: \"この投稿も非表示になります\";\n\tfont-weight: bold;\n\tfont-style: italic;\n\tcolor: red;\n}\n.a-tree {\n\tfont-style: initial;\n}\n\n.inner {\n/*\t\t\tborder: 2px solid yellow; */\n\ttop: -1rem;\n}\n.outer {\n\tborder-left: 1px solid #ADB;\n\ttop: 1rem;\n}\n.thumbnail-img {\n\twidth: 80px;\n\tmax-height: 400px;\n\timage-orientation: from-image;\n}\n#image-view {\n\tposition: fixed;\n\ttop: 50%;\n\tleft: 50%;\n\ttransform: translate(-50%, -50%);\n\tbackground: #004040;\n\tcolor: white;\n\tfont-weight: bold;\n\tfont-style: italic;\n\tmargin: 0;\n\timage-orientation: from-image;\n}\n.image-view-img {\n\tbackground-color: white;\n}\n\n.focused {\n\tbox-shadow: 0px 0px 0px 2px yellow;\n}\n.truncation, .NGThread .messages, .hidden {\n\tdisplay: none;\n}\n.spacing {\n\tpadding-bottom: 1rem;\n}\n"; var applyCss = function(config) { document.head.insertAdjacentHTML( "beforeend", ("<style>" + (css + config.css) + "</style>") ); }; function zero(config) { if (config.zero) { var d = document.getElementsByName("d")[0]; if (d && d.value !== "0") { d.value = "0"; } } } var id; function progress(after, controller, fun) { clearTimeout(id); var info = controller.$("#configInfo"); info.textContent = "保存中"; setTimeout(function () { fun().then(function () { info.textContent = after; id = setTimeout(function () { info.innerHTML = ""; }, 5000); }); }); } function ConfigController(item) { var this$1 = this; this.item = item; var el = document.createElement("form"); el.id = "config"; this.el = el; var events = [ "save", "clear", "close", "clearVanishThread", "clearVanishMessage", "addToNGWord" ]; for (var i = events.length - 1; i >= 0; i--) { var event = events[i]; on(el, "click", "#" + event, this$1[event].bind(this$1)); } on(el, "keyup", "#quote-input", this.quotemeta.bind(this)); this.render(); } ConfigController.prototype = { $: function(selector) { return this.el.querySelector(selector); }, $$: function(selector) { return Array.prototype.slice.call(this.el.querySelectorAll(selector)); }, render: function() { this.el.innerHTML = this.template(); if (IS_EXTENSION) { var close = this.$("#close"); close.parentNode.removeChild(close); } this.restore(); }, template: function() { return '<style type="text/css">\ <!--\ li {\ list-style-type: none;\ }\ #configInfo {\ font-weight: bold;\ font-style: italic;\ }\ legend + ul {\ margin: 0 0 0 0;\ }\ -->\ </style>\ <fieldset>\ <legend>設定</legend>\ <fieldset>\ <legend>表示</legend>\ <ul>\ <li><label><input type="radio" name="viewMode" value="t">ツリー表示</label></li>\ <li><label><input type="radio" name="viewMode" value="s">スタック表示</label></li>\ </ul>\ </fieldset>\ <fieldset>\ <legend>共通</legend>\ <ul>\ <li><label><input type="checkbox" name="zero">常に0件リロード</label><em>(チェックを外しても「表示件数」は0のままなので手動で直してね)</em></li>\ <li><label>未読リロードに使うアクセスキー<input type="text" name="accesskeyReload" size="1"></label></li>\ <li><label>内容欄へのアクセスキー<input type="text" name="accesskeyV" size="1"></label></li>\ <li><label><input type="checkbox" name="keyboardNavigation">jkで移動、rでレス窓開く</label><em><a href="@GF@#keyboardNavigation">chrome以外の人は説明を読む</a></em></li>\ <ul>\ <li><label>上から<input type="text" name="keyboardNavigationOffsetTop" size="4">pxの位置に合わせる</label></li>\ </ul>\ <li><label><input type="checkbox" name="closeResWindow">書き込み完了した窓を閉じる</label> <em><a href="@GF@#close-tab-in-firefox">firefoxは説明を読むこと</a></em><li>\ <li><label><input type="checkbox" name="openLinkInNewTab">target属性の付いたリンクを常に新しいタブで開く</label></li>\ </ul>\ </fieldset>\ <fieldset>\ <legend>ツリーのみ</legend>\ <ul style="display:inline-block">\ <li><label><input type="checkbox" name="deleteOriginal">元の投稿を非表示にする</label>(高速化)</li>\ <li>スレッドの表示順\ <ul>\ <li><label><input type="radio" name="threadOrder" value="ascending">古→新</label></li>\ <li><label><input type="radio" name="threadOrder" value="descending">新→古</label></li>\ </ul>\ </li>\ <li>ツリーの表示に使うのは\ <ul>\ <li><label><input type="radio" name="treeMode" value="tree-mode-css">CSS</label></li>\ <li><label><input type="radio" name="treeMode" value="tree-mode-ascii">文字</label></li>\ </ul>\ </li>\ <li><label><input type="checkbox" name="spacingBetweenMessages">記事の間隔を開ける</label></li>\ <li><label><input type="text" name="maxLine" size="2">行以上は省略する</label></li>\ <li><label><input type="checkbox" name="characterEntity">数値文字参照を展開</label> <em>(&#数字;が置き換わる)</em></li>\ <li><label><input type="checkbox" name="toggleTreeMode">CSSツリー時にスレッド毎に一時的な文字/CSSの切り替えが出来るようにする</label></li>\ </ul>\ <fieldset style="display:inline-block">\ <legend>投稿非表示設定</legend>\ <ul>\ <li><label><input type="checkbox" name="useVanishMessage">投稿非表示機能を使う</label> <em>使う前に<a href="@GF@#vanish">投稿非表示機能の注意点</a>を読むこと。</em><li>\ <ul>\ <li><span id="vanishedMessageIDs"></span>個の投稿を非表示中<input type="button" value="クリア" id="clearVanishMessage"></li>\ <li><label><input type="checkbox" name="utterlyVanishMessage">完全に非表示</label></li>\ <li><label><input type="checkbox" name="vanishMessageAggressive">パラノイア</label></li>\ <ul>\ </ul>\ </fieldset>\ </fieldset>\ <fieldset>\ <legend>スレッド非表示設定</legend>\ <ul>\ <li><label><input type="checkbox" name="useVanishThread">スレッド非表示機能を使う</label><li>\ <ul>\ <li><span id="vanishedThreadIDs"></span>個のスレッドを非表示中<input type="button" value="クリア" id="clearVanishThread"></li>\ <li><label><input type="checkbox" name="utterlyVanishNGThread">完全に非表示</label></li>\ <li><label><input type="checkbox" name="autovanishThread">NGワードを含む投稿があったら、そのスレッドを自動的に非表示に追加する(ツリーのみ)</label></li>\ </ul>\ </ul>\ </fieldset>\ <fieldset>\ <legend>画像</legend>\ <ul>\ <li>\ <label><input type="checkbox" name="thumbnail">小町と退避の画像のサムネイルを表示</label>\ <ul>\ <li>\ <label><input type="checkbox" name="thumbnailPopup">ポップアップ表示</label>\ <ul>\ <li><label><input type="checkbox" name="popupBestFit">画面サイズに合わせる</label></li>\ <li><label>最大幅:<input type="text" name="popupMaxWidth" size="5">px </label><label>最大高:<input type="text" name="popupMaxHeight" size="5">px <em>画面サイズに合わせない時の設定。空欄で原寸表示</em></label></li>\ </ul>\ </li>\ <li><label><input type="checkbox" name="linkAnimation">描画アニメがある場合にリンクする</label></li>\ <li><label><input type="checkbox" name="shouki">詳希(;゚Д゚)</label></li>\ </ul>\ </li>\ <li><label><input type="checkbox" name="popupAny">小町と退避以外の画像も対象にする</label></li>\ </ul>\ </fieldset>\ <fieldset>\ <legend>NGワード</legend>\ <ul>\ <li><label><input type="checkbox" name="useNG">NGワードを使う</label>\ <p>指定には正規表現を使う。以下簡易説明。複数指定するには|(縦棒)で"区切る"(先頭や末尾につけてはいけない)。()?*+[]{}^$.の前には\\を付ける。</p>\ <li><table>\ <tr>\ <td><label for="NGHandle">ハンドル</label>\ <td><input id="NGHandle" type="text" name="NGHandle" size="30"><em>投稿者とメールと題名</em>\ <tr>\ <td><label for="NGWord">本文</label>\ <td><input id="NGWord" type="text" name="NGWord" size="30">\ <tr><td><td><input id="quote-input" type="text" size="15" value=""> よく分からん人はここにNGワードを一つづつ入力して追加ボタンだ\ <tr><td><td><input id="quote-output" type="text" size="15" readonly><input type="button" id="addToNGWord" value="本文に追加">\ </table>\ <li><label><input type="checkbox" name="NGCheckMode">NGワードを含む投稿を畳まず、NGワードをハイライトする</label>\ <li><label><input type="checkbox" name="utterlyVanishNGStack">完全非表示</label>\ </ul>\ </fieldset>\ <p>\ <label>追加CSS<br><textarea name="css" cols="70" rows="5"></textarea></label>\ </p>\ <p>\ <input type="submit" id="save" accesskey="s" value="保存(s)">\ <input type="button" id="clear" style="float:right" value="デフォルトに戻す">\ <input type="button" id="close" accesskey="c" value="閉じる(c)">\ <span id="configInfo"></span>\ </p>\ </fieldset>'.replace( /@GF@/g, "https://greasyfork.org/scripts/1971-tree-view-for-qwerty" ); }, quotemeta: function() { var output = this.$("#quote-output"); var input = this.$("#quote-input"); output.value = ConfigController.quotemeta(input.value); }, addToNGWord: function() { var output = this.$("#quote-output").value; if (!output.length) { return; } var word = this.$("#NGWord").value; if (word.length) { output = word + "|" + output; } this.$("#NGWord").value = output; this.$$("#quote-output, #quote-input").forEach(function(el) { el.value = ""; }); }, save: function(e) { var this$1 = this; e.preventDefault(); var items = {}; this.$$("input, select, textarea").forEach(function(el) { var k = el.name; var v = null; if (!k) { return; } switch (el.type) { case "radio": if (el.checked) { v = el.value; } break; case "text": case "textarea": v = el.value; break; case "checkbox": v = el.checked; break; } if (v !== null) { items[k] = v; } }); progress("保存しました。", this, function () { return this$1.item.update(items); }); }, clear: function() { var this$1 = this; progress("デフォルトに戻しました。", this, function () { return this$1.item.clear().then(function () { return this$1.restore(); }); } ); }, close: function() { this.el.parentNode.removeChild(this.el); window.scrollTo(0, 0); }, clearVanishThread: function() { this.item.clearVanishedThreadIDs(); this.$("#vanishedThreadIDs").textContent = "0"; this.info("非表示に設定されていたスレッドを解除しました。"); }, clearVanishMessage: function() { this.item.clearVanishedMessageIDs(); this.$("#vanishedMessageIDs").textContent = "0"; this.info("非表示に設定されていた投稿を解除しました。"); }, info: function(text) { clearTimeout(this.id); var info = this.$("#configInfo"); info.textContent = text; this.id = setTimeout(function() { info.innerHTML = ""; }, 5000); }, restore: function restore() { var config = this.item; this.$("#vanishedThreadIDs").textContent = config.vanishedThreadIDs.length; this.$("#vanishedMessageIDs").textContent = config.vanishedMessageIDs.length; this.$$("input, select, textarea").forEach(function(el) { var name = el.name; if (!name) { return; } switch (el.type) { case "radio": el.checked = config[name] === el.value; break; case "text": case "textarea": el.value = config[name]; break; case "checkbox": el.checked = config[name]; break; } }); }, }; ConfigController.quotemeta = function(str) { return (str + "").replace(/([()[\]{}|*+.^$?\\])/g, "\\$1"); }; var Body = (function () { function anonymous() { this.body = document.body; } anonymous.prototype.prepend = function prepend (el) { this.body.insertBefore(el, this.body.firstChild); }; return anonymous; }()); var openConfig = function(config) { if (IS_EXTENSION) { sendMessageToRuntime({type: "openConfig"}); } else if (!document.getElementById("config")) { new Body().prepend(new ConfigController(config).el); window.scrollTo(0, 0); } }; var tweakLink = function(config, a) { if (config.openLinkInNewTab && a.target === "link") { a.target = "_blank"; } if (a.target) { a.rel += " noreferrer noopener"; } }; function addCommonEvents(config) { var body = getBody(); on(body, "click", "#openConfig", function (e) { e.preventDefault(); openConfig(config); }); var delegateTweakLink = function (e) { tweakLink(config, e.target); }; on(body, "mousedown", "a", delegateTweakLink); on(body, "keydown", "a", delegateTweakLink); } function setAccesskeyToV(config) { var accessKey = config.accesskeyV; if (accessKey.length === 1) { var v = document.getElementsByName("v")[0]; if (v) { v.accessKey = accessKey; } } } function KeyboardNavigation(config, window) { if (!window) { throw new Error("missing window"); } //同じキーでもkeypressとkeydownでe.whichの値が違うので注意 var messages = document.getElementsByClassName("message"); var focusedIndex = -1; var done = -1; this.isReloadableNow = function() { done = Date.now(); }; this.isValid = function(index) { return !!messages[index]; }; // jQuery 2系 jQuery.expr.filters.visibleより function isVisible(elem) { return ( elem.offsetWidth > 0 || elem.offsetHeight > 0 || elem.getClientRects().length > 0 ); } function isHidden(elem) { return !isVisible(elem); } this.indexOfNextVisible = function(index, dir) { var el = messages[index]; if (el && isHidden(el)) { return this.indexOfNextVisible(index + dir, dir); } return index; }; var isUpdateScheduled = false; this.updateIfNeeded = function() { if (isUpdateScheduled) { return; } isUpdateScheduled = true; requestAnimationFrame(this.changeFocusedMessage); }; this.changeFocusedMessage = function() { var m = messages[focusedIndex]; var top = m.getBoundingClientRect().top; var x = window.pageXOffset; var y = window.pageYOffset; window.scrollTo(x, top + y - config.keyboardNavigationOffsetTop); var focused = document.getElementsByClassName("focused")[0]; if (focused) { focused.classList.remove("focused"); } m.classList.add("focused"); isUpdateScheduled = false; }; this.focus = function(dir) { var index = this.indexOfNextVisible(focusedIndex + dir, dir); if (this.isValid(index)) { focusedIndex = index; this.updateIfNeeded(); } else if (dir === 1) { var now = Date.now(); if (done >= 0 && now - done >= 500) { done = now; midokureload(); } } }; this.res = function() { var focused = document.querySelector(".focused"); if (!focused) { return; } var selector; if (focused.classList.contains("original")) { selector = "font > a:first-child"; } else { selector = ".res"; } var res = focused.querySelector(selector); if (res) { if (typeof GM_openInTab === "function") { GM_openInTab(res.href, false); } else if (typeof GM === "object" && GM.openInTab) { GM.openInTab(res.href, false); } else { window.open(res.href); } } }; this.handleEvent = function(e) { switch (e.type) { case "keypress": this.move(e); break; case "view is done": this.isReloadableNow(); break; default: throw new Error("should not reach here: " + e.type); } }; this.move = function(e) { var target = e.target; if ( /^(?:INPUT|SELECT|TEXTAREA)$/.test(target.nodeName) || target.isContentEditable ) { return; } switch (e.which) { case 106: //j this.focus(1); break; case 107: //k this.focus(-1); break; case 114: //r this.res(); break; default: } }; } KeyboardNavigation.prototype.registerToDocument = function(doc) { if ( doc === void 0 ) doc = document; doc.addEventListener("keypress", this, false); doc.addEventListener("view is done", this, false); }; function registerKeyboardNavigation(config) { if (config.keyboardNavigation) { var keyboardNavigation = new KeyboardNavigation(config, window); keyboardNavigation.registerToDocument(); } } function setID() { var form = document.forms[0]; if (form) { form.id = "form"; var fonts = form.getElementsByTagName("font"); var link = fonts[fonts.length - 3]; if (link) { link.id = "link"; } } } function setup(config) { applyCss(config); zero(config); addCommonEvents(config); setAccesskeyToV(config); setID(); registerKeyboardNavigation(config); } var shouldQuitHere = function (config, title) { if ( title === void 0 ) title = getTitle(); return (IS_USAMIN && config.viewMode === "s") || title.endsWith(" 個人用環境設定"); }; var quitOrExecute = function (gotConfig) { return function (execute) { return gotConfig.then(function (config) { if (shouldQuitHere(config)) { return; } setup(config); if (IS_USAMIN) { config = Object.create(config); config.deleteOriginal = false; config.useVanishMessage = false; config.useVanishThread = false; config.autovanishThread = false; } return execute(config, function () { return document.dispatchEvent(new Event("view is done")); } ); }); }; }; function main(location) { if ( location === void 0 ) location = window.location; var q = new Query(location.search, location.hostname); switch (q.get("m")) { case "f": //レス窓 tweakResWindow(); return; case "l": //トピック一覧 case "c": //個人用設定 return; case "g": //過去ログ if (!q.shouldHaveValidPosts()) { return; } } var gotConfig = Config.load(); var execute = quitOrExecute(gotConfig); var main = IS_USAMIN ? endMain : canProcessStreamingly() ? streamMain : endMain; main(gotConfig, q, execute); } main(); }());