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
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const Parser = require('../..'),
4
+ NestedToken = require('.');
5
+
6
+ /**
7
+ * `<choose>`
8
+ * @classdesc `{childNodes: [...ExtToken|NoincludeToken]}`
9
+ */
10
+ class ChooseToken extends NestedToken {
11
+ name = 'choose';
12
+
13
+ /**
14
+ * @param {string|undefined} wikitext wikitext
15
+ * @param {accum} accum
16
+ */
17
+ constructor(wikitext, config = Parser.getConfig(), accum = []) {
18
+ const regex = /<(option|choicetemplate)(\s[^>]*)?>(.*?)<\/(\1)>/gsu;
19
+ super(wikitext, regex, ['option', 'choicetemplate'], config, accum);
20
+ }
21
+ }
22
+
23
+ Parser.classes.ChooseToken = __filename;
24
+ module.exports = ChooseToken;
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const Parser = require('../..'),
4
+ NestedToken = require('.');
5
+
6
+ /**
7
+ * `<combobox>`
8
+ * @classdesc `{childNodes: [...ExtToken|NoincludeToken]}`
9
+ */
10
+ class ComboboxToken extends NestedToken {
11
+ name = 'combobox';
12
+
13
+ /**
14
+ * @param {string|undefined} wikitext wikitext
15
+ * @param {accum} accum
16
+ */
17
+ constructor(wikitext, config = Parser.getConfig(), accum = []) {
18
+ super(wikitext, /<(combooption)(\s[^>]*)?>(.*?)<\/(combooption\s*)>/gisu, ['combooption'], config, accum);
19
+ }
20
+ }
21
+
22
+ Parser.classes.ComboboxToken = __filename;
23
+ module.exports = ComboboxToken;
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ const {generateForChild} = require('../../util/lint'),
4
+ Parser = require('../..'),
5
+ Token = require('..');
6
+
7
+ /**
8
+ * 嵌套式的扩展标签
9
+ * @classdesc `{childNodes: [...ExtToken|NoincludeToken]}`
10
+ */
11
+ class NestedToken extends Token {
12
+ type = 'ext-inner';
13
+ #tags;
14
+
15
+ /**
16
+ * @param {string|undefined} wikitext wikitext
17
+ * @param {RegExp} regex 内层正则
18
+ * @param {string[]} tags 内层标签名
19
+ * @param {accum} accum
20
+ */
21
+ constructor(wikitext, regex, tags, config = Parser.getConfig(), accum = []) {
22
+ const ExtToken = require('../tagPair/ext'),
23
+ NoincludeToken = require('../nowiki/noinclude');
24
+ const text = wikitext?.replaceAll(
25
+ regex,
26
+ /** @type {function(...string): string} */ (_, name, attr, inner, closing) => {
27
+ const str = `\0${accum.length + 1}e\x7F`;
28
+ new ExtToken(name, attr, inner, closing, config, accum);
29
+ return str;
30
+ },
31
+ )?.replaceAll(/(?<=^|\0\d+e\x7F).*?(?=$|\0\d+e\x7F)/gsu, substr => {
32
+ if (substr === '') {
33
+ return '';
34
+ }
35
+ new NoincludeToken(substr, config, accum);
36
+ return `\0${accum.length}c\x7F`;
37
+ });
38
+ super(text, config, true, accum, {NoincludeToken: ':', ExtToken: ':'});
39
+ this.#tags = tags;
40
+ }
41
+
42
+ /**
43
+ * @override
44
+ * @param {number} start 起始位置
45
+ */
46
+ lint(start = 0) {
47
+ let rect;
48
+ return [
49
+ ...super.lint(start),
50
+ ...this.childNodes.filter(child => {
51
+ if (child.type === 'ext') {
52
+ return false;
53
+ }
54
+ const str = String(child).trim();
55
+ return str && !/^<!--.*-->$/u.test(str);
56
+ }).map(child => {
57
+ rect ||= this.getRootNode().posFromIndex(start);
58
+ return generateForChild(child, rect, `<${this.name}>内的无效内容`);
59
+ }),
60
+ ];
61
+ }
62
+
63
+ /**
64
+ * @override
65
+ * @template {string|Token} T
66
+ * @param {T} token 待插入的子节点
67
+ * @param {number} i 插入位置
68
+ */
69
+ insertAt(token, i = this.childNodes.length) {
70
+ return token.type === 'ext' && !this.#tags.includes(token.name)
71
+ ? this.typeError(`${this.constructor.name}只能以${this.#tags.join('或')}标签作为子节点!`)
72
+ : super.insertAt(token, i);
73
+ }
74
+
75
+ /** @override */
76
+ cloneNode() {
77
+ const cloned = this.cloneChildNodes(),
78
+ config = this.getAttribute('config');
79
+ return Parser.run(() => {
80
+ const token = new this.constructor(undefined, config);
81
+ token.append(...cloned);
82
+ return token;
83
+ });
84
+ }
85
+ }
86
+
87
+ Parser.classes.NestedToken = __filename;
88
+ module.exports = NestedToken;
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const Parser = require('../..'),
4
+ NestedToken = require('.');
5
+
6
+ /**
7
+ * `<references>`
8
+ * @classdesc `{childNodes: [...ExtToken|NoincludeToken]}`
9
+ */
10
+ class ReferencesToken extends NestedToken {
11
+ name = 'references';
12
+
13
+ /**
14
+ * @param {string|undefined} wikitext wikitext
15
+ * @param {accum} accum
16
+ */
17
+ constructor(wikitext, config = Parser.getConfig(), accum = []) {
18
+ super(wikitext, /<(ref)(\s[^>]*)?>(.*?)<\/(ref\s*)>/gisu, ['ref'], config, accum);
19
+ }
20
+ }
21
+
22
+ Parser.classes.ReferencesToken = __filename;
23
+ module.exports = ReferencesToken;
@@ -30,8 +30,21 @@ class CommentToken extends hidden(NowikiToken) {
30
30
  }
31
31
 
32
32
  /** @override */
33
- cloneNode() {
34
- return Parser.run(() => new CommentToken(String(this.firstChild), this.closed, this.getAttribute('config')));
33
+ getPadding() {
34
+ return 4;
35
+ }
36
+
37
+ /** @override */
38
+ print() {
39
+ return super.print({pre: '&lt;!--', post: this.closed ? '--&gt;' : ''});
40
+ }
41
+
42
+ /**
43
+ * @override
44
+ * @param {number} start 起始位置
45
+ */
46
+ lint(start = 0) {
47
+ return this.closed ? [] : [generateForSelf(this, this.getRootNode().posFromIndex(start), '未闭合的HTML注释')];
35
48
  }
36
49
 
37
50
  /**
@@ -49,21 +62,8 @@ class CommentToken extends hidden(NowikiToken) {
49
62
  }
50
63
 
51
64
  /** @override */
52
- getPadding() {
53
- return 4;
54
- }
55
-
56
- /** @override */
57
- print() {
58
- return super.print({pre: '&lt;!--', post: this.closed ? '--&gt;' : ''});
59
- }
60
-
61
- /**
62
- * @override
63
- * @param {number} start 起始位置
64
- */
65
- lint(start = 0) {
66
- return this.closed ? [] : [generateForSelf(this, this.getRootNode().posFromIndex(start), '未闭合的HTML注释')];
65
+ cloneNode() {
66
+ return Parser.run(() => new CommentToken(String(this.firstChild), this.closed, this.getAttribute('config')));
67
67
  }
68
68
  }
69
69
 
package/src/nowiki/dd.js CHANGED
@@ -32,9 +32,9 @@ class DdToken extends NowikiToken {
32
32
 
33
33
  set indent(indent) {
34
34
  if (this.type === 'dd') {
35
- if (typeof indent !== 'number') {
35
+ if (!Number.isInteger(indent)) {
36
36
  this.typeError('set indent', 'Number');
37
- } else if (!Number.isInteger(indent) || indent < 0) {
37
+ } else if (indent < 0) {
38
38
  throw new RangeError(`indent 应为自然数!${indent}`);
39
39
  }
40
40
  this.setText(':'.repeat(indent));
@@ -11,18 +11,14 @@ const hidden = require('../../mixin/hidden'),
11
11
  class DoubleUnderscoreToken extends hidden(NowikiToken) {
12
12
  type = 'double-underscore';
13
13
 
14
- /**
15
- * @param {string} word 状态开关名
16
- * @param {accum} accum
17
- */
18
- constructor(word, config = Parser.getConfig(), accum = []) {
19
- super(word, config, accum);
20
- this.setAttribute('name', word.toLowerCase());
14
+ /** @override */
15
+ getPadding() {
16
+ return 2;
21
17
  }
22
18
 
23
19
  /** @override */
24
- cloneNode() {
25
- return Parser.run(() => new DoubleUnderscoreToken(String(this.firstChild), this.getAttribute('config')));
20
+ print() {
21
+ return super.print({pre: '__', post: '__'});
26
22
  }
27
23
 
28
24
  /**
@@ -33,14 +29,18 @@ class DoubleUnderscoreToken extends hidden(NowikiToken) {
33
29
  return selector && this.matches(selector) ? '' : `__${String(this.firstChild)}__`;
34
30
  }
35
31
 
36
- /** @override */
37
- getPadding() {
38
- return 2;
32
+ /**
33
+ * @param {string} word 状态开关名
34
+ * @param {accum} accum
35
+ */
36
+ constructor(word, config = Parser.getConfig(), accum = []) {
37
+ super(word, config, accum);
38
+ this.setAttribute('name', word.toLowerCase());
39
39
  }
40
40
 
41
41
  /** @override */
42
- print() {
43
- return super.print({pre: '__', post: '__'});
42
+ cloneNode() {
43
+ return Parser.run(() => new DoubleUnderscoreToken(String(this.firstChild), this.getAttribute('config')));
44
44
  }
45
45
 
46
46
  /**
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const fixedToken = require('../../mixin/fixedToken'),
4
+ {generateForSelf} = require('../../util/lint'),
4
5
  Parser = require('../..'),
5
6
  Token = require('..'),
6
7
  AstText = require('../../lib/text');
@@ -20,6 +21,17 @@ class NowikiToken extends fixedToken(Token) {
20
21
  super(wikitext, config, true, accum);
21
22
  }
22
23
 
24
+ /**
25
+ * @override
26
+ * @param {number} start 起始位置
27
+ */
28
+ lint(start = 0) {
29
+ const {type, name} = this;
30
+ return type === 'ext-inner' && (name === 'templatestyles' || name === 'section') && String(this)
31
+ ? [generateForSelf(this, this.getRootNode().posFromIndex(start), `<${name}>标签内不应有任何内容`)]
32
+ : super.lint(start);
33
+ }
34
+
23
35
  /**
24
36
  * @override
25
37
  * @this {NowikiToken & {firstChild: AstText, constructor: typeof NowikiToken}}
@@ -23,14 +23,6 @@ class OnlyincludeToken extends Token {
23
23
  super(inner, config, true, accum);
24
24
  }
25
25
 
26
- /** @override */
27
- cloneNode() {
28
- const cloned = this.cloneChildNodes(),
29
- token = Parser.run(() => new OnlyincludeToken(undefined, this.getAttribute('config')));
30
- token.append(...cloned);
31
- return token;
32
- }
33
-
34
26
  /**
35
27
  * @override
36
28
  * @param {string} selector
@@ -53,6 +45,16 @@ class OnlyincludeToken extends Token {
53
45
  isPlain() {
54
46
  return true;
55
47
  }
48
+
49
+ /** @override */
50
+ cloneNode() {
51
+ const cloned = this.cloneChildNodes();
52
+ return Parser.run(() => {
53
+ const token = new OnlyincludeToken(undefined, this.getAttribute('config'));
54
+ token.append(...cloned);
55
+ return token;
56
+ });
57
+ }
56
58
  }
57
59
 
58
60
  Parser.classes.OnlyincludeToken = __filename;
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ const {generateForChild} = require('../../util/lint'),
4
+ Parser = require('../..'),
5
+ Token = require('..'),
6
+ AtomToken = require('../atom');
7
+
8
+ /**
9
+ * `<inputbox>`
10
+ * @classdesc `{childNodes: ...AtomToken}`
11
+ */
12
+ class ParamTagToken extends Token {
13
+ type = 'ext-inner';
14
+
15
+ /**
16
+ * @param {string} wikitext wikitext
17
+ * @param {accum} accum
18
+ */
19
+ constructor(wikitext, config = Parser.getConfig(), accum = []) {
20
+ super(undefined, config, true, accum, {AtomToken: ':'});
21
+ if (wikitext) {
22
+ this.append(
23
+ ...wikitext.split('\n').map(line => new AtomToken(line, 'param-line', config, accum, {AstText: ':'})),
24
+ );
25
+ }
26
+ }
27
+
28
+ /**
29
+ * @override
30
+ * @param {string} selector
31
+ */
32
+ toString(selector) {
33
+ return super.toString(selector, '\n');
34
+ }
35
+
36
+ /** @override */
37
+ getGaps() {
38
+ return 1;
39
+ }
40
+
41
+ /** @override */
42
+ print() {
43
+ return super.print({sep: '\n'});
44
+ }
45
+
46
+ /**
47
+ * @override
48
+ * @param {number} start 起始位置
49
+ */
50
+ lint(start = 0) {
51
+ let /** @type {{top: number, left: number}} */ rect;
52
+ return this.childNodes.filter(child => {
53
+ const {childNodes} = child,
54
+ i = childNodes.findIndex(({type}) => type !== 'text'),
55
+ str = (i >= 0 ? childNodes.slice(0, i).map(String).join('') : String(child)).trim();
56
+ return str && !(i >= 0 ? /^[a-z]+\s*(?:=|$)/iu : /^[a-z]+\s*=/iu).test(str);
57
+ }).map(child => {
58
+ rect ||= this.getRootNode().posFromIndex(start);
59
+ return generateForChild(child, rect, `${this.name}的无效参数`);
60
+ });
61
+ }
62
+
63
+ /** @override */
64
+ text() {
65
+ return super.text('\n');
66
+ }
67
+
68
+ /**
69
+ * @override
70
+ * @this {ParamTagToken & {constructor: typeof ParamTagToken}}
71
+ */
72
+ cloneNode() {
73
+ const cloned = this.cloneChildNodes();
74
+ return Parser.run(() => {
75
+ const token = new this.constructor(undefined, this.getAttribute('config'));
76
+ token.append(...cloned);
77
+ return token;
78
+ });
79
+ }
80
+ }
81
+
82
+ Parser.classes.ParamTagToken = __filename;
83
+ module.exports = ParamTagToken;
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ const parseBrackets = require('../../parser/brackets'),
4
+ Parser = require('../..'),
5
+ ParamTagToken = require('.'),
6
+ AtomToken = require('../atom');
7
+
8
+ /**
9
+ * `<inputbox>`
10
+ * @classdesc `{childNodes: ...AtomToken}`
11
+ */
12
+ class InputboxToken extends ParamTagToken {
13
+ name = 'inputbox';
14
+
15
+ /**
16
+ * @param {string} wikitext wikitext
17
+ * @param {accum} accum
18
+ */
19
+ constructor(wikitext, config = Parser.getConfig(), accum = []) {
20
+ super(undefined, config, accum);
21
+ wikitext = parseBrackets(wikitext, config, accum);
22
+ accum.splice(accum.indexOf(this), 1);
23
+ accum.push(this);
24
+ if (wikitext) {
25
+ this.append(...wikitext.split('\n').map(line => new AtomToken(
26
+ line, 'param-line', config, accum, {AstText: ':', ArgToken: ':', TranscludeToken: ':'},
27
+ )));
28
+ }
29
+ }
30
+
31
+ /** @override */
32
+ afterBuild() {
33
+ for (const heading of this.querySelectorAll('heading')) {
34
+ const {firstChild, lastChild, name} = heading,
35
+ syntax = '='.repeat(name);
36
+ heading.replaceWith(syntax, ...firstChild.cloneChildNodes(), `${syntax}${String(lastChild)}`);
37
+ }
38
+ }
39
+ }
40
+
41
+ Parser.classes.InputboxToken = __filename;
42
+ module.exports = InputboxToken;
package/src/parameter.js CHANGED
@@ -26,6 +26,19 @@ class ParameterToken extends fixedToken(Token) {
26
26
  this.setValue(value);
27
27
  }
28
28
 
29
+ /**
30
+ * 是否是重复参数
31
+ * @this {ParameterToken & {parentNode: TranscludeToken}}
32
+ */
33
+ get duplicated() {
34
+ const TranscludeToken = require('./transclude');
35
+ try {
36
+ return Boolean(this.parentNode?.getDuplicatedArgs()?.some(([key]) => key === this.name));
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
29
42
  /**
30
43
  * @param {string|number} key 参数名
31
44
  * @param {string} value 参数值
@@ -42,25 +55,14 @@ class ParameterToken extends fixedToken(Token) {
42
55
  this.append(keyToken, token.setAttribute('stage', 2));
43
56
  }
44
57
 
45
- /** @override */
46
- cloneNode() {
47
- const [key, value] = this.cloneChildNodes(),
48
- config = this.getAttribute('config');
49
- return Parser.run(() => {
50
- const token = new ParameterToken(this.anon ? Number(this.name) : undefined, undefined, config);
51
- token.firstChild.safeReplaceWith(key);
52
- token.lastChild.safeReplaceWith(value);
53
- return token.afterBuild();
54
- });
55
- }
56
-
57
58
  /** @override */
58
59
  afterBuild() {
59
60
  if (!this.anon) {
61
+ const TranscludeToken = require('./transclude');
60
62
  const name = this.firstChild.text().trim(),
61
63
  {parentNode} = this;
62
64
  this.setAttribute('name', name);
63
- if (parentNode && parentNode instanceof require('./transclude')) {
65
+ if (parentNode && parentNode instanceof TranscludeToken) {
64
66
  parentNode.getAttribute('keys').add(name);
65
67
  parentNode.getArgs(name, false, false).add(this);
66
68
  }
@@ -101,6 +103,18 @@ class ParameterToken extends fixedToken(Token) {
101
103
  return super.print({sep: this.anon ? '' : '='});
102
104
  }
103
105
 
106
+ /** @override */
107
+ cloneNode() {
108
+ const [key, value] = this.cloneChildNodes(),
109
+ config = this.getAttribute('config');
110
+ return Parser.run(() => {
111
+ const token = new ParameterToken(this.anon ? Number(this.name) : undefined, undefined, config);
112
+ token.firstChild.safeReplaceWith(key);
113
+ token.lastChild.safeReplaceWith(value);
114
+ return token.afterBuild();
115
+ });
116
+ }
117
+
104
118
  /**
105
119
  * @override
106
120
  * @returns {string}
@@ -141,9 +155,9 @@ class ParameterToken extends fixedToken(Token) {
141
155
  const templateLike = this.parentNode?.isTemplate(),
142
156
  wikitext = `{{${templateLike ? ':T|' : 'lc:'}${this.anon ? '' : '1='}${value}}}`,
143
157
  root = Parser.parse(wikitext, this.getAttribute('include'), 2, this.getAttribute('config')),
144
- {childNodes: {length}, firstChild: transclude} = root,
158
+ {length, firstChild: transclude} = root,
145
159
  /** @type {Token & {lastChild: ParameterToken}} */
146
- {lastChild: parameter, type, name, childNodes: {length: transcludeLength}} = transclude,
160
+ {lastChild: parameter, type, name, length: transcludeLength} = transclude,
147
161
  targetType = templateLike ? 'template' : 'magic-word',
148
162
  targetName = templateLike ? 'T' : 'lc';
149
163
  if (length !== 1 || type !== targetType || name !== targetName || transcludeLength !== 2
@@ -172,12 +186,12 @@ class ParameterToken extends fixedToken(Token) {
172
186
  const TranscludeToken = require('./transclude');
173
187
  const {parentNode} = this;
174
188
  // 必须检测是否是TranscludeToken
175
- if (!parentNode?.isTemplate() || !(parentNode instanceof require('./transclude'))) {
189
+ if (!parentNode?.isTemplate() || !(parentNode instanceof TranscludeToken)) {
176
190
  throw new Error(`${this.constructor.name}.rename 方法仅用于模板参数!`);
177
191
  }
178
192
  const root = Parser.parse(`{{:T|${key}=}}`, this.getAttribute('include'), 2, this.getAttribute('config')),
179
- {childNodes: {length}, firstChild: template} = root,
180
- {type, name, lastChild: parameter, childNodes: {length: templateLength}} = template;
193
+ {length, firstChild: template} = root,
194
+ {type, name, lastChild: parameter, length: templateLength} = template;
181
195
  if (length !== 1 || type !== 'template' || name !== 'T' || templateLength !== 2) {
182
196
  throw new SyntaxError(`非法的模板参数名:${key}`);
183
197
  }
package/src/syntax.js CHANGED
@@ -7,7 +7,7 @@ const {undo} = require('../util/debug'),
7
7
 
8
8
  /**
9
9
  * 满足特定语法格式的plain Token
10
- * @classdesc `{childNodes: (AstText|Token)[]}`
10
+ * @classdesc `{childNodes: ...AstText|Token}`
11
11
  */
12
12
  class SyntaxToken extends Token {
13
13
  #pattern;
@@ -65,6 +65,14 @@ class SyntaxToken extends Token {
65
65
  return key === 'pattern' ? this.#pattern : super.getAttribute(key);
66
66
  }
67
67
 
68
+ /**
69
+ * @override
70
+ * @param {PropertyKey} key 属性键
71
+ */
72
+ hasAttribute(key) {
73
+ return key === 'pattern' || super.hasAttribute(key);
74
+ }
75
+
68
76
  /**
69
77
  * @override
70
78
  * @param {...Token} elements 待替换的子节点