bugs and types

This commit is contained in:
Lalle 2023-04-21 05:34:54 +02:00
parent 759be29bd1
commit 6b81346b46
No known key found for this signature in database
GPG Key ID: A6583D207A8F6B0D
12 changed files with 343 additions and 378 deletions

View File

@ -89,9 +89,6 @@ var Get = {
}
}
// First:
// In every posts,
// if it did quote this post,
// get all their backlinks.
posts.forEach(function (qPost) {
if (qPost.quotes.includes(fullID)) {
return handleQuotes(qPost, 'quotelinks')
@ -99,10 +96,6 @@ var Get = {
})
// Second:
// If we have quote backlinks:
// in all posts this post quoted
// and their clones,
// get all of their backlinks.
if (Conf['Quote Backlinks']) {
for (var quote of post.quotes) {
var qPost
@ -113,7 +106,6 @@ var Get = {
}
// Third:
// Filter out irrelevant quotelinks.
return quotelinks.filter(function (quotelink) {
const { boardID, postID } = Get.postDataFromLink(quotelink)
return boardID === post.board.ID && postID === post.ID

View File

@ -12,13 +12,7 @@ import Settings from './Settings'
import UI from './UI'
import meta from '../../package.json'
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS104: Avoid inline assignments
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
var Header = {
init() {
$.onExists(doc, 'body', () => {

View File

@ -143,10 +143,8 @@ const Test = {
testOne(post) {
Test.postsRemaining++
return $.cache(
g.SITE.urls.threadJSON({
boardID: post.boardID,
threadID: post.threadID,
}),
g.SITE.urls.threadJSON(post.thread.ID, post.board),
{ responseType: 'json' },
function () {
if (!this.response) {
return
@ -179,7 +177,7 @@ const Test = {
for (var key in Config.filter) {
if (
!key === 'General' &&
key !== 'MD5' ||
!(key === 'MD5' && post.board.ID === 'f')
) {
var val1 = Filter.values(key, obj)

View File

@ -115,12 +115,12 @@ var ImageLoader = {
if (!replace && !ImageLoader.prefetchEnabled) {
return
}
if ($.hasClass(doc, 'catalog-mode')) {
if ($.hasClass(d, 'catalog-mode')) {
return
}
if (
![post, ...Array.from(post.clones)].some((clone) =>
doc.contains(clone.nodes.root),
d.contains(clone.nodes.root),
)
) {
return
@ -175,12 +175,13 @@ var ImageLoader = {
return g.posts.forEach(function (post) {
for (post of [post, ...Array.from(post.clones)]) {
for (var file of post.files) {
if (file.videoThumb) {
var { thumb } = file
if (Header.isNodeVisible(thumb) || post.nodes.root === qpClone) {
thumb.play()
} else {
thumb.pause()
if (file.isVideo && !file.isPrefetched) {
const { thumb } = file
if (qpClone === thumb) {
continue
}
if (thumb.getBoundingClientRect().top < window.innerHeight) {
ImageLoader.prefetch(post, file)
}
}
}

View File

@ -130,8 +130,10 @@ var Volume = {
if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey) {
return
}
if (!(el = $('video:not([data-md5])', this))) {
return
if (this instanceof HTMLAnchorElement) {
el = this.querySelector('video')
} else {
el = this.querySelector('video, audio')
}
if (el.muted || !$.hasAudio(el)) {
return

View File

@ -9,6 +9,7 @@ export default class CatalogThreadNative {
siteID: number
threadID: number
ID: string
thread: any
toString() {
return this.ID
}

View File

@ -1,4 +1,4 @@
function $$(selector: string, root: HTMLElement = document.body): HTMLElement[] {
function $$(selector: string, root: HTMLElement = document.body): HTMLElement[] | HTMLAnchorElement[] {
return Array.from(root.querySelectorAll(selector));
}
export default $$;

View File

@ -1,21 +1,14 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS205: Consider reworking code to avoid use of IIFEs
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
// loosely follows the jquery api:
// http://api.jquery.com/
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";
import { AjaxPageOptions, ElementProperties } from "../types/$";
// not chainable
const $ = (selector, root = document.body) => root.querySelector(selector);
type AjaxPageRequest = XMLHttpRequest & {
abort: () => void;
}
$.id = id => d.getElementById(id);
$.cache = dict();
$.ajaxPage = function (url, options) {
@ -344,19 +337,9 @@ $.addStyle = function (css, id, test = 'head') {
};
$.addCSP = function (policy) {
const meta = $.el('meta', {
httpEquiv: 'Content-Security-Policy',
content: policy
}
);
if (d.head) {
$.add(d.head, meta);
return $.rm(meta);
} else {
const head = $.add((doc || d), $.el('head'));
$.add(head, meta);
return $.rm(head);
}
const meta = $.el('meta', { httpEquiv: 'Content-Security-Policy', content: policy }, { display: 'none' });
$.onExists(doc, 'head', () => $.add(d.head, meta));
return meta;
};
$.x = function (path, root) {
@ -413,11 +396,10 @@ $.before = (root, el) => root.parentNode.insertBefore($.nodes(el), root);
$.replace = (root, el) => root.parentNode.replaceChild($.nodes(el), root);
$.el = function (tag, properties, properties2) {
const el = d.createElement(tag);
if (properties) { $.extend(el, properties); }
if (properties2) { $.extend(el, properties2); }
return el;
$.el = function (tag: keyof HTMLElementTagNameMap, properties?: ElementProperties, properties2?: ElementProperties): HTMLElement {
const element = document.createElement(tag);
Object.assign(element, properties, properties2);
return element;
};
$.on = function (el, events, handler) {
@ -555,42 +537,41 @@ $.global = function (fn, data) {
};
$.bytesToString = function (size) {
let unit = 0; // Bytes
while (size >= 1024) {
size /= 1024;
unit++;
if (size < 1024) {
return `${size} B`;
} else if (size < 1048576) {
return `${(size / 1024).toFixed(1)} KB`;
} else if (size < 1073741824) {
return `${(size / 1048576).toFixed(1)} MB`;
} else {
return `${(size / 1073741824).toFixed(1)} GB`;
}
// Remove trailing 0s.
size =
unit > 1 ?
// Keep the size as a float if the size is greater than 2^20 B.
// Round to hundredth.
Math.round(size * 100) / 100
:
// Round to an integer otherwise.
Math.round(size);
return `${size} ${['B', 'KB', 'MB', 'GB'][unit]}`;
};
$.minmax = (value, min, max) => value < min ?
min
:
value > max ?
max
:
value;
$.minmax = (value, min, max) => Math.max(min, Math.min(max, value));
$.hasAudio = video => video.mozHasAudio || !!video.webkitAudioDecodedByteCount;
$.hasAudio = function (el: HTMLVideoElement | HTMLAudioElement) {
if (el.tagName === 'VIDEO') {
return !el.muted;
} else if (el.tagName === 'AUDIO') {
return true;
} else {
return el.querySelector('video:not([muted]), audio') != null;
}
};
$.luma = rgb => (rgb[0] * 0.299) + (rgb[1] * 0.587) + (rgb[2] * 0.114);
$.luma = (rgb: number[]) => { // rgb: [r, g, b]
const [r, g, b] = rgb;
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
$.unescape = function (text) {
if (text == null) { return text; }
return text.replace(/<[^>]*>/g, '').replace(/&(amp|#039|quot|lt|gt|#44);/g, c => ({ '&amp;': '&', '&#039;': "'", '&quot;': '"', '&lt;': '<', '&gt;': '>', '&#44;': ',' })[c]);
};
$.isImage = url => /\.(jpe?g|jfif|png|gif|bmp|webp|avif|jxl)$/i.test(url);
$.isVideo = url => /\.(webm|mp4|ogv)$/i.test(url);
$.isImage = url => /\.(jpe?g|png|gif|bmp|webp|svg|ico|tiff?)$/i.test(url);
$.isVideo = url => /\.(webm|mp4|og[gv]|m4v|mov|avi|flv|wmv|mpg|mpeg|mkv|rm|rmvb|3gp|3g2|asf|swf|vob)$/i.test(url);
$.engine = (function () {
if (/Edge\//.test(navigator.userAgent)) { return 'edge'; }

View File

@ -1,289 +0,0 @@
import QR from '../Posting/QR'
import $ from './$'
import { dict, platform } from './helpers'
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS205: Consider reworking code to avoid use of IIFEs
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
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()) {
// 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 (platform === 'crx') {
eventPageRequest(
{ type: 'ajax', url, headers, responseType: 'arraybuffer' },
function ({ response, responseHeaderString }) {
if (response) {
response = new Uint8Array(response)
}
return cb(response, responseHeaderString)
},
)
} 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',
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++
}
}
return cb(data, xhr.responseHeaders)
},
onerror() {
return cb(null)
},
onabort() {
return cb(null)
},
}
try {
return (GM?.xmlHttpRequest || GM_xmlhttpRequest)(gmOptions)
} catch (error) {
return fallback()
}
}
},
file(url, cb) {
return CrossOrigin.binary(url, function (data, headers) {
if (data == null) {
return cb(null)
}
let name = url.match(/([^\/?#]+)\/*(?:$|[?#])/)?.[1]
const contentType = headers.match(/Content-Type:\s*(.*)/i)?.[1]
const contentDisposition = headers.match(
/Content-Disposition:\s*(.*)/i,
)?.[1]
let mime = contentType?.match(/[^;]*/)[0] || 'application/octet-stream'
const match =
contentDisposition?.match(/\bfilename\s*=\s*"((\\"|[^"])+)"/i)?.[1] ||
contentType?.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)?.[1]
if (match) {
name = match.replace(/\\"/g, '"')
}
if (/^text\/plain;\s*charset=x-user-defined$/i.test(mime)) {
// In JS Blocker (Safari) content type comes back as 'text/plain; charset=x-user-defined'; guess from filename instead.
mime =
$.getOwn(
QR.typeFromExtension,
name.match(/[^.]*$/)[0].toLowerCase(),
) || 'application/octet-stream'
}
const blob = new Blob([data], { type: mime })
blob.name = name
return cb(blob)
})
},
Request: (function () {
const Request = class Request {
static initClass() {
this.prototype.status = 0
this.prototype.statusText = ''
this.prototype.response = null
this.prototype.responseHeaderString = null
}
getResponseHeader(headerName) {
if (this.responseHeaders == null && this.responseHeaderString != null) {
this.responseHeaders = dict()
for (var header of this.responseHeaderString.split('\r\n')) {
var i
if ((i = header.indexOf(':')) >= 0) {
var key = header.slice(0, i).trim().toLowerCase()
var val = header.slice(i + 1).trim()
this.responseHeaders[key] = val
}
}
}
return this.responseHeaders?.[headerName.toLowerCase()] ?? null
}
abort() {}
onloadend() {}
}
Request.initClass()
return Request
})(),
// Attempts to fetch `url` using cross-origin privileges, if available.
// Interface is a subset of that of $.ajax.
// Options:
// `onloadend` - called with the returned object as `this` on success or error/abort/timeout.
// `timeout` - time limit for request
// `responseType` - expected response type, 'json' by default; 'json' and 'text' supported
// `headers` - request headers
// Returned object properties:
// `status` - HTTP status (0 if connection not successful)
// `statusText` - HTTP status text
// `response` - decoded response body
// `abort` - function for aborting the request (silently fails on some platforms)
// `getResponseHeader` - function for reading response headers
ajax(url, options = {}) {
let gmReq
let { onloadend, timeout, responseType, headers } = options
if (responseType == null) {
responseType = 'json'
}
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 (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 (gmReq && typeof gmReq.abort === 'function') {
req.abort = function () {
try {
return gmReq.abort()
} catch (error1) {}
}
}
} else {
eventPageRequest(
{ type: 'ajax', url, responseType, headers, timeout },
function (result) {
if (result.status) {
$.extend(req, result)
}
return req.onloadend()
},
)
}
return req
},
cache(url, cb) {
if (platform === 'userscript') {
return CrossOrigin.file(url, cb)
}
return eventPageRequest({ type: 'cache', url }, function (result) {
if (result) {
return cb(result)
} else {
return cb(null)
}
})
},
permission(cb, cbFail, origins) {
if (platform === 'crx') {
return eventPageRequest(
{ type: 'permission', origins },
function (result) {
if (result) {
return cb()
} else {
return cbFail()
}
},
)
}
return cb()
},
}
export default CrossOrigin

264
src/platform/CrossOrigin.ts Normal file
View File

@ -0,0 +1,264 @@
import { Options } from '../../node_modules/@vitejs/plugin-react/dist/index';
import QR from '../Posting/QR';
import $ from './$';
import { dict, platform } from './helpers';
type Callback = (response: any, responseHeaderString?: string) => void;
type GMXhrCallback = (xhr: XMLHttpRequestResponseType) => void;
let eventPageRequest: ((params: any, cb: Callback) => void) | undefined;
if (platform === 'crx') {
eventPageRequest = (function () {
const callbacks: { [id: string]: Callback } = {};
chrome.runtime.onMessage.addListener(function (response) {
callbacks[response.id](response.data);
return delete callbacks[response.id];
});
return (params: any, cb: Callback) =>
chrome.runtime.sendMessage(params, (id) => (callbacks[id] = cb));
})();
}
interface ICrossOrigin {
binary(url: string, cb: Callback, headers?: typeof dict): void;
file(url: string, cb: Callback): void;
Request: any;
ajax(url: string, options?: any): any;
cache(url: string, cb: Callback): void;
permission(cb: () => void, cbFail: () => void, origins?: any): void;
}
const CrossOrigin: ICrossOrigin = {
binary(url, cb, headers = dict()) {
url = url.replace(
/^((?:https?:)?\/\/(?:\w+\.)?(?:4chan|4channel|4cdn)\.org)\/adv\//,
'$1//adv/',
);
if (platform === 'crx') {
eventPageRequest?.(
{ type: 'ajax', url, headers, responseType: 'arraybuffer' },
function ({ response, responseHeaderString }) {
if (response) {
response = new Uint8Array(response);
}
return cb(response, responseHeaderString);
},
);
} 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: any = {
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++;
}
}
return cb(data, xhr.responseHeaders);
},
onerror: () => cb(null),
onabort: () => cb(null),
};
try {
return window.GM_xmlhttpRequest(gmOptions);
} catch (error) {
return fallback();
}
}
},
file(url, cb) {
return CrossOrigin.binary(url, function (data, headers) {
if (data == null) {
return cb(null);
}
let name = url.match(/([^\/?#]+)\/*(?:$|[?#])/)?.[1];
const contentType = headers.match(/Content-Type:\s*(.*)/i)?.[1];
const contentDisposition = headers.match(
/Content-Disposition:\s*(.*)/i,
)?.[1];
let mime =
contentType?.match(/[^;]*/)[0] || 'application/octet-stream';
const match =
contentDisposition?.match(/\bfilename\s*=\s*"((\\"|[^"])+)"/i)?.[1] ||
contentType?.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)?.[1];
if (match) {
name = match.replace(/\\"/g, '"');
}
if (/^text\/plain;\s*charset=x-user-defined$/i.test(mime)) {
mime =
$.getOwn(
QR.typeFromExtension,
name.match(/[^.]*$/)[0].toLowerCase(),
) || 'application/octet-stream';
}
const blob = new Blob([data], { type: mime });
return cb({ name, blob });
});
},
Request: (function () {
class Request {
status: number;
statusText: string;
response: any;
responseHeaders: string;
constructor() {
this.status = 0;
this.statusText = '';
this.response = null;
this.responseHeaders = '';
}
getResponseHeader(headerName: string) {
const match = this.getResponseHeader
.toString()
.match(new RegExp(`^${headerName}: (.*)`, 'im'));
return match?.[1];
}
abort() {}
onloadend() {}
}
return Request;
})(),
ajax(url, options: any = {}) {
let gmReq: any;
let { onloadend, timeout, responseType, headers } = options;
if (responseType == null) {
responseType = 'json';
}
if (onloadend == null) {
onloadend = function () {};
} else {
onloadend = onloadend.bind(this);
}
const req = new CrossOrigin.Request();
req.onloadend = onloadend;
if (platform === 'userscript') {
const gmOptions: any = {
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;
}
})();
Object.assign(req, {
response,
status: xhr.status,
statusText: xhr.statusText,
responseHeaderString: xhr.responseHeaders,
});
} catch (error) {}
return req.onloadend();
},
onerror: () => req.onloadend(),
onabort: () => req.onloadend(),
ontimeout: () => 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) {}
};
}
} else {
eventPageRequest?.(
{ type: 'ajax', url, headers, responseType, timeout },
function ({ response, responseHeaderString }) {
Object.assign(req, {
response,
status: 200,
statusText: 'OK',
responseHeaderString,
});
return req.onloadend();
}
);
}
return req;
},
cache(url, cb) {
const cached = CrossOrigin.cache[url];
if (cached) {
return cb(cached);
} else {
return CrossOrigin.binary(url, function (data) {
if (data == null) {
return cb(null);
}
const blob = new Blob([data]);
CrossOrigin.cache[url] = blob;
return cb(blob);
});
}
},
permission(cb, cbFail, origins) {
if (platform === 'crx') {
return eventPageRequest(
{ type: 'permission', origins },
function (result) {
if (result) {
return cb()
} else {
return cbFail()
}
}
)
} else {
return cb()
}
},
};
export default CrossOrigin;

View File

@ -287,8 +287,8 @@ $\
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 node of $$('.abbr, .abbr-exp', abbr)) {
$.replace(node, $.tn(node.textContent));
}
for (let i = 0; i < 2; i++) {
var br;
@ -337,7 +337,9 @@ $\
testNativeExtension() {
return $.global(function() {
if (window.Parser?.postMenuIcon) { return this.enabled = 'true'; }
if (window.File && window.FileReader && window.FileList && window.Blob) {
return true;
}
});
},
@ -413,10 +415,15 @@ $\
const o = {
// id
ID: data.no,
info: null,
capcodeHighlight: data.capcode === 'admin_highlight',
files: [],
file: null,
postID: data.no,
threadID: data.resto || data.no,
boardID,
siteID,
extra: {},
isReply: !!data.resto,
// thread status
isSticky: !!data.sticky,
@ -471,6 +478,7 @@ $\
name: ($.unescape(data.filename)) + data.ext,
url: site.urls.file({ siteID, boardID }, filename),
height: data.h,
dimensions: '',
width: data.w,
MD5: data.md5,
size: $.bytesToString(data.fsize),
@ -579,7 +587,7 @@ $\
$.extend(container, wholePost);
// Fix quotelinks
for (var quote of $$('.quotelink', container)) {
for (var quote of container.querySelectorAll('a.quoteLink')) {
var href = quote.getAttribute('href');
if (href[0] === '#') {
if (!this.sameThread(boardID, threadID)) {

13
src/types/$.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
export interface ElementProperties {
[key: string]: any;
}
interface AjaxPageOptions {
onloadend?: (this: XMLHttpRequest, ev: ProgressEvent<EventTarget>) => void;
timeout?: number;
responseType?: XMLHttpRequestResponseType;
withCredentials?: boolean;
type?: string;
onprogress?: (this: XMLHttpRequest, ev: ProgressEvent<EventTarget>) => void;
form?: FormData;
headers?: Record<string, string>;
}