@wix/himalaya-cli 0.1079.0 → 0.1080.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.
Files changed (45) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/builder.d.ts +2 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +41 -1
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/site-shell.d.ts +0 -14
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/validator.d.ts +36 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/rich-content/src/display.d.ts +26 -0
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/rich-content/src/index.d.ts +79 -0
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +23 -49
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdui.mjs +11 -3
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-rich-content.mjs +344 -0
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/package.json +4 -0
  11. package/dist/cli.mjs +12 -4
  12. package/guides/importable-sdks.md +1 -0
  13. package/guides/sdk-reference.json +74 -1
  14. package/guides/splash-screens.md +13 -1
  15. package/package.json +2 -2
  16. package/templates/beauty-salon-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  17. package/templates/beauty-salon-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  18. package/templates/community-club-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  19. package/templates/community-club-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  20. package/templates/community-club-owner/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
  21. package/templates/events-meetups-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  22. package/templates/events-meetups-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  23. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
  24. package/templates/gym-fitness-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  25. package/templates/gym-fitness-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  26. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
  27. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  28. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  29. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
  30. package/templates/owner-lite/tier5-src/_shared/owner-post-content.ts +7 -26
  31. package/templates/owner-lite/tier5-src/_shared/owner-rich-guard.ts +12 -41
  32. package/templates/owner-lite/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
  33. package/templates/podcast-creator-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  34. package/templates/podcast-creator-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  35. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
  36. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  37. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  38. package/templates/restaurant-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  39. package/templates/restaurant-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  40. package/templates/restaurant-owner/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
  41. package/templates/retail-store-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  42. package/templates/retail-store-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  43. package/templates/yoga-studio-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  44. package/templates/yoga-studio-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  45. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
@@ -0,0 +1,344 @@
1
+ // ../../stdlib/wix-sdk/media/src/index.ts
2
+ var WIX_LIST_IMAGE = { width: 1e3, height: 1e3, quality: 85 };
3
+ var WIX_DETAIL_IMAGE = { width: 1400, height: 1400, quality: 85 };
4
+ var WIX_MEDIA = /^https?:\/\/static\.wixstatic\.com\/media\/[^/]/;
5
+ var WIX_IMAGE = /^wix:image:\/\/v1\/([^/]+)/i;
6
+ var WIX_TRANSFORM = /^(https?:\/\/static\.wixstatic\.com\/media\/[^/]+\/v1\/)([^/]+)\/([^/?#]+)(?:\/([^/?#]+))?([?#].*)?$/i;
7
+ var SIZING_PARAM = /^(w|h|q)_\d+$/i;
8
+ var WEB_URL = /^https?:\/\//i;
9
+ var RESIZABLE_MODES = /* @__PURE__ */ new Set(["fit", "fill"]);
10
+ var OWNED_PARAM = /^(w|h|q)_/i;
11
+ var DROPPED_PARAM = /^enc_/i;
12
+ function wixMediaId(reference) {
13
+ return WIX_IMAGE.exec(reference)?.[1] ?? (reference.includes("/") || reference.includes(":") ? "" : reference);
14
+ }
15
+ function resolveWixMediaReference(reference) {
16
+ const mediaId = wixMediaId(reference);
17
+ return mediaId ? `https://static.wixstatic.com/media/${mediaId}` : reference;
18
+ }
19
+ function requestedParams(size) {
20
+ return [`w_${size.width}`, `h_${size.height}`, `q_${size.quality ?? 85}`];
21
+ }
22
+ function fittedParams(size, source, mode) {
23
+ const quality = `q_${size.quality ?? 85}`;
24
+ const { width: sourceWidth, height: sourceHeight } = source;
25
+ if (sourceWidth && sourceHeight) {
26
+ if (mode.toLowerCase() === "fill") {
27
+ const scale = Math.min(size.width / sourceWidth, size.height / sourceHeight, 1);
28
+ return [
29
+ `w_${Math.max(1, Math.round(sourceWidth * scale))}`,
30
+ `h_${Math.max(1, Math.round(sourceHeight * scale))}`,
31
+ quality
32
+ ];
33
+ }
34
+ return [
35
+ `w_${Math.min(sourceWidth, size.width)}`,
36
+ `h_${Math.min(sourceHeight, size.height)}`,
37
+ quality
38
+ ];
39
+ }
40
+ if (sourceWidth) return [`w_${Math.min(sourceWidth, size.width)}`, quality];
41
+ if (sourceHeight) return [`h_${Math.min(sourceHeight, size.height)}`, quality];
42
+ return requestedParams(size);
43
+ }
44
+ function resizeWixTransform(url, size) {
45
+ const transform = WIX_TRANSFORM.exec(url);
46
+ if (!transform || !RESIZABLE_MODES.has(transform[2].toLowerCase())) return url;
47
+ const [, base, mode, rawParams, filename, suffix] = transform;
48
+ const source = rawParams.split(",").filter(Boolean);
49
+ if (!filename && !source.some((param) => SIZING_PARAM.test(param))) return url;
50
+ const dimensions = dimensionsOf(source);
51
+ if (mode.toLowerCase() === "fill" && !dimensions.width && !dimensions.height) return url;
52
+ const stated = [dimensions.width, dimensions.height].filter((d) => d !== void 0);
53
+ if (stated.length > 0 && (dimensions.width ?? 0) <= size.width && (dimensions.height ?? 0) <= size.height) return url;
54
+ const carried = source.filter((p) => !OWNED_PARAM.test(p) && !DROPPED_PARAM.test(p));
55
+ const params = [...fittedParams(size, dimensions, mode), ...carried];
56
+ return `${base}${mode}/${params.join(",")}/image.jpg${suffix ?? ""}`;
57
+ }
58
+ function wixTransformDimension(params, name) {
59
+ const match = params.find((param) => new RegExp(`^${name}_\\d+$`, "i").test(param));
60
+ return match ? Number(match.slice(2)) : void 0;
61
+ }
62
+ function dimensionsOf(params) {
63
+ return {
64
+ width: wixTransformDimension(params, "w"),
65
+ height: wixTransformDimension(params, "h")
66
+ };
67
+ }
68
+ function wixMediaUrl(value, size = WIX_LIST_IMAGE) {
69
+ const reference = typeof value === "string" ? value.trim() : "";
70
+ if (!reference) return "";
71
+ const resolvedUrl = resolveWixMediaReference(reference);
72
+ if (!WEB_URL.test(resolvedUrl)) return "";
73
+ if (size === null) return resolvedUrl;
74
+ if (!WIX_MEDIA.test(resolvedUrl)) return resolvedUrl;
75
+ const base = resolvedUrl.split(/[?#]/)[0];
76
+ if (base.includes("/v1/")) return resizeWixTransform(resolvedUrl, size);
77
+ const suffix = resolvedUrl.slice(base.length);
78
+ return `${base}/v1/fit/${requestedParams(size).join(",")}/image.jpg${suffix}`;
79
+ }
80
+
81
+ // ../../stdlib/wix-sdk/rich-content/src/display.ts
82
+ var HTTPS = /^https:\/\/[^\s/?#]\S*$/i;
83
+ var LINK = /^(https?:\/\/[^\s/?#]\S*|mailto:\S+|tel:\S+)$/i;
84
+ var HTTP_HOST = /^https?:\/\/([^\s/?#]+)/i;
85
+ var VIDEO_CARD_HOST = /^(?:(?:www\.|m\.)?youtube\.com|youtu\.be|(?:www\.|player\.)?vimeo\.com)$/;
86
+ function trimAscii(s) {
87
+ return s.replace(/^[\t\n\v\f\r ]+|[\t\n\v\f\r ]+$/g, "");
88
+ }
89
+ function normalizeMediaRef(s) {
90
+ return s.trim().replace(/^http:\/\/(static|video)\.wixstatic\.com\//i, (_all, sub) => `https://${sub.toLowerCase()}.wixstatic.com/`);
91
+ }
92
+ var hostOf = (url) => HTTP_HOST.exec(url)?.[1]?.toLowerCase().replace(/:\d+$/, "");
93
+ function cleanLink(url) {
94
+ if (typeof url !== "string") return void 0;
95
+ const t = trimAscii(url);
96
+ return LINK.test(t) ? t : void 0;
97
+ }
98
+ function ricosImageUrl(holder, width) {
99
+ const src = holder?.image?.src;
100
+ const fromUrl = typeof src?.url === "string" ? normalizeMediaRef(src.url) : "";
101
+ const ref = fromUrl !== "" ? fromUrl : typeof src?.id === "string" ? normalizeMediaRef(src.id) : src?.id;
102
+ const url = wixMediaUrl(ref, { width, height: width, quality: 85 });
103
+ return HTTPS.test(url) ? url : "";
104
+ }
105
+ function ricosVideoUrl(src) {
106
+ if (!src || typeof src !== "object") return "";
107
+ const { url, id } = src;
108
+ if (typeof url === "string" && url.trim() !== "") {
109
+ const link = cleanLink(url);
110
+ const linkHost = link === void 0 ? void 0 : hostOf(link);
111
+ if (link !== void 0 && linkHost !== void 0 && VIDEO_CARD_HOST.test(linkHost)) return link;
112
+ const media = normalizeMediaRef(url);
113
+ const mediaHost = hostOf(media);
114
+ return HTTPS.test(media) && !(mediaHost !== void 0 && VIDEO_CARD_HOST.test(mediaHost)) ? media : "";
115
+ }
116
+ const ref = typeof id === "string" ? id.trim().replace(/^video\//, "") : "";
117
+ if (/^\w[\w~.-]*$/.test(ref)) return `https://video.wixstatic.com/video/${ref}/file`;
118
+ if (/^\w[\w~.-]*\/[\w~./-]+$/.test(ref)) return `https://video.wixstatic.com/video/${ref}`;
119
+ return "";
120
+ }
121
+
122
+ // ../../stdlib/wix-sdk/rich-content/src/index.ts
123
+ var RICH_CONTENT_LIMITS = { maxBytes: 262144, maxNodes: 5e3, maxDepth: 16 };
124
+ var isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
125
+ function parseRicosJson(value) {
126
+ let v = value;
127
+ if (typeof v === "string") {
128
+ if (v.trim() === "") return null;
129
+ try {
130
+ v = JSON.parse(v);
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+ return isObj(v) && Array.isArray(v.nodes) ? v : null;
136
+ }
137
+ function ricosToPlainText(doc) {
138
+ const kids = (v) => Array.isArray(v) ? v.filter(isObj) : [];
139
+ const inline = (node) => typeof node.textData?.text === "string" ? node.textData.text : kids(node.nodes).map(inline).join("");
140
+ const blocks = (nodes) => nodes.flatMap((node) => node.type === "PARAGRAPH" || node.type === "HEADING" || node.type === "TEXT" ? [inline(node)] : blocks(kids(node.nodes)));
141
+ return blocks(kids(doc?.nodes)).join("\n\n");
142
+ }
143
+ function plainTextToRicos(text) {
144
+ return { nodes: text.split("\n\n").map((t) => ({ type: "PARAGRAPH", nodes: [{ type: "TEXT", textData: { text: t, decorations: [] } }] })) };
145
+ }
146
+ var onlyKeys = (o, allowed) => Object.keys(o).every((k) => allowed.includes(k));
147
+ var emptyObject = (v) => v === void 0 || v === null || typeof v === "object" && Object.keys(v).length === 0;
148
+ function defaultParagraphData(pd) {
149
+ if (pd === void 0) return true;
150
+ if (!pd || typeof pd !== "object" || !onlyKeys(pd, ["textStyle", "indentation"])) return false;
151
+ const { textStyle, indentation } = pd;
152
+ if (indentation !== void 0 && indentation !== 0) return false;
153
+ if (textStyle === void 0) return true;
154
+ if (!textStyle || typeof textStyle !== "object" || !onlyKeys(textStyle, ["textAlignment"])) return false;
155
+ const align = textStyle.textAlignment;
156
+ return align === void 0 || align === "AUTO";
157
+ }
158
+ function plainRun(raw) {
159
+ if (!raw || typeof raw !== "object") return false;
160
+ const t = raw;
161
+ return t.type === "TEXT" && onlyKeys(t, ["type", "id", "textData", "nodes"]) && (t.nodes === void 0 || Array.isArray(t.nodes) && t.nodes.length === 0) && !(Array.isArray(t.textData?.decorations) && t.textData.decorations.length > 0);
162
+ }
163
+ function isPlainRicos(doc) {
164
+ if (doc === null || doc === void 0) return true;
165
+ if (typeof doc !== "object" || Array.isArray(doc)) return false;
166
+ if (!emptyObject(doc.documentStyle)) return false;
167
+ if (doc.nodes !== void 0 && !Array.isArray(doc.nodes)) return false;
168
+ const nodes = Array.isArray(doc.nodes) ? doc.nodes : [];
169
+ return nodes.every((raw) => {
170
+ if (!raw || typeof raw !== "object") return false;
171
+ const n = raw;
172
+ if (n.nodes !== void 0 && !Array.isArray(n.nodes)) return false;
173
+ return n.type === "PARAGRAPH" && onlyKeys(n, ["type", "id", "nodes", "paragraphData"]) && defaultParagraphData(n.paragraphData) && (Array.isArray(n.nodes) ? n.nodes : []).every(plainRun);
174
+ });
175
+ }
176
+ var HTML_TAG = /<[a-z!/?]/i;
177
+ function htmlIsTrivial(html) {
178
+ const s = (html ?? "").trim();
179
+ return !s || !HTML_TAG.test(s) || /^<p>[^<>]*<\/p>$/.test(s);
180
+ }
181
+ function encodeRichContent(doc, opts = {}) {
182
+ if (doc === null || doc === void 0) return { json: "" };
183
+ if (!isObj(doc) || !Array.isArray(doc.nodes)) return { json: "", rejected: "not_a_document" };
184
+ const w = opts.imageWidth;
185
+ const imageWidth = typeof w === "number" && Number.isInteger(w) && w > 0 ? Math.min(w, WIX_DETAIL_IMAGE.width) : WIX_DETAIL_IMAGE.width;
186
+ const ctx = {
187
+ imageWidth,
188
+ // Gallery tiles and video posters are half-width at most, so the list budget caps them.
189
+ tileWidth: Math.min(imageWidth, WIX_LIST_IMAGE.width),
190
+ // Serialized nesting allowed: each node level is a node and its `nodes` array, plus generous room for type
191
+ // data. Past it the document is rejected rather than overflowing the worker's stack.
192
+ maxJsonDepth: RICH_CONTENT_LIMITS.maxDepth * 2 + 64,
193
+ ids: /* @__PURE__ */ new Set()
194
+ };
195
+ let nodes;
196
+ let json;
197
+ try {
198
+ nodes = cleanNodes(doc.nodes, "", 1, ctx, void 0);
199
+ if (nodes.length === 0) return { json: "" };
200
+ const out = { nodes };
201
+ if (doc.documentStyle !== void 0) out.documentStyle = doc.documentStyle;
202
+ if (doc.metadata !== void 0) out.metadata = doc.metadata;
203
+ json = canonicalJson(out, 0, ctx.maxJsonDepth);
204
+ } catch (e) {
205
+ if (e === TOO_DEEP) return { json: "", rejected: "too_deep" };
206
+ throw e;
207
+ }
208
+ if (utf8Length(json) > RICH_CONTENT_LIMITS.maxBytes) return { json: "", rejected: "too_large" };
209
+ if (countNodes(nodes) > RICH_CONTENT_LIMITS.maxNodes) return { json: "", rejected: "too_many_nodes" };
210
+ return { json };
211
+ }
212
+ var TOO_DEEP = { reason: "too_deep" };
213
+ var PATH_SHAPED = /^[0-9.]+$/;
214
+ var isWrapper = (t) => t === "COLLAPSIBLE_LIST" || t === "COLLAPSIBLE_ITEM" || t === "COLLAPSIBLE_ITEM_TITLE" || t === "COLLAPSIBLE_ITEM_BODY";
215
+ var holdsBlocks = (t) => t === void 0 || t === "BULLETED_LIST" || t === "ORDERED_LIST" || t === "LIST_ITEM" || t === "BLOCKQUOTE" || isWrapper(t);
216
+ var LINK_FIELDS = {
217
+ EMBED: [["embedData", "oembed", "url"], ["embedData", "src"]],
218
+ HTML: [["htmlData", "url"]],
219
+ APP_EMBED: [["appEmbedData", "url"]],
220
+ LINK_PREVIEW: [["linkPreviewData", "link", "url"]],
221
+ FILE: [["fileData", "src", "url"]],
222
+ AUDIO: [["audioData", "audio", "src", "url"]],
223
+ GIF: [["gifData", "original", "gif"]],
224
+ IMAGE: [["imageData", "link", "url"]]
225
+ };
226
+ function rewriteAt(v, path, fix) {
227
+ if (!isObj(v) || !(path[0] in v)) return v;
228
+ const [k, ...rest] = path;
229
+ if (rest.length > 0) return { ...v, [k]: rewriteAt(v[k], rest, fix) };
230
+ const next = fix(v[k]);
231
+ if (next === v[k]) return v;
232
+ const { [k]: _old, ...kept } = v;
233
+ return next === void 0 ? kept : { ...kept, [k]: next };
234
+ }
235
+ function getAt(v, path) {
236
+ let cur = v;
237
+ for (const k of path) cur = isObj(cur) ? cur[k] : void 0;
238
+ return cur;
239
+ }
240
+ var drop = () => void 0;
241
+ var LINK_DECORATION_URL = ["linkData", "link", "url"];
242
+ var BUTTON_URL = ["link", "url"];
243
+ function cleanNodes(nodes, base, depth, ctx, parentType) {
244
+ const out = [];
245
+ for (const raw of nodes) {
246
+ if (!isObj(raw)) continue;
247
+ if (depth > RICH_CONTENT_LIMITS.maxDepth) throw TOO_DEEP;
248
+ const path = base === "" ? String(out.length) : `${base}.${out.length}`;
249
+ const node = cleanNode({ ...raw }, path, depth, ctx, parentType);
250
+ if (node) out.push(node);
251
+ }
252
+ return out;
253
+ }
254
+ function cleanNode(node, path, depth, ctx, parentType) {
255
+ if (node.type === "TEXT") {
256
+ const decorations = node.textData?.decorations;
257
+ if (Array.isArray(decorations)) {
258
+ const kept = decorations.flatMap((d) => {
259
+ if (!(isObj(d) && d.type === "LINK")) return [d];
260
+ const next = rewriteAt(d, LINK_DECORATION_URL, cleanLink);
261
+ return getAt(next, LINK_DECORATION_URL) === void 0 ? [] : [next];
262
+ });
263
+ node.textData = { ...node.textData, decorations: kept };
264
+ }
265
+ } else if (node.type === "IMAGE") {
266
+ const data = isObj(node.imageData) ? node.imageData : {};
267
+ const url = ricosImageUrl(data, ctx.imageWidth);
268
+ if (!url) return null;
269
+ node.imageData = { ...data, image: { ...isObj(data.image) ? data.image : {}, src: { url } } };
270
+ } else if (node.type === "GALLERY") {
271
+ const data = isObj(node.galleryData) ? node.galleryData : {};
272
+ const items = (Array.isArray(data.items) ? data.items : []).flatMap((item) => {
273
+ if (!isObj(item) || !isObj(item.image)) return [];
274
+ const media = isObj(item.image.media) ? item.image.media : {};
275
+ const url = ricosImageUrl({ image: media }, ctx.tileWidth);
276
+ return url ? [{ ...item, image: { ...item.image, media: { ...media, src: { url } } } }] : [];
277
+ });
278
+ if (items.length === 0) return null;
279
+ node.galleryData = { ...data, items };
280
+ } else if (node.type === "VIDEO") {
281
+ const data = isObj(node.videoData) ? { ...node.videoData } : {};
282
+ const video = isObj(data.video) ? data.video : {};
283
+ const url = ricosVideoUrl(video.src);
284
+ if (!url) return null;
285
+ data.video = { ...video, src: { url } };
286
+ const poster = ricosImageUrl({ image: data.thumbnail }, ctx.tileWidth);
287
+ if (poster) data.thumbnail = { ...data.thumbnail, src: { url: poster } };
288
+ else delete data.thumbnail;
289
+ node.videoData = data;
290
+ } else if (node.type === "BUTTON" || node.type === "LINK_BUTTON") {
291
+ node.buttonData = rewriteAt(node.buttonData, BUTTON_URL, cleanLink);
292
+ if (getAt(node.buttonData, BUTTON_URL) === void 0) return null;
293
+ } else if (node.type === "HTML") {
294
+ node.htmlData = rewriteAt(node.htmlData, ["html"], drop);
295
+ } else if (node.type === "EMBED") {
296
+ node.embedData = rewriteAt(node.embedData, ["oembed", "html"], drop);
297
+ }
298
+ for (const [field, ...rest] of (typeof node.type === "string" ? LINK_FIELDS[node.type] : void 0) ?? []) {
299
+ if (field in node) node[field] = rewriteAt(node[field], rest, cleanLink);
300
+ }
301
+ const inBlocks = holdsBlocks(parentType);
302
+ if (node.type !== "TEXT" || inBlocks) {
303
+ const own = typeof node.id === "string" ? node.id : "";
304
+ const usable = own !== "" && !PATH_SHAPED.test(own);
305
+ if (inBlocks && !isWrapper(node.type)) {
306
+ if (usable && !ctx.ids.has(own)) ctx.ids.add(own);
307
+ else node.id = path;
308
+ } else if (!usable) node.id = path;
309
+ }
310
+ if (Array.isArray(node.nodes)) node.nodes = cleanNodes(node.nodes, path, depth + 1, ctx, typeof node.type === "string" ? node.type : "");
311
+ return node;
312
+ }
313
+ function countNodes(nodes) {
314
+ return nodes.reduce((n, node) => isObj(node) ? n + 1 + (Array.isArray(node.nodes) ? countNodes(node.nodes) : 0) : n, 0);
315
+ }
316
+ function canonicalJson(v, depth, max) {
317
+ if (depth > max) throw TOO_DEEP;
318
+ if (Array.isArray(v)) return `[${v.map((x) => x === void 0 ? "null" : canonicalJson(x, depth + 1, max)).join(",")}]`;
319
+ if (isObj(v)) return `{${Object.keys(v).sort().filter((k) => v[k] !== void 0).map((k) => `${JSON.stringify(k)}:${canonicalJson(v[k], depth + 1, max)}`).join(",")}}`;
320
+ return JSON.stringify(v) ?? "null";
321
+ }
322
+ function utf8Length(s) {
323
+ let bytes = 0;
324
+ for (let i = 0; i < s.length; i++) {
325
+ const c = s.charCodeAt(i);
326
+ if (c < 128) bytes += 1;
327
+ else if (c < 2048) bytes += 2;
328
+ else if (c >= 55296 && c <= 56319 && i + 1 < s.length && s.charCodeAt(i + 1) >= 56320 && s.charCodeAt(i + 1) <= 57343) {
329
+ bytes += 4;
330
+ i++;
331
+ } else bytes += 3;
332
+ }
333
+ return bytes;
334
+ }
335
+ export {
336
+ RICH_CONTENT_LIMITS,
337
+ encodeRichContent,
338
+ htmlIsTrivial,
339
+ isPlainRicos,
340
+ parseRicosJson,
341
+ plainTextToRicos,
342
+ ricosToPlainText
343
+ };
344
+ //# sourceMappingURL=wix-rich-content.mjs.map
@@ -434,6 +434,10 @@
434
434
  "types": "./dist/_types/stdlib/wix-sdk/reviews/src/index.d.ts",
435
435
  "import": "./dist/wix-reviews.mjs"
436
436
  },
437
+ "./wix-rich-content": {
438
+ "types": "./dist/_types/stdlib/wix-sdk/rich-content/src/index.d.ts",
439
+ "import": "./dist/wix-rich-content.mjs"
440
+ },
437
441
  "./wix-seo": {
438
442
  "types": "./dist/_types/stdlib/wix-sdk/seo-v1/src/index.d.ts",
439
443
  "import": "./dist/wix-seo.mjs"
package/dist/cli.mjs CHANGED
@@ -4964,6 +4964,7 @@ var init_build_lib = __esm({
4964
4964
  // Cross-vertical media sizing. Not a generated domain client — the one place a
4965
4965
  // Wix media handle becomes a fetchable URL, so no vertical re-derives it.
4966
4966
  "@himalaya/wix-media": SDK("media"),
4967
+ "@himalaya/wix-rich-content": SDK("rich-content"),
4967
4968
  "@himalaya/wix-stores": SDK("stores-v1"),
4968
4969
  "@himalaya/wix-ecom": SDK("ecom"),
4969
4970
  "@himalaya/wix-redirects": SDK("redirects-v1"),
@@ -7592,9 +7593,17 @@ var init_validator = __esm({
7592
7593
  hideHostedSiteHeader: { type: "boolean" },
7593
7594
  immersive: { type: "boolean" },
7594
7595
  showCloseButton: { type: "boolean" },
7595
- // Keep the native WebView instance warm across re-presentations (covers reload cold
7596
- // otherwise). iOS-implemented; Android ignores it (cold-loads) for now.
7597
- keepAlive: { type: "boolean" }
7596
+ // Keep the native WebView instance warm across re-presentations and tab switches
7597
+ // (per screen id, LRU — WebViewKeepAliveStore on both platforms).
7598
+ keepAlive: { type: "boolean" },
7599
+ // Hosted-site polish — native hosts honoured these before the validator knew them.
7600
+ pinHostedHeader: { type: "boolean" },
7601
+ internalLinksAsScreens: { type: "boolean" },
7602
+ respectHttpCache: { type: "boolean" },
7603
+ coverBandColor: { type: "string" },
7604
+ // RFC 0062 — binary-shipped chrome hiding for a hosted site (no page script over the air).
7605
+ hideWixAds: { type: "boolean" },
7606
+ hideHostedSiteFooter: { type: "boolean" }
7598
7607
  }
7599
7608
  }
7600
7609
  }
@@ -12495,7 +12504,6 @@ function resolveIdiom(tokens, ctx = {}) {
12495
12504
  var IDIOM_CONTEXTS, UNSAFE_KEYS;
12496
12505
  var init_idiom = __esm({
12497
12506
  "../../core/ts/src/idiom.ts"() {
12498
- "use strict";
12499
12507
  IDIOM_CONTEXTS = {
12500
12508
  ios: { native: ["ios"] },
12501
12509
  android: { native: ["material"] },
@@ -111,6 +111,7 @@ direction the platform is heading — recommended, not required). Usage is unifo
111
111
  - `@himalaya/wix-redirects` — Wix redirects — typed REST client
112
112
  - `@himalaya/wix-restaurants` — Restaurants — menus and orders
113
113
  - `@himalaya/wix-reviews` — Product and site reviews
114
+ - `@himalaya/wix-rich-content` — Ricos rich content — encode a post body for the RichContent viewer; plain-text helpers
114
115
  - `@himalaya/wix-seo` — Wix seo — typed REST client
115
116
  - `@himalaya/wix-site-list` — The user's Wix sites
116
117
  - `@himalaya/wix-site-media` — Wix site media — typed REST client
@@ -907,7 +907,7 @@
907
907
  "name": "SplashConfig",
908
908
  "kind": "interface",
909
909
  "jsdoc": "/**\n * The IN-APP splash — the brand surface between the OS splash handing off and the\n * app being ready. The *OS* splash is generated at build time from the same\n * `splash` block in config.ts and can never be OTA (iOS reads `UILaunchScreen`\n * before any app code runs); this layer is OTA. Artwork stays baked in the binary\n * — `logoAsset` / `backgroundImageAsset` name a baked asset, so only the\n * composition travels.\n */",
910
- "decl": "export interface SplashConfig {\n /** Master switch / kill-switch. Absent or false ⇒ no surface. */\n enabled?: boolean;\n /** Background color TOKEN name (e.g. \"accentPrimary\"). */\n backgroundColor?: TokenName;\n /** Gradient token name; wins over `backgroundColor`. */\n backgroundGradient?: TokenName;\n /** BAKED asset name for a full-bleed background image; wins over color/gradient. */\n backgroundImageAsset?: string;\n /** BAKED asset name for the centered logo (e.g. \"LaunchLogo\"). */\n logoAsset?: string;\n /** Logo width in pt/dp. 0 / absent ⇒ 96. */\n logoWidth?: number;\n /** Optional wordmark line. NOTE: a literal string, so an OTA edit bypasses\n * localization (same caveat as `NavTabItem.label`). */\n caption?: string;\n /** Typography token for the caption. Absent ⇒ \"title1\". */\n captionStyle?: TokenName;\n /** Color token for the caption. Absent ⇒ \"textOnAccent\". */\n captionColor?: TokenName;\n /** Absent / unknown ⇒ \"centered\". */\n layout?: SplashLayout;\n /** Anti-flash floor, ms. Clamped client-side to [0, 2000]. */\n minVisibleMs?: number;\n /** Hard ceiling on the readiness hold, ms — a stuck boot can never wedge the\n * UI. 0 / absent ⇒ 3000. Clamped client-side to [0, 8000]. */\n maxHoldMs?: number;\n}"
910
+ "decl": "export interface SplashConfig {\n /** Master switch / kill-switch. Absent or false ⇒ no surface. */\n enabled?: boolean;\n /** Background color TOKEN name (e.g. \"accentPrimary\"). */\n backgroundColor?: TokenName;\n /** Gradient token name; wins over `backgroundColor`. */\n backgroundGradient?: TokenName;\n /** BAKED asset name for a full-bleed background image; wins over color/gradient. */\n backgroundImageAsset?: string;\n /** BAKED asset name for the centered logo (e.g. \"LaunchLogo\"). */\n logoAsset?: string;\n /** Logo width in pt/dp. 0 / absent ⇒ 96. */\n logoWidth?: number;\n /** Optional wordmark line. NOTE: a literal string, so an OTA edit bypasses\n * localization (same caveat as `NavTabItem.label`). */\n caption?: string;\n /** Typography token for the caption. Absent ⇒ \"title1\". */\n captionStyle?: TokenName;\n /** Color token for the caption. Absent ⇒ \"textOnAccent\". */\n captionColor?: TokenName;\n /** Absent / unknown ⇒ \"centered\". */\n layout?: SplashLayout;\n /** Anti-flash floor, ms. Clamped client-side to [0, 2000]. */\n minVisibleMs?: number;\n /** Hard ceiling on the readiness hold, ms — a stuck boot can never wedge the\n * UI. 0 / absent ⇒ 3000. Clamped client-side to [0, 8000]. */\n maxHoldMs?: number;\n /** What readiness waits for beyond the seed. Absent / \"\" ⇒ seeded is\n * enough. \"first_content\" ⇒ also the first WebView page (a site shell), still\n * under `maxHoldMs`. Unknown coalesces to \"\" on device. */\n readySignal?: SplashReadySignal;\n}"
911
911
  },
912
912
  {
913
913
  "name": "OrientationConfig",
@@ -20943,6 +20943,79 @@
20943
20943
  }
20944
20944
  ]
20945
20945
  },
20946
+ {
20947
+ "name": "@himalaya/wix-rich-content",
20948
+ "source": "stdlib/wix-sdk/rich-content/src/index.ts",
20949
+ "description": "Ricos rich content — encode a post body for the RichContent viewer; plain-text helpers",
20950
+ "api": [
20951
+ {
20952
+ "name": "RicosNode",
20953
+ "kind": "type",
20954
+ "jsdoc": "/** One Ricos node: a block (PARAGRAPH, HEADING, IMAGE, …) or an inline TEXT run. Type-specific data\n * (`imageData`, `headingData`, …) rides along as extra keys. */",
20955
+ "decl": "export type RicosNode = { type?: string; id?: string; nodes?: RicosNode[]; textData?: { text?: string; decorations?: { type: string; linkData?: { link?: { url?: string } } }[] }; [k: string]: unknown };"
20956
+ },
20957
+ {
20958
+ "name": "RicosDocument",
20959
+ "kind": "type",
20960
+ "jsdoc": "/** A Ricos document with its `nodes` checked to be an array (what parseRicosJson and plainTextToRicos return). */",
20961
+ "decl": "export type RicosDocument = { nodes: RicosNode[]; metadata?: unknown; documentStyle?: unknown };"
20962
+ },
20963
+ {
20964
+ "name": "RicosInput",
20965
+ "kind": "type",
20966
+ "jsdoc": "/** Any Ricos-shaped value, as Wix APIs return it (a blog post's `richContent`, an event's description) — their\n * generated types declare `nodes` loosely, so the helpers accept it as is and check the shape at runtime. */",
20967
+ "decl": "export type RicosInput = { nodes?: unknown; metadata?: unknown; documentStyle?: unknown };"
20968
+ },
20969
+ {
20970
+ "name": "RejectReason",
20971
+ "kind": "type",
20972
+ "jsdoc": "/** Why a document is not shown: the viewer draws its fallback instead and reports only this code. */",
20973
+ "decl": "export type RejectReason = \"invalid_json\" | \"too_large\" | \"too_many_nodes\" | \"too_deep\" | \"not_a_document\";"
20974
+ },
20975
+ {
20976
+ "name": "RICH_CONTENT_LIMITS",
20977
+ "kind": "const",
20978
+ "jsdoc": "/** The RichContent viewer's ceilings; every renderer rejects a document past any of them. Kept equal to the platform limits file by a test. */",
20979
+ "decl": "export const RICH_CONTENT_LIMITS = { maxBytes: 262144, maxNodes: 5000, maxDepth: 16 } as const;"
20980
+ },
20981
+ {
20982
+ "name": "parseRicosJson",
20983
+ "kind": "function",
20984
+ "jsdoc": "/** Reads a Ricos document from a JSON string or an already-parsed object (page messages arrive as either,\n * depending on the platform). Returns null for empty input, invalid JSON, or anything without a `nodes` array. */",
20985
+ "decl": "export function parseRicosJson(value: unknown): RicosDocument | null;"
20986
+ },
20987
+ {
20988
+ "name": "ricosToPlainText",
20989
+ "kind": "function",
20990
+ "jsdoc": "/** Plain text of a document. Paragraphs, headings and loose text runs are blocks joined by a blank line; runs\n * inside a block join with nothing between them, so a word split across bold and plain runs stays one word.\n * Media and other nodes contribute only the text nested inside them. Malformed parts (a non-object node, a\n * `nodes` that is not an array) contribute nothing — it never throws. */",
20991
+ "decl": "export function ricosToPlainText(doc: RicosInput | null | undefined): string;"
20992
+ },
20993
+ {
20994
+ "name": "plainTextToRicos",
20995
+ "kind": "function",
20996
+ "jsdoc": "/** A document of undecorated paragraphs, one per blank-line-separated block of `text` — the inverse of\n * ricosToPlainText for plain text. An empty string gives one empty paragraph. */",
20997
+ "decl": "export function plainTextToRicos(text: string): RicosDocument;"
20998
+ },
20999
+ {
21000
+ "name": "isPlainRicos",
21001
+ "kind": "function",
21002
+ "jsdoc": "/** True when a plain-text editor can rewrite the document without losing anything: no document styling, and\n * only paragraphs of undecorated text runs whose paragraph settings are the defaults a plain paragraph carries\n * (`{ textStyle: { textAlignment: \"AUTO\" }, indentation: 0 }`, or none). A heading, list, link, bold run,\n * media node, alignment or indent makes it rich. A missing or empty document (no `nodes`, or `[]`) is plain —\n * there is nothing in it to lose. Anything malformed is not: a value that is not a document object (a JSON\n * string, an array), or `nodes` that is not an array, so a caller holding the wrong shape never overwrites it. */",
21003
+ "decl": "export function isPlainRicos(doc: RicosInput | null | undefined): boolean;"
21004
+ },
21005
+ {
21006
+ "name": "htmlIsTrivial",
21007
+ "kind": "function",
21008
+ "jsdoc": "/** True when an HTML description has no formatting to protect: empty, text with no tag in it (a lone `<` in\n * prose is text), or exactly one attribute-free `<p>` around text with no inner tags. A form that edits plain\n * text may overwrite only these. */",
21009
+ "decl": "export function htmlIsTrivial(html: string | null | undefined): boolean;"
21010
+ },
21011
+ {
21012
+ "name": "encodeRichContent",
21013
+ "kind": "function",
21014
+ "jsdoc": "/** Prepares a Ricos document for the RichContent viewer; bind the returned `json` as `documentJsonPath`.\n * Wix media ids become sized https URLs (`imageWidth`, default and maximum 1400 px; galleries and posters at most\n * 1000). Block ids follow the viewer's own rule on the encoded document — a block's missing, all-digits-and-dots\n * or repeated id becomes its path index there (\"0.2.1\") — so every renderer shows the same ids and encoding twice\n * changes nothing. Images, galleries and videos without https media (an http YouTube or Vimeo link is kept: the\n * viewer shows it as a card), buttons without an http(s)/mailto/tel link, any other link url with a disallowed\n * scheme (link decorations, card and image links) and inline HTML (`htmlData.html`, an embed's `oembed.html`) are\n * removed; an allowed link is emitted trimmed of surrounding whitespace, as the viewer emits it. Text is never\n * changed. Keys are sorted, so equal documents encode to equal strings. The input is not\n * modified. A missing or empty document gives `{ json: \"\" }`; a non-document or one past the viewer's limits gives\n * `{ json: \"\", rejected }` (checked in the order not_a_document, too_deep, too_large, too_many_nodes). */",
21015
+ "decl": "export function encodeRichContent(doc: RicosInput | null | undefined, opts: { imageWidth?: number } = {}): { json: string; rejected?: RejectReason };"
21016
+ }
21017
+ ]
21018
+ },
20946
21019
  {
20947
21020
  "name": "@himalaya/wix-seo",
20948
21021
  "source": "stdlib/wix-sdk/seo-v1/src/index.ts",
@@ -148,6 +148,7 @@ splash: {
148
148
  layout: "centeredStacked", // centered | centeredStacked | logoOnly
149
149
  minVisibleMs: 600, // anti-flash floor, clamped ≤ 2000
150
150
  maxHoldMs: 3000, // hard ceiling, clamped ≤ 8000
151
+ readySignal: "", // "" | "first_content" — see "Readiness, not a timer"
151
152
  }
152
153
  ```
153
154
 
@@ -163,13 +164,24 @@ A `splashScreenId` pointing at an OTA descriptor fails at exactly the moment it'
163
164
  render the generic `ProgressView` + literal `"Loading…"` on precisely the launch we're fixing.
164
165
  It would also invert boot ordering (`applyProdServeFromCache` and `LocaleStore.initShared` must
165
166
  precede any fetch) and put descriptor-shaped payload inside `NavigationConfig`, where a single
166
- strict-decode throw disables **all** OTA. Proto field 13+ is reserved should that ever change.
167
+ strict-decode throw disables **all** OTA. Proto field 14+ is reserved should that ever change
168
+ (13 is `ready_signal`).
167
169
 
168
170
  ### Readiness, not a timer
169
171
 
170
172
  `BootReadiness` (iOS + Android, one tested implementation each) decides when the surface may
171
173
  go: **seeded AND past the anti-flash floor, OR the hard ceiling elapsed.**
172
174
 
175
+ `readySignal: "first_content"` adds one
176
+ condition to "seeded": the first WebView page has finished loading. It exists for a site shell
177
+ (`himi-brandy`), where every tab is the customer's website and a splash that leaves on `seeded`
178
+ fades into a blank WebView. Each native WebView host calls `BootReadiness.markFirstContent()` on
179
+ a real page load (not on an error page or a cancelled navigation), and the shell re-evaluates
180
+ on it: `BootReadiness.firstContentNotification` on iOS, the `firstContent` `StateFlow` on
181
+ Android. The shell must also MOUNT its content under the splash, not after it, or there is no
182
+ page to wait for. The ceiling still applies, and a launch it releases while waiting for content
183
+ reports `splash_timeout`. Unknown values read as `""` on device; `defineApp` rejects them in source.
184
+
173
185
  The ceiling is the load-bearing part. The app must always become interactive, so `maxHoldMs`
174
186
  dismisses the splash even if readiness is never reported — a failed seed prefill, a slow disk,
175
187
  or a bug in boot cannot wedge the UI behind a splash. `effectiveReady` is **monotonic**: once
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1079.0",
3
+ "version": "0.1080.0",
4
4
  "type": "module",
5
5
  "description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
6
6
  "keywords": [
@@ -67,5 +67,5 @@
67
67
  "artifactId": "himalaya-cli"
68
68
  }
69
69
  },
70
- "falconPackageHash": "d5eb7481dca2d7a6272424f6a9a08ddc1bca8661f0a9a11443e2ca45"
70
+ "falconPackageHash": "1e0c05f97a07293123b9d983e46d508913e929db4fde75461f56a065"
71
71
  }
@@ -1,35 +1,16 @@
1
1
  import type { WixRichContent } from "@himalaya/wix-blog";
2
+ // The rich-content helpers. Inside the platform source this import is a relative path, so node test runs
3
+ // resolve it; an app's copy imports "@himalaya/wix-rich-content".
4
+ import { ricosToPlainText, isPlainRicos, plainTextToRicos } from "@himalaya/wix-rich-content";
2
5
  import { wixMediaUrl, WIX_DETAIL_IMAGE } from "./himi-store.js";
3
6
 
4
- type Node = { type?: string; nodes?: Node[]; textData?: { text?: string; decorations?: unknown[] }; [key: string]: unknown };
7
+ // Thin wrappers keep the WixRichContent signatures callers compile against.
5
8
  /** Join inline runs without adding paragraph breaks between bold/plain spans. */
6
- export function postBodyText(rich: WixRichContent | null | undefined): string {
7
- const inline = (node: Node): string => typeof node.textData?.text === "string"
8
- ? node.textData.text : (node.nodes ?? []).map(inline).join("");
9
- const blocks = (nodes: Node[]): string[] => nodes.flatMap(node =>
10
- node.type === "PARAGRAPH" || node.type === "HEADING" || node.type === "TEXT"
11
- ? [inline(node)] : blocks(node.nodes ?? []));
12
- return blocks((rich?.nodes ?? []) as Node[]).join("\n\n");
13
- }
9
+ export const postBodyText = (rich: WixRichContent | null | undefined): string => ricosToPlainText(rich);
14
10
  /** The plain editor must not overwrite formatting or any opaque media node.
15
11
  * Rich drafts remain editable for title/excerpt/cover; body stays untouched. */
16
- export function hasRichBody(rich: WixRichContent | null | undefined): boolean {
17
- return (rich?.nodes ?? []).some(raw => {
18
- if (!raw || typeof raw !== "object") return true;
19
- const node = raw as Node;
20
- if (node.type !== "PARAGRAPH") return true;
21
- if (Object.keys(node).some(key => !["type", "id", "nodes", "paragraphData"].includes(key))) return true;
22
- if (node.paragraphData && Object.keys(node.paragraphData as object).length) return true;
23
- return (node.nodes ?? []).some(child => child.type !== "TEXT"
24
- || !!child.textData?.decorations?.length
25
- || Object.keys(child).some(key => !["type", "id", "textData", "nodes"].includes(key)));
26
- });
27
- }
28
- export function plainPostBody(body: string): WixRichContent {
29
- return { nodes: body.split("\n\n").map(text => ({
30
- type: "PARAGRAPH", nodes: [{ type: "TEXT", textData: { text, decorations: [] } }],
31
- })) };
32
- }
12
+ export const hasRichBody = (rich: WixRichContent | null | undefined): boolean => !isPlainRicos(rich);
13
+ export const plainPostBody = (body: string): WixRichContent => plainTextToRicos(body);
33
14
  /** Current Blog media shape plus older coverImage projections. */
34
15
  export function postCoverUrl(raw: Record<string, any> | null | undefined): string {
35
16
  if (!raw) return "";