@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.
- package/dist/cli.mjs +0 -3
- package/fixtures/wix-canned.json +146 -0
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +22 -79
- package/templates/beauty-salon/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/beauty-salon/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/beauty-salon/tier5-src/himi-blog-post/index.ts +26 -82
- package/templates/beauty-salon-owner/dev/index.ts +44 -1
- package/templates/beauty-salon-owner/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-post/index.ts +49 -2
- package/templates/branded-lite/dev/index.ts +22 -79
- package/templates/branded-lite/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/branded-lite/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/branded-lite/tier5-src/himi-blog-post/index.ts +26 -82
- package/templates/community-club/dev/index.ts +22 -79
- package/templates/community-club/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/community-club/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/community-club/tier5-src/himi-blog-post/index.ts +26 -82
- package/templates/community-club-owner/dev/index.ts +44 -1
- package/templates/community-club-owner/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/community-club-owner/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-post/index.ts +49 -2
- package/templates/events-meetups/dev/index.ts +22 -79
- package/templates/events-meetups/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/events-meetups/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/events-meetups/tier5-src/himi-blog-post/index.ts +26 -82
- package/templates/events-meetups-owner/dev/index.ts +44 -1
- package/templates/events-meetups-owner/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/events-meetups-owner/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-post/index.ts +49 -2
- package/templates/gym-fitness/dev/index.ts +22 -79
- package/templates/gym-fitness/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/gym-fitness/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/gym-fitness/tier5-src/himi-blog-post/index.ts +26 -82
- package/templates/gym-fitness-owner/dev/index.ts +44 -1
- package/templates/gym-fitness-owner/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-post/index.ts +49 -2
- package/templates/nonprofit-cause/dev/index.ts +22 -79
- package/templates/nonprofit-cause/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/nonprofit-cause/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/nonprofit-cause/tier5-src/himi-blog-post/index.ts +26 -82
- package/templates/nonprofit-cause-owner/dev/index.ts +44 -1
- package/templates/nonprofit-cause-owner/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-post/index.ts +49 -2
- package/templates/owner-lite/dev/index.ts +44 -1
- package/templates/owner-lite/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/owner-lite/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-post/index.ts +49 -2
- package/templates/podcast-creator/dev/index.ts +22 -79
- package/templates/podcast-creator/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/podcast-creator/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/podcast-creator/tier5-src/himi-blog-post/index.ts +26 -82
- package/templates/podcast-creator-owner/dev/index.ts +44 -1
- package/templates/podcast-creator-owner/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-post/index.ts +49 -2
- package/templates/portfolio-creative/dev/index.ts +22 -79
- package/templates/portfolio-creative/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/portfolio-creative/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/portfolio-creative/tier5-src/himi-blog-post/index.ts +26 -82
- package/templates/portfolio-creative-owner/dev/index.ts +44 -1
- package/templates/portfolio-creative-owner/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-post/index.ts +49 -2
- package/templates/restaurant/dev/index.ts +22 -79
- package/templates/restaurant/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/restaurant/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/restaurant/tier5-src/himi-blog-post/index.ts +26 -82
- package/templates/restaurant-owner/dev/index.ts +44 -1
- package/templates/restaurant-owner/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/restaurant-owner/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-post/index.ts +49 -2
- package/templates/retail-store/dev/index.ts +22 -79
- package/templates/retail-store/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/retail-store/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/retail-store/tier5-src/himi-blog-post/index.ts +26 -82
- package/templates/retail-store-owner/dev/index.ts +44 -1
- package/templates/retail-store-owner/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/retail-store-owner/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-post/index.ts +49 -2
- package/templates/yoga-studio/dev/index.ts +22 -79
- package/templates/yoga-studio/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/yoga-studio/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/yoga-studio/tier5-src/himi-blog-post/index.ts +26 -82
- package/templates/yoga-studio-owner/dev/index.ts +44 -1
- package/templates/yoga-studio-owner/tier5-src/_shared/rich-body.ts +28 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-post/index.ts +49 -2
|
@@ -19032,103 +19032,45 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
19032
19032
|
}
|
|
19033
19033
|
},
|
|
19034
19034
|
{
|
|
19035
|
-
"id": "
|
|
19035
|
+
"id": "post_body",
|
|
19036
19036
|
"body": {
|
|
19037
19037
|
"package": "himalaya",
|
|
19038
|
-
"type": "
|
|
19038
|
+
"type": "RichContent",
|
|
19039
19039
|
"props": {
|
|
19040
|
-
"
|
|
19041
|
-
"
|
|
19042
|
-
|
|
19043
|
-
|
|
19044
|
-
|
|
19045
|
-
|
|
19046
|
-
|
|
19047
|
-
|
|
19048
|
-
|
|
19049
|
-
|
|
19050
|
-
|
|
19051
|
-
|
|
19052
|
-
|
|
19053
|
-
|
|
19054
|
-
"id": "himi_blog_post.block_heading",
|
|
19055
|
-
"body": {
|
|
19056
|
-
"package": "himalaya",
|
|
19057
|
-
"type": "Text",
|
|
19058
|
-
"props": {
|
|
19059
|
-
"text": "$item.text",
|
|
19060
|
-
"style": "title3",
|
|
19061
|
-
"collapseWhenEmpty": true
|
|
19062
|
-
}
|
|
19063
|
-
},
|
|
19064
|
-
"modifiers": {
|
|
19065
|
-
"padding": {
|
|
19066
|
-
"top": 8,
|
|
19067
|
-
"bottom": 2
|
|
19068
|
-
},
|
|
19069
|
-
"hiddenPath": "$item.notHeading"
|
|
19070
|
-
}
|
|
19071
|
-
},
|
|
19072
|
-
{
|
|
19073
|
-
"id": "",
|
|
19074
|
-
"body": {
|
|
19075
|
-
"package": "himalaya",
|
|
19076
|
-
"type": "Text",
|
|
19077
|
-
"props": {
|
|
19078
|
-
"text": "$item.text",
|
|
19079
|
-
"style": "body",
|
|
19080
|
-
"color": "textPrimary",
|
|
19081
|
-
"collapseWhenEmpty": true
|
|
19082
|
-
}
|
|
19083
|
-
},
|
|
19084
|
-
"modifiers": {
|
|
19085
|
-
"hiddenPath": "$item.notText"
|
|
19086
|
-
}
|
|
19087
|
-
},
|
|
19088
|
-
{
|
|
19089
|
-
"id": "",
|
|
19090
|
-
"body": {
|
|
19091
|
-
"package": "himalaya",
|
|
19092
|
-
"type": "Image",
|
|
19093
|
-
"props": {
|
|
19094
|
-
"url": "$item.image",
|
|
19095
|
-
"fillWidth": true,
|
|
19096
|
-
"height": 220,
|
|
19097
|
-
"contentMode": "fill"
|
|
19098
|
-
}
|
|
19099
|
-
},
|
|
19100
|
-
"modifiers": {
|
|
19101
|
-
"accessibilityRole": "decorative",
|
|
19102
|
-
"cornerRadius": 8,
|
|
19103
|
-
"fillMaxWidth": true,
|
|
19104
|
-
"hiddenPath": "$item.notImage"
|
|
19105
|
-
}
|
|
19106
|
-
}
|
|
19107
|
-
]
|
|
19108
|
-
}
|
|
19040
|
+
"documentJsonPath": "$service:himiBlogPost.bodyJson",
|
|
19041
|
+
"onLink": {
|
|
19042
|
+
"actionId": "tier5.himiBlogPost.openLink"
|
|
19043
|
+
},
|
|
19044
|
+
"fallback": {
|
|
19045
|
+
"id": "",
|
|
19046
|
+
"body": {
|
|
19047
|
+
"package": "himalaya",
|
|
19048
|
+
"type": "Text",
|
|
19049
|
+
"props": {
|
|
19050
|
+
"text": "$service:himiBlogPost.bodyText",
|
|
19051
|
+
"style": "body",
|
|
19052
|
+
"color": "textPrimary",
|
|
19053
|
+
"collapseWhenEmpty": true
|
|
19109
19054
|
}
|
|
19110
19055
|
}
|
|
19111
|
-
|
|
19056
|
+
}
|
|
19112
19057
|
}
|
|
19113
|
-
},
|
|
19114
|
-
"modifiers": {
|
|
19115
|
-
"hiddenPath": "$service:himiBlogPost.noBlocks"
|
|
19116
19058
|
}
|
|
19117
19059
|
},
|
|
19118
19060
|
{
|
|
19119
|
-
"id": "",
|
|
19061
|
+
"id": "post_body_text",
|
|
19120
19062
|
"body": {
|
|
19121
19063
|
"package": "himalaya",
|
|
19122
19064
|
"type": "Text",
|
|
19123
19065
|
"props": {
|
|
19124
|
-
"text": "$service:himiBlogPost.
|
|
19066
|
+
"text": "$service:himiBlogPost.bodyText",
|
|
19125
19067
|
"style": "body",
|
|
19126
19068
|
"color": "textPrimary",
|
|
19127
19069
|
"collapseWhenEmpty": true
|
|
19128
19070
|
}
|
|
19129
19071
|
},
|
|
19130
19072
|
"modifiers": {
|
|
19131
|
-
"hiddenPath": "$service:himiBlogPost.
|
|
19073
|
+
"hiddenPath": "$service:himiBlogPost.hasBodyJson"
|
|
19132
19074
|
}
|
|
19133
19075
|
},
|
|
19134
19076
|
{
|
|
@@ -19529,7 +19471,8 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
19529
19471
|
"declaredCapabilities": [
|
|
19530
19472
|
"wix_api",
|
|
19531
19473
|
"share",
|
|
19532
|
-
"local_database"
|
|
19474
|
+
"local_database",
|
|
19475
|
+
"open_url"
|
|
19533
19476
|
],
|
|
19534
19477
|
"minBinaryVersion": "0.1.0",
|
|
19535
19478
|
"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:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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
|
|
362
|
-
|
|
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
|
|
|
@@ -77444,6 +77444,48 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
77444
77444
|
}
|
|
77445
77445
|
}
|
|
77446
77446
|
},
|
|
77447
|
+
{
|
|
77448
|
+
"id": "post_body",
|
|
77449
|
+
"body": {
|
|
77450
|
+
"package": "himalaya",
|
|
77451
|
+
"type": "RichContent",
|
|
77452
|
+
"props": {
|
|
77453
|
+
"documentJsonPath": "$service:himiOwnerLitePost.bodyJson",
|
|
77454
|
+
"onLink": {
|
|
77455
|
+
"actionId": "tier5.himiOwnerLitePost.openLink"
|
|
77456
|
+
},
|
|
77457
|
+
"fallback": {
|
|
77458
|
+
"id": "",
|
|
77459
|
+
"body": {
|
|
77460
|
+
"package": "himalaya",
|
|
77461
|
+
"type": "Text",
|
|
77462
|
+
"props": {
|
|
77463
|
+
"text": "$service:himiOwnerLitePost.bodyText",
|
|
77464
|
+
"style": "body",
|
|
77465
|
+
"color": "textPrimary",
|
|
77466
|
+
"collapseWhenEmpty": true
|
|
77467
|
+
}
|
|
77468
|
+
}
|
|
77469
|
+
}
|
|
77470
|
+
}
|
|
77471
|
+
}
|
|
77472
|
+
},
|
|
77473
|
+
{
|
|
77474
|
+
"id": "post_body_text",
|
|
77475
|
+
"body": {
|
|
77476
|
+
"package": "himalaya",
|
|
77477
|
+
"type": "Text",
|
|
77478
|
+
"props": {
|
|
77479
|
+
"text": "$service:himiOwnerLitePost.bodyText",
|
|
77480
|
+
"style": "body",
|
|
77481
|
+
"color": "textPrimary",
|
|
77482
|
+
"collapseWhenEmpty": true
|
|
77483
|
+
}
|
|
77484
|
+
},
|
|
77485
|
+
"modifiers": {
|
|
77486
|
+
"hiddenPath": "$service:himiOwnerLitePost.hasBodyJson"
|
|
77487
|
+
}
|
|
77488
|
+
},
|
|
77447
77489
|
{
|
|
77448
77490
|
"id": "post_info_card",
|
|
77449
77491
|
"body": {
|
|
@@ -77818,7 +77860,8 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
77818
77860
|
"contentType": "application/javascript+ts-bundle",
|
|
77819
77861
|
"declaredCapabilities": [
|
|
77820
77862
|
"wix_api",
|
|
77821
|
-
"local_database"
|
|
77863
|
+
"local_database",
|
|
77864
|
+
"open_url"
|
|
77822
77865
|
],
|
|
77823
77866
|
"minBinaryVersion": "0.1.0",
|
|
77824
77867
|
"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
|
+
}
|
|
@@ -7,6 +7,9 @@ import { postCoverUrl } from "../_shared/owner-post-content.js";
|
|
|
7
7
|
|
|
8
8
|
import { defineService } from "@himalaya/state";
|
|
9
9
|
import { getPost, getDraftPost, publishDraftPost, revertDraftPost, deleteDraftPost } from "@himalaya/wix-blog";
|
|
10
|
+
import { parseRicosJson } from "@himalaya/wix-rich-content";
|
|
11
|
+
import { openRichLink } from "../_shared/rich-content-link.js";
|
|
12
|
+
import { richBody } from "../_shared/rich-body.js";
|
|
10
13
|
import { ownerLoadError, isBridgeUnavailable, isOwnerPreviewDemo, shouldQueueOwnerWrite } from "../_shared/owner-load-error.js";
|
|
11
14
|
import { DEMO_POSTS, DEMO_NOTE, DEMO_SAVE_NOTE } from "../_shared/owner-preview-demo.js";
|
|
12
15
|
import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
@@ -25,6 +28,14 @@ async function cachePut(row: DbEntity): Promise<void> { try { await dbUpsert(ENT
|
|
|
25
28
|
async function cacheDrop(id: string): Promise<void> { try { await dbRemove(ENTITY, id); } catch { /* capability not granted */ } }
|
|
26
29
|
async function cacheGet(id: string): Promise<DbEntity | null> { try { return await dbGet(ENTITY, id); } catch { return null; } }
|
|
27
30
|
async function queueWrite(actionId: string, params: Record<string, string>): Promise<void> { try { await dbEnqueue(actionId, params); } catch { /* capability not granted */ } }
|
|
31
|
+
/** [rico] #3370 — keep the body's plain text with the post's cached row, so the offline detail shows it. It is
|
|
32
|
+
* merged into the row the posts list wrote rather than replacing it; a later list re-sync writes its own rows
|
|
33
|
+
* and drops it again, until the detail is next opened online. */
|
|
34
|
+
async function cacheBodyText(s: PostState): Promise<void> {
|
|
35
|
+
if (s.status !== "ready" || !s.id) return;
|
|
36
|
+
const prev = await cacheGet(s.id);
|
|
37
|
+
await cachePut(prev ? { ...prev, bodyText: s.bodyText } : cacheRow(s));
|
|
38
|
+
}
|
|
28
39
|
|
|
29
40
|
interface PostState {
|
|
30
41
|
id: string;
|
|
@@ -38,6 +49,14 @@ interface PostState {
|
|
|
38
49
|
summary: string;
|
|
39
50
|
initial: string;
|
|
40
51
|
image: string;
|
|
52
|
+
// Body ([rico] #3370) — RichContent binds bodyJson; the plain text shows when there is no document.
|
|
53
|
+
/** The body as ONE Ricos document JSON string for RichContent, from the draft when there is one; "" when the post
|
|
54
|
+
* has none, the encoder rejected it, or this is the offline copy. */
|
|
55
|
+
bodyJson: string;
|
|
56
|
+
/** bodyJson !== "" — gates the plain text, because RichContent draws nothing for "". */
|
|
57
|
+
hasBodyJson: boolean;
|
|
58
|
+
/** Plain text of the body: RichContent's `fallback`, and the offline copy's body. */
|
|
59
|
+
bodyText: string;
|
|
41
60
|
// Info rows
|
|
42
61
|
postStatus: string;
|
|
43
62
|
category: string;
|
|
@@ -118,6 +137,9 @@ function shape(s: Partial<PostState>): PostState {
|
|
|
118
137
|
summary: s.summary ?? "",
|
|
119
138
|
initial: s.initial ?? "?",
|
|
120
139
|
image,
|
|
140
|
+
bodyJson: s.bodyJson ?? "",
|
|
141
|
+
hasBodyJson: !!s.bodyJson,
|
|
142
|
+
bodyText: s.bodyText ?? "",
|
|
121
143
|
postStatus: s.postStatus ?? statusLabel(raw),
|
|
122
144
|
category: s.category ?? "—",
|
|
123
145
|
published: s.published ?? "—",
|
|
@@ -153,6 +175,8 @@ function demoDetail(s: PostState, id: string): PostState | null {
|
|
|
153
175
|
summary: d.excerpt,
|
|
154
176
|
initial: (d.title[0] || "?").toUpperCase(),
|
|
155
177
|
image: "",
|
|
178
|
+
bodyJson: "",
|
|
179
|
+
bodyText: d.body,
|
|
156
180
|
postStatus: d.postStatus,
|
|
157
181
|
category: d.category,
|
|
158
182
|
published: d.published,
|
|
@@ -173,6 +197,8 @@ function cacheRow(s: PostState): DbEntity {
|
|
|
173
197
|
summary: s.summary,
|
|
174
198
|
initial: s.initial,
|
|
175
199
|
image: s.image,
|
|
200
|
+
// The plain text only: the offline copy shows it, and a stale document is never drawn from cache.
|
|
201
|
+
bodyText: s.bodyText,
|
|
176
202
|
postStatus: s.postStatus,
|
|
177
203
|
category: s.category,
|
|
178
204
|
published: s.published,
|
|
@@ -194,6 +220,10 @@ function cachedDetail(s: PostState, row: DbEntity): PostState {
|
|
|
194
220
|
summary: String(row.summary ?? row.excerpt ?? ""),
|
|
195
221
|
initial: String(row.initial ?? (title[0] || "?")).slice(0, 1).toUpperCase(),
|
|
196
222
|
image: String(row.image ?? ""),
|
|
223
|
+
bodyJson: "",
|
|
224
|
+
// The post form upserts a whole row with `body` and no `bodyText`; cacheBodyText later merges `bodyText` into it.
|
|
225
|
+
// So a present `bodyText` is the newer value, and an explicit "" (a body cleared online) is kept.
|
|
226
|
+
bodyText: String(row.bodyText ?? row.body ?? ""),
|
|
197
227
|
postStatus: row.postStatus != null ? String(row.postStatus) : statusLabel(raw),
|
|
198
228
|
category: String(row.category ?? "—"),
|
|
199
229
|
published: String(row.published ?? "—"),
|
|
@@ -221,6 +251,10 @@ async function refresh(prev: PostState, id: string, actionMessage = ""): Promise
|
|
|
221
251
|
const slug = draft?.seoSlug || post?.slug || "";
|
|
222
252
|
const draftHidesCover = (draft?.media ?? draft?.coverMedia)?.displayed === false;
|
|
223
253
|
const image = draftHidesCover ? "" : coverUrl(draft) || coverUrl(post);
|
|
254
|
+
// The draft carries the owner's latest edits, so its body wins over the published one. With no usable
|
|
255
|
+
// document, the published post's own plain text (CONTENT_TEXT) is the body.
|
|
256
|
+
const body = richBody(parseRicosJson(draft?.richContent) ? draft?.richContent : post?.richContent,
|
|
257
|
+
typeof post?.contentText === "string" ? post.contentText.trim() : "");
|
|
224
258
|
return shape({
|
|
225
259
|
...prev,
|
|
226
260
|
id,
|
|
@@ -230,6 +264,7 @@ async function refresh(prev: PostState, id: string, actionMessage = ""): Promise
|
|
|
230
264
|
summary: (draft?.excerpt || post?.excerpt || "").trim(),
|
|
231
265
|
initial: (title[0] || "?").toUpperCase(),
|
|
232
266
|
image,
|
|
267
|
+
...body,
|
|
233
268
|
postStatus: statusLabel(raw),
|
|
234
269
|
category: "—",
|
|
235
270
|
published: fmtDate(post?.firstPublishedDate),
|
|
@@ -247,6 +282,8 @@ const svc = defineService<PostState, {
|
|
|
247
282
|
publish: (s: PostState) => Promise<PostState>;
|
|
248
283
|
unpublish: (s: PostState) => Promise<PostState>;
|
|
249
284
|
delete: (s: PostState) => Promise<PostState>;
|
|
285
|
+
/** [rico] #3370 — RichContent's on_link: `{ url }` of the tapped link. */
|
|
286
|
+
openLink: (s: PostState, arg?: unknown) => Promise<PostState>;
|
|
250
287
|
}>("himiOwnerLitePost", {
|
|
251
288
|
state: shape({ status: "loading" }),
|
|
252
289
|
actions: {
|
|
@@ -261,7 +298,9 @@ const svc = defineService<PostState, {
|
|
|
261
298
|
return shape({ ...s, status: "error", error: "Sample item not found." });
|
|
262
299
|
}
|
|
263
300
|
try {
|
|
264
|
-
|
|
301
|
+
const loaded = await refresh(shape({ ...s, id, status: "loading" }), id);
|
|
302
|
+
await cacheBodyText(loaded);
|
|
303
|
+
return loaded;
|
|
265
304
|
} catch (e) {
|
|
266
305
|
// Offline: fall back to the last-synced local row before the demo/error paths.
|
|
267
306
|
const cached = await cacheGet(id);
|
|
@@ -282,7 +321,9 @@ const svc = defineService<PostState, {
|
|
|
282
321
|
return shape({ ...s, status: "error", error: "Sample item not found." });
|
|
283
322
|
}
|
|
284
323
|
try {
|
|
285
|
-
|
|
324
|
+
const loaded = await refresh(shape({ ...s, status: "loading" }), s.id);
|
|
325
|
+
await cacheBodyText(loaded);
|
|
326
|
+
return loaded;
|
|
286
327
|
} catch (e) {
|
|
287
328
|
const cached = await cacheGet(s.id);
|
|
288
329
|
if (cached) return cachedDetail(s, cached);
|
|
@@ -347,6 +388,12 @@ const svc = defineService<PostState, {
|
|
|
347
388
|
return shape({ ...s, actionMessage: ownerLoadError(e) });
|
|
348
389
|
}
|
|
349
390
|
},
|
|
391
|
+
// Only http(s), mailto and tel links reach open_url; a refused link, or one nothing opened, says so, and
|
|
392
|
+
// a link that opens clears that message rather than leaving an earlier failure on screen.
|
|
393
|
+
openLink: async (s, arg) => shape({
|
|
394
|
+
...s,
|
|
395
|
+
actionMessage: (await openRichLink(arg)) ? "" : copyParam("ownerPost.linkOpenFailed", "This link could not be opened."),
|
|
396
|
+
}),
|
|
350
397
|
},
|
|
351
398
|
});
|
|
352
399
|
|