您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
It adds various features to strangeworld@misao.
当前为
// ==UserScript== // @name tree view for qwerty // @name:ja くわツリービュー // @namespace strangeworld // @description It adds various features to strangeworld@misao. // @description:ja あやしいわーるど@みさおの投稿をツリーで表示できます。スタック表示の方にもいくつか機能を追加できます。 // @match *://misao.mixh.jp/cgi-bin/bbs.cgi* // @match *://misao.biz/cgi-bin/bbs.cgi* // @match *://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_openInTab // @grant GM.openInTab // @grant window.close // @version 10.19 // @run-at document-start // ==/UserScript== (function () { 'use strict'; // @ts-ignore var IS_GM = typeof GM_setValue === "function"; // @ts-ignore 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)); var GMStorage = { /** * @returns {Promise<import("Config").default>} */ load: function () { return new Promise(function (resolve) { var config = Object.create(null); // @ts-ignore var keys = GM_listValues(); var i = keys.length; while (i--) { var key = keys[i]; // @ts-ignore var value = GM_getValue(key); if (value != null) { config[key] = JSON.parse(value); } else { // @ts-ignore GM_deleteValue(key); } } resolve(config); }) }, /** * @param {string|string[]} keyOrKeys - 削除したいキー * @returns {Promise<void>} */ remove: function (keyOrKeys) { return new Promise(function (resolve) { var keys = Array.isArray(keyOrKeys) ? keyOrKeys : [keyOrKeys]; // @ts-ignore keys.forEach(function (key) { return GM_deleteValue(key); }); resolve(); }) }, /** * @param {string} key * @param {any} value * @returns {Promise<any>} */ set: function (key, value) { return new Promise(function (resolve) { // @ts-ignore GM_setValue(key, JSON.stringify(value)); resolve(value); }) }, /** * @param {{}} items - kv * @returns {Promise<void>} */ setAll: function (items) { var this$1$1 = this; return new Promise(function (resolve) { for (var key in items) { this$1$1.set(key, items[key]); } resolve(); }) }, /** * @returns {Promise<void>} */ clear: function () { return new Promise(function (resolve) { // @ts-ignore GM_listValues().forEach(GM_deleteValue); resolve(); }) }, /** * @param {string} key * @returns {Promise<any>} */ get: function (key) { return new Promise(function (resolve) { // @ts-ignore resolve(JSON.parse(GM_getValue(key, "null"))); }) }, }; var GM4Storage = { /** * @returns {Promise<import("Config").default>} */ load: function () { var this$1$1 = this; return this.storage() .listValues() .then(function (keys) { return Promise.all(keys.map(function (key) { return this$1$1.storage().getValue(key); })).then( function (values) { return values.reduce(function (config, value, i) { if (value != null) { config[keys[i]] = JSON.parse(value); } else { this$1$1.remove(keys[i]); } return config }, Object.create(null)); } ); } ) }, /** * @param {string|string[]} keyOrKeys - 削除したいキー * @returns {Promise<any>} */ remove: function (keyOrKeys) { var this$1$1 = this; var keys = Array.isArray(keyOrKeys) ? keyOrKeys : [keyOrKeys]; return Promise.all(keys.map(function (key) { return this$1$1.storage().deleteValue(key); })) }, /** * @param {string} key * @param {any} value * @returns {Promise<any>} */ set: function (key, value) { return this.storage() .setValue(key, JSON.stringify(value)) .then(function () { return key; }) }, /** * @param {{}} items - kv * @returns {Promise<>} */ setAll: function (items) { var promises = []; for (var key in items) { promises.push(this.set(key, items[key])); } return Promise.all(promises) }, /** * @returns {Promise<void>} */ clear: function () { var storage = this.storage(); return storage .listValues() .then(function (keys) { return keys.forEach(storage.deleteValue); }) }, /** * @param {string} key * @returns {Promise<any>} */ get: function (key) { return this.storage().getValue(key, "null").then(JSON.parse) }, storage: function () { // @ts-ignore // GM return GM }, }; var ChromeStorage = { /** * @returns {Promise<import("Config").default>} */ load: function () { var this$1$1 = this; return new Promise(function (resolve) { this$1$1.storage().get(null, resolve); }) }, /** * @param {string|string[]} keyOrKeys - 削除したいキー * @returns {Promise<void>} */ remove: function (keyOrKeys) { var this$1$1 = this; return new Promise(function (resolve) { return this$1$1.storage().remove(keyOrKeys, resolve); }) }, /** * @param {string} key * @param {any} value * @returns {Promise<any>} - value returns */ set: function (key, value) { var this$1$1 = this; return new Promise(function (resolve) { var obj; return this$1$1.storage().set(( obj = {}, obj[key] = value, obj ), resolve); }) }, /** * @param {{}} items - kv * @returns {Promise<void>} */ setAll: function (items) { var this$1$1 = this; return new Promise(function (resolve) { return this$1$1.storage().set(items, resolve); }) }, /** * @returns {Promise<void>} */ clear: function () { var this$1$1 = this; return new Promise(function (resolve) { this$1$1.storage().clear(resolve); }) }, /** * @param {string} key * @returns {Promise<any>} */ get: function (key) { var this$1$1 = this; return new Promise(function (resolve) { return this$1$1.storage().get(key, function (item) { return resolve(item[key]); }); } ) }, storage: function () { // @ts-ignore // chrome return chrome.storage.local }, }; function getStorage$1 () { return IS_GM ? GMStorage : IS_GM4 ? GM4Storage : ChromeStorage } /** * @typedef {"vanishedMessageIDs"|"vanishedThreadIDs"} propertyOfVanishedIDs */ var Config = function Config(config, storage) { Object.assign(this, config); this._storage = storage; }; Config.load = function load () { var storage = getStorage$1(); return storage.load().then(function (config) { if (IS_USAMIN) { config.deleteOriginal = false; config.useVanishMessage = false; config.useVanishThread = false; config.autovanishThread = false; } return new Config(config, storage) }) }; /** * @param {propertyOfVanishedIDs} target * @param {string|string[]} id_or_ids * @return {Promise} */ Config.prototype.addID = function addID (target, id_or_ids) { var this$1$1 = this; var ids = Array.isArray(id_or_ids) ? id_or_ids : [id_or_ids]; this[target] = ids.concat(this[target]); return this._storage.get(target).then(function (/** @type {?string[]} */ 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$1[target] = IDs; return this$1$1._storage.set(target, IDs) }) }; /** * @param {propertyOfVanishedIDs} target * @param {string} id */ Config.prototype.removeID = function removeID (target, id) { var this$1$1 = this; return this._storage.get(target).then(function (/** @type {?string[]} */ ids) { ids = Array.isArray(ids) ? ids : []; var index = ids.indexOf(id); if (index !== -1) { ids.splice(index, 1); this$1$1[target] = ids; return ids.length ? this$1$1._storage.set(target, ids) : this$1$1._storage.remove(target) } else { this$1$1[target] = ids; } }) }; /** * @param {propertyOfVanishedIDs} target */ Config.prototype.clearIDs = function clearIDs (target) { var this$1$1 = this; return this._storage.remove(target).then(function () { this$1$1[target] = []; }) }; /** @param {string|string[]} id_or_ids */ Config.prototype.addVanishedMessage = function addVanishedMessage (id_or_ids) { return this.addID("vanishedMessageIDs", id_or_ids) }; /** @param {string} id */ Config.prototype.removeVanishedMessage = function removeVanishedMessage (id) { return this.removeID("vanishedMessageIDs", id) }; Config.prototype.clearVanishedMessageIDs = function clearVanishedMessageIDs () { return this.clearIDs("vanishedMessageIDs") }; /** @param {string|string[]} id_or_ids */ Config.prototype.addVanishedThread = function addVanishedThread (id_or_ids) { return this.addID("vanishedThreadIDs", id_or_ids) }; /** @param {string} id */ Config.prototype.removeVanishedThread = function removeVanishedThread (id) { return this.removeID("vanishedThreadIDs", id) }; Config.prototype.clearVanishedThreadIDs = function clearVanishedThreadIDs () { return this.clearIDs("vanishedThreadIDs") }; Config.prototype.clear = function clear () { var this$1$1 = this; return this._storage.clear().then(function () { Object.assign(this$1$1, Config.prototype); }) }; /** * @param {{ [key: string]: any; }} items */ Config.prototype.update = function update (items) { var this$1$1 = this; // Config.prototypeとは違うキー var keysToSet = Object.keys(items).filter(function (key) { var value = Config.prototype[key]; var type = typeof value; return type !== "undefined" && type !== "function" && items[key] !== value }); var newConfig = Object.assign.apply( Object, [ Object.create(null) ].concat( keysToSet.map(function (key) { var obj; return (( obj = {}, obj[key] = items[key], obj )); }) ) ); // Config.prototypeと同じキー var keysToRemove = Object.keys(items).filter( function (key) { return items[key] === Config.prototype[key]; } ); return Promise.all([ this._storage.setAll(newConfig), this._storage.remove(keysToRemove) ]).then(function () { Object.assign(this$1$1, newConfig); }) }; Config.prototype.toMinimalJson = function toMinimalJson () { var config = this; return JSON.stringify( Object.keys(Config.prototype) .filter(function (key) { return config[key] !== Config.prototype[key]; }) .reduce( function (newConfig, key) { var obj; return Object.assign(newConfig, ( obj = {}, obj[key] = config[key], obj )); }, Object.create(null) ) ) }; /** * @param {string} id */ Config.prototype.isVanishedThread = function isVanishedThread (id) { return this.useVanishThread && this.vanishedThreadIDs.indexOf(id) > -1 }; Config.prototype.isTreeView = function isTreeView () { return this.viewMode === "t" }; /** @type {"tree-mode-ascii"|"tree-mode-css"} */ Config.prototype.treeMode = "tree-mode-ascii"; Config.prototype.toggleTreeMode = false; Config.prototype.thumbnail = true; Config.prototype.thumbnailPopup = true; Config.prototype.popupAny = false; Config.prototype.popupMaxWidth = ""; Config.prototype.popupMaxHeight = ""; Config.prototype.popupBestFit = true; /** @type {"ascending"|"descending"} ascending スレッド内の最新投稿を基準に、古いものを上に、新しいものを下に。*/ Config.prototype.threadOrder = "ascending"; Config.prototype.NGHandle = ""; Config.prototype.NGWord = ""; Config.prototype.useNG = true; Config.prototype.NGCheckMode = false; Config.prototype.spacingBetweenMessages = false; Config.prototype.useVanishThread = true; /** @type {string[]} */ Config.prototype.vanishedThreadIDs = []; Config.prototype.autovanishThread = false; Config.prototype.utterlyVanishNGThread = false; Config.prototype.useVanishMessage = false; /** @type {string[]} */ Config.prototype.vanishedMessageIDs = []; Config.prototype.vanishMessageAggressive = false; Config.prototype.utterlyVanishMessage = false; /** NGにヒットした投稿を完全に非表示にするか。Stackという名前に反して、ツリーモードでもスタックモードでも使うので注意。 */ Config.prototype.utterlyVanishNGStack = false; Config.prototype.deleteOriginal = true; Config.prototype.zero = true; Config.prototype.accesskeyReload = "R"; Config.prototype.accesskeyV = ""; Config.prototype.keyboardNavigation = false; Config.prototype.keyboardNavigationOffsetTop = "200"; Config.prototype.viewMode = "t"; Config.prototype.css = ""; Config.prototype.shouki = true; Config.prototype.closeResWindow = false; Config.prototype.maxLine = ""; Config.prototype.openLinkInNewTab = false; Config.prototype.characterEntity = true; /** * @typedef {string} ff 日付.dat */ var ConcurrentFetcher = function ConcurrentFetcher(fetcher) { this.fetcher = fetcher; }; /** * @param {ff[]} ffs */ ConcurrentFetcher.prototype.fetch = function fetch (ffs) { var this$1$1 = this; return Promise.all(ffs.map(function (ff) { return this$1$1.fetcher.fetch(ff); })) }; /** * @returns {Promise<string>} */ function ajax(ref) { if ( ref === void 0 ) ref = {}; var type = ref.type; if ( type === void 0 ) type = "GET"; var url = ref.url; if ( url === void 0 ) url = location.href; var data = ref.data; if ( data === void 0 ) 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 defaultExport$4 = function defaultExport(range) { if ( range === void 0 ) range = document.createRange(); this.range = range; }; /** * @param {Node} start * @param {Node} end */ defaultExport$4.prototype.extractContents = function extractContents (start, end) { this.range.setStartBefore(start); this.range.setEndAfter(end); return this.range.extractContents() }; /** * @param {Node} start * @param {Node} end */ defaultExport$4.prototype.deleteContents = function deleteContents (start, end) { this.range.setStartBefore(start); this.range.setEndAfter(end); this.range.deleteContents(); }; /** * @param {Node} wrapper * @param {Node} start * @param {Node} end */ defaultExport$4.prototype.surroundContents = function surroundContents (wrapper, start, end) { this.range.setStartBefore(start); this.range.setEndAfter(end); this.range.surroundContents(wrapper); }; /** * @param {Node} node */ defaultExport$4.prototype.selectNodeContents = function selectNodeContents (node) { this.range.selectNodeContents(node); }; /** * @param {string} html */ defaultExport$4.prototype.createContextualFragment = function createContextualFragment (html) { return this.range.createContextualFragment(html) }; /** * @param {{type?: "GET"|"POST", url?: string, data?: {}}} options `data` is translated into search string */ function fetch (options) { return ajax(options) .then(translateIntoDocumentFragment) .catch(translateIntoDocumentFragment); } function translateIntoDocumentFragment(object) { var range = new defaultExport$4(); var f = range.createContextualFragment(object); return f } var fill = function (n) { return (n < 10 ? "0" + n : String(n)); }; /** * @param {Date} date */ function breakDate (date) { return ({ year: fill(date.getFullYear()), month: fill(date.getMonth() + 1), date: fill(date.getDate()), }); } var RecentOldLogNames = function RecentOldLogNames(now) { if ( now === void 0 ) now = new Date(); this.now = now; }; /** * @param {string} pivot - ff 最近7つのログからこれを除くログ名を返す */ RecentOldLogNames.prototype.generate = function generate (pivot) { var dates = this.getThese7LogNames(pivot); var afters = []; var befores = []; for (var i = 0, list = dates; i < list.length; i += 1) { var date = list[i]; var diff = this.asInt(date) - this.asInt(pivot); if (diff < 0) { befores.push(date); } else if (diff > 0) { afters.push(date); } } return {afters: afters, befores: befores} }; // くずはすくりぷとの過去ログ保存日数はデフォルトで5日間だし、 // 任意の日数を設定できるので7日間/7ヶ月間の決め打ちは良くない。 // 保存方法を月毎にしている場合はログの自動消去が起こらないため無限に溜まる。 RecentOldLogNames.prototype.getThese7LogNames = function getThese7LogNames (pivot) { var dates = []; var back = new Date(this.now); if (this.logsAreSavedDaily(pivot)) { for (var i = 0; i < 7; i++) { var ref = breakDate(back); var year = ref.year; var month = ref.month; var date = ref.date; dates.push(("" + year + month + date + ".dat")); back.setDate(back.getDate() - 1); } } else { for (var i$1 = 0; i$1 < 7; i$1++) { var ref$1 = breakDate(back); var year$1 = ref$1.year; var month$1 = ref$1.month; dates.push(("" + year$1 + month$1 + ".dat")); back.setMonth(back.getMonth() - 1); } } return dates }; /** * @param {string} pivot */ RecentOldLogNames.prototype.logsAreSavedDaily = function logsAreSavedDaily (pivot) { return pivot.length === 12 }; /** * @param {string} string */ RecentOldLogNames.prototype.asInt = function asInt (string) { return Number.parseInt(string, 10) }; /** * @typedef {{fragment: DocumentFragment, ff: ff}} FetchResult * @typedef {string} ff 日付.dat */ var StoppableSequenceFetcher = function StoppableSequenceFetcher(fetcher) { this.fetcher = fetcher; }; /** * @param {ff[]} ffs * @param {ParentNode} container */ StoppableSequenceFetcher.prototype.fetch = function fetch (ffs, container) { var this$1$1 = this; return ffs.reduce( function (sequence, ff) { return sequence.then(function (fetchResults) { if (this$1$1.shouldStop(container)) { return fetchResults } return this$1$1.fetcher.fetch(ff).then(function (fetchResult) { container = fetchResult.fragment; return fetchResults.concat( [fetchResult]) }) }); }, Promise.resolve(/** @type {FetchResult[]} */ ([])) ) }; /** * @param {ParentNode} container */ StoppableSequenceFetcher.prototype.shouldStop = function shouldStop (container) { return this.fetcher.shouldStop(container) }; /** * @typedef {{fragment: DocumentFragment, ff: ff}} FetchResult * @typedef {string} ff 日付.dat */ var Query = function Query(location, referrer) { if ( location === void 0 ) location = window.location; if ( referrer === void 0 ) referrer = document.referrer; /** * @type {{ * m: "g", * sv?: "on", * btn?: "checked", * e?: string, * ff?: string, * kwd?: string, * } | { * m: "s" | "t", * ff?: string, * s: string, * } | { * m?: "f" | "o" | "op" | "p" | "c" | "l" | "n" | "ad", * }} * @param m `g`: 過去ログ * `s`: 投稿者検索 * `t`: スレッド検索 * `f`: レス画面 * `l`: トピック一覧 * `o`|`op`: ログ読み専用 * `ad`: 管理者モード(くずはすくりぷと内では何の判定にも使われていない) * @param sv 検索窓で検索ボタンを押した。検索するログは`chk日付.dat=checked`で指定される(複数可)。 * @param btn 検索窓で`引用機能`にチェックを入れた。レスボタン、投稿者検索ボタン、スレッドボタンが表示される。 * これがあるなら、過去ログの保存形式はバイナリ(.dat)で、過去ログからのフォロー投稿・投稿者検索が可になっている。 * @param e 検索窓で日付のリンクを押した。それに対応するファイル名。`日付.html?`か`日付.dat`。 * くずはすくりぷとの処理上はsvと組み合わせてもいいようだ。 * @param ff `m=s`,`m=t`のときに開くファイル名。`日付.html?`か`日付.dat`。空なら`bbs.log` * @param s `m=s`なら*投稿者名*, `m=t`なら*スレッドID*。 * @param ad 管理者モードは`POST`が使われるので`location.search`からでは読めない。 * @param {"1"} ac `m=f`とともに使う。レス窓の投稿のように、書き込み後「書き込み完了」と表示され、掲示板に戻らない。 */ this.q = Query.parse(location.search); this.search = location.search; this.href = location.href; this.referrer = referrer; }; /** * @param {string} search */ Query.parse = function parse (search) { if (typeof search === "object") { return search } var obj = Object.create(null); var kvs = search.substring(1).split("&"); kvs.forEach(function (kv) { obj[kv.split("=")[0]] = kv.split("=")[1]; }); return obj }; /** * @param {string} key */ Query.prototype.get = function get (key) { return this.q[key] }; /** * @param {string} key * @param {string} value */ Query.prototype.set = function set (key, value) { this.q[key] = value; }; /** * 過去ログでスレッドボタンがあるか? * @returns {boolean} */ Query.prototype.shouldHaveValidPosts = function shouldHaveValidPosts () { if (this.q.m !== "g") { return false } // html形式にはボタンが付かない if (/^\d+\.html?$/.test(this.q.e)) { return false } // 検索ボタンを押した && `引用機能`にチェックが入っている return !!(this.q.sv && this.q.btn) }; Query.prototype.isNormalMode = function isNormalMode () { return !this.q.m }; /** * @param {ParentNode} fragment */ Query.prototype.suggestLink = function suggestLink (fragment) { if (this.searchedBbsLogButOpNotFound(fragment)) { var ref = breakDate(new Date()); var year = ref.year; var month = ref.month; var date = ref.date; return ((this.href) + "&ff=" + year + month + date + ".dat") } }; /** * `bbs.log`内をスレッド検索し、スレッドの先頭が存在ない。 * @param {ParentNode} fragment */ Query.prototype.searchedBbsLogButOpNotFound = function searchedBbsLogButOpNotFound (fragment) { return this.isSearchingBbsLogForThread() && !this.hasOP(fragment) }; /** * 通常モードからスレッドボタンを押した場合 * @private */ Query.prototype.isSearchingBbsLogForThread = function isSearchingBbsLogForThread () { return this.q.m === "t" && !this.q.ff && /^[1-9]\d*$/.test(this.q.s) }; Query.prototype.isSearchingForThreadOrPoster = function isSearchingForThreadOrPoster () { return this.q.m === "t" || this.q.m === "s" }; /** ログ補完するべきか */ Query.prototype.shouldFetch = function shouldFetch () { return this.shouldSearchLog() || this.isFromKomachi() }; /** * 過去ログ検索して、スレッドボタンを押した。くずはすくりぷとはスレッドの投稿を日付を跨いで探してくれない。 * @returns {boolean} */ Query.prototype.shouldSearchLog = function shouldSearchLog () { return ( this.q.m === "t" && /^\d+\.dat$/.test(this.q.ff) && /^[1-9]\d*$/.test(this.q.s) ) }; /** * 小町のlogボタンから来た? */ Query.prototype.isFromKomachi = function isFromKomachi () { // referrerを切ってる人もいるだろうし、referrerのチェックはない方がいい // かもしれないが、わざわざ検索窓で一日だけにチェックを入れて小町のファ // イルを検索する人は、複数日検索されると困ったりするんだろうか。 return ( /^https?:\/\/misao\.(?:mixh|on\.arena\.ne)\.jp\/c\/upload\.cgi/.test( this.referrer ) && /^\?chk\d+\.dat=checked&kwd=https?:\/\/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( this.search ) ) }; /** * @param {ParentNode} fragment */ Query.prototype.fetchOldLogs = function fetchOldLogs (fragment) { return this.run(fragment) }; /** * @param {ParentNode} container - この中にOPがあれば遡らない */ Query.prototype.run = function run (container) { var ref = new RecentOldLogNames().generate( this.getCurrentBbsLogName() ); var afters = ref.afters; var befores = ref.befores; if (this.isFromKomachi()) { befores.length = 0; } var after = this.concurrent(afters); var before = this.sequence(befores, container); return Promise.all([after, before]).then(function (ref) { var afters = ref[0]; var befores = ref[1]; return ({ afters: afters, befores: befores, }); }) }; /** * @param {ff[]} afters */ Query.prototype.concurrent = function concurrent (afters) { return new ConcurrentFetcher(this).fetch(afters) }; /** * @param {ff[]} befores * @param {ParentNode} container */ Query.prototype.sequence = function sequence (befores, container) { return new StoppableSequenceFetcher(this).fetch(befores, container) }; /** * @param {ParentNode} container */ Query.prototype.shouldStop = function shouldStop (container) { return this.hasOP(container) }; /** * スレッド検索中で`container`にスレッドの先頭が含まれているなら`true`、それ以外は`false` * @param {ParentNode} container */ Query.prototype.hasOP = function hasOP (container) { return this.q.m === "t" && !!container.querySelector(this.selectorForOP()) }; Query.prototype.selectorForOP = function selectorForOP () { if (this.q.m === "t") { return 'a[name="' + this.q.s + '"]' } }; /** * @param {ff} ff * @returns {Promise<FetchResult>} */ Query.prototype.fetch = function fetch$1 (ff) { return fetch({url: "bbs.cgi", data: this.queryFor(ff)}).then(function (fragment) { return {fragment: fragment, ff: ff} }) }; /** * @param {ff} ff */ Query.prototype.queryFor = function queryFor (ff) { return Object.assign( Object.create(null), this.q, // m=gのときの検索対象は、eに指定されたファイルが優先で、次にchk日付.dat=checked // m=tのときの検索対象は、ffに指定されたファイル this.isFromKomachi() ? {e: ff} : {ff: ff} ) }; Query.prototype.getCurrentBbsLogName = function getCurrentBbsLogName () { if (this.q.m === "g") { if (this.q.e) { return this.q.e } else { return Object.keys(this.q) .find(function (key) { return /^chk\d+\.dat$/.test(key); }) .replace(/^chk/, "") } } else if (this.q.m === "t") { return this.q.ff } }; /** @public */ Query.prototype.getLogName = function getLogName () { return this.getCurrentBbsLogName() }; /** * 内容欄にフォーカスして表示 * @param {HTMLBodyElement} body */ function tweak (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; } } /** * @returns {Promise<void>} */ function ready$1(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" || // @ts-ignore // doScroll (readyState !== "loading" && !doc.documentElement.doScroll) ) { resolve(); } else { doc.addEventListener("DOMContentLoaded", function () { return resolve(); }, { capture: capture, once: true, }); } }) } function getBody() { return document.body } function tweakResWindow () { return ready$1().then(getBody).then(tweak); } var nullAnchor = document.createElement("a"); Object.defineProperties(nullAnchor, { outerHTML: {value: ""}, search: { get: function get() { return "" }, set: function set() {}, }, }); var a = document.createElement("a"); a.href = ">"; var gt = a.outerHTML === '<a href=">"></a>'; var replacer = function (rel) { return function (match) { var href = match.replace(/"/g, """); if (gt) { href = href.replace(/>/g, ">").replace(/</g, "<"); } return ("<a href=\"" + href + "\" target=\"link\" rel=\"" + rel + "\">" + match + "</a>") }; }; /** * @param {string} url */ function relinkify (url, rel) { if ( rel === void 0 ) rel = "noreferrer noopener"; return url.replace( /(?:https?|ftp|gopher|telnet|whois|news):\/\/[\x21-\x7e]+/gi, replacer(rel) ) } var Post = function Post(id, postParent) { this.id = id; this.postParent = postParent; /** @type {Post} */ this.parent = null; /** @type {Post} */ this.child = null; /** @type {Post} */ this.next = null; /** @type {boolean} */ this.isNG = null; }; /** * @param {Post} l * @param {Post} r */ Post.byID = function byID (l, r) { return +l.id - +r.id }; /** * @param {import("NG").default} ng * @param {Post} post * @returns */ Post.checkNG = function checkNG (ng, post) { post.isNG = ng.testWord(post.text) || ng.testHandle(post.name) || ng.testHandle(post.title); }; /** * @param {import("NG").default} ng */ Post.prototype.checkNG = function checkNG (ng) { Post.checkNG(ng, this); }; /** * @param {Post} post * @returns {boolean} */ Post.wantsParent = function wantsParent (post) { return !!post.parentId }; /** * @param {Post} post * @returns {boolean} */ Post.isOrphan = function isOrphan (post) { return post.parent === null && !!post.parentId }; /** * @param {Post} post * @returns {boolean} */ Post.isRootCandidate = function isRootCandidate (post) { return post.parent === null }; /** * @param {Post} post */ Post.mayHaveParent = function mayHaveParent (post) { return post.mayHaveParent() }; /** * 個別非表示でない * @param {Post} post */ Post.isClean = function isClean (post) { return !post.rejectLevel }; Post.prototype.isOP = function isOP () { return this.id === this.threadId }; Post.prototype.getText = function getText () { if (this.hasDefaultReference()) { return this.text.slice(0, this.text.lastIndexOf("\n\n")) //参考と空行を除去 } return this.text }; Post.prototype.hasDefaultReference = function hasDefaultReference () { var parent = this.parent; if (!parent) { return false } if (parent.date === this.parentDate) { return true } // usaminは、ヘッダの日時の表示の仕方が違う if (IS_USAMIN) { var ref = /^(\d+)\/(\d+)\/(\d+) \(([月火水木金土日])\) (\d+):(\d+):(\d+)$/.exec( parent.date ) || []; 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 + "秒") ) } else { return false } }; Post.prototype.computeQuotedText = function computeQuotedText () { 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="([^"]*)")?>([^<]+)<\/a>/gi, this.relinkify ) .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 }; /** * @param {any} _ * @param {string} rel * @param {string} url */ Post.prototype.relinkify = function relinkify$1 (_, rel, url) { return relinkify(url, rel) }; Post.prototype.textCandidate = function textCandidate () { 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"; }; Post.prototype.textCandidateLooksValid = function textCandidateLooksValid () { return ( this.getText() .replace(/^> .*/gm, "") .trim() !== "" ) }; Post.prototype.dateCandidate = function dateCandidate () { return this.parentDate }; Post.prototype.dateCandidateLooksValid = function dateCandidateLooksValid (candidate) { return /^\d{4}\/\d{2}\/\d{2}\(.\)\d{2}時\d{2}分\d{2}秒$/.test(candidate) }; Post.prototype.hasQuote = function hasQuote () { return /^> /m.test(this.text) }; Post.prototype.mayHaveParent = function mayHaveParent () { return this.isRead && !this.isOP() && this.hasQuote() }; Post.prototype.adoptAsEldestChild = function adoptAsEldestChild (childToBeAdopted) { var child = this.child; if (child) { childToBeAdopted.next = child; } this.child = childToBeAdopted; childToBeAdopted.parent = this; }; Post.prototype.getKeyForOwnParent = function getKeyForOwnParent () { return this.parentId }; /** * @returns {Post} */ Post.prototype.makeParent = function makeParent () { throw new Error("Should not be called") }; /** * @param {string[]} vanishedMessageIDs */ Post.prototype.computeRejectLevel = function computeRejectLevel (vanishedMessageIDs) { this.computeRejectLevelForRoot(vanishedMessageIDs, this.id); if (this.child) { this.child.inheritRejectLevel(vanishedMessageIDs, this.rejectLevel - 1); } }; /** * @param {string[]} vanishedMessageIDs * @param {string} id */ Post.prototype.computeRejectLevelForRoot = function computeRejectLevelForRoot (vanishedMessageIDs, id, level) { if ( level === void 0 ) level = 3; if (!id || level === 0) { this.rejectLevel = 0; return } if (vanishedMessageIDs.indexOf(id) > -1) { this.rejectLevel = level; return } this.computeRejectLevelForRoot( vanishedMessageIDs, this.postParent.get(id), level - 1 ); }; Post.prototype.inheritRejectLevel = function inheritRejectLevel (vanishedMessageIDs, generation) { var rejectLevel = 0; if (vanishedMessageIDs.indexOf(this.id) > -1) { rejectLevel = 3; } else if (generation > 0) { rejectLevel = generation; } this.rejectLevel = rejectLevel; if (this.child) { this.child.inheritRejectLevel(vanishedMessageIDs, rejectLevel - 1); } if (this.next) { this.next.inheritRejectLevel(vanishedMessageIDs, generation); } }; /** * @param {Array<Post>} newRoots 新しい`root`ならこれに追加される * @param {boolean} [isRoot=true] このメソッドを呼ぶときに`root`でないなら`false` * @returns {?Post} */ /* isRoot は post.parent == null で代用できる? */ Post.prototype.drop = function drop (newRoots, isRoot) { if ( isRoot === void 0 ) isRoot = true; if (this.child) { this.child = this.child.drop(newRoots, false); } if (this.next) { this.next = this.next.drop(newRoots, false); } if (this.rejectLevel === 0) { if (this.isRead && !this.child) { return this.next } if (isRoot) { newRoots.push(this); } return this } else { if (this.child && this.child.rejectLevel === 0) { newRoots.push(this.child); } return this.next } }; Post.prototype.hasChildren = function hasChildren () { return this.child && this.child.next }; Post.prototype.getYoungestChild = function getYoungestChild () { var youngest = this.child; if (!youngest) { return null } while (youngest.next) { youngest = youngest.next; } return youngest }; Post.prototype.appendFfToButtons = function appendFfToButtons () { var ref = this.date.match(/\d+/g) || []; var year = ref[0]; var month = ref[1]; var day = ref[2]; var ff = "&ff=" + year + month + day + ".dat"; for (var i = 0, list = ["threadButton", "resButton", "posterButton"]; i < list.length; i += 1) { var target = list[i]; this[target].search = this[target].search + ff; } }; /** * @param {{visit: (post: Post, depth: number) => void}} visitor * @param {number} depth */ Post.prototype.accept = function accept (visitor, depth) { if ( depth === void 0 ) depth = 1; visitor.visit(this, depth); if (this.child) { this.child.accept(visitor, depth + 1); } if (this.next) { this.next.accept(visitor, depth); } }; Post.prototype.id = ""; Post.prototype.title = " "; Post.prototype.name = " "; Post.prototype.date = ""; Post.prototype.resButton = nullAnchor; Post.prototype.posterButton = nullAnchor; Post.prototype.threadButton = nullAnchor; Post.prototype.threadId = ""; /** うさみんの、どの掲示板からの投稿かを示すもの [misao] */ Post.prototype.site = ""; /** うさみん特有のボタン */ Post.prototype.usaminButtons = ""; /** * 親のid。string: 自然数の文字列。null: 親なし。undefined: 不明。 * @type {(undefined|?string)} */ Post.prototype.parentId = null; Post.prototype.parentDate = ""; Post.prototype.text = ""; Post.prototype.env = null; Post.prototype.showAsIs = false; Post.prototype.rejectLevel = 0; Post.prototype.isRead = false; Post.prototype.characterEntity = false; /** 非表示投稿の強制表示 */ Post.prototype.showForcibly = false; Post.prototype.truncation = false; Post.prototype.textBonus = 2; Post.prototype.dateBonus = 100; Post.prototype.parent = null; Post.prototype.child = null; Post.prototype.next = null; var ImaginaryPostPrototype = /*@__PURE__*/(function (Post) { function ImaginaryPostPrototype(child, postParent) { Post.call(this, child.parentId, postParent); this.setFields(child); } if ( Post ) ImaginaryPostPrototype.__proto__ = Post; ImaginaryPostPrototype.prototype = Object.create( Post && Post.prototype ); ImaginaryPostPrototype.prototype.constructor = ImaginaryPostPrototype; var prototypeAccessors = { text: { configurable: true } }; /** * @param {Post} child */ ImaginaryPostPrototype.prototype.setFields = function setFields (child) { this.threadId = child.threadId; this.threadButton = child.threadButton; this.parentId = this.isOP() ? null : this.postParent.get(this.id); if (this.id) { this.setResButton(child); } }; ImaginaryPostPrototype.prototype.calculate = function calculate (property) { var candidates = this.collectCandidates(property); var value = this.pickMostAppropriateCandidate(candidates); return Object.defineProperty(this, property, {value: value})[property] }; ImaginaryPostPrototype.prototype.collectCandidates = function collectCandidates (property) { var getCandidate = property + "Candidate"; var validates = getCandidate + "LooksValid"; var bonus = this[(property + "Bonus")]; var ranks = Object.create(null); var child = this.child; while (child) { var candidate = child[getCandidate](); ranks[candidate] = (ranks[candidate] || 0) + 1; if (child[validates](candidate)) { ranks[candidate] += bonus; } child = child.next; } return ranks }; ImaginaryPostPrototype.prototype.pickMostAppropriateCandidate = function pickMostAppropriateCandidate (ranks) { var winner; var max = 0; for (var candidate in ranks) { var rank = ranks[candidate]; if (max < rank) { max = rank; winner = candidate; } } return winner }; ImaginaryPostPrototype.prototype.getText = function getText () { return this.text }; ImaginaryPostPrototype.prototype.setResButton = function setResButton (child) { var resButton = child.resButton.cloneNode(true); resButton.search = resButton.search.replace(/(?:&s=)\d+/, "&s=" + this.id); this.resButton = resButton; }; ImaginaryPostPrototype.prototype.getKeyForOwnParent = function getKeyForOwnParent () { return this.parentId ? this.parentId : "parent of " + this.id }; // @ts-ignore prototypeAccessors.text.get = function () { return this.calculate("text") }; Object.defineProperties( ImaginaryPostPrototype.prototype, prototypeAccessors ); return ImaginaryPostPrototype; }(Post)); ImaginaryPostPrototype.prototype.isRead = true; var GhostPost = /*@__PURE__*/(function (ImaginaryPostPrototype) { function GhostPost () { ImaginaryPostPrototype.apply(this, arguments); } if ( ImaginaryPostPrototype ) GhostPost.__proto__ = ImaginaryPostPrototype; GhostPost.prototype = Object.create( ImaginaryPostPrototype && ImaginaryPostPrototype.prototype ); GhostPost.prototype.constructor = GhostPost; GhostPost.prototype.getIdForcibly = function getIdForcibly () { return this.postParent.findAsync(this.child) }; return GhostPost; }(ImaginaryPostPrototype)); GhostPost.prototype.date = "?"; var MergedPost = /*@__PURE__*/(function (ImaginaryPostPrototype) { function MergedPost(child, postParent) { ImaginaryPostPrototype.call(this, child, postParent); if (child.title.startsWith(">")) { this.name = child.title.slice(1); } } if ( ImaginaryPostPrototype ) MergedPost.__proto__ = ImaginaryPostPrototype; MergedPost.prototype = Object.create( ImaginaryPostPrototype && ImaginaryPostPrototype.prototype ); MergedPost.prototype.constructor = MergedPost; var prototypeAccessors = { date: { configurable: true } }; MergedPost.prototype.makeParent = function makeParent () { return new GhostPost(this, this.postParent) }; // @ts-ignore prototypeAccessors.date.get = function () { return this.calculate("date") }; Object.defineProperties( MergedPost.prototype, prototypeAccessors ); return MergedPost; }(ImaginaryPostPrototype)); var ActualPost = /*@__PURE__*/(function (Post) { function ActualPost () { Post.apply(this, arguments); } if ( Post ) ActualPost.__proto__ = Post; ActualPost.prototype = Object.create( Post && Post.prototype ); ActualPost.prototype.constructor = ActualPost; ActualPost.prototype.makeParent = function makeParent () { return new MergedPost(this, this.postParent) }; return ActualPost; }(Post)); /** * @param {"nextSibling"|"nextElementSibling"} type - トラバースの仕方 * @param {string} nodeName - タグ名 * @param {Node} node - スタート地点 * @returns {(nodeName: string) => (node: Node) => ?Node} */ function next (type) { return function (nodeName) { return function (node) { while ((node = node[type])) { if (node.nodeName === nodeName) { return node } } }; }; } /** @type {function(string): function(HTMLElement): HTMLElement} */ // @ts-ignore var nextElement = next("nextElementSibling"); /** @type {function(Node): ?HTMLFontElement} */ // @ts-ignore var nextFont = nextElement("FONT"); /** @type {function(Node): ?HTMLElement} */ var nextB = nextElement("B"); /** @type {function(Node): ?HTMLQuoteElement} */ // @ts-ignore var nextBlockquote = nextElement("BLOCKQUOTE"); /** @param {HTMLAnchorElement} anchor */ function collectEssentialElements(anchor) { var header = nextFont(anchor); var title = /** @type {HTMLElement} */ (header.firstChild); var name = nextB(header); var info = nextFont(name); var date = /** @type {Text} */ (info.firstChild); // レスボタン var resButton = /** @type {HTMLAnchorElement} */ (info.firstElementChild); var posterButton, threadButton; var nextButton = /** @type {?HTMLAnchorElement} */ (resButton.nextElementSibling); // 投稿者検索ボタン? if (nextButton && nextButton.search && nextButton.search.startsWith("?m=s")) { posterButton = /** @type {HTMLAnchorElement} */ nextButton; nextButton = /** @type {?HTMLAnchorElement} */ (nextButton.nextElementSibling); } // スレッドボタン? if (nextButton) { threadButton = /** @type {HTMLAnchorElement} */ nextButton; } var blockquote = nextBlockquote(info); var pre = /** @type {HTMLPreElement} */ (blockquote.firstElementChild); return { anchor: anchor, title: title, name: name, date: date, resButton: resButton, posterButton: posterButton, threadButton: threadButton, blockquote: blockquote, pre: pre, } } /** * 新しいのが先 * @param {ParentNode} context * @param {import("postParent/PostParent").default} postParent */ function makePosts (context, postParent) { var posts = IS_USAMIN ? makePostsUsamin(context, postParent) : makePostsKuzuha(context, postParent); sortByTime(posts); return posts } /** * @param {ParentNode} context * @param {import("postParent/PostParent").default} postParent * @returns {ActualPost[]} */ var makePostsKuzuha = function (context, postParent) { /** @type {ActualPost[]} */ var posts = []; /** @type {NodeListOf<HTMLAnchorElement>} */ var as = context.querySelectorAll("a[name]"); for (var i = 0, len = as.length; i < len; i++) { var a = as[i]; var el = collectEssentialElements(a); var post = new ActualPost(a.name, postParent); posts.push(post); post.title = el.title.innerHTML; post.name = el.name.innerHTML; post.date = el.date.nodeValue.trim().slice(4); //「投稿日:」削除 post.resButton = el.resButton; if (el.posterButton) { post.posterButton = el.posterButton; } if (el.threadButton) { post.threadButton = /** @type {HTMLAnchorElement} */ (el.threadButton.cloneNode( true )); post.threadId = /&s=([^&]+)/.exec(post.threadButton.search)[1]; } else { var id = post.id; var threadButton = /** @type {HTMLAnchorElement} */ (el.resButton.cloneNode( true )); threadButton.search = threadButton.search .replace(/^\?m=f/, "?m=t") .replace(/&[udp]=[^&]*/g, "") .replace(/(&s=)\d+/, ("$1" + id)); threadButton.text = "◆"; post.threadButton = threadButton; post.threadId = id; } var env = nextFont(el.pre); if (env) { post.env = /** @type {HTMLElement} */ (env.firstChild).innerHTML; // font > i > env } var ref = breakdownPre(el.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; } } return posts }; /** * @param {ParentNode} context * @param {import("postParent/PostParent").default} postParent * @returns {ActualPost[]} */ var makePostsUsamin = function (context, postParent) { 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 } } }; return Array.prototype.map.call(as, function (a) { var post = new ActualPost(a.id, postParent); 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(); post.threadButton = info.firstElementChild; post.usaminButtons = info.innerHTML .replace(/^[^<]+/, "") .replace(/[^>]*$/, "") .replace(/\s+/g, " "); post.site = info.lastChild.textContent; var pre = nextPre(info); var ref = 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; } return post }) }; /** * * @param {string} html * @param {string} id * @returns */ var breakdownPre = function (html, id) { var assign; var parentId, parentDate; var text = html .replace(/<\/?font[^>]*>/gi, "") .replace(/\r\n?/g, "\n") .replace(/\n$/, ""); if (text.includes("<A")) { text = text.replace( //属性内の " < > は以下のようになる //chrome " < > //firefox91 " < > //opera12 " < > //firefox56 " < > //古いfirefox %22 %3C %3E /<A href="<a href="(.*)(?:"|%22)"( target="link"(?: rel="noreferrer noopener")?)>\1"<\/a>\2><a href="\1(?:<\/A>|<\/A>|%3C\/A%3E)"\2>\1<\/A><\/a>/g, '<a href="$1" target="link">$1</a>' ); } var candidate = text; var reference = /\n\n<a href="h[^"]+&s=([1-9]\d*)&r=[^"]+">参考:([^<]+)<\/a>$/.exec( text ) || /\n\n<a href="#([1-9]\d*)">参考:([^<]+)<\/a>$/.exec(text); if (reference) { (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, } }; /** * 新しいのが先 * @param {ActualPost[]} posts */ var sortByTime = function (posts) { if (posts.length >= 2 && +posts[0].id < +posts[1].id) { posts.reverse(); } }; function originalRange (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 } /** * @typedef {import("./ActualPost").default} ActualPost */ var Context = function Context(config, q, postParent) { this.config = config; this.q = q; this.postParent = postParent; }; /** * @param {ParentNode} fragment * @param {() => void} callback called before fetching posts from external resource */ Context.prototype.makePosts = function makePosts (fragment, callback) { return this.collectPosts(fragment, callback) }; /** * @param {ParentNode} fragment * @param {() => void} callback * @return {Promise<ActualPost[]>} */ Context.prototype.collectPosts = function collectPosts (fragment, callback) { var this$1$1 = this; return new Promise(function (resolve) { var posts = makePosts(fragment, this$1$1.postParent); if (this$1$1.q.shouldFetch()) { callback(); var makePostsAndConcat = function ( /** @type {ActualPost[]} */ posts, ref ) { var fragment = ref.fragment; return posts.concat( makePosts(fragment, this$1$1.postParent)); }; return this$1$1.q .fetchOldLogs(fragment) .then(function (ref) { var afters = ref.afters; var befores = ref.befores; return afters.reduce(makePostsAndConcat, []).concat( posts, befores.reduce(makePostsAndConcat, []) ); }) .then(resolve) } else { resolve(posts); } }).then(function (posts) { this$1$1.postParent.insert(posts); this$1$1.makeButtonsPointToOldLog(posts); return posts }) }; /** * @param {ActualPost[]} posts */ Context.prototype.makeButtonsPointToOldLog = function makeButtonsPointToOldLog (posts) { if (this.q.isSearchingForThreadOrPoster()) { posts.forEach(function (post) { post.appendFfToButtons(); }); } }; /** * @param {ParentNode} fragment */ Context.prototype.suggestLink = function suggestLink (fragment) { return this.q.suggestLink(fragment) }; Context.prototype.getLogName = function getLogName () { return this.q.getLogName() }; /** * `config.deleteOriginal` = true の場合はない振りをする * @param {ParentNode} fragment */ Context.prototype.extractOriginalPostsAreaFrom = function extractOriginalPostsAreaFrom (fragment) { if (IS_USAMIN) { return document.createDocumentFragment() } var range = originalRange(fragment); if (this.config.deleteOriginal) { range.deleteContents(); return document.createDocumentFragment() } else { return range.extractContents() } }; /** * @param {function} fn * @returns {(arg: string) => any} */ function memoize(fn) { var cache = {}; return function (arg) { if (!Object.prototype.hasOwnProperty.call(cache, arg)) { cache[arg] = fn(arg); } return cache[arg] } } /** * type のストレージが利用可能か * @param {"localStorage"|"sessionStorage"} type * @param {Window} win * @returns */ 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 InMemoryStorage = function InMemoryStorage() { this.data = Object.create(null); }; InMemoryStorage.prototype.setItem = function setItem (k, v) { this.data[k] = v; }; InMemoryStorage.prototype.getItem = function getItem (k) { var v = this.data[k]; return v != null ? v : null }; InMemoryStorage.prototype.removeItem = function removeItem (k) { delete this.data[k]; }; /** * @param {import("Config").default} config * @returns {{setItem: (k: string, v: string) => void; getItem: (k: string) => ?string; removeItem: (k: string) => void}} */ var getStorage = function (config) { if (IS_USAMIN) { return new InMemoryStorage() } if (config.useVanishMessage && storageIsAvailable("localStorage")) { return localStorage } if (storageIsAvailable("sessionStorage")) { return sessionStorage } return new InMemoryStorage() }; /** * @typedef {{[id: string]: string}} IdMap * @typedef {import("Post").default} Post */ var PostParent = function PostParent(config) { this.config = config; this.storage = null; /** @type {IdMap} */ this.data = null; this.updateThread = memoize(this.updateThread.bind(this)); }; /** * @param {Post[]} posts */ PostParent.prototype.insert = function insert (posts) { if (!posts.length) { return } this.load(); for (var i = 0; i < posts.length; i++) { var ref = posts[i]; var id = ref.id; var parentId = ref.parentId; if (Object.prototype.hasOwnProperty.call(this.data, id)) { continue } this.data[id] = parentId; this.changed = true; } this.cleanUpAndSave(); }; PostParent.prototype.load = function load () { if (!this.storage) { this.storage = getStorage(this.config); } if (!this.data) { this.data = JSON.parse(this.storage.getItem("postParent")) || {}; } }; PostParent.prototype.cleanUpAndSave = function cleanUpAndSave () { if (!this.changed) { return } var ids = Object.keys(this.data); var limits = this.getLimits(); if (ids.length <= limits.upper) { this.save(this.data); return } ids = ids .map(function (id) { return +id; }) .sort(function (l, r) { return r - l; }) .map(function (id) { return "" + id; }); // @ts-ignore // migrationだった気がする if (this.data[ids[0]] === false) { ids.shift(); } /** @type {IdMap} */ var saveData = Object.create(null); var i = limits.lower; while (i--) { saveData[ids[i]] = this.data[ids[i]]; } this.save(saveData); }; PostParent.prototype.getLimits = function getLimits () { var config = this.config; if (!config.useVanishMessage) { // 最新の投稿の親の親がこれくらいに収まる return {upper: 500, lower: 300} } if (config.vanishMessageAggressive) { // 一日の投稿数の平均が3000件超えくらいだった return {upper: 3500, lower: 3300} } else { // 1000件目の親の親がこれくらいに収まる return {upper: 1500, lower: 1300} } // なぜ差が200なのかは覚えていない }; /** * @param {IdMap} data */ PostParent.prototype.save = function save (data) { this.storage.setItem("postParent", JSON.stringify(data)); }; /** * @public * @param {string} id * @returns {string=} */ PostParent.prototype.get = function get (id) { this.load(); return this.data[id] }; /** * 親のIDを返す * @public * @param {Object} data * @param {string} data.id 子のId * @param {string} data.threadId 探索するスレッドのId * @returns {Promise<string|undefined>} */ PostParent.prototype.findAsync = function findAsync (ref) { var this$1$1 = this; var id = ref.id; var threadId = ref.threadId; if (this.shouldFetch(id, threadId)) { return this.updateThread(threadId).then(function () { return this$1$1.get(id); }) } else { return Promise.resolve(this.get(id)) } }; /** * @param {string} childId * @param {string} threadId */ PostParent.prototype.areValidIds = function areValidIds (childId, threadId) { return ( /^(?!0)\d+$/.test(threadId) && /^(?!0)\d+$/.test(childId) && +threadId <= +childId ) }; PostParent.prototype.isPersistentStorage = function isPersistentStorage () { return !(this.storage instanceof InMemoryStorage) }; /** * @param {string} childId * @param {string} threadId */ PostParent.prototype.shouldFetch = function shouldFetch (childId, threadId) { return ( typeof this.data[childId] === "undefined" && this.isPersistentStorage() && this.areValidIds(childId, threadId) ) }; /** * @param {string} threadId */ PostParent.prototype.updateThread = function updateThread (threadId) { var this$1$1 = this; return fetch({data: {m: "t", s: threadId}}) .then(function (fragment) { return makePosts(fragment, this$1$1); }) .then(this.insert.bind(this)) }; /** * @typedef {import("main/PrestageController").Presenter} Presenter * @implements {Presenter} */ var StackPresenter = function StackPresenter(config, q, range) { if ( range === void 0 ) range = new defaultExport$4(); this.config = config; this.q = q; this.range = range; /** @type {import("./StackView").default} */ this.view = null; }; StackPresenter.prototype.setView = function setView (view) { this.view = view; }; StackPresenter.prototype.clearVanishedThreadIDs = function clearVanishedThreadIDs () { var this$1$1 = this; return this.config .clearVanishedThreadIDs() .then(function () { return this$1$1.view.clearVanishedThreadIDs(); }) }; StackPresenter.prototype.removeVanishedThread = function removeVanishedThread (threadId) { return this.config.removeVanishedThread(threadId) }; StackPresenter.prototype.addVanishedThread = function addVanishedThread (threadId) { return this.config.addVanishedThread(threadId) }; /** * @param {ParentNode} fragment `fragment`の先頭は通常は空白。ログの一番先頭のみ\<A> */ StackPresenter.prototype.render = function render (fragment) { this.view.render(fragment); }; /** * @param {ParentNode} fragment */ StackPresenter.prototype.finish = function finish (fragment) { var this$1$1 = this; this.view.finishFooter(fragment); return new Promise(function (resolve) { if (this$1$1.shouldFetch()) { this$1$1.complementThread().then(resolve); } else { resolve(); } }).then(function () { return this$1$1.view.finish(); }) }; StackPresenter.prototype.shouldFetch = function shouldFetch () { return this.q.shouldFetch() }; StackPresenter.prototype.complementThread = function complementThread () { var this$1$1 = this; this.view.showIsSearchingOldLogsExceptFor(this.q.getLogName()); return this.q.fetchOldLogs(this.view.el).then(function (ref) { var befores = ref.befores; var afters = ref.afters; this$1$1.view.setBeforesAndAfters(this$1$1.q.getLogName(), befores, afters); this$1$1.view.doneSearchingOldLogs(); }) }; var NG = function NG(config) { var word = config.NGWord; var handle = config.NGHandle; var isInvalid = ""; if (config.useNG) { if (handle) { try { this.handle_ = new RegExp(handle); this.handleg_ = new RegExp(handle, "g"); } catch (e) { isInvalid += "NGワード(ハンドル)が不正です。"; } } if (word) { try { this.word_ = new RegExp(word); this.wordg_ = new RegExp(word, "g"); } catch (e$1) { isInvalid += "NGワード(本文)が不正です。"; } } } this.message = isInvalid ? ("<span>" + isInvalid + "NGワードを適用しませんでした</span>") : ""; this.isEnabled = !!(this.word_ || this.handle_); }; NG.prototype.mark = function mark (reg, value) { return value.replace(reg, "<mark class='NGWordHighlight'>$&</mark>") }; NG.prototype.markWord = function markWord (value) { return this.mark(this.wordg_, value) }; NG.prototype.markHandle = function markHandle (value) { return this.mark(this.handleg_, value) }; NG.prototype.testWord = function testWord (value) { if (this.word_) { return this.word_.test(value) } }; NG.prototype.testHandle = function testHandle (value) { if (this.handle_) { return this.handle_.test(value) } }; /** * @param {HTMLElement} el * @param {string} event * @param {string} selector * @param {EventListenerOrEventListenerObject} callback */ function on (el, event, selector, callback) { el.addEventListener(event, function (e) { if (/** @type {HTMLElement} */ (e.target).closest(selector)) { if ("handleEvent" in callback) { callback.handleEvent(e); } else { callback(e); } } }); } function sendMessageToRuntime(message) { // @ts-ignore // chrome chrome.runtime.sendMessage(message); } function closeTab() { if (IS_EXTENSION) { sendMessageToRuntime({type: "closeTab"}); } else { window.open("", "_parent"); window.close(); } } /** * @type {NodeJS.Timeout} */ var id; /** * * @param {string} after - 終了後に表示されるテキスト * @param {HTMLElement} el - テキストを表示する場所 * @param {() => Promise} fun - これが終わったら after を5秒間表示する */ function progress(after, el, fun) { clearTimeout(id); el.textContent = "保存中"; return new Promise(function (resolve) { setTimeout(function () { fun().then(function () { el.textContent = after; id = setTimeout(function () { el.innerHTML = ""; resolve(); }, 5000); }); }); }) } /** * * @param {import("Config").default} item */ function ConfigController(item) { var this$1$1 = this; this.item = item; var el = document.createElement("form"); el.id = "config"; this.el = el; var events = [ "save", "clear", "close", "showExportArea", "showImportArea", "import", "clearVanishThread", "clearVanishMessage", "addToNGWord" ]; for (var i = events.length - 1; i >= 0; i--) { var event = events[i]; on(el, "click", "#" + event, this[event].bind(this)); } on(el, "keyup", "#quote-input", this.quotemeta.bind(this)); this.render(); this.invalidRegExp = {} ;["#NGWord", "#NGHandle"].forEach(function (target) { on(el, "input", target, this$1$1.validateRegExp.bind(this$1$1, target)); this$1$1.validateRegExp(target); }); if (this.areNGRegExpsInvalid()) { this.showRegExpNotes(); } } ConfigController.prototype = { /** * querySelector * @param {string} selector * @returns {HTMLInputElement} */ $: function (selector) { return this.el.querySelector(selector) }, /** * querySelectorAll * @param {string} selector * @returns {HTMLInputElement[]} */ $$: function (selector) { return Array.prototype.slice.call(this.el.querySelectorAll(selector)) }, render: function () { this.el.innerHTML = this.template(); 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@#vanishMessage">投稿非表示機能の注意点</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="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> <span id="NGHandleNote" style="display:none;"></span>\ <tr>\ <td><label for="NGWord">本文</label>\ <td><input id="NGWord" type="text" name="NGWord" size="30"> <span id="NGWordNote" style="display:none;"></span>\ <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>\ <fieldset>\ <legend>エクスポート/インポート</legend>\ <input type="button" id="showExportArea" value="エクスポート"/>\ <input type="button" id="showImportArea" value="インポート"/>\ <div class="exportArea" style="display:none">\ <textarea rows="5" cols="50"></textarea>\ </div>\ <div class="importArea" style="display:none">\ <textarea rows="5" cols="50"></textarea>\ <div class="import" style="flex-direction:column;align-items:flex-start">\ <input type="button" id="import" value="インポートする"/>\ <span class="import note"></span>\ </div>\ </div>\ </fieldset>\ <p style="display:flex;justify-content:space-between">\ <span>\ <input type="submit" id="save" accesskey="s" title="くわツリービューの設定を保存する" value="保存[s]">\ <input type="button" id="close" accesskey="c" title="くわツリービューの設定を閉じる" value="閉じる[c]">\ <span id="configInfo"></span>\ </span>\ <span>\ <input type="button" id="clear" value="デフォルトに戻す">\ </span>\ </p>\ </fieldset>'.replace( /@GF@/g, "https://greasyfork.org/scripts/1971-tree-view-for-qwerty" ) }, scrollIntoView: function scrollIntoView() { this.el.scrollIntoView(); }, showExportArea: function showExportArea() { this.toggleExportImportArea(".exportArea", this.item.toMinimalJson()); }, showImportArea: function showImportArea() { this.toggleExportImportArea(".importArea", ""); }, /** * * @param {string} targetClassToShow * @param {string} text - 表示するテキスト */ toggleExportImportArea: function toggleExportImportArea(targetClassToShow, text) { this.$((targetClassToShow + " textarea")).value = text; this.$$(".importArea, .exportArea").forEach(function (el) { el.style.display = "none"; }); this.$(targetClassToShow).style.display = "flex"; }, import: function import$1() { var this$1$1 = this; var text = this.$(".importArea textarea").value.replace(/^\s+|\s+$/g, ""); var note = this.$(".importArea .note"); note.textContent = ""; if (text === "") { return } try { var json = JSON.parse(text); this.info("インポートしました。", function () { return this$1$1.item.update(json); }); } catch (e) { note.textContent = "データが不正のため、インポート出来ませんでした。"; } }, quotemeta: function () { var output = this.$("#quote-output"); var input = this.$("#quote-input"); output.value = 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 = ""; }); }, validateRegExp: function (target) { var regexp = this.$(target).value; var note = this.$((target + "Note")); try { //chrome70くらい: regを使わないと、最適化(?)でnew RegExp(regexp)が削除されてしまい //文法にミスがあってもエラーが発生しない var reg = new RegExp(regexp); note.textContent = ""; this.invalidRegExp[target] = !reg; // false } catch (e) { note.textContent = e.message; this.invalidRegExp[target] = true; } }, save: function (e) { var this$1$1 = this; e.preventDefault(); var items = this.parse(); if (items) { this.info("保存しました。", function () { return this$1$1.item.update(items); }); } }, parse: function () { if (this.areNGRegExpsInvalid()) { this.explainWhyNotSave(); this.showRegExpNotes(); return } this.removeExplainWhyNotSave(); 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; } }); return items }, areNGRegExpsInvalid: function () { var this$1$1 = this; return Object.keys(this.invalidRegExp).some( function (regexp) { return this$1$1.invalidRegExp[regexp]; } ) }, showRegExpNotes: function () { var this$1$1 = this; ["#NGWordNote", "#NGHandleNote"].forEach(function (id) { this$1$1.$(id).style.display = null; }); }, explainWhyNotSave: function () { var explain = this.$("#explainWhyNotSave"); if (!explain) { this.$("#save").insertAdjacentHTML( "afterend", '<span id="explainWhyNotSave">NGワードの正規表現が不正なので保存しませんでした</span>' ); } }, removeExplainWhyNotSave: function () { var explain = this.$("#explainWhyNotSave"); if (explain) { explain.parentNode.removeChild(explain); } }, info: function (text, fun) { return progress(text, this.$("#configInfo"), fun) }, clear: function () { var this$1$1 = this; return this.info("デフォルトに戻しました。", function () { return this$1$1.item.clear().then(function () { return this$1$1.restore(); }); } ) }, close: function () { if (IS_EXTENSION) { closeTab(); } else { this.el.parentNode.removeChild(this.el); window.scrollTo(0, 0); } }, clearVanishThread: function () { var this$1$1 = this; return this.info("非表示に設定されていたスレッドを解除しました。", function () { return this$1$1.item.clearVanishedThreadIDs().then(function () { this$1$1.$("#vanishedThreadIDs").textContent = "0"; }); } ) }, clearVanishMessage: function () { var this$1$1 = this; return this.info("非表示に設定されていた投稿を解除しました。", function () { return this$1$1.item.clearVanishedMessageIDs().then(function () { this$1$1.$("#vanishedMessageIDs").textContent = "0"; }); } ) }, 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 } var value = config[name]; switch (el.type) { case "radio": el.checked = value === el.value; break case "text": case "textarea": el.value = value; break case "checkbox": el.checked = value; break } }); }, }; /** * * @param {string} str * @returns {string} */ var quotemeta = function (str) { return (str + "").replace(/([()[\]{}|*+.^$?\\])/g, "\\$1") }; function locationReload () { window.location.reload(); } function midokureload () { /** @type {HTMLInputElement} */ var midoku = document.querySelector('#form input[name="midokureload"]'); if (midoku) { midoku.click(); } else { locationReload(); } } /** * @param {string} href */ function openInTab (href) { // @ts-ignore if (typeof GM_openInTab === "function") { // @ts-ignore GM_openInTab(href, false); // @ts-ignore // GM4Storage.openInTabがない場合があるからこうなっているらしい } else if (typeof GM === "object" && GM.openInTab) { // @ts-ignore GM.openInTab(href, false); } else { window.open(href); } } /** * @param {import("Config").default} config */ function KeyboardNavigation(config) { //同じキーでもkeypressとkeydownでe.whichの値が違うので注意 var messages = document.getElementsByClassName("message"); var focusedIndex = -1; var done = -1; this.enableToReload = 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) || el.classList.contains("invalid"))) { 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.querySelector(".res")) { selector = ".res"; } else { selector = "font > a:first-child"; } var res = /** @type {HTMLAnchorElement} */ (focused.querySelector(selector)); if (res) { openInTab(res.href); } }; this.handleEvent = function (e) { switch (e.type) { case "keypress": this.move(e); 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 } }; } 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\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\tjustify-content: space-between;\n}\n.thread {\n\tmargin-bottom: 1rem;\n\toverflow: initial;\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\tvertical-align: top;\n}\n\n.messagesWithLine {\n\tdisplay: flex;\n\tflex-flow: row;\n}\n.border {\n\tborder-left: 1px solid #ADB;\n\ttop: 1rem;\n\tposition: relative;\n}\n.messageAndChildrenButLast {\n\tposition: relative;\n\tleft: -1px;\n}\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\toutline: 2px solid yellow;\n}\n\n.truncation, .NGThread .messages, .hidden {\n\tdisplay: none;\n}\n.spacing {\n\tpadding-bottom: 1rem;\n}\n.spacer:first-child {\n\tdisplay: none;\n}\n\n.vanishThread {\n\tdisplay: none;\n}\n.useVanishThreadButton .vanishThread {\n\tdisplay: inline;\n\tdisplay: initial;\n}\n.thread.useVanishThreadButton .vanishThread {\n\tmargin-left: 1em;\n}\n\n.toggleTreeMode {\n\tdisplay: none;\n}\n.useToggleTreeModeButton .toggleTreeMode {\n\tdisplay: inline;\n\tdisplay: initial;\n\tmargin-left: 1em;\n}\n\n.vanishThread::after, .toggleMessage::after {\n\tcontent: \"消\";\n}\n.vanishThread.revert::after, .toggleMessage.revert::after {\n\tcontent: \"戻\";\n}\n\n.showOriginalButtons + .message {\n\tdisplay: none;\n}\n\n.qtv-error {\n\tfont-family: initial;\n\tborder:red solid\n}\n"; var Qtv = function Qtv(config) { this.config = config; }; Qtv.prototype.initializeComponent = function initializeComponent () { this.applyCss(); this.zero(); this.addEventListeners(); this.setAccesskeyToV(); this.setIdsToFormAndLinks(); this.registerKeyboardNavigation(); }; Qtv.prototype.applyCss = function applyCss () { document.head.insertAdjacentHTML( "beforeend", ("<style>" + (css + this.config.css) + "</style>") ); }; Qtv.prototype.zero = function zero () { if (this.config.zero) { var d = this.getD(); this.setZeroToD(d); } }; /** @returns {HTMLInputElement} */ Qtv.prototype.getD = function getD () { return /** @type {HTMLInputElement} */ (document.getElementsByName("d")[0]) }; /** * @param {?HTMLInputElement} d */ Qtv.prototype.setZeroToD = function setZeroToD (d) { if (d && d.value !== "0") { d.value = "0"; } }; Qtv.prototype.addEventListeners = function addEventListeners () { var this$1$1 = this; var body = getBody(); on(body, "click", "#openConfig", function (e) { e.preventDefault(); this$1$1.openConfig(); }); var delegateTweakLink = function (/** @type {Event} */ e) { this$1$1.tweakLink(/** @type {HTMLAnchorElement} */ (e.target)); }; on(body, "mousedown", "a", delegateTweakLink); on(body, "keydown", "a", delegateTweakLink); }; Qtv.prototype.openConfig = function openConfig () { if (IS_EXTENSION) { this.openConfigOnExtension(); } else if (this.thereIsNoConfigPageOpen()) { this.openConfigOnGreaseMonkey(); } }; Qtv.prototype.openConfigOnExtension = function openConfigOnExtension () { sendMessageToRuntime({type: "openConfig"}); }; Qtv.prototype.thereIsNoConfigPageOpen = function thereIsNoConfigPageOpen () { return !document.getElementById("config") }; Qtv.prototype.openConfigOnGreaseMonkey = function openConfigOnGreaseMonkey () { this.openConfigAtTopOfPage(); }; Qtv.prototype.openConfigAtTopOfPage = function openConfigAtTopOfPage () { var controller = new ConfigController(this.config); document.body.insertBefore(controller.el, document.body.firstChild); controller.scrollIntoView(); }; /** * @param {HTMLAnchorElement} a */ Qtv.prototype.tweakLink = function tweakLink (a) { this.changeTargetToBlank(a); this.appendNoreferrerAndNoopenerToPreventFromModifyingURL(a); }; /** * @param {HTMLAnchorElement} a */ Qtv.prototype.changeTargetToBlank = function changeTargetToBlank (a) { if (this.config.openLinkInNewTab && a.target === "link") { a.target = "_blank"; } }; Qtv.prototype.appendNoreferrerAndNoopenerToPreventFromModifyingURL = function appendNoreferrerAndNoopenerToPreventFromModifyingURL (a) { if (a.target) { a.rel += " noreferrer noopener"; } }; Qtv.prototype.setAccesskeyToV = function setAccesskeyToV () { var accessKey = this.config.accesskeyV; if (accessKey.length === 1) { var v = document.getElementsByName("v")[0]; if (v) { v.accessKey = accessKey; v.title = "内容"; } } }; Qtv.prototype.setIdsToFormAndLinks = function setIdsToFormAndLinks () { var form = document.forms[0]; if (form) { this.setIdToForm(form); this.setIdToLinks(form); } }; /** * @param {HTMLFormElement} form */ Qtv.prototype.setIdToForm = function setIdToForm (form) { form.id = "form"; }; /** * @param {HTMLFormElement} form */ Qtv.prototype.setIdToLinks = function setIdToLinks (form) { var fonts = form.getElementsByTagName("font"); // これ以外に指定のしようがない var link = fonts[fonts.length - 3]; if (link) { link.id = "link"; } }; Qtv.prototype.registerKeyboardNavigation = function registerKeyboardNavigation () { if (this.config.keyboardNavigation) { this.keyboardNavigation = new KeyboardNavigation(this.config); document.addEventListener("keypress", this.keyboardNavigation, false); } }; /** * @param {ParentNode} _fragment */ Qtv.prototype.render = function render (_fragment) { //empty }; Qtv.prototype.finish = function finish (_fragment) { if (this.keyboardNavigation) { this.keyboardNavigation.enableToReload(); } }; /** * 本来投稿が来るところの先頭に挿入 * @param {Node} node */ Qtv.prototype.insert = function insert (node) { var hr = document.body.querySelector("body > hr"); if (hr) { hr.parentNode.insertBefore(node, hr.nextSibling); } }; /** * 一番下に追加 * @param {Node} node */ Qtv.prototype.append = function append (node) { document.body.appendChild(node); }; /** * 一番上に追加 * @param {Node} node */ Qtv.prototype.prepend = function prepend (node) { document.body.insertBefore(node, document.body.firstChild); }; /** * @param {Node} node */ Qtv.prototype.remove = function remove (node) { node.parentNode.removeChild(node); }; /** * @param {any} howManyPosts 何件表示されている振りをする?表示がある振りをする? * @param {ParentNode} container Modify. \<P>\<I>\</I>\</P>から\<HR>が含まれている */ function tweakFooter (howManyPosts, container) { var i = container.querySelector("p i"); if (!i) { return container } /* <P><I><FONT size="-1">ここまでは、現在登録されている新着順1番目から1番目までの記事っぽい!</FONT></I></P> <TABLE>次のページ、リロードボタン</TABLE> <HR> `<TABLE>`は、このページに投稿がない、次のページに表示すべき投稿がない、のいずれかの場合は含まれない */ var p = /** @type {HTMLElement} */ (i.parentNode); // === <P> var table = nextElement("TABLE")(p); var end; if (table && howManyPosts) { // 消すのはpだけ end = p; } else { // tableはないか、あるが0件の振りをするためtableは飛ばす var hr = nextElement("HR")(p); end = hr; } new defaultExport$4().deleteContents(p, end); return container } /** * @param {Element} element * @returns () => void */ function savePosition (element) { var top = element.getBoundingClientRect().top; return function restorePosition() { window.scrollTo( window.pageXOffset, window.pageYOffset + element.getBoundingClientRect().top - top ); } } var defaultExport$3 = function defaultExport(presenter) { this.presenter = presenter; }; /** * @param {MouseEvent} e */ /* * <span class="showOriginalButtons"><a>NG</a><a>非表示解除</a>e.targetは左のaのどちらか</span> * <div class="original message"></div> */ defaultExport$3.prototype.handleEvent = function handleEvent (e) { e.preventDefault(); var button = /** @type {Element} */ (e.target); var buttons = /** @type {Element} */ (button.parentNode); if (button.matches(".showNG")) { this.showNG(buttons); } else if (button.matches(".showThread")) { this.showThread(buttons); } }; /** * @param {Element} buttons */ defaultExport$3.prototype.showNG = function showNG (buttons) { this.removeButtons(buttons); }; /** * @param {Element} buttons */ defaultExport$3.prototype.showThread = function showThread (buttons) { var thisMessage = /** @type {HTMLElement} */ (buttons.nextElementSibling); var threadId = thisMessage.dataset.threadId; var restore = savePosition(buttons); this.presenter.removeVanishedThread(threadId); for (var i = 0, list = document.querySelectorAll( (".message[data-thread-id=\"" + threadId + "\"]") ); i < list.length; i += 1) { var message = list[i]; if (message === thisMessage) { restore(); } var buttons$1 = message.previousElementSibling; if (buttons$1.matches(".showOriginalButtons")) { this.removeButtons(buttons$1); } } }; /** * @param {Element} buttons */ defaultExport$3.prototype.removeButtons = function removeButtons (buttons) { buttons.parentNode.removeChild(buttons); }; /** * @param {string} nodeName * @param {Node} node - スタート地点 * @returns {(node: Node) => ?Node} */ var nextSibling = next("nextSibling"); /** * @param {Element} el * @param {string} event * @param {EventListenerOrEventListenerObject} listener */ function addEventListener (el, event, listener) { el.addEventListener(event, listener, false); } /** * @template T */ var Builder = function Builder(node) { /** @type {(string | Node | Builder)[]} */ this.children = []; this.node = node; }; /** * @param {string | Node | Builder} something */ Builder.prototype.add = function add (something) { this.children.push(something); return this }; /** * @param {string} html */ Builder.prototype.addHtml = function addHtml (html) { this.children.push(document.createRange().createContextualFragment(html)); return this }; Builder.prototype.appendChildren = function appendChildren () { for (var i = 0, list = this.children; i < list.length; i += 1) { var something = list[i]; if (something instanceof Builder) { this.node.appendChild(something.build()); } else if (typeof something === "string") { this.node.appendChild(document.createTextNode(something)); } else if (something instanceof Node) { this.node.appendChild(something); } } this.node.normalize(); }; Builder.prototype.build = function build () { this.appendChildren(); return this.node }; /** * @template T */ var ElementBuilder = /*@__PURE__*/(function (Builder) { function ElementBuilder(tagName) { Builder.call(this, document.createElement(tagName)); this.className = ""; this.style = ""; /** @type {DOMStringMap} */ this.data = Object.create(null); /** @type {{[key: string]: string}} */ this.attributes = Object.create(null); } if ( Builder ) ElementBuilder.__proto__ = Builder; ElementBuilder.prototype = Object.create( Builder && Builder.prototype ); ElementBuilder.prototype.constructor = ElementBuilder; /** * @param {string} className */ ElementBuilder.prototype.withClass = function withClass (className) { this.className = className; return this }; /** * @param {string} attribute * @param {string} value */ ElementBuilder.prototype.with = function with$1 (attribute, value) { this.attributes[attribute] = value; return this }; /** * @param {string} key * @param {string} value */ ElementBuilder.prototype.withData = function withData (key, value) { this.data[key] = value; return this }; /** * @param {string} style */ ElementBuilder.prototype.withStyle = function withStyle (style) { this.style = style; return this }; /** @return {HTMLElementTagNameMap[T]} */ ElementBuilder.prototype.build = function build () { if (this.className) { this.node.className = this.className; } if (this.style) { this.node.style.cssText = this.style; } Object.assign(this.node.dataset, this.data); Object.assign(this.node, this.attributes); return Builder.prototype.build.call(this) }; return ElementBuilder; }(Builder)); var GroupBuilder = /*@__PURE__*/(function (Builder) { function GroupBuilder() { Builder.call(this, document.createDocumentFragment()); } if ( Builder ) GroupBuilder.__proto__ = Builder; GroupBuilder.prototype = Object.create( Builder && Builder.prototype ); GroupBuilder.prototype.constructor = GroupBuilder; return GroupBuilder; }(Builder)); function aPre() { return new ElementBuilder("pre") } function aDiv() { return new ElementBuilder("div") } function aSpan() { return new ElementBuilder("span") } function aStrong() { return new ElementBuilder("strong") } function anA() { return new ElementBuilder("a").with("href", "javascript:;") } /** * @template T * @param {T extends keyof HTMLElementTagNameMap ? T : never} tag */ function aTag(tag) { return new ElementBuilder(tag) } function aGroup() { return new GroupBuilder() } var mayHaveSmallerImage = /^https?:\/\/misao\.(?:mixh|on\.arena\.ne)\.jp\/c\/up\/misao\d+\.\w+$/; var misao = /^https?:\/\/misao\.(?:mixh|on\.arena\.ne)\.jp\/c\//; var imageReg = /\.(?:jpe?g|png|gif|bmp|webp)$/; var videoReg = /^[^?#]+\.(?:webm|avi|mov|mp[4g]|wmv)(?:[?#]|$)/i; var audioReg = /^[^?#]+\.(?:mp3|m4a|wma|au|mid|wav)(?:[?#]|$)/i; /** * @typedef {(href: string) => string} small サムネイル用の小さい画像のURL * @typedef {(href: string) => string} original - オリジナルサイズの画像のURL * @typedef {(href: string) => HTMLMediaElement} embed 埋め込まれるElement * @typedef {(href: string, ext: string) => string} replaceExtra 内部利用 * @typedef {{ * name: string; * prefix?: RegExp; * suffix?: RegExp; * small?: small; * strange?: boolean; * embed?: embed; * original?: original; * replaceExtra?: replaceExtra; * }} Site * @typedef {Site[]} Sites */ /** @type {Sites} */ var sites = [ { name: "misao", prefix: misao, suffix: imageReg, small: function (href) { return mayHaveSmallerImage.test(href) ? href.replace(/up\//, "up/pixy_") : href; }, strange: true, }, { name: "misaoAudio", prefix: misao, suffix: audioReg, strange: true, embed: function (href) { return anAudio(href).build(); }, }, { name: "misaoVideo", prefix: misao, suffix: videoReg, strange: true, embed: function (href) { return aVideo(href).build(); }, }, { name: "imgur", prefix: /^https?:\/\/(?:i\.)?imgur\.com\/[^/]+$/, small: function small(href) { var original = href.replace(/^https?:\/\/(?:i\.)?/, "https:/i."); return original.replace(/\.\w+$/, "t$&") }, }, { name: "twimg", prefix: /^https?:\/\/pbs\.twimg\.com\/media\/[\w_-]+\.\w+/, suffix: /(?::(?:orig|large|medium|small|thumb))?$/, original: function original(href) { return this.replaceExtra(href, ":orig") }, small: function small(href) { return this.replaceExtra(href, ":thumb") }, replaceExtra: function replaceExtra(href, ext) { var ref = this.prefix.exec(href) || []; var hrefWithoutTag = ref[0]; return hrefWithoutTag ? hrefWithoutTag + ext : "" }, }, { name: "anyImage", suffix: /^[^?#]+\.(?:jpe?g|png|gif|bmp|webp)(?:[?#]|$)/i, }, { name: "anyAudio", suffix: audioReg, embed: function (href) { return anAudio(href).build(); }, }, { name: "anyVideo", suffix: videoReg, embed: function (href) { return aVideo(href).build(); }, } ]; /** * @param {string} src */ function anAudio(src) { return new ElementBuilder("audio") .with("controls", "on") .with("preload", "on") .with("src", src) } /** * @param {string} src */ function aVideo(src) { return new ElementBuilder("video") .with("controls", "on") .with("preload", "on") .with("loop", "on") .with("src", src) } function toggle(e) { e.preventDefault(); doToggle(e.target); } function doToggle(el) { var ref = el.dataset; var name = ref.name; var href = ref.href; var site = sites.find(function (site) { return site.name === name; }); var a = nextElement("A")(el); if (el.classList.contains("embedded")) { a.parentNode.removeChild(a.nextElementSibling); } else { var media = site.embed(href); if (media instanceof HTMLVideoElement) { var metadata = el.nextElementSibling; if (!metadata.classList.contains("metadata")) { media.addEventListener("loadedmetadata", function () { var videoHeight = media.videoHeight; var videoWidth = media.videoWidth; metadata.insertAdjacentHTML( "beforebegin", ("<span class=\"metadata\">[" + videoWidth + "x" + videoHeight + "]</span>") ); }); } } var text = el.closest(".text_tree-mode-ascii"); var branch = text ? text.querySelector(".a-tree:not(.spacer)") : null; var div = aDiv() .withStyle("white-space: initial") .add(branch ? branch.cloneNode(true) : null) .build(); div.appendChild(media); a.parentNode.insertBefore(div, a.nextSibling); } el.classList.toggle("embedded"); } function Popup(config, image, body) { if ( body === void 0 ) 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(); 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 Popup */ 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) { if ( head === void 0 ) head = document.head; this.preloads = Object.create(null); this.head = 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 = Object.assign(document.createElement("link"), { rel: "preload", as: "image", href: url, }); this.head.appendChild(link); this.preloads[url] = true; }; Preload.prototype.isFetched = function (url) { return this.preloads[url] }; /** * @param {import("../Config").default} config */ function Embedder(config, preload) { if ( preload === void 0 ) preload = new Preload(); this.config = config; this.preload = preload; /** * @param {string} href */ this.thumbnailLink = function (href) { var site = pickAppropriateSite(href); if (!site) { return } var original = site.original ? site.original(href) : href; var thumbnail = this.thumbnailHtml(original, href, site); if (config.shouki && !site.embed) { thumbnail += shouki(original); } return thumbnail }; var pickAppropriateSite = function (href) { return sites.find( function (ref) { var prefix = ref.prefix; var suffix = ref.suffix; var strange = ref.strange; return (strange ? true : config.popupAny) && test(href, prefix) && test(href, suffix); } ); }; var test = function (href, test) { return !test || test.test(href); }; this.thumbnailHtml = function (original, href, site) { var small = this.small(href, site); if (small) { return a(original, thumbnailHtml(small)) } else if (site.embed) { return ("[<a href = \"javascript:;\" class=\"embed\" data-name=\"" + (site.name) + "\" data-href=\"" + href + "\">埋</a>]") } else { return "[" + a(original, "■") + "]" } }; this.small = function (href, site) { if (!site.small) { return } var small = site.small(href); if (!small) { return } if (href === small) { return href } if (!config.thumbnailPopup) { return small } this.preload.fetch(href); if (this.preload.isFetched(href)) { return small } else { return href } }; var a = function (href, content) { return ("<a href=\"" + href + "\" target=\"link\" class=\"thumbnail\">" + content + "</a>"); }; var thumbnailHtml = function (src) { return ("<img referrerpolicy=\"no-referrer\" class=\"thumbnail-img\" src=\"" + src + "\">"); }; var shouki = function (href) { return ("[<a href=\"https://images.google.com/searchbyimage?image_url=" + href + "\" target=\"link\">詳</a>]"); }; /** ポップアップを消した時、カーソルがサムネイルの上にあるか */ 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(); }; } /** @param {HTMLElement} container */ Embedder.prototype.register = function (container) { /** @type {NodeListOf<HTMLAnchorElement>} */ var as = container.querySelectorAll("a[target]"); var has = false; for (var i = as.length - 1; i >= 0; i--) { var a = as[i]; var thumbnail = this.thumbnailLink(a.href); if (thumbnail) { a.insertAdjacentHTML("beforebegin", thumbnail); has = true; } } if (has) { if (this.config.thumbnailPopup) { var thumbs = container.getElementsByClassName("thumbnail"); for (var i$1 = thumbs.length - 1; i$1 >= 0; i$1--) { addEventListener(thumbs[i$1], "mouseover", this); } } var embeds = container.getElementsByClassName("embed"); for (var i$2 = embeds.length - 1; i$2 >= 0; i$2--) { addEventListener(embeds[i$2], "click", toggle); } } }; /** * @param {HTMLAnchorElement} a */ function collectElements(a) { var el = collectEssentialElements(a); return { el: el, name: el.name.innerHTML, title: el.title.innerHTML, text: el.pre.innerHTML, threadId: el.threadButton ? /&s=([^&]+)/.exec(el.threadButton.search)[1] : el.anchor.name, } } var defaultExport$2 = function defaultExport(config, range) { if ( range === void 0 ) range = new defaultExport$4(); this.config = config; this.range = range; this.ng = new NG(config); this.embedder = new Embedder(config); this.original = document.createElement("div"); this.original.className = "message original"; this.vanishButton = this.range.createContextualFragment( '<a href="javascript:;" class="vanishThread" title="スレッドを非表示にします"></a> ' ); this.nextComment_ = nextSibling("#comment"); }; /** * @param {HTMLAnchorElement} a */ defaultExport$2.prototype.render = function render (a) { var post = collectElements(a); var buttons = []; if (this.vanishThread(post, buttons)) { return } if (this.vanishByNG(post, buttons)) { return } this.buildMessage(post, buttons); this.registerThumbnail(post); }; defaultExport$2.prototype.vanishThread = function vanishThread (post, buttons) { if (this.config.isVanishedThread(post.threadId)) { if (this.config.utterlyVanishNGThread) { this.hideMessage(post); return true } else { buttons.push( '<a href="javascript:;" class="showThread" title="このスレッドは非表示に設定されています">非表示解除</a>' ); } } }; /** * 投稿を隠す。消しはしない。消してしまうと存在したかを判定できない。 */ defaultExport$2.prototype.hideMessage = function hideMessage (post) { var el = post.el; var end = this.nextComment(el.blockquote); var wrapper = document.createElement("div"); this.range.surroundContents(wrapper, el.anchor, end); wrapper.classList.add("hidden"); }; defaultExport$2.prototype.nextComment = function nextComment (element) { return this.nextComment_(element) }; defaultExport$2.prototype.vanishByNG = function vanishByNG (post, buttons) { var ng = this.ng; if (ng.isEnabled) { Post.checkNG(ng, post); if (post.isNG) { if (this.config.utterlyVanishNGStack) { this.hideMessage(post); return true } else if (this.config.NGCheckMode) { this.markNG(post); } else { buttons.push( '<a href="javascript:;" class="showNG" title="この投稿にはNGワードが含まれるため、非表示になっています">NG</a>' ); } } } }; defaultExport$2.prototype.markNG = function markNG (post) { var el = post.el; if (this.ng.testWord(post.text)) { el.pre.innerHTML = this.ng.markWord(post.text); } if (this.ng.testHandle(post.name)) { el.name.innerHTML = this.ng.markHandle(post.name); } if (this.ng.testHandle(post.title)) { el.title.innerHTML = this.ng.markHandle(post.title); } }; defaultExport$2.prototype.buildMessage = function buildMessage (post, buttons) { if (this.needToWrap() || buttons.length) { var wrapper = this.wrapMessage(post); if (buttons.length) { // chromeのinsertAdjacentHTMLは // DocumentFragmentの中のNode(ここでいうwrapper)だとエラーになる時代があった // もう大丈夫そうなのでいつか直す var showOriginalButtons = document.createElement("span"); showOriginalButtons.className = "showOriginalButtons"; showOriginalButtons.innerHTML = buttons.join(" "); wrapper.parentNode.insertBefore(showOriginalButtons, wrapper); } } }; defaultExport$2.prototype.needToWrap = function needToWrap () { return ( this.config.useVanishThread || this.config.keyboardNavigation || // @ts-ignore (window.Intl && Intl.v8BreakIterator) // or blink ) }; defaultExport$2.prototype.wrapMessage = function wrapMessage (post) { var el = post.el; var wrapper = /** @type {HTMLElement} */ (this.original.cloneNode(false)); this.range.surroundContents(wrapper, el.anchor, el.blockquote); if (this.config.useVanishThread) { wrapper.classList.add("useVanishThreadButton"); el.resButton.parentNode.insertBefore( this.vanishButton.cloneNode(true), el.threadButton ); wrapper.dataset.threadId = post.threadId; } return wrapper }; defaultExport$2.prototype.registerThumbnail = function registerThumbnail (post) { if (this.config.thumbnail) { this.embedder.register(post.el.pre); } }; var StackView = /*@__PURE__*/(function (Qtv) { function StackView( config, range, renderer ) { if ( range === void 0 ) range = new defaultExport$4(); if ( renderer === void 0 ) renderer = new defaultExport$2(config, range); Qtv.call(this, config); this.range = range; this.renderer = renderer; this.ng = new NG(config); this.el = document.createElement("main"); this.el.id = "qtv-main"; this.miniinfo = null; /** @type {import("./StackPresenter").default} */ this.presenter = null; } if ( Qtv ) StackView.__proto__ = Qtv; StackView.prototype = Object.create( Qtv && Qtv.prototype ); StackView.prototype.constructor = StackView; StackView.prototype.setPresenter = function setPresenter (presenter) { this.presenter = presenter; }; StackView.prototype.initializeComponent = function initializeComponent () { this.setupMiniInfo(); this.accesskey(); Qtv.prototype.initializeComponent.call(this); this.insert(this.el); }; StackView.prototype.setupMiniInfo = function setupMiniInfo () { var setup = document.body.querySelector('input[name="setup"]'); if (!setup) { return } var miniinfo = (this.miniinfo = this.createMiniInfo(setup)); this.showOptionButton(miniinfo); this.showClearVanishedThreadsButton(miniinfo); this.showNGMessage(miniinfo); this.showMiniInfo(setup, miniinfo); }; StackView.prototype.showMiniInfo = function showMiniInfo (setup, miniinfo) { setup.parentNode.insertBefore(miniinfo, setup.nextSibling); }; StackView.prototype.showNGMessage = function showNGMessage (miniinfo) { if (this.ng.message) { miniinfo.insertAdjacentHTML("beforeend", " " + this.ng.message); } }; StackView.prototype.showClearVanishedThreadsButton = function showClearVanishedThreadsButton (miniinfo) { var numVanishedThreads = this.config.vanishedThreadIDs.length; if (numVanishedThreads) { miniinfo.insertAdjacentHTML( "beforeend", ' 非表示解除(<a class="clearVanishedThreadIDs" href="javascript:;"><span class="count">' + numVanishedThreads + "</span>スレッド</a>)" ); } }; StackView.prototype.showOptionButton = function showOptionButton (miniinfo) { miniinfo.insertAdjacentHTML( "beforeend", ' <a href="javascript:;" id="openConfig">★くわツリービューの設定★</a>' ); }; StackView.prototype.createMiniInfo = function createMiniInfo (setup) { var miniinfo = document.createElement("span"); miniinfo.id = "qtv-miniinfo"; setup.parentNode.insertBefore(miniinfo, setup.nextSibling); return miniinfo }; StackView.prototype.accesskey = function accesskey () { var midoku = /** @type {HTMLElement} */ (document.body.querySelector( 'input[name="midokureload"]' )); if (midoku) { midoku.accessKey = this.config.accesskeyReload; midoku.title = "ヽ(´ー`)ノロード"; } }; /** @override */ StackView.prototype.addEventListeners = function addEventListeners () { var this$1$1 = this; Qtv.prototype.addEventListeners.call(this); var el = this.el; var showHiddenMessage = new defaultExport$3(this.presenter); on(el, "click", ".showNG", showHiddenMessage); on(el, "click", ".showThread", showHiddenMessage); on(el, "click", ".vanishThread", function (/** @type {MouseEvent} */ e) { this$1$1.handleVanishThread(e); }); on(document.body, "click", ".clearVanishedThreadIDs", function (e) { e.preventDefault(); this$1$1.presenter.clearVanishedThreadIDs(); }); }; /** * @param {MouseEvent} e */ StackView.prototype.handleVanishThread = function handleVanishThread (e) { e.preventDefault(); var thisVanishButton = /** @type {Element} */ (e.target); var revert = thisVanishButton.classList.contains("revert"); var message = /** @type {HTMLElement} */ (thisVanishButton.closest( ".message" )); var threadId = message.dataset.threadId; var restore = savePosition(message); if (revert) { this.presenter.removeVanishedThread(threadId); } else { this.presenter.addVanishedThread(threadId); } for (var i = 0, list = document.querySelectorAll( (".message[data-thread-id=\"" + threadId + "\"]") ); i < list.length; i += 1) { // 投稿が有効か無効かのトグル var message$1 = list[i]; message$1.classList.toggle("invalid"); // 削除なら本文を消す。復帰なら表示する。 // スレッドが表示・非表示が切り替わったことを分かりやすく示す。 message$1.querySelector("blockquote").classList.toggle("hidden"); // ボタンを削除ボタンを復帰ボタンに、またはその逆 var vanishButton = message$1.querySelector(".vanishThread"); vanishButton.classList.toggle("revert"); } restore(); }; StackView.prototype.clearVanishedThreadIDs = function clearVanishedThreadIDs () { var count = document.body.querySelector(".clearVanishedThreadIDs .count"); if (count) { count.innerHTML = "0"; } }; /** * @param {ParentNode} fragment `fragment`の先頭は通常は空白。ログの一番先頭のみ\<A> * @param {ParentNode} container */ StackView.prototype.render = function render (fragment, container) { if ( container === void 0 ) container = this.el; var ref = this; var range = ref.range; var comment; while ((comment = this.firstComment(fragment))) { var first = /** @type {Text|HTMLAnchorElement} */ (fragment.firstChild); var one = range.extractContents(first, comment); // 以下のように一つずつやるとO(n) // 一気に全部やるとO(n^2) // chrome57の時点で一気にやってもO(n)になってる var a = /** @type {HTMLAnchorElement} */ (one.querySelector("a[name]")); try { if (a) { this.renderer.render(a); } container.appendChild(one); } catch (e) { container.appendChild(one); this.skipThisPost(a, e); } } }; /** * @param {ParentNode} fragment */ StackView.prototype.firstComment = function firstComment (fragment) { var first = fragment.firstChild; while (first) { if (first.nodeType === Node.COMMENT_NODE && first.nodeValue === " ") { return first } first = first.nextSibling; } return null }; /** * @param {HTMLAnchorElement} a * @param {Error} e */ StackView.prototype.skipThisPost = function skipThisPost (a, e) { a.insertAdjacentHTML( "beforebegin", ("<div class=\"qtv-error\">問題が発生したため、この投稿の処理を中断しました。<pre class=\"qtv-error\">" + (e.message) + "</pre></div>") ); }; /** * @param {ParentNode} fragment */ StackView.prototype.finishFooter = function finishFooter (fragment) { fragment = this.tweakFooter(fragment); return this.append(fragment) }; /** * @param {ParentNode} fragment */ StackView.prototype.tweakFooter = function tweakFooter$1 (fragment) { if (this.needsToTweakFooter()) { return tweakFooter(this.countMessages(), fragment) } return fragment }; StackView.prototype.needsToTweakFooter = function needsToTweakFooter () { var config = this.config; return ( (this.ng.isEnabled && config.utterlyVanishNGStack) || (config.useVanishThread && config.utterlyVanishNGThread) ) }; StackView.prototype.countMessages = function countMessages () { return this.el.querySelectorAll(".message").length }; StackView.prototype.showIsSearchingOldLogsExceptFor = function showIsSearchingOldLogsExceptFor (ff) { var info = document.createElement("div"); info.id = "qtv-info"; info.innerHTML = "<strong>" + ff + "以外の過去ログを検索中...</strong>"; this.prepend(info); }; StackView.prototype.doneSearchingOldLogs = function doneSearchingOldLogs () { this.remove(document.querySelector("#qtv-info")); }; /** * @param {string} ff 日付.dat * @param {import("Query").FetchResult[]} befores * @param {import("Query").FetchResult[]} afters */ StackView.prototype.setBeforesAndAfters = function setBeforesAndAfters (ff, befores, afters) { var this$1$1 = this; if (!document.body.querySelector("h1")) { document.body.insertAdjacentHTML("afterbegin", ("<h1>" + ff + "</h1>")); } var h1 = document.querySelector("h1"); befores.reverse().forEach(function (before) { var container = this$1$1.createPseudoPage(before); h1.parentNode.insertBefore(container, h1); }, this); afters.reverse().forEach(function (after) { var container = this$1$1.createPseudoPage(after); this$1$1.el.appendChild(container); }, this); }; /** * * @param {Object} data * @param {DocumentFragment} data.fragment * @param {import("Query").ff} data.ff */ StackView.prototype.createPseudoPage = function createPseudoPage (ref) { var fragment = ref.fragment; var ff = ref.ff; for (var i = 0, list = fragment.querySelectorAll("script"); i < list.length; i += 1) { var script = list[i]; fragment.removeChild(script); } var container; if (fragment.querySelector("h1")) { container = document.createDocumentFragment(); } else { container = this.range.createContextualFragment(("<h1>" + ff + "</h1>")); } this.render(fragment, container); // 何か余り物があるかもしれないのでそれも追加 container.appendChild(fragment); var numPosts = container.querySelectorAll(".message").length; container.appendChild( this.range.createContextualFragment( numPosts ? ("<h3>" + numPosts + "件見つかりました。</h3>") : "<h3>指定されたスレッドは見つかりませんでした。</h3>" ) ); return container }; return StackView; }(Qtv)); /** * Configが読み込まれるまで、送られてきたHTMLはここに溜め込み、表示されないようにする。 */ var Buffer = function Buffer(range) { if ( range === void 0 ) range = document.createRange(); this.range = range; this.fragment = document.createDocumentFragment(); /** これを基準にする。これの次が新しいデータ。`hr`の次に要素を挿入しても新しいデータだと勘違いしない */ this.marker = document.createComment("qtv-main-started"); this.listener = null; }; /** * @param {{onProgress: (fragment: DocumentFragment) => void, onLoaded: (fragment: DocumentFragment) => void}} listener */ Buffer.prototype.setListener = function setListener (listener) { this.listener = listener; }; /** * @param {HTMLHRElement} hr `BODY`直下の一番目の`HR`。投稿はこの下から始まる。 */ Buffer.prototype.onHr = function onHr (hr) { hr.parentNode.insertBefore(this.marker, hr.nextSibling); this.range.setStartAfter(this.marker); }; /** * @param {Node} lastChild 読み込まれた一番最後のノード */ Buffer.prototype.onProgress = function onProgress (lastChild) { if (lastChild !== this.marker) { this.range.setEndAfter(lastChild); this.fragment.appendChild(this.range.extractContents()); } // lastChild === markerの場合、つまりbufferに変化がなくてもlistener.renderを呼んでいる。 // 無駄に見える。何か理由があってこうした気がするけど覚えていない。 this.listener.onProgress(this.fragment); }; Buffer.prototype.onLoaded = function onLoaded () { this.listener.onLoaded(this.fragment); }; var delayPromise = function (ms) { return new Promise(function (resolve) { return setTimeout(resolve, ms); }); }; var DelayNotice = function DelayNotice(gotConfig, timeout_ms) { var this$1$1 = this; if ( timeout_ms === void 0 ) timeout_ms = 700; this.gotConfig = gotConfig; this.timeout_ms = timeout_ms; this.configLoaded = false; this.notice = null; this.gotConfig.then(function () { this$1$1.configLoaded = true; }); }; DelayNotice.prototype.onHr = function onHr () { return delayPromise(this.timeout_ms).then(this.popup.bind(this)) }; DelayNotice.prototype.popup = function popup () { var this$1$1 = this; if (this.configLoaded) { return } this.notice = document.createElement("aside"); this.notice.id = "qtv-status"; this.notice.style.cssText = "position:fixed;top:0px;left:0px;background-color:black;color:white;z-index:1"; this.notice.textContent = "設定読込待ち"; var body = getBody(); body.insertBefore(this.notice, body.firstChild); var removeNotice = function () { return body.removeChild(this$1$1.notice); }; this.gotConfig.then(removeNotice, removeNotice); }; var LoadedObserver = function LoadedObserver() { /** * @type {import("./LoadingObserver").Listener[]} */ this.listeners = []; }; /** * @param {import("./LoadingObserver").Listener} listener */ LoadedObserver.prototype.addListener = function addListener (listener) { this.listeners.push(listener); }; LoadedObserver.prototype.observe = function observe () { var this$1$1 = this; ready$1().then(function () { var hr = document.body.querySelector("body > hr"); if (hr) { this$1$1.notify("onHr", hr); this$1$1.notify("onProgress", document.body.lastChild); } this$1$1.notify("onLoaded"); }); }; LoadedObserver.prototype.notify = function notify (event, arg) { this.listeners.forEach(function (listener) { if (listener[event]) { listener[event](arg); } }); }; function doNothing() {} function getInfo () { return IS_GM ? // @ts-ignore getGMInfo(GM_info) : IS_GM4 ? // @ts-ignore getGMInfo(GM.info) : { platform: "chrome", // @ts-ignore version: chrome.runtime.getManifest().version, }; } var getGMInfo = function (info) { return ({ platform: info.scriptHandler + info.version, version: info.script.version, }); }; var e; function handleError(error) { if (e) { return } e = error; return ready$1().then(getBody).then(doHandle) } function doHandle(body) { var lineNumber = e.lineNumber || 0; var pre = document.createElement("pre"); pre.className = "qtv-error"; pre.innerHTML = 'くわツリービューの処理を中断しました。表示出来なかった投稿があります。<a href="javascript:;">スタックトレースを表示する</a>'; var dStackTrace = document.createElement("pre"); dStackTrace.style.display = "none"; var stackTrace = "qtvstacktrace/"; var info = getInfo(); stackTrace += info.platform + "+" + info.version + "\n"; var stack = e.stackTrace || e.stack || ""; stackTrace += e.name + ": " + e.stackTrace + ":" + lineNumber + "\n" + stack; dStackTrace.textContent = stackTrace; pre.appendChild(dStackTrace); pre.addEventListener("click", showStackTrace); body.insertBefore(pre, body.firstChild); throw e } function showStackTrace(e) { e.target.parentNode.querySelector("pre").style.display = null; } var find = Array.prototype.find; var isHR = function (node) { return node.nodeName === "HR"; }; function findHr (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 waitForDomContentLoaded () { return ready$1({capture: true}); } /** * @typedef {{ * onHr?: (hr: HTMLHRElement) => void; * onProgress?: (lastChild: ChildNode) => void; * onLoaded?: () => void; * }} Listener */ var LoadingObserver = function LoadingObserver(loaded, doc) { var this$1$1 = this; if ( loaded === void 0 ) loaded = waitForDomContentLoaded(); if ( doc === void 0 ) doc = document; /** * @type {Listener[]} */ this.listeners = []; this.doc = doc; this.hr = null; this.observer = this.makeMutationObserver(this.processRecords.bind(this)); this.isFirstCall = true; loaded .then(function () { var records = this$1$1.observer.takeRecords(); this$1$1.observer.disconnect(); if (records.length) { this$1$1.inspect(records); } this$1$1.notify("onLoaded"); }) .catch(function () {}); }; LoadingObserver.prototype.makeMutationObserver = function makeMutationObserver (callback) { return new MutationObserver(callback) }; /** * @param {MutationRecord[]} mutations * @param {MutationObserver} observer */ LoadingObserver.prototype.processRecords = function processRecords (mutations, observer) { observer.disconnect(); this.inspect(mutations); this.observe(); }; /** * @param {MutationRecord[]} mutations */ LoadingObserver.prototype.inspect = function inspect (mutations) { if (!this.hr) { this.hr = findHr(mutations); if (this.hr) { this.notify("onHr", this.hr); } } if (this.hr) { this.notify("onProgress", this.doc.body.lastChild); } }; /** * @param {string} event * @param {ChildNode} [arg] */ LoadingObserver.prototype.notify = function notify (event, arg) { for (var i = 0; i < this.listeners.length; i++) { var listener = this.listeners[i]; if (!listener[event]) { continue } try { var ret = listener[event](arg); // エラーの処理はここでやるべきではないと思う if (ret && ret.catch) { ret.catch(this.cleanupAfterError); } } catch (e) { this.cleanupAfterError(e); } } }; LoadingObserver.prototype.cleanupAfterError = function cleanupAfterError (e) { this.observer.disconnect(); this.observer.observe = doNothing; handleError(e); }; LoadingObserver.prototype.observe = function observe () { if (this.doc.body) { if (this.isFirstCall) { this.first(); } this.observer.observe(this.doc.body, {childList: true}); } else { this.observer.observe(this.doc.documentElement, { childList: true, subtree: true, }); } this.isFirstCall = false; }; LoadingObserver.prototype.first = function first () { this.hr = this.doc.body.querySelector("body > hr"); if (this.hr) { this.notify("onHr", this.hr); this.notify("onProgress", this.doc.body.lastChild); } }; /** * @param {Listener} listener */ LoadingObserver.prototype.addListener = function addListener (listener) { this.listeners.push(listener); }; 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); }; var Thread = function Thread(config) { this.config = config; /** @type {Array<Post>} */ this.posts = []; /** @type {Array<Post>} */ this.roots = null; this.isNG = false; /** @type {{[id: string]: Post}} */ this.allPosts = Object.create(null); }; /** * @param {Post} post */ Thread.prototype.addPost = function addPost (post) { this.posts.push(post); this.allPosts[post.id] = post; }; Thread.prototype.computeRoots = function computeRoots () { if (this.roots) { return this.roots } this.setNGtoPosts(); if (this.shouldExcludeNGPosts()) { this.excludeNGPosts(); } this.makeFamilyTree(); this.makeMissingParent(); this.makeMissingGrandParent(); if (this.shouldSetRejectLevel()) { this.setRejectLevel(); } if (this.shouldRejectPosts()) { this.dropRejectedPosts(); } return this.roots }; /** * 今実際にある`Post`を繋いで親子関係を作る */ Thread.prototype.makeFamilyTree = function makeFamilyTree () { this.posts.filter(Post.wantsParent).forEach(this.adopt, this); }; /** * @param {Post} post */ Thread.prototype.adopt = function adopt (post) { var parent = this.allPosts[post.getKeyForOwnParent()]; if (!parent) { return } parent.adoptAsEldestChild(post); }; /** * 仮想の親(`MergedPost`)を作り親子関係を作る */ Thread.prototype.makeMissingParent = function makeMissingParent () { var orphans = this.posts.filter(Post.isOrphan); this.connect(orphans); this.roots = this.getRootCandidates(); }; Thread.prototype.connect = function connect (orphans) { orphans.forEach(this.makeParent, this); orphans.forEach(this.adopt, this); }; Thread.prototype.getRootCandidates = function getRootCandidates () { return Object.values(this.allPosts) .filter(Post.isRootCandidate) .sort(this.byID) }; /** * 想像上の親(`GhostPost`)を作り親子関係を作る */ Thread.prototype.makeMissingGrandParent = function makeMissingGrandParent () { var orphans = this.roots.filter(Post.mayHaveParent); this.connect(orphans); this.roots = this.getRootCandidates(); }; /** * @param {Post} orphan */ Thread.prototype.makeParent = function makeParent (orphan) { var key = orphan.getKeyForOwnParent(); this.allPosts[key] = this.allPosts[key] || orphan.makeParent(); }; /** * @param {Post} l * @param {Post} r */ Thread.prototype.byID = function byID (l, r) { var lid = l.id ? l.id : l.child.id; var rid = r.id ? r.id : r.child.id; return +lid - +rid }; Thread.prototype.shouldSetRejectLevel = function shouldSetRejectLevel () { return +this.getSmallestMessageID() <= this.getThreshold() }; Thread.prototype.getThreshold = function getThreshold () { return +this.config.vanishedMessageIDs[0] }; Thread.prototype.getSmallestMessageID = function getSmallestMessageID (keys) { if ( keys === void 0 ) keys = Object.keys; return keys(this.allPosts).sort(this.byNumericalOrder)[0] }; /** * @param {string} l * @param {string} r */ Thread.prototype.byNumericalOrder = function byNumericalOrder (l, r) { return +l - +r }; Thread.prototype.setNGtoPosts = function setNGtoPosts () { var this$1$1 = this; var ng = new NG(this.config); this.posts.forEach(function (post) { post.checkNG(ng); this$1$1.isNG = this$1$1.isNG || post.isNG; }); }; Thread.prototype.shouldExcludeNGPosts = function shouldExcludeNGPosts () { return this.config.utterlyVanishNGStack }; Thread.prototype.excludeNGPosts = function excludeNGPosts () { var this$1$1 = this; this.posts .filter(function (post) { return post.isNG; }) .forEach(function (post) { delete this$1$1.allPosts[post.id]; }); this.posts = this.posts.filter(function (post) { return !post.isNG; }); }; Thread.prototype.shouldRejectPosts = function shouldRejectPosts () { return this.config.utterlyVanishMessage }; Thread.prototype.setRejectLevel = function setRejectLevel () { var vanishedMessageIDs = this.config.vanishedMessageIDs; // なぜ逆順なのかは覚えていない for (var i = this.roots.length - 1; i >= 0; i--) { this.roots[i].computeRejectLevel(vanishedMessageIDs); } }; Thread.prototype.dropRejectedPosts = function dropRejectedPosts () { /** @type {Post[]} */ var newRoots = []; for (var i = this.roots.length - 1; i >= 0; i--) { this.roots[i].drop(newRoots); } this.roots = newRoots.sort(Post.byID); }; Thread.prototype.getDate = function getDate () { return this.posts[0].date }; Thread.prototype.getNumber = function getNumber () { if (this.shouldRejectPosts()) { return this.posts.filter(Post.isClean).length } else { return this.posts.length } }; Thread.prototype.getID = function getID () { return this.posts[0].threadId }; Thread.prototype.getThreadButton = function getThreadButton () { return this.posts[0].threadButton }; Thread.prototype.getSite = function getSite () { return this.posts[0].site }; Thread.prototype.isVanished = function isVanished () { return this.config.isVanishedThread(this.getID()) }; /** * @param {import("./tree/view/FlatDepthFirstPosts").default} visitor */ Thread.prototype.accept = function accept (visitor) { for (var i = 0, length = this.roots.length; i < length; i++) { this.roots[i].accept(visitor, 1); } }; /** * @typedef {import("ActualPost").default} ActualPost */ var ThreadMaker = function ThreadMaker(config) { this.config = config; /** @type {Thread[]} */ this.threads = []; }; /** * @param {ActualPost[]} posts */ ThreadMaker.prototype.make = function make (posts) { var this$1$1 = this; var allThreads = Object.create(null); posts.forEach(function (post) { var id = post.threadId; var thread = allThreads[id]; if (!thread) { thread = allThreads[id] = new Thread(this$1$1.config); this$1$1.threads.push(thread); } thread.addPost(post); }); this.sortThreads(); this.computeRoots(); return this.threads }; ThreadMaker.prototype.sortThreads = function sortThreads () { if (this.config.threadOrder === "ascending") { this.threads.reverse(); } }; ThreadMaker.prototype.computeRoots = function computeRoots () { this.threads.forEach(function (thread) { return thread.computeRoots(); }); }; /** * @typedef {import("Thread").default} Thread */ var defaultExport$1 = function defaultExport(config, postRenderer) { this.config = config; this.renderer = postRenderer; }; /** * @param {Thread} thread */ defaultExport$1.prototype.render = function render (thread) { var number = thread.getNumber(); if (!number) { return document.createDocumentFragment() } var dthread = /** @type {HTMLPreElement & {thread: Thread}} */ (this.makeEl(thread)); if ( this.config.toggleTreeMode && this.config.treeMode === "tree-mode-css" ) { dthread.classList.add("useToggleTreeModeButton"); } if (thread.isVanished()) { dthread.classList.add("NGThread"); dthread.querySelector(".vanishThread").classList.add("revert"); } if (this.config.useVanishThread) { dthread.classList.add("useVanishThreadButton"); } thread.accept(this.renderer); dthread.appendChild(this.renderer.getElement()); dthread.thread = thread; return dthread }; /** * @param {Thread} thread */ defaultExport$1.prototype.makeEl = function makeEl (thread) { var button = thread.getThreadButton(); return aPre() .withData("threadId", thread.getID()) .withClass(("thread " + (this.config.treeMode))) .add( aDiv() .withClass("thread-header") .add(button.cloneNode(true)) .add( // eslint-disable-next-line no-irregular-whitespace (" 更新日:" + (thread.getDate()) + " 記事数:" + (thread.getNumber())) ) .add(anA().withClass("toggleTreeMode").add("●")) .add(anA().withClass("vanishThread")) .add(" ") .add(button.cloneNode(true)) .add(thread.getSite()) ) .build() }; /** * @typedef {import("Post").default} Post */ var Message = function Message(ref, post, depth) { var config = ref.config; var embedder = ref.embedder; var ng = ref.ng; this.config = config; this.embedder = embedder; this.ng = ng; this.post = post; this.depth = depth; var el = (this.el = /** @type {HTMLDivElement & {post: Post}} */ (document.createElement("div"))); el.id = post.id; el.post = post; el.dataset.depth = "" + depth; this.text = null; this.dText = document.createElement("div"); this.dText.className = this.classNames("text"); this.headerBuilder = aDiv().withClass(this.classNames("message-header")); }; /** * @param {string} className */ Message.prototype.classNames = function classNames (className) { return (className + " " + className + "_" + (this.mode())) }; Message.prototype.mode = function mode () { throw new Error("Should be implemented in a subclass") }; Message.prototype.getElement = function getElement () { return this.el }; Message.prototype.render = function render () { this.renderMessageOrUnfoldButton(); }; Message.prototype.renderMessageOrUnfoldButton = function renderMessageOrUnfoldButton () { if (this.shouldBeHidden() && !this.post.showForcibly) { this.el.className = this.classNames("showMessage"); this.unfoldButton(); } else { this.el.className = this.classNames("message"); this.renderText(); this.renderHeader(); this.renderEnv(); this.renderElement(); } this.decorateElement(); }; Message.prototype.unfoldButton = function unfoldButton () { var rejectLevel = this.post.rejectLevel; var reasons = []; if (rejectLevel) { reasons.push([null, "孫", "子", "個"][rejectLevel]); } if (this.post.isNG) { reasons.push("NG"); } var button = this.unfoldButtonEl(reasons); this.el.appendChild(button); }; Message.prototype.unfoldButtonEl = function unfoldButtonEl (reasons) { return this.button("showMessageButton", reasons.join(",")).build() }; Message.prototype.button = function button (classes, text) { if ( classes === void 0 ) classes = ""; if ( text === void 0 ) text = ""; return anA().withClass(classes).add(text) }; Message.prototype.renderText = function renderText () { this.transformText(); this.buildDText(); }; Message.prototype.transformText = function transformText () { this.makeText(); this.checkThumbnails(); this.checkCharacterEntity(); this.markNG(); this.truncate(); }; Message.prototype.makeText = function makeText () { var post = this.post; this.text = post.getText(); var parent = post.parent ? post.parent.computeQuotedText() : ""; if (post.showAsIs || post.isNG) { this.markQuote(parent); return } this.snipOutQuotedArea(parent); this.trimText(); this.specialTextForEmptyText(); }; Message.prototype.snipOutQuotedArea = function snipOutQuotedArea (parent) { if (this.text.startsWith(parent)) { this.text = this.text.slice(parent.length); return } //右端に空白があるかもしれないので消してからチェック var trimmedParent = this.trimRights(parent); this.text = this.trimRights(this.text); if (this.text.startsWith(trimmedParent)) { this.text = this.text.slice(trimmedParent.length); return } /* 終わりの空行引用は消してレスする人がいる > PARENT > TEXT 上のようになるところを下のようにレスする > PARENT TEXT */ var a = trimmedParent.replace(/\n(?:> *\n)+\n$/, "\n\n"); if (this.text.startsWith(a)) { this.text = this.text.slice(a.length); return } //親の親の消す深海式レスのチェック var parentWithoutGrandParent = trimmedParent.replace( /^> > .*\n/gm, "" ); if (this.text.startsWith(parentWithoutGrandParent)) { this.text = this.text.slice(parentWithoutGrandParent.length); return } //諦める this.markQuote(parent); }; Message.prototype.specialTextForEmptyText = function specialTextForEmptyText () { if (this.text.length === 0) { this.text = '<span class="note">(空投稿)</span>'; } }; Message.prototype.trimText = function trimText () { //空白のみの投稿が空投稿になってしまうが、分かりやすくなっていいだろう this.text = this.text.trimRight().replace(/^\s*\n/, ""); }; /** * @param {string} parent */ Message.prototype.markQuote = function markQuote (parent) { if (parent.length === 0) { return } var parentLines = parent.split("\n"); parentLines.pop(); var lines = this.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>"; } this.text = lines.join("\n"); }; /** * @param {string} string */ Message.prototype.trimRights = function trimRights (string) { return string.replace(/^.+$/gm, this.trimRight) }; /** * @param {string} string */ Message.prototype.trimRight = function trimRight (string) { return string.trimRight() }; Message.prototype.checkThumbnails = function checkThumbnails () { this.mayHaveThumbnails = this.text.includes("<a"); }; Message.prototype.checkCharacterEntity = function checkCharacterEntity () { var post = this.post; this.hasCharacterEntity = /&#(?:\d+|x[\da-fA-F]+);/.test(this.text); this.expandCharacterEntity = this.hasCharacterEntity && (Object.prototype.hasOwnProperty.call(post, "characterEntity") ? post.characterEntity : this.config.characterEntity); }; Message.prototype.markNG = function markNG () { if (this.post.isNG) { this.text = this.ng.markWord(this.text); } }; Message.prototype.truncate = function truncate () { var post = this.post; var config = this.config; if (!config.maxLine || post.showAsIs) { return } var text = this.text; var maxLine = +config.maxLine; var lines = text.split("\n"); var length = lines.length; if (length > maxLine) { var truncation = Object.prototype.hasOwnProperty.call( post, "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 += "(" + (this.button("toggleTruncation note", label).build().outerHTML) + ")"; } this.text = text; }; Message.prototype.buildDText = function buildDText () { this.createDText(); this.characterEntity(); this.putThumbnails(); }; Message.prototype.createDText = function createDText () { if (this.post.isRead) { this.dText.classList.add("read"); } this.dText.innerHTML = this.text; }; Message.prototype.characterEntity = function characterEntity () { if (this.expandCharacterEntity) { // operaは省略可能な第3,4引数も渡さないとエラーを吐く var iter = document.createNodeIterator( this.dText, NodeFilter.SHOW_TEXT, null, // @ts-ignore false ); var node; while ((node = iter.nextNode())) { node.nodeValue = node.nodeValue.replace( /&#(\d+|x[0-9a-fA-F]+);/g, this.replaceCharacterEntity ); } } }; /** * @param {any} _str * @param {string} p1 */ Message.prototype.replaceCharacterEntity = function replaceCharacterEntity (_str, p1) { var number; if (p1[0] === "x") { number = parseInt(p1.slice(1), 16); } else { number = +p1; } return String.fromCharCode(number) }; Message.prototype.putThumbnails = function putThumbnails () { if (!this.config.thumbnail) { return } if (this.mayHaveThumbnails) { this.embedder.register(this.dText); } }; Message.prototype.renderHeader = function renderHeader () { var post = this.post; var title = post.title; var name = post.name; if (post.isNG) { title = this.ng.markHandle(title); name = this.ng.markHandle(name); } this.buildHeaderContent(name, title); this.dHeader = this.headerBuilder.build(); }; /** * @param {string} name * @param {string} title */ Message.prototype.buildHeaderContent = function buildHeaderContent (name, title) { var post = this.post; var resButton = this.post.resButton; var info = aSpan().withClass("message-info"); if (!this.fromKuuhakuToKuuhaku(title, name)) { info .add(aStrong().addHtml(title)) .add(" : ") .add(aStrong().addHtml(name)) .add(" #"); } info.add(post.date); this.headerBuilder.add(this.resButton(resButton)).add(info).add(" "); if (IS_USAMIN) { this.headerBuilder.addHtml(post.usaminButtons); } else { this.headerBuilder.add(resButton.cloneNode(true)); } this.headerBuilder .add(" ") .add(this.vanishButton()) .add(" ") .add(this.foldButton()) .add(" ") .add(post.posterButton.cloneNode(true)) .add(" ") .add(this.characterEntityButton()) .add(" ") .add(post.threadButton.cloneNode(true)); }; /** * @param {string} title * @param {string} name */ Message.prototype.fromKuuhakuToKuuhaku = function fromKuuhakuToKuuhaku (title, name) { return (title === "> " || title === " ") && name === " " }; /** * @param {HTMLAnchorElement} button */ Message.prototype.resButton = function resButton (button) { var b = /** @type {HTMLAnchorElement} */ (button.cloneNode(true)); b.classList.add("res"); b.target = "link"; b.textContent = "■"; return b }; Message.prototype.vanishButton = function vanishButton () { return this.post.rejectLevel === 3 ? this.button("cancelVanishedMessage", "非表示を解除") : this.config.useVanishMessage ? this.button("toggleMessage") : "" }; Message.prototype.foldButton = function foldButton () { return this.ifTruthy(this.shouldBeHidden(), this.button("fold", "畳む")) }; Message.prototype.characterEntityButton = function characterEntityButton () { return this.ifTruthy( this.hasCharacterEntity, this.button( ("characterEntity " + (this.expandCharacterEntity ? "characterEntityOn" : "")), "文字参照" ) ) }; /** * @template T * @param {boolean} predicate * @param {T} yes */ Message.prototype.ifTruthy = function ifTruthy (predicate, yes) { return predicate ? yes : "" }; /** * NGか個別非表示になっている */ Message.prototype.shouldBeHidden = function shouldBeHidden () { var notCheckMode = !this.config.NGCheckMode; var post = this.post; return (post.isNG && notCheckMode) || !!post.rejectLevel }; Message.prototype.renderEnv = function renderEnv () { if (!this.post.env) { return } this.dEnv = this.envEl(); }; Message.prototype.envEl = function envEl () { return aDiv() .withClass(this.classNames("extra")) .add(this.buildEnvContent()) .build() }; Message.prototype.buildEnvContent = function buildEnvContent () { return aSpan() .withClass("env") .add(("(" + (this.post.env.replace(/<br>/, "/")) + ")")) }; Message.prototype.renderElement = function renderElement () { this.el.appendChild(this.dHeader); this.el.appendChild(this.dText); if (this.dEnv) { this.el.appendChild(this.dEnv); } }; Message.prototype.decorateElement = function decorateElement () { if (this.config.spacingBetweenMessages) { this.setSpacer(); } }; Message.prototype.setSpacer = function setSpacer () { throw new Error("Should be implemented in a subclass") }; /** * @typedef {import("Post").default} Post */ var FlatDepthFirstPosts = function FlatDepthFirstPosts(config, embedder, ng) { if ( embedder === void 0 ) embedder = new Embedder(config); if ( ng === void 0 ) ng = new NG(config); this.argForMessage = {config: config, embedder: embedder, ng: ng}; this.el = document.createElement("span"); this.el.className = "messages"; this.post = null; this.depth = 0; }; /** * @param {Post} post * @param {number} depth */ FlatDepthFirstPosts.prototype.visit = function visit (post, depth) { this.post = post; this.depth = depth; var m = this.renderPost(); this.append(m); }; /** * @private */ FlatDepthFirstPosts.prototype.renderPost = function renderPost () { try { var m = this.makeMessage(); m.render(); return m.getElement() } catch (e) { return this.errorMessageWithPlainMessage(e) } }; /** * @param {Error} e */ FlatDepthFirstPosts.prototype.errorMessageWithPlainMessage = function errorMessageWithPlainMessage (e) { return document .createRange() .createContextualFragment( ("<div class=\"qtv-error\"><p>エラーが発生したため、この投稿をスキップしました。</p><p>" + (e.message) + "</p><b>" + (this.post.title) + "</b> 投稿者:<b>" + (this.post.name) + "</b> 投稿日:" + (this.post.date) + " " + (this.post.resButton.outerHTML) + " " + (this.post.posterButton.outerHTML) + " " + (this.post.threadButton.outerHTML) + "<blockquote><pre>" + (this.post.text) + "</pre></blockquote></div>") ) }; /** * @param {Post} post * @param {number} depth */ FlatDepthFirstPosts.prototype.renderOnePost = function renderOnePost (post, depth) { this.post = post; this.depth = depth; return this.renderPost() }; /** * @return {Message} */ FlatDepthFirstPosts.prototype.makeMessage = function makeMessage () { return new (this.message())(this.argForMessage, this.post, this.depth) }; FlatDepthFirstPosts.prototype.message = function message () { return Message }; /** * @param {Node} node */ FlatDepthFirstPosts.prototype.append = function append (node) { this.getContainer().appendChild(node); }; /** @protected */ FlatDepthFirstPosts.prototype.getContainer = function getContainer () { return this.el }; FlatDepthFirstPosts.prototype.getElement = function getElement () { return this.el }; /** * @typedef {import("Post").default} Post */ var MessageAscii = /*@__PURE__*/(function (Message) { function MessageAscii () { Message.apply(this, arguments); } if ( Message ) MessageAscii.__proto__ = Message; MessageAscii.prototype = Object.create( Message && Message.prototype ); MessageAscii.prototype.constructor = MessageAscii; MessageAscii.prototype.mode = function mode () { return "tree-mode-ascii" }; /** @override */ MessageAscii.prototype.render = function render () { this.computeExtension(); Message.prototype.render.call(this); }; MessageAscii.prototype.computeExtension = function computeExtension () { var depth = this.depth - 1; var tree = new Array(depth); var post = this.post; var parent = post.parent; for (; depth > 0; depth--) { tree[depth] = parent.next ? "|" : " "; parent = parent.parent; } var init = tree.join(""); var hasNext = post.next; var header = post.isOP() ? " " : init + (hasNext ? "├" : "└"); var text = init + (hasNext ? "|" : " ") + (post.child ? "|" : " "); this.extension = { header: this.wrapTree("span", header), text: ("<span class=\"a-tree\">" + text + "</span>"), spacer: this.wrapTree("div", text, "spacer"), env: this.wrapTree("span", text), }; }; /** @override */ MessageAscii.prototype.unfoldButtonEl = function unfoldButtonEl (reasons) { return aGroup() .add(this.extension.header) .add(Message.prototype.unfoldButtonEl.call(this, reasons)) .build() }; /** * @param {keyof HTMLElementTagNameMap} tag */ MessageAscii.prototype.wrapTree = function wrapTree (tag, tree, classes) { if ( classes === void 0 ) classes = ""; return aTag(tag).withClass(("a-tree " + classes)).add(tree) }; /** @override */ MessageAscii.prototype.transformText = function transformText () { Message.prototype.transformText.call(this); this.prependTreeToText(); }; MessageAscii.prototype.prependTreeToText = function prependTreeToText () { this.text = this.text.replace(/^/gm, this.extension.text); }; /** * @param {string} name * @param {string} title * @override */ MessageAscii.prototype.buildHeaderContent = function buildHeaderContent (name, title) { this.headerBuilder.add(this.extension.header); Message.prototype.buildHeaderContent.call(this, name, title); }; /** @override */ MessageAscii.prototype.buildEnvContent = function buildEnvContent () { return aGroup().add(this.extension.env).add(Message.prototype.buildEnvContent.call(this)).build() }; /** @override */ MessageAscii.prototype.setSpacer = function setSpacer () { var spacer = this.extension.spacer.build(); this.el.appendChild(spacer); var text = this.el.getElementsByClassName("text")[0]; if (text) { text.insertBefore(spacer.cloneNode(true), text.firstChild); } }; return MessageAscii; }(Message)); var AsciiTree = /*@__PURE__*/(function (FlatDepthFirstPosts) { function AsciiTree () { FlatDepthFirstPosts.apply(this, arguments); } if ( FlatDepthFirstPosts ) AsciiTree.__proto__ = FlatDepthFirstPosts; AsciiTree.prototype = Object.create( FlatDepthFirstPosts && FlatDepthFirstPosts.prototype ); AsciiTree.prototype.constructor = AsciiTree; AsciiTree.prototype.message = function message () { return MessageAscii }; return AsciiTree; }(FlatDepthFirstPosts)); /** * @typedef {import("Post").default} Post */ var MessageCss = /*@__PURE__*/(function (Message) { function MessageCss () { Message.apply(this, arguments); } if ( Message ) MessageCss.__proto__ = Message; MessageCss.prototype = Object.create( Message && Message.prototype ); MessageCss.prototype.constructor = MessageCss; MessageCss.prototype.mode = function mode () { return "tree-mode-css" }; /** @override */ MessageCss.prototype.decorateElement = function decorateElement () { Message.prototype.decorateElement.call(this); this.setMargin(); }; MessageCss.prototype.setMargin = function setMargin () { this.el.style.marginLeft = this.depth + "rem"; }; /** @override */ MessageCss.prototype.setSpacer = function setSpacer () { this.el.classList.add("spacing"); }; return MessageCss; }(Message)); var CssTree = /*@__PURE__*/(function (FlatDepthFirstPosts) { function CssTree(config) { FlatDepthFirstPosts.call(this, config); /** @type {{dcontainer:HTMLSpanElement, lastChildID?: string}[]} */ this.containers = [{dcontainer: this.el}]; } if ( FlatDepthFirstPosts ) CssTree.__proto__ = FlatDepthFirstPosts; CssTree.prototype = Object.create( FlatDepthFirstPosts && FlatDepthFirstPosts.prototype ); CssTree.prototype.constructor = CssTree; /** @override */ CssTree.prototype.getContainer = function getContainer () { var post = this.post; 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]; } if (post.hasChildren()) { var dout = this.border(); container.dcontainer.appendChild(dout); container = { lastChildID: post.getYoungestChild().id, dcontainer: /** @type {HTMLSpanElement} */ (dout.lastChild), }; containers.push(container); } return container.dcontainer }; CssTree.prototype.border = function border () { return aDiv() .withClass("messagesWithLine") .add( aDiv() .withClass("border") .withStyle(("left:" + (this.depth + 0.5) + "rem")) ) .add(aDiv().withClass("messageAndChildrenButLast")) .build() }; /** @override */ CssTree.prototype.message = function message () { return MessageCss }; return CssTree; }(FlatDepthFirstPosts)); /** * @param {import("Config").default} config */ function createView (config, treeMode) { if ( treeMode === void 0 ) treeMode = config.treeMode; return new { "tree-mode-css": CssTree, "tree-mode-ascii": AsciiTree, }[treeMode](config) } /** * @typedef {import("Post").default} Post * @typedef {import("Thread").default} Thread */ var TreePresenter = function TreePresenter(ctxt, config) { this.ctxt = ctxt; this.config = config; /** @type {Thread[]} */ this.threads = null; /** @type {ParentNode} */ this.fragment = null; }; /** * @param {import("./TreeView").default} view */ TreePresenter.prototype.setView = function setView (view) { this.view = view; }; TreePresenter.prototype.render = function render () { // empty. ツリーでは逐一処理はしない。 }; /** * @param {ParentNode} fragment */ TreePresenter.prototype.finish = function finish (fragment) { var this$1$1 = this; this.fragment = fragment; return this.ctxt .makePosts(fragment, function () { return this$1$1.searchOldLogs(); }) .then(function (posts) { this$1$1.threads = new ThreadMaker(this$1$1.config).make(posts); this$1$1.autovanishThreads(); this$1$1.excludeVanishedThreads(); this$1$1.showPostCount(); var threadsWereShown = this$1$1.renderThreads(); this$1$1.suggestLinkToOldLog(); this$1$1.prepareToggleOriginal(threadsWereShown); return threadsWereShown }) .then(function () { this$1$1.view.finish(fragment); }) }; TreePresenter.prototype.showPostCount = function showPostCount () { var numPosts = this.threads.reduce( function (total, thread) { return total + thread.getNumber(); }, 0 ); this.view.showPostCount(numPosts); }; TreePresenter.prototype.searchOldLogs = function searchOldLogs () { this.view.setInfo( ("<strong>" + (this.ctxt.getLogName()) + "以外の過去ログを検索中...</strong>") ); }; TreePresenter.prototype.excludeVanishedThreads = function excludeVanishedThreads () { if (this.config.utterlyVanishNGThread) { this.threads = this.threads.filter(function (thread) { return !thread.isVanished(); }); } }; TreePresenter.prototype.autovanishThreads = function autovanishThreads () { var this$1$1 = this; if (!this.config.autovanishThread) { return } var ids = this.threads .filter(function (thread) { return thread.isNG; }) .map(function (thread) { return thread.getID(); }); if (!ids.length) { return } var done = this.config.addVanishedThread(ids); if (this.config.utterlyVanishNGThread) { this.view.savingAutovanishedThreads(); return done.then(function () { return this$1$1.view.doneSavingAutovanishedThreads( this$1$1.config.vanishedThreadIDs.length ); } ) } }; TreePresenter.prototype.renderThreads = function renderThreads () { var this$1$1 = this; this.view.setInfo(" - スレッド構築中"); return new Promise(function (resolve) { var i = 0; var length = this$1$1.threads.length; var loop = function () { var t = Date.now(); do { if (i === length) { resolve(); return } this$1$1.showThread(this$1$1.threads[i]); i++; } while (Date.now() - t < 20) setTimeout(loop, 0); }; loop(); }).then(function () { return this$1$1.view.clearInfo(); }) }; /** * @param {Thread} thread */ TreePresenter.prototype.showThread = function showThread (thread) { try { var renderer = new defaultExport$1(this.config, createView(this.config)); var dthread = renderer.render(thread); this.view.renderThread(dthread); } catch (e) { handleError(e); } }; /** * @param {Promise} threadsWereShown */ TreePresenter.prototype.prepareToggleOriginal = function prepareToggleOriginal (threadsWereShown) { var this$1$1 = this; var postsArea = this.ctxt.extractOriginalPostsAreaFrom(this.fragment); return threadsWereShown.then(function () { return this$1$1.appendToggleOriginal(postsArea); }) }; /** * @param {ParentNode} original 元の投稿表示部分 */ TreePresenter.prototype.appendToggleOriginal = function appendToggleOriginal (original) { if (original.querySelector("a[name]")) { this.view.appendToggleOriginal(original); } }; /** * `bbs.log`内をスレッド検索したが、スレッドの先頭が存在しない。 */ TreePresenter.prototype.suggestLinkToOldLog = function suggestLinkToOldLog () { var link = this.ctxt.suggestLink(this.fragment); if (link) { this.view.suggestLinkToOldLog(this.ctxt.suggestLink(this.fragment)); } }; function clearVanishedIds (config, method, button) { return config[method]().then(function () { button.firstElementChild.innerHTML = "0"; }) } function clickQtvReload (form) { form.querySelector("#qtv-reload").click(); } function reload () { var form = document.getElementById("form"); if (!form) { locationReload(); 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;">' ); } clickQtvReload(form); } function createReload (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 } function getAccesskey (config) { var accesskey = config.accesskeyReload; return /^\w$/.test(accesskey) ? accesskey : "R" } function getCounterAndViewing (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 から counter(こわれにくさレベル4) 現在の参加者 : viewing名 (300秒以内) var ref = font.textContent.match(/[\d,]+/g) || []; var counter = ref[3]; var viewing = ref[5]; return (counter + " / " + viewing + " 名") } } return "" } function getTreeMode (node) { return node.closest(".tree-mode-css") ? "tree-mode-css" : "tree-mode-ascii" } /** * @param {import("Config").default} config * @param {(post: import("Post").default) => void} change * @returns {(e: MouseEvent) => void} */ function replace (config, change) { return function (e) { e.preventDefault(); var message = /**@type {HTMLElement & {post: any}} */ ( /** @type {HTMLElement} */ (e.target).closest(".message, .showMessage") ); var post = message.post; change(post); var view = createView(config, getTreeMode(message)); var depth = Number(message.dataset.depth); var newMessage = view.renderOnePost(post, depth); message.parentNode.replaceChild(newMessage, message); }; } function showAsIs (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); }; } /** * @typedef {import("Post").default} Post */ var ToggleMessage = function ToggleMessage(config) { this.config = config; }; /** * @param {MouseEvent} e */ ToggleMessage.prototype.handleEvent = function handleEvent (e) { this.button = /** @type {HTMLAnchorElement} */ (e.target); this.revert = this.button.classList.contains("revert"); /** @type {HTMLElement & {post: Post}} */ 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() }; ToggleMessage.prototype.execute = function execute () { return this.setIDToPost() .then(this.toggle.bind(this)) .catch(this.error.bind(this)) }; ToggleMessage.prototype.toggle = function toggle () { this.setRejectLevel(); this.save(); this.changeButtonState(); this.setChildrensRejectLevel(this.post.child, 2); }; ToggleMessage.prototype.changeButtonState = function changeButtonState () { this.button.classList.toggle("revert"); this.text.classList.toggle("hidden"); }; /** * @param {Error} error */ ToggleMessage.prototype.error = function error (error$1) { this.button.parentNode.replaceChild( document.createTextNode(error$1.message), this.button ); }; ToggleMessage.prototype.setIDToPost = function setIDToPost () { var this$1$1 = this; return this.findPostID().then(function (/** @type {?string} */ id) { if (!id) { throw new Error( "最新1000件以内に存在しないため投稿番号が取得できませんでした。過去ログからなら消せるかもしれません" ) } if (id.length > 100) { throw new Error("この投稿は実在しないようです") } this$1$1.post.id = id; }) }; /** * @typedef {import("GhostPost").default} GhostPost */ ToggleMessage.prototype.findPostID = function findPostID () { var this$1$1 = this; return new Promise(function (resolve) { var post = this$1$1.post; var id = post.id; if (id) { resolve(id); } else { resolve(/** @type {GhostPost} */ (post).getIdForcibly()); } }) }; /** * @param {Post} post * @param {number} rejectLevel */ ToggleMessage.prototype.setChildrensRejectLevel = function setChildrensRejectLevel (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); }; /** * @param {Post} post */ ToggleMessage.prototype.getTargetMessage = function getTargetMessage (post) { return this.messages.querySelector(("[id=\"" + (post.id) + "\"]")) }; ToggleMessage.prototype.save = function save () { if (this.revert) { this.config.removeVanishedMessage(this.post.id); } else { this.config.addVanishedMessage(this.post.id); } }; ToggleMessage.prototype.setRejectLevel = function setRejectLevel () { var post = /** @type {Post & {previousRejectLevel: number, rejectLevel: number}} */ (this .post); if (this.revert) { post.rejectLevel = post.previousRejectLevel; } else { post.previousRejectLevel = post.rejectLevel; post.rejectLevel = 3; } }; /** * @param {Post} post * @param {number} rejectLevel */ ToggleMessage.prototype.shouldProcess = function shouldProcess (post, rejectLevel) { if (this.revert) { return post.rejectLevel <= rejectLevel } else { return post.rejectLevel < rejectLevel } }; /** * @param {Post} post * @param {any} rejectLevel */ ToggleMessage.prototype.setChildRejectLevel = function setChildRejectLevel (post, rejectLevel) { if (this.revert) { post.rejectLevel = 0; } else { post.rejectLevel = rejectLevel; } }; /** * @param {Element} message */ ToggleMessage.prototype.processMarking = function processMarking (message) { if (this.revert) { var mark = message.querySelector(".chainingHidden"); if (mark) { mark.classList.remove("chainingHidden"); } } else { if (!message.querySelector(".chainingHidden")) { message.firstElementChild.classList.add("chainingHidden"); } } }; function toggleThread (config) { return function (e) { var button = e.target; var thread = button.closest(".thread"); var id = thread.dataset.threadId; if (thread.classList.contains("NGThread")) { config.removeVanishedThread(id); } else { config.addVanishedThread(id); } thread.classList.toggle("NGThread"); button.classList.toggle("revert"); }; } function toggleTreeMode (/** @type {import("Config").default} */ config) { return function ( /** @type {Event} */ e ) { e.preventDefault(); var button = /** @type {HTMLElement} */ (e.target); var thread = /** @type {HTMLElement & {thread: import("Thread").default}} */ (button.closest(".thread")); thread.classList.toggle("tree-mode-css"); thread.classList.toggle("tree-mode-ascii"); var renderer = createView(config, getTreeMode(thread)); thread.thread.accept(renderer); var newMessages = renderer.getElement(); thread.replaceChild(newMessages, thread.querySelector(".messages")); }; } /** * @typedef {import("Config").default} Config * @typedef {import("Post").default} Post */ var defaultExport = function defaultExport(config) { this.config = config; this.createTreeGuiContainer(); this.addEventListeners(); }; /** * @param {string} html */ defaultExport.prototype.setInfo = function setInfo (html) { this.info.innerHTML = html; }; defaultExport.prototype.clearInfo = function clearInfo () { this.info.innerHTML = ""; }; /** * @param {string} html */ defaultExport.prototype.setExtraInfo = function setExtraInfo (html) { this.el.querySelector("#extrainfo").innerHTML = html; }; /** * @param {number} numPosts */ defaultExport.prototype.setPostCount = function setPostCount (numPosts) { if (numPosts) { this.postcount.textContent = numPosts + "件取得"; } else { this.postcount.textContent = "未読メッセージはありません。"; } }; defaultExport.prototype.getContent = function getContent () { return this.content }; defaultExport.prototype.addEventListeners = function addEventListeners () { var this$1$1 = this; var ref = this; var el = ref.el; var config = ref.config; var click = on.bind(null, el, "click"); click(".reload", reload); click(".mattari", midokureload); click(".goToForm", this.focusV) ;["Message", "Thread"].forEach(function (type) { var id = "clearVanished" + type + "IDs"; click("#" + id, function (/** @type {MouseEvent} */ e) { e.preventDefault(); clearVanishedIds(this$1$1.config, id, e.target); }); }); /** * @param {string} selector * @param {(post: Post) => void} callback */ function replaceWithNewPostByClicking(selector, callback) { on(el, "click", selector, replace(config, callback)); } replaceWithNewPostByClicking(".characterEntity", function (post) { var characterEntity = Object.prototype.hasOwnProperty.call( post, "characterEntity" ) ? post.characterEntity : config.characterEntity; post.characterEntity = !characterEntity; }); replaceWithNewPostByClicking(".showMessageButton", function (post) { post.showForcibly = true; }); replaceWithNewPostByClicking(".cancelVanishedMessage", function (post) { config.removeVanishedMessage(post.id); delete post.rejectLevel; }); replaceWithNewPostByClicking(".fold", function (post) { post.showForcibly = false; }); on(el, "mousedown", ".message", showAsIs(config)); replaceWithNewPostByClicking(".toggleTruncation", function (post) { post.truncation = Object.prototype.hasOwnProperty.call(post, "truncation") ? !post.truncation : false; }); if (config.useVanishMessage) { on(el, "click", ".toggleMessage", new ToggleMessage(config)); } on(el, "click", ".vanishThread", toggleThread(config)); on(el, "click", ".toggleTreeMode", toggleTreeMode(config)); }; defaultExport.prototype.focusV = function focusV () { setTimeout(function () { document.getElementsByName("v")[0].focus(); }, 50); }; defaultExport.prototype.savingAutovanishedThreads = function savingAutovanishedThreads () { var buttons = this.footer.querySelector(".clearVanishedButtons"); buttons.insertAdjacentHTML( "beforebegin", '<span class="savingVanishedThreadIDs">非表示スレッド保存中</span>' ); }; /** * @param {number} numVanishedThreads */ defaultExport.prototype.doneSavingAutovanishedThreads = function doneSavingAutovanishedThreads (numVanishedThreads) { var saving = this.footer.querySelector(".savingVanishedThreadIDs"); if (!saving) { return } saving.parentNode.removeChild(saving); if (numVanishedThreads) { var buttons = this.footer.querySelector(".clearVanishedButtons"); buttons.querySelector( "#clearVanishedThreadIDs .count" ).textContent = String(numVanishedThreads); buttons.classList.remove("hidden"); } }; defaultExport.prototype.createTreeGuiContainer = function createTreeGuiContainer (body) { if ( body === void 0 ) body = document.body; var el = document.createElement("div"); el.id = "container"; el.innerHTML = this.headerTemplate(body) + '<div id="content"></div><hr>' + this.footerTemplate(); var ng = new NG(this.config); if (ng.message) { el.querySelector("#header").lastElementChild.insertAdjacentHTML( "beforebegin", ng.message ); } var header = el.firstElementChild; var info = header.querySelector("#info"); var postcount = info.previousElementSibling; this.el = el; this.info = info; this.postcount = postcount; this.content = /** @type {HTMLElement} */ (header.nextSibling); this.footer = /** @type {HTMLElement}*/ (el.lastChild); }; /** * @param {HTMLElement} body */ defaultExport.prototype.headerTemplate = function headerTemplate (body) { var reload = createReload(this.config); var accesskey = getAccesskey(this.config); var counterAndViewing = getCounterAndViewing(body); return ("\n\t\t<header id=\"header\">\n\t\t\t<span>\n\t\t\t\t" + (reload.replace( 'class="mattari"', ("$& title=\"ヽ(´ー`)ノロード\" accesskey=\"" + accesskey + "\"") )) + "\n\t\t\t\t" + counterAndViewing + "\n\t\t\t\t<span id=\"postcount\"></span>\n\t\t\t\t<span id=\"info\">ダウンロード中...</span>\n\t\t\t\t<span id=\"extrainfo\"></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 + "\n\t\t\t</span>\n\t\t</header>") }; defaultExport.prototype.footerTemplate = function footerTemplate () { var reload = createReload(this.config); var length = { Thread: this.config.vanishedThreadIDs.length, Message: this.config.vanishedMessageIDs.length, }; var hidden = length.Thread || length.Message ? "" : "hidden"; var count = function (/** @type {string} */ type, /** @type {string} */ 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>\n\t\t\t\t" + reload + "\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 + "\n\t\t\t</span>\n\t\t</footer>") }; defaultExport.prototype.hasMessage = function hasMessage () { return !!this.content.querySelector(".message:not(.read)") }; var ToggleOriginal = function ToggleOriginal(original) { this.toggle = document.createElement("div"); this.appendButton(); this.stack = this.createStackArea(original); this.toggle.appendChild(this.stack); }; ToggleOriginal.prototype.getUI = function getUI () { return this.toggle }; /** * @param {ParentNode} original */ 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.appendButton = function appendButton () { var this$1$1 = this; this.toggle.insertAdjacentHTML( "beforeend", '<div style="text-align:center"><a class="toggleOriginal" href="javascript:;">元の投稿の表示する(時間がかかることがあります)</a></div><hr>' ); this.toggle .querySelector(".toggleOriginal") .addEventListener("click", function (e) { return this$1$1.toggleOriginal(e); }); }; 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 TreeView = /*@__PURE__*/(function (Qtv) { function TreeView(config) { Qtv.call(this, config); this.gui = new defaultExport(this.config); /** @type {import("./TreePresenter").default} */ this.presenter = null; this.ng = new NG(config); } if ( Qtv ) TreeView.__proto__ = Qtv; TreeView.prototype = Object.create( Qtv && Qtv.prototype ); TreeView.prototype.constructor = TreeView; TreeView.prototype.setPresenter = function setPresenter (presenter) { this.presenter = presenter; }; /** * @override */ TreeView.prototype.initializeComponent = function initializeComponent () { Qtv.prototype.initializeComponent.call(this); this.prepend(this.gui.el); }; /** * @param {ParentNode} original 元の投稿表示部分 */ TreeView.prototype.appendToggleOriginal = function appendToggleOriginal (original) { var toggle = new ToggleOriginal(original); this.insert(toggle.getUI()); }; TreeView.prototype.showPostCount = function showPostCount (numPosts) { this.gui.setPostCount(numPosts); }; TreeView.prototype.setInfo = function setInfo (html) { this.gui.setInfo(html); }; TreeView.prototype.clearInfo = function clearInfo () { this.gui.clearInfo(); }; TreeView.prototype.savingAutovanishedThreads = function savingAutovanishedThreads () { this.gui.savingAutovanishedThreads(); }; /** * @param {number} [numVanishedThreads] */ TreeView.prototype.doneSavingAutovanishedThreads = function doneSavingAutovanishedThreads (numVanishedThreads) { this.gui.doneSavingAutovanishedThreads(numVanishedThreads); }; /** * @param {string} href */ TreeView.prototype.suggestLinkToOldLog = function suggestLinkToOldLog (href) { this.gui.setExtraInfo(("<a id=\"hint\" href=\"" + href + "\">過去ログを検索する</a>")); }; /** * @param {ParentNode} fragment */ TreeView.prototype.appendLeftovers = function appendLeftovers (fragment) { this.append(fragment); }; /** * @param {ParentNode} fragment * @override */ TreeView.prototype.finish = function finish (fragment) { tweakFooter(this.gui.hasMessage(), fragment); this.appendLeftovers(fragment); return Qtv.prototype.finish.call(this) }; TreeView.prototype.renderThread = function renderThread (el) { this.gui.getContent().appendChild(el); }; return TreeView; }(Qtv)); function getTitle() { return document.title } var CloseResWindow = function CloseResWindow(gotConfig) { this.gotConfig = gotConfig; }; CloseResWindow.prototype.onLoaded = function onLoaded () { return this.closeIfNeeded() }; CloseResWindow.prototype.closeIfNeeded = function closeIfNeeded () { var this$1$1 = this; return this.gotConfig.then(function (config) { if (this$1$1.shouldClose(config)) { this$1$1.close(); } }) }; /** * @param {import("Config").default} config * @private */ CloseResWindow.prototype.shouldClose = function shouldClose (config) { return config.closeResWindow && getTitle().endsWith(" 書き込み完了") }; /** @private */ CloseResWindow.prototype.close = function close () { closeTab(); }; /** * @typedef {import("Config").default} Config */ var State = function State () {}; State.prototype.configLoaded = function configLoaded (_p, _config) { throw new Error("Undefined") }; /** * @param {Prestage} _p * @param {ParentNode} _fragment */ State.prototype.onProgress = function onProgress (_p, _fragment) { throw new Error("Undefined") }; /** * @param {Prestage} _p * @param {ParentNode} _fragment */ State.prototype.onLoaded = function onLoaded (_p, _fragment) { throw new Error("Undefined") }; /** * @param {Prestage} _p */ State.prototype.proceed = function proceed (_p) { throw new Error("Undefined") }; /** * @param {Prestage} _p */ State.prototype.abort = function abort (_p) { throw new Error("Undefined") }; var Init = /*@__PURE__*/(function (State) { function Init () { State.apply(this, arguments); } if ( State ) Init.__proto__ = State; Init.prototype = Object.create( State && State.prototype ); Init.prototype.constructor = Init; Init.prototype.configLoaded = function configLoaded (p, config) { p.setReady(); p.setConfig(config); }; /** * @override * @param {Prestage} p * @param {ParentNode} fragment */ Init.prototype.onProgress = function onProgress (p, fragment) { p.setBuffering(); p.onProgress(fragment); }; /** * @override * @param {Prestage} p */ Init.prototype.onLoaded = function onLoaded (p) { p.setDead(); }; return Init; }(State)); var Buffering = /*@__PURE__*/(function (State) { function Buffering () { State.apply(this, arguments); } if ( State ) Buffering.__proto__ = State; Buffering.prototype = Object.create( State && State.prototype ); Buffering.prototype.constructor = Buffering; Buffering.prototype.configLoaded = function configLoaded (p, config) { p.setConfig(config); p.setRendering(); p.prepareRendering(); }; /** * @override * @param {Prestage} _p * @param {ParentNode} _fragment */ Buffering.prototype.onProgress = function onProgress (_p, _fragment) { // Bufferがバッファリング中 }; /** * @override * @param {Prestage} p * @param {ParentNode} fragment */ Buffering.prototype.onLoaded = function onLoaded (p, fragment) { p.setWaitingForConfig(); p.stash(fragment); }; return Buffering; }(State)); var WaitingForConfig = /*@__PURE__*/(function (State) { function WaitingForConfig () { State.apply(this, arguments); } if ( State ) WaitingForConfig.__proto__ = State; WaitingForConfig.prototype = Object.create( State && State.prototype ); WaitingForConfig.prototype.constructor = WaitingForConfig; WaitingForConfig.prototype.configLoaded = function configLoaded (p, config) { p.setConfig(config); p.checkToProceed(); }; /** * @override * @param {Prestage} p */ WaitingForConfig.prototype.proceed = function proceed (p) { p.prepareRendering(); p.rewindAndFinish(); }; /** * @override * @param {Prestage} p */ WaitingForConfig.prototype.abort = function abort (p) { p.restore(); p.setDead(); }; return WaitingForConfig; }(State)); /** * `Config`ロード済み。まだ投稿を受信していない。 */ var Ready = /*@__PURE__*/(function (State) { function Ready () { State.apply(this, arguments); } if ( State ) Ready.__proto__ = State; Ready.prototype = Object.create( State && State.prototype ); Ready.prototype.constructor = Ready; Ready.prototype.onProgress = function onProgress (p, _fragment) { p.setCheckingToProceed(); p.checkToProceed(); }; /** * @override * @param {Prestage} p */ Ready.prototype.onLoaded = function onLoaded (p) { p.setDead(); }; return Ready; }(State)); var CheckingToProceed = /*@__PURE__*/(function (State) { function CheckingToProceed () { State.apply(this, arguments); } if ( State ) CheckingToProceed.__proto__ = State; CheckingToProceed.prototype = Object.create( State && State.prototype ); CheckingToProceed.prototype.constructor = CheckingToProceed; CheckingToProceed.prototype.proceed = function proceed (p) { p.setRendering(); p.prepareRendering(); }; /** * @override * @param {Prestage} p */ CheckingToProceed.prototype.abort = function abort (p) { p.setDead(); }; return CheckingToProceed; }(State)); var Rendering = /*@__PURE__*/(function (State) { function Rendering () { State.apply(this, arguments); } if ( State ) Rendering.__proto__ = State; Rendering.prototype = Object.create( State && State.prototype ); Rendering.prototype.constructor = Rendering; Rendering.prototype.onProgress = function onProgress (p, fragment) { p.render(fragment); }; /** * @override * @param {Prestage} p * @param {ParentNode} fragment */ Rendering.prototype.onLoaded = function onLoaded (p, fragment) { p.finish(fragment); p.setDead(); }; return Rendering; }(State)); var Dead = /*@__PURE__*/(function (State) { function Dead () { State.apply(this, arguments); } if ( State ) Dead.__proto__ = State; Dead.prototype = Object.create( State && State.prototype ); Dead.prototype.constructor = Dead; Dead.prototype.configLoaded = function configLoaded () {}; /** * @override * @param {Prestage} p * @param {ParentNode} fragment */ Dead.prototype.onProgress = function onProgress (p, fragment) { p.passThrough(fragment); }; /** * @override * @param {Prestage} p * @param {ParentNode} fragment */ Dead.prototype.onLoaded = function onLoaded (p, fragment) { p.passThrough(fragment); }; /** @override */ Dead.prototype.proceed = function proceed () {}; /** @override */ Dead.prototype.abort = function abort () {}; return Dead; }(State)); var init = new Init(); var buffering = new Buffering(); var ready = new Ready(); var waitingForConfig = new WaitingForConfig(); var checkingToProceed = new CheckingToProceed(); var rendering = new Rendering(); var dead = new Dead(); var Prestage = function Prestage(controller) { /** @type {State} */ this.state = init; this.controller = controller; }; Prestage.prototype.prepareRendering = function prepareRendering () { this.controller.prepareRendering(); }; /** * @param {Config} config */ Prestage.prototype.setConfig = function setConfig (config) { this.controller.setConfig(config); }; /** * @param {ParentNode} fragment */ Prestage.prototype.render = function render (fragment) { this.controller.render(fragment); }; /** * @param {ParentNode} fragment */ Prestage.prototype.finish = function finish (fragment) { this.controller.finish(fragment); }; /** * @param {ParentNode} fragment */ Prestage.prototype.stash = function stash (fragment) { this.controller.stash(fragment); }; Prestage.prototype.rewindAndFinish = function rewindAndFinish () { this.controller.rewindAndFinish(); }; Prestage.prototype.restore = function restore () { this.controller.restore(); }; Prestage.prototype.checkToProceed = function checkToProceed () { this.controller.checkToProceed(this); }; /** * @param {ParentNode} fragment */ Prestage.prototype.passThrough = function passThrough (fragment) { this.controller.passThrough(fragment); }; /** * @param {Config} config */ Prestage.prototype.configLoaded = function configLoaded (config) { this.state.configLoaded(this, config); }; /** * @param {ParentNode} fragment */ Prestage.prototype.onProgress = function onProgress (fragment) { this.state.onProgress(this, fragment); }; /** * @param {ParentNode} fragment */ Prestage.prototype.onLoaded = function onLoaded (fragment) { this.state.onLoaded(this, fragment); }; Prestage.prototype.proceed = function proceed () { this.state.proceed(this); }; Prestage.prototype.abort = function abort () { this.state.abort(this); }; Prestage.prototype.setBuffering = function setBuffering () { this.state = buffering; }; Prestage.prototype.setWaitingForConfig = function setWaitingForConfig () { this.state = waitingForConfig; }; Prestage.prototype.setReady = function setReady () { this.state = ready; }; Prestage.prototype.setRendering = function setRendering () { this.state = rendering; }; Prestage.prototype.setCheckingToProceed = function setCheckingToProceed () { this.state = checkingToProceed; }; Prestage.prototype.setDead = function setDead () { this.state = dead; }; /** * @param {import("Config").default} config */ function shouldQuitHere (config, title) { if ( title === void 0 ) title = getTitle(); return ( (IS_USAMIN && config.viewMode === "s") || title.endsWith(" 個人用環境設定") || title.startsWith("くずはすくりぷと ") || title === "パスワード" ) } /** * @typedef {import("Config").default} Config */ var PrestageController = function PrestageController(stasher, factory) { this.stasher = stasher; this.factory = factory; /** @type {Config} */ this.config = null; this.qtv = null; }; /** * @param {Config} config */ PrestageController.prototype.setConfig = function setConfig (config) { this.config = config; }; PrestageController.prototype.prepareRendering = function prepareRendering () { if (this.config.isTreeView()) { this.qtv = this.factory.treeView(this.config); } else { this.qtv = this.factory.stackView(this.config); } }; /** * @param {ParentNode} fragment */ PrestageController.prototype.render = function render (fragment) { this.qtv.render(fragment); }; /** * @param {ParentNode} fragment */ PrestageController.prototype.finish = function finish (fragment) { this.render(fragment); return this.qtv.finish(fragment).catch(handleError) }; /** * @param {ParentNode} fragment */ PrestageController.prototype.stash = function stash (fragment) { this.stasher.stash(fragment); this.stasher.appendTo(document.body); }; PrestageController.prototype.rewindAndFinish = function rewindAndFinish () { var fragment = this.stasher.restore(); return this.finish(fragment) }; PrestageController.prototype.restore = function restore () { var fragment = this.stasher.restore(); document.body.appendChild(fragment); }; /** * @param {import("./Prestage").default} p */ PrestageController.prototype.checkToProceed = function checkToProceed (p) { if (shouldQuitHere(this.config)) { p.abort(); } else { p.proceed(); } }; /** * @param {ParentNode} fragment */ PrestageController.prototype.passThrough = function passThrough (fragment) { document.body.appendChild(fragment); }; /** * @typedef {import("../Config").default} Config */ var Factory = function Factory(q) { this.q = q; }; /** * @param {Promise<Config>} gotConfig */ Factory.prototype.prestage = function prestage (gotConfig) { var observer = window.MutationObserver ? new LoadingObserver() : new LoadedObserver(); var buffer = new Buffer(); var closeResWindow = new CloseResWindow(gotConfig); var notice = new DelayNotice(gotConfig); var stasher = new Stash(); var controller = new PrestageController(stasher, this); var prestage = new Prestage(controller); observer.addListener(notice); observer.addListener(buffer); observer.addListener(closeResWindow); buffer.setListener(prestage); gotConfig.then(function (config) { return prestage.configLoaded(config); }); observer.observe(); return prestage }; /** * @param {Config} config */ Factory.prototype.treeView = function treeView (config) { var postParent = new PostParent(config); var ctxt = new Context(config, this.q, postParent); var presenter = new TreePresenter(ctxt, config); var view = new TreeView(config); view.setPresenter(presenter); view.initializeComponent(); presenter.setView(view); return presenter }; /** * @param {Config} config */ Factory.prototype.stackView = function stackView (config) { var view = new StackView(config); var presenter = new StackPresenter(config, this.q); view.setPresenter(presenter); view.initializeComponent(); presenter.setView(view); return presenter }; var Main = function Main () {}; Main.main = function main (q) { if ( q === void 0 ) q = new Query(); switch (q.get("m")) { case "f": //レス窓 tweakResWindow(); return case "l": //トピック一覧 case "c": //個人用設定 return case "g": //過去ログ if (!q.shouldHaveValidPosts()) { return } } var factory = new Factory(q); factory.prestage(Config.load()); }; var isOpera = "opera" in window || navigator.userAgent.indexOf(" OPR/") >= 0; if (isOpera) { var createRange = document.createRange.bind(document); document.createRange = function () { var range = createRange(); // 引数は何でもいいが何かを設定しないとopera12のcreateContextualFragmentで<html>...</html>が返る range.selectNodeContents(document.createElement("div")); return range }; } // 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) // License: MIT var _undefined = undefined, // let the obfiscator compress these down STATE_PENDING = _undefined, // 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 _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 bufferSize = 1024; var fqStart = 0; // avoid using shift() by maintaining a start pointer - and remove items in chunks of 1024 (bufferSize) 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) { Zousan.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 for Node if(typeof process !== _undefinedString && typeof process.nextTick === "function") { return function() { process.nextTick(callQueue); } } // if No MutationObserver - this is the next best thing for 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 Zousan is called without "new", throw an error if (!(this instanceof Zousan)) { throw new TypeError("Zousan must be created with the new keyword") } // If a function was specified, call it back with the resolve/reject functions bound to this context if(typeof func === "function") { var me = this; try { func( function (arg) { return me.resolve(arg); }, // the resolve function bound to this context. (actually using bind() is slower) function (arg) { return me.reject(arg); }); // the reject function bound to this context } catch(e) { me.reject(e); } } else if(arguments.length > 0) // If an argument was specified and it is NOT a function, throw an error { throw new TypeError("Zousan resolver " + func + " is not a function") } } 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")) { var first = true; // first time through? try { 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 } var me = this; // preserve this 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 { soon(function() { if(!me.handled) { if(!Zousan.suppressUncaughtRejectionError) { Zousan.warn("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; // In the case that the original promise is already fulfilled, any uncaught rejection should already have been warned about this.handled = true; // set promise as "handled" to suppress warning for unhandled rejections 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) { return new Zousan(function (resolve) { return resolve(val); }); }; Zousan.reject = function(err) { var z = new Zousan(); z.c=[]; // see https://github.com/bluejava/zousan/issues/7#issuecomment-415394963 z.reject(err); return z }; Zousan.all = function(pa) { var results = [ ], retP = new Zousan(); // results and final return promise var rc = 0; // 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 we have a console, use it for our errors and warnings, else do nothing (either/both can be overwritten) var nop = function () { }; Zousan.warn = typeof console !== _undefinedString ? console.warn : nop; Zousan.error = typeof console !== _undefinedString ? console.error : nop; // make soon accessable from Zousan Zousan.soon = soon; if (!window.Promise && typeof it == "undefined") { window.Promise = 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); }; } if (!Array.prototype.find) { Array.prototype.find = function (predicate) { var found; this.some(function (value) { if (predicate(value)) { found = value; return true } return false }); return found }; } Main.main(); })();