nodebb-plugin-link-preview 2.1.6 → 2.2.0

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/library.js CHANGED
@@ -7,7 +7,6 @@ const nconf = require.main.require('nconf');
7
7
  const dns = require('dns');
8
8
 
9
9
  const { getLinkPreview } = require('link-preview-js');
10
- const { load } = require('cheerio');
11
10
  const { isURL } = require('validator');
12
11
 
13
12
  const meta = require.main.require('./src/meta');
@@ -96,30 +95,46 @@ async function process(content, { type, pid, tid, attachments }) {
96
95
  const requests = new Map();
97
96
  let attachmentHtml = '';
98
97
  let placeholderHtml = '';
98
+ let hits = [];
99
99
 
100
100
  // Parse inline urls
101
- const $ = load(content, null, false);
102
- for (const anchor of $('a')) {
103
- const $anchor = $(anchor);
104
-
105
- // Skip if the anchor has link text, or has text on the same line.
106
- let url = $anchor.attr('href');
107
- url = decodeURI(url);
108
- const text = $anchor.text();
109
- const hasSiblings = !!anchor.prev || !!anchor.next;
110
- if (hasSiblings || url !== text || anchor.parent.name !== 'p') {
101
+ const anchorRegex = /<a [^>]+>.*?<\/a>/gi;
102
+ let match = anchorRegex.exec(content);
103
+ while (match !== null) {
104
+ const { index } = match;
105
+ const { length } = match[0];
106
+ const before = content.slice(Math.max(0, index - 20), index);
107
+ const after = content.slice(index + length, index + length + 20);
108
+ const wrapped = before.trim().endsWith('<p dir="auto">') || before.trim().endsWith('<p>');
109
+ const closed = after.trim().startsWith('</p>');
110
+
111
+ if (!wrapped || !closed) {
112
+ match = anchorRegex.exec(content);
111
113
  continue;
112
114
  }
113
115
 
114
- // Handle relative URLs
115
- if (!url.startsWith('http')) {
116
+ const urlMatch = match[0].match(/href=["'](.*?)["']/);
117
+ let url = urlMatch ? decodeURI(urlMatch[1]) : '';
118
+ const text = match[0].replace(/<[^>]+>/g, ''); // Strip tags to get text
119
+ if (url === text) {
120
+ match = anchorRegex.exec(content);
121
+ continue;
122
+ }
123
+
124
+ // Otherwise, process the anchor...
125
+
126
+ if (url.startsWith('//')) { // Handle protocol-relative URLs
127
+ url = `${nconf.get('url_parsed').protocol}${url}`;
128
+ } else if (!url.startsWith('http')) { // Handle relative URLs
116
129
  url = `${nconf.get('url')}${url.startsWith('/') ? url : `/${url}`}`;
117
130
  }
118
131
 
119
132
  if (processInline) {
120
- const special = await handleSpecialEmbed(url, $anchor);
121
- if (special) {
133
+ const html = await handleSpecialEmbed(url);
134
+ if (html) {
122
135
  requests.delete(url);
136
+ hits.push({ index, length, html });
137
+ match = anchorRegex.exec(content);
123
138
  continue;
124
139
  }
125
140
  }
@@ -127,8 +142,11 @@ async function process(content, { type, pid, tid, attachments }) {
127
142
  // Inline url takes precedence over attachment
128
143
  requests.set(url, {
129
144
  type: 'inline',
130
- target: $anchor,
145
+ index,
146
+ length,
131
147
  });
148
+
149
+ match = anchorRegex.exec(content);
132
150
  }
133
151
 
134
152
  // Post attachments
@@ -186,8 +204,8 @@ async function process(content, { type, pid, tid, attachments }) {
186
204
  switch (options.type) {
187
205
  case 'inline': {
188
206
  if (processInline) {
189
- const $anchor = options.target;
190
- $anchor.replaceWith($(html));
207
+ const { index, length } = options;
208
+ hits.push({ index, length, html });
191
209
  }
192
210
  break;
193
211
  }
@@ -210,6 +228,7 @@ async function process(content, { type, pid, tid, attachments }) {
210
228
  if (cold.size) {
211
229
  const coldArr = Array.from(cold);
212
230
  const failures = new Set();
231
+ let successes = [];
213
232
  Promise.all(coldArr.map(preview)).then(async (previews) => {
214
233
  await Promise.all(previews.map(async (preview, idx) => {
215
234
  if (!preview) {
@@ -226,8 +245,8 @@ async function process(content, { type, pid, tid, attachments }) {
226
245
  switch (options.type) {
227
246
  case 'inline': {
228
247
  if (processInline) {
229
- const $anchor = options.target;
230
- $anchor.replaceWith($(html));
248
+ const { index, length } = options;
249
+ successes.push({ index, length, html });
231
250
  }
232
251
  break;
233
252
  }
@@ -247,15 +266,23 @@ async function process(content, { type, pid, tid, attachments }) {
247
266
  html += `<p><a href="${cur}" rel="nofollow ugc">${cur}</a></p>`;
248
267
  return html;
249
268
  }, '');
250
- let content = $.html();
251
- content += attachmentHtml ? `\n\n<div class="row mt-3">${attachmentHtml}</div>` : '';
252
- content += placeholderHtml ? `\n\n<div class="row mt-3"><div class="col-12 mt-3">${placeholderHtml}</div></div>` : '';
269
+ let modified = content;
270
+
271
+ successes = successes.sort((a, b) => b.index - a.index);
272
+ successes.forEach(({ html, index, length }) => {
273
+ modified =
274
+ modified.slice(0, index) +
275
+ html +
276
+ modified.slice(index + length);
277
+ });
278
+ modified += attachmentHtml ? `\n\n<div class="row mt-3">${attachmentHtml}</div>` : '';
279
+ modified += placeholderHtml ? `\n\n<div class="row mt-3"><div class="col-12 mt-3">${placeholderHtml}</div></div>` : '';
253
280
 
254
281
  // bust posts cache item
255
282
  if (pid) {
256
283
  const cache = postsCache.getOrCreate();
257
284
  const cacheKey = `${String(pid)}|${type}`;
258
- cache.set(cacheKey, content);
285
+ cache.set(cacheKey, modified);
259
286
 
260
287
  // fire post edit event with mocked data
261
288
  if (type === 'default' && tid) {
@@ -264,7 +291,7 @@ async function process(content, { type, pid, tid, attachments }) {
264
291
  tid,
265
292
  pid,
266
293
  changed: true,
267
- content,
294
+ content: modified,
268
295
  },
269
296
  topic: {},
270
297
  });
@@ -273,10 +300,18 @@ async function process(content, { type, pid, tid, attachments }) {
273
300
  });
274
301
  }
275
302
 
276
- content = $.html();
277
- content += attachmentHtml ? `\n\n<div class="row mt-3"><div class="col-12 mt-3">${attachmentHtml}</div></div>` : '';
278
- content += placeholderHtml ? `\n\n<div class="row mt-3"><div class="col-12 mt-3">${placeholderHtml}</div></div>` : '';
279
- return content;
303
+ let modified = content;
304
+
305
+ hits = hits.sort((a, b) => b.index - a.index);
306
+ hits.forEach(({ html, index, length }) => {
307
+ modified =
308
+ modified.slice(0, index) +
309
+ html +
310
+ modified.slice(index + length);
311
+ });
312
+ modified += attachmentHtml ? `\n\n<div class="row mt-3"><div class="col-12 mt-3">${attachmentHtml}</div></div>` : '';
313
+ modified += placeholderHtml ? `\n\n<div class="row mt-3"><div class="col-12 mt-3">${placeholderHtml}</div></div>` : '';
314
+ return modified;
280
315
  }
281
316
 
282
317
  async function render(preview) {
@@ -308,7 +343,7 @@ async function render(preview) {
308
343
  return false;
309
344
  }
310
345
 
311
- async function handleSpecialEmbed(url, $anchor) {
346
+ async function handleSpecialEmbed(url) {
312
347
  const { app } = require.main.require('./src/webserver');
313
348
  const { hostname, searchParams, pathname } = new URL(url);
314
349
  const { embedYoutube, embedVimeo, embedTiktok } = await meta.settings.get('link-preview');
@@ -327,12 +362,6 @@ async function handleSpecialEmbed(url, $anchor) {
327
362
  video = searchParams.get('v');
328
363
  }
329
364
  const html = await app.renderAsync(short ? 'partials/link-preview/youtube-short' : 'partials/link-preview/youtube', { video });
330
-
331
- if ($anchor) {
332
- $anchor.replaceWith(html);
333
- return true;
334
- }
335
-
336
365
  return html;
337
366
  }
338
367
 
@@ -340,11 +369,6 @@ async function handleSpecialEmbed(url, $anchor) {
340
369
  const video = pathname.slice(1);
341
370
  const html = await app.renderAsync('partials/link-preview/vimeo', { video });
342
371
 
343
- if ($anchor) {
344
- $anchor.replaceWith(html);
345
- return true;
346
- }
347
-
348
372
  return html;
349
373
  }
350
374
 
@@ -352,11 +376,6 @@ async function handleSpecialEmbed(url, $anchor) {
352
376
  const video = pathname.split('/')[3];
353
377
  const html = await app.renderAsync('partials/link-preview/tiktok', { video });
354
378
 
355
- if ($anchor) {
356
- $anchor.replaceWith(html);
357
- return true;
358
- }
359
-
360
379
  return html;
361
380
  }
362
381
 
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "nodebb-plugin-link-preview",
3
- "version": "2.1.6",
3
+ "version": "2.2.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "nodebb-plugin-link-preview",
9
- "version": "2.1.6",
9
+ "version": "2.2.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "cheerio": "^1.0.0-rc.12",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-link-preview",
3
- "version": "2.1.6",
3
+ "version": "2.2.0",
4
4
  "description": "A starter kit for quickly creating NodeBB plugins",
5
5
  "main": "library.js",
6
6
  "repository": {
@@ -43,7 +43,6 @@
43
43
  "lint-staged": "13.2.2"
44
44
  },
45
45
  "dependencies": {
46
- "cheerio": "^1.0.0-rc.12",
47
46
  "link-preview-js": "^3.0.4",
48
47
  "validator": "^13.11.0"
49
48
  }