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