@select-org/post-components 0.2.1 → 1.0.1

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.
@@ -0,0 +1,439 @@
1
+ // src/utils/postItemType.ts
2
+ var postItemType = {
3
+ ERRORED_ITEM: "erroredItem",
4
+ IMAGE_ITEM: "imageItem",
5
+ MEDIA_ITEM: "mediaItem",
6
+ LINK_ITEM: "linkItem",
7
+ TEXT_ITEM: "textItem",
8
+ VOICE_ITEM: "voiceItem",
9
+ VOICE_ITEM_TO_UPLOAD: "voiceItemToUpload",
10
+ GIF_ITEM: "gifItem",
11
+ GIF_ITEM_TO_UPLOAD: "gifItemToUpload",
12
+ POLL_TEXT_ITEM: "pollTextItem",
13
+ POLL_IMAGE_ITEM: "pollImageItem",
14
+ POLL_IMAGE_ITEM_TO_UPLOAD: "pollImageItemToUpload",
15
+ POLL_MEDIA_ITEM: "pollMediaItem",
16
+ POLL_MEDIA_ITEM_TO_UPLOAD: "pollMediaItemToUpload",
17
+ COMMENT_ITEM_TO_UPLOAD: "commentItemToUpload",
18
+ WEB_IMAGE_ITEM: "webImageItem",
19
+ IMAGE_ITEM_TO_UPLOAD: "imageItemToUpload",
20
+ MEDIA_ITEM_TO_UPLOAD: "mediaItemToUpload",
21
+ LINK_ITEM_TO_FETCH_META: "linkItemToFetchMeta"
22
+ };
23
+
24
+ // src/utils/specialType.ts
25
+ var specialType = {
26
+ NORMAL: 0,
27
+ FEATURED_POST: 1
28
+ };
29
+
30
+ // src/resolver/constants/embeds.ts
31
+ var YOUTUBE_HEIGHT = 360;
32
+ var SPOTIFY_HEIGHT = 152;
33
+
34
+ // src/resolver/embedUtils.ts
35
+ var YOUTUBE_REGEX = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?(?:[^\s&]*&)*v=|embed\/)|youtu\.be\/)[\w-]+(?:[^\s]*)/gi;
36
+ var YOUTUBE_ID_REGEX = /(?:v=|embed\/|\/)([a-zA-Z0-9_-]{11})(?:[?&].*)?$/;
37
+ var SPOTIFY_REGEX = /(?:https?:\/\/)?(?:www\.)?open\.spotify\.com\/(?:embed\/)?(track|playlist|album)\/[A-Za-z0-9_-]+/gi;
38
+ var isYouTubeUrl = (url) => new RegExp(YOUTUBE_REGEX).test(url.trim());
39
+ var isSpotifyUrl = (url) => new RegExp(SPOTIFY_REGEX).test(url.trim());
40
+ var getEmbedUrl = (url) => {
41
+ const trimmed = url.trim();
42
+ if (isYouTubeUrl(trimmed)) {
43
+ const match = trimmed.match(YOUTUBE_ID_REGEX);
44
+ const videoId = match?.[1];
45
+ return videoId ? `https://www.youtube.com/embed/${videoId}` : null;
46
+ }
47
+ if (isSpotifyUrl(trimmed)) {
48
+ const match = trimmed.match(
49
+ /open\.spotify\.com\/(?:embed\/)?(track|playlist|album)\/([A-Za-z0-9]+)/
50
+ );
51
+ if (!match) return null;
52
+ const [, type, id] = match;
53
+ return `https://open.spotify.com/embed/${type}/${id}?utm_source=generator`;
54
+ }
55
+ return null;
56
+ };
57
+ var getEmbedProvider = (url) => {
58
+ if (isYouTubeUrl(url)) return "youtube";
59
+ if (isSpotifyUrl(url)) return "spotify";
60
+ return null;
61
+ };
62
+ var getEmbedHeight = (provider, url) => {
63
+ if (provider === "youtube") return YOUTUBE_HEIGHT;
64
+ if (provider === "spotify") return SPOTIFY_HEIGHT;
65
+ return 320;
66
+ };
67
+ var isEmbeddableUrl = (url) => isYouTubeUrl(url) || isSpotifyUrl(url);
68
+
69
+ // src/resolver/nodeTraversal.ts
70
+ function traverse(node, visitor, options = {}) {
71
+ const { includeRoot = true, maxDepth = -1 } = options;
72
+ function visit(current, parent, index, depth) {
73
+ if (maxDepth !== -1 && depth > maxDepth) return true;
74
+ const result = visitor(current, parent, index, depth);
75
+ if (result === false) return false;
76
+ if (current.content && Array.isArray(current.content)) {
77
+ for (let i = 0; i < current.content.length; i++) {
78
+ if (!visit(current.content[i], current, i, depth + 1)) return false;
79
+ }
80
+ }
81
+ return true;
82
+ }
83
+ if (includeRoot) {
84
+ visit(node, null, 0, 0);
85
+ } else if (node.content) {
86
+ node.content.forEach((child, index) => {
87
+ visit(child, node, index, 1);
88
+ });
89
+ }
90
+ }
91
+ function findFirstByType(node, type) {
92
+ let result = null;
93
+ traverse(node, (current) => {
94
+ if (current.type === type) {
95
+ result = current;
96
+ return false;
97
+ }
98
+ });
99
+ return result;
100
+ }
101
+ function extractText(node) {
102
+ const BLOCK_TYPES = /* @__PURE__ */ new Set([
103
+ "paragraph",
104
+ "heading",
105
+ "blockquote",
106
+ "listItem",
107
+ "bulletList",
108
+ "orderedList",
109
+ "codeBlock"
110
+ ]);
111
+ function walk(current) {
112
+ if (!current) return "";
113
+ if (current.type === "text") {
114
+ return current.text || "";
115
+ }
116
+ if (current.type === "hardBreak") {
117
+ return "\n";
118
+ }
119
+ const children = Array.isArray(current.content) ? current.content.map((child) => walk(child)).join("") : "";
120
+ if (BLOCK_TYPES.has(current.type || "")) {
121
+ return `${children}
122
+ `;
123
+ }
124
+ return children;
125
+ }
126
+ return walk(node).replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
127
+ }
128
+
129
+ // src/resolver/TruncateText.ts
130
+ function TruncateText(text, wordsCount) {
131
+ if (typeof text === "string" && text?.length > wordsCount) {
132
+ return text.slice(0, wordsCount) + "...";
133
+ } else {
134
+ return text;
135
+ }
136
+ }
137
+
138
+ // src/resolver/postContentResolver.ts
139
+ var FEED_TRUNCATION_LIMIT = 200;
140
+ var MEDIA_ITEM_TYPES = [
141
+ postItemType.IMAGE_ITEM,
142
+ postItemType.IMAGE_ITEM_TO_UPLOAD,
143
+ postItemType.MEDIA_ITEM,
144
+ postItemType.MEDIA_ITEM_TO_UPLOAD
145
+ ];
146
+ var LINK_ITEM_TYPES = [
147
+ postItemType.LINK_ITEM,
148
+ postItemType.LINK_ITEM_TO_FETCH_META
149
+ ];
150
+ var POLL_ITEM_TYPES = [
151
+ postItemType.POLL_TEXT_ITEM,
152
+ postItemType.POLL_IMAGE_ITEM,
153
+ postItemType.POLL_IMAGE_ITEM_TO_UPLOAD,
154
+ postItemType.POLL_MEDIA_ITEM,
155
+ postItemType.POLL_MEDIA_ITEM_TO_UPLOAD
156
+ ];
157
+ function extractEmbedFromJsonBody(jsonBody) {
158
+ const iframeNode = findFirstByType(jsonBody, "iframe");
159
+ if (!iframeNode?.attrs?.src) return null;
160
+ const src = iframeNode.attrs.src;
161
+ const provider = getEmbedProvider(src);
162
+ const embedUrl = provider ? src : getEmbedUrl(src);
163
+ const resolvedProvider = provider || getEmbedProvider(embedUrl || "");
164
+ if (!embedUrl || !resolvedProvider) return null;
165
+ const previewPayload = iframeNode.attrs.previewPayload;
166
+ return {
167
+ type: "embed",
168
+ embedUrl,
169
+ provider: resolvedProvider,
170
+ originalUrl: previewPayload?.linkUrl || src,
171
+ height: getEmbedHeight(resolvedProvider, src) || parseInt(iframeNode.attrs.height),
172
+ preview: previewPayload ? {
173
+ title: previewPayload.linkTitle,
174
+ description: previewPayload.linkDesc,
175
+ image: previewPayload.linkImage
176
+ } : void 0
177
+ };
178
+ }
179
+ var URL_EXTRACT_REGEX = /https?:\/\/[^\s<>"']+/gi;
180
+ function extractEmbedFromBodyText(body) {
181
+ if (!body) return null;
182
+ const urlMatches = body.match(URL_EXTRACT_REGEX);
183
+ if (urlMatches) {
184
+ for (const url of urlMatches) {
185
+ if (isEmbeddableUrl(url)) {
186
+ const embedUrl = getEmbedUrl(url);
187
+ const provider = getEmbedProvider(url);
188
+ if (embedUrl && provider) {
189
+ return {
190
+ type: "embed",
191
+ embedUrl,
192
+ provider,
193
+ originalUrl: url,
194
+ height: getEmbedHeight(provider, url)
195
+ };
196
+ }
197
+ }
198
+ }
199
+ }
200
+ const tokens = body.split(/\s+/);
201
+ for (const token of tokens) {
202
+ if (isEmbeddableUrl(token)) {
203
+ const embedUrl = getEmbedUrl(token);
204
+ const provider = getEmbedProvider(token);
205
+ if (embedUrl && provider) {
206
+ return {
207
+ type: "embed",
208
+ embedUrl,
209
+ provider,
210
+ originalUrl: token,
211
+ height: getEmbedHeight(provider, token)
212
+ };
213
+ }
214
+ }
215
+ }
216
+ return null;
217
+ }
218
+ function extractEmbedFromLinkItems(items) {
219
+ if (!items || items.length === 0) return null;
220
+ for (const item of items) {
221
+ const linkUrl = item?.linkUrl;
222
+ if (!linkUrl) continue;
223
+ const embedUrl = getEmbedUrl(linkUrl);
224
+ const provider = getEmbedProvider(linkUrl);
225
+ if (embedUrl && provider) {
226
+ return {
227
+ type: "embed",
228
+ embedUrl,
229
+ provider,
230
+ originalUrl: linkUrl,
231
+ height: getEmbedHeight(provider, linkUrl),
232
+ preview: {
233
+ title: item.linkTitle,
234
+ description: item.linkDesc,
235
+ image: item.linkImage
236
+ }
237
+ };
238
+ }
239
+ }
240
+ return null;
241
+ }
242
+ function hasMeaningfulHtml(html) {
243
+ if (!html) return false;
244
+ const text = html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div|h1|h2|h3|h4|h5|h6|li|blockquote)>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").trim();
245
+ if (text.length > 0) return true;
246
+ return /<img\b|<video\b|<audio\b/i.test(html);
247
+ }
248
+ function filterEmailBoilerplate(text) {
249
+ return text.split("\n").map((line) => line.trim()).filter((line) => {
250
+ if (line.length === 0) return false;
251
+ const lower = line.toLowerCase();
252
+ if (/unsubscribe|manage preferences|email preferences|opt.?out/.test(lower))
253
+ return false;
254
+ if (/view (this|in|online|browser)|browser version/.test(lower))
255
+ return false;
256
+ if (/^https?:\/\/\S+$/.test(line)) return false;
257
+ if (/^\s*©|\bcopyright\b|\ball rights reserved\b/.test(lower))
258
+ return false;
259
+ if (line.split(/\s+/).length <= 3 && line.length < 25) return false;
260
+ return true;
261
+ }).join(" ").replace(/\s{2,}/g, " ").trim();
262
+ }
263
+ function resolveText(contextData, jsonBody, isEmailPost) {
264
+ let plain = null;
265
+ const html = contextData.htmlBody || null;
266
+ const mentionData = contextData.mentionData || [];
267
+ if (jsonBody && html) {
268
+ plain = extractText(jsonBody) || null;
269
+ } else if (html) {
270
+ const cleanHtml = isEmailPost ? html.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<!--[\s\S]*?-->/g, "") : html;
271
+ const htmlAsText = cleanHtml.replace(/<br\s*\/?>/gi, "\n").replace(
272
+ /<\/(p|div|h1|h2|h3|h4|h5|h6|li|blockquote|td|th|tr|caption)>/gi,
273
+ "\n"
274
+ ).replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;|&apos;/g, "'").replace(/&ndash;/g, "\u2013").replace(/&mdash;/g, "\u2014").replace(/&hellip;/g, "\u2026").replace(/&rsquo;|&lsquo;/g, "'").replace(/&rdquo;|&ldquo;/g, '"').replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code))).trim();
275
+ plain = isEmailPost ? filterEmailBoilerplate(htmlAsText) || null : htmlAsText || null;
276
+ } else if (contextData.body) {
277
+ const bodyAsText = contextData.body.replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div|h1|h2|h3|h4|h5|h6|li|blockquote)>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").trim();
278
+ plain = bodyAsText || null;
279
+ }
280
+ const truncatedPlain = plain ? TruncateText(plain, FEED_TRUNCATION_LIMIT) : null;
281
+ return {
282
+ plain,
283
+ html,
284
+ truncatedPlain,
285
+ mentionData
286
+ };
287
+ }
288
+ function resolvePrimaryContent(contextData, jsonBody, pollResult, mode, isEmailPost) {
289
+ const items = contextData.items || [];
290
+ const allowEmbeds = mode === "feed" || mode === "detail";
291
+ const pollItems = items.filter((item) => POLL_ITEM_TYPES.includes(item.type));
292
+ if (pollItems.length > 0) {
293
+ return {
294
+ type: "poll",
295
+ items: pollItems,
296
+ pollResult
297
+ };
298
+ }
299
+ const linkItems = items.filter((item) => LINK_ITEM_TYPES.includes(item.type));
300
+ if (allowEmbeds) {
301
+ if (jsonBody) {
302
+ const embed = extractEmbedFromJsonBody(jsonBody);
303
+ if (embed) return embed;
304
+ }
305
+ if (linkItems.length > 0) {
306
+ const embed = extractEmbedFromLinkItems(linkItems);
307
+ if (embed) return embed;
308
+ }
309
+ if (contextData.body) {
310
+ const embed = extractEmbedFromBodyText(contextData.body);
311
+ if (embed) return embed;
312
+ }
313
+ }
314
+ if (isEmailPost && mode === "feed") {
315
+ const mediaItems2 = items.filter(
316
+ (item) => MEDIA_ITEM_TYPES.includes(item.type)
317
+ );
318
+ if (mediaItems2.length > 0) {
319
+ return { type: "media", items: mediaItems2 };
320
+ }
321
+ return null;
322
+ }
323
+ if (hasMeaningfulHtml(contextData.htmlBody)) return null;
324
+ if (linkItems.length > 0) {
325
+ const link = linkItems[0];
326
+ return {
327
+ type: "link",
328
+ linkUrl: link.linkUrl || "",
329
+ title: link.linkTitle || "",
330
+ description: link.linkDesc || "",
331
+ image: link.linkImage || null
332
+ };
333
+ }
334
+ const mediaItems = items?.filter(
335
+ (item) => MEDIA_ITEM_TYPES.includes(item.type)
336
+ );
337
+ if (mediaItems.length > 0) {
338
+ return {
339
+ type: "media",
340
+ items: mediaItems
341
+ };
342
+ }
343
+ if (!contextData.body && !jsonBody && !contextData.htmlBody) {
344
+ const textItems = items?.filter(
345
+ (item) => item.type === postItemType.TEXT_ITEM
346
+ );
347
+ if (textItems.length > 0) {
348
+ return { type: "textItem", items: textItems };
349
+ }
350
+ }
351
+ return null;
352
+ }
353
+ function resolveIsRichContent(contextData, isEmailPost) {
354
+ if (isEmailPost) return false;
355
+ return hasMeaningfulHtml(contextData.htmlBody);
356
+ }
357
+ function getPrimaryUrl(primaryContent) {
358
+ if (!primaryContent) return null;
359
+ if (primaryContent.type === "embed") return primaryContent.originalUrl || null;
360
+ if (primaryContent.type === "link") return primaryContent.linkUrl || null;
361
+ return null;
362
+ }
363
+ function escapeRegExp(value) {
364
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
365
+ }
366
+ function dedupePrimaryUrlFromText(text, primaryContent) {
367
+ if (!text) {
368
+ return { text, dedupedUrls: [], hasPrimaryUrlInText: false };
369
+ }
370
+ const primaryUrl = getPrimaryUrl(primaryContent);
371
+ if (!primaryUrl) {
372
+ return { text, dedupedUrls: [], hasPrimaryUrlInText: false };
373
+ }
374
+ const escaped = escapeRegExp(primaryUrl);
375
+ const regex = new RegExp(escaped, "gi");
376
+ const hasPrimaryUrlInText = regex.test(text);
377
+ if (!hasPrimaryUrlInText) {
378
+ return { text, dedupedUrls: [], hasPrimaryUrlInText: false };
379
+ }
380
+ return {
381
+ text,
382
+ dedupedUrls: [primaryUrl],
383
+ hasPrimaryUrlInText: true
384
+ };
385
+ }
386
+ function resolvePostContent(contextData, opts) {
387
+ const {
388
+ postType,
389
+ pollResult,
390
+ postSpecialType,
391
+ mode,
392
+ isEmailPost = false
393
+ } = opts;
394
+ const jsonBody = contextData?.jsonBody;
395
+ const jsonBodyOrNull = jsonBody?.type ? jsonBody : null;
396
+ const baseTextContent = resolveText(contextData, jsonBodyOrNull, isEmailPost);
397
+ const primaryContent = resolvePrimaryContent(
398
+ contextData,
399
+ jsonBodyOrNull,
400
+ pollResult,
401
+ mode,
402
+ isEmailPost
403
+ );
404
+ const dedupeResult = dedupePrimaryUrlFromText(
405
+ baseTextContent.plain,
406
+ primaryContent
407
+ );
408
+ const plain = dedupeResult.text;
409
+ const truncatedPlain = plain ? TruncateText(plain, FEED_TRUNCATION_LIMIT) : null;
410
+ const textContent = {
411
+ ...baseTextContent,
412
+ plain,
413
+ truncatedPlain,
414
+ ...isEmailPost && { subject: contextData.body || null }
415
+ };
416
+ const rawSections = contextData?.sections;
417
+ const sections = Array.isArray(rawSections) && rawSections.length > 0 ? { type: "sections", sections: rawSections } : null;
418
+ return {
419
+ textContent,
420
+ primaryContent,
421
+ sections,
422
+ isRichContent: resolveIsRichContent(contextData, isEmailPost),
423
+ postType,
424
+ isFeatured: postSpecialType === specialType.FEATURED_POST,
425
+ mode,
426
+ flags: {
427
+ hasPrimaryUrlInText: dedupeResult.hasPrimaryUrlInText
428
+ }
429
+ };
430
+ }
431
+
432
+ export {
433
+ postItemType,
434
+ SPOTIFY_HEIGHT,
435
+ TruncateText,
436
+ hasMeaningfulHtml,
437
+ resolvePostContent
438
+ };
439
+ //# sourceMappingURL=chunk-JPYOLIZH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/postItemType.ts","../src/utils/specialType.ts","../src/resolver/constants/embeds.ts","../src/resolver/embedUtils.ts","../src/resolver/nodeTraversal.ts","../src/resolver/TruncateText.ts","../src/resolver/postContentResolver.ts"],"sourcesContent":["// Verbatim port of postItemType from web client src/Constants.ts\n// These values must match the backend exactly — do not rename or change values.\nexport const postItemType = {\n ERRORED_ITEM: 'erroredItem',\n\n IMAGE_ITEM: 'imageItem',\n MEDIA_ITEM: 'mediaItem',\n LINK_ITEM: 'linkItem',\n TEXT_ITEM: 'textItem',\n VOICE_ITEM: 'voiceItem',\n VOICE_ITEM_TO_UPLOAD: 'voiceItemToUpload',\n GIF_ITEM: 'gifItem',\n GIF_ITEM_TO_UPLOAD: 'gifItemToUpload',\n POLL_TEXT_ITEM: 'pollTextItem',\n POLL_IMAGE_ITEM: 'pollImageItem',\n POLL_IMAGE_ITEM_TO_UPLOAD: 'pollImageItemToUpload',\n POLL_MEDIA_ITEM: 'pollMediaItem',\n POLL_MEDIA_ITEM_TO_UPLOAD: 'pollMediaItemToUpload',\n COMMENT_ITEM_TO_UPLOAD: 'commentItemToUpload',\n WEB_IMAGE_ITEM: 'webImageItem',\n IMAGE_ITEM_TO_UPLOAD: 'imageItemToUpload',\n MEDIA_ITEM_TO_UPLOAD: 'mediaItemToUpload',\n LINK_ITEM_TO_FETCH_META: 'linkItemToFetchMeta',\n} as const\n","// Verbatim port of specialType from web client src/Constants.ts\n// These values must match the backend exactly — do not rename or change values.\nexport const specialType = {\n NORMAL: 0,\n FEATURED_POST: 1,\n} as const\n","export const YOUTUBE_HEIGHT = 360\nexport const SPOTIFY_HEIGHT = 152\n","import { SPOTIFY_HEIGHT, YOUTUBE_HEIGHT } from './constants/embeds'\n\n// YouTube URL detection (matches youtube.com/watch?v=, youtu.be/, and youtube.com/embed/)\nconst YOUTUBE_REGEX =\n /(?:https?:\\/\\/)?(?:www\\.)?(?:youtube\\.com\\/(?:watch\\?(?:[^\\s&]*&)*v=|embed\\/)|youtu\\.be\\/)[\\w-]+(?:[^\\s]*)/gi\n\n// Extract YouTube video ID (exactly 11 chars) from watch, embed, or youtu.be URLs\nconst YOUTUBE_ID_REGEX = /(?:v=|embed\\/|\\/)([a-zA-Z0-9_-]{11})(?:[?&].*)?$/\n\n// Spotify URL detection (track, playlist, or album - with or without /embed/ prefix)\nconst SPOTIFY_REGEX =\n /(?:https?:\\/\\/)?(?:www\\.)?open\\.spotify\\.com\\/(?:embed\\/)?(track|playlist|album)\\/[A-Za-z0-9_-]+/gi\n\nexport const isYouTubeUrl = (url: string): boolean =>\n new RegExp(YOUTUBE_REGEX).test(url.trim())\n\nexport const isSpotifyUrl = (url: string): boolean =>\n new RegExp(SPOTIFY_REGEX).test(url.trim())\n\n/**\n * Converts supported media URLs to embeddable iframe URLs.\n * Returns null if URL is not recognized as embeddable.\n */\nexport const getEmbedUrl = (url: string): string | null => {\n const trimmed = url.trim()\n\n if (isYouTubeUrl(trimmed)) {\n const match = trimmed.match(YOUTUBE_ID_REGEX)\n const videoId = match?.[1]\n return videoId ? `https://www.youtube.com/embed/${videoId}` : null\n }\n\n if (isSpotifyUrl(trimmed)) {\n const match = trimmed.match(\n /open\\.spotify\\.com\\/(?:embed\\/)?(track|playlist|album)\\/([A-Za-z0-9]+)/\n )\n if (!match) return null\n const [, type, id] = match\n return `https://open.spotify.com/embed/${type}/${id}?utm_source=generator`\n }\n\n return null\n}\n\nexport type EmbedProvider = 'youtube' | 'spotify'\n\nexport const getEmbedProvider = (url: string): EmbedProvider | null => {\n if (isYouTubeUrl(url)) return 'youtube'\n if (isSpotifyUrl(url)) return 'spotify'\n return null\n}\n\n/**\n * Returns default iframe height for a given provider.\n * YouTube: 320px. Spotify tracks: 152px. Spotify playlists/albums: 352px.\n */\nexport const getEmbedHeight = (\n provider: EmbedProvider,\n url?: string\n): number => {\n if (provider === 'youtube') return YOUTUBE_HEIGHT\n if (provider === 'spotify') return SPOTIFY_HEIGHT\n\n return 320\n}\n\nexport const isEmbeddableUrl = (url: string): boolean =>\n isYouTubeUrl(url) || isSpotifyUrl(url)\n","/**\n * Lightweight TipTap JSONContent traversal utilities.\n * Ported from select-post-builder/src/lib/node-traversal.utils.ts.\n *\n * These operate on the JSONContent structure stored in context_data.jsonBody.\n */\n\n// Minimal JSONContent type (avoids depending on @tiptap/core)\nexport interface JSONContent {\n type?: string\n attrs?: Record<string, any>\n content?: JSONContent[]\n text?: string\n marks?: Array<{ type: string; attrs?: Record<string, any> }>\n}\n\ntype VisitorFn = (\n node: JSONContent,\n parent: JSONContent | null,\n index: number,\n depth: number\n) => void | boolean // return false to stop traversal\n\n/**\n * Depth-first traversal of TipTap JSON structure.\n */\nexport function traverse(\n node: JSONContent,\n visitor: VisitorFn,\n options: { includeRoot?: boolean; maxDepth?: number } = {}\n): void {\n const { includeRoot = true, maxDepth = -1 } = options\n\n function visit(\n current: JSONContent,\n parent: JSONContent | null,\n index: number,\n depth: number\n ): boolean {\n if (maxDepth !== -1 && depth > maxDepth) return true\n\n const result = visitor(current, parent, index, depth)\n if (result === false) return false\n\n if (current.content && Array.isArray(current.content)) {\n for (let i = 0; i < current.content.length; i++) {\n if (!visit(current.content[i], current, i, depth + 1)) return false\n }\n }\n\n return true\n }\n\n if (includeRoot) {\n visit(node, null, 0, 0)\n } else if (node.content) {\n node.content.forEach((child, index) => {\n visit(child, node, index, 1)\n })\n }\n}\n\n/**\n * Find all nodes matching a type.\n */\nexport function findByType(node: JSONContent, type: string): JSONContent[] {\n const results: JSONContent[] = []\n traverse(node, (current) => {\n if (current.type === type) results.push(current)\n })\n return results\n}\n\n/**\n * Find the first node matching a type.\n */\nexport function findFirstByType(\n node: JSONContent,\n type: string\n): JSONContent | null {\n let result: JSONContent | null = null\n traverse(node, (current) => {\n if (current.type === type) {\n result = current\n return false // stop traversal\n }\n })\n return result\n}\n\n/**\n * Find nodes matching a predicate.\n */\nexport function find(\n node: JSONContent,\n predicate: (node: JSONContent) => boolean\n): JSONContent[] {\n const results: JSONContent[] = []\n traverse(node, (current) => {\n if (predicate(current)) results.push(current)\n })\n return results\n}\n\n/**\n * Check if a node type exists anywhere in the tree.\n */\nexport function hasNodeType(node: JSONContent, type: string): boolean {\n let found = false\n traverse(node, (current) => {\n if (current.type === type) {\n found = true\n return false\n }\n })\n return found\n}\n\n/**\n * Extract all text content from the JSON tree.\n */\nexport function extractText(node: JSONContent): string {\n const BLOCK_TYPES = new Set([\n 'paragraph',\n 'heading',\n 'blockquote',\n 'listItem',\n 'bulletList',\n 'orderedList',\n 'codeBlock',\n ])\n\n function walk(current: JSONContent): string {\n if (!current) return ''\n\n if (current.type === 'text') {\n return current.text || ''\n }\n\n if (current.type === 'hardBreak') {\n return '\\n'\n }\n\n const children = Array.isArray(current.content)\n ? current.content.map((child) => walk(child)).join('')\n : ''\n\n if (BLOCK_TYPES.has(current.type || '')) {\n return `${children}\\n`\n }\n\n return children\n }\n\n return walk(node)\n .replace(/[ \\t]+\\n/g, '\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n","export default function TruncateText(text: any, wordsCount: number) {\n if (typeof text === 'string' && text?.length > wordsCount) {\n return text.slice(0, wordsCount) + '...'\n } else {\n return text\n }\n}\n","import {\n PostDataMetaData,\n LinkItemMetaData,\n PollItemData,\n ItemData,\n PollResult,\n MentionData,\n} from '../types'\nimport { postItemType } from '../utils/postItemType'\nimport { specialType as specialTypeConst } from '../utils/specialType'\nimport {\n getEmbedUrl,\n getEmbedProvider,\n getEmbedHeight,\n isEmbeddableUrl,\n type EmbedProvider,\n} from './embedUtils'\nimport { findFirstByType, extractText, type JSONContent } from './nodeTraversal'\nimport TruncateText from './TruncateText'\n\n// ============================================================================\n// CONTRACT TYPES\n// ============================================================================\n\nexport type EmbedContent = {\n type: 'embed'\n embedUrl: string\n provider: EmbedProvider\n originalUrl: string\n height: number\n preview?: {\n title?: string\n description?: string\n image?: { width: number; height: number; url: string }\n }\n}\n\nexport type LinkContent = {\n type: 'link'\n linkUrl: string\n title: string\n description: string\n image: { width: number; height: number; url: string } | null\n}\n\nexport type MediaContent = {\n type: 'media'\n items: ItemData[]\n}\n\nexport type PollContent = {\n type: 'poll'\n items: PollItemData[]\n pollResult: PollResult | null\n}\n\nexport type SectionItem = {\n type: string\n data: {\n body?: string\n items?: ItemData[]\n mentionData?: MentionData[]\n sourceInfo?: { avatar: string; username: string; url: string }\n [key: string]: unknown\n }\n}\n\nexport type SectionsContent = {\n type: 'sections'\n sections: SectionItem[]\n}\n\nexport type TextItemContent = {\n type: 'textItem'\n items: (ItemData | LinkItemMetaData | PollItemData)[]\n}\n\nexport type PrimaryContent =\n | EmbedContent\n | LinkContent\n | MediaContent\n | PollContent\n | TextItemContent\n | null\n\nexport type ResolvedTextContent = {\n plain: string | null\n html: string | null\n truncatedPlain: string | null\n mentionData: MentionData[]\n subject?: string | null\n}\n\nexport type ResolvePostContentMode = 'feed' | 'detail' | 'inbox' | 'draft'\n\nexport type ResolvedPostContent = {\n textContent: ResolvedTextContent\n primaryContent: PrimaryContent\n sections: SectionsContent | null\n isRichContent: boolean\n postType: string\n isFeatured: boolean\n mode: ResolvePostContentMode\n flags: {\n hasPrimaryUrlInText: boolean\n }\n}\n\n// ============================================================================\n// CONSTANTS\n// ============================================================================\n\nconst FEED_TRUNCATION_LIMIT = 200\n\nconst MEDIA_ITEM_TYPES: string[] = [\n postItemType.IMAGE_ITEM,\n postItemType.IMAGE_ITEM_TO_UPLOAD,\n postItemType.MEDIA_ITEM,\n postItemType.MEDIA_ITEM_TO_UPLOAD,\n]\n\nconst LINK_ITEM_TYPES: string[] = [\n postItemType.LINK_ITEM,\n postItemType.LINK_ITEM_TO_FETCH_META,\n]\n\nconst POLL_ITEM_TYPES: string[] = [\n postItemType.POLL_TEXT_ITEM,\n postItemType.POLL_IMAGE_ITEM,\n postItemType.POLL_IMAGE_ITEM_TO_UPLOAD,\n postItemType.POLL_MEDIA_ITEM,\n postItemType.POLL_MEDIA_ITEM_TO_UPLOAD,\n]\n\n// ============================================================================\n// EMBED EXTRACTION\n// ============================================================================\n\nfunction extractEmbedFromJsonBody(jsonBody: JSONContent): EmbedContent | null {\n const iframeNode = findFirstByType(jsonBody, 'iframe')\n if (!iframeNode?.attrs?.src) return null\n\n const src = iframeNode.attrs.src as string\n const provider = getEmbedProvider(src)\n const embedUrl = provider ? src : getEmbedUrl(src)\n const resolvedProvider = provider || getEmbedProvider(embedUrl || '')\n\n if (!embedUrl || !resolvedProvider) return null\n\n const previewPayload = iframeNode.attrs.previewPayload\n\n return {\n type: 'embed',\n embedUrl,\n provider: resolvedProvider,\n originalUrl: previewPayload?.linkUrl || src,\n height:\n getEmbedHeight(resolvedProvider, src) ||\n parseInt(iframeNode.attrs.height as string),\n\n preview: previewPayload\n ? {\n title: previewPayload.linkTitle,\n description: previewPayload.linkDesc,\n image: previewPayload.linkImage,\n }\n : undefined,\n }\n}\n\n// Regex to extract URLs from text, even when concatenated with surrounding characters\nconst URL_EXTRACT_REGEX = /https?:\\/\\/[^\\s<>\"']+/gi\n\n/**\n * Try to extract an embed from plain body text (e.g. a Spotify URL pasted as text).\n * Uses URL regex extraction instead of whitespace splitting to handle URLs\n * that are concatenated with surrounding text (e.g. \"asdbopen.spotify.com/embed/...\").\n */\nfunction extractEmbedFromBodyText(body: string): EmbedContent | null {\n if (!body) return null\n\n // First try: extract proper URLs (with protocol) from body\n const urlMatches = body.match(URL_EXTRACT_REGEX)\n if (urlMatches) {\n for (const url of urlMatches) {\n if (isEmbeddableUrl(url)) {\n const embedUrl = getEmbedUrl(url)\n const provider = getEmbedProvider(url)\n if (embedUrl && provider) {\n return {\n type: 'embed',\n embedUrl,\n provider,\n originalUrl: url,\n height: getEmbedHeight(provider, url),\n }\n }\n }\n }\n }\n\n // Second try: split on whitespace for URLs without protocol (e.g. \"open.spotify.com/...\")\n const tokens = body.split(/\\s+/)\n for (const token of tokens) {\n if (isEmbeddableUrl(token)) {\n const embedUrl = getEmbedUrl(token)\n const provider = getEmbedProvider(token)\n if (embedUrl && provider) {\n return {\n type: 'embed',\n embedUrl,\n provider,\n originalUrl: token,\n height: getEmbedHeight(provider, token),\n }\n }\n }\n }\n\n return null\n}\n\nfunction extractEmbedFromLinkItems(\n items: LinkItemMetaData[]\n): EmbedContent | null {\n if (!items || items.length === 0) return null\n\n for (const item of items) {\n const linkUrl = item?.linkUrl\n if (!linkUrl) continue\n\n const embedUrl = getEmbedUrl(linkUrl)\n const provider = getEmbedProvider(linkUrl)\n if (embedUrl && provider) {\n return {\n type: 'embed',\n embedUrl,\n provider,\n originalUrl: linkUrl,\n height: getEmbedHeight(provider, linkUrl),\n preview: {\n title: item.linkTitle,\n description: item.linkDesc,\n image: item.linkImage,\n },\n }\n }\n }\n\n return null\n}\n\n// ============================================================================\n// HELPERS\n// ============================================================================\n\nexport function hasMeaningfulHtml(html: string | null | undefined): boolean {\n if (!html) return false\n const text = html\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/(p|div|h1|h2|h3|h4|h5|h6|li|blockquote)>/gi, '\\n')\n .replace(/<[^>]+>/g, '')\n .replace(/&nbsp;/g, ' ')\n .trim()\n if (text.length > 0) return true\n // No text — but a Blog post can carry only media. Media-tab posts never\n // set htmlBody (verified across payload.utils.ts, CreatePostInput, and the\n // saga's outgoing spread), so any media element here signals a Blog post\n // whose body must render via htmlBody. iframe is intentionally NOT in this\n // regex — DOMPurify in PostBody forbids it, and embeds come from jsonBody.\n return /<img\\b|<video\\b|<audio\\b/i.test(html)\n}\n\n// ============================================================================\n// TEXT RESOLUTION\n// ============================================================================\n\nfunction filterEmailBoilerplate(text: string): string {\n return text\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => {\n if (line.length === 0) return false\n const lower = line.toLowerCase()\n if (\n /unsubscribe|manage preferences|email preferences|opt.?out/.test(lower)\n )\n return false\n if (/view (this|in|online|browser)|browser version/.test(lower))\n return false\n if (/^https?:\\/\\/\\S+$/.test(line)) return false\n if (/^\\s*©|\\bcopyright\\b|\\ball rights reserved\\b/.test(lower))\n return false\n if (line.split(/\\s+/).length <= 3 && line.length < 25) return false\n return true\n })\n .join(' ')\n .replace(/\\s{2,}/g, ' ')\n .trim()\n}\n\nfunction resolveText(\n contextData: PostDataMetaData,\n jsonBody: JSONContent | null,\n isEmailPost: boolean\n): ResolvedTextContent {\n let plain: string | null = null\n const html = contextData.htmlBody || null\n const mentionData = contextData.mentionData || []\n\n if (jsonBody && html) {\n // Only use jsonBody for text extraction when htmlBody is also present —\n // that combination signals genuinely rich content. Plain posts also get a\n // jsonBody from TipTap, but body is the canonical plain-text source\n // (always kept in sync server-side) and is what mentionData was built from.\n plain = extractText(jsonBody) || null\n } else if (html) {\n // htmlBody is authoritative if no jsonBody.\n // For email posts, strip <head> (contains <style> and <title>) and any\n // remaining <style> blocks before extracting text, so CSS and the title\n // tag don't surface as visible content.\n const cleanHtml = isEmailPost\n ? html\n .replace(/<head[^>]*>[\\s\\S]*?<\\/head>/gi, '')\n .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<!--[\\s\\S]*?-->/g, '')\n : html\n const htmlAsText = cleanHtml\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(\n /<\\/(p|div|h1|h2|h3|h4|h5|h6|li|blockquote|td|th|tr|caption)>/gi,\n '\\n'\n )\n .replace(/<[^>]+>/g, '')\n .replace(/&nbsp;/g, ' ')\n .replace(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;|&apos;/g, \"'\")\n .replace(/&ndash;/g, '–')\n .replace(/&mdash;/g, '—')\n .replace(/&hellip;/g, '…')\n .replace(/&rsquo;|&lsquo;/g, \"'\")\n .replace(/&rdquo;|&ldquo;/g, '\"')\n .replace(/&#(\\d+);/g, (_, code) => String.fromCodePoint(Number(code)))\n .trim()\n plain = isEmailPost\n ? filterEmailBoilerplate(htmlAsText) || null\n : htmlAsText || null\n } else if (contextData.body) {\n // Old-format posts stored HTML in the body field. Strip tags so raw\n // iframe/HTML markup never surfaces as visible plain text.\n const bodyAsText = contextData.body\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/(p|div|h1|h2|h3|h4|h5|h6|li|blockquote)>/gi, '\\n')\n .replace(/<[^>]+>/g, '')\n .replace(/&nbsp;/g, ' ')\n .trim()\n plain = bodyAsText || null\n }\n\n const truncatedPlain = plain\n ? TruncateText(plain, FEED_TRUNCATION_LIMIT)\n : null\n\n return {\n plain,\n html,\n truncatedPlain,\n mentionData,\n }\n}\n\n// ============================================================================\n// PRIMARY CONTENT RESOLUTION\n// ============================================================================\n\nfunction resolvePrimaryContent(\n contextData: PostDataMetaData,\n jsonBody: JSONContent | null,\n pollResult: PollResult | null,\n mode: ResolvePostContentMode,\n isEmailPost: boolean\n): PrimaryContent {\n const items = contextData.items || []\n const allowEmbeds = mode === 'feed' || mode === 'detail'\n\n // Priority 1: Polls\n const pollItems = items.filter((item) => POLL_ITEM_TYPES.includes(item.type))\n if (pollItems.length > 0) {\n return {\n type: 'poll',\n items: pollItems as PollItemData[],\n pollResult,\n }\n }\n\n const linkItems = items.filter((item) => LINK_ITEM_TYPES.includes(item.type))\n\n // Priority 2: Embeds (always checked — iframes are stripped from htmlBody by DOMPurify)\n if (allowEmbeds) {\n if (jsonBody) {\n const embed = extractEmbedFromJsonBody(jsonBody)\n if (embed) return embed\n }\n if (linkItems.length > 0) {\n const embed = extractEmbedFromLinkItems(linkItems as LinkItemMetaData[])\n if (embed) return embed\n }\n if (contextData.body) {\n const embed = extractEmbedFromBodyText(contextData.body)\n if (embed) return embed\n }\n }\n\n // For email posts in feed mode: surface the backend-provided image from items\n // instead of rendering the full HTML body. If no image exists the caller will\n // fall back to showing the description text.\n if (isEmailPost && mode === 'feed') {\n const mediaItems = items.filter((item) =>\n MEDIA_ITEM_TYPES.includes(item.type)\n )\n if (mediaItems.length > 0) {\n return { type: 'media', items: mediaItems as ItemData[] }\n }\n return null\n }\n\n // For rich HTML posts, images/media are already rendered inside htmlBody.\n // Skip link previews and media items to prevent duplication.\n if (hasMeaningfulHtml(contextData.htmlBody)) return null\n\n // Priority 3: Link preview\n if (linkItems.length > 0) {\n const link = linkItems[0] as LinkItemMetaData\n return {\n type: 'link',\n linkUrl: link.linkUrl || '',\n title: link.linkTitle || '',\n description: link.linkDesc || '',\n image: link.linkImage || null,\n }\n }\n\n // Priority 4: Media\n const mediaItems = items?.filter((item) =>\n MEDIA_ITEM_TYPES.includes(item.type)\n )\n if (mediaItems.length > 0) {\n return {\n type: 'media',\n items: mediaItems as ItemData[],\n }\n }\n\n // Priority 5: Legacy TEXT_ITEMs — only for posts with no body/jsonBody/htmlBody\n if (!contextData.body && !jsonBody && !contextData.htmlBody) {\n const textItems = items?.filter(\n (item) => item.type === postItemType.TEXT_ITEM\n )\n if (textItems.length > 0) {\n return { type: 'textItem', items: textItems }\n }\n }\n\n return null\n}\n\nfunction resolveIsRichContent(\n contextData: PostDataMetaData,\n isEmailPost: boolean\n): boolean {\n if (isEmailPost) return false\n return hasMeaningfulHtml(contextData.htmlBody)\n}\n\nfunction getPrimaryUrl(primaryContent: PrimaryContent): string | null {\n if (!primaryContent) return null\n if (primaryContent.type === 'embed') return primaryContent.originalUrl || null\n if (primaryContent.type === 'link') return primaryContent.linkUrl || null\n return null\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\nfunction dedupePrimaryUrlFromText(\n text: string | null,\n primaryContent: PrimaryContent\n): {\n text: string | null\n dedupedUrls: string[]\n hasPrimaryUrlInText: boolean\n} {\n if (!text) {\n return { text, dedupedUrls: [], hasPrimaryUrlInText: false }\n }\n\n const primaryUrl = getPrimaryUrl(primaryContent)\n if (!primaryUrl) {\n return { text, dedupedUrls: [], hasPrimaryUrlInText: false }\n }\n\n const escaped = escapeRegExp(primaryUrl)\n const regex = new RegExp(escaped, 'gi')\n const hasPrimaryUrlInText = regex.test(text)\n\n if (!hasPrimaryUrlInText) {\n return { text, dedupedUrls: [], hasPrimaryUrlInText: false }\n }\n\n return {\n text,\n dedupedUrls: [primaryUrl],\n hasPrimaryUrlInText: true,\n }\n}\n\n// ============================================================================\n// MAIN RESOLVER\n// ============================================================================\n\nexport function resolvePostContent(\n contextData: PostDataMetaData,\n opts: {\n postType: string\n pollResult: PollResult | null\n postSpecialType: number\n mode: ResolvePostContentMode\n isEmailPost?: boolean\n }\n): ResolvedPostContent {\n const {\n postType,\n pollResult,\n postSpecialType,\n mode,\n isEmailPost = false,\n } = opts\n const jsonBody = contextData?.jsonBody as JSONContent | undefined\n const jsonBodyOrNull = jsonBody?.type ? jsonBody : null\n\n const baseTextContent = resolveText(contextData, jsonBodyOrNull, isEmailPost)\n const primaryContent = resolvePrimaryContent(\n contextData,\n jsonBodyOrNull,\n pollResult,\n mode,\n isEmailPost\n )\n const dedupeResult = dedupePrimaryUrlFromText(\n baseTextContent.plain,\n primaryContent\n )\n const plain = dedupeResult.text\n const truncatedPlain = plain\n ? TruncateText(plain, FEED_TRUNCATION_LIMIT)\n : null\n const textContent: ResolvedTextContent = {\n ...baseTextContent,\n plain,\n truncatedPlain,\n ...(isEmailPost && { subject: contextData.body || null }),\n }\n const rawSections = contextData?.sections\n const sections: SectionsContent | null =\n Array.isArray(rawSections) && rawSections.length > 0\n ? { type: 'sections', sections: rawSections }\n : null\n\n return {\n textContent,\n primaryContent,\n sections,\n isRichContent: resolveIsRichContent(contextData, isEmailPost),\n postType,\n isFeatured: postSpecialType === specialTypeConst.FEATURED_POST,\n mode,\n flags: {\n hasPrimaryUrlInText: dedupeResult.hasPrimaryUrlInText,\n },\n }\n}\n"],"mappings":"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","names":["mediaItems"]}