@ecency/render-helper 2.5.9 → 2.5.11

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"],
@@ -205,8 +212,14 @@ function createParser() {
205
212
  });
206
213
  }
207
214
  var DOMParser = createParser();
208
-
209
- // src/helper.ts
215
+ function decodeImageSrc(src) {
216
+ const entityDecoded = he2__default.default.decode(src);
217
+ try {
218
+ return decodeURIComponent(entityDecoded).trim();
219
+ } catch {
220
+ return entityDecoded.trim();
221
+ }
222
+ }
210
223
  function isSpaceChar(c) {
211
224
  return c === 32 || c === 9 || c === 10 || c === 13 || c === 12;
212
225
  }
@@ -462,33 +475,6 @@ function removeChildNodes(node) {
462
475
  node.removeChild(node.firstChild);
463
476
  }
464
477
  }
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
478
  var proxyBase = "https://i.ecency.com";
493
479
  var urlHashCache = new lruCache.LRUCache({ max: 500 });
494
480
  function getUrlHash(url) {
@@ -524,7 +510,7 @@ function getLatestUrl(str) {
524
510
  const [last] = [...str.replace(/https?:\/\//g, "\n$&").trim().split("\n")].reverse();
525
511
  return last;
526
512
  }
527
- function proxifyImageSrc(url, width = 0, height = 0, _format = "match", opts = {}) {
513
+ function proxifyForFormat(url, width = 0, height = 0, format = "match", opts = {}) {
528
514
  if (!url || typeof url !== "string" || !isValidUrl(url)) {
529
515
  return "";
530
516
  }
@@ -541,7 +527,7 @@ function proxifyImageSrc(url, width = 0, height = 0, _format = "match", opts = {
541
527
  const realUrl = getLatestUrl(url);
542
528
  const pHash = extractPHash(realUrl);
543
529
  const options = {
544
- format: "match",
530
+ format,
545
531
  mode: "fit"
546
532
  };
547
533
  if (width > 0) {
@@ -560,29 +546,127 @@ function proxifyImageSrc(url, width = 0, height = 0, _format = "match", opts = {
560
546
  const b58url = getUrlHash(realUrl.toString());
561
547
  return `${proxyBase}/p/${b58url}?${qs}`;
562
548
  }
549
+ function proxifyImageSrc(url, width = 0, height = 0, _format = "match", opts = {}) {
550
+ return proxifyForFormat(url, width, height, "match", opts);
551
+ }
563
552
  var SRCSET_WIDTHS = [320, 600, 800, 1024, 1280];
564
553
  function buildSrcSet(url) {
554
+ return buildSrcSetForFormat(url, "match");
555
+ }
556
+ function buildSrcSetForFormat(url, format = "match") {
565
557
  if (!url || typeof url !== "string") return "";
566
558
  const proxyPrefix = `${proxyBase}/p/`;
559
+ let result;
567
560
  if (url.startsWith(proxyPrefix)) {
568
561
  const rest = url.slice(proxyPrefix.length);
569
562
  const q = rest.indexOf("?");
570
563
  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(", ");
564
+ result = SRCSET_WIDTHS.map((w) => `${proxyBase}/p/${phash}?format=${format}&mode=fit&width=${w} ${w}w`).join(", ");
565
+ } else {
566
+ result = SRCSET_WIDTHS.map((w) => {
567
+ const proxied = proxifyForFormat(url, w, 0, format);
568
+ return proxied ? `${proxied} ${w}w` : "";
569
+ }).filter(Boolean).join(", ");
570
+ }
571
+ if (format !== "match" && result && !result.split(",").every((c) => c.includes(`format=${format}`))) {
572
+ return "";
572
573
  }
573
- return SRCSET_WIDTHS.map((w) => {
574
- const proxied = proxifyImageSrc(url, w);
575
- return proxied ? `${proxied} ${w}w` : "";
576
- }).filter(Boolean).join(", ");
574
+ return result;
575
+ }
576
+ var STATIC_RASTER_PATH_EXT = /\.(?:jpe?g|png|webp)$/i;
577
+ var SIZED_PROXY_PATH = /^\/\d+x\d+\//;
578
+ function isPictureEligibleRawUrl(rawUrl) {
579
+ if (!rawUrl || typeof rawUrl !== "string") return false;
580
+ let u;
581
+ try {
582
+ u = new URL(rawUrl);
583
+ } catch {
584
+ return false;
585
+ }
586
+ if (u.protocol !== "http:" && u.protocol !== "https:") return false;
587
+ const host = `${u.protocol}//${u.host}`;
588
+ const isProxyHost = host === proxyBase || host === "https://images.ecency.com";
589
+ if (isProxyHost && (u.pathname.startsWith("/p/") || u.pathname.startsWith("/u/") || SIZED_PROXY_PATH.test(u.pathname))) {
590
+ return false;
591
+ }
592
+ return STATIC_RASTER_PATH_EXT.test(u.pathname);
593
+ }
594
+ function buildPictureSources(rawUrl) {
595
+ if (!isPictureEligibleRawUrl(rawUrl)) return null;
596
+ const avif = buildSrcSetForFormat(rawUrl, "avif");
597
+ const webp = buildSrcSetForFormat(rawUrl, "webp");
598
+ if (!avif || !webp) return null;
599
+ return { avif, webp };
600
+ }
601
+
602
+ // src/methods/sanitize-html.method.ts
603
+ var decodeEntities = (input) => input.replace(/&#(\d+);?/g, (_, dec) => String.fromCodePoint(Number(dec))).replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)));
604
+ var isProxyPSrcset = (srcset) => {
605
+ const base = trimTrailingSlash(getProxyBase());
606
+ const candidates = srcset.split(",").map((c) => c.trim().split(/\s+/)[0]).filter(Boolean);
607
+ return candidates.length > 0 && candidates.every((url) => url.startsWith(`${base}/p/`));
608
+ };
609
+ function sanitizeHtml(html) {
610
+ const cleaned = xss__default.default(html, {
611
+ whiteList: ALLOWED_ATTRIBUTES,
612
+ stripIgnoreTag: true,
613
+ stripIgnoreTagBody: ["style"],
614
+ css: false,
615
+ // block style attrs entirely for safety
616
+ onTagAttr: (tag, name, value) => {
617
+ const decoded = decodeEntities(value.trim());
618
+ const decodedLower = decoded.toLowerCase();
619
+ if (name.startsWith("on")) return "";
620
+ if (tag === "img" && name === "src" && !/^https?:\/\//.test(decodedLower)) return "";
621
+ if (tag === "img" && name === "srcset") {
622
+ const candidates = decoded.split(",").map((c) => c.trim().split(/\s+/)[0]);
623
+ if (candidates.some((url) => !/^https?:\/\//i.test(url))) return "";
624
+ }
625
+ if (tag === "source" && name === "srcset" && !isProxyPSrcset(decoded)) return "";
626
+ if (tag === "source" && name === "type" && decodedLower !== "image/avif" && decodedLower !== "image/webp") return "";
627
+ if (tag === "video" && ["src", "poster"].includes(name) && !/^https?:\/\//.test(decodedLower)) return "";
628
+ if (tag === "img" && ["dynsrc", "lowsrc"].includes(name)) return "";
629
+ if (tag === "span" && name === "class" && decoded.toLowerCase().trim() === "wr") return "";
630
+ if (name === "id") {
631
+ if (!ID_WHITELIST.test(decoded)) return "";
632
+ }
633
+ return void 0;
634
+ }
635
+ });
636
+ return cleaned.replace(
637
+ /<source\b(?:[^>"']|"[^"]*"|'[^']*')*>/gi,
638
+ (t) => /\btype\s*=\s*["'](?:image\/avif|image\/webp)["']/i.test(t) ? t : ""
639
+ );
577
640
  }
578
641
 
579
642
  // src/methods/img.method.ts
580
643
  var IMAGE_SIZES = "(max-width: 768px) 100vw, 700px";
581
- function img(el, state) {
644
+ function wrapInPicture(el, rawUrl) {
645
+ const parent = el.parentNode;
646
+ if (!parent) return;
647
+ if (parent.nodeName && parent.nodeName.toLowerCase() === "picture") return;
648
+ const sources = buildPictureSources(rawUrl);
649
+ if (!sources) return;
650
+ const doc = el.ownerDocument;
651
+ if (!doc) return;
652
+ const sizes = el.getAttribute("sizes") || IMAGE_SIZES;
653
+ const picture = doc.createElement("picture");
654
+ const avif = doc.createElement("source");
655
+ avif.setAttribute("type", "image/avif");
656
+ avif.setAttribute("srcset", sources.avif);
657
+ avif.setAttribute("sizes", sizes);
658
+ const webp = doc.createElement("source");
659
+ webp.setAttribute("type", "image/webp");
660
+ webp.setAttribute("srcset", sources.webp);
661
+ webp.setAttribute("sizes", sizes);
662
+ parent.insertBefore(picture, el);
663
+ picture.appendChild(avif);
664
+ picture.appendChild(webp);
665
+ picture.appendChild(el);
666
+ }
667
+ function img(el, state, forApp = true) {
582
668
  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();
669
+ const decodedSrc = decodeImageSrc(src);
586
670
  ["onerror", "dynsrc", "lowsrc", "width", "height"].forEach((attr) => el.removeAttribute(attr));
587
671
  const isInvalid = !src || decodedSrc.startsWith("javascript") || decodedSrc.startsWith("vbscript") || decodedSrc === "x";
588
672
  if (isInvalid) {
@@ -621,6 +705,9 @@ function img(el, state) {
621
705
  el.setAttribute("srcset", srcset);
622
706
  el.setAttribute("sizes", IMAGE_SIZES);
623
707
  }
708
+ if (!forApp) {
709
+ wrapInPicture(el, decodedSrc);
710
+ }
624
711
  }
625
712
  } else if (shouldReplace && hasAlreadyProxied) {
626
713
  if (src.startsWith(`${base}/p/`)) {
@@ -632,16 +719,17 @@ function img(el, state) {
632
719
  }
633
720
  }
634
721
  }
635
- function createImageHTML(src, isLCP) {
636
- const proxified = proxifyImageSrc(src, 0, 0, "match", { forceProxy: true });
722
+ function createImageHTML(src, isLCP, forApp = true) {
723
+ const decoded = decodeImageSrc(src);
724
+ const proxified = proxifyImageSrc(decoded, 0, 0, "match", { forceProxy: true });
637
725
  if (!proxified) return "";
638
726
  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);
727
+ const isAlreadyProxied = decoded.startsWith(`${base}/u/`) || new RegExp(`^${base.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/\\d+x\\d+/`).test(decoded);
728
+ const srcset = isAlreadyProxied ? "" : buildSrcSet(decoded);
641
729
  const loading = isLCP ? "eager" : "lazy";
642
730
  const fetch = isLCP ? 'fetchpriority="high"' : 'decoding="async"';
643
731
  const srcsetAttr = srcset ? `srcset="${srcset}" sizes="${IMAGE_SIZES}"` : "";
644
- return `<img
732
+ const imgTag = `<img
645
733
  class="markdown-img-link"
646
734
  src="${proxified}"
647
735
  ${srcsetAttr}
@@ -649,6 +737,13 @@ function createImageHTML(src, isLCP) {
649
737
  ${fetch}
650
738
  itemprop="image"
651
739
  />`;
740
+ if (!forApp) {
741
+ const sources = buildPictureSources(decoded);
742
+ if (sources) {
743
+ return `<picture><source type="image/avif" srcset="${sources.avif}" sizes="${IMAGE_SIZES}" /><source type="image/webp" srcset="${sources.webp}" sizes="${IMAGE_SIZES}" />${imgTag}</picture>`;
744
+ }
745
+ }
746
+ return imgTag;
652
747
  }
653
748
 
654
749
  // src/methods/a.method.ts
@@ -715,7 +810,7 @@ function a(el, forApp, parentDomain = "ecency.com", seoContext, renderOptions) {
715
810
  }
716
811
  if (href.match(IMG_REGEX) && href.trim().replace(/&amp;/g, "&") === getSerializedInnerHTML(el).trim().replace(/&amp;/g, "&")) {
717
812
  const isLCP = false;
718
- const imgHTML = createImageHTML(href, isLCP);
813
+ const imgHTML = createImageHTML(href, isLCP, forApp);
719
814
  const doc = DOMParser.parseFromString(imgHTML, "text/html");
720
815
  const replaceNode = doc.body?.firstChild || doc.firstChild;
721
816
  if (replaceNode && el.parentNode) {
@@ -756,7 +851,7 @@ function a(el, forApp, parentDomain = "ecency.com", seoContext, renderOptions) {
756
851
  el.setAttribute("data-author", author);
757
852
  el.setAttribute("data-permlink", permlink);
758
853
  } else {
759
- const h = `/${tag}/@${author}/${permlink}`;
854
+ const h = `/@${author}/${permlink}`;
760
855
  el.setAttribute("href", h);
761
856
  el.setAttribute("data-is-inline", "" + isInline);
762
857
  }
@@ -835,7 +930,7 @@ function a(el, forApp, parentDomain = "ecency.com", seoContext, renderOptions) {
835
930
  el.setAttribute("data-author", author);
836
931
  el.setAttribute("data-permlink", permlink);
837
932
  } else {
838
- const h = `/${tag}/@${author}/${permlink}`;
933
+ const h = `/@${author}/${permlink}`;
839
934
  el.setAttribute("href", h);
840
935
  el.setAttribute("data-is-inline", "" + isInline);
841
936
  }
@@ -900,7 +995,7 @@ function a(el, forApp, parentDomain = "ecency.com", seoContext, renderOptions) {
900
995
  el.setAttribute("data-author", author);
901
996
  el.setAttribute("data-permlink", permlink);
902
997
  } else {
903
- const h = `/${tag}/@${author}/${permlink}`;
998
+ const h = `/@${author}/${permlink}`;
904
999
  el.setAttribute("href", h);
905
1000
  el.setAttribute("data-is-inline", "" + isInline);
906
1001
  }
@@ -986,7 +1081,7 @@ function a(el, forApp, parentDomain = "ecency.com", seoContext, renderOptions) {
986
1081
  el.setAttribute("data-author", author);
987
1082
  el.setAttribute("data-permlink", permlink);
988
1083
  } else {
989
- const h = `/${tag}/@${author}/${permlink}`;
1084
+ const h = `/@${author}/${permlink}`;
990
1085
  el.setAttribute("href", h);
991
1086
  el.setAttribute("data-is-inline", "" + isInline);
992
1087
  }
@@ -1530,7 +1625,7 @@ function linkify(content, forApp, renderOptions) {
1530
1625
  const attrs = forApp ? `href="https://ecency.com/@${authorLower}/${permlink}"` : `href="/@${authorLower}/${permlink}"`;
1531
1626
  return `${preceding}<a class="markdown-profile-link" ${attrs}>@${authorLower}/${permlink}</a>`;
1532
1627
  } else {
1533
- const attrs = forApp ? `data-author="${authorLower}" data-tag="${tag}" data-permlink="${permlink}"` : `href="/${tag}/@${authorLower}/${permlink}"`;
1628
+ const attrs = forApp ? `data-author="${authorLower}" data-tag="${tag}" data-permlink="${permlink}"` : `href="/@${authorLower}/${permlink}"`;
1534
1629
  return `${preceding}<a class="markdown-post-link" ${attrs}>@${authorLower}/${permlink}</a>`;
1535
1630
  }
1536
1631
  }
@@ -1545,7 +1640,7 @@ function linkify(content, forApp, renderOptions) {
1545
1640
  const attrs = forApp ? `href="https://ecency.com/@${uu}/${permlink}"` : `href="/@${uu}/${permlink}"`;
1546
1641
  return ` <a class="markdown-profile-link" ${attrs}>@${uu}/${permlink}</a>`;
1547
1642
  } else {
1548
- const attrs = forApp ? `data-author="${uu}" data-tag="post" data-permlink="${permlink}"` : `href="/post/@${uu}/${permlink}"`;
1643
+ const attrs = forApp ? `data-author="${uu}" data-tag="post" data-permlink="${permlink}"` : `href="/@${uu}/${permlink}"`;
1549
1644
  return ` <a class="markdown-post-link" ${attrs}>@${uu}/${permlink}</a>`;
1550
1645
  }
1551
1646
  }
@@ -1554,7 +1649,7 @@ function linkify(content, forApp, renderOptions) {
1554
1649
  content = content.replace(IMG_REGEX, (imglink) => {
1555
1650
  const isLCP = !firstImageUsed;
1556
1651
  firstImageUsed = true;
1557
- return createImageHTML(imglink, isLCP);
1652
+ return createImageHTML(imglink, isLCP, forApp);
1558
1653
  });
1559
1654
  authorPlaceholders.forEach(({ placeholder, html }) => {
1560
1655
  content = content.replace(placeholder, html);
@@ -1596,7 +1691,7 @@ function text(node, forApp, renderOptions) {
1596
1691
  }
1597
1692
  if (nodeValue.match(IMG_REGEX)) {
1598
1693
  const isLCP = false;
1599
- const imageHTML = createImageHTML(nodeValue, isLCP);
1694
+ const imageHTML = createImageHTML(nodeValue, isLCP, forApp);
1600
1695
  const doc = DOMParser.parseFromString(imageHTML, "text/html");
1601
1696
  const replaceNode = doc.body?.firstChild || doc.firstChild;
1602
1697
  if (replaceNode) {
@@ -1640,7 +1735,7 @@ function text(node, forApp, renderOptions) {
1640
1735
  if (!tag || !/^[a-z0-9_-]+$/i.test(tag)) return;
1641
1736
  if (!isValidUsername(author)) return;
1642
1737
  if (!isValidPermlink(permlink)) return;
1643
- const attrs = forApp ? `data-tag="${tag}" data-author="${author}" data-permlink="${permlink}" class="markdown-post-link"` : `class="markdown-post-link" href="/${tag}/@${author}/${permlink}"`;
1738
+ const attrs = forApp ? `data-tag="${tag}" data-author="${author}" data-permlink="${permlink}" class="markdown-post-link"` : `class="markdown-post-link" href="/@${author}/${permlink}"`;
1644
1739
  const doc = DOMParser.parseFromString(
1645
1740
  `<a ${attrs}>/@${author}/${permlink}</a>`,
1646
1741
  "text/html"
@@ -1672,7 +1767,7 @@ function traverse(node, forApp, depth = 0, state = { firstImageFound: false }, p
1672
1767
  text(child, forApp);
1673
1768
  }
1674
1769
  if (child.nodeName.toLowerCase() === "img") {
1675
- img(child, state);
1770
+ img(child, state, forApp);
1676
1771
  }
1677
1772
  if (child.nodeName.toLowerCase() === "p") {
1678
1773
  p(child);
@@ -1895,8 +1990,11 @@ var INLINE_CODE_RE = /`[^`\n]*`/g;
1895
1990
  var INDENTED_CODE_RE = /^(?: {4}|\t).+$/gm;
1896
1991
  var MD_IMAGE_RE = /!\[[^\]]*\]\(\s*([^)\s]+)(?:\s+["'][^"']*["'])?\s*\)/;
1897
1992
  var HTML_IMAGE_RE = /<img\b[^>]*?\bsrc\s*=\s*["']([^"']+)["']/i;
1993
+ var BARE_IMAGE_RE = /(^|\s)(https?:\/\/[^\s<>"'()[\]]+\.(?:tiff?|jpe?g|gif|png|svg|ico|heic|webp|arw)(?:[?#][^\s<>"'()[\]]*)?)/im;
1994
+ var MD_LINK_RE = /\[([^\]]*)\]\(\s*([^)\s]+)(?:\s+["'][^"']*["'])?\s*\)/g;
1995
+ var IMG_HREF_RE = /https?:\/\/.*\.(?:tiff?|jpe?g|gif|png|svg|ico|heic|webp|arw)/i;
1898
1996
  var SAFE_URL_RE = /^https?:\/\//i;
1899
- function findFirstImageUrl(body) {
1997
+ function findFirstImageUrl(body, includeBareUrls = false) {
1900
1998
  if (!body) return null;
1901
1999
  const cleaned = body.replace(BACKTICK_FENCE_RE, "").replace(TILDE_FENCE_RE, "").replace(INLINE_CODE_RE, "").replace(INDENTED_CODE_RE, "");
1902
2000
  const mdMatch = cleaned.match(MD_IMAGE_RE);
@@ -1907,17 +2005,33 @@ function findFirstImageUrl(body) {
1907
2005
  return null;
1908
2006
  }
1909
2007
  }
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];
2008
+ const candidates = [];
2009
+ if (mdMatch) candidates.push({ url: mdMatch[1], pos: mdMatch.index ?? 0 });
2010
+ if (htmlMatch && htmlMatch[1] && SAFE_URL_RE.test(htmlMatch[1])) {
2011
+ candidates.push({ url: htmlMatch[1], pos: htmlMatch.index ?? 0 });
1914
2012
  }
1915
- if (mdValid) return mdMatch[1];
1916
- if (htmlValid) return htmlMatch[1];
1917
- return null;
2013
+ if (includeBareUrls) {
2014
+ const bareMatch = cleaned.match(BARE_IMAGE_RE);
2015
+ if (bareMatch && bareMatch[2] && SAFE_URL_RE.test(bareMatch[2])) {
2016
+ candidates.push({ url: bareMatch[2], pos: (bareMatch.index ?? 0) + bareMatch[1].length });
2017
+ }
2018
+ const deAmp = (s) => s.trim().replace(/&amp;/g, "&");
2019
+ for (const m of cleaned.matchAll(MD_LINK_RE)) {
2020
+ const idx = m.index ?? 0;
2021
+ if (idx > 0 && cleaned[idx - 1] === "!") continue;
2022
+ const href = m[2];
2023
+ if (href && SAFE_URL_RE.test(href) && IMG_HREF_RE.test(href) && deAmp(m[1]) === deAmp(href)) {
2024
+ candidates.push({ url: href, pos: idx });
2025
+ break;
2026
+ }
2027
+ }
2028
+ }
2029
+ if (candidates.length === 0) return null;
2030
+ candidates.sort((a2, b) => a2.pos - b.pos);
2031
+ return candidates[0].url;
1918
2032
  }
1919
2033
  function proxifyFound(src, width, height, format) {
1920
- const decoded = he__default.default.decode(src);
2034
+ const decoded = he2__default.default.decode(src);
1921
2035
  if (isGifLink(decoded)) {
1922
2036
  return proxifyImageSrc(decoded, 0, 0, format);
1923
2037
  }
@@ -1935,7 +2049,7 @@ function getImage(entry, width = 0, height = 0, format = "match") {
1935
2049
  }
1936
2050
  }
1937
2051
  if (meta && typeof meta.image === "string" && meta.image.length > 0) {
1938
- const decodedImage = he__default.default.decode(meta.image);
2052
+ const decodedImage = he2__default.default.decode(meta.image);
1939
2053
  if (isGifLink(decodedImage)) {
1940
2054
  return proxifyImageSrc(decodedImage, 0, 0, format);
1941
2055
  }
@@ -1943,7 +2057,7 @@ function getImage(entry, width = 0, height = 0, format = "match") {
1943
2057
  }
1944
2058
  if (meta && meta.image && !!meta.image.length && meta.image[0]) {
1945
2059
  if (typeof meta.image[0] === "string") {
1946
- const decodedImage = he__default.default.decode(meta.image[0]);
2060
+ const decodedImage = he2__default.default.decode(meta.image[0]);
1947
2061
  if (isGifLink(decodedImage)) {
1948
2062
  return proxifyImageSrc(decodedImage, 0, 0, format);
1949
2063
  }
@@ -1973,6 +2087,30 @@ function getImage(entry, width = 0, height = 0, format = "match") {
1973
2087
  }
1974
2088
  return null;
1975
2089
  }
2090
+ function getEntryImageRawUrl(obj) {
2091
+ if (typeof obj === "string") {
2092
+ const src = findFirstImageUrl(obj, true);
2093
+ return src ? decodeImageSrc(src) : null;
2094
+ }
2095
+ let meta;
2096
+ if (typeof obj.json_metadata === "object") {
2097
+ meta = obj.json_metadata;
2098
+ } else {
2099
+ try {
2100
+ meta = JSON.parse(obj.json_metadata);
2101
+ } catch (e) {
2102
+ meta = null;
2103
+ }
2104
+ }
2105
+ if (meta && typeof meta.image === "string" && meta.image.length > 0) {
2106
+ return decodeImageSrc(meta.image);
2107
+ }
2108
+ if (meta && meta.image && !!meta.image.length && typeof meta.image[0] === "string" && meta.image[0].length > 0) {
2109
+ return decodeImageSrc(meta.image[0]);
2110
+ }
2111
+ const bodySrc = findFirstImageUrl(obj.body, true);
2112
+ return bodySrc ? decodeImageSrc(bodySrc) : null;
2113
+ }
1976
2114
  function catchPostImage(obj, width = 0, height = 0, format = "match") {
1977
2115
  if (typeof obj === "string") {
1978
2116
  const fast = findFirstImageUrl(obj);
@@ -2074,7 +2212,7 @@ function postBodySummary(entryBody, length = 200, platform = "web") {
2074
2212
  text2 = joint(text2.split(" "), length);
2075
2213
  }
2076
2214
  if (text2) {
2077
- text2 = he__default.default.decode(text2);
2215
+ text2 = he2__default.default.decode(text2);
2078
2216
  }
2079
2217
  return text2;
2080
2218
  }
@@ -2096,8 +2234,12 @@ function getPostBodySummary(obj, length, platform) {
2096
2234
 
2097
2235
  exports.IMAGE_SIZES = IMAGE_SIZES;
2098
2236
  exports.SECTION_LIST = SECTION_LIST;
2237
+ exports.buildPictureSources = buildPictureSources;
2099
2238
  exports.buildSrcSet = buildSrcSet;
2239
+ exports.buildSrcSetForFormat = buildSrcSetForFormat;
2100
2240
  exports.catchPostImage = catchPostImage;
2241
+ exports.getEntryImageRawUrl = getEntryImageRawUrl;
2242
+ exports.isPictureEligibleRawUrl = isPictureEligibleRawUrl;
2101
2243
  exports.isValidPermlink = isValidPermlink;
2102
2244
  exports.postBodySummary = getPostBodySummary;
2103
2245
  exports.proxifyImageSrc = proxifyImageSrc;