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.
- package/CHANGELOG.md +22 -0
- package/README.md +2 -2
- package/dist/astro.d.ts +2 -2
- package/dist/astro.js +234 -13
- package/dist/content.d.ts +1 -1
- package/dist/content.js +3 -3
- package/dist/index.d.ts +35 -23
- package/dist/index.js +9 -7
- package/dist/{migrate-B56wxCiC.js → migrate-b408EaLp.js} +3 -1
- package/dist/{options-qFsKXOuE.d.ts → options-DdCyRbPs.d.ts} +35 -2
- package/dist/{page-C87bm-bf.d.ts → page-BFuPEq6Q.d.ts} +56 -3
- package/dist/{page-ON4Jkcm-.js → page-NOFVEqn1.js} +125 -3
- package/dist/{preview-CTVEBdEY.js → preview--S1QUrMM.js} +1 -1
- package/dist/{reload-Cq434rtI.js → reload-D0vwI0OT.js} +1 -1
- package/dist/rich-text.d.ts +17 -8
- package/dist/rich-text.js +45 -24
- package/dist/{theme-DMpt3hcT.js → theme-DFjHjgFO.js} +1 -1
- package/dist/{theme-colors-zICpze42.js → theme-colors-CHROGZkI.js} +24 -49
- package/dist/ui/mount.js +902 -245
- package/package.json +5 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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 {
|
|
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 };
|
package/dist/rich-text.d.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
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,56 +1,31 @@
|
|
|
1
|
-
//#region src/
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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,
|
|
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 };
|