wikiparser-node 0.8.1-m → 0.9.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/moegirl.json +1 -0
- package/i18n/zh-hans.json +44 -0
- package/i18n/zh-hant.json +44 -0
- package/index.js +264 -10
- package/lib/element.js +507 -33
- package/lib/node.js +550 -6
- package/lib/ranges.js +130 -0
- package/lib/text.js +111 -41
- package/lib/title.js +28 -5
- 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 +54 -0
- package/package.json +9 -8
- package/parser/brackets.js +9 -2
- package/parser/commentAndExt.js +3 -5
- 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 +6 -5
- package/parser/list.js +1 -0
- package/parser/magicLinks.js +5 -4
- package/parser/quotes.js +1 -0
- package/parser/selector.js +177 -0
- package/parser/table.js +1 -0
- package/src/arg.js +123 -5
- package/src/atom/hidden.js +2 -0
- package/src/atom/index.js +17 -0
- package/src/attribute.js +191 -8
- package/src/attributes.js +311 -8
- package/src/charinsert.js +97 -0
- package/src/converter.js +108 -2
- package/src/converterFlags.js +190 -3
- package/src/converterRule.js +185 -4
- package/src/extLink.js +122 -2
- package/src/gallery.js +59 -11
- package/src/hasNowiki/index.js +12 -0
- package/src/hasNowiki/pre.js +12 -0
- package/src/heading.js +57 -6
- package/src/html.js +133 -12
- package/src/imageParameter.js +232 -38
- package/src/imagemap.js +65 -6
- package/src/imagemapLink.js +14 -2
- package/src/index.js +537 -8
- package/src/link/category.js +32 -1
- package/src/link/file.js +173 -11
- package/src/link/galleryImage.js +63 -5
- package/src/link/index.js +268 -9
- package/src/magicLink.js +92 -11
- package/src/nested/choose.js +1 -0
- package/src/nested/combobox.js +1 -0
- package/src/nested/index.js +31 -7
- package/src/nested/references.js +1 -0
- package/src/nowiki/comment.js +27 -3
- 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 +25 -3
- package/src/nowiki/list.js +5 -2
- package/src/nowiki/noinclude.js +14 -0
- package/src/nowiki/quote.js +17 -3
- package/src/onlyinclude.js +26 -1
- package/src/paramTag/index.js +27 -4
- package/src/paramTag/inputbox.js +4 -1
- package/src/parameter.js +150 -8
- package/src/syntax.js +68 -0
- package/src/table/index.js +941 -4
- package/src/table/td.js +229 -10
- package/src/table/tr.js +249 -4
- package/src/tagPair/ext.js +36 -9
- package/src/tagPair/include.js +24 -0
- package/src/tagPair/index.js +51 -2
- package/src/transclude.js +547 -29
- package/tool/index.js +1202 -0
- package/util/debug.js +73 -0
- package/util/lint.js +8 -7
- package/util/string.js +67 -1
- package/config/minimum.json +0 -142
package/src/index.js
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* -: `{{!-}}`专用
|
|
30
30
|
* +: `{{!!}}`专用
|
|
31
31
|
* ~: `{{=}}`专用
|
|
32
|
+
* s: `{{{|subst:}}}`
|
|
32
33
|
* m: `{{fullurl:}}`、`{{canonicalurl:}}`或`{{filepath:}}`
|
|
33
34
|
* t: ArgToken或TranscludeToken
|
|
34
35
|
* h: HeadingToken
|
|
@@ -44,10 +45,13 @@
|
|
|
44
45
|
*/
|
|
45
46
|
|
|
46
47
|
const {text} = require('../util/string'),
|
|
48
|
+
{externalUse} = require('../util/debug'),
|
|
49
|
+
assert = require('assert/strict'),
|
|
50
|
+
Ranges = require('../lib/ranges'),
|
|
47
51
|
Parser = require('..'),
|
|
48
52
|
AstElement = require('../lib/element'),
|
|
49
53
|
AstText = require('../lib/text');
|
|
50
|
-
const {MAX_STAGE} = Parser;
|
|
54
|
+
const {MAX_STAGE, aliases} = Parser;
|
|
51
55
|
|
|
52
56
|
/**
|
|
53
57
|
* 所有节点的基类
|
|
@@ -60,6 +64,8 @@ class Token extends AstElement {
|
|
|
60
64
|
// 这个数组起两个作用:1. 数组中的Token会在build时替换`/\0\d+.\x7F/`标记;2. 数组中的Token会依次执行parseOnce和build方法。
|
|
61
65
|
#accum;
|
|
62
66
|
/** @type {boolean} */ #include;
|
|
67
|
+
/** @type {Record<string, Ranges>} */ #acceptable;
|
|
68
|
+
#protectedChildren = new Ranges();
|
|
63
69
|
|
|
64
70
|
/**
|
|
65
71
|
* 将维基语法替换为占位符
|
|
@@ -130,7 +136,7 @@ class Token extends AstElement {
|
|
|
130
136
|
#buildFromStr = (str, type) => {
|
|
131
137
|
const nodes = str.split(/[\0\x7F]/u).map((s, i) => {
|
|
132
138
|
if (i % 2 === 0) {
|
|
133
|
-
return new AstText(s
|
|
139
|
+
return new AstText(s);
|
|
134
140
|
} else if (isNaN(s.at(-1))) {
|
|
135
141
|
return this.#accum[Number(s.slice(0, -1))];
|
|
136
142
|
}
|
|
@@ -163,9 +169,33 @@ class Token extends AstElement {
|
|
|
163
169
|
}
|
|
164
170
|
};
|
|
165
171
|
|
|
172
|
+
/**
|
|
173
|
+
* 保护部分子节点不被移除
|
|
174
|
+
* @param {...string|number|Range} args 子节点范围
|
|
175
|
+
*/
|
|
176
|
+
#protectChildren = (...args) => {
|
|
177
|
+
this.#protectedChildren.push(...new Ranges(args));
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/** 所有图片,包括图库 */
|
|
181
|
+
get images() {
|
|
182
|
+
return this.querySelectorAll('file, gallery-image, imagemap-image');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 所有内链、外链和自由外链 */
|
|
186
|
+
get links() {
|
|
187
|
+
return this.querySelectorAll('link, ext-link, free-ext-link, image-parameter#link');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 所有模板和模块 */
|
|
191
|
+
get embeds() {
|
|
192
|
+
return this.querySelectorAll('template, magic-word#invoke');
|
|
193
|
+
}
|
|
194
|
+
|
|
166
195
|
/**
|
|
167
196
|
* @param {string} wikitext wikitext
|
|
168
197
|
* @param {accum} accum
|
|
198
|
+
* @param {acceptable} acceptable 可接受的子节点设置
|
|
169
199
|
*/
|
|
170
200
|
constructor(wikitext, config = Parser.getConfig(), halfParsed = false, accum = [], acceptable = undefined) {
|
|
171
201
|
super();
|
|
@@ -174,6 +204,7 @@ class Token extends AstElement {
|
|
|
174
204
|
}
|
|
175
205
|
this.#config = config;
|
|
176
206
|
this.#accum = accum;
|
|
207
|
+
this.setAttribute('acceptable', acceptable);
|
|
177
208
|
accum.push(this);
|
|
178
209
|
}
|
|
179
210
|
|
|
@@ -203,8 +234,21 @@ class Token extends AstElement {
|
|
|
203
234
|
if (root.type === 'root' && root !== this) {
|
|
204
235
|
return root.getAttribute('include');
|
|
205
236
|
}
|
|
206
|
-
|
|
237
|
+
const includeToken = root.querySelector('include');
|
|
238
|
+
if (includeToken) {
|
|
239
|
+
return includeToken.name === 'noinclude';
|
|
240
|
+
}
|
|
241
|
+
const noincludeToken = root.querySelector('noinclude');
|
|
242
|
+
return Boolean(noincludeToken) && !/^<\/?noinclude(?:\s[^>]*)?\/?>$/iu.test(String(noincludeToken));
|
|
207
243
|
}
|
|
244
|
+
case 'stage':
|
|
245
|
+
return this.#stage;
|
|
246
|
+
case 'acceptable':
|
|
247
|
+
return this.#acceptable ? {...this.#acceptable} : undefined;
|
|
248
|
+
case 'protectChildren':
|
|
249
|
+
return this.#protectChildren;
|
|
250
|
+
case 'protectedChildren':
|
|
251
|
+
return new Ranges(this.#protectedChildren);
|
|
208
252
|
default:
|
|
209
253
|
return super.getAttribute(key);
|
|
210
254
|
}
|
|
@@ -224,6 +268,26 @@ class Token extends AstElement {
|
|
|
224
268
|
}
|
|
225
269
|
this.#stage = value;
|
|
226
270
|
return this;
|
|
271
|
+
case 'acceptable': {
|
|
272
|
+
const /** @type {acceptable} */ acceptable = {};
|
|
273
|
+
if (value) {
|
|
274
|
+
for (const [k, v] of Object.entries(value)) {
|
|
275
|
+
if (k.startsWith('Stage-')) {
|
|
276
|
+
for (let i = 0; i <= Number(k.slice(6)); i++) {
|
|
277
|
+
for (const type of aliases[i]) {
|
|
278
|
+
acceptable[type] = new Ranges(v);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} else if (k[0] === '!') { // `!`项必须放在最后
|
|
282
|
+
delete acceptable[k.slice(1)];
|
|
283
|
+
} else {
|
|
284
|
+
acceptable[k] = new Ranges(v);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
this.#acceptable = value && acceptable;
|
|
289
|
+
return this;
|
|
290
|
+
}
|
|
227
291
|
default:
|
|
228
292
|
return super.setAttribute(key, value);
|
|
229
293
|
}
|
|
@@ -241,10 +305,25 @@ class Token extends AstElement {
|
|
|
241
305
|
* @param {number} i 插入位置
|
|
242
306
|
* @complexity `n`
|
|
243
307
|
* @returns {T extends Token ? Token : AstText}
|
|
308
|
+
* @throws `RangeError` 不可插入的子节点
|
|
244
309
|
*/
|
|
245
310
|
insertAt(token, i = this.length) {
|
|
246
311
|
if (typeof token === 'string') {
|
|
247
|
-
token = new AstText(token
|
|
312
|
+
token = new AstText(token);
|
|
313
|
+
}
|
|
314
|
+
if (!Parser.running && this.#acceptable) {
|
|
315
|
+
const acceptableIndices = Object.fromEntries(
|
|
316
|
+
Object.entries(this.#acceptable)
|
|
317
|
+
.map(([str, ranges]) => [str, ranges.applyTo(this.length + 1)]),
|
|
318
|
+
),
|
|
319
|
+
nodesAfter = this.childNodes.slice(i),
|
|
320
|
+
{constructor: {name: insertedName}} = token,
|
|
321
|
+
k = i < 0 ? i + this.length : i;
|
|
322
|
+
if (!acceptableIndices[insertedName].includes(k)) {
|
|
323
|
+
throw new RangeError(`${this.constructor.name} 的第 ${k} 个子节点不能为 ${insertedName}!`);
|
|
324
|
+
} else if (nodesAfter.some(({constructor: {name}}, j) => !acceptableIndices[name].includes(k + j + 1))) {
|
|
325
|
+
throw new Error(`${this.constructor.name} 插入新的第 ${k} 个子节点会破坏规定的顺序!`);
|
|
326
|
+
}
|
|
248
327
|
}
|
|
249
328
|
super.insertAt(token, i);
|
|
250
329
|
if (token.type === 'root') {
|
|
@@ -264,9 +343,453 @@ class Token extends AstElement {
|
|
|
264
343
|
return Parser.normalizeTitle(title, defaultNs, this.#include, this.#config, halfParsed, decode, selfLink);
|
|
265
344
|
}
|
|
266
345
|
|
|
346
|
+
/**
|
|
347
|
+
* @override
|
|
348
|
+
* @param {number} i 移除位置
|
|
349
|
+
* @returns {Token}
|
|
350
|
+
* @complexity `n`
|
|
351
|
+
* @throws `Error` 不可移除的子节点
|
|
352
|
+
*/
|
|
353
|
+
removeAt(i) {
|
|
354
|
+
if (!Number.isInteger(i)) {
|
|
355
|
+
this.typeError('removeAt', 'Number');
|
|
356
|
+
}
|
|
357
|
+
const iPos = i < 0 ? i + this.length : i;
|
|
358
|
+
if (!Parser.running) {
|
|
359
|
+
const protectedIndices = this.#protectedChildren.applyTo(this.childNodes);
|
|
360
|
+
if (protectedIndices.includes(iPos)) {
|
|
361
|
+
throw new Error(`${this.constructor.name} 的第 ${i} 个子节点不可移除!`);
|
|
362
|
+
} else if (this.#acceptable) {
|
|
363
|
+
const acceptableIndices = Object.fromEntries(
|
|
364
|
+
Object.entries(this.#acceptable)
|
|
365
|
+
.map(([str, ranges]) => [str, ranges.applyTo(this.length - 1)]),
|
|
366
|
+
),
|
|
367
|
+
nodesAfter = i === -1 ? [] : this.childNodes.slice(i + 1);
|
|
368
|
+
if (nodesAfter.some(({constructor: {name}}, j) => !acceptableIndices[name].includes(i + j))) {
|
|
369
|
+
throw new Error(`移除 ${this.constructor.name} 的第 ${i} 个子节点会破坏规定的顺序!`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return super.removeAt(i);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* 替换为同类节点
|
|
378
|
+
* @param {Token} token 待替换的节点
|
|
379
|
+
* @complexity `n`
|
|
380
|
+
* @throws `Error` 不存在父节点
|
|
381
|
+
* @throws `Error` 待替换的节点具有不同属性
|
|
382
|
+
*/
|
|
383
|
+
safeReplaceWith(token) {
|
|
384
|
+
const {parentNode} = this;
|
|
385
|
+
if (!parentNode) {
|
|
386
|
+
throw new Error('不存在父节点!');
|
|
387
|
+
} else if (token.constructor !== this.constructor) {
|
|
388
|
+
this.typeError('safeReplaceWith', this.constructor.name);
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
assert.deepEqual(token.getAttribute('acceptable'), this.#acceptable);
|
|
392
|
+
} catch (e) {
|
|
393
|
+
if (e instanceof assert.AssertionError) {
|
|
394
|
+
throw new Error(`待替换的 ${this.constructor.name} 带有不同的 #acceptable 属性!`);
|
|
395
|
+
}
|
|
396
|
+
throw e;
|
|
397
|
+
}
|
|
398
|
+
const i = parentNode.childNodes.indexOf(this);
|
|
399
|
+
super.removeAt.call(parentNode, i);
|
|
400
|
+
super.insertAt.call(parentNode, token, i);
|
|
401
|
+
if (token.type === 'root') {
|
|
402
|
+
token.type = 'plain';
|
|
403
|
+
}
|
|
404
|
+
const e = new Event('replace', {bubbles: true});
|
|
405
|
+
token.dispatchEvent(e, {position: i, oldToken: this, newToken: token});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* 创建HTML注释
|
|
410
|
+
* @param {string} data 注释内容
|
|
411
|
+
*/
|
|
412
|
+
createComment(data = '') {
|
|
413
|
+
if (typeof data === 'string') {
|
|
414
|
+
const CommentToken = require('./nowiki/comment');
|
|
415
|
+
const config = this.getAttribute('config');
|
|
416
|
+
return Parser.run(() => new CommentToken(data.replaceAll('-->', '-->'), true, config));
|
|
417
|
+
}
|
|
418
|
+
return this.typeError('createComment', 'String');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* 创建标签
|
|
423
|
+
* @param {string} tagName 标签名
|
|
424
|
+
* @param {{selfClosing: boolean, closing: boolean}} options 选项
|
|
425
|
+
* @throws `RangeError` 非法的标签名
|
|
426
|
+
*/
|
|
427
|
+
createElement(tagName, {selfClosing, closing} = {}) {
|
|
428
|
+
if (typeof tagName !== 'string') {
|
|
429
|
+
this.typeError('createElement', 'String');
|
|
430
|
+
}
|
|
431
|
+
const config = this.getAttribute('config'),
|
|
432
|
+
include = this.getAttribute('include');
|
|
433
|
+
if (tagName === (include ? 'noinclude' : 'includeonly')) {
|
|
434
|
+
const IncludeToken = require('./tagPair/include');
|
|
435
|
+
return Parser.run(
|
|
436
|
+
() => new IncludeToken(tagName, '', undefined, selfClosing ? undefined : tagName, config),
|
|
437
|
+
);
|
|
438
|
+
} else if (config.ext.includes(tagName)) {
|
|
439
|
+
const ExtToken = require('./tagPair/ext');
|
|
440
|
+
return Parser.run(() => new ExtToken(tagName, '', '', selfClosing ? undefined : '', config));
|
|
441
|
+
} else if (config.html.flat().includes(tagName)) {
|
|
442
|
+
const HtmlToken = require('./html');
|
|
443
|
+
return Parser.run(() => new HtmlToken(tagName, '', closing, selfClosing, config));
|
|
444
|
+
}
|
|
445
|
+
throw new RangeError(`非法的标签名!${tagName}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* 创建纯文本节点
|
|
450
|
+
* @param {string} data 文本内容
|
|
451
|
+
*/
|
|
452
|
+
createTextNode(data = '') {
|
|
453
|
+
return typeof data === 'string' ? new AstText(data) : this.typeError('createComment', 'String');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* 找到给定位置所在的节点
|
|
458
|
+
* @param {number} index 位置
|
|
459
|
+
*/
|
|
460
|
+
caretPositionFromIndex(index) {
|
|
461
|
+
if (index === undefined) {
|
|
462
|
+
return undefined;
|
|
463
|
+
} else if (!Number.isInteger(index)) {
|
|
464
|
+
this.typeError('caretPositionFromIndex', 'Number');
|
|
465
|
+
}
|
|
466
|
+
const {length} = String(this);
|
|
467
|
+
if (index > length || index < -length) {
|
|
468
|
+
return undefined;
|
|
469
|
+
} else if (index < 0) {
|
|
470
|
+
index += length;
|
|
471
|
+
}
|
|
472
|
+
let child = this, // eslint-disable-line unicorn/no-this-assignment
|
|
473
|
+
acc = 0,
|
|
474
|
+
start = 0;
|
|
475
|
+
while (child.type !== 'text') {
|
|
476
|
+
const {childNodes} = child;
|
|
477
|
+
acc += child.getPadding();
|
|
478
|
+
for (let i = 0; acc <= index && i < childNodes.length; i++) {
|
|
479
|
+
const cur = childNodes[i],
|
|
480
|
+
{length: l} = String(cur);
|
|
481
|
+
acc += l;
|
|
482
|
+
if (acc >= index) {
|
|
483
|
+
child = cur;
|
|
484
|
+
acc -= l;
|
|
485
|
+
start = acc;
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
acc += child.getGaps(i);
|
|
489
|
+
}
|
|
490
|
+
if (child.childNodes === childNodes) {
|
|
491
|
+
return {offsetNode: child, offset: index - start};
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return {offsetNode: child, offset: index - start};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* 找到给定位置所在的节点
|
|
499
|
+
* @param {number} x 列数
|
|
500
|
+
* @param {number} y 行数
|
|
501
|
+
*/
|
|
502
|
+
caretPositionFromPoint(x, y) {
|
|
503
|
+
return this.caretPositionFromIndex(this.indexFromPos(y, x));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* 找到给定位置所在的最外层节点
|
|
508
|
+
* @param {number} index 位置
|
|
509
|
+
* @throws `Error` 不是根节点
|
|
510
|
+
*/
|
|
511
|
+
elementFromIndex(index) {
|
|
512
|
+
if (index === undefined) {
|
|
513
|
+
return undefined;
|
|
514
|
+
} else if (!Number.isInteger(index)) {
|
|
515
|
+
this.typeError('elementFromIndex', 'Number');
|
|
516
|
+
} else if (this.type !== 'root') {
|
|
517
|
+
throw new Error('elementFromIndex方法只可用于根节点!');
|
|
518
|
+
}
|
|
519
|
+
const {length} = String(this);
|
|
520
|
+
if (index > length || index < -length) {
|
|
521
|
+
return undefined;
|
|
522
|
+
} else if (index < 0) {
|
|
523
|
+
index += length;
|
|
524
|
+
}
|
|
525
|
+
const {childNodes} = this;
|
|
526
|
+
let acc = 0,
|
|
527
|
+
i = 0;
|
|
528
|
+
for (; acc < index && i < childNodes.length; i++) {
|
|
529
|
+
const {length: l} = String(childNodes[i]);
|
|
530
|
+
acc += l;
|
|
531
|
+
}
|
|
532
|
+
return childNodes[i && i - 1];
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* 找到给定位置所在的最外层节点
|
|
537
|
+
* @param {number} x 列数
|
|
538
|
+
* @param {number} y 行数
|
|
539
|
+
*/
|
|
540
|
+
elementFromPoint(x, y) {
|
|
541
|
+
return this.elementFromIndex(this.indexFromPos(y, x));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* 找到给定位置所在的所有节点
|
|
546
|
+
* @param {number} index 位置
|
|
547
|
+
*/
|
|
548
|
+
elementsFromIndex(index) {
|
|
549
|
+
const offsetNode = this.caretPositionFromIndex(index)?.offsetNode;
|
|
550
|
+
return offsetNode && [...offsetNode.getAncestors().reverse(), offsetNode];
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* 找到给定位置所在的所有节点
|
|
555
|
+
* @param {number} x 列数
|
|
556
|
+
* @param {number} y 行数
|
|
557
|
+
*/
|
|
558
|
+
elementsFromPoint(x, y) {
|
|
559
|
+
return this.elementsFromIndex(this.indexFromPos(y, x));
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* 判断标题是否是跨维基链接
|
|
564
|
+
* @param {string} title 标题
|
|
565
|
+
*/
|
|
566
|
+
isInterwiki(title) {
|
|
567
|
+
return Parser.isInterwiki(title, this.#config);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* 深拷贝所有子节点
|
|
572
|
+
* @complexity `n`
|
|
573
|
+
* @returns {(AstText|Token)[]}
|
|
574
|
+
*/
|
|
575
|
+
cloneChildNodes() {
|
|
576
|
+
return this.childNodes.map(child => child.cloneNode());
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* 深拷贝节点
|
|
581
|
+
* @complexity `n`
|
|
582
|
+
* @throws `Error` 未定义复制方法
|
|
583
|
+
*/
|
|
584
|
+
cloneNode() {
|
|
585
|
+
if (this.constructor !== Token) {
|
|
586
|
+
throw new Error(`未定义 ${this.constructor.name} 的复制方法!`);
|
|
587
|
+
}
|
|
588
|
+
const cloned = this.cloneChildNodes();
|
|
589
|
+
return Parser.run(() => {
|
|
590
|
+
const token = new Token(undefined, this.#config, false, [], this.#acceptable);
|
|
591
|
+
token.type = this.type;
|
|
592
|
+
token.append(...cloned);
|
|
593
|
+
token.getAttribute('protectChildren')(...this.#protectedChildren);
|
|
594
|
+
return token;
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* 获取全部章节
|
|
600
|
+
* @complexity `n`
|
|
601
|
+
*/
|
|
602
|
+
sections() {
|
|
603
|
+
if (this.type !== 'root') {
|
|
604
|
+
return undefined;
|
|
605
|
+
}
|
|
606
|
+
const {childNodes} = this,
|
|
607
|
+
headings = [...childNodes.entries()].filter(([, {type}]) => type === 'heading')
|
|
608
|
+
.map(([i, {name}]) => [i, Number(name)]),
|
|
609
|
+
lastHeading = [-1, -1, -1, -1, -1, -1],
|
|
610
|
+
/** @type {(AstText|Token)[][]} */ sections = new Array(headings.length);
|
|
611
|
+
for (let i = 0; i < headings.length; i++) {
|
|
612
|
+
const [index, level] = headings[i];
|
|
613
|
+
for (let j = level; j < 6; j++) {
|
|
614
|
+
const last = lastHeading[j];
|
|
615
|
+
if (last >= 0) {
|
|
616
|
+
sections[last] = childNodes.slice(headings[last][0], index);
|
|
617
|
+
}
|
|
618
|
+
lastHeading[j] = j === level ? i : -1;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
for (const last of lastHeading) {
|
|
622
|
+
if (last >= 0) {
|
|
623
|
+
sections[last] = childNodes.slice(headings[last][0]);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
sections.unshift(childNodes.slice(0, headings[0]?.[0]));
|
|
627
|
+
return sections;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* 获取指定章节
|
|
632
|
+
* @param {number} n 章节序号
|
|
633
|
+
* @complexity `n`
|
|
634
|
+
*/
|
|
635
|
+
section(n) {
|
|
636
|
+
return Number.isInteger(n) ? this.sections()?.[n] : this.typeError('section', 'Number');
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* 获取指定的外层HTML标签
|
|
641
|
+
* @param {string|undefined} tag HTML标签名
|
|
642
|
+
* @returns {[Token, Token]}
|
|
643
|
+
* @complexity `n`
|
|
644
|
+
* @throws `RangeError` 非法的标签或空标签
|
|
645
|
+
*/
|
|
646
|
+
findEnclosingHtml(tag) {
|
|
647
|
+
if (tag !== undefined && typeof tag !== 'string') {
|
|
648
|
+
this.typeError('findEnclosingHtml', 'String');
|
|
649
|
+
}
|
|
650
|
+
tag = tag?.toLowerCase();
|
|
651
|
+
if (tag !== undefined && !this.#config.html.slice(0, 2).flat().includes(tag)) {
|
|
652
|
+
throw new RangeError(`非法的标签或空标签:${tag}`);
|
|
653
|
+
}
|
|
654
|
+
const {parentNode} = this;
|
|
655
|
+
if (!parentNode) {
|
|
656
|
+
return undefined;
|
|
657
|
+
}
|
|
658
|
+
const {childNodes} = parentNode,
|
|
659
|
+
index = childNodes.indexOf(this);
|
|
660
|
+
let i;
|
|
661
|
+
for (i = index - 1; i >= 0; i--) {
|
|
662
|
+
const {type, name, selfClosing, closing} = childNodes[i];
|
|
663
|
+
if (type === 'html' && (!tag || name === tag) && selfClosing === false && closing === false) {
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (i === -1) {
|
|
668
|
+
return parentNode.findEnclosingHtml(tag);
|
|
669
|
+
}
|
|
670
|
+
const opening = childNodes[i];
|
|
671
|
+
for (i = index + 1; i < childNodes.length; i++) {
|
|
672
|
+
const {type, name, selfClosing, closing} = childNodes[i];
|
|
673
|
+
if (type === 'html' && name === opening.name && selfClosing === false && closing === true) {
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return i === childNodes.length
|
|
678
|
+
? parentNode.findEnclosingHtml(tag)
|
|
679
|
+
: [opening, childNodes[i]];
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* 获取全部分类
|
|
684
|
+
* @complexity `n`
|
|
685
|
+
*/
|
|
686
|
+
getCategories() {
|
|
687
|
+
return this.querySelectorAll('category').map(({name, sortkey}) => [name, sortkey]);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* 重新解析单引号
|
|
692
|
+
* @throws `Error` 不接受QuoteToken作为子节点
|
|
693
|
+
*/
|
|
694
|
+
redoQuotes() {
|
|
695
|
+
const acceptable = this.getAttribute('acceptable');
|
|
696
|
+
if (acceptable && !acceptable.QuoteToken?.some(
|
|
697
|
+
range => typeof range !== 'number' && range.start === 0 && range.end === Infinity && range.step === 1,
|
|
698
|
+
)) {
|
|
699
|
+
throw new Error(`${this.constructor.name} 不接受 QuoteToken 作为子节点!`);
|
|
700
|
+
}
|
|
701
|
+
for (const quote of this.childNodes) {
|
|
702
|
+
if (quote.type === 'quote') {
|
|
703
|
+
quote.replaceWith(String(quote));
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
this.normalize();
|
|
707
|
+
/** @type {[number, AstText][]} */
|
|
708
|
+
const textNodes = [...this.childNodes.entries()].filter(([, {type}]) => type === 'text'),
|
|
709
|
+
indices = textNodes.map(([i]) => this.getRelativeIndex(i)),
|
|
710
|
+
token = Parser.run(() => {
|
|
711
|
+
const root = new Token(text(textNodes.map(([, str]) => str)), this.getAttribute('config'));
|
|
712
|
+
return root.setAttribute('stage', 6).parse(7);
|
|
713
|
+
});
|
|
714
|
+
for (const quote of [...token.childNodes].reverse()) {
|
|
715
|
+
if (quote.type === 'quote') {
|
|
716
|
+
const index = quote.getRelativeIndex(),
|
|
717
|
+
n = indices.findLastIndex(textIndex => textIndex <= index);
|
|
718
|
+
this.childNodes[n].splitText(index - indices[n]);
|
|
719
|
+
this.childNodes[n + 1].splitText(Number(quote.name));
|
|
720
|
+
this.removeAt(n + 1);
|
|
721
|
+
this.insertAt(quote, n + 1);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
this.normalize();
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** 解析部分魔术字 */
|
|
728
|
+
solveConst() {
|
|
729
|
+
const ArgToken = require('./arg'),
|
|
730
|
+
ParameterToken = require('./parameter');
|
|
731
|
+
const targets = this.querySelectorAll('magic-word, arg'),
|
|
732
|
+
magicWords = new Set(['if', 'ifeq', 'switch']);
|
|
733
|
+
for (let i = targets.length - 1; i >= 0; i--) {
|
|
734
|
+
const /** @type {ArgToken} */ target = targets[i],
|
|
735
|
+
{type, name, default: argDefault, childNodes, length} = target;
|
|
736
|
+
if (type === 'arg' || type === 'magic-word' && magicWords.has(name)) {
|
|
737
|
+
let replace = '';
|
|
738
|
+
if (type === 'arg') {
|
|
739
|
+
replace = argDefault === false ? String(target) : argDefault;
|
|
740
|
+
} else if (name === 'if' && !childNodes[1].querySelector('magic-word, template')) {
|
|
741
|
+
replace = String(childNodes[String(childNodes[1] ?? '').trim() ? 2 : 3] ?? '').trim();
|
|
742
|
+
} else if (name === 'ifeq'
|
|
743
|
+
&& !childNodes.slice(1, 3).some(child => child.querySelector('magic-word, template'))
|
|
744
|
+
) {
|
|
745
|
+
replace = String(childNodes[
|
|
746
|
+
String(childNodes[1] ?? '').trim() === String(childNodes[2] ?? '') ? 3 : 4
|
|
747
|
+
] ?? '').trim();
|
|
748
|
+
} else if (name === 'switch' && !childNodes[1].querySelector('magic-word, template')) {
|
|
749
|
+
const key = String(childNodes[1] ?? '').trim();
|
|
750
|
+
let defaultVal = '',
|
|
751
|
+
found = false,
|
|
752
|
+
transclusion = false,
|
|
753
|
+
j = 2;
|
|
754
|
+
for (; j < length; j++) {
|
|
755
|
+
const /** @type {ParameterToken} */ {anon, name: option, value, firstChild} = childNodes[j];
|
|
756
|
+
transclusion = firstChild.querySelector('magic-word, template');
|
|
757
|
+
if (anon) {
|
|
758
|
+
if (j === length - 1) {
|
|
759
|
+
defaultVal = value;
|
|
760
|
+
} else if (transclusion) {
|
|
761
|
+
break;
|
|
762
|
+
} else {
|
|
763
|
+
found ||= key === value;
|
|
764
|
+
}
|
|
765
|
+
} else if (transclusion) {
|
|
766
|
+
break;
|
|
767
|
+
} else if (found || option === key) {
|
|
768
|
+
replace = value;
|
|
769
|
+
break;
|
|
770
|
+
} else if (option.toLowerCase() === '#default') {
|
|
771
|
+
defaultVal = value;
|
|
772
|
+
}
|
|
773
|
+
if (j === length - 1) {
|
|
774
|
+
replace = defaultVal;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (transclusion) {
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
} else {
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
target.replaceWith(replace);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
267
788
|
/** 生成部分Token的`name`属性 */
|
|
268
789
|
afterBuild() {
|
|
269
|
-
if (
|
|
790
|
+
if (!Parser.debugging && externalUse('afterBuild')) {
|
|
791
|
+
this.debugOnly('afterBuild');
|
|
792
|
+
} else if (this.type === 'root') {
|
|
270
793
|
for (const token of this.#accum) {
|
|
271
794
|
token.afterBuild();
|
|
272
795
|
}
|
|
@@ -279,6 +802,9 @@ class Token extends AstElement {
|
|
|
279
802
|
* @param {boolean} include 是否嵌入
|
|
280
803
|
*/
|
|
281
804
|
parse(n = MAX_STAGE, include = false) {
|
|
805
|
+
if (!Number.isInteger(n)) {
|
|
806
|
+
this.typeError('parse', 'Number');
|
|
807
|
+
}
|
|
282
808
|
while (this.#stage < n) {
|
|
283
809
|
this.#parseOnce(this.#stage, include);
|
|
284
810
|
}
|
|
@@ -390,7 +916,7 @@ class Token extends AstElement {
|
|
|
390
916
|
}
|
|
391
917
|
const parseList = require('../parser/list');
|
|
392
918
|
const lines = String(this.firstChild).split('\n');
|
|
393
|
-
let i = this.type === 'root' || this.type === 'ext-inner' && this.
|
|
919
|
+
let i = this.type === 'root' || this.type === 'ext-inner' && this.name === 'poem' ? 0 : 1;
|
|
394
920
|
for (; i < lines.length; i++) {
|
|
395
921
|
lines[i] = parseList(lines[i], this.#config, this.#accum);
|
|
396
922
|
}
|
|
@@ -399,9 +925,12 @@ class Token extends AstElement {
|
|
|
399
925
|
|
|
400
926
|
/** 解析语言变体转换 */
|
|
401
927
|
#parseConverter() {
|
|
402
|
-
|
|
403
|
-
|
|
928
|
+
if (this.#config.variants?.length > 0) {
|
|
929
|
+
const parseConverter = require('../parser/converter');
|
|
930
|
+
this.setText(parseConverter(String(this.firstChild), this.#config, this.#accum));
|
|
931
|
+
}
|
|
404
932
|
}
|
|
405
933
|
}
|
|
406
934
|
|
|
935
|
+
Parser.classes.Token = __filename;
|
|
407
936
|
module.exports = Token;
|
package/src/link/category.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const {decodeHtml} = require('../../util/string'),
|
|
4
|
+
Parser = require('../..'),
|
|
5
|
+
LinkToken = require('.');
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* 分类
|
|
@@ -8,6 +10,35 @@ const LinkToken = require('.');
|
|
|
8
10
|
*/
|
|
9
11
|
class CategoryToken extends LinkToken {
|
|
10
12
|
type = 'category';
|
|
13
|
+
|
|
14
|
+
/** 分类排序关键字 */
|
|
15
|
+
get sortkey() {
|
|
16
|
+
return decodeHtml(this.childNodes[1]?.text());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
set sortkey(text) {
|
|
20
|
+
this.setSortkey(text);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} link 分类名
|
|
25
|
+
* @param {string|undefined} text 排序关键字
|
|
26
|
+
* @param {accum} accum
|
|
27
|
+
* @param {string} delimiter `|`
|
|
28
|
+
*/
|
|
29
|
+
constructor(link, text, config = Parser.getConfig(), accum = [], delimiter = '|') {
|
|
30
|
+
super(link, text, config, accum, delimiter);
|
|
31
|
+
this.seal(['selfLink', 'interwiki', 'setLangLink', 'setFragment', 'asSelfLink', 'pipeTrick'], true);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 设置排序关键字
|
|
36
|
+
* @param {string} text 排序关键字
|
|
37
|
+
*/
|
|
38
|
+
setSortkey(text) {
|
|
39
|
+
this.setLinkText(text);
|
|
40
|
+
}
|
|
11
41
|
}
|
|
12
42
|
|
|
43
|
+
Parser.classes.CategoryToken = __filename;
|
|
13
44
|
module.exports = CategoryToken;
|