@ecency/render-helper 2.5.28 → 2.5.30
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.
- package/dist/browser/index.d.ts +21 -7
- package/dist/browser/index.js +504 -21
- package/dist/browser/index.js.map +1 -1
- package/dist/node/index.cjs +504 -21
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.mjs +504 -21
- package/dist/node/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/browser/index.d.ts
CHANGED
|
@@ -65,15 +65,29 @@ declare function setSlowRenderThresholdMs(ms: number): void;
|
|
|
65
65
|
declare function markdown2Html(obj: Entry | string, forApp?: boolean, _webp?: boolean, parentDomain?: string, seoContext?: SeoContext, renderOptions?: RenderOptions): string;
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
* The RAW (pre-proxify) URL of an entry's
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
68
|
+
* The RAW (pre-proxify) URL of the image an entry's BODY renders first:
|
|
69
|
+
* json_metadata.image, then the first body image. Deliberately NOT the same
|
|
70
|
+
* order as catchPostImage, which reads json_metadata.thumbnails ahead of
|
|
71
|
+
* image: a thumbnail is a card concern and the post body never renders it, so
|
|
72
|
+
* a preload built from it would be wasted. Unlike catchPostImage it does NOT
|
|
73
|
+
* proxify — callers need the original URL (e.g. to test picture-eligibility
|
|
74
|
+
* for an LCP preload, since catchPostImage returns an already-proxified /p/
|
|
75
|
+
* URL). Returns null when the fast path finds no unambiguous image (the caller
|
|
76
|
+
* can fall back to catchPostImage).
|
|
74
77
|
*/
|
|
75
78
|
declare function getEntryImageRawUrl(obj: Entry | string): string | null;
|
|
76
|
-
|
|
79
|
+
interface CatchPostImageOptions {
|
|
80
|
+
/**
|
|
81
|
+
* Stop after the metadata and regex tiers. The last tier is a full
|
|
82
|
+
* markdown2Html + DOM parse, which on a long body with no image at all costs
|
|
83
|
+
* hundreds of milliseconds of synchronous CPU; a feed of such rows can hold a
|
|
84
|
+
* server's event loop for seconds. Callers that can live without the rare
|
|
85
|
+
* markdown-only finds (video embed posters, for instance) set this and get
|
|
86
|
+
* null back instead. Default false keeps every existing caller byte-identical.
|
|
87
|
+
*/
|
|
88
|
+
fast?: boolean;
|
|
89
|
+
}
|
|
90
|
+
declare function catchPostImage(obj: Entry | string, width?: number, height?: number, format?: string, options?: CatchPostImageOptions): string | null;
|
|
77
91
|
|
|
78
92
|
/**
|
|
79
93
|
* Generate a text summary from an Entry object or raw string
|
package/dist/browser/index.js
CHANGED
|
@@ -835,15 +835,31 @@ function img(el, state, forApp = true) {
|
|
|
835
835
|
return;
|
|
836
836
|
}
|
|
837
837
|
el.setAttribute("itemprop", "image");
|
|
838
|
-
const
|
|
839
|
-
|
|
838
|
+
const avatarRoute = new RegExp(`^${trimTrailingSlash(getProxyBase()).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/u/[^/]+/avatar(?:/[a-z]+)?$`);
|
|
839
|
+
const classTokens = (el.getAttribute("class") || "").split(/\s+/);
|
|
840
|
+
const isAvatar = avatarRoute.test(decodedSrc) || classTokens.includes("er-author-link-image");
|
|
841
|
+
if (isAvatar) {
|
|
842
|
+
el.setAttribute("loading", "lazy");
|
|
843
|
+
el.setAttribute("decoding", "async");
|
|
844
|
+
}
|
|
845
|
+
const imageIndex = !state || isAvatar ? 3 : state.imageCount ?? (state.firstImageFound ? 1 : 0);
|
|
846
|
+
if (state && !isAvatar) {
|
|
847
|
+
state.imageCount = imageIndex + 1;
|
|
848
|
+
state.firstImageFound = true;
|
|
849
|
+
}
|
|
850
|
+
if (isAvatar) ; else if (imageIndex === 0) {
|
|
840
851
|
el.setAttribute("loading", "eager");
|
|
841
852
|
el.setAttribute("fetchpriority", "high");
|
|
842
|
-
|
|
853
|
+
} else if (imageIndex === 1) {
|
|
854
|
+
el.setAttribute("loading", "eager");
|
|
855
|
+
el.setAttribute("decoding", "async");
|
|
843
856
|
} else {
|
|
844
857
|
el.setAttribute("loading", "lazy");
|
|
845
858
|
el.setAttribute("decoding", "async");
|
|
846
859
|
}
|
|
860
|
+
if (isAvatar || imageIndex !== 0) {
|
|
861
|
+
el.removeAttribute("fetchpriority");
|
|
862
|
+
}
|
|
847
863
|
const cls = el.getAttribute("class") || "";
|
|
848
864
|
const shouldReplace = !cls.includes("no-replace");
|
|
849
865
|
const base = trimTrailingSlash(getProxyBase());
|
|
@@ -2198,28 +2214,455 @@ function isGifLink(link) {
|
|
|
2198
2214
|
var BACKTICK_FENCE_RE = /```[\s\S]*?```/g;
|
|
2199
2215
|
var TILDE_FENCE_RE = /~~~[\s\S]*?~~~/g;
|
|
2200
2216
|
var INLINE_CODE_RE = /`[^`\n]*`/g;
|
|
2201
|
-
var
|
|
2217
|
+
var OPEN_TAG_NAME_END = /[\t\f\r />]/;
|
|
2218
|
+
var CLOSE_TAG_NAME_END = /[\s>]/;
|
|
2219
|
+
function isWholeTagName(lower, idx, end) {
|
|
2220
|
+
const next = lower[idx];
|
|
2221
|
+
return next === void 0 || end.test(next);
|
|
2222
|
+
}
|
|
2223
|
+
function findTag(lower, tag, from, end) {
|
|
2224
|
+
let at = lower.indexOf(tag, from);
|
|
2225
|
+
while (at !== -1 && !isWholeTagName(lower, at + tag.length, end)) {
|
|
2226
|
+
at = lower.indexOf(tag, at + tag.length);
|
|
2227
|
+
}
|
|
2228
|
+
return at;
|
|
2229
|
+
}
|
|
2230
|
+
function findOpenTagEnd(lower, openAt) {
|
|
2231
|
+
let quote = "";
|
|
2232
|
+
for (let i = openAt + 1; i < lower.length; i++) {
|
|
2233
|
+
const c = lower[i];
|
|
2234
|
+
if (c === "\n") return NaN;
|
|
2235
|
+
if (quote) {
|
|
2236
|
+
if (c === quote) quote = "";
|
|
2237
|
+
} else if (c === '"' || c === "'") {
|
|
2238
|
+
quote = c;
|
|
2239
|
+
} else if (c === ">") {
|
|
2240
|
+
return lower[i - 1] === "/" ? NaN : i;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
return -1;
|
|
2244
|
+
}
|
|
2245
|
+
var HTML_BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
2246
|
+
"article",
|
|
2247
|
+
"aside",
|
|
2248
|
+
"button",
|
|
2249
|
+
"blockquote",
|
|
2250
|
+
"body",
|
|
2251
|
+
"canvas",
|
|
2252
|
+
"caption",
|
|
2253
|
+
"col",
|
|
2254
|
+
"colgroup",
|
|
2255
|
+
"dd",
|
|
2256
|
+
"div",
|
|
2257
|
+
"dl",
|
|
2258
|
+
"dt",
|
|
2259
|
+
"embed",
|
|
2260
|
+
"fieldset",
|
|
2261
|
+
"figcaption",
|
|
2262
|
+
"figure",
|
|
2263
|
+
"footer",
|
|
2264
|
+
"form",
|
|
2265
|
+
"h1",
|
|
2266
|
+
"h2",
|
|
2267
|
+
"h3",
|
|
2268
|
+
"h4",
|
|
2269
|
+
"h5",
|
|
2270
|
+
"h6",
|
|
2271
|
+
"header",
|
|
2272
|
+
"hgroup",
|
|
2273
|
+
"hr",
|
|
2274
|
+
"iframe",
|
|
2275
|
+
"li",
|
|
2276
|
+
"map",
|
|
2277
|
+
"object",
|
|
2278
|
+
"ol",
|
|
2279
|
+
"output",
|
|
2280
|
+
"p",
|
|
2281
|
+
"pre",
|
|
2282
|
+
"progress",
|
|
2283
|
+
"script",
|
|
2284
|
+
"section",
|
|
2285
|
+
"style",
|
|
2286
|
+
"table",
|
|
2287
|
+
"tbody",
|
|
2288
|
+
"td",
|
|
2289
|
+
"textarea",
|
|
2290
|
+
"tfoot",
|
|
2291
|
+
"th",
|
|
2292
|
+
"tr",
|
|
2293
|
+
"thead",
|
|
2294
|
+
"ul",
|
|
2295
|
+
"video"
|
|
2296
|
+
]);
|
|
2297
|
+
var HTML_BLOCK_LINE_RE = /^ {0,3}<(?:[!?]|([a-z]{1,15})[\s/>]|\/([a-z]{1,15})[\s>])/;
|
|
2298
|
+
var BLOCKQUOTE_PREFIX_RE = /^ {0,3}> ?/;
|
|
2299
|
+
var LIST_PREFIX_RE = /^(?:[-*+]|\d{1,9}[.)]) +/;
|
|
2300
|
+
function markLines(lower) {
|
|
2301
|
+
const block = new Uint8Array(lower.length);
|
|
2302
|
+
const code = new Uint8Array(lower.length);
|
|
2303
|
+
let inBlock = false;
|
|
2304
|
+
let listIndent = 0;
|
|
2305
|
+
let nestedItem = false;
|
|
2306
|
+
let lineStart = 0;
|
|
2307
|
+
while (lineStart <= lower.length) {
|
|
2308
|
+
let lineEnd = lower.indexOf("\n", lineStart);
|
|
2309
|
+
if (lineEnd === -1) lineEnd = lower.length;
|
|
2310
|
+
let line = lower.slice(lineStart, lineEnd);
|
|
2311
|
+
if (inBlock) {
|
|
2312
|
+
if (line.trim() === "") {
|
|
2313
|
+
inBlock = false;
|
|
2314
|
+
listIndent = 0;
|
|
2315
|
+
nestedItem = false;
|
|
2316
|
+
} else {
|
|
2317
|
+
block.fill(1, lineStart, lineEnd);
|
|
2318
|
+
}
|
|
2319
|
+
lineStart = lineEnd + 1;
|
|
2320
|
+
continue;
|
|
2321
|
+
}
|
|
2322
|
+
let stripped = 0;
|
|
2323
|
+
let sawList = false;
|
|
2324
|
+
let lastWasList = false;
|
|
2325
|
+
let inlineRemainder = false;
|
|
2326
|
+
for (; ; ) {
|
|
2327
|
+
const bq = BLOCKQUOTE_PREFIX_RE.exec(line);
|
|
2328
|
+
if (bq) {
|
|
2329
|
+
line = line.slice(bq[0].length);
|
|
2330
|
+
stripped += bq[0].length;
|
|
2331
|
+
lastWasList = false;
|
|
2332
|
+
inlineRemainder = false;
|
|
2333
|
+
continue;
|
|
2334
|
+
}
|
|
2335
|
+
const lm = LIST_PREFIX_RE.exec(line);
|
|
2336
|
+
if (lm) {
|
|
2337
|
+
line = line.slice(lm[0].length);
|
|
2338
|
+
stripped += lm[0].length;
|
|
2339
|
+
if (lastWasList) inlineRemainder = true;
|
|
2340
|
+
sawList = true;
|
|
2341
|
+
lastWasList = true;
|
|
2342
|
+
continue;
|
|
2343
|
+
}
|
|
2344
|
+
break;
|
|
2345
|
+
}
|
|
2346
|
+
if (sawList) {
|
|
2347
|
+
listIndent = stripped;
|
|
2348
|
+
nestedItem = inlineRemainder;
|
|
2349
|
+
} else if (listIndent > 0 && line.trim() !== "") {
|
|
2350
|
+
let indent = 0;
|
|
2351
|
+
while (indent < listIndent && line[indent] === " ") indent++;
|
|
2352
|
+
if (indent >= Math.min(listIndent, 2)) {
|
|
2353
|
+
line = line.slice(indent);
|
|
2354
|
+
inlineRemainder = nestedItem;
|
|
2355
|
+
} else {
|
|
2356
|
+
listIndent = 0;
|
|
2357
|
+
nestedItem = false;
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
const blank = line.trim() === "";
|
|
2361
|
+
if (blank) {
|
|
2362
|
+
inBlock = false;
|
|
2363
|
+
listIndent = 0;
|
|
2364
|
+
nestedItem = false;
|
|
2365
|
+
} else if (inlineRemainder) {
|
|
2366
|
+
inBlock = false;
|
|
2367
|
+
} else if (!inBlock && /^(?: {4}|\t)/.test(line)) {
|
|
2368
|
+
code.fill(1, lineStart, lineEnd);
|
|
2369
|
+
} else if (!inBlock) {
|
|
2370
|
+
const m = HTML_BLOCK_LINE_RE.exec(line);
|
|
2371
|
+
if (m) {
|
|
2372
|
+
const tag = m[1] ?? m[2];
|
|
2373
|
+
inBlock = tag === void 0 || HTML_BLOCK_TAGS.has(tag);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
if (inBlock && !blank) block.fill(1, lineStart, lineEnd);
|
|
2377
|
+
lineStart = lineEnd + 1;
|
|
2378
|
+
}
|
|
2379
|
+
return { block, code };
|
|
2380
|
+
}
|
|
2381
|
+
var blankChars = (s) => s.replace(/[^\n]/g, " ");
|
|
2382
|
+
function blankMatches(text2, re) {
|
|
2383
|
+
return text2.replace(re, blankChars);
|
|
2384
|
+
}
|
|
2385
|
+
function blankSpans(input, open, close, tagNames, blockMask) {
|
|
2386
|
+
const { text: text2, lower } = input;
|
|
2387
|
+
const findOpen = (from2) => {
|
|
2388
|
+
if (!tagNames) return lower.indexOf(open, from2);
|
|
2389
|
+
let at = findTag(lower, open, from2, OPEN_TAG_NAME_END);
|
|
2390
|
+
while (at !== -1 && (Number.isNaN(findOpenTagEnd(lower, at)) || blockMask !== null && !blockMask[at])) {
|
|
2391
|
+
at = findTag(lower, open, at + open.length, OPEN_TAG_NAME_END);
|
|
2392
|
+
}
|
|
2393
|
+
return at;
|
|
2394
|
+
};
|
|
2395
|
+
const findClose = (from2) => tagNames ? findTag(lower, close, from2, CLOSE_TAG_NAME_END) : lower.indexOf(close, from2);
|
|
2396
|
+
let start = findOpen(0);
|
|
2397
|
+
if (start === -1) return input;
|
|
2398
|
+
const textParts = [];
|
|
2399
|
+
const lowerParts = [];
|
|
2400
|
+
let from = 0;
|
|
2401
|
+
while (start !== -1) {
|
|
2402
|
+
const end = findClose(start + open.length);
|
|
2403
|
+
let to;
|
|
2404
|
+
if (end === -1) {
|
|
2405
|
+
to = text2.length;
|
|
2406
|
+
} else if (tagNames) {
|
|
2407
|
+
const gt = lower.indexOf(">", end + close.length);
|
|
2408
|
+
to = gt === -1 ? text2.length : gt + 1;
|
|
2409
|
+
} else {
|
|
2410
|
+
to = end + close.length;
|
|
2411
|
+
}
|
|
2412
|
+
const blanked = blankChars(lower.slice(start, to));
|
|
2413
|
+
textParts.push(text2.slice(from, start), blanked);
|
|
2414
|
+
lowerParts.push(lower.slice(from, start), blanked);
|
|
2415
|
+
from = to;
|
|
2416
|
+
start = to >= text2.length ? -1 : findOpen(to);
|
|
2417
|
+
}
|
|
2418
|
+
textParts.push(text2.slice(from));
|
|
2419
|
+
lowerParts.push(lower.slice(from));
|
|
2420
|
+
return { text: textParts.join(""), lower: lowerParts.join("") };
|
|
2421
|
+
}
|
|
2422
|
+
function blankMasked(input, mask) {
|
|
2423
|
+
let text2 = "";
|
|
2424
|
+
let lower = "";
|
|
2425
|
+
let from = 0;
|
|
2426
|
+
for (let i = 0; i < mask.length; i++) {
|
|
2427
|
+
if (!mask[i]) continue;
|
|
2428
|
+
let j = i;
|
|
2429
|
+
while (j < mask.length && mask[j]) j++;
|
|
2430
|
+
text2 += input.text.slice(from, i) + blankChars(input.text.slice(i, j));
|
|
2431
|
+
lower += input.lower.slice(from, i) + blankChars(input.lower.slice(i, j));
|
|
2432
|
+
from = j;
|
|
2433
|
+
i = j;
|
|
2434
|
+
}
|
|
2435
|
+
if (from === 0) return input;
|
|
2436
|
+
return { text: text2 + input.text.slice(from), lower: lower + input.lower.slice(from) };
|
|
2437
|
+
}
|
|
2438
|
+
function stripHiddenRegions(text2) {
|
|
2439
|
+
let spellings = { text: text2, lower: text2.toLowerCase() };
|
|
2440
|
+
const { block: blockMask, code: codeMask } = markLines(spellings.lower);
|
|
2441
|
+
spellings = blankMasked(spellings, codeMask);
|
|
2442
|
+
spellings = blankSpans(spellings, "<!--", "-->", false, null);
|
|
2443
|
+
spellings = blankSpans(spellings, "<style", "</style", true, null);
|
|
2444
|
+
spellings = blankSpans(spellings, "<pre", "</pre", true, blockMask);
|
|
2445
|
+
spellings = blankSpans(spellings, "<code", "</code", true, blockMask);
|
|
2446
|
+
return spellings.text;
|
|
2447
|
+
}
|
|
2202
2448
|
var MD_IMAGE_RE = /!\[[^[\]]*\]\(\s*([^)\s]{1,2048})(?:\s+["'][^"']*["'])?\s*\)/;
|
|
2203
2449
|
var MD_IMAGE_PRESENT_RE = /!\[[^[\]]*\]\(\s*[^\s)]/;
|
|
2204
2450
|
var HTML_IMAGE_RE = /<img\b[^>]*?\bsrc\s*=\s*["']([^"']+)["']/i;
|
|
2205
|
-
var
|
|
2451
|
+
var URL_TOKEN_RE = /https?:\/\/[^\s<>"'()[\]]+/gi;
|
|
2452
|
+
var IMAGE_EXT_G = /\.(?:tiff?|jpe?g|gif|png|svg|ico|heic|webp|arw)/gi;
|
|
2453
|
+
var YOUTUBE_ID_RE = /^[^"&?/\s]{11}$/;
|
|
2454
|
+
function imageToken(token) {
|
|
2455
|
+
let end = -1;
|
|
2456
|
+
for (const m of token.matchAll(IMAGE_EXT_G)) {
|
|
2457
|
+
end = (m.index ?? 0) + m[0].length;
|
|
2458
|
+
}
|
|
2459
|
+
if (end === -1) return null;
|
|
2460
|
+
if (end < token.length && (token[end] === "?" || token[end] === "#")) end = token.length;
|
|
2461
|
+
const url = token.slice(0, end);
|
|
2462
|
+
return SAFE_URL_RE.test(url) ? url : null;
|
|
2463
|
+
}
|
|
2464
|
+
function youtubeIdOf(url) {
|
|
2465
|
+
const m = /^https?:\/\/([^/?#]+)/i.exec(url);
|
|
2466
|
+
if (!m) return null;
|
|
2467
|
+
const host = m[1].toLowerCase();
|
|
2468
|
+
const isShort = host === "youtu.be";
|
|
2469
|
+
const isFull = host === "youtube.com" || host.endsWith(".youtube.com");
|
|
2470
|
+
if (!isShort && !isFull) return null;
|
|
2471
|
+
const rest = url.slice(m[0].length);
|
|
2472
|
+
const hashAt = rest.indexOf("#");
|
|
2473
|
+
const beforeHash = hashAt === -1 ? rest : rest.slice(0, hashAt);
|
|
2474
|
+
const qAt = beforeHash.indexOf("?");
|
|
2475
|
+
const path = qAt === -1 ? beforeHash : beforeHash.slice(0, qAt);
|
|
2476
|
+
const query = qAt === -1 ? "" : beforeHash.slice(qAt + 1);
|
|
2477
|
+
const candidate = (value) => {
|
|
2478
|
+
const id = value === void 0 ? "" : value.slice(0, 11);
|
|
2479
|
+
return YOUTUBE_ID_RE.test(id) ? id : null;
|
|
2480
|
+
};
|
|
2481
|
+
if (isFull && query) {
|
|
2482
|
+
for (const part of query.split("&")) {
|
|
2483
|
+
if (part.startsWith("v=")) {
|
|
2484
|
+
const id = candidate(part.slice(2));
|
|
2485
|
+
if (id) return id;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
const segments = path.split("/").filter((seg) => seg.length > 0);
|
|
2490
|
+
if (isShort) return candidate(segments[0]);
|
|
2491
|
+
if (segments.length >= 2 && ["v", "e", "embed", "shorts"].includes(segments[0].toLowerCase())) {
|
|
2492
|
+
return candidate(segments[1]);
|
|
2493
|
+
}
|
|
2494
|
+
if (segments.length >= 3) return candidate(segments[segments.length - 1]);
|
|
2495
|
+
return null;
|
|
2496
|
+
}
|
|
2497
|
+
function isAutolinkAt(text2, idx) {
|
|
2498
|
+
return /^https?:\/\//i.test(text2.slice(idx, idx + 8));
|
|
2499
|
+
}
|
|
2500
|
+
function markInsideTags(text2) {
|
|
2501
|
+
const marks = new Uint8Array(text2.length);
|
|
2502
|
+
let inTag = false;
|
|
2503
|
+
let quote = "";
|
|
2504
|
+
for (let i = 0; i < text2.length; i++) {
|
|
2505
|
+
const c = text2[i];
|
|
2506
|
+
if (!inTag) {
|
|
2507
|
+
if (c === "<" && i + 1 < text2.length && /[A-Za-z/!?]/.test(text2[i + 1]) && !isAutolinkAt(text2, i + 1)) {
|
|
2508
|
+
inTag = true;
|
|
2509
|
+
marks[i] = 1;
|
|
2510
|
+
}
|
|
2511
|
+
continue;
|
|
2512
|
+
}
|
|
2513
|
+
marks[i] = 1;
|
|
2514
|
+
if (quote) {
|
|
2515
|
+
if (c === quote) quote = "";
|
|
2516
|
+
} else if (c === '"' || c === "'") {
|
|
2517
|
+
quote = c;
|
|
2518
|
+
} else if (c === ">") {
|
|
2519
|
+
inTag = false;
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
return marks;
|
|
2523
|
+
}
|
|
2524
|
+
function isStandalone(scan, idx) {
|
|
2525
|
+
if (scan.inTag[idx]) return false;
|
|
2526
|
+
if (idx === 0) return true;
|
|
2527
|
+
const text2 = scan.text;
|
|
2528
|
+
const prev = text2[idx - 1];
|
|
2529
|
+
if (/[\w/.:%?&=#[-]/.test(prev)) return false;
|
|
2530
|
+
const prev2 = idx > 1 ? text2[idx - 2] : "";
|
|
2531
|
+
if (prev === "(" && prev2 === "]") return false;
|
|
2532
|
+
return true;
|
|
2533
|
+
}
|
|
2534
|
+
function* standaloneMatches(scan, classify) {
|
|
2535
|
+
for (const m of scan.text.matchAll(URL_TOKEN_RE)) {
|
|
2536
|
+
const idx = m.index ?? 0;
|
|
2537
|
+
if (!isStandalone(scan, idx)) continue;
|
|
2538
|
+
const value = classify(m[0]);
|
|
2539
|
+
if (value !== null) yield { url: value, pos: idx };
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
function firstStandalone(scan, classify) {
|
|
2543
|
+
for (const hit of standaloneMatches(scan, classify)) return hit;
|
|
2544
|
+
return null;
|
|
2545
|
+
}
|
|
2546
|
+
var HREF_ATTR_RE = /\shref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/i;
|
|
2547
|
+
function hasGluedAttribute(tag) {
|
|
2548
|
+
let quote = "";
|
|
2549
|
+
for (let i = 0; i < tag.length; i++) {
|
|
2550
|
+
const c = tag[i];
|
|
2551
|
+
if (quote) {
|
|
2552
|
+
if (c === quote) {
|
|
2553
|
+
quote = "";
|
|
2554
|
+
const next = tag[i + 1];
|
|
2555
|
+
if (next !== void 0 && /[A-Za-z]/.test(next)) return true;
|
|
2556
|
+
}
|
|
2557
|
+
} else if (c === '"' || c === "'") {
|
|
2558
|
+
quote = c;
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
return false;
|
|
2562
|
+
}
|
|
2206
2563
|
var MD_LINK_RE = /\[([^[\]]*)\]\(\s*([^)\s[]+)(?:\s+["'][^"']*["'])?\s*\)/g;
|
|
2207
|
-
var IMG_HREF_RE = /https?:\/\/.*\.(?:tiff?|jpe?g|gif|png|svg|ico|heic|webp|arw)/i;
|
|
2208
2564
|
var SAFE_URL_RE = /^https?:\/\//i;
|
|
2565
|
+
var IMG_EXT_RE = /\.(?:tiff?|jpe?g|gif|png|svg|ico|heic|webp|arw)/i;
|
|
2566
|
+
var isImageHref = (href) => SAFE_URL_RE.test(href) && IMG_EXT_RE.test(href);
|
|
2209
2567
|
function findFirstImageUrl(body, includeBareUrls = false) {
|
|
2210
|
-
|
|
2211
|
-
|
|
2568
|
+
return findFirstImageCandidate(prepareBody(body), includeBareUrls).candidate?.url ?? null;
|
|
2569
|
+
}
|
|
2570
|
+
function stripCodeRegions(body) {
|
|
2571
|
+
let text2 = blankMatches(body, BACKTICK_FENCE_RE);
|
|
2572
|
+
text2 = blankMatches(text2, TILDE_FENCE_RE);
|
|
2573
|
+
text2 = blankMatches(text2, INLINE_CODE_RE);
|
|
2574
|
+
return stripHiddenRegions(text2);
|
|
2575
|
+
}
|
|
2576
|
+
function blankUnequalAnchors(cleaned, textContent) {
|
|
2577
|
+
const lower = cleaned.toLowerCase();
|
|
2578
|
+
const parts = [];
|
|
2579
|
+
let from = 0;
|
|
2580
|
+
let at = findTag(lower, "<a", 0, OPEN_TAG_NAME_END);
|
|
2581
|
+
while (at !== -1) {
|
|
2582
|
+
const gt = findOpenTagEnd(lower, at);
|
|
2583
|
+
if (Number.isNaN(gt) || gt === -1 || hasGluedAttribute(cleaned.slice(at, gt))) {
|
|
2584
|
+
at = findTag(lower, "<a", at + 2, OPEN_TAG_NAME_END);
|
|
2585
|
+
continue;
|
|
2586
|
+
}
|
|
2587
|
+
const closeAt = findTag(lower, "</a", gt + 1, CLOSE_TAG_NAME_END);
|
|
2588
|
+
const innerEnd = closeAt === -1 ? cleaned.length : closeAt;
|
|
2589
|
+
let spanEnd = cleaned.length;
|
|
2590
|
+
if (closeAt !== -1) {
|
|
2591
|
+
const closeGt = lower.indexOf(">", closeAt + 3);
|
|
2592
|
+
spanEnd = closeGt === -1 ? cleaned.length : closeGt + 1;
|
|
2593
|
+
}
|
|
2594
|
+
const hrefMatch = HREF_ATTR_RE.exec(cleaned.slice(at, gt));
|
|
2595
|
+
const href = hrefMatch ? hrefMatch[1] ?? hrefMatch[2] ?? hrefMatch[3] ?? "" : "";
|
|
2596
|
+
const inner = cleaned.slice(gt + 1, innerEnd);
|
|
2597
|
+
let text2;
|
|
2598
|
+
if (textContent) {
|
|
2599
|
+
text2 = stripHtmlTags(inner);
|
|
2600
|
+
} else {
|
|
2601
|
+
const firstTag = inner.search(/<[A-Za-z/!]/);
|
|
2602
|
+
text2 = firstTag === -1 ? inner : inner.slice(0, firstTag);
|
|
2603
|
+
}
|
|
2604
|
+
if (!href || decodeEntities(text2.trim()) !== decodeEntities(href.trim())) {
|
|
2605
|
+
parts.push(cleaned.slice(from, at), blankChars(cleaned.slice(at, spanEnd)));
|
|
2606
|
+
from = spanEnd;
|
|
2607
|
+
}
|
|
2608
|
+
at = spanEnd >= cleaned.length ? -1 : findTag(lower, "<a", spanEnd, OPEN_TAG_NAME_END);
|
|
2609
|
+
}
|
|
2610
|
+
if (parts.length === 0) return cleaned;
|
|
2611
|
+
parts.push(cleaned.slice(from));
|
|
2612
|
+
return parts.join("");
|
|
2613
|
+
}
|
|
2614
|
+
var EMPTY_SCAN = { text: "", inTag: new Uint8Array(0) };
|
|
2615
|
+
function prepareBody(body) {
|
|
2616
|
+
const cleaned = body ? stripCodeRegions(body) : "";
|
|
2617
|
+
if (!cleaned) return { cleaned, image: EMPTY_SCAN, video: EMPTY_SCAN };
|
|
2618
|
+
const imageText = blankUnequalAnchors(cleaned, false);
|
|
2619
|
+
const videoText = blankUnequalAnchors(cleaned, true);
|
|
2620
|
+
return {
|
|
2621
|
+
cleaned,
|
|
2622
|
+
image: { text: imageText, inTag: markInsideTags(imageText) },
|
|
2623
|
+
video: { text: videoText, inTag: markInsideTags(videoText) }
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
2626
|
+
function findFirstVideoPoster(prepared) {
|
|
2627
|
+
const { cleaned } = prepared;
|
|
2628
|
+
if (!cleaned) return null;
|
|
2629
|
+
let best = null;
|
|
2630
|
+
for (const hit of standaloneMatches(prepared.video, youtubeIdOf)) {
|
|
2631
|
+
best = { url: hit.url, pos: hit.pos };
|
|
2632
|
+
break;
|
|
2633
|
+
}
|
|
2634
|
+
for (const m of cleaned.matchAll(MD_LINK_RE)) {
|
|
2635
|
+
const idx = m.index ?? 0;
|
|
2636
|
+
if (idx > 0 && cleaned[idx - 1] === "!") continue;
|
|
2637
|
+
if (best && idx >= best.pos) break;
|
|
2638
|
+
const href = m[2];
|
|
2639
|
+
if (href && m[1].trim() === href) {
|
|
2640
|
+
const id = youtubeIdOf(href);
|
|
2641
|
+
if (id) {
|
|
2642
|
+
best = { url: id, pos: idx };
|
|
2643
|
+
break;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
if (!best) return null;
|
|
2648
|
+
return { url: `https://img.youtube.com/vi/${best.url.split("?")[0]}/hqdefault.jpg`, pos: best.pos };
|
|
2649
|
+
}
|
|
2650
|
+
var NONE = { candidate: null, ambiguous: false };
|
|
2651
|
+
var AMBIGUOUS = { candidate: null, ambiguous: true };
|
|
2652
|
+
function findFirstImageCandidate(prepared, includeBareUrls = false) {
|
|
2653
|
+
const { cleaned } = prepared;
|
|
2654
|
+
if (!cleaned) return NONE;
|
|
2212
2655
|
const mdMatch = cleaned.match(MD_IMAGE_RE);
|
|
2213
2656
|
const htmlMatch = cleaned.match(HTML_IMAGE_RE);
|
|
2214
2657
|
if (mdMatch) {
|
|
2215
2658
|
const url = mdMatch[1];
|
|
2216
2659
|
if (!url || !SAFE_URL_RE.test(url) || url.includes("(")) {
|
|
2217
|
-
return
|
|
2660
|
+
return AMBIGUOUS;
|
|
2218
2661
|
}
|
|
2219
2662
|
}
|
|
2220
2663
|
const priorRegion = mdMatch ? cleaned.slice(0, mdMatch.index ?? 0) : cleaned;
|
|
2221
2664
|
if (MD_IMAGE_PRESENT_RE.test(priorRegion)) {
|
|
2222
|
-
return
|
|
2665
|
+
return AMBIGUOUS;
|
|
2223
2666
|
}
|
|
2224
2667
|
const candidates = [];
|
|
2225
2668
|
if (mdMatch) candidates.push({ url: mdMatch[1], pos: mdMatch.index ?? 0 });
|
|
@@ -2227,24 +2670,49 @@ function findFirstImageUrl(body, includeBareUrls = false) {
|
|
|
2227
2670
|
candidates.push({ url: htmlMatch[1], pos: htmlMatch.index ?? 0 });
|
|
2228
2671
|
}
|
|
2229
2672
|
if (includeBareUrls) {
|
|
2230
|
-
const bareMatch =
|
|
2231
|
-
if (bareMatch &&
|
|
2232
|
-
candidates.push(
|
|
2673
|
+
const bareMatch = firstStandalone(prepared.image, imageToken);
|
|
2674
|
+
if (bareMatch && SAFE_URL_RE.test(bareMatch.url)) {
|
|
2675
|
+
candidates.push(bareMatch);
|
|
2233
2676
|
}
|
|
2234
2677
|
const deAmp = (s) => s.trim().replace(/&/g, "&");
|
|
2235
2678
|
for (const m of cleaned.matchAll(MD_LINK_RE)) {
|
|
2236
2679
|
const idx = m.index ?? 0;
|
|
2237
2680
|
if (idx > 0 && cleaned[idx - 1] === "!") continue;
|
|
2238
2681
|
const href = m[2];
|
|
2239
|
-
if (href &&
|
|
2682
|
+
if (href && isImageHref(href) && deAmp(m[1]) === deAmp(href)) {
|
|
2240
2683
|
candidates.push({ url: href, pos: idx });
|
|
2241
2684
|
break;
|
|
2242
2685
|
}
|
|
2243
2686
|
}
|
|
2244
2687
|
}
|
|
2245
|
-
if (candidates.length === 0) return
|
|
2688
|
+
if (candidates.length === 0) return NONE;
|
|
2246
2689
|
candidates.sort((a2, b) => a2.pos - b.pos);
|
|
2247
|
-
return candidates[0]
|
|
2690
|
+
return { candidate: candidates[0], ambiguous: false };
|
|
2691
|
+
}
|
|
2692
|
+
function fastBodyImage(body, width, height, format) {
|
|
2693
|
+
const prepared = prepareBody(body);
|
|
2694
|
+
const strict = findFirstImageCandidate(prepared, false);
|
|
2695
|
+
if (strict.candidate) {
|
|
2696
|
+
return proxifyFound(strict.candidate.url, width, height, format);
|
|
2697
|
+
}
|
|
2698
|
+
if (strict.ambiguous) {
|
|
2699
|
+
return null;
|
|
2700
|
+
}
|
|
2701
|
+
const bare = findFirstImageCandidate(prepared, true).candidate;
|
|
2702
|
+
const poster = findFirstVideoPoster(prepared);
|
|
2703
|
+
if (poster && (!bare || poster.pos < bare.pos)) {
|
|
2704
|
+
return proxifyFound(proxifyImageSrc(poster.url, 0, 0, "match"), width, height, format);
|
|
2705
|
+
}
|
|
2706
|
+
return bare ? proxifyFound(bare.url, width, height, format) : null;
|
|
2707
|
+
}
|
|
2708
|
+
function firstMetaUrl(value) {
|
|
2709
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
2710
|
+
return value;
|
|
2711
|
+
}
|
|
2712
|
+
if (Array.isArray(value)) {
|
|
2713
|
+
return value.find((url) => typeof url === "string" && url.trim().length > 0);
|
|
2714
|
+
}
|
|
2715
|
+
return void 0;
|
|
2248
2716
|
}
|
|
2249
2717
|
function proxifyFound(src, width, height, format) {
|
|
2250
2718
|
const decoded = decodeEntities(src);
|
|
@@ -2253,7 +2721,7 @@ function proxifyFound(src, width, height, format) {
|
|
|
2253
2721
|
}
|
|
2254
2722
|
return proxifyImageSrc(decoded, width, height, format);
|
|
2255
2723
|
}
|
|
2256
|
-
function getImage(entry, width = 0, height = 0, format = "match") {
|
|
2724
|
+
function getImage(entry, width = 0, height = 0, format = "match", fastMode = false) {
|
|
2257
2725
|
let meta;
|
|
2258
2726
|
if (typeof entry.json_metadata === "object") {
|
|
2259
2727
|
meta = entry.json_metadata;
|
|
@@ -2264,6 +2732,14 @@ function getImage(entry, width = 0, height = 0, format = "match") {
|
|
|
2264
2732
|
meta = null;
|
|
2265
2733
|
}
|
|
2266
2734
|
}
|
|
2735
|
+
const thumbnail = firstMetaUrl(meta?.thumbnails);
|
|
2736
|
+
if (thumbnail) {
|
|
2737
|
+
const decodedThumbnail = decodeEntities(thumbnail);
|
|
2738
|
+
const proxied = isGifLink(decodedThumbnail) ? proxifyImageSrc(decodedThumbnail, 0, 0, format) : proxifyImageSrc(decodedThumbnail, width, height, format);
|
|
2739
|
+
if (proxied) {
|
|
2740
|
+
return proxied;
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2267
2743
|
if (meta && typeof meta.image === "string" && meta.image.length > 0) {
|
|
2268
2744
|
const decodedImage = decodeEntities(meta.image);
|
|
2269
2745
|
if (isGifLink(decodedImage)) {
|
|
@@ -2284,6 +2760,9 @@ function getImage(entry, width = 0, height = 0, format = "match") {
|
|
|
2284
2760
|
}
|
|
2285
2761
|
return proxifyImageSrc(meta.image[0], width, height, format);
|
|
2286
2762
|
}
|
|
2763
|
+
if (fastMode) {
|
|
2764
|
+
return fastBodyImage(entry.body, width, height, format);
|
|
2765
|
+
}
|
|
2287
2766
|
const fast = findFirstImageUrl(entry.body);
|
|
2288
2767
|
if (fast) {
|
|
2289
2768
|
return proxifyFound(fast, width, height, format);
|
|
@@ -2327,8 +2806,12 @@ function getEntryImageRawUrl(obj) {
|
|
|
2327
2806
|
const bodySrc = findFirstImageUrl(obj.body, true);
|
|
2328
2807
|
return bodySrc ? decodeImageSrc(bodySrc) : null;
|
|
2329
2808
|
}
|
|
2330
|
-
function catchPostImage(obj, width = 0, height = 0, format = "match") {
|
|
2809
|
+
function catchPostImage(obj, width = 0, height = 0, format = "match", options = {}) {
|
|
2810
|
+
const fastMode = options.fast === true;
|
|
2331
2811
|
if (typeof obj === "string") {
|
|
2812
|
+
if (fastMode) {
|
|
2813
|
+
return fastBodyImage(obj, width, height, format);
|
|
2814
|
+
}
|
|
2332
2815
|
const fast = findFirstImageUrl(obj);
|
|
2333
2816
|
if (fast) {
|
|
2334
2817
|
return proxifyFound(fast, width, height, format);
|
|
@@ -2348,12 +2831,12 @@ function catchPostImage(obj, width = 0, height = 0, format = "match") {
|
|
|
2348
2831
|
}
|
|
2349
2832
|
return null;
|
|
2350
2833
|
}
|
|
2351
|
-
const key = `${makeEntryCacheKey(obj)}-${width}x${height}-${format}`;
|
|
2834
|
+
const key = `${makeEntryCacheKey(obj)}-${width}x${height}-${format}${fastMode ? "-fast" : ""}`;
|
|
2352
2835
|
const item = cacheGet(key);
|
|
2353
|
-
if (item) {
|
|
2836
|
+
if (item !== void 0) {
|
|
2354
2837
|
return item;
|
|
2355
2838
|
}
|
|
2356
|
-
const res = getImage(obj, width, height, format);
|
|
2839
|
+
const res = getImage(obj, width, height, format, fastMode);
|
|
2357
2840
|
cacheSet(key, res);
|
|
2358
2841
|
return res;
|
|
2359
2842
|
}
|