@aturi.to/waypoints 0.1.2 → 0.1.3

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/dist/index.cjs CHANGED
@@ -77,6 +77,9 @@ var COMPAT_FAMILY_ORDER = [
77
77
  "blento",
78
78
  "atproto-explorer"
79
79
  ];
80
+ function socialAppComposeIntent(origin) {
81
+ return { url: `${origin}/intent/compose`, textParam: "text" };
82
+ }
80
83
  var WAYPOINT_DESTINATIONS_DATA = {
81
84
  aturi: {
82
85
  id: "aturi",
@@ -136,6 +139,11 @@ var WAYPOINT_DESTINATIONS_DATA = {
136
139
  },
137
140
  supportedTypes: ["post", "profile", "list", "record"],
138
141
  category: "blueskyClients",
142
+ // Anisota implements the Bluesky shape deliberately, then extends it with
143
+ // `url`, `tags`, `channel`, and `title` (and answers `/post` with the same
144
+ // query). What's declared here is the interoperable subset every caller can
145
+ // rely on; the extras are Anisota-only and unrepresentable in this field.
146
+ composeIntent: socialAppComposeIntent("https://anisota.net"),
139
147
  redirectCompat: ["bluesky-social", "standard-site"],
140
148
  expectedCollections: ["app.bsky.", "net.anisota."]
141
149
  },
@@ -152,12 +160,19 @@ var WAYPOINT_DESTINATIONS_DATA = {
152
160
  if (collection === "app.bsky.feed.post") {
153
161
  return `https://bsky.app/profile/${handle}/post/${rkey}`;
154
162
  }
163
+ if (collection === "app.bsky.graph.list") {
164
+ return `https://bsky.app/profile/${handle}/lists/${rkey}`;
165
+ }
155
166
  return `https://bsky.app/profile/${handle}`;
156
167
  }
157
168
  return `https://bsky.app/profile/${handle}`;
158
169
  },
159
170
  supportedTypes: ["post", "profile", "list", "record"],
160
171
  category: "blueskyClients",
172
+ composeIntent: {
173
+ ...socialAppComposeIntent("https://bsky.app"),
174
+ appUrl: "bluesky://intent/compose"
175
+ },
161
176
  redirectCompat: ["bluesky-social"],
162
177
  expectedCollections: ["app.bsky."]
163
178
  },
@@ -174,12 +189,16 @@ var WAYPOINT_DESTINATIONS_DATA = {
174
189
  if (collection === "app.bsky.feed.post") {
175
190
  return `https://blacksky.community/profile/${handle}/post/${rkey}`;
176
191
  }
192
+ if (collection === "app.bsky.graph.list") {
193
+ return `https://blacksky.community/profile/${handle}/lists/${rkey}`;
194
+ }
177
195
  return `https://blacksky.community/profile/${handle}`;
178
196
  }
179
197
  return `https://blacksky.community/profile/${handle}`;
180
198
  },
181
199
  supportedTypes: ["post", "profile", "list", "record"],
182
200
  category: "blueskyForks",
201
+ composeIntent: socialAppComposeIntent("https://blacksky.community"),
183
202
  redirectCompat: ["bluesky-social"],
184
203
  expectedCollections: ["app.bsky."]
185
204
  },
@@ -188,7 +207,6 @@ var WAYPOINT_DESTINATIONS_DATA = {
188
207
  name: "Red Dwarf",
189
208
  description: (collection) => {
190
209
  if (collection === "app.bsky.feed.post") return "View post on reddwarf.app";
191
- if (collection === "app.bsky.graph.list") return "View list on reddwarf.app";
192
210
  return "View profile on reddwarf.app";
193
211
  },
194
212
  getUrl: (handle, collection, rkey) => {
@@ -200,7 +218,10 @@ var WAYPOINT_DESTINATIONS_DATA = {
200
218
  }
201
219
  return `https://reddwarf.app/profile/${handle}`;
202
220
  },
203
- supportedTypes: ["post", "profile", "list", "record"],
221
+ // No `list`: unlike the social-app forks in this family, Red Dwarf has no
222
+ // `/profile/:handle/lists/:rkey` route, so offering it for a list would
223
+ // silently land the user on the list's author instead.
224
+ supportedTypes: ["post", "profile", "record"],
204
225
  category: "blueskyClients",
205
226
  redirectCompat: ["bluesky-social"],
206
227
  expectedCollections: ["app.bsky."]
@@ -227,6 +248,33 @@ var WAYPOINT_DESTINATIONS_DATA = {
227
248
  },
228
249
  supportedTypes: ["post", "profile", "list", "record"],
229
250
  category: "blueskyClients",
251
+ // Impro routes /intent/compose to its home view, which opens the composer
252
+ // for a signed-in user — but it never reads `?text`, so no textParam here.
253
+ composeIntent: { url: "https://impro.social/intent/compose" },
254
+ redirectCompat: ["bluesky-social"],
255
+ expectedCollections: ["app.bsky."]
256
+ },
257
+ lea: {
258
+ id: "lea",
259
+ name: "Lea",
260
+ description: (collection) => {
261
+ if (collection === "app.bsky.feed.post") return "View post on lea.ac";
262
+ return "View profile on lea.ac";
263
+ },
264
+ getUrl: (handle, collection, rkey) => {
265
+ if (collection && rkey) {
266
+ if (collection === "app.bsky.feed.post") {
267
+ return `https://lea.ac/profile/${handle}/post/${rkey}`;
268
+ }
269
+ return `https://lea.ac/profile/${handle}`;
270
+ }
271
+ return `https://lea.ac/profile/${handle}`;
272
+ },
273
+ // No `list`: like Red Dwarf, Lea serves only `/profile/:handle` and
274
+ // `/profile/:handle/post/:rkey` — `/lists/:rkey` 404s — so claiming list
275
+ // support would drop the user on an error page instead of the list.
276
+ supportedTypes: ["post", "profile", "record"],
277
+ category: "blueskyClients",
230
278
  redirectCompat: ["bluesky-social"],
231
279
  expectedCollections: ["app.bsky."]
232
280
  },
@@ -327,12 +375,16 @@ var WAYPOINT_DESTINATIONS_DATA = {
327
375
  if (collection === "app.bsky.feed.post") {
328
376
  return `https://witchsky.app/profile/${handle}/post/${rkey}`;
329
377
  }
378
+ if (collection === "app.bsky.graph.list") {
379
+ return `https://witchsky.app/profile/${handle}/lists/${rkey}`;
380
+ }
330
381
  return `https://witchsky.app/profile/${handle}`;
331
382
  }
332
383
  return `https://witchsky.app/profile/${handle}`;
333
384
  },
334
385
  supportedTypes: ["post", "profile", "list", "record"],
335
386
  category: "blueskyForks",
387
+ composeIntent: socialAppComposeIntent("https://witchsky.app"),
336
388
  redirectCompat: ["bluesky-social"],
337
389
  expectedCollections: ["app.bsky."]
338
390
  },
@@ -349,12 +401,16 @@ var WAYPOINT_DESTINATIONS_DATA = {
349
401
  if (collection === "app.bsky.feed.post") {
350
402
  return `https://deer.social/profile/${handle}/post/${rkey}`;
351
403
  }
404
+ if (collection === "app.bsky.graph.list") {
405
+ return `https://deer.social/profile/${handle}/lists/${rkey}`;
406
+ }
352
407
  return `https://deer.social/profile/${handle}`;
353
408
  }
354
409
  return `https://deer.social/profile/${handle}`;
355
410
  },
356
411
  supportedTypes: ["post", "profile", "list", "record"],
357
412
  category: "blueskyForks",
413
+ composeIntent: socialAppComposeIntent("https://deer.social"),
358
414
  redirectCompat: ["bluesky-social"],
359
415
  expectedCollections: ["app.bsky."]
360
416
  },
@@ -371,12 +427,42 @@ var WAYPOINT_DESTINATIONS_DATA = {
371
427
  if (collection === "app.bsky.feed.post") {
372
428
  return `https://mu.social/profile/${handle}/post/${rkey}`;
373
429
  }
430
+ if (collection === "app.bsky.graph.list") {
431
+ return `https://mu.social/profile/${handle}/lists/${rkey}`;
432
+ }
374
433
  return `https://mu.social/profile/${handle}`;
375
434
  }
376
435
  return `https://mu.social/profile/${handle}`;
377
436
  },
378
437
  supportedTypes: ["post", "profile", "list", "record"],
379
438
  category: "blueskyForks",
439
+ composeIntent: socialAppComposeIntent("https://mu.social"),
440
+ redirectCompat: ["bluesky-social"],
441
+ expectedCollections: ["app.bsky."]
442
+ },
443
+ northsky: {
444
+ id: "northsky",
445
+ name: "Northsky",
446
+ description: (collection) => {
447
+ if (collection === "app.bsky.feed.post") return "View post on northsky.app";
448
+ if (collection === "app.bsky.graph.list") return "View list on northsky.app";
449
+ return "View profile on northsky.app";
450
+ },
451
+ getUrl: (handle, collection, rkey) => {
452
+ if (collection && rkey) {
453
+ if (collection === "app.bsky.feed.post") {
454
+ return `https://northsky.app/profile/${handle}/post/${rkey}`;
455
+ }
456
+ if (collection === "app.bsky.graph.list") {
457
+ return `https://northsky.app/profile/${handle}/lists/${rkey}`;
458
+ }
459
+ return `https://northsky.app/profile/${handle}`;
460
+ }
461
+ return `https://northsky.app/profile/${handle}`;
462
+ },
463
+ supportedTypes: ["post", "profile", "list", "record"],
464
+ category: "blueskyForks",
465
+ composeIntent: socialAppComposeIntent("https://northsky.app"),
380
466
  redirectCompat: ["bluesky-social"],
381
467
  expectedCollections: ["app.bsky."]
382
468
  },
@@ -655,7 +741,9 @@ var WAYPOINT_ORDER = [
655
741
  "taproot",
656
742
  "witchsky",
657
743
  "mu",
658
- "deer"
744
+ "deer",
745
+ "lea",
746
+ "northsky"
659
747
  ];
660
748
  function getWaypointDataForType(type) {
661
749
  return WAYPOINT_ORDER.map((id) => WAYPOINT_DESTINATIONS_DATA[id]).filter((waypoint) => waypoint.supportedTypes.includes(type));
@@ -822,6 +910,47 @@ function waypointActivity(waypoint, repoCollections) {
822
910
  }
823
911
  return "absent";
824
912
  }
913
+ var COMPOSE_INTENT_TEXT_PLACEHOLDER = "{text}";
914
+ function supportsComposeIntent(waypoint) {
915
+ return !!waypoint.composeIntent;
916
+ }
917
+ function getComposeIntentUrl(waypoint, text) {
918
+ const intent = waypoint.composeIntent;
919
+ if (!intent) return null;
920
+ return appendComposeText(intent.url, intent.textParam, text);
921
+ }
922
+ function getComposeIntentAppUrl(waypoint, text) {
923
+ const intent = waypoint.composeIntent;
924
+ if (!intent?.appUrl) return null;
925
+ return appendComposeText(intent.appUrl, intent.textParam, text);
926
+ }
927
+ function getComposeIntentTemplate(waypoint) {
928
+ const intent = waypoint.composeIntent;
929
+ if (!intent) return null;
930
+ if (!intent.textParam) return intent.url;
931
+ return `${intent.url}?${intent.textParam}=${COMPOSE_INTENT_TEXT_PLACEHOLDER}`;
932
+ }
933
+ function getComposeIntentWaypoints(type) {
934
+ const waypoints = type ? getWaypointDataForType(type) : WAYPOINT_ORDER.map((id) => WAYPOINT_DESTINATIONS_DATA[id]).filter(Boolean);
935
+ return waypoints.filter(supportsComposeIntent);
936
+ }
937
+ function describeComposeIntent(waypoint, text) {
938
+ const intent = waypoint.composeIntent;
939
+ if (!intent) return null;
940
+ const descriptor = {
941
+ url: getComposeIntentUrl(waypoint, text),
942
+ urlTemplate: getComposeIntentTemplate(waypoint),
943
+ textParam: intent.textParam ?? null,
944
+ prefillsText: !!intent.textParam
945
+ };
946
+ const appUrl = getComposeIntentAppUrl(waypoint, text);
947
+ if (appUrl) descriptor.appUrl = appUrl;
948
+ return descriptor;
949
+ }
950
+ function appendComposeText(base, textParam, text) {
951
+ if (!textParam || !text) return base;
952
+ return `${base}?${textParam}=${encodeURIComponent(text)}`;
953
+ }
825
954
 
826
955
  // src/upstreamFetch.ts
827
956
  var RETRY_DELAY_MS = 250;
@@ -920,8 +1049,10 @@ var BLUESKY_FAMILY = [
920
1049
  { source: "blacksky", hosts: ["blacksky.community"] },
921
1050
  { source: "reddwarf", hosts: ["reddwarf.app"] },
922
1051
  { source: "impro", hosts: ["impro.social"] },
1052
+ { source: "lea", hosts: ["lea.ac"] },
923
1053
  { source: "witchsky", hosts: ["witchsky.app"] },
924
1054
  { source: "deer", hosts: ["deer.social"] },
1055
+ { source: "northsky", hosts: ["northsky.app"] },
925
1056
  { source: "mu", hosts: ["mu.social"] },
926
1057
  { source: "anisota", hosts: ["anisota.net"], matchSubdomains: true }
927
1058
  ];
@@ -1341,7 +1472,7 @@ function matchTaproot(host, pathname) {
1341
1472
  }
1342
1473
  function matchAturi(host, parts) {
1343
1474
  if (host !== "aturi.to") return null;
1344
- const isIdentifier = (seg) => !!seg && (seg.startsWith("did:") || seg.includes("."));
1475
+ const isIdentifier = (seg2) => !!seg2 && (seg2.startsWith("did:") || seg2.includes("."));
1345
1476
  if (parts[0] === "explore" && isIdentifier(parts[1])) {
1346
1477
  const handle = parts[1];
1347
1478
  const did = handle.startsWith("did:") ? handle : void 0;
@@ -1490,18 +1621,26 @@ function buildWaypointsForParsed(parsed, options = {}) {
1490
1621
  if (DID_REQUIRED_WAYPOINTS.has(w.id) && !did) return null;
1491
1622
  const url = w.getUrl(parsed.handle, parsed.collection, parsed.rkey, did);
1492
1623
  if (!url) return null;
1493
- return { id: w.id, name: w.name, category: w.category, url };
1624
+ return {
1625
+ id: w.id,
1626
+ name: w.name,
1627
+ category: w.category,
1628
+ url,
1629
+ composeIntent: describeComposeIntent(w, options.composeText)
1630
+ };
1494
1631
  }).filter((w) => !!w);
1495
1632
  const recommendedRaw = getRecommendedWaypointsData(type, parsed.collection);
1496
1633
  const availableIds = new Set(waypoints.map((w) => w.id));
1497
1634
  const ids = recommendedRaw.waypoints.map((w) => w.id).filter((id) => id !== exclude && availableIds.has(id));
1498
1635
  return { waypoints, recommended: { ids, label: recommendedRaw.label } };
1499
1636
  }
1500
- function resolveAtUri(uri) {
1637
+ function resolveAtUri(uri, options = {}) {
1501
1638
  const match = parseAtUri(uri);
1502
1639
  if (!match) return null;
1503
1640
  const { parsed, source } = match;
1504
- const { waypoints, recommended } = buildWaypointsForParsed(parsed);
1641
+ const { waypoints, recommended } = buildWaypointsForParsed(parsed, {
1642
+ composeText: options.composeText
1643
+ });
1505
1644
  return {
1506
1645
  parsed,
1507
1646
  source,
@@ -1543,7 +1682,8 @@ async function resolveUrl(url, options = {}) {
1543
1682
  }
1544
1683
  const { waypoints, recommended } = buildWaypointsForParsed(parsed, {
1545
1684
  did,
1546
- excludeSourceId: source
1685
+ excludeSourceId: source,
1686
+ composeText: options.composeText
1547
1687
  });
1548
1688
  return {
1549
1689
  parsed: { ...parsed, did },
@@ -1566,6 +1706,7 @@ async function resolveViaApi(input, options = {}) {
1566
1706
  throw new Error("resolveViaApi requires either `url` or `atUri`");
1567
1707
  }
1568
1708
  if (input.headDetect === false) params.set("headDetect", "false");
1709
+ if (input.composeText) params.set("composeText", input.composeText);
1569
1710
  const res = await fetchImpl(`${endpoint}?${params.toString()}`, {
1570
1711
  method: "GET",
1571
1712
  headers: { Accept: "application/json" },
@@ -1605,30 +1746,264 @@ async function detectAtUriInHead(url, timeoutMs) {
1605
1746
  }
1606
1747
  }
1607
1748
 
1749
+ // src/universalLinks.ts
1750
+ var UNIVERSAL_LINK_ORIGIN = "https://aturi.to";
1751
+ var OEMBED_PATH = "/api/oembed";
1752
+ var OEMBED_COLLECTION = "app.bsky.feed.post";
1753
+ var OEMBED_LINK_TYPE = "application/json+oembed";
1754
+ function buildUniversalLink(input, options = {}) {
1755
+ const parsed = parseTarget(input);
1756
+ if (!parsed) return null;
1757
+ const origin = normalizeOrigin(options.origin);
1758
+ const did = options.did ?? parsed.did;
1759
+ const { collection, rkey } = parsed;
1760
+ const identifier = options.preferDid && did ? did : parsed.handle;
1761
+ let path;
1762
+ if (collection && rkey) {
1763
+ if (collection === "app.bsky.feed.post") {
1764
+ path = `/profile/${seg(identifier)}/post/${seg(rkey)}`;
1765
+ } else if (collection === "app.bsky.graph.list") {
1766
+ path = `/profile/${seg(identifier)}/lists/${seg(rkey)}`;
1767
+ } else {
1768
+ path = `/profile/${seg(did || parsed.handle)}/${seg(collection)}/${seg(rkey)}`;
1769
+ }
1770
+ } else {
1771
+ path = `/profile/${seg(identifier)}`;
1772
+ }
1773
+ return `${origin}${path}${queryString(options.params)}`;
1774
+ }
1775
+ function parseUniversalLink(url, options = {}) {
1776
+ let target;
1777
+ try {
1778
+ target = typeof url === "string" ? new URL(url.trim()) : url;
1779
+ } catch {
1780
+ return null;
1781
+ }
1782
+ if (!/^https?:$/.test(target.protocol)) return null;
1783
+ let expectedHost;
1784
+ try {
1785
+ expectedHost = bareHost(new URL(normalizeOrigin(options.origin)).hostname);
1786
+ } catch {
1787
+ return null;
1788
+ }
1789
+ if (bareHost(target.hostname) !== expectedHost) return null;
1790
+ const probe = new URL(target.toString());
1791
+ probe.protocol = "https:";
1792
+ probe.hostname = "aturi.to";
1793
+ probe.port = "";
1794
+ const match = matchSupportedUrl(probe);
1795
+ if (match && (match.source === "aturi" || match.source === "aturiExplore")) {
1796
+ return normalizeParsed(match.parsed);
1797
+ }
1798
+ return parseEmbeddedAtUri(probe.pathname) ?? parsePath(probe.pathname);
1799
+ }
1800
+ function isUniversalLink(url, options = {}) {
1801
+ return parseUniversalLink(url, options) !== null;
1802
+ }
1803
+ function describeUniversalLink(input, options = {}) {
1804
+ const parsed = parseTarget(input);
1805
+ if (!parsed) return null;
1806
+ const url = buildUniversalLink(parsed, options);
1807
+ if (!url) return null;
1808
+ const did = options.did ?? parsed.did ?? null;
1809
+ const collection = parsed.collection ?? null;
1810
+ const rkey = parsed.rkey ?? null;
1811
+ const authority = did ?? parsed.handle;
1812
+ const atUri = collection && rkey ? `at://${authority}/${collection}/${rkey}` : `at://${authority}`;
1813
+ const label = describeTarget(parsed);
1814
+ return {
1815
+ url,
1816
+ atUri,
1817
+ type: parsed.type,
1818
+ handle: parsed.handle,
1819
+ did,
1820
+ collection,
1821
+ rkey,
1822
+ label,
1823
+ share: {
1824
+ title: options.title ?? label,
1825
+ text: options.text ?? label,
1826
+ url
1827
+ },
1828
+ snippets: {
1829
+ url,
1830
+ atUri,
1831
+ markdown: `[${label}](${url})`,
1832
+ html: `<a href="${escapeHtml(url)}">${escapeHtml(label)}</a>`
1833
+ },
1834
+ oembedUrl: buildOEmbedUrl(url, collection, options.origin)
1835
+ };
1836
+ }
1837
+ function buildUniversalLinkTags(input, options = {}) {
1838
+ const described = describeUniversalLink(input, options);
1839
+ if (!described) return null;
1840
+ const meta = [
1841
+ { name: "at:canonical", content: described.atUri }
1842
+ ];
1843
+ if (described.collection && described.rkey) {
1844
+ meta.push({
1845
+ name: "at:author",
1846
+ content: `at://${described.did ?? described.handle}`
1847
+ });
1848
+ }
1849
+ const link = [
1850
+ { rel: "alternate", href: described.atUri }
1851
+ ];
1852
+ if (described.oembedUrl) {
1853
+ link.push({
1854
+ rel: "alternate",
1855
+ type: OEMBED_LINK_TYPE,
1856
+ href: described.oembedUrl
1857
+ });
1858
+ }
1859
+ const html = [
1860
+ ...meta.map(
1861
+ (tag) => `<meta name="${escapeHtml(tag.name)}" content="${escapeHtml(tag.content)}" />`
1862
+ ),
1863
+ ...link.map((tag) => {
1864
+ const type = tag.type ? ` type="${escapeHtml(tag.type)}"` : "";
1865
+ return `<link rel="${escapeHtml(tag.rel)}"${type} href="${escapeHtml(tag.href)}" />`;
1866
+ })
1867
+ ].join("\n");
1868
+ return { meta, link, html };
1869
+ }
1870
+ function parseTarget(input) {
1871
+ if (typeof input !== "string") {
1872
+ return input && input.handle ? normalizeParsed(input) : null;
1873
+ }
1874
+ const trimmed = input.trim();
1875
+ if (!trimmed) return null;
1876
+ if (trimmed.toLowerCase().startsWith("at://")) {
1877
+ const parsed = parseAtUri(trimmed)?.parsed;
1878
+ return parsed ? normalizeParsed(parsed) : null;
1879
+ }
1880
+ if (/^https?:\/\//i.test(trimmed)) {
1881
+ let target;
1882
+ try {
1883
+ target = new URL(trimmed);
1884
+ } catch {
1885
+ return null;
1886
+ }
1887
+ const embedded = parseEmbeddedAtUri(target.pathname);
1888
+ if (embedded) return embedded;
1889
+ const matched = matchSupportedUrl(target)?.parsed;
1890
+ return matched ? normalizeParsed(matched) : null;
1891
+ }
1892
+ return parsePath(trimmed.startsWith("@") ? trimmed.slice(1) : trimmed);
1893
+ }
1894
+ function parsePath(path) {
1895
+ const segments = path.split("/").filter(Boolean);
1896
+ const handle = decodeSegment(segments[0] ?? "");
1897
+ if (!handle) return null;
1898
+ if (!handle.startsWith("did:") && !handle.includes(".")) return null;
1899
+ const parsed = parseAtUri(`at://${segments.join("/")}`)?.parsed;
1900
+ if (!parsed) return null;
1901
+ const collection = parsed.collection ? decodeSegment(parsed.collection) : void 0;
1902
+ const rkey = parsed.rkey ? decodeSegment(parsed.rkey) : void 0;
1903
+ return {
1904
+ ...parsed,
1905
+ handle,
1906
+ did: handle.startsWith("did:") ? handle : void 0,
1907
+ collection,
1908
+ rkey,
1909
+ uri: collection && rkey ? `at://${handle}/${collection}/${rkey}` : `at://${handle}`
1910
+ };
1911
+ }
1912
+ function normalizeParsed(parsed) {
1913
+ if (!parsed.handle.startsWith("@")) return parsed;
1914
+ const handle = parsed.handle.slice(1);
1915
+ if (!handle) return parsed;
1916
+ return {
1917
+ ...parsed,
1918
+ handle,
1919
+ uri: parsed.collection && parsed.rkey ? `at://${handle}/${parsed.collection}/${parsed.rkey}` : `at://${handle}`
1920
+ };
1921
+ }
1922
+ function parseEmbeddedAtUri(pathname) {
1923
+ const match = /^\/at:\/{1,2}(.+)$/i.exec(pathname);
1924
+ if (!match) return null;
1925
+ return parsePath(match[1]);
1926
+ }
1927
+ function buildOEmbedUrl(url, collection, origin) {
1928
+ if (collection !== OEMBED_COLLECTION) return null;
1929
+ return `${normalizeOrigin(origin)}${OEMBED_PATH}?url=${encodeURIComponent(url)}`;
1930
+ }
1931
+ function describeTarget(parsed) {
1932
+ const who = parsed.handle.startsWith("did:") ? parsed.handle : `@${parsed.handle}`;
1933
+ if (parsed.type === "post") return `Post by ${who}`;
1934
+ if (parsed.type === "list") return `List by ${who}`;
1935
+ if (parsed.collection) return `${parsed.collection} record by ${who}`;
1936
+ return who;
1937
+ }
1938
+ function normalizeOrigin(origin) {
1939
+ if (!origin) return UNIVERSAL_LINK_ORIGIN;
1940
+ return origin.trim().replace(/\/+$/, "") || UNIVERSAL_LINK_ORIGIN;
1941
+ }
1942
+ function bareHost(hostname) {
1943
+ return hostname.toLowerCase().replace(/^www\./, "");
1944
+ }
1945
+ function seg(value) {
1946
+ return encodeURIComponent(value).replace(/%3A/gi, ":");
1947
+ }
1948
+ function decodeSegment(value) {
1949
+ try {
1950
+ return decodeURIComponent(value);
1951
+ } catch {
1952
+ return value;
1953
+ }
1954
+ }
1955
+ function queryString(params) {
1956
+ if (!params) return "";
1957
+ const search = new URLSearchParams();
1958
+ for (const [key, value] of Object.entries(params)) {
1959
+ if (value === null || value === void 0 || value === "") continue;
1960
+ search.set(key, String(value));
1961
+ }
1962
+ const query = search.toString();
1963
+ return query ? `?${query}` : "";
1964
+ }
1965
+ function escapeHtml(value) {
1966
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1967
+ }
1968
+
1608
1969
  exports.CATEGORY_ORDER = CATEGORY_ORDER;
1609
1970
  exports.COMPAT_FAMILIES = COMPAT_FAMILIES;
1610
1971
  exports.COMPAT_FAMILY_ORDER = COMPAT_FAMILY_ORDER;
1972
+ exports.COMPOSE_INTENT_TEXT_PLACEHOLDER = COMPOSE_INTENT_TEXT_PLACEHOLDER;
1611
1973
  exports.DID_REQUIRED_WAYPOINTS = DID_REQUIRED_WAYPOINTS;
1974
+ exports.OEMBED_LINK_TYPE = OEMBED_LINK_TYPE;
1612
1975
  exports.SUPPORTED_HOSTS = SUPPORTED_HOSTS;
1976
+ exports.UNIVERSAL_LINK_ORIGIN = UNIVERSAL_LINK_ORIGIN;
1613
1977
  exports.WAYPOINT_CATEGORIES_DATA = WAYPOINT_CATEGORIES_DATA;
1614
1978
  exports.WAYPOINT_DESTINATIONS_DATA = WAYPOINT_DESTINATIONS_DATA;
1615
1979
  exports.WAYPOINT_ORDER = WAYPOINT_ORDER;
1980
+ exports.buildUniversalLink = buildUniversalLink;
1981
+ exports.buildUniversalLinkTags = buildUniversalLinkTags;
1616
1982
  exports.buildWaypointsForParsed = buildWaypointsForParsed;
1983
+ exports.describeComposeIntent = describeComposeIntent;
1984
+ exports.describeUniversalLink = describeUniversalLink;
1617
1985
  exports.getCategorizedWaypointsData = getCategorizedWaypointsData;
1986
+ exports.getComposeIntentAppUrl = getComposeIntentAppUrl;
1987
+ exports.getComposeIntentTemplate = getComposeIntentTemplate;
1988
+ exports.getComposeIntentUrl = getComposeIntentUrl;
1989
+ exports.getComposeIntentWaypoints = getComposeIntentWaypoints;
1618
1990
  exports.getDisplayName = getDisplayName;
1619
1991
  exports.getFeaturedWaypointData = getFeaturedWaypointData;
1620
1992
  exports.getRecommendedWaypointsData = getRecommendedWaypointsData;
1621
1993
  exports.getWaypointCountData = getWaypointCountData;
1622
1994
  exports.getWaypointDataForType = getWaypointDataForType;
1623
1995
  exports.isSupportedHost = isSupportedHost;
1996
+ exports.isUniversalLink = isUniversalLink;
1624
1997
  exports.matchSupportedUrl = matchSupportedUrl;
1625
1998
  exports.parseAtUri = parseAtUri;
1626
1999
  exports.parseURI = parseURI;
2000
+ exports.parseUniversalLink = parseUniversalLink;
1627
2001
  exports.resolveAtUri = resolveAtUri;
1628
2002
  exports.resolveHandle = resolveHandle;
1629
2003
  exports.resolveHandleStatus = resolveHandleStatus;
1630
2004
  exports.resolveUrl = resolveUrl;
1631
2005
  exports.resolveViaApi = resolveViaApi;
2006
+ exports.supportsComposeIntent = supportsComposeIntent;
1632
2007
  exports.waypointActivity = waypointActivity;
1633
2008
  //# sourceMappingURL=index.cjs.map
1634
2009
  //# sourceMappingURL=index.cjs.map