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.
- package/README.md +39 -0
- package/index.js +253 -11
- package/lib/element.js +481 -7
- package/lib/node.js +552 -6
- package/lib/ranges.js +130 -0
- package/lib/text.js +108 -16
- package/lib/title.js +21 -0
- 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 +65 -0
- package/package.json +5 -4
- package/parser/brackets.js +1 -0
- package/parser/commentAndExt.js +4 -3
- 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 +5 -4
- package/parser/list.js +1 -0
- package/parser/magicLinks.js +5 -4
- package/parser/quotes.js +2 -1
- package/parser/selector.js +177 -0
- package/parser/table.js +1 -0
- package/src/arg.js +116 -2
- package/src/atom/hidden.js +2 -0
- package/src/atom/index.js +17 -0
- package/src/attribute.js +181 -4
- package/src/attributes.js +308 -4
- package/src/charinsert.js +97 -0
- package/src/converter.js +108 -2
- package/src/converterFlags.js +187 -0
- package/src/converterRule.js +184 -1
- package/src/extLink.js +120 -1
- package/src/gallery.js +57 -6
- package/src/hasNowiki/index.js +12 -0
- package/src/hasNowiki/pre.js +12 -0
- package/src/heading.js +55 -4
- package/src/html.js +118 -3
- package/src/imageParameter.js +176 -5
- package/src/imagemap.js +60 -1
- package/src/imagemapLink.js +13 -1
- package/src/index.js +529 -3
- package/src/link/category.js +37 -1
- package/src/link/file.js +159 -2
- package/src/link/galleryImage.js +59 -1
- package/src/link/index.js +259 -1
- package/src/magicLink.js +90 -9
- package/src/nested/choose.js +1 -0
- package/src/nested/combobox.js +1 -0
- package/src/nested/index.js +30 -3
- package/src/nested/references.js +1 -0
- package/src/nowiki/comment.js +25 -1
- 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 +23 -1
- package/src/nowiki/list.js +5 -2
- package/src/nowiki/noinclude.js +14 -0
- package/src/nowiki/quote.js +16 -2
- package/src/onlyinclude.js +26 -1
- package/src/paramTag/index.js +24 -1
- package/src/paramTag/inputbox.js +4 -1
- package/src/parameter.js +148 -6
- package/src/syntax.js +68 -0
- package/src/table/index.js +940 -2
- package/src/table/td.js +225 -5
- package/src/table/tr.js +247 -2
- package/src/tagPair/ext.js +24 -3
- package/src/tagPair/include.js +24 -0
- package/src/tagPair/index.js +51 -2
- package/src/transclude.js +512 -11
- package/tool/index.js +1202 -0
- package/util/debug.js +73 -0
- package/util/string.js +48 -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,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.
|
|
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 = {
|
|
24
|
-
|
|
25
|
-
|
|
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;
|
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', 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
|
|
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.
|
|
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
|
|
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: `<${this.#closing ? '/' : ''}${this.#tag}`,
|
|
102
|
+
post: `${this.#selfClosing ? '/' : ''}>`,
|
|
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;
|