billy-herrington-utils 1.6.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +2 -2
  2. package/dist/billy-herrington-utils.es.js +5908 -327
  3. package/dist/billy-herrington-utils.es.js.map +1 -1
  4. package/dist/billy-herrington-utils.umd.js +5912 -331
  5. package/dist/billy-herrington-utils.umd.js.map +1 -1
  6. package/dist/index.d.ts +111 -103
  7. package/package.json +7 -5
  8. package/src/index.ts +18 -5
  9. package/src/types/globals.d.ts +0 -2
  10. package/src/userscripts/data-manager/data-filter.ts +110 -0
  11. package/src/userscripts/data-manager/index.ts +77 -174
  12. package/src/userscripts/infinite-scroll/index.ts +72 -71
  13. package/src/userscripts/pagination-parsing/index.ts +21 -29
  14. package/src/userscripts/pagination-parsing/pagination-strategies/PaginationStrategy.ts +4 -12
  15. package/src/userscripts/pagination-parsing/pagination-strategies/PaginationStrategyDataParams.ts +11 -2
  16. package/src/userscripts/pagination-parsing/pagination-strategies/PaginationStrategyPathnameParams.ts +29 -2
  17. package/src/userscripts/pagination-parsing/pagination-strategies/PaginationStrategySearchParams.ts +15 -7
  18. package/src/userscripts/pagination-parsing/pagination-strategies/PaginationStrategyTrash.ts +9 -12
  19. package/src/userscripts/pagination-parsing/pagination-strategies/index.ts +1 -1
  20. package/src/userscripts/pagination-parsing/pagination-utils/index.ts +36 -7
  21. package/src/userscripts/router/router.ts +71 -0
  22. package/src/userscripts/rules/index.ts +228 -3
  23. package/src/userscripts/types/index.ts +19 -0
  24. package/src/utils/arrays/index.ts +3 -1
  25. package/src/utils/async/index.ts +22 -6
  26. package/src/utils/dom/index.ts +35 -68
  27. package/src/utils/dom/observers.ts +76 -0
  28. package/src/utils/events/index.ts +9 -2
  29. package/src/utils/fetch/index.ts +14 -7
  30. package/src/utils/objects/index.ts +25 -0
  31. package/src/utils/observers/index.ts +8 -2
  32. package/src/utils/parsers/index.ts +18 -11
  33. package/src/utils/strings/index.ts +5 -5
  34. package/src/utils/strings/regexes.ts +31 -0
  35. package/src/utils/userscript/index.ts +10 -0
  36. package/src/userscripts/jabroni-outfit-wrap/index.ts +0 -40
@@ -1,15 +1,31 @@
1
+ import { sanitizeStr } from '../strings';
2
+ import { waitForElementToAppear } from './observers';
3
+
4
+ export {
5
+ waitForElementToAppear,
6
+ waitForElementToDisappear,
7
+ watchDomChangesWithThrottle,
8
+ watchElementChildrenCount,
9
+ } from './observers';
10
+
11
+ export function querySelectorText(el: HTMLElement, selector?: string): string {
12
+ if (!selector || typeof selector !== 'string') return '';
13
+ const text = el.querySelector<HTMLElement>(selector)?.innerText || '';
14
+ return sanitizeStr(text);
15
+ }
16
+
1
17
  export function parseDom(html: string): HTMLElement {
2
18
  const parsed = new DOMParser().parseFromString(html, 'text/html').body;
3
- return parsed.children.length > 1 ? parsed : parsed.firstElementChild as HTMLElement;
19
+ return parsed.children.length > 1 ? parsed : (parsed.firstElementChild as HTMLElement);
4
20
  }
5
21
 
6
- export function copyAttributes(target: HTMLElement | Element, source: HTMLElement | Element) {
22
+ export function copyAttributes(target: HTMLElement, source: HTMLElement) {
7
23
  for (const attr of source.attributes) {
8
24
  attr.nodeValue && target.setAttribute(attr.nodeName, attr.nodeValue);
9
25
  }
10
26
  }
11
27
 
12
- export function replaceElementTag(e: HTMLElement | Element, tagName: string) {
28
+ export function replaceElementTag(e: HTMLElement, tagName: string) {
13
29
  const newTagElement = document.createElement(tagName);
14
30
  copyAttributes(newTagElement, e);
15
31
  newTagElement.innerHTML = e.innerHTML;
@@ -17,74 +33,31 @@ export function replaceElementTag(e: HTMLElement | Element, tagName: string) {
17
33
  return newTagElement;
18
34
  }
19
35
 
20
- export function getAllUniqueParents(elements: HTMLCollection): Array<HTMLElement | Element> {
21
- return Array.from(elements).reduce((acc, v) => {
22
- if (v.parentElement && !acc.includes(v.parentElement as HTMLElement)) { acc.push(v.parentElement); }
23
- return acc;
24
- }, [] as Array<HTMLElement | Element>);
36
+ export function getAllUniqueParents(
37
+ elements: HTMLCollection | HTMLElement[],
38
+ ): HTMLElement[] {
39
+ const parents = Array.from(elements)
40
+ .map((el) => el.parentElement)
41
+ .filter((parent): parent is HTMLElement => parent !== null);
42
+
43
+ return [...new Set(parents)];
25
44
  }
26
45
 
27
- export function findNextSibling(el: HTMLElement | Element) {
46
+ export function findNextSibling(el: HTMLElement) {
28
47
  if (el.nextElementSibling) return el.nextElementSibling;
29
48
  if (el.parentElement) return findNextSibling(el.parentElement);
30
49
  return null;
31
50
  }
32
51
 
33
- export function waitForElementExists(parent: HTMLElement | Element, selector: string, callback: (el: Element) => void): void {
34
- const observer = new MutationObserver((_mutations) => {
35
- const el = parent.querySelector(selector);
36
- if (el) {
37
- observer.disconnect();
38
- callback(el);
39
- }
40
- });
41
- observer.observe(document.body, { childList: true, subtree: true });
42
- }
43
-
44
- export function watchElementChildrenCount(element: HTMLElement | Element,
45
- callback: (observer: MutationObserver, count: number) => void): void {
46
- let count = element.children.length;
47
- const observer = new MutationObserver((mutationList, observer) => {
48
- for (const mutation of mutationList) {
49
- if (mutation.type === "childList") {
50
- if (count !== element.children.length) {
51
- count = element.children.length;
52
- callback(observer, count);
53
- }
54
- }
55
- }
56
- });
57
- observer.observe(element, { childList: true });
52
+ export function exterminateVideo(video: HTMLVideoElement) {
53
+ video.removeAttribute('src');
54
+ video.load();
55
+ video.remove();
58
56
  }
59
57
 
60
- export function watchDomChangesWithThrottle(
61
- element: HTMLElement | Element,
62
- callback: () => void,
63
- throttle = 1000,
64
- times = Infinity,
65
- options: Record<string, boolean> = { childList: true, subtree: true, attributes: true }
58
+ export function downloader(
59
+ options = { append: '', after: '', button: '', cbBefore: () => {} },
66
60
  ) {
67
- let lastMutationTime: number;
68
- let timeout: number;
69
- let times_ = times;
70
- const observer = new MutationObserver((_mutationList, _observer) => {
71
- if (times_ !== Infinity && times_ < 1) {
72
- observer.disconnect();
73
- return;
74
- }
75
- times_--;
76
- const now = Date.now();
77
- if (lastMutationTime && now - lastMutationTime < throttle) {
78
- timeout && clearTimeout(timeout);
79
- }
80
- timeout = setTimeout(callback, throttle);
81
- lastMutationTime = now;
82
- });
83
- observer.observe(element, options);
84
- return observer;
85
- }
86
-
87
- export function downloader(options = { append: "", after: "", button: "", cbBefore: () => { } }) {
88
61
  const btn = parseDom(options.button);
89
62
 
90
63
  if (options.append) document.querySelector(options.append)?.append(btn);
@@ -95,14 +68,8 @@ export function downloader(options = { append: "", after: "", button: "", cbBefo
95
68
 
96
69
  if (options.cbBefore) options.cbBefore();
97
70
 
98
- waitForElementExists(document.body, 'video', (video: Element) => {
71
+ waitForElementToAppear(document.body, 'video', (video: Element) => {
99
72
  window.location.href = video.getAttribute('src') as string;
100
73
  });
101
74
  });
102
75
  }
103
-
104
- export function exterminateVideo(video: HTMLVideoElement) {
105
- video.removeAttribute('src');
106
- video.load();
107
- video.remove();
108
- }
@@ -0,0 +1,76 @@
1
+ export function waitForElementToAppear(
2
+ parent: HTMLElement,
3
+ selector: string,
4
+ callback: (el: Element) => void,
5
+ ) {
6
+ const observer = new MutationObserver((_mutations) => {
7
+ const el = parent.querySelector(selector);
8
+ if (el) {
9
+ observer.disconnect();
10
+ callback(el);
11
+ }
12
+ });
13
+
14
+ observer.observe(document.body, { childList: true, subtree: true });
15
+ return observer;
16
+ }
17
+
18
+ export function waitForElementToDisappear(observable: HTMLElement, callback: () => void) {
19
+ const observer = new MutationObserver((_mutations) => {
20
+ if (!observable.isConnected) {
21
+ observer.disconnect();
22
+ callback();
23
+ }
24
+ });
25
+
26
+ observer.observe(document.body, { childList: true, subtree: true });
27
+ return observer;
28
+ }
29
+
30
+ export function watchElementChildrenCount(
31
+ element: HTMLElement,
32
+ callback: (observer: MutationObserver, count: number) => void,
33
+ ) {
34
+ let count = element.children.length;
35
+ const observer = new MutationObserver((mutationList, observer) => {
36
+ for (const mutation of mutationList) {
37
+ if (mutation.type === 'childList') {
38
+ if (count !== element.children.length) {
39
+ count = element.children.length;
40
+ callback(observer, count);
41
+ }
42
+ }
43
+ }
44
+ });
45
+
46
+ observer.observe(element, { childList: true });
47
+ return observer;
48
+ }
49
+
50
+ export function watchDomChangesWithThrottle(
51
+ element: HTMLElement,
52
+ callback: () => void,
53
+ throttle = 1000,
54
+ times = Infinity,
55
+ options: MutationObserverInit = { childList: true, subtree: true, attributes: true },
56
+ ) {
57
+ let lastMutationTime: number;
58
+ let timeout: number;
59
+ let times_ = times;
60
+ const observer = new MutationObserver((_mutationList, _observer) => {
61
+ if (times_ !== Infinity && times_ < 1) {
62
+ observer.disconnect();
63
+ return;
64
+ }
65
+ times_--;
66
+ const now = Date.now();
67
+ if (lastMutationTime && now - lastMutationTime < throttle) {
68
+ timeout && clearTimeout(timeout);
69
+ }
70
+ timeout = setTimeout(callback, throttle);
71
+ lastMutationTime = now;
72
+ });
73
+
74
+ observer.observe(element, options);
75
+ return observer;
76
+ }
@@ -1,4 +1,8 @@
1
- export function listenEvents(dom: HTMLElement | Element, events: Array<string>, callback: (e: Event) => void): void {
1
+ export function listenEvents(
2
+ dom: HTMLElement,
3
+ events: Array<string>,
4
+ callback: (e: Event) => void,
5
+ ): void {
2
6
  for (const e of events) {
3
7
  dom.addEventListener(e, callback, true);
4
8
  }
@@ -8,7 +12,10 @@ export class Tick {
8
12
  private tick?: number;
9
13
  private callbackFinal?: () => void;
10
14
 
11
- constructor(private delay: number, private startImmediate: boolean = true) {}
15
+ constructor(
16
+ private delay: number,
17
+ private startImmediate: boolean = true,
18
+ ) {}
12
19
 
13
20
  public start(callback: () => void, callbackFinal?: () => void): void {
14
21
  this.stop();
@@ -6,23 +6,30 @@ export const MOBILE_UA = [
6
6
  'Chrome/114.0.0.0 Mobile Safari/537.36',
7
7
  ].join(' ');
8
8
 
9
- export function fetchWith(
9
+ export async function fetchWith(
10
10
  url: string,
11
- options: Record<string, boolean> = { html: false, mobile: false },
11
+ options?: { html?: boolean; mobile?: boolean },
12
12
  ) {
13
13
  const reqOpts = {};
14
- if (options.mobile) Object.assign(reqOpts, { headers: new Headers({ 'User-Agent': MOBILE_UA }) });
14
+
15
+ if (options?.mobile) {
16
+ Object.assign(reqOpts, { headers: new Headers({ 'User-Agent': MOBILE_UA }) });
17
+ }
18
+
15
19
  return fetch(url, reqOpts)
16
20
  .then((r) => r.text())
17
- .then((r) => (options.html ? parseDom(r) : r));
21
+ .then((r) => (options?.html ? parseDom(r) : r));
18
22
  }
19
23
 
20
- export const fetchHtml = (url: string) => fetchWith(url, { html: true }) as Promise<HTMLElement>;
24
+ export const fetchHtml = (url: string) =>
25
+ fetchWith(url, { html: true }) as Promise<HTMLElement>;
21
26
 
22
27
  export const fetchText = (url: string) => fetchWith(url) as Promise<string>;
23
28
 
24
- export function objectToFormData(object: Record<string, number | boolean | string>): FormData {
29
+ export function objectToFormData<T extends {}>(obj: T): FormData {
25
30
  const formData = new FormData();
26
- Object.entries(object).forEach(([k, v]) => formData.append(k, v as string));
31
+ Object.entries(obj).forEach(([k, v]) => {
32
+ formData.append(k, v as string);
33
+ });
27
34
  return formData;
28
35
  }
@@ -0,0 +1,25 @@
1
+ type AnyFunction = (...args: any[]) => any;
2
+
3
+ export interface MemoizedFunction<T extends AnyFunction> extends CallableFunction {
4
+ (...args: Parameters<T>): ReturnType<T>;
5
+ clear: () => void;
6
+ }
7
+
8
+ export function memoize<T extends AnyFunction>(fn: T): MemoizedFunction<T> {
9
+ const cache = new Map<string, ReturnType<T>>();
10
+
11
+ const memoizedFunction = ((...args: Parameters<T>): ReturnType<T> => {
12
+ const key = JSON.stringify(args);
13
+
14
+ if (cache.has(key)) {
15
+ return cache.get(key) as ReturnType<T>;
16
+ }
17
+
18
+ const result = fn(...args);
19
+ cache.set(key, result);
20
+
21
+ return result;
22
+ }) as MemoizedFunction<T>;
23
+
24
+ return memoizedFunction;
25
+ }
@@ -1,5 +1,6 @@
1
1
  export class Observer {
2
2
  public observer: IntersectionObserver;
3
+ private timeout: number | null = null;
3
4
  constructor(private callback: (entry: Element) => void) {
4
5
  this.observer = new IntersectionObserver(this.handleIntersection.bind(this));
5
6
  }
@@ -10,7 +11,7 @@ export class Observer {
10
11
 
11
12
  throttle(target: Element, throttleTime: number) {
12
13
  this.observer.unobserve(target);
13
- setTimeout(() => this.observer.observe(target), throttleTime);
14
+ this.timeout = setTimeout(() => this.observer.observe(target), throttleTime);
14
15
  }
15
16
 
16
17
  handleIntersection(entries: Iterable<IntersectionObserverEntry>) {
@@ -21,6 +22,11 @@ export class Observer {
21
22
  }
22
23
  }
23
24
 
25
+ dispose() {
26
+ if (this.timeout) clearTimeout(this.timeout);
27
+ this.observer.disconnect();
28
+ }
29
+
24
30
  static observeWhile(
25
31
  target: Element,
26
32
  callback: () => Promise<boolean> | boolean,
@@ -47,7 +53,7 @@ export class LazyImgLoader {
47
53
  });
48
54
  }
49
55
 
50
- lazify(_target: Element, img: HTMLImageElement, imgSrc: string) {
56
+ lazify(_target: Element, img?: HTMLImageElement, imgSrc?: string) {
51
57
  if (!img || !imgSrc) return;
52
58
  img.setAttribute(this.attributeName, imgSrc);
53
59
  img.src = '';
@@ -13,29 +13,36 @@ export function timeToSeconds(t: string): number {
13
13
  const r = /sec|min|h|m/.test(t) ? formatTimeToHHMMSS(t) : t;
14
14
  return (r?.match(/\d+/gm) || [0])
15
15
  .reverse()
16
+
16
17
  .map((s, i) => parseInt(s as string) * 60 ** i)
17
18
  .reduce((a, b) => a + b);
18
19
  }
19
20
 
20
21
  export function parseIntegerOr(n: string | number, or: number): number {
21
- return (num => Number.isNaN(num) ? or : num)(parseInt(n as string));
22
+ const num = Number(n);
23
+ return Number.isSafeInteger(num) ? num : or;
22
24
  }
23
25
 
24
26
  // "data:02;body+head:async;void:;zero:;"
25
27
  export function parseDataParams(str: string): Record<string, string> {
26
28
  const paramsStr = decodeURI(str.trim()).split(';');
27
- return paramsStr.reduce((acc, s) => {
28
- const parsed = s.match(/([\+\w]+):([\w\-\ ]+)?/);
29
- if (parsed) {
30
- const [, key, value] = parsed;
31
- if (value) {
32
- key.split('+').forEach(p => { acc[p] = value; });
29
+ return paramsStr.reduce(
30
+ (acc, s) => {
31
+ const parsed = s.match(/([+\w]+):([\w\- ]+)?/);
32
+ if (parsed) {
33
+ const [, key, value] = parsed;
34
+ if (value) {
35
+ key.split('+').forEach((p) => {
36
+ acc[p] = value;
37
+ });
38
+ }
33
39
  }
34
- }
35
- return acc;
36
- }, {} as Record<string, string>);
40
+ return acc;
41
+ },
42
+ {} as Record<string, string>,
43
+ );
37
44
  }
38
45
 
39
46
  export function parseCSSUrl(s: string) {
40
- return s.replace(/url\("|\"\).*/g, '');
47
+ return s.replace(/url\("|"\).*/g, '');
41
48
  }
@@ -1,10 +1,10 @@
1
- export function stringToWords(s: string): Array<string> {
1
+ export function splitWith(s: string, c: string = ','): Array<string> {
2
2
  return s
3
- .split(',')
4
- .map((s) => s.trim().toLowerCase())
5
- .filter((_) => _);
3
+ .split(c)
4
+ .map((s) => s.trim())
5
+ .filter(Boolean);
6
6
  }
7
7
 
8
8
  export function sanitizeStr(s: string) {
9
- return s?.replace(/\n|\t/g, ' ').replace(/ {2,}/g, ' ').trim().toLowerCase() || '';
9
+ return s?.replace(/\n|\t/g, ' ').replace(/ {2,}/g, ' ').trim() || '';
10
10
  }
@@ -0,0 +1,31 @@
1
+ import { memoize } from '../objects';
2
+ import { splitWith } from '.';
3
+
4
+ export class RegexFilter {
5
+ private regexes: RegExp[];
6
+
7
+ constructor(str: string, flags: string = 'i') {
8
+ this.regexes = memoize(this.compileSearchRegex)(str, flags);
9
+ }
10
+
11
+ // 'dog,bog,f:girl' or r:dog|bog... => [r/dog/i, r/bog/i, r/(^|\ )girl($|\ )/i]
12
+ private compileSearchRegex(str: string, flags: string): RegExp[] {
13
+ if (str.startsWith('r:')) return [new RegExp(str.slice(2), flags)];
14
+
15
+ const regexes = splitWith(str)
16
+ .map(
17
+ (s) => s.replace(/f:(\w+)/g, (_, w) => `(^|\\ |,)${w}($|\\ |,)`), // full word
18
+ )
19
+ .map((_) => new RegExp(_, flags));
20
+
21
+ return regexes;
22
+ }
23
+
24
+ public hasEvery(str: string) {
25
+ return this.regexes.every((r) => r.test(str));
26
+ }
27
+
28
+ public hasNone(str: string) {
29
+ return this.regexes.every((r) => !r.test(str));
30
+ }
31
+ }
@@ -0,0 +1,10 @@
1
+ export function getWindows(): Window[] {
2
+ return [window, unsafeWindow].filter(Boolean);
3
+ }
4
+
5
+ export function assignGlobals<T extends {}>(obj: T) {
6
+ const windows = getWindows();
7
+ windows.forEach((w: Window) => {
8
+ Object.assign(w, obj);
9
+ });
10
+ }
@@ -1,40 +0,0 @@
1
- import { InfiniteScroller } from '../infinite-scroll';
2
- import type { IRules } from '../rules';
3
-
4
- export interface JabroniStore {
5
- state: Record<string, boolean | string | number>;
6
- localState: Record<string, boolean | string | number>;
7
- subscribe: (callback: () => void) => void;
8
- }
9
-
10
- export function createInfiniteScroller(
11
- store: JabroniStore,
12
- parseData: (document: HTMLElement) => void,
13
- rules: IRules,
14
- ) {
15
- const enabled = store.state.infiniteScrollEnabled as boolean;
16
-
17
- const paginationOffset = rules.paginationStrategy.getPaginationOffset();
18
- const paginationElement = rules.paginationStrategy.getPaginationElement() as HTMLElement;
19
- const paginationLast = rules.paginationStrategy.getPaginationLast();
20
- const paginationUrlGenerator = rules.paginationStrategy.getPaginationUrlGenerator();
21
-
22
- const iscroller = new InfiniteScroller({
23
- enabled,
24
- parseData,
25
- paginationLast,
26
- paginationOffset,
27
- paginationElement,
28
- paginationUrlGenerator,
29
- ...rules,
30
- }).onScroll(({ paginationLast, paginationOffset }) => {
31
- store.localState.pagIndexLast = paginationLast;
32
- store.localState.pagIndexCur = paginationOffset;
33
- }, true);
34
-
35
- store.subscribe(() => {
36
- iscroller.enabled = store.state.infiniteScrollEnabled as boolean;
37
- });
38
-
39
- return iscroller;
40
- }