@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.
@@ -9,7 +9,20 @@ interface Entry {
9
9
  json_metadata?: any;
10
10
  }
11
11
 
12
- declare function markdown2Html(obj: Entry | string, forApp?: boolean, webp?: boolean, parentDomain?: string): string;
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 };
@@ -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 doc = DOMParser.parseFromString(`<body>${html}</body>`, "text/html");
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", "noopener");
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
  }