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