wikiparser-node 1.9.1 → 1.9.2
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 +18 -18
- package/dist/addon/token.js +1 -0
- package/dist/index.js +24 -13
- package/dist/lib/node.js +1 -9
- package/dist/parser/braces.js +12 -8
- package/dist/src/arg.js +1 -1
- package/dist/src/attribute.js +1 -8
- package/dist/src/attributes.js +4 -4
- package/dist/src/converterFlags.js +1 -5
- package/dist/src/converterRule.js +1 -1
- package/dist/src/html.js +0 -5
- package/dist/src/imageParameter.js +1 -16
- package/dist/src/index.js +12 -8
- package/dist/src/link/base.js +1 -1
- package/dist/src/link/galleryImage.js +1 -0
- package/dist/src/magicLink.js +0 -1
- package/dist/src/nowiki/base.js +0 -1
- package/dist/src/nowiki/doubleUnderscore.js +2 -6
- package/dist/src/parameter.js +1 -1
- package/dist/src/syntax.js +0 -1
- package/dist/src/table/base.d.ts +4 -2
- package/dist/src/table/base.js +3 -2
- package/dist/src/table/index.js +10 -7
- package/dist/src/table/td.js +8 -8
- package/dist/src/table/tr.js +1 -1
- package/dist/src/transclude.js +5 -8
- package/dist/util/constants.js +2 -4
- package/dist/util/debug.js +7 -0
- package/package.json +1 -1
- package/README.en.md +0 -57
package/README.md
CHANGED
|
@@ -4,54 +4,54 @@
|
|
|
4
4
|
|
|
5
5
|
# Other Languages
|
|
6
6
|
|
|
7
|
-
- [
|
|
7
|
+
- [简体中文](./README-%28ZH%29.md)
|
|
8
8
|
|
|
9
|
-
#
|
|
9
|
+
# Introduction
|
|
10
10
|
|
|
11
|
-
WikiParser-Node
|
|
11
|
+
WikiParser-Node is an offline [Wikitext](https://www.mediawiki.org/wiki/Wikitext) parser developed by Bhsd for the [Node.js](https://nodejs.org/) environment. It can parse almost all wiki syntax and generate an [Abstract Syntax Tree (AST)](https://en.wikipedia.org/wiki/Abstract_syntax_tree) ([Try it online](https://bhsd-harry.github.io/wikiparser-node/#editor)). It also allows for easy querying and modification of the AST, and returns the modified wikitext.
|
|
12
12
|
|
|
13
|
-
#
|
|
13
|
+
# Other Versions
|
|
14
14
|
|
|
15
|
-
## Mini (
|
|
15
|
+
## Mini (also known as [WikiLint](https://www.npmjs.com/package/wikilint))
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
This version provides a [CLI](https://en.wikipedia.org/wiki/Command-line_interface), but only retains the parsing functionality and linting functionality. The parsed AST cannot be modified. It is used in the [eslint-plugin-wikitext](https://www.npmjs.com/package/eslint-plugin-wikitext) plugin.
|
|
18
18
|
|
|
19
19
|
## Browser-compatible
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
A browser-compatible version, which can be used for code highlighting or as a linting plugin in conjunction with editors such as [CodeMirror](https://codemirror.net/) and [Monaco](https://microsoft.github.io/monaco-editor/). ([Usage example](https://bhsd-harry.github.io/wikiparser-node))
|
|
22
22
|
|
|
23
|
-
#
|
|
23
|
+
# Installation
|
|
24
24
|
|
|
25
25
|
## Node.js
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Please install the corresponding version as needed (`WikiParser-Node` or `WikiLint`), for example:
|
|
28
28
|
|
|
29
29
|
```sh
|
|
30
30
|
npm i wikiparser-node
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
or
|
|
34
34
|
|
|
35
35
|
```sh
|
|
36
36
|
npm i wikilint
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
##
|
|
39
|
+
## Browser
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
You can download the code via CDN, for example:
|
|
42
42
|
|
|
43
43
|
```html
|
|
44
|
-
<script src="//cdn.jsdelivr.net/npm/wikiparser-node@browser"></script>
|
|
44
|
+
<script src="//cdn.jsdelivr.net/npm/wikiparser-node@browser/bundle/bundle.min.js"></script>
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
or
|
|
48
48
|
|
|
49
49
|
```html
|
|
50
|
-
<script src="//unpkg.com/wikiparser-node@browser"></script>
|
|
50
|
+
<script src="//unpkg.com/wikiparser-node@browser/bundle/bundle.min.js"></script>
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
For more browser extensions, please refer to the corresponding [documentation](https://github.com/bhsd-harry/wikiparser-node/wiki/Browser-%28EN%29).
|
|
54
54
|
|
|
55
|
-
#
|
|
55
|
+
# Usage
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
Please refer to the [Wiki](https://github.com/bhsd-harry/wikiparser-node/wiki/Home-%28EN%29).
|
package/dist/addon/token.js
CHANGED
|
@@ -33,6 +33,7 @@ index_1.Token.prototype.createElement =
|
|
|
33
33
|
return debug_1.Shadow.run(() => {
|
|
34
34
|
// @ts-expect-error abstract class
|
|
35
35
|
const attr = new attributes_1.AttributesToken(undefined, 'html-attrs', tagName, config);
|
|
36
|
+
attr.afterBuild();
|
|
36
37
|
// @ts-expect-error abstract class
|
|
37
38
|
return new html_1.HtmlToken(tagName, attr, Boolean(closing), Boolean(selfClosing), config);
|
|
38
39
|
});
|
package/dist/index.js
CHANGED
|
@@ -14,14 +14,26 @@ const diff_1 = require("./util/diff");
|
|
|
14
14
|
* @param dir 子路径
|
|
15
15
|
*/
|
|
16
16
|
const rootRequire = (file, dir) => require(file.startsWith('/') ? file : `../${file.includes('/') ? '' : dir}${file}`);
|
|
17
|
+
/* NOT FOR BROWSER */
|
|
17
18
|
const promises = [Promise.resolve()];
|
|
19
|
+
let viewOnly = false;
|
|
20
|
+
/* NOT FOR BROWSER END */
|
|
18
21
|
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
|
19
22
|
const Parser = {
|
|
20
23
|
config: 'default',
|
|
21
24
|
i18n: undefined,
|
|
22
25
|
rules: base_1.rules,
|
|
23
26
|
/* NOT FOR BROWSER */
|
|
24
|
-
|
|
27
|
+
/** @implements */
|
|
28
|
+
get viewOnly() {
|
|
29
|
+
return viewOnly;
|
|
30
|
+
},
|
|
31
|
+
set viewOnly(value) {
|
|
32
|
+
if (viewOnly && !value) {
|
|
33
|
+
debug_1.Shadow.rev++;
|
|
34
|
+
}
|
|
35
|
+
viewOnly = value;
|
|
36
|
+
},
|
|
25
37
|
conversionTable: new Map(),
|
|
26
38
|
redirects: new Map(),
|
|
27
39
|
warning: true,
|
|
@@ -62,24 +74,23 @@ const Parser = {
|
|
|
62
74
|
return new Title(title, defaultNs, config, decode, selfLink);
|
|
63
75
|
}
|
|
64
76
|
const { Token } = require('./src/index');
|
|
65
|
-
|
|
77
|
+
return debug_1.Shadow.run(() => {
|
|
66
78
|
const root = new Token(title, config);
|
|
67
79
|
root.type = 'root';
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
debug_1.Shadow.run(() => {
|
|
80
|
+
root.parseOnce(0, include).parseOnce();
|
|
81
|
+
const titleObj = new Title(root.toString(), defaultNs, config, decode, selfLink);
|
|
71
82
|
for (const key of ['main', 'fragment']) {
|
|
72
83
|
const str = titleObj[key];
|
|
73
84
|
if (str?.includes('\0')) {
|
|
74
|
-
titleObj[key] =
|
|
85
|
+
titleObj[key] = root.buildFromStr(str, constants_1.BuildMethod.Text);
|
|
75
86
|
}
|
|
76
87
|
}
|
|
88
|
+
/* NOT FOR BROWSER */
|
|
89
|
+
titleObj.conversionTable = this.conversionTable;
|
|
90
|
+
titleObj.redirects = this.redirects;
|
|
91
|
+
/* NOT FOR BROWSER END */
|
|
92
|
+
return titleObj;
|
|
77
93
|
});
|
|
78
|
-
/* NOT FOR BROWSER */
|
|
79
|
-
titleObj.conversionTable = this.conversionTable;
|
|
80
|
-
titleObj.redirects = this.redirects;
|
|
81
|
-
/* NOT FOR BROWSER END */
|
|
82
|
-
return titleObj;
|
|
83
94
|
},
|
|
84
95
|
/** @implements */
|
|
85
96
|
parse(wikitext, include, maxStage = constants_1.MAX_STAGE, config = Parser.getConfig()) {
|
|
@@ -148,6 +159,7 @@ const Parser = {
|
|
|
148
159
|
...Object.entries(constants_1.classes),
|
|
149
160
|
...Object.entries(constants_1.mixins),
|
|
150
161
|
...Object.entries(constants_1.parsers),
|
|
162
|
+
...Object.entries(constants_1.constants),
|
|
151
163
|
];
|
|
152
164
|
for (const [, filePath] of entries) {
|
|
153
165
|
try {
|
|
@@ -182,7 +194,7 @@ const Parser = {
|
|
|
182
194
|
}
|
|
183
195
|
const file = path.join(__dirname, '..', 'errors', main), wikitext = fs.readFileSync(file, 'utf8');
|
|
184
196
|
const { stage, include, config } = require(`${file}.json`), { Token } = require('./src/index');
|
|
185
|
-
|
|
197
|
+
debug_1.Shadow.run(() => {
|
|
186
198
|
const halfParsed = stage < constants_1.MAX_STAGE, token = new Token(halfParsed ? wikitext : (0, string_1.tidy)(wikitext), config);
|
|
187
199
|
token.type = 'root';
|
|
188
200
|
if (halfParsed) {
|
|
@@ -195,7 +207,6 @@ const Parser = {
|
|
|
195
207
|
fs.unlinkSync(file);
|
|
196
208
|
fs.unlinkSync(`${file}.err`);
|
|
197
209
|
fs.unlinkSync(`${file}.json`);
|
|
198
|
-
return token;
|
|
199
210
|
});
|
|
200
211
|
},
|
|
201
212
|
};
|
package/dist/lib/node.js
CHANGED
|
@@ -163,15 +163,7 @@ class AstNode {
|
|
|
163
163
|
/* NOT FOR BROWSER END */
|
|
164
164
|
/** @private */
|
|
165
165
|
getAttribute(key) {
|
|
166
|
-
|
|
167
|
-
return 0;
|
|
168
|
-
/* NOT FOR BROWSER */
|
|
169
|
-
}
|
|
170
|
-
else if (key === 'optional') {
|
|
171
|
-
return this.#optional;
|
|
172
|
-
/* NOT FOR BROWSER END */
|
|
173
|
-
}
|
|
174
|
-
return this[key];
|
|
166
|
+
return (key === 'padding' ? 0 : this[key]);
|
|
175
167
|
}
|
|
176
168
|
/** @private */
|
|
177
169
|
setAttribute(key, value) {
|
package/dist/parser/braces.js
CHANGED
|
@@ -22,7 +22,7 @@ const closes = {
|
|
|
22
22
|
* @throws TranscludeToken.constructor()
|
|
23
23
|
*/
|
|
24
24
|
const parseBraces = (wikitext, config, accum) => {
|
|
25
|
-
const source = String.raw `${config.excludes?.includes('heading') ? '' : String.raw `^(\0\d+c\x7F)*={1,6}|`}\[\[
|
|
25
|
+
const source = String.raw `${config.excludes?.includes('heading') ? '' : String.raw `^(\0\d+c\x7F)*={1,6}|`}\[\[|-\{(?!\{)`, openBraces = String.raw `|\{{2,}`, { parserFunction: [, , , subst] } = config, stack = [];
|
|
26
26
|
wikitext = wikitext.replace(re, (m, p1) => {
|
|
27
27
|
// @ts-expect-error abstract class
|
|
28
28
|
new transclude_1.TranscludeToken(m.slice(2, -2), [], config, accum);
|
|
@@ -30,8 +30,8 @@ const parseBraces = (wikitext, config, accum) => {
|
|
|
30
30
|
});
|
|
31
31
|
const lastBraces = wikitext.lastIndexOf('}}') - wikitext.length;
|
|
32
32
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
33
|
-
/^(\0\d+c\x7F)*={1,6}|\[\[|\{{2,}
|
|
34
|
-
let regex = new RegExp(source, 'gmu'), mt = regex.exec(wikitext),
|
|
33
|
+
/^(\0\d+c\x7F)*={1,6}|\[\[|-\{(?!\{)|\{{2,}|[\n|=]|\}{2,}|\}-|\]\]/gmu;
|
|
34
|
+
let moreBraces = lastBraces + wikitext.length !== -1, regex = new RegExp(source + (moreBraces ? openBraces : ''), 'gmu'), mt = regex.exec(wikitext), lastIndex;
|
|
35
35
|
while (mt
|
|
36
36
|
|| lastIndex !== undefined && lastIndex <= wikitext.length && stack[stack.length - 1]?.[0]?.startsWith('=')) {
|
|
37
37
|
if (mt?.[1]) {
|
|
@@ -130,13 +130,17 @@ const parseBraces = (wikitext, config, accum) => {
|
|
|
130
130
|
}
|
|
131
131
|
stack.push(...'0' in top ? [top] : [], mt);
|
|
132
132
|
}
|
|
133
|
-
moreBraces &&= lastBraces + wikitext.length >= lastIndex;
|
|
134
133
|
let curTop = stack[stack.length - 1];
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
curTop
|
|
134
|
+
if (moreBraces && lastBraces + wikitext.length < lastIndex) {
|
|
135
|
+
moreBraces = false;
|
|
136
|
+
while (curTop?.[0]?.startsWith('{')) {
|
|
137
|
+
stack.pop();
|
|
138
|
+
curTop = stack[stack.length - 1];
|
|
139
|
+
}
|
|
138
140
|
}
|
|
139
|
-
regex = new RegExp(source
|
|
141
|
+
regex = new RegExp(source
|
|
142
|
+
+ (moreBraces ? openBraces : '')
|
|
143
|
+
+ (curTop ? `|${closes[curTop[0][0]]}${curTop.findEqual ? '|=' : ''}` : ''), 'gmu');
|
|
140
144
|
regex.lastIndex = lastIndex;
|
|
141
145
|
mt = regex.exec(wikitext);
|
|
142
146
|
}
|
package/dist/src/arg.js
CHANGED
|
@@ -127,7 +127,6 @@ class ArgToken extends index_2.Token {
|
|
|
127
127
|
const token = new ArgToken([''], this.getAttribute('config'));
|
|
128
128
|
token.firstChild.safeReplaceWith(name);
|
|
129
129
|
token.append(...cloned);
|
|
130
|
-
token.afterBuild();
|
|
131
130
|
return token;
|
|
132
131
|
});
|
|
133
132
|
}
|
|
@@ -138,6 +137,7 @@ class ArgToken extends index_2.Token {
|
|
|
138
137
|
/** @private */
|
|
139
138
|
afterBuild() {
|
|
140
139
|
this.#setName();
|
|
140
|
+
super.afterBuild();
|
|
141
141
|
const /** @implements */ argListener = ({ prevTarget }) => {
|
|
142
142
|
if (prevTarget === this.firstChild) {
|
|
143
143
|
this.#setName();
|
package/dist/src/attribute.js
CHANGED
|
@@ -321,6 +321,7 @@ let AttributeToken = (() => {
|
|
|
321
321
|
this.#tag = this.parentNode.name;
|
|
322
322
|
}
|
|
323
323
|
this.setAttribute('name', this.firstChild.text().trim().toLowerCase());
|
|
324
|
+
super.afterBuild();
|
|
324
325
|
}
|
|
325
326
|
/** @private */
|
|
326
327
|
toString() {
|
|
@@ -408,13 +409,6 @@ let AttributeToken = (() => {
|
|
|
408
409
|
return this.#equal ? super.print({ sep: (0, string_1.escape)(this.#equal) + quoteStart, post: quoteEnd }) : super.print();
|
|
409
410
|
}
|
|
410
411
|
/* NOT FOR BROWSER */
|
|
411
|
-
/** @private */
|
|
412
|
-
getAttribute(key) {
|
|
413
|
-
if (key === 'equal') {
|
|
414
|
-
return this.#equal;
|
|
415
|
-
}
|
|
416
|
-
return key === 'quotes' ? this.#quotes : super.getAttribute(key);
|
|
417
|
-
}
|
|
418
412
|
/** @override */
|
|
419
413
|
cloneNode() {
|
|
420
414
|
const [key, value] = this.cloneChildNodes(), config = this.getAttribute('config');
|
|
@@ -423,7 +417,6 @@ let AttributeToken = (() => {
|
|
|
423
417
|
const token = new AttributeToken(this.type, this.tag, '', this.#equal, '', this.#quotes, config);
|
|
424
418
|
token.firstChild.safeReplaceWith(key);
|
|
425
419
|
token.lastChild.safeReplaceWith(value);
|
|
426
|
-
token.setAttribute('name', this.name);
|
|
427
420
|
return token;
|
|
428
421
|
});
|
|
429
422
|
}
|
package/dist/src/attributes.js
CHANGED
|
@@ -114,10 +114,11 @@ class AttributesToken extends index_2.Token {
|
|
|
114
114
|
}
|
|
115
115
|
/** @private */
|
|
116
116
|
afterBuild() {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
this.setAttribute('name',
|
|
117
|
+
const { parentNode } = this;
|
|
118
|
+
if (parentNode?.type === 'td' && parentNode.subtype === 'caption') {
|
|
119
|
+
this.setAttribute('name', 'caption');
|
|
120
120
|
}
|
|
121
|
+
super.afterBuild();
|
|
121
122
|
}
|
|
122
123
|
/**
|
|
123
124
|
* 所有指定属性名的AttributeToken
|
|
@@ -208,7 +209,6 @@ class AttributesToken extends index_2.Token {
|
|
|
208
209
|
// @ts-expect-error abstract class
|
|
209
210
|
const token = new AttributesToken(undefined, this.type, this.name, this.getAttribute('config'));
|
|
210
211
|
token.append(...cloned);
|
|
211
|
-
token.setAttribute('name', this.name);
|
|
212
212
|
return token;
|
|
213
213
|
});
|
|
214
214
|
}
|
|
@@ -37,6 +37,7 @@ class ConverterFlagsToken extends index_2.Token {
|
|
|
37
37
|
/** @private */
|
|
38
38
|
afterBuild() {
|
|
39
39
|
this.#flags = this.childNodes.map(child => child.text().trim());
|
|
40
|
+
super.afterBuild();
|
|
40
41
|
/* NOT FOR BROWSER */
|
|
41
42
|
const /** @implements */ converterFlagsListener = ({ prevTarget }) => {
|
|
42
43
|
if (prevTarget) {
|
|
@@ -112,14 +113,9 @@ class ConverterFlagsToken extends index_2.Token {
|
|
|
112
113
|
// @ts-expect-error abstract class
|
|
113
114
|
const token = new ConverterFlagsToken([], this.getAttribute('config'));
|
|
114
115
|
token.append(...cloned);
|
|
115
|
-
token.afterBuild();
|
|
116
116
|
return token;
|
|
117
117
|
});
|
|
118
118
|
}
|
|
119
|
-
/** @private */
|
|
120
|
-
getAttribute(key) {
|
|
121
|
-
return key === 'flags' ? this.#flags : super.getAttribute(key);
|
|
122
|
-
}
|
|
123
119
|
/**
|
|
124
120
|
* @override
|
|
125
121
|
* @param i 移除位置
|
|
@@ -116,12 +116,12 @@ class ConverterRuleToken extends index_2.Token {
|
|
|
116
116
|
for (let i = 0; i < cloned.length; i++) {
|
|
117
117
|
token.childNodes[i].safeReplaceWith(cloned[i]);
|
|
118
118
|
}
|
|
119
|
-
token.afterBuild();
|
|
120
119
|
return token;
|
|
121
120
|
});
|
|
122
121
|
}
|
|
123
122
|
/** @private */
|
|
124
123
|
afterBuild() {
|
|
124
|
+
super.afterBuild();
|
|
125
125
|
const /** @implements */ converterRuleListener = (e, data) => {
|
|
126
126
|
const { prevTarget } = e;
|
|
127
127
|
if (this.length > 1 && this.childNodes[this.length - 2] === prevTarget) {
|
package/dist/src/html.js
CHANGED
|
@@ -168,11 +168,6 @@ let HtmlToken = (() => {
|
|
|
168
168
|
}
|
|
169
169
|
/** @private */
|
|
170
170
|
getAttribute(key) {
|
|
171
|
-
/* NOT FOR BROWSER */
|
|
172
|
-
if (key === 'tag') {
|
|
173
|
-
return this.#tag;
|
|
174
|
-
}
|
|
175
|
-
/* NOT FOR BROWSER END */
|
|
176
171
|
return key === 'padding'
|
|
177
172
|
? this.#tag.length + (this.closing ? 2 : 1)
|
|
178
173
|
: super.getAttribute(key);
|
|
@@ -151,6 +151,7 @@ class ImageParameterToken extends index_2.Token {
|
|
|
151
151
|
if (this.parentNode?.type === 'gallery-image' && !exports.galleryParams.has(this.name)) {
|
|
152
152
|
this.setAttribute('name', 'invalid');
|
|
153
153
|
}
|
|
154
|
+
super.afterBuild();
|
|
154
155
|
}
|
|
155
156
|
/** @private */
|
|
156
157
|
toString() {
|
|
@@ -164,11 +165,6 @@ class ImageParameterToken extends index_2.Token {
|
|
|
164
165
|
getAttribute(key) {
|
|
165
166
|
if (key === 'plain') {
|
|
166
167
|
return (this.name === 'caption');
|
|
167
|
-
/* NOT FOR BROWSER */
|
|
168
|
-
}
|
|
169
|
-
else if (key === 'syntax') {
|
|
170
|
-
return this.#syntax;
|
|
171
|
-
/* NOT FOR BROWSER END */
|
|
172
168
|
}
|
|
173
169
|
return key === 'padding'
|
|
174
170
|
? Math.max(0, this.#syntax.indexOf('$1'))
|
|
@@ -213,20 +209,9 @@ class ImageParameterToken extends index_2.Token {
|
|
|
213
209
|
// @ts-expect-error abstract class
|
|
214
210
|
const token = new ImageParameterToken(this.#syntax.replace('$1', ''), this.#extension, config);
|
|
215
211
|
token.replaceChildren(...cloned);
|
|
216
|
-
token.setAttribute('name', this.name);
|
|
217
|
-
token.setAttribute('syntax', this.#syntax);
|
|
218
212
|
return token;
|
|
219
213
|
});
|
|
220
214
|
}
|
|
221
|
-
/** @private */
|
|
222
|
-
setAttribute(key, value) {
|
|
223
|
-
if (key === 'syntax') {
|
|
224
|
-
this.#syntax = value;
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
super.setAttribute(key, value);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
215
|
insertAt(token, i) {
|
|
231
216
|
if (!debug_1.Shadow.running && this.#isVoid()) {
|
|
232
217
|
throw new Error(`Image parameter ${this.name} does not accept custom input!`);
|
package/dist/src/index.js
CHANGED
|
@@ -197,8 +197,8 @@ class Token extends element_1.AstElement {
|
|
|
197
197
|
}
|
|
198
198
|
return nodes;
|
|
199
199
|
}
|
|
200
|
-
/**
|
|
201
|
-
|
|
200
|
+
/** @private */
|
|
201
|
+
build() {
|
|
202
202
|
this.#stage = constants_1.MAX_STAGE;
|
|
203
203
|
const { length, firstChild } = this, str = String(firstChild);
|
|
204
204
|
if (length === 1 && firstChild.type === 'text' && str.includes('\0')) {
|
|
@@ -206,7 +206,7 @@ class Token extends element_1.AstElement {
|
|
|
206
206
|
this.normalize();
|
|
207
207
|
if (this.type === 'root') {
|
|
208
208
|
for (const token of this.#accum) {
|
|
209
|
-
token
|
|
209
|
+
token.build();
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
}
|
|
@@ -227,7 +227,7 @@ class Token extends element_1.AstElement {
|
|
|
227
227
|
this.parseOnce(this.#stage, include);
|
|
228
228
|
}
|
|
229
229
|
if (n) {
|
|
230
|
-
this
|
|
230
|
+
this.build();
|
|
231
231
|
this.afterBuild();
|
|
232
232
|
}
|
|
233
233
|
return this;
|
|
@@ -343,6 +343,8 @@ class Token extends element_1.AstElement {
|
|
|
343
343
|
return (this.#include ?? Boolean(this.getRootNode().#include));
|
|
344
344
|
case 'accum':
|
|
345
345
|
return this.#accum;
|
|
346
|
+
case 'built':
|
|
347
|
+
return this.#built;
|
|
346
348
|
/* NOT FOR BROWSER */
|
|
347
349
|
case 'stage':
|
|
348
350
|
return this.#stage;
|
|
@@ -478,14 +480,16 @@ class Token extends element_1.AstElement {
|
|
|
478
480
|
}
|
|
479
481
|
/** @private */
|
|
480
482
|
toString(separator) {
|
|
481
|
-
const root = this.getRootNode();
|
|
483
|
+
const { rev } = debug_1.Shadow, root = this.getRootNode();
|
|
484
|
+
if (this.#string && this.#string[0] !== rev) {
|
|
485
|
+
this.#string = undefined;
|
|
486
|
+
}
|
|
482
487
|
if (root.type === 'root'
|
|
483
488
|
&& root.#built
|
|
484
489
|
&& index_1.default.viewOnly) {
|
|
485
|
-
this.#string ??= super.toString(separator);
|
|
486
|
-
return this.#string;
|
|
490
|
+
this.#string ??= [rev, super.toString(separator)];
|
|
491
|
+
return this.#string[1];
|
|
487
492
|
}
|
|
488
|
-
this.#string = undefined;
|
|
489
493
|
return super.toString(separator);
|
|
490
494
|
}
|
|
491
495
|
/* NOT FOR BROWSER */
|
package/dist/src/link/base.js
CHANGED
|
@@ -64,6 +64,7 @@ class LinkBaseToken extends index_2.Token {
|
|
|
64
64
|
this.#delimiter = this.buildFromStr(this.#delimiter, constants_1.BuildMethod.String);
|
|
65
65
|
}
|
|
66
66
|
this.setAttribute('name', this.#title.title);
|
|
67
|
+
super.afterBuild();
|
|
67
68
|
/* NOT FOR BROWSER */
|
|
68
69
|
const /** @implements */ linkListener = (e, data) => {
|
|
69
70
|
const { prevTarget } = e;
|
|
@@ -183,7 +184,6 @@ class LinkBaseToken extends index_2.Token {
|
|
|
183
184
|
const token = new this.constructor('', undefined, this.getAttribute('config'));
|
|
184
185
|
token.firstChild.safeReplaceWith(link);
|
|
185
186
|
token.append(...linkText);
|
|
186
|
-
token.afterBuild();
|
|
187
187
|
return token;
|
|
188
188
|
});
|
|
189
189
|
}
|
package/dist/src/magicLink.js
CHANGED
package/dist/src/nowiki/base.js
CHANGED
|
@@ -86,12 +86,8 @@ let DoubleUnderscoreToken = (() => {
|
|
|
86
86
|
/** @override */
|
|
87
87
|
cloneNode() {
|
|
88
88
|
const config = this.getAttribute('config');
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const token = new DoubleUnderscoreToken(this.innerText, this.#sensitive, config);
|
|
92
|
-
token.afterBuild();
|
|
93
|
-
return token;
|
|
94
|
-
});
|
|
89
|
+
// @ts-expect-error abstract class
|
|
90
|
+
return debug_1.Shadow.run(() => new DoubleUnderscoreToken(this.innerText, this.#sensitive, config));
|
|
95
91
|
}
|
|
96
92
|
};
|
|
97
93
|
return DoubleUnderscoreToken = _classThis;
|
package/dist/src/parameter.js
CHANGED
|
@@ -129,6 +129,7 @@ let ParameterToken = (() => {
|
|
|
129
129
|
parentNode.getAttribute('keys').add(name);
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
+
super.afterBuild();
|
|
132
133
|
/* NOT FOR BROWSER */
|
|
133
134
|
const /** @implements */ parameterListener = ({ prevTarget }, data) => {
|
|
134
135
|
if (!this.anon) { // 匿名参数不管怎么变动还是匿名
|
|
@@ -192,7 +193,6 @@ let ParameterToken = (() => {
|
|
|
192
193
|
const token = new ParameterToken(this.anon ? Number(this.name) : undefined, undefined, config);
|
|
193
194
|
token.firstChild.safeReplaceWith(key);
|
|
194
195
|
token.lastChild.safeReplaceWith(value);
|
|
195
|
-
token.afterBuild();
|
|
196
196
|
if (this.anon) {
|
|
197
197
|
token.setAttribute('name', this.name);
|
|
198
198
|
}
|
package/dist/src/syntax.js
CHANGED
package/dist/src/table/base.d.ts
CHANGED
|
@@ -3,10 +3,11 @@ import { Token } from '../index';
|
|
|
3
3
|
import { SyntaxToken } from '../syntax';
|
|
4
4
|
import { AttributesToken } from '../attributes';
|
|
5
5
|
import type { AttributesParentBase } from '../../mixin/attributesParent';
|
|
6
|
+
declare type TableTypes = 'table' | 'tr' | 'td';
|
|
6
7
|
export interface TableBaseToken extends AttributesParentBase {
|
|
7
8
|
}
|
|
8
9
|
export declare abstract class TableBaseToken extends Token {
|
|
9
|
-
type:
|
|
10
|
+
type: TableTypes;
|
|
10
11
|
readonly childNodes: readonly [SyntaxToken, AttributesToken, ...Token[]];
|
|
11
12
|
abstract get firstChild(): SyntaxToken;
|
|
12
13
|
abstract get lastChild(): Token;
|
|
@@ -16,9 +17,10 @@ export declare abstract class TableBaseToken extends Token {
|
|
|
16
17
|
/**
|
|
17
18
|
* @param pattern 表格语法正则
|
|
18
19
|
* @param syntax 表格语法
|
|
20
|
+
* @param type 节点类型
|
|
19
21
|
* @param attr 表格属性
|
|
20
22
|
*/
|
|
21
|
-
constructor(pattern: RegExp, syntax
|
|
23
|
+
constructor(pattern: RegExp, syntax: string, type: TableTypes, attr?: string, config?: Parser.Config, accum?: Token[], acceptable?: Acceptable);
|
|
22
24
|
/** @override */
|
|
23
25
|
cloneNode(): this;
|
|
24
26
|
/** 转义表格语法 */
|
package/dist/src/table/base.js
CHANGED
|
@@ -28,15 +28,16 @@ class TableBaseToken extends (0, attributesParent_1.attributesParent)(1)(index_2
|
|
|
28
28
|
/**
|
|
29
29
|
* @param pattern 表格语法正则
|
|
30
30
|
* @param syntax 表格语法
|
|
31
|
+
* @param type 节点类型
|
|
31
32
|
* @param attr 表格属性
|
|
32
33
|
*/
|
|
33
|
-
constructor(pattern, syntax, attr, config = index_1.default.getConfig(), accum = [], acceptable = {}) {
|
|
34
|
+
constructor(pattern, syntax, type, attr, config = index_1.default.getConfig(), accum = [], acceptable = {}) {
|
|
34
35
|
super(undefined, config, accum, acceptable);
|
|
35
36
|
this.append(new syntax_1.SyntaxToken(syntax, pattern, 'table-syntax', config, accum, {
|
|
36
37
|
'Stage-1': ':', '!ExtToken': '', TranscludeToken: ':',
|
|
37
38
|
}),
|
|
38
39
|
// @ts-expect-error abstract class
|
|
39
|
-
new attributes_1.AttributesToken(attr, 'table-attrs',
|
|
40
|
+
new attributes_1.AttributesToken(attr, 'table-attrs', type, config, accum));
|
|
40
41
|
/* NOT FOR BROWSER */
|
|
41
42
|
this.protectChildren(0, 1);
|
|
42
43
|
}
|
package/dist/src/table/index.js
CHANGED
|
@@ -65,7 +65,7 @@ class TableToken extends trBase_1.TrBaseToken {
|
|
|
65
65
|
* @param attr 表格属性
|
|
66
66
|
*/
|
|
67
67
|
constructor(syntax, attr, config, accum) {
|
|
68
|
-
super(/^(?:\{\||\{\{\{\s*!\s*\}\}|\{\{\s*\(!\s*\}\})$/u, syntax, attr, config, accum, {
|
|
68
|
+
super(/^(?:\{\||\{\{\{\s*!\s*\}\}|\{\{\s*\(!\s*\}\})$/u, syntax, 'table', attr, config, accum, {
|
|
69
69
|
Token: 2, SyntaxToken: [0, -1], AttributesToken: 1, TdToken: '2:', TrToken: '2:',
|
|
70
70
|
});
|
|
71
71
|
}
|
|
@@ -112,12 +112,15 @@ class TableToken extends trBase_1.TrBaseToken {
|
|
|
112
112
|
close(syntax = '\n|}', halfParsed) {
|
|
113
113
|
const config = this.getAttribute('config'), accum = this.getAttribute('accum'), inner = halfParsed ? [syntax] : index_1.default.parse(syntax, this.getAttribute('include'), 2, config).childNodes;
|
|
114
114
|
if (this.lastChild.type !== 'table-syntax') {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
debug_1.Shadow.run(() => {
|
|
116
|
+
const token = new syntax_1.SyntaxToken(undefined, closingPattern, 'table-syntax', config, accum, {
|
|
117
|
+
'Stage-1': ':', '!ExtToken': '', TranscludeToken: ':',
|
|
118
|
+
});
|
|
119
|
+
super.insertAt(token);
|
|
120
|
+
if (!halfParsed) {
|
|
121
|
+
token.afterBuild();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
121
124
|
}
|
|
122
125
|
this.lastChild.replaceChildren(...inner);
|
|
123
126
|
}
|
package/dist/src/table/td.js
CHANGED
|
@@ -108,7 +108,7 @@ let TdToken = (() => {
|
|
|
108
108
|
innerSyntax = null;
|
|
109
109
|
attr = '';
|
|
110
110
|
}
|
|
111
|
-
super(/^(?:\n[^\S\n]*(?:[|!]|\|\+|\{\{\s*!\s*\}\}\+?)|(?:\||\{\{\s*!\s*\}\}){2}|!!|\{\{\s*!!\s*\}\})$/u, syntax, attr, config, accum, { SyntaxToken: 0, AttributesToken: 1, Token: 2 });
|
|
111
|
+
super(/^(?:\n[^\S\n]*(?:[|!]|\|\+|\{\{\s*!\s*\}\}\+?)|(?:\||\{\{\s*!\s*\}\}){2}|!!|\{\{\s*!!\s*\}\})$/u, syntax, 'td', attr, config, accum, { SyntaxToken: 0, AttributesToken: 1, Token: 2 });
|
|
112
112
|
if (innerSyntax) {
|
|
113
113
|
[this.#innerSyntax] = innerSyntax;
|
|
114
114
|
}
|
|
@@ -119,11 +119,14 @@ let TdToken = (() => {
|
|
|
119
119
|
}
|
|
120
120
|
/** 表格语法信息 */
|
|
121
121
|
#getSyntax() {
|
|
122
|
+
const { rev } = debug_1.Shadow;
|
|
123
|
+
if (this.#syntax && this.#syntax[0] !== rev) {
|
|
124
|
+
this.#syntax = undefined;
|
|
125
|
+
}
|
|
122
126
|
if (index_1.default.viewOnly) {
|
|
123
|
-
this.#syntax ??= this.#computeSyntax();
|
|
124
|
-
return this.#syntax;
|
|
127
|
+
this.#syntax ??= [rev, this.#computeSyntax()];
|
|
128
|
+
return this.#syntax[1];
|
|
125
129
|
}
|
|
126
|
-
this.#syntax = undefined;
|
|
127
130
|
return this.#computeSyntax();
|
|
128
131
|
}
|
|
129
132
|
/** 表格语法信息 */
|
|
@@ -177,6 +180,7 @@ let TdToken = (() => {
|
|
|
177
180
|
if (this.#innerSyntax.includes('\0')) {
|
|
178
181
|
this.#innerSyntax = this.buildFromStr(this.#innerSyntax, constants_1.BuildMethod.String);
|
|
179
182
|
}
|
|
183
|
+
super.afterBuild();
|
|
180
184
|
}
|
|
181
185
|
/** @private */
|
|
182
186
|
toString() {
|
|
@@ -266,10 +270,6 @@ let TdToken = (() => {
|
|
|
266
270
|
return token;
|
|
267
271
|
}
|
|
268
272
|
/** @private */
|
|
269
|
-
getAttribute(key) {
|
|
270
|
-
return key === 'innerSyntax' ? this.#innerSyntax : super.getAttribute(key);
|
|
271
|
-
}
|
|
272
|
-
/** @private */
|
|
273
273
|
setAttribute(key, value) {
|
|
274
274
|
if (key === 'innerSyntax') {
|
|
275
275
|
this.#innerSyntax = (value ?? '');
|
package/dist/src/table/tr.js
CHANGED
|
@@ -16,7 +16,7 @@ class TrToken extends trBase_1.TrBaseToken {
|
|
|
16
16
|
* @param attr 表格属性
|
|
17
17
|
*/
|
|
18
18
|
constructor(syntax, attr, config, accum) {
|
|
19
|
-
super(/^\n[^\S\n]*(?:\|-+|\{\{\s*!\s*\}\}-+|\{\{\s*!-\s*\}\}-*)$/u, syntax, attr, config, accum, {
|
|
19
|
+
super(/^\n[^\S\n]*(?:\|-+|\{\{\s*!\s*\}\}-+|\{\{\s*!-\s*\}\}-*)$/u, syntax, 'tr', attr, config, accum, {
|
|
20
20
|
Token: 2, SyntaxToken: 0, AttributesToken: 1, TdToken: '2:',
|
|
21
21
|
});
|
|
22
22
|
}
|
package/dist/src/transclude.js
CHANGED
|
@@ -157,11 +157,12 @@ class TranscludeToken extends index_2.Token {
|
|
|
157
157
|
if (this.modifier.includes('\0')) {
|
|
158
158
|
this.setAttribute('modifier', this.buildFromStr(this.modifier, constants_1.BuildMethod.String));
|
|
159
159
|
}
|
|
160
|
+
super.afterBuild();
|
|
160
161
|
/* NOT FOR BROWSER */
|
|
161
162
|
if (this.isTemplate()) {
|
|
162
163
|
const isTemplate = this.type === 'template';
|
|
163
|
-
if (isTemplate
|
|
164
|
-
this.setAttribute(
|
|
164
|
+
if (isTemplate) {
|
|
165
|
+
this.setAttribute('name', this.#getTitle().title);
|
|
165
166
|
}
|
|
166
167
|
/**
|
|
167
168
|
* 当事件bubble到`parameter`时,将`oldKey`和`newKey`保存进AstEventData。
|
|
@@ -174,9 +175,8 @@ class TranscludeToken extends index_2.Token {
|
|
|
174
175
|
delete data.oldKey;
|
|
175
176
|
delete data.newKey;
|
|
176
177
|
}
|
|
177
|
-
if (prevTarget === this.firstChild && isTemplate
|
|
178
|
-
|
|
179
|
-
this.setAttribute(isTemplate ? 'name' : 'module', this.#getTitle().title);
|
|
178
|
+
if (prevTarget === this.firstChild && isTemplate) {
|
|
179
|
+
this.setAttribute('name', this.#getTitle().title);
|
|
180
180
|
}
|
|
181
181
|
else if (oldKey !== newKey && prevTarget instanceof parameter_1.ParameterToken) {
|
|
182
182
|
const oldArgs = this.getArgs(oldKey, false, false);
|
|
@@ -212,8 +212,6 @@ class TranscludeToken extends index_2.Token {
|
|
|
212
212
|
case 'padding':
|
|
213
213
|
return this.modifier.length + 2;
|
|
214
214
|
/* NOT FOR BROWSER */
|
|
215
|
-
case 'args':
|
|
216
|
-
return this.#args;
|
|
217
215
|
case 'keys':
|
|
218
216
|
return this.#keys;
|
|
219
217
|
/* NOT FOR BROWSER END */
|
|
@@ -409,7 +407,6 @@ class TranscludeToken extends index_2.Token {
|
|
|
409
407
|
token.setAttribute('modifier', this.modifier);
|
|
410
408
|
}
|
|
411
409
|
token.firstChild.safeReplaceWith(first);
|
|
412
|
-
token.afterBuild();
|
|
413
410
|
token.append(...cloned);
|
|
414
411
|
return token;
|
|
415
412
|
});
|
package/dist/util/constants.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.aliases = exports.parsers = exports.mixins = exports.classes = exports.BuildMethod = exports.MAX_STAGE = void 0;
|
|
3
|
+
exports.aliases = exports.constants = exports.parsers = exports.mixins = exports.classes = exports.BuildMethod = exports.MAX_STAGE = void 0;
|
|
4
4
|
exports.MAX_STAGE = 11;
|
|
5
5
|
var BuildMethod;
|
|
6
6
|
(function (BuildMethod) {
|
|
@@ -8,9 +8,7 @@ var BuildMethod;
|
|
|
8
8
|
BuildMethod[BuildMethod["Text"] = 1] = "Text";
|
|
9
9
|
})(BuildMethod || (exports.BuildMethod = BuildMethod = {}));
|
|
10
10
|
/* NOT FOR BROWSER */
|
|
11
|
-
exports.classes = {};
|
|
12
|
-
exports.mixins = {};
|
|
13
|
-
exports.parsers = {};
|
|
11
|
+
exports.classes = {}, exports.mixins = {}, exports.parsers = {}, exports.constants = {};
|
|
14
12
|
exports.aliases = [
|
|
15
13
|
['AstText'],
|
|
16
14
|
['CommentToken', 'ExtToken', 'IncludeToken', 'NoincludeToken'],
|
package/dist/util/debug.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.undo = exports.mixin = exports.emptyArray = exports.setChildNodes = exports.isToken = exports.Shadow = void 0;
|
|
4
|
+
const constants_1 = require("./constants");
|
|
4
5
|
exports.Shadow = {
|
|
5
6
|
running: false,
|
|
6
7
|
/** @private */
|
|
@@ -8,7 +9,11 @@ exports.Shadow = {
|
|
|
8
9
|
const { running } = this;
|
|
9
10
|
this.running = true;
|
|
10
11
|
try {
|
|
12
|
+
const { Token: AnyToken } = require('../src/index');
|
|
11
13
|
const result = callback();
|
|
14
|
+
if (result instanceof AnyToken && !result.getAttribute('built')) {
|
|
15
|
+
result.afterBuild();
|
|
16
|
+
}
|
|
12
17
|
this.running = running;
|
|
13
18
|
return result;
|
|
14
19
|
}
|
|
@@ -17,6 +22,7 @@ exports.Shadow = {
|
|
|
17
22
|
throw e;
|
|
18
23
|
}
|
|
19
24
|
},
|
|
25
|
+
rev: 0,
|
|
20
26
|
};
|
|
21
27
|
/**
|
|
22
28
|
* 是否是某一特定类型的节点
|
|
@@ -88,3 +94,4 @@ const undo = (e, data) => {
|
|
|
88
94
|
}
|
|
89
95
|
};
|
|
90
96
|
exports.undo = undo;
|
|
97
|
+
constants_1.constants['Shadow'] = __filename;
|
package/package.json
CHANGED
package/README.en.md
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
[](https://www.npmjs.com/package/wikiparser-node)
|
|
2
|
-
[](https://github.com/bhsd-harry/wikiparser-node/actions/workflows/codeql.yml)
|
|
3
|
-
[](https://github.com/bhsd-harry/wikiparser-node/actions/workflows/node.js.yml)
|
|
4
|
-
|
|
5
|
-
# Other Languages
|
|
6
|
-
|
|
7
|
-
- [简体中文](./README.md)
|
|
8
|
-
|
|
9
|
-
# Introduction
|
|
10
|
-
|
|
11
|
-
WikiParser-Node is an offline [Wikitext](https://www.mediawiki.org/wiki/Wikitext) parser developed by Bhsd for the [Node.js](https://nodejs.org/) environment. It can parse almost all wiki syntax and generate an [Abstract Syntax Tree (AST)](https://en.wikipedia.org/wiki/Abstract_syntax_tree) ([Try it online](https://bhsd-harry.github.io/wikiparser-node/#editor)). It also allows for easy querying and modification of the AST, and returns the modified wikitext.
|
|
12
|
-
|
|
13
|
-
# Other Versions
|
|
14
|
-
|
|
15
|
-
## Mini (also known as [WikiLint](https://www.npmjs.com/package/wikilint))
|
|
16
|
-
|
|
17
|
-
This version provides a [CLI](https://en.wikipedia.org/wiki/Command-line_interface), but only retains the parsing functionality and linting functionality. The parsed AST cannot be modified. It is used in the [eslint-plugin-wikitext](https://www.npmjs.com/package/eslint-plugin-wikitext) plugin.
|
|
18
|
-
|
|
19
|
-
## Browser-compatible
|
|
20
|
-
|
|
21
|
-
A browser-compatible version, which can be used for code highlighting or as a linting plugin in conjunction with editors such as [CodeMirror](https://codemirror.net/) and [Monaco](https://microsoft.github.io/monaco-editor/). ([Usage example](https://bhsd-harry.github.io/wikiparser-node))
|
|
22
|
-
|
|
23
|
-
# Installation
|
|
24
|
-
|
|
25
|
-
## Node.js
|
|
26
|
-
|
|
27
|
-
Please install the corresponding version as needed (`WikiParser-Node` or `WikiLint`), for example:
|
|
28
|
-
|
|
29
|
-
```sh
|
|
30
|
-
npm i wikiparser-node
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
or
|
|
34
|
-
|
|
35
|
-
```sh
|
|
36
|
-
npm i wikilint
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Browser
|
|
40
|
-
|
|
41
|
-
You can download the code via CDN, for example:
|
|
42
|
-
|
|
43
|
-
```html
|
|
44
|
-
<script src="//cdn.jsdelivr.net/npm/wikiparser-node@browser/bundle/bundle.min.js"></script>
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
or
|
|
48
|
-
|
|
49
|
-
```html
|
|
50
|
-
<script src="//unpkg.com/wikiparser-node@browser/bundle/bundle.min.js"></script>
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
For more browser extensions, please refer to the corresponding [documentation](https://github.com/bhsd-harry/wikiparser-node/wiki/Browser-%28EN%29).
|
|
54
|
-
|
|
55
|
-
# Usage
|
|
56
|
-
|
|
57
|
-
Please refer to the [Wiki](https://github.com/bhsd-harry/wikiparser-node/wiki/Home-%28EN%29).
|