@ecency/render-helper 2.5.27 → 2.5.29

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,5 +1,5 @@
1
1
  import { DOMParser as DOMParser$1, XMLSerializer } from '@xmldom/xmldom';
2
- import he2 from 'he';
2
+ import { decodeHTML } from 'entities';
3
3
  import xss from 'xss';
4
4
  import querystring from 'querystring';
5
5
  import { LRUCache } from 'lru-cache';
@@ -280,8 +280,19 @@ function createParser() {
280
280
  });
281
281
  }
282
282
  var DOMParser = createParser();
283
+ var LEADING_ZEROS_DEC = /&#0+(?=[0-9])/g;
284
+ var LEADING_ZEROS_HEX = /&#x0+(?=[0-9a-f])/gi;
285
+ var OVERLONG_NUMERIC_REF = /&#(?:x[0-9a-f]{256,}|[0-9]{309,});?/gi;
286
+ function decodeEntities(value) {
287
+ const safe = value.replace(LEADING_ZEROS_DEC, "&#").replace(LEADING_ZEROS_HEX, (m) => m.slice(0, 3)).replace(OVERLONG_NUMERIC_REF, "\uFFFD");
288
+ try {
289
+ return decodeHTML(safe);
290
+ } catch {
291
+ return safe;
292
+ }
293
+ }
283
294
  function decodeImageSrc(src) {
284
- const entityDecoded = he2.decode(src);
295
+ const entityDecoded = decodeEntities(src);
285
296
  try {
286
297
  return decodeURIComponent(entityDecoded).trim();
287
298
  } catch {
@@ -736,7 +747,7 @@ var isSafeNavValue = (value) => {
736
747
  const isRelative = /^(\/\/|\/[^/]?|#|\?|[a-z0-9._\-]+(\/|$))/i.test(trimmed);
737
748
  return isSafeScheme || isRelative;
738
749
  };
739
- var decodeEntities = (input) => input.replace(/&#(\d+);?/g, (_, dec) => String.fromCodePoint(Number(dec))).replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)));
750
+ var decodeEntities2 = (input) => input.replace(/&#(\d+);?/g, (_, dec) => String.fromCodePoint(Number(dec))).replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)));
740
751
  var isProxyPSrcset = (srcset) => {
741
752
  const base = trimTrailingSlash(getProxyBase());
742
753
  const candidates = srcset.split(",").map((c) => c.trim().split(/\s+/)[0]).filter(Boolean);
@@ -750,7 +761,7 @@ function sanitizeHtml(html) {
750
761
  css: false,
751
762
  // block style attrs entirely for safety
752
763
  onTagAttr: (tag, name, value) => {
753
- const decoded = decodeEntities(value.trim());
764
+ const decoded = decodeEntities2(value.trim());
754
765
  const decodedLower = decoded.toLowerCase();
755
766
  if (name.startsWith("on")) return "";
756
767
  if (tag === "img" && name === "src" && !/^https?:\/\//.test(decodedLower)) return "";
@@ -10223,6 +10234,8 @@ function markdown2Html(obj, forApp = true, _webp = false, parentDomain = "ecency
10223
10234
  cacheSet(key, res);
10224
10235
  return res;
10225
10236
  }
10237
+
10238
+ // src/catch-post-image.ts
10226
10239
  var gifLinkRegex = /\.(gif)$/i;
10227
10240
  function isGifLink(link) {
10228
10241
  return gifLinkRegex.test(link);
@@ -10230,28 +10243,455 @@ function isGifLink(link) {
10230
10243
  var BACKTICK_FENCE_RE = /```[\s\S]*?```/g;
10231
10244
  var TILDE_FENCE_RE = /~~~[\s\S]*?~~~/g;
10232
10245
  var INLINE_CODE_RE = /`[^`\n]*`/g;
10233
- var INDENTED_CODE_RE = /^(?: {4}|\t).+$/gm;
10246
+ var OPEN_TAG_NAME_END = /[\t\f\r />]/;
10247
+ var CLOSE_TAG_NAME_END = /[\s>]/;
10248
+ function isWholeTagName(lower, idx, end) {
10249
+ const next = lower[idx];
10250
+ return next === void 0 || end.test(next);
10251
+ }
10252
+ function findTag(lower, tag, from, end) {
10253
+ let at = lower.indexOf(tag, from);
10254
+ while (at !== -1 && !isWholeTagName(lower, at + tag.length, end)) {
10255
+ at = lower.indexOf(tag, at + tag.length);
10256
+ }
10257
+ return at;
10258
+ }
10259
+ function findOpenTagEnd(lower, openAt) {
10260
+ let quote = "";
10261
+ for (let i2 = openAt + 1; i2 < lower.length; i2++) {
10262
+ const c = lower[i2];
10263
+ if (c === "\n") return NaN;
10264
+ if (quote) {
10265
+ if (c === quote) quote = "";
10266
+ } else if (c === '"' || c === "'") {
10267
+ quote = c;
10268
+ } else if (c === ">") {
10269
+ return lower[i2 - 1] === "/" ? NaN : i2;
10270
+ }
10271
+ }
10272
+ return -1;
10273
+ }
10274
+ var HTML_BLOCK_TAGS = /* @__PURE__ */ new Set([
10275
+ "article",
10276
+ "aside",
10277
+ "button",
10278
+ "blockquote",
10279
+ "body",
10280
+ "canvas",
10281
+ "caption",
10282
+ "col",
10283
+ "colgroup",
10284
+ "dd",
10285
+ "div",
10286
+ "dl",
10287
+ "dt",
10288
+ "embed",
10289
+ "fieldset",
10290
+ "figcaption",
10291
+ "figure",
10292
+ "footer",
10293
+ "form",
10294
+ "h1",
10295
+ "h2",
10296
+ "h3",
10297
+ "h4",
10298
+ "h5",
10299
+ "h6",
10300
+ "header",
10301
+ "hgroup",
10302
+ "hr",
10303
+ "iframe",
10304
+ "li",
10305
+ "map",
10306
+ "object",
10307
+ "ol",
10308
+ "output",
10309
+ "p",
10310
+ "pre",
10311
+ "progress",
10312
+ "script",
10313
+ "section",
10314
+ "style",
10315
+ "table",
10316
+ "tbody",
10317
+ "td",
10318
+ "textarea",
10319
+ "tfoot",
10320
+ "th",
10321
+ "tr",
10322
+ "thead",
10323
+ "ul",
10324
+ "video"
10325
+ ]);
10326
+ var HTML_BLOCK_LINE_RE = /^ {0,3}<(?:[!?]|([a-z]{1,15})[\s/>]|\/([a-z]{1,15})[\s>])/;
10327
+ var BLOCKQUOTE_PREFIX_RE = /^ {0,3}> ?/;
10328
+ var LIST_PREFIX_RE = /^(?:[-*+]|\d{1,9}[.)]) +/;
10329
+ function markLines(lower) {
10330
+ const block2 = new Uint8Array(lower.length);
10331
+ const code2 = new Uint8Array(lower.length);
10332
+ let inBlock = false;
10333
+ let listIndent = 0;
10334
+ let nestedItem = false;
10335
+ let lineStart = 0;
10336
+ while (lineStart <= lower.length) {
10337
+ let lineEnd = lower.indexOf("\n", lineStart);
10338
+ if (lineEnd === -1) lineEnd = lower.length;
10339
+ let line = lower.slice(lineStart, lineEnd);
10340
+ if (inBlock) {
10341
+ if (line.trim() === "") {
10342
+ inBlock = false;
10343
+ listIndent = 0;
10344
+ nestedItem = false;
10345
+ } else {
10346
+ block2.fill(1, lineStart, lineEnd);
10347
+ }
10348
+ lineStart = lineEnd + 1;
10349
+ continue;
10350
+ }
10351
+ let stripped = 0;
10352
+ let sawList = false;
10353
+ let lastWasList = false;
10354
+ let inlineRemainder = false;
10355
+ for (; ; ) {
10356
+ const bq = BLOCKQUOTE_PREFIX_RE.exec(line);
10357
+ if (bq) {
10358
+ line = line.slice(bq[0].length);
10359
+ stripped += bq[0].length;
10360
+ lastWasList = false;
10361
+ inlineRemainder = false;
10362
+ continue;
10363
+ }
10364
+ const lm = LIST_PREFIX_RE.exec(line);
10365
+ if (lm) {
10366
+ line = line.slice(lm[0].length);
10367
+ stripped += lm[0].length;
10368
+ if (lastWasList) inlineRemainder = true;
10369
+ sawList = true;
10370
+ lastWasList = true;
10371
+ continue;
10372
+ }
10373
+ break;
10374
+ }
10375
+ if (sawList) {
10376
+ listIndent = stripped;
10377
+ nestedItem = inlineRemainder;
10378
+ } else if (listIndent > 0 && line.trim() !== "") {
10379
+ let indent = 0;
10380
+ while (indent < listIndent && line[indent] === " ") indent++;
10381
+ if (indent >= Math.min(listIndent, 2)) {
10382
+ line = line.slice(indent);
10383
+ inlineRemainder = nestedItem;
10384
+ } else {
10385
+ listIndent = 0;
10386
+ nestedItem = false;
10387
+ }
10388
+ }
10389
+ const blank = line.trim() === "";
10390
+ if (blank) {
10391
+ inBlock = false;
10392
+ listIndent = 0;
10393
+ nestedItem = false;
10394
+ } else if (inlineRemainder) {
10395
+ inBlock = false;
10396
+ } else if (!inBlock && /^(?: {4}|\t)/.test(line)) {
10397
+ code2.fill(1, lineStart, lineEnd);
10398
+ } else if (!inBlock) {
10399
+ const m = HTML_BLOCK_LINE_RE.exec(line);
10400
+ if (m) {
10401
+ const tag = m[1] ?? m[2];
10402
+ inBlock = tag === void 0 || HTML_BLOCK_TAGS.has(tag);
10403
+ }
10404
+ }
10405
+ if (inBlock && !blank) block2.fill(1, lineStart, lineEnd);
10406
+ lineStart = lineEnd + 1;
10407
+ }
10408
+ return { block: block2, code: code2 };
10409
+ }
10410
+ var blankChars = (s) => s.replace(/[^\n]/g, " ");
10411
+ function blankMatches(text3, re) {
10412
+ return text3.replace(re, blankChars);
10413
+ }
10414
+ function blankSpans(input, open, close, tagNames, blockMask) {
10415
+ const { text: text3, lower } = input;
10416
+ const findOpen = (from2) => {
10417
+ if (!tagNames) return lower.indexOf(open, from2);
10418
+ let at = findTag(lower, open, from2, OPEN_TAG_NAME_END);
10419
+ while (at !== -1 && (Number.isNaN(findOpenTagEnd(lower, at)) || blockMask !== null && !blockMask[at])) {
10420
+ at = findTag(lower, open, at + open.length, OPEN_TAG_NAME_END);
10421
+ }
10422
+ return at;
10423
+ };
10424
+ const findClose = (from2) => tagNames ? findTag(lower, close, from2, CLOSE_TAG_NAME_END) : lower.indexOf(close, from2);
10425
+ let start = findOpen(0);
10426
+ if (start === -1) return input;
10427
+ const textParts = [];
10428
+ const lowerParts = [];
10429
+ let from = 0;
10430
+ while (start !== -1) {
10431
+ const end = findClose(start + open.length);
10432
+ let to;
10433
+ if (end === -1) {
10434
+ to = text3.length;
10435
+ } else if (tagNames) {
10436
+ const gt = lower.indexOf(">", end + close.length);
10437
+ to = gt === -1 ? text3.length : gt + 1;
10438
+ } else {
10439
+ to = end + close.length;
10440
+ }
10441
+ const blanked = blankChars(lower.slice(start, to));
10442
+ textParts.push(text3.slice(from, start), blanked);
10443
+ lowerParts.push(lower.slice(from, start), blanked);
10444
+ from = to;
10445
+ start = to >= text3.length ? -1 : findOpen(to);
10446
+ }
10447
+ textParts.push(text3.slice(from));
10448
+ lowerParts.push(lower.slice(from));
10449
+ return { text: textParts.join(""), lower: lowerParts.join("") };
10450
+ }
10451
+ function blankMasked(input, mask) {
10452
+ let text3 = "";
10453
+ let lower = "";
10454
+ let from = 0;
10455
+ for (let i2 = 0; i2 < mask.length; i2++) {
10456
+ if (!mask[i2]) continue;
10457
+ let j = i2;
10458
+ while (j < mask.length && mask[j]) j++;
10459
+ text3 += input.text.slice(from, i2) + blankChars(input.text.slice(i2, j));
10460
+ lower += input.lower.slice(from, i2) + blankChars(input.lower.slice(i2, j));
10461
+ from = j;
10462
+ i2 = j;
10463
+ }
10464
+ if (from === 0) return input;
10465
+ return { text: text3 + input.text.slice(from), lower: lower + input.lower.slice(from) };
10466
+ }
10467
+ function stripHiddenRegions(text3) {
10468
+ let spellings = { text: text3, lower: text3.toLowerCase() };
10469
+ const { block: blockMask, code: codeMask } = markLines(spellings.lower);
10470
+ spellings = blankMasked(spellings, codeMask);
10471
+ spellings = blankSpans(spellings, "<!--", "-->", false, null);
10472
+ spellings = blankSpans(spellings, "<style", "</style", true, null);
10473
+ spellings = blankSpans(spellings, "<pre", "</pre", true, blockMask);
10474
+ spellings = blankSpans(spellings, "<code", "</code", true, blockMask);
10475
+ return spellings.text;
10476
+ }
10234
10477
  var MD_IMAGE_RE = /!\[[^[\]]*\]\(\s*([^)\s]{1,2048})(?:\s+["'][^"']*["'])?\s*\)/;
10235
10478
  var MD_IMAGE_PRESENT_RE = /!\[[^[\]]*\]\(\s*[^\s)]/;
10236
10479
  var HTML_IMAGE_RE = /<img\b[^>]*?\bsrc\s*=\s*["']([^"']+)["']/i;
10237
- var BARE_IMAGE_RE = /(^|\s)(https?:\/\/[^\s<>"'()[\]]+\.(?:tiff?|jpe?g|gif|png|svg|ico|heic|webp|arw)(?:[?#][^\s<>"'()[\]]*)?)/im;
10480
+ var URL_TOKEN_RE = /https?:\/\/[^\s<>"'()[\]]+/gi;
10481
+ var IMAGE_EXT_G = /\.(?:tiff?|jpe?g|gif|png|svg|ico|heic|webp|arw)/gi;
10482
+ var YOUTUBE_ID_RE = /^[^"&?/\s]{11}$/;
10483
+ function imageToken(token) {
10484
+ let end = -1;
10485
+ for (const m of token.matchAll(IMAGE_EXT_G)) {
10486
+ end = (m.index ?? 0) + m[0].length;
10487
+ }
10488
+ if (end === -1) return null;
10489
+ if (end < token.length && (token[end] === "?" || token[end] === "#")) end = token.length;
10490
+ const url = token.slice(0, end);
10491
+ return SAFE_URL_RE.test(url) ? url : null;
10492
+ }
10493
+ function youtubeIdOf(url) {
10494
+ const m = /^https?:\/\/([^/?#]+)/i.exec(url);
10495
+ if (!m) return null;
10496
+ const host = m[1].toLowerCase();
10497
+ const isShort = host === "youtu.be";
10498
+ const isFull = host === "youtube.com" || host.endsWith(".youtube.com");
10499
+ if (!isShort && !isFull) return null;
10500
+ const rest = url.slice(m[0].length);
10501
+ const hashAt = rest.indexOf("#");
10502
+ const beforeHash = hashAt === -1 ? rest : rest.slice(0, hashAt);
10503
+ const qAt = beforeHash.indexOf("?");
10504
+ const path = qAt === -1 ? beforeHash : beforeHash.slice(0, qAt);
10505
+ const query = qAt === -1 ? "" : beforeHash.slice(qAt + 1);
10506
+ const candidate = (value) => {
10507
+ const id = value === void 0 ? "" : value.slice(0, 11);
10508
+ return YOUTUBE_ID_RE.test(id) ? id : null;
10509
+ };
10510
+ if (isFull && query) {
10511
+ for (const part of query.split("&")) {
10512
+ if (part.startsWith("v=")) {
10513
+ const id = candidate(part.slice(2));
10514
+ if (id) return id;
10515
+ }
10516
+ }
10517
+ }
10518
+ const segments = path.split("/").filter((seg) => seg.length > 0);
10519
+ if (isShort) return candidate(segments[0]);
10520
+ if (segments.length >= 2 && ["v", "e", "embed", "shorts"].includes(segments[0].toLowerCase())) {
10521
+ return candidate(segments[1]);
10522
+ }
10523
+ if (segments.length >= 3) return candidate(segments[segments.length - 1]);
10524
+ return null;
10525
+ }
10526
+ function isAutolinkAt(text3, idx) {
10527
+ return /^https?:\/\//i.test(text3.slice(idx, idx + 8));
10528
+ }
10529
+ function markInsideTags(text3) {
10530
+ const marks = new Uint8Array(text3.length);
10531
+ let inTag = false;
10532
+ let quote = "";
10533
+ for (let i2 = 0; i2 < text3.length; i2++) {
10534
+ const c = text3[i2];
10535
+ if (!inTag) {
10536
+ if (c === "<" && i2 + 1 < text3.length && /[A-Za-z/!?]/.test(text3[i2 + 1]) && !isAutolinkAt(text3, i2 + 1)) {
10537
+ inTag = true;
10538
+ marks[i2] = 1;
10539
+ }
10540
+ continue;
10541
+ }
10542
+ marks[i2] = 1;
10543
+ if (quote) {
10544
+ if (c === quote) quote = "";
10545
+ } else if (c === '"' || c === "'") {
10546
+ quote = c;
10547
+ } else if (c === ">") {
10548
+ inTag = false;
10549
+ }
10550
+ }
10551
+ return marks;
10552
+ }
10553
+ function isStandalone(scan, idx) {
10554
+ if (scan.inTag[idx]) return false;
10555
+ if (idx === 0) return true;
10556
+ const text3 = scan.text;
10557
+ const prev = text3[idx - 1];
10558
+ if (/[\w/.:%?&=#[-]/.test(prev)) return false;
10559
+ const prev2 = idx > 1 ? text3[idx - 2] : "";
10560
+ if (prev === "(" && prev2 === "]") return false;
10561
+ return true;
10562
+ }
10563
+ function* standaloneMatches(scan, classify) {
10564
+ for (const m of scan.text.matchAll(URL_TOKEN_RE)) {
10565
+ const idx = m.index ?? 0;
10566
+ if (!isStandalone(scan, idx)) continue;
10567
+ const value = classify(m[0]);
10568
+ if (value !== null) yield { url: value, pos: idx };
10569
+ }
10570
+ }
10571
+ function firstStandalone(scan, classify) {
10572
+ for (const hit of standaloneMatches(scan, classify)) return hit;
10573
+ return null;
10574
+ }
10575
+ var HREF_ATTR_RE = /\shref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/i;
10576
+ function hasGluedAttribute(tag) {
10577
+ let quote = "";
10578
+ for (let i2 = 0; i2 < tag.length; i2++) {
10579
+ const c = tag[i2];
10580
+ if (quote) {
10581
+ if (c === quote) {
10582
+ quote = "";
10583
+ const next = tag[i2 + 1];
10584
+ if (next !== void 0 && /[A-Za-z]/.test(next)) return true;
10585
+ }
10586
+ } else if (c === '"' || c === "'") {
10587
+ quote = c;
10588
+ }
10589
+ }
10590
+ return false;
10591
+ }
10238
10592
  var MD_LINK_RE = /\[([^[\]]*)\]\(\s*([^)\s[]+)(?:\s+["'][^"']*["'])?\s*\)/g;
10239
- var IMG_HREF_RE = /https?:\/\/.*\.(?:tiff?|jpe?g|gif|png|svg|ico|heic|webp|arw)/i;
10240
10593
  var SAFE_URL_RE = /^https?:\/\//i;
10594
+ var IMG_EXT_RE = /\.(?:tiff?|jpe?g|gif|png|svg|ico|heic|webp|arw)/i;
10595
+ var isImageHref = (href) => SAFE_URL_RE.test(href) && IMG_EXT_RE.test(href);
10241
10596
  function findFirstImageUrl(body, includeBareUrls = false) {
10242
- if (!body) return null;
10243
- const cleaned = body.replace(BACKTICK_FENCE_RE, "").replace(TILDE_FENCE_RE, "").replace(INLINE_CODE_RE, "").replace(INDENTED_CODE_RE, "");
10597
+ return findFirstImageCandidate(prepareBody(body), includeBareUrls).candidate?.url ?? null;
10598
+ }
10599
+ function stripCodeRegions(body) {
10600
+ let text3 = blankMatches(body, BACKTICK_FENCE_RE);
10601
+ text3 = blankMatches(text3, TILDE_FENCE_RE);
10602
+ text3 = blankMatches(text3, INLINE_CODE_RE);
10603
+ return stripHiddenRegions(text3);
10604
+ }
10605
+ function blankUnequalAnchors(cleaned, textContent) {
10606
+ const lower = cleaned.toLowerCase();
10607
+ const parts = [];
10608
+ let from = 0;
10609
+ let at = findTag(lower, "<a", 0, OPEN_TAG_NAME_END);
10610
+ while (at !== -1) {
10611
+ const gt = findOpenTagEnd(lower, at);
10612
+ if (Number.isNaN(gt) || gt === -1 || hasGluedAttribute(cleaned.slice(at, gt))) {
10613
+ at = findTag(lower, "<a", at + 2, OPEN_TAG_NAME_END);
10614
+ continue;
10615
+ }
10616
+ const closeAt = findTag(lower, "</a", gt + 1, CLOSE_TAG_NAME_END);
10617
+ const innerEnd = closeAt === -1 ? cleaned.length : closeAt;
10618
+ let spanEnd = cleaned.length;
10619
+ if (closeAt !== -1) {
10620
+ const closeGt = lower.indexOf(">", closeAt + 3);
10621
+ spanEnd = closeGt === -1 ? cleaned.length : closeGt + 1;
10622
+ }
10623
+ const hrefMatch = HREF_ATTR_RE.exec(cleaned.slice(at, gt));
10624
+ const href = hrefMatch ? hrefMatch[1] ?? hrefMatch[2] ?? hrefMatch[3] ?? "" : "";
10625
+ const inner = cleaned.slice(gt + 1, innerEnd);
10626
+ let text3;
10627
+ if (textContent) {
10628
+ text3 = stripHtmlTags(inner);
10629
+ } else {
10630
+ const firstTag = inner.search(/<[A-Za-z/!]/);
10631
+ text3 = firstTag === -1 ? inner : inner.slice(0, firstTag);
10632
+ }
10633
+ if (!href || decodeEntities(text3.trim()) !== decodeEntities(href.trim())) {
10634
+ parts.push(cleaned.slice(from, at), blankChars(cleaned.slice(at, spanEnd)));
10635
+ from = spanEnd;
10636
+ }
10637
+ at = spanEnd >= cleaned.length ? -1 : findTag(lower, "<a", spanEnd, OPEN_TAG_NAME_END);
10638
+ }
10639
+ if (parts.length === 0) return cleaned;
10640
+ parts.push(cleaned.slice(from));
10641
+ return parts.join("");
10642
+ }
10643
+ var EMPTY_SCAN = { text: "", inTag: new Uint8Array(0) };
10644
+ function prepareBody(body) {
10645
+ const cleaned = body ? stripCodeRegions(body) : "";
10646
+ if (!cleaned) return { cleaned, image: EMPTY_SCAN, video: EMPTY_SCAN };
10647
+ const imageText = blankUnequalAnchors(cleaned, false);
10648
+ const videoText = blankUnequalAnchors(cleaned, true);
10649
+ return {
10650
+ cleaned,
10651
+ image: { text: imageText, inTag: markInsideTags(imageText) },
10652
+ video: { text: videoText, inTag: markInsideTags(videoText) }
10653
+ };
10654
+ }
10655
+ function findFirstVideoPoster(prepared) {
10656
+ const { cleaned } = prepared;
10657
+ if (!cleaned) return null;
10658
+ let best = null;
10659
+ for (const hit of standaloneMatches(prepared.video, youtubeIdOf)) {
10660
+ best = { url: hit.url, pos: hit.pos };
10661
+ break;
10662
+ }
10663
+ for (const m of cleaned.matchAll(MD_LINK_RE)) {
10664
+ const idx = m.index ?? 0;
10665
+ if (idx > 0 && cleaned[idx - 1] === "!") continue;
10666
+ if (best && idx >= best.pos) break;
10667
+ const href = m[2];
10668
+ if (href && m[1].trim() === href) {
10669
+ const id = youtubeIdOf(href);
10670
+ if (id) {
10671
+ best = { url: id, pos: idx };
10672
+ break;
10673
+ }
10674
+ }
10675
+ }
10676
+ if (!best) return null;
10677
+ return { url: `https://img.youtube.com/vi/${best.url.split("?")[0]}/hqdefault.jpg`, pos: best.pos };
10678
+ }
10679
+ var NONE = { candidate: null, ambiguous: false };
10680
+ var AMBIGUOUS = { candidate: null, ambiguous: true };
10681
+ function findFirstImageCandidate(prepared, includeBareUrls = false) {
10682
+ const { cleaned } = prepared;
10683
+ if (!cleaned) return NONE;
10244
10684
  const mdMatch = cleaned.match(MD_IMAGE_RE);
10245
10685
  const htmlMatch = cleaned.match(HTML_IMAGE_RE);
10246
10686
  if (mdMatch) {
10247
10687
  const url = mdMatch[1];
10248
10688
  if (!url || !SAFE_URL_RE.test(url) || url.includes("(")) {
10249
- return null;
10689
+ return AMBIGUOUS;
10250
10690
  }
10251
10691
  }
10252
10692
  const priorRegion = mdMatch ? cleaned.slice(0, mdMatch.index ?? 0) : cleaned;
10253
10693
  if (MD_IMAGE_PRESENT_RE.test(priorRegion)) {
10254
- return null;
10694
+ return AMBIGUOUS;
10255
10695
  }
10256
10696
  const candidates = [];
10257
10697
  if (mdMatch) candidates.push({ url: mdMatch[1], pos: mdMatch.index ?? 0 });
@@ -10259,33 +10699,58 @@ function findFirstImageUrl(body, includeBareUrls = false) {
10259
10699
  candidates.push({ url: htmlMatch[1], pos: htmlMatch.index ?? 0 });
10260
10700
  }
10261
10701
  if (includeBareUrls) {
10262
- const bareMatch = cleaned.match(BARE_IMAGE_RE);
10263
- if (bareMatch && bareMatch[2] && SAFE_URL_RE.test(bareMatch[2])) {
10264
- candidates.push({ url: bareMatch[2], pos: (bareMatch.index ?? 0) + bareMatch[1].length });
10702
+ const bareMatch = firstStandalone(prepared.image, imageToken);
10703
+ if (bareMatch && SAFE_URL_RE.test(bareMatch.url)) {
10704
+ candidates.push(bareMatch);
10265
10705
  }
10266
10706
  const deAmp = (s) => s.trim().replace(/&amp;/g, "&");
10267
10707
  for (const m of cleaned.matchAll(MD_LINK_RE)) {
10268
10708
  const idx = m.index ?? 0;
10269
10709
  if (idx > 0 && cleaned[idx - 1] === "!") continue;
10270
10710
  const href = m[2];
10271
- if (href && SAFE_URL_RE.test(href) && IMG_HREF_RE.test(href) && deAmp(m[1]) === deAmp(href)) {
10711
+ if (href && isImageHref(href) && deAmp(m[1]) === deAmp(href)) {
10272
10712
  candidates.push({ url: href, pos: idx });
10273
10713
  break;
10274
10714
  }
10275
10715
  }
10276
10716
  }
10277
- if (candidates.length === 0) return null;
10717
+ if (candidates.length === 0) return NONE;
10278
10718
  candidates.sort((a2, b) => a2.pos - b.pos);
10279
- return candidates[0].url;
10719
+ return { candidate: candidates[0], ambiguous: false };
10720
+ }
10721
+ function fastBodyImage(body, width, height, format) {
10722
+ const prepared = prepareBody(body);
10723
+ const strict = findFirstImageCandidate(prepared, false);
10724
+ if (strict.candidate) {
10725
+ return proxifyFound(strict.candidate.url, width, height, format);
10726
+ }
10727
+ if (strict.ambiguous) {
10728
+ return null;
10729
+ }
10730
+ const bare = findFirstImageCandidate(prepared, true).candidate;
10731
+ const poster = findFirstVideoPoster(prepared);
10732
+ if (poster && (!bare || poster.pos < bare.pos)) {
10733
+ return proxifyFound(proxifyImageSrc(poster.url, 0, 0, "match"), width, height, format);
10734
+ }
10735
+ return bare ? proxifyFound(bare.url, width, height, format) : null;
10736
+ }
10737
+ function firstMetaUrl(value) {
10738
+ if (typeof value === "string" && value.trim().length > 0) {
10739
+ return value;
10740
+ }
10741
+ if (Array.isArray(value)) {
10742
+ return value.find((url) => typeof url === "string" && url.trim().length > 0);
10743
+ }
10744
+ return void 0;
10280
10745
  }
10281
10746
  function proxifyFound(src, width, height, format) {
10282
- const decoded = he2.decode(src);
10747
+ const decoded = decodeEntities(src);
10283
10748
  if (isGifLink(decoded)) {
10284
10749
  return proxifyImageSrc(decoded, 0, 0, format);
10285
10750
  }
10286
10751
  return proxifyImageSrc(decoded, width, height, format);
10287
10752
  }
10288
- function getImage(entry, width = 0, height = 0, format = "match") {
10753
+ function getImage(entry, width = 0, height = 0, format = "match", fastMode = false) {
10289
10754
  let meta;
10290
10755
  if (typeof entry.json_metadata === "object") {
10291
10756
  meta = entry.json_metadata;
@@ -10296,8 +10761,16 @@ function getImage(entry, width = 0, height = 0, format = "match") {
10296
10761
  meta = null;
10297
10762
  }
10298
10763
  }
10764
+ const thumbnail = firstMetaUrl(meta?.thumbnails);
10765
+ if (thumbnail) {
10766
+ const decodedThumbnail = decodeEntities(thumbnail);
10767
+ const proxied = isGifLink(decodedThumbnail) ? proxifyImageSrc(decodedThumbnail, 0, 0, format) : proxifyImageSrc(decodedThumbnail, width, height, format);
10768
+ if (proxied) {
10769
+ return proxied;
10770
+ }
10771
+ }
10299
10772
  if (meta && typeof meta.image === "string" && meta.image.length > 0) {
10300
- const decodedImage = he2.decode(meta.image);
10773
+ const decodedImage = decodeEntities(meta.image);
10301
10774
  if (isGifLink(decodedImage)) {
10302
10775
  return proxifyImageSrc(decodedImage, 0, 0, format);
10303
10776
  }
@@ -10305,7 +10778,7 @@ function getImage(entry, width = 0, height = 0, format = "match") {
10305
10778
  }
10306
10779
  if (meta && meta.image && !!meta.image.length && meta.image[0]) {
10307
10780
  if (typeof meta.image[0] === "string") {
10308
- const decodedImage = he2.decode(meta.image[0]);
10781
+ const decodedImage = decodeEntities(meta.image[0]);
10309
10782
  if (isGifLink(decodedImage)) {
10310
10783
  return proxifyImageSrc(decodedImage, 0, 0, format);
10311
10784
  }
@@ -10316,6 +10789,9 @@ function getImage(entry, width = 0, height = 0, format = "match") {
10316
10789
  }
10317
10790
  return proxifyImageSrc(meta.image[0], width, height, format);
10318
10791
  }
10792
+ if (fastMode) {
10793
+ return fastBodyImage(entry.body, width, height, format);
10794
+ }
10319
10795
  const fast = findFirstImageUrl(entry.body);
10320
10796
  if (fast) {
10321
10797
  return proxifyFound(fast, width, height, format);
@@ -10359,8 +10835,12 @@ function getEntryImageRawUrl(obj) {
10359
10835
  const bodySrc = findFirstImageUrl(obj.body, true);
10360
10836
  return bodySrc ? decodeImageSrc(bodySrc) : null;
10361
10837
  }
10362
- function catchPostImage(obj, width = 0, height = 0, format = "match") {
10838
+ function catchPostImage(obj, width = 0, height = 0, format = "match", options = {}) {
10839
+ const fastMode = options.fast === true;
10363
10840
  if (typeof obj === "string") {
10841
+ if (fastMode) {
10842
+ return fastBodyImage(obj, width, height, format);
10843
+ }
10364
10844
  const fast = findFirstImageUrl(obj);
10365
10845
  if (fast) {
10366
10846
  return proxifyFound(fast, width, height, format);
@@ -10380,15 +10860,17 @@ function catchPostImage(obj, width = 0, height = 0, format = "match") {
10380
10860
  }
10381
10861
  return null;
10382
10862
  }
10383
- const key = `${makeEntryCacheKey(obj)}-${width}x${height}-${format}`;
10863
+ const key = `${makeEntryCacheKey(obj)}-${width}x${height}-${format}${fastMode ? "-fast" : ""}`;
10384
10864
  const item = cacheGet(key);
10385
- if (item) {
10865
+ if (item !== void 0) {
10386
10866
  return item;
10387
10867
  }
10388
- const res = getImage(obj, width, height, format);
10868
+ const res = getImage(obj, width, height, format, fastMode);
10389
10869
  cacheSet(key, res);
10390
10870
  return res;
10391
10871
  }
10872
+
10873
+ // src/post-body-summary.ts
10392
10874
  var summaryRenderer = new Remarkable({
10393
10875
  html: true,
10394
10876
  breaks: true,
@@ -10460,7 +10942,7 @@ function postBodySummary(entryBody, length = 200, platform = "web") {
10460
10942
  text3 = joint(text3.split(" "), length);
10461
10943
  }
10462
10944
  if (text3) {
10463
- text3 = he2.decode(text3);
10945
+ text3 = decodeEntities(text3);
10464
10946
  }
10465
10947
  return text3;
10466
10948
  }