nodebb-plugin-link-preview 2.1.5 → 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
  }
@@ -209,6 +227,8 @@ async function process(content, { type, pid, tid, attachments }) {
209
227
  // Start preview for cache misses, but continue for now so as to not block response
210
228
  if (cold.size) {
211
229
  const coldArr = Array.from(cold);
230
+ const failures = new Set();
231
+ let successes = [];
212
232
  Promise.all(coldArr.map(preview)).then(async (previews) => {
213
233
  await Promise.all(previews.map(async (preview, idx) => {
214
234
  if (!preview) {
@@ -225,8 +245,8 @@ async function process(content, { type, pid, tid, attachments }) {
225
245
  switch (options.type) {
226
246
  case 'inline': {
227
247
  if (processInline) {
228
- const $anchor = options.target;
229
- $anchor.replaceWith($(html));
248
+ const { index, length } = options;
249
+ successes.push({ index, length, html });
230
250
  }
231
251
  break;
232
252
  }
@@ -236,17 +256,33 @@ async function process(content, { type, pid, tid, attachments }) {
236
256
  break;
237
257
  }
238
258
  }
259
+ } else if (options.type === 'attachment') {
260
+ // Preview failed, put back in placeholders
261
+ failures.add(url);
239
262
  }
240
263
  }));
241
264
 
242
- let content = $.html();
243
- content += attachmentHtml ? `\n\n<div class="row mt-3">${attachmentHtml}</div>` : '';
265
+ const placeholderHtml = Array.from(failures).reduce((html, cur) => {
266
+ html += `<p><a href="${cur}" rel="nofollow ugc">${cur}</a></p>`;
267
+ return html;
268
+ }, '');
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>` : '';
244
280
 
245
281
  // bust posts cache item
246
282
  if (pid) {
247
283
  const cache = postsCache.getOrCreate();
248
284
  const cacheKey = `${String(pid)}|${type}`;
249
- cache.set(cacheKey, content);
285
+ cache.set(cacheKey, modified);
250
286
 
251
287
  // fire post edit event with mocked data
252
288
  if (type === 'default' && tid) {
@@ -255,7 +291,7 @@ async function process(content, { type, pid, tid, attachments }) {
255
291
  tid,
256
292
  pid,
257
293
  changed: true,
258
- content,
294
+ content: modified,
259
295
  },
260
296
  topic: {},
261
297
  });
@@ -264,10 +300,18 @@ async function process(content, { type, pid, tid, attachments }) {
264
300
  });
265
301
  }
266
302
 
267
- content = $.html();
268
- content += attachmentHtml ? `\n\n<div class="row mt-3"><div class="col-12 mt-3">${attachmentHtml}</div></div>` : '';
269
- content += placeholderHtml ? `\n\n<div class="row mt-3"><div class="col-12 mt-3">${placeholderHtml}</div></div>` : '';
270
- 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;
271
315
  }
272
316
 
273
317
  async function render(preview) {
@@ -299,7 +343,7 @@ async function render(preview) {
299
343
  return false;
300
344
  }
301
345
 
302
- async function handleSpecialEmbed(url, $anchor) {
346
+ async function handleSpecialEmbed(url) {
303
347
  const { app } = require.main.require('./src/webserver');
304
348
  const { hostname, searchParams, pathname } = new URL(url);
305
349
  const { embedYoutube, embedVimeo, embedTiktok } = await meta.settings.get('link-preview');
@@ -318,12 +362,6 @@ async function handleSpecialEmbed(url, $anchor) {
318
362
  video = searchParams.get('v');
319
363
  }
320
364
  const html = await app.renderAsync(short ? 'partials/link-preview/youtube-short' : 'partials/link-preview/youtube', { video });
321
-
322
- if ($anchor) {
323
- $anchor.replaceWith(html);
324
- return true;
325
- }
326
-
327
365
  return html;
328
366
  }
329
367
 
@@ -331,11 +369,6 @@ async function handleSpecialEmbed(url, $anchor) {
331
369
  const video = pathname.slice(1);
332
370
  const html = await app.renderAsync('partials/link-preview/vimeo', { video });
333
371
 
334
- if ($anchor) {
335
- $anchor.replaceWith(html);
336
- return true;
337
- }
338
-
339
372
  return html;
340
373
  }
341
374
 
@@ -343,11 +376,6 @@ async function handleSpecialEmbed(url, $anchor) {
343
376
  const video = pathname.split('/')[3];
344
377
  const html = await app.renderAsync('partials/link-preview/tiktok', { video });
345
378
 
346
- if ($anchor) {
347
- $anchor.replaceWith(html);
348
- return true;
349
- }
350
-
351
379
  return html;
352
380
  }
353
381
 
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "nodebb-plugin-link-preview",
3
- "version": "2.1.5",
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.5",
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.5",
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
  }