wikiparser-node 0.8.0-m → 0.8.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 (77) hide show
  1. package/README.md +39 -0
  2. package/index.js +253 -11
  3. package/lib/element.js +481 -7
  4. package/lib/node.js +552 -6
  5. package/lib/ranges.js +130 -0
  6. package/lib/text.js +108 -16
  7. package/lib/title.js +21 -0
  8. package/mixin/attributeParent.js +117 -0
  9. package/mixin/fixedToken.js +40 -0
  10. package/mixin/hidden.js +3 -0
  11. package/mixin/singleLine.js +31 -0
  12. package/mixin/sol.js +65 -0
  13. package/package.json +5 -4
  14. package/parser/brackets.js +1 -0
  15. package/parser/commentAndExt.js +4 -3
  16. package/parser/converter.js +1 -0
  17. package/parser/externalLinks.js +1 -0
  18. package/parser/hrAndDoubleUnderscore.js +1 -0
  19. package/parser/html.js +1 -0
  20. package/parser/links.js +5 -4
  21. package/parser/list.js +1 -0
  22. package/parser/magicLinks.js +5 -4
  23. package/parser/quotes.js +2 -1
  24. package/parser/selector.js +177 -0
  25. package/parser/table.js +1 -0
  26. package/src/arg.js +116 -2
  27. package/src/atom/hidden.js +2 -0
  28. package/src/atom/index.js +17 -0
  29. package/src/attribute.js +181 -4
  30. package/src/attributes.js +308 -4
  31. package/src/charinsert.js +97 -0
  32. package/src/converter.js +108 -2
  33. package/src/converterFlags.js +187 -0
  34. package/src/converterRule.js +184 -1
  35. package/src/extLink.js +120 -1
  36. package/src/gallery.js +57 -6
  37. package/src/hasNowiki/index.js +12 -0
  38. package/src/hasNowiki/pre.js +12 -0
  39. package/src/heading.js +55 -4
  40. package/src/html.js +118 -3
  41. package/src/imageParameter.js +176 -5
  42. package/src/imagemap.js +60 -1
  43. package/src/imagemapLink.js +13 -1
  44. package/src/index.js +529 -3
  45. package/src/link/category.js +37 -1
  46. package/src/link/file.js +159 -2
  47. package/src/link/galleryImage.js +59 -1
  48. package/src/link/index.js +259 -1
  49. package/src/magicLink.js +90 -9
  50. package/src/nested/choose.js +1 -0
  51. package/src/nested/combobox.js +1 -0
  52. package/src/nested/index.js +30 -3
  53. package/src/nested/references.js +1 -0
  54. package/src/nowiki/comment.js +25 -1
  55. package/src/nowiki/dd.js +47 -1
  56. package/src/nowiki/doubleUnderscore.js +31 -1
  57. package/src/nowiki/hr.js +20 -1
  58. package/src/nowiki/index.js +23 -1
  59. package/src/nowiki/list.js +5 -2
  60. package/src/nowiki/noinclude.js +14 -0
  61. package/src/nowiki/quote.js +16 -2
  62. package/src/onlyinclude.js +26 -1
  63. package/src/paramTag/index.js +24 -1
  64. package/src/paramTag/inputbox.js +4 -1
  65. package/src/parameter.js +148 -6
  66. package/src/syntax.js +68 -0
  67. package/src/table/index.js +940 -2
  68. package/src/table/td.js +225 -5
  69. package/src/table/tr.js +247 -2
  70. package/src/tagPair/ext.js +24 -3
  71. package/src/tagPair/include.js +24 -0
  72. package/src/tagPair/index.js +51 -2
  73. package/src/transclude.js +512 -11
  74. package/tool/index.js +1202 -0
  75. package/util/debug.js +73 -0
  76. package/util/string.js +48 -1
  77. package/config/minimum.json +0 -142
package/src/extLink.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const Parser = require('..'),
4
+ {noWrap, normalizeSpace} = require('../util/string'),
4
5
  Token = require('.'),
5
6
  MagicLinkToken = require('./magicLink');
6
7
 
@@ -12,6 +13,38 @@ class ExtLinkToken extends Token {
12
13
  type = 'ext-link';
13
14
  #space;
14
15
 
16
+ /**
17
+ * 协议
18
+ * @this {{firstChild: MagicLinkToken}}
19
+ */
20
+ get protocol() {
21
+ return this.firstChild.protocol;
22
+ }
23
+
24
+ /** @this {{firstChild: MagicLinkToken}} */
25
+ set protocol(value) {
26
+ this.firstChild.protocol = value;
27
+ }
28
+
29
+ /**
30
+ * 和内链保持一致
31
+ * @this {{firstChild: MagicLinkToken}}
32
+ */
33
+ get link() {
34
+ return this.firstChild.link;
35
+ }
36
+
37
+ set link(url) {
38
+ this.setTarget(url);
39
+ }
40
+
41
+ /** 链接显示文字 */
42
+ get innerText() {
43
+ return this.length > 1
44
+ ? this.lastChild.text()
45
+ : `[${this.getRootNode().querySelectorAll('ext-link[childElementCount=1]').indexOf(this) + 1}]`;
46
+ }
47
+
15
48
  /**
16
49
  * @param {string} url 网址
17
50
  * @param {string} space 空白字符
@@ -20,29 +53,38 @@ class ExtLinkToken extends Token {
20
53
  */
21
54
  constructor(url, space, text, config = Parser.getConfig(), accum = []) {
22
55
  super(undefined, config, true, accum, {
56
+ MagicLinkToken: 0, Token: 1,
23
57
  });
24
58
  this.insertAt(new MagicLinkToken(url, true, config, accum));
25
59
  this.#space = space;
26
60
  if (text) {
27
61
  const inner = new Token(text, config, true, accum, {
62
+ 'Stage-7': ':', ConverterToken: ':',
28
63
  });
29
64
  inner.type = 'ext-link-text';
30
65
  this.insertAt(inner.setAttribute('stage', Parser.MAX_STAGE - 1));
31
66
  }
67
+ this.getAttribute('protectChildren')(0);
32
68
  }
33
69
 
34
70
  /**
35
71
  * @override
72
+ * @param {string} selector
36
73
  */
37
74
  toString(selector) {
38
- if (this.length === 1) {
75
+ if (selector && this.matches(selector)) {
76
+ return '';
77
+ } else if (this.length === 1) {
39
78
  return `[${super.toString(selector)}${this.#space}]`;
40
79
  }
80
+ this.#correct();
81
+ normalizeSpace(this.lastChild);
41
82
  return `[${super.toString(selector, this.#space)}]`;
42
83
  }
43
84
 
44
85
  /** @override */
45
86
  text() {
87
+ normalizeSpace(this.childNodes[1]);
46
88
  return `[${super.text(' ')}]`;
47
89
  }
48
90
 
@@ -53,8 +95,85 @@ class ExtLinkToken extends Token {
53
95
 
54
96
  /** @override */
55
97
  getGaps() {
98
+ this.#correct();
56
99
  return this.#space.length;
57
100
  }
101
+
102
+ /** @override */
103
+ print() {
104
+ const {length} = this;
105
+ return super.print(length > 1 ? {pre: '[', sep: this.#space, post: ']'} : {pre: '[', post: `${this.#space}]`});
106
+ }
107
+
108
+ /** @override */
109
+ cloneNode() {
110
+ const [url, text] = this.cloneChildNodes();
111
+ return Parser.run(() => {
112
+ const token = new ExtLinkToken(undefined, '', '', this.getAttribute('config'));
113
+ token.firstChild.safeReplaceWith(url);
114
+ if (text) {
115
+ token.insertAt(text);
116
+ }
117
+ return token;
118
+ });
119
+ }
120
+
121
+ /** 修正空白字符 */
122
+ #correct() {
123
+ if (!this.#space && this.length > 1
124
+ // 都替换成`<`肯定不对,但无妨
125
+ && /^[^[\]<>"{\0-\x1F\x7F\p{Zs}\uFFFD]/u.test(this.lastChild.text().replace(/&[lg]t;/u, '<'))
126
+ ) {
127
+ this.#space = ' ';
128
+ }
129
+ }
130
+
131
+ /**
132
+ * 获取网址
133
+ * @this {{firstChild: MagicLinkToken}}
134
+ */
135
+ getUrl() {
136
+ return this.firstChild.getUrl();
137
+ }
138
+
139
+ /**
140
+ * 设置链接目标
141
+ * @param {string|URL} url 网址
142
+ * @throws `SyntaxError` 非法的外链目标
143
+ */
144
+ setTarget(url) {
145
+ url = String(url);
146
+ const root = Parser.parse(`[${url}]`, this.getAttribute('include'), 8, this.getAttribute('config')),
147
+ {length, firstChild: extLink} = root;
148
+ if (length !== 1 || extLink.type !== 'ext-link' || extLink.length !== 1) {
149
+ throw new SyntaxError(`非法的外链目标:${url}`);
150
+ }
151
+ const {firstChild} = extLink;
152
+ extLink.destroy(true);
153
+ this.firstChild.safeReplaceWith(firstChild);
154
+ }
155
+
156
+ /**
157
+ * 设置链接显示文字
158
+ * @param {string} text 链接显示文字
159
+ * @throws `SyntaxError` 非法的链接显示文字
160
+ */
161
+ setLinkText(text) {
162
+ text = String(text);
163
+ const root = Parser.parse(`[//url ${text}]`, this.getAttribute('include'), 8, this.getAttribute('config')),
164
+ {length, firstChild: extLink} = root;
165
+ if (length !== 1 || extLink.type !== 'ext-link' || extLink.length !== 2) {
166
+ throw new SyntaxError(`非法的外链文字:${noWrap(text)}`);
167
+ }
168
+ const {lastChild} = extLink;
169
+ if (this.length === 1) {
170
+ this.insertAt(lastChild);
171
+ } else {
172
+ this.lastChild.safeReplaceWith(lastChild);
173
+ }
174
+ this.#space ||= ' ';
175
+ }
58
176
  }
59
177
 
178
+ Parser.classes.ExtLinkToken = __filename;
60
179
  module.exports = ExtLinkToken;
package/src/gallery.js CHANGED
@@ -13,24 +13,28 @@ 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
26
  super(undefined, config, true, accum, {
27
+ AstText: ':', GalleryImageToken: ':', HiddenToken: ':',
22
28
  });
23
- const /** @type {ParserConfig} */ newConfig = {...config, img: {...config.img}};
24
- for (const [k, v] of Object.entries(config.img)) {
25
- if (v === 'width') {
26
- delete newConfig.img[k];
27
- }
28
- }
29
+ const /** @type {ParserConfig} */ newConfig = {
30
+ ...config, img: Object.fromEntries(Object.entries(config.img).filter(([, param]) => param !== 'width')),
31
+ };
29
32
  for (const line of inner?.split('\n') ?? []) {
30
33
  const matches = /^([^|]+)(?:\|(.*))?/u.exec(line);
31
34
  if (!matches) {
32
35
  super.insertAt(line.trim()
33
36
  ? new HiddenToken(line, undefined, newConfig, [], {
37
+ AstText: ':',
34
38
  })
35
39
  : line);
36
40
  continue;
@@ -41,6 +45,7 @@ class GalleryToken extends Token {
41
45
  super.insertAt(new GalleryImageToken(file, alt, title, newConfig, accum));
42
46
  } else {
43
47
  super.insertAt(new HiddenToken(line, undefined, newConfig, [], {
48
+ AstText: ':',
44
49
  }));
45
50
  }
46
51
  }
@@ -48,6 +53,7 @@ class GalleryToken extends Token {
48
53
 
49
54
  /**
50
55
  * @override
56
+ * @param {string} selector
51
57
  */
52
58
  toString(selector) {
53
59
  return super.toString(selector, '\n');
@@ -63,6 +69,11 @@ class GalleryToken extends Token {
63
69
  return 1;
64
70
  }
65
71
 
72
+ /** @override */
73
+ print() {
74
+ return super.print({sep: '\n'});
75
+ }
76
+
66
77
  /**
67
78
  * @override
68
79
  * @param {number} start 起始位置
@@ -96,6 +107,46 @@ class GalleryToken extends Token {
96
107
  }
97
108
  return errors;
98
109
  }
110
+
111
+ /** @override */
112
+ cloneNode() {
113
+ const cloned = this.cloneChildNodes();
114
+ return Parser.run(() => {
115
+ const token = new GalleryToken(undefined, this.getAttribute('config'));
116
+ token.append(...cloned);
117
+ return token;
118
+ });
119
+ }
120
+
121
+ /**
122
+ * 插入图片
123
+ * @param {string} file 图片文件名
124
+ * @param {number} i 插入位置
125
+ * @throws `SyntaxError` 非法的文件名
126
+ */
127
+ insertImage(file, i = this.length) {
128
+ const title = this.normalizeTitle(file, 6, true, true);
129
+ if (title.valid) {
130
+ const token = Parser.run(() => new GalleryImageToken(file, undefined, title, this.getAttribute('config')));
131
+ return this.insertAt(token, i);
132
+ }
133
+ throw new SyntaxError(`非法的文件名:${file}`);
134
+ }
135
+
136
+ /**
137
+ * @override
138
+ * @template {string|Token} T
139
+ * @param {T} token 待插入的节点
140
+ * @param {number} i 插入位置
141
+ * @throws `RangeError` 插入不可见内容
142
+ */
143
+ insertAt(token, i = 0) {
144
+ if (typeof token === 'string' && token.trim() || token instanceof HiddenToken) {
145
+ throw new RangeError('请勿向图库中插入不可见内容!');
146
+ }
147
+ return super.insertAt(token, i);
148
+ }
99
149
  }
100
150
 
151
+ Parser.classes.GalleryToken = __filename;
101
152
  module.exports = GalleryToken;
@@ -24,9 +24,21 @@ class HasNowikiToken extends Token {
24
24
  },
25
25
  );
26
26
  super(wikitext, config, true, accum, {
27
+ AstText: ':', NoincludeToken: ':',
27
28
  });
28
29
  this.type = type;
29
30
  }
31
+
32
+ /** @override */
33
+ cloneNode() {
34
+ const cloned = this.cloneChildNodes();
35
+ return Parser.run(() => {
36
+ const token = new HasNowikiToken(undefined, this.type, this.getAttribute('config'));
37
+ token.append(...cloned);
38
+ return token;
39
+ });
40
+ }
30
41
  }
31
42
 
43
+ Parser.classes.HasNowikiToken = __filename;
32
44
  module.exports = HasNowikiToken;
@@ -17,12 +17,24 @@ class PreToken extends HasNowikiToken {
17
17
  constructor(wikitext, config = Parser.getConfig(), accum = []) {
18
18
  super(wikitext, 'ext-inner', config, accum);
19
19
  this.setAttribute('stage', Parser.MAX_STAGE - 1);
20
+ this.setAttribute('acceptable', {AstText: ':', NoincludeToken: ':', ConverterToken: ':'});
20
21
  }
21
22
 
22
23
  /** @override */
23
24
  isPlain() {
24
25
  return true;
25
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
+ }
26
37
  }
27
38
 
39
+ Parser.classes.PreToken = __filename;
28
40
  module.exports = PreToken;
package/src/heading.js CHANGED
@@ -1,6 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  const {generateForSelf} = require('../util/lint'),
4
+ fixedToken = require('../mixin/fixedToken'),
5
+ sol = require('../mixin/sol'),
4
6
  Parser = require('..'),
5
7
  Token = require('.'),
6
8
  SyntaxToken = require('./syntax');
@@ -9,9 +11,14 @@ const {generateForSelf} = require('../util/lint'),
9
11
  * 章节标题
10
12
  * @classdesc `{childNodes: [Token, SyntaxToken]}`
11
13
  */
12
- class HeadingToken extends Token {
14
+ class HeadingToken extends fixedToken(sol(Token)) {
13
15
  type = 'heading';
14
16
 
17
+ /** 内部wikitext */
18
+ get innerText() {
19
+ return this.firstChild.text();
20
+ }
21
+
15
22
  /**
16
23
  * @param {number} level 标题层级
17
24
  * @param {string[]} input 标题文字
@@ -22,28 +29,37 @@ class HeadingToken extends Token {
22
29
  this.setAttribute('name', String(level));
23
30
  const token = new Token(input[0], config, true, accum);
24
31
  token.type = 'heading-title';
32
+ token.setAttribute('name', this.name);
25
33
  token.setAttribute('stage', 2);
26
34
  const trail = new SyntaxToken(input[1], /^[^\S\n]*$/u, 'heading-trail', config, accum, {
35
+ 'Stage-1': ':', '!ExtToken': '',
27
36
  });
28
37
  this.append(token, trail);
29
38
  }
30
39
 
31
40
  /**
32
41
  * @override
42
+ * @this {{prependNewLine(): ''|'\n', appendNewLine(): ''|'\n'} & HeadingToken}
43
+ * @param {string} selector
33
44
  * @returns {string}
34
45
  */
35
46
  toString(selector) {
36
47
  const equals = '='.repeat(Number(this.name));
37
- return `${equals}${this.firstChild.toString()}${equals}${this.lastChild.toString()}`;
48
+ return selector && this.matches(selector)
49
+ ? ''
50
+ : `${this.prependNewLine()}${equals}${
51
+ this.firstChild.toString(selector)
52
+ }${equals}${this.lastChild.toString(selector)}${this.appendNewLine()}`;
38
53
  }
39
54
 
40
55
  /**
41
56
  * @override
57
+ * @this {HeadingToken & {prependNewLine(): ''|'\n', appendNewLine(): ''|'\n'}}
42
58
  * @returns {string}
43
59
  */
44
60
  text() {
45
61
  const equals = '='.repeat(Number(this.name));
46
- return `${equals}${this.firstChild.text()}${equals}`;
62
+ return `${this.prependNewLine()}${equals}${this.firstChild.text()}${equals}${this.appendNewLine()}`;
47
63
  }
48
64
 
49
65
  /** @override */
@@ -56,6 +72,12 @@ class HeadingToken extends Token {
56
72
  return Number(this.name);
57
73
  }
58
74
 
75
+ /** @override */
76
+ print() {
77
+ const equals = '='.repeat(Number(this.name));
78
+ return super.print({pre: equals, sep: equals});
79
+ }
80
+
59
81
  /**
60
82
  * @override
61
83
  * @param {number} start 起始位置
@@ -68,7 +90,7 @@ class HeadingToken extends Token {
68
90
  refError = generateForSelf(this, {start}, '<h1>');
69
91
  errors.push(refError);
70
92
  }
71
- if (innerText[0] === '=' || innerText.endsWith('=')) {
93
+ if (innerText[0] === '=' || innerText.at(-1) === '=') {
72
94
  refError ||= generateForSelf(this, {start}, '');
73
95
  errors.push({...refError, message: '段落标题中不平衡的"="'});
74
96
  }
@@ -78,6 +100,35 @@ class HeadingToken extends Token {
78
100
  }
79
101
  return errors;
80
102
  }
103
+
104
+ /** @override */
105
+ cloneNode() {
106
+ const [title, trail] = this.cloneChildNodes();
107
+ return Parser.run(() => {
108
+ const token = new HeadingToken(Number(this.name), [], this.getAttribute('config'));
109
+ token.firsthild.safeReplaceWith(title);
110
+ token.lastChild.safeReplaceWith(trail);
111
+ return token;
112
+ });
113
+ }
114
+
115
+ /**
116
+ * 设置标题层级
117
+ * @param {number} n 标题层级
118
+ */
119
+ setLevel(n) {
120
+ if (!Number.isInteger(n)) {
121
+ this.typeError('setLevel', 'Number');
122
+ }
123
+ n = Math.min(Math.max(n, 1), 6);
124
+ this.setAttribute('name', String(n)).firstChild.setAttribute('name', this.name);
125
+ }
126
+
127
+ /** 移除标题后的不可见内容 */
128
+ removeTrail() {
129
+ this.lastChild.replaceChildren();
130
+ }
81
131
  }
82
132
 
133
+ Parser.classes.HeadingToken = __filename;
83
134
  module.exports = HeadingToken;
package/src/html.js CHANGED
@@ -1,6 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  const {generateForSelf} = require('../util/lint'),
4
+ {noWrap} = require('../util/string'),
5
+ fixedToken = require('../mixin/fixedToken'),
6
+ attributeParent = require('../mixin/attributeParent'),
4
7
  Parser = require('..'),
5
8
  Token = require('.');
6
9
 
@@ -8,7 +11,7 @@ const {generateForSelf} = require('../util/lint'),
8
11
  * HTML标签
9
12
  * @classdesc `{childNodes: [AttributesToken]}`
10
13
  */
11
- class HtmlToken extends Token {
14
+ class HtmlToken extends attributeParent(fixedToken(Token)) {
12
15
  type = 'html';
13
16
  #closing;
14
17
  #selfClosing;
@@ -19,6 +22,41 @@ class HtmlToken extends Token {
19
22
  return this.#closing;
20
23
  }
21
24
 
25
+ /** @throws `Error` 自闭合标签或空标签 */
26
+ set closing(value) {
27
+ if (!value) {
28
+ this.#closing = false;
29
+ return;
30
+ } else if (this.#selfClosing) {
31
+ throw new Error('这是一个自闭合标签!');
32
+ }
33
+ const {html: [,, tags]} = this.getAttribute('config');
34
+ if (tags.includes(this.name)) {
35
+ throw new Error('这是一个空标签!');
36
+ }
37
+ this.#closing = true;
38
+ }
39
+
40
+ /** getter */
41
+ get selfClosing() {
42
+ return this.#selfClosing;
43
+ }
44
+
45
+ /** @throws `Error` 闭合标签或无效自闭合标签 */
46
+ set selfClosing(value) {
47
+ if (!value) {
48
+ this.#selfClosing = false;
49
+ return;
50
+ } else if (this.#closing) {
51
+ throw new Error('这是一个闭合标签!');
52
+ }
53
+ const {html: [tags]} = this.getAttribute('config');
54
+ if (tags.includes(this.name)) {
55
+ throw new Error(`<${this.name}>标签自闭合无效!`);
56
+ }
57
+ this.#selfClosing = true;
58
+ }
59
+
22
60
  /**
23
61
  * @param {string} name 标签名
24
62
  * @param {AttributesToken} attr 标签属性
@@ -37,9 +75,12 @@ class HtmlToken extends Token {
37
75
 
38
76
  /**
39
77
  * @override
78
+ * @param {string} selector
40
79
  */
41
80
  toString(selector) {
42
- return `<${this.#closing ? '/' : ''}${this.#tag}${super.toString()}${this.#selfClosing ? '/' : ''}>`;
81
+ return selector && this.matches(selector)
82
+ ? ''
83
+ : `<${this.#closing ? '/' : ''}${this.#tag}${super.toString(selector)}${this.#selfClosing ? '/' : ''}>`;
43
84
  }
44
85
 
45
86
  /** @override */
@@ -54,6 +95,14 @@ class HtmlToken extends Token {
54
95
  return this.#tag.length + (this.#closing ? 2 : 1);
55
96
  }
56
97
 
98
+ /** @override */
99
+ print() {
100
+ return super.print({
101
+ pre: `&lt;${this.#closing ? '/' : ''}${this.#tag}`,
102
+ post: `${this.#selfClosing ? '/' : ''}&gt;`,
103
+ });
104
+ }
105
+
57
106
  /**
58
107
  * @override
59
108
  * @param {number} start 起始位置
@@ -100,7 +149,7 @@ class HtmlToken extends Token {
100
149
  findMatchingTag() {
101
150
  const {html} = this.getAttribute('config'),
102
151
  {name: tagName, parentNode} = this,
103
- string = String(this);
152
+ string = noWrap(String(this));
104
153
  if (this.#closing && (this.#selfClosing || html[2].includes(tagName))) {
105
154
  throw new SyntaxError(`同时闭合和自封闭的标签:${string}`);
106
155
  } else if (html[2].includes(tagName) || this.#selfClosing && html[1].includes(tagName)) { // 自封闭标签
@@ -128,6 +177,72 @@ class HtmlToken extends Token {
128
177
  }
129
178
  throw new SyntaxError(`未${this.#closing ? '匹配的闭合' : '闭合的'}标签:${string}`);
130
179
  }
180
+
181
+ /** @override */
182
+ cloneNode() {
183
+ const [attr] = this.cloneChildNodes(),
184
+ config = this.getAttribute('config');
185
+ return Parser.run(() => new HtmlToken(this.#tag, attr, this.#closing, this.#selfClosing, config));
186
+ }
187
+
188
+ /**
189
+ * @override
190
+ * @template {string} T
191
+ * @param {T} key 属性键
192
+ * @returns {TokenAttribute<T>}
193
+ */
194
+ getAttribute(key) {
195
+ return key === 'tag' ? this.#tag : super.getAttribute(key);
196
+ }
197
+
198
+ /**
199
+ * 更换标签名
200
+ * @param {string} tag 标签名
201
+ * @throws `RangeError` 非法的HTML标签
202
+ */
203
+ replaceTag(tag) {
204
+ const name = tag.toLowerCase();
205
+ if (!this.getAttribute('config').html.flat().includes(name)) {
206
+ throw new RangeError(`非法的HTML标签:${tag}`);
207
+ }
208
+ this.setAttribute('name', name).#tag = tag;
209
+ }
210
+
211
+ /** 局部闭合 */
212
+ #localMatch() {
213
+ this.#selfClosing = false;
214
+ const root = Parser.parse(`</${this.name}>`, false, 3, this.getAttribute('config'));
215
+ this.after(root.firstChild);
216
+ }
217
+
218
+ /**
219
+ * 修复无效自封闭标签
220
+ * @complexity `n`
221
+ * @throws `Error` 无法修复无效自封闭标签
222
+ */
223
+ fix() {
224
+ const config = this.getAttribute('config'),
225
+ {parentNode, name: tagName, firstChild} = this;
226
+ if (!parentNode || !this.#selfClosing || !config.html[0].includes(tagName)) {
227
+ return;
228
+ } else if (firstChild.text().trim()) {
229
+ this.#localMatch();
230
+ return;
231
+ }
232
+ const {childNodes} = parentNode,
233
+ i = childNodes.indexOf(this),
234
+ /** @type {HtmlToken[]} */
235
+ prevSiblings = childNodes.slice(0, i).filter(({type, name}) => type === 'html' && name === tagName),
236
+ imbalance = prevSiblings.reduce((acc, {closing}) => acc + (closing ? 1 : -1), 0);
237
+ if (imbalance < 0) {
238
+ this.#selfClosing = false;
239
+ this.#closing = true;
240
+ } else {
241
+ Parser.warn('无法修复无效自封闭标签', noWrap(String(this)));
242
+ throw new Error(`无法修复无效自封闭标签:前文共有 ${imbalance} 个未匹配的闭合标签`);
243
+ }
244
+ }
131
245
  }
132
246
 
247
+ Parser.classes.HtmlToken = __filename;
133
248
  module.exports = HtmlToken;