diff --git a/src/Quotelinks/QuoteBacklink.js b/src/Quotelinks/QuoteBacklink.js
index 213cb5c905..a0fe8b646d 100644
--- a/src/Quotelinks/QuoteBacklink.js
+++ b/src/Quotelinks/QuoteBacklink.js
@@ -1,3 +1,11 @@
+import Callbacks from "../classes/Callbacks";
+import { g, Conf, doc } from "../globals/globals";
+import $ from "../platform/$";
+import { dict } from "../platform/helpers";
+import QuoteInline from "./QuoteInline";
+import QuotePreview from "./QuotePreview";
+import QuoteYou from "./QuoteYou";
+
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
@@ -15,7 +23,7 @@ var QuoteBacklink = {
// Second callback adds relevant containers into posts.
// This is is so that fetched posts can get their backlinks,
// and that as much backlinks are appended in the background as possible.
- containers: $.dict(),
+ containers: dict(),
init() {
if (!['index', 'thread'].includes(g.VIEW) || !Conf['Quote Backlinks']) { return; }
@@ -91,3 +99,4 @@ var QuoteBacklink = {
(this.containers[id] = $.el('span', {className: 'container'}));
}
};
+export default QuoteBacklink;
diff --git a/src/Quotelinks/QuoteCT.js b/src/Quotelinks/QuoteCT.js
index 2af14e5c47..00eaba5224 100644
--- a/src/Quotelinks/QuoteCT.js
+++ b/src/Quotelinks/QuoteCT.js
@@ -1,3 +1,9 @@
+import $ from "../platform/$";
+import Callbacks from "../classes/Callbacks";
+import ExpandComment from "../Miscellaneous/ExpandComment";
+import { g, Conf } from "../globals/globals";
+import Get from "../General/Get";
+
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
@@ -39,3 +45,4 @@ var QuoteCT = {
}
}
};
+export default QuoteCT;
diff --git a/src/Quotelinks/QuoteInline.js b/src/Quotelinks/QuoteInline.js
index 1d47fdc327..131f2d7e2b 100644
--- a/src/Quotelinks/QuoteInline.js
+++ b/src/Quotelinks/QuoteInline.js
@@ -1,3 +1,11 @@
+import Callbacks from "../classes/Callbacks";
+import Fetcher from "../classes/Fetcher";
+import Get from "../General/Get";
+import { g, Conf, doc } from "../globals/globals";
+import ExpandComment from "../Miscellaneous/ExpandComment";
+import Unread from "../Monitoring/Unread";
+import $ from "../platform/$";
+
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
@@ -143,3 +151,4 @@ var QuoteInline = {
}
}
};
+export default QuoteInline;
diff --git a/src/Quotelinks/QuoteOP.js b/src/Quotelinks/QuoteOP.js
index beb953a10a..5c986f8e64 100644
--- a/src/Quotelinks/QuoteOP.js
+++ b/src/Quotelinks/QuoteOP.js
@@ -1,3 +1,9 @@
+import Callbacks from "../classes/Callbacks";
+import Get from "../General/Get";
+import { g, Conf } from "../globals/globals";
+import ExpandComment from "../Miscellaneous/ExpandComment";
+import $ from "../platform/$";
+
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
@@ -52,3 +58,4 @@ var QuoteOP = {
}
}
};
+export default QuoteOP;
diff --git a/src/Quotelinks/QuotePreview.js b/src/Quotelinks/QuotePreview.js
index a73d822f1b..d00635c365 100644
--- a/src/Quotelinks/QuotePreview.js
+++ b/src/Quotelinks/QuotePreview.js
@@ -1,3 +1,12 @@
+import Callbacks from "../classes/Callbacks";
+import Fetcher from "../classes/Fetcher";
+import Get from "../General/Get";
+import Header from "../General/Header";
+import UI from "../General/UI";
+import { Conf, d, doc, g } from "../globals/globals";
+import ExpandComment from "../Miscellaneous/ExpandComment";
+import $ from "../platform/$";
+
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
@@ -84,3 +93,4 @@ var QuotePreview = {
}
}
};
+export default QuotePreview;
diff --git a/src/Quotelinks/QuoteStrikeThrough.js b/src/Quotelinks/QuoteStrikeThrough.js
index 2cb38efb48..ceabbef4df 100644
--- a/src/Quotelinks/QuoteStrikeThrough.js
+++ b/src/Quotelinks/QuoteStrikeThrough.js
@@ -1,3 +1,8 @@
+import Callbacks from "../classes/Callbacks";
+import Get from "../General/Get";
+import { g, Conf } from "../globals/globals";
+import $ from "../platform/$";
+
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
@@ -24,3 +29,4 @@ const QuoteStrikeThrough = {
}
}
};
+export default QuoteStrikeThrough;
diff --git a/src/Quotelinks/QuoteThreading.js b/src/Quotelinks/QuoteThreading.js
index b170da2597..4cf265fdc6 100644
--- a/src/Quotelinks/QuoteThreading.js
+++ b/src/Quotelinks/QuoteThreading.js
@@ -1,3 +1,12 @@
+import Callbacks from "../classes/Callbacks";
+import RandomAccessList from "../classes/RandomAccessList";
+import Header from "../General/Header";
+import { Conf, d, g } from "../globals/globals";
+import ReplyPruning from "../Monitoring/ReplyPruning";
+import Unread from "../Monitoring/Unread";
+import $ from "../platform/$";
+import { dict } from "../platform/helpers";
+
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
@@ -46,9 +55,9 @@ var QuoteThreading = {
});
},
- parent: $.dict(),
- children: $.dict(),
- inserted: $.dict(),
+ parent: dict(),
+ children: dict(),
+ inserted: dict(),
toggleThreading() {
return this.setThreadingState(!Conf['Thread Quotes']);
@@ -181,7 +190,7 @@ var QuoteThreading = {
} else {
const nodes = [];
Unread.order = new RandomAccessList();
- QuoteThreading.inserted = $.dict();
+ QuoteThreading.inserted = dict();
posts.forEach(function(post) {
if (post.isFetchedQuote) { return; }
Unread.order.push(post);
@@ -203,3 +212,4 @@ var QuoteThreading = {
return Unread.update();
}
};
+export default QuoteThreading;
diff --git a/src/Quotelinks/QuoteYou.js b/src/Quotelinks/QuoteYou.js
index f44807f2b3..b6d9b9a6fd 100644
--- a/src/Quotelinks/QuoteYou.js
+++ b/src/Quotelinks/QuoteYou.js
@@ -1,3 +1,15 @@
+import Callbacks from "../classes/Callbacks";
+import DataBoard from "../classes/DataBoard";
+import Notice from "../classes/Notice";
+import Get from "../General/Get";
+import Header from "../General/Header";
+import { Conf, d, doc, g } from "../globals/globals";
+import Menu from "../Menu/Menu";
+import ExpandComment from "../Miscellaneous/ExpandComment";
+import $ from "../platform/$";
+import $$ from "../platform/$$";
+import PostRedirect from "../Posting/PostRedirect";
+
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
@@ -164,3 +176,4 @@ var QuoteYou = {
}
}
};
+export default QuoteYou;
diff --git a/src/Quotelinks/Quotify.js b/src/Quotelinks/Quotify.js
index 4ed31e708f..6f11c740fa 100644
--- a/src/Quotelinks/Quotify.js
+++ b/src/Quotelinks/Quotify.js
@@ -1,3 +1,11 @@
+import Redirect from "../Archive/Redirect";
+import Callbacks from "../classes/Callbacks";
+import Post from "../classes/Post";
+import { g, Conf, doc } from "../globals/globals";
+import ExpandComment from "../Miscellaneous/ExpandComment";
+import $ from "../platform/$";
+import $$ from "../platform/$$";
+
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
@@ -138,3 +146,4 @@ var Quotify = {
return $.replace(deadlink, [...Array.from(deadlink.childNodes)]);
}
};
+export default Quotify;
diff --git a/src/classes/Board.js b/src/classes/Board.js
index 02dc8b1c1b..88e55a5cb3 100644
--- a/src/classes/Board.js
+++ b/src/classes/Board.js
@@ -1,9 +1,13 @@
+import BoardConfig from "../General/BoardConfig";
+import { d, g } from "../globals/globals";
+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
*/
-class Board {
+export default class Board {
toString() { return this.ID; }
constructor(ID) {
diff --git a/src/classes/Callbacks.js b/src/classes/Callbacks.js
index 143c794af3..24aed5edba 100644
--- a/src/classes/Callbacks.js
+++ b/src/classes/Callbacks.js
@@ -1,10 +1,12 @@
+import Main from "../main/Main";
+
/*
* 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
*/
-class Callbacks {
+export default class Callbacks {
static initClass() {
this.Post = new Callbacks('Post');
this.Thread = new Callbacks('Thread');
diff --git a/src/classes/CatalogThread.js b/src/classes/CatalogThread.js
index e6337a4a3d..510308280d 100644
--- a/src/classes/CatalogThread.js
+++ b/src/classes/CatalogThread.js
@@ -1,4 +1,6 @@
-class CatalogThread {
+import $ from "../platform/$";
+
+export default class CatalogThread {
toString() { return this.ID; }
constructor(root, thread) {
diff --git a/src/classes/CatalogThreadNative.js b/src/classes/CatalogThreadNative.js
index dfe097aaf6..93c8696fd4 100644
--- a/src/classes/CatalogThreadNative.js
+++ b/src/classes/CatalogThreadNative.js
@@ -1,4 +1,9 @@
-class CatalogThreadNative {
+import { g } from "../globals/globals";
+import $ from "../platform/$";
+import Board from "./Board";
+import Thread from "./Thread";
+
+export default class CatalogThreadNative {
toString() { return this.ID; }
constructor(root) {
diff --git a/src/classes/Connection.js b/src/classes/Connection.js
index 2ea41ba3d5..58a280e430 100644
--- a/src/classes/Connection.js
+++ b/src/classes/Connection.js
@@ -1,9 +1,12 @@
+import $ from "../platform/$";
+import { g } from "../globals/globals";
+
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
-class Connection {
+export default class Connection {
constructor(target, origin, cb={}) {
this.send = this.send.bind(this);
this.onMessage = this.onMessage.bind(this);
diff --git a/src/classes/DataBoard.js b/src/classes/DataBoard.js
index 03e9938a1f..b17a33b073 100644
--- a/src/classes/DataBoard.js
+++ b/src/classes/DataBoard.js
@@ -1,3 +1,7 @@
+import { Conf, d, g } from "../globals/globals";
+import $ from "../platform/$";
+import { dict, HOUR } from "../platform/helpers";
+
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
@@ -6,14 +10,13 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
-class DataBoard {
+export default class DataBoard {
static initClass() {
this.keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'watcherLastModified', 'customTitles'];
-
- this.prototype.changes = [];
}
constructor(key, sync, dontClean) {
+ this.changes = [];
this.onSync = this.onSync.bind(this);
this.key = key;
this.initData(Conf[this.key]);
@@ -39,13 +42,13 @@ class DataBoard {
delete this.data.boards;
delete this.data.lastChecked;
}
- return this.data[g.SITE.ID] || (this.data[g.SITE.ID] = {boards: $.dict()});
+ return this.data[g.SITE.ID] || (this.data[g.SITE.ID] = { boards: dict() });
}
save(change, cb) {
change();
this.changes.push(change);
- return $.get(this.key, {boards: $.dict()}, items => {
+ return $.get(this.key, { boards: dict() }, items => {
if (!this.changes.length) { return; }
const needSync = ((items[this.key].version || 0) > (this.data.version || 0));
if (needSync) {
@@ -62,7 +65,7 @@ class DataBoard {
}
forceSync(cb) {
- return $.get(this.key, {boards: $.dict()}, items => {
+ return $.get(this.key, { boards: dict() }, items => {
if ((items[this.key].version || 0) > (this.data.version || 0)) {
this.initData(items[this.key]);
for (var change of this.changes) { change(); }
@@ -112,12 +115,12 @@ class DataBoard {
setUnsafe({siteID, boardID, threadID, postID, val}) {
if (!siteID) { siteID = g.SITE.ID; }
- if (!this.data[siteID]) { this.data[siteID] = {boards: $.dict()}; }
+ if (!this.data[siteID]) { this.data[siteID] = { boards: dict() }; }
if (postID !== undefined) {
let base;
- return (((base = this.data[siteID].boards[boardID] || (this.data[siteID].boards[boardID] = $.dict())))[threadID] || (base[threadID] = $.dict()))[postID] = val;
+ return (((base = this.data[siteID].boards[boardID] || (this.data[siteID].boards[boardID] = dict())))[threadID] || (base[threadID] = dict()))[postID] = val;
} else if (threadID !== undefined) {
- return (this.data[siteID].boards[boardID] || (this.data[siteID].boards[boardID] = $.dict()))[threadID] = val;
+ return (this.data[siteID].boards[boardID] || (this.data[siteID].boards[boardID] = dict()))[threadID] = val;
} else {
return this.data[siteID].boards[boardID] = val;
}
@@ -125,7 +128,7 @@ class DataBoard {
extend({siteID, boardID, threadID, postID, val}, cb) {
return this.save(() => {
- const oldVal = this.get({siteID, boardID, threadID, postID, defaultValue: $.dict()});
+ const oldVal = this.get({ siteID, boardID, threadID, postID, defaultValue: dict() });
for (var key in val) {
var subVal = val[key];
if (typeof subVal === 'undefined') {
@@ -180,7 +183,7 @@ class DataBoard {
this.deleteIfEmpty({siteID, boardID});
}
const now = Date.now();
- if (now - (2 * $.HOUR) >= ((middle = this.data[siteID].lastChecked || 0)) || middle > now) {
+ if (now - (2 * HOUR) >= ((middle = this.data[siteID].lastChecked || 0)) || middle > now) {
this.data[siteID].lastChecked = now;
for (boardID in this.data[siteID].boards) {
this.ajaxClean(boardID);
@@ -209,7 +212,7 @@ class DataBoard {
let board, ID;
const siteID = g.SITE.ID;
if (!(board = this.data[siteID].boards[boardID])) { return; }
- const threads = $.dict();
+ const threads = dict();
if (response1) {
for (var page of response1) {
for (var thread of page.threads) {
diff --git a/src/classes/Fetcher.js b/src/classes/Fetcher.js
index 721547f243..2a1901f906 100644
--- a/src/classes/Fetcher.js
+++ b/src/classes/Fetcher.js
@@ -1,3 +1,17 @@
+import Redirect from "../Archive/Redirect";
+import Board from "./Board";
+import Post from "./Post";
+import Thread from "./Thread";
+import $ from "../platform/$";
+import Main from "../main/Main";
+import Index from "../General/Index";
+import { E, g, Conf, d } from "../globals/globals";
+import ImageHost from "../Images/ImageHost";
+import CrossOrigin from "../platform/CrossOrigin";
+import Get from "../General/Get";
+import { dict } from "../platform/helpers";
+import { isEscaped } from "../globals/jsx";
+
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
@@ -7,9 +21,9 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
-class Fetcher {
+export default class Fetcher {
static initClass() {
-
+
this.prototype.archiveTags = {
'\n': {innerHTML: "
"},
'[b]': {innerHTML: "
"},
@@ -79,7 +93,8 @@ class Fetcher {
$.add(nodes.root, nodes.post);
// Indicate links to the containing post.
- for (var quote of clone.nodes.quotelinks.concat([...Array.from(clone.nodes.backlinks)])) {
+ const quotes = [...clone.nodes.quotelinks, ...clone.nodes.backlinks];
+ for (var quote of quotes) {
var {boardID, postID} = Get.postDataFromLink(quote);
if ((postID === this.quoter.ID) && (boardID === this.quoter.board.ID)) {
$.addClass(quote, 'forwardlink');
@@ -218,20 +233,20 @@ class Fetcher {
for (let i = 0; i < comment.length; i++) {
var text = comment[i];
if ((i % 2) === 1) {
- var tag = this.archiveTags[text.replace(/\ .*\]/, ']')];
+ var tag = Fetcher.archiveTags[text.replace(/\ .*\]/, ']')];
if (typeof tag === 'function') { result.push(tag(text)); } else { result.push(tag); }
} else {
var greentext = text[0] === '>';
text = text.replace(/(\[\/?[a-z]+):lit(\])/g, '$1$2');
text = text.split(/(>>(?:>\/[a-z\d]+\/)?\d+)/g).map((text2, j) =>
- {innerHTML: ((j % 2) ? "" + E(text2) + " " : E(text2));});
+ ({innerHTML: ((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), [isEscaped]: true };
this.threadID = +data.thread_num;
const o = {
@@ -291,7 +306,7 @@ class Fetcher {
if (!/\.pdf$/.test(o.file.url)) { o.file.dimensions = `${o.file.width}x${o.file.height}`; }
if ((this.boardID === 'f') && data.media.exif) { o.file.tag = JSON.parse(data.media.exif).Tag; }
}
- o.extra = $.dict();
+ o.extra = dict();
const board = g.boards[this.boardID] ||
new Board(this.boardID);
diff --git a/src/classes/Notice.js b/src/classes/Notice.js
index 8290f1643d..7e1f212fd5 100644
--- a/src/classes/Notice.js
+++ b/src/classes/Notice.js
@@ -1,9 +1,14 @@
+import Header from "../General/Header";
+import { d } from "../globals/globals";
+import $ from "../platform/$";
+import { SECOND } from "../platform/helpers";
+
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
-class Notice {
+export default class Notice {
constructor(type, content, timeout, onclose) {
this.add = this.add.bind(this);
this.close = this.close.bind(this);
@@ -36,7 +41,7 @@ class Notice {
$.add(Header.noticesRoot, this.el);
this.el.clientHeight; // force reflow
this.el.style.opacity = 1;
- if (this.timeout) { return setTimeout(this.close, this.timeout * $.SECOND); }
+ if (this.timeout) { return setTimeout(this.close, this.timeout * SECOND); }
}
close() {
diff --git a/src/classes/Post.Clone.js b/src/classes/Post.Clone.js
deleted file mode 100644
index 9b78253789..0000000000
--- a/src/classes/Post.Clone.js
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * decaffeinate suggestions:
- * DS002: Fix invalid constructor
- * DS101: Remove unnecessary use of Array.from
- * 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
- */
-Post.Clone = (function() {
- const Cls = class extends Post {
- static initClass() {
- this.prototype.isClone = true;
- }
-
- constructor() {
- const that = Object.create(Post.Clone.prototype);
- that.construct(...arguments);
- return that;
- }
-
- construct(origin, context, contractThumb) {
- let file, fileRoots, key;
- this.origin = origin;
- this.context = context;
- for (key of ['ID', 'postID', 'threadID', 'boardID', 'siteID', 'fullID', 'board', 'thread', 'info', 'quotes', 'isReply']) {
- // Copy or point to the origin's key value.
- this[key] = this.origin[key];
- }
-
- const {nodes} = this.origin;
- const root = contractThumb ?
- this.cloneWithoutVideo(nodes.root)
- :
- nodes.root.cloneNode(true);
- if (!Post.Clone.suffix) { Post.Clone.suffix = 0; }
- for (var node of [root, ...Array.from($$('[id]', root))]) {
- node.id += `_${Post.Clone.suffix}`;
- }
- Post.Clone.suffix++;
-
- // Remove inlined posts inside of this post.
- for (var inline of $$('.inline', root)) {
- $.rm(inline);
- }
- for (var inlined of $$('.inlined', root)) {
- $.rmClass(inlined, 'inlined');
- }
-
- this.nodes = this.parseNodes(root);
-
- root.hidden = false; // post hiding
- $.rmClass(root, 'forwarded'); // quote inlining
- $.rmClass(this.nodes.post, 'highlight'); // keybind navigation, ID highlighting
-
- // Remove catalog stuff.
- if (!this.isReply) {
- this.setCatalogOP(false);
- $.rm($('.catalog-link', this.nodes.post));
- $.rm($('.catalog-stats', this.nodes.post));
- $.rm($('.catalog-replies', this.nodes.post));
- }
-
- this.parseQuotes();
- this.quotes = [...Array.from(this.origin.quotes)];
-
- this.files = [];
- if (this.origin.files.length) { fileRoots = this.fileRoots(); }
- for (var originFile of this.origin.files) {
- // Copy values, point to relevant elements.
- file = {};
- for (key in originFile) {
- var val = originFile[key];
- file[key] = val;
- }
- var fileRoot = fileRoots[file.docIndex];
- for (key in g.SITE.selectors.file) {
- var selector = g.SITE.selectors.file[key];
- file[key] = $(selector, fileRoot);
- }
- file.thumbLink = file.thumb?.parentNode;
- if (file.thumbLink) { file.fullImage = $('.full-image', file.thumbLink); }
- file.videoControls = $('.video-controls', file.text);
- if (file.videoThumb) { file.thumb.muted = true; }
- this.files.push(file);
- }
-
- if (this.files.length) {
- this.file = this.files[0];
-
- // Contract thumbnails in quote preview
- if (this.file.thumb && contractThumb) { ImageExpand.contract(this); }
- }
-
- if (this.origin.isDead) { this.isDead = true; }
- return root.dataset.clone = this.origin.clones.push(this) - 1;
- }
-
- cloneWithoutVideo(node) {
- if ((node.tagName === 'VIDEO') && !node.dataset.md5) { // (exception for WebM thumbnails)
- return [];
- } else if ((node.nodeType === Node.ELEMENT_NODE) && $('video', node)) {
- const clone = node.cloneNode(false);
- for (var child of node.childNodes) { $.add(clone, this.cloneWithoutVideo(child)); }
- return clone;
- } else {
- return node.cloneNode(true);
- }
- }
- };
- Cls.initClass();
- return Cls;
-})();
diff --git a/src/classes/Post.js b/src/classes/Post.js
index 1292990fb1..23269ee6cc 100644
--- a/src/classes/Post.js
+++ b/src/classes/Post.js
@@ -1,27 +1,40 @@
+import Get from "../General/Get";
+// #region tests_enabled
+import Test from "../General/Test";
+// #endregion
+import { g, Conf } from "../globals/globals";
+import ImageExpand from "../Images/ImageExpand";
+import $ from "../platform/$";
+import $$ from "../platform/$$";
+import Callbacks from "./Callbacks";
+
/*
* 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
*/
-class Post {
- static initClass() {
-
- this.deadMark =
- // \u00A0 is nbsp
- $.el('span', {
- textContent: '\u00A0(Dead)',
- className: 'qmark-dead'
- }
- );
- }
+export default class Post {
+ // because of a circular dependency $ might not be initialized, so we can't use $.el
+ static deadMark = (() => {
+ const el = document.createElement('span');
+ // \u00A0 is nbsp
+ el.textContent = '\u00A0(Dead)';
+ el.className = 'qmark-dead';
+ return el;
+ })();
+
toString() { return this.ID; }
constructor(root, thread, board, flags={}) {
- // <% if (readJSON('/.tests_enabled')) { %>
- // @normalizedOriginal = Test.normalize root
- // <% } %>
+ // #region tests_enabled
+ if (root) this.normalizedOriginal = Test.normalize(root);
+ // #endregion
+ // Skip initialization for PostClone
+ if (root === undefined && thread === undefined && board === undefined) return;
+
+ this.root = root;
this.thread = thread;
this.board = board;
$.extend(this, flags);
@@ -82,9 +95,9 @@ class Post {
this.isHidden = false;
this.clones = [];
- // <% if (readJSON('/.tests_enabled')) { %>
- // return if @forBuildTest
- // <% } %>
+ // #region tests_enabled
+ if (this.forBuildTest) return;
+ // #endregion
if (g.posts.get(this.fullID)) {
this.isRebuilt = true;
this.clones = g.posts.get(this.fullID).clones;
@@ -95,6 +108,9 @@ class Post {
this.board.posts.push(this.ID, this);
this.thread.posts.push(this.ID, this);
g.posts.push(this.fullID, this);
+
+ this.isFetchedQuote = false;
+ this.isClone = false;
}
parseNodes(root) {
@@ -337,7 +353,7 @@ class Post {
addClone(context, contractThumb) {
// Callbacks may not have been run yet due to anti-browser-lock delay in Main.callbackNodesDB.
Callbacks.Post.execute(this);
- return new Post.Clone(this, context, contractThumb);
+ return new PostClone(this, context, contractThumb);
}
rmClone(index) {
@@ -354,5 +370,93 @@ class Post {
this.nodes.post.classList.toggle('op', !isCatalogOP);
return this.nodes.post.style.left = (this.nodes.post.style.right = null);
}
-}
-Post.initClass();
+};
+
+export class PostClone extends Post {
+ static suffix = 0;
+
+ constructor(origin, context, contractThumb) {
+ super();
+ this.isClone = true;
+
+ let file, fileRoots, key;
+ this.origin = origin;
+ this.context = context;
+ for (key of ['ID', 'postID', 'threadID', 'boardID', 'siteID', 'fullID', 'board', 'thread', 'info', 'quotes', 'isReply']) {
+ // Copy or point to the origin's key value.
+ this[key] = this.origin[key];
+ }
+
+ const { nodes } = this.origin;
+ const root = contractThumb ? this.cloneWithoutVideo(nodes.root) : nodes.root.cloneNode(true);
+ for (var node of [root, ...$$('[id]', root)]) {
+ node.id += `_${PostClone.suffix}`;
+ }
+ PostClone.suffix++;
+
+ // Remove inlined posts inside of this post.
+ for (var inline of $$('.inline', root)) {
+ $.rm(inline);
+ }
+ for (var inlined of $$('.inlined', root)) {
+ $.rmClass(inlined, 'inlined');
+ }
+
+ this.nodes = this.parseNodes(root);
+
+ root.hidden = false; // post hiding
+ $.rmClass(root, 'forwarded'); // quote inlining
+ $.rmClass(this.nodes.post, 'highlight'); // keybind navigation, ID highlighting
+
+ // Remove catalog stuff.
+ if (!this.isReply) {
+ this.setCatalogOP(false);
+ $.rm($('.catalog-link', this.nodes.post));
+ $.rm($('.catalog-stats', this.nodes.post));
+ $.rm($('.catalog-replies', this.nodes.post));
+ }
+
+ this.parseQuotes();
+ this.quotes = [...this.origin.quotes];
+
+ this.files = [];
+ if (this.origin.files.length) { fileRoots = this.fileRoots(); }
+ for (var originFile of this.origin.files) {
+ // Copy values, point to relevant elements.
+ file = { ...originFile };
+ var fileRoot = fileRoots[file.docIndex];
+ for (key in g.SITE.selectors.file) {
+ var selector = g.SITE.selectors.file[key];
+ file[key] = $(selector, fileRoot);
+ }
+ file.thumbLink = file.thumb?.parentNode;
+ if (file.thumbLink) { file.fullImage = $('.full-image', file.thumbLink); }
+ file.videoControls = $('.video-controls', file.text);
+ if (file.videoThumb) { file.thumb.muted = true; }
+ this.files.push(file);
+ }
+
+ if (this.files.length) {
+ this.file = this.files[0];
+
+ // Contract thumbnails in quote preview
+ if (this.file.thumb && contractThumb) { ImageExpand.contract(this); }
+ }
+
+ if (this.origin.isDead) { this.isDead = true; }
+ root.dataset.clone = this.origin.clones.push(this) - 1;
+ return this;
+ }
+
+ cloneWithoutVideo(node) {
+ if ((node.tagName === 'VIDEO') && !node.dataset.md5) { // (exception for WebM thumbnails)
+ return [];
+ } else if ((node.nodeType === Node.ELEMENT_NODE) && $('video', node)) {
+ const clone = node.cloneNode(false);
+ for (var child of node.childNodes) { $.add(clone, this.cloneWithoutVideo(child)); }
+ return clone;
+ } else {
+ return node.cloneNode(true);
+ }
+ }
+};
diff --git a/src/classes/RandomAccessList.js b/src/classes/RandomAccessList.js
index 48c50e9c67..5a4dfa2e7a 100644
--- a/src/classes/RandomAccessList.js
+++ b/src/classes/RandomAccessList.js
@@ -3,7 +3,7 @@
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
-class RandomAccessList {
+export default class RandomAccessList {
constructor(items) {
this.length = 0;
if (items) { for (var item of items) { this.push(item); } }
diff --git a/src/classes/SimpleDict.js b/src/classes/SimpleDict.ts
similarity index 85%
rename from src/classes/SimpleDict.js
rename to src/classes/SimpleDict.ts
index 6e0b9461ed..7ab5ca9020 100644
--- a/src/classes/SimpleDict.js
+++ b/src/classes/SimpleDict.ts
@@ -1,15 +1,19 @@
+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
*/
-class SimpleDict {
+export default class SimpleDict {
+ keys: string[]
+
constructor() {
this.keys = [];
}
- push(key, data) {
+ push(key, data: T) {
key = `${key}`;
if (!this[key]) { this.keys.push(key); }
return this[key] = data;
@@ -28,7 +32,7 @@ class SimpleDict {
for (var key of [...Array.from(this.keys)]) { fn(this[key]); }
}
- get(key) {
+ get(key): T {
if (key === 'keys') {
return undefined;
} else {
diff --git a/src/classes/Thread.js b/src/classes/Thread.js
index 423f036428..2b340d5c22 100644
--- a/src/classes/Thread.js
+++ b/src/classes/Thread.js
@@ -1,9 +1,13 @@
+import SimpleDict from "./SimpleDict";
+import $ from "../platform/$";
+import { g } from "../globals/globals";
+
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
-class Thread {
+export default class Thread {
toString() { return this.ID; }
constructor(ID, board) {
diff --git a/src/config/Config.js b/src/config/Config.js
index 374e365f88..7e98fde899 100644
--- a/src/config/Config.js
+++ b/src/config/Config.js
@@ -1,5 +1,6 @@
import userCss from './user.css';
-import banners from './banners.js';
+import banners from './banners.json';
+import meta from '../../package.json';
const Config = {
main: {
@@ -12,9 +13,9 @@ const Config = {
true,
'Replace the original board index with one supporting searching, sorting, infinite scrolling, and a catalog mode.'
],
- 'Use <%= meta.name %> Catalog': [
+ [`Use ${meta.name} Catalog`]: [
true,
- 'Link to <%= meta.name %>\'s catalog instead of the native 4chan one.',
+ `Link to ${meta.name}'s catalog instead of the native 4chan one.`,
1
],
'Index Refresh Notifications': [
@@ -28,7 +29,7 @@ const Config = {
],
'Open Threads in New Tab': [
false,
- 'Make links to threads in the index / <%= meta.name %> catalog open in a new tab.'
+ `Make links to threads in the index / ${meta.name} catalog open in a new tab.`
],
'External Catalog': [
false,
@@ -44,7 +45,7 @@ const Config = {
],
'Desktop Notifications': [
true,
- 'Enables desktop notifications across various <%= meta.name %> features.'
+ `Enables desktop notifications across various ${meta.name} features.`
],
'404 Redirect': [
true,
@@ -110,7 +111,7 @@ const Config = {
],
'Show Updated Notifications': [
true,
- 'Show notifications when <%= meta.name %> is successfully updated.'
+ `Show notifications when ${meta.name} is successfully updated.`
],
'Color User IDs': [
true,
@@ -142,7 +143,7 @@ const Config = {
],
'Disable Native Extension': [
true,
- '<%= meta.name %> is NOT designed to work with the native extension.'
+ `${meta.name} is NOT designed to work with the native extension.`
],
'Enable Native Flash Embedding': [
true,
@@ -235,7 +236,7 @@ const Config = {
],
'Image Hover in Catalog': [
true,
- 'Show full image / video on mouseover in <%= meta.name %> catalog.'
+ `Show full image / video on mouseover in ${meta.name} catalog.`
],
'Gallery': [
true,
@@ -322,7 +323,7 @@ const Config = {
],
'Volume in New Tab': [
true,
- 'Apply <%= meta.name %> mute and volume settings to videos opened in their own tabs.'
+ `Apply ${meta.name} mute and volume settings to videos opened in their own tabs.`
]
},
@@ -860,7 +861,7 @@ http://eye.swfchan.com/search/?q=%name;types:swf
current-catalog-text:"Catalog"
current-expired-text:"Expired"
current-archive-text:"Archive"]
-[external-text:"FAQ","<%= meta.faq %>"]\
+[external-text:"FAQ","${meta.name}"]\
`,
QR: {
@@ -870,21 +871,7 @@ current-archive-text:"Archive"]
sjisPreview: false
},
- jsWhitelist: `\
-http://s.4cdn.org
-https://s.4cdn.org
-http://www.google.com
-https://www.google.com
-https://www.gstatic.com
-http://cdn.mathjax.org
-https://cdn.mathjax.org
-https://cdnjs.cloudflare.com
-https://hcaptcha.com
-https://*.hcaptcha.com
-'self'
-'unsafe-inline'
-'unsafe-eval'\
-`,
+ jsWhitelist: '',
captchaLanguage: '',
@@ -1201,3 +1188,4 @@ https://*.hcaptcha.com
'PSAseen': [[]]
};
+export default Config;
diff --git a/src/config/banners.js b/src/config/banners.js
deleted file mode 100644
index 9cf65d7426..0000000000
--- a/src/config/banners.js
+++ /dev/null
@@ -1,2 +0,0 @@
-const banners = ["0.jpg", "1.jpg", "2.jpg", "4.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg", "12.jpg", "13.jpg", "14.jpg", "16.jpg", "17.jpg", "18.jpg", "19.jpg", "20.jpg", "21.jpg", "22.jpg", "24.jpg", "25.jpg", "26.jpg", "28.jpg", "29.jpg", "33.jpg", "38.jpg", "39.jpg", "43.jpg", "44.jpg", "45.jpg", "46.jpg", "47.jpg", "52.jpg", "54.jpg", "57.jpg", "59.jpg", "60.jpg", "61.jpg", "64.jpg", "66.jpg", "67.jpg", "69.jpg", "71.jpg", "72.jpg", "76.jpg", "77.jpg", "81.jpg", "82.jpg", "83.jpg", "84.jpg", "88.jpg", "90.jpg", "91.jpg", "96.jpg", "98.jpg", "99.jpg", "100.jpg", "104.jpg", "106.jpg", "116.jpg", "119.jpg", "137.jpg", "140.jpg", "148.jpg", "149.jpg", "150.jpg", "154.jpg", "156.jpg", "157.jpg", "158.jpg", "159.jpg", "161.jpg", "162.jpg", "164.jpg", "165.jpg", "166.jpg", "167.jpg", "168.jpg", "169.jpg", "170.jpg", "171.jpg", "172.jpg", "173.jpg", "174.jpg", "175.jpg", "176.jpg", "178.jpg", "179.jpg", "180.jpg", "181.jpg", "182.jpg", "183.jpg", "186.jpg", "189.jpg", "190.jpg", "192.jpg", "193.jpg", "194.jpg", "197.jpg", "198.jpg", "200.jpg", "201.jpg", "202.jpg", "203.jpg", "205.jpg", "206.jpg", "207.jpg", "208.jpg", "210.jpg", "213.jpg", "214.jpg", "215.jpg", "216.jpg", "218.jpg", "219.jpg", "220.jpg", "221.jpg", "222.jpg", "223.jpg", "224.jpg", "227.jpg", "0.png", "1.png", "2.png", "3.png", "5.png", "6.png", "9.png", "10.png", "11.png", "12.png", "14.png", "16.png", "19.png", "20.png", "21.png", "22.png", "23.png", "24.png", "26.png", "27.png", "28.png", "29.png", "30.png", "31.png", "32.png", "33.png", "34.png", "37.png", "39.png", "40.png", "41.png", "42.png", "43.png", "44.png", "45.png", "48.png", "49.png", "50.png", "51.png", "52.png", "53.png", "57.png", "58.png", "59.png", "64.png", "66.png", "67.png", "68.png", "69.png", "70.png", "71.png", "72.png", "76.png", "78.png", "79.png", "81.png", "82.png", "85.png", "86.png", "87.png", "89.png", "95.png", "98.png", "100.png", "101.png", "102.png", "105.png", "106.png", "107.png", "109.png", "110.png", "111.png", "112.png", "113.png", "114.png", "115.png", "116.png", "118.png", "119.png", "120.png", "121.png", "122.png", "123.png", "126.png", "128.png", "130.png", "134.png", "136.png", "138.png", "139.png", "140.png", "142.png", "145.png", "146.png", "149.png", "150.png", "151.png", "152.png", "153.png", "154.png", "155.png", "156.png", "157.png", "158.png", "159.png", "160.png", "163.png", "164.png", "165.png", "166.png", "167.png", "168.png", "169.png", "170.png", "171.png", "172.png", "173.png", "174.png", "178.png", "179.png", "180.png", "181.png", "182.png", "184.png", "186.png", "188.png", "190.png", "192.png", "193.png", "194.png", "195.png", "196.png", "197.png", "198.png", "200.png", "202.png", "203.png", "205.png", "206.png", "207.png", "209.png", "212.png", "213.png", "214.png", "216.png", "217.png", "218.png", "219.png", "220.png", "221.png", "222.png", "223.png", "224.png", "225.png", "226.png", "229.png", "231.png", "232.png", "233.png", "234.png", "235.png", "237.png", "238.png", "239.png", "240.png", "241.png", "242.png", "244.png", "245.png", "246.png", "247.png", "248.png", "249.png", "250.png", "253.png", "254.png", "255.png", "256.png", "257.png", "258.png", "259.png", "260.png", "262.png", "268.png", "0.gif", "1.gif", "2.gif", "3.gif", "4.gif", "5.gif", "6.gif", "7.gif", "8.gif", "9.gif", "10.gif", "12.gif", "13.gif", "14.gif", "15.gif", "16.gif", "18.gif", "19.gif", "20.gif", "21.gif", "22.gif", "23.gif", "24.gif", "28.gif", "29.gif", "30.gif", "33.gif", "34.gif", "35.gif", "36.gif", "37.gif", "39.gif", "40.gif", "42.gif", "44.gif", "45.gif", "46.gif", "48.gif", "50.gif", "52.gif", "54.gif", "55.gif", "57.gif", "58.gif", "59.gif", "60.gif", "61.gif", "63.gif", "64.gif", "66.gif", "67.gif", "68.gif", "69.gif", "70.gif", "72.gif", "73.gif", "75.gif", "76.gif", "77.gif", "78.gif", "80.gif", "81.gif", "82.gif", "83.gif", "86.gif", "87.gif", "88.gif", "92.gif", "93.gif", "94.gif", "95.gif", "96.gif", "97.gif", "98.gif", "99.gif", "100.gif", "101.gif", "102.gif", "103.gif", "104.gif", "105.gif", "106.gif", "108.gif", "109.gif", "110.gif", "111.gif", "112.gif", "113.gif", "115.gif", "116.gif", "117.gif", "118.gif", "119.gif", "120.gif", "122.gif", "123.gif", "124.gif", "127.gif", "129.gif", "130.gif", "131.gif", "134.gif", "135.gif", "136.gif", "138.gif", "139.gif", "141.gif", "144.gif", "146.gif", "148.gif", "149.gif", "153.gif", "154.gif", "155.gif", "157.gif", "158.gif", "159.gif", "160.gif", "161.gif", "162.gif", "164.gif", "166.gif", "167.gif", "168.gif", "169.gif", "170.gif", "171.gif", "172.gif", "173.gif", "174.gif", "175.gif", "176.gif", "177.gif", "178.gif", "181.gif", "182.gif", "183.gif", "185.gif", "186.gif", "187.gif", "188.gif", "189.gif", "190.gif", "191.gif", "192.gif", "193.gif", "195.gif", "196.gif", "197.gif", "200.gif", "201.gif", "202.gif", "203.gif", "204.gif", "205.gif", "206.gif", "207.gif", "208.gif", "209.gif", "210.gif", "211.gif", "212.gif", "213.gif", "214.gif", "215.gif", "216.gif", "217.gif", "219.gif", "220.gif", "221.gif", "222.gif", "224.gif", "225.gif", "226.gif", "227.gif", "228.gif", "230.gif", "232.gif", "233.gif", "234.gif", "235.gif", "238.gif", "240.gif", "241.gif", "243.gif", "244.gif", "245.gif", "246.gif", "247.gif", "249.gif", "250.gif", "251.gif", "253.gif"];
-export default banners;
diff --git a/src/config/banners.json b/src/config/banners.json
new file mode 100644
index 0000000000..785cc83064
--- /dev/null
+++ b/src/config/banners.json
@@ -0,0 +1 @@
+["0.jpg", "1.jpg", "2.jpg", "4.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg", "12.jpg", "13.jpg", "14.jpg", "16.jpg", "17.jpg", "18.jpg", "19.jpg", "20.jpg", "21.jpg", "22.jpg", "24.jpg", "25.jpg", "26.jpg", "28.jpg", "29.jpg", "33.jpg", "38.jpg", "39.jpg", "43.jpg", "44.jpg", "45.jpg", "46.jpg", "47.jpg", "52.jpg", "54.jpg", "57.jpg", "59.jpg", "60.jpg", "61.jpg", "64.jpg", "66.jpg", "67.jpg", "69.jpg", "71.jpg", "72.jpg", "76.jpg", "77.jpg", "81.jpg", "82.jpg", "83.jpg", "84.jpg", "88.jpg", "90.jpg", "91.jpg", "96.jpg", "98.jpg", "99.jpg", "100.jpg", "104.jpg", "106.jpg", "116.jpg", "119.jpg", "137.jpg", "140.jpg", "148.jpg", "149.jpg", "150.jpg", "154.jpg", "156.jpg", "157.jpg", "158.jpg", "159.jpg", "161.jpg", "162.jpg", "164.jpg", "165.jpg", "166.jpg", "167.jpg", "168.jpg", "169.jpg", "170.jpg", "171.jpg", "172.jpg", "173.jpg", "174.jpg", "175.jpg", "176.jpg", "178.jpg", "179.jpg", "180.jpg", "181.jpg", "182.jpg", "183.jpg", "186.jpg", "189.jpg", "190.jpg", "192.jpg", "193.jpg", "194.jpg", "197.jpg", "198.jpg", "200.jpg", "201.jpg", "202.jpg", "203.jpg", "205.jpg", "206.jpg", "207.jpg", "208.jpg", "210.jpg", "213.jpg", "214.jpg", "215.jpg", "216.jpg", "218.jpg", "219.jpg", "220.jpg", "221.jpg", "222.jpg", "223.jpg", "224.jpg", "227.jpg", "0.png", "1.png", "2.png", "3.png", "5.png", "6.png", "9.png", "10.png", "11.png", "12.png", "14.png", "16.png", "19.png", "20.png", "21.png", "22.png", "23.png", "24.png", "26.png", "27.png", "28.png", "29.png", "30.png", "31.png", "32.png", "33.png", "34.png", "37.png", "39.png", "40.png", "41.png", "42.png", "43.png", "44.png", "45.png", "48.png", "49.png", "50.png", "51.png", "52.png", "53.png", "57.png", "58.png", "59.png", "64.png", "66.png", "67.png", "68.png", "69.png", "70.png", "71.png", "72.png", "76.png", "78.png", "79.png", "81.png", "82.png", "85.png", "86.png", "87.png", "89.png", "95.png", "98.png", "100.png", "101.png", "102.png", "105.png", "106.png", "107.png", "109.png", "110.png", "111.png", "112.png", "113.png", "114.png", "115.png", "116.png", "118.png", "119.png", "120.png", "121.png", "122.png", "123.png", "126.png", "128.png", "130.png", "134.png", "136.png", "138.png", "139.png", "140.png", "142.png", "145.png", "146.png", "149.png", "150.png", "151.png", "152.png", "153.png", "154.png", "155.png", "156.png", "157.png", "158.png", "159.png", "160.png", "163.png", "164.png", "165.png", "166.png", "167.png", "168.png", "169.png", "170.png", "171.png", "172.png", "173.png", "174.png", "178.png", "179.png", "180.png", "181.png", "182.png", "184.png", "186.png", "188.png", "190.png", "192.png", "193.png", "194.png", "195.png", "196.png", "197.png", "198.png", "200.png", "202.png", "203.png", "205.png", "206.png", "207.png", "209.png", "212.png", "213.png", "214.png", "216.png", "217.png", "218.png", "219.png", "220.png", "221.png", "222.png", "223.png", "224.png", "225.png", "226.png", "229.png", "231.png", "232.png", "233.png", "234.png", "235.png", "237.png", "238.png", "239.png", "240.png", "241.png", "242.png", "244.png", "245.png", "246.png", "247.png", "248.png", "249.png", "250.png", "253.png", "254.png", "255.png", "256.png", "257.png", "258.png", "259.png", "260.png", "262.png", "268.png", "0.gif", "1.gif", "2.gif", "3.gif", "4.gif", "5.gif", "6.gif", "7.gif", "8.gif", "9.gif", "10.gif", "12.gif", "13.gif", "14.gif", "15.gif", "16.gif", "18.gif", "19.gif", "20.gif", "21.gif", "22.gif", "23.gif", "24.gif", "28.gif", "29.gif", "30.gif", "33.gif", "34.gif", "35.gif", "36.gif", "37.gif", "39.gif", "40.gif", "42.gif", "44.gif", "45.gif", "46.gif", "48.gif", "50.gif", "52.gif", "54.gif", "55.gif", "57.gif", "58.gif", "59.gif", "60.gif", "61.gif", "63.gif", "64.gif", "66.gif", "67.gif", "68.gif", "69.gif", "70.gif", "72.gif", "73.gif", "75.gif", "76.gif", "77.gif", "78.gif", "80.gif", "81.gif", "82.gif", "83.gif", "86.gif", "87.gif", "88.gif", "92.gif", "93.gif", "94.gif", "95.gif", "96.gif", "97.gif", "98.gif", "99.gif", "100.gif", "101.gif", "102.gif", "103.gif", "104.gif", "105.gif", "106.gif", "108.gif", "109.gif", "110.gif", "111.gif", "112.gif", "113.gif", "115.gif", "116.gif", "117.gif", "118.gif", "119.gif", "120.gif", "122.gif", "123.gif", "124.gif", "127.gif", "129.gif", "130.gif", "131.gif", "134.gif", "135.gif", "136.gif", "138.gif", "139.gif", "141.gif", "144.gif", "146.gif", "148.gif", "149.gif", "153.gif", "154.gif", "155.gif", "157.gif", "158.gif", "159.gif", "160.gif", "161.gif", "162.gif", "164.gif", "166.gif", "167.gif", "168.gif", "169.gif", "170.gif", "171.gif", "172.gif", "173.gif", "174.gif", "175.gif", "176.gif", "177.gif", "178.gif", "181.gif", "182.gif", "183.gif", "185.gif", "186.gif", "187.gif", "188.gif", "189.gif", "190.gif", "191.gif", "192.gif", "193.gif", "195.gif", "196.gif", "197.gif", "200.gif", "201.gif", "202.gif", "203.gif", "204.gif", "205.gif", "206.gif", "207.gif", "208.gif", "209.gif", "210.gif", "211.gif", "212.gif", "213.gif", "214.gif", "215.gif", "216.gif", "217.gif", "219.gif", "220.gif", "221.gif", "222.gif", "224.gif", "225.gif", "226.gif", "227.gif", "228.gif", "230.gif", "232.gif", "233.gif", "234.gif", "235.gif", "238.gif", "240.gif", "241.gif", "243.gif", "244.gif", "245.gif", "246.gif", "247.gif", "249.gif", "250.gif", "251.gif", "253.gif"]
\ No newline at end of file
diff --git a/src/css/CSS.js b/src/css/CSS.js
deleted file mode 100644
index 8d106a0892..0000000000
--- a/src/css/CSS.js
+++ /dev/null
@@ -1,37 +0,0 @@
-<%
- var inc = require['style'];
- var faCSS = read('/node_modules/font-awesome/css/font-awesome.css');
- var faWebFont = readBase64('/node_modules/font-awesome/fonts/fontawesome-webfont.woff');
- var mainCSS = ['font-awesome', 'style', 'yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon', 'spooky'].map(x => read(`${x}.css`)).join('');
- var iconNames = files.filter(f => /^linkify\.[^.]+\.png$/.test(f));
- var icons = iconNames.map(readBase64);
-%>CSS = {
-
-boards:
-<%= multiline(
- inc.fa(faCSS, faWebFont) + mainCSS + inc.icons(iconNames, icons) + read('supports.css')
-) %>,
-
-report:
-<%= multiline(read('report.css')) %>,
-
-www:
-<%= multiline(read('www.css')) %>,
-
-sub: function(css) {
- var variables = {
- site: g.SITE.selectors
- };
- return css.replace(/\$[\w\$]+/g, function(name) {
- var words = name.slice(1).split('$');
- var sel = variables;
- for (var i = 0; i < words.length; i++) {
- if (typeof sel !== 'object') return ':not(*)';
- sel = $.getOwn(sel, words[i]);
- }
- if (typeof sel !== 'string') return ':not(*)';
- return sel;
- });
-}
-
-};
diff --git a/src/css/CSS.ts b/src/css/CSS.ts
new file mode 100644
index 0000000000..7d9bf94c0a
--- /dev/null
+++ b/src/css/CSS.ts
@@ -0,0 +1,107 @@
+// cSpell:ignore installGentoo, fontawesome, webfont
+
+import $ from '../platform/$';
+
+// import boardCss from './board.css';
+import faCSS from '../../node_modules/font-awesome/css/font-awesome.css';
+import faWebFont from '../../node_modules/font-awesome/fonts/fontawesome-webfont.woff';
+
+import burichan from './burichan.css';
+import fontAwesome from './font-awesome.css';
+import futaba from './futaba.css';
+import linkifyAudio from './linkify.audio.png';
+import linkifyBitchute from './linkify.bitchute.png';
+import linkifyClyp from './linkify.clyp.png';
+import linkifyDailymotion from './linkify.dailymotion.png';
+import linkifyGfycat from './linkify.gfycat.png';
+import linkifyGist from './linkify.gist.png';
+import linkifyImage from './linkify.image.png';
+import linkifyInstallgentoo from './linkify.installgentoo.png';
+import linkifyLiveleak from './linkify.liveleak.png';
+import linkifyPastebin from './linkify.pastebin.png';
+import linkifyPeertube from './linkify.peertube.png';
+import linkifySoundcloud from './linkify.soundcloud.png';
+import linkifyStreamable from './linkify.streamable.png';
+import linkifyTwitchtv from './linkify.twitchtv.png';
+import linkifyTwitter from './linkify.twitter.png';
+import linkifyVideo from './linkify.video.png';
+import linkifyVidlii from './linkify.vidlii.png';
+import linkifyCimeo from './linkify.vimeo.png';
+import linkifyVine from './linkify.vine.png';
+import linkifyVocaroo from './linkify.vocaroo.png';
+import linkifyYoutube from './linkify.youtube.png';
+
+import photon from './photon.css';
+import report from './report.css';
+import spooky from './spooky.css';
+import style from './style.css';
+// style.inc
+import supports from './supports.css';
+import tomorrow from './tomorrow.css';
+import www from './www.css';
+import yotsubaB from './yotsuba-b.css';
+import yotsuba from './yotsuba.css';
+import { fa, icons } from './style';
+import { g } from '../globals/globals';
+
+// <%
+ // var inc = require['style'];
+ // var faCSS = read('/node_modules/font-awesome/css/font-awesome.css');
+ // var faWebFont = readBase64('/node_modules/font-awesome/fonts/fontawesome-webfont.woff');
+ // var mainCSS = ['font-awesome', 'style', 'yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon', 'spooky'].map(x => read(`${x}.css`)).join('');
+// var iconNames = files.filter(f => /^linkify\.[^.]+\.png$/.test(f));
+// var icons = iconNames.map(readBase64);
+// %>
+
+const mainCSS = fontAwesome + style + yotsuba +yotsubaB+futaba+burichan+tomorrow + photon + spooky;
+const faIcons: { name: string, data: string }[] = [
+ { name: "Audio", data: linkifyAudio },
+ { name: "Bitchute", data: linkifyBitchute },
+ { name: "Clyp", data: linkifyClyp },
+ { name: "Dailymotion", data: linkifyDailymotion },
+ { name: "Gfycat", data: linkifyGfycat },
+ { name: "Gist", data: linkifyGist },
+ { name: "Image", data: linkifyImage },
+ { name: "Installgentoo", data: linkifyInstallgentoo },
+ { name: "Liveleak", data: linkifyLiveleak },
+ { name: "Pastebin", data: linkifyPastebin },
+ { name: "Peertube", data: linkifyPeertube },
+ { name: "Soundcloud", data: linkifySoundcloud },
+ { name: "Streamable", data: linkifyStreamable },
+ { name: "Twitchtv", data: linkifyTwitchtv },
+ { name: "Twitter", data: linkifyTwitter },
+ { name: "Video", data: linkifyVideo },
+ { name: "Vidlii", data: linkifyVidlii },
+ { name: "Cimeo", data: linkifyCimeo },
+ { name: "Vine", data: linkifyVine },
+ { name: "Vocaroo", data: linkifyVocaroo },
+ { name: "Youtube", data: linkifyYoutube },
+];
+
+const CSS = {
+
+ boards: fa(faCSS, faWebFont) + mainCSS + icons(faIcons) + supports,
+
+ report,
+
+ www,
+
+ sub: function(css: string) {
+ var variables = {
+ site: g.SITE.selectors
+ };
+ return css.replace(/\$[\w\$]+/g, function(name) {
+ var words = name.slice(1).split('$');
+ var sel = variables;
+ for (var i = 0; i < words.length; i++) {
+ if (typeof sel !== 'object') return ':not(*)';
+ sel = $.getOwn(sel, words[i]);
+ }
+ if (typeof sel !== 'string') return ':not(*)';
+ return sel;
+ });
+ }
+
+};
+
+export default CSS;
diff --git a/src/css/style.css b/src/css/style.css
index b00e259337..569d016ffd 100644
--- a/src/css/style.css
+++ b/src/css/style.css
@@ -1918,7 +1918,7 @@ input[type="checkbox"]:checked ~ .checkbox-letter {
opacity: 0;
pointer-events: none;
}
-.checkbox-letter, #paste-area, #url-button, #custom-cooldown-button, #dump-button {
+.checkbox-letter, #paste-area, #url-button, #custom-cooldown-button, #dump-button, #split-post {
opacity: 0.6;
}
#paste-area {
diff --git a/src/css/style.inc b/src/css/style.inc
deleted file mode 100644
index 8eb7dae906..0000000000
--- a/src/css/style.inc
+++ /dev/null
@@ -1,41 +0,0 @@
-/* jshint esnext: true */
-
-// == Reprocess Font Awesome CSS == //
-var fa = (css, font) => (
-
-// Font Awesome CSS attribution and license
-css.match(/\/\*\![^]*?\*\//)[0] + '\n' +
-
-// Font Awesome web font
-`@font-face {
- font-family: FontAwesome;
- src: url('data:application/font-woff;base64,${font}') format('woff');
- font-weight: 400;
- font-style: normal;
-}
-` +
-
-// fa-[icon name] classes
-css
- .match(/(\.fa-[^{]*{\s*content:[^}]*}\s*)+/)[0]
- .replace(/([,{;])\s+/g, '$1')
- .replace(/,/g, ', ')
-
-);
-
-// == Create CSS for Link Title Favicons == //
-var icons = (names, data) => (
-
-'/* Link Title Favicons */\n' +
-names.map((file, i) =>
-`.linkify.${file.split('.')[1]}::before {
- content: "";
- background: transparent url('data:image/png;base64,${data[i]}') center left no-repeat!important;
- padding-left: 18px;
-}
-`
-).join('')
-
-);
-
-return {fa, icons};
diff --git a/src/css/style.ts b/src/css/style.ts
new file mode 100644
index 0000000000..f13f37f8e7
--- /dev/null
+++ b/src/css/style.ts
@@ -0,0 +1,37 @@
+// == Reprocess Font Awesome CSS == //
+export const fa = (css: string, font: string) => (
+
+ // Font Awesome CSS attribution and license
+ css.match(/\/\*\![^]*?\*\//)[0] + '\n' +
+
+ // Font Awesome web font
+ `@font-face {
+ font-family: FontAwesome;
+ src: url('data:application/font-woff;base64,${font}') format('woff');
+ font-weight: 400;
+ font-style: normal;
+}
+` +
+
+ // fa-[icon name] classes
+ css
+ .match(/(\.fa-[^{]*{\s*content:[^}]*}\s*)+/)[0]
+ .replace(/([,{;])\s+/g, '$1')
+ .replace(/,/g, ', ')
+
+);
+
+// == Create CSS for Link Title Favicons == //
+export const icons = (data: { name: string, data: string }[]) => (
+
+ '/* Link Title Favicons */\n' +
+ data.map(({ name, data }) =>
+ `.linkify.${name}::before {
+ content: "";
+ background: transparent url('data:image/png;base64,${data}') center left no-repeat!important;
+ padding-left: 18px;
+}
+`
+ ).join('')
+
+);
diff --git a/src/globals/globals.js b/src/globals/globals.js
deleted file mode 100644
index 6b3f2b47f8..0000000000
--- a/src/globals/globals.js
+++ /dev/null
@@ -1,46 +0,0 @@
-var Conf, E, c, d, doc, docSet, g;
-
-Conf = Object.create(null);
-c = console;
-d = document;
-doc = d.documentElement;
-
-// Workaround for userscript managers that run script before document.documentElement is set
-docSet = function() {
- return (doc = d.documentElement);
-};
-
-g = {
- VERSION: '<%= readJSON('/version.json').version %>',
- NAMESPACE: '<%= meta.name %>.',
- sites: Object.create(null),
- boards: Object.create(null)
-};
-
-E = (function() {
- var fn, r, regex, str;
- str = {
- '&': '&',
- "'": ''',
- '"': '"',
- '<': '<',
- '>': '>'
- };
- r = String.prototype.replace;
- regex = /[&"'<>]/g;
- fn = function(x) {
- return str[x];
- };
- return function(text) {
- return r.call(text, regex, fn);
- };
-})();
-
-E.cat = function(templates) {
- var html, i, len;
- html = '';
- for (i = 0, len = templates.length; i < len; i++) {
- html += templates[i].innerHTML;
- }
- return html;
-};
diff --git a/src/globals/globals.ts b/src/globals/globals.ts
new file mode 100644
index 0000000000..bf97f379b9
--- /dev/null
+++ b/src/globals/globals.ts
@@ -0,0 +1,94 @@
+import version from "../../version.json";
+import meta from "../../package.json";
+import type SimpleDict from "../classes/SimpleDict";
+import type Post from "../classes/Post";
+import type Thread from "../classes/Thread";
+import type SWTinyboard from "../site/SW.tinyboard";
+
+// interfaces might be incomplete
+export interface BoardConfig {
+ board: string
+ bump_limit: number
+ cooldowns: {
+ threads: number,
+ replies: number,
+ images: number,
+ }
+ custom_spoilers: 1 | 0,
+ image_limit: number,
+ is_archived: 1 | 0,
+ max_comment_chars: number
+ max_filesize: number
+ max_webm_duration: number
+ max_webm_filesize: number
+ meta_description: string,
+ pages: number,
+ per_page: number,
+ spoilers: number,
+ title: string
+ ws_board: 1 | 0
+}
+
+export interface Board {
+ ID: string,
+ boardID: string,
+ siteID: string,
+ config: BoardConfig,
+ posts: SimpleDict,
+ threads: SimpleDict,
+}
+
+export const Conf = Object.create(null);
+
+export const g: {
+ VERSION: string,
+ NAMESPACE: string,
+ sites: (typeof SWTinyboard)[],
+ boards: Board[],
+ posts?: SimpleDict,
+ threads?: SimpleDict
+ THREADID?: number,
+ SITE?: typeof SWTinyboard,
+ BOARD?: Board,
+ VIEW?: string,
+} = {
+ VERSION: version.version,
+ NAMESPACE: meta.name,
+ sites: Object.create(null),
+ boards: Object.create(null)
+};
+
+export const E = (function () {
+ const str = {
+ '&': '&',
+ "'": ''',
+ '"': '"',
+ '<': '<',
+ '>': '>'
+ };
+ const regex = /[&"'<>]/g;
+ const fn = function (x: string) {
+ return str[x];
+ };
+ const output = function (text: string) {
+ return text.toString().replace(regex, fn);
+ };
+ output.cat = function (templates) {
+ let html = '';
+ for (let i = 0; i < templates.length; i++) {
+ html += templates[i].innerHTML;
+ }
+ return html;
+ };
+ return output;
+})();
+
+export const d = document;
+export const doc = d.documentElement;
+
+export const c = console;
+
+export const docSet = function () {
+ // return (doc = d.documentElement);
+ return doc;
+};
diff --git a/src/globals/jsx.ts b/src/globals/jsx.ts
new file mode 100644
index 0000000000..9556753ca4
--- /dev/null
+++ b/src/globals/jsx.ts
@@ -0,0 +1,70 @@
+/*
+ * This file has the code for the jsx to { innerHTML: "safe string" }
+ *
+ * Usage: import h from this file.
+ * Attributes are stringified raw, so the names must be like html text: eg class and not className.
+ * Boolean values are stringified as followed: true will mean the attribute is there, false means it will be omitted.
+ * Strings bound to attributes and children will be escaped automatically.
+ * It returns interface EscapedHtml { innerHTML: "safe string", [isEscaped]: true }
+ *
+ * For strings that don't have a parent element you can use fragments: <>>.
+ * Note that you need to import hFragment, which for some reason isn't auto imported on "add all missing imports"
+ */
+
+import { E } from "./globals";
+
+/**
+ * The symbol indicating that a string is safely escaped.
+ * This is a symbol so it can't be faked by a json blob from the internet.
+ */
+export const isEscaped = Symbol('isEscaped');
+
+export interface EscapedHtml {
+ innerHTML: string,
+ [isEscaped]: true,
+}
+
+const voidElements = new Set(
+ ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'source', 'track', 'wbr',]
+);
+
+export const hFragment = Symbol('hFragment');
+
+/** Function that jsx/tsx will be compiled to. */
+export default function h(
+ tag: string | typeof hFragment,
+ attributes: Record | null,
+ ...children: unknown[]
+): EscapedHtml {
+ let innerHTML = tag === hFragment ? '' : `<${tag}`;
+
+ if (attributes) {
+ for (const [attribute, value] of Object.entries(attributes)) {
+ if (!value && value !== 0) continue;
+ innerHTML += ` ${attribute}`;
+ if (value === true) continue;
+ innerHTML += `="${E(value.toString())}"`;
+ }
+ }
+ if (tag !== hFragment) innerHTML += '>';
+
+ const isVoid = tag !== hFragment && voidElements.has(tag);
+ if (isVoid) {
+ if (children.length) throw new TypeError(`${tag} is a void html element and can't have child elements`);
+ } else {
+ for (const child of children) {
+ if (child === null || child === undefined || child === '') continue;
+
+ if (child instanceof Object && "innerHTML" in child && child[isEscaped]) {
+ innerHTML += child.innerHTML;
+ continue;
+ }
+
+ innerHTML += E(child.toString());
+ }
+ }
+
+ if (!isVoid && tag !== hFragment) innerHTML += `${tag}>`;
+
+ return { innerHTML, [isEscaped]: true };
+}
diff --git a/src/main/Main.js b/src/main/Main.js
index fdd8aa11f7..ce2eddb34d 100644
--- a/src/main/Main.js
+++ b/src/main/Main.js
@@ -1,3 +1,99 @@
+import Redirect from "../Archive/Redirect";
+import Board from "../classes/Board";
+import Callbacks from "../classes/Callbacks";
+import CatalogThreadNative from "../classes/CatalogThreadNative";
+import DataBoard from "../classes/DataBoard";
+import Notice from "../classes/Notice";
+import Post from "../classes/Post";
+import SimpleDict from "../classes/SimpleDict";
+import Thread from "../classes/Thread";
+import Config from "../config/Config";
+import Anonymize from "../Filtering/Anonymize";
+import Filter from "../Filtering/Filter";
+import PostHiding from "../Filtering/PostHiding";
+import Recursive from "../Filtering/Recursive";
+import ThreadHiding from "../Filtering/ThreadHiding";
+import Index from "../General/Index";
+import Settings from "../General/Settings";
+import FappeTyme from "../Images/FappeTyme";
+import Gallery from "../Images/Gallery";
+import ImageCommon from "../Images/ImageCommon";
+import ImageExpand from "../Images/ImageExpand";
+import ImageHost from "../Images/ImageHost";
+import ImageHover from "../Images/ImageHover";
+import ImageLoader from "../Images/ImageLoader";
+import Metadata from "../Images/Metadata";
+import RevealSpoilers from "../Images/RevealSpoilers";
+import Sauce from "../Images/Sauce";
+import Volume from "../Images/Volume";
+import Linkify from "../Linkification/Linkify";
+import ArchiveLink from "../Menu/ArchiveLink";
+import CopyTextLink from "../Menu/CopyTextLink";
+import DeleteLink from "../Menu/DeleteLink";
+import DownloadLink from "../Menu/DownloadLink";
+import ReportLink from "../Menu/ReportLink";
+import AntiAutoplay from "../Miscellaneous/AntiAutoplay";
+import Banner from "../Miscellaneous/Banner";
+import CatalogLinks from "../Miscellaneous/CatalogLinks";
+import CustomCSS from "../Miscellaneous/CustomCSS";
+import ExpandComment from "../Miscellaneous/ExpandComment";
+import ExpandThread from "../Miscellaneous/ExpandThread";
+import FileInfo from "../Miscellaneous/FileInfo";
+import Flash from "../Miscellaneous/Flash";
+import Fourchan from "../Miscellaneous/Fourchan";
+import IDColor from "../Miscellaneous/IDColor";
+import IDHighlight from "../Miscellaneous/IDHighlight";
+import IDPostCount from "../Miscellaneous/IDPostCount";
+import Keybinds from "../Miscellaneous/Keybinds";
+import ModContact from "../Miscellaneous/ModContact";
+import Nav from "../Miscellaneous/Nav";
+import NormalizeURL from "../Miscellaneous/NormalizeURL";
+import PostJumper from "../Miscellaneous/PostJumper";
+import PSA from "../Miscellaneous/PSA";
+import PSAHiding from "../Miscellaneous/PSAHiding";
+import RelativeDates from "../Miscellaneous/RelativeDates";
+import RemoveSpoilers from "../Miscellaneous/RemoveSpoilers";
+import ThreadLinks from "../Miscellaneous/ThreadLinks";
+import Time from "../Miscellaneous/Time";
+import Tinyboard from "../Miscellaneous/Tinyboard";
+import Favicon from "../Monitoring/Favicon";
+import MarkNewIPs from "../Monitoring/MarkNewIPs";
+import ReplyPruning from "../Monitoring/ReplyPruning";
+import ThreadStats from "../Monitoring/ThreadStats";
+import ThreadUpdater from "../Monitoring/ThreadUpdater";
+import ThreadWatcher from "../Monitoring/ThreadWatcher";
+import Unread from "../Monitoring/Unread";
+import UnreadIndex from "../Monitoring/UnreadIndex";
+import $ from "../platform/$";
+import $$ from "../platform/$$";
+import PassLink from "../Posting/PassLink";
+import PostRedirect from "../Posting/PostRedirect";
+import QR from "../Posting/QR";
+import QuoteBacklink from "../Quotelinks/QuoteBacklink";
+import QuoteCT from "../Quotelinks/QuoteCT";
+import QuoteInline from "../Quotelinks/QuoteInline";
+import QuoteOP from "../Quotelinks/QuoteOP";
+import QuotePreview from "../Quotelinks/QuotePreview";
+import QuoteStrikeThrough from "../Quotelinks/QuoteStrikeThrough";
+import QuoteThreading from "../Quotelinks/QuoteThreading";
+import QuoteYou from "../Quotelinks/QuoteYou";
+import Quotify from "../Quotelinks/Quotify";
+import Site from "../site/Site";
+import SW from "../site/SW";
+import CSS from "../css/CSS";
+import meta from '../../package.json';
+import Header from "../General/Header";
+import { c, Conf, d, doc, docSet, E, g } from "../globals/globals";
+import Menu from "../Menu/Menu";
+import BoardConfig from "../General/BoardConfig";
+import CaptchaReplace from "../Posting/Captcha.replace";
+import Get from "../General/Get";
+import { dict, platform } from "../platform/helpers";
+import Polyfill from "../General/Polyfill";
+// #region tests_enabled
+import Test from "../General/Test";
+// #endregion
+
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
@@ -14,9 +110,9 @@ var Main = {
let key;
try {
let w = window;
- if ($.platform === 'crx') { w = (w.wrappedJSObject || w); }
- if ('<%= meta.name %> antidup' in w) { return; }
- w['<%= meta.name %> antidup'] = true;
+ if (platform === 'crx') { w = (w.wrappedJSObject || w); }
+ if (`${meta.name} antidup` in w) { return; }
+ w[`${meta.name} antidup`] = true;
} catch (error) {}
// Don't run inside ad iframes.
@@ -53,7 +149,7 @@ var Main = {
// Flatten default values from Config into Conf
var flatten = function(parent, obj) {
if (obj instanceof Array) {
- Conf[parent] = $.dict.clone(obj[0]);
+ Conf[parent] = dict.clone(obj[0]);
} else if (typeof obj === 'object') {
for (var key in obj) {
var val = obj[key];
@@ -83,16 +179,16 @@ var Main = {
flatten(null, Config);
for (var db of DataBoard.keys) {
- Conf[db] = $.dict();
+ Conf[db] = dict();
}
- Conf['customTitles'] = $.dict.clone({'4chan.org': {boards: {'qa': {'boardTitle': {orig: '/qa/ - Question & Answer', title: '/qa/ - 2D/Random'}}}}});
- Conf['boardConfig'] = {boards: $.dict()};
+ Conf['customTitles'] = dict.clone({'4chan.org': {boards: {'qa': {'boardTitle': {orig: '/qa/ - Question & Answer', title: '/qa/ - 2D/Random'}}}}});
+ Conf['boardConfig'] = {boards: dict()};
Conf['archives'] = Redirect.archives;
- Conf['selectedArchives'] = $.dict();
- Conf['cooldowns'] = $.dict();
- Conf['Index Sort'] = $.dict();
- for (let i = 0; i < 2; i++) { Conf[`Last Long Reply Thresholds ${i}`] = $.dict(); }
- Conf['siteProperties'] = $.dict();
+ Conf['selectedArchives'] = dict();
+ Conf['cooldowns'] = dict();
+ Conf['Index Sort'] = dict();
+ for (let i = 0; i < 2; i++) { Conf[`Last Long Reply Thresholds ${i}`] = dict(); }
+ Conf['siteProperties'] = dict();
// XXX old key names
Conf['Except Archives from Encryption'] = false;
@@ -106,7 +202,7 @@ var Main = {
Conf['Use Faster Image Host'] = 'true';
Conf['Captcha Fixes'] = true;
Conf['captchaServiceDomain'] = '';
- Conf['captchaServiceKey'] = $.dict();
+ Conf['captchaServiceKey'] = dict();
// Enforce JS whitelist
if (
@@ -114,11 +210,14 @@ var Main = {
!SW.yotsuba.regexp.pass.test(location.href) &&
!$$('script:not([src])', d).filter(s => /this\[/.test(s.textContent)).length
) {
- ($.getSync || $.get)({'jsWhitelist': Conf['jsWhitelist']}, ({jsWhitelist}) => $.addCSP(`script-src ${jsWhitelist.replace(/^#.*$/mg, '').replace(/[\s;]+/g, ' ').trim()}`));
+ ($.getSync || $.get)({'jsWhitelist': Conf['jsWhitelist']}, ({jsWhitelist}) => {
+ const parsedList = jsWhitelist.replace(/^#.*$/mg, '').replace(/[\s;]+/g, ' ').trim();
+ if (/\S/.test(parsedList)) $.addCSP(`script-src ${parsedList}`);
+ });
}
// Get saved values as items
- const items = $.dict();
+ const items = dict();
for (key in Conf) { items[key] = undefined; }
items['previousversion'] = undefined;
return ($.getSync || $.get)(items, function(items) {
@@ -162,9 +261,8 @@ var Main = {
items.previousversion = (changes.previousversion = g.VERSION);
return $.set(changes, function() {
if (items['Show Updated Notifications'] ?? true) {
- // TODO meta
const el = $.el('span',
- { innerHTML: 'meta.name has been updated to version ${g.VERSION} .'});
+ { innerHTML: `${meta.name} has been updated to version ${g.VERSION} .` });
return new Notice('info', el, 15);
}
});
@@ -271,8 +369,8 @@ var Main = {
if (!Main.isThisPageLegit()) { return; }
// disable the mobile layout
- // TODO check if exists
- $('link[href*=mobile]', d.head).disabled = true;
+ const mobileLink = $('link[href*=mobile]', d.head);
+ if (mobileLink) mobileLink.disabled = true;
doc.dataset.host = location.host;
$.addClass(doc, `sw-${g.SITE.software}`);
$.addClass(doc, g.VIEW === 'thread' ? 'thread-view' : g.VIEW);
@@ -670,7 +768,7 @@ var Main = {
$.addClass(doc, 'tainted');
if (Conf['Disable Native Extension'] && !Main.isFirstRun) {
const msg = $.el('div',
- {innerHTML: 'Failed to disable the native extension. You may need to block it .'});
+ { innerHTML: 'Failed to disable the native extension. You may need to block it .' });
new Notice('error', msg);
}
}
@@ -686,9 +784,11 @@ var Main = {
return;
}
- const div = $.el('div',
- {innerHTML: '${errors.length} errors occurred.&{Main.reportLink(errors)} [show ]'});
- $.on(div.lastElementChild, 'click', function() {
+ const div = $.el('div', {
+ innerHTML:
+ `${errors.length} errors occurred.${Main.reportLink(errors).innerHTML} [show ]`
+ });
+ $.on(div.lastElementChild, 'click', function () {
let ref;
return [this.textContent, logs.hidden] = Array.from(ref = this.textContent === 'show' ? (
['hide', false]
@@ -709,12 +809,12 @@ var Main = {
parseError(data, reportLink) {
c.error(data.message, data.error.stack);
const message = $.el('div',
- {innerHTML: '${data.message}?{reportLink}{&{reportLink}}'});
+ { innerHTML: E(data.message) + ((reportLink) ? (reportLink).innerHTML : "") });
const error = $.el('div',
{textContent: `${data.error.name || 'Error'}: ${data.error.message || 'see console for details'}`});
const lines = data.error.stack?.match(/\d+(?=:\d+\)?$)/mg)?.join().replace(/^/, ' at ') || '';
const context = $.el('div',
- {textContent: `(<%= meta.name %> <%= meta.fork %> v${g.VERSION} ${$.platform} on ${$.engine}${lines})`});
+ { textContent: `(${meta.name} ${meta.fork} v${g.VERSION} ${platform} on ${$.engine}${lines})` });
return [message, error, context];
},
@@ -725,20 +825,19 @@ var Main = {
if (errors.length > 1) { title += ` (+${errors.length - 1} other errors)`; }
let details = '';
const addDetails = function(text) {
- // TODO meta
- if (encodeURIComponent(title + details + text + '\n').length <= "meta.newIssueMaxLength - meta.newIssue.replace(/%(title|details)/, '')".length) {
+ if (encodeURIComponent(title + details + text + '\n').length <= meta.newIssueMaxLength - meta.newIssue.replace(/%(title|details)/, '').length) {
return details += text + '\n';
}
};
addDetails(`\
[Please describe the steps needed to reproduce this error.]
-Script: <%= meta.name %> <%= meta.fork %> v${g.VERSION} ${$.platform}
+Script: ${meta.name} ${meta.fork} v${g.VERSION} ${platform}
URL: ${location.href}
User agent: ${navigator.userAgent}\
`
);
- if (($.platform === 'userscript') && (info = (() => {
+ if ((platform === 'userscript') && (info = (() => {
if (typeof GM !== 'undefined' && GM !== null) { return GM.info; } else { if (typeof GM_info !== 'undefined' && GM_info !== null) { return GM_info; }
}
})())) {
@@ -748,8 +847,8 @@ User agent: ${navigator.userAgent}\
if (data.error.stack) { addDetails(data.error.stack.replace(data.error.toString(), '').trim()); }
if (data.html) { addDetails('\n`' + data.html + '`'); }
details = details.replace(/file:\/{3}.+\//g, ''); // Remove local file paths
- const url = '<%= meta.newIssue %>'.replace('%title', encodeURIComponent(title)).replace('%details', encodeURIComponent(details));
- return {innerHTML: ' [report ] '};
+ const url = meta.newIssue.replace('%title', encodeURIComponent(title)).replace('%details', encodeURIComponent(details));
+ return { innerHTML: ` [report ] ` };
},
isThisPageLegit() {
@@ -784,7 +883,7 @@ User agent: ${navigator.userAgent}\
['Board Configuration', BoardConfig],
['Normalize URL', NormalizeURL],
['Delay Redirect on Post', PostRedirect],
- ['Captcha Configuration', Captcha.replace],
+ ['Captcha Configuration', CaptchaReplace],
['Image Host Rewriting', ImageHost],
['Redirect', Redirect],
['Header', Header],
@@ -865,7 +964,9 @@ User agent: ${navigator.userAgent}\
['Mod Contact Links', ModContact]
]
};
+export default Main;
+$.ready(() => Main.init());
-// <% if (readJSON('/.tests_enabled')) { %>
-// Main.features.push ['Build Test', Test]
-// <% } %>
+// #region tests_enabled
+Main.features.push(['Build Test', Test]);
+// #endregion
diff --git a/src/meta/manifest.json b/src/meta/manifest.json
deleted file mode 100644
index 1c82e9c8e0..0000000000
--- a/src/meta/manifest.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
- "name": "<%= meta.name %>",
- "version": "<%= readJSON('/version.json').version %>",
- "manifest_version": 2,
- "description": "<%= description %>",
- "icons": {
- "16": "icon16.png",
- "48": "icon48.png",
- "128": "icon128.png"
- },
- "content_scripts": [{
- "js": ["script.js"],
- "matches": <%= JSON.stringify(meta.matches_only.concat(meta.matches, meta.matches_extra)) %>,
- "exclude_matches": <%= JSON.stringify(meta.exclude_matches) %>,
- "all_frames": true,
- "run_at": "document_start"
- }],
- "background": {
- "scripts": ["eventPage.js"],
- "persistent": false
- },
- "homepage_url": "<%= meta.page %>",
-<% if (channel !== '-noupdate') { %> "update_url": "<%= meta.downloads %>updates<%= channel %>.xml",
- "key": "<%= meta.appid %>",
-<% } %> "minimum_chrome_version": "<%= meta.min.chrome %>",
- "permissions": <%= JSON.stringify(meta.matches_only.concat(meta.matches, ["storage"])) %>,
- "optional_permissions": [
- "*://*/"
- ],
- "applications": {
- "gecko": {
- "id": "<%= meta.appidGecko %>"<% if (channel !== '-noupdate') { %>,
- "update_url": "<%= meta.downloads %>updates<%= channel %>.json"
-<% } %> }
- }
-}
diff --git a/src/meta/manifestJson.js b/src/meta/manifestJson.js
new file mode 100644
index 0000000000..92cea7a96e
--- /dev/null
+++ b/src/meta/manifestJson.js
@@ -0,0 +1,42 @@
+export default function generateManifestJson(p, version, channel) {
+ const manifest = {
+ "name": p.meta.name,
+ "version": version.version,
+ "manifest_version": 2,
+ "description": p.description,
+ "icons": {
+ "16": "icon16.png",
+ "48": "icon48.png",
+ "128": "icon128.png"
+ },
+ "content_scripts": [{
+ "js": ["script.js"],
+ "matches": p.meta.matches_only.concat(p.meta.matches, p.meta.matches_extra),
+ "exclude_matches": p.meta.exclude_matches,
+ "all_frames": true,
+ "run_at": "document_start"
+ }],
+ "background": {
+ "scripts": ["eventPage.js"],
+ "persistent": false
+ },
+ "homepage_url": p.meta.page,
+ "minimum_chrome_version": p.meta.min.chrome,
+ "permissions": p.meta.matches_only.concat(p.meta.matches, ["storage"]),
+ "optional_permissions": [
+ "*://*/"
+ ],
+ "applications": {
+ "gecko": {
+ "id": p.meta.appidGecko,
+ }
+ }
+ };
+
+ if (channel !== '-noupdate') {
+ manifest.update_url = `${p.meta.downloads}updates${channel}.xml`;
+ manifest.applications.gecko.update_url = `${p.meta.downloads}updates${channel}.json`;
+ }
+
+ return JSON.stringify(manifest, undefined, 2);
+}
diff --git a/src/meta/metadata.js b/src/meta/metadata.js
index f154139e15..061f7ec1d9 100644
--- a/src/meta/metadata.js
+++ b/src/meta/metadata.js
@@ -1,18 +1,38 @@
-// ==UserScript==
-// @name <%= meta.name %><%= (channel === '-beta') ? ' beta' : '' %>
-// @version <%= readJSON('/version.json').version %>
-// @minGMVer <%= meta.min.greasemonkey %>
-// @minFFVer <%= meta.min.firefox %>
-// @namespace <%= name %>
-// @description <%= description %>
-// @license MIT; <%= meta.license %>
-<%=
- (function() {
+// this file is needed in the build script, keep it .js
+
+import { readFile } from "fs/promises";
+import { dirname, resolve } from "path";
+import { fileURLToPath } from 'url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+export default async function generateMetadata(packageJson, channel) {
+ const meta = packageJson.meta;
+
+ const versionFile = await readFile(resolve(__dirname, '../../version.json'));
+ const version = JSON.parse(versionFile.toString());
+
+ const iconFile = await readFile(resolve(__dirname, './icon48.png'));
+ const icon = Buffer.from(iconFile).toString('base64');
+
+ const archives = JSON.parse(await readFile(resolve(__dirname, '../Archive/archives.json'), { encoding: 'utf-8' }));
+
+ let output = `// ==UserScript==
+// @name ${meta.name}${channel === '-beta' ? ' beta' : ''}
+// @version ${version.version}
+// @minGMVer ${meta.min.greasemonkey}
+// @minFFVer ${meta.min.firefox}
+// @namespace ${packageJson.name}
+// @description ${packageJson.description}
+// @license MIT; ${meta.license}
+`;
+
+ output += (function () {
function expand(items, regex, substitutions) {
var results = [];
- items.forEach(function(item) {
+ items.forEach(function (item) {
if (regex.test(item)) {
- substitutions.forEach(function(s) {
+ substitutions.forEach(function (s) {
results.push(item.replace(regex, s));
});
} else {
@@ -25,24 +45,26 @@
return expand(matches, /^\*/, ['http', 'https']);
}
return [].concat(
- expandMatches(meta.includes_only.concat(meta.matches, meta.matches_extra)).map(function(match) {
+ expandMatches(meta.includes_only.concat(meta.matches, meta.matches_extra)).map(function (match) {
return '// @include ' + match;
}),
- expandMatches(meta.exclude_matches).map(function(match) {
+ expandMatches(meta.exclude_matches).map(function (match) {
return '// @exclude ' + match;
})
).join('\n');
- })()
-%>
+ })();
+
+ output += `
// @connect 4chan.org
// @connect 4channel.org
// @connect 4cdn.org
// @connect 4chenz.github.io
-<%=
- readJSON('/src/Archive/archives.json').map(function(archive) {
+`;
+ output += archives.map(function (archive) {
return '// @connect ' + archive.domain;
- }).join('\n')
-%>
+ }).join('\n');
+
+ output += `
// @connect api.clyp.it
// @connect api.dailymotion.com
// @connect api.github.com
@@ -51,13 +73,24 @@
// @connect vimeo.com
// @connect www.youtube.com
// @connect *
-<%=
- meta.grants.map(function(grant) {
+`;
+ output += meta.grants.map(function (grant) {
return '// @grant ' + grant;
- }).join('\n')
-%>
-// @run-at document-start
-// @updateURL <%= (channel !== '-noupdate') ? `${meta.downloads}${name}${channel}.meta.js` : 'https://noupdate.invalid/' %>
-// @downloadURL <%= (channel !== '-noupdate') ? `${meta.downloads}${name}${channel}.user.js` : 'https://noupdate.invalid/' %>
-// @icon data:image/png;base64,<%= readBase64('/src/meta/icon48.png') %>
+ }).join('\n');
+
+ output += '\n// @run-at document-start';
+
+ if (channel === '-noupdate') {
+ output += '\n// @updateURL https://noupdate.invalid/\n// @downloadURL https://noupdate.invalid/';
+ } else {
+ output += `
+// @updateURL ${meta.downloads}${packageJson.name}${channel}.meta.js
+// @downloadURL ${meta.downloads}${packageJson.name}${channel}.user.js`;
+ }
+ output += `
+// @icon data:image/png;base64,${icon}
// ==/UserScript==
+`;
+
+ return output;
+}
diff --git a/src/meta/tsconfig.json b/src/meta/tsconfig.json
new file mode 100644
index 0000000000..d37071c3e2
--- /dev/null
+++ b/src/meta/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "compilerOptions": {
+ "moduleResolution": "node16"
+ },
+ "extends": "../../package.json"
+}
\ No newline at end of file
diff --git a/src/platform/$$.js b/src/platform/$$.js
index 16fa613360..1945af560e 100644
--- a/src/platform/$$.js
+++ b/src/platform/$$.js
@@ -1,7 +1,10 @@
+import { d } from "../globals/globals";
+
/*
* 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
*/
-const $$ = (selector, root=d.body) => [...Array.from(root.querySelectorAll(selector))];
+const $$ = (selector, root = d.body) => [...Array.from(root.querySelectorAll(selector))];
+export default $$;
diff --git a/src/platform/$.js b/src/platform/$.js
index bbae05122f..40bd9f83b8 100644
--- a/src/platform/$.js
+++ b/src/platform/$.js
@@ -7,16 +7,14 @@
*/
// loosely follows the jquery api:
// http://api.jquery.com/
-// not chainable
-const $ = (selector, root=d.body) => root.querySelector(selector);
-$.DAY = 24 * (
- $.HOUR = 60 * (
- $.MINUTE = 60 * (
- $.SECOND = 1000
- )
- )
-);
+import Notice from "../classes/Notice";
+import { c, Conf, d, doc, g } from "../globals/globals";
+import CrossOrigin from "./CrossOrigin";
+import { debounce, dict, MINUTE, platform, SECOND } from "./helpers";
+
+// not chainable
+const $ = (selector, root = document.body) => root.querySelector(selector);
$.id = id => d.getElementById(id);
@@ -57,29 +55,6 @@ $.extend = function(object, properties) {
}
};
-$.dict = () => Object.create(null);
-
-$.dict.clone = function(obj) {
- if ((typeof obj !== 'object') || (obj === null)) {
- return obj;
- } else if (obj instanceof Array) {
- const arr = [];
- for (let i = 0, end = obj.length; i < end; i++) {
- arr.push($.dict.clone(obj[i]));
- }
- return arr;
- } else {
- const map = Object.create(null);
- for (var key in obj) {
- var val = obj[key];
- map[key] = $.dict.clone(val);
- }
- return map;
- }
-};
-
-$.dict.json = str => $.dict.clone(JSON.parse(str));
-
$.hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
$.getOwn = function(obj, key) {
@@ -87,23 +62,24 @@ $.getOwn = function(obj, key) {
};
$.ajax = (function() {
- let pageXHR;
+ let pageXHR = XMLHttpRequest;
if (window.wrappedJSObject && !XMLHttpRequest.wrappedJSObject) {
- pageXHR = XPCNativeWrapper(window.wrappedJSObject.XMLHttpRequest);
- } else {
- pageXHR = XMLHttpRequest;
+ try {
+ pageXHR = XPCNativeWrapper(window.wrappedJSObject.XMLHttpRequest);
+ } catch (e) {}
}
- (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
url = url.replace(/^((?:https?:)?\/\/(?:\w+\.)?(?:4chan|4channel|4cdn)\.org)\/adv\//, '$1//adv/');
- // <% if (type === 'crx') { %>
- // # XXX https://bugs.chromium.org/p/chromium/issues/detail?id=920638
- // if Conf['Work around CORB Bug'] and g.SITE.software is 'yotsuba' and !options.testCORB and FormData.prototype.entries
- // return $.ajaxPage url, options
- // <% } %>
+ if (platform === 'crx') {
+ // XXX https://bugs.chromium.org/p/chromium/issues/detail?id=920638
+ if (Conf['Work around CORB Bug'] && g.SITE.software === 'yotsuba' && !options.testCORB && FormData.prototype.entries) {
+ return $.ajaxPage(url, options);
+ }
+ }
const {onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers} = options;
const r = new pageXHR();
try {
@@ -117,12 +93,14 @@ $.ajax = (function() {
$.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}`); } });
- // <% if (type === 'crx') { %>
- // # XXX https://bugs.chromium.org/p/chromium/issues/detail?id=920638
- // $.on r, 'load', ->
- // if !Conf['Work around CORB Bug'] and r.readyState is 4 and r.status is 200 and r.statusText is '' and r.response is null
- // $.set 'Work around CORB Bug', (Conf['Work around CORB Bug'] = Date.now())
- // <% } %>
+ if (platform === 'crx') {
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=920638
+ $.on(r, 'load', () => {
+ if (!Conf['Work around CORB Bug'] && r.readyState === 4 && r.status === 200 && r.statusText === '' && r.response === null) {
+ $.set('Work around CORB Bug', (Conf['Work around CORB Bug'] = Date.now()));
+ }
+ });
+ }
r.send(form);
} catch (err) {
// XXX Some content blockers in Firefox (e.g. Adblock Plus and NoScript) throw an exception instead of simulating a connection error.
@@ -134,58 +112,59 @@ $.ajax = (function() {
return r;
});
-// <% if (type === 'crx') { %>
-// # XXX https://bugs.chromium.org/p/chromium/issues/detail?id=920638
-// do ->
- let requestID = 0;
- const requests = $.dict();
-
- $.ajaxPageInit = function() {
- $.global(function() {
- window.FCX.requests = Object.create(null);
-
- document.addEventListener('4chanXAjax', function(e) {
- let fd, r;
- 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 || {};
- for (var key in object) {
- var value = object[key];
- r.setRequestHeader(key, value);
- }
- r.responseType = responseType === 'document' ? 'text' : responseType;
- 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}));
+ if (platform === 'userscript') {
+ return r;
+ } else {
+ // # XXX https://bugs.chromium.org/p/chromium/issues/detail?id=920638
+ let requestID = 0;
+ const requests = dict();
+
+ $.ajaxPageInit = function() {
+ $.global(function() {
+ window.FCX.requests = Object.create(null);
+
+ document.addEventListener('4chanXAjax', function(e) {
+ let fd, r;
+ 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 || {};
+ for (var key in object) {
+ var value = object[key];
+ r.setRequestHeader(key, value);
+ }
+ r.responseType = responseType === 'document' ? 'text' : responseType;
+ 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.onloadend = function() {
+ delete window.FCX.requests[id];
+ 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}));
};
- }
- r.onloadend = function() {
- delete window.FCX.requests[id];
- 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}));
- };
- // connection error or content blocker
- r.onerror = function() {
- if (!r.status) { return console.warn(`4chan X failed to load: ${url}`); }
- };
- if (form) {
- fd = new FormData();
- for (var entry of form) {
- fd.append(entry[0], entry[1]);
+ // connection error or content blocker
+ r.onerror = function() {
+ if (!r.status) { return console.warn(`4chan X failed to load: ${url}`); }
+ };
+ if (form) {
+ fd = new FormData();
+ for (var entry of form) {
+ fd.append(entry[0], entry[1]);
+ }
+ } else {
+ fd = null;
}
- } else {
- fd = null;
- }
- return r.send(fd);
+ return r.send(fd);
}
- , false);
+ , false);
return document.addEventListener('4chanXAjaxAbort', function(e) {
let r;
@@ -218,24 +197,24 @@ $.ajax = (function() {
};
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 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;
+ };
+ }
})();
-// <% } %>
// Status Code 304: Not modified
// 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();
+$.lastModified = dict();
$.whenModified = function(url, bucket, cb, options={}) {
let t;
const {timeout, ajax} = options;
@@ -245,13 +224,13 @@ $.whenModified = function(url, bucket, cb, options={}) {
if (url.split('/')[2] === 'a.4cdn.org') { params.push(`t=${Date.now()}`); }
const url0 = url;
if (params.length) { url += '?' + params.join('&'); }
- const headers = $.dict();
+ const headers = dict();
if ((t = $.lastModified[bucket]?.[url0]) != null) {
headers['If-Modified-Since'] = t;
}
const r = (ajax || $.ajax)(url, {
onloadend() {
- ($.lastModified[bucket] || ($.lastModified[bucket] = $.dict()))[url0] = this.getResponseHeader('Last-Modified');
+ ($.lastModified[bucket] || ($.lastModified[bucket] = dict()))[url0] = this.getResponseHeader('Last-Modified');
return cb.call(this);
},
timeout,
@@ -261,7 +240,7 @@ $.whenModified = function(url, bucket, cb, options={}) {
};
(function() {
- const reqs = $.dict();
+ const reqs = dict();
$.cache = function(url, cb, options={}) {
let req;
const {ajax} = options;
@@ -378,7 +357,7 @@ $.rmClass = function(el, ...classNames) {
$.toggleClass = (el, className) => el.classList.toggle(className);
-$.hasClass = (el, className) => el.classList.includes(className);
+$.hasClass = (el, className) => el.classList.contains(className);
$.rm = el => el?.remove();
@@ -438,55 +417,55 @@ $.one = function(el, events, handler) {
};
$.event = function(event, detail, root=d) {
- // <% if (type === 'userscript') { %>
- if ((detail != null) && (typeof cloneInto === 'function')) {
- detail = cloneInto(detail, d.defaultView);
+ 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}));
};
-// <% if (type === 'userscript') { %>
-// XXX Make $.event work in Pale Moon with GM 3.x (no cloneInto function).
-(function() {
- if (!/PaleMoon\//.test(navigator.userAgent) || (+GM_info?.version?.split('.')[0] < 2) || (typeof cloneInto !== 'undefined')) { return; }
+if (platform === 'userscript') {
+ // XXX Make $.event work in Pale Moon with GM 3.x (no cloneInto function).
+ (function() {
+ if (!/PaleMoon\//.test(navigator.userAgent) || (+GM_info?.version?.split('.')[0] < 2) || (typeof cloneInto !== 'undefined')) { return; }
- try {
- return new CustomEvent('x', {detail: {}});
- } catch (err) {
- const unsafeConstructors = {
- Object: unsafeWindow.Object,
- Array: unsafeWindow.Array
- };
- var clone = function(obj) {
- let constructor;
- if ((obj != null) && (typeof obj === 'object') && (constructor = unsafeConstructors[obj.constructor.name])) {
- const obj2 = new constructor();
- for (var key in obj) { var val = obj[key]; obj2[key] = clone(val); }
- return obj2;
- } else {
- return obj;
- }
- };
- return $.event = (event, detail, root=d) => root.dispatchEvent(new CustomEvent(event, {bubbles: true, cancelable: true, detail: clone(detail)}));
- }
-})();
-// <% } %>
+ try {
+ return new CustomEvent('x', {detail: {}});
+ } catch (err) {
+ const unsafeConstructors = {
+ Object: unsafeWindow.Object,
+ Array: unsafeWindow.Array
+ };
+ var clone = function(obj) {
+ let constructor;
+ if ((obj != null) && (typeof obj === 'object') && (constructor = unsafeConstructors[obj.constructor.name])) {
+ const obj2 = new constructor();
+ for (var key in obj) { var val = obj[key]; obj2[key] = clone(val); }
+ return obj2;
+ } else {
+ return obj;
+ }
+ };
+ 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 (type === 'userscript') { %>
-$.open =
- (GM?.openInTab != null) ?
- GM.openInTab
- : (typeof GM_openInTab !== 'undefined' && GM_openInTab !== null) ?
- GM_openInTab
- :
+if (!globalThis.chrome?.extension) {
+ $.open =
+ (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) {
let lastCall = 0;
@@ -596,8 +575,6 @@ $.engine = (function() {
if (/Gecko\/|Goanna/.test(navigator.userAgent)) { return 'gecko'; } // Goanna = Pale Moon 26+
})();
-$.platform = '<%= type %>';
-
$.hasStorage = (function() {
try {
if (localStorage.getItem(g.NAMESPACE + 'hasStorage') === 'true') { return true; }
@@ -609,7 +586,7 @@ $.hasStorage = (function() {
})();
$.item = function(key, val) {
- const item = $.dict();
+ const item = dict();
item[key] = val;
return item;
};
@@ -622,7 +599,7 @@ $.oneItemSugar = fn => (function(key, val, cb) {
}
});
-$.syncing = $.dict();
+$.syncing = dict();
$.securityCheck = function(data) {
if (location.protocol !== 'https:') {
@@ -630,387 +607,388 @@ $.securityCheck = function(data) {
}
};
-// <% if (type === 'crx') { %>
-// https://developer.chrome.com/extensions/storage.html
-$.oldValue = {
- local: $.dict(),
- sync: $.dict()
-};
-
-chrome.storage.onChanged.addListener(function(changes, area) {
- for (var key in changes) {
- var oldValue = $.oldValue.local[key] ?? $.oldValue.sync[key];
- $.oldValue[area][key] = $.dict.clone(changes[key].newValue);
- var newValue = $.oldValue.local[key] ?? $.oldValue.sync[key];
- var cb = $.syncing[key];
- if (cb && (JSON.stringify(newValue) !== JSON.stringify(oldValue))) {
- cb(newValue, key);
- }
- }
-});
-$.sync = (key, cb) => $.syncing[key] = cb;
-$.forceSync = function() { };
+if (platform === 'crx') {
+ // https://developer.chrome.com/extensions/storage.html
+ $.oldValue = {
+ local: dict(),
+ sync: dict()
+ };
-$.crxWorking = function() {
- try {
- if (chrome.runtime.getManifest()) {
- return true;
+ chrome.storage.onChanged.addListener(function(changes, area) {
+ for (var key in changes) {
+ var oldValue = $.oldValue.local[key] ?? $.oldValue.sync[key];
+ $.oldValue[area][key] = dict.clone(changes[key].newValue);
+ var newValue = $.oldValue.local[key] ?? $.oldValue.sync[key];
+ var cb = $.syncing[key];
+ if (cb && (JSON.stringify(newValue) !== JSON.stringify(oldValue))) {
+ cb(newValue, key);
+ }
}
- } catch (error) {}
- if (!$.crxWarningShown) {
- const msg = $.el('div',
- {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;
- }
- return false;
-};
+ });
+ $.sync = (key, cb) => $.syncing[key] = cb;
+ $.forceSync = function() { };
-$.get = $.oneItemSugar(function(data, cb) {
- if (!$.crxWorking()) { return; }
- const results = {};
- 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) {
- let key;
- result = $.dict.clone(result);
- if (chrome.runtime.lastError) {
- c.error(chrome.runtime.lastError.message);
- }
- if (keys === null) {
- const result2 = $.dict();
- for (key in result) { var val = result[key]; if ($.hasOwn(data, key)) { result2[key] = val; } }
- result = result2;
- }
- for (key in data) {
- $.oldValue[area][key] = result[key];
- }
- results[area] = result;
- if (results.local && results.sync) {
- $.extend(data, results.sync);
- $.extend(data, results.local);
- return cb(data);
+ $.crxWorking = function() {
+ try {
+ if (chrome.runtime.getManifest()) {
+ return true;
}
- });
+ } catch (error) {}
+ if (!$.crxWarningShown) {
+ const msg = $.el('div',
+ {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;
+ }
+ return false;
};
- get('local');
- return get('sync');
-});
-(function() {
- const items = {
- local: $.dict(),
- sync: $.dict()
- };
+ $.get = $.oneItemSugar(function(data, cb) {
+ if (!$.crxWorking()) { return; }
+ const results = {};
+ 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) {
+ let key;
+ result = dict.clone(result);
+ if (chrome.runtime.lastError) {
+ c.error(chrome.runtime.lastError.message);
+ }
+ if (keys === null) {
+ const result2 = dict();
+ for (key in result) { var val = result[key]; if ($.hasOwn(data, key)) { result2[key] = val; } }
+ result = result2;
+ }
+ for (key in data) {
+ $.oldValue[area][key] = result[key];
+ }
+ results[area] = result;
+ if (results.local && results.sync) {
+ $.extend(data, results.sync);
+ $.extend(data, results.local);
+ return cb(data);
+ }
+ });
+ };
+ get('local');
+ return get('sync');
+ });
- 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;
+ (function() {
+ const items = {
+ local: dict(),
+ sync: dict()
+ };
- $.delete = function(keys) {
- if (!$.crxWorking()) { return; }
- if (typeof keys === 'string') {
- keys = [keys];
- }
- for (var key of keys) {
- delete items.local[key];
- delete items.sync[key];
- }
- chrome.storage.local.remove(keys);
- return chrome.storage.sync.remove(keys);
- };
+ 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;
- const timeout = {};
- var 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() {
- let err;
- let key;
- if (err = chrome.runtime.lastError) {
- c.error(err.message);
- setTimeout(setArea, $.MINUTE, area);
- timeout[area] = Date.now() + $.MINUTE;
- return cb?.(err);
+ $.delete = function(keys) {
+ if (!$.crxWorking()) { return; }
+ if (typeof keys === 'string') {
+ keys = [keys];
+ }
+ for (var key of keys) {
+ delete items.local[key];
+ delete items.sync[key];
}
+ chrome.storage.local.remove(keys);
+ return chrome.storage.sync.remove(keys);
+ };
- delete timeout[area];
- for (key in data) { if (items[area][key] === data[key]) { delete items[area][key]; } }
- if (area === 'local') {
- for (key in data) { var val = data[key]; if (!exceedsQuota(key, val)) { items.sync[key] = val; } }
- setSync();
- } else {
- chrome.storage.local.remove(((() => {
- const result = [];
- for (key in data) {
- if (!(key in items.local)) {
- result.push(key);
+ const timeout = {};
+ var 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() {
+ let err;
+ let key;
+ if (err = chrome.runtime.lastError) {
+ c.error(err.message);
+ setTimeout(setArea, MINUTE, area);
+ timeout[area] = Date.now() + MINUTE;
+ return cb?.(err);
+ }
+
+ delete timeout[area];
+ for (key in data) { if (items[area][key] === data[key]) { delete items[area][key]; } }
+ if (area === 'local') {
+ for (key in data) { var val = data[key]; if (!exceedsQuota(key, val)) { items.sync[key] = val; } }
+ setSync();
+ } else {
+ chrome.storage.local.remove(((() => {
+ const result = [];
+ for (key in data) {
+ if (!(key in items.local)) {
+ result.push(key);
+ }
}
- }
- return result;
- })()));
- }
- return cb?.();
- });
- };
+ return result;
+ })()));
+ }
+ return cb?.();
+ });
+ };
- var setSync = $.debounce($.SECOND, () => setArea('sync'));
+ var setSync = debounce(SECOND, () => setArea('sync'));
- $.set = $.oneItemSugar(function(data, cb) {
- if (!$.crxWorking()) { return; }
- $.securityCheck(data);
- $.extend(items.local, data);
- return setArea('local', cb);
- });
+ $.set = $.oneItemSugar(function(data, cb) {
+ if (!$.crxWorking()) { return; }
+ $.securityCheck(data);
+ $.extend(items.local, data);
+ return setArea('local', cb);
+ });
- return $.clear = function(cb) {
- if (!$.crxWorking()) { return; }
- items.local = $.dict();
- items.sync = $.dict();
- let count = 2;
- let err = null;
- const done = function() {
- if (chrome.runtime.lastError) {
- c.error(chrome.runtime.lastError.message);
- }
- if (err == null) { err = chrome.runtime.lastError; }
- if (!--count) { return cb?.(err); }
+ return $.clear = function(cb) {
+ if (!$.crxWorking()) { return; }
+ items.local = dict();
+ items.sync = dict();
+ let count = 2;
+ let err = null;
+ const done = function() {
+ if (chrome.runtime.lastError) {
+ c.error(chrome.runtime.lastError.message);
+ }
+ if (err == null) { err = chrome.runtime.lastError; }
+ if (!--count) { return cb?.(err); }
+ };
+ chrome.storage.local.clear(done);
+ return chrome.storage.sync.clear(done);
};
- chrome.storage.local.clear(done);
- return chrome.storage.sync.clear(done);
- };
-})();
-// <% } else { %>
+ })();
+} else {
-// http://wiki.greasespot.net/Main_Page
-// https://tampermonkey.net/documentation.php
+ // http://wiki.greasespot.net/Main_Page
+ // https://tampermonkey.net/documentation.php
-if ((GM?.deleteValue != null) && window.BroadcastChannel && (typeof GM_addValueChangeListener === 'undefined' || GM_addValueChangeListener === null)) {
+ if ((GM?.deleteValue != null) && window.BroadcastChannel && (typeof GM_addValueChangeListener === 'undefined' || GM_addValueChangeListener === null)) {
- $.syncChannel = new BroadcastChannel(g.NAMESPACE + 'sync');
+ $.syncChannel = new BroadcastChannel(g.NAMESPACE + 'sync');
- $.on($.syncChannel, 'message', e => (() => {
- const result = [];
- for (var key in e.data) {
- var cb;
- var val = e.data[key];
- if (cb = $.syncing[key]) {
- result.push(cb($.dict.json(JSON.stringify(val)), key));
+ $.on($.syncChannel, 'message', e => (() => {
+ const result = [];
+ for (var key in e.data) {
+ var cb;
+ var val = e.data[key];
+ if (cb = $.syncing[key]) {
+ result.push(cb(dict.json(JSON.stringify(val)), key));
+ }
}
- }
- return result;
- })());
+ return result;
+ })());
- $.sync = (key, cb) => $.syncing[key] = cb;
+ $.sync = (key, cb) => $.syncing[key] = cb;
- $.forceSync = function() {};
+ $.forceSync = function() {};
- $.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));
+ $.delete = function(keys, cb) {
+ let key;
+ if (!(keys instanceof Array)) {
+ keys = [keys];
}
- return result;
- })()).then(function() {
- const items = $.dict();
- for (key of keys) { items[key] = undefined; }
- $.syncChannel.postMessage(items);
- return cb?.();
- });
- };
+ return Promise.all((() => {
+ const result = [];
+ for (key of keys) { result.push(GM.deleteValue(g.NAMESPACE + key));
+ }
+ return result;
+ })()).then(function() {
+ const items = dict();
+ for (key of keys) { items[key] = undefined; }
+ $.syncChannel.postMessage(items);
+ return 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) {
- for (let i = 0; i < values.length; i++) {
- var val = values[i];
- if (val) {
- items[keys[i]] = $.dict.json(val);
+ $.get = $.oneItemSugar(function(items, cb) {
+ const keys = Object.keys(items);
+ return Promise.all(keys.map((key) => GM.getValue(g.NAMESPACE + key))).then(function(values) {
+ for (let i = 0; i < values.length; i++) {
+ var val = values[i];
+ if (val) {
+ items[keys[i]] = dict.json(val);
+ }
}
- }
- return cb(items);
+ return cb(items);
+ });
});
- });
- $.set = $.oneItemSugar(function(items, cb) {
- $.securityCheck(items);
- return Promise.all((() => {
- const result = [];
- for (var key in items) {
- var val = items[key];
- result.push(GM.setValue(g.NAMESPACE + key, JSON.stringify(val)));
- }
- return result;
- })()).then(function() {
- $.syncChannel.postMessage(items);
- return cb?.();
+ $.set = $.oneItemSugar(function(items, cb) {
+ $.securityCheck(items);
+ return Promise.all((() => {
+ const result = [];
+ for (var key in items) {
+ var val = items[key];
+ result.push(GM.setValue(g.NAMESPACE + key, JSON.stringify(val)));
+ }
+ return result;
+ })()).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));
-} else {
-
- if (typeof GM_deleteValue === 'undefined' || GM_deleteValue === null) {
- $.perProtocolSettings = true;
- }
- if (typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) {
- $.getValue = GM_getValue;
- $.listValues = () => GM_listValues(); // error when called if missing
- } else if ($.hasStorage) {
- $.getValue = key => localStorage.getItem(key);
- $.listValues = () => (() => {
- const result = [];
- for (var key in localStorage) {
- if (key.slice(0, g.NAMESPACE.length) === g.NAMESPACE) {
- result.push(key);
- }
- }
- return result;
- })();
+ $.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 {
- $.getValue = function() {};
- $.listValues = () => [];
- }
- if (typeof GM_addValueChangeListener !== 'undefined' && GM_addValueChangeListener !== null) {
- $.setValue = GM_setValue;
- $.deleteValue = GM_deleteValue;
- } else if (typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) {
- $.oldValue = $.dict();
- $.setValue = function(key, val) {
- GM_setValue(key, val);
- if (key in $.syncing) {
- $.oldValue[key] = val;
- if ($.hasStorage) { return localStorage.setItem(key, val); } // for `storage` events
- }
- };
- $.deleteValue = function(key) {
- GM_deleteValue(key);
- if (key in $.syncing) {
- delete $.oldValue[key];
- if ($.hasStorage) { return localStorage.removeItem(key); } // for `storage` events
- }
- };
- if (!$.hasStorage) { $.cantSync = true; }
- } else if ($.hasStorage) {
- $.oldValue = $.dict();
- $.setValue = function(key, val) {
- if (key in $.syncing) { $.oldValue[key] = val; }
- return localStorage.setItem(key, val);
- };
- $.deleteValue = function(key) {
- if (key in $.syncing) { delete $.oldValue[key]; }
- return localStorage.removeItem(key);
- };
- } else {
- $.setValue = function() {};
- $.deleteValue = function() {};
- $.cantSync = ($.cantSet = true);
- }
+ if (typeof GM_deleteValue === 'undefined' || GM_deleteValue === null) {
+ $.perProtocolSettings = true;
+ }
- if (typeof GM_addValueChangeListener !== 'undefined' && GM_addValueChangeListener !== null) {
- $.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() {};
- } else if ((typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) || $.hasStorage) {
- $.sync = function(key, cb) {
- key = g.NAMESPACE + key;
- $.syncing[key] = cb;
- return $.oldValue[key] = $.getValue(key);
- };
+ if (typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) {
+ $.getValue = GM_getValue;
+ $.listValues = () => GM_listValues(); // error when called if missing
+ } else if ($.hasStorage) {
+ $.getValue = key => localStorage.getItem(key);
+ $.listValues = () => (() => {
+ const result = [];
+ for (var key in localStorage) {
+ if (key.slice(0, g.NAMESPACE.length) === g.NAMESPACE) {
+ result.push(key);
+ }
+ }
+ return result;
+ })();
+ } else {
+ $.getValue = function() {};
+ $.listValues = () => [];
+ }
- (function() {
- const onChange = function({key, newValue}) {
- let cb;
- if (!(cb = $.syncing[key])) { return; }
- if (newValue != null) {
- if (newValue === $.oldValue[key]) { return; }
- $.oldValue[key] = newValue;
- return cb($.dict.json(newValue), key.slice(g.NAMESPACE.length));
- } else {
- if ($.oldValue[key] == null) { return; }
+ if (typeof GM_addValueChangeListener !== 'undefined' && GM_addValueChangeListener !== null) {
+ $.setValue = GM_setValue;
+ $.deleteValue = GM_deleteValue;
+ } else if (typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) {
+ $.oldValue = dict();
+ $.setValue = function(key, val) {
+ GM_setValue(key, val);
+ if (key in $.syncing) {
+ $.oldValue[key] = val;
+ if ($.hasStorage) { return localStorage.setItem(key, val); } // for `storage` events
+ }
+ };
+ $.deleteValue = function(key) {
+ GM_deleteValue(key);
+ if (key in $.syncing) {
delete $.oldValue[key];
- return cb(undefined, key.slice(g.NAMESPACE.length));
+ if ($.hasStorage) { return localStorage.removeItem(key); } // for `storage` events
}
};
- $.on(window, 'storage', onChange);
+ if (!$.hasStorage) { $.cantSync = true; }
+ } else if ($.hasStorage) {
+ $.oldValue = dict();
+ $.setValue = function(key, val) {
+ if (key in $.syncing) { $.oldValue[key] = val; }
+ return localStorage.setItem(key, val);
+ };
+ $.deleteValue = function(key) {
+ if (key in $.syncing) { delete $.oldValue[key]; }
+ return localStorage.removeItem(key);
+ };
+ } else {
+ $.setValue = function() {};
+ $.deleteValue = function() {};
+ $.cantSync = ($.cantSet = true);
+ }
- 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.
+ if (typeof GM_addValueChangeListener !== 'undefined' && GM_addValueChangeListener !== null) {
+ $.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() {};
+ } else if ((typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) || $.hasStorage) {
+ $.sync = function(key, cb) {
key = g.NAMESPACE + key;
- return onChange({key, newValue: $.getValue(key)});
+ $.syncing[key] = cb;
+ return $.oldValue[key] = $.getValue(key);
};
- })();
- } else {
- $.sync = function() {};
- $.forceSync = function() {};
- }
- $.delete = function(keys) {
- if (!(keys instanceof Array)) {
- keys = [keys];
- }
- for (var key of keys) {
- $.deleteValue(g.NAMESPACE + key);
+ (function() {
+ const onChange = function({key, newValue}) {
+ let cb;
+ if (!(cb = $.syncing[key])) { return; }
+ if (newValue != null) {
+ if (newValue === $.oldValue[key]) { return; }
+ $.oldValue[key] = newValue;
+ return cb(dict.json(newValue), key.slice(g.NAMESPACE.length));
+ } else {
+ if ($.oldValue[key] == null) { return; }
+ delete $.oldValue[key];
+ return cb(undefined, key.slice(g.NAMESPACE.length));
+ }
+ };
+ $.on(window, 'storage', onChange);
+
+ 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)});
+ };
+ })();
+ } else {
+ $.sync = function() {};
+ $.forceSync = function() {};
}
- };
- $.get = $.oneItemSugar((items, cb) => $.queueTask($.getSync, items, cb));
-
- $.getSync = function(items, cb) {
- for (var key in items) {
- var val2;
- if (val2 = $.getValue(g.NAMESPACE + key)) {
- try {
- items[key] = $.dict.json(val2);
- } catch (err) {
- // XXX https://github.com/ccd0/4chan-x/issues/2218
- if (!/^(?:undefined)*$/.test(val2)) {
- throw err;
- }
- }
+ $.delete = function(keys) {
+ if (!(keys instanceof Array)) {
+ keys = [keys];
}
- }
- return cb(items);
- };
+ for (var key of keys) {
+ $.deleteValue(g.NAMESPACE + key);
+ }
+ };
- $.set = $.oneItemSugar(function(items, cb) {
- $.securityCheck(items);
- return $.queueTask(function() {
+ $.get = $.oneItemSugar((items, cb) => $.queueTask($.getSync, items, cb));
+
+ $.getSync = function(items, cb) {
for (var key in items) {
- var value = items[key];
- $.setValue(g.NAMESPACE + key, JSON.stringify(value));
+ var val2;
+ if (val2 = $.getValue(g.NAMESPACE + key)) {
+ try {
+ items[key] = dict.json(val2);
+ } catch (err) {
+ // XXX https://github.com/ccd0/4chan-x/issues/2218
+ if (!/^(?:undefined)*$/.test(val2)) {
+ throw err;
+ }
+ }
+ }
}
- return cb?.();
+ return cb(items);
+ };
+
+ $.set = $.oneItemSugar(function(items, cb) {
+ $.securityCheck(items);
+ return $.queueTask(function() {
+ for (var key in items) {
+ var value = items[key];
+ $.setValue(g.NAMESPACE + key, JSON.stringify(value));
+ }
+ return 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) {}
- return 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) {}
+ return cb?.();
+ };
+ }
}
-// <% } %>
+export default $;
diff --git a/src/platform/CrossOrigin.js b/src/platform/CrossOrigin.js
index d025550698..18af394793 100644
--- a/src/platform/CrossOrigin.js
+++ b/src/platform/CrossOrigin.js
@@ -1,3 +1,7 @@
+import QR from "../Posting/QR";
+import $ from "./$";
+import { dict, platform } from "./helpers";
+
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
@@ -6,80 +10,78 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
-// <% if (type === 'crx') { %>
-let Request;
-const eventPageRequest = (function() {
- const callbacks = [];
- chrome.runtime.onMessage.addListener(function(response) {
- callbacks[response.id](response.data);
- return delete callbacks[response.id];});
- return (params, cb) => chrome.runtime.sendMessage(params, id => callbacks[id] = cb);
-})();
-
-// <% } %>
+let eventPageRequest;
+if (platform === 'crx') {
+ eventPageRequest = (function () {
+ const callbacks = [];
+ chrome.runtime.onMessage.addListener(function(response) {
+ callbacks[response.id](response.data);
+ return delete callbacks[response.id];});
+ return (params, cb) => chrome.runtime.sendMessage(params, id => callbacks[id] = cb);
+ })();
+}
var CrossOrigin = {
- binary(url, cb, headers=$.dict()) {
+ binary(url, cb, headers = dict()) {
// XXX https://forums.lanik.us/viewtopic.php?f=64&t=24173&p=78310
url = url.replace(/^((?:https?:)?\/\/(?:\w+\.)?(?:4chan|4channel|4cdn)\.org)\/adv\//, '$1//adv/');
- // <% if (type === 'crx') { %>
+ if (platform === 'crx') {
eventPageRequest({type: 'ajax', url, headers, responseType: 'arraybuffer'}, function({response, responseHeaderString}) {
if (response) { response = new Uint8Array(response); }
return cb(response, responseHeaderString);
});
- // <% } %>
- // <% if (type === 'userscript') { %>
- const fallback = function() {
- return $.ajax(url, {
+ } else {
+ const fallback = function() {
+ return $.ajax(url, {
+ headers,
+ responseType: 'arraybuffer',
+ onloadend() {
+ if (this.status && this.response) {
+ return cb(new Uint8Array(this.response), this.getAllResponseHeaders());
+ } else {
+ return cb(null);
+ }
+ }
+ });
+ };
+ if ((typeof window.GM_xmlhttpRequest === 'undefined' || window.GM_xmlhttpRequest === null)) {
+ fallback();
+ return;
+ }
+ const gmOptions = {
+ method: "GET",
+ url,
headers,
responseType: 'arraybuffer',
- onloadend() {
- if (this.status && this.response) {
- return cb(new Uint8Array(this.response), this.getAllResponseHeaders());
+ overrideMimeType: 'text/plain; charset=x-user-defined',
+ onload(xhr) {
+ let data;
+ if (xhr.response instanceof ArrayBuffer) {
+ data = new Uint8Array(xhr.response);
} else {
- return cb(null);
- }
- }
- });
- };
- if ((GM?.xmlHttpRequest == null) && (typeof GM_xmlhttpRequest === 'undefined' || GM_xmlhttpRequest === null)) {
- fallback();
- return;
- }
- const gmOptions = {
- method: "GET",
- url,
- headers,
- responseType: 'arraybuffer',
- overrideMimeType: 'text/plain; charset=x-user-defined',
- onload(xhr) {
- let data;
- if (xhr.response instanceof ArrayBuffer) {
- data = new Uint8Array(xhr.response);
- } else {
- const r = xhr.responseText;
- data = new Uint8Array(r.length);
- let i = 0;
- while (i < r.length) {
- data[i] = r.charCodeAt(i);
- i++;
+ const r = xhr.responseText;
+ data = new Uint8Array(r.length);
+ let i = 0;
+ while (i < r.length) {
+ data[i] = r.charCodeAt(i);
+ i++;
+ }
}
+ return cb(data, xhr.responseHeaders);
+ },
+ onerror() {
+ return cb(null);
+ },
+ onabort() {
+ return cb(null);
}
- return cb(data, xhr.responseHeaders);
- },
- onerror() {
- return cb(null);
- },
- onabort() {
- return cb(null);
+ };
+ try {
+ return (GM?.xmlHttpRequest || GM_xmlhttpRequest)(gmOptions);
+ } catch (error) {
+ return fallback();
}
- };
- try {
- return (GM?.xmlHttpRequest || GM_xmlhttpRequest)(gmOptions);
- } catch (error) {
- return fallback();
}
},
- // <% } %>
file(url, cb) {
return CrossOrigin.binary(url, function(data, headers) {
@@ -104,8 +106,8 @@ var CrossOrigin = {
});
},
- Request: (Request = (function() {
- Request = class Request {
+ Request: (function() {
+ const Request = class Request {
static initClass() {
this.prototype.status = 0;
this.prototype.statusText = '';
@@ -114,7 +116,7 @@ var CrossOrigin = {
}
getResponseHeader(headerName) {
if ((this.responseHeaders == null) && (this.responseHeaderString != null)) {
- this.responseHeaders = $.dict();
+ this.responseHeaders = dict();
for (var header of this.responseHeaderString.split('\r\n')) {
var i;
if ((i = header.indexOf(':')) >= 0) {
@@ -131,7 +133,7 @@ var CrossOrigin = {
};
Request.initClass();
return Request;
- })()),
+ })(),
// Attempts to fetch `url` using cross-origin privileges, if available.
// Interface is a subset of that of $.ajax.
@@ -151,65 +153,61 @@ var CrossOrigin = {
let {onloadend, timeout, responseType, headers} = options;
if (responseType == null) { responseType = 'json'; }
- // <% if (type === 'userscript') { %>
- if ((GM?.xmlHttpRequest == null) && (typeof GM_xmlhttpRequest === 'undefined' || GM_xmlhttpRequest === null)) {
+ if ((window.GM?.xmlHttpRequest == null) && (typeof window.GM_xmlhttpRequest === 'undefined' || window.GM_xmlhttpRequest === null)) {
return $.ajax(url, options);
}
- // <% } %>
const req = new CrossOrigin.Request();
req.onloadend = onloadend;
- // <% if (type === 'userscript') { %>
- const gmOptions = {
- method: 'GET',
- url,
- headers,
- timeout,
- onload(xhr) {
- try {
- const response = (() => { switch (responseType) {
- case 'json':
- if (xhr.responseText) { return JSON.parse(xhr.responseText); } else { return null; }
- default:
- return xhr.responseText;
- } })();
- $.extend(req, {
- response,
- status: xhr.status,
- statusText: xhr.statusText,
- responseHeaderString: xhr.responseHeaders
- });
- } catch (error) {}
- return req.onloadend();
- },
- onerror() { return req.onloadend(); },
- onabort() { return req.onloadend(); },
- ontimeout() { return req.onloadend(); }
- };
- try {
- gmReq = (GM?.xmlHttpRequest || GM_xmlhttpRequest)(gmOptions);
- } catch (error) {
- return $.ajax(url, options);
- }
-
- if (gmReq && (typeof gmReq.abort === 'function')) {
- req.abort = function() {
- try {
- return gmReq.abort();
- } catch (error1) {}
+ if (platform === 'userscript') {
+ const gmOptions = {
+ method: 'GET',
+ url,
+ headers,
+ timeout,
+ onload(xhr) {
+ try {
+ const response = (() => { switch (responseType) {
+ case 'json':
+ if (xhr.responseText) { return JSON.parse(xhr.responseText); } else { return null; }
+ default:
+ return xhr.responseText;
+ } })();
+ $.extend(req, {
+ response,
+ status: xhr.status,
+ statusText: xhr.statusText,
+ responseHeaderString: xhr.responseHeaders
+ });
+ } catch (error) {}
+ return req.onloadend();
+ },
+ onerror() { return req.onloadend(); },
+ onabort() { return req.onloadend(); },
+ ontimeout() { return req.onloadend(); }
};
- }
- // <% } %>
+ try {
+ gmReq = (GM?.xmlHttpRequest || GM_xmlhttpRequest)(gmOptions);
+ } catch (error) {
+ return $.ajax(url, options);
+ }
- // <% if (type === 'crx') { %>
- eventPageRequest({type: 'ajax', url, responseType, headers, timeout}, function(result) {
- if (result.status) {
- $.extend(req, result);
+ if (gmReq && (typeof gmReq.abort === 'function')) {
+ req.abort = function() {
+ try {
+ return gmReq.abort();
+ } catch (error1) {}
+ };
}
- return req.onloadend();
- });
- // <% } %>
+ } else {
+ eventPageRequest({type: 'ajax', url, responseType, headers, timeout}, function(result) {
+ if (result.status) {
+ $.extend(req, result);
+ }
+ return req.onloadend();
+ });
+ }
return req;
},
@@ -219,20 +217,17 @@ var CrossOrigin = {
{ajax: CrossOrigin.ajax});
},
- // <% if (type === 'crx') { %>
permission(cb, cbFail, origins) {
- return eventPageRequest({type: 'permission', origins}, function(result) {
- if (result) {
- return cb();
- } else {
- return cbFail();
- }
- });
- },
- // <% } %>
- // <% if (type === 'userscript') { %>
- permission(cb) {
+ if (platform === 'crx') {
+ return eventPageRequest({type: 'permission', origins}, function(result) {
+ if (result) {
+ return cb();
+ } else {
+ return cbFail();
+ }
+ });
+ }
return cb();
- }
+ },
};
- // <% } %>
+export default CrossOrigin;
diff --git a/src/platform/helpers.ts b/src/platform/helpers.ts
new file mode 100644
index 0000000000..63db5aa6b2
--- /dev/null
+++ b/src/platform/helpers.ts
@@ -0,0 +1,54 @@
+// This file was created because these functions on $ were sometimes not initialized yet because of circular
+// dependencies, so try to keep this file without dependencies, so these functions don't have to wait for something else
+
+export const debounce = (wait: number, fn: Function) => {
+ let lastCall = 0;
+ let timeout = null;
+ let that = null;
+ let args = null;
+ const exec = function () {
+ lastCall = Date.now();
+ return fn.apply(that, args);
+ };
+ return function () {
+ args = arguments;
+ that = this;
+ if (lastCall < (Date.now() - wait)) {
+ return exec();
+ }
+ // stop current reset
+ clearTimeout(timeout);
+ // after wait, let next invocation execute immediately
+ return timeout = setTimeout(exec, wait);
+ };
+};
+
+export const dict = () => Object.create(null);
+
+dict.clone = function (obj) {
+ if ((typeof obj !== 'object') || (obj === null)) {
+ return obj;
+ } else if (obj instanceof Array) {
+ const arr = [];
+ for (let i = 0, end = obj.length; i < end; i++) {
+ arr.push(dict.clone(obj[i]));
+ }
+ return arr;
+ } else {
+ const map = Object.create(null);
+ for (var key in obj) {
+ var val = obj[key];
+ map[key] = dict.clone(val);
+ }
+ return map;
+ }
+};
+
+dict.json = (str: string) => dict.clone(JSON.parse(str));
+
+export const SECOND = 1000;
+export const MINUTE = SECOND * 60;
+export const HOUR = MINUTE * 60;
+export const DAY = HOUR * 24;
+
+export const platform = window.GM_xmlhttpRequest ? 'userscript' : 'crx';
diff --git a/src/site/SW.js b/src/site/SW.js
index 120a971ba3..f5f8adbd33 100644
--- a/src/site/SW.js
+++ b/src/site/SW.js
@@ -1 +1,5 @@
-SW = {};
+import SWTinyboard from "./SW.tinyboard";
+import SWYotsuba from "./SW.yotsuba";
+
+const SW = { tinyboard: SWTinyboard, yotsuba: SWYotsuba };
+export default SW;
diff --git a/src/site/SW.tinyboard.js b/src/site/SW.tinyboard.js
index d7e2b978b0..89e3985408 100644
--- a/src/site/SW.tinyboard.js
+++ b/src/site/SW.tinyboard.js
@@ -1,9 +1,15 @@
+import { Conf, d } from "../globals/globals";
+import Main from "../main/Main";
+import $ from "../platform/$";
+import $$ from "../platform/$$";
+import { dict } from "../platform/helpers";
+
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
-SW.tinyboard = {
+const SWTinyboard = {
isOPContainerThread: true,
mayLackJSON: true,
threadModTimeIgnoresSage: true,
@@ -36,7 +42,7 @@ SW.tinyboard = {
for (var script of $$('script:not([src])', d.head)) {
var m;
if (m = script.textContent.match(/\bvar configRoot=(".*?")/)) {
- var properties = $.dict();
+ var properties = dict();
try {
var root = JSON.parse(m[1]);
if (root[0] === '/') {
@@ -75,7 +81,7 @@ SW.tinyboard = {
if (root) { return `${root}${boardID}/${isArchived ? 'archive/' : ''}res/${threadID}.json`; } else { return ''; }
},
archivedThreadJSON(thread) {
- return SW.tinyboard.urls.threadJSON(thread, true);
+ return SWTinyboard.urls.threadJSON(thread, true);
},
threadsListJSON({siteID, boardID}) {
const root = Conf['siteProperties'][siteID]?.root;
@@ -93,7 +99,7 @@ SW.tinyboard = {
return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/${filename}`;
},
thumb(board, filename) {
- return SW.tinyboard.urls.file(board, filename);
+ return SWTinyboard.urls.file(board, filename);
}
},
@@ -180,7 +186,7 @@ $\
Build: {
parseJSON(data, board) {
- const o = SW.yotsuba.Build.parseJSON(data, board);
+ const o = this.parseJSON(data, board);
if (data.ext === 'deleted') {
delete o.file;
$.extend(o, {
@@ -196,7 +202,7 @@ $\
if (extra_file.ext === 'deleted') {
o.filesDeleted.push(i);
} else {
- file = SW.yotsuba.Build.parseJSONFile(data, board);
+ file = this.parseJSONFile(data, board);
o.files.push(file);
}
}
@@ -296,3 +302,4 @@ $\
return threadRoot.dataset.sticky = 'true';
}
};
+export default SWTinyboard;
diff --git a/src/site/SW.yotsuba.Build.js b/src/site/SW.yotsuba.Build.js
deleted file mode 100644
index 655ca0a62b..0000000000
--- a/src/site/SW.yotsuba.Build.js
+++ /dev/null
@@ -1,346 +0,0 @@
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
- */
-import PostInfoPage from './SW.yotsuba.Build/PostInfo.html';
-import FilePage from './SW.yotsuba.Build/File.html';
-import PostPage from './SW.yotsuba.Build/Post.html';
-import CatalogThreadPage from './SW.yotsuba.Build/CatalogThread.html';
-import CatalogReplyPage from './SW.yotsuba.Build/CatalogReply.html';
-
-var Build = {
- staticPath: '//s.4cdn.org/image/',
- gifIcon: window.devicePixelRatio >= 2 ? '@2x.gif' : '.gif',
- spoilerRange: $.dict(),
-
- shortFilename(filename) {
- const ext = filename.match(/\.?[^\.]*$/)[0];
- if ((filename.length - ext.length) > 30) {
- return `${filename.match(/(?:[\uD800-\uDBFF][\uDC00-\uDFFF]|[^]){0,25}/)[0]}(...)${ext}`;
- } else {
- return filename;
- }
- },
-
- spoilerThumb(boardID) {
- let spoilerRange;
- if ((spoilerRange = Build.spoilerRange[boardID])) {
- // Randomize the spoiler image.
- return `${Build.staticPath}spoiler-${boardID}${Math.floor(1 + (spoilerRange * Math.random()))}.png`;
- } else {
- return `${Build.staticPath}spoiler.png`;
- }
- },
-
- sameThread(boardID, threadID) {
- return (g.VIEW === 'thread') && (g.BOARD.ID === boardID) && (g.THREADID === +threadID);
- },
-
- threadURL(boardID, threadID) {
- if (boardID !== g.BOARD.ID) {
- return `//${BoardConfig.domain(boardID)}/${boardID}/thread/${threadID}`;
- } else if ((g.VIEW !== 'thread') || (+threadID !== g.THREADID)) {
- return `/${boardID}/thread/${threadID}`;
- } else {
- return '';
- }
- },
-
- postURL(boardID, threadID, postID) {
- return `${Build.threadURL(boardID, threadID)}#p${postID}`;
- },
-
- parseJSON(data, {siteID, boardID}) {
- const o = {
- // id
- ID: data.no,
- postID: data.no,
- threadID: data.resto || data.no,
- boardID,
- siteID,
- isReply: !!data.resto,
- // thread status
- isSticky: !!data.sticky,
- isClosed: !!data.closed,
- isArchived: !!data.archived,
- // file status
- fileDeleted: !!data.filedeleted,
- filesDeleted: data.filedeleted ? [0] : []
- };
- o.info = {
- subject: $.unescape(data.sub),
- email: $.unescape(data.email),
- name: $.unescape(data.name) || '',
- tripcode: data.trip,
- pass: (data.since4pass != null) ? `${data.since4pass}` : undefined,
- uniqueID: data.id,
- flagCode: data.country,
- flagCodeTroll: data.board_flag,
- flag: $.unescape((data.country_name || data.flag_name)),
- dateUTC: data.time,
- dateText: data.now,
- commentHTML: {innerHTML: data.com || ''}
- };
- if (data.capcode) {
- o.info.capcode = data.capcode.replace(/_highlight$/, '').replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
- o.capcodeHighlight = /_highlight$/.test(data.capcode);
- delete o.info.uniqueID;
- }
- o.files = [];
- if (data.ext) {
- o.file = SW.yotsuba.Build.parseJSONFile(data, {siteID, boardID});
- o.files.push(o.file);
- }
- // Temporary JSON properties for events such as April 1 / Halloween
- o.extra = $.dict();
- for (var key in data) {
- if (key[0] === 'x') {
- o.extra[key] = data[key];
- }
- }
- return o;
- },
-
- parseJSONFile(data, {siteID, boardID}) {
- const site = g.sites[siteID];
- const filename = (site.software === 'yotsuba') && (boardID === 'f') ?
- `${encodeURIComponent(data.filename)}${data.ext}`
- :
- `${data.tim}${data.ext}`;
- const o = {
- name: ($.unescape(data.filename)) + data.ext,
- url: site.urls.file({siteID, boardID}, filename),
- height: data.h,
- width: data.w,
- MD5: data.md5,
- size: $.bytesToString(data.fsize),
- thumbURL: site.urls.thumb({siteID, boardID}, `${data.tim}s.jpg`),
- theight: data.tn_h,
- twidth: data.tn_w,
- isSpoiler: !!data.spoiler,
- tag: data.tag,
- hasDownscale: !!data.m_img
- };
- if ((data.h != null) && !/\.pdf$/.test(o.url)) { o.dimensions = `${o.width}x${o.height}`; }
- return o;
- },
-
- parseComment(html) {
- html = html
- .replace(/ /gi, '\n')
- .replace(/\n\n]*>/g, '');
- return $.unescape(html);
- },
-
- parseCommentDisplay(html) {
- // Hide spoilers.
- if (!Conf['Remove Spoilers'] && !Conf['Reveal Spoilers']) {
- let html2;
- while ((html2 = html.replace(/(?:(?!<\/?s>).)*<\/s>/g, '[spoiler]')) !== html) {
- html = html2;
- }
- }
- html = html
- .replace(/^Rolled [^<]*<\/b>/i, '') // Rolls (/tg/, /qst/)
- .replace(/ 1 ? 's' : ''}`;
- if (+files) { text += ` and ${files} image repl${files > 1 ? 'ies' : 'y'}`; }
- return text += ` ${status === '-' ? 'shown' : 'omitted'}.`;
- },
-
- summary(boardID, threadID, posts, files) {
- return $.el('a', {
- className: 'summary',
- textContent: Build.summaryText('', posts, files),
- href: `/${boardID}/thread/${threadID}`
- }
- );
- },
-
- thread(thread, data, withReplies) {
- let root;
- if (root = thread.nodes.root) {
- $.rmAll(root);
- } else {
- thread.nodes.root = (root = $.el('div', {
- className: 'thread',
- id: `t${data.no}`
- }
- ));
- }
- if (Build.hat) { $.add(root, Build.hat.cloneNode(false)); }
- $.add(root, thread.OP.nodes.root);
- if (data.omitted_posts || (!withReplies && data.replies)) {
- const [posts, files] = Array.from(withReplies ?
- // XXX data.omitted_images is not accurate.
- [data.omitted_posts, data.images - data.last_replies.filter(data => !!data.ext).length]
- :
- [data.replies, data.images]);
- const summary = Build.summary(thread.board.ID, data.no, posts, files);
- $.add(root, summary);
- }
- return root;
- },
-
- catalogThread(thread, data, pageCount) {
- let cssText, imgClass, src;
- const {staticPath, gifIcon} = Build;
- const {tn_w, tn_h} = data;
-
- if (data.spoiler && !Conf['Reveal Spoiler Thumbnails']) {
- let spoilerRange;
- src = `${staticPath}spoiler`;
- if (spoilerRange = Build.spoilerRange[thread.board]) {
- // Randomize the spoiler image.
- src += (`-${thread.board}`) + Math.floor(1 + (spoilerRange * Math.random()));
- }
- src += '.png';
- imgClass = 'spoiler-file';
- cssText = "--tn-w: 100; --tn-h: 100;";
- } else if (data.filedeleted) {
- src = `${staticPath}filedeleted-res${gifIcon}`;
- imgClass = 'deleted-file';
- } else if (thread.OP.file) {
- src = thread.OP.file.thumbURL;
- const ratio = 250 / Math.max(tn_w, tn_h);
- cssText = `--tn-w: ${tn_w * ratio}; --tn-h: ${tn_h * ratio};`;
- } else {
- src = `${staticPath}nofile.png`;
- imgClass = 'no-file';
- }
-
- const postCount = data.replies + 1;
- const fileCount = data.images + !!data.ext;
-
- const container = $.el('div', { innerHTML: CatalogThreadPage });
- $.before(thread.OP.nodes.info, [...Array.from(container.childNodes)]);
-
- for (var br of $$('br', thread.OP.nodes.comment)) {
- if (br.previousSibling && (br.previousSibling.nodeName === 'BR')) {
- $.addClass(br, 'extra-linebreak');
- }
- }
-
- const root = $.el('div', {
- className: 'thread catalog-thread',
- id: `t${thread}`
- }
- );
- if (thread.OP.highlights) { $.addClass(root, ...Array.from(thread.OP.highlights)); }
- if (!thread.OP.file) { $.addClass(root, 'noFile'); }
- root.style.cssText = cssText || '';
-
- return root;
- },
-
- catalogReply(thread, data) {
- let excerpt = '';
- if (data.com) {
- excerpt = Build.parseCommentDisplay(data.com).replace(/>>\d+/g, '').trim().replace(/\n+/g, ' // ');
- }
- if (data.ext) {
- if (!excerpt) { excerpt = `${$.unescape(data.filename)}${data.ext}`; }
- }
- if (data.com) {
- if (!excerpt) { excerpt = $.unescape(data.com.replace(/ /gi, ' // ')); }
- }
- if (!excerpt) { excerpt = '\xA0'; }
- if (excerpt.length > 73) { excerpt = `${excerpt.slice(0, 70)}...`; }
-
- const link = Build.postURL(thread.board.ID, thread.ID, data.no);
- return $.el('div', {className: 'catalog-reply'},
- { innerHTML: CatalogReplyPage });
- }
-};
-
-SW.yotsuba.Build = Build;
diff --git a/src/site/SW.yotsuba.Build/CatalogReply.html b/src/site/SW.yotsuba.Build/CatalogReply.html
deleted file mode 100644
index 6bd68c0139..0000000000
--- a/src/site/SW.yotsuba.Build/CatalogReply.html
+++ /dev/null
@@ -1,3 +0,0 @@
-... :
-${excerpt}
-...
diff --git a/src/site/SW.yotsuba.Build/CatalogThread.html b/src/site/SW.yotsuba.Build/CatalogThread.html
deleted file mode 100644
index 70aacb3baa..0000000000
--- a/src/site/SW.yotsuba.Build/CatalogThread.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
- ${postCount} /
- ${fileCount} /
- ${pageCount}
-
-
- ?{thread.isSticky}{ }
- ?{thread.isClosed}{ }
-
-
diff --git a/src/site/SW.yotsuba.Build/CatalogThreadHtml.tsx b/src/site/SW.yotsuba.Build/CatalogThreadHtml.tsx
new file mode 100644
index 0000000000..43f5be7271
--- /dev/null
+++ b/src/site/SW.yotsuba.Build/CatalogThreadHtml.tsx
@@ -0,0 +1,25 @@
+import h, { hFragment, EscapedHtml } from "../../globals/jsx";
+
+export default function generateCatalogThreadHtml(
+ thread, src, imgClass, data, postCount, fileCount, pageCount, staticPath, gifIcon,
+): EscapedHtml {
+ return <>
+
+ {imgClass ?
+ :
+
+ }
+
+
+
+ {postCount} {' / '}
+ {fileCount} {' / '}
+ {pageCount}
+
+
+ {thread.isSticky ? : ''}
+ {thread.isClosed ? : ''}
+
+
+ >;
+}
diff --git a/src/site/SW.yotsuba.Build/File.html b/src/site/SW.yotsuba.Build/File.html
deleted file mode 100644
index 504ae42946..0000000000
--- a/src/site/SW.yotsuba.Build/File.html
+++ /dev/null
@@ -1,36 +0,0 @@
-?{file}{
-
-}{
- ?{o.fileDeleted}{
-
-
-
-
-
- }
-}
diff --git a/src/site/SW.yotsuba.Build/FileHtml.tsx b/src/site/SW.yotsuba.Build/FileHtml.tsx
new file mode 100644
index 0000000000..ce08287887
--- /dev/null
+++ b/src/site/SW.yotsuba.Build/FileHtml.tsx
@@ -0,0 +1,49 @@
+import h, { EscapedHtml, isEscaped } from "../../globals/jsx";
+
+export default function generateFileHtml(
+ file, ID, boardID, fileURL, shortFilename, fileThumb, o, staticPath, gifIcon
+): EscapedHtml {
+ if (file) {
+ const fileContent: (EscapedHtml | string)[] = [];
+ if (boardID === "f") {
+ fileContent.push(
+
+ {'File: '}
+ {file.name}
+ -({file.size}, {file.dimensions}{file.tag ? ', ' + file.tag : ''})
+
+ );
+ } else {
+ fileContent.push(
+
+ {'File: '}
+
+ {file.isSpoiler ? 'Spoiler Image' : shortFilename}
+
+ {` (${file.size}, ${file.dimensions || "PDF"})`}
+
,
+
+
+
+ );
+ }
+ return {...fileContent}
;
+ } else if (o.fileDeleted) {
+ return
+
+
+
+
;
+ }
+ return { innerHTML: '', [isEscaped]: true };
+}
diff --git a/src/site/SW.yotsuba.Build/Post.html b/src/site/SW.yotsuba.Build/Post.html
deleted file mode 100644
index 655c4ea45e..0000000000
--- a/src/site/SW.yotsuba.Build/Post.html
+++ /dev/null
@@ -1,5 +0,0 @@
-?{o.isReply}{>>
}
-
- ?{o.isReply}{&{postInfo}&{fileBlock}}{&{fileBlock}&{postInfo}}
-
&{commentHTML}
-
diff --git a/src/site/SW.yotsuba.Build/PostInfo.html b/src/site/SW.yotsuba.Build/PostInfo.html
deleted file mode 100644
index 9fc75a063c..0000000000
--- a/src/site/SW.yotsuba.Build/PostInfo.html
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
- ?{!o.isReply || boardID === "f" || subject}{
${subject || ""} }
-
- ?{email}{}
- ${name}
- ?{tripcode}{ ${tripcode} }
- ?{pass}{ }
- ?{capcode}{ ## ${capcode} }
- ?{email}{ }
- ?{boardID === "f" && !o.isReply || capcodeDescription}{}{ }
- ?{capcodeDescription}{ }
- ?{uniqueID && !capcode}{ (ID: ${uniqueID} ) }
- ?{flagCode}{ }
- ?{flagCodeTroll}{ }
-
-
${dateText}
-
- No.
- ${ID}
- ?{o.isSticky}{ }
- ?{o.isClosed && !o.isArchived}{ }
- ?{o.isArchived}{ }
- ?{!o.isReply && g.VIEW === "index"}{ [Reply ] }
-
-
diff --git a/src/site/SW.yotsuba.Build/PostInfoHtml.tsx b/src/site/SW.yotsuba.Build/PostInfoHtml.tsx
new file mode 100644
index 0000000000..c9a1c554fe
--- /dev/null
+++ b/src/site/SW.yotsuba.Build/PostInfoHtml.tsx
@@ -0,0 +1,91 @@
+import { g } from "../../globals/globals";
+import h, { EscapedHtml } from "../../globals/jsx";
+
+export default function generatePostInfoHtml(
+ ID, o, subject, capcode, email, name, tripcode, pass, capcodeLC, capcodePlural, staticPath, gifIcon,
+ capcodeDescription, uniqueID, flag, flagCode, flagCodeTroll, dateUTC, dateText, postLink, quoteLink, boardID,
+ threadID,
+): EscapedHtml {
+ const nameHtml: (EscapedHtml | string)[] = [{name} ];
+ if (tripcode) nameHtml.push(' ', {tripcode} );
+ if (pass) nameHtml.push(' ', )
+ if (capcode) {
+ nameHtml.push(
+ ' ',
+ ## {capcode}
+ )
+ }
+
+ const nameBlockContent: (EscapedHtml | string)[] =
+ email ? [' ', {...nameHtml} ] : nameHtml;
+ if (!(boardID === "f" && !o.isReply || capcodeDescription)) nameBlockContent.push(' ');
+ if (capcodeDescription) {
+ nameBlockContent.push(
+
+ );
+ if (uniqueID && !capcode) {
+ nameBlockContent.push(
+
+ (ID: ${uniqueID} )
+
+ )
+ }
+ }
+ if (flagCode) nameBlockContent.push(' ', );
+ if (flagCodeTroll) nameBlockContent.push(' ', );
+
+ const postNumContent: (EscapedHtml | string)[] = [
+ No. ,
+ {ID} ,
+ ];
+
+ if (o.isSticky) {
+ const src = `${staticPath}sticky${gifIcon}`;
+ postNumContent.push(' ');
+ if (boardID === "f") {
+ postNumContent.push( );
+ } else {
+ postNumContent.push( )
+ }
+ }
+ if (o.isClosed && !o.isArchived) {
+ postNumContent.push(' ');
+ const src = `${staticPath}closed${gifIcon}`
+ if (boardID === "f") {
+ postNumContent.push( )
+ } else {
+ postNumContent.push( )
+ }
+ }
+ if (o.isArchived) {
+ postNumContent.push(
+ ' ',
+
+ )
+ }
+ if (!o.isReply && g.VIEW === "index") {
+ // \u00A0 is nbsp
+ postNumContent.push(' \u00A0 ')
+ postNumContent.push([Reply ] )
+ }
+
+ return
+
+ {' '}
+ {...((!o.isReply || boardID === "f" || subject) ? [{subject} , ' '] : [])}
+
+ {...nameBlockContent}
+
+ {' '}
+ {dateText}
+ {' '}
+
+ {...postNumContent}
+
+
;
+}
diff --git a/src/site/SW.yotsuba.js b/src/site/SW.yotsuba.js
deleted file mode 100644
index 0917395135..0000000000
--- a/src/site/SW.yotsuba.js
+++ /dev/null
@@ -1,349 +0,0 @@
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS205: Consider reworking code to avoid use of IIFEs
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
- */
-SW.yotsuba = {
- isOPContainerThread: false,
- hasIPCount: true,
- archivedBoardsKnown: true,
-
- urls: {
- thread({boardID, threadID}) { return `${location.protocol}//${BoardConfig.domain(boardID)}/${boardID}/thread/${threadID}`; },
- post({postID}) { return `#p${postID}`; },
- index({boardID}) { return `${location.protocol}//${BoardConfig.domain(boardID)}/${boardID}/`; },
- catalog({boardID}) { if (boardID === 'f') { return undefined; } else { return `${location.protocol}//${BoardConfig.domain(boardID)}/${boardID}/catalog`; } },
- archive({boardID}) { if (BoardConfig.isArchived(boardID)) { return `${location.protocol}//${BoardConfig.domain(boardID)}/${boardID}/archive`; } else { return undefined; } },
- threadJSON({boardID, threadID}) { return `${location.protocol}//a.4cdn.org/${boardID}/thread/${threadID}.json`; },
- threadsListJSON({boardID}) { return `${location.protocol}//a.4cdn.org/${boardID}/threads.json`; },
- archiveListJSON({boardID}) { if (BoardConfig.isArchived(boardID)) { return `${location.protocol}//a.4cdn.org/${boardID}/archive.json`; } else { return ''; } },
- catalogJSON({boardID}) { return `${location.protocol}//a.4cdn.org/${boardID}/catalog.json`; },
- file({boardID}, filename) {
- const hostname = boardID === 'f' ? ImageHost.flashHost() : ImageHost.host();
- return `${location.protocol}//${hostname}/${boardID}/${filename}`;
- },
- thumb({boardID}, filename) {
- return `${location.protocol}//${ImageHost.thumbHost()}/${boardID}/${filename}`;
- }
- },
-
- isPrunedByAge({boardID}) { return boardID === 'f'; },
- areMD5sDeferred({boardID}) { return boardID === 'f'; },
- isOnePage({boardID}) { return boardID === 'f'; },
- noAudio({boardID}) { return BoardConfig.noAudio(boardID); },
-
- selectors: {
- board: '.board',
- thread: '.thread',
- threadDivider: '.board > hr',
- summary: '.summary',
- postContainer: '.postContainer',
- replyOriginal: '.replyContainer:not([data-clone])',
- sideArrows: 'div.sideArrows',
- post: '.post',
- infoRoot: '.postInfo',
- info: {
- subject: '.subject',
- name: '.name',
- email: '.useremail',
- tripcode: '.postertrip',
- uniqueIDRoot: '.posteruid',
- uniqueID: '.posteruid > .hand',
- capcode: '.capcode.hand',
- pass: '.n-pu',
- flag: '.flag, .bfl',
- date: '.dateTime',
- nameBlock: '.nameBlock',
- quote: '.postNum > a:nth-of-type(2)',
- reply: '.replylink'
- },
- icons: {
- isSticky: '.stickyIcon',
- isClosed: '.closedIcon',
- isArchived: '.archivedIcon'
- },
- file: {
- text: '.file > :first-child',
- link: '.fileText > a',
- thumb: 'a.fileThumb > [data-md5]'
- },
- thumbLink: 'a.fileThumb',
- highlightable: {
- op: '.opContainer',
- reply: ' > .reply',
- catalog: ''
- },
- comment: '.postMessage',
- spoiler: 's',
- quotelink: ':not(pre) > .quotelink', // XXX https://github.com/4chan/4chan-JS/issues/77: 4chan currently creates quote links inside [code] tags; ignore them
- catalog: {
- board: '#threads',
- thread: '.thread',
- thumb: '.thumb'
- },
- boardList: '#boardNavDesktop > .boardList',
- boardListBottom: '#boardNavDesktopFoot > .boardList',
- styleSheet: 'link[title=switch]',
- psa: '#globalMessage',
- psaTop: '#globalToggle',
- searchBox: '#search-box',
- nav: {
- prev: '.prev > form > [type=submit]',
- next: '.next > form > [type=submit]'
- }
- },
-
- classes: {
- highlight: 'highlight'
- },
-
- xpath: {
- thread: 'div[contains(concat(" ",@class," ")," thread ")]',
- postContainer: 'div[contains(@class,"postContainer")]',
- replyContainer: 'div[contains(@class,"replyContainer")]'
- },
-
- regexp: {
- quotelink:
- new RegExp(`\
-^https?://boards\\.4chan(?:nel)?\\.org/+\
-([^/]+)\
-/+thread/+\
-(\\d+)\
-(?:[/?][^#]*)?\
-(?:#p\
-(\\d+)\
-)?\
-$\
-`),
- quotelinkHTML:
- /]*\bhref="(?:(?:\/\/boards\.4chan(?:nel)?\.org)?\/([^\/]+)\/thread\/)?(\d+)?(?:#p(\d+))?"/g,
- pass:
- /^https?:\/\/www\.4chan(?:nel)?\.org\/+pass(?:$|[?#])/
- },
-
- bgColoredEl() {
- return $.el('div', {className: 'reply'});
- },
-
- isThisPageLegit() {
- // not 404 error page or similar.
- return ['boards.4chan.org', 'boards.4channel.org'].includes(location.hostname) &&
- d.doctype &&
- !$('link[href*="favicon-status.ico"]', d.head) &&
- !['4chan - Temporarily Offline', '4chan - Error', '504 Gateway Time-out', 'MathJax Equation Source'].includes(d.title);
- },
-
- is404() {
- // XXX Sometimes threads don't 404 but are left over as stubs containing one garbage reply post.
- return ['4chan - Temporarily Offline', '4chan - 404 Not Found'].includes(d.title) || ((g.VIEW === 'thread') && $('.board') && !$('.opContainer'));
- },
-
- isIncomplete() {
- return ['index', 'thread'].includes(g.VIEW) && !$('.board + *');
- },
-
- isBoardlessPage(url) {
- return ['www.4chan.org', 'www.4channel.org'].includes(url.hostname);
- },
-
- isAuxiliaryPage(url) {
- return !['boards.4chan.org', 'boards.4channel.org'].includes(url.hostname);
- },
-
- isFileURL(url) {
- return ImageHost.test(url.hostname);
- },
-
- initAuxiliary() {
- switch (location.hostname) {
- case 'www.4chan.org': case 'www.4channel.org':
- if (SW.yotsuba.regexp.pass.test(location.href)) {
- PassMessage.init();
- } else {
- $.onExists(doc, 'body', () => $.addStyle(CSS.www));
- Captcha.replace.init();
- }
- return;
- case 'sys.4chan.org': case 'sys.4channel.org':
- var pathname = location.pathname.split(/\/+/);
- if (pathname[2] === 'imgboard.php') {
- let match;
- if (/\bmode=report\b/.test(location.search)) {
- Report.init();
- } else if (match = location.search.match(/\bres=(\d+)/)) {
- $.ready(function() {
- if (Conf['404 Redirect'] && ($.id('errmsg')?.textContent === 'Error: Specified thread does not exist.')) {
- return Redirect.navigate('thread', {
- boardID: g.BOARD.ID,
- postID: +match[1]
- });
- }});
- }
- } else if (pathname[2] === 'post') {
- PostSuccessful.init();
- }
- return;
- }
- },
-
- scriptData() {
- for (var script of $$('script:not([src])', d.head)) {
- if (/\bcooldowns *=/.test(script.textContent)) { return script.textContent; }
- }
- return '';
- },
-
- parseThreadMetadata(thread) {
- let m;
- const scriptData = this.scriptData();
- thread.postLimit = /\bbumplimit *= *1\b/.test(scriptData);
- thread.fileLimit = /\bimagelimit *= *1\b/.test(scriptData);
- thread.ipCount = (m = scriptData.match(/\bunique_ips *= *(\d+)\b/)) ? +m[1] : undefined;
-
- if ((g.BOARD.ID === 'f') && thread.OP.file) {
- const {file} = thread.OP;
- return $.ajax(this.urls.threadJSON({boardID: 'f', threadID: thread.ID}), {
- timeout: $.MINUTE,
- onloadend() {
- if (this.response) {
- return file.text.dataset.md5 = (file.MD5 = this.response.posts[0].md5);
- }
- }
- }
- );
- }
- },
-
- parseNodes(post, nodes) {
- // Add CSS classes to sticky/closed icons on /f/ to match other boards.
- if (post.boardID === 'f') {
- return (() => {
- const result = [];
- for (var type of ['Sticky', 'Closed']) {
- var icon;
- if (icon = $(`img[alt=${type}]`, nodes.info)) {
- result.push($.addClass(icon, `${type.toLowerCase()}Icon`, 'retina'));
- }
- }
- return result;
- })();
- }
- },
-
- parseDate(node) {
- return new Date(node.dataset.utc * 1000);
- },
-
- parseFile(post, file) {
- let info;
- const {text, link, thumb} = file;
- if (!(info = link.nextSibling?.textContent.match(/\(([\d.]+ [KMG]?B).*\)/))) { return false; }
- $.extend(file, {
- name: text.title || link.title || link.textContent,
- size: info[1],
- dimensions: info[0].match(/\d+x\d+/)?.[0],
- tag: info[0].match(/,[^,]*, ([a-z]+)\)/i)?.[1],
- MD5: text.dataset.md5
- }
- );
- if (thumb) {
- $.extend(file, {
- thumbURL: thumb.src,
- MD5: thumb.dataset.md5,
- isSpoiler: $.hasClass(thumb.parentNode, 'imgspoiler')
- }
- );
- if (file.isSpoiler) {
- let m;
- file.thumbURL = (m = link.href.match(/\d+(?=\.\w+$)/)) ? `${location.protocol}//${ImageHost.thumbHost()}/${post.board}/${m[0]}s.jpg` : undefined;
- }
- }
- return true;
- },
-
- cleanComment(bq) {
- let abbr;
- if (abbr = $('.abbr', bq)) { // 'Comment too long' or 'EXIF data available'
- for (var node of $$('.abbr + br, .exif', bq)) {
- $.rm(node);
- }
- for (let i = 0; i < 2; i++) {
- var br;
- if ((br = abbr.previousSibling) && (br.nodeName === 'BR')) { $.rm(br); }
- }
- return $.rm(abbr);
- }
- },
-
- cleanCommentDisplay(bq) {
- let b;
- if ((b = $('b', bq)) && /^Rolled /.test(b.textContent)) { $.rm(b); }
- return $.rm($('.fortune', bq));
- },
-
- insertTags(bq) {
- let node;
- for (node of $$('s, .removed-spoiler', bq)) {
- $.replace(node, [$.tn('[spoiler]'), ...Array.from(node.childNodes), $.tn('[/spoiler]')]);
- }
- for (node of $$('.prettyprint', bq)) {
- $.replace(node, [$.tn('[code]'), ...Array.from(node.childNodes), $.tn('[/code]')]);
- }
- },
-
- hasCORS(url) {
- return url.split('/').slice(0, 3).join('/') === (location.protocol + '//a.4cdn.org');
- },
-
- sfwBoards(sfw) {
- return BoardConfig.sfwBoards(sfw);
- },
-
- uidColor(uid) {
- let msg = 0;
- let i = 0;
- while (i < 8) {
- msg = ((msg << 5) - msg) + uid.charCodeAt(i++);
- }
- return (msg >> 8) & 0xFFFFFF;
- },
-
- isLinkified(link) {
- return ImageHost.test(link.hostname);
- },
-
- testNativeExtension() {
- return $.global(function() {
- if (window.Parser.postMenuIcon) { return this.enabled = 'true'; }
- });
- },
-
- transformBoardList() {
- let node;
- const nodes = [];
- const spacer = () => $.el('span', {className: 'spacer'});
- const items = $.X('.//a|.//text()[not(ancestor::a)]', $(SW.yotsuba.selectors.boardList));
- let i = 0;
- while ((node = items.snapshotItem(i++))) {
- switch (node.nodeName) {
- case '#text':
- for (var chr of node.nodeValue) {
- var span = $.el('span', {textContent: chr});
- if (chr === ' ') { span.className = 'space'; }
- if (chr === ']') { nodes.push(spacer()); }
- nodes.push(span);
- if (chr === '[') { nodes.push(spacer()); }
- }
- break;
- case 'A':
- var a = node.cloneNode(true);
- nodes.push(a);
- break;
- }
- }
- return nodes;
- }
-};
diff --git a/src/site/SW.yotsuba.tsx b/src/site/SW.yotsuba.tsx
new file mode 100644
index 0000000000..1331b5a220
--- /dev/null
+++ b/src/site/SW.yotsuba.tsx
@@ -0,0 +1,718 @@
+import Redirect from "../Archive/Redirect";
+import PassMessage from "../Miscellaneous/PassMessage";
+import Report from "../Miscellaneous/Report";
+import $ from "../platform/$";
+import $$ from "../platform/$$";
+import Captcha from "../Posting/Captcha";
+import PostSuccessful from "../Posting/PostSuccessful";
+import ImageHost from "../Images/ImageHost";
+import { g, Conf, E, d, doc } from "../globals/globals";
+import BoardConfig from "../General/BoardConfig";
+import CSS from "../css/CSS";
+
+import generatePostInfoHtml from './SW.yotsuba.Build/PostInfoHtml';
+import generateFileHtml from "./SW.yotsuba.Build/FileHtml";
+import generateCatalogThreadHtml from "./SW.yotsuba.Build/CatalogThreadHtml";
+import h, { hFragment, isEscaped } from "../globals/jsx";
+import { dict, MINUTE } from "../platform/helpers";
+
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS205: Consider reworking code to avoid use of IIFEs
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
+ */
+const SWYotsuba = {
+ isOPContainerThread: false,
+ hasIPCount: true,
+ archivedBoardsKnown: true,
+
+ urls: {
+ thread({boardID, threadID}) { return `${location.protocol}//${BoardConfig.domain(boardID)}/${boardID}/thread/${threadID}`; },
+ post({postID}) { return `#p${postID}`; },
+ index({boardID}) { return `${location.protocol}//${BoardConfig.domain(boardID)}/${boardID}/`; },
+ catalog({boardID}) { if (boardID === 'f') { return undefined; } else { return `${location.protocol}//${BoardConfig.domain(boardID)}/${boardID}/catalog`; } },
+ archive({boardID}) { if (BoardConfig.isArchived(boardID)) { return `${location.protocol}//${BoardConfig.domain(boardID)}/${boardID}/archive`; } else { return undefined; } },
+ threadJSON({boardID, threadID}) { return `${location.protocol}//a.4cdn.org/${boardID}/thread/${threadID}.json`; },
+ threadsListJSON({boardID}) { return `${location.protocol}//a.4cdn.org/${boardID}/threads.json`; },
+ archiveListJSON({boardID}) { if (BoardConfig.isArchived(boardID)) { return `${location.protocol}//a.4cdn.org/${boardID}/archive.json`; } else { return ''; } },
+ catalogJSON({boardID}) { return `${location.protocol}//a.4cdn.org/${boardID}/catalog.json`; },
+ file({boardID}, filename) {
+ const hostname = boardID === 'f' ? ImageHost.flashHost() : ImageHost.host();
+ return `${location.protocol}//${hostname}/${boardID}/${filename}`;
+ },
+ thumb({boardID}, filename) {
+ return `${location.protocol}//${ImageHost.thumbHost()}/${boardID}/${filename}`;
+ }
+ },
+
+ isPrunedByAge({boardID}) { return boardID === 'f'; },
+ areMD5sDeferred({boardID}) { return boardID === 'f'; },
+ isOnePage({boardID}) { return boardID === 'f'; },
+ noAudio({boardID}) { return BoardConfig.noAudio(boardID); },
+
+ selectors: {
+ board: '.board',
+ thread: '.thread',
+ threadDivider: '.board > hr',
+ summary: '.summary',
+ postContainer: '.postContainer',
+ replyOriginal: '.replyContainer:not([data-clone])',
+ sideArrows: 'div.sideArrows',
+ post: '.post',
+ infoRoot: '.postInfo',
+ info: {
+ subject: '.subject',
+ name: '.name',
+ email: '.useremail',
+ tripcode: '.postertrip',
+ uniqueIDRoot: '.posteruid',
+ uniqueID: '.posteruid > .hand',
+ capcode: '.capcode.hand',
+ pass: '.n-pu',
+ flag: '.flag, .bfl',
+ date: '.dateTime',
+ nameBlock: '.nameBlock',
+ quote: '.postNum > a:nth-of-type(2)',
+ reply: '.replylink'
+ },
+ icons: {
+ isSticky: '.stickyIcon',
+ isClosed: '.closedIcon',
+ isArchived: '.archivedIcon'
+ },
+ file: {
+ text: '.file > :first-child',
+ link: '.fileText > a',
+ thumb: 'a.fileThumb > [data-md5]'
+ },
+ thumbLink: 'a.fileThumb',
+ highlightable: {
+ op: '.opContainer',
+ reply: ' > .reply',
+ catalog: ''
+ },
+ comment: '.postMessage',
+ spoiler: 's',
+ quotelink: ':not(pre) > .quotelink', // XXX https://github.com/4chan/4chan-JS/issues/77: 4chan currently creates quote links inside [code] tags; ignore them
+ catalog: {
+ board: '#threads',
+ thread: '.thread',
+ thumb: '.thumb'
+ },
+ boardList: '#boardNavDesktop > .boardList',
+ boardListBottom: '#boardNavDesktopFoot > .boardList',
+ styleSheet: 'link[title=switch]',
+ psa: '#globalMessage',
+ psaTop: '#globalToggle',
+ searchBox: '#search-box',
+ nav: {
+ prev: '.prev > form > [type=submit]',
+ next: '.next > form > [type=submit]'
+ }
+ },
+
+ classes: {
+ highlight: 'highlight'
+ },
+
+ xpath: {
+ thread: 'div[contains(concat(" ",@class," ")," thread ")]',
+ postContainer: 'div[contains(@class,"postContainer")]',
+ replyContainer: 'div[contains(@class,"replyContainer")]'
+ },
+
+ regexp: {
+ quotelink:
+ new RegExp(`\
+^https?://boards\\.4chan(?:nel)?\\.org/+\
+([^/]+)\
+/+thread/+\
+(\\d+)\
+(?:[/?][^#]*)?\
+(?:#p\
+(\\d+)\
+)?\
+$\
+`),
+ quotelinkHTML:
+ / ]*\bhref="(?:(?:\/\/boards\.4chan(?:nel)?\.org)?\/([^\/]+)\/thread\/)?(\d+)?(?:#p(\d+))?"/g,
+ pass:
+ /^https?:\/\/www\.4chan(?:nel)?\.org\/+pass(?:$|[?#])/
+ },
+
+ bgColoredEl() {
+ return $.el('div', {className: 'reply'});
+ },
+
+ isThisPageLegit() {
+ // not 404 error page or similar.
+ return ['boards.4chan.org', 'boards.4channel.org'].includes(location.hostname) &&
+ d.doctype &&
+ !$('link[href*="favicon-status.ico"]', d.head) &&
+ !['4chan - Temporarily Offline', '4chan - Error', '504 Gateway Time-out', 'MathJax Equation Source'].includes(d.title);
+ },
+
+ is404() {
+ // XXX Sometimes threads don't 404 but are left over as stubs containing one garbage reply post.
+ return ['4chan - Temporarily Offline', '4chan - 404 Not Found'].includes(d.title) || ((g.VIEW === 'thread') && $('.board') && !$('.opContainer'));
+ },
+
+ isIncomplete() {
+ return ['index', 'thread'].includes(g.VIEW) && !$('.board + *');
+ },
+
+ isBoardlessPage(url) {
+ return ['www.4chan.org', 'www.4channel.org'].includes(url.hostname);
+ },
+
+ isAuxiliaryPage(url) {
+ return !['boards.4chan.org', 'boards.4channel.org'].includes(url.hostname);
+ },
+
+ isFileURL(url) {
+ return ImageHost.test(url.hostname);
+ },
+
+ initAuxiliary() {
+ switch (location.hostname) {
+ case 'www.4chan.org': case 'www.4channel.org':
+ if (SWYotsuba.regexp.pass.test(location.href)) {
+ PassMessage.init();
+ } else {
+ $.onExists(doc, 'body', () => $.addStyle(CSS.www));
+ Captcha.replace.init();
+ }
+ return;
+ case 'sys.4chan.org': case 'sys.4channel.org':
+ var pathname = location.pathname.split(/\/+/);
+ if (pathname[2] === 'imgboard.php') {
+ let match;
+ if (/\bmode=report\b/.test(location.search)) {
+ Report.init();
+ } else if (match = location.search.match(/\bres=(\d+)/)) {
+ $.ready(function() {
+ if (Conf['404 Redirect'] && ($.id('errmsg')?.textContent === 'Error: Specified thread does not exist.')) {
+ return Redirect.navigate('thread', {
+ boardID: g.BOARD.ID,
+ postID: +match[1]
+ });
+ }});
+ }
+ } else if (pathname[2] === 'post') {
+ PostSuccessful.init();
+ }
+ return;
+ }
+ },
+
+ scriptData() {
+ for (var script of $$('script:not([src])', d.head)) {
+ if (/\bcooldowns *=/.test(script.textContent)) { return script.textContent; }
+ }
+ return '';
+ },
+
+ parseThreadMetadata(thread) {
+ let m;
+ const scriptData = this.scriptData();
+ thread.postLimit = /\bbumplimit *= *1\b/.test(scriptData);
+ thread.fileLimit = /\bimagelimit *= *1\b/.test(scriptData);
+ thread.ipCount = (m = scriptData.match(/\bunique_ips *= *(\d+)\b/)) ? +m[1] : undefined;
+
+ if ((g.BOARD.ID === 'f') && thread.OP.file) {
+ const {file} = thread.OP;
+ return $.ajax(this.urls.threadJSON({boardID: 'f', threadID: thread.ID}), {
+ timeout: MINUTE,
+ onloadend() {
+ if (this.response) {
+ return file.text.dataset.md5 = (file.MD5 = this.response.posts[0].md5);
+ }
+ }
+ }
+ );
+ }
+ },
+
+ parseNodes(post, nodes) {
+ // Add CSS classes to sticky/closed icons on /f/ to match other boards.
+ if (post.boardID === 'f') {
+ return (() => {
+ const result = [];
+ for (var type of ['Sticky', 'Closed']) {
+ var icon;
+ if (icon = $(`img[alt=${type}]`, nodes.info)) {
+ result.push($.addClass(icon, `${type.toLowerCase()}Icon`, 'retina'));
+ }
+ }
+ return result;
+ })();
+ }
+ },
+
+ parseDate(node) {
+ return new Date(node.dataset.utc * 1000);
+ },
+
+ parseFile(post, file) {
+ let info;
+ const {text, link, thumb} = file;
+ if (!(info = link.nextSibling?.textContent.match(/\(([\d.]+ [KMG]?B).*\)/))) { return false; }
+ $.extend(file, {
+ name: text.title || link.title || link.textContent,
+ size: info[1],
+ dimensions: info[0].match(/\d+x\d+/)?.[0],
+ tag: info[0].match(/,[^,]*, ([a-z]+)\)/i)?.[1],
+ MD5: text.dataset.md5
+ }
+ );
+ if (thumb) {
+ $.extend(file, {
+ thumbURL: thumb.src,
+ MD5: thumb.dataset.md5,
+ isSpoiler: $.hasClass(thumb.parentNode, 'imgspoiler')
+ }
+ );
+ if (file.isSpoiler) {
+ let m;
+ file.thumbURL = (m = link.href.match(/\d+(?=\.\w+$)/)) ? `${location.protocol}//${ImageHost.thumbHost()}/${post.board}/${m[0]}s.jpg` : undefined;
+ }
+ }
+ return true;
+ },
+
+ cleanComment(bq) {
+ let abbr;
+ if (abbr = $('.abbr', bq)) { // 'Comment too long' or 'EXIF data available'
+ for (var node of $$('.abbr + br, .exif', bq)) {
+ $.rm(node);
+ }
+ for (let i = 0; i < 2; i++) {
+ var br;
+ if ((br = abbr.previousSibling) && (br.nodeName === 'BR')) { $.rm(br); }
+ }
+ return $.rm(abbr);
+ }
+ },
+
+ cleanCommentDisplay(bq) {
+ let b;
+ if ((b = $('b', bq)) && /^Rolled /.test(b.textContent)) { $.rm(b); }
+ return $.rm($('.fortune', bq));
+ },
+
+ insertTags(bq) {
+ let node;
+ for (node of $$('s, .removed-spoiler', bq)) {
+ $.replace(node, [$.tn('[spoiler]'), ...Array.from(node.childNodes), $.tn('[/spoiler]')]);
+ }
+ for (node of $$('.prettyprint', bq)) {
+ $.replace(node, [$.tn('[code]'), ...Array.from(node.childNodes), $.tn('[/code]')]);
+ }
+ },
+
+ hasCORS(url) {
+ return url.split('/').slice(0, 3).join('/') === (location.protocol + '//a.4cdn.org');
+ },
+
+ sfwBoards(sfw) {
+ return BoardConfig.sfwBoards(sfw);
+ },
+
+ uidColor(uid) {
+ let msg = 0;
+ let i = 0;
+ while (i < 8) {
+ msg = ((msg << 5) - msg) + uid.charCodeAt(i++);
+ }
+ return (msg >> 8) & 0xFFFFFF;
+ },
+
+ isLinkified(link) {
+ return ImageHost.test(link.hostname);
+ },
+
+ testNativeExtension() {
+ return $.global(function() {
+ if (window.Parser?.postMenuIcon) { return this.enabled = 'true'; }
+ });
+ },
+
+ transformBoardList() {
+ let node;
+ const nodes = [];
+ const spacer = () => $.el('span', {className: 'spacer'});
+ const items = $.X('.//a|.//text()[not(ancestor::a)]', $(SWYotsuba.selectors.boardList));
+ let i = 0;
+ while ((node = items.snapshotItem(i++))) {
+ switch (node.nodeName) {
+ case '#text':
+ for (var chr of node.nodeValue) {
+ var span = $.el('span', {textContent: chr});
+ if (chr === ' ') { span.className = 'space'; }
+ if (chr === ']') { nodes.push(spacer()); }
+ nodes.push(span);
+ if (chr === '[') { nodes.push(spacer()); }
+ }
+ break;
+ case 'A':
+ var a = node.cloneNode(true);
+ nodes.push(a);
+ break;
+ }
+ }
+ return nodes;
+ },
+
+ Build: {
+ staticPath: '//s.4cdn.org/image/',
+ gifIcon: window.devicePixelRatio >= 2 ? '@2x.gif' : '.gif',
+ spoilerRange: Object.create(null),
+
+ shortFilename(filename) {
+ const ext = filename.match(/\.?[^\.]*$/)[0];
+ if ((filename.length - ext.length) > 30) {
+ return `${filename.match(/(?:[\uD800-\uDBFF][\uDC00-\uDFFF]|[^]){0,25}/)[0]}(...)${ext}`;
+ } else {
+ return filename;
+ }
+ },
+
+ spoilerThumb(boardID) {
+ let spoilerRange;
+ if ((spoilerRange = this.spoilerRange[boardID])) {
+ // Randomize the spoiler image.
+ return `${this.staticPath}spoiler-${boardID}${Math.floor(1 + (spoilerRange * Math.random()))}.png`;
+ } else {
+ return `${this.staticPath}spoiler.png`;
+ }
+ },
+
+ sameThread(boardID, threadID) {
+ return (g.VIEW === 'thread') && (g.BOARD.ID === boardID) && (g.THREADID === +threadID);
+ },
+
+ threadURL(boardID, threadID) {
+ if (boardID !== g.BOARD.ID) {
+ return `//${BoardConfig.domain(boardID)}/${boardID}/thread/${threadID}`;
+ } else if ((g.VIEW !== 'thread') || (+threadID !== g.THREADID)) {
+ return `/${boardID}/thread/${threadID}`;
+ } else {
+ return '';
+ }
+ },
+
+ postURL(boardID, threadID, postID) {
+ return `${this.threadURL(boardID, threadID)}#p${postID}`;
+ },
+
+ parseJSON(data, { siteID, boardID }) {
+ const o = {
+ // id
+ ID: data.no,
+ postID: data.no,
+ threadID: data.resto || data.no,
+ boardID,
+ siteID,
+ isReply: !!data.resto,
+ // thread status
+ isSticky: !!data.sticky,
+ isClosed: !!data.closed,
+ isArchived: !!data.archived,
+ // file status
+ fileDeleted: !!data.filedeleted,
+ filesDeleted: data.filedeleted ? [0] : []
+ };
+ o.info = {
+ subject: $.unescape(data.sub),
+ email: $.unescape(data.email),
+ name: $.unescape(data.name) || '',
+ tripcode: data.trip,
+ pass: (data.since4pass != null) ? `${data.since4pass}` : undefined,
+ uniqueID: data.id,
+ flagCode: data.country,
+ flagCodeTroll: data.board_flag,
+ flag: $.unescape((data.country_name || data.flag_name)),
+ dateUTC: data.time,
+ dateText: data.now,
+ // Yes, we use the raw string here
+ commentHTML: { innerHTML: data.com || '', [isEscaped]: true }
+ };
+ if (data.capcode) {
+ o.info.capcode = data.capcode.replace(/_highlight$/, '').replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
+ o.capcodeHighlight = /_highlight$/.test(data.capcode);
+ delete o.info.uniqueID;
+ }
+ o.files = [];
+ if (data.ext) {
+ o.file = this.parseJSONFile(data, { siteID, boardID });
+ o.files.push(o.file);
+ }
+ // Temporary JSON properties for events such as April 1 / Halloween
+ o.extra = dict();
+ for (var key in data) {
+ if (key[0] === 'x') {
+ o.extra[key] = data[key];
+ }
+ }
+ return o;
+ },
+
+ parseJSONFile(data, { siteID, boardID }) {
+ const site = g.sites[siteID];
+ const filename = (site.software === 'yotsuba') && (boardID === 'f') ?
+ `${encodeURIComponent(data.filename)}${data.ext}`
+ :
+ `${data.tim}${data.ext}`;
+ const o = {
+ name: ($.unescape(data.filename)) + data.ext,
+ url: site.urls.file({ siteID, boardID }, filename),
+ height: data.h,
+ width: data.w,
+ MD5: data.md5,
+ size: $.bytesToString(data.fsize),
+ thumbURL: site.urls.thumb({ siteID, boardID }, `${data.tim}s.jpg`),
+ theight: data.tn_h,
+ twidth: data.tn_w,
+ isSpoiler: !!data.spoiler,
+ tag: data.tag,
+ hasDownscale: !!data.m_img
+ };
+ if ((data.h != null) && !/\.pdf$/.test(o.url)) { o.dimensions = `${o.width}x${o.height}`; }
+ return o;
+ },
+
+ parseComment(html) {
+ html = html
+ .replace(/ /gi, '\n')
+ .replace(/\n\n]*>/g, '');
+ return $.unescape(html);
+ },
+
+ parseCommentDisplay(html) {
+ // Hide spoilers.
+ if (!Conf['Remove Spoilers'] && !Conf['Reveal Spoilers']) {
+ let html2;
+ while ((html2 = html.replace(/(?:(?!<\/?s>).)*<\/s>/g, '[spoiler]')) !== html) {
+ html = html2;
+ }
+ }
+ html = html
+ .replace(/^Rolled [^<]*<\/b>/i, '') // Rolls (/tg/, /qst/)
+ .replace(/
+ {(o.isReply ? >>
: '')}
+
+ {(o.isReply ? <>{postInfo}{fileBlock}> : <>{fileBlock}{postInfo}>)}
+
{commentHTML}
+
+ >;
+
+ const container = $.el('div', {
+ className: `postContainer ${postClass}Container`,
+ id: `pc${ID}`
+ });
+ $.extend(container, wholePost);
+
+ // Fix quotelinks
+ for (var quote of $$('.quotelink', container)) {
+ var href = quote.getAttribute('href');
+ if (href[0] === '#') {
+ if (!this.sameThread(boardID, threadID)) {
+ quote.href = this.threadURL(boardID, threadID) + href;
+ }
+ } else {
+ var match;
+ if ((match = quote.href.match(SWYotsuba.regexp.quotelink)) && (this.sameThread(match[1], match[2]))) {
+ quote.href = href.match(/(#[^#]*)?$/)[0] || '#';
+ }
+ }
+ }
+
+ return container;
+ },
+
+ summaryText(status, posts, files) {
+ let text = '';
+ if (status) { text += `${status} `; }
+ text += `${posts} post${posts > 1 ? 's' : ''}`;
+ if (+files) { text += ` and ${files} image repl${files > 1 ? 'ies' : 'y'}`; }
+ return text += ` ${status === '-' ? 'shown' : 'omitted'}.`;
+ },
+
+ summary(boardID, threadID, posts, files) {
+ return $.el('a', {
+ className: 'summary',
+ textContent: this.summaryText('', posts, files),
+ href: `/${boardID}/thread/${threadID}`
+ }
+ );
+ },
+
+ thread(thread, data, withReplies) {
+ let root;
+ if (root = thread.nodes.root) {
+ $.rmAll(root);
+ } else {
+ thread.nodes.root = (root = $.el('div', {
+ className: 'thread',
+ id: `t${data.no}`
+ }
+ ));
+ }
+ if (this.hat) { $.add(root, this.hat.cloneNode(false)); }
+ $.add(root, thread.OP.nodes.root);
+ if (data.omitted_posts || (!withReplies && data.replies)) {
+ const [posts, files] = Array.from(withReplies ?
+ // XXX data.omitted_images is not accurate.
+ [data.omitted_posts, data.images - data.last_replies.filter(data => !!data.ext).length]
+ :
+ [data.replies, data.images]);
+ const summary = this.summary(thread.board.ID, data.no, posts, files);
+ $.add(root, summary);
+ }
+ return root;
+ },
+
+ catalogThread(thread, data, pageCount) {
+ let cssText, imgClass, src;
+ const { staticPath, gifIcon } = this;
+ const { tn_w, tn_h } = data;
+
+ if (data.spoiler && !Conf['Reveal Spoiler Thumbnails']) {
+ let spoilerRange;
+ src = `${staticPath}spoiler`;
+ if (spoilerRange = this.spoilerRange[thread.board]) {
+ // Randomize the spoiler image.
+ src += (`-${thread.board}`) + Math.floor(1 + (spoilerRange * Math.random()));
+ }
+ src += '.png';
+ imgClass = 'spoiler-file';
+ cssText = "--tn-w: 100; --tn-h: 100;";
+ } else if (data.filedeleted) {
+ src = `${staticPath}filedeleted-res${gifIcon}`;
+ imgClass = 'deleted-file';
+ } else if (thread.OP.file) {
+ src = thread.OP.file.thumbURL;
+ const ratio = 250 / Math.max(tn_w, tn_h);
+ cssText = `--tn-w: ${tn_w * ratio}; --tn-h: ${tn_h * ratio};`;
+ } else {
+ src = `${staticPath}nofile.png`;
+ imgClass = 'no-file';
+ }
+
+ const postCount = data.replies + 1;
+ const fileCount = data.images + !!data.ext;
+
+ const container = $.el(
+ 'div',
+ generateCatalogThreadHtml(thread, src, imgClass, data, postCount, fileCount, pageCount, staticPath, gifIcon)
+ );
+ $.before(thread.OP.nodes.info, [...Array.from(container.childNodes)]);
+
+ for (var br of $$('br', thread.OP.nodes.comment)) {
+ if (br.previousSibling && (br.previousSibling.nodeName === 'BR')) {
+ $.addClass(br, 'extra-linebreak');
+ }
+ }
+
+ const root = $.el('div', {
+ className: 'thread catalog-thread',
+ id: `t${thread}`
+ }
+ );
+ if (thread.OP.highlights) { $.addClass(root, ...Array.from(thread.OP.highlights)); }
+ if (!thread.OP.file) { $.addClass(root, 'noFile'); }
+ root.style.cssText = cssText || '';
+
+ return root;
+ },
+
+ catalogReply(thread, data) {
+ let excerpt = '';
+ if (data.com) {
+ excerpt = this.parseCommentDisplay(data.com).replace(/>>\d+/g, '').trim().replace(/\n+/g, ' // ');
+ }
+ if (data.ext) {
+ if (!excerpt) { excerpt = `${$.unescape(data.filename)}${data.ext}`; }
+ }
+ if (data.com) {
+ if (!excerpt) { excerpt = $.unescape(data.com.replace(/ /gi, ' // ')); }
+ }
+ if (!excerpt) { excerpt = '\xA0'; }
+ if (excerpt.length > 73) { excerpt = `${excerpt.slice(0, 70)}...`; }
+
+ const link = this.postURL(thread.board.ID, thread.ID, data.no);
+ return $.el('div', { className: 'catalog-reply' },
+ <>
+ ... :
+ {excerpt}
+ ...
+ >
+ );
+ }
+ }
+};
+export default SWYotsuba;
diff --git a/src/site/Site.js b/src/site/Site.js
index db70615cfc..f7bbc3ed36 100644
--- a/src/site/Site.js
+++ b/src/site/Site.js
@@ -1,3 +1,9 @@
+import { Conf, doc, g } from "../globals/globals";
+import Main from "../main/Main";
+import $ from "../platform/$";
+import { dict } from "../platform/helpers";
+import SW from "./SW";
+
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
@@ -26,7 +32,7 @@ var Site = {
if (changes = SW[software].detect?.()) {
changes.software = software;
hostname = location.hostname.replace(/^www\./, '');
- var properties = (Conf['siteProperties'][hostname] || (Conf['siteProperties'][hostname] = $.dict()));
+ var properties = (Conf['siteProperties'][hostname] || (Conf['siteProperties'][hostname] = dict()));
var changed = 0;
for (var key in changes) {
if (properties[key] !== changes[key]) {
@@ -79,3 +85,4 @@ var Site = {
return g.SITE = g.sites[hostname];
}
};
+export default Site;
diff --git a/src/types/customImports.d.ts b/src/types/customImports.d.ts
new file mode 100644
index 0000000000..84eb6e4dcb
--- /dev/null
+++ b/src/types/customImports.d.ts
@@ -0,0 +1,70 @@
+declare module '*.css' { const css: string; export default css; }
+declare module '*.woff' { const woff: string; export default woff; }
+declare module '*.woff2' { const woff2: string; export default woff2; }
+declare module '*.html' { const html: string; export default html; }
+declare module '*.gif' { const gif: string; export default gif; }
+declare module '*.png' { const png: string; export default png; }
+declare module '*.wav' { const wav: string; export default wav; }
+declare module '*/package.json' {
+ const meta: {
+ "name": string,
+ "path": string,
+ "fork": string,
+ "page": string,
+ "downloads": string,
+ "oldVersions": string,
+ "faq": string,
+ "captchaFAQ": string,
+ "cssGuide": string,
+ "license": string,
+ "changelog": string,
+ "issues": string,
+ "newIssue": string,
+ "newIssueMaxLength": number,
+ "alternatives": string,
+ "appid": string,
+ "appidGecko": string,
+ "chromeStoreID": string,
+ "recaptchaKey": string,
+ "distBranch": string,
+ "includes_only": string[],
+ "matches_only": string[],
+ "matches": string[],
+ "matches_extra": string[],
+ "exclude_matches": string[],
+ "grants": string[],
+ "min": {
+ "chrome": string,
+ "firefox": string,
+ "greasemonkey": string
+ }
+ }
+ export default meta;
+}
+declare module '*/version.json' {
+ const versionInfo: {
+ version: string,
+ /** ISO */
+ date: string,
+ }
+ export default versionInfo;
+}
+declare module '*/archives.json' {
+ const archives: {
+ uid: number,
+ name: string,
+ domain: string,
+ http: boolean,
+ https: boolean,
+ software: string,
+ boards: string[],
+ files: string[],
+ search?: string[],
+ reports?: boolean,
+ }[];
+ export default archives;
+}
+declare module '*/banners.json' {
+ const banners: string[];
+ export default banners;
+}
diff --git a/src/types/globals.d.ts b/src/types/globals.d.ts
new file mode 100644
index 0000000000..2cc8ff6836
--- /dev/null
+++ b/src/types/globals.d.ts
@@ -0,0 +1,2 @@
+declare const cloneInto: Function;
+declare const XPCNativeWrapper: any;
diff --git a/src/types/jsx.d.ts b/src/types/jsx.d.ts
new file mode 100644
index 0000000000..f1dc25db81
--- /dev/null
+++ b/src/types/jsx.d.ts
@@ -0,0 +1,6 @@
+import { EscapedHtml } from "../globals/jsx";
+
+declare namespace JSX {
+ interface IntrinsicElements extends Record { }
+ interface Element extends EscapedHtml { }
+}
diff --git a/tools/chain.js b/tools/chain.js
index e3a02e44a6..88eb2c7fbe 100644
--- a/tools/chain.js
+++ b/tools/chain.js
@@ -1,5 +1,5 @@
var fs = require('fs');
-var template = require('./template');
+var template = require('./template.js');
var coffee = require('coffeescript');
for (var name of process.argv.slice(2)) {
diff --git a/tools/rollup-plugin-base64.js b/tools/rollup-plugin-base64.js
new file mode 100644
index 0000000000..06175bfe76
--- /dev/null
+++ b/tools/rollup-plugin-base64.js
@@ -0,0 +1,27 @@
+import { readFile } from 'fs/promises';
+import { createFilter } from "@rollup/pluginutils";
+
+/**
+ * @param {{
+ * include: import("@rollup/pluginutils").FilterPattern,
+ * exclude?: import("@rollup/pluginutils").FilterPattern,
+ * }} opts
+ * @returns {import("rollup").Plugin}
+ */
+export default function importBase64(opts) {
+ if (!opts.include) {
+ throw Error("include option should be specified");
+ }
+ const filter = createFilter(opts.include, opts.exclude);
+
+ return {
+ name: "base64",
+
+ async load(id) {
+ if (!filter(id)) return;
+
+ const file = await readFile(id);
+ return `export default '${file.toString('base64')}';`;
+ }
+ };
+};
diff --git a/tools/rollup-plugin-inline-file.js b/tools/rollup-plugin-inline-file.js
index df9b9c5f39..9320736861 100644
--- a/tools/rollup-plugin-inline-file.js
+++ b/tools/rollup-plugin-inline-file.js
@@ -1,29 +1,52 @@
-import { createFilter } from "rollup-pluginutils";
+import { createFilter } from "@rollup/pluginutils";
+import { dirname } from "path";
+import { fileURLToPath } from "url";
-export default function inlineFile(opts = {}) {
- if (!opts.include) {
- throw Error("include option should be specified");
- }
+const __dirname = dirname(fileURLToPath(import.meta.url));
- if (opts.transformer && typeof opts.transformer !== 'function') {
- throw new Error('If transformer is given, it must be a function');
- }
+export default async function setupFileInliner(packageJson) {
+ /** @param {string} string */
+ const escape = (string) => string.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\\${');
- const filter = createFilter(opts.include, opts.exclude);
+ /**
+ * @param {{
+ * include: import("@rollup/pluginutils").FilterPattern,
+ * exclude?: import("@rollup/pluginutils").FilterPattern,
+ * transformer?: (input: string) => string
+ * wrap?: boolean
+ * }} opts
+ * @returns {import("rollup").Plugin}
+ */
+ return function inlineFile(opts) {
+ if (!opts.include) {
+ throw Error("include option should be specified");
+ }
+
+ if (opts.transformer && typeof opts.transformer !== 'function') {
+ throw new Error('If transformer is given, it must be a function');
+ }
+
+ const wrap = 'wrap' in opts ? opts.wrap : true;
- return {
- name: "inlineFile",
+ const filter = createFilter(opts.include, opts.exclude);
- transform(code, id) {
- if (filter(id)) {
- if (opts.transformer) {
- code = opts.transformer(code);
+ return {
+ name: "inlineFile",
+
+ async transform(code, id) {
+ if (filter(id)) {
+ if (opts.transformer) {
+ code = opts.transformer(code);
+ }
+ if (!wrap) return code;
+
+ code = escape(code);
+ code = code.replace(/<%= meta\.(\w+) %>/g, (match, $1) => {
+ return escape(packageJson.meta[$1]);
+ });
+ return `export default \`${code}\`;`;
}
- return {
- code: `export default ${JSON.stringify(code)};`,
- map: { mappings: "" }
- };
}
- }
+ };
};
-}
\ No newline at end of file
+}
diff --git a/tools/rollup-plugin-remove-test-code.js b/tools/rollup-plugin-remove-test-code.js
new file mode 100644
index 0000000000..70f124bc96
--- /dev/null
+++ b/tools/rollup-plugin-remove-test-code.js
@@ -0,0 +1,40 @@
+import { createFilter } from "@rollup/pluginutils";
+import MagicString from 'magic-string';
+
+/**
+ * Remove test code comments from the output build.
+ *
+ * @param {Object} opts
+ * @param {import("@rollup/pluginutils").FilterPattern} opts.include
+ * @param {import("@rollup/pluginutils").FilterPattern} [opts.exclude]
+ * @param {boolean} opts.sourceMap
+ * @returns {import("rollup").Plugin}
+ */
+export default function removeTestCode(opts) {
+ if (!opts.include) {
+ throw Error("include option should be specified");
+ }
+
+ const filter = createFilter(opts.include, opts.exclude);
+
+ return {
+ name: "removeTestCode",
+
+ async transform(code, id) {
+ if (!filter(id)) return;
+
+ const ms = new MagicString(code);
+
+ let index = 0;
+ while (index >= 0) {
+ const startIndex = code.indexOf('// #region tests_enabled', index);
+ if (startIndex < 0) break;
+
+ index = code.indexOf('// #endregion', startIndex) + 13
+ ms.remove(startIndex, index);
+ }
+
+ return { code: ms.toString(), map: opts.sourceMap ? ms.generateMap() : { mappings: '' } };
+ }
+ };
+};
\ No newline at end of file
diff --git a/tools/rollup.js b/tools/rollup.js
index 4758a3cbc2..572a099ae7 100644
--- a/tools/rollup.js
+++ b/tools/rollup.js
@@ -1,16 +1,107 @@
-import { rollup } from "rollup";
-import inlineFile from "./rollup-plugin-inline-file";
-
-rollup({
- entry: "main.js",
- plugins: [
- inlineFile({
- include: ["**/*.html", "**/*.css"],
- }),
- inlineFile({
- include: ["**/*.png", "**/*.gif"],
- // base64 encode
- transformer: btoa
- })
- ]
-});
\ No newline at end of file
+import { rollup } from 'rollup';
+import typescript from '@rollup/plugin-typescript';
+import setupFileInliner from './rollup-plugin-inline-file.js';
+import { dirname, resolve } from 'path';
+import { fileURLToPath } from 'url';
+import generateMetadata from '../src/meta/metadata.js';
+import { copyFile, readFile, writeFile } from 'fs/promises';
+import importBase64 from './rollup-plugin-base64.js';
+import generateManifestJson from '../src/meta/manifestJson.js';
+import removeTestCode from './rollup-plugin-remove-test-code.js';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+const buildDir = resolve(__dirname, '../builds/test/');
+
+let channel = '';
+
+if (process.argv.includes('-beta')) {
+ channel = '-beta';
+} else if (process.argv.includes('-noupdate')) {
+ channel = '-noupdate';
+}
+const buildForTest = process.argv.includes('-test');
+
+(async () => {
+ const packageJson = JSON.parse(await readFile(resolve(__dirname, '../package.json'), 'utf-8'));
+
+ const metadata = await generateMetadata(packageJson, channel);
+
+ const license = await readFile(resolve(__dirname, '../LICENSE'), 'utf8');
+
+ const version = JSON.parse(await readFile(resolve(__dirname, '../version.json'), 'utf-8'));
+
+
+ const inlineFile = await setupFileInliner(packageJson);
+
+ const bundle = await rollup({
+ input: resolve(__dirname, '../src/main/Main.js'),
+ plugins: [
+ buildForTest ? undefined : removeTestCode({
+ include: [
+ // Only files that actually have test code.
+ "**/src/main/Main.js",
+ "**/src/classes/Post.js",
+ "**/src/Linkification/Linkify.js",
+ ],
+ sourceMap: false,
+ }),
+ typescript(),
+ inlineFile({
+ include: ["**/*.html", "**/*.css"],
+ }),
+ importBase64({ include: ["**/*.png", "**/*.gif", "**/*.wav", "**/*.woff", "**/*.woff2"] }),
+ inlineFile({
+ include: "**/package.json",
+ wrap: false,
+ transformer(input) {
+ const data = JSON.parse(input);
+ return `export default ${JSON.stringify(data.meta, undefined, 1)};`;
+ }
+ }),
+ inlineFile({
+ include: "**/*.json",
+ exclude: "**/package.json",
+ wrap: false,
+ transformer(input) {
+ return `export default ${input};`;
+ }
+ })
+ ]
+ });
+
+ /** @type {import('rollup').OutputOptions} */
+ const sharedBundleOpts = {
+ format: "iife",
+ generatedCode: {
+ // needed for possible circular dependencies
+ constBindings: false,
+ },
+ // Can't be none as long as the root file defined exports
+ // exports: 'none',
+ };
+
+ // user script
+ await bundle.write({
+ ...sharedBundleOpts,
+ banner: metadata + license,
+ // file: '../builds/test/rollupOutput.js',
+ file: resolve(buildDir, `${packageJson.meta.path}${channel}.user.js`),
+ });
+
+ // chrome extension
+ const crxDir = resolve(buildDir, 'crx');
+ await bundle.write({
+ ...sharedBundleOpts,
+ banner: license,
+ file: resolve(crxDir, 'script.js'),
+ });
+
+ await copyFile(resolve(__dirname, '../src/meta/eventPage.js'), resolve(crxDir, 'eventPage.js'));
+
+ writeFile(resolve(crxDir, 'manifest.json'), generateManifestJson(packageJson, version, channel));
+
+ for (const file of ['icon16.png', 'icon48.png', 'icon128.png']) {
+ await copyFile(resolve(__dirname, '../src/meta/', file), resolve(crxDir, file));
+ };
+})();
diff --git a/tools/tsconfig.json b/tools/tsconfig.json
new file mode 100644
index 0000000000..b8183e413a
--- /dev/null
+++ b/tools/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "moduleResolution": "node16",
+ "types": [
+ "@violentmonkey/types",
+ "@types/chrome",
+ "node"
+ ],
+ },
+ "extends": "../tsconfig.json"
+}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000000..e8f32ff188
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "module": "ES2022",
+ "noImplicitAny": false,
+ "removeComments": false,
+ "sourceMap": true,
+ "target": "ES2020",
+ "allowJs": true,
+ "checkJs": true,
+ "noEmit": true,
+ "jsx": "react",
+ "jsxFactory": "h",
+ "jsxFragmentFactory": "hFragment",
+ "types": [
+ "@violentmonkey/types",
+ "@types/chrome"
+ ],
+ "lib": [
+ "DOM",
+ "ES2020"
+ ],
+ // needs to be in the deepest dir used as target in the rollup build
+ // https://stackoverflow.com/q/40460790, https://github.com/rollup/plugins/issues/243
+ "outDir": "builds/test/crx/tsOutput",
+ },
+ "exclude": [
+ "builds/test/tsOutput"
+ ]
+}