@wocart/theme 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 +71 -0
- package/README.md +299 -17
- package/dist/astro/InstanceView.astro +21 -5
- package/dist/astro/index.d.ts +15 -14
- package/dist/astro/index.js +44 -0
- package/dist/composition.d.ts +17 -0
- package/dist/composition.js +60 -0
- package/dist/definitions.js +305 -6
- package/dist/index.d.ts +18 -5
- package/dist/index.js +12 -4
- package/dist/instances.d.ts +7 -1
- package/dist/instances.js +251 -13
- package/dist/manifest-diff.d.ts +14 -0
- package/dist/manifest-diff.js +160 -0
- package/dist/manifest.d.ts +192 -19
- package/dist/manifest.js +77 -16
- package/dist/operations.d.ts +71 -0
- package/dist/operations.js +377 -0
- package/dist/page-data.js +29 -2
- package/dist/preview-bridge.d.ts +28 -0
- package/dist/preview-bridge.js +475 -0
- package/dist/preview-protocol.d.ts +147 -0
- package/dist/preview-protocol.js +51 -0
- package/dist/rich-text.d.ts +31 -0
- package/dist/rich-text.js +233 -0
- package/dist/schemas.d.ts +21 -0
- package/dist/schemas.js +35 -2
- package/dist/types.d.ts +259 -15
- package/dist/version.d.ts +7 -0
- package/dist/version.js +7 -0
- package/package.json +7 -2
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RichTextDocument, RichTextFormat } from "./types.js";
|
|
2
|
+
/** Every format a rich-text field can allow, in the order an editor lists them. */
|
|
3
|
+
export declare const RICH_TEXT_FORMATS: readonly RichTextFormat[];
|
|
4
|
+
/** Whether a stored value is meant as one document (rather than a per-language map). */
|
|
5
|
+
export declare function isRichTextDocumentShape(value: unknown): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Why `value` is not a valid rich-text document under `formats`, or `null`
|
|
8
|
+
* when it is. Absent `formats` allows every format.
|
|
9
|
+
*/
|
|
10
|
+
export declare function richTextProblem(value: unknown, formats?: readonly RichTextFormat[]): string | null;
|
|
11
|
+
/** How `richTextToHtml` writes links. */
|
|
12
|
+
export interface RichTextHtmlOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Turns a stored link target into the `href` to render — a theme passes its
|
|
15
|
+
* own localizing function so a site path keeps the shopper's language. The
|
|
16
|
+
* result is escaped; a target that fails the link rule never reaches it.
|
|
17
|
+
*/
|
|
18
|
+
href?: (target: string) => string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The document as HTML: `<p>`, `<h2>`–`<h4>`, `<ul>`/`<ol>`, `<strong>`, `<em>`,
|
|
22
|
+
* `<u>`, `<s>`, `<a href>` and `<br>`, every text and attribute escaped. Nodes
|
|
23
|
+
* outside the format and unsafe links are left out rather than trusted, so
|
|
24
|
+
* the output is safe for Astro's `set:html` even for an unchecked value.
|
|
25
|
+
*/
|
|
26
|
+
export declare function richTextToHtml(document: RichTextDocument, options?: RichTextHtmlOptions): string;
|
|
27
|
+
/**
|
|
28
|
+
* The document's text without formatting: blocks and list items on their own
|
|
29
|
+
* lines, line breaks kept — for meta descriptions, `aria-label`s and previews.
|
|
30
|
+
*/
|
|
31
|
+
export declare function richTextToPlainText(document: RichTextDocument): string;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rich text: a closed JSON format, checked on read and on write, rendered to
|
|
3
|
+
* escaped HTML or to plain text here — a theme never walks the nodes itself.
|
|
4
|
+
*
|
|
5
|
+
* The format has no way to carry markup: text is always text, links pass the
|
|
6
|
+
* `url` field's rule, and every node and key outside the format makes the
|
|
7
|
+
* value invalid. Rendering re-checks what it emits, so a value that skipped
|
|
8
|
+
* validation still cannot produce markup.
|
|
9
|
+
*/
|
|
10
|
+
import { isSafeLinkTarget } from "./schemas.js";
|
|
11
|
+
/** Every format a rich-text field can allow, in the order an editor lists them. */
|
|
12
|
+
export const RICH_TEXT_FORMATS = [
|
|
13
|
+
"bold",
|
|
14
|
+
"italic",
|
|
15
|
+
"underline",
|
|
16
|
+
"strike",
|
|
17
|
+
"link",
|
|
18
|
+
"heading",
|
|
19
|
+
"list",
|
|
20
|
+
];
|
|
21
|
+
const MARKS = ["bold", "italic", "underline", "strike"];
|
|
22
|
+
const MARK_TAGS = { bold: "strong", italic: "em", underline: "u", strike: "s" };
|
|
23
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
function unexpectedKey(value, allowed) {
|
|
25
|
+
return Object.keys(value).find((key) => !allowed.includes(key));
|
|
26
|
+
}
|
|
27
|
+
/** Whether a stored value is meant as one document (rather than a per-language map). */
|
|
28
|
+
export function isRichTextDocumentShape(value) {
|
|
29
|
+
return isRecord(value) && Object.hasOwn(value, "type");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Why `value` is not a valid rich-text document under `formats`, or `null`
|
|
33
|
+
* when it is. Absent `formats` allows every format.
|
|
34
|
+
*/
|
|
35
|
+
export function richTextProblem(value, formats) {
|
|
36
|
+
const allowed = new Set(formats ?? RICH_TEXT_FORMATS);
|
|
37
|
+
if (!isRecord(value))
|
|
38
|
+
return "must be a rich-text document";
|
|
39
|
+
const extra = unexpectedKey(value, ["type", "version", "content"]);
|
|
40
|
+
if (extra !== undefined)
|
|
41
|
+
return `has an unknown key "${extra}"`;
|
|
42
|
+
if (value.type !== "doc")
|
|
43
|
+
return 'must have type "doc"';
|
|
44
|
+
if (value.version !== 1)
|
|
45
|
+
return `has unsupported version ${JSON.stringify(value.version ?? null)}; this SDK reads version 1`;
|
|
46
|
+
if (!Array.isArray(value.content))
|
|
47
|
+
return "must have a `content` array";
|
|
48
|
+
for (const [index, block] of value.content.entries()) {
|
|
49
|
+
const problem = blockProblem(block, allowed);
|
|
50
|
+
if (problem !== null)
|
|
51
|
+
return `content[${index}] ${problem}`;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
function blockProblem(block, allowed) {
|
|
56
|
+
if (!isRecord(block))
|
|
57
|
+
return "must be a block";
|
|
58
|
+
switch (block.type) {
|
|
59
|
+
case "paragraph": {
|
|
60
|
+
const extra = unexpectedKey(block, ["type", "content"]);
|
|
61
|
+
if (extra !== undefined)
|
|
62
|
+
return `has an unknown key "${extra}"`;
|
|
63
|
+
return inlinesProblem(block.content, allowed);
|
|
64
|
+
}
|
|
65
|
+
case "heading": {
|
|
66
|
+
if (!allowed.has("heading"))
|
|
67
|
+
return "is a heading, which this field does not allow";
|
|
68
|
+
const extra = unexpectedKey(block, ["type", "level", "content"]);
|
|
69
|
+
if (extra !== undefined)
|
|
70
|
+
return `has an unknown key "${extra}"`;
|
|
71
|
+
if (block.level !== 2 && block.level !== 3 && block.level !== 4)
|
|
72
|
+
return "must have level 2, 3 or 4";
|
|
73
|
+
return inlinesProblem(block.content, allowed);
|
|
74
|
+
}
|
|
75
|
+
case "list": {
|
|
76
|
+
if (!allowed.has("list"))
|
|
77
|
+
return "is a list, which this field does not allow";
|
|
78
|
+
const extra = unexpectedKey(block, ["type", "style", "items"]);
|
|
79
|
+
if (extra !== undefined)
|
|
80
|
+
return `has an unknown key "${extra}"`;
|
|
81
|
+
if (block.style !== "bullet" && block.style !== "ordered")
|
|
82
|
+
return 'must have style "bullet" or "ordered"';
|
|
83
|
+
if (!Array.isArray(block.items) || block.items.length === 0)
|
|
84
|
+
return "must have at least one item";
|
|
85
|
+
for (const [index, item] of block.items.entries()) {
|
|
86
|
+
if (!isRecord(item))
|
|
87
|
+
return `items[${index}] must be an object`;
|
|
88
|
+
const itemExtra = unexpectedKey(item, ["content"]);
|
|
89
|
+
if (itemExtra !== undefined)
|
|
90
|
+
return `items[${index}] has an unknown key "${itemExtra}"`;
|
|
91
|
+
const problem = inlinesProblem(item.content, allowed);
|
|
92
|
+
if (problem !== null)
|
|
93
|
+
return `items[${index}] ${problem}`;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
default:
|
|
98
|
+
return `has unknown block type ${JSON.stringify(block.type ?? null)}`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function inlinesProblem(content, allowed) {
|
|
102
|
+
if (content === undefined)
|
|
103
|
+
return null;
|
|
104
|
+
if (!Array.isArray(content))
|
|
105
|
+
return "must have a `content` array";
|
|
106
|
+
for (const [index, inline] of content.entries()) {
|
|
107
|
+
const problem = inlineProblem(inline, allowed);
|
|
108
|
+
if (problem !== null)
|
|
109
|
+
return `content[${index}] ${problem}`;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
function inlineProblem(inline, allowed) {
|
|
114
|
+
if (!isRecord(inline))
|
|
115
|
+
return "must be a text or break node";
|
|
116
|
+
if (inline.type === "break") {
|
|
117
|
+
const extra = unexpectedKey(inline, ["type"]);
|
|
118
|
+
return extra === undefined ? null : `has an unknown key "${extra}"`;
|
|
119
|
+
}
|
|
120
|
+
if (inline.type !== "text")
|
|
121
|
+
return `has unknown inline type ${JSON.stringify(inline.type ?? null)}`;
|
|
122
|
+
const extra = unexpectedKey(inline, ["type", "text", "marks", "link"]);
|
|
123
|
+
if (extra !== undefined)
|
|
124
|
+
return `has an unknown key "${extra}"`;
|
|
125
|
+
if (typeof inline.text !== "string" || inline.text.length === 0)
|
|
126
|
+
return "must have non-empty `text`";
|
|
127
|
+
if (inline.marks !== undefined) {
|
|
128
|
+
if (!Array.isArray(inline.marks))
|
|
129
|
+
return "`marks` must be an array";
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
for (const mark of inline.marks) {
|
|
132
|
+
if (!MARKS.includes(mark))
|
|
133
|
+
return `has unknown mark ${JSON.stringify(mark)}`;
|
|
134
|
+
if (!allowed.has(mark))
|
|
135
|
+
return `uses "${String(mark)}", which this field does not allow`;
|
|
136
|
+
if (seen.has(mark))
|
|
137
|
+
return `repeats mark "${String(mark)}"`;
|
|
138
|
+
seen.add(mark);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (inline.link !== undefined) {
|
|
142
|
+
if (!allowed.has("link"))
|
|
143
|
+
return "has a link, which this field does not allow";
|
|
144
|
+
if (typeof inline.link !== "string" || !isSafeLinkTarget(inline.link)) {
|
|
145
|
+
return "`link` must be a site path, #fragment, ?query, or an http(s), mailto or tel URL";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const escapeHtml = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
151
|
+
/**
|
|
152
|
+
* The document as HTML: `<p>`, `<h2>`–`<h4>`, `<ul>`/`<ol>`, `<strong>`, `<em>`,
|
|
153
|
+
* `<u>`, `<s>`, `<a href>` and `<br>`, every text and attribute escaped. Nodes
|
|
154
|
+
* outside the format and unsafe links are left out rather than trusted, so
|
|
155
|
+
* the output is safe for Astro's `set:html` even for an unchecked value.
|
|
156
|
+
*/
|
|
157
|
+
export function richTextToHtml(document, options = {}) {
|
|
158
|
+
if (!isRecord(document) || !Array.isArray(document.content))
|
|
159
|
+
return "";
|
|
160
|
+
return document.content.map((block) => blockHtml(block, options)).join("");
|
|
161
|
+
}
|
|
162
|
+
function blockHtml(block, options) {
|
|
163
|
+
if (!isRecord(block))
|
|
164
|
+
return "";
|
|
165
|
+
switch (block.type) {
|
|
166
|
+
case "paragraph":
|
|
167
|
+
return `<p>${inlinesHtml(block.content, options)}</p>`;
|
|
168
|
+
case "heading": {
|
|
169
|
+
const level = block.level === 3 || block.level === 4 ? block.level : 2;
|
|
170
|
+
return `<h${level}>${inlinesHtml(block.content, options)}</h${level}>`;
|
|
171
|
+
}
|
|
172
|
+
case "list": {
|
|
173
|
+
const tag = block.style === "ordered" ? "ol" : "ul";
|
|
174
|
+
const items = Array.isArray(block.items) ? block.items : [];
|
|
175
|
+
return `<${tag}>${items.map((item) => `<li>${inlinesHtml(isRecord(item) ? item.content : undefined, options)}</li>`).join("")}</${tag}>`;
|
|
176
|
+
}
|
|
177
|
+
default:
|
|
178
|
+
return "";
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function inlinesHtml(content, options) {
|
|
182
|
+
if (!Array.isArray(content))
|
|
183
|
+
return "";
|
|
184
|
+
return content.map((inline) => inlineHtml(inline, options)).join("");
|
|
185
|
+
}
|
|
186
|
+
function inlineHtml(inline, options) {
|
|
187
|
+
if (!isRecord(inline))
|
|
188
|
+
return "";
|
|
189
|
+
if (inline.type === "break")
|
|
190
|
+
return "<br>";
|
|
191
|
+
if (inline.type !== "text" || typeof inline.text !== "string")
|
|
192
|
+
return "";
|
|
193
|
+
let html = escapeHtml(inline.text);
|
|
194
|
+
const marks = Array.isArray(inline.marks) ? inline.marks : [];
|
|
195
|
+
for (const mark of [...MARKS].reverse()) {
|
|
196
|
+
if (marks.includes(mark))
|
|
197
|
+
html = `<${MARK_TAGS[mark]}>${html}</${MARK_TAGS[mark]}>`;
|
|
198
|
+
}
|
|
199
|
+
if (typeof inline.link === "string" && isSafeLinkTarget(inline.link)) {
|
|
200
|
+
const href = options.href ? options.href(inline.link) : inline.link;
|
|
201
|
+
html = `<a href="${escapeHtml(href)}">${html}</a>`;
|
|
202
|
+
}
|
|
203
|
+
return html;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The document's text without formatting: blocks and list items on their own
|
|
207
|
+
* lines, line breaks kept — for meta descriptions, `aria-label`s and previews.
|
|
208
|
+
*/
|
|
209
|
+
export function richTextToPlainText(document) {
|
|
210
|
+
if (!isRecord(document) || !Array.isArray(document.content))
|
|
211
|
+
return "";
|
|
212
|
+
const lines = [];
|
|
213
|
+
for (const block of document.content) {
|
|
214
|
+
if (!isRecord(block))
|
|
215
|
+
continue;
|
|
216
|
+
if (block.type === "list") {
|
|
217
|
+
for (const item of Array.isArray(block.items) ? block.items : []) {
|
|
218
|
+
lines.push(inlinesText(isRecord(item) ? item.content : undefined));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else if (block.type === "paragraph" || block.type === "heading") {
|
|
222
|
+
lines.push(inlinesText(block.content));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return lines.join("\n");
|
|
226
|
+
}
|
|
227
|
+
function inlinesText(content) {
|
|
228
|
+
if (!Array.isArray(content))
|
|
229
|
+
return "";
|
|
230
|
+
return content
|
|
231
|
+
.map((inline) => (!isRecord(inline) ? "" : inline.type === "break" ? "\n" : inline.type === "text" && typeof inline.text === "string" ? inline.text : ""))
|
|
232
|
+
.join("");
|
|
233
|
+
}
|
package/dist/schemas.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export declare const TYPE_KEY_PATTERN: RegExp;
|
|
|
21
21
|
* not of the package's curated public surface (`src/index.ts`).
|
|
22
22
|
*/
|
|
23
23
|
export declare const SURFACE_KEY_PATTERN: RegExp;
|
|
24
|
+
/** The platform page types a surface can render (`SurfacePageType`), mirrored from the storefront's page types. */
|
|
25
|
+
export declare const PAGE_TYPES: readonly ["home", "catalog", "category", "collection", "product", "page", "landing", "checkout", "thankYou"];
|
|
24
26
|
/** A `color` field's value: a hex color, `#rgb` or `#rrggbb`. */
|
|
25
27
|
export declare const COLOR_PATTERN: RegExp;
|
|
26
28
|
/**
|
|
@@ -30,12 +32,31 @@ export declare const COLOR_PATTERN: RegExp;
|
|
|
30
32
|
*/
|
|
31
33
|
export declare const MENU_HANDLE_PATTERN: RegExp;
|
|
32
34
|
export declare const COLLECTION_HANDLE_PATTERN: RegExp;
|
|
35
|
+
/**
|
|
36
|
+
* A `product` or `category` value: a handle or slug as the store's URLs use
|
|
37
|
+
* it. Merchants may type their own, so the rule only keeps out what cannot be
|
|
38
|
+
* one URL path segment: whitespace, control characters, `/`, `\`, `?`, `#`
|
|
39
|
+
* and `%`, and anything over 255 characters.
|
|
40
|
+
*/
|
|
41
|
+
export declare const RESOURCE_HANDLE_PATTERN: RegExp;
|
|
42
|
+
/**
|
|
43
|
+
* A media object key in the store's bucket: a namespace and a name
|
|
44
|
+
* (`products/3f2a9c.webp`), segments of letters, digits, `.`, `_` and `-`
|
|
45
|
+
* that never start with a dot — so no `..`, no leading slash, no empty
|
|
46
|
+
* segment — and at most 512 characters.
|
|
47
|
+
*/
|
|
48
|
+
export declare const MEDIA_KEY_PATTERN: RegExp;
|
|
33
49
|
export declare function isLocaleTag(value: string): boolean;
|
|
34
50
|
/**
|
|
35
51
|
* Whether `value` is a per-language map (`{ ar: "…", fr: "…" }`): a plain
|
|
36
52
|
* object with at least one entry, every key a locale tag, every value a string.
|
|
37
53
|
*/
|
|
38
54
|
export declare function isLocaleMap(value: unknown): value is Readonly<Record<string, string>>;
|
|
55
|
+
/**
|
|
56
|
+
* Whether `value` is a per-language map whose entries pass `isEntry`: a plain
|
|
57
|
+
* object with at least one entry, every key a locale tag.
|
|
58
|
+
*/
|
|
59
|
+
export declare function isLocaleMapOf<T>(value: unknown, isEntry: (entry: unknown) => entry is T): value is Readonly<Record<string, T>>;
|
|
39
60
|
/**
|
|
40
61
|
* Whether a `url` field value is safe to render as an `href`: a site path,
|
|
41
62
|
* fragment or query, or an `http(s)`, `mailto` or `tel` URL. Rejects
|
package/dist/schemas.js
CHANGED
|
@@ -20,6 +20,18 @@ export const TYPE_KEY_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
|
20
20
|
* not of the package's curated public surface (`src/index.ts`).
|
|
21
21
|
*/
|
|
22
22
|
export const SURFACE_KEY_PATTERN = /^[a-z0-9][a-z0-9._:-]{0,63}$/;
|
|
23
|
+
/** The platform page types a surface can render (`SurfacePageType`), mirrored from the storefront's page types. */
|
|
24
|
+
export const PAGE_TYPES = [
|
|
25
|
+
"home",
|
|
26
|
+
"catalog",
|
|
27
|
+
"category",
|
|
28
|
+
"collection",
|
|
29
|
+
"product",
|
|
30
|
+
"page",
|
|
31
|
+
"landing",
|
|
32
|
+
"checkout",
|
|
33
|
+
"thankYou",
|
|
34
|
+
];
|
|
23
35
|
/** A `color` field's value: a hex color, `#rgb` or `#rrggbb`. */
|
|
24
36
|
export const COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
25
37
|
/**
|
|
@@ -29,6 +41,20 @@ export const COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
|
29
41
|
*/
|
|
30
42
|
export const MENU_HANDLE_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
31
43
|
export const COLLECTION_HANDLE_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
44
|
+
/**
|
|
45
|
+
* A `product` or `category` value: a handle or slug as the store's URLs use
|
|
46
|
+
* it. Merchants may type their own, so the rule only keeps out what cannot be
|
|
47
|
+
* one URL path segment: whitespace, control characters, `/`, `\`, `?`, `#`
|
|
48
|
+
* and `%`, and anything over 255 characters.
|
|
49
|
+
*/
|
|
50
|
+
export const RESOURCE_HANDLE_PATTERN = /^[^\s/\\?#%\u0000-\u001f\u007f]{1,255}$/u;
|
|
51
|
+
/**
|
|
52
|
+
* A media object key in the store's bucket: a namespace and a name
|
|
53
|
+
* (`products/3f2a9c.webp`), segments of letters, digits, `.`, `_` and `-`
|
|
54
|
+
* that never start with a dot — so no `..`, no leading slash, no empty
|
|
55
|
+
* segment — and at most 512 characters.
|
|
56
|
+
*/
|
|
57
|
+
export const MEDIA_KEY_PATTERN = /^(?=.{3,512}$)[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)+$/;
|
|
32
58
|
/**
|
|
33
59
|
* A BCP 47 language tag in canonical case: a 2–3 letter language, an optional
|
|
34
60
|
* 4-letter script, an optional 2-letter or 3-digit region (`ar`, `zgh`,
|
|
@@ -44,11 +70,17 @@ export function isLocaleTag(value) {
|
|
|
44
70
|
* object with at least one entry, every key a locale tag, every value a string.
|
|
45
71
|
*/
|
|
46
72
|
export function isLocaleMap(value) {
|
|
73
|
+
return isLocaleMapOf(value, (entry) => typeof entry === "string");
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Whether `value` is a per-language map whose entries pass `isEntry`: a plain
|
|
77
|
+
* object with at least one entry, every key a locale tag.
|
|
78
|
+
*/
|
|
79
|
+
export function isLocaleMapOf(value, isEntry) {
|
|
47
80
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
48
81
|
return false;
|
|
49
82
|
const entries = Object.entries(value);
|
|
50
|
-
return
|
|
51
|
-
entries.every(([key, text]) => isLocaleTag(key) && typeof text === "string"));
|
|
83
|
+
return entries.length > 0 && entries.every(([key, entry]) => isLocaleTag(key) && isEntry(entry));
|
|
52
84
|
}
|
|
53
85
|
/**
|
|
54
86
|
* Whether a `url` field value is safe to render as an `href`: a site path,
|
|
@@ -94,6 +126,7 @@ export const ComponentInstanceSchema = z.lazy(() => z.object({
|
|
|
94
126
|
props: z.exactOptional(z.record(z.string(), JsonValueSchema)),
|
|
95
127
|
children: z.exactOptional(z.array(ComponentInstanceSchema)),
|
|
96
128
|
slots: z.exactOptional(z.record(z.string(), z.array(ComponentInstanceSchema))),
|
|
129
|
+
hidden: z.exactOptional(z.boolean()),
|
|
97
130
|
}));
|
|
98
131
|
/**
|
|
99
132
|
* The envelope schema. **Loose on purpose**: unknown top-level keys
|