wikiparser-node 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/.eslintrc.json +446 -51
  2. package/README.md +1 -1
  3. package/config/default.json +13 -17
  4. package/config/llwiki.json +11 -79
  5. package/config/moegirl.json +7 -1
  6. package/config/zhwiki.json +1269 -0
  7. package/index.js +106 -96
  8. package/lib/element.js +448 -440
  9. package/lib/node.js +335 -115
  10. package/lib/ranges.js +27 -18
  11. package/lib/text.js +146 -0
  12. package/lib/title.js +13 -5
  13. package/mixin/attributeParent.js +70 -24
  14. package/mixin/fixedToken.js +14 -6
  15. package/mixin/hidden.js +6 -4
  16. package/mixin/sol.js +27 -10
  17. package/package.json +7 -4
  18. package/parser/brackets.js +18 -18
  19. package/parser/commentAndExt.js +15 -13
  20. package/parser/converter.js +14 -13
  21. package/parser/externalLinks.js +12 -11
  22. package/parser/hrAndDoubleUnderscore.js +23 -14
  23. package/parser/html.js +8 -7
  24. package/parser/links.js +13 -12
  25. package/parser/list.js +12 -11
  26. package/parser/magicLinks.js +11 -10
  27. package/parser/quotes.js +6 -5
  28. package/parser/selector.js +175 -0
  29. package/parser/table.js +24 -17
  30. package/printed/example.json +120 -0
  31. package/src/arg.js +56 -32
  32. package/src/atom/hidden.js +5 -2
  33. package/src/atom/index.js +17 -9
  34. package/src/attribute.js +180 -98
  35. package/src/converter.js +68 -41
  36. package/src/converterFlags.js +63 -41
  37. package/src/converterRule.js +117 -65
  38. package/src/extLink.js +66 -18
  39. package/src/gallery.js +28 -16
  40. package/src/heading.js +33 -14
  41. package/src/html.js +97 -35
  42. package/src/imageParameter.js +82 -53
  43. package/src/index.js +296 -175
  44. package/src/link/category.js +20 -52
  45. package/src/link/file.js +59 -28
  46. package/src/link/galleryImage.js +21 -7
  47. package/src/link/index.js +143 -57
  48. package/src/magicLink.js +33 -11
  49. package/src/nowiki/comment.js +22 -10
  50. package/src/nowiki/dd.js +37 -22
  51. package/src/nowiki/doubleUnderscore.js +16 -7
  52. package/src/nowiki/hr.js +11 -7
  53. package/src/nowiki/index.js +16 -9
  54. package/src/nowiki/list.js +2 -2
  55. package/src/nowiki/noinclude.js +8 -4
  56. package/src/nowiki/quote.js +11 -7
  57. package/src/onlyinclude.js +19 -7
  58. package/src/parameter.js +65 -38
  59. package/src/syntax.js +23 -20
  60. package/src/table/index.js +260 -165
  61. package/src/table/td.js +97 -52
  62. package/src/table/tr.js +102 -58
  63. package/src/tagPair/ext.js +27 -19
  64. package/src/tagPair/include.js +16 -11
  65. package/src/tagPair/index.js +64 -29
  66. package/src/transclude.js +167 -92
  67. package/test/api.js +83 -0
  68. package/test/real.js +133 -0
  69. package/test/test.js +28 -0
  70. package/test/util.js +80 -0
  71. package/tool/index.js +41 -31
  72. package/typings/api.d.ts +13 -0
  73. package/typings/array.d.ts +28 -0
  74. package/typings/event.d.ts +24 -0
  75. package/typings/index.d.ts +46 -4
  76. package/typings/node.d.ts +15 -9
  77. package/typings/parser.d.ts +7 -0
  78. package/typings/tool.d.ts +3 -2
  79. package/util/debug.js +21 -18
  80. package/util/string.js +38 -25
  81. package/typings/element.d.ts +0 -28
package/src/gallery.js CHANGED
@@ -1,28 +1,28 @@
1
1
  'use strict';
2
2
 
3
- const {text} = require('../util/string'),
4
- /** @type {Parser} */ Parser = require('..'),
3
+ const Parser = require('..'),
5
4
  Token = require('.'),
6
- GalleryImageToken = require('./link/galleryImage');
5
+ GalleryImageToken = require('./link/galleryImage'),
6
+ HiddenToken = require('./atom/hidden');
7
7
 
8
8
  /**
9
9
  * gallery标签
10
- * @classdesc `{childNodes: (string|FileToken)[]]}`
10
+ * @classdesc `{childNodes: ...(GalleryImageToken|HiddenToken|AstText)}`
11
11
  */
12
12
  class GalleryToken extends Token {
13
13
  type = 'ext-inner';
14
14
  name = 'gallery';
15
15
 
16
16
  /**
17
- * @param {string} inner
17
+ * @param {string} inner 标签内部wikitext
18
18
  * @param {accum} accum
19
19
  */
20
20
  constructor(inner, config = Parser.getConfig(), accum = []) {
21
- super(undefined, config, true, accum, {String: ':', GalleryImageToken: ':'});
21
+ super(undefined, config, true, accum, {AstText: ':', GalleryImageToken: ':'});
22
22
  for (const line of inner?.split('\n') ?? []) {
23
- const matches = /^([^|]+)(?:\|(.*))?/.exec(line);
23
+ const matches = /^([^|]+)(?:\|(.*))?/u.exec(line);
24
24
  if (!matches) {
25
- this.appendChild(line);
25
+ this.appendChild(line.trim() ? new HiddenToken(line, undefined, config, [], {AstText: ':'}) : line);
26
26
  continue;
27
27
  }
28
28
  const [, file, alt] = matches;
@@ -32,34 +32,46 @@ class GalleryToken extends Token {
32
32
  } catch {
33
33
  title = this.normalizeTitle(file, 6, true);
34
34
  }
35
- if (!title.valid) {
36
- this.appendChild(line);
37
- } else {
35
+ if (title.valid) {
38
36
  this.appendChild(new GalleryImageToken(file, alt, title, config, accum));
37
+ } else {
38
+ this.appendChild(new HiddenToken(line, undefined, config, [], {AstText: ':'}));
39
39
  }
40
40
  }
41
41
  }
42
42
 
43
+ /** @override */
43
44
  cloneNode() {
44
- const cloned = this.cloneChildren(),
45
+ const cloned = this.cloneChildNodes(),
45
46
  token = Parser.run(() => new GalleryToken(undefined, this.getAttribute('config')));
46
47
  token.append(...cloned);
47
48
  return token;
48
49
  }
49
50
 
50
- toString() {
51
- return super.toString('\n');
51
+ /**
52
+ * @override
53
+ * @param {string} selector
54
+ */
55
+ toString(selector) {
56
+ return super.toString(selector, '\n');
52
57
  }
53
58
 
59
+ /** @override */
54
60
  getGaps() {
55
61
  return 1;
56
62
  }
57
63
 
64
+ /** @override */
58
65
  text() {
59
- return text(this.children, '\n');
66
+ return super.text('\n').replaceAll(/\n\s*\n/gu, '\n');
60
67
  }
61
68
 
62
- /** @param {string} file */
69
+ /**
70
+ * 插入图片
71
+ * @param {string} file 图片文件名
72
+ * @param {number} i 插入位置
73
+ * @throws `SyntaxError` 非法的文件名
74
+ */
63
75
  insertImage(file, i = this.childNodes.length) {
64
76
  let title;
65
77
  try {
package/src/heading.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fixedToken = require('../mixin/fixedToken'),
4
4
  sol = require('../mixin/sol'),
5
- /** @type {Parser} */ Parser = require('..'),
5
+ Parser = require('..'),
6
6
  Token = require('.');
7
7
 
8
8
  /**
@@ -12,9 +12,14 @@ const fixedToken = require('../mixin/fixedToken'),
12
12
  class HeadingToken extends fixedToken(sol(Token)) {
13
13
  type = 'heading';
14
14
 
15
+ /** 内部wikitext */
16
+ get innerText() {
17
+ return this.firstElementChild.text();
18
+ }
19
+
15
20
  /**
16
- * @param {number} level
17
- * @param {string[]} input
21
+ * @param {number} level 标题层级
22
+ * @param {string[]} input 标题文字
18
23
  * @param {accum} accum
19
24
  */
20
25
  constructor(level, input, config = Parser.getConfig(), accum = []) {
@@ -23,38 +28,48 @@ class HeadingToken extends fixedToken(sol(Token)) {
23
28
  const token = new Token(input[0], config, true, accum);
24
29
  token.type = 'heading-title';
25
30
  token.setAttribute('name', this.name).setAttribute('stage', 2);
26
- const SyntaxToken = require('./syntax'),
27
- trail = new SyntaxToken(input[1], /^[^\S\n]*$/, 'heading-trail', config, accum, {
28
- 'Stage-1': ':', '!ExtToken': '',
29
- });
31
+ const SyntaxToken = require('./syntax');
32
+ const trail = new SyntaxToken(input[1], /^[^\S\n]*$/u, 'heading-trail', config, accum, {
33
+ 'Stage-1': ':', '!ExtToken': '',
34
+ });
30
35
  this.append(token, trail);
31
36
  }
32
37
 
38
+ /** @override */
33
39
  cloneNode() {
34
- const [title, trail] = this.cloneChildren(),
40
+ const [title, trail] = this.cloneChildNodes(),
35
41
  token = Parser.run(() => new HeadingToken(Number(this.name), [], this.getAttribute('config')));
36
42
  token.firstElementChild.safeReplaceWith(title);
37
43
  token.lastElementChild.safeReplaceWith(trail);
38
44
  return token;
39
45
  }
40
46
 
41
- /** @this {HeadingToken & {prependNewLine(): ''|'\n', appendNewLine(): ''|'\n'}} */
42
- toString() {
47
+ /**
48
+ * @override
49
+ * @this {{prependNewLine(): ''|'\n', appendNewLine(): ''|'\n'} & HeadingToken}
50
+ * @param {string} selector
51
+ */
52
+ toString(selector) {
43
53
  const equals = '='.repeat(Number(this.name));
44
- return `${this.prependNewLine()}${equals}${
45
- this.firstElementChild.toString()
46
- }${equals}${this.lastElementChild.toString()}${this.appendNewLine()}`;
54
+ return selector && this.matches(selector)
55
+ ? ''
56
+ : `${this.prependNewLine()}${equals}${
57
+ this.firstElementChild.toString(selector)
58
+ }${equals}${this.lastElementChild.toString(selector)}${this.appendNewLine()}`;
47
59
  }
48
60
 
61
+ /** @override */
49
62
  getPadding() {
50
63
  return super.getPadding() + Number(this.name);
51
64
  }
52
65
 
66
+ /** @override */
53
67
  getGaps() {
54
68
  return Number(this.name);
55
69
  }
56
70
 
57
71
  /**
72
+ * @override
58
73
  * @this {HeadingToken & {prependNewLine(): ''|'\n', appendNewLine(): ''|'\n'}}
59
74
  * @returns {string}
60
75
  */
@@ -63,7 +78,10 @@ class HeadingToken extends fixedToken(sol(Token)) {
63
78
  return `${this.prependNewLine()}${equals}${this.firstElementChild.text()}${equals}${this.appendNewLine()}`;
64
79
  }
65
80
 
66
- /** @param {number} n */
81
+ /**
82
+ * 设置标题层级
83
+ * @param {number} n 标题层级
84
+ */
67
85
  setLevel(n) {
68
86
  if (typeof n !== 'number') {
69
87
  this.typeError('setLevel', 'Number');
@@ -72,6 +90,7 @@ class HeadingToken extends fixedToken(sol(Token)) {
72
90
  this.setAttribute('name', String(n)).firstElementChild.setAttribute('name', this.name);
73
91
  }
74
92
 
93
+ /** 移除标题后的不可见内容 */
75
94
  removeTrail() {
76
95
  this.lastElementChild.replaceChildren();
77
96
  }
package/src/html.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const {noWrap} = require('../util/string'),
4
4
  fixedToken = require('../mixin/fixedToken'),
5
5
  attributeParent = require('../mixin/attributeParent'),
6
- /** @type {Parser} */ Parser = require('..'),
6
+ Parser = require('..'),
7
7
  Token = require('.');
8
8
 
9
9
  /**
@@ -12,57 +12,108 @@ const {noWrap} = require('../util/string'),
12
12
  */
13
13
  class HtmlToken extends attributeParent(fixedToken(Token)) {
14
14
  type = 'html';
15
- closing;
16
- selfClosing;
15
+ #closing;
16
+ #selfClosing;
17
17
  #tag;
18
18
 
19
+ /** getter */
20
+ get closing() {
21
+ return this.#closing;
22
+ }
23
+
24
+ /** @throws `Error` 自闭合标签或空标签 */
25
+ set closing(value) {
26
+ if (!value) {
27
+ this.#closing = false;
28
+ return;
29
+ } else if (this.#selfClosing) {
30
+ throw new Error(`这是一个自闭合标签!`);
31
+ }
32
+ const {html: [,, tags]} = this.getAttribute('config');
33
+ if (tags.includes(this.name)) {
34
+ throw new Error(`这是一个空标签!`);
35
+ }
36
+ this.#closing = true;
37
+ }
38
+
39
+ /** getter */
40
+ get selfClosing() {
41
+ return this.#selfClosing;
42
+ }
43
+
44
+ /** @throws `Error` 闭合标签或无效自闭合标签 */
45
+ set selfClosing(value) {
46
+ if (!value) {
47
+ this.#selfClosing = false;
48
+ return;
49
+ } else if (this.#closing) {
50
+ throw new Error('这是一个闭合标签!');
51
+ }
52
+ const {html: [tags]} = this.getAttribute('config');
53
+ if (tags.includes(this.name)) {
54
+ throw new Error(`<${this.name}>标签自闭合无效!`);
55
+ }
56
+ this.#selfClosing = true;
57
+ }
58
+
19
59
  /**
20
- * @param {string} name
21
- * @param {AttributeToken} attr
22
- * @param {boolean} closing
23
- * @param {boolean} selfClosing
60
+ * @param {string} name 标签名
61
+ * @param {AttributeToken} attr 标签属性
62
+ * @param {boolean} closing 是否闭合
63
+ * @param {boolean} selfClosing 是否自封闭
24
64
  * @param {accum} accum
25
65
  */
26
66
  constructor(name, attr, closing, selfClosing, config = Parser.getConfig(), accum = []) {
27
67
  super(undefined, config, true, accum);
28
68
  this.appendChild(attr);
29
69
  this.setAttribute('name', name.toLowerCase());
30
- this.closing = closing;
31
- this.selfClosing = selfClosing;
70
+ this.#closing = closing;
71
+ this.#selfClosing = selfClosing;
32
72
  this.#tag = name;
33
73
  }
34
74
 
75
+ /** @override */
35
76
  cloneNode() {
36
- const [attr] = this.cloneChildren(),
77
+ const [attr] = this.cloneChildNodes(),
37
78
  config = this.getAttribute('config');
38
- return Parser.run(() => new HtmlToken(this.#tag, attr, this.closing, this.selfClosing, config));
79
+ return Parser.run(() => new HtmlToken(this.#tag, attr, this.#closing, this.#selfClosing, config));
39
80
  }
40
81
 
41
82
  /**
83
+ * @override
42
84
  * @template {string} T
43
- * @param {T} key
85
+ * @param {T} key 属性键
44
86
  * @returns {TokenAttribute<T>}
45
87
  */
46
88
  getAttribute(key) {
47
- if (key === 'tag') {
48
- return this.#tag;
49
- }
50
- return super.getAttribute(key);
89
+ return key === 'tag' ? this.#tag : super.getAttribute(key);
51
90
  }
52
91
 
53
- toString() {
54
- return `<${this.closing ? '/' : ''}${this.#tag}${super.toString()}${this.selfClosing ? '/' : ''}>`;
92
+ /**
93
+ * @override
94
+ * @param {string} selector
95
+ */
96
+ toString(selector) {
97
+ return selector && this.matches(selector)
98
+ ? ''
99
+ : `<${this.#closing ? '/' : ''}${this.#tag}${super.toString(selector)}${this.#selfClosing ? '/' : ''}>`;
55
100
  }
56
101
 
102
+ /** @override */
57
103
  getPadding() {
58
- return this.#tag.length + (this.closing ? 2 : 1);
104
+ return this.#tag.length + (this.#closing ? 2 : 1);
59
105
  }
60
106
 
107
+ /** @override */
61
108
  text() {
62
- return `<${this.closing ? '/' : ''}${this.#tag}${super.text()}${this.selfClosing ? '/' : ''}>`;
109
+ return `<${this.#closing ? '/' : ''}${this.#tag}${super.text()}${this.#selfClosing ? '/' : ''}>`;
63
110
  }
64
111
 
65
- /** @param {string} tag */
112
+ /**
113
+ * 更换标签名
114
+ * @param {string} tag 标签名
115
+ * @throws `RangeError` 非法的HTML标签
116
+ */
66
117
  replaceTag(tag) {
67
118
  const name = tag.toLowerCase();
68
119
  if (!this.getAttribute('config').html.flat().includes(name)) {
@@ -71,21 +122,27 @@ class HtmlToken extends attributeParent(fixedToken(Token)) {
71
122
  this.setAttribute('name', name).#tag = tag;
72
123
  }
73
124
 
74
- /** @complexity `n` */
125
+ /**
126
+ * 搜索匹配的标签
127
+ * @complexity `n`
128
+ * @throws `SyntaxError` 同时闭合和自封闭的标签
129
+ * @throws `SyntaxError` 无效自封闭标签
130
+ * @throws `SyntaxError` 未闭合的标签
131
+ */
75
132
  findMatchingTag() {
76
133
  const {html} = this.getAttribute('config'),
77
- {name, parentElement, closing, selfClosing} = this,
78
- string = noWrap(this.toString());
134
+ {name, parentNode, closing, selfClosing} = this,
135
+ string = noWrap(String(this));
79
136
  if (closing && selfClosing) {
80
137
  throw new SyntaxError(`同时闭合和自封闭的标签:${string}`);
81
138
  } else if (html[2].includes(name) || selfClosing && html[1].includes(name)) { // 自封闭标签
82
139
  return this;
83
140
  } else if (selfClosing && html[0].includes(name)) {
84
141
  throw new SyntaxError(`无效自封闭标签:${string}`);
85
- } else if (!parentElement) {
86
- return;
142
+ } else if (!parentNode) {
143
+ return undefined;
87
144
  }
88
- const {children} = parentElement,
145
+ const {children} = parentNode,
89
146
  i = children.indexOf(this),
90
147
  selector = `html#${name}`,
91
148
  siblings = closing
@@ -105,32 +162,37 @@ class HtmlToken extends attributeParent(fixedToken(Token)) {
105
162
  throw new SyntaxError(`未${closing ? '匹配的闭合' : '闭合的'}标签:${string}`);
106
163
  }
107
164
 
165
+ /** 局部闭合 */
108
166
  #localMatch() {
109
- this.selfClosing = false;
167
+ this.#selfClosing = false;
110
168
  const root = Parser.parse(`</${this.name}>`, false, 3, this.getAttribute('config'));
111
169
  this.after(root.firstChild);
112
170
  }
113
171
 
114
- /** @complexity `n` */
172
+ /**
173
+ * 修复无效自封闭标签
174
+ * @complexity `n`
175
+ * @throws `Error` 无法修复无效自封闭标签
176
+ */
115
177
  fix() {
116
178
  const config = this.getAttribute('config'),
117
- {parentElement, selfClosing, name, firstElementChild} = this;
118
- if (!parentElement || !selfClosing || !config.html[0].includes(name)) {
179
+ {parentNode, name, firstElementChild} = this;
180
+ if (!parentNode || !this.#selfClosing || !config.html[0].includes(name)) {
119
181
  return;
120
182
  } else if (firstElementChild.text().trim()) {
121
183
  this.#localMatch();
122
184
  return;
123
185
  }
124
- const {children} = parentElement,
186
+ const {children} = parentNode,
125
187
  i = children.indexOf(this),
126
188
  /** @type {HtmlToken[]} */
127
189
  prevSiblings = children.slice(0, i).filter(child => child.matches(`html#${name}`)),
128
190
  imbalance = prevSiblings.reduce((acc, {closing}) => acc + (closing ? 1 : -1), 0);
129
191
  if (imbalance < 0) {
130
- this.selfClosing = false;
131
- this.closing = true;
192
+ this.#selfClosing = false;
193
+ this.#closing = true;
132
194
  } else {
133
- Parser.warn('无法修复无效自封闭标签', noWrap(this.toString()));
195
+ Parser.warn('无法修复无效自封闭标签', noWrap(String(this)));
134
196
  throw new Error(`无法修复无效自封闭标签:前文共有 ${imbalance} 个未匹配的闭合标签`);
135
197
  }
136
198
  }
@@ -1,41 +1,39 @@
1
1
  'use strict';
2
2
 
3
3
  const {text, noWrap, extUrlChar} = require('../util/string'),
4
- Title = require('../lib/title'),
5
- /** @type {Parser} */ Parser = require('..'),
6
- Token = require('.');
4
+ Parser = require('..'),
5
+ Token = require('.'),
6
+ AstText = require('../lib/text');
7
7
 
8
8
  /**
9
9
  * 图片参数
10
- * @classdesc `{childNodes: ...(string|Token)}`
10
+ * @classdesc `{childNodes: ...(AstText|Token)}`
11
11
  */
12
12
  class ImageParameterToken extends Token {
13
- type = 'image-parameter';
14
- #syntax = '';
15
-
16
- static #noLink = Symbol('no-link');
13
+ static noLink = Symbol('no-link'); // 这个Symbol需要公开
17
14
 
18
15
  /**
16
+ * 检查图片参数是否合法
19
17
  * @template {string} T
20
- * @param {T} key
21
- * @param {string} value
18
+ * @param {T} key 参数名
19
+ * @param {string} value 参数值
22
20
  * @returns {T extends 'link' ? string|Symbol : boolean}
23
21
  */
24
- static #validate(key, value, config = Parser.getConfig()) {
25
- value = value.replace(/\0\d+t\x7f/g, '').trim();
22
+ static #validate(key, value, config = Parser.getConfig(), halfParsed = false) {
23
+ value = value.replaceAll(/\0\d+t\x7F/gu, '').trim();
26
24
  if (key === 'width') {
27
- return /^\d*(?:x\d*)?$/.test(value);
25
+ return /^\d*(?:x\d*)?$/u.test(value);
28
26
  } else if (['alt', 'class', 'manualthumb', 'frameless', 'framed', 'thumbnail'].includes(key)) {
29
27
  return true;
30
28
  } else if (key === 'link') {
31
29
  if (!value) {
32
- return this.#noLink;
30
+ return this.noLink;
33
31
  }
34
- const regex = RegExp(`(?:${config.protocol}|//)${extUrlChar}(?=\0\\d+t\x7f|$)`, 'iu');
32
+ const regex = new RegExp(`(?:${config.protocol}|//)${extUrlChar}(?=\0\\d+t\x7F|$)`, 'iu');
35
33
  if (regex.test(value)) {
36
34
  return value;
37
35
  }
38
- if (/^\[\[.+\]\]$/.test(value)) {
36
+ if (value.startsWith('[[') && value.endsWith(']]')) {
39
37
  value = value.slice(2, -2);
40
38
  }
41
39
  if (value.includes('%')) {
@@ -43,24 +41,39 @@ class ImageParameterToken extends Token {
43
41
  value = decodeURIComponent(value);
44
42
  } catch {}
45
43
  }
46
- const {title, fragment, valid} = new Title(value, 0, config);
47
- return valid && `${title}${fragment && '#'}${fragment}`;
44
+ const title = Parser.normalizeTitle(value, 0, false, config, halfParsed);
45
+ return title.valid && String(title);
48
46
  }
49
47
  return !isNaN(value);
50
48
  }
51
49
 
50
+ type = 'image-parameter';
51
+ #syntax = '';
52
+
53
+ /** getValue()的getter */
54
+ get value() {
55
+ return this.getValue();
56
+ }
57
+
58
+ set value(value) {
59
+ this.setValue(value);
60
+ }
61
+
62
+ /** 图片链接 */
52
63
  get link() {
53
- if (this.name === 'link') {
54
- return ImageParameterToken.#validate('link', this.getValue(), this.getAttribute('config'));
55
- }
56
- return undefined;
64
+ return this.name === 'link'
65
+ ? ImageParameterToken.#validate('link', this.getValue(), this.getAttribute('config'))
66
+ : undefined;
57
67
  }
68
+
58
69
  set link(value) {
59
70
  if (this.name === 'link') {
60
- value = value === ImageParameterToken.#noLink ? '' : value;
71
+ value = value === ImageParameterToken.noLink ? '' : value;
61
72
  this.setValue(value);
62
73
  }
63
74
  }
75
+
76
+ /** 图片大小 */
64
77
  get size() {
65
78
  if (this.name === 'width') {
66
79
  const /** @type {string} */ size = this.getValue().trim();
@@ -68,30 +81,37 @@ class ImageParameterToken extends Token {
68
81
  const [width, height = ''] = size.split('x');
69
82
  return {width, height};
70
83
  }
71
- const token = Parser.parse(size, false, 2, this.getAttribute('config')),
72
- {childNodes} = token,
73
- i = childNodes.findIndex(child => typeof child === 'string' && child.includes('x'));
84
+ const /** @type {{childNodes: AstText[]}} */ token = Parser.parse(size, false, 2, this.getAttribute('config')),
85
+ i = token.childNodes.findIndex(({type, data}) => type === 'text' && data.includes('x')),
86
+ str = token.childNodes[i];
74
87
  if (i === -1) {
75
88
  return {width: size, height: ''};
76
89
  }
77
- token.splitText(i, childNodes[i].indexOf('x'));
78
- token.splitText(i + 1, 1);
90
+ str.splitText(str.data.indexOf('x'));
91
+ str.nextSibling.splitText(1);
92
+ // eslint-disable-next-line unicorn/consistent-destructuring
79
93
  return {width: text(token.childNodes.slice(0, i + 1)), height: text(token.childNodes.slice(i + 2))};
80
94
  }
81
95
  return undefined;
82
96
  }
97
+
98
+ /** 图片宽度 */
83
99
  get width() {
84
100
  return this.size?.width;
85
101
  }
102
+
86
103
  set width(width) {
87
104
  if (this.name === 'width') {
88
105
  const {height} = this;
89
106
  this.setValue(`${String(width || '')}${height && 'x'}${height}`);
90
107
  }
91
108
  }
109
+
110
+ /** 图片高度 */
92
111
  get height() {
93
112
  return this.size?.height;
94
113
  }
114
+
95
115
  set height(height) {
96
116
  height = String(height || '');
97
117
  if (this.name === 'width') {
@@ -100,20 +120,18 @@ class ImageParameterToken extends Token {
100
120
  }
101
121
 
102
122
  /**
103
- * @param {string} str
123
+ * @param {string} str 图片参数
104
124
  * @param {accum} accum
105
125
  */
106
126
  constructor(str, config = Parser.getConfig(), accum = []) {
107
127
  const regexes = Object.entries(config.img).map(
108
128
  /** @returns {[string, string, RegExp]} */
109
- ([syntax, param]) => [syntax, param, RegExp(`^(\\s*)${syntax.replace('$1', '(.*)')}(\\s*)$`)],
129
+ ([syntax, param]) => [syntax, param, new RegExp(`^(\\s*)${syntax.replace('$1', '(.*)')}(\\s*)$`, 'u')],
110
130
  ),
111
131
  param = regexes.find(([,, regex]) => regex.test(str));
112
132
  if (param) {
113
133
  const mt = param[2].exec(str);
114
- if (mt.length === 4 && !ImageParameterToken.#validate(param[1], mt[2], config)) {
115
- // pass
116
- } else {
134
+ if (mt.length !== 4 || ImageParameterToken.#validate(param[1], mt[2], config, true)) {
117
135
  if (mt.length === 3) {
118
136
  super(undefined, config, true, accum);
119
137
  this.#syntax = str;
@@ -129,8 +147,9 @@ class ImageParameterToken extends Token {
129
147
  this.setAttribute('name', 'caption').setAttribute('stage', 7);
130
148
  }
131
149
 
150
+ /** @override */
132
151
  cloneNode() {
133
- const cloned = this.cloneChildren(),
152
+ const cloned = this.cloneChildNodes(),
134
153
  config = this.getAttribute('config'),
135
154
  token = Parser.run(() => new ImageParameterToken(this.#syntax.replace('$1', ''), config));
136
155
  token.replaceChildren(...cloned);
@@ -138,47 +157,52 @@ class ImageParameterToken extends Token {
138
157
  }
139
158
 
140
159
  /**
160
+ * @override
141
161
  * @template {string} T
142
- * @param {T} key
162
+ * @param {T} key 属性键
143
163
  * @returns {TokenAttribute<T>}
144
164
  */
145
165
  getAttribute(key) {
146
- if (key === 'syntax') {
147
- return this.#syntax;
148
- }
149
- return super.getAttribute(key);
166
+ return key === 'syntax' ? this.#syntax : super.getAttribute(key);
150
167
  }
151
168
 
169
+ /** @override */
152
170
  isPlain() {
153
171
  return true;
154
172
  }
155
173
 
174
+ /** 是否是不可变参数 */
156
175
  #isVoid() {
157
176
  return this.#syntax && !this.#syntax.includes('$1');
158
177
  }
159
178
 
160
- toString() {
161
- if (!this.#syntax) {
162
- return super.toString();
163
- }
164
- return this.#syntax.replace('$1', super.toString());
179
+ /**
180
+ * @override
181
+ * @param {string} selector
182
+ */
183
+ toString(selector) {
184
+ return this.#syntax && !(selector && this.matches(selector))
185
+ ? this.#syntax.replace('$1', super.toString(selector))
186
+ : super.toString(selector);
165
187
  }
166
188
 
189
+ /** @override */
167
190
  getPadding() {
168
191
  return Math.max(0, this.#syntax.indexOf('$1'));
169
192
  }
170
193
 
194
+ /** @override */
171
195
  text() {
172
- if (!this.#syntax) {
173
- return super.text().trim();
174
- }
175
- return this.#syntax.replace('$1', super.text()).trim();
196
+ return this.#syntax ? this.#syntax.replace('$1', super.text()).trim() : super.text().trim();
176
197
  }
177
198
 
178
199
  /**
179
- * @template {string|Token} T
180
- * @param {T} token
200
+ * @override
201
+ * @template {Token} T
202
+ * @param {T} token 待插入的子节点
203
+ * @param {number} i 插入位置
181
204
  * @complexity `n`
205
+ * @throws `Error` 不接受自定义输入的图片参数
182
206
  */
183
207
  insertAt(token, i = this.childNodes.length) {
184
208
  if (!Parser.running && this.#isVoid()) {
@@ -187,14 +211,19 @@ class ImageParameterToken extends Token {
187
211
  return super.insertAt(token, i);
188
212
  }
189
213
 
190
- /** @complexity `n` */
214
+ /**
215
+ * 获取参数值
216
+ * @complexity `n`
217
+ */
191
218
  getValue() {
192
219
  return this.#isVoid() || super.text();
193
220
  }
194
221
 
195
222
  /**
196
- * @param {string|boolean} value
223
+ * 设置参数值
224
+ * @param {string|boolean} value 参数值
197
225
  * @complexity `n`
226
+ * @throws SyntaxError` 非法的参数值
198
227
  */
199
228
  setValue(value) {
200
229
  if (this.#isVoid()) {
@@ -212,7 +241,7 @@ class ImageParameterToken extends Token {
212
241
  }]]`, this.getAttribute('include'), 6, this.getAttribute('config')),
213
242
  {childNodes: {length}, firstElementChild} = root,
214
243
  param = firstElementChild?.lastElementChild;
215
- if (length !== 1 || !firstElementChild?.matches('file#File:F')
244
+ if (length !== 1 || !firstElementChild?.matches('file#File\\:F')
216
245
  || firstElementChild.childNodes.length !== 2 || param.name !== this.name
217
246
  ) {
218
247
  throw new SyntaxError(`非法的 ${this.name} 参数:${noWrap(value)}`);