wikiparser-node 0.5.0 → 0.6.1
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/config/default.json +129 -66
- package/config/zhwiki.json +4 -4
- package/index.js +74 -65
- package/lib/element.js +125 -152
- package/lib/node.js +251 -223
- package/lib/ranges.js +2 -2
- package/lib/text.js +64 -64
- package/lib/title.js +8 -7
- package/mixin/hidden.js +2 -0
- package/mixin/sol.js +1 -2
- package/package.json +4 -3
- package/parser/brackets.js +8 -2
- package/parser/externalLinks.js +1 -1
- package/parser/hrAndDoubleUnderscore.js +4 -4
- package/parser/links.js +7 -7
- package/parser/table.js +12 -10
- package/src/arg.js +53 -48
- package/src/atom/index.js +7 -5
- package/src/attribute.js +91 -80
- package/src/charinsert.js +91 -0
- package/src/converter.js +22 -11
- package/src/converterFlags.js +72 -62
- package/src/converterRule.js +49 -49
- package/src/extLink.js +30 -28
- package/src/gallery.js +56 -32
- package/src/hasNowiki/index.js +42 -0
- package/src/hasNowiki/pre.js +40 -0
- package/src/heading.js +15 -11
- package/src/html.js +38 -38
- package/src/imageParameter.js +64 -48
- package/src/imagemap.js +205 -0
- package/src/imagemapLink.js +43 -0
- package/src/index.js +222 -124
- package/src/link/category.js +4 -8
- package/src/link/file.js +95 -59
- package/src/link/galleryImage.js +74 -10
- package/src/link/index.js +61 -39
- package/src/magicLink.js +21 -22
- package/src/nested/choose.js +24 -0
- package/src/nested/combobox.js +23 -0
- package/src/nested/index.js +88 -0
- package/src/nested/references.js +23 -0
- package/src/nowiki/comment.js +17 -17
- package/src/nowiki/dd.js +2 -2
- package/src/nowiki/doubleUnderscore.js +14 -14
- package/src/nowiki/index.js +12 -0
- package/src/onlyinclude.js +10 -8
- package/src/paramTag/index.js +83 -0
- package/src/paramTag/inputbox.js +42 -0
- package/src/parameter.js +32 -18
- package/src/syntax.js +9 -1
- package/src/table/index.js +33 -32
- package/src/table/td.js +51 -57
- package/src/table/tr.js +6 -6
- package/src/tagPair/ext.js +58 -40
- package/src/tagPair/include.js +1 -1
- package/src/tagPair/index.js +21 -20
- package/src/transclude.js +158 -143
- package/tool/index.js +720 -439
- package/util/base.js +17 -0
- package/util/debug.js +1 -1
- package/util/diff.js +1 -1
- package/util/string.js +20 -20
package/src/imagemap.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {generateForSelf, generateForChild} = require('../util/lint'),
|
|
4
|
+
Parser = require('..'),
|
|
5
|
+
AstText = require('../lib/text'),
|
|
6
|
+
Token = require('.'),
|
|
7
|
+
NoincludeToken = require('./nowiki/noinclude'),
|
|
8
|
+
GalleryImageToken = require('./link/galleryImage'),
|
|
9
|
+
ImagemapLinkToken = require('./imagemapLink');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `<imagemap>`
|
|
13
|
+
* @classdesc `{childNodes: ...NoincludeToken, GalleryImageToken, ...(NoincludeToken|ImagemapLinkToken|AstText)}`
|
|
14
|
+
*/
|
|
15
|
+
class ImagemapToken extends Token {
|
|
16
|
+
type = 'ext-inner';
|
|
17
|
+
name = 'imagemap';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 图片
|
|
21
|
+
* @returns {GalleryImageToken}
|
|
22
|
+
*/
|
|
23
|
+
get image() {
|
|
24
|
+
return this.childNodes.find(({type}) => type === 'imagemap-image');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 链接
|
|
29
|
+
* @returns {ImagemapLinkToken[]}
|
|
30
|
+
*/
|
|
31
|
+
get links() {
|
|
32
|
+
return this.childNodes.filter(({type}) => type === 'imagemap-link');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} inner 标签内部wikitext
|
|
37
|
+
* @param {accum} accum
|
|
38
|
+
* @throws `SyntaxError` 没有合法图片
|
|
39
|
+
*/
|
|
40
|
+
constructor(inner, config = Parser.getConfig(), accum = []) {
|
|
41
|
+
super(undefined, config, true, accum, {
|
|
42
|
+
GalleryImageToken: ':', ImagemapLinkToken: ':', NoincludeToken: ':', AstText: ':',
|
|
43
|
+
});
|
|
44
|
+
if (!inner) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const lines = inner.split('\n'),
|
|
48
|
+
fallback = /** @param {string} line 一行文本 */ line => {
|
|
49
|
+
super.insertAt(new NoincludeToken(line, config, accum));
|
|
50
|
+
};
|
|
51
|
+
let first = true,
|
|
52
|
+
error = false;
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
const trimmed = line.trim();
|
|
55
|
+
if (error || !trimmed || trimmed[0] === '#') {
|
|
56
|
+
//
|
|
57
|
+
} else if (first) {
|
|
58
|
+
const [file, ...options] = line.split('|');
|
|
59
|
+
let title;
|
|
60
|
+
try {
|
|
61
|
+
title = this.normalizeTitle(decodeURIComponent(file), 0, true);
|
|
62
|
+
} catch {
|
|
63
|
+
title = this.normalizeTitle(file, 0, true);
|
|
64
|
+
}
|
|
65
|
+
if (title.valid && !title.interwiki && title.ns === 6) {
|
|
66
|
+
const token = new GalleryImageToken(
|
|
67
|
+
file, options.length > 0 ? options.join('|') : undefined, title, config, accum,
|
|
68
|
+
);
|
|
69
|
+
token.type = 'imagemap-image';
|
|
70
|
+
super.insertAt(token);
|
|
71
|
+
first = false;
|
|
72
|
+
continue;
|
|
73
|
+
} else {
|
|
74
|
+
Parser.error('<imagemap>标签内必须先包含一张合法图片!', line);
|
|
75
|
+
error = true;
|
|
76
|
+
}
|
|
77
|
+
} else if (line.trim().split(/[\t ]/u)[0] === 'desc') {
|
|
78
|
+
super.insertAt(new AstText(line));
|
|
79
|
+
continue;
|
|
80
|
+
} else if (line.includes('[')) {
|
|
81
|
+
const i = line.indexOf('['),
|
|
82
|
+
substr = line.slice(i),
|
|
83
|
+
mtIn = /^\[{2}([^|]+)(?:\|([^\]]+))?\]{2}[\w\s]*$/u.exec(substr);
|
|
84
|
+
if (mtIn) {
|
|
85
|
+
const title = this.normalizeTitle(mtIn[1], 0, true);
|
|
86
|
+
if (title.valid) {
|
|
87
|
+
super.insertAt(new ImagemapLinkToken(
|
|
88
|
+
line.slice(0, i),
|
|
89
|
+
[...mtIn.slice(1), title],
|
|
90
|
+
substr.slice(substr.indexOf(']]') + 2),
|
|
91
|
+
config,
|
|
92
|
+
accum,
|
|
93
|
+
));
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
const protocols = config.protocol.split('|');
|
|
98
|
+
if (protocols.includes(substr.slice(1, substr.indexOf(':') + 1))
|
|
99
|
+
|| protocols.includes(substr.slice(1, substr.indexOf('//') + 2))
|
|
100
|
+
) {
|
|
101
|
+
const mtEx = /^\[([^\]\s]+)(?:(\s+)(\S[^\]]*)?)?\][\w\s]*$/u.exec(substr);
|
|
102
|
+
if (mtEx) {
|
|
103
|
+
super.insertAt(new ImagemapLinkToken(
|
|
104
|
+
line.slice(0, i),
|
|
105
|
+
mtEx.slice(1),
|
|
106
|
+
substr.slice(substr.indexOf(']') + 1),
|
|
107
|
+
config,
|
|
108
|
+
accum,
|
|
109
|
+
));
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
fallback(line);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @override
|
|
121
|
+
* @param {string} selector
|
|
122
|
+
*/
|
|
123
|
+
toString(selector) {
|
|
124
|
+
return super.toString(selector, '\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** @override */
|
|
128
|
+
getGaps() {
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** @override */
|
|
133
|
+
print() {
|
|
134
|
+
return super.print({sep: '\n'});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @override
|
|
139
|
+
* @param {number} start 起始位置
|
|
140
|
+
*/
|
|
141
|
+
lint(start = 0) {
|
|
142
|
+
const errors = super.lint(start),
|
|
143
|
+
rect = this.getRootNode().posFromIndex(start);
|
|
144
|
+
if (this.image) {
|
|
145
|
+
errors.push(
|
|
146
|
+
...this.childNodes.filter(child => {
|
|
147
|
+
const str = String(child).trim();
|
|
148
|
+
return child.type === 'noinclude' && str && str[0] !== '#';
|
|
149
|
+
}).map(child => generateForChild(child, rect, '无效的<imagemap>链接')),
|
|
150
|
+
);
|
|
151
|
+
} else {
|
|
152
|
+
errors.push(generateForSelf(this, rect, '缺少图片的<imagemap>'));
|
|
153
|
+
}
|
|
154
|
+
return errors;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @override
|
|
159
|
+
* @template {string|Token} T
|
|
160
|
+
* @param {T} token 待插入的节点
|
|
161
|
+
* @param {number} i 插入位置
|
|
162
|
+
* @throws `Error` 当前缺少合法图片
|
|
163
|
+
* @throws `RangeError` 已有一张合法图片
|
|
164
|
+
*/
|
|
165
|
+
insertAt(token, i = 0) {
|
|
166
|
+
const {image} = this;
|
|
167
|
+
if (!image && (token.type === 'imagemap-link' || token.type === 'text')) {
|
|
168
|
+
throw new Error('当前缺少一张合法图片!');
|
|
169
|
+
} else if (image && token.type === 'imagemap-image') {
|
|
170
|
+
throw new RangeError('已有一张合法图片!');
|
|
171
|
+
}
|
|
172
|
+
return super.insertAt(token, i);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @override
|
|
177
|
+
* @param {number} i 移除位置
|
|
178
|
+
* @throws `Error` 禁止移除图片
|
|
179
|
+
*/
|
|
180
|
+
removeAt(i) {
|
|
181
|
+
const child = this.childNodes[i];
|
|
182
|
+
if (child.type === 'imagemap-image') {
|
|
183
|
+
throw new Error('禁止移除<imagemap>内的图片!');
|
|
184
|
+
}
|
|
185
|
+
return super.removeAt(i);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** @override */
|
|
189
|
+
cloneNode() {
|
|
190
|
+
const cloned = this.cloneChildNodes();
|
|
191
|
+
return Parser.run(() => {
|
|
192
|
+
const token = new ImagemapToken(undefined, this.getAttribute('config'));
|
|
193
|
+
token.append(...cloned);
|
|
194
|
+
return token;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** @override */
|
|
199
|
+
text() {
|
|
200
|
+
return super.text('\n').replaceAll(/\n{2,}/gu, '\n');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
Parser.classes.ImagemapToken = __filename;
|
|
205
|
+
module.exports = ImagemapToken;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fixedToken = require('../mixin/fixedToken'),
|
|
4
|
+
Parser = require('..'),
|
|
5
|
+
Token = require('.'),
|
|
6
|
+
LinkToken = require('./link'),
|
|
7
|
+
ExtLinkToken = require('./extLink');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `<imagemap>`内的链接
|
|
11
|
+
* @classdesc `{childNodes: [AstText, LinkToken|ExtLinkToken, NoincludeToken]}`
|
|
12
|
+
*/
|
|
13
|
+
class ImagemapLinkToken extends fixedToken(Token) {
|
|
14
|
+
type = 'imagemap-link';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 内外链接
|
|
18
|
+
* @this {{childNodes: (LinkToken|ExtLinkToken)[]}}
|
|
19
|
+
*/
|
|
20
|
+
get link() {
|
|
21
|
+
return this.childNodes[1].link;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} pre 链接前的文本
|
|
26
|
+
* @param {[string, string, string|Title]} linkStuff 内外链接
|
|
27
|
+
* @param {string} post 链接后的文本
|
|
28
|
+
* @param {accum} accum
|
|
29
|
+
*/
|
|
30
|
+
constructor(pre, linkStuff, post, config, accum) {
|
|
31
|
+
const Title = require('../lib/title'),
|
|
32
|
+
AstText = require('../lib/text'),
|
|
33
|
+
NoincludeToken = require('./nowiki/noinclude');
|
|
34
|
+
const SomeLinkToken = linkStuff[2] instanceof Title ? LinkToken : ExtLinkToken;
|
|
35
|
+
super(undefined, config, true, accum);
|
|
36
|
+
this.append(
|
|
37
|
+
new AstText(pre), new SomeLinkToken(...linkStuff, config, accum), new NoincludeToken(post, config, accum),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Parser.classes.ImagemapLinkToken = __filename;
|
|
43
|
+
module.exports = ImagemapLinkToken;
|
package/src/index.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* -: `{{!-}}`专用
|
|
29
29
|
* +: `{{!!}}`专用
|
|
30
30
|
* ~: `{{=}}`专用
|
|
31
|
+
* m: `{{fullurl:}}`、`{{canonicalurl:}}`或`{{filepath:}}`
|
|
31
32
|
* t: ArgToken或TranscludeToken
|
|
32
33
|
* h: HeadingToken
|
|
33
34
|
* x: HtmlToken
|
|
@@ -64,14 +65,6 @@ class Token extends AstElement {
|
|
|
64
65
|
#protectedChildren = new Ranges();
|
|
65
66
|
/** @type {boolean} */ #include;
|
|
66
67
|
|
|
67
|
-
/**
|
|
68
|
-
* 保护部分子节点不被移除
|
|
69
|
-
* @param {...string|number|Range} args 子节点范围
|
|
70
|
-
*/
|
|
71
|
-
#protectChildren = (...args) => {
|
|
72
|
-
this.#protectedChildren.push(...new Ranges(args));
|
|
73
|
-
};
|
|
74
|
-
|
|
75
68
|
/**
|
|
76
69
|
* 将维基语法替换为占位符
|
|
77
70
|
* @param {number} n 解析阶段
|
|
@@ -144,9 +137,36 @@ class Token extends AstElement {
|
|
|
144
137
|
throw new Error(`解析错误!未正确标记的 Token:${s}`);
|
|
145
138
|
});
|
|
146
139
|
|
|
140
|
+
/**
|
|
141
|
+
* 将占位符替换为子Token
|
|
142
|
+
* @complexity `n`
|
|
143
|
+
*/
|
|
144
|
+
#build = () => {
|
|
145
|
+
this.#stage = MAX_STAGE;
|
|
146
|
+
const {length, firstChild} = this,
|
|
147
|
+
str = String(firstChild);
|
|
148
|
+
if (length === 1 && firstChild.type === 'text' && str.includes('\0')) {
|
|
149
|
+
this.replaceChildren(...this.#buildFromStr(str));
|
|
150
|
+
this.normalize();
|
|
151
|
+
if (this.type === 'root') {
|
|
152
|
+
for (const token of this.#accum) {
|
|
153
|
+
token.getAttribute('build')();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 保护部分子节点不被移除
|
|
161
|
+
* @param {...string|number|Range} args 子节点范围
|
|
162
|
+
*/
|
|
163
|
+
#protectChildren = (...args) => {
|
|
164
|
+
this.#protectedChildren.push(...new Ranges(args));
|
|
165
|
+
};
|
|
166
|
+
|
|
147
167
|
/** 所有图片,包括图库 */
|
|
148
168
|
get images() {
|
|
149
|
-
return this.querySelectorAll('file, gallery-image');
|
|
169
|
+
return this.querySelectorAll('file, gallery-image, imagemap-image');
|
|
150
170
|
}
|
|
151
171
|
|
|
152
172
|
/** 所有内链、外链和自由外链 */
|
|
@@ -167,7 +187,7 @@ class Token extends AstElement {
|
|
|
167
187
|
constructor(wikitext, config = Parser.getConfig(), halfParsed = false, accum = [], acceptable = null) {
|
|
168
188
|
super();
|
|
169
189
|
if (typeof wikitext === 'string') {
|
|
170
|
-
this.
|
|
190
|
+
this.insertAt(halfParsed ? wikitext : wikitext.replaceAll(/[\0\x7F]/gu, ''));
|
|
171
191
|
}
|
|
172
192
|
this.#config = config;
|
|
173
193
|
this.#accum = accum;
|
|
@@ -175,34 +195,6 @@ class Token extends AstElement {
|
|
|
175
195
|
accum.push(this);
|
|
176
196
|
}
|
|
177
197
|
|
|
178
|
-
/**
|
|
179
|
-
* 深拷贝所有子节点
|
|
180
|
-
* @complexity `n`
|
|
181
|
-
* @returns {(AstText|Token)[]}
|
|
182
|
-
*/
|
|
183
|
-
cloneChildNodes() {
|
|
184
|
-
return this.childNodes.map(child => child.cloneNode());
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* 深拷贝节点
|
|
189
|
-
* @complexity `n`
|
|
190
|
-
* @throws `Error` 未定义复制方法
|
|
191
|
-
*/
|
|
192
|
-
cloneNode() {
|
|
193
|
-
if (!this.isPlain()) {
|
|
194
|
-
throw new Error(`未定义 ${this.constructor.name} 的复制方法!`);
|
|
195
|
-
}
|
|
196
|
-
const cloned = this.cloneChildNodes();
|
|
197
|
-
return Parser.run(() => {
|
|
198
|
-
const token = new Token(undefined, this.#config, false, [], this.#acceptable);
|
|
199
|
-
token.type = this.type;
|
|
200
|
-
token.append(...cloned);
|
|
201
|
-
token.getAttribute('protectChildren')(...this.#protectedChildren);
|
|
202
|
-
return token;
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
198
|
/**
|
|
207
199
|
* @override
|
|
208
200
|
* @template {string} T
|
|
@@ -211,22 +203,24 @@ class Token extends AstElement {
|
|
|
211
203
|
*/
|
|
212
204
|
getAttribute(key) {
|
|
213
205
|
switch (key) {
|
|
214
|
-
case 'stage':
|
|
215
|
-
return this.#stage;
|
|
216
206
|
case 'config':
|
|
217
207
|
return structuredClone(this.#config);
|
|
218
208
|
case 'accum':
|
|
219
209
|
return this.#accum;
|
|
210
|
+
case 'parseOnce':
|
|
211
|
+
return this.#parseOnce;
|
|
212
|
+
case 'buildFromStr':
|
|
213
|
+
return this.#buildFromStr;
|
|
214
|
+
case 'build':
|
|
215
|
+
return this.#build;
|
|
216
|
+
case 'stage':
|
|
217
|
+
return this.#stage;
|
|
220
218
|
case 'acceptable':
|
|
221
219
|
return this.#acceptable ? {...this.#acceptable} : null;
|
|
222
220
|
case 'protectChildren':
|
|
223
221
|
return this.#protectChildren;
|
|
224
222
|
case 'protectedChildren':
|
|
225
223
|
return new Ranges(this.#protectedChildren);
|
|
226
|
-
case 'parseOnce':
|
|
227
|
-
return this.#parseOnce;
|
|
228
|
-
case 'buildFromStr':
|
|
229
|
-
return this.#buildFromStr;
|
|
230
224
|
case 'include': {
|
|
231
225
|
if (this.#include !== undefined) {
|
|
232
226
|
return this.#include;
|
|
@@ -252,16 +246,8 @@ class Token extends AstElement {
|
|
|
252
246
|
* @template {string} T
|
|
253
247
|
* @param {T} key 属性键
|
|
254
248
|
* @param {TokenAttribute<T>} value 属性值
|
|
255
|
-
* @throws `RangeError` 禁止手动指定私有属性
|
|
256
249
|
*/
|
|
257
250
|
setAttribute(key, value) {
|
|
258
|
-
if (key === 'include' || !Parser.running && (key === 'config' || key === 'accum')) {
|
|
259
|
-
throw new RangeError(`禁止手动指定私有的 #${key} 属性!`);
|
|
260
|
-
} else if (!Parser.debugging && (key === 'stage' || key === 'acceptable' || key === 'protectedChildren')
|
|
261
|
-
&& externalUse('setAttribute')
|
|
262
|
-
) {
|
|
263
|
-
throw new RangeError(`使用 ${this.constructor.name}.setAttribute 方法设置私有属性 #${key} 仅用于代码调试!`);
|
|
264
|
-
}
|
|
265
251
|
switch (key) {
|
|
266
252
|
case 'stage':
|
|
267
253
|
if (this.#stage === 0 && this.type === 'root') {
|
|
@@ -269,15 +255,6 @@ class Token extends AstElement {
|
|
|
269
255
|
}
|
|
270
256
|
this.#stage = value;
|
|
271
257
|
return this;
|
|
272
|
-
case 'config':
|
|
273
|
-
this.#config = value;
|
|
274
|
-
return this;
|
|
275
|
-
case 'accum':
|
|
276
|
-
this.#accum = value;
|
|
277
|
-
return this;
|
|
278
|
-
case 'protectedChildren':
|
|
279
|
-
this.#protectedChildren = value;
|
|
280
|
-
return this;
|
|
281
258
|
case 'acceptable': {
|
|
282
259
|
const /** @type {acceptable} */ acceptable = {};
|
|
283
260
|
if (value) {
|
|
@@ -308,36 +285,6 @@ class Token extends AstElement {
|
|
|
308
285
|
return this.constructor === Token;
|
|
309
286
|
}
|
|
310
287
|
|
|
311
|
-
/**
|
|
312
|
-
* @override
|
|
313
|
-
* @param {number} i 移除位置
|
|
314
|
-
* @returns {Token}
|
|
315
|
-
* @complexity `n`
|
|
316
|
-
* @throws `Error` 不可移除的子节点
|
|
317
|
-
*/
|
|
318
|
-
removeAt(i) {
|
|
319
|
-
if (typeof i !== 'number') {
|
|
320
|
-
this.typeError('removeAt', 'Number');
|
|
321
|
-
}
|
|
322
|
-
const iPos = i < 0 ? i + this.childNodes.length : i;
|
|
323
|
-
if (!Parser.running) {
|
|
324
|
-
const protectedIndices = this.#protectedChildren.applyTo(this.childNodes);
|
|
325
|
-
if (protectedIndices.includes(iPos)) {
|
|
326
|
-
throw new Error(`${this.constructor.name} 的第 ${i} 个子节点不可移除!`);
|
|
327
|
-
} else if (this.#acceptable) {
|
|
328
|
-
const acceptableIndices = Object.fromEntries(
|
|
329
|
-
Object.entries(this.#acceptable)
|
|
330
|
-
.map(([str, ranges]) => [str, ranges.applyTo(this.childNodes.length - 1)]),
|
|
331
|
-
),
|
|
332
|
-
nodesAfter = i === -1 ? [] : this.childNodes.slice(i + 1);
|
|
333
|
-
if (nodesAfter.some(({constructor: {name}}, j) => !acceptableIndices[name].includes(i + j))) {
|
|
334
|
-
throw new Error(`移除 ${this.constructor.name} 的第 ${i} 个子节点会破坏规定的顺序!`);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
return super.removeAt(i);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
288
|
/**
|
|
342
289
|
* @override
|
|
343
290
|
* @template {string|Token} T
|
|
@@ -372,6 +319,45 @@ class Token extends AstElement {
|
|
|
372
319
|
return token;
|
|
373
320
|
}
|
|
374
321
|
|
|
322
|
+
/**
|
|
323
|
+
* 规范化页面标题
|
|
324
|
+
* @param {string} title 标题(含或不含命名空间前缀)
|
|
325
|
+
* @param {number} defaultNs 命名空间
|
|
326
|
+
*/
|
|
327
|
+
normalizeTitle(title, defaultNs = 0, halfParsed = false) {
|
|
328
|
+
return Parser.normalizeTitle(title, defaultNs, this.#include, this.#config, halfParsed);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* @override
|
|
333
|
+
* @param {number} i 移除位置
|
|
334
|
+
* @returns {Token}
|
|
335
|
+
* @complexity `n`
|
|
336
|
+
* @throws `Error` 不可移除的子节点
|
|
337
|
+
*/
|
|
338
|
+
removeAt(i) {
|
|
339
|
+
if (!Number.isInteger(i)) {
|
|
340
|
+
this.typeError('removeAt', 'Number');
|
|
341
|
+
}
|
|
342
|
+
const iPos = i < 0 ? i + this.childNodes.length : i;
|
|
343
|
+
if (!Parser.running) {
|
|
344
|
+
const protectedIndices = this.#protectedChildren.applyTo(this.childNodes);
|
|
345
|
+
if (protectedIndices.includes(iPos)) {
|
|
346
|
+
throw new Error(`${this.constructor.name} 的第 ${i} 个子节点不可移除!`);
|
|
347
|
+
} else if (this.#acceptable) {
|
|
348
|
+
const acceptableIndices = Object.fromEntries(
|
|
349
|
+
Object.entries(this.#acceptable)
|
|
350
|
+
.map(([str, ranges]) => [str, ranges.applyTo(this.childNodes.length - 1)]),
|
|
351
|
+
),
|
|
352
|
+
nodesAfter = i === -1 ? [] : this.childNodes.slice(i + 1);
|
|
353
|
+
if (nodesAfter.some(({constructor: {name}}, j) => !acceptableIndices[name].includes(i + j))) {
|
|
354
|
+
throw new Error(`移除 ${this.constructor.name} 的第 ${i} 个子节点会破坏规定的顺序!`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return super.removeAt(i);
|
|
359
|
+
}
|
|
360
|
+
|
|
375
361
|
/**
|
|
376
362
|
* 替换为同类节点
|
|
377
363
|
* @param {Token} token 待替换的节点
|
|
@@ -444,6 +430,120 @@ class Token extends AstElement {
|
|
|
444
430
|
throw new RangeError(`非法的标签名!${tagName}`);
|
|
445
431
|
}
|
|
446
432
|
|
|
433
|
+
/**
|
|
434
|
+
* 创建纯文本节点
|
|
435
|
+
* @param {string} data 文本内容
|
|
436
|
+
*/
|
|
437
|
+
createTextNode(data = '') {
|
|
438
|
+
return typeof data === 'string' ? new AstText(data) : this.typeError('createComment', 'String');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* 找到给定位置所在的节点
|
|
443
|
+
* @param {number} index 位置
|
|
444
|
+
*/
|
|
445
|
+
caretPositionFromIndex(index) {
|
|
446
|
+
if (index === undefined) {
|
|
447
|
+
return undefined;
|
|
448
|
+
} else if (!Number.isInteger(index)) {
|
|
449
|
+
this.typeError('caretPositionFromIndex', 'Number');
|
|
450
|
+
}
|
|
451
|
+
const {length} = String(this);
|
|
452
|
+
if (index > length || index < -length) {
|
|
453
|
+
return undefined;
|
|
454
|
+
} else if (index < 0) {
|
|
455
|
+
index += length;
|
|
456
|
+
}
|
|
457
|
+
let child = this, // eslint-disable-line unicorn/no-this-assignment
|
|
458
|
+
acc = 0,
|
|
459
|
+
start = 0;
|
|
460
|
+
while (child.type !== 'text') {
|
|
461
|
+
const {childNodes} = child;
|
|
462
|
+
acc += child.getPadding();
|
|
463
|
+
for (let i = 0; acc <= index && i < childNodes.length; i++) {
|
|
464
|
+
const cur = childNodes[i],
|
|
465
|
+
{length: l} = String(cur);
|
|
466
|
+
acc += l;
|
|
467
|
+
if (acc >= index) {
|
|
468
|
+
child = cur;
|
|
469
|
+
acc -= l;
|
|
470
|
+
start = acc;
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
acc += child.getGaps(i);
|
|
474
|
+
}
|
|
475
|
+
if (child.childNodes === childNodes) {
|
|
476
|
+
return {offsetNode: child, offset: index - start};
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return {offsetNode: child, offset: index - start};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* 找到给定位置所在的节点
|
|
484
|
+
* @param {number} x 列数
|
|
485
|
+
* @param {number} y 行数
|
|
486
|
+
*/
|
|
487
|
+
caretPositionFromPoint(x, y) {
|
|
488
|
+
return this.caretPositionFromIndex(this.indexFromPos(y, x));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* 找到给定位置所在的最外层节点
|
|
493
|
+
* @param {number} index 位置
|
|
494
|
+
* @throws `Error` 不是根节点
|
|
495
|
+
*/
|
|
496
|
+
elementFromIndex(index) {
|
|
497
|
+
if (index === undefined) {
|
|
498
|
+
return undefined;
|
|
499
|
+
} else if (!Number.isInteger(index)) {
|
|
500
|
+
this.typeError('elementFromIndex', 'Number');
|
|
501
|
+
} else if (this.type !== 'root') {
|
|
502
|
+
throw new Error('elementFromIndex方法只可用于根节点!');
|
|
503
|
+
}
|
|
504
|
+
const {length} = String(this);
|
|
505
|
+
if (index > length || index < -length) {
|
|
506
|
+
return undefined;
|
|
507
|
+
} else if (index < 0) {
|
|
508
|
+
index += length;
|
|
509
|
+
}
|
|
510
|
+
const {childNodes} = this;
|
|
511
|
+
let acc = 0,
|
|
512
|
+
i = 0;
|
|
513
|
+
for (; acc < index && i < childNodes.length; i++) {
|
|
514
|
+
const {length: l} = String(childNodes[i]);
|
|
515
|
+
acc += l;
|
|
516
|
+
}
|
|
517
|
+
return childNodes[i && i - 1];
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* 找到给定位置所在的最外层节点
|
|
522
|
+
* @param {number} x 列数
|
|
523
|
+
* @param {number} y 行数
|
|
524
|
+
*/
|
|
525
|
+
elementFromPoint(x, y) {
|
|
526
|
+
return this.elementFromIndex(this.indexFromPos(y, x));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* 找到给定位置所在的所有节点
|
|
531
|
+
* @param {number} index 位置
|
|
532
|
+
*/
|
|
533
|
+
elementsFromIndex(index) {
|
|
534
|
+
const offsetNode = this.caretPositionFromIndex(index)?.offsetNode;
|
|
535
|
+
return offsetNode && [...offsetNode.getAncestors().reverse(), offsetNode];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* 找到给定位置所在的所有节点
|
|
540
|
+
* @param {number} x 列数
|
|
541
|
+
* @param {number} y 行数
|
|
542
|
+
*/
|
|
543
|
+
elementsFromPoint(x, y) {
|
|
544
|
+
return this.elementsFromIndex(this.indexFromPos(y, x));
|
|
545
|
+
}
|
|
546
|
+
|
|
447
547
|
/**
|
|
448
548
|
* 判断标题是否是跨维基链接
|
|
449
549
|
* @param {string} title 标题
|
|
@@ -453,12 +553,31 @@ class Token extends AstElement {
|
|
|
453
553
|
}
|
|
454
554
|
|
|
455
555
|
/**
|
|
456
|
-
*
|
|
457
|
-
* @
|
|
458
|
-
* @
|
|
556
|
+
* 深拷贝所有子节点
|
|
557
|
+
* @complexity `n`
|
|
558
|
+
* @returns {(AstText|Token)[]}
|
|
459
559
|
*/
|
|
460
|
-
|
|
461
|
-
return
|
|
560
|
+
cloneChildNodes() {
|
|
561
|
+
return this.childNodes.map(child => child.cloneNode());
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* 深拷贝节点
|
|
566
|
+
* @complexity `n`
|
|
567
|
+
* @throws `Error` 未定义复制方法
|
|
568
|
+
*/
|
|
569
|
+
cloneNode() {
|
|
570
|
+
if (this.constructor !== Token) {
|
|
571
|
+
throw new Error(`未定义 ${this.constructor.name} 的复制方法!`);
|
|
572
|
+
}
|
|
573
|
+
const cloned = this.cloneChildNodes();
|
|
574
|
+
return Parser.run(() => {
|
|
575
|
+
const token = new Token(undefined, this.#config, false, [], this.#acceptable);
|
|
576
|
+
token.type = this.type;
|
|
577
|
+
token.append(...cloned);
|
|
578
|
+
token.getAttribute('protectChildren')(...this.#protectedChildren);
|
|
579
|
+
return token;
|
|
580
|
+
});
|
|
462
581
|
}
|
|
463
582
|
|
|
464
583
|
/**
|
|
@@ -499,7 +618,7 @@ class Token extends AstElement {
|
|
|
499
618
|
* @complexity `n`
|
|
500
619
|
*/
|
|
501
620
|
section(n) {
|
|
502
|
-
return
|
|
621
|
+
return Number.isInteger(n) ? this.sections()?.[n] : this.typeError('section', 'Number');
|
|
503
622
|
}
|
|
504
623
|
|
|
505
624
|
/**
|
|
@@ -590,29 +709,6 @@ class Token extends AstElement {
|
|
|
590
709
|
this.normalize();
|
|
591
710
|
}
|
|
592
711
|
|
|
593
|
-
/**
|
|
594
|
-
* 将占位符替换为子Token
|
|
595
|
-
* @complexity `n`
|
|
596
|
-
*/
|
|
597
|
-
build() {
|
|
598
|
-
if (!Parser.debugging && externalUse('build')) {
|
|
599
|
-
this.debugOnly('build');
|
|
600
|
-
}
|
|
601
|
-
this.#stage = MAX_STAGE;
|
|
602
|
-
const {childNodes: {length}, firstChild} = this,
|
|
603
|
-
str = String(firstChild);
|
|
604
|
-
if (length === 1 && firstChild.type === 'text' && str.includes('\0')) {
|
|
605
|
-
this.replaceChildren(...this.#buildFromStr(str));
|
|
606
|
-
this.normalize();
|
|
607
|
-
if (this.type === 'root') {
|
|
608
|
-
for (const token of this.#accum) {
|
|
609
|
-
token.build();
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
return this;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
712
|
/** 生成部分Token的`name`属性 */
|
|
617
713
|
afterBuild() {
|
|
618
714
|
if (!Parser.debugging && externalUse('afterBuild')) {
|
|
@@ -631,16 +727,18 @@ class Token extends AstElement {
|
|
|
631
727
|
* @param {boolean} include 是否嵌入
|
|
632
728
|
*/
|
|
633
729
|
parse(n = MAX_STAGE, include = false) {
|
|
634
|
-
if (
|
|
730
|
+
if (!Number.isInteger(n)) {
|
|
635
731
|
this.typeError('parse', 'Number');
|
|
636
|
-
} else if (n < MAX_STAGE && !Parser.debugging && Parser.warning && externalUse('parse')) {
|
|
637
|
-
Parser.warn('指定解析层级的方法仅供熟练用户使用!');
|
|
638
732
|
}
|
|
639
733
|
this.#include = Boolean(include);
|
|
640
734
|
while (this.#stage < n) {
|
|
641
735
|
this.#parseOnce(this.#stage, include);
|
|
642
736
|
}
|
|
643
|
-
|
|
737
|
+
if (n) {
|
|
738
|
+
this.#build();
|
|
739
|
+
this.afterBuild();
|
|
740
|
+
}
|
|
741
|
+
return this;
|
|
644
742
|
}
|
|
645
743
|
|
|
646
744
|
/**
|