@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,4 +1,5 @@
1
1
  import { DOMParser as DOMParser$1, XMLSerializer } from '@xmldom/xmldom';
2
+ import he2 from 'he';
2
3
  import xss from 'xss';
3
4
  import multihash from 'multihashes';
4
5
  import querystring from 'querystring';
@@ -7,7 +8,6 @@ import { Remarkable } from 'remarkable';
7
8
  import { linkify as linkify$1 } from 'remarkable/linkify';
8
9
  import * as htmlparser2 from 'htmlparser2';
9
10
  import * as domSerializerModule from 'dom-serializer';
10
- import he from 'he';
11
11
 
12
12
  // src/consts/white-list.const.ts
13
13
  var WHITE_LIST = [
@@ -133,6 +133,13 @@ var ALLOWED_ATTRIBUTES = {
133
133
  "decoding",
134
134
  "itemprop"
135
135
  ],
136
+ // Responsive image content-negotiation wrapper emitted for web/self-hosted
137
+ // (forApp === false). Without these entries `xss` silently collapses the
138
+ // <picture>/<source> to a bare <img>. `source` attrs are further constrained
139
+ // in sanitize-html (srcset must be a proxy /p/ URL; type must be avif/webp;
140
+ // a type-less <source> is dropped post-pass).
141
+ "picture": [],
142
+ "source": ["type", "srcset", "sizes"],
136
143
  "span": ["class", "id", "data-align"],
137
144
  "iframe": ["src", "class", "frameborder", "allowfullscreen", "webkitallowfullscreen", "mozallowfullscreen", "sandbox"],
138
145
  "video": ["src", "controls", "poster"],
@@ -169,6 +176,96 @@ var ALLOWED_ATTRIBUTES = {
169
176
  "del": [],
170
177
  "ins": []
171
178
  };
179
+
180
+ // src/consts/embed-hosts.const.ts
181
+ var ALLOWED_EMBED_HOSTS = /* @__PURE__ */ new Set([
182
+ // YouTube
183
+ "www.youtube.com",
184
+ "youtube.com",
185
+ "www.youtube-nocookie.com",
186
+ "youtube-nocookie.com",
187
+ // Vimeo
188
+ "player.vimeo.com",
189
+ // Twitch
190
+ "player.twitch.tv",
191
+ // DTube
192
+ "emb.d.tube",
193
+ // 3Speak (video + audio)
194
+ "play.3speak.tv",
195
+ "3speak.tv",
196
+ "audio.3speak.tv",
197
+ // Loom
198
+ "www.loom.com",
199
+ // Spotify
200
+ "open.spotify.com",
201
+ // SoundCloud
202
+ "w.soundcloud.com",
203
+ // BitChute
204
+ "www.bitchute.com",
205
+ "bitchute.com",
206
+ // Rumble
207
+ "www.rumble.com",
208
+ "rumble.com",
209
+ // Brighteon
210
+ "www.brighteon.com",
211
+ "brighteon.com",
212
+ // VIMM
213
+ "www.vimm.tv",
214
+ // BrandNewTube
215
+ "brandnewtube.com",
216
+ // LBRY / Odysee
217
+ "lbry.tv",
218
+ "odysee.com",
219
+ // Skatehive / Skatehype
220
+ "ipfs.skatehive.app",
221
+ "www.skatehype.com",
222
+ "skatehype.com",
223
+ // archive.org
224
+ "archive.org",
225
+ // Truvvl
226
+ "embed.truvvl.com",
227
+ // Aureal
228
+ "aureal-embed.web.app",
229
+ "www.aureal-embed.web.app"
230
+ // Dapplr (player.*.dapplr.in / *.dapplr.in) — host suffix, handled below
231
+ ]);
232
+ var ALLOWED_EMBED_HOST_SUFFIXES = [".dapplr.in"];
233
+ var EMBED_HOST_PATH_PATTERNS = {
234
+ "www.youtube.com": /^\/embed\//,
235
+ "youtube.com": /^\/embed\//,
236
+ "www.youtube-nocookie.com": /^\/embed\//,
237
+ "youtube-nocookie.com": /^\/embed\//,
238
+ "player.vimeo.com": /^\/video\//,
239
+ "player.twitch.tv": /^\/$/,
240
+ // channel/video carried in the query string
241
+ "emb.d.tube": /^\/$/,
242
+ // dtube carries the ref in the #! fragment
243
+ "play.3speak.tv": /^\/(watch|embed)/,
244
+ "open.spotify.com": /^\/embed\//,
245
+ "www.loom.com": /^\/embed\//,
246
+ "www.bitchute.com": /^\/embed\//,
247
+ "bitchute.com": /^\/embed\//,
248
+ "www.rumble.com": /^\/embed\//,
249
+ "rumble.com": /^\/embed\//,
250
+ "www.brighteon.com": /^\/embed\//,
251
+ "brighteon.com": /^\/embed\//
252
+ };
253
+ function isAllowedEmbedSrc(value) {
254
+ if (!value) return false;
255
+ let url;
256
+ try {
257
+ url = new URL(value.trim());
258
+ } catch {
259
+ return false;
260
+ }
261
+ if (url.protocol !== "https:") return false;
262
+ const host = url.hostname.toLowerCase();
263
+ const hostAllowed = ALLOWED_EMBED_HOSTS.has(host) || ALLOWED_EMBED_HOST_SUFFIXES.some((suffix) => host.endsWith(suffix));
264
+ if (!hostAllowed) return false;
265
+ const pathPattern = EMBED_HOST_PATH_PATTERNS[host];
266
+ if (pathPattern && !pathPattern.test(url.pathname)) return false;
267
+ return true;
268
+ }
172
269
  function createParser() {
173
270
  return new DOMParser$1({
174
271
  onError(level, msg) {
@@ -176,8 +273,14 @@ function createParser() {
176
273
  });
177
274
  }
178
275
  var DOMParser = createParser();
179
-
180
- // src/helper.ts
276
+ function decodeImageSrc(src) {
277
+ const entityDecoded = he2.decode(src);
278
+ try {
279
+ return decodeURIComponent(entityDecoded).trim();
280
+ } catch {
281
+ return entityDecoded.trim();
282
+ }
283
+ }
181
284
  function isSpaceChar(c) {
182
285
  return c === 32 || c === 9 || c === 10 || c === 13 || c === 12;
183
286
  }
@@ -433,33 +536,6 @@ function removeChildNodes(node) {
433
536
  node.removeChild(node.firstChild);
434
537
  }
435
538
  }
436
- var decodeEntities = (input) => input.replace(/&#(\d+);?/g, (_, dec) => String.fromCodePoint(Number(dec))).replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)));
437
- function sanitizeHtml(html) {
438
- return xss(html, {
439
- whiteList: ALLOWED_ATTRIBUTES,
440
- stripIgnoreTag: true,
441
- stripIgnoreTagBody: ["style"],
442
- css: false,
443
- // block style attrs entirely for safety
444
- onTagAttr: (tag, name, value) => {
445
- const decoded = decodeEntities(value.trim());
446
- const decodedLower = decoded.toLowerCase();
447
- if (name.startsWith("on")) return "";
448
- if (tag === "img" && name === "src" && !/^https?:\/\//.test(decodedLower)) return "";
449
- if (tag === "img" && name === "srcset") {
450
- const candidates = decoded.split(",").map((c) => c.trim().split(/\s+/)[0]);
451
- if (candidates.some((url) => !/^https?:\/\//i.test(url))) return "";
452
- }
453
- if (tag === "video" && ["src", "poster"].includes(name) && !/^https?:\/\//.test(decodedLower)) return "";
454
- if (tag === "img" && ["dynsrc", "lowsrc"].includes(name)) return "";
455
- if (tag === "span" && name === "class" && decoded.toLowerCase().trim() === "wr") return "";
456
- if (name === "id") {
457
- if (!ID_WHITELIST.test(decoded)) return "";
458
- }
459
- return void 0;
460
- }
461
- });
462
- }
463
539
  var proxyBase = "https://i.ecency.com";
464
540
  var urlHashCache = new LRUCache({ max: 500 });
465
541
  function getUrlHash(url) {
@@ -495,7 +571,7 @@ function getLatestUrl(str) {
495
571
  const [last] = [...str.replace(/https?:\/\//g, "\n$&").trim().split("\n")].reverse();
496
572
  return last;
497
573
  }
498
- function proxifyImageSrc(url, width = 0, height = 0, _format = "match", opts = {}) {
574
+ function proxifyForFormat(url, width = 0, height = 0, format = "match", opts = {}) {
499
575
  if (!url || typeof url !== "string" || !isValidUrl(url)) {
500
576
  return "";
501
577
  }
@@ -512,7 +588,7 @@ function proxifyImageSrc(url, width = 0, height = 0, _format = "match", opts = {
512
588
  const realUrl = getLatestUrl(url);
513
589
  const pHash = extractPHash(realUrl);
514
590
  const options = {
515
- format: "match",
591
+ format,
516
592
  mode: "fit"
517
593
  };
518
594
  if (width > 0) {
@@ -531,29 +607,137 @@ function proxifyImageSrc(url, width = 0, height = 0, _format = "match", opts = {
531
607
  const b58url = getUrlHash(realUrl.toString());
532
608
  return `${proxyBase}/p/${b58url}?${qs}`;
533
609
  }
610
+ function proxifyImageSrc(url, width = 0, height = 0, _format = "match", opts = {}) {
611
+ return proxifyForFormat(url, width, height, "match", opts);
612
+ }
534
613
  var SRCSET_WIDTHS = [320, 600, 800, 1024, 1280];
535
614
  function buildSrcSet(url) {
615
+ return buildSrcSetForFormat(url, "match");
616
+ }
617
+ function buildSrcSetForFormat(url, format = "match") {
536
618
  if (!url || typeof url !== "string") return "";
537
619
  const proxyPrefix = `${proxyBase}/p/`;
620
+ let result;
538
621
  if (url.startsWith(proxyPrefix)) {
539
622
  const rest = url.slice(proxyPrefix.length);
540
623
  const q = rest.indexOf("?");
541
624
  const phash = extractPHash(url) || (q >= 0 ? rest.slice(0, q) : rest);
542
- return SRCSET_WIDTHS.map((w) => `${proxyBase}/p/${phash}?format=match&mode=fit&width=${w} ${w}w`).join(", ");
625
+ result = SRCSET_WIDTHS.map((w) => `${proxyBase}/p/${phash}?format=${format}&mode=fit&width=${w} ${w}w`).join(", ");
626
+ } else {
627
+ result = SRCSET_WIDTHS.map((w) => {
628
+ const proxied = proxifyForFormat(url, w, 0, format);
629
+ return proxied ? `${proxied} ${w}w` : "";
630
+ }).filter(Boolean).join(", ");
631
+ }
632
+ if (format !== "match" && result && !result.split(",").every((c) => c.includes(`format=${format}`))) {
633
+ return "";
543
634
  }
544
- return SRCSET_WIDTHS.map((w) => {
545
- const proxied = proxifyImageSrc(url, w);
546
- return proxied ? `${proxied} ${w}w` : "";
547
- }).filter(Boolean).join(", ");
635
+ return result;
636
+ }
637
+ var STATIC_RASTER_PATH_EXT = /\.(?:jpe?g|png|webp)$/i;
638
+ var SIZED_PROXY_PATH = /^\/\d+x\d+\//;
639
+ function isPictureEligibleRawUrl(rawUrl) {
640
+ if (!rawUrl || typeof rawUrl !== "string") return false;
641
+ let u;
642
+ try {
643
+ u = new URL(rawUrl);
644
+ } catch {
645
+ return false;
646
+ }
647
+ if (u.protocol !== "http:" && u.protocol !== "https:") return false;
648
+ const host = `${u.protocol}//${u.host}`;
649
+ const isProxyHost = host === proxyBase || host === "https://images.ecency.com";
650
+ if (isProxyHost && (u.pathname.startsWith("/p/") || u.pathname.startsWith("/u/") || SIZED_PROXY_PATH.test(u.pathname))) {
651
+ return false;
652
+ }
653
+ return STATIC_RASTER_PATH_EXT.test(u.pathname);
654
+ }
655
+ function buildPictureSources(rawUrl) {
656
+ if (!isPictureEligibleRawUrl(rawUrl)) return null;
657
+ const avif = buildSrcSetForFormat(rawUrl, "avif");
658
+ const webp = buildSrcSetForFormat(rawUrl, "webp");
659
+ if (!avif || !webp) return null;
660
+ return { avif, webp };
661
+ }
662
+
663
+ // src/methods/sanitize-html.method.ts
664
+ var EMBED_SRC_DATA_ATTRS = /* @__PURE__ */ new Set(["data-embed-src", "data-video-href"]);
665
+ var isSafeNavValue = (value) => {
666
+ const trimmed = value.trim().replace(/[\t\n\r\f\v\0]/g, "").toLowerCase();
667
+ if (!trimmed) return false;
668
+ const isSafeScheme = /^(https?|mailto|hive|tel|web\+[a-z0-9.+-]+):/i.test(trimmed);
669
+ const isRelative = /^(\/\/|\/[^/]?|#|\?|[a-z0-9._\-]+(\/|$))/i.test(trimmed);
670
+ return isSafeScheme || isRelative;
671
+ };
672
+ var decodeEntities = (input) => input.replace(/&#(\d+);?/g, (_, dec) => String.fromCodePoint(Number(dec))).replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)));
673
+ var isProxyPSrcset = (srcset) => {
674
+ const base = trimTrailingSlash(getProxyBase());
675
+ const candidates = srcset.split(",").map((c) => c.trim().split(/\s+/)[0]).filter(Boolean);
676
+ return candidates.length > 0 && candidates.every((url) => url.startsWith(`${base}/p/`));
677
+ };
678
+ function sanitizeHtml(html) {
679
+ const cleaned = xss(html, {
680
+ whiteList: ALLOWED_ATTRIBUTES,
681
+ stripIgnoreTag: true,
682
+ stripIgnoreTagBody: ["style"],
683
+ css: false,
684
+ // block style attrs entirely for safety
685
+ onTagAttr: (tag, name, value) => {
686
+ const decoded = decodeEntities(value.trim());
687
+ const decodedLower = decoded.toLowerCase();
688
+ if (name.startsWith("on")) return "";
689
+ if (tag === "img" && name === "src" && !/^https?:\/\//.test(decodedLower)) return "";
690
+ if (tag === "img" && name === "srcset") {
691
+ const candidates = decoded.split(",").map((c) => c.trim().split(/\s+/)[0]);
692
+ if (candidates.some((url) => !/^https?:\/\//i.test(url))) return "";
693
+ }
694
+ if (tag === "source" && name === "srcset" && !isProxyPSrcset(decoded)) return "";
695
+ if (tag === "source" && name === "type" && decodedLower !== "image/avif" && decodedLower !== "image/webp") return "";
696
+ if (tag === "video" && ["src", "poster"].includes(name) && !/^https?:\/\//.test(decodedLower)) return "";
697
+ if (tag === "img" && ["dynsrc", "lowsrc"].includes(name)) return "";
698
+ if (tag === "span" && name === "class" && decoded.toLowerCase().trim() === "wr") return "";
699
+ if (EMBED_SRC_DATA_ATTRS.has(name) && !isAllowedEmbedSrc(decoded)) return "";
700
+ if (name === "data-href" && !isSafeNavValue(decoded)) return "";
701
+ if (name === "id") {
702
+ if (!ID_WHITELIST.test(decoded)) return "";
703
+ }
704
+ return void 0;
705
+ }
706
+ });
707
+ return cleaned.replace(
708
+ /<source\b(?:[^>"']|"[^"]*"|'[^']*')*>/gi,
709
+ (t) => /\btype\s*=\s*["'](?:image\/avif|image\/webp)["']/i.test(t) ? t : ""
710
+ );
548
711
  }
549
712
 
550
713
  // src/methods/img.method.ts
551
714
  var IMAGE_SIZES = "(max-width: 768px) 100vw, 700px";
552
- function img(el, state) {
715
+ function wrapInPicture(el, rawUrl) {
716
+ const parent = el.parentNode;
717
+ if (!parent) return;
718
+ if (parent.nodeName && parent.nodeName.toLowerCase() === "picture") return;
719
+ const sources = buildPictureSources(rawUrl);
720
+ if (!sources) return;
721
+ const doc = el.ownerDocument;
722
+ if (!doc) return;
723
+ const sizes = el.getAttribute("sizes") || IMAGE_SIZES;
724
+ const picture = doc.createElement("picture");
725
+ const avif = doc.createElement("source");
726
+ avif.setAttribute("type", "image/avif");
727
+ avif.setAttribute("srcset", sources.avif);
728
+ avif.setAttribute("sizes", sizes);
729
+ const webp = doc.createElement("source");
730
+ webp.setAttribute("type", "image/webp");
731
+ webp.setAttribute("srcset", sources.webp);
732
+ webp.setAttribute("sizes", sizes);
733
+ parent.insertBefore(picture, el);
734
+ picture.appendChild(avif);
735
+ picture.appendChild(webp);
736
+ picture.appendChild(el);
737
+ }
738
+ function img(el, state, forApp = true) {
553
739
  const src = el.getAttribute("src") || "";
554
- const decodedSrc = decodeURIComponent(
555
- src.replace(/&#(\d+);/g, (_, dec) => String.fromCharCode(dec)).replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
556
- ).trim();
740
+ const decodedSrc = decodeImageSrc(src);
557
741
  ["onerror", "dynsrc", "lowsrc", "width", "height"].forEach((attr) => el.removeAttribute(attr));
558
742
  const isInvalid = !src || decodedSrc.startsWith("javascript") || decodedSrc.startsWith("vbscript") || decodedSrc === "x";
559
743
  if (isInvalid) {
@@ -592,6 +776,9 @@ function img(el, state) {
592
776
  el.setAttribute("srcset", srcset);
593
777
  el.setAttribute("sizes", IMAGE_SIZES);
594
778
  }
779
+ if (!forApp) {
780
+ wrapInPicture(el, decodedSrc);
781
+ }
595
782
  }
596
783
  } else if (shouldReplace && hasAlreadyProxied) {
597
784
  if (src.startsWith(`${base}/p/`)) {
@@ -603,16 +790,17 @@ function img(el, state) {
603
790
  }
604
791
  }
605
792
  }
606
- function createImageHTML(src, isLCP) {
607
- const proxified = proxifyImageSrc(src, 0, 0, "match", { forceProxy: true });
793
+ function createImageHTML(src, isLCP, forApp = true) {
794
+ const decoded = decodeImageSrc(src);
795
+ const proxified = proxifyImageSrc(decoded, 0, 0, "match", { forceProxy: true });
608
796
  if (!proxified) return "";
609
797
  const base = trimTrailingSlash(getProxyBase());
610
- const isAlreadyProxied = src.startsWith(`${base}/u/`) || new RegExp(`^${base.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/\\d+x\\d+/`).test(src);
611
- const srcset = isAlreadyProxied ? "" : buildSrcSet(src);
798
+ const isAlreadyProxied = decoded.startsWith(`${base}/u/`) || new RegExp(`^${base.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/\\d+x\\d+/`).test(decoded);
799
+ const srcset = isAlreadyProxied ? "" : buildSrcSet(decoded);
612
800
  const loading = isLCP ? "eager" : "lazy";
613
801
  const fetch = isLCP ? 'fetchpriority="high"' : 'decoding="async"';
614
802
  const srcsetAttr = srcset ? `srcset="${srcset}" sizes="${IMAGE_SIZES}"` : "";
615
- return `<img
803
+ const imgTag = `<img
616
804
  class="markdown-img-link"
617
805
  src="${proxified}"
618
806
  ${srcsetAttr}
@@ -620,6 +808,13 @@ function createImageHTML(src, isLCP) {
620
808
  ${fetch}
621
809
  itemprop="image"
622
810
  />`;
811
+ if (!forApp) {
812
+ const sources = buildPictureSources(decoded);
813
+ if (sources) {
814
+ return `<picture><source type="image/avif" srcset="${sources.avif}" sizes="${IMAGE_SIZES}" /><source type="image/webp" srcset="${sources.webp}" sizes="${IMAGE_SIZES}" />${imgTag}</picture>`;
815
+ }
816
+ }
817
+ return imgTag;
623
818
  }
624
819
 
625
820
  // src/methods/a.method.ts
@@ -686,7 +881,7 @@ function a(el, forApp, parentDomain = "ecency.com", seoContext, renderOptions) {
686
881
  }
687
882
  if (href.match(IMG_REGEX) && href.trim().replace(/&amp;/g, "&") === getSerializedInnerHTML(el).trim().replace(/&amp;/g, "&")) {
688
883
  const isLCP = false;
689
- const imgHTML = createImageHTML(href, isLCP);
884
+ const imgHTML = createImageHTML(href, isLCP, forApp);
690
885
  const doc = DOMParser.parseFromString(imgHTML, "text/html");
691
886
  const replaceNode = doc.body?.firstChild || doc.firstChild;
692
887
  if (replaceNode && el.parentNode) {
@@ -1525,7 +1720,7 @@ function linkify(content, forApp, renderOptions) {
1525
1720
  content = content.replace(IMG_REGEX, (imglink) => {
1526
1721
  const isLCP = !firstImageUsed;
1527
1722
  firstImageUsed = true;
1528
- return createImageHTML(imglink, isLCP);
1723
+ return createImageHTML(imglink, isLCP, forApp);
1529
1724
  });
1530
1725
  authorPlaceholders.forEach(({ placeholder, html }) => {
1531
1726
  content = content.replace(placeholder, html);
@@ -1567,7 +1762,7 @@ function text(node, forApp, renderOptions) {
1567
1762
  }
1568
1763
  if (nodeValue.match(IMG_REGEX)) {
1569
1764
  const isLCP = false;
1570
- const imageHTML = createImageHTML(nodeValue, isLCP);
1765
+ const imageHTML = createImageHTML(nodeValue, isLCP, forApp);
1571
1766
  const doc = DOMParser.parseFromString(imageHTML, "text/html");
1572
1767
  const replaceNode = doc.body?.firstChild || doc.firstChild;
1573
1768
  if (replaceNode) {
@@ -1643,7 +1838,7 @@ function traverse(node, forApp, depth = 0, state = { firstImageFound: false }, p
1643
1838
  text(child, forApp);
1644
1839
  }
1645
1840
  if (child.nodeName.toLowerCase() === "img") {
1646
- img(child, state);
1841
+ img(child, state, forApp);
1647
1842
  }
1648
1843
  if (child.nodeName.toLowerCase() === "p") {
1649
1844
  p(child);
@@ -1866,8 +2061,11 @@ var INLINE_CODE_RE = /`[^`\n]*`/g;
1866
2061
  var INDENTED_CODE_RE = /^(?: {4}|\t).+$/gm;
1867
2062
  var MD_IMAGE_RE = /!\[[^\]]*\]\(\s*([^)\s]+)(?:\s+["'][^"']*["'])?\s*\)/;
1868
2063
  var HTML_IMAGE_RE = /<img\b[^>]*?\bsrc\s*=\s*["']([^"']+)["']/i;
2064
+ var BARE_IMAGE_RE = /(^|\s)(https?:\/\/[^\s<>"'()[\]]+\.(?:tiff?|jpe?g|gif|png|svg|ico|heic|webp|arw)(?:[?#][^\s<>"'()[\]]*)?)/im;
2065
+ var MD_LINK_RE = /\[([^\]]*)\]\(\s*([^)\s]+)(?:\s+["'][^"']*["'])?\s*\)/g;
2066
+ var IMG_HREF_RE = /https?:\/\/.*\.(?:tiff?|jpe?g|gif|png|svg|ico|heic|webp|arw)/i;
1869
2067
  var SAFE_URL_RE = /^https?:\/\//i;
1870
- function findFirstImageUrl(body) {
2068
+ function findFirstImageUrl(body, includeBareUrls = false) {
1871
2069
  if (!body) return null;
1872
2070
  const cleaned = body.replace(BACKTICK_FENCE_RE, "").replace(TILDE_FENCE_RE, "").replace(INLINE_CODE_RE, "").replace(INDENTED_CODE_RE, "");
1873
2071
  const mdMatch = cleaned.match(MD_IMAGE_RE);
@@ -1878,17 +2076,33 @@ function findFirstImageUrl(body) {
1878
2076
  return null;
1879
2077
  }
1880
2078
  }
1881
- const mdValid = !!mdMatch;
1882
- const htmlValid = !!(htmlMatch && htmlMatch[1] && SAFE_URL_RE.test(htmlMatch[1]));
1883
- if (mdValid && htmlValid) {
1884
- return (mdMatch.index ?? 0) < (htmlMatch.index ?? 0) ? mdMatch[1] : htmlMatch[1];
2079
+ const candidates = [];
2080
+ if (mdMatch) candidates.push({ url: mdMatch[1], pos: mdMatch.index ?? 0 });
2081
+ if (htmlMatch && htmlMatch[1] && SAFE_URL_RE.test(htmlMatch[1])) {
2082
+ candidates.push({ url: htmlMatch[1], pos: htmlMatch.index ?? 0 });
1885
2083
  }
1886
- if (mdValid) return mdMatch[1];
1887
- if (htmlValid) return htmlMatch[1];
1888
- return null;
2084
+ if (includeBareUrls) {
2085
+ const bareMatch = cleaned.match(BARE_IMAGE_RE);
2086
+ if (bareMatch && bareMatch[2] && SAFE_URL_RE.test(bareMatch[2])) {
2087
+ candidates.push({ url: bareMatch[2], pos: (bareMatch.index ?? 0) + bareMatch[1].length });
2088
+ }
2089
+ const deAmp = (s) => s.trim().replace(/&amp;/g, "&");
2090
+ for (const m of cleaned.matchAll(MD_LINK_RE)) {
2091
+ const idx = m.index ?? 0;
2092
+ if (idx > 0 && cleaned[idx - 1] === "!") continue;
2093
+ const href = m[2];
2094
+ if (href && SAFE_URL_RE.test(href) && IMG_HREF_RE.test(href) && deAmp(m[1]) === deAmp(href)) {
2095
+ candidates.push({ url: href, pos: idx });
2096
+ break;
2097
+ }
2098
+ }
2099
+ }
2100
+ if (candidates.length === 0) return null;
2101
+ candidates.sort((a2, b) => a2.pos - b.pos);
2102
+ return candidates[0].url;
1889
2103
  }
1890
2104
  function proxifyFound(src, width, height, format) {
1891
- const decoded = he.decode(src);
2105
+ const decoded = he2.decode(src);
1892
2106
  if (isGifLink(decoded)) {
1893
2107
  return proxifyImageSrc(decoded, 0, 0, format);
1894
2108
  }
@@ -1906,7 +2120,7 @@ function getImage(entry, width = 0, height = 0, format = "match") {
1906
2120
  }
1907
2121
  }
1908
2122
  if (meta && typeof meta.image === "string" && meta.image.length > 0) {
1909
- const decodedImage = he.decode(meta.image);
2123
+ const decodedImage = he2.decode(meta.image);
1910
2124
  if (isGifLink(decodedImage)) {
1911
2125
  return proxifyImageSrc(decodedImage, 0, 0, format);
1912
2126
  }
@@ -1914,7 +2128,7 @@ function getImage(entry, width = 0, height = 0, format = "match") {
1914
2128
  }
1915
2129
  if (meta && meta.image && !!meta.image.length && meta.image[0]) {
1916
2130
  if (typeof meta.image[0] === "string") {
1917
- const decodedImage = he.decode(meta.image[0]);
2131
+ const decodedImage = he2.decode(meta.image[0]);
1918
2132
  if (isGifLink(decodedImage)) {
1919
2133
  return proxifyImageSrc(decodedImage, 0, 0, format);
1920
2134
  }
@@ -1944,6 +2158,30 @@ function getImage(entry, width = 0, height = 0, format = "match") {
1944
2158
  }
1945
2159
  return null;
1946
2160
  }
2161
+ function getEntryImageRawUrl(obj) {
2162
+ if (typeof obj === "string") {
2163
+ const src = findFirstImageUrl(obj, true);
2164
+ return src ? decodeImageSrc(src) : null;
2165
+ }
2166
+ let meta;
2167
+ if (typeof obj.json_metadata === "object") {
2168
+ meta = obj.json_metadata;
2169
+ } else {
2170
+ try {
2171
+ meta = JSON.parse(obj.json_metadata);
2172
+ } catch (e) {
2173
+ meta = null;
2174
+ }
2175
+ }
2176
+ if (meta && typeof meta.image === "string" && meta.image.length > 0) {
2177
+ return decodeImageSrc(meta.image);
2178
+ }
2179
+ if (meta && meta.image && !!meta.image.length && typeof meta.image[0] === "string" && meta.image[0].length > 0) {
2180
+ return decodeImageSrc(meta.image[0]);
2181
+ }
2182
+ const bodySrc = findFirstImageUrl(obj.body, true);
2183
+ return bodySrc ? decodeImageSrc(bodySrc) : null;
2184
+ }
1947
2185
  function catchPostImage(obj, width = 0, height = 0, format = "match") {
1948
2186
  if (typeof obj === "string") {
1949
2187
  const fast = findFirstImageUrl(obj);
@@ -2045,7 +2283,7 @@ function postBodySummary(entryBody, length = 200, platform = "web") {
2045
2283
  text2 = joint(text2.split(" "), length);
2046
2284
  }
2047
2285
  if (text2) {
2048
- text2 = he.decode(text2);
2286
+ text2 = he2.decode(text2);
2049
2287
  }
2050
2288
  return text2;
2051
2289
  }
@@ -2065,6 +2303,6 @@ function getPostBodySummary(obj, length, platform) {
2065
2303
  return res;
2066
2304
  }
2067
2305
 
2068
- export { IMAGE_SIZES, SECTION_LIST, buildSrcSet, catchPostImage, isValidPermlink, getPostBodySummary as postBodySummary, proxifyImageSrc, markdown2Html as renderPostBody, setCacheSize, setProxyBase, setSlowRenderThresholdMs, simpleMarkdownToHTML };
2306
+ export { IMAGE_SIZES, SECTION_LIST, buildPictureSources, buildSrcSet, buildSrcSetForFormat, catchPostImage, getEntryImageRawUrl, isAllowedEmbedSrc, isPictureEligibleRawUrl, isValidPermlink, getPostBodySummary as postBodySummary, proxifyImageSrc, markdown2Html as renderPostBody, setCacheSize, setProxyBase, setSlowRenderThresholdMs, simpleMarkdownToHTML };
2069
2307
  //# sourceMappingURL=index.mjs.map
2070
2308
  //# sourceMappingURL=index.mjs.map