lordwelch 6fab6022d0 Remove grab
Switch to using bootstrap-table
Add DownThemAll plugin
Remove Firefox plugin
2021-01-09 16:36:18 -08:00

381 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
// License: MIT
import * as psl from "psl";
import { identity, memoize } from "./memoize";
import { IPReg } from "./ipreg";
export { debounce } from "../uikit/lib/util";
export class Promised {
private promise: Promise<any>;
resolve: (value?: any) => void;
reject: (reason?: any) => void;
constructor() {
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
then(
resolve: (value: any) => any,
reject: (reason: any) => PromiseLike<never>) {
return this.promise.then(resolve).catch(reject);
}
}
export function timeout<T>(to: number) {
return new Promise<T>((resolve, reject) => {
setTimeout(() => reject(new Error("timeout")), to);
});
}
export function lazy<T>(object: any, name: string, fun: (...any: any[]) => T) {
Object.defineProperty(object, name, {
get() {
const value = fun();
Object.defineProperty(object, name, {
value,
enumerable: true, writable: true, configurable: true
});
return value;
},
enumerable: true, configurable: true
});
return object;
}
export function none() { /* ignored */ }
export function sanitizePathGeneric(path: string) {
return path.
replace(/:+/g, "ː").
replace(/\?+/g, "_").
replace(/\*+/g, "_").
replace(/<+/g, "◄").
replace(/>+/g, "▶").
replace(/"+/g, "'").
replace(/\|+/g, "¦").
replace(/#+/g, "♯").
replace(/[.\s]+$/g, "").
trim();
}
const REG_TRIMMORE = /^[\s.]+|[\s.]+$/g;
const REG_RESERVED = new RegExp(
`^(?:${"CON, PRN, AUX, NUL, COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9, LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, LPT9".split(", ").join("|")})(?:\\..*)$`,
"i");
export function sanitizePathWindows(path: string) {
path = path.
replace(/:+/g, "ː").
replace(/\?+/g, "_").
replace(/\*+/g, "_").
replace(/<+/g, "◄").
replace(/>+/g, "▶").
replace(/"+/g, "'").
replace(/\|+/g, "¦").
replace(/#+/g, "♯").
replace(/[.\s]+$/g, "").
replace(REG_TRIMMORE, "");
// Legacy file names
if (REG_RESERVED.test(path)) {
path = `!${path}`;
}
return path;
}
// Cannot use browser.runtime here
export const IS_WIN = typeof navigator !== "undefined" &&
navigator.platform &&
navigator.platform.includes("Win");
export const sanitizePath = identity(
IS_WIN ? sanitizePathWindows : sanitizePathGeneric);
export class PathInfo {
private baseField: string;
private extField: string;
private pathField: string;
private nameField: string;
private fullField: string;
constructor(base: string, ext: string, path: string) {
this.baseField = base;
this.extField = ext;
this.pathField = path;
this.update();
}
get base() {
return this.baseField;
}
set base(nv) {
this.baseField = sanitizePath(nv);
this.update();
}
get ext() {
return this.extField;
}
set ext(nv) {
this.extField = sanitizePath(nv);
this.update();
}
get name() {
return this.nameField;
}
get path() {
return this.pathField;
}
set path(nv) {
this.pathField = sanitizePath(nv);
this.update();
}
get full() {
return this.fullField;
}
private update() {
this.nameField = this.extField ? `${this.baseField}.${this.extField}` : this.baseField;
this.fullField = this.pathField ? `${this.pathField}/${this.nameField}` : this.nameField;
}
clone() {
return new PathInfo(this.baseField, this.extField, this.pathField);
}
}
// XXX cleanup + test
export const parsePath = memoize(function parsePath(
path: string | URL): PathInfo {
if (path instanceof URL) {
path = decodeURIComponent(path.pathname);
}
path = path.trim().replace(/\\/g, "/");
const pieces = path.split("/").
map((e: string) => sanitizePath(e)).
filter((e: string) => e && e !== ".");
const name = path.endsWith("/") ? "" : pieces.pop() || "";
const idx = name.lastIndexOf(".");
let base = name;
let ext = "";
if (idx >= 0) {
base = sanitizePath(name.substr(0, idx));
ext = sanitizePath(name.substr(idx + 1));
}
for (let i = 0; i < pieces.length;) {
if (pieces[i] !== "..") {
++i;
continue;
}
if (i === 0) {
throw Error("Invalid traversal");
}
pieces.slice(i - 1, 2);
}
path = pieces.join("/");
return new PathInfo(base, ext, path);
});
export class CoalescedUpdate<T> extends Set<T> {
private readonly to: number;
private readonly cb: Function;
private triggerTimer: any;
constructor(to: number, cb: Function) {
super();
this.to = to;
this.cb = cb;
this.triggerTimer = 0;
this.trigger = this.trigger.bind(this);
Object.seal(this);
}
add(s: T) {
if (!this.triggerTimer) {
this.triggerTimer = setTimeout(this.trigger, this.to);
}
return super.add(s);
}
trigger() {
this.triggerTimer = 0;
if (!this.size) {
return;
}
const a = Array.from(this);
this.clear();
this.cb(a);
}
}
export const hostToDomain = memoize(psl.get, 1000);
export interface URLd extends URL {
domain: string;
}
Object.defineProperty(URL.prototype, "domain", {
get() {
try {
const {hostname} = this;
return IPReg.test(hostname) ?
hostname :
hostToDomain(hostname) || hostname;
}
catch (ex) {
console.error(ex);
return this.host;
}
},
enumerable: true,
configurable: false,
});
/**
* Filter arrays in-situ. Like Array.filter, but in place
*
* @param {Array} arr
* @param {Function} cb
* @param {Object} tp
* @returns {Array} Filtered array (identity)
*/
export function filterInSitu<T>(
arr: (T | null | undefined)[], cb: (value: T) => boolean, tp?: any) {
tp = tp || null;
let i; let k; let e;
const carr = arr as unknown as T[];
for (i = 0, k = 0, e = arr.length; i < e; i++) {
const a = arr[i]; // replace filtered items
if (!a) {
continue;
}
if (cb.call(tp, a, i, arr)) {
carr[k] = a;
k += 1;
}
}
carr.length = k; // truncate
return carr;
}
/**
* Map arrays in-situ. Like Array.map, but in place.
* @param {Array} arr
* @param {Function} cb
* @param {Object} tp
* @returns {Array} Mapped array (identity)
*/
export function mapInSitu<TRes, T>(arr: T[], cb: (value: T) => TRes, tp?: any) {
tp = tp || null;
const carr = arr as unknown as TRes[];
for (let i = 0, e = arr.length; i < e; i++) {
carr[i] = cb.call(tp, arr[i], i, arr);
}
return carr;
}
/**
* Filters and then maps an array in-situ
* @param {Array} arr
* @param {Function} filterStep
* @param {Function} mapStep
* @param {Object} tp
* @returns {Array} Filtered and mapped array (identity)
*/
export function filterMapInSitu<TRes, T>(
arr: T[],
filterStep: (value: T) => boolean,
mapStep: (value: T) => TRes,
tp?: any) {
tp = tp || null;
const carr = arr as unknown as TRes[];
let i; let k; let e;
for (i = 0, k = 0, e = arr.length; i < e; i++) {
const a = arr[i]; // replace filtered items
if (a && filterStep.call(tp, a, i, arr)) {
carr[k] = mapStep.call(tp, a, i, arr);
k += 1;
}
}
carr.length = k; // truncate
return carr;
}
/**
* Map and then filter an array in place
*
* @param {Array} arr
* @param {Function} mapStep
* @param {Function} filterStep
* @param {Object} tp
* @returns {Array} Mapped and filtered array (identity)
*/
export function mapFilterInSitu<TRes, T>(
arr: T[],
mapStep: (value: T) => TRes | null | undefined,
filterStep: (value: T) => boolean,
tp?: any) {
tp = tp || null;
const carr = arr as unknown as TRes[];
let i; let k; let e;
for (i = 0, k = 0, e = arr.length; i < e; i++) {
const a = carr[k] = mapStep.call(tp, arr[i], i, arr);
if (a && filterStep.call(tp, a, i, arr)) {
k += 1;
}
}
carr.length = k; // truncate
return carr;
}
/**
* Get a random integer
* @param {Number} min
* @param {Number} max
* @returns {Number}
*/
export function randint(min: number, max: number) {
return Math.floor(Math.random() * (max - min)) + min;
}
export function validateSubFolder(folder: string) {
if (!folder) {
return;
}
folder = folder.replace(/[/\\]+/g, "/");
if (folder.startsWith("/")) {
throw new Error("error.noabsolutepath");
}
if (/^[a-z]:\//i.test(folder)) {
throw new Error("error.noabsolutepath");
}
if (/^\.+\/|\/\.+\/|\/\.+$/g.test(folder)) {
throw new Error("error.nodotsinpath");
}
}