@wix/himalaya-cli 0.1079.0 → 0.1081.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 (49) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/builder.d.ts +20 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +62 -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 +25 -49
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdui.mjs +508 -41
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-rich-content.mjs +345 -0
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/package.json +4 -0
  11. package/dist/cli.mjs +2043 -1280
  12. package/dist/preview-assets.generated.bin +0 -0
  13. package/guides/component-reference.json +10 -1
  14. package/guides/component-reference.md +20 -2
  15. package/guides/importable-sdks.md +1 -0
  16. package/guides/index.json +1 -1
  17. package/guides/sdk-reference.json +74 -1
  18. package/guides/splash-screens.md +13 -1
  19. package/package.json +2 -2
  20. package/templates/beauty-salon-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  21. package/templates/beauty-salon-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  22. package/templates/community-club-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  23. package/templates/community-club-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  24. package/templates/community-club-owner/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
  25. package/templates/events-meetups-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  26. package/templates/events-meetups-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  27. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
  28. package/templates/gym-fitness-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  29. package/templates/gym-fitness-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  30. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
  31. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  32. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  33. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
  34. package/templates/owner-lite/tier5-src/_shared/owner-post-content.ts +7 -26
  35. package/templates/owner-lite/tier5-src/_shared/owner-rich-guard.ts +12 -41
  36. package/templates/owner-lite/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
  37. package/templates/podcast-creator-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  38. package/templates/podcast-creator-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  39. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
  40. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  41. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  42. package/templates/restaurant-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  43. package/templates/restaurant-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  44. package/templates/restaurant-owner/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
  45. package/templates/retail-store-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  46. package/templates/retail-store-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  47. package/templates/yoga-studio-owner/tier5-src/_shared/owner-post-content.ts +7 -26
  48. package/templates/yoga-studio-owner/tier5-src/_shared/owner-rich-guard.ts +12 -41
  49. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event-form/index.ts +2 -2
Binary file
@@ -2,7 +2,7 @@
2
2
  "_comment": "GENERATED by core/web-embed/authoring/gen-reference.mjs from core/ts/src/schema.ts — do not hand-edit. The schema is the source of truth; regenerate on schema changes.",
3
3
  "schemaSource": "core/ts/src/schema.ts",
4
4
  "bindingGrammar": "$service:<service>.<path> (worker state) · $item.<field> (list row) · $flag:<key> (feature flag) · $env:<key> (client environment — #1056). Bindings resolve in the text/value props marked 'binding-aware' below — INCLUDING Modifiers token refs — but only `background` and `border.color`, which are the two whose token a binding actually fills: a worker can drive per-item theming (e.g. a game board's per-square color), not just text. `shadow` is NOT binding-aware on any renderer, and a gradient is driven by its own `backgroundGradientPath` prop resolving to a gradient token NAME, not by a binding inside a token ref — so `shadow: { token: \"$item.x\" }` and `backgroundGradient: { token: \"$item.x\" }` keep the sentinel and silently lose the per-item styling. Colour slots split by LEVEL, not by component: inside modifiers a colour is a TokenRef OBJECT ({ token: \"…\" }), inside props it is a bare TokenName STRING — there is no modifiers.color or modifiers.tint (see the 'Token slots' section in component-reference.md). A few input props render the LITERAL and are NOT binding-aware — notably SearchField.placeholder and Chip.label; SearchField.value is live and binding-aware like other content props. Bindings also CANNOT target strict-enum fields (style, size, alignment, severity, presentation, contentMode, …) — those fail at render, not validation. $env: is client-resolved (no worker) and valid in modifiers.hiddenPath and FragmentSlot.casePath ONLY — keys reduceMotion/colorScheme/sizeClass/isRTL, e.g. hiddenPath:\"$env:reduceMotion\" or hiddenPath:\"$env:colorScheme==light\", casePath:\"$env:colorScheme\"; unknown key fails open (shown / defaultCase).",
5
- "componentCount": 82,
5
+ "componentCount": 83,
6
6
  "notImplementedComponents": [
7
7
  "FieldError",
8
8
  "Picker",
@@ -981,6 +981,15 @@
981
981
  ],
982
982
  "propsSource": "export interface DataTableProps {\n columns?: DataTableColumn[];\n rows?: DataTableRow[];\n /**\n * `$service:` ONLY → an array of OBJECTS keyed by `column.key`. An array of\n * arrays (positional, like the literal `rows`) is also accepted.\n */\n rowsPath?: string;\n /**\n * `hideHeader`, not `showHeader`: the proto field is a bool that defaults to\n * false, so the affirmative spelling would default a table to headerless.\n */\n hideHeader?: boolean;\n totals?: DataTableRow;\n /** `$service:` ONLY → one row object. */\n totalsPath?: string;\n /** 0 / absent ⇒ unbounded */\n maxRows?: number;\n zebra?: boolean;\n}"
983
983
  },
984
+ {
985
+ "name": "RichContent",
986
+ "category": "Rich content",
987
+ "propsType": "RichContentProps",
988
+ "bindings": [
989
+ "documentJsonPath"
990
+ ],
991
+ "propsSource": "export interface RichContentProps {\n /** `$service:<svc>.<path>` or `$item.<field>` → the UTF-8 JSON of `{ nodes, metadata?, documentStyle? }`. */\n documentJsonPath?: string;\n /** Literal document JSON (seed / offline). */\n documentJson?: string;\n /** Dispatched on a link, button or card tap with `args.url`. Absent ⇒ http(s) opens the in-app browser. */\n onLink?: Action;\n /** Rendered when the document is rejected (over budget / invalid). Absent ⇒ nothing. */\n fallback?: Component;\n}"
992
+ },
984
993
  {
985
994
  "name": "VoiceNote",
986
995
  "category": "Recorded-audio bubble (messenger design G4)",
@@ -3,7 +3,7 @@
3
3
  > GENERATED from `core/ts/src/schema.ts` by `core/web-embed/authoring/gen-reference.mjs`.
4
4
  > The schema is the source of truth — regenerate on schema changes; do not hand-edit.
5
5
 
6
- **82 core component types.** Every component body is `{ "package": "himalaya", "type": "<Type>", "props": { … } }`.
6
+ **83 core component types.** Every component body is `{ "package": "himalaya", "type": "<Type>", "props": { … } }`.
7
7
  Binding grammar: `$service:<service>.<path>` (worker state) · `$item.<field>` (list row template) · `$flag:<key>` (feature flag) · `$env:<key>` (client environment — #1056).
8
8
  Bindings resolve in the text/value props marked **binding-aware** below — **including two `Modifiers` token refs**, `background` and `border.color`, which are the only two whose token a binding actually fills: a worker can drive per-item **theming** (e.g. a game board's per-square color, `background: { token: "$item.bg" }`), not just text content. `shadow` and `backgroundGradient` are `TokenRef`s but are **not** binding-aware on any renderer — `shadow: { token: "$item.x" }` keeps the sentinel as a literal token name and silently loses the per-item styling, and a gradient is driven by its own `backgroundGradientPath` prop resolving to a gradient token NAME, not by a binding inside a token ref.
9
9
  `$env:` is **client-resolved** (no worker) and valid in `modifiers.hiddenPath` and `FragmentSlot.casePath` **only** — keys `reduceMotion` / `colorScheme` / `sizeClass` / `isRTL`, with an optional `==`/`!=` comparison (e.g. `hiddenPath: "$env:reduceMotion"`, `hiddenPath: "$env:colorScheme==light"`, `casePath: "$env:colorScheme"`). An unknown key fails open (shown / `defaultCase`).
@@ -92,6 +92,7 @@ See also **[Modifiers](#modifiers)** (the `modifiers` block) and **[Actions](#ac
92
92
  | `DonutChart` | Analytics data-viz | `slicesPath` |
93
93
  | `Legend` | Analytics data-viz | `itemsPath` |
94
94
  | `DataTable` | Analytics data-viz | `rowsPath`, `totalsPath` |
95
+ | `RichContent` | Rich content | `documentJsonPath` |
95
96
  | `VoiceNote` | Recorded-audio bubble (messenger design G4) | `waveformPath`, `durationMsPath`, `positionMsPath`, `statePath`, `deliveryPath`, `ratePath` |
96
97
  | `Marquee` | Recorded-audio bubble (messenger design G4) | — |
97
98
  | `Timeline` | Recorded-audio bubble (messenger design G4) | `entriesPath` |
@@ -114,7 +115,7 @@ See also **[Modifiers](#modifiers)** (the `modifiers` block) and **[Actions](#ac
114
115
 
115
116
  These props are accepted by the schema and pass validation, but the named platform's renderer never reads them — they silently do nothing (no warning, no log). Prefer a prop NOT listed here, or drive the behaviour from a worker.
116
117
 
117
- **Scope: 14 of 82 components, all of them ios-only** — every other component/platform pair reads every prop it decodes. The list splits in two, and the halves mean different things:
118
+ **Scope: 14 of 83 components, all of them ios-only** — every other component/platform pair reads every prop it decodes. The list splits in two, and the halves mean different things:
118
119
 
119
120
  - **`category` (4: `BottomSheet`, `Menu`, `Tooltip`, `Popover`)** — the prop changes the component's *kind*. These genuinely are **not implemented**: an overlay renders always-visible inline, so the component does not do the job its name implies. Check the row's note for a working alternative — for a dismissible sheet it is `presentation: "sheet"` on the navigating `Action`, not `BottomSheet`.
120
121
  - **`styling` (10: `FieldError`, `Picker`, `DatePicker`, `Tabs`, `Snackbar`, `Slider`, `ActivityIndicator`, `ChipsInput`, `OtpInput`, `Video`)** — the component itself **works**; only the listed prop is inert. `Slider` on iOS is a working slider whose `debounceMs` does not throttle — not a broken `Slider`. Use the component; just do not depend on that one prop.
@@ -1597,6 +1598,23 @@ export interface DataTableProps {
1597
1598
  }
1598
1599
  ```
1599
1600
 
1601
+ ### Rich content
1602
+
1603
+ #### RichContent
1604
+
1605
+ ```ts
1606
+ export interface RichContentProps {
1607
+ /** `$service:<svc>.<path>` or `$item.<field>` → the UTF-8 JSON of `{ nodes, metadata?, documentStyle? }`. */
1608
+ documentJsonPath?: string;
1609
+ /** Literal document JSON (seed / offline). */
1610
+ documentJson?: string;
1611
+ /** Dispatched on a link, button or card tap with `args.url`. Absent ⇒ http(s) opens the in-app browser. */
1612
+ onLink?: Action;
1613
+ /** Rendered when the document is rejected (over budget / invalid). Absent ⇒ nothing. */
1614
+ fallback?: Component;
1615
+ }
1616
+ ```
1617
+
1600
1618
  ### Recorded-audio bubble (messenger design G4)
1601
1619
 
1602
1620
  #### VoiceNote
@@ -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
package/guides/index.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "_comment": "AUTO-GENERATED by tools/authoring-kit/bundle.mjs — DO NOT EDIT. Regenerate with: npm run authoring:gen",
3
3
  "schemaStamp": {
4
- "componentCount": 82,
4
+ "componentCount": 83,
5
5
  "schemaSource": "core/ts/src/schema.ts"
6
6
  },
7
7
  "portalUrl": "https://apps.wix.com/_api/himi-serve/docs",
@@ -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.1081.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": "fabad058e08298bebcb3bacb89f82255ed6b92c6ee395d7b444c7fd3"
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). */