@driftime/sanity-plugin-link 0.1.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 +593 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1010 -0
- package/dist/index.js.map +1 -0
- package/dist/render.d.ts +164 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +413 -0
- package/dist/render.js.map +1 -0
- package/dist/types-BR08fVY8.js +188 -0
- package/dist/types-BR08fVY8.js.map +1 -0
- package/dist/types-BXseTKgu.d.ts +176 -0
- package/dist/types-BXseTKgu.d.ts.map +1 -0
- package/package.json +89 -0
package/dist/render.js
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { a as appendDestination, c as composePhoneHref, d as readPath, f as defaultTitleField, h as isDevelopment, o as composeAnchorHref, p as logger, s as composeEmailHref, t as linkDestinations, u as isDefined } from "./types-BR08fVY8.js";
|
|
2
|
+
import { stegaClean } from "@sanity/client/stega";
|
|
3
|
+
/**
|
|
4
|
+
* Builds the GROQ that expands a link's destinations, so the document behind a page link and the
|
|
5
|
+
* asset behind a file link both arrive with the link itself.
|
|
6
|
+
*
|
|
7
|
+
* @param routeParamsFragment - GROQ resolving route parameters onto the document a page link points at.
|
|
8
|
+
* @param titleField - Field an internal link borrows its label from when none was written.
|
|
9
|
+
* @returns GROQ conditional projections covering every destination that needs expanding.
|
|
10
|
+
*/
|
|
11
|
+
function createLinkFragment(routeParamsFragment, titleField) {
|
|
12
|
+
return [`type == "page" => { ..., reference-> { ${[
|
|
13
|
+
"_id",
|
|
14
|
+
"_type",
|
|
15
|
+
titleField,
|
|
16
|
+
routeParamsFragment
|
|
17
|
+
].filter((part) => isDefined(part)).join(", ")} } }`, "type == \"file\" => { ..., file { ..., asset-> } }"].join(", ");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Checks whether a query expanded a reference into the document it points at. Resolution reports an
|
|
21
|
+
* unexpanded one rather than working around it, because the fix belongs in the query.
|
|
22
|
+
*
|
|
23
|
+
* @param reference - The reference to check.
|
|
24
|
+
* @returns True if the reference holds the document itself.
|
|
25
|
+
*/
|
|
26
|
+
function isExpandedReference(reference) {
|
|
27
|
+
return isDefined(reference) ? "_ref" in reference ? (logger.error(`Reference "${reference._ref}" has not been expanded. See: https://www.sanity.io/docs/content-lake/how-queries-work#k8ca3cefc3a31`), !1) : !0 : !1;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Reads a route's parameter values off a fetched document: those a query projected into `_routeParams`
|
|
31
|
+
* first, then any whose GROQ is a plain field path, read directly, so a query need only project the
|
|
32
|
+
* parameters that follow a reference or compute a value.
|
|
33
|
+
*
|
|
34
|
+
* @param document - The document a route is being resolved for.
|
|
35
|
+
* @param expressions - The GROQ expression each parameter is filled from.
|
|
36
|
+
* @returns The parameter values that could be read.
|
|
37
|
+
*/
|
|
38
|
+
function readDocumentParams(document, expressions) {
|
|
39
|
+
let projected = document._routeParams ?? {};
|
|
40
|
+
return Object.fromEntries(Object.entries(expressions).map(([param, expression]) => isDefined(projected[param]) ? [param, projected[param]] : /^[A-Za-z_][\w.]*$/u.test(expression) ? [param, readPath(document, expression.split("."))] : [param, void 0]));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Binds a site's route table to the functions that read it, so a path is declared once and both the
|
|
44
|
+
* query filling its parameters and the resolver spending them follow the same declaration.
|
|
45
|
+
*
|
|
46
|
+
* @param routes - Route definitions keyed by the document type each one renders.
|
|
47
|
+
* @returns An object holding the route resolver and the GROQ that feeds it.
|
|
48
|
+
*/
|
|
49
|
+
function createRouteResolver(routes) {
|
|
50
|
+
let routeParamsFragment = Object.entries(routes).flatMap(([type, route]) => {
|
|
51
|
+
let projection = Object.entries(route.params ?? {}).map(([param, expression]) => `"${param}": ${expression}`).join(", ");
|
|
52
|
+
return isDefined(projection) ? [`_type == "${type}" => { "_routeParams": { ${projection} } }`] : [];
|
|
53
|
+
}).join(", ");
|
|
54
|
+
/**
|
|
55
|
+
* Resolves a document, or a route named in code, into the path it is served at.
|
|
56
|
+
*
|
|
57
|
+
* @param destination - The document or route to resolve.
|
|
58
|
+
* @returns The path, or undefined when the type has no route or a parameter has no value.
|
|
59
|
+
*/
|
|
60
|
+
function resolveRoute(destination) {
|
|
61
|
+
let type = stegaClean(destination._type), route = routes[type];
|
|
62
|
+
if (!isDefined(route)) {
|
|
63
|
+
logger.error(`No route is configured for the "${type}" type, so a link to it leads nowhere.`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
let params = "_id" in destination ? readDocumentParams(destination, route.params ?? {}) : destination, { path } = route;
|
|
67
|
+
for (let [param, value] of Object.entries(params)) typeof value == "string" && (path = path.replaceAll(`[${param}]`, stegaClean(value)));
|
|
68
|
+
let unresolved = path.match(/\[[^\]]+\]/gu);
|
|
69
|
+
if (isDefined(unresolved)) {
|
|
70
|
+
logger.error(`Could not resolve ${unresolved.join(", ")} for the "${type}" route. The fetching query must spread \`routeParamsFragment\`, and the document must hold a value for every parameter.`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
return path;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
resolveRoute,
|
|
77
|
+
routeParamsFragment
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Creates a URL from an address, resolving a root-relative one against the site it belongs to.
|
|
82
|
+
*
|
|
83
|
+
* @param href - The address to read.
|
|
84
|
+
* @param origin - Origin the site is served from.
|
|
85
|
+
* @returns The URL, or undefined when the address cannot be read as one.
|
|
86
|
+
*/
|
|
87
|
+
function createUrl(href, origin) {
|
|
88
|
+
if (isDefined(href)) try {
|
|
89
|
+
return new URL(href, origin);
|
|
90
|
+
} catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Reduces an absolute address pointing back at the site to a root-relative one, so an address an
|
|
96
|
+
* author pasted in full still routes on the client rather than reloading the page.
|
|
97
|
+
*
|
|
98
|
+
* @param href - The absolute address to reduce.
|
|
99
|
+
* @param origin - Origin the site is served from.
|
|
100
|
+
* @returns A root-relative address, or undefined when the address points elsewhere.
|
|
101
|
+
*/
|
|
102
|
+
function resolveRelativeHref(href, origin) {
|
|
103
|
+
try {
|
|
104
|
+
let url = new URL(href);
|
|
105
|
+
return url.origin === origin ? url.pathname + url.search + url.hash : void 0;
|
|
106
|
+
} catch {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Splits a path into the segments that carry it, discarding the empty ones a leading or trailing
|
|
112
|
+
* slash leaves behind.
|
|
113
|
+
*
|
|
114
|
+
* @param path - The path to split.
|
|
115
|
+
* @returns The segments the path is built from.
|
|
116
|
+
*/
|
|
117
|
+
function splitSegments(path) {
|
|
118
|
+
return path.split("/").filter((segment) => isDefined(segment));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Checks whether a URL covers the page being read, matching every segment it declares against the
|
|
122
|
+
* start of the current path. The root is compared exactly, since it precedes every path and would
|
|
123
|
+
* otherwise cover every page on the site.
|
|
124
|
+
*
|
|
125
|
+
* @param url - The URL to compare.
|
|
126
|
+
* @param pathname - Path of the page being read.
|
|
127
|
+
* @param origin - Origin the site is served from.
|
|
128
|
+
* @returns Whether the URL is the current page or one of the paths above it.
|
|
129
|
+
*/
|
|
130
|
+
function checkContainsActivePath(url, pathname, origin) {
|
|
131
|
+
if (url.origin !== origin) return !1;
|
|
132
|
+
let linkSegments = splitSegments(url.pathname), pageSegments = splitSegments(pathname);
|
|
133
|
+
return isDefined(linkSegments) ? linkSegments.every((segment, index) => pageSegments.at(index) === segment) : !isDefined(pageSegments);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Checks whether a URL points at the page being read and nothing else. An address carrying an anchor
|
|
137
|
+
* names a location within a page rather than the page itself, so it is never the page already open.
|
|
138
|
+
*
|
|
139
|
+
* @param url - The URL to compare.
|
|
140
|
+
* @param pathname - Path of the page being read.
|
|
141
|
+
* @param origin - Origin the site is served from.
|
|
142
|
+
* @returns Whether the URL matches the current page exactly.
|
|
143
|
+
*/
|
|
144
|
+
function checkIsActivePath(url, pathname, origin) {
|
|
145
|
+
return url.origin !== origin || isDefined(url.hash) ? !1 : splitSegments(pathname).join("/") === splitSegments(url.pathname).join("/");
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Answers a navigation state from the current path, or refuses when no path was given, since a state
|
|
149
|
+
* read without one would silently render every link inactive. Production answers false instead of
|
|
150
|
+
* throwing, so a missing path never takes a page down.
|
|
151
|
+
*
|
|
152
|
+
* @param pathname - Path of the page being drawn, or undefined when none was given.
|
|
153
|
+
* @param state - Name of the state being read, for the message.
|
|
154
|
+
* @param check - Reads the state from the path.
|
|
155
|
+
* @returns Whether the state holds.
|
|
156
|
+
* @throws In development, when the state is read without a pathname.
|
|
157
|
+
*/
|
|
158
|
+
function readActiveState(pathname, state, check) {
|
|
159
|
+
if (isDefined(pathname)) return check(pathname);
|
|
160
|
+
if (isDevelopment) throw Error(logger.format(`Reading \`${state}\` needs \`pathname\` to be passed to \`resolveLink\`.`));
|
|
161
|
+
return !1;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Builds the title reader used when a site gives a field but no resolver, so naming the field once
|
|
165
|
+
* covers both the fragment and the label.
|
|
166
|
+
*
|
|
167
|
+
* @param field - Dotted path to the field a page's title lives in.
|
|
168
|
+
* @returns A reader returning the title, or undefined when the document holds no readable one.
|
|
169
|
+
*/
|
|
170
|
+
function createTitleResolver(field) {
|
|
171
|
+
return (document) => {
|
|
172
|
+
let title = readPath(document, field.split("."));
|
|
173
|
+
return typeof title == "string" ? title : void 0;
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function readRouteTable(routes) {
|
|
177
|
+
return routes ?? {};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Checks whether a resolver was declared to answer with a promise, which is known of it before it has
|
|
181
|
+
* ever run.
|
|
182
|
+
*
|
|
183
|
+
* @param resolver - The resolver to check.
|
|
184
|
+
* @returns True if the resolver was declared asynchronous.
|
|
185
|
+
*/
|
|
186
|
+
function isAsyncResolver(resolver) {
|
|
187
|
+
return isDefined(resolver) && resolver.constructor.name === "AsyncFunction";
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Reads which destination a stored link points at, clearing the stega characters a Sanity fetch leaves
|
|
191
|
+
* on the value, so a link fetched in Presentation mode resolves the way it does anywhere else.
|
|
192
|
+
*
|
|
193
|
+
* @param link - The stored link to read.
|
|
194
|
+
* @returns The destination, or undefined when the link stores none or one the plugin does not know.
|
|
195
|
+
*/
|
|
196
|
+
function readDestination(link) {
|
|
197
|
+
let stored = stegaClean(link.type);
|
|
198
|
+
if (!isDefined(stored)) return;
|
|
199
|
+
let destination = linkDestinations.find((candidate) => candidate === stored);
|
|
200
|
+
return isDefined(destination) || logger.error(`A link stores "${stored}" as its destination, which is not one the plugin resolves, so it leads nowhere.`), destination;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Checks whether a stored link points at a given destination, narrowing it to that destination's
|
|
204
|
+
* fields. The comparison clears stega characters first, so it holds in Presentation mode too.
|
|
205
|
+
*
|
|
206
|
+
* @param link - The stored link to check.
|
|
207
|
+
* @param destination - The destination to check for.
|
|
208
|
+
* @returns True if the link points at that destination.
|
|
209
|
+
*/
|
|
210
|
+
function pointsAt(link, destination) {
|
|
211
|
+
return stegaClean(link.type) === destination;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Adds the anchor and parameters an author wrote onto a page's address, once whatever decides that
|
|
215
|
+
* address has had its say.
|
|
216
|
+
*
|
|
217
|
+
* @param link - The stored link the address was built from.
|
|
218
|
+
* @param resolvedLink - The resolved link to finish.
|
|
219
|
+
* @returns The resolved link carrying the author's additions, or undefined when it leads nowhere.
|
|
220
|
+
*/
|
|
221
|
+
function appendAuthoredDestination(link, resolvedLink) {
|
|
222
|
+
if (!pointsAt(link, "page") || !isDefined(resolvedLink?.href)) return resolvedLink;
|
|
223
|
+
let { anchor, searchParams } = link;
|
|
224
|
+
return {
|
|
225
|
+
...resolvedLink,
|
|
226
|
+
href: appendDestination(resolvedLink.href, anchor, searchParams)
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Builds the state a link takes when it leads nowhere, so every unusable destination reads the same
|
|
231
|
+
* way to the site drawing it.
|
|
232
|
+
*
|
|
233
|
+
* @returns Navigation state describing a link that points at nothing.
|
|
234
|
+
*/
|
|
235
|
+
function createEmptyLinkState() {
|
|
236
|
+
return {
|
|
237
|
+
resolvedLink: void 0,
|
|
238
|
+
isExternal: !1,
|
|
239
|
+
opensNewTab: !1,
|
|
240
|
+
hasAnchor: !1,
|
|
241
|
+
containsActivePath: !1,
|
|
242
|
+
isActivePath: !1
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Binds a site's routing and conventions to the resolver, so a link is resolved the same way
|
|
247
|
+
* everywhere it is drawn.
|
|
248
|
+
*
|
|
249
|
+
* @param linkConfig - What the resolver needs to know about the site.
|
|
250
|
+
* @returns An object holding the resolver, the route table, and the GROQ the two of them rely on.
|
|
251
|
+
* @public
|
|
252
|
+
* @throws If the base address is not an absolute one.
|
|
253
|
+
*/
|
|
254
|
+
function defineLinkConfig(linkConfig) {
|
|
255
|
+
let { baseUrl, resolvers, openExternalInNewTab = !0, title: { field: titleField = defaultTitleField, resolver: resolveTitle = createTitleResolver(titleField) } = {} } = linkConfig, routes = readRouteTable(linkConfig.routes), declaredResolvers = {
|
|
256
|
+
page: resolvers?.page,
|
|
257
|
+
anchor: resolvers?.anchor,
|
|
258
|
+
url: resolvers?.url,
|
|
259
|
+
email: resolvers?.email,
|
|
260
|
+
phone: resolvers?.phone,
|
|
261
|
+
file: resolvers?.file
|
|
262
|
+
}, isAsynchronous = Object.values(declaredResolvers).some((resolver) => isAsyncResolver(resolver)), { origin } = (() => {
|
|
263
|
+
try {
|
|
264
|
+
return new URL(baseUrl);
|
|
265
|
+
} catch {
|
|
266
|
+
throw TypeError(logger.format(`A link configuration needs an absolute base URL, and "${baseUrl}" is not one.`));
|
|
267
|
+
}
|
|
268
|
+
})(), { resolveRoute, routeParamsFragment } = createRouteResolver(routes), linkFragment = createLinkFragment(routeParamsFragment, titleField);
|
|
269
|
+
/**
|
|
270
|
+
* Resolves a stored link into the parts an anchor is drawn from, according to the kind of
|
|
271
|
+
* destination it points at. A page's anchor and parameters are left off, since a resolver reads the
|
|
272
|
+
* path before an author's additions rather than after them.
|
|
273
|
+
*
|
|
274
|
+
* @param link - The stored link to resolve.
|
|
275
|
+
* @returns The resolved link, or undefined when it leads nowhere usable.
|
|
276
|
+
*/
|
|
277
|
+
function composeLink(link) {
|
|
278
|
+
if (pointsAt(link, "page")) {
|
|
279
|
+
let { reference, label } = link;
|
|
280
|
+
if (!isExpandedReference(reference)) return;
|
|
281
|
+
let path = resolveRoute(reference);
|
|
282
|
+
return isDefined(path) ? {
|
|
283
|
+
href: path,
|
|
284
|
+
label: label ?? resolveTitle(reference)
|
|
285
|
+
} : void 0;
|
|
286
|
+
}
|
|
287
|
+
if (pointsAt(link, "anchor")) {
|
|
288
|
+
let { anchor, label } = link;
|
|
289
|
+
return {
|
|
290
|
+
href: composeAnchorHref(anchor),
|
|
291
|
+
label
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (pointsAt(link, "url")) {
|
|
295
|
+
let { url, label } = link, address = stegaClean(url);
|
|
296
|
+
return {
|
|
297
|
+
href: (isDefined(address) ? resolveRelativeHref(address, origin) : void 0) ?? address,
|
|
298
|
+
label
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (pointsAt(link, "email")) {
|
|
302
|
+
let { email, subject, label } = link;
|
|
303
|
+
return {
|
|
304
|
+
href: composeEmailHref(email, subject),
|
|
305
|
+
label
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (pointsAt(link, "phone")) {
|
|
309
|
+
let { phone, label } = link;
|
|
310
|
+
return {
|
|
311
|
+
href: composePhoneHref(phone),
|
|
312
|
+
label
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
if (pointsAt(link, "file")) {
|
|
316
|
+
let { file, label } = link;
|
|
317
|
+
if (!isExpandedReference(file?.asset)) return;
|
|
318
|
+
let address = stegaClean(file.asset.url), filename = stegaClean(file.asset.originalFilename);
|
|
319
|
+
return {
|
|
320
|
+
href: isDefined(address) && isDefined(filename) ? `${address}?dl=${encodeURIComponent(filename)}` : address,
|
|
321
|
+
label,
|
|
322
|
+
download: !0
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Hands an address to the resolver its own destination declared, which has the last word on where
|
|
328
|
+
* the link points.
|
|
329
|
+
*
|
|
330
|
+
* @param link - The stored link being resolved.
|
|
331
|
+
* @param href - The address the plugin built.
|
|
332
|
+
* @returns The address to use, undefined for a link leading nowhere, or a promise of either.
|
|
333
|
+
*/
|
|
334
|
+
function runResolver(link, href) {
|
|
335
|
+
if (pointsAt(link, "page")) return resolvers?.page?.(href, link);
|
|
336
|
+
if (pointsAt(link, "anchor")) return resolvers?.anchor?.(href, link);
|
|
337
|
+
if (pointsAt(link, "url")) return resolvers?.url?.(href, link);
|
|
338
|
+
if (pointsAt(link, "email")) return resolvers?.email?.(href, link);
|
|
339
|
+
if (pointsAt(link, "phone")) return resolvers?.phone?.(href, link);
|
|
340
|
+
if (pointsAt(link, "file")) return resolvers?.file?.(href, link);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Reads how a resolved link stands against the page being drawn, which is what a site needs beyond
|
|
344
|
+
* the address itself.
|
|
345
|
+
*
|
|
346
|
+
* @param resolvedLink - The resolved link to read.
|
|
347
|
+
* @param pathname - Path of the page being drawn.
|
|
348
|
+
* @returns The resolved link and the navigation state around it.
|
|
349
|
+
*/
|
|
350
|
+
function readLinkState(resolvedLink, pathname) {
|
|
351
|
+
let url = createUrl(resolvedLink?.href, origin);
|
|
352
|
+
if (!isDefined(url)) return isDefined(resolvedLink) && logger.error("Could not resolve an address from the given link, route, or href."), createEmptyLinkState();
|
|
353
|
+
let isExternal = ["http:", "https:"].includes(url.protocol) && url.origin !== origin;
|
|
354
|
+
return {
|
|
355
|
+
resolvedLink,
|
|
356
|
+
isExternal,
|
|
357
|
+
opensNewTab: isExternal && resolvedLink?.download !== !0 && openExternalInNewTab,
|
|
358
|
+
hasAnchor: isDefined(url.hash),
|
|
359
|
+
get containsActivePath() {
|
|
360
|
+
return readActiveState(pathname, "containsActivePath", (path) => checkContainsActivePath(url, path, origin));
|
|
361
|
+
},
|
|
362
|
+
get isActivePath() {
|
|
363
|
+
return readActiveState(pathname, "isActivePath", (path) => checkIsActivePath(url, path, origin));
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Resolves a stored link, offering the address it built to the resolver that destination declared
|
|
369
|
+
* before the author's own additions go back on.
|
|
370
|
+
*
|
|
371
|
+
* @param link - The stored link to resolve.
|
|
372
|
+
* @param pathname - Path of the page being drawn.
|
|
373
|
+
* @returns The navigation state, or a promise of it when the resolver answers with one.
|
|
374
|
+
*/
|
|
375
|
+
function resolveStoredLink(link, pathname) {
|
|
376
|
+
let destination = readDestination(link);
|
|
377
|
+
if (!isDefined(destination)) return createEmptyLinkState();
|
|
378
|
+
let composed = composeLink(link), { href } = composed ?? {};
|
|
379
|
+
if (!isDefined(href) || !isDefined(declaredResolvers[destination])) return readLinkState(appendAuthoredDestination(link, composed), pathname);
|
|
380
|
+
/**
|
|
381
|
+
* Puts a resolver's answer in place of the address the plugin built.
|
|
382
|
+
*
|
|
383
|
+
* @param answer - The address the resolver answered with.
|
|
384
|
+
* @returns The navigation state around the resolved link.
|
|
385
|
+
*/
|
|
386
|
+
function readAnsweredState(answer) {
|
|
387
|
+
return readLinkState(appendAuthoredDestination(link, isDefined(answer) ? {
|
|
388
|
+
...composed,
|
|
389
|
+
href: answer
|
|
390
|
+
} : void 0), pathname);
|
|
391
|
+
}
|
|
392
|
+
let answer = runResolver(link, href);
|
|
393
|
+
return answer instanceof Promise ? (isAsynchronous = !0, answer.then((resolved) => readAnsweredState(resolved))) : readAnsweredState(answer);
|
|
394
|
+
}
|
|
395
|
+
function resolveLink({ link, route, href, pathname }) {
|
|
396
|
+
let state = isDefined([
|
|
397
|
+
link,
|
|
398
|
+
route,
|
|
399
|
+
href
|
|
400
|
+
]) ? isDefined(link) ? resolveStoredLink(link, pathname) : isDefined(route) ? readLinkState({ href: resolveRoute(route) }, pathname) : readLinkState({ href: stegaClean(href) ?? void 0 }, pathname) : createEmptyLinkState();
|
|
401
|
+
return isAsynchronous ? Promise.resolve(state) : state;
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
resolveLink,
|
|
405
|
+
resolveRoute,
|
|
406
|
+
routes,
|
|
407
|
+
routeParamsFragment,
|
|
408
|
+
linkFragment
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
export { defineLinkConfig };
|
|
412
|
+
|
|
413
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","names":["isDefined","SanityLinkDestination","createLinkFragment","routeParamsFragment","titleField","reference","filter","part","join","isDefined","logger","SanityLinkReference","isExpandedReference","reference","T","error","_ref","isDefined","readPath","stegaClean","logger","SanityLinkDocument","SanityLinkRouteDefinition","path","params","Record","SanityLinkRoutes","SanityLinkRouteParamNames","TPath","TParam","TRest","SanityCheckedLinkRoutes","TRoutes","K","P","SanityLinkRouteInput","_type","readDocumentParams","document","expressions","projected","_routeParams","Object","fromEntries","entries","map","param","expression","test","undefined","split","createRouteResolver","routes","routeParamsFragment","flatMap","type","route","projection","join","resolveRoute","destination","error","value","replaceAll","unresolved","match","isDefined","createUrl","href","origin","undefined","URL","resolveRelativeHref","url","pathname","search","hash","splitSegments","path","split","filter","segment","checkContainsActivePath","linkSegments","pageSegments","every","index","at","checkIsActivePath","join","isDevelopment","isDefined","readPath","stegaClean","defaultTitleField","logger","createLinkFragment","appendDestination","composeAnchorHref","composeEmailHref","composePhoneHref","isExpandedReference","createRouteResolver","SanityCheckedLinkRoutes","SanityLinkRouteInput","SanityLinkRoutes","checkContainsActivePath","checkIsActivePath","createUrl","resolveRelativeHref","SanityLink","SanityLinkDestination","SanityLinkDocument","linkDestinations","SanityLinkResolvers","href","link","Extract","TDocument","type","K","Promise","SanityCheckedLinkResolvers","TResolvers","SanityLinkResolverConfig","baseUrl","routes","TRoutes","resolvers","openExternalInNewTab","title","SanityLinkTitleConfig","field","resolver","document","SanityResolvedLink","label","download","SanityLinkState","resolvedLink","isExternal","opensNewTab","hasAnchor","containsActivePath","isActivePath","SanityLinkResolution","NonNullable","args","Exclude","TAnswer","PromiseLike","SanityResolveLinkProps","route","pathname","UncorrelatedResolver","readActiveState","state","check","Error","format","createTitleResolver","split","undefined","readRouteTable","isAsyncResolver","constructor","name","readDestination","stored","destination","find","candidate","error","pointsAt","TDestination","appendAuthoredDestination","anchor","searchParams","createEmptyLinkState","defineLinkConfig","linkConfig","titleField","resolveTitle","declaredResolvers","Partial","Record","page","url","email","phone","file","isAsynchronous","Object","values","some","origin","URL","TypeError","resolveRoute","routeParamsFragment","linkFragment","composeLink","reference","path","address","relativeHref","subject","asset","filename","originalFilename","encodeURIComponent","runResolver","readLinkState","includes","protocol","hash","resolveStoredLink","composed","readAnsweredState","answer","resolved","then","resolveLink","props","resolve"],"sources":["../src/groq/fragments.ts","../src/lib/references.ts","../src/lib/routes.ts","../src/lib/urls.ts","../src/lib/links.ts"],"sourcesContent":["import { isDefined } from \"@repo/lib/utils\";\n\nimport type { SanityLinkDestination } from \"@/types\";\n\n/**\n * Builds the GROQ that expands a link's destinations, so the document behind a page link and the\n * asset behind a file link both arrive with the link itself.\n *\n * @param routeParamsFragment - GROQ resolving route parameters onto the document a page link points at.\n * @param titleField - Field an internal link borrows its label from when none was written.\n * @returns GROQ conditional projections covering every destination that needs expanding.\n */\nexport function createLinkFragment(routeParamsFragment: string, titleField: string) {\n const reference = [\"_id\", \"_type\", titleField, routeParamsFragment].filter((part) => isDefined(part)).join(\", \");\n\n return [\n `type == \"${\"page\" satisfies SanityLinkDestination}\" => { ..., reference-> { ${reference} } }`,\n `type == \"${\"file\" satisfies SanityLinkDestination}\" => { ..., file { ..., asset-> } }`,\n ].join(\", \");\n}\n","import { isDefined } from \"@repo/lib/utils\";\n\nimport { logger } from \"@/config/defaults\";\nimport type { SanityLinkReference } from \"@/types\";\n\n/**\n * Checks whether a query expanded a reference into the document it points at. Resolution reports an\n * unexpanded one rather than working around it, because the fix belongs in the query.\n *\n * @param reference - The reference to check.\n * @returns True if the reference holds the document itself.\n */\nexport function isExpandedReference<T extends object>(reference: SanityLinkReference<T> | undefined): reference is T {\n if (!isDefined(reference)) return false;\n\n if (\"_ref\" in reference) {\n logger.error(\n `Reference \"${reference._ref}\" has not been expanded. See: https://www.sanity.io/docs/content-lake/how-queries-work#k8ca3cefc3a31`,\n );\n\n return false;\n }\n\n return true;\n}\n","import { isDefined, readPath } from \"@repo/lib/utils\";\nimport { stegaClean } from \"@sanity/client/stega\";\n\nimport { logger } from \"@/config/defaults\";\nimport type { SanityLinkDocument } from \"@/types\";\n\n/**\n * One entry in a site's route table, pairing the path a document is served at with the GROQ that\n * fills the parameters in it. Keys beyond these two are kept as written and mean nothing here.\n *\n * @public\n */\nexport interface SanityLinkRouteDefinition {\n /** URL path pattern, in which every `[name]` segment is a parameter. */\n path: string;\n /** GROQ expression resolving each path parameter against the documents the route renders. */\n params?: Record<string, string>;\n}\n\n/**\n * Route definitions keyed by the document type each one renders.\n *\n * @public\n */\nexport type SanityLinkRoutes = Record<string, SanityLinkRouteDefinition>;\n\n/**\n * Parameter names a path pattern declares, so `\"/[category]/[slug]\"` reads as `\"category\" | \"slug\"`.\n *\n */\nexport type SanityLinkRouteParamNames<TPath extends string> = TPath extends `${string}[${infer TParam}]${infer TRest}`\n ? TParam | SanityLinkRouteParamNames<TRest>\n : never;\n\n/**\n * A route table checked against the patterns it declares, so every parameter a path names has a GROQ\n * expression and nothing else does.\n *\n */\nexport type SanityCheckedLinkRoutes<TRoutes extends SanityLinkRoutes> = {\n [K in keyof TRoutes]: [SanityLinkRouteParamNames<TRoutes[K][\"path\"]>] extends [never]\n ? { params?: undefined }\n : {\n params: Record<SanityLinkRouteParamNames<TRoutes[K][\"path\"]>, string> & {\n [P in keyof TRoutes[K][\"params\"]]: P extends SanityLinkRouteParamNames<TRoutes[K][\"path\"]> ? string : never;\n };\n };\n};\n\n/**\n * Every route addressable without a document, each carrying the parameters its own path declares.\n *\n * @public\n */\nexport type SanityLinkRouteInput<TRoutes extends SanityLinkRoutes> = {\n [K in keyof TRoutes & string]: { _type: K } & Record<SanityLinkRouteParamNames<TRoutes[K][\"path\"]>, string>;\n}[keyof TRoutes & string];\n\n/**\n * Reads a route's parameter values off a fetched document: those a query projected into `_routeParams`\n * first, then any whose GROQ is a plain field path, read directly, so a query need only project the\n * parameters that follow a reference or compute a value.\n *\n * @param document - The document a route is being resolved for.\n * @param expressions - The GROQ expression each parameter is filled from.\n * @returns The parameter values that could be read.\n */\nfunction readDocumentParams(document: SanityLinkDocument, expressions: Record<string, string>) {\n const projected = document._routeParams ?? {};\n\n return Object.fromEntries(\n Object.entries(expressions).map(([param, expression]) => {\n if (isDefined(projected[param])) return [param, projected[param]];\n if (!/^[A-Za-z_][\\w.]*$/u.test(expression)) return [param, undefined];\n\n return [param, readPath(document, expression.split(\".\"))];\n }),\n );\n}\n\n/**\n * Binds a site's route table to the functions that read it, so a path is declared once and both the\n * query filling its parameters and the resolver spending them follow the same declaration.\n *\n * @param routes - Route definitions keyed by the document type each one renders.\n * @returns An object holding the route resolver and the GROQ that feeds it.\n */\nexport function createRouteResolver<TRoutes extends SanityLinkRoutes>(routes: TRoutes) {\n const routeParamsFragment = Object.entries(routes)\n .flatMap(([type, route]) => {\n const projection = Object.entries(route.params ?? {})\n .map(([param, expression]) => `\"${param}\": ${expression}`)\n .join(\", \");\n\n return isDefined(projection) ? [`_type == \"${type}\" => { \"_routeParams\": { ${projection} } }`] : [];\n })\n .join(\", \");\n\n /**\n * Resolves a document, or a route named in code, into the path it is served at.\n *\n * @param destination - The document or route to resolve.\n * @returns The path, or undefined when the type has no route or a parameter has no value.\n */\n function resolveRoute(destination: SanityLinkRouteInput<TRoutes> | SanityLinkDocument) {\n const type: string = stegaClean(destination._type);\n const route = routes[type];\n\n if (!isDefined(route)) {\n logger.error(`No route is configured for the \"${type}\" type, so a link to it leads nowhere.`);\n\n return undefined;\n }\n\n const params: Record<string, unknown> =\n \"_id\" in destination ? readDocumentParams(destination, route.params ?? {}) : destination;\n\n let { path } = route;\n for (const [param, value] of Object.entries(params)) {\n if (typeof value === \"string\") path = path.replaceAll(`[${param}]`, stegaClean(value));\n }\n\n const unresolved = path.match(/\\[[^\\]]+\\]/gu);\n\n if (isDefined(unresolved)) {\n logger.error(\n `Could not resolve ${unresolved.join(\", \")} for the \"${type}\" route. The fetching query must spread \\`routeParamsFragment\\`, and the document must hold a value for every parameter.`,\n );\n\n return undefined;\n }\n\n return path;\n }\n\n return { resolveRoute, routeParamsFragment };\n}\n","import { isDefined } from \"@repo/lib/utils\";\n\n/**\n * Creates a URL from an address, resolving a root-relative one against the site it belongs to.\n *\n * @param href - The address to read.\n * @param origin - Origin the site is served from.\n * @returns The URL, or undefined when the address cannot be read as one.\n */\nexport function createUrl(href: string | undefined, origin: string) {\n if (!isDefined(href)) return undefined;\n\n try {\n return new URL(href, origin);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Reduces an absolute address pointing back at the site to a root-relative one, so an address an\n * author pasted in full still routes on the client rather than reloading the page.\n *\n * @param href - The absolute address to reduce.\n * @param origin - Origin the site is served from.\n * @returns A root-relative address, or undefined when the address points elsewhere.\n */\nexport function resolveRelativeHref(href: string, origin: string) {\n try {\n const url = new URL(href);\n if (url.origin !== origin) return undefined;\n\n return url.pathname + url.search + url.hash;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Splits a path into the segments that carry it, discarding the empty ones a leading or trailing\n * slash leaves behind.\n *\n * @param path - The path to split.\n * @returns The segments the path is built from.\n */\nfunction splitSegments(path: string) {\n return path.split(\"/\").filter((segment) => isDefined(segment));\n}\n\n/**\n * Checks whether a URL covers the page being read, matching every segment it declares against the\n * start of the current path. The root is compared exactly, since it precedes every path and would\n * otherwise cover every page on the site.\n *\n * @param url - The URL to compare.\n * @param pathname - Path of the page being read.\n * @param origin - Origin the site is served from.\n * @returns Whether the URL is the current page or one of the paths above it.\n */\nexport function checkContainsActivePath(url: URL, pathname: string, origin: string) {\n if (url.origin !== origin) return false;\n\n const linkSegments = splitSegments(url.pathname);\n const pageSegments = splitSegments(pathname);\n if (!isDefined(linkSegments)) return !isDefined(pageSegments);\n\n return linkSegments.every((segment, index) => pageSegments.at(index) === segment);\n}\n\n/**\n * Checks whether a URL points at the page being read and nothing else. An address carrying an anchor\n * names a location within a page rather than the page itself, so it is never the page already open.\n *\n * @param url - The URL to compare.\n * @param pathname - Path of the page being read.\n * @param origin - Origin the site is served from.\n * @returns Whether the URL matches the current page exactly.\n */\nexport function checkIsActivePath(url: URL, pathname: string, origin: string) {\n if (url.origin !== origin || isDefined(url.hash)) return false;\n\n return splitSegments(pathname).join(\"/\") === splitSegments(url.pathname).join(\"/\");\n}\n","import { isDevelopment } from \"@repo/lib/environment\";\nimport { isDefined, readPath } from \"@repo/lib/utils\";\nimport { stegaClean } from \"@sanity/client/stega\";\n\nimport { defaultTitleField, logger } from \"@/config/defaults\";\nimport { createLinkFragment } from \"@/groq/fragments\";\nimport { appendDestination, composeAnchorHref, composeEmailHref, composePhoneHref } from \"@/lib/destinations\";\nimport { isExpandedReference } from \"@/lib/references\";\nimport { createRouteResolver } from \"@/lib/routes\";\nimport type { SanityCheckedLinkRoutes, SanityLinkRouteInput, SanityLinkRoutes } from \"@/lib/routes\";\nimport { checkContainsActivePath, checkIsActivePath, createUrl, resolveRelativeHref } from \"@/lib/urls\";\nimport type { SanityLink, SanityLinkDestination, SanityLinkDocument } from \"@/types\";\nimport { linkDestinations } from \"@/types\";\n\n/**\n * A hook run after a link has been resolved, one per kind of destination, receiving the address the\n * plugin built and the stored link it was built from. Returning nothing leaves the link pointing\n * nowhere, as an unresolvable route does.\n *\n * @public\n */\nexport type SanityLinkResolvers<TDocument extends SanityLinkDocument = SanityLinkDocument> = {\n [K in SanityLinkDestination]?: (\n href: string,\n link: Extract<SanityLink<TDocument>, { type: K }>,\n ) => string | undefined | Promise<string | undefined>;\n};\n\n/**\n * Resolvers checked against the destinations a link may point at, so a key naming anything else is\n * rejected rather than quietly never run.\n *\n */\nexport type SanityCheckedLinkResolvers<TResolvers, TDocument extends SanityLinkDocument = SanityLinkDocument> = {\n [K in keyof TResolvers]: K extends SanityLinkDestination\n ? (\n href: string,\n link: Extract<SanityLink<TDocument>, { type: K }>,\n ) => string | undefined | Promise<string | undefined>\n : never;\n};\n\n/**\n * Everything the resolver needs to know about a site before it can resolve a link against it. A site\n * whose documents carry more than the plugin reads narrows the document type as well.\n *\n * @public\n */\nexport interface SanityLinkResolverConfig<\n TRoutes extends SanityLinkRoutes = SanityLinkRoutes,\n TDocument extends SanityLinkDocument = SanityLinkDocument,\n TResolvers extends SanityLinkResolvers<TDocument> = SanityLinkResolvers<TDocument>,\n> {\n /** Absolute address the site is served from, which decides what counts as leaving it. */\n baseUrl: string;\n /** Path patterns keyed by the document type each one renders. Page links lead nowhere when omitted. */\n routes?: TRoutes & SanityCheckedLinkRoutes<TRoutes>;\n /** Hooks having the last word on an address, one per kind of destination. */\n resolvers?: TResolvers & SanityCheckedLinkResolvers<TResolvers, TDocument>;\n /** Whether links leaving the site open in a new browser tab. Enabled when omitted. */\n openExternalInNewTab?: boolean;\n /** Where a page's title comes from, for the label an internal link borrows when none was written. */\n title?: SanityLinkTitleConfig<TDocument>;\n}\n\n/**\n * Where a page's title is read from, both by the fragment that fetches it and by the resolver that\n * labels a link with it.\n *\n * @public\n */\nexport interface SanityLinkTitleConfig<TDocument extends SanityLinkDocument = SanityLinkDocument> {\n /** Field the link fragment fetches from a page. Reads `title` when omitted. */\n field?: string;\n /** Reads the title from a fetched page. Reads `field` when omitted. */\n resolver?: (document: TDocument) => string | undefined;\n}\n\n/**\n * A link resolved into the parts an anchor is drawn from.\n *\n * @public\n */\nexport interface SanityResolvedLink {\n /** Address the link points at. */\n href?: string;\n /** Text a visitor reads, written by the author or borrowed from the destination. */\n label?: string;\n /** Whether the link serves a file rather than opening a page. */\n download?: boolean;\n}\n\n/**\n * A resolved link alongside the navigation state around it, all of which a site needs to draw the\n * link correctly rather than merely point it somewhere.\n *\n * @public\n */\nexport interface SanityLinkState {\n /** The link resolved into the parts an anchor is drawn from, absent when it leads nowhere usable. */\n resolvedLink: SanityResolvedLink | undefined;\n /** Whether the destination is a web address on another site. */\n isExternal: boolean;\n /** Whether the link should open in a new browser tab. */\n opensNewTab: boolean;\n /** Whether the destination names a location within a page rather than the page itself. */\n hasAnchor: boolean;\n /** Whether the destination is the page being read or one of the paths above it. */\n containsActivePath: boolean;\n /** Whether the destination is the page being read and nothing else. */\n isActivePath: boolean;\n}\n\n/**\n * What a resolution hands back, which is a promise when a declared resolver can only answer with one\n * and the navigation state itself otherwise. A resolver that might answer either way reads as the\n * synchronous form, since its declaration decides nothing.\n *\n * @public\n */\nexport type SanityLinkResolution<TResolvers> = [\n {\n [K in keyof TResolvers]-?: NonNullable<TResolvers[K]> extends (...args: never[]) => infer TAnswer\n ? [Exclude<TAnswer, PromiseLike<unknown>>] extends [never]\n ? true\n : never\n : never;\n }[keyof TResolvers],\n] extends [never]\n ? SanityLinkState\n : Promise<SanityLinkState>;\n\n/**\n * A link to resolve, taken from the first source declared, and the page it is being drawn on.\n *\n * @public\n */\nexport interface SanityResolveLinkProps<\n TRoutes extends SanityLinkRoutes = SanityLinkRoutes,\n TDocument extends SanityLinkDocument = SanityLinkDocument,\n> {\n /** Stored link an author authored. */\n link?: SanityLink<TDocument> | null | undefined;\n /** Route named in code, for a destination no author authored. */\n route?: SanityLinkRouteInput<TRoutes> | null | undefined;\n /** Address to point at as it is. */\n href?: string | null | undefined;\n /** Path of the page being drawn, compared against the link to read its navigation state. Reading a navigation state without it throws in development. */\n pathname?: string;\n}\n\n/** A resolver read back from the table, where the key no longer says which link it receives. */\ntype UncorrelatedResolver = (href: string, link: never) => unknown;\n\n/**\n * Answers a navigation state from the current path, or refuses when no path was given, since a state\n * read without one would silently render every link inactive. Production answers false instead of\n * throwing, so a missing path never takes a page down.\n *\n * @param pathname - Path of the page being drawn, or undefined when none was given.\n * @param state - Name of the state being read, for the message.\n * @param check - Reads the state from the path.\n * @returns Whether the state holds.\n * @throws In development, when the state is read without a pathname.\n */\nfunction readActiveState(pathname: string | undefined, state: string, check: (pathname: string) => boolean) {\n if (isDefined(pathname)) return check(pathname);\n if (isDevelopment) {\n throw new Error(logger.format(`Reading \\`${state}\\` needs \\`pathname\\` to be passed to \\`resolveLink\\`.`));\n }\n\n return false;\n}\n\n/**\n * Builds the title reader used when a site gives a field but no resolver, so naming the field once\n * covers both the fragment and the label.\n *\n * @param field - Dotted path to the field a page's title lives in.\n * @returns A reader returning the title, or undefined when the document holds no readable one.\n */\nfunction createTitleResolver(field: string) {\n return (document: SanityLinkDocument) => {\n const title = readPath(document, field.split(\".\"));\n\n return typeof title === \"string\" ? title : undefined;\n };\n}\n\n/**\n * Reads the route table a configuration declared, standing an empty one in where it declared none so\n * that a table is read back either way.\n *\n * @param routes - The route definitions a configuration declared.\n * @returns The table as it was declared, or an empty table.\n */\nfunction readRouteTable<TRoutes extends SanityLinkRoutes>(routes: TRoutes | undefined): TRoutes;\nfunction readRouteTable(routes: SanityLinkRoutes | undefined) {\n return routes ?? {};\n}\n\n/**\n * Checks whether a resolver was declared to answer with a promise, which is known of it before it has\n * ever run.\n *\n * @param resolver - The resolver to check.\n * @returns True if the resolver was declared asynchronous.\n */\nfunction isAsyncResolver(resolver: UncorrelatedResolver | undefined) {\n return isDefined(resolver) && resolver.constructor.name === \"AsyncFunction\";\n}\n\n/**\n * Reads which destination a stored link points at, clearing the stega characters a Sanity fetch leaves\n * on the value, so a link fetched in Presentation mode resolves the way it does anywhere else.\n *\n * @param link - The stored link to read.\n * @returns The destination, or undefined when the link stores none or one the plugin does not know.\n */\nfunction readDestination(link: SanityLink) {\n const stored = stegaClean(link.type);\n if (!isDefined(stored)) return undefined;\n\n const destination = linkDestinations.find((candidate) => candidate === stored);\n if (!isDefined(destination)) {\n logger.error(\n `A link stores \"${stored}\" as its destination, which is not one the plugin resolves, so it leads nowhere.`,\n );\n }\n\n return destination;\n}\n\n/**\n * Checks whether a stored link points at a given destination, narrowing it to that destination's\n * fields. The comparison clears stega characters first, so it holds in Presentation mode too.\n *\n * @param link - The stored link to check.\n * @param destination - The destination to check for.\n * @returns True if the link points at that destination.\n */\nfunction pointsAt<TDocument extends SanityLinkDocument, TDestination extends SanityLinkDestination>(\n link: SanityLink<TDocument>,\n destination: TDestination,\n): link is Extract<SanityLink<TDocument>, { type: TDestination }> {\n return stegaClean(link.type) === destination;\n}\n\n/**\n * Adds the anchor and parameters an author wrote onto a page's address, once whatever decides that\n * address has had its say.\n *\n * @param link - The stored link the address was built from.\n * @param resolvedLink - The resolved link to finish.\n * @returns The resolved link carrying the author's additions, or undefined when it leads nowhere.\n */\nfunction appendAuthoredDestination(link: SanityLink, resolvedLink: SanityResolvedLink | undefined) {\n if (!pointsAt(link, \"page\") || !isDefined(resolvedLink?.href)) return resolvedLink;\n\n const { anchor, searchParams } = link;\n\n return { ...resolvedLink, href: appendDestination(resolvedLink.href, anchor, searchParams) };\n}\n\n/**\n * Builds the state a link takes when it leads nowhere, so every unusable destination reads the same\n * way to the site drawing it.\n *\n * @returns Navigation state describing a link that points at nothing.\n */\nfunction createEmptyLinkState(): SanityLinkState {\n return {\n resolvedLink: undefined,\n isExternal: false,\n opensNewTab: false,\n hasAnchor: false,\n containsActivePath: false,\n isActivePath: false,\n };\n}\n\n/**\n * Binds a site's routing and conventions to the resolver, so a link is resolved the same way\n * everywhere it is drawn.\n *\n * @param linkConfig - What the resolver needs to know about the site.\n * @returns An object holding the resolver, the route table, and the GROQ the two of them rely on.\n * @public\n * @throws If the base address is not an absolute one.\n */\nexport function defineLinkConfig<\n const TRoutes extends SanityLinkRoutes = SanityLinkRoutes,\n TDocument extends SanityLinkDocument = SanityLinkDocument,\n TResolvers extends SanityLinkResolvers<TDocument> = SanityLinkResolvers<TDocument>,\n>(linkConfig: SanityLinkResolverConfig<TRoutes, TDocument, TResolvers>) {\n const {\n baseUrl,\n resolvers,\n openExternalInNewTab = true,\n title: { field: titleField = defaultTitleField, resolver: resolveTitle = createTitleResolver(titleField) } = {},\n } = linkConfig;\n\n const routes = readRouteTable(linkConfig.routes);\n\n // Reading a resolver by the type a link stores loses which link it was declared against, so calls go through `runResolver`.\n const declaredResolvers: Partial<Record<SanityLinkDestination, UncorrelatedResolver>> = {\n page: resolvers?.page,\n anchor: resolvers?.anchor,\n url: resolvers?.url,\n email: resolvers?.email,\n phone: resolvers?.phone,\n file: resolvers?.file,\n };\n\n // A resolver declared async is known to be one before it runs; one merely answering with a promise is known once it has.\n let isAsynchronous = Object.values(declaredResolvers).some((resolver) => isAsyncResolver(resolver));\n\n // Thrown rather than reported, because a base URL that cannot be read leaves every link on the site undecidable.\n const { origin } = (() => {\n try {\n return new URL(baseUrl);\n } catch {\n throw new TypeError(\n logger.format(`A link configuration needs an absolute base URL, and \"${baseUrl}\" is not one.`),\n );\n }\n })();\n\n const { resolveRoute, routeParamsFragment } = createRouteResolver(routes);\n const linkFragment = createLinkFragment(routeParamsFragment, titleField);\n\n /**\n * Resolves a stored link into the parts an anchor is drawn from, according to the kind of\n * destination it points at. A page's anchor and parameters are left off, since a resolver reads the\n * path before an author's additions rather than after them.\n *\n * @param link - The stored link to resolve.\n * @returns The resolved link, or undefined when it leads nowhere usable.\n */\n function composeLink(link: SanityLink<TDocument>): SanityResolvedLink | undefined {\n if (pointsAt(link, \"page\")) {\n const { reference, label } = link;\n if (!isExpandedReference(reference)) return undefined;\n\n const path = resolveRoute(reference);\n if (!isDefined(path)) return undefined;\n\n return { href: path, label: label ?? resolveTitle(reference) };\n }\n\n if (pointsAt(link, \"anchor\")) {\n const { anchor, label } = link;\n\n return { href: composeAnchorHref(anchor), label };\n }\n\n if (pointsAt(link, \"url\")) {\n const { url, label } = link;\n const address = stegaClean(url);\n const relativeHref = isDefined(address) ? resolveRelativeHref(address, origin) : undefined;\n\n // An address pointing back at this site is external in the authoring only, so it routes as internal.\n return { href: relativeHref ?? address, label };\n }\n\n if (pointsAt(link, \"email\")) {\n const { email, subject, label } = link;\n\n return { href: composeEmailHref(email, subject), label };\n }\n\n if (pointsAt(link, \"phone\")) {\n const { phone, label } = link;\n\n return { href: composePhoneHref(phone), label };\n }\n\n if (pointsAt(link, \"file\")) {\n const { file, label } = link;\n if (!isExpandedReference(file?.asset)) return undefined;\n\n const address = stegaClean(file.asset.url);\n const filename = stegaClean(file.asset.originalFilename);\n const href =\n isDefined(address) && isDefined(filename) ? `${address}?dl=${encodeURIComponent(filename)}` : address;\n\n return { href, label, download: true };\n }\n\n return undefined;\n }\n\n /**\n * Hands an address to the resolver its own destination declared, which has the last word on where\n * the link points.\n *\n * @param link - The stored link being resolved.\n * @param href - The address the plugin built.\n * @returns The address to use, undefined for a link leading nowhere, or a promise of either.\n */\n function runResolver(link: SanityLink<TDocument>, href: string): string | undefined | Promise<string | undefined> {\n if (pointsAt(link, \"page\")) {\n return resolvers?.page?.(href, link);\n }\n if (pointsAt(link, \"anchor\")) {\n return resolvers?.anchor?.(href, link);\n }\n if (pointsAt(link, \"url\")) {\n return resolvers?.url?.(href, link);\n }\n if (pointsAt(link, \"email\")) {\n return resolvers?.email?.(href, link);\n }\n if (pointsAt(link, \"phone\")) {\n return resolvers?.phone?.(href, link);\n }\n if (pointsAt(link, \"file\")) {\n return resolvers?.file?.(href, link);\n }\n\n return undefined;\n }\n\n /**\n * Reads how a resolved link stands against the page being drawn, which is what a site needs beyond\n * the address itself.\n *\n * @param resolvedLink - The resolved link to read.\n * @param pathname - Path of the page being drawn.\n * @returns The resolved link and the navigation state around it.\n */\n function readLinkState(resolvedLink: SanityResolvedLink | undefined, pathname: string | undefined): SanityLinkState {\n const url = createUrl(resolvedLink?.href, origin);\n\n if (!isDefined(url)) {\n if (isDefined(resolvedLink)) logger.error(\"Could not resolve an address from the given link, route, or href.\");\n\n return createEmptyLinkState();\n }\n\n const isExternal = [\"http:\", \"https:\"].includes(url.protocol) && url.origin !== origin;\n\n return {\n resolvedLink,\n isExternal,\n opensNewTab: isExternal && resolvedLink?.download !== true && openExternalInNewTab,\n hasAnchor: isDefined(url.hash),\n // Getters, so a navigation state read without a pathname fails at the read rather than passing as inactive.\n get containsActivePath() {\n return readActiveState(pathname, \"containsActivePath\", (path) => checkContainsActivePath(url, path, origin));\n },\n get isActivePath() {\n return readActiveState(pathname, \"isActivePath\", (path) => checkIsActivePath(url, path, origin));\n },\n };\n }\n\n /**\n * Resolves a stored link, offering the address it built to the resolver that destination declared\n * before the author's own additions go back on.\n *\n * @param link - The stored link to resolve.\n * @param pathname - Path of the page being drawn.\n * @returns The navigation state, or a promise of it when the resolver answers with one.\n */\n function resolveStoredLink(\n link: SanityLink<TDocument>,\n pathname: string | undefined,\n ): SanityLinkState | Promise<SanityLinkState> {\n const destination = readDestination(link);\n if (!isDefined(destination)) return createEmptyLinkState();\n\n const composed = composeLink(link);\n const { href } = composed ?? {};\n\n if (!isDefined(href) || !isDefined(declaredResolvers[destination])) {\n return readLinkState(appendAuthoredDestination(link, composed), pathname);\n }\n\n /**\n * Puts a resolver's answer in place of the address the plugin built.\n *\n * @param answer - The address the resolver answered with.\n * @returns The navigation state around the resolved link.\n */\n function readAnsweredState(answer: string | undefined) {\n const resolved = isDefined(answer) ? { ...composed, href: answer } : undefined;\n\n return readLinkState(appendAuthoredDestination(link, resolved), pathname);\n }\n\n const answer = runResolver(link, href);\n if (!(answer instanceof Promise)) return readAnsweredState(answer);\n\n isAsynchronous = true;\n\n return answer.then((resolved) => readAnsweredState(resolved));\n }\n\n /**\n * Resolves a link from the first source given, reading both where it leads and how it should be\n * drawn against the page it appears on.\n *\n * @returns The resolved link and the navigation state around it, as a promise when any declared\n * resolver answers with one.\n */\n function resolveLink(props: SanityResolveLinkProps<TRoutes, TDocument>): SanityLinkResolution<TResolvers>;\n function resolveLink({ link, route, href, pathname }: SanityResolveLinkProps<TRoutes, TDocument>) {\n const state = ((): SanityLinkState | Promise<SanityLinkState> => {\n if (!isDefined([link, route, href])) return createEmptyLinkState();\n if (isDefined(link)) return resolveStoredLink(link, pathname);\n if (isDefined(route)) return readLinkState({ href: resolveRoute(route) }, pathname);\n\n return readLinkState({ href: stegaClean(href) ?? undefined }, pathname);\n })();\n\n return isAsynchronous ? Promise.resolve(state) : state;\n }\n\n return { resolveLink, resolveRoute, routes, routeParamsFragment, linkFragment };\n}\n"],"mappings":"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"}
|