@wix/himalaya-cli 0.1104.0 → 0.1106.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 (91) hide show
  1. package/dist/cli.mjs +0 -3
  2. package/fixtures/wix-canned.json +146 -0
  3. package/package.json +2 -2
  4. package/templates/beauty-salon/dev/index.ts +22 -79
  5. package/templates/beauty-salon/tier5-src/_shared/rich-body.ts +28 -0
  6. package/templates/beauty-salon/tier5-src/_shared/rich-content-link.ts +34 -0
  7. package/templates/beauty-salon/tier5-src/himi-blog-post/index.ts +26 -82
  8. package/templates/beauty-salon-owner/dev/index.ts +44 -1
  9. package/templates/beauty-salon-owner/tier5-src/_shared/rich-body.ts +28 -0
  10. package/templates/beauty-salon-owner/tier5-src/_shared/rich-content-link.ts +34 -0
  11. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-post/index.ts +49 -2
  12. package/templates/branded-lite/dev/index.ts +22 -79
  13. package/templates/branded-lite/tier5-src/_shared/rich-body.ts +28 -0
  14. package/templates/branded-lite/tier5-src/_shared/rich-content-link.ts +34 -0
  15. package/templates/branded-lite/tier5-src/himi-blog-post/index.ts +26 -82
  16. package/templates/community-club/dev/index.ts +22 -79
  17. package/templates/community-club/tier5-src/_shared/rich-body.ts +28 -0
  18. package/templates/community-club/tier5-src/_shared/rich-content-link.ts +34 -0
  19. package/templates/community-club/tier5-src/himi-blog-post/index.ts +26 -82
  20. package/templates/community-club-owner/dev/index.ts +44 -1
  21. package/templates/community-club-owner/tier5-src/_shared/rich-body.ts +28 -0
  22. package/templates/community-club-owner/tier5-src/_shared/rich-content-link.ts +34 -0
  23. package/templates/community-club-owner/tier5-src/himi-owner-lite-post/index.ts +49 -2
  24. package/templates/events-meetups/dev/index.ts +22 -79
  25. package/templates/events-meetups/tier5-src/_shared/rich-body.ts +28 -0
  26. package/templates/events-meetups/tier5-src/_shared/rich-content-link.ts +34 -0
  27. package/templates/events-meetups/tier5-src/himi-blog-post/index.ts +26 -82
  28. package/templates/events-meetups-owner/dev/index.ts +44 -1
  29. package/templates/events-meetups-owner/tier5-src/_shared/rich-body.ts +28 -0
  30. package/templates/events-meetups-owner/tier5-src/_shared/rich-content-link.ts +34 -0
  31. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-post/index.ts +49 -2
  32. package/templates/gym-fitness/dev/index.ts +22 -79
  33. package/templates/gym-fitness/tier5-src/_shared/rich-body.ts +28 -0
  34. package/templates/gym-fitness/tier5-src/_shared/rich-content-link.ts +34 -0
  35. package/templates/gym-fitness/tier5-src/himi-blog-post/index.ts +26 -82
  36. package/templates/gym-fitness-owner/dev/index.ts +44 -1
  37. package/templates/gym-fitness-owner/tier5-src/_shared/rich-body.ts +28 -0
  38. package/templates/gym-fitness-owner/tier5-src/_shared/rich-content-link.ts +34 -0
  39. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-post/index.ts +49 -2
  40. package/templates/nonprofit-cause/dev/index.ts +22 -79
  41. package/templates/nonprofit-cause/tier5-src/_shared/rich-body.ts +28 -0
  42. package/templates/nonprofit-cause/tier5-src/_shared/rich-content-link.ts +34 -0
  43. package/templates/nonprofit-cause/tier5-src/himi-blog-post/index.ts +26 -82
  44. package/templates/nonprofit-cause-owner/dev/index.ts +44 -1
  45. package/templates/nonprofit-cause-owner/tier5-src/_shared/rich-body.ts +28 -0
  46. package/templates/nonprofit-cause-owner/tier5-src/_shared/rich-content-link.ts +34 -0
  47. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-post/index.ts +49 -2
  48. package/templates/owner-lite/dev/index.ts +44 -1
  49. package/templates/owner-lite/tier5-src/_shared/rich-body.ts +28 -0
  50. package/templates/owner-lite/tier5-src/_shared/rich-content-link.ts +34 -0
  51. package/templates/owner-lite/tier5-src/himi-owner-lite-post/index.ts +49 -2
  52. package/templates/podcast-creator/dev/index.ts +22 -79
  53. package/templates/podcast-creator/tier5-src/_shared/rich-body.ts +28 -0
  54. package/templates/podcast-creator/tier5-src/_shared/rich-content-link.ts +34 -0
  55. package/templates/podcast-creator/tier5-src/himi-blog-post/index.ts +26 -82
  56. package/templates/podcast-creator-owner/dev/index.ts +44 -1
  57. package/templates/podcast-creator-owner/tier5-src/_shared/rich-body.ts +28 -0
  58. package/templates/podcast-creator-owner/tier5-src/_shared/rich-content-link.ts +34 -0
  59. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-post/index.ts +49 -2
  60. package/templates/portfolio-creative/dev/index.ts +22 -79
  61. package/templates/portfolio-creative/tier5-src/_shared/rich-body.ts +28 -0
  62. package/templates/portfolio-creative/tier5-src/_shared/rich-content-link.ts +34 -0
  63. package/templates/portfolio-creative/tier5-src/himi-blog-post/index.ts +26 -82
  64. package/templates/portfolio-creative-owner/dev/index.ts +44 -1
  65. package/templates/portfolio-creative-owner/tier5-src/_shared/rich-body.ts +28 -0
  66. package/templates/portfolio-creative-owner/tier5-src/_shared/rich-content-link.ts +34 -0
  67. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-post/index.ts +49 -2
  68. package/templates/restaurant/dev/index.ts +22 -79
  69. package/templates/restaurant/tier5-src/_shared/rich-body.ts +28 -0
  70. package/templates/restaurant/tier5-src/_shared/rich-content-link.ts +34 -0
  71. package/templates/restaurant/tier5-src/himi-blog-post/index.ts +26 -82
  72. package/templates/restaurant-owner/dev/index.ts +44 -1
  73. package/templates/restaurant-owner/tier5-src/_shared/rich-body.ts +28 -0
  74. package/templates/restaurant-owner/tier5-src/_shared/rich-content-link.ts +34 -0
  75. package/templates/restaurant-owner/tier5-src/himi-owner-lite-post/index.ts +49 -2
  76. package/templates/retail-store/dev/index.ts +22 -79
  77. package/templates/retail-store/tier5-src/_shared/rich-body.ts +28 -0
  78. package/templates/retail-store/tier5-src/_shared/rich-content-link.ts +34 -0
  79. package/templates/retail-store/tier5-src/himi-blog-post/index.ts +26 -82
  80. package/templates/retail-store-owner/dev/index.ts +44 -1
  81. package/templates/retail-store-owner/tier5-src/_shared/rich-body.ts +28 -0
  82. package/templates/retail-store-owner/tier5-src/_shared/rich-content-link.ts +34 -0
  83. package/templates/retail-store-owner/tier5-src/himi-owner-lite-post/index.ts +49 -2
  84. package/templates/yoga-studio/dev/index.ts +22 -79
  85. package/templates/yoga-studio/tier5-src/_shared/rich-body.ts +28 -0
  86. package/templates/yoga-studio/tier5-src/_shared/rich-content-link.ts +34 -0
  87. package/templates/yoga-studio/tier5-src/himi-blog-post/index.ts +26 -82
  88. package/templates/yoga-studio-owner/dev/index.ts +44 -1
  89. package/templates/yoga-studio-owner/tier5-src/_shared/rich-body.ts +28 -0
  90. package/templates/yoga-studio-owner/tier5-src/_shared/rich-content-link.ts +34 -0
  91. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-post/index.ts +49 -2
package/dist/cli.mjs CHANGED
@@ -6762,7 +6762,6 @@ function walk(node, registry, path2, out2) {
6762
6762
  var SYSTEM_FONT_FAMILY, TONAL_ELEVATION_ALPHA;
6763
6763
  var init_tokens = __esm({
6764
6764
  "../../core/ts/src/tokens.ts"() {
6765
- "use strict";
6766
6765
  SYSTEM_FONT_FAMILY = "$system";
6767
6766
  TONAL_ELEVATION_ALPHA = [0, 0.05, 0.08, 0.11, 0.12, 0.14];
6768
6767
  }
@@ -24718,7 +24717,6 @@ function shouldRevealError(args) {
24718
24717
  var EMAIL_RE, URL_HOST_RE, INTEGER_RE, NUMBER_RE;
24719
24718
  var init_validation = __esm({
24720
24719
  "../../core/ts/src/validation.ts"() {
24721
- "use strict";
24722
24720
  EMAIL_RE = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/;
24723
24721
  URL_HOST_RE = /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*(:[0-9]+)?$/;
24724
24722
  INTEGER_RE = /^[+-]?[0-9]+$/;
@@ -29119,7 +29117,6 @@ function validateWorkerSubtree(raw, slotId, policy, actionCatalog) {
29119
29117
  var WORKER_SUBTREE_CEILINGS, ID_PART, ACTION_ID;
29120
29118
  var init_worker_subtree = __esm({
29121
29119
  "../../core/ts/src/worker-subtree.ts"() {
29122
- "use strict";
29123
29120
  init_worker_subtree_safe_types_generated();
29124
29121
  WORKER_SUBTREE_CEILINGS = Object.freeze({
29125
29122
  maxEncodedBytes: 65536,
@@ -1646,6 +1646,152 @@
1646
1646
  }
1647
1647
  }
1648
1648
  },
1649
+ "GET /blog/v3/posts/rico-post": {
1650
+ "post": {
1651
+ "id": "rico-post",
1652
+ "title": "Raku glazes, step by step",
1653
+ "slug": "raku-glazes",
1654
+ "excerpt": "How we fire raku in the studio yard.",
1655
+ "firstPublishedDate": "2026-06-03T08:00:00.000Z",
1656
+ "minutesToRead": 3,
1657
+ "richContent": {
1658
+ "nodes": [
1659
+ {
1660
+ "type": "HEADING",
1661
+ "id": "rp-h",
1662
+ "headingData": {
1663
+ "level": 2
1664
+ },
1665
+ "nodes": [
1666
+ {
1667
+ "type": "TEXT",
1668
+ "id": "rp-h-t",
1669
+ "textData": {
1670
+ "text": "Before you fire",
1671
+ "decorations": []
1672
+ }
1673
+ }
1674
+ ]
1675
+ },
1676
+ {
1677
+ "type": "PARAGRAPH",
1678
+ "id": "rp-p",
1679
+ "nodes": [
1680
+ {
1681
+ "type": "TEXT",
1682
+ "id": "rp-p-t1",
1683
+ "textData": {
1684
+ "text": "Always wear gloves.",
1685
+ "decorations": [
1686
+ {
1687
+ "type": "BOLD"
1688
+ }
1689
+ ]
1690
+ }
1691
+ },
1692
+ {
1693
+ "type": "TEXT",
1694
+ "id": "rp-p-t2",
1695
+ "textData": {
1696
+ "text": " Then ",
1697
+ "decorations": []
1698
+ }
1699
+ },
1700
+ {
1701
+ "type": "TEXT",
1702
+ "id": "rp-p-t3",
1703
+ "textData": {
1704
+ "text": "read the firing guide",
1705
+ "decorations": [
1706
+ {
1707
+ "type": "LINK",
1708
+ "linkData": {
1709
+ "link": {
1710
+ "url": "https://www.wix.com/raku-guide",
1711
+ "target": "BLANK"
1712
+ }
1713
+ }
1714
+ }
1715
+ ]
1716
+ }
1717
+ },
1718
+ {
1719
+ "type": "TEXT",
1720
+ "id": "rp-p-t4",
1721
+ "textData": {
1722
+ "text": ".",
1723
+ "decorations": []
1724
+ }
1725
+ }
1726
+ ]
1727
+ },
1728
+ {
1729
+ "type": "BULLETED_LIST",
1730
+ "id": "rp-l",
1731
+ "nodes": [
1732
+ {
1733
+ "type": "LIST_ITEM",
1734
+ "id": "rp-l-0",
1735
+ "nodes": [
1736
+ {
1737
+ "type": "PARAGRAPH",
1738
+ "id": "rp-l-0-p",
1739
+ "nodes": [
1740
+ {
1741
+ "type": "TEXT",
1742
+ "id": "rp-l-0-t",
1743
+ "textData": {
1744
+ "text": "Bisque first",
1745
+ "decorations": []
1746
+ }
1747
+ }
1748
+ ]
1749
+ }
1750
+ ]
1751
+ },
1752
+ {
1753
+ "type": "LIST_ITEM",
1754
+ "id": "rp-l-1",
1755
+ "nodes": [
1756
+ {
1757
+ "type": "PARAGRAPH",
1758
+ "id": "rp-l-1-p",
1759
+ "nodes": [
1760
+ {
1761
+ "type": "TEXT",
1762
+ "id": "rp-l-1-t",
1763
+ "textData": {
1764
+ "text": "Glaze thin",
1765
+ "decorations": []
1766
+ }
1767
+ }
1768
+ ]
1769
+ }
1770
+ ]
1771
+ }
1772
+ ]
1773
+ },
1774
+ {
1775
+ "type": "IMAGE",
1776
+ "id": "rp-img",
1777
+ "imageData": {
1778
+ "image": {
1779
+ "src": {
1780
+ "id": "8ce05f_d81f257ab773486d83187e34816ce1a4~mv2.png"
1781
+ },
1782
+ "width": 1024,
1783
+ "height": 1024
1784
+ },
1785
+ "altText": "Dawn over the mountains behind the studio"
1786
+ }
1787
+ }
1788
+ ],
1789
+ "metadata": {
1790
+ "version": 1
1791
+ }
1792
+ }
1793
+ }
1794
+ },
1649
1795
  "GET /comments/v1/comments/list-by-resource": {
1650
1796
  "comments": [],
1651
1797
  "commentCount": 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1104.0",
3
+ "version": "0.1106.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": "3b9d4a5bc87c7f44c95094ff409a54b61fb726b2c3062b7a587718be"
70
+ "falconPackageHash": "faa54f382a4c5a4903c112395c7bf69887e9b0d8ff91ea2e682af85d"
71
71
  }
@@ -28514,103 +28514,45 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
28514
28514
  }
28515
28515
  },
28516
28516
  {
28517
- "id": "himi_blog_post.blocks",
28517
+ "id": "post_body",
28518
28518
  "body": {
28519
28519
  "package": "himalaya",
28520
- "type": "Grid",
28520
+ "type": "RichContent",
28521
28521
  "props": {
28522
- "columns": 1,
28523
- "spacing": 12,
28524
- "itemsPath": "$service:himiBlogPost.blocks",
28525
- "items": [
28526
- {
28527
- "id": "",
28528
- "body": {
28529
- "package": "himalaya",
28530
- "type": "VStack",
28531
- "props": {
28532
- "spacing": 0,
28533
- "alignment": "leading",
28534
- "children": [
28535
- {
28536
- "id": "himi_blog_post.block_heading",
28537
- "body": {
28538
- "package": "himalaya",
28539
- "type": "Text",
28540
- "props": {
28541
- "text": "$item.text",
28542
- "style": "title3",
28543
- "collapseWhenEmpty": true
28544
- }
28545
- },
28546
- "modifiers": {
28547
- "padding": {
28548
- "top": 8,
28549
- "bottom": 2
28550
- },
28551
- "hiddenPath": "$item.notHeading"
28552
- }
28553
- },
28554
- {
28555
- "id": "",
28556
- "body": {
28557
- "package": "himalaya",
28558
- "type": "Text",
28559
- "props": {
28560
- "text": "$item.text",
28561
- "style": "body",
28562
- "color": "textPrimary",
28563
- "collapseWhenEmpty": true
28564
- }
28565
- },
28566
- "modifiers": {
28567
- "hiddenPath": "$item.notText"
28568
- }
28569
- },
28570
- {
28571
- "id": "",
28572
- "body": {
28573
- "package": "himalaya",
28574
- "type": "Image",
28575
- "props": {
28576
- "url": "$item.image",
28577
- "fillWidth": true,
28578
- "height": 220,
28579
- "contentMode": "fill"
28580
- }
28581
- },
28582
- "modifiers": {
28583
- "accessibilityRole": "decorative",
28584
- "cornerRadius": 8,
28585
- "fillMaxWidth": true,
28586
- "hiddenPath": "$item.notImage"
28587
- }
28588
- }
28589
- ]
28590
- }
28522
+ "documentJsonPath": "$service:himiBlogPost.bodyJson",
28523
+ "onLink": {
28524
+ "actionId": "tier5.himiBlogPost.openLink"
28525
+ },
28526
+ "fallback": {
28527
+ "id": "",
28528
+ "body": {
28529
+ "package": "himalaya",
28530
+ "type": "Text",
28531
+ "props": {
28532
+ "text": "$service:himiBlogPost.bodyText",
28533
+ "style": "body",
28534
+ "color": "textPrimary",
28535
+ "collapseWhenEmpty": true
28591
28536
  }
28592
28537
  }
28593
- ]
28538
+ }
28594
28539
  }
28595
- },
28596
- "modifiers": {
28597
- "hiddenPath": "$service:himiBlogPost.noBlocks"
28598
28540
  }
28599
28541
  },
28600
28542
  {
28601
- "id": "",
28543
+ "id": "post_body_text",
28602
28544
  "body": {
28603
28545
  "package": "himalaya",
28604
28546
  "type": "Text",
28605
28547
  "props": {
28606
- "text": "$service:himiBlogPost.body",
28548
+ "text": "$service:himiBlogPost.bodyText",
28607
28549
  "style": "body",
28608
28550
  "color": "textPrimary",
28609
28551
  "collapseWhenEmpty": true
28610
28552
  }
28611
28553
  },
28612
28554
  "modifiers": {
28613
- "hiddenPath": "$service:himiBlogPost.hasBlocks"
28555
+ "hiddenPath": "$service:himiBlogPost.hasBodyJson"
28614
28556
  }
28615
28557
  },
28616
28558
  {
@@ -29011,7 +28953,8 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
29011
28953
  "declaredCapabilities": [
29012
28954
  "wix_api",
29013
28955
  "share",
29014
- "local_database"
28956
+ "local_database",
28957
+ "open_url"
29015
28958
  ],
29016
28959
  "minBinaryVersion": "0.1.0",
29017
28960
  "bundleVersion": "0.0.1"
@@ -0,0 +1,28 @@
1
+ // [rico] #3370 — an entity's Ricos body, prepared for a RichContent component and its plain-text fallback.
2
+ // One rule for every worker that renders a body (the blog post and the owner post detail today; #3372's
3
+ // verticals next), so they cannot drift apart.
4
+ // Inside the platform source this import is a relative path, so node test runs resolve it; an app's copy
5
+ // imports "@himalaya/wix-rich-content".
6
+ import { encodeRichContent, parseRicosJson, ricosToPlainText } from "@himalaya/wix-rich-content";
7
+
8
+ /** What a body-rendering worker publishes: `<name>Json` for `documentJsonPath`, and the plain text. */
9
+ export interface RichBody {
10
+ /** The document as ONE JSON string (media sized, disallowed links dropped); "" when there is no document
11
+ * or the encoder rejected it. RichContent draws nothing for "", so a screen gates its plain text on it. */
12
+ bodyJson: string;
13
+ /** The plain text: the document's own text when it has any, else `fallback`. */
14
+ bodyText: string;
15
+ }
16
+
17
+ /** Prepares `richContent` (a Ricos document, as a Wix API returns it, or its JSON string). No document, or one
18
+ * that is not a document, gives `{ bodyJson: "", bodyText: fallback }`. A document past the viewer's limits
19
+ * gives bodyJson "" and its plain text — except one rejected as too deep, which is not walked again (the
20
+ * plain-text walk has no depth limit), so `fallback` stands in for it. */
21
+ export function richBody(richContent: unknown, fallback = ""): RichBody {
22
+ const doc = parseRicosJson(richContent);
23
+ if (!doc) return { bodyJson: "", bodyText: fallback };
24
+ // 1400 px — the detail-image budget, which is also the encoder's maximum.
25
+ const { json, rejected } = encodeRichContent(doc, { imageWidth: 1400 });
26
+ const text = rejected === "too_deep" ? "" : ricosToPlainText(doc);
27
+ return { bodyJson: json, bodyText: text || fallback };
28
+ }
@@ -0,0 +1,34 @@
1
+ // [rico] #3370 — a RichContent `on_link` tap reaches the worker as `{ url }` (the renderers merge the tapped
2
+ // link into the action's args). Only http(s) with a host, mailto and tel ever reach the OS; anything
3
+ // else is dropped here, before any dispatch. The renderers already refuse other schemes, but the action
4
+ // arrives here as plain data, so the worker checks it again rather than trusting whoever sent it.
5
+ import { open, defaultMode } from "@himalaya/open-url";
6
+
7
+ /** The viewer's link rule, verbatim (`LINK` in @himalaya/wix-rich-content's display.ts, which the package does
8
+ * not export); a parity test pins this copy to the original. */
9
+ const LINK = /^(https?:\/\/[^\s/?#]\S*|mailto:\S+|tel:\S+)$/i;
10
+ /** ASCII whitespace only, as the viewer trims a link before applying the rule. */
11
+ const trimAscii = (s: string): string => s.replace(/^[\t\n\v\f\r ]+|[\t\n\v\f\r ]+$/g, "");
12
+
13
+ /** The url of a link tap (`{ url }`, `{ args: { url } }` or a bare string), or "" when it is missing or
14
+ * not an http(s), mailto or tel link. */
15
+ export function richLinkUrl(arg: unknown): string {
16
+ const a = arg as { url?: unknown; args?: { url?: unknown } } | null | undefined;
17
+ const raw = typeof arg === "string" ? arg
18
+ : typeof a?.url === "string" ? a.url
19
+ : typeof a?.args?.url === "string" ? a.args.url : "";
20
+ const url = trimAscii(raw);
21
+ return LINK.test(url) ? url : "";
22
+ }
23
+
24
+ /** Opens a link tap through `capability.open_url` (http(s) in the in-app browser, mailto/tel in the system
25
+ * handler). False when the link is blocked, nothing took it, or the host threw. */
26
+ export async function openRichLink(arg: unknown): Promise<boolean> {
27
+ const url = richLinkUrl(arg);
28
+ if (!url) return false;
29
+ try {
30
+ return (await open(url, defaultMode(url))).opened;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
@@ -3,11 +3,12 @@ import { defineService } from "@himalaya/state";
3
3
  import { share as shareSheet } from "@himalaya/share";
4
4
  import { getPost, likePost, unlikePost, type WixPost } from "@himalaya/wix-blog";
5
5
  import { comments, BLOG_COMMENTS_APP_ID, blogCommentRow, listBlogComments, type WixBlogCommentRow } from "@himalaya/wix-comments";
6
- import { wixMediaUrl, WIX_DETAIL_IMAGE } from "@himalaya/wix-media";
7
6
  import { get, type DbEntity } from "@himalaya/local-db";
8
7
  import { isSavedId, toggleSavedId } from "../_shared/himi-blog-saved";
9
8
  import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
10
9
  import { formatDateMedium, formatMonthDay } from "../_shared/engine-intl.js";
10
+ import { openRichLink } from "../_shared/rich-content-link.js";
11
+ import { richBody } from "../_shared/rich-body.js";
11
12
 
12
13
  // Offline-first (data-offline): the post body is a public read-through — when the
13
14
  // live fetch fails (offline) fall back to the last-synced row the feed cached under
@@ -26,23 +27,6 @@ interface CommentRow {
26
27
  meta: string;
27
28
  }
28
29
 
29
- /** One rich-content block row for the mixed-row body template: a text
30
- * paragraph, a heading, or an inline image — exactly one variant renders per
31
- * row (per-$item hiddenWhen, the himi_menu mixed-row contract). Rendering the
32
- * ricos `richContent` this way keeps INLINE IMAGES (previously lost by the
33
- * plainContent flattening). */
34
- interface BlockRow {
35
- id: string;
36
- isText: boolean;
37
- notText: boolean;
38
- isHeading: boolean;
39
- notHeading: boolean;
40
- isImage: boolean;
41
- notImage: boolean;
42
- text: string;
43
- image: string;
44
- }
45
-
46
30
  interface S {
47
31
  status: "loading" | "ready" | "error";
48
32
  id: string;
@@ -54,12 +38,13 @@ interface S {
54
38
  /** "N likes · M comments" from post.metrics — collapses at zero/absent. */
55
39
  countsLine: string;
56
40
  excerpt: string;
57
- body: string;
58
- /** Rich-content block rows (paragraphs / headings / inline images). When
59
- * present the screen renders these INSTEAD of the flattened `body`. */
60
- blocks: BlockRow[];
61
- hasBlocks: boolean;
62
- noBlocks: boolean;
41
+ /** [rico] #3370 — the body as ONE Ricos document JSON string for RichContent: the encoder has
42
+ * sized its media and dropped disallowed links. "" when the post has none, or the encoder rejected it. */
43
+ bodyJson: string;
44
+ /** bodyJson !== "" — gates the plain text, because RichContent draws nothing for "". */
45
+ hasBodyJson: boolean;
46
+ /** Plain text of the body: RichContent's `fallback`, and what shows when there is no document. */
47
+ bodyText: string;
63
48
  /** Share payload — the post's public URL when the payload carries one. */
64
49
  shareUrl: string;
65
50
  cover: string;
@@ -101,55 +86,6 @@ type PostWithExtras = WixPost & {
101
86
  author?: { name?: string; nickname?: string } | string;
102
87
  };
103
88
 
104
- /** One ricos node's plain text (joins the leaf textData runs). */
105
- function ricosText(node: unknown): string {
106
- const n = node as { textData?: { text?: string }; nodes?: unknown[] } | undefined;
107
- const own = typeof n?.textData?.text === "string" ? n.textData.text : "";
108
- const nested = Array.isArray(n?.nodes) ? n.nodes.map(ricosText).join("") : "";
109
- return own + nested;
110
- }
111
-
112
- /** A ricos IMAGE node's URL — direct `src.url` or a wixstatic media id. */
113
- function ricosImageUrl(node: unknown): string {
114
- const src = (node as { imageData?: { image?: { src?: { url?: string; id?: string } } } } | undefined)
115
- ?.imageData?.image?.src;
116
- return wixMediaUrl(src?.url ?? src?.id, WIX_DETAIL_IMAGE);
117
- }
118
-
119
- /** ricos richContent.nodes → mixed block rows (paragraphs, headings, inline
120
- * images). Anything unrecognized flattens to its text (or is dropped). */
121
- export function richContentBlocks(richContent: unknown): BlockRow[] {
122
- const nodes = (richContent as { nodes?: unknown[] } | undefined)?.nodes;
123
- if (!Array.isArray(nodes)) return [];
124
- const rows: BlockRow[] = [];
125
- const push = (kind: "text" | "heading" | "image", text: string, image: string) => {
126
- rows.push({
127
- id: `block_${rows.length}`,
128
- isText: kind === "text",
129
- notText: kind !== "text",
130
- isHeading: kind === "heading",
131
- notHeading: kind !== "heading",
132
- isImage: kind === "image",
133
- notImage: kind !== "image",
134
- text,
135
- image,
136
- });
137
- };
138
- for (const node of nodes) {
139
- const type = String((node as { type?: unknown } | undefined)?.type ?? "").toUpperCase();
140
- if (type === "IMAGE") {
141
- const url = ricosImageUrl(node);
142
- if (url) push("image", "", url);
143
- continue;
144
- }
145
- const text = ricosText(node).trim();
146
- if (!text) continue;
147
- if (type.startsWith("HEADING")) push("heading", text, "");
148
- else push("text", text, "");
149
- }
150
- return rows;
151
- }
152
-
153
89
  /** "By <name>" from the payload's writer-ish fields ("" when absent). */
154
90
  function bylineOf(post: PostWithExtras): string {
155
91
  const author = typeof post.author === "string" ? post.author : undefined;
@@ -260,8 +196,7 @@ function derive(s: S): S {
260
196
  noPost: !hasPost,
261
197
  hasCover: Boolean(s.cover),
262
198
  noCover: !s.cover,
263
- hasBlocks: s.blocks.length > 0,
264
- noBlocks: s.blocks.length === 0,
199
+ hasBodyJson: s.bodyJson !== "",
265
200
  notLiked: !s.liked,
266
201
  notSaved: !s.saved,
267
202
  commentsAvailable: !s.commentsUnavailable,
@@ -276,10 +211,9 @@ const baseState: S = derive({
276
211
  byline: "",
277
212
  countsLine: "",
278
213
  excerpt: "",
279
- body: "",
280
- blocks: [],
281
- hasBlocks: false,
282
- noBlocks: true,
214
+ bodyJson: "",
215
+ hasBodyJson: false,
216
+ bodyText: "",
283
217
  shareUrl: "",
284
218
  cover: "",
285
219
  hasCover: false,
@@ -321,7 +255,9 @@ async function cachedPostState(s: S, id: string): Promise<S> {
321
255
  title,
322
256
  subtitle: String(cached.meta ?? ""),
323
257
  excerpt: String(cached.excerpt ?? "").trim(),
324
- body: String(cached.body ?? cached.excerpt ?? "This post has no readable body yet."),
258
+ // The feed caches no Ricos body, so the offline copy is plain text (the descriptor's gated text shows it).
259
+ bodyJson: "",
260
+ bodyText: String(cached.body ?? cached.excerpt ?? "This post has no readable body yet."),
325
261
  cover: String(cached.cover ?? ""),
326
262
  initial: String(cached.initial ?? "") || (title[0] || "#").toUpperCase(),
327
263
  liked: false,
@@ -358,8 +294,8 @@ async function loadPostState(s: S): Promise<S> {
358
294
  byline: bylineOf(post),
359
295
  countsLine: countsLineOf(post),
360
296
  excerpt: (post.excerpt || "").trim(),
361
- body: bodyText(post),
362
- blocks: richContentBlocks(post.richContent),
297
+ // No Ricos body, or one the encoder rejects: bodyJson "" and the gated plain text shows bodyText.
298
+ ...richBody(post.richContent, bodyText(post)),
363
299
  shareUrl: postShareUrl(post),
364
300
  cover,
365
301
  initial: (title[0] || "#").toUpperCase(),
@@ -387,6 +323,8 @@ const svc = defineService<S, {
387
323
  share: (s: S) => Promise<S>;
388
324
  setComment: (s: S, arg?: unknown) => S;
389
325
  submitComment: (s: S) => Promise<S>;
326
+ /** [rico] #3370 — RichContent's on_link: `{ url }` of the tapped link. */
327
+ openLink: (s: S, arg?: unknown) => Promise<S>;
390
328
  }>("himiBlogPost", {
391
329
  state: baseState,
392
330
  actions: {
@@ -452,6 +390,12 @@ const svc = defineService<S, {
452
390
  });
453
391
  }
454
392
  },
393
+ // Only http(s), mailto and tel links reach open_url; a refused link, or one nothing opened, says so, and
394
+ // a link that opens clears that message rather than leaving an earlier failure on screen.
395
+ openLink: async (s, arg) => derive({
396
+ ...s,
397
+ actionMessage: (await openRichLink(arg)) ? "" : copyParam("blogPost.linkOpenFailed", "This link could not be opened."),
398
+ }),
455
399
  },
456
400
  });
457
401
 
@@ -78410,6 +78410,48 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
78410
78410
  }
78411
78411
  }
78412
78412
  },
78413
+ {
78414
+ "id": "post_body",
78415
+ "body": {
78416
+ "package": "himalaya",
78417
+ "type": "RichContent",
78418
+ "props": {
78419
+ "documentJsonPath": "$service:himiOwnerLitePost.bodyJson",
78420
+ "onLink": {
78421
+ "actionId": "tier5.himiOwnerLitePost.openLink"
78422
+ },
78423
+ "fallback": {
78424
+ "id": "",
78425
+ "body": {
78426
+ "package": "himalaya",
78427
+ "type": "Text",
78428
+ "props": {
78429
+ "text": "$service:himiOwnerLitePost.bodyText",
78430
+ "style": "body",
78431
+ "color": "textPrimary",
78432
+ "collapseWhenEmpty": true
78433
+ }
78434
+ }
78435
+ }
78436
+ }
78437
+ }
78438
+ },
78439
+ {
78440
+ "id": "post_body_text",
78441
+ "body": {
78442
+ "package": "himalaya",
78443
+ "type": "Text",
78444
+ "props": {
78445
+ "text": "$service:himiOwnerLitePost.bodyText",
78446
+ "style": "body",
78447
+ "color": "textPrimary",
78448
+ "collapseWhenEmpty": true
78449
+ }
78450
+ },
78451
+ "modifiers": {
78452
+ "hiddenPath": "$service:himiOwnerLitePost.hasBodyJson"
78453
+ }
78454
+ },
78413
78455
  {
78414
78456
  "id": "post_info_card",
78415
78457
  "body": {
@@ -78783,7 +78825,8 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
78783
78825
  "contentType": "application/javascript+ts-bundle",
78784
78826
  "declaredCapabilities": [
78785
78827
  "wix_api",
78786
- "local_database"
78828
+ "local_database",
78829
+ "open_url"
78787
78830
  ],
78788
78831
  "minBinaryVersion": "0.1.0",
78789
78832
  "bundleVersion": "0.0.1"
@@ -0,0 +1,28 @@
1
+ // [rico] #3370 — an entity's Ricos body, prepared for a RichContent component and its plain-text fallback.
2
+ // One rule for every worker that renders a body (the blog post and the owner post detail today; #3372's
3
+ // verticals next), so they cannot drift apart.
4
+ // Inside the platform source this import is a relative path, so node test runs resolve it; an app's copy
5
+ // imports "@himalaya/wix-rich-content".
6
+ import { encodeRichContent, parseRicosJson, ricosToPlainText } from "@himalaya/wix-rich-content";
7
+
8
+ /** What a body-rendering worker publishes: `<name>Json` for `documentJsonPath`, and the plain text. */
9
+ export interface RichBody {
10
+ /** The document as ONE JSON string (media sized, disallowed links dropped); "" when there is no document
11
+ * or the encoder rejected it. RichContent draws nothing for "", so a screen gates its plain text on it. */
12
+ bodyJson: string;
13
+ /** The plain text: the document's own text when it has any, else `fallback`. */
14
+ bodyText: string;
15
+ }
16
+
17
+ /** Prepares `richContent` (a Ricos document, as a Wix API returns it, or its JSON string). No document, or one
18
+ * that is not a document, gives `{ bodyJson: "", bodyText: fallback }`. A document past the viewer's limits
19
+ * gives bodyJson "" and its plain text — except one rejected as too deep, which is not walked again (the
20
+ * plain-text walk has no depth limit), so `fallback` stands in for it. */
21
+ export function richBody(richContent: unknown, fallback = ""): RichBody {
22
+ const doc = parseRicosJson(richContent);
23
+ if (!doc) return { bodyJson: "", bodyText: fallback };
24
+ // 1400 px — the detail-image budget, which is also the encoder's maximum.
25
+ const { json, rejected } = encodeRichContent(doc, { imageWidth: 1400 });
26
+ const text = rejected === "too_deep" ? "" : ricosToPlainText(doc);
27
+ return { bodyJson: json, bodyText: text || fallback };
28
+ }