wikilint 0.10.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/LICENSE +674 -0
- package/README.md +17 -0
- package/config/.schema.json +127 -0
- package/config/default.json +831 -0
- package/config/llwiki.json +595 -0
- package/config/moegirl.json +685 -0
- package/config/zhwiki.json +803 -0
- package/index.js +76 -0
- package/lib/element.js +115 -0
- package/lib/node.js +226 -0
- package/lib/text.js +166 -0
- package/lib/title.js +56 -0
- package/mixin/hidden.js +18 -0
- package/package.json +51 -0
- package/parser/brackets.js +125 -0
- package/parser/commentAndExt.js +61 -0
- package/parser/converter.js +45 -0
- package/parser/externalLinks.js +32 -0
- package/parser/hrAndDoubleUnderscore.js +48 -0
- package/parser/html.js +41 -0
- package/parser/links.js +98 -0
- package/parser/list.js +58 -0
- package/parser/magicLinks.js +40 -0
- package/parser/quotes.js +63 -0
- package/parser/table.js +113 -0
- package/src/arg.js +93 -0
- package/src/atom/hidden.js +11 -0
- package/src/atom/index.js +26 -0
- package/src/attribute.js +284 -0
- package/src/attributes.js +147 -0
- package/src/converter.js +70 -0
- package/src/converterFlags.js +97 -0
- package/src/converterRule.js +74 -0
- package/src/extLink.js +60 -0
- package/src/gallery.js +94 -0
- package/src/hasNowiki/index.js +32 -0
- package/src/hasNowiki/pre.js +28 -0
- package/src/heading.js +83 -0
- package/src/html.js +130 -0
- package/src/imageParameter.js +141 -0
- package/src/imagemap.js +140 -0
- package/src/imagemapLink.js +29 -0
- package/src/index.js +406 -0
- package/src/link/category.js +13 -0
- package/src/link/file.js +132 -0
- package/src/link/galleryImage.js +62 -0
- package/src/link/index.js +119 -0
- package/src/magicLink.js +67 -0
- package/src/nested/choose.js +23 -0
- package/src/nested/combobox.js +22 -0
- package/src/nested/index.js +69 -0
- package/src/nested/references.js +22 -0
- package/src/nowiki/comment.js +47 -0
- package/src/nowiki/dd.js +13 -0
- package/src/nowiki/doubleUnderscore.js +26 -0
- package/src/nowiki/hr.js +22 -0
- package/src/nowiki/index.js +34 -0
- package/src/nowiki/list.js +13 -0
- package/src/nowiki/noinclude.js +14 -0
- package/src/nowiki/quote.js +51 -0
- package/src/onlyinclude.js +39 -0
- package/src/paramTag/index.js +66 -0
- package/src/paramTag/inputbox.js +32 -0
- package/src/parameter.js +96 -0
- package/src/syntax.js +23 -0
- package/src/table/index.js +45 -0
- package/src/table/td.js +118 -0
- package/src/table/tr.js +73 -0
- package/src/tagPair/ext.js +125 -0
- package/src/tagPair/include.js +26 -0
- package/src/tagPair/index.js +77 -0
- package/src/transclude.js +336 -0
- package/util/base.js +17 -0
- package/util/diff.js +76 -0
- package/util/lint.js +53 -0
- package/util/string.js +75 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const hidden = require('../../mixin/hidden'),
|
|
4
|
+
Parser = require('../..'),
|
|
5
|
+
TagPairToken = require('.');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `<includeonly>`或`<noinclude>`
|
|
9
|
+
* @classdesc `{childNodes: [AstText, AstText]}`
|
|
10
|
+
*/
|
|
11
|
+
class IncludeToken extends hidden(TagPairToken) {
|
|
12
|
+
type = 'include';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} name 标签名
|
|
16
|
+
* @param {string} attr 标签属性
|
|
17
|
+
* @param {string|undefined} inner 内部wikitext
|
|
18
|
+
* @param {string|undefined} closed 是否封闭
|
|
19
|
+
* @param {accum} accum
|
|
20
|
+
*/
|
|
21
|
+
constructor(name, attr = '', inner = undefined, closed = undefined, config = Parser.getConfig(), accum = []) {
|
|
22
|
+
super(name, attr, inner ?? '', inner === undefined ? closed : closed ?? '', config, accum);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = IncludeToken;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Parser = require('../..'),
|
|
4
|
+
Token = require('..');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 成对标签
|
|
8
|
+
* @classdesc `{childNodes: [AstText|AttributesToken, AstText|Token]}`
|
|
9
|
+
*/
|
|
10
|
+
class TagPairToken extends Token {
|
|
11
|
+
#selfClosing;
|
|
12
|
+
#closed;
|
|
13
|
+
#tags;
|
|
14
|
+
|
|
15
|
+
/** getter */
|
|
16
|
+
get closed() {
|
|
17
|
+
return this.#closed;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} name 标签名
|
|
22
|
+
* @param {string|Token} attr 标签属性
|
|
23
|
+
* @param {string|Token} inner 内部wikitext
|
|
24
|
+
* @param {string|undefined} closed 是否封闭;约定`undefined`表示自闭合,`''`表示未闭合
|
|
25
|
+
* @param {accum} accum
|
|
26
|
+
*/
|
|
27
|
+
constructor(name, attr, inner, closed, config = Parser.getConfig(), accum = []) {
|
|
28
|
+
super(undefined, config, true);
|
|
29
|
+
this.setAttribute('name', name.toLowerCase());
|
|
30
|
+
this.#tags = [name, closed || name];
|
|
31
|
+
this.#selfClosing = closed === undefined;
|
|
32
|
+
this.#closed = closed !== '';
|
|
33
|
+
this.append(attr, inner);
|
|
34
|
+
let index = accum.indexOf(attr);
|
|
35
|
+
if (index === -1) {
|
|
36
|
+
index = accum.indexOf(inner);
|
|
37
|
+
}
|
|
38
|
+
if (index === -1) {
|
|
39
|
+
index = Infinity;
|
|
40
|
+
}
|
|
41
|
+
accum.splice(index, 0, this);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @override
|
|
46
|
+
*/
|
|
47
|
+
toString(selector) {
|
|
48
|
+
const {firstChild, lastChild} = this,
|
|
49
|
+
[opening, closing] = this.#tags;
|
|
50
|
+
return this.#selfClosing
|
|
51
|
+
? `<${opening}${String(firstChild)}/>`
|
|
52
|
+
: `<${opening}${String(firstChild)}>${String(lastChild)}${this.#closed ? `</${closing}>` : ''}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @override
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
text() {
|
|
60
|
+
const [opening, closing] = this.#tags;
|
|
61
|
+
return this.#selfClosing
|
|
62
|
+
? `<${opening}${this.firstChild.text()}/>`
|
|
63
|
+
: `<${opening}${super.text('>')}${this.#closed ? `</${closing}>` : ''}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @override */
|
|
67
|
+
getPadding() {
|
|
68
|
+
return this.#tags[0].length + 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** @override */
|
|
72
|
+
getGaps() {
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = TagPairToken;
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {removeComment, text, decodeHtml} = require('../util/string'),
|
|
4
|
+
{generateForChild} = require('../util/lint'),
|
|
5
|
+
Parser = require('..'),
|
|
6
|
+
Token = require('.'),
|
|
7
|
+
ParameterToken = require('./parameter'),
|
|
8
|
+
AtomToken = require('./atom'),
|
|
9
|
+
SyntaxToken = require('./syntax');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 模板或魔术字
|
|
13
|
+
* @classdesc `{childNodes: [AtomToken|SyntaxToken, ...ParameterToken]}`
|
|
14
|
+
*/
|
|
15
|
+
class TranscludeToken extends Token {
|
|
16
|
+
type = 'template';
|
|
17
|
+
modifier = '';
|
|
18
|
+
/** @type {Record<string, Set<ParameterToken>>} */ #args = {};
|
|
19
|
+
#fragment;
|
|
20
|
+
#valid = true;
|
|
21
|
+
#raw = false;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 设置引用修饰符
|
|
25
|
+
* @param {string} modifier 引用修饰符
|
|
26
|
+
* @complexity `n`
|
|
27
|
+
*/
|
|
28
|
+
setModifier(modifier = '') {
|
|
29
|
+
const {parserFunction: [,, raw, subst]} = this.getAttribute('config'),
|
|
30
|
+
lcModifier = removeComment(modifier).trim();
|
|
31
|
+
if (modifier && !lcModifier.endsWith(':')) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const magicWord = lcModifier.slice(0, -1).toLowerCase(),
|
|
35
|
+
isRaw = raw.includes(magicWord),
|
|
36
|
+
isSubst = subst.includes(magicWord);
|
|
37
|
+
if (isRaw || isSubst || modifier === '') {
|
|
38
|
+
this.setAttribute('modifier', modifier);
|
|
39
|
+
this.#raw = isRaw;
|
|
40
|
+
return Boolean(modifier);
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {string} title 模板标题或魔术字
|
|
47
|
+
* @param {[string, string|undefined][]} parts 参数各部分
|
|
48
|
+
* @param {accum} accum
|
|
49
|
+
* @complexity `n`
|
|
50
|
+
* @throws `SyntaxError` 非法的模板名称
|
|
51
|
+
*/
|
|
52
|
+
constructor(title, parts, config = Parser.getConfig(), accum = []) {
|
|
53
|
+
super(undefined, config, true, accum, {
|
|
54
|
+
});
|
|
55
|
+
const {parserFunction: [insensitive, sensitive]} = config,
|
|
56
|
+
argSubst = /^(?:\s|\0\d+c\x7F)*\0\d+s\x7F/u.exec(title)?.[0];
|
|
57
|
+
if (argSubst) {
|
|
58
|
+
this.setAttribute('modifier', argSubst);
|
|
59
|
+
title = title.slice(argSubst.length);
|
|
60
|
+
} else if (title.includes(':')) {
|
|
61
|
+
const [modifier, ...arg] = title.split(':'),
|
|
62
|
+
[mt] = /^(?:\s|\0\d+c\x7F)*/u.exec(arg[0] ?? '');
|
|
63
|
+
if (this.setModifier(`${modifier}:${mt}`)) {
|
|
64
|
+
title = arg.join(':').slice(mt.length);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (title.includes(':') || parts.length === 0 && !this.#raw) {
|
|
68
|
+
const [magicWord, ...arg] = title.split(':'),
|
|
69
|
+
cleaned = removeComment(magicWord),
|
|
70
|
+
name = cleaned[arg.length > 0 ? 'trimStart' : 'trim'](),
|
|
71
|
+
isSensitive = sensitive.includes(name),
|
|
72
|
+
canonicalCame = insensitive[name.toLowerCase()];
|
|
73
|
+
if (isSensitive || canonicalCame) {
|
|
74
|
+
this.setAttribute('name', canonicalCame || name.toLowerCase()).type = 'magic-word';
|
|
75
|
+
const pattern = new RegExp(`^\\s*${name}\\s*$`, isSensitive ? 'u' : 'iu'),
|
|
76
|
+
token = new SyntaxToken(magicWord, pattern, 'magic-word-name', config, accum, {
|
|
77
|
+
});
|
|
78
|
+
this.insertAt(token);
|
|
79
|
+
if (arg.length > 0) {
|
|
80
|
+
parts.unshift([arg.join(':')]);
|
|
81
|
+
}
|
|
82
|
+
if (this.name === 'invoke') {
|
|
83
|
+
for (let i = 0; i < 2; i++) {
|
|
84
|
+
const part = parts.shift();
|
|
85
|
+
if (!part) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
const invoke = new AtomToken(part.join('='), `invoke-${
|
|
89
|
+
i ? 'function' : 'module'
|
|
90
|
+
}`, config, accum, {
|
|
91
|
+
});
|
|
92
|
+
this.insertAt(invoke);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (this.type === 'template') {
|
|
98
|
+
const name = removeComment(decodeHtml(title)).split('#')[0].trim();
|
|
99
|
+
if (!name || /\0\d+[eh!+-]\x7F|[<>[\]{}\n]|%[\da-f]{2}/u.test(name)) {
|
|
100
|
+
accum.pop();
|
|
101
|
+
throw new SyntaxError(`非法的模板名称:${name}`);
|
|
102
|
+
}
|
|
103
|
+
const token = new AtomToken(title, 'template-name', config, accum, {
|
|
104
|
+
});
|
|
105
|
+
this.insertAt(token);
|
|
106
|
+
}
|
|
107
|
+
const templateLike = this.isTemplate();
|
|
108
|
+
let i = 1;
|
|
109
|
+
for (let j = 0; j < parts.length; j++) {
|
|
110
|
+
const part = parts[j];
|
|
111
|
+
if (!templateLike && !(this.name === 'switch' && j > 0)) {
|
|
112
|
+
part[0] = part.join('=');
|
|
113
|
+
part.length = 1;
|
|
114
|
+
}
|
|
115
|
+
if (part.length === 1) {
|
|
116
|
+
part.unshift(i);
|
|
117
|
+
i++;
|
|
118
|
+
}
|
|
119
|
+
this.insertAt(new ParameterToken(...part, config, accum));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** @override */
|
|
124
|
+
afterBuild() {
|
|
125
|
+
if (this.modifier.includes('\0')) {
|
|
126
|
+
this.setAttribute('modifier', this.getAttribute('buildFromStr')(this.modifier, 'string'));
|
|
127
|
+
}
|
|
128
|
+
if (this.isTemplate()) {
|
|
129
|
+
const isTemplate = this.type === 'template',
|
|
130
|
+
titleObj = this.normalizeTitle(this.childNodes[isTemplate ? 0 : 1].text(), isTemplate ? 10 : 828);
|
|
131
|
+
this.#fragment = titleObj.fragment;
|
|
132
|
+
this.#valid = titleObj.valid;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @override
|
|
138
|
+
*/
|
|
139
|
+
toString(selector) {
|
|
140
|
+
const {childNodes, firstChild, modifier} = this;
|
|
141
|
+
return `{{${modifier}${
|
|
142
|
+
this.type === 'magic-word'
|
|
143
|
+
? `${String(firstChild)}${childNodes.length > 1 ? ':' : ''}${childNodes.slice(1).map(String).join('|')}`
|
|
144
|
+
: super.toString(selector, '|')
|
|
145
|
+
}}}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @override
|
|
150
|
+
* @returns {string}
|
|
151
|
+
* @complexity `n`
|
|
152
|
+
*/
|
|
153
|
+
text() {
|
|
154
|
+
const {childNodes, firstChild, modifier, type, name} = this;
|
|
155
|
+
return type === 'magic-word' && name === 'vardefine'
|
|
156
|
+
? ''
|
|
157
|
+
: `{{${modifier}${
|
|
158
|
+
this.type === 'magic-word'
|
|
159
|
+
? `${firstChild.text()}${childNodes.length > 1 ? ':' : ''}${text(childNodes.slice(1), '|')}`
|
|
160
|
+
: super.text('|')
|
|
161
|
+
}}}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** @override */
|
|
165
|
+
getPadding() {
|
|
166
|
+
return this.modifier.length + 2;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** @override */
|
|
170
|
+
getGaps() {
|
|
171
|
+
return 1;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @override
|
|
176
|
+
* @param {number} start 起始位置
|
|
177
|
+
*/
|
|
178
|
+
lint(start) {
|
|
179
|
+
const errors = super.lint(start),
|
|
180
|
+
{type, childNodes} = this;
|
|
181
|
+
let rect;
|
|
182
|
+
if (!this.isTemplate()) {
|
|
183
|
+
return errors;
|
|
184
|
+
} else if (this.#fragment !== undefined) {
|
|
185
|
+
rect = {start, ...this.getRootNode().posFromIndex(start)};
|
|
186
|
+
errors.push(generateForChild(childNodes[type === 'template' ? 0 : 1], rect, 'useless fragment'));
|
|
187
|
+
}
|
|
188
|
+
if (!this.#valid) {
|
|
189
|
+
rect = {start, ...this.getRootNode().posFromIndex(start)};
|
|
190
|
+
errors.push(generateForChild(childNodes[1], rect, 'illegal module name'));
|
|
191
|
+
}
|
|
192
|
+
const duplicatedArgs = this.getDuplicatedArgs();
|
|
193
|
+
if (duplicatedArgs.length > 0) {
|
|
194
|
+
rect ||= {start, ...this.getRootNode().posFromIndex(start)};
|
|
195
|
+
errors.push(...duplicatedArgs.flatMap(([, args]) => args).map(
|
|
196
|
+
arg => generateForChild(arg, rect, 'duplicated parameter'),
|
|
197
|
+
));
|
|
198
|
+
}
|
|
199
|
+
return errors;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 是否是模板 */
|
|
203
|
+
isTemplate() {
|
|
204
|
+
return this.type === 'template' || this.type === 'magic-word' && this.name === 'invoke';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 处理匿名参数更改
|
|
209
|
+
* @param {number|ParameterToken} addedToken 新增的参数
|
|
210
|
+
* @complexity `n`
|
|
211
|
+
*/
|
|
212
|
+
#handleAnonArgChange(addedToken) {
|
|
213
|
+
const args = this.getAnonArgs(),
|
|
214
|
+
j = args.indexOf(addedToken);
|
|
215
|
+
for (let i = j; i < args.length; i++) {
|
|
216
|
+
const token = args[i],
|
|
217
|
+
{name} = token,
|
|
218
|
+
newName = String(i + 1);
|
|
219
|
+
if (name !== newName) {
|
|
220
|
+
this.getArgs(newName, false, false).add(token.setAttribute('name', newName));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* @override
|
|
227
|
+
* @param {ParameterToken} token 待插入的子节点
|
|
228
|
+
* @param {number} i 插入位置
|
|
229
|
+
* @complexity `n`
|
|
230
|
+
*/
|
|
231
|
+
insertAt(token, i = this.length) {
|
|
232
|
+
super.insertAt(token, i);
|
|
233
|
+
if (token.anon) {
|
|
234
|
+
this.#handleAnonArgChange(token);
|
|
235
|
+
} else if (token.name) {
|
|
236
|
+
this.getArgs(token.name, false, false).add(token);
|
|
237
|
+
}
|
|
238
|
+
return token;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* 获取所有参数
|
|
243
|
+
* @returns {ParameterToken[]}
|
|
244
|
+
* @complexity `n`
|
|
245
|
+
*/
|
|
246
|
+
getAllArgs() {
|
|
247
|
+
return this.childNodes.filter(child => child instanceof ParameterToken);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 获取匿名参数
|
|
252
|
+
* @complexity `n`
|
|
253
|
+
*/
|
|
254
|
+
getAnonArgs() {
|
|
255
|
+
return this.getAllArgs().filter(({anon}) => anon);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 获取指定参数
|
|
260
|
+
* @param {string|number} key 参数名
|
|
261
|
+
* @param {boolean} exact 是否匹配匿名性
|
|
262
|
+
* @param {boolean} copy 是否返回一个备份
|
|
263
|
+
* @complexity `n`
|
|
264
|
+
*/
|
|
265
|
+
getArgs(key, exact, copy = true) {
|
|
266
|
+
const keyStr = String(key).replace(/^[ \t\n\0\v]+|(?<=[^ \t\n\0\v])[ \t\n\0\v]+$/gu, '');
|
|
267
|
+
let args;
|
|
268
|
+
if (Object.hasOwn(this.#args, keyStr)) {
|
|
269
|
+
args = this.#args[keyStr];
|
|
270
|
+
} else {
|
|
271
|
+
args = new Set(this.getAllArgs().filter(({name}) => keyStr === name));
|
|
272
|
+
this.#args[keyStr] = args;
|
|
273
|
+
}
|
|
274
|
+
return args;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 获取重名参数
|
|
279
|
+
* @complexity `n`
|
|
280
|
+
* @returns {[string, ParameterToken[]][]}
|
|
281
|
+
*/
|
|
282
|
+
getDuplicatedArgs() {
|
|
283
|
+
if (this.isTemplate()) {
|
|
284
|
+
return Object.entries(this.#args).filter(([, {size}]) => size > 1)
|
|
285
|
+
.map(([key, args]) => [key, [...args]]);
|
|
286
|
+
}
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 对特定魔术字获取可能的取值
|
|
292
|
+
* @this {ParameterToken}}
|
|
293
|
+
* @throws `Error` 不是可接受的魔术字
|
|
294
|
+
*/
|
|
295
|
+
getPossibleValues() {
|
|
296
|
+
const {type, name, childNodes, constructor: {name: cName}} = this;
|
|
297
|
+
if (type === 'template') {
|
|
298
|
+
throw new Error(`${cName}.getPossibleValues 方法仅供特定魔术字使用!`);
|
|
299
|
+
}
|
|
300
|
+
let start;
|
|
301
|
+
switch (name) {
|
|
302
|
+
case 'if':
|
|
303
|
+
case 'ifexist':
|
|
304
|
+
case 'ifexpr':
|
|
305
|
+
case 'iferror':
|
|
306
|
+
start = 2;
|
|
307
|
+
break;
|
|
308
|
+
case 'ifeq':
|
|
309
|
+
start = 3;
|
|
310
|
+
break;
|
|
311
|
+
default:
|
|
312
|
+
throw new Error(`${cName}.getPossibleValues 方法仅供特定魔术字使用!`);
|
|
313
|
+
}
|
|
314
|
+
const /** @type {Token[]} */ queue = childNodes.slice(start, start + 2).map(({childNodes: [, value]}) => value);
|
|
315
|
+
for (let i = 0; i < queue.length;) {
|
|
316
|
+
/** @type {Token[] & {0: TranscludeToken}} */
|
|
317
|
+
const {length, 0: first} = queue[i].childNodes.filter(child => child.text().trim());
|
|
318
|
+
if (length === 0) {
|
|
319
|
+
queue.splice(i, 1);
|
|
320
|
+
} else if (length > 1 || first.type !== 'magic-word') {
|
|
321
|
+
i++;
|
|
322
|
+
} else {
|
|
323
|
+
try {
|
|
324
|
+
const possibleValues = first.getPossibleValues();
|
|
325
|
+
queue.splice(i, 1, ...possibleValues);
|
|
326
|
+
i += possibleValues.length;
|
|
327
|
+
} catch {
|
|
328
|
+
i++;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return queue;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
module.exports = TranscludeToken;
|
package/util/base.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 是否是普通对象
|
|
5
|
+
* @param {*} obj 对象
|
|
6
|
+
*/
|
|
7
|
+
const isPlainObject = obj => Boolean(obj) && Object.getPrototypeOf(obj).constructor === Object;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 延时
|
|
11
|
+
* @param {number} t 秒数
|
|
12
|
+
*/
|
|
13
|
+
const sleep = t => new Promise(resolve => {
|
|
14
|
+
setTimeout(resolve, t * 1000);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
module.exports = {isPlainObject, sleep};
|
package/util/diff.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {spawn} = require('child_process'),
|
|
4
|
+
fs = require('fs/promises');
|
|
5
|
+
|
|
6
|
+
process.on('unhandledRejection', e => {
|
|
7
|
+
console.error(e);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 将shell命令转化为Promise对象
|
|
12
|
+
* @param {string} command shell指令
|
|
13
|
+
* @param {string[]} args shell输入参数
|
|
14
|
+
* @returns {Promise<?string>}
|
|
15
|
+
*/
|
|
16
|
+
const cmd = (command, args) => new Promise(resolve => {
|
|
17
|
+
let timer, shell;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 清除进程并返回
|
|
21
|
+
* @param {*} val 返回值
|
|
22
|
+
*/
|
|
23
|
+
const r = val => {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
shell.kill('SIGINT');
|
|
26
|
+
resolve(val);
|
|
27
|
+
};
|
|
28
|
+
try {
|
|
29
|
+
shell = spawn(command, args);
|
|
30
|
+
timer = setTimeout(() => {
|
|
31
|
+
shell.kill('SIGINT');
|
|
32
|
+
}, 60 * 1000);
|
|
33
|
+
let buf = '';
|
|
34
|
+
shell.stdout.on('data', data => {
|
|
35
|
+
buf += data.toString();
|
|
36
|
+
});
|
|
37
|
+
shell.stdout.on('end', () => {
|
|
38
|
+
r(buf);
|
|
39
|
+
});
|
|
40
|
+
shell.on('exit', () => {
|
|
41
|
+
r(shell.killed ? undefined : '');
|
|
42
|
+
});
|
|
43
|
+
shell.on('error', () => {
|
|
44
|
+
r(undefined);
|
|
45
|
+
});
|
|
46
|
+
} catch {
|
|
47
|
+
r(undefined);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 比较两个文件
|
|
53
|
+
* @param {string} oldStr 旧文本
|
|
54
|
+
* @param {string} newStr 新文本
|
|
55
|
+
* @param {string} uid 唯一标识
|
|
56
|
+
*/
|
|
57
|
+
const diff = async (oldStr, newStr, uid = '') => {
|
|
58
|
+
if (oldStr === newStr) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const oldFile = `diffOld${uid}`,
|
|
62
|
+
newFile = `diffNew${uid}`;
|
|
63
|
+
await Promise.all([fs.writeFile(oldFile, oldStr), fs.writeFile(newFile, newStr)]);
|
|
64
|
+
const stdout = await cmd('git', [
|
|
65
|
+
'diff',
|
|
66
|
+
'--color-words=[\xC0-\xFF][\x80-\xBF]+|<?/?\\w+/?>?|[^[:space:]]',
|
|
67
|
+
'-U0',
|
|
68
|
+
'--no-index',
|
|
69
|
+
oldFile,
|
|
70
|
+
newFile,
|
|
71
|
+
]);
|
|
72
|
+
await Promise.all([fs.unlink(oldFile), fs.unlink(newFile)]);
|
|
73
|
+
console.log(stdout?.split('\n')?.slice(4)?.join('\n'));
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
module.exports = diff;
|
package/util/lint.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Parser = require('..'),
|
|
4
|
+
Token = require('../src');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 生成对于子节点的LintError对象
|
|
8
|
+
* @param {Token} child 子节点
|
|
9
|
+
* @param {{top: number, left: number, start: number}} boundingRect 父节点的绝对定位
|
|
10
|
+
* @param {string} msg 错误信息
|
|
11
|
+
* @param {'error'|'warning'} severity 严重程度
|
|
12
|
+
* @returns {LintError}
|
|
13
|
+
*/
|
|
14
|
+
const generateForChild = (child, boundingRect, msg, severity = 'error') => {
|
|
15
|
+
const index = child.getRelativeIndex(),
|
|
16
|
+
{offsetHeight, offsetWidth, parentNode, length} = child,
|
|
17
|
+
{top: offsetTop, left: offsetLeft} = parentNode.posFromIndex(index),
|
|
18
|
+
{start} = boundingRect,
|
|
19
|
+
{top, left} = 'top' in boundingRect ? boundingRect : child.getRootNode().posFromIndex(start),
|
|
20
|
+
startIndex = start + index,
|
|
21
|
+
endIndex = startIndex + length,
|
|
22
|
+
startLine = top + offsetTop,
|
|
23
|
+
endLine = startLine + offsetHeight - 1,
|
|
24
|
+
startCol = offsetTop ? offsetLeft : left + offsetLeft,
|
|
25
|
+
endCol = offsetHeight > 1 ? offsetWidth : startCol + offsetWidth;
|
|
26
|
+
return {message: Parser.msg(msg), severity, startIndex, endIndex, startLine, endLine, startCol, endCol};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 生成对于自己的LintError对象
|
|
31
|
+
* @param {Token} token 节点
|
|
32
|
+
* @param {{top: number, left: number, start: number}} boundingRect 绝对定位
|
|
33
|
+
* @param {string} msg 错误信息
|
|
34
|
+
* @param {'error'|'warning'} severity 严重程度
|
|
35
|
+
* @returns {LintError}
|
|
36
|
+
*/
|
|
37
|
+
const generateForSelf = (token, boundingRect, msg, severity = 'error') => {
|
|
38
|
+
const {start} = boundingRect,
|
|
39
|
+
{offsetHeight, offsetWidth, length} = token,
|
|
40
|
+
{top, left} = 'top' in boundingRect ? boundingRect : token.getRootNode().posFromIndex(start);
|
|
41
|
+
return {
|
|
42
|
+
message: Parser.msg(msg),
|
|
43
|
+
severity,
|
|
44
|
+
startIndex: start,
|
|
45
|
+
endIndex: start + length,
|
|
46
|
+
startLine: top,
|
|
47
|
+
endLine: top + offsetHeight - 1,
|
|
48
|
+
startCol: left,
|
|
49
|
+
endCol: offsetHeight > 1 ? offsetWidth : left + offsetWidth,
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
module.exports = {generateForChild, generateForSelf};
|
package/util/string.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const extUrlCharFirst = '(?:\\[[\\da-f:.]+\\]|[^[\\]<>"\\0-\\x1F\\x7F\\p{Zs}\\uFFFD])',
|
|
4
|
+
extUrlChar = '(?:[^[\\]<>"\\0-\\x1F\\x7F\\p{Zs}\\uFFFD]|\\0\\d+c\\x7F)*';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* remove half-parsed comment-like tokens
|
|
8
|
+
* @param {string} str 原字符串
|
|
9
|
+
*/
|
|
10
|
+
const removeComment = str => str.replace(/\0\d+c\x7F/gu, '');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* escape special chars for RegExp constructor
|
|
14
|
+
* @param {string} str RegExp source
|
|
15
|
+
*/
|
|
16
|
+
const escapeRegExp = str => str.replace(/[\\{}()|.?*+^$[\]]/gu, '\\$&');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* a more sophisticated string-explode function
|
|
20
|
+
* @param {string} start start syntax of a nested AST node
|
|
21
|
+
* @param {string} end end syntax of a nested AST node
|
|
22
|
+
* @param {string} separator syntax for explosion
|
|
23
|
+
* @param {string} str string to be exploded
|
|
24
|
+
*/
|
|
25
|
+
const explode = (start, end, separator, str) => {
|
|
26
|
+
if (str === undefined) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
const regex = new RegExp(`${[start, end, separator].map(escapeRegExp).join('|')}`, 'gu'),
|
|
30
|
+
/** @type {string[]} */ exploded = [];
|
|
31
|
+
let mt = regex.exec(str),
|
|
32
|
+
depth = 0,
|
|
33
|
+
lastIndex = 0;
|
|
34
|
+
while (mt) {
|
|
35
|
+
const {0: match, index} = mt;
|
|
36
|
+
if (match !== separator) {
|
|
37
|
+
depth += match === start ? 1 : -1;
|
|
38
|
+
} else if (depth === 0) {
|
|
39
|
+
exploded.push(str.slice(lastIndex, index));
|
|
40
|
+
({lastIndex} = regex);
|
|
41
|
+
}
|
|
42
|
+
mt = regex.exec(str);
|
|
43
|
+
}
|
|
44
|
+
exploded.push(str.slice(lastIndex));
|
|
45
|
+
return exploded;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* extract effective wikitext
|
|
50
|
+
* @param {(string|AstNode)[]} childNodes a Token's contents
|
|
51
|
+
* @param {string} separator delimiter between nodes
|
|
52
|
+
*/
|
|
53
|
+
const text = (childNodes, separator = '') => {
|
|
54
|
+
const AstNode = require('../lib/node');
|
|
55
|
+
return childNodes.map(child => typeof child === 'string' ? child : child.text()).join(separator);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* decode HTML entities
|
|
60
|
+
* @param {string} str 原字符串
|
|
61
|
+
*/
|
|
62
|
+
const decodeHtml = str => str?.replace(
|
|
63
|
+
/&#(\d+|x[\da-f]+);/giu,
|
|
64
|
+
/** @param {string} code */ (_, code) => String.fromCodePoint(`${code[0].toLowerCase() === 'x' ? '0' : ''}${code}`),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
extUrlCharFirst,
|
|
69
|
+
extUrlChar,
|
|
70
|
+
removeComment,
|
|
71
|
+
escapeRegExp,
|
|
72
|
+
explode,
|
|
73
|
+
text,
|
|
74
|
+
decodeHtml,
|
|
75
|
+
};
|