@adriangalilea/utils 2.11.0 → 2.12.0

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.
package/README.md CHANGED
@@ -101,44 +101,56 @@ money(100, 'EUR') // "€100.00"
101
101
 
102
102
  ### URL (`url`)
103
103
 
104
- Everything between "text someone typed" and "a URL you can use": detection,
105
- tracking-parameter stripping, canonical cache keys, host questions. Each
106
- concern rides its state of the art instead of hand-rolls — detection is
107
- [linkifyjs](https://linkify.js.org) (scanner-based, TLD-aware); parsing is the
108
- WHATWG URL API, never regexes; the knowledge of WHICH params are tracking is
109
- vendored from
104
+ URLs, from user-typed text to cache identity. The whole funnel — *is there a
105
+ URL? which resource? whose content? what identity?* — answered once, on one
106
+ object. Each concern rides its state of the art instead of hand-rolls:
107
+ detection is [linkifyjs](https://linkify.js.org) (scanner-based, TLD-aware);
108
+ parsing is the WHATWG URL API, never regexes; the knowledge of WHICH params
109
+ are tracking is vendored from
110
110
  [@protontech/tidy-url](https://www.npmjs.com/package/@protontech/tidy-url)
111
111
  (Proton's maintained fork of DrKain/tidy-url, MIT; refresh with
112
- `pnpm update-url-rules`) plus a small tested overlay. Global rules only for
112
+ `pnpm update-url-rules`) plus a small tested overlay; per-site content
113
+ identity is an adapter registry (YouTube built in). Global rules only for
113
114
  unambiguous trackers (utm_*, fbclid, gclid, …); ambiguous names stay per-host —
114
115
  `si` is junk on YouTube/Spotify but `ref` on GitHub names a branch and
115
116
  survives.
116
117
 
117
118
  ```typescript
118
- import { findUrls, asHttpUrl, cleanUrl, urlKey, hostOf, hostMatches } from '@adriangalilea/utils/url'
119
-
120
- // Every http(s) URL in free text, in order, cleaned — with spans and
121
- // whether the user typed the scheme (requireScheme drops bare domains).
122
- findUrls('check https://a.com/x?utm_source=t and example.com/y')
123
- // [{ url: 'https://a.com/x', start: 6, end: 34, hadScheme: true },
124
- // { url: 'https://example.com/y', start: 39, end: 52, hadScheme: false }]
125
-
126
- // One pasted token → one clean URL (or null). Punctuation, brackets,
119
+ import { urlsIn, urlOf, cleanUrl, urlKey, hostOf, hostMatches,
120
+ youtubeVideoId, youtubeUrl, youtubeThumbnailUrl } from '@adriangalilea/utils/url'
121
+
122
+ // Every http(s) URL in free text, in order, fully resolved:
123
+ urlsIn('watch youtu.be/dQw4w9WgXcQ?si=junk and https://a.com/x?utm_source=t')
124
+ // [{ raw: 'youtu.be/dQw4w9WgXcQ?si=junk', start: 6, end: 34, hadScheme: false,
125
+ // href: 'https://youtu.be/dQw4w9WgXcQ', // cleaned — fetch/share this
126
+ // host: 'youtu.be', site: 'youtube', id: 'dQw4w9WgXcQ',
127
+ // key: 'youtube:dQw4w9WgXcQ' }, // THE cache/dedupe identity
128
+ // { …, href: 'https://a.com/x', site: null, id: null, key: 'a.com/x' }]
129
+
130
+ // One pasted token → one resolved Url (or null). Punctuation, brackets,
127
131
  // emails-are-not-URLs: the scanner's problem, not yours.
128
- asHttpUrl('<https://example.com/a?utm_source=x>,') // 'https://example.com/a'
132
+ urlOf('<https://example.com/a?utm_source=x>,')?.href // 'https://example.com/a'
133
+ urlOf('example.com/a', { requireScheme: true }) // null (bare domains opt-out)
134
+
135
+ // `key` is the point: every spelling of one resource collides.
136
+ // Recognized sites collapse to their native content id —
137
+ urlKey('https://music.youtube.com/watch?v=dQw4w9WgXcQ&si=x') // 'youtube:dQw4w9WgXcQ'
138
+ urlKey('https://www.youtube.com/shorts/dQw4w9WgXcQ') // 'youtube:dQw4w9WgXcQ'
139
+ // — everything else canonicalizes generically:
140
+ urlKey('https://www.theverge.com/2026/story/?utm_source=x#c') // 'theverge.com/2026/story'
129
141
 
130
- // The SAME resource minus tracking — safe to fetch, share, display.
131
- // Keeps scheme, www., param order, and the fragment (a real anchor).
142
+ // The SAME resource minus tracking — keeps scheme, www., order, fragment.
132
143
  cleanUrl('https://www.youtube.com/watch?v=abc&t=120s&si=junk')
133
144
  // 'https://www.youtube.com/watch?v=abc&t=120s'
134
145
 
135
- // The resource's IDENTITY — for cache keys and dedupe. Scheme-agnostic,
136
- // www-less lowercase host, no trailing slash, params stripped + sorted,
137
- // fragment dropped: every spelling of one page collides, different pages never do.
138
- urlKey('https://www.theverge.com/2026/1/1/story/?utm_source=twitter#comments')
139
- // 'theverge.com/2026/1/1/story'
146
+ // YouTube identity from ANY spelling (watch, youtu.be, shorts, live, embed,
147
+ // music., nocookie, redirect wrappers, bare 11-char id):
148
+ youtubeVideoId('https://www.youtube.com/attribution_link?u=%2Fwatch%3Fv%3DdQw4w9WgXcQ')
149
+ // 'dQw4w9WgXcQ'
150
+ youtubeUrl('dQw4w9WgXcQ') // 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
151
+ youtubeThumbnailUrl('dQw4w9WgXcQ') // 'https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg'
140
152
 
141
- hostOf('https://www.theverge.com/a') // 'theverge.com'
153
+ hostOf('https://www.theverge.com/a') // 'theverge.com'
142
154
  hostMatches('music.youtube.com', 'youtube.com') // true (RFC 6265 domain-match)
143
155
  hostMatches('notyoutube.com', 'youtube.com') // false
144
156
 
@@ -1,37 +1,41 @@
1
1
  export { TIDY_RULES, TIDY_VERSION, type TrackingProvider } from "./tidy-rules.js";
2
2
  export { OVERLAY_RULES } from "./overlay.js";
3
+ export { SITES, siteFor, youtubeThumbnailUrl, youtubeUrl, youtubeVideoId, type SiteAdapter } from "./sites.js";
3
4
  export interface StripOptions {
4
5
  /** Extra param names to strip (compared lowercased), for app-specific junk. */
5
6
  strip?: readonly string[];
6
7
  }
7
- export interface FindUrlsOptions extends StripOptions {
8
+ export interface UrlsInOptions extends StripOptions {
8
9
  /** Drop scheme-less matches (`example.com/x`) instead of coercing them to https://. */
9
10
  requireScheme?: boolean;
10
11
  }
11
- /** One URL found in free text by {@link findUrls}. */
12
- export interface FoundUrl {
13
- /** The cleaned http(s) URL (https:// coerced onto scheme-less matches). */
14
- url: string;
15
- /** Span of the raw match in the input text. */
12
+ /** One URL resolved from text: every layer of the funnel, answered once. */
13
+ export interface Url {
14
+ /** Exactly the text that matched (punctuation and tracking still on it). */
15
+ raw: string;
16
+ /** Span of the raw match in the input text (cut links out of a message). */
16
17
  start: number;
17
18
  end: number;
18
- /** False for scheme-less matches the scanner inferred from a known TLD. */
19
+ /** False when the scanner inferred the link from a bare domain (`example.com/x`). */
19
20
  hadScheme: boolean;
21
+ /** The cleaned absolute URL — fetch, share, and display this. */
22
+ href: string;
23
+ /** The www-less host. */
24
+ host: string;
25
+ /** The recognized service owning the host ("youtube"), or null. */
26
+ site: string | null;
27
+ /** The service's native content id (a YouTube video id), or null. */
28
+ id: string | null;
29
+ /** Canonical identity for caches/dedupe: every spelling of one resource collides. */
30
+ key: string;
20
31
  }
21
32
  /**
22
- * Every http(s) URL in free text, in order, cleaned. Detection is linkifyjs —
23
- * a real scanner, TLD-aware for scheme-less domains, and it owns the messy
24
- * text-boundary problems (trailing punctuation, brackets, emails-are-not-URLs)
25
- * — then each match goes through the URL parser and tracking-param stripping.
26
- * Scheme-less matches coerce to https:// (drop them with `requireScheme`).
33
+ * Every http(s) URL in free text, in order, fully resolved. Duplicate
34
+ * spellings are kept (spans matter); dedupe is one line on `key`.
27
35
  */
28
- export declare function findUrls(text: string, opts?: FindUrlsOptions): FoundUrl[];
29
- /**
30
- * A user-pasted token as one CLEAN http(s) URL, or null: {@link findUrls}
31
- * over the token, first match. The one verb between "text someone typed"
32
- * and "a URL you can use".
33
- */
34
- export declare function asHttpUrl(token: string, opts?: FindUrlsOptions): string | null;
36
+ export declare function urlsIn(text: string, opts?: UrlsInOptions): Url[];
37
+ /** One pasted token / URL string as a resolved {@link Url}, or null. */
38
+ export declare function urlOf(token: string, opts?: UrlsInOptions): Url | null;
35
39
  /** A URL's host without a leading www., or the input unchanged if it won't parse. */
36
40
  export declare function hostOf(url: string): string;
37
41
  /**
@@ -46,9 +50,12 @@ export declare function hostMatches(host: string, domain: string): boolean;
46
50
  */
47
51
  export declare function cleanUrl(url: string, opts?: StripOptions): string;
48
52
  /**
49
- * A canonical identity for the resource behind `url` — `host/path?sortedQuery`
50
- * — for cache keys and dedupe. Unparseable or non-http(s) input is returned
51
- * unchanged so it still keys consistently.
53
+ * A canonical identity for the resource behind `url`, for cache keys and
54
+ * dedupe. A recognized site's content collapses to its native id
55
+ * ("youtube:dQw4w9WgXcQ" — watch, youtu.be, shorts, live, embed, all one
56
+ * key); everything else canonicalizes to `host/path?sortedQuery`,
57
+ * scheme-agnostic, www-less, tracking-stripped, fragment-free. Unparseable
58
+ * or non-http(s) input is returned unchanged so it still keys consistently.
52
59
  */
53
60
  export declare function urlKey(url: string, opts?: StripOptions): string;
54
61
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/universal/url/index.ts"],"names":[],"mappings":"AAkCA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAClF,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,MAAM,WAAW,YAAY;IAC5B,+EAA+E;IAC/E,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,eAAgB,SAAQ,YAAY;IACpD,uFAAuF;IACvF,aAAa,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,sDAAsD;AACtD,MAAM,WAAW,QAAQ;IACxB,2EAA2E;IAC3E,GAAG,EAAE,MAAM,CAAC;IACZ,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,2EAA2E;IAC3E,SAAS,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,QAAQ,EAAE,CAazE;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAE9E;AAED,qFAAqF;AACrF,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM1C;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAIjE;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,MAAM,CAKjE;AAED;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,MAAM,CAU/D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,SAAK,GAAG,OAAO,CAUhE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/universal/url/index.ts"],"names":[],"mappings":"AA4CA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAClF,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAE/G,MAAM,WAAW,YAAY;IAC5B,+EAA+E;IAC/E,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,aAAc,SAAQ,YAAY;IAClD,uFAAuF;IACvF,aAAa,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,4EAA4E;AAC5E,MAAM,WAAW,GAAG;IACnB,4EAA4E;IAC5E,GAAG,EAAE,MAAM,CAAC;IACZ,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,qFAAqF;IACrF,SAAS,EAAE,OAAO,CAAC;IACnB,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,qEAAqE;IACrE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,qFAAqF;IACrF,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,GAAG,EAAE,CAahE;AAED,wEAAwE;AACxE,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,GAAG,GAAG,IAAI,CAErE;AAED,qFAAqF;AACrF,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM1C;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAIjE;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,MAAM,CAKjE;AAED;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,MAAM,CAY/D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,SAAK,GAAG,OAAO,CAUhE"}
@@ -1,46 +1,54 @@
1
1
  /**
2
- * URL hygiene: tracking-parameter stripping and canonical cache keys.
3
- * Pure and dependency-free — safe in Node, Workers, and browsers.
2
+ * URLs, from user-typed text to cache identity. Safe in Node, Workers, and
3
+ * browsers. The whole funnel every consumer walks — is there a URL? which
4
+ * resource? whose content? what identity? — answered ONCE, on one object:
4
5
  *
5
- * Two verbs for two jobs:
6
- * - `cleanUrl(url)`: the SAME resource, minus tracking params. Safe to fetch,
7
- * share, and display. Keeps scheme, www., remaining param order, and the
8
- * fragment (a fragment can be a real anchor).
9
- * - `urlKey(url)`: the resource's IDENTITY, for cache keys and dedupe.
10
- * Scheme-agnostic, lowercase host without www. or a trailing dot, no
11
- * trailing slash, tracking params stripped, surviving params sorted,
12
- * fragment dropped. Two spellings of one page collide; two different
13
- * pages never do.
6
+ * Url {
7
+ * raw, start, end what they typed, and where in the text
8
+ * hadScheme typed https:// vs inferred from a bare domain
9
+ * href cleaned absolute URL — fetch, share, display this
10
+ * host www-less host
11
+ * site recognized service ("youtube") or null
12
+ * id the service's native content id, or null
13
+ * key canonical identity for caches/dedupe:
14
+ * "youtube:dQw4w9WgXcQ" | "theverge.com/2026/story"
15
+ * }
14
16
  *
15
- * Plus the text side: `findUrls(text)` / `asHttpUrl(token)` pull clean URLs
16
- * out of what a user typed, and `hostOf` / `hostMatches` (RFC 6265 domain-
17
- * matching) answer host questions.
17
+ * Two verbs, one per input shape:
18
+ * urlsIn(text) → Url[] every URL in free text, in order, with spans
19
+ * urlOf(token) → Url | null one pasted token / URL string
20
+ *
21
+ * The `key` is the point: every spelling of one resource collides (scheme,
22
+ * www., trailing slash, tracking params, and for recognized sites every URL
23
+ * shape of the same content id), and two different resources never do.
18
24
  *
19
25
  * Each concern rides its state of the art instead of hand-rolls:
20
- * - detection in free text: linkifyjs (scanner-based, TLD-aware, MIT);
26
+ * - detection in free text: linkifyjs (scanner-based, TLD-aware, MIT) — it
27
+ * owns the messy boundaries (trailing punctuation, brackets, emails);
21
28
  * - parsing/serialization: the WHATWG URL API, never regexes;
22
29
  * - which params are tracking: vendored from @protontech/tidy-url (Proton's
23
30
  * maintained fork of DrKain/tidy-url, MIT; see tidy-rules.ts, refresh with
24
31
  * `pnpm update-url-rules`) plus a small tested overlay (overlay.ts) —
25
32
  * param names compare literally (lowercased), per host;
26
- * - `urlKey` is ours: no standard exists for cache identity.
33
+ * - per-site content identity: the adapter registry (sites.ts).
27
34
  *
28
- * Unparseable or non-http(s) input passes through unchanged: these are hygiene
29
- * functions over user-pasted text, not validators.
35
+ * The low-level verbs (`cleanUrl`, `urlKey`, `hostOf`, `hostMatches`,
36
+ * `isTrackingParam`, the youtube helpers) stay exported for surgical use.
37
+ * Unparseable or non-http(s) input passes through unchanged: these are
38
+ * hygiene functions over user-pasted text, not validators.
30
39
  */
31
40
  import { find } from "linkifyjs";
32
41
  import { OVERLAY_RULES } from "./overlay.js";
42
+ import { siteFor } from "./sites.js";
33
43
  import { TIDY_RULES } from "./tidy-rules.js";
34
44
  export { TIDY_RULES, TIDY_VERSION } from "./tidy-rules.js";
35
45
  export { OVERLAY_RULES } from "./overlay.js";
46
+ export { SITES, siteFor, youtubeThumbnailUrl, youtubeUrl, youtubeVideoId } from "./sites.js";
36
47
  /**
37
- * Every http(s) URL in free text, in order, cleaned. Detection is linkifyjs —
38
- * a real scanner, TLD-aware for scheme-less domains, and it owns the messy
39
- * text-boundary problems (trailing punctuation, brackets, emails-are-not-URLs)
40
- * — then each match goes through the URL parser and tracking-param stripping.
41
- * Scheme-less matches coerce to https:// (drop them with `requireScheme`).
48
+ * Every http(s) URL in free text, in order, fully resolved. Duplicate
49
+ * spellings are kept (spans matter); dedupe is one line on `key`.
42
50
  */
43
- export function findUrls(text, opts) {
51
+ export function urlsIn(text, opts) {
44
52
  const out = [];
45
53
  for (const link of find(text, "url", { defaultProtocol: "https" })) {
46
54
  // href === value exactly when the text already carried a scheme; otherwise
@@ -52,17 +60,13 @@ export function findUrls(text, opts) {
52
60
  if (!u)
53
61
  continue; // a non-http scheme the scanner knows; not our job
54
62
  deleteTracking(u, opts);
55
- out.push({ url: u.toString(), start: link.start, end: link.end, hadScheme });
63
+ out.push(resolveUrl(u, { raw: link.value, start: link.start, end: link.end, hadScheme }));
56
64
  }
57
65
  return out;
58
66
  }
59
- /**
60
- * A user-pasted token as one CLEAN http(s) URL, or null: {@link findUrls}
61
- * over the token, first match. The one verb between "text someone typed"
62
- * and "a URL you can use".
63
- */
64
- export function asHttpUrl(token, opts) {
65
- return findUrls(token, opts)[0]?.url ?? null;
67
+ /** One pasted token / URL string as a resolved {@link Url}, or null. */
68
+ export function urlOf(token, opts) {
69
+ return urlsIn(token, opts)[0] ?? null;
66
70
  }
67
71
  /** A URL's host without a leading www., or the input unchanged if it won't parse. */
68
72
  export function hostOf(url) {
@@ -95,21 +99,27 @@ export function cleanUrl(url, opts) {
95
99
  return u.toString();
96
100
  }
97
101
  /**
98
- * A canonical identity for the resource behind `url` — `host/path?sortedQuery`
99
- * — for cache keys and dedupe. Unparseable or non-http(s) input is returned
100
- * unchanged so it still keys consistently.
102
+ * A canonical identity for the resource behind `url`, for cache keys and
103
+ * dedupe. A recognized site's content collapses to its native id
104
+ * ("youtube:dQw4w9WgXcQ" — watch, youtu.be, shorts, live, embed, all one
105
+ * key); everything else canonicalizes to `host/path?sortedQuery`,
106
+ * scheme-agnostic, www-less, tracking-stripped, fragment-free. Unparseable
107
+ * or non-http(s) input is returned unchanged so it still keys consistently.
101
108
  */
102
109
  export function urlKey(url, opts) {
103
110
  const u = parseHttp(url);
104
111
  if (!u)
105
112
  return url;
113
+ const site = siteFor(normalizedHost(u));
114
+ const id = site?.id(u);
115
+ if (site && id)
116
+ return site.key(id);
106
117
  deleteTracking(u, opts);
107
118
  u.searchParams.sort();
108
- const host = u.hostname.toLowerCase().replace(/^www\./, "").replace(/\.$/, "");
109
119
  const port = u.port ? `:${u.port}` : ""; // default ports never survive URL parsing
110
120
  const path = u.pathname.replace(/\/+$/, "") || "/";
111
121
  const query = u.searchParams.toString();
112
- return `${host}${port}${path}${query ? `?${query}` : ""}`;
122
+ return `${normalizedHost(u)}${port}${path}${query ? `?${query}` : ""}`;
113
123
  }
114
124
  /**
115
125
  * True if `name` is a tracking param — globally, or on `host` specifically.
@@ -129,6 +139,25 @@ export function isTrackingParam(name, host = "") {
129
139
  }
130
140
  return doomed.has(n) && !allowed.has(n);
131
141
  }
142
+ // ── internals ───────────────────────────────────────────────────────────────
143
+ /** Assemble a {@link Url} from an already-cleaned parsed URL + its text facts. */
144
+ function resolveUrl(u, text) {
145
+ const host = normalizedHost(u);
146
+ const site = siteFor(host);
147
+ const id = site?.id(u) ?? null;
148
+ return {
149
+ ...text,
150
+ href: u.toString(),
151
+ host,
152
+ site: site?.name ?? null,
153
+ id,
154
+ key: site && id ? site.key(id) : urlKey(u.toString()),
155
+ };
156
+ }
157
+ /** Lowercased hostname without a leading www. or a trailing dot. */
158
+ function normalizedHost(u) {
159
+ return u.hostname.toLowerCase().replace(/^www\./, "").replace(/\.$/, "");
160
+ }
132
161
  let cache = null;
133
162
  /** All providers (vendored + overlay), matchers compiled once per process. */
134
163
  function compiled() {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/universal/url/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/universal/url/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The site registry: per-service identity knowledge, one adapter per service
3
+ * (the same knowledge-as-data pattern as tidy-rules.ts). An adapter answers
4
+ * "is this host yours?" and "what's the native content id in this URL?", and
5
+ * from the id derives the canonical URL and the cache-identity key — so every
6
+ * spelling of one video (watch, youtu.be, shorts, live, embed, music.,
7
+ * nocookie, nested redirects) collapses to ONE identity.
8
+ *
9
+ * Extraction is WHATWG-URL based, nested-redirect aware, and percent-decode
10
+ * tolerant — no regex URL parsing. Adding a service (spotify, vimeo, …) is
11
+ * one more adapter entry.
12
+ */
13
+ export interface SiteAdapter {
14
+ /** The service name, and the prefix of its identity keys ("youtube"). */
15
+ name: string;
16
+ /** Whether this (lowercased, www-less) host belongs to the service. */
17
+ matches(host: string): boolean;
18
+ /** The service's native content id in this URL, or null. */
19
+ id(url: URL): string | null;
20
+ /** The canonical URL for a content id. */
21
+ canonicalUrl(id: string): string;
22
+ /** The cache-identity key for a content id ("youtube:dQw4w9WgXcQ"). */
23
+ key(id: string): string;
24
+ }
25
+ /**
26
+ * The 11-char video id from ANY YouTube URL spelling or a bare id; null if
27
+ * the input isn't one. Percent-encoded input gets one decode attempt so a
28
+ * copied-from-HTML link still resolves.
29
+ */
30
+ export declare function youtubeVideoId(input: string): string | null;
31
+ /** The canonical watch URL for a video id or any YouTube URL spelling; null if neither. */
32
+ export declare function youtubeUrl(idOrUrl: string): string | null;
33
+ /**
34
+ * A public thumbnail URL for a video id or any YouTube URL spelling; null if
35
+ * neither. Uses `hqdefault`, which exists for every valid video (unlike
36
+ * `maxresdefault`, which 404s for many).
37
+ */
38
+ export declare function youtubeThumbnailUrl(idOrUrl: string): string | null;
39
+ export declare const SITES: readonly SiteAdapter[];
40
+ /** The adapter owning this (lowercased, www-less) host, or null. */
41
+ export declare function siteFor(host: string): SiteAdapter | null;
42
+ //# sourceMappingURL=sites.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sites.d.ts","sourceRoot":"","sources":["../../../src/universal/url/sites.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,WAAW;IAC3B,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,4DAA4D;IAC5D,EAAE,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,0CAA0C;IAC1C,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACjC,uEAAuE;IACvE,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;CACxB;AAgED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAY3D;AAED,2FAA2F;AAC3F,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGzD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGlE;AAID,eAAO,MAAM,KAAK,EAAE,SAAS,WAAW,EAAc,CAAC;AAEvD,oEAAoE;AACpE,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAExD"}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * The site registry: per-service identity knowledge, one adapter per service
3
+ * (the same knowledge-as-data pattern as tidy-rules.ts). An adapter answers
4
+ * "is this host yours?" and "what's the native content id in this URL?", and
5
+ * from the id derives the canonical URL and the cache-identity key — so every
6
+ * spelling of one video (watch, youtu.be, shorts, live, embed, music.,
7
+ * nocookie, nested redirects) collapses to ONE identity.
8
+ *
9
+ * Extraction is WHATWG-URL based, nested-redirect aware, and percent-decode
10
+ * tolerant — no regex URL parsing. Adding a service (spotify, vimeo, …) is
11
+ * one more adapter entry.
12
+ */
13
+ // ── youtube ─────────────────────────────────────────────────────────────────
14
+ const YOUTUBE_ID = /^[a-zA-Z0-9_-]{11}$/;
15
+ const YOUTUBE_PATH_KINDS = new Set(["embed", "v", "shorts", "live"]);
16
+ function isYouTubeHost(host) {
17
+ return (host === "youtube.com" ||
18
+ host.endsWith(".youtube.com") ||
19
+ host === "youtube-nocookie.com" ||
20
+ host.endsWith(".youtube-nocookie.com"));
21
+ }
22
+ function validYouTubeId(value) {
23
+ return value && YOUTUBE_ID.test(value) ? value : null;
24
+ }
25
+ function youtubeIdFromUrl(url) {
26
+ const host = url.hostname.toLowerCase().replace(/^www\./, "");
27
+ if (host === "youtu.be") {
28
+ return validYouTubeId(url.pathname.split("/").filter(Boolean)[0]);
29
+ }
30
+ if (!isYouTubeHost(host))
31
+ return null;
32
+ const queryId = validYouTubeId(url.searchParams.get("v") ?? url.searchParams.get("video_id"));
33
+ if (queryId)
34
+ return queryId;
35
+ const [kind, id] = url.pathname.split("/").filter(Boolean);
36
+ if (YOUTUBE_PATH_KINDS.has(kind)) {
37
+ const pathId = validYouTubeId(id);
38
+ if (pathId)
39
+ return pathId;
40
+ }
41
+ // Redirect wrappers (attribution_link?u=/watch%3Fv%3D…, redirect?q=…): the
42
+ // nested value may be relative, so resolve it against the outer URL.
43
+ for (const param of ["u", "url", "q"]) {
44
+ const nested = url.searchParams.get(param);
45
+ if (!nested)
46
+ continue;
47
+ let nestedUrl;
48
+ try {
49
+ nestedUrl = new URL(nested, url);
50
+ }
51
+ catch {
52
+ continue;
53
+ }
54
+ if (nestedUrl.href === url.href)
55
+ continue;
56
+ const nestedId = youtubeIdFromUrl(nestedUrl);
57
+ if (nestedId)
58
+ return nestedId;
59
+ }
60
+ return null;
61
+ }
62
+ const youtube = {
63
+ name: "youtube",
64
+ matches: (host) => host === "youtu.be" || isYouTubeHost(host),
65
+ id: youtubeIdFromUrl,
66
+ canonicalUrl: (id) => `https://www.youtube.com/watch?v=${id}`,
67
+ key: (id) => `youtube:${id}`,
68
+ };
69
+ /**
70
+ * The 11-char video id from ANY YouTube URL spelling or a bare id; null if
71
+ * the input isn't one. Percent-encoded input gets one decode attempt so a
72
+ * copied-from-HTML link still resolves.
73
+ */
74
+ export function youtubeVideoId(input) {
75
+ const trimmed = input.trim();
76
+ if (YOUTUBE_ID.test(trimmed))
77
+ return trimmed;
78
+ for (const candidate of [trimmed, safeDecode(trimmed)]) {
79
+ const url = parseLoose(candidate);
80
+ if (url) {
81
+ const id = youtubeIdFromUrl(url);
82
+ if (id)
83
+ return id;
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+ /** The canonical watch URL for a video id or any YouTube URL spelling; null if neither. */
89
+ export function youtubeUrl(idOrUrl) {
90
+ const id = youtubeVideoId(idOrUrl);
91
+ return id ? youtube.canonicalUrl(id) : null;
92
+ }
93
+ /**
94
+ * A public thumbnail URL for a video id or any YouTube URL spelling; null if
95
+ * neither. Uses `hqdefault`, which exists for every valid video (unlike
96
+ * `maxresdefault`, which 404s for many).
97
+ */
98
+ export function youtubeThumbnailUrl(idOrUrl) {
99
+ const id = youtubeVideoId(idOrUrl);
100
+ return id ? `https://i.ytimg.com/vi/${id}/hqdefault.jpg` : null;
101
+ }
102
+ // ── the registry ────────────────────────────────────────────────────────────
103
+ export const SITES = [youtube];
104
+ /** The adapter owning this (lowercased, www-less) host, or null. */
105
+ export function siteFor(host) {
106
+ return SITES.find((s) => s.matches(host)) ?? null;
107
+ }
108
+ // ── internals ───────────────────────────────────────────────────────────────
109
+ function parseLoose(input) {
110
+ try {
111
+ return new URL(input);
112
+ }
113
+ catch {
114
+ try {
115
+ return new URL(`https://${input}`);
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }
121
+ }
122
+ function safeDecode(input) {
123
+ try {
124
+ return decodeURIComponent(input);
125
+ }
126
+ catch {
127
+ return input;
128
+ }
129
+ }
130
+ //# sourceMappingURL=sites.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sites.js","sourceRoot":"","sources":["../../../src/universal/url/sites.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adriangalilea/utils",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "TypeScript utilities - logger, currency, formatter, GramIO bot plugins, and more",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",