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.
- package/README.md +39 -0
- package/config/moegirl.json +1 -0
- package/i18n/zh-hans.json +44 -0
- package/i18n/zh-hant.json +44 -0
- package/index.js +264 -10
- package/lib/element.js +507 -33
- package/lib/node.js +550 -6
- package/lib/ranges.js +130 -0
- package/lib/text.js +111 -41
- package/lib/title.js +28 -5
- package/mixin/attributeParent.js +117 -0
- package/mixin/fixedToken.js +40 -0
- package/mixin/hidden.js +3 -0
- package/mixin/singleLine.js +31 -0
- package/mixin/sol.js +54 -0
- package/package.json +9 -8
- package/parser/brackets.js +9 -2
- package/parser/commentAndExt.js +3 -5
- package/parser/converter.js +1 -0
- package/parser/externalLinks.js +1 -0
- package/parser/hrAndDoubleUnderscore.js +1 -0
- package/parser/html.js +1 -0
- package/parser/links.js +6 -5
- package/parser/list.js +1 -0
- package/parser/magicLinks.js +5 -4
- package/parser/quotes.js +1 -0
- package/parser/selector.js +177 -0
- package/parser/table.js +1 -0
- package/src/arg.js +123 -5
- package/src/atom/hidden.js +2 -0
- package/src/atom/index.js +17 -0
- package/src/attribute.js +191 -8
- package/src/attributes.js +311 -8
- package/src/charinsert.js +97 -0
- package/src/converter.js +108 -2
- package/src/converterFlags.js +190 -3
- package/src/converterRule.js +185 -4
- package/src/extLink.js +122 -2
- package/src/gallery.js +59 -11
- package/src/hasNowiki/index.js +12 -0
- package/src/hasNowiki/pre.js +12 -0
- package/src/heading.js +57 -6
- package/src/html.js +133 -12
- package/src/imageParameter.js +232 -38
- package/src/imagemap.js +65 -6
- package/src/imagemapLink.js +14 -2
- package/src/index.js +537 -8
- package/src/link/category.js +32 -1
- package/src/link/file.js +173 -11
- package/src/link/galleryImage.js +63 -5
- package/src/link/index.js +268 -9
- package/src/magicLink.js +92 -11
- package/src/nested/choose.js +1 -0
- package/src/nested/combobox.js +1 -0
- package/src/nested/index.js +31 -7
- package/src/nested/references.js +1 -0
- package/src/nowiki/comment.js +27 -3
- package/src/nowiki/dd.js +47 -1
- package/src/nowiki/doubleUnderscore.js +31 -1
- package/src/nowiki/hr.js +20 -1
- package/src/nowiki/index.js +25 -3
- package/src/nowiki/list.js +5 -2
- package/src/nowiki/noinclude.js +14 -0
- package/src/nowiki/quote.js +17 -3
- package/src/onlyinclude.js +26 -1
- package/src/paramTag/index.js +27 -4
- package/src/paramTag/inputbox.js +4 -1
- package/src/parameter.js +150 -8
- package/src/syntax.js +68 -0
- package/src/table/index.js +941 -4
- package/src/table/td.js +229 -10
- package/src/table/tr.js +249 -4
- package/src/tagPair/ext.js +36 -9
- package/src/tagPair/include.js +24 -0
- package/src/tagPair/index.js +51 -2
- package/src/transclude.js +547 -29
- package/tool/index.js +1202 -0
- package/util/debug.js +73 -0
- package/util/lint.js +8 -7
- package/util/string.js +67 -1
- 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.
|
|
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,
|
|
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,
|
|
42
|
+
super.insertAt(new GalleryImageToken(file, alt, config, accum));
|
|
42
43
|
} else {
|
|
43
|
-
super.insertAt(new HiddenToken(line, undefined,
|
|
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 =
|
|
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;
|
package/src/hasNowiki/index.js
CHANGED
|
@@ -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;
|
package/src/hasNowiki/pre.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
|
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: `<${this.#closing ? '/' : ''}${this.#tag}`,
|
|
104
|
+
post: `${this.#selfClosing ? '/' : ''}>`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
57
108
|
/**
|
|
58
109
|
* @override
|
|
59
110
|
* @param {number} start 起始位置
|
|
60
111
|
*/
|
|
61
|
-
lint(start =
|
|
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: '
|
|
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 [
|
|
81
|
-
error = {...refError, message
|
|
82
|
-
if (
|
|
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 (
|
|
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(
|
|
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(
|
|
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(
|
|
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;
|