wikiparser-node 0.8.1 → 0.9.0-m

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 (83) hide show
  1. package/config/default.json +0 -1
  2. package/config/llwiki.json +0 -35
  3. package/config/moegirl.json +1 -44
  4. package/config/zhwiki.json +0 -466
  5. package/i18n/zh-hans.json +44 -0
  6. package/i18n/zh-hant.json +44 -0
  7. package/index.js +16 -256
  8. package/lib/element.js +29 -504
  9. package/lib/node.js +6 -542
  10. package/lib/text.js +10 -127
  11. package/lib/title.js +7 -32
  12. package/mixin/hidden.js +0 -3
  13. package/package.json +8 -8
  14. package/parser/brackets.js +8 -3
  15. package/parser/commentAndExt.js +1 -5
  16. package/parser/converter.js +0 -1
  17. package/parser/externalLinks.js +0 -1
  18. package/parser/hrAndDoubleUnderscore.js +0 -1
  19. package/parser/html.js +0 -1
  20. package/parser/links.js +5 -6
  21. package/parser/list.js +0 -1
  22. package/parser/magicLinks.js +0 -1
  23. package/parser/quotes.js +1 -2
  24. package/parser/table.js +0 -1
  25. package/src/arg.js +9 -119
  26. package/src/atom/hidden.js +0 -2
  27. package/src/atom/index.js +0 -17
  28. package/src/attribute.js +7 -181
  29. package/src/attributes.js +8 -312
  30. package/src/converter.js +2 -108
  31. package/src/converterFlags.js +3 -190
  32. package/src/converterRule.js +3 -187
  33. package/src/extLink.js +2 -121
  34. package/src/gallery.js +5 -62
  35. package/src/hasNowiki/index.js +0 -12
  36. package/src/hasNowiki/pre.js +0 -12
  37. package/src/heading.js +7 -58
  38. package/src/html.js +18 -127
  39. package/src/imageParameter.js +61 -200
  40. package/src/imagemap.js +7 -66
  41. package/src/imagemapLink.js +2 -14
  42. package/src/index.js +11 -534
  43. package/src/link/category.js +1 -37
  44. package/src/link/file.js +18 -168
  45. package/src/link/galleryImage.js +6 -64
  46. package/src/link/index.js +9 -277
  47. package/src/magicLink.js +2 -85
  48. package/src/nested/choose.js +0 -1
  49. package/src/nested/combobox.js +0 -1
  50. package/src/nested/index.js +3 -33
  51. package/src/nested/references.js +0 -1
  52. package/src/nowiki/comment.js +3 -27
  53. package/src/nowiki/dd.js +1 -47
  54. package/src/nowiki/doubleUnderscore.js +1 -31
  55. package/src/nowiki/hr.js +1 -20
  56. package/src/nowiki/index.js +3 -25
  57. package/src/nowiki/list.js +2 -5
  58. package/src/nowiki/noinclude.js +0 -14
  59. package/src/nowiki/quote.js +3 -17
  60. package/src/onlyinclude.js +1 -26
  61. package/src/paramTag/index.js +4 -27
  62. package/src/paramTag/inputbox.js +1 -4
  63. package/src/parameter.js +7 -149
  64. package/src/syntax.js +0 -68
  65. package/src/table/index.js +4 -942
  66. package/src/table/td.js +9 -230
  67. package/src/table/tr.js +4 -249
  68. package/src/tagPair/ext.js +13 -27
  69. package/src/tagPair/include.js +0 -24
  70. package/src/tagPair/index.js +2 -51
  71. package/src/transclude.js +38 -528
  72. package/util/lint.js +8 -7
  73. package/util/string.js +12 -44
  74. package/README.md +0 -39
  75. package/lib/ranges.js +0 -130
  76. package/mixin/attributeParent.js +0 -117
  77. package/mixin/fixedToken.js +0 -40
  78. package/mixin/singleLine.js +0 -31
  79. package/mixin/sol.js +0 -65
  80. package/parser/selector.js +0 -177
  81. package/src/charinsert.js +0 -97
  82. package/tool/index.js +0 -1202
  83. package/util/debug.js +0 -73
package/src/transclude.js CHANGED
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const {removeComment, escapeRegExp, text, noWrap, print} = require('../util/string'),
4
- {externalUse} = require('../util/debug'),
3
+ const {removeComment, text, decodeHtml} = require('../util/string'),
5
4
  {generateForChild} = require('../util/lint'),
6
5
  Parser = require('..'),
7
6
  Token = require('.'),
@@ -17,14 +16,9 @@ class TranscludeToken extends Token {
17
16
  type = 'template';
18
17
  modifier = '';
19
18
  /** @type {Record<string, Set<ParameterToken>>} */ #args = {};
20
- /** @type {Set<string>} */ #keys = new Set();
21
- #fragment = '';
19
+ #fragment;
22
20
  #valid = true;
23
-
24
- /** 是否存在重复参数 */
25
- get duplication() {
26
- return this.isTemplate() && Boolean(this.hasDuplicatedArgs());
27
- }
21
+ #raw = false;
28
22
 
29
23
  /**
30
24
  * 设置引用修饰符
@@ -32,18 +26,17 @@ class TranscludeToken extends Token {
32
26
  * @complexity `n`
33
27
  */
34
28
  setModifier(modifier = '') {
35
- if (typeof modifier !== 'string') {
36
- this.typeError('setModifier', 'String');
37
- }
38
29
  const {parserFunction: [,, raw, subst]} = this.getAttribute('config'),
39
- lcModifier = modifier.trimStart().toLowerCase(),
40
- isRaw = raw.includes(lcModifier),
41
- isSubst = subst.includes(lcModifier),
42
- wasRaw = raw.includes(this.modifier.trimStart().toLowerCase());
43
- if (wasRaw && isRaw || !wasRaw && (isSubst || modifier === '')
44
- || (Parser.running || this.length > 1) && (isRaw || isSubst || modifier === '')
45
- ) {
30
+ lcModifier = removeComment(modifier).trim();
31
+ if (modifier && !lcModifier.endsWith(':')) {
32
+ return false;
33
+ }
34
+ const magicWord = lcModifier.slice(0, -1).toLowerCase(),
35
+ isRaw = raw.includes(magicWord),
36
+ isSubst = subst.includes(magicWord);
37
+ if (isRaw || isSubst || modifier === '') {
46
38
  this.setAttribute('modifier', modifier);
39
+ this.#raw = isRaw;
47
40
  return Boolean(modifier);
48
41
  }
49
42
  return false;
@@ -58,17 +51,20 @@ class TranscludeToken extends Token {
58
51
  */
59
52
  constructor(title, parts, config = Parser.getConfig(), accum = []) {
60
53
  super(undefined, config, true, accum, {
61
- AtomToken: 0, SyntaxToken: 0, ParameterToken: '1:',
62
54
  });
63
- const {parserFunction: [insensitive, sensitive, raw]} = config;
64
- this.seal('modifier');
65
- if (title.includes(':')) {
66
- const [modifier, ...arg] = title.split(':');
67
- if (this.setModifier(modifier)) {
68
- title = arg.join(':');
55
+ const {parserFunction: [insensitive, sensitive]} = config,
56
+ argSubst = /^(?:\s|\0\d+c\x7F)*\0\d+s\x7F/u.exec(title)?.[0];
57
+ if (argSubst) {
58
+ this.setAttribute('modifier', argSubst);
59
+ title = title.slice(argSubst.length);
60
+ } else if (title.includes(':')) {
61
+ const [modifier, ...arg] = title.split(':'),
62
+ [mt] = /^(?:\s|\0\d+c\x7F)*/u.exec(arg[0] ?? '');
63
+ if (this.setModifier(`${modifier}:${mt}`)) {
64
+ title = arg.join(':').slice(mt.length);
69
65
  }
70
66
  }
71
- if (title.includes(':') || parts.length === 0 && !raw.includes(this.modifier.toLowerCase())) {
67
+ if (title.includes(':') || parts.length === 0 && !this.#raw) {
72
68
  const [magicWord, ...arg] = title.split(':'),
73
69
  cleaned = removeComment(magicWord),
74
70
  name = cleaned[arg.length > 0 ? 'trimStart' : 'trim'](),
@@ -78,14 +74,12 @@ class TranscludeToken extends Token {
78
74
  this.setAttribute('name', canonicalCame || name.toLowerCase()).type = 'magic-word';
79
75
  const pattern = new RegExp(`^\\s*${name}\\s*$`, isSensitive ? 'u' : 'iu'),
80
76
  token = new SyntaxToken(magicWord, pattern, 'magic-word-name', config, accum, {
81
- 'Stage-1': ':', '!ExtToken': '',
82
77
  });
83
78
  this.insertAt(token);
84
79
  if (arg.length > 0) {
85
80
  parts.unshift([arg.join(':')]);
86
81
  }
87
82
  if (this.name === 'invoke') {
88
- this.setAttribute('acceptable', {SyntaxToken: 0, AtomToken: '1:3', ParameterToken: '3:'});
89
83
  for (let i = 0; i < 2; i++) {
90
84
  const part = parts.shift();
91
85
  if (!part) {
@@ -94,22 +88,19 @@ class TranscludeToken extends Token {
94
88
  const invoke = new AtomToken(part.join('='), `invoke-${
95
89
  i ? 'function' : 'module'
96
90
  }`, config, accum, {
97
- 'Stage-1': ':', '!ExtToken': '',
98
91
  });
99
92
  this.insertAt(invoke);
100
93
  }
101
- this.getAttribute('protectChildren')('1:3');
102
94
  }
103
95
  }
104
96
  }
105
97
  if (this.type === 'template') {
106
- const name = removeComment(title).split('#')[0].trim();
98
+ const name = removeComment(decodeHtml(title)).split('#')[0].trim();
107
99
  if (!name || /\0\d+[eh!+-]\x7F|[<>[\]{}\n]|%[\da-f]{2}/u.test(name)) {
108
100
  accum.pop();
109
- throw new SyntaxError(`非法的模板名称:${noWrap(name)}`);
101
+ throw new SyntaxError(`非法的模板名称:${name}`);
110
102
  }
111
103
  const token = new AtomToken(title, 'template-name', config, accum, {
112
- 'Stage-2': ':', '!HeadingToken': '',
113
104
  });
114
105
  this.insertAt(token);
115
106
  }
@@ -127,62 +118,27 @@ class TranscludeToken extends Token {
127
118
  }
128
119
  this.insertAt(new ParameterToken(...part, config, accum));
129
120
  }
130
- this.getAttribute('protectChildren')(0);
131
121
  }
132
122
 
133
123
  /** @override */
134
124
  afterBuild() {
125
+ if (this.modifier.includes('\0')) {
126
+ this.setAttribute('modifier', this.getAttribute('buildFromStr')(this.modifier, 'string'));
127
+ }
135
128
  if (this.isTemplate()) {
136
129
  const isTemplate = this.type === 'template',
137
130
  titleObj = this.normalizeTitle(this.childNodes[isTemplate ? 0 : 1].text(), isTemplate ? 10 : 828);
138
- this.setAttribute(isTemplate ? 'name' : 'module', titleObj.title);
139
131
  this.#fragment = titleObj.fragment;
140
132
  this.#valid = titleObj.valid;
141
-
142
- /**
143
- * 当事件bubble到`parameter`时,将`oldKey`和`newKey`保存进AstEventData。
144
- * 当继续bubble到`template`时,处理并删除`oldKey`和`newKey`。
145
- * @type {AstListener}
146
- */
147
- const transcludeListener = (e, data) => {
148
- const {prevTarget} = e,
149
- {oldKey, newKey} = data ?? {};
150
- if (typeof oldKey === 'string') {
151
- delete data.oldKey;
152
- delete data.newKey;
153
- }
154
- if (prevTarget === this.firstChild && isTemplate
155
- || prevTarget === this.childNodes[1] && !isTemplate && this.name === 'invoke'
156
- ) {
157
- const name = prevTarget.text(),
158
- {title, fragment, valid} = this.normalizeTitle(name, 10);
159
- this.setAttribute(isTemplate ? 'name' : 'module', title);
160
- this.#fragment = fragment;
161
- this.#valid = valid;
162
- } else if (oldKey !== newKey && prevTarget instanceof ParameterToken) {
163
- const oldArgs = this.getArgs(oldKey, false, false);
164
- oldArgs.delete(prevTarget);
165
- this.getArgs(newKey, false, false).add(prevTarget);
166
- this.#keys.add(newKey);
167
- if (oldArgs.size === 0) {
168
- this.#keys.delete(oldKey);
169
- }
170
- }
171
- };
172
- this.addEventListener(['remove', 'insert', 'replace', 'text'], transcludeListener);
173
133
  }
174
134
  }
175
135
 
176
136
  /**
177
137
  * @override
178
- * @param {string} selector
179
138
  */
180
139
  toString(selector) {
181
- if (selector && this.matches(selector)) {
182
- return '';
183
- }
184
140
  const {childNodes, firstChild, modifier} = this;
185
- return `{{${modifier}${modifier && ':'}${
141
+ return `{{${modifier}${
186
142
  this.type === 'magic-word'
187
143
  ? `${String(firstChild)}${childNodes.length > 1 ? ':' : ''}${childNodes.slice(1).map(String).join('|')}`
188
144
  : super.toString(selector, '|')
@@ -198,7 +154,7 @@ class TranscludeToken extends Token {
198
154
  const {childNodes, firstChild, modifier, type, name} = this;
199
155
  return type === 'magic-word' && name === 'vardefine'
200
156
  ? ''
201
- : `{{${modifier}${modifier && ':'}${
157
+ : `{{${modifier}${
202
158
  this.type === 'magic-word'
203
159
  ? `${firstChild.text()}${childNodes.length > 1 ? ':' : ''}${text(childNodes.slice(1), '|')}`
204
160
  : super.text('|')
@@ -207,7 +163,7 @@ class TranscludeToken extends Token {
207
163
 
208
164
  /** @override */
209
165
  getPadding() {
210
- return this.modifier ? this.modifier.length + 3 : 2;
166
+ return this.modifier.length + 2;
211
167
  }
212
168
 
213
169
  /** @override */
@@ -215,39 +171,29 @@ class TranscludeToken extends Token {
215
171
  return 1;
216
172
  }
217
173
 
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
174
  /**
229
175
  * @override
230
176
  * @param {number} start 起始位置
231
177
  */
232
- lint(start = 0) {
178
+ lint(start = this.getAbsoluteIndex()) {
233
179
  const errors = super.lint(start),
234
180
  {type, childNodes} = this;
235
181
  let rect;
236
182
  if (!this.isTemplate()) {
237
183
  return errors;
238
- } else if (this.#fragment) {
184
+ } else if (this.#fragment !== undefined) {
239
185
  rect = {start, ...this.getRootNode().posFromIndex(start)};
240
- errors.push(generateForChild(childNodes[type === 'template' ? 0 : 1], rect, '多余的fragment'));
186
+ errors.push(generateForChild(childNodes[type === 'template' ? 0 : 1], rect, 'useless fragment'));
241
187
  }
242
188
  if (!this.#valid) {
243
189
  rect = {start, ...this.getRootNode().posFromIndex(start)};
244
- errors.push(generateForChild(childNodes[1], rect, '非法的模块名称'));
190
+ errors.push(generateForChild(childNodes[1], rect, 'illegal module name'));
245
191
  }
246
192
  const duplicatedArgs = this.getDuplicatedArgs();
247
193
  if (duplicatedArgs.length > 0) {
248
194
  rect ||= {start, ...this.getRootNode().posFromIndex(start)};
249
195
  errors.push(...duplicatedArgs.flatMap(([, args]) => args).map(
250
- arg => generateForChild(arg, rect, '重复参数'),
196
+ arg => generateForChild(arg, rect, 'duplicated parameter'),
251
197
  ));
252
198
  }
253
199
  return errors;
@@ -265,23 +211,13 @@ class TranscludeToken extends Token {
265
211
  */
266
212
  #handleAnonArgChange(addedToken) {
267
213
  const args = this.getAnonArgs(),
268
- added = typeof addedToken !== 'number',
269
- maxAnon = String(args.length + (added ? 0 : 1));
270
- if (added) {
271
- this.#keys.add(maxAnon);
272
- } else if (!this.hasArg(maxAnon, true)) {
273
- this.#keys.delete(maxAnon);
274
- }
275
- const j = added ? args.indexOf(addedToken) : addedToken - 1;
214
+ j = args.indexOf(addedToken);
276
215
  for (let i = j; i < args.length; i++) {
277
216
  const token = args[i],
278
217
  {name} = token,
279
218
  newName = String(i + 1);
280
219
  if (name !== newName) {
281
220
  this.getArgs(newName, false, false).add(token.setAttribute('name', newName));
282
- if (name) {
283
- this.getArgs(name, false, false).delete(token);
284
- }
285
221
  }
286
222
  }
287
223
  }
@@ -298,7 +234,6 @@ class TranscludeToken extends Token {
298
234
  this.#handleAnonArgChange(token);
299
235
  } else if (token.name) {
300
236
  this.getArgs(token.name, false, false).add(token);
301
- this.#keys.add(token.name);
302
237
  }
303
238
  return token;
304
239
  }
@@ -328,10 +263,6 @@ class TranscludeToken extends Token {
328
263
  * @complexity `n`
329
264
  */
330
265
  getArgs(key, exact, copy = true) {
331
- if (typeof key !== 'string' && typeof key !== 'number') {
332
- this.typeError('getArgs', 'String', 'Number');
333
- }
334
- copy ||= !Parser.debugging && externalUse('getArgs');
335
266
  const keyStr = String(key).replace(/^[ \t\n\0\v]+|(?<=[^ \t\n\0\v])[ \t\n\0\v]+$/gu, '');
336
267
  let args;
337
268
  if (Object.hasOwn(this.#args, keyStr)) {
@@ -340,11 +271,6 @@ class TranscludeToken extends Token {
340
271
  args = new Set(this.getAllArgs().filter(({name}) => keyStr === name));
341
272
  this.#args[keyStr] = args;
342
273
  }
343
- if (exact && !isNaN(keyStr)) {
344
- args = new Set([...args].filter(({anon}) => typeof key === 'number' === anon));
345
- } else if (copy) {
346
- args = new Set(args);
347
- }
348
274
  return args;
349
275
  }
350
276
 
@@ -352,14 +278,13 @@ class TranscludeToken extends Token {
352
278
  * 获取重名参数
353
279
  * @complexity `n`
354
280
  * @returns {[string, ParameterToken[]][]}
355
- * @throws `Error` 仅用于模板
356
281
  */
357
282
  getDuplicatedArgs() {
358
283
  if (this.isTemplate()) {
359
284
  return Object.entries(this.#args).filter(([, {size}]) => size > 1)
360
285
  .map(([key, args]) => [key, [...args]]);
361
286
  }
362
- throw new Error(`${this.constructor.name}.getDuplicatedArgs 方法仅供模板使用!`);
287
+ return [];
363
288
  }
364
289
 
365
290
  /**
@@ -406,421 +331,6 @@ class TranscludeToken extends Token {
406
331
  }
407
332
  return queue;
408
333
  }
409
-
410
- /** @override */
411
- cloneNode() {
412
- const [first, ...cloned] = this.cloneChildNodes(),
413
- config = this.getAttribute('config');
414
- return Parser.run(() => {
415
- const token = new TranscludeToken(this.type === 'template' ? '' : first.text(), [], config);
416
- token.setModifier(this.modifier);
417
- token.firstChild.safeReplaceWith(first);
418
- token.afterBuild();
419
- token.append(...cloned);
420
- return token;
421
- });
422
- }
423
-
424
- /** 替换引用 */
425
- subst() {
426
- this.setModifier('subst');
427
- }
428
-
429
- /** 安全的替换引用 */
430
- safesubst() {
431
- this.setModifier('safesubst');
432
- }
433
-
434
- /**
435
- * @override
436
- * @param {PropertyKey} key 属性键
437
- */
438
- hasAttribute(key) {
439
- return key === 'keys' || super.hasAttribute(key);
440
- }
441
-
442
- /**
443
- * @override
444
- * @template {string} T
445
- * @param {T} key 属性键
446
- * @returns {TokenAttribute<T>}
447
- */
448
- getAttribute(key) {
449
- if (key === 'args') {
450
- return {...this.#args};
451
- } else if (key === 'keys') {
452
- return !Parser.debugging && externalUse('getAttribute') ? new Set(this.#keys) : this.#keys;
453
- }
454
- return super.getAttribute(key);
455
- }
456
-
457
- /**
458
- * @override
459
- * @param {number} i 移除位置
460
- * @complexity `n`
461
- */
462
- removeAt(i) {
463
- const /** @type {ParameterToken} */ token = super.removeAt(i);
464
- if (token.anon) {
465
- this.#handleAnonArgChange(Number(token.name));
466
- } else {
467
- const args = this.getArgs(token.name, false, false);
468
- args.delete(token);
469
- if (args.size === 0) {
470
- this.#keys.delete(token.name);
471
- }
472
- }
473
- return token;
474
- }
475
-
476
- /**
477
- * 是否具有某参数
478
- * @param {string|number} key 参数名
479
- * @param {boolean} exact 是否匹配匿名性
480
- * @complexity `n`
481
- */
482
- hasArg(key, exact) {
483
- return this.getArgs(key, exact, false).size > 0;
484
- }
485
-
486
- /**
487
- * 获取生效的指定参数
488
- * @param {string|number} key 参数名
489
- * @param {boolean} exact 是否匹配匿名性
490
- * @complexity `n`
491
- */
492
- getArg(key, exact) {
493
- return [...this.getArgs(key, exact, false)].sort((a, b) => a.compareDocumentPosition(b)).at(-1);
494
- }
495
-
496
- /**
497
- * 移除指定参数
498
- * @param {string|number} key 参数名
499
- * @param {boolean} exact 是否匹配匿名性
500
- * @complexity `n`
501
- */
502
- removeArg(key, exact) {
503
- Parser.run(() => {
504
- for (const token of this.getArgs(key, exact, false)) {
505
- this.removeChild(token);
506
- }
507
- });
508
- }
509
-
510
- /**
511
- * 获取所有参数名
512
- * @complexity `n`
513
- */
514
- getKeys() {
515
- const args = this.getAllArgs();
516
- if (this.#keys.size === 0 && args.length > 0) {
517
- for (const {name} of args) {
518
- this.#keys.add(name);
519
- }
520
- }
521
- return [...this.#keys];
522
- }
523
-
524
- /**
525
- * 获取参数值
526
- * @param {string|number} key 参数名
527
- * @complexity `n`
528
- */
529
- getValues(key) {
530
- return [...this.getArgs(key, false, false)].map(token => token.getValue());
531
- }
532
-
533
- /**
534
- * 获取生效的参数值
535
- * @template {string|number|undefined} T
536
- * @param {T} key 参数名
537
- * @returns {T extends undefined ? Record<string, string> : string}
538
- * @complexity `n`
539
- */
540
- getValue(key) {
541
- return key === undefined
542
- ? Object.fromEntries(this.getKeys().map(k => [k, this.getValue(k)]))
543
- : this.getArg(key)?.getValue();
544
- }
545
-
546
- /**
547
- * 插入匿名参数
548
- * @param {string} val 参数值
549
- * @returns {ParameterToken}
550
- * @complexity `n`
551
- * @throws `SyntaxError` 非法的匿名参数
552
- */
553
- newAnonArg(val) {
554
- val = String(val);
555
- const templateLike = this.isTemplate(),
556
- wikitext = `{{${templateLike ? ':T|' : 'lc:'}${val}}}`,
557
- root = Parser.parse(wikitext, this.getAttribute('include'), 2, this.getAttribute('config')),
558
- {length, firstChild: transclude} = root,
559
- /** @type {Token & {lastChild: ParameterToken}} */
560
- {type, name, length: transcludeLength, lastChild} = transclude,
561
- targetType = templateLike ? 'template' : 'magic-word',
562
- targetName = templateLike ? 'T' : 'lc';
563
- if (length === 1 && type === targetType && name === targetName && transcludeLength === 2 && lastChild.anon) {
564
- return this.insertAt(lastChild);
565
- }
566
- throw new SyntaxError(`非法的匿名参数:${noWrap(val)}`);
567
- }
568
-
569
- /**
570
- * 设置参数值
571
- * @param {string} key 参数名
572
- * @param {string} value 参数值
573
- * @complexity `n`
574
- * @throws `Error` 仅用于模板
575
- * @throws `SyntaxError` 非法的命名参数
576
- */
577
- setValue(key, value) {
578
- if (typeof key !== 'string') {
579
- this.typeError('setValue', 'String');
580
- } else if (!this.isTemplate()) {
581
- throw new Error(`${this.constructor.name}.setValue 方法仅供模板使用!`);
582
- }
583
- const token = this.getArg(key);
584
- value = String(value);
585
- if (token) {
586
- token.setValue(value);
587
- return;
588
- }
589
- const wikitext = `{{:T|${key}=${value}}}`,
590
- root = Parser.parse(wikitext, this.getAttribute('include'), 2, this.getAttribute('config')),
591
- {length, firstChild: template} = root,
592
- {type, name, length: templateLength, lastChild: parameter} = template;
593
- if (length !== 1 || type !== 'template' || name !== 'T' || templateLength !== 2 || parameter.name !== key) {
594
- throw new SyntaxError(`非法的命名参数:${key}=${noWrap(value)}`);
595
- }
596
- this.insertAt(parameter);
597
- }
598
-
599
- /**
600
- * 将匿名参数改写为命名参数
601
- * @complexity `n`
602
- * @throws `Error` 仅用于模板
603
- */
604
- anonToNamed() {
605
- if (!this.isTemplate()) {
606
- throw new Error(`${this.constructor.name}.anonToNamed 方法仅供模板使用!`);
607
- }
608
- for (const token of this.getAnonArgs()) {
609
- token.firstChild.replaceChildren(token.name);
610
- }
611
- }
612
-
613
- /**
614
- * 替换模板名
615
- * @param {string} title 模板名
616
- * @throws `Error` 仅用于模板
617
- * @throws `SyntaxError` 非法的模板名称
618
- */
619
- replaceTemplate(title) {
620
- if (this.type === 'magic-word') {
621
- throw new Error(`${this.constructor.name}.replaceTemplate 方法仅用于更换模板!`);
622
- } else if (typeof title !== 'string') {
623
- this.typeError('replaceTemplate', 'String');
624
- }
625
- const root = Parser.parse(`{{${title}}}`, this.getAttribute('include'), 2, this.getAttribute('config')),
626
- {length, firstChild: template} = root;
627
- if (length !== 1 || template.type !== 'template' || template.length !== 1) {
628
- throw new SyntaxError(`非法的模板名称:${title}`);
629
- }
630
- this.firstChild.replaceChildren(...template.firstChild.childNodes);
631
- }
632
-
633
- /**
634
- * 替换模块名
635
- * @param {string} title 模块名
636
- * @throws `Error` 仅用于模块
637
- * @throws `SyntaxError` 非法的模块名称
638
- */
639
- replaceModule(title) {
640
- if (this.type !== 'magic-word' || this.name !== 'invoke') {
641
- throw new Error(`${this.constructor.name}.replaceModule 方法仅用于更换模块!`);
642
- } else if (typeof title !== 'string') {
643
- this.typeError('replaceModule', 'String');
644
- }
645
- const root = Parser.parse(`{{#invoke:${title}}}`, this.getAttribute('include'), 2, this.getAttribute('config')),
646
- {length, firstChild: invoke} = root,
647
- {type, name, length: invokeLength, lastChild} = invoke;
648
- if (length !== 1 || type !== 'magic-word' || name !== 'invoke' || invokeLength !== 2) {
649
- throw new SyntaxError(`非法的模块名称:${title}`);
650
- } else if (this.length > 1) {
651
- this.childNodes[1].replaceChildren(...lastChild.childNodes);
652
- } else {
653
- invoke.destroy(true);
654
- this.insertAt(lastChild);
655
- }
656
- }
657
-
658
- /**
659
- * 替换模块函数
660
- * @param {string} func 模块函数名
661
- * @throws `Error` 仅用于模块
662
- * @throws `Error` 尚未指定模块名称
663
- * @throws `SyntaxError` 非法的模块函数名
664
- */
665
- replaceFunction(func) {
666
- if (this.type !== 'magic-word' || this.name !== 'invoke') {
667
- throw new Error(`${this.constructor.name}.replaceModule 方法仅用于更换模块!`);
668
- } else if (typeof func !== 'string') {
669
- this.typeError('replaceFunction', 'String');
670
- } else if (this.length < 2) {
671
- throw new Error('尚未指定模块名称!');
672
- }
673
- const root = Parser.parse(
674
- `{{#invoke:M|${func}}}`, this.getAttribute('include'), 2, this.getAttribute('config'),
675
- ),
676
- {length, firstChild: invoke} = root,
677
- {type, name, length: invokeLength, lastChild} = invoke;
678
- if (length !== 1 || type !== 'magic-word' || name !== 'invoke' || invokeLength !== 3) {
679
- throw new SyntaxError(`非法的模块函数名:${func}`);
680
- } else if (this.length > 2) {
681
- this.childNodes[2].replaceChildren(...lastChild.childNodes);
682
- } else {
683
- invoke.destroy(true);
684
- this.insertAt(lastChild);
685
- }
686
- }
687
-
688
- /**
689
- * 是否存在重名参数
690
- * @complexity `n`
691
- * @throws `Error` 仅用于模板
692
- */
693
- hasDuplicatedArgs() {
694
- if (this.isTemplate()) {
695
- return this.getAllArgs().length - this.getKeys().length;
696
- }
697
- throw new Error(`${this.constructor.name}.hasDuplicatedArgs 方法仅供模板使用!`);
698
- }
699
-
700
- /**
701
- * 修复重名参数:
702
- * `aggressive = false`时只移除空参数和全同参数,优先保留匿名参数,否则将所有匿名参数更改为命名。
703
- * `aggressive = true`时还会尝试处理连续的以数字编号的参数。
704
- * @param {boolean} aggressive 是否使用有更大风险的修复手段
705
- * @complexity `n²`
706
- */
707
- fixDuplication(aggressive) {
708
- if (!this.hasDuplicatedArgs()) {
709
- return [];
710
- }
711
- const /** @type {string[]} */ duplicatedKeys = [];
712
- let {length: anonCount} = this.getAnonArgs();
713
- for (const [key, args] of this.getDuplicatedArgs()) {
714
- if (args.length <= 1) {
715
- continue;
716
- }
717
- const /** @type {Record<string, ParameterToken[]>} */ values = {};
718
- for (const arg of args) {
719
- const val = arg.getValue().trim();
720
- if (Object.hasOwn(values, val)) {
721
- values[val].push(arg);
722
- } else {
723
- values[val] = [arg];
724
- }
725
- }
726
- let noMoreAnon = anonCount === 0 || isNaN(key);
727
- const emptyArgs = values[''] ?? [],
728
- duplicatedArgs = Object.entries(values).filter(([val, {length}]) => val && length > 1)
729
- .flatMap(([, curArgs]) => {
730
- const anonIndex = noMoreAnon ? -1 : curArgs.findIndex(({anon}) => anon);
731
- if (anonIndex !== -1) {
732
- noMoreAnon = true;
733
- }
734
- curArgs.splice(anonIndex, 1);
735
- return curArgs;
736
- }),
737
- badArgs = [...emptyArgs, ...duplicatedArgs],
738
- index = noMoreAnon ? -1 : emptyArgs.findIndex(({anon}) => anon);
739
- if (badArgs.length === args.length) {
740
- badArgs.splice(index, 1);
741
- } else if (index !== -1) {
742
- this.anonToNamed();
743
- anonCount = 0;
744
- }
745
- for (const arg of badArgs) {
746
- arg.remove();
747
- }
748
- let remaining = args.length - badArgs.length;
749
- if (remaining === 1) {
750
- continue;
751
- } else if (aggressive && (anonCount ? /\D\d+$/u : /(?:^|\D)\d+$/u).test(key)) {
752
- let /** @type {number} */ last;
753
- const str = key.slice(0, -/(?<!\d)\d+$/u.exec(key)[0].length),
754
- regex = new RegExp(`^${escapeRegExp(str)}\\d+$`, 'u'),
755
- series = this.getAllArgs().filter(({name}) => regex.test(name)),
756
- ordered = series.every(({name}, i) => {
757
- const j = Number(name.slice(str.length)),
758
- cmp = j <= i + 1 && (i === 0 || j >= last || name === key);
759
- last = j;
760
- return cmp;
761
- });
762
- if (ordered) {
763
- for (let i = 0; i < series.length; i++) {
764
- const name = `${str}${i + 1}`,
765
- arg = series[i];
766
- if (arg.name !== name) {
767
- if (arg.name === key) {
768
- remaining--;
769
- }
770
- arg.rename(name, true);
771
- }
772
- }
773
- }
774
- }
775
- if (remaining > 1) {
776
- Parser.error(`${this.type === 'template'
777
- ? this.name
778
- : this.normalizeTitle(this.childNodes[1]?.text() ?? '', 828).title
779
- } 还留有 ${remaining} 个重复的 ${key} 参数:${[...this.getArgs(key)].map(arg => {
780
- const {top, left} = arg.getBoundingClientRect();
781
- return `第 ${top} 行第 ${left} 列`;
782
- }).join('、')}`);
783
- duplicatedKeys.push(key);
784
- continue;
785
- }
786
- }
787
- return duplicatedKeys;
788
- }
789
-
790
- /**
791
- * 转义模板内的表格
792
- * @returns {TranscludeToken}
793
- * @complexity `n`
794
- * @throws `Error` 转义失败
795
- */
796
- escapeTables() {
797
- const count = this.hasDuplicatedArgs();
798
- if (!/\n[^\S\n]*(?::+\s*)?\{\|[^\n]*\n\s*(?:\S[^\n]*\n\s*)*\|\}/u.test(this.text()) || !count) {
799
- return this;
800
- }
801
- const stripped = String(this).slice(2, -2),
802
- include = this.getAttribute('include'),
803
- config = this.getAttribute('config'),
804
- parsed = Parser.parse(stripped, include, 4, config);
805
- const TableToken = require('./table');
806
- for (const table of parsed.childNodes) {
807
- if (table instanceof TableToken) {
808
- table.escape();
809
- }
810
- }
811
- const {firstChild, childNodes} = Parser.parse(`{{${String(parsed)}}}`, include, 2, config);
812
- if (childNodes.length !== 1 || !(firstChild instanceof TranscludeToken)) {
813
- throw new Error('转义表格失败!');
814
- }
815
- const newCount = firstChild.hasDuplicatedArgs();
816
- if (newCount === count) {
817
- return this;
818
- }
819
- Parser.info(`共修复了 ${count - newCount} 个重复参数。`);
820
- this.safeReplaceWith(firstChild);
821
- return firstChild;
822
- }
823
334
  }
824
335
 
825
- Parser.classes.TranscludeToken = __filename;
826
336
  module.exports = TranscludeToken;