wikiparser-node 0.3.1 → 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/README.md +1 -1
- package/config/default.json +13 -17
- package/config/llwiki.json +11 -79
- package/config/moegirl.json +7 -1
- package/config/zhwiki.json +1269 -0
- package/index.js +130 -97
- package/lib/element.js +410 -518
- package/lib/node.js +493 -115
- package/lib/ranges.js +27 -19
- package/lib/text.js +175 -0
- package/lib/title.js +14 -6
- package/mixin/attributeParent.js +70 -24
- package/mixin/fixedToken.js +18 -10
- package/mixin/hidden.js +6 -4
- package/mixin/sol.js +39 -12
- package/package.json +17 -4
- package/parser/brackets.js +18 -18
- package/parser/commentAndExt.js +16 -14
- package/parser/converter.js +14 -13
- package/parser/externalLinks.js +12 -11
- package/parser/hrAndDoubleUnderscore.js +24 -14
- package/parser/html.js +8 -7
- package/parser/links.js +13 -13
- package/parser/list.js +12 -11
- package/parser/magicLinks.js +11 -10
- package/parser/quotes.js +6 -5
- package/parser/selector.js +175 -0
- package/parser/table.js +31 -24
- package/src/arg.js +91 -43
- package/src/atom/hidden.js +5 -2
- package/src/atom/index.js +17 -9
- package/src/attribute.js +210 -101
- package/src/converter.js +78 -43
- package/src/converterFlags.js +104 -45
- package/src/converterRule.js +136 -78
- package/src/extLink.js +81 -27
- package/src/gallery.js +63 -20
- package/src/heading.js +58 -20
- package/src/html.js +138 -48
- package/src/imageParameter.js +93 -58
- package/src/index.js +314 -186
- package/src/link/category.js +22 -54
- package/src/link/file.js +83 -32
- package/src/link/galleryImage.js +21 -7
- package/src/link/index.js +170 -81
- package/src/magicLink.js +64 -14
- package/src/nowiki/comment.js +36 -10
- package/src/nowiki/dd.js +37 -22
- package/src/nowiki/doubleUnderscore.js +21 -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 +38 -7
- package/src/onlyinclude.js +24 -7
- package/src/parameter.js +102 -62
- package/src/syntax.js +23 -20
- package/src/table/index.js +282 -174
- package/src/table/td.js +112 -61
- package/src/table/tr.js +135 -74
- package/src/tagPair/ext.js +30 -23
- package/src/tagPair/include.js +26 -11
- package/src/tagPair/index.js +72 -29
- package/src/transclude.js +235 -127
- package/tool/index.js +42 -32
- package/util/debug.js +21 -18
- package/util/diff.js +76 -0
- package/util/lint.js +40 -0
- package/util/string.js +56 -26
- package/.eslintrc.json +0 -319
- package/errors/README +0 -1
- package/jsconfig.json +0 -7
- package/printed/README +0 -1
- package/typings/element.d.ts +0 -28
- package/typings/index.d.ts +0 -52
- package/typings/node.d.ts +0 -23
- package/typings/parser.d.ts +0 -9
- package/typings/table.d.ts +0 -14
- package/typings/token.d.ts +0 -22
- package/typings/tool.d.ts +0 -10
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
|
* !: `{{!}}`专用
|
|
@@ -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\x7F]/gu, ''));
|
|
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,38 +517,46 @@ 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 {
|
|
351
|
-
index =
|
|
524
|
+
const {childNodes} = parentNode,
|
|
525
|
+
index = childNodes.indexOf(this);
|
|
352
526
|
let i;
|
|
353
527
|
for (i = index - 1; i >= 0; i--) {
|
|
354
|
-
|
|
528
|
+
const {type, name, selfClosing, closing} = childNodes[i];
|
|
529
|
+
if (type === 'html' && (!tag || name === tag) && selfClosing === false && closing === false) {
|
|
355
530
|
break;
|
|
356
531
|
}
|
|
357
532
|
}
|
|
358
533
|
if (i === -1) {
|
|
359
|
-
return
|
|
534
|
+
return parentNode.findEnclosingHtml(tag);
|
|
360
535
|
}
|
|
361
|
-
const opening =
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
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) {
|
|
365
540
|
break;
|
|
366
541
|
}
|
|
367
542
|
}
|
|
368
|
-
return i ===
|
|
369
|
-
?
|
|
370
|
-
: [opening,
|
|
543
|
+
return i === childNodes.length
|
|
544
|
+
? parentNode.findEnclosingHtml(tag)
|
|
545
|
+
: [opening, childNodes[i]];
|
|
371
546
|
}
|
|
372
547
|
|
|
373
|
-
/**
|
|
548
|
+
/**
|
|
549
|
+
* 获取全部分类
|
|
550
|
+
* @complexity `n`
|
|
551
|
+
*/
|
|
374
552
|
getCategories() {
|
|
375
553
|
return this.querySelectorAll('category').map(({name, sortkey}) => [name, sortkey]);
|
|
376
554
|
}
|
|
377
555
|
|
|
556
|
+
/**
|
|
557
|
+
* 重新解析单引号
|
|
558
|
+
* @throws `Error` 不接受QuoteToken作为子节点
|
|
559
|
+
*/
|
|
378
560
|
redoQuotes() {
|
|
379
561
|
const acceptable = this.getAttribute('acceptable');
|
|
380
562
|
if (acceptable && !acceptable.QuoteToken?.some(
|
|
@@ -383,24 +565,24 @@ class Token extends AstElement {
|
|
|
383
565
|
throw new Error(`${this.constructor.name} 不接受 QuoteToken 作为子节点!`);
|
|
384
566
|
}
|
|
385
567
|
for (const quote of this.childNodes) {
|
|
386
|
-
if (quote
|
|
387
|
-
quote.replaceWith(quote
|
|
568
|
+
if (quote.type === 'quote') {
|
|
569
|
+
quote.replaceWith(String(quote));
|
|
388
570
|
}
|
|
389
571
|
}
|
|
390
572
|
this.normalize();
|
|
391
|
-
/** @type {[number,
|
|
392
|
-
const textNodes = [...this.childNodes.entries()].filter(([,
|
|
573
|
+
/** @type {[number, AstText][]} */
|
|
574
|
+
const textNodes = [...this.childNodes.entries()].filter(([, {type}]) => type === 'text'),
|
|
393
575
|
indices = textNodes.map(([i]) => this.getRelativeIndex(i)),
|
|
394
576
|
token = Parser.run(() => {
|
|
395
|
-
const root = new Token(textNodes.map(([, str]) => str)
|
|
577
|
+
const root = new Token(text(textNodes.map(([, str]) => str)), this.getAttribute('config'));
|
|
396
578
|
return root.setAttribute('stage', 6).parse(7);
|
|
397
579
|
});
|
|
398
580
|
for (const quote of token.children.reverse()) {
|
|
399
581
|
if (quote.type === 'quote') {
|
|
400
582
|
const index = quote.getRelativeIndex(),
|
|
401
583
|
n = indices.findLastIndex(textIndex => textIndex <= index);
|
|
402
|
-
this.splitText(
|
|
403
|
-
this.
|
|
584
|
+
this.childNodes[n].splitText(index - indices[n]);
|
|
585
|
+
this.childNodes[n + 1].splitText(Number(quote.name));
|
|
404
586
|
this.removeAt(n + 1);
|
|
405
587
|
this.insertAt(quote, n + 1);
|
|
406
588
|
}
|
|
@@ -408,82 +590,6 @@ class Token extends AstElement {
|
|
|
408
590
|
this.normalize();
|
|
409
591
|
}
|
|
410
592
|
|
|
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(/[\0\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
593
|
/**
|
|
488
594
|
* 将占位符替换为子Token
|
|
489
595
|
* @complexity `n`
|
|
@@ -493,15 +599,15 @@ class Token extends AstElement {
|
|
|
493
599
|
this.debugOnly('build');
|
|
494
600
|
}
|
|
495
601
|
this.#stage = MAX_STAGE;
|
|
496
|
-
const {childNodes: {length}, firstChild} = this
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
+
}
|
|
505
611
|
}
|
|
506
612
|
}
|
|
507
613
|
return this;
|
|
@@ -519,35 +625,48 @@ class Token extends AstElement {
|
|
|
519
625
|
return this;
|
|
520
626
|
}
|
|
521
627
|
|
|
522
|
-
/**
|
|
628
|
+
/**
|
|
629
|
+
* 解析、重构、生成部分Token的`name`属性
|
|
630
|
+
* @param {number} n 最大解析层级
|
|
631
|
+
* @param {boolean} include 是否嵌入
|
|
632
|
+
*/
|
|
523
633
|
parse(n = MAX_STAGE, include = false) {
|
|
524
634
|
if (typeof n !== 'number') {
|
|
525
635
|
this.typeError('parse', 'Number');
|
|
526
|
-
} else if (n < MAX_STAGE && !Parser.debugging && externalUse('parse')) {
|
|
636
|
+
} else if (n < MAX_STAGE && !Parser.debugging && Parser.warning && externalUse('parse')) {
|
|
527
637
|
Parser.warn('指定解析层级的方法仅供熟练用户使用!');
|
|
528
638
|
}
|
|
529
639
|
this.#include = Boolean(include);
|
|
530
640
|
while (this.#stage < n) {
|
|
531
|
-
this
|
|
641
|
+
this.#parseOnce(this.#stage, include);
|
|
532
642
|
}
|
|
533
643
|
return n ? this.build().afterBuild() : this;
|
|
534
644
|
}
|
|
535
645
|
|
|
536
|
-
|
|
646
|
+
/**
|
|
647
|
+
* 解析HTML注释和扩展标签
|
|
648
|
+
* @param {boolean} includeOnly 是否嵌入
|
|
649
|
+
*/
|
|
650
|
+
#parseCommentAndExt(includeOnly) {
|
|
537
651
|
const parseCommentAndExt = require('../parser/commentAndExt');
|
|
538
|
-
this.setText(parseCommentAndExt(this
|
|
652
|
+
this.setText(parseCommentAndExt(String(this), this.#config, this.#accum, includeOnly));
|
|
539
653
|
}
|
|
540
654
|
|
|
655
|
+
/** 解析花括号 */
|
|
541
656
|
#parseBrackets() {
|
|
542
657
|
const parseBrackets = require('../parser/brackets');
|
|
543
|
-
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));
|
|
544
661
|
}
|
|
545
662
|
|
|
663
|
+
/** 解析HTML标签 */
|
|
546
664
|
#parseHtml() {
|
|
547
665
|
const parseHtml = require('../parser/html');
|
|
548
|
-
this.setText(parseHtml(this
|
|
666
|
+
this.setText(parseHtml(String(this), this.#config, this.#accum));
|
|
549
667
|
}
|
|
550
668
|
|
|
669
|
+
/** 解析表格 */
|
|
551
670
|
#parseTable() {
|
|
552
671
|
const parseTable = require('../parser/table'),
|
|
553
672
|
TableToken = require('./table');
|
|
@@ -555,7 +674,7 @@ class Token extends AstElement {
|
|
|
555
674
|
for (const table of this.#accum) {
|
|
556
675
|
if (table instanceof TableToken && table.type !== 'td') {
|
|
557
676
|
table.normalize();
|
|
558
|
-
const [, child] = table
|
|
677
|
+
const {childNodes: [, child]} = table;
|
|
559
678
|
if (typeof child === 'string' && child.includes('\0')) {
|
|
560
679
|
table.removeAt(1);
|
|
561
680
|
const inner = new Token(child, this.#config, true, this.#accum);
|
|
@@ -566,49 +685,58 @@ class Token extends AstElement {
|
|
|
566
685
|
}
|
|
567
686
|
}
|
|
568
687
|
|
|
688
|
+
/** 解析\<hr\>和状态开关 */
|
|
569
689
|
#parseHrAndDoubleUndescore() {
|
|
570
690
|
const parseHrAndDoubleUnderscore = require('../parser/hrAndDoubleUnderscore');
|
|
571
|
-
this.setText(parseHrAndDoubleUnderscore(this
|
|
691
|
+
this.setText(parseHrAndDoubleUnderscore(this, this.#config, this.#accum));
|
|
572
692
|
}
|
|
573
693
|
|
|
694
|
+
/** 解析内部链接 */
|
|
574
695
|
#parseLinks() {
|
|
575
696
|
const parseLinks = require('../parser/links');
|
|
576
|
-
this.setText(parseLinks(this
|
|
697
|
+
this.setText(parseLinks(String(this), this.#config, this.#accum));
|
|
577
698
|
}
|
|
578
699
|
|
|
579
|
-
/**
|
|
700
|
+
/** 解析单引号 */
|
|
580
701
|
#parseQuotes() {
|
|
581
|
-
const parseQuotes = require('../parser/quotes')
|
|
582
|
-
|
|
702
|
+
const parseQuotes = require('../parser/quotes');
|
|
703
|
+
const lines = String(this).split('\n');
|
|
583
704
|
for (let i = 0; i < lines.length; i++) {
|
|
584
705
|
lines[i] = parseQuotes(lines[i], this.#config, this.#accum);
|
|
585
706
|
}
|
|
586
707
|
this.setText(lines.join('\n'));
|
|
587
708
|
}
|
|
588
709
|
|
|
710
|
+
/** 解析外部链接 */
|
|
589
711
|
#parseExternalLinks() {
|
|
590
712
|
const parseExternalLinks = require('../parser/externalLinks');
|
|
591
|
-
this.setText(parseExternalLinks(this
|
|
713
|
+
this.setText(parseExternalLinks(String(this), this.#config, this.#accum));
|
|
592
714
|
}
|
|
593
715
|
|
|
716
|
+
/** 解析自由外链 */
|
|
594
717
|
#parseMagicLinks() {
|
|
595
718
|
const parseMagicLinks = require('../parser/magicLinks');
|
|
596
|
-
this.setText(parseMagicLinks(this
|
|
719
|
+
this.setText(parseMagicLinks(String(this), this.#config, this.#accum));
|
|
597
720
|
}
|
|
598
721
|
|
|
599
|
-
/**
|
|
722
|
+
/** 解析列表 */
|
|
600
723
|
#parseList() {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
724
|
+
if (this.type === 'image-parameter') {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
const parseList = require('../parser/list');
|
|
728
|
+
const lines = String(this).split('\n');
|
|
729
|
+
let i = this.type === 'root' || this.type === 'ext-inner' && this.type === 'poem' ? 0 : 1;
|
|
730
|
+
for (; i < lines.length; i++) {
|
|
604
731
|
lines[i] = parseList(lines[i], this.#config, this.#accum);
|
|
605
732
|
}
|
|
606
733
|
this.setText(lines.join('\n'));
|
|
607
734
|
}
|
|
608
735
|
|
|
736
|
+
/** 解析语言变体转换 */
|
|
609
737
|
#parseConverter() {
|
|
610
738
|
const parseConverter = require('../parser/converter');
|
|
611
|
-
this.setText(parseConverter(this
|
|
739
|
+
this.setText(parseConverter(String(this), this.#config, this.#accum));
|
|
612
740
|
}
|
|
613
741
|
}
|
|
614
742
|
|