wikiparser-node 0.5.0 → 0.6.1

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 (63) hide show
  1. package/config/default.json +129 -66
  2. package/config/zhwiki.json +4 -4
  3. package/index.js +74 -65
  4. package/lib/element.js +125 -152
  5. package/lib/node.js +251 -223
  6. package/lib/ranges.js +2 -2
  7. package/lib/text.js +64 -64
  8. package/lib/title.js +8 -7
  9. package/mixin/hidden.js +2 -0
  10. package/mixin/sol.js +1 -2
  11. package/package.json +4 -3
  12. package/parser/brackets.js +8 -2
  13. package/parser/externalLinks.js +1 -1
  14. package/parser/hrAndDoubleUnderscore.js +4 -4
  15. package/parser/links.js +7 -7
  16. package/parser/table.js +12 -10
  17. package/src/arg.js +53 -48
  18. package/src/atom/index.js +7 -5
  19. package/src/attribute.js +91 -80
  20. package/src/charinsert.js +91 -0
  21. package/src/converter.js +22 -11
  22. package/src/converterFlags.js +72 -62
  23. package/src/converterRule.js +49 -49
  24. package/src/extLink.js +30 -28
  25. package/src/gallery.js +56 -32
  26. package/src/hasNowiki/index.js +42 -0
  27. package/src/hasNowiki/pre.js +40 -0
  28. package/src/heading.js +15 -11
  29. package/src/html.js +38 -38
  30. package/src/imageParameter.js +64 -48
  31. package/src/imagemap.js +205 -0
  32. package/src/imagemapLink.js +43 -0
  33. package/src/index.js +222 -124
  34. package/src/link/category.js +4 -8
  35. package/src/link/file.js +95 -59
  36. package/src/link/galleryImage.js +74 -10
  37. package/src/link/index.js +61 -39
  38. package/src/magicLink.js +21 -22
  39. package/src/nested/choose.js +24 -0
  40. package/src/nested/combobox.js +23 -0
  41. package/src/nested/index.js +88 -0
  42. package/src/nested/references.js +23 -0
  43. package/src/nowiki/comment.js +17 -17
  44. package/src/nowiki/dd.js +2 -2
  45. package/src/nowiki/doubleUnderscore.js +14 -14
  46. package/src/nowiki/index.js +12 -0
  47. package/src/onlyinclude.js +10 -8
  48. package/src/paramTag/index.js +83 -0
  49. package/src/paramTag/inputbox.js +42 -0
  50. package/src/parameter.js +32 -18
  51. package/src/syntax.js +9 -1
  52. package/src/table/index.js +33 -32
  53. package/src/table/td.js +51 -57
  54. package/src/table/tr.js +6 -6
  55. package/src/tagPair/ext.js +58 -40
  56. package/src/tagPair/include.js +1 -1
  57. package/src/tagPair/index.js +21 -20
  58. package/src/transclude.js +158 -143
  59. package/tool/index.js +720 -439
  60. package/util/base.js +17 -0
  61. package/util/debug.js +1 -1
  62. package/util/diff.js +1 -1
  63. package/util/string.js +20 -20
@@ -0,0 +1,205 @@
1
+ 'use strict';
2
+
3
+ const {generateForSelf, generateForChild} = require('../util/lint'),
4
+ Parser = require('..'),
5
+ AstText = require('../lib/text'),
6
+ Token = require('.'),
7
+ NoincludeToken = require('./nowiki/noinclude'),
8
+ GalleryImageToken = require('./link/galleryImage'),
9
+ ImagemapLinkToken = require('./imagemapLink');
10
+
11
+ /**
12
+ * `<imagemap>`
13
+ * @classdesc `{childNodes: ...NoincludeToken, GalleryImageToken, ...(NoincludeToken|ImagemapLinkToken|AstText)}`
14
+ */
15
+ class ImagemapToken extends Token {
16
+ type = 'ext-inner';
17
+ name = 'imagemap';
18
+
19
+ /**
20
+ * 图片
21
+ * @returns {GalleryImageToken}
22
+ */
23
+ get image() {
24
+ return this.childNodes.find(({type}) => type === 'imagemap-image');
25
+ }
26
+
27
+ /**
28
+ * 链接
29
+ * @returns {ImagemapLinkToken[]}
30
+ */
31
+ get links() {
32
+ return this.childNodes.filter(({type}) => type === 'imagemap-link');
33
+ }
34
+
35
+ /**
36
+ * @param {string} inner 标签内部wikitext
37
+ * @param {accum} accum
38
+ * @throws `SyntaxError` 没有合法图片
39
+ */
40
+ constructor(inner, config = Parser.getConfig(), accum = []) {
41
+ super(undefined, config, true, accum, {
42
+ GalleryImageToken: ':', ImagemapLinkToken: ':', NoincludeToken: ':', AstText: ':',
43
+ });
44
+ if (!inner) {
45
+ return;
46
+ }
47
+ const lines = inner.split('\n'),
48
+ fallback = /** @param {string} line 一行文本 */ line => {
49
+ super.insertAt(new NoincludeToken(line, config, accum));
50
+ };
51
+ let first = true,
52
+ error = false;
53
+ for (const line of lines) {
54
+ const trimmed = line.trim();
55
+ if (error || !trimmed || trimmed[0] === '#') {
56
+ //
57
+ } else if (first) {
58
+ const [file, ...options] = line.split('|');
59
+ let title;
60
+ try {
61
+ title = this.normalizeTitle(decodeURIComponent(file), 0, true);
62
+ } catch {
63
+ title = this.normalizeTitle(file, 0, true);
64
+ }
65
+ if (title.valid && !title.interwiki && title.ns === 6) {
66
+ const token = new GalleryImageToken(
67
+ file, options.length > 0 ? options.join('|') : undefined, title, config, accum,
68
+ );
69
+ token.type = 'imagemap-image';
70
+ super.insertAt(token);
71
+ first = false;
72
+ continue;
73
+ } else {
74
+ Parser.error('<imagemap>标签内必须先包含一张合法图片!', line);
75
+ error = true;
76
+ }
77
+ } else if (line.trim().split(/[\t ]/u)[0] === 'desc') {
78
+ super.insertAt(new AstText(line));
79
+ continue;
80
+ } else if (line.includes('[')) {
81
+ const i = line.indexOf('['),
82
+ substr = line.slice(i),
83
+ mtIn = /^\[{2}([^|]+)(?:\|([^\]]+))?\]{2}[\w\s]*$/u.exec(substr);
84
+ if (mtIn) {
85
+ const title = this.normalizeTitle(mtIn[1], 0, true);
86
+ if (title.valid) {
87
+ super.insertAt(new ImagemapLinkToken(
88
+ line.slice(0, i),
89
+ [...mtIn.slice(1), title],
90
+ substr.slice(substr.indexOf(']]') + 2),
91
+ config,
92
+ accum,
93
+ ));
94
+ continue;
95
+ }
96
+ } else {
97
+ const protocols = config.protocol.split('|');
98
+ if (protocols.includes(substr.slice(1, substr.indexOf(':') + 1))
99
+ || protocols.includes(substr.slice(1, substr.indexOf('//') + 2))
100
+ ) {
101
+ const mtEx = /^\[([^\]\s]+)(?:(\s+)(\S[^\]]*)?)?\][\w\s]*$/u.exec(substr);
102
+ if (mtEx) {
103
+ super.insertAt(new ImagemapLinkToken(
104
+ line.slice(0, i),
105
+ mtEx.slice(1),
106
+ substr.slice(substr.indexOf(']') + 1),
107
+ config,
108
+ accum,
109
+ ));
110
+ continue;
111
+ }
112
+ }
113
+ }
114
+ }
115
+ fallback(line);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * @override
121
+ * @param {string} selector
122
+ */
123
+ toString(selector) {
124
+ return super.toString(selector, '\n');
125
+ }
126
+
127
+ /** @override */
128
+ getGaps() {
129
+ return 1;
130
+ }
131
+
132
+ /** @override */
133
+ print() {
134
+ return super.print({sep: '\n'});
135
+ }
136
+
137
+ /**
138
+ * @override
139
+ * @param {number} start 起始位置
140
+ */
141
+ lint(start = 0) {
142
+ const errors = super.lint(start),
143
+ rect = this.getRootNode().posFromIndex(start);
144
+ if (this.image) {
145
+ errors.push(
146
+ ...this.childNodes.filter(child => {
147
+ const str = String(child).trim();
148
+ return child.type === 'noinclude' && str && str[0] !== '#';
149
+ }).map(child => generateForChild(child, rect, '无效的<imagemap>链接')),
150
+ );
151
+ } else {
152
+ errors.push(generateForSelf(this, rect, '缺少图片的<imagemap>'));
153
+ }
154
+ return errors;
155
+ }
156
+
157
+ /**
158
+ * @override
159
+ * @template {string|Token} T
160
+ * @param {T} token 待插入的节点
161
+ * @param {number} i 插入位置
162
+ * @throws `Error` 当前缺少合法图片
163
+ * @throws `RangeError` 已有一张合法图片
164
+ */
165
+ insertAt(token, i = 0) {
166
+ const {image} = this;
167
+ if (!image && (token.type === 'imagemap-link' || token.type === 'text')) {
168
+ throw new Error('当前缺少一张合法图片!');
169
+ } else if (image && token.type === 'imagemap-image') {
170
+ throw new RangeError('已有一张合法图片!');
171
+ }
172
+ return super.insertAt(token, i);
173
+ }
174
+
175
+ /**
176
+ * @override
177
+ * @param {number} i 移除位置
178
+ * @throws `Error` 禁止移除图片
179
+ */
180
+ removeAt(i) {
181
+ const child = this.childNodes[i];
182
+ if (child.type === 'imagemap-image') {
183
+ throw new Error('禁止移除<imagemap>内的图片!');
184
+ }
185
+ return super.removeAt(i);
186
+ }
187
+
188
+ /** @override */
189
+ cloneNode() {
190
+ const cloned = this.cloneChildNodes();
191
+ return Parser.run(() => {
192
+ const token = new ImagemapToken(undefined, this.getAttribute('config'));
193
+ token.append(...cloned);
194
+ return token;
195
+ });
196
+ }
197
+
198
+ /** @override */
199
+ text() {
200
+ return super.text('\n').replaceAll(/\n{2,}/gu, '\n');
201
+ }
202
+ }
203
+
204
+ Parser.classes.ImagemapToken = __filename;
205
+ module.exports = ImagemapToken;
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ const fixedToken = require('../mixin/fixedToken'),
4
+ Parser = require('..'),
5
+ Token = require('.'),
6
+ LinkToken = require('./link'),
7
+ ExtLinkToken = require('./extLink');
8
+
9
+ /**
10
+ * `<imagemap>`内的链接
11
+ * @classdesc `{childNodes: [AstText, LinkToken|ExtLinkToken, NoincludeToken]}`
12
+ */
13
+ class ImagemapLinkToken extends fixedToken(Token) {
14
+ type = 'imagemap-link';
15
+
16
+ /**
17
+ * 内外链接
18
+ * @this {{childNodes: (LinkToken|ExtLinkToken)[]}}
19
+ */
20
+ get link() {
21
+ return this.childNodes[1].link;
22
+ }
23
+
24
+ /**
25
+ * @param {string} pre 链接前的文本
26
+ * @param {[string, string, string|Title]} linkStuff 内外链接
27
+ * @param {string} post 链接后的文本
28
+ * @param {accum} accum
29
+ */
30
+ constructor(pre, linkStuff, post, config, accum) {
31
+ const Title = require('../lib/title'),
32
+ AstText = require('../lib/text'),
33
+ NoincludeToken = require('./nowiki/noinclude');
34
+ const SomeLinkToken = linkStuff[2] instanceof Title ? LinkToken : ExtLinkToken;
35
+ super(undefined, config, true, accum);
36
+ this.append(
37
+ new AstText(pre), new SomeLinkToken(...linkStuff, config, accum), new NoincludeToken(post, config, accum),
38
+ );
39
+ }
40
+ }
41
+
42
+ Parser.classes.ImagemapLinkToken = __filename;
43
+ module.exports = ImagemapLinkToken;
package/src/index.js CHANGED
@@ -28,6 +28,7 @@
28
28
  * -: `{{!-}}`专用
29
29
  * +: `{{!!}}`专用
30
30
  * ~: `{{=}}`专用
31
+ * m: `{{fullurl:}}`、`{{canonicalurl:}}`或`{{filepath:}}`
31
32
  * t: ArgToken或TranscludeToken
32
33
  * h: HeadingToken
33
34
  * x: HtmlToken
@@ -64,14 +65,6 @@ class Token extends AstElement {
64
65
  #protectedChildren = new Ranges();
65
66
  /** @type {boolean} */ #include;
66
67
 
67
- /**
68
- * 保护部分子节点不被移除
69
- * @param {...string|number|Range} args 子节点范围
70
- */
71
- #protectChildren = (...args) => {
72
- this.#protectedChildren.push(...new Ranges(args));
73
- };
74
-
75
68
  /**
76
69
  * 将维基语法替换为占位符
77
70
  * @param {number} n 解析阶段
@@ -144,9 +137,36 @@ class Token extends AstElement {
144
137
  throw new Error(`解析错误!未正确标记的 Token:${s}`);
145
138
  });
146
139
 
140
+ /**
141
+ * 将占位符替换为子Token
142
+ * @complexity `n`
143
+ */
144
+ #build = () => {
145
+ this.#stage = MAX_STAGE;
146
+ const {length, firstChild} = this,
147
+ str = String(firstChild);
148
+ if (length === 1 && firstChild.type === 'text' && str.includes('\0')) {
149
+ this.replaceChildren(...this.#buildFromStr(str));
150
+ this.normalize();
151
+ if (this.type === 'root') {
152
+ for (const token of this.#accum) {
153
+ token.getAttribute('build')();
154
+ }
155
+ }
156
+ }
157
+ };
158
+
159
+ /**
160
+ * 保护部分子节点不被移除
161
+ * @param {...string|number|Range} args 子节点范围
162
+ */
163
+ #protectChildren = (...args) => {
164
+ this.#protectedChildren.push(...new Ranges(args));
165
+ };
166
+
147
167
  /** 所有图片,包括图库 */
148
168
  get images() {
149
- return this.querySelectorAll('file, gallery-image');
169
+ return this.querySelectorAll('file, gallery-image, imagemap-image');
150
170
  }
151
171
 
152
172
  /** 所有内链、外链和自由外链 */
@@ -167,7 +187,7 @@ class Token extends AstElement {
167
187
  constructor(wikitext, config = Parser.getConfig(), halfParsed = false, accum = [], acceptable = null) {
168
188
  super();
169
189
  if (typeof wikitext === 'string') {
170
- this.appendChild(halfParsed ? wikitext : wikitext.replaceAll(/[\0\x7F]/gu, ''));
190
+ this.insertAt(halfParsed ? wikitext : wikitext.replaceAll(/[\0\x7F]/gu, ''));
171
191
  }
172
192
  this.#config = config;
173
193
  this.#accum = accum;
@@ -175,34 +195,6 @@ class Token extends AstElement {
175
195
  accum.push(this);
176
196
  }
177
197
 
178
- /**
179
- * 深拷贝所有子节点
180
- * @complexity `n`
181
- * @returns {(AstText|Token)[]}
182
- */
183
- cloneChildNodes() {
184
- return this.childNodes.map(child => child.cloneNode());
185
- }
186
-
187
- /**
188
- * 深拷贝节点
189
- * @complexity `n`
190
- * @throws `Error` 未定义复制方法
191
- */
192
- cloneNode() {
193
- if (!this.isPlain()) {
194
- throw new Error(`未定义 ${this.constructor.name} 的复制方法!`);
195
- }
196
- const cloned = this.cloneChildNodes();
197
- return Parser.run(() => {
198
- const token = new Token(undefined, this.#config, false, [], this.#acceptable);
199
- token.type = this.type;
200
- token.append(...cloned);
201
- token.getAttribute('protectChildren')(...this.#protectedChildren);
202
- return token;
203
- });
204
- }
205
-
206
198
  /**
207
199
  * @override
208
200
  * @template {string} T
@@ -211,22 +203,24 @@ class Token extends AstElement {
211
203
  */
212
204
  getAttribute(key) {
213
205
  switch (key) {
214
- case 'stage':
215
- return this.#stage;
216
206
  case 'config':
217
207
  return structuredClone(this.#config);
218
208
  case 'accum':
219
209
  return this.#accum;
210
+ case 'parseOnce':
211
+ return this.#parseOnce;
212
+ case 'buildFromStr':
213
+ return this.#buildFromStr;
214
+ case 'build':
215
+ return this.#build;
216
+ case 'stage':
217
+ return this.#stage;
220
218
  case 'acceptable':
221
219
  return this.#acceptable ? {...this.#acceptable} : null;
222
220
  case 'protectChildren':
223
221
  return this.#protectChildren;
224
222
  case 'protectedChildren':
225
223
  return new Ranges(this.#protectedChildren);
226
- case 'parseOnce':
227
- return this.#parseOnce;
228
- case 'buildFromStr':
229
- return this.#buildFromStr;
230
224
  case 'include': {
231
225
  if (this.#include !== undefined) {
232
226
  return this.#include;
@@ -252,16 +246,8 @@ class Token extends AstElement {
252
246
  * @template {string} T
253
247
  * @param {T} key 属性键
254
248
  * @param {TokenAttribute<T>} value 属性值
255
- * @throws `RangeError` 禁止手动指定私有属性
256
249
  */
257
250
  setAttribute(key, value) {
258
- if (key === 'include' || !Parser.running && (key === 'config' || key === 'accum')) {
259
- throw new RangeError(`禁止手动指定私有的 #${key} 属性!`);
260
- } else if (!Parser.debugging && (key === 'stage' || key === 'acceptable' || key === 'protectedChildren')
261
- && externalUse('setAttribute')
262
- ) {
263
- throw new RangeError(`使用 ${this.constructor.name}.setAttribute 方法设置私有属性 #${key} 仅用于代码调试!`);
264
- }
265
251
  switch (key) {
266
252
  case 'stage':
267
253
  if (this.#stage === 0 && this.type === 'root') {
@@ -269,15 +255,6 @@ class Token extends AstElement {
269
255
  }
270
256
  this.#stage = value;
271
257
  return this;
272
- case 'config':
273
- this.#config = value;
274
- return this;
275
- case 'accum':
276
- this.#accum = value;
277
- return this;
278
- case 'protectedChildren':
279
- this.#protectedChildren = value;
280
- return this;
281
258
  case 'acceptable': {
282
259
  const /** @type {acceptable} */ acceptable = {};
283
260
  if (value) {
@@ -308,36 +285,6 @@ class Token extends AstElement {
308
285
  return this.constructor === Token;
309
286
  }
310
287
 
311
- /**
312
- * @override
313
- * @param {number} i 移除位置
314
- * @returns {Token}
315
- * @complexity `n`
316
- * @throws `Error` 不可移除的子节点
317
- */
318
- removeAt(i) {
319
- if (typeof i !== 'number') {
320
- this.typeError('removeAt', 'Number');
321
- }
322
- const iPos = i < 0 ? i + this.childNodes.length : i;
323
- if (!Parser.running) {
324
- const protectedIndices = this.#protectedChildren.applyTo(this.childNodes);
325
- if (protectedIndices.includes(iPos)) {
326
- throw new Error(`${this.constructor.name} 的第 ${i} 个子节点不可移除!`);
327
- } else if (this.#acceptable) {
328
- const acceptableIndices = Object.fromEntries(
329
- Object.entries(this.#acceptable)
330
- .map(([str, ranges]) => [str, ranges.applyTo(this.childNodes.length - 1)]),
331
- ),
332
- nodesAfter = i === -1 ? [] : this.childNodes.slice(i + 1);
333
- if (nodesAfter.some(({constructor: {name}}, j) => !acceptableIndices[name].includes(i + j))) {
334
- throw new Error(`移除 ${this.constructor.name} 的第 ${i} 个子节点会破坏规定的顺序!`);
335
- }
336
- }
337
- }
338
- return super.removeAt(i);
339
- }
340
-
341
288
  /**
342
289
  * @override
343
290
  * @template {string|Token} T
@@ -372,6 +319,45 @@ class Token extends AstElement {
372
319
  return token;
373
320
  }
374
321
 
322
+ /**
323
+ * 规范化页面标题
324
+ * @param {string} title 标题(含或不含命名空间前缀)
325
+ * @param {number} defaultNs 命名空间
326
+ */
327
+ normalizeTitle(title, defaultNs = 0, halfParsed = false) {
328
+ return Parser.normalizeTitle(title, defaultNs, this.#include, this.#config, halfParsed);
329
+ }
330
+
331
+ /**
332
+ * @override
333
+ * @param {number} i 移除位置
334
+ * @returns {Token}
335
+ * @complexity `n`
336
+ * @throws `Error` 不可移除的子节点
337
+ */
338
+ removeAt(i) {
339
+ if (!Number.isInteger(i)) {
340
+ this.typeError('removeAt', 'Number');
341
+ }
342
+ const iPos = i < 0 ? i + this.childNodes.length : i;
343
+ if (!Parser.running) {
344
+ const protectedIndices = this.#protectedChildren.applyTo(this.childNodes);
345
+ if (protectedIndices.includes(iPos)) {
346
+ throw new Error(`${this.constructor.name} 的第 ${i} 个子节点不可移除!`);
347
+ } else if (this.#acceptable) {
348
+ const acceptableIndices = Object.fromEntries(
349
+ Object.entries(this.#acceptable)
350
+ .map(([str, ranges]) => [str, ranges.applyTo(this.childNodes.length - 1)]),
351
+ ),
352
+ nodesAfter = i === -1 ? [] : this.childNodes.slice(i + 1);
353
+ if (nodesAfter.some(({constructor: {name}}, j) => !acceptableIndices[name].includes(i + j))) {
354
+ throw new Error(`移除 ${this.constructor.name} 的第 ${i} 个子节点会破坏规定的顺序!`);
355
+ }
356
+ }
357
+ }
358
+ return super.removeAt(i);
359
+ }
360
+
375
361
  /**
376
362
  * 替换为同类节点
377
363
  * @param {Token} token 待替换的节点
@@ -444,6 +430,120 @@ class Token extends AstElement {
444
430
  throw new RangeError(`非法的标签名!${tagName}`);
445
431
  }
446
432
 
433
+ /**
434
+ * 创建纯文本节点
435
+ * @param {string} data 文本内容
436
+ */
437
+ createTextNode(data = '') {
438
+ return typeof data === 'string' ? new AstText(data) : this.typeError('createComment', 'String');
439
+ }
440
+
441
+ /**
442
+ * 找到给定位置所在的节点
443
+ * @param {number} index 位置
444
+ */
445
+ caretPositionFromIndex(index) {
446
+ if (index === undefined) {
447
+ return undefined;
448
+ } else if (!Number.isInteger(index)) {
449
+ this.typeError('caretPositionFromIndex', 'Number');
450
+ }
451
+ const {length} = String(this);
452
+ if (index > length || index < -length) {
453
+ return undefined;
454
+ } else if (index < 0) {
455
+ index += length;
456
+ }
457
+ let child = this, // eslint-disable-line unicorn/no-this-assignment
458
+ acc = 0,
459
+ start = 0;
460
+ while (child.type !== 'text') {
461
+ const {childNodes} = child;
462
+ acc += child.getPadding();
463
+ for (let i = 0; acc <= index && i < childNodes.length; i++) {
464
+ const cur = childNodes[i],
465
+ {length: l} = String(cur);
466
+ acc += l;
467
+ if (acc >= index) {
468
+ child = cur;
469
+ acc -= l;
470
+ start = acc;
471
+ break;
472
+ }
473
+ acc += child.getGaps(i);
474
+ }
475
+ if (child.childNodes === childNodes) {
476
+ return {offsetNode: child, offset: index - start};
477
+ }
478
+ }
479
+ return {offsetNode: child, offset: index - start};
480
+ }
481
+
482
+ /**
483
+ * 找到给定位置所在的节点
484
+ * @param {number} x 列数
485
+ * @param {number} y 行数
486
+ */
487
+ caretPositionFromPoint(x, y) {
488
+ return this.caretPositionFromIndex(this.indexFromPos(y, x));
489
+ }
490
+
491
+ /**
492
+ * 找到给定位置所在的最外层节点
493
+ * @param {number} index 位置
494
+ * @throws `Error` 不是根节点
495
+ */
496
+ elementFromIndex(index) {
497
+ if (index === undefined) {
498
+ return undefined;
499
+ } else if (!Number.isInteger(index)) {
500
+ this.typeError('elementFromIndex', 'Number');
501
+ } else if (this.type !== 'root') {
502
+ throw new Error('elementFromIndex方法只可用于根节点!');
503
+ }
504
+ const {length} = String(this);
505
+ if (index > length || index < -length) {
506
+ return undefined;
507
+ } else if (index < 0) {
508
+ index += length;
509
+ }
510
+ const {childNodes} = this;
511
+ let acc = 0,
512
+ i = 0;
513
+ for (; acc < index && i < childNodes.length; i++) {
514
+ const {length: l} = String(childNodes[i]);
515
+ acc += l;
516
+ }
517
+ return childNodes[i && i - 1];
518
+ }
519
+
520
+ /**
521
+ * 找到给定位置所在的最外层节点
522
+ * @param {number} x 列数
523
+ * @param {number} y 行数
524
+ */
525
+ elementFromPoint(x, y) {
526
+ return this.elementFromIndex(this.indexFromPos(y, x));
527
+ }
528
+
529
+ /**
530
+ * 找到给定位置所在的所有节点
531
+ * @param {number} index 位置
532
+ */
533
+ elementsFromIndex(index) {
534
+ const offsetNode = this.caretPositionFromIndex(index)?.offsetNode;
535
+ return offsetNode && [...offsetNode.getAncestors().reverse(), offsetNode];
536
+ }
537
+
538
+ /**
539
+ * 找到给定位置所在的所有节点
540
+ * @param {number} x 列数
541
+ * @param {number} y 行数
542
+ */
543
+ elementsFromPoint(x, y) {
544
+ return this.elementsFromIndex(this.indexFromPos(y, x));
545
+ }
546
+
447
547
  /**
448
548
  * 判断标题是否是跨维基链接
449
549
  * @param {string} title 标题
@@ -453,12 +553,31 @@ class Token extends AstElement {
453
553
  }
454
554
 
455
555
  /**
456
- * 规范化页面标题
457
- * @param {string} title 标题(含或不含命名空间前缀)
458
- * @param {number} defaultNs 命名空间
556
+ * 深拷贝所有子节点
557
+ * @complexity `n`
558
+ * @returns {(AstText|Token)[]}
459
559
  */
460
- normalizeTitle(title, defaultNs = 0, halfParsed = false) {
461
- return Parser.normalizeTitle(title, defaultNs, this.#include, this.#config, halfParsed);
560
+ cloneChildNodes() {
561
+ return this.childNodes.map(child => child.cloneNode());
562
+ }
563
+
564
+ /**
565
+ * 深拷贝节点
566
+ * @complexity `n`
567
+ * @throws `Error` 未定义复制方法
568
+ */
569
+ cloneNode() {
570
+ if (this.constructor !== Token) {
571
+ throw new Error(`未定义 ${this.constructor.name} 的复制方法!`);
572
+ }
573
+ const cloned = this.cloneChildNodes();
574
+ return Parser.run(() => {
575
+ const token = new Token(undefined, this.#config, false, [], this.#acceptable);
576
+ token.type = this.type;
577
+ token.append(...cloned);
578
+ token.getAttribute('protectChildren')(...this.#protectedChildren);
579
+ return token;
580
+ });
462
581
  }
463
582
 
464
583
  /**
@@ -499,7 +618,7 @@ class Token extends AstElement {
499
618
  * @complexity `n`
500
619
  */
501
620
  section(n) {
502
- return typeof n === 'number' ? this.sections()?.[n] : this.typeError('section', 'Number');
621
+ return Number.isInteger(n) ? this.sections()?.[n] : this.typeError('section', 'Number');
503
622
  }
504
623
 
505
624
  /**
@@ -590,29 +709,6 @@ class Token extends AstElement {
590
709
  this.normalize();
591
710
  }
592
711
 
593
- /**
594
- * 将占位符替换为子Token
595
- * @complexity `n`
596
- */
597
- build() {
598
- if (!Parser.debugging && externalUse('build')) {
599
- this.debugOnly('build');
600
- }
601
- this.#stage = MAX_STAGE;
602
- const {childNodes: {length}, firstChild} = this,
603
- str = String(firstChild);
604
- if (length === 1 && firstChild.type === 'text' && str.includes('\0')) {
605
- this.replaceChildren(...this.#buildFromStr(str));
606
- this.normalize();
607
- if (this.type === 'root') {
608
- for (const token of this.#accum) {
609
- token.build();
610
- }
611
- }
612
- }
613
- return this;
614
- }
615
-
616
712
  /** 生成部分Token的`name`属性 */
617
713
  afterBuild() {
618
714
  if (!Parser.debugging && externalUse('afterBuild')) {
@@ -631,16 +727,18 @@ class Token extends AstElement {
631
727
  * @param {boolean} include 是否嵌入
632
728
  */
633
729
  parse(n = MAX_STAGE, include = false) {
634
- if (typeof n !== 'number') {
730
+ if (!Number.isInteger(n)) {
635
731
  this.typeError('parse', 'Number');
636
- } else if (n < MAX_STAGE && !Parser.debugging && Parser.warning && externalUse('parse')) {
637
- Parser.warn('指定解析层级的方法仅供熟练用户使用!');
638
732
  }
639
733
  this.#include = Boolean(include);
640
734
  while (this.#stage < n) {
641
735
  this.#parseOnce(this.#stage, include);
642
736
  }
643
- return n ? this.build().afterBuild() : this;
737
+ if (n) {
738
+ this.#build();
739
+ this.afterBuild();
740
+ }
741
+ return this;
644
742
  }
645
743
 
646
744
  /**