@waline/vercel 1.26.3 → 1.26.4

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.
Files changed (60) hide show
  1. package/dist/404.html +39 -0
  2. package/dist/500.html +275 -0
  3. package/dist/index.js +58501 -0
  4. package/dist/package.json +54 -0
  5. package/dist/src/config/adapter.js +170 -0
  6. package/dist/src/config/config.js +134 -0
  7. package/dist/src/config/extend.js +38 -0
  8. package/dist/src/config/middleware.js +66 -0
  9. package/dist/src/config/router.js +1 -0
  10. package/dist/src/controller/article.js +91 -0
  11. package/dist/src/controller/comment.js +758 -0
  12. package/dist/src/controller/db.js +71 -0
  13. package/dist/src/controller/index.js +36 -0
  14. package/dist/src/controller/oauth.js +136 -0
  15. package/dist/src/controller/rest.js +60 -0
  16. package/dist/src/controller/token/2fa.js +66 -0
  17. package/dist/src/controller/token.js +75 -0
  18. package/dist/src/controller/user/password.js +52 -0
  19. package/dist/src/controller/user.js +289 -0
  20. package/dist/src/controller/verification.js +35 -0
  21. package/dist/src/extend/controller.js +25 -0
  22. package/dist/src/extend/think.js +84 -0
  23. package/dist/src/locales/en.json +19 -0
  24. package/dist/src/locales/index.js +12 -0
  25. package/dist/src/locales/zh-CN.json +19 -0
  26. package/dist/src/locales/zh-TW.json +19 -0
  27. package/dist/src/logic/article.js +27 -0
  28. package/dist/src/logic/base.js +164 -0
  29. package/dist/src/logic/comment.js +317 -0
  30. package/dist/src/logic/db.js +81 -0
  31. package/dist/src/logic/oauth.js +10 -0
  32. package/dist/src/logic/token/2fa.js +28 -0
  33. package/dist/src/logic/token.js +53 -0
  34. package/dist/src/logic/user/password.js +11 -0
  35. package/dist/src/logic/user.js +117 -0
  36. package/dist/src/middleware/dashboard.js +23 -0
  37. package/dist/src/middleware/version.js +6 -0
  38. package/dist/src/service/akismet.js +41 -0
  39. package/dist/src/service/avatar.js +35 -0
  40. package/dist/src/service/markdown/highlight.js +32 -0
  41. package/dist/src/service/markdown/index.js +63 -0
  42. package/dist/src/service/markdown/katex.js +49 -0
  43. package/dist/src/service/markdown/mathCommon.js +156 -0
  44. package/dist/src/service/markdown/mathjax.js +78 -0
  45. package/dist/src/service/markdown/utils.js +11 -0
  46. package/dist/src/service/markdown/xss.js +44 -0
  47. package/dist/src/service/notify.js +537 -0
  48. package/dist/src/service/storage/base.js +31 -0
  49. package/dist/src/service/storage/cloudbase.js +221 -0
  50. package/dist/src/service/storage/deta.js +307 -0
  51. package/dist/src/service/storage/github.js +377 -0
  52. package/dist/src/service/storage/leancloud.js +430 -0
  53. package/dist/src/service/storage/mongodb.js +179 -0
  54. package/dist/src/service/storage/mysql.js +123 -0
  55. package/dist/src/service/storage/postgresql.js +84 -0
  56. package/dist/src/service/storage/sqlite.js +11 -0
  57. package/dist/src/service/storage/tidb.js +3 -0
  58. package/package.json +1 -1
  59. package/src/controller/comment.js +1 -2
  60. package/src/extend/think.js +19 -0
@@ -0,0 +1,117 @@
1
+ const Base = require('./base');
2
+
3
+ module.exports = class extends Base {
4
+ /**
5
+ * @api {GET} /user user top list without admin
6
+ * @apiGroup User
7
+ * @apiVersion 0.0.1
8
+ *
9
+ * @apiParam {String} pageSize page size
10
+ * @apiParam {String} lang language
11
+ *
12
+ * @apiSuccess (200) {Number} errno 0
13
+ * @apiSuccess (200) {String} errmsg return error message if error
14
+ * @apiSuccess (200) {Object[]} data user list
15
+ * @apiSuccess (200) {String} data.nick comment user nick name
16
+ * @apiSuccess (200) {String} data.link comment user link
17
+ * @apiSuccess (200) {String} data.avatar comment user avatar
18
+ * @apiSuccess (200) {String} data.level comment user level
19
+ * @apiSuccess (200) {String} data.count user comment count
20
+ */
21
+ /**
22
+ * @api {GET} /user?token user list with admin login
23
+ * @apiGroup User
24
+ * @apiVersion 0.0.1
25
+ *
26
+ * @apiParam {String} page page
27
+ * @apiParam {String} pageSize page size
28
+ * @apiParam {String} lang language
29
+ *
30
+ * @apiSuccess (200) {Number} errno 0
31
+ * @apiSuccess (200) {String} errmsg return error message if error
32
+ * @apiSuccess (200) {Object} data user list
33
+ * @apiSuccess (200) {Number} data.page user list current page
34
+ * @apiSuccess (200) {Number} data.pageSize user list page size
35
+ * @apiSuccess (200) {Number} data.totalPages user list total pages
36
+ * @apiSuccess (200) {Object[]} data.data user list data
37
+ * @apiSuccess (200) {String} data.data.nick comment user nick name
38
+ * @apiSuccess (200) {String} data.data.link comment user link
39
+ * @apiSuccess (200) {String} data.data.avatar comment user avatar
40
+ * @apiSuccess (200) {String} data.data.level comment user level
41
+ * @apiSuccess (200) {String} data.data.label comment user label
42
+ */
43
+ getAction() {
44
+ const { userInfo } = this.ctx.state;
45
+
46
+ if (think.isEmpty(userInfo) || userInfo.type !== 'administrator') {
47
+ this.rules = {
48
+ pageSize: {
49
+ int: { max: 50 },
50
+ default: 20,
51
+ },
52
+ };
53
+
54
+ return;
55
+ }
56
+
57
+ this.rules = {
58
+ page: {
59
+ int: true,
60
+ default: 1,
61
+ },
62
+ pageSize: {
63
+ int: { max: 100 },
64
+ default: 10,
65
+ },
66
+ email: {
67
+ string: true,
68
+ },
69
+ };
70
+ }
71
+
72
+ /**
73
+ * @api {POST} /user user register
74
+ * @apiGroup User
75
+ * @apiVersion 0.0.1
76
+ *
77
+ * @apiParam {String} display_name user nick name
78
+ * @apiParam {String} email user email
79
+ * @apiParam {String} password user password
80
+ * @apiParam {String} url user link
81
+ * @apiParam {String} lang language
82
+ *
83
+ * @apiSuccess (200) {Number} errno 0
84
+ * @apiSuccess (200) {String} errmsg return error message if error
85
+ */
86
+ postAction() {
87
+ return this.useCaptchaCheck();
88
+ }
89
+
90
+ /**
91
+ * @api {PUT} /user update user profile
92
+ * @apiGroup User
93
+ * @apiVersion 0.0.1
94
+ *
95
+ * @apiParam {String} [display_name] user new nick name
96
+ * @apiParam {String} [url] user new link
97
+ * @apiParam {String} [password] user new password
98
+ * @apiParam {String} [github] user github account name
99
+ * @apiParam {String} lang language
100
+ *
101
+ * @apiSuccess (200) {Number} errno 0
102
+ * @apiSuccess (200) {String} errmsg return error message if error
103
+ */
104
+ putAction() {
105
+ // you need login to update yourself profile
106
+ const { userInfo } = this.ctx.state;
107
+
108
+ if (think.isEmpty(userInfo)) {
109
+ return this.fail();
110
+ }
111
+
112
+ // you should be a administrator to update others info
113
+ if (this.id && userInfo.type !== 'administrator') {
114
+ return this.fail();
115
+ }
116
+ }
117
+ };
@@ -0,0 +1,23 @@
1
+ module.exports = function () {
2
+ return (ctx) => {
3
+ ctx.type = 'html';
4
+ ctx.body = `<!doctype html>
5
+ <html>
6
+ <head>
7
+ <meta charset="utf-8">
8
+ <title>Waline Management System</title>
9
+ <meta name="viewport" content="width=device-width,initial-scale=1">
10
+ </head>
11
+ <body>
12
+ <script>
13
+ window.SITE_URL = ${JSON.stringify(process.env.SITE_URL)};
14
+ window.SITE_NAME = ${JSON.stringify(process.env.SITE_NAME)};
15
+ window.recaptchaV3Key = ${JSON.stringify(process.env.RECAPTCHA_V3_KEY)};
16
+ </script>
17
+ <script src="${
18
+ process.env.WALINE_ADMIN_MODULE_ASSET_URL || '//unpkg.com/@waline/admin'
19
+ }"></script>
20
+ </body>
21
+ </html>`;
22
+ };
23
+ };
@@ -0,0 +1,6 @@
1
+ const pkg = require('../../package.json');
2
+
3
+ module.exports = () => async (ctx, next) => {
4
+ ctx.set('x-waline-version', pkg.version);
5
+ await next();
6
+ };
@@ -0,0 +1,41 @@
1
+ const Akismet = require('akismet');
2
+ const DEFAULT_KEY = '70542d86693e';
3
+
4
+ module.exports = function (comment, blog) {
5
+ let { AKISMET_KEY, SITE_URL } = process.env;
6
+
7
+ if (!AKISMET_KEY) {
8
+ AKISMET_KEY = DEFAULT_KEY;
9
+ }
10
+
11
+ if (AKISMET_KEY.toLowerCase() === 'false') {
12
+ return Promise.resolve(false);
13
+ }
14
+
15
+ return new Promise(function (resolve, reject) {
16
+ const akismet = Akismet.client({ blog, apiKey: AKISMET_KEY });
17
+
18
+ akismet.verifyKey(function (err, verifyKey) {
19
+ if (err) {
20
+ return reject(err);
21
+ } else if (!verifyKey) {
22
+ return reject(new Error('Akismet API_KEY verify failed!'));
23
+ }
24
+
25
+ akismet.checkComment(
26
+ {
27
+ user_ip: comment.ip,
28
+ permalink: SITE_URL + comment.url,
29
+ comment_author: comment.nick,
30
+ comment_content: comment.comment,
31
+ },
32
+ function (err, spam) {
33
+ if (err) {
34
+ return reject(err);
35
+ }
36
+ resolve(spam);
37
+ }
38
+ );
39
+ });
40
+ });
41
+ };
@@ -0,0 +1,35 @@
1
+ const nunjucks = require('nunjucks');
2
+ const helper = require('think-helper');
3
+ const { GRAVATAR_STR } = process.env;
4
+
5
+ const env = new nunjucks.Environment();
6
+
7
+ env.addFilter('md5', (str) => helper.md5(str));
8
+
9
+ const DEFAULT_GRAVATAR_STR = `{%- set numExp = r/^[0-9]+$/g -%}
10
+ {%- set qqMailExp = r/^[0-9]+@qq.com$/ig -%}
11
+ {%- if numExp.test(nick) -%}
12
+ https://q1.qlogo.cn/g?b=qq&nk={{nick}}&s=100
13
+ {%- elif qqMailExp.test(mail) -%}
14
+ https://q1.qlogo.cn/g?b=qq&nk={{mail|replace('@qq.com', '')}}&s=100
15
+ {%- else -%}
16
+ https://seccdn.libravatar.org/avatar/{{mail|md5}}
17
+ {%- endif -%}`;
18
+
19
+ module.exports = class extends think.Service {
20
+ async stringify(comment) {
21
+ const fn = think.config('avatarUrl');
22
+
23
+ if (think.isFunction(fn)) {
24
+ const ret = await fn(comment);
25
+
26
+ if (think.isString(ret) && ret) {
27
+ return ret;
28
+ }
29
+ }
30
+
31
+ const gravatarStr = GRAVATAR_STR || DEFAULT_GRAVATAR_STR;
32
+
33
+ return env.renderString(gravatarStr, comment);
34
+ }
35
+ };
@@ -0,0 +1,32 @@
1
+ const prism = require('prismjs');
2
+ const rawLoadLanguages = require('prismjs/components/index');
3
+
4
+ // prevent warning messages
5
+ rawLoadLanguages.silent = true;
6
+
7
+ const loadLanguages = (languages = []) => {
8
+ const langsToLoad = languages.filter((item) => !prism.languages[item]);
9
+
10
+ if (langsToLoad.length) {
11
+ rawLoadLanguages(langsToLoad);
12
+ }
13
+ };
14
+
15
+ /**
16
+ * Resolve syntax highlighter for corresponding language
17
+ */
18
+ const resolveHighlighter = (language) => {
19
+ // try to load languages
20
+ loadLanguages([language]);
21
+
22
+ // return null if current language could not be loaded
23
+ if (!prism.languages[language]) {
24
+ return null;
25
+ }
26
+
27
+ return (code) => prism.highlight(code, prism.languages[language], language);
28
+ };
29
+
30
+ module.exports = {
31
+ resolveHighlighter,
32
+ };
@@ -0,0 +1,63 @@
1
+ const MarkdownIt = require('markdown-it');
2
+ const emojiPlugin = require('markdown-it-emoji');
3
+ const subPlugin = require('markdown-it-sub');
4
+ const supPlugin = require('markdown-it-sup');
5
+ const { katexPlugin } = require('./katex');
6
+ const { mathjaxPlugin } = require('./mathjax');
7
+ const { resolveHighlighter } = require('./highlight');
8
+ const { sanitize } = require('./xss');
9
+
10
+ const getMarkdownParser = () => {
11
+ const { markdown = {} } = think.config();
12
+ const { config = {}, plugin = {} } = markdown;
13
+
14
+ // markdown-it instance
15
+ const markdownIt = MarkdownIt({
16
+ breaks: true,
17
+ linkify: true, // Auto convert URL-like text to links
18
+ typographer: true, // Enable some language-neutral replacement + quotes beautification
19
+
20
+ // default highlight
21
+ highlight: (code, lang) => {
22
+ const highlighter = resolveHighlighter(lang);
23
+
24
+ return highlighter ? highlighter(code) : '';
25
+ },
26
+
27
+ ...config,
28
+
29
+ // should always enable html option due to parsed emoji
30
+ html: true,
31
+ });
32
+
33
+ const { emoji, tex, mathjax, katex, sub, sup } = plugin;
34
+
35
+ // parse emoji
36
+ if (emoji !== false) {
37
+ markdownIt.use(emojiPlugin, typeof emoji === 'object' ? emoji : {});
38
+ }
39
+
40
+ // parse sub
41
+ if (sub !== false) {
42
+ markdownIt.use(subPlugin);
43
+ }
44
+
45
+ // parse sup
46
+ if (sup !== false) {
47
+ markdownIt.use(supPlugin);
48
+ }
49
+
50
+ // parse tex
51
+ if (tex === 'katex') {
52
+ markdownIt.use(katexPlugin, {
53
+ ...katex,
54
+ output: 'mathml',
55
+ });
56
+ } else if (tex !== false) {
57
+ markdownIt.use(mathjaxPlugin, mathjax);
58
+ }
59
+
60
+ return (content) => sanitize(markdownIt.render(content));
61
+ };
62
+
63
+ module.exports = { getMarkdownParser };
@@ -0,0 +1,49 @@
1
+ const katex = require('katex');
2
+ const { escapeHtml } = require('./utils');
3
+ const { inlineTex, blockTex } = require('./mathCommon');
4
+
5
+ // set KaTeX as the renderer for markdown-it-simplemath
6
+ const katexInline = (tex, options) => {
7
+ options.displayMode = false;
8
+ try {
9
+ return katex.renderToString(tex, options);
10
+ } catch (error) {
11
+ if (options.throwOnError) console.warn(error);
12
+
13
+ return `<span class='katex-error' title='${escapeHtml(
14
+ error.toString()
15
+ )}'>${escapeHtml(tex)}</span>`;
16
+ }
17
+ };
18
+
19
+ const katexBlock = (tex, options) => {
20
+ options.displayMode = true;
21
+ try {
22
+ return `<p class='katex-block'>${katex.renderToString(tex, options)}</p>`;
23
+ } catch (error) {
24
+ if (options.throwOnError) console.warn(error);
25
+
26
+ return `<p class='katex-block katex-error' title='${escapeHtml(
27
+ error.toString()
28
+ )}'>${escapeHtml(tex)}</p>`;
29
+ }
30
+ };
31
+
32
+ const katexPlugin = (md, options = { throwOnError: false }) => {
33
+ md.inline.ruler.after('escape', 'inlineTex', inlineTex);
34
+
35
+ // It’s a workaround here because types issue
36
+ md.block.ruler.after('blockquote', 'blockTex', blockTex, {
37
+ alt: ['paragraph', 'reference', 'blockquote', 'list'],
38
+ });
39
+
40
+ md.renderer.rules.inlineTex = (tokens, idx) =>
41
+ katexInline(tokens[idx].content, options);
42
+
43
+ md.renderer.rules.blockTex = (tokens, idx) =>
44
+ `${katexBlock(tokens[idx].content, options)}\n`;
45
+ };
46
+
47
+ module.exports = {
48
+ katexPlugin,
49
+ };
@@ -0,0 +1,156 @@
1
+ /*
2
+ * Test if potential opening or closing delimiter
3
+ * Assumes that there is a "$" at state.src[pos]
4
+ */
5
+ const isValidDelim = (state, pos) => {
6
+ const prevChar = pos > 0 ? state.src.charAt(pos - 1) : '';
7
+ const nextChar = pos + 1 <= state.posMax ? state.src.charAt(pos + 1) : '';
8
+
9
+ return {
10
+ canOpen: nextChar !== ' ' && nextChar !== '\t',
11
+ /*
12
+ * Check non-whitespace conditions for opening and closing, and
13
+ * check that closing delimiter isn’t followed by a number
14
+ */
15
+ canClose: !(
16
+ prevChar === ' ' ||
17
+ prevChar === '\t' ||
18
+ /[0-9]/u.exec(nextChar)
19
+ ),
20
+ };
21
+ };
22
+
23
+ const inlineTex = (state, silent) => {
24
+ let match;
25
+ let pos;
26
+ let res;
27
+ let token;
28
+
29
+ if (state.src[state.pos] !== '$') return false;
30
+ res = isValidDelim(state, state.pos);
31
+
32
+ if (!res.canOpen) {
33
+ if (!silent) state.pending += '$';
34
+ state.pos += 1;
35
+
36
+ return true;
37
+ }
38
+ /*
39
+ * First check for and bypass all properly escaped delimiters
40
+ * This loop will assume that the first leading backtick can not
41
+ * be the first character in state.src, which is known since
42
+ * we have found an opening delimiter already.
43
+ */
44
+ const start = state.pos + 1;
45
+
46
+ match = start;
47
+ while ((match = state.src.indexOf('$', match)) !== -1) {
48
+ /*
49
+ * Found potential $, look for escapes, pos will point to
50
+ * first non escape when complete
51
+ */
52
+ pos = match - 1;
53
+ while (state.src[pos] === '\\') pos -= 1;
54
+ // Even number of escapes, potential closing delimiter found
55
+ if ((match - pos) % 2 === 1) break;
56
+ match += 1;
57
+ }
58
+
59
+ // No closing delimiter found. Consume $ and continue.
60
+ if (match === -1) {
61
+ if (!silent) state.pending += '$';
62
+ state.pos = start;
63
+
64
+ return true;
65
+ }
66
+
67
+ // Check if we have empty content, ie: $$. Do not parse.
68
+ if (match - start === 0) {
69
+ if (!silent) state.pending += '$$';
70
+ state.pos = start + 1;
71
+
72
+ return true;
73
+ }
74
+
75
+ // Check for valid closing delimiter
76
+ res = isValidDelim(state, match);
77
+
78
+ if (!res.canClose) {
79
+ if (!silent) state.pending += '$';
80
+ state.pos = start;
81
+
82
+ return true;
83
+ }
84
+
85
+ if (!silent) {
86
+ token = state.push('inlineTex', 'math', 0);
87
+ token.markup = '$';
88
+ token.content = state.src.slice(start, match);
89
+ }
90
+
91
+ state.pos = match + 1;
92
+
93
+ return true;
94
+ };
95
+
96
+ const blockTex = (state, start, end, silent) => {
97
+ let firstLine;
98
+ let lastLine;
99
+ let next;
100
+ let lastPos;
101
+ let found = false;
102
+ let pos = state.bMarks[start] + state.tShift[start];
103
+ let max = state.eMarks[start];
104
+
105
+ if (pos + 2 > max) return false;
106
+ if (state.src.slice(pos, pos + 2) !== '$$') return false;
107
+ pos += 2;
108
+
109
+ firstLine = state.src.slice(pos, max);
110
+
111
+ if (silent) return true;
112
+
113
+ if (firstLine.trim().endsWith('$$')) {
114
+ // Single line expression
115
+ firstLine = firstLine.trim().slice(0, -2);
116
+ found = true;
117
+ }
118
+
119
+ for (next = start; !found; ) {
120
+ next += 1;
121
+ if (next >= end) break;
122
+ pos = state.bMarks[next] + state.tShift[next];
123
+ max = state.eMarks[next];
124
+ if (pos < max && state.tShift[next] < state.blkIndent)
125
+ // non-empty line with negative indent should stop the list:
126
+ break;
127
+ if (state.src.slice(pos, max).trim().endsWith('$$')) {
128
+ lastPos = state.src.slice(0, max).lastIndexOf('$$');
129
+ lastLine = state.src.slice(pos, lastPos);
130
+ found = true;
131
+ }
132
+ }
133
+
134
+ state.line = next + 1;
135
+
136
+ const token = state.push('blockTex', 'math', 0);
137
+
138
+ token.block = true;
139
+ token.content =
140
+ ((firstLine === null || firstLine === void 0 ? void 0 : firstLine.trim())
141
+ ? `${firstLine}\n`
142
+ : '') +
143
+ state.getLines(start + 1, next, state.tShift[start], true) +
144
+ ((lastLine === null || lastLine === void 0 ? void 0 : lastLine.trim())
145
+ ? lastLine
146
+ : '');
147
+ token.map = [start, state.line];
148
+ token.markup = '$$';
149
+
150
+ return true;
151
+ };
152
+
153
+ module.exports = {
154
+ inlineTex,
155
+ blockTex,
156
+ };
@@ -0,0 +1,78 @@
1
+ const { mathjax } = require('mathjax-full/js/mathjax');
2
+ const { TeX } = require('mathjax-full/js/input/tex.js');
3
+ const { SVG } = require('mathjax-full/js/output/svg.js');
4
+ const { liteAdaptor } = require('mathjax-full/js/adaptors/liteAdaptor.js');
5
+ const { RegisterHTMLHandler } = require('mathjax-full/js/handlers/html.js');
6
+ const { AllPackages } = require('mathjax-full/js/input/tex/AllPackages.js');
7
+
8
+ const { escapeHtml } = require('./utils');
9
+ const { inlineTex, blockTex } = require('./mathCommon');
10
+
11
+ // set MathJax as the renderer
12
+ class MathToSvg {
13
+ constructor() {
14
+ const adaptor = liteAdaptor();
15
+
16
+ RegisterHTMLHandler(adaptor);
17
+
18
+ const packages = AllPackages.sort();
19
+ const tex = new TeX({ packages });
20
+ const svg = new SVG({ fontCache: 'none' });
21
+
22
+ this.adaptor = adaptor;
23
+ this.texToNode = mathjax.document('', { InputJax: tex, OutputJax: svg });
24
+
25
+ this.inline = function (tex) {
26
+ const node = this.texToNode.convert(tex, { display: false });
27
+ let svg = this.adaptor.innerHTML(node);
28
+
29
+ if (svg.includes('data-mml-node="merror"')) {
30
+ const errorTitle = svg.match(/<title>(.*?)<\/title>/)[1];
31
+
32
+ svg = `<span class='mathjax-error' title='${escapeHtml(
33
+ errorTitle
34
+ )}'>${escapeHtml(tex)}</span>`;
35
+ }
36
+
37
+ return svg;
38
+ };
39
+
40
+ this.block = function (tex) {
41
+ const node = this.texToNode.convert(tex, { display: true });
42
+ let svg = this.adaptor.innerHTML(node);
43
+
44
+ if (svg.includes('data-mml-node="merror"')) {
45
+ const errorTitle = svg.match(/<title>(.*?)<\/title>/)[1];
46
+
47
+ svg = `<p class='mathjax-block mathjax-error' title='${escapeHtml(
48
+ errorTitle
49
+ )}'>${escapeHtml(tex)}</p>`;
50
+ } else {
51
+ svg = svg.replace(/(width=".*?")/, 'width="100%"');
52
+ }
53
+
54
+ return svg;
55
+ };
56
+ }
57
+ }
58
+
59
+ const mathjaxPlugin = (md) => {
60
+ const mathToSvg = new MathToSvg();
61
+
62
+ md.inline.ruler.after('escape', 'inlineTex', inlineTex);
63
+
64
+ // It’s a workaround here because types issue
65
+ md.block.ruler.after('blockquote', 'blockTex', blockTex, {
66
+ alt: ['paragraph', 'reference', 'blockquote', 'list'],
67
+ });
68
+
69
+ md.renderer.rules.inlineTex = (tokens, idx) =>
70
+ mathToSvg.inline(tokens[idx].content);
71
+
72
+ md.renderer.rules.blockTex = (tokens, idx) =>
73
+ `${mathToSvg.block(tokens[idx].content)}\n`;
74
+ };
75
+
76
+ module.exports = {
77
+ mathjaxPlugin,
78
+ };
@@ -0,0 +1,11 @@
1
+ const escapeHtml = (unsafeHTML) =>
2
+ unsafeHTML
3
+ .replace(/&/gu, '&amp;')
4
+ .replace(/</gu, '&lt;')
5
+ .replace(/>/gu, '&gt;')
6
+ .replace(/"/gu, '&quot;')
7
+ .replace(/'/gu, '&#039;');
8
+
9
+ module.exports = {
10
+ escapeHtml,
11
+ };
@@ -0,0 +1,44 @@
1
+ const createDOMPurify = require('dompurify');
2
+ const { JSDOM } = require('jsdom');
3
+
4
+ const DOMPurify = createDOMPurify(new JSDOM('').window);
5
+
6
+ /**
7
+ * Add a hook to make all links open a new window
8
+ * and force their rel to be 'noreferrer noopener'
9
+ */
10
+ DOMPurify.addHook('afterSanitizeAttributes', function (node) {
11
+ // set all elements owning target to target=_blank
12
+ if ('target' in node && node.href && !node.href.startsWith('about:blank#')) {
13
+ node.setAttribute('target', '_blank');
14
+ node.setAttribute('rel', 'noreferrer noopener');
15
+ }
16
+
17
+ // set non-HTML/MathML links to xlink:show=new
18
+ if (
19
+ !node.hasAttribute('target') &&
20
+ (node.hasAttribute('xlink:href') || node.hasAttribute('href'))
21
+ ) {
22
+ node.setAttribute('xlink:show', 'new');
23
+ }
24
+
25
+ if ('preload' in node) {
26
+ node.setAttribute('preload', 'none');
27
+ }
28
+ });
29
+
30
+ const sanitize = (content) =>
31
+ DOMPurify.sanitize(
32
+ content,
33
+ Object.assign(
34
+ {
35
+ FORBID_TAGS: ['form', 'input', 'style'],
36
+ FORBID_ATTR: ['autoplay', 'style'],
37
+ },
38
+ think.config('domPurify') || {}
39
+ )
40
+ );
41
+
42
+ module.exports = {
43
+ sanitize,
44
+ };