nodebb-plugin-markdown 8.14.3 → 9.0.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/.eslintignore +1 -1
- package/.eslintrc +128 -128
- package/.jshintrc +85 -85
- package/README.md +9 -9
- package/commitlint.config.js +26 -26
- package/index.js +482 -476
- package/lib/controllers.js +32 -32
- package/package.json +51 -50
- package/plugin.json +40 -41
- package/public/js/admin.js +76 -76
- package/public/js/client.js +251 -246
- package/public/js/highlightjs-line-numbers.js +372 -189
- package/public/languages/de/markdown.json +11 -11
- package/public/languages/en-GB/markdown.json +11 -11
- package/public/languages/fa-IR/markdown.json +11 -11
- package/public/languages/fr/markdown.json +11 -11
- package/public/languages/he/markdown.json +12 -12
- package/public/languages/pl/markdown.json +11 -11
- package/public/languages/ru/markdown.json +10 -10
- package/public/languages/tr/markdown.json +11 -11
- package/public/languages/zh-CN/markdown.json +11 -11
- package/public/less/default.less +58 -58
- package/public/templates/admin/plugins/markdown.tpl +190 -180
- package/renovate.json +5 -0
- package/websockets.js +37 -37
- package/public/js/highlight.js +0 -2
package/index.js
CHANGED
|
@@ -1,476 +1,482 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const MarkdownIt = require('markdown-it');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const url = require('url');
|
|
7
|
-
|
|
8
|
-
const probe = require('probe-image-size');
|
|
9
|
-
|
|
10
|
-
const nconf = require.main.require('nconf');
|
|
11
|
-
const winston = require.main.require('winston');
|
|
12
|
-
const meta = require.main.require('./src/meta');
|
|
13
|
-
const posts = require.main.require('./src/posts');
|
|
14
|
-
const translator = require.main.require('./src/translator');
|
|
15
|
-
const plugins = require.main.require('./src/plugins');
|
|
16
|
-
const cacheCreate = require.main.require('./src/cacheCreate');
|
|
17
|
-
|
|
18
|
-
const SocketPlugins = require.main.require('./src/socket.io/plugins');
|
|
19
|
-
SocketPlugins.markdown = require('./websockets');
|
|
20
|
-
|
|
21
|
-
let parser;
|
|
22
|
-
|
|
23
|
-
const Markdown = {
|
|
24
|
-
config: {},
|
|
25
|
-
_externalImageCache: undefined,
|
|
26
|
-
onLoad: async function (params) {
|
|
27
|
-
const controllers = require('./lib/controllers');
|
|
28
|
-
const hostMiddleware = require.main.require('./src/middleware');
|
|
29
|
-
const middlewares = [hostMiddleware.maintenanceMode, hostMiddleware.registrationComplete, hostMiddleware.pluginHooks];
|
|
30
|
-
|
|
31
|
-
params.router.get('/admin/plugins/markdown', params.middleware.admin.buildHeader, controllers.renderAdmin);
|
|
32
|
-
params.router.get('/api/admin/plugins/markdown', controllers.renderAdmin);
|
|
33
|
-
|
|
34
|
-
// Return raw markdown via GET
|
|
35
|
-
params.router.get('/api/post/:pid/raw', middlewares, controllers.retrieveRaw);
|
|
36
|
-
|
|
37
|
-
Markdown.init();
|
|
38
|
-
Markdown.loadThemes();
|
|
39
|
-
|
|
40
|
-
return params;
|
|
41
|
-
},
|
|
42
|
-
|
|
43
|
-
getConfig:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
_self.config[field] =
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
html = html.replace(
|
|
254
|
-
return '@
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
payload = execute(payload);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
{ name: '
|
|
286
|
-
{ name: '
|
|
287
|
-
{ name: '
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
config.
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
parser.renderer.rules.
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if (
|
|
379
|
-
tokens[idx].attrPush(['
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
tokens[idx].
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MarkdownIt = require('markdown-it');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const url = require('url');
|
|
7
|
+
|
|
8
|
+
const probe = require('probe-image-size');
|
|
9
|
+
|
|
10
|
+
const nconf = require.main.require('nconf');
|
|
11
|
+
const winston = require.main.require('winston');
|
|
12
|
+
const meta = require.main.require('./src/meta');
|
|
13
|
+
const posts = require.main.require('./src/posts');
|
|
14
|
+
const translator = require.main.require('./src/translator');
|
|
15
|
+
const plugins = require.main.require('./src/plugins');
|
|
16
|
+
const cacheCreate = require.main.require('./src/cacheCreate');
|
|
17
|
+
|
|
18
|
+
const SocketPlugins = require.main.require('./src/socket.io/plugins');
|
|
19
|
+
SocketPlugins.markdown = require('./websockets');
|
|
20
|
+
|
|
21
|
+
let parser;
|
|
22
|
+
|
|
23
|
+
const Markdown = {
|
|
24
|
+
config: {},
|
|
25
|
+
_externalImageCache: undefined,
|
|
26
|
+
onLoad: async function (params) {
|
|
27
|
+
const controllers = require('./lib/controllers');
|
|
28
|
+
const hostMiddleware = require.main.require('./src/middleware');
|
|
29
|
+
const middlewares = [hostMiddleware.maintenanceMode, hostMiddleware.registrationComplete, hostMiddleware.pluginHooks];
|
|
30
|
+
|
|
31
|
+
params.router.get('/admin/plugins/markdown', params.middleware.admin.buildHeader, controllers.renderAdmin);
|
|
32
|
+
params.router.get('/api/admin/plugins/markdown', controllers.renderAdmin);
|
|
33
|
+
|
|
34
|
+
// Return raw markdown via GET
|
|
35
|
+
params.router.get('/api/post/:pid/raw', middlewares, controllers.retrieveRaw);
|
|
36
|
+
|
|
37
|
+
Markdown.init();
|
|
38
|
+
Markdown.loadThemes();
|
|
39
|
+
|
|
40
|
+
return params;
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
getConfig: async (config) => {
|
|
44
|
+
const { defaultHighlightLanguage } = await meta.settings.get('markdown');
|
|
45
|
+
|
|
46
|
+
config.markdown = {
|
|
47
|
+
highlight: Markdown.highlight ? 1 : 0,
|
|
48
|
+
highlightLinesLanguageList: Markdown.config.highlightLinesLanguageList,
|
|
49
|
+
theme: Markdown.config.highlightTheme || 'default.min.css',
|
|
50
|
+
defaultHighlightLanguage,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return config;
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
getLinkTags: async (hookData) => {
|
|
57
|
+
const { highlightTheme } = await meta.settings.get('markdown');
|
|
58
|
+
|
|
59
|
+
hookData.links.push({
|
|
60
|
+
rel: 'prefetch stylesheet',
|
|
61
|
+
type: '',
|
|
62
|
+
href: `https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.4.0/styles/${highlightTheme || 'default.min.css'}`,
|
|
63
|
+
}, {
|
|
64
|
+
rel: 'prefetch',
|
|
65
|
+
href: `${nconf.get('relative_path')}/assets/language/${meta.config.defaultLang || 'en-GB'}/markdown.json?${meta.config['cache-buster']}`,
|
|
66
|
+
}, {
|
|
67
|
+
rel: 'prefetch',
|
|
68
|
+
href: 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.4.0/highlight.min.js',
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return hookData;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
init: function () {
|
|
75
|
+
// Load saved config
|
|
76
|
+
const _self = this;
|
|
77
|
+
const defaults = {
|
|
78
|
+
html: false,
|
|
79
|
+
|
|
80
|
+
langPrefix: 'language-',
|
|
81
|
+
highlight: true,
|
|
82
|
+
highlightLinesLanguageList: [],
|
|
83
|
+
highlightTheme: 'default.min.css',
|
|
84
|
+
|
|
85
|
+
probe: true,
|
|
86
|
+
probeCacheSize: 256,
|
|
87
|
+
|
|
88
|
+
xhtmlOut: true,
|
|
89
|
+
breaks: true,
|
|
90
|
+
linkify: true,
|
|
91
|
+
typographer: false,
|
|
92
|
+
externalBlank: false,
|
|
93
|
+
nofollow: true,
|
|
94
|
+
allowRTLO: false,
|
|
95
|
+
checkboxes: true,
|
|
96
|
+
multimdTables: true,
|
|
97
|
+
};
|
|
98
|
+
const notCheckboxes = ['langPrefix', 'highlightTheme', 'highlightLinesLanguageList', 'probeCacheSize'];
|
|
99
|
+
|
|
100
|
+
meta.settings.get('markdown', function (err, options) {
|
|
101
|
+
if (err) {
|
|
102
|
+
winston.warn(`[plugin/markdown] Unable to retrieve settings, assuming defaults: ${err.message}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const field in defaults) {
|
|
106
|
+
// If not set in config (nil)
|
|
107
|
+
if (!options.hasOwnProperty(field)) {
|
|
108
|
+
_self.config[field] = defaults[field];
|
|
109
|
+
} else if (!notCheckboxes.includes(field)) {
|
|
110
|
+
_self.config[field] = options[field] === 'on';
|
|
111
|
+
} else {
|
|
112
|
+
_self.config[field] = options[field];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_self.highlight = _self.config.highlight;
|
|
117
|
+
delete _self.config.highlight;
|
|
118
|
+
|
|
119
|
+
if (typeof _self.config.highlightLinesLanguageList === 'string') {
|
|
120
|
+
try {
|
|
121
|
+
_self.config.highlightLinesLanguageList = JSON.parse(_self.config.highlightLinesLanguageList);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
winston.warn('[plugins/markdown] Invalid config for highlightLinesLanguageList, blanking.');
|
|
124
|
+
_self.config.highlightLinesLanguageList = [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
_self.config.highlightLinesLanguageList = _self.config.highlightLinesLanguageList.join(',').split(',');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
parser = new MarkdownIt(_self.config);
|
|
131
|
+
|
|
132
|
+
Markdown.updateParserRules(parser);
|
|
133
|
+
|
|
134
|
+
// External image size cache
|
|
135
|
+
if (_self.config.probe) {
|
|
136
|
+
Markdown._externalImageCache = cacheCreate({
|
|
137
|
+
name: 'markdown.externalImageCache',
|
|
138
|
+
max: parseInt(_self.config.probeCacheSize, 10) || 256,
|
|
139
|
+
length: function () { return 1; },
|
|
140
|
+
maxAge: 1000 * 60 * 60 * 24, // 1 day
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
loadThemes: function () {
|
|
147
|
+
fs.readdir(path.resolve(__dirname, 'node_modules/@highlightjs/cdn-assets/styles'), function (err, files) {
|
|
148
|
+
if (err) {
|
|
149
|
+
winston.error('[plugin/markdown] Could not load Markdown themes: ' + err.message);
|
|
150
|
+
Markdown.themes = [];
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const isStylesheet = /\.css$/;
|
|
154
|
+
Markdown.themes = files.filter(function (file) {
|
|
155
|
+
return isStylesheet.test(file);
|
|
156
|
+
}).map(function (file) {
|
|
157
|
+
return {
|
|
158
|
+
name: file,
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
parsePost: async function (data) {
|
|
165
|
+
const env = await Markdown.beforeParse(data);
|
|
166
|
+
if (data && data.postData && data.postData.content && parser) {
|
|
167
|
+
data.postData.content = parser.render(data.postData.content, env || {});
|
|
168
|
+
}
|
|
169
|
+
return Markdown.afterParse(data);
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
parseSignature: async function (data) {
|
|
173
|
+
if (data && data.userData && data.userData.signature && parser) {
|
|
174
|
+
data.userData.signature = parser.render(data.userData.signature);
|
|
175
|
+
}
|
|
176
|
+
return Markdown.afterParse(data);
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
parseAboutMe: async function (aboutme) {
|
|
180
|
+
aboutme = (aboutme && parser) ? parser.render(aboutme) : aboutme;
|
|
181
|
+
// process.nextTick(next, null, aboutme);
|
|
182
|
+
return Markdown.afterParse(aboutme);
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
parseRaw: async function (raw) {
|
|
186
|
+
raw = (raw && parser) ? parser.render(raw) : raw;
|
|
187
|
+
return Markdown.afterParse(raw);
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
beforeParse: async (data) => {
|
|
191
|
+
const env = {
|
|
192
|
+
images: new Map(),
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
if (data && data.postData && data.postData.pid) {
|
|
196
|
+
// Check that pid for images, and return their sizes
|
|
197
|
+
const images = await posts.uploads.listWithSizes(data.postData.pid);
|
|
198
|
+
env.images = images.reduce((memo, cur) => {
|
|
199
|
+
memo.set(cur.name, cur);
|
|
200
|
+
delete cur.name;
|
|
201
|
+
return memo;
|
|
202
|
+
}, env.images);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Probe post data for external images as well
|
|
206
|
+
if (Markdown.config.probe && data && data.postData && data.postData.content) {
|
|
207
|
+
const matcher = /!\[[^\]]*?\]\((https?[^)]+?)\)/g;
|
|
208
|
+
let current;
|
|
209
|
+
|
|
210
|
+
// eslint-disable-next-line no-cond-assign
|
|
211
|
+
while ((current = matcher.exec(data.postData.content)) !== null) {
|
|
212
|
+
const match = current[1];
|
|
213
|
+
if (match && Markdown.isExternalLink(match)) { // for security only parse external images
|
|
214
|
+
const parsedUrl = url.parse(match);
|
|
215
|
+
const filename = path.basename(parsedUrl.pathname);
|
|
216
|
+
const size = Markdown._externalImageCache.get(match);
|
|
217
|
+
if (size) {
|
|
218
|
+
env.images.set(filename, size);
|
|
219
|
+
} else {
|
|
220
|
+
try {
|
|
221
|
+
// eslint-disable-next-line no-await-in-loop
|
|
222
|
+
const size = await probe(match);
|
|
223
|
+
|
|
224
|
+
let { width, height } = size;
|
|
225
|
+
|
|
226
|
+
// Swap width and height if orientation bit is set
|
|
227
|
+
if (size.orientation >= 5 && size.orientation <= 8) {
|
|
228
|
+
[width, height] = [height, width];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
env.images.set(filename, { width, height });
|
|
232
|
+
Markdown._externalImageCache.set(match, { width, height });
|
|
233
|
+
} catch (e) {
|
|
234
|
+
// No handling required
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return env;
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
afterParse: function (payload) {
|
|
245
|
+
if (!payload) {
|
|
246
|
+
return payload;
|
|
247
|
+
}
|
|
248
|
+
const italicMention = /@<em>([^<]+)<\/em>/g;
|
|
249
|
+
const boldMention = /@<strong>([^<]+)<\/strong>/g;
|
|
250
|
+
const execute = function (html) {
|
|
251
|
+
// Replace all italicised mentions back to regular mentions
|
|
252
|
+
if (italicMention.test(html)) {
|
|
253
|
+
html = html.replace(italicMention, function (match, slug) {
|
|
254
|
+
return '@_' + slug + '_';
|
|
255
|
+
});
|
|
256
|
+
} else if (boldMention.test(html)) {
|
|
257
|
+
html = html.replace(boldMention, function (match, slug) {
|
|
258
|
+
return '@__' + slug + '__';
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return html;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
if (payload.hasOwnProperty('postData')) {
|
|
266
|
+
payload.postData.content = execute(payload.postData.content);
|
|
267
|
+
} else if (payload.hasOwnProperty('userData')) {
|
|
268
|
+
payload.userData.signature = execute(payload.userData.signature);
|
|
269
|
+
} else {
|
|
270
|
+
payload = execute(payload);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return payload;
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
renderHelp: async function (helpContent) {
|
|
277
|
+
const translated = await translator.translate('[[markdown:help_text]]');
|
|
278
|
+
const parsed = await plugins.hooks.fire('filter:parse.raw', `## Markdown\n${translated}`);
|
|
279
|
+
helpContent += parsed;
|
|
280
|
+
return helpContent;
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
registerFormatting: async function (payload) {
|
|
284
|
+
const formatting = [
|
|
285
|
+
{ name: 'bold', className: 'fa fa-bold', title: '[[modules:composer.formatting.bold]]' },
|
|
286
|
+
{ name: 'italic', className: 'fa fa-italic', title: '[[modules:composer.formatting.italic]]' },
|
|
287
|
+
{ name: 'list', className: 'fa fa-list-ul', title: '[[modules:composer.formatting.list]]' },
|
|
288
|
+
{ name: 'strikethrough', className: 'fa fa-strikethrough', title: '[[modules:composer.formatting.strikethrough]]' },
|
|
289
|
+
{ name: 'code', className: 'fa fa-code', title: '[[modules:composer.formatting.code]]' },
|
|
290
|
+
{ name: 'link', className: 'fa fa-link', title: '[[modules:composer.formatting.link]]' },
|
|
291
|
+
{ name: 'picture-o', className: 'fa fa-picture-o', title: '[[modules:composer.formatting.picture]]' },
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
payload.options = formatting.concat(payload.options);
|
|
295
|
+
|
|
296
|
+
return payload;
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
updateSanitizeConfig: async (config) => {
|
|
300
|
+
config.allowedTags.push('input');
|
|
301
|
+
config.allowedAttributes.input = ['type', 'checked'];
|
|
302
|
+
config.allowedAttributes.ol.push('start');
|
|
303
|
+
config.allowedAttributes.th.push('colspan', 'rowspan');
|
|
304
|
+
config.allowedAttributes.td.push('colspan', 'rowspan');
|
|
305
|
+
|
|
306
|
+
return config;
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
updateParserRules: function (parser) {
|
|
310
|
+
if (Markdown.config.checkboxes) {
|
|
311
|
+
// Add support for checkboxes
|
|
312
|
+
parser.use(require('markdown-it-checkbox'), {
|
|
313
|
+
divWrap: true,
|
|
314
|
+
divClass: 'plugin-markdown',
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (Markdown.config.multimdTables) {
|
|
319
|
+
parser.use(require('markdown-it-multimd-table'), {
|
|
320
|
+
multiline: true,
|
|
321
|
+
rowspan: true,
|
|
322
|
+
headerless: true,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
parser.use((md) => {
|
|
327
|
+
md.core.ruler.before('linkify', 'autodir', (state) => {
|
|
328
|
+
state.tokens.forEach((token) => {
|
|
329
|
+
if (token.type === 'paragraph_open') {
|
|
330
|
+
token.attrJoin('dir', 'auto');
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Update renderer to add some classes to all images
|
|
337
|
+
const renderImage = parser.renderer.rules.image || function (tokens, idx, options, env, self) {
|
|
338
|
+
return self.renderToken.apply(self, arguments);
|
|
339
|
+
};
|
|
340
|
+
const renderLink = parser.renderer.rules.link_open || function (tokens, idx, options, env, self) {
|
|
341
|
+
return self.renderToken.apply(self, arguments);
|
|
342
|
+
};
|
|
343
|
+
const renderTable = parser.renderer.rules.table_open || function (tokens, idx, options, env, self) {
|
|
344
|
+
return self.renderToken.apply(self, arguments);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
parser.renderer.rules.image = function (tokens, idx, options, env, self) {
|
|
348
|
+
const token = tokens[idx];
|
|
349
|
+
const attributes = new Map(token.attrs);
|
|
350
|
+
const parsedSrc = url.parse(attributes.get('src'));
|
|
351
|
+
|
|
352
|
+
// Validate the url
|
|
353
|
+
if (!Markdown.isUrlValid(attributes.get('src'))) { return ''; }
|
|
354
|
+
|
|
355
|
+
token.attrSet('class', (token.attrGet('class') || '') + ' img-responsive img-markdown');
|
|
356
|
+
|
|
357
|
+
// Append sizes to images
|
|
358
|
+
if (parsedSrc.pathname) {
|
|
359
|
+
const filename = path.basename(parsedSrc.pathname);
|
|
360
|
+
if (env.images && env.images.has(filename)) {
|
|
361
|
+
const size = env.images.get(filename);
|
|
362
|
+
token.attrSet('width', size.width);
|
|
363
|
+
token.attrSet('height', size.height);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return renderImage(tokens, idx, options, env, self);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
parser.renderer.rules.link_open = function (tokens, idx, options, env, self) {
|
|
371
|
+
// Add target="_blank" to all links
|
|
372
|
+
const targetIdx = tokens[idx].attrIndex('target');
|
|
373
|
+
let relIdx = tokens[idx].attrIndex('rel');
|
|
374
|
+
const hrefIdx = tokens[idx].attrIndex('href');
|
|
375
|
+
|
|
376
|
+
if (Markdown.isExternalLink(tokens[idx].attrs[hrefIdx][1])) {
|
|
377
|
+
if (Markdown.config.externalBlank) {
|
|
378
|
+
if (targetIdx < 0) {
|
|
379
|
+
tokens[idx].attrPush(['target', '_blank']);
|
|
380
|
+
} else {
|
|
381
|
+
tokens[idx].attrs[targetIdx][1] = '_blank';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (relIdx < 0) {
|
|
385
|
+
tokens[idx].attrPush(['rel', 'noopener noreferrer']);
|
|
386
|
+
relIdx = tokens[idx].attrIndex('rel');
|
|
387
|
+
} else {
|
|
388
|
+
tokens[idx].attrs[relIdx][1] = 'noopener noreferrer';
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (Markdown.config.nofollow) {
|
|
393
|
+
if (relIdx < 0) {
|
|
394
|
+
tokens[idx].attrPush(['rel', 'nofollow ugc']);
|
|
395
|
+
} else {
|
|
396
|
+
tokens[idx].attrs[relIdx][1] += ' nofollow ugc';
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (!Markdown.config.allowRTLO) {
|
|
402
|
+
if (tokens[idx + 1] && tokens[idx + 1].type === 'text') {
|
|
403
|
+
if (tokens[idx + 1].content.match(Markdown.regexes.rtl_override)) {
|
|
404
|
+
tokens[idx + 1].content = tokens[idx + 1].content.replace(Markdown.regexes.rtl_override, '');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return renderLink(tokens, idx, options, env, self);
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
parser.renderer.rules.table_open = function (tokens, idx, options, env, self) {
|
|
413
|
+
const classIdx = tokens[idx].attrIndex('class');
|
|
414
|
+
|
|
415
|
+
if (classIdx < 0) {
|
|
416
|
+
tokens[idx].attrPush(['class', 'table table-bordered table-striped']);
|
|
417
|
+
} else {
|
|
418
|
+
tokens[idx].attrs[classIdx][1] += ' table table-bordered table-striped';
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return renderTable(tokens, idx, options, env, self);
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
plugins.hooks.fire('action:markdown.updateParserRules', parser);
|
|
425
|
+
},
|
|
426
|
+
|
|
427
|
+
isUrlValid: function (src) {
|
|
428
|
+
/**
|
|
429
|
+
* Images linking to a relative path are only allowed from the root prefixes
|
|
430
|
+
* defined in allowedRoots. We allow both with and without relative_path
|
|
431
|
+
* even though upload_url should handle it, because sometimes installs
|
|
432
|
+
* migrate to (non-)subfolder and switch mid-way, but the uploads urls don't
|
|
433
|
+
* get updated.
|
|
434
|
+
*/
|
|
435
|
+
const allowedRoots = [nconf.get('upload_url'), '/uploads'];
|
|
436
|
+
const allowed = (pathname) => allowedRoots.some((root) => pathname.toString().startsWith(root) || pathname.toString().startsWith(nconf.get('relative_path') + root));
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
const urlObj = url.parse(src, false, true);
|
|
440
|
+
return !(urlObj.host === null && !allowed(urlObj.pathname));
|
|
441
|
+
} catch (e) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
isExternalLink: function (urlString) {
|
|
447
|
+
let urlObj;
|
|
448
|
+
let baseUrlObj;
|
|
449
|
+
try {
|
|
450
|
+
urlObj = url.parse(urlString);
|
|
451
|
+
baseUrlObj = url.parse(nconf.get('url'));
|
|
452
|
+
} catch (err) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (
|
|
457
|
+
urlObj.host === null // Relative paths are always internal links...
|
|
458
|
+
|| (urlObj.host === baseUrlObj.host && urlObj.protocol === baseUrlObj.protocol // Otherwise need to check that protocol and host match
|
|
459
|
+
&& (nconf.get('relative_path').length > 0 ? urlObj.pathname.indexOf(nconf.get('relative_path')) === 0 : true)) // Subfolder installs need this additional check
|
|
460
|
+
) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
return true;
|
|
464
|
+
},
|
|
465
|
+
|
|
466
|
+
admin: {
|
|
467
|
+
menu: async function (custom_header) {
|
|
468
|
+
custom_header.plugins.push({
|
|
469
|
+
route: '/plugins/markdown',
|
|
470
|
+
icon: 'fa-edit',
|
|
471
|
+
name: 'Markdown',
|
|
472
|
+
});
|
|
473
|
+
return custom_header;
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
|
|
477
|
+
regexes: {
|
|
478
|
+
rtl_override: /\u202E/gi,
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
module.exports = Markdown;
|