wikiparser-node 0.4.0 → 0.5.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/index.js +25 -2
- package/lib/element.js +69 -185
- package/lib/node.js +159 -1
- package/lib/ranges.js +1 -2
- package/lib/text.js +35 -6
- package/lib/title.js +1 -1
- package/mixin/fixedToken.js +4 -4
- package/mixin/sol.js +17 -7
- package/package.json +11 -1
- package/parser/commentAndExt.js +1 -1
- package/parser/converter.js +1 -1
- package/parser/externalLinks.js +1 -1
- package/parser/hrAndDoubleUnderscore.js +6 -5
- package/parser/links.js +1 -2
- package/parser/magicLinks.js +1 -1
- package/parser/selector.js +5 -5
- package/parser/table.js +12 -12
- package/src/arg.js +44 -20
- package/src/attribute.js +34 -7
- package/src/converter.js +13 -5
- package/src/converterFlags.js +42 -5
- package/src/converterRule.js +25 -19
- package/src/extLink.js +20 -14
- package/src/gallery.js +35 -4
- package/src/heading.js +28 -9
- package/src/html.js +46 -18
- package/src/imageParameter.js +13 -7
- package/src/index.js +22 -15
- package/src/link/category.js +6 -6
- package/src/link/file.js +25 -5
- package/src/link/index.js +36 -33
- package/src/magicLink.js +32 -4
- package/src/nowiki/comment.js +14 -0
- package/src/nowiki/doubleUnderscore.js +5 -0
- package/src/nowiki/quote.js +28 -1
- package/src/onlyinclude.js +5 -0
- package/src/parameter.js +48 -35
- package/src/table/index.js +37 -24
- package/src/table/td.js +23 -17
- package/src/table/tr.js +47 -30
- package/src/tagPair/ext.js +4 -5
- package/src/tagPair/include.js +10 -0
- package/src/tagPair/index.js +8 -0
- package/src/transclude.js +79 -46
- package/tool/index.js +1 -1
- package/{test/util.js → util/diff.js} +14 -18
- package/util/lint.js +40 -0
- package/util/string.js +20 -3
- package/.eslintrc.json +0 -714
- package/errors/README +0 -1
- package/jsconfig.json +0 -7
- package/printed/README +0 -1
- package/printed/example.json +0 -120
- package/test/api.js +0 -83
- package/test/real.js +0 -133
- package/test/test.js +0 -28
- package/typings/api.d.ts +0 -13
- package/typings/array.d.ts +0 -28
- package/typings/event.d.ts +0 -24
- package/typings/index.d.ts +0 -94
- package/typings/node.d.ts +0 -29
- package/typings/parser.d.ts +0 -16
- package/typings/table.d.ts +0 -14
- package/typings/token.d.ts +0 -22
- package/typings/tool.d.ts +0 -11
package/src/converterRule.js
CHANGED
|
@@ -15,7 +15,7 @@ class ConverterRuleToken extends Token {
|
|
|
15
15
|
|
|
16
16
|
/** 语言变体 */
|
|
17
17
|
get variant() {
|
|
18
|
-
return this.
|
|
18
|
+
return this.childNodes.at(-2)?.text()?.trim() ?? '';
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
set variant(variant) {
|
|
@@ -72,7 +72,7 @@ class ConverterRuleToken extends Token {
|
|
|
72
72
|
placeholder = placeholders[cloned.length - 1],
|
|
73
73
|
token = Parser.run(() => new ConverterRuleToken(placeholder, placeholder, this.getAttribute('config')));
|
|
74
74
|
for (let i = 0; i < cloned.length; i++) {
|
|
75
|
-
token.
|
|
75
|
+
token.childNodes[i].safeReplaceWith(cloned[i]);
|
|
76
76
|
}
|
|
77
77
|
token.afterBuild();
|
|
78
78
|
return token;
|
|
@@ -128,7 +128,7 @@ class ConverterRuleToken extends Token {
|
|
|
128
128
|
*/
|
|
129
129
|
toString(selector) {
|
|
130
130
|
if (this.childNodes.length === 3 && !(selector && this.matches(selector))) {
|
|
131
|
-
const {
|
|
131
|
+
const {childNodes: [from, variant, to]} = this;
|
|
132
132
|
return `${from.toString(selector)}=>${variant.toString(selector)}:${to.toString(selector)}`;
|
|
133
133
|
}
|
|
134
134
|
return super.toString(selector, ':');
|
|
@@ -144,13 +144,22 @@ class ConverterRuleToken extends Token {
|
|
|
144
144
|
return i === 0 && length === 3 ? 2 : 1;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/** @override */
|
|
148
|
+
print() {
|
|
149
|
+
if (this.childNodes.length === 3) {
|
|
150
|
+
const {childNodes: [from, variant, to]} = this;
|
|
151
|
+
return `<span class="wpb-converter-rule">${from.print()}=>${variant.print()}:${to.print()}</span>`;
|
|
152
|
+
}
|
|
153
|
+
return super.print({sep: ':'});
|
|
154
|
+
}
|
|
155
|
+
|
|
147
156
|
/**
|
|
148
157
|
* @override
|
|
149
158
|
* @returns {string}
|
|
150
159
|
*/
|
|
151
160
|
text() {
|
|
152
161
|
if (this.childNodes.length === 3) {
|
|
153
|
-
const {
|
|
162
|
+
const {childNodes: [from, variant, to]} = this;
|
|
154
163
|
return `${from.text()}=>${variant.text()}:${to.text()}`;
|
|
155
164
|
}
|
|
156
165
|
return super.text(':');
|
|
@@ -174,16 +183,14 @@ class ConverterRuleToken extends Token {
|
|
|
174
183
|
to = String(to);
|
|
175
184
|
const config = this.getAttribute('config'),
|
|
176
185
|
root = Parser.parse(`-{|${config.variants[0]}:${to}}-`, this.getAttribute('include'), undefined, config),
|
|
177
|
-
{childNodes: {length},
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
) {
|
|
186
|
+
{childNodes: {length}, firstChild: converter} = root,
|
|
187
|
+
{lastChild: converterRule, type, childNodes: {length: converterLength}} = converter;
|
|
188
|
+
if (length !== 1 || type !== 'converter' || converterLength !== 2 || converterRule.childNodes.length !== 2) {
|
|
181
189
|
throw new SyntaxError(`非法的转换目标:${noWrap(to)}`);
|
|
182
190
|
}
|
|
183
|
-
const {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
this.lastElementChild.safeReplaceWith(lastChild);
|
|
191
|
+
const {lastChild} = converterRule;
|
|
192
|
+
converterRule.destroy(true);
|
|
193
|
+
this.lastChild.safeReplaceWith(lastChild);
|
|
187
194
|
}
|
|
188
195
|
|
|
189
196
|
/**
|
|
@@ -202,7 +209,7 @@ class ConverterRuleToken extends Token {
|
|
|
202
209
|
} else if (this.childNodes.length === 1) {
|
|
203
210
|
super.insertAt(Parser.run(() => new AtomToken(variant, 'converter-rule-variant', config)), 0);
|
|
204
211
|
} else {
|
|
205
|
-
this.
|
|
212
|
+
this.childNodes.at(-2).setText(variant);
|
|
206
213
|
}
|
|
207
214
|
}
|
|
208
215
|
|
|
@@ -220,16 +227,15 @@ class ConverterRuleToken extends Token {
|
|
|
220
227
|
from = String(from);
|
|
221
228
|
const config = this.getAttribute('config'),
|
|
222
229
|
root = Parser.parse(`-{|${from}=>${variant}:}-`, this.getAttribute('include'), undefined, config),
|
|
223
|
-
{childNodes: {length},
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
) {
|
|
230
|
+
{childNodes: {length}, firstChild: converter} = root,
|
|
231
|
+
{type, childNodes: {length: converterLength}, lastChild: converterRule} = converter;
|
|
232
|
+
if (length !== 1 || type !== 'converter' || converterLength !== 2 || converterRule.childNodes.length !== 3) {
|
|
227
233
|
throw new SyntaxError(`非法的转换原文:${noWrap(from)}`);
|
|
228
234
|
}
|
|
229
235
|
if (unidirectional) {
|
|
230
|
-
this.
|
|
236
|
+
this.firstChild.safeReplaceWith(converterRule.firstChild);
|
|
231
237
|
} else {
|
|
232
|
-
super.insertAt(
|
|
238
|
+
super.insertAt(converterRule.firstChild, 0);
|
|
233
239
|
}
|
|
234
240
|
}
|
|
235
241
|
|
package/src/extLink.js
CHANGED
|
@@ -41,7 +41,7 @@ class ExtLinkToken extends Token {
|
|
|
41
41
|
/** 链接显示文字 */
|
|
42
42
|
get innerText() {
|
|
43
43
|
return this.childNodes.length > 1
|
|
44
|
-
? this.
|
|
44
|
+
? this.lastChild.text()
|
|
45
45
|
: `[${this.getRootNode().querySelectorAll('ext-link[childElementCount=1]').indexOf(this) + 1}]`;
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -67,7 +67,7 @@ class ExtLinkToken extends Token {
|
|
|
67
67
|
cloneNode() {
|
|
68
68
|
const [url, text] = this.cloneChildNodes(),
|
|
69
69
|
token = Parser.run(() => new ExtLinkToken(undefined, '', '', this.getAttribute('config')));
|
|
70
|
-
token.
|
|
70
|
+
token.firstChild.safeReplaceWith(url);
|
|
71
71
|
if (text) {
|
|
72
72
|
token.appendChild(text);
|
|
73
73
|
}
|
|
@@ -78,7 +78,7 @@ class ExtLinkToken extends Token {
|
|
|
78
78
|
#correct() {
|
|
79
79
|
if (!this.#space && this.childNodes.length > 1
|
|
80
80
|
// 都替换成`<`肯定不对,但无妨
|
|
81
|
-
&& /^[^[\]<>"{\0-\x1F\x7F\p{Zs}\uFFFD]/u.test(this.
|
|
81
|
+
&& /^[^[\]<>"{\0-\x1F\x7F\p{Zs}\uFFFD]/u.test(this.lastChild.text().replace(/&[lg]t;/u, '<'))
|
|
82
82
|
) {
|
|
83
83
|
this.#space = ' ';
|
|
84
84
|
}
|
|
@@ -95,7 +95,7 @@ class ExtLinkToken extends Token {
|
|
|
95
95
|
return `[${super.toString(selector)}${this.#space}]`;
|
|
96
96
|
}
|
|
97
97
|
this.#correct();
|
|
98
|
-
normalizeSpace(this.
|
|
98
|
+
normalizeSpace(this.lastChild);
|
|
99
99
|
return `[${super.toString(selector, this.#space)}]`;
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -110,9 +110,15 @@ class ExtLinkToken extends Token {
|
|
|
110
110
|
return this.#space.length;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/** @override */
|
|
114
|
+
print() {
|
|
115
|
+
const {childNodes: {length}} = this;
|
|
116
|
+
return super.print(length > 1 ? {pre: '[', sep: this.#space, post: ']'} : {pre: '[', post: `${this.#space}]`});
|
|
117
|
+
}
|
|
118
|
+
|
|
113
119
|
/** @override */
|
|
114
120
|
text() {
|
|
115
|
-
normalizeSpace(this.
|
|
121
|
+
normalizeSpace(this.childNodes[1]);
|
|
116
122
|
return `[${super.text(' ')}]`;
|
|
117
123
|
}
|
|
118
124
|
|
|
@@ -132,13 +138,13 @@ class ExtLinkToken extends Token {
|
|
|
132
138
|
setTarget(url) {
|
|
133
139
|
url = String(url);
|
|
134
140
|
const root = Parser.parse(`[${url}]`, this.getAttribute('include'), 8, this.getAttribute('config')),
|
|
135
|
-
{childNodes: {length},
|
|
136
|
-
if (length !== 1 ||
|
|
141
|
+
{childNodes: {length}, firstChild: extLink} = root;
|
|
142
|
+
if (length !== 1 || extLink.type !== 'ext-link' || extLink.childNodes.length !== 1) {
|
|
137
143
|
throw new SyntaxError(`非法的外链目标:${url}`);
|
|
138
144
|
}
|
|
139
|
-
const {firstChild} =
|
|
140
|
-
|
|
141
|
-
this.
|
|
145
|
+
const {firstChild} = extLink;
|
|
146
|
+
extLink.destroy(true);
|
|
147
|
+
this.firstChild.safeReplaceWith(firstChild);
|
|
142
148
|
}
|
|
143
149
|
|
|
144
150
|
/**
|
|
@@ -149,15 +155,15 @@ class ExtLinkToken extends Token {
|
|
|
149
155
|
setLinkText(text) {
|
|
150
156
|
text = String(text);
|
|
151
157
|
const root = Parser.parse(`[//url ${text}]`, this.getAttribute('include'), 8, this.getAttribute('config')),
|
|
152
|
-
{childNodes: {length},
|
|
153
|
-
if (length !== 1 ||
|
|
158
|
+
{childNodes: {length}, firstChild: extLink} = root;
|
|
159
|
+
if (length !== 1 || extLink.type !== 'ext-link' || extLink.childNodes.length !== 2) {
|
|
154
160
|
throw new SyntaxError(`非法的外链文字:${noWrap(text)}`);
|
|
155
161
|
}
|
|
156
|
-
const {lastChild} =
|
|
162
|
+
const {lastChild} = extLink;
|
|
157
163
|
if (this.childNodes.length === 1) {
|
|
158
164
|
this.appendChild(lastChild);
|
|
159
165
|
} else {
|
|
160
|
-
this.
|
|
166
|
+
this.lastChild.safeReplaceWith(lastChild);
|
|
161
167
|
}
|
|
162
168
|
this.#space ||= ' ';
|
|
163
169
|
}
|
package/src/gallery.js
CHANGED
|
@@ -61,6 +61,11 @@ class GalleryToken extends Token {
|
|
|
61
61
|
return 1;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** @override */
|
|
65
|
+
print() {
|
|
66
|
+
return super.print({sep: '\n'});
|
|
67
|
+
}
|
|
68
|
+
|
|
64
69
|
/** @override */
|
|
65
70
|
text() {
|
|
66
71
|
return super.text('\n').replaceAll(/\n\s*\n/gu, '\n');
|
|
@@ -79,11 +84,37 @@ class GalleryToken extends Token {
|
|
|
79
84
|
} catch {
|
|
80
85
|
title = this.normalizeTitle(file, 6, true);
|
|
81
86
|
}
|
|
82
|
-
if (
|
|
83
|
-
|
|
87
|
+
if (title.valid) {
|
|
88
|
+
const token = Parser.run(() => new GalleryImageToken(file, undefined, title, this.getAttribute('config')));
|
|
89
|
+
return this.insertAt(token, i);
|
|
90
|
+
}
|
|
91
|
+
throw new SyntaxError(`非法的文件名:${file}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @override
|
|
96
|
+
* @param {number} start 起始位置
|
|
97
|
+
*/
|
|
98
|
+
lint(start = 0) {
|
|
99
|
+
const {top, left} = this.getRootNode().posFromIndex(start),
|
|
100
|
+
/** @type {LintError[]} */ errors = [];
|
|
101
|
+
for (let i = 0, cur = start; i < this.childNodes.length; i++) {
|
|
102
|
+
const child = this.childNodes[i],
|
|
103
|
+
str = String(child);
|
|
104
|
+
if (child.type === 'hidden' && str.trim() && !/^<!--.*-->$/u.test(str)) {
|
|
105
|
+
errors.push({
|
|
106
|
+
message: '图库中的无效内容',
|
|
107
|
+
startLine: top + i,
|
|
108
|
+
endLine: top + i,
|
|
109
|
+
startCol: i ? 0 : left,
|
|
110
|
+
endCol: i ? str.length : left + str.length,
|
|
111
|
+
});
|
|
112
|
+
} else if (child.type !== 'hidden' && child.type !== 'text') {
|
|
113
|
+
errors.push(...child.lint(cur));
|
|
114
|
+
}
|
|
115
|
+
cur += str.length + 1;
|
|
84
116
|
}
|
|
85
|
-
|
|
86
|
-
return this.insertAt(token, i);
|
|
117
|
+
return errors;
|
|
87
118
|
}
|
|
88
119
|
}
|
|
89
120
|
|
package/src/heading.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const {generateForSelf} = require('../util/lint'),
|
|
4
|
+
fixedToken = require('../mixin/fixedToken'),
|
|
4
5
|
sol = require('../mixin/sol'),
|
|
5
6
|
Parser = require('..'),
|
|
6
7
|
Token = require('.');
|
|
@@ -14,7 +15,7 @@ class HeadingToken extends fixedToken(sol(Token)) {
|
|
|
14
15
|
|
|
15
16
|
/** 内部wikitext */
|
|
16
17
|
get innerText() {
|
|
17
|
-
return this.
|
|
18
|
+
return this.firstChild.text();
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -39,8 +40,8 @@ class HeadingToken extends fixedToken(sol(Token)) {
|
|
|
39
40
|
cloneNode() {
|
|
40
41
|
const [title, trail] = this.cloneChildNodes(),
|
|
41
42
|
token = Parser.run(() => new HeadingToken(Number(this.name), [], this.getAttribute('config')));
|
|
42
|
-
token.
|
|
43
|
-
token.
|
|
43
|
+
token.firsthild.safeReplaceWith(title);
|
|
44
|
+
token.lastChild.safeReplaceWith(trail);
|
|
44
45
|
return token;
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -54,8 +55,8 @@ class HeadingToken extends fixedToken(sol(Token)) {
|
|
|
54
55
|
return selector && this.matches(selector)
|
|
55
56
|
? ''
|
|
56
57
|
: `${this.prependNewLine()}${equals}${
|
|
57
|
-
this.
|
|
58
|
-
}${equals}${this.
|
|
58
|
+
this.firstChild.toString(selector)
|
|
59
|
+
}${equals}${this.lastChild.toString(selector)}${this.appendNewLine()}`;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
/** @override */
|
|
@@ -68,6 +69,24 @@ class HeadingToken extends fixedToken(sol(Token)) {
|
|
|
68
69
|
return Number(this.name);
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
/** @override */
|
|
73
|
+
print() {
|
|
74
|
+
const equals = '='.repeat(Number(this.name));
|
|
75
|
+
return super.print({pre: equals, sep: equals});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @override
|
|
80
|
+
* @param {number} start 起始位置
|
|
81
|
+
*/
|
|
82
|
+
lint(start = 0) {
|
|
83
|
+
const errors = super.lint(start);
|
|
84
|
+
if (this.name === '1') {
|
|
85
|
+
errors.push(generateForSelf(this, this.getRootNode().posFromIndex(start), '<h1>'));
|
|
86
|
+
}
|
|
87
|
+
return errors;
|
|
88
|
+
}
|
|
89
|
+
|
|
71
90
|
/**
|
|
72
91
|
* @override
|
|
73
92
|
* @this {HeadingToken & {prependNewLine(): ''|'\n', appendNewLine(): ''|'\n'}}
|
|
@@ -75,7 +94,7 @@ class HeadingToken extends fixedToken(sol(Token)) {
|
|
|
75
94
|
*/
|
|
76
95
|
text() {
|
|
77
96
|
const equals = '='.repeat(Number(this.name));
|
|
78
|
-
return `${this.prependNewLine()}${equals}${this.
|
|
97
|
+
return `${this.prependNewLine()}${equals}${this.firstChild.text()}${equals}${this.appendNewLine()}`;
|
|
79
98
|
}
|
|
80
99
|
|
|
81
100
|
/**
|
|
@@ -87,12 +106,12 @@ class HeadingToken extends fixedToken(sol(Token)) {
|
|
|
87
106
|
this.typeError('setLevel', 'Number');
|
|
88
107
|
}
|
|
89
108
|
n = Math.min(Math.max(n, 1), 6);
|
|
90
|
-
this.setAttribute('name', String(n)).
|
|
109
|
+
this.setAttribute('name', String(n)).firstChild.setAttribute('name', this.name);
|
|
91
110
|
}
|
|
92
111
|
|
|
93
112
|
/** 移除标题后的不可见内容 */
|
|
94
113
|
removeTrail() {
|
|
95
|
-
this.
|
|
114
|
+
this.lastChild.replaceChildren();
|
|
96
115
|
}
|
|
97
116
|
}
|
|
98
117
|
|
package/src/html.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const {noWrap} = require('../util/string'),
|
|
4
|
+
{generateForSelf} = require('../util/lint'),
|
|
4
5
|
fixedToken = require('../mixin/fixedToken'),
|
|
5
6
|
attributeParent = require('../mixin/attributeParent'),
|
|
6
7
|
Parser = require('..'),
|
|
@@ -104,6 +105,34 @@ class HtmlToken extends attributeParent(fixedToken(Token)) {
|
|
|
104
105
|
return this.#tag.length + (this.#closing ? 2 : 1);
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
/** @override */
|
|
109
|
+
print() {
|
|
110
|
+
return super.print({
|
|
111
|
+
pre: `<${this.#closing ? '/' : ''}${this.#tag}`, post: `${this.#selfClosing ? '/' : ''}>`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @override
|
|
117
|
+
* @param {number} start 起始位置
|
|
118
|
+
*/
|
|
119
|
+
lint(start = 0) {
|
|
120
|
+
const errors = super.lint(start);
|
|
121
|
+
let /** @type {LintError} */ refError;
|
|
122
|
+
if (this.name === 'h1' && !this.#closing) {
|
|
123
|
+
refError = generateForSelf(this, this.getRootNode().posFromIndex(start), '<h1>');
|
|
124
|
+
errors.push(refError);
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
this.findMatchingTag();
|
|
128
|
+
} catch ({message: errorMsg}) {
|
|
129
|
+
const [message] = errorMsg.split(':');
|
|
130
|
+
refError ||= generateForSelf(this, this.getRootNode().posFromIndex(start), '');
|
|
131
|
+
errors.push({...refError, message, severity: message === '未闭合的标签' ? 'warning' : 'error'});
|
|
132
|
+
}
|
|
133
|
+
return errors;
|
|
134
|
+
}
|
|
135
|
+
|
|
107
136
|
/** @override */
|
|
108
137
|
text() {
|
|
109
138
|
return `<${this.#closing ? '/' : ''}${this.#tag}${super.text()}${this.#selfClosing ? '/' : ''}>`;
|
|
@@ -131,24 +160,23 @@ class HtmlToken extends attributeParent(fixedToken(Token)) {
|
|
|
131
160
|
*/
|
|
132
161
|
findMatchingTag() {
|
|
133
162
|
const {html} = this.getAttribute('config'),
|
|
134
|
-
{name, parentNode
|
|
163
|
+
{name: tagName, parentNode} = this,
|
|
135
164
|
string = noWrap(String(this));
|
|
136
|
-
if (closing && selfClosing) {
|
|
165
|
+
if (this.#closing && this.#selfClosing) {
|
|
137
166
|
throw new SyntaxError(`同时闭合和自封闭的标签:${string}`);
|
|
138
|
-
} else if (html[2].includes(
|
|
167
|
+
} else if (html[2].includes(tagName) || this.#selfClosing && html[1].includes(tagName)) { // 自封闭标签
|
|
139
168
|
return this;
|
|
140
|
-
} else if (selfClosing && html[0].includes(
|
|
169
|
+
} else if (this.#selfClosing && html[0].includes(tagName)) {
|
|
141
170
|
throw new SyntaxError(`无效自封闭标签:${string}`);
|
|
142
171
|
} else if (!parentNode) {
|
|
143
172
|
return undefined;
|
|
144
173
|
}
|
|
145
|
-
const {
|
|
146
|
-
i =
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
let imbalance = closing ? -1 : 1;
|
|
174
|
+
const {childNodes} = parentNode,
|
|
175
|
+
i = childNodes.indexOf(this),
|
|
176
|
+
siblings = this.#closing
|
|
177
|
+
? childNodes.slice(0, i).reverse().filter(({type, name}) => type === 'html' && name === tagName)
|
|
178
|
+
: childNodes.slice(i + 1).filter(({type, name}) => type === 'html' && name === tagName);
|
|
179
|
+
let imbalance = this.#closing ? -1 : 1;
|
|
152
180
|
for (const token of siblings) {
|
|
153
181
|
if (token.closing) {
|
|
154
182
|
imbalance--;
|
|
@@ -159,7 +187,7 @@ class HtmlToken extends attributeParent(fixedToken(Token)) {
|
|
|
159
187
|
return token;
|
|
160
188
|
}
|
|
161
189
|
}
|
|
162
|
-
throw new SyntaxError(`未${closing ? '匹配的闭合' : '闭合的'}标签:${string}`);
|
|
190
|
+
throw new SyntaxError(`未${this.#closing ? '匹配的闭合' : '闭合的'}标签:${string}`);
|
|
163
191
|
}
|
|
164
192
|
|
|
165
193
|
/** 局部闭合 */
|
|
@@ -176,17 +204,17 @@ class HtmlToken extends attributeParent(fixedToken(Token)) {
|
|
|
176
204
|
*/
|
|
177
205
|
fix() {
|
|
178
206
|
const config = this.getAttribute('config'),
|
|
179
|
-
{parentNode, name,
|
|
180
|
-
if (!parentNode || !this.#selfClosing || !config.html[0].includes(
|
|
207
|
+
{parentNode, name: tagName, firstChild} = this;
|
|
208
|
+
if (!parentNode || !this.#selfClosing || !config.html[0].includes(tagName)) {
|
|
181
209
|
return;
|
|
182
|
-
} else if (
|
|
210
|
+
} else if (firstChild.text().trim()) {
|
|
183
211
|
this.#localMatch();
|
|
184
212
|
return;
|
|
185
213
|
}
|
|
186
|
-
const {
|
|
187
|
-
i =
|
|
214
|
+
const {childNodes} = parentNode,
|
|
215
|
+
i = childNodes.indexOf(this),
|
|
188
216
|
/** @type {HtmlToken[]} */
|
|
189
|
-
prevSiblings =
|
|
217
|
+
prevSiblings = childNodes.slice(0, i).filter(({type, name}) => type === 'html' && name === tagName),
|
|
190
218
|
imbalance = prevSiblings.reduce((acc, {closing}) => acc + (closing ? 1 : -1), 0);
|
|
191
219
|
if (imbalance < 0) {
|
|
192
220
|
this.#selfClosing = false;
|
package/src/imageParameter.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {text, noWrap, extUrlChar} = require('../util/string'),
|
|
3
|
+
const {text, noWrap, print, extUrlChar} = require('../util/string'),
|
|
4
4
|
Parser = require('..'),
|
|
5
5
|
Token = require('.'),
|
|
6
6
|
AstText = require('../lib/text');
|
|
@@ -89,7 +89,6 @@ class ImageParameterToken extends Token {
|
|
|
89
89
|
}
|
|
90
90
|
str.splitText(str.data.indexOf('x'));
|
|
91
91
|
str.nextSibling.splitText(1);
|
|
92
|
-
// eslint-disable-next-line unicorn/consistent-destructuring
|
|
93
92
|
return {width: text(token.childNodes.slice(0, i + 1)), height: text(token.childNodes.slice(i + 2))};
|
|
94
93
|
}
|
|
95
94
|
return undefined;
|
|
@@ -191,6 +190,13 @@ class ImageParameterToken extends Token {
|
|
|
191
190
|
return Math.max(0, this.#syntax.indexOf('$1'));
|
|
192
191
|
}
|
|
193
192
|
|
|
193
|
+
/** @override */
|
|
194
|
+
print() {
|
|
195
|
+
return this.#syntax
|
|
196
|
+
? `<span class="wpb-image-parameter">${this.#syntax.replace('$1', print(this.childNodes))}</span>`
|
|
197
|
+
: super.print({class: 'image-caption'});
|
|
198
|
+
}
|
|
199
|
+
|
|
194
200
|
/** @override */
|
|
195
201
|
text() {
|
|
196
202
|
return this.#syntax ? this.#syntax.replace('$1', super.text()).trim() : super.text().trim();
|
|
@@ -239,14 +245,14 @@ class ImageParameterToken extends Token {
|
|
|
239
245
|
const root = Parser.parse(`[[File:F|${
|
|
240
246
|
this.#syntax ? this.#syntax.replace('$1', value) : value
|
|
241
247
|
}]]`, this.getAttribute('include'), 6, this.getAttribute('config')),
|
|
242
|
-
{childNodes: {length},
|
|
243
|
-
|
|
244
|
-
if (length !== 1 ||
|
|
245
|
-
||
|
|
248
|
+
{childNodes: {length}, firstChild: file} = root,
|
|
249
|
+
{lastChild: imageParameter, type, name, childNodes: {length: fileLength}} = file;
|
|
250
|
+
if (length !== 1 || type !== 'file' || name !== 'File:F' || fileLength !== 2
|
|
251
|
+
|| imageParameter.name !== this.name
|
|
246
252
|
) {
|
|
247
253
|
throw new SyntaxError(`非法的 ${this.name} 参数:${noWrap(value)}`);
|
|
248
254
|
}
|
|
249
|
-
this.replaceChildren(...
|
|
255
|
+
this.replaceChildren(...imageParameter.childNodes);
|
|
250
256
|
}
|
|
251
257
|
}
|
|
252
258
|
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/*
|
|
4
4
|
* PHP解析器的步骤:
|
|
5
5
|
* -1. 替换签名和`{{subst:}}`,参见Parser::preSaveTransform;这在revision中不可能保留,可以跳过
|
|
6
|
-
* 0. 移除特定字符`\0`和`\
|
|
6
|
+
* 0. 移除特定字符`\0`和`\x7F`,参见Parser::parse
|
|
7
7
|
* 1. 注释/扩展标签('<'相关),参见Preprocessor_Hash::buildDomTreeArrayFromText和Sanitizer::decodeTagAttributes
|
|
8
8
|
* 2. 模板/模板变量/标题,注意rightmost法则,以及`-{`和`[[`可以破坏`{{`或`{{{`语法,
|
|
9
9
|
* 参见Preprocessor_Hash::buildDomTreeArrayFromText
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
/*
|
|
22
|
-
* \0\d+.\
|
|
22
|
+
* \0\d+.\x7F标记Token:
|
|
23
23
|
* e: ExtToken
|
|
24
24
|
* c: CommentToken、NoIncludeToken和IncludeToken
|
|
25
25
|
* !: `{{!}}`专用
|
|
@@ -58,7 +58,7 @@ class Token extends AstElement {
|
|
|
58
58
|
type = 'root';
|
|
59
59
|
#stage = 0; // 解析阶段,参见顶部注释。只对plain Token有意义。
|
|
60
60
|
#config;
|
|
61
|
-
// 这个数组起两个作用:1. 数组中的Token会在build时替换`/\0\d+.\
|
|
61
|
+
// 这个数组起两个作用:1. 数组中的Token会在build时替换`/\0\d+.\x7F/`标记;2. 数组中的Token会依次执行parseOnce和build方法。
|
|
62
62
|
#accum;
|
|
63
63
|
/** @type {Record<string, Ranges>} */ #acceptable;
|
|
64
64
|
#protectedChildren = new Ranges();
|
|
@@ -167,7 +167,7 @@ class Token extends AstElement {
|
|
|
167
167
|
constructor(wikitext, config = Parser.getConfig(), halfParsed = false, accum = [], acceptable = null) {
|
|
168
168
|
super();
|
|
169
169
|
if (typeof wikitext === 'string') {
|
|
170
|
-
this.appendChild(halfParsed ? wikitext : wikitext.replaceAll(
|
|
170
|
+
this.appendChild(halfParsed ? wikitext : wikitext.replaceAll(/[\0\x7F]/gu, ''));
|
|
171
171
|
}
|
|
172
172
|
this.#config = config;
|
|
173
173
|
this.#accum = accum;
|
|
@@ -521,27 +521,28 @@ class Token extends AstElement {
|
|
|
521
521
|
if (!parentNode) {
|
|
522
522
|
return undefined;
|
|
523
523
|
}
|
|
524
|
-
const {
|
|
525
|
-
index =
|
|
524
|
+
const {childNodes} = parentNode,
|
|
525
|
+
index = childNodes.indexOf(this);
|
|
526
526
|
let i;
|
|
527
527
|
for (i = index - 1; i >= 0; i--) {
|
|
528
|
-
|
|
528
|
+
const {type, name, selfClosing, closing} = childNodes[i];
|
|
529
|
+
if (type === 'html' && (!tag || name === tag) && selfClosing === false && closing === false) {
|
|
529
530
|
break;
|
|
530
531
|
}
|
|
531
532
|
}
|
|
532
533
|
if (i === -1) {
|
|
533
534
|
return parentNode.findEnclosingHtml(tag);
|
|
534
535
|
}
|
|
535
|
-
const opening =
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
if (
|
|
536
|
+
const opening = childNodes[i];
|
|
537
|
+
for (i = index + 1; i < childNodes.length; i++) {
|
|
538
|
+
const {type, name, selfClosing, closing} = childNodes[i];
|
|
539
|
+
if (type === 'html' && name === opening.name && selfClosing === false && closing === true) {
|
|
539
540
|
break;
|
|
540
541
|
}
|
|
541
542
|
}
|
|
542
|
-
return i ===
|
|
543
|
+
return i === childNodes.length
|
|
543
544
|
? parentNode.findEnclosingHtml(tag)
|
|
544
|
-
: [opening,
|
|
545
|
+
: [opening, childNodes[i]];
|
|
545
546
|
}
|
|
546
547
|
|
|
547
548
|
/**
|
|
@@ -654,7 +655,9 @@ class Token extends AstElement {
|
|
|
654
655
|
/** 解析花括号 */
|
|
655
656
|
#parseBrackets() {
|
|
656
657
|
const parseBrackets = require('../parser/brackets');
|
|
657
|
-
this.
|
|
658
|
+
const str = this.type === 'root' ? String(this) : `\0${String(this)}`,
|
|
659
|
+
parsed = parseBrackets(str, this.#config, this.#accum);
|
|
660
|
+
this.setText(this.type === 'root' ? parsed : parsed.slice(1));
|
|
658
661
|
}
|
|
659
662
|
|
|
660
663
|
/** 解析HTML标签 */
|
|
@@ -718,9 +721,13 @@ class Token extends AstElement {
|
|
|
718
721
|
|
|
719
722
|
/** 解析列表 */
|
|
720
723
|
#parseList() {
|
|
724
|
+
if (this.type === 'image-parameter') {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
721
727
|
const parseList = require('../parser/list');
|
|
722
728
|
const lines = String(this).split('\n');
|
|
723
|
-
|
|
729
|
+
let i = this.type === 'root' || this.type === 'ext-inner' && this.type === 'poem' ? 0 : 1;
|
|
730
|
+
for (; i < lines.length; i++) {
|
|
724
731
|
lines[i] = parseList(lines[i], this.#config, this.#accum);
|
|
725
732
|
}
|
|
726
733
|
this.setText(lines.join('\n'));
|
package/src/link/category.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const Title = require('../../lib/title'),
|
|
4
4
|
Parser = require('../..'),
|
|
5
|
-
LinkToken = require('.')
|
|
6
|
-
Token = require('..');
|
|
5
|
+
LinkToken = require('.');
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* 分类
|
|
@@ -19,10 +18,11 @@ class CategoryToken extends LinkToken {
|
|
|
19
18
|
|
|
20
19
|
/** 分类排序关键字 */
|
|
21
20
|
get sortkey() {
|
|
22
|
-
return this.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
return this.childNodes[1]?.text()?.replaceAll(
|
|
22
|
+
/&#(\d+|x[\da-f]+);|\n/gu,
|
|
23
|
+
/** @param {string} p */
|
|
24
|
+
(_, p) => p ? String.fromCodePoint(p[0] === 'x' ? parseInt(p.slice(1), 16) : Number(p)) : '',
|
|
25
|
+
);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
set sortkey(text) {
|
package/src/link/file.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const Title = require('../../lib/title'),
|
|
4
4
|
{explode, noWrap} = require('../../util/string'),
|
|
5
5
|
{externalUse} = require('../../util/debug'),
|
|
6
|
+
{generateForChild} = require('../../util/lint'),
|
|
6
7
|
Parser = require('../..'),
|
|
7
8
|
LinkToken = require('.'),
|
|
8
9
|
ImageParameterToken = require('../imageParameter');
|
|
@@ -78,6 +79,26 @@ class FileToken extends LinkToken {
|
|
|
78
79
|
this.seal(['setLangLink', 'setFragment', 'asSelfLink', 'setLinkText', 'pipeTrick'], true);
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
/**
|
|
83
|
+
* @override
|
|
84
|
+
* @param {number} start 起始位置
|
|
85
|
+
*/
|
|
86
|
+
lint(start = 0) {
|
|
87
|
+
const errors = super.lint(start),
|
|
88
|
+
frameArgs = this.getFrameArgs(),
|
|
89
|
+
captions = this.getArgs('caption');
|
|
90
|
+
if (frameArgs.length > 1 || captions.size > 1) {
|
|
91
|
+
const rect = this.getRootNode().posFromIndex(start);
|
|
92
|
+
if (frameArgs.length > 1) {
|
|
93
|
+
errors.push(...frameArgs.map(arg => generateForChild(arg, rect, '重复或冲突的图片框架参数')));
|
|
94
|
+
}
|
|
95
|
+
if (captions.size > 1) {
|
|
96
|
+
errors.push(...[...captions].map(arg => generateForChild(arg, rect, '重复的图片说明')));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return errors;
|
|
100
|
+
}
|
|
101
|
+
|
|
81
102
|
/**
|
|
82
103
|
* @override
|
|
83
104
|
* @param {number} i 移除位置
|
|
@@ -251,13 +272,12 @@ class FileToken extends LinkToken {
|
|
|
251
272
|
}
|
|
252
273
|
const wikitext = `[[File:F|${syntax ? syntax.replace('$1', value) : value}]]`,
|
|
253
274
|
root = Parser.parse(wikitext, this.getAttribute('include'), 6, config),
|
|
254
|
-
{childNodes: {length},
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
) {
|
|
275
|
+
{childNodes: {length}, firstChild: file} = root,
|
|
276
|
+
{name, type, childNodes: {length: fileLength}, lastChild: imageParameter} = file;
|
|
277
|
+
if (length !== 1 || type !== 'file' || name !== 'File:F' || fileLength !== 2 || imageParameter.name !== key) {
|
|
258
278
|
throw new SyntaxError(`非法的 ${key} 参数:${noWrap(value)}`);
|
|
259
279
|
}
|
|
260
|
-
this.appendChild(
|
|
280
|
+
this.appendChild(imageParameter);
|
|
261
281
|
}
|
|
262
282
|
}
|
|
263
283
|
|