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
@@ -11,11 +11,6 @@ const Title = require('../../lib/title'),
11
11
  class CategoryToken extends LinkToken {
12
12
  type = 'category';
13
13
 
14
- setLangLink = undefined;
15
- setFragment = undefined;
16
- asSelfLink = undefined;
17
- pipeTrick = undefined;
18
-
19
14
  /** 分类排序关键字 */
20
15
  get sortkey() {
21
16
  return this.childNodes[1]?.text()?.replaceAll(
@@ -34,10 +29,11 @@ class CategoryToken extends LinkToken {
34
29
  * @param {string|undefined} text 排序关键字
35
30
  * @param {Title} title 分类页面标题对象
36
31
  * @param {accum} accum
32
+ * @param {string} delimiter `|`
37
33
  */
38
- constructor(link, text, title, config = Parser.getConfig(), accum = []) {
39
- super(link, text, title, config, accum);
40
- this.seal(['setFragment', 'asSelfLink', 'pipeTrick'], true);
34
+ constructor(link, text, title, config = Parser.getConfig(), accum = [], delimiter = '|') {
35
+ super(link, text, title, config, accum, delimiter);
36
+ this.seal(['selfLink', 'interwiki', 'setLangLink', 'setFragment', 'asSelfLink', 'pipeTrick'], true);
41
37
  }
42
38
 
43
39
  /**
package/src/link/file.js CHANGED
@@ -17,12 +17,6 @@ class FileToken extends LinkToken {
17
17
  /** @type {Set<string>} */ #keys = new Set();
18
18
  /** @type {Record<string, Set<ImageParameterToken>>} */ #args = {};
19
19
 
20
- setLangLink = undefined;
21
- setFragment = undefined;
22
- asSelfLink = undefined;
23
- setLinkText = undefined;
24
- pipeTrick = undefined;
25
-
26
20
  /** 图片链接 */
27
21
  get link() {
28
22
  return this.getArg('link')?.link;
@@ -70,13 +64,17 @@ class FileToken extends LinkToken {
70
64
  * @param {string|undefined} text 图片参数
71
65
  * @param {Title} title 文件标题对象
72
66
  * @param {accum} accum
67
+ * @param {string} delimiter `|`
73
68
  * @complexity `n`
74
69
  */
75
- constructor(link, text, title, config = Parser.getConfig(), accum = []) {
76
- super(link, undefined, title, config, accum);
70
+ constructor(link, text, title, config = Parser.getConfig(), accum = [], delimiter = '|') {
71
+ super(link, undefined, title, config, accum, delimiter);
77
72
  this.setAttribute('acceptable', {AtomToken: 0, ImageParameterToken: '1:'});
78
73
  this.append(...explode('-{', '}-', '|', text).map(part => new ImageParameterToken(part, config, accum)));
79
- this.seal(['setLangLink', 'setFragment', 'asSelfLink', 'setLinkText', 'pipeTrick'], true);
74
+ this.seal(
75
+ ['selfLink', 'interwiki', 'setLangLink', 'setFragment', 'asSelfLink', 'setLinkText', 'pipeTrick'],
76
+ true,
77
+ );
80
78
  }
81
79
 
82
80
  /**
@@ -86,12 +84,20 @@ class FileToken extends LinkToken {
86
84
  lint(start = 0) {
87
85
  const errors = super.lint(start),
88
86
  frameArgs = this.getFrameArgs(),
87
+ horizAlignArgs = this.getHorizAlignArgs(),
88
+ vertAlignArgs = this.getVertAlignArgs(),
89
89
  captions = this.getArgs('caption');
90
- if (frameArgs.length > 1 || captions.size > 1) {
90
+ if (frameArgs.length > 1 || horizAlignArgs.length > 1 || vertAlignArgs.length > 1 || captions.size > 1) {
91
91
  const rect = this.getRootNode().posFromIndex(start);
92
92
  if (frameArgs.length > 1) {
93
93
  errors.push(...frameArgs.map(arg => generateForChild(arg, rect, '重复或冲突的图片框架参数')));
94
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
+ }
95
101
  if (captions.size > 1) {
96
102
  errors.push(...[...captions].map(arg => generateForChild(arg, rect, '重复的图片说明')));
97
103
  }
@@ -100,40 +106,32 @@ class FileToken extends LinkToken {
100
106
  }
101
107
 
102
108
  /**
103
- * @override
104
- * @param {number} i 移除位置
105
- * @complexity `n`
109
+ * 获取所有图片参数节点
110
+ * @returns {ImageParameterToken[]}
106
111
  */
107
- removeAt(i) {
108
- const /** @type {ImageParameterToken} */ token = super.removeAt(i),
109
- args = this.getArgs(token.name, false, false);
110
- args.delete(token);
111
- if (args.size === 0) {
112
- this.#keys.delete(token.name);
113
- }
114
- return token;
112
+ getAllArgs() {
113
+ return this.childNodes.slice(1);
115
114
  }
116
115
 
117
116
  /**
118
- * @override
119
- * @param {ImageParameterToken} token 待插入的子节点
120
- * @param {number} i 插入位置
117
+ * 获取指定图片参数
118
+ * @param {string} key 参数名
119
+ * @param {boolean} copy 是否返回备份
121
120
  * @complexity `n`
122
121
  */
123
- insertAt(token, i = this.childNodes.length) {
124
- if (!Parser.running) {
125
- this.getArgs(token.name, false, false).add(token);
126
- this.#keys.add(token.name);
122
+ getArgs(key, copy = true) {
123
+ if (typeof key !== 'string') {
124
+ this.typeError('getArgs', 'String');
127
125
  }
128
- return super.insertAt(token, i);
129
- }
130
-
131
- /**
132
- * 获取所有图片参数节点
133
- * @returns {ImageParameterToken[]}
134
- */
135
- getAllArgs() {
136
- return this.childNodes.slice(1);
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;
137
135
  }
138
136
 
139
137
  /**
@@ -150,31 +148,31 @@ class FileToken extends LinkToken {
150
148
  }
151
149
 
152
150
  /**
153
- * 获取指定图片参数
154
- * @param {string} key 参数名
155
- * @param {boolean} copy 是否返回备份
151
+ * 获取图片水平对齐参数节点
156
152
  * @complexity `n`
157
153
  */
158
- getArgs(key, copy = true) {
159
- if (typeof key !== 'string') {
160
- this.typeError('getArgs', 'String');
161
- }
162
- copy ||= !Parser.debugging && externalUse('getArgs');
163
- let args = this.#args[key];
164
- if (!args) {
165
- args = new Set(this.getAllArgs().filter(({name}) => key === name));
166
- 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} 会生效!`);
167
159
  }
168
- return copy ? new Set(args) : args;
160
+ return args;
169
161
  }
170
162
 
171
163
  /**
172
- * 是否具有指定图片参数
173
- * @param {string} key 参数名
164
+ * 获取图片垂直对齐参数节点
174
165
  * @complexity `n`
175
166
  */
176
- hasArg(key) {
177
- 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;
178
176
  }
179
177
 
180
178
  /**
@@ -186,6 +184,15 @@ class FileToken extends LinkToken {
186
184
  return [...this.getArgs(key, false)].sort((a, b) => a.compareDocumentPosition(b)).at(-1);
187
185
  }
188
186
 
187
+ /**
188
+ * 是否具有指定图片参数
189
+ * @param {string} key 参数名
190
+ * @complexity `n`
191
+ */
192
+ hasArg(key) {
193
+ return this.getArgs(key, false).size > 0;
194
+ }
195
+
189
196
  /**
190
197
  * 移除指定图片参数
191
198
  * @param {string} key 参数名
@@ -224,7 +231,7 @@ class FileToken extends LinkToken {
224
231
  * 获取生效的指定图片参数值
225
232
  * @template {string|undefined} T
226
233
  * @param {T} key 参数名
227
- * @returns {T extends undefined ? Object<string, string> : string|true}
234
+ * @returns {T extends undefined ? Record<string, string> : string|true}
228
235
  * @complexity `n`
229
236
  */
230
237
  getValue(key) {
@@ -267,17 +274,46 @@ class FileToken extends LinkToken {
267
274
  this.typeError('setValue', 'Boolean');
268
275
  }
269
276
  const newArg = Parser.run(() => new ImageParameterToken(syntax, config));
270
- this.appendChild(newArg);
277
+ this.insertAt(newArg);
271
278
  return;
272
279
  }
273
280
  const wikitext = `[[File:F|${syntax ? syntax.replace('$1', value) : value}]]`,
274
281
  root = Parser.parse(wikitext, this.getAttribute('include'), 6, config),
275
- {childNodes: {length}, firstChild: file} = root,
276
- {name, type, childNodes: {length: fileLength}, lastChild: imageParameter} = file;
282
+ {length, firstChild: file} = root,
283
+ {name, type, length: fileLength, lastChild: imageParameter} = file;
277
284
  if (length !== 1 || type !== 'file' || name !== 'File:F' || fileLength !== 2 || imageParameter.name !== key) {
278
285
  throw new SyntaxError(`非法的 ${key} 参数:${noWrap(value)}`);
279
286
  }
280
- this.appendChild(imageParameter);
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);
281
317
  }
282
318
  }
283
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() {
@@ -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.firstChild.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.firstChild.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,18 +162,37 @@ class LinkToken extends Token {
158
162
 
159
163
  /** @override */
160
164
  getGaps() {
161
- return 1;
165
+ return this.#delimiter.length;
162
166
  }
163
167
 
164
168
  /** @override */
165
169
  print() {
166
- return super.print(this.type === 'gallery-image' ? {sep: '|'} : {pre: '[[', post: ']]', sep: '|'});
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
+ });
167
190
  }
168
191
 
169
192
  /** @override */
170
193
  text() {
171
194
  const str = super.text('|');
172
- return this.type === 'gallery-image' ? str : `[[${str}]]`;
195
+ return this.#bracket ? `[[${str}]]` : str;
173
196
  }
174
197
 
175
198
  /**
@@ -183,10 +206,10 @@ class LinkToken extends Token {
183
206
  link = `:${link}`;
184
207
  }
185
208
  const root = Parser.parse(`[[${link}]]`, this.getAttribute('include'), 6, this.getAttribute('config')),
186
- {childNodes: {length}, firstChild: wikiLink} = root,
187
- {type, firstChild, childNodes: {length: linkLength}} = wikiLink;
209
+ {length, firstChild: wikiLink} = root,
210
+ {type, firstChild, length: linkLength} = wikiLink;
188
211
  if (length !== 1 || type !== this.type || linkLength !== 1) {
189
- const msgs = {link: '内链', file: '文件链接', category: '分类', 'gallery-image': '文件链接'};
212
+ const msgs = {link: '内链', file: '文件链接', category: '分类'};
190
213
  throw new SyntaxError(`非法的${msgs[this.type]}目标:${link}`);
191
214
  }
192
215
  wikiLink.destroy(true);
@@ -206,13 +229,13 @@ class LinkToken extends Token {
206
229
  link = String(link).trim();
207
230
  const [char] = link;
208
231
  if (char === '#') {
209
- throw new SyntaxError(`跨语言链接不能仅为fragment!`);
232
+ throw new SyntaxError('跨语言链接不能仅为fragment!');
210
233
  } else if (char === ':') {
211
234
  link = link.slice(1);
212
235
  }
213
236
  const root = Parser.parse(`[[${lang}:${link}]]`, this.getAttribute('include'), 6, this.getAttribute('config')),
214
- /** @type {Token & {firstChild: LinkToken}} */ {childNodes: {length}, firstChild: wikiLink} = root,
215
- {type, childNodes: {length: linkLength}, interwiki, firstChild} = wikiLink;
237
+ /** @type {Token & {firstChild: LinkToken}} */ {length, firstChild: wikiLink} = root,
238
+ {type, length: linkLength, interwiki, firstChild} = wikiLink;
216
239
  if (length !== 1 || type !== 'link' || linkLength !== 1 || interwiki !== lang.toLowerCase()) {
217
240
  throw new SyntaxError(`非法的跨语言链接目标:${lang}:${link}`);
218
241
  }
@@ -231,12 +254,11 @@ class LinkToken extends Token {
231
254
  const include = this.getAttribute('include'),
232
255
  config = this.getAttribute('config'),
233
256
  root = Parser.parse(`[[${page ? `:${this.name}` : ''}#${fragment}]]`, include, 6, config),
234
- {childNodes: {length}, firstChild: wikiLink} = root,
235
- {type, childNodes: {length: linkLength}, firstChild} = wikiLink;
257
+ {length, firstChild: wikiLink} = root,
258
+ {type, length: linkLength, firstChild} = wikiLink;
236
259
  if (length !== 1 || type !== 'link' || linkLength !== 1) {
237
260
  throw new SyntaxError(`非法的 fragment:${fragment}`);
238
- }
239
- if (page) {
261
+ } else if (page) {
240
262
  Parser.warn(`${this.constructor.name}.setFragment 方法会同时规范化页面名!`);
241
263
  }
242
264
  wikiLink.destroy(true);
@@ -277,7 +299,7 @@ class LinkToken extends Token {
277
299
  const root = Parser.parse(`[[${
278
300
  this.type === 'category' ? 'Category:' : ''
279
301
  }L|${linkText}]]`, this.getAttribute('include'), 6, config),
280
- {childNodes: {length}, firstChild: wikiLink} = root;
302
+ {length, firstChild: wikiLink} = root;
281
303
  if (length !== 1 || wikiLink.type !== this.type || wikiLink.childNodes.length !== 2) {
282
304
  throw new SyntaxError(`非法的${this.type === 'link' ? '内链文字' : '分类关键字'}:${noWrap(linkText)}`);
283
305
  }
@@ -287,7 +309,7 @@ class LinkToken extends Token {
287
309
  lastChild.setAttribute('stage', 7).type = 'link-text';
288
310
  }
289
311
  if (this.childNodes.length === 1) {
290
- this.appendChild(lastChild);
312
+ this.insertAt(lastChild);
291
313
  } else {
292
314
  this.lastChild.safeReplaceWith(lastChild);
293
315
  }
package/src/magicLink.js CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  const {generateForChild} = require('../util/lint'),
4
4
  Parser = require('..'),
5
- AstText = require('../lib/text'),
6
5
  Token = require('.');
7
6
 
8
7
  /**
@@ -21,11 +20,14 @@ class MagicLinkToken extends Token {
21
20
  set protocol(value) {
22
21
  if (typeof value !== 'string') {
23
22
  this.typeError('protocol', 'String');
24
- }
25
- if (!new RegExp(`${this.#protocolRegex.source}$`, 'iu').test(value)) {
23
+ } else if (!new RegExp(`${this.#protocolRegex.source}$`, 'iu').test(value)) {
26
24
  throw new RangeError(`非法的外链协议:${value}`);
27
25
  }
28
- this.replaceChildren(this.text().replace(this.#protocolRegex, value));
26
+ const {link} = this;
27
+ if (!this.#protocolRegex.test(link)) {
28
+ throw new Error(`特殊外链无法更改协议!${link}`);
29
+ }
30
+ this.replaceChildren(link.replace(this.#protocolRegex, value));
29
31
  }
30
32
 
31
33
  /** 和内链保持一致 */
@@ -76,25 +78,15 @@ class MagicLinkToken extends Token {
76
78
  return errors;
77
79
  }
78
80
 
79
- /** @override */
80
- afterBuild() {
81
- const ParameterToken = require('./parameter');
82
- const /** @type {ParameterToken} */ parameter = this.closest('parameter');
83
- if (parameter?.getValue() === this.text()) {
84
- this.replaceWith(...this.childNodes);
85
- }
86
- return this;
87
- }
88
-
89
81
  /** @override */
90
82
  cloneNode() {
91
- const cloned = this.cloneChildNodes(),
92
- token = Parser.run(() => new MagicLinkToken(
93
- undefined, this.type === 'ext-link-url', this.getAttribute('config'),
94
- ));
95
- token.append(...cloned);
96
- token.afterBuild();
97
- return token;
83
+ const cloned = this.cloneChildNodes();
84
+ return Parser.run(() => {
85
+ const token = new MagicLinkToken(undefined, this.type === 'ext-link-url', this.getAttribute('config'));
86
+ token.append(...cloned);
87
+ token.afterBuild();
88
+ return token;
89
+ });
98
90
  }
99
91
 
100
92
  /**
@@ -124,12 +116,19 @@ class MagicLinkToken extends Token {
124
116
  setTarget(url) {
125
117
  url = String(url);
126
118
  const root = Parser.parse(url, this.getAttribute('include'), 9, this.getAttribute('config')),
127
- {childNodes: {length}, firstChild: freeExtLink} = root;
119
+ {length, firstChild: freeExtLink} = root;
128
120
  if (length !== 1 || freeExtLink.type !== 'free-ext-link') {
129
121
  throw new SyntaxError(`非法的自由外链目标:${url}`);
130
122
  }
131
123
  this.replaceChildren(...freeExtLink.childNodes);
132
124
  }
125
+
126
+ /** 是否是模板或魔术字参数 */
127
+ isParamValue() {
128
+ const ParameterToken = require('./parameter');
129
+ const /** @type {ParameterToken} */ parameter = this.closest('parameter');
130
+ return parameter?.getValue() === this.text();
131
+ }
133
132
  }
134
133
 
135
134
  Parser.classes.MagicLinkToken = __filename;