From 329fc4dd140f2db87749926a923656e71354a641 Mon Sep 17 00:00:00 2001 From: Lalle <29478339+LalleSX@users.noreply.github.com> Date: Fri, 28 Apr 2023 03:25:13 +0200 Subject: [PATCH] added a few types --- .eslintrc | 3 +- src/classes/Board.ts | 24 +- src/classes/Callbacks.ts | 22 +- src/classes/CatalogThread.ts | 25 +- src/classes/CatalogThreadNative.ts | 9 +- src/classes/Connection.ts | 14 +- src/classes/DataBoard.ts | 49 ++-- src/classes/Fetcher.ts | 162 ++++++------ src/classes/Post.ts | 148 +++++------ src/classes/RandomAccessList.ts | 19 +- src/classes/ShimSet.ts | 2 + src/classes/SimpleDict.ts | 16 +- src/classes/Thread.ts | 91 ++++--- src/platform/$.ts | 391 +++++++++++++++-------------- src/site/SW.tinyboard.ts | 92 +++---- 15 files changed, 559 insertions(+), 508 deletions(-) diff --git a/.eslintrc b/.eslintrc index 2950865..f162e86 100644 --- a/.eslintrc +++ b/.eslintrc @@ -48,6 +48,7 @@ "never" ], "simple-import-sort/imports": "error", - "simple-import-sort/exports": "error" + "simple-import-sort/exports": "error", + "no-cond-assign": "off" } } \ No newline at end of file diff --git a/src/classes/Board.ts b/src/classes/Board.ts index 9cca359..ecccaaa 100644 --- a/src/classes/Board.ts +++ b/src/classes/Board.ts @@ -1,22 +1,26 @@ import BoardConfig from "../General/BoardConfig" import { d, g } from "../globals/globals" +import Post from "./Post" import SimpleDict from "./SimpleDict" +import Thread from "./Thread" + -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ export default class Board { + ID: string + boardID: number | string + siteID: number + threads: SimpleDict + posts: SimpleDict + config: any toString() { return this.ID } constructor(ID) { this.ID = ID this.boardID = this.ID - this.siteID = g.SITE.ID + this.siteID = g.SITE.ID this.threads = new SimpleDict() - this.posts = new SimpleDict() - this.config = BoardConfig.boards?.[this.ID] || {} + this.posts = new SimpleDict() + this.config = BoardConfig.boards?.[this.ID] || {} g.boards[this] = this } @@ -25,8 +29,8 @@ export default class Board { const c2 = (this.config || {}).cooldowns || {} const c = { thread: c2.threads || 0, - reply: c2.replies || 0, - image: c2.images || 0, + reply: c2.replies || 0, + image: c2.images || 0, thread_global: 300 // inter-board thread cooldown } // Pass users have reduced cooldowns. diff --git a/src/classes/Callbacks.ts b/src/classes/Callbacks.ts index 2e62112..2d0c824 100644 --- a/src/classes/Callbacks.ts +++ b/src/classes/Callbacks.ts @@ -1,15 +1,17 @@ import Main from "../main/Main" +import Post from "./Post" + -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS206: Consider reworking classes to avoid initClass - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ export default class Callbacks { + static Post: Callbacks + static Thread: Callbacks + static CatalogThread: Callbacks + static CatalogThreadNative: Callbacks + type: string + keys: string[] static initClass() { - this.Post = new Callbacks('Post') - this.Thread = new Callbacks('Thread') + this.Post = new Callbacks('Post') + this.Thread = new Callbacks('Thread') this.CatalogThread = new Callbacks('Catalog Thread') this.CatalogThreadNative = new Callbacks('Catalog Thread') } @@ -19,12 +21,12 @@ export default class Callbacks { this.keys = [] } - push({name, cb}) { + push({ name, cb }) { if (!this[name]) { this.keys.push(name) } return this[name] = cb } - execute(node, keys=this.keys, force=false) { + execute(node, keys = this.keys, force = false) { let errors if (node.callbacksExecuted && !force) { return } node.callbacksExecuted = true diff --git a/src/classes/CatalogThread.ts b/src/classes/CatalogThread.ts index 899fa00..06adda6 100644 --- a/src/classes/CatalogThread.ts +++ b/src/classes/CatalogThread.ts @@ -1,21 +1,28 @@ import $ from "../platform/$" +import Board from "./Board" +import Post from "./Post" +import Thread from "./Thread" export default class CatalogThread { + ID: any + thread: Thread + board: any + nodes: { root: Post; thumb: HTMLElement; icons: any; postCount: number; fileCount: number; pageCount: number; replies: any } toString() { return this.ID } - constructor(root, thread) { + constructor(root: Post, thread: Thread) { this.thread = thread - this.ID = this.thread.ID + this.ID = this.thread.ID + '' this.board = this.thread.board - const {post} = this.thread.OP.nodes + const { post } = this.thread.OP.nodes this.nodes = { root, - thumb: $('.catalog-thumb', post), - icons: $('.catalog-icons', post), - postCount: $('.post-count', post), - fileCount: $('.file-count', post), - pageCount: $('.page-count', post), - replies: null + thumb: $('.catalog-thumb', post), + icons: $('.catalog-icons', post), + postCount: $('.post-count', post), + fileCount: $('.file-count', post), + pageCount: $('.page-count', post), + replies: null } this.thread.catalogView = this } diff --git a/src/classes/CatalogThreadNative.ts b/src/classes/CatalogThreadNative.ts index d1d0eb7..3c55ea6 100644 --- a/src/classes/CatalogThreadNative.ts +++ b/src/classes/CatalogThreadNative.ts @@ -4,6 +4,13 @@ import Board from "./Board" import Thread from "./Thread" export default class CatalogThreadNative { + ID: number | string + nodes: { root: Thread; thumb: HTMLElement } + siteID: string + boardID: string + board: Board | import("/home/victor/proj/4chan-XZ/src/globals/globals").Board + threadID: number + thread: Thread toString() { return this.ID } constructor(root) { @@ -11,7 +18,7 @@ export default class CatalogThreadNative { root, thumb: $(g.SITE.selectors.catalog.thumb, root) } - this.siteID = g.SITE.ID + this.siteID = g.SITE.ID this.boardID = this.nodes.thumb.parentNode.pathname.split(/\/+/)[1] this.board = g.boards[this.boardID] || new Board(this.boardID) this.ID = (this.threadID = +(root.dataset.id || root.id).match(/\d*$/)[0]) diff --git a/src/classes/Connection.ts b/src/classes/Connection.ts index 846f912..115e84e 100644 --- a/src/classes/Connection.ts +++ b/src/classes/Connection.ts @@ -1,13 +1,13 @@ import { g } from "../globals/globals" import $ from "../platform/$" +import Callbacks from "./Callbacks" + -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ export default class Connection { - constructor(target, origin, cb={}) { + target: any + origin: any + cb: Callbacks + constructor(target: Window, origin: string, cb: Callbacks) { this.send = this.send.bind(this) this.onMessage = this.onMessage.bind(this) this.target = target @@ -28,7 +28,7 @@ export default class Connection { return this.targetWindow().postMessage(`${g.NAMESPACE}${JSON.stringify(data)}`, this.origin) } - onMessage(e) { + onMessage(e: MessageEvent) { if ((e.source !== this.targetWindow()) || (e.origin !== this.origin) || (typeof e.data !== 'string') || diff --git a/src/classes/DataBoard.ts b/src/classes/DataBoard.ts index c9b0527..a6d9aac 100644 --- a/src/classes/DataBoard.ts +++ b/src/classes/DataBoard.ts @@ -11,6 +11,11 @@ import { dict, HOUR } from "../platform/helpers" * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md */ export default class DataBoard { + static keys: string[] + static changes: string[] + key: string + sync: VoidFunction + data: any static initClass() { this.keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'watcherLastModified', 'customTitles'] @@ -38,8 +43,8 @@ export default class DataBoard { this.data = data if (this.data.boards) { let lastChecked; - ({boards, lastChecked} = this.data) - this.data['4chan.org'] = {boards, lastChecked} + ({ boards, lastChecked } = this.data) + this.data['4chan.org'] = { boards, lastChecked } delete this.data.boards delete this.data.lastChecked } @@ -76,31 +81,31 @@ export default class DataBoard { }) } - delete({siteID, boardID, threadID, postID}, cb) { + delete({ siteID, boardID, threadID, postID }, cb) { if (!siteID) { siteID = g.SITE.ID } if (!this.data[siteID]) { return } return this.save(() => { if (postID) { if (!this.data[siteID].boards[boardID]?.[threadID]) { return } delete this.data[siteID].boards[boardID][threadID][postID] - return this.deleteIfEmpty({siteID, boardID, threadID}) + return this.deleteIfEmpty({ siteID, boardID, threadID }) } else if (threadID) { if (!this.data[siteID].boards[boardID]) { return } delete this.data[siteID].boards[boardID][threadID] - return this.deleteIfEmpty({siteID, boardID}) + return this.deleteIfEmpty({ siteID, boardID }) } else { return delete this.data[siteID].boards[boardID] } } - , cb) + , cb) } - deleteIfEmpty({siteID, boardID, threadID}) { + deleteIfEmpty({ siteID, boardID, threadID }) { if (!this.data[siteID]) { return } if (threadID) { if (!Object.keys(this.data[siteID].boards[boardID][threadID]).length) { delete this.data[siteID].boards[boardID][threadID] - return this.deleteIfEmpty({siteID, boardID}) + return this.deleteIfEmpty({ siteID, boardID }) } } else if (!Object.keys(this.data[siteID].boards[boardID]).length) { return delete this.data[siteID].boards[boardID] @@ -111,10 +116,10 @@ export default class DataBoard { return this.save(() => { return this.setUnsafe(data) } - , cb) + , cb) } - setUnsafe({siteID, boardID, threadID, postID, val}) { + setUnsafe({ siteID, boardID, threadID, postID, val }) { if (!siteID) { siteID = g.SITE.ID } if (!this.data[siteID]) { this.data[siteID] = { boards: dict() } } if (postID !== undefined) { @@ -127,7 +132,7 @@ export default class DataBoard { } } - extend({siteID, boardID, threadID, postID, val}, cb) { + extend({ siteID, boardID, threadID, postID, val }, cb) { return this.save(() => { const oldVal = this.get({ siteID, boardID, threadID, postID, defaultValue: dict() }) for (const key in val) { @@ -138,18 +143,18 @@ export default class DataBoard { oldVal[key] = subVal } } - return this.setUnsafe({siteID, boardID, threadID, postID, val: oldVal}) + return this.setUnsafe({ siteID, boardID, threadID, postID, val: oldVal }) } - , cb) + , cb) } - setLastChecked(key='lastChecked') { + setLastChecked(key = 'lastChecked') { return this.save(() => { return this.data[key] = Date.now() }) } - get({siteID, boardID, threadID, postID, defaultValue}) { + get({ siteID, boardID, threadID, postID, defaultValue }) { let board, val if (!siteID) { siteID = g.SITE.ID } if (board = this.data[siteID]?.boards[boardID]) { @@ -169,7 +174,7 @@ export default class DataBoard { } else if (thread = board[threadID]) { val = (postID != null) ? thread[postID] - : + : thread } } @@ -181,7 +186,7 @@ export default class DataBoard { const siteID = g.SITE.ID for (boardID in this.data[siteID].boards) { const val = this.data[siteID].boards[boardID] - this.deleteIfEmpty({siteID, boardID}) + this.deleteIfEmpty({ siteID, boardID }) } const now = Date.now() if (now - (2 * HOUR) >= ((middle = this.data[siteID].lastChecked || 0)) || middle > now) { @@ -195,14 +200,14 @@ export default class DataBoard { ajaxClean(boardID) { const that = this const siteID = g.SITE.ID - const threadsList = g.SITE.urls.threadsListJSON?.({siteID, boardID}) + const threadsList = g.SITE.urls.threadsListJSON?.({ siteID, boardID }) if (!threadsList) { return } - return $.cache(threadsList, function() { + return $.cache(threadsList, function () { if (this.status !== 200) { return } - const archiveList = g.SITE.urls.archiveListJSON?.({siteID, boardID}) + const archiveList = g.SITE.urls.archiveListJSON?.({ siteID, boardID }) if (!archiveList) { return that.ajaxCleanParse(boardID, this.response) } const response1 = this.response - return $.cache(archiveList, function() { + return $.cache(archiveList, function () { if ((this.status !== 200) && (!!g.SITE.archivedBoardsKnown || (this.status !== 404))) { return } return that.ajaxCleanParse(boardID, response1, this.response) }) @@ -228,7 +233,7 @@ export default class DataBoard { } } this.data[siteID].boards[boardID] = threads - this.deleteIfEmpty({siteID, boardID}) + this.deleteIfEmpty({ siteID, boardID }) return $.set(this.key, this.data) } diff --git a/src/classes/Fetcher.ts b/src/classes/Fetcher.ts index f7ba790..d99c7d4 100644 --- a/src/classes/Fetcher.ts +++ b/src/classes/Fetcher.ts @@ -1,7 +1,7 @@ import Redirect from "../Archive/Redirect" import Get from "../General/Get" import Index from "../General/Index" -import { Conf, d,E, g } from "../globals/globals" +import { Conf, d, E, g } from "../globals/globals" import ImageHost from "../Images/ImageHost" import Main from "../main/Main" import $ from "../platform/$" @@ -21,30 +21,37 @@ import Thread from "./Thread" * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md */ export default class Fetcher { + archiveTags: { '\n': { innerHTML: string }; '[b]': { innerHTML: string }; '[/b]': { innerHTML: string }; '[spoiler]': { innerHTML: string }; '[/spoiler]': { innerHTML: string }; '[code]': { innerHTML: string }; '[/code]': { innerHTML: string }; '[moot]': { innerHTML: string }; '[/moot]': { innerHTML: string }; '[banned]': { innerHTML: string }; '[/banned]': { innerHTML: string }; '[fortune]'(text: string): { innerHTML: string }; '[/fortune]': { innerHTML: string }; '[i]': { innerHTML: string }; '[/i]': { innerHTML: string }; '[red]': { innerHTML: string }; '[/red]': { innerHTML: string }; '[green]': { innerHTML: string }; '[/green]': { innerHTML: string }; '[blue]': { innerHTML: string }; '[/blue]': { innerHTML: string } } + boardID: string + threadID: number + postID: number + root: HTMLElement + quoter: any + static flagCSS: any static initClass() { - + this.prototype.archiveTags = { - '\n': {innerHTML: "
"}, - '[b]': {innerHTML: ""}, - '[/b]': {innerHTML: ""}, - '[spoiler]': {innerHTML: ""}, - '[/spoiler]': {innerHTML: ""}, - '[code]': {innerHTML: "
"},
-      '[/code]':    {innerHTML: "
"}, - '[moot]': {innerHTML: "
"}, - '[/moot]': {innerHTML: "
"}, - '[banned]': {innerHTML: ""}, - '[/banned]': {innerHTML: ""}, - '[fortune]'(text) { return {innerHTML: ""} }, - '[/fortune]': {innerHTML: ""}, - '[i]': {innerHTML: ""}, - '[/i]': {innerHTML: ""}, - '[red]': {innerHTML: ""}, - '[/red]': {innerHTML: ""}, - '[green]': {innerHTML: ""}, - '[/green]': {innerHTML: ""}, - '[blue]': {innerHTML: ""}, - '[/blue]': {innerHTML: ""} + '\n': { innerHTML: "
" }, + '[b]': { innerHTML: "" }, + '[/b]': { innerHTML: "" }, + '[spoiler]': { innerHTML: "" }, + '[/spoiler]': { innerHTML: "" }, + '[code]': { innerHTML: "
" },
+      '[/code]': { innerHTML: "
" }, + '[moot]': { innerHTML: "
" }, + '[/moot]': { innerHTML: "
" }, + '[banned]': { innerHTML: "" }, + '[/banned]': { innerHTML: "" }, + '[fortune]'(text) { return { innerHTML: "" } }, + '[/fortune]': { innerHTML: "" }, + '[i]': { innerHTML: "" }, + '[/i]': { innerHTML: "" }, + '[red]': { innerHTML: "" }, + '[/red]': { innerHTML: "" }, + '[green]': { innerHTML: "" }, + '[/green]': { innerHTML: "" }, + '[blue]': { innerHTML: "" }, + '[/blue]': { innerHTML: "" } } } constructor(boardID, threadID, postID, root, quoter) { @@ -61,8 +68,8 @@ export default class Fetcher { // 4chan X catalog data if ((post = Index.replyData?.[`${this.boardID}.${this.postID}`]) && (thread = g.threads.get(`${this.boardID}.${this.threadID}`))) { - const board = g.boards[this.boardID] - post = new Post(g.SITE.Build.postFromObject(post, this.boardID), thread, board, {isFetchedQuote: true}) + const board = g.boards[this.boardID] + post = new Post(g.SITE.Build.postFromObject(post, this.boardID), thread, board, { isFetchedQuote: true }) Main.callbackNodes('Post', [post]) this.insert(post) return @@ -71,7 +78,7 @@ export default class Fetcher { this.root.textContent = `Loading post No.${this.postID}...` if (this.threadID) { const that = this - $.cache(g.SITE.urls.threadJSON({boardID: this.boardID, threadID: this.threadID}), function({isCached}) { + $.cache(g.SITE.urls.threadJSON({ boardID: this.boardID, threadID: this.threadID }), function ({ isCached }) { return that.fetchedPost(this, isCached) }) } else { @@ -87,14 +94,14 @@ export default class Fetcher { Main.callbackNodes('Post', [clone]) // Get rid of the side arrows/stubs. - const {nodes} = clone + const { nodes } = clone $.rmAll(nodes.root) $.add(nodes.root, nodes.post) // Indicate links to the containing post. const quotes = [...clone.nodes.quotelinks, ...clone.nodes.backlinks] for (const quote of quotes) { - const {boardID, postID} = Get.postDataFromLink(quote) + const { boardID, postID } = Get.postDataFromLink(quote) if ((postID === this.quoter.ID) && (boardID === this.quoter.board.ID)) { $.addClass(quote, 'forwardlink') } @@ -125,7 +132,7 @@ export default class Fetcher { return } - const {status} = req + const { status } = req if (status !== 200) { // The thread can die by the time we check a quote. if (status && this.archivedPost()) { return } @@ -134,14 +141,14 @@ export default class Fetcher { this.root.textContent = status === 404 ? `Thread No.${this.threadID} 404'd.` - : !status ? - 'Connection Error' - : - `Error ${req.statusText} (${req.status}).` + : !status ? + 'Connection Error' + : + `Error ${req.statusText} (${req.status}).` return } - const {posts} = req.response + const { posts } = req.response g.SITE.Build.spoilerRange[this.boardID] = posts[0].custom_spoiler for (post of posts) { if (post.no === this.postID) { break } @@ -150,10 +157,10 @@ export default class Fetcher { if (post.no !== this.postID) { // Cached requests can be stale and must be rechecked. if (isCached) { - const api = g.SITE.urls.threadJSON({boardID: this.boardID, threadID: this.threadID}) + const api = g.SITE.urls.threadJSON({ boardID: this.boardID, threadID: this.threadID }) $.cleanCache(url => url === api) const that = this - $.cache(api, function() { + $.cache(api, function () { return that.fetchedPost(this, false) }) return @@ -171,7 +178,7 @@ export default class Fetcher { new Board(this.boardID) const thread = g.threads.get(`${this.boardID}.${this.threadID}`) || new Thread(this.threadID, board) - post = new Post(g.SITE.Build.postFromObject(post, this.boardID), thread, board, {isFetchedQuote: true}) + post = new Post(g.SITE.Build.postFromObject(post, this.boardID), thread, board, { isFetchedQuote: true }) Main.callbackNodes('Post', [post]) return this.insert(post) } @@ -179,14 +186,14 @@ export default class Fetcher { archivedPost() { let url if (!Conf['Resurrect Quotes']) { return false } - if (!(url = Redirect.to('post', {boardID: this.boardID, postID: this.postID}))) { return false } + if (!(url = Redirect.to('post', { boardID: this.boardID, postID: this.postID }))) { return false } const archive = Redirect.data.post[this.boardID] const encryptionOK = /^https:\/\//.test(url) || (location.protocol === 'http:') if (encryptionOK || Conf['Exempt Archives from Encryption']) { const that = this - CrossOrigin.cache(url, function() { + CrossOrigin.cache(url, function () { if (!encryptionOK && this.response?.media) { - const {media} = this.response + const { media } = this.response for (const key in media) { // Image/thumbnail URLs loaded over HTTP can be modified in transit. // Require them to be from an HTTP host so that no referrer is sent to them from an HTTPS page. @@ -237,42 +244,43 @@ export default class Fetcher { } else { const greentext = text[0] === '>' text = text.replace(/(\[\/?[a-z]+):lit(\])/g, '$1$2') - text = text.split(/(>>(?:>\/[a-z\d]+\/)?\d+)/g).map((text2, j) => - {((j % 2) ? "" + E(text2) + "" : E(text2))}) - text = {innerHTML: ((greentext) ? "" + E.cat(text) + "" : E.cat(text))} + text = text.split(/(>>(?:>\/[a-z\d]+\/)?\d+)/g).map((text2, j) => { ((j % 2) ? "" + E(text2) + "" : E(text2)) }) + text = { innerHTML: ((greentext) ? "" + E.cat(text) + "" : E.cat(text)) } result.push(text) } } return result })() - comment = {innerHTML: E.cat(comment)} + comment = { innerHTML: E.cat(comment) } this.threadID = +data.thread_num const o = { - ID: this.postID, + ID: this.postID, threadID: this.threadID, - boardID: this.boardID, - isReply: this.postID !== this.threadID + boardID: this.boardID, + isReply: this.postID !== this.threadID } o.info = { - subject: data.title, - email: data.email, - name: data.name || '', + subject: data.title, + email: data.email, + name: data.name || '', tripcode: data.trip, - capcode: (() => { switch (data.capcode) { - // https://github.com/pleebe/FoolFuuka/blob/bf4224eed04637a4d0bd4411c2bf5f9945dfec0b/assets/themes/foolz/foolfuuka-theme-fuuka/src/Partial/Board.php#L77 - case 'M': return 'Mod' - case 'A': return 'Admin' - case 'D': return 'Developer' - case 'V': return 'Verified' - case 'F': return 'Founder' - case 'G': return 'Manager' - } })(), + capcode: (() => { + switch (data.capcode) { + // https://github.com/pleebe/FoolFuuka/blob/bf4224eed04637a4d0bd4411c2bf5f9945dfec0b/assets/themes/foolz/foolfuuka-theme-fuuka/src/Partial/Board.php#L77 + case 'M': return 'Mod' + case 'A': return 'Admin' + case 'D': return 'Developer' + case 'V': return 'Verified' + case 'F': return 'Founder' + case 'G': return 'Manager' + } + })(), uniqueID: data.poster_hash, flagCode: data.poster_country, flagCodeTroll: data.troll_country_code, - flag: data.poster_country_name || data.troll_country_name, - dateUTC: data.timestamp, + flag: data.poster_country_name || data.troll_country_name, + dateUTC: data.timestamp, dateText: data.fourchan_date, commentHTML: comment } @@ -280,26 +288,26 @@ export default class Fetcher { if (data.media && !!+data.media.banned) { o.fileDeleted = true } else if (data.media?.media_filename) { - let {thumb_link} = data.media + let { thumb_link } = data.media // Fix URLs missing origin if (thumb_link?.[0] === '/') { thumb_link = url.split('/', 3).join('/') + thumb_link } if (!Redirect.securityCheck(thumb_link)) { thumb_link = '' } - let media_link = Redirect.to('file', {boardID: this.boardID, filename: data.media.media_orig}) + let media_link = Redirect.to('file', { boardID: this.boardID, filename: data.media.media_orig }) if (!Redirect.securityCheck(media_link)) { media_link = '' } o.file = { - name: data.media.media_filename, - url: media_link || - (this.boardID === 'f' ? - `${location.protocol}//${ImageHost.flashHost()}/${this.boardID}/${encodeURIComponent(E(data.media.media_filename))}` - : - `${location.protocol}//${ImageHost.host()}/${this.boardID}/${data.media.media_orig}`), - height: data.media.media_h, - width: data.media.media_w, - MD5: data.media.media_hash, - size: $.bytesToString(data.media.media_size), - thumbURL: thumb_link || `${location.protocol}//${ImageHost.thumbHost()}/${this.boardID}/${data.media.preview_orig}`, - theight: data.media.preview_h, - twidth: data.media.preview_w, + name: data.media.media_filename, + url: media_link || + (this.boardID === 'f' ? + `${location.protocol}//${ImageHost.flashHost()}/${this.boardID}/${encodeURIComponent(E(data.media.media_filename))}` + : + `${location.protocol}//${ImageHost.host()}/${this.boardID}/${data.media.media_orig}`), + height: data.media.media_h, + width: data.media.media_w, + MD5: data.media.media_hash, + size: $.bytesToString(data.media.media_size), + thumbURL: thumb_link || `${location.protocol}//${ImageHost.thumbHost()}/${this.boardID}/${data.media.preview_orig}`, + theight: data.media.preview_h, + twidth: data.media.preview_w, isSpoiler: data.media.spoiler === '1' } if (!/\.pdf$/.test(o.file.url)) { o.file.dimensions = `${o.file.width}x${o.file.height}` } @@ -311,7 +319,7 @@ export default class Fetcher { new Board(this.boardID) const thread = g.threads.get(`${this.boardID}.${this.threadID}`) || new Thread(this.threadID, board) - post = new Post(g.SITE.Build.post(o), thread, board, {isFetchedQuote: true}) + post = new Post(g.SITE.Build.post(o), thread, board, { isFetchedQuote: true }) post.kill() if (post.file) { post.file.thumbURL = o.file.thumbURL } Main.callbackNodes('Post', [post]) diff --git a/src/classes/Post.ts b/src/classes/Post.ts index 813f528..dc145d9 100644 --- a/src/classes/Post.ts +++ b/src/classes/Post.ts @@ -1,5 +1,5 @@ import Get from "../General/Get" -import { Conf,g } from "../globals/globals" +import { Conf, g } from "../globals/globals" import ImageExpand from "../Images/ImageExpand" import $ from "../platform/$" import $$ from "../platform/$$" @@ -8,41 +8,41 @@ import Callbacks from "./Callbacks" import type Thread from "./Thread" export default class Post { - declare root: HTMLElement - declare thread: Thread - declare board: Board - declare ID: number - declare postID: number - declare threadID: number - declare boardID: number | string - declare siteID: number | string - declare fullID: string - declare context: Post - declare isReply: boolean - declare nodes: ReturnType - declare isDead: boolean - declare isHidden: boolean - declare clones: any[] - declare isRebuilt?: boolean + declare root: HTMLElement + declare thread: Thread + declare board: Board + declare ID: number + declare postID: number + declare threadID: number + declare boardID: number | string + declare siteID: number | string + declare fullID: string + declare context: Post + declare isReply: boolean + declare nodes: ReturnType + declare isDead: boolean + declare isHidden: boolean + declare clones: any[] + declare isRebuilt?: boolean declare isFetchedQuote: boolean - declare isClone: boolean - declare quotes: string[] - declare file: ReturnType - declare files: ReturnType[] + declare isClone: boolean + declare quotes: string[] + declare file: ReturnType + declare files: ReturnType[] declare info: { - subject: string | undefined, - name: string | undefined, - email: string | undefined, - tripcode: string | undefined, - uniqueID: string | undefined, - capcode: string | undefined, - pass: string | undefined, - flagCode: string | undefined, + subject: string | undefined, + name: string | undefined, + email: string | undefined, + tripcode: string | undefined, + uniqueID: string | undefined, + capcode: string | undefined, + pass: string | undefined, + flagCode: string | undefined, flagCodeTroll: string | undefined, - flag: string | undefined, - date: Date | undefined, - nameBlock: string, + flag: string | undefined, + date: Date | undefined, + nameBlock: string, } // because of a circular dependency $ might not be initialized, so we can't use $.el @@ -57,7 +57,7 @@ export default class Post { toString() { return this.ID } - constructor(root?: HTMLElement, thread?: Thread, board?: Board, flags={}) { + constructor(root?: HTMLElement, thread?: Thread, board?: Board, flags = {}) { // <% if (readJSON('/.tests_enabled')) { %> // @normalizedOriginal = Test.normalize root // <% } %> @@ -69,14 +69,14 @@ export default class Post { this.thread = thread this.board = board $.extend(this, flags) - this.ID = +root.id.match(/\d*$/)[0] - this.postID = this.ID + this.ID = +root.id.match(/\d*$/)[0] + this.postID = this.ID this.threadID = this.thread.ID - this.boardID = this.board.ID - this.siteID = g.SITE.ID - this.fullID = `${this.board}.${this.ID}` - this.context = this - this.isReply = (this.ID !== this.threadID) + this.boardID = this.board.ID + this.siteID = g.SITE.ID + this.fullID = `${this.board}.${this.ID}` + this.context = this + this.isReply = (this.ID !== this.threadID) root.dataset.fullID = this.fullID @@ -100,17 +100,17 @@ export default class Post { const tripcode = this.nodes.tripcode?.textContent this.info = { - subject: this.nodes.subject?.textContent || undefined, + subject: this.nodes.subject?.textContent || undefined, name, - email: this.nodes.email ? decodeURIComponent(this.nodes.email.href.replace(/^mailto:/, '')) : undefined, + email: this.nodes.email ? decodeURIComponent(this.nodes.email.href.replace(/^mailto:/, '')) : undefined, tripcode, - uniqueID: this.nodes.uniqueID?.textContent, - capcode: this.nodes.capcode?.textContent.replace('## ', ''), - pass: this.nodes.pass?.title.match(/\d*$/)[0], - flagCode: this.nodes.flag?.className.match(/flag-(\w+)/)?.[1].toUpperCase(), + uniqueID: this.nodes.uniqueID?.textContent, + capcode: this.nodes.capcode?.textContent.replace('## ', ''), + pass: this.nodes.pass?.title.match(/\d*$/)[0], + flagCode: this.nodes.flag?.className.match(/flag-(\w+)/)?.[1].toUpperCase(), flagCodeTroll: this.nodes.flag?.className.match(/bfl-(\w+)/)?.[1].toUpperCase(), - flag: this.nodes.flag?.title, - date: this.nodes.date ? g.SITE.parseDate(this.nodes.date) : undefined, + flag: this.nodes.flag?.title, + date: this.nodes.date ? g.SITE.parseDate(this.nodes.date) : undefined, nameBlock: Conf['Anonymize'] ? 'Anonymous' : `${name || ''} ${tripcode || ''}`.trim(), } @@ -121,7 +121,7 @@ export default class Post { this.parseQuotes() this.parseFiles() - this.isDead = false + this.isDead = false this.isHidden = false this.clones = [] @@ -149,31 +149,31 @@ export default class Post { const info: HTMLElement = $(s.infoRoot, post) interface Node { - root: HTMLElement, - bottom: false | HTMLElement, - post: HTMLElement, - info: HTMLElement, - comment: HTMLElement; - quotelinks: HTMLAnchorElement[], + root: HTMLElement, + bottom: false | HTMLElement, + post: HTMLElement, + info: HTMLElement, + comment: HTMLElement; + quotelinks: HTMLAnchorElement[], archivelinks: HTMLAnchorElement[], - embedlinks: HTMLAnchorElement[], - backlinks: HTMLCollectionOf; + embedlinks: HTMLAnchorElement[], + backlinks: HTMLCollectionOf; uniqueIDRoot: any, - uniqueID: any, + uniqueID: any, } const nodes: Node & Partial> = { root, - bottom: this.isReply || !g.SITE.isOPContainerThread ? root : $(s.opBottom, root), + bottom: this.isReply || !g.SITE.isOPContainerThread ? root : $(s.opBottom, root), post, info, - comment: $(s.comment, post), + comment: $(s.comment, post), quotelinks: [], archivelinks: [], - embedlinks: [], - backlinks: post.getElementsByClassName('backlink') as HTMLCollectionOf, + embedlinks: [], + backlinks: post.getElementsByClassName('backlink') as HTMLCollectionOf, uniqueIDRoot: undefined as any, - uniqueID: undefined as any, + uniqueID: undefined as any, } for (const key in s.info) { const selector = s.info[key] @@ -293,13 +293,13 @@ export default class Post { parseFile(fileRoot: HTMLElement) { interface File { - text: string, - link: HTMLAnchorElement, - thumb: HTMLElement, - thumbLink: HTMLElement, - size: string, + text: string, + link: HTMLAnchorElement, + thumb: HTMLElement, + thumbLink: HTMLElement, + size: string, sizeInBytes: number, - isDead: boolean, + isDead: boolean, } const file: Partial = { isDead: false } @@ -313,20 +313,20 @@ export default class Post { if (!g.SITE.parseFile(this, file)) { return } $.extend(file, { - url: file.link.href, + url: file.link.href, isImage: $.isImage(file.link.href), isVideo: $.isVideo(file.link.href) } ) - let size = +file.size.match(/[\d.]+/)[0] - let unit = ['B', 'KB', 'MB', 'GB'].indexOf(file.size.match(/\w+$/)[0]) + let size = +file.size.match(/[\d.]+/)[0] + let unit = ['B', 'KB', 'MB', 'GB'].indexOf(file.size.match(/\w+$/)[0]) while (unit-- > 0) { size *= 1024 } file.sizeInBytes = size return file as File } - kill(file, index=0) { + kill(file, index = 0) { let strong if (file) { if (this.isDead || this.files[index].isDead) { return } @@ -341,7 +341,7 @@ export default class Post { if (!(strong = $('strong.warning', this.nodes.info))) { strong = $.el('strong', - {className: 'warning'}) + { className: 'warning' }) $.after($('input', this.nodes.info), strong) } strong.textContent = file ? '[File deleted]' : '[Deleted]' diff --git a/src/classes/RandomAccessList.ts b/src/classes/RandomAccessList.ts index 95b897e..d023f42 100644 --- a/src/classes/RandomAccessList.ts +++ b/src/classes/RandomAccessList.ts @@ -4,6 +4,9 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md */ export default class RandomAccessList { + length: number + last: any + first: any constructor(items) { this.length = 0 if (items) { for (const item of items) { this.push(item) } } @@ -11,10 +14,10 @@ export default class RandomAccessList { push(data) { let item - let {ID} = data + let { ID } = data if (!ID) { ID = data.id } if (this[ID]) { return } - const {last} = this + const { last } = this this[ID] = (item = { prev: last, next: null, @@ -24,7 +27,7 @@ export default class RandomAccessList { item.prev = last this.last = last ? (last.next = item) - : + : (this.first = item) return this.length++ } @@ -34,7 +37,7 @@ export default class RandomAccessList { this.rmi(item) - const {prev} = root + const { prev } = root root.prev = item item.next = root item.prev = prev @@ -50,7 +53,7 @@ export default class RandomAccessList { this.rmi(item) - const {next} = root + const { next } = root root.next = item item.prev = root item.next = next @@ -62,10 +65,10 @@ export default class RandomAccessList { } prepend(item) { - const {first} = this + const { first } = this if ((item === first) || !this[item.ID]) { return } this.rmi(item) - item.next = first + item.next = first if (first) { first.prev = item } else { @@ -97,7 +100,7 @@ export default class RandomAccessList { } rmi(item) { - const {prev, next} = item + const { prev, next } = item if (prev) { prev.next = next } else { diff --git a/src/classes/ShimSet.ts b/src/classes/ShimSet.ts index 54df89d..a19e9cd 100644 --- a/src/classes/ShimSet.ts +++ b/src/classes/ShimSet.ts @@ -4,6 +4,8 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md */ class ShimSet { + elements: any + size: number constructor() { this.elements = $.dict() this.size = 0 diff --git a/src/classes/SimpleDict.ts b/src/classes/SimpleDict.ts index 820d001..1dc2ac3 100644 --- a/src/classes/SimpleDict.ts +++ b/src/classes/SimpleDict.ts @@ -1,11 +1,5 @@ import $ from "../platform/$" -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ export default class SimpleDict { keys: string[] @@ -13,14 +7,14 @@ export default class SimpleDict { this.keys = [] } - push(key, data: T) { + push(key: string, data: T): T { key = `${key}` if (!this[key]) { this.keys.push(key) } return this[key] = data } - rm(key) { - let i + rm(key: string) { + let i: number key = `${key}` if ((i = this.keys.indexOf(key)) !== -1) { this.keys.splice(i, 1) @@ -28,11 +22,11 @@ export default class SimpleDict { } } - forEach(fn) { + forEach(fn: (value: T) => void): void { for (const key of [...Array.from(this.keys)]) { fn(this[key]) } } - get(key): T { + get(key: string): T { if (key === 'keys') { return undefined } else { diff --git a/src/classes/Thread.ts b/src/classes/Thread.ts index beaad53..1ff90b1 100644 --- a/src/classes/Thread.ts +++ b/src/classes/Thread.ts @@ -1,74 +1,92 @@ import { g } from "../globals/globals" import $ from "../platform/$" +import Board from "./Board" +import Post from "./Post" import SimpleDict from "./SimpleDict" -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ + export default class Thread { + ID: number + OP: Post + isArchived: boolean + isClosed: boolean + lastPost: number + posts: SimpleDict + board: Board + threadID: number + boardID: number | string + siteID: number + fullID: string + isDead: boolean + isHidden: boolean + isSticky: boolean + postLimit: boolean + fileLimit: boolean + ipCount: number + json: JSON + catalogView: Node + nodes: { root: Post } toString() { return this.ID } - constructor(ID, board) { + constructor(ID: number | string, board: Board) { this.board = board - this.ID = +ID - this.threadID = this.ID - this.boardID = this.board.ID - this.siteID = g.SITE.ID - this.fullID = `${this.board}.${this.ID}` - this.posts = new SimpleDict() - this.isDead = false - this.isHidden = false - this.isSticky = false - this.isClosed = false + this.ID = +ID + this.threadID = this.ID + this.boardID = this.board.ID + this.siteID = g.SITE.ID + this.fullID = `${this.board}.${this.ID}` + this.posts = new SimpleDict() + this.isDead = false + this.isHidden = false + this.isSticky = false + this.isClosed = false this.isArchived = false - this.postLimit = false - this.fileLimit = false - this.lastPost = 0 - this.ipCount = undefined - this.json = null + this.postLimit = false + this.fileLimit = false + this.lastPost = 0 + this.ipCount = undefined + this.json = null this.OP = null this.catalogView = null this.nodes = - {root: null} + { root: null } this.board.threads.push(this.ID, this) g.threads.push(this.fullID, this) } - setPage(pageNum) { - let icon - const {info, reply} = this.OP.nodes + setPage(pageNum: number) { + let icon: HTMLElement + const { info, reply } = this.OP.nodes if (!(icon = $('.page-num', info))) { - icon = $.el('span', {className: 'page-num'}) + icon = $.el('span', { className: 'page-num' }) $.replace(reply.parentNode.previousSibling, [$.tn(' '), icon, $.tn(' ')]) } - icon.title = `This thread is on page ${pageNum} in the original index.` + icon.title = `This thread is on page ${pageNum} in the original index.` icon.textContent = `[${pageNum}]` if (this.catalogView) { return this.catalogView.nodes.pageCount.textContent = pageNum } } - setCount(type, count, reachedLimit) { + setCount(type: string, count: number, reachedLimit: boolean) { if (!this.catalogView) { return } const el = this.catalogView.nodes[`${type}Count`] el.textContent = count return (reachedLimit ? $.addClass : $.rmClass)(el, 'warning') } - setStatus(type, status) { + setStatus(type: string, status: boolean) { const name = `is${type}` if (this[name] === status) { return } this[name] = status if (!this.OP) { return } - this.setIcon('Sticky', this.isSticky) - this.setIcon('Closed', this.isClosed && !this.isArchived) + this.setIcon('Sticky', this.isSticky) + this.setIcon('Closed', this.isClosed && !this.isArchived) return this.setIcon('Archived', this.isArchived) } - setIcon(type, status) { + setIcon(type: string, status: boolean) { const typeLC = type.toLowerCase() let icon = $(`.${typeLC}Icon`, this.OP.nodes.info) if (!!icon === status) { return } @@ -81,18 +99,17 @@ export default class Thread { } icon = $.el('img', { src: `${g.SITE.Build.staticPath}${typeLC}${g.SITE.Build.gifIcon}`, - alt: type, + alt: type, title: type, className: `${typeLC}Icon retina` - } - ) + }, g.BOARD.ID) if (g.BOARD.ID === 'f') { icon.style.cssText = 'height: 18px; width: 18px;' } const root = (type !== 'Sticky') && this.isSticky ? $('.stickyIcon', this.OP.nodes.info) - : + : $('.page-num', this.OP.nodes.info) || this.OP.nodes.quote $.after(root, [$.tn(' '), icon]) @@ -106,7 +123,7 @@ export default class Thread { collect() { let n = 0 - this.posts.forEach(function(post) { + this.posts.forEach(function (post) { if (post.clones.length) { return n++ } else { diff --git a/src/platform/$.ts b/src/platform/$.ts index 8dfad97..cf232a2 100644 --- a/src/platform/$.ts +++ b/src/platform/$.ts @@ -18,10 +18,10 @@ const $ = (selector, root = document.body) => root.querySelector(selector) $.id = id => d.getElementById(id) -$.ajaxPage = function(url, options) { +$.ajaxPage = function (url, options) { if (options.responseType == null) { options.responseType = 'json' } if (!options.type) { options.type = (options.form && 'post') || 'get' } - const {onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers} = options + const { onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers } = options const r = new XMLHttpRequest() r.open(type, url, true) const object = headers || {} @@ -29,26 +29,26 @@ $.ajaxPage = function(url, options) { const value = object[key] r.setRequestHeader(key, value) } - $.extend(r, {onloadend, timeout, responseType, withCredentials}) - $.extend(r.upload, {onprogress}) + $.extend(r, { onloadend, timeout, responseType, withCredentials }) + $.extend(r.upload, { onprogress }) // connection error or content blocker - $.on(r, 'error', function() { if (!r.status) { return c.warn(`4chan X failed to load: ${url}`) } }) + $.on(r, 'error', function () { if (!r.status) { return c.warn(`4chan X failed to load: ${url}`) } }) r.send(form) return r } -$.ready = function(fc) { +$.ready = function (fc) { if (d.readyState !== 'loading') { $.queueTask(fc) return } - const cb = function() { + const cb = function () { $.off(d, 'DOMContentLoaded', cb) return fc() } return $.on(d, 'DOMContentLoaded', cb) } -$.formData = function(form) { +$.formData = function (form) { if (form instanceof HTMLFormElement) { return new FormData(form) } @@ -66,7 +66,7 @@ $.formData = function(form) { return fd } -$.extend = function(object, properties) { +$.extend = function (object, properties) { for (const key in properties) { const val = properties[key] object[key] = val @@ -75,11 +75,11 @@ $.extend = function(object, properties) { $.hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key) -$.getOwn = function(obj, key) { +$.getOwn = function (obj, key) { if (Object.prototype.hasOwnProperty.call(obj, key)) { return obj[key] } else { return undefined } } -$.ajax = (function() { +$.ajax = (function () { let pageXHR // @ts-ignore if (window.wrappedJSObject && !XMLHttpRequest.wrappedJSObject) { @@ -88,7 +88,7 @@ $.ajax = (function() { pageXHR = XMLHttpRequest } - const r = (function (url, options={}) { + const r = (function (url, options = {}) { if (options.responseType == null) { options.responseType = 'json' } if (!options.type) { options.type = (options.form && 'post') || 'get' } // XXX https://forums.lanik.us/viewtopic.php?f=64&t=24173&p=78310 @@ -99,7 +99,7 @@ $.ajax = (function() { return $.ajaxPage(url, options) } } - const {onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers} = options + const { onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers } = options const r = new pageXHR() try { r.open(type, url, true) @@ -108,10 +108,10 @@ $.ajax = (function() { const value = object[key] r.setRequestHeader(key, value) } - $.extend(r, {onloadend, timeout, responseType, withCredentials}) - $.extend(r.upload, {onprogress}) + $.extend(r, { onloadend, timeout, responseType, withCredentials }) + $.extend(r.upload, { onprogress }) // connection error or content blocker - $.on(r, 'error', function() { if (!r.status) { return c.warn(`4chan X failed to load: ${url}`) } }) + $.on(r, 'error', function () { if (!r.status) { return c.warn(`4chan X failed to load: ${url}`) } }) if (platform === 'crx') { // https://bugs.chromium.org/p/chromium/issues/detail?id=920638 $.on(r, 'load', () => { @@ -125,7 +125,7 @@ $.ajax = (function() { // XXX Some content blockers in Firefox (e.g. Adblock Plus and NoScript) throw an exception instead of simulating a connection error. if (err.result !== 0x805e0006) { throw err } r.onloadend = onloadend - $.queueTask($.event, 'error', null, r) + $.queueTask($.event, 'error', null, r) $.queueTask($.event, 'loadend', null, r) } return r @@ -138,13 +138,13 @@ $.ajax = (function() { let requestID = 0 const requests = dict() - $.ajaxPageInit = function() { - $.global(function() { + $.ajaxPageInit = function () { + $.global(function () { window.FCX.requests = Object.create(null) - document.addEventListener('4chanXAjax', function(e) { + document.addEventListener('4chanXAjax', function (e) { let fd, r - const {url, timeout, responseType, withCredentials, type, onprogress, form, headers, id} = e.detail + const { url, timeout, responseType, withCredentials, type, onprogress, form, headers, id } = e.detail window.FCX.requests[id] = (r = new XMLHttpRequest()) r.open(type, url, true) const object = headers || {} @@ -156,21 +156,21 @@ $.ajax = (function() { r.timeout = timeout r.withCredentials = withCredentials if (onprogress) { - r.upload.onprogress = function(e) { - const {loaded, total} = e - const detail = {loaded, total, id} - return document.dispatchEvent(new CustomEvent('4chanXAjaxProgress', {bubbles: true, detail})) + r.upload.onprogress = function (e) { + const { loaded, total } = e + const detail = { loaded, total, id } + return document.dispatchEvent(new CustomEvent('4chanXAjaxProgress', { bubbles: true, detail })) } } - r.onloadend = function() { + r.onloadend = function () { delete window.FCX.requests[id] - const {status, statusText, response} = this + const { status, statusText, response } = this const responseHeaderString = this.getAllResponseHeaders() - const detail = {status, statusText, response, responseHeaderString, id} - return document.dispatchEvent(new CustomEvent('4chanXAjaxLoadend', {bubbles: true, detail})) + const detail = { status, statusText, response, responseHeaderString, id } + return document.dispatchEvent(new CustomEvent('4chanXAjaxLoadend', { bubbles: true, detail })) } // connection error or content blocker - r.onerror = function() { + r.onerror = function () { if (!r.status) { return console.warn(`4chan X failed to load: ${url}`) } } if (form) { @@ -182,50 +182,50 @@ $.ajax = (function() { fd = null } return r.send(fd) - } - , false) - - return document.addEventListener('4chanXAjaxAbort', function(e) { - let r - if (!(r = window.FCX.requests[e.detail.id])) { return } - return r.abort() - } - , false) - }) - - $.on(d, '4chanXAjaxProgress', function(e) { - let req - if (!(req = requests[e.detail.id])) { return } - return req.upload.onprogress.call(req.upload, e.detail) - }) - - return $.on(d, '4chanXAjaxLoadend', function(e) { - let req - if (!(req = requests[e.detail.id])) { return } - delete requests[e.detail.id] - if (e.detail.status) { - for (const key of ['status', 'statusText', 'response', 'responseHeaderString']) { - req[key] = e.detail[key] } - if (req.responseType === 'document') { - req.response = new DOMParser().parseFromString(e.detail.response, 'text/html') - } - } - return req.onloadend() - }) - } + , false) - return $.ajaxPage = function(url, options={}) { + return document.addEventListener('4chanXAjaxAbort', function (e) { + let r + if (!(r = window.FCX.requests[e.detail.id])) { return } + return r.abort() + } + , false) + }) + + $.on(d, '4chanXAjaxProgress', function (e) { + let req + if (!(req = requests[e.detail.id])) { return } + return req.upload.onprogress.call(req.upload, e.detail) + }) + + return $.on(d, '4chanXAjaxLoadend', function (e) { + let req + if (!(req = requests[e.detail.id])) { return } + delete requests[e.detail.id] + if (e.detail.status) { + for (const key of ['status', 'statusText', 'response', 'responseHeaderString']) { + req[key] = e.detail[key] + } + if (req.responseType === 'document') { + req.response = new DOMParser().parseFromString(e.detail.response, 'text/html') + } + } + return req.onloadend() + }) + } + + return $.ajaxPage = function (url, options = {}) { let req - let {onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers} = options - const id = requestID++ - requests[id] = (req = new CrossOrigin.Request()) - $.extend(req, {responseType, onloadend}) - req.upload = {onprogress} - req.abort = () => $.event('4chanXAjaxAbort', {id}) - if (form) { form = Array.from(form.entries()) } - $.event('4chanXAjax', {url, timeout, responseType, withCredentials, type, onprogress: !!onprogress, form, headers, id}) - return req + let { onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers } = options + const id = requestID++ + requests[id] = (req = new CrossOrigin.Request()) + $.extend(req, { responseType, onloadend }) + req.upload = { onprogress } + req.abort = () => $.event('4chanXAjaxAbort', { id }) + if (form) { form = Array.from(form.entries()) } + $.event('4chanXAjax', { url, timeout, responseType, withCredentials, type, onprogress: !!onprogress, form, headers, id }) + return req } } })() @@ -234,9 +234,9 @@ $.ajax = (function() { // With the `If-Modified-Since` header we only receive the HTTP headers and no body for 304 responses. // This saves a lot of bandwidth and CPU time for both the users and the servers. $.lastModified = dict() -$.whenModified = function(url, bucket, cb, options={}) { +$.whenModified = function (url, bucket, cb, options = {}) { let t - const {timeout, ajax} = options + const { timeout, ajax } = options const params = [] // XXX https://bugs.chromium.org/p/chromium/issues/detail?id=643659 if ($.engine === 'blink') { params.push(`s=${bucket}`) } @@ -258,33 +258,33 @@ $.whenModified = function(url, bucket, cb, options={}) { return r }; -(function() { +(function () { const reqs = dict() - $.cache = function(url, cb, options={}) { + $.cache = function (url, cb, options = {}) { let req - const {ajax} = options + const { ajax } = options if (req = reqs[url]) { if (req.callbacks) { req.callbacks.push(cb) } else { - $.queueTask(() => cb.call(req, {isCached: true})) + $.queueTask(() => cb.call(req, { isCached: true })) } return req } - const onloadend = function() { + const onloadend = function () { if (!this.status) { delete reqs[url] } for (cb of this.callbacks) { - (cb => $.queueTask(() => cb.call(this, {isCached: false})))(cb) + (cb => $.queueTask(() => cb.call(this, { isCached: false })))(cb) } return delete this.callbacks } - req = (ajax || $.ajax)(url, {onloadend}) + req = (ajax || $.ajax)(url, { onloadend }) req.callbacks = [cb] return reqs[url] = req } - return $.cleanCache = function(testf) { + return $.cleanCache = function (testf) { for (const url in reqs) { if (testf(url)) { delete reqs[url] @@ -308,7 +308,7 @@ $.cb = { } } -$.asap = function(test, cb) { +$.asap = function (test, cb) { if (test()) { return cb() } else { @@ -316,32 +316,32 @@ $.asap = function(test, cb) { } } -$.onExists = function(root, selector, cb) { +$.onExists = function (root, selector, cb) { let el if (el = $(selector, root)) { return cb(el) } - var observer = new MutationObserver(function() { + var observer = new MutationObserver(function () { if (el = $(selector, root)) { observer.disconnect() return cb(el) } }) - return observer.observe(root, {childList: true, subtree: true}) + return observer.observe(root, { childList: true, subtree: true }) } -$.addStyle = function(css, id, test='head') { +$.addStyle = function (css, id, test = 'head') { const style = $.el('style', - {textContent: css}) + { textContent: css }) if (id != null) { style.id = id } $.onExists(doc, test, () => $.add(d.head, style)) return style } -$.addCSP = function(policy) { +$.addCSP = function (policy) { const meta = $.el('meta', { httpEquiv: 'Content-Security-Policy', - content: policy + content: policy } ) if (d.head) { @@ -354,23 +354,23 @@ $.addCSP = function(policy) { } } -$.x = function(path, root) { +$.x = function (path, root) { if (!root) { root = d.body } // XPathResult.ANY_UNORDERED_NODE_TYPE === 8 return d.evaluate(path, root, null, 8, null).singleNodeValue } -$.X = function(path, root) { +$.X = function (path, root) { if (!root) { root = d.body } // XPathResult.ORDERED_NODE_SNAPSHOT_TYPE === 7 return d.evaluate(path, root, null, 7, null) } -$.addClass = function(el, ...classNames) { +$.addClass = function (el, ...classNames) { for (const className of classNames) { el.classList.add(className) } } -$.rmClass = function(el, ...classNames) { +$.rmClass = function (el, ...classNames) { for (const className of classNames) { el.classList.remove(className) } } @@ -381,13 +381,13 @@ $.hasClass = (el, className) => el.classList.contains(className) $.rm = el => el?.remove() $.rmAll = root => // https://gist.github.com/MayhemYDG/8646194 -root.textContent = null + root.textContent = null $.tn = s => d.createTextNode(s) $.frag = () => d.createDocumentFragment() -$.nodes = function(nodes) { +$.nodes = function (nodes) { if (!(nodes instanceof Array)) { return nodes } @@ -408,55 +408,55 @@ $.before = (root, el) => root.parentNode.insertBefore($.nodes(el), root) $.replace = (root, el) => root.parentNode.replaceChild($.nodes(el), root) -$.el = function(tag, properties, properties2) { +$.el = function (tag, properties, properties2?) { const el = d.createElement(tag) if (properties) { $.extend(el, properties) } if (properties2) { $.extend(el, properties2) } return el } -$.on = function(el, events, handler) { +$.on = function (el, events, handler) { for (const event of events.split(' ')) { el.addEventListener(event, handler, false) } } -$.off = function(el, events, handler) { +$.off = function (el, events, handler) { for (const event of events.split(' ')) { el.removeEventListener(event, handler, false) } } -$.one = function(el, events, handler) { - const cb = function(e) { +$.one = function (el, events, handler) { + const cb = function (e) { $.off(el, events, cb) return handler.call(this, e) } return $.on(el, events, cb) } -$.event = function(event, detail, root=d) { +$.event = function (event, detail, root = d) { if (!globalThis.chrome?.extension) { if ((detail != null) && (typeof cloneInto === 'function')) { detail = cloneInto(detail, d.defaultView) } } - return root.dispatchEvent(new CustomEvent(event, {bubbles: true, cancelable: true, detail})) + return root.dispatchEvent(new CustomEvent(event, { bubbles: true, cancelable: true, detail })) } if (platform === 'userscript') { // XXX Make $.event work in Pale Moon with GM 3.x (no cloneInto function). - (function() { + (function () { if (!/PaleMoon\//.test(navigator.userAgent) || (+GM_info?.version?.split('.')[0] < 2) || (typeof cloneInto !== 'undefined')) { return } try { - return new CustomEvent('x', {detail: {}}) + return new CustomEvent('x', { detail: {} }) } catch (err) { const unsafeConstructors = { Object: unsafeWindow.Object, - Array: unsafeWindow.Array + Array: unsafeWindow.Array } - const clone = function(obj) { + const clone = function (obj) { let constructor if ((obj != null) && (typeof obj === 'object') && (constructor = unsafeConstructors[obj.constructor.name])) { const obj2 = new constructor() @@ -466,36 +466,36 @@ if (platform === 'userscript') { return obj } } - return $.event = (event, detail, root=d) => root.dispatchEvent(new CustomEvent(event, {bubbles: true, cancelable: true, detail: clone(detail)})) + return $.event = (event, detail, root = d) => root.dispatchEvent(new CustomEvent(event, { bubbles: true, cancelable: true, detail: clone(detail) })) } })() } $.modifiedClick = e => e.shiftKey || e.altKey || e.ctrlKey || e.metaKey || (e.button !== 0) - if (!globalThis.chrome?.extension) { - $.open = +if (!globalThis.chrome?.extension) { + $.open = (GM?.openInTab != null) ? GM.openInTab - : (typeof GM_openInTab !== 'undefined' && GM_openInTab !== null) ? - GM_openInTab - : + : (typeof GM_openInTab !== 'undefined' && GM_openInTab !== null) ? + GM_openInTab + : + url => window.open(url, '_blank') +} else { + $.open = url => window.open(url, '_blank') - } else { - $.open = - url => window.open(url, '_blank') - } +} -$.debounce = function(wait, fn) { +$.debounce = function (wait, fn) { let lastCall = 0 - let timeout = null - let that = null - let args = null - const exec = function() { + let timeout = null + let that = null + let args = null + const exec = function () { lastCall = Date.now() return fn.apply(that, args) } - return function() { + return function () { args = arguments that = this if (lastCall < (Date.now() - wait)) { @@ -508,10 +508,10 @@ $.debounce = function(wait, fn) { } } -$.queueTask = (function() { +$.queueTask = (function () { // inspired by https://www.w3.org/Bugs/Public/show_bug.cgi?id=15007 const taskQueue = [] - const execTask = function() { + const execTask = function () { const task = taskQueue.shift() const func = task[0] const args = Array.prototype.slice.call(task, 1) @@ -520,22 +520,22 @@ $.queueTask = (function() { if (window.MessageChannel) { const taskChannel = new MessageChannel() taskChannel.port1.onmessage = execTask - return function() { + return function () { taskQueue.push(arguments) return taskChannel.port2.postMessage(null) } } else { // XXX Firefox - return function() { + return function () { taskQueue.push(arguments) return setTimeout(execTask, 0) } } })() -$.global = function(fn, data) { +$.global = function (fn, data) { if (doc) { const script = $.el('script', - {textContent: `(${fn}).call(document.currentScript.dataset);`}) + { textContent: `(${fn}).call(document.currentScript.dataset);` }) if (data) { $.extend(script.dataset, data) } $.add((d.head || doc), script) $.rm(script) @@ -544,12 +544,12 @@ $.global = function(fn, data) { // XXX dwb try { fn.call(data) - } catch (error) {} + } catch (error) { } return data } } -$.bytesToString = function(size) { +$.bytesToString = function (size) { let unit = 0 // Bytes while (size >= 1024) { size /= 1024 @@ -561,7 +561,7 @@ $.bytesToString = function(size) { // Keep the size as a float if the size is greater than 2^20 B. // Round to hundredth. Math.round(size * 100) / 100 - : + : // Round to an integer otherwise. Math.round(size) return `${size} ${['B', 'KB', 'MB', 'GB'][unit]}` @@ -569,32 +569,32 @@ $.bytesToString = function(size) { $.minmax = (value, min, max) => value < min ? min -: + : value > max ? max - : + : value $.hasAudio = video => video.mozHasAudio || !!video.webkitAudioDecodedByteCount $.luma = rgb => (rgb[0] * 0.299) + (rgb[1] * 0.587) + (rgb[2] * 0.114) -$.unescape = function(text) { +$.unescape = function (text) { if (text == null) { return text } - return text.replace(/<[^>]*>/g, '').replace(/&(amp|#039|quot|lt|gt|#44);/g, c => ({'&': '&', ''': "'", '"': '"', '<': '<', '>': '>', ',': ','})[c]) + return text.replace(/<[^>]*>/g, '').replace(/&(amp|#039|quot|lt|gt|#44);/g, c => ({ '&': '&', ''': "'", '"': '"', '<': '<', '>': '>', ',': ',' })[c]) } $.isImage = url => /\.(jpe?g|jfif|png|gif|bmp|webp|avif|jxl)$/i.test(url) $.isVideo = url => /\.(webm|mp4|ogv)$/i.test(url) -$.engine = (function() { +$.engine = (function () { if (/Edge\//.test(navigator.userAgent)) { return 'edge' } if (/Chrome\//.test(navigator.userAgent)) { return 'blink' } if (/WebKit\//.test(navigator.userAgent)) { return 'webkit' } if (/Gecko\/|Goanna/.test(navigator.userAgent)) { return 'gecko' } // Goanna = Pale Moon 26+ })() -$.hasStorage = (function() { +$.hasStorage = (function () { try { if (localStorage.getItem(g.NAMESPACE + 'hasStorage') === 'true') { return true } localStorage.setItem(g.NAMESPACE + 'hasStorage', 'true') @@ -604,13 +604,13 @@ $.hasStorage = (function() { } })() -$.item = function(key, val) { +$.item = function (key, val) { const item = dict() item[key] = val return item } -$.oneItemSugar = fn => (function(key, val, cb) { +$.oneItemSugar = fn => (function (key, val, cb) { if (typeof key === 'string') { return fn($.item(key, val), cb) } else { @@ -620,7 +620,7 @@ $.oneItemSugar = fn => (function(key, val, cb) { $.syncing = dict() -$.securityCheck = function(data) { +$.securityCheck = function (data) { if (location.protocol !== 'https:') { return delete data['Redirect to HTTPS'] } @@ -630,10 +630,10 @@ if (platform === 'crx') { // https://developer.chrome.com/extensions/storage.html $.oldValue = { local: dict(), - sync: dict() + sync: dict() } - chrome.storage.onChanged.addListener(function(changes, area) { + chrome.storage.onChanged.addListener(function (changes, area) { for (const key in changes) { const oldValue = $.oldValue.local[key] ?? $.oldValue.sync[key] $.oldValue[area][key] = dict.clone(changes[key].newValue) @@ -645,17 +645,17 @@ if (platform === 'crx') { } }) $.sync = (key, cb) => $.syncing[key] = cb - $.forceSync = function() { } + $.forceSync = function () { } - $.crxWorking = function() { + $.crxWorking = function () { try { if (chrome.runtime.getManifest()) { return true } - } catch (error) {} + } catch (error) { } if (!$.crxWarningShown) { const msg = $.el('div', - {innerHTML: '4chan X seems to have been updated. You will need to reload the page.'}) + { innerHTML: '4chan X seems to have been updated. You will need to reload the page.' }) $.on($('a', msg), 'click', () => location.reload()) new Notice('warning', msg) $.crxWarningShown = true @@ -663,16 +663,16 @@ if (platform === 'crx') { return false } - $.get = $.oneItemSugar(function(data, cb) { + $.get = $.oneItemSugar(function (data, cb) { if (!$.crxWorking()) { return } const results = {} - const get = function(area) { + const get = function (area) { let keys = Object.keys(data) // XXX slow performance in Firefox if (($.engine === 'gecko') && (area === 'sync') && (keys.length > 3)) { keys = null } - return chrome.storage[area].get(keys, function(result) { + return chrome.storage[area].get(keys, function (result) { let key result = dict.clone(result) if (chrome.runtime.lastError) { @@ -698,16 +698,16 @@ if (platform === 'crx') { return get('sync') }); - (function() { + (function () { const items = { local: dict(), - sync: dict() + sync: dict() } const exceedsQuota = (key, value) => // bytes in UTF-8 - unescape(encodeURIComponent(JSON.stringify(key))).length + unescape(encodeURIComponent(JSON.stringify(value))).length > chrome.storage.sync.QUOTA_BYTES_PER_ITEM + unescape(encodeURIComponent(JSON.stringify(key))).length + unescape(encodeURIComponent(JSON.stringify(value))).length > chrome.storage.sync.QUOTA_BYTES_PER_ITEM - $.delete = function(keys) { + $.delete = function (keys) { if (!$.crxWorking()) { return } if (typeof keys === 'string') { keys = [keys] @@ -721,11 +721,11 @@ if (platform === 'crx') { } const timeout = {} - const setArea = function(area, cb) { + const setArea = function (area, cb) { const data = dict() $.extend(data, items[area]) if (!Object.keys(data).length || (timeout[area] > Date.now())) { return } - return chrome.storage[area].set(data, function() { + return chrome.storage[area].set(data, function () { let err let key if (err = chrome.runtime.lastError) { @@ -757,20 +757,20 @@ if (platform === 'crx') { var setSync = debounce(SECOND, () => setArea('sync')) - $.set = $.oneItemSugar(function(data, cb) { + $.set = $.oneItemSugar(function (data, cb) { if (!$.crxWorking()) { return } $.securityCheck(data) $.extend(items.local, data) return setArea('local', cb) }) - return $.clear = function(cb) { + return $.clear = function (cb) { if (!$.crxWorking()) { return } items.local = dict() - items.sync = dict() + items.sync = dict() let count = 2 - let err = null - const done = function() { + let err = null + const done = function () { if (chrome.runtime.lastError) { c.error(chrome.runtime.lastError.message) } @@ -804,19 +804,20 @@ if (platform === 'crx') { $.sync = (key, cb) => $.syncing[key] = cb - $.forceSync = function() {} + $.forceSync = function () { } - $.delete = function(keys, cb) { + $.delete = function (keys, cb) { let key if (!(keys instanceof Array)) { keys = [keys] } return Promise.all((() => { const result = [] - for (key of keys) { result.push(GM.deleteValue(g.NAMESPACE + key)) + for (key of keys) { + result.push(GM.deleteValue(g.NAMESPACE + key)) } return result - })()).then(function() { + })()).then(function () { const items = dict() for (key of keys) { items[key] = undefined } $.syncChannel.postMessage(items) @@ -824,9 +825,9 @@ if (platform === 'crx') { }) } - $.get = $.oneItemSugar(function(items, cb) { + $.get = $.oneItemSugar(function (items, cb) { const keys = Object.keys(items) - return Promise.all(keys.map((key) => GM.getValue(g.NAMESPACE + key))).then(function(values) { + return Promise.all(keys.map((key) => GM.getValue(g.NAMESPACE + key))).then(function (values) { for (let i = 0; i < values.length; i++) { const val = values[i] if (val) { @@ -837,7 +838,7 @@ if (platform === 'crx') { }) }) - $.set = $.oneItemSugar(function(items, cb) { + $.set = $.oneItemSugar(function (items, cb) { $.securityCheck(items) return Promise.all((() => { const result = [] @@ -846,13 +847,13 @@ if (platform === 'crx') { result.push(GM.setValue(g.NAMESPACE + key, JSON.stringify(val))) } return result - })()).then(function() { + })()).then(function () { $.syncChannel.postMessage(items) return cb?.() }) }) - $.clear = cb => GM.listValues().then(keys => $.delete(keys.map(key => key.replace(g.NAMESPACE, '')), cb)).catch( () => $.delete(Object.keys(Conf).concat(['previousversion', 'QR Size', 'QR.persona']), cb)) + $.clear = cb => GM.listValues().then(keys => $.delete(keys.map(key => key.replace(g.NAMESPACE, '')), cb)).catch(() => $.delete(Object.keys(Conf).concat(['previousversion', 'QR Size', 'QR.persona']), cb)) } else { if (typeof GM_deleteValue === 'undefined' || GM_deleteValue === null) { @@ -860,7 +861,7 @@ if (platform === 'crx') { } if (typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) { - $.getValue = GM_getValue + $.getValue = GM_getValue $.listValues = () => GM_listValues() // error when called if missing } else if ($.hasStorage) { $.getValue = key => localStorage.getItem(key) @@ -874,23 +875,23 @@ if (platform === 'crx') { return result })() } else { - $.getValue = function() {} + $.getValue = function () { } $.listValues = () => [] } if (typeof GM_addValueChangeListener !== 'undefined' && GM_addValueChangeListener !== null) { - $.setValue = GM_setValue + $.setValue = GM_setValue $.deleteValue = GM_deleteValue } else if (typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) { $.oldValue = dict() - $.setValue = function(key, val) { + $.setValue = function (key, val) { GM_setValue(key, val) if (key in $.syncing) { - $.oldValue[key] = val + $.oldValue[key] = val if ($.hasStorage) { return localStorage.setItem(key, val) } // for `storage` events } } - $.deleteValue = function(key) { + $.deleteValue = function (key) { GM_deleteValue(key) if (key in $.syncing) { delete $.oldValue[key] @@ -900,37 +901,37 @@ if (platform === 'crx') { if (!$.hasStorage) { $.cantSync = true } } else if ($.hasStorage) { $.oldValue = dict() - $.setValue = function(key, val) { - if (key in $.syncing) { $.oldValue[key] = val } + $.setValue = function (key, val) { + if (key in $.syncing) { $.oldValue[key] = val } return localStorage.setItem(key, val) } - $.deleteValue = function(key) { + $.deleteValue = function (key) { if (key in $.syncing) { delete $.oldValue[key] } return localStorage.removeItem(key) } } else { - $.setValue = function() {} - $.deleteValue = function() {} + $.setValue = function () { } + $.deleteValue = function () { } $.cantSync = ($.cantSet = true) } if (typeof GM_addValueChangeListener !== 'undefined' && GM_addValueChangeListener !== null) { - $.sync = (key, cb) => $.syncing[key] = GM_addValueChangeListener(g.NAMESPACE + key, function(key2, oldValue, newValue, remote) { + $.sync = (key, cb) => $.syncing[key] = GM_addValueChangeListener(g.NAMESPACE + key, function (key2, oldValue, newValue, remote) { if (remote) { if (newValue !== undefined) { newValue = dict.json(newValue) } return cb(newValue, key) } }) - $.forceSync = function() {} + $.forceSync = function () { } } else if ((typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) || $.hasStorage) { - $.sync = function(key, cb) { + $.sync = function (key, cb) { key = g.NAMESPACE + key $.syncing[key] = cb return $.oldValue[key] = $.getValue(key) }; - (function() { - const onChange = function({key, newValue}) { + (function () { + const onChange = function ({ key, newValue }) { let cb if (!(cb = $.syncing[key])) { return } if (newValue != null) { @@ -945,20 +946,20 @@ if (platform === 'crx') { } $.on(window, 'storage', onChange) - return $.forceSync = function(key) { + return $.forceSync = function (key) { // Storage events don't work across origins // e.g. http://boards.4chan.org and https://boards.4chan.org // so force a check for changes to avoid lost data. key = g.NAMESPACE + key - return onChange({key, newValue: $.getValue(key)}) + return onChange({ key, newValue: $.getValue(key) }) } })() } else { - $.sync = function() {} - $.forceSync = function() {} + $.sync = function () { } + $.forceSync = function () { } } - $.delete = function(keys) { + $.delete = function (keys) { if (!(keys instanceof Array)) { keys = [keys] } @@ -969,7 +970,7 @@ if (platform === 'crx') { $.get = $.oneItemSugar((items, cb) => $.queueTask($.getSync, items, cb)) - $.getSync = function(items, cb) { + $.getSync = function (items, cb) { for (const key in items) { var val2 if (val2 = $.getValue(g.NAMESPACE + key)) { @@ -986,9 +987,9 @@ if (platform === 'crx') { return cb(items) } - $.set = $.oneItemSugar(function(items, cb) { + $.set = $.oneItemSugar(function (items, cb) { $.securityCheck(items) - return $.queueTask(function() { + return $.queueTask(function () { for (const key in items) { const value = items[key] $.setValue(g.NAMESPACE + key, JSON.stringify(value)) @@ -997,14 +998,14 @@ if (platform === 'crx') { }) }) - $.clear = function(cb) { + $.clear = function (cb) { // XXX https://github.com/greasemonkey/greasemonkey/issues/2033 // Also support case where GM_listValues is not defined. $.delete(Object.keys(Conf)) $.delete(['previousversion', 'QR Size', 'QR.persona']) try { $.delete($.listValues().map(key => key.replace(g.NAMESPACE, ''))) - } catch (error) {} + } catch (error) { } return cb?.() } } diff --git a/src/site/SW.tinyboard.ts b/src/site/SW.tinyboard.ts index a805d02..07e7ebf 100644 --- a/src/site/SW.tinyboard.ts +++ b/src/site/SW.tinyboard.ts @@ -50,7 +50,7 @@ const SWTinyboard = { } else if (/^https?:/.test(root)) { properties.root = root } - } catch (error) {} + } catch (error) { } return properties } } @@ -61,7 +61,7 @@ const SWTinyboard = { let reactUI if (reactUI = $.id('react-ui')) { const s = (this.selectors = Object.create(this.selectors)) - s.boardFor = {index: '.page-container'} + s.boardFor = { index: '.page-container' } s.thread = 'div[id^="thread_"]' return Main.mounted(cb) } else { @@ -70,32 +70,32 @@ const SWTinyboard = { }, urls: { - thread({siteID, boardID, threadID}, isArchived) { + thread({ siteID, boardID, threadID }, isArchived) { return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/${isArchived ? 'archive/' : ''}res/${threadID}.html` }, - post({postID}) { return `#${postID}` }, - index({siteID, boardID}) { return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/` }, - catalog({siteID, boardID}) { return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/catalog.html` }, - threadJSON({siteID, boardID, threadID}, isArchived) { + post({ postID }) { return `#${postID}` }, + index({ siteID, boardID }) { return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/` }, + catalog({ siteID, boardID }) { return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/catalog.html` }, + threadJSON({ siteID, boardID, threadID }, isArchived) { const root = Conf['siteProperties'][siteID]?.root if (root) { return `${root}${boardID}/${isArchived ? 'archive/' : ''}res/${threadID}.json` } else { return '' } }, archivedThreadJSON(thread) { return SWTinyboard.urls.threadJSON(thread, true) }, - threadsListJSON({siteID, boardID}) { + threadsListJSON({ siteID, boardID }) { const root = Conf['siteProperties'][siteID]?.root if (root) { return `${root}${boardID}/threads.json` } else { return '' } }, - archiveListJSON({siteID, boardID}) { + archiveListJSON({ siteID, boardID }) { const root = Conf['siteProperties'][siteID]?.root if (root) { return `${root}${boardID}/archive/archive.json` } else { return '' } }, - catalogJSON({siteID, boardID}) { + catalogJSON({ siteID, boardID }) { const root = Conf['siteProperties'][siteID]?.root if (root) { return `${root}${boardID}/catalog.json` } else { return '' } }, - file({siteID, boardID}, filename) { + file({ siteID, boardID }, filename) { return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/${filename}` }, thumb(board, filename) { @@ -104,55 +104,55 @@ const SWTinyboard = { }, selectors: { - board: 'form[name="postcontrols"]', - thread: 'input[name="board"] ~ div[id^="thread_"]', + board: 'form[name="postcontrols"]', + thread: 'input[name="board"] ~ div[id^="thread_"]', threadDivider: 'div[id^="thread_"] > hr:last-child', - summary: '.omitted', + summary: '.omitted', postContainer: 'div[id^="reply_"]:not(.hidden)', // postContainer is thread for OP - opBottom: '.op', + opBottom: '.op', replyOriginal: 'div[id^="reply_"]:not(.hidden)', - infoRoot: '.intro', + infoRoot: '.intro', info: { - subject: '.subject', - name: '.name', - email: '.email', - tripcode: '.trip', - uniqueID: '.poster_id', - capcode: '.capcode', - flag: '.flag', - date: 'time', + subject: '.subject', + name: '.name', + email: '.email', + tripcode: '.trip', + uniqueID: '.poster_id', + capcode: '.capcode', + flag: '.flag', + date: 'time', nameBlock: 'label', - quote: 'a[href*="#q"]', - reply: 'a[href*="/res/"]:not([href*="#"])' + quote: 'a[href*="#q"]', + reply: 'a[href*="/res/"]:not([href*="#"])' }, icons: { - isSticky: '.fa-thumb-tack', - isClosed: '.fa-lock' + isSticky: '.fa-thumb-tack', + isClosed: '.fa-lock' }, file: { - text: '.fileinfo', - link: '.fileinfo > a', + text: '.fileinfo', + link: '.fileinfo > a', thumb: 'a > .post-image' }, thumbLink: '.file > a', multifile: '.files > .file', highlightable: { - op: ' > .op', - reply: '.reply', + op: ' > .op', + reply: '.reply', catalog: ' > .thread' }, - comment: '.body', - spoiler: '.spoiler', + comment: '.body', + spoiler: '.spoiler', quotelink: 'a[onclick*="highlightReply("]', catalog: { - board: '#Grid', + board: '#Grid', thread: '.mix', - thumb: '.thread-image' + thumb: '.thread-image' }, boardList: '.boardlist', boardListBottom: '.boardlist.bottom', styleSheet: '#stylesheet', - psa: '.blotter', + psa: '.blotter', nav: { prev: '.pages > form > [value=Previous]', next: '.pages > form > [value=Next]' @@ -164,8 +164,8 @@ const SWTinyboard = { }, xpath: { - thread: 'div[starts-with(@id,"thread_")]', - postContainer: 'div[starts-with(@id,"reply_") or starts-with(@id,"thread_")]', + thread: 'div[starts-with(@id,"thread_")]', + postContainer: 'div[starts-with(@id,"reply_") or starts-with(@id,"thread_")]', replyContainer: 'div[starts-with(@id,"reply_")]' }, @@ -222,7 +222,7 @@ $\ }, bgColoredEl() { - return $.el('div', {className: 'post reply'}) + return $.el('div', { className: 'post reply' }) }, isFileURL(url) { @@ -250,10 +250,10 @@ $\ if (m = text.match(/(\s*ID:\s*)(\S+)/)) { let uniqueID nodes.info.normalize() - let {nextSibling} = nodes.nameBlock + let { nextSibling } = nodes.nameBlock nextSibling = nextSibling.splitText(m[1].length) nextSibling.splitText(m[2].length) - nodes.uniqueID = (uniqueID = $.el('span', {className: 'poster_id'})) + nodes.uniqueID = (uniqueID = $.el('span', { className: 'poster_id' })) $.replace(nextSibling, uniqueID) return $.add(uniqueID, nextSibling) } @@ -269,19 +269,19 @@ $\ parseFile(post, file) { let info, infoNode - const {text, link, thumb} = file + const { text, link, thumb } = file if ($.x(`ancestor::${this.xpath.postContainer}[1]`, text) !== post.nodes.root) { return false } // file belongs to a reply if (!(infoNode = link.nextSibling?.textContent.includes('(') ? link.nextSibling : link.nextElementSibling)) { return false } if (!(info = infoNode.textContent.match(/\((.*,\s*)?([\d.]+ ?[KMG]?B).*\)/))) { return false } const nameNode = $('.postfilename', text) $.extend(file, { - name: nameNode ? (nameNode.title || nameNode.textContent) : link.pathname.match(/[^/]*$/)[0], - size: info[2], + name: nameNode ? (nameNode.title || nameNode.textContent) : link.pathname.match(/[^/]*$/)[0], + size: info[2], dimensions: info[0].match(/\d+x\d+/)?.[0] }) if (thumb) { $.extend(file, { - thumbURL: /\/static\//.test(thumb.src) && $.isImage(link.href) ? link.href : thumb.src, + thumbURL: /\/static\//.test(thumb.src) && $.isImage(link.href) ? link.href : thumb.src, isSpoiler: /^Spoiler/i.test(info[1] || '') || (link.textContent === 'Spoiler Image') } )