USToolkit

simple toolkit to help me create userscripts

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/526417/1639004/USToolkit.js

  1. // ==UserScript==
  2. // @name USToolkit
  3. // @namespace https://gf.qytechs.cn/pt-BR/users/821661
  4. // @version 0.0.5
  5. // @run-at document-start
  6. // @author hdyzen
  7. // @description simple toolkit to help me create userscripts
  8. // @license MIT
  9. // ==/UserScript==
  10.  
  11. /**
  12. * Some functions are strongly inspired by:
  13. * github.com/violentmonkey/
  14. * github.com/gorhill/uBlock/
  15. *
  16. */
  17.  
  18. (() => {
  19. /**
  20. * Sets up a MutationObserver to watch for DOM changes and executes a callback function.
  21. * @param {function(MutationRecord[]): (boolean|void)} func The callback function to execute on mutation.
  22. * It receives an array of MutationRecord objects. If the function returns `true`, the observer is disconnected.
  23. * @param {MutationObserverInit} [options={ childList: true, subtree: true }] The options object for the MutationObserver.
  24. * @param {Node} [scope=document] The target node to observe.
  25. * @returns {MutationObserver} The created MutationObserver instance.
  26. */
  27. function observe(func, options = { childList: true, subtree: true }, scope = document) {
  28. const observer = new MutationObserver((mut) => {
  29. const shouldDisconnect = func(mut);
  30.  
  31. if (shouldDisconnect === true) {
  32. observer.disconnect();
  33. }
  34. });
  35.  
  36. observer.observe(scope, options);
  37.  
  38. return observer;
  39. }
  40.  
  41. class ObserverEverywhere extends MutationObserver {
  42. #callback;
  43. #options;
  44. #connectedInstances = new Set();
  45.  
  46. constructor(callback) {
  47. const wrapper = (mutations, observer) => {
  48. this.#handleMutations(mutations);
  49. if (this.#callback) {
  50. this.#callback(mutations, observer);
  51. }
  52. };
  53.  
  54. super(wrapper);
  55. this.#callback = callback;
  56. }
  57.  
  58. #handleMutations(mutations) {
  59. for (const mutation of mutations) {
  60. for (const node of mutation.addedNodes) {
  61. if (node.nodeType === Node.ELEMENT_NODE) {
  62. this.#scanNodeForShadows(node);
  63. }
  64. }
  65. }
  66. }
  67.  
  68. #scanNodeForShadows(node) {
  69. if (node.shadowRoot) {
  70. this.#createInstance(node.shadowRoot);
  71. }
  72.  
  73. const elementsWithShadow = node.querySelectorAll("*");
  74. elementsWithShadow.forEach((el) => {
  75. if (el.shadowRoot) {
  76. this.#createInstance(el.shadowRoot);
  77. }
  78. });
  79. }
  80.  
  81. #createInstance(root) {
  82. const newObserver = new ObserverEverywhere(this.#callback, root);
  83. newObserver.observe(root, this.#options);
  84. this.#connectedInstances.add(newObserver);
  85. }
  86.  
  87. observe(targetNode, options) {
  88. this.#options = options;
  89.  
  90. super.observe(targetNode, this.#options);
  91.  
  92. this.#scanNodeForShadows(targetNode);
  93. }
  94.  
  95. disconnect() {
  96. super.disconnect();
  97. this.#connectedInstances.forEach((instance) => instance.disconnect());
  98. this.#connectedInstances.clear();
  99. }
  100. }
  101.  
  102. class OnElement {
  103. #rules = new Map();
  104. #combinedSelector = "";
  105. #processedChildList = new WeakSet();
  106. #scope;
  107.  
  108. constructor(scope = document.documentElement) {
  109. this.#scope = scope;
  110. this.#observe();
  111. }
  112.  
  113. add(selector, fn) {
  114. this.#scope.querySelectorAll(selector).forEach((element) => {
  115. fn(element);
  116. this.#processedChildList.add(element);
  117. });
  118.  
  119. if (!this.#rules.has(selector)) {
  120. this.#rules.set(selector, []);
  121. }
  122. this.#rules.get(selector).push(fn);
  123.  
  124. this.#updateSelector();
  125. }
  126.  
  127. once(selector, fn) {
  128. const onceFn = (element) => {
  129. fn(element);
  130. this.remove(selector, onceFn);
  131. };
  132.  
  133. this.add(selector, onceFn);
  134. }
  135.  
  136. remove(selector, fn) {
  137. const fns = this.#rules.get(selector);
  138. if (!fns) return;
  139.  
  140. if (!fn) {
  141. this.#rules.delete(selector);
  142. this.#updateSelector();
  143. return;
  144. }
  145.  
  146. const filteredFns = fns.filter((existingFn) => existingFn !== fn);
  147. if (!filteredFns.length) {
  148. this.#rules.delete(selector);
  149. this.#updateSelector();
  150. return;
  151. }
  152.  
  153. this.#rules.set(selector, filteredFns);
  154. this.#updateSelector();
  155. }
  156.  
  157. #observe() {
  158. const processedInBatch = new Set();
  159.  
  160. const unprocessElement = (elementToForget) => {
  161. this.#processedChildList.delete(elementToForget);
  162. };
  163.  
  164. const processElement = (element) => {
  165. if (processedInBatch.has(element) || this.#processedChildList.has(element)) return;
  166.  
  167. this.#applyRulesToElement(element);
  168. processedInBatch.add(element);
  169. this.#processedChildList.add(element);
  170. };
  171.  
  172. const observer = new MutationObserver((mutations) => {
  173. if (this.#rules.size === 0) return;
  174.  
  175. for (const mutation of mutations) {
  176. if (
  177. mutation.type === "attributes" &&
  178. this.#combinedSelector !== "" &&
  179. mutation.target.matches(this.#combinedSelector) &&
  180. !processedInBatch.has(mutation.target) &&
  181. mutation.oldValue !== mutation.target.getAttribute(mutation.attributeName)
  182. ) {
  183. this.#applyRulesToElement(mutation.target);
  184. processedInBatch.add(mutation.target);
  185. continue;
  186. }
  187.  
  188. for (const node of mutation.removedNodes) {
  189. if (node.nodeType !== Node.ELEMENT_NODE) continue;
  190.  
  191. unprocessElement(node);
  192.  
  193. node.querySelectorAll("*").forEach(unprocessElement);
  194. }
  195.  
  196. for (const node of mutation.addedNodes) {
  197. if (node.nodeType !== Node.ELEMENT_NODE || this.#combinedSelector === "") continue;
  198.  
  199. if (node.matches(this.#combinedSelector)) processElement(node);
  200.  
  201. node.querySelectorAll(this.#combinedSelector).forEach(processElement);
  202. }
  203. }
  204.  
  205. processedInBatch.clear();
  206. });
  207.  
  208. observer.observe(this.#scope, { childList: true, subtree: true, attributes: true, attributeOldValue: true });
  209. }
  210.  
  211. #applyRulesToElement(element) {
  212. for (const [selector, fns] of this.#rules.entries()) {
  213. if (!element.matches(selector)) continue;
  214.  
  215. fns.forEach((fn) => {
  216. if (fn(element)) this.remove(selector, fn);
  217. });
  218. }
  219. }
  220.  
  221. #updateSelector() {
  222. this.#combinedSelector = [...this.#rules.keys()].join(",");
  223. }
  224. }
  225.  
  226. function* queryAll(scope, selector) {
  227. for (const element of scope.querySelectorAll("*")) {
  228. if (element.matches(selector)) {
  229. yield element;
  230. }
  231.  
  232. if (element.shadowRoot) {
  233. yield* queryAll(element.shadowRoot, selector);
  234. }
  235. }
  236. }
  237.  
  238. function query(scope, selector) {
  239. const iterator = queryAll(scope, selector);
  240. const result = iterator.next();
  241. return result.done ? null : result.value;
  242. }
  243.  
  244. function injectScriptInline(code) {
  245. const script = document.createElement("script");
  246.  
  247. script.textContent = code;
  248. (document.head || document.documentElement).appendChild(script);
  249. script.remove();
  250. return;
  251. }
  252.  
  253. /**
  254. * Waits for an element that matches a given CSS selector to appear in the DOM.
  255. * @param {string} selector The CSS selector for the element to wait for.
  256. * @param {number} [timeout=5000] The maximum time to wait in milliseconds.
  257. * @param {Node} [scope=document] The scope within which to search.
  258. * @returns {Promise<HTMLElement>} A promise that resolves with the found element, or rejects on timeout.
  259. */
  260. function wait(selector, timeout = 5000, scope = document) {
  261. return new Promise((resolve, reject) => {
  262. const element = scope.querySelector(selector);
  263. if (element) {
  264. resolve(element);
  265. return true;
  266. }
  267.  
  268. const onMutation = (mutations) => {
  269. for (const mutation of mutations) {
  270. if (mutation.type === "attributes" && mutation.target.matches(selector)) {
  271. resolve(mutation.target);
  272. return true;
  273. }
  274.  
  275. for (const node of mutation.addedNodes) {
  276. if (node.nodeType !== Node.ELEMENT_NODE) {
  277. continue;
  278. }
  279.  
  280. if (node.matches(selector)) {
  281. resolve(node);
  282. return true;
  283. }
  284.  
  285. const target = node.querySelector(selector);
  286. if (target) {
  287. resolve(target);
  288. return true;
  289. }
  290. }
  291. }
  292. };
  293.  
  294. const options = {
  295. childList: true,
  296. subtree: true,
  297. attributes: true,
  298. };
  299.  
  300. if (onMutation([]) !== true) {
  301. observe(onMutation, options, scope);
  302. }
  303.  
  304. setTimeout(() => reject(`Element ${selector} not found`), timeout);
  305. });
  306. }
  307.  
  308. /**
  309. * Attaches a delegated event listener to a scope.
  310. * @param {string} event The name of the event (e.g., 'click').
  311. * @param {string} selector A CSS selector to filter the event target.
  312. * @param {function(Event): void} handler The event handler function.
  313. * @param {Node} [scope=document] The parent element to attach the listener to.
  314. */
  315. function on(event, selector, handler, scope = document) {
  316. scope.addEventListener(event, (e) => {
  317. if (e.target.matches(selector)) handler(e);
  318. });
  319. }
  320.  
  321. function createDeepProxy(target) {
  322. return new Proxy(target, {
  323. get(obj, prop) {
  324. console.log(obj, prop);
  325. const val = Reflect.get(obj, prop);
  326. if (typeof val === "object" && val !== null) {
  327. return createDeepProxy(val);
  328. }
  329. if (typeof val === "function") {
  330. return (...args) => {
  331. const res = val.apply(obj, args);
  332. return typeof res === "object" && res !== null ? createDeepProxy(res) : res;
  333. };
  334. }
  335. return val;
  336. },
  337. });
  338. }
  339.  
  340. /**
  341. * Safely retrieves a nested property from an object using a string path.
  342. * Supports special wildcards for arrays ('[]') and objects ('{}' or '*').
  343. * @param {object} obj The source object.
  344. * @param {string} chain A dot-separated string for the property path (e.g., 'user.address.street').
  345. * @returns {*} The value of the nested property, or undefined if not found.
  346. */
  347. function safeGet(obj, chain) {
  348. if (!obj || typeof chain !== "string" || chain === "") {
  349. return;
  350. }
  351.  
  352. const props = chain.split(".");
  353. // const props = propChain.match(/'[^']*'|"[^"]*"|\[[^\]]*]|\([^)]*\)|{[^}]*}|[^.()[\]{}\n]+/g);
  354. // const props = parsePropChain(propChain);
  355. console.log("Tokens:", props);
  356. let current = obj;
  357.  
  358. for (let i = 0; i < props.length; i++) {
  359. const prop = props[i];
  360.  
  361. if (current === undefined || current === null) {
  362. break;
  363. }
  364.  
  365. // console.log(current, prop);
  366.  
  367. if (prop === "[]") {
  368. i++;
  369. current = handleArray(current, props[i]);
  370. continue;
  371. }
  372. if (prop === "{}" || prop === "*") {
  373. i++;
  374. current = handleObject(current, props[i]);
  375. continue;
  376. }
  377. if (startsEndsWith(prop, "(", ")")) {
  378. current = handleFunction(current, prop);
  379. continue;
  380. }
  381. if (startsEndsWith(prop, "[", "]")) {
  382. current = handleArrayQuery(current, prop);
  383. continue;
  384. }
  385. if (startsEndsWith(prop, "{", "}")) {
  386. current = handleObjectQuery(current, prop);
  387. continue;
  388. }
  389. if (startsEndsWith(prop, "'") || startsEndsWith(prop, '"')) {
  390. current = handleQuoted(current, prop);
  391. continue;
  392. }
  393. if (prop.startsWith("/") && /^\/.*\/[gmiyuvsd]+$/.test(prop)) {
  394. current = handlePattern(current, prop);
  395. continue;
  396. }
  397.  
  398. current = current[prop];
  399. }
  400.  
  401. return current;
  402. }
  403.  
  404. function handlePattern(obj, nextProp) {
  405. const lastIndexSlash = nextProp.lastIndexOf("/");
  406. const pattenString = nextProp.slice(1, lastIndexSlash);
  407. const flags = nextProp.slice(lastIndexSlash + 1);
  408. const pattern = new RegExp(pattenString, flags);
  409. const results = [];
  410.  
  411. for (const key of Object.keys(obj)) {
  412. pattern.lastIndex = 0;
  413. if (pattern.test(key)) {
  414. results.push(obj[key]);
  415. }
  416. }
  417.  
  418. return results;
  419. }
  420.  
  421. /**
  422. * Safely handles function calls from the property chain.
  423. * It parses arguments as JSON.
  424. * @param {function} fn The function to call.
  425. * @param {string} prop The string containing arguments, e.g., '({"name": "test"})'.
  426. * @returns {*} The result of the function call.
  427. */
  428. function handleFunction(fn, prop) {
  429. const argString = prop.slice(1, -1).trim().replaceAll("'", '"');
  430. let args;
  431.  
  432. if (argString === "") {
  433. return fn();
  434. }
  435.  
  436. try {
  437. args = JSON.parse(`[${argString}]`);
  438. } catch (err) {
  439. console.error(`[UST.safeGet] Failed to execute function in property chain "${prop}":`, err);
  440. }
  441.  
  442. return typeof fn === "function" ? fn(...args) : undefined;
  443. }
  444.  
  445. function handleQuoted(obj, prop) {
  446. return obj[prop.slice(1, -1)];
  447. }
  448.  
  449. /**
  450. * Filters an array of objects based on a query string in the format "[key=value]" or "[key]".
  451. *
  452. * @param {Array<Object>} arr - The array of objects to filter.
  453. * @param {string} rawQuery - The query string, expected to be in the format "[key=value]" or "[key]".
  454. * @returns {Array<Object>|undefined} The filtered array of objects if any match the query, otherwise undefined.
  455. */
  456. function handleArrayQuery(arr, rawQuery) {
  457. const query = rawQuery.slice(1, -1);
  458. const match = query.match(/[!?*=<>]+/);
  459. const operator = match[0];
  460. console.log("Operator:", operator);
  461.  
  462. const [key, rawValue] = query.split(operator || "");
  463. const arrayFiltered = [];
  464.  
  465. // TODO: Terminar de filtrar a query / Suportar regex na query / Suportar operador para pegar o objeto mais aninhado disponivel
  466. for (const item of arr) {
  467. const propValue = safeGet(item, key);
  468.  
  469. // if (!operator && propValue !== undefined) {
  470. // arrayFiltered.push(item);
  471. // continue;
  472. // }
  473.  
  474. const queryValue = parseValue(rawValue);
  475. console.log(arr, propValue, queryValue);
  476. if (
  477. (operator === "=" && propValue === queryValue) ||
  478. (operator === "!=" && propValue !== queryValue) ||
  479. (operator === ">=" && propValue >= queryValue) ||
  480. (operator === "<=" && propValue <= queryValue) ||
  481. (operator === "<" && propValue < queryValue) ||
  482. (operator === ">" && propValue > queryValue) ||
  483. (operator === "!" && propValue === undefined)
  484. ) {
  485. console.log("Retornando objeto");
  486. arrayFiltered.push(item);
  487. }
  488. }
  489.  
  490. return arrayFiltered;
  491. // const query = rawQuery.slice(1, -1);
  492. // const [key, rawValue] = query.split("=");
  493. // const filteredArray = [];
  494.  
  495. // for (const item of arr) {
  496. // const propValue = safeGet(item, key);
  497. // if (
  498. // (rawValue === undefined && propValue !== undefined) ||
  499. // (rawValue !== undefined && propValue === parseValue(rawValue))
  500. // ) {
  501. // filteredArray.push(item);
  502. // }
  503. // }
  504.  
  505. // return filteredArray.length ? filteredArray : undefined;
  506. }
  507.  
  508. /**
  509. * Handles a query on an object, checking if the object's property matches the query.
  510. *
  511. * @param {Object} obj - The object to query.
  512. * @param {string} rawQuery - The query string in the format "[key=value]" or "[key]".
  513. * @returns {Object|undefined} The object if the query matches, otherwise undefined.
  514. */
  515. function handleObjectQuery(obj, rawQuery) {
  516. const query = rawQuery.slice(1, -1);
  517. const [operator] = query.match(/[!?*=<>]+/);
  518. const [key, rawValue] = query.split(operator);
  519. const propValue = safeGet(obj, key);
  520.  
  521. if (!operator && propValue !== undefined) {
  522. return obj;
  523. }
  524.  
  525. const queryValue = parseValue(rawValue);
  526. if (
  527. (operator === "=" && propValue === queryValue) ||
  528. (operator === "!=" && propValue !== queryValue) ||
  529. (operator === ">=" && propValue >= queryValue) ||
  530. (operator === "<=" && propValue <= queryValue) ||
  531. (operator === "<" && propValue < queryValue) ||
  532. (operator === ">" && propValue > queryValue) ||
  533. (operator === "!" && propValue === undefined)
  534. ) {
  535. console.log("Retornando objeto");
  536. return obj;
  537. }
  538. }
  539.  
  540. function parseValue(value) {
  541. if (value === "true") {
  542. return true;
  543. }
  544. if (value === "false") {
  545. return false;
  546. }
  547. if (value === "null") {
  548. return null;
  549. }
  550. if (value === "undefined") {
  551. return undefined;
  552. }
  553. if (typeof value === "string" && (startsEndsWith(value, "'") || startsEndsWith(value, '"'))) {
  554. return value.slice(1, -1);
  555. }
  556. if (typeof value === "string" && value.trim() !== "") {
  557. const num = Number(value);
  558. return !Number.isNaN(num) ? num : value;
  559. }
  560.  
  561. return value;
  562. }
  563.  
  564. /**
  565. * Helper for `prop` to handle array wildcards. It maps over an array and extracts a property from each item.
  566. * @param {Array<object>} arr The array to process.
  567. * @param {string} nextProp The property to extract from each item.
  568. * @returns {*} An array of results, or a single result if only one is found.
  569. */
  570. function handleArray(arr, nextProp) {
  571. const results = [];
  572. for (const item of arr) {
  573. if (getProp(item, nextProp) !== undefined) {
  574. results.push(item);
  575. }
  576. }
  577.  
  578. return results;
  579. }
  580.  
  581. /**
  582. * Helper for `prop` to handle object wildcards. It maps over an object's values and extracts a property.
  583. * @param {object} obj The object to process.
  584. * @param {string} nextProp The property to extract from each value.
  585. * @returns {*} An array of results, or a single result if only one is found.
  586. */
  587. function handleObject(obj, nextProp) {
  588. const keys = Object.keys(obj);
  589. const results = [];
  590. for (const key of keys) {
  591. if (getProp(obj[key], nextProp) !== undefined) {
  592. results.push(obj[key]);
  593. }
  594. }
  595.  
  596. return results;
  597. }
  598.  
  599. /**
  600. * Safely gets an own property from an object.
  601. * @param {object} obj The source object.
  602. * @param {string} prop The property name.
  603. * @returns {*} The property value or undefined if it doesn't exist.
  604. */
  605. function getProp(obj, prop) {
  606. if (obj && Object.hasOwn(obj, prop)) {
  607. return obj[prop];
  608. }
  609.  
  610. return;
  611. }
  612.  
  613. /**
  614. * Checks if a value is a plain JavaScript object.
  615. * @param {*} val The value to check.
  616. * @returns {boolean} True if the value is a plain object, otherwise false.
  617. */
  618. function isObject(val) {
  619. return Object.prototype.toString.call(val) === "[object Object]";
  620. }
  621.  
  622. // function compareProps(objToCompare, obj) {
  623. // return Object.entries(obj).every(([prop, value]) => {
  624. // return Object.hasOwn(objToCompare, prop) && objToCompare[prop] === value;
  625. // });
  626. // }
  627.  
  628. /**
  629. * Checks if all properties and their values in the targetObject exist and are equal in the referenceObject.
  630. * @param {Object} referenceObject The object to compare against.
  631. * @param {Object} targetObject The object whose properties and values are checked for equality.
  632. * @returns {boolean} Returns true if all properties and values in targetObject are present and equal in referenceObject, otherwise false.
  633. */
  634. function checkPropertyEquality(referenceObject, targetObject) {
  635. const entries = Object.entries(targetObject);
  636.  
  637. for (const [prop, value] of entries) {
  638. if (!Object.hasOwn(referenceObject, prop)) {
  639. return false;
  640. }
  641. if (referenceObject[prop] !== value) {
  642. return false;
  643. }
  644. }
  645.  
  646. return true;
  647. }
  648.  
  649. function containsValue(valueReference, ...values) {
  650. for (const value of values) {
  651. if (valueReference === value) return true;
  652. }
  653. return false;
  654. }
  655.  
  656. function startsEndsWith(string, ...searchs) {
  657. const [startSearch, endSearch] = searchs;
  658. const firstChar = string[0];
  659. const lastChar = string[string.length - 1];
  660.  
  661. if (endSearch === undefined) {
  662. return firstChar === startSearch && lastChar === startSearch;
  663. }
  664.  
  665. return firstChar === startSearch && lastChar === endSearch;
  666. }
  667.  
  668. /**
  669. * Gets a more specific type of a value than `typeof`.
  670. * @param {*} val The value whose type is to be determined.
  671. * @returns {string} The type of the value (e.g., 'string', 'array', 'object', 'class', 'null').
  672. */
  673. function valType(val) {
  674. if (val?.prototype?.constructor === val) {
  675. return "class";
  676. }
  677. return Object.prototype.toString.call(val).slice(8, -1).toLowerCase();
  678. }
  679.  
  680. /**
  681. * Returns the length or size of the given target based on its type.
  682. *
  683. * Supported types:
  684. * - string: Returns the string's length.
  685. * - array: Returns the array's length.
  686. * - object: Returns the number of own enumerable properties.
  687. * - set: Returns the number of elements in the Set.
  688. * - map: Returns the number of elements in the Map.
  689. * - null: Returns 0.
  690. *
  691. * @param {*} target - The value whose length or size is to be determined.
  692. * @returns {number} The length or size of the target.
  693. * @throws {Error} If the type of target is unsupported.
  694. */
  695. function len(target) {
  696. const type = valType(target);
  697. const types = {
  698. string: () => target.length,
  699. object: () => Object.keys(target).length,
  700. array: () => target.length,
  701. set: () => target.size,
  702. map: () => target.size,
  703. null: () => 0,
  704. };
  705.  
  706. if (types[type]) {
  707. return types[type]();
  708. } else {
  709. throw new Error(`Unsupported type: ${type}`);
  710. }
  711. }
  712.  
  713. /**
  714. * Repeatedly calls a function with a delay until it returns `true`.
  715. * Uses `requestAnimationFrame` for scheduling.
  716. * @param {function(): (boolean|void)} func The function to run. The loop stops if it returns `true`.
  717. * @param {number} [time=250] The delay in milliseconds between executions.
  718. */
  719. function update(func, time = 250) {
  720. const exec = () => {
  721. if (func() === true) {
  722. return;
  723. }
  724.  
  725. setTimeout(() => {
  726. requestAnimationFrame(exec);
  727. }, time);
  728. };
  729.  
  730. requestAnimationFrame(exec);
  731. }
  732.  
  733. /**
  734. * Runs a function on every animation frame until the function returns `true`.
  735. * @param {function(): (boolean|void)} func The function to execute. The loop stops if it returns `true`.
  736. */
  737. function loop(func) {
  738. const exec = () => {
  739. if (func() === true) {
  740. return;
  741. }
  742.  
  743. requestAnimationFrame(exec);
  744. };
  745.  
  746. requestAnimationFrame(exec);
  747. }
  748.  
  749. /**
  750. * Injects a CSS string into the document by creating a `<style>` element.
  751. * @param {string} css The CSS text to apply.
  752. * @returns {Promise<HTMLStyleElement>} A promise that resolves with the created style element.
  753. */
  754. function style(css) {
  755. return new Promise((resolve) => {
  756. const toAppend = document.documentElement;
  757. const styleElement = document.createElement("style");
  758. styleElement.className = "ust-style";
  759.  
  760. styleElement.innerHTML = css;
  761. toAppend.appendChild(styleElement);
  762.  
  763. resolve(styleElement);
  764. });
  765. }
  766.  
  767. /**
  768. * Intercepts calls to an object's method using a Proxy, allowing modification of its behavior.
  769. * @param {object} owner The object that owns the method.
  770. * @param {string} methodName The name of the method to hook.
  771. * @param {ProxyHandler<function>} handler The proxy handler to intercept the method call.
  772. * @returns {function(): void} A function that, when called, reverts the method to its original implementation.
  773. */
  774. function hook(owner, methodName, handler) {
  775. const originalMethod = owner[methodName];
  776.  
  777. // if (typeof originalMethod !== "function") {
  778. // throw new Error(`[UST.patch] The method “${methodName}” was not found in the object "${owner}".`);
  779. // }
  780.  
  781. const proxy = new Proxy(originalMethod, handler);
  782.  
  783. owner[methodName] = proxy;
  784.  
  785. return () => {
  786. owner[methodName] = originalMethod;
  787. };
  788. }
  789.  
  790. /**
  791. * An object to execute callbacks based on changes in the page URL, useful for Single Page Applications (SPAs).
  792. */
  793. const watchUrl = {
  794. _enabled: false,
  795. _onUrlRules: [],
  796.  
  797. /**
  798. * Adds a URL pattern and a callback to execute when the URL matches.
  799. * @param {string|RegExp} pattern The URL pattern to match against. Can be a string or a RegExp.
  800. * @param {function(): void} func The callback to execute on match.
  801. */
  802. add(pattern, func) {
  803. const isRegex = pattern instanceof RegExp;
  804. const patternRule = pattern.startsWith("/") ? unsafeWindow.location.origin + pattern : pattern;
  805.  
  806. this._onUrlRules.push({ pattern: patternRule, func, isRegex });
  807.  
  808. if (this._enabled === false) {
  809. this._enabled = true;
  810. this.init();
  811. }
  812. },
  813.  
  814. /**
  815. * @private
  816. * Initializes the URL watching mechanism.
  817. */
  818. init() {
  819. const exec = (currentUrl) => {
  820. const ruleFound = this._onUrlRules.find((rule) =>
  821. rule.isRegex ? rule.pattern.test(currentUrl) : rule.pattern === currentUrl,
  822. );
  823.  
  824. if (ruleFound) {
  825. ruleFound.func();
  826. }
  827. };
  828.  
  829. watchLocation(exec);
  830. },
  831. };
  832.  
  833. /**
  834. * Monitors `location.href` for changes and triggers a callback. It handles history API changes (pushState, replaceState)
  835. * and popstate events, making it suitable for SPAs.
  836. * @param {function(string): void} callback The function to call with the new URL when a change is detected.
  837. */
  838. function watchLocation(callback) {
  839. let previousUrl = location.href;
  840.  
  841. const observer = new MutationObserver(() => checkForChanges());
  842.  
  843. observer.observe(unsafeWindow.document, { childList: true, subtree: true });
  844.  
  845. const checkForChanges = () => {
  846. requestAnimationFrame(() => {
  847. const currentUrl = location.href;
  848. if (currentUrl !== previousUrl) {
  849. previousUrl = currentUrl;
  850. callback(currentUrl);
  851. }
  852. });
  853. };
  854.  
  855. const historyHandler = {
  856. apply(target, thisArg, args) {
  857. const result = Reflect.apply(target, thisArg, args);
  858. checkForChanges();
  859. return result;
  860. },
  861. };
  862. hook(history, "pushState", historyHandler);
  863. hook(history, "replaceState", historyHandler);
  864.  
  865. unsafeWindow.addEventListener("popstate", checkForChanges);
  866.  
  867. callback(previousUrl);
  868. }
  869.  
  870. /**
  871. * A promise-based wrapper for the Greasemonkey `GM_xmlhttpRequest` function.
  872. * @param {object} options The options for the request, matching the `GM_xmlhttpRequest` specification.
  873. * @returns {Promise<object>} A promise that resolves with the response object on success or rejects on error/timeout.
  874. */
  875. function request(options) {
  876. return new Promise((resolve, reject) => {
  877. GM_xmlhttpRequest({
  878. onload: resolve,
  879. onerror: reject,
  880. ontimeout: reject,
  881. ...options,
  882. });
  883. });
  884. }
  885.  
  886. /**
  887. * Extracts data from an element based on an array of property path definitions.
  888. * @param {HTMLElement} element The root element to extract properties from.
  889. * @param {Array<string>} propsArray Array of property definitions, e.g., ["name:innerText", "link:href"].
  890. * @returns {object} An object containing the extracted data.
  891. */
  892. function extractProps(element, propsArray) {
  893. const data = {};
  894.  
  895. for (const propDefinition of propsArray) {
  896. const [label, valuePath] = propDefinition.split(":");
  897.  
  898. if (valuePath) {
  899. data[label] = safeGet(element, valuePath);
  900. } else {
  901. data[label] = safeGet(element, label);
  902. }
  903. }
  904. return data;
  905. }
  906.  
  907. /**
  908. * @private
  909. * Handles a string rule in the scrape schema.
  910. * @param {HTMLElement} container The container element.
  911. * @param {string} rule The CSS selector for the target element.
  912. * @returns {string|null} The text content of the found element, or null.
  913. */
  914. function _handleStringRule(container, rule) {
  915. const element = container.querySelector(rule);
  916. return element ? element.textContent.trim() : null;
  917. }
  918.  
  919. /**
  920. * @private
  921. * Handles an array rule in the scrape schema.
  922. * @param {HTMLElement} container The container element.
  923. * @param {Array<string>} rule An array where the first item is a sub-selector and the rest are property definitions.
  924. * @returns {object} The extracted properties from the sub-element.
  925. */
  926. function _handleArrayRule(container, rule) {
  927. const [subSelector, ...propsToGet] = rule;
  928. if (!subSelector) {
  929. throw new Error("[UST.scrape] No subselector provided as the first item in the rule");
  930. }
  931. const element = container.querySelector(subSelector);
  932. return extractProps(element, propsToGet);
  933. }
  934.  
  935. const ruleHandlers = {
  936. string: _handleStringRule,
  937. array: _handleArrayRule,
  938. };
  939.  
  940. /**
  941. * @private
  942. * Determines the type of a scrape rule.
  943. * @param {*} rule The rule to check.
  944. * @returns {string} The type of the rule ('string', 'array', or 'unknown').
  945. */
  946. function _getRuleType(rule) {
  947. if (typeof rule === "string") return "string";
  948. if (Array.isArray(rule)) return "array";
  949. return "unknown";
  950. }
  951.  
  952. /**
  953. * @private
  954. * Processes an object schema for scraping.
  955. * @param {HTMLElement} container The container element.
  956. * @param {object} schema The schema object.
  957. * @returns {object} The scraped data object.
  958. */
  959. function _processObjectSchema(container, schema) {
  960. const item = {};
  961. for (const key in schema) {
  962. const rule = schema[key];
  963. const ruleType = _getRuleType(rule);
  964.  
  965. const handler = ruleHandlers[ruleType];
  966. if (handler) {
  967. item[key] = handler(container, rule);
  968. continue;
  969. }
  970.  
  971. console.warn(`[UST.scrape] Rule for key ${key}” has an unsupported type.`);
  972. }
  973. return item;
  974. }
  975.  
  976. /**
  977. * @private
  978. * Processes a single container element based on the provided schema.
  979. * @param {HTMLElement} container The container element to process.
  980. * @param {object|Array<string>} schema The schema to apply.
  981. * @returns {object} The scraped data.
  982. */
  983. function _processContainer(container, schema) {
  984. if (Array.isArray(schema)) {
  985. return extractProps(container, schema);
  986. }
  987.  
  988. if (isObject(schema)) {
  989. return _processObjectSchema(container, schema);
  990. }
  991.  
  992. console.warn("[UST.scrape] Invalid schema format.");
  993. return {};
  994. }
  995.  
  996. /**
  997. * Scrapes structured data from the DOM based on a selector and a schema.
  998. * @param {string} selector CSS selector for the container elements to scrape.
  999. * @param {object|Array<string>} schema Defines the data to extract from each container.
  1000. * @param {function(HTMLElement, object): void} func A callback for each scraped item, receiving the container element and the extracted data object.
  1001. * @param {Node} [scope=document] The scope within which to search for containers.
  1002. * @returns {Array<object>} An array of the scraped data objects.
  1003. */
  1004. function scrape(selector, schema, func, scope = document) {
  1005. const containers = scope.querySelectorAll(selector);
  1006. const results = [];
  1007. for (const container of containers) {
  1008. const item = _processContainer(container, schema);
  1009. func(container, item);
  1010. results.push(item);
  1011. }
  1012. return results;
  1013. }
  1014.  
  1015. /**
  1016. * Iterates over all elements matching a selector and applies a function to each.
  1017. * @param {string} selector A CSS selector.
  1018. * @param {function(Node): void} func The function to execute for each matching element.
  1019. * @returns {NodeListOf<Element>} The list of nodes found.
  1020. */
  1021. function each(selector, func) {
  1022. const nodes = document.querySelectorAll(selector);
  1023. for (const node of nodes) {
  1024. func(node);
  1025. }
  1026. return nodes;
  1027. }
  1028.  
  1029. /**
  1030. * Chains multiple iterables together into a single sequence.
  1031. * @param {...Iterable} iterables One or more iterable objects (e.g., arrays, sets).
  1032. * @returns {Generator} A generator that yields values from each iterable in order.
  1033. */
  1034. function* chain(...iterables) {
  1035. for (const it of iterables) {
  1036. yield* it;
  1037. }
  1038. }
  1039.  
  1040. /**
  1041. * Creates a debounced version of a function that delays its execution until after a certain time has passed
  1042. * without it being called.
  1043. * @param {function} func The function to debounce.
  1044. * @param {number} wait The debounce delay in milliseconds.
  1045. * @returns {function} The new debounced function.
  1046. */
  1047. function debounce(func, wait) {
  1048. let timeout;
  1049. return function (...args) {
  1050. clearTimeout(timeout);
  1051. timeout = setTimeout(() => func.apply(this, args), wait);
  1052. };
  1053. }
  1054.  
  1055. /**
  1056. * Pauses execution for a specified number of milliseconds.
  1057. * @param {number} ms The number of milliseconds to sleep.
  1058. * @returns {Promise<void>} A promise that resolves after the specified time.
  1059. */
  1060. function sleep(ms) {
  1061. return new Promise((resolve) => setTimeout(resolve, ms));
  1062. }
  1063.  
  1064. /**
  1065. * A simple template engine that extends Map. It replaces `{{placeholder}}` syntax in strings.
  1066. * @extends Map
  1067. */
  1068. class Templates extends Map {
  1069. /**
  1070. * Fills a template with the provided data.
  1071. * @param {*} key The key of the template stored in the map.
  1072. * @param {object} [data={}] An object with key-value pairs to replace placeholders.
  1073. * @returns {string|null} The template string with placeholders filled, or null if the template is not found.
  1074. */
  1075. fill(key, data = {}) {
  1076. const template = super.get(key);
  1077. if (!template) {
  1078. console.warn(`[UST.Templates] Template with key ${key}” not found.`);
  1079. return null;
  1080. }
  1081.  
  1082. return template.replace(/\{\{(\s*\w+\s*)\}\}/g, (match, placeholder) =>
  1083. Object.hasOwn(data, placeholder) ? data[placeholder] : match,
  1084. );
  1085. }
  1086.  
  1087. /**
  1088. * Renders a template into a DocumentFragment.
  1089. * @param {*} key The key of the template stored in the map.
  1090. * @param {object} [data={}] An object with data to fill the placeholders.
  1091. * @returns {DocumentFragment|null} A document fragment containing the rendered HTML, or null if the template is not found.
  1092. */
  1093. render(key, data = {}) {
  1094. const filledHtml = this.fill(key, data);
  1095. if (filledHtml === null) {
  1096. return null;
  1097. }
  1098.  
  1099. const templateElement = document.createElement("template");
  1100. templateElement.innerHTML = filledHtml;
  1101.  
  1102. return templateElement.content.cloneNode(true);
  1103. }
  1104. }
  1105.  
  1106. /**
  1107. * Factory function to create a new Templates instance.
  1108. * @returns {Templates} A new instance of the Templates class.
  1109. */
  1110. function templates() {
  1111. return new Templates();
  1112. }
  1113.  
  1114. /**
  1115. * A class for creating lazy, chainable operations (map, filter, take) on iterables.
  1116. * Operations are only executed when the sequence is consumed.
  1117. */
  1118. class LazySequence extends Array {
  1119. /**
  1120. * @param {Iterable<any>} iterable The initial iterable.
  1121. */
  1122. constructor(iterable) {
  1123. super();
  1124. this.iterable = iterable;
  1125. }
  1126.  
  1127. /**
  1128. * Creates a new lazy sequence with a mapping function.
  1129. * @param {function(*): *} func The mapping function.
  1130. * @returns {LazySequence} A new LazySequence instance.
  1131. */
  1132. map(func) {
  1133. const self = this;
  1134. return new LazySequence({
  1135. *[Symbol.iterator]() {
  1136. for (const value of self.iterable) {
  1137. yield func(value);
  1138. }
  1139. },
  1140. });
  1141. }
  1142.  
  1143. /**
  1144. * Creates a new lazy sequence with a filtering function.
  1145. * @param {function(*): boolean} func The filtering function.
  1146. * @returns {LazySequence} A new LazySequence instance.
  1147. */
  1148. filter(func) {
  1149. const self = this;
  1150. return new LazySequence({
  1151. *[Symbol.iterator]() {
  1152. for (const value of self.iterable) {
  1153. if (func(value)) {
  1154. yield value;
  1155. }
  1156. }
  1157. },
  1158. });
  1159. }
  1160.  
  1161. /**
  1162. * Creates a new lazy sequence that takes only the first n items.
  1163. * @param {number} n The number of items to take.
  1164. * @returns {LazySequence} A new LazySequence instance.
  1165. */
  1166. take(n) {
  1167. const self = this;
  1168. return new LazySequence({
  1169. *[Symbol.iterator]() {
  1170. let count = 0;
  1171. for (const value of self.iterable) {
  1172. if (count >= n) break;
  1173. yield value;
  1174. count++;
  1175. }
  1176. },
  1177. });
  1178. }
  1179.  
  1180. /**
  1181. * Makes the LazySequence itself iterable.
  1182. */
  1183. *[Symbol.iterator]() {
  1184. yield* this.iterable;
  1185. }
  1186.  
  1187. /**
  1188. * Executes all lazy operations and returns the results as an array.
  1189. * @returns {Array<*>} An array containing all values from the processed iterable.
  1190. */
  1191. collect() {
  1192. return [...this.iterable];
  1193. }
  1194. }
  1195.  
  1196. /**
  1197. * Factory function to create a new LazySequence.
  1198. * @param {Iterable<any>} iterable An iterable to wrap.
  1199. * @returns {LazySequence} A new LazySequence instance.
  1200. */
  1201. function lazy(iterable) {
  1202. return new LazySequence(iterable);
  1203. }
  1204.  
  1205. /**
  1206. * Creates a DocumentFragment and populates it using a callback.
  1207. * This is useful for building a piece of DOM in memory before attaching it to the live DOM.
  1208. * @param {function(DocumentFragment): void} builderCallback A function that receives a document fragment and can append nodes to it.
  1209. * @returns {DocumentFragment} The populated document fragment.
  1210. */
  1211. function createFromFragment(builderCallback) {
  1212. const fragment = document.createDocumentFragment();
  1213. builderCallback(fragment);
  1214. return fragment;
  1215. }
  1216.  
  1217. /**
  1218. * Detaches an element from the DOM, runs a callback to perform modifications, and then re-attaches it.
  1219. * This can improve performance by preventing multiple browser reflows and repaints during manipulation.
  1220. * @param {HTMLElement|string} elementOrSelector The element or its CSS selector.
  1221. * @param {function(HTMLElement): void} callback The function to execute with the detached element.
  1222. */
  1223. function withDetached(elementOrSelector, callback) {
  1224. const element = typeof elementOrSelector === "string" ? document.querySelector(elementOrSelector) : elementOrSelector;
  1225.  
  1226. if (!element || !element.parentElement) return;
  1227.  
  1228. const parent = element.parentElement;
  1229. const nextSibling = element.nextElementSibling;
  1230.  
  1231. parent.removeChild(element);
  1232.  
  1233. try {
  1234. callback(element);
  1235. } finally {
  1236. parent.insertBefore(element, nextSibling);
  1237. }
  1238. }
  1239.  
  1240. window.UST = window.UST || {};
  1241.  
  1242. Object.assign(window.UST, {
  1243. observe,
  1244. OnElement,
  1245. queryAll,
  1246. query,
  1247. injectScriptInline,
  1248. wait,
  1249. on,
  1250. createDeepProxy,
  1251. safeGet,
  1252. handleArray,
  1253. handleObject,
  1254. checkPropertyEquality,
  1255. getProp,
  1256. isObject,
  1257. containsValue,
  1258. valType,
  1259. len,
  1260. update,
  1261. loop,
  1262. style,
  1263. hook,
  1264. watchUrl,
  1265. watchLocation,
  1266. request,
  1267. extractProps,
  1268. scrape,
  1269. each,
  1270. chain,
  1271. debounce,
  1272. sleep,
  1273. templates,
  1274. lazy,
  1275. createFromFragment,
  1276. withDetached,
  1277. });
  1278. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址