wikiparser-node 1.4.5 → 1.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 CHANGED
@@ -13,18 +13,28 @@ export interface Config {
13
13
  readonly conversionTable?: [string, string][];
14
14
  readonly redirects?: [string, string][];
15
15
  }
16
- export type Severity = 'error' | 'warning';
17
- export 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
+ export declare namespace LintError {
17
+ type Severity = 'error' | 'warning';
18
+ 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';
19
+ interface Fix {
20
+ readonly range: [number, number];
21
+ text: string;
22
+ }
23
+ }
18
24
  export interface LintError {
19
- readonly rule: Rule;
20
- readonly message: string;
21
- readonly severity: Severity;
22
- readonly startIndex: number;
23
- readonly endIndex: number;
24
- readonly startLine: number;
25
- readonly startCol: number;
26
- readonly endLine: number;
27
- readonly endCol: number;
25
+ rule: LintError.Rule;
26
+ message: string;
27
+ severity: LintError.Severity;
28
+ startIndex: number;
29
+ endIndex: number;
30
+ startLine: number;
31
+ startCol: number;
32
+ endLine: number;
33
+ endCol: number;
34
+ fix?: LintError.Fix;
35
+ suggestions?: (LintError.Fix & {
36
+ desc: string;
37
+ })[];
28
38
  }
29
39
  export type AST = Record<string, string | number | boolean> & {
30
40
  range: [number, number];
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  /* eslint n/exports-style: 0 */
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
+ const chalk = require("chalk");
5
6
  const debug_1 = require("./util/debug");
6
7
  const constants_1 = require("./util/constants");
7
8
  const string_1 = require("./util/string");
@@ -118,13 +119,13 @@ const Parser = {
118
119
  /** @implements */
119
120
  warn(msg, ...args) {
120
121
  if (this.warning) {
121
- console.warn('\x1B[33m%s\x1B[0m', msg, ...args);
122
+ console.warn(chalk.yellow(msg), ...args);
122
123
  }
123
124
  },
124
125
  /** @implements */
125
126
  debug(msg, ...args) {
126
127
  if (this.debugging) {
127
- console.debug('\x1B[34m%s\x1B[0m', msg, ...args);
128
+ console.debug(chalk.blue(msg), ...args);
128
129
  }
129
130
  },
130
131
  error: diff_1.error,
@@ -147,9 +147,4 @@ export declare abstract class AstElement extends AstNode {
147
147
  */
148
148
  insertBefore(child: string, reference?: AstNodes): AstText;
149
149
  insertBefore<T extends AstNodes>(child: T, reference?: AstNodes): T;
150
- /**
151
- * 输出AST
152
- * @param depth 当前深度
153
- */
154
- echo(depth?: number): void;
155
150
  }
@@ -9,7 +9,6 @@ const constants_1 = require("../util/constants");
9
9
  const selector_1 = require("../parser/selector");
10
10
  const ranges_1 = require("./ranges");
11
11
  const title_1 = require("./title");
12
- const index_1 = require("../index");
13
12
  const node_1 = require("./node");
14
13
  /* NOT FOR BROWSER */
15
14
  /**
@@ -613,43 +612,6 @@ class AstElement extends node_1.AstNode {
613
612
  ? this.insertAt(child)
614
613
  : this.insertAt(child, this.#getChildIndex(reference));
615
614
  }
616
- /**
617
- * 输出AST
618
- * @param depth 当前深度
619
- */
620
- echo(depth = 0) {
621
- const indent = ' '.repeat(depth), str = String(this), { childNodes, type, length } = this;
622
- if (childNodes.every(child => child.type === 'text' || !String(child))) {
623
- console.log(`${indent}\x1B[32m<%s>\x1B[0m${(0, string_1.noWrap)(str)}\x1B[32m</%s>\x1B[0m`, type, type);
624
- return;
625
- }
626
- index_1.default.info(`${indent}<${type}>`);
627
- let i = this.getAttribute('padding');
628
- if (i) {
629
- console.log(`${indent} ${(0, string_1.noWrap)(str.slice(0, i))}`);
630
- }
631
- for (let j = 0; j < length; j++) {
632
- const child = childNodes[j], childStr = String(child), gap = j === length - 1 ? 0 : this.getGaps(j);
633
- if (!childStr) {
634
- //
635
- }
636
- else if (child.type === 'text') {
637
- console.log(`${indent} ${(0, string_1.noWrap)(child.data)}`);
638
- }
639
- else {
640
- child.echo(depth + 1);
641
- }
642
- i += childStr.length;
643
- if (gap) {
644
- console.log(`${indent} ${(0, string_1.noWrap)(str.slice(i, i + gap))}`);
645
- i += gap;
646
- }
647
- }
648
- if (i < str.length) {
649
- console.log(`${indent} ${(0, string_1.noWrap)(str.slice(i))}`);
650
- }
651
- index_1.default.info(`${indent}</${type}>`);
652
- }
653
615
  }
654
616
  exports.AstElement = AstElement;
655
617
  constants_1.classes['AstElement'] = __filename;
package/dist/lib/text.js CHANGED
@@ -116,14 +116,13 @@ class AstText extends node_1.AstNode {
116
116
  */
117
117
  lint(start = this.getAbsoluteIndex()) {
118
118
  const { data, parentNode, nextSibling, previousSibling } = this;
119
- /* NOT FOR BROWSER */
120
119
  if (!parentNode) {
120
+ /* NOT FOR BROWSER */
121
121
  throw new Error('无法对孤立文本节点进行语法分析!');
122
+ /* NOT FOR BROWSER END */
122
123
  }
123
- /* NOT FOR BROWSER END */
124
124
  const { NowikiToken } = require('../src/nowiki');
125
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
126
- const { type, name } = parentNode, nowiki = name === 'nowiki' || name === 'pre';
125
+ const { type, name } = parentNode, nowiki = name === 'nowiki' || name === 'pre', isHtmlAttrVal = type === 'attr-value' && parentNode.parentNode.type !== 'ext-attr';
127
126
  let errorRegex;
128
127
  if (type === 'ext-inner' && (name === 'pre' || parentNode instanceof NowikiToken)) {
129
128
  errorRegex = new RegExp(`<\\s*(?:\\/\\s*)${nowiki ? '' : '?'}(${name})\\b`, 'giu');
@@ -131,7 +130,7 @@ class AstText extends node_1.AstNode {
131
130
  else if (type === 'free-ext-link'
132
131
  || type === 'ext-link-url'
133
132
  || type === 'image-parameter' && name === 'link'
134
- || type === 'attr-value') {
133
+ || isHtmlAttrVal) {
135
134
  errorRegex = errorSyntaxUrl;
136
135
  }
137
136
  else {
@@ -163,14 +162,15 @@ class AstText extends node_1.AstNode {
163
162
  else if (char === ']' && (index || length > 1)) {
164
163
  errorRegex.lastIndex--;
165
164
  }
166
- 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 ?? ''))
165
+ 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 ?? ''))
166
+ || isHtmlAttrVal && (char === '[' || char === ']'))
167
167
  || char === '{' && (nextChar === char || previousChar === '-')
168
168
  || char === '}' && (previousChar === char || nextChar === '-')
169
169
  || char === '[' && (nextChar === char
170
170
  || type === 'ext-link-text'
171
- || !data.slice(index + 1).trim() && nextType === 'free-ext-link')
171
+ || nextType === 'free-ext-link' && !data.slice(index + 1).trim())
172
172
  || char === ']' && (previousChar === char
173
- || !data.slice(0, index).trim() && previousType === 'free-ext-link')
173
+ || previousType === 'free-ext-link' && !data.slice(0, index).includes(']'))
174
174
  ? 'error'
175
175
  : 'warning';
176
176
  const leftBracket = char === '{' || char === '[', rightBracket = char === ']' || char === '}';
@@ -191,8 +191,7 @@ class AstText extends node_1.AstNode {
191
191
  }
192
192
  }
193
193
  }
194
- 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;
195
- errors.push({
194
+ 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 = {
196
195
  rule: ruleMap[char],
197
196
  message: index_1.default.msg('lonely "$1"', char === 'h' ? error : char),
198
197
  severity,
@@ -202,7 +201,45 @@ class AstText extends node_1.AstNode {
202
201
  endLine: startLine,
203
202
  startCol,
204
203
  endCol: startCol + length,
205
- });
204
+ };
205
+ if (char === '<') {
206
+ e.suggestions = [
207
+ {
208
+ desc: 'escape',
209
+ range: [startIndex, startIndex + 1],
210
+ text: '&lt;',
211
+ },
212
+ ];
213
+ }
214
+ else if (char === 'h'
215
+ && !(type === 'ext-link-text' || type === 'link-text')
216
+ && /[\p{L}\d_]/u.test(previousChar || '')) {
217
+ e.suggestions = [
218
+ {
219
+ desc: 'whitespace',
220
+ range: [startIndex, startIndex],
221
+ text: ' ',
222
+ },
223
+ ];
224
+ }
225
+ else if (char === '[' && type === 'ext-link-text') {
226
+ const i = parentNode.getAbsoluteIndex() + String(parentNode).length;
227
+ e.suggestions = [
228
+ {
229
+ desc: 'escape',
230
+ range: [i, i + 1],
231
+ text: '&#93;',
232
+ },
233
+ ];
234
+ }
235
+ else if (char === ']' && previousType === 'free-ext-link' && severity === 'error') {
236
+ const i = start - String(previousSibling).length;
237
+ e.fix = {
238
+ range: [i, i],
239
+ text: '[',
240
+ };
241
+ }
242
+ errors.push(e);
206
243
  }
207
244
  return errors;
208
245
  }
package/dist/src/arg.js CHANGED
@@ -71,22 +71,40 @@ class ArgToken extends index_2.Token {
71
71
  }
72
72
  /** @override */
73
73
  lint(start = this.getAbsoluteIndex()) {
74
+ const { childNodes: [argName, argDefault, ...rest] } = this;
74
75
  if (!this.getAttribute('include')) {
75
- return [(0, lint_1.generateForSelf)(this, { start }, 'no-arg', 'unexpected template argument')];
76
+ const e = (0, lint_1.generateForSelf)(this, { start }, 'no-arg', 'unexpected template argument');
77
+ if (argDefault) {
78
+ e.fix = {
79
+ range: [start, e.endIndex],
80
+ text: argDefault.text(),
81
+ };
82
+ }
83
+ return [e];
76
84
  }
77
- const { childNodes: [argName, argDefault, ...rest] } = this, errors = argName.lint(start + 3);
85
+ const errors = argName.lint(start + 3);
78
86
  if (argDefault) {
79
87
  errors.push(...argDefault.lint(start + 4 + String(argName).length));
80
88
  }
81
89
  if (rest.length > 0) {
82
90
  const rect = { start, ...this.getRootNode().posFromIndex(start) };
83
91
  errors.push(...rest.map(child => {
84
- const error = (0, lint_1.generateForChild)(child, rect, 'no-ignored', 'invisible content inside triple braces');
85
- return {
86
- ...error,
87
- startIndex: error.startIndex - 1,
88
- startCol: error.startCol - 1,
89
- };
92
+ const e = (0, lint_1.generateForChild)(child, rect, 'no-ignored', 'invisible content inside triple braces');
93
+ e.startIndex--;
94
+ e.startCol--;
95
+ e.suggestions = [
96
+ {
97
+ desc: 'remove',
98
+ range: [e.startIndex, e.endIndex],
99
+ text: '',
100
+ },
101
+ {
102
+ desc: 'escape',
103
+ range: [e.startIndex, e.startIndex + 1],
104
+ text: '{{!}}',
105
+ },
106
+ ];
107
+ return e;
90
108
  }));
91
109
  }
92
110
  return errors;
@@ -347,11 +347,24 @@ let AttributeToken = (() => {
347
347
  const root = this.getRootNode();
348
348
  rect = { start, ...root.posFromIndex(start) };
349
349
  const e = (0, lint_1.generateForChild)(lastChild, rect, 'unclosed-quote', index_1.default.msg('unclosed $1', 'quotes'), 'warning');
350
- errors.push({
351
- ...e,
352
- startIndex: e.startIndex - 1,
353
- startCol: e.startCol - 1,
354
- });
350
+ e.startIndex--;
351
+ e.startCol--;
352
+ const fix = {
353
+ range: [e.endIndex, e.endIndex],
354
+ text: this.#quotes[0],
355
+ };
356
+ if (lastChild.childNodes.some(child => child.type === 'text' && /\s/u.test(child.text()))) {
357
+ e.suggestions = [
358
+ {
359
+ desc: 'quote',
360
+ ...fix,
361
+ },
362
+ ];
363
+ }
364
+ else {
365
+ e.fix = fix;
366
+ }
367
+ errors.push(e);
355
368
  }
356
369
  const attrs = extAttrs[tag];
357
370
  if (attrs && !attrs.has(name)
@@ -372,7 +385,20 @@ let AttributeToken = (() => {
372
385
  }
373
386
  else if (name === 'tabindex' && typeof value === 'string' && value.trim() !== '0') {
374
387
  rect ??= { start, ...this.getRootNode().posFromIndex(start) };
375
- errors.push((0, lint_1.generateForChild)(lastChild, rect, 'illegal-attr', 'nonzero tabindex'));
388
+ const e = (0, lint_1.generateForChild)(lastChild, rect, 'illegal-attr', 'nonzero tabindex');
389
+ e.suggestions = [
390
+ {
391
+ desc: 'remove',
392
+ range: [start, e.endIndex],
393
+ text: '',
394
+ },
395
+ {
396
+ desc: '0 tabindex',
397
+ range: [e.startIndex, e.endIndex],
398
+ text: '0',
399
+ },
400
+ ];
401
+ errors.push(e);
376
402
  }
377
403
  return errors;
378
404
  }
@@ -149,13 +149,23 @@ class AttributesToken extends index_2.Token {
149
149
  let rect;
150
150
  if (parentNode?.type === 'html' && parentNode.closing && this.text().trim()) {
151
151
  rect = { start, ...this.getRootNode().posFromIndex(start) };
152
- errors.push((0, lint_1.generateForSelf)(this, rect, 'no-ignored', 'attributes of a closing tag'));
152
+ const e = (0, lint_1.generateForSelf)(this, rect, 'no-ignored', 'attributes of a closing tag');
153
+ e.fix = { range: [start, e.endIndex], text: '' };
154
+ errors.push(e);
153
155
  }
154
156
  for (let i = 0; i < length; i++) {
155
157
  const attr = childNodes[i];
156
158
  if (attr instanceof atom_1.AtomToken && attr.text().trim()) {
157
159
  rect ??= { start, ...this.getRootNode().posFromIndex(start) };
158
- errors.push((0, lint_1.generateForChild)(attr, rect, 'no-ignored', 'containing invalid attribute'));
160
+ const e = (0, lint_1.generateForChild)(attr, rect, 'no-ignored', 'containing invalid attribute');
161
+ e.suggestions = [
162
+ {
163
+ desc: 'remove',
164
+ range: [e.startIndex, e.endIndex],
165
+ text: ' ',
166
+ },
167
+ ];
168
+ errors.push(e);
159
169
  }
160
170
  else if (attr instanceof attribute_1.AttributeToken) {
161
171
  const { name } = attr;
@@ -78,7 +78,23 @@ class ConverterFlagsToken extends index_2.Token {
78
78
  && !variantFlags.has(flag)
79
79
  && !unknownFlags.has(flag)
80
80
  && (variantFlags.size > 0 || !validFlags.has(flag))) {
81
- errors.push((0, lint_1.generateForChild)(child, rect, 'no-ignored', 'invalid conversion flag'));
81
+ const e = (0, lint_1.generateForChild)(child, rect, 'no-ignored', 'invalid conversion flag');
82
+ if (variantFlags.size === 0 && definedFlags.has(flag.toUpperCase())) {
83
+ e.fix = {
84
+ range: [e.startIndex, e.endIndex],
85
+ text: flag.toUpperCase(),
86
+ };
87
+ }
88
+ else {
89
+ e.suggestions = [
90
+ {
91
+ desc: 'remove',
92
+ range: [e.startIndex, e.endIndex],
93
+ text: '',
94
+ },
95
+ ];
96
+ }
97
+ errors.push(e);
82
98
  }
83
99
  }
84
100
  return errors;
@@ -76,6 +76,18 @@ class GalleryToken extends index_2.Token {
76
76
  endLine: startLine,
77
77
  startCol,
78
78
  endCol: startCol + length,
79
+ suggestions: [
80
+ {
81
+ desc: 'remove',
82
+ range: [start, start + length],
83
+ text: '',
84
+ },
85
+ {
86
+ desc: 'comment',
87
+ range: [start, start + length],
88
+ text: `<!--${str}-->`,
89
+ },
90
+ ],
79
91
  });
80
92
  }
81
93
  else if (child.type !== 'noinclude' && child.type !== 'text') {
package/dist/src/html.js CHANGED
@@ -216,6 +216,24 @@ let HtmlToken = (() => {
216
216
  if (ancestor && magicWords.has(ancestor.name)) {
217
217
  error.severity = 'warning';
218
218
  }
219
+ else {
220
+ error.suggestions = [
221
+ {
222
+ desc: 'remove',
223
+ range: [start, error.endIndex],
224
+ text: '',
225
+ },
226
+ ];
227
+ }
228
+ }
229
+ else if (msg === 'tag that is both closing and self-closing') {
230
+ const { html: [, , voidTags] } = this.getAttribute('config');
231
+ if (voidTags.includes(this.name)) {
232
+ error.fix = {
233
+ range: [start + 1, start + 2],
234
+ text: '',
235
+ };
236
+ }
219
237
  }
220
238
  errors.push(error);
221
239
  }
@@ -233,7 +251,7 @@ let HtmlToken = (() => {
233
251
  refError ??= (0, lint_1.generateForSelf)(this, { start }, 'h1', '');
234
252
  errors.push({
235
253
  ...refError,
236
- rule: 'format-leakage',
254
+ rule: 'bold-header',
237
255
  message: index_1.default.msg('bold in section header'),
238
256
  severity: 'warning',
239
257
  });
@@ -168,7 +168,12 @@ class ImageParameterToken extends index_2.Token {
168
168
  lint(start = this.getAbsoluteIndex()) {
169
169
  const errors = super.lint(start), { link, name } = this;
170
170
  if (name === 'invalid') {
171
- errors.push((0, lint_1.generateForSelf)(this, { start }, 'invalid-gallery', 'invalid gallery image parameter'));
171
+ const e = (0, lint_1.generateForSelf)(this, { start }, 'invalid-gallery', 'invalid gallery image parameter');
172
+ e.fix = {
173
+ range: [start, start + e.endIndex],
174
+ text: '',
175
+ };
176
+ errors.push(e);
172
177
  }
173
178
  else if (typeof link === 'object' && link.encoded) {
174
179
  errors.push((0, lint_1.generateForSelf)(this, { start }, 'url-encoding', 'unnecessary URL encoding in an internal link'));
package/dist/src/index.js CHANGED
@@ -414,7 +414,17 @@ class Token extends element_1.AstElement {
414
414
  }
415
415
  for (const value of Object.values(record)) {
416
416
  if (value.size > 1) {
417
- errors.push(...[...value].map(cat => (0, lint_1.generateForSelf)(cat, { start: cat.getAbsoluteIndex() }, 'no-duplicate', 'duplicated category')));
417
+ errors.push(...[...value].map(cat => {
418
+ const e = (0, lint_1.generateForSelf)(cat, { start: cat.getAbsoluteIndex() }, 'no-duplicate', 'duplicated category');
419
+ e.suggestions = [
420
+ {
421
+ desc: 'remove',
422
+ range: [e.startIndex, e.endIndex],
423
+ text: '',
424
+ },
425
+ ];
426
+ return e;
427
+ }));
418
428
  }
419
429
  }
420
430
  }
@@ -141,14 +141,34 @@ class LinkBaseToken extends index_2.Token {
141
141
  rect ??= { start, ...this.getRootNode().posFromIndex(start) };
142
142
  errors.push((0, lint_1.generateForChild)(target, rect, 'url-encoding', 'unnecessary URL encoding in an internal link'));
143
143
  }
144
- if ((linkType === 'link' || linkType === 'category')
145
- && linkText?.childNodes.some(({ type, data }) => type === 'text' && data.includes('|'))) {
146
- rect ??= { start, ...this.getRootNode().posFromIndex(start) };
147
- errors.push((0, lint_1.generateForChild)(linkText, rect, 'pipe-like', 'additional "|" in the link text', 'warning'));
144
+ if (linkType === 'link' || linkType === 'category') {
145
+ const textNode = linkText?.childNodes.find((c) => c.type === 'text' && c.data.includes('|'));
146
+ if (textNode) {
147
+ rect ??= { start, ...this.getRootNode().posFromIndex(start) };
148
+ const e = (0, lint_1.generateForChild)(linkText, rect, 'pipe-like', 'additional "|" in the link text', 'warning');
149
+ e.suggestions = [
150
+ {
151
+ desc: 'escape',
152
+ range: [
153
+ e.startIndex + textNode.getRelativeIndex(),
154
+ e.startIndex + textNode.getRelativeIndex() + textNode.data.length,
155
+ ],
156
+ text: textNode.data.replace(/\|/gu, '&#124;'),
157
+ },
158
+ ];
159
+ errors.push(e);
160
+ }
148
161
  }
149
- else if (linkType !== 'link' && fragment !== undefined) {
162
+ if (linkType !== 'link' && fragment !== undefined) {
150
163
  rect ??= { start, ...this.getRootNode().posFromIndex(start) };
151
- errors.push((0, lint_1.generateForChild)(target, rect, 'no-ignored', 'useless fragment'));
164
+ const e = (0, lint_1.generateForChild)(target, rect, 'no-ignored', 'useless fragment'), textNode = target.childNodes.find((c) => c.type === 'text' && c.data.includes('#'));
165
+ if (textNode) {
166
+ e.fix = {
167
+ range: [e.startIndex + textNode.getRelativeIndex() + textNode.data.indexOf('#'), e.endIndex],
168
+ text: '',
169
+ };
170
+ }
171
+ errors.push(e);
152
172
  }
153
173
  return errors;
154
174
  }
@@ -114,17 +114,41 @@ let MagicLinkToken = (() => {
114
114
  const refError = (0, lint_1.generateForChild)(child, rect, 'unterminated-url', '', 'warning');
115
115
  regexGlobal.lastIndex = 0;
116
116
  for (let mt = regexGlobal.exec(data); mt; mt = regexGlobal.exec(data)) {
117
- 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;
118
- errors.push({
117
+ 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('|');
118
+ const e = {
119
119
  ...refError,
120
- message: index_1.default.msg('$1 in URL', s.startsWith('|') ? '"|"' : 'full-width punctuation'),
120
+ message: index_1.default.msg('$1 in URL', pipe ? '"|"' : 'full-width punctuation'),
121
121
  startIndex,
122
122
  endIndex: startIndex + s.length,
123
123
  startLine,
124
124
  endLine: startLine,
125
125
  startCol,
126
126
  endCol: startCol + s.length,
127
- });
127
+ };
128
+ if (!pipe) {
129
+ e.suggestions = [
130
+ {
131
+ desc: 'whitespace',
132
+ range: [startIndex, startIndex],
133
+ text: ' ',
134
+ },
135
+ {
136
+ desc: 'escape',
137
+ range: [startIndex, e.endIndex],
138
+ text: encodeURI(s),
139
+ },
140
+ ];
141
+ }
142
+ else if (s.length === 1) {
143
+ e.suggestions = [
144
+ {
145
+ desc: 'whitespace',
146
+ range: [startIndex, startIndex + 1],
147
+ text: ' ',
148
+ },
149
+ ];
150
+ }
151
+ errors.push(e);
128
152
  }
129
153
  }
130
154
  return errors;
@@ -148,7 +172,7 @@ let MagicLinkToken = (() => {
148
172
  this.constructorError('不可插入模板');
149
173
  }
150
174
  else if (!debug_1.Shadow.running && type === 'magic-word' && name !== '!' && name !== '=') {
151
- this.constructorError('不可插入 `{{!}}``{{=}}` 以外的魔术字');
175
+ this.constructorError('不可插入 "{{!}}""{{=}}" 以外的魔术字');
152
176
  }
153
177
  }
154
178
  return super.insertAt(token, i);
@@ -61,7 +61,20 @@ class NestedToken extends index_2.Token {
61
61
  return str && !/^<!--.*-->$/su.test(str);
62
62
  }).map(child => {
63
63
  rect ??= { start, ...this.getRootNode().posFromIndex(start) };
64
- return (0, lint_1.generateForChild)(child, rect, 'no-ignored', index_1.default.msg('invalid content in <$1>', this.name));
64
+ const e = (0, lint_1.generateForChild)(child, rect, 'no-ignored', index_1.default.msg('invalid content in <$1>', this.name));
65
+ e.suggestions = [
66
+ {
67
+ desc: 'remove',
68
+ range: [e.startIndex, e.endIndex],
69
+ text: '',
70
+ },
71
+ {
72
+ desc: 'comment',
73
+ range: [e.startIndex, e.startIndex],
74
+ text: `<!--${String(child)}-->`,
75
+ },
76
+ ];
77
+ return e;
65
78
  }),
66
79
  ];
67
80
  }
@@ -31,9 +31,15 @@ class CommentToken extends (0, hidden_1.hiddenToken)(base_1.NowikiBaseToken) {
31
31
  }
32
32
  /** @override */
33
33
  lint(start = this.getAbsoluteIndex()) {
34
- return this.closed
35
- ? []
36
- : [(0, lint_1.generateForSelf)(this, { start }, 'unclosed-comment', index_1.default.msg('unclosed $1', 'HTML comment'))];
34
+ if (this.closed) {
35
+ return [];
36
+ }
37
+ const e = (0, lint_1.generateForSelf)(this, { start }, 'unclosed-comment', index_1.default.msg('unclosed $1', 'HTML comment'));
38
+ e.fix = {
39
+ range: [e.endIndex, e.endIndex],
40
+ text: '-->',
41
+ };
42
+ return [e];
37
43
  }
38
44
  /** @private */
39
45
  toString() {
@@ -12,9 +12,15 @@ class NowikiToken extends base_1.NowikiBaseToken {
12
12
  /** @override */
13
13
  lint(start = this.getAbsoluteIndex()) {
14
14
  const { name, firstChild: { data } } = this;
15
- return (name === 'templatestyles' || name === 'section') && data
16
- ? [(0, lint_1.generateForSelf)(this, { start }, 'void-ext', index_1.default.msg('nothing should be in <$1>', name))]
17
- : super.lint(start);
15
+ if ((name === 'templatestyles' || name === 'section') && data) {
16
+ const e = (0, lint_1.generateForSelf)(this, { start }, 'void-ext', index_1.default.msg('nothing should be in <$1>', name));
17
+ e.fix = {
18
+ range: [start - 1, e.endIndex + name.length + 3],
19
+ text: '/>',
20
+ };
21
+ return [e];
22
+ }
23
+ return super.lint(start);
18
24
  }
19
25
  }
20
26
  exports.NowikiToken = NowikiToken;
@@ -79,6 +79,13 @@ let QuoteToken = (() => {
79
79
  startCol: endCol - length,
80
80
  endLine,
81
81
  endCol,
82
+ suggestions: [
83
+ {
84
+ desc: 'escape',
85
+ range: [startIndex, endIndex],
86
+ text: '&apos;'.repeat(length),
87
+ },
88
+ ],
82
89
  });
83
90
  }
84
91
  if (nextSibling?.type === 'text' && nextSibling.data.startsWith(`'`)) {
@@ -91,6 +98,13 @@ let QuoteToken = (() => {
91
98
  startLine,
92
99
  startCol,
93
100
  endCol: startCol + length,
101
+ suggestions: [
102
+ {
103
+ desc: 'escape',
104
+ range: [startIndex, endIndex],
105
+ text: '&apos;'.repeat(length),
106
+ },
107
+ ],
94
108
  });
95
109
  }
96
110
  if (bold && this.closest('heading-title')) {
@@ -47,7 +47,15 @@ class ParamTagToken extends index_2.Token {
47
47
  return str && !(i >= 0 ? /^[a-z]+(?:\[\])?\s*(?:=|$)/iu : /^[a-z]+(?:\[\])?\s*=/iu).test(str);
48
48
  }).map(child => {
49
49
  rect ??= { start, ...this.getRootNode().posFromIndex(start) };
50
- return (0, lint_1.generateForChild)(child, rect, 'no-ignored', index_1.default.msg('invalid parameter of <$1>', this.name));
50
+ const e = (0, lint_1.generateForChild)(child, rect, 'no-ignored', index_1.default.msg('invalid parameter of <$1>', this.name));
51
+ e.suggestions = [
52
+ {
53
+ desc: 'remove',
54
+ range: [e.startIndex, e.endIndex],
55
+ text: '',
56
+ },
57
+ ];
58
+ return e;
51
59
  });
52
60
  }
53
61
  /** @override */
@@ -159,14 +159,16 @@ let ParameterToken = (() => {
159
159
  const errors = super.lint(start), { firstChild } = this, link = new RegExp(`https?://${string_1.extUrlCharFirst}${string_1.extUrlChar}$`, 'iu').exec(firstChild.text())?.[0];
160
160
  if (link && new URL(link).search) {
161
161
  const e = (0, lint_1.generateForChild)(firstChild, { start }, 'unescaped', 'unescaped query string in an anonymous parameter');
162
- errors.push({
163
- ...e,
164
- startIndex: e.endIndex,
165
- endIndex: e.endIndex + 1,
166
- startLine: e.endLine,
167
- startCol: e.endCol,
168
- endCol: e.endCol + 1,
169
- });
162
+ e.startIndex = e.endIndex;
163
+ e.startLine = e.endLine;
164
+ e.startCol = e.endCol;
165
+ e.endIndex++;
166
+ e.endCol++;
167
+ e.fix = {
168
+ range: [e.startIndex, e.endIndex],
169
+ text: '{{=}}',
170
+ };
171
+ errors.push(e);
170
172
  }
171
173
  return errors;
172
174
  }
@@ -193,7 +193,24 @@ let TdToken = (() => {
193
193
  if (child.type === 'text') {
194
194
  const { data } = child;
195
195
  if (data.includes('|')) {
196
- errors.push((0, lint_1.generateForChild)(child, { start }, 'pipe-like', 'additional "|" in a table cell', data.includes('||') ? 'error' : 'warning'));
196
+ const isError = data.includes('||'), e = (0, lint_1.generateForChild)(child, { start }, 'pipe-like', 'additional "|" in a table cell', isError ? 'error' : 'warning');
197
+ if (isError) {
198
+ const syntax = { caption: '|+', td: '|', th: '!' }[this.subtype];
199
+ e.fix = {
200
+ range: [e.startIndex, e.endIndex],
201
+ text: data.replace(/\|\|/gu, `\n${syntax}`),
202
+ };
203
+ }
204
+ else {
205
+ e.suggestions = [
206
+ {
207
+ desc: 'escape',
208
+ range: [e.startIndex, e.endIndex],
209
+ text: data.replace(/\|/gu, '&#124;'),
210
+ },
211
+ ];
212
+ }
213
+ errors.push(e);
197
214
  }
198
215
  }
199
216
  }
@@ -30,13 +30,11 @@ class TrBaseToken extends base_1.TableBaseToken {
30
30
  catch { }
31
31
  }
32
32
  const error = (0, lint_1.generateForChild)(inter, { start }, 'fostered-content', 'content to be moved out from the table');
33
- errors.push({
34
- ...error,
35
- severity: first.type === 'template' ? 'warning' : 'error',
36
- startIndex: error.startIndex + 1,
37
- startLine: error.startLine + 1,
38
- startCol: 0,
39
- });
33
+ error.severity = first.type === 'template' ? 'warning' : 'error';
34
+ error.startIndex++;
35
+ error.startLine++;
36
+ error.startCol = 0;
37
+ errors.push(error);
40
38
  return errors;
41
39
  }
42
40
  /* NOT FOR BROWSER */
@@ -38,9 +38,15 @@ class IncludeToken extends (0, hidden_1.hiddenToken)(index_2.TagPairToken) {
38
38
  }
39
39
  /** @override */
40
40
  lint(start = this.getAbsoluteIndex()) {
41
- return this.closed
42
- ? []
43
- : [(0, lint_1.generateForSelf)(this, { start }, 'unclosed-comment', index_1.default.msg('unclosed $1', `<${this.name}>`))];
41
+ if (this.closed) {
42
+ return [];
43
+ }
44
+ const e = (0, lint_1.generateForSelf)(this, { start }, 'unclosed-comment', index_1.default.msg('unclosed $1', `<${this.name}>`));
45
+ e.fix = {
46
+ range: [e.endIndex, e.endIndex],
47
+ text: `</${this.name}>`,
48
+ };
49
+ return [e];
44
50
  }
45
51
  /* NOT FOR BROWSER */
46
52
  /** @override */
@@ -232,7 +232,14 @@ class TranscludeToken extends index_2.Token {
232
232
  const title = this.#getTitle();
233
233
  if (title.fragment !== undefined) {
234
234
  rect = { start, ...this.getRootNode().posFromIndex(start) };
235
- errors.push((0, lint_1.generateForChild)(childNodes[type === 'template' ? 0 : 1], rect, 'no-ignored', 'useless fragment'));
235
+ 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('#'));
236
+ if (textNode) {
237
+ e.fix = {
238
+ range: [e.startIndex + textNode.getRelativeIndex() + textNode.data.indexOf('#'), e.endIndex],
239
+ text: '',
240
+ };
241
+ }
242
+ errors.push(e);
236
243
  }
237
244
  if (!title.valid) {
238
245
  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('\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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wikiparser-node",
3
- "version": "1.4.5",
3
+ "version": "1.5.0",
4
4
  "description": "A Node.js parser for MediaWiki markup with AST",
5
5
  "keywords": [
6
6
  "mediawiki",
@@ -48,6 +48,7 @@
48
48
  "@typescript-eslint/eslint-plugin": "^6.19.1",
49
49
  "@typescript-eslint/parser": "^6.19.1",
50
50
  "ajv-cli": "^5.0.0",
51
+ "chalk": "^4.1.2",
51
52
  "eslint": "^8.56.0",
52
53
  "eslint-plugin-es-x": "^7.5.0",
53
54
  "eslint-plugin-eslint-comments": "^3.2.0",