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 +73 -45
- package/package-lock.json +2 -2
- package/package.json +1 -2
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
|
121
|
-
if (
|
|
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
|
-
|
|
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
|
|
190
|
-
|
|
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
|
|
229
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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,
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
}
|