@ecency/render-helper 2.4.15 → 2.4.17

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.
@@ -22,7 +22,14 @@ interface SeoContext {
22
22
  postPayout?: number;
23
23
  }
24
24
 
25
- declare function markdown2Html(obj: Entry | string, forApp?: boolean, webp?: boolean, parentDomain?: string, seoContext?: SeoContext): string;
25
+ /**
26
+ * @param obj - Entry object or raw markdown string
27
+ * @param forApp - Whether rendering for app context
28
+ * @param _webp - @deprecated Ignored. Format is now handled server-side via Accept header content negotiation.
29
+ * @param parentDomain - Parent domain for iframe embed parameters
30
+ * @param seoContext - Optional SEO context for structured data
31
+ */
32
+ declare function markdown2Html(obj: Entry | string, forApp?: boolean, _webp?: boolean, parentDomain?: string, seoContext?: SeoContext): string;
26
33
 
27
34
  declare function catchPostImage(obj: Entry | string, width?: number, height?: number, format?: string): string | null;
28
35
 
@@ -37,7 +44,10 @@ declare function catchPostImage(obj: Entry | string, width?: number, height?: nu
37
44
  declare function getPostBodySummary(obj: Entry | string, length?: number, platform?: 'ios' | 'android' | 'web'): string;
38
45
 
39
46
  declare function setProxyBase(p: string): void;
40
- declare function proxifyImageSrc(url?: string, width?: number, height?: number, format?: string): string;
47
+ /**
48
+ * @param _format - @deprecated Ignored. Always uses 'match' — format is handled server-side via Accept header.
49
+ */
50
+ declare function proxifyImageSrc(url?: string, width?: number, height?: number, _format?: string): string;
41
51
 
42
52
  declare function setCacheSize(size: number): void;
43
53
 
@@ -78,8 +78,8 @@ var LBRY_REGEX = /^(https?:)?\/\/lbry.tv\/\$\/embed\/[^?#]+(?:$|[?#])/i;
78
78
  var ODYSEE_REGEX = /^(https?:)?\/\/odysee\.com\/(?:\$|%24)\/embed\/[^/?#]+(?:$|[?#])/i;
79
79
  var SKATEHIVE_IPFS_REGEX = /^https?:\/\/ipfs\.skatehive\.app\/ipfs\/([^/?#]+)/i;
80
80
  var ARCH_REGEX = /^(https?:)?\/\/archive.org\/embed\/[^/?#]+(?:$|[?#])/i;
81
- var SPEAK_REGEX = /(?:https?:\/\/(?:(?:play\.)?3speak.([a-z]+)\/watch\?v=)|(?:(?:play\.)?3speak.([a-z]+)\/embed\?v=))([A-Za-z0-9\_\-\.\/]+)(&.*)?/i;
82
- var SPEAK_EMBED_REGEX = /^(https?:)?\/\/(?:play\.)?3speak.([a-z]+)\/embed\?[^/]+$/i;
81
+ var SPEAK_REGEX = /(?:https?:\/\/(?:(?:play\.)?3speak\.([a-z]+)\/watch\?v=)|(?:(?:play\.)?3speak\.([a-z]+)\/embed\?v=))([A-Za-z0-9_\-\.\/]+)(&.*)?/i;
82
+ var SPEAK_EMBED_REGEX = /^(https?:)?\/\/(?:play\.)?3speak\.([a-z]+)\/(?:embed|watch)\?.+$/i;
83
83
  var TWITTER_REGEX = /(?:https?:\/\/(?:(?:twitter\.com\/(.*?)\/status\/(.*))))/gi;
84
84
  var SPOTIFY_REGEX = /^https:\/\/open\.spotify\.com\/playlist\/(.*)?$/gi;
85
85
  var RUMBLE_REGEX = /^https:\/\/rumble.com\/embed\/([a-zA-Z0-9-]+)\/\?pub=\w+/;
@@ -284,15 +284,13 @@ function sanitizeHtml(html) {
284
284
  });
285
285
  }
286
286
  var proxyBase = "https://images.ecency.com";
287
- var fileExtension = true;
288
287
  function setProxyBase(p2) {
289
288
  proxyBase = p2;
290
- fileExtension = proxyBase == "https://images.ecency.com";
291
289
  }
292
290
  function extractPHash(url) {
293
291
  if (url.startsWith(`${proxyBase}/p/`)) {
294
292
  const [hash] = url.split("/p/")[1].split("?");
295
- return hash.replace(/.webp/, "").replace(/.png/, "");
293
+ return hash.replace(/\.(webp|png)$/, "");
296
294
  }
297
295
  return null;
298
296
  }
@@ -307,7 +305,7 @@ function getLatestUrl(str) {
307
305
  const [last] = [...str.replace(/https?:\/\//g, "\n$&").trim().split("\n")].reverse();
308
306
  return last;
309
307
  }
310
- function proxifyImageSrc(url, width = 0, height = 0, format = "match") {
308
+ function proxifyImageSrc(url, width = 0, height = 0, _format = "match") {
311
309
  if (!url || typeof url !== "string" || !isValidUrl(url)) {
312
310
  return "";
313
311
  }
@@ -320,7 +318,7 @@ function proxifyImageSrc(url, width = 0, height = 0, format = "match") {
320
318
  const realUrl = getLatestUrl(url);
321
319
  const pHash = extractPHash(realUrl);
322
320
  const options = {
323
- format,
321
+ format: "match",
324
322
  mode: "fit"
325
323
  };
326
324
  if (width > 0) {
@@ -331,31 +329,29 @@ function proxifyImageSrc(url, width = 0, height = 0, format = "match") {
331
329
  }
332
330
  const qs = querystring.stringify(options);
333
331
  if (pHash) {
334
- if (fileExtension) {
335
- return `${proxyBase}/p/${pHash}${format === "webp" ? ".webp" : ".png"}?${qs}`;
336
- } else {
337
- return `${proxyBase}/p/${pHash}?${qs}`;
338
- }
332
+ return `${proxyBase}/p/${pHash}?${qs}`;
339
333
  }
340
334
  const b58url = multihash.toB58String(Buffer.from(realUrl.toString()));
341
- return `${proxyBase}/p/${b58url}${fileExtension ? format === "webp" ? ".webp" : ".png" : ""}?${qs}`;
335
+ return `${proxyBase}/p/${b58url}?${qs}`;
342
336
  }
343
337
 
344
338
  // src/methods/img.method.ts
345
- function img(el, webp, state) {
346
- let src = el.getAttribute("src") || "";
339
+ function img(el, state) {
340
+ const src = el.getAttribute("src") || "";
347
341
  const decodedSrc = decodeURIComponent(
348
342
  src.replace(/&#(\d+);/g, (_, dec) => String.fromCharCode(dec)).replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
349
343
  ).trim();
344
+ ["onerror", "dynsrc", "lowsrc", "width", "height"].forEach((attr) => el.removeAttribute(attr));
350
345
  const isInvalid = !src || decodedSrc.startsWith("javascript") || decodedSrc.startsWith("vbscript") || decodedSrc === "x";
351
346
  if (isInvalid) {
352
- src = "";
347
+ el.removeAttribute("src");
348
+ return;
353
349
  }
354
350
  const isRelative = !/^https?:\/\//i.test(decodedSrc) && !decodedSrc.startsWith("/");
355
351
  if (isRelative) {
356
- src = "";
352
+ el.removeAttribute("src");
353
+ return;
357
354
  }
358
- ["onerror", "dynsrc", "lowsrc", "width", "height"].forEach((attr) => el.removeAttribute(attr));
359
355
  el.setAttribute("itemprop", "image");
360
356
  const isLCP = state && !state.firstImageFound;
361
357
  if (isLCP) {
@@ -370,14 +366,17 @@ function img(el, webp, state) {
370
366
  const shouldReplace = !cls.includes("no-replace");
371
367
  const hasAlreadyProxied = src.startsWith("https://images.ecency.com");
372
368
  if (shouldReplace && !hasAlreadyProxied) {
373
- const proxified = proxifyImageSrc(src, 0, 0, webp ? "webp" : "match");
374
- el.setAttribute("src", proxified);
369
+ const proxified = proxifyImageSrc(decodedSrc);
370
+ if (proxified) {
371
+ el.setAttribute("src", proxified);
372
+ }
375
373
  }
376
374
  }
377
- function createImageHTML(src, isLCP, webp) {
375
+ function createImageHTML(src, isLCP) {
376
+ const proxified = proxifyImageSrc(src);
377
+ if (!proxified) return "";
378
378
  const loading = isLCP ? "eager" : "lazy";
379
379
  const fetch = isLCP ? 'fetchpriority="high"' : 'decoding="async"';
380
- const proxified = proxifyImageSrc(src, 0, 0, webp ? "webp" : "match");
381
380
  return `<img
382
381
  class="markdown-img-link"
383
382
  src="${proxified}"
@@ -429,7 +428,7 @@ var addLineBreakBeforePostLink = (el, forApp, isInline) => {
429
428
  el.parentNode.insertBefore(br, el);
430
429
  }
431
430
  };
432
- function a(el, forApp, webp, parentDomain = "ecency.com", seoContext) {
431
+ function a(el, forApp, parentDomain = "ecency.com", seoContext) {
433
432
  if (!el || !el.parentNode) {
434
433
  return;
435
434
  }
@@ -447,10 +446,10 @@ function a(el, forApp, webp, parentDomain = "ecency.com", seoContext) {
447
446
  }
448
447
  if (href.match(IMG_REGEX) && href.trim().replace(/&amp;/g, "&") === getSerializedInnerHTML(el).trim().replace(/&amp;/g, "&")) {
449
448
  const isLCP = false;
450
- const imgHTML = createImageHTML(href, isLCP, webp);
449
+ const imgHTML = createImageHTML(href, isLCP);
451
450
  const doc = DOMParser.parseFromString(imgHTML, "text/html");
452
451
  const replaceNode = doc.body?.firstChild || doc.firstChild;
453
- if (replaceNode) {
452
+ if (replaceNode && el.parentNode) {
454
453
  const importedNode = el.ownerDocument.importNode(replaceNode, true);
455
454
  el.parentNode.replaceChild(importedNode, el);
456
455
  }
@@ -769,7 +768,7 @@ function a(el, forApp, webp, parentDomain = "ecency.com", seoContext) {
769
768
  el.setAttribute("class", "markdown-video-link markdown-video-link-youtube");
770
769
  el.removeAttribute("href");
771
770
  const vid = match[1];
772
- const thumbnail = proxifyImageSrc(`https://img.youtube.com/vi/${vid.split("?")[0]}/hqdefault.jpg`, 0, 0, webp ? "webp" : "match");
771
+ const thumbnail = proxifyImageSrc(`https://img.youtube.com/vi/${vid.split("?")[0]}/hqdefault.jpg`, 0, 0, "match");
773
772
  const embedSrc = `https://www.youtube.com/embed/${vid}?autoplay=1`;
774
773
  el.textContent = "";
775
774
  el.setAttribute("data-embed-src", embedSrc);
@@ -865,7 +864,7 @@ function a(el, forApp, webp, parentDomain = "ecency.com", seoContext) {
865
864
  if (imgEls.length === 1) {
866
865
  const src = imgEls[0].getAttribute("src");
867
866
  if (src) {
868
- const thumbnail = proxifyImageSrc(src.replace(/\s+/g, ""), 0, 0, webp ? "webp" : "match");
867
+ const thumbnail = proxifyImageSrc(src.replace(/\s+/g, ""), 0, 0, "match");
869
868
  const thumbImg = el.ownerDocument.createElement("img");
870
869
  thumbImg.setAttribute("class", "no-replace video-thumbnail");
871
870
  thumbImg.setAttribute("itemprop", "thumbnailUrl");
@@ -900,7 +899,7 @@ function a(el, forApp, webp, parentDomain = "ecency.com", seoContext) {
900
899
  const imgEls2 = el.getElementsByTagName("img");
901
900
  if (imgEls2.length === 1 || el.textContent.trim() === href) {
902
901
  if ((match[1] || match[2]) && match[3]) {
903
- const videoHref = `https://play.3speak.tv/watch?v=${match[3]}&mode=iframe`;
902
+ const videoHref = `https://play.3speak.tv/embed?v=${match[3]}&mode=iframe`;
904
903
  el.setAttribute("class", "markdown-video-link markdown-video-link-speak");
905
904
  el.removeAttribute("href");
906
905
  el.setAttribute("data-embed-src", videoHref);
@@ -910,7 +909,7 @@ function a(el, forApp, webp, parentDomain = "ecency.com", seoContext) {
910
909
  if (imgEls2.length === 1) {
911
910
  const src = imgEls2[0].getAttribute("src");
912
911
  if (src) {
913
- const thumbnail = proxifyImageSrc(src.replace(/\s+/g, ""), 0, 0, webp ? "webp" : "match");
912
+ const thumbnail = proxifyImageSrc(src.replace(/\s+/g, ""), 0, 0, "match");
914
913
  const thumbImg = el.ownerDocument.createElement("img");
915
914
  thumbImg.setAttribute("class", "no-replace video-thumbnail");
916
915
  thumbImg.setAttribute("itemprop", "thumbnailUrl");
@@ -944,7 +943,9 @@ function a(el, forApp, webp, parentDomain = "ecency.com", seoContext) {
944
943
  blockquote.appendChild(p2);
945
944
  blockquote.appendChild(textNode);
946
945
  blockquote.appendChild(a2);
947
- el.parentNode.replaceChild(blockquote, el);
946
+ if (el.parentNode) {
947
+ el.parentNode.replaceChild(blockquote, el);
948
+ }
948
949
  return;
949
950
  }
950
951
  }
@@ -1033,7 +1034,8 @@ function iframe(el, parentDomain = "ecency.com") {
1033
1034
  return;
1034
1035
  }
1035
1036
  if (src.match(SPEAK_EMBED_REGEX)) {
1036
- let normalizedSrc = src.replace(/3speak\.[a-z]+/i, "play.3speak.tv");
1037
+ let normalizedSrc = src.replace(/(?:play\.)?3speak\.[a-z]+/i, "play.3speak.tv");
1038
+ normalizedSrc = normalizedSrc.replace(/\/watch\?/, "/embed?");
1037
1039
  const hasMode = /[?&]mode=/.test(normalizedSrc);
1038
1040
  if (!hasMode) {
1039
1041
  normalizedSrc = `${normalizedSrc}&mode=iframe`;
@@ -1041,6 +1043,7 @@ function iframe(el, parentDomain = "ecency.com") {
1041
1043
  const hasAutoplay = /[?&]autoplay=/.test(normalizedSrc);
1042
1044
  const s = hasAutoplay ? normalizedSrc : `${normalizedSrc}&autoplay=true`;
1043
1045
  el.setAttribute("src", s);
1046
+ el.setAttribute("class", "speak-iframe");
1044
1047
  return;
1045
1048
  }
1046
1049
  if (src.match(SPOTIFY_EMBED_REGEX)) {
@@ -1149,7 +1152,7 @@ function p(el) {
1149
1152
  }
1150
1153
 
1151
1154
  // src/methods/linkify.method.ts
1152
- function linkify(content, forApp, webp) {
1155
+ function linkify(content, forApp) {
1153
1156
  content = content.replace(/(^|\s|>)(#[-a-z\d]+)/gi, (tag) => {
1154
1157
  if (/#[\d]+$/.test(tag)) return tag;
1155
1158
  const preceding = /^\s|>/.test(tag) ? tag[0] : "";
@@ -1191,21 +1194,21 @@ function linkify(content, forApp, webp) {
1191
1194
  content = content.replace(IMG_REGEX, (imglink) => {
1192
1195
  const isLCP = !firstImageUsed;
1193
1196
  firstImageUsed = true;
1194
- return createImageHTML(imglink, isLCP, webp);
1197
+ return createImageHTML(imglink, isLCP);
1195
1198
  });
1196
1199
  return content;
1197
1200
  }
1198
1201
 
1199
1202
  // src/methods/text.method.ts
1200
- function text(node, forApp, webp) {
1203
+ function text(node, forApp) {
1201
1204
  if (!node || !node.parentNode) {
1202
1205
  return;
1203
1206
  }
1204
- if (node.parentNode && ["a", "code"].includes(node.parentNode.nodeName.toLowerCase())) {
1207
+ if (["a", "code"].includes(node.parentNode.nodeName.toLowerCase())) {
1205
1208
  return;
1206
1209
  }
1207
1210
  const nodeValue = node.nodeValue || "";
1208
- const linkified = linkify(nodeValue, forApp, webp);
1211
+ const linkified = linkify(nodeValue, forApp);
1209
1212
  if (linkified !== nodeValue) {
1210
1213
  const doc = DOMParser.parseFromString(
1211
1214
  `<span class="wr">${linkified}</span>`,
@@ -1220,7 +1223,7 @@ function text(node, forApp, webp) {
1220
1223
  }
1221
1224
  if (nodeValue.match(IMG_REGEX)) {
1222
1225
  const isLCP = false;
1223
- const imageHTML = createImageHTML(nodeValue, isLCP, webp);
1226
+ const imageHTML = createImageHTML(nodeValue, isLCP);
1224
1227
  const doc = DOMParser.parseFromString(imageHTML, "text/html");
1225
1228
  const replaceNode = doc.body?.firstChild || doc.firstChild;
1226
1229
  if (replaceNode) {
@@ -1232,7 +1235,7 @@ function text(node, forApp, webp) {
1232
1235
  const e = YOUTUBE_REGEX.exec(nodeValue);
1233
1236
  if (e && e[1]) {
1234
1237
  const vid = e[1];
1235
- const thumbnail = proxifyImageSrc(`https://img.youtube.com/vi/${vid.split("?")[0]}/hqdefault.jpg`, 0, 0, webp ? "webp" : "match");
1238
+ const thumbnail = proxifyImageSrc(`https://img.youtube.com/vi/${vid.split("?")[0]}/hqdefault.jpg`, 0, 0, "match");
1236
1239
  const embedSrc = `https://www.youtube.com/embed/${vid}?autoplay=1`;
1237
1240
  const startTime = extractYtStartTime(nodeValue);
1238
1241
  const container = node.ownerDocument.createElement("p");
@@ -1278,7 +1281,7 @@ function text(node, forApp, webp) {
1278
1281
  }
1279
1282
 
1280
1283
  // src/methods/traverse.method.ts
1281
- function traverse(node, forApp, depth = 0, webp = false, state = { firstImageFound: false }, parentDomain = "ecency.com", seoContext) {
1284
+ function traverse(node, forApp, depth = 0, state = { firstImageFound: false }, parentDomain = "ecency.com", seoContext) {
1282
1285
  if (!node || !node.childNodes) {
1283
1286
  return;
1284
1287
  }
@@ -1286,23 +1289,23 @@ function traverse(node, forApp, depth = 0, webp = false, state = { firstImageFou
1286
1289
  const child = node.childNodes[i];
1287
1290
  if (!child) return;
1288
1291
  if (child.nodeName.toLowerCase() === "a") {
1289
- a(child, forApp, webp, parentDomain, seoContext);
1292
+ a(child, forApp, parentDomain, seoContext);
1290
1293
  }
1291
1294
  if (child.nodeName.toLowerCase() === "iframe") {
1292
1295
  iframe(child, parentDomain);
1293
1296
  }
1294
1297
  if (child.nodeName === "#text") {
1295
- text(child, forApp, webp);
1298
+ text(child, forApp);
1296
1299
  }
1297
1300
  if (child.nodeName.toLowerCase() === "img") {
1298
- img(child, webp, state);
1301
+ img(child, state);
1299
1302
  }
1300
1303
  if (child.nodeName.toLowerCase() === "p") {
1301
1304
  p(child);
1302
1305
  }
1303
1306
  const currentChild = node.childNodes[i];
1304
1307
  if (currentChild) {
1305
- traverse(currentChild, forApp, depth + 1, webp, state, parentDomain, seoContext);
1308
+ traverse(currentChild, forApp, depth + 1, state, parentDomain, seoContext);
1306
1309
  }
1307
1310
  });
1308
1311
  }
@@ -1353,7 +1356,7 @@ function fixBlockLevelTagsInParagraphs(html) {
1353
1356
  html = html.replace(/<p><br>\s*<\/p>/g, "");
1354
1357
  return html;
1355
1358
  }
1356
- function markdownToHTML(input, forApp, webp, parentDomain = "ecency.com", seoContext) {
1359
+ function markdownToHTML(input, forApp, parentDomain = "ecency.com", seoContext) {
1357
1360
  input = input.replace(new RegExp("https://leofinance.io/threads/view/", "g"), "/@");
1358
1361
  input = input.replace(new RegExp("https://leofinance.io/posts/", "g"), "/@");
1359
1362
  input = input.replace(new RegExp("https://leofinance.io/threads/", "g"), "/@");
@@ -1413,7 +1416,7 @@ function markdownToHTML(input, forApp, webp, parentDomain = "ecency.com", seoCon
1413
1416
  output = md.render(input);
1414
1417
  output = fixBlockLevelTagsInParagraphs(output);
1415
1418
  const doc = DOMParser.parseFromString(`<body id="root">${removeDuplicateAttributes(output)}</body>`, "text/html");
1416
- traverse(doc, forApp, 0, webp, { firstImageFound: false }, parentDomain, seoContext);
1419
+ traverse(doc, forApp, 0, { firstImageFound: false }, parentDomain, seoContext);
1417
1420
  output = serializer.serializeToString(doc);
1418
1421
  } catch (error) {
1419
1422
  try {
@@ -1426,7 +1429,7 @@ function markdownToHTML(input, forApp, webp, parentDomain = "ecency.com", seoCon
1426
1429
  });
1427
1430
  const repairedHtml = domSerializer(dom.children);
1428
1431
  const doc = DOMParser.parseFromString(`<body id="root">${removeDuplicateAttributes(repairedHtml)}</body>`, "text/html");
1429
- traverse(doc, forApp, 0, webp, { firstImageFound: false }, parentDomain, seoContext);
1432
+ traverse(doc, forApp, 0, { firstImageFound: false }, parentDomain, seoContext);
1430
1433
  output = serializer.serializeToString(doc);
1431
1434
  } catch (fallbackError) {
1432
1435
  const escapedContent = he2.encode(output || md.render(input));
@@ -1454,18 +1457,18 @@ function cacheSet(key, value) {
1454
1457
  }
1455
1458
 
1456
1459
  // src/markdown-2-html.ts
1457
- function markdown2Html(obj, forApp = true, webp = false, parentDomain = "ecency.com", seoContext) {
1460
+ function markdown2Html(obj, forApp = true, _webp = false, parentDomain = "ecency.com", seoContext) {
1458
1461
  if (typeof obj === "string") {
1459
1462
  const cleanedStr = cleanReply(obj);
1460
- return markdownToHTML(cleanedStr, forApp, webp, parentDomain, seoContext);
1463
+ return markdownToHTML(cleanedStr, forApp, parentDomain, seoContext);
1461
1464
  }
1462
- const key = `${makeEntryCacheKey(obj)}-md${webp ? "-webp" : ""}-${forApp ? "app" : "site"}-${parentDomain}${seoContext ? `-seo${seoContext.authorReputation ?? ""}-${seoContext.postPayout ?? ""}` : ""}`;
1465
+ const key = `${makeEntryCacheKey(obj)}-md-${forApp ? "app" : "site"}-${parentDomain}${seoContext ? `-seo${seoContext.authorReputation ?? ""}-${seoContext.postPayout ?? ""}` : ""}`;
1463
1466
  const item = cacheGet(key);
1464
1467
  if (item) {
1465
1468
  return item;
1466
1469
  }
1467
1470
  const cleanBody = cleanReply(obj.body);
1468
- const res = markdownToHTML(cleanBody, forApp, webp, parentDomain, seoContext);
1471
+ const res = markdownToHTML(cleanBody, forApp, parentDomain, seoContext);
1469
1472
  cacheSet(key, res);
1470
1473
  return res;
1471
1474
  }