wikiparser-node 0.9.2-b → 0.10.0-m
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/.schema.json +127 -0
- package/config/default.json +831 -0
- package/config/llwiki.json +595 -0
- package/config/moegirl.json +685 -0
- package/config/zhwiki.json +803 -0
- package/i18n/zh-hans.json +1 -0
- package/i18n/zh-hant.json +1 -0
- package/index.js +81 -0
- package/lib/element.js +136 -0
- package/lib/node.js +236 -0
- package/lib/text.js +168 -0
- package/lib/title.js +56 -0
- package/mixin/hidden.js +18 -0
- package/package.json +48 -47
- package/parser/brackets.js +125 -0
- package/parser/commentAndExt.js +58 -0
- package/parser/converter.js +45 -0
- package/parser/externalLinks.js +32 -0
- package/parser/hrAndDoubleUnderscore.js +48 -0
- package/parser/html.js +41 -0
- package/parser/links.js +93 -0
- package/parser/list.js +58 -0
- package/parser/magicLinks.js +40 -0
- package/parser/quotes.js +63 -0
- package/parser/table.js +113 -0
- package/src/arg.js +93 -0
- package/src/atom/hidden.js +11 -0
- package/src/atom/index.js +26 -0
- package/src/attribute.js +290 -0
- package/src/attributes.js +150 -0
- package/src/converter.js +70 -0
- package/src/converterFlags.js +97 -0
- package/src/converterRule.js +74 -0
- package/src/extLink.js +60 -0
- package/src/gallery.js +95 -0
- package/src/hasNowiki/index.js +32 -0
- package/src/hasNowiki/pre.js +28 -0
- package/src/heading.js +83 -0
- package/src/html.js +139 -0
- package/src/imageParameter.js +141 -0
- package/src/imagemap.js +140 -0
- package/src/imagemapLink.js +29 -0
- package/src/index.js +406 -0
- package/src/link/category.js +13 -0
- package/src/link/file.js +132 -0
- package/src/link/galleryImage.js +62 -0
- package/src/link/index.js +119 -0
- package/src/magicLink.js +66 -0
- package/src/nested/choose.js +23 -0
- package/src/nested/combobox.js +22 -0
- package/src/nested/index.js +66 -0
- package/src/nested/references.js +22 -0
- package/src/nowiki/comment.js +47 -0
- package/src/nowiki/dd.js +13 -0
- package/src/nowiki/doubleUnderscore.js +26 -0
- package/src/nowiki/hr.js +22 -0
- package/src/nowiki/index.js +34 -0
- package/src/nowiki/list.js +13 -0
- package/src/nowiki/noinclude.js +14 -0
- package/src/nowiki/quote.js +55 -0
- package/src/onlyinclude.js +39 -0
- package/src/paramTag/index.js +66 -0
- package/src/paramTag/inputbox.js +32 -0
- package/src/parameter.js +97 -0
- package/src/syntax.js +23 -0
- package/src/table/index.js +46 -0
- package/src/table/td.js +118 -0
- package/src/table/tr.js +74 -0
- package/src/tagPair/ext.js +125 -0
- package/src/tagPair/include.js +26 -0
- package/src/tagPair/index.js +77 -0
- package/src/transclude.js +336 -0
- package/util/base.js +17 -0
- package/util/diff.js +76 -0
- package/util/lint.js +55 -0
- package/util/string.js +75 -0
- package/bundle/bundle.min.js +0 -38
- package/extensions/editor.css +0 -62
- package/extensions/editor.js +0 -328
- package/extensions/ui.css +0 -119
package/i18n/zh-hans.json
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"nothing should be in <$1>": "<$1>标签内不应有任何内容",
|
|
31
31
|
"section header in a HTML tag": "HTML标签属性中的段落标题",
|
|
32
32
|
"tag that is both closing and self-closing": "同时闭合和自封闭的标签",
|
|
33
|
+
"template in an internal link target": "内链目标包含模板",
|
|
33
34
|
"unbalanced \"=\" in a section header": "段落标题中不平衡的\"=\"",
|
|
34
35
|
"unclosed HTML comment": "未闭合的HTML注释",
|
|
35
36
|
"unclosed quotes": "未闭合的引号",
|
package/i18n/zh-hant.json
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"nothing should be in <$1>": "<$1>標籤內不應有任何內容",
|
|
31
31
|
"section header in a HTML tag": "HTML標籤屬性中的段落標題",
|
|
32
32
|
"tag that is both closing and self-closing": "同時閉合和自封閉的標籤",
|
|
33
|
+
"template in an internal link target": "內部連結目標包含模板",
|
|
33
34
|
"unbalanced \"=\" in a section header": "段落標題中不平衡的\"=\"",
|
|
34
35
|
"unclosed HTML comment": "未閉合的HTML註釋",
|
|
35
36
|
"unclosed quotes": "未閉合的引號",
|
package/index.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const /** @type {Parser} */ Parser = {
|
|
4
|
+
config: './config/default',
|
|
5
|
+
i18n: undefined,
|
|
6
|
+
|
|
7
|
+
MAX_STAGE: 11,
|
|
8
|
+
|
|
9
|
+
getConfig() {
|
|
10
|
+
if (typeof this.config === 'string') {
|
|
11
|
+
this.config = require(this.config);
|
|
12
|
+
}
|
|
13
|
+
return {...this.config, excludes: []};
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
msg(msg, arg) {
|
|
17
|
+
if (typeof this.i18n === 'string') {
|
|
18
|
+
this.i18n = require(this.i18n);
|
|
19
|
+
}
|
|
20
|
+
msg = this.i18n?.[msg] ?? msg;
|
|
21
|
+
return msg.replace('$1', arg);
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
normalizeTitle(
|
|
25
|
+
title,
|
|
26
|
+
defaultNs = 0,
|
|
27
|
+
include = false,
|
|
28
|
+
config = Parser.getConfig(),
|
|
29
|
+
halfParsed = false,
|
|
30
|
+
decode = false,
|
|
31
|
+
selfLink = false,
|
|
32
|
+
) {
|
|
33
|
+
let /** @type {Token} */ token;
|
|
34
|
+
if (!halfParsed) {
|
|
35
|
+
const Token = require('./src');
|
|
36
|
+
token = this.run(() => {
|
|
37
|
+
const newToken = new Token(String(title), config),
|
|
38
|
+
parseOnce = newToken.getAttribute('parseOnce');
|
|
39
|
+
parseOnce(0, include);
|
|
40
|
+
return parseOnce();
|
|
41
|
+
});
|
|
42
|
+
title = token.firstChild;
|
|
43
|
+
}
|
|
44
|
+
const Title = require('./lib/title');
|
|
45
|
+
const titleObj = new Title(String(title), defaultNs, config, decode, selfLink);
|
|
46
|
+
return titleObj;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
parse(wikitext, include, maxStage = Parser.MAX_STAGE, config = Parser.getConfig()) {
|
|
50
|
+
if (typeof wikitext !== 'string') {
|
|
51
|
+
throw new TypeError('待解析的内容应为 String!');
|
|
52
|
+
}
|
|
53
|
+
const Token = require('./src');
|
|
54
|
+
let /** @type {Token} */ token;
|
|
55
|
+
this.run(() => {
|
|
56
|
+
token = new Token(wikitext, config);
|
|
57
|
+
try {
|
|
58
|
+
token.parse(maxStage, include);
|
|
59
|
+
} catch {}
|
|
60
|
+
});
|
|
61
|
+
return token;
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
run(callback) {
|
|
65
|
+
return callback();
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const /** @type {PropertyDescriptorMap} */ def = {},
|
|
70
|
+
immutable = new Set(['MAX_STAGE']),
|
|
71
|
+
enumerable = new Set(['config', 'normalizeTitle', 'parse']);
|
|
72
|
+
for (const key in Parser) {
|
|
73
|
+
if (immutable.has(key)) {
|
|
74
|
+
def[key] = {enumerable: false, writable: false};
|
|
75
|
+
} else if (!enumerable.has(key)) {
|
|
76
|
+
def[key] = {enumerable: false};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
Object.defineProperties(Parser, def);
|
|
80
|
+
|
|
81
|
+
module.exports = Parser;
|
package/lib/element.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs'),
|
|
4
|
+
path = require('path'),
|
|
5
|
+
AstNode = require('./node'),
|
|
6
|
+
AstText = require('./text');
|
|
7
|
+
|
|
8
|
+
const lintIgnoredExt = new Set([
|
|
9
|
+
'nowiki',
|
|
10
|
+
'pre',
|
|
11
|
+
'charinsert',
|
|
12
|
+
'score',
|
|
13
|
+
'syntaxhighlight',
|
|
14
|
+
'source',
|
|
15
|
+
'math',
|
|
16
|
+
'chem',
|
|
17
|
+
'ce',
|
|
18
|
+
'graph',
|
|
19
|
+
'mapframe',
|
|
20
|
+
'maplink',
|
|
21
|
+
'quiz',
|
|
22
|
+
'templatedata',
|
|
23
|
+
'timeline',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/** 类似HTMLElement */
|
|
27
|
+
class AstElement extends AstNode {
|
|
28
|
+
/** @type {string} */ name;
|
|
29
|
+
|
|
30
|
+
/** 子节点总数 */
|
|
31
|
+
get length() {
|
|
32
|
+
return this.childNodes.length;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 最近的祖先节点
|
|
37
|
+
* @param {string} selector
|
|
38
|
+
*/
|
|
39
|
+
closest(selector) {
|
|
40
|
+
const types = new Set(selector.split(',').map(type => type.trim()));
|
|
41
|
+
let {parentNode} = this;
|
|
42
|
+
while (parentNode) {
|
|
43
|
+
if (types.has(parentNode.type)) {
|
|
44
|
+
return parentNode;
|
|
45
|
+
}
|
|
46
|
+
({parentNode} = parentNode);
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 在末尾批量插入子节点
|
|
53
|
+
* @param {...this} elements 插入节点
|
|
54
|
+
* @complexity `n`
|
|
55
|
+
*/
|
|
56
|
+
append(...elements) {
|
|
57
|
+
for (const element of elements) {
|
|
58
|
+
this.insertAt(element);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 批量替换子节点
|
|
64
|
+
* @param {...this} elements 新的子节点
|
|
65
|
+
* @complexity `n`
|
|
66
|
+
*/
|
|
67
|
+
replaceChildren(...elements) {
|
|
68
|
+
this.childNodes.length = 0;
|
|
69
|
+
this.append(...elements);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 修改文本子节点
|
|
74
|
+
* @param {string} str 新文本
|
|
75
|
+
* @param {number} i 子节点位置
|
|
76
|
+
*/
|
|
77
|
+
setText(str, i = 0) {
|
|
78
|
+
const /** @type {AstText} */ oldText = this.childNodes.at(i),
|
|
79
|
+
{type, data} = oldText;
|
|
80
|
+
if (type === 'text') {
|
|
81
|
+
oldText.replaceData(str);
|
|
82
|
+
return data;
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 还原为wikitext
|
|
89
|
+
* @param {string} separator 子节点间的连接符
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
toString(selector, separator = '') {
|
|
93
|
+
return this.childNodes.map(child => child.toString()).join(separator);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Linter
|
|
98
|
+
* @param {number} start 起始位置
|
|
99
|
+
*/
|
|
100
|
+
lint(start = this.getAbsoluteIndex()) {
|
|
101
|
+
const SyntaxToken = require('../src/syntax');
|
|
102
|
+
if (this instanceof SyntaxToken || this.constructor.hidden
|
|
103
|
+
|| this.type === 'ext-inner' && lintIgnoredExt.has(this.name)
|
|
104
|
+
) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
const /** @type {LintError[]} */ errors = [];
|
|
108
|
+
for (let i = 0, cur = start + this.getPadding(); i < this.length; i++) {
|
|
109
|
+
const child = this.childNodes[i];
|
|
110
|
+
errors.push(...child.lint(cur));
|
|
111
|
+
cur += String(child).length + this.getGaps(i);
|
|
112
|
+
}
|
|
113
|
+
return errors;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 保存为JSON
|
|
118
|
+
* @param {string} file 文件名
|
|
119
|
+
* @returns {Record<string, *>}
|
|
120
|
+
*/
|
|
121
|
+
json(file) {
|
|
122
|
+
const json = {
|
|
123
|
+
...this,
|
|
124
|
+
childNodes: this.childNodes.map(child => child.type === 'text' ? String(child) : child.json()),
|
|
125
|
+
};
|
|
126
|
+
if (typeof file === 'string') {
|
|
127
|
+
fs.writeFileSync(
|
|
128
|
+
path.join(__dirname.slice(0, -4), 'printed', `${file}${file.endsWith('.json') ? '' : '.json'}`),
|
|
129
|
+
JSON.stringify(json, null, 2),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return json;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = AstElement;
|
package/lib/node.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {text} = require('../util/string');
|
|
4
|
+
|
|
5
|
+
/** 类似Node */
|
|
6
|
+
class AstNode {
|
|
7
|
+
/** @type {string} */ type;
|
|
8
|
+
/** @type {this[]} */ childNodes = [];
|
|
9
|
+
/** @type {this} */ #parentNode;
|
|
10
|
+
|
|
11
|
+
/** 首位子节点 */
|
|
12
|
+
get firstChild() {
|
|
13
|
+
return this.childNodes[0];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 末位子节点 */
|
|
17
|
+
get lastChild() {
|
|
18
|
+
return this.childNodes.at(-1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 父节点 */
|
|
22
|
+
get parentNode() {
|
|
23
|
+
return this.#parentNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 后一个兄弟节点
|
|
28
|
+
* @complexity `n`
|
|
29
|
+
*/
|
|
30
|
+
get nextSibling() {
|
|
31
|
+
const childNodes = this.#parentNode?.childNodes;
|
|
32
|
+
return childNodes && childNodes[childNodes.indexOf(this) + 1];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 前一个兄弟节点
|
|
37
|
+
* @complexity `n`
|
|
38
|
+
*/
|
|
39
|
+
get previousSibling() {
|
|
40
|
+
const childNodes = this.#parentNode?.childNodes;
|
|
41
|
+
return childNodes && childNodes[childNodes.indexOf(this) - 1];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 是否具有某属性
|
|
46
|
+
* @param {PropertyKey} key 属性键
|
|
47
|
+
*/
|
|
48
|
+
hasAttribute(key) {
|
|
49
|
+
const type = typeof key;
|
|
50
|
+
return type === 'string' || type === 'number' || type === 'symbol'
|
|
51
|
+
? key in this
|
|
52
|
+
: false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 获取属性值。除非用于私有属性,否则总是返回字符串。
|
|
57
|
+
* @template {string} T
|
|
58
|
+
* @param {T} key 属性键
|
|
59
|
+
* @returns {TokenAttribute<T>}
|
|
60
|
+
*/
|
|
61
|
+
getAttribute(key) {
|
|
62
|
+
return this.hasAttribute(key) ? String(this[key]) : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 设置属性
|
|
67
|
+
* @template {string} T
|
|
68
|
+
* @param {T} key 属性键
|
|
69
|
+
* @param {TokenAttribute<T>} value 属性值
|
|
70
|
+
*/
|
|
71
|
+
setAttribute(key, value) {
|
|
72
|
+
if (key === 'parentNode') {
|
|
73
|
+
this.#parentNode = value;
|
|
74
|
+
} else {
|
|
75
|
+
this[key] = value;
|
|
76
|
+
}
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 可见部分
|
|
82
|
+
* @param {string} separator 子节点间的连接符
|
|
83
|
+
* @returns {string}
|
|
84
|
+
* @complexity `n`
|
|
85
|
+
*/
|
|
86
|
+
text(separator = '') {
|
|
87
|
+
return text(this.childNodes, separator);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 移除子节点
|
|
92
|
+
* @param {number} i 移除位置
|
|
93
|
+
*/
|
|
94
|
+
removeAt(i) {
|
|
95
|
+
const {childNodes} = this,
|
|
96
|
+
[node] = childNodes.splice(i, 1);
|
|
97
|
+
return node;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 插入子节点
|
|
102
|
+
* @template {this} T
|
|
103
|
+
* @param {T} node 待插入的子节点
|
|
104
|
+
* @param {number} i 插入位置
|
|
105
|
+
* @complexity `n`
|
|
106
|
+
*/
|
|
107
|
+
insertAt(node, i = this.childNodes.length) {
|
|
108
|
+
const {childNodes} = this,
|
|
109
|
+
j = -1;
|
|
110
|
+
if (j === -1) {
|
|
111
|
+
node.setAttribute('parentNode', this);
|
|
112
|
+
}
|
|
113
|
+
childNodes.splice(i, 0, node);
|
|
114
|
+
return node;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 合并相邻的文本子节点
|
|
119
|
+
* @complexity `n`
|
|
120
|
+
*/
|
|
121
|
+
normalize() {
|
|
122
|
+
const AstText = require('./text');
|
|
123
|
+
const /** @type {{childNodes: AstText[]}} */ {childNodes} = this;
|
|
124
|
+
for (let i = childNodes.length - 1; i >= 0; i--) {
|
|
125
|
+
const {type, data} = childNodes[i];
|
|
126
|
+
if (this.getGaps(i - 1)) {
|
|
127
|
+
//
|
|
128
|
+
} else if (data === '') {
|
|
129
|
+
childNodes.splice(i, 1);
|
|
130
|
+
} else if (type === 'text' && childNodes[i - 1]?.type === 'text') {
|
|
131
|
+
childNodes[i - 1].setAttribute('data', childNodes[i - 1].data + data);
|
|
132
|
+
childNodes.splice(i, 1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 获取根节点 */
|
|
138
|
+
getRootNode() {
|
|
139
|
+
let {parentNode} = this;
|
|
140
|
+
while (parentNode?.parentNode) {
|
|
141
|
+
({parentNode} = parentNode);
|
|
142
|
+
}
|
|
143
|
+
return parentNode ?? this;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 将字符位置转换为行列号
|
|
148
|
+
* @param {number} index 字符位置
|
|
149
|
+
* @complexity `n`
|
|
150
|
+
*/
|
|
151
|
+
posFromIndex(index) {
|
|
152
|
+
const str = String(this);
|
|
153
|
+
if (index >= -str.length && index <= str.length) {
|
|
154
|
+
const lines = str.slice(0, index).split('\n');
|
|
155
|
+
return {top: lines.length - 1, left: lines.at(-1).length};
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 获取行数和最后一行的列数
|
|
162
|
+
* @complexity `n`
|
|
163
|
+
*/
|
|
164
|
+
#getDimension() {
|
|
165
|
+
const lines = String(this).split('\n');
|
|
166
|
+
return {height: lines.length, width: lines.at(-1).length};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 第一个子节点前的间距 */
|
|
170
|
+
getPadding() {
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 子节点间距 */
|
|
175
|
+
getGaps() {
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 获取当前节点的相对字符位置,或其第`j`个子节点的相对字符位置
|
|
181
|
+
* @param {number|undefined} j 子节点序号
|
|
182
|
+
* @complexity `n`
|
|
183
|
+
*/
|
|
184
|
+
getRelativeIndex(j) {
|
|
185
|
+
let /** @type {this[]} */ childNodes;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 获取子节点相对于父节点的字符位置,使用前需要先给`childNodes`赋值
|
|
189
|
+
* @param {number} end 子节点序号
|
|
190
|
+
* @param {this} parent 父节点
|
|
191
|
+
* @returns {number}
|
|
192
|
+
*/
|
|
193
|
+
const getIndex = (end, parent) => childNodes.slice(0, end).reduce(
|
|
194
|
+
(acc, cur, i) => acc + String(cur).length + parent.getGaps(i),
|
|
195
|
+
0,
|
|
196
|
+
) + parent.getPadding();
|
|
197
|
+
if (j === undefined) {
|
|
198
|
+
const {parentNode} = this;
|
|
199
|
+
if (parentNode) {
|
|
200
|
+
({childNodes} = parentNode);
|
|
201
|
+
return getIndex(childNodes.indexOf(this), parentNode);
|
|
202
|
+
}
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
({childNodes} = this);
|
|
206
|
+
return getIndex(j, this);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 获取当前节点的绝对位置
|
|
211
|
+
* @returns {number}
|
|
212
|
+
* @complexity `n`
|
|
213
|
+
*/
|
|
214
|
+
getAbsoluteIndex() {
|
|
215
|
+
const {parentNode} = this;
|
|
216
|
+
return parentNode ? parentNode.getAbsoluteIndex() + this.getRelativeIndex() : 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 行数
|
|
221
|
+
* @complexity `n`
|
|
222
|
+
*/
|
|
223
|
+
get offsetHeight() {
|
|
224
|
+
return this.#getDimension().height;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 最后一行的列数
|
|
229
|
+
* @complexity `n`
|
|
230
|
+
*/
|
|
231
|
+
get offsetWidth() {
|
|
232
|
+
return this.#getDimension().width;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = AstNode;
|
package/lib/text.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Parser = require('..'),
|
|
4
|
+
AstNode = require('./node'),
|
|
5
|
+
AstElement = require('./element');
|
|
6
|
+
|
|
7
|
+
const errorSyntax = /https?:\/\/|\{+|\}+|\[{2,}|\[(?![^[]*\])|(?<=^|\])([^[]*?)\]+|\]{2,}|<\s*\/?([a-z]\w*)/giu,
|
|
8
|
+
errorSyntaxUrl = /\{+|\}+|\[{2,}|\[(?![^[]*\])|(?<=^|\])([^[]*?)\]+|\]{2,}|<\s*\/?([a-z]\w*)/giu,
|
|
9
|
+
disallowedTags = [
|
|
10
|
+
'html',
|
|
11
|
+
'base',
|
|
12
|
+
'head',
|
|
13
|
+
'style',
|
|
14
|
+
'title',
|
|
15
|
+
'body',
|
|
16
|
+
'menu',
|
|
17
|
+
'a',
|
|
18
|
+
'area',
|
|
19
|
+
'audio',
|
|
20
|
+
'img',
|
|
21
|
+
'map',
|
|
22
|
+
'track',
|
|
23
|
+
'video',
|
|
24
|
+
'embed',
|
|
25
|
+
'iframe',
|
|
26
|
+
'object',
|
|
27
|
+
'picture',
|
|
28
|
+
'source',
|
|
29
|
+
'canvas',
|
|
30
|
+
'script',
|
|
31
|
+
'col',
|
|
32
|
+
'colgroup',
|
|
33
|
+
'tbody',
|
|
34
|
+
'tfoot',
|
|
35
|
+
'thead',
|
|
36
|
+
'button',
|
|
37
|
+
'datalist',
|
|
38
|
+
'fieldset',
|
|
39
|
+
'form',
|
|
40
|
+
'input',
|
|
41
|
+
'label',
|
|
42
|
+
'legend',
|
|
43
|
+
'meter',
|
|
44
|
+
'optgroup',
|
|
45
|
+
'option',
|
|
46
|
+
'output',
|
|
47
|
+
'progress',
|
|
48
|
+
'select',
|
|
49
|
+
'textarea',
|
|
50
|
+
'details',
|
|
51
|
+
'dialog',
|
|
52
|
+
'slot',
|
|
53
|
+
'template',
|
|
54
|
+
'dir',
|
|
55
|
+
'frame',
|
|
56
|
+
'frameset',
|
|
57
|
+
'marquee',
|
|
58
|
+
'param',
|
|
59
|
+
'xmp',
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
/** 文本节点 */
|
|
63
|
+
class AstText extends AstNode {
|
|
64
|
+
type = 'text';
|
|
65
|
+
/** @type {string} */ data;
|
|
66
|
+
|
|
67
|
+
/** @param {string} text 包含文本 */
|
|
68
|
+
constructor(text = '') {
|
|
69
|
+
super();
|
|
70
|
+
this.data = text;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 输出字符串 */
|
|
74
|
+
toString() {
|
|
75
|
+
return this.data;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** @override */
|
|
79
|
+
text() {
|
|
80
|
+
return this.data;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Linter
|
|
85
|
+
* @this {AstText & {parentNode: AstElement}}
|
|
86
|
+
* @param {number} start 起始位置
|
|
87
|
+
* @returns {LintError[]}
|
|
88
|
+
*/
|
|
89
|
+
lint(start = this.getAbsoluteIndex()) {
|
|
90
|
+
const {data, parentNode, nextSibling, previousSibling} = this,
|
|
91
|
+
type = parentNode?.type,
|
|
92
|
+
name = parentNode?.name,
|
|
93
|
+
nextType = nextSibling?.type,
|
|
94
|
+
previousType = previousSibling?.type,
|
|
95
|
+
errorRegex
|
|
96
|
+
= type === 'free-ext-link' || type === 'ext-link-url' || type === 'image-parameter' && name === 'link'
|
|
97
|
+
? errorSyntaxUrl
|
|
98
|
+
: errorSyntax,
|
|
99
|
+
errors = [...data.matchAll(errorRegex)],
|
|
100
|
+
{ext, html} = this.getRootNode().getAttribute('config');
|
|
101
|
+
if (errors.length > 0) {
|
|
102
|
+
const root = this.getRootNode(),
|
|
103
|
+
{top, left} = root.posFromIndex(start),
|
|
104
|
+
tags = new Set([ext, html, disallowedTags].flat(2));
|
|
105
|
+
return errors.map(/** @returns {LintError} */ ({0: error, 1: prefix, 2: tag, index}) => {
|
|
106
|
+
if (prefix) {
|
|
107
|
+
index += prefix.length;
|
|
108
|
+
error = error.slice(prefix.length);
|
|
109
|
+
}
|
|
110
|
+
const startIndex = start + index,
|
|
111
|
+
lines = data.slice(0, index).split('\n'),
|
|
112
|
+
startLine = lines.length + top - 1,
|
|
113
|
+
line = lines.at(-1),
|
|
114
|
+
startCol = lines.length > 1 ? line.length : left + line.length,
|
|
115
|
+
{0: char, length} = error,
|
|
116
|
+
endIndex = startIndex + length,
|
|
117
|
+
end = char === '}' || char === ']' ? endIndex + 1 : startIndex + 49,
|
|
118
|
+
rootStr = String(root),
|
|
119
|
+
nextChar = rootStr[endIndex],
|
|
120
|
+
previousChar = rootStr[startIndex - 1],
|
|
121
|
+
severity = length > 1 && (char !== '<' || /[\s/>]/u.test(nextChar))
|
|
122
|
+
|| char === '{' && (nextChar === char || previousChar === '-')
|
|
123
|
+
|| char === '}' && (previousChar === char || nextChar === '-')
|
|
124
|
+
|| char === '[' && (
|
|
125
|
+
nextChar === char || type === 'ext-link-text'
|
|
126
|
+
|| !data.slice(index + 1).trim() && nextType === 'free-ext-link'
|
|
127
|
+
)
|
|
128
|
+
|| char === ']' && (
|
|
129
|
+
previousChar === char
|
|
130
|
+
|| !data.slice(0, index).trim() && previousType === 'free-ext-link'
|
|
131
|
+
)
|
|
132
|
+
? 'error'
|
|
133
|
+
: 'warning';
|
|
134
|
+
return (char !== 'h' || index > 0) && (char !== '<' || tags.has(tag.toLowerCase())) && {
|
|
135
|
+
message: Parser.msg('lonely "$1"', char === 'h' ? error : char),
|
|
136
|
+
severity,
|
|
137
|
+
startIndex,
|
|
138
|
+
endIndex,
|
|
139
|
+
startLine,
|
|
140
|
+
endLine: startLine,
|
|
141
|
+
startCol,
|
|
142
|
+
endCol: startCol + length,
|
|
143
|
+
excerpt: rootStr.slice(Math.max(0, end - 50), end),
|
|
144
|
+
};
|
|
145
|
+
}).filter(Boolean);
|
|
146
|
+
}
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 修改内容
|
|
152
|
+
* @param {string} text 新内容
|
|
153
|
+
*/
|
|
154
|
+
#setData(text) {
|
|
155
|
+
text = String(text);
|
|
156
|
+
this.setAttribute('data', text);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 替换字符串
|
|
161
|
+
* @param {string} text 替换的字符串
|
|
162
|
+
*/
|
|
163
|
+
replaceData(text = '') {
|
|
164
|
+
this.#setData(text);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = AstText;
|
package/lib/title.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {decodeHtml} = require('../util/string'),
|
|
4
|
+
Parser = require('..');
|
|
5
|
+
|
|
6
|
+
/** MediaWiki页面标题对象 */
|
|
7
|
+
class Title {
|
|
8
|
+
valid = true;
|
|
9
|
+
ns = 0;
|
|
10
|
+
fragment;
|
|
11
|
+
encoded = false;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} title 标题(含或不含命名空间前缀)
|
|
15
|
+
* @param {number} defaultNs 命名空间
|
|
16
|
+
* @param {boolean} decode 是否需要解码
|
|
17
|
+
* @param {boolean} selfLink 是否允许selfLink
|
|
18
|
+
*/
|
|
19
|
+
constructor(title, defaultNs = 0, config = Parser.getConfig(), decode = false, selfLink = false) {
|
|
20
|
+
const {namespaces, nsid} = config;
|
|
21
|
+
let namespace = namespaces[defaultNs];
|
|
22
|
+
title = decodeHtml(title);
|
|
23
|
+
if (decode && title.includes('%')) {
|
|
24
|
+
try {
|
|
25
|
+
const encoded = /%(?!21|3[ce]|5[bd]|7[b-d])[\da-f]{2}/iu.test(title);
|
|
26
|
+
title = decodeURIComponent(title);
|
|
27
|
+
this.encoded = encoded;
|
|
28
|
+
} catch {}
|
|
29
|
+
}
|
|
30
|
+
title = title.replaceAll('_', ' ').trim();
|
|
31
|
+
if (title[0] === ':') {
|
|
32
|
+
namespace = '';
|
|
33
|
+
title = title.slice(1).trim();
|
|
34
|
+
}
|
|
35
|
+
const m = title.split(':');
|
|
36
|
+
if (m.length > 1) {
|
|
37
|
+
const id = namespaces[nsid[m[0].trim().toLowerCase()]];
|
|
38
|
+
if (id !== undefined) {
|
|
39
|
+
namespace = id;
|
|
40
|
+
title = m.slice(1).join(':').trim();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
this.ns = nsid[namespace.toLowerCase()];
|
|
44
|
+
const i = title.indexOf('#');
|
|
45
|
+
let fragment;
|
|
46
|
+
if (i !== -1) {
|
|
47
|
+
fragment = title.slice(i + 1).trimEnd();
|
|
48
|
+
title = title.slice(0, i).trim();
|
|
49
|
+
}
|
|
50
|
+
this.valid = Boolean(title || selfLink && fragment !== undefined)
|
|
51
|
+
&& !/\0\d+[eh!+-]\x7F|[<>[\]{}|]|%[\da-f]{2}/iu.test(title);
|
|
52
|
+
this.fragment = fragment;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = Title;
|
package/mixin/hidden.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 解析后不可见的类
|
|
5
|
+
* @template T
|
|
6
|
+
* @param {T} Constructor 基类
|
|
7
|
+
* @returns {T}
|
|
8
|
+
*/
|
|
9
|
+
const hidden = Constructor => class extends Constructor {
|
|
10
|
+
static hidden = true;
|
|
11
|
+
|
|
12
|
+
/** 没有可见部分 */
|
|
13
|
+
text() { // eslint-disable-line class-methods-use-this
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
module.exports = hidden;
|