wikiparser-node 0.3.0 → 0.4.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/.eslintrc.json +472 -34
  2. package/README.md +1 -1
  3. package/config/default.json +58 -30
  4. package/config/llwiki.json +22 -90
  5. package/config/moegirl.json +51 -13
  6. package/config/zhwiki.json +1269 -0
  7. package/index.js +114 -104
  8. package/lib/element.js +448 -440
  9. package/lib/node.js +335 -115
  10. package/lib/ranges.js +27 -18
  11. package/lib/text.js +146 -0
  12. package/lib/title.js +13 -5
  13. package/mixin/attributeParent.js +70 -24
  14. package/mixin/fixedToken.js +14 -6
  15. package/mixin/hidden.js +6 -4
  16. package/mixin/sol.js +27 -10
  17. package/package.json +9 -3
  18. package/parser/brackets.js +22 -17
  19. package/parser/commentAndExt.js +18 -16
  20. package/parser/converter.js +14 -13
  21. package/parser/externalLinks.js +12 -11
  22. package/parser/hrAndDoubleUnderscore.js +23 -14
  23. package/parser/html.js +10 -9
  24. package/parser/links.js +15 -14
  25. package/parser/list.js +12 -11
  26. package/parser/magicLinks.js +12 -11
  27. package/parser/quotes.js +6 -5
  28. package/parser/selector.js +175 -0
  29. package/parser/table.js +25 -18
  30. package/printed/example.json +120 -0
  31. package/src/arg.js +56 -32
  32. package/src/atom/hidden.js +5 -2
  33. package/src/atom/index.js +17 -9
  34. package/src/attribute.js +182 -100
  35. package/src/converter.js +68 -41
  36. package/src/converterFlags.js +67 -45
  37. package/src/converterRule.js +117 -65
  38. package/src/extLink.js +66 -18
  39. package/src/gallery.js +42 -15
  40. package/src/heading.js +34 -15
  41. package/src/html.js +97 -35
  42. package/src/imageParameter.js +83 -54
  43. package/src/index.js +299 -178
  44. package/src/link/category.js +20 -52
  45. package/src/link/file.js +59 -28
  46. package/src/link/galleryImage.js +21 -7
  47. package/src/link/index.js +146 -60
  48. package/src/magicLink.js +34 -12
  49. package/src/nowiki/comment.js +22 -10
  50. package/src/nowiki/dd.js +37 -22
  51. package/src/nowiki/doubleUnderscore.js +16 -7
  52. package/src/nowiki/hr.js +11 -7
  53. package/src/nowiki/index.js +16 -9
  54. package/src/nowiki/list.js +2 -2
  55. package/src/nowiki/noinclude.js +8 -4
  56. package/src/nowiki/quote.js +11 -7
  57. package/src/onlyinclude.js +19 -7
  58. package/src/parameter.js +65 -38
  59. package/src/syntax.js +26 -20
  60. package/src/table/index.js +260 -165
  61. package/src/table/td.js +98 -52
  62. package/src/table/tr.js +102 -58
  63. package/src/tagPair/ext.js +27 -19
  64. package/src/tagPair/include.js +16 -11
  65. package/src/tagPair/index.js +64 -29
  66. package/src/transclude.js +170 -93
  67. package/test/api.js +83 -0
  68. package/test/real.js +133 -0
  69. package/test/test.js +28 -0
  70. package/test/util.js +80 -0
  71. package/tool/index.js +41 -31
  72. package/typings/api.d.ts +13 -0
  73. package/typings/array.d.ts +28 -0
  74. package/typings/event.d.ts +24 -0
  75. package/typings/index.d.ts +46 -4
  76. package/typings/node.d.ts +15 -9
  77. package/typings/parser.d.ts +7 -0
  78. package/typings/tool.d.ts +3 -2
  79. package/util/debug.js +21 -18
  80. package/util/string.js +40 -27
  81. package/typings/element.d.ts +0 -28
package/src/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  /*
4
4
  * PHP解析器的步骤:
5
5
  * -1. 替换签名和`{{subst:}}`,参见Parser::preSaveTransform;这在revision中不可能保留,可以跳过
6
- * 0. 移除特定字符`\x00`和`\x7f`,参见Parser::parse
6
+ * 0. 移除特定字符`\0`和`\x7f`,参见Parser::parse
7
7
  * 1. 注释/扩展标签('<'相关),参见Preprocessor_Hash::buildDomTreeArrayFromText和Sanitizer::decodeTagAttributes
8
8
  * 2. 模板/模板变量/标题,注意rightmost法则,以及`-{`和`[[`可以破坏`{{`或`{{{`语法,
9
9
  * 参见Preprocessor_Hash::buildDomTreeArrayFromText
@@ -19,7 +19,7 @@
19
19
  */
20
20
 
21
21
  /*
22
- * \x00\d+.\x7f标记Token:
22
+ * \0\d+.\x7f标记Token:
23
23
  * e: ExtToken
24
24
  * c: CommentToken、NoIncludeToken和IncludeToken
25
25
  * !: `{{!}}`专用
@@ -42,31 +42,132 @@
42
42
  */
43
43
 
44
44
  const {externalUse} = require('../util/debug'),
45
+ {text} = require('../util/string'),
45
46
  Ranges = require('../lib/ranges'),
46
- AstElement = require('../lib/element'),
47
47
  assert = require('assert/strict'),
48
- /** @type {Parser} */ Parser = require('..'),
49
- {MAX_STAGE} = Parser;
48
+ Parser = require('..'),
49
+ AstElement = require('../lib/element'),
50
+ AstText = require('../lib/text');
51
+ const {MAX_STAGE, aliases} = Parser;
50
52
 
53
+ /**
54
+ * 所有节点的基类
55
+ * @classdesc `{childNodes: ...(AstText|Token)}`
56
+ */
51
57
  class Token extends AstElement {
52
58
  type = 'root';
53
- /** 解析阶段,参见顶部注释。只对plain Token有意义。 */ #stage = 0;
59
+ #stage = 0; // 解析阶段,参见顶部注释。只对plain Token有意义。
54
60
  #config;
55
- /** 这个数组起两个作用:1. 数组中的Token会在build时替换`/\x00\d+.\x7f/`标记;2. 数组中的Token会依次执行parseOnce和build方法。 */
61
+ // 这个数组起两个作用:1. 数组中的Token会在build时替换`/\0\d+.\x7f/`标记;2. 数组中的Token会依次执行parseOnce和build方法。
56
62
  #accum;
57
63
  /** @type {Record<string, Ranges>} */ #acceptable;
58
64
  #protectedChildren = new Ranges();
59
65
  /** @type {boolean} */ #include;
60
66
 
61
67
  /**
62
- * @param {?string} wikitext
68
+ * 保护部分子节点不被移除
69
+ * @param {...string|number|Range} args 子节点范围
70
+ */
71
+ #protectChildren = (...args) => {
72
+ this.#protectedChildren.push(...new Ranges(args));
73
+ };
74
+
75
+ /**
76
+ * 将维基语法替换为占位符
77
+ * @param {number} n 解析阶段
78
+ * @param {boolean} include 是否嵌入
79
+ */
80
+ #parseOnce = (n = this.#stage, include = false) => {
81
+ if (n < this.#stage || !this.isPlain() || this.childNodes.length === 0) {
82
+ return this;
83
+ }
84
+ switch (n) {
85
+ case 0:
86
+ if (this.type === 'root') {
87
+ this.#accum.shift();
88
+ }
89
+ this.#parseCommentAndExt(include);
90
+ break;
91
+ case 1:
92
+ this.#parseBrackets();
93
+ break;
94
+ case 2:
95
+ this.#parseHtml();
96
+ break;
97
+ case 3:
98
+ this.#parseTable();
99
+ break;
100
+ case 4:
101
+ this.#parseHrAndDoubleUndescore();
102
+ break;
103
+ case 5:
104
+ this.#parseLinks();
105
+ break;
106
+ case 6:
107
+ this.#parseQuotes();
108
+ break;
109
+
110
+ case 7:
111
+ this.#parseExternalLinks();
112
+ break;
113
+ case 8:
114
+ this.#parseMagicLinks();
115
+ break;
116
+ case 9:
117
+ this.#parseList();
118
+ break;
119
+ case 10:
120
+ this.#parseConverter();
121
+ // no default
122
+ }
123
+ if (this.type === 'root') {
124
+ for (const token of this.#accum) {
125
+ token.getAttribute('parseOnce')(n, include);
126
+ }
127
+ }
128
+ this.#stage++;
129
+ return this;
130
+ };
131
+
132
+ /**
133
+ * 重建wikitext
134
+ * @param {string} str 半解析的字符串
135
+ * @complexity `n`
136
+ * @returns {(Token|AstText)[]}
137
+ */
138
+ #buildFromStr = str => str.split(/[\0\x7F]/u).map((s, i) => {
139
+ if (i % 2 === 0) {
140
+ return new AstText(s);
141
+ } else if (isNaN(s.at(-1))) {
142
+ return this.#accum[Number(s.slice(0, -1))];
143
+ }
144
+ throw new Error(`解析错误!未正确标记的 Token:${s}`);
145
+ });
146
+
147
+ /** 所有图片,包括图库 */
148
+ get images() {
149
+ return this.querySelectorAll('file, gallery-image');
150
+ }
151
+
152
+ /** 所有内链、外链和自由外链 */
153
+ get links() {
154
+ return this.querySelectorAll('link, ext-link, free-ext-link');
155
+ }
156
+
157
+ /** 所有模板和模块 */
158
+ get embeds() {
159
+ return this.querySelectorAll('template, magic-word#invoke');
160
+ }
161
+
162
+ /**
163
+ * @param {string} wikitext wikitext
63
164
  * @param {accum} accum
64
- * @param {acceptable} acceptable
165
+ * @param {acceptable} acceptable 可接受的子节点设置
65
166
  */
66
167
  constructor(wikitext, config = Parser.getConfig(), halfParsed = false, accum = [], acceptable = null) {
67
168
  super();
68
169
  if (typeof wikitext === 'string') {
69
- this.appendChild(halfParsed ? wikitext : wikitext.replace(/[\x00\x7f]/g, ''));
170
+ this.appendChild(halfParsed ? wikitext : wikitext.replaceAll('\0', '').replaceAll('\x7F', ''));
70
171
  }
71
172
  this.#config = config;
72
173
  this.#accum = accum;
@@ -74,32 +175,38 @@ class Token extends AstElement {
74
175
  accum.push(this);
75
176
  }
76
177
 
77
- /** @complexity `n` */
78
- cloneChildren() {
79
- if (!Parser.debugging && externalUse('cloneChildren')) {
80
- this.debugOnly('cloneChildren');
81
- }
82
- return this.childNodes.map(child => typeof child === 'string' ? child : child.cloneNode());
178
+ /**
179
+ * 深拷贝所有子节点
180
+ * @complexity `n`
181
+ * @returns {(AstText|Token)[]}
182
+ */
183
+ cloneChildNodes() {
184
+ return this.childNodes.map(child => child.cloneNode());
83
185
  }
84
186
 
85
- /** @complexity `n` */
187
+ /**
188
+ * 深拷贝节点
189
+ * @complexity `n`
190
+ * @throws `Error` 未定义复制方法
191
+ */
86
192
  cloneNode() {
87
193
  if (!this.isPlain()) {
88
194
  throw new Error(`未定义 ${this.constructor.name} 的复制方法!`);
89
195
  }
90
- const cloned = this.cloneChildren();
196
+ const cloned = this.cloneChildNodes();
91
197
  return Parser.run(() => {
92
198
  const token = new Token(undefined, this.#config, false, [], this.#acceptable);
93
199
  token.type = this.type;
94
200
  token.append(...cloned);
95
- token.protectChildren(...this.#protectedChildren);
201
+ token.getAttribute('protectChildren')(...this.#protectedChildren);
96
202
  return token;
97
203
  });
98
204
  }
99
205
 
100
206
  /**
207
+ * @override
101
208
  * @template {string} T
102
- * @param {T} key
209
+ * @param {T} key 属性键
103
210
  * @returns {TokenAttribute<T>}
104
211
  */
105
212
  getAttribute(key) {
@@ -112,18 +219,28 @@ class Token extends AstElement {
112
219
  return this.#accum;
113
220
  case 'acceptable':
114
221
  return this.#acceptable ? {...this.#acceptable} : null;
222
+ case 'protectChildren':
223
+ return this.#protectChildren;
115
224
  case 'protectedChildren':
116
225
  return new Ranges(this.#protectedChildren);
226
+ case 'parseOnce':
227
+ return this.#parseOnce;
228
+ case 'buildFromStr':
229
+ return this.#buildFromStr;
117
230
  case 'include': {
118
231
  if (this.#include !== undefined) {
119
232
  return this.#include;
120
233
  }
121
- const includeToken = this.querySelector('include');
234
+ const root = this.getRootNode();
235
+ if (root.type === 'root' && root !== this) {
236
+ return root.getAttribute('include');
237
+ }
238
+ const includeToken = root.querySelector('include');
122
239
  if (includeToken) {
123
240
  return includeToken.name === 'noinclude';
124
241
  }
125
- const noincludeToken = this.querySelector('noinclude');
126
- return Boolean(noincludeToken) && !/^<\/?noinclude(?:\s[^>]*)?\/?>$/i.test(noincludeToken.toString());
242
+ const noincludeToken = root.querySelector('noinclude');
243
+ return Boolean(noincludeToken) && !/^<\/?noinclude(?:\s[^>]*)?\/?>$/iu.test(String(noincludeToken));
127
244
  }
128
245
  default:
129
246
  return super.getAttribute(key);
@@ -131,14 +248,16 @@ class Token extends AstElement {
131
248
  }
132
249
 
133
250
  /**
251
+ * @override
134
252
  * @template {string} T
135
- * @param {T} key
136
- * @param {TokenAttribute<T>} value
253
+ * @param {T} key 属性键
254
+ * @param {TokenAttribute<T>} value 属性值
255
+ * @throws `RangeError` 禁止手动指定私有属性
137
256
  */
138
257
  setAttribute(key, value) {
139
- if (key === 'include' || !Parser.running && ['config', 'accum'].includes(key)) {
258
+ if (key === 'include' || !Parser.running && (key === 'config' || key === 'accum')) {
140
259
  throw new RangeError(`禁止手动指定私有的 #${key} 属性!`);
141
- } else if (!Parser.debugging && ['stage', 'acceptable', 'protectedChildren'].includes(key)
260
+ } else if (!Parser.debugging && (key === 'stage' || key === 'acceptable' || key === 'protectedChildren')
142
261
  && externalUse('setAttribute')
143
262
  ) {
144
263
  throw new RangeError(`使用 ${this.constructor.name}.setAttribute 方法设置私有属性 #${key} 仅用于代码调试!`);
@@ -165,11 +284,11 @@ class Token extends AstElement {
165
284
  for (const [k, v] of Object.entries(value)) {
166
285
  if (k.startsWith('Stage-')) {
167
286
  for (let i = 0; i <= Number(k.slice(6)); i++) {
168
- for (const type of Parser.aliases[i]) {
287
+ for (const type of aliases[i]) {
169
288
  acceptable[type] = new Ranges(v);
170
289
  }
171
290
  }
172
- } else if (k.startsWith('!')) { // `!`项必须放在最后
291
+ } else if (k[0] === '!') { // `!`项必须放在最后
173
292
  delete acceptable[k.slice(1)];
174
293
  } else {
175
294
  acceptable[k] = new Ranges(v);
@@ -184,22 +303,17 @@ class Token extends AstElement {
184
303
  }
185
304
  }
186
305
 
306
+ /** 是否是普通节点 */
187
307
  isPlain() {
188
308
  return this.constructor === Token;
189
309
  }
190
310
 
191
- /** @param {...string|number|Range} args */
192
- protectChildren(...args) {
193
- if (!Parser.debugging && externalUse('protectChildren')) {
194
- this.debugOnly('protectChildren');
195
- }
196
- this.#protectedChildren.push(...new Ranges(args));
197
- }
198
-
199
311
  /**
200
- * @param {number} i
201
- * @returns {string|Token}
312
+ * @override
313
+ * @param {number} i 移除位置
314
+ * @returns {Token}
202
315
  * @complexity `n`
316
+ * @throws `Error` 不可移除的子节点
203
317
  */
204
318
  removeAt(i) {
205
319
  if (typeof i !== 'number') {
@@ -225,18 +339,25 @@ class Token extends AstElement {
225
339
  }
226
340
 
227
341
  /**
342
+ * @override
228
343
  * @template {string|Token} T
229
- * @param {T} token
344
+ * @param {T} token 待插入的子节点
345
+ * @param {number} i 插入位置
230
346
  * @complexity `n`
347
+ * @returns {T extends Token ? Token : AstText}
348
+ * @throws `RangeError` 不可插入的子节点
231
349
  */
232
350
  insertAt(token, i = this.childNodes.length) {
351
+ if (typeof token === 'string') {
352
+ token = new AstText(token);
353
+ }
233
354
  if (!Parser.running && this.#acceptable) {
234
355
  const acceptableIndices = Object.fromEntries(
235
356
  Object.entries(this.#acceptable)
236
357
  .map(([str, ranges]) => [str, ranges.applyTo(this.childNodes.length + 1)]),
237
358
  ),
238
359
  nodesAfter = this.childNodes.slice(i),
239
- insertedName = token.constructor.name,
360
+ {constructor: {name: insertedName}} = token,
240
361
  k = i < 0 ? i + this.childNodes.length : i;
241
362
  if (!acceptableIndices[insertedName].includes(k)) {
242
363
  throw new RangeError(`${this.constructor.name} 的第 ${k} 个子节点不能为 ${insertedName}!`);
@@ -245,15 +366,18 @@ class Token extends AstElement {
245
366
  }
246
367
  }
247
368
  super.insertAt(token, i);
248
- if (token instanceof Token && token.type === 'root') {
369
+ if (token.type === 'root') {
249
370
  token.type = 'plain';
250
371
  }
251
372
  return token;
252
373
  }
253
374
 
254
375
  /**
255
- * @param {Token} token
376
+ * 替换为同类节点
377
+ * @param {Token} token 待替换的节点
256
378
  * @complexity `n`
379
+ * @throws `Error` 不存在父节点
380
+ * @throws `Error` 待替换的节点具有不同属性
257
381
  */
258
382
  safeReplaceWith(token) {
259
383
  const {parentNode} = this;
@@ -280,28 +404,78 @@ class Token extends AstElement {
280
404
  token.dispatchEvent(e, {position: i, oldToken: this, newToken: token});
281
405
  }
282
406
 
283
- /** @param {string} title */
407
+ /**
408
+ * 创建HTML注释
409
+ * @param {string} data 注释内容
410
+ */
411
+ createComment(data = '') {
412
+ if (typeof data === 'string') {
413
+ const CommentToken = require('./nowiki/comment');
414
+ const config = this.getAttribute('config');
415
+ return Parser.run(() => new CommentToken(data.replaceAll('-->', '--&gt;'), true, config));
416
+ }
417
+ return this.typeError('createComment', 'String');
418
+ }
419
+
420
+ /**
421
+ * 创建标签
422
+ * @param {string} tagName 标签名
423
+ * @param {{selfClosing: boolean, closing: boolean}} options 选项
424
+ * @throws `RangeError` 非法的标签名
425
+ */
426
+ createElement(tagName, {selfClosing, closing} = {}) {
427
+ if (typeof tagName !== 'string') {
428
+ this.typeError('createElement', 'String');
429
+ }
430
+ const config = this.getAttribute('config'),
431
+ include = this.getAttribute('include');
432
+ if (tagName === (include ? 'noinclude' : 'includeonly')) {
433
+ const IncludeToken = require('./tagPair/include');
434
+ return Parser.run(
435
+ () => new IncludeToken(tagName, '', undefined, selfClosing ? undefined : tagName, config),
436
+ );
437
+ } else if (config.ext.includes(tagName)) {
438
+ const ExtToken = require('./tagPair/ext');
439
+ return Parser.run(() => new ExtToken(tagName, '', '', selfClosing ? undefined : '', config));
440
+ } else if (config.html.flat().includes(tagName)) {
441
+ const HtmlToken = require('./html');
442
+ return Parser.run(() => new HtmlToken(tagName, '', closing, selfClosing, config));
443
+ }
444
+ throw new RangeError(`非法的标签名!${tagName}`);
445
+ }
446
+
447
+ /**
448
+ * 判断标题是否是跨维基链接
449
+ * @param {string} title 标题
450
+ */
284
451
  isInterwiki(title) {
285
452
  return Parser.isInterwiki(title, this.#config);
286
453
  }
287
454
 
288
- /** @param {string} title */
455
+ /**
456
+ * 规范化页面标题
457
+ * @param {string} title 标题(含或不含命名空间前缀)
458
+ * @param {number} defaultNs 命名空间
459
+ */
289
460
  normalizeTitle(title, defaultNs = 0, halfParsed = false) {
290
461
  return Parser.normalizeTitle(title, defaultNs, this.#include, this.#config, halfParsed);
291
462
  }
292
463
 
293
- /** @complexity `n` */
464
+ /**
465
+ * 获取全部章节
466
+ * @complexity `n`
467
+ */
294
468
  sections() {
295
469
  if (this.type !== 'root') {
296
- return;
470
+ return undefined;
297
471
  }
298
472
  const {childNodes} = this,
299
- headings = [...childNodes.entries()]
300
- .filter(([, child]) => child instanceof Token && child.type === 'heading')
301
- .map(/** @param {[number, Token]} */ ([i, {name}]) => [i, Number(name)]),
302
- lastHeading = [-1, -1, -1, -1, -1, -1];
303
- const /** @type {(string|Token)[][]} */ sections = new Array(headings.length);
304
- for (const [i, [index, level]] of headings.entries()) {
473
+ headings = [...childNodes.entries()].filter(([, {type}]) => type === 'heading')
474
+ .map(([i, {name}]) => [i, Number(name)]),
475
+ lastHeading = [-1, -1, -1, -1, -1, -1],
476
+ /** @type {(AstText|Token)[][]} */ sections = new Array(headings.length);
477
+ for (let i = 0; i < headings.length; i++) {
478
+ const [index, level] = headings[i];
305
479
  for (let j = level; j < 6; j++) {
306
480
  const last = lastHeading[j];
307
481
  if (last >= 0) {
@@ -320,20 +494,20 @@ class Token extends AstElement {
320
494
  }
321
495
 
322
496
  /**
323
- * @param {number} n
497
+ * 获取指定章节
498
+ * @param {number} n 章节序号
324
499
  * @complexity `n`
325
500
  */
326
501
  section(n) {
327
- if (typeof n !== 'number') {
328
- this.typeError('section', 'Number');
329
- }
330
- return this.sections()[n];
502
+ return typeof n === 'number' ? this.sections()?.[n] : this.typeError('section', 'Number');
331
503
  }
332
504
 
333
505
  /**
334
- * @param {string|undefined} tag
506
+ * 获取指定的外层HTML标签
507
+ * @param {string|undefined} tag HTML标签名
335
508
  * @returns {[Token, Token]}
336
509
  * @complexity `n`
510
+ * @throws `RangeError` 非法的标签或空标签
337
511
  */
338
512
  findEnclosingHtml(tag) {
339
513
  if (tag !== undefined && typeof tag !== 'string') {
@@ -343,11 +517,11 @@ class Token extends AstElement {
343
517
  if (tag !== undefined && !this.#config.html.slice(0, 2).flat().includes(tag)) {
344
518
  throw new RangeError(`非法的标签或空标签:${tag}`);
345
519
  }
346
- const {parentElement} = this;
347
- if (!parentElement) {
348
- return;
520
+ const {parentNode} = this;
521
+ if (!parentNode) {
522
+ return undefined;
349
523
  }
350
- const {children} = parentElement,
524
+ const {children} = parentNode,
351
525
  index = children.indexOf(this);
352
526
  let i;
353
527
  for (i = index - 1; i >= 0; i--) {
@@ -356,7 +530,7 @@ class Token extends AstElement {
356
530
  }
357
531
  }
358
532
  if (i === -1) {
359
- return parentElement.findEnclosingHtml(tag);
533
+ return parentNode.findEnclosingHtml(tag);
360
534
  }
361
535
  const opening = children[i],
362
536
  {name} = opening;
@@ -366,15 +540,22 @@ class Token extends AstElement {
366
540
  }
367
541
  }
368
542
  return i === children.length
369
- ? parentElement.findEnclosingHtml(tag)
543
+ ? parentNode.findEnclosingHtml(tag)
370
544
  : [opening, children[i]];
371
545
  }
372
546
 
373
- /** @complexity `n` */
547
+ /**
548
+ * 获取全部分类
549
+ * @complexity `n`
550
+ */
374
551
  getCategories() {
375
552
  return this.querySelectorAll('category').map(({name, sortkey}) => [name, sortkey]);
376
553
  }
377
554
 
555
+ /**
556
+ * 重新解析单引号
557
+ * @throws `Error` 不接受QuoteToken作为子节点
558
+ */
378
559
  redoQuotes() {
379
560
  const acceptable = this.getAttribute('acceptable');
380
561
  if (acceptable && !acceptable.QuoteToken?.some(
@@ -383,24 +564,24 @@ class Token extends AstElement {
383
564
  throw new Error(`${this.constructor.name} 不接受 QuoteToken 作为子节点!`);
384
565
  }
385
566
  for (const quote of this.childNodes) {
386
- if (quote instanceof Token && quote.type === 'quote') {
387
- quote.replaceWith(quote.firstChild);
567
+ if (quote.type === 'quote') {
568
+ quote.replaceWith(String(quote));
388
569
  }
389
570
  }
390
571
  this.normalize();
391
- /** @type {[number, string][]} */
392
- const textNodes = [...this.childNodes.entries()].filter(([, child]) => typeof child === 'string'),
572
+ /** @type {[number, AstText][]} */
573
+ const textNodes = [...this.childNodes.entries()].filter(([, {type}]) => type === 'text'),
393
574
  indices = textNodes.map(([i]) => this.getRelativeIndex(i)),
394
575
  token = Parser.run(() => {
395
- const root = new Token(textNodes.map(([, str]) => str).join(''), this.getAttribute('config'));
576
+ const root = new Token(text(textNodes.map(([, str]) => str)), this.getAttribute('config'));
396
577
  return root.setAttribute('stage', 6).parse(7);
397
578
  });
398
579
  for (const quote of token.children.reverse()) {
399
580
  if (quote.type === 'quote') {
400
581
  const index = quote.getRelativeIndex(),
401
582
  n = indices.findLastIndex(textIndex => textIndex <= index);
402
- this.splitText(n, index - indices[n]);
403
- this.splitText(n + 1, Number(quote.name));
583
+ this.childNodes[n].splitText(index - indices[n]);
584
+ this.childNodes[n + 1].splitText(Number(quote.name));
404
585
  this.removeAt(n + 1);
405
586
  this.insertAt(quote, n + 1);
406
587
  }
@@ -408,82 +589,6 @@ class Token extends AstElement {
408
589
  this.normalize();
409
590
  }
410
591
 
411
- /**
412
- * 将维基语法替换为占位符
413
- * @this {Token & {firstChild: string}}
414
- */
415
- parseOnce(n = this.#stage, include = false) {
416
- if (!Parser.debugging && externalUse('parseOnce')) {
417
- this.debugOnly('parseOnce');
418
- } else if (n < this.#stage || !this.isPlain() || this.childNodes.length === 0) {
419
- return this;
420
- }
421
- switch (n) {
422
- case 0:
423
- if (this.type === 'root') {
424
- this.#accum.shift();
425
- }
426
- this.#parseCommentAndExt(include);
427
- break;
428
- case 1:
429
- this.#parseBrackets();
430
- break;
431
- case 2:
432
- this.#parseHtml();
433
- break;
434
- case 3:
435
- this.#parseTable();
436
- break;
437
- case 4:
438
- this.#parseHrAndDoubleUndescore();
439
- break;
440
- case 5:
441
- this.#parseLinks();
442
- break;
443
- case 6: {
444
- this.#parseQuotes();
445
- break;
446
- }
447
- case 7:
448
- this.#parseExternalLinks();
449
- break;
450
- case 8:
451
- this.#parseMagicLinks();
452
- break;
453
- case 9:
454
- this.#parseList();
455
- break;
456
- case 10:
457
- this.#parseConverter();
458
- // no default
459
- }
460
- if (this.type === 'root') {
461
- for (const token of this.#accum) {
462
- token.parseOnce(n, include);
463
- }
464
- }
465
- this.#stage++;
466
- return this;
467
- }
468
-
469
- /**
470
- * @param {string} str
471
- * @complexity `n`
472
- */
473
- buildFromStr(str) {
474
- if (!Parser.debugging && externalUse('buildFromStr')) {
475
- this.debugOnly('buildFromStr');
476
- }
477
- return str.split(/[\x00\x7f]/).map((s, i) => {
478
- if (i % 2 === 0) {
479
- return s;
480
- } else if (!isNaN(s.at(-1))) {
481
- throw new Error(`解析错误!未正确标记的 Token:${s}`);
482
- }
483
- return this.#accum[Number(s.slice(0, -1))];
484
- });
485
- }
486
-
487
592
  /**
488
593
  * 将占位符替换为子Token
489
594
  * @complexity `n`
@@ -493,15 +598,15 @@ class Token extends AstElement {
493
598
  this.debugOnly('build');
494
599
  }
495
600
  this.#stage = MAX_STAGE;
496
- const {childNodes: {length}, firstChild} = this;
497
- if (length !== 1 || typeof firstChild !== 'string' || !firstChild.includes('\x00')) {
498
- return this;
499
- }
500
- this.replaceChildren(...this.buildFromStr(firstChild));
501
- this.normalize();
502
- if (this.type === 'root') {
503
- for (const token of this.#accum) {
504
- token.build();
601
+ const {childNodes: {length}, firstChild} = this,
602
+ str = String(firstChild);
603
+ if (length === 1 && firstChild.type === 'text' && str.includes('\0')) {
604
+ this.replaceChildren(...this.#buildFromStr(str));
605
+ this.normalize();
606
+ if (this.type === 'root') {
607
+ for (const token of this.#accum) {
608
+ token.build();
609
+ }
505
610
  }
506
611
  }
507
612
  return this;
@@ -519,35 +624,46 @@ class Token extends AstElement {
519
624
  return this;
520
625
  }
521
626
 
522
- /** 解析、重构、生成部分Token的`name`属性 */
627
+ /**
628
+ * 解析、重构、生成部分Token的`name`属性
629
+ * @param {number} n 最大解析层级
630
+ * @param {boolean} include 是否嵌入
631
+ */
523
632
  parse(n = MAX_STAGE, include = false) {
524
633
  if (typeof n !== 'number') {
525
634
  this.typeError('parse', 'Number');
526
- } else if (n < MAX_STAGE && !Parser.debugging && externalUse('parse')) {
635
+ } else if (n < MAX_STAGE && !Parser.debugging && Parser.warning && externalUse('parse')) {
527
636
  Parser.warn('指定解析层级的方法仅供熟练用户使用!');
528
637
  }
529
638
  this.#include = Boolean(include);
530
639
  while (this.#stage < n) {
531
- this.parseOnce(this.#stage, include);
640
+ this.#parseOnce(this.#stage, include);
532
641
  }
533
642
  return n ? this.build().afterBuild() : this;
534
643
  }
535
644
 
536
- #parseCommentAndExt(includeOnly = false) {
645
+ /**
646
+ * 解析HTML注释和扩展标签
647
+ * @param {boolean} includeOnly 是否嵌入
648
+ */
649
+ #parseCommentAndExt(includeOnly) {
537
650
  const parseCommentAndExt = require('../parser/commentAndExt');
538
- this.setText(parseCommentAndExt(this.firstChild, this.#config, this.#accum, includeOnly));
651
+ this.setText(parseCommentAndExt(String(this), this.#config, this.#accum, includeOnly));
539
652
  }
540
653
 
654
+ /** 解析花括号 */
541
655
  #parseBrackets() {
542
656
  const parseBrackets = require('../parser/brackets');
543
- this.setText(parseBrackets(this.firstChild, this.#config, this.#accum));
657
+ this.setText(parseBrackets(String(this), this.#config, this.#accum));
544
658
  }
545
659
 
660
+ /** 解析HTML标签 */
546
661
  #parseHtml() {
547
662
  const parseHtml = require('../parser/html');
548
- this.setText(parseHtml(this.firstChild, this.#config, this.#accum));
663
+ this.setText(parseHtml(String(this), this.#config, this.#accum));
549
664
  }
550
665
 
666
+ /** 解析表格 */
551
667
  #parseTable() {
552
668
  const parseTable = require('../parser/table'),
553
669
  TableToken = require('./table');
@@ -555,8 +671,8 @@ class Token extends AstElement {
555
671
  for (const table of this.#accum) {
556
672
  if (table instanceof TableToken && table.type !== 'td') {
557
673
  table.normalize();
558
- const [, child] = table.childNodes;
559
- if (typeof child === 'string' && child.includes('\x00')) {
674
+ const {childNodes: [, child]} = table;
675
+ if (typeof child === 'string' && child.includes('\0')) {
560
676
  table.removeAt(1);
561
677
  const inner = new Token(child, this.#config, true, this.#accum);
562
678
  table.insertAt(inner, 1);
@@ -566,49 +682,54 @@ class Token extends AstElement {
566
682
  }
567
683
  }
568
684
 
685
+ /** 解析\<hr\>和状态开关 */
569
686
  #parseHrAndDoubleUndescore() {
570
687
  const parseHrAndDoubleUnderscore = require('../parser/hrAndDoubleUnderscore');
571
- this.setText(parseHrAndDoubleUnderscore(this.firstChild, this.#config, this.#accum));
688
+ this.setText(parseHrAndDoubleUnderscore(this, this.#config, this.#accum));
572
689
  }
573
690
 
691
+ /** 解析内部链接 */
574
692
  #parseLinks() {
575
693
  const parseLinks = require('../parser/links');
576
- this.setText(parseLinks(this.firstChild, this.#config, this.#accum));
694
+ this.setText(parseLinks(String(this), this.#config, this.#accum));
577
695
  }
578
696
 
579
- /** @this {Token & {firstChild: string}} */
697
+ /** 解析单引号 */
580
698
  #parseQuotes() {
581
- const parseQuotes = require('../parser/quotes'),
582
- lines = this.firstChild.split('\n');
699
+ const parseQuotes = require('../parser/quotes');
700
+ const lines = String(this).split('\n');
583
701
  for (let i = 0; i < lines.length; i++) {
584
702
  lines[i] = parseQuotes(lines[i], this.#config, this.#accum);
585
703
  }
586
704
  this.setText(lines.join('\n'));
587
705
  }
588
706
 
707
+ /** 解析外部链接 */
589
708
  #parseExternalLinks() {
590
709
  const parseExternalLinks = require('../parser/externalLinks');
591
- this.setText(parseExternalLinks(this.firstChild, this.#config, this.#accum));
710
+ this.setText(parseExternalLinks(String(this), this.#config, this.#accum));
592
711
  }
593
712
 
713
+ /** 解析自由外链 */
594
714
  #parseMagicLinks() {
595
715
  const parseMagicLinks = require('../parser/magicLinks');
596
- this.setText(parseMagicLinks(this.firstChild, this.#config, this.#accum));
716
+ this.setText(parseMagicLinks(String(this), this.#config, this.#accum));
597
717
  }
598
718
 
599
- /** @this {Token & {firstChild: string}} */
719
+ /** 解析列表 */
600
720
  #parseList() {
601
- const parseList = require('../parser/list'),
602
- lines = this.firstChild.split('\n');
721
+ const parseList = require('../parser/list');
722
+ const lines = String(this).split('\n');
603
723
  for (let i = this.type === 'root' ? 0 : 1; i < lines.length; i++) {
604
724
  lines[i] = parseList(lines[i], this.#config, this.#accum);
605
725
  }
606
726
  this.setText(lines.join('\n'));
607
727
  }
608
728
 
729
+ /** 解析语言变体转换 */
609
730
  #parseConverter() {
610
731
  const parseConverter = require('../parser/converter');
611
- this.setText(parseConverter(this.firstChild, this.#config, this.#accum));
732
+ this.setText(parseConverter(String(this), this.#config, this.#accum));
612
733
  }
613
734
  }
614
735