@ecency/render-helper 2.5.10 → 2.5.12

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.
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var xmldom = require('@xmldom/xmldom');
4
+ var he2 = require('he');
4
5
  var xss = require('xss');
5
6
  var multihash = require('multihashes');
6
7
  var querystring = require('querystring');
@@ -9,7 +10,6 @@ var remarkable = require('remarkable');
9
10
  var linkify$1 = require('remarkable/linkify');
10
11
  var htmlparser2 = require('htmlparser2');
11
12
  var domSerializerModule = require('dom-serializer');
12
- var he = require('he');
13
13
 
14
14
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
15
15
 
@@ -31,12 +31,12 @@ function _interopNamespace(e) {
31
31
  return Object.freeze(n);
32
32
  }
33
33
 
34
+ var he2__default = /*#__PURE__*/_interopDefault(he2);
34
35
  var xss__default = /*#__PURE__*/_interopDefault(xss);
35
36
  var multihash__default = /*#__PURE__*/_interopDefault(multihash);
36
37
  var querystring__default = /*#__PURE__*/_interopDefault(querystring);
37
38
  var htmlparser2__namespace = /*#__PURE__*/_interopNamespace(htmlparser2);
38
39
  var domSerializerModule__namespace = /*#__PURE__*/_interopNamespace(domSerializerModule);
39
- var he__default = /*#__PURE__*/_interopDefault(he);
40
40
 
41
41
  // src/consts/white-list.const.ts
42
42
  var WHITE_LIST = [
@@ -162,6 +162,13 @@ var ALLOWED_ATTRIBUTES = {
162
162
  "decoding",
163
163
  "itemprop"
164
164
  ],
165
+ // Responsive image content-negotiation wrapper emitted for web/self-hosted
166
+ // (forApp === false). Without these entries `xss` silently collapses the
167
+ // <picture>/<source> to a bare <img>. `source` attrs are further constrained
168
+ // in sanitize-html (srcset must be a proxy /p/ URL; type must be avif/webp;
169
+ // a type-less <source> is dropped post-pass).
170
+ "picture": [],
171
+ "source": ["type", "srcset", "sizes"],
165
172
  "span": ["class", "id", "data-align"],
166
173
  "iframe": ["src", "class", "frameborder", "allowfullscreen", "webkitallowfullscreen", "mozallowfullscreen", "sandbox"],
167
174
  "video": ["src", "controls", "poster"],
@@ -198,6 +205,96 @@ var ALLOWED_ATTRIBUTES = {
198
205
  "del": [],
199
206
  "ins": []
200
207
  };
208
+
209
+ // src/consts/embed-hosts.const.ts
210
+ var ALLOWED_EMBED_HOSTS = /* @__PURE__ */ new Set([
211
+ // YouTube
212
+ "www.youtube.com",
213
+ "youtube.com",
214
+ "www.youtube-nocookie.com",
215
+ "youtube-nocookie.com",
216
+ // Vimeo
217
+ "player.vimeo.com",
218
+ // Twitch
219
+ "player.twitch.tv",
220
+ // DTube
221
+ "emb.d.tube",
222
+ // 3Speak (video + audio)
223
+ "play.3speak.tv",
224
+ "3speak.tv",
225
+ "audio.3speak.tv",
226
+ // Loom
227
+ "www.loom.com",
228
+ // Spotify
229
+ "open.spotify.com",
230
+ // SoundCloud
231
+ "w.soundcloud.com",
232
+ // BitChute
233
+ "www.bitchute.com",
234
+ "bitchute.com",
235
+ // Rumble
236
+ "www.rumble.com",
237
+ "rumble.com",
238
+ // Brighteon
239
+ "www.brighteon.com",
240
+ "brighteon.com",
241
+ // VIMM
242
+ "www.vimm.tv",
243
+ // BrandNewTube
244
+ "brandnewtube.com",
245
+ // LBRY / Odysee
246
+ "lbry.tv",
247
+ "odysee.com",
248
+ // Skatehive / Skatehype
249
+ "ipfs.skatehive.app",
250
+ "www.skatehype.com",
251
+ "skatehype.com",
252
+ // archive.org
253
+ "archive.org",
254
+ // Truvvl
255
+ "embed.truvvl.com",
256
+ // Aureal
257
+ "aureal-embed.web.app",
258
+ "www.aureal-embed.web.app"
259
+ // Dapplr (player.*.dapplr.in / *.dapplr.in) — host suffix, handled below
260
+ ]);
261
+ var ALLOWED_EMBED_HOST_SUFFIXES = [".dapplr.in"];
262
+ var EMBED_HOST_PATH_PATTERNS = {
263
+ "www.youtube.com": /^\/embed\//,
264
+ "youtube.com": /^\/embed\//,
265
+ "www.youtube-nocookie.com": /^\/embed\//,
266
+ "youtube-nocookie.com": /^\/embed\//,
267
+ "player.vimeo.com": /^\/video\//,
268
+ "player.twitch.tv": /^\/$/,
269
+ // channel/video carried in the query string
270
+ "emb.d.tube": /^\/$/,
271
+ // dtube carries the ref in the #! fragment
272
+ "play.3speak.tv": /^\/(watch|embed)/,
273
+ "open.spotify.com": /^\/embed\//,
274
+ "www.loom.com": /^\/embed\//,
275
+ "www.bitchute.com": /^\/embed\//,
276
+ "bitchute.com": /^\/embed\//,
277
+ "www.rumble.com": /^\/embed\//,
278
+ "rumble.com": /^\/embed\//,
279
+ "www.brighteon.com": /^\/embed\//,
280
+ "brighteon.com": /^\/embed\//
281
+ };
282
+ function isAllowedEmbedSrc(value) {
283
+ if (!value) return false;
284
+ let url;
285
+ try {
286
+ url = new URL(value.trim());
287
+ } catch {
288
+ return false;
289
+ }
290
+ if (url.protocol !== "https:") return false;
291
+ const host = url.hostname.toLowerCase();
292
+ const hostAllowed = ALLOWED_EMBED_HOSTS.has(host) || ALLOWED_EMBED_HOST_SUFFIXES.some((suffix) => host.endsWith(suffix));
293
+ if (!hostAllowed) return false;
294
+ const pathPattern = EMBED_HOST_PATH_PATTERNS[host];
295
+ if (pathPattern && !pathPattern.test(url.pathname)) return false;
296
+ return true;
297
+ }
201
298
  function createParser() {
202
299
  return new xmldom.DOMParser({
203
300
  onError(level, msg) {
@@ -205,8 +302,14 @@ function createParser() {
205
302
  });
206
303
  }
207
304
  var DOMParser = createParser();
208
-
209
- // src/helper.ts
305
+ function decodeImageSrc(src) {
306
+ const entityDecoded = he2__default.default.decode(src);
307
+ try {
308
+ return decodeURIComponent(entityDecoded).trim();
309
+ } catch {
310
+ return entityDecoded.trim();
311
+ }
312
+ }
210
313
  function isSpaceChar(c) {
211
314
  return c === 32 || c === 9 || c === 10 || c === 13 || c === 12;
212
315
  }
@@ -462,33 +565,6 @@ function removeChildNodes(node) {
462
565
  node.removeChild(node.firstChild);
463
566
  }
464
567
  }
465
- var decodeEntities = (input) => input.replace(/&#(\d+);?/g, (_, dec) => String.fromCodePoint(Number(dec))).replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)));
466
- function sanitizeHtml(html) {
467
- return xss__default.default(html, {
468
- whiteList: ALLOWED_ATTRIBUTES,
469
- stripIgnoreTag: true,
470
- stripIgnoreTagBody: ["style"],
471
- css: false,
472
- // block style attrs entirely for safety
473
- onTagAttr: (tag, name, value) => {
474
- const decoded = decodeEntities(value.trim());
475
- const decodedLower = decoded.toLowerCase();
476
- if (name.startsWith("on")) return "";
477
- if (tag === "img" && name === "src" && !/^https?:\/\//.test(decodedLower)) return "";
478
- if (tag === "img" && name === "srcset") {
479
- const candidates = decoded.split(",").map((c) => c.trim().split(/\s+/)[0]);
480
- if (candidates.some((url) => !/^https?:\/\//i.test(url))) return "";
481
- }
482
- if (tag === "video" && ["src", "poster"].includes(name) && !/^https?:\/\//.test(decodedLower)) return "";
483
- if (tag === "img" && ["dynsrc", "lowsrc"].includes(name)) return "";
484
- if (tag === "span" && name === "class" && decoded.toLowerCase().trim() === "wr") return "";
485
- if (name === "id") {
486
- if (!ID_WHITELIST.test(decoded)) return "";
487
- }
488
- return void 0;
489
- }
490
- });
491
- }
492
568
  var proxyBase = "https://i.ecency.com";
493
569
  var urlHashCache = new lruCache.LRUCache({ max: 500 });
494
570
  function getUrlHash(url) {
@@ -524,7 +600,7 @@ function getLatestUrl(str) {
524
600
  const [last] = [...str.replace(/https?:\/\//g, "\n$&").trim().split("\n")].reverse();
525
601
  return last;
526
602
  }
527
- function proxifyImageSrc(url, width = 0, height = 0, _format = "match", opts = {}) {
603
+ function proxifyForFormat(url, width = 0, height = 0, format = "match", opts = {}) {
528
604
  if (!url || typeof url !== "string" || !isValidUrl(url)) {
529
605
  return "";
530
606
  }
@@ -541,7 +617,7 @@ function proxifyImageSrc(url, width = 0, height = 0, _format = "match", opts = {
541
617
  const realUrl = getLatestUrl(url);
542
618
  const pHash = extractPHash(realUrl);
543
619
  const options = {
544
- format: "match",
620
+ format,
545
621
  mode: "fit"
546
622
  };
547
623
  if (width > 0) {
@@ -560,29 +636,137 @@ function proxifyImageSrc(url, width = 0, height = 0, _format = "match", opts = {
560
636
  const b58url = getUrlHash(realUrl.toString());
561
637
  return `${proxyBase}/p/${b58url}?${qs}`;
562
638
  }
639
+ function proxifyImageSrc(url, width = 0, height = 0, _format = "match", opts = {}) {
640
+ return proxifyForFormat(url, width, height, "match", opts);
641
+ }
563
642
  var SRCSET_WIDTHS = [320, 600, 800, 1024, 1280];
564
643
  function buildSrcSet(url) {
644
+ return buildSrcSetForFormat(url, "match");
645
+ }
646
+ function buildSrcSetForFormat(url, format = "match") {
565
647
  if (!url || typeof url !== "string") return "";
566
648
  const proxyPrefix = `${proxyBase}/p/`;
649
+ let result;
567
650
  if (url.startsWith(proxyPrefix)) {
568
651
  const rest = url.slice(proxyPrefix.length);
569
652
  const q = rest.indexOf("?");
570
653
  const phash = extractPHash(url) || (q >= 0 ? rest.slice(0, q) : rest);
571
- return SRCSET_WIDTHS.map((w) => `${proxyBase}/p/${phash}?format=match&mode=fit&width=${w} ${w}w`).join(", ");
654
+ result = SRCSET_WIDTHS.map((w) => `${proxyBase}/p/${phash}?format=${format}&mode=fit&width=${w} ${w}w`).join(", ");
655
+ } else {
656
+ result = SRCSET_WIDTHS.map((w) => {
657
+ const proxied = proxifyForFormat(url, w, 0, format);
658
+ return proxied ? `${proxied} ${w}w` : "";
659
+ }).filter(Boolean).join(", ");
660
+ }
661
+ if (format !== "match" && result && !result.split(",").every((c) => c.includes(`format=${format}`))) {
662
+ return "";
572
663
  }
573
- return SRCSET_WIDTHS.map((w) => {
574
- const proxied = proxifyImageSrc(url, w);
575
- return proxied ? `${proxied} ${w}w` : "";
576
- }).filter(Boolean).join(", ");
664
+ return result;
665
+ }
666
+ var STATIC_RASTER_PATH_EXT = /\.(?:jpe?g|png|webp)$/i;
667
+ var SIZED_PROXY_PATH = /^\/\d+x\d+\//;
668
+ function isPictureEligibleRawUrl(rawUrl) {
669
+ if (!rawUrl || typeof rawUrl !== "string") return false;
670
+ let u;
671
+ try {
672
+ u = new URL(rawUrl);
673
+ } catch {
674
+ return false;
675
+ }
676
+ if (u.protocol !== "http:" && u.protocol !== "https:") return false;
677
+ const host = `${u.protocol}//${u.host}`;
678
+ const isProxyHost = host === proxyBase || host === "https://images.ecency.com";
679
+ if (isProxyHost && (u.pathname.startsWith("/p/") || u.pathname.startsWith("/u/") || SIZED_PROXY_PATH.test(u.pathname))) {
680
+ return false;
681
+ }
682
+ return STATIC_RASTER_PATH_EXT.test(u.pathname);
683
+ }
684
+ function buildPictureSources(rawUrl) {
685
+ if (!isPictureEligibleRawUrl(rawUrl)) return null;
686
+ const avif = buildSrcSetForFormat(rawUrl, "avif");
687
+ const webp = buildSrcSetForFormat(rawUrl, "webp");
688
+ if (!avif || !webp) return null;
689
+ return { avif, webp };
690
+ }
691
+
692
+ // src/methods/sanitize-html.method.ts
693
+ var EMBED_SRC_DATA_ATTRS = /* @__PURE__ */ new Set(["data-embed-src", "data-video-href"]);
694
+ var isSafeNavValue = (value) => {
695
+ const trimmed = value.trim().replace(/[\t\n\r\f\v\0]/g, "").toLowerCase();
696
+ if (!trimmed) return false;
697
+ const isSafeScheme = /^(https?|mailto|hive|tel|web\+[a-z0-9.+-]+):/i.test(trimmed);
698
+ const isRelative = /^(\/\/|\/[^/]?|#|\?|[a-z0-9._\-]+(\/|$))/i.test(trimmed);
699
+ return isSafeScheme || isRelative;
700
+ };
701
+ var decodeEntities = (input) => input.replace(/&#(\d+);?/g, (_, dec) => String.fromCodePoint(Number(dec))).replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)));
702
+ var isProxyPSrcset = (srcset) => {
703
+ const base = trimTrailingSlash(getProxyBase());
704
+ const candidates = srcset.split(",").map((c) => c.trim().split(/\s+/)[0]).filter(Boolean);
705
+ return candidates.length > 0 && candidates.every((url) => url.startsWith(`${base}/p/`));
706
+ };
707
+ function sanitizeHtml(html) {
708
+ const cleaned = xss__default.default(html, {
709
+ whiteList: ALLOWED_ATTRIBUTES,
710
+ stripIgnoreTag: true,
711
+ stripIgnoreTagBody: ["style"],
712
+ css: false,
713
+ // block style attrs entirely for safety
714
+ onTagAttr: (tag, name, value) => {
715
+ const decoded = decodeEntities(value.trim());
716
+ const decodedLower = decoded.toLowerCase();
717
+ if (name.startsWith("on")) return "";
718
+ if (tag === "img" && name === "src" && !/^https?:\/\//.test(decodedLower)) return "";
719
+ if (tag === "img" && name === "srcset") {
720
+ const candidates = decoded.split(",").map((c) => c.trim().split(/\s+/)[0]);
721
+ if (candidates.some((url) => !/^https?:\/\//i.test(url))) return "";
722
+ }
723
+ if (tag === "source" && name === "srcset" && !isProxyPSrcset(decoded)) return "";
724
+ if (tag === "source" && name === "type" && decodedLower !== "image/avif" && decodedLower !== "image/webp") return "";
725
+ if (tag === "video" && ["src", "poster"].includes(name) && !/^https?:\/\//.test(decodedLower)) return "";
726
+ if (tag === "img" && ["dynsrc", "lowsrc"].includes(name)) return "";
727
+ if (tag === "span" && name === "class" && decoded.toLowerCase().trim() === "wr") return "";
728
+ if (EMBED_SRC_DATA_ATTRS.has(name) && !isAllowedEmbedSrc(decoded)) return "";
729
+ if (name === "data-href" && !isSafeNavValue(decoded)) return "";
730
+ if (name === "id") {
731
+ if (!ID_WHITELIST.test(decoded)) return "";
732
+ }
733
+ return void 0;
734
+ }
735
+ });
736
+ return cleaned.replace(
737
+ /<source\b(?:[^>"']|"[^"]*"|'[^']*')*>/gi,
738
+ (t) => /\btype\s*=\s*["'](?:image\/avif|image\/webp)["']/i.test(t) ? t : ""
739
+ );
577
740
  }
578
741
 
579
742
  // src/methods/img.method.ts
580
743
  var IMAGE_SIZES = "(max-width: 768px) 100vw, 700px";
581
- function img(el, state) {
744
+ function wrapInPicture(el, rawUrl) {
745
+ const parent = el.parentNode;
746
+ if (!parent) return;
747
+ if (parent.nodeName && parent.nodeName.toLowerCase() === "picture") return;
748
+ const sources = buildPictureSources(rawUrl);
749
+ if (!sources) return;
750
+ const doc = el.ownerDocument;
751
+ if (!doc) return;
752
+ const sizes = el.getAttribute("sizes") || IMAGE_SIZES;
753
+ const picture = doc.createElement("picture");
754
+ const avif = doc.createElement("source");
755
+ avif.setAttribute("type", "image/avif");
756
+ avif.setAttribute("srcset", sources.avif);
757
+ avif.setAttribute("sizes", sizes);
758
+ const webp = doc.createElement("source");
759
+ webp.setAttribute("type", "image/webp");
760
+ webp.setAttribute("srcset", sources.webp);
761
+ webp.setAttribute("sizes", sizes);
762
+ parent.insertBefore(picture, el);
763
+ picture.appendChild(avif);
764
+ picture.appendChild(webp);
765
+ picture.appendChild(el);
766
+ }
767
+ function img(el, state, forApp = true) {
582
768
  const src = el.getAttribute("src") || "";
583
- const decodedSrc = decodeURIComponent(
584
- src.replace(/&#(\d+);/g, (_, dec) => String.fromCharCode(dec)).replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
585
- ).trim();
769
+ const decodedSrc = decodeImageSrc(src);
586
770
  ["onerror", "dynsrc", "lowsrc", "width", "height"].forEach((attr) => el.removeAttribute(attr));
587
771
  const isInvalid = !src || decodedSrc.startsWith("javascript") || decodedSrc.startsWith("vbscript") || decodedSrc === "x";
588
772
  if (isInvalid) {
@@ -621,6 +805,9 @@ function img(el, state) {
621
805
  el.setAttribute("srcset", srcset);
622
806
  el.setAttribute("sizes", IMAGE_SIZES);
623
807
  }
808
+ if (!forApp) {
809
+ wrapInPicture(el, decodedSrc);
810
+ }
624
811
  }
625
812
  } else if (shouldReplace && hasAlreadyProxied) {
626
813
  if (src.startsWith(`${base}/p/`)) {
@@ -632,16 +819,17 @@ function img(el, state) {
632
819
  }
633
820
  }
634
821
  }
635
- function createImageHTML(src, isLCP) {
636
- const proxified = proxifyImageSrc(src, 0, 0, "match", { forceProxy: true });
822
+ function createImageHTML(src, isLCP, forApp = true) {
823
+ const decoded = decodeImageSrc(src);
824
+ const proxified = proxifyImageSrc(decoded, 0, 0, "match", { forceProxy: true });
637
825
  if (!proxified) return "";
638
826
  const base = trimTrailingSlash(getProxyBase());
639
- const isAlreadyProxied = src.startsWith(`${base}/u/`) || new RegExp(`^${base.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/\\d+x\\d+/`).test(src);
640
- const srcset = isAlreadyProxied ? "" : buildSrcSet(src);
827
+ const isAlreadyProxied = decoded.startsWith(`${base}/u/`) || new RegExp(`^${base.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/\\d+x\\d+/`).test(decoded);
828
+ const srcset = isAlreadyProxied ? "" : buildSrcSet(decoded);
641
829
  const loading = isLCP ? "eager" : "lazy";
642
830
  const fetch = isLCP ? 'fetchpriority="high"' : 'decoding="async"';
643
831
  const srcsetAttr = srcset ? `srcset="${srcset}" sizes="${IMAGE_SIZES}"` : "";
644
- return `<img
832
+ const imgTag = `<img
645
833
  class="markdown-img-link"
646
834
  src="${proxified}"
647
835
  ${srcsetAttr}
@@ -649,6 +837,13 @@ function createImageHTML(src, isLCP) {
649
837
  ${fetch}
650
838
  itemprop="image"
651
839
  />`;
840
+ if (!forApp) {
841
+ const sources = buildPictureSources(decoded);
842
+ if (sources) {
843
+ return `<picture><source type="image/avif" srcset="${sources.avif}" sizes="${IMAGE_SIZES}" /><source type="image/webp" srcset="${sources.webp}" sizes="${IMAGE_SIZES}" />${imgTag}</picture>`;
844
+ }
845
+ }
846
+ return imgTag;
652
847
  }
653
848
 
654
849
  // src/methods/a.method.ts
@@ -715,7 +910,7 @@ function a(el, forApp, parentDomain = "ecency.com", seoContext, renderOptions) {
715
910
  }
716
911
  if (href.match(IMG_REGEX) && href.trim().replace(/&amp;/g, "&") === getSerializedInnerHTML(el).trim().replace(/&amp;/g, "&")) {
717
912
  const isLCP = false;
718
- const imgHTML = createImageHTML(href, isLCP);
913
+ const imgHTML = createImageHTML(href, isLCP, forApp);
719
914
  const doc = DOMParser.parseFromString(imgHTML, "text/html");
720
915
  const replaceNode = doc.body?.firstChild || doc.firstChild;
721
916
  if (replaceNode && el.parentNode) {
@@ -1554,7 +1749,7 @@ function linkify(content, forApp, renderOptions) {
1554
1749
  content = content.replace(IMG_REGEX, (imglink) => {
1555
1750
  const isLCP = !firstImageUsed;
1556
1751
  firstImageUsed = true;
1557
- return createImageHTML(imglink, isLCP);
1752
+ return createImageHTML(imglink, isLCP, forApp);
1558
1753
  });
1559
1754
  authorPlaceholders.forEach(({ placeholder, html }) => {
1560
1755
  content = content.replace(placeholder, html);
@@ -1596,7 +1791,7 @@ function text(node, forApp, renderOptions) {
1596
1791
  }
1597
1792
  if (nodeValue.match(IMG_REGEX)) {
1598
1793
  const isLCP = false;
1599
- const imageHTML = createImageHTML(nodeValue, isLCP);
1794
+ const imageHTML = createImageHTML(nodeValue, isLCP, forApp);
1600
1795
  const doc = DOMParser.parseFromString(imageHTML, "text/html");
1601
1796
  const replaceNode = doc.body?.firstChild || doc.firstChild;
1602
1797
  if (replaceNode) {
@@ -1672,7 +1867,7 @@ function traverse(node, forApp, depth = 0, state = { firstImageFound: false }, p
1672
1867
  text(child, forApp);
1673
1868
  }
1674
1869
  if (child.nodeName.toLowerCase() === "img") {
1675
- img(child, state);
1870
+ img(child, state, forApp);
1676
1871
  }
1677
1872
  if (child.nodeName.toLowerCase() === "p") {
1678
1873
  p(child);
@@ -1895,8 +2090,11 @@ var INLINE_CODE_RE = /`[^`\n]*`/g;
1895
2090
  var INDENTED_CODE_RE = /^(?: {4}|\t).+$/gm;
1896
2091
  var MD_IMAGE_RE = /!\[[^\]]*\]\(\s*([^)\s]+)(?:\s+["'][^"']*["'])?\s*\)/;
1897
2092
  var HTML_IMAGE_RE = /<img\b[^>]*?\bsrc\s*=\s*["']([^"']+)["']/i;
2093
+ var BARE_IMAGE_RE = /(^|\s)(https?:\/\/[^\s<>"'()[\]]+\.(?:tiff?|jpe?g|gif|png|svg|ico|heic|webp|arw)(?:[?#][^\s<>"'()[\]]*)?)/im;
2094
+ var MD_LINK_RE = /\[([^\]]*)\]\(\s*([^)\s]+)(?:\s+["'][^"']*["'])?\s*\)/g;
2095
+ var IMG_HREF_RE = /https?:\/\/.*\.(?:tiff?|jpe?g|gif|png|svg|ico|heic|webp|arw)/i;
1898
2096
  var SAFE_URL_RE = /^https?:\/\//i;
1899
- function findFirstImageUrl(body) {
2097
+ function findFirstImageUrl(body, includeBareUrls = false) {
1900
2098
  if (!body) return null;
1901
2099
  const cleaned = body.replace(BACKTICK_FENCE_RE, "").replace(TILDE_FENCE_RE, "").replace(INLINE_CODE_RE, "").replace(INDENTED_CODE_RE, "");
1902
2100
  const mdMatch = cleaned.match(MD_IMAGE_RE);
@@ -1907,17 +2105,33 @@ function findFirstImageUrl(body) {
1907
2105
  return null;
1908
2106
  }
1909
2107
  }
1910
- const mdValid = !!mdMatch;
1911
- const htmlValid = !!(htmlMatch && htmlMatch[1] && SAFE_URL_RE.test(htmlMatch[1]));
1912
- if (mdValid && htmlValid) {
1913
- return (mdMatch.index ?? 0) < (htmlMatch.index ?? 0) ? mdMatch[1] : htmlMatch[1];
2108
+ const candidates = [];
2109
+ if (mdMatch) candidates.push({ url: mdMatch[1], pos: mdMatch.index ?? 0 });
2110
+ if (htmlMatch && htmlMatch[1] && SAFE_URL_RE.test(htmlMatch[1])) {
2111
+ candidates.push({ url: htmlMatch[1], pos: htmlMatch.index ?? 0 });
1914
2112
  }
1915
- if (mdValid) return mdMatch[1];
1916
- if (htmlValid) return htmlMatch[1];
1917
- return null;
2113
+ if (includeBareUrls) {
2114
+ const bareMatch = cleaned.match(BARE_IMAGE_RE);
2115
+ if (bareMatch && bareMatch[2] && SAFE_URL_RE.test(bareMatch[2])) {
2116
+ candidates.push({ url: bareMatch[2], pos: (bareMatch.index ?? 0) + bareMatch[1].length });
2117
+ }
2118
+ const deAmp = (s) => s.trim().replace(/&amp;/g, "&");
2119
+ for (const m of cleaned.matchAll(MD_LINK_RE)) {
2120
+ const idx = m.index ?? 0;
2121
+ if (idx > 0 && cleaned[idx - 1] === "!") continue;
2122
+ const href = m[2];
2123
+ if (href && SAFE_URL_RE.test(href) && IMG_HREF_RE.test(href) && deAmp(m[1]) === deAmp(href)) {
2124
+ candidates.push({ url: href, pos: idx });
2125
+ break;
2126
+ }
2127
+ }
2128
+ }
2129
+ if (candidates.length === 0) return null;
2130
+ candidates.sort((a2, b) => a2.pos - b.pos);
2131
+ return candidates[0].url;
1918
2132
  }
1919
2133
  function proxifyFound(src, width, height, format) {
1920
- const decoded = he__default.default.decode(src);
2134
+ const decoded = he2__default.default.decode(src);
1921
2135
  if (isGifLink(decoded)) {
1922
2136
  return proxifyImageSrc(decoded, 0, 0, format);
1923
2137
  }
@@ -1935,7 +2149,7 @@ function getImage(entry, width = 0, height = 0, format = "match") {
1935
2149
  }
1936
2150
  }
1937
2151
  if (meta && typeof meta.image === "string" && meta.image.length > 0) {
1938
- const decodedImage = he__default.default.decode(meta.image);
2152
+ const decodedImage = he2__default.default.decode(meta.image);
1939
2153
  if (isGifLink(decodedImage)) {
1940
2154
  return proxifyImageSrc(decodedImage, 0, 0, format);
1941
2155
  }
@@ -1943,7 +2157,7 @@ function getImage(entry, width = 0, height = 0, format = "match") {
1943
2157
  }
1944
2158
  if (meta && meta.image && !!meta.image.length && meta.image[0]) {
1945
2159
  if (typeof meta.image[0] === "string") {
1946
- const decodedImage = he__default.default.decode(meta.image[0]);
2160
+ const decodedImage = he2__default.default.decode(meta.image[0]);
1947
2161
  if (isGifLink(decodedImage)) {
1948
2162
  return proxifyImageSrc(decodedImage, 0, 0, format);
1949
2163
  }
@@ -1973,6 +2187,30 @@ function getImage(entry, width = 0, height = 0, format = "match") {
1973
2187
  }
1974
2188
  return null;
1975
2189
  }
2190
+ function getEntryImageRawUrl(obj) {
2191
+ if (typeof obj === "string") {
2192
+ const src = findFirstImageUrl(obj, true);
2193
+ return src ? decodeImageSrc(src) : null;
2194
+ }
2195
+ let meta;
2196
+ if (typeof obj.json_metadata === "object") {
2197
+ meta = obj.json_metadata;
2198
+ } else {
2199
+ try {
2200
+ meta = JSON.parse(obj.json_metadata);
2201
+ } catch (e) {
2202
+ meta = null;
2203
+ }
2204
+ }
2205
+ if (meta && typeof meta.image === "string" && meta.image.length > 0) {
2206
+ return decodeImageSrc(meta.image);
2207
+ }
2208
+ if (meta && meta.image && !!meta.image.length && typeof meta.image[0] === "string" && meta.image[0].length > 0) {
2209
+ return decodeImageSrc(meta.image[0]);
2210
+ }
2211
+ const bodySrc = findFirstImageUrl(obj.body, true);
2212
+ return bodySrc ? decodeImageSrc(bodySrc) : null;
2213
+ }
1976
2214
  function catchPostImage(obj, width = 0, height = 0, format = "match") {
1977
2215
  if (typeof obj === "string") {
1978
2216
  const fast = findFirstImageUrl(obj);
@@ -2074,7 +2312,7 @@ function postBodySummary(entryBody, length = 200, platform = "web") {
2074
2312
  text2 = joint(text2.split(" "), length);
2075
2313
  }
2076
2314
  if (text2) {
2077
- text2 = he__default.default.decode(text2);
2315
+ text2 = he2__default.default.decode(text2);
2078
2316
  }
2079
2317
  return text2;
2080
2318
  }
@@ -2096,8 +2334,13 @@ function getPostBodySummary(obj, length, platform) {
2096
2334
 
2097
2335
  exports.IMAGE_SIZES = IMAGE_SIZES;
2098
2336
  exports.SECTION_LIST = SECTION_LIST;
2337
+ exports.buildPictureSources = buildPictureSources;
2099
2338
  exports.buildSrcSet = buildSrcSet;
2339
+ exports.buildSrcSetForFormat = buildSrcSetForFormat;
2100
2340
  exports.catchPostImage = catchPostImage;
2341
+ exports.getEntryImageRawUrl = getEntryImageRawUrl;
2342
+ exports.isAllowedEmbedSrc = isAllowedEmbedSrc;
2343
+ exports.isPictureEligibleRawUrl = isPictureEligibleRawUrl;
2101
2344
  exports.isValidPermlink = isValidPermlink;
2102
2345
  exports.postBodySummary = getPostBodySummary;
2103
2346
  exports.proxifyImageSrc = proxifyImageSrc;