@escape-game-over/atlas 0.1.96 → 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 +30 -1
- package/package.json +1 -1
- package/src/content/rich.ts +47 -19
- 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
|
|
@@ -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/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/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>({
|