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/link/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const Title = require('../../lib/title'),
|
|
4
|
-
{
|
|
3
|
+
const Title = require('../../lib/title'),
|
|
4
|
+
{noWrap} = require('../../util/string'),
|
|
5
5
|
{undo} = require('../../util/debug'),
|
|
6
|
-
|
|
6
|
+
Parser = require('../..'),
|
|
7
|
+
AstText = require('../../lib/text'),
|
|
7
8
|
Token = require('..');
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -12,14 +13,67 @@ const Title = require('../../lib/title'), // eslint-disable-line no-unused-vars
|
|
|
12
13
|
*/
|
|
13
14
|
class LinkToken extends Token {
|
|
14
15
|
type = 'link';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
|
|
17
|
+
/** 完整链接,和FileToken保持一致 */
|
|
18
|
+
get link() {
|
|
19
|
+
return String(this.#getTitle());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
set link(link) {
|
|
23
|
+
this.setTarget(link);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 是否链接到自身 */
|
|
27
|
+
get selfLink() {
|
|
28
|
+
return !this.#getTitle().title;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
set selfLink(selfLink) {
|
|
32
|
+
if (selfLink === true) {
|
|
33
|
+
this.asSelfLink();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** fragment */
|
|
38
|
+
get fragment() {
|
|
39
|
+
return this.#getTitle().fragment;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
set fragment(fragment) {
|
|
43
|
+
this.setFragment(fragment);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** interwiki */
|
|
47
|
+
get interwiki() {
|
|
48
|
+
return this.#getTitle().interwiki;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
set interwiki(interwiki) {
|
|
52
|
+
if (typeof interwiki !== 'string') {
|
|
53
|
+
this.typeError('set interwiki', 'String');
|
|
54
|
+
}
|
|
55
|
+
const {prefix, main, fragment} = this.#getTitle(),
|
|
56
|
+
link = `${interwiki}:${prefix}${main}${fragment && '#'}${fragment}`;
|
|
57
|
+
if (interwiki && !this.isInterwiki(link)) {
|
|
58
|
+
throw new RangeError(`${interwiki} 不是合法的跨维基前缀!`);
|
|
59
|
+
}
|
|
60
|
+
this.setTarget(link);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 链接显示文字 */
|
|
64
|
+
get innerText() {
|
|
65
|
+
if (this.type === 'link') {
|
|
66
|
+
return this.childNodes.length > 1
|
|
67
|
+
? this.lastChild.text()
|
|
68
|
+
: this.firstChild.text().replace(/^\s*:/u, '');
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
18
72
|
|
|
19
73
|
/**
|
|
20
|
-
* @param {string} link
|
|
21
|
-
* @param {string|undefined} linkText
|
|
22
|
-
* @param {Title} title
|
|
74
|
+
* @param {string} link 链接标题
|
|
75
|
+
* @param {string|undefined} linkText 链接显示文字
|
|
76
|
+
* @param {Title} title 链接标题对象
|
|
23
77
|
* @param {accum} accum
|
|
24
78
|
*/
|
|
25
79
|
constructor(link, linkText, title, config = Parser.getConfig(), accum = []) {
|
|
@@ -33,149 +87,175 @@ class LinkToken extends Token {
|
|
|
33
87
|
inner.type = 'link-text';
|
|
34
88
|
this.appendChild(inner.setAttribute('stage', Parser.MAX_STAGE - 1));
|
|
35
89
|
}
|
|
36
|
-
this.
|
|
37
|
-
this.fragment = title.fragment;
|
|
38
|
-
this.interwiki = title.interwiki;
|
|
39
|
-
this.setAttribute('name', title.title).seal(['selfLink', 'fragment', 'interwiki']).protectChildren(0);
|
|
90
|
+
this.setAttribute('name', title.title).getAttribute('protectChildren')(0);
|
|
40
91
|
}
|
|
41
92
|
|
|
93
|
+
/** 生成Title对象 */
|
|
94
|
+
#getTitle() {
|
|
95
|
+
return this.normalizeTitle(this.firstChild.text());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** @override */
|
|
42
99
|
cloneNode() {
|
|
43
|
-
const [link, ...linkText] = this.
|
|
100
|
+
const [link, ...linkText] = this.cloneChildNodes();
|
|
44
101
|
return Parser.run(() => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
token.firstElementChild.safeReplaceWith(link);
|
|
102
|
+
/** @type {this & {constructor: typeof LinkToken}} */
|
|
103
|
+
const {constructor} = this,
|
|
104
|
+
token = new constructor('', undefined, this.#getTitle(), this.getAttribute('config'));
|
|
105
|
+
token.firstChild.safeReplaceWith(link);
|
|
50
106
|
token.append(...linkText);
|
|
51
107
|
return token.afterBuild();
|
|
52
108
|
});
|
|
53
109
|
}
|
|
54
110
|
|
|
111
|
+
/**
|
|
112
|
+
* @override
|
|
113
|
+
* @throws `Error` 非法的内链目标
|
|
114
|
+
* @throws `Error` 不可更改命名空间
|
|
115
|
+
*/
|
|
55
116
|
afterBuild() {
|
|
56
|
-
if (this.name.includes('\0')) {
|
|
57
|
-
this.setAttribute('name', text(this.buildFromStr(this.name)));
|
|
58
|
-
}
|
|
59
|
-
if (this.fragment.includes('\0')) {
|
|
60
|
-
this.setAttribute('fragment', text(this.buildFromStr(this.fragment)));
|
|
61
|
-
}
|
|
62
|
-
const that = this;
|
|
63
117
|
const /** @type {AstListener} */ linkListener = (e, data) => {
|
|
64
118
|
const {prevTarget} = e;
|
|
65
119
|
if (prevTarget?.type === 'link-target') {
|
|
66
120
|
const name = prevTarget.text(),
|
|
67
|
-
{title, interwiki,
|
|
121
|
+
{title, interwiki, ns, valid} = this.normalizeTitle(name);
|
|
68
122
|
if (!valid) {
|
|
69
123
|
undo(e, data);
|
|
70
124
|
throw new Error(`非法的内链目标:${name}`);
|
|
71
|
-
} else if (
|
|
72
|
-
||
|
|
125
|
+
} else if (this.type === 'category' && (interwiki || ns !== 14)
|
|
126
|
+
|| this.type === 'file' && (interwiki || ns !== 6)
|
|
73
127
|
) {
|
|
74
128
|
undo(e, data);
|
|
75
|
-
throw new Error(`${
|
|
76
|
-
} else if (
|
|
77
|
-
const {firstChild} = prevTarget;
|
|
78
|
-
if (
|
|
79
|
-
|
|
129
|
+
throw new Error(`${this.type === 'file' ? '文件' : '分类'}链接不可更改命名空间:${name}`);
|
|
130
|
+
} else if (this.type === 'link' && !interwiki && (ns === 6 || ns === 14) && name.trim()[0] !== ':') {
|
|
131
|
+
const /** @type {{firstChild: AstText}} */ {firstChild} = prevTarget;
|
|
132
|
+
if (firstChild.type === 'text') {
|
|
133
|
+
firstChild.insertData(0, ':');
|
|
80
134
|
} else {
|
|
81
135
|
prevTarget.prepend(':');
|
|
82
136
|
}
|
|
83
137
|
}
|
|
84
|
-
|
|
85
|
-
.setAttribute('name', title).setAttribute('fragment', fragment);
|
|
138
|
+
this.setAttribute('name', title);
|
|
86
139
|
}
|
|
87
140
|
};
|
|
88
141
|
this.addEventListener(['remove', 'insert', 'replace', 'text'], linkListener);
|
|
89
142
|
return this;
|
|
90
143
|
}
|
|
91
144
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
145
|
+
/**
|
|
146
|
+
* @override
|
|
147
|
+
* @param {string} selector
|
|
148
|
+
*/
|
|
149
|
+
toString(selector) {
|
|
150
|
+
const str = super.toString(selector, '|');
|
|
151
|
+
return this.type === 'gallery-image' || selector && this.matches(selector) ? str : `[[${str}]]`;
|
|
95
152
|
}
|
|
96
153
|
|
|
154
|
+
/** @override */
|
|
97
155
|
getPadding() {
|
|
98
156
|
return 2;
|
|
99
157
|
}
|
|
100
158
|
|
|
159
|
+
/** @override */
|
|
101
160
|
getGaps() {
|
|
102
161
|
return 1;
|
|
103
162
|
}
|
|
104
163
|
|
|
164
|
+
/** @override */
|
|
165
|
+
print() {
|
|
166
|
+
return super.print(this.type === 'gallery-image' ? {sep: '|'} : {pre: '[[', post: ']]', sep: '|'});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** @override */
|
|
105
170
|
text() {
|
|
106
171
|
const str = super.text('|');
|
|
107
172
|
return this.type === 'gallery-image' ? str : `[[${str}]]`;
|
|
108
173
|
}
|
|
109
174
|
|
|
110
|
-
/**
|
|
175
|
+
/**
|
|
176
|
+
* 设置链接目标
|
|
177
|
+
* @param {string} link 链接目标
|
|
178
|
+
* @throws `SyntaxError` 非法的链接目标
|
|
179
|
+
*/
|
|
111
180
|
setTarget(link) {
|
|
112
181
|
link = String(link);
|
|
113
|
-
if (
|
|
182
|
+
if (this.type === 'link' && !/^\s*[:#]/u.test(link)) {
|
|
114
183
|
link = `:${link}`;
|
|
115
184
|
}
|
|
116
185
|
const root = Parser.parse(`[[${link}]]`, this.getAttribute('include'), 6, this.getAttribute('config')),
|
|
117
|
-
{childNodes: {length},
|
|
118
|
-
|
|
186
|
+
{childNodes: {length}, firstChild: wikiLink} = root,
|
|
187
|
+
{type, firstChild, childNodes: {length: linkLength}} = wikiLink;
|
|
188
|
+
if (length !== 1 || type !== this.type || linkLength !== 1) {
|
|
119
189
|
const msgs = {link: '内链', file: '文件链接', category: '分类', 'gallery-image': '文件链接'};
|
|
120
190
|
throw new SyntaxError(`非法的${msgs[this.type]}目标:${link}`);
|
|
121
191
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
firstElementChild.destroy();
|
|
125
|
-
this.firstElementChild.safeReplaceWith(firstChild);
|
|
192
|
+
wikiLink.destroy(true);
|
|
193
|
+
this.firstChild.safeReplaceWith(firstChild);
|
|
126
194
|
}
|
|
127
195
|
|
|
128
196
|
/**
|
|
129
|
-
*
|
|
130
|
-
* @param {string}
|
|
197
|
+
* 设置跨语言链接
|
|
198
|
+
* @param {string} lang 语言前缀
|
|
199
|
+
* @param {string} link 页面标题
|
|
200
|
+
* @throws `SyntaxError` 非法的跨语言链接
|
|
131
201
|
*/
|
|
132
202
|
setLangLink(lang, link) {
|
|
133
203
|
if (typeof lang !== 'string') {
|
|
134
204
|
this.typeError('setLangLink', 'String');
|
|
135
205
|
}
|
|
136
206
|
link = String(link).trim();
|
|
137
|
-
|
|
207
|
+
const [char] = link;
|
|
208
|
+
if (char === '#') {
|
|
138
209
|
throw new SyntaxError(`跨语言链接不能仅为fragment!`);
|
|
139
|
-
} else if (
|
|
210
|
+
} else if (char === ':') {
|
|
140
211
|
link = link.slice(1);
|
|
141
212
|
}
|
|
142
213
|
const root = Parser.parse(`[[${lang}:${link}]]`, this.getAttribute('include'), 6, this.getAttribute('config')),
|
|
143
|
-
/** @type {Token & {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
) {
|
|
214
|
+
/** @type {Token & {firstChild: LinkToken}} */ {childNodes: {length}, firstChild: wikiLink} = root,
|
|
215
|
+
{type, childNodes: {length: linkLength}, interwiki, firstChild} = wikiLink;
|
|
216
|
+
if (length !== 1 || type !== 'link' || linkLength !== 1 || interwiki !== lang.toLowerCase()) {
|
|
147
217
|
throw new SyntaxError(`非法的跨语言链接目标:${lang}:${link}`);
|
|
148
218
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
firstElementChild.destroy();
|
|
152
|
-
this.firstElementChild.safeReplaceWith(firstChild);
|
|
219
|
+
wikiLink.destroy(true);
|
|
220
|
+
this.firstChild.safeReplaceWith(firstChild);
|
|
153
221
|
}
|
|
154
222
|
|
|
155
|
-
/**
|
|
223
|
+
/**
|
|
224
|
+
* 设置fragment
|
|
225
|
+
* @param {string} fragment fragment
|
|
226
|
+
* @param {boolean} page 是否是其他页面
|
|
227
|
+
* @throws `SyntaxError` 非法的fragment
|
|
228
|
+
*/
|
|
156
229
|
#setFragment(fragment, page = true) {
|
|
157
|
-
fragment = String(fragment).
|
|
230
|
+
fragment = String(fragment).replaceAll(/[<>[\]#|=]/gu, p => encodeURIComponent(p));
|
|
158
231
|
const include = this.getAttribute('include'),
|
|
159
232
|
config = this.getAttribute('config'),
|
|
160
233
|
root = Parser.parse(`[[${page ? `:${this.name}` : ''}#${fragment}]]`, include, 6, config),
|
|
161
|
-
{childNodes: {length},
|
|
162
|
-
|
|
234
|
+
{childNodes: {length}, firstChild: wikiLink} = root,
|
|
235
|
+
{type, childNodes: {length: linkLength}, firstChild} = wikiLink;
|
|
236
|
+
if (length !== 1 || type !== 'link' || linkLength !== 1) {
|
|
163
237
|
throw new SyntaxError(`非法的 fragment:${fragment}`);
|
|
164
238
|
}
|
|
165
239
|
if (page) {
|
|
166
240
|
Parser.warn(`${this.constructor.name}.setFragment 方法会同时规范化页面名!`);
|
|
167
241
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
firstElementChild.destroy();
|
|
171
|
-
this.firstElementChild.safeReplaceWith(firstChild);
|
|
242
|
+
wikiLink.destroy(true);
|
|
243
|
+
this.firstChild.safeReplaceWith(firstChild);
|
|
172
244
|
}
|
|
173
245
|
|
|
174
|
-
/**
|
|
246
|
+
/**
|
|
247
|
+
* 设置fragment
|
|
248
|
+
* @param {string} fragment fragment
|
|
249
|
+
*/
|
|
175
250
|
setFragment(fragment) {
|
|
176
251
|
this.#setFragment(fragment);
|
|
177
252
|
}
|
|
178
253
|
|
|
254
|
+
/**
|
|
255
|
+
* 修改为到自身的链接
|
|
256
|
+
* @param {string} fragment fragment
|
|
257
|
+
* @throws `RangeError` 空fragment
|
|
258
|
+
*/
|
|
179
259
|
asSelfLink(fragment = this.fragment) {
|
|
180
260
|
fragment = String(fragment);
|
|
181
261
|
if (!fragment.trim()) {
|
|
@@ -184,46 +264,55 @@ class LinkToken extends Token {
|
|
|
184
264
|
this.#setFragment(fragment, false);
|
|
185
265
|
}
|
|
186
266
|
|
|
267
|
+
/**
|
|
268
|
+
* 设置链接显示文字
|
|
269
|
+
* @param {string} linkText 链接显示文字
|
|
270
|
+
* @throws `SyntaxError` 非法的链接显示文字
|
|
271
|
+
*/
|
|
187
272
|
setLinkText(linkText = '') {
|
|
188
273
|
linkText = String(linkText);
|
|
189
|
-
let
|
|
274
|
+
let lastChild;
|
|
190
275
|
const config = this.getAttribute('config');
|
|
191
276
|
if (linkText) {
|
|
192
277
|
const root = Parser.parse(`[[${
|
|
193
278
|
this.type === 'category' ? 'Category:' : ''
|
|
194
279
|
}L|${linkText}]]`, this.getAttribute('include'), 6, config),
|
|
195
|
-
{childNodes: {length},
|
|
196
|
-
if (length !== 1 ||
|
|
280
|
+
{childNodes: {length}, firstChild: wikiLink} = root;
|
|
281
|
+
if (length !== 1 || wikiLink.type !== this.type || wikiLink.childNodes.length !== 2) {
|
|
197
282
|
throw new SyntaxError(`非法的${this.type === 'link' ? '内链文字' : '分类关键字'}:${noWrap(linkText)}`);
|
|
198
283
|
}
|
|
199
|
-
({
|
|
284
|
+
({lastChild} = wikiLink);
|
|
200
285
|
} else {
|
|
201
|
-
|
|
202
|
-
|
|
286
|
+
lastChild = Parser.run(() => new Token('', config));
|
|
287
|
+
lastChild.setAttribute('stage', 7).type = 'link-text';
|
|
203
288
|
}
|
|
204
289
|
if (this.childNodes.length === 1) {
|
|
205
|
-
this.appendChild(
|
|
290
|
+
this.appendChild(lastChild);
|
|
206
291
|
} else {
|
|
207
|
-
this.
|
|
292
|
+
this.lastChild.safeReplaceWith(lastChild);
|
|
208
293
|
}
|
|
209
294
|
}
|
|
210
295
|
|
|
296
|
+
/**
|
|
297
|
+
* 自动生成管道符后的链接文字
|
|
298
|
+
* @throws `Error` 带有"#"或"%"时不可用
|
|
299
|
+
*/
|
|
211
300
|
pipeTrick() {
|
|
212
|
-
const linkText = this.
|
|
213
|
-
if (
|
|
301
|
+
const linkText = this.firstChild.text();
|
|
302
|
+
if (linkText.includes('#') || linkText.includes('%')) {
|
|
214
303
|
throw new Error('Pipe trick 不能用于带有"#"或"%"的场合!');
|
|
215
304
|
}
|
|
216
|
-
const m1 = /^:?(?:[ \w\x80-\
|
|
305
|
+
const m1 = /^:?(?:[ \w\x80-\xFF-]+:)?([^(]+)\(.+\)$/u.exec(linkText);
|
|
217
306
|
if (m1) {
|
|
218
307
|
this.setLinkText(m1[1].trim());
|
|
219
308
|
return;
|
|
220
309
|
}
|
|
221
|
-
const m2 = /^:?(?:[ \w\x80-\
|
|
310
|
+
const m2 = /^:?(?:[ \w\x80-\xFF-]+:)?([^(]+)(.+)$/u.exec(linkText);
|
|
222
311
|
if (m2) {
|
|
223
312
|
this.setLinkText(m2[1].trim());
|
|
224
313
|
return;
|
|
225
314
|
}
|
|
226
|
-
const m3 = /^:?(?:[ \w\x80-\
|
|
315
|
+
const m3 = /^:?(?:[ \w\x80-\xFF-]+:)?(.+?)(?:(?<!\()\(.+\))?(?:, |,|، )./u.exec(linkText);
|
|
227
316
|
if (m3) {
|
|
228
317
|
this.setLinkText(m3[1].trim());
|
|
229
318
|
return;
|
package/src/magicLink.js
CHANGED
|
@@ -1,52 +1,94 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const {generateForChild} = require('../util/lint'),
|
|
4
|
+
Parser = require('..'),
|
|
5
|
+
AstText = require('../lib/text'),
|
|
4
6
|
Token = require('.');
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* 自由外链
|
|
8
|
-
* @classdesc `{childNodes: [...
|
|
10
|
+
* @classdesc `{childNodes: [...AstText|CommentToken|IncludeToken|NoincludeToken]}`
|
|
9
11
|
*/
|
|
10
12
|
class MagicLinkToken extends Token {
|
|
11
13
|
type = 'free-ext-link';
|
|
12
14
|
#protocolRegex;
|
|
13
15
|
|
|
16
|
+
/** 协议 */
|
|
14
17
|
get protocol() {
|
|
15
18
|
return this.#protocolRegex.exec(this.text())?.[0];
|
|
16
19
|
}
|
|
20
|
+
|
|
17
21
|
set protocol(value) {
|
|
18
22
|
if (typeof value !== 'string') {
|
|
19
23
|
this.typeError('protocol', 'String');
|
|
20
24
|
}
|
|
21
|
-
if (!RegExp(`${this.#protocolRegex.source}$`, '
|
|
25
|
+
if (!new RegExp(`${this.#protocolRegex.source}$`, 'iu').test(value)) {
|
|
22
26
|
throw new RangeError(`非法的外链协议:${value}`);
|
|
23
27
|
}
|
|
24
28
|
this.replaceChildren(this.text().replace(this.#protocolRegex, value));
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
/** 和内链保持一致 */
|
|
32
|
+
get link() {
|
|
33
|
+
return this.text();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
set link(url) {
|
|
37
|
+
this.setTarget(url);
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
/**
|
|
28
|
-
* @param {string} url
|
|
41
|
+
* @param {string} url 网址
|
|
42
|
+
* @param {boolean} doubleSlash 是否接受"//"作为协议
|
|
29
43
|
* @param {accum} accum
|
|
30
44
|
*/
|
|
31
|
-
constructor(url, doubleSlash
|
|
45
|
+
constructor(url, doubleSlash, config = Parser.getConfig(), accum = []) {
|
|
32
46
|
super(url, config, true, accum, {'Stage-1': ':', '!ExtToken': ''});
|
|
33
47
|
if (doubleSlash) {
|
|
34
48
|
this.type = 'ext-link-url';
|
|
35
49
|
}
|
|
36
|
-
this.#protocolRegex = RegExp(`^(?:${config.protocol}${doubleSlash ? '|//' : ''})`, '
|
|
50
|
+
this.#protocolRegex = new RegExp(`^(?:${config.protocol}${doubleSlash ? '|//' : ''})`, 'iu');
|
|
37
51
|
}
|
|
38
52
|
|
|
53
|
+
/**
|
|
54
|
+
* @override
|
|
55
|
+
* @param {number} start 起始位置
|
|
56
|
+
*/
|
|
57
|
+
lint(start = 0) {
|
|
58
|
+
const errors = super.lint(start);
|
|
59
|
+
let /** @type {{top: number, left: number}} */ rect;
|
|
60
|
+
for (const child of this.childNodes) {
|
|
61
|
+
const str = String(child);
|
|
62
|
+
if (child.type !== 'text' || !/[,;。:!?()【】]/u.test(str)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
rect ||= this.getRootNode().posFromIndex(start);
|
|
66
|
+
const refError = generateForChild(child, rect, 'URL中的全角标点', 'warning');
|
|
67
|
+
errors.push(...[...str.matchAll(/[,;。:!?()【】]/gu)].map(({index}) => {
|
|
68
|
+
const lines = str.slice(0, index).split('\n'),
|
|
69
|
+
{length: top} = lines,
|
|
70
|
+
{length: left} = lines.at(-1),
|
|
71
|
+
startLine = refError.startLine + top - 1,
|
|
72
|
+
startCol = top > 1 ? left : refError.startCol + left;
|
|
73
|
+
return {...refError, startLine, endLine: startLine, startCol, endCol: startCol + 1};
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
return errors;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @override */
|
|
39
80
|
afterBuild() {
|
|
40
|
-
const ParameterToken = require('./parameter')
|
|
41
|
-
|
|
81
|
+
const ParameterToken = require('./parameter');
|
|
82
|
+
const /** @type {ParameterToken} */ parameter = this.closest('parameter');
|
|
42
83
|
if (parameter?.getValue() === this.text()) {
|
|
43
|
-
this.replaceWith(this.
|
|
84
|
+
this.replaceWith(...this.childNodes);
|
|
44
85
|
}
|
|
45
86
|
return this;
|
|
46
87
|
}
|
|
47
88
|
|
|
89
|
+
/** @override */
|
|
48
90
|
cloneNode() {
|
|
49
|
-
const cloned = this.
|
|
91
|
+
const cloned = this.cloneChildNodes(),
|
|
50
92
|
token = Parser.run(() => new MagicLinkToken(
|
|
51
93
|
undefined, this.type === 'ext-link-url', this.getAttribute('config'),
|
|
52
94
|
));
|
|
@@ -55,6 +97,10 @@ class MagicLinkToken extends Token {
|
|
|
55
97
|
return token;
|
|
56
98
|
}
|
|
57
99
|
|
|
100
|
+
/**
|
|
101
|
+
* 获取网址
|
|
102
|
+
* @throws `Error` 非标准协议
|
|
103
|
+
*/
|
|
58
104
|
getUrl() {
|
|
59
105
|
let url = this.text();
|
|
60
106
|
if (url.startsWith('//')) {
|
|
@@ -70,15 +116,19 @@ class MagicLinkToken extends Token {
|
|
|
70
116
|
}
|
|
71
117
|
}
|
|
72
118
|
|
|
73
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* 设置外链目标
|
|
121
|
+
* @param {string|URL} url 含协议的网址
|
|
122
|
+
* @throws `SyntaxError` 非法的自由外链目标
|
|
123
|
+
*/
|
|
74
124
|
setTarget(url) {
|
|
75
125
|
url = String(url);
|
|
76
126
|
const root = Parser.parse(url, this.getAttribute('include'), 9, this.getAttribute('config')),
|
|
77
|
-
{childNodes: {length},
|
|
78
|
-
if (length !== 1 ||
|
|
127
|
+
{childNodes: {length}, firstChild: freeExtLink} = root;
|
|
128
|
+
if (length !== 1 || freeExtLink.type !== 'free-ext-link') {
|
|
79
129
|
throw new SyntaxError(`非法的自由外链目标:${url}`);
|
|
80
130
|
}
|
|
81
|
-
this.replaceChildren(...
|
|
131
|
+
this.replaceChildren(...freeExtLink.childNodes);
|
|
82
132
|
}
|
|
83
133
|
}
|
|
84
134
|
|
package/src/nowiki/comment.js
CHANGED
|
@@ -1,44 +1,70 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const hidden = require('../../mixin/hidden'),
|
|
4
|
-
|
|
4
|
+
{generateForSelf} = require('../../util/lint'),
|
|
5
|
+
Parser = require('../..'),
|
|
5
6
|
NowikiToken = require('.');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* HTML注释,不可见
|
|
9
|
-
* @classdesc `{childNodes: [
|
|
10
|
+
* @classdesc `{childNodes: [AstText]}`
|
|
10
11
|
*/
|
|
11
12
|
class CommentToken extends hidden(NowikiToken) {
|
|
12
13
|
type = 'comment';
|
|
13
14
|
closed;
|
|
14
15
|
|
|
16
|
+
/** 内部wikitext */
|
|
17
|
+
get innerText() {
|
|
18
|
+
return String(this.firstChild);
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
/**
|
|
16
|
-
* @param {string} wikitext
|
|
22
|
+
* @param {string} wikitext wikitext
|
|
23
|
+
* @param {boolean} closed 是否闭合
|
|
17
24
|
* @param {accum} accum
|
|
18
25
|
*/
|
|
19
26
|
constructor(wikitext, closed = true, config = Parser.getConfig(), accum = []) {
|
|
20
27
|
super(wikitext, config, accum);
|
|
21
28
|
this.closed = closed;
|
|
29
|
+
Object.defineProperty(this, 'closed', {enumerable: false});
|
|
22
30
|
}
|
|
23
31
|
|
|
24
|
-
/** @
|
|
32
|
+
/** @override */
|
|
25
33
|
cloneNode() {
|
|
26
|
-
return Parser.run(() => new CommentToken(this.firstChild, this.closed, this.getAttribute('config')));
|
|
34
|
+
return Parser.run(() => new CommentToken(String(this.firstChild), this.closed, this.getAttribute('config')));
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
/**
|
|
38
|
+
* @override
|
|
39
|
+
* @param {string} selector
|
|
40
|
+
*/
|
|
41
|
+
toString(selector) {
|
|
42
|
+
if (!this.closed && this.nextSibling) {
|
|
33
43
|
Parser.error('自动闭合HTML注释', this);
|
|
34
44
|
this.closed = true;
|
|
35
45
|
}
|
|
36
|
-
return
|
|
46
|
+
return selector && this.matches(selector)
|
|
47
|
+
? ''
|
|
48
|
+
: `<!--${String(this.firstChild)}${this.closed ? '-->' : ''}`;
|
|
37
49
|
}
|
|
38
50
|
|
|
51
|
+
/** @override */
|
|
39
52
|
getPadding() {
|
|
40
53
|
return 4;
|
|
41
54
|
}
|
|
55
|
+
|
|
56
|
+
/** @override */
|
|
57
|
+
print() {
|
|
58
|
+
return super.print({pre: '<!--', post: this.closed ? '-->' : ''});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @override
|
|
63
|
+
* @param {number} start 起始位置
|
|
64
|
+
*/
|
|
65
|
+
lint(start = 0) {
|
|
66
|
+
return this.closed ? [] : [generateForSelf(this, this.getRootNode().posFromIndex(start), '未闭合的HTML注释')];
|
|
67
|
+
}
|
|
42
68
|
}
|
|
43
69
|
|
|
44
70
|
Parser.classes.CommentToken = __filename;
|