Itsnotlupus' Tiny Utilities

small utilities that I'm tired of digging from old scripts to put in new ones.

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/468394/1247001/Itsnotlupus%27%20Tiny%20Utilities.js

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Itsnotlupus' Tiny Utilities
// @namespace    Itsnotlupus Industries
// @version      1.27.1
// @description  small utilities that I'm tired of digging from old scripts to put in new ones.
// @author       Itsnotlupus
// @license      MIT
// ==/UserScript==

/* jshint esversion:11 */
/* jshint -W138 */

/** DOM queries - CSS selectors and XPath */
const $ = (q,d=document)=>d.querySelector(q);
const $$ = (q,d=document)=>d.querySelectorAll(q);
const $$$ = (q,d=document,x=d.evaluate(q,d),a=[],n=x.iterateNext()) => n ? (a.push(n), $$$(q,d,x,a)) : a;

/** calls a function whenever the DOM changes */
const observeDOM = (fn, e=document, config = { attributes: 1, childList: 1, subtree: 1 }, o = new MutationObserver(fn)) => (o.observe(e,config),()=>o.disconnect());

/** check a condition upfront, and on every DOM change until true */
const untilDOM = async (v, e=document, f=v.sup?()=>$(v,e):v) => f() || new Promise((r,_,d = observeDOM(() => (_=f()) && d() | r(_), e)) => 0);

/** promisify setTimeout and setInterval */
const sleep = (w = 100) => new Promise(r=>setTimeout(r, w));
const until = async (v, w=100, t, f=v.sup?()=>$(v):v) => f() || new Promise(r => t=setInterval((s=f()) => s && (clearInterval(t), r(s)), w));

/** slightly less painful syntax to create DOM trees */
const crel = (name, attrs, ...children) => ((e = Object.assign(document.createElement(name), attrs)) => (children.length && e.append(...children), e))();

/** same, for SVG content. */
const svg = (name, attrs={}, ...children) => ((e=document.createElementNS('http://www.w3.org/2000/svg', name), _=Object.keys(attrs).forEach(k=>e.setAttribute(k,attrs[k])),__=children.length && e.append(...children)) => e)();

/** create a shadow dom with an isolated stylesheet. tbh you're better off just creating a custom element. */
const custom = (name, css, dom, e = crel(name), ss = e.attachShadow({mode:'closed'}), s = new CSSStyleSheet(), t = ss.adoptedStyleSheets = [ (s.replaceSync(css),s) ], u = dom.length && ss.append(...dom)) => e;

/** add a stylesheet */
const addStyles = async css => (await untilDOM('head')).append(crel('style', { type: 'text/css', textContent: css }));

/** decode HTML entities in a string */
const decodeEntities = str => crel('textarea', { innerHTML: str }).value;

/** stolen from https://gist.github.com/nmsdvid/8807205 */
const slowDebounce = (a,b=250,c=0)=>(...d)=>clearTimeout(c,c=setTimeout(a,b,...d));

/** microtask debounce */
const fastDebounce = (f, l, s=0) => async (...a) => (l = a, !s && (await (s=1), s = 0, f(...l)));

/** remember and shortcut what a (pure) function returns */
const memoize = (f, mkKey=args=>args.join(), cache = Object.create(null)) => (...args) => cache[mkKey(args)] ??= f(...args);

/** given an acyclic graph `obj`, visit every node recursively, depth first. 
 *  call fn(obj[key], obj, key) on every non-root node. If fn returns `false`, don't traverse that section further. */
const traverse = (obj, fn) => obj && typeof obj == 'object' && Object.keys(obj).forEach(key => fn(obj[key], obj, key) !== false && traverse(obj[key], fn) );

/** requestAnimationFrame wrapper that allows a callback to request another run without referencing itself 
 * Use as: 
 * rAF((time, next) => {
 *   // cool animation code goes here.
 *   next(); // run again next frame
 * });
 */
const rAF = (f, n=t=>f(t,r), r=_=>requestAnimationFrame(n)) => r();

/** define a few event listeners in one shot - call the returned function to remove them. */
const events = (o, t=window, opts, f=op=>Object.keys(o).forEach(e=>t[op](e,o[e],opts))) => (f("addEventListener"), () => f("removeEventListener"));

/** insta-drag handler. just add callbacks. */
function makeDraggable(elt, update, init=update, final=()=>{}) {
  return events({
    pointerdown(e) { elt.setPointerCapture(e.pointerId, e.preventDefault(init(e))) },
    pointermove(e) { elt.hasPointerCapture(e.pointerId) && update(e) },
    pointerup(e) { elt.releasePointerCapture(e.pointerId, final(e)) }
  }, elt, true);
}

/** promisify a @grant-less XHR. probably useless. */
const xhr = (url, type='') => new Promise((r,e,x=Object.assign(new XMLHttpRequest(), {responseType: type,onload() { r(x.response); },onerror:e}),_=x.open('GET',url)) => x.send());

/** fetch and parse */
const fetchDOM = (url, mimeType) => fetch(url).then(r=>r.text()).then(t=>new DOMParser().parseFromString(t,mimeType));
const fetchHTML = url => fetchDOM(url, 'text/html');
const fetchJSON = url => fetch(url).then(r=>r.json());

/** Prefetch a URL */
const prefetch = url => document.head.append(crel('link', { rel: 'prefetch', href: url }));

/** Some sites break the `console` API. This attempts to restore a working console object. */
const fixConsole = (i=crel('iframe',{style:'display:none'}),_=document.body.append(i),c=unsafeWindow.console) => console.log.name!='log' ? (unsafeWindow.console = i.contentWindow.console,()=>(i.remove(),unsafeWindow.console=c)):()=>{};

/** Another take on logging */
let logger = console;
/** a cheap way to get logs to show up on sites that damaged their console.log */
async function withLogs(f) {
  if (logger.log.name == 'log') return await f();
  const iframe=crel('iframe', { style: 'display:none' });
  document.body.append(iframe);
  const prevLogger = logger;
  logger = iframe.contentWindow.console;
  try {
    return await f();
  } finally {
    iframe.remove();
    logger = prevLogger;
  }
}
const logg = (type, color={log:'#ccf',warn:'#fcf',error:'#fcc'}[type]) => (msg, ...args) => logger[type](`%c ${GM_info.script.name}: ${msg}`, 'font-weight:600;color:${color};background:#114;padding:.2em', ...args);
const log = logg('log');
const warn = logg('warn');
const error = logg('error');

const logGroup = (msg, ...args) => {
  logger.groupCollapsed(`%c ${GM_info.script.name}: ${msg}`, 'font-weight:600;color:#ccf;background:#114;padding:.2em');
  args.forEach(arg=>Array.isArray(arg)?logger.log(...arg):logger.log(arg));
  logger.groupEnd();
};