wikiparser-node 0.3.1 → 0.4.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/.eslintrc.json +446 -51
- package/README.md +1 -1
- package/config/default.json +13 -17
- package/config/llwiki.json +11 -79
- package/config/moegirl.json +7 -1
- package/config/zhwiki.json +1269 -0
- package/index.js +106 -96
- package/lib/element.js +448 -440
- package/lib/node.js +335 -115
- package/lib/ranges.js +27 -18
- package/lib/text.js +146 -0
- package/lib/title.js +13 -5
- package/mixin/attributeParent.js +70 -24
- package/mixin/fixedToken.js +14 -6
- package/mixin/hidden.js +6 -4
- package/mixin/sol.js +27 -10
- package/package.json +7 -4
- package/parser/brackets.js +18 -18
- package/parser/commentAndExt.js +15 -13
- package/parser/converter.js +14 -13
- package/parser/externalLinks.js +12 -11
- package/parser/hrAndDoubleUnderscore.js +23 -14
- package/parser/html.js +8 -7
- package/parser/links.js +13 -12
- package/parser/list.js +12 -11
- package/parser/magicLinks.js +11 -10
- package/parser/quotes.js +6 -5
- package/parser/selector.js +175 -0
- package/parser/table.js +24 -17
- package/printed/example.json +120 -0
- package/src/arg.js +56 -32
- package/src/atom/hidden.js +5 -2
- package/src/atom/index.js +17 -9
- package/src/attribute.js +180 -98
- package/src/converter.js +68 -41
- package/src/converterFlags.js +63 -41
- package/src/converterRule.js +117 -65
- package/src/extLink.js +66 -18
- package/src/gallery.js +28 -16
- package/src/heading.js +33 -14
- package/src/html.js +97 -35
- package/src/imageParameter.js +82 -53
- package/src/index.js +296 -175
- package/src/link/category.js +20 -52
- package/src/link/file.js +59 -28
- package/src/link/galleryImage.js +21 -7
- package/src/link/index.js +143 -57
- package/src/magicLink.js +33 -11
- package/src/nowiki/comment.js +22 -10
- package/src/nowiki/dd.js +37 -22
- package/src/nowiki/doubleUnderscore.js +16 -7
- package/src/nowiki/hr.js +11 -7
- package/src/nowiki/index.js +16 -9
- package/src/nowiki/list.js +2 -2
- package/src/nowiki/noinclude.js +8 -4
- package/src/nowiki/quote.js +11 -7
- package/src/onlyinclude.js +19 -7
- package/src/parameter.js +65 -38
- package/src/syntax.js +23 -20
- package/src/table/index.js +260 -165
- package/src/table/td.js +97 -52
- package/src/table/tr.js +102 -58
- package/src/tagPair/ext.js +27 -19
- package/src/tagPair/include.js +16 -11
- package/src/tagPair/index.js +64 -29
- package/src/transclude.js +167 -92
- package/test/api.js +83 -0
- package/test/real.js +133 -0
- package/test/test.js +28 -0
- package/test/util.js +80 -0
- package/tool/index.js +41 -31
- package/typings/api.d.ts +13 -0
- package/typings/array.d.ts +28 -0
- package/typings/event.d.ts +24 -0
- package/typings/index.d.ts +46 -4
- package/typings/node.d.ts +15 -9
- package/typings/parser.d.ts +7 -0
- package/typings/tool.d.ts +3 -2
- package/util/debug.js +21 -18
- package/util/string.js +38 -25
- package/typings/element.d.ts +0 -28
package/src/gallery.js
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
/** @type {Parser} */ Parser = require('..'),
|
|
3
|
+
const Parser = require('..'),
|
|
5
4
|
Token = require('.'),
|
|
6
|
-
GalleryImageToken = require('./link/galleryImage')
|
|
5
|
+
GalleryImageToken = require('./link/galleryImage'),
|
|
6
|
+
HiddenToken = require('./atom/hidden');
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* gallery标签
|
|
10
|
-
* @classdesc `{childNodes: (
|
|
10
|
+
* @classdesc `{childNodes: ...(GalleryImageToken|HiddenToken|AstText)}`
|
|
11
11
|
*/
|
|
12
12
|
class GalleryToken extends Token {
|
|
13
13
|
type = 'ext-inner';
|
|
14
14
|
name = 'gallery';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* @param {string} inner
|
|
17
|
+
* @param {string} inner 标签内部wikitext
|
|
18
18
|
* @param {accum} accum
|
|
19
19
|
*/
|
|
20
20
|
constructor(inner, config = Parser.getConfig(), accum = []) {
|
|
21
|
-
super(undefined, config, true, accum, {
|
|
21
|
+
super(undefined, config, true, accum, {AstText: ':', GalleryImageToken: ':'});
|
|
22
22
|
for (const line of inner?.split('\n') ?? []) {
|
|
23
|
-
const matches = /^([^|]+)(?:\|(.*))
|
|
23
|
+
const matches = /^([^|]+)(?:\|(.*))?/u.exec(line);
|
|
24
24
|
if (!matches) {
|
|
25
|
-
this.appendChild(line);
|
|
25
|
+
this.appendChild(line.trim() ? new HiddenToken(line, undefined, config, [], {AstText: ':'}) : line);
|
|
26
26
|
continue;
|
|
27
27
|
}
|
|
28
28
|
const [, file, alt] = matches;
|
|
@@ -32,34 +32,46 @@ class GalleryToken extends Token {
|
|
|
32
32
|
} catch {
|
|
33
33
|
title = this.normalizeTitle(file, 6, true);
|
|
34
34
|
}
|
|
35
|
-
if (
|
|
36
|
-
this.appendChild(line);
|
|
37
|
-
} else {
|
|
35
|
+
if (title.valid) {
|
|
38
36
|
this.appendChild(new GalleryImageToken(file, alt, title, config, accum));
|
|
37
|
+
} else {
|
|
38
|
+
this.appendChild(new HiddenToken(line, undefined, config, [], {AstText: ':'}));
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** @override */
|
|
43
44
|
cloneNode() {
|
|
44
|
-
const cloned = this.
|
|
45
|
+
const cloned = this.cloneChildNodes(),
|
|
45
46
|
token = Parser.run(() => new GalleryToken(undefined, this.getAttribute('config')));
|
|
46
47
|
token.append(...cloned);
|
|
47
48
|
return token;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
/**
|
|
52
|
+
* @override
|
|
53
|
+
* @param {string} selector
|
|
54
|
+
*/
|
|
55
|
+
toString(selector) {
|
|
56
|
+
return super.toString(selector, '\n');
|
|
52
57
|
}
|
|
53
58
|
|
|
59
|
+
/** @override */
|
|
54
60
|
getGaps() {
|
|
55
61
|
return 1;
|
|
56
62
|
}
|
|
57
63
|
|
|
64
|
+
/** @override */
|
|
58
65
|
text() {
|
|
59
|
-
return text(
|
|
66
|
+
return super.text('\n').replaceAll(/\n\s*\n/gu, '\n');
|
|
60
67
|
}
|
|
61
68
|
|
|
62
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* 插入图片
|
|
71
|
+
* @param {string} file 图片文件名
|
|
72
|
+
* @param {number} i 插入位置
|
|
73
|
+
* @throws `SyntaxError` 非法的文件名
|
|
74
|
+
*/
|
|
63
75
|
insertImage(file, i = this.childNodes.length) {
|
|
64
76
|
let title;
|
|
65
77
|
try {
|
package/src/heading.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fixedToken = require('../mixin/fixedToken'),
|
|
4
4
|
sol = require('../mixin/sol'),
|
|
5
|
-
|
|
5
|
+
Parser = require('..'),
|
|
6
6
|
Token = require('.');
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -12,9 +12,14 @@ const fixedToken = require('../mixin/fixedToken'),
|
|
|
12
12
|
class HeadingToken extends fixedToken(sol(Token)) {
|
|
13
13
|
type = 'heading';
|
|
14
14
|
|
|
15
|
+
/** 内部wikitext */
|
|
16
|
+
get innerText() {
|
|
17
|
+
return this.firstElementChild.text();
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
/**
|
|
16
|
-
* @param {number} level
|
|
17
|
-
* @param {string[]} input
|
|
21
|
+
* @param {number} level 标题层级
|
|
22
|
+
* @param {string[]} input 标题文字
|
|
18
23
|
* @param {accum} accum
|
|
19
24
|
*/
|
|
20
25
|
constructor(level, input, config = Parser.getConfig(), accum = []) {
|
|
@@ -23,38 +28,48 @@ class HeadingToken extends fixedToken(sol(Token)) {
|
|
|
23
28
|
const token = new Token(input[0], config, true, accum);
|
|
24
29
|
token.type = 'heading-title';
|
|
25
30
|
token.setAttribute('name', this.name).setAttribute('stage', 2);
|
|
26
|
-
const SyntaxToken = require('./syntax')
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
const SyntaxToken = require('./syntax');
|
|
32
|
+
const trail = new SyntaxToken(input[1], /^[^\S\n]*$/u, 'heading-trail', config, accum, {
|
|
33
|
+
'Stage-1': ':', '!ExtToken': '',
|
|
34
|
+
});
|
|
30
35
|
this.append(token, trail);
|
|
31
36
|
}
|
|
32
37
|
|
|
38
|
+
/** @override */
|
|
33
39
|
cloneNode() {
|
|
34
|
-
const [title, trail] = this.
|
|
40
|
+
const [title, trail] = this.cloneChildNodes(),
|
|
35
41
|
token = Parser.run(() => new HeadingToken(Number(this.name), [], this.getAttribute('config')));
|
|
36
42
|
token.firstElementChild.safeReplaceWith(title);
|
|
37
43
|
token.lastElementChild.safeReplaceWith(trail);
|
|
38
44
|
return token;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
47
|
+
/**
|
|
48
|
+
* @override
|
|
49
|
+
* @this {{prependNewLine(): ''|'\n', appendNewLine(): ''|'\n'} & HeadingToken}
|
|
50
|
+
* @param {string} selector
|
|
51
|
+
*/
|
|
52
|
+
toString(selector) {
|
|
43
53
|
const equals = '='.repeat(Number(this.name));
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
return selector && this.matches(selector)
|
|
55
|
+
? ''
|
|
56
|
+
: `${this.prependNewLine()}${equals}${
|
|
57
|
+
this.firstElementChild.toString(selector)
|
|
58
|
+
}${equals}${this.lastElementChild.toString(selector)}${this.appendNewLine()}`;
|
|
47
59
|
}
|
|
48
60
|
|
|
61
|
+
/** @override */
|
|
49
62
|
getPadding() {
|
|
50
63
|
return super.getPadding() + Number(this.name);
|
|
51
64
|
}
|
|
52
65
|
|
|
66
|
+
/** @override */
|
|
53
67
|
getGaps() {
|
|
54
68
|
return Number(this.name);
|
|
55
69
|
}
|
|
56
70
|
|
|
57
71
|
/**
|
|
72
|
+
* @override
|
|
58
73
|
* @this {HeadingToken & {prependNewLine(): ''|'\n', appendNewLine(): ''|'\n'}}
|
|
59
74
|
* @returns {string}
|
|
60
75
|
*/
|
|
@@ -63,7 +78,10 @@ class HeadingToken extends fixedToken(sol(Token)) {
|
|
|
63
78
|
return `${this.prependNewLine()}${equals}${this.firstElementChild.text()}${equals}${this.appendNewLine()}`;
|
|
64
79
|
}
|
|
65
80
|
|
|
66
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* 设置标题层级
|
|
83
|
+
* @param {number} n 标题层级
|
|
84
|
+
*/
|
|
67
85
|
setLevel(n) {
|
|
68
86
|
if (typeof n !== 'number') {
|
|
69
87
|
this.typeError('setLevel', 'Number');
|
|
@@ -72,6 +90,7 @@ class HeadingToken extends fixedToken(sol(Token)) {
|
|
|
72
90
|
this.setAttribute('name', String(n)).firstElementChild.setAttribute('name', this.name);
|
|
73
91
|
}
|
|
74
92
|
|
|
93
|
+
/** 移除标题后的不可见内容 */
|
|
75
94
|
removeTrail() {
|
|
76
95
|
this.lastElementChild.replaceChildren();
|
|
77
96
|
}
|
package/src/html.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const {noWrap} = require('../util/string'),
|
|
4
4
|
fixedToken = require('../mixin/fixedToken'),
|
|
5
5
|
attributeParent = require('../mixin/attributeParent'),
|
|
6
|
-
|
|
6
|
+
Parser = require('..'),
|
|
7
7
|
Token = require('.');
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -12,57 +12,108 @@ const {noWrap} = require('../util/string'),
|
|
|
12
12
|
*/
|
|
13
13
|
class HtmlToken extends attributeParent(fixedToken(Token)) {
|
|
14
14
|
type = 'html';
|
|
15
|
-
closing;
|
|
16
|
-
selfClosing;
|
|
15
|
+
#closing;
|
|
16
|
+
#selfClosing;
|
|
17
17
|
#tag;
|
|
18
18
|
|
|
19
|
+
/** getter */
|
|
20
|
+
get closing() {
|
|
21
|
+
return this.#closing;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @throws `Error` 自闭合标签或空标签 */
|
|
25
|
+
set closing(value) {
|
|
26
|
+
if (!value) {
|
|
27
|
+
this.#closing = false;
|
|
28
|
+
return;
|
|
29
|
+
} else if (this.#selfClosing) {
|
|
30
|
+
throw new Error(`这是一个自闭合标签!`);
|
|
31
|
+
}
|
|
32
|
+
const {html: [,, tags]} = this.getAttribute('config');
|
|
33
|
+
if (tags.includes(this.name)) {
|
|
34
|
+
throw new Error(`这是一个空标签!`);
|
|
35
|
+
}
|
|
36
|
+
this.#closing = true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** getter */
|
|
40
|
+
get selfClosing() {
|
|
41
|
+
return this.#selfClosing;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @throws `Error` 闭合标签或无效自闭合标签 */
|
|
45
|
+
set selfClosing(value) {
|
|
46
|
+
if (!value) {
|
|
47
|
+
this.#selfClosing = false;
|
|
48
|
+
return;
|
|
49
|
+
} else if (this.#closing) {
|
|
50
|
+
throw new Error('这是一个闭合标签!');
|
|
51
|
+
}
|
|
52
|
+
const {html: [tags]} = this.getAttribute('config');
|
|
53
|
+
if (tags.includes(this.name)) {
|
|
54
|
+
throw new Error(`<${this.name}>标签自闭合无效!`);
|
|
55
|
+
}
|
|
56
|
+
this.#selfClosing = true;
|
|
57
|
+
}
|
|
58
|
+
|
|
19
59
|
/**
|
|
20
|
-
* @param {string} name
|
|
21
|
-
* @param {AttributeToken} attr
|
|
22
|
-
* @param {boolean} closing
|
|
23
|
-
* @param {boolean} selfClosing
|
|
60
|
+
* @param {string} name 标签名
|
|
61
|
+
* @param {AttributeToken} attr 标签属性
|
|
62
|
+
* @param {boolean} closing 是否闭合
|
|
63
|
+
* @param {boolean} selfClosing 是否自封闭
|
|
24
64
|
* @param {accum} accum
|
|
25
65
|
*/
|
|
26
66
|
constructor(name, attr, closing, selfClosing, config = Parser.getConfig(), accum = []) {
|
|
27
67
|
super(undefined, config, true, accum);
|
|
28
68
|
this.appendChild(attr);
|
|
29
69
|
this.setAttribute('name', name.toLowerCase());
|
|
30
|
-
this
|
|
31
|
-
this
|
|
70
|
+
this.#closing = closing;
|
|
71
|
+
this.#selfClosing = selfClosing;
|
|
32
72
|
this.#tag = name;
|
|
33
73
|
}
|
|
34
74
|
|
|
75
|
+
/** @override */
|
|
35
76
|
cloneNode() {
|
|
36
|
-
const [attr] = this.
|
|
77
|
+
const [attr] = this.cloneChildNodes(),
|
|
37
78
|
config = this.getAttribute('config');
|
|
38
|
-
return Parser.run(() => new HtmlToken(this.#tag, attr, this
|
|
79
|
+
return Parser.run(() => new HtmlToken(this.#tag, attr, this.#closing, this.#selfClosing, config));
|
|
39
80
|
}
|
|
40
81
|
|
|
41
82
|
/**
|
|
83
|
+
* @override
|
|
42
84
|
* @template {string} T
|
|
43
|
-
* @param {T} key
|
|
85
|
+
* @param {T} key 属性键
|
|
44
86
|
* @returns {TokenAttribute<T>}
|
|
45
87
|
*/
|
|
46
88
|
getAttribute(key) {
|
|
47
|
-
|
|
48
|
-
return this.#tag;
|
|
49
|
-
}
|
|
50
|
-
return super.getAttribute(key);
|
|
89
|
+
return key === 'tag' ? this.#tag : super.getAttribute(key);
|
|
51
90
|
}
|
|
52
91
|
|
|
53
|
-
|
|
54
|
-
|
|
92
|
+
/**
|
|
93
|
+
* @override
|
|
94
|
+
* @param {string} selector
|
|
95
|
+
*/
|
|
96
|
+
toString(selector) {
|
|
97
|
+
return selector && this.matches(selector)
|
|
98
|
+
? ''
|
|
99
|
+
: `<${this.#closing ? '/' : ''}${this.#tag}${super.toString(selector)}${this.#selfClosing ? '/' : ''}>`;
|
|
55
100
|
}
|
|
56
101
|
|
|
102
|
+
/** @override */
|
|
57
103
|
getPadding() {
|
|
58
|
-
return this.#tag.length + (this
|
|
104
|
+
return this.#tag.length + (this.#closing ? 2 : 1);
|
|
59
105
|
}
|
|
60
106
|
|
|
107
|
+
/** @override */
|
|
61
108
|
text() {
|
|
62
|
-
return `<${this
|
|
109
|
+
return `<${this.#closing ? '/' : ''}${this.#tag}${super.text()}${this.#selfClosing ? '/' : ''}>`;
|
|
63
110
|
}
|
|
64
111
|
|
|
65
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* 更换标签名
|
|
114
|
+
* @param {string} tag 标签名
|
|
115
|
+
* @throws `RangeError` 非法的HTML标签
|
|
116
|
+
*/
|
|
66
117
|
replaceTag(tag) {
|
|
67
118
|
const name = tag.toLowerCase();
|
|
68
119
|
if (!this.getAttribute('config').html.flat().includes(name)) {
|
|
@@ -71,21 +122,27 @@ class HtmlToken extends attributeParent(fixedToken(Token)) {
|
|
|
71
122
|
this.setAttribute('name', name).#tag = tag;
|
|
72
123
|
}
|
|
73
124
|
|
|
74
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* 搜索匹配的标签
|
|
127
|
+
* @complexity `n`
|
|
128
|
+
* @throws `SyntaxError` 同时闭合和自封闭的标签
|
|
129
|
+
* @throws `SyntaxError` 无效自封闭标签
|
|
130
|
+
* @throws `SyntaxError` 未闭合的标签
|
|
131
|
+
*/
|
|
75
132
|
findMatchingTag() {
|
|
76
133
|
const {html} = this.getAttribute('config'),
|
|
77
|
-
{name,
|
|
78
|
-
string = noWrap(this
|
|
134
|
+
{name, parentNode, closing, selfClosing} = this,
|
|
135
|
+
string = noWrap(String(this));
|
|
79
136
|
if (closing && selfClosing) {
|
|
80
137
|
throw new SyntaxError(`同时闭合和自封闭的标签:${string}`);
|
|
81
138
|
} else if (html[2].includes(name) || selfClosing && html[1].includes(name)) { // 自封闭标签
|
|
82
139
|
return this;
|
|
83
140
|
} else if (selfClosing && html[0].includes(name)) {
|
|
84
141
|
throw new SyntaxError(`无效自封闭标签:${string}`);
|
|
85
|
-
} else if (!
|
|
86
|
-
return;
|
|
142
|
+
} else if (!parentNode) {
|
|
143
|
+
return undefined;
|
|
87
144
|
}
|
|
88
|
-
const {children} =
|
|
145
|
+
const {children} = parentNode,
|
|
89
146
|
i = children.indexOf(this),
|
|
90
147
|
selector = `html#${name}`,
|
|
91
148
|
siblings = closing
|
|
@@ -105,32 +162,37 @@ class HtmlToken extends attributeParent(fixedToken(Token)) {
|
|
|
105
162
|
throw new SyntaxError(`未${closing ? '匹配的闭合' : '闭合的'}标签:${string}`);
|
|
106
163
|
}
|
|
107
164
|
|
|
165
|
+
/** 局部闭合 */
|
|
108
166
|
#localMatch() {
|
|
109
|
-
this
|
|
167
|
+
this.#selfClosing = false;
|
|
110
168
|
const root = Parser.parse(`</${this.name}>`, false, 3, this.getAttribute('config'));
|
|
111
169
|
this.after(root.firstChild);
|
|
112
170
|
}
|
|
113
171
|
|
|
114
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* 修复无效自封闭标签
|
|
174
|
+
* @complexity `n`
|
|
175
|
+
* @throws `Error` 无法修复无效自封闭标签
|
|
176
|
+
*/
|
|
115
177
|
fix() {
|
|
116
178
|
const config = this.getAttribute('config'),
|
|
117
|
-
{
|
|
118
|
-
if (!
|
|
179
|
+
{parentNode, name, firstElementChild} = this;
|
|
180
|
+
if (!parentNode || !this.#selfClosing || !config.html[0].includes(name)) {
|
|
119
181
|
return;
|
|
120
182
|
} else if (firstElementChild.text().trim()) {
|
|
121
183
|
this.#localMatch();
|
|
122
184
|
return;
|
|
123
185
|
}
|
|
124
|
-
const {children} =
|
|
186
|
+
const {children} = parentNode,
|
|
125
187
|
i = children.indexOf(this),
|
|
126
188
|
/** @type {HtmlToken[]} */
|
|
127
189
|
prevSiblings = children.slice(0, i).filter(child => child.matches(`html#${name}`)),
|
|
128
190
|
imbalance = prevSiblings.reduce((acc, {closing}) => acc + (closing ? 1 : -1), 0);
|
|
129
191
|
if (imbalance < 0) {
|
|
130
|
-
this
|
|
131
|
-
this
|
|
192
|
+
this.#selfClosing = false;
|
|
193
|
+
this.#closing = true;
|
|
132
194
|
} else {
|
|
133
|
-
Parser.warn('无法修复无效自封闭标签', noWrap(this
|
|
195
|
+
Parser.warn('无法修复无效自封闭标签', noWrap(String(this)));
|
|
134
196
|
throw new Error(`无法修复无效自封闭标签:前文共有 ${imbalance} 个未匹配的闭合标签`);
|
|
135
197
|
}
|
|
136
198
|
}
|
package/src/imageParameter.js
CHANGED
|
@@ -1,41 +1,39 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const {text, noWrap, extUrlChar} = require('../util/string'),
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Parser = require('..'),
|
|
5
|
+
Token = require('.'),
|
|
6
|
+
AstText = require('../lib/text');
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* 图片参数
|
|
10
|
-
* @classdesc `{childNodes: ...(
|
|
10
|
+
* @classdesc `{childNodes: ...(AstText|Token)}`
|
|
11
11
|
*/
|
|
12
12
|
class ImageParameterToken extends Token {
|
|
13
|
-
|
|
14
|
-
#syntax = '';
|
|
15
|
-
|
|
16
|
-
static #noLink = Symbol('no-link');
|
|
13
|
+
static noLink = Symbol('no-link'); // 这个Symbol需要公开
|
|
17
14
|
|
|
18
15
|
/**
|
|
16
|
+
* 检查图片参数是否合法
|
|
19
17
|
* @template {string} T
|
|
20
|
-
* @param {T} key
|
|
21
|
-
* @param {string} value
|
|
18
|
+
* @param {T} key 参数名
|
|
19
|
+
* @param {string} value 参数值
|
|
22
20
|
* @returns {T extends 'link' ? string|Symbol : boolean}
|
|
23
21
|
*/
|
|
24
|
-
static #validate(key, value, config = Parser.getConfig()) {
|
|
25
|
-
value = value.
|
|
22
|
+
static #validate(key, value, config = Parser.getConfig(), halfParsed = false) {
|
|
23
|
+
value = value.replaceAll(/\0\d+t\x7F/gu, '').trim();
|
|
26
24
|
if (key === 'width') {
|
|
27
|
-
return /^\d*(?:x\d*)
|
|
25
|
+
return /^\d*(?:x\d*)?$/u.test(value);
|
|
28
26
|
} else if (['alt', 'class', 'manualthumb', 'frameless', 'framed', 'thumbnail'].includes(key)) {
|
|
29
27
|
return true;
|
|
30
28
|
} else if (key === 'link') {
|
|
31
29
|
if (!value) {
|
|
32
|
-
return this
|
|
30
|
+
return this.noLink;
|
|
33
31
|
}
|
|
34
|
-
const regex = RegExp(`(?:${config.protocol}|//)${extUrlChar}(?=\0\\d+t\
|
|
32
|
+
const regex = new RegExp(`(?:${config.protocol}|//)${extUrlChar}(?=\0\\d+t\x7F|$)`, 'iu');
|
|
35
33
|
if (regex.test(value)) {
|
|
36
34
|
return value;
|
|
37
35
|
}
|
|
38
|
-
if (
|
|
36
|
+
if (value.startsWith('[[') && value.endsWith(']]')) {
|
|
39
37
|
value = value.slice(2, -2);
|
|
40
38
|
}
|
|
41
39
|
if (value.includes('%')) {
|
|
@@ -43,24 +41,39 @@ class ImageParameterToken extends Token {
|
|
|
43
41
|
value = decodeURIComponent(value);
|
|
44
42
|
} catch {}
|
|
45
43
|
}
|
|
46
|
-
const
|
|
47
|
-
return valid &&
|
|
44
|
+
const title = Parser.normalizeTitle(value, 0, false, config, halfParsed);
|
|
45
|
+
return title.valid && String(title);
|
|
48
46
|
}
|
|
49
47
|
return !isNaN(value);
|
|
50
48
|
}
|
|
51
49
|
|
|
50
|
+
type = 'image-parameter';
|
|
51
|
+
#syntax = '';
|
|
52
|
+
|
|
53
|
+
/** getValue()的getter */
|
|
54
|
+
get value() {
|
|
55
|
+
return this.getValue();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
set value(value) {
|
|
59
|
+
this.setValue(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 图片链接 */
|
|
52
63
|
get link() {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return undefined;
|
|
64
|
+
return this.name === 'link'
|
|
65
|
+
? ImageParameterToken.#validate('link', this.getValue(), this.getAttribute('config'))
|
|
66
|
+
: undefined;
|
|
57
67
|
}
|
|
68
|
+
|
|
58
69
|
set link(value) {
|
|
59
70
|
if (this.name === 'link') {
|
|
60
|
-
value = value === ImageParameterToken
|
|
71
|
+
value = value === ImageParameterToken.noLink ? '' : value;
|
|
61
72
|
this.setValue(value);
|
|
62
73
|
}
|
|
63
74
|
}
|
|
75
|
+
|
|
76
|
+
/** 图片大小 */
|
|
64
77
|
get size() {
|
|
65
78
|
if (this.name === 'width') {
|
|
66
79
|
const /** @type {string} */ size = this.getValue().trim();
|
|
@@ -68,30 +81,37 @@ class ImageParameterToken extends Token {
|
|
|
68
81
|
const [width, height = ''] = size.split('x');
|
|
69
82
|
return {width, height};
|
|
70
83
|
}
|
|
71
|
-
const token = Parser.parse(size, false, 2, this.getAttribute('config')),
|
|
72
|
-
|
|
73
|
-
|
|
84
|
+
const /** @type {{childNodes: AstText[]}} */ token = Parser.parse(size, false, 2, this.getAttribute('config')),
|
|
85
|
+
i = token.childNodes.findIndex(({type, data}) => type === 'text' && data.includes('x')),
|
|
86
|
+
str = token.childNodes[i];
|
|
74
87
|
if (i === -1) {
|
|
75
88
|
return {width: size, height: ''};
|
|
76
89
|
}
|
|
77
|
-
|
|
78
|
-
|
|
90
|
+
str.splitText(str.data.indexOf('x'));
|
|
91
|
+
str.nextSibling.splitText(1);
|
|
92
|
+
// eslint-disable-next-line unicorn/consistent-destructuring
|
|
79
93
|
return {width: text(token.childNodes.slice(0, i + 1)), height: text(token.childNodes.slice(i + 2))};
|
|
80
94
|
}
|
|
81
95
|
return undefined;
|
|
82
96
|
}
|
|
97
|
+
|
|
98
|
+
/** 图片宽度 */
|
|
83
99
|
get width() {
|
|
84
100
|
return this.size?.width;
|
|
85
101
|
}
|
|
102
|
+
|
|
86
103
|
set width(width) {
|
|
87
104
|
if (this.name === 'width') {
|
|
88
105
|
const {height} = this;
|
|
89
106
|
this.setValue(`${String(width || '')}${height && 'x'}${height}`);
|
|
90
107
|
}
|
|
91
108
|
}
|
|
109
|
+
|
|
110
|
+
/** 图片高度 */
|
|
92
111
|
get height() {
|
|
93
112
|
return this.size?.height;
|
|
94
113
|
}
|
|
114
|
+
|
|
95
115
|
set height(height) {
|
|
96
116
|
height = String(height || '');
|
|
97
117
|
if (this.name === 'width') {
|
|
@@ -100,20 +120,18 @@ class ImageParameterToken extends Token {
|
|
|
100
120
|
}
|
|
101
121
|
|
|
102
122
|
/**
|
|
103
|
-
* @param {string} str
|
|
123
|
+
* @param {string} str 图片参数
|
|
104
124
|
* @param {accum} accum
|
|
105
125
|
*/
|
|
106
126
|
constructor(str, config = Parser.getConfig(), accum = []) {
|
|
107
127
|
const regexes = Object.entries(config.img).map(
|
|
108
128
|
/** @returns {[string, string, RegExp]} */
|
|
109
|
-
([syntax, param]) => [syntax, param, RegExp(`^(\\s*)${syntax.replace('$1', '(.*)')}(\\s*)
|
|
129
|
+
([syntax, param]) => [syntax, param, new RegExp(`^(\\s*)${syntax.replace('$1', '(.*)')}(\\s*)$`, 'u')],
|
|
110
130
|
),
|
|
111
131
|
param = regexes.find(([,, regex]) => regex.test(str));
|
|
112
132
|
if (param) {
|
|
113
133
|
const mt = param[2].exec(str);
|
|
114
|
-
if (mt.length
|
|
115
|
-
// pass
|
|
116
|
-
} else {
|
|
134
|
+
if (mt.length !== 4 || ImageParameterToken.#validate(param[1], mt[2], config, true)) {
|
|
117
135
|
if (mt.length === 3) {
|
|
118
136
|
super(undefined, config, true, accum);
|
|
119
137
|
this.#syntax = str;
|
|
@@ -129,8 +147,9 @@ class ImageParameterToken extends Token {
|
|
|
129
147
|
this.setAttribute('name', 'caption').setAttribute('stage', 7);
|
|
130
148
|
}
|
|
131
149
|
|
|
150
|
+
/** @override */
|
|
132
151
|
cloneNode() {
|
|
133
|
-
const cloned = this.
|
|
152
|
+
const cloned = this.cloneChildNodes(),
|
|
134
153
|
config = this.getAttribute('config'),
|
|
135
154
|
token = Parser.run(() => new ImageParameterToken(this.#syntax.replace('$1', ''), config));
|
|
136
155
|
token.replaceChildren(...cloned);
|
|
@@ -138,47 +157,52 @@ class ImageParameterToken extends Token {
|
|
|
138
157
|
}
|
|
139
158
|
|
|
140
159
|
/**
|
|
160
|
+
* @override
|
|
141
161
|
* @template {string} T
|
|
142
|
-
* @param {T} key
|
|
162
|
+
* @param {T} key 属性键
|
|
143
163
|
* @returns {TokenAttribute<T>}
|
|
144
164
|
*/
|
|
145
165
|
getAttribute(key) {
|
|
146
|
-
|
|
147
|
-
return this.#syntax;
|
|
148
|
-
}
|
|
149
|
-
return super.getAttribute(key);
|
|
166
|
+
return key === 'syntax' ? this.#syntax : super.getAttribute(key);
|
|
150
167
|
}
|
|
151
168
|
|
|
169
|
+
/** @override */
|
|
152
170
|
isPlain() {
|
|
153
171
|
return true;
|
|
154
172
|
}
|
|
155
173
|
|
|
174
|
+
/** 是否是不可变参数 */
|
|
156
175
|
#isVoid() {
|
|
157
176
|
return this.#syntax && !this.#syntax.includes('$1');
|
|
158
177
|
}
|
|
159
178
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
179
|
+
/**
|
|
180
|
+
* @override
|
|
181
|
+
* @param {string} selector
|
|
182
|
+
*/
|
|
183
|
+
toString(selector) {
|
|
184
|
+
return this.#syntax && !(selector && this.matches(selector))
|
|
185
|
+
? this.#syntax.replace('$1', super.toString(selector))
|
|
186
|
+
: super.toString(selector);
|
|
165
187
|
}
|
|
166
188
|
|
|
189
|
+
/** @override */
|
|
167
190
|
getPadding() {
|
|
168
191
|
return Math.max(0, this.#syntax.indexOf('$1'));
|
|
169
192
|
}
|
|
170
193
|
|
|
194
|
+
/** @override */
|
|
171
195
|
text() {
|
|
172
|
-
|
|
173
|
-
return super.text().trim();
|
|
174
|
-
}
|
|
175
|
-
return this.#syntax.replace('$1', super.text()).trim();
|
|
196
|
+
return this.#syntax ? this.#syntax.replace('$1', super.text()).trim() : super.text().trim();
|
|
176
197
|
}
|
|
177
198
|
|
|
178
199
|
/**
|
|
179
|
-
* @
|
|
180
|
-
* @
|
|
200
|
+
* @override
|
|
201
|
+
* @template {Token} T
|
|
202
|
+
* @param {T} token 待插入的子节点
|
|
203
|
+
* @param {number} i 插入位置
|
|
181
204
|
* @complexity `n`
|
|
205
|
+
* @throws `Error` 不接受自定义输入的图片参数
|
|
182
206
|
*/
|
|
183
207
|
insertAt(token, i = this.childNodes.length) {
|
|
184
208
|
if (!Parser.running && this.#isVoid()) {
|
|
@@ -187,14 +211,19 @@ class ImageParameterToken extends Token {
|
|
|
187
211
|
return super.insertAt(token, i);
|
|
188
212
|
}
|
|
189
213
|
|
|
190
|
-
/**
|
|
214
|
+
/**
|
|
215
|
+
* 获取参数值
|
|
216
|
+
* @complexity `n`
|
|
217
|
+
*/
|
|
191
218
|
getValue() {
|
|
192
219
|
return this.#isVoid() || super.text();
|
|
193
220
|
}
|
|
194
221
|
|
|
195
222
|
/**
|
|
196
|
-
*
|
|
223
|
+
* 设置参数值
|
|
224
|
+
* @param {string|boolean} value 参数值
|
|
197
225
|
* @complexity `n`
|
|
226
|
+
* @throws SyntaxError` 非法的参数值
|
|
198
227
|
*/
|
|
199
228
|
setValue(value) {
|
|
200
229
|
if (this.#isVoid()) {
|
|
@@ -212,7 +241,7 @@ class ImageParameterToken extends Token {
|
|
|
212
241
|
}]]`, this.getAttribute('include'), 6, this.getAttribute('config')),
|
|
213
242
|
{childNodes: {length}, firstElementChild} = root,
|
|
214
243
|
param = firstElementChild?.lastElementChild;
|
|
215
|
-
if (length !== 1 || !firstElementChild?.matches('file#File
|
|
244
|
+
if (length !== 1 || !firstElementChild?.matches('file#File\\:F')
|
|
216
245
|
|| firstElementChild.childNodes.length !== 2 || param.name !== this.name
|
|
217
246
|
) {
|
|
218
247
|
throw new SyntaxError(`非法的 ${this.name} 参数:${noWrap(value)}`);
|