@seamlet/astro 0.11.2 → 0.12.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 +18 -6
- package/components/RichLine.astro +2 -5
- package/components/RichText.astro +2 -4
- package/dist/bridge.d.ts +2 -3
- package/dist/bridge.js +598 -374
- package/dist/{chunk-DRDPKFHZ.js → chunk-7GYJS7ZT.js} +386 -203
- package/dist/{chunk-2E7VDSUK.js → chunk-MW3BE5O6.js} +34 -43
- package/dist/chunk-QCVWHHJC.js +14 -0
- package/dist/{chunk-P2OQOZBC.js → chunk-QL4K4HR6.js} +8 -0
- package/dist/{chunk-3BS5CE26.js → chunk-W2LIOR2C.js} +6 -2
- package/dist/cli.js +96 -37
- package/dist/client.js +4 -1
- package/dist/index.js +52 -19
- package/dist/{messages-Dx0jNy3N.d.ts → messages-CvZhLRSE.d.ts} +29 -52
- package/dist/{content-6k-E7NWf.d.ts → parse-content-Eg8yRq_t.d.ts} +69 -1
- package/dist/preview-requests.d.ts +6 -1
- package/dist/preview-requests.js +3 -1
- package/dist/protocol.d.ts +31 -5
- package/dist/protocol.js +4 -3
- package/dist/routes/media.js +26 -5
- package/dist/routes/own-photos.js +2 -1
- package/dist/schema.d.ts +13 -8
- package/dist/schema.js +13 -1
- package/dist/site-description-Cg5KNhhG.d.ts +112 -0
- package/dist/site.d.ts +2 -2
- package/dist/site.js +83 -9
- package/package.json +1 -1
- package/routes/preview.astro +8 -11
- package/dist/parse-content-C9ldnmvj.d.ts +0 -69
- package/dist/site-description-4Jx6Nq6X.d.ts +0 -36
|
@@ -168,6 +168,8 @@ declare function asOneParagraph(text: RichText): RichText;
|
|
|
168
168
|
declare function readRichText(input: unknown): RichText | undefined;
|
|
169
169
|
/** The same line with empty pieces left out and neighbors with the same marks joined. */
|
|
170
170
|
declare function normalizedLine(spans: readonly RichSpan[]): RichLine;
|
|
171
|
+
/** The lines of a text: its paragraphs and the items of its lists, in their order. */
|
|
172
|
+
declare function linesOf(text: RichText): readonly RichLine[];
|
|
171
173
|
|
|
172
174
|
/** The point of a photo that must stay in view when a site crops it: 0 to 1 from the left and the top. */
|
|
173
175
|
interface ImageFocus {
|
|
@@ -211,4 +213,70 @@ type FieldValue = string | number | boolean | null | readonly string[] | ImageVa
|
|
|
211
213
|
declare function defaultValue(field: Field): FieldValue;
|
|
212
214
|
declare function defaultContent(fields: Fields): Content;
|
|
213
215
|
|
|
214
|
-
|
|
216
|
+
/** Where a value lives in the content: field names and, inside a collection, record indexes. */
|
|
217
|
+
type FieldPath = readonly (string | number)[];
|
|
218
|
+
type ContentIssue = {
|
|
219
|
+
readonly path: FieldPath;
|
|
220
|
+
readonly code: "invalid-type";
|
|
221
|
+
} | {
|
|
222
|
+
readonly path: FieldPath;
|
|
223
|
+
readonly code: "invalid-date";
|
|
224
|
+
} | {
|
|
225
|
+
readonly path: FieldPath;
|
|
226
|
+
readonly code: "invalid-time";
|
|
227
|
+
} | {
|
|
228
|
+
readonly path: FieldPath;
|
|
229
|
+
readonly code: "too-long";
|
|
230
|
+
readonly maxLength: number;
|
|
231
|
+
} | {
|
|
232
|
+
readonly path: FieldPath;
|
|
233
|
+
readonly code: "below-min";
|
|
234
|
+
readonly min: number;
|
|
235
|
+
} | {
|
|
236
|
+
readonly path: FieldPath;
|
|
237
|
+
readonly code: "above-max";
|
|
238
|
+
readonly max: number;
|
|
239
|
+
} | {
|
|
240
|
+
readonly path: FieldPath;
|
|
241
|
+
readonly code: "missing-image-src";
|
|
242
|
+
} | {
|
|
243
|
+
readonly path: FieldPath;
|
|
244
|
+
readonly code: "missing-link-href";
|
|
245
|
+
} | {
|
|
246
|
+
readonly path: FieldPath;
|
|
247
|
+
readonly code: "unsupported-link-href";
|
|
248
|
+
} | {
|
|
249
|
+
readonly path: FieldPath;
|
|
250
|
+
readonly code: "unsupported-url";
|
|
251
|
+
} | {
|
|
252
|
+
readonly path: FieldPath;
|
|
253
|
+
readonly code: "unknown-choice";
|
|
254
|
+
readonly value: string;
|
|
255
|
+
} | {
|
|
256
|
+
readonly path: FieldPath;
|
|
257
|
+
readonly code: "missing-record";
|
|
258
|
+
readonly id: string;
|
|
259
|
+
};
|
|
260
|
+
interface ParsedContent<C> {
|
|
261
|
+
readonly content: C;
|
|
262
|
+
readonly issues: readonly ContentIssue[];
|
|
263
|
+
}
|
|
264
|
+
declare function formatFieldPath(path: FieldPath): string;
|
|
265
|
+
/**
|
|
266
|
+
* The length that `maxLength` limits: characters as people see them (grapheme clusters), so
|
|
267
|
+
* "й" and "👍🏽" are one character each, whatever number of code units they take.
|
|
268
|
+
*/
|
|
269
|
+
declare function countCharacters(text: string): number;
|
|
270
|
+
/**
|
|
271
|
+
* Brings any input to the shape of the schema, so the result is always content of that schema:
|
|
272
|
+
* - a field missing from the input (the schema grew after the content was saved) gets its default;
|
|
273
|
+
* - a value of the wrong type or format gets the default too, and an issue;
|
|
274
|
+
* - a value of the right type that breaks a rule of its field (too long, below the minimum) is
|
|
275
|
+
* kept, with an issue: nothing a person typed is lost;
|
|
276
|
+
* - keys the schema does not know are dropped; a record keeps its id (see withRecordIds);
|
|
277
|
+
* - a reference to a record its collection does not have (removed since) is kept, with an issue.
|
|
278
|
+
* Content with issues is valid to show and edit, but not to save.
|
|
279
|
+
*/
|
|
280
|
+
declare function parseContent<F extends Fields>(schema: F, input: unknown): ParsedContent<ContentOf<F>>;
|
|
281
|
+
|
|
282
|
+
export { plainTextOf as $, collection as A, countCharacters as B, type ContentOf as C, type DateField as D, date as E, type Fields as F, type GroupField as G, defaultContent as H, type ImageField as I, defaultValue as J, defineSchema as K, type LinkValue as L, formatFieldPath as M, type NumberField as N, type OptionValue as O, type ParsedContent as P, group as Q, type RichText as R, image as S, type TextField as T, type UrlField as U, type ValueOf as V, linesOf as W, link as X, normalizedLine as Y, number as Z, parseContent as _, type Content as a, readRichText as a0, reference as a1, references as a2, richText as a3, richTextOf as a4, text as a5, time as a6, toggle as a7, url as a8, type FieldPath as b, type ContentIssue as c, type ImageValue as d, type Field as e, type CollectionField as f, type RecordFields as g, type LinkField as h, type ChoicesField as i, type TimeField as j, type ChoiceField as k, type ChoiceOption as l, type FieldKind as m, type FieldValue as n, type ImageFocus as o, type RecordOf as p, type ReferenceField as q, type ReferencesField as r, type RichBlock as s, type RichLine as t, type RichSpan as u, type RichTextField as v, type ToggleField as w, asOneParagraph as x, choice as y, choices as z };
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/** Far more than the content of a site ever takes; a bigger body is not a draft. */
|
|
2
2
|
declare const MAX_DRAFT_BYTES: number;
|
|
3
|
+
/**
|
|
4
|
+
* What every answer of the preview says, a refusal too: only the CMS's editor frames it, and
|
|
5
|
+
* nobody indexes or keeps it.
|
|
6
|
+
*/
|
|
7
|
+
declare function previewHeadersOf(cmsOrigin: string): Readonly<Record<string, string>>;
|
|
3
8
|
type DraftReading = {
|
|
4
9
|
readonly isRead: true;
|
|
5
10
|
readonly draft: unknown;
|
|
@@ -15,4 +20,4 @@ declare function readDraft(request: Request): Promise<DraftReading>;
|
|
|
15
20
|
/** Whether a name can be a photo uploaded to the CMS: the hash of its bytes, as WebP. */
|
|
16
21
|
declare function isUploadedPhoto(fileName: string | undefined): fileName is string;
|
|
17
22
|
|
|
18
|
-
export { type DraftReading, MAX_DRAFT_BYTES, isUploadedPhoto, readDraft };
|
|
23
|
+
export { type DraftReading, MAX_DRAFT_BYTES, isUploadedPhoto, previewHeadersOf, readDraft };
|
package/dist/preview-requests.js
CHANGED
package/dist/protocol.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { T as
|
|
2
|
-
export { B as Box, a as EditorKey, E as EditorMessage, I as InlineEditingOutcome, L as LinkBox, M as
|
|
3
|
-
import { F as Fields, I as ImageField, R as RichText } from './content-
|
|
4
|
-
|
|
1
|
+
import { S as Shortcut, T as TextFormats } from './messages-CvZhLRSE.js';
|
|
2
|
+
export { B as Box, a as EditorKey, E as EditorMessage, I as InlineEditingOutcome, L as LinkBox, M as MarkerBox, b as PROTOCOL_VERSION, c as PageLayout, P as PageMessage, d as Point, e as TextFormat, f as TypedSelection, V as Viewport, r as readEditorMessage, g as readPageMessage } from './messages-CvZhLRSE.js';
|
|
3
|
+
import { F as Fields, b as FieldPath, I as ImageField, R as RichText } from './parse-content-Eg8yRq_t.js';
|
|
4
|
+
|
|
5
|
+
declare function shortcutOf(event: KeyboardEvent): Shortcut | undefined;
|
|
5
6
|
|
|
6
7
|
declare function carriesFiles(event: DragEvent): boolean;
|
|
7
8
|
|
|
@@ -17,6 +18,31 @@ declare function imageFieldAt(schema: Fields, path: string | undefined): {
|
|
|
17
18
|
*/
|
|
18
19
|
declare function closestMarkedPath(path: FieldPath, isMarked: (path: string) => boolean): string | undefined;
|
|
19
20
|
|
|
21
|
+
interface TextPoint {
|
|
22
|
+
/** The paragraph or the list item, counted from the start of the text. */
|
|
23
|
+
readonly line: number;
|
|
24
|
+
/** Characters from the start of the line; a line break is one character. */
|
|
25
|
+
readonly offset: number;
|
|
26
|
+
}
|
|
27
|
+
interface TextSelection {
|
|
28
|
+
readonly start: TextPoint;
|
|
29
|
+
readonly end: TextPoint;
|
|
30
|
+
}
|
|
31
|
+
type Mark = "bold" | "italic";
|
|
32
|
+
/** What the selected words have, or the words at the cursor when nothing is selected. */
|
|
33
|
+
/** Whether the selection holds no characters: it is a cursor. */
|
|
34
|
+
declare function isCollapsed(selection: TextSelection): boolean;
|
|
35
|
+
/** The marks every selected word has, and the link they are in. */
|
|
36
|
+
declare function formatsAt(text: RichText, selection: TextSelection): TextFormats;
|
|
37
|
+
/** The words of the link the cursor is in, or undefined outside a link. */
|
|
38
|
+
declare function linkSelectionAt(text: RichText, point: TextPoint): TextSelection | undefined;
|
|
39
|
+
/** The mark on every selected word, or off them all when every one has it already. */
|
|
40
|
+
declare function toggleMark(text: RichText, selection: TextSelection, mark: Mark): RichText;
|
|
41
|
+
/** The selected words lead to the address, or lead nowhere when it is undefined. */
|
|
42
|
+
declare function setLink(text: RichText, selection: TextSelection, href: string | undefined): RichText;
|
|
43
|
+
/** The selected lines become a list, or paragraphs again when they all are a list already. */
|
|
44
|
+
declare function toggleList(text: RichText, selection: TextSelection): RichText;
|
|
45
|
+
|
|
20
46
|
/** The elements of a text, put into the editable element in place of what it held. */
|
|
21
47
|
declare function renderRichText(element: HTMLElement, text: RichText): void;
|
|
22
48
|
/** What the editable element holds now, as formatted text. */
|
|
@@ -35,4 +61,4 @@ declare function isOwnPhotoSrc(src: string): boolean;
|
|
|
35
61
|
/** Where the site hands out its own photo; undefined for an upload or another site's address. */
|
|
36
62
|
declare function ownPhotoPath(src: string): string | undefined;
|
|
37
63
|
|
|
38
|
-
export { OWN_PHOTOS_PATH, TextSelection, carriesFiles, closestMarkedPath, imageFieldAt, insertPlainText, isOwnPhotoSrc, ownPhotoPath, readRichTextFrom, readSelectionFrom, renderRichText, selectIn };
|
|
64
|
+
export { type Mark, OWN_PHOTOS_PATH, Shortcut, TextFormats, type TextPoint, type TextSelection, carriesFiles, closestMarkedPath, formatsAt, imageFieldAt, insertPlainText, isCollapsed, isOwnPhotoSrc, linkSelectionAt, ownPhotoPath, readRichTextFrom, readSelectionFrom, renderRichText, selectIn, setLink, shortcutOf, toggleList, toggleMark };
|
package/dist/protocol.js
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
OWN_PHOTOS_PATH,
|
|
3
3
|
isOwnPhotoSrc,
|
|
4
4
|
ownPhotoPath
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-W2LIOR2C.js";
|
|
6
6
|
import {
|
|
7
7
|
PROTOCOL_VERSION,
|
|
8
8
|
carriesFiles,
|
|
@@ -22,8 +22,9 @@ import {
|
|
|
22
22
|
shortcutOf,
|
|
23
23
|
toggleList,
|
|
24
24
|
toggleMark
|
|
25
|
-
} from "./chunk-
|
|
26
|
-
import "./chunk-
|
|
25
|
+
} from "./chunk-MW3BE5O6.js";
|
|
26
|
+
import "./chunk-7GYJS7ZT.js";
|
|
27
|
+
import "./chunk-QCVWHHJC.js";
|
|
27
28
|
export {
|
|
28
29
|
OWN_PHOTOS_PATH,
|
|
29
30
|
PROTOCOL_VERSION,
|
package/dist/routes/media.js
CHANGED
|
@@ -1,28 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
uploadedPhotoPath
|
|
3
|
+
} from "../chunk-QCVWHHJC.js";
|
|
1
4
|
import {
|
|
2
5
|
isUploadedPhoto
|
|
3
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-QL4K4HR6.js";
|
|
4
7
|
|
|
5
8
|
// src/routes/media.ts
|
|
6
9
|
import { cmsOrigin } from "virtual:seamlet/config";
|
|
7
10
|
import site from "virtual:seamlet/site";
|
|
8
11
|
var NOT_FOUND = 404;
|
|
12
|
+
var BAD_GATEWAY = 502;
|
|
13
|
+
var PHOTO_TIMEOUT_MS = 1e4;
|
|
9
14
|
var GET = async ({ params }) => {
|
|
10
15
|
const fileName = params["file"];
|
|
11
16
|
const isOurs = params["site"] === site.id && isUploadedPhoto(fileName);
|
|
12
17
|
if (!isOurs) {
|
|
13
18
|
return new Response(null, { status: NOT_FOUND });
|
|
14
19
|
}
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
)
|
|
20
|
+
const url = new URL(uploadedPhotoPath(site.id, fileName), cmsOrigin);
|
|
21
|
+
const response = await photoFromCms(url);
|
|
22
|
+
if (response === void 0) {
|
|
23
|
+
return new Response(null, { status: BAD_GATEWAY });
|
|
24
|
+
}
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
const status = response.status === NOT_FOUND ? NOT_FOUND : BAD_GATEWAY;
|
|
27
|
+
return new Response(null, { status });
|
|
28
|
+
}
|
|
18
29
|
return new Response(response.body, {
|
|
19
|
-
status: response.status,
|
|
20
30
|
headers: {
|
|
21
31
|
"Content-Type": response.headers.get("Content-Type") ?? "image/webp",
|
|
22
32
|
"Cache-Control": response.headers.get("Cache-Control") ?? "no-cache"
|
|
23
33
|
}
|
|
24
34
|
});
|
|
25
35
|
};
|
|
36
|
+
async function photoFromCms(url) {
|
|
37
|
+
try {
|
|
38
|
+
return await fetch(url, { signal: AbortSignal.timeout(PHOTO_TIMEOUT_MS) });
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.warn(
|
|
41
|
+
`@seamlet/astro: the photo ${url.href} could not be taken from the CMS`,
|
|
42
|
+
error
|
|
43
|
+
);
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
26
47
|
export {
|
|
27
48
|
GET
|
|
28
49
|
};
|
package/dist/schema.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
3
|
-
|
|
4
|
-
export { C as ContentIssue, P as ParsedContent, c as countCharacters, f as formatFieldPath, p as parseContent } from './parse-content-C9ldnmvj.js';
|
|
5
|
-
export { a as SITE_DESCRIPTION_PATH, S as SiteDescription, b as SitePageDescription, i as isSearchTextsGroup, r as readSiteDescription } from './site-description-4Jx6Nq6X.js';
|
|
1
|
+
import { d as ImageValue, L as LinkValue, b as FieldPath, e as Field, f as CollectionField, F as Fields, a as Content, g as RecordFields, C as ContentOf, G as GroupField, h as LinkField, T as TextField, U as UrlField, i as ChoicesField, j as TimeField, I as ImageField } from './parse-content-Eg8yRq_t.js';
|
|
2
|
+
export { k as ChoiceField, l as ChoiceOption, c as ContentIssue, D as DateField, m as FieldKind, n as FieldValue, o as ImageFocus, N as NumberField, O as OptionValue, P as ParsedContent, p as RecordOf, q as ReferenceField, r as ReferencesField, s as RichBlock, t as RichLine, u as RichSpan, R as RichText, v as RichTextField, w as ToggleField, V as ValueOf, x as asOneParagraph, y as choice, z as choices, A as collection, B as countCharacters, E as date, H as defaultContent, J as defaultValue, K as defineSchema, M as formatFieldPath, Q as group, S as image, W as linesOf, X as link, Y as normalizedLine, Z as number, _ as parseContent, $ as plainTextOf, a0 as readRichText, a1 as reference, a2 as references, a3 as richText, a4 as richTextOf, a5 as text, a6 as time, a7 as toggle, a8 as url } from './parse-content-Eg8yRq_t.js';
|
|
3
|
+
export { F as FieldsReading, H as HOME_PAGE_PATH, P as PageProblemReason, a as SITE_DESCRIPTION_PATH, b as SchemaProblem, c as SchemaProblemReason, S as SiteDescription, d as SiteDescriptionProblem, e as SiteDescriptionReading, f as SitePageDescription, g as checkFields, h as checkSiteDescription, i as isSearchTextsGroup, r as readFields, j as readSiteDescription } from './site-description-Cg5KNhhG.js';
|
|
6
4
|
|
|
7
5
|
declare const FIELD_PATH_ATTRIBUTE = "data-cms-path";
|
|
8
6
|
interface FieldMarker {
|
|
@@ -91,6 +89,12 @@ type LinkKind = LinkTarget["kind"];
|
|
|
91
89
|
* is checked exactly as it is stored and rendered: spaces around it make it no address at all.
|
|
92
90
|
*/
|
|
93
91
|
declare function linkTargetOf(href: string): LinkTarget | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* The address as it is when a link of a site may lead there, or undefined when it may not: what a
|
|
94
|
+
* site puts into `href` of content it renders, since content that is not saved yet (the editor's
|
|
95
|
+
* draft in the preview) may hold any address.
|
|
96
|
+
*/
|
|
97
|
+
declare function safeHref(href: string): string | undefined;
|
|
94
98
|
/**
|
|
95
99
|
* The address of a link from what a person typed: a phone or an email without `tel:` or
|
|
96
100
|
* `mailto:`, a site without `https://`.
|
|
@@ -172,8 +176,9 @@ declare function openingHoursField(options?: {
|
|
|
172
176
|
note: TextField;
|
|
173
177
|
}>;
|
|
174
178
|
|
|
175
|
-
/**
|
|
176
|
-
|
|
179
|
+
/** An object of named values: not null, not a list. */
|
|
180
|
+
type JsonObject = Readonly<Record<string, unknown>>;
|
|
181
|
+
declare function isJsonObject(value: unknown): value is JsonObject;
|
|
177
182
|
|
|
178
183
|
/** The file at the root of a site's repository. */
|
|
179
184
|
declare const SITE_CONFIG_FILE = "seamlet.json";
|
|
@@ -198,4 +203,4 @@ declare function isSiteId(value: unknown): value is string;
|
|
|
198
203
|
/** The config the input gives (the parsed `seamlet.json`); fields it does not know are left out. */
|
|
199
204
|
declare function readSiteConfig(input: unknown): SiteConfigReading;
|
|
200
205
|
|
|
201
|
-
export { ChoicesField, CollectionField, type CollectionRecords, Content, ContentOf, type ContentPath, FIELD_PATH_ATTRIBUTE, Field, type FieldMarker, FieldPath, type FieldTarget, Fields, GroupField, ImageField, ImageValue, LinkField, type LinkKind, type LinkTarget, LinkValue, RECORD_ID_KEY, REVEALED_ATTRIBUTE, REVEAL_ATTRIBUTE, REVEAL_EVENT, RecordFields, SITE_CONFIG_FILE, type SiteConfig, type SiteConfigField, type SiteConfigReading, TextField, TimeField, UrlField, WEEKDAYS, collectionAt, contactsFields, editable, findRecord, hrefOf, imagePosition, isRecordId, isSiteId, linkTargetOf, newRecord, openingHoursField,
|
|
206
|
+
export { ChoicesField, CollectionField, type CollectionRecords, Content, ContentOf, type ContentPath, FIELD_PATH_ATTRIBUTE, Field, type FieldMarker, FieldPath, type FieldTarget, Fields, GroupField, ImageField, ImageValue, type JsonObject, LinkField, type LinkKind, type LinkTarget, LinkValue, RECORD_ID_KEY, REVEALED_ATTRIBUTE, REVEAL_ATTRIBUTE, REVEAL_EVENT, RecordFields, SITE_CONFIG_FILE, type SiteConfig, type SiteConfigField, type SiteConfigReading, TextField, TimeField, UrlField, WEEKDAYS, collectionAt, contactsFields, editable, findRecord, hrefOf, imagePosition, isJsonObject, isRecordId, isSiteId, linkTargetOf, newRecord, openingHoursField, readSiteConfig, resolveFieldPath, safeHref, seoFields, withRecordIds };
|
package/dist/schema.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
FIELD_PATH_ATTRIBUTE,
|
|
3
|
+
HOME_PAGE_PATH,
|
|
3
4
|
RECORD_ID_KEY,
|
|
4
5
|
REVEALED_ATTRIBUTE,
|
|
5
6
|
REVEAL_ATTRIBUTE,
|
|
@@ -8,6 +9,8 @@ import {
|
|
|
8
9
|
SITE_DESCRIPTION_PATH,
|
|
9
10
|
WEEKDAYS,
|
|
10
11
|
asOneParagraph,
|
|
12
|
+
checkFields,
|
|
13
|
+
checkSiteDescription,
|
|
11
14
|
choice,
|
|
12
15
|
choices,
|
|
13
16
|
collection,
|
|
@@ -25,9 +28,11 @@ import {
|
|
|
25
28
|
hrefOf,
|
|
26
29
|
image,
|
|
27
30
|
imagePosition,
|
|
31
|
+
isJsonObject,
|
|
28
32
|
isRecordId,
|
|
29
33
|
isSearchTextsGroup,
|
|
30
34
|
isSiteId,
|
|
35
|
+
linesOf,
|
|
31
36
|
link,
|
|
32
37
|
linkTargetOf,
|
|
33
38
|
newRecord,
|
|
@@ -45,15 +50,17 @@ import {
|
|
|
45
50
|
resolveFieldPath,
|
|
46
51
|
richText,
|
|
47
52
|
richTextOf,
|
|
53
|
+
safeHref,
|
|
48
54
|
seoFields,
|
|
49
55
|
text,
|
|
50
56
|
time,
|
|
51
57
|
toggle,
|
|
52
58
|
url,
|
|
53
59
|
withRecordIds
|
|
54
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-7GYJS7ZT.js";
|
|
55
61
|
export {
|
|
56
62
|
FIELD_PATH_ATTRIBUTE,
|
|
63
|
+
HOME_PAGE_PATH,
|
|
57
64
|
RECORD_ID_KEY,
|
|
58
65
|
REVEALED_ATTRIBUTE,
|
|
59
66
|
REVEAL_ATTRIBUTE,
|
|
@@ -62,6 +69,8 @@ export {
|
|
|
62
69
|
SITE_DESCRIPTION_PATH,
|
|
63
70
|
WEEKDAYS,
|
|
64
71
|
asOneParagraph,
|
|
72
|
+
checkFields,
|
|
73
|
+
checkSiteDescription,
|
|
65
74
|
choice,
|
|
66
75
|
choices,
|
|
67
76
|
collection,
|
|
@@ -79,9 +88,11 @@ export {
|
|
|
79
88
|
hrefOf,
|
|
80
89
|
image,
|
|
81
90
|
imagePosition,
|
|
91
|
+
isJsonObject,
|
|
82
92
|
isRecordId,
|
|
83
93
|
isSearchTextsGroup,
|
|
84
94
|
isSiteId,
|
|
95
|
+
linesOf,
|
|
85
96
|
link,
|
|
86
97
|
linkTargetOf,
|
|
87
98
|
newRecord,
|
|
@@ -99,6 +110,7 @@ export {
|
|
|
99
110
|
resolveFieldPath,
|
|
100
111
|
richText,
|
|
101
112
|
richTextOf,
|
|
113
|
+
safeHref,
|
|
102
114
|
seoFields,
|
|
103
115
|
text,
|
|
104
116
|
time,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { F as Fields, a as Content, c as ContentIssue } from './parse-content-Eg8yRq_t.js';
|
|
2
|
+
|
|
3
|
+
/** Why a schema cannot be read. */
|
|
4
|
+
type SchemaProblemReason =
|
|
5
|
+
/** Not an object of fields, or a field that is not an object. */
|
|
6
|
+
"not-fields"
|
|
7
|
+
/** A name that is empty, holds the separator of paths (`.`) or is a number, which a path reads as a record. */
|
|
8
|
+
| "invalid-name" | "invalid-label" | "unknown-kind"
|
|
9
|
+
/** An option of the field (`maxLength`, `inline`…) is there but not of its kind. */
|
|
10
|
+
| "invalid-option" | "invalid-choice-options"
|
|
11
|
+
/** A record has a field named `id`, which is the record's own. */
|
|
12
|
+
| "record-id-field"
|
|
13
|
+
/** `itemTitle` names no text or link field of the record. */
|
|
14
|
+
| "invalid-item-title"
|
|
15
|
+
/** A reference names a collection the schema does not have. */
|
|
16
|
+
| "unknown-collection";
|
|
17
|
+
interface SchemaProblem {
|
|
18
|
+
/** The names of the fields down to the field with the problem; empty for the schema itself. */
|
|
19
|
+
readonly path: readonly string[];
|
|
20
|
+
readonly reason: SchemaProblemReason;
|
|
21
|
+
}
|
|
22
|
+
type FieldsReading = {
|
|
23
|
+
readonly status: "read";
|
|
24
|
+
readonly fields: Fields;
|
|
25
|
+
} | {
|
|
26
|
+
readonly status: "invalid";
|
|
27
|
+
readonly problem: SchemaProblem;
|
|
28
|
+
};
|
|
29
|
+
/** The schema the input describes, or undefined when it is not one. */
|
|
30
|
+
declare function readFields(input: unknown): Fields | undefined;
|
|
31
|
+
/** The schema the input describes, or the first problem that keeps it from being one. */
|
|
32
|
+
declare function checkFields(input: unknown): FieldsReading;
|
|
33
|
+
|
|
34
|
+
/** Where a site hands out its description, on the origin of its preview. */
|
|
35
|
+
declare const SITE_DESCRIPTION_PATH = "/seamlet/site.json";
|
|
36
|
+
/** A page of a site, which the site renders itself. */
|
|
37
|
+
interface SitePageDescription {
|
|
38
|
+
/** The address of the page on the site: `/` for the first page, `/menu` for others. */
|
|
39
|
+
readonly path: string;
|
|
40
|
+
/** How the editor names the page in its list of pages. */
|
|
41
|
+
readonly title: string;
|
|
42
|
+
/**
|
|
43
|
+
* Where the content holds what search engines and link previews show for the page
|
|
44
|
+
* (`seo.menu`): a group of a `title` and a `description` text.
|
|
45
|
+
*/
|
|
46
|
+
readonly searchTexts?: string;
|
|
47
|
+
}
|
|
48
|
+
interface SiteDescription {
|
|
49
|
+
/** The address of the site in the CMS (`/sites/<id>`). */
|
|
50
|
+
readonly id: string;
|
|
51
|
+
/** How the CMS names the site. */
|
|
52
|
+
readonly title: string;
|
|
53
|
+
readonly schema: Fields;
|
|
54
|
+
/** The content shown and edited until the first save. */
|
|
55
|
+
readonly initialContent: Content;
|
|
56
|
+
/** The pages of the site; the first is the home page at `/`. */
|
|
57
|
+
readonly pages: readonly [SitePageDescription, ...SitePageDescription[]];
|
|
58
|
+
}
|
|
59
|
+
/** Why a site's description cannot be read: what to fix in the site. */
|
|
60
|
+
type SiteDescriptionProblem = {
|
|
61
|
+
readonly code: "not-a-description";
|
|
62
|
+
} | {
|
|
63
|
+
readonly code: "invalid-id";
|
|
64
|
+
} | {
|
|
65
|
+
readonly code: "invalid-title";
|
|
66
|
+
} | {
|
|
67
|
+
readonly code: "invalid-schema";
|
|
68
|
+
readonly problem: SchemaProblem;
|
|
69
|
+
}
|
|
70
|
+
/** The initial content is not content the CMS could save. */
|
|
71
|
+
| {
|
|
72
|
+
readonly code: "invalid-initial-content";
|
|
73
|
+
readonly issues: readonly ContentIssue[];
|
|
74
|
+
} | {
|
|
75
|
+
readonly code: "no-pages";
|
|
76
|
+
}
|
|
77
|
+
/** The page at this place of the list is not a page, or its path or title is not one. */
|
|
78
|
+
| {
|
|
79
|
+
readonly code: "invalid-page";
|
|
80
|
+
readonly index: number;
|
|
81
|
+
readonly reason: PageProblemReason;
|
|
82
|
+
} | {
|
|
83
|
+
readonly code: "home-page-not-first";
|
|
84
|
+
} | {
|
|
85
|
+
readonly code: "repeated-page-path";
|
|
86
|
+
readonly path: string;
|
|
87
|
+
};
|
|
88
|
+
type PageProblemReason = "not-a-page"
|
|
89
|
+
/** Not `/`, or not lower-case segments without a trailing slash, such as `/menu`. */
|
|
90
|
+
| "invalid-path" | "invalid-title"
|
|
91
|
+
/** Not where the content holds a group of a `title` and a `description` text. */
|
|
92
|
+
| "invalid-search-texts";
|
|
93
|
+
type SiteDescriptionReading = {
|
|
94
|
+
readonly status: "read";
|
|
95
|
+
readonly description: SiteDescription;
|
|
96
|
+
} | {
|
|
97
|
+
readonly status: "invalid";
|
|
98
|
+
readonly problems: readonly SiteDescriptionProblem[];
|
|
99
|
+
};
|
|
100
|
+
/** The first page of every site. */
|
|
101
|
+
declare const HOME_PAGE_PATH = "/";
|
|
102
|
+
/**
|
|
103
|
+
* The description the input gives, or undefined when it is not a whole and sound one: a known
|
|
104
|
+
* schema, an initial content with nothing to fix, the home page first, each page once.
|
|
105
|
+
*/
|
|
106
|
+
declare function readSiteDescription(input: unknown): SiteDescription | undefined;
|
|
107
|
+
/** The description the input gives, or every problem that keeps it from being whole and sound. */
|
|
108
|
+
declare function checkSiteDescription(input: unknown): SiteDescriptionReading;
|
|
109
|
+
/** Whether the path names a group of a `title` and a `description` text. */
|
|
110
|
+
declare function isSearchTextsGroup(schema: Fields, path: string): boolean;
|
|
111
|
+
|
|
112
|
+
export { type FieldsReading as F, HOME_PAGE_PATH as H, type PageProblemReason as P, type SiteDescription as S, SITE_DESCRIPTION_PATH as a, type SchemaProblem as b, type SchemaProblemReason as c, type SiteDescriptionProblem as d, type SiteDescriptionReading as e, type SitePageDescription as f, checkFields as g, checkSiteDescription as h, isSearchTextsGroup as i, readSiteDescription as j, readFields as r };
|
package/dist/site.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ImageMetadata } from 'astro';
|
|
2
2
|
import { AstroComponentFactory } from 'astro/runtime/server/index.js';
|
|
3
|
-
import { F as Fields, C as ContentOf } from './content-
|
|
4
|
-
import { S as SiteDescription } from './site-description-
|
|
3
|
+
import { F as Fields, C as ContentOf } from './parse-content-Eg8yRq_t.js';
|
|
4
|
+
import { S as SiteDescription } from './site-description-Cg5KNhhG.js';
|
|
5
5
|
|
|
6
6
|
/** A page of the site: how the editor names it, and the component that renders it. */
|
|
7
7
|
interface SeamletPage {
|
package/dist/site.js
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
import {
|
|
2
|
+
HOME_PAGE_PATH,
|
|
3
|
+
checkSiteDescription,
|
|
4
|
+
formatFieldPath,
|
|
2
5
|
parseContent
|
|
3
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-7GYJS7ZT.js";
|
|
7
|
+
import {
|
|
8
|
+
SITE_MODULE_FILE
|
|
9
|
+
} from "./chunk-QCVWHHJC.js";
|
|
4
10
|
|
|
5
11
|
// src/site.ts
|
|
6
12
|
import { cmsOrigin, siteId, siteTitle } from "virtual:seamlet/config";
|
|
7
13
|
|
|
8
14
|
// src/published-answers.ts
|
|
9
15
|
var NOT_FOUND = 404;
|
|
16
|
+
var PUBLISHED_CONTENT_TIMEOUT_MS = 3e4;
|
|
10
17
|
var UNKNOWN_SITE = "unknown-site";
|
|
18
|
+
async function fetchPublishedAnswer(url) {
|
|
19
|
+
let response;
|
|
20
|
+
try {
|
|
21
|
+
response = await fetch(url, {
|
|
22
|
+
signal: AbortSignal.timeout(PUBLISHED_CONTENT_TIMEOUT_MS)
|
|
23
|
+
});
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Loading the published content from ${url.href} failed: the CMS did not answer`,
|
|
27
|
+
{ cause: error }
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return readPublishedAnswer(response, url);
|
|
31
|
+
}
|
|
11
32
|
async function readPublishedAnswer(response, url) {
|
|
12
33
|
if (response.ok)
|
|
13
34
|
return { status: "published", content: await response.json() };
|
|
@@ -35,21 +56,74 @@ function describeSite(identity, definition) {
|
|
|
35
56
|
...page.searchTexts === void 0 ? {} : { searchTexts: page.searchTexts }
|
|
36
57
|
})
|
|
37
58
|
);
|
|
38
|
-
const
|
|
39
|
-
if (home === void 0) {
|
|
40
|
-
throw new Error(`The site "${identity.id}" has no pages`);
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
59
|
+
const description = {
|
|
43
60
|
id: identity.id,
|
|
44
61
|
title: identity.title,
|
|
45
62
|
schema: definition.schema,
|
|
46
63
|
initialContent: definition.initialContent,
|
|
47
|
-
pages
|
|
64
|
+
pages
|
|
48
65
|
};
|
|
66
|
+
const asSent = JSON.parse(JSON.stringify(description));
|
|
67
|
+
const reading = checkSiteDescription(asSent);
|
|
68
|
+
if (reading.status === "invalid") {
|
|
69
|
+
throw new Error(refusalMessage(identity.id, reading.problems));
|
|
70
|
+
}
|
|
71
|
+
return reading.description;
|
|
72
|
+
}
|
|
73
|
+
function refusalMessage(id, problems) {
|
|
74
|
+
const lines = problems.map((problem) => ` - ${problemText(problem)}`);
|
|
75
|
+
return [
|
|
76
|
+
`@seamlet/astro: the CMS would refuse the site "${id}" (${SITE_MODULE_FILE}):`,
|
|
77
|
+
...lines
|
|
78
|
+
].join("\n");
|
|
79
|
+
}
|
|
80
|
+
function problemText(problem) {
|
|
81
|
+
switch (problem.code) {
|
|
82
|
+
case "not-a-description":
|
|
83
|
+
return "the site describes nothing";
|
|
84
|
+
case "invalid-id":
|
|
85
|
+
return "its id in seamlet.json is not one";
|
|
86
|
+
case "invalid-title":
|
|
87
|
+
return "its title in seamlet.json is empty";
|
|
88
|
+
case "invalid-schema": {
|
|
89
|
+
const path = formatFieldPath(problem.problem.path);
|
|
90
|
+
const where = path === "" ? "the schema" : `the field "${path}"`;
|
|
91
|
+
return `${where}: ${SCHEMA_PROBLEM_TEXTS[problem.problem.reason]}`;
|
|
92
|
+
}
|
|
93
|
+
case "invalid-initial-content":
|
|
94
|
+
return `the initial content: ${problem.issues.map(issueText).join("; ")}`;
|
|
95
|
+
case "no-pages":
|
|
96
|
+
return "it has no pages";
|
|
97
|
+
case "invalid-page":
|
|
98
|
+
return `page ${String(problem.index + 1)} of pages: ${PAGE_PROBLEM_TEXTS[problem.reason]}`;
|
|
99
|
+
case "home-page-not-first":
|
|
100
|
+
return 'the first of the pages is not the home page "/"';
|
|
101
|
+
case "repeated-page-path":
|
|
102
|
+
return `the page "${problem.path}" is named twice`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
var SCHEMA_PROBLEM_TEXTS = {
|
|
106
|
+
"not-fields": "not fields of the schema language",
|
|
107
|
+
"invalid-name": "a field's name must not be empty, hold a dot or be a number",
|
|
108
|
+
"invalid-label": "its label is not a text",
|
|
109
|
+
"unknown-kind": "a kind of field the schema language does not have",
|
|
110
|
+
"invalid-option": "an option of the field is not of its kind",
|
|
111
|
+
"invalid-choice-options": "its options are not a list of { value, label }",
|
|
112
|
+
"record-id-field": "a record has its own id: name the field otherwise",
|
|
113
|
+
"invalid-item-title": "itemTitle names no text or link field of the record",
|
|
114
|
+
"unknown-collection": "it refers to a collection the schema does not have"
|
|
115
|
+
};
|
|
116
|
+
var PAGE_PROBLEM_TEXTS = {
|
|
117
|
+
"not-a-page": "not a page",
|
|
118
|
+
"invalid-path": 'its address is not "/" or lower-case segments without a trailing slash, such as "/menu"',
|
|
119
|
+
"invalid-title": "its title is empty",
|
|
120
|
+
"invalid-search-texts": "searchTexts names no group of a title and a description text"
|
|
121
|
+
};
|
|
122
|
+
function issueText(issue) {
|
|
123
|
+
return `"${formatFieldPath(issue.path)}" is ${issue.code}`;
|
|
49
124
|
}
|
|
50
125
|
|
|
51
126
|
// src/site.ts
|
|
52
|
-
var HOME_PAGE_PATH = "/";
|
|
53
127
|
function defineSeamletSite(definition) {
|
|
54
128
|
const pages = new Map(Object.entries(definition.pages));
|
|
55
129
|
return {
|
|
@@ -63,7 +137,7 @@ function defineSeamletSite(definition) {
|
|
|
63
137
|
}
|
|
64
138
|
async function loadPublishedContent(schema) {
|
|
65
139
|
const url = new URL(`/api/sites/${siteId}/published`, cmsOrigin);
|
|
66
|
-
const answer = await
|
|
140
|
+
const answer = await fetchPublishedAnswer(url);
|
|
67
141
|
const content = answer.status === "published" ? answer.content : await initialContentOfSite();
|
|
68
142
|
return parseContent(schema, content).content;
|
|
69
143
|
}
|
package/package.json
CHANGED