@escape-game-over/atlas 0.1.32 → 0.1.34

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escape-game-over/atlas",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "type": "module",
5
5
  "description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
6
6
  "private": false,
@@ -1,3 +1,4 @@
1
+ import type { NonEmpty } from "../types.ts";
1
2
  import { warn } from "../warn.ts";
2
3
  import { type AnalyticsTags, literal, preconnect } from "./tags.ts";
3
4
 
@@ -129,7 +130,7 @@ export interface GoogleSettings {
129
130
  * banner that loads slowly. See `DENIED_BY_DEFAULT` for exactly what the
130
131
  * unstated case emits.
131
132
  */
132
- readonly consent?: readonly [ConsentDefaults, ...ConsentDefaults[]];
133
+ readonly consent?: NonEmpty<ConsentDefaults>;
133
134
  }
134
135
 
135
136
  /**
@@ -1,4 +1,5 @@
1
1
  import type { MetaTag } from "../meta/tag.ts";
2
+ import type { NonEmpty } from "../types.ts";
2
3
  import { type HttpsUrl, joinUrl, type UrlPath } from "../url.ts";
3
4
  import { warn } from "../warn.ts";
4
5
  import { preconnect } from "./tags.ts";
@@ -142,7 +143,7 @@ export interface UmamiSettings {
142
143
  * `www.` traffic of every site serving both. Stating it is a moment's
143
144
  * thought; the alternative is a number that reads low forever.
144
145
  */
145
- readonly domains: readonly [string, ...string[]];
146
+ readonly domains: NonEmpty<string>;
146
147
  /**
147
148
  * Where the collected data is *sent*, if not where the script came from.
148
149
  *
package/src/hours.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Digit } from "./types.ts";
1
+ import type { Digit, NonEmpty } from "./types.ts";
2
2
 
3
3
  /**
4
4
  * When a venue is open, as a week rather than a sentence.
@@ -69,7 +69,7 @@ export interface TimeRange {
69
69
  *
70
70
  * Several spans cover a venue that shuts for the afternoon.
71
71
  */
72
- export type DayHours = "closed" | readonly [TimeRange, ...TimeRange[]];
72
+ export type DayHours = "closed" | NonEmpty<TimeRange>;
73
73
 
74
74
  /**
75
75
  * The whole week, every day stated.
@@ -83,7 +83,7 @@ export type WeeklyHours = Readonly<Record<Weekday, DayHours>>;
83
83
  /** Days that share the same hours, ready to be printed or serialised. */
84
84
  export interface HoursGroup {
85
85
  /** Every day in the group, in week order. */
86
- readonly days: readonly [Weekday, ...Weekday[]];
86
+ readonly days: NonEmpty<Weekday>;
87
87
  /**
88
88
  * The same days as consecutive spans: `[["monday", "thursday"],
89
89
  * ["sunday", "sunday"]]` is what a footer renders as "Mon–Thu & Sun".
package/src/index.ts CHANGED
@@ -160,6 +160,8 @@ export {
160
160
  type ArticleContent,
161
161
  buildMeta,
162
162
  buildNotFoundMeta,
163
+ fitDescription,
164
+ fitTitle,
163
165
  type MetaContent,
164
166
  type MetaTag,
165
167
  type OpenGraphType,
@@ -232,7 +234,7 @@ export type {
232
234
  StaticPath,
233
235
  } from "./site/index.ts";
234
236
  export type { Sitemap, SitemapConfig } from "./sitemap.ts";
235
- export type { IsoDate, Letter, StringKeys } from "./types.ts";
237
+ export type { IsoDate, Letter, NonEmpty, StringKeys } from "./types.ts";
236
238
  export {
237
239
  absoluteUrl,
238
240
  type Hash,
@@ -7,6 +7,7 @@ import {
7
7
  } from "../contact.ts";
8
8
  import type { WeeklyHours } from "../hours.ts";
9
9
  import type { ImageAsset } from "../image.ts";
10
+ import type { NonEmpty } from "../types.ts";
10
11
  import { absoluteUrl, type HttpsUrl } from "../url.ts";
11
12
  import { businessId, type OrganizationId } from "./ids.ts";
12
13
  import { alternateName, type JsonLdNode, schemaType } from "./node.ts";
@@ -126,7 +127,7 @@ export interface LocalBusinessInput {
126
127
  * Reference:
127
128
  * <https://developers.google.com/search/docs/appearance/structured-data/local-business>
128
129
  */
129
- readonly type?: string | readonly [string, ...string[]];
130
+ readonly type?: string | NonEmpty<string>;
130
131
  /**
131
132
  * Pages elsewhere that are unmistakably this same venue.
132
133
  *
@@ -5,6 +5,7 @@ import {
5
5
  quantitiesOf,
6
6
  quantityRange,
7
7
  } from "../money.ts";
8
+ import type { NonEmpty } from "../types.ts";
8
9
  import type { JsonLdNode } from "./node.ts";
9
10
 
10
11
  /**
@@ -46,7 +47,7 @@ export function quantitativeValue(quantity: Quantity): JsonLdNode {
46
47
  * Reference: <https://schema.org/QuantitativeValue>
47
48
  */
48
49
  export function quantitiesFor(
49
- tiers: readonly [PriceTier, ...PriceTier[]]
50
+ tiers: NonEmpty<PriceTier>
50
51
  ): JsonLdNode | readonly JsonLdNode[] {
51
52
  return isContiguous(tiers)
52
53
  ? quantitativeValue(quantityRange(tiers))
package/src/meta/index.ts CHANGED
@@ -57,7 +57,12 @@ export type {
57
57
  ThemeColor,
58
58
  } from "./content.ts";
59
59
  export type { RobotsDirective, RobotsPolicy } from "./robots.ts";
60
- export { resolveShareImage, type ShareImage } from "./share-image.ts";
60
+ export {
61
+ fitDescription,
62
+ fitTitle,
63
+ resolveShareImage,
64
+ type ShareImage,
65
+ } from "./share-image.ts";
61
66
  export type { MetaTag } from "./tag.ts";
62
67
  export type { SiteVerification } from "./verification.ts";
63
68
 
@@ -1,4 +1,5 @@
1
1
  import type { ImageAsset } from "../image.ts";
2
+ import type { NonEmpty } from "../types.ts";
2
3
  import { absoluteUrl, type HttpsUrl } from "../url.ts";
3
4
  import { warn } from "../warn.ts";
4
5
  import { link, type MetaTag } from "./tag.ts";
@@ -138,6 +139,33 @@ export function warnIfClipped(
138
139
  warn(at, `${field} is ${value.length} characters, past ~${limit}: ${why}.`);
139
140
  }
140
141
 
142
+ /**
143
+ * The first of several wordings, longest first, within the limit. When none
144
+ * fits, the last one — and `warnIfClipped` reports it once it reaches
145
+ * `buildMeta`.
146
+ */
147
+ function firstFitting(
148
+ field: keyof typeof LENGTH_LIMITS,
149
+ variants: NonEmpty<string>
150
+ ): string {
151
+ const { limit } = LENGTH_LIMITS[field];
152
+ // `reduce` to the last, which a non-empty tuple always has; indexing it
153
+ // would type as possibly undefined.
154
+ return (
155
+ variants.find((text) => text.length <= limit) ??
156
+ variants.reduce((_, text) => text)
157
+ );
158
+ }
159
+
160
+ /** A title as the page will print it, suffix included. */
161
+ export function fitTitle(variants: NonEmpty<string>): string {
162
+ return firstFitting("title", variants);
163
+ }
164
+
165
+ export function fitDescription(variants: NonEmpty<string>): string {
166
+ return firstFitting("description", variants);
167
+ }
168
+
141
169
  /**
142
170
  * Warns about a share image rather than rejecting it.
143
171
  *
package/src/money.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { NonEmpty } from "./types.ts";
2
+
1
3
  /**
2
4
  * An ISO 4217 currency code: `"EUR"`, `"RON"`, `"GBP"`.
3
5
  *
@@ -230,7 +232,7 @@ export interface TieredPrice {
230
232
  * The bands, cheapest quantity first. Never empty — a product with no price
231
233
  * at all is an offer of nothing.
232
234
  */
233
- readonly tiers: readonly [PriceTier, ...PriceTier[]];
235
+ readonly tiers: NonEmpty<PriceTier>;
234
236
  /**
235
237
  * What the quantity counts: `"players"`, `"seats"`, `"nights"`.
236
238
  *
@@ -259,9 +261,7 @@ export function tierRange(tier: PriceTier): QuantityRange {
259
261
  * a caller wants is `formatQuantities` or `quantitiesFor`, which pick between
260
262
  * this and `quantitiesOf` by reading the tiers.
261
263
  */
262
- export function quantityRange(
263
- tiers: readonly [PriceTier, ...PriceTier[]]
264
- ): QuantityRange {
264
+ export function quantityRange(tiers: NonEmpty<PriceTier>): QuantityRange {
265
265
  const first = tierRange(tiers[0]);
266
266
  const last = tiers.at(-1);
267
267
  return {
@@ -285,9 +285,7 @@ export function quantityRange(
285
285
  * different things: a contiguous table is one `QuantitativeValue` with a min and
286
286
  * a max, and a table with holes has to enumerate.
287
287
  */
288
- export function quantitiesOf(
289
- tiers: readonly [PriceTier, ...PriceTier[]]
290
- ): readonly number[] {
288
+ export function quantitiesOf(tiers: NonEmpty<PriceTier>): readonly number[] {
291
289
  const sizes = new Set<number>();
292
290
  for (const tier of tiers) {
293
291
  const band = tierRange(tier);
@@ -305,9 +303,7 @@ export function quantitiesOf(
305
303
  * broken set has to be listed. Derived rather than declared, so it cannot
306
304
  * disagree with the tiers it describes.
307
305
  */
308
- export function isContiguous(
309
- tiers: readonly [PriceTier, ...PriceTier[]]
310
- ): boolean {
306
+ export function isContiguous(tiers: NonEmpty<PriceTier>): boolean {
311
307
  const sizes = quantitiesOf(tiers);
312
308
  const first = sizes[0];
313
309
  const last = sizes.at(-1);
@@ -335,7 +331,7 @@ export function isContiguous(
335
331
  * language and no opinion about dashes.
336
332
  */
337
333
  export function formatQuantities(
338
- tiers: readonly [PriceTier, ...PriceTier[]],
334
+ tiers: NonEmpty<PriceTier>,
339
335
  format: {
340
336
  /** Between the ends of a run: `2–6`. */
341
337
  readonly between?: string;
package/src/types.ts CHANGED
@@ -13,6 +13,9 @@
13
13
  */
14
14
  export type StringKeys<T> = keyof T & string;
15
15
 
16
+ /** A list with at least one item, so `list[0]` needs no `undefined` check. */
17
+ export type NonEmpty<T> = readonly [T, ...T[]];
18
+
16
19
  /**
17
20
  * `true` when `T` is exactly `never`.
18
21
  *