@seamlet/astro 1.4.0 → 1.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.0
4
+
5
+ - Feeds of the catalog: `@seamlet/astro/feeds` with `yandexFeedOf` (YML for Яндекс), `avitoFeedOf` (an Avito autoload file, with the fields of its category from the site) and `productOffersOf`, the offers of `productFields` records.
6
+ - A calculator whose prices the owner keeps: `ratesField()` and `estimateOf(rates, amounts)`.
7
+
3
8
  ## 1.4.0
4
9
 
5
10
  - Documents: `file({ label })` holds a PDF the owner uploads in the CMS, `{ src, name }` (`FileValue`): a price list, a menu, a contract. An empty name of a file asks for its file (`missing-file-src`). A static build copies the uploaded documents its pages link to, as it copies the photos, and the route of uploads serves them on request; `isUploadedFile` names both.
package/README.md CHANGED
@@ -438,6 +438,41 @@ document.addEventListener(REQUEST_SENT_EVENT, (event) => {
438
438
 
439
439
  A site whose `Content-Security-Policy` sets `connect-src` or `form-action` lets the CMS's origin in both. A request the policy stops on its way is posted by the browser as a plain form.
440
440
 
441
+ ## A calculator
442
+
443
+ `ratesField()` is a collection of rates the owner keeps (what is counted, its unit, its price), and `estimateOf(rates, amounts)` sums what a visitor chose, the amounts known by the ids of their rates, in whole rubles. The site's own script asks the amounts and shows the sum; the page writes the rates for it, `data-rates={JSON.stringify(content.rates)}`. A new price reaches the calculator with the next publication.
444
+
445
+ ## Feeds of the catalog
446
+
447
+ `@seamlet/astro/feeds` writes the catalog for the places that sell it, from the published content: `yandexFeedOf(shop, offers, madeAt)` in YML (Яндекс Директ, Бизнес, Маркет) and `avitoFeedOf(seller, offers, fieldsOf?)` as an Avito autoload file, `fieldsOf` giving the fields Avito's category asks. `productOffersOf(products, { siteUrl, pathOf })` makes the offers of `productFields` records, leaving the hidden ones out; a product without a price is left out of both feeds.
448
+
449
+ ```ts
450
+ // src/pages/feeds/yandex.xml.ts
451
+ import { productOffersOf, yandexFeedOf } from "@seamlet/astro/feeds";
452
+ import site from "../../seamlet";
453
+ import { productPages } from "../../schema";
454
+
455
+ export async function GET() {
456
+ const content = await site.loadPublishedContent();
457
+ const offers = productOffersOf(content.products, {
458
+ siteUrl: "https://lesnaya.ru",
459
+ pathOf: productPages.pathOf,
460
+ });
461
+ const feed = yandexFeedOf(
462
+ {
463
+ name: "Лесная мастерская",
464
+ company: "ООО «Лесная мастерская»",
465
+ url: "https://lesnaya.ru",
466
+ },
467
+ offers,
468
+ new Date(),
469
+ );
470
+ return new Response(feed, {
471
+ headers: { "Content-Type": "application/xml; charset=utf-8" },
472
+ });
473
+ }
474
+ ```
475
+
441
476
  ## What the integration adds
442
477
 
443
478
  - `/preview` and `/preview/<page>`: the preview the CMS's editor frames, the pages of records too (at their address or at the record's id, a page the site does not show too). `GET` shows the published content; `POST` with the content as JSON shows that content without saving it, which is how the editor shows every edit at once. Only the CMS may frame it (`Content-Security-Policy: frame-ancestors`), and it is never indexed or cached; its refusals (an address of no page, a record a draft does not have, a draft that is not JSON or is over 1 MB) say the same.
package/dist/bridge.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { F as Fields } from './parse-content-CT2cUVL-.js';
2
- import { P as PageMessage, E as EditorMessage } from './messages-Dwjthhrk.js';
1
+ import { F as Fields } from './content-ZG6F3MPA.js';
2
+ import { P as PageMessage, E as EditorMessage } from './messages-d-elsH_0.js';
3
+ import './parse-content-Dtec5MKj.js';
3
4
 
4
5
  interface BridgeOptions {
5
6
  /** The origin of the CMS whose editor may frame the page, such as `https://cms.example.com`. */
package/dist/bridge.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  shortcutOf,
19
19
  toggleList,
20
20
  toggleMark
21
- } from "./chunk-BCJUH3ZD.js";
21
+ } from "./chunk-WABVCYGZ.js";
22
22
  import {
23
23
  FIELD_PATH_ATTRIBUTE,
24
24
  REVEALED_ATTRIBUTE,
@@ -28,7 +28,7 @@ import {
28
28
  readRichText,
29
29
  resolveFieldPath,
30
30
  safeHref
31
- } from "./chunk-TMN7KG5M.js";
31
+ } from "./chunk-53O34DN3.js";
32
32
 
33
33
  // src/bridge/typed-fields.ts
34
34
  function typedKindAt(schema, path) {
@@ -1002,6 +1002,28 @@ function percentOf(fraction) {
1002
1002
  return `${String(rounded)}%`;
1003
1003
  }
1004
1004
 
1005
+ // src/schema/rates.ts
1006
+ function ratesField(options = {}) {
1007
+ return collection({
1008
+ label: options.label ?? "\u0420\u0430\u0441\u0446\u0435\u043D\u043A\u0438 \u043A\u0430\u043B\u044C\u043A\u0443\u043B\u044F\u0442\u043E\u0440\u0430",
1009
+ itemTitle: "name",
1010
+ item: {
1011
+ name: text({ label: "\u0427\u0442\u043E \u0441\u0447\u0438\u0442\u0430\u0435\u043C", maxLength: 80 }),
1012
+ unit: text({ label: "\u0415\u0434\u0438\u043D\u0438\u0446\u0430", maxLength: 12 }),
1013
+ price: number({ label: "\u0426\u0435\u043D\u0430 \u0437\u0430 \u0435\u0434\u0438\u043D\u0438\u0446\u0443, \u20BD", min: 0 })
1014
+ }
1015
+ });
1016
+ }
1017
+ function estimateOf(rates, amounts) {
1018
+ const total = rates.reduce((sum, rate) => {
1019
+ const price = rate.price ?? 0;
1020
+ const amount = rate.id === void 0 ? 0 : amounts[rate.id] ?? 0;
1021
+ const isCounted = Number.isFinite(amount) && amount > 0;
1022
+ return isCounted ? sum + amount * price : sum;
1023
+ }, 0);
1024
+ return Math.round(total);
1025
+ }
1026
+
1005
1027
  // src/schema/presets.ts
1006
1028
  function seoFields(options = {}) {
1007
1029
  return group({
@@ -1868,6 +1890,8 @@ export {
1868
1890
  REVEALED_ATTRIBUTE,
1869
1891
  REVEAL_EVENT,
1870
1892
  imagePosition,
1893
+ ratesField,
1894
+ estimateOf,
1871
1895
  seoFields,
1872
1896
  contactsFields,
1873
1897
  WEEKDAYS,
@@ -6,7 +6,7 @@ import {
6
6
  recordAt,
7
7
  recordPageAt,
8
8
  recordPagePath
9
- } from "./chunk-TMN7KG5M.js";
9
+ } from "./chunk-53O34DN3.js";
10
10
 
11
11
  // src/site-pages.ts
12
12
  function pagePathOf(rest) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  pagePathOf
3
- } from "./chunk-ULOKDC6D.js";
3
+ } from "./chunk-62XHZAPK.js";
4
4
  import {
5
5
  FIELD_PATH_SEPARATOR,
6
6
  collectionAt,
@@ -9,7 +9,7 @@ import {
9
9
  newRecord,
10
10
  recordAt,
11
11
  recordPageAt
12
- } from "./chunk-TMN7KG5M.js";
12
+ } from "./chunk-53O34DN3.js";
13
13
 
14
14
  // src/preview-requests.ts
15
15
  var MAX_DRAFT_BYTES = 1024 * 1024;
@@ -6,7 +6,7 @@ import {
6
6
  readRichText,
7
7
  resolveFieldPath,
8
8
  safeHref
9
- } from "./chunk-TMN7KG5M.js";
9
+ } from "./chunk-53O34DN3.js";
10
10
 
11
11
  // src/bridge/editor-shortcuts.ts
12
12
  function shortcutOf(event) {
package/dist/cli.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- SITE_MODULE_FILE
4
- } from "./chunk-QCVWHHJC.js";
5
2
  import {
6
3
  SITE_CONFIG_FILE,
7
4
  isSiteId,
8
5
  readSiteConfig
9
- } from "./chunk-TMN7KG5M.js";
6
+ } from "./chunk-53O34DN3.js";
7
+ import {
8
+ SITE_MODULE_FILE
9
+ } from "./chunk-QCVWHHJC.js";
10
10
 
11
11
  // src/cli.ts
12
12
  import process from "process";
package/dist/client.js CHANGED
@@ -16,7 +16,7 @@ if (isFramed && isPreview) {
16
16
  });
17
17
  }
18
18
  if (document.querySelector(REQUEST_FORM_SELECTOR) !== null) {
19
- void import("./request-sending-ZBTK5L7U.js").then(({ connectRequestForms }) => {
19
+ void import("./request-sending-PW6OB4W2.js").then(({ connectRequestForms }) => {
20
20
  connectRequestForms(document, { isPreview });
21
21
  });
22
22
  }
@@ -239,84 +239,4 @@ type FieldValue = string | number | boolean | null | readonly string[] | ImageVa
239
239
  declare function defaultValue(field: Field): FieldValue;
240
240
  declare function defaultContent(fields: Fields): Content;
241
241
 
242
- /** Where a value lives in the content: field names and, inside a collection, record indexes. */
243
- type FieldPath = readonly (string | number)[];
244
- type ContentIssue = {
245
- readonly path: FieldPath;
246
- readonly code: "invalid-type";
247
- } | {
248
- readonly path: FieldPath;
249
- readonly code: "invalid-date";
250
- } | {
251
- readonly path: FieldPath;
252
- readonly code: "invalid-time";
253
- } | {
254
- readonly path: FieldPath;
255
- readonly code: "too-long";
256
- readonly maxLength: number;
257
- } | {
258
- readonly path: FieldPath;
259
- readonly code: "below-min";
260
- readonly min: number;
261
- } | {
262
- readonly path: FieldPath;
263
- readonly code: "above-max";
264
- readonly max: number;
265
- } | {
266
- readonly path: FieldPath;
267
- readonly code: "missing-image-src";
268
- } | {
269
- readonly path: FieldPath;
270
- readonly code: "missing-file-src";
271
- } | {
272
- readonly path: FieldPath;
273
- readonly code: "missing-link-href";
274
- } | {
275
- readonly path: FieldPath;
276
- readonly code: "unsupported-link-href";
277
- } | {
278
- readonly path: FieldPath;
279
- readonly code: "unsupported-url";
280
- } | {
281
- readonly path: FieldPath;
282
- readonly code: "unknown-choice";
283
- readonly value: string;
284
- } | {
285
- readonly path: FieldPath;
286
- readonly code: "missing-record";
287
- readonly id: string;
288
- }
289
- /** A slug with a character an address does not have. */
290
- | {
291
- readonly path: FieldPath;
292
- readonly code: "invalid-slug";
293
- }
294
- /** A slug another record of the collection has as its slug or its id. */
295
- | {
296
- readonly path: FieldPath;
297
- readonly code: "taken-slug";
298
- };
299
- interface ParsedContent<C> {
300
- readonly content: C;
301
- readonly issues: readonly ContentIssue[];
302
- }
303
- declare function formatFieldPath(path: FieldPath): string;
304
- /**
305
- * The length that `maxLength` limits: characters as people see them (grapheme clusters), so
306
- * "й" and "👍🏽" are one character each, whatever number of code units they take.
307
- */
308
- declare function countCharacters(text: string): number;
309
- /**
310
- * Brings any input to the shape of the schema, so the result is always content of that schema:
311
- * - a field missing from the input (the schema grew after the content was saved) gets its default;
312
- * - a value of the wrong type or format gets the default too, and an issue;
313
- * - a value of the right type that breaks a rule of its field (too long, below the minimum) is
314
- * kept, with an issue: nothing a person typed is lost;
315
- * - keys the schema does not know are dropped; a record keeps its id (see withRecordIds);
316
- * - a reference to a record its collection does not have (removed since) is kept, with an issue;
317
- * - a slug another record of the collection has is kept, with an issue.
318
- * Content with issues is valid to show and edit, but not to save.
319
- */
320
- declare function parseContent<F extends Fields>(schema: F, input: unknown): ParsedContent<ContentOf<F>>;
321
-
322
- export { link as $, choice as A, choices as B, type Content as C, type DateField as D, collection as E, type Fields as F, type GroupField as G, countCharacters as H, type ImageField as I, date as J, defaultContent as K, type LinkValue as L, defaultValue as M, type NumberField as N, type OptionValue as O, type ParsedContent as P, defineSchema as Q, type RichText as R, type SlugField as S, type TextField as T, type UrlField as U, type ValueOf as V, file as W, formatFieldPath as X, group as Y, image as Z, linesOf as _, type FieldPath as a, normalizedLine as a0, number as a1, parseContent as a2, plainTextOf as a3, readRichText as a4, reference as a5, references as a6, richText as a7, richTextOf as a8, slug as a9, text as aa, time as ab, toggle as ac, url as ad, type ContentOf as b, type Field as c, type CollectionField as d, type RecordFields as e, type RecordOf as f, type ContentIssue as g, type ImageValue as h, type LinkField as i, type ChoicesField as j, type TimeField as k, type RichTextField as l, type ToggleField as m, type ChoiceField as n, type ChoiceOption as o, type FieldKind as p, type FieldValue as q, type FileField as r, type FileValue as s, type ImageFocus as t, type ReferenceField as u, type ReferencesField as v, type RichBlock as w, type RichLine as x, type RichSpan as y, asOneParagraph as z };
242
+ export { reference as $, collection as A, date as B, type Content as C, type DateField as D, defaultContent as E, type Fields as F, type GroupField as G, defaultValue as H, type ImageField as I, defineSchema as J, file as K, type LinkField as L, group as M, type NumberField as N, type OptionValue as O, image as P, linesOf as Q, type RichTextField as R, type SlugField as S, type TextField as T, type UrlField as U, type ValueOf as V, link as W, normalizedLine as X, number as Y, plainTextOf as Z, readRichText as _, type CollectionField as a, references as a0, richText as a1, richTextOf as a2, slug as a3, text as a4, time as a5, toggle as a6, url as a7, type ToggleField as b, type ChoicesField as c, type TimeField as d, type ContentOf as e, type RichText as f, type RecordOf as g, type Field as h, type RecordFields as i, type ImageValue as j, type LinkValue as k, type ChoiceField as l, type ChoiceOption as m, type FieldKind as n, type FieldValue as o, type FileField as p, type FileValue as q, type ImageFocus as r, type ReferenceField as s, type ReferencesField as t, type RichBlock as u, type RichLine as v, type RichSpan as w, asOneParagraph as x, choice as y, choices as z };
@@ -0,0 +1,50 @@
1
+ import { g as RecordOf } from './content-ZG6F3MPA.js';
2
+ import { p as productFields } from './presets-Ch54FeQK.js';
3
+
4
+ interface FeedOffer {
5
+ /** Stays the same while the product lives: the record's id. */
6
+ readonly id: string;
7
+ readonly name: string;
8
+ /** The whole address of the product's page, or of the catalog when products have no pages. */
9
+ readonly url: string;
10
+ /** In rubles; a product without a price is left out of the feeds, which require one. */
11
+ readonly price: number | null;
12
+ readonly description: string;
13
+ /** Whole addresses of its photos, the main one first. */
14
+ readonly pictures: readonly string[];
15
+ /** The name of its category, if the catalog has them. */
16
+ readonly category?: string;
17
+ }
18
+ type ProductRecord = RecordOf<ReturnType<typeof productFields>>;
19
+ interface ProductOffersOptions {
20
+ /** The site's address: `https://lesnaya.ru`. */
21
+ readonly siteUrl: string;
22
+ /** The page of a product (`recordPages(...).pathOf`), or none: the catalog's page is used. */
23
+ readonly pathOf?: (product: ProductRecord) => string | undefined;
24
+ /** The page of the catalog, for products without pages of their own. */
25
+ readonly catalogPath?: string;
26
+ readonly categoryOf?: (product: ProductRecord) => string | undefined;
27
+ }
28
+ /** The offers of the products a visitor sees: hidden ones and ones without an id are left out. */
29
+ declare function productOffersOf(products: readonly ProductRecord[], options: ProductOffersOptions): readonly FeedOffer[];
30
+
31
+ interface AvitoSeller {
32
+ /** Where the goods are: the address of the shop or the workshop. */
33
+ readonly address: string;
34
+ readonly contactPhone: string;
35
+ /** The category of Avito the ads go to: «Ремонт и строительство». */
36
+ readonly category: string;
37
+ }
38
+ declare function avitoFeedOf(seller: AvitoSeller, offers: readonly FeedOffer[], fieldsOf?: (offer: FeedOffer) => Readonly<Record<string, string>>): string;
39
+
40
+ interface YandexShop {
41
+ /** The short name buyers know: «Лесная мастерская». */
42
+ readonly name: string;
43
+ /** The company's full name: «ООО «Лесная мастерская»». */
44
+ readonly company: string;
45
+ /** The site's address. */
46
+ readonly url: string;
47
+ }
48
+ declare function yandexFeedOf(shop: YandexShop, offers: readonly FeedOffer[], madeAt: Date): string;
49
+
50
+ export { type AvitoSeller, type FeedOffer, type ProductOffersOptions, type ProductRecord, type YandexShop, avitoFeedOf, productOffersOf, yandexFeedOf };
package/dist/feeds.js ADDED
@@ -0,0 +1,174 @@
1
+ import {
2
+ plainTextOf
3
+ } from "./chunk-53O34DN3.js";
4
+
5
+ // src/feeds/xml.ts
6
+ var ESCAPES = {
7
+ "&": "&amp;",
8
+ "<": "&lt;",
9
+ ">": "&gt;",
10
+ '"': "&quot;",
11
+ "'": "&apos;"
12
+ };
13
+ var TAB = 9;
14
+ var LINE_FEED = 10;
15
+ var CARRIAGE_RETURN = 13;
16
+ var FIRST_PRINTED = 32;
17
+ var NOT_CHARACTERS = /* @__PURE__ */ new Set([65534, 65535]);
18
+ function escapeXml(text) {
19
+ const kept = Array.from(text).filter(isXmlCharacter).join("");
20
+ return kept.replace(
21
+ /[&<>"']/gu,
22
+ (character) => ESCAPES[character] ?? character
23
+ );
24
+ }
25
+ function isXmlCharacter(character) {
26
+ const code = character.codePointAt(0) ?? 0;
27
+ const isWhitespace = code === TAB || code === LINE_FEED || code === CARRIAGE_RETURN;
28
+ return (isWhitespace || code >= FIRST_PRINTED) && !NOT_CHARACTERS.has(code);
29
+ }
30
+ function textElement(name, text) {
31
+ return `<${name}>${escapeXml(text)}</${name}>`;
32
+ }
33
+ var XML_DECLARATION = '<?xml version="1.0" encoding="UTF-8"?>';
34
+
35
+ // src/feeds/avito-feed.ts
36
+ var TITLE_LENGTH = 50;
37
+ var MOST_IMAGES = 10;
38
+ var DESCRIPTION_LENGTH = 7500;
39
+ function avitoFeedOf(seller, offers, fieldsOf = () => ({})) {
40
+ const priced = offers.filter((offer) => (offer.price ?? 0) > 0);
41
+ const ads = priced.map((offer) => adOf(seller, offer, fieldsOf(offer)));
42
+ return [
43
+ XML_DECLARATION,
44
+ '<Ads formatVersion="3" target="Avito.ru">',
45
+ ...ads,
46
+ "</Ads>"
47
+ ].join("\n");
48
+ }
49
+ function adOf(seller, offer, fields) {
50
+ const images = offer.pictures.slice(0, MOST_IMAGES).map((picture) => `<Image url="${escapeXml(picture)}"/>`);
51
+ const extra = Object.entries(fields).map(
52
+ ([name, value]) => textElement(name, value)
53
+ );
54
+ return [
55
+ "<Ad>",
56
+ textElement("Id", offer.id),
57
+ textElement("Address", seller.address),
58
+ textElement("Category", seller.category),
59
+ textElement("Title", titleOf(offer.name)),
60
+ textElement("Description", offer.description.slice(0, DESCRIPTION_LENGTH)),
61
+ textElement("Price", String(offer.price)),
62
+ textElement("ContactPhone", seller.contactPhone),
63
+ images.length === 0 ? "" : `<Images>${images.join("")}</Images>`,
64
+ ...extra,
65
+ "</Ad>"
66
+ ].join("");
67
+ }
68
+ var GRAPHEMES = new Intl.Segmenter("ru-RU", { granularity: "grapheme" });
69
+ function titleOf(name) {
70
+ const characters = Array.from(
71
+ GRAPHEMES.segment(name.trim()),
72
+ ({ segment }) => segment
73
+ );
74
+ const cut = characters.slice(0, TITLE_LENGTH).join("");
75
+ const isWhole = characters.length <= TITLE_LENGTH;
76
+ if (isWhole) return cut;
77
+ const lastSpace = cut.lastIndexOf(" ");
78
+ return (lastSpace > 0 ? cut.slice(0, lastSpace) : cut).trim();
79
+ }
80
+
81
+ // src/feeds/offers.ts
82
+ function productOffersOf(products, options) {
83
+ return products.flatMap((product) => {
84
+ const id = product.id;
85
+ if (product.isHidden || id === void 0) return [];
86
+ return [offerOf(product, id, options)];
87
+ });
88
+ }
89
+ function offerOf(product, id, options) {
90
+ const path = options.pathOf?.(product) ?? options.catalogPath ?? "/";
91
+ const photos = product.photos.flatMap(
92
+ ({ photo }) => photo === null || photo.src.trim() === "" ? [] : [photo.src]
93
+ );
94
+ const description = descriptionOf(product);
95
+ const category = options.categoryOf?.(product);
96
+ return {
97
+ id,
98
+ name: product.name,
99
+ url: new URL(path, options.siteUrl).href,
100
+ price: product.price,
101
+ description,
102
+ pictures: photos.map((src) => new URL(src, options.siteUrl).href),
103
+ ...category === void 0 ? {} : { category }
104
+ };
105
+ }
106
+ function descriptionOf(product) {
107
+ const whole = plainTextOf(product.description).trim();
108
+ return whole === "" ? plainTextOf(product.summary).trim() : whole;
109
+ }
110
+
111
+ // src/feeds/yandex-feed.ts
112
+ var OFFER_ID_LENGTH = 20;
113
+ var NOT_OFFER_ID = /[^A-Za-z0-9]/gu;
114
+ var DESCRIPTION_LENGTH2 = 3e3;
115
+ var SHOP_CATEGORY_ID = "1";
116
+ function yandexFeedOf(shop, offers, madeAt) {
117
+ const priced = offers.filter((offer) => (offer.price ?? 0) > 0);
118
+ const categories = categoryIdsOf(priced, shop.name);
119
+ const offerElements = priced.map(
120
+ (offer) => offerElementOf(
121
+ offer,
122
+ categories.get(offer.category ?? shop.name) ?? SHOP_CATEGORY_ID
123
+ )
124
+ );
125
+ const categoryElements = [...categories].map(
126
+ ([name, id]) => `<category id="${id}">${escapeXml(name)}</category>`
127
+ );
128
+ return [
129
+ XML_DECLARATION,
130
+ `<yml_catalog date="${dateOf(madeAt)}">`,
131
+ "<shop>",
132
+ textElement("name", shop.name),
133
+ textElement("company", shop.company),
134
+ textElement("url", shop.url),
135
+ '<currencies><currency id="RUB" rate="1"/></currencies>',
136
+ `<categories>${categoryElements.join("")}</categories>`,
137
+ `<offers>${offerElements.join("")}</offers>`,
138
+ "</shop>",
139
+ "</yml_catalog>"
140
+ ].join("\n");
141
+ }
142
+ function categoryIdsOf(offers, shopName) {
143
+ const names = offers.map((offer) => offer.category ?? shopName);
144
+ const unique = [...new Set(names)];
145
+ return new Map(unique.map((name, index) => [name, String(index + 1)]));
146
+ }
147
+ function offerElementOf(offer, categoryId) {
148
+ const pictures = offer.pictures.map(
149
+ (picture) => textElement("picture", picture)
150
+ );
151
+ const description = offer.description.slice(0, DESCRIPTION_LENGTH2);
152
+ return [
153
+ `<offer id="${offerIdOf(offer.id)}" available="true">`,
154
+ textElement("name", offer.name),
155
+ textElement("url", offer.url),
156
+ textElement("price", String(offer.price)),
157
+ "<currencyId>RUB</currencyId>",
158
+ textElement("categoryId", categoryId),
159
+ ...pictures,
160
+ description === "" ? "" : textElement("description", description),
161
+ "</offer>"
162
+ ].join("");
163
+ }
164
+ function offerIdOf(id) {
165
+ return id.replace(NOT_OFFER_ID, "").slice(0, OFFER_ID_LENGTH);
166
+ }
167
+ function dateOf(moment) {
168
+ return `${moment.toISOString().slice(0, 19)}Z`;
169
+ }
170
+ export {
171
+ avitoFeedOf,
172
+ productOffersOf,
173
+ yandexFeedOf
174
+ };
package/dist/index.js CHANGED
@@ -1,21 +1,21 @@
1
+ import {
2
+ isUploadedFile
3
+ } from "./chunk-JN6XVHCW.js";
1
4
  import {
2
5
  OWN_PHOTOS_PATH
3
6
  } from "./chunk-W2LIOR2C.js";
4
- import {
5
- PREVIEW_PATH,
6
- SITE_MODULE_FILE,
7
- UPLOADS_PATH
8
- } from "./chunk-QCVWHHJC.js";
9
- import {
10
- isUploadedFile
11
- } from "./chunk-JQ4YD7MM.js";
12
- import "./chunk-ULOKDC6D.js";
7
+ import "./chunk-62XHZAPK.js";
13
8
  import {
14
9
  SITE_CONFIG_FILE,
15
10
  SITE_DESCRIPTION_PATH,
16
11
  isJsonObject,
17
12
  readSiteConfig
18
- } from "./chunk-TMN7KG5M.js";
13
+ } from "./chunk-53O34DN3.js";
14
+ import {
15
+ PREVIEW_PATH,
16
+ SITE_MODULE_FILE,
17
+ UPLOADS_PATH
18
+ } from "./chunk-QCVWHHJC.js";
19
19
 
20
20
  // src/index.ts
21
21
  import process from "process";
@@ -1,4 +1,5 @@
1
- import { R as RichText, F as Fields, C as Content, a as FieldPath } from './parse-content-CT2cUVL-.js';
1
+ import { f as RichText, F as Fields, C as Content } from './content-ZG6F3MPA.js';
2
+ import { F as FieldPath } from './parse-content-Dtec5MKj.js';
2
3
 
3
4
  interface Box {
4
5
  readonly top: number;
@@ -0,0 +1,83 @@
1
+ import { F as Fields, e as ContentOf } from './content-ZG6F3MPA.js';
2
+
3
+ /** Where a value lives in the content: field names and, inside a collection, record indexes. */
4
+ type FieldPath = readonly (string | number)[];
5
+ type ContentIssue = {
6
+ readonly path: FieldPath;
7
+ readonly code: "invalid-type";
8
+ } | {
9
+ readonly path: FieldPath;
10
+ readonly code: "invalid-date";
11
+ } | {
12
+ readonly path: FieldPath;
13
+ readonly code: "invalid-time";
14
+ } | {
15
+ readonly path: FieldPath;
16
+ readonly code: "too-long";
17
+ readonly maxLength: number;
18
+ } | {
19
+ readonly path: FieldPath;
20
+ readonly code: "below-min";
21
+ readonly min: number;
22
+ } | {
23
+ readonly path: FieldPath;
24
+ readonly code: "above-max";
25
+ readonly max: number;
26
+ } | {
27
+ readonly path: FieldPath;
28
+ readonly code: "missing-image-src";
29
+ } | {
30
+ readonly path: FieldPath;
31
+ readonly code: "missing-file-src";
32
+ } | {
33
+ readonly path: FieldPath;
34
+ readonly code: "missing-link-href";
35
+ } | {
36
+ readonly path: FieldPath;
37
+ readonly code: "unsupported-link-href";
38
+ } | {
39
+ readonly path: FieldPath;
40
+ readonly code: "unsupported-url";
41
+ } | {
42
+ readonly path: FieldPath;
43
+ readonly code: "unknown-choice";
44
+ readonly value: string;
45
+ } | {
46
+ readonly path: FieldPath;
47
+ readonly code: "missing-record";
48
+ readonly id: string;
49
+ }
50
+ /** A slug with a character an address does not have. */
51
+ | {
52
+ readonly path: FieldPath;
53
+ readonly code: "invalid-slug";
54
+ }
55
+ /** A slug another record of the collection has as its slug or its id. */
56
+ | {
57
+ readonly path: FieldPath;
58
+ readonly code: "taken-slug";
59
+ };
60
+ interface ParsedContent<C> {
61
+ readonly content: C;
62
+ readonly issues: readonly ContentIssue[];
63
+ }
64
+ declare function formatFieldPath(path: FieldPath): string;
65
+ /**
66
+ * The length that `maxLength` limits: characters as people see them (grapheme clusters), so
67
+ * "й" and "👍🏽" are one character each, whatever number of code units they take.
68
+ */
69
+ declare function countCharacters(text: string): number;
70
+ /**
71
+ * Brings any input to the shape of the schema, so the result is always content of that schema:
72
+ * - a field missing from the input (the schema grew after the content was saved) gets its default;
73
+ * - a value of the wrong type or format gets the default too, and an issue;
74
+ * - a value of the right type that breaks a rule of its field (too long, below the minimum) is
75
+ * kept, with an issue: nothing a person typed is lost;
76
+ * - keys the schema does not know are dropped; a record keeps its id (see withRecordIds);
77
+ * - a reference to a record its collection does not have (removed since) is kept, with an issue;
78
+ * - a slug another record of the collection has is kept, with an issue.
79
+ * Content with issues is valid to show and edit, but not to save.
80
+ */
81
+ declare function parseContent<F extends Fields>(schema: F, input: unknown): ParsedContent<ContentOf<F>>;
82
+
83
+ export { type ContentIssue as C, type FieldPath as F, type ParsedContent as P, countCharacters as c, formatFieldPath as f, parseContent as p };
@@ -0,0 +1,104 @@
1
+ import { T as TextField, S as SlugField, a as CollectionField, I as ImageField, R as RichTextField, N as NumberField, b as ToggleField, G as GroupField, L as LinkField, U as UrlField, c as ChoicesField, d as TimeField } from './content-ZG6F3MPA.js';
2
+
3
+ /** What search engines and a link shared in a messenger show: one per page. */
4
+ declare function seoFields(options?: {
5
+ readonly label?: string;
6
+ }): GroupField<{
7
+ title: TextField;
8
+ description: TextField;
9
+ image: ImageField;
10
+ }>;
11
+ /** How to reach the business: the phone, the address with a map, the messengers. */
12
+ declare function contactsFields(options?: {
13
+ readonly label?: string;
14
+ }): GroupField<{
15
+ phone: LinkField;
16
+ address: TextField;
17
+ map: UrlField;
18
+ messengers: CollectionField<{
19
+ link: LinkField;
20
+ }>;
21
+ }>;
22
+ declare const WEEKDAYS: readonly [{
23
+ readonly value: "mon";
24
+ readonly label: "Пн";
25
+ }, {
26
+ readonly value: "tue";
27
+ readonly label: "Вт";
28
+ }, {
29
+ readonly value: "wed";
30
+ readonly label: "Ср";
31
+ }, {
32
+ readonly value: "thu";
33
+ readonly label: "Чт";
34
+ }, {
35
+ readonly value: "fri";
36
+ readonly label: "Пт";
37
+ }, {
38
+ readonly value: "sat";
39
+ readonly label: "Сб";
40
+ }, {
41
+ readonly value: "sun";
42
+ readonly label: "Вс";
43
+ }];
44
+ /** When the business is open: rows of days with the same hours, «Пн–Пт 08:00–21:00». */
45
+ declare function openingHoursField(options?: {
46
+ readonly label?: string;
47
+ }): CollectionField<{
48
+ days: ChoicesField<readonly [{
49
+ readonly value: "mon";
50
+ readonly label: "Пн";
51
+ }, {
52
+ readonly value: "tue";
53
+ readonly label: "Вт";
54
+ }, {
55
+ readonly value: "wed";
56
+ readonly label: "Ср";
57
+ }, {
58
+ readonly value: "thu";
59
+ readonly label: "Чт";
60
+ }, {
61
+ readonly value: "fri";
62
+ readonly label: "Пт";
63
+ }, {
64
+ readonly value: "sat";
65
+ readonly label: "Сб";
66
+ }, {
67
+ readonly value: "sun";
68
+ readonly label: "Вс";
69
+ }]>;
70
+ opens: TimeField;
71
+ closes: TimeField;
72
+ note: TextField;
73
+ }>;
74
+ /**
75
+ * The fields of a product of a catalog: its name and the address of its page, its photos (the first
76
+ * is the main one), a short line for a card and a whole description for its page, the price and a
77
+ * word about it («от», «за м²»), characteristics as pairs of a name and a value, so the owner adds a
78
+ * property without code, the switch that hides it, and what search engines show for its page. A
79
+ * site puts them into its collection and adds its own fields beside them:
80
+ * `item: { ...productFields(), category: reference({ label: "Категория", collection: "categories" }) }`.
81
+ */
82
+ declare function productFields(): {
83
+ name: TextField;
84
+ slug: SlugField;
85
+ photos: CollectionField<{
86
+ photo: ImageField;
87
+ }>;
88
+ summary: RichTextField;
89
+ description: RichTextField;
90
+ price: NumberField;
91
+ priceNote: TextField;
92
+ characteristics: CollectionField<{
93
+ name: TextField;
94
+ value: TextField;
95
+ }>;
96
+ isHidden: ToggleField;
97
+ seo: GroupField<{
98
+ title: TextField;
99
+ description: TextField;
100
+ image: ImageField;
101
+ }>;
102
+ };
103
+
104
+ export { WEEKDAYS as W, contactsFields as c, openingHoursField as o, productFields as p, seoFields as s };
@@ -1,5 +1,6 @@
1
- import { C as Content } from './parse-content-CT2cUVL-.js';
2
- import { S as SiteDescription } from './site-description-cKiFtNHW.js';
1
+ import { C as Content } from './content-ZG6F3MPA.js';
2
+ import { S as SiteDescription } from './site-description-0UU5Wukx.js';
3
+ import './parse-content-Dtec5MKj.js';
3
4
 
4
5
  /** Far more than the content of a site ever takes; a bigger body is not a draft. */
5
6
  declare const MAX_DRAFT_BYTES: number;
@@ -6,9 +6,9 @@ import {
6
6
  previewContentAt,
7
7
  previewHeadersOf,
8
8
  readDraft
9
- } from "./chunk-JQ4YD7MM.js";
10
- import "./chunk-ULOKDC6D.js";
11
- import "./chunk-TMN7KG5M.js";
9
+ } from "./chunk-JN6XVHCW.js";
10
+ import "./chunk-62XHZAPK.js";
11
+ import "./chunk-53O34DN3.js";
12
12
  export {
13
13
  MAX_DRAFT_BYTES,
14
14
  isPageAddress,
@@ -1,6 +1,7 @@
1
- import { S as Shortcut, T as TextFormats } from './messages-Dwjthhrk.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-Dwjthhrk.js';
3
- import { F as Fields, a as FieldPath, I as ImageField, R as RichText } from './parse-content-CT2cUVL-.js';
1
+ import { S as Shortcut, T as TextFormats } from './messages-d-elsH_0.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-d-elsH_0.js';
3
+ import { F as Fields, I as ImageField, f as RichText } from './content-ZG6F3MPA.js';
4
+ import { F as FieldPath } from './parse-content-Dtec5MKj.js';
4
5
 
5
6
  declare function shortcutOf(event: KeyboardEvent): Shortcut | undefined;
6
7
 
package/dist/protocol.js CHANGED
@@ -22,9 +22,9 @@ import {
22
22
  shortcutOf,
23
23
  toggleList,
24
24
  toggleMark
25
- } from "./chunk-BCJUH3ZD.js";
25
+ } from "./chunk-WABVCYGZ.js";
26
+ import "./chunk-53O34DN3.js";
26
27
  import "./chunk-QCVWHHJC.js";
27
- import "./chunk-TMN7KG5M.js";
28
28
  export {
29
29
  OWN_PHOTOS_PATH,
30
30
  PROTOCOL_VERSION,
@@ -12,7 +12,7 @@ import {
12
12
  isJsonObject,
13
13
  readRequestForm,
14
14
  readRequestValues
15
- } from "./chunk-TMN7KG5M.js";
15
+ } from "./chunk-53O34DN3.js";
16
16
 
17
17
  // src/request-sending.ts
18
18
  var STATUS_CREATED = 201;
@@ -1,11 +1,11 @@
1
+ import {
2
+ isUploadedFile
3
+ } from "../chunk-JN6XVHCW.js";
4
+ import "../chunk-62XHZAPK.js";
5
+ import "../chunk-53O34DN3.js";
1
6
  import {
2
7
  uploadedPhotoPath
3
8
  } from "../chunk-QCVWHHJC.js";
4
- import {
5
- isUploadedFile
6
- } from "../chunk-JQ4YD7MM.js";
7
- import "../chunk-ULOKDC6D.js";
8
- import "../chunk-TMN7KG5M.js";
9
9
 
10
10
  // src/routes/media.ts
11
11
  import { cmsOrigin } from "virtual:seamlet/config";
package/dist/schema.d.ts CHANGED
@@ -1,7 +1,9 @@
1
- import { h as ImageValue, L as LinkValue, d as CollectionField, C as Content, F as Fields, e as RecordFields, b as ContentOf, G as GroupField, i as LinkField, T as TextField, U as UrlField, j as ChoicesField, k as TimeField, S as SlugField, I as ImageField, l as RichTextField, N as NumberField, m as ToggleField } from './parse-content-CT2cUVL-.js';
2
- export { n as ChoiceField, o as ChoiceOption, g as ContentIssue, D as DateField, c as Field, p as FieldKind, a as FieldPath, q as FieldValue, r as FileField, s as FileValue, t as ImageFocus, O as OptionValue, P as ParsedContent, f as RecordOf, u as ReferenceField, v as ReferencesField, w as RichBlock, x as RichLine, y as RichSpan, R as RichText, V as ValueOf, z as asOneParagraph, A as choice, B as choices, E as collection, H as countCharacters, J as date, K as defaultContent, M as defaultValue, Q as defineSchema, W as file, X as formatFieldPath, Y as group, Z as image, _ as linesOf, $ as link, a0 as normalizedLine, a1 as number, a2 as parseContent, a3 as plainTextOf, a4 as readRichText, a5 as reference, a6 as references, a7 as richText, a8 as richTextOf, a9 as slug, aa as text, ab as time, ac as toggle, ad as url } from './parse-content-CT2cUVL-.js';
3
- import { R as RequestForm } from './site-description-cKiFtNHW.js';
4
- export { C as CollectionPath, D as DEFAULT_LONG_TEXT_LENGTH, b as DEFAULT_TEXT_LENGTH, F as FIELD_PATH_SEPARATOR, c as FieldTarget, d as FieldsReading, H as HOME_PAGE_PATH, M as MAX_REQUEST_FIELD_LENGTH, P as PAGE_PATH_SEPARATOR, e as PageProblemReason, f as PlacedRecord, g as RECORD_PLACEHOLDER, h as RecordFieldsAt, i as RecordPagePath, j as RecordPageProblemReason, k as RecordPages, l as RequestChoiceField, m as RequestEmailField, n as RequestField, o as RequestFieldKind, p as RequestFields, q as RequestFormProblemReason, r as RequestFormReading, s as RequestLongTextField, t as RequestPhoneField, u as RequestTextField, v as SITE_DESCRIPTION_PATH, w as SchemaProblem, x as SchemaProblemReason, S as SiteDescription, y as SiteDescriptionProblem, z as SiteDescriptionReading, A as SitePageDescription, B as SiteRecordPageDescription, a as SiteRecordPages, E as checkFields, G as checkSiteDescription, I as isFormId, J as isRecordPagePath, K as isSearchTextsGroup, L as maxLengthOf, N as readFields, O as readRequestForm, Q as readSiteDescription, T as recordAddressOf, U as recordAt, V as recordPageAt, W as recordPagePath, X as recordPages, Y as requestChoice, Z as requestEmail, _ as requestForm, $ as requestLongText, a0 as requestPhone, a1 as requestText, a2 as resolveFieldPath } from './site-description-cKiFtNHW.js';
1
+ import { j as ImageValue, k as LinkValue, a as CollectionField, C as Content, F as Fields, i as RecordFields, e as ContentOf, g as RecordOf, T as TextField, N as NumberField } from './content-ZG6F3MPA.js';
2
+ export { l as ChoiceField, m as ChoiceOption, c as ChoicesField, D as DateField, h as Field, n as FieldKind, o as FieldValue, p as FileField, q as FileValue, G as GroupField, I as ImageField, r as ImageFocus, L as LinkField, O as OptionValue, s as ReferenceField, t as ReferencesField, u as RichBlock, v as RichLine, w as RichSpan, f as RichText, R as RichTextField, S as SlugField, d as TimeField, b as ToggleField, U as UrlField, V as ValueOf, x as asOneParagraph, y as choice, z as choices, A as collection, B as date, E as defaultContent, H as defaultValue, J as defineSchema, K as file, M as group, P as image, Q as linesOf, W as link, X as normalizedLine, Y as number, Z as plainTextOf, _ as readRichText, $ as reference, a0 as references, a1 as richText, a2 as richTextOf, a3 as slug, a4 as text, a5 as time, a6 as toggle, a7 as url } from './content-ZG6F3MPA.js';
3
+ export { C as ContentIssue, F as FieldPath, P as ParsedContent, c as countCharacters, f as formatFieldPath, p as parseContent } from './parse-content-Dtec5MKj.js';
4
+ import { R as RequestForm } from './site-description-0UU5Wukx.js';
5
+ export { C as CollectionPath, D as DEFAULT_LONG_TEXT_LENGTH, b as DEFAULT_TEXT_LENGTH, F as FIELD_PATH_SEPARATOR, c as FieldTarget, d as FieldsReading, H as HOME_PAGE_PATH, M as MAX_REQUEST_FIELD_LENGTH, P as PAGE_PATH_SEPARATOR, e as PageProblemReason, f as PlacedRecord, g as RECORD_PLACEHOLDER, h as RecordFieldsAt, i as RecordPagePath, j as RecordPageProblemReason, k as RecordPages, l as RequestChoiceField, m as RequestEmailField, n as RequestField, o as RequestFieldKind, p as RequestFields, q as RequestFormProblemReason, r as RequestFormReading, s as RequestLongTextField, t as RequestPhoneField, u as RequestTextField, v as SITE_DESCRIPTION_PATH, w as SchemaProblem, x as SchemaProblemReason, S as SiteDescription, y as SiteDescriptionProblem, z as SiteDescriptionReading, A as SitePageDescription, B as SiteRecordPageDescription, a as SiteRecordPages, E as checkFields, G as checkSiteDescription, I as isFormId, J as isRecordPagePath, K as isSearchTextsGroup, L as maxLengthOf, N as readFields, O as readRequestForm, Q as readSiteDescription, T as recordAddressOf, U as recordAt, V as recordPageAt, W as recordPagePath, X as recordPages, Y as requestChoice, Z as requestEmail, _ as requestForm, $ as requestLongText, a0 as requestPhone, a1 as requestText, a2 as resolveFieldPath } from './site-description-0UU5Wukx.js';
6
+ export { W as WEEKDAYS, c as contactsFields, o as openingHoursField, p as productFields, s as seoFields } from './presets-Ch54FeQK.js';
5
7
 
6
8
  declare const FIELD_PATH_ATTRIBUTE = "data-cms-path";
7
9
  interface FieldMarker {
@@ -89,106 +91,21 @@ declare function hrefOf(target: LinkTarget): string;
89
91
  /** `object-position` of the photo: "50% 30%"; the middle when no point is chosen. */
90
92
  declare function imagePosition(image: ImageValue | null): string;
91
93
 
92
- /** What search engines and a link shared in a messenger show: one per page. */
93
- declare function seoFields(options?: {
94
- readonly label?: string;
95
- }): GroupField<{
96
- title: TextField;
97
- description: TextField;
98
- image: ImageField;
99
- }>;
100
- /** How to reach the business: the phone, the address with a map, the messengers. */
101
- declare function contactsFields(options?: {
102
- readonly label?: string;
103
- }): GroupField<{
104
- phone: LinkField;
105
- address: TextField;
106
- map: UrlField;
107
- messengers: CollectionField<{
108
- link: LinkField;
109
- }>;
110
- }>;
111
- declare const WEEKDAYS: readonly [{
112
- readonly value: "mon";
113
- readonly label: "Пн";
114
- }, {
115
- readonly value: "tue";
116
- readonly label: "Вт";
117
- }, {
118
- readonly value: "wed";
119
- readonly label: "Ср";
120
- }, {
121
- readonly value: "thu";
122
- readonly label: "Чт";
123
- }, {
124
- readonly value: "fri";
125
- readonly label: "Пт";
126
- }, {
127
- readonly value: "sat";
128
- readonly label: "Сб";
129
- }, {
130
- readonly value: "sun";
131
- readonly label: "Вс";
132
- }];
133
- /** When the business is open: rows of days with the same hours, «Пн–Пт 08:00–21:00». */
134
- declare function openingHoursField(options?: {
94
+ /** The rates of a calculator: what is counted, in what unit and at what price. */
95
+ declare function ratesField(options?: {
135
96
  readonly label?: string;
136
97
  }): CollectionField<{
137
- days: ChoicesField<readonly [{
138
- readonly value: "mon";
139
- readonly label: "Пн";
140
- }, {
141
- readonly value: "tue";
142
- readonly label: "Вт";
143
- }, {
144
- readonly value: "wed";
145
- readonly label: "Ср";
146
- }, {
147
- readonly value: "thu";
148
- readonly label: "Чт";
149
- }, {
150
- readonly value: "fri";
151
- readonly label: "Пт";
152
- }, {
153
- readonly value: "sat";
154
- readonly label: "Сб";
155
- }, {
156
- readonly value: "sun";
157
- readonly label: "Вс";
158
- }]>;
159
- opens: TimeField;
160
- closes: TimeField;
161
- note: TextField;
98
+ name: TextField;
99
+ unit: TextField;
100
+ price: NumberField;
162
101
  }>;
102
+ type Rate = RecordOf<ReturnType<typeof ratesField>["item"]>;
163
103
  /**
164
- * The fields of a product of a catalog: its name and the address of its page, its photos (the first
165
- * is the main one), a short line for a card and a whole description for its page, the price and a
166
- * word about it («от», «за м²»), characteristics as pairs of a name and a value, so the owner adds a
167
- * property without code, the switch that hides it, and what search engines show for its page. A
168
- * site puts them into its collection and adds its own fields beside them:
169
- * `item: { ...productFields(), category: reference({ label: "Категория", collection: "categories" }) }`.
104
+ * The sum of the amounts a visitor chose at the rates, in whole rubles. An amount is known by the
105
+ * id of its rate; a rate without a price, an amount of no rate and an amount below zero add
106
+ * nothing.
170
107
  */
171
- declare function productFields(): {
172
- name: TextField;
173
- slug: SlugField;
174
- photos: CollectionField<{
175
- photo: ImageField;
176
- }>;
177
- summary: RichTextField;
178
- description: RichTextField;
179
- price: NumberField;
180
- priceNote: TextField;
181
- characteristics: CollectionField<{
182
- name: TextField;
183
- value: TextField;
184
- }>;
185
- isHidden: ToggleField;
186
- seo: GroupField<{
187
- title: TextField;
188
- description: TextField;
189
- image: ImageField;
190
- }>;
191
- };
108
+ declare function estimateOf(rates: readonly Rate[], amounts: Readonly<Record<string, number>>): number;
192
109
 
193
110
  /** An object of named values: not null, not a list. */
194
111
  type JsonObject = Readonly<Record<string, unknown>>;
@@ -333,4 +250,4 @@ interface RequestFailedDetail {
333
250
  readonly issues: readonly RequestIssue[];
334
251
  }
335
252
 
336
- export { CONSENT_REQUIRED_TEXT, ChoicesField, CollectionField, type CollectionRecords, Content, ContentOf, type ContentPath, FIELD_PATH_ATTRIBUTE, type FieldMarker, Fields, GroupField, ImageField, ImageValue, type JsonObject, type LegalDocument, LinkField, type LinkKind, type LinkTarget, LinkValue, MAX_SLUG_LENGTH, NumberField, RECORD_ID_KEY, REQUEST_CONSENT_KEY, REQUEST_FAILED_EVENT, REQUEST_FORM_KEY, REQUEST_ISSUE_TEXTS, REQUEST_PAGE_KEY, REQUEST_SEND_EVENT, REQUEST_SENT_EVENT, REQUEST_TRAP_KEY, REVEALED_ATTRIBUTE, REVEAL_ATTRIBUTE, REVEAL_EVENT, RecordFields, type RequestEntries, type RequestFailedDetail, type RequestFailure, RequestForm, type RequestIssue, type RequestIssueCode, type RequestReading, type RequestSendDetail, type RequestSentDetail, type RequestValue, RichTextField, SITE_CONFIG_FILE, type SiteConfig, type SiteConfigField, type SiteConfigReading, SlugField, TextField, TimeField, ToggleField, UrlField, WEEKDAYS, collectionAt, collectionFieldAt, contactsFields, editable, findRecord, hrefOf, imagePosition, isConsentGiven, isContentObject, isEmailAddress, isJsonObject, isPhoneNumber, isRecordId, isSiteId, isSlug, isTrapFilled, legalPagePathOf, linkTargetOf, newRecord, openingHoursField, productFields, readRequestEntries, readRequestValues, readSiteConfig, requestPageOf, requestsPathOf, safeHref, seoFields, slugFieldOf, slugOf, withRecordIds };
253
+ export { CONSENT_REQUIRED_TEXT, CollectionField, type CollectionRecords, Content, ContentOf, type ContentPath, FIELD_PATH_ATTRIBUTE, type FieldMarker, Fields, ImageValue, type JsonObject, type LegalDocument, type LinkKind, type LinkTarget, LinkValue, MAX_SLUG_LENGTH, NumberField, RECORD_ID_KEY, REQUEST_CONSENT_KEY, REQUEST_FAILED_EVENT, REQUEST_FORM_KEY, REQUEST_ISSUE_TEXTS, REQUEST_PAGE_KEY, REQUEST_SEND_EVENT, REQUEST_SENT_EVENT, REQUEST_TRAP_KEY, REVEALED_ATTRIBUTE, REVEAL_ATTRIBUTE, REVEAL_EVENT, type Rate, RecordFields, RecordOf, type RequestEntries, type RequestFailedDetail, type RequestFailure, RequestForm, type RequestIssue, type RequestIssueCode, type RequestReading, type RequestSendDetail, type RequestSentDetail, type RequestValue, SITE_CONFIG_FILE, type SiteConfig, type SiteConfigField, type SiteConfigReading, TextField, collectionAt, collectionFieldAt, editable, estimateOf, findRecord, hrefOf, imagePosition, isConsentGiven, isContentObject, isEmailAddress, isJsonObject, isPhoneNumber, isRecordId, isSiteId, isSlug, isTrapFilled, legalPagePathOf, linkTargetOf, newRecord, ratesField, readRequestEntries, readRequestValues, readSiteConfig, requestPageOf, requestsPathOf, safeHref, slugFieldOf, slugOf, withRecordIds };
package/dist/schema.js CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  defaultValue,
40
40
  defineSchema,
41
41
  editable,
42
+ estimateOf,
42
43
  file,
43
44
  findRecord,
44
45
  formatFieldPath,
@@ -70,6 +71,7 @@ import {
70
71
  parseContent,
71
72
  plainTextOf,
72
73
  productFields,
74
+ ratesField,
73
75
  readFields,
74
76
  readRequestEntries,
75
77
  readRequestForm,
@@ -105,7 +107,7 @@ import {
105
107
  toggle,
106
108
  url,
107
109
  withRecordIds
108
- } from "./chunk-TMN7KG5M.js";
110
+ } from "./chunk-53O34DN3.js";
109
111
  export {
110
112
  CONSENT_REQUIRED_TEXT,
111
113
  DEFAULT_LONG_TEXT_LENGTH,
@@ -147,6 +149,7 @@ export {
147
149
  defaultValue,
148
150
  defineSchema,
149
151
  editable,
152
+ estimateOf,
150
153
  file,
151
154
  findRecord,
152
155
  formatFieldPath,
@@ -178,6 +181,7 @@ export {
178
181
  parseContent,
179
182
  plainTextOf,
180
183
  productFields,
184
+ ratesField,
181
185
  readFields,
182
186
  readRequestEntries,
183
187
  readRequestForm,
@@ -1,4 +1,5 @@
1
- import { a as FieldPath, c as Field, d as CollectionField, F as Fields, G as GroupField, C as Content, e as RecordFields, f as RecordOf, g as ContentIssue } from './parse-content-CT2cUVL-.js';
1
+ import { h as Field, a as CollectionField, F as Fields, G as GroupField, C as Content, i as RecordFields, g as RecordOf } from './content-ZG6F3MPA.js';
2
+ import { F as FieldPath, C as ContentIssue } from './parse-content-Dtec5MKj.js';
2
3
 
3
4
  /** What a path written on a page points at: a field, or a whole record of a collection. */
4
5
  type FieldTarget = {
package/dist/site.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { ImageMetadata } from 'astro';
2
2
  import { AstroComponentFactory } from 'astro/runtime/server/index.js';
3
- import { C as Content, F as Fields, b as ContentOf } from './parse-content-CT2cUVL-.js';
4
- import { a as SiteRecordPages, S as SiteDescription, R as RequestForm } from './site-description-cKiFtNHW.js';
3
+ import { C as Content, F as Fields, e as ContentOf } from './content-ZG6F3MPA.js';
4
+ import { a as SiteRecordPages, S as SiteDescription, R as RequestForm } from './site-description-0UU5Wukx.js';
5
+ import './parse-content-Dtec5MKj.js';
5
6
 
6
7
  /** A page of the site: how the editor names it, and the component that renders it. */
7
8
  interface SeamletPage {
package/dist/site.js CHANGED
@@ -1,17 +1,17 @@
1
- import {
2
- SITE_MODULE_FILE
3
- } from "./chunk-QCVWHHJC.js";
4
1
  import {
5
2
  pagePathOf,
6
3
  previewedPageAt,
7
4
  renderedPageAt,
8
5
  staticPagesOf
9
- } from "./chunk-ULOKDC6D.js";
6
+ } from "./chunk-62XHZAPK.js";
10
7
  import {
11
8
  checkSiteDescription,
12
9
  formatFieldPath,
13
10
  parseContent
14
- } from "./chunk-TMN7KG5M.js";
11
+ } from "./chunk-53O34DN3.js";
12
+ import {
13
+ SITE_MODULE_FILE
14
+ } from "./chunk-QCVWHHJC.js";
15
15
 
16
16
  // src/site.ts
17
17
  import { cmsOrigin, siteId, siteTitle } from "virtual:seamlet/config";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seamlet/astro",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Connects an Astro site to the Seamlet CMS: the schema of its content, the preview the editor frames, and its published content.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -21,6 +21,10 @@
21
21
  "types": "./dist/schema.d.ts",
22
22
  "default": "./dist/schema.js"
23
23
  },
24
+ "./feeds": {
25
+ "types": "./dist/feeds.d.ts",
26
+ "default": "./dist/feeds.js"
27
+ },
24
28
  "./bridge": {
25
29
  "types": "./dist/bridge.d.ts",
26
30
  "default": "./dist/bridge.js"