1{2 "solution": "shared",3 "requires": [],4 "entrypoints": [5 "js/wix/client.js",6 "js/wix/media.js",7 "js/wix/rich-content.js"8 ],9 "files": {10 "js/wix/client.js": "// Declarations of client.ts: what this file exports, their arguments and what they return.\n// export declare const WIX_API_BASE = \"https://www.wixapis.com\";
11 "js/wix/media.js": "// Declarations of media.ts: what this file exports, their arguments and what they return.\n// export type MediaLike = string | null | undefined | {\n// image?: unknown;\n// url?: string | null;\n// id?: string | null;\n// };\n// export declare function imgSrc(value: unknown, width?: number, height?: number): string;\n// export declare function imgSrcSet(value: unknown, widths?: number[], ratio?: number): string;\n// export declare function imgAttrs(value: unknown, sizes: string, ratio?: number): Record<string, string>;\n// export declare function imgRatio(value: unknown): number | null;\n// export declare function mediaKey(value: unknown): string;\n// Wix media → browser-loadable URLs — the twin of app/wix/media.ts, without the SDK.\n//\n// A media value arrives as an absolute https://static.wixstatic.com/... URL, a raw\n// `wix:image://v1/<file>/<name>#originWidth=W&originHeight=H` identifier a browser cannot load\n// (ERR_UNKNOWN_URL_SCHEME), or, over REST, an object whose `url` (and `id`) is the BARE file id\n// `e6a89e_…~mv2.png` (Services V2 media, verified live) which a browser resolves as a relative path\n// and 404s. Both raw forms are scaled through this ONE URL shape; any other hand-built form 403s:\n// https://static.wixstatic.com/media/<file>/v1/fill/w_<W>,h_<H>,al_c,q_90/<file>\nconst STATIC = \"https://static.wixstatic.com/media\";\nconst FILL = /\\/v1\\/fill\\/w_\\d+,h_\\d+/;\n/** A bare Wix media file id: no scheme, no path, an extension — `e6a89e_06cf…~mv2.png`, `11062b_27ca…~mv2.jpg`. */\nconst BARE_FILE_ID = /^[\\w-]+~mv2\\.[a-z0-9]+$/i;\nfunction rawOf(value) {\n if (!value)\n return \"\";\n if (typeof value === \"string\")\n return value;\n const o = value;\n if (o.image != null && typeof o.image === \"object\")\n return rawOf(o.image); // { image: { id, url } }\n return (typeof o.image === \"string\" ? o.image : null) ?? o.url ?? o.id ?? \"\";\n}\n/** The file id behind any raw form (a wix:image id, a bare file id, a static.wixstatic.com URL); \"\" otherwise. */\nfunction fileOf(v) {\n if (v.startsWith(\"wix:image://\"))\n return v.slice(\"wix:image://v1/\".length).split(\"/\")[0].split(\"#\")[0];\n if (BARE_FILE_ID.test(v))\n return v;\n const m = v.match(/^https:\\/\\/static\\.wixstatic\\.com\\/media\\/([^/?#]+)/);\n return m ? m[1] : \"\";\n}\n/** Resolve any Wix media value to a URL sized w×h (\"\" when absent). */\nexport function imgSrc(value, width = 600, height = 600) {\n const v = rawOf(value);\n if (!v)\n return \"\";\n // A wix:image id, a bare file id, or an absolute Wix media URL (bare, or already scaled to some\n // other size): every one re-issued through the scaler at the requested size, one shape.\n const file = fileOf(v);\n if (file)\n return `${STATIC}/${file}/v1/fill/w_${width},h_${height},al_c,q_90/${file}`;\n return v;\n}\n/** srcset candidates at several widths; `ratio` is height/width. \"\" only when there is no image. */\nexport function imgSrcSet(value, widths = [320, 480, 640, 960], ratio = 1) {\n const v = rawOf(value);\n if (!v)\n return \"\";\n if (v.startsWith(\"wix:image://\") || BARE_FILE_ID.test(v))\n return widths.map((w) => `${imgSrc(value, w, Math.round(w * ratio))} ${w}w`).join(\", \");\n if (v.includes(\"static.wixstatic.com/\") && FILL.test(v)) {\n return widths.map((w) => `${v.replace(FILL, `/v1/fill/w_${w},h_${Math.round(w * ratio)}`)} ${w}w`).join(\", \");\n }\n return v;\n}\n/**\n * Everything an <img> needs for one Wix image, so a tile can never ship `srcset` without `src`.\n * Attribute names as HTML spells them — spread into a template or apply with setAttribute:\n * `<img ${attrs(imgAttrs(p.imageUrl, \"(min-width: 1024px) 25vw, 50vw\"))} alt=\"…\">`\n * `sizes` is the width the image renders at; `ratio` is height/width. {} when there is no image.\n */\nexport function imgAttrs(value, sizes, ratio = 1) {\n const src = imgSrc(value, 640, Math.round(640 * ratio));\n if (!src)\n return {};\n return { src, srcset: imgSrcSet(value, undefined, ratio), sizes, loading: \"lazy\", decoding: \"async\" };\n}\n/**\n * Height/width of a Wix image when the value carries its size: a raw\n * `wix:image://…#originWidth=W&originHeight=H` id (the natural size) or a resolved `/v1/fill/w_W,h_H/`\n * URL (the size it was scaled to). null when neither is present. Pass it as `ratio` to keep a\n * srcset at the image's own aspect instead of the square default:\n * imgAttrs(item.photo, \"33vw\", imgRatio(item.photo) ?? 1)\n */\nexport function imgRatio(value) {\n const v = rawOf(value);\n if (!v)\n return null;\n const size = v.startsWith(\"wix:image://\")\n ? [v.match(/[#&]originWidth=(\\d+)/)?.[1], v.match(/[#&]originHeight=(\\d+)/)?.[1]]\n : (v.match(/\\/v1\\/fill\\/w_(\\d+),h_(\\d+)/)?.slice(1) ?? []);\n const w = Number(size[0]);\n const h = Number(size[1]);\n return w > 0 && h > 0 ? h / w : null;\n}\n/** The media identity before scaling — de-duplicate galleries on this, never on a resolved URL. */\nexport function mediaKey(value) {\n const v = rawOf(value);\n if (!v)\n return \"\";\n // The file id, whichever form the value takes — a raw id and a resolved URL of one photo share it.\n if (v.startsWith(\"wix:image://\"))\n return v.slice(\"wix:image://v1/\".length).split(\"/\")[0].split(\"#\")[0];\n const m = v.match(/^https:\\/\\/static\\.wixstatic\\.com\\/media\\/([^/?#]+)/);\n return m ? m[1] : v;\n}\n",
12 "js/wix/rich-content.js": "// Declarations of rich-content.ts: what this file exports, their arguments and what they return.\n// type Raw = Record<string, any>;\n// export type ImgSrc = (value: any, width: number, height: number) => string;\n// export interface RichContentHtmlOptions {\n// headingLevel?: (level: number) => number;\n// imageWidth?: number;\n// imageHeight?: number;\n// }\n// export declare function escapeHtml(s: string): string;\n// export declare function imageValue(image: Raw | undefined): string;\n// export declare function richContentToHtml(rc: Raw | null | undefined, imgSrc: ImgSrc, options?: RichContentHtmlOptions): string;\n// export declare function richContentToText(rc: Raw | null | undefined): string;\n// Ricos rich content → sanitized HTML or plain text, with no framework. Copy as-is.\n//\n// A post body, an event description and an FAQ answer are Ricos documents ({ nodes: [...] }), not\n// HTML and not text. On a React stack the @wix/ricos viewer renders them; everywhere else (a static\n// page, a server template, an email) this does. Text is always escaped, attribute values are escaped,\n// links are kept only when http(s) or an in-page anchor, no inline styles, no event attributes;\n// `htmlData` is never emitted. A node type outside the table below degrades to its text.\nconst IMAGE_WIDTH = 1200;\nconst IMAGE_HEIGHT = 800;\nexport function escapeHtml(s) {\n return s.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\").replace(/'/g, \"'\");\n}\nconst isHttpUrl = (u) => typeof u === \"string\" && /^https?:\\/\\//i.test(u);\n/** Ricos Link → the anchor's attributes, or null when the URL is not http(s) (javascript:, data:, relative). */\nfunction linkAttrs(link) {\n if (!link)\n return null;\n if (isHttpUrl(link.url)) {\n const rel = [\"noopener\", \"noreferrer\"];\n if (link.rel?.nofollow)\n rel.push(\"nofollow\");\n if (link.rel?.sponsored)\n rel.push(\"sponsored\");\n if (link.rel?.ugc)\n rel.push(\"ugc\");\n const target = link.target === \"BLANK\" ? ` target=\"_blank\"` : \"\";\n return `href=\"${escapeHtml(link.url)}\" rel=\"${rel.join(\" \")}\"${target}`;\n }\n if (typeof link.anchor === \"string\" && link.anchor)\n return `href=\"#${escapeHtml(link.anchor)}\"`;\n return null;\n}\n/** One TEXT node → inline HTML: escaped text wrapped by its decorations (unknown decorations keep the text). */\nfunction textNodeToHtml(node) {\n let html = escapeHtml(String(node.textData?.text ?? \"\"));\n for (const d of (node.textData?.decorations ?? [])) {\n switch (d?.type) {\n case \"BOLD\":\n // The editor writes fontWeightValue 700 for bold and a lighter weight to un-bold inside a bold block.\n if (typeof d.fontWeightValue !== \"number\" || d.fontWeightValue >= 600)\n html = `<strong>${html}</strong>`;\n break;\n case \"ITALIC\":\n if (d.italicData !== false)\n html = `<em>${html}</em>`;\n break;\n case \"UNDERLINE\":\n html = `<u>${html}</u>`;\n break;\n case \"STRIKETHROUGH\":\n html = `<s>${html}</s>`;\n break;\n case \"SUPERSCRIPT\":\n html = `<sup>${html}</sup>`;\n break;\n case \"SUBSCRIPT\":\n html = `<sub>${html}</sub>`;\n break;\n case \"LINK\": {\n const attrs = linkAttrs(d.linkData?.link);\n if (attrs)\n html = `<a ${attrs}>${html}</a>`;\n break;\n }\n case \"ANCHOR\":\n if (typeof d.anchorData?.anchor === \"string\" && d.anchorData.anchor)\n html = `<a href=\"#${escapeHtml(d.anchorData.anchor)}\">${html}</a>`;\n break;\n default:\n break; // COLOR, FONT_SIZE, FONT_FAMILY, MENTION, SPOILER, SKETCH, EXTERNAL — text kept, styling dropped\n }\n }\n return html;\n}\n/**\n * The media value in the form imgSrc scales: `wix:image://v1/<id>/<id>#originWidth=W&originHeight=H`\n * (the Ricos FileSource has no filename; the id stands in — the scaler reads only the id). An absolute\n * https URL passes through; anything else is no image.\n */\nexport function imageValue(image) {\n const src = image?.src;\n if (!src)\n return \"\";\n if (typeof src.id === \"string\" && src.id) {\n const size = image?.width && image?.height ? `#originWidth=${image.width}&originHeight=${image.height}` : \"\";\n return `wix:image://v1/${src.id}/${src.id}${size}`;\n }\n return isHttpUrl(src.url) ? src.url : \"\";\n}\nfunction imgTag(image, alt, ctx) {\n const src = ctx.imgSrc(imageValue(image), ctx.width, ctx.height);\n if (!src)\n return \"\";\n return `<img src=\"${escapeHtml(src)}\" alt=\"${escapeHtml(String(alt ?? \"\"))}\" loading=\"lazy\" decoding=\"async\">`;\n}\nfunction childrenToHtml(node, ctx) {\n return (node.nodes ?? []).map((n) => nodeToHtml(n, ctx)).join(\"\");\n}\nfunction nodeToHtml(node, ctx) {\n switch (node?.type) {\n case \"TEXT\": return textNodeToHtml(node);\n case \"PARAGRAPH\": return `<p>${childrenToHtml(node, ctx)}</p>`;\n case \"HEADING\": {\n const level = Math.min(6, Math.max(1, Math.round(ctx.headingLevel(Number(node.headingData?.level) || 2))));\n return `<h${level}>${childrenToHtml(node, ctx)}</h${level}>`;\n }\n case \"BULLETED_LIST\": return `<ul>${childrenToHtml(node, ctx)}</ul>`;\n case \"ORDERED_LIST\": return `<ol>${childrenToHtml(node, ctx)}</ol>`;\n case \"LIST_ITEM\": return `<li>${childrenToHtml(node, ctx)}</li>`;\n case \"BLOCKQUOTE\": return `<blockquote>${childrenToHtml(node, ctx)}</blockquote>`;\n case \"CODE_BLOCK\": return `<pre><code>${childrenToHtml(node, ctx)}</code></pre>`;\n case \"DIVIDER\": return \"<hr>\";\n case \"TABLE\": return `<table><tbody>${childrenToHtml(node, ctx)}</tbody></table>`;\n case \"TABLE_ROW\": return `<tr>${childrenToHtml(node, ctx)}</tr>`;\n case \"TABLE_CELL\": return `<td>${childrenToHtml(node, ctx)}</td>`;\n case \"IMAGE\": {\n const img = imgTag(node.imageData?.image, node.imageData?.altText, ctx);\n if (!img)\n return \"\";\n const attrs = linkAttrs(node.imageData?.link);\n return attrs ? `<a ${attrs}>${img}</a>` : img;\n }\n case \"GALLERY\": {\n const imgs = (node.galleryData?.items ?? [])\n .map((item) => imgTag(item?.image?.media, item?.altText ?? item?.title, ctx))\n .filter(Boolean);\n return imgs.length ? `<figure>${imgs.join(\"\")}</figure>` : \"\";\n }\n case \"VIDEO\": {\n // A link to the video, not a player: an embed would need a host-specific iframe.\n const url = node.videoData?.video?.src?.url;\n if (!isHttpUrl(url))\n break;\n const label = escapeHtml(String(node.videoData?.title || url));\n return `<p><a href=\"${escapeHtml(url)}\" rel=\"noopener noreferrer\" target=\"_blank\">${label}</a></p>`;\n }\n default:\n break;\n }\n // COLLAPSIBLE_LIST, GIF, HTML, EMBED, APP_EMBED, BUTTON, … (and a video with no link) — text only.\n const text = nodeToText(node).trim();\n return text ? `<p>${escapeHtml(text)}</p>` : \"\";\n}\n/** Sanitized HTML for a Ricos document (\"\" for none). */\nexport function richContentToHtml(rc, imgSrc, options = {}) {\n const ctx = {\n imgSrc,\n headingLevel: options.headingLevel ?? ((level) => Math.max(2, level)),\n width: options.imageWidth ?? IMAGE_WIDTH,\n height: options.imageHeight ?? IMAGE_HEIGHT,\n };\n return (rc?.nodes ?? []).map((n) => nodeToHtml(n, ctx)).join(\"\");\n}\nconst INLINE_TYPES = new Set([\"TEXT\"]);\nfunction nodeToText(node) {\n if (node?.type === \"TEXT\")\n return String(node.textData?.text ?? \"\");\n if (node?.type === \"BUTTON\")\n return String(node.buttonData?.text ?? \"\");\n const children = (node?.nodes ?? []);\n const parts = children.map(nodeToText);\n // Blocks join with newlines; inline runs concatenate.\n return children.every((c) => INLINE_TYPES.has(c?.type)) ? parts.join(\"\") : parts.filter(Boolean).join(\"\\n\");\n}\n/** Plain text of a Ricos document — one line per block (\"\" for none). */\nexport function richContentToText(rc) {\n return (rc?.nodes ?? []).map(nodeToText).filter(Boolean).join(\"\\n\").trim();\n}\n"