wikiparser-node 0.3.0 → 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 +472 -34
- package/README.md +1 -1
- package/config/default.json +58 -30
- package/config/llwiki.json +22 -90
- package/config/moegirl.json +51 -13
- package/config/zhwiki.json +1269 -0
- package/index.js +114 -104
- 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 +9 -3
- package/parser/brackets.js +22 -17
- package/parser/commentAndExt.js +18 -16
- package/parser/converter.js +14 -13
- package/parser/externalLinks.js +12 -11
- package/parser/hrAndDoubleUnderscore.js +23 -14
- package/parser/html.js +10 -9
- package/parser/links.js +15 -14
- package/parser/list.js +12 -11
- package/parser/magicLinks.js +12 -11
- package/parser/quotes.js +6 -5
- package/parser/selector.js +175 -0
- package/parser/table.js +25 -18
- 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 +182 -100
- package/src/converter.js +68 -41
- package/src/converterFlags.js +67 -45
- package/src/converterRule.js +117 -65
- package/src/extLink.js +66 -18
- package/src/gallery.js +42 -15
- package/src/heading.js +34 -15
- package/src/html.js +97 -35
- package/src/imageParameter.js +83 -54
- package/src/index.js +299 -178
- 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 +146 -60
- package/src/magicLink.js +34 -12
- 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 +26 -20
- package/src/table/index.js +260 -165
- package/src/table/td.js +98 -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 +170 -93
- 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 +40 -27
- package/typings/element.d.ts +0 -28
package/src/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/*
|
|
4
4
|
* PHP解析器的步骤:
|
|
5
5
|
* -1. 替换签名和`{{subst:}}`,参见Parser::preSaveTransform;这在revision中不可能保留,可以跳过
|
|
6
|
-
* 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
|
-
* \
|
|
22
|
+
* \0\d+.\x7f标记Token:
|
|
23
23
|
* e: ExtToken
|
|
24
24
|
* c: CommentToken、NoIncludeToken和IncludeToken
|
|
25
25
|
* !: `{{!}}`专用
|
|
@@ -42,31 +42,132 @@
|
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
44
|
const {externalUse} = require('../util/debug'),
|
|
45
|
+
{text} = require('../util/string'),
|
|
45
46
|
Ranges = require('../lib/ranges'),
|
|
46
|
-
AstElement = require('../lib/element'),
|
|
47
47
|
assert = require('assert/strict'),
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
Parser = require('..'),
|
|
49
|
+
AstElement = require('../lib/element'),
|
|
50
|
+
AstText = require('../lib/text');
|
|
51
|
+
const {MAX_STAGE, aliases} = Parser;
|
|
50
52
|
|
|
53
|
+
/**
|
|
54
|
+
* 所有节点的基类
|
|
55
|
+
* @classdesc `{childNodes: ...(AstText|Token)}`
|
|
56
|
+
*/
|
|
51
57
|
class Token extends AstElement {
|
|
52
58
|
type = 'root';
|
|
53
|
-
|
|
59
|
+
#stage = 0; // 解析阶段,参见顶部注释。只对plain Token有意义。
|
|
54
60
|
#config;
|
|
55
|
-
|
|
61
|
+
// 这个数组起两个作用:1. 数组中的Token会在build时替换`/\0\d+.\x7f/`标记;2. 数组中的Token会依次执行parseOnce和build方法。
|
|
56
62
|
#accum;
|
|
57
63
|
/** @type {Record<string, Ranges>} */ #acceptable;
|
|
58
64
|
#protectedChildren = new Ranges();
|
|
59
65
|
/** @type {boolean} */ #include;
|
|
60
66
|
|
|
61
67
|
/**
|
|
62
|
-
*
|
|
68
|
+
* 保护部分子节点不被移除
|
|
69
|
+
* @param {...string|number|Range} args 子节点范围
|
|
70
|
+
*/
|
|
71
|
+
#protectChildren = (...args) => {
|
|
72
|
+
this.#protectedChildren.push(...new Ranges(args));
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 将维基语法替换为占位符
|
|
77
|
+
* @param {number} n 解析阶段
|
|
78
|
+
* @param {boolean} include 是否嵌入
|
|
79
|
+
*/
|
|
80
|
+
#parseOnce = (n = this.#stage, include = false) => {
|
|
81
|
+
if (n < this.#stage || !this.isPlain() || this.childNodes.length === 0) {
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
switch (n) {
|
|
85
|
+
case 0:
|
|
86
|
+
if (this.type === 'root') {
|
|
87
|
+
this.#accum.shift();
|
|
88
|
+
}
|
|
89
|
+
this.#parseCommentAndExt(include);
|
|
90
|
+
break;
|
|
91
|
+
case 1:
|
|
92
|
+
this.#parseBrackets();
|
|
93
|
+
break;
|
|
94
|
+
case 2:
|
|
95
|
+
this.#parseHtml();
|
|
96
|
+
break;
|
|
97
|
+
case 3:
|
|
98
|
+
this.#parseTable();
|
|
99
|
+
break;
|
|
100
|
+
case 4:
|
|
101
|
+
this.#parseHrAndDoubleUndescore();
|
|
102
|
+
break;
|
|
103
|
+
case 5:
|
|
104
|
+
this.#parseLinks();
|
|
105
|
+
break;
|
|
106
|
+
case 6:
|
|
107
|
+
this.#parseQuotes();
|
|
108
|
+
break;
|
|
109
|
+
|
|
110
|
+
case 7:
|
|
111
|
+
this.#parseExternalLinks();
|
|
112
|
+
break;
|
|
113
|
+
case 8:
|
|
114
|
+
this.#parseMagicLinks();
|
|
115
|
+
break;
|
|
116
|
+
case 9:
|
|
117
|
+
this.#parseList();
|
|
118
|
+
break;
|
|
119
|
+
case 10:
|
|
120
|
+
this.#parseConverter();
|
|
121
|
+
// no default
|
|
122
|
+
}
|
|
123
|
+
if (this.type === 'root') {
|
|
124
|
+
for (const token of this.#accum) {
|
|
125
|
+
token.getAttribute('parseOnce')(n, include);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
this.#stage++;
|
|
129
|
+
return this;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 重建wikitext
|
|
134
|
+
* @param {string} str 半解析的字符串
|
|
135
|
+
* @complexity `n`
|
|
136
|
+
* @returns {(Token|AstText)[]}
|
|
137
|
+
*/
|
|
138
|
+
#buildFromStr = str => str.split(/[\0\x7F]/u).map((s, i) => {
|
|
139
|
+
if (i % 2 === 0) {
|
|
140
|
+
return new AstText(s);
|
|
141
|
+
} else if (isNaN(s.at(-1))) {
|
|
142
|
+
return this.#accum[Number(s.slice(0, -1))];
|
|
143
|
+
}
|
|
144
|
+
throw new Error(`解析错误!未正确标记的 Token:${s}`);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
/** 所有图片,包括图库 */
|
|
148
|
+
get images() {
|
|
149
|
+
return this.querySelectorAll('file, gallery-image');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 所有内链、外链和自由外链 */
|
|
153
|
+
get links() {
|
|
154
|
+
return this.querySelectorAll('link, ext-link, free-ext-link');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 所有模板和模块 */
|
|
158
|
+
get embeds() {
|
|
159
|
+
return this.querySelectorAll('template, magic-word#invoke');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {string} wikitext wikitext
|
|
63
164
|
* @param {accum} accum
|
|
64
|
-
* @param {acceptable} acceptable
|
|
165
|
+
* @param {acceptable} acceptable 可接受的子节点设置
|
|
65
166
|
*/
|
|
66
167
|
constructor(wikitext, config = Parser.getConfig(), halfParsed = false, accum = [], acceptable = null) {
|
|
67
168
|
super();
|
|
68
169
|
if (typeof wikitext === 'string') {
|
|
69
|
-
this.appendChild(halfParsed ? wikitext : wikitext.
|
|
170
|
+
this.appendChild(halfParsed ? wikitext : wikitext.replaceAll('\0', '').replaceAll('\x7F', ''));
|
|
70
171
|
}
|
|
71
172
|
this.#config = config;
|
|
72
173
|
this.#accum = accum;
|
|
@@ -74,32 +175,38 @@ class Token extends AstElement {
|
|
|
74
175
|
accum.push(this);
|
|
75
176
|
}
|
|
76
177
|
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
178
|
+
/**
|
|
179
|
+
* 深拷贝所有子节点
|
|
180
|
+
* @complexity `n`
|
|
181
|
+
* @returns {(AstText|Token)[]}
|
|
182
|
+
*/
|
|
183
|
+
cloneChildNodes() {
|
|
184
|
+
return this.childNodes.map(child => child.cloneNode());
|
|
83
185
|
}
|
|
84
186
|
|
|
85
|
-
/**
|
|
187
|
+
/**
|
|
188
|
+
* 深拷贝节点
|
|
189
|
+
* @complexity `n`
|
|
190
|
+
* @throws `Error` 未定义复制方法
|
|
191
|
+
*/
|
|
86
192
|
cloneNode() {
|
|
87
193
|
if (!this.isPlain()) {
|
|
88
194
|
throw new Error(`未定义 ${this.constructor.name} 的复制方法!`);
|
|
89
195
|
}
|
|
90
|
-
const cloned = this.
|
|
196
|
+
const cloned = this.cloneChildNodes();
|
|
91
197
|
return Parser.run(() => {
|
|
92
198
|
const token = new Token(undefined, this.#config, false, [], this.#acceptable);
|
|
93
199
|
token.type = this.type;
|
|
94
200
|
token.append(...cloned);
|
|
95
|
-
token.protectChildren(...this.#protectedChildren);
|
|
201
|
+
token.getAttribute('protectChildren')(...this.#protectedChildren);
|
|
96
202
|
return token;
|
|
97
203
|
});
|
|
98
204
|
}
|
|
99
205
|
|
|
100
206
|
/**
|
|
207
|
+
* @override
|
|
101
208
|
* @template {string} T
|
|
102
|
-
* @param {T} key
|
|
209
|
+
* @param {T} key 属性键
|
|
103
210
|
* @returns {TokenAttribute<T>}
|
|
104
211
|
*/
|
|
105
212
|
getAttribute(key) {
|
|
@@ -112,18 +219,28 @@ class Token extends AstElement {
|
|
|
112
219
|
return this.#accum;
|
|
113
220
|
case 'acceptable':
|
|
114
221
|
return this.#acceptable ? {...this.#acceptable} : null;
|
|
222
|
+
case 'protectChildren':
|
|
223
|
+
return this.#protectChildren;
|
|
115
224
|
case 'protectedChildren':
|
|
116
225
|
return new Ranges(this.#protectedChildren);
|
|
226
|
+
case 'parseOnce':
|
|
227
|
+
return this.#parseOnce;
|
|
228
|
+
case 'buildFromStr':
|
|
229
|
+
return this.#buildFromStr;
|
|
117
230
|
case 'include': {
|
|
118
231
|
if (this.#include !== undefined) {
|
|
119
232
|
return this.#include;
|
|
120
233
|
}
|
|
121
|
-
const
|
|
234
|
+
const root = this.getRootNode();
|
|
235
|
+
if (root.type === 'root' && root !== this) {
|
|
236
|
+
return root.getAttribute('include');
|
|
237
|
+
}
|
|
238
|
+
const includeToken = root.querySelector('include');
|
|
122
239
|
if (includeToken) {
|
|
123
240
|
return includeToken.name === 'noinclude';
|
|
124
241
|
}
|
|
125
|
-
const noincludeToken =
|
|
126
|
-
return Boolean(noincludeToken) && !/^<\/?noinclude(?:\s[^>]*)?\/?>$/
|
|
242
|
+
const noincludeToken = root.querySelector('noinclude');
|
|
243
|
+
return Boolean(noincludeToken) && !/^<\/?noinclude(?:\s[^>]*)?\/?>$/iu.test(String(noincludeToken));
|
|
127
244
|
}
|
|
128
245
|
default:
|
|
129
246
|
return super.getAttribute(key);
|
|
@@ -131,14 +248,16 @@ class Token extends AstElement {
|
|
|
131
248
|
}
|
|
132
249
|
|
|
133
250
|
/**
|
|
251
|
+
* @override
|
|
134
252
|
* @template {string} T
|
|
135
|
-
* @param {T} key
|
|
136
|
-
* @param {TokenAttribute<T>} value
|
|
253
|
+
* @param {T} key 属性键
|
|
254
|
+
* @param {TokenAttribute<T>} value 属性值
|
|
255
|
+
* @throws `RangeError` 禁止手动指定私有属性
|
|
137
256
|
*/
|
|
138
257
|
setAttribute(key, value) {
|
|
139
|
-
if (key === 'include' || !Parser.running &&
|
|
258
|
+
if (key === 'include' || !Parser.running && (key === 'config' || key === 'accum')) {
|
|
140
259
|
throw new RangeError(`禁止手动指定私有的 #${key} 属性!`);
|
|
141
|
-
} else if (!Parser.debugging &&
|
|
260
|
+
} else if (!Parser.debugging && (key === 'stage' || key === 'acceptable' || key === 'protectedChildren')
|
|
142
261
|
&& externalUse('setAttribute')
|
|
143
262
|
) {
|
|
144
263
|
throw new RangeError(`使用 ${this.constructor.name}.setAttribute 方法设置私有属性 #${key} 仅用于代码调试!`);
|
|
@@ -165,11 +284,11 @@ class Token extends AstElement {
|
|
|
165
284
|
for (const [k, v] of Object.entries(value)) {
|
|
166
285
|
if (k.startsWith('Stage-')) {
|
|
167
286
|
for (let i = 0; i <= Number(k.slice(6)); i++) {
|
|
168
|
-
for (const type of
|
|
287
|
+
for (const type of aliases[i]) {
|
|
169
288
|
acceptable[type] = new Ranges(v);
|
|
170
289
|
}
|
|
171
290
|
}
|
|
172
|
-
} else if (k
|
|
291
|
+
} else if (k[0] === '!') { // `!`项必须放在最后
|
|
173
292
|
delete acceptable[k.slice(1)];
|
|
174
293
|
} else {
|
|
175
294
|
acceptable[k] = new Ranges(v);
|
|
@@ -184,22 +303,17 @@ class Token extends AstElement {
|
|
|
184
303
|
}
|
|
185
304
|
}
|
|
186
305
|
|
|
306
|
+
/** 是否是普通节点 */
|
|
187
307
|
isPlain() {
|
|
188
308
|
return this.constructor === Token;
|
|
189
309
|
}
|
|
190
310
|
|
|
191
|
-
/** @param {...string|number|Range} args */
|
|
192
|
-
protectChildren(...args) {
|
|
193
|
-
if (!Parser.debugging && externalUse('protectChildren')) {
|
|
194
|
-
this.debugOnly('protectChildren');
|
|
195
|
-
}
|
|
196
|
-
this.#protectedChildren.push(...new Ranges(args));
|
|
197
|
-
}
|
|
198
|
-
|
|
199
311
|
/**
|
|
200
|
-
* @
|
|
201
|
-
* @
|
|
312
|
+
* @override
|
|
313
|
+
* @param {number} i 移除位置
|
|
314
|
+
* @returns {Token}
|
|
202
315
|
* @complexity `n`
|
|
316
|
+
* @throws `Error` 不可移除的子节点
|
|
203
317
|
*/
|
|
204
318
|
removeAt(i) {
|
|
205
319
|
if (typeof i !== 'number') {
|
|
@@ -225,18 +339,25 @@ class Token extends AstElement {
|
|
|
225
339
|
}
|
|
226
340
|
|
|
227
341
|
/**
|
|
342
|
+
* @override
|
|
228
343
|
* @template {string|Token} T
|
|
229
|
-
* @param {T} token
|
|
344
|
+
* @param {T} token 待插入的子节点
|
|
345
|
+
* @param {number} i 插入位置
|
|
230
346
|
* @complexity `n`
|
|
347
|
+
* @returns {T extends Token ? Token : AstText}
|
|
348
|
+
* @throws `RangeError` 不可插入的子节点
|
|
231
349
|
*/
|
|
232
350
|
insertAt(token, i = this.childNodes.length) {
|
|
351
|
+
if (typeof token === 'string') {
|
|
352
|
+
token = new AstText(token);
|
|
353
|
+
}
|
|
233
354
|
if (!Parser.running && this.#acceptable) {
|
|
234
355
|
const acceptableIndices = Object.fromEntries(
|
|
235
356
|
Object.entries(this.#acceptable)
|
|
236
357
|
.map(([str, ranges]) => [str, ranges.applyTo(this.childNodes.length + 1)]),
|
|
237
358
|
),
|
|
238
359
|
nodesAfter = this.childNodes.slice(i),
|
|
239
|
-
insertedName = token
|
|
360
|
+
{constructor: {name: insertedName}} = token,
|
|
240
361
|
k = i < 0 ? i + this.childNodes.length : i;
|
|
241
362
|
if (!acceptableIndices[insertedName].includes(k)) {
|
|
242
363
|
throw new RangeError(`${this.constructor.name} 的第 ${k} 个子节点不能为 ${insertedName}!`);
|
|
@@ -245,15 +366,18 @@ class Token extends AstElement {
|
|
|
245
366
|
}
|
|
246
367
|
}
|
|
247
368
|
super.insertAt(token, i);
|
|
248
|
-
if (token
|
|
369
|
+
if (token.type === 'root') {
|
|
249
370
|
token.type = 'plain';
|
|
250
371
|
}
|
|
251
372
|
return token;
|
|
252
373
|
}
|
|
253
374
|
|
|
254
375
|
/**
|
|
255
|
-
*
|
|
376
|
+
* 替换为同类节点
|
|
377
|
+
* @param {Token} token 待替换的节点
|
|
256
378
|
* @complexity `n`
|
|
379
|
+
* @throws `Error` 不存在父节点
|
|
380
|
+
* @throws `Error` 待替换的节点具有不同属性
|
|
257
381
|
*/
|
|
258
382
|
safeReplaceWith(token) {
|
|
259
383
|
const {parentNode} = this;
|
|
@@ -280,28 +404,78 @@ class Token extends AstElement {
|
|
|
280
404
|
token.dispatchEvent(e, {position: i, oldToken: this, newToken: token});
|
|
281
405
|
}
|
|
282
406
|
|
|
283
|
-
/**
|
|
407
|
+
/**
|
|
408
|
+
* 创建HTML注释
|
|
409
|
+
* @param {string} data 注释内容
|
|
410
|
+
*/
|
|
411
|
+
createComment(data = '') {
|
|
412
|
+
if (typeof data === 'string') {
|
|
413
|
+
const CommentToken = require('./nowiki/comment');
|
|
414
|
+
const config = this.getAttribute('config');
|
|
415
|
+
return Parser.run(() => new CommentToken(data.replaceAll('-->', '-->'), true, config));
|
|
416
|
+
}
|
|
417
|
+
return this.typeError('createComment', 'String');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* 创建标签
|
|
422
|
+
* @param {string} tagName 标签名
|
|
423
|
+
* @param {{selfClosing: boolean, closing: boolean}} options 选项
|
|
424
|
+
* @throws `RangeError` 非法的标签名
|
|
425
|
+
*/
|
|
426
|
+
createElement(tagName, {selfClosing, closing} = {}) {
|
|
427
|
+
if (typeof tagName !== 'string') {
|
|
428
|
+
this.typeError('createElement', 'String');
|
|
429
|
+
}
|
|
430
|
+
const config = this.getAttribute('config'),
|
|
431
|
+
include = this.getAttribute('include');
|
|
432
|
+
if (tagName === (include ? 'noinclude' : 'includeonly')) {
|
|
433
|
+
const IncludeToken = require('./tagPair/include');
|
|
434
|
+
return Parser.run(
|
|
435
|
+
() => new IncludeToken(tagName, '', undefined, selfClosing ? undefined : tagName, config),
|
|
436
|
+
);
|
|
437
|
+
} else if (config.ext.includes(tagName)) {
|
|
438
|
+
const ExtToken = require('./tagPair/ext');
|
|
439
|
+
return Parser.run(() => new ExtToken(tagName, '', '', selfClosing ? undefined : '', config));
|
|
440
|
+
} else if (config.html.flat().includes(tagName)) {
|
|
441
|
+
const HtmlToken = require('./html');
|
|
442
|
+
return Parser.run(() => new HtmlToken(tagName, '', closing, selfClosing, config));
|
|
443
|
+
}
|
|
444
|
+
throw new RangeError(`非法的标签名!${tagName}`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* 判断标题是否是跨维基链接
|
|
449
|
+
* @param {string} title 标题
|
|
450
|
+
*/
|
|
284
451
|
isInterwiki(title) {
|
|
285
452
|
return Parser.isInterwiki(title, this.#config);
|
|
286
453
|
}
|
|
287
454
|
|
|
288
|
-
/**
|
|
455
|
+
/**
|
|
456
|
+
* 规范化页面标题
|
|
457
|
+
* @param {string} title 标题(含或不含命名空间前缀)
|
|
458
|
+
* @param {number} defaultNs 命名空间
|
|
459
|
+
*/
|
|
289
460
|
normalizeTitle(title, defaultNs = 0, halfParsed = false) {
|
|
290
461
|
return Parser.normalizeTitle(title, defaultNs, this.#include, this.#config, halfParsed);
|
|
291
462
|
}
|
|
292
463
|
|
|
293
|
-
/**
|
|
464
|
+
/**
|
|
465
|
+
* 获取全部章节
|
|
466
|
+
* @complexity `n`
|
|
467
|
+
*/
|
|
294
468
|
sections() {
|
|
295
469
|
if (this.type !== 'root') {
|
|
296
|
-
return;
|
|
470
|
+
return undefined;
|
|
297
471
|
}
|
|
298
472
|
const {childNodes} = this,
|
|
299
|
-
headings = [...childNodes.entries()]
|
|
300
|
-
.
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
473
|
+
headings = [...childNodes.entries()].filter(([, {type}]) => type === 'heading')
|
|
474
|
+
.map(([i, {name}]) => [i, Number(name)]),
|
|
475
|
+
lastHeading = [-1, -1, -1, -1, -1, -1],
|
|
476
|
+
/** @type {(AstText|Token)[][]} */ sections = new Array(headings.length);
|
|
477
|
+
for (let i = 0; i < headings.length; i++) {
|
|
478
|
+
const [index, level] = headings[i];
|
|
305
479
|
for (let j = level; j < 6; j++) {
|
|
306
480
|
const last = lastHeading[j];
|
|
307
481
|
if (last >= 0) {
|
|
@@ -320,20 +494,20 @@ class Token extends AstElement {
|
|
|
320
494
|
}
|
|
321
495
|
|
|
322
496
|
/**
|
|
323
|
-
*
|
|
497
|
+
* 获取指定章节
|
|
498
|
+
* @param {number} n 章节序号
|
|
324
499
|
* @complexity `n`
|
|
325
500
|
*/
|
|
326
501
|
section(n) {
|
|
327
|
-
|
|
328
|
-
this.typeError('section', 'Number');
|
|
329
|
-
}
|
|
330
|
-
return this.sections()[n];
|
|
502
|
+
return typeof n === 'number' ? this.sections()?.[n] : this.typeError('section', 'Number');
|
|
331
503
|
}
|
|
332
504
|
|
|
333
505
|
/**
|
|
334
|
-
*
|
|
506
|
+
* 获取指定的外层HTML标签
|
|
507
|
+
* @param {string|undefined} tag HTML标签名
|
|
335
508
|
* @returns {[Token, Token]}
|
|
336
509
|
* @complexity `n`
|
|
510
|
+
* @throws `RangeError` 非法的标签或空标签
|
|
337
511
|
*/
|
|
338
512
|
findEnclosingHtml(tag) {
|
|
339
513
|
if (tag !== undefined && typeof tag !== 'string') {
|
|
@@ -343,11 +517,11 @@ class Token extends AstElement {
|
|
|
343
517
|
if (tag !== undefined && !this.#config.html.slice(0, 2).flat().includes(tag)) {
|
|
344
518
|
throw new RangeError(`非法的标签或空标签:${tag}`);
|
|
345
519
|
}
|
|
346
|
-
const {
|
|
347
|
-
if (!
|
|
348
|
-
return;
|
|
520
|
+
const {parentNode} = this;
|
|
521
|
+
if (!parentNode) {
|
|
522
|
+
return undefined;
|
|
349
523
|
}
|
|
350
|
-
const {children} =
|
|
524
|
+
const {children} = parentNode,
|
|
351
525
|
index = children.indexOf(this);
|
|
352
526
|
let i;
|
|
353
527
|
for (i = index - 1; i >= 0; i--) {
|
|
@@ -356,7 +530,7 @@ class Token extends AstElement {
|
|
|
356
530
|
}
|
|
357
531
|
}
|
|
358
532
|
if (i === -1) {
|
|
359
|
-
return
|
|
533
|
+
return parentNode.findEnclosingHtml(tag);
|
|
360
534
|
}
|
|
361
535
|
const opening = children[i],
|
|
362
536
|
{name} = opening;
|
|
@@ -366,15 +540,22 @@ class Token extends AstElement {
|
|
|
366
540
|
}
|
|
367
541
|
}
|
|
368
542
|
return i === children.length
|
|
369
|
-
?
|
|
543
|
+
? parentNode.findEnclosingHtml(tag)
|
|
370
544
|
: [opening, children[i]];
|
|
371
545
|
}
|
|
372
546
|
|
|
373
|
-
/**
|
|
547
|
+
/**
|
|
548
|
+
* 获取全部分类
|
|
549
|
+
* @complexity `n`
|
|
550
|
+
*/
|
|
374
551
|
getCategories() {
|
|
375
552
|
return this.querySelectorAll('category').map(({name, sortkey}) => [name, sortkey]);
|
|
376
553
|
}
|
|
377
554
|
|
|
555
|
+
/**
|
|
556
|
+
* 重新解析单引号
|
|
557
|
+
* @throws `Error` 不接受QuoteToken作为子节点
|
|
558
|
+
*/
|
|
378
559
|
redoQuotes() {
|
|
379
560
|
const acceptable = this.getAttribute('acceptable');
|
|
380
561
|
if (acceptable && !acceptable.QuoteToken?.some(
|
|
@@ -383,24 +564,24 @@ class Token extends AstElement {
|
|
|
383
564
|
throw new Error(`${this.constructor.name} 不接受 QuoteToken 作为子节点!`);
|
|
384
565
|
}
|
|
385
566
|
for (const quote of this.childNodes) {
|
|
386
|
-
if (quote
|
|
387
|
-
quote.replaceWith(quote
|
|
567
|
+
if (quote.type === 'quote') {
|
|
568
|
+
quote.replaceWith(String(quote));
|
|
388
569
|
}
|
|
389
570
|
}
|
|
390
571
|
this.normalize();
|
|
391
|
-
/** @type {[number,
|
|
392
|
-
const textNodes = [...this.childNodes.entries()].filter(([,
|
|
572
|
+
/** @type {[number, AstText][]} */
|
|
573
|
+
const textNodes = [...this.childNodes.entries()].filter(([, {type}]) => type === 'text'),
|
|
393
574
|
indices = textNodes.map(([i]) => this.getRelativeIndex(i)),
|
|
394
575
|
token = Parser.run(() => {
|
|
395
|
-
const root = new Token(textNodes.map(([, str]) => str)
|
|
576
|
+
const root = new Token(text(textNodes.map(([, str]) => str)), this.getAttribute('config'));
|
|
396
577
|
return root.setAttribute('stage', 6).parse(7);
|
|
397
578
|
});
|
|
398
579
|
for (const quote of token.children.reverse()) {
|
|
399
580
|
if (quote.type === 'quote') {
|
|
400
581
|
const index = quote.getRelativeIndex(),
|
|
401
582
|
n = indices.findLastIndex(textIndex => textIndex <= index);
|
|
402
|
-
this.splitText(
|
|
403
|
-
this.
|
|
583
|
+
this.childNodes[n].splitText(index - indices[n]);
|
|
584
|
+
this.childNodes[n + 1].splitText(Number(quote.name));
|
|
404
585
|
this.removeAt(n + 1);
|
|
405
586
|
this.insertAt(quote, n + 1);
|
|
406
587
|
}
|
|
@@ -408,82 +589,6 @@ class Token extends AstElement {
|
|
|
408
589
|
this.normalize();
|
|
409
590
|
}
|
|
410
591
|
|
|
411
|
-
/**
|
|
412
|
-
* 将维基语法替换为占位符
|
|
413
|
-
* @this {Token & {firstChild: string}}
|
|
414
|
-
*/
|
|
415
|
-
parseOnce(n = this.#stage, include = false) {
|
|
416
|
-
if (!Parser.debugging && externalUse('parseOnce')) {
|
|
417
|
-
this.debugOnly('parseOnce');
|
|
418
|
-
} else if (n < this.#stage || !this.isPlain() || this.childNodes.length === 0) {
|
|
419
|
-
return this;
|
|
420
|
-
}
|
|
421
|
-
switch (n) {
|
|
422
|
-
case 0:
|
|
423
|
-
if (this.type === 'root') {
|
|
424
|
-
this.#accum.shift();
|
|
425
|
-
}
|
|
426
|
-
this.#parseCommentAndExt(include);
|
|
427
|
-
break;
|
|
428
|
-
case 1:
|
|
429
|
-
this.#parseBrackets();
|
|
430
|
-
break;
|
|
431
|
-
case 2:
|
|
432
|
-
this.#parseHtml();
|
|
433
|
-
break;
|
|
434
|
-
case 3:
|
|
435
|
-
this.#parseTable();
|
|
436
|
-
break;
|
|
437
|
-
case 4:
|
|
438
|
-
this.#parseHrAndDoubleUndescore();
|
|
439
|
-
break;
|
|
440
|
-
case 5:
|
|
441
|
-
this.#parseLinks();
|
|
442
|
-
break;
|
|
443
|
-
case 6: {
|
|
444
|
-
this.#parseQuotes();
|
|
445
|
-
break;
|
|
446
|
-
}
|
|
447
|
-
case 7:
|
|
448
|
-
this.#parseExternalLinks();
|
|
449
|
-
break;
|
|
450
|
-
case 8:
|
|
451
|
-
this.#parseMagicLinks();
|
|
452
|
-
break;
|
|
453
|
-
case 9:
|
|
454
|
-
this.#parseList();
|
|
455
|
-
break;
|
|
456
|
-
case 10:
|
|
457
|
-
this.#parseConverter();
|
|
458
|
-
// no default
|
|
459
|
-
}
|
|
460
|
-
if (this.type === 'root') {
|
|
461
|
-
for (const token of this.#accum) {
|
|
462
|
-
token.parseOnce(n, include);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
this.#stage++;
|
|
466
|
-
return this;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* @param {string} str
|
|
471
|
-
* @complexity `n`
|
|
472
|
-
*/
|
|
473
|
-
buildFromStr(str) {
|
|
474
|
-
if (!Parser.debugging && externalUse('buildFromStr')) {
|
|
475
|
-
this.debugOnly('buildFromStr');
|
|
476
|
-
}
|
|
477
|
-
return str.split(/[\x00\x7f]/).map((s, i) => {
|
|
478
|
-
if (i % 2 === 0) {
|
|
479
|
-
return s;
|
|
480
|
-
} else if (!isNaN(s.at(-1))) {
|
|
481
|
-
throw new Error(`解析错误!未正确标记的 Token:${s}`);
|
|
482
|
-
}
|
|
483
|
-
return this.#accum[Number(s.slice(0, -1))];
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
|
|
487
592
|
/**
|
|
488
593
|
* 将占位符替换为子Token
|
|
489
594
|
* @complexity `n`
|
|
@@ -493,15 +598,15 @@ class Token extends AstElement {
|
|
|
493
598
|
this.debugOnly('build');
|
|
494
599
|
}
|
|
495
600
|
this.#stage = MAX_STAGE;
|
|
496
|
-
const {childNodes: {length}, firstChild} = this
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
601
|
+
const {childNodes: {length}, firstChild} = this,
|
|
602
|
+
str = String(firstChild);
|
|
603
|
+
if (length === 1 && firstChild.type === 'text' && str.includes('\0')) {
|
|
604
|
+
this.replaceChildren(...this.#buildFromStr(str));
|
|
605
|
+
this.normalize();
|
|
606
|
+
if (this.type === 'root') {
|
|
607
|
+
for (const token of this.#accum) {
|
|
608
|
+
token.build();
|
|
609
|
+
}
|
|
505
610
|
}
|
|
506
611
|
}
|
|
507
612
|
return this;
|
|
@@ -519,35 +624,46 @@ class Token extends AstElement {
|
|
|
519
624
|
return this;
|
|
520
625
|
}
|
|
521
626
|
|
|
522
|
-
/**
|
|
627
|
+
/**
|
|
628
|
+
* 解析、重构、生成部分Token的`name`属性
|
|
629
|
+
* @param {number} n 最大解析层级
|
|
630
|
+
* @param {boolean} include 是否嵌入
|
|
631
|
+
*/
|
|
523
632
|
parse(n = MAX_STAGE, include = false) {
|
|
524
633
|
if (typeof n !== 'number') {
|
|
525
634
|
this.typeError('parse', 'Number');
|
|
526
|
-
} else if (n < MAX_STAGE && !Parser.debugging && externalUse('parse')) {
|
|
635
|
+
} else if (n < MAX_STAGE && !Parser.debugging && Parser.warning && externalUse('parse')) {
|
|
527
636
|
Parser.warn('指定解析层级的方法仅供熟练用户使用!');
|
|
528
637
|
}
|
|
529
638
|
this.#include = Boolean(include);
|
|
530
639
|
while (this.#stage < n) {
|
|
531
|
-
this
|
|
640
|
+
this.#parseOnce(this.#stage, include);
|
|
532
641
|
}
|
|
533
642
|
return n ? this.build().afterBuild() : this;
|
|
534
643
|
}
|
|
535
644
|
|
|
536
|
-
|
|
645
|
+
/**
|
|
646
|
+
* 解析HTML注释和扩展标签
|
|
647
|
+
* @param {boolean} includeOnly 是否嵌入
|
|
648
|
+
*/
|
|
649
|
+
#parseCommentAndExt(includeOnly) {
|
|
537
650
|
const parseCommentAndExt = require('../parser/commentAndExt');
|
|
538
|
-
this.setText(parseCommentAndExt(this
|
|
651
|
+
this.setText(parseCommentAndExt(String(this), this.#config, this.#accum, includeOnly));
|
|
539
652
|
}
|
|
540
653
|
|
|
654
|
+
/** 解析花括号 */
|
|
541
655
|
#parseBrackets() {
|
|
542
656
|
const parseBrackets = require('../parser/brackets');
|
|
543
|
-
this.setText(parseBrackets(this
|
|
657
|
+
this.setText(parseBrackets(String(this), this.#config, this.#accum));
|
|
544
658
|
}
|
|
545
659
|
|
|
660
|
+
/** 解析HTML标签 */
|
|
546
661
|
#parseHtml() {
|
|
547
662
|
const parseHtml = require('../parser/html');
|
|
548
|
-
this.setText(parseHtml(this
|
|
663
|
+
this.setText(parseHtml(String(this), this.#config, this.#accum));
|
|
549
664
|
}
|
|
550
665
|
|
|
666
|
+
/** 解析表格 */
|
|
551
667
|
#parseTable() {
|
|
552
668
|
const parseTable = require('../parser/table'),
|
|
553
669
|
TableToken = require('./table');
|
|
@@ -555,8 +671,8 @@ class Token extends AstElement {
|
|
|
555
671
|
for (const table of this.#accum) {
|
|
556
672
|
if (table instanceof TableToken && table.type !== 'td') {
|
|
557
673
|
table.normalize();
|
|
558
|
-
const [, child] = table
|
|
559
|
-
if (typeof child === 'string' && child.includes('\
|
|
674
|
+
const {childNodes: [, child]} = table;
|
|
675
|
+
if (typeof child === 'string' && child.includes('\0')) {
|
|
560
676
|
table.removeAt(1);
|
|
561
677
|
const inner = new Token(child, this.#config, true, this.#accum);
|
|
562
678
|
table.insertAt(inner, 1);
|
|
@@ -566,49 +682,54 @@ class Token extends AstElement {
|
|
|
566
682
|
}
|
|
567
683
|
}
|
|
568
684
|
|
|
685
|
+
/** 解析\<hr\>和状态开关 */
|
|
569
686
|
#parseHrAndDoubleUndescore() {
|
|
570
687
|
const parseHrAndDoubleUnderscore = require('../parser/hrAndDoubleUnderscore');
|
|
571
|
-
this.setText(parseHrAndDoubleUnderscore(this
|
|
688
|
+
this.setText(parseHrAndDoubleUnderscore(this, this.#config, this.#accum));
|
|
572
689
|
}
|
|
573
690
|
|
|
691
|
+
/** 解析内部链接 */
|
|
574
692
|
#parseLinks() {
|
|
575
693
|
const parseLinks = require('../parser/links');
|
|
576
|
-
this.setText(parseLinks(this
|
|
694
|
+
this.setText(parseLinks(String(this), this.#config, this.#accum));
|
|
577
695
|
}
|
|
578
696
|
|
|
579
|
-
/**
|
|
697
|
+
/** 解析单引号 */
|
|
580
698
|
#parseQuotes() {
|
|
581
|
-
const parseQuotes = require('../parser/quotes')
|
|
582
|
-
|
|
699
|
+
const parseQuotes = require('../parser/quotes');
|
|
700
|
+
const lines = String(this).split('\n');
|
|
583
701
|
for (let i = 0; i < lines.length; i++) {
|
|
584
702
|
lines[i] = parseQuotes(lines[i], this.#config, this.#accum);
|
|
585
703
|
}
|
|
586
704
|
this.setText(lines.join('\n'));
|
|
587
705
|
}
|
|
588
706
|
|
|
707
|
+
/** 解析外部链接 */
|
|
589
708
|
#parseExternalLinks() {
|
|
590
709
|
const parseExternalLinks = require('../parser/externalLinks');
|
|
591
|
-
this.setText(parseExternalLinks(this
|
|
710
|
+
this.setText(parseExternalLinks(String(this), this.#config, this.#accum));
|
|
592
711
|
}
|
|
593
712
|
|
|
713
|
+
/** 解析自由外链 */
|
|
594
714
|
#parseMagicLinks() {
|
|
595
715
|
const parseMagicLinks = require('../parser/magicLinks');
|
|
596
|
-
this.setText(parseMagicLinks(this
|
|
716
|
+
this.setText(parseMagicLinks(String(this), this.#config, this.#accum));
|
|
597
717
|
}
|
|
598
718
|
|
|
599
|
-
/**
|
|
719
|
+
/** 解析列表 */
|
|
600
720
|
#parseList() {
|
|
601
|
-
const parseList = require('../parser/list')
|
|
602
|
-
|
|
721
|
+
const parseList = require('../parser/list');
|
|
722
|
+
const lines = String(this).split('\n');
|
|
603
723
|
for (let i = this.type === 'root' ? 0 : 1; i < lines.length; i++) {
|
|
604
724
|
lines[i] = parseList(lines[i], this.#config, this.#accum);
|
|
605
725
|
}
|
|
606
726
|
this.setText(lines.join('\n'));
|
|
607
727
|
}
|
|
608
728
|
|
|
729
|
+
/** 解析语言变体转换 */
|
|
609
730
|
#parseConverter() {
|
|
610
731
|
const parseConverter = require('../parser/converter');
|
|
611
|
-
this.setText(parseConverter(this
|
|
732
|
+
this.setText(parseConverter(String(this), this.#config, this.#accum));
|
|
612
733
|
}
|
|
613
734
|
}
|
|
614
735
|
|