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