wikiparser-node 0.3.0 → 0.4.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/.eslintrc.json +472 -34
- package/README.md +1 -1
- package/config/default.json +58 -30
- package/config/llwiki.json +22 -90
- package/config/moegirl.json +51 -13
- package/config/zhwiki.json +1269 -0
- package/index.js +114 -104
- package/lib/element.js +448 -440
- package/lib/node.js +335 -115
- package/lib/ranges.js +27 -18
- package/lib/text.js +146 -0
- package/lib/title.js +13 -5
- package/mixin/attributeParent.js +70 -24
- package/mixin/fixedToken.js +14 -6
- package/mixin/hidden.js +6 -4
- package/mixin/sol.js +27 -10
- package/package.json +9 -3
- package/parser/brackets.js +22 -17
- package/parser/commentAndExt.js +18 -16
- package/parser/converter.js +14 -13
- package/parser/externalLinks.js +12 -11
- package/parser/hrAndDoubleUnderscore.js +23 -14
- package/parser/html.js +10 -9
- package/parser/links.js +15 -14
- package/parser/list.js +12 -11
- package/parser/magicLinks.js +12 -11
- package/parser/quotes.js +6 -5
- package/parser/selector.js +175 -0
- package/parser/table.js +25 -18
- package/printed/example.json +120 -0
- package/src/arg.js +56 -32
- package/src/atom/hidden.js +5 -2
- package/src/atom/index.js +17 -9
- package/src/attribute.js +182 -100
- package/src/converter.js +68 -41
- package/src/converterFlags.js +67 -45
- package/src/converterRule.js +117 -65
- package/src/extLink.js +66 -18
- package/src/gallery.js +42 -15
- package/src/heading.js +34 -15
- package/src/html.js +97 -35
- package/src/imageParameter.js +83 -54
- package/src/index.js +299 -178
- package/src/link/category.js +20 -52
- package/src/link/file.js +59 -28
- package/src/link/galleryImage.js +21 -7
- package/src/link/index.js +146 -60
- package/src/magicLink.js +34 -12
- package/src/nowiki/comment.js +22 -10
- package/src/nowiki/dd.js +37 -22
- package/src/nowiki/doubleUnderscore.js +16 -7
- package/src/nowiki/hr.js +11 -7
- package/src/nowiki/index.js +16 -9
- package/src/nowiki/list.js +2 -2
- package/src/nowiki/noinclude.js +8 -4
- package/src/nowiki/quote.js +11 -7
- package/src/onlyinclude.js +19 -7
- package/src/parameter.js +65 -38
- package/src/syntax.js +26 -20
- package/src/table/index.js +260 -165
- package/src/table/td.js +98 -52
- package/src/table/tr.js +102 -58
- package/src/tagPair/ext.js +27 -19
- package/src/tagPair/include.js +16 -11
- package/src/tagPair/index.js +64 -29
- package/src/transclude.js +170 -93
- package/test/api.js +83 -0
- package/test/real.js +133 -0
- package/test/test.js +28 -0
- package/test/util.js +80 -0
- package/tool/index.js +41 -31
- package/typings/api.d.ts +13 -0
- package/typings/array.d.ts +28 -0
- package/typings/event.d.ts +24 -0
- package/typings/index.d.ts +46 -4
- package/typings/node.d.ts +15 -9
- package/typings/parser.d.ts +7 -0
- package/typings/tool.d.ts +3 -2
- package/util/debug.js +21 -18
- package/util/string.js +40 -27
- package/typings/element.d.ts +0 -28
package/src/transclude.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const {removeComment, escapeRegExp, text, noWrap} = require('../util/string'),
|
|
4
4
|
{externalUse} = require('../util/debug'),
|
|
5
|
-
|
|
5
|
+
Parser = require('..'),
|
|
6
6
|
Token = require('.'),
|
|
7
7
|
ParameterToken = require('./parameter');
|
|
8
8
|
|
|
@@ -16,12 +16,16 @@ class TranscludeToken extends Token {
|
|
|
16
16
|
/** @type {Set<string>} */ #keys = new Set();
|
|
17
17
|
/** @type {Record<string, Set<ParameterToken>>} */ #args = {};
|
|
18
18
|
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* 设置引用修饰符
|
|
21
|
+
* @param {string} modifier 引用修饰符
|
|
22
|
+
* @complexity `n`
|
|
23
|
+
*/
|
|
20
24
|
setModifier(modifier = '') {
|
|
21
25
|
if (typeof modifier !== 'string') {
|
|
22
26
|
this.typeError('setModifier', 'String');
|
|
23
27
|
}
|
|
24
|
-
const [,, raw, subst] = this.getAttribute('config')
|
|
28
|
+
const {parserFunction: [,, raw, subst]} = this.getAttribute('config'),
|
|
25
29
|
lcModifier = modifier.trim().toLowerCase(),
|
|
26
30
|
isRaw = raw.includes(lcModifier),
|
|
27
31
|
isSubst = subst.includes(lcModifier),
|
|
@@ -36,16 +40,17 @@ class TranscludeToken extends Token {
|
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
/**
|
|
39
|
-
* @param {string} title
|
|
40
|
-
* @param {[string, string|undefined][]} parts
|
|
43
|
+
* @param {string} title 模板标题或魔术字
|
|
44
|
+
* @param {[string, string|undefined][]} parts 参数各部分
|
|
41
45
|
* @param {accum} accum
|
|
42
46
|
* @complexity `n`
|
|
47
|
+
* @throws `SyntaxError` 非法的模板名称
|
|
43
48
|
*/
|
|
44
49
|
constructor(title, parts, config = Parser.getConfig(), accum = []) {
|
|
45
50
|
super(undefined, config, true, accum, {AtomToken: 0, SyntaxToken: 0, ParameterToken: '1:'});
|
|
46
51
|
const AtomToken = require('./atom'),
|
|
47
|
-
SyntaxToken = require('./syntax')
|
|
48
|
-
|
|
52
|
+
SyntaxToken = require('./syntax');
|
|
53
|
+
const {parserFunction: [insensitive, sensitive, raw]} = config;
|
|
49
54
|
this.seal('modifier');
|
|
50
55
|
if (title.includes(':')) {
|
|
51
56
|
const [modifier, ...arg] = title.split(':');
|
|
@@ -58,13 +63,13 @@ class TranscludeToken extends Token {
|
|
|
58
63
|
name = removeComment(magicWord),
|
|
59
64
|
isSensitive = sensitive.includes(name);
|
|
60
65
|
if (isSensitive || insensitive.includes(name.toLowerCase())) {
|
|
61
|
-
this.setAttribute('name', name.toLowerCase().replace(
|
|
66
|
+
this.setAttribute('name', name.toLowerCase().replace(/^#/u, '')).type = 'magic-word';
|
|
62
67
|
const pattern = new RegExp(`^\\s*${name}\\s*$`, isSensitive ? '' : 'i'),
|
|
63
68
|
token = new SyntaxToken(magicWord, pattern, 'magic-word-name', config, accum, {
|
|
64
69
|
'Stage-1': ':', '!ExtToken': '',
|
|
65
70
|
});
|
|
66
71
|
this.appendChild(token);
|
|
67
|
-
if (arg.length) {
|
|
72
|
+
if (arg.length > 0) {
|
|
68
73
|
parts.unshift([arg.join(':')]);
|
|
69
74
|
}
|
|
70
75
|
if (this.name === 'invoke') {
|
|
@@ -79,13 +84,13 @@ class TranscludeToken extends Token {
|
|
|
79
84
|
}`, config, accum, {'Stage-1': ':', '!ExtToken': ''});
|
|
80
85
|
this.appendChild(invoke);
|
|
81
86
|
}
|
|
82
|
-
this.protectChildren('1:3');
|
|
87
|
+
this.getAttribute('protectChildren')('1:3');
|
|
83
88
|
}
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
if (this.type === 'template') {
|
|
87
92
|
const [name] = removeComment(title).split('#');
|
|
88
|
-
if (/\
|
|
93
|
+
if (/\0\d+[eh!+-]\x7F|[<>[\]{}]/u.test(name)) {
|
|
89
94
|
accum.pop();
|
|
90
95
|
throw new SyntaxError(`非法的模板名称:${name}`);
|
|
91
96
|
}
|
|
@@ -106,11 +111,12 @@ class TranscludeToken extends Token {
|
|
|
106
111
|
}
|
|
107
112
|
this.appendChild(new ParameterToken(...part, config, accum));
|
|
108
113
|
}
|
|
109
|
-
this.protectChildren(0);
|
|
114
|
+
this.getAttribute('protectChildren')(0);
|
|
110
115
|
}
|
|
111
116
|
|
|
117
|
+
/** @override */
|
|
112
118
|
cloneNode() {
|
|
113
|
-
const [first, ...cloned] = this.
|
|
119
|
+
const [first, ...cloned] = this.cloneChildNodes(),
|
|
114
120
|
config = this.getAttribute('config');
|
|
115
121
|
return Parser.run(() => {
|
|
116
122
|
const token = new TranscludeToken(this.type === 'template' ? '' : first.text(), [], config);
|
|
@@ -122,12 +128,12 @@ class TranscludeToken extends Token {
|
|
|
122
128
|
});
|
|
123
129
|
}
|
|
124
130
|
|
|
131
|
+
/** @override */
|
|
125
132
|
afterBuild() {
|
|
126
|
-
if (this.name.includes('\
|
|
127
|
-
this.setAttribute('name', text(this.buildFromStr(this.name)));
|
|
133
|
+
if (this.name.includes('\0')) {
|
|
134
|
+
this.setAttribute('name', text(this.getAttribute('buildFromStr')(this.name)));
|
|
128
135
|
}
|
|
129
136
|
if (this.matches('template, magic-word#invoke')) {
|
|
130
|
-
const that = this;
|
|
131
137
|
/**
|
|
132
138
|
* 当事件bubble到`parameter`时,将`oldKey`和`newKey`保存进AstEventData。
|
|
133
139
|
* 当继续bubble到`template`时,处理并删除`oldKey`和`newKey`。
|
|
@@ -140,15 +146,15 @@ class TranscludeToken extends Token {
|
|
|
140
146
|
delete data.oldKey;
|
|
141
147
|
delete data.newKey;
|
|
142
148
|
}
|
|
143
|
-
if (prevTarget ===
|
|
144
|
-
|
|
149
|
+
if (prevTarget === this.firstElementChild && this.type === 'template') {
|
|
150
|
+
this.setAttribute('name', this.normalizeTitle(prevTarget.text(), 10).title);
|
|
145
151
|
} else if (oldKey !== newKey && prevTarget instanceof ParameterToken) {
|
|
146
|
-
const oldArgs =
|
|
152
|
+
const oldArgs = this.getArgs(oldKey, false, false);
|
|
147
153
|
oldArgs.delete(prevTarget);
|
|
148
|
-
|
|
149
|
-
|
|
154
|
+
this.getArgs(newKey, false, false).add(prevTarget);
|
|
155
|
+
this.#keys.add(newKey);
|
|
150
156
|
if (oldArgs.size === 0) {
|
|
151
|
-
|
|
157
|
+
this.#keys.delete(oldKey);
|
|
152
158
|
}
|
|
153
159
|
}
|
|
154
160
|
};
|
|
@@ -157,17 +163,20 @@ class TranscludeToken extends Token {
|
|
|
157
163
|
return this;
|
|
158
164
|
}
|
|
159
165
|
|
|
166
|
+
/** 替换引用 */
|
|
160
167
|
subst() {
|
|
161
168
|
this.setModifier('subst');
|
|
162
169
|
}
|
|
163
170
|
|
|
171
|
+
/** 安全的替换引用 */
|
|
164
172
|
safesubst() {
|
|
165
173
|
this.setModifier('safesubst');
|
|
166
174
|
}
|
|
167
175
|
|
|
168
176
|
/**
|
|
177
|
+
* @override
|
|
169
178
|
* @template {string} T
|
|
170
|
-
* @param {T} key
|
|
179
|
+
* @param {T} key 属性键
|
|
171
180
|
* @returns {TokenAttribute<T>}
|
|
172
181
|
*/
|
|
173
182
|
getAttribute(key) {
|
|
@@ -179,30 +188,40 @@ class TranscludeToken extends Token {
|
|
|
179
188
|
return super.getAttribute(key);
|
|
180
189
|
}
|
|
181
190
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
191
|
+
/**
|
|
192
|
+
* @override
|
|
193
|
+
* @param {string} selector
|
|
194
|
+
*/
|
|
195
|
+
toString(selector) {
|
|
196
|
+
if (selector && this.matches(selector)) {
|
|
197
|
+
return '';
|
|
198
|
+
}
|
|
199
|
+
const {children, childNodes: {length}, firstChild, modifier} = this;
|
|
200
|
+
return `{{${modifier}${modifier && ':'}${
|
|
185
201
|
this.type === 'magic-word'
|
|
186
202
|
? `${String(firstChild)}${length > 1 ? ':' : ''}${children.slice(1).map(String).join('|')}`
|
|
187
|
-
: super.toString('|')
|
|
203
|
+
: super.toString(selector, '|')
|
|
188
204
|
}}}`;
|
|
189
205
|
}
|
|
190
206
|
|
|
207
|
+
/** @override */
|
|
191
208
|
getPadding() {
|
|
192
209
|
return this.modifier ? this.modifier.length + 3 : 2;
|
|
193
210
|
}
|
|
194
211
|
|
|
212
|
+
/** @override */
|
|
195
213
|
getGaps() {
|
|
196
214
|
return 1;
|
|
197
215
|
}
|
|
198
216
|
|
|
199
217
|
/**
|
|
218
|
+
* @override
|
|
200
219
|
* @returns {string}
|
|
201
220
|
* @complexity `n`
|
|
202
221
|
*/
|
|
203
222
|
text() {
|
|
204
|
-
const {children, childNodes: {length}, firstElementChild} = this;
|
|
205
|
-
return `{{${
|
|
223
|
+
const {children, childNodes: {length}, firstElementChild, modifier} = this;
|
|
224
|
+
return `{{${modifier}${modifier && ':'}${
|
|
206
225
|
this.type === 'magic-word'
|
|
207
226
|
? `${firstElementChild.text()}${length > 1 ? ':' : ''}${text(children.slice(1), '|')}`
|
|
208
227
|
: super.text('|')
|
|
@@ -210,7 +229,8 @@ class TranscludeToken extends Token {
|
|
|
210
229
|
}
|
|
211
230
|
|
|
212
231
|
/**
|
|
213
|
-
*
|
|
232
|
+
* 处理匿名参数更改
|
|
233
|
+
* @param {number|ParameterToken} addedToken 新增的参数
|
|
214
234
|
* @complexity `n`
|
|
215
235
|
*/
|
|
216
236
|
#handleAnonArgChange(addedToken) {
|
|
@@ -223,8 +243,9 @@ class TranscludeToken extends Token {
|
|
|
223
243
|
this.#keys.delete(maxAnon);
|
|
224
244
|
}
|
|
225
245
|
const j = added ? args.indexOf(addedToken) : addedToken - 1;
|
|
226
|
-
for (
|
|
227
|
-
const
|
|
246
|
+
for (let i = j; i < args.length; i++) {
|
|
247
|
+
const token = args[i],
|
|
248
|
+
{name} = token,
|
|
228
249
|
newName = String(i + 1);
|
|
229
250
|
if (name !== newName) {
|
|
230
251
|
this.getArgs(newName, false, false).add(token.setAttribute('name', newName));
|
|
@@ -236,7 +257,8 @@ class TranscludeToken extends Token {
|
|
|
236
257
|
}
|
|
237
258
|
|
|
238
259
|
/**
|
|
239
|
-
* @
|
|
260
|
+
* @override
|
|
261
|
+
* @param {number} i 移除位置
|
|
240
262
|
* @complexity `n`
|
|
241
263
|
*/
|
|
242
264
|
removeAt(i) {
|
|
@@ -254,7 +276,9 @@ class TranscludeToken extends Token {
|
|
|
254
276
|
}
|
|
255
277
|
|
|
256
278
|
/**
|
|
257
|
-
* @
|
|
279
|
+
* @override
|
|
280
|
+
* @param {ParameterToken} token 待插入的子节点
|
|
281
|
+
* @param {number} i 插入位置
|
|
258
282
|
* @complexity `n`
|
|
259
283
|
*/
|
|
260
284
|
insertAt(token, i = this.childNodes.length) {
|
|
@@ -269,6 +293,7 @@ class TranscludeToken extends Token {
|
|
|
269
293
|
}
|
|
270
294
|
|
|
271
295
|
/**
|
|
296
|
+
* 获取所有参数
|
|
272
297
|
* @returns {ParameterToken[]}
|
|
273
298
|
* @complexity `n`
|
|
274
299
|
*/
|
|
@@ -276,21 +301,26 @@ class TranscludeToken extends Token {
|
|
|
276
301
|
return this.children.filter(child => child instanceof ParameterToken);
|
|
277
302
|
}
|
|
278
303
|
|
|
279
|
-
/**
|
|
304
|
+
/**
|
|
305
|
+
* 获取匿名参数
|
|
306
|
+
* @complexity `n`
|
|
307
|
+
*/
|
|
280
308
|
getAnonArgs() {
|
|
281
309
|
return this.getAllArgs().filter(({anon}) => anon);
|
|
282
310
|
}
|
|
283
311
|
|
|
284
312
|
/**
|
|
285
|
-
*
|
|
313
|
+
* 获取指定参数
|
|
314
|
+
* @param {string|number} key 参数名
|
|
315
|
+
* @param {boolean} exact 是否匹配匿名性
|
|
316
|
+
* @param {boolean} copy 是否返回一个备份
|
|
286
317
|
* @complexity `n`
|
|
287
318
|
*/
|
|
288
|
-
getArgs(key, exact
|
|
289
|
-
if (
|
|
319
|
+
getArgs(key, exact, copy = true) {
|
|
320
|
+
if (typeof key !== 'string' && typeof key !== 'number') {
|
|
290
321
|
this.typeError('getArgs', 'String', 'Number');
|
|
291
|
-
} else if (!copy && !Parser.debugging && externalUse('getArgs')) {
|
|
292
|
-
this.debugOnly('getArgs');
|
|
293
322
|
}
|
|
323
|
+
copy ||= !Parser.debugging && externalUse('getArgs');
|
|
294
324
|
const keyStr = String(key).trim();
|
|
295
325
|
let args = this.#args[keyStr];
|
|
296
326
|
if (!args) {
|
|
@@ -306,26 +336,32 @@ class TranscludeToken extends Token {
|
|
|
306
336
|
}
|
|
307
337
|
|
|
308
338
|
/**
|
|
309
|
-
*
|
|
339
|
+
* 是否具有某参数
|
|
340
|
+
* @param {string|number} key 参数名
|
|
341
|
+
* @param {boolean} exact 是否匹配匿名性
|
|
310
342
|
* @complexity `n`
|
|
311
343
|
*/
|
|
312
|
-
hasArg(key, exact
|
|
344
|
+
hasArg(key, exact) {
|
|
313
345
|
return this.getArgs(key, exact, false).size > 0;
|
|
314
346
|
}
|
|
315
347
|
|
|
316
348
|
/**
|
|
317
|
-
*
|
|
349
|
+
* 获取生效的指定参数
|
|
350
|
+
* @param {string|number} key 参数名
|
|
351
|
+
* @param {boolean} exact 是否匹配匿名性
|
|
318
352
|
* @complexity `n`
|
|
319
353
|
*/
|
|
320
|
-
getArg(key, exact
|
|
321
|
-
return [...this.getArgs(key, exact, false)].sort((a, b) => a.
|
|
354
|
+
getArg(key, exact) {
|
|
355
|
+
return [...this.getArgs(key, exact, false)].sort((a, b) => a.compareDocumentPosition(b)).at(-1);
|
|
322
356
|
}
|
|
323
357
|
|
|
324
358
|
/**
|
|
325
|
-
*
|
|
359
|
+
* 移除指定参数
|
|
360
|
+
* @param {string|number} key 参数名
|
|
361
|
+
* @param {boolean} exact 是否匹配匿名性
|
|
326
362
|
* @complexity `n`
|
|
327
363
|
*/
|
|
328
|
-
removeArg(key, exact
|
|
364
|
+
removeArg(key, exact) {
|
|
329
365
|
Parser.run(() => {
|
|
330
366
|
for (const token of this.getArgs(key, exact, false)) {
|
|
331
367
|
this.removeChild(token);
|
|
@@ -333,10 +369,13 @@ class TranscludeToken extends Token {
|
|
|
333
369
|
});
|
|
334
370
|
}
|
|
335
371
|
|
|
336
|
-
/**
|
|
372
|
+
/**
|
|
373
|
+
* 获取所有参数名
|
|
374
|
+
* @complexity `n`
|
|
375
|
+
*/
|
|
337
376
|
getKeys() {
|
|
338
377
|
const args = this.getAllArgs();
|
|
339
|
-
if (this.#keys.size === 0 && args.length) {
|
|
378
|
+
if (this.#keys.size === 0 && args.length > 0) {
|
|
340
379
|
for (const {name} of args) {
|
|
341
380
|
this.#keys.add(name);
|
|
342
381
|
}
|
|
@@ -345,7 +384,8 @@ class TranscludeToken extends Token {
|
|
|
345
384
|
}
|
|
346
385
|
|
|
347
386
|
/**
|
|
348
|
-
*
|
|
387
|
+
* 获取参数值
|
|
388
|
+
* @param {string|number} key 参数名
|
|
349
389
|
* @complexity `n`
|
|
350
390
|
*/
|
|
351
391
|
getValues(key) {
|
|
@@ -353,22 +393,24 @@ class TranscludeToken extends Token {
|
|
|
353
393
|
}
|
|
354
394
|
|
|
355
395
|
/**
|
|
396
|
+
* 获取生效的参数值
|
|
356
397
|
* @template {string|number|undefined} T
|
|
357
|
-
* @param {T} key
|
|
398
|
+
* @param {T} key 参数名
|
|
358
399
|
* @returns {T extends undefined ? Object<string, string> : string}
|
|
359
400
|
* @complexity `n`
|
|
360
401
|
*/
|
|
361
402
|
getValue(key) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
return Object.fromEntries(this.getKeys().map(k => [k, this.getValue(k)]));
|
|
403
|
+
return key === undefined
|
|
404
|
+
? Object.fromEntries(this.getKeys().map(k => [k, this.getValue(k)]))
|
|
405
|
+
: this.getArg(key)?.getValue();
|
|
366
406
|
}
|
|
367
407
|
|
|
368
408
|
/**
|
|
369
|
-
*
|
|
409
|
+
* 插入匿名参数
|
|
410
|
+
* @param {string} val 参数值
|
|
370
411
|
* @returns {ParameterToken}
|
|
371
412
|
* @complexity `n`
|
|
413
|
+
* @throws `SyntaxError` 非法的匿名参数
|
|
372
414
|
*/
|
|
373
415
|
newAnonArg(val) {
|
|
374
416
|
val = String(val);
|
|
@@ -376,18 +418,21 @@ class TranscludeToken extends Token {
|
|
|
376
418
|
wikitext = `{{${templateLike ? ':T|' : 'lc:'}${val}}}`,
|
|
377
419
|
root = Parser.parse(wikitext, this.getAttribute('include'), 2, this.getAttribute('config')),
|
|
378
420
|
{childNodes: {length}, firstElementChild} = root;
|
|
379
|
-
if (length
|
|
380
|
-
|
|
421
|
+
if (length === 1 && firstElementChild?.matches(templateLike ? 'template#T' : 'magic-word#lc')
|
|
422
|
+
&& firstElementChild.childNodes.length === 2 && firstElementChild.lastElementChild.anon
|
|
381
423
|
) {
|
|
382
|
-
|
|
424
|
+
return this.appendChild(firstElementChild.lastChild);
|
|
383
425
|
}
|
|
384
|
-
|
|
426
|
+
throw new SyntaxError(`非法的匿名参数:${noWrap(val)}`);
|
|
385
427
|
}
|
|
386
428
|
|
|
387
429
|
/**
|
|
388
|
-
*
|
|
389
|
-
* @param {string}
|
|
430
|
+
* 设置参数值
|
|
431
|
+
* @param {string} key 参数名
|
|
432
|
+
* @param {string} value 参数值
|
|
390
433
|
* @complexity `n`
|
|
434
|
+
* @throws `Error` 仅用于模板
|
|
435
|
+
* @throws `SyntaxError` 非法的命名参数
|
|
391
436
|
*/
|
|
392
437
|
setValue(key, value) {
|
|
393
438
|
if (typeof key !== 'string') {
|
|
@@ -412,18 +457,26 @@ class TranscludeToken extends Token {
|
|
|
412
457
|
this.appendChild(firstElementChild.lastChild);
|
|
413
458
|
}
|
|
414
459
|
|
|
415
|
-
/**
|
|
460
|
+
/**
|
|
461
|
+
* 将匿名参数改写为命名参数
|
|
462
|
+
* @complexity `n`
|
|
463
|
+
* @throws `Error` 仅用于模板
|
|
464
|
+
*/
|
|
416
465
|
anonToNamed() {
|
|
417
466
|
if (!this.matches('template, magic-word#invoke')) {
|
|
418
467
|
throw new Error(`${this.constructor.name}.anonToNamed 方法仅供模板使用!`);
|
|
419
468
|
}
|
|
420
469
|
for (const token of this.getAnonArgs()) {
|
|
421
|
-
token.anon = false;
|
|
422
470
|
token.firstElementChild.replaceChildren(token.name);
|
|
423
471
|
}
|
|
424
472
|
}
|
|
425
473
|
|
|
426
|
-
/**
|
|
474
|
+
/**
|
|
475
|
+
* 替换模板名
|
|
476
|
+
* @param {string} title 模板名
|
|
477
|
+
* @throws `Error` 仅用于模板
|
|
478
|
+
* @throws `SyntaxError` 非法的模板名称
|
|
479
|
+
*/
|
|
427
480
|
replaceTemplate(title) {
|
|
428
481
|
if (this.type === 'magic-word') {
|
|
429
482
|
throw new Error(`${this.constructor.name}.replaceTemplate 方法仅用于更换模板!`);
|
|
@@ -438,7 +491,12 @@ class TranscludeToken extends Token {
|
|
|
438
491
|
this.firstElementChild.replaceChildren(...firstElementChild.firstElementChild.childNodes);
|
|
439
492
|
}
|
|
440
493
|
|
|
441
|
-
/**
|
|
494
|
+
/**
|
|
495
|
+
* 替换模块名
|
|
496
|
+
* @param {string} title 模块名
|
|
497
|
+
* @throws `Error` 仅用于模块
|
|
498
|
+
* @throws `SyntaxError` 非法的模块名称
|
|
499
|
+
*/
|
|
442
500
|
replaceModule(title) {
|
|
443
501
|
if (this.type !== 'magic-word' || this.name !== 'invoke') {
|
|
444
502
|
throw new Error(`${this.constructor.name}.replaceModule 方法仅用于更换模块!`);
|
|
@@ -455,13 +513,18 @@ class TranscludeToken extends Token {
|
|
|
455
513
|
this.children[1].replaceChildren(...firstElementChild.lastElementChild.childNodes);
|
|
456
514
|
} else {
|
|
457
515
|
const {lastChild} = firstElementChild;
|
|
458
|
-
|
|
459
|
-
firstElementChild.destroy();
|
|
516
|
+
firstElementChild.destroy(true);
|
|
460
517
|
this.appendChild(lastChild);
|
|
461
518
|
}
|
|
462
519
|
}
|
|
463
520
|
|
|
464
|
-
/**
|
|
521
|
+
/**
|
|
522
|
+
* 替换模块函数
|
|
523
|
+
* @param {string} func 模块函数名
|
|
524
|
+
* @throws `Error` 仅用于模块
|
|
525
|
+
* @throws `Error` 尚未指定模块名称
|
|
526
|
+
* @throws `SyntaxError` 非法的模块函数名
|
|
527
|
+
*/
|
|
465
528
|
replaceFunction(func) {
|
|
466
529
|
if (this.type !== 'magic-word' || this.name !== 'invoke') {
|
|
467
530
|
throw new Error(`${this.constructor.name}.replaceModule 方法仅用于更换模块!`);
|
|
@@ -470,7 +533,9 @@ class TranscludeToken extends Token {
|
|
|
470
533
|
} else if (this.childNodes.length < 2) {
|
|
471
534
|
throw new Error('尚未指定模块名称!');
|
|
472
535
|
}
|
|
473
|
-
const root = Parser.parse(
|
|
536
|
+
const root = Parser.parse(
|
|
537
|
+
`{{#invoke:M|${func}}}`, this.getAttribute('include'), 2, this.getAttribute('config'),
|
|
538
|
+
),
|
|
474
539
|
{childNodes: {length}, firstElementChild} = root;
|
|
475
540
|
if (length !== 1 || !firstElementChild?.matches('magic-word#invoke')
|
|
476
541
|
|| firstElementChild.childNodes.length !== 3
|
|
@@ -480,39 +545,48 @@ class TranscludeToken extends Token {
|
|
|
480
545
|
this.children[2].replaceChildren(...firstElementChild.lastElementChild.childNodes);
|
|
481
546
|
} else {
|
|
482
547
|
const {lastChild} = firstElementChild;
|
|
483
|
-
|
|
484
|
-
firstElementChild.destroy();
|
|
548
|
+
firstElementChild.destroy(true);
|
|
485
549
|
this.appendChild(lastChild);
|
|
486
550
|
}
|
|
487
551
|
}
|
|
488
552
|
|
|
489
|
-
/**
|
|
553
|
+
/**
|
|
554
|
+
* 是否存在重名参数
|
|
555
|
+
* @complexity `n`
|
|
556
|
+
* @throws `Error` 仅用于模板
|
|
557
|
+
*/
|
|
490
558
|
hasDuplicatedArgs() {
|
|
491
|
-
if (
|
|
492
|
-
|
|
559
|
+
if (this.matches('template, magic-word#invoke')) {
|
|
560
|
+
return this.getAllArgs().length - this.getKeys().length;
|
|
493
561
|
}
|
|
494
|
-
|
|
562
|
+
throw new Error(`${this.constructor.name}.hasDuplicatedArgs 方法仅供模板使用!`);
|
|
495
563
|
}
|
|
496
564
|
|
|
497
|
-
/**
|
|
565
|
+
/**
|
|
566
|
+
* 获取重名参数
|
|
567
|
+
* @complexity `n`
|
|
568
|
+
* @throws `Error` 仅用于模板
|
|
569
|
+
*/
|
|
498
570
|
getDuplicatedArgs() {
|
|
499
|
-
if (
|
|
500
|
-
|
|
571
|
+
if (this.matches('template, magic-word#invoke')) {
|
|
572
|
+
return Object.entries(this.#args).filter(([, {size}]) => size > 1).map(([key, args]) => [key, new Set(args)]);
|
|
501
573
|
}
|
|
502
|
-
|
|
574
|
+
throw new Error(`${this.constructor.name}.getDuplicatedArgs 方法仅供模板使用!`);
|
|
503
575
|
}
|
|
504
576
|
|
|
505
577
|
/**
|
|
578
|
+
* 修复重名参数:
|
|
506
579
|
* `aggressive = false`时只移除空参数和全同参数,优先保留匿名参数,否则将所有匿名参数更改为命名。
|
|
507
|
-
* `aggressive = true
|
|
580
|
+
* `aggressive = true`时还会尝试处理连续的以数字编号的参数。
|
|
581
|
+
* @param {boolean} aggressive 是否使用有更大风险的修复手段
|
|
508
582
|
* @complexity `n²`
|
|
509
583
|
*/
|
|
510
|
-
fixDuplication(aggressive
|
|
584
|
+
fixDuplication(aggressive) {
|
|
511
585
|
if (!this.hasDuplicatedArgs()) {
|
|
512
586
|
return [];
|
|
513
587
|
}
|
|
514
588
|
const /** @type {string[]} */ duplicatedKeys = [];
|
|
515
|
-
let anonCount = this.getAnonArgs()
|
|
589
|
+
let {length: anonCount} = this.getAnonArgs();
|
|
516
590
|
for (const [key, args] of this.getDuplicatedArgs()) {
|
|
517
591
|
if (args.size <= 1) {
|
|
518
592
|
continue;
|
|
@@ -551,10 +625,10 @@ class TranscludeToken extends Token {
|
|
|
551
625
|
let remaining = args.size - badArgs.length;
|
|
552
626
|
if (remaining === 1) {
|
|
553
627
|
continue;
|
|
554
|
-
} else if (aggressive && (anonCount ? /\D\d+$/ : /(?:^|\D)\d+$/).test(key)) {
|
|
628
|
+
} else if (aggressive && (anonCount ? /\D\d+$/u : /(?:^|\D)\d+$/u).test(key)) {
|
|
555
629
|
let /** @type {number} */ last;
|
|
556
|
-
const str = key.slice(0,
|
|
557
|
-
regex = new RegExp(`^${escapeRegExp(str)}\\d
|
|
630
|
+
const str = key.slice(0, -/(?<!\d)\d+$/u.exec(key)[0].length),
|
|
631
|
+
regex = new RegExp(`^${escapeRegExp(str)}\\d+$`, 'u'),
|
|
558
632
|
series = this.getAllArgs().filter(({name}) => regex.test(name)),
|
|
559
633
|
ordered = series.every(({name}, i) => {
|
|
560
634
|
const j = Number(name.slice(str.length)),
|
|
@@ -563,8 +637,9 @@ class TranscludeToken extends Token {
|
|
|
563
637
|
return cmp;
|
|
564
638
|
});
|
|
565
639
|
if (ordered) {
|
|
566
|
-
for (
|
|
567
|
-
const name = `${str}${i + 1}
|
|
640
|
+
for (let i = 0; i < series.length; i++) {
|
|
641
|
+
const name = `${str}${i + 1}`,
|
|
642
|
+
arg = series[i];
|
|
568
643
|
if (arg.name !== name) {
|
|
569
644
|
if (arg.name === key) {
|
|
570
645
|
remaining--;
|
|
@@ -590,25 +665,27 @@ class TranscludeToken extends Token {
|
|
|
590
665
|
}
|
|
591
666
|
|
|
592
667
|
/**
|
|
668
|
+
* 转义模板内的表格
|
|
593
669
|
* @returns {TranscludeToken}
|
|
594
670
|
* @complexity `n`
|
|
671
|
+
* @throws `Error` 转义失败
|
|
595
672
|
*/
|
|
596
673
|
escapeTables() {
|
|
597
674
|
const count = this.hasDuplicatedArgs();
|
|
598
|
-
if (!/\n\s
|
|
675
|
+
if (!/\n[^\S\n]*(?::+\s*)?\{\|[^\n]*\n\s*(?:\S[^\n]*\n\s*)*\|\}/u.test(this.text()) || !count) {
|
|
599
676
|
return this;
|
|
600
677
|
}
|
|
601
|
-
const stripped = this
|
|
678
|
+
const stripped = String(this).slice(2, -2),
|
|
602
679
|
include = this.getAttribute('include'),
|
|
603
680
|
config = this.getAttribute('config'),
|
|
604
|
-
parsed = Parser.parse(stripped, include, 4, config)
|
|
605
|
-
|
|
681
|
+
parsed = Parser.parse(stripped, include, 4, config);
|
|
682
|
+
const TableToken = require('./table');
|
|
606
683
|
for (const table of parsed.children) {
|
|
607
684
|
if (table instanceof TableToken) {
|
|
608
685
|
table.escape();
|
|
609
686
|
}
|
|
610
687
|
}
|
|
611
|
-
const {firstChild, childNodes} = Parser.parse(`{{${parsed
|
|
688
|
+
const {firstChild, childNodes} = Parser.parse(`{{${String(parsed)}}}`, include, 2, config);
|
|
612
689
|
if (childNodes.length !== 1 || !(firstChild instanceof TranscludeToken)) {
|
|
613
690
|
throw new Error('转义表格失败!');
|
|
614
691
|
}
|
package/test/api.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** @file 为MediaWiki API请求提供Promise界面 */
|
|
2
|
+
'use strict';
|
|
3
|
+
const request = require('request'),
|
|
4
|
+
{sleep} = require('./util'),
|
|
5
|
+
{info} = require('..');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 规范化API请求参数
|
|
9
|
+
* @param {Record<string, string|number|boolean|(string|number)[]>} obj 请求参数
|
|
10
|
+
* @returns {Record<string, string}
|
|
11
|
+
* @throws `TypeError` 部分参数不是字符串或数字
|
|
12
|
+
*/
|
|
13
|
+
const normalizeValues = obj => {
|
|
14
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
15
|
+
if (val === undefined || val === false) {
|
|
16
|
+
delete obj[key];
|
|
17
|
+
} else if (val === true) {
|
|
18
|
+
obj[key] = 1;
|
|
19
|
+
} else if (Array.isArray(val)) {
|
|
20
|
+
obj[key] = val.join('|');
|
|
21
|
+
} else if (typeof val !== 'string' && typeof val !== 'number') {
|
|
22
|
+
throw new TypeError('API请求的各项参数均为字符串或数字!');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return obj;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** 通用MediaWiki站点的请求 */
|
|
29
|
+
class Api {
|
|
30
|
+
url;
|
|
31
|
+
request = request.defaults({jar: true});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} url 网址
|
|
35
|
+
* @throws `RangeError` 无效网址
|
|
36
|
+
*/
|
|
37
|
+
constructor(url) {
|
|
38
|
+
try {
|
|
39
|
+
new URL(url);
|
|
40
|
+
} catch {
|
|
41
|
+
throw new RangeError('不是有效的网址!');
|
|
42
|
+
}
|
|
43
|
+
this.url = url;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* GET请求
|
|
48
|
+
* @param {Record<string, string|number|boolean|(string|number)[]>} params 请求参数
|
|
49
|
+
* @returns {Promise<any>}
|
|
50
|
+
*/
|
|
51
|
+
async get(params) {
|
|
52
|
+
params = normalizeValues(params);
|
|
53
|
+
const /** @type {Record<string, string|number>} */ qs = {
|
|
54
|
+
action: 'query', format: 'json', formatversion: 2, errorformat: 'plaintext', ...params,
|
|
55
|
+
};
|
|
56
|
+
try {
|
|
57
|
+
return await new Promise((resolve, reject) => {
|
|
58
|
+
this.request.get({url: this.url, qs}, (e, response, body) => {
|
|
59
|
+
const statusCode = response?.statusCode;
|
|
60
|
+
if (e) {
|
|
61
|
+
reject({statusCode, ...e});
|
|
62
|
+
} else {
|
|
63
|
+
try {
|
|
64
|
+
const data = JSON.parse(body);
|
|
65
|
+
resolve(data);
|
|
66
|
+
} catch {
|
|
67
|
+
reject({statusCode, body});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
} catch (e) {
|
|
73
|
+
if ([500, 502, 504].includes(e.statusCode)) {
|
|
74
|
+
info(`对网址 ${this.url} 发出的GET请求触发错误代码 ${e.statusCode},30秒后将再次尝试。`);
|
|
75
|
+
await sleep(30);
|
|
76
|
+
return this.get(params);
|
|
77
|
+
}
|
|
78
|
+
throw e;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = Api;
|