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.
Files changed (81) hide show
  1. package/README.md +39 -0
  2. package/config/moegirl.json +1 -0
  3. package/i18n/zh-hans.json +44 -0
  4. package/i18n/zh-hant.json +44 -0
  5. package/index.js +264 -10
  6. package/lib/element.js +507 -33
  7. package/lib/node.js +550 -6
  8. package/lib/ranges.js +130 -0
  9. package/lib/text.js +111 -41
  10. package/lib/title.js +28 -5
  11. package/mixin/attributeParent.js +117 -0
  12. package/mixin/fixedToken.js +40 -0
  13. package/mixin/hidden.js +3 -0
  14. package/mixin/singleLine.js +31 -0
  15. package/mixin/sol.js +54 -0
  16. package/package.json +9 -8
  17. package/parser/brackets.js +9 -2
  18. package/parser/commentAndExt.js +3 -5
  19. package/parser/converter.js +1 -0
  20. package/parser/externalLinks.js +1 -0
  21. package/parser/hrAndDoubleUnderscore.js +1 -0
  22. package/parser/html.js +1 -0
  23. package/parser/links.js +6 -5
  24. package/parser/list.js +1 -0
  25. package/parser/magicLinks.js +5 -4
  26. package/parser/quotes.js +1 -0
  27. package/parser/selector.js +177 -0
  28. package/parser/table.js +1 -0
  29. package/src/arg.js +123 -5
  30. package/src/atom/hidden.js +2 -0
  31. package/src/atom/index.js +17 -0
  32. package/src/attribute.js +191 -8
  33. package/src/attributes.js +311 -8
  34. package/src/charinsert.js +97 -0
  35. package/src/converter.js +108 -2
  36. package/src/converterFlags.js +190 -3
  37. package/src/converterRule.js +185 -4
  38. package/src/extLink.js +122 -2
  39. package/src/gallery.js +59 -11
  40. package/src/hasNowiki/index.js +12 -0
  41. package/src/hasNowiki/pre.js +12 -0
  42. package/src/heading.js +57 -6
  43. package/src/html.js +133 -12
  44. package/src/imageParameter.js +232 -38
  45. package/src/imagemap.js +65 -6
  46. package/src/imagemapLink.js +14 -2
  47. package/src/index.js +537 -8
  48. package/src/link/category.js +32 -1
  49. package/src/link/file.js +173 -11
  50. package/src/link/galleryImage.js +63 -5
  51. package/src/link/index.js +268 -9
  52. package/src/magicLink.js +92 -11
  53. package/src/nested/choose.js +1 -0
  54. package/src/nested/combobox.js +1 -0
  55. package/src/nested/index.js +31 -7
  56. package/src/nested/references.js +1 -0
  57. package/src/nowiki/comment.js +27 -3
  58. package/src/nowiki/dd.js +47 -1
  59. package/src/nowiki/doubleUnderscore.js +31 -1
  60. package/src/nowiki/hr.js +20 -1
  61. package/src/nowiki/index.js +25 -3
  62. package/src/nowiki/list.js +5 -2
  63. package/src/nowiki/noinclude.js +14 -0
  64. package/src/nowiki/quote.js +17 -3
  65. package/src/onlyinclude.js +26 -1
  66. package/src/paramTag/index.js +27 -4
  67. package/src/paramTag/inputbox.js +4 -1
  68. package/src/parameter.js +150 -8
  69. package/src/syntax.js +68 -0
  70. package/src/table/index.js +941 -4
  71. package/src/table/td.js +229 -10
  72. package/src/table/tr.js +249 -4
  73. package/src/tagPair/ext.js +36 -9
  74. package/src/tagPair/include.js +24 -0
  75. package/src/tagPair/index.js +51 -2
  76. package/src/transclude.js +547 -29
  77. package/tool/index.js +1202 -0
  78. package/util/debug.js +73 -0
  79. package/util/lint.js +8 -7
  80. package/util/string.js +67 -1
  81. 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, this.#config);
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
- return false;
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, this.#config);
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('-->', '--&gt;'), 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 (this.type === 'root') {
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.type === 'poem' ? 0 : 1;
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
- const parseConverter = require('../parser/converter');
403
- this.setText(parseConverter(String(this.firstChild), this.#config, this.#accum));
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;
@@ -1,6 +1,8 @@
1
1
  'use strict';
2
2
 
3
- const LinkToken = require('.');
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;