@adriangalilea/utils 2.10.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,28 +101,58 @@ money(100, 'EUR') // "€100.00"
101
101
 
102
102
  ### URL (`url`)
103
103
 
104
- Tracking-parameter stripping and canonical cache keys, on the WHATWG URL API.
105
- The knowledge of WHICH params are tracking is 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
106
110
  [@protontech/tidy-url](https://www.npmjs.com/package/@protontech/tidy-url)
107
111
  (Proton's maintained fork of DrKain/tidy-url, MIT; refresh with
108
- `pnpm update-url-rules`) plus a small tested overlay: global rules only for
109
- unambiguous trackers (utm_*, fbclid, gclid, …), ambiguous names per-host —
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
114
+ unambiguous trackers (utm_*, fbclid, gclid, …); ambiguous names stay per-host —
110
115
  `si` is junk on YouTube/Spotify but `ref` on GitHub names a branch and
111
- survives. Two verbs for two jobs:
116
+ survives.
112
117
 
113
118
  ```typescript
114
- import { cleanUrl, urlKey } from '@adriangalilea/utils/url'
115
-
116
- // The SAME resource minus tracking — safe to fetch, share, display.
117
- // Keeps scheme, www., param order, and the fragment (a real anchor).
118
- cleanUrl('https://www.youtube.com/watch?v=abc&t=120s&si=junk&utm_source=x')
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,
131
+ // emails-are-not-URLs: the scanner's problem, not yours.
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'
141
+
142
+ // The SAME resource minus tracking — keeps scheme, www., order, fragment.
143
+ cleanUrl('https://www.youtube.com/watch?v=abc&t=120s&si=junk')
119
144
  // 'https://www.youtube.com/watch?v=abc&t=120s'
120
145
 
121
- // The resource's IDENTITY — for cache keys and dedupe. Scheme-agnostic,
122
- // www-less lowercase host, no trailing slash, params stripped + sorted,
123
- // fragment dropped: every spelling of one page collides, different pages never do.
124
- urlKey('https://www.theverge.com/2026/1/1/story/?utm_source=twitter#comments')
125
- // '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'
152
+
153
+ hostOf('https://www.theverge.com/a') // 'theverge.com'
154
+ hostMatches('music.youtube.com', 'youtube.com') // true (RFC 6265 domain-match)
155
+ hostMatches('notyoutube.com', 'youtube.com') // false
126
156
 
127
157
  // App-specific junk rides along per call
128
158
  urlKey(url, { strip: ['session_id'] })
@@ -1,18 +1,61 @@
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
  }
8
+ export interface UrlsInOptions extends StripOptions {
9
+ /** Drop scheme-less matches (`example.com/x`) instead of coercing them to https://. */
10
+ requireScheme?: boolean;
11
+ }
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). */
17
+ start: number;
18
+ end: number;
19
+ /** False when the scanner inferred the link from a bare domain (`example.com/x`). */
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;
31
+ }
32
+ /**
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`.
35
+ */
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;
39
+ /** A URL's host without a leading www., or the input unchanged if it won't parse. */
40
+ export declare function hostOf(url: string): string;
41
+ /**
42
+ * True if `host` IS `domain` or a subdomain of it — never a lookalike suffix
43
+ * (`notyoutube.com` doesn't match `youtube.com`). RFC 6265 §5.1.3 domain-
44
+ * matching semantics, the same rule cookies use.
45
+ */
46
+ export declare function hostMatches(host: string, domain: string): boolean;
7
47
  /**
8
48
  * The same URL minus tracking params: safe to fetch, share, and display.
9
49
  * Unparseable or non-http(s) input is returned unchanged.
10
50
  */
11
51
  export declare function cleanUrl(url: string, opts?: StripOptions): string;
12
52
  /**
13
- * A canonical identity for the resource behind `url` — `host/path?sortedQuery`
14
- * — for cache keys and dedupe. Unparseable or non-http(s) input is returned
15
- * 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.
16
59
  */
17
60
  export declare function urlKey(url: string, opts?: StripOptions): string;
18
61
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/universal/url/index.ts"],"names":[],"mappings":"AA2BA,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;;;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,31 +1,92 @@
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
- * The knowledge of WHICH params are tracking is not ours: it's vendored from
16
- * @protontech/tidy-url (Proton's maintained fork of DrKain/tidy-url, MIT; see
17
- * tidy-rules.ts, refresh with `pnpm update-url-rules`) plus a small tested
18
- * overlay for gaps (overlay.ts). URLs are parsed with the WHATWG URL API,
19
- * never regexes; the dataset's per-provider matchers select which rules apply
20
- * to a host, and param names are compared literally (lowercased).
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
21
20
  *
22
- * Unparseable or non-http(s) input passes through unchanged: these are hygiene
23
- * functions over user-pasted text, not validators.
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.
24
+ *
25
+ * Each concern rides its state of the art instead of hand-rolls:
26
+ * - detection in free text: linkifyjs (scanner-based, TLD-aware, MIT) — it
27
+ * owns the messy boundaries (trailing punctuation, brackets, emails);
28
+ * - parsing/serialization: the WHATWG URL API, never regexes;
29
+ * - which params are tracking: vendored from @protontech/tidy-url (Proton's
30
+ * maintained fork of DrKain/tidy-url, MIT; see tidy-rules.ts, refresh with
31
+ * `pnpm update-url-rules`) plus a small tested overlay (overlay.ts) —
32
+ * param names compare literally (lowercased), per host;
33
+ * - per-site content identity: the adapter registry (sites.ts).
34
+ *
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.
24
39
  */
40
+ import { find } from "linkifyjs";
25
41
  import { OVERLAY_RULES } from "./overlay.js";
42
+ import { siteFor } from "./sites.js";
26
43
  import { TIDY_RULES } from "./tidy-rules.js";
27
44
  export { TIDY_RULES, TIDY_VERSION } from "./tidy-rules.js";
28
45
  export { OVERLAY_RULES } from "./overlay.js";
46
+ export { SITES, siteFor, youtubeThumbnailUrl, youtubeUrl, youtubeVideoId } from "./sites.js";
47
+ /**
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`.
50
+ */
51
+ export function urlsIn(text, opts) {
52
+ const out = [];
53
+ for (const link of find(text, "url", { defaultProtocol: "https" })) {
54
+ // href === value exactly when the text already carried a scheme; otherwise
55
+ // the scanner built href by prepending defaultProtocol onto the bare match.
56
+ const hadScheme = link.href === link.value;
57
+ if (!hadScheme && opts?.requireScheme)
58
+ continue;
59
+ const u = parseHttp(link.href);
60
+ if (!u)
61
+ continue; // a non-http scheme the scanner knows; not our job
62
+ deleteTracking(u, opts);
63
+ out.push(resolveUrl(u, { raw: link.value, start: link.start, end: link.end, hadScheme }));
64
+ }
65
+ return out;
66
+ }
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;
70
+ }
71
+ /** A URL's host without a leading www., or the input unchanged if it won't parse. */
72
+ export function hostOf(url) {
73
+ try {
74
+ return new URL(url).hostname.replace(/^www\./, "");
75
+ }
76
+ catch {
77
+ return url;
78
+ }
79
+ }
80
+ /**
81
+ * True if `host` IS `domain` or a subdomain of it — never a lookalike suffix
82
+ * (`notyoutube.com` doesn't match `youtube.com`). RFC 6265 §5.1.3 domain-
83
+ * matching semantics, the same rule cookies use.
84
+ */
85
+ export function hostMatches(host, domain) {
86
+ const h = host.toLowerCase().replace(/^www\./, "").replace(/\.$/, "");
87
+ const d = domain.toLowerCase();
88
+ return h === d || h.endsWith(`.${d}`);
89
+ }
29
90
  /**
30
91
  * The same URL minus tracking params: safe to fetch, share, and display.
31
92
  * Unparseable or non-http(s) input is returned unchanged.
@@ -38,21 +99,27 @@ export function cleanUrl(url, opts) {
38
99
  return u.toString();
39
100
  }
40
101
  /**
41
- * A canonical identity for the resource behind `url` — `host/path?sortedQuery`
42
- * — for cache keys and dedupe. Unparseable or non-http(s) input is returned
43
- * 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.
44
108
  */
45
109
  export function urlKey(url, opts) {
46
110
  const u = parseHttp(url);
47
111
  if (!u)
48
112
  return url;
113
+ const site = siteFor(normalizedHost(u));
114
+ const id = site?.id(u);
115
+ if (site && id)
116
+ return site.key(id);
49
117
  deleteTracking(u, opts);
50
118
  u.searchParams.sort();
51
- const host = u.hostname.toLowerCase().replace(/^www\./, "").replace(/\.$/, "");
52
119
  const port = u.port ? `:${u.port}` : ""; // default ports never survive URL parsing
53
120
  const path = u.pathname.replace(/\/+$/, "") || "/";
54
121
  const query = u.searchParams.toString();
55
- return `${host}${port}${path}${query ? `?${query}` : ""}`;
122
+ return `${normalizedHost(u)}${port}${path}${query ? `?${query}` : ""}`;
56
123
  }
57
124
  /**
58
125
  * True if `name` is a tracking param — globally, or on `host` specifically.
@@ -72,6 +139,25 @@ export function isTrackingParam(name, host = "") {
72
139
  }
73
140
  return doomed.has(n) && !allowed.has(n);
74
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
+ }
75
161
  let cache = null;
76
162
  /** All providers (vendored + overlay), matchers compiled once per process. */
77
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.10.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",
@@ -220,17 +220,17 @@
220
220
  "zod": "^4.4.3"
221
221
  },
222
222
  "peerDependencies": {
223
+ "@ai-sdk/anthropic": "^4.0.0",
224
+ "@ai-sdk/openai-compatible": "^3.0.0",
223
225
  "@gramio/format": ">=0.8.0",
224
226
  "@gramio/session": ">=0.2.0",
227
+ "@gramio/split": "^0.0.1",
225
228
  "@gramio/storage": ">=2.0.0",
229
+ "@openrouter/ai-sdk-provider": "^3.0.0",
230
+ "ai": "^7.0.0",
226
231
  "gramio": "^0.12.0",
227
232
  "marked": ">=12",
228
- "zod": ">=3.22",
229
- "ai": "^7.0.0",
230
- "@ai-sdk/anthropic": "^4.0.0",
231
- "@ai-sdk/openai-compatible": "^3.0.0",
232
- "@openrouter/ai-sdk-provider": "^3.0.0",
233
- "@gramio/split": "^0.0.1"
233
+ "zod": ">=3.22"
234
234
  },
235
235
  "peerDependenciesMeta": {
236
236
  "@gramio/format": {
@@ -279,5 +279,8 @@
279
279
  "url": "https://github.com/adriangalilea/ts-utils/issues"
280
280
  },
281
281
  "homepage": "https://github.com/adriangalilea/ts-utils#readme",
282
- "sideEffects": false
282
+ "sideEffects": false,
283
+ "dependencies": {
284
+ "linkifyjs": "^4.3.3"
285
+ }
283
286
  }