@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.
- package/dist/browser/index.d.ts +21 -7
- package/dist/browser/index.js +506 -26
- package/dist/browser/index.js.map +1 -1
- package/dist/node/index.cjs +508 -27
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.mjs +508 -26
- package/dist/node/index.mjs.map +1 -1
- package/package.json +2 -3
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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DOMParser as DOMParser$1, XMLSerializer } from '@xmldom/xmldom';
|
|
2
|
-
import
|
|
2
|
+
import { decodeHTML } from 'entities';
|
|
3
3
|
import xss from 'xss';
|
|
4
4
|
import querystring from 'querystring';
|
|
5
5
|
import { LRUCache } from 'lru-cache';
|
|
@@ -282,8 +282,19 @@ function createParser() {
|
|
|
282
282
|
});
|
|
283
283
|
}
|
|
284
284
|
var DOMParser = createParser();
|
|
285
|
+
var LEADING_ZEROS_DEC = /�+(?=[0-9])/g;
|
|
286
|
+
var LEADING_ZEROS_HEX = /�+(?=[0-9a-f])/gi;
|
|
287
|
+
var OVERLONG_NUMERIC_REF = /&#(?:x[0-9a-f]{256,}|[0-9]{309,});?/gi;
|
|
288
|
+
function decodeEntities(value) {
|
|
289
|
+
const safe = value.replace(LEADING_ZEROS_DEC, "&#").replace(LEADING_ZEROS_HEX, (m) => m.slice(0, 3)).replace(OVERLONG_NUMERIC_REF, "\uFFFD");
|
|
290
|
+
try {
|
|
291
|
+
return decodeHTML(safe);
|
|
292
|
+
} catch {
|
|
293
|
+
return safe;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
285
296
|
function decodeImageSrc(src) {
|
|
286
|
-
const entityDecoded =
|
|
297
|
+
const entityDecoded = decodeEntities(src);
|
|
287
298
|
try {
|
|
288
299
|
return decodeURIComponent(entityDecoded).trim();
|
|
289
300
|
} catch {
|
|
@@ -738,7 +749,7 @@ var isSafeNavValue = (value) => {
|
|
|
738
749
|
const isRelative = /^(\/\/|\/[^/]?|#|\?|[a-z0-9._\-]+(\/|$))/i.test(trimmed);
|
|
739
750
|
return isSafeScheme || isRelative;
|
|
740
751
|
};
|
|
741
|
-
var
|
|
752
|
+
var decodeEntities2 = (input) => input.replace(/&#(\d+);?/g, (_, dec) => String.fromCodePoint(Number(dec))).replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)));
|
|
742
753
|
var isProxyPSrcset = (srcset) => {
|
|
743
754
|
const base = trimTrailingSlash(getProxyBase());
|
|
744
755
|
const candidates = srcset.split(",").map((c) => c.trim().split(/\s+/)[0]).filter(Boolean);
|
|
@@ -752,7 +763,7 @@ function sanitizeHtml(html) {
|
|
|
752
763
|
css: false,
|
|
753
764
|
// block style attrs entirely for safety
|
|
754
765
|
onTagAttr: (tag, name, value) => {
|
|
755
|
-
const decoded =
|
|
766
|
+
const decoded = decodeEntities2(value.trim());
|
|
756
767
|
const decodedLower = decoded.toLowerCase();
|
|
757
768
|
if (name.startsWith("on")) return "";
|
|
758
769
|
if (tag === "img" && name === "src" && !/^https?:\/\//.test(decodedLower)) return "";
|
|
@@ -2178,6 +2189,8 @@ function markdown2Html(obj, forApp = true, _webp = false, parentDomain = "ecency
|
|
|
2178
2189
|
cacheSet(key, res);
|
|
2179
2190
|
return res;
|
|
2180
2191
|
}
|
|
2192
|
+
|
|
2193
|
+
// src/catch-post-image.ts
|
|
2181
2194
|
var gifLinkRegex = /\.(gif)$/i;
|
|
2182
2195
|
function isGifLink(link) {
|
|
2183
2196
|
return gifLinkRegex.test(link);
|
|
@@ -2185,28 +2198,455 @@ function isGifLink(link) {
|
|
|
2185
2198
|
var BACKTICK_FENCE_RE = /```[\s\S]*?```/g;
|
|
2186
2199
|
var TILDE_FENCE_RE = /~~~[\s\S]*?~~~/g;
|
|
2187
2200
|
var INLINE_CODE_RE = /`[^`\n]*`/g;
|
|
2188
|
-
var
|
|
2201
|
+
var OPEN_TAG_NAME_END = /[\t\f\r />]/;
|
|
2202
|
+
var CLOSE_TAG_NAME_END = /[\s>]/;
|
|
2203
|
+
function isWholeTagName(lower, idx, end) {
|
|
2204
|
+
const next = lower[idx];
|
|
2205
|
+
return next === void 0 || end.test(next);
|
|
2206
|
+
}
|
|
2207
|
+
function findTag(lower, tag, from, end) {
|
|
2208
|
+
let at = lower.indexOf(tag, from);
|
|
2209
|
+
while (at !== -1 && !isWholeTagName(lower, at + tag.length, end)) {
|
|
2210
|
+
at = lower.indexOf(tag, at + tag.length);
|
|
2211
|
+
}
|
|
2212
|
+
return at;
|
|
2213
|
+
}
|
|
2214
|
+
function findOpenTagEnd(lower, openAt) {
|
|
2215
|
+
let quote = "";
|
|
2216
|
+
for (let i = openAt + 1; i < lower.length; i++) {
|
|
2217
|
+
const c = lower[i];
|
|
2218
|
+
if (c === "\n") return NaN;
|
|
2219
|
+
if (quote) {
|
|
2220
|
+
if (c === quote) quote = "";
|
|
2221
|
+
} else if (c === '"' || c === "'") {
|
|
2222
|
+
quote = c;
|
|
2223
|
+
} else if (c === ">") {
|
|
2224
|
+
return lower[i - 1] === "/" ? NaN : i;
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
return -1;
|
|
2228
|
+
}
|
|
2229
|
+
var HTML_BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
2230
|
+
"article",
|
|
2231
|
+
"aside",
|
|
2232
|
+
"button",
|
|
2233
|
+
"blockquote",
|
|
2234
|
+
"body",
|
|
2235
|
+
"canvas",
|
|
2236
|
+
"caption",
|
|
2237
|
+
"col",
|
|
2238
|
+
"colgroup",
|
|
2239
|
+
"dd",
|
|
2240
|
+
"div",
|
|
2241
|
+
"dl",
|
|
2242
|
+
"dt",
|
|
2243
|
+
"embed",
|
|
2244
|
+
"fieldset",
|
|
2245
|
+
"figcaption",
|
|
2246
|
+
"figure",
|
|
2247
|
+
"footer",
|
|
2248
|
+
"form",
|
|
2249
|
+
"h1",
|
|
2250
|
+
"h2",
|
|
2251
|
+
"h3",
|
|
2252
|
+
"h4",
|
|
2253
|
+
"h5",
|
|
2254
|
+
"h6",
|
|
2255
|
+
"header",
|
|
2256
|
+
"hgroup",
|
|
2257
|
+
"hr",
|
|
2258
|
+
"iframe",
|
|
2259
|
+
"li",
|
|
2260
|
+
"map",
|
|
2261
|
+
"object",
|
|
2262
|
+
"ol",
|
|
2263
|
+
"output",
|
|
2264
|
+
"p",
|
|
2265
|
+
"pre",
|
|
2266
|
+
"progress",
|
|
2267
|
+
"script",
|
|
2268
|
+
"section",
|
|
2269
|
+
"style",
|
|
2270
|
+
"table",
|
|
2271
|
+
"tbody",
|
|
2272
|
+
"td",
|
|
2273
|
+
"textarea",
|
|
2274
|
+
"tfoot",
|
|
2275
|
+
"th",
|
|
2276
|
+
"tr",
|
|
2277
|
+
"thead",
|
|
2278
|
+
"ul",
|
|
2279
|
+
"video"
|
|
2280
|
+
]);
|
|
2281
|
+
var HTML_BLOCK_LINE_RE = /^ {0,3}<(?:[!?]|([a-z]{1,15})[\s/>]|\/([a-z]{1,15})[\s>])/;
|
|
2282
|
+
var BLOCKQUOTE_PREFIX_RE = /^ {0,3}> ?/;
|
|
2283
|
+
var LIST_PREFIX_RE = /^(?:[-*+]|\d{1,9}[.)]) +/;
|
|
2284
|
+
function markLines(lower) {
|
|
2285
|
+
const block = new Uint8Array(lower.length);
|
|
2286
|
+
const code = new Uint8Array(lower.length);
|
|
2287
|
+
let inBlock = false;
|
|
2288
|
+
let listIndent = 0;
|
|
2289
|
+
let nestedItem = false;
|
|
2290
|
+
let lineStart = 0;
|
|
2291
|
+
while (lineStart <= lower.length) {
|
|
2292
|
+
let lineEnd = lower.indexOf("\n", lineStart);
|
|
2293
|
+
if (lineEnd === -1) lineEnd = lower.length;
|
|
2294
|
+
let line = lower.slice(lineStart, lineEnd);
|
|
2295
|
+
if (inBlock) {
|
|
2296
|
+
if (line.trim() === "") {
|
|
2297
|
+
inBlock = false;
|
|
2298
|
+
listIndent = 0;
|
|
2299
|
+
nestedItem = false;
|
|
2300
|
+
} else {
|
|
2301
|
+
block.fill(1, lineStart, lineEnd);
|
|
2302
|
+
}
|
|
2303
|
+
lineStart = lineEnd + 1;
|
|
2304
|
+
continue;
|
|
2305
|
+
}
|
|
2306
|
+
let stripped = 0;
|
|
2307
|
+
let sawList = false;
|
|
2308
|
+
let lastWasList = false;
|
|
2309
|
+
let inlineRemainder = false;
|
|
2310
|
+
for (; ; ) {
|
|
2311
|
+
const bq = BLOCKQUOTE_PREFIX_RE.exec(line);
|
|
2312
|
+
if (bq) {
|
|
2313
|
+
line = line.slice(bq[0].length);
|
|
2314
|
+
stripped += bq[0].length;
|
|
2315
|
+
lastWasList = false;
|
|
2316
|
+
inlineRemainder = false;
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
const lm = LIST_PREFIX_RE.exec(line);
|
|
2320
|
+
if (lm) {
|
|
2321
|
+
line = line.slice(lm[0].length);
|
|
2322
|
+
stripped += lm[0].length;
|
|
2323
|
+
if (lastWasList) inlineRemainder = true;
|
|
2324
|
+
sawList = true;
|
|
2325
|
+
lastWasList = true;
|
|
2326
|
+
continue;
|
|
2327
|
+
}
|
|
2328
|
+
break;
|
|
2329
|
+
}
|
|
2330
|
+
if (sawList) {
|
|
2331
|
+
listIndent = stripped;
|
|
2332
|
+
nestedItem = inlineRemainder;
|
|
2333
|
+
} else if (listIndent > 0 && line.trim() !== "") {
|
|
2334
|
+
let indent = 0;
|
|
2335
|
+
while (indent < listIndent && line[indent] === " ") indent++;
|
|
2336
|
+
if (indent >= Math.min(listIndent, 2)) {
|
|
2337
|
+
line = line.slice(indent);
|
|
2338
|
+
inlineRemainder = nestedItem;
|
|
2339
|
+
} else {
|
|
2340
|
+
listIndent = 0;
|
|
2341
|
+
nestedItem = false;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
const blank = line.trim() === "";
|
|
2345
|
+
if (blank) {
|
|
2346
|
+
inBlock = false;
|
|
2347
|
+
listIndent = 0;
|
|
2348
|
+
nestedItem = false;
|
|
2349
|
+
} else if (inlineRemainder) {
|
|
2350
|
+
inBlock = false;
|
|
2351
|
+
} else if (!inBlock && /^(?: {4}|\t)/.test(line)) {
|
|
2352
|
+
code.fill(1, lineStart, lineEnd);
|
|
2353
|
+
} else if (!inBlock) {
|
|
2354
|
+
const m = HTML_BLOCK_LINE_RE.exec(line);
|
|
2355
|
+
if (m) {
|
|
2356
|
+
const tag = m[1] ?? m[2];
|
|
2357
|
+
inBlock = tag === void 0 || HTML_BLOCK_TAGS.has(tag);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
if (inBlock && !blank) block.fill(1, lineStart, lineEnd);
|
|
2361
|
+
lineStart = lineEnd + 1;
|
|
2362
|
+
}
|
|
2363
|
+
return { block, code };
|
|
2364
|
+
}
|
|
2365
|
+
var blankChars = (s) => s.replace(/[^\n]/g, " ");
|
|
2366
|
+
function blankMatches(text2, re) {
|
|
2367
|
+
return text2.replace(re, blankChars);
|
|
2368
|
+
}
|
|
2369
|
+
function blankSpans(input, open, close, tagNames, blockMask) {
|
|
2370
|
+
const { text: text2, lower } = input;
|
|
2371
|
+
const findOpen = (from2) => {
|
|
2372
|
+
if (!tagNames) return lower.indexOf(open, from2);
|
|
2373
|
+
let at = findTag(lower, open, from2, OPEN_TAG_NAME_END);
|
|
2374
|
+
while (at !== -1 && (Number.isNaN(findOpenTagEnd(lower, at)) || blockMask !== null && !blockMask[at])) {
|
|
2375
|
+
at = findTag(lower, open, at + open.length, OPEN_TAG_NAME_END);
|
|
2376
|
+
}
|
|
2377
|
+
return at;
|
|
2378
|
+
};
|
|
2379
|
+
const findClose = (from2) => tagNames ? findTag(lower, close, from2, CLOSE_TAG_NAME_END) : lower.indexOf(close, from2);
|
|
2380
|
+
let start = findOpen(0);
|
|
2381
|
+
if (start === -1) return input;
|
|
2382
|
+
const textParts = [];
|
|
2383
|
+
const lowerParts = [];
|
|
2384
|
+
let from = 0;
|
|
2385
|
+
while (start !== -1) {
|
|
2386
|
+
const end = findClose(start + open.length);
|
|
2387
|
+
let to;
|
|
2388
|
+
if (end === -1) {
|
|
2389
|
+
to = text2.length;
|
|
2390
|
+
} else if (tagNames) {
|
|
2391
|
+
const gt = lower.indexOf(">", end + close.length);
|
|
2392
|
+
to = gt === -1 ? text2.length : gt + 1;
|
|
2393
|
+
} else {
|
|
2394
|
+
to = end + close.length;
|
|
2395
|
+
}
|
|
2396
|
+
const blanked = blankChars(lower.slice(start, to));
|
|
2397
|
+
textParts.push(text2.slice(from, start), blanked);
|
|
2398
|
+
lowerParts.push(lower.slice(from, start), blanked);
|
|
2399
|
+
from = to;
|
|
2400
|
+
start = to >= text2.length ? -1 : findOpen(to);
|
|
2401
|
+
}
|
|
2402
|
+
textParts.push(text2.slice(from));
|
|
2403
|
+
lowerParts.push(lower.slice(from));
|
|
2404
|
+
return { text: textParts.join(""), lower: lowerParts.join("") };
|
|
2405
|
+
}
|
|
2406
|
+
function blankMasked(input, mask) {
|
|
2407
|
+
let text2 = "";
|
|
2408
|
+
let lower = "";
|
|
2409
|
+
let from = 0;
|
|
2410
|
+
for (let i = 0; i < mask.length; i++) {
|
|
2411
|
+
if (!mask[i]) continue;
|
|
2412
|
+
let j = i;
|
|
2413
|
+
while (j < mask.length && mask[j]) j++;
|
|
2414
|
+
text2 += input.text.slice(from, i) + blankChars(input.text.slice(i, j));
|
|
2415
|
+
lower += input.lower.slice(from, i) + blankChars(input.lower.slice(i, j));
|
|
2416
|
+
from = j;
|
|
2417
|
+
i = j;
|
|
2418
|
+
}
|
|
2419
|
+
if (from === 0) return input;
|
|
2420
|
+
return { text: text2 + input.text.slice(from), lower: lower + input.lower.slice(from) };
|
|
2421
|
+
}
|
|
2422
|
+
function stripHiddenRegions(text2) {
|
|
2423
|
+
let spellings = { text: text2, lower: text2.toLowerCase() };
|
|
2424
|
+
const { block: blockMask, code: codeMask } = markLines(spellings.lower);
|
|
2425
|
+
spellings = blankMasked(spellings, codeMask);
|
|
2426
|
+
spellings = blankSpans(spellings, "<!--", "-->", false, null);
|
|
2427
|
+
spellings = blankSpans(spellings, "<style", "</style", true, null);
|
|
2428
|
+
spellings = blankSpans(spellings, "<pre", "</pre", true, blockMask);
|
|
2429
|
+
spellings = blankSpans(spellings, "<code", "</code", true, blockMask);
|
|
2430
|
+
return spellings.text;
|
|
2431
|
+
}
|
|
2189
2432
|
var MD_IMAGE_RE = /!\[[^[\]]*\]\(\s*([^)\s]{1,2048})(?:\s+["'][^"']*["'])?\s*\)/;
|
|
2190
2433
|
var MD_IMAGE_PRESENT_RE = /!\[[^[\]]*\]\(\s*[^\s)]/;
|
|
2191
2434
|
var HTML_IMAGE_RE = /<img\b[^>]*?\bsrc\s*=\s*["']([^"']+)["']/i;
|
|
2192
|
-
var
|
|
2435
|
+
var URL_TOKEN_RE = /https?:\/\/[^\s<>"'()[\]]+/gi;
|
|
2436
|
+
var IMAGE_EXT_G = /\.(?:tiff?|jpe?g|gif|png|svg|ico|heic|webp|arw)/gi;
|
|
2437
|
+
var YOUTUBE_ID_RE = /^[^"&?/\s]{11}$/;
|
|
2438
|
+
function imageToken(token) {
|
|
2439
|
+
let end = -1;
|
|
2440
|
+
for (const m of token.matchAll(IMAGE_EXT_G)) {
|
|
2441
|
+
end = (m.index ?? 0) + m[0].length;
|
|
2442
|
+
}
|
|
2443
|
+
if (end === -1) return null;
|
|
2444
|
+
if (end < token.length && (token[end] === "?" || token[end] === "#")) end = token.length;
|
|
2445
|
+
const url = token.slice(0, end);
|
|
2446
|
+
return SAFE_URL_RE.test(url) ? url : null;
|
|
2447
|
+
}
|
|
2448
|
+
function youtubeIdOf(url) {
|
|
2449
|
+
const m = /^https?:\/\/([^/?#]+)/i.exec(url);
|
|
2450
|
+
if (!m) return null;
|
|
2451
|
+
const host = m[1].toLowerCase();
|
|
2452
|
+
const isShort = host === "youtu.be";
|
|
2453
|
+
const isFull = host === "youtube.com" || host.endsWith(".youtube.com");
|
|
2454
|
+
if (!isShort && !isFull) return null;
|
|
2455
|
+
const rest = url.slice(m[0].length);
|
|
2456
|
+
const hashAt = rest.indexOf("#");
|
|
2457
|
+
const beforeHash = hashAt === -1 ? rest : rest.slice(0, hashAt);
|
|
2458
|
+
const qAt = beforeHash.indexOf("?");
|
|
2459
|
+
const path = qAt === -1 ? beforeHash : beforeHash.slice(0, qAt);
|
|
2460
|
+
const query = qAt === -1 ? "" : beforeHash.slice(qAt + 1);
|
|
2461
|
+
const candidate = (value) => {
|
|
2462
|
+
const id = value === void 0 ? "" : value.slice(0, 11);
|
|
2463
|
+
return YOUTUBE_ID_RE.test(id) ? id : null;
|
|
2464
|
+
};
|
|
2465
|
+
if (isFull && query) {
|
|
2466
|
+
for (const part of query.split("&")) {
|
|
2467
|
+
if (part.startsWith("v=")) {
|
|
2468
|
+
const id = candidate(part.slice(2));
|
|
2469
|
+
if (id) return id;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
const segments = path.split("/").filter((seg) => seg.length > 0);
|
|
2474
|
+
if (isShort) return candidate(segments[0]);
|
|
2475
|
+
if (segments.length >= 2 && ["v", "e", "embed", "shorts"].includes(segments[0].toLowerCase())) {
|
|
2476
|
+
return candidate(segments[1]);
|
|
2477
|
+
}
|
|
2478
|
+
if (segments.length >= 3) return candidate(segments[segments.length - 1]);
|
|
2479
|
+
return null;
|
|
2480
|
+
}
|
|
2481
|
+
function isAutolinkAt(text2, idx) {
|
|
2482
|
+
return /^https?:\/\//i.test(text2.slice(idx, idx + 8));
|
|
2483
|
+
}
|
|
2484
|
+
function markInsideTags(text2) {
|
|
2485
|
+
const marks = new Uint8Array(text2.length);
|
|
2486
|
+
let inTag = false;
|
|
2487
|
+
let quote = "";
|
|
2488
|
+
for (let i = 0; i < text2.length; i++) {
|
|
2489
|
+
const c = text2[i];
|
|
2490
|
+
if (!inTag) {
|
|
2491
|
+
if (c === "<" && i + 1 < text2.length && /[A-Za-z/!?]/.test(text2[i + 1]) && !isAutolinkAt(text2, i + 1)) {
|
|
2492
|
+
inTag = true;
|
|
2493
|
+
marks[i] = 1;
|
|
2494
|
+
}
|
|
2495
|
+
continue;
|
|
2496
|
+
}
|
|
2497
|
+
marks[i] = 1;
|
|
2498
|
+
if (quote) {
|
|
2499
|
+
if (c === quote) quote = "";
|
|
2500
|
+
} else if (c === '"' || c === "'") {
|
|
2501
|
+
quote = c;
|
|
2502
|
+
} else if (c === ">") {
|
|
2503
|
+
inTag = false;
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
return marks;
|
|
2507
|
+
}
|
|
2508
|
+
function isStandalone(scan, idx) {
|
|
2509
|
+
if (scan.inTag[idx]) return false;
|
|
2510
|
+
if (idx === 0) return true;
|
|
2511
|
+
const text2 = scan.text;
|
|
2512
|
+
const prev = text2[idx - 1];
|
|
2513
|
+
if (/[\w/.:%?&=#[-]/.test(prev)) return false;
|
|
2514
|
+
const prev2 = idx > 1 ? text2[idx - 2] : "";
|
|
2515
|
+
if (prev === "(" && prev2 === "]") return false;
|
|
2516
|
+
return true;
|
|
2517
|
+
}
|
|
2518
|
+
function* standaloneMatches(scan, classify) {
|
|
2519
|
+
for (const m of scan.text.matchAll(URL_TOKEN_RE)) {
|
|
2520
|
+
const idx = m.index ?? 0;
|
|
2521
|
+
if (!isStandalone(scan, idx)) continue;
|
|
2522
|
+
const value = classify(m[0]);
|
|
2523
|
+
if (value !== null) yield { url: value, pos: idx };
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
function firstStandalone(scan, classify) {
|
|
2527
|
+
for (const hit of standaloneMatches(scan, classify)) return hit;
|
|
2528
|
+
return null;
|
|
2529
|
+
}
|
|
2530
|
+
var HREF_ATTR_RE = /\shref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/i;
|
|
2531
|
+
function hasGluedAttribute(tag) {
|
|
2532
|
+
let quote = "";
|
|
2533
|
+
for (let i = 0; i < tag.length; i++) {
|
|
2534
|
+
const c = tag[i];
|
|
2535
|
+
if (quote) {
|
|
2536
|
+
if (c === quote) {
|
|
2537
|
+
quote = "";
|
|
2538
|
+
const next = tag[i + 1];
|
|
2539
|
+
if (next !== void 0 && /[A-Za-z]/.test(next)) return true;
|
|
2540
|
+
}
|
|
2541
|
+
} else if (c === '"' || c === "'") {
|
|
2542
|
+
quote = c;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
return false;
|
|
2546
|
+
}
|
|
2193
2547
|
var MD_LINK_RE = /\[([^[\]]*)\]\(\s*([^)\s[]+)(?:\s+["'][^"']*["'])?\s*\)/g;
|
|
2194
|
-
var IMG_HREF_RE = /https?:\/\/.*\.(?:tiff?|jpe?g|gif|png|svg|ico|heic|webp|arw)/i;
|
|
2195
2548
|
var SAFE_URL_RE = /^https?:\/\//i;
|
|
2549
|
+
var IMG_EXT_RE = /\.(?:tiff?|jpe?g|gif|png|svg|ico|heic|webp|arw)/i;
|
|
2550
|
+
var isImageHref = (href) => SAFE_URL_RE.test(href) && IMG_EXT_RE.test(href);
|
|
2196
2551
|
function findFirstImageUrl(body, includeBareUrls = false) {
|
|
2197
|
-
|
|
2198
|
-
|
|
2552
|
+
return findFirstImageCandidate(prepareBody(body), includeBareUrls).candidate?.url ?? null;
|
|
2553
|
+
}
|
|
2554
|
+
function stripCodeRegions(body) {
|
|
2555
|
+
let text2 = blankMatches(body, BACKTICK_FENCE_RE);
|
|
2556
|
+
text2 = blankMatches(text2, TILDE_FENCE_RE);
|
|
2557
|
+
text2 = blankMatches(text2, INLINE_CODE_RE);
|
|
2558
|
+
return stripHiddenRegions(text2);
|
|
2559
|
+
}
|
|
2560
|
+
function blankUnequalAnchors(cleaned, textContent) {
|
|
2561
|
+
const lower = cleaned.toLowerCase();
|
|
2562
|
+
const parts = [];
|
|
2563
|
+
let from = 0;
|
|
2564
|
+
let at = findTag(lower, "<a", 0, OPEN_TAG_NAME_END);
|
|
2565
|
+
while (at !== -1) {
|
|
2566
|
+
const gt = findOpenTagEnd(lower, at);
|
|
2567
|
+
if (Number.isNaN(gt) || gt === -1 || hasGluedAttribute(cleaned.slice(at, gt))) {
|
|
2568
|
+
at = findTag(lower, "<a", at + 2, OPEN_TAG_NAME_END);
|
|
2569
|
+
continue;
|
|
2570
|
+
}
|
|
2571
|
+
const closeAt = findTag(lower, "</a", gt + 1, CLOSE_TAG_NAME_END);
|
|
2572
|
+
const innerEnd = closeAt === -1 ? cleaned.length : closeAt;
|
|
2573
|
+
let spanEnd = cleaned.length;
|
|
2574
|
+
if (closeAt !== -1) {
|
|
2575
|
+
const closeGt = lower.indexOf(">", closeAt + 3);
|
|
2576
|
+
spanEnd = closeGt === -1 ? cleaned.length : closeGt + 1;
|
|
2577
|
+
}
|
|
2578
|
+
const hrefMatch = HREF_ATTR_RE.exec(cleaned.slice(at, gt));
|
|
2579
|
+
const href = hrefMatch ? hrefMatch[1] ?? hrefMatch[2] ?? hrefMatch[3] ?? "" : "";
|
|
2580
|
+
const inner = cleaned.slice(gt + 1, innerEnd);
|
|
2581
|
+
let text2;
|
|
2582
|
+
if (textContent) {
|
|
2583
|
+
text2 = stripHtmlTags(inner);
|
|
2584
|
+
} else {
|
|
2585
|
+
const firstTag = inner.search(/<[A-Za-z/!]/);
|
|
2586
|
+
text2 = firstTag === -1 ? inner : inner.slice(0, firstTag);
|
|
2587
|
+
}
|
|
2588
|
+
if (!href || decodeEntities(text2.trim()) !== decodeEntities(href.trim())) {
|
|
2589
|
+
parts.push(cleaned.slice(from, at), blankChars(cleaned.slice(at, spanEnd)));
|
|
2590
|
+
from = spanEnd;
|
|
2591
|
+
}
|
|
2592
|
+
at = spanEnd >= cleaned.length ? -1 : findTag(lower, "<a", spanEnd, OPEN_TAG_NAME_END);
|
|
2593
|
+
}
|
|
2594
|
+
if (parts.length === 0) return cleaned;
|
|
2595
|
+
parts.push(cleaned.slice(from));
|
|
2596
|
+
return parts.join("");
|
|
2597
|
+
}
|
|
2598
|
+
var EMPTY_SCAN = { text: "", inTag: new Uint8Array(0) };
|
|
2599
|
+
function prepareBody(body) {
|
|
2600
|
+
const cleaned = body ? stripCodeRegions(body) : "";
|
|
2601
|
+
if (!cleaned) return { cleaned, image: EMPTY_SCAN, video: EMPTY_SCAN };
|
|
2602
|
+
const imageText = blankUnequalAnchors(cleaned, false);
|
|
2603
|
+
const videoText = blankUnequalAnchors(cleaned, true);
|
|
2604
|
+
return {
|
|
2605
|
+
cleaned,
|
|
2606
|
+
image: { text: imageText, inTag: markInsideTags(imageText) },
|
|
2607
|
+
video: { text: videoText, inTag: markInsideTags(videoText) }
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2610
|
+
function findFirstVideoPoster(prepared) {
|
|
2611
|
+
const { cleaned } = prepared;
|
|
2612
|
+
if (!cleaned) return null;
|
|
2613
|
+
let best = null;
|
|
2614
|
+
for (const hit of standaloneMatches(prepared.video, youtubeIdOf)) {
|
|
2615
|
+
best = { url: hit.url, pos: hit.pos };
|
|
2616
|
+
break;
|
|
2617
|
+
}
|
|
2618
|
+
for (const m of cleaned.matchAll(MD_LINK_RE)) {
|
|
2619
|
+
const idx = m.index ?? 0;
|
|
2620
|
+
if (idx > 0 && cleaned[idx - 1] === "!") continue;
|
|
2621
|
+
if (best && idx >= best.pos) break;
|
|
2622
|
+
const href = m[2];
|
|
2623
|
+
if (href && m[1].trim() === href) {
|
|
2624
|
+
const id = youtubeIdOf(href);
|
|
2625
|
+
if (id) {
|
|
2626
|
+
best = { url: id, pos: idx };
|
|
2627
|
+
break;
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
if (!best) return null;
|
|
2632
|
+
return { url: `https://img.youtube.com/vi/${best.url.split("?")[0]}/hqdefault.jpg`, pos: best.pos };
|
|
2633
|
+
}
|
|
2634
|
+
var NONE = { candidate: null, ambiguous: false };
|
|
2635
|
+
var AMBIGUOUS = { candidate: null, ambiguous: true };
|
|
2636
|
+
function findFirstImageCandidate(prepared, includeBareUrls = false) {
|
|
2637
|
+
const { cleaned } = prepared;
|
|
2638
|
+
if (!cleaned) return NONE;
|
|
2199
2639
|
const mdMatch = cleaned.match(MD_IMAGE_RE);
|
|
2200
2640
|
const htmlMatch = cleaned.match(HTML_IMAGE_RE);
|
|
2201
2641
|
if (mdMatch) {
|
|
2202
2642
|
const url = mdMatch[1];
|
|
2203
2643
|
if (!url || !SAFE_URL_RE.test(url) || url.includes("(")) {
|
|
2204
|
-
return
|
|
2644
|
+
return AMBIGUOUS;
|
|
2205
2645
|
}
|
|
2206
2646
|
}
|
|
2207
2647
|
const priorRegion = mdMatch ? cleaned.slice(0, mdMatch.index ?? 0) : cleaned;
|
|
2208
2648
|
if (MD_IMAGE_PRESENT_RE.test(priorRegion)) {
|
|
2209
|
-
return
|
|
2649
|
+
return AMBIGUOUS;
|
|
2210
2650
|
}
|
|
2211
2651
|
const candidates = [];
|
|
2212
2652
|
if (mdMatch) candidates.push({ url: mdMatch[1], pos: mdMatch.index ?? 0 });
|
|
@@ -2214,33 +2654,58 @@ function findFirstImageUrl(body, includeBareUrls = false) {
|
|
|
2214
2654
|
candidates.push({ url: htmlMatch[1], pos: htmlMatch.index ?? 0 });
|
|
2215
2655
|
}
|
|
2216
2656
|
if (includeBareUrls) {
|
|
2217
|
-
const bareMatch =
|
|
2218
|
-
if (bareMatch &&
|
|
2219
|
-
candidates.push(
|
|
2657
|
+
const bareMatch = firstStandalone(prepared.image, imageToken);
|
|
2658
|
+
if (bareMatch && SAFE_URL_RE.test(bareMatch.url)) {
|
|
2659
|
+
candidates.push(bareMatch);
|
|
2220
2660
|
}
|
|
2221
2661
|
const deAmp = (s) => s.trim().replace(/&/g, "&");
|
|
2222
2662
|
for (const m of cleaned.matchAll(MD_LINK_RE)) {
|
|
2223
2663
|
const idx = m.index ?? 0;
|
|
2224
2664
|
if (idx > 0 && cleaned[idx - 1] === "!") continue;
|
|
2225
2665
|
const href = m[2];
|
|
2226
|
-
if (href &&
|
|
2666
|
+
if (href && isImageHref(href) && deAmp(m[1]) === deAmp(href)) {
|
|
2227
2667
|
candidates.push({ url: href, pos: idx });
|
|
2228
2668
|
break;
|
|
2229
2669
|
}
|
|
2230
2670
|
}
|
|
2231
2671
|
}
|
|
2232
|
-
if (candidates.length === 0) return
|
|
2672
|
+
if (candidates.length === 0) return NONE;
|
|
2233
2673
|
candidates.sort((a2, b) => a2.pos - b.pos);
|
|
2234
|
-
return candidates[0]
|
|
2674
|
+
return { candidate: candidates[0], ambiguous: false };
|
|
2675
|
+
}
|
|
2676
|
+
function fastBodyImage(body, width, height, format) {
|
|
2677
|
+
const prepared = prepareBody(body);
|
|
2678
|
+
const strict = findFirstImageCandidate(prepared, false);
|
|
2679
|
+
if (strict.candidate) {
|
|
2680
|
+
return proxifyFound(strict.candidate.url, width, height, format);
|
|
2681
|
+
}
|
|
2682
|
+
if (strict.ambiguous) {
|
|
2683
|
+
return null;
|
|
2684
|
+
}
|
|
2685
|
+
const bare = findFirstImageCandidate(prepared, true).candidate;
|
|
2686
|
+
const poster = findFirstVideoPoster(prepared);
|
|
2687
|
+
if (poster && (!bare || poster.pos < bare.pos)) {
|
|
2688
|
+
return proxifyFound(proxifyImageSrc(poster.url, 0, 0, "match"), width, height, format);
|
|
2689
|
+
}
|
|
2690
|
+
return bare ? proxifyFound(bare.url, width, height, format) : null;
|
|
2691
|
+
}
|
|
2692
|
+
function firstMetaUrl(value) {
|
|
2693
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
2694
|
+
return value;
|
|
2695
|
+
}
|
|
2696
|
+
if (Array.isArray(value)) {
|
|
2697
|
+
return value.find((url) => typeof url === "string" && url.trim().length > 0);
|
|
2698
|
+
}
|
|
2699
|
+
return void 0;
|
|
2235
2700
|
}
|
|
2236
2701
|
function proxifyFound(src, width, height, format) {
|
|
2237
|
-
const decoded =
|
|
2702
|
+
const decoded = decodeEntities(src);
|
|
2238
2703
|
if (isGifLink(decoded)) {
|
|
2239
2704
|
return proxifyImageSrc(decoded, 0, 0, format);
|
|
2240
2705
|
}
|
|
2241
2706
|
return proxifyImageSrc(decoded, width, height, format);
|
|
2242
2707
|
}
|
|
2243
|
-
function getImage(entry, width = 0, height = 0, format = "match") {
|
|
2708
|
+
function getImage(entry, width = 0, height = 0, format = "match", fastMode = false) {
|
|
2244
2709
|
let meta;
|
|
2245
2710
|
if (typeof entry.json_metadata === "object") {
|
|
2246
2711
|
meta = entry.json_metadata;
|
|
@@ -2251,8 +2716,16 @@ function getImage(entry, width = 0, height = 0, format = "match") {
|
|
|
2251
2716
|
meta = null;
|
|
2252
2717
|
}
|
|
2253
2718
|
}
|
|
2719
|
+
const thumbnail = firstMetaUrl(meta?.thumbnails);
|
|
2720
|
+
if (thumbnail) {
|
|
2721
|
+
const decodedThumbnail = decodeEntities(thumbnail);
|
|
2722
|
+
const proxied = isGifLink(decodedThumbnail) ? proxifyImageSrc(decodedThumbnail, 0, 0, format) : proxifyImageSrc(decodedThumbnail, width, height, format);
|
|
2723
|
+
if (proxied) {
|
|
2724
|
+
return proxied;
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2254
2727
|
if (meta && typeof meta.image === "string" && meta.image.length > 0) {
|
|
2255
|
-
const decodedImage =
|
|
2728
|
+
const decodedImage = decodeEntities(meta.image);
|
|
2256
2729
|
if (isGifLink(decodedImage)) {
|
|
2257
2730
|
return proxifyImageSrc(decodedImage, 0, 0, format);
|
|
2258
2731
|
}
|
|
@@ -2260,7 +2733,7 @@ function getImage(entry, width = 0, height = 0, format = "match") {
|
|
|
2260
2733
|
}
|
|
2261
2734
|
if (meta && meta.image && !!meta.image.length && meta.image[0]) {
|
|
2262
2735
|
if (typeof meta.image[0] === "string") {
|
|
2263
|
-
const decodedImage =
|
|
2736
|
+
const decodedImage = decodeEntities(meta.image[0]);
|
|
2264
2737
|
if (isGifLink(decodedImage)) {
|
|
2265
2738
|
return proxifyImageSrc(decodedImage, 0, 0, format);
|
|
2266
2739
|
}
|
|
@@ -2271,6 +2744,9 @@ function getImage(entry, width = 0, height = 0, format = "match") {
|
|
|
2271
2744
|
}
|
|
2272
2745
|
return proxifyImageSrc(meta.image[0], width, height, format);
|
|
2273
2746
|
}
|
|
2747
|
+
if (fastMode) {
|
|
2748
|
+
return fastBodyImage(entry.body, width, height, format);
|
|
2749
|
+
}
|
|
2274
2750
|
const fast = findFirstImageUrl(entry.body);
|
|
2275
2751
|
if (fast) {
|
|
2276
2752
|
return proxifyFound(fast, width, height, format);
|
|
@@ -2314,8 +2790,12 @@ function getEntryImageRawUrl(obj) {
|
|
|
2314
2790
|
const bodySrc = findFirstImageUrl(obj.body, true);
|
|
2315
2791
|
return bodySrc ? decodeImageSrc(bodySrc) : null;
|
|
2316
2792
|
}
|
|
2317
|
-
function catchPostImage(obj, width = 0, height = 0, format = "match") {
|
|
2793
|
+
function catchPostImage(obj, width = 0, height = 0, format = "match", options = {}) {
|
|
2794
|
+
const fastMode = options.fast === true;
|
|
2318
2795
|
if (typeof obj === "string") {
|
|
2796
|
+
if (fastMode) {
|
|
2797
|
+
return fastBodyImage(obj, width, height, format);
|
|
2798
|
+
}
|
|
2319
2799
|
const fast = findFirstImageUrl(obj);
|
|
2320
2800
|
if (fast) {
|
|
2321
2801
|
return proxifyFound(fast, width, height, format);
|
|
@@ -2335,12 +2815,12 @@ function catchPostImage(obj, width = 0, height = 0, format = "match") {
|
|
|
2335
2815
|
}
|
|
2336
2816
|
return null;
|
|
2337
2817
|
}
|
|
2338
|
-
const key = `${makeEntryCacheKey(obj)}-${width}x${height}-${format}`;
|
|
2818
|
+
const key = `${makeEntryCacheKey(obj)}-${width}x${height}-${format}${fastMode ? "-fast" : ""}`;
|
|
2339
2819
|
const item = cacheGet(key);
|
|
2340
|
-
if (item) {
|
|
2820
|
+
if (item !== void 0) {
|
|
2341
2821
|
return item;
|
|
2342
2822
|
}
|
|
2343
|
-
const res = getImage(obj, width, height, format);
|
|
2823
|
+
const res = getImage(obj, width, height, format, fastMode);
|
|
2344
2824
|
cacheSet(key, res);
|
|
2345
2825
|
return res;
|
|
2346
2826
|
}
|
|
@@ -2415,7 +2895,7 @@ function postBodySummary(entryBody, length = 200, platform = "web") {
|
|
|
2415
2895
|
text2 = joint(text2.split(" "), length);
|
|
2416
2896
|
}
|
|
2417
2897
|
if (text2) {
|
|
2418
|
-
text2 =
|
|
2898
|
+
text2 = decodeEntities(text2);
|
|
2419
2899
|
}
|
|
2420
2900
|
return text2;
|
|
2421
2901
|
}
|