wikilint 2.13.9 → 2.14.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
@@ -59,9 +59,7 @@ export interface LintError {
59
59
  endLine: number;
60
60
  endCol: number;
61
61
  fix?: LintError.Fix;
62
- suggestions?: (LintError.Fix & {
63
- desc: string;
64
- })[];
62
+ suggestions?: LintError.Fix[];
65
63
  }
66
64
  /** 类似Node */
67
65
  export interface AstNode {
package/dist/lib/text.js CHANGED
@@ -101,7 +101,7 @@ class AstText extends node_1.AstNode {
101
101
  return [];
102
102
  }
103
103
  errorRegex.lastIndex = 0;
104
- const errors = [], nextType = nextSibling?.type, nextName = nextSibling?.name, previousType = previousSibling?.type, root = this.getRootNode(), { ext, html } = root.getAttribute('config'), { top, left } = root.posFromIndex(start), tags = new Set(['onlyinclude', 'noinclude', 'includeonly', ext, html, disallowedTags].flat(2));
104
+ const errors = [], nextType = nextSibling?.type, nextName = nextSibling?.name, previousType = previousSibling?.type, root = this.getRootNode(), rootStr = root.toString(), { ext, html } = root.getAttribute('config'), { top, left } = root.posFromIndex(start), tags = new Set(['onlyinclude', 'noinclude', 'includeonly', ext, html, disallowedTags].flat(2));
105
105
  for (let mt = errorRegex.exec(data); mt; mt = errorRegex.exec(data)) {
106
106
  const [, tag, prefix] = mt;
107
107
  let { index } = mt, error = mt[0].toLowerCase();
@@ -121,7 +121,7 @@ class AstText extends node_1.AstNode {
121
121
  else if (char === ']' && (index || length > 1)) {
122
122
  errorRegex.lastIndex--;
123
123
  }
124
- const startIndex = start + index, endIndex = startIndex + length, rootStr = root.toString(), nextChar = rootStr[endIndex], previousChar = rootStr[startIndex - 1], severity = length > 1 && !(char === '<' && !/[\s/>]/u.test(nextChar ?? '')
124
+ const startIndex = start + index, endIndex = startIndex + length, nextChar = rootStr[endIndex], previousChar = rootStr[startIndex - 1], severity = length > 1 && !(char === '<' && !/[\s/>]/u.test(nextChar ?? '')
125
125
  || isHtmlAttrVal && (char === '[' || char === ']')
126
126
  || magicLink && type === 'parameter-value')
127
127
  || char === '{' && (nextChar === char || previousChar === '-')
@@ -166,39 +166,31 @@ class AstText extends node_1.AstNode {
166
166
  endCol: startCol + length,
167
167
  };
168
168
  if (char === '<') {
169
- e.suggestions = [
170
- {
171
- desc: 'escape',
172
- range: [startIndex, startIndex + 1],
173
- text: '&lt;',
174
- },
175
- ];
169
+ e.suggestions = [{ desc: 'escape', range: [startIndex, startIndex + 1], text: '&lt;' }];
176
170
  }
177
171
  else if (char === 'h'
178
172
  && !(type === 'ext-link-text' || type === 'link-text')
179
173
  && /[\p{L}\d_]/u.test(previousChar || '')) {
180
- e.suggestions = [
181
- {
182
- desc: 'whitespace',
183
- range: [startIndex, startIndex],
184
- text: ' ',
185
- },
186
- ];
174
+ e.suggestions = [{ desc: 'whitespace', range: [startIndex, startIndex], text: ' ' }];
187
175
  }
188
176
  else if (char === '[' && type === 'ext-link-text') {
189
177
  const i = parentNode.getAbsoluteIndex() + parentNode.toString().length;
190
- e.suggestions = [
191
- {
192
- desc: 'escape',
193
- range: [i, i + 1],
194
- text: '&#93;',
195
- },
196
- ];
178
+ e.suggestions = [{ desc: 'escape', range: [i, i + 1], text: '&#93;' }];
197
179
  }
198
180
  else if (char === ']' && previousType === 'free-ext-link' && severity === 'error') {
199
181
  const i = start - previousSibling.toString().length;
200
182
  e.fix = { range: [i, i], text: '[', desc: 'left bracket' };
201
183
  }
184
+ else if (magicLink) {
185
+ e.suggestions = [
186
+ ...mt[0] === error
187
+ ? []
188
+ : [{ desc: 'uppercase', range: [startIndex, endIndex], text: error }],
189
+ ...nextChar === ':' || nextChar === ':'
190
+ ? [{ desc: 'whitespace', range: [endIndex, endIndex + 1], text: ' ' }]
191
+ : [],
192
+ ];
193
+ }
202
194
  errors.push(e);
203
195
  }
204
196
  return errors;
package/dist/src/arg.js CHANGED
@@ -71,7 +71,7 @@ class ArgToken extends index_2.Token {
71
71
  if (!this.getAttribute('include')) {
72
72
  const e = (0, lint_1.generateForSelf)(this, { start }, 'no-arg', 'unexpected template argument');
73
73
  if (argDefault) {
74
- e.fix = { range: [start, e.endIndex], text: argDefault.text(), desc: 'expand' };
74
+ e.suggestions = [{ range: [start, e.endIndex], text: argDefault.text(), desc: 'expand' }];
75
75
  }
76
76
  return [e];
77
77
  }
@@ -86,16 +86,8 @@ class ArgToken extends index_2.Token {
86
86
  e.startIndex--;
87
87
  e.startCol--;
88
88
  e.suggestions = [
89
- {
90
- desc: 'remove',
91
- range: [e.startIndex, e.endIndex],
92
- text: '',
93
- },
94
- {
95
- desc: 'escape',
96
- range: [e.startIndex, e.startIndex + 1],
97
- text: '{{!}}',
98
- },
89
+ { desc: 'remove', range: [e.startIndex, e.endIndex], text: '' },
90
+ { desc: 'escape', range: [e.startIndex, e.startIndex + 1], text: '{{!}}' },
99
91
  ];
100
92
  return e;
101
93
  }));
@@ -98,16 +98,10 @@ class AttributeToken extends index_2.Token {
98
98
  const e = (0, lint_1.generateForChild)(lastChild, rect, 'unclosed-quote', index_1.default.msg('unclosed $1', 'quotes'), 'warning');
99
99
  e.startIndex--;
100
100
  e.startCol--;
101
- const fix = { range: [e.endIndex, e.endIndex], text: this.#quotes[0], desc: 'close' };
102
- if (lastChild.childNodes.some(({ type: t, data }) => t === 'text' && /\s/u.test(data))) {
103
- e.suggestions = [fix];
104
- }
105
- else {
106
- e.fix = fix;
107
- }
101
+ e.suggestions = [{ range: [e.endIndex, e.endIndex], text: this.#quotes[0], desc: 'close' }];
108
102
  errors.push(e);
109
103
  }
110
- const attrs = sharable_1.extAttrs[tag], attrs2 = sharable_1.htmlAttrs[tag];
104
+ const attrs = sharable_1.extAttrs[tag], attrs2 = sharable_1.htmlAttrs[tag], { length } = this.toString();
111
105
  if (!attrs?.has(name)
112
106
  && !attrs2?.has(name)
113
107
  // 不是未定义的扩展标签或包含嵌入的HTML标签
@@ -115,7 +109,10 @@ class AttributeToken extends index_2.Token {
115
109
  && (type === 'ext-attr' && !attrs2
116
110
  || !/^(?:xmlns:[\w:.-]+|data-(?!ooui|mw|parsoid)[^:]*)$/u.test(name)
117
111
  && (tag === 'meta' || tag === 'link' || !sharable_1.commonHtmlAttrs.has(name)))) {
118
- errors.push((0, lint_1.generateForChild)(firstChild, rect, 'illegal-attr', 'illegal attribute name'));
112
+ errors.push({
113
+ ...(0, lint_1.generateForChild)(firstChild, rect, 'illegal-attr', 'illegal attribute name'),
114
+ suggestions: [{ desc: 'remove', range: [start, start + length], text: '' }],
115
+ });
119
116
  }
120
117
  else if (sharable_1.obsoleteAttrs[tag]?.has(name)) {
121
118
  errors.push((0, lint_1.generateForChild)(firstChild, rect, 'obsolete-attr', 'obsolete attribute', 'warning'));
@@ -126,16 +123,8 @@ class AttributeToken extends index_2.Token {
126
123
  else if (name === 'tabindex' && typeof value === 'string' && value !== '0') {
127
124
  const e = (0, lint_1.generateForChild)(lastChild, rect, 'illegal-attr', 'nonzero tabindex');
128
125
  e.suggestions = [
129
- {
130
- desc: 'remove',
131
- range: [start, e.endIndex],
132
- text: '',
133
- },
134
- {
135
- desc: '0 tabindex',
136
- range: [e.startIndex, e.endIndex],
137
- text: '0',
138
- },
126
+ { desc: 'remove', range: [start, start + length], text: '' },
127
+ { desc: '0 tabindex', range: [e.startIndex, e.endIndex], text: '0' },
139
128
  ];
140
129
  errors.push(e);
141
130
  }
@@ -99,8 +99,11 @@ class AttributesToken extends index_2.Token {
99
99
  lint(start = this.getAbsoluteIndex(), re) {
100
100
  const errors = super.lint(start, re), { parentNode, childNodes } = this, attrs = new Map(), duplicated = new Set(), rect = new rect_1.BoundingRect(this, start);
101
101
  if (parentNode?.type === 'html' && parentNode.closing && this.text().trim()) {
102
- const e = (0, lint_1.generateForSelf)(this, rect, 'no-ignored', 'attributes of a closing tag');
103
- e.fix = { range: [start, e.endIndex], text: '', desc: 'remove' };
102
+ const e = (0, lint_1.generateForSelf)(this, rect, 'no-ignored', 'attributes of a closing tag'), index = parentNode.getAbsoluteIndex();
103
+ e.suggestions = [
104
+ { desc: 'remove', range: [start, e.endIndex], text: '' },
105
+ { desc: 'open', range: [index + 1, index + 2], text: '' },
106
+ ];
104
107
  errors.push(e);
105
108
  }
106
109
  for (const attr of childNodes) {
@@ -118,20 +121,27 @@ class AttributesToken extends index_2.Token {
118
121
  const str = attr.text().trim();
119
122
  if (str) {
120
123
  const e = (0, lint_1.generateForChild)(attr, rect, 'no-ignored', 'containing invalid attribute', /[\p{L}\d]/u.test(str) ? 'error' : 'warning');
121
- e.suggestions = [
122
- {
123
- desc: 'remove',
124
- range: [e.startIndex, e.endIndex],
125
- text: ' ',
126
- },
127
- ];
124
+ e.suggestions = [{ desc: 'remove', range: [e.startIndex, e.endIndex], text: ' ' }];
128
125
  errors.push(e);
129
126
  }
130
127
  }
131
128
  }
132
129
  if (duplicated.size > 0) {
133
130
  for (const key of duplicated) {
134
- errors.push(...attrs.get(key).map(attr => (0, lint_1.generateForChild)(attr, rect, 'no-duplicate', index_1.default.msg('duplicated $1 attribute', key))));
131
+ const pairs = attrs.get(key).map(attr => {
132
+ const value = attr.getValue();
133
+ return [attr, value === true ? '' : value];
134
+ });
135
+ errors.push(...pairs.map(([attr, value], i) => {
136
+ const e = (0, lint_1.generateForChild)(attr, rect, 'no-duplicate', index_1.default.msg('duplicated $1 attribute', key)), remove = { desc: 'remove', range: [e.startIndex, e.endIndex], text: '' };
137
+ if (!value || pairs.slice(0, i).some(([, v]) => v === value)) {
138
+ e.fix = remove;
139
+ }
140
+ else {
141
+ e.suggestions = [remove];
142
+ }
143
+ return e;
144
+ }));
135
145
  }
136
146
  }
137
147
  return errors;
@@ -65,13 +65,7 @@ class ConverterFlagsToken extends index_2.Token {
65
65
  e.fix = { range: [e.startIndex, e.endIndex], text: flag.toUpperCase(), desc: 'uppercase' };
66
66
  }
67
67
  else {
68
- e.suggestions = [
69
- {
70
- desc: 'remove',
71
- range: [e.startIndex - (i && 1), e.endIndex],
72
- text: '',
73
- },
74
- ];
68
+ e.suggestions = [{ desc: 'remove', range: [e.startIndex - (i && 1), e.endIndex], text: '' }];
75
69
  }
76
70
  errors.push(e);
77
71
  }
@@ -71,16 +71,8 @@ class GalleryToken extends index_2.Token {
71
71
  startCol,
72
72
  endCol: startCol + length,
73
73
  suggestions: [
74
- {
75
- desc: 'remove',
76
- range: [start, endIndex],
77
- text: '',
78
- },
79
- {
80
- desc: 'comment',
81
- range: [start, endIndex],
82
- text: `<!--${str}-->`,
83
- },
74
+ { desc: 'remove', range: [start, endIndex], text: '' },
75
+ { desc: 'comment', range: [start, endIndex], text: `<!--${str}-->` },
84
76
  ],
85
77
  });
86
78
  }
@@ -57,21 +57,55 @@ class HeadingToken extends index_2.Token {
57
57
  }
58
58
  /** @private */
59
59
  lint(start = this.getAbsoluteIndex(), re) {
60
- const errors = super.lint(start, re), { firstChild, level } = this, innerStr = firstChild.toString(), quotes = firstChild.childNodes.filter((0, debug_1.isToken)('quote')), boldQuotes = quotes.filter(({ bold }) => bold), italicQuotes = quotes.filter(({ italic }) => italic), rect = new rect_1.BoundingRect(this, start);
60
+ const errors = super.lint(start, re), { firstChild, level } = this, innerStr = firstChild.toString(), unbalancedStart = innerStr.startsWith('='), unbalanced = unbalancedStart || innerStr.endsWith('='), quotes = firstChild.childNodes.filter((0, debug_1.isToken)('quote')), boldQuotes = quotes.filter(({ bold }) => bold), italicQuotes = quotes.filter(({ italic }) => italic), rect = new rect_1.BoundingRect(this, start);
61
61
  if (this.level === 1) {
62
- errors.push((0, lint_1.generateForChild)(firstChild, rect, 'h1', '<h1>'));
62
+ const e = (0, lint_1.generateForChild)(firstChild, rect, 'h1', '<h1>');
63
+ if (!unbalanced) {
64
+ e.suggestions = [{ desc: 'h2', range: [e.startIndex, e.endIndex], text: `=${innerStr}=` }];
65
+ }
66
+ errors.push(e);
63
67
  }
64
- if (innerStr.startsWith('=') || innerStr.endsWith('=')) {
65
- errors.push((0, lint_1.generateForChild)(firstChild, rect, 'unbalanced-header', index_1.default.msg('unbalanced $1 in a section header', '"="')));
68
+ if (unbalanced) {
69
+ const e = (0, lint_1.generateForChild)(firstChild, rect, 'unbalanced-header', index_1.default.msg('unbalanced $1 in a section header', '"="'));
70
+ if (innerStr === '=') {
71
+ //
72
+ }
73
+ else if (unbalancedStart) {
74
+ const [extra] = /^=+/u.exec(innerStr);
75
+ e.suggestions = [
76
+ { desc: `h${level}`, range: [e.startIndex, e.startIndex + extra.length], text: '' },
77
+ { desc: `h${level + extra.length}`, range: [e.endIndex, e.endIndex], text: extra },
78
+ ];
79
+ }
80
+ else {
81
+ const extra = /[^=](=+)$/u.exec(innerStr)[1];
82
+ e.suggestions = [
83
+ { desc: `h${level}`, range: [e.endIndex - extra.length, e.endIndex], text: '' },
84
+ { desc: `h${level + extra.length}`, range: [e.startIndex, e.startIndex], text: extra },
85
+ ];
86
+ }
87
+ errors.push(e);
66
88
  }
67
89
  if (this.closest('html-attrs,table-attrs')) {
68
90
  errors.push((0, lint_1.generateForSelf)(this, rect, 'parsing-order', 'section header in a HTML tag'));
69
91
  }
92
+ const rootStr = this.getRootNode().toString();
70
93
  if (boldQuotes.length % 2) {
71
- 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')));
94
+ const e = (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')), end = start + level + innerStr.length;
95
+ if (rootStr.slice(e.endIndex, end).trim()) {
96
+ e.suggestions = [{ desc: 'close', range: [end, end], text: "'''" }];
97
+ }
98
+ else {
99
+ e.fix = { desc: 'remove', range: [e.startIndex, e.endIndex], text: '' };
100
+ }
101
+ errors.push(e);
72
102
  }
73
103
  if (italicQuotes.length % 2) {
74
- 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')));
104
+ const e = (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')), end = start + level + innerStr.length;
105
+ e.fix = rootStr.slice(e.endIndex, end).trim()
106
+ ? { desc: 'close', range: [end, end], text: "''" }
107
+ : { desc: 'remove', range: [e.startIndex, e.endIndex], text: '' };
108
+ errors.push(e);
75
109
  }
76
110
  return errors;
77
111
  }
package/dist/src/html.js CHANGED
@@ -84,10 +84,15 @@ class HtmlToken extends index_1.Token {
84
84
  lint(start = this.getAbsoluteIndex(), re) {
85
85
  const errors = super.lint(start, re), rect = new rect_1.BoundingRect(this, start);
86
86
  if (this.name === 'h1' && !this.closing) {
87
- errors.push((0, lint_1.generateForSelf)(this, rect, 'h1', '<h1>'));
87
+ errors.push({
88
+ ...(0, lint_1.generateForSelf)(this, rect, 'h1', '<h1>'),
89
+ suggestions: [{ desc: 'h2', range: [start + 2, start + 3], text: '2' }],
90
+ });
88
91
  }
89
92
  if (this.closest('table-attrs')) {
90
- errors.push((0, lint_1.generateForSelf)(this, rect, 'parsing-order', 'HTML tag in table attributes'));
93
+ const e = (0, lint_1.generateForSelf)(this, rect, 'parsing-order', 'HTML tag in table attributes');
94
+ e.fix = { desc: 'remove', range: [start, e.endIndex], text: '' };
95
+ errors.push(e);
91
96
  }
92
97
  try {
93
98
  this.findMatchingTag();
@@ -95,38 +100,53 @@ class HtmlToken extends index_1.Token {
95
100
  catch (e) {
96
101
  if (e instanceof SyntaxError) {
97
102
  const { message } = e;
98
- const msg = message.split(':')[0].toLowerCase(), error = (0, lint_1.generateForSelf)(this, rect, 'unmatched-tag', msg);
99
- if (msg === 'unclosed tag' && !this.closest('heading-title')) {
100
- if (formattingTags.has(this.name)) {
101
- const childNodes = this.parentNode?.childNodes, i = childNodes?.indexOf(this);
102
- if (!childNodes?.slice(0, i).some(({ type, name }) => type === 'html' && name === this.name)) {
103
+ const msg = message.split(':')[0].toLowerCase(), error = (0, lint_1.generateForSelf)(this, rect, 'unmatched-tag', msg), noSelfClosing = {
104
+ desc: 'no self-closing',
105
+ range: [error.endIndex - 2, error.endIndex - 1],
106
+ text: '',
107
+ };
108
+ switch (msg) {
109
+ case 'unclosed tag': {
110
+ const childNodes = this.parentNode?.childNodes;
111
+ if (formattingTags.has(this.name)
112
+ && childNodes?.slice(0, childNodes.indexOf(this))
113
+ .some(({ type, name }) => type === 'html' && name === this.name)) {
114
+ error.suggestions = [{ desc: 'close', range: [start + 1, start + 1], text: '/' }];
115
+ }
116
+ else if (!this.closest('heading-title')) {
103
117
  error.severity = 'warning';
104
118
  }
119
+ break;
105
120
  }
106
- else {
107
- error.severity = 'warning';
121
+ case 'unmatched closing tag': {
122
+ const ancestor = this.closest('magic-word');
123
+ if (ancestor && magicWords.has(ancestor.name)) {
124
+ error.severity = 'warning';
125
+ }
126
+ else {
127
+ error.suggestions = [{ desc: 'remove', range: [start, error.endIndex], text: '' }];
128
+ }
129
+ break;
108
130
  }
109
- }
110
- else if (msg === 'unmatched closing tag') {
111
- const ancestor = this.closest('magic-word');
112
- if (ancestor && magicWords.has(ancestor.name)) {
113
- error.severity = 'warning';
131
+ case 'tag that is both closing and self-closing': {
132
+ const { html: [normalTags, , voidTags] } = this.getAttribute('config'), open = { desc: 'open', range: [start + 1, start + 2], text: '' };
133
+ if (voidTags.includes(this.name)) {
134
+ error.fix = open;
135
+ }
136
+ else if (normalTags.includes(this.name)) {
137
+ error.fix = noSelfClosing;
138
+ }
139
+ else {
140
+ error.suggestions = [open, noSelfClosing];
141
+ }
142
+ break;
114
143
  }
115
- else {
144
+ case 'invalid self-closing tag':
116
145
  error.suggestions = [
117
- {
118
- desc: 'remove',
119
- range: [start, error.endIndex],
120
- text: '',
121
- },
146
+ noSelfClosing,
147
+ { desc: 'close', range: [error.endIndex - 2, error.endIndex], text: `></${this.name}>` },
122
148
  ];
123
- }
124
- }
125
- else if (msg === 'tag that is both closing and self-closing') {
126
- const { html: [, , voidTags] } = this.getAttribute('config');
127
- if (voidTags.includes(this.name)) {
128
- error.fix = { range: [start + 1, start + 2], text: '', desc: 'open' };
129
- }
149
+ // no default
130
150
  }
131
151
  errors.push(error);
132
152
  }
@@ -111,7 +111,9 @@ class ImageParameterToken extends index_2.Token {
111
111
  errors.push(e);
112
112
  }
113
113
  else if (typeof link === 'object' && link.encoded) {
114
- errors.push((0, lint_1.generateForSelf)(this, { start }, 'url-encoding', 'unnecessary URL encoding in an internal link'));
114
+ const e = (0, lint_1.generateForSelf)(this, { start }, 'url-encoding', 'unnecessary URL encoding in an internal link');
115
+ e.suggestions = [{ desc: 'decode', range: [start, e.endIndex], text: (0, string_1.rawurldecode)(this.text()) }];
116
+ errors.push(e);
115
117
  }
116
118
  return errors;
117
119
  }
@@ -95,7 +95,14 @@ class ImagemapToken extends index_2.Token {
95
95
  errors.push(...this.childNodes.filter(child => {
96
96
  const str = child.toString().trim();
97
97
  return child.type === 'noinclude' && str && !str.startsWith('#');
98
- }).map(child => (0, lint_1.generateForChild)(child, rect, 'invalid-imagemap', 'invalid link in <imagemap>')));
98
+ }).map(child => {
99
+ const e = (0, lint_1.generateForChild)(child, rect, 'invalid-imagemap', 'invalid link in <imagemap>');
100
+ e.suggestions = [
101
+ { desc: 'remove', range: [e.startIndex - 1, e.endIndex], text: '' },
102
+ { desc: 'comment', range: [e.startIndex, e.startIndex], text: '# ' },
103
+ ];
104
+ return e;
105
+ }));
99
106
  }
100
107
  else {
101
108
  errors.push((0, lint_1.generateForSelf)(this, rect, 'invalid-imagemap', '<imagemap> without an image'));
package/dist/src/index.js CHANGED
@@ -337,47 +337,35 @@ class Token extends element_1.AstElement {
337
337
  lint(start = this.getAbsoluteIndex(), re) {
338
338
  let errors = super.lint(start, re);
339
339
  if (this.type === 'root') {
340
- const record = new Map(), selector = 'category,html-attr#id,ext-attr#id,table-attr#id,ext-attr#name';
340
+ const record = new Map(), selector = 'category,html-attr#id,ext-attr#id,table-attr#id';
341
341
  for (const cat of this.querySelectorAll(selector)) {
342
342
  let key;
343
343
  if (cat.type === 'category') {
344
344
  key = cat.name;
345
345
  }
346
346
  else {
347
- const value = cat.getValue(), attrs = cat.parentNode;
348
- if (cat.name === 'id') {
349
- key = `#${value === true ? '' : value}`;
347
+ const value = cat.getValue();
348
+ if (value && value !== true) {
349
+ key = `#${value}`;
350
350
  }
351
- else if (cat.tag === 'ref' && value !== true && value
352
- && attrs.parentNode.innerText) {
353
- const group = attrs.getAttr('group');
354
- key = `${typeof group === 'string' && group || ' '}#${value}`;
351
+ }
352
+ if (key) {
353
+ const thisCat = record.get(key);
354
+ if (thisCat) {
355
+ thisCat.add(cat);
355
356
  }
356
357
  else {
357
- continue;
358
+ record.set(key, new Set([cat]));
358
359
  }
359
360
  }
360
- const thisCat = record.get(key);
361
- if (thisCat) {
362
- thisCat.add(cat);
363
- }
364
- else {
365
- record.set(key, new Set([cat]));
366
- }
367
361
  }
368
362
  for (const [key, value] of record) {
369
363
  if (value.size > 1 && !key.startsWith('#mw-customcollapsible-')) {
370
- const isCat = !key.includes('#'), msg = `duplicated ${isCat ? 'category' : 'id/name'}`, severity = key.startsWith('#') ? 'warning' : 'error';
364
+ const isCat = !key.startsWith('#'), msg = `duplicated ${isCat ? 'category' : 'id'}`, severity = isCat ? 'error' : 'warning';
371
365
  errors.push(...[...value].map(cat => {
372
366
  const e = (0, lint_1.generateForSelf)(cat, { start: cat.getAbsoluteIndex() }, 'no-duplicate', msg, severity);
373
367
  if (isCat) {
374
- e.suggestions = [
375
- {
376
- desc: 'remove',
377
- range: [e.startIndex, e.endIndex],
378
- text: '',
379
- },
380
- ];
368
+ e.suggestions = [{ desc: 'remove', range: [e.startIndex, e.endIndex], text: '' }];
381
369
  }
382
370
  return e;
383
371
  }));
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LinkBaseToken = void 0;
4
4
  const lint_1 = require("../../util/lint");
5
5
  const constants_1 = require("../../util/constants");
6
+ const string_1 = require("../../util/string");
6
7
  const rect_1 = require("../../lib/rect");
7
8
  const index_1 = require("../../index");
8
9
  const index_2 = require("../index");
@@ -85,7 +86,9 @@ class LinkBaseToken extends index_2.Token {
85
86
  errors.push((0, lint_1.generateForChild)(target, rect, 'unknown-page', 'template in an internal link target', 'warning'));
86
87
  }
87
88
  if (encoded) {
88
- errors.push((0, lint_1.generateForChild)(target, rect, 'url-encoding', 'unnecessary URL encoding in an internal link'));
89
+ const e = (0, lint_1.generateForChild)(target, rect, 'url-encoding', 'unnecessary URL encoding in an internal link');
90
+ e.suggestions = [{ desc: 'decode', range: [e.startIndex, e.endIndex], text: (0, string_1.rawurldecode)(target.text()) }];
91
+ errors.push(e);
89
92
  }
90
93
  if (type === 'link' || type === 'category') {
91
94
  const textNode = linkText?.childNodes.find((c) => c.type === 'text' && c.data.includes('|'));
@@ -91,7 +91,11 @@ class FileToken extends base_1.LinkBaseToken {
91
91
  * @param msg 消息键
92
92
  * @param p1 替换$1
93
93
  */
94
- const generate = (msg, p1) => (arg) => (0, lint_1.generateForChild)(arg, rect, 'no-duplicate', index_1.default.msg(`${msg} image $1 parameter`, p1));
94
+ const generate = (msg, p1) => (arg) => {
95
+ const e = (0, lint_1.generateForChild)(arg, rect, 'no-duplicate', index_1.default.msg(`${msg} image $1 parameter`, p1));
96
+ e.suggestions = [{ desc: 'remove', range: [e.startIndex - 1, e.endIndex], text: '' }];
97
+ return e;
98
+ };
95
99
  for (const key of keys) {
96
100
  if (key === 'invalid' || key === 'width' && unscaled) {
97
101
  continue;
@@ -46,7 +46,9 @@ class GalleryImageToken extends file_1.FileToken {
46
46
  lint(start = this.getAbsoluteIndex(), re) {
47
47
  const errors = super.lint(start, re), { ns, interwiki } = this.getAttribute('title');
48
48
  if (interwiki || ns !== 6) {
49
- errors.push((0, lint_1.generateForSelf)(this, { start }, 'invalid-gallery', 'invalid gallery image'));
49
+ const e = (0, lint_1.generateForSelf)(this, { start }, 'invalid-gallery', 'invalid gallery image');
50
+ e.suggestions = [{ desc: 'prefix', range: [start, start], text: 'File:' }];
51
+ errors.push(e);
50
52
  }
51
53
  return errors;
52
54
  }
@@ -66,24 +66,10 @@ class MagicLinkToken extends index_2.Token {
66
66
  if (child) {
67
67
  const { data } = child, e = (0, lint_1.generateForChild)(child, rect, 'unterminated-url', index_1.default.msg('$1 in URL', pipe ? '"|"' : 'full-width punctuation'), 'warning'), { index, 0: s } = regex.exec(data), i = e.startIndex + index;
68
68
  e.suggestions = pipe
69
- ? [
70
- {
71
- desc: 'whitespace',
72
- range: [i, i + 1],
73
- text: ' ',
74
- },
75
- ]
69
+ ? [{ desc: 'whitespace', range: [i, i + 1], text: ' ' }]
76
70
  : [
77
- {
78
- desc: 'whitespace',
79
- range: [i, i],
80
- text: ' ',
81
- },
82
- {
83
- desc: 'escape',
84
- range: [i, i + s.length],
85
- text: encodeURI(s),
86
- },
71
+ { desc: 'whitespace', range: [i, i], text: ' ' },
72
+ { desc: 'escape', range: [i, i + s.length], text: encodeURI(s) },
87
73
  ];
88
74
  errors.push(e);
89
75
  }
@@ -68,16 +68,8 @@ class NestedToken extends index_2.Token {
68
68
  }).map(child => {
69
69
  const e = (0, lint_1.generateForChild)(child, rect, 'no-ignored', index_1.default.msg('invalid content in <$1>', this.name));
70
70
  e.suggestions = [
71
- {
72
- desc: 'remove',
73
- range: [e.startIndex, e.endIndex],
74
- text: '',
75
- },
76
- {
77
- desc: 'comment',
78
- range: [e.startIndex, e.endIndex],
79
- text: `<!--${child.toString()}-->`,
80
- },
71
+ { desc: 'remove', range: [e.startIndex, e.endIndex], text: '' },
72
+ { desc: 'comment', range: [e.startIndex, e.endIndex], text: `<!--${child.toString()}-->` },
81
73
  ];
82
74
  return e;
83
75
  }),
@@ -74,7 +74,7 @@ let CommentToken = (() => {
74
74
  return [];
75
75
  }
76
76
  const e = (0, lint_1.generateForSelf)(this, { start }, 'unclosed-comment', index_1.default.msg('unclosed $1', 'HTML comment'));
77
- e.fix = { range: [e.endIndex, e.endIndex], text: '-->', desc: 'close' };
77
+ e.suggestions = [{ range: [e.endIndex, e.endIndex], text: '-->', desc: 'close' }];
78
78
  return [e];
79
79
  }
80
80
  /** @private */
@@ -33,11 +33,10 @@ class QuoteToken extends base_1.NowikiBaseToken {
33
33
  * @param endIndex 终点
34
34
  * @param length 长度
35
35
  */
36
- const getSuggestion = (startIndex, endIndex, length) => ({
37
- desc: 'escape',
38
- range: [startIndex, endIndex],
39
- text: '&apos;'.repeat(length),
40
- });
36
+ const getSuggestion = (startIndex, endIndex, length) => [
37
+ { desc: 'escape', range: [startIndex, endIndex], text: '&apos;'.repeat(length) },
38
+ { desc: 'remove', range: [startIndex, endIndex], text: '' },
39
+ ];
41
40
  if (previousSibling?.type === 'text' && previousSibling.data.endsWith(`'`)) {
42
41
  refError = (0, lint_1.generateForSelf)(this, rect, 'lonely-apos', message);
43
42
  const { startIndex: endIndex, startLine: endLine, startCol: endCol } = refError, [, { length }] = /(?:^|[^'])('+)$/u.exec(previousSibling.data), startIndex = start - length;
@@ -48,7 +47,7 @@ class QuoteToken extends base_1.NowikiBaseToken {
48
47
  startCol: endCol - length,
49
48
  endLine,
50
49
  endCol,
51
- suggestions: [getSuggestion(startIndex, endIndex, length)],
50
+ suggestions: getSuggestion(startIndex, endIndex, length),
52
51
  });
53
52
  }
54
53
  if (nextSibling?.type === 'text' && nextSibling.data.startsWith(`'`)) {
@@ -61,11 +60,14 @@ class QuoteToken extends base_1.NowikiBaseToken {
61
60
  startLine,
62
61
  startCol,
63
62
  endCol: startCol + length,
64
- suggestions: [getSuggestion(startIndex, endIndex, length)],
63
+ suggestions: getSuggestion(startIndex, endIndex, length),
65
64
  });
66
65
  }
67
66
  if (bold && this.closest('heading-title')) {
68
- errors.push((0, lint_1.generateForSelf)(this, rect, 'bold-header', 'bold in section header', 'warning'));
67
+ errors.push({
68
+ ...(0, lint_1.generateForSelf)(this, rect, 'bold-header', 'bold in section header', 'warning'),
69
+ suggestions: [{ desc: 'remove', range: [start, start + 3], text: '' }],
70
+ });
69
71
  }
70
72
  return errors;
71
73
  }
@@ -52,13 +52,7 @@ class ParamTagToken extends index_2.Token {
52
52
  const i = grandChildren.findIndex(({ type }) => type !== 'text'), str = grandChildren.slice(0, i === -1 ? undefined : i).map(String).join('');
53
53
  if (str && !(i === -1 ? /^[a-z]+(?:\[\])?\s*=/iu : /^[a-z]+(?:\[\])?\s*(?:=|$)/iu).test(str)) {
54
54
  const e = (0, lint_1.generateForChild)(child, rect, 'no-ignored', msg);
55
- e.suggestions = [
56
- {
57
- desc: 'remove',
58
- range: [e.startIndex, e.endIndex],
59
- text: '',
60
- },
61
- ];
55
+ e.suggestions = [{ desc: 'remove', range: [e.startIndex, e.endIndex], text: '' }];
62
56
  errors.push(e);
63
57
  }
64
58
  else {
@@ -80,24 +80,12 @@ let IncludeToken = (() => {
80
80
  const errors = [], { firstChild, closed, name } = this, rect = new rect_1.BoundingRect(this, start);
81
81
  if (firstChild.data.trim()) {
82
82
  const e = (0, lint_1.generateForChild)(firstChild, rect, 'no-ignored', 'useless attribute', 'warning');
83
- e.suggestions = [
84
- {
85
- desc: 'remove',
86
- range: [e.startIndex, e.endIndex],
87
- text: '',
88
- },
89
- ];
83
+ e.suggestions = [{ desc: 'remove', range: [e.startIndex, e.endIndex], text: '' }];
90
84
  errors.push(e);
91
85
  }
92
86
  if (!closed) {
93
87
  const e = (0, lint_1.generateForSelf)(this, rect, 'unclosed-comment', index_1.default.msg('unclosed $1', `<${name}>`));
94
- e.suggestions = [
95
- {
96
- desc: 'close',
97
- range: [e.endIndex, e.endIndex],
98
- text: `</${name}>`,
99
- },
100
- ];
88
+ e.suggestions = [{ desc: 'close', range: [e.endIndex, e.endIndex], text: `</${name}>` }];
101
89
  errors.push(e);
102
90
  }
103
91
  return errors;
@@ -204,7 +204,11 @@ class TranscludeToken extends index_2.Token {
204
204
  }
205
205
  const duplicatedArgs = this.getDuplicatedArgs().filter(([, parameter]) => !parameter[0].querySelector('ext'));
206
206
  if (duplicatedArgs.length > 0) {
207
- errors.push(...duplicatedArgs.flatMap(([, args]) => args).map(arg => (0, lint_1.generateForChild)(arg, rect, 'no-duplicate', 'duplicated parameter')));
207
+ errors.push(...duplicatedArgs.flatMap(([, args]) => args).map(arg => {
208
+ const e = (0, lint_1.generateForChild)(arg, rect, 'no-duplicate', 'duplicated parameter');
209
+ e.suggestions = [{ desc: 'remove', range: [e.startIndex - 1, e.endIndex], text: '' }];
210
+ return e;
211
+ }));
208
212
  }
209
213
  return errors;
210
214
  }
package/i18n/zh-hans.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "content to be moved out from the table": "将被移出表格的内容",
12
12
  "duplicated $1 attribute": "重复的$1属性",
13
13
  "duplicated category": "重复的分类",
14
- "duplicated id/name": "重复的id/name",
14
+ "duplicated id": "重复的id",
15
15
  "duplicated image $1 parameter": "重复的图片$1参数",
16
16
  "duplicated parameter": "重复参数",
17
17
  "extension tag in HTML tag attributes": "HTML标签属性中的扩展标签",
package/i18n/zh-hant.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "content to be moved out from the table": "將被移出表格的內容",
12
12
  "duplicated $1 attribute": "重複的$1屬性",
13
13
  "duplicated category": "重複的分類",
14
- "duplicated id/name": "重複的id/name",
14
+ "duplicated id": "重複的id",
15
15
  "duplicated image $1 parameter": "重複的圖片$1參數",
16
16
  "duplicated parameter": "重複參數",
17
17
  "extension tag in HTML tag attributes": "HTML標籤屬性中的擴展標籤",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wikilint",
3
- "version": "2.13.9",
3
+ "version": "2.14.0",
4
4
  "description": "A Node.js linter for MediaWiki markup",
5
5
  "keywords": [
6
6
  "mediawiki",
@@ -75,6 +75,6 @@
75
75
  "v8r": "^4.2.0"
76
76
  },
77
77
  "engines": {
78
- "node": ">=22.11.0"
78
+ "node": ">=18.12.0"
79
79
  }
80
80
  }