wikilint 2.4.4 → 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/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
  [![CodeQL](https://github.com/bhsd-harry/wikiparser-node/actions/workflows/codeql.yml/badge.svg)](https://github.com/bhsd-harry/wikiparser-node/actions/workflows/codeql.yml)
3
3
  [![CI](https://github.com/bhsd-harry/wikiparser-node/actions/workflows/node.js.yml/badge.svg)](https://github.com/bhsd-harry/wikiparser-node/actions/workflows/node.js.yml)
4
4
 
5
- # wikilint
5
+ # WikiLint
6
6
 
7
- This is a minimal version of [wikiparser-node](https://www.npmjs.com/package/wikiparser-node) customized for [eslint-plugin-wikitext](https://www.npmjs.com/package/eslint-plugin-wikitext).
7
+ This is a minimal version of [WikiParser-Node](https://www.npmjs.com/package/wikiparser-node) customized for [eslint-plugin-wikitext](https://www.npmjs.com/package/eslint-plugin-wikitext).
8
8
 
9
9
  You can also directly lint Wikitext articles in the command line using this package:
10
10
 
@@ -19,7 +19,7 @@ npx wikilint --config zhwiki --include --lang zh-hans *.wiki
19
19
  | `-c`, `--config` \<path or preset config\> | Choose parser's configuration | `default` |
20
20
  | `-h`, `--help` | Print available options | |
21
21
  | `-i`, `--include` | Parse for inclusion | no inclusion |
22
- | `-l`, `--lang` | Choose i18n language | English
22
+ | `-l`, `--lang` | Choose i18n language | English |
23
23
  | `-q`, `--quiet` | Report errors only | errors and warnings |
24
24
  | `-s`, `--strict` | Exit when there is an error or warning<br>Override `-q` or `--quiet` | Exit `1` only where there is an error |
25
25
  | `-v`, `--version` | Print package version | |
package/dist/base.d.ts CHANGED
@@ -10,16 +10,28 @@ export interface Config {
10
10
  readonly variants: string[];
11
11
  readonly excludes?: string[];
12
12
  }
13
- export type Severity = 'error' | 'warning';
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
+ }
14
21
  export interface LintError {
15
- readonly message: string;
16
- readonly severity: Severity;
17
- readonly startIndex: number;
18
- readonly endIndex: number;
19
- readonly startLine: number;
20
- readonly startCol: number;
21
- readonly endLine: number;
22
- readonly endCol: number;
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
+ })[];
23
35
  }
24
36
  /** 类似Node */
25
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(['default', 'zhwiki', 'moegirl', 'llwiki']), { argv } = process, files = [];
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 > 1 ? 's' : ''}`;
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) => `\x1B[${severity === 'error' ? 31 : 33}m${severity}\x1B[0m`.padEnd(16);
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
- const wikitext = (0, fs_1.readFileSync)(file, 'utf8');
109
- let problems = Parser.parse(wikitext, include).lint();
110
- const errors = problems.filter(({ severity }) => severity === 'error'), { length: nLocalErr } = errors, nLocalWarn = problems.length - nLocalErr;
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('\x1B[4m%s\x1B[0m', (0, path_1.resolve)(file));
119
- const { length: maxLineChars } = String(Math.max(...problems.map(({ startLine }) => startLine))), { length: maxColChars } = String(Math.max(...problems.map(({ startCol }) => startCol)));
120
- for (const { message, severity, startLine, startCol } of problems) {
121
- console.error(` ${String(startLine).padStart(maxLineChars)}:${String(startCol).padEnd(maxColChars)} ${coloredSeverity(severity)} ${message}`);
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));
139
+ for (const { rule, message, severity, startLine, startCol } of problems) {
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('\x1B[1;31m%s\x1B[0m\n', `✖ ${plural(nErr + nWarn, 'problem')} (${plural(nErr, 'error')}, ${plural(nWarn, 'warning')})`);
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
@@ -17,6 +17,13 @@ errorSyntax = new RegExp(`${source}|https?[:/]\\/+`, 'giu'), errorSyntaxUrl = ne
17
17
  '{': /[{}]/u,
18
18
  ']': /[[\]](?=[^[\]]*$)/u,
19
19
  '}': /[{}](?=[^{}]*$)/u,
20
+ }, ruleMap = {
21
+ '<': 'tag-like',
22
+ '[': 'lonely-bracket',
23
+ '{': 'lonely-bracket',
24
+ ']': 'lonely-bracket',
25
+ '}': 'lonely-bracket',
26
+ h: 'lonely-http',
20
27
  }, disallowedTags = [
21
28
  'html',
22
29
  'head',
@@ -73,8 +80,11 @@ class AstText extends node_1.AstNode {
73
80
  */
74
81
  lint(start = this.getAbsoluteIndex()) {
75
82
  const { data, parentNode, nextSibling, previousSibling } = this;
83
+ if (!parentNode) {
84
+ return [];
85
+ }
76
86
  const { NowikiToken } = require('../src/nowiki');
77
- 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';
78
88
  let errorRegex;
79
89
  if (type === 'ext-inner' && (name === 'pre' || parentNode instanceof NowikiToken)) {
80
90
  errorRegex = new RegExp(`<\\s*(?:\\/\\s*)${nowiki ? '' : '?'}(${name})\\b`, 'giu');
@@ -82,7 +92,7 @@ class AstText extends node_1.AstNode {
82
92
  else if (type === 'free-ext-link'
83
93
  || type === 'ext-link-url'
84
94
  || type === 'image-parameter' && name === 'link'
85
- || type === 'attr-value') {
95
+ || isHtmlAttrVal) {
86
96
  errorRegex = errorSyntaxUrl;
87
97
  }
88
98
  else {
@@ -114,14 +124,15 @@ class AstText extends node_1.AstNode {
114
124
  else if (char === ']' && (index || length > 1)) {
115
125
  errorRegex.lastIndex--;
116
126
  }
117
- 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 ?? ''))
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 === ']'))
118
129
  || char === '{' && (nextChar === char || previousChar === '-')
119
130
  || char === '}' && (previousChar === char || nextChar === '-')
120
131
  || char === '[' && (nextChar === char
121
132
  || type === 'ext-link-text'
122
- || !data.slice(index + 1).trim() && nextType === 'free-ext-link')
133
+ || nextType === 'free-ext-link' && !data.slice(index + 1).trim())
123
134
  || char === ']' && (previousChar === char
124
- || !data.slice(0, index).trim() && previousType === 'free-ext-link')
135
+ || previousType === 'free-ext-link' && !data.slice(0, index).includes(']'))
125
136
  ? 'error'
126
137
  : 'warning';
127
138
  const leftBracket = char === '{' || char === '[', rightBracket = char === ']' || char === '}';
@@ -142,8 +153,8 @@ class AstText extends node_1.AstNode {
142
153
  }
143
154
  }
144
155
  }
145
- 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;
146
- 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 = {
157
+ rule: ruleMap[char],
147
158
  message: index_1.default.msg('lonely "$1"', char === 'h' ? error : char),
148
159
  severity,
149
160
  startIndex,
@@ -152,7 +163,45 @@ class AstText extends node_1.AstNode {
152
163
  endLine: startLine,
153
164
  startCol,
154
165
  endCol: startCol + length,
155
- });
166
+ };
167
+ if (char === '<') {
168
+ e.suggestions = [
169
+ {
170
+ desc: 'escape',
171
+ range: [startIndex, startIndex + 1],
172
+ text: '&lt;',
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: '&#93;',
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);
156
205
  }
157
206
  return errors;
158
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
- return [(0, lint_1.generateForSelf)(this, { start }, 'unexpected template argument')];
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 { childNodes: [argName, argDefault, ...rest] } = this, errors = argName.lint(start + 3);
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 error = (0, lint_1.generateForChild)(child, rect, 'invisible content inside triple braces');
69
- return {
70
- ...error,
71
- startIndex: error.startIndex - 1,
72
- startCol: error.startCol - 1,
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;
@@ -260,12 +260,25 @@ class AttributeToken extends index_2.Token {
260
260
  if (!balanced) {
261
261
  const root = this.getRootNode();
262
262
  rect = { start, ...root.posFromIndex(start) };
263
- const e = (0, lint_1.generateForChild)(lastChild, rect, index_1.default.msg('unclosed $1', 'quotes'), 'warning');
264
- errors.push({
265
- ...e,
266
- startIndex: e.startIndex - 1,
267
- startCol: e.startCol - 1,
268
- });
263
+ const e = (0, lint_1.generateForChild)(lastChild, rect, 'unclosed-quote', index_1.default.msg('unclosed $1', 'quotes'), 'warning');
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)
@@ -274,19 +287,32 @@ class AttributeToken extends index_2.Token {
274
287
  && !/^(?:xmlns:[\w:.-]+|data-[^:]*)$/u.test(name)
275
288
  && (tag === 'meta' || tag === 'link' || !commonHtmlAttrs.has(name))) {
276
289
  rect ??= { start, ...this.getRootNode().posFromIndex(start) };
277
- errors.push((0, lint_1.generateForChild)(firstChild, rect, 'illegal attribute name'));
290
+ errors.push((0, lint_1.generateForChild)(firstChild, rect, 'illegal-attr', 'illegal attribute name'));
278
291
  }
279
292
  else if (obsoleteAttrs[tag]?.has(name)) {
280
293
  rect ??= { start, ...this.getRootNode().posFromIndex(start) };
281
- errors.push((0, lint_1.generateForChild)(firstChild, rect, 'obsolete attribute', 'warning'));
294
+ errors.push((0, lint_1.generateForChild)(firstChild, rect, 'obsolete-attr', 'obsolete attribute', 'warning'));
282
295
  }
283
296
  else if (name === 'style' && typeof value === 'string' && insecureStyle.test(value)) {
284
297
  rect ??= { start, ...this.getRootNode().posFromIndex(start) };
285
- errors.push((0, lint_1.generateForChild)(lastChild, rect, 'insecure style'));
298
+ errors.push((0, lint_1.generateForChild)(lastChild, rect, 'insecure-style', 'insecure style'));
286
299
  }
287
300
  else if (name === 'tabindex' && typeof value === 'string' && value.trim() !== '0') {
288
301
  rect ??= { start, ...this.getRootNode().posFromIndex(start) };
289
- errors.push((0, lint_1.generateForChild)(lastChild, rect, 'nonzero tabindex'));
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
  }
@@ -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
- errors.push((0, lint_1.generateForSelf)(this, rect, 'attributes of a closing tag'));
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
- errors.push((0, lint_1.generateForChild)(attr, rect, 'containing invalid attribute'));
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;
@@ -122,7 +132,7 @@ class AttributesToken extends index_2.Token {
122
132
  if (duplicated.size > 0) {
123
133
  rect ??= { start, ...this.getRootNode().posFromIndex(start) };
124
134
  for (const key of duplicated) {
125
- errors.push(...attrs.get(key).map(attr => (0, lint_1.generateForChild)(attr, rect, index_1.default.msg('duplicated $1 attribute', key))));
135
+ errors.push(...attrs.get(key).map(attr => (0, lint_1.generateForChild)(attr, rect, 'no-duplicate', index_1.default.msg('duplicated $1 attribute', key))));
126
136
  }
127
137
  }
128
138
  return errors;
@@ -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
- errors.push((0, lint_1.generateForChild)(child, rect, 'invalid conversion flag'));
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;
@@ -53,7 +53,7 @@ class ExtLinkToken extends index_2.Token {
53
53
  lint(start = this.getAbsoluteIndex()) {
54
54
  const errors = super.lint(start);
55
55
  if (this.length === 1 && this.closest('heading-title')) {
56
- errors.push((0, lint_1.generateForSelf)(this, { start }, 'variable anchor in a section header'));
56
+ errors.push((0, lint_1.generateForSelf)(this, { start }, 'var-anchor', 'variable anchor in a section header'));
57
57
  }
58
58
  return errors;
59
59
  }
@@ -58,6 +58,7 @@ class GalleryToken extends index_2.Token {
58
58
  const child = this.childNodes[i], str = String(child), { length } = str, trimmed = str.trim(), startLine = top + i, startCol = i ? 0 : left;
59
59
  if (child.type === 'noinclude' && trimmed && !/^<!--.*-->$/u.test(trimmed)) {
60
60
  errors.push({
61
+ rule: 'no-ignored',
61
62
  message: index_1.default.msg('invalid content in <$1>', 'gallery'),
62
63
  severity: 'error',
63
64
  startIndex: start,
@@ -66,6 +67,18 @@ class GalleryToken extends index_2.Token {
66
67
  endLine: startLine,
67
68
  startCol,
68
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
+ ],
69
82
  });
70
83
  }
71
84
  else if (child.type !== 'noinclude' && child.type !== 'text') {
@@ -57,23 +57,23 @@ class HeadingToken extends index_2.Token {
57
57
  let rect;
58
58
  if (this.level === 1) {
59
59
  rect = { start, ...this.getRootNode().posFromIndex(start) };
60
- errors.push((0, lint_1.generateForChild)(firstChild, rect, '<h1>'));
60
+ errors.push((0, lint_1.generateForChild)(firstChild, rect, 'h1', '<h1>'));
61
61
  }
62
62
  if (innerStr.startsWith('=') || innerStr.endsWith('=')) {
63
63
  rect ??= { start, ...this.getRootNode().posFromIndex(start) };
64
- errors.push((0, lint_1.generateForChild)(firstChild, rect, index_1.default.msg('unbalanced $1 in a section header', '"="')));
64
+ errors.push((0, lint_1.generateForChild)(firstChild, rect, 'unbalanced-header', index_1.default.msg('unbalanced $1 in a section header', '"="')));
65
65
  }
66
66
  if (this.closest('html-attrs, table-attrs')) {
67
67
  rect ??= { start, ...this.getRootNode().posFromIndex(start) };
68
- errors.push((0, lint_1.generateForSelf)(this, rect, 'section header in a HTML tag'));
68
+ errors.push((0, lint_1.generateForSelf)(this, rect, 'parsing-order', 'section header in a HTML tag'));
69
69
  }
70
70
  if (boldQuotes.length % 2) {
71
71
  rect ??= { start, ...this.getRootNode().posFromIndex(start) };
72
- errors.push((0, lint_1.generateForChild)(boldQuotes[boldQuotes.length - 1], { ...rect, start: start + level, left: rect.left + level }, index_1.default.msg('unbalanced $1 in a section header', 'bold apostrophes')));
72
+ errors.push((0, lint_1.generateForChild)(boldQuotes[boldQuotes.length - 1], { ...rect, start: start + level, left: rect.left + level }, 'format-leakage', index_1.default.msg('unbalanced $1 in a section header', 'bold apostrophes')));
73
73
  }
74
74
  if (italicQuotes.length % 2) {
75
75
  rect ??= { start, ...this.getRootNode().posFromIndex(start) };
76
- errors.push((0, lint_1.generateForChild)(italicQuotes[italicQuotes.length - 1], { start: start + level }, index_1.default.msg('unbalanced $1 in a section header', 'italic apostrophes')));
76
+ errors.push((0, lint_1.generateForChild)(italicQuotes[italicQuotes.length - 1], { start: start + level }, 'format-leakage', index_1.default.msg('unbalanced $1 in a section header', 'italic apostrophes')));
77
77
  }
78
78
  return errors;
79
79
  }
package/dist/src/html.js CHANGED
@@ -79,13 +79,14 @@ class HtmlToken extends index_2.Token {
79
79
  const errors = super.lint(start);
80
80
  let refError;
81
81
  if (this.name === 'h1' && !this.closing) {
82
- refError = (0, lint_1.generateForSelf)(this, { start }, '<h1>');
82
+ refError = (0, lint_1.generateForSelf)(this, { start }, 'h1', '<h1>');
83
83
  errors.push(refError);
84
84
  }
85
85
  if (this.closest('table-attrs')) {
86
- refError ??= (0, lint_1.generateForSelf)(this, { start }, '');
86
+ refError ??= (0, lint_1.generateForSelf)(this, { start }, 'h1', '');
87
87
  errors.push({
88
88
  ...refError,
89
+ rule: 'parsing-order',
89
90
  message: index_1.default.msg('HTML tag in table attributes'),
90
91
  });
91
92
  }
@@ -95,8 +96,8 @@ class HtmlToken extends index_2.Token {
95
96
  catch (e) {
96
97
  if (e instanceof SyntaxError) {
97
98
  const { message } = e;
98
- refError ??= (0, lint_1.generateForSelf)(this, { start }, '');
99
- const [msg] = message.split(':'), error = { ...refError, message: index_1.default.msg(msg) };
99
+ refError ??= (0, lint_1.generateForSelf)(this, { start }, 'h1', '');
100
+ const [msg] = message.split(':'), error = { ...refError, rule: 'unmatched-tag', message: index_1.default.msg(msg) };
100
101
  if (msg === 'unclosed tag' && !this.closest('heading-title')) {
101
102
  if (formattingTags.has(this.name)) {
102
103
  const childNodes = this.parentNode?.childNodes, i = childNodes?.indexOf(this);
@@ -113,22 +114,42 @@ class HtmlToken extends index_2.Token {
113
114
  if (ancestor && magicWords.has(ancestor.name)) {
114
115
  error.severity = 'warning';
115
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
+ }
116
135
  }
117
136
  errors.push(error);
118
137
  }
119
138
  }
120
139
  if (obsoleteTags.has(this.name)) {
121
- refError ??= (0, lint_1.generateForSelf)(this, { start }, '');
140
+ refError ??= (0, lint_1.generateForSelf)(this, { start }, 'h1', '');
122
141
  errors.push({
123
142
  ...refError,
143
+ rule: 'obsolete-tag',
124
144
  message: index_1.default.msg('obsolete HTML tag'),
125
145
  severity: 'warning',
126
146
  });
127
147
  }
128
148
  if ((this.name === 'b' || this.name === 'strong') && this.closest('heading-title')) {
129
- refError ??= (0, lint_1.generateForSelf)(this, { start }, '');
149
+ refError ??= (0, lint_1.generateForSelf)(this, { start }, 'h1', '');
130
150
  errors.push({
131
151
  ...refError,
152
+ rule: 'bold-header',
132
153
  message: index_1.default.msg('bold in section header'),
133
154
  severity: 'warning',
134
155
  });
@@ -102,10 +102,15 @@ 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
- errors.push((0, lint_1.generateForSelf)(this, { start }, 'invalid gallery image parameter'));
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
- errors.push((0, lint_1.generateForSelf)(this, { start }, 'unnecessary URL encoding in an internal link'));
113
+ errors.push((0, lint_1.generateForSelf)(this, { start }, 'url-encoding', 'unnecessary URL encoding in an internal link'));
109
114
  }
110
115
  return errors;
111
116
  }
@@ -92,10 +92,10 @@ class ImagemapToken extends index_2.Token {
92
92
  errors.push(...this.childNodes.filter(child => {
93
93
  const str = String(child).trim();
94
94
  return child.type === 'noinclude' && str && !str.startsWith('#');
95
- }).map(child => (0, lint_1.generateForChild)(child, rect, 'invalid link in <imagemap>')));
95
+ }).map(child => (0, lint_1.generateForChild)(child, rect, 'invalid-imagemap', 'invalid link in <imagemap>')));
96
96
  }
97
97
  else {
98
- errors.push((0, lint_1.generateForSelf)(this, rect, '<imagemap> without an image'));
98
+ errors.push((0, lint_1.generateForSelf)(this, rect, 'invalid-imagemap', '<imagemap> without an image'));
99
99
  }
100
100
  return errors;
101
101
  }
@@ -76,20 +76,40 @@ class LinkBaseToken extends index_2.Token {
76
76
  let rect;
77
77
  if (target.childNodes.some(({ type }) => type === 'template')) {
78
78
  rect = { start, ...this.getRootNode().posFromIndex(start) };
79
- errors.push((0, lint_1.generateForChild)(target, rect, 'template in an internal link target', 'warning'));
79
+ errors.push((0, lint_1.generateForChild)(target, rect, 'unknown-page', 'template in an internal link target', 'warning'));
80
80
  }
81
81
  if (encoded) {
82
82
  rect ??= { start, ...this.getRootNode().posFromIndex(start) };
83
- errors.push((0, lint_1.generateForChild)(target, rect, 'unnecessary URL encoding in an internal link'));
83
+ errors.push((0, lint_1.generateForChild)(target, rect, 'url-encoding', 'unnecessary URL encoding in an internal link'));
84
84
  }
85
- if ((linkType === 'link' || linkType === 'category')
86
- && linkText?.childNodes.some(({ type, data }) => type === 'text' && data.includes('|'))) {
87
- rect ??= { start, ...this.getRootNode().posFromIndex(start) };
88
- errors.push((0, lint_1.generateForChild)(linkText, rect, 'additional "|" in the link text', 'warning'));
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, '&#124;'),
98
+ },
99
+ ];
100
+ errors.push(e);
101
+ }
89
102
  }
90
- else if (linkType !== 'link' && fragment !== undefined) {
103
+ if (linkType !== 'link' && fragment !== undefined) {
91
104
  rect ??= { start, ...this.getRootNode().posFromIndex(start) };
92
- errors.push((0, lint_1.generateForChild)(target, rect, 'useless fragment'));
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
  }
@@ -58,7 +58,7 @@ class FileToken extends base_1.LinkBaseToken {
58
58
  return visibleNodes.length !== 1 || visibleNodes[0].type !== 'arg';
59
59
  }), keys = [...new Set(args.map(({ name }) => name))].filter(key => key !== 'invalid'), frameKeys = keys.filter(key => frame.has(key)), horizAlignKeys = keys.filter(key => horizAlign.has(key)), vertAlignKeys = keys.filter(key => vertAlign.has(key));
60
60
  if (this.closest('ext-link-text') && this.getValue('link')?.trim() !== '') {
61
- errors.push((0, lint_1.generateForSelf)(this, { start }, 'internal link in an external link'));
61
+ errors.push((0, lint_1.generateForSelf)(this, { start }, 'nested-link', 'internal link in an external link'));
62
62
  }
63
63
  if (args.length === keys.length
64
64
  && frameKeys.length < 2
@@ -72,7 +72,7 @@ class FileToken extends base_1.LinkBaseToken {
72
72
  * @param msg 消息键
73
73
  * @param p1 替换$1
74
74
  */
75
- const generate = (msg, p1) => (arg) => (0, lint_1.generateForChild)(arg, rect, index_1.default.msg(`${msg} image $1 parameter`, p1));
75
+ const generate = (msg, p1) => (arg) => (0, lint_1.generateForChild)(arg, rect, 'no-duplicate', index_1.default.msg(`${msg} image $1 parameter`, p1));
76
76
  for (const key of keys) {
77
77
  let relevantArgs = args.filter(({ name }) => name === key);
78
78
  if (key === 'caption') {
@@ -40,7 +40,7 @@ class GalleryImageToken extends file_1.FileToken {
40
40
  lint(start = this.getAbsoluteIndex()) {
41
41
  const errors = super.lint(start), { ns, interwiki } = this.getAttribute('title');
42
42
  if (interwiki || ns !== 6) {
43
- errors.push((0, lint_1.generateForSelf)(this, { start }, 'invalid gallery image'));
43
+ errors.push((0, lint_1.generateForSelf)(this, { start }, 'invalid-gallery', 'invalid gallery image'));
44
44
  }
45
45
  return errors;
46
46
  }
@@ -13,7 +13,7 @@ class LinkToken extends base_1.LinkBaseToken {
13
13
  lint(start = this.getAbsoluteIndex()) {
14
14
  const errors = super.lint(start);
15
15
  if (this.closest('ext-link-text')) {
16
- errors.push((0, lint_1.generateForSelf)(this, { start }, 'internal link in an external link'));
16
+ errors.push((0, lint_1.generateForSelf)(this, { start }, 'nested-link', 'internal link in an external link'));
17
17
  }
18
18
  return errors;
19
19
  }
@@ -27,20 +27,44 @@ class MagicLinkToken extends index_2.Token {
27
27
  continue;
28
28
  }
29
29
  rect ??= { start, ...this.getRootNode().posFromIndex(start) };
30
- const refError = (0, lint_1.generateForChild)(child, rect, '', 'warning');
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
- errors.push({
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', s.startsWith('|') ? '"|"' : 'full-width punctuation'),
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;
@@ -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
- return (0, lint_1.generateForChild)(child, rect, index_1.default.msg('invalid content in <$1>', this.name));
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,7 +20,15 @@ class CommentToken extends (0, hidden_1.hiddenToken)(base_1.NowikiBaseToken) {
20
20
  }
21
21
  /** @override */
22
22
  lint(start = this.getAbsoluteIndex()) {
23
- return this.closed ? [] : [(0, lint_1.generateForSelf)(this, { start }, index_1.default.msg('unclosed $1', 'HTML comment'))];
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];
24
32
  }
25
33
  /** @private */
26
34
  toString() {
@@ -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
- return (name === 'templatestyles' || name === 'section') && data
14
- ? [(0, lint_1.generateForSelf)(this, { start }, index_1.default.msg('nothing should be in <$1>', name))]
15
- : super.lint(start);
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;
@@ -20,7 +20,7 @@ class QuoteToken extends base_1.NowikiBaseToken {
20
20
  const { previousSibling, nextSibling, bold } = this, message = index_1.default.msg('lonely "$1"', `'`), errors = [];
21
21
  let refError;
22
22
  if (previousSibling?.type === 'text' && previousSibling.data.endsWith(`'`)) {
23
- refError = (0, lint_1.generateForSelf)(this, { start }, message);
23
+ refError = (0, lint_1.generateForSelf)(this, { start }, 'lonely-apos', message);
24
24
  const { startIndex: endIndex, startLine: endLine, startCol: endCol } = refError, [, { length }] = /(?:^|[^'])('+)$/u.exec(previousSibling.data), startIndex = start - length;
25
25
  errors.push({
26
26
  ...refError,
@@ -29,10 +29,17 @@ 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: '&apos;'.repeat(length),
37
+ },
38
+ ],
32
39
  });
33
40
  }
34
41
  if (nextSibling?.type === 'text' && nextSibling.data.startsWith(`'`)) {
35
- refError ??= (0, lint_1.generateForSelf)(this, { start }, message);
42
+ refError ??= (0, lint_1.generateForSelf)(this, { start }, 'lonely-apos', message);
36
43
  const { endIndex: startIndex, endLine: startLine, endCol: startCol } = refError, [{ length }] = /^'+/u.exec(nextSibling.data), endIndex = startIndex + length;
37
44
  errors.push({
38
45
  ...refError,
@@ -41,12 +48,20 @@ 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: '&apos;'.repeat(length),
56
+ },
57
+ ],
44
58
  });
45
59
  }
46
60
  if (bold && this.closest('heading-title')) {
47
- refError ??= (0, lint_1.generateForSelf)(this, { start }, message);
61
+ refError ??= (0, lint_1.generateForSelf)(this, { start }, 'lonely-apos', message);
48
62
  errors.push({
49
63
  ...refError,
64
+ rule: 'bold-header',
50
65
  message: index_1.default.msg('bold in section header'),
51
66
  severity: 'warning',
52
67
  });
@@ -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
- return (0, lint_1.generateForChild)(child, rect, index_1.default.msg('invalid parameter of <$1>', this.name));
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
  }
@@ -60,15 +60,17 @@ class ParameterToken extends index_2.Token {
60
60
  /https?:\/\/(?:\[[\da-f:.]+\]|[^[\]<>"\t\n\p{Zs}])(?:[^[\]<>"\0\t\n\p{Zs}]|\0\d+c\x7F)*$/iu;
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
- const e = (0, lint_1.generateForChild)(firstChild, { start }, 'unescaped query string in an anonymous parameter');
64
- errors.push({
65
- ...e,
66
- startIndex: e.endIndex,
67
- endIndex: e.endIndex + 1,
68
- startLine: e.endLine,
69
- startCol: e.endCol,
70
- endCol: e.endCol + 1,
71
- });
63
+ const e = (0, lint_1.generateForChild)(firstChild, { start }, 'unescaped', 'unescaped query string in an anonymous parameter');
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
  }
@@ -28,7 +28,7 @@ class TableToken extends trBase_1.TrBaseToken {
28
28
  lint(start = this.getAbsoluteIndex()) {
29
29
  const errors = super.lint(start);
30
30
  if (!this.closed) {
31
- errors.push((0, lint_1.generateForChild)(this.firstChild, { start }, index_1.default.msg('unclosed $1', 'table')));
31
+ errors.push((0, lint_1.generateForChild)(this.firstChild, { start }, 'unclosed-table', index_1.default.msg('unclosed $1', 'table')));
32
32
  }
33
33
  return errors;
34
34
  }
@@ -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
- errors.push((0, lint_1.generateForChild)(child, { start }, 'additional "|" in a table cell', data.includes('||') ? 'error' : 'warning'));
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, '&#124;'),
98
+ },
99
+ ];
100
+ }
101
+ errors.push(e);
85
102
  }
86
103
  }
87
104
  }
@@ -26,14 +26,12 @@ class TrBaseToken extends base_1.TableBaseToken {
26
26
  }
27
27
  catch { }
28
28
  }
29
- const error = (0, lint_1.generateForChild)(inter, { start }, 'content to be moved out from the table');
30
- errors.push({
31
- ...error,
32
- severity: first.type === 'template' ? 'warning' : 'error',
33
- startIndex: error.startIndex + 1,
34
- startLine: error.startLine + 1,
35
- startCol: 0,
36
- });
29
+ const error = (0, lint_1.generateForChild)(inter, { start }, 'fostered-content', 'content to be moved out from the table');
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
  }
@@ -124,11 +124,11 @@ class ExtToken extends index_3.TagPairToken {
124
124
  let rect;
125
125
  if (this.name !== 'nowiki' && this.closest('html-attrs, table-attrs')) {
126
126
  rect = { start, ...this.getRootNode().posFromIndex(start) };
127
- errors.push((0, lint_1.generateForSelf)(this, rect, 'extension tag in HTML tag attributes'));
127
+ errors.push((0, lint_1.generateForSelf)(this, rect, 'parsing-order', 'extension tag in HTML tag attributes'));
128
128
  }
129
129
  if (this.name === 'ref' && this.closest('heading-title')) {
130
130
  rect ??= { start, ...this.getRootNode().posFromIndex(start) };
131
- errors.push((0, lint_1.generateForSelf)(this, rect, 'variable anchor in a section header'));
131
+ errors.push((0, lint_1.generateForSelf)(this, rect, 'var-anchor', 'variable anchor in a section header'));
132
132
  }
133
133
  return errors;
134
134
  }
@@ -22,7 +22,15 @@ class IncludeToken extends (0, hidden_1.hiddenToken)(index_2.TagPairToken) {
22
22
  }
23
23
  /** @override */
24
24
  lint(start = this.getAbsoluteIndex()) {
25
- return this.closed ? [] : [(0, lint_1.generateForSelf)(this, { start }, index_1.default.msg('unclosed $1', `<${this.name}>`))];
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];
26
34
  }
27
35
  }
28
36
  exports.IncludeToken = IncludeToken;
@@ -166,21 +166,28 @@ 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
- errors.push((0, lint_1.generateForChild)(childNodes[type === 'template' ? 0 : 1], rect, 'useless fragment'));
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) };
173
- errors.push((0, lint_1.generateForChild)(childNodes[1], rect, 'illegal module name'));
180
+ errors.push((0, lint_1.generateForChild)(childNodes[1], rect, 'invalid-invoke', 'illegal module name'));
174
181
  }
175
182
  if (type === 'magic-word' && length === 2) {
176
183
  rect ??= { start, ...this.getRootNode().posFromIndex(start) };
177
- errors.push((0, lint_1.generateForSelf)(this, rect, 'missing module function'));
184
+ errors.push((0, lint_1.generateForSelf)(this, rect, 'invalid-invoke', 'missing module function'));
178
185
  return errors;
179
186
  }
180
187
  const duplicatedArgs = this.getDuplicatedArgs();
181
188
  if (duplicatedArgs.length > 0) {
182
189
  rect ??= { start, ...this.getRootNode().posFromIndex(start) };
183
- errors.push(...duplicatedArgs.flatMap(([, args]) => args).map(arg => (0, lint_1.generateForChild)(arg, rect, 'duplicated parameter')));
190
+ errors.push(...duplicatedArgs.flatMap(([, args]) => args).map(arg => (0, lint_1.generateForChild)(arg, rect, 'no-duplicate', 'duplicated parameter')));
184
191
  }
185
192
  return errors;
186
193
  }
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('\x1B[31m%s\x1B[0m', msg, ...args);
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('\x1B[32m%s\x1B[0m', msg, ...args);
81
+ console.info(chalk.green(msg), ...args);
81
82
  };
82
83
  exports.info = info;
package/dist/util/lint.js CHANGED
@@ -6,9 +6,10 @@ const index_1 = require("../index");
6
6
  * 生成lint函数
7
7
  * @param func lint函数
8
8
  */
9
- const factory = (func) => (token, boundingRect, msg, severity = 'error') => {
9
+ const factory = (func) => (token, boundingRect, rule, msg, severity = 'error') => {
10
10
  const { start } = boundingRect, { top, left } = 'top' in boundingRect ? boundingRect : token.getRootNode().posFromIndex(start), { offsetHeight, offsetWidth } = token, { startIndex, startLine, startCol } = func(token, start, top, left);
11
11
  return {
12
+ rule,
12
13
  message: index_1.default.msg(msg),
13
14
  severity,
14
15
  startIndex,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wikilint",
3
- "version": "2.4.4",
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",