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.
Files changed (81) hide show
  1. package/.eslintrc.json +472 -34
  2. package/README.md +1 -1
  3. package/config/default.json +58 -30
  4. package/config/llwiki.json +22 -90
  5. package/config/moegirl.json +51 -13
  6. package/config/zhwiki.json +1269 -0
  7. package/index.js +114 -104
  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 +9 -3
  18. package/parser/brackets.js +22 -17
  19. package/parser/commentAndExt.js +18 -16
  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 +10 -9
  24. package/parser/links.js +15 -14
  25. package/parser/list.js +12 -11
  26. package/parser/magicLinks.js +12 -11
  27. package/parser/quotes.js +6 -5
  28. package/parser/selector.js +175 -0
  29. package/parser/table.js +25 -18
  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 +182 -100
  35. package/src/converter.js +68 -41
  36. package/src/converterFlags.js +67 -45
  37. package/src/converterRule.js +117 -65
  38. package/src/extLink.js +66 -18
  39. package/src/gallery.js +42 -15
  40. package/src/heading.js +34 -15
  41. package/src/html.js +97 -35
  42. package/src/imageParameter.js +83 -54
  43. package/src/index.js +299 -178
  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 +146 -60
  48. package/src/magicLink.js +34 -12
  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 +26 -20
  60. package/src/table/index.js +260 -165
  61. package/src/table/td.js +98 -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 +170 -93
  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 +40 -27
  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';
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 (/\x00\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
- if (this.name.includes('\x00')) {
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 === 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 方法仅用于更换模块!`);
@@ -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(`{{#invoke:M|${func}}}`, this.getAttribute('include'), 2, this.getAttribute('config')),
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
- root.destroy();
484
- firstElementChild.destroy();
548
+ firstElementChild.destroy(true);
485
549
  this.appendChild(lastChild);
486
550
  }
487
551
  }
488
552
 
489
- /** @complexity `n` */
553
+ /**
554
+ * 是否存在重名参数
555
+ * @complexity `n`
556
+ * @throws `Error` 仅用于模板
557
+ */
490
558
  hasDuplicatedArgs() {
491
- if (!this.matches('template, magic-word#invoke')) {
492
- throw new Error(`${this.constructor.name}.hasDuplicatedArgs 方法仅供模板使用!`);
559
+ if (this.matches('template, magic-word#invoke')) {
560
+ return this.getAllArgs().length - this.getKeys().length;
493
561
  }
494
- return this.getAllArgs().length - this.getKeys().length;
562
+ throw new Error(`${this.constructor.name}.hasDuplicatedArgs 方法仅供模板使用!`);
495
563
  }
496
564
 
497
- /** @complexity `n` */
565
+ /**
566
+ * 获取重名参数
567
+ * @complexity `n`
568
+ * @throws `Error` 仅用于模板
569
+ */
498
570
  getDuplicatedArgs() {
499
- if (!this.matches('template, magic-word#invoke')) {
500
- 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)]);
501
573
  }
502
- return Object.entries(this.#args).filter(([, {size}]) => size > 1).map(([key, args]) => [key, new Set(args)]);
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 = false) {
584
+ fixDuplication(aggressive) {
511
585
  if (!this.hasDuplicatedArgs()) {
512
586
  return [];
513
587
  }
514
588
  const /** @type {string[]} */ duplicatedKeys = [];
515
- let anonCount = this.getAnonArgs().length;
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, -key.match(/\d+$/)[0].length),
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 (const [i, arg] of series.entries()) {
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*:*\s*{\|.*\n\s*\|}/s.test(this.text()) || !count) {
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.toString().slice(2, -2),
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
- TableToken = require('./table');
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.toString()}}}`, include, 2, config);
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;