bearminion 1.4.0 → 1.6.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.
@@ -1,4 +1,4 @@
1
- import { F as SiteDefinition, It as ContentMigrator, _ as SectionRegistry, g as ContentModel, y as ThemeDefinition } from "./page-C87bm-bf.js";
1
+ import { A as SectionRegistry, J as SiteDefinition, M as ThemeDefinition, Yt as ContentMigrator, k as ContentModel } from "./page-BFuPEq6Q.js";
2
2
  //#region src/images.d.ts
3
3
  type ImageKind = 'png' | 'jpeg' | 'webp' | 'gif' | 'avif';
4
4
  interface SavedImage {
@@ -11,6 +11,20 @@ interface ListedImage {
11
11
  path: string;
12
12
  }
13
13
  //#endregion
14
+ //#region src/videos.d.ts
15
+ type VideoKind = 'mp4' | 'mov' | 'webm' | 'ogv';
16
+ interface SavedVideo {
17
+ filename: string;
18
+ /** Project-relative path of the file. */
19
+ path: string;
20
+ /** What a Media section's `video` field holds: the file's address on the site, such as `/videos/demo-1a2b.mp4`. */
21
+ url: string;
22
+ bytes: number;
23
+ }
24
+ type ListedVideo = Omit<SavedVideo, 'bytes'> & {
25
+ bytes?: number;
26
+ };
27
+ //#endregion
14
28
  //#region src/store.d.ts
15
29
  /**
16
30
  * Where the editor reads and writes content. The editor API talks only to this interface, so the
@@ -37,6 +51,8 @@ interface ContentStore {
37
51
  apply(operations: readonly StoreOperation[]): Promise<void>;
38
52
  /** Uploaded images. */
39
53
  readonly uploads: UploadStore;
54
+ /** Uploaded videos, served from `public/`. Absent in stores that only hold images. */
55
+ readonly videos?: VideoStore;
40
56
  /**
41
57
  * Whether the image file an image field points at (`src/assets/...`) exists. Saves that point at a missing file
42
58
  * fail validation. A store without it skips that check.
@@ -62,6 +78,15 @@ interface UploadStore {
62
78
  /** Delete one upload by file name. False when there is no such upload. */
63
79
  remove(filename: string): Promise<boolean>;
64
80
  }
81
+ interface VideoStore {
82
+ list(): Promise<ListedVideo[]>;
83
+ save(input: {
84
+ originalName: string;
85
+ bytes: Uint8Array;
86
+ kind: VideoKind;
87
+ }): Promise<SavedVideo>;
88
+ remove(filename: string): Promise<boolean>;
89
+ }
65
90
  /** A key that is empty, absolute or leaves the content directory. */
66
91
  declare class StoreKeyError extends Error {
67
92
  constructor(key: string);
@@ -71,9 +96,11 @@ interface FileStoreOptions {
71
96
  root: string;
72
97
  contentDir: string;
73
98
  uploadsDir: string;
99
+ /** Where uploaded videos go. Without it the store takes no video uploads. */
100
+ videosDir?: string;
74
101
  }
75
102
  /** The content store for a local project: content as JSON files, uploads in a folder. */
76
- declare function createFileStore({ root, contentDir, uploadsDir }: FileStoreOptions): ContentStore;
103
+ declare function createFileStore({ root, contentDir, uploadsDir, videosDir }: FileStoreOptions): ContentStore;
77
104
  //#endregion
78
105
  //#region src/options.d.ts
79
106
  interface BearminionOptions extends ContentModel {
@@ -81,6 +108,10 @@ interface BearminionOptions extends ContentModel {
81
108
  contentDir?: string;
82
109
  /** Directory for uploaded images. Default `src/assets/uploads`. */
83
110
  uploadsDir?: string;
111
+ /** Directory for uploaded videos. It must be inside `public/`, which serves them as they are. Default `public/videos`. */
112
+ videosDir?: string;
113
+ /** Largest video the editor accepts, in bytes. Default 50 MB. */
114
+ maxVideoBytes?: number;
84
115
  /**
85
116
  * Vite module id loaded by the dev server to render a draft page.
86
117
  * Default `/src/lib/render-preview.ts`. Preview never writes to disk.
@@ -102,6 +133,8 @@ interface EditorContext {
102
133
  root: string;
103
134
  contentDir: string;
104
135
  uploadsDir: string;
136
+ videosDir: string;
137
+ maxVideoBytes: number;
105
138
  sections: SectionRegistry;
106
139
  site: SiteDefinition;
107
140
  theme: ThemeDefinition;
@@ -567,6 +567,7 @@ declare const siteSeoSchema: z.ZodObject<{
567
567
  titleTemplate: z.ZodOptional<z.ZodString>;
568
568
  description: z.ZodOptional<z.ZodString>;
569
569
  ogImage: z.ZodOptional<z.ZodString>;
570
+ jsonLd: z.ZodOptional<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
570
571
  }, z.core.$strict>;
571
572
  /**
572
573
  * Raw HTML snippets for analytics and similar tags. A theme renders `head` at the end of `<head>` and
@@ -677,6 +678,7 @@ declare function createSiteSchema(header: HeaderParts, footer: FooterParts): z.Z
677
678
  titleTemplate: z.ZodOptional<z.ZodString>;
678
679
  description: z.ZodOptional<z.ZodString>;
679
680
  ogImage: z.ZodOptional<z.ZodString>;
681
+ jsonLd: z.ZodOptional<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
680
682
  }, z.core.$strict>;
681
683
  analytics: z.ZodOptional<z.ZodObject<{
682
684
  head: z.ZodOptional<z.ZodString>;
@@ -856,6 +858,7 @@ declare function createSiteSchema(header: HeaderParts, footer: FooterParts): z.Z
856
858
  titleTemplate?: string | undefined;
857
859
  description?: string | undefined;
858
860
  ogImage?: string | undefined;
861
+ jsonLd?: Record<string, unknown>[] | undefined;
859
862
  };
860
863
  theme: {
861
864
  mode: "light" | "dark";
@@ -991,6 +994,7 @@ declare function createSiteSchema(header: HeaderParts, footer: FooterParts): z.Z
991
994
  titleTemplate?: string | undefined;
992
995
  description?: string | undefined;
993
996
  ogImage?: string | undefined;
997
+ jsonLd?: Record<string, unknown>[] | undefined;
994
998
  };
995
999
  theme: {
996
1000
  mode: "light" | "dark";
@@ -1268,6 +1272,57 @@ interface ContentModel extends ContentVersioning {
1268
1272
  theme?: ThemeDefinition;
1269
1273
  }
1270
1274
  //#endregion
1275
+ //#region src/json-ld.d.ts
1276
+ /** JSON-LD blocks as a page stores them: parsed, so diffs stay readable and output can be escaped safely. */
1277
+ type JsonLdBlock = Record<string, unknown>;
1278
+ type JsonLdResult = {
1279
+ ok: true;
1280
+ value: JsonLdBlock[];
1281
+ } | {
1282
+ ok: false;
1283
+ error: string;
1284
+ };
1285
+ /**
1286
+ * Parses what a person pastes into the JSON-LD box: an object, an array of objects, or either one inside a
1287
+ * `<script type="application/ld+json">` tag. Empty input is valid and means "no custom JSON-LD".
1288
+ */
1289
+ declare function parseJsonLdInput(raw: string): JsonLdResult;
1290
+ /** The text shown in the editor box: one block as an object, several as an array, none as empty. */
1291
+ declare function formatJsonLd(blocks: readonly JsonLdBlock[] | undefined): string;
1292
+ /** Advice, not errors: a block without `@type` or `@context` is usually a paste mistake, but Google decides what it accepts. */
1293
+ declare function jsonLdWarnings(blocks: readonly JsonLdBlock[]): string[];
1294
+ /** Custom structured data. At most 10 blocks and 50 KB, so a stray paste cannot bloat every page. */
1295
+ declare const jsonLdSchema: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1296
+ /** The `@id` the generated site blocks use, so a page's markup can point at them. */
1297
+ interface SiteJsonLdIds {
1298
+ organization: string;
1299
+ website: string;
1300
+ }
1301
+ /** `<site.url>/#organization` and `<site.url>/#website`, with trailing slashes on the site URL ignored. */
1302
+ declare function siteJsonLdIds(siteUrl: string): SiteJsonLdIds;
1303
+ /** Whether a block with this `@id` is in the list. */
1304
+ declare function hasJsonLdId(blocks: readonly JsonLdBlock[] | undefined, id: string): boolean;
1305
+ /**
1306
+ * The blocks a page emits: every site block, then the page's own. A block with the same `@id` is
1307
+ * emitted once and the page's copy wins, so a page can refine what the site says without repeating it.
1308
+ */
1309
+ declare function mergeJsonLd(site: readonly JsonLdBlock[] | undefined, page: readonly JsonLdBlock[]): JsonLdBlock[];
1310
+ /** What the Generate button reads from the site settings. */
1311
+ interface SiteJsonLdSource {
1312
+ name: string;
1313
+ url: string;
1314
+ /** BCP 47 tag. Default `en`. */
1315
+ lang?: string;
1316
+ /** An absolute https address. Local images get their address at build time, so they cannot go here. */
1317
+ logo?: string;
1318
+ }
1319
+ /** An `Organization` and a `WebSite` block for the site, in the `@id` convention of {@link siteJsonLdIds}. */
1320
+ declare function generateSiteJsonLd(source: SiteJsonLdSource): JsonLdBlock[];
1321
+ /** Puts generated blocks into a list: a block with the same `@id` is replaced in place, the rest is kept. */
1322
+ declare function upsertJsonLd(blocks: readonly JsonLdBlock[], generated: readonly JsonLdBlock[]): JsonLdBlock[];
1323
+ /** True while the site URL is still the template's `example.com` placeholder. */
1324
+ declare function isPlaceholderSiteUrl(siteUrl: string): boolean;
1325
+ //#endregion
1271
1326
  //#region src/section-schema.d.ts
1272
1327
  declare const sectionIdSchema: z.ZodString;
1273
1328
  declare const anchorSchema: z.ZodString;
@@ -1301,8 +1356,6 @@ type PropsOf<TRegistry extends SectionRegistry, TKey extends keyof TRegistry & s
1301
1356
  declare function createSectionSchema<TRegistry extends SectionRegistry>(registry: TRegistry): z.ZodType<SectionOf<TRegistry>>;
1302
1357
  //#endregion
1303
1358
  //#region src/page.d.ts
1304
- /** Custom structured data. At most 10 blocks and 50 KB, so a stray paste cannot bloat every page. */
1305
- declare const jsonLdSchema: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1306
1359
  declare const seoSchema: z.ZodObject<{
1307
1360
  title: z.ZodString;
1308
1361
  description: z.ZodString;
@@ -1336,4 +1389,4 @@ declare function withoutOrphanSections<TPage extends {
1336
1389
  sections: readonly unknown[];
1337
1390
  }>(page: TPage, registry: SectionRegistry): TPage;
1338
1391
  //#endregion
1339
- export { Tone as $, HeaderConfig as A, variantOption as At, brandLogoSchema as B, createMigrator as Bt, BRAND_LOGO_HEIGHT as C, Option as Ct, FooterColumn as D, themeModes as Dt, Footer as E, motionOption as Et, SiteDefinition as F, ContentMigration as Ft, themeColorsSchema as G, siteAnalyticsSchema as H, SiteOf as I, ContentMigrator as It, Image as J, themeSchema as K, SiteSeo as L, ContentVersioning as Lt, SiteAnalytics as M, FieldIcons as Mt, SiteConfig as N, ShowCondition as Nt, FooterConfig as O, toneOption as Ot, SiteConsent as P, ShowWhen as Pt, Text as Q, SiteWith as R, DocumentKind as Rt, defineTheme as S, NumberOption as St, DEFAULT_MAX_HEADER_ACTIONS as T, buttonStyles as Tt, siteConsentSchema as U, defineSite as V, siteSeoSchema as W, MAX_NAV_DEPTH as X, Link as Y, RichText as Z, SectionRegistry as _, wireframeIssues as _t, isSystemPage as a, textSchema as at, ThemeFont as b, CountRule as bt, withoutOrphanSections as c, withEmphasis as ct, SectionOf as d, defineSection as dt, htmlIdSchema as et, anchorSchema as f, WireKind as ft, ContentModel as g, wireKinds as gt, API_PREFIX as h, defineWireframe as ht, createPageSchema as i, textMarkSchema as it, Site as j, FieldChoices as jt, Header as k, tones as kt, BaseSection as l, SectionConfig as lt, sectionIdSchema as m, Wireframe as mt, SYSTEM_PAGES as n, linkSchema as nt, jsonLdSchema as o, toneSchema as ot, createSectionSchema as p, WireRect as pt, ButtonStyle as q, Seo as r, richTextSchema as rt, seoSchema as s, trackingIdSchema as st, Page as t, imageSchema as tt, PropsOf as u, SectionDefinition as ut, ThemeConfig as v, OptionalContent as vt, DEFAULT_MAX_FOOTER_COLUMNS as w, StringOption as wt, defaultTheme as x, EnumOption as xt, ThemeDefinition as y, BooleanOption as yt, Theme as z, SCHEMA_VERSION_KEY as zt };
1392
+ export { brandLogoSchema as $, createMigrator as $t, SectionRegistry as A, wireframeIssues as At, FooterColumn as B, themeModes as Bt, jsonLdWarnings as C, SectionDefinition as Ct, upsertJsonLd as D, Wireframe as Dt, siteJsonLdIds as E, WireRect as Et, defineTheme as F, NumberOption as Ft, SiteAnalytics as G, FieldIcons as Gt, Header as H, tones as Ht, BRAND_LOGO_HEIGHT as I, Option as It, SiteDefinition as J, ContentMigration as Jt, SiteConfig as K, ShowCondition as Kt, DEFAULT_MAX_FOOTER_COLUMNS as L, StringOption as Lt, ThemeDefinition as M, BooleanOption as Mt, ThemeFont as N, CountRule as Nt, API_PREFIX as O, defineWireframe as Ot, defaultTheme as P, EnumOption as Pt, Theme as Q, SCHEMA_VERSION_KEY as Qt, DEFAULT_MAX_HEADER_ACTIONS as R, buttonStyles as Rt, jsonLdSchema as S, SectionConfig as St, parseJsonLdInput as T, WireKind as Tt, HeaderConfig as U, variantOption as Ut, FooterConfig as V, toneOption as Vt, Site as W, FieldChoices as Wt, SiteSeo as X, ContentVersioning as Xt, SiteOf as Y, ContentMigrator as Yt, SiteWith as Z, DocumentKind as Zt, SiteJsonLdSource as _, textMarkSchema as _t, isSystemPage as a, themeSchema as at, hasJsonLdId as b, trackingIdSchema as bt, BaseSection as c, Link as ct, anchorSchema as d, Text as dt, defineSite as et, createSectionSchema as f, Tone as ft, SiteJsonLdIds as g, richTextSchema as gt, JsonLdResult as h, linkSchema as ht, createPageSchema as i, themeColorsSchema as it, ThemeConfig as j, OptionalContent as jt, ContentModel as k, wireKinds as kt, PropsOf as l, MAX_NAV_DEPTH as lt, JsonLdBlock as m, imageSchema as mt, SYSTEM_PAGES as n, siteConsentSchema as nt, seoSchema as o, ButtonStyle as ot, sectionIdSchema as p, htmlIdSchema as pt, SiteConsent as q, ShowWhen as qt, Seo as r, siteSeoSchema as rt, withoutOrphanSections as s, Image as st, Page as t, siteAnalyticsSchema as tt, SectionOf as u, RichText as ut, formatJsonLd as v, textSchema as vt, mergeJsonLd as w, defineSection as wt, isPlaceholderSiteUrl as x, withEmphasis as xt, generateSiteJsonLd as y, toneSchema as yt, Footer as z, motionOption as zt };
@@ -203,6 +203,130 @@ function addMarkIssues(text, marks, ctx, path = ["marks"]) {
203
203
  });
204
204
  }
205
205
  //#endregion
206
+ //#region src/json-ld.ts
207
+ /** What generators hand out: the JSON wrapped in its own script tag. */
208
+ const SCRIPT_WRAPPER = /^\s*<script\b[^>]*\bapplication\/ld\+json\b[^>]*>([\s\S]*?)<\/script\s*>\s*$/i;
209
+ /**
210
+ * Parses what a person pastes into the JSON-LD box: an object, an array of objects, or either one inside a
211
+ * `<script type="application/ld+json">` tag. Empty input is valid and means "no custom JSON-LD".
212
+ */
213
+ function parseJsonLdInput(raw) {
214
+ const body = (SCRIPT_WRAPPER.exec(raw)?.[1] ?? raw).trim();
215
+ if (body === "") return {
216
+ ok: true,
217
+ value: []
218
+ };
219
+ let data;
220
+ try {
221
+ data = JSON.parse(body);
222
+ } catch (error) {
223
+ return {
224
+ ok: false,
225
+ error: error instanceof Error ? error.message : "Invalid JSON"
226
+ };
227
+ }
228
+ const items = Array.isArray(data) ? data : [data];
229
+ if (items.length === 0) return {
230
+ ok: true,
231
+ value: []
232
+ };
233
+ if (!items.every(isBlock)) return {
234
+ ok: false,
235
+ error: "JSON-LD must be an object or an array of objects."
236
+ };
237
+ return {
238
+ ok: true,
239
+ value: items
240
+ };
241
+ }
242
+ /** The text shown in the editor box: one block as an object, several as an array, none as empty. */
243
+ function formatJsonLd(blocks) {
244
+ if (!blocks || blocks.length === 0) return "";
245
+ return JSON.stringify(blocks.length === 1 ? blocks[0] : blocks, null, 2);
246
+ }
247
+ /** Advice, not errors: a block without `@type` or `@context` is usually a paste mistake, but Google decides what it accepts. */
248
+ function jsonLdWarnings(blocks) {
249
+ const warnings = [];
250
+ blocks.forEach((block, index) => {
251
+ const name = blocks.length === 1 ? "The block" : `Block ${index + 1}`;
252
+ if (!("@context" in block)) warnings.push(`${name} has no @context. Search engines expect "https://schema.org".`);
253
+ if (!("@type" in block) && !("@graph" in block)) warnings.push(`${name} has no @type.`);
254
+ });
255
+ return warnings;
256
+ }
257
+ function isBlock(value) {
258
+ return typeof value === "object" && value !== null && !Array.isArray(value);
259
+ }
260
+ /** Custom structured data. At most 10 blocks and 50 KB, so a stray paste cannot bloat every page. */
261
+ const jsonLdSchema = z.array(z.record(z.string(), z.unknown())).max(10).refine((blocks) => JSON.stringify(blocks).length <= 5e4, "JSON-LD is limited to 50 KB");
262
+ /** `<site.url>/#organization` and `<site.url>/#website`, with trailing slashes on the site URL ignored. */
263
+ function siteJsonLdIds(siteUrl) {
264
+ const base = siteUrl.replace(/\/+$/, "");
265
+ return {
266
+ organization: `${base}/#organization`,
267
+ website: `${base}/#website`
268
+ };
269
+ }
270
+ function blockId(block) {
271
+ const id = block["@id"];
272
+ return typeof id === "string" ? id : void 0;
273
+ }
274
+ /** Whether a block with this `@id` is in the list. */
275
+ function hasJsonLdId(blocks, id) {
276
+ return (blocks ?? []).some((block) => blockId(block) === id);
277
+ }
278
+ /**
279
+ * The blocks a page emits: every site block, then the page's own. A block with the same `@id` is
280
+ * emitted once and the page's copy wins, so a page can refine what the site says without repeating it.
281
+ */
282
+ function mergeJsonLd(site, page) {
283
+ const taken = new Set(page.map(blockId).filter((id) => id !== void 0));
284
+ return [...(site ?? []).filter((block) => {
285
+ const id = blockId(block);
286
+ return id === void 0 || !taken.has(id);
287
+ }), ...page];
288
+ }
289
+ /** An `Organization` and a `WebSite` block for the site, in the `@id` convention of {@link siteJsonLdIds}. */
290
+ function generateSiteJsonLd(source) {
291
+ const ids = siteJsonLdIds(source.url);
292
+ const url = `${source.url.replace(/\/+$/, "")}/`;
293
+ return [{
294
+ "@context": "https://schema.org",
295
+ "@type": "Organization",
296
+ "@id": ids.organization,
297
+ name: source.name,
298
+ url,
299
+ ...source.logo ? { logo: source.logo } : {}
300
+ }, {
301
+ "@context": "https://schema.org",
302
+ "@type": "WebSite",
303
+ "@id": ids.website,
304
+ name: source.name,
305
+ url,
306
+ inLanguage: source.lang || "en"
307
+ }];
308
+ }
309
+ /** Puts generated blocks into a list: a block with the same `@id` is replaced in place, the rest is kept. */
310
+ function upsertJsonLd(blocks, generated) {
311
+ const next = [...blocks];
312
+ for (const block of generated) {
313
+ const id = blockId(block);
314
+ const at = id === void 0 ? -1 : next.findIndex((item) => blockId(item) === id);
315
+ if (at >= 0) next[at] = block;
316
+ else next.push(block);
317
+ }
318
+ return next;
319
+ }
320
+ /** True while the site URL is still the template's `example.com` placeholder. */
321
+ function isPlaceholderSiteUrl(siteUrl) {
322
+ try {
323
+ const host = new URL(siteUrl).hostname.toLowerCase();
324
+ return host === "example.com" || host.endsWith(".example.com");
325
+ } catch {
326
+ return false;
327
+ }
328
+ }
329
+ //#endregion
206
330
  //#region src/link-ids.ts
207
331
  /** Walk objects that look like links (`label` + `href`) and report each `id`. */
208
332
  function walkLinkIds(value, path, visit) {
@@ -362,8 +486,6 @@ function addSectionIdentityIssues(sections, ctx) {
362
486
  });
363
487
  addLinkIdIssues(sections, ctx);
364
488
  }
365
- /** Custom structured data. At most 10 blocks and 50 KB, so a stray paste cannot bloat every page. */
366
- const jsonLdSchema = z.array(z.record(z.string(), z.unknown())).max(10).refine((blocks) => JSON.stringify(blocks).length <= 5e4, "JSON-LD is limited to 50 KB");
367
489
  const seoSchema = z.object({
368
490
  title: textSchema,
369
491
  description: z.string(),
@@ -425,4 +547,4 @@ function withoutOrphanSections(page, registry) {
425
547
  };
426
548
  }
427
549
  //#endregion
428
- export { isImagePosition as A, trackingIdSchema as C, IMAGE_FITS as D, DEFAULT_IMAGE_POSITION as E, toneOption as F, tones as I, variantOption as L, buttonStyles as M, motionOption as N, IMAGE_POSITIONS as O, themeModes as P, toneSchema as S, DEFAULT_IMAGE_FIT as T, imageSchema as _, jsonLdSchema as a, textMarkSchema as b, withoutOrphanSections as c, isOrphanSection as d, sectionIdSchema as f, htmlIdSchema as g, addMarkIssues as h, isSystemPage as i, withImageLayout as j, isImageFit as k, anchorSchema as l, MAX_NAV_DEPTH as m, createPageSchema as n, seoSchema as o, walkLinkIds as p, createTolerantPageSchema as r, slugSchema as s, SYSTEM_PAGES as t, createSectionSchema as u, linkSchema as v, withEmphasis as w, textSchema as x, richTextSchema as y };
550
+ export { textSchema as A, withImageLayout as B, MAX_NAV_DEPTH as C, linkSchema as D, imageSchema as E, DEFAULT_IMAGE_POSITION as F, tones as G, motionOption as H, IMAGE_FITS as I, variantOption as K, IMAGE_POSITIONS as L, trackingIdSchema as M, withEmphasis as N, richTextSchema as O, DEFAULT_IMAGE_FIT as P, isImageFit as R, upsertJsonLd as S, htmlIdSchema as T, themeModes as U, buttonStyles as V, toneOption as W, jsonLdSchema as _, seoSchema as a, parseJsonLdInput as b, anchorSchema as c, sectionIdSchema as d, walkLinkIds as f, isPlaceholderSiteUrl as g, hasJsonLdId as h, isSystemPage as i, toneSchema as j, textMarkSchema as k, createSectionSchema as l, generateSiteJsonLd as m, createPageSchema as n, slugSchema as o, formatJsonLd as p, createTolerantPageSchema as r, withoutOrphanSections as s, SYSTEM_PAGES as t, isOrphanSection as u, jsonLdWarnings as v, addMarkIssues as w, siteJsonLdIds as x, mergeJsonLd as y, isImagePosition as z };
@@ -1,4 +1,4 @@
1
- import { r as fieldKey } from "./theme-DMpt3hcT.js";
1
+ import { r as fieldKey } from "./theme-DFjHjgFO.js";
2
2
  //#region src/errors.ts
3
3
  function formatContentIssues(issues) {
4
4
  return issues.map((issue) => {
@@ -1,4 +1,4 @@
1
- import { s as slugSchema } from "./page-ON4Jkcm-.js";
1
+ import { o as slugSchema } from "./page-NOFVEqn1.js";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import path from "node:path";
4
4
  //#region src/security.ts
@@ -1,21 +1,30 @@
1
1
  //#region src/rich-text.d.ts
2
2
  /**
3
3
  * The subset the editor toolbar writes: paragraphs, line breaks, bold, italic, inline code (backticks),
4
- * links, and bulleted or numbered lists. Headings and images are not supported.
4
+ * links, and bulleted or numbered lists. Images are not supported. Headings are not either, unless a field asks
5
+ * for them (`{ headings: true }`): then `## ` and `### ` lines are h2 and h3, and nothing else is a heading.
5
6
  * Stored as markdown. Rendered as a fixed set of tags, with text escaped.
6
7
  *
7
- * The output is always block content (`<p>`, `<ul>`, `<ol>`), so the element that holds it must accept blocks:
8
- * a `div` or a `blockquote`, never a `<p>` or a heading.
8
+ * The output is always block content (`<p>`, `<ul>`, `<ol>`, with headings `<h2>`, `<h3>`), so the element that
9
+ * holds it must accept blocks: a `div` or a `blockquote`, never a `<p>` or a heading.
9
10
  */
10
- export declare function renderRichText(markdown: string): string;
11
+ /** What a rich field allows beyond the base subset. */
12
+ export interface RichOptions {
13
+ /** Allow h2 and h3 as `## ` and `### ` lines. Page titles are h1, so nothing above h2 exists, and nothing below h3. */
14
+ headings?: boolean;
15
+ }
16
+ export declare function renderRichText(markdown: string, options?: RichOptions): string;
11
17
  /**
12
18
  * Pasted HTML, reduced to what a rich field can hold. The HTML is parsed (never inserted), read back as the
13
19
  * markdown subset, and rendered again, so only the renderer's own tags survive: no attributes, handlers or
14
20
  * elements from the clipboard reach the page. Headings and images are dropped. Browser only.
15
21
  */
16
- export declare function sanitizeRichHtml(html: string): string;
17
- /** Drop markdown headings and images from pasted plain text. */
18
- export declare function stripPlainRich(text: string): string;
22
+ export declare function sanitizeRichHtml(html: string, options?: RichOptions): string;
23
+ /**
24
+ * Drop markdown images from pasted plain text, and headings too unless the field allows them. Then a heading of any
25
+ * level becomes the nearest one the field has: `#` is `##`, and `####` and deeper are `###`.
26
+ */
27
+ export declare function stripPlainRich(text: string, options?: RichOptions): string;
19
28
  /** True when a link URL is safe to put in an href. */
20
29
  export declare function isSafeHref(url: string): boolean;
21
30
  export declare function escapeHtml(value: string): string;
@@ -23,7 +32,7 @@ export declare function escapeHtml(value: string): string;
23
32
  * Turn the formatted DOM of a rich field back into the stored markdown subset. The HTML is parsed with
24
33
  * `DOMParser`, which never runs scripts or loads resources. Browser only (the editor overlay).
25
34
  */
26
- export declare function markdownFromHtml(html: string): string;
35
+ export declare function markdownFromHtml(html: string, options?: RichOptions): string;
27
36
  export interface RichLinkSpan {
28
37
  label: string;
29
38
  href: string;
package/dist/rich-text.js CHANGED
@@ -1,27 +1,35 @@
1
1
  //#region src/rich-text.ts
2
- /**
3
- * The subset the editor toolbar writes: paragraphs, line breaks, bold, italic, inline code (backticks),
4
- * links, and bulleted or numbered lists. Headings and images are not supported.
5
- * Stored as markdown. Rendered as a fixed set of tags, with text escaped.
6
- *
7
- * The output is always block content (`<p>`, `<ul>`, `<ol>`), so the element that holds it must accept blocks:
8
- * a `div` or a `blockquote`, never a `<p>` or a heading.
9
- */
2
+ const HEADING_LINE = /^(#{2,3})[ \t]+(\S.*)$/;
10
3
  const BLOCKED_PROTOCOL = /^(?:javascript|data|vbscript):/i;
11
- function renderRichText(markdown) {
12
- return markdown.replace(/\r\n/g, "\n").split(/\n{2,}/).filter((block) => block.trim() !== "").map((block) => renderBlock(block)).map((html) => isListHtml(html) ? html : `<p>${html}</p>`).join("");
4
+ function renderRichText(markdown, options = {}) {
5
+ const headings = options.headings === true;
6
+ const text = markdown.replace(/\r\n/g, "\n");
7
+ return (headings ? text.split("\n").map((line) => HEADING_LINE.test(line.trim()) ? `\n\n${line.trim()}\n\n` : line).join("\n") : text).split(/\n{2,}/).filter((block) => block.trim() !== "").map((block) => {
8
+ const heading = headings ? HEADING_LINE.exec(block.trim()) : null;
9
+ if (heading) {
10
+ const tag = heading[1]?.length === 2 ? "h2" : "h3";
11
+ return `<${tag}>${renderInline((heading[2] ?? "").trim())}</${tag}>`;
12
+ }
13
+ const html = renderBlock(block);
14
+ return isListHtml(html) ? html : `<p>${html}</p>`;
15
+ }).join("");
13
16
  }
14
17
  /**
15
18
  * Pasted HTML, reduced to what a rich field can hold. The HTML is parsed (never inserted), read back as the
16
19
  * markdown subset, and rendered again, so only the renderer's own tags survive: no attributes, handlers or
17
20
  * elements from the clipboard reach the page. Headings and images are dropped. Browser only.
18
21
  */
19
- function sanitizeRichHtml(html) {
20
- return renderRichText(markdownFromHtml(html));
22
+ function sanitizeRichHtml(html, options = {}) {
23
+ return renderRichText(markdownFromHtml(html, options), options);
21
24
  }
22
- /** Drop markdown headings and images from pasted plain text. */
23
- function stripPlainRich(text) {
24
- return text.replace(/!\[[^\]]*\]\([^)\n]*\)/g, "").replace(/^#{1,6}[ \t]+.*$/gm, "");
25
+ /**
26
+ * Drop markdown images from pasted plain text, and headings too unless the field allows them. Then a heading of any
27
+ * level becomes the nearest one the field has: `#` is `##`, and `####` and deeper are `###`.
28
+ */
29
+ function stripPlainRich(text, options = {}) {
30
+ const withoutImages = text.replace(/!\[[^\]]*\]\([^)\n]*\)/g, "");
31
+ if (options.headings !== true) return withoutImages.replace(/^#{1,6}[ \t]+.*$/gm, "");
32
+ return withoutImages.replace(/^#{4,6}([ \t]+)/gm, "###$1").replace(/^#([ \t]+)/gm, "##$1");
25
33
  }
26
34
  function renderBlock(block) {
27
35
  const lines = block.split("\n");
@@ -155,8 +163,8 @@ function escapeAttr(value) {
155
163
  * Turn the formatted DOM of a rich field back into the stored markdown subset. The HTML is parsed with
156
164
  * `DOMParser`, which never runs scripts or loads resources. Browser only (the editor overlay).
157
165
  */
158
- function markdownFromHtml(html) {
159
- return cleanup(serialize(parseHtml(html)));
166
+ function markdownFromHtml(html, options = {}) {
167
+ return cleanup(serialize(parseHtml(html, options.headings === true)));
160
168
  }
161
169
  /** Elements whose content is dropped with them. */
162
170
  const DROPPED = /* @__PURE__ */ new Set([
@@ -171,7 +179,10 @@ const DROPPED = /* @__PURE__ */ new Set([
171
179
  "img",
172
180
  "picture",
173
181
  "video",
174
- "audio",
182
+ "audio"
183
+ ]);
184
+ /** Headings a field without them drops, with their text. */
185
+ const HEADING_TAGS = /* @__PURE__ */ new Set([
175
186
  "h1",
176
187
  "h2",
177
188
  "h3",
@@ -179,16 +190,16 @@ const DROPPED = /* @__PURE__ */ new Set([
179
190
  "h5",
180
191
  "h6"
181
192
  ]);
182
- function parseHtml(html) {
193
+ function parseHtml(html, headings) {
183
194
  const root = {
184
195
  mark: "root",
185
196
  children: []
186
197
  };
187
198
  const body = new DOMParser().parseFromString(`<!doctype html><body>${html}`, "text/html").body;
188
- appendChildren(body, root);
199
+ appendChildren(body, root, headings);
189
200
  return root;
190
201
  }
191
- function appendChildren(parent, into) {
202
+ function appendChildren(parent, into, headings) {
192
203
  for (const child of Array.from(parent.childNodes)) {
193
204
  if (child.nodeType === TEXT_NODE) {
194
205
  into.children.push({
@@ -202,6 +213,7 @@ function appendChildren(parent, into) {
202
213
  const element = child;
203
214
  const name = element.tagName.toLowerCase();
204
215
  if (DROPPED.has(name)) continue;
216
+ if (HEADING_TAGS.has(name) && !headings) continue;
205
217
  if (name === "br") {
206
218
  into.children.push({
207
219
  mark: "text",
@@ -212,7 +224,7 @@ function appendChildren(parent, into) {
212
224
  }
213
225
  const mark = blockOf(name) ?? markOf(name, element.getAttribute("style") ?? "");
214
226
  if (!mark) {
215
- appendChildren(element, into);
227
+ appendChildren(element, into, headings);
216
228
  continue;
217
229
  }
218
230
  const tab = mark === "link" ? element.getAttribute("data-new-tab") : null;
@@ -223,12 +235,14 @@ function appendChildren(parent, into) {
223
235
  newTab: tab === "true" ? "new" : tab === "false" ? "same" : void 0
224
236
  };
225
237
  into.children.push(node);
226
- appendChildren(element, node);
238
+ appendChildren(element, node, headings);
227
239
  }
228
240
  }
229
241
  const ELEMENT_NODE = 1;
230
242
  const TEXT_NODE = 3;
231
243
  function blockOf(name) {
244
+ if (name === "h1" || name === "h2") return "h2";
245
+ if (name === "h3" || name === "h4" || name === "h5" || name === "h6") return "h3";
232
246
  if (name === "ul") return "ul";
233
247
  if (name === "ol") return "ol";
234
248
  if (name === "li") return "li";
@@ -260,6 +274,13 @@ function serializeFlow(nodes) {
260
274
  out += `${list}\n\n`;
261
275
  continue;
262
276
  }
277
+ if (node.mark === "h2" || node.mark === "h3") {
278
+ const text = node.children.map((child) => serializeInline(child)).join("").replace(/\s*\n+\s*/g, " ").trim();
279
+ if (text === "") continue;
280
+ if (out.trim() !== "" && !out.endsWith("\n\n")) out = out.replace(/\n*$/, "\n\n");
281
+ out += `${node.mark === "h2" ? "##" : "###"} ${text}\n\n`;
282
+ continue;
283
+ }
263
284
  if (node.mark === "block") {
264
285
  const inner = serializeFlow(node.children).replace(/\n+$/, "");
265
286
  if (inner === "") continue;
@@ -291,7 +312,7 @@ function serializeInline(node) {
291
312
  return text.trim() === "" ? text : `\`${text}\``;
292
313
  }
293
314
  if (node.mark === "ul" || node.mark === "ol") return serializeList(node);
294
- if (node.mark === "li" || node.mark === "block" || node.mark === "root") return serializeFlow(node.children);
315
+ if (node.mark === "li" || node.mark === "block" || node.mark === "root" || node.mark === "h2" || node.mark === "h3") return serializeFlow(node.mark === "h2" || node.mark === "h3" ? [node] : node.children);
295
316
  const inner = node.children.map((child) => serializeInline(child)).join("");
296
317
  if (!inner) return "";
297
318
  if (node.mark === "strong") return `**${inner}**`;
@@ -1,4 +1,4 @@
1
- import { I as tones, M as buttonStyles, P as themeModes } from "./page-ON4Jkcm-.js";
1
+ import { G as tones, U as themeModes, V as buttonStyles } from "./page-NOFVEqn1.js";
2
2
  import { fontMarks } from "./marks.js";
3
3
  //#region src/field-rules.ts
4
4
  /**
@@ -1,56 +1,31 @@
1
- //#region src/json-ld.ts
2
- /** What generators hand out: the JSON wrapped in its own script tag. */
3
- const SCRIPT_WRAPPER = /^\s*<script\b[^>]*\bapplication\/ld\+json\b[^>]*>([\s\S]*?)<\/script\s*>\s*$/i;
4
- /**
5
- * Parses what a person pastes into the JSON-LD box: an object, an array of objects, or either one inside a
6
- * `<script type="application/ld+json">` tag. Empty input is valid and means "no custom JSON-LD".
7
- */
8
- function parseJsonLdInput(raw) {
9
- const body = (SCRIPT_WRAPPER.exec(raw)?.[1] ?? raw).trim();
10
- if (body === "") return {
11
- ok: true,
12
- value: []
13
- };
14
- let data;
15
- try {
16
- data = JSON.parse(body);
17
- } catch (error) {
1
+ //#region src/media-source.ts
2
+ const YOUTUBE = /^https?:\/\/(?:(?:www|m)\.)?(?:youtube\.com\/(?:watch\?(?:[^#\s]*&)?v=|embed\/|shorts\/|live\/)|youtube-nocookie\.com\/embed\/|youtu\.be\/)([\w-]{6,})(?:[?&#][^\s]*)?$/i;
3
+ const VIMEO = /^https?:\/\/(?:(?:www|player)\.)?vimeo\.com\/(?:video\/)?(\d+)(?:[/?#][^\s]*)?$/i;
4
+ /** A path in `public/` (`/videos/demo.webm`) or an `https://` link to a video file. */
5
+ const VIDEO_FILE = /^(?:\/(?!\/)|https?:\/\/)\S+\.(?:mp4|webm|m4v|mov|ogv)(?:[?#]\S*)?$/i;
6
+ function parseMediaSource(value) {
7
+ const src = value.trim();
8
+ const youtube = YOUTUBE.exec(src);
9
+ if (youtube?.[1]) {
10
+ const id = youtube[1];
18
11
  return {
19
- ok: false,
20
- error: error instanceof Error ? error.message : "Invalid JSON"
12
+ kind: "youtube",
13
+ id,
14
+ embed: `https://www.youtube-nocookie.com/embed/${id}?autoplay=1&rel=0`,
15
+ thumbnail: `https://i.ytimg.com/vi/${id}/hqdefault.jpg`
21
16
  };
22
17
  }
23
- const items = Array.isArray(data) ? data : [data];
24
- if (items.length === 0) return {
25
- ok: true,
26
- value: []
27
- };
28
- if (!items.every(isBlock)) return {
29
- ok: false,
30
- error: "JSON-LD must be an object or an array of objects."
18
+ const vimeo = VIMEO.exec(src);
19
+ if (vimeo?.[1]) return {
20
+ kind: "vimeo",
21
+ id: vimeo[1],
22
+ embed: `https://player.vimeo.com/video/${vimeo[1]}?autoplay=1&dnt=1`
31
23
  };
32
- return {
33
- ok: true,
34
- value: items
24
+ if (VIDEO_FILE.test(src)) return {
25
+ kind: "file",
26
+ src
35
27
  };
36
- }
37
- /** The text shown in the editor box: one block as an object, several as an array, none as empty. */
38
- function formatJsonLd(blocks) {
39
- if (!blocks || blocks.length === 0) return "";
40
- return JSON.stringify(blocks.length === 1 ? blocks[0] : blocks, null, 2);
41
- }
42
- /** Advice, not errors: a block without `@type` or `@context` is usually a paste mistake, but Google decides what it accepts. */
43
- function jsonLdWarnings(blocks) {
44
- const warnings = [];
45
- blocks.forEach((block, index) => {
46
- const name = blocks.length === 1 ? "The block" : `Block ${index + 1}`;
47
- if (!("@context" in block)) warnings.push(`${name} has no @context. Search engines expect "https://schema.org".`);
48
- if (!("@type" in block) && !("@graph" in block)) warnings.push(`${name} has no @type.`);
49
- });
50
- return warnings;
51
- }
52
- function isBlock(value) {
53
- return typeof value === "object" && value !== null && !Array.isArray(value);
28
+ return null;
54
29
  }
55
30
  //#endregion
56
31
  //#region src/consent.ts
@@ -533,4 +508,4 @@ function contrastChecks(colors) {
533
508
  return checks;
534
509
  }
535
510
  //#endregion
536
- export { consentCopy as _, contrastRatio as a, jsonLdWarnings as b, hasThemeColors as c, tokenToHex as d, CONSENT_COPY as f, CONSENT_REOPEN as g, CONSENT_LIMITS as h, contrastChecks as i, hexToToken as l, CONSENT_HREF_MESSAGE as m, HEX_COLOR as n, derivePalette as o, CONSENT_HREF as p, OVERRIDE_TOKENS as r, hasOverrides as s, DEFAULT_BRAND as t, themeCss as u, consentTextIssues as v, parseJsonLdInput as x, formatJsonLd as y };
511
+ export { consentCopy as _, contrastRatio as a, parseMediaSource as b, hasThemeColors as c, tokenToHex as d, CONSENT_COPY as f, CONSENT_REOPEN as g, CONSENT_LIMITS as h, contrastChecks as i, hexToToken as l, CONSENT_HREF_MESSAGE as m, HEX_COLOR as n, derivePalette as o, CONSENT_HREF as p, OVERRIDE_TOKENS as r, hasOverrides as s, DEFAULT_BRAND as t, themeCss as u, consentTextIssues as v, VIDEO_FILE as y };