@wix/himalaya-cli 0.1078.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 (46) 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/define-app.d.ts +9 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +41 -1
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/site-shell.d.ts +0 -14
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/validator.d.ts +36 -0
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/rich-content/src/display.d.ts +26 -0
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/rich-content/src/index.d.ts +79 -0
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +26 -49
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdui.mjs +11 -3
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-rich-content.mjs +344 -0
  11. package/dist/authoring-runtime/node_modules/@wix/himalaya/package.json +4 -0
  12. package/dist/cli.mjs +104 -7
  13. package/guides/importable-sdks.md +1 -0
  14. package/guides/sdk-reference.json +74 -1
  15. package/guides/splash-screens.md +13 -1
  16. package/package.json +2 -2
  17. package/templates/beauty-salon-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  18. package/templates/beauty-salon-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  19. package/templates/community-club-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  20. package/templates/community-club-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  21. package/templates/community-club-owner/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
  22. package/templates/events-meetups-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  23. package/templates/events-meetups-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  24. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
  25. package/templates/gym-fitness-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  26. package/templates/gym-fitness-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  27. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
  28. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  29. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  30. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
  31. package/templates/owner-lite/tier5-src/_shared/owner-post-content.ts +7 -26
  32. package/templates/owner-lite/tier5-src/_shared/owner-rich-guard.ts +12 -41
  33. package/templates/owner-lite/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
  34. package/templates/podcast-creator-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  35. package/templates/podcast-creator-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  36. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
  37. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  38. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  39. package/templates/restaurant-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  40. package/templates/restaurant-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  41. package/templates/restaurant-owner/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
  42. package/templates/retail-store-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  43. package/templates/retail-store-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  44. package/templates/yoga-studio-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  45. package/templates/yoga-studio-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  46. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
@@ -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.1078.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": "70cae8e9f3455dbda3af632c51aa39b8900dab8e391552018b340702"
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 "";
@@ -1,17 +1,10 @@
1
- // [rico] #3363 — may a plain owner form round-trip this body? Ricos uses owner-post-content's
2
- // hasRichBody; these cover the Stores HTML description and the FAQ's legacy DraftJS answer.
3
- // [rico] #3365 moves htmlIsTrivial into @himalaya/wix-rich-content with these exact semantics.
1
+ // May a plain owner form round-trip this body? The Ricos and HTML checks are @himalaya/wix-rich-content's;
2
+ // this file adds the read-only previews the forms show for a locked body, and the FAQ's legacy DraftJS answer.
3
+ // Inside the platform source this import is a relative path, so node test runs resolve it; an app's copy
4
+ // imports "@himalaya/wix-rich-content".
5
+ import { htmlIsTrivial, isPlainRicos } from "@himalaya/wix-rich-content";
4
6
 
5
- /** A tag opener (`<p`, `</p`, `<!--`), as opposed to a bare `<` in prose ("cups < 8 cm"). */
6
- const TAG = /<[a-z!/?]/i;
7
-
8
- /** True when a plain text field can hold this HTML without losing anything: empty, bare text
9
- * (no tag — a lone `<` in prose is text), or exactly one attribute-less `<p>…</p>` with no
10
- * inner tags. */
11
- export function htmlIsTrivial(html: string | null | undefined): boolean {
12
- const s = (html ?? "").trim();
13
- return !s || !TAG.test(s) || /^<p>[^<>]*<\/p>$/.test(s);
14
- }
7
+ export { htmlIsTrivial };
15
8
 
16
9
  const ENTITIES: Record<string, string> = { "&nbsp;": " ", "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": "\"", "&#39;": "'" };
17
10
 
@@ -23,35 +16,13 @@ export function htmlPreviewText(html: string | null | undefined): string {
23
16
  .split("\n").map((l) => l.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n");
24
17
  }
25
18
 
26
- type RicosNode = { type?: unknown; nodes?: unknown; textData?: { decorations?: unknown }; paragraphData?: unknown; [key: string]: unknown };
27
- const onlyKeys = (o: object, allowed: string[]) => Object.keys(o).every((k) => allowed.includes(k));
28
- const emptyObject = (v: unknown) => v === undefined || (!!v && typeof v === "object" && Object.keys(v as object).length === 0);
29
-
30
- /** The paragraph styling a PLAIN paragraph still carries on the Ricos wire (Events v3 sends
31
- * `{ textStyle: { textAlignment: "AUTO" }, indentation: 0 }` on every paragraph). */
32
- function defaultParagraphData(pd: unknown): boolean {
33
- if (pd === undefined) return true;
34
- if (!pd || typeof pd !== "object" || !onlyKeys(pd, ["textStyle", "indentation"])) return false;
35
- const { textStyle, indentation } = pd as { textStyle?: unknown; indentation?: unknown };
36
- if (indentation !== undefined && indentation !== 0) return false;
37
- if (textStyle === undefined) return true;
38
- if (!textStyle || typeof textStyle !== "object" || !onlyKeys(textStyle, ["textAlignment"])) return false;
39
- const align = (textStyle as { textAlignment?: unknown }).textAlignment;
40
- return align === undefined || align === "AUTO";
41
- }
42
-
43
19
  /** True when a plain text field can round-trip this Ricos document: no document styling, and
44
- * only paragraphs of undecorated text whose paragraph data is the wire default. Anything else —
45
- * a heading, list, link, bold run, media node, alignment or indent — is rich. */
46
- export function ricosIsPlain(doc: { nodes?: unknown; documentStyle?: unknown } | null | undefined): boolean {
47
- if (!doc || !emptyObject(doc.documentStyle)) return false;
48
- const nodes = Array.isArray(doc.nodes) ? (doc.nodes as RicosNode[]) : [];
49
- return nodes.every((n) => !!n && typeof n === "object" && n.type === "PARAGRAPH"
50
- && onlyKeys(n, ["type", "id", "nodes", "paragraphData"]) && defaultParagraphData(n.paragraphData)
51
- && (Array.isArray(n.nodes) ? (n.nodes as RicosNode[]) : []).every((t) => !!t && typeof t === "object" && t.type === "TEXT"
52
- && onlyKeys(t, ["type", "id", "textData", "nodes"]) && (!Array.isArray(t.nodes) || t.nodes.length === 0)
53
- && !(Array.isArray(t.textData?.decorations) && t.textData.decorations.length)));
54
- }
20
+ * only paragraphs of undecorated text whose paragraph data is the wire default (Events v3 sends
21
+ * `{ textStyle: { textAlignment: "AUTO" }, indentation: 0 }` on every paragraph). Anything else —
22
+ * a heading, list, link, bold run, media node, alignment or indent — is rich, and so is a missing
23
+ * document (callers decide what an absent body means). */
24
+ export const ricosIsPlain = (doc: { nodes?: unknown; documentStyle?: unknown } | null | undefined): boolean =>
25
+ !!doc && isPlainRicos(doc);
55
26
 
56
27
  /** Preview text for a legacy DraftJS document: its `blocks[].text` joined by a blank line, or ""
57
28
  * when the payload is not DraftJS JSON (never a throw). */
@@ -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 "";
@@ -1,17 +1,10 @@
1
- // [rico] #3363 — may a plain owner form round-trip this body? Ricos uses owner-post-content's
2
- // hasRichBody; these cover the Stores HTML description and the FAQ's legacy DraftJS answer.
3
- // [rico] #3365 moves htmlIsTrivial into @himalaya/wix-rich-content with these exact semantics.
1
+ // May a plain owner form round-trip this body? The Ricos and HTML checks are @himalaya/wix-rich-content's;
2
+ // this file adds the read-only previews the forms show for a locked body, and the FAQ's legacy DraftJS answer.
3
+ // Inside the platform source this import is a relative path, so node test runs resolve it; an app's copy
4
+ // imports "@himalaya/wix-rich-content".
5
+ import { htmlIsTrivial, isPlainRicos } from "@himalaya/wix-rich-content";
4
6
 
5
- /** A tag opener (`<p`, `</p`, `<!--`), as opposed to a bare `<` in prose ("cups < 8 cm"). */
6
- const TAG = /<[a-z!/?]/i;
7
-
8
- /** True when a plain text field can hold this HTML without losing anything: empty, bare text
9
- * (no tag — a lone `<` in prose is text), or exactly one attribute-less `<p>…</p>` with no
10
- * inner tags. */
11
- export function htmlIsTrivial(html: string | null | undefined): boolean {
12
- const s = (html ?? "").trim();
13
- return !s || !TAG.test(s) || /^<p>[^<>]*<\/p>$/.test(s);
14
- }
7
+ export { htmlIsTrivial };
15
8
 
16
9
  const ENTITIES: Record<string, string> = { "&nbsp;": " ", "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": "\"", "&#39;": "'" };
17
10
 
@@ -23,35 +16,13 @@ export function htmlPreviewText(html: string | null | undefined): string {
23
16
  .split("\n").map((l) => l.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n");
24
17
  }
25
18
 
26
- type RicosNode = { type?: unknown; nodes?: unknown; textData?: { decorations?: unknown }; paragraphData?: unknown; [key: string]: unknown };
27
- const onlyKeys = (o: object, allowed: string[]) => Object.keys(o).every((k) => allowed.includes(k));
28
- const emptyObject = (v: unknown) => v === undefined || (!!v && typeof v === "object" && Object.keys(v as object).length === 0);
29
-
30
- /** The paragraph styling a PLAIN paragraph still carries on the Ricos wire (Events v3 sends
31
- * `{ textStyle: { textAlignment: "AUTO" }, indentation: 0 }` on every paragraph). */
32
- function defaultParagraphData(pd: unknown): boolean {
33
- if (pd === undefined) return true;
34
- if (!pd || typeof pd !== "object" || !onlyKeys(pd, ["textStyle", "indentation"])) return false;
35
- const { textStyle, indentation } = pd as { textStyle?: unknown; indentation?: unknown };
36
- if (indentation !== undefined && indentation !== 0) return false;
37
- if (textStyle === undefined) return true;
38
- if (!textStyle || typeof textStyle !== "object" || !onlyKeys(textStyle, ["textAlignment"])) return false;
39
- const align = (textStyle as { textAlignment?: unknown }).textAlignment;
40
- return align === undefined || align === "AUTO";
41
- }
42
-
43
19
  /** True when a plain text field can round-trip this Ricos document: no document styling, and
44
- * only paragraphs of undecorated text whose paragraph data is the wire default. Anything else —
45
- * a heading, list, link, bold run, media node, alignment or indent — is rich. */
46
- export function ricosIsPlain(doc: { nodes?: unknown; documentStyle?: unknown } | null | undefined): boolean {
47
- if (!doc || !emptyObject(doc.documentStyle)) return false;
48
- const nodes = Array.isArray(doc.nodes) ? (doc.nodes as RicosNode[]) : [];
49
- return nodes.every((n) => !!n && typeof n === "object" && n.type === "PARAGRAPH"
50
- && onlyKeys(n, ["type", "id", "nodes", "paragraphData"]) && defaultParagraphData(n.paragraphData)
51
- && (Array.isArray(n.nodes) ? (n.nodes as RicosNode[]) : []).every((t) => !!t && typeof t === "object" && t.type === "TEXT"
52
- && onlyKeys(t, ["type", "id", "textData", "nodes"]) && (!Array.isArray(t.nodes) || t.nodes.length === 0)
53
- && !(Array.isArray(t.textData?.decorations) && t.textData.decorations.length)));
54
- }
20
+ * only paragraphs of undecorated text whose paragraph data is the wire default (Events v3 sends
21
+ * `{ textStyle: { textAlignment: "AUTO" }, indentation: 0 }` on every paragraph). Anything else —
22
+ * a heading, list, link, bold run, media node, alignment or indent — is rich, and so is a missing
23
+ * document (callers decide what an absent body means). */
24
+ export const ricosIsPlain = (doc: { nodes?: unknown; documentStyle?: unknown } | null | undefined): boolean =>
25
+ !!doc && isPlainRicos(doc);
55
26
 
56
27
  /** Preview text for a legacy DraftJS document: its `blocks[].text` joined by a blank line, or ""
57
28
  * when the payload is not DraftJS JSON (never a throw). */
@@ -10,8 +10,8 @@ import { defineService } from "@himalaya/state";
10
10
  import { getEvent, updateEvent, type WixEvent } from "@himalaya/wix-events";
11
11
  import { normalizeEvent } from "../_shared/himi-events.js";
12
12
  import { postBodyText, plainPostBody } from "../_shared/owner-post-content.js";
13
- // Not owner-post-content's hasRichBody: Events v3 sends default paragraphData on EVERY paragraph
14
- // (which hasRichBody reads as formatting) and a documentStyle it never looks at.
13
+ // Events v3 sends default paragraphData on EVERY paragraph and an empty documentStyle; ricosIsPlain
14
+ // reads both as plain.
15
15
  import { ricosIsPlain } from "../_shared/owner-rich-guard.js";
16
16
  import { ownerLoadError, isBridgeUnavailable, isOwnerPreviewDemo, shouldQueueOwnerWrite } from "../_shared/owner-load-error.js";
17
17
  import { DEMO_EVENTS, DEMO_NOTE, DEMO_SAVE_NOTE, demoRefusalPatch } from "../_shared/owner-preview-demo.js";
@@ -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 "";
@@ -1,17 +1,10 @@
1
- // [rico] #3363 — may a plain owner form round-trip this body? Ricos uses owner-post-content's
2
- // hasRichBody; these cover the Stores HTML description and the FAQ's legacy DraftJS answer.
3
- // [rico] #3365 moves htmlIsTrivial into @himalaya/wix-rich-content with these exact semantics.
1
+ // May a plain owner form round-trip this body? The Ricos and HTML checks are @himalaya/wix-rich-content's;
2
+ // this file adds the read-only previews the forms show for a locked body, and the FAQ's legacy DraftJS answer.
3
+ // Inside the platform source this import is a relative path, so node test runs resolve it; an app's copy
4
+ // imports "@himalaya/wix-rich-content".
5
+ import { htmlIsTrivial, isPlainRicos } from "@himalaya/wix-rich-content";
4
6
 
5
- /** A tag opener (`<p`, `</p`, `<!--`), as opposed to a bare `<` in prose ("cups < 8 cm"). */
6
- const TAG = /<[a-z!/?]/i;
7
-
8
- /** True when a plain text field can hold this HTML without losing anything: empty, bare text
9
- * (no tag — a lone `<` in prose is text), or exactly one attribute-less `<p>…</p>` with no
10
- * inner tags. */
11
- export function htmlIsTrivial(html: string | null | undefined): boolean {
12
- const s = (html ?? "").trim();
13
- return !s || !TAG.test(s) || /^<p>[^<>]*<\/p>$/.test(s);
14
- }
7
+ export { htmlIsTrivial };
15
8
 
16
9
  const ENTITIES: Record<string, string> = { "&nbsp;": " ", "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": "\"", "&#39;": "'" };
17
10
 
@@ -23,35 +16,13 @@ export function htmlPreviewText(html: string | null | undefined): string {
23
16
  .split("\n").map((l) => l.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n");
24
17
  }
25
18
 
26
- type RicosNode = { type?: unknown; nodes?: unknown; textData?: { decorations?: unknown }; paragraphData?: unknown; [key: string]: unknown };
27
- const onlyKeys = (o: object, allowed: string[]) => Object.keys(o).every((k) => allowed.includes(k));
28
- const emptyObject = (v: unknown) => v === undefined || (!!v && typeof v === "object" && Object.keys(v as object).length === 0);
29
-
30
- /** The paragraph styling a PLAIN paragraph still carries on the Ricos wire (Events v3 sends
31
- * `{ textStyle: { textAlignment: "AUTO" }, indentation: 0 }` on every paragraph). */
32
- function defaultParagraphData(pd: unknown): boolean {
33
- if (pd === undefined) return true;
34
- if (!pd || typeof pd !== "object" || !onlyKeys(pd, ["textStyle", "indentation"])) return false;
35
- const { textStyle, indentation } = pd as { textStyle?: unknown; indentation?: unknown };
36
- if (indentation !== undefined && indentation !== 0) return false;
37
- if (textStyle === undefined) return true;
38
- if (!textStyle || typeof textStyle !== "object" || !onlyKeys(textStyle, ["textAlignment"])) return false;
39
- const align = (textStyle as { textAlignment?: unknown }).textAlignment;
40
- return align === undefined || align === "AUTO";
41
- }
42
-
43
19
  /** True when a plain text field can round-trip this Ricos document: no document styling, and
44
- * only paragraphs of undecorated text whose paragraph data is the wire default. Anything else —
45
- * a heading, list, link, bold run, media node, alignment or indent — is rich. */
46
- export function ricosIsPlain(doc: { nodes?: unknown; documentStyle?: unknown } | null | undefined): boolean {
47
- if (!doc || !emptyObject(doc.documentStyle)) return false;
48
- const nodes = Array.isArray(doc.nodes) ? (doc.nodes as RicosNode[]) : [];
49
- return nodes.every((n) => !!n && typeof n === "object" && n.type === "PARAGRAPH"
50
- && onlyKeys(n, ["type", "id", "nodes", "paragraphData"]) && defaultParagraphData(n.paragraphData)
51
- && (Array.isArray(n.nodes) ? (n.nodes as RicosNode[]) : []).every((t) => !!t && typeof t === "object" && t.type === "TEXT"
52
- && onlyKeys(t, ["type", "id", "textData", "nodes"]) && (!Array.isArray(t.nodes) || t.nodes.length === 0)
53
- && !(Array.isArray(t.textData?.decorations) && t.textData.decorations.length)));
54
- }
20
+ * only paragraphs of undecorated text whose paragraph data is the wire default (Events v3 sends
21
+ * `{ textStyle: { textAlignment: "AUTO" }, indentation: 0 }` on every paragraph). Anything else —
22
+ * a heading, list, link, bold run, media node, alignment or indent — is rich, and so is a missing
23
+ * document (callers decide what an absent body means). */
24
+ export const ricosIsPlain = (doc: { nodes?: unknown; documentStyle?: unknown } | null | undefined): boolean =>
25
+ !!doc && isPlainRicos(doc);
55
26
 
56
27
  /** Preview text for a legacy DraftJS document: its `blocks[].text` joined by a blank line, or ""
57
28
  * when the payload is not DraftJS JSON (never a throw). */
@@ -10,8 +10,8 @@ import { defineService } from "@himalaya/state";
10
10
  import { getEvent, updateEvent, type WixEvent } from "@himalaya/wix-events";
11
11
  import { normalizeEvent } from "../_shared/himi-events.js";
12
12
  import { postBodyText, plainPostBody } from "../_shared/owner-post-content.js";
13
- // Not owner-post-content's hasRichBody: Events v3 sends default paragraphData on EVERY paragraph
14
- // (which hasRichBody reads as formatting) and a documentStyle it never looks at.
13
+ // Events v3 sends default paragraphData on EVERY paragraph and an empty documentStyle; ricosIsPlain
14
+ // reads both as plain.
15
15
  import { ricosIsPlain } from "../_shared/owner-rich-guard.js";
16
16
  import { ownerLoadError, isBridgeUnavailable, isOwnerPreviewDemo, shouldQueueOwnerWrite } from "../_shared/owner-load-error.js";
17
17
  import { DEMO_EVENTS, DEMO_NOTE, DEMO_SAVE_NOTE, demoRefusalPatch } from "../_shared/owner-preview-demo.js";
@@ -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 "";
@@ -1,17 +1,10 @@
1
- // [rico] #3363 — may a plain owner form round-trip this body? Ricos uses owner-post-content's
2
- // hasRichBody; these cover the Stores HTML description and the FAQ's legacy DraftJS answer.
3
- // [rico] #3365 moves htmlIsTrivial into @himalaya/wix-rich-content with these exact semantics.
1
+ // May a plain owner form round-trip this body? The Ricos and HTML checks are @himalaya/wix-rich-content's;
2
+ // this file adds the read-only previews the forms show for a locked body, and the FAQ's legacy DraftJS answer.
3
+ // Inside the platform source this import is a relative path, so node test runs resolve it; an app's copy
4
+ // imports "@himalaya/wix-rich-content".
5
+ import { htmlIsTrivial, isPlainRicos } from "@himalaya/wix-rich-content";
4
6
 
5
- /** A tag opener (`<p`, `</p`, `<!--`), as opposed to a bare `<` in prose ("cups < 8 cm"). */
6
- const TAG = /<[a-z!/?]/i;
7
-
8
- /** True when a plain text field can hold this HTML without losing anything: empty, bare text
9
- * (no tag — a lone `<` in prose is text), or exactly one attribute-less `<p>…</p>` with no
10
- * inner tags. */
11
- export function htmlIsTrivial(html: string | null | undefined): boolean {
12
- const s = (html ?? "").trim();
13
- return !s || !TAG.test(s) || /^<p>[^<>]*<\/p>$/.test(s);
14
- }
7
+ export { htmlIsTrivial };
15
8
 
16
9
  const ENTITIES: Record<string, string> = { "&nbsp;": " ", "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": "\"", "&#39;": "'" };
17
10
 
@@ -23,35 +16,13 @@ export function htmlPreviewText(html: string | null | undefined): string {
23
16
  .split("\n").map((l) => l.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n");
24
17
  }
25
18
 
26
- type RicosNode = { type?: unknown; nodes?: unknown; textData?: { decorations?: unknown }; paragraphData?: unknown; [key: string]: unknown };
27
- const onlyKeys = (o: object, allowed: string[]) => Object.keys(o).every((k) => allowed.includes(k));
28
- const emptyObject = (v: unknown) => v === undefined || (!!v && typeof v === "object" && Object.keys(v as object).length === 0);
29
-
30
- /** The paragraph styling a PLAIN paragraph still carries on the Ricos wire (Events v3 sends
31
- * `{ textStyle: { textAlignment: "AUTO" }, indentation: 0 }` on every paragraph). */
32
- function defaultParagraphData(pd: unknown): boolean {
33
- if (pd === undefined) return true;
34
- if (!pd || typeof pd !== "object" || !onlyKeys(pd, ["textStyle", "indentation"])) return false;
35
- const { textStyle, indentation } = pd as { textStyle?: unknown; indentation?: unknown };
36
- if (indentation !== undefined && indentation !== 0) return false;
37
- if (textStyle === undefined) return true;
38
- if (!textStyle || typeof textStyle !== "object" || !onlyKeys(textStyle, ["textAlignment"])) return false;
39
- const align = (textStyle as { textAlignment?: unknown }).textAlignment;
40
- return align === undefined || align === "AUTO";
41
- }
42
-
43
19
  /** True when a plain text field can round-trip this Ricos document: no document styling, and
44
- * only paragraphs of undecorated text whose paragraph data is the wire default. Anything else —
45
- * a heading, list, link, bold run, media node, alignment or indent — is rich. */
46
- export function ricosIsPlain(doc: { nodes?: unknown; documentStyle?: unknown } | null | undefined): boolean {
47
- if (!doc || !emptyObject(doc.documentStyle)) return false;
48
- const nodes = Array.isArray(doc.nodes) ? (doc.nodes as RicosNode[]) : [];
49
- return nodes.every((n) => !!n && typeof n === "object" && n.type === "PARAGRAPH"
50
- && onlyKeys(n, ["type", "id", "nodes", "paragraphData"]) && defaultParagraphData(n.paragraphData)
51
- && (Array.isArray(n.nodes) ? (n.nodes as RicosNode[]) : []).every((t) => !!t && typeof t === "object" && t.type === "TEXT"
52
- && onlyKeys(t, ["type", "id", "textData", "nodes"]) && (!Array.isArray(t.nodes) || t.nodes.length === 0)
53
- && !(Array.isArray(t.textData?.decorations) && t.textData.decorations.length)));
54
- }
20
+ * only paragraphs of undecorated text whose paragraph data is the wire default (Events v3 sends
21
+ * `{ textStyle: { textAlignment: "AUTO" }, indentation: 0 }` on every paragraph). Anything else —
22
+ * a heading, list, link, bold run, media node, alignment or indent — is rich, and so is a missing
23
+ * document (callers decide what an absent body means). */
24
+ export const ricosIsPlain = (doc: { nodes?: unknown; documentStyle?: unknown } | null | undefined): boolean =>
25
+ !!doc && isPlainRicos(doc);
55
26
 
56
27
  /** Preview text for a legacy DraftJS document: its `blocks[].text` joined by a blank line, or ""
57
28
  * when the payload is not DraftJS JSON (never a throw). */
@@ -10,8 +10,8 @@ import { defineService } from "@himalaya/state";
10
10
  import { getEvent, updateEvent, type WixEvent } from "@himalaya/wix-events";
11
11
  import { normalizeEvent } from "../_shared/himi-events.js";
12
12
  import { postBodyText, plainPostBody } from "../_shared/owner-post-content.js";
13
- // Not owner-post-content's hasRichBody: Events v3 sends default paragraphData on EVERY paragraph
14
- // (which hasRichBody reads as formatting) and a documentStyle it never looks at.
13
+ // Events v3 sends default paragraphData on EVERY paragraph and an empty documentStyle; ricosIsPlain
14
+ // reads both as plain.
15
15
  import { ricosIsPlain } from "../_shared/owner-rich-guard.js";
16
16
  import { ownerLoadError, isBridgeUnavailable, isOwnerPreviewDemo, shouldQueueOwnerWrite } from "../_shared/owner-load-error.js";
17
17
  import { DEMO_EVENTS, DEMO_NOTE, DEMO_SAVE_NOTE, demoRefusalPatch } from "../_shared/owner-preview-demo.js";
@@ -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 "";