wikilint 2.6.0 → 2.6.2

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
@@ -47,9 +47,6 @@ interface AstElement extends AstNode {
47
47
  export interface Parser {
48
48
  config: string | Config;
49
49
  i18n: string | Record<string, string> | undefined;
50
- rules: readonly LintError.Rule[];
51
- /** 获取解析设置 */
52
- getConfig(): Config;
53
50
  /**
54
51
  * 解析wikitext
55
52
  * @param include 是否嵌入
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { Config, LintError, Parser as ParserBase } from './base';
2
2
  import type { Title } from './lib/title';
3
3
  import type { Token } from './internal';
4
4
  declare interface Parser extends ParserBase {
5
+ rules: readonly LintError.Rule[];
5
6
  /**
6
7
  * 规范化页面标题
7
8
  * @param title 标题(含或不含命名空间前缀)
package/dist/index.js CHANGED
@@ -37,13 +37,21 @@ const Parser = {
37
37
  return msg && (this.i18n?.[msg] ?? msg).replace('$1', this.msg(arg));
38
38
  },
39
39
  /** @implements */
40
- normalizeTitle(title, defaultNs = 0, include = false, config = Parser.getConfig(), halfParsed = false, decode = false, selfLink = false) {
40
+ normalizeTitle(title, defaultNs = 0, include, config = Parser.getConfig(), halfParsed, decode = false, selfLink = false) {
41
41
  const { Title } = require('./lib/title');
42
42
  if (halfParsed) {
43
43
  return new Title(title, defaultNs, config, decode, selfLink);
44
44
  }
45
45
  const { Token } = require('./src/index');
46
46
  const token = debug_1.Shadow.run(() => new Token(title, config).parseOnce(0, include).parseOnce()), titleObj = new Title(String(token), defaultNs, config, decode, selfLink);
47
+ debug_1.Shadow.run(() => {
48
+ for (const key of ['main', 'fragment']) {
49
+ const str = titleObj[key];
50
+ if (str?.includes('\0')) {
51
+ titleObj[key] = token.buildFromStr(str, constants_1.BuildMethod.Text);
52
+ }
53
+ }
54
+ });
47
55
  return titleObj;
48
56
  },
49
57
  /** @implements */
@@ -60,9 +68,7 @@ const Parser = {
60
68
  const file = path.join(__dirname, '..', 'errors', new Date().toISOString()), stage = token.getAttribute('stage');
61
69
  fs.writeFileSync(file, stage === constants_1.MAX_STAGE ? wikitext : String(token));
62
70
  fs.writeFileSync(`${file}.err`, e.stack);
63
- fs.writeFileSync(`${file}.json`, JSON.stringify({
64
- stage, include: token.getAttribute('include'), config: this.config,
65
- }, null, '\t'));
71
+ fs.writeFileSync(`${file}.json`, JSON.stringify({ stage, include, config }, null, '\t'));
66
72
  }
67
73
  throw e;
68
74
  }
@@ -31,6 +31,11 @@ export declare abstract class AstElement extends AstNode {
31
31
  * @param selector 选择器
32
32
  */
33
33
  closest<T = Token>(selector: string): T | undefined;
34
+ /**
35
+ * 符合选择器的所有后代节点
36
+ * @param selector 选择器
37
+ */
38
+ querySelectorAll<T = Token>(selector: string): T[];
34
39
  /**
35
40
  * 在末尾批量插入子节点
36
41
  * @param elements 插入节点
@@ -70,6 +70,31 @@ class AstElement extends node_1.AstNode {
70
70
  }
71
71
  return undefined;
72
72
  }
73
+ /**
74
+ * 符合条件的所有后代节点
75
+ * @param condition 条件
76
+ */
77
+ #getElementsBy(condition) {
78
+ const descendants = [];
79
+ for (const child of this.childNodes) {
80
+ if (child.type === 'text') {
81
+ continue;
82
+ }
83
+ else if (condition(child)) {
84
+ descendants.push(child);
85
+ }
86
+ descendants.push(...child.#getElementsBy(condition));
87
+ }
88
+ return descendants;
89
+ }
90
+ /**
91
+ * 符合选择器的所有后代节点
92
+ * @param selector 选择器
93
+ */
94
+ querySelectorAll(selector) {
95
+ const condition = this.#getCondition(selector);
96
+ return this.#getElementsBy(condition);
97
+ }
73
98
  /**
74
99
  * 在末尾批量插入子节点
75
100
  * @param elements 插入节点
@@ -1,20 +1,27 @@
1
- import Parser from '../index';
1
+ import type { Config } from '../base';
2
2
  /** MediaWiki页面标题对象 */
3
3
  export declare class Title {
4
4
  #private;
5
- readonly valid: boolean;
6
5
  ns: number;
7
6
  fragment: string | undefined;
7
+ interwiki: string;
8
+ readonly valid: boolean;
8
9
  /** 不含命名空间的标题主体部分 */
9
10
  get main(): string;
10
11
  set main(title: string);
12
+ /** 命名空间前缀 */
13
+ get prefix(): string;
14
+ /** 完整标题 */
15
+ get title(): string;
11
16
  /** 扩展名 */
12
17
  get extension(): string | undefined;
13
18
  /**
19
+ * @see MediaWikiTitleCodec::splitTitleString
20
+ *
14
21
  * @param title 标题(含或不含命名空间前缀)
15
22
  * @param defaultNs 命名空间
16
23
  * @param decode 是否需要解码
17
24
  * @param selfLink 是否允许selfLink
18
25
  */
19
- constructor(title: string, defaultNs?: number, config?: Parser.Config, decode?: boolean, selfLink?: boolean);
26
+ constructor(title: string, defaultNs: number, config: Config, decode: boolean, selfLink: boolean);
20
27
  }
package/dist/lib/title.js CHANGED
@@ -2,15 +2,21 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Title = void 0;
4
4
  const string_1 = require("../util/string");
5
- const index_1 = require("../index");
5
+ /**
6
+ * PHP的`rawurldecode`函数的JavaScript实现
7
+ * @param str 要解码的字符串
8
+ */
9
+ const rawurldecode = (str) => decodeURIComponent(str.replace(/%(?![\da-f]{2})/giu, '%25'));
6
10
  /** MediaWiki页面标题对象 */
7
11
  class Title {
8
- valid;
12
+ #main;
13
+ #namespaces;
9
14
  ns;
10
15
  fragment;
16
+ interwiki = '';
17
+ valid;
11
18
  /** @private */
12
19
  encoded = false;
13
- #main;
14
20
  /** 不含命名空间的标题主体部分 */
15
21
  get main() {
16
22
  return this.#main;
@@ -19,57 +25,85 @@ class Title {
19
25
  title = title.replace(/_/gu, ' ').trim();
20
26
  this.#main = title && `${title[0].toUpperCase()}${title.slice(1)}`;
21
27
  }
28
+ /** 命名空间前缀 */
29
+ get prefix() {
30
+ const namespace = this.#namespaces[this.ns];
31
+ return `${namespace}${namespace && ':'}`;
32
+ }
33
+ /** 完整标题 */
34
+ get title() {
35
+ const prefix = `${this.interwiki}${this.interwiki && ':'}${this.prefix}`;
36
+ // eslint-disable-next-line prefer-const
37
+ let title = `${prefix}${this.main}`.replace(/ /gu, '_');
38
+ return title;
39
+ }
22
40
  /** 扩展名 */
23
41
  get extension() {
24
42
  const { main } = this, i = main.lastIndexOf('.');
25
43
  return i === -1 ? undefined : main.slice(i + 1).toLowerCase();
26
44
  }
27
45
  /**
46
+ * @see MediaWikiTitleCodec::splitTitleString
47
+ *
28
48
  * @param title 标题(含或不含命名空间前缀)
29
49
  * @param defaultNs 命名空间
30
50
  * @param decode 是否需要解码
31
51
  * @param selfLink 是否允许selfLink
32
52
  */
33
- constructor(title, defaultNs = 0, config = index_1.default.getConfig(), decode = false, selfLink = false) {
53
+ constructor(title, defaultNs, config, decode, selfLink) {
54
+ const subpage = title.trim().startsWith('../');
34
55
  title = (0, string_1.decodeHtml)(title);
35
56
  if (decode && title.includes('%')) {
36
57
  try {
37
58
  const encoded = /%(?!21|3[ce]|5[bd]|7[b-d])[\da-f]{2}/iu.test(title);
38
- title = decodeURIComponent(title);
59
+ title = rawurldecode(title);
39
60
  this.encoded = encoded;
40
61
  }
41
62
  catch { }
42
63
  }
43
64
  title = title.replace(/_/gu, ' ').trim();
44
- let ns = defaultNs;
45
- if (title.startsWith(':')) {
46
- ns = 0;
47
- title = title.slice(1).trim();
65
+ if (subpage) {
66
+ this.ns = 0;
48
67
  }
49
- const m = title.split(':');
50
- if (m.length > 1) {
51
- const id = config.nsid[m[0].trim().toLowerCase()];
52
- if (id) {
53
- ns = id;
54
- title = m.slice(1).join(':').trim();
68
+ else {
69
+ let ns = defaultNs;
70
+ if (title.startsWith(':')) {
71
+ ns = 0;
72
+ title = title.slice(1).trim();
73
+ }
74
+ const m = title.split(':');
75
+ if (m.length > 1) {
76
+ const id = config.nsid[m[0].trim().toLowerCase()];
77
+ if (id) {
78
+ ns = id;
79
+ title = m.slice(1).join(':').trim();
80
+ }
55
81
  }
82
+ this.ns = ns;
56
83
  }
57
- this.ns = ns;
58
84
  const i = title.indexOf('#');
59
85
  if (i !== -1) {
60
86
  let fragment = title.slice(i + 1).trimEnd();
61
87
  if (fragment.includes('%')) {
62
88
  try {
63
- fragment = decodeURIComponent(fragment);
89
+ fragment = rawurldecode(fragment);
64
90
  }
65
91
  catch { }
66
92
  }
67
93
  this.fragment = fragment;
68
94
  title = title.slice(0, i).trim();
69
95
  }
70
- this.valid = Boolean(title || this.interwiki || selfLink && this.fragment !== undefined)
71
- && !/^:|\0\d+[eh!+-]\x7F|[<>[\]{}|]|%[\da-f]{2}/iu.test(title);
96
+ this.valid = Boolean(title || this.interwiki || selfLink && this.ns === 0 && this.fragment !== undefined)
97
+ && !/^:|\0\d+[eh!+-]\x7F|[<>[\]{}|\n]|%[\da-f]{2}|(?:^|\/)\.{1,2}(?:$|\/)/iu.test(subpage ? /^(?:\.\.\/)+(.*)/u.exec(title)[1] : title);
72
98
  this.main = title;
99
+ Object.defineProperties(this, {
100
+ encoded: { enumerable: false, writable: false },
101
+ });
102
+ this.#namespaces = config.namespaces;
103
+ }
104
+ /** @private */
105
+ toString() {
106
+ return `${this.title}${this.fragment === undefined ? '' : `#${this.fragment}`}`;
73
107
  }
74
108
  }
75
109
  exports.Title = Title;
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseBraces = void 0;
4
4
  const string_1 = require("../util/string");
5
- const index_1 = require("../index");
6
5
  const heading_1 = require("../src/heading");
7
6
  const transclude_1 = require("../src/transclude");
8
7
  const arg_1 = require("../src/arg");
@@ -13,7 +12,7 @@ const arg_1 = require("../src/arg");
13
12
  * @param accum
14
13
  * @throws TranscludeToken.constructor()
15
14
  */
16
- const parseBraces = (wikitext, config = index_1.default.getConfig(), accum = []) => {
15
+ const parseBraces = (wikitext, config, accum) => {
17
16
  const source = `${config.excludes?.includes('heading') ? '' : '^(\0\\d+c\x7F)*={1,6}|'}\\[\\[|\\{{2,}|-\\{(?!\\{)`, { parserFunction: [, , , subst] } = config, stack = [], closes = { '=': '\n', '{': '\\}{2,}|\\|', '-': '\\}-', '[': '\\]\\]' }, marks = new Map([['!', '!'], ['!!', '+'], ['(!', '{'], ['!)', '}'], ['!-', '-'], ['=', '~']]);
18
17
  let regex = new RegExp(source, 'gmu'), mt = regex.exec(wikitext), moreBraces = wikitext.includes('}}'), lastIndex;
19
18
  while (mt
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseCommentAndExt = void 0;
4
- const index_1 = require("../index");
5
4
  const onlyinclude_1 = require("../src/onlyinclude");
6
5
  const noinclude_1 = require("../src/nowiki/noinclude");
7
6
  const include_1 = require("../src/tagPair/include");
@@ -14,7 +13,7 @@ const comment_1 = require("../src/nowiki/comment");
14
13
  * @param accum
15
14
  * @param includeOnly 是否嵌入
16
15
  */
17
- const parseCommentAndExt = (wikitext, config = index_1.default.getConfig(), accum = [], includeOnly = false) => {
16
+ const parseCommentAndExt = (wikitext, config, accum, includeOnly) => {
18
17
  const onlyincludeLeft = '<onlyinclude>', onlyincludeRight = '</onlyinclude>', { length } = onlyincludeLeft;
19
18
  /** 更新`<onlyinclude>`和`</onlyinclude>`的位置 */
20
19
  const update = () => {
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseConverter = void 0;
4
- const index_1 = require("../index");
5
4
  const converter_1 = require("../src/converter");
6
5
  /**
7
6
  * 解析语言变体转换
@@ -9,7 +8,7 @@ const converter_1 = require("../src/converter");
9
8
  * @param config
10
9
  * @param accum
11
10
  */
12
- const parseConverter = (text, config = index_1.default.getConfig(), accum = []) => {
11
+ const parseConverter = (text, config, accum) => {
13
12
  const regex1 = /-\{/gu, regex2 = /-\{|\}-/gu, stack = [];
14
13
  let regex = regex1, mt = regex.exec(text);
15
14
  while (mt) {
@@ -2,26 +2,39 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseExternalLinks = void 0;
4
4
  const string_1 = require("../util/string");
5
- const index_1 = require("../index");
6
5
  const extLink_1 = require("../src/extLink");
6
+ const magicLink_1 = require("../src/magicLink");
7
7
  /**
8
8
  * 解析外部链接
9
9
  * @param wikitext
10
10
  * @param config
11
11
  * @param accum
12
+ * @param inFile 是否在图链中
12
13
  */
13
- const parseExternalLinks = (wikitext, config = index_1.default.getConfig(), accum = []) => {
14
+ const parseExternalLinks = (wikitext, config, accum, inFile) => {
14
15
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
15
16
  /\[((?:\[[\da-f:.]+\]|[^[\]\t\n\p{Zs}])[^[\]\t\n\p{Zs}]*(?=[[\]\t\p{Zs}]|\0\d))(\p{Zs}*(?=\P{Zs}))([^\]\n]*)\]/giu;
16
- const regex = new RegExp(`\\[((?:(?:${config.protocol}|//)${string_1.extUrlCharFirst}|\0\\d+m\x7F)${string_1.extUrlChar}(?=[[\\]<>"\\t\\p{Zs}]|\0\\d))`
17
- + '(\\p{Zs}*(?=\\P{Zs}))([^\\]\x01-\x08\x0A-\x1F\uFFFD]*)\\]', 'giu');
17
+ const regex = new RegExp('\\[' // 左括号
18
+ + `(${'\0\\d+f\x7F' // 预先解析的MagicLinkToken
19
+ + '|'
20
+ + `(?:(?:${config.protocol}|//)${string_1.extUrlCharFirst}|\0\\d+m\x7F)${string_1.extUrlChar}(?=[[\\]<>"\\t\\p{Zs}]|\0\\d)`})` // 链接网址
21
+ + '(\\p{Zs}*(?=\\P{Zs}))' // 空格
22
+ + '([^\\]\x01-\x08\x0A-\x1F\uFFFD]*)' // 链接文字
23
+ + '\\]', // 右括号
24
+ 'giu');
18
25
  return wikitext.replace(regex, (_, url, space, text) => {
19
- const { length } = accum, mt = /&[lg]t;/u.exec(url);
26
+ const { length } = accum;
27
+ const mt = /&[lg]t;/u.exec(url);
20
28
  if (mt) {
21
29
  url = url.slice(0, mt.index);
22
30
  space = '';
23
31
  text = `${url.slice(mt.index)}${space}${text}`;
24
32
  }
33
+ if (inFile) {
34
+ // @ts-expect-error abstract class
35
+ new magicLink_1.MagicLinkToken(url, true, config, accum);
36
+ return `[\0${length}f\x7F${space}${text}]`;
37
+ }
25
38
  // @ts-expect-error abstract class
26
39
  new extLink_1.ExtLinkToken(url, space, text, config, accum);
27
40
  return `\0${length}w\x7F`;
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseHrAndDoubleUnderscore = void 0;
4
- const index_1 = require("../index");
5
4
  const hr_1 = require("../src/nowiki/hr");
6
5
  const doubleUnderscore_1 = require("../src/nowiki/doubleUnderscore");
7
6
  const heading_1 = require("../src/heading");
@@ -11,7 +10,7 @@ const heading_1 = require("../src/heading");
11
10
  * @param config
12
11
  * @param accum
13
12
  */
14
- const parseHrAndDoubleUnderscore = ({ firstChild: { data }, type, name }, config = index_1.default.getConfig(), accum = []) => {
13
+ const parseHrAndDoubleUnderscore = ({ firstChild: { data }, type, name }, config, accum) => {
15
14
  const { doubleUnderscore } = config, insensitive = new Set(doubleUnderscore[0]), sensitive = new Set(doubleUnderscore[1]);
16
15
  if (type !== 'root' && (type !== 'ext-inner' || name !== 'poem')) {
17
16
  data = `\0${data}`;
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseHtml = void 0;
4
- const index_1 = require("../index");
5
4
  const attributes_1 = require("../src/attributes");
6
5
  const html_1 = require("../src/html");
7
6
  /**
@@ -10,7 +9,7 @@ const html_1 = require("../src/html");
10
9
  * @param config
11
10
  * @param accum
12
11
  */
13
- const parseHtml = (wikitext, config = index_1.default.getConfig(), accum = []) => {
12
+ const parseHtml = (wikitext, config, accum) => {
14
13
  const regex = /^(\/?)([a-z][^\s/>]*)((?:\s|\/(?!>))[^>]*?)?(\/?>)([^<]*)$/iu, elements = new Set(config.html.flat()), bits = wikitext.split('<');
15
14
  let text = bits.shift();
16
15
  for (const x of bits) {
@@ -19,18 +18,18 @@ const parseHtml = (wikitext, config = index_1.default.getConfig(), accum = []) =
19
18
  text += `<${x}`;
20
19
  continue;
21
20
  }
22
- const [, slash, , params = '', brace, rest] = mt,
21
+ const [, slash, , params = '', brace, rest] = mt, { length } = accum,
23
22
  // @ts-expect-error abstract class
24
- attr = new attributes_1.AttributesToken(params, 'html-attrs', name, config, accum), itemprop = attr.getAttr('itemprop');
25
- if (name === 'meta' && (itemprop === undefined || attr.getAttr('content') === undefined)
26
- || name === 'link' && (itemprop === undefined || attr.getAttr('href') === undefined)) {
23
+ attrs = new attributes_1.AttributesToken(params, 'html-attrs', name, config, accum), itemprop = attrs.getAttr('itemprop');
24
+ if (name === 'meta' && (itemprop === undefined || attrs.getAttr('content') === undefined)
25
+ || name === 'link' && (itemprop === undefined || attrs.getAttr('href') === undefined)) {
27
26
  text += `<${x}`;
28
- accum.pop();
27
+ accum.length = length;
29
28
  continue;
30
29
  }
31
30
  text += `\0${accum.length}x\x7F${rest}`;
32
31
  // @ts-expect-error abstract class
33
- new html_1.HtmlToken(t, attr, slash === '/', brace === '/>', config, accum);
32
+ new html_1.HtmlToken(t, attrs, slash === '/', brace === '/>', config, accum);
34
33
  }
35
34
  return text;
36
35
  };
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseLinks = void 0;
4
4
  const index_1 = require("../index");
5
+ const quotes_1 = require("./quotes");
6
+ const externalLinks_1 = require("./externalLinks");
5
7
  const index_2 = require("../src/link/index");
6
8
  const file_1 = require("../src/link/file");
7
9
  const category_1 = require("../src/link/category");
@@ -11,8 +13,7 @@ const category_1 = require("../src/link/category");
11
13
  * @param config
12
14
  * @param accum
13
15
  */
14
- const parseLinks = (wikitext, config = index_1.default.getConfig(), accum = []) => {
15
- const { parseQuotes } = require('./quotes');
16
+ const parseLinks = (wikitext, config, accum) => {
16
17
  const regex = /^((?:(?!\0\d+!\x7F)[^\n[\]{}|])+)(?:(\||\0\d+!\x7F)(.*?[^\]]))?\]\](.*)$/su, regexImg = /^((?:(?!\0\d+!\x7F)[^\n[\]{}|])+)(\||\0\d+!\x7F)(.*)$/su, regexExt = new RegExp(`^\\s*(?:${config.protocol}|//)`, 'iu'), bits = wikitext.split('[[');
17
18
  let s = bits.shift();
18
19
  for (let i = 0; i < bits.length; i++) {
@@ -74,17 +75,18 @@ const parseLinks = (wikitext, config = index_1.default.getConfig(), accum = [])
74
75
  continue;
75
76
  }
76
77
  }
77
- text &&= parseQuotes(text, config, accum);
78
- s += `\0${accum.length}l\x7F${after}`;
78
+ text &&= (0, quotes_1.parseQuotes)(text, config, accum);
79
79
  let SomeLinkToken = index_2.LinkToken;
80
80
  if (!force) {
81
81
  if (!interwiki && ns === 6) {
82
+ text &&= (0, externalLinks_1.parseExternalLinks)(text, config, accum, true);
82
83
  SomeLinkToken = file_1.FileToken;
83
84
  }
84
85
  else if (!interwiki && ns === 14) {
85
86
  SomeLinkToken = category_1.CategoryToken;
86
87
  }
87
88
  }
89
+ s += `\0${accum.length}l\x7F${after}`;
88
90
  // @ts-expect-error abstract class
89
91
  new SomeLinkToken(link, text, config, accum, delimiter);
90
92
  }
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseList = void 0;
4
- const index_1 = require("../index");
5
4
  const list_1 = require("../src/nowiki/list");
6
5
  const dd_1 = require("../src/nowiki/dd");
7
6
  /**
@@ -10,7 +9,7 @@ const dd_1 = require("../src/nowiki/dd");
10
9
  * @param config
11
10
  * @param accum
12
11
  */
13
- const parseList = (wikitext, config = index_1.default.getConfig(), accum = []) => {
12
+ const parseList = (wikitext, config, accum) => {
14
13
  const mt = /^((?:\0\d+c\x7F)*)([;:*#]+)/u.exec(wikitext);
15
14
  if (!mt) {
16
15
  return wikitext;
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseMagicLinks = void 0;
4
4
  const string_1 = require("../util/string");
5
- const index_1 = require("../index");
6
5
  const magicLink_1 = require("../src/magicLink");
7
6
  /**
8
7
  * 解析自由外链
@@ -10,18 +9,18 @@ const magicLink_1 = require("../src/magicLink");
10
9
  * @param config
11
10
  * @param accum
12
11
  */
13
- const parseMagicLinks = (wikitext, config = index_1.default.getConfig(), accum = []) => {
12
+ const parseMagicLinks = (wikitext, config, accum) => {
14
13
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
15
14
  /(^|[^\p{L}\d_])((?:\[[\da-f:.]+\]|[^[\]<>"\t\n\p{Zs}])(?:[^[\]<>"\0\t\n\p{Zs}]|\0\d+c\x7F)*)/giu;
16
15
  const regex = new RegExp(`(^|[^\\p{L}\\d_])(?:${config.protocol})(${string_1.extUrlCharFirst}${string_1.extUrlChar})`, 'giu');
17
16
  return wikitext.replace(regex, (m, lead, p1) => {
18
- let trail = '', url = lead ? m.slice(1) : m;
17
+ let trail = '', url = lead ? m.slice(lead.length) : m;
19
18
  const m2 = /&(?:lt|gt|nbsp|#x0*(?:3[ce]|a0)|#0*(?:6[02]|160));/iu.exec(url);
20
19
  if (m2) {
21
20
  trail = url.slice(m2.index);
22
21
  url = url.slice(0, m2.index);
23
22
  }
24
- const sep = new RegExp(`[,;.:!?${url.includes('(') ? '' : ')'}]+$`, 'u'), sepChars = sep.exec(url);
23
+ const sep = new RegExp(`[,;\\\\.:!?${url.includes('(') ? '' : ')'}]+$`, 'u'), sepChars = sep.exec(url);
25
24
  if (sepChars) {
26
25
  let correction = 0;
27
26
  if (sepChars[0].startsWith(';') && /&(?:[a-z]+|#x[\da-f]+|#\d+)$/iu.test(url.slice(0, sepChars.index))) {
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseQuotes = void 0;
4
- const index_1 = require("../index");
5
4
  const quote_1 = require("../src/nowiki/quote");
6
5
  /**
7
6
  * 解析单引号
@@ -9,7 +8,7 @@ const quote_1 = require("../src/nowiki/quote");
9
8
  * @param config
10
9
  * @param accum
11
10
  */
12
- const parseQuotes = (wikitext, config = index_1.default.getConfig(), accum = []) => {
11
+ const parseQuotes = (wikitext, config, accum) => {
13
12
  const arr = wikitext.split(/('{2,})/u), { length } = arr;
14
13
  if (length === 1) {
15
14
  return wikitext;
@@ -1,9 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseTable = void 0;
4
- const index_1 = require("../index");
5
- const index_2 = require("../src/index");
6
- const index_3 = require("../src/table/index");
4
+ const index_1 = require("../src/index");
5
+ const index_2 = require("../src/table/index");
7
6
  const tr_1 = require("../src/table/tr");
8
7
  const td_1 = require("../src/table/td");
9
8
  const dd_1 = require("../src/nowiki/dd");
@@ -11,14 +10,14 @@ const dd_1 = require("../src/nowiki/dd");
11
10
  * 判断是否为表格行或表格
12
11
  * @param token 表格节点
13
12
  */
14
- const isTr = (token) => token.lastChild.constructor !== index_2.Token;
13
+ const isTr = (token) => token.lastChild.constructor !== index_1.Token;
15
14
  /**
16
15
  * 解析表格,注意`tr`和`td`包含开头的换行
17
16
  * @param {Token & {firstChild: AstText}} root 根节点
18
17
  * @param config
19
18
  * @param accum
20
19
  */
21
- const parseTable = ({ firstChild: { data }, type, name }, config = index_1.default.getConfig(), accum = []) => {
20
+ const parseTable = ({ firstChild: { data }, type, name }, config, accum) => {
22
21
  const stack = [], lines = data.split('\n');
23
22
  let out = type === 'root' || type === 'parameter-value' || type === 'ext-inner' && name === 'poem'
24
23
  ? ''
@@ -35,7 +34,7 @@ const parseTable = ({ firstChild: { data }, type, name }, config = index_1.defau
35
34
  }
36
35
  const { lastChild } = topToken;
37
36
  if (isTr(topToken)) {
38
- const token = new index_2.Token(str, config, accum);
37
+ const token = new index_1.Token(str, config, accum);
39
38
  token.type = 'table-inter';
40
39
  token.setAttribute('stage', 3);
41
40
  topToken.insertAt(token);
@@ -61,7 +60,7 @@ const parseTable = ({ firstChild: { data }, type, name }, config = index_1.defau
61
60
  }
62
61
  push(`\n${spaces}${indent && `\0${accum.length - 1}d\x7F`}${moreSpaces}\0${accum.length}b\x7F`, top);
63
62
  // @ts-expect-error abstract class
64
- stack.push(...top ? [top] : [], new index_3.TableToken(tableSyntax, attr, config, accum));
63
+ stack.push(...top ? [top] : [], new index_2.TableToken(tableSyntax, attr, config, accum));
65
64
  continue;
66
65
  }
67
66
  else if (!top) {
@@ -20,8 +20,11 @@ class ExtLinkToken extends index_2.Token {
20
20
  */
21
21
  constructor(url, space = '', text = '', config = index_1.default.getConfig(), accum = []) {
22
22
  super(undefined, config, accum, {});
23
- // @ts-expect-error abstract class
24
- this.insertAt(new magicLink_1.MagicLinkToken(url, true, config, accum));
23
+ const link = url && /\0\d+f\x7F/u.test(url)
24
+ ? accum[Number(url.slice(1, -2))]
25
+ // @ts-expect-error abstract class
26
+ : new magicLink_1.MagicLinkToken(url, true, config, accum);
27
+ this.insertAt(link);
25
28
  this.#space = space;
26
29
  if (text) {
27
30
  const inner = new index_2.Token(text, config, accum, {});
@@ -6,12 +6,12 @@ const lint_1 = require("../util/lint");
6
6
  const index_1 = require("../index");
7
7
  const index_2 = require("./index");
8
8
  exports.galleryParams = new Set(['alt', 'link', 'lang', 'page', 'caption']);
9
- function validate(key, val, config, halfParsed = false, ext) {
9
+ function validate(key, val, config, halfParsed, ext) {
10
10
  val = val.trim();
11
11
  let value = val.replace(/\0\d+t\x7F/gu, '').trim();
12
12
  switch (key) {
13
13
  case 'width':
14
- return !value || /^(?:\d+x?|\d*x\d+)$/u.test(value);
14
+ return !value || /^(?:\d+x?|\d*x\d+)(?:\s*px)?$/u.test(value);
15
15
  case 'link': {
16
16
  if (!value) {
17
17
  return val;
@@ -35,9 +35,9 @@ function validate(key, val, config, halfParsed = false, ext) {
35
35
  case 'manualthumb':
36
36
  return true;
37
37
  case 'page':
38
- return (ext === 'djvu' || ext === 'djv') && Number(value) > 0;
38
+ return (ext === 'djvu' || ext === 'djv' || ext === 'pdf') && Number(value) > 0;
39
39
  default:
40
- return !Number.isNaN(Number(value));
40
+ return Boolean(value) && !isNaN(value);
41
41
  }
42
42
  }
43
43
  /** 图片参数 */
@@ -49,7 +49,7 @@ class ImagemapToken extends index_2.Token {
49
49
  continue;
50
50
  }
51
51
  else if (line.includes('[')) {
52
- const i = line.indexOf('['), substr = line.slice(i), mtIn = /^\[{2}([^|]+)(?:\|([^\]]+))?\]{2}[\w\s]*$/u
52
+ const i = line.indexOf('['), substr = line.slice(i), mtIn = /^\[\[([^|]+)(?:\|([^\]]+))?\]\][\w\s]*$/u
53
53
  .exec(substr);
54
54
  if (mtIn) {
55
55
  if (this.normalizeTitle(mtIn[1], 0, true, false, true).valid) {
package/dist/src/index.js CHANGED
@@ -26,6 +26,7 @@
26
26
  // c: CommentToken、NoIncludeToken和IncludeToken
27
27
  // d: ListToken
28
28
  // e: ExtToken
29
+ // f: MagicLinkToken inside ImageParameterToken
29
30
  // h: HeadingToken
30
31
  // l: LinkToken
31
32
  // m: `{{fullurl:}}`、`{{canonicalurl:}}`或`{{filepath:}}`
@@ -128,7 +129,7 @@ class Token extends element_1.AstElement {
128
129
  if (i % 2 === 0) {
129
130
  return new text_1.AstText(s);
130
131
  }
131
- else if (Number.isNaN(Number(s.slice(-1)))) {
132
+ else if (isNaN(s.slice(-1))) {
132
133
  return this.#accum[Number(s.slice(0, -1))];
133
134
  }
134
135
  throw new Error(`解析错误!未正确标记的 Token:${s}`);
@@ -164,7 +165,7 @@ class Token extends element_1.AstElement {
164
165
  }
165
166
  }
166
167
  /** @private */
167
- parse(n = constants_1.MAX_STAGE, include = false) {
168
+ parse(n = constants_1.MAX_STAGE, include) {
168
169
  n = Math.min(n, constants_1.MAX_STAGE);
169
170
  while (this.#stage < n) {
170
171
  this.parseOnce(this.#stage, include);
@@ -315,38 +316,38 @@ class Token extends element_1.AstElement {
315
316
  * @param decode 是否需要解码
316
317
  * @param selfLink 是否允许selfLink
317
318
  */
318
- normalizeTitle(title, defaultNs = 0, halfParsed = false, decode = false, selfLink = false) {
319
+ normalizeTitle(title, defaultNs = 0, halfParsed, decode, selfLink) {
319
320
  return index_1.default.normalizeTitle(title, defaultNs, this.#include, this.#config, halfParsed, decode, selfLink);
320
321
  }
321
322
  /** @override */
322
323
  lint(start = this.getAbsoluteIndex(), re) {
323
324
  const errors = super.lint(start, re);
324
- const record = {};
325
- for (const cat of this.childNodes.filter((node) => node.type === 'category')) {
326
- const thisCat = record[cat.name];
327
- if (thisCat) {
328
- thisCat.add(cat);
329
- }
330
- else {
331
- record[cat.name] = new Set([cat]);
325
+ if (this.type === 'root') {
326
+ const record = {};
327
+ for (const cat of this.querySelectorAll('category')) {
328
+ const thisCat = record[cat.name];
329
+ if (thisCat) {
330
+ thisCat.add(cat);
331
+ }
332
+ else {
333
+ record[cat.name] = new Set([cat]);
334
+ }
332
335
  }
333
- }
334
- for (const value of Object.values(record)) {
335
- if (value.size > 1) {
336
- errors.push(...[...value].map(cat => {
337
- const e = (0, lint_1.generateForSelf)(cat, { start: cat.getAbsoluteIndex() }, 'no-duplicate', 'duplicated category');
338
- e.suggestions = [
339
- {
340
- desc: 'remove',
341
- range: [e.startIndex, e.endIndex],
342
- text: '',
343
- },
344
- ];
345
- return e;
346
- }));
336
+ for (const value of Object.values(record)) {
337
+ if (value.size > 1) {
338
+ errors.push(...[...value].map(cat => {
339
+ const e = (0, lint_1.generateForSelf)(cat, { start: cat.getAbsoluteIndex() }, 'no-duplicate', 'duplicated category');
340
+ e.suggestions = [
341
+ {
342
+ desc: 'remove',
343
+ range: [e.startIndex, e.endIndex],
344
+ text: '',
345
+ },
346
+ ];
347
+ return e;
348
+ }));
349
+ }
347
350
  }
348
- }
349
- if (this.type === 'root') {
350
351
  const regex = /<!--\s*lint-(disable(?:(?:-next)?-line)?|enable)(\s[\sa-z,-]*)?-->/gu, wikitext = String(this), ignores = [];
351
352
  let mt = regex.exec(wikitext), last = 0, curLine = 0;
352
353
  while (mt) {
@@ -36,12 +36,12 @@ class LinkBaseToken extends index_2.Token {
36
36
  if (this.#delimiter.includes('\0')) {
37
37
  this.#delimiter = this.buildFromStr(this.#delimiter, constants_1.BuildMethod.String);
38
38
  }
39
- this.setAttribute('name', this.#title.main);
39
+ this.setAttribute('name', this.#title.title);
40
40
  }
41
41
  /** @private */
42
42
  setAttribute(key, value) {
43
43
  if (key === 'bracket') {
44
- this.#bracket = Boolean(value);
44
+ this.#bracket = value;
45
45
  }
46
46
  else if (key === 'title') {
47
47
  this.#title = value;
@@ -115,7 +115,7 @@ class LinkBaseToken extends index_2.Token {
115
115
  return errors;
116
116
  }
117
117
  /** @private */
118
- getTitle(halfParsed = false) {
118
+ getTitle(halfParsed) {
119
119
  return this.normalizeTitle(this.firstChild.text(), 0, halfParsed, true, true);
120
120
  }
121
121
  }
@@ -16,12 +16,13 @@ class GalleryImageToken extends file_1.FileToken {
16
16
  constructor(type, link, text, config = index_1.default.getConfig(), accum = []) {
17
17
  let token;
18
18
  if (text !== undefined) {
19
+ const { length } = accum;
19
20
  token = new index_2.Token(text, config, accum);
20
21
  token.type = 'plain';
21
22
  for (let n = 1; n < constants_1.MAX_STAGE; n++) {
22
23
  token.parseOnce();
23
24
  }
24
- accum.splice(accum.indexOf(token), 1);
25
+ accum.splice(length, 1);
25
26
  }
26
27
  super(link, token?.toString(), config, accum);
27
28
  this.setAttribute('bracket', false);
@@ -13,7 +13,7 @@ class MagicLinkToken extends index_2.Token {
13
13
  * @param url 网址
14
14
  * @param doubleSlash 是否接受"//"作为协议
15
15
  */
16
- constructor(url, doubleSlash = false, config = index_1.default.getConfig(), accum = []) {
16
+ constructor(url, doubleSlash, config = index_1.default.getConfig(), accum = []) {
17
17
  super(url, config, accum, {});
18
18
  this.type = doubleSlash ? 'ext-link-url' : 'free-ext-link';
19
19
  }
@@ -8,10 +8,10 @@ const index_2 = require("./index");
8
8
  class InputboxToken extends index_2.ParamTagToken {
9
9
  /** @class */
10
10
  constructor(wikitext, config = index_1.default.getConfig(), accum = []) {
11
- const placeholder = Symbol('InputboxToken');
11
+ const placeholder = Symbol('InputboxToken'), { length } = accum;
12
12
  accum.push(placeholder);
13
13
  wikitext &&= (0, braces_1.parseBraces)(wikitext, config, accum);
14
- accum.splice(accum.indexOf(placeholder), 1);
14
+ accum.splice(length, 1);
15
15
  super(wikitext, config, accum, {});
16
16
  }
17
17
  }
@@ -60,13 +60,17 @@ class TableToken extends trBase_1.TrBaseToken {
60
60
  }
61
61
  }
62
62
  if (j < length) {
63
- errors.push((0, lint_1.generateForChild)(this.getNthRow(j), { start }, 'table-layout', 'inconsistent table layout', 'warning'));
63
+ const e = (0, lint_1.generateForChild)(this.getNthRow(j), { start }, 'table-layout', 'inconsistent table layout', 'warning');
64
+ e.startIndex++;
65
+ e.startLine++;
66
+ e.startCol = 0;
67
+ errors.push(e);
64
68
  }
65
69
  }
66
70
  return errors;
67
71
  }
68
72
  /** @private */
69
- close(syntax = '\n|}', halfParsed = false) {
73
+ close(syntax = '\n|}', halfParsed) {
70
74
  const config = this.getAttribute('config'), accum = this.getAttribute('accum'), inner = halfParsed ? [syntax] : index_1.default.parse(syntax, this.getAttribute('include'), 2, config).childNodes;
71
75
  const token = debug_1.Shadow.run(() => super.insertAt(new syntax_1.SyntaxToken(undefined, closingPattern, 'table-syntax', config, accum, {})));
72
76
  if (!halfParsed) {
@@ -125,12 +129,16 @@ class TableToken extends trBase_1.TrBaseToken {
125
129
  * 获取指定坐标的单元格
126
130
  * @param coords 表格坐标
127
131
  */
128
- getNthCell(coords) {
129
- const rawCoords = coords;
132
+ getNthCell(
133
+ // eslint-disable-next-line @stylistic/comma-dangle
134
+ coords) {
130
135
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
136
+ const rawCoords = coords.row === undefined
137
+ ? undefined
138
+ : coords;
131
139
  return rawCoords && this.getNthRow(rawCoords.row, false, false)?.getNthCol(rawCoords.column);
132
140
  }
133
- getNthRow(n, force = false, insert = false) {
141
+ getNthRow(n, force, insert) {
134
142
  const isRow = super.getRowCount();
135
143
  if (n === 0
136
144
  // eslint-disable-next-line @stylistic/no-extra-parens
@@ -127,7 +127,7 @@ class TdToken extends base_1.TableBaseToken {
127
127
  */
128
128
  getAttr(key) {
129
129
  const value = super.getAttr(key);
130
- return (key === 'rowspan' || key === 'colspan' ? Number(value) || 1 : value);
130
+ return (key === 'rowspan' || key === 'colspan' ? parseInt(value) || 1 : value);
131
131
  }
132
132
  }
133
133
  exports.TdToken = TdToken;
@@ -39,7 +39,7 @@ class TrBaseToken extends base_1.TableBaseToken {
39
39
  getRowCount() {
40
40
  return Number(this.childNodes.some(child => child instanceof td_1.TdToken && child.isIndependent() && !child.firstChild.text().endsWith('+')));
41
41
  }
42
- getNthCol(n, insert = false) {
42
+ getNthCol(n, insert) {
43
43
  let last = 0;
44
44
  for (const child of this.childNodes.slice(2)) {
45
45
  if (child instanceof td_1.TdToken) {
@@ -69,8 +69,8 @@ class TranscludeToken extends index_2.Token {
69
69
  }
70
70
  }
71
71
  if (this.type === 'template') {
72
- const name = (0, string_1.removeComment)((0, string_1.decodeHtml)(title)).split('#')[0].trim();
73
- if (!name || /^:[\s_]*:|\0\d+[eh!+-]\x7F|[<>[\]{}\n]|%[\da-f]{2}/iu.test(name)) {
72
+ const name = (0, string_1.removeComment)(title).trim();
73
+ if (!this.normalizeTitle(name, 10, true).valid) {
74
74
  accum.pop();
75
75
  throw new SyntaxError('非法的模板名称');
76
76
  }
@@ -234,7 +234,7 @@ class TranscludeToken extends index_2.Token {
234
234
  * @param exact 是否匹配匿名性
235
235
  * @param copy 是否返回一个备份
236
236
  */
237
- getArgs(key, exact = false, copy = true) {
237
+ getArgs(key, exact, copy = true) {
238
238
  const keyStr = String(key).replace(/^[ \t\n\0\v]+|([^ \t\n\0\v])[ \t\n\0\v]+$/gu, '$1');
239
239
  let args;
240
240
  if (this.#args.has(keyStr)) {
@@ -5,8 +5,17 @@ exports.Shadow = {
5
5
  running: false,
6
6
  /** @private */
7
7
  run(callback) {
8
- const result = callback();
9
- return result;
8
+ const { running } = this;
9
+ this.running = true;
10
+ try {
11
+ const result = callback();
12
+ this.running = running;
13
+ return result;
14
+ }
15
+ catch (e) {
16
+ this.running = running;
17
+ throw e;
18
+ }
10
19
  },
11
20
  };
12
21
  /**
package/dist/util/diff.js CHANGED
@@ -53,7 +53,7 @@ exports.cmd = cmd;
53
53
  * @param newStr 新文本
54
54
  * @param uid 唯一标识
55
55
  */
56
- const diff = async (oldStr, newStr, uid = -1) => {
56
+ const diff = async (oldStr, newStr, uid) => {
57
57
  if (oldStr === newStr) {
58
58
  return;
59
59
  }
@@ -67,8 +67,8 @@ const diff = async (oldStr, newStr, uid = -1) => {
67
67
  oldFile,
68
68
  newFile,
69
69
  ]);
70
- await Promise.all([fs.unlink(oldFile), fs.unlink(newFile)]);
71
70
  console.log(stdout?.split('\n').slice(4).join('\n'));
71
+ await Promise.all([fs.unlink(oldFile), fs.unlink(newFile)]);
72
72
  };
73
73
  exports.diff = diff;
74
74
  /** @implements */
@@ -23,7 +23,10 @@ exports.escapeRegExp = factory(/[\\{}()|.?*+^$[\]]/gu, '\\$&');
23
23
  */
24
24
  const text = (childNodes, separator = '') => childNodes.map(child => typeof child === 'string' ? child : child.text()).join(separator);
25
25
  exports.text = text;
26
+ const names = { lt: '<', gt: '>', lbrack: '[', rbrack: ']', lbrace: '{', rbrace: '}' };
26
27
  /** decode HTML entities */
27
- exports.decodeHtml = factory(/&#(\d+|x[\da-f]+);/giu, (_, code) => String.fromCodePoint(Number(`${code.toLowerCase().startsWith('x') ? '0' : ''}${code}`)));
28
+ exports.decodeHtml = factory(/&(?:#(\d+|x[\da-fA-F]+)|([lLgG][tT]|[lr]brac[ke]));/gu, (_, code, name) => code
29
+ ? String.fromCodePoint(Number(`${/^x/iu.test(code) ? '0' : ''}${code}`))
30
+ : names[name.toLowerCase()]);
28
31
  /** escape newlines */
29
32
  exports.noWrap = factory(/\n/gu, '\\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wikilint",
3
- "version": "2.6.0",
3
+ "version": "2.6.2",
4
4
  "description": "A Node.js linter for MediaWiki markup",
5
5
  "keywords": [
6
6
  "mediawiki",
@@ -40,6 +40,7 @@
40
40
  "lint:ts": "tsc --noEmit && eslint --cache .",
41
41
  "lint:json": "ajv -s config/.schema.json -d 'config/*.json' --strict=true --strict-required=false",
42
42
  "lint": "npm run lint:ts && npm run lint:json",
43
+ "test": "node dist/test/test.js",
43
44
  "test:end": "pkill -x http-server",
44
45
  "test:real": "node dist/test/real.js"
45
46
  },