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