@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.
- package/dist/404.html +39 -0
- package/dist/500.html +275 -0
- package/dist/index.js +58501 -0
- package/dist/package.json +54 -0
- package/dist/src/config/adapter.js +170 -0
- package/dist/src/config/config.js +134 -0
- package/dist/src/config/extend.js +38 -0
- package/dist/src/config/middleware.js +66 -0
- package/dist/src/config/router.js +1 -0
- package/dist/src/controller/article.js +91 -0
- package/dist/src/controller/comment.js +758 -0
- package/dist/src/controller/db.js +71 -0
- package/dist/src/controller/index.js +36 -0
- package/dist/src/controller/oauth.js +136 -0
- package/dist/src/controller/rest.js +60 -0
- package/dist/src/controller/token/2fa.js +66 -0
- package/dist/src/controller/token.js +75 -0
- package/dist/src/controller/user/password.js +52 -0
- package/dist/src/controller/user.js +289 -0
- package/dist/src/controller/verification.js +35 -0
- package/dist/src/extend/controller.js +25 -0
- package/dist/src/extend/think.js +84 -0
- package/dist/src/locales/en.json +19 -0
- package/dist/src/locales/index.js +12 -0
- package/dist/src/locales/zh-CN.json +19 -0
- package/dist/src/locales/zh-TW.json +19 -0
- package/dist/src/logic/article.js +27 -0
- package/dist/src/logic/base.js +164 -0
- package/dist/src/logic/comment.js +317 -0
- package/dist/src/logic/db.js +81 -0
- package/dist/src/logic/oauth.js +10 -0
- package/dist/src/logic/token/2fa.js +28 -0
- package/dist/src/logic/token.js +53 -0
- package/dist/src/logic/user/password.js +11 -0
- package/dist/src/logic/user.js +117 -0
- package/dist/src/middleware/dashboard.js +23 -0
- package/dist/src/middleware/version.js +6 -0
- package/dist/src/service/akismet.js +41 -0
- package/dist/src/service/avatar.js +35 -0
- package/dist/src/service/markdown/highlight.js +32 -0
- package/dist/src/service/markdown/index.js +63 -0
- package/dist/src/service/markdown/katex.js +49 -0
- package/dist/src/service/markdown/mathCommon.js +156 -0
- package/dist/src/service/markdown/mathjax.js +78 -0
- package/dist/src/service/markdown/utils.js +11 -0
- package/dist/src/service/markdown/xss.js +44 -0
- package/dist/src/service/notify.js +537 -0
- package/dist/src/service/storage/base.js +31 -0
- package/dist/src/service/storage/cloudbase.js +221 -0
- package/dist/src/service/storage/deta.js +307 -0
- package/dist/src/service/storage/github.js +377 -0
- package/dist/src/service/storage/leancloud.js +430 -0
- package/dist/src/service/storage/mongodb.js +179 -0
- package/dist/src/service/storage/mysql.js +123 -0
- package/dist/src/service/storage/postgresql.js +84 -0
- package/dist/src/service/storage/sqlite.js +11 -0
- package/dist/src/service/storage/tidb.js +3 -0
- package/package.json +1 -1
- package/src/controller/comment.js +1 -2
- 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,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,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
|
+
};
|