wikiparser-node 0.4.0 → 0.6.1

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 (87) hide show
  1. package/config/default.json +129 -66
  2. package/config/zhwiki.json +4 -4
  3. package/index.js +97 -65
  4. package/lib/element.js +159 -302
  5. package/lib/node.js +384 -198
  6. package/lib/ranges.js +3 -4
  7. package/lib/text.js +65 -36
  8. package/lib/title.js +9 -8
  9. package/mixin/fixedToken.js +4 -4
  10. package/mixin/hidden.js +2 -0
  11. package/mixin/sol.js +16 -7
  12. package/package.json +14 -3
  13. package/parser/brackets.js +8 -2
  14. package/parser/commentAndExt.js +1 -1
  15. package/parser/converter.js +1 -1
  16. package/parser/externalLinks.js +2 -2
  17. package/parser/hrAndDoubleUnderscore.js +8 -7
  18. package/parser/links.js +8 -9
  19. package/parser/magicLinks.js +1 -1
  20. package/parser/selector.js +5 -5
  21. package/parser/table.js +18 -16
  22. package/src/arg.js +71 -42
  23. package/src/atom/index.js +7 -5
  24. package/src/attribute.js +102 -64
  25. package/src/charinsert.js +91 -0
  26. package/src/converter.js +34 -15
  27. package/src/converterFlags.js +87 -40
  28. package/src/converterRule.js +59 -53
  29. package/src/extLink.js +45 -37
  30. package/src/gallery.js +71 -16
  31. package/src/hasNowiki/index.js +42 -0
  32. package/src/hasNowiki/pre.js +40 -0
  33. package/src/heading.js +41 -18
  34. package/src/html.js +76 -48
  35. package/src/imageParameter.js +73 -51
  36. package/src/imagemap.js +205 -0
  37. package/src/imagemapLink.js +43 -0
  38. package/src/index.js +243 -138
  39. package/src/link/category.js +10 -14
  40. package/src/link/file.js +112 -56
  41. package/src/link/galleryImage.js +74 -10
  42. package/src/link/index.js +86 -61
  43. package/src/magicLink.js +48 -21
  44. package/src/nested/choose.js +24 -0
  45. package/src/nested/combobox.js +23 -0
  46. package/src/nested/index.js +88 -0
  47. package/src/nested/references.js +23 -0
  48. package/src/nowiki/comment.js +18 -4
  49. package/src/nowiki/dd.js +2 -2
  50. package/src/nowiki/doubleUnderscore.js +16 -11
  51. package/src/nowiki/index.js +12 -0
  52. package/src/nowiki/quote.js +28 -1
  53. package/src/onlyinclude.js +15 -8
  54. package/src/paramTag/index.js +83 -0
  55. package/src/paramTag/inputbox.js +42 -0
  56. package/src/parameter.js +73 -46
  57. package/src/syntax.js +9 -1
  58. package/src/table/index.js +58 -44
  59. package/src/table/td.js +63 -63
  60. package/src/table/tr.js +52 -35
  61. package/src/tagPair/ext.js +60 -43
  62. package/src/tagPair/include.js +11 -1
  63. package/src/tagPair/index.js +29 -20
  64. package/src/transclude.js +214 -166
  65. package/tool/index.js +720 -439
  66. package/util/base.js +17 -0
  67. package/util/debug.js +1 -1
  68. package/{test/util.js → util/diff.js} +15 -19
  69. package/util/lint.js +40 -0
  70. package/util/string.js +37 -20
  71. package/.eslintrc.json +0 -714
  72. package/errors/README +0 -1
  73. package/jsconfig.json +0 -7
  74. package/printed/README +0 -1
  75. package/printed/example.json +0 -120
  76. package/test/api.js +0 -83
  77. package/test/real.js +0 -133
  78. package/test/test.js +0 -28
  79. package/typings/api.d.ts +0 -13
  80. package/typings/array.d.ts +0 -28
  81. package/typings/event.d.ts +0 -24
  82. package/typings/index.d.ts +0 -94
  83. package/typings/node.d.ts +0 -29
  84. package/typings/parser.d.ts +0 -16
  85. package/typings/table.d.ts +0 -14
  86. package/typings/token.d.ts +0 -22
  87. package/typings/tool.d.ts +0 -11
@@ -13,6 +13,15 @@ class TagPairToken extends fixedToken(Token) {
13
13
  #closed;
14
14
  #tags;
15
15
 
16
+ /** getter */
17
+ get closed() {
18
+ return this.#closed;
19
+ }
20
+
21
+ set closed(value) {
22
+ this.#closed ||= Boolean(value);
23
+ }
24
+
16
25
  /** getter */
17
26
  get selfClosing() {
18
27
  return this.#selfClosing;
@@ -26,15 +35,6 @@ class TagPairToken extends fixedToken(Token) {
26
35
  this.#selfClosing = value;
27
36
  }
28
37
 
29
- /** getter */
30
- get closed() {
31
- return this.#closed;
32
- }
33
-
34
- set closed(value) {
35
- this.#closed ||= Boolean(value);
36
- }
37
-
38
38
  /** 内部wikitext */
39
39
  get innerText() {
40
40
  return this.#selfClosing ? undefined : this.lastChild.text();
@@ -49,7 +49,8 @@ class TagPairToken extends fixedToken(Token) {
49
49
  */
50
50
  constructor(name, attr, inner, closed, config = Parser.getConfig(), accum = []) {
51
51
  super(undefined, config, true);
52
- this.setAttribute('name', name.toLowerCase()).#tags = [name, closed || name];
52
+ this.setAttribute('name', name.toLowerCase());
53
+ this.#tags = [name, closed || name];
53
54
  this.#selfClosing = closed === undefined;
54
55
  this.#closed = closed !== '';
55
56
  this.append(attr, inner);
@@ -63,16 +64,6 @@ class TagPairToken extends fixedToken(Token) {
63
64
  accum.splice(index, 0, this);
64
65
  }
65
66
 
66
- /**
67
- * @override
68
- * @template {string} T
69
- * @param {T} key 属性键
70
- * @returns {TokenAttribute<T>}
71
- */
72
- getAttribute(key) {
73
- return key === 'tags' ? [...this.#tags] : super.getAttribute(key);
74
- }
75
-
76
67
  /**
77
68
  * @override
78
69
  * @param {string} selector
@@ -101,6 +92,24 @@ class TagPairToken extends fixedToken(Token) {
101
92
  return 1;
102
93
  }
103
94
 
95
+ /** @override */
96
+ print() {
97
+ const [opening, closing] = this.#tags;
98
+ return super.print(this.#selfClosing
99
+ ? {pre: `&lt;${opening}`, post: '/&gt;'}
100
+ : {pre: `&lt;${opening}`, sep: '&gt;', post: this.#closed ? `&lt;/${closing}&gt;` : ''});
101
+ }
102
+
103
+ /**
104
+ * @override
105
+ * @template {string} T
106
+ * @param {T} key 属性键
107
+ * @returns {TokenAttribute<T>}
108
+ */
109
+ getAttribute(key) {
110
+ return key === 'tags' ? [...this.#tags] : super.getAttribute(key);
111
+ }
112
+
104
113
  /**
105
114
  * @override
106
115
  * @returns {string}
package/src/transclude.js CHANGED
@@ -1,7 +1,8 @@
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
+ {generateForChild} = require('../util/lint'),
5
6
  Parser = require('..'),
6
7
  Token = require('.'),
7
8
  ParameterToken = require('./parameter');
@@ -13,8 +14,13 @@ const {removeComment, escapeRegExp, text, noWrap} = require('../util/string'),
13
14
  class TranscludeToken extends Token {
14
15
  type = 'template';
15
16
  modifier = '';
16
- /** @type {Set<string>} */ #keys = new Set();
17
17
  /** @type {Record<string, Set<ParameterToken>>} */ #args = {};
18
+ /** @type {Set<string>} */ #keys = new Set();
19
+
20
+ /** 是否存在重复参数 */
21
+ get duplication() {
22
+ return this.isTemplate() && Boolean(this.hasDuplicatedArgs());
23
+ }
18
24
 
19
25
  /**
20
26
  * 设置引用修饰符
@@ -64,11 +70,11 @@ class TranscludeToken extends Token {
64
70
  isSensitive = sensitive.includes(name);
65
71
  if (isSensitive || insensitive.includes(name.toLowerCase())) {
66
72
  this.setAttribute('name', name.toLowerCase().replace(/^#/u, '')).type = 'magic-word';
67
- const pattern = new RegExp(`^\\s*${name}\\s*$`, isSensitive ? '' : 'i'),
73
+ const pattern = new RegExp(`^\\s*${name}\\s*$`, isSensitive ? 'u' : 'iu'),
68
74
  token = new SyntaxToken(magicWord, pattern, 'magic-word-name', config, accum, {
69
75
  'Stage-1': ':', '!ExtToken': '',
70
76
  });
71
- this.appendChild(token);
77
+ this.insertAt(token);
72
78
  if (arg.length > 0) {
73
79
  parts.unshift([arg.join(':')]);
74
80
  }
@@ -82,7 +88,7 @@ class TranscludeToken extends Token {
82
88
  const invoke = new AtomToken(part.join('='), `invoke-${
83
89
  i ? 'function' : 'module'
84
90
  }`, config, accum, {'Stage-1': ':', '!ExtToken': ''});
85
- this.appendChild(invoke);
91
+ this.insertAt(invoke);
86
92
  }
87
93
  this.getAttribute('protectChildren')('1:3');
88
94
  }
@@ -94,11 +100,10 @@ class TranscludeToken extends Token {
94
100
  accum.pop();
95
101
  throw new SyntaxError(`非法的模板名称:${name}`);
96
102
  }
97
- this.setAttribute('name', this.normalizeTitle(name, 10, true).title);
98
103
  const token = new AtomToken(title, 'template-name', config, accum, {'Stage-2': ':', '!HeadingToken': ''});
99
- this.appendChild(token);
104
+ this.insertAt(token);
100
105
  }
101
- const templateLike = this.matches('template, magic-word#invoke');
106
+ const templateLike = this.isTemplate();
102
107
  let i = 1;
103
108
  for (const part of parts) {
104
109
  if (!templateLike) {
@@ -109,85 +114,11 @@ class TranscludeToken extends Token {
109
114
  part.unshift(i);
110
115
  i++;
111
116
  }
112
- this.appendChild(new ParameterToken(...part, config, accum));
117
+ this.insertAt(new ParameterToken(...part, config, accum));
113
118
  }
114
119
  this.getAttribute('protectChildren')(0);
115
120
  }
116
121
 
117
- /** @override */
118
- cloneNode() {
119
- const [first, ...cloned] = this.cloneChildNodes(),
120
- config = this.getAttribute('config');
121
- return Parser.run(() => {
122
- const token = new TranscludeToken(this.type === 'template' ? '' : first.text(), [], config);
123
- token.setModifier(this.modifier);
124
- token.firstElementChild.safeReplaceWith(first);
125
- token.afterBuild();
126
- token.append(...cloned);
127
- return token;
128
- });
129
- }
130
-
131
- /** @override */
132
- afterBuild() {
133
- if (this.name.includes('\0')) {
134
- this.setAttribute('name', text(this.getAttribute('buildFromStr')(this.name)));
135
- }
136
- if (this.matches('template, magic-word#invoke')) {
137
- /**
138
- * 当事件bubble到`parameter`时,将`oldKey`和`newKey`保存进AstEventData。
139
- * 当继续bubble到`template`时,处理并删除`oldKey`和`newKey`。
140
- * @type {AstListener}
141
- */
142
- const transcludeListener = (e, data) => {
143
- const {prevTarget} = e,
144
- {oldKey, newKey} = data ?? {};
145
- if (typeof oldKey === 'string') {
146
- delete data.oldKey;
147
- delete data.newKey;
148
- }
149
- if (prevTarget === this.firstElementChild && this.type === 'template') {
150
- this.setAttribute('name', this.normalizeTitle(prevTarget.text(), 10).title);
151
- } else if (oldKey !== newKey && prevTarget instanceof ParameterToken) {
152
- const oldArgs = this.getArgs(oldKey, false, false);
153
- oldArgs.delete(prevTarget);
154
- this.getArgs(newKey, false, false).add(prevTarget);
155
- this.#keys.add(newKey);
156
- if (oldArgs.size === 0) {
157
- this.#keys.delete(oldKey);
158
- }
159
- }
160
- };
161
- this.addEventListener(['remove', 'insert', 'replace', 'text'], transcludeListener);
162
- }
163
- return this;
164
- }
165
-
166
- /** 替换引用 */
167
- subst() {
168
- this.setModifier('subst');
169
- }
170
-
171
- /** 安全的替换引用 */
172
- safesubst() {
173
- this.setModifier('safesubst');
174
- }
175
-
176
- /**
177
- * @override
178
- * @template {string} T
179
- * @param {T} key 属性键
180
- * @returns {TokenAttribute<T>}
181
- */
182
- getAttribute(key) {
183
- if (key === 'args') {
184
- return {...this.#args};
185
- } else if (key === 'keys') {
186
- return !Parser.debugging && externalUse('getAttribute') ? new Set(this.#keys) : this.#keys;
187
- }
188
- return super.getAttribute(key);
189
- }
190
-
191
122
  /**
192
123
  * @override
193
124
  * @param {string} selector
@@ -196,10 +127,10 @@ class TranscludeToken extends Token {
196
127
  if (selector && this.matches(selector)) {
197
128
  return '';
198
129
  }
199
- const {children, childNodes: {length}, firstChild, modifier} = this;
130
+ const {childNodes, firstChild, modifier} = this;
200
131
  return `{{${modifier}${modifier && ':'}${
201
132
  this.type === 'magic-word'
202
- ? `${String(firstChild)}${length > 1 ? ':' : ''}${children.slice(1).map(String).join('|')}`
133
+ ? `${String(firstChild)}${childNodes.length > 1 ? ':' : ''}${childNodes.slice(1).map(String).join('|')}`
203
134
  : super.toString(selector, '|')
204
135
  }}}`;
205
136
  }
@@ -214,18 +145,38 @@ class TranscludeToken extends Token {
214
145
  return 1;
215
146
  }
216
147
 
148
+ /** @override */
149
+ print() {
150
+ const {childNodes, firstChild, modifier} = this;
151
+ return `<span class="wpb-${this.type}">{{${modifier}${modifier && ':'}${
152
+ this.type === 'magic-word'
153
+ ? `${firstChild.print()}${childNodes.length > 1 ? ':' : ''}${print(childNodes.slice(1), {sep: '|'})}`
154
+ : print(childNodes, {sep: '|'})
155
+ }}}</span>`;
156
+ }
157
+
217
158
  /**
218
159
  * @override
219
- * @returns {string}
220
- * @complexity `n`
160
+ * @param {number} start 起始位置
221
161
  */
222
- text() {
223
- const {children, childNodes: {length}, firstElementChild, modifier} = this;
224
- return `{{${modifier}${modifier && ':'}${
225
- this.type === 'magic-word'
226
- ? `${firstElementChild.text()}${length > 1 ? ':' : ''}${text(children.slice(1), '|')}`
227
- : super.text('|')
228
- }}}`;
162
+ lint(start = 0) {
163
+ const errors = super.lint(start);
164
+ if (!this.isTemplate()) {
165
+ return errors;
166
+ }
167
+ const duplicatedArgs = this.getDuplicatedArgs();
168
+ if (duplicatedArgs.length > 0) {
169
+ const rect = this.getRootNode().posFromIndex(start);
170
+ errors.push(...duplicatedArgs.flatMap(([, args]) => [...args]).map(
171
+ arg => generateForChild(arg, rect, '重复参数'),
172
+ ));
173
+ }
174
+ return errors;
175
+ }
176
+
177
+ /** 是否是模板 */
178
+ isTemplate() {
179
+ return this.type === 'template' || this.type === 'magic-word' && this.name === 'invoke';
229
180
  }
230
181
 
231
182
  /**
@@ -256,25 +207,6 @@ class TranscludeToken extends Token {
256
207
  }
257
208
  }
258
209
 
259
- /**
260
- * @override
261
- * @param {number} i 移除位置
262
- * @complexity `n`
263
- */
264
- removeAt(i) {
265
- const /** @type {ParameterToken} */ token = super.removeAt(i);
266
- if (token.anon) {
267
- this.#handleAnonArgChange(Number(token.name));
268
- } else {
269
- const args = this.getArgs(token.name, false, false);
270
- args.delete(token);
271
- if (args.size === 0) {
272
- this.#keys.delete(token.name);
273
- }
274
- }
275
- return token;
276
- }
277
-
278
210
  /**
279
211
  * @override
280
212
  * @param {ParameterToken} token 待插入的子节点
@@ -298,7 +230,7 @@ class TranscludeToken extends Token {
298
230
  * @complexity `n`
299
231
  */
300
232
  getAllArgs() {
301
- return this.children.filter(child => child instanceof ParameterToken);
233
+ return this.childNodes.filter(child => child instanceof ParameterToken);
302
234
  }
303
235
 
304
236
  /**
@@ -322,8 +254,10 @@ class TranscludeToken extends Token {
322
254
  }
323
255
  copy ||= !Parser.debugging && externalUse('getArgs');
324
256
  const keyStr = String(key).trim();
325
- let args = this.#args[keyStr];
326
- if (!args) {
257
+ let args;
258
+ if (Object.hasOwn(this.#args, keyStr)) {
259
+ args = this.#args[keyStr];
260
+ } else {
327
261
  args = new Set(this.getAllArgs().filter(({name}) => keyStr === name));
328
262
  this.#args[keyStr] = args;
329
263
  }
@@ -335,6 +269,135 @@ class TranscludeToken extends Token {
335
269
  return args;
336
270
  }
337
271
 
272
+ /**
273
+ * 获取重名参数
274
+ * @complexity `n`
275
+ * @returns {[string, Set<ParameterToken>][]}
276
+ * @throws `Error` 仅用于模板
277
+ */
278
+ getDuplicatedArgs() {
279
+ if (this.isTemplate()) {
280
+ return Object.entries(this.#args).filter(([, {size}]) => size > 1)
281
+ .map(([key, args]) => [key, new Set(args)]);
282
+ }
283
+ throw new Error(`${this.constructor.name}.getDuplicatedArgs 方法仅供模板使用!`);
284
+ }
285
+
286
+ /** @override */
287
+ cloneNode() {
288
+ const [first, ...cloned] = this.cloneChildNodes(),
289
+ config = this.getAttribute('config');
290
+ return Parser.run(() => {
291
+ const token = new TranscludeToken(this.type === 'template' ? '' : first.text(), [], config);
292
+ token.setModifier(this.modifier);
293
+ token.firstChild.safeReplaceWith(first);
294
+ token.afterBuild();
295
+ token.append(...cloned);
296
+ return token;
297
+ });
298
+ }
299
+
300
+ /** @override */
301
+ afterBuild() {
302
+ if (this.type === 'template') {
303
+ this.setAttribute('name', this.normalizeTitle(this.firstChild.text(), 10).title);
304
+ }
305
+ if (this.isTemplate()) {
306
+ /**
307
+ * 当事件bubble到`parameter`时,将`oldKey`和`newKey`保存进AstEventData。
308
+ * 当继续bubble到`template`时,处理并删除`oldKey`和`newKey`。
309
+ * @type {AstListener}
310
+ */
311
+ const transcludeListener = (e, data) => {
312
+ const {prevTarget} = e,
313
+ {oldKey, newKey} = data ?? {};
314
+ if (typeof oldKey === 'string') {
315
+ delete data.oldKey;
316
+ delete data.newKey;
317
+ }
318
+ if (prevTarget === this.firstChild && this.type === 'template') {
319
+ this.setAttribute('name', this.normalizeTitle(prevTarget.text(), 10).title);
320
+ } else if (oldKey !== newKey && prevTarget instanceof ParameterToken) {
321
+ const oldArgs = this.getArgs(oldKey, false, false);
322
+ oldArgs.delete(prevTarget);
323
+ this.getArgs(newKey, false, false).add(prevTarget);
324
+ this.#keys.add(newKey);
325
+ if (oldArgs.size === 0) {
326
+ this.#keys.delete(oldKey);
327
+ }
328
+ }
329
+ };
330
+ this.addEventListener(['remove', 'insert', 'replace', 'text'], transcludeListener);
331
+ }
332
+ return this;
333
+ }
334
+
335
+ /** 替换引用 */
336
+ subst() {
337
+ this.setModifier('subst');
338
+ }
339
+
340
+ /** 安全的替换引用 */
341
+ safesubst() {
342
+ this.setModifier('safesubst');
343
+ }
344
+
345
+ /**
346
+ * @override
347
+ * @param {PropertyKey} key 属性键
348
+ */
349
+ hasAttribute(key) {
350
+ return key === 'keys' || super.hasAttribute(key);
351
+ }
352
+
353
+ /**
354
+ * @override
355
+ * @template {string} T
356
+ * @param {T} key 属性键
357
+ * @returns {TokenAttribute<T>}
358
+ */
359
+ getAttribute(key) {
360
+ if (key === 'args') {
361
+ return {...this.#args};
362
+ } else if (key === 'keys') {
363
+ return !Parser.debugging && externalUse('getAttribute') ? new Set(this.#keys) : this.#keys;
364
+ }
365
+ return super.getAttribute(key);
366
+ }
367
+
368
+ /**
369
+ * @override
370
+ * @returns {string}
371
+ * @complexity `n`
372
+ */
373
+ text() {
374
+ const {childNodes, firstChild, modifier} = this;
375
+ return `{{${modifier}${modifier && ':'}${
376
+ this.type === 'magic-word'
377
+ ? `${firstChild.text()}${childNodes.length > 1 ? ':' : ''}${text(childNodes.slice(1), '|')}`
378
+ : super.text('|')
379
+ }}}`;
380
+ }
381
+
382
+ /**
383
+ * @override
384
+ * @param {number} i 移除位置
385
+ * @complexity `n`
386
+ */
387
+ removeAt(i) {
388
+ const /** @type {ParameterToken} */ token = super.removeAt(i);
389
+ if (token.anon) {
390
+ this.#handleAnonArgChange(Number(token.name));
391
+ } else {
392
+ const args = this.getArgs(token.name, false, false);
393
+ args.delete(token);
394
+ if (args.size === 0) {
395
+ this.#keys.delete(token.name);
396
+ }
397
+ }
398
+ return token;
399
+ }
400
+
338
401
  /**
339
402
  * 是否具有某参数
340
403
  * @param {string|number} key 参数名
@@ -396,7 +459,7 @@ class TranscludeToken extends Token {
396
459
  * 获取生效的参数值
397
460
  * @template {string|number|undefined} T
398
461
  * @param {T} key 参数名
399
- * @returns {T extends undefined ? Object<string, string> : string}
462
+ * @returns {T extends undefined ? Record<string, string> : string}
400
463
  * @complexity `n`
401
464
  */
402
465
  getValue(key) {
@@ -414,14 +477,16 @@ class TranscludeToken extends Token {
414
477
  */
415
478
  newAnonArg(val) {
416
479
  val = String(val);
417
- const templateLike = this.matches('template, magic-word#invoke'),
480
+ const templateLike = this.isTemplate(),
418
481
  wikitext = `{{${templateLike ? ':T|' : 'lc:'}${val}}}`,
419
482
  root = Parser.parse(wikitext, this.getAttribute('include'), 2, this.getAttribute('config')),
420
- {childNodes: {length}, firstElementChild} = root;
421
- if (length === 1 && firstElementChild?.matches(templateLike ? 'template#T' : 'magic-word#lc')
422
- && firstElementChild.childNodes.length === 2 && firstElementChild.lastElementChild.anon
423
- ) {
424
- return this.appendChild(firstElementChild.lastChild);
483
+ {length, firstChild: transclude} = root,
484
+ /** @type {Token & {lastChild: ParameterToken}} */
485
+ {type, name, length: transcludeLength, lastChild} = transclude,
486
+ targetType = templateLike ? 'template' : 'magic-word',
487
+ targetName = templateLike ? 'T' : 'lc';
488
+ if (length === 1 && type === targetType && name === targetName && transcludeLength === 2 && lastChild.anon) {
489
+ return this.insertAt(lastChild);
425
490
  }
426
491
  throw new SyntaxError(`非法的匿名参数:${noWrap(val)}`);
427
492
  }
@@ -437,7 +502,7 @@ class TranscludeToken extends Token {
437
502
  setValue(key, value) {
438
503
  if (typeof key !== 'string') {
439
504
  this.typeError('setValue', 'String');
440
- } else if (!this.matches('template, magic-word#invoke')) {
505
+ } else if (!this.isTemplate()) {
441
506
  throw new Error(`${this.constructor.name}.setValue 方法仅供模板使用!`);
442
507
  }
443
508
  const token = this.getArg(key);
@@ -448,13 +513,12 @@ class TranscludeToken extends Token {
448
513
  }
449
514
  const wikitext = `{{:T|${key}=${value}}}`,
450
515
  root = Parser.parse(wikitext, this.getAttribute('include'), 2, this.getAttribute('config')),
451
- {childNodes: {length}, firstElementChild} = root;
452
- if (length !== 1 || !firstElementChild?.matches('template#T')
453
- || firstElementChild.childNodes.length !== 2 || firstElementChild.lastElementChild.name !== key
454
- ) {
516
+ {length, firstChild: template} = root,
517
+ {type, name, length: templateLength, lastChild: parameter} = template;
518
+ if (length !== 1 || type !== 'template' || name !== 'T' || templateLength !== 2 || parameter.name !== key) {
455
519
  throw new SyntaxError(`非法的命名参数:${key}=${noWrap(value)}`);
456
520
  }
457
- this.appendChild(firstElementChild.lastChild);
521
+ this.insertAt(parameter);
458
522
  }
459
523
 
460
524
  /**
@@ -463,11 +527,11 @@ class TranscludeToken extends Token {
463
527
  * @throws `Error` 仅用于模板
464
528
  */
465
529
  anonToNamed() {
466
- if (!this.matches('template, magic-word#invoke')) {
530
+ if (!this.isTemplate()) {
467
531
  throw new Error(`${this.constructor.name}.anonToNamed 方法仅供模板使用!`);
468
532
  }
469
533
  for (const token of this.getAnonArgs()) {
470
- token.firstElementChild.replaceChildren(token.name);
534
+ token.firstChild.replaceChildren(token.name);
471
535
  }
472
536
  }
473
537
 
@@ -484,11 +548,11 @@ class TranscludeToken extends Token {
484
548
  this.typeError('replaceTemplate', 'String');
485
549
  }
486
550
  const root = Parser.parse(`{{${title}}}`, this.getAttribute('include'), 2, this.getAttribute('config')),
487
- {childNodes: {length}, firstElementChild} = root;
488
- if (length !== 1 || firstElementChild?.type !== 'template' || firstElementChild.childNodes.length !== 1) {
551
+ {length, firstChild: template} = root;
552
+ if (length !== 1 || template.type !== 'template' || template.childNodes.length !== 1) {
489
553
  throw new SyntaxError(`非法的模板名称:${title}`);
490
554
  }
491
- this.firstElementChild.replaceChildren(...firstElementChild.firstElementChild.childNodes);
555
+ this.firstChild.replaceChildren(...template.firstChild.childNodes);
492
556
  }
493
557
 
494
558
  /**
@@ -504,17 +568,15 @@ class TranscludeToken extends Token {
504
568
  this.typeError('replaceModule', 'String');
505
569
  }
506
570
  const root = Parser.parse(`{{#invoke:${title}}}`, this.getAttribute('include'), 2, this.getAttribute('config')),
507
- {childNodes: {length}, firstElementChild} = root;
508
- if (length !== 1 || !firstElementChild?.matches('magic-word#invoke')
509
- || firstElementChild.childNodes.length !== 2
510
- ) {
571
+ {length, firstChild: invoke} = root,
572
+ {type, name, length: invokeLength, lastChild} = invoke;
573
+ if (length !== 1 || type !== 'magic-word' || name !== 'invoke' || invokeLength !== 2) {
511
574
  throw new SyntaxError(`非法的模块名称:${title}`);
512
575
  } else if (this.childNodes.length > 1) {
513
- this.children[1].replaceChildren(...firstElementChild.lastElementChild.childNodes);
576
+ this.childNodes[1].replaceChildren(...lastChild.childNodes);
514
577
  } else {
515
- const {lastChild} = firstElementChild;
516
- firstElementChild.destroy(true);
517
- this.appendChild(lastChild);
578
+ invoke.destroy(true);
579
+ this.insertAt(lastChild);
518
580
  }
519
581
  }
520
582
 
@@ -536,17 +598,15 @@ class TranscludeToken extends Token {
536
598
  const root = Parser.parse(
537
599
  `{{#invoke:M|${func}}}`, this.getAttribute('include'), 2, this.getAttribute('config'),
538
600
  ),
539
- {childNodes: {length}, firstElementChild} = root;
540
- if (length !== 1 || !firstElementChild?.matches('magic-word#invoke')
541
- || firstElementChild.childNodes.length !== 3
542
- ) {
601
+ {length, firstChild: invoke} = root,
602
+ {type, name, length: invokeLength, lastChild} = invoke;
603
+ if (length !== 1 || type !== 'magic-word' || name !== 'invoke' || invokeLength !== 3) {
543
604
  throw new SyntaxError(`非法的模块函数名:${func}`);
544
605
  } else if (this.childNodes.length > 2) {
545
- this.children[2].replaceChildren(...firstElementChild.lastElementChild.childNodes);
606
+ this.childNodes[2].replaceChildren(...lastChild.childNodes);
546
607
  } else {
547
- const {lastChild} = firstElementChild;
548
- firstElementChild.destroy(true);
549
- this.appendChild(lastChild);
608
+ invoke.destroy(true);
609
+ this.insertAt(lastChild);
550
610
  }
551
611
  }
552
612
 
@@ -556,24 +616,12 @@ class TranscludeToken extends Token {
556
616
  * @throws `Error` 仅用于模板
557
617
  */
558
618
  hasDuplicatedArgs() {
559
- if (this.matches('template, magic-word#invoke')) {
619
+ if (this.isTemplate()) {
560
620
  return this.getAllArgs().length - this.getKeys().length;
561
621
  }
562
622
  throw new Error(`${this.constructor.name}.hasDuplicatedArgs 方法仅供模板使用!`);
563
623
  }
564
624
 
565
- /**
566
- * 获取重名参数
567
- * @complexity `n`
568
- * @throws `Error` 仅用于模板
569
- */
570
- 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)]);
573
- }
574
- throw new Error(`${this.constructor.name}.getDuplicatedArgs 方法仅供模板使用!`);
575
- }
576
-
577
625
  /**
578
626
  * 修复重名参数:
579
627
  * `aggressive = false`时只移除空参数和全同参数,优先保留匿名参数,否则将所有匿名参数更改为命名。
@@ -594,7 +642,7 @@ class TranscludeToken extends Token {
594
642
  const /** @type {Record<string, ParameterToken[]>} */ values = {};
595
643
  for (const arg of args) {
596
644
  const val = arg.getValue().trim();
597
- if (val in values) {
645
+ if (Object.hasOwn(values, val)) {
598
646
  values[val].push(arg);
599
647
  } else {
600
648
  values[val] = [arg];
@@ -652,7 +700,7 @@ class TranscludeToken extends Token {
652
700
  if (remaining > 1) {
653
701
  Parser.error(`${this.type === 'template'
654
702
  ? this.name
655
- : this.normalizeTitle(this.children[1]?.text() ?? '', 828).title
703
+ : this.normalizeTitle(this.childNodes[1]?.text() ?? '', 828).title
656
704
  } 还留有 ${remaining} 个重复的 ${key} 参数:${[...this.getArgs(key)].map(arg => {
657
705
  const {top, left} = arg.getBoundingClientRect();
658
706
  return `第 ${top} 行第 ${left} 列`;
@@ -680,7 +728,7 @@ class TranscludeToken extends Token {
680
728
  config = this.getAttribute('config'),
681
729
  parsed = Parser.parse(stripped, include, 4, config);
682
730
  const TableToken = require('./table');
683
- for (const table of parsed.children) {
731
+ for (const table of parsed.childNodes) {
684
732
  if (table instanceof TableToken) {
685
733
  table.escape();
686
734
  }