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