@ecency/render-helper 2.4.13 → 2.4.15
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 +15 -2
- package/dist/browser/index.js +44 -16
- package/dist/browser/index.js.map +1 -1
- package/dist/node/index.cjs +45 -17
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.mjs +45 -17
- package/dist/node/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/browser/index.d.ts
CHANGED
|
@@ -9,7 +9,20 @@ interface Entry {
|
|
|
9
9
|
json_metadata?: any;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* SEO context for controlling rel attributes on external links in user-generated content.
|
|
14
|
+
*
|
|
15
|
+
* By default, all external links get rel="nofollow ugc noopener" to prevent link spam.
|
|
16
|
+
* High-quality content (high author reputation + meaningful post rewards) earns followed links.
|
|
17
|
+
*/
|
|
18
|
+
interface SeoContext {
|
|
19
|
+
/** Human-readable author reputation score (after accountReputation() conversion) */
|
|
20
|
+
authorReputation?: number;
|
|
21
|
+
/** Total post payout in USD */
|
|
22
|
+
postPayout?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
declare function markdown2Html(obj: Entry | string, forApp?: boolean, webp?: boolean, parentDomain?: string, seoContext?: SeoContext): string;
|
|
13
26
|
|
|
14
27
|
declare function catchPostImage(obj: Entry | string, width?: number, height?: number, format?: string): string | null;
|
|
15
28
|
|
|
@@ -32,4 +45,4 @@ declare const SECTION_LIST: string[];
|
|
|
32
45
|
|
|
33
46
|
declare function isValidPermlink(permlink: string): boolean;
|
|
34
47
|
|
|
35
|
-
export { type Entry, SECTION_LIST, catchPostImage, isValidPermlink, getPostBodySummary as postBodySummary, proxifyImageSrc, markdown2Html as renderPostBody, setCacheSize, setProxyBase };
|
|
48
|
+
export { type Entry, SECTION_LIST, type SeoContext, catchPostImage, isValidPermlink, getPostBodySummary as postBodySummary, proxifyImageSrc, markdown2Html as renderPostBody, setCacheSize, setProxyBase };
|
package/dist/browser/index.js
CHANGED
|
@@ -164,7 +164,8 @@ var ALLOWED_ATTRIBUTES = {
|
|
|
164
164
|
"del": [],
|
|
165
165
|
"ins": []
|
|
166
166
|
};
|
|
167
|
-
var lenientErrorHandler = (level, msg) => {
|
|
167
|
+
var lenientErrorHandler = (level, msg, context) => {
|
|
168
|
+
return void 0;
|
|
168
169
|
};
|
|
169
170
|
var DOMParser = new DOMParser$1({
|
|
170
171
|
// Use onError instead of deprecated errorHandler
|
|
@@ -173,11 +174,30 @@ var DOMParser = new DOMParser$1({
|
|
|
173
174
|
});
|
|
174
175
|
|
|
175
176
|
// src/helper.ts
|
|
177
|
+
function removeDuplicateAttributes(html) {
|
|
178
|
+
const tagRegex = /<([a-zA-Z][a-zA-Z0-9]*)\s+((?:[^>"']+|"[^"]*"|'[^']*')*?)\s*(\/?)>/g;
|
|
179
|
+
return html.replace(tagRegex, (match, tagName, attrsString, selfClose) => {
|
|
180
|
+
const seenAttrs = /* @__PURE__ */ new Set();
|
|
181
|
+
const cleanedAttrs = [];
|
|
182
|
+
const attrRegex = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)\s*(?:=\s*(?:"[^"]*"|'[^']*'|[^\s/>]+))?/g;
|
|
183
|
+
let attrMatch;
|
|
184
|
+
while ((attrMatch = attrRegex.exec(attrsString)) !== null) {
|
|
185
|
+
const attrName = attrMatch[1].toLowerCase();
|
|
186
|
+
if (!seenAttrs.has(attrName)) {
|
|
187
|
+
seenAttrs.add(attrName);
|
|
188
|
+
cleanedAttrs.push(attrMatch[0]);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const attrsJoined = cleanedAttrs.length > 0 ? ` ${cleanedAttrs.join(" ")}` : "";
|
|
192
|
+
return `<${tagName}${attrsJoined}${selfClose ? " /" : ""}>`;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
176
195
|
function createDoc(html) {
|
|
177
196
|
if (html.trim() === "") {
|
|
178
197
|
return null;
|
|
179
198
|
}
|
|
180
|
-
const
|
|
199
|
+
const cleanedHtml = removeDuplicateAttributes(html);
|
|
200
|
+
const doc = DOMParser.parseFromString(`<body>${cleanedHtml}</body>`, "text/html");
|
|
181
201
|
return doc;
|
|
182
202
|
}
|
|
183
203
|
function makeEntryCacheKey(entry) {
|
|
@@ -368,6 +388,14 @@ function createImageHTML(src, isLCP, webp) {
|
|
|
368
388
|
}
|
|
369
389
|
|
|
370
390
|
// src/methods/a.method.ts
|
|
391
|
+
var NOFOLLOW_REPUTATION_THRESHOLD = 40;
|
|
392
|
+
var FOLLOW_PAYOUT_THRESHOLD = 5;
|
|
393
|
+
function getExternalLinkRel(seoContext) {
|
|
394
|
+
if (seoContext?.authorReputation !== void 0 && seoContext?.postPayout !== void 0 && seoContext.authorReputation >= NOFOLLOW_REPUTATION_THRESHOLD && seoContext.postPayout > FOLLOW_PAYOUT_THRESHOLD) {
|
|
395
|
+
return "noopener";
|
|
396
|
+
}
|
|
397
|
+
return "nofollow ugc noopener";
|
|
398
|
+
}
|
|
371
399
|
var normalizeValue = (value) => value ? value.trim() : "";
|
|
372
400
|
var matchesHref = (href, value) => {
|
|
373
401
|
const normalizedHref = normalizeValue(href);
|
|
@@ -401,7 +429,7 @@ var addLineBreakBeforePostLink = (el, forApp, isInline) => {
|
|
|
401
429
|
el.parentNode.insertBefore(br, el);
|
|
402
430
|
}
|
|
403
431
|
};
|
|
404
|
-
function a(el, forApp, webp, parentDomain = "ecency.com") {
|
|
432
|
+
function a(el, forApp, webp, parentDomain = "ecency.com", seoContext) {
|
|
405
433
|
if (!el || !el.parentNode) {
|
|
406
434
|
return;
|
|
407
435
|
}
|
|
@@ -961,7 +989,7 @@ function a(el, forApp, webp, parentDomain = "ecency.com") {
|
|
|
961
989
|
el.setAttribute("class", "markdown-internal-link");
|
|
962
990
|
} else {
|
|
963
991
|
el.setAttribute("target", "_blank");
|
|
964
|
-
el.setAttribute("rel",
|
|
992
|
+
el.setAttribute("rel", getExternalLinkRel(seoContext));
|
|
965
993
|
}
|
|
966
994
|
el.setAttribute("href", href);
|
|
967
995
|
}
|
|
@@ -1250,7 +1278,7 @@ function text(node, forApp, webp) {
|
|
|
1250
1278
|
}
|
|
1251
1279
|
|
|
1252
1280
|
// src/methods/traverse.method.ts
|
|
1253
|
-
function traverse(node, forApp, depth = 0, webp = false, state = { firstImageFound: false }, parentDomain = "ecency.com") {
|
|
1281
|
+
function traverse(node, forApp, depth = 0, webp = false, state = { firstImageFound: false }, parentDomain = "ecency.com", seoContext) {
|
|
1254
1282
|
if (!node || !node.childNodes) {
|
|
1255
1283
|
return;
|
|
1256
1284
|
}
|
|
@@ -1258,7 +1286,7 @@ function traverse(node, forApp, depth = 0, webp = false, state = { firstImageFou
|
|
|
1258
1286
|
const child = node.childNodes[i];
|
|
1259
1287
|
if (!child) return;
|
|
1260
1288
|
if (child.nodeName.toLowerCase() === "a") {
|
|
1261
|
-
a(child, forApp, webp, parentDomain);
|
|
1289
|
+
a(child, forApp, webp, parentDomain, seoContext);
|
|
1262
1290
|
}
|
|
1263
1291
|
if (child.nodeName.toLowerCase() === "iframe") {
|
|
1264
1292
|
iframe(child, parentDomain);
|
|
@@ -1274,7 +1302,7 @@ function traverse(node, forApp, depth = 0, webp = false, state = { firstImageFou
|
|
|
1274
1302
|
}
|
|
1275
1303
|
const currentChild = node.childNodes[i];
|
|
1276
1304
|
if (currentChild) {
|
|
1277
|
-
traverse(currentChild, forApp, depth + 1, webp, state, parentDomain);
|
|
1305
|
+
traverse(currentChild, forApp, depth + 1, webp, state, parentDomain, seoContext);
|
|
1278
1306
|
}
|
|
1279
1307
|
});
|
|
1280
1308
|
}
|
|
@@ -1325,7 +1353,7 @@ function fixBlockLevelTagsInParagraphs(html) {
|
|
|
1325
1353
|
html = html.replace(/<p><br>\s*<\/p>/g, "");
|
|
1326
1354
|
return html;
|
|
1327
1355
|
}
|
|
1328
|
-
function markdownToHTML(input, forApp, webp, parentDomain = "ecency.com") {
|
|
1356
|
+
function markdownToHTML(input, forApp, webp, parentDomain = "ecency.com", seoContext) {
|
|
1329
1357
|
input = input.replace(new RegExp("https://leofinance.io/threads/view/", "g"), "/@");
|
|
1330
1358
|
input = input.replace(new RegExp("https://leofinance.io/posts/", "g"), "/@");
|
|
1331
1359
|
input = input.replace(new RegExp("https://leofinance.io/threads/", "g"), "/@");
|
|
@@ -1384,8 +1412,8 @@ function markdownToHTML(input, forApp, webp, parentDomain = "ecency.com") {
|
|
|
1384
1412
|
try {
|
|
1385
1413
|
output = md.render(input);
|
|
1386
1414
|
output = fixBlockLevelTagsInParagraphs(output);
|
|
1387
|
-
const doc = DOMParser.parseFromString(`<body id="root">${output}</body>`, "text/html");
|
|
1388
|
-
traverse(doc, forApp, 0, webp, { firstImageFound: false }, parentDomain);
|
|
1415
|
+
const doc = DOMParser.parseFromString(`<body id="root">${removeDuplicateAttributes(output)}</body>`, "text/html");
|
|
1416
|
+
traverse(doc, forApp, 0, webp, { firstImageFound: false }, parentDomain, seoContext);
|
|
1389
1417
|
output = serializer.serializeToString(doc);
|
|
1390
1418
|
} catch (error) {
|
|
1391
1419
|
try {
|
|
@@ -1397,8 +1425,8 @@ function markdownToHTML(input, forApp, webp, parentDomain = "ecency.com") {
|
|
|
1397
1425
|
lowerCaseAttributeNames: false
|
|
1398
1426
|
});
|
|
1399
1427
|
const repairedHtml = domSerializer(dom.children);
|
|
1400
|
-
const doc = DOMParser.parseFromString(`<body id="root">${repairedHtml}</body>`, "text/html");
|
|
1401
|
-
traverse(doc, forApp, 0, webp, { firstImageFound: false }, parentDomain);
|
|
1428
|
+
const doc = DOMParser.parseFromString(`<body id="root">${removeDuplicateAttributes(repairedHtml)}</body>`, "text/html");
|
|
1429
|
+
traverse(doc, forApp, 0, webp, { firstImageFound: false }, parentDomain, seoContext);
|
|
1402
1430
|
output = serializer.serializeToString(doc);
|
|
1403
1431
|
} catch (fallbackError) {
|
|
1404
1432
|
const escapedContent = he2.encode(output || md.render(input));
|
|
@@ -1426,18 +1454,18 @@ function cacheSet(key, value) {
|
|
|
1426
1454
|
}
|
|
1427
1455
|
|
|
1428
1456
|
// src/markdown-2-html.ts
|
|
1429
|
-
function markdown2Html(obj, forApp = true, webp = false, parentDomain = "ecency.com") {
|
|
1457
|
+
function markdown2Html(obj, forApp = true, webp = false, parentDomain = "ecency.com", seoContext) {
|
|
1430
1458
|
if (typeof obj === "string") {
|
|
1431
1459
|
const cleanedStr = cleanReply(obj);
|
|
1432
|
-
return markdownToHTML(cleanedStr, forApp, webp, parentDomain);
|
|
1460
|
+
return markdownToHTML(cleanedStr, forApp, webp, parentDomain, seoContext);
|
|
1433
1461
|
}
|
|
1434
|
-
const key = `${makeEntryCacheKey(obj)}-md${webp ? "-webp" : ""}-${forApp ? "app" : "site"}-${parentDomain}`;
|
|
1462
|
+
const key = `${makeEntryCacheKey(obj)}-md${webp ? "-webp" : ""}-${forApp ? "app" : "site"}-${parentDomain}${seoContext ? `-seo${seoContext.authorReputation ?? ""}-${seoContext.postPayout ?? ""}` : ""}`;
|
|
1435
1463
|
const item = cacheGet(key);
|
|
1436
1464
|
if (item) {
|
|
1437
1465
|
return item;
|
|
1438
1466
|
}
|
|
1439
1467
|
const cleanBody = cleanReply(obj.body);
|
|
1440
|
-
const res = markdownToHTML(cleanBody, forApp, webp, parentDomain);
|
|
1468
|
+
const res = markdownToHTML(cleanBody, forApp, webp, parentDomain, seoContext);
|
|
1441
1469
|
cacheSet(key, res);
|
|
1442
1470
|
return res;
|
|
1443
1471
|
}
|