@seamlet/astro 0.11.3 → 0.12.1

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/dist/cli.js CHANGED
@@ -3,7 +3,10 @@ import {
3
3
  SITE_CONFIG_FILE,
4
4
  isSiteId,
5
5
  readSiteConfig
6
- } from "./chunk-DRDPKFHZ.js";
6
+ } from "./chunk-ZS32CW5F.js";
7
+ import {
8
+ SITE_MODULE_FILE
9
+ } from "./chunk-QCVWHHJC.js";
7
10
 
8
11
  // src/cli.ts
9
12
  import process from "process";
@@ -14,20 +17,21 @@ import { join as join2 } from "path";
14
17
  import { parseArgs } from "util";
15
18
 
16
19
  // src/init/site-files.ts
17
- var SITE_MODULE_PATH = "src/seamlet.ts";
18
20
  var SCHEMA_PATH = "src/schema.ts";
19
- var HOME_PAGE_PATH = "src/components/HomePage.astro";
21
+ var HOME_PAGE_FILE = "src/components/HomePage.astro";
20
22
  var JSON_INDENT = 2;
21
- function siteFilesOf(config) {
22
- return [
23
- {
24
- path: SITE_CONFIG_FILE,
25
- text: `${JSON.stringify(config, null, JSON_INDENT)}
23
+ function configFileOf(config) {
24
+ return {
25
+ path: SITE_CONFIG_FILE,
26
+ text: `${JSON.stringify(config, null, JSON_INDENT)}
26
27
  `
27
- },
28
+ };
29
+ }
30
+ function moduleFilesOf(config) {
31
+ return [
28
32
  { path: SCHEMA_PATH, text: schemaModuleOf(config.title) },
29
- { path: SITE_MODULE_PATH, text: SITE_MODULE },
30
- { path: HOME_PAGE_PATH, text: HOME_PAGE }
33
+ { path: SITE_MODULE_FILE, text: SITE_MODULE },
34
+ { path: HOME_PAGE_FILE, text: HOME_PAGE }
31
35
  ];
32
36
  }
33
37
  function siteIdOf(packageName) {
@@ -41,7 +45,7 @@ function schemaModuleOf(title) {
41
45
  // What the site's owner edits in the CMS. Add a field here, show it on a page and mark its element
42
46
  // with edit(...): the CMS builds its form from this schema alone.
43
47
  export const schema = defineSchema({
44
- title: text({ label: "\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A", maxLength: 80 }),
48
+ title: text({ label: "\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A" }),
45
49
  });
46
50
 
47
51
  export type Content = ContentOf<typeof schema>;
@@ -83,30 +87,46 @@ const edit = editable<Content>();
83
87
  `;
84
88
 
85
89
  // src/init/write-site-files.ts
86
- import { mkdir, writeFile } from "fs/promises";
90
+ import { access, mkdir, writeFile } from "fs/promises";
87
91
  import { dirname, join } from "path";
88
92
  var ALREADY_EXISTS = "EEXIST";
93
+ var MISSING = "ENOENT";
89
94
  async function writeSiteFiles(root, files) {
90
- const written = [];
91
- const kept = [];
95
+ const paths = files.map((file) => file.path);
96
+ if (await isAnyThere(root, paths)) return { written: [], kept: paths };
92
97
  for (const file of files) {
93
- const isWritten = await writeNewFile(join(root, file.path), file.text);
94
- (isWritten ? written : kept).push(file.path);
98
+ await writeNewFile(join(root, file.path), file.text);
99
+ }
100
+ return { written: paths, kept: [] };
101
+ }
102
+ async function isAnyThere(root, paths) {
103
+ for (const path of paths) {
104
+ if (await isThere(join(root, path))) return true;
105
+ }
106
+ return false;
107
+ }
108
+ async function isThere(path) {
109
+ try {
110
+ await access(path);
111
+ return true;
112
+ } catch (error) {
113
+ if (hasCode(error, MISSING)) return false;
114
+ throw new Error(`@seamlet/astro: cannot read ${path}`, { cause: error });
95
115
  }
96
- return { written, kept };
97
116
  }
98
117
  async function writeNewFile(path, text) {
99
- await mkdir(dirname(path), { recursive: true });
100
118
  try {
119
+ await mkdir(dirname(path), { recursive: true });
101
120
  await writeFile(path, text, { flag: "wx" });
102
- return true;
103
121
  } catch (error) {
104
- if (isAlreadyThere(error)) return false;
105
- throw new Error(`@seamlet/astro: cannot write ${path}`, { cause: error });
122
+ const reason = hasCode(error, ALREADY_EXISTS) ? " (it appeared meanwhile)" : "";
123
+ throw new Error(`@seamlet/astro: cannot write ${path}${reason}`, {
124
+ cause: error
125
+ });
106
126
  }
107
127
  }
108
- function isAlreadyThere(error) {
109
- return error instanceof Error && "code" in error && error.code === ALREADY_EXISTS;
128
+ function hasCode(error, code2) {
129
+ return error instanceof Error && "code" in error && error.code === code2;
110
130
  }
111
131
 
112
132
  // src/init/init-command.ts
@@ -114,6 +134,7 @@ var DEFAULT_CMS = "https://cms.seamlet.ru";
114
134
  var SUCCESS = 0;
115
135
  var FAILURE = 1;
116
136
  var MISUSE = 2;
137
+ var MISSING2 = "ENOENT";
117
138
  var USAGE = `Usage: npx @seamlet/astro init [--id <id>] [--title <title>] [--cms <address>]
118
139
 
119
140
  Writes ${SITE_CONFIG_FILE} and a first module of the site in the Astro project here. The id is the
@@ -135,26 +156,64 @@ async function runInit(run) {
135
156
  run.log(USAGE);
136
157
  return MISUSE;
137
158
  }
138
- const packageName = await packageNameOf(run.root);
159
+ const existing = await existingConfigOf(run.root);
160
+ const reading = existing ?? await configOfOptions(run.root, options);
161
+ if (reading.status === "invalid") {
162
+ run.log(invalidConfigMessage(reading, existing !== void 0));
163
+ return FAILURE;
164
+ }
165
+ const config = reading.config;
166
+ const configFile = await writeSiteFiles(run.root, [configFileOf(config)]);
167
+ const moduleFiles = await writeSiteFiles(run.root, moduleFilesOf(config));
168
+ logWritten(run, configFile);
169
+ logWritten(run, moduleFiles);
170
+ run.log(NEXT_STEPS);
171
+ return SUCCESS;
172
+ }
173
+ async function configOfOptions(root, options) {
174
+ const packageName = await packageNameOf(root);
139
175
  const id = options.id ?? siteIdOf(packageName ?? "");
140
- const reading = readSiteConfig({
176
+ return readSiteConfig({
141
177
  id,
142
178
  title: options.title ?? id,
143
179
  cms: options.cms ?? DEFAULT_CMS
144
180
  });
145
- if (reading.status === "invalid") {
146
- const fields = reading.invalidFields.map((field) => `--${field}`).join(", ");
147
- run.log(`Give the site ${fields}.
148
-
149
- ${USAGE}`);
150
- return FAILURE;
181
+ }
182
+ async function existingConfigOf(root) {
183
+ const path = join2(root, SITE_CONFIG_FILE);
184
+ let text;
185
+ try {
186
+ text = await readFile(path, "utf8");
187
+ } catch (error) {
188
+ if (hasCode(error, MISSING2)) return void 0;
189
+ throw new Error(`@seamlet/astro: cannot read ${path}`, { cause: error });
151
190
  }
152
- const files = await writeSiteFiles(run.root, siteFilesOf(reading.config));
191
+ return readSiteConfig(parsedOrNothing(text));
192
+ }
193
+ function parsedOrNothing(text) {
194
+ try {
195
+ return JSON.parse(text);
196
+ } catch {
197
+ return void 0;
198
+ }
199
+ }
200
+ function invalidConfigMessage(reading, isExisting) {
201
+ if (isExisting) {
202
+ const fields = reading.invalidFields.map((field) => `"${field}"`).join(", ");
203
+ return `The site's ${SITE_CONFIG_FILE} needs ${fields}: fix it, or remove it to write a new one.`;
204
+ }
205
+ const options = reading.invalidFields.map((field) => `--${field}`).join(", ");
206
+ return `Give the site ${options}.
207
+
208
+ ${USAGE}`;
209
+ }
210
+ function logWritten(run, files) {
153
211
  for (const path of files.written) run.log(`wrote ${path}`);
154
- for (const path of files.kept)
155
- run.log(`kept ${path}: the site has it already`);
156
- run.log(NEXT_STEPS);
157
- return SUCCESS;
212
+ for (const path of files.kept) {
213
+ run.log(
214
+ `kept ${path}: the site has it, or a file that goes with it, already`
215
+ );
216
+ }
158
217
  }
159
218
  function optionsOf(argv) {
160
219
  try {
package/dist/client.js CHANGED
@@ -1,6 +1,9 @@
1
+ import {
2
+ PREVIEW_PATH
3
+ } from "./chunk-QCVWHHJC.js";
4
+
1
5
  // src/client.ts
2
6
  import { cmsOrigin } from "virtual:seamlet/config";
3
- var PREVIEW_PATH = "/preview";
4
7
  var isFramed = window.parent !== window;
5
8
  var { pathname } = window.location;
6
9
  var isPreview = pathname === PREVIEW_PATH || pathname.startsWith(`${PREVIEW_PATH}/`);
package/dist/index.js CHANGED
@@ -1,14 +1,20 @@
1
1
  import {
2
2
  OWN_PHOTOS_PATH
3
- } from "./chunk-3BS5CE26.js";
3
+ } from "./chunk-W2LIOR2C.js";
4
4
  import {
5
5
  SITE_CONFIG_FILE,
6
6
  SITE_DESCRIPTION_PATH,
7
+ isJsonObject,
7
8
  readSiteConfig
8
- } from "./chunk-DRDPKFHZ.js";
9
+ } from "./chunk-ZS32CW5F.js";
10
+ import {
11
+ PREVIEW_PATH,
12
+ SITE_MODULE_FILE,
13
+ UPLOADS_PATH
14
+ } from "./chunk-QCVWHHJC.js";
9
15
  import {
10
16
  isUploadedPhoto
11
- } from "./chunk-P2OQOZBC.js";
17
+ } from "./chunk-QL4K4HR6.js";
12
18
 
13
19
  // src/index.ts
14
20
  import process from "process";
@@ -17,28 +23,35 @@ import { fileURLToPath as fileURLToPath2 } from "url";
17
23
  // src/uploaded-photos.ts
18
24
  import { mkdir, readdir, readFile, writeFile } from "fs/promises";
19
25
  import { dirname, join } from "path";
20
- var PHOTO_ADDRESS = /\/media\/([a-z0-9-]+)\/([A-Za-z0-9_-]+\.webp)/gu;
26
+ var PHOTO_ADDRESS = new RegExp(
27
+ `${UPLOADS_PATH}/([^/\\s"'<>]+)/([^/\\s"'<>?#]+)`,
28
+ "gu"
29
+ );
21
30
  var HTML_EXTENSION = ".html";
22
- function uploadedPhotoAddresses(html) {
31
+ var PHOTOS_COPIED_AT_ONCE = 6;
32
+ var PHOTO_TIMEOUT_MS = 6e4;
33
+ function uploadedPhotoAddresses(html, siteId) {
23
34
  const addresses = [...html.matchAll(PHOTO_ADDRESS)].flatMap(
24
- ([address, , fileName]) => isUploadedPhoto(fileName) ? [address] : []
35
+ ([address, site, fileName]) => site === siteId && isUploadedPhoto(fileName) ? [address] : []
25
36
  );
26
37
  return [...new Set(addresses)];
27
38
  }
28
39
  async function copyUploadedPhotos(options) {
29
40
  const pages = await htmlFilesIn(options.directory);
30
41
  const texts = await Promise.all(pages.map((page) => readFile(page, "utf8")));
31
- const addresses = new Set(texts.flatMap(uploadedPhotoAddresses));
32
- await Promise.all(
33
- [...addresses].map(
34
- (address) => copyPhoto(address, options.directory, options.cmsOrigin)
35
- )
42
+ const addresses = new Set(
43
+ texts.flatMap((text) => uploadedPhotoAddresses(text, options.siteId))
44
+ );
45
+ await inTurns(
46
+ [...addresses],
47
+ PHOTOS_COPIED_AT_ONCE,
48
+ (address) => copyPhoto(address, options.directory, options.cmsOrigin)
36
49
  );
37
50
  return addresses.size;
38
51
  }
39
52
  async function copyPhoto(address, directory, cmsOrigin) {
40
53
  const url = new URL(address, cmsOrigin);
41
- const response = await fetch(url);
54
+ const response = await fetchPhoto(url);
42
55
  if (!response.ok) {
43
56
  throw new Error(
44
57
  `Copying the photo ${url.href} into the build failed with status ${String(response.status)}`
@@ -48,6 +61,29 @@ async function copyPhoto(address, directory, cmsOrigin) {
48
61
  await mkdir(dirname(target), { recursive: true });
49
62
  await writeFile(target, new Uint8Array(await response.arrayBuffer()));
50
63
  }
64
+ async function fetchPhoto(url) {
65
+ try {
66
+ return await fetch(url, { signal: AbortSignal.timeout(PHOTO_TIMEOUT_MS) });
67
+ } catch (error) {
68
+ throw new Error(
69
+ `Copying the photo ${url.href} into the build failed: the CMS did not answer`,
70
+ { cause: error }
71
+ );
72
+ }
73
+ }
74
+ async function inTurns(items, limit, task) {
75
+ const waiting = [...items];
76
+ const runInTurn = async () => {
77
+ for (let item = waiting.shift(); item !== void 0; item = waiting.shift()) {
78
+ await task(item);
79
+ }
80
+ };
81
+ const runners = Array.from(
82
+ { length: Math.min(limit, waiting.length) },
83
+ runInTurn
84
+ );
85
+ await Promise.all(runners);
86
+ }
51
87
  async function htmlFilesIn(directory) {
52
88
  const entries = await readdir(directory, {
53
89
  recursive: true,
@@ -68,7 +104,7 @@ async function loadSiteConfig(root, cmsOverride) {
68
104
  const file = new URL(SITE_CONFIG_FILE, root);
69
105
  const parsed = await readJsonFile(file);
70
106
  const isOverridden = cmsOverride !== void 0 && cmsOverride !== "";
71
- const input = isOverridden ? { ...asObject(parsed), cms: cmsOverride } : parsed;
107
+ const input = isOverridden ? { ...isJsonObject(parsed) ? parsed : {}, cms: cmsOverride } : parsed;
72
108
  const reading = readSiteConfig(input);
73
109
  if (reading.status === "read") return reading.config;
74
110
  throw new Error(invalidFieldsMessage(file, reading.invalidFields));
@@ -91,9 +127,6 @@ async function readJsonFile(file) {
91
127
  });
92
128
  }
93
129
  }
94
- function asObject(value) {
95
- return typeof value === "object" && value !== null ? value : {};
96
- }
97
130
  function invalidFieldsMessage(file, fields) {
98
131
  const lines = fields.map((field) => ` "${field}": ${FIELD_RULES[field]}`);
99
132
  return [
@@ -106,7 +139,6 @@ function invalidFieldsMessage(file, fields) {
106
139
  var CONFIG_MODULE = "virtual:seamlet/config";
107
140
  var SITE_MODULE = "virtual:seamlet/site";
108
141
  var RESOLVED_CONFIG_MODULE = `\0${CONFIG_MODULE}`;
109
- var SITE_MODULE_FILE = "src/seamlet.ts";
110
142
  var CMS_ORIGIN_VARIABLE = "CMS_ORIGIN";
111
143
  function seamlet() {
112
144
  let siteConfig;
@@ -146,12 +178,12 @@ function seamlet() {
146
178
  });
147
179
  injectScript("page", `import "@seamlet/astro/client";`);
148
180
  injectRoute({
149
- pattern: "/preview/[...page]",
181
+ pattern: `${PREVIEW_PATH}/[...page]`,
150
182
  entrypoint: "@seamlet/astro/routes/preview.astro",
151
183
  prerender: false
152
184
  });
153
185
  injectRoute({
154
- pattern: "/media/[site]/[file]",
186
+ pattern: `${UPLOADS_PATH}/[site]/[file]`,
155
187
  entrypoint: "@seamlet/astro/routes/media",
156
188
  prerender: false
157
189
  });
@@ -173,6 +205,7 @@ function seamlet() {
173
205
  if (siteConfig === void 0) return;
174
206
  const copied = await copyUploadedPhotos({
175
207
  directory: fileURLToPath2(dir),
208
+ siteId: siteConfig.id,
176
209
  cmsOrigin: siteConfig.cms
177
210
  });
178
211
  if (copied > 0) logger.info(`copied ${String(copied)} uploaded photos`);
@@ -1,5 +1,4 @@
1
- import { R as RichText, F as Fields, a as Content } from './content-6k-E7NWf.js';
2
- import { F as FieldPath } from './parse-content-C9ldnmvj.js';
1
+ import { R as RichText, F as Fields, a as Content, b as FieldPath } from './parse-content-Eg8yRq_t.js';
3
2
 
4
3
  interface Box {
5
4
  readonly top: number;
@@ -12,52 +11,6 @@ interface Viewport {
12
11
  readonly height: number;
13
12
  }
14
13
 
15
- type Shortcut = "save" | "undo" | "redo";
16
- declare function shortcutOf(event: KeyboardEvent): Shortcut | undefined;
17
-
18
- type InlineEditingOutcome = "kept" | "reverted";
19
-
20
- interface TextPoint {
21
- /** The paragraph or the list item, counted from the start of the text. */
22
- readonly line: number;
23
- /** Characters from the start of the line; a line break is one character. */
24
- readonly offset: number;
25
- }
26
- interface TextSelection {
27
- readonly start: TextPoint;
28
- readonly end: TextPoint;
29
- }
30
- type Mark = "bold" | "italic";
31
- /** What the selected words have, or the words at the cursor when nothing is selected. */
32
- interface TextFormats {
33
- readonly isBold: boolean;
34
- readonly isItalic: boolean;
35
- readonly isList: boolean;
36
- /** The address of the link the selection is in, when all of it is in one link. */
37
- readonly href: string | undefined;
38
- }
39
- /** Whether the selection holds no characters: it is a cursor. */
40
- declare function isCollapsed(selection: TextSelection): boolean;
41
- /** The marks every selected word has, and the link they are in. */
42
- declare function formatsAt(text: RichText, selection: TextSelection): TextFormats;
43
- /** The words of the link the cursor is in, or undefined outside a link. */
44
- declare function linkSelectionAt(text: RichText, point: TextPoint): TextSelection | undefined;
45
- /** The mark on every selected word, or off them all when every one has it already. */
46
- declare function toggleMark(text: RichText, selection: TextSelection, mark: Mark): RichText;
47
- /** The selected words lead to the address, or lead nowhere when it is undefined. */
48
- declare function setLink(text: RichText, selection: TextSelection, href: string | undefined): RichText;
49
- /** The selected lines become a list, or paragraphs again when they all are a list already. */
50
- declare function toggleList(text: RichText, selection: TextSelection): RichText;
51
-
52
- /** A change of formatting the editor's toolbar asks for. */
53
- type TextFormat = "bold" | "italic" | "list";
54
- /** The selection in the typed text: where it is on the page and what its words have. */
55
- interface TypedSelection {
56
- readonly box: Box;
57
- readonly isCollapsed: boolean;
58
- readonly formats: TextFormats;
59
- }
60
-
61
14
  /**
62
15
  * The version of what the two say to each other. A site updates @seamlet/astro when it likes, so the
63
16
  * editor may meet a bridge of another version: it tells by this number, said with `ready`, instead
@@ -87,8 +40,32 @@ interface PageLayout {
87
40
  /** The link under the pointer, if it is not a field. */
88
41
  readonly link: LinkBox | undefined;
89
42
  }
43
+ declare const SHORTCUTS: readonly ["save", "undo", "redo"];
44
+ declare const EDITOR_KEY_NAMES: readonly ["save", "undo", "redo", "escape"];
45
+ declare const TEXT_FORMAT_NAMES: readonly ["bold", "italic", "list"];
46
+ declare const TYPING_OUTCOME_NAMES: readonly ["kept", "reverted"];
47
+ /** A keyboard shortcut of the editor, pressed on the page. */
48
+ type Shortcut = (typeof SHORTCUTS)[number];
90
49
  /** A key the editor acts on: its shortcuts, and Escape that closes what is open. */
91
- type EditorKey = Shortcut | "escape";
50
+ type EditorKey = (typeof EDITOR_KEY_NAMES)[number];
51
+ /** A change of formatting the editor's toolbar asks for. */
52
+ type TextFormat = (typeof TEXT_FORMAT_NAMES)[number];
53
+ /** How a typing in place ended: the typed text kept, or brought back as it was. */
54
+ type InlineEditingOutcome = (typeof TYPING_OUTCOME_NAMES)[number];
55
+ /** The marks of the selected words of a formatted text. */
56
+ interface TextFormats {
57
+ readonly isBold: boolean;
58
+ readonly isItalic: boolean;
59
+ readonly isList: boolean;
60
+ /** The address of the link the selection is in, when all of it is in one link. */
61
+ readonly href: string | undefined;
62
+ }
63
+ /** The selection in the typed text: where it is on the page and what its words have. */
64
+ interface TypedSelection {
65
+ readonly box: Box;
66
+ readonly isCollapsed: boolean;
67
+ readonly formats: TextFormats;
68
+ }
92
69
  /** What the page tells the editor. */
93
70
  type PageMessage =
94
71
  /** The bridge runs and waits for the editor to connect; it speaks this version of the protocol. */
@@ -208,8 +185,8 @@ type EditorMessage =
208
185
  readonly path: FieldPath;
209
186
  };
210
187
  /** The message the page sent, or undefined for anything that is not one. */
211
- declare function readPageMessage(data: unknown): PageMessage | undefined;
188
+ declare function readPageMessage(posted: unknown): PageMessage | undefined;
212
189
  /** The message the editor sent, or undefined for anything that is not one. */
213
- declare function readEditorMessage(data: unknown): EditorMessage | undefined;
190
+ declare function readEditorMessage(posted: unknown): EditorMessage | undefined;
214
191
 
215
- export { type Box as B, type EditorMessage as E, type InlineEditingOutcome as I, type LinkBox as L, type Mark as M, type PageMessage as P, type Shortcut as S, type TextSelection as T, type Viewport as V, type EditorKey as a, type MarkerBox as b, PROTOCOL_VERSION as c, type PageLayout as d, type Point as e, type TextFormat as f, type TextFormats as g, type TextPoint as h, type TypedSelection as i, formatsAt as j, isCollapsed as k, linkSelectionAt as l, readPageMessage as m, shortcutOf as n, toggleMark as o, readEditorMessage as r, setLink as s, toggleList as t };
192
+ export { type Box as B, type EditorMessage as E, type InlineEditingOutcome as I, type LinkBox as L, type MarkerBox as M, type PageMessage as P, type Shortcut as S, type TextFormats as T, type Viewport as V, type EditorKey as a, PROTOCOL_VERSION as b, type PageLayout as c, type Point as d, type TextFormat as e, type TypedSelection as f, readPageMessage as g, readEditorMessage as r };
@@ -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
- export { time as $, defaultContent as A, defaultValue as B, type ContentOf as C, type DateField as D, defineSchema as E, type Fields as F, type GroupField as G, group as H, type ImageField as I, image as J, link as K, type LinkValue as L, normalizedLine as M, type NumberField as N, type OptionValue as O, number as P, plainTextOf as Q, type RichText as R, readRichText as S, type TextField as T, type UrlField as U, type ValueOf as V, reference as W, references as X, richText as Y, richTextOf as Z, text as _, type Content as a, toggle as a0, url as a1, type ImageValue as b, type Field as c, type CollectionField as d, type RecordFields as e, type LinkField as f, type ChoicesField as g, type TimeField as h, type ChoiceField as i, type ChoiceOption as j, type FieldKind as k, type FieldValue as l, type ImageFocus as m, type RecordOf as n, type ReferenceField as o, type ReferencesField as p, type RichBlock as q, type RichLine as r, type RichSpan as s, type RichTextField as t, type ToggleField as u, asOneParagraph as v, choice as w, choices as x, collection as y, date as z };
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 };
@@ -1,10 +1,12 @@
1
1
  import {
2
2
  MAX_DRAFT_BYTES,
3
3
  isUploadedPhoto,
4
+ previewHeadersOf,
4
5
  readDraft
5
- } from "./chunk-P2OQOZBC.js";
6
+ } from "./chunk-QL4K4HR6.js";
6
7
  export {
7
8
  MAX_DRAFT_BYTES,
8
9
  isUploadedPhoto,
10
+ previewHeadersOf,
9
11
  readDraft
10
12
  };
@@ -1,7 +1,8 @@
1
- import { T as TextSelection } from './messages-Dx0jNy3N.js';
2
- export { B as Box, a as EditorKey, E as EditorMessage, I as InlineEditingOutcome, L as LinkBox, M as Mark, b as MarkerBox, c as PROTOCOL_VERSION, d as PageLayout, P as PageMessage, e as Point, S as Shortcut, f as TextFormat, g as TextFormats, h as TextPoint, i as TypedSelection, V as Viewport, j as formatsAt, k as isCollapsed, l as linkSelectionAt, r as readEditorMessage, m as readPageMessage, s as setLink, n as shortcutOf, t as toggleList, o as toggleMark } from './messages-Dx0jNy3N.js';
3
- import { F as Fields, I as ImageField, R as RichText } from './content-6k-E7NWf.js';
4
- import { F as FieldPath } from './parse-content-C9ldnmvj.js';
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-3BS5CE26.js";
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-2E7VDSUK.js";
26
- import "./chunk-DRDPKFHZ.js";
25
+ } from "./chunk-SSECNMAD.js";
26
+ import "./chunk-ZS32CW5F.js";
27
+ import "./chunk-QCVWHHJC.js";
27
28
  export {
28
29
  OWN_PHOTOS_PATH,
29
30
  PROTOCOL_VERSION,