@ecency/render-helper 2.4.33 → 2.4.35

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.
@@ -2,11 +2,11 @@ import { DOMParser as DOMParser$1, XMLSerializer } from '@xmldom/xmldom';
2
2
  import xss from 'xss';
3
3
  import multihash from 'multihashes';
4
4
  import querystring from 'querystring';
5
+ import { LRUCache } from 'lru-cache';
5
6
  import { Remarkable } from 'remarkable';
6
7
  import { linkify as linkify$1 } from 'remarkable/linkify';
7
8
  import * as htmlparser2 from 'htmlparser2';
8
9
  import * as domSerializerModule from 'dom-serializer';
9
- import { LRUCache } from 'lru-cache';
10
10
  import he from 'he';
11
11
 
12
12
  // src/consts/white-list.const.ts
@@ -177,23 +177,112 @@ function createParser() {
177
177
  var DOMParser = createParser();
178
178
 
179
179
  // src/helper.ts
180
+ function isSpaceChar(c) {
181
+ return c === 32 || c === 9 || c === 10 || c === 13 || c === 12;
182
+ }
183
+ function isAsciiLetter(c) {
184
+ return c >= 65 && c <= 90 || c >= 97 && c <= 122;
185
+ }
186
+ function isTagNameChar(c) {
187
+ return isAsciiLetter(c) || c >= 48 && c <= 57;
188
+ }
189
+ function isAttrNameChar(c) {
190
+ return isAsciiLetter(c) || c >= 48 && c <= 57 || c === 45 || c === 95 || c === 58 || c === 46;
191
+ }
180
192
  function removeDuplicateAttributes(html) {
181
- const tagRegex = /<([a-zA-Z][a-zA-Z0-9]*)\s+((?:[^>"']+|"[^"]*"|'[^']*')*?)\s*(\/?)>/g;
182
- return html.replace(tagRegex, (match, tagName, attrsString, selfClose) => {
183
- const seenAttrs = /* @__PURE__ */ new Set();
184
- const cleanedAttrs = [];
185
- const attrRegex = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)\s*(?:=\s*(?:"[^"]*"|'[^']*'|[^\s/>]+))?/g;
186
- let attrMatch;
187
- while ((attrMatch = attrRegex.exec(attrsString)) !== null) {
188
- const attrName = attrMatch[1].toLowerCase();
189
- if (!seenAttrs.has(attrName)) {
190
- seenAttrs.add(attrName);
191
- cleanedAttrs.push(attrMatch[0]);
193
+ const n = html.length;
194
+ let out = "";
195
+ let i = 0;
196
+ while (i < n) {
197
+ const lt = html.indexOf("<", i);
198
+ if (lt < 0) {
199
+ out += html.slice(i);
200
+ break;
201
+ }
202
+ out += html.slice(i, lt);
203
+ if (lt + 1 >= n || !isAsciiLetter(html.charCodeAt(lt + 1))) {
204
+ out += "<";
205
+ i = lt + 1;
206
+ continue;
207
+ }
208
+ let p2 = lt + 1;
209
+ while (p2 < n && isTagNameChar(html.charCodeAt(p2))) p2++;
210
+ const tagName = html.slice(lt + 1, p2);
211
+ if (p2 >= n || !isSpaceChar(html.charCodeAt(p2))) {
212
+ out += "<";
213
+ i = lt + 1;
214
+ continue;
215
+ }
216
+ const attrs = [];
217
+ const seen = /* @__PURE__ */ new Set();
218
+ let q = p2;
219
+ while (q < n) {
220
+ while (q < n && isSpaceChar(html.charCodeAt(q))) q++;
221
+ if (q >= n) break;
222
+ const ch = html.charCodeAt(q);
223
+ if (ch === 62) break;
224
+ if (ch === 47 && q + 1 < n && html.charCodeAt(q + 1) === 62) break;
225
+ const nameStart = q;
226
+ while (q < n && isAttrNameChar(html.charCodeAt(q))) q++;
227
+ if (q === nameStart) {
228
+ q++;
229
+ continue;
230
+ }
231
+ const attrName = html.slice(nameStart, q);
232
+ let r = q;
233
+ while (r < n && isSpaceChar(html.charCodeAt(r))) r++;
234
+ let valueEnd = q;
235
+ if (r < n && html.charCodeAt(r) === 61) {
236
+ r++;
237
+ while (r < n && isSpaceChar(html.charCodeAt(r))) r++;
238
+ if (r < n) {
239
+ const v = html.charCodeAt(r);
240
+ if (v === 34 || v === 39) {
241
+ const quote = html[r];
242
+ const end = html.indexOf(quote, r + 1);
243
+ if (end < 0) {
244
+ const gt = html.indexOf(">", r + 1);
245
+ valueEnd = gt < 0 ? n : gt;
246
+ } else {
247
+ valueEnd = end + 1;
248
+ }
249
+ } else {
250
+ let s = r;
251
+ while (s < n) {
252
+ const k = html.charCodeAt(s);
253
+ if (isSpaceChar(k) || k === 62) break;
254
+ s++;
255
+ }
256
+ valueEnd = s;
257
+ }
258
+ } else {
259
+ valueEnd = r;
260
+ }
261
+ }
262
+ const fullAttr = html.slice(nameStart, valueEnd);
263
+ q = valueEnd;
264
+ const key = attrName.toLowerCase();
265
+ if (!seen.has(key)) {
266
+ seen.add(key);
267
+ attrs.push(fullAttr);
192
268
  }
193
269
  }
194
- const attrsJoined = cleanedAttrs.length > 0 ? ` ${cleanedAttrs.join(" ")}` : "";
195
- return `<${tagName}${attrsJoined}${selfClose ? " /" : ""}>`;
196
- });
270
+ let selfClose = false;
271
+ if (q < n && html.charCodeAt(q) === 47) {
272
+ selfClose = true;
273
+ q++;
274
+ }
275
+ if (q >= n || html.charCodeAt(q) !== 62) {
276
+ out += "<";
277
+ i = lt + 1;
278
+ continue;
279
+ }
280
+ q++;
281
+ const attrsJoined = attrs.length > 0 ? " " + attrs.join(" ") : "";
282
+ out += "<" + tagName + attrsJoined + (selfClose ? " /" : "") + ">";
283
+ i = q;
284
+ }
285
+ return out;
197
286
  }
198
287
  function createDoc(html) {
199
288
  if (html.trim() === "") {
@@ -296,6 +385,14 @@ function sanitizeHtml(html) {
296
385
  });
297
386
  }
298
387
  var proxyBase = "https://images.ecency.com";
388
+ var urlHashCache = new LRUCache({ max: 500 });
389
+ function getUrlHash(url) {
390
+ const cached = urlHashCache.get(url);
391
+ if (cached) return cached;
392
+ const hash = multihash.toB58String(Buffer.from(url));
393
+ urlHashCache.set(url, hash);
394
+ return hash;
395
+ }
299
396
  function setProxyBase(p2) {
300
397
  proxyBase = p2;
301
398
  }
@@ -346,7 +443,7 @@ function proxifyImageSrc(url, width = 0, height = 0, _format = "match") {
346
443
  if (pHash) {
347
444
  return `${proxyBase}/p/${pHash}?${qs}`;
348
445
  }
349
- const b58url = multihash.toB58String(Buffer.from(realUrl.toString()));
446
+ const b58url = getUrlHash(realUrl.toString());
350
447
  return `${proxyBase}/p/${b58url}?${qs}`;
351
448
  }
352
449
  var SRCSET_WIDTHS = [320, 600, 800, 1024, 1280];
@@ -1578,7 +1675,6 @@ function markdownToHTML(input, forApp, parentDomain = "ecency.com", seoContext,
1578
1675
  output = serializer.serializeToString(doc);
1579
1676
  } catch (error) {
1580
1677
  try {
1581
- output = md.render(input);
1582
1678
  const preSanitized = sanitizeHtml(output);
1583
1679
  const dom = htmlparser2.parseDocument(preSanitized, {
1584
1680
  // lenient options - don't throw on malformed HTML
@@ -1618,7 +1714,7 @@ function simpleMarkdownToHTML(input) {
1618
1714
  const html = getMd().render(input);
1619
1715
  return sanitizeHtml(html);
1620
1716
  }
1621
- var cache = new LRUCache({ max: 60 });
1717
+ var cache = new LRUCache({ max: 500 });
1622
1718
  function setCacheSize(size) {
1623
1719
  cache = new LRUCache({ max: size });
1624
1720
  }
@@ -1649,6 +1745,40 @@ var gifLinkRegex = /\.(gif)$/i;
1649
1745
  function isGifLink(link) {
1650
1746
  return gifLinkRegex.test(link);
1651
1747
  }
1748
+ var BACKTICK_FENCE_RE = /```[\s\S]*?```/g;
1749
+ var TILDE_FENCE_RE = /~~~[\s\S]*?~~~/g;
1750
+ var INLINE_CODE_RE = /`[^`\n]*`/g;
1751
+ var INDENTED_CODE_RE = /^(?: {4}|\t).+$/gm;
1752
+ var MD_IMAGE_RE = /!\[[^\]]*\]\(\s*([^)\s]+)(?:\s+["'][^"']*["'])?\s*\)/;
1753
+ var HTML_IMAGE_RE = /<img\b[^>]*?\bsrc\s*=\s*["']([^"']+)["']/i;
1754
+ var SAFE_URL_RE = /^https?:\/\//i;
1755
+ function findFirstImageUrl(body) {
1756
+ if (!body) return null;
1757
+ const cleaned = body.replace(BACKTICK_FENCE_RE, "").replace(TILDE_FENCE_RE, "").replace(INLINE_CODE_RE, "").replace(INDENTED_CODE_RE, "");
1758
+ const mdMatch = cleaned.match(MD_IMAGE_RE);
1759
+ const htmlMatch = cleaned.match(HTML_IMAGE_RE);
1760
+ if (mdMatch) {
1761
+ const url = mdMatch[1];
1762
+ if (!url || !SAFE_URL_RE.test(url) || url.includes("(")) {
1763
+ return null;
1764
+ }
1765
+ }
1766
+ const mdValid = !!mdMatch;
1767
+ const htmlValid = !!(htmlMatch && htmlMatch[1] && SAFE_URL_RE.test(htmlMatch[1]));
1768
+ if (mdValid && htmlValid) {
1769
+ return (mdMatch.index ?? 0) < (htmlMatch.index ?? 0) ? mdMatch[1] : htmlMatch[1];
1770
+ }
1771
+ if (mdValid) return mdMatch[1];
1772
+ if (htmlValid) return htmlMatch[1];
1773
+ return null;
1774
+ }
1775
+ function proxifyFound(src, width, height, format) {
1776
+ const decoded = he.decode(src);
1777
+ if (isGifLink(decoded)) {
1778
+ return proxifyImageSrc(decoded, 0, 0, format);
1779
+ }
1780
+ return proxifyImageSrc(decoded, width, height, format);
1781
+ }
1652
1782
  function getImage(entry, width = 0, height = 0, format = "match") {
1653
1783
  let meta;
1654
1784
  if (typeof entry.json_metadata === "object") {
@@ -1680,6 +1810,10 @@ function getImage(entry, width = 0, height = 0, format = "match") {
1680
1810
  }
1681
1811
  return proxifyImageSrc(meta.image[0], width, height, format);
1682
1812
  }
1813
+ const fast = findFirstImageUrl(entry.body);
1814
+ if (fast) {
1815
+ return proxifyFound(fast, width, height, format);
1816
+ }
1683
1817
  const html = markdown2Html(entry);
1684
1818
  const doc = createDoc(html);
1685
1819
  if (!doc) {
@@ -1691,16 +1825,16 @@ function getImage(entry, width = 0, height = 0, format = "match") {
1691
1825
  if (!src) {
1692
1826
  return null;
1693
1827
  }
1694
- const decodedSrc = he.decode(src);
1695
- if (isGifLink(decodedSrc)) {
1696
- return proxifyImageSrc(decodedSrc, 0, 0, format);
1697
- }
1698
- return proxifyImageSrc(decodedSrc, width, height, format);
1828
+ return proxifyFound(src, width, height, format);
1699
1829
  }
1700
1830
  return null;
1701
1831
  }
1702
1832
  function catchPostImage(obj, width = 0, height = 0, format = "match") {
1703
1833
  if (typeof obj === "string") {
1834
+ const fast = findFirstImageUrl(obj);
1835
+ if (fast) {
1836
+ return proxifyFound(fast, width, height, format);
1837
+ }
1704
1838
  const html = markdown2Html(obj);
1705
1839
  const doc = createDoc(html);
1706
1840
  if (!doc) {
@@ -1712,11 +1846,7 @@ function catchPostImage(obj, width = 0, height = 0, format = "match") {
1712
1846
  if (!src) {
1713
1847
  return null;
1714
1848
  }
1715
- const decodedSrc = he.decode(src);
1716
- if (isGifLink(decodedSrc)) {
1717
- return proxifyImageSrc(decodedSrc, 0, 0, format);
1718
- }
1719
- return proxifyImageSrc(decodedSrc, width, height, format);
1849
+ return proxifyFound(src, width, height, format);
1720
1850
  }
1721
1851
  return null;
1722
1852
  }
@@ -1729,6 +1859,20 @@ function catchPostImage(obj, width = 0, height = 0, format = "match") {
1729
1859
  cacheSet(key, res);
1730
1860
  return res;
1731
1861
  }
1862
+ var summaryRenderer = new Remarkable({
1863
+ html: true,
1864
+ breaks: true,
1865
+ typographer: false
1866
+ });
1867
+ summaryRenderer.core.ruler.enable(["abbr"]);
1868
+ summaryRenderer.block.ruler.enable(["footnote", "deflist"]);
1869
+ summaryRenderer.inline.ruler.enable([
1870
+ "footnote_inline",
1871
+ "ins",
1872
+ "mark",
1873
+ "sub",
1874
+ "sup"
1875
+ ]);
1732
1876
  var joint = (arr, limit = 200) => {
1733
1877
  let result = "";
1734
1878
  if (arr) {
@@ -1754,25 +1898,6 @@ function postBodySummary(entryBody, length = 200, platform = "web") {
1754
1898
  return "";
1755
1899
  }
1756
1900
  entryBody = cleanReply(entryBody);
1757
- const mdd = new Remarkable({
1758
- html: true,
1759
- breaks: true,
1760
- typographer: false
1761
- }).use(linkify$1);
1762
- mdd.core.ruler.enable([
1763
- "abbr"
1764
- ]);
1765
- mdd.block.ruler.enable([
1766
- "footnote",
1767
- "deflist"
1768
- ]);
1769
- mdd.inline.ruler.enable([
1770
- "footnote_inline",
1771
- "ins",
1772
- "mark",
1773
- "sub",
1774
- "sup"
1775
- ]);
1776
1901
  const entities = entryBody.match(ENTITY_REGEX);
1777
1902
  const entityPlaceholders = [];
1778
1903
  if (entities && platform !== "web") {
@@ -1785,7 +1910,7 @@ function postBodySummary(entryBody, length = 200, platform = "web") {
1785
1910
  }
1786
1911
  let text2 = "";
1787
1912
  try {
1788
- text2 = mdd.render(entryBody);
1913
+ text2 = summaryRenderer.render(entryBody);
1789
1914
  } catch (err) {
1790
1915
  console.error("[postBodySummary] Failed to render markdown:", {
1791
1916
  error: err instanceof Error ? err.message : String(err),