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
package/src/extLink.js CHANGED
@@ -52,38 +52,17 @@ class ExtLinkToken extends Token {
52
52
  * @param {accum} accum
53
53
  */
54
54
  constructor(url, space, text, config = Parser.getConfig(), accum = []) {
55
- super(undefined, config, true, accum, {AtomToken: 0, Token: 1});
56
- this.appendChild(new MagicLinkToken(url, true, config, accum));
55
+ super(undefined, config, true, accum, {MagicLinkToken: 0, Token: 1});
56
+ this.insertAt(new MagicLinkToken(url, true, config, accum));
57
57
  this.#space = space;
58
58
  if (text) {
59
59
  const inner = new Token(text, config, true, accum, {'Stage-7': ':', ConverterToken: ':'});
60
60
  inner.type = 'ext-link-text';
61
- this.appendChild(inner.setAttribute('stage', Parser.MAX_STAGE - 1));
61
+ this.insertAt(inner.setAttribute('stage', Parser.MAX_STAGE - 1));
62
62
  }
63
63
  this.getAttribute('protectChildren')(0);
64
64
  }
65
65
 
66
- /** @override */
67
- cloneNode() {
68
- const [url, text] = this.cloneChildNodes(),
69
- token = Parser.run(() => new ExtLinkToken(undefined, '', '', this.getAttribute('config')));
70
- token.firstChild.safeReplaceWith(url);
71
- if (text) {
72
- token.appendChild(text);
73
- }
74
- return token;
75
- }
76
-
77
- /** 修正空白字符 */
78
- #correct() {
79
- if (!this.#space && this.childNodes.length > 1
80
- // 都替换成`<`肯定不对,但无妨
81
- && /^[^[\]<>"{\0-\x1F\x7F\p{Zs}\uFFFD]/u.test(this.lastChild.text().replace(/&[lg]t;/u, '<'))
82
- ) {
83
- this.#space = ' ';
84
- }
85
- }
86
-
87
66
  /**
88
67
  * @override
89
68
  * @param {string} selector
@@ -112,10 +91,33 @@ class ExtLinkToken extends Token {
112
91
 
113
92
  /** @override */
114
93
  print() {
115
- const {childNodes: {length}} = this;
94
+ const {length} = this;
116
95
  return super.print(length > 1 ? {pre: '[', sep: this.#space, post: ']'} : {pre: '[', post: `${this.#space}]`});
117
96
  }
118
97
 
98
+ /** @override */
99
+ cloneNode() {
100
+ const [url, text] = this.cloneChildNodes();
101
+ return Parser.run(() => {
102
+ const token = new ExtLinkToken(undefined, '', '', this.getAttribute('config'));
103
+ token.firstChild.safeReplaceWith(url);
104
+ if (text) {
105
+ token.insertAt(text);
106
+ }
107
+ return token;
108
+ });
109
+ }
110
+
111
+ /** 修正空白字符 */
112
+ #correct() {
113
+ if (!this.#space && this.childNodes.length > 1
114
+ // 都替换成`<`肯定不对,但无妨
115
+ && /^[^[\]<>"{\0-\x1F\x7F\p{Zs}\uFFFD]/u.test(this.lastChild.text().replace(/&[lg]t;/u, '<'))
116
+ ) {
117
+ this.#space = ' ';
118
+ }
119
+ }
120
+
119
121
  /** @override */
120
122
  text() {
121
123
  normalizeSpace(this.childNodes[1]);
@@ -138,7 +140,7 @@ class ExtLinkToken extends Token {
138
140
  setTarget(url) {
139
141
  url = String(url);
140
142
  const root = Parser.parse(`[${url}]`, this.getAttribute('include'), 8, this.getAttribute('config')),
141
- {childNodes: {length}, firstChild: extLink} = root;
143
+ {length, firstChild: extLink} = root;
142
144
  if (length !== 1 || extLink.type !== 'ext-link' || extLink.childNodes.length !== 1) {
143
145
  throw new SyntaxError(`非法的外链目标:${url}`);
144
146
  }
@@ -155,13 +157,13 @@ class ExtLinkToken extends Token {
155
157
  setLinkText(text) {
156
158
  text = String(text);
157
159
  const root = Parser.parse(`[//url ${text}]`, this.getAttribute('include'), 8, this.getAttribute('config')),
158
- {childNodes: {length}, firstChild: extLink} = root;
160
+ {length, firstChild: extLink} = root;
159
161
  if (length !== 1 || extLink.type !== 'ext-link' || extLink.childNodes.length !== 2) {
160
162
  throw new SyntaxError(`非法的外链文字:${noWrap(text)}`);
161
163
  }
162
164
  const {lastChild} = extLink;
163
165
  if (this.childNodes.length === 1) {
164
- this.appendChild(lastChild);
166
+ this.insertAt(lastChild);
165
167
  } else {
166
168
  this.lastChild.safeReplaceWith(lastChild);
167
169
  }
package/src/gallery.js CHANGED
@@ -13,16 +13,23 @@ class GalleryToken extends Token {
13
13
  type = 'ext-inner';
14
14
  name = 'gallery';
15
15
 
16
+ /** 所有图片 */
17
+ get images() {
18
+ return this.childNodes.filter(({type}) => type === 'gallery-image');
19
+ }
20
+
16
21
  /**
17
22
  * @param {string} inner 标签内部wikitext
18
23
  * @param {accum} accum
19
24
  */
20
25
  constructor(inner, config = Parser.getConfig(), accum = []) {
21
- super(undefined, config, true, accum, {AstText: ':', GalleryImageToken: ':'});
26
+ super(undefined, config, true, accum, {AstText: ':', GalleryImageToken: ':', HiddenToken: ':'});
27
+ const newConfig = structuredClone(config);
28
+ newConfig.img = Object.fromEntries(Object.entries(config.img).filter(([, param]) => param !== 'width'));
22
29
  for (const line of inner?.split('\n') ?? []) {
23
30
  const matches = /^([^|]+)(?:\|(.*))?/u.exec(line);
24
31
  if (!matches) {
25
- this.appendChild(line.trim() ? new HiddenToken(line, undefined, config, [], {AstText: ':'}) : line);
32
+ super.insertAt(line.trim() ? new HiddenToken(line, undefined, config, [], {AstText: ':'}) : line);
26
33
  continue;
27
34
  }
28
35
  const [, file, alt] = matches;
@@ -33,21 +40,13 @@ class GalleryToken extends Token {
33
40
  title = this.normalizeTitle(file, 6, true);
34
41
  }
35
42
  if (title.valid) {
36
- this.appendChild(new GalleryImageToken(file, alt, title, config, accum));
43
+ super.insertAt(new GalleryImageToken(file, alt, title, newConfig, accum));
37
44
  } else {
38
- this.appendChild(new HiddenToken(line, undefined, config, [], {AstText: ':'}));
45
+ super.insertAt(new HiddenToken(line, undefined, config, [], {AstText: ':'}));
39
46
  }
40
47
  }
41
48
  }
42
49
 
43
- /** @override */
44
- cloneNode() {
45
- const cloned = this.cloneChildNodes(),
46
- token = Parser.run(() => new GalleryToken(undefined, this.getAttribute('config')));
47
- token.append(...cloned);
48
- return token;
49
- }
50
-
51
50
  /**
52
51
  * @override
53
52
  * @param {string} selector
@@ -66,6 +65,43 @@ class GalleryToken extends Token {
66
65
  return super.print({sep: '\n'});
67
66
  }
68
67
 
68
+ /**
69
+ * @override
70
+ * @param {number} start 起始位置
71
+ */
72
+ lint(start = 0) {
73
+ const {top, left} = this.getRootNode().posFromIndex(start),
74
+ /** @type {LintError[]} */ errors = [];
75
+ for (let i = 0, cur = start; i < this.childNodes.length; i++) {
76
+ const child = this.childNodes[i],
77
+ str = String(child),
78
+ trimmed = str.trim();
79
+ if (child.type === 'hidden' && trimmed && !/^<!--.*-->$/u.test(trimmed)) {
80
+ errors.push({
81
+ message: '图库中的无效内容',
82
+ startLine: top + i,
83
+ endLine: top + i,
84
+ startCol: i ? 0 : left,
85
+ endCol: i ? str.length : left + str.length,
86
+ });
87
+ } else if (child.type !== 'hidden' && child.type !== 'text') {
88
+ errors.push(...child.lint(cur));
89
+ }
90
+ cur += str.length + 1;
91
+ }
92
+ return errors;
93
+ }
94
+
95
+ /** @override */
96
+ cloneNode() {
97
+ const cloned = this.cloneChildNodes();
98
+ return Parser.run(() => {
99
+ const token = new GalleryToken(undefined, this.getAttribute('config'));
100
+ token.append(...cloned);
101
+ return token;
102
+ });
103
+ }
104
+
69
105
  /** @override */
70
106
  text() {
71
107
  return super.text('\n').replaceAll(/\n\s*\n/gu, '\n');
@@ -93,28 +129,16 @@ class GalleryToken extends Token {
93
129
 
94
130
  /**
95
131
  * @override
96
- * @param {number} start 起始位置
132
+ * @template {string|Token} T
133
+ * @param {T} token 待插入的节点
134
+ * @param {number} i 插入位置
135
+ * @throws `RangeError` 插入不可见内容
97
136
  */
98
- lint(start = 0) {
99
- const {top, left} = this.getRootNode().posFromIndex(start),
100
- /** @type {LintError[]} */ errors = [];
101
- for (let i = 0, cur = start; i < this.childNodes.length; i++) {
102
- const child = this.childNodes[i],
103
- str = String(child);
104
- if (child.type === 'hidden' && str.trim() && !/^<!--.*-->$/u.test(str)) {
105
- errors.push({
106
- message: '图库中的无效内容',
107
- startLine: top + i,
108
- endLine: top + i,
109
- startCol: i ? 0 : left,
110
- endCol: i ? str.length : left + str.length,
111
- });
112
- } else if (child.type !== 'hidden' && child.type !== 'text') {
113
- errors.push(...child.lint(cur));
114
- }
115
- cur += str.length + 1;
137
+ insertAt(token, i = 0) {
138
+ if (typeof token === 'string' && token.trim() || token instanceof HiddenToken) {
139
+ throw new RangeError('请勿向图库中插入不可见内容!');
116
140
  }
117
- return errors;
141
+ return super.insertAt(token, i);
118
142
  }
119
143
  }
120
144
 
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ const Parser = require('../..'),
4
+ Token = require('..');
5
+
6
+ /**
7
+ * `<pre>`
8
+ * @classdesc `{childNodes: [...AstText|NoincludeToken]}`
9
+ */
10
+ class HasNowikiToken extends Token {
11
+ /**
12
+ * @param {string} wikitext wikitext
13
+ * @param {string} type type
14
+ * @param {accum} accum
15
+ */
16
+ constructor(wikitext, type, config = Parser.getConfig(), accum = []) {
17
+ const NoincludeToken = require('../nowiki/noinclude');
18
+ wikitext = wikitext.replaceAll(
19
+ /(<nowiki>)(.*?)(<\/nowiki>)/giu,
20
+ /** @type {function(...string): string} */ (_, opening, inner, closing) => {
21
+ new NoincludeToken(opening, config, accum);
22
+ new NoincludeToken(closing, config, accum);
23
+ return `\0${accum.length - 1}c\x7F${inner}\0${accum.length}c\x7F`;
24
+ },
25
+ );
26
+ super(wikitext, config, true, accum, {AstText: ':', NoincludeToken: ':'});
27
+ this.type = type;
28
+ }
29
+
30
+ /** @override */
31
+ cloneNode() {
32
+ const cloned = this.cloneChildNodes();
33
+ return Parser.run(() => {
34
+ const token = new HasNowikiToken(undefined, this.type, this.getAttribute('config'));
35
+ token.append(...cloned);
36
+ return token;
37
+ });
38
+ }
39
+ }
40
+
41
+ Parser.classes.HasNowikiToken = __filename;
42
+ module.exports = HasNowikiToken;
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ const Parser = require('../..'),
4
+ HasNowikiToken = require('.');
5
+
6
+ /**
7
+ * `<pre>`
8
+ * @classdesc `{childNodes: [...AstText|NoincludeToken|ConverterToken]}`
9
+ */
10
+ class PreToken extends HasNowikiToken {
11
+ name = 'pre';
12
+
13
+ /**
14
+ * @param {string} wikitext wikitext
15
+ * @param {accum} accum
16
+ */
17
+ constructor(wikitext, config = Parser.getConfig(), accum = []) {
18
+ super(wikitext, 'ext-inner', config, accum);
19
+ this.setAttribute('stage', Parser.MAX_STAGE - 1);
20
+ this.setAttribute('acceptable', {AstText: ':', NoincludeToken: ':', ConverterToken: ':'});
21
+ }
22
+
23
+ /** @override */
24
+ isPlain() {
25
+ return true;
26
+ }
27
+
28
+ /** @override */
29
+ cloneNode() {
30
+ const cloned = this.cloneChildNodes();
31
+ return Parser.run(() => {
32
+ const token = new PreToken(undefined, this.getAttribute('config'));
33
+ token.append(...cloned);
34
+ return token;
35
+ });
36
+ }
37
+ }
38
+
39
+ Parser.classes.PreToken = __filename;
40
+ module.exports = PreToken;
package/src/heading.js CHANGED
@@ -28,7 +28,8 @@ class HeadingToken extends fixedToken(sol(Token)) {
28
28
  this.setAttribute('name', String(level));
29
29
  const token = new Token(input[0], config, true, accum);
30
30
  token.type = 'heading-title';
31
- token.setAttribute('name', this.name).setAttribute('stage', 2);
31
+ token.setAttribute('name', this.name);
32
+ token.setAttribute('stage', 2);
32
33
  const SyntaxToken = require('./syntax');
33
34
  const trail = new SyntaxToken(input[1], /^[^\S\n]*$/u, 'heading-trail', config, accum, {
34
35
  'Stage-1': ':', '!ExtToken': '',
@@ -36,19 +37,11 @@ class HeadingToken extends fixedToken(sol(Token)) {
36
37
  this.append(token, trail);
37
38
  }
38
39
 
39
- /** @override */
40
- cloneNode() {
41
- const [title, trail] = this.cloneChildNodes(),
42
- token = Parser.run(() => new HeadingToken(Number(this.name), [], this.getAttribute('config')));
43
- token.firsthild.safeReplaceWith(title);
44
- token.lastChild.safeReplaceWith(trail);
45
- return token;
46
- }
47
-
48
40
  /**
49
41
  * @override
50
42
  * @this {{prependNewLine(): ''|'\n', appendNewLine(): ''|'\n'} & HeadingToken}
51
43
  * @param {string} selector
44
+ * @returns {string}
52
45
  */
53
46
  toString(selector) {
54
47
  const equals = '='.repeat(Number(this.name));
@@ -87,6 +80,17 @@ class HeadingToken extends fixedToken(sol(Token)) {
87
80
  return errors;
88
81
  }
89
82
 
83
+ /** @override */
84
+ cloneNode() {
85
+ const [title, trail] = this.cloneChildNodes();
86
+ return Parser.run(() => {
87
+ const token = new HeadingToken(Number(this.name), [], this.getAttribute('config'));
88
+ token.firsthild.safeReplaceWith(title);
89
+ token.lastChild.safeReplaceWith(trail);
90
+ return token;
91
+ });
92
+ }
93
+
90
94
  /**
91
95
  * @override
92
96
  * @this {HeadingToken & {prependNewLine(): ''|'\n', appendNewLine(): ''|'\n'}}
@@ -102,7 +106,7 @@ class HeadingToken extends fixedToken(sol(Token)) {
102
106
  * @param {number} n 标题层级
103
107
  */
104
108
  setLevel(n) {
105
- if (typeof n !== 'number') {
109
+ if (!Number.isInteger(n)) {
106
110
  this.typeError('setLevel', 'Number');
107
111
  }
108
112
  n = Math.min(Math.max(n, 1), 6);
package/src/html.js CHANGED
@@ -28,11 +28,11 @@ class HtmlToken extends attributeParent(fixedToken(Token)) {
28
28
  this.#closing = false;
29
29
  return;
30
30
  } else if (this.#selfClosing) {
31
- throw new Error(`这是一个自闭合标签!`);
31
+ throw new Error('这是一个自闭合标签!');
32
32
  }
33
33
  const {html: [,, tags]} = this.getAttribute('config');
34
34
  if (tags.includes(this.name)) {
35
- throw new Error(`这是一个空标签!`);
35
+ throw new Error('这是一个空标签!');
36
36
  }
37
37
  this.#closing = true;
38
38
  }
@@ -66,30 +66,13 @@ class HtmlToken extends attributeParent(fixedToken(Token)) {
66
66
  */
67
67
  constructor(name, attr, closing, selfClosing, config = Parser.getConfig(), accum = []) {
68
68
  super(undefined, config, true, accum);
69
- this.appendChild(attr);
69
+ this.insertAt(attr);
70
70
  this.setAttribute('name', name.toLowerCase());
71
71
  this.#closing = closing;
72
72
  this.#selfClosing = selfClosing;
73
73
  this.#tag = name;
74
74
  }
75
75
 
76
- /** @override */
77
- cloneNode() {
78
- const [attr] = this.cloneChildNodes(),
79
- config = this.getAttribute('config');
80
- return Parser.run(() => new HtmlToken(this.#tag, attr, this.#closing, this.#selfClosing, config));
81
- }
82
-
83
- /**
84
- * @override
85
- * @template {string} T
86
- * @param {T} key 属性键
87
- * @returns {TokenAttribute<T>}
88
- */
89
- getAttribute(key) {
90
- return key === 'tag' ? this.#tag : super.getAttribute(key);
91
- }
92
-
93
76
  /**
94
77
  * @override
95
78
  * @param {string} selector
@@ -133,24 +116,6 @@ class HtmlToken extends attributeParent(fixedToken(Token)) {
133
116
  return errors;
134
117
  }
135
118
 
136
- /** @override */
137
- text() {
138
- return `<${this.#closing ? '/' : ''}${this.#tag}${super.text()}${this.#selfClosing ? '/' : ''}>`;
139
- }
140
-
141
- /**
142
- * 更换标签名
143
- * @param {string} tag 标签名
144
- * @throws `RangeError` 非法的HTML标签
145
- */
146
- replaceTag(tag) {
147
- const name = tag.toLowerCase();
148
- if (!this.getAttribute('config').html.flat().includes(name)) {
149
- throw new RangeError(`非法的HTML标签:${tag}`);
150
- }
151
- this.setAttribute('name', name).#tag = tag;
152
- }
153
-
154
119
  /**
155
120
  * 搜索匹配的标签
156
121
  * @complexity `n`
@@ -190,6 +155,41 @@ class HtmlToken extends attributeParent(fixedToken(Token)) {
190
155
  throw new SyntaxError(`未${this.#closing ? '匹配的闭合' : '闭合的'}标签:${string}`);
191
156
  }
192
157
 
158
+ /** @override */
159
+ cloneNode() {
160
+ const [attr] = this.cloneChildNodes(),
161
+ config = this.getAttribute('config');
162
+ return Parser.run(() => new HtmlToken(this.#tag, attr, this.#closing, this.#selfClosing, config));
163
+ }
164
+
165
+ /**
166
+ * @override
167
+ * @template {string} T
168
+ * @param {T} key 属性键
169
+ * @returns {TokenAttribute<T>}
170
+ */
171
+ getAttribute(key) {
172
+ return key === 'tag' ? this.#tag : super.getAttribute(key);
173
+ }
174
+
175
+ /** @override */
176
+ text() {
177
+ return `<${this.#closing ? '/' : ''}${this.#tag}${super.text()}${this.#selfClosing ? '/' : ''}>`;
178
+ }
179
+
180
+ /**
181
+ * 更换标签名
182
+ * @param {string} tag 标签名
183
+ * @throws `RangeError` 非法的HTML标签
184
+ */
185
+ replaceTag(tag) {
186
+ const name = tag.toLowerCase();
187
+ if (!this.getAttribute('config').html.flat().includes(name)) {
188
+ throw new RangeError(`非法的HTML标签:${tag}`);
189
+ }
190
+ this.setAttribute('name', name).#tag = tag;
191
+ }
192
+
193
193
  /** 局部闭合 */
194
194
  #localMatch() {
195
195
  this.#selfClosing = false;
@@ -21,30 +21,36 @@ class ImageParameterToken extends Token {
21
21
  */
22
22
  static #validate(key, value, config = Parser.getConfig(), halfParsed = false) {
23
23
  value = value.replaceAll(/\0\d+t\x7F/gu, '').trim();
24
- if (key === 'width') {
25
- return /^\d*(?:x\d*)?$/u.test(value);
26
- } else if (['alt', 'class', 'manualthumb', 'frameless', 'framed', 'thumbnail'].includes(key)) {
27
- return true;
28
- } else if (key === 'link') {
29
- if (!value) {
30
- return this.noLink;
31
- }
32
- const regex = new RegExp(`(?:${config.protocol}|//)${extUrlChar}(?=\0\\d+t\x7F|$)`, 'iu');
33
- if (regex.test(value)) {
34
- return value;
35
- }
36
- if (value.startsWith('[[') && value.endsWith(']]')) {
37
- value = value.slice(2, -2);
38
- }
39
- if (value.includes('%')) {
40
- try {
41
- value = decodeURIComponent(value);
42
- } catch {}
24
+ switch (key) {
25
+ case 'width':
26
+ return /^\d*(?:x\d*)?$/u.test(value);
27
+ case 'link': {
28
+ if (!value) {
29
+ return this.noLink;
30
+ }
31
+ const regex = new RegExp(`(?:${config.protocol}|//)${extUrlChar}(?=\0\\d+t\x7F|$)`, 'iu');
32
+ if (regex.test(value)) {
33
+ return value;
34
+ } else if (value.startsWith('[[') && value.endsWith(']]')) {
35
+ value = value.slice(2, -2);
36
+ }
37
+ if (value.includes('%')) {
38
+ try {
39
+ value = decodeURIComponent(value);
40
+ } catch {}
41
+ }
42
+ const title = Parser.normalizeTitle(value, 0, false, config, halfParsed);
43
+ return title.valid && String(title);
43
44
  }
44
- const title = Parser.normalizeTitle(value, 0, false, config, halfParsed);
45
- return title.valid && String(title);
45
+ case 'lang':
46
+ return config.variants.includes(value);
47
+ case 'alt':
48
+ case 'class':
49
+ case 'manualthumb':
50
+ return true;
51
+ default:
52
+ return !isNaN(value);
46
53
  }
47
- return !isNaN(value);
48
54
  }
49
55
 
50
56
  type = 'image-parameter';
@@ -146,35 +152,11 @@ class ImageParameterToken extends Token {
146
152
  this.setAttribute('name', 'caption').setAttribute('stage', 7);
147
153
  }
148
154
 
149
- /** @override */
150
- cloneNode() {
151
- const cloned = this.cloneChildNodes(),
152
- config = this.getAttribute('config'),
153
- token = Parser.run(() => new ImageParameterToken(this.#syntax.replace('$1', ''), config));
154
- token.replaceChildren(...cloned);
155
- return token;
156
- }
157
-
158
- /**
159
- * @override
160
- * @template {string} T
161
- * @param {T} key 属性键
162
- * @returns {TokenAttribute<T>}
163
- */
164
- getAttribute(key) {
165
- return key === 'syntax' ? this.#syntax : super.getAttribute(key);
166
- }
167
-
168
155
  /** @override */
169
156
  isPlain() {
170
157
  return true;
171
158
  }
172
159
 
173
- /** 是否是不可变参数 */
174
- #isVoid() {
175
- return this.#syntax && !this.#syntax.includes('$1');
176
- }
177
-
178
160
  /**
179
161
  * @override
180
162
  * @param {string} selector
@@ -197,6 +179,40 @@ class ImageParameterToken extends Token {
197
179
  : super.print({class: 'image-caption'});
198
180
  }
199
181
 
182
+ /** @override */
183
+ cloneNode() {
184
+ const cloned = this.cloneChildNodes(),
185
+ config = this.getAttribute('config');
186
+ return Parser.run(() => {
187
+ const token = new ImageParameterToken(this.#syntax.replace('$1', ''), config);
188
+ token.replaceChildren(...cloned);
189
+ return token;
190
+ });
191
+ }
192
+
193
+ /**
194
+ * @override
195
+ * @template {string} T
196
+ * @param {T} key 属性键
197
+ * @returns {TokenAttribute<T>}
198
+ */
199
+ getAttribute(key) {
200
+ return key === 'syntax' ? this.#syntax : super.getAttribute(key);
201
+ }
202
+
203
+ /**
204
+ * @override
205
+ * @param {PropertyKey} key 属性键
206
+ */
207
+ hasAttribute(key) {
208
+ return key === 'syntax' || super.hasAttribute(key);
209
+ }
210
+
211
+ /** 是否是不可变参数 */
212
+ #isVoid() {
213
+ return this.#syntax && !this.#syntax.includes('$1');
214
+ }
215
+
200
216
  /** @override */
201
217
  text() {
202
218
  return this.#syntax ? this.#syntax.replace('$1', super.text()).trim() : super.text().trim();
@@ -245,8 +261,8 @@ class ImageParameterToken extends Token {
245
261
  const root = Parser.parse(`[[File:F|${
246
262
  this.#syntax ? this.#syntax.replace('$1', value) : value
247
263
  }]]`, this.getAttribute('include'), 6, this.getAttribute('config')),
248
- {childNodes: {length}, firstChild: file} = root,
249
- {lastChild: imageParameter, type, name, childNodes: {length: fileLength}} = file;
264
+ {length, firstChild: file} = root,
265
+ {lastChild: imageParameter, type, name, length: fileLength} = file;
250
266
  if (length !== 1 || type !== 'file' || name !== 'File:F' || fileLength !== 2
251
267
  || imageParameter.name !== this.name
252
268
  ) {