@adriangalilea/utils 2.11.0 → 2.13.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 +37 -25
- package/dist/universal/url/index.d.ts +29 -22
- package/dist/universal/url/index.d.ts.map +1 -1
- package/dist/universal/url/index.js +66 -37
- package/dist/universal/url/index.js.map +1 -1
- package/dist/universal/url/sites.d.ts +48 -0
- package/dist/universal/url/sites.d.ts.map +1 -0
- package/dist/universal/url/sites.js +151 -0
- package/dist/universal/url/sites.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,44 +101,56 @@ money(100, 'EUR') // "€100.00"
|
|
|
101
101
|
|
|
102
102
|
### URL (`url`)
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
concern rides its state of the art instead of hand-rolls
|
|
107
|
-
[linkifyjs](https://linkify.js.org) (scanner-based, TLD-aware);
|
|
108
|
-
WHATWG URL API, never regexes; the knowledge of WHICH params
|
|
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
|
|
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 {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
// [{
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
//
|
|
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
|
-
|
|
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 —
|
|
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
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
// '
|
|
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')
|
|
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, youtubeTimestampUrl, 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
|
|
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
|
|
12
|
-
export interface
|
|
13
|
-
/**
|
|
14
|
-
|
|
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
|
|
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,
|
|
23
|
-
*
|
|
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
|
|
29
|
-
/**
|
|
30
|
-
|
|
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
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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":"
|
|
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,EACN,KAAK,EACL,OAAO,EACP,mBAAmB,EACnB,mBAAmB,EACnB,UAAU,EACV,cAAc,EACd,KAAK,WAAW,GAChB,MAAM,YAAY,CAAC;AAEpB,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
|
-
*
|
|
3
|
-
*
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
* -
|
|
33
|
+
* - per-site content identity: the adapter registry (sites.ts).
|
|
27
34
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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, youtubeTimestampUrl, youtubeUrl, youtubeVideoId, } from "./sites.js";
|
|
36
47
|
/**
|
|
37
|
-
* Every http(s) URL in free text, in order,
|
|
38
|
-
*
|
|
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
|
|
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({
|
|
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
|
-
|
|
61
|
-
|
|
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
|
|
99
|
-
*
|
|
100
|
-
*
|
|
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 `${
|
|
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":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/universal/url/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
/**
|
|
40
|
+
* Add or replace the playhead offset (`t=90s`) on a YouTube video URL,
|
|
41
|
+
* preserving the URL's shape and other params. A bare id gets the canonical
|
|
42
|
+
* watch URL first. Non-video input is returned unchanged.
|
|
43
|
+
*/
|
|
44
|
+
export declare function youtubeTimestampUrl(idOrUrl: string, seconds: number): string;
|
|
45
|
+
export declare const SITES: readonly SiteAdapter[];
|
|
46
|
+
/** The adapter owning this (lowercased, www-less) host, or null. */
|
|
47
|
+
export declare function siteFor(host: string): SiteAdapter | null;
|
|
48
|
+
//# 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;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAY5E;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,151 @@
|
|
|
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
|
+
/**
|
|
103
|
+
* Add or replace the playhead offset (`t=90s`) on a YouTube video URL,
|
|
104
|
+
* preserving the URL's shape and other params. A bare id gets the canonical
|
|
105
|
+
* watch URL first. Non-video input is returned unchanged.
|
|
106
|
+
*/
|
|
107
|
+
export function youtubeTimestampUrl(idOrUrl, seconds) {
|
|
108
|
+
if (!youtubeVideoId(idOrUrl))
|
|
109
|
+
return idOrUrl;
|
|
110
|
+
const source = YOUTUBE_ID.test(idOrUrl.trim()) ? youtube.canonicalUrl(idOrUrl.trim()) : idOrUrl;
|
|
111
|
+
const offset = Number.isFinite(seconds) ? Math.max(0, Math.floor(seconds)) : 0;
|
|
112
|
+
try {
|
|
113
|
+
const url = new URL(source);
|
|
114
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
115
|
+
return idOrUrl;
|
|
116
|
+
url.searchParams.set("t", `${offset}s`);
|
|
117
|
+
return url.toString();
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return idOrUrl;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// ── the registry ────────────────────────────────────────────────────────────
|
|
124
|
+
export const SITES = [youtube];
|
|
125
|
+
/** The adapter owning this (lowercased, www-less) host, or null. */
|
|
126
|
+
export function siteFor(host) {
|
|
127
|
+
return SITES.find((s) => s.matches(host)) ?? null;
|
|
128
|
+
}
|
|
129
|
+
// ── internals ───────────────────────────────────────────────────────────────
|
|
130
|
+
function parseLoose(input) {
|
|
131
|
+
try {
|
|
132
|
+
return new URL(input);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
try {
|
|
136
|
+
return new URL(`https://${input}`);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function safeDecode(input) {
|
|
144
|
+
try {
|
|
145
|
+
return decodeURIComponent(input);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return input;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
//# 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