wikilint 2.4.5 → 2.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/dist/base.d.ts +21 -11
- package/dist/bin/cli.js +35 -12
- package/dist/lib/text.js +49 -8
- package/dist/src/arg.js +26 -8
- package/dist/src/attribute.js +32 -6
- package/dist/src/attributes.js +12 -2
- package/dist/src/converterFlags.js +17 -1
- package/dist/src/gallery.js +12 -0
- package/dist/src/html.js +19 -1
- package/dist/src/imageParameter.js +6 -1
- package/dist/src/link/base.js +26 -6
- package/dist/src/magicLink.js +28 -4
- package/dist/src/nested.js +14 -1
- package/dist/src/nowiki/comment.js +9 -3
- package/dist/src/nowiki/index.js +9 -3
- package/dist/src/nowiki/quote.js +14 -0
- package/dist/src/paramTag/index.js +9 -1
- package/dist/src/parameter.js +10 -8
- package/dist/src/table/td.js +18 -1
- package/dist/src/table/trBase.js +5 -7
- package/dist/src/tagPair/include.js +9 -3
- package/dist/src/transclude.js +8 -1
- package/dist/util/diff.js +3 -2
- package/package.json +2 -1
package/dist/base.d.ts
CHANGED
|
@@ -10,18 +10,28 @@ export interface Config {
|
|
|
10
10
|
readonly variants: string[];
|
|
11
11
|
readonly excludes?: string[];
|
|
12
12
|
}
|
|
13
|
-
export
|
|
14
|
-
|
|
13
|
+
export declare namespace LintError {
|
|
14
|
+
type Severity = 'error' | 'warning';
|
|
15
|
+
type Rule = 'bold-header' | 'format-leakage' | 'fostered-content' | 'h1' | 'illegal-attr' | 'insecure-style' | 'invalid-gallery' | 'invalid-imagemap' | 'invalid-invoke' | 'lonely-apos' | 'lonely-bracket' | 'lonely-http' | 'nested-link' | 'no-arg' | 'no-duplicate' | 'no-ignored' | 'obsolete-attr' | 'obsolete-tag' | 'parsing-order' | 'pipe-like' | 'table-layout' | 'tag-like' | 'unbalanced-header' | 'unclosed-comment' | 'unclosed-quote' | 'unclosed-table' | 'unescaped' | 'unknown-page' | 'unmatched-tag' | 'unterminated-url' | 'url-encoding' | 'var-anchor' | 'void-ext';
|
|
16
|
+
interface Fix {
|
|
17
|
+
readonly range: [number, number];
|
|
18
|
+
text: string;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
15
21
|
export interface LintError {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
rule: LintError.Rule;
|
|
23
|
+
message: string;
|
|
24
|
+
severity: LintError.Severity;
|
|
25
|
+
startIndex: number;
|
|
26
|
+
endIndex: number;
|
|
27
|
+
startLine: number;
|
|
28
|
+
startCol: number;
|
|
29
|
+
endLine: number;
|
|
30
|
+
endCol: number;
|
|
31
|
+
fix?: LintError.Fix;
|
|
32
|
+
suggestions?: (LintError.Fix & {
|
|
33
|
+
desc: string;
|
|
34
|
+
})[];
|
|
25
35
|
}
|
|
26
36
|
/** 类似Node */
|
|
27
37
|
export interface AstNode {
|
package/dist/bin/cli.js
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const fs_1 = require("fs");
|
|
5
5
|
const path_1 = require("path");
|
|
6
|
+
const chalk = require("chalk");
|
|
6
7
|
const Parser = require("../index");
|
|
7
8
|
const man = `
|
|
8
9
|
Available options:
|
|
9
10
|
-c, --config <path or preset config> Choose parser's configuration
|
|
11
|
+
--fix Automatically fix problems
|
|
10
12
|
-h, --help Print available options
|
|
11
13
|
-i, --include Parse for inclusion
|
|
12
14
|
-l, --lang Choose i18n language
|
|
@@ -14,8 +16,8 @@ Available options:
|
|
|
14
16
|
-s, --strict Exit when there is an error or warning
|
|
15
17
|
Override -q or --quiet
|
|
16
18
|
-v, --version Print package version
|
|
17
|
-
`, preset = new Set(
|
|
18
|
-
let include = false, quiet = false, strict = false, exit = false, nErr = 0, nWarn = 0, option, config, lang;
|
|
19
|
+
`, preset = new Set((0, fs_1.readdirSync)('./config').filter(file => file.endsWith('.json')).map(file => file.slice(0, -5))), { argv } = process, files = [];
|
|
20
|
+
let include = false, quiet = false, strict = false, exit = false, fixing = false, nErr = 0, nWarn = 0, nFixableErr = 0, nFixableWarn = 0, option, config, lang;
|
|
19
21
|
/**
|
|
20
22
|
* throw if `-c` or `--config` option is incorrect
|
|
21
23
|
* @throws `Error` unrecognized config input
|
|
@@ -33,12 +35,12 @@ const throwOnConfig = () => {
|
|
|
33
35
|
* @param n number of items
|
|
34
36
|
* @param word item name
|
|
35
37
|
*/
|
|
36
|
-
const plural = (n, word) => `${n} ${word}${n
|
|
38
|
+
const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
37
39
|
/**
|
|
38
40
|
* color the severity
|
|
39
41
|
* @param severity problem severity
|
|
40
42
|
*/
|
|
41
|
-
const coloredSeverity = (severity) =>
|
|
43
|
+
const coloredSeverity = (severity) => chalk[severity === 'error' ? 'red' : 'yellow'](severity.padEnd(7));
|
|
42
44
|
for (let i = 2; i < argv.length; i++) {
|
|
43
45
|
option = argv[i];
|
|
44
46
|
switch (option) {
|
|
@@ -47,6 +49,9 @@ for (let i = 2; i < argv.length; i++) {
|
|
|
47
49
|
config = argv[++i];
|
|
48
50
|
throwOnConfig();
|
|
49
51
|
break;
|
|
52
|
+
case '--fix':
|
|
53
|
+
fixing = true;
|
|
54
|
+
break;
|
|
50
55
|
case '-h':
|
|
51
56
|
case '--help':
|
|
52
57
|
console.log(man);
|
|
@@ -105,28 +110,46 @@ if (quiet && strict) {
|
|
|
105
110
|
console.error('-s or --strict will override -q or --quiet\n');
|
|
106
111
|
}
|
|
107
112
|
for (const file of files) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
let wikitext = (0, fs_1.readFileSync)(file, 'utf8'), problems = Parser.parse(wikitext, include).lint();
|
|
114
|
+
if (fixing && problems.some(({ fix }) => fix)) {
|
|
115
|
+
// 倒序修复,跳过嵌套的修复
|
|
116
|
+
const fixable = problems.map(({ fix }) => fix).filter(Boolean)
|
|
117
|
+
.sort(({ range: [aFrom, aTo] }, { range: [bFrom, bTo] }) => aTo === bTo ? bFrom - aFrom : bTo - aTo);
|
|
118
|
+
let start = Infinity;
|
|
119
|
+
for (const { range: [from, to], text } of fixable) {
|
|
120
|
+
if (to <= start) {
|
|
121
|
+
wikitext = `${wikitext.slice(0, from)}${text}${wikitext.slice(to)}`;
|
|
122
|
+
start = from;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
void fs_1.promises.writeFile(file, wikitext);
|
|
126
|
+
problems = Parser.parse(wikitext, include).lint();
|
|
127
|
+
}
|
|
128
|
+
const errors = problems.filter(({ severity }) => severity === 'error'), fixable = problems.filter(({ fix }) => fix), nLocalErr = errors.length, nLocalWarn = problems.length - nLocalErr, nLocalFixableErr = fixable.filter(({ severity }) => severity === 'error').length, nLocalFixableWarn = fixable.length - nLocalFixableErr;
|
|
111
129
|
if (quiet) {
|
|
112
130
|
problems = errors;
|
|
113
131
|
}
|
|
114
132
|
else {
|
|
115
133
|
nWarn += nLocalWarn;
|
|
134
|
+
nFixableWarn += nLocalFixableWarn;
|
|
116
135
|
}
|
|
117
136
|
if (problems.length > 0) {
|
|
118
|
-
console.error('
|
|
119
|
-
const
|
|
137
|
+
console.error(`\n${chalk.underline('%s')}`, (0, path_1.resolve)(file));
|
|
138
|
+
const maxLineChars = String(Math.max(...problems.map(({ startLine }) => startLine))).length, maxColChars = String(Math.max(...problems.map(({ startCol }) => startCol))).length, maxMessageChars = Math.max(...problems.map(({ message: { length } }) => length));
|
|
120
139
|
for (const { rule, message, severity, startLine, startCol } of problems) {
|
|
121
|
-
console.error(`
|
|
140
|
+
console.error(` ${chalk.dim('%s:%s')} %s %s ${chalk.dim('%s')}`, String(startLine).padStart(maxLineChars), String(startCol).padEnd(maxColChars), coloredSeverity(severity), message.padEnd(maxMessageChars), rule);
|
|
122
141
|
}
|
|
123
|
-
console.error();
|
|
124
142
|
}
|
|
125
143
|
nErr += nLocalErr;
|
|
144
|
+
nFixableErr += nLocalFixableErr;
|
|
126
145
|
exit ||= Boolean(nLocalErr || strict && nLocalWarn);
|
|
127
146
|
}
|
|
128
147
|
if (nErr || nWarn) {
|
|
129
|
-
console.error('
|
|
148
|
+
console.error(chalk.red.bold('%s'), `\n✖ ${plural(nErr + nWarn, 'problem')} (${plural(nErr, 'error')}, ${plural(nWarn, 'warning')})`);
|
|
149
|
+
if (nFixableErr || nFixableWarn) {
|
|
150
|
+
console.error(chalk.red.bold('%s'), ` ${plural(nFixableErr, 'error')} and ${plural(nFixableWarn, 'warning')} potentially fixable with the \`--fix\` option.`);
|
|
151
|
+
}
|
|
152
|
+
console.error();
|
|
130
153
|
}
|
|
131
154
|
if (exit) {
|
|
132
155
|
process.exitCode = 1;
|
package/dist/lib/text.js
CHANGED
|
@@ -80,8 +80,11 @@ class AstText extends node_1.AstNode {
|
|
|
80
80
|
*/
|
|
81
81
|
lint(start = this.getAbsoluteIndex()) {
|
|
82
82
|
const { data, parentNode, nextSibling, previousSibling } = this;
|
|
83
|
+
if (!parentNode) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
83
86
|
const { NowikiToken } = require('../src/nowiki');
|
|
84
|
-
const { type, name } = parentNode, nowiki = name === 'nowiki' || name === 'pre';
|
|
87
|
+
const { type, name } = parentNode, nowiki = name === 'nowiki' || name === 'pre', isHtmlAttrVal = type === 'attr-value' && parentNode.parentNode.type !== 'ext-attr';
|
|
85
88
|
let errorRegex;
|
|
86
89
|
if (type === 'ext-inner' && (name === 'pre' || parentNode instanceof NowikiToken)) {
|
|
87
90
|
errorRegex = new RegExp(`<\\s*(?:\\/\\s*)${nowiki ? '' : '?'}(${name})\\b`, 'giu');
|
|
@@ -89,7 +92,7 @@ class AstText extends node_1.AstNode {
|
|
|
89
92
|
else if (type === 'free-ext-link'
|
|
90
93
|
|| type === 'ext-link-url'
|
|
91
94
|
|| type === 'image-parameter' && name === 'link'
|
|
92
|
-
||
|
|
95
|
+
|| isHtmlAttrVal) {
|
|
93
96
|
errorRegex = errorSyntaxUrl;
|
|
94
97
|
}
|
|
95
98
|
else {
|
|
@@ -121,14 +124,15 @@ class AstText extends node_1.AstNode {
|
|
|
121
124
|
else if (char === ']' && (index || length > 1)) {
|
|
122
125
|
errorRegex.lastIndex--;
|
|
123
126
|
}
|
|
124
|
-
const startIndex = start + index, endIndex = startIndex + length, rootStr = String(root), nextChar = rootStr[endIndex], previousChar = rootStr[startIndex - 1], severity = length > 1 && (char
|
|
127
|
+
const startIndex = start + index, endIndex = startIndex + length, rootStr = String(root), nextChar = rootStr[endIndex], previousChar = rootStr[startIndex - 1], severity = length > 1 && !(char === '<' && (nowiki || !/[\s/>]/u.test(nextChar ?? ''))
|
|
128
|
+
|| isHtmlAttrVal && (char === '[' || char === ']'))
|
|
125
129
|
|| char === '{' && (nextChar === char || previousChar === '-')
|
|
126
130
|
|| char === '}' && (previousChar === char || nextChar === '-')
|
|
127
131
|
|| char === '[' && (nextChar === char
|
|
128
132
|
|| type === 'ext-link-text'
|
|
129
|
-
|| !data.slice(index + 1).trim()
|
|
133
|
+
|| nextType === 'free-ext-link' && !data.slice(index + 1).trim())
|
|
130
134
|
|| char === ']' && (previousChar === char
|
|
131
|
-
|| !data.slice(0, index).
|
|
135
|
+
|| previousType === 'free-ext-link' && !data.slice(0, index).includes(']'))
|
|
132
136
|
? 'error'
|
|
133
137
|
: 'warning';
|
|
134
138
|
const leftBracket = char === '{' || char === '[', rightBracket = char === ']' || char === '}';
|
|
@@ -149,8 +153,7 @@ class AstText extends node_1.AstNode {
|
|
|
149
153
|
}
|
|
150
154
|
}
|
|
151
155
|
}
|
|
152
|
-
const lines = data.slice(0, index).split('\n'), startLine = lines.length + top - 1, line = lines[lines.length - 1], startCol = lines.length === 1 ? left + line.length : line.length
|
|
153
|
-
errors.push({
|
|
156
|
+
const lines = data.slice(0, index).split('\n'), startLine = lines.length + top - 1, line = lines[lines.length - 1], startCol = lines.length === 1 ? left + line.length : line.length, e = {
|
|
154
157
|
rule: ruleMap[char],
|
|
155
158
|
message: index_1.default.msg('lonely "$1"', char === 'h' ? error : char),
|
|
156
159
|
severity,
|
|
@@ -160,7 +163,45 @@ class AstText extends node_1.AstNode {
|
|
|
160
163
|
endLine: startLine,
|
|
161
164
|
startCol,
|
|
162
165
|
endCol: startCol + length,
|
|
163
|
-
}
|
|
166
|
+
};
|
|
167
|
+
if (char === '<') {
|
|
168
|
+
e.suggestions = [
|
|
169
|
+
{
|
|
170
|
+
desc: 'escape',
|
|
171
|
+
range: [startIndex, startIndex + 1],
|
|
172
|
+
text: '<',
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
else if (char === 'h'
|
|
177
|
+
&& !(type === 'ext-link-text' || type === 'link-text')
|
|
178
|
+
&& /[\p{L}\d_]/u.test(previousChar || '')) {
|
|
179
|
+
e.suggestions = [
|
|
180
|
+
{
|
|
181
|
+
desc: 'whitespace',
|
|
182
|
+
range: [startIndex, startIndex],
|
|
183
|
+
text: ' ',
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
}
|
|
187
|
+
else if (char === '[' && type === 'ext-link-text') {
|
|
188
|
+
const i = parentNode.getAbsoluteIndex() + String(parentNode).length;
|
|
189
|
+
e.suggestions = [
|
|
190
|
+
{
|
|
191
|
+
desc: 'escape',
|
|
192
|
+
range: [i, i + 1],
|
|
193
|
+
text: ']',
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
}
|
|
197
|
+
else if (char === ']' && previousType === 'free-ext-link' && severity === 'error') {
|
|
198
|
+
const i = start - String(previousSibling).length;
|
|
199
|
+
e.fix = {
|
|
200
|
+
range: [i, i],
|
|
201
|
+
text: '[',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
errors.push(e);
|
|
164
205
|
}
|
|
165
206
|
return errors;
|
|
166
207
|
}
|
package/dist/src/arg.js
CHANGED
|
@@ -55,22 +55,40 @@ class ArgToken extends index_2.Token {
|
|
|
55
55
|
}
|
|
56
56
|
/** @override */
|
|
57
57
|
lint(start = this.getAbsoluteIndex()) {
|
|
58
|
+
const { childNodes: [argName, argDefault, ...rest] } = this;
|
|
58
59
|
if (!this.getAttribute('include')) {
|
|
59
|
-
|
|
60
|
+
const e = (0, lint_1.generateForSelf)(this, { start }, 'no-arg', 'unexpected template argument');
|
|
61
|
+
if (argDefault) {
|
|
62
|
+
e.fix = {
|
|
63
|
+
range: [start, e.endIndex],
|
|
64
|
+
text: argDefault.text(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return [e];
|
|
60
68
|
}
|
|
61
|
-
const
|
|
69
|
+
const errors = argName.lint(start + 3);
|
|
62
70
|
if (argDefault) {
|
|
63
71
|
errors.push(...argDefault.lint(start + 4 + String(argName).length));
|
|
64
72
|
}
|
|
65
73
|
if (rest.length > 0) {
|
|
66
74
|
const rect = { start, ...this.getRootNode().posFromIndex(start) };
|
|
67
75
|
errors.push(...rest.map(child => {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
const e = (0, lint_1.generateForChild)(child, rect, 'no-ignored', 'invisible content inside triple braces');
|
|
77
|
+
e.startIndex--;
|
|
78
|
+
e.startCol--;
|
|
79
|
+
e.suggestions = [
|
|
80
|
+
{
|
|
81
|
+
desc: 'remove',
|
|
82
|
+
range: [e.startIndex, e.endIndex],
|
|
83
|
+
text: '',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
desc: 'escape',
|
|
87
|
+
range: [e.startIndex, e.startIndex + 1],
|
|
88
|
+
text: '{{!}}',
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
return e;
|
|
74
92
|
}));
|
|
75
93
|
}
|
|
76
94
|
return errors;
|
package/dist/src/attribute.js
CHANGED
|
@@ -261,11 +261,24 @@ class AttributeToken extends index_2.Token {
|
|
|
261
261
|
const root = this.getRootNode();
|
|
262
262
|
rect = { start, ...root.posFromIndex(start) };
|
|
263
263
|
const e = (0, lint_1.generateForChild)(lastChild, rect, 'unclosed-quote', index_1.default.msg('unclosed $1', 'quotes'), 'warning');
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
264
|
+
e.startIndex--;
|
|
265
|
+
e.startCol--;
|
|
266
|
+
const fix = {
|
|
267
|
+
range: [e.endIndex, e.endIndex],
|
|
268
|
+
text: this.#quotes[0],
|
|
269
|
+
};
|
|
270
|
+
if (lastChild.childNodes.some(child => child.type === 'text' && /\s/u.test(child.text()))) {
|
|
271
|
+
e.suggestions = [
|
|
272
|
+
{
|
|
273
|
+
desc: 'quote',
|
|
274
|
+
...fix,
|
|
275
|
+
},
|
|
276
|
+
];
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
e.fix = fix;
|
|
280
|
+
}
|
|
281
|
+
errors.push(e);
|
|
269
282
|
}
|
|
270
283
|
const attrs = extAttrs[tag];
|
|
271
284
|
if (attrs && !attrs.has(name)
|
|
@@ -286,7 +299,20 @@ class AttributeToken extends index_2.Token {
|
|
|
286
299
|
}
|
|
287
300
|
else if (name === 'tabindex' && typeof value === 'string' && value.trim() !== '0') {
|
|
288
301
|
rect ??= { start, ...this.getRootNode().posFromIndex(start) };
|
|
289
|
-
|
|
302
|
+
const e = (0, lint_1.generateForChild)(lastChild, rect, 'illegal-attr', 'nonzero tabindex');
|
|
303
|
+
e.suggestions = [
|
|
304
|
+
{
|
|
305
|
+
desc: 'remove',
|
|
306
|
+
range: [start, e.endIndex],
|
|
307
|
+
text: '',
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
desc: '0 tabindex',
|
|
311
|
+
range: [e.startIndex, e.endIndex],
|
|
312
|
+
text: '0',
|
|
313
|
+
},
|
|
314
|
+
];
|
|
315
|
+
errors.push(e);
|
|
290
316
|
}
|
|
291
317
|
return errors;
|
|
292
318
|
}
|
package/dist/src/attributes.js
CHANGED
|
@@ -100,13 +100,23 @@ class AttributesToken extends index_2.Token {
|
|
|
100
100
|
let rect;
|
|
101
101
|
if (parentNode?.type === 'html' && parentNode.closing && this.text().trim()) {
|
|
102
102
|
rect = { start, ...this.getRootNode().posFromIndex(start) };
|
|
103
|
-
|
|
103
|
+
const e = (0, lint_1.generateForSelf)(this, rect, 'no-ignored', 'attributes of a closing tag');
|
|
104
|
+
e.fix = { range: [start, e.endIndex], text: '' };
|
|
105
|
+
errors.push(e);
|
|
104
106
|
}
|
|
105
107
|
for (let i = 0; i < length; i++) {
|
|
106
108
|
const attr = childNodes[i];
|
|
107
109
|
if (attr instanceof atom_1.AtomToken && attr.text().trim()) {
|
|
108
110
|
rect ??= { start, ...this.getRootNode().posFromIndex(start) };
|
|
109
|
-
|
|
111
|
+
const e = (0, lint_1.generateForChild)(attr, rect, 'no-ignored', 'containing invalid attribute');
|
|
112
|
+
e.suggestions = [
|
|
113
|
+
{
|
|
114
|
+
desc: 'remove',
|
|
115
|
+
range: [e.startIndex, e.endIndex],
|
|
116
|
+
text: ' ',
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
errors.push(e);
|
|
110
120
|
}
|
|
111
121
|
else if (attr instanceof attribute_1.AttributeToken) {
|
|
112
122
|
const { name } = attr;
|
|
@@ -56,7 +56,23 @@ class ConverterFlagsToken extends index_2.Token {
|
|
|
56
56
|
&& !variantFlags.has(flag)
|
|
57
57
|
&& !unknownFlags.has(flag)
|
|
58
58
|
&& (variantFlags.size > 0 || !validFlags.has(flag))) {
|
|
59
|
-
|
|
59
|
+
const e = (0, lint_1.generateForChild)(child, rect, 'no-ignored', 'invalid conversion flag');
|
|
60
|
+
if (variantFlags.size === 0 && definedFlags.has(flag.toUpperCase())) {
|
|
61
|
+
e.fix = {
|
|
62
|
+
range: [e.startIndex, e.endIndex],
|
|
63
|
+
text: flag.toUpperCase(),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
e.suggestions = [
|
|
68
|
+
{
|
|
69
|
+
desc: 'remove',
|
|
70
|
+
range: [e.startIndex, e.endIndex],
|
|
71
|
+
text: '',
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
errors.push(e);
|
|
60
76
|
}
|
|
61
77
|
}
|
|
62
78
|
return errors;
|
package/dist/src/gallery.js
CHANGED
|
@@ -67,6 +67,18 @@ class GalleryToken extends index_2.Token {
|
|
|
67
67
|
endLine: startLine,
|
|
68
68
|
startCol,
|
|
69
69
|
endCol: startCol + length,
|
|
70
|
+
suggestions: [
|
|
71
|
+
{
|
|
72
|
+
desc: 'remove',
|
|
73
|
+
range: [start, start + length],
|
|
74
|
+
text: '',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
desc: 'comment',
|
|
78
|
+
range: [start, start + length],
|
|
79
|
+
text: `<!--${str}-->`,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
70
82
|
});
|
|
71
83
|
}
|
|
72
84
|
else if (child.type !== 'noinclude' && child.type !== 'text') {
|
package/dist/src/html.js
CHANGED
|
@@ -114,6 +114,24 @@ class HtmlToken extends index_2.Token {
|
|
|
114
114
|
if (ancestor && magicWords.has(ancestor.name)) {
|
|
115
115
|
error.severity = 'warning';
|
|
116
116
|
}
|
|
117
|
+
else {
|
|
118
|
+
error.suggestions = [
|
|
119
|
+
{
|
|
120
|
+
desc: 'remove',
|
|
121
|
+
range: [start, error.endIndex],
|
|
122
|
+
text: '',
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else if (msg === 'tag that is both closing and self-closing') {
|
|
128
|
+
const { html: [, , voidTags] } = this.getAttribute('config');
|
|
129
|
+
if (voidTags.includes(this.name)) {
|
|
130
|
+
error.fix = {
|
|
131
|
+
range: [start + 1, start + 2],
|
|
132
|
+
text: '',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
117
135
|
}
|
|
118
136
|
errors.push(error);
|
|
119
137
|
}
|
|
@@ -131,7 +149,7 @@ class HtmlToken extends index_2.Token {
|
|
|
131
149
|
refError ??= (0, lint_1.generateForSelf)(this, { start }, 'h1', '');
|
|
132
150
|
errors.push({
|
|
133
151
|
...refError,
|
|
134
|
-
rule: '
|
|
152
|
+
rule: 'bold-header',
|
|
135
153
|
message: index_1.default.msg('bold in section header'),
|
|
136
154
|
severity: 'warning',
|
|
137
155
|
});
|
|
@@ -102,7 +102,12 @@ class ImageParameterToken extends index_2.Token {
|
|
|
102
102
|
lint(start = this.getAbsoluteIndex()) {
|
|
103
103
|
const errors = super.lint(start), { link, name } = this;
|
|
104
104
|
if (name === 'invalid') {
|
|
105
|
-
|
|
105
|
+
const e = (0, lint_1.generateForSelf)(this, { start }, 'invalid-gallery', 'invalid gallery image parameter');
|
|
106
|
+
e.fix = {
|
|
107
|
+
range: [start, start + e.endIndex],
|
|
108
|
+
text: '',
|
|
109
|
+
};
|
|
110
|
+
errors.push(e);
|
|
106
111
|
}
|
|
107
112
|
else if (typeof link === 'object' && link.encoded) {
|
|
108
113
|
errors.push((0, lint_1.generateForSelf)(this, { start }, 'url-encoding', 'unnecessary URL encoding in an internal link'));
|
package/dist/src/link/base.js
CHANGED
|
@@ -82,14 +82,34 @@ class LinkBaseToken extends index_2.Token {
|
|
|
82
82
|
rect ??= { start, ...this.getRootNode().posFromIndex(start) };
|
|
83
83
|
errors.push((0, lint_1.generateForChild)(target, rect, 'url-encoding', 'unnecessary URL encoding in an internal link'));
|
|
84
84
|
}
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
if (linkType === 'link' || linkType === 'category') {
|
|
86
|
+
const textNode = linkText?.childNodes.find((c) => c.type === 'text' && c.data.includes('|'));
|
|
87
|
+
if (textNode) {
|
|
88
|
+
rect ??= { start, ...this.getRootNode().posFromIndex(start) };
|
|
89
|
+
const e = (0, lint_1.generateForChild)(linkText, rect, 'pipe-like', 'additional "|" in the link text', 'warning');
|
|
90
|
+
e.suggestions = [
|
|
91
|
+
{
|
|
92
|
+
desc: 'escape',
|
|
93
|
+
range: [
|
|
94
|
+
e.startIndex + textNode.getRelativeIndex(),
|
|
95
|
+
e.startIndex + textNode.getRelativeIndex() + textNode.data.length,
|
|
96
|
+
],
|
|
97
|
+
text: textNode.data.replace(/\|/gu, '|'),
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
errors.push(e);
|
|
101
|
+
}
|
|
89
102
|
}
|
|
90
|
-
|
|
103
|
+
if (linkType !== 'link' && fragment !== undefined) {
|
|
91
104
|
rect ??= { start, ...this.getRootNode().posFromIndex(start) };
|
|
92
|
-
|
|
105
|
+
const e = (0, lint_1.generateForChild)(target, rect, 'no-ignored', 'useless fragment'), textNode = target.childNodes.find((c) => c.type === 'text' && c.data.includes('#'));
|
|
106
|
+
if (textNode) {
|
|
107
|
+
e.fix = {
|
|
108
|
+
range: [e.startIndex + textNode.getRelativeIndex() + textNode.data.indexOf('#'), e.endIndex],
|
|
109
|
+
text: '',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
errors.push(e);
|
|
93
113
|
}
|
|
94
114
|
return errors;
|
|
95
115
|
}
|
package/dist/src/magicLink.js
CHANGED
|
@@ -30,17 +30,41 @@ class MagicLinkToken extends index_2.Token {
|
|
|
30
30
|
const refError = (0, lint_1.generateForChild)(child, rect, 'unterminated-url', '', 'warning');
|
|
31
31
|
regexGlobal.lastIndex = 0;
|
|
32
32
|
for (let mt = regexGlobal.exec(data); mt; mt = regexGlobal.exec(data)) {
|
|
33
|
-
const { index, 0: s } = mt, lines = data.slice(0, index).split('\n'), top = lines.length, left = lines[top - 1].length, startIndex = refError.startIndex + index, startLine = refError.startLine + top - 1, startCol = top === 1 ? refError.startCol + left : left;
|
|
34
|
-
|
|
33
|
+
const { index, 0: s } = mt, lines = data.slice(0, index).split('\n'), top = lines.length, left = lines[top - 1].length, startIndex = refError.startIndex + index, startLine = refError.startLine + top - 1, startCol = top === 1 ? refError.startCol + left : left, pipe = s.startsWith('|');
|
|
34
|
+
const e = {
|
|
35
35
|
...refError,
|
|
36
|
-
message: index_1.default.msg('$1 in URL',
|
|
36
|
+
message: index_1.default.msg('$1 in URL', pipe ? '"|"' : 'full-width punctuation'),
|
|
37
37
|
startIndex,
|
|
38
38
|
endIndex: startIndex + s.length,
|
|
39
39
|
startLine,
|
|
40
40
|
endLine: startLine,
|
|
41
41
|
startCol,
|
|
42
42
|
endCol: startCol + s.length,
|
|
43
|
-
}
|
|
43
|
+
};
|
|
44
|
+
if (!pipe) {
|
|
45
|
+
e.suggestions = [
|
|
46
|
+
{
|
|
47
|
+
desc: 'whitespace',
|
|
48
|
+
range: [startIndex, startIndex],
|
|
49
|
+
text: ' ',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
desc: 'escape',
|
|
53
|
+
range: [startIndex, e.endIndex],
|
|
54
|
+
text: encodeURI(s),
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
else if (s.length === 1) {
|
|
59
|
+
e.suggestions = [
|
|
60
|
+
{
|
|
61
|
+
desc: 'whitespace',
|
|
62
|
+
range: [startIndex, startIndex + 1],
|
|
63
|
+
text: ' ',
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
errors.push(e);
|
|
44
68
|
}
|
|
45
69
|
}
|
|
46
70
|
return errors;
|
package/dist/src/nested.js
CHANGED
|
@@ -50,7 +50,20 @@ class NestedToken extends index_2.Token {
|
|
|
50
50
|
return str && !/^<!--.*-->$/su.test(str);
|
|
51
51
|
}).map(child => {
|
|
52
52
|
rect ??= { start, ...this.getRootNode().posFromIndex(start) };
|
|
53
|
-
|
|
53
|
+
const e = (0, lint_1.generateForChild)(child, rect, 'no-ignored', index_1.default.msg('invalid content in <$1>', this.name));
|
|
54
|
+
e.suggestions = [
|
|
55
|
+
{
|
|
56
|
+
desc: 'remove',
|
|
57
|
+
range: [e.startIndex, e.endIndex],
|
|
58
|
+
text: '',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
desc: 'comment',
|
|
62
|
+
range: [e.startIndex, e.startIndex],
|
|
63
|
+
text: `<!--${String(child)}-->`,
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
return e;
|
|
54
67
|
}),
|
|
55
68
|
];
|
|
56
69
|
}
|
|
@@ -20,9 +20,15 @@ class CommentToken extends (0, hidden_1.hiddenToken)(base_1.NowikiBaseToken) {
|
|
|
20
20
|
}
|
|
21
21
|
/** @override */
|
|
22
22
|
lint(start = this.getAbsoluteIndex()) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
if (this.closed) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
const e = (0, lint_1.generateForSelf)(this, { start }, 'unclosed-comment', index_1.default.msg('unclosed $1', 'HTML comment'));
|
|
27
|
+
e.fix = {
|
|
28
|
+
range: [e.endIndex, e.endIndex],
|
|
29
|
+
text: '-->',
|
|
30
|
+
};
|
|
31
|
+
return [e];
|
|
26
32
|
}
|
|
27
33
|
/** @private */
|
|
28
34
|
toString() {
|
package/dist/src/nowiki/index.js
CHANGED
|
@@ -10,9 +10,15 @@ class NowikiToken extends base_1.NowikiBaseToken {
|
|
|
10
10
|
/** @override */
|
|
11
11
|
lint(start = this.getAbsoluteIndex()) {
|
|
12
12
|
const { name, firstChild: { data } } = this;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
if ((name === 'templatestyles' || name === 'section') && data) {
|
|
14
|
+
const e = (0, lint_1.generateForSelf)(this, { start }, 'void-ext', index_1.default.msg('nothing should be in <$1>', name));
|
|
15
|
+
e.fix = {
|
|
16
|
+
range: [start - 1, e.endIndex + name.length + 3],
|
|
17
|
+
text: '/>',
|
|
18
|
+
};
|
|
19
|
+
return [e];
|
|
20
|
+
}
|
|
21
|
+
return super.lint(start);
|
|
16
22
|
}
|
|
17
23
|
}
|
|
18
24
|
exports.NowikiToken = NowikiToken;
|
package/dist/src/nowiki/quote.js
CHANGED
|
@@ -29,6 +29,13 @@ class QuoteToken extends base_1.NowikiBaseToken {
|
|
|
29
29
|
startCol: endCol - length,
|
|
30
30
|
endLine,
|
|
31
31
|
endCol,
|
|
32
|
+
suggestions: [
|
|
33
|
+
{
|
|
34
|
+
desc: 'escape',
|
|
35
|
+
range: [startIndex, endIndex],
|
|
36
|
+
text: '''.repeat(length),
|
|
37
|
+
},
|
|
38
|
+
],
|
|
32
39
|
});
|
|
33
40
|
}
|
|
34
41
|
if (nextSibling?.type === 'text' && nextSibling.data.startsWith(`'`)) {
|
|
@@ -41,6 +48,13 @@ class QuoteToken extends base_1.NowikiBaseToken {
|
|
|
41
48
|
startLine,
|
|
42
49
|
startCol,
|
|
43
50
|
endCol: startCol + length,
|
|
51
|
+
suggestions: [
|
|
52
|
+
{
|
|
53
|
+
desc: 'escape',
|
|
54
|
+
range: [startIndex, endIndex],
|
|
55
|
+
text: '''.repeat(length),
|
|
56
|
+
},
|
|
57
|
+
],
|
|
44
58
|
});
|
|
45
59
|
}
|
|
46
60
|
if (bold && this.closest('heading-title')) {
|
|
@@ -39,7 +39,15 @@ class ParamTagToken extends index_2.Token {
|
|
|
39
39
|
return str && !(i >= 0 ? /^[a-z]+(?:\[\])?\s*(?:=|$)/iu : /^[a-z]+(?:\[\])?\s*=/iu).test(str);
|
|
40
40
|
}).map(child => {
|
|
41
41
|
rect ??= { start, ...this.getRootNode().posFromIndex(start) };
|
|
42
|
-
|
|
42
|
+
const e = (0, lint_1.generateForChild)(child, rect, 'no-ignored', index_1.default.msg('invalid parameter of <$1>', this.name));
|
|
43
|
+
e.suggestions = [
|
|
44
|
+
{
|
|
45
|
+
desc: 'remove',
|
|
46
|
+
range: [e.startIndex, e.endIndex],
|
|
47
|
+
text: '',
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
return e;
|
|
43
51
|
});
|
|
44
52
|
}
|
|
45
53
|
}
|
package/dist/src/parameter.js
CHANGED
|
@@ -61,14 +61,16 @@ class ParameterToken extends index_2.Token {
|
|
|
61
61
|
const errors = super.lint(start), { firstChild } = this, link = new RegExp(`https?://${string_1.extUrlCharFirst}${string_1.extUrlChar}$`, 'iu').exec(firstChild.text())?.[0];
|
|
62
62
|
if (link && new URL(link).search) {
|
|
63
63
|
const e = (0, lint_1.generateForChild)(firstChild, { start }, 'unescaped', 'unescaped query string in an anonymous parameter');
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
e.startIndex = e.endIndex;
|
|
65
|
+
e.startLine = e.endLine;
|
|
66
|
+
e.startCol = e.endCol;
|
|
67
|
+
e.endIndex++;
|
|
68
|
+
e.endCol++;
|
|
69
|
+
e.fix = {
|
|
70
|
+
range: [e.startIndex, e.endIndex],
|
|
71
|
+
text: '{{=}}',
|
|
72
|
+
};
|
|
73
|
+
errors.push(e);
|
|
72
74
|
}
|
|
73
75
|
return errors;
|
|
74
76
|
}
|
package/dist/src/table/td.js
CHANGED
|
@@ -81,7 +81,24 @@ class TdToken extends base_1.TableBaseToken {
|
|
|
81
81
|
if (child.type === 'text') {
|
|
82
82
|
const { data } = child;
|
|
83
83
|
if (data.includes('|')) {
|
|
84
|
-
|
|
84
|
+
const isError = data.includes('||'), e = (0, lint_1.generateForChild)(child, { start }, 'pipe-like', 'additional "|" in a table cell', isError ? 'error' : 'warning');
|
|
85
|
+
if (isError) {
|
|
86
|
+
const syntax = { caption: '|+', td: '|', th: '!' }[this.subtype];
|
|
87
|
+
e.fix = {
|
|
88
|
+
range: [e.startIndex, e.endIndex],
|
|
89
|
+
text: data.replace(/\|\|/gu, `\n${syntax}`),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
e.suggestions = [
|
|
94
|
+
{
|
|
95
|
+
desc: 'escape',
|
|
96
|
+
range: [e.startIndex, e.endIndex],
|
|
97
|
+
text: data.replace(/\|/gu, '|'),
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
errors.push(e);
|
|
85
102
|
}
|
|
86
103
|
}
|
|
87
104
|
}
|
package/dist/src/table/trBase.js
CHANGED
|
@@ -27,13 +27,11 @@ class TrBaseToken extends base_1.TableBaseToken {
|
|
|
27
27
|
catch { }
|
|
28
28
|
}
|
|
29
29
|
const error = (0, lint_1.generateForChild)(inter, { start }, 'fostered-content', 'content to be moved out from the table');
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
startCol: 0,
|
|
36
|
-
});
|
|
30
|
+
error.severity = first.type === 'template' ? 'warning' : 'error';
|
|
31
|
+
error.startIndex++;
|
|
32
|
+
error.startLine++;
|
|
33
|
+
error.startCol = 0;
|
|
34
|
+
errors.push(error);
|
|
37
35
|
return errors;
|
|
38
36
|
}
|
|
39
37
|
}
|
|
@@ -22,9 +22,15 @@ class IncludeToken extends (0, hidden_1.hiddenToken)(index_2.TagPairToken) {
|
|
|
22
22
|
}
|
|
23
23
|
/** @override */
|
|
24
24
|
lint(start = this.getAbsoluteIndex()) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
if (this.closed) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
const e = (0, lint_1.generateForSelf)(this, { start }, 'unclosed-comment', index_1.default.msg('unclosed $1', `<${this.name}>`));
|
|
29
|
+
e.fix = {
|
|
30
|
+
range: [e.endIndex, e.endIndex],
|
|
31
|
+
text: `</${this.name}>`,
|
|
32
|
+
};
|
|
33
|
+
return [e];
|
|
28
34
|
}
|
|
29
35
|
}
|
|
30
36
|
exports.IncludeToken = IncludeToken;
|
package/dist/src/transclude.js
CHANGED
|
@@ -166,7 +166,14 @@ class TranscludeToken extends index_2.Token {
|
|
|
166
166
|
const title = this.#getTitle();
|
|
167
167
|
if (title.fragment !== undefined) {
|
|
168
168
|
rect = { start, ...this.getRootNode().posFromIndex(start) };
|
|
169
|
-
|
|
169
|
+
const child = childNodes[type === 'template' ? 0 : 1], e = (0, lint_1.generateForChild)(child, rect, 'no-ignored', 'useless fragment'), textNode = child.childNodes.find((c) => c.type === 'text' && c.data.includes('#'));
|
|
170
|
+
if (textNode) {
|
|
171
|
+
e.fix = {
|
|
172
|
+
range: [e.startIndex + textNode.getRelativeIndex() + textNode.data.indexOf('#'), e.endIndex],
|
|
173
|
+
text: '',
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
errors.push(e);
|
|
170
177
|
}
|
|
171
178
|
if (!title.valid) {
|
|
172
179
|
rect ??= { start, ...this.getRootNode().posFromIndex(start) };
|
package/dist/util/diff.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.info = exports.error = exports.diff = exports.cmd = void 0;
|
|
4
4
|
const fs = require("fs/promises");
|
|
5
5
|
const child_process_1 = require("child_process");
|
|
6
|
+
const chalk = require("chalk");
|
|
6
7
|
process.on('unhandledRejection', e => {
|
|
7
8
|
console.error(e);
|
|
8
9
|
});
|
|
@@ -72,11 +73,11 @@ const diff = async (oldStr, newStr, uid = -1) => {
|
|
|
72
73
|
exports.diff = diff;
|
|
73
74
|
/** @implements */
|
|
74
75
|
const error = (msg, ...args) => {
|
|
75
|
-
console.error(
|
|
76
|
+
console.error(chalk.red(msg), ...args);
|
|
76
77
|
};
|
|
77
78
|
exports.error = error;
|
|
78
79
|
/** @implements */
|
|
79
80
|
const info = (msg, ...args) => {
|
|
80
|
-
console.info(
|
|
81
|
+
console.info(chalk.green(msg), ...args);
|
|
81
82
|
};
|
|
82
83
|
exports.info = info;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wikilint",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "A Node.js linter for MediaWiki markup",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mediawiki",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"@typescript-eslint/eslint-plugin": "^6.19.1",
|
|
52
52
|
"@typescript-eslint/parser": "^6.19.1",
|
|
53
53
|
"ajv-cli": "^5.0.0",
|
|
54
|
+
"chalk": "^4.1.2",
|
|
54
55
|
"eslint": "^8.56.0",
|
|
55
56
|
"eslint-plugin-es-x": "^7.5.0",
|
|
56
57
|
"eslint-plugin-eslint-comments": "^3.2.0",
|