wikiparser-node 0.9.2-b → 0.10.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 +40 -0
- package/config/.schema.json +134 -0
- package/config/default.json +832 -0
- package/config/llwiki.json +630 -0
- package/config/moegirl.json +729 -0
- package/config/zhwiki.json +1269 -0
- package/i18n/zh-hans.json +1 -0
- package/i18n/zh-hant.json +1 -0
- package/index.js +333 -0
- package/lib/element.js +611 -0
- package/lib/node.js +770 -0
- package/lib/ranges.js +130 -0
- package/lib/text.js +263 -0
- package/lib/title.js +83 -0
- package/mixin/attributeParent.js +117 -0
- package/mixin/fixedToken.js +40 -0
- package/mixin/hidden.js +21 -0
- package/mixin/singleLine.js +31 -0
- package/mixin/sol.js +54 -0
- package/package.json +49 -47
- package/parser/brackets.js +126 -0
- package/parser/commentAndExt.js +59 -0
- package/parser/converter.js +46 -0
- package/parser/externalLinks.js +33 -0
- package/parser/hrAndDoubleUnderscore.js +49 -0
- package/parser/html.js +42 -0
- package/parser/links.js +94 -0
- package/parser/list.js +59 -0
- package/parser/magicLinks.js +41 -0
- package/parser/quotes.js +64 -0
- package/parser/selector.js +177 -0
- package/parser/table.js +114 -0
- package/src/arg.js +207 -0
- package/src/atom/hidden.js +13 -0
- package/src/atom/index.js +43 -0
- package/src/attribute.js +470 -0
- package/src/attributes.js +453 -0
- package/src/charinsert.js +97 -0
- package/src/converter.js +176 -0
- package/src/converterFlags.js +284 -0
- package/src/converterRule.js +256 -0
- package/src/extLink.js +180 -0
- package/src/gallery.js +149 -0
- package/src/hasNowiki/index.js +44 -0
- package/src/hasNowiki/pre.js +40 -0
- package/src/heading.js +134 -0
- package/src/html.js +254 -0
- package/src/imageParameter.js +303 -0
- package/src/imagemap.js +199 -0
- package/src/imagemapLink.js +41 -0
- package/src/index.js +932 -0
- package/src/link/category.js +44 -0
- package/src/link/file.js +287 -0
- package/src/link/galleryImage.js +120 -0
- package/src/link/index.js +388 -0
- package/src/magicLink.js +149 -0
- package/src/nested/choose.js +24 -0
- package/src/nested/combobox.js +23 -0
- package/src/nested/index.js +93 -0
- package/src/nested/references.js +23 -0
- package/src/nowiki/comment.js +71 -0
- package/src/nowiki/dd.js +59 -0
- package/src/nowiki/doubleUnderscore.js +56 -0
- package/src/nowiki/hr.js +41 -0
- package/src/nowiki/index.js +56 -0
- package/src/nowiki/list.js +16 -0
- package/src/nowiki/noinclude.js +28 -0
- package/src/nowiki/quote.js +69 -0
- package/src/onlyinclude.js +64 -0
- package/src/paramTag/index.js +89 -0
- package/src/paramTag/inputbox.js +35 -0
- package/src/parameter.js +239 -0
- package/src/syntax.js +91 -0
- package/src/table/index.js +983 -0
- package/src/table/td.js +338 -0
- package/src/table/tr.js +319 -0
- package/src/tagPair/ext.js +145 -0
- package/src/tagPair/include.js +50 -0
- package/src/tagPair/index.js +126 -0
- package/src/transclude.js +843 -0
- package/tool/index.js +1202 -0
- package/util/base.js +17 -0
- package/util/debug.js +73 -0
- package/util/diff.js +76 -0
- package/util/lint.js +55 -0
- package/util/string.js +126 -0
- package/bundle/bundle.min.js +0 -38
- package/extensions/editor.css +0 -62
- package/extensions/editor.js +0 -328
- package/extensions/ui.css +0 -119
package/src/html.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {generateForSelf} = require('../util/lint'),
|
|
4
|
+
{noWrap} = require('../util/string'),
|
|
5
|
+
fixedToken = require('../mixin/fixedToken'),
|
|
6
|
+
attributeParent = require('../mixin/attributeParent'),
|
|
7
|
+
Parser = require('..'),
|
|
8
|
+
Token = require('.');
|
|
9
|
+
|
|
10
|
+
const magicWords = new Set(['if', 'ifeq', 'ifexpr', 'ifexist', 'iferror', 'switch']);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* HTML标签
|
|
14
|
+
* @classdesc `{childNodes: [AttributesToken]}`
|
|
15
|
+
*/
|
|
16
|
+
class HtmlToken extends attributeParent(fixedToken(Token)) {
|
|
17
|
+
type = 'html';
|
|
18
|
+
#closing;
|
|
19
|
+
#selfClosing;
|
|
20
|
+
#tag;
|
|
21
|
+
|
|
22
|
+
/** getter */
|
|
23
|
+
get closing() {
|
|
24
|
+
return this.#closing;
|
|
25
|
+
}
|
|
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
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {string} name 标签名
|
|
64
|
+
* @param {AttributesToken} attr 标签属性
|
|
65
|
+
* @param {boolean} closing 是否闭合
|
|
66
|
+
* @param {boolean} selfClosing 是否自封闭
|
|
67
|
+
* @param {accum} accum
|
|
68
|
+
*/
|
|
69
|
+
constructor(name, attr, closing, selfClosing, config = Parser.getConfig(), accum = []) {
|
|
70
|
+
super(undefined, config, true, accum);
|
|
71
|
+
this.insertAt(attr);
|
|
72
|
+
this.setAttribute('name', name.toLowerCase());
|
|
73
|
+
this.#closing = closing;
|
|
74
|
+
this.#selfClosing = selfClosing;
|
|
75
|
+
this.#tag = name;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @override
|
|
80
|
+
* @param {string} selector
|
|
81
|
+
*/
|
|
82
|
+
toString(selector) {
|
|
83
|
+
return selector && this.matches(selector)
|
|
84
|
+
? ''
|
|
85
|
+
: `<${this.#closing ? '/' : ''}${this.#tag}${super.toString(selector)}${this.#selfClosing ? '/' : ''}>`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @override */
|
|
89
|
+
text() {
|
|
90
|
+
return `<${this.#closing ? '/' : ''}${this.#tag}${
|
|
91
|
+
this.#closing ? '' : super.text()
|
|
92
|
+
}${this.#selfClosing ? '/' : ''}>`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @override */
|
|
96
|
+
getPadding() {
|
|
97
|
+
return this.#tag.length + (this.#closing ? 2 : 1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** @override */
|
|
101
|
+
print() {
|
|
102
|
+
return super.print({
|
|
103
|
+
pre: `<${this.#closing ? '/' : ''}${this.#tag}`,
|
|
104
|
+
post: `${this.#selfClosing ? '/' : ''}>`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @override
|
|
110
|
+
* @param {number} start 起始位置
|
|
111
|
+
*/
|
|
112
|
+
lint(start = this.getAbsoluteIndex()) {
|
|
113
|
+
const errors = super.lint(start);
|
|
114
|
+
let wikitext, /** @type {LintError} */ refError;
|
|
115
|
+
if (this.name === 'h1' && !this.#closing) {
|
|
116
|
+
wikitext = String(this.getRootNode());
|
|
117
|
+
refError = generateForSelf(this, {start}, '<h1>');
|
|
118
|
+
errors.push({...refError, excerpt: wikitext.slice(start, start + 50)});
|
|
119
|
+
}
|
|
120
|
+
if (this.closest('table-attrs')) {
|
|
121
|
+
wikitext ||= String(this.getRootNode());
|
|
122
|
+
refError ||= generateForSelf(this, {start}, '');
|
|
123
|
+
const excerpt = wikitext.slice(Math.max(0, start - 25), start + 25);
|
|
124
|
+
errors.push({...refError, message: Parser.msg('HTML tag in table attributes'), excerpt});
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
this.findMatchingTag();
|
|
128
|
+
} catch ({message: errorMsg}) {
|
|
129
|
+
wikitext ||= String(this.getRootNode());
|
|
130
|
+
refError ||= generateForSelf(this, {start}, '');
|
|
131
|
+
const [msg] = errorMsg.split(':'),
|
|
132
|
+
error = {...refError, message: Parser.msg(msg)};
|
|
133
|
+
if (msg === 'unclosed tag') {
|
|
134
|
+
error.severity = 'warning';
|
|
135
|
+
error.excerpt = wikitext.slice(start, start + 50);
|
|
136
|
+
} else if (msg === 'unmatched closing tag') {
|
|
137
|
+
const end = start + String(this).length;
|
|
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
|
+
}
|
|
142
|
+
}
|
|
143
|
+
errors.push(error);
|
|
144
|
+
}
|
|
145
|
+
return errors;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 搜索匹配的标签
|
|
150
|
+
* @complexity `n`
|
|
151
|
+
* @throws `SyntaxError` 同时闭合和自封闭的标签
|
|
152
|
+
* @throws `SyntaxError` 无效自封闭标签
|
|
153
|
+
* @throws `SyntaxError` 未闭合的标签
|
|
154
|
+
*/
|
|
155
|
+
findMatchingTag() {
|
|
156
|
+
const {html} = this.getAttribute('config'),
|
|
157
|
+
{name: tagName, parentNode} = this,
|
|
158
|
+
string = noWrap(String(this));
|
|
159
|
+
if (this.#closing && (this.#selfClosing || html[2].includes(tagName))) {
|
|
160
|
+
throw new SyntaxError(`tag that is both closing and self-closing: ${string}`);
|
|
161
|
+
} else if (html[2].includes(tagName) || this.#selfClosing && html[1].includes(tagName)) { // 自封闭标签
|
|
162
|
+
return this;
|
|
163
|
+
} else if (this.#selfClosing && html[0].includes(tagName)) {
|
|
164
|
+
throw new SyntaxError(`invalid self-closing tag: ${string}`);
|
|
165
|
+
} else if (!parentNode) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
const {childNodes} = parentNode,
|
|
169
|
+
i = childNodes.indexOf(this),
|
|
170
|
+
siblings = this.#closing
|
|
171
|
+
? childNodes.slice(0, i).reverse().filter(({type, name}) => type === 'html' && name === tagName)
|
|
172
|
+
: childNodes.slice(i + 1).filter(({type, name}) => type === 'html' && name === tagName);
|
|
173
|
+
let imbalance = this.#closing ? -1 : 1;
|
|
174
|
+
for (const token of siblings) {
|
|
175
|
+
if (token.closing) {
|
|
176
|
+
imbalance--;
|
|
177
|
+
} else {
|
|
178
|
+
imbalance++;
|
|
179
|
+
}
|
|
180
|
+
if (imbalance === 0) {
|
|
181
|
+
return token;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
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
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
Parser.classes.HtmlToken = __filename;
|
|
254
|
+
module.exports = HtmlToken;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {text, noWrap, print, extUrlChar, extUrlCharFirst} = require('../util/string'),
|
|
4
|
+
{generateForSelf} = require('../util/lint'),
|
|
5
|
+
Title = require('../lib/title'),
|
|
6
|
+
Parser = require('..'),
|
|
7
|
+
AstText = require('../lib/text'),
|
|
8
|
+
Token = require('.');
|
|
9
|
+
|
|
10
|
+
const params = new Set(['alt', 'link', 'lang', 'page', 'caption']);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 检查图片参数是否合法
|
|
14
|
+
* @template {string} T
|
|
15
|
+
* @param {T} key 参数名
|
|
16
|
+
* @param {string} val 参数值
|
|
17
|
+
* @returns {T extends 'link' ? string|Title : boolean}
|
|
18
|
+
*/
|
|
19
|
+
const validate = (key, val, config = Parser.getConfig(), halfParsed = false) => {
|
|
20
|
+
val = val.trim();
|
|
21
|
+
let value = val.replace(/\0\d+t\x7F/gu, '').trim();
|
|
22
|
+
switch (key) {
|
|
23
|
+
case 'width':
|
|
24
|
+
return /^(?:\d+x?|\d*x\d+)$/u.test(value);
|
|
25
|
+
case 'link': {
|
|
26
|
+
if (!value) {
|
|
27
|
+
return val;
|
|
28
|
+
}
|
|
29
|
+
const regex = new RegExp(
|
|
30
|
+
`^(?:(?:${config.protocol}|//)${extUrlCharFirst}|\0\\d+m\x7F)${extUrlChar}$`,
|
|
31
|
+
'iu',
|
|
32
|
+
);
|
|
33
|
+
if (regex.test(value)) {
|
|
34
|
+
return val;
|
|
35
|
+
} else if (value.startsWith('[[') && value.endsWith(']]')) {
|
|
36
|
+
value = value.slice(2, -2);
|
|
37
|
+
}
|
|
38
|
+
const title = Parser.normalizeTitle(value, 0, false, config, halfParsed, true, true);
|
|
39
|
+
return title.valid && title;
|
|
40
|
+
}
|
|
41
|
+
case 'lang':
|
|
42
|
+
return config.variants.includes(value);
|
|
43
|
+
case 'alt':
|
|
44
|
+
case 'class':
|
|
45
|
+
case 'manualthumb':
|
|
46
|
+
return true;
|
|
47
|
+
default:
|
|
48
|
+
return !isNaN(value);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 图片参数
|
|
54
|
+
* @classdesc `{childNodes: ...(AstText|Token)}`
|
|
55
|
+
*/
|
|
56
|
+
class ImageParameterToken extends Token {
|
|
57
|
+
type = 'image-parameter';
|
|
58
|
+
#syntax = '';
|
|
59
|
+
|
|
60
|
+
/** 图片链接 */
|
|
61
|
+
get link() {
|
|
62
|
+
return this.name === 'link' ? validate('link', super.text(), this.getAttribute('config')) : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
set link(value) {
|
|
66
|
+
if (this.name === 'link') {
|
|
67
|
+
this.setValue(value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** getValue()的getter */
|
|
72
|
+
get value() {
|
|
73
|
+
return this.getValue();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
set value(value) {
|
|
77
|
+
this.setValue(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 图片大小 */
|
|
81
|
+
get size() {
|
|
82
|
+
if (this.name === 'width') {
|
|
83
|
+
const /** @type {string} */ size = this.getValue().trim();
|
|
84
|
+
if (!size.includes('{{')) {
|
|
85
|
+
const [width, height = ''] = size.split('x');
|
|
86
|
+
return {width, height};
|
|
87
|
+
}
|
|
88
|
+
const /** @type {{childNodes: AstText[]}} */ token = Parser.parse(size, false, 2, this.getAttribute('config')),
|
|
89
|
+
i = token.childNodes.findIndex(({type, data}) => type === 'text' && data.includes('x')),
|
|
90
|
+
str = token.childNodes[i];
|
|
91
|
+
if (i === -1) {
|
|
92
|
+
return {width: size, height: ''};
|
|
93
|
+
}
|
|
94
|
+
str.splitText(str.data.indexOf('x'));
|
|
95
|
+
str.nextSibling.splitText(1);
|
|
96
|
+
return {width: text(token.childNodes.slice(0, i + 1)), height: text(token.childNodes.slice(i + 2))};
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 图片宽度 */
|
|
102
|
+
get width() {
|
|
103
|
+
return this.size?.width;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
set width(width) {
|
|
107
|
+
if (this.name === 'width') {
|
|
108
|
+
const {height} = this;
|
|
109
|
+
this.setValue(`${String(width || '')}${height && 'x'}${height}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 图片高度 */
|
|
114
|
+
get height() {
|
|
115
|
+
return this.size?.height;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
set height(height) {
|
|
119
|
+
height = String(height || '');
|
|
120
|
+
if (this.name === 'width') {
|
|
121
|
+
this.setValue(`${this.width}${height && 'x'}${height}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} str 图片参数
|
|
127
|
+
* @param {accum} accum
|
|
128
|
+
*/
|
|
129
|
+
constructor(str, config = Parser.getConfig(), accum = []) {
|
|
130
|
+
let mt;
|
|
131
|
+
const regexes = Object.entries(config.img).map(
|
|
132
|
+
/** @returns {[string, string, RegExp]} */
|
|
133
|
+
([syntax, param]) => [syntax, param, new RegExp(`^(\\s*)${syntax.replace('$1', '(.*)')}(\\s*)$`, 'u')],
|
|
134
|
+
),
|
|
135
|
+
param = regexes.find(([, key, regex]) => {
|
|
136
|
+
mt = regex.exec(str);
|
|
137
|
+
return mt && (mt.length !== 4 || validate(key, mt[2], config, true) !== false);
|
|
138
|
+
});
|
|
139
|
+
if (param) {
|
|
140
|
+
if (mt.length === 3) {
|
|
141
|
+
super(undefined, config, true, accum);
|
|
142
|
+
this.#syntax = str;
|
|
143
|
+
} else {
|
|
144
|
+
super(mt[2], config, true, accum, {
|
|
145
|
+
'Stage-2': ':', '!HeadingToken': ':',
|
|
146
|
+
});
|
|
147
|
+
this.#syntax = `${mt[1]}${param[0]}${mt[3]}`;
|
|
148
|
+
}
|
|
149
|
+
this.setAttribute('name', param[1]);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
super(str, {...config, excludes: [...config.excludes, 'list']}, true, accum);
|
|
153
|
+
this.setAttribute('name', 'caption').setAttribute('stage', 7);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** @override */
|
|
157
|
+
afterBuild() {
|
|
158
|
+
if (this.parentNode.type === 'gallery-image' && !params.has(this.name)) {
|
|
159
|
+
this.setAttribute('name', 'invalid');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** @override */
|
|
164
|
+
isPlain() {
|
|
165
|
+
return this.name === 'caption';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @override
|
|
170
|
+
* @param {string} selector
|
|
171
|
+
*/
|
|
172
|
+
toString(selector) {
|
|
173
|
+
return this.#syntax && !(selector && this.matches(selector))
|
|
174
|
+
? this.#syntax.replace('$1', super.toString(selector))
|
|
175
|
+
: super.toString(selector);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** @override */
|
|
179
|
+
text() {
|
|
180
|
+
return this.#syntax ? this.#syntax.replace('$1', super.text()).trim() : super.text().trim();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** @override */
|
|
184
|
+
getPadding() {
|
|
185
|
+
return Math.max(0, this.#syntax.indexOf('$1'));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @override
|
|
190
|
+
* @this {ImageParameterToken & {link: Title}}
|
|
191
|
+
* @param {number} start 起始位置
|
|
192
|
+
*/
|
|
193
|
+
lint(start = this.getAbsoluteIndex()) {
|
|
194
|
+
const errors = super.lint(start);
|
|
195
|
+
if (this.name === 'invalid') {
|
|
196
|
+
errors.push(generateForSelf(this, {start}, 'invalid gallery image parameter'));
|
|
197
|
+
} else if (this.link?.encoded) {
|
|
198
|
+
errors.push(generateForSelf(this, {start}, 'unnecessary URL encoding in an internal link'));
|
|
199
|
+
}
|
|
200
|
+
return errors;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** @override */
|
|
204
|
+
print() {
|
|
205
|
+
return this.#syntax
|
|
206
|
+
? `<span class="wpb-image-parameter">${
|
|
207
|
+
this.#syntax.replace('$1', `<span class="wpb-image-caption">${print(this.childNodes)}</span>`)
|
|
208
|
+
}</span>`
|
|
209
|
+
: super.print({class: 'image-caption'});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** @override */
|
|
213
|
+
cloneNode() {
|
|
214
|
+
const cloned = this.cloneChildNodes(),
|
|
215
|
+
config = this.getAttribute('config');
|
|
216
|
+
return Parser.run(() => {
|
|
217
|
+
const token = new ImageParameterToken(this.#syntax.replace('$1', ''), config);
|
|
218
|
+
token.replaceChildren(...cloned);
|
|
219
|
+
return token;
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @override
|
|
225
|
+
* @template {string} T
|
|
226
|
+
* @param {T} key 属性键
|
|
227
|
+
* @returns {TokenAttribute<T>}
|
|
228
|
+
*/
|
|
229
|
+
getAttribute(key) {
|
|
230
|
+
return key === 'syntax' ? this.#syntax : super.getAttribute(key);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @override
|
|
235
|
+
* @param {PropertyKey} key 属性键
|
|
236
|
+
*/
|
|
237
|
+
hasAttribute(key) {
|
|
238
|
+
return key === 'syntax' || super.hasAttribute(key);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** 是否是不可变参数 */
|
|
242
|
+
#isVoid() {
|
|
243
|
+
return this.#syntax && !this.#syntax.includes('$1');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @override
|
|
248
|
+
* @template {Token} T
|
|
249
|
+
* @param {T} token 待插入的子节点
|
|
250
|
+
* @param {number} i 插入位置
|
|
251
|
+
* @complexity `n`
|
|
252
|
+
* @throws `Error` 不接受自定义输入的图片参数
|
|
253
|
+
*/
|
|
254
|
+
insertAt(token, i = this.length) {
|
|
255
|
+
if (!Parser.running && this.#isVoid()) {
|
|
256
|
+
throw new Error(`图片参数 ${this.name} 不接受自定义输入!`);
|
|
257
|
+
}
|
|
258
|
+
return super.insertAt(token, i);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 获取参数值
|
|
263
|
+
* @complexity `n`
|
|
264
|
+
*/
|
|
265
|
+
getValue() {
|
|
266
|
+
return this.name === 'invalid' ? this.text() : this.#isVoid() || super.text();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* 设置参数值
|
|
271
|
+
* @param {string|boolean} value 参数值
|
|
272
|
+
* @complexity `n`
|
|
273
|
+
* @throws SyntaxError` 非法的参数值
|
|
274
|
+
*/
|
|
275
|
+
setValue(value) {
|
|
276
|
+
if (this.name === 'invalid') {
|
|
277
|
+
throw new Error('无效的图片参数!');
|
|
278
|
+
} else if (this.#isVoid()) {
|
|
279
|
+
if (typeof value !== 'boolean') {
|
|
280
|
+
this.typeError('setValue', 'Boolean');
|
|
281
|
+
} else if (value === false) {
|
|
282
|
+
this.remove();
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
} else if (typeof value !== 'string') {
|
|
286
|
+
this.typeError('setValue', 'String');
|
|
287
|
+
}
|
|
288
|
+
const root = Parser.parse(`[[File:F|${
|
|
289
|
+
this.#syntax ? this.#syntax.replace('$1', value) : value
|
|
290
|
+
}]]`, this.getAttribute('include'), 6, this.getAttribute('config')),
|
|
291
|
+
{length, firstChild: file} = root,
|
|
292
|
+
{lastChild: imageParameter, type, name, length: fileLength} = file;
|
|
293
|
+
if (length !== 1 || type !== 'file' || name !== 'File:F' || fileLength !== 2
|
|
294
|
+
|| imageParameter.name !== this.name
|
|
295
|
+
) {
|
|
296
|
+
throw new SyntaxError(`非法的 ${this.name} 参数:${noWrap(value)}`);
|
|
297
|
+
}
|
|
298
|
+
this.replaceChildren(...imageParameter.childNodes);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
Parser.classes.ImageParameterToken = __filename;
|
|
303
|
+
module.exports = ImageParameterToken;
|