wikiparser-node 0.4.0 → 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.
Files changed (65) hide show
  1. package/index.js +25 -2
  2. package/lib/element.js +69 -185
  3. package/lib/node.js +159 -1
  4. package/lib/ranges.js +1 -2
  5. package/lib/text.js +35 -6
  6. package/lib/title.js +1 -1
  7. package/mixin/fixedToken.js +4 -4
  8. package/mixin/sol.js +17 -7
  9. package/package.json +11 -1
  10. package/parser/commentAndExt.js +1 -1
  11. package/parser/converter.js +1 -1
  12. package/parser/externalLinks.js +1 -1
  13. package/parser/hrAndDoubleUnderscore.js +6 -5
  14. package/parser/links.js +1 -2
  15. package/parser/magicLinks.js +1 -1
  16. package/parser/selector.js +5 -5
  17. package/parser/table.js +12 -12
  18. package/src/arg.js +44 -20
  19. package/src/attribute.js +34 -7
  20. package/src/converter.js +13 -5
  21. package/src/converterFlags.js +42 -5
  22. package/src/converterRule.js +25 -19
  23. package/src/extLink.js +20 -14
  24. package/src/gallery.js +35 -4
  25. package/src/heading.js +28 -9
  26. package/src/html.js +46 -18
  27. package/src/imageParameter.js +13 -7
  28. package/src/index.js +22 -15
  29. package/src/link/category.js +6 -6
  30. package/src/link/file.js +25 -5
  31. package/src/link/index.js +36 -33
  32. package/src/magicLink.js +32 -4
  33. package/src/nowiki/comment.js +14 -0
  34. package/src/nowiki/doubleUnderscore.js +5 -0
  35. package/src/nowiki/quote.js +28 -1
  36. package/src/onlyinclude.js +5 -0
  37. package/src/parameter.js +48 -35
  38. package/src/table/index.js +37 -24
  39. package/src/table/td.js +23 -17
  40. package/src/table/tr.js +47 -30
  41. package/src/tagPair/ext.js +4 -5
  42. package/src/tagPair/include.js +10 -0
  43. package/src/tagPair/index.js +8 -0
  44. package/src/transclude.js +79 -46
  45. package/tool/index.js +1 -1
  46. package/{test/util.js → util/diff.js} +14 -18
  47. package/util/lint.js +40 -0
  48. package/util/string.js +20 -3
  49. package/.eslintrc.json +0 -714
  50. package/errors/README +0 -1
  51. package/jsconfig.json +0 -7
  52. package/printed/README +0 -1
  53. package/printed/example.json +0 -120
  54. package/test/api.js +0 -83
  55. package/test/real.js +0 -133
  56. package/test/test.js +0 -28
  57. package/typings/api.d.ts +0 -13
  58. package/typings/array.d.ts +0 -28
  59. package/typings/event.d.ts +0 -24
  60. package/typings/index.d.ts +0 -94
  61. package/typings/node.d.ts +0 -29
  62. package/typings/parser.d.ts +0 -16
  63. package/typings/table.d.ts +0 -14
  64. package/typings/token.d.ts +0 -22
  65. package/typings/tool.d.ts +0 -11
package/index.js CHANGED
@@ -75,6 +75,8 @@ const /** @type {Parser} */ Parser = {
75
75
  'converter-rule-from': ['convert-rule-from', 'conversion-rule-from'],
76
76
  },
77
77
 
78
+ promises: [Promise.resolve()],
79
+
78
80
  warn(msg, ...args) {
79
81
  if (this.warning) {
80
82
  console.warn('\x1B[33m%s\x1B[0m', msg, ...args);
@@ -172,7 +174,7 @@ const /** @type {Parser} */ Parser = {
172
174
 
173
175
  parse(wikitext, include, maxStage = Parser.MAX_STAGE, config = Parser.getConfig()) {
174
176
  const Token = require('./src');
175
- let token;
177
+ let /** @type {Token} */ token;
176
178
  this.run(() => {
177
179
  if (typeof wikitext === 'string') {
178
180
  token = new Token(wikitext, config);
@@ -197,6 +199,27 @@ const /** @type {Parser} */ Parser = {
197
199
  throw e;
198
200
  }
199
201
  });
202
+ if (this.debugging) {
203
+ let restored = String(token),
204
+ process = '解析';
205
+ if (restored === wikitext) {
206
+ const entities = {lt: '<', gt: '>', amp: '&'};
207
+ restored = token.print().replaceAll(
208
+ /<[^<]+?>|&([lg]t|amp);/gu,
209
+ /** @param {string} s */ (_, s) => s ? entities[s] : '',
210
+ );
211
+ process = '渲染HTML';
212
+ }
213
+ if (restored !== wikitext) {
214
+ const diff = require('./util/diff');
215
+ const {promises: {0: cur, length}} = this;
216
+ this.promises.unshift((async () => {
217
+ await cur;
218
+ this.error(`${process}过程中不可逆地修改了原始文本!`);
219
+ return diff(wikitext, restored, length);
220
+ })());
221
+ }
222
+ }
200
223
  return token;
201
224
  },
202
225
 
@@ -234,7 +257,7 @@ const /** @type {Parser} */ Parser = {
234
257
 
235
258
  const /** @type {PropertyDescriptorMap} */ def = {};
236
259
  for (const key in Parser) {
237
- if (['aliases', 'MAX_STAGE', 'typeAliases'].includes(key)) {
260
+ if (['aliases', 'MAX_STAGE', 'typeAliases', 'promises'].includes(key)) {
238
261
  def[key] = {enumerable: false, writable: false};
239
262
  } else if (!['config', 'isInterwiki', 'normalizeTitle', 'parse', 'getTool'].includes(key)) {
240
263
  def[key] = {enumerable: false};
package/lib/element.js CHANGED
@@ -1,8 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs'),
4
+ path = require('path'),
4
5
  {externalUse} = require('../util/debug'),
5
- {toCase, noWrap} = require('../util/string'),
6
+ {toCase, noWrap, print} = require('../util/string'),
6
7
  {nth} = require('./ranges'),
7
8
  parseSelector = require('../parser/selector'),
8
9
  Parser = require('..'),
@@ -138,6 +139,18 @@ class AstElement extends AstNode {
138
139
  return previousSibling;
139
140
  }
140
141
 
142
+ /** 内部高度 */
143
+ get clientHeight() {
144
+ const {innerText} = this;
145
+ return typeof innerText === 'string' ? innerText.split('\n').length : undefined;
146
+ }
147
+
148
+ /** 内部宽度 */
149
+ get clientWidth() {
150
+ const {innerText} = this;
151
+ return typeof innerText === 'string' ? innerText.split('\n').at(-1).length : undefined;
152
+ }
153
+
141
154
  constructor() {
142
155
  super();
143
156
  this.seal('name');
@@ -228,11 +241,11 @@ class AstElement extends AstNode {
228
241
  this.getAttribute('verifyChild')(i);
229
242
  const /** @type {AstText} */ oldText = this.childNodes.at(i),
230
243
  {type, data, constructor: {name}} = oldText;
231
- if (type !== 'text') {
232
- throw new RangeError(`第 ${i} 个子节点是 ${name}!`);
244
+ if (type === 'text') {
245
+ oldText.replaceData(str);
246
+ return data;
233
247
  }
234
- oldText.replaceData(str);
235
- return data;
248
+ throw new RangeError(`第 ${i} 个子节点是 ${name}!`);
236
249
  }
237
250
 
238
251
  /** 是否受保护。保护条件来自Token,这里仅提前用于:required和:optional伪选择器。 */
@@ -298,7 +311,7 @@ class AstElement extends AstNode {
298
311
  || (type === 'file' || type === 'gallery-image' && link);
299
312
  case ':local-link':
300
313
  return (type === 'link' || type === 'file' || type === 'gallery-image')
301
- && link?.startsWith('#');
314
+ && link?.[0] === '#';
302
315
  case ':read-only':
303
316
  return fixed;
304
317
  case ':read-write':
@@ -358,7 +371,7 @@ class AstElement extends AstNode {
358
371
  return Parser.run(() => {
359
372
  const stack = parseSelector(selector),
360
373
  /** @type {Set<string>} */
361
- pseudos = new Set(stack.flat(2).filter(step => typeof step === 'string' && step.startsWith(':')));
374
+ pseudos = new Set(stack.flat(2).filter(step => typeof step === 'string' && step[0] === ':'));
362
375
  if (pseudos.size > 0) {
363
376
  Parser.warn('检测到伪选择器,请确认是否需要将":"转义成"\\:"。', pseudos);
364
377
  }
@@ -521,204 +534,74 @@ class AstElement extends AstNode {
521
534
  return String(this).split('\n', n + 1).at(-1);
522
535
  }
523
536
 
524
- /**
525
- * 将字符位置转换为行列号
526
- * @param {number} index 字符位置
527
- * @complexity `n`
528
- */
529
- posFromIndex(index) {
530
- if (typeof index !== 'number') {
531
- this.typeError('posFromIndex', 'Number');
532
- }
533
- const text = String(this);
534
- if (index >= -text.length && index < text.length && Number.isInteger(index)) {
535
- const lines = text.slice(0, index).split('\n');
536
- return {top: lines.length - 1, left: lines.at(-1).length};
537
- }
538
- return undefined;
539
- }
540
-
541
- /**
542
- * 将行列号转换为字符位置
543
- * @param {number} top 行号
544
- * @param {number} left 列号
545
- * @complexity `n`
546
- */
547
- indexFromPos(top, left) {
548
- if (typeof top !== 'number' || typeof left !== 'number') {
549
- this.typeError('indexFromPos', 'Number');
550
- }
551
- const lines = String(this).split('\n');
552
- return top >= 0 && left >= 0 && Number.isInteger(top) && Number.isInteger(left)
553
- && lines.length >= top + 1 && lines[top].length >= left
554
- ? lines.slice(0, top).reduce((acc, curLine) => acc + curLine.length + 1, 0) + left
555
- : undefined;
556
- }
557
-
558
- /**
559
- * 获取行数和最后一行的列数
560
- * @complexity `n`
561
- */
562
- #getDimension() {
563
- const lines = String(this).split('\n');
564
- return {height: lines.length, width: lines.at(-1).length};
565
- }
537
+ static lintIgnoredHidden = new Set(['noinclude', 'double-underscore', 'hidden']);
538
+ static lintIgnoredSyntax = new Set(['magic-word-name', 'heading-trail', 'table-syntax']);
539
+ static lintIgnoredExt = new Set(['nowiki', 'pre', 'syntaxhighlight', 'source', 'math', 'timeline']);
566
540
 
567
541
  /**
568
- * 获取当前节点的相对字符位置,或其第`j`个子节点的相对字符位置
569
- * @param {number|undefined} j 子节点序号
570
- * @complexity `n`
542
+ * Linter
543
+ * @param {number} start 起始位置
571
544
  */
572
- getRelativeIndex(j) {
573
- if (j !== undefined && typeof j !== 'number') {
574
- this.typeError('getRelativeIndex', 'Number');
545
+ lint(start = 0) {
546
+ if (AstElement.lintIgnoredHidden.has(this.type) || AstElement.lintIgnoredSyntax.has(this.type)
547
+ || this.type === 'ext-inner' && AstElement.lintIgnoredExt.has(this.name)
548
+ ) {
549
+ return [];
575
550
  }
576
- let /** @type {this[]} */ childNodes;
577
-
578
- /**
579
- * 获取子节点相对于父节点的字符位置,使用前需要先给`childNodes`赋值
580
- * @param {number} end 子节点序号
581
- * @param {this} parent 父节点
582
- * @returns {number}
583
- */
584
- const getIndex = (end, parent) => childNodes.slice(0, end).reduce(
585
- (acc, cur, i) => acc + String(cur).length + parent.getGaps(i),
586
- 0,
587
- ) + parent.getPadding();
588
- if (j === undefined) {
589
- const {parentNode} = this;
590
- if (!parentNode) {
591
- return 0;
592
- }
593
- ({childNodes} = parentNode);
594
- return getIndex(childNodes.indexOf(this), parentNode);
551
+ const /** @type {LintError[]} */ errors = [];
552
+ for (let i = 0, cur = start + this.getPadding(); i < this.childNodes.length; i++) {
553
+ const child = this.childNodes[i];
554
+ errors.push(...child.lint(cur));
555
+ cur += String(child).length + this.getGaps(i);
595
556
  }
596
- this.getAttribute('verifyChild')(j, 1);
597
- ({childNodes} = this);
598
- return getIndex(j, this);
557
+ return errors;
599
558
  }
600
559
 
601
560
  /**
602
- * 获取当前节点的绝对位置
603
- * @returns {number}
604
- * @complexity `n`
605
- */
606
- getAbsoluteIndex() {
607
- const {parentNode} = this;
608
- return parentNode ? parentNode.getAbsoluteIndex() + this.getRelativeIndex() : 0;
609
- }
610
-
611
- /**
612
- * 获取当前节点的相对位置,或其第`j`个子节点的相对位置
613
- * @param {number|undefined} j 子节点序号
614
- * @complexity `n`
561
+ * 以HTML格式打印
562
+ * @param {printOpt} opt 选项
563
+ * @returns {string}
615
564
  */
616
- #getPosition(j) {
617
- return j === undefined
618
- ? this.parentNode?.posFromIndex(this.getRelativeIndex()) ?? {top: 0, left: 0}
619
- : this.posFromIndex(this.getRelativeIndex(j));
620
- }
621
-
622
- /**
623
- * 获取当前节点的行列位置和大小
624
- * @complexity `n`
625
- */
626
- getBoundingClientRect() {
627
- const root = this.getRootNode();
628
- return {...this.#getDimension(), ...root.posFromIndex(this.getAbsoluteIndex())};
629
- }
630
-
631
- /** 第一个子节点前的间距 */
632
- getPadding() {
633
- return 0;
634
- }
635
-
636
- /** 子节点间距 */
637
- getGaps() {
638
- return 0;
639
- }
640
-
641
- /**
642
- * 行数
643
- * @complexity `n`
644
- */
645
- get offsetHeight() {
646
- return this.#getDimension().height;
647
- }
648
-
649
- /**
650
- * 最后一行的列数
651
- * @complexity `n`
652
- */
653
- get offsetWidth() {
654
- return this.#getDimension().width;
655
- }
656
-
657
- /**
658
- * 行号
659
- * @complexity `n`
660
- */
661
- get offsetTop() {
662
- return this.#getPosition().top;
663
- }
664
-
665
- /**
666
- * 列号
667
- * @complexity `n`
668
- */
669
- get offsetLeft() {
670
- return this.#getPosition().left;
565
+ print(opt = {}) {
566
+ return this.childNodes.length === 0
567
+ ? ''
568
+ : `<span class="wpb-${opt.class ?? this.type}">${print(this.childNodes, opt)}</span>`;
671
569
  }
672
570
 
673
571
  /**
674
- * 位置、大小和padding
675
- * @complexity `n`
572
+ * 保存为JSON
573
+ * @param {string} file 文件名
574
+ * @returns {Record<string, any>}
676
575
  */
677
- get style() {
678
- return {...this.#getPosition(), ...this.#getDimension(), padding: this.getPadding()};
679
- }
680
-
681
- /** 内部高度 */
682
- get clientHeight() {
683
- const {innerText} = this;
684
- return typeof innerText === 'string' ? innerText.split('\n').length : undefined;
685
- }
686
-
687
- /** 内部宽度 */
688
- get clientWidth() {
689
- const {innerText} = this;
690
- return typeof innerText === 'string' ? innerText.split('\n').at(-1).length : undefined;
576
+ json(file) {
577
+ const {childNodes, ...prop} = this,
578
+ json = {
579
+ ...prop,
580
+ childNodes: childNodes.map(child => child.type === 'text' ? String(child) : child.json()),
581
+ };
582
+ if (typeof file === 'string') {
583
+ fs.writeFileSync(
584
+ path.join(__dirname.slice(0, -4), 'printed', `${file}${file.endsWith('.json') ? '' : '.json'}`),
585
+ JSON.stringify(json, null, 2),
586
+ );
587
+ }
588
+ return json;
691
589
  }
692
590
 
693
591
  /**
694
592
  * 输出AST
695
- * @template {'markup'|'json'} T
696
- * @param {T} format 输出格式
697
- * @param {T extends 'markup' ? number : string} depth 输出深度
698
- * @returns {T extends 'markup' ? void : Record<string, any>}
593
+ * @param {number} depth 当前深度
594
+ * @returns {void}
699
595
  */
700
- print(format = 'markup', depth = 0) {
701
- if (format === 'json') {
702
- const {childNodes, ...prop} = this,
703
- json = {
704
- ...prop,
705
- childNodes: childNodes.map(child => child.type === 'text' ? String(child) : child.print('json')),
706
- };
707
- if (typeof depth === 'string') {
708
- fs.writeFileSync(
709
- `${__dirname.slice(0, -3)}printed/${depth}${depth.endsWith('.json') ? '' : '.json'}`,
710
- JSON.stringify(json, null, 2),
711
- );
712
- }
713
- return json;
714
- } else if (typeof depth !== 'number') {
596
+ echo(depth = 0) {
597
+ if (typeof depth !== 'number') {
715
598
  this.typeError('print', 'Number');
716
599
  }
717
600
  const indent = ' '.repeat(depth),
718
601
  str = String(this),
719
- {childNodes, type, firstChild} = this,
602
+ {childNodes, type} = this,
720
603
  {length} = childNodes;
721
- if (!str || length === 0 || firstChild.type === 'text' && String(firstChild) === str) {
604
+ if (childNodes.every(child => child.type === 'text' || !String(child))) {
722
605
  console.log(`${indent}\x1B[32m<%s>\x1B[0m${noWrap(str)}\x1B[32m</%s>\x1B[0m`, type, type);
723
606
  return undefined;
724
607
  }
@@ -736,11 +619,12 @@ class AstElement extends AstNode {
736
619
  } else if (child.type === 'text') {
737
620
  console.log(`${indent} ${noWrap(String(child))}`);
738
621
  } else {
739
- child.print('markup', depth + 1);
622
+ child.echo(depth + 1);
740
623
  }
741
- i += childStr.length + gap;
624
+ i += childStr.length;
742
625
  if (gap) {
743
- console.log(`${indent} ${noWrap(str.slice(i - gap, i))}`);
626
+ console.log(`${indent} ${noWrap(str.slice(i, i + gap))}`);
627
+ i += gap;
744
628
  }
745
629
  }
746
630
  if (i < str.length) {
package/lib/node.js CHANGED
@@ -561,7 +561,9 @@ class AstNode {
561
561
  const /** @type {AstText[]} */ childNodes = [...this.childNodes];
562
562
  for (let i = childNodes.length - 1; i >= 0; i--) {
563
563
  const {type, data} = childNodes[i];
564
- if (data === '') {
564
+ if (this.getGaps(i - 1)) {
565
+ //
566
+ } else if (data === '') {
565
567
  childNodes.splice(i, 1);
566
568
  } else if (type === 'text' && childNodes[i - 1]?.type === 'text') {
567
569
  childNodes[i - 1].setAttribute('data', childNodes[i - 1].data + data);
@@ -579,6 +581,162 @@ class AstNode {
579
581
  }
580
582
  return parentNode ?? this;
581
583
  }
584
+
585
+ /**
586
+ * 将字符位置转换为行列号
587
+ * @param {number} index 字符位置
588
+ * @complexity `n`
589
+ */
590
+ posFromIndex(index) {
591
+ if (typeof index !== 'number') {
592
+ this.typeError('posFromIndex', 'Number');
593
+ }
594
+ const str = String(this);
595
+ if (index >= -str.length && index <= str.length && Number.isInteger(index)) {
596
+ const lines = str.slice(0, index).split('\n');
597
+ return {top: lines.length - 1, left: lines.at(-1).length};
598
+ }
599
+ return undefined;
600
+ }
601
+
602
+ /**
603
+ * 将行列号转换为字符位置
604
+ * @param {number} top 行号
605
+ * @param {number} left 列号
606
+ * @complexity `n`
607
+ */
608
+ indexFromPos(top, left) {
609
+ if (typeof top !== 'number' || typeof left !== 'number') {
610
+ this.typeError('indexFromPos', 'Number');
611
+ }
612
+ const lines = String(this).split('\n');
613
+ return top >= 0 && left >= 0 && Number.isInteger(top) && Number.isInteger(left)
614
+ && lines.length >= top + 1 && lines[top].length >= left
615
+ ? lines.slice(0, top).reduce((acc, curLine) => acc + curLine.length + 1, 0) + left
616
+ : undefined;
617
+ }
618
+
619
+ /**
620
+ * 获取行数和最后一行的列数
621
+ * @complexity `n`
622
+ */
623
+ #getDimension() {
624
+ const lines = String(this).split('\n');
625
+ return {height: lines.length, width: lines.at(-1).length};
626
+ }
627
+
628
+ /** 第一个子节点前的间距 */
629
+ getPadding() {
630
+ return 0;
631
+ }
632
+
633
+ /** 子节点间距 */
634
+ getGaps() {
635
+ return 0;
636
+ }
637
+
638
+ /**
639
+ * 获取当前节点的相对字符位置,或其第`j`个子节点的相对字符位置
640
+ * @param {number|undefined} j 子节点序号
641
+ * @complexity `n`
642
+ */
643
+ getRelativeIndex(j) {
644
+ if (j !== undefined && typeof j !== 'number') {
645
+ this.typeError('getRelativeIndex', 'Number');
646
+ }
647
+ let /** @type {this[]} */ childNodes;
648
+
649
+ /**
650
+ * 获取子节点相对于父节点的字符位置,使用前需要先给`childNodes`赋值
651
+ * @param {number} end 子节点序号
652
+ * @param {this} parent 父节点
653
+ * @returns {number}
654
+ */
655
+ const getIndex = (end, parent) => childNodes.slice(0, end).reduce(
656
+ (acc, cur, i) => acc + String(cur).length + parent.getGaps(i),
657
+ 0,
658
+ ) + parent.getPadding();
659
+ if (j === undefined) {
660
+ const {parentNode} = this;
661
+ if (parentNode) {
662
+ ({childNodes} = parentNode);
663
+ return getIndex(childNodes.indexOf(this), parentNode);
664
+ }
665
+ return 0;
666
+ }
667
+ this.getAttribute('verifyChild')(j, 1);
668
+ ({childNodes} = this);
669
+ return getIndex(j, this);
670
+ }
671
+
672
+ /**
673
+ * 获取当前节点的绝对位置
674
+ * @returns {number}
675
+ * @complexity `n`
676
+ */
677
+ getAbsoluteIndex() {
678
+ const {parentNode} = this;
679
+ return parentNode ? parentNode.getAbsoluteIndex() + this.getRelativeIndex() : 0;
680
+ }
681
+
682
+ /**
683
+ * 获取当前节点的相对位置,或其第`j`个子节点的相对位置
684
+ * @param {number|undefined} j 子节点序号
685
+ * @complexity `n`
686
+ */
687
+ #getPosition(j) {
688
+ return j === undefined
689
+ ? this.parentNode?.posFromIndex(this.getRelativeIndex()) ?? {top: 0, left: 0}
690
+ : this.posFromIndex(this.getRelativeIndex(j));
691
+ }
692
+
693
+ /**
694
+ * 获取当前节点的行列位置和大小
695
+ * @complexity `n`
696
+ */
697
+ getBoundingClientRect() {
698
+ return {...this.#getDimension(), ...this.getRootNode().posFromIndex(this.getAbsoluteIndex())};
699
+ }
700
+
701
+ /**
702
+ * 行数
703
+ * @complexity `n`
704
+ */
705
+ get offsetHeight() {
706
+ return this.#getDimension().height;
707
+ }
708
+
709
+ /**
710
+ * 最后一行的列数
711
+ * @complexity `n`
712
+ */
713
+ get offsetWidth() {
714
+ return this.#getDimension().width;
715
+ }
716
+
717
+ /**
718
+ * 相对于父容器的行号
719
+ * @complexity `n`
720
+ */
721
+ get offsetTop() {
722
+ return this.#getPosition().top;
723
+ }
724
+
725
+ /**
726
+ * 相对于父容器的列号
727
+ * @complexity `n`
728
+ */
729
+ get offsetLeft() {
730
+ return this.#getPosition().left;
731
+ }
732
+
733
+ /**
734
+ * 位置、大小和padding
735
+ * @complexity `n`
736
+ */
737
+ get style() {
738
+ return {...this.#getPosition(), ...this.#getDimension(), padding: this.getPadding()};
739
+ }
582
740
  }
583
741
 
584
742
  Parser.classes.AstNode = __filename;
package/lib/ranges.js CHANGED
@@ -66,8 +66,7 @@ class Range {
66
66
  * @complexity `n`
67
67
  */
68
68
  applyTo(arr) {
69
- return new Array(typeof arr === 'number' ? arr : arr.length).fill().map((_, i) => i)
70
- .slice(this.start, this.end)
69
+ return new Array(typeof arr === 'number' ? arr : arr.length).fill().map((_, i) => i).slice(this.start, this.end)
71
70
  .filter((_, j) => j % this.step === 0);
72
71
  }
73
72
  }
package/lib/text.js CHANGED
@@ -33,6 +33,7 @@ class AstText extends AstNode {
33
33
  * @template {string} T
34
34
  * @param {T} key 属性键
35
35
  * @returns {TokenAttribute<T>}
36
+ * @throws `Error` 文本节点没有子节点
36
37
  */
37
38
  getAttribute(key) {
38
39
  return key === 'verifyChild'
@@ -70,7 +71,6 @@ class AstText extends AstNode {
70
71
  /**
71
72
  * 在后方添加字符串
72
73
  * @param {string} text 添加的字符串
73
- * @throws `Error` 禁止外部调用
74
74
  */
75
75
  appendData(text) {
76
76
  this.#setData(this.data + text);
@@ -80,8 +80,6 @@ class AstText extends AstNode {
80
80
  * 删减字符串
81
81
  * @param {number} offset 起始位置
82
82
  * @param {number} count 删减字符数
83
- * @throws `RangeError` 错误的删减位置
84
- * @throws `Error` 禁止外部调用
85
83
  */
86
84
  deleteData(offset, count) {
87
85
  this.#setData(this.data.slice(0, offset) + this.data.slice(offset + count));
@@ -91,8 +89,6 @@ class AstText extends AstNode {
91
89
  * 插入字符串
92
90
  * @param {number} offset 插入位置
93
91
  * @param {string} text 待插入的字符串
94
- * @throws `RangeError` 错误的插入位置
95
- * @throws `Error` 禁止外部调用
96
92
  */
97
93
  insertData(offset, text) {
98
94
  this.#setData(this.data.slice(0, offset) + text + this.data.slice(offset));
@@ -101,7 +97,6 @@ class AstText extends AstNode {
101
97
  /**
102
98
  * 替换字符串
103
99
  * @param {string} text 替换的字符串
104
- * @throws `Error` 禁止外部调用
105
100
  */
106
101
  replaceData(text = '') {
107
102
  this.#setData(text);
@@ -140,6 +135,40 @@ class AstText extends AstNode {
140
135
  parentNode.setAttribute('childNodes', childNodes);
141
136
  return newText;
142
137
  }
138
+
139
+ static errorSyntax = /[{}]+|\[{2,}|\[(?!(?:(?!https?\b)[^[])*\])|(?<=^|\])([^[]*?)\]+|<(?=\s*\/?\w+[\s/>])/giu;
140
+
141
+ /**
142
+ * Linter
143
+ * @param {number} start 起始位置
144
+ * @returns {LintError[]}
145
+ */
146
+ lint(start = 0) {
147
+ const {data} = this,
148
+ errors = [...data.matchAll(AstText.errorSyntax)];
149
+ if (errors.length > 0) {
150
+ const {top, left} = this.getRootNode().posFromIndex(start);
151
+ return errors.map(({0: error, 1: prefix, index}) => {
152
+ if (prefix) {
153
+ index += prefix.length;
154
+ error = error.slice(prefix.length);
155
+ }
156
+ const lines = data.slice(0, index).split('\n'),
157
+ startLine = lines.length + top - 1,
158
+ {length} = lines.at(-1),
159
+ startCol = lines.length > 1 ? length : left + length;
160
+ return {
161
+ message: `孤立的"${error[0]}"`,
162
+ severity: error[0] === '{' || error[0] === '}' ? 'error' : 'warning',
163
+ startLine,
164
+ endLine: startLine,
165
+ startCol,
166
+ endCol: startCol + error.length,
167
+ };
168
+ });
169
+ }
170
+ return [];
171
+ }
143
172
  }
144
173
 
145
174
  Parser.classes.AstText = __filename;
package/lib/title.js CHANGED
@@ -55,7 +55,7 @@ class Title {
55
55
  }
56
56
  this.main = title && `${title[0].toUpperCase()}${title.slice(1)}`;
57
57
  this.prefix = `${namespace}${namespace && ':'}`;
58
- this.title = `${iw ? `${this.interwiki}:` : ''}${this.prefix}${this.main}`;
58
+ this.title = `${iw ? `${this.interwiki}:` : ''}${this.prefix}${this.main.replaceAll(' ', '_')}`;
59
59
  this.valid = Boolean(this.main || this.fragment) && !/\0\d+[eh!+-]\x7F|[<>[\]{}|]/u.test(this.title);
60
60
  }
61
61
 
@@ -28,11 +28,11 @@ const fixedToken = Constructor => class extends Constructor {
28
28
  * @throws `Error`
29
29
  */
30
30
  insertAt(token, i = this.childNodes.length) {
31
- if (!Parser.running) {
32
- throw new Error(`${this.constructor.name} 不可插入元素!`);
31
+ if (Parser.running) {
32
+ super.insertAt(token, i);
33
+ return token;
33
34
  }
34
- super.insertAt(token, i);
35
- return token;
35
+ throw new Error(`${this.constructor.name} 不可插入元素!`);
36
36
  }
37
37
  };
38
38