wikiparser-node 0.4.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 (87) hide show
  1. package/config/default.json +129 -66
  2. package/config/zhwiki.json +4 -4
  3. package/index.js +97 -65
  4. package/lib/element.js +159 -302
  5. package/lib/node.js +384 -198
  6. package/lib/ranges.js +3 -4
  7. package/lib/text.js +65 -36
  8. package/lib/title.js +9 -8
  9. package/mixin/fixedToken.js +4 -4
  10. package/mixin/hidden.js +2 -0
  11. package/mixin/sol.js +16 -7
  12. package/package.json +14 -3
  13. package/parser/brackets.js +8 -2
  14. package/parser/commentAndExt.js +1 -1
  15. package/parser/converter.js +1 -1
  16. package/parser/externalLinks.js +2 -2
  17. package/parser/hrAndDoubleUnderscore.js +8 -7
  18. package/parser/links.js +8 -9
  19. package/parser/magicLinks.js +1 -1
  20. package/parser/selector.js +5 -5
  21. package/parser/table.js +18 -16
  22. package/src/arg.js +71 -42
  23. package/src/atom/index.js +7 -5
  24. package/src/attribute.js +102 -64
  25. package/src/charinsert.js +91 -0
  26. package/src/converter.js +34 -15
  27. package/src/converterFlags.js +87 -40
  28. package/src/converterRule.js +59 -53
  29. package/src/extLink.js +45 -37
  30. package/src/gallery.js +71 -16
  31. package/src/hasNowiki/index.js +42 -0
  32. package/src/hasNowiki/pre.js +40 -0
  33. package/src/heading.js +41 -18
  34. package/src/html.js +76 -48
  35. package/src/imageParameter.js +73 -51
  36. package/src/imagemap.js +205 -0
  37. package/src/imagemapLink.js +43 -0
  38. package/src/index.js +243 -138
  39. package/src/link/category.js +10 -14
  40. package/src/link/file.js +112 -56
  41. package/src/link/galleryImage.js +74 -10
  42. package/src/link/index.js +86 -61
  43. package/src/magicLink.js +48 -21
  44. package/src/nested/choose.js +24 -0
  45. package/src/nested/combobox.js +23 -0
  46. package/src/nested/index.js +88 -0
  47. package/src/nested/references.js +23 -0
  48. package/src/nowiki/comment.js +18 -4
  49. package/src/nowiki/dd.js +2 -2
  50. package/src/nowiki/doubleUnderscore.js +16 -11
  51. package/src/nowiki/index.js +12 -0
  52. package/src/nowiki/quote.js +28 -1
  53. package/src/onlyinclude.js +15 -8
  54. package/src/paramTag/index.js +83 -0
  55. package/src/paramTag/inputbox.js +42 -0
  56. package/src/parameter.js +73 -46
  57. package/src/syntax.js +9 -1
  58. package/src/table/index.js +58 -44
  59. package/src/table/td.js +63 -63
  60. package/src/table/tr.js +52 -35
  61. package/src/tagPair/ext.js +60 -43
  62. package/src/tagPair/include.js +11 -1
  63. package/src/tagPair/index.js +29 -20
  64. package/src/transclude.js +214 -166
  65. package/tool/index.js +720 -439
  66. package/util/base.js +17 -0
  67. package/util/debug.js +1 -1
  68. package/{test/util.js → util/diff.js} +15 -19
  69. package/util/lint.js +40 -0
  70. package/util/string.js +37 -20
  71. package/.eslintrc.json +0 -714
  72. package/errors/README +0 -1
  73. package/jsconfig.json +0 -7
  74. package/printed/README +0 -1
  75. package/printed/example.json +0 -120
  76. package/test/api.js +0 -83
  77. package/test/real.js +0 -133
  78. package/test/test.js +0 -28
  79. package/typings/api.d.ts +0 -13
  80. package/typings/array.d.ts +0 -28
  81. package/typings/event.d.ts +0 -24
  82. package/typings/index.d.ts +0 -94
  83. package/typings/node.d.ts +0 -29
  84. package/typings/parser.d.ts +0 -16
  85. package/typings/table.d.ts +0 -14
  86. package/typings/token.d.ts +0 -22
  87. package/typings/tool.d.ts +0 -11
package/src/link/file.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const Title = require('../../lib/title'),
4
4
  {explode, noWrap} = require('../../util/string'),
5
5
  {externalUse} = require('../../util/debug'),
6
+ {generateForChild} = require('../../util/lint'),
6
7
  Parser = require('../..'),
7
8
  LinkToken = require('.'),
8
9
  ImageParameterToken = require('../imageParameter');
@@ -16,12 +17,6 @@ class FileToken extends LinkToken {
16
17
  /** @type {Set<string>} */ #keys = new Set();
17
18
  /** @type {Record<string, Set<ImageParameterToken>>} */ #args = {};
18
19
 
19
- setLangLink = undefined;
20
- setFragment = undefined;
21
- asSelfLink = undefined;
22
- setLinkText = undefined;
23
- pipeTrick = undefined;
24
-
25
20
  /** 图片链接 */
26
21
  get link() {
27
22
  return this.getArg('link')?.link;
@@ -69,42 +64,45 @@ class FileToken extends LinkToken {
69
64
  * @param {string|undefined} text 图片参数
70
65
  * @param {Title} title 文件标题对象
71
66
  * @param {accum} accum
67
+ * @param {string} delimiter `|`
72
68
  * @complexity `n`
73
69
  */
74
- constructor(link, text, title, config = Parser.getConfig(), accum = []) {
75
- super(link, undefined, title, config, accum);
70
+ constructor(link, text, title, config = Parser.getConfig(), accum = [], delimiter = '|') {
71
+ super(link, undefined, title, config, accum, delimiter);
76
72
  this.setAttribute('acceptable', {AtomToken: 0, ImageParameterToken: '1:'});
77
73
  this.append(...explode('-{', '}-', '|', text).map(part => new ImageParameterToken(part, config, accum)));
78
- this.seal(['setLangLink', 'setFragment', 'asSelfLink', 'setLinkText', 'pipeTrick'], true);
74
+ this.seal(
75
+ ['selfLink', 'interwiki', 'setLangLink', 'setFragment', 'asSelfLink', 'setLinkText', 'pipeTrick'],
76
+ true,
77
+ );
79
78
  }
80
79
 
81
80
  /**
82
81
  * @override
83
- * @param {number} i 移除位置
84
- * @complexity `n`
82
+ * @param {number} start 起始位置
85
83
  */
86
- removeAt(i) {
87
- const /** @type {ImageParameterToken} */ token = super.removeAt(i),
88
- args = this.getArgs(token.name, false, false);
89
- args.delete(token);
90
- if (args.size === 0) {
91
- this.#keys.delete(token.name);
84
+ lint(start = 0) {
85
+ const errors = super.lint(start),
86
+ frameArgs = this.getFrameArgs(),
87
+ horizAlignArgs = this.getHorizAlignArgs(),
88
+ vertAlignArgs = this.getVertAlignArgs(),
89
+ captions = this.getArgs('caption');
90
+ if (frameArgs.length > 1 || horizAlignArgs.length > 1 || vertAlignArgs.length > 1 || captions.size > 1) {
91
+ const rect = this.getRootNode().posFromIndex(start);
92
+ if (frameArgs.length > 1) {
93
+ errors.push(...frameArgs.map(arg => generateForChild(arg, rect, '重复或冲突的图片框架参数')));
94
+ }
95
+ if (horizAlignArgs.length > 1) {
96
+ errors.push(...horizAlignArgs.map(arg => generateForChild(arg, rect, '重复或冲突的图片水平对齐参数')));
97
+ }
98
+ if (vertAlignArgs.length > 1) {
99
+ errors.push(...vertAlignArgs.map(arg => generateForChild(arg, rect, '重复或冲突的图片垂直对齐参数')));
100
+ }
101
+ if (captions.size > 1) {
102
+ errors.push(...[...captions].map(arg => generateForChild(arg, rect, '重复的图片说明')));
103
+ }
92
104
  }
93
- return token;
94
- }
95
-
96
- /**
97
- * @override
98
- * @param {ImageParameterToken} token 待插入的子节点
99
- * @param {number} i 插入位置
100
- * @complexity `n`
101
- */
102
- insertAt(token, i = this.childNodes.length) {
103
- if (!Parser.running) {
104
- this.getArgs(token.name, false, false).add(token);
105
- this.#keys.add(token.name);
106
- }
107
- return super.insertAt(token, i);
105
+ return errors;
108
106
  }
109
107
 
110
108
  /**
@@ -115,6 +113,27 @@ class FileToken extends LinkToken {
115
113
  return this.childNodes.slice(1);
116
114
  }
117
115
 
116
+ /**
117
+ * 获取指定图片参数
118
+ * @param {string} key 参数名
119
+ * @param {boolean} copy 是否返回备份
120
+ * @complexity `n`
121
+ */
122
+ getArgs(key, copy = true) {
123
+ if (typeof key !== 'string') {
124
+ this.typeError('getArgs', 'String');
125
+ }
126
+ copy ||= !Parser.debugging && externalUse('getArgs');
127
+ let args;
128
+ if (Object.hasOwn(this.#args, key)) {
129
+ args = this.#args[key];
130
+ } else {
131
+ args = new Set(this.getAllArgs().filter(({name}) => key === name));
132
+ this.#args[key] = args;
133
+ }
134
+ return copy ? new Set(args) : args;
135
+ }
136
+
118
137
  /**
119
138
  * 获取图片框架属性参数节点
120
139
  * @complexity `n`
@@ -129,31 +148,31 @@ class FileToken extends LinkToken {
129
148
  }
130
149
 
131
150
  /**
132
- * 获取指定图片参数
133
- * @param {string} key 参数名
134
- * @param {boolean} copy 是否返回备份
151
+ * 获取图片水平对齐参数节点
135
152
  * @complexity `n`
136
153
  */
137
- getArgs(key, copy = true) {
138
- if (typeof key !== 'string') {
139
- this.typeError('getArgs', 'String');
140
- }
141
- copy ||= !Parser.debugging && externalUse('getArgs');
142
- let args = this.#args[key];
143
- if (!args) {
144
- args = new Set(this.getAllArgs().filter(({name}) => key === name));
145
- this.#args[key] = args;
154
+ getHorizAlignArgs() {
155
+ const args = this.getAllArgs()
156
+ .filter(({name}) => ['left', 'right', 'center', 'none'].includes(name));
157
+ if (args.length > 1) {
158
+ Parser.error(`图片 ${this.name} 带有 ${args.length} 个水平对齐参数,只有第 1 个 ${args[0].name} 会生效!`);
146
159
  }
147
- return copy ? new Set(args) : args;
160
+ return args;
148
161
  }
149
162
 
150
163
  /**
151
- * 是否具有指定图片参数
152
- * @param {string} key 参数名
164
+ * 获取图片垂直对齐参数节点
153
165
  * @complexity `n`
154
166
  */
155
- hasArg(key) {
156
- return this.getArgs(key, false).size > 0;
167
+ getVertAlignArgs() {
168
+ const args = this.getAllArgs().filter(
169
+ ({name}) => ['baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom']
170
+ .includes(name),
171
+ );
172
+ if (args.length > 1) {
173
+ Parser.error(`图片 ${this.name} 带有 ${args.length} 个垂直对齐架参数,只有第 1 个 ${args[0].name} 会生效!`);
174
+ }
175
+ return args;
157
176
  }
158
177
 
159
178
  /**
@@ -165,6 +184,15 @@ class FileToken extends LinkToken {
165
184
  return [...this.getArgs(key, false)].sort((a, b) => a.compareDocumentPosition(b)).at(-1);
166
185
  }
167
186
 
187
+ /**
188
+ * 是否具有指定图片参数
189
+ * @param {string} key 参数名
190
+ * @complexity `n`
191
+ */
192
+ hasArg(key) {
193
+ return this.getArgs(key, false).size > 0;
194
+ }
195
+
168
196
  /**
169
197
  * 移除指定图片参数
170
198
  * @param {string} key 参数名
@@ -203,7 +231,7 @@ class FileToken extends LinkToken {
203
231
  * 获取生效的指定图片参数值
204
232
  * @template {string|undefined} T
205
233
  * @param {T} key 参数名
206
- * @returns {T extends undefined ? Object<string, string> : string|true}
234
+ * @returns {T extends undefined ? Record<string, string> : string|true}
207
235
  * @complexity `n`
208
236
  */
209
237
  getValue(key) {
@@ -246,18 +274,46 @@ class FileToken extends LinkToken {
246
274
  this.typeError('setValue', 'Boolean');
247
275
  }
248
276
  const newArg = Parser.run(() => new ImageParameterToken(syntax, config));
249
- this.appendChild(newArg);
277
+ this.insertAt(newArg);
250
278
  return;
251
279
  }
252
280
  const wikitext = `[[File:F|${syntax ? syntax.replace('$1', value) : value}]]`,
253
281
  root = Parser.parse(wikitext, this.getAttribute('include'), 6, config),
254
- {childNodes: {length}, firstElementChild} = root;
255
- if (length !== 1 || !firstElementChild?.matches('file#File\\:F')
256
- || firstElementChild.childNodes.length !== 2 || firstElementChild.lastElementChild.name !== key
257
- ) {
282
+ {length, firstChild: file} = root,
283
+ {name, type, length: fileLength, lastChild: imageParameter} = file;
284
+ if (length !== 1 || type !== 'file' || name !== 'File:F' || fileLength !== 2 || imageParameter.name !== key) {
258
285
  throw new SyntaxError(`非法的 ${key} 参数:${noWrap(value)}`);
259
286
  }
260
- this.appendChild(firstElementChild.lastChild);
287
+ this.insertAt(imageParameter);
288
+ }
289
+
290
+ /**
291
+ * @override
292
+ * @param {number} i 移除位置
293
+ * @complexity `n`
294
+ */
295
+ removeAt(i) {
296
+ const /** @type {ImageParameterToken} */ token = super.removeAt(i),
297
+ args = this.getArgs(token.name, false, false);
298
+ args.delete(token);
299
+ if (args.size === 0) {
300
+ this.#keys.delete(token.name);
301
+ }
302
+ return token;
303
+ }
304
+
305
+ /**
306
+ * @override
307
+ * @param {ImageParameterToken} token 待插入的子节点
308
+ * @param {number} i 插入位置
309
+ * @complexity `n`
310
+ */
311
+ insertAt(token, i = this.childNodes.length) {
312
+ if (!Parser.running) {
313
+ this.getArgs(token.name, false, false).add(token);
314
+ this.#keys.add(token.name);
315
+ }
316
+ return super.insertAt(token, i);
261
317
  }
262
318
  }
263
319
 
@@ -1,8 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const Title = require('../../lib/title'),
3
+ const {undo} = require('../../util/debug'),
4
+ {generateForSelf} = require('../../util/lint'),
5
+ Title = require('../../lib/title'),
4
6
  Parser = require('../..'),
5
- Token = require('..'),
6
7
  FileToken = require('./file');
7
8
 
8
9
  /**
@@ -11,10 +12,7 @@ const Title = require('../../lib/title'),
11
12
  */
12
13
  class GalleryImageToken extends FileToken {
13
14
  type = 'gallery-image';
14
-
15
- size = undefined;
16
- width = undefined;
17
- height = undefined;
15
+ #invalid = false;
18
16
 
19
17
  /**
20
18
  * @param {string} link 图片文件名
@@ -25,6 +23,7 @@ class GalleryImageToken extends FileToken {
25
23
  constructor(link, text, title, config = Parser.getConfig(), accum = []) {
26
24
  let token;
27
25
  if (text !== undefined) {
26
+ const Token = require('..');
28
27
  token = new Token(text, config, true, accum);
29
28
  token.type = 'temp';
30
29
  for (let n = 1; n < Parser.MAX_STAGE; n++) {
@@ -32,10 +31,43 @@ class GalleryImageToken extends FileToken {
32
31
  }
33
32
  accum.splice(accum.indexOf(token), 1);
34
33
  }
35
- const newConfig = structuredClone(config);
36
- newConfig.img = Object.fromEntries(Object.entries(config.img).filter(([, param]) => param !== 'width'));
37
- super(link, token?.toString(), title, newConfig, accum);
38
- this.seal(['size', 'width', 'height'], true);
34
+ super(link, token?.toString(), title, config, accum);
35
+ this.setAttribute('bracket', false);
36
+ if (!Object.values(config.img).includes('width')) {
37
+ this.seal(['size', 'width', 'height'], true);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * @override
43
+ * @throws `Error` 非法的内链目标
44
+ * @throws `Error` 不可更改命名空间
45
+ */
46
+ afterBuild() {
47
+ const {
48
+ title: initTitle, interwiki: initInterwiki, ns: initNs,
49
+ } = this.normalizeTitle(String(this.firstChild), this.type === 'imagemap-image' ? 0 : 6, true);
50
+ this.setAttribute('name', initTitle);
51
+ this.#invalid = initInterwiki || initNs !== 6; // 只用于gallery-image的首次解析
52
+ const /** @type {AstListener} */ linkListener = (e, data) => {
53
+ const {prevTarget} = e;
54
+ if (prevTarget?.type === 'link-target') {
55
+ const name = String(prevTarget),
56
+ defaultNs = this.type === 'imagemap-image' ? 0 : 6,
57
+ {title, interwiki, ns, valid} = this.normalizeTitle(name, defaultNs, true);
58
+ if (!valid) {
59
+ undo(e, data);
60
+ throw new Error(`非法的图片文件名:${name}`);
61
+ } else if (interwiki || ns !== 6) {
62
+ undo(e, data);
63
+ throw new Error(`图片链接不可更改命名空间:${name}`);
64
+ }
65
+ this.setAttribute('name', title);
66
+ this.#invalid = false;
67
+ }
68
+ };
69
+ this.addEventListener(['remove', 'insert', 'replace', 'text'], linkListener);
70
+ return this;
39
71
  }
40
72
 
41
73
  /** @override */
@@ -43,6 +75,18 @@ class GalleryImageToken extends FileToken {
43
75
  return 0;
44
76
  }
45
77
 
78
+ /**
79
+ * @override
80
+ * @param {number} start 起始位置
81
+ */
82
+ lint(start = 0) {
83
+ const errors = super.lint(start);
84
+ if (this.#invalid) {
85
+ errors.push(generateForSelf(this, this.getRootNode().posFromIndex(start), '无效的图库图片'));
86
+ }
87
+ return errors;
88
+ }
89
+
46
90
  /**
47
91
  * @override
48
92
  * @param {string} selector
@@ -55,6 +99,26 @@ class GalleryImageToken extends FileToken {
55
99
  text() {
56
100
  return super.text().replaceAll('\n', ' ');
57
101
  }
102
+
103
+ /**
104
+ * @override
105
+ * @param {string} link 链接目标
106
+ * @throws `SyntaxError` 非法的链接目标
107
+ */
108
+ setTarget(link) {
109
+ link = String(link);
110
+ const include = this.getAttribute('include'),
111
+ config = this.getAttribute('config'),
112
+ root = Parser.parse(`<gallery>${link}</gallery>`, include, 1, config),
113
+ {length, firstChild: gallery} = root,
114
+ {type, lastChild: {length: galleryLength, firstChild: image}} = gallery;
115
+ if (length !== 1 || type !== 'ext' || galleryLength !== 1 || image.type !== 'gallery-image') {
116
+ throw new SyntaxError(`非法的图库文件名:${link}`);
117
+ }
118
+ const {firstChild} = image;
119
+ image.destroy(true);
120
+ this.firstChild.safeReplaceWith(firstChild);
121
+ }
58
122
  }
59
123
 
60
124
  Parser.classes.GalleryImageToken = __filename;
package/src/link/index.js CHANGED
@@ -13,6 +13,8 @@ const Title = require('../../lib/title'),
13
13
  */
14
14
  class LinkToken extends Token {
15
15
  type = 'link';
16
+ #bracket = true;
17
+ #delimiter;
16
18
 
17
19
  /** 完整链接,和FileToken保持一致 */
18
20
  get link() {
@@ -62,12 +64,12 @@ class LinkToken extends Token {
62
64
 
63
65
  /** 链接显示文字 */
64
66
  get innerText() {
65
- if (this.type !== 'link') {
66
- return undefined;
67
+ if (this.type === 'link') {
68
+ return this.childNodes.length > 1
69
+ ? this.lastChild.text()
70
+ : this.firstChild.text().replace(/^\s*:/u, '');
67
71
  }
68
- return this.childNodes.length > 1
69
- ? this.lastElementChild.text()
70
- : this.firstElementChild.text().replace(/^\s*:/u, '');
72
+ return undefined;
71
73
  }
72
74
 
73
75
  /**
@@ -75,37 +77,21 @@ class LinkToken extends Token {
75
77
  * @param {string|undefined} linkText 链接显示文字
76
78
  * @param {Title} title 链接标题对象
77
79
  * @param {accum} accum
80
+ * @param {string} delimiter `|`
78
81
  */
79
- constructor(link, linkText, title, config = Parser.getConfig(), accum = []) {
82
+ constructor(link, linkText, title, config = Parser.getConfig(), accum = [], delimiter = '|') {
80
83
  super(undefined, config, true, accum, {AtomToken: 0, Token: 1});
81
84
  const AtomToken = require('../atom');
82
- this.appendChild(new AtomToken(link, 'link-target', config, accum, {
85
+ this.insertAt(new AtomToken(link, 'link-target', config, accum, {
83
86
  'Stage-2': ':', '!ExtToken': '', '!HeadingToken': '',
84
87
  }));
85
88
  if (linkText !== undefined) {
86
89
  const inner = new Token(linkText, config, true, accum, {'Stage-5': ':', ConverterToken: ':'});
87
90
  inner.type = 'link-text';
88
- this.appendChild(inner.setAttribute('stage', Parser.MAX_STAGE - 1));
91
+ this.insertAt(inner.setAttribute('stage', Parser.MAX_STAGE - 1));
89
92
  }
90
- this.setAttribute('name', title.title).getAttribute('protectChildren')(0);
91
- }
92
-
93
- /** 生成Title对象 */
94
- #getTitle() {
95
- return this.normalizeTitle(this.firstElementChild.text());
96
- }
97
-
98
- /** @override */
99
- cloneNode() {
100
- const [link, ...linkText] = this.cloneChildNodes();
101
- return Parser.run(() => {
102
- /** @type {this & {constructor: typeof LinkToken}} */
103
- const {constructor} = this,
104
- token = new constructor('', undefined, this.#getTitle(), this.getAttribute('config'));
105
- token.firstElementChild.safeReplaceWith(link);
106
- token.append(...linkText);
107
- return token.afterBuild();
108
- });
93
+ this.#delimiter = delimiter;
94
+ this.getAttribute('protectChildren')(0);
109
95
  }
110
96
 
111
97
  /**
@@ -114,6 +100,10 @@ class LinkToken extends Token {
114
100
  * @throws `Error` 不可更改命名空间
115
101
  */
116
102
  afterBuild() {
103
+ this.setAttribute('name', this.normalizeTitle(this.firstChild.text()).title);
104
+ if (this.#delimiter?.includes('\0')) {
105
+ this.#delimiter = this.getAttribute('buildFromStr')(this.#delimiter).map(String).join('');
106
+ }
117
107
  const /** @type {AstListener} */ linkListener = (e, data) => {
118
108
  const {prevTarget} = e;
119
109
  if (prevTarget?.type === 'link-target') {
@@ -142,13 +132,27 @@ class LinkToken extends Token {
142
132
  return this;
143
133
  }
144
134
 
135
+ /**
136
+ * @override
137
+ * @template {string} T
138
+ * @param {T} key 属性键
139
+ * @param {TokenAttribute<T>} value 属性值
140
+ */
141
+ setAttribute(key, value) {
142
+ if (key === 'bracket') {
143
+ this.#bracket = Boolean(value);
144
+ return this;
145
+ }
146
+ return super.setAttribute(key, value);
147
+ }
148
+
145
149
  /**
146
150
  * @override
147
151
  * @param {string} selector
148
152
  */
149
153
  toString(selector) {
150
- const str = super.toString(selector, '|');
151
- return this.type === 'gallery-image' || selector && this.matches(selector) ? str : `[[${str}]]`;
154
+ const str = super.toString(selector, this.#delimiter);
155
+ return this.#bracket && !(selector && this.matches(selector)) ? `[[${str}]]` : str;
152
156
  }
153
157
 
154
158
  /** @override */
@@ -158,13 +162,37 @@ class LinkToken extends Token {
158
162
 
159
163
  /** @override */
160
164
  getGaps() {
161
- return 1;
165
+ return this.#delimiter.length;
166
+ }
167
+
168
+ /** @override */
169
+ print() {
170
+ return super.print(this.#bracket ? {pre: '[[', post: ']]', sep: this.#delimiter} : {sep: this.#delimiter});
171
+ }
172
+
173
+ /** 生成Title对象 */
174
+ #getTitle() {
175
+ return this.normalizeTitle(this.firstChild.text());
176
+ }
177
+
178
+ /**
179
+ * @override
180
+ * @this {LinkToken & {constructor: typeof LinkToken}}
181
+ */
182
+ cloneNode() {
183
+ const [link, ...linkText] = this.cloneChildNodes();
184
+ return Parser.run(() => {
185
+ const token = new this.constructor('', undefined, this.#getTitle(), this.getAttribute('config'));
186
+ token.firstChild.safeReplaceWith(link);
187
+ token.append(...linkText);
188
+ return token.afterBuild();
189
+ });
162
190
  }
163
191
 
164
192
  /** @override */
165
193
  text() {
166
194
  const str = super.text('|');
167
- return this.type === 'gallery-image' ? str : `[[${str}]]`;
195
+ return this.#bracket ? `[[${str}]]` : str;
168
196
  }
169
197
 
170
198
  /**
@@ -178,14 +206,14 @@ class LinkToken extends Token {
178
206
  link = `:${link}`;
179
207
  }
180
208
  const root = Parser.parse(`[[${link}]]`, this.getAttribute('include'), 6, this.getAttribute('config')),
181
- {childNodes: {length}, firstElementChild} = root;
182
- if (length !== 1 || firstElementChild?.type !== this.type || firstElementChild.childNodes.length !== 1) {
183
- const msgs = {link: '内链', file: '文件链接', category: '分类', 'gallery-image': '文件链接'};
209
+ {length, firstChild: wikiLink} = root,
210
+ {type, firstChild, length: linkLength} = wikiLink;
211
+ if (length !== 1 || type !== this.type || linkLength !== 1) {
212
+ const msgs = {link: '内链', file: '文件链接', category: '分类'};
184
213
  throw new SyntaxError(`非法的${msgs[this.type]}目标:${link}`);
185
214
  }
186
- const {firstChild} = firstElementChild;
187
- firstElementChild.destroy(true);
188
- this.firstElementChild.safeReplaceWith(firstChild);
215
+ wikiLink.destroy(true);
216
+ this.firstChild.safeReplaceWith(firstChild);
189
217
  }
190
218
 
191
219
  /**
@@ -201,20 +229,18 @@ class LinkToken extends Token {
201
229
  link = String(link).trim();
202
230
  const [char] = link;
203
231
  if (char === '#') {
204
- throw new SyntaxError(`跨语言链接不能仅为fragment!`);
232
+ throw new SyntaxError('跨语言链接不能仅为fragment!');
205
233
  } else if (char === ':') {
206
234
  link = link.slice(1);
207
235
  }
208
236
  const root = Parser.parse(`[[${lang}:${link}]]`, this.getAttribute('include'), 6, this.getAttribute('config')),
209
- /** @type {Token & {firstElementChild: LinkToken}} */ {childNodes: {length}, firstElementChild} = root;
210
- if (length !== 1 || firstElementChild?.type !== 'link' || firstElementChild.childNodes.length !== 1
211
- || firstElementChild.interwiki !== lang.toLowerCase()
212
- ) {
237
+ /** @type {Token & {firstChild: LinkToken}} */ {length, firstChild: wikiLink} = root,
238
+ {type, length: linkLength, interwiki, firstChild} = wikiLink;
239
+ if (length !== 1 || type !== 'link' || linkLength !== 1 || interwiki !== lang.toLowerCase()) {
213
240
  throw new SyntaxError(`非法的跨语言链接目标:${lang}:${link}`);
214
241
  }
215
- const {firstChild} = firstElementChild;
216
- firstElementChild.destroy(true);
217
- this.firstElementChild.safeReplaceWith(firstChild);
242
+ wikiLink.destroy(true);
243
+ this.firstChild.safeReplaceWith(firstChild);
218
244
  }
219
245
 
220
246
  /**
@@ -228,16 +254,15 @@ class LinkToken extends Token {
228
254
  const include = this.getAttribute('include'),
229
255
  config = this.getAttribute('config'),
230
256
  root = Parser.parse(`[[${page ? `:${this.name}` : ''}#${fragment}]]`, include, 6, config),
231
- {childNodes: {length}, firstElementChild} = root;
232
- if (length !== 1 || firstElementChild?.type !== 'link' || firstElementChild.childNodes.length !== 1) {
257
+ {length, firstChild: wikiLink} = root,
258
+ {type, length: linkLength, firstChild} = wikiLink;
259
+ if (length !== 1 || type !== 'link' || linkLength !== 1) {
233
260
  throw new SyntaxError(`非法的 fragment:${fragment}`);
234
- }
235
- if (page) {
261
+ } else if (page) {
236
262
  Parser.warn(`${this.constructor.name}.setFragment 方法会同时规范化页面名!`);
237
263
  }
238
- const {firstChild} = firstElementChild;
239
- firstElementChild.destroy(true);
240
- this.firstElementChild.safeReplaceWith(firstChild);
264
+ wikiLink.destroy(true);
265
+ this.firstChild.safeReplaceWith(firstChild);
241
266
  }
242
267
 
243
268
  /**
@@ -268,25 +293,25 @@ class LinkToken extends Token {
268
293
  */
269
294
  setLinkText(linkText = '') {
270
295
  linkText = String(linkText);
271
- let lastElementChild;
296
+ let lastChild;
272
297
  const config = this.getAttribute('config');
273
298
  if (linkText) {
274
299
  const root = Parser.parse(`[[${
275
300
  this.type === 'category' ? 'Category:' : ''
276
301
  }L|${linkText}]]`, this.getAttribute('include'), 6, config),
277
- {childNodes: {length}, firstElementChild} = root;
278
- if (length !== 1 || firstElementChild?.type !== this.type || firstElementChild.childNodes.length !== 2) {
302
+ {length, firstChild: wikiLink} = root;
303
+ if (length !== 1 || wikiLink.type !== this.type || wikiLink.childNodes.length !== 2) {
279
304
  throw new SyntaxError(`非法的${this.type === 'link' ? '内链文字' : '分类关键字'}:${noWrap(linkText)}`);
280
305
  }
281
- ({lastElementChild} = firstElementChild);
306
+ ({lastChild} = wikiLink);
282
307
  } else {
283
- lastElementChild = Parser.run(() => new Token('', config));
284
- lastElementChild.setAttribute('stage', 7).type = 'link-text';
308
+ lastChild = Parser.run(() => new Token('', config));
309
+ lastChild.setAttribute('stage', 7).type = 'link-text';
285
310
  }
286
311
  if (this.childNodes.length === 1) {
287
- this.appendChild(lastElementChild);
312
+ this.insertAt(lastChild);
288
313
  } else {
289
- this.lastElementChild.safeReplaceWith(lastElementChild);
314
+ this.lastChild.safeReplaceWith(lastChild);
290
315
  }
291
316
  }
292
317
 
@@ -295,7 +320,7 @@ class LinkToken extends Token {
295
320
  * @throws `Error` 带有"#"或"%"时不可用
296
321
  */
297
322
  pipeTrick() {
298
- const linkText = this.firstElementChild.text();
323
+ const linkText = this.firstChild.text();
299
324
  if (linkText.includes('#') || linkText.includes('%')) {
300
325
  throw new Error('Pipe trick 不能用于带有"#"或"%"的场合!');
301
326
  }