wikilint 2.10.0 → 2.11.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
@@ -11,7 +11,7 @@ export interface Config {
11
11
  readonly variants: string[];
12
12
  readonly excludes?: string[];
13
13
  }
14
- export type TokenTypes = 'root' | 'plain' | 'redirect' | 'redirect-syntax' | 'redirect-target' | 'onlyinclude' | 'noinclude' | 'include' | 'comment' | 'ext' | 'ext-attrs' | 'ext-attr-dirty' | 'ext-attr' | 'attr-key' | 'attr-value' | 'ext-inner' | 'arg' | 'arg-name' | 'arg-default' | 'hidden' | 'magic-word' | 'magic-word-name' | 'invoke-function' | 'invoke-module' | 'template' | 'template-name' | 'parameter' | 'parameter-key' | 'parameter-value' | 'heading' | 'heading-title' | 'heading-trail' | 'html' | 'html-attrs' | 'html-attr-dirty' | 'html-attr' | 'table' | 'tr' | 'td' | 'table-syntax' | 'table-attrs' | 'table-attr-dirty' | 'table-attr' | 'table-inter' | 'td-inner' | 'hr' | 'double-underscore' | 'link' | 'link-target' | 'link-text' | 'category' | 'file' | 'gallery-image' | 'imagemap-image' | 'image-parameter' | 'quote' | 'ext-link' | 'ext-link-text' | 'ext-link-url' | 'free-ext-link' | 'magic-link' | 'list' | 'dd' | 'converter' | 'converter-flags' | 'converter-flag' | 'converter-rule' | 'converter-rule-variant' | 'converter-rule-to' | 'converter-rule-from' | 'param-line' | 'imagemap-link';
14
+ export type TokenTypes = 'root' | 'plain' | 'redirect' | 'redirect-syntax' | 'redirect-target' | 'onlyinclude' | 'noinclude' | 'include' | 'comment' | 'ext' | 'ext-attrs' | 'ext-attr-dirty' | 'ext-attr' | 'attr-key' | 'attr-value' | 'ext-inner' | 'arg' | 'arg-name' | 'arg-default' | 'hidden' | 'magic-word' | 'magic-word-name' | 'invoke-function' | 'invoke-module' | 'template' | 'template-name' | 'parameter' | 'parameter-key' | 'parameter-value' | 'heading' | 'heading-title' | 'heading-trail' | 'html' | 'html-attrs' | 'html-attr-dirty' | 'html-attr' | 'table' | 'tr' | 'td' | 'table-syntax' | 'table-attrs' | 'table-attr-dirty' | 'table-attr' | 'table-inter' | 'td-inner' | 'hr' | 'double-underscore' | 'link' | 'link-target' | 'link-text' | 'category' | 'file' | 'gallery-image' | 'imagemap-image' | 'image-parameter' | 'quote' | 'ext-link' | 'ext-link-text' | 'ext-link-url' | 'free-ext-link' | 'magic-link' | 'list' | 'dd' | 'list-range' | 'converter' | 'converter-flags' | 'converter-flag' | 'converter-rule' | 'converter-rule-variant' | 'converter-rule-to' | 'converter-rule-from' | 'param-line' | 'imagemap-link';
15
15
  export declare const rules: readonly ["bold-header", "format-leakage", "fostered-content", "h1", "illegal-attr", "insecure-style", "invalid-gallery", "invalid-imagemap", "invalid-invoke", "invalid-isbn", "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
16
  export declare namespace LintError {
17
17
  type Severity = 'error' | 'warning';
package/dist/index.js CHANGED
@@ -38,23 +38,27 @@ const Parser = {
38
38
  /** @implements */
39
39
  normalizeTitle(title, defaultNs = 0, include, config = Parser.getConfig(), halfParsed, decode = false, selfLink = false) {
40
40
  const { Title } = require('./lib/title');
41
+ let titleObj;
41
42
  if (halfParsed) {
42
- return new Title(title, defaultNs, config, decode, selfLink);
43
+ titleObj = new Title(title, defaultNs, config, decode, selfLink);
43
44
  }
44
- const { Token } = require('./src/index');
45
- return debug_1.Shadow.run(() => {
46
- const root = new Token(title, config);
47
- root.type = 'root';
48
- root.parseOnce(0, include).parseOnce();
49
- const titleObj = new Title(root.toString(), defaultNs, config, decode, selfLink);
50
- for (const key of ['main', 'fragment']) {
51
- const str = titleObj[key];
52
- if (str?.includes('\0')) {
53
- titleObj[key] = root.buildFromStr(str, constants_1.BuildMethod.Text);
45
+ else {
46
+ const { Token } = require('./src/index');
47
+ titleObj = debug_1.Shadow.run(() => {
48
+ const root = new Token(title, config);
49
+ root.type = 'root';
50
+ root.parseOnce(0, include).parseOnce();
51
+ const t = new Title(root.toString(), defaultNs, config, decode, selfLink);
52
+ for (const key of ['main', 'fragment']) {
53
+ const str = t[key];
54
+ if (str?.includes('\0')) {
55
+ t[key] = root.buildFromStr(str, constants_1.BuildMethod.Text);
56
+ }
54
57
  }
55
- }
56
- return titleObj;
57
- });
58
+ return t;
59
+ });
60
+ }
61
+ return titleObj;
58
62
  },
59
63
  /** @implements */
60
64
  parse(wikitext, include, maxStage = constants_1.MAX_STAGE, config = Parser.getConfig()) {
package/dist/lib/text.js CHANGED
@@ -60,8 +60,8 @@ class AstText extends node_1.AstNode {
60
60
  });
61
61
  }
62
62
  /** @private */
63
- toString() {
64
- return this.data;
63
+ toString(skip) {
64
+ return skip && !this.parentNode?.getAttribute('built') ? (0, string_1.removeComment)(this.data) : this.data;
65
65
  }
66
66
  /** @private */
67
67
  text() {
@@ -192,10 +192,7 @@ class AstText extends node_1.AstNode {
192
192
  }
193
193
  else if (char === ']' && previousType === 'free-ext-link' && severity === 'error') {
194
194
  const i = start - previousSibling.toString().length;
195
- e.fix = {
196
- range: [i, i],
197
- text: '[',
198
- };
195
+ e.fix = { range: [i, i], text: '[' };
199
196
  }
200
197
  errors.push(e);
201
198
  }
package/dist/lib/title.js CHANGED
@@ -2,11 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Title = void 0;
4
4
  const string_1 = require("../util/string");
5
- /**
6
- * PHP的`rawurldecode`函数的JavaScript实现
7
- * @param str 要解码的字符串
8
- */
9
- const rawurldecode = (str) => decodeURIComponent(str.replace(/%(?![\da-f]{2})/giu, '%25'));
10
5
  /** MediaWiki页面标题对象 */
11
6
  class Title {
12
7
  #main;
@@ -56,12 +51,12 @@ class Title {
56
51
  if (decode && title.includes('%')) {
57
52
  try {
58
53
  const encoded = /%(?!21|3[ce]|5[bd]|7[b-d])[\da-f]{2}/iu.test(title);
59
- title = rawurldecode(title);
54
+ title = (0, string_1.rawurldecode)(title);
60
55
  this.encoded = encoded;
61
56
  }
62
57
  catch { }
63
58
  }
64
- title = title.replace(/_/gu, ' ').trim();
59
+ title = title.replace(/[_ ]+/gu, ' ').trim();
65
60
  if (subpage) {
66
61
  this.ns = 0;
67
62
  }
@@ -73,7 +68,7 @@ class Title {
73
68
  }
74
69
  const m = title.split(':');
75
70
  if (m.length > 1) {
76
- const id = config.nsid[m[0].trim().toLowerCase()];
71
+ const k = m[0].trim().toLowerCase(), id = Object.prototype.hasOwnProperty.call(config.nsid, k) && config.nsid[k];
77
72
  if (id) {
78
73
  ns = id;
79
74
  title = m.slice(1).join(':').trim();
@@ -86,7 +81,7 @@ class Title {
86
81
  let fragment = title.slice(i + 1).trimEnd();
87
82
  if (fragment.includes('%')) {
88
83
  try {
89
- fragment = rawurldecode(fragment);
84
+ fragment = (0, string_1.rawurldecode)(fragment);
90
85
  }
91
86
  catch { }
92
87
  }
@@ -1,21 +1,36 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseList = void 0;
4
+ const html_1 = require("../util/html");
4
5
  const list_1 = require("../src/nowiki/list");
5
6
  const dd_1 = require("../src/nowiki/dd");
6
7
  /**
7
8
  * 解析列表
8
9
  * @param wikitext
10
+ * @param state
11
+ * @param state.lastPrefix 上一个列表的前缀
9
12
  * @param config
10
13
  * @param accum
11
14
  */
12
- const parseList = (wikitext, config, accum) => {
15
+ const parseList = (wikitext, state, config, accum) => {
13
16
  const mt = /^((?:\0\d+c\x7F)*)([;:*#]+\s*)/u.exec(wikitext);
14
17
  if (!mt) {
18
+ state.lastPrefix = '';
15
19
  return wikitext;
16
20
  }
17
- const [total, comment, prefix] = mt, parts = prefix.split(/(?=;)/u);
18
- let text = comment + parts.map((_, i) => `\0${accum.length + i}d\x7F`).join('') + wikitext.slice(total.length), dt = parts.length - (parts[0].startsWith(';') ? 0 : 1);
21
+ const [total, comment, prefix] = mt, prefix2 = prefix.replace(/;/gu, ':'), commonPrefixLength = (0, html_1.getCommon)(prefix2, state.lastPrefix), parts = (commonPrefixLength > 1 ? prefix.slice(commonPrefixLength - 1) : prefix).split(/(?=;)/u), isDt = parts[0].startsWith(';');
22
+ let dt = parts.length - (isDt ? 0 : 1);
23
+ if (commonPrefixLength > 1) {
24
+ const commonPrefix = prefix.slice(0, commonPrefixLength - 1);
25
+ if (isDt) {
26
+ parts.unshift(commonPrefix);
27
+ }
28
+ else {
29
+ parts[0] = commonPrefix + parts[0];
30
+ }
31
+ }
32
+ state.lastPrefix = prefix2;
33
+ let text = comment + parts.map((_, i) => `\0${accum.length + i}d\x7F`).join('') + wikitext.slice(total.length);
19
34
  for (const part of parts) {
20
35
  // @ts-expect-error abstract class
21
36
  new list_1.ListToken(part, config, accum);
@@ -23,7 +38,7 @@ const parseList = (wikitext, config, accum) => {
23
38
  if (!dt) {
24
39
  return text;
25
40
  }
26
- const { html: [normalTags] } = config, fullRegex = /:+\s*|-\{|\0\d+[xq]\x7F/gu;
41
+ const { html: [normalTags] } = config, fullRegex = /:+|-\{|\0\d+[xq]\x7F/gu;
27
42
  let regex = fullRegex, ex = regex.exec(text), lt = 0, lb = false, li = false, lc = 0;
28
43
  /**
29
44
  * 创建`DdToken`
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseMagicLinks = void 0;
4
4
  const string_1 = require("../util/string");
5
5
  const magicLink_1 = require("../src/magicLink");
6
- const sepRegex = /[^,;\\.:!?)][,;\\.:!?)]+$/u, sepLparRegex = /[^,;\\.:!?][,;\\.:!?]+$/u;
7
6
  /**
8
7
  * 解析自由外链
9
8
  * @param wikitext
@@ -21,7 +20,7 @@ const parseMagicLinks = (wikitext, config, accum) => {
21
20
  trail = url.slice(m2.index);
22
21
  url = url.slice(0, m2.index);
23
22
  }
24
- const sep = url.includes('(') ? sepLparRegex : sepRegex, sepChars = sep.exec(url);
23
+ const sep = url.includes('(') ? /[^,;\\.:!?][,;\\.:!?]+$/u : /[^,;\\.:!?)][,;\\.:!?)]+$/u, sepChars = sep.exec(url);
25
24
  if (sepChars) {
26
25
  let correction = 1;
27
26
  if (sepChars[0][1] === ';'
package/dist/src/arg.js CHANGED
@@ -62,10 +62,7 @@ class ArgToken extends index_2.Token {
62
62
  if (!this.getAttribute('include')) {
63
63
  const e = (0, lint_1.generateForSelf)(this, { start }, 'no-arg', 'unexpected template argument');
64
64
  if (argDefault) {
65
- e.fix = {
66
- range: [start, e.endIndex],
67
- text: argDefault.text(),
68
- };
65
+ e.fix = { range: [start, e.endIndex], text: argDefault.text() };
69
66
  }
70
67
  return [e];
71
68
  }
@@ -252,10 +252,7 @@ class AttributeToken extends index_2.Token {
252
252
  const e = (0, lint_1.generateForChild)(lastChild, rect, 'unclosed-quote', index_1.default.msg('unclosed $1', 'quotes'), 'warning');
253
253
  e.startIndex--;
254
254
  e.startCol--;
255
- const fix = {
256
- range: [e.endIndex, e.endIndex],
257
- text: this.#quotes[0],
258
- };
255
+ const fix = { range: [e.endIndex, e.endIndex], text: this.#quotes[0] };
259
256
  if (lastChild.childNodes.some(({ type: t, data }) => t === 'text' && /\s/u.test(data))) {
260
257
  e.suggestions = [
261
258
  {
@@ -8,7 +8,6 @@ const index_1 = require("../index");
8
8
  const index_2 = require("./index");
9
9
  const atom_1 = require("./atom");
10
10
  const attribute_1 = require("./attribute");
11
- const regex = /([^\s/](?:(?!\0\d+~\x7F)[^\s/=])*)(?:((?:\s(?:\s|\0\d+c\x7F)*)?(?:=|\0\d+~\x7F)(?:\s|\0\d+c\x7F)*)(?:(["'])(.*?)(\3|$)|(\S*)))?/gsu;
12
11
  /**
13
12
  * 将属性类型转换为单属性类型
14
13
  * @param type 属性类型
@@ -38,7 +37,7 @@ class AttributesToken extends index_2.Token {
38
37
  this.#type = type;
39
38
  this.setAttribute('name', name);
40
39
  if (attr) {
41
- regex.lastIndex = 0;
40
+ const regex = /([^\s/](?:(?!\0\d+~\x7F)[^\s/=])*)(?:((?:\s(?:\s|\0\d+c\x7F)*)?(?:=|\0\d+~\x7F)(?:\s|\0\d+c\x7F)*)(?:(["'])(.*?)(\3|$)|(\S*)))?/gsu;
42
41
  let out = '', mt = regex.exec(attr), lastIndex = 0;
43
42
  const insertDirty = /** 插入无效属性 */ () => {
44
43
  if (out) {
@@ -62,16 +62,13 @@ class ConverterFlagsToken extends index_2.Token {
62
62
  && (variantFlags.size > 0 || !validFlags.has(flag))) {
63
63
  const e = (0, lint_1.generateForChild)(child, rect, 'no-ignored', 'invalid conversion flag');
64
64
  if (variantFlags.size === 0 && definedFlags.has(flag.toUpperCase())) {
65
- e.fix = {
66
- range: [e.startIndex, e.endIndex],
67
- text: flag.toUpperCase(),
68
- };
65
+ e.fix = { range: [e.startIndex, e.endIndex], text: flag.toUpperCase() };
69
66
  }
70
67
  else {
71
68
  e.suggestions = [
72
69
  {
73
70
  desc: 'remove',
74
- range: [e.startIndex, e.endIndex],
71
+ range: [e.startIndex - (i && 1), e.endIndex],
75
72
  text: '',
76
73
  },
77
74
  ];
@@ -59,12 +59,13 @@ class GalleryToken extends index_2.Token {
59
59
  for (let i = 0; i < this.length; i++) {
60
60
  const child = this.childNodes[i], str = child.toString(), { length } = str, trimmed = str.trim(), startLine = top + i, startCol = i ? 0 : left;
61
61
  if (child.type === 'noinclude' && trimmed && !/^<!--.*-->$/u.test(trimmed)) {
62
+ const endIndex = start + length;
62
63
  errors.push({
63
64
  rule: 'no-ignored',
64
65
  message: index_1.default.msg('invalid content in <$1>', 'gallery'),
65
66
  severity: trimmed.startsWith('|') ? 'warning' : 'error',
66
67
  startIndex: start,
67
- endIndex: start + length,
68
+ endIndex,
68
69
  startLine,
69
70
  endLine: startLine,
70
71
  startCol,
@@ -72,12 +73,12 @@ class GalleryToken extends index_2.Token {
72
73
  suggestions: [
73
74
  {
74
75
  desc: 'remove',
75
- range: [start, start + length],
76
+ range: [start, endIndex],
76
77
  text: '',
77
78
  },
78
79
  {
79
80
  desc: 'comment',
80
- range: [start, start + length],
81
+ range: [start, endIndex],
81
82
  text: `<!--${str}-->`,
82
83
  },
83
84
  ],
package/dist/src/html.js CHANGED
@@ -125,10 +125,7 @@ class HtmlToken extends index_1.Token {
125
125
  else if (msg === 'tag that is both closing and self-closing') {
126
126
  const { html: [, , voidTags] } = this.getAttribute('config');
127
127
  if (voidTags.includes(this.name)) {
128
- error.fix = {
129
- range: [start + 1, start + 2],
130
- text: '',
131
- };
128
+ error.fix = { range: [start + 1, start + 2], text: '' };
132
129
  }
133
130
  }
134
131
  errors.push(error);
@@ -8,7 +8,7 @@ const index_2 = require("./index");
8
8
  exports.galleryParams = new Set(['alt', 'link', 'lang', 'page', 'caption']);
9
9
  function validate(key, val, config, halfParsed, ext) {
10
10
  val = val.trim();
11
- let value = val.replace(/\0\d+t\x7F/gu, '').trim();
11
+ let value = val.replace(key === 'link' ? /\0\d+[tq]\x7F/gu : /\0\d+t\x7F/gu, '').trim();
12
12
  switch (key) {
13
13
  case 'width':
14
14
  return !value || /^(?:\d+x?|\d*x\d+)(?:\s*px)?$/u.test(value);
@@ -16,9 +16,9 @@ function validate(key, val, config, halfParsed, ext) {
16
16
  if (!value) {
17
17
  return val;
18
18
  }
19
- const regex = new RegExp(String.raw `^(?:(?:${config.protocol}|//)${string_1.extUrlCharFirst}|\0\d+m\x7F)${string_1.extUrlChar}$`, 'iu');
20
- if (regex.test(value)) {
21
- return val;
19
+ const re1 = new RegExp(String.raw `^(?:${config.protocol}|//|\0\d+m\x7F)`, 'iu'), re2 = new RegExp(String.raw `^(?:(?:${config.protocol}|//)${string_1.extUrlCharFirst}|\0\d+m\x7F)${string_1.extUrlChar}$`, 'iu');
20
+ if (re1.test(value)) {
21
+ return re2.test(value) && val;
22
22
  }
23
23
  else if (value.startsWith('[[') && value.endsWith(']]')) {
24
24
  value = value.slice(2, -2);
@@ -106,11 +106,8 @@ class ImageParameterToken extends index_2.Token {
106
106
  lint(start = this.getAbsoluteIndex(), re) {
107
107
  const errors = super.lint(start, re), { link, name } = this;
108
108
  if (name === 'invalid') {
109
- const e = (0, lint_1.generateForSelf)(this, { start }, 'invalid-gallery', 'invalid gallery image parameter');
110
- e.fix = {
111
- range: [start, start + e.endIndex],
112
- text: '',
113
- };
109
+ const e = (0, lint_1.generateForSelf)(this, { start }, 'invalid-gallery', 'invalid image parameter');
110
+ e.fix = { range: [start - 1, e.endIndex], text: '' };
114
111
  errors.push(e);
115
112
  }
116
113
  else if (typeof link === 'object' && link.encoded) {
package/dist/src/index.js CHANGED
@@ -275,10 +275,10 @@ class Token extends element_1.AstElement {
275
275
  return;
276
276
  }
277
277
  const { parseList } = require('../parser/list');
278
- const lines = this.firstChild.toString().split('\n');
278
+ const lines = this.firstChild.toString().split('\n'), state = { lastPrefix: '' };
279
279
  let i = this.type === 'root' || this.type === 'ext-inner' && this.name === 'poem' ? 0 : 1;
280
280
  for (; i < lines.length; i++) {
281
- lines[i] = parseList(lines[i], this.#config, this.#accum);
281
+ lines[i] = parseList(lines[i], state, this.#config, this.#accum);
282
282
  }
283
283
  this.setText(lines.join('\n'));
284
284
  }
@@ -2,8 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LinkBaseToken = void 0;
4
4
  const lint_1 = require("../../util/lint");
5
- const rect_1 = require("../../lib/rect");
6
5
  const constants_1 = require("../../util/constants");
6
+ const rect_1 = require("../../lib/rect");
7
7
  const index_1 = require("../../index");
8
8
  const index_2 = require("../index");
9
9
  const atom_1 = require("../atom");
@@ -90,14 +90,11 @@ class LinkBaseToken extends index_2.Token {
90
90
  if (type === 'link' || type === 'category') {
91
91
  const textNode = linkText?.childNodes.find((c) => c.type === 'text' && c.data.includes('|'));
92
92
  if (textNode) {
93
- const e = (0, lint_1.generateForChild)(linkText, rect, 'pipe-like', 'additional "|" in the link text', 'warning');
93
+ const e = (0, lint_1.generateForChild)(linkText, rect, 'pipe-like', 'additional "|" in the link text', 'warning'), i = e.startIndex + textNode.getRelativeIndex();
94
94
  e.suggestions = [
95
95
  {
96
96
  desc: 'escape',
97
- range: [
98
- e.startIndex + textNode.getRelativeIndex(),
99
- e.startIndex + textNode.getRelativeIndex() + textNode.data.length,
100
- ],
97
+ range: [i, i + textNode.data.length],
101
98
  text: textNode.data.replace(/\|/gu, '&#124;'),
102
99
  },
103
100
  ];
@@ -8,7 +8,6 @@ import type { Token, AtomToken } from '../../internal';
8
8
  * @classdesc `{childNodes: [AtomToken, ...ImageParameterToken]}`
9
9
  */
10
10
  export declare abstract class FileToken extends LinkBaseToken {
11
- #private;
12
11
  readonly childNodes: readonly [AtomToken, ...ImageParameterToken[]];
13
12
  abstract get lastChild(): AtomToken | ImageParameterToken;
14
13
  get type(): 'file' | 'gallery-image' | 'imagemap-image';
@@ -27,12 +26,6 @@ export declare abstract class FileToken extends LinkBaseToken {
27
26
  * @param key 参数名
28
27
  */
29
28
  getArgs(key: string): ImageParameterToken[];
30
- /** 获取图片框架属性参数节点 */
31
- getFrameArgs(): ImageParameterToken[];
32
- /** 获取图片水平对齐参数节点 */
33
- getHorizAlignArgs(): ImageParameterToken[];
34
- /** 获取图片垂直对齐参数节点 */
35
- getVertAlignArgs(): ImageParameterToken[];
36
29
  /**
37
30
  * 获取生效的指定图片参数
38
31
  * @param key 参数名
@@ -7,7 +7,12 @@ const rect_1 = require("../../lib/rect");
7
7
  const index_1 = require("../../index");
8
8
  const base_1 = require("./base");
9
9
  const imageParameter_1 = require("../imageParameter");
10
- const frame = new Set(['manualthumb', 'frameless', 'framed', 'thumbnail']), horizAlign = new Set(['left', 'right', 'center', 'none']), vertAlign = new Set(['baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom']);
10
+ const frame = new Map([
11
+ ['manualthumb', 'Thumb'],
12
+ ['frameless', 'Frameless'],
13
+ ['framed', 'Frame'],
14
+ ['thumbnail', 'Thumb'],
15
+ ]), horizAlign = new Set(['left', 'right', 'center', 'none']), vertAlign = new Set(['baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom']);
11
16
  /**
12
17
  * a more sophisticated string-explode function
13
18
  * @param start start syntax of a nested AST node
@@ -64,10 +69,17 @@ class FileToken extends base_1.LinkBaseToken {
64
69
  const errors = super.lint(start, re), args = this.getAllArgs().filter(({ childNodes }) => {
65
70
  const visibleNodes = childNodes.filter(node => node.text().trim());
66
71
  return visibleNodes.length !== 1 || visibleNodes[0].type !== 'arg';
67
- }), 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)), rect = new rect_1.BoundingRect(this, start);
72
+ }), keys = [...new Set(args.map(({ name }) => name))], frameKeys = keys.filter(key => frame.has(key)), horizAlignKeys = keys.filter(key => horizAlign.has(key)), vertAlignKeys = keys.filter(key => vertAlign.has(key)), [fr] = frameKeys, unscaled = fr === 'framed' || fr === 'manualthumb', rect = new rect_1.BoundingRect(this, start);
68
73
  if (this.closest('ext-link-text') && this.getValue('link')?.trim() !== '') {
69
74
  errors.push((0, lint_1.generateForSelf)(this, rect, 'nested-link', 'internal link in an external link'));
70
75
  }
76
+ if (unscaled) {
77
+ for (const arg of args.filter(({ name }) => name === 'width')) {
78
+ const e = (0, lint_1.generateForChild)(arg, rect, 'invalid-gallery', 'invalid image parameter');
79
+ e.fix = { range: [e.startIndex - 1, e.endIndex], text: '' };
80
+ errors.push(e);
81
+ }
82
+ }
71
83
  if (args.length === keys.length
72
84
  && frameKeys.length < 2
73
85
  && horizAlignKeys.length < 2
@@ -81,6 +93,9 @@ class FileToken extends base_1.LinkBaseToken {
81
93
  */
82
94
  const generate = (msg, p1) => (arg) => (0, lint_1.generateForChild)(arg, rect, 'no-duplicate', index_1.default.msg(`${msg} image $1 parameter`, p1));
83
95
  for (const key of keys) {
96
+ if (key === 'invalid' || key === 'width' && unscaled) {
97
+ continue;
98
+ }
84
99
  let relevantArgs = args.filter(({ name }) => name === key);
85
100
  if (key === 'caption') {
86
101
  relevantArgs = [...relevantArgs.slice(0, -1).filter(arg => arg.text()), ...relevantArgs.slice(-1)];
@@ -111,34 +126,13 @@ class FileToken extends base_1.LinkBaseToken {
111
126
  getArgs(key) {
112
127
  return this.getAllArgs().filter(({ name }) => key === name);
113
128
  }
114
- /**
115
- * 获取特定类型的图片属性参数节点
116
- * @param keys 接受的参数名
117
- * @param type 类型名
118
- */
119
- #getTypedArgs(keys, type) {
120
- const args = this.getAllArgs().filter(({ name }) => keys.has(name));
121
- return args;
122
- }
123
- /** 获取图片框架属性参数节点 */
124
- getFrameArgs() {
125
- return this.#getTypedArgs(frame, 'frame');
126
- }
127
- /** 获取图片水平对齐参数节点 */
128
- getHorizAlignArgs() {
129
- return this.#getTypedArgs(horizAlign, 'horizontal-align');
130
- }
131
- /** 获取图片垂直对齐参数节点 */
132
- getVertAlignArgs() {
133
- return this.#getTypedArgs(vertAlign, 'vertical-align');
134
- }
135
129
  /**
136
130
  * 获取生效的指定图片参数
137
131
  * @param key 参数名
138
132
  */
139
133
  getArg(key) {
140
134
  const args = this.getArgs(key);
141
- return args[args.length - 1];
135
+ return args[key === 'manualthumb' ? 0 : args.length - 1];
142
136
  }
143
137
  /**
144
138
  * 获取生效的指定图片参数值
@@ -36,10 +36,7 @@ class RedirectTargetToken extends base_1.LinkBaseToken {
36
36
  const e = (0, lint_1.generateForChild)(this.lastChild, { start }, 'no-ignored', 'useless link text');
37
37
  e.startIndex--;
38
38
  e.startCol--;
39
- e.fix = {
40
- range: [e.startIndex, e.endIndex],
41
- text: '',
42
- };
39
+ e.fix = { range: [e.startIndex, e.endIndex], text: '' };
43
40
  errors.push(e);
44
41
  }
45
42
  return errors;
@@ -61,7 +61,7 @@ class NestedToken extends index_2.Token {
61
61
  },
62
62
  {
63
63
  desc: 'comment',
64
- range: [e.startIndex, e.startIndex],
64
+ range: [e.startIndex, e.endIndex],
65
65
  text: `<!--${child.toString()}-->`,
66
66
  },
67
67
  ];
@@ -74,10 +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 = {
78
- range: [e.endIndex, e.endIndex],
79
- text: '-->',
80
- };
77
+ e.fix = { range: [e.endIndex, e.endIndex], text: '-->' };
81
78
  return [e];
82
79
  }
83
80
  /** @private */
@@ -14,10 +14,7 @@ class NowikiToken extends base_1.NowikiBaseToken {
14
14
  const { name, firstChild: { data } } = this;
15
15
  if ((name === 'templatestyles' || name === 'section') && data) {
16
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
- };
17
+ e.fix = { range: [start, e.endIndex], text: '' };
21
18
  return [e];
22
19
  }
23
20
  return super.lint(start, new RegExp(String.raw `<\s*(?:/\s*)${name === 'nowiki' ? '' : '?'}(${name})\b`, 'giu'));
@@ -19,6 +19,11 @@ class QuoteToken extends base_1.NowikiBaseToken {
19
19
  return this.innerText.length !== 3;
20
20
  }
21
21
  /** @private */
22
+ text() {
23
+ const { parentNode, innerText } = this;
24
+ return parentNode?.type === 'image-parameter' && parentNode.name !== 'caption' ? '' : innerText;
25
+ }
26
+ /** @private */
22
27
  lint(start = this.getAbsoluteIndex()) {
23
28
  const { previousSibling, nextSibling, bold } = this, message = index_1.default.msg('lonely "$1"', `'`), errors = [], rect = new rect_1.BoundingRect(this, start);
24
29
  let refError;
@@ -33,8 +33,7 @@ class ParameterToken extends index_2.Token {
33
33
  }
34
34
  /** @private */
35
35
  trimName(name, set = true) {
36
- const trimmed = (typeof name === 'string' ? name : name.toString(true))
37
- .replace(/^[ \t\n\0\v]+|([^ \t\n\0\v])[ \t\n\0\v]+$/gu, '$1');
36
+ const trimmed = (0, string_1.trimPHP)(typeof name === 'string' ? name : name.toString(true));
38
37
  this.setAttribute('name', trimmed);
39
38
  return trimmed;
40
39
  }
@@ -69,10 +68,7 @@ class ParameterToken extends index_2.Token {
69
68
  e.startCol = e.endCol;
70
69
  e.endIndex++;
71
70
  e.endCol++;
72
- e.fix = {
73
- range: [e.startIndex, e.endIndex],
74
- text: '{{=}}',
75
- };
71
+ e.fix = { range: [e.startIndex, e.endIndex], text: '{{=}}' };
76
72
  errors.push(e);
77
73
  }
78
74
  return errors;
@@ -75,19 +75,19 @@ class ExtToken extends index_3.TagPairToken {
75
75
  case 'references': {
76
76
  const { NestedToken } = require('../nested');
77
77
  // @ts-expect-error abstract class
78
- innerToken = new NestedToken(inner, /<!--.*?(?:-->|$)|<(ref)(\s[^>]*)?>(.*?)<\/(ref\s*)>/gisu, ['ref'], newConfig, accum);
78
+ innerToken = new NestedToken(inner, /<!--.*?(?:-->|$)|<(ref)(\s[^>]*?)?(?:\/>|>(.*?)<\/(ref\s*)>)/gisu, ['ref'], newConfig, accum);
79
79
  break;
80
80
  }
81
81
  case 'choose': {
82
82
  const { NestedToken } = require('../nested');
83
83
  // @ts-expect-error abstract class
84
- innerToken = new NestedToken(inner, /<(option|choicetemplate)(\s[^>]*)?>(.*?)<\/(\1)>/gsu, ['option', 'choicetemplate'], newConfig, accum);
84
+ innerToken = new NestedToken(inner, /<(option|choicetemplate)(\s[^>]*?)?(?:\/>|>(.*?)<\/(\1)>)/gsu, ['option', 'choicetemplate'], newConfig, accum);
85
85
  break;
86
86
  }
87
87
  case 'combobox': {
88
88
  const { NestedToken } = require('../nested');
89
89
  // @ts-expect-error abstract class
90
- innerToken = new NestedToken(inner, /<(combooption)(\s[^>]*)?>(.*?)<\/(combooption\s*)>/gisu, ['combooption'], newConfig, accum);
90
+ innerToken = new NestedToken(inner, /<(combooption)(\s[^>]*?)?(?:\/>|>(.*?)<\/(combooption\s*)>)/gisu, ['combooption'], newConfig, accum);
91
91
  break;
92
92
  }
93
93
  case 'gallery': {
@@ -51,9 +51,9 @@ class TranscludeToken extends index_2.Token {
51
51
  }
52
52
  const isFunction = title.includes(':');
53
53
  if (isFunction || parts.length === 0 && !this.#raw) {
54
- const [magicWord, ...arg] = title.split(':'), cleaned = (0, string_1.removeComment)(magicWord), name = cleaned[arg.length > 0 ? 'trimStart' : 'trim'](), lcName = name.toLowerCase(), canonicalName = insensitive[lcName], isSensitive = sensitive.includes(name), isVar = isSensitive || insensitiveVars.has(canonicalName);
54
+ const [magicWord, ...arg] = title.split(':'), cleaned = (0, string_1.removeComment)(magicWord), name = cleaned[arg.length > 0 ? 'trimStart' : 'trim'](), lcName = name.toLowerCase(), canonicalName = Object.prototype.hasOwnProperty.call(insensitive, lcName) && insensitive[lcName], isSensitive = sensitive.includes(name), isVar = isSensitive || insensitiveVars.has(canonicalName);
55
55
  if (isVar || isFunction && canonicalName) {
56
- this.setAttribute('name', canonicalName ?? lcName);
56
+ this.setAttribute('name', canonicalName || lcName);
57
57
  this.#type = 'magic-word';
58
58
  const pattern = new RegExp(String.raw `^\s*${name}\s*$`, isSensitive ? 'u' : 'iu'), token = new syntax_1.SyntaxToken(magicWord, pattern, 'magic-word-name', config, accum, {});
59
59
  super.insertAt(token);
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getCommon = void 0;
4
+ /**
5
+ * get common prefix length
6
+ * @param prefix
7
+ * @param lastPrefix
8
+ */
9
+ const getCommon = (prefix, lastPrefix) => prefix.startsWith(lastPrefix) ? lastPrefix.length : [...lastPrefix].findIndex((ch, i) => ch !== prefix[i]);
10
+ exports.getCommon = getCommon;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.noWrap = exports.decodeHtml = exports.text = exports.escapeRegExp = exports.removeComment = exports.tidy = exports.extUrlChar = exports.extUrlCharFirst = void 0;
3
+ exports.noWrap = exports.decodeHtml = exports.text = exports.trimPHP = exports.rawurldecode = exports.escapeRegExp = exports.removeComment = exports.tidy = exports.extUrlChar = exports.extUrlCharFirst = void 0;
4
4
  const commonExtUrlChar = String.raw `[^[\]<>"\0-\x1F\x7F\p{Zs}\uFFFD]`;
5
5
  exports.extUrlCharFirst = String.raw `(?:\[[\da-f:.]+\]|${commonExtUrlChar})`;
6
6
  exports.extUrlChar = String.raw `(?:${commonExtUrlChar}|\0\d+[c!~]\x7F)*`;
@@ -16,6 +16,14 @@ exports.tidy = factory(/[\0\x7F]|\r$/gmu, '');
16
16
  exports.removeComment = factory(/\0\d+c\x7F/gu, '');
17
17
  /** escape special chars for RegExp constructor */
18
18
  exports.escapeRegExp = factory(/[\\{}()|.?*+^$[\]]/gu, String.raw `\$&`);
19
+ /**
20
+ * PHP的`rawurldecode`函数的JavaScript实现
21
+ * @param str 要解码的字符串
22
+ */
23
+ const rawurldecode = (str) => decodeURIComponent(str.replace(/%(?![\da-f]{2})/giu, '%25'));
24
+ exports.rawurldecode = rawurldecode;
25
+ /** PHP的`trim`函数的JavaScript实现 */
26
+ exports.trimPHP = factory(/^[ \t\n\0\v]+|([^ \t\n\0\v])[ \t\n\0\v]+$/gu, '$1');
19
27
  /**
20
28
  * extract effective wikitext
21
29
  * @param childNodes a Token's contents
@@ -23,9 +31,9 @@ exports.escapeRegExp = factory(/[\\{}()|.?*+^$[\]]/gu, String.raw `\$&`);
23
31
  */
24
32
  const text = (childNodes, separator = '') => childNodes.map(child => typeof child === 'string' ? child : child.text()).join(separator);
25
33
  exports.text = text;
26
- const names = { lt: '<', gt: '>', lbrack: '[', rbrack: ']', lbrace: '{', rbrace: '}' };
34
+ const names = { lt: '<', gt: '>', lbrack: '[', rbrack: ']', lbrace: '{', rbrace: '}', nbsp: ' ' };
27
35
  /** decode HTML entities */
28
- exports.decodeHtml = factory(/&(?:#(\d+|x[\da-fA-F]+)|([lLgG][tT]|[lr]brac[ke]));/gu, (_, code, name) => code
36
+ exports.decodeHtml = factory(/&(?:#(\d+|x[\da-fA-F]+)|([lLgG][tT]|[lr]brac[ke]|nbsp));/gu, (_, code, name) => code
29
37
  ? String.fromCodePoint(Number((/^x/iu.test(code) ? '0' : '') + code))
30
38
  : names[name.toLowerCase()]);
31
39
  /** escape newlines */
package/i18n/zh-hans.json CHANGED
@@ -27,7 +27,7 @@
27
27
  "invalid content in <$1>": "<$1>内的无效内容",
28
28
  "invalid conversion flag": "无效的转换标记",
29
29
  "invalid gallery image": "无效的图库图片",
30
- "invalid gallery image parameter": "无效的图库图片参数",
30
+ "invalid image parameter": "无效的图片参数",
31
31
  "invalid ISBN": "无效的ISBN",
32
32
  "invalid link in <imagemap>": "无效的<imagemap>链接",
33
33
  "invalid parameter of <$1>": "<$1>的无效参数",
package/i18n/zh-hant.json CHANGED
@@ -27,7 +27,7 @@
27
27
  "invalid content in <$1>": "<$1>內的無效內容",
28
28
  "invalid conversion flag": "無效的轉換標記",
29
29
  "invalid gallery image": "無效的圖庫圖片",
30
- "invalid gallery image parameter": "無效的圖庫圖片參數",
30
+ "invalid image parameter": "無效的圖片參數",
31
31
  "invalid ISBN": "無效的ISBN",
32
32
  "invalid link in <imagemap>": "無效的<imagemap>連結",
33
33
  "invalid parameter of <$1>": "<$1>的無效參數",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wikilint",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "A Node.js linter for MediaWiki markup",
5
5
  "keywords": [
6
6
  "mediawiki",
@@ -51,20 +51,20 @@
51
51
  "chalk": "^4.1.2"
52
52
  },
53
53
  "devDependencies": {
54
- "@stylistic/eslint-plugin": "^2.1.0",
54
+ "@stylistic/eslint-plugin": "^2.3.0",
55
55
  "@types/node": "^20.11.6",
56
- "@typescript-eslint/eslint-plugin": "^7.10.0",
57
- "@typescript-eslint/parser": "^7.10.0",
56
+ "@typescript-eslint/eslint-plugin": "^7.15.0",
57
+ "@typescript-eslint/parser": "^7.15.0",
58
58
  "eslint": "^8.56.0",
59
- "eslint-plugin-es-x": "^7.5.0",
59
+ "eslint-plugin-es-x": "^8.0.0",
60
60
  "eslint-plugin-eslint-comments": "^3.2.0",
61
- "eslint-plugin-jsdoc": "^48.0.2",
61
+ "eslint-plugin-jsdoc": "^48.5.2",
62
62
  "eslint-plugin-json-es": "^1.6.0",
63
- "eslint-plugin-n": "^17.6.0",
64
- "eslint-plugin-promise": "^6.1.1",
65
- "eslint-plugin-regexp": "^2.2.0",
66
- "eslint-plugin-unicorn": "^53.0.0",
67
- "typescript": "^5.4.5",
63
+ "eslint-plugin-n": "^17.9.0",
64
+ "eslint-plugin-promise": "^6.2.0",
65
+ "eslint-plugin-regexp": "^2.6.0",
66
+ "eslint-plugin-unicorn": "^54.0.0",
67
+ "typescript": "^5.5.3",
68
68
  "v8r": "^3.0.0"
69
69
  },
70
70
  "engines": {