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