@openrails/contentkit-ui 0.83.0 → 0.86.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.
Files changed (219) hide show
  1. package/README.md +18 -7
  2. package/dist/client/beacons.d.ts +18 -6
  3. package/dist/client/beacons.js +26 -11
  4. package/dist/client/beacons.js.map +1 -1
  5. package/dist/client/client.d.ts +3 -0
  6. package/dist/client/client.js +29 -27
  7. package/dist/client/client.js.map +1 -1
  8. package/dist/client/codes.d.ts +6 -1
  9. package/dist/client/codes.js +4 -1
  10. package/dist/client/codes.js.map +1 -1
  11. package/dist/client/content/moderation.d.ts +14 -2
  12. package/dist/client/content/moderation.js +34 -0
  13. package/dist/client/content/moderation.js.map +1 -1
  14. package/dist/client/content/types.d.ts +13 -0
  15. package/dist/client/content/types.js.map +1 -1
  16. package/dist/client/discovery.d.ts +6 -0
  17. package/dist/client/discovery.js.map +1 -1
  18. package/dist/client/generated/error-codes.d.ts +1 -1
  19. package/dist/client/generated/error-codes.js +9 -1
  20. package/dist/client/generated/error-codes.js.map +1 -1
  21. package/dist/client/generated/routes.d.ts +91 -13
  22. package/dist/client/generated/routes.js +123 -2
  23. package/dist/client/generated/routes.js.map +1 -1
  24. package/dist/client/generated/wire.d.ts +81 -0
  25. package/dist/client/generated/wire.js.map +1 -1
  26. package/dist/client/index.d.ts +4 -2
  27. package/dist/client/media/queue.d.ts +4 -2
  28. package/dist/client/media/queue.js +18 -12
  29. package/dist/client/media/queue.js.map +1 -1
  30. package/dist/client/media/rules.d.ts +7 -1
  31. package/dist/client/media/rules.js +27 -21
  32. package/dist/client/media/rules.js.map +1 -1
  33. package/dist/client/reader-meta.d.ts +27 -0
  34. package/dist/client/reader-meta.js +99 -0
  35. package/dist/client/reader-meta.js.map +1 -0
  36. package/dist/client/stats.d.ts +32 -0
  37. package/dist/client/stats.js +35 -0
  38. package/dist/client/stats.js.map +1 -0
  39. package/dist/client/versions.d.ts +1 -1
  40. package/dist/client/versions.js +7 -2
  41. package/dist/client/versions.js.map +1 -1
  42. package/dist/client.js +30 -28
  43. package/dist/components/admin/content-manager.js +206 -206
  44. package/dist/components/admin/content-manager.js.map +1 -1
  45. package/dist/components/admin/group-manager.js +71 -70
  46. package/dist/components/admin/group-manager.js.map +1 -1
  47. package/dist/components/admin/media-item-panel.d.ts +24 -7
  48. package/dist/components/admin/media-item-panel.js +209 -197
  49. package/dist/components/admin/media-item-panel.js.map +1 -1
  50. package/dist/components/admin/poll-manager.js +25 -25
  51. package/dist/components/admin/reader-manifest.d.ts +18 -0
  52. package/dist/components/admin/reader-manifest.js +490 -0
  53. package/dist/components/admin/reader-manifest.js.map +1 -0
  54. package/dist/components/admin/stats-dashboard.d.ts +22 -0
  55. package/dist/components/admin/stats-dashboard.js +257 -0
  56. package/dist/components/admin/stats-dashboard.js.map +1 -0
  57. package/dist/components/content/card.js +43 -43
  58. package/dist/components/content/card.js.map +1 -1
  59. package/dist/components/content/editors.d.ts +2 -1
  60. package/dist/components/content/editors.js +479 -416
  61. package/dist/components/content/editors.js.map +1 -1
  62. package/dist/components/content/grid.js +47 -47
  63. package/dist/components/content/index.d.ts +2 -1
  64. package/dist/components/content/listing.js +34 -34
  65. package/dist/components/content/listing.js.map +1 -1
  66. package/dist/components/content/paywall.d.ts +17 -5
  67. package/dist/components/content/paywall.js +115 -100
  68. package/dist/components/content/paywall.js.map +1 -1
  69. package/dist/components/content/reader-stats.d.ts +12 -0
  70. package/dist/components/content/reader-stats.js +47 -0
  71. package/dist/components/content/reader-stats.js.map +1 -0
  72. package/dist/components/groups/banner.d.ts +1 -1
  73. package/dist/components/groups/banner.js +34 -34
  74. package/dist/components/groups/banner.js.map +1 -1
  75. package/dist/components/groups/directory.js +11 -11
  76. package/dist/components/groups/editor.js +294 -294
  77. package/dist/components/groups/editor.js.map +1 -1
  78. package/dist/components/groups/header.js +27 -27
  79. package/dist/components/groups/header.js.map +1 -1
  80. package/dist/components/groups/memberships.js +180 -180
  81. package/dist/components/groups/parts.d.ts +2 -4
  82. package/dist/components/groups/parts.js +69 -88
  83. package/dist/components/groups/parts.js.map +1 -1
  84. package/dist/components/media-folder-editor.js +239 -189
  85. package/dist/components/media-folder-editor.js.map +1 -1
  86. package/dist/components/notifications.js +17 -17
  87. package/dist/components/slot-upload.d.ts +2 -2
  88. package/dist/components/slot-upload.js +84 -79
  89. package/dist/components/slot-upload.js.map +1 -1
  90. package/dist/components/social/bans.js +34 -34
  91. package/dist/components/social/comments.js +59 -45
  92. package/dist/components/social/comments.js.map +1 -1
  93. package/dist/components/social/index.d.ts +1 -0
  94. package/dist/components/social/moderation.d.ts +5 -4
  95. package/dist/components/social/moderation.js +177 -160
  96. package/dist/components/social/moderation.js.map +1 -1
  97. package/dist/components/social/parts.js +22 -14
  98. package/dist/components/social/parts.js.map +1 -1
  99. package/dist/components/social/rules.d.ts +23 -0
  100. package/dist/components/social/rules.js +261 -0
  101. package/dist/components/social/rules.js.map +1 -0
  102. package/dist/components/video-player.d.ts +2 -0
  103. package/dist/components/video-player.js +318 -314
  104. package/dist/components/video-player.js.map +1 -1
  105. package/dist/i18n/context.d.ts +2 -0
  106. package/dist/i18n/context.js +23 -5
  107. package/dist/i18n/context.js.map +1 -1
  108. package/dist/index.d.ts +3 -1
  109. package/dist/index.js +28 -24
  110. package/dist/locales/admin/de.js +105 -0
  111. package/dist/locales/admin/de.js.map +1 -1
  112. package/dist/locales/admin/en.d.ts +105 -0
  113. package/dist/locales/admin/en.js +105 -0
  114. package/dist/locales/admin/en.js.map +1 -1
  115. package/dist/locales/admin/es.js +105 -0
  116. package/dist/locales/admin/es.js.map +1 -1
  117. package/dist/locales/admin/ja.js +105 -0
  118. package/dist/locales/admin/ja.js.map +1 -1
  119. package/dist/locales/admin/ko.js +105 -0
  120. package/dist/locales/admin/ko.js.map +1 -1
  121. package/dist/locales/admin/zh.js +105 -0
  122. package/dist/locales/admin/zh.js.map +1 -1
  123. package/dist/locales/content/de.js +6 -0
  124. package/dist/locales/content/de.js.map +1 -1
  125. package/dist/locales/content/en.d.ts +6 -0
  126. package/dist/locales/content/en.js +6 -0
  127. package/dist/locales/content/en.js.map +1 -1
  128. package/dist/locales/content/es.js +6 -0
  129. package/dist/locales/content/es.js.map +1 -1
  130. package/dist/locales/content/ja.js +6 -0
  131. package/dist/locales/content/ja.js.map +1 -1
  132. package/dist/locales/content/ko.js +6 -0
  133. package/dist/locales/content/ko.js.map +1 -1
  134. package/dist/locales/content/zh.js +6 -0
  135. package/dist/locales/content/zh.js.map +1 -1
  136. package/dist/locales/de.js +4 -1
  137. package/dist/locales/de.js.map +1 -1
  138. package/dist/locales/discovery/de.js +1 -0
  139. package/dist/locales/discovery/de.js.map +1 -1
  140. package/dist/locales/discovery/en.d.ts +1 -0
  141. package/dist/locales/discovery/en.js +1 -0
  142. package/dist/locales/discovery/en.js.map +1 -1
  143. package/dist/locales/discovery/es.js +1 -0
  144. package/dist/locales/discovery/es.js.map +1 -1
  145. package/dist/locales/discovery/ja.js +1 -0
  146. package/dist/locales/discovery/ja.js.map +1 -1
  147. package/dist/locales/discovery/ko.js +1 -0
  148. package/dist/locales/discovery/ko.js.map +1 -1
  149. package/dist/locales/discovery/zh.js +1 -0
  150. package/dist/locales/discovery/zh.js.map +1 -1
  151. package/dist/locales/en.d.ts +118 -0
  152. package/dist/locales/en.js +4 -1
  153. package/dist/locales/en.js.map +1 -1
  154. package/dist/locales/es.js +4 -1
  155. package/dist/locales/es.js.map +1 -1
  156. package/dist/locales/ja.js +4 -1
  157. package/dist/locales/ja.js.map +1 -1
  158. package/dist/locales/ko.js +4 -1
  159. package/dist/locales/ko.js.map +1 -1
  160. package/dist/locales/social/de.js +3 -0
  161. package/dist/locales/social/de.js.map +1 -1
  162. package/dist/locales/social/en.d.ts +3 -0
  163. package/dist/locales/social/en.js +3 -0
  164. package/dist/locales/social/en.js.map +1 -1
  165. package/dist/locales/social/es.js +3 -0
  166. package/dist/locales/social/es.js.map +1 -1
  167. package/dist/locales/social/ja.js +3 -0
  168. package/dist/locales/social/ja.js.map +1 -1
  169. package/dist/locales/social/ko.js +3 -0
  170. package/dist/locales/social/ko.js.map +1 -1
  171. package/dist/locales/social/zh.js +3 -0
  172. package/dist/locales/social/zh.js.map +1 -1
  173. package/dist/locales/zh.js +4 -1
  174. package/dist/locales/zh.js.map +1 -1
  175. package/dist/react/admin.d.ts +1 -1
  176. package/dist/react/admin.js +24 -23
  177. package/dist/react/admin.js.map +1 -1
  178. package/dist/react/canonical.d.ts +4 -3
  179. package/dist/react/canonical.js +34 -33
  180. package/dist/react/canonical.js.map +1 -1
  181. package/dist/react/comments.d.ts +29 -1
  182. package/dist/react/comments.js +30 -2
  183. package/dist/react/comments.js.map +1 -1
  184. package/dist/react/contents.js +144 -144
  185. package/dist/react/contents.js.map +1 -1
  186. package/dist/react/context.d.ts +1 -1
  187. package/dist/react/context.js.map +1 -1
  188. package/dist/react/detail.d.ts +12 -6
  189. package/dist/react/detail.js +31 -24
  190. package/dist/react/detail.js.map +1 -1
  191. package/dist/react/discovery.js +1 -1
  192. package/dist/react/discovery.js.map +1 -1
  193. package/dist/react/folder.d.ts +7 -2
  194. package/dist/react/folder.js +165 -147
  195. package/dist/react/folder.js.map +1 -1
  196. package/dist/react/groups.d.ts +0 -1
  197. package/dist/react/groups.js +13 -12
  198. package/dist/react/groups.js.map +1 -1
  199. package/dist/react/index.d.ts +5 -3
  200. package/dist/react/location.d.ts +11 -0
  201. package/dist/react/location.js +18 -0
  202. package/dist/react/location.js.map +1 -0
  203. package/dist/react/public.d.ts +8 -0
  204. package/dist/react/public.js +32 -27
  205. package/dist/react/public.js.map +1 -1
  206. package/dist/react/resources.js +6 -0
  207. package/dist/react/resources.js.map +1 -1
  208. package/dist/react/stats.d.ts +24 -0
  209. package/dist/react/stats.js +28 -0
  210. package/dist/react/stats.js.map +1 -0
  211. package/dist/react/use-resource.js +8 -7
  212. package/dist/react/use-resource.js.map +1 -1
  213. package/dist/react/versions.js +20 -20
  214. package/dist/react.js +26 -24
  215. package/dist/styles.css +1 -1
  216. package/dist/urls/index.d.ts +35 -10
  217. package/dist/urls.js +100 -70
  218. package/dist/urls.js.map +1 -1
  219. package/package.json +1 -1
@@ -7,6 +7,15 @@ export declare const CODE_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
7
7
  * not a code. An all-digit result is never a code (it reads as a legacy id).
8
8
  */
9
9
  export declare function parseCode(input: string): string | null;
10
+ /** The query parameter that selects a version on its content's page. */
11
+ export declare const VERSION_PARAM = "v";
12
+ export declare const VERSION_CODE_LENGTH = 4;
13
+ /** Reads a version code (four characters, parseCode's rules); null when the input is not one. */
14
+ export declare function parseVersionCode(input: string): string | null;
15
+ /** The version code a location's (or search string's) ?v= names; null when it names none. */
16
+ export declare function versionParam(location: string | URLSearchParams): string | null;
17
+ /** location with ?v= set to code, or removed for null; its other parameters and hash stay. */
18
+ export declare function withVersion(location: string, code: string | null): string;
10
19
  /** Reports whether input is a code in canonical form. */
11
20
  export declare function isCode(input: string): boolean;
12
21
  /** A resolved content link, as ContentKit and host APIs return it. */
@@ -21,18 +30,29 @@ export interface ContentLink {
21
30
  export declare function slugFor(link: Pick<ContentLink, "slug" | "slugs">, language?: string): string;
22
31
  export type QueryInit = string | URLSearchParams | Record<string, string | number | boolean | null | undefined>;
23
32
  export interface PathOptions {
24
- /** Language prefix segment; omitted when empty. */
33
+ /** The page's language; ContentURLs use the site's default when unset. */
25
34
  language?: string;
26
35
  /** Query string for sub-pages, e.g. { p: 12 }. Null and undefined values are dropped. */
27
36
  query?: QueryInit;
37
+ /** A version code (Version.code): ?v= selects that version on the page. */
38
+ version?: string | null;
28
39
  }
29
- /** Joins [/{language}]/{route}/{code}[/{slug}][?query]. */
40
+ /**
41
+ * The language of a page asked for in language on a site of languages, its
42
+ * default first: language, else the default. Every content page is under its
43
+ * language. Mirrors Go's contenturl.Prefix.
44
+ */
45
+ export declare function languagePrefix(languages: readonly string[], language?: string): string;
46
+ /** Joins /{language}/{route}/{code}[/{slug}][?query]; ContentURLs.path() picks the language. */
30
47
  export declare function contentPath(route: string, code: string, slug?: string, options?: PathOptions): string;
31
48
  export interface ContentURLConfig {
32
49
  /** Content kind -> the host's route segment ({ video: "watch", gallery: "g" }). */
33
50
  routes: Record<string, string>;
34
- /** Language segments a path may start with; kept in canonical paths. */
35
- languages?: string[];
51
+ /**
52
+ * The site's languages, its default first (the content model's). Required:
53
+ * every content path starts with one; an unprefixed location is not canonical.
54
+ */
55
+ languages: string[];
36
56
  /** The site's origin ("https://example.com") for absolute URLs (canonical links, og:url). */
37
57
  origin?: string;
38
58
  }
@@ -54,10 +74,14 @@ export interface CanonicalResult {
54
74
  /** The location's path is not canonical: replace it with location. */
55
75
  redirect: boolean;
56
76
  }
77
+ export interface CanonicalOptions {
78
+ /** The language an unprefixed location redirects to (the reader's); default the site's default. */
79
+ language?: string;
80
+ }
57
81
  export interface HreflangOptions {
58
82
  /** Languages the content exists in; default every configured language. */
59
83
  languages?: readonly string[];
60
- /** The language x-default points at. */
84
+ /** The language x-default points at, listed or not; default the site's default language. */
61
85
  defaultLanguage?: string;
62
86
  /** The origin for absolute URLs; default the configured one (throws without either). */
63
87
  origin?: string;
@@ -67,7 +91,7 @@ export interface ContentURLs {
67
91
  readonly languages: readonly string[];
68
92
  /** The configured origin, without a trailing slash. */
69
93
  readonly origin?: string;
70
- /** The canonical path of link; throws when its kind has no route. */
94
+ /** link's page in a language (the default when unset), with ?v= for a version; throws when its kind has no route. */
71
95
  path(link: ContentLink, options?: PathOptions): string;
72
96
  /** path() on the configured origin; throws without one. */
73
97
  url(link: ContentLink, options?: PathOptions): string;
@@ -76,13 +100,14 @@ export interface ContentURLs {
76
100
  /**
77
101
  * Compares a location (pathname plus optional ?query and #hash) with link's
78
102
  * canonical path, for a client-side history.replaceState after the page's
79
- * data arrives. null when the location is not a content path or link's kind
80
- * has no route.
103
+ * data arrives; the query (?v= included) stays. null when the location is
104
+ * not a content path or link's kind has no route.
81
105
  */
82
- canonical(location: string, link: ContentLink): CanonicalResult | null;
106
+ canonical(location: string, link: ContentLink, options?: CanonicalOptions): CanonicalResult | null;
83
107
  /**
84
108
  * hreflang alternates: each language's absolute URL of link (its own
85
- * slug), plus x-default; empty when no language is configured.
109
+ * slug), plus x-default, the default language's. Never a version's: a
110
+ * version's canonical URL is its language page.
86
111
  */
87
112
  hreflang(link: ContentLink, options?: HreflangOptions): Record<string, string>;
88
113
  }
package/dist/urls.js CHANGED
@@ -1,37 +1,62 @@
1
1
  //#region src/urls/index.ts
2
2
  var e = 9, t = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
3
- function n(e) {
4
- let t = "";
5
- for (let n of e) {
6
- if (n === "-") continue;
7
- if (n.length !== 1 || n.charCodeAt(0) > 127) return null;
8
- let e = n.toUpperCase();
9
- if (e === "O" ? e = "0" : (e === "I" || e === "L") && (e = "1"), !"0123456789ABCDEFGHJKMNPQRSTVWXYZ".includes(e) || t.length === 9) return null;
10
- t += e;
3
+ function n(e, t) {
4
+ let n = "";
5
+ for (let r of e) {
6
+ if (r === "-") continue;
7
+ if (r.length !== 1 || r.charCodeAt(0) > 127) return null;
8
+ let e = r.toUpperCase();
9
+ if (e === "O" ? e = "0" : (e === "I" || e === "L") && (e = "1"), !"0123456789ABCDEFGHJKMNPQRSTVWXYZ".includes(e) || n.length === t) return null;
10
+ n += e;
11
11
  }
12
- return t.length !== 9 || !/[A-Z]/.test(t) ? null : t;
12
+ return n.length !== t || !/[A-Z]/.test(n) ? null : n;
13
13
  }
14
14
  function r(e) {
15
- return n(e) === e;
15
+ return n(e, 9);
16
16
  }
17
- function i(e, t = "") {
17
+ var i = "v", a = 4;
18
+ function o(e) {
19
+ return n(e, 4);
20
+ }
21
+ function s(e) {
22
+ let t = (typeof e == "string" ? c(e) : e).get("v");
23
+ return t === null ? null : o(t);
24
+ }
25
+ function c(e) {
26
+ let t = e.split("#", 1)[0], n = t.indexOf("?");
27
+ return new URLSearchParams(n < 0 ? "" : t.slice(n + 1));
28
+ }
29
+ function l(e, t) {
30
+ let n = e.indexOf("#"), r = n < 0 ? "" : e.slice(n), i = n < 0 ? e : e.slice(0, n), a = i.indexOf("?"), o = new URLSearchParams(a < 0 ? "" : i.slice(a + 1));
31
+ t === null ? o.delete("v") : o.set("v", t);
32
+ let s = o.toString();
33
+ return (a < 0 ? i : i.slice(0, a)) + (s ? `?${s}` : "") + r;
34
+ }
35
+ function u(e) {
36
+ return r(e) === e;
37
+ }
38
+ function d(e, t = "") {
18
39
  return t && e.slugs?.[t] || e.slug;
19
40
  }
20
- function a(e) {
21
- if (e === void 0) return "";
22
- let t;
23
- if (typeof e == "string" || e instanceof URLSearchParams) t = new URLSearchParams(e);
41
+ function f(e, t) {
42
+ let n;
43
+ if (e === void 0) n = new URLSearchParams();
44
+ else if (typeof e == "string" || e instanceof URLSearchParams) n = new URLSearchParams(e);
24
45
  else {
25
- t = new URLSearchParams();
26
- for (let [n, r] of Object.entries(e)) r != null && t.append(n, String(r));
46
+ n = new URLSearchParams();
47
+ for (let [t, r] of Object.entries(e)) r != null && n.append(t, String(r));
27
48
  }
28
- let n = t.toString();
29
- return n ? `?${n}` : "";
49
+ t && n.set("v", t);
50
+ let r = n.toString();
51
+ return r ? `?${r}` : "";
52
+ }
53
+ function p(e, t = "") {
54
+ return t || e[0] || "";
30
55
  }
31
- function o(e, t, n = "", r = {}) {
32
- return `${r.language ? `/${r.language}` : ""}/${e}/${t}${n ? `/${n}` : ""}${a(r.query)}`;
56
+ function m(e, t, n = "", r = {}) {
57
+ return `${r.language ? `/${r.language}` : ""}/${e}/${t}${n ? `/${n}` : ""}${f(r.query, r.version)}`;
33
58
  }
34
- var s = [
59
+ var h = [
35
60
  "gclid",
36
61
  "gbraid",
37
62
  "wbraid",
@@ -46,73 +71,78 @@ var s = [
46
71
  "_ga",
47
72
  "_gl"
48
73
  ];
49
- function c(e, t = {}) {
50
- let n = new URL(e), r = /* @__PURE__ */ new Set([...s, ...t.drop ?? []]);
74
+ function g(e, t = {}) {
75
+ let n = new URL(e), r = /* @__PURE__ */ new Set([...h, ...t.drop ?? []]);
51
76
  for (let e of [...n.searchParams.keys()]) (e.startsWith("utm_") || r.has(e)) && n.searchParams.delete(e);
52
77
  return n.hash = "", n.pathname.length > 1 && n.pathname.endsWith("/") && (n.pathname = n.pathname.replace(/\/+$/, "")), n.toString();
53
78
  }
54
- var l = /^[a-z0-9][a-z0-9_-]*$/;
55
- function u(e) {
56
- let t = { ...e.routes }, r = e.origin?.replace(/\/+$/, "");
57
- if (r !== void 0 && !/^https?:\/\/[^/?#]+$/.test(r)) throw Error(`contentkit-ui/urls: origin ${JSON.stringify(e.origin)} is not an http(s) origin`);
58
- let a = [...e.languages ?? []], s = new Set(Object.values(t));
59
- if (s.size === 0) throw Error("contentkit-ui/urls: routes is empty");
60
- for (let e of [...s, ...a]) if (!l.test(e)) throw Error(`contentkit-ui/urls: ${JSON.stringify(e)} is not one lowercase path segment`);
61
- for (let e of a) if (s.has(e)) throw Error(`contentkit-ui/urls: language ${JSON.stringify(e)} is also a route`);
62
- let c = (e, n) => {
63
- let r = t[e.content_kind];
64
- return r === void 0 ? null : o(r, e.code, i(e, n), { language: n });
65
- }, u = (e) => {
79
+ var _ = /^[a-z0-9][a-z0-9_-]*$/;
80
+ function v(e) {
81
+ let t = { ...e.routes }, n = e.origin?.replace(/\/+$/, "");
82
+ if (n !== void 0 && !/^https?:\/\/[^/?#]+$/.test(n)) throw Error(`contentkit-ui/urls: origin ${JSON.stringify(e.origin)} is not an http(s) origin`);
83
+ let i = [...e.languages ?? []], a = new Set(Object.values(t));
84
+ if (a.size === 0) throw Error("contentkit-ui/urls: routes is empty");
85
+ if (i.length === 0) throw Error("contentkit-ui/urls: languages is empty");
86
+ for (let e of [...a, ...i]) if (!_.test(e)) throw Error(`contentkit-ui/urls: ${JSON.stringify(e)} is not one lowercase path segment`);
87
+ for (let e of i) if (a.has(e)) throw Error(`contentkit-ui/urls: language ${JSON.stringify(e)} is also a route`);
88
+ let o = (e, n) => {
89
+ let r = t[e.content_kind], a = p(i, n);
90
+ return r === void 0 ? null : m(r, e.code, d(e, a), { language: a });
91
+ }, s = (e) => {
66
92
  if (!e.startsWith("/")) return null;
67
93
  let t = e.slice(1);
68
94
  t.endsWith("/") && (t = t.slice(0, -1));
69
- let r = t.split("/");
70
- if (r.some((e) => e === "")) return null;
71
- let i = "";
72
- if (r.length > 2 && a.includes(r[0]) && (i = r[0], r = r.slice(1)), r.length < 2 || r.length > 3 || !s.has(r[0])) return null;
73
- let o = n(r[1]);
74
- return o === null ? null : {
75
- language: i,
76
- route: r[0],
77
- code: o,
78
- rawCode: r[1],
79
- slug: r[2] ?? ""
95
+ let n = t.split("/");
96
+ if (n.some((e) => e === "")) return null;
97
+ let o = "";
98
+ if (n.length > 2 && i.includes(n[0]) && (o = n[0], n = n.slice(1)), n.length < 2 || n.length > 3 || !a.has(n[0])) return null;
99
+ let s = r(n[1]);
100
+ return s === null ? null : {
101
+ language: o,
102
+ route: n[0],
103
+ code: s,
104
+ rawCode: n[1],
105
+ slug: n[2] ?? ""
80
106
  };
81
- }, d = (e, n = {}) => {
107
+ }, c = (e, n = {}) => {
82
108
  let r = t[e.content_kind];
83
109
  if (r === void 0) throw Error(`contentkit-ui/urls: no route for kind ${JSON.stringify(e.content_kind)}`);
84
- return o(r, e.code, i(e, n.language), n);
110
+ let a = p(i, n.language);
111
+ return m(r, e.code, d(e, a), {
112
+ ...n,
113
+ language: a
114
+ });
85
115
  };
86
116
  return {
87
- languages: a,
88
- origin: r,
89
- path: d,
117
+ languages: i,
118
+ origin: n,
119
+ path: c,
90
120
  url(e, t) {
91
- if (r === void 0) throw Error("contentkit-ui/urls: url() needs ContentURLConfig.origin");
92
- return r + d(e, t);
121
+ if (n === void 0) throw Error("contentkit-ui/urls: url() needs ContentURLConfig.origin");
122
+ return n + c(e, t);
93
123
  },
94
- parse: u,
95
- canonical(e, t) {
96
- let n = e.split("#", 1)[0], r = n.indexOf("?"), i = r < 0 ? n : n.slice(0, r), a = r < 0 ? "" : n.slice(r + 1), o = u(i);
97
- if (o === null) return null;
98
- let s = c(t, o.language);
99
- return s === null ? null : {
100
- path: s,
101
- location: a ? `${s}?${a}` : s,
102
- redirect: i !== s
124
+ parse: s,
125
+ canonical(e, t, n = {}) {
126
+ let r = e.split("#", 1)[0], a = r.indexOf("?"), c = a < 0 ? r : r.slice(0, a), l = a < 0 ? "" : r.slice(a + 1), u = s(c);
127
+ if (u === null) return null;
128
+ let d = u.language || (n.language && i.includes(n.language) ? n.language : ""), f = o(t, d);
129
+ return f === null ? null : {
130
+ path: f,
131
+ location: l ? `${f}?${l}` : f,
132
+ redirect: c !== f
103
133
  };
104
134
  },
105
135
  hreflang(e, t = {}) {
106
- let n = t.origin?.replace(/\/+$/, "") ?? r;
107
- if (n === void 0) throw Error("contentkit-ui/urls: hreflang() needs an origin");
108
- let i = {}, o = (t.languages ?? a).filter((e) => a.includes(e));
109
- for (let t of o) i[t] = n + d(e, { language: t });
110
- let s = t.defaultLanguage;
111
- return s && i[s] && (i["x-default"] = i[s]), i;
136
+ let r = t.origin?.replace(/\/+$/, "") ?? n;
137
+ if (r === void 0) throw Error("contentkit-ui/urls: hreflang() needs an origin");
138
+ let a = {}, o = (t.languages ?? i).filter((e) => i.includes(e));
139
+ for (let t of o) a[t] = r + c(e, { language: t });
140
+ let s = t.defaultLanguage ?? i[0];
141
+ return o.length && (a["x-default"] = r + c(e, { language: s })), a;
112
142
  }
113
143
  };
114
144
  }
115
145
  //#endregion
116
- export { t as CODE_ALPHABET, e as CODE_LENGTH, s as TRACKING_PARAMS, c as canonicalURL, o as contentPath, u as createContentURLs, r as isCode, n as parseCode, i as slugFor };
146
+ export { t as CODE_ALPHABET, e as CODE_LENGTH, h as TRACKING_PARAMS, a as VERSION_CODE_LENGTH, i as VERSION_PARAM, g as canonicalURL, m as contentPath, v as createContentURLs, u as isCode, p as languagePrefix, r as parseCode, o as parseVersionCode, d as slugFor, s as versionParam, l as withVersion };
117
147
 
118
148
  //# sourceMappingURL=urls.js.map
package/dist/urls.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"urls.js","names":[],"sources":["../src/urls/index.ts"],"sourcesContent":["// ContentKit content URLs: [/{lang}]/{route}/{CODE}[/{slug}]. The code is the\n// only part the server reads; the slug is decoration. Mirrors Go's contenturl\n// (the two share contenturl/testdata/vectors.json).\n\nexport const CODE_LENGTH = 9;\n\n/** Crockford base32: digits and uppercase letters without I, L, O, U. */\nexport const CODE_ALPHABET = \"0123456789ABCDEFGHJKMNPQRSTVWXYZ\";\n\n/**\n * Reads a code with Crockford's decoding rules (any case, O as 0, I and L as 1,\n * hyphens ignored) and returns its canonical form, or null when the input is\n * not a code. An all-digit result is never a code (it reads as a legacy id).\n */\nexport function parseCode(input: string): string | null {\n let out = \"\";\n for (const ch of input) {\n if (ch === \"-\") continue;\n if (ch.length !== 1 || ch.charCodeAt(0) > 0x7f) return null;\n let c = ch.toUpperCase();\n if (c === \"O\") c = \"0\";\n else if (c === \"I\" || c === \"L\") c = \"1\";\n if (!CODE_ALPHABET.includes(c) || out.length === CODE_LENGTH) return null;\n out += c;\n }\n if (out.length !== CODE_LENGTH || !/[A-Z]/.test(out)) return null;\n return out;\n}\n\n/** Reports whether input is a code in canonical form. */\nexport function isCode(input: string): boolean {\n return parseCode(input) === input;\n}\n\n/** A resolved content link, as ContentKit and host APIs return it. */\nexport interface ContentLink {\n content_kind: string;\n code: string;\n slug: string;\n /** Localized slugs keyed by language; the default slug applies otherwise. */\n slugs?: Record<string, string>;\n}\n\n/** The slug for language, falling back to the default slug. */\nexport function slugFor(link: Pick<ContentLink, \"slug\" | \"slugs\">, language = \"\"): string {\n return (language && link.slugs?.[language]) || link.slug;\n}\n\nexport type QueryInit = string | URLSearchParams | Record<string, string | number | boolean | null | undefined>;\n\nexport interface PathOptions {\n /** Language prefix segment; omitted when empty. */\n language?: string;\n /** Query string for sub-pages, e.g. { p: 12 }. Null and undefined values are dropped. */\n query?: QueryInit;\n}\n\nfunction queryString(query: QueryInit | undefined): string {\n if (query === undefined) return \"\";\n let params: URLSearchParams;\n if (typeof query === \"string\" || query instanceof URLSearchParams) {\n params = new URLSearchParams(query);\n } else {\n params = new URLSearchParams();\n for (const [k, v] of Object.entries(query)) {\n if (v !== null && v !== undefined) params.append(k, String(v));\n }\n }\n const s = params.toString();\n return s ? `?${s}` : \"\";\n}\n\n/** Joins [/{language}]/{route}/{code}[/{slug}][?query]. */\nexport function contentPath(route: string, code: string, slug = \"\", options: PathOptions = {}): string {\n const prefix = options.language ? `/${options.language}` : \"\";\n return `${prefix}/${route}/${code}${slug ? `/${slug}` : \"\"}${queryString(options.query)}`;\n}\n\nexport interface ContentURLConfig {\n /** Content kind -> the host's route segment ({ video: \"watch\", gallery: \"g\" }). */\n routes: Record<string, string>;\n /** Language segments a path may start with; kept in canonical paths. */\n languages?: string[];\n /** The site's origin (\"https://example.com\") for absolute URLs (canonical links, og:url). */\n origin?: string;\n}\n\nexport interface ParsedContentPath {\n /** \"\" when unprefixed. */\n language: string;\n route: string;\n /** Canonical form. */\n code: string;\n rawCode: string;\n /** As written; \"\" when absent. */\n slug: string;\n}\n\nexport interface CanonicalResult {\n /** The canonical path. */\n path: string;\n /** path plus the location's query. */\n location: string;\n /** The location's path is not canonical: replace it with location. */\n redirect: boolean;\n}\n\nexport interface HreflangOptions {\n /** Languages the content exists in; default every configured language. */\n languages?: readonly string[];\n /** The language x-default points at. */\n defaultLanguage?: string;\n /** The origin for absolute URLs; default the configured one (throws without either). */\n origin?: string;\n}\n\nexport interface ContentURLs {\n /** The configured language segments. */\n readonly languages: readonly string[];\n /** The configured origin, without a trailing slash. */\n readonly origin?: string;\n /** The canonical path of link; throws when its kind has no route. */\n path(link: ContentLink, options?: PathOptions): string;\n /** path() on the configured origin; throws without one. */\n url(link: ContentLink, options?: PathOptions): string;\n /** Splits a pathname; null when it is not a content path (or its code is invalid). */\n parse(pathname: string): ParsedContentPath | null;\n /**\n * Compares a location (pathname plus optional ?query and #hash) with link's\n * canonical path, for a client-side history.replaceState after the page's\n * data arrives. null when the location is not a content path or link's kind\n * has no route.\n */\n canonical(location: string, link: ContentLink): CanonicalResult | null;\n /**\n * hreflang alternates: each language's absolute URL of link (its own\n * slug), plus x-default; empty when no language is configured.\n */\n hreflang(link: ContentLink, options?: HreflangOptions): Record<string, string>;\n}\n\n/** Query parameters that only track a visit: never part of a canonical URL. */\nexport const TRACKING_PARAMS = [\"gclid\", \"gbraid\", \"wbraid\", \"dclid\", \"fbclid\", \"msclkid\", \"yclid\", \"twclid\", \"igshid\", \"mc_cid\", \"mc_eid\", \"_ga\", \"_gl\"];\n\n/**\n * href without tracking parameters (utm_* and TRACKING_PARAMS, plus drop),\n * its fragment or a trailing slash: the canonical URL of a page that is not\n * a content page.\n */\nexport function canonicalURL(href: string, options: { drop?: readonly string[] } = {}): string {\n const url = new URL(href);\n const drop = new Set([...TRACKING_PARAMS, ...(options.drop ?? [])]);\n for (const key of [...url.searchParams.keys()]) if (key.startsWith(\"utm_\") || drop.has(key)) url.searchParams.delete(key);\n url.hash = \"\";\n if (url.pathname.length > 1 && url.pathname.endsWith(\"/\")) url.pathname = url.pathname.replace(/\\/+$/, \"\");\n return url.toString();\n}\n\nconst SEGMENT = /^[a-z0-9][a-z0-9_-]*$/;\n\n/** Builds the URL helpers for one host's routes. */\nexport function createContentURLs(config: ContentURLConfig): ContentURLs {\n const routes = { ...config.routes };\n const origin = config.origin?.replace(/\\/+$/, \"\");\n if (origin !== undefined && !/^https?:\\/\\/[^/?#]+$/.test(origin)) throw new Error(`contentkit-ui/urls: origin ${JSON.stringify(config.origin)} is not an http(s) origin`);\n const languages = [...(config.languages ?? [])];\n const routeSet = new Set(Object.values(routes));\n if (routeSet.size === 0) throw new Error(\"contentkit-ui/urls: routes is empty\");\n for (const r of [...routeSet, ...languages]) {\n if (!SEGMENT.test(r)) throw new Error(`contentkit-ui/urls: ${JSON.stringify(r)} is not one lowercase path segment`);\n }\n for (const l of languages) {\n if (routeSet.has(l)) throw new Error(`contentkit-ui/urls: language ${JSON.stringify(l)} is also a route`);\n }\n\n const canonicalPath = (link: ContentLink, language: string): string | null => {\n const route = routes[link.content_kind];\n return route === undefined ? null : contentPath(route, link.code, slugFor(link, language), { language });\n };\n\n const parse = (pathname: string): ParsedContentPath | null => {\n if (!pathname.startsWith(\"/\")) return null;\n let rest = pathname.slice(1);\n if (rest.endsWith(\"/\")) rest = rest.slice(0, -1);\n let segs = rest.split(\"/\");\n if (segs.some((s) => s === \"\")) return null;\n let language = \"\";\n if (segs.length > 2 && languages.includes(segs[0]!)) {\n language = segs[0]!;\n segs = segs.slice(1);\n }\n if (segs.length < 2 || segs.length > 3 || !routeSet.has(segs[0]!)) return null;\n const code = parseCode(segs[1]!);\n if (code === null) return null;\n return { language, route: segs[0]!, code, rawCode: segs[1]!, slug: segs[2] ?? \"\" };\n };\n\n const path = (link: ContentLink, options: PathOptions = {}): string => {\n const route = routes[link.content_kind];\n if (route === undefined) throw new Error(`contentkit-ui/urls: no route for kind ${JSON.stringify(link.content_kind)}`);\n return contentPath(route, link.code, slugFor(link, options.language), options);\n };\n\n return {\n languages,\n origin,\n path,\n url(link, options) {\n if (origin === undefined) throw new Error(\"contentkit-ui/urls: url() needs ContentURLConfig.origin\");\n return origin + path(link, options);\n },\n parse,\n canonical(location, link) {\n const noHash = location.split(\"#\", 1)[0]!;\n const q = noHash.indexOf(\"?\");\n const pathname = q < 0 ? noHash : noHash.slice(0, q);\n const search = q < 0 ? \"\" : noHash.slice(q + 1);\n const parsed = parse(pathname);\n if (parsed === null) return null;\n const path = canonicalPath(link, parsed.language);\n if (path === null) return null;\n return { path, location: search ? `${path}?${search}` : path, redirect: pathname !== path };\n },\n hreflang(link, options = {}) {\n const base = options.origin?.replace(/\\/+$/, \"\") ?? origin;\n if (base === undefined) throw new Error(\"contentkit-ui/urls: hreflang() needs an origin\");\n const out: Record<string, string> = {};\n const langs = (options.languages ?? languages).filter((l) => languages.includes(l));\n for (const language of langs) out[language] = base + path(link, { language });\n const def = options.defaultLanguage;\n if (def && out[def]) out[\"x-default\"] = out[def];\n return out;\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"urls.js","names":[],"sources":["../src/urls/index.ts"],"sourcesContent":["// ContentKit content URLs: /{lang}/{route}/{CODE}[/{slug}][?v={version}]. Every\n// page is under its language, the default included; the code is the only part\n// of the path the server reads, the slug is decoration, and ?v= selects a\n// version. Mirrors Go's contenturl (the two share contenturl/testdata/vectors.json).\n\nexport const CODE_LENGTH = 9;\n\n/** Crockford base32: digits and uppercase letters without I, L, O, U. */\nexport const CODE_ALPHABET = \"0123456789ABCDEFGHJKMNPQRSTVWXYZ\";\n\nfunction parseCrockford(input: string, length: number): string | null {\n let out = \"\";\n for (const ch of input) {\n if (ch === \"-\") continue;\n if (ch.length !== 1 || ch.charCodeAt(0) > 0x7f) return null;\n let c = ch.toUpperCase();\n if (c === \"O\") c = \"0\";\n else if (c === \"I\" || c === \"L\") c = \"1\";\n if (!CODE_ALPHABET.includes(c) || out.length === length) return null;\n out += c;\n }\n if (out.length !== length || !/[A-Z]/.test(out)) return null;\n return out;\n}\n\n/**\n * Reads a code with Crockford's decoding rules (any case, O as 0, I and L as 1,\n * hyphens ignored) and returns its canonical form, or null when the input is\n * not a code. An all-digit result is never a code (it reads as a legacy id).\n */\nexport function parseCode(input: string): string | null {\n return parseCrockford(input, CODE_LENGTH);\n}\n\n/** The query parameter that selects a version on its content's page. */\nexport const VERSION_PARAM = \"v\";\n\nexport const VERSION_CODE_LENGTH = 4;\n\n/** Reads a version code (four characters, parseCode's rules); null when the input is not one. */\nexport function parseVersionCode(input: string): string | null {\n return parseCrockford(input, VERSION_CODE_LENGTH);\n}\n\n/** The version code a location's (or search string's) ?v= names; null when it names none. */\nexport function versionParam(location: string | URLSearchParams): string | null {\n const v = (typeof location === \"string\" ? searchOf(location) : location).get(VERSION_PARAM);\n return v === null ? null : parseVersionCode(v);\n}\n\nfunction searchOf(location: string): URLSearchParams {\n const noHash = location.split(\"#\", 1)[0]!;\n const q = noHash.indexOf(\"?\");\n return new URLSearchParams(q < 0 ? \"\" : noHash.slice(q + 1));\n}\n\n/** location with ?v= set to code, or removed for null; its other parameters and hash stay. */\nexport function withVersion(location: string, code: string | null): string {\n const hashAt = location.indexOf(\"#\");\n const hash = hashAt < 0 ? \"\" : location.slice(hashAt);\n const rest = hashAt < 0 ? location : location.slice(0, hashAt);\n const q = rest.indexOf(\"?\");\n const params = new URLSearchParams(q < 0 ? \"\" : rest.slice(q + 1));\n if (code === null) params.delete(VERSION_PARAM);\n else params.set(VERSION_PARAM, code);\n const search = params.toString();\n return (q < 0 ? rest : rest.slice(0, q)) + (search ? `?${search}` : \"\") + hash;\n}\n\n/** Reports whether input is a code in canonical form. */\nexport function isCode(input: string): boolean {\n return parseCode(input) === input;\n}\n\n/** A resolved content link, as ContentKit and host APIs return it. */\nexport interface ContentLink {\n content_kind: string;\n code: string;\n slug: string;\n /** Localized slugs keyed by language; the default slug applies otherwise. */\n slugs?: Record<string, string>;\n}\n\n/** The slug for language, falling back to the default slug. */\nexport function slugFor(link: Pick<ContentLink, \"slug\" | \"slugs\">, language = \"\"): string {\n return (language && link.slugs?.[language]) || link.slug;\n}\n\nexport type QueryInit = string | URLSearchParams | Record<string, string | number | boolean | null | undefined>;\n\nexport interface PathOptions {\n /** The page's language; ContentURLs use the site's default when unset. */\n language?: string;\n /** Query string for sub-pages, e.g. { p: 12 }. Null and undefined values are dropped. */\n query?: QueryInit;\n /** A version code (Version.code): ?v= selects that version on the page. */\n version?: string | null;\n}\n\nfunction queryString(query: QueryInit | undefined, version?: string | null): string {\n let params: URLSearchParams;\n if (query === undefined) {\n params = new URLSearchParams();\n } else if (typeof query === \"string\" || query instanceof URLSearchParams) {\n params = new URLSearchParams(query);\n } else {\n params = new URLSearchParams();\n for (const [k, v] of Object.entries(query)) {\n if (v !== null && v !== undefined) params.append(k, String(v));\n }\n }\n if (version) params.set(VERSION_PARAM, version);\n const s = params.toString();\n return s ? `?${s}` : \"\";\n}\n\n/**\n * The language of a page asked for in language on a site of languages, its\n * default first: language, else the default. Every content page is under its\n * language. Mirrors Go's contenturl.Prefix.\n */\nexport function languagePrefix(languages: readonly string[], language = \"\"): string {\n return language || languages[0] || \"\";\n}\n\n/** Joins /{language}/{route}/{code}[/{slug}][?query]; ContentURLs.path() picks the language. */\nexport function contentPath(route: string, code: string, slug = \"\", options: PathOptions = {}): string {\n const prefix = options.language ? `/${options.language}` : \"\";\n return `${prefix}/${route}/${code}${slug ? `/${slug}` : \"\"}${queryString(options.query, options.version)}`;\n}\n\nexport interface ContentURLConfig {\n /** Content kind -> the host's route segment ({ video: \"watch\", gallery: \"g\" }). */\n routes: Record<string, string>;\n /**\n * The site's languages, its default first (the content model's). Required:\n * every content path starts with one; an unprefixed location is not canonical.\n */\n languages: string[];\n /** The site's origin (\"https://example.com\") for absolute URLs (canonical links, og:url). */\n origin?: string;\n}\n\nexport interface ParsedContentPath {\n /** \"\" when unprefixed. */\n language: string;\n route: string;\n /** Canonical form. */\n code: string;\n rawCode: string;\n /** As written; \"\" when absent. */\n slug: string;\n}\n\nexport interface CanonicalResult {\n /** The canonical path. */\n path: string;\n /** path plus the location's query. */\n location: string;\n /** The location's path is not canonical: replace it with location. */\n redirect: boolean;\n}\n\nexport interface CanonicalOptions {\n /** The language an unprefixed location redirects to (the reader's); default the site's default. */\n language?: string;\n}\n\nexport interface HreflangOptions {\n /** Languages the content exists in; default every configured language. */\n languages?: readonly string[];\n /** The language x-default points at, listed or not; default the site's default language. */\n defaultLanguage?: string;\n /** The origin for absolute URLs; default the configured one (throws without either). */\n origin?: string;\n}\n\nexport interface ContentURLs {\n /** The configured language segments. */\n readonly languages: readonly string[];\n /** The configured origin, without a trailing slash. */\n readonly origin?: string;\n /** link's page in a language (the default when unset), with ?v= for a version; throws when its kind has no route. */\n path(link: ContentLink, options?: PathOptions): string;\n /** path() on the configured origin; throws without one. */\n url(link: ContentLink, options?: PathOptions): string;\n /** Splits a pathname; null when it is not a content path (or its code is invalid). */\n parse(pathname: string): ParsedContentPath | null;\n /**\n * Compares a location (pathname plus optional ?query and #hash) with link's\n * canonical path, for a client-side history.replaceState after the page's\n * data arrives; the query (?v= included) stays. null when the location is\n * not a content path or link's kind has no route.\n */\n canonical(location: string, link: ContentLink, options?: CanonicalOptions): CanonicalResult | null;\n /**\n * hreflang alternates: each language's absolute URL of link (its own\n * slug), plus x-default, the default language's. Never a version's: a\n * version's canonical URL is its language page.\n */\n hreflang(link: ContentLink, options?: HreflangOptions): Record<string, string>;\n}\n\n/** Query parameters that only track a visit: never part of a canonical URL. */\nexport const TRACKING_PARAMS = [\"gclid\", \"gbraid\", \"wbraid\", \"dclid\", \"fbclid\", \"msclkid\", \"yclid\", \"twclid\", \"igshid\", \"mc_cid\", \"mc_eid\", \"_ga\", \"_gl\"];\n\n/**\n * href without tracking parameters (utm_* and TRACKING_PARAMS, plus drop),\n * its fragment or a trailing slash: the canonical URL of a page that is not\n * a content page.\n */\nexport function canonicalURL(href: string, options: { drop?: readonly string[] } = {}): string {\n const url = new URL(href);\n const drop = new Set([...TRACKING_PARAMS, ...(options.drop ?? [])]);\n for (const key of [...url.searchParams.keys()]) if (key.startsWith(\"utm_\") || drop.has(key)) url.searchParams.delete(key);\n url.hash = \"\";\n if (url.pathname.length > 1 && url.pathname.endsWith(\"/\")) url.pathname = url.pathname.replace(/\\/+$/, \"\");\n return url.toString();\n}\n\nconst SEGMENT = /^[a-z0-9][a-z0-9_-]*$/;\n\n/** Builds the URL helpers for one host's routes. */\nexport function createContentURLs(config: ContentURLConfig): ContentURLs {\n const routes = { ...config.routes };\n const origin = config.origin?.replace(/\\/+$/, \"\");\n if (origin !== undefined && !/^https?:\\/\\/[^/?#]+$/.test(origin)) throw new Error(`contentkit-ui/urls: origin ${JSON.stringify(config.origin)} is not an http(s) origin`);\n const languages = [...(config.languages ?? [])];\n const routeSet = new Set(Object.values(routes));\n if (routeSet.size === 0) throw new Error(\"contentkit-ui/urls: routes is empty\");\n if (languages.length === 0) throw new Error(\"contentkit-ui/urls: languages is empty\");\n for (const r of [...routeSet, ...languages]) {\n if (!SEGMENT.test(r)) throw new Error(`contentkit-ui/urls: ${JSON.stringify(r)} is not one lowercase path segment`);\n }\n for (const l of languages) {\n if (routeSet.has(l)) throw new Error(`contentkit-ui/urls: language ${JSON.stringify(l)} is also a route`);\n }\n\n const canonicalPath = (link: ContentLink, language: string): string | null => {\n const route = routes[link.content_kind];\n const lang = languagePrefix(languages, language);\n return route === undefined ? null : contentPath(route, link.code, slugFor(link, lang), { language: lang });\n };\n\n const parse = (pathname: string): ParsedContentPath | null => {\n if (!pathname.startsWith(\"/\")) return null;\n let rest = pathname.slice(1);\n if (rest.endsWith(\"/\")) rest = rest.slice(0, -1);\n let segs = rest.split(\"/\");\n if (segs.some((s) => s === \"\")) return null;\n let language = \"\";\n if (segs.length > 2 && languages.includes(segs[0]!)) {\n language = segs[0]!;\n segs = segs.slice(1);\n }\n if (segs.length < 2 || segs.length > 3 || !routeSet.has(segs[0]!)) return null;\n const code = parseCode(segs[1]!);\n if (code === null) return null;\n return { language, route: segs[0]!, code, rawCode: segs[1]!, slug: segs[2] ?? \"\" };\n };\n\n const path = (link: ContentLink, options: PathOptions = {}): string => {\n const route = routes[link.content_kind];\n if (route === undefined) throw new Error(`contentkit-ui/urls: no route for kind ${JSON.stringify(link.content_kind)}`);\n const language = languagePrefix(languages, options.language);\n return contentPath(route, link.code, slugFor(link, language), { ...options, language });\n };\n\n return {\n languages,\n origin,\n path,\n url(link, options) {\n if (origin === undefined) throw new Error(\"contentkit-ui/urls: url() needs ContentURLConfig.origin\");\n return origin + path(link, options);\n },\n parse,\n canonical(location, link, options = {}) {\n const noHash = location.split(\"#\", 1)[0]!;\n const q = noHash.indexOf(\"?\");\n const pathname = q < 0 ? noHash : noHash.slice(0, q);\n const search = q < 0 ? \"\" : noHash.slice(q + 1);\n const parsed = parse(pathname);\n if (parsed === null) return null;\n const language = parsed.language || (options.language && languages.includes(options.language) ? options.language : \"\");\n const path = canonicalPath(link, language);\n if (path === null) return null;\n return { path, location: search ? `${path}?${search}` : path, redirect: pathname !== path };\n },\n hreflang(link, options = {}) {\n const base = options.origin?.replace(/\\/+$/, \"\") ?? origin;\n if (base === undefined) throw new Error(\"contentkit-ui/urls: hreflang() needs an origin\");\n const out: Record<string, string> = {};\n const langs = (options.languages ?? languages).filter((l) => languages.includes(l));\n for (const language of langs) out[language] = base + path(link, { language });\n const def = options.defaultLanguage ?? languages[0];\n if (langs.length) out[\"x-default\"] = base + path(link, { language: def });\n return out;\n },\n };\n}\n"],"mappings":";AAKA,IAAa,IAAc,GAGd,IAAgB;AAE7B,SAAS,EAAe,GAAe,GAA+B;CACpE,IAAI,IAAM;CACV,KAAK,IAAM,KAAM,GAAO;EACtB,IAAI,MAAO,KAAK;EAChB,IAAI,EAAG,WAAW,KAAK,EAAG,WAAW,CAAC,IAAI,KAAM,OAAO;EACvD,IAAI,IAAI,EAAG,YAAY;EAGvB,IAFI,MAAM,MAAK,IAAI,OACV,MAAM,OAAO,MAAM,SAAK,IAAI,MACjC,CAAA,mCAAe,SAAS,CAAC,KAAK,EAAI,WAAW,GAAQ,OAAO;EAChE,KAAO;CACT;CAEA,OADI,EAAI,WAAW,KAAU,CAAC,QAAQ,KAAK,CAAG,IAAU,OACjD;AACT;AAOA,SAAgB,EAAU,GAA8B;CACtD,OAAO,EAAe,GAAA,CAAkB;AAC1C;AAGA,IAAa,IAAgB,KAEhB,IAAsB;AAGnC,SAAgB,EAAiB,GAA8B;CAC7D,OAAO,EAAe,GAAA,CAA0B;AAClD;AAGA,SAAgB,EAAa,GAAmD;CAC9E,IAAM,KAAK,OAAO,KAAa,WAAW,EAAS,CAAQ,IAAI,EAAA,CAAU,IAAA,GAAiB;CAC1F,OAAO,MAAM,OAAO,OAAO,EAAiB,CAAC;AAC/C;AAEA,SAAS,EAAS,GAAmC;CACnD,IAAM,IAAS,EAAS,MAAM,KAAK,CAAC,CAAC,CAAC,IAChC,IAAI,EAAO,QAAQ,GAAG;CAC5B,OAAO,IAAI,gBAAgB,IAAI,IAAI,KAAK,EAAO,MAAM,IAAI,CAAC,CAAC;AAC7D;AAGA,SAAgB,EAAY,GAAkB,GAA6B;CACzE,IAAM,IAAS,EAAS,QAAQ,GAAG,GAC7B,IAAO,IAAS,IAAI,KAAK,EAAS,MAAM,CAAM,GAC9C,IAAO,IAAS,IAAI,IAAW,EAAS,MAAM,GAAG,CAAM,GACvD,IAAI,EAAK,QAAQ,GAAG,GACpB,IAAS,IAAI,gBAAgB,IAAI,IAAI,KAAK,EAAK,MAAM,IAAI,CAAC,CAAC;CACjE,AAAI,MAAS,OAAM,EAAO,OAAA,GAAoB,IACzC,EAAO,IAAA,KAAmB,CAAI;CACnC,IAAM,IAAS,EAAO,SAAS;CAC/B,QAAQ,IAAI,IAAI,IAAO,EAAK,MAAM,GAAG,CAAC,MAAM,IAAS,IAAI,MAAW,MAAM;AAC5E;AAGA,SAAgB,EAAO,GAAwB;CAC7C,OAAO,EAAU,CAAK,MAAM;AAC9B;AAYA,SAAgB,EAAQ,GAA2C,IAAW,IAAY;CACxF,OAAQ,KAAY,EAAK,QAAQ,MAAc,EAAK;AACtD;AAaA,SAAS,EAAY,GAA8B,GAAiC;CAClF,IAAI;CACJ,IAAI,MAAU,KAAA,GACZ,IAAS,IAAI,gBAAgB;MACxB,IAAI,OAAO,KAAU,YAAY,aAAiB,iBACvD,IAAS,IAAI,gBAAgB,CAAK;MAC7B;EACL,IAAS,IAAI,gBAAgB;EAC7B,KAAK,IAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,CAAK,GACvC,AAAI,KAAM,QAAyB,EAAO,OAAO,GAAG,OAAO,CAAC,CAAC;CAEjE;CACA,AAAI,KAAS,EAAO,IAAA,KAAmB,CAAO;CAC9C,IAAM,IAAI,EAAO,SAAS;CAC1B,OAAO,IAAI,IAAI,MAAM;AACvB;AAOA,SAAgB,EAAe,GAA8B,IAAW,IAAY;CAClF,OAAO,KAAY,EAAU,MAAM;AACrC;AAGA,SAAgB,EAAY,GAAe,GAAc,IAAO,IAAI,IAAuB,CAAC,GAAW;CAErG,OAAO,GADQ,EAAQ,WAAW,IAAI,EAAQ,aAAa,GAC1C,GAAG,EAAM,GAAG,IAAO,IAAO,IAAI,MAAS,KAAK,EAAY,EAAQ,OAAO,EAAQ,OAAO;AACzG;AA2EA,IAAa,IAAkB;CAAC;CAAS;CAAU;CAAU;CAAS;CAAU;CAAW;CAAS;CAAU;CAAU;CAAU;CAAU;CAAO;AAAK;AAOxJ,SAAgB,EAAa,GAAc,IAAwC,CAAC,GAAW;CAC7F,IAAM,IAAM,IAAI,IAAI,CAAI,GAClB,oBAAO,IAAI,IAAI,CAAC,GAAG,GAAiB,GAAI,EAAQ,QAAQ,CAAC,CAAE,CAAC;CAClE,KAAK,IAAM,KAAO,CAAC,GAAG,EAAI,aAAa,KAAK,CAAC,GAAG,CAAI,EAAI,WAAW,MAAM,KAAK,EAAK,IAAI,CAAG,MAAG,EAAI,aAAa,OAAO,CAAG;CAGxH,OAFA,EAAI,OAAO,IACP,EAAI,SAAS,SAAS,KAAK,EAAI,SAAS,SAAS,GAAG,MAAG,EAAI,WAAW,EAAI,SAAS,QAAQ,QAAQ,EAAE,IAClG,EAAI,SAAS;AACtB;AAEA,IAAM,IAAU;AAGhB,SAAgB,EAAkB,GAAuC;CACvE,IAAM,IAAS,EAAE,GAAG,EAAO,OAAO,GAC5B,IAAS,EAAO,QAAQ,QAAQ,QAAQ,EAAE;CAChD,IAAI,MAAW,KAAA,KAAa,CAAC,uBAAuB,KAAK,CAAM,GAAG,MAAU,MAAM,8BAA8B,KAAK,UAAU,EAAO,MAAM,EAAE,0BAA0B;CACxK,IAAM,IAAY,CAAC,GAAI,EAAO,aAAa,CAAC,CAAE,GACxC,IAAW,IAAI,IAAI,OAAO,OAAO,CAAM,CAAC;CAC9C,IAAI,EAAS,SAAS,GAAG,MAAU,MAAM,qCAAqC;CAC9E,IAAI,EAAU,WAAW,GAAG,MAAU,MAAM,wCAAwC;CACpF,KAAK,IAAM,KAAK,CAAC,GAAG,GAAU,GAAG,CAAS,GACxC,IAAI,CAAC,EAAQ,KAAK,CAAC,GAAG,MAAU,MAAM,uBAAuB,KAAK,UAAU,CAAC,EAAE,mCAAmC;CAEpH,KAAK,IAAM,KAAK,GACd,IAAI,EAAS,IAAI,CAAC,GAAG,MAAU,MAAM,gCAAgC,KAAK,UAAU,CAAC,EAAE,iBAAiB;CAG1G,IAAM,KAAiB,GAAmB,MAAoC;EAC5E,IAAM,IAAQ,EAAO,EAAK,eACpB,IAAO,EAAe,GAAW,CAAQ;EAC/C,OAAO,MAAU,KAAA,IAAY,OAAO,EAAY,GAAO,EAAK,MAAM,EAAQ,GAAM,CAAI,GAAG,EAAE,UAAU,EAAK,CAAC;CAC3G,GAEM,KAAS,MAA+C;EAC5D,IAAI,CAAC,EAAS,WAAW,GAAG,GAAG,OAAO;EACtC,IAAI,IAAO,EAAS,MAAM,CAAC;EAC3B,AAAI,EAAK,SAAS,GAAG,MAAG,IAAO,EAAK,MAAM,GAAG,EAAE;EAC/C,IAAI,IAAO,EAAK,MAAM,GAAG;EACzB,IAAI,EAAK,MAAM,MAAM,MAAM,EAAE,GAAG,OAAO;EACvC,IAAI,IAAW;EAKf,IAJI,EAAK,SAAS,KAAK,EAAU,SAAS,EAAK,EAAG,MAChD,IAAW,EAAK,IAChB,IAAO,EAAK,MAAM,CAAC,IAEjB,EAAK,SAAS,KAAK,EAAK,SAAS,KAAK,CAAC,EAAS,IAAI,EAAK,EAAG,GAAG,OAAO;EAC1E,IAAM,IAAO,EAAU,EAAK,EAAG;EAE/B,OADI,MAAS,OAAa,OACnB;GAAE;GAAU,OAAO,EAAK;GAAK;GAAM,SAAS,EAAK;GAAK,MAAM,EAAK,MAAM;EAAG;CACnF,GAEM,KAAQ,GAAmB,IAAuB,CAAC,MAAc;EACrE,IAAM,IAAQ,EAAO,EAAK;EAC1B,IAAI,MAAU,KAAA,GAAW,MAAU,MAAM,yCAAyC,KAAK,UAAU,EAAK,YAAY,GAAG;EACrH,IAAM,IAAW,EAAe,GAAW,EAAQ,QAAQ;EAC3D,OAAO,EAAY,GAAO,EAAK,MAAM,EAAQ,GAAM,CAAQ,GAAG;GAAE,GAAG;GAAS;EAAS,CAAC;CACxF;CAEA,OAAO;EACL;EACA;EACA;EACA,IAAI,GAAM,GAAS;GACjB,IAAI,MAAW,KAAA,GAAW,MAAU,MAAM,yDAAyD;GACnG,OAAO,IAAS,EAAK,GAAM,CAAO;EACpC;EACA;EACA,UAAU,GAAU,GAAM,IAAU,CAAC,GAAG;GACtC,IAAM,IAAS,EAAS,MAAM,KAAK,CAAC,CAAC,CAAC,IAChC,IAAI,EAAO,QAAQ,GAAG,GACtB,IAAW,IAAI,IAAI,IAAS,EAAO,MAAM,GAAG,CAAC,GAC7C,IAAS,IAAI,IAAI,KAAK,EAAO,MAAM,IAAI,CAAC,GACxC,IAAS,EAAM,CAAQ;GAC7B,IAAI,MAAW,MAAM,OAAO;GAC5B,IAAM,IAAW,EAAO,aAAa,EAAQ,YAAY,EAAU,SAAS,EAAQ,QAAQ,IAAI,EAAQ,WAAW,KAC7G,IAAO,EAAc,GAAM,CAAQ;GAEzC,OADI,MAAS,OAAa,OACnB;IAAE;IAAM,UAAU,IAAS,GAAG,EAAK,GAAG,MAAW;IAAM,UAAU,MAAa;GAAK;EAC5F;EACA,SAAS,GAAM,IAAU,CAAC,GAAG;GAC3B,IAAM,IAAO,EAAQ,QAAQ,QAAQ,QAAQ,EAAE,KAAK;GACpD,IAAI,MAAS,KAAA,GAAW,MAAU,MAAM,gDAAgD;GACxF,IAAM,IAA8B,CAAC,GAC/B,KAAS,EAAQ,aAAa,EAAA,CAAW,QAAQ,MAAM,EAAU,SAAS,CAAC,CAAC;GAClF,KAAK,IAAM,KAAY,GAAO,EAAI,KAAY,IAAO,EAAK,GAAM,EAAE,YAAS,CAAC;GAC5E,IAAM,IAAM,EAAQ,mBAAmB,EAAU;GAEjD,OADI,EAAM,WAAQ,EAAI,eAAe,IAAO,EAAK,GAAM,EAAE,UAAU,EAAI,CAAC,IACjE;EACT;CACF;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openrails/contentkit-ui",
3
- "version": "0.83.0",
3
+ "version": "0.86.0",
4
4
  "description": "ContentKit in the browser: client, React hooks and styled components for media, uploads and content URLs.",
5
5
  "type": "module",
6
6
  "license": "MIT",