wikiparser-node 0.8.1 → 0.9.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.
package/src/attribute.js CHANGED
@@ -302,7 +302,7 @@ class AttributeToken extends fixedToken(Token) {
302
302
  * @override
303
303
  * @param {number} start 起始位置
304
304
  */
305
- lint(start = 0) {
305
+ lint(start = this.getAbsoluteIndex()) {
306
306
  const errors = super.lint(start),
307
307
  {balanced, firstChild, lastChild, type, name, parentNode, value} = this,
308
308
  tagName = parentNode?.name;
@@ -310,7 +310,7 @@ class AttributeToken extends fixedToken(Token) {
310
310
  if (!balanced) {
311
311
  const root = this.getRootNode();
312
312
  rect = {start, ...root.posFromIndex(start)};
313
- const e = generateForChild(lastChild, rect, '未闭合的引号', 'warning'),
313
+ const e = generateForChild(lastChild, rect, 'unclosed quotes', 'warning'),
314
314
  startIndex = e.startIndex - 1,
315
315
  startCol = e.startCol - 1;
316
316
  errors.push({...e, startIndex, startCol, excerpt: String(root).slice(startIndex, startIndex + 50)});
@@ -322,10 +322,10 @@ class AttributeToken extends fixedToken(Token) {
322
322
  && (tagName === 'meta' || tagName === 'link' || !commonHtmlAttrs.has(name))
323
323
  )) {
324
324
  rect ||= {start, ...this.getRootNode().posFromIndex(start)};
325
- errors.push(generateForChild(firstChild, rect, '非法的属性名'));
325
+ errors.push(generateForChild(firstChild, rect, 'illegal attribute name'));
326
326
  } else if (name === 'style' && typeof value === 'string' && insecureStyle.test(value)) {
327
327
  rect ||= {start, ...this.getRootNode().posFromIndex(start)};
328
- errors.push(generateForChild(lastChild, rect, '不安全的样式'));
328
+ errors.push(generateForChild(lastChild, rect, 'insecure style'));
329
329
  }
330
330
  return errors;
331
331
  }
@@ -355,6 +355,14 @@ class AttributeToken extends fixedToken(Token) {
355
355
  return key === 'quotes' ? this.#quotes : super.getAttribute(key);
356
356
  }
357
357
 
358
+ /**
359
+ * @override
360
+ * @param {PropertyKey} key 属性键
361
+ */
362
+ hasAttribute(key) {
363
+ return key === 'equal' || key === 'quotes' || super.hasAttribute(key);
364
+ }
365
+
358
366
  /** @override */
359
367
  cloneNode() {
360
368
  const [key, value] = this.cloneChildNodes(),
@@ -401,8 +409,7 @@ class AttributeToken extends fixedToken(Token) {
401
409
  if (length !== 1 || tag.type !== type.slice(0, -5)) {
402
410
  throw new SyntaxError(`非法的标签属性:${noWrap(value)}`);
403
411
  } else if (type === 'table-attr') {
404
- const {length: tableLength} = tag;
405
- if (tableLength !== 2) {
412
+ if (tag.length !== 2) {
406
413
  throw new SyntaxError(`非法的标签属性:${noWrap(value)}`);
407
414
  }
408
415
  attrs = tag.lastChild;
@@ -442,8 +449,7 @@ class AttributeToken extends fixedToken(Token) {
442
449
  if (length !== 1 || tag.type !== type.slice(0, -5)) {
443
450
  throw new SyntaxError(`非法的标签属性名:${noWrap(key)}`);
444
451
  } else if (type === 'table-attr') {
445
- const {length: tableLength} = tag;
446
- if (tableLength !== 2) {
452
+ if (tag.length !== 2) {
447
453
  throw new SyntaxError(`非法的标签属性名:${noWrap(key)}`);
448
454
  }
449
455
  attrs = tag.lastChild;
package/src/attributes.js CHANGED
@@ -183,7 +183,7 @@ class AttributesToken extends Token {
183
183
  * @this {AttributesToken & {parentNode: HtmlToken}}
184
184
  * @param {number} start 起始位置
185
185
  */
186
- lint(start = 0) {
186
+ lint(start = this.getAbsoluteIndex()) {
187
187
  const HtmlToken = require('./html');
188
188
  const errors = super.lint(start),
189
189
  {parentNode: {closing}, length, childNodes} = this,
@@ -192,14 +192,14 @@ class AttributesToken extends Token {
192
192
  let rect;
193
193
  if (closing && this.text().trim()) {
194
194
  rect = {start, ...this.getRootNode().posFromIndex(start)};
195
- errors.push(generateForSelf(this, rect, '位于闭合标签的属性'));
195
+ errors.push(generateForSelf(this, rect, 'attributes of a closing tag'));
196
196
  }
197
197
  for (let i = 0; i < length; i++) {
198
198
  const /** @type {AtomToken|AttributeToken} */ attr = childNodes[i];
199
199
  if (attr instanceof AtomToken && attr.text().trim()) {
200
200
  rect ||= {start, ...this.getRootNode().posFromIndex(start)};
201
201
  errors.push({
202
- ...generateForChild(attr, rect, '包含无效属性'),
202
+ ...generateForChild(attr, rect, 'containing invalid attribute'),
203
203
  excerpt: childNodes.slice(i).map(String).join('').slice(0, 50),
204
204
  });
205
205
  } else if (attr instanceof AttributeToken) {
@@ -215,7 +215,7 @@ class AttributesToken extends Token {
215
215
  if (duplicated.size > 0) {
216
216
  rect ||= {start, ...this.getRootNode().posFromIndex(start)};
217
217
  for (const key of duplicated) {
218
- errors.push(...attrs[key].map(attr => generateForChild(attr, rect, `重复的${key}属性`)));
218
+ errors.push(...attrs[key].map(attr => generateForChild(attr, rect, Parser.msg('duplicated $1 attribute', key))));
219
219
  }
220
220
  }
221
221
  return errors;
@@ -236,10 +236,9 @@ class AttributesToken extends Token {
236
236
 
237
237
  /** 清理标签属性 */
238
238
  sanitize() {
239
- const {childNodes, length} = this;
240
239
  let dirty = false;
241
- for (let i = length - 1; i >= 0; i--) {
242
- const child = childNodes[i];
240
+ for (let i = this.length - 1; i >= 0; i--) {
241
+ const child = this.childNodes[i];
243
242
  if (child instanceof AtomToken && child.text().trim()) {
244
243
  dirty = true;
245
244
  this.removeAt(i);
@@ -67,7 +67,7 @@ class ConverterFlagsToken extends Token {
67
67
  * @override
68
68
  * @param {number} start 起始位置
69
69
  */
70
- lint(start = 0) {
70
+ lint(start = this.getAbsoluteIndex()) {
71
71
  const variantFlags = this.getVariantFlags(),
72
72
  unknownFlags = this.getUnknownFlags(),
73
73
  validFlags = new Set(this.#flags.filter(flag => definedFlags.has(flag))),
@@ -85,7 +85,7 @@ class ConverterFlagsToken extends Token {
85
85
  if (flag && !variantFlags.has(flag) && !unknownFlags.has(flag)
86
86
  && (variantFlags.size > 0 || !validFlags.has(flag))
87
87
  ) {
88
- const error = generateForChild(child, rect, '无效的转换标记');
88
+ const error = generateForChild(child, rect, 'invalid conversion flag');
89
89
  errors.push({...error, excerpt: childNodes.slice(0, i + 1).map(String).join(';').slice(-50)});
90
90
  }
91
91
  }
@@ -97,7 +97,7 @@ class ConverterFlagsToken extends Token {
97
97
  * @complexity `n`
98
98
  */
99
99
  getUnknownFlags() {
100
- return new Set(this.#flags.filter(flag => /\{\{[^{}]+\}\}/u.test(flag)));
100
+ return new Set(this.#flags.filter(flag => /\{{3}[^{}]+\}{3}/u.test(flag)));
101
101
  }
102
102
 
103
103
  /** 获取指定语言变体的转换标记 */
@@ -48,9 +48,8 @@ class ConverterRuleToken extends Token {
48
48
  if (hasColon) {
49
49
  const i = rule.indexOf(':'),
50
50
  j = rule.slice(0, i).indexOf('=>'),
51
- v = j === -1 ? rule.slice(0, i) : rule.slice(j + 2, i),
52
- {variants} = config;
53
- if (variants.includes(v.trim())) {
51
+ v = j === -1 ? rule.slice(0, i) : rule.slice(j + 2, i);
52
+ if (config.variants.includes(v.trim())) {
54
53
  super.insertAt(new AtomToken(v, 'converter-rule-variant', config, accum));
55
54
  super.insertAt(new AtomToken(rule.slice(i + 1), 'converter-rule-to', config, accum));
56
55
  if (j !== -1) {
@@ -127,9 +126,8 @@ class ConverterRuleToken extends Token {
127
126
  /** @override */
128
127
  afterBuild() {
129
128
  const /** @type {AstListener} */ converterRuleListener = (e, data) => {
130
- const {childNodes, length} = this,
131
- {prevTarget} = e;
132
- if (length > 1 && childNodes.at(-2) === prevTarget) {
129
+ const {prevTarget} = e;
130
+ if (this.length > 1 && this.childNodes.at(-2) === prevTarget) {
133
131
  const v = prevTarget.text().trim(),
134
132
  {variants} = this.getAttribute('config');
135
133
  if (!variants.includes(v)) {
package/src/extLink.js CHANGED
@@ -51,7 +51,7 @@ class ExtLinkToken extends Token {
51
51
  * @param {string} text 链接文字
52
52
  * @param {accum} accum
53
53
  */
54
- constructor(url, space, text, config = Parser.getConfig(), accum = []) {
54
+ constructor(url, space = '', text = '', config = Parser.getConfig(), accum = []) {
55
55
  super(undefined, config, true, accum, {
56
56
  MagicLinkToken: 0, Token: 1,
57
57
  });
@@ -101,8 +101,9 @@ class ExtLinkToken extends Token {
101
101
 
102
102
  /** @override */
103
103
  print() {
104
- const {length} = this;
105
- return super.print(length > 1 ? {pre: '[', sep: this.#space, post: ']'} : {pre: '[', post: `${this.#space}]`});
104
+ return super.print(
105
+ this.length > 1 ? {pre: '[', sep: this.#space, post: ']'} : {pre: '[', post: `${this.#space}]`},
106
+ );
106
107
  }
107
108
 
108
109
  /** @override */
package/src/gallery.js CHANGED
@@ -26,14 +26,11 @@ class GalleryToken extends Token {
26
26
  super(undefined, config, true, accum, {
27
27
  AstText: ':', GalleryImageToken: ':', HiddenToken: ':',
28
28
  });
29
- const /** @type {ParserConfig} */ newConfig = {
30
- ...config, img: Object.fromEntries(Object.entries(config.img).filter(([, param]) => param !== 'width')),
31
- };
32
29
  for (const line of inner?.split('\n') ?? []) {
33
30
  const matches = /^([^|]+)(?:\|(.*))?/u.exec(line);
34
31
  if (!matches) {
35
32
  super.insertAt(line.trim()
36
- ? new HiddenToken(line, undefined, newConfig, [], {
33
+ ? new HiddenToken(line, undefined, config, [], {
37
34
  AstText: ':',
38
35
  })
39
36
  : line);
@@ -42,9 +39,9 @@ class GalleryToken extends Token {
42
39
  const [, file, alt] = matches,
43
40
  title = this.normalizeTitle(file, 6, true, true);
44
41
  if (title.valid) {
45
- super.insertAt(new GalleryImageToken(file, alt, title, newConfig, accum));
42
+ super.insertAt(new GalleryImageToken(file, alt, config, accum));
46
43
  } else {
47
- super.insertAt(new HiddenToken(line, undefined, newConfig, [], {
44
+ super.insertAt(new HiddenToken(line, undefined, config, [], {
48
45
  AstText: ':',
49
46
  }));
50
47
  }
@@ -78,7 +75,7 @@ class GalleryToken extends Token {
78
75
  * @override
79
76
  * @param {number} start 起始位置
80
77
  */
81
- lint(start = 0) {
78
+ lint(start = this.getAbsoluteIndex()) {
82
79
  const {top, left} = this.getRootNode().posFromIndex(start),
83
80
  /** @type {LintError[]} */ errors = [];
84
81
  for (let i = 0, startIndex = start; i < this.length; i++) {
@@ -90,7 +87,7 @@ class GalleryToken extends Token {
90
87
  startCol = i ? 0 : left;
91
88
  if (child.type === 'hidden' && trimmed && !/^<!--.*-->$/u.test(trimmed)) {
92
89
  errors.push({
93
- message: '图库中的无效内容',
90
+ message: Parser.msg('invalid content in <$1>', 'gallery'),
94
91
  severity: 'error',
95
92
  startIndex,
96
93
  endIndex: startIndex + length,
@@ -127,7 +124,7 @@ class GalleryToken extends Token {
127
124
  insertImage(file, i = this.length) {
128
125
  const title = this.normalizeTitle(file, 6, true, true);
129
126
  if (title.valid) {
130
- const token = Parser.run(() => new GalleryImageToken(file, undefined, title, this.getAttribute('config')));
127
+ const token = Parser.run(() => new GalleryImageToken(file, undefined, this.getAttribute('config')));
131
128
  return this.insertAt(token, i);
132
129
  }
133
130
  throw new SyntaxError(`非法的文件名:${file}`);
package/src/heading.js CHANGED
@@ -39,7 +39,7 @@ class HeadingToken extends fixedToken(sol(Token)) {
39
39
 
40
40
  /**
41
41
  * @override
42
- * @this {{prependNewLine(): ''|'\n', appendNewLine(): ''|'\n'} & HeadingToken}
42
+ * @this {{prependNewLine(): ''|'\n'} & HeadingToken}
43
43
  * @param {string} selector
44
44
  * @returns {string}
45
45
  */
@@ -49,17 +49,17 @@ class HeadingToken extends fixedToken(sol(Token)) {
49
49
  ? ''
50
50
  : `${this.prependNewLine()}${equals}${
51
51
  this.firstChild.toString(selector)
52
- }${equals}${this.lastChild.toString(selector)}${this.appendNewLine()}`;
52
+ }${equals}${this.lastChild.toString(selector)}`;
53
53
  }
54
54
 
55
55
  /**
56
56
  * @override
57
- * @this {HeadingToken & {prependNewLine(): ''|'\n', appendNewLine(): ''|'\n'}}
57
+ * @this {HeadingToken & {prependNewLine(): ''|'\n'}}
58
58
  * @returns {string}
59
59
  */
60
60
  text() {
61
61
  const equals = '='.repeat(Number(this.name));
62
- return `${this.prependNewLine()}${equals}${this.firstChild.text()}${equals}${this.appendNewLine()}`;
62
+ return `${this.prependNewLine()}${equals}${this.firstChild.text()}${equals}`;
63
63
  }
64
64
 
65
65
  /** @override */
@@ -82,7 +82,7 @@ class HeadingToken extends fixedToken(sol(Token)) {
82
82
  * @override
83
83
  * @param {number} start 起始位置
84
84
  */
85
- lint(start = 0) {
85
+ lint(start = this.getAbsoluteIndex()) {
86
86
  const errors = super.lint(start),
87
87
  innerText = String(this.firstChild);
88
88
  let refError;
@@ -90,13 +90,13 @@ class HeadingToken extends fixedToken(sol(Token)) {
90
90
  refError = generateForSelf(this, {start}, '<h1>');
91
91
  errors.push(refError);
92
92
  }
93
- if (innerText[0] === '=' || innerText.at(-1) === '=') {
93
+ if (innerText[0] === '=' || innerText.endsWith('=')) {
94
94
  refError ||= generateForSelf(this, {start}, '');
95
- errors.push({...refError, message: '段落标题中不平衡的"="'});
95
+ errors.push({...refError, message: Parser.msg('unbalanced "=" in a section header')});
96
96
  }
97
97
  if (this.closest('html-attrs, table-attrs')) {
98
98
  refError ||= generateForSelf(this, {start}, '');
99
- errors.push({...refError, message: 'HTML标签属性中的段落标题'});
99
+ errors.push({...refError, message: Parser.msg('section header in a HTML tag')});
100
100
  }
101
101
  return errors;
102
102
  }
package/src/html.js CHANGED
@@ -7,6 +7,8 @@ const {generateForSelf} = require('../util/lint'),
7
7
  Parser = require('..'),
8
8
  Token = require('.');
9
9
 
10
+ const magicWords = new Set(['if', 'ifeq', 'ifexpr', 'ifexist', 'iferror', 'switch']);
11
+
10
12
  /**
11
13
  * HTML标签
12
14
  * @classdesc `{childNodes: [AttributesToken]}`
@@ -107,7 +109,7 @@ class HtmlToken extends attributeParent(fixedToken(Token)) {
107
109
  * @override
108
110
  * @param {number} start 起始位置
109
111
  */
110
- lint(start = 0) {
112
+ lint(start = this.getAbsoluteIndex()) {
111
113
  const errors = super.lint(start);
112
114
  let wikitext, /** @type {LintError} */ refError;
113
115
  if (this.name === 'h1' && !this.#closing) {
@@ -119,20 +121,24 @@ class HtmlToken extends attributeParent(fixedToken(Token)) {
119
121
  wikitext ||= String(this.getRootNode());
120
122
  refError ||= generateForSelf(this, {start}, '');
121
123
  const excerpt = wikitext.slice(Math.max(0, start - 25), start + 25);
122
- errors.push({...refError, message: '表格属性中的HTML标签', excerpt});
124
+ errors.push({...refError, message: Parser.msg('HTML tag in table attributes'), excerpt});
123
125
  }
124
126
  try {
125
127
  this.findMatchingTag();
126
128
  } catch ({message: errorMsg}) {
127
129
  wikitext ||= String(this.getRootNode());
128
130
  refError ||= generateForSelf(this, {start}, '');
129
- const [message] = errorMsg.split(''),
130
- error = {...refError, message, severity: message === '未闭合的标签' ? 'warning' : 'error'};
131
- if (message === '未闭合的标签') {
131
+ const [msg] = errorMsg.split(':'),
132
+ error = {...refError, message: Parser.msg(msg)};
133
+ if (msg === 'unclosed tag') {
134
+ error.severity = 'warning';
132
135
  error.excerpt = wikitext.slice(start, start + 50);
133
- } else if (message === '未匹配的闭合标签') {
136
+ } else if (msg === 'unmatched closing tag') {
134
137
  const end = start + String(this).length;
135
138
  error.excerpt = wikitext.slice(Math.max(0, end - 50), end);
139
+ if (magicWords.has(this.closest('magic-word')?.name)) {
140
+ error.severity = 'warning';
141
+ }
136
142
  }
137
143
  errors.push(error);
138
144
  }
@@ -151,11 +157,11 @@ class HtmlToken extends attributeParent(fixedToken(Token)) {
151
157
  {name: tagName, parentNode} = this,
152
158
  string = noWrap(String(this));
153
159
  if (this.#closing && (this.#selfClosing || html[2].includes(tagName))) {
154
- throw new SyntaxError(`同时闭合和自封闭的标签:${string}`);
160
+ throw new SyntaxError(`tag that is both closing and self-closing: ${string}`);
155
161
  } else if (html[2].includes(tagName) || this.#selfClosing && html[1].includes(tagName)) { // 自封闭标签
156
162
  return this;
157
163
  } else if (this.#selfClosing && html[0].includes(tagName)) {
158
- throw new SyntaxError(`无效自封闭标签:${string}`);
164
+ throw new SyntaxError(`invalid self-closing tag: ${string}`);
159
165
  } else if (!parentNode) {
160
166
  return undefined;
161
167
  }
@@ -175,7 +181,7 @@ class HtmlToken extends attributeParent(fixedToken(Token)) {
175
181
  return token;
176
182
  }
177
183
  }
178
- throw new SyntaxError(`未${this.#closing ? '匹配的闭合' : '闭合的'}标签:${string}`);
184
+ throw new SyntaxError(`${this.#closing ? 'unmatched closing' : 'unclosed'} tag: ${string}`);
179
185
  }
180
186
 
181
187
  /** @override */
@@ -1,81 +1,80 @@
1
1
  'use strict';
2
2
 
3
3
  const {text, noWrap, print, extUrlChar, extUrlCharFirst} = require('../util/string'),
4
+ {generateForSelf} = require('../util/lint'),
5
+ Title = require('../lib/title'),
4
6
  Parser = require('..'),
5
7
  AstText = require('../lib/text'),
6
8
  Token = require('.');
7
9
 
10
+ const params = new Set(['alt', 'link', 'lang', 'page', 'caption']);
11
+
8
12
  /**
9
- * 图片参数
10
- * @classdesc `{childNodes: ...(AstText|Token)}`
13
+ * 检查图片参数是否合法
14
+ * @template {string} T
15
+ * @param {T} key 参数名
16
+ * @param {string} value 参数值
17
+ * @returns {T extends 'link' ? string|Title : boolean}
11
18
  */
12
- class ImageParameterToken extends Token {
13
- static noLink = Symbol('no-link'); // 这个Symbol需要公开
14
-
15
- /**
16
- * 检查图片参数是否合法
17
- * @template {string} T
18
- * @param {T} key 参数名
19
- * @param {string} value 参数值
20
- * @returns {T extends 'link' ? string|Symbol : boolean}
21
- */
22
- static #validate(key, value, config = Parser.getConfig(), halfParsed = false) {
23
- value = value.replace(/\0\d+t\x7F/gu, '').trim();
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}|//)${extUrlCharFirst}|\0\\d+m\x7F)${
32
- extUrlChar
33
- }(?=\0\\d+t\x7F|$)`, 'iu');
34
- if (regex.test(value)) {
35
- return value;
36
- } else if (value.startsWith('[[') && value.endsWith(']]')) {
37
- value = value.slice(2, -2);
38
- }
39
- const title = Parser.normalizeTitle(value, 0, false, config, halfParsed, true, true);
40
- return title.valid && String(title);
19
+ const validate = (key, value, config = Parser.getConfig(), halfParsed = false) => {
20
+ value = value.replace(/\0\d+t\x7F/gu, '').trim();
21
+ switch (key) {
22
+ case 'width':
23
+ return /^(?:\d+x?|\d*x\d+)$/u.test(value);
24
+ case 'link': {
25
+ if (!value) {
26
+ return '';
27
+ }
28
+ const regex = new RegExp(`(?:(?:${config.protocol}|//)${extUrlCharFirst}|\0\\d+m\x7F)${
29
+ extUrlChar
30
+ }(?=\0\\d+t\x7F|$)`, 'iu');
31
+ if (regex.test(value)) {
32
+ return value;
33
+ } else if (value.startsWith('[[') && value.endsWith(']]')) {
34
+ value = value.slice(2, -2);
41
35
  }
42
- case 'lang':
43
- return config.variants.includes(value);
44
- case 'alt':
45
- case 'class':
46
- case 'manualthumb':
47
- return true;
48
- default:
49
- return !isNaN(value);
36
+ const title = Parser.normalizeTitle(value, 0, false, config, halfParsed, true, true);
37
+ return title.valid && title;
50
38
  }
39
+ case 'lang':
40
+ return config.variants.includes(value);
41
+ case 'alt':
42
+ case 'class':
43
+ case 'manualthumb':
44
+ return true;
45
+ default:
46
+ return !isNaN(value);
51
47
  }
48
+ };
52
49
 
50
+ /**
51
+ * 图片参数
52
+ * @classdesc `{childNodes: ...(AstText|Token)}`
53
+ */
54
+ class ImageParameterToken extends Token {
53
55
  type = 'image-parameter';
54
56
  #syntax = '';
55
57
 
56
- /** getValue()的getter */
57
- get value() {
58
- return this.getValue();
59
- }
60
-
61
- set value(value) {
62
- this.setValue(value);
63
- }
64
-
65
58
  /** 图片链接 */
66
59
  get link() {
67
- return this.name === 'link'
68
- ? ImageParameterToken.#validate('link', this.getValue(), this.getAttribute('config'))
69
- : undefined;
60
+ return this.name === 'link' ? validate('link', super.text(), this.getAttribute('config')) : undefined;
70
61
  }
71
62
 
72
63
  set link(value) {
73
64
  if (this.name === 'link') {
74
- value = value === ImageParameterToken.noLink ? '' : value;
75
65
  this.setValue(value);
76
66
  }
77
67
  }
78
68
 
69
+ /** getValue()的getter */
70
+ get value() {
71
+ return this.getValue();
72
+ }
73
+
74
+ set value(value) {
75
+ this.setValue(value);
76
+ }
77
+
79
78
  /** 图片大小 */
80
79
  get size() {
81
80
  if (this.name === 'width') {
@@ -133,7 +132,7 @@ class ImageParameterToken extends Token {
133
132
  ),
134
133
  param = regexes.find(([, key, regex]) => {
135
134
  mt = regex.exec(str);
136
- return mt && (mt.length !== 4 || ImageParameterToken.#validate(key, mt[2], config, true));
135
+ return mt && (mt.length !== 4 || validate(key, mt[2], config, true) !== false);
137
136
  });
138
137
  if (param) {
139
138
  if (mt.length === 3) {
@@ -152,6 +151,13 @@ class ImageParameterToken extends Token {
152
151
  this.setAttribute('name', 'caption').setAttribute('stage', 7);
153
152
  }
154
153
 
154
+ /** @override */
155
+ afterBuild() {
156
+ if (this.parentNode.type === 'gallery-image' && !params.has(this.name)) {
157
+ this.setAttribute('name', 'invalid');
158
+ }
159
+ }
160
+
155
161
  /** @override */
156
162
  isPlain() {
157
163
  return this.name === 'caption';
@@ -177,6 +183,21 @@ class ImageParameterToken extends Token {
177
183
  return Math.max(0, this.#syntax.indexOf('$1'));
178
184
  }
179
185
 
186
+ /**
187
+ * @override
188
+ * @this {ImageParameterToken & {link: Title}}
189
+ * @param {number} start 起始位置
190
+ */
191
+ lint(start = this.getAbsoluteIndex()) {
192
+ const errors = super.lint(start);
193
+ if (this.name === 'invalid') {
194
+ errors.push(generateForSelf(this, {start}, 'invalid gallery image parameter'));
195
+ } else if (this.link?.encoded) {
196
+ errors.push(generateForSelf(this, {start}, 'unnecessary URL encoding in an internal link'));
197
+ }
198
+ return errors;
199
+ }
200
+
180
201
  /** @override */
181
202
  print() {
182
203
  return this.#syntax
@@ -240,7 +261,7 @@ class ImageParameterToken extends Token {
240
261
  * @complexity `n`
241
262
  */
242
263
  getValue() {
243
- return this.#isVoid() || super.text();
264
+ return this.name === 'invalid' ? this.text() : this.#isVoid() || super.text();
244
265
  }
245
266
 
246
267
  /**
@@ -250,7 +271,9 @@ class ImageParameterToken extends Token {
250
271
  * @throws SyntaxError` 非法的参数值
251
272
  */
252
273
  setValue(value) {
253
- if (this.#isVoid()) {
274
+ if (this.name === 'invalid') {
275
+ throw new Error('无效的图片参数!');
276
+ } else if (this.#isVoid()) {
254
277
  if (typeof value !== 'boolean') {
255
278
  this.typeError('setValue', 'Boolean');
256
279
  } else if (value === false) {
package/src/imagemap.js CHANGED
@@ -61,7 +61,7 @@ class ImagemapToken extends Token {
61
61
  title = this.normalizeTitle(file, 0, true);
62
62
  if (title.valid && !title.interwiki && title.ns === 6) {
63
63
  const token = new GalleryImageToken(
64
- file, options.length > 0 ? options.join('|') : undefined, title, config, accum,
64
+ file, options.length > 0 ? options.join('|') : undefined, config, accum,
65
65
  );
66
66
  token.type = 'imagemap-image';
67
67
  super.insertAt(token);
@@ -83,7 +83,7 @@ class ImagemapToken extends Token {
83
83
  if (title.valid) {
84
84
  super.insertAt(new ImagemapLinkToken(
85
85
  line.slice(0, i),
86
- [...mtIn.slice(1), title],
86
+ mtIn.slice(1),
87
87
  substr.slice(substr.indexOf(']]') + 2),
88
88
  config,
89
89
  accum,
@@ -137,7 +137,7 @@ class ImagemapToken extends Token {
137
137
  * @override
138
138
  * @param {number} start 起始位置
139
139
  */
140
- lint(start = 0) {
140
+ lint(start = this.getAbsoluteIndex()) {
141
141
  const errors = super.lint(start),
142
142
  rect = {start, ...this.getRootNode().posFromIndex(start)};
143
143
  if (this.image) {
@@ -145,10 +145,10 @@ class ImagemapToken extends Token {
145
145
  ...this.childNodes.filter(child => {
146
146
  const str = String(child).trim();
147
147
  return child.type === 'noinclude' && str && str[0] !== '#';
148
- }).map(child => generateForChild(child, rect, '无效的<imagemap>链接')),
148
+ }).map(child => generateForChild(child, rect, 'invalid link in <imagemap>')),
149
149
  );
150
150
  } else {
151
- errors.push(generateForSelf(this, rect, '缺少图片的<imagemap>'));
151
+ errors.push(generateForSelf(this, rect, '<imagemap> without an image'));
152
152
  }
153
153
  return errors;
154
154
  }
@@ -31,7 +31,7 @@ class ImagemapLinkToken extends fixedToken(singleLine(Token)) {
31
31
  * @param {accum} accum
32
32
  */
33
33
  constructor(pre, linkStuff, post, config, accum) {
34
- const SomeLinkToken = linkStuff[2] instanceof Title ? LinkToken : ExtLinkToken;
34
+ const SomeLinkToken = linkStuff.length === 2 ? LinkToken : ExtLinkToken;
35
35
  super(undefined, config, true, accum);
36
36
  this.append(pre, new SomeLinkToken(...linkStuff, config, accum), new NoincludeToken(post, config, accum));
37
37
  }
package/src/index.js CHANGED
@@ -29,6 +29,7 @@
29
29
  * -: `{{!-}}`专用
30
30
  * +: `{{!!}}`专用
31
31
  * ~: `{{=}}`专用
32
+ * s: `{{{|subst:}}}`
32
33
  * m: `{{fullurl:}}`、`{{canonicalurl:}}`或`{{filepath:}}`
33
34
  * t: ArgToken或TranscludeToken
34
35
  * h: HeadingToken
@@ -135,7 +136,7 @@ class Token extends AstElement {
135
136
  #buildFromStr = (str, type) => {
136
137
  const nodes = str.split(/[\0\x7F]/u).map((s, i) => {
137
138
  if (i % 2 === 0) {
138
- return new AstText(s, this.#config);
139
+ return new AstText(s);
139
140
  } else if (isNaN(s.at(-1))) {
140
141
  return this.#accum[Number(s.slice(0, -1))];
141
142
  }
@@ -308,7 +309,7 @@ class Token extends AstElement {
308
309
  */
309
310
  insertAt(token, i = this.length) {
310
311
  if (typeof token === 'string') {
311
- token = new AstText(token, this.#config);
312
+ token = new AstText(token);
312
313
  }
313
314
  if (!Parser.running && this.#acceptable) {
314
315
  const acceptableIndices = Object.fromEntries(
@@ -449,7 +450,7 @@ class Token extends AstElement {
449
450
  * @param {string} data 文本内容
450
451
  */
451
452
  createTextNode(data = '') {
452
- return typeof data === 'string' ? new AstText(data, this.#config) : this.typeError('createComment', 'String');
453
+ return typeof data === 'string' ? new AstText(data) : this.typeError('createComment', 'String');
453
454
  }
454
455
 
455
456
  /**
@@ -915,7 +916,7 @@ class Token extends AstElement {
915
916
  }
916
917
  const parseList = require('../parser/list');
917
918
  const lines = String(this.firstChild).split('\n');
918
- let i = this.type === 'root' || this.type === 'ext-inner' && this.type === 'poem' ? 0 : 1;
919
+ let i = this.type === 'root' || this.type === 'ext-inner' && this.name === 'poem' ? 0 : 1;
919
920
  for (; i < lines.length; i++) {
920
921
  lines[i] = parseList(lines[i], this.#config, this.#accum);
921
922
  }
@@ -924,8 +925,10 @@ class Token extends AstElement {
924
925
 
925
926
  /** 解析语言变体转换 */
926
927
  #parseConverter() {
927
- const parseConverter = require('../parser/converter');
928
- this.setText(parseConverter(String(this.firstChild), this.#config, this.#accum));
928
+ if (this.#config.variants?.length > 0) {
929
+ const parseConverter = require('../parser/converter');
930
+ this.setText(parseConverter(String(this.firstChild), this.#config, this.#accum));
931
+ }
929
932
  }
930
933
  }
931
934