@escape-game-over/atlas 0.1.95 → 0.1.97
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/docs/rich-text.md +31 -2
- package/package.json +1 -1
- package/src/astro/Phone.astro +2 -2
- package/src/astro/RichText.astro +1 -1
- package/src/content/rich.ts +47 -19
- package/src/jsonld/business.ts +11 -2
- package/src/jsonld/ids.ts +17 -5
- package/src/site/api.ts +15 -2
- package/src/site/create.ts +7 -2
package/docs/rich-text.md
CHANGED
|
@@ -167,6 +167,34 @@ A caller that does can build one — `joinUrl(site.urlFor(id, locale), href)`.
|
|
|
167
167
|
address or number is written once. The number must be in international form —
|
|
168
168
|
`"+30 210 0000000"` — because a local one does not dial from abroad.
|
|
169
169
|
|
|
170
|
+
## Binding a slot once
|
|
171
|
+
|
|
172
|
+
A view that renders many messages through one call — a FAQ page reading every
|
|
173
|
+
answer in a loop — cannot pass a slot to some messages and not others: a
|
|
174
|
+
message that declares no slot refuses the argument, and one that does demands
|
|
175
|
+
it. Bind the slots when the `rich()` is made instead:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const rich = site.rich(locale, { contact: "contact", rooms: "rooms" });
|
|
179
|
+
|
|
180
|
+
rich("faq.hours.a"); // "…on our [a:contact]Contact page[/a]." needs nothing more
|
|
181
|
+
rich("faq.age.a"); // no slots, still no argument
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Nothing is loosened:
|
|
185
|
+
|
|
186
|
+
- **A bound destination is checked where it is bound**, against the routes this
|
|
187
|
+
project builds, as a call's would be.
|
|
188
|
+
- **A slot left unbound is still required** at the call, and still a compile
|
|
189
|
+
error without it — for a single key and for a union of keys alike, so a loop
|
|
190
|
+
over answers fails to compile if one answer names a slot nobody bound.
|
|
191
|
+
- **A call's own destination wins** over a bound one, should it pass the same
|
|
192
|
+
slot; the type does not offer a bound slot to the call, so this only matters
|
|
193
|
+
for a key that arrived as a plain `string`.
|
|
194
|
+
|
|
195
|
+
Copy still names a slot, never an address. Binding moves where the destination
|
|
196
|
+
is written — to the one place that renders the copy — not into the copy.
|
|
197
|
+
|
|
170
198
|
## What the compiler holds in step
|
|
171
199
|
|
|
172
200
|
Which marks a message uses is checked across locales by the same machinery that
|
|
@@ -253,7 +281,7 @@ per `[v:name]` and one for links — usually in a small wrapper
|
|
|
253
281
|
```
|
|
254
282
|
|
|
255
283
|
A `[tel]` link renders with `dir="ltr"`: a number has no letters to set its
|
|
256
|
-
direction, and on a right-to-left page it would otherwise read "
|
|
284
|
+
direction, and on a right-to-left page it would otherwise read "0100 555 202 1+".
|
|
257
285
|
A number on its own, outside copy, is `@escape-game-over/atlas/astro/phone`.
|
|
258
286
|
|
|
259
287
|
`attrs` puts attributes on a variant's run, so a script can find it and rewrite
|
|
@@ -266,7 +294,8 @@ class nor attrs still renders its words, and warns at build.
|
|
|
266
294
|
| --------------------------------------------------- | --------------------------------------------------------------------- |
|
|
267
295
|
| A locale dropping `[a:venue]` | compile error — the marks must match across a message |
|
|
268
296
|
| `[a:venue]` in one locale, `[a:booking]` in another | compile error — the argument is part of the token |
|
|
269
|
-
| `rich()` without a slot's target
|
|
297
|
+
| `rich()` without a slot's target, unbound | compile error, then a build error naming the slot |
|
|
298
|
+
| A slot bound to an unbuilt route | compile error where it is bound |
|
|
270
299
|
| A slot filled with an unbuilt route | compile error at the call site |
|
|
271
300
|
| `[Mon-Fri]` | build error, listing the marks and the `[[` escape |
|
|
272
301
|
| `[b]` never closed, `[/b]` never opened | build error naming the message and locale |
|
package/package.json
CHANGED
package/src/astro/Phone.astro
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* <a href={telHref(phone)}><Phone phone={phone} /></a>
|
|
7
7
|
* ```
|
|
8
8
|
*
|
|
9
|
-
* "+
|
|
10
|
-
* the browser would lay it out right to left and show "
|
|
9
|
+
* "+1 202 555 0100" has no letters to set its direction, so on an Arabic page
|
|
10
|
+
* the browser would lay it out right to left and show "0100 555 202 1+". Its own
|
|
11
11
|
* element with `dir="ltr"` keeps it whole, the way the page expects a number
|
|
12
12
|
* to read, and copies as just the number. An email needs none of this: its
|
|
13
13
|
* letters already make it read left to right.
|
package/src/astro/RichText.astro
CHANGED
|
@@ -82,7 +82,7 @@ const variantAttrs = (variant: string) =>
|
|
|
82
82
|
</a>
|
|
83
83
|
);
|
|
84
84
|
// A number has no letters to set its direction; on a right-to-left
|
|
85
|
-
// page it would read "
|
|
85
|
+
// page it would read "0100 555 202 1+". See `Phone.astro`.
|
|
86
86
|
case "tel":
|
|
87
87
|
return (
|
|
88
88
|
<a
|
package/src/content/rich.ts
CHANGED
|
@@ -179,6 +179,12 @@ export interface RichTextOptions<L extends string> {
|
|
|
179
179
|
* on a bad one. `joinUrl` already answers it once.
|
|
180
180
|
*/
|
|
181
181
|
readonly origin: HttpsUrl;
|
|
182
|
+
/**
|
|
183
|
+
* Destinations bound once, for every message this `rich()` reads: a slot
|
|
184
|
+
* named here is filled without the call passing it. Already checked by
|
|
185
|
+
* the type that built this; strings by the time they reach the runtime.
|
|
186
|
+
*/
|
|
187
|
+
readonly links?: Readonly<Record<string, string>>;
|
|
182
188
|
}
|
|
183
189
|
|
|
184
190
|
/** The link slots message `K` declares, e.g. `"venue"` from `[a:venue]`. */
|
|
@@ -200,8 +206,11 @@ export type RichParamsOf<
|
|
|
200
206
|
Catalog,
|
|
201
207
|
K extends StringKeys<Catalog>,
|
|
202
208
|
RouteId extends string,
|
|
209
|
+
Bound extends string = never,
|
|
203
210
|
> = Readonly<Record<MessageParamsOf<Catalog, K>, string>> &
|
|
204
|
-
Readonly<
|
|
211
|
+
Readonly<
|
|
212
|
+
Record<Exclude<LinkNamesOf<Catalog, K>, Bound>, LinkTarget<RouteId>>
|
|
213
|
+
>;
|
|
205
214
|
|
|
206
215
|
/**
|
|
207
216
|
* `[key]` for a message that needs nothing, `[key, params]` otherwise.
|
|
@@ -214,20 +223,26 @@ export type RichArgsOf<
|
|
|
214
223
|
Catalog,
|
|
215
224
|
K extends StringKeys<Catalog>,
|
|
216
225
|
RouteId extends string,
|
|
217
|
-
|
|
226
|
+
Bound extends string = never,
|
|
227
|
+
> = [
|
|
228
|
+
MessageParamsOf<Catalog, K> | Exclude<LinkNamesOf<Catalog, K>, Bound>,
|
|
229
|
+
] extends [never]
|
|
218
230
|
? [key: K]
|
|
219
|
-
: [key: K, params: RichParamsOf<Catalog, K, RouteId>];
|
|
231
|
+
: [key: K, params: RichParamsOf<Catalog, K, RouteId, Bound>];
|
|
220
232
|
|
|
221
233
|
/**
|
|
222
234
|
* `rich()`, keyed like `t()` and asking for a little more.
|
|
223
235
|
*
|
|
224
236
|
* Generic over the routes as well as the catalog, which is what lets a link
|
|
225
|
-
* slot be typed. `createSite` supplies both.
|
|
237
|
+
* slot be typed. `createSite` supplies both. `Bound` names the slots bound
|
|
238
|
+
* when this `rich()` was made, which a call no longer passes.
|
|
226
239
|
*/
|
|
227
|
-
export type RichTextFor<
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
240
|
+
export type RichTextFor<
|
|
241
|
+
Catalog,
|
|
242
|
+
RouteId extends string,
|
|
243
|
+
Bound extends string = never,
|
|
244
|
+
> = <K extends StringKeys<Catalog>>(
|
|
245
|
+
...args: RichArgsOf<Catalog, K, RouteId, Bound>
|
|
231
246
|
) => RichText;
|
|
232
247
|
|
|
233
248
|
/** `site.plain()`, keyed and parameterised exactly as `t()` is. */
|
|
@@ -252,11 +267,16 @@ export function createRichText<
|
|
|
252
267
|
Catalog,
|
|
253
268
|
L extends string,
|
|
254
269
|
RouteId extends string = string,
|
|
255
|
-
|
|
256
|
-
|
|
270
|
+
Bound extends string = never,
|
|
271
|
+
>(
|
|
272
|
+
options: RichTextOptions<L> & {
|
|
273
|
+
readonly links?: Readonly<Record<Bound, LinkTarget<RouteId>>>;
|
|
274
|
+
}
|
|
275
|
+
): RichTextFor<Catalog, RouteId, Bound> {
|
|
276
|
+
const { catalog, locale, link, origin, links } = options;
|
|
257
277
|
|
|
258
278
|
return <K extends StringKeys<Catalog>>(
|
|
259
|
-
...args: RichArgsOf<Catalog, K, RouteId>
|
|
279
|
+
...args: RichArgsOf<Catalog, K, RouteId, Bound>
|
|
260
280
|
): RichText => {
|
|
261
281
|
// The conditional tuple cannot be destructured directly; the runtime
|
|
262
282
|
// shape is always `[key]` or `[key, params]`. As in `createTranslate`.
|
|
@@ -267,7 +287,7 @@ export function createRichText<
|
|
|
267
287
|
const template = lookupTemplate(catalog, locale, String(key));
|
|
268
288
|
|
|
269
289
|
return parseMarks(template, at).map((span) =>
|
|
270
|
-
resolve(span, at, params, link, origin)
|
|
290
|
+
resolve(span, at, params, links, link, origin)
|
|
271
291
|
);
|
|
272
292
|
};
|
|
273
293
|
}
|
|
@@ -276,6 +296,7 @@ function resolve(
|
|
|
276
296
|
span: ParsedSpan,
|
|
277
297
|
at: string,
|
|
278
298
|
params: Readonly<Record<string, string>> | undefined,
|
|
299
|
+
links: Readonly<Record<string, string>> | undefined,
|
|
279
300
|
link: LinkResolver,
|
|
280
301
|
origin: HttpsUrl
|
|
281
302
|
): Span {
|
|
@@ -291,7 +312,7 @@ function resolve(
|
|
|
291
312
|
case "link":
|
|
292
313
|
return resolveLink(
|
|
293
314
|
text,
|
|
294
|
-
targetFor(span.name, at, params),
|
|
315
|
+
targetFor(span.name, at, params, links),
|
|
295
316
|
at,
|
|
296
317
|
link,
|
|
297
318
|
origin
|
|
@@ -346,15 +367,22 @@ function fill(
|
|
|
346
367
|
function targetFor(
|
|
347
368
|
name: string,
|
|
348
369
|
at: string,
|
|
349
|
-
params: Readonly<Record<string, string>> | undefined
|
|
370
|
+
params: Readonly<Record<string, string>> | undefined,
|
|
371
|
+
links: Readonly<Record<string, string>> | undefined
|
|
350
372
|
): string {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
373
|
+
// The call's own destination first, then one bound for every message.
|
|
374
|
+
const target = (() => {
|
|
375
|
+
if (params !== undefined && Object.hasOwn(params, name)) {
|
|
376
|
+
return params[name];
|
|
377
|
+
}
|
|
378
|
+
if (links !== undefined && Object.hasOwn(links, name)) {
|
|
379
|
+
return links[name];
|
|
380
|
+
}
|
|
381
|
+
return undefined;
|
|
382
|
+
})();
|
|
355
383
|
if (target === undefined) {
|
|
356
384
|
throw new Error(
|
|
357
|
-
`${at} has no destination for the link slot "[a:${name}]". Pass one: rich(key, { ${name}: "some-route-id" }).`
|
|
385
|
+
`${at} has no destination for the link slot "[a:${name}]". Pass one: rich(key, { ${name}: "some-route-id" }), or bind it: site.rich(locale, { ${name}: "some-route-id" }).`
|
|
358
386
|
);
|
|
359
387
|
}
|
|
360
388
|
return target;
|
package/src/jsonld/business.ts
CHANGED
|
@@ -15,8 +15,17 @@ import { geoCoordinates, openingHours, postalAddress } from "./place.ts";
|
|
|
15
15
|
|
|
16
16
|
export interface LocalBusinessInput {
|
|
17
17
|
readonly name: string;
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* The page about it: the site, or one location's page on a site with
|
|
20
|
+
* several. Its origin carries the node's identity, see `@id` below.
|
|
21
|
+
*/
|
|
19
22
|
readonly url: HttpsUrl;
|
|
23
|
+
/**
|
|
24
|
+
* Which of the site's businesses this is, on a site with several: `north`
|
|
25
|
+
* makes the `@id` `<origin>#business-north`. Point at it with
|
|
26
|
+
* `businessId(url, location)`.
|
|
27
|
+
*/
|
|
28
|
+
readonly location?: string;
|
|
20
29
|
readonly address: PostalAddress;
|
|
21
30
|
/**
|
|
22
31
|
* Another name this venue actually goes by.
|
|
@@ -162,7 +171,7 @@ export interface LocalBusinessInput {
|
|
|
162
171
|
export function localBusiness(business: LocalBusinessInput): JsonLdNode {
|
|
163
172
|
return {
|
|
164
173
|
"@type": schemaType(business.type ?? "LocalBusiness"),
|
|
165
|
-
"@id": businessId(business.url),
|
|
174
|
+
"@id": businessId(business.url, business.location),
|
|
166
175
|
name: business.name,
|
|
167
176
|
...alternateName(business.name, business.alternateName),
|
|
168
177
|
url: business.url,
|
package/src/jsonld/ids.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type HttpsUrl, normalizeOrigin } from "../url.ts";
|
|
1
|
+
import { type HttpsUrl, isHttpsUrl, normalizeOrigin } from "../url.ts";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The `@id` each addressable node takes, and the types that keep them apart.
|
|
@@ -43,7 +43,9 @@ import { type HttpsUrl, normalizeOrigin } from "../url.ts";
|
|
|
43
43
|
* -checks, while the mistake this exists to stop — passing a bare URL where a
|
|
44
44
|
* reference belongs — does not.
|
|
45
45
|
*/
|
|
46
|
-
export type BusinessId =
|
|
46
|
+
export type BusinessId =
|
|
47
|
+
| `${HttpsUrl}#business`
|
|
48
|
+
| `${HttpsUrl}#business-${string}`;
|
|
47
49
|
|
|
48
50
|
/** The identity of a brand node. See `BusinessId`. */
|
|
49
51
|
export type OrganizationId = `${HttpsUrl}#organization`;
|
|
@@ -68,15 +70,25 @@ export type WebsiteId = `${HttpsUrl}#website`;
|
|
|
68
70
|
export type ServiceId = `${HttpsUrl}#service`;
|
|
69
71
|
|
|
70
72
|
/**
|
|
71
|
-
* The `@id` a business node takes
|
|
73
|
+
* The `@id` a business node takes: on the origin of `url`, whichever page of
|
|
74
|
+
* the site that is.
|
|
72
75
|
*
|
|
73
76
|
* Exported because a reference has to be built by whoever points at it, and the
|
|
74
77
|
* fragment must match exactly — a node linking to `#local-business` when the
|
|
75
78
|
* business calls itself `#business` is a dangling reference that no validator
|
|
76
79
|
* reports and no crawler resolves. One definition, used by both ends.
|
|
80
|
+
*
|
|
81
|
+
* A site with several businesses — one per location — names each:
|
|
82
|
+
* `businessId(origin, "north")` is `<origin>#business-north`.
|
|
83
|
+
* Without a name they would all be `#business`, and a crawler merges nodes
|
|
84
|
+
* that share an id, so the locations would read as one place.
|
|
77
85
|
*/
|
|
78
|
-
export function businessId(url: HttpsUrl): BusinessId {
|
|
79
|
-
|
|
86
|
+
export function businessId(url: HttpsUrl, location?: string): BusinessId {
|
|
87
|
+
const origin = new URL(url).origin;
|
|
88
|
+
if (!isHttpsUrl(origin)) throw new Error(`"${url}" has no https origin.`);
|
|
89
|
+
return location === undefined
|
|
90
|
+
? `${origin}#business`
|
|
91
|
+
: `${origin}#business-${location}`;
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
/** The `@id` a brand node takes. See `businessId`. */
|
package/src/site/api.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { ResolvedLocaleMeta } from "../config.ts";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
LinkTarget,
|
|
4
|
+
PlainTextFor,
|
|
5
|
+
RichTextFor,
|
|
6
|
+
} from "../content/index.ts";
|
|
3
7
|
import type { GeneratedFile } from "../file.ts";
|
|
4
8
|
import type { PublicFilePath } from "../files.ts";
|
|
5
9
|
import type { Fonts } from "../fonts.ts";
|
|
@@ -174,8 +178,17 @@ export interface Site<
|
|
|
174
178
|
* Bound to a locale here rather than taking one per call, because a link
|
|
175
179
|
* inside the copy resolves to *this* language's path, and a `rich()` that
|
|
176
180
|
* took the locale late would be a second place to get that wrong.
|
|
181
|
+
*
|
|
182
|
+
* `links` binds slots once for every message read through it, for a view
|
|
183
|
+
* that renders many messages through one call: `{ contact: "contact" }`
|
|
184
|
+
* lets any of them write `[a:contact]` with nothing passed per call. The
|
|
185
|
+
* targets are checked here, as a call's would be; a slot left unbound is
|
|
186
|
+
* still required at the call, and still a compile error without it.
|
|
177
187
|
*/
|
|
178
|
-
rich
|
|
188
|
+
rich<const Bound extends string = never>(
|
|
189
|
+
locale: L,
|
|
190
|
+
links?: Readonly<Record<Bound, LinkTarget<RouteId>>>
|
|
191
|
+
): RichTextFor<Catalog, RouteId, Bound>;
|
|
179
192
|
|
|
180
193
|
/**
|
|
181
194
|
* A locale-bound `plain()`: the same catalog, read as words alone.
|
package/src/site/create.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
createPlainText,
|
|
10
10
|
createRichText,
|
|
11
11
|
type LinkResolver,
|
|
12
|
+
type LinkTarget,
|
|
12
13
|
} from "../content/index.ts";
|
|
13
14
|
import type { GeneratedFile } from "../file.ts";
|
|
14
15
|
import type { PublicFilePath } from "../files.ts";
|
|
@@ -837,12 +838,16 @@ export function createSite<
|
|
|
837
838
|
routes: enabledRouteIds,
|
|
838
839
|
|
|
839
840
|
translate: (locale) => translateFactory(catalog, locale),
|
|
840
|
-
rich: (
|
|
841
|
-
|
|
841
|
+
rich: <const Bound extends string = never>(
|
|
842
|
+
locale: L,
|
|
843
|
+
links?: Readonly<Record<Bound, LinkTarget<RouteId>>>
|
|
844
|
+
) =>
|
|
845
|
+
createRichText<Full, L, RouteId, Bound>({
|
|
842
846
|
catalog,
|
|
843
847
|
locale,
|
|
844
848
|
link: richLink(locale),
|
|
845
849
|
origin: siteUrl,
|
|
850
|
+
...(links === undefined ? {} : { links }),
|
|
846
851
|
}),
|
|
847
852
|
plain: (locale) =>
|
|
848
853
|
createPlainText<Full, L>({
|