wikiparser-node 0.3.1 → 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.
Files changed (81) hide show
  1. package/.eslintrc.json +446 -51
  2. package/README.md +1 -1
  3. package/config/default.json +13 -17
  4. package/config/llwiki.json +11 -79
  5. package/config/moegirl.json +7 -1
  6. package/config/zhwiki.json +1269 -0
  7. package/index.js +106 -96
  8. package/lib/element.js +448 -440
  9. package/lib/node.js +335 -115
  10. package/lib/ranges.js +27 -18
  11. package/lib/text.js +146 -0
  12. package/lib/title.js +13 -5
  13. package/mixin/attributeParent.js +70 -24
  14. package/mixin/fixedToken.js +14 -6
  15. package/mixin/hidden.js +6 -4
  16. package/mixin/sol.js +27 -10
  17. package/package.json +7 -4
  18. package/parser/brackets.js +18 -18
  19. package/parser/commentAndExt.js +15 -13
  20. package/parser/converter.js +14 -13
  21. package/parser/externalLinks.js +12 -11
  22. package/parser/hrAndDoubleUnderscore.js +23 -14
  23. package/parser/html.js +8 -7
  24. package/parser/links.js +13 -12
  25. package/parser/list.js +12 -11
  26. package/parser/magicLinks.js +11 -10
  27. package/parser/quotes.js +6 -5
  28. package/parser/selector.js +175 -0
  29. package/parser/table.js +24 -17
  30. package/printed/example.json +120 -0
  31. package/src/arg.js +56 -32
  32. package/src/atom/hidden.js +5 -2
  33. package/src/atom/index.js +17 -9
  34. package/src/attribute.js +180 -98
  35. package/src/converter.js +68 -41
  36. package/src/converterFlags.js +63 -41
  37. package/src/converterRule.js +117 -65
  38. package/src/extLink.js +66 -18
  39. package/src/gallery.js +28 -16
  40. package/src/heading.js +33 -14
  41. package/src/html.js +97 -35
  42. package/src/imageParameter.js +82 -53
  43. package/src/index.js +296 -175
  44. package/src/link/category.js +20 -52
  45. package/src/link/file.js +59 -28
  46. package/src/link/galleryImage.js +21 -7
  47. package/src/link/index.js +143 -57
  48. package/src/magicLink.js +33 -11
  49. package/src/nowiki/comment.js +22 -10
  50. package/src/nowiki/dd.js +37 -22
  51. package/src/nowiki/doubleUnderscore.js +16 -7
  52. package/src/nowiki/hr.js +11 -7
  53. package/src/nowiki/index.js +16 -9
  54. package/src/nowiki/list.js +2 -2
  55. package/src/nowiki/noinclude.js +8 -4
  56. package/src/nowiki/quote.js +11 -7
  57. package/src/onlyinclude.js +19 -7
  58. package/src/parameter.js +65 -38
  59. package/src/syntax.js +23 -20
  60. package/src/table/index.js +260 -165
  61. package/src/table/td.js +97 -52
  62. package/src/table/tr.js +102 -58
  63. package/src/tagPair/ext.js +27 -19
  64. package/src/tagPair/include.js +16 -11
  65. package/src/tagPair/index.js +64 -29
  66. package/src/transclude.js +167 -92
  67. package/test/api.js +83 -0
  68. package/test/real.js +133 -0
  69. package/test/test.js +28 -0
  70. package/test/util.js +80 -0
  71. package/tool/index.js +41 -31
  72. package/typings/api.d.ts +13 -0
  73. package/typings/array.d.ts +28 -0
  74. package/typings/event.d.ts +24 -0
  75. package/typings/index.d.ts +46 -4
  76. package/typings/node.d.ts +15 -9
  77. package/typings/parser.d.ts +7 -0
  78. package/typings/tool.d.ts +3 -2
  79. package/util/debug.js +21 -18
  80. package/util/string.js +38 -25
  81. 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
- /** @type {Parser} */ Parser = require('..'),
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
- /** @complexity `n` */
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').parserFunction,
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
- {parserFunction: [insensitive, sensitive, raw]} = config;
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(/^#/, '')).type = 'magic-word';
62
- const pattern = RegExp(`^\\s*${name}\\s*$`, isSensitive ? '' : 'i'),
66
+ this.setAttribute('name', name.toLowerCase().replace(/^#/u, '')).type = 'magic-word';
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 (/\0\d+[eh!+-]\x7f|[<>[\]{}]/.test(name)) {
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.cloneChildren(),
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
133
  if (this.name.includes('\0')) {
127
- this.setAttribute('name', text(this.buildFromStr(this.name)));
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 === that.firstElementChild && that.type === 'template') {
144
- that.setAttribute('name', that.normalizeTitle(prevTarget.text(), 10).title);
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 = that.getArgs(oldKey, false, false);
152
+ const oldArgs = this.getArgs(oldKey, false, false);
147
153
  oldArgs.delete(prevTarget);
148
- that.getArgs(newKey, false, false).add(prevTarget);
149
- that.#keys.add(newKey);
154
+ this.getArgs(newKey, false, false).add(prevTarget);
155
+ this.#keys.add(newKey);
150
156
  if (oldArgs.size === 0) {
151
- that.#keys.delete(oldKey);
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
- toString() {
183
- const {children, childNodes: {length}, firstChild} = this;
184
- return `{{${this.modifier}${this.modifier && ':'}${
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 `{{${this.modifier}${this.modifier && ':'}${
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
- * @param {number|ParameterToken} addedToken
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 (const [i, token] of [...args.entries()].slice(j)) {
227
- const {name} = token,
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
- * @param {number} i
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
- * @param {ParameterToken} token
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
- /** @complexity `n` */
304
+ /**
305
+ * 获取匿名参数
306
+ * @complexity `n`
307
+ */
280
308
  getAnonArgs() {
281
309
  return this.getAllArgs().filter(({anon}) => anon);
282
310
  }
283
311
 
284
312
  /**
285
- * @param {string|number} key
313
+ * 获取指定参数
314
+ * @param {string|number} key 参数名
315
+ * @param {boolean} exact 是否匹配匿名性
316
+ * @param {boolean} copy 是否返回一个备份
286
317
  * @complexity `n`
287
318
  */
288
- getArgs(key, exact = false, copy = true) {
289
- if (!['string', 'number'].includes(typeof key)) {
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
- * @param {string|number} key
339
+ * 是否具有某参数
340
+ * @param {string|number} key 参数名
341
+ * @param {boolean} exact 是否匹配匿名性
310
342
  * @complexity `n`
311
343
  */
312
- hasArg(key, exact = false) {
344
+ hasArg(key, exact) {
313
345
  return this.getArgs(key, exact, false).size > 0;
314
346
  }
315
347
 
316
348
  /**
317
- * @param {string|number} key
349
+ * 获取生效的指定参数
350
+ * @param {string|number} key 参数名
351
+ * @param {boolean} exact 是否匹配匿名性
318
352
  * @complexity `n`
319
353
  */
320
- getArg(key, exact = false) {
321
- return [...this.getArgs(key, exact, false)].sort((a, b) => a.comparePosition(b)).at(-1);
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
- * @param {string|number} key
359
+ * 移除指定参数
360
+ * @param {string|number} key 参数名
361
+ * @param {boolean} exact 是否匹配匿名性
326
362
  * @complexity `n`
327
363
  */
328
- removeArg(key, exact = false) {
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
- /** @complexity `n` */
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
- * @param {string|number} key
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
- if (key !== undefined) {
363
- return this.getArg(key)?.getValue();
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
- * @param {string} val
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 !== 1 || !firstElementChild?.matches(templateLike ? 'template#T' : 'magic-word#lc')
380
- || firstElementChild.childNodes.length !== 2 || !firstElementChild.lastElementChild.anon
421
+ if (length === 1 && firstElementChild?.matches(templateLike ? 'template#T' : 'magic-word#lc')
422
+ && firstElementChild.childNodes.length === 2 && firstElementChild.lastElementChild.anon
381
423
  ) {
382
- throw new SyntaxError(`非法的匿名参数:${noWrap(val)}`);
424
+ return this.appendChild(firstElementChild.lastChild);
383
425
  }
384
- return this.appendChild(firstElementChild.lastChild);
426
+ throw new SyntaxError(`非法的匿名参数:${noWrap(val)}`);
385
427
  }
386
428
 
387
429
  /**
388
- * @param {string} key
389
- * @param {string} value
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
- /** @complexity `n` */
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
- /** @param {string} title */
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
- /** @param {string} title */
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
- root.destroy();
459
- firstElementChild.destroy();
516
+ firstElementChild.destroy(true);
460
517
  this.appendChild(lastChild);
461
518
  }
462
519
  }
463
520
 
464
- /** @param {string} func */
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 方法仅用于更换模块!`);
@@ -482,39 +545,48 @@ class TranscludeToken extends Token {
482
545
  this.children[2].replaceChildren(...firstElementChild.lastElementChild.childNodes);
483
546
  } else {
484
547
  const {lastChild} = firstElementChild;
485
- root.destroy();
486
- firstElementChild.destroy();
548
+ firstElementChild.destroy(true);
487
549
  this.appendChild(lastChild);
488
550
  }
489
551
  }
490
552
 
491
- /** @complexity `n` */
553
+ /**
554
+ * 是否存在重名参数
555
+ * @complexity `n`
556
+ * @throws `Error` 仅用于模板
557
+ */
492
558
  hasDuplicatedArgs() {
493
- if (!this.matches('template, magic-word#invoke')) {
494
- throw new Error(`${this.constructor.name}.hasDuplicatedArgs 方法仅供模板使用!`);
559
+ if (this.matches('template, magic-word#invoke')) {
560
+ return this.getAllArgs().length - this.getKeys().length;
495
561
  }
496
- return this.getAllArgs().length - this.getKeys().length;
562
+ throw new Error(`${this.constructor.name}.hasDuplicatedArgs 方法仅供模板使用!`);
497
563
  }
498
564
 
499
- /** @complexity `n` */
565
+ /**
566
+ * 获取重名参数
567
+ * @complexity `n`
568
+ * @throws `Error` 仅用于模板
569
+ */
500
570
  getDuplicatedArgs() {
501
- if (!this.matches('template, magic-word#invoke')) {
502
- throw new Error(`${this.constructor.name}.getDuplicatedArgs 方法仅供模板使用!`);
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)]);
503
573
  }
504
- return Object.entries(this.#args).filter(([, {size}]) => size > 1).map(([key, args]) => [key, new Set(args)]);
574
+ throw new Error(`${this.constructor.name}.getDuplicatedArgs 方法仅供模板使用!`);
505
575
  }
506
576
 
507
577
  /**
578
+ * 修复重名参数:
508
579
  * `aggressive = false`时只移除空参数和全同参数,优先保留匿名参数,否则将所有匿名参数更改为命名。
509
- * `aggressive = true`时还会尝试处理连续的以数字编号的参数
580
+ * `aggressive = true`时还会尝试处理连续的以数字编号的参数。
581
+ * @param {boolean} aggressive 是否使用有更大风险的修复手段
510
582
  * @complexity `n²`
511
583
  */
512
- fixDuplication(aggressive = false) {
584
+ fixDuplication(aggressive) {
513
585
  if (!this.hasDuplicatedArgs()) {
514
586
  return [];
515
587
  }
516
588
  const /** @type {string[]} */ duplicatedKeys = [];
517
- let anonCount = this.getAnonArgs().length;
589
+ let {length: anonCount} = this.getAnonArgs();
518
590
  for (const [key, args] of this.getDuplicatedArgs()) {
519
591
  if (args.size <= 1) {
520
592
  continue;
@@ -553,10 +625,10 @@ class TranscludeToken extends Token {
553
625
  let remaining = args.size - badArgs.length;
554
626
  if (remaining === 1) {
555
627
  continue;
556
- } else if (aggressive && (anonCount ? /\D\d+$/ : /(?:^|\D)\d+$/).test(key)) {
628
+ } else if (aggressive && (anonCount ? /\D\d+$/u : /(?:^|\D)\d+$/u).test(key)) {
557
629
  let /** @type {number} */ last;
558
- const str = key.slice(0, -/(?<!\d)\d+$/.exec(key)[0].length),
559
- regex = 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'),
560
632
  series = this.getAllArgs().filter(({name}) => regex.test(name)),
561
633
  ordered = series.every(({name}, i) => {
562
634
  const j = Number(name.slice(str.length)),
@@ -565,8 +637,9 @@ class TranscludeToken extends Token {
565
637
  return cmp;
566
638
  });
567
639
  if (ordered) {
568
- for (const [i, arg] of series.entries()) {
569
- const name = `${str}${i + 1}`;
640
+ for (let i = 0; i < series.length; i++) {
641
+ const name = `${str}${i + 1}`,
642
+ arg = series[i];
570
643
  if (arg.name !== name) {
571
644
  if (arg.name === key) {
572
645
  remaining--;
@@ -592,25 +665,27 @@ class TranscludeToken extends Token {
592
665
  }
593
666
 
594
667
  /**
668
+ * 转义模板内的表格
595
669
  * @returns {TranscludeToken}
596
670
  * @complexity `n`
671
+ * @throws `Error` 转义失败
597
672
  */
598
673
  escapeTables() {
599
674
  const count = this.hasDuplicatedArgs();
600
- if (!/\n[^\S\n]*(?::+\s*)?\{\|[^\n]*\n\s*(?:\S[^\n]*\n\s*)*\|\}/.test(this.text()) || !count) {
675
+ if (!/\n[^\S\n]*(?::+\s*)?\{\|[^\n]*\n\s*(?:\S[^\n]*\n\s*)*\|\}/u.test(this.text()) || !count) {
601
676
  return this;
602
677
  }
603
- const stripped = this.toString().slice(2, -2),
678
+ const stripped = String(this).slice(2, -2),
604
679
  include = this.getAttribute('include'),
605
680
  config = this.getAttribute('config'),
606
- parsed = Parser.parse(stripped, include, 4, config),
607
- TableToken = require('./table');
681
+ parsed = Parser.parse(stripped, include, 4, config);
682
+ const TableToken = require('./table');
608
683
  for (const table of parsed.children) {
609
684
  if (table instanceof TableToken) {
610
685
  table.escape();
611
686
  }
612
687
  }
613
- const {firstChild, childNodes} = Parser.parse(`{{${parsed.toString()}}}`, include, 2, config);
688
+ const {firstChild, childNodes} = Parser.parse(`{{${String(parsed)}}}`, include, 2, config);
614
689
  if (childNodes.length !== 1 || !(firstChild instanceof TranscludeToken)) {
615
690
  throw new Error('转义表格失败!');
616
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;