wikilint 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -97,6 +97,6 @@ repos:
97
97
  # You can add any CLI options as needed
98
98
  entry: wikilint
99
99
  language: node
100
- additional_dependencies: ['wikilint@3.0.0']
100
+ additional_dependencies: ['wikilint@3.1.0']
101
101
  files: \.(mediawiki|wiki|wikitext)$
102
102
  ```
@@ -247,6 +247,11 @@
247
247
  "description": "base URL of the server",
248
248
  "type": "string",
249
249
  "pattern": "^(?:https?:)?//"
250
+ },
251
+ "capitalLinks": {
252
+ "description": "whether to capitalize the first letter of page titles",
253
+ "type": "boolean",
254
+ "default": true
250
255
  }
251
256
  },
252
257
  "required": [
package/dist/base.d.mts CHANGED
@@ -11,6 +11,7 @@ export interface Config {
11
11
  readonly redirection: string[];
12
12
  variable: string[];
13
13
  functionHook: string[];
14
+ capitalLinks?: boolean;
14
15
  variants: string[];
15
16
  articlePath?: string;
16
17
  }
package/dist/base.d.ts CHANGED
@@ -11,6 +11,7 @@ export interface Config {
11
11
  readonly redirection: string[];
12
12
  variable: string[];
13
13
  functionHook: string[];
14
+ capitalLinks?: boolean;
14
15
  variants: string[];
15
16
  articlePath?: string;
16
17
  }
@@ -41,7 +41,7 @@ const filterGadget = (id) => {
41
41
  const n = Number(id);
42
42
  return n < 2300 || n > 2303; // Gadget, Gadget talk, Gadget definition, Gadget definition talk
43
43
  };
44
- const pkg = 'wikilint', version = '3.0.0';
44
+ const pkg = 'wikilint', version = '3.1.0';
45
45
  /**
46
46
  * Get the parser configuration for a Wikimedia Foundation project.
47
47
  * @param site site nickname
package/dist/index.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { LintConfiguration } from './lib/lintConfig';
2
- import { Title } from './lib/title';
3
2
  import type { MaxStage, Config, ConfigData, LintError, LintConfig, TokenTypes, Parser as ParserBase } from './base';
4
- import type { TitleOptions } from './lib/title';
3
+ import type { TitleOptions, Title } from './lib/title';
5
4
  import type { LanguageService, QuickFixData } from './lib/lsp';
6
5
  import type { Token } from './internal';
7
6
  declare interface Parser extends ParserBase {
package/dist/index.js CHANGED
@@ -9,14 +9,13 @@ const debug_1 = require("./util/debug");
9
9
  const constants_1 = require("./util/constants");
10
10
  const string_1 = require("./util/string");
11
11
  const lintConfig_1 = require("./lib/lintConfig");
12
- const title_1 = require("./lib/title");
13
12
  /* NOT FOR BROWSER ONLY */
14
13
  const fs_1 = __importDefault(require("fs"));
15
14
  const path_1 = __importDefault(require("path"));
16
15
  const common_1 = require("@bhsd/common");
17
16
  const diff_1 = require("./util/diff");
18
17
  /* NOT FOR BROWSER ONLY */
19
- const re = new RegExp(String.raw `^https?:\/\/([^./]+)\.(${common_1.wmf})\.org`, 'iu');
18
+ const re = new RegExp(String.raw `^https?://([^./]+)\.(${common_1.wmf})\.org`, 'iu');
20
19
  /**
21
20
  * require一个JSON文件
22
21
  * @param file 文件名
@@ -139,9 +138,10 @@ const Parser = {
139
138
  },
140
139
  /** @implements */
141
140
  normalizeTitle(title, defaultNs = 0, include, config = Parser.getConfig(), opt, accum = []) {
141
+ const { Title: TitleObj } = require('./lib/title');
142
142
  let titleObj;
143
143
  if (opt?.halfParsed) {
144
- titleObj = new title_1.Title(title, defaultNs, config, opt, accum);
144
+ titleObj = new TitleObj(title, defaultNs, config, opt, accum);
145
145
  }
146
146
  else {
147
147
  const { Token } = require('./src/index');
@@ -150,7 +150,7 @@ const Parser = {
150
150
  root.type = 'root';
151
151
  root.pageName = opt?.page;
152
152
  root.parseOnce(0, include).parseOnce();
153
- const t = new title_1.Title(root.firstChild.toString(), defaultNs, config, opt, accum);
153
+ const t = new TitleObj(root.firstChild.toString(), defaultNs, config, opt, accum);
154
154
  root.build();
155
155
  const keys = [
156
156
  'main',
@@ -3,7 +3,7 @@ import type { TextDocument, Range } from 'vscode-languageserver-textdocument';
3
3
  import type * as JsonLanguageService from 'vscode-json-languageservice';
4
4
  import type * as CssLanguageService from 'vscode-css-languageservice';
5
5
  import type * as HtmlLanguageService from 'vscode-html-languageservice';
6
- import type { PublicApi } from 'stylelint';
6
+ import type { Warning } from 'stylelint';
7
7
  import type { Token } from '../internal';
8
8
  export interface TexvcLocation {
9
9
  offset: number;
@@ -31,11 +31,12 @@ declare interface Texvcjs {
31
31
  usemhchem?: boolean;
32
32
  }): TexvcReport;
33
33
  }
34
+ declare type StylelintUtil = (code: string) => Promise<Warning[]>;
34
35
  export declare const loadTexvcjs: () => Texvcjs | null;
35
36
  export declare const loadJsonLSP: () => JsonLanguageService.LanguageService | null;
36
37
  export declare const loadCssLSP: () => CssLanguageService.LanguageService | null;
37
38
  export declare const loadHtmlData: () => HtmlLanguageService.IHTMLDataProvider | null;
38
- export declare const loadStylelint: () => PublicApi | null;
39
+ export declare const loadStylelint: () => StylelintUtil | null;
39
40
  /** embedded document */
40
41
  declare abstract class EmbeddedDocument implements TextDocument {
41
42
  #private;
@@ -96,7 +96,9 @@ const loadStylelint = () => {
96
96
  NPM: {
97
97
  if (stylelint === undefined) {
98
98
  try {
99
- stylelint = require('stylelint');
99
+ const stylelintApi = require('stylelint'), { styleLint } = require('@bhsd/stylelint-util');
100
+ const cssRules = { 'block-no-empty': null };
101
+ stylelint = /** @implements */ (code) => styleLint(stylelintApi, code, cssRules);
100
102
  }
101
103
  catch /* c8 ignore start */ {
102
104
  stylelint = null;
package/dist/lib/lsp.js CHANGED
@@ -20,7 +20,6 @@ const path_1 = __importDefault(require("path"));
20
20
  const util_1 = __importDefault(require("util"));
21
21
  const child_process_1 = require("child_process");
22
22
  const crypto_1 = require("crypto");
23
- const stylelint_util_1 = require("@bhsd/stylelint-util");
24
23
  const color_1 = require("@bhsd/common/color");
25
24
  const search_1 = __importDefault(require("../vendor/search"));
26
25
  const constants_2 = require("../util/constants");
@@ -298,7 +297,7 @@ const partialParse = async (wikitext, watch, include, config = index_1.default.g
298
297
  };
299
298
  /* NOT FOR BROWSER ONLY */
300
299
  /** @see https://www.npmjs.com/package/stylelint-config-recommended */
301
- const cssRules = { 'block-no-empty': null }, colors = Object.keys(color_1.colorsNamed), sources = { 'invalid-css': 'css', 'invalid-math': 'texvc' }, jsonSelector = constants_2.jsonTags.map(s => `ext#${s}`).join(), scores = new Map();
300
+ const colors = Object.keys(color_1.colorsNamed), sources = { 'invalid-css': 'css', 'invalid-math': 'texvc' }, jsonSelector = constants_2.jsonTags.map(s => `ext#${s}`).join(), scores = new Map();
302
301
  /**
303
302
  * Correct the position of an error.
304
303
  * @param height
@@ -586,7 +585,7 @@ class LanguageService {
586
585
  this.config ??= index_1.default.getConfig();
587
586
  const { nsid, ext, html, parserFunction: [insensitive, sensitive, ...other], doubleUnderscore, protocol, img, } = this.config, tags = new Set([ext, html].flat(2));
588
587
  const re = new RegExp('(?:' // eslint-disable-line prefer-template
589
- + String.raw `<(\/?\w*)` // tag ($1)
588
+ + String.raw `<(/?\w*)` // tag ($1)
590
589
  + '|'
591
590
  + String.raw `(\{{2,4}|\[\[)\s*([^|{}<>[\]\s][^|{}<>[\]#]*)?` // braces and brackets ($2, $3)
592
591
  + '|'
@@ -628,7 +627,7 @@ class LanguageService {
628
627
  * @param position position / 位置
629
628
  */
630
629
  async provideCompletionItems(text, position) {
631
- const { re, allTags, functions, switches, jaSwitches, protocols, params, tags, ext, } = this.#prepareCompletionConfig(), { line, character } = position, curLine = text.split(/\r?\n/u, line + 1)[line], mt = re.exec(curLine?.slice(0, character) ?? '');
630
+ const { re, allTags, functions, switches, jaSwitches, protocols, params, tags, ext, } = this.#prepareCompletionConfig(), { line, character } = position, curLine = text.split(/\r?\n/u, line + 1)[line], mt = re.exec(curLine?.slice(Math.max(0, character - 1e4), character) ?? '');
632
631
  if (mt?.[1] !== undefined) { // tag
633
632
  const closing = mt[1].startsWith('/');
634
633
  return getCompletion(allTags, 'Class', mt[1].slice(closing ? 1 : 0), position, closing && !curLine?.slice(character).trimStart().startsWith('>') ? '>' : '');
@@ -880,7 +879,7 @@ class LanguageService {
880
879
  if (tokens.length === 0) {
881
880
  return [];
882
881
  }
883
- const code = tokens.map(({ type, tag, lastChild }, i) => `${type === 'ext-attr' ? 'div' : tag}#${i}{\n${(0, common_1.sanitizeInlineStyle)(lastChild.toString())}\n}`).join('\n'), cssErrors = await (0, stylelint_util_1.styleLint)(stylelint, code, cssRules);
882
+ const code = tokens.map(({ type, tag, lastChild }, i) => `${type === 'ext-attr' ? 'div' : tag}#${i}{\n${(0, common_1.sanitizeInlineStyle)(lastChild.toString())}\n}`).join('\n'), cssErrors = await stylelint(code);
884
883
  if (cssErrors.length === 0) {
885
884
  return [];
886
885
  }
package/dist/lib/text.js CHANGED
@@ -43,16 +43,6 @@ const errorSyntaxUrl = /* #__PURE__ */ new RegExp(source, 'giu'), noLinkTypes =
43
43
  'select',
44
44
  'textarea',
45
45
  ]);
46
- const wordRegex = /* #__PURE__ */ (() => {
47
- try {
48
- // eslint-disable-next-line prefer-regex-literals
49
- return new RegExp(String.raw `[\p{L}\p{N}_]`, 'u');
50
- }
51
- catch /* c8 ignore start */ {
52
- return /\w/u;
53
- }
54
- /* c8 ignore stop */
55
- })();
56
46
  /**
57
47
  * text node
58
48
  *
@@ -105,7 +95,8 @@ class AstText extends node_1.AstNode {
105
95
  return [];
106
96
  }
107
97
  errorRegex.lastIndex = 0;
108
- const errors = [], nextType = nextSibling?.type, nextName = nextSibling?.name, previousType = previousSibling?.type, root = this.getRootNode(), rootStr = root.toString(), { ext, html, variants } = root.getAttribute('config'), { top, left } = root.posFromIndex(start), { lintConfig } = index_1.default, tagLike = lintConfig.rules['tag-like'], specified = tagLike[1]
98
+ let rule = 'tag-like';
99
+ const errors = [], nextType = nextSibling?.type, nextName = nextSibling?.name, previousType = previousSibling?.type, root = this.getRootNode(), rootStr = root.toString(), { ext, html, variants } = root.getAttribute('config'), { top, left } = root.posFromIndex(start), { lintConfig } = index_1.default, tagLike = lintConfig.rules[rule], specified = tagLike[1]
109
100
  ? new Set(Object.keys(tagLike[1]).filter(tag => tag !== 'invalid' && tag !== 'disallowed'))
110
101
  : new Set(), tags = new Set([
111
102
  'onlyinclude',
@@ -116,7 +107,7 @@ class AstText extends node_1.AstNode {
116
107
  ...html[1],
117
108
  ...html[2],
118
109
  ...specified,
119
- ...lintConfig.getSeverity('tag-like', 'disallowed') ? disallowedTags : [],
110
+ ...lintConfig.getSeverity(rule, 'disallowed') ? disallowedTags : [],
120
111
  ]);
121
112
  for (let mt = errorRegex.exec(data); mt; mt = errorRegex.exec(data)) {
122
113
  let { index, 0: error } = mt;
@@ -143,7 +134,7 @@ class AstText extends node_1.AstNode {
143
134
  errorRegex.lastIndex--;
144
135
  }
145
136
  // Rule & Severity
146
- let startIndex = start + index, endIndex = startIndex + length, rule, severity;
137
+ let startIndex = start + index, endIndex = startIndex + length, severity;
147
138
  const nextChar = rootStr[endIndex], previousChar = rootStr[startIndex - 1], leftBracket = lbrace || lbrack, lConverter = error === '{' && previousChar === '-' && variants.length > 0, rConverter = error === '}' && nextChar === '-' && variants.length > 0, brokenExtLink = lbrack && nextType === 'free-ext-link' && !data.slice(index + 1).trim()
148
139
  || rbrack && previousType === 'free-ext-link' && index === 0;
149
140
  if (magicLink) {
@@ -274,7 +265,7 @@ class AstText extends node_1.AstNode {
274
265
  if (char === '<') {
275
266
  e.suggestions = [(0, lint_1.fixByEscape)(startIndex, '&lt;')];
276
267
  }
277
- else if (char === 'h' && type !== 'link-text' && wordRegex.test(previousChar ?? '')) {
268
+ else if (char === 'h' && type !== 'link-text' && /[\p{L}\p{N}_]/u.test(previousChar ?? '')) {
278
269
  e.suggestions = [(0, lint_1.fixBySpace)(startIndex)];
279
270
  }
280
271
  else if (lbrackInExtLinkText) {
package/dist/lib/title.js CHANGED
@@ -11,6 +11,7 @@ const resolve = (title) => {
11
11
  const [, { length }, sub] = /^((?:\.\.\/)*)(.*)/su.exec(title);
12
12
  return [length / 3, sub];
13
13
  };
14
+ const capitalLinkNs = new Set([-1, 2, 3, 8, 9]);
14
15
  /**
15
16
  * title object of a MediaWiki page
16
17
  *
@@ -22,6 +23,7 @@ class Title {
22
23
  #namespaces;
23
24
  #path;
24
25
  #ns;
26
+ #capitalLinks;
25
27
  #fragment;
26
28
  /** @private */
27
29
  page;
@@ -42,7 +44,9 @@ class Title {
42
44
  }
43
45
  set main(title) {
44
46
  title = title.replace(/_/gu, ' ').trim();
45
- this.#main = title && title[0].toUpperCase() + title.slice(1);
47
+ this.#main = this.#capitalLinks || capitalLinkNs.has(this.#ns)
48
+ ? title && title[0].toUpperCase() + title.slice(1)
49
+ : title;
46
50
  }
47
51
  /** namespace prefix / 命名空间前缀 */
48
52
  get prefix() {
@@ -129,6 +133,7 @@ class Title {
129
133
  && (0, string_1.decodeHtml)(title) === title
130
134
  && (level === 0 || page === undefined || page.split('/', level + 1).length > level)
131
135
  && !/^:|\0\d+[eh!+-]\x7F|[<>[\]{}|\n]|%[\da-f]{2}|(?:^|\/)\.{1,2}(?:$|\/)/iu.test(sub);
136
+ this.#capitalLinks = config.capitalLinks !== false;
132
137
  this.main = title;
133
138
  this.#namespaces = config.namespaces;
134
139
  this.#path = config.articlePath || '/wiki/$1';
@@ -3,23 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseCommentAndExt = void 0;
4
4
  const common_1 = require("@bhsd/common");
5
5
  const string_1 = require("../util/string");
6
+ const replace_1 = require("../util/replace");
6
7
  const onlyinclude_1 = require("../src/onlyinclude");
7
8
  const noinclude_1 = require("../src/nowiki/noinclude");
8
9
  const include_1 = require("../src/tagPair/include");
9
10
  const ext_1 = require("../src/tagPair/ext");
10
11
  const comment_1 = require("../src/nowiki/comment");
11
- const onlyincludeLeft = '<onlyinclude>', onlyincludeRight = '</onlyinclude>', { length } = onlyincludeLeft, getExtRegex = [false, true].map(includeOnly => {
12
+ const onlyincludeLeft = '<onlyinclude>', onlyincludeRight = '</onlyinclude>', getExtRegex = [false, true].map(includeOnly => {
12
13
  const noincludeRegex = includeOnly ? 'includeonly' : '(?:no|only)include', includeRegex = includeOnly ? 'noinclude' : 'includeonly';
13
- return (0, common_1.getRegex)(exts => new RegExp(String.raw `<!--.*?(?:-->|$)|<${noincludeRegex}(?:\s[^>]*)?/?>|</${noincludeRegex}\s*>|<(${exts})(\s[^>]*?)?(?:/>|>(.*?)</(${'\\1'}\s*)>)|<(${includeRegex})(\s[^>]*?)?(?:/>|>(.*?)(?:</(${includeRegex}\s*)>|$))`, 'gisu'));
14
+ return (0, common_1.getRegex)(exts => new RegExp(String.raw `<!--.*?(?:-->|$)|<${noincludeRegex}(?=\s|/?>)|</${noincludeRegex}\s*>|<(${exts})(?=\s|/?>)|<(${includeRegex})(?=\s|/?>)`, 'gisu'));
14
15
  });
15
- /**
16
- * 更新`<onlyinclude>`和`</onlyinclude>`的位置
17
- * @param wikitext
18
- */
19
- const update = (wikitext) => {
20
- const i = wikitext.indexOf(onlyincludeLeft);
21
- return { i, j: wikitext.indexOf(onlyincludeRight, i + length) };
22
- };
23
16
  /**
24
17
  * 解析HTML注释和扩展标签
25
18
  * @param wikitext
@@ -29,32 +22,20 @@ const update = (wikitext) => {
29
22
  */
30
23
  const parseCommentAndExt = (wikitext, config, accum, includeOnly) => {
31
24
  if (includeOnly) {
32
- let { i, j } = update(wikitext);
33
- if (i !== -1 && j !== -1) { // `<onlyinclude>`拥有最高优先级
34
- let str = '';
35
- /**
36
- * 忽略未被`<onlyinclude>`和`</onlyinclude>`包裹的内容
37
- * @param text 未被包裹的内容
38
- */
39
- const noinclude = (text) => {
40
- // @ts-expect-error abstract class
41
- new noinclude_1.NoincludeToken(text, config, accum);
42
- str += `\0${accum.length - 1}n\x7F`;
43
- };
44
- while (i !== -1 && j !== -1) {
25
+ const i = wikitext.indexOf(onlyincludeLeft);
26
+ if (i !== -1 && wikitext.includes(onlyincludeRight, i)) { // `<onlyinclude>`拥有最高优先级
27
+ return (0, replace_1.safeStringReplace)(wikitext, onlyincludeLeft, onlyincludeRight, false, inner => {
45
28
  const token = `\0${accum.length}g\x7F`;
46
- new onlyinclude_1.OnlyincludeToken(wikitext.slice(i + length, j), config, accum);
47
- if (i > 0) {
48
- noinclude(wikitext.slice(0, i));
29
+ new onlyinclude_1.OnlyincludeToken(inner, config, accum);
30
+ return token;
31
+ }, s => {
32
+ if (!s) {
33
+ return '';
49
34
  }
50
- str += token;
51
- wikitext = wikitext.slice(j + length + 1);
52
- ({ i, j } = update(wikitext));
53
- }
54
- if (wikitext) {
55
- noinclude(wikitext);
56
- }
57
- return str;
35
+ // @ts-expect-error abstract class
36
+ new noinclude_1.NoincludeToken(s, config, accum);
37
+ return `\0${accum.length - 1}n\x7F`;
38
+ });
58
39
  }
59
40
  }
60
41
  const { ext } = config;
@@ -64,45 +45,103 @@ const parseCommentAndExt = (wikitext, config, accum, includeOnly) => {
64
45
  const stack = [];
65
46
  newExt = ext.filter(e => e !== 'translate' && e !== 'tvar');
66
47
  newConfig = { ...config, ext: newExt };
67
- wikitext = wikitext.replace(/<nowiki>.*?<\/nowiki>/gisu, m => {
68
- stack.push(m);
48
+ // 隐藏`<nowiki>`标签
49
+ const text = (0, replace_1.safeStringReplace)(wikitext, '<nowiki>', '</nowiki>', true, (inner, open, close) => {
50
+ stack.push(open + inner + close);
69
51
  return `\0${stack.length - 1}\x7F`;
70
- }).replace(/<translate( nowrap)?>(.*?)<\/translate>/gsu, (_, p1, p2) => {
52
+ });
53
+ // 解析`<translate>`标签
54
+ wikitext = (0, replace_1.safePatternReplace)(text, /<translate( nowrap)?>/gu, '</translate>', (inner, [, p1]) => {
71
55
  const l = accum.length;
72
56
  // @ts-expect-error abstract class
73
- new TranslateToken(p1, p2 && (0, string_1.restore)(p2, stack), newConfig, accum);
57
+ new TranslateToken(p1, inner && (0, string_1.restore)(inner, stack), newConfig, accum);
74
58
  return `\0${l}g\x7F`;
75
59
  });
76
60
  wikitext = (0, string_1.restore)(wikitext, stack);
77
61
  }
78
- const re = getExtRegex[includeOnly ? 1 : 0](newExt.join('|'));
62
+ const re = getExtRegex[includeOnly ? 1 : 0](newExt.join('|')), noMoreTags = new Set();
79
63
  re.lastIndex = 0;
80
- return re.test(wikitext)
81
- ? wikitext.replace(re, (substr, name, attr, inner, closing, include, includeAttr, includeInner, includeClosing) => {
82
- const l = accum.length;
83
- let ch = 'n';
84
- if (name) {
85
- ch = 'e';
86
- // @ts-expect-error abstract class
87
- new ext_1.ExtToken(name, attr, inner, closing, newConfig, include, accum);
64
+ let lastIndex = 0, text = '', noMoreGt = false;
65
+ for (let mt = re.exec(wikitext); mt; mt = re.exec(wikitext)) {
66
+ const [substr, name, include] = mt, tag = name || include, { index } = mt, l = accum.length;
67
+ text += wikitext.slice(lastIndex, index);
68
+ ({ lastIndex } = re);
69
+ let ch = 'n', matched = true;
70
+ if (substr.startsWith('<!--')) {
71
+ ch = 'c';
72
+ const closed = substr.endsWith('-->');
73
+ // @ts-expect-error abstract class
74
+ new comment_1.CommentToken((0, string_1.restore)(substr, accum, 1)
75
+ .slice(4, closed ? -3 : undefined), closed, config, accum);
76
+ }
77
+ else if (noMoreGt) {
78
+ matched = false;
79
+ }
80
+ else if (tag) {
81
+ const i = wikitext.indexOf('>', lastIndex);
82
+ let attr, inner, closing;
83
+ if (i === -1) {
84
+ matched = false;
85
+ noMoreGt = true;
88
86
  }
89
- else if (substr.startsWith('<!--')) {
90
- ch = 'c';
91
- const closed = substr.endsWith('-->');
92
- // @ts-expect-error abstract class
93
- new comment_1.CommentToken((0, string_1.restore)(substr, accum, 1)
94
- .slice(4, closed ? -3 : undefined), closed, config, accum);
87
+ else if (wikitext[i - 1] === '/') {
88
+ attr = wikitext.slice(lastIndex, i - 1);
89
+ lastIndex = i + 1;
90
+ re.lastIndex = lastIndex;
95
91
  }
96
- else if (include) {
97
- // @ts-expect-error abstract class
98
- new include_1.IncludeToken(include, includeAttr && (0, string_1.restore)(includeAttr, accum, 1), includeInner && (0, string_1.restore)(includeInner, accum, 1), includeClosing, config, accum);
92
+ else if (noMoreTags.has(tag.toLowerCase())) {
93
+ matched = false;
99
94
  }
100
95
  else {
96
+ attr = wikitext.slice(lastIndex, i);
97
+ const after = wikitext.slice(i + 1), m = new RegExp(String.raw `</(${tag}\s*)>`, 'iu').exec(after);
98
+ if (m) {
99
+ inner = after.slice(0, m.index);
100
+ [, closing] = m;
101
+ lastIndex = i + 1 + m.index + m[0].length;
102
+ re.lastIndex = lastIndex;
103
+ }
104
+ else if (include) {
105
+ inner = after;
106
+ lastIndex = wikitext.length;
107
+ re.lastIndex = lastIndex;
108
+ }
109
+ else {
110
+ matched = false;
111
+ noMoreTags.add(tag.toLowerCase());
112
+ }
113
+ }
114
+ if (matched) {
115
+ if (name) {
116
+ ch = 'e';
117
+ // @ts-expect-error abstract class
118
+ new ext_1.ExtToken(name, attr, inner, closing, newConfig, include, accum);
119
+ }
120
+ else {
121
+ // @ts-expect-error abstract class
122
+ new include_1.IncludeToken(include, attr && (0, string_1.restore)(attr, accum, 1), inner && (0, string_1.restore)(inner, accum, 1), closing, config, accum);
123
+ }
124
+ }
125
+ }
126
+ else if (substr.startsWith('</')) {
127
+ // @ts-expect-error abstract class
128
+ new noinclude_1.NoincludeToken(wikitext.slice(index, lastIndex), config, accum, true);
129
+ }
130
+ else {
131
+ const i = wikitext.indexOf('>', lastIndex);
132
+ if (i === -1) {
133
+ matched = false;
134
+ noMoreGt = true;
135
+ }
136
+ else {
137
+ lastIndex = i + 1;
138
+ re.lastIndex = lastIndex;
101
139
  // @ts-expect-error abstract class
102
- new noinclude_1.NoincludeToken(substr, config, accum, true);
140
+ new noinclude_1.NoincludeToken(wikitext.slice(index, lastIndex), config, accum, true);
103
141
  }
104
- return `\0${l}${ch}\x7F`;
105
- })
106
- : wikitext;
142
+ }
143
+ text += matched ? `\0${l}${ch}\x7F` : substr;
144
+ }
145
+ return text + wikitext.slice(lastIndex);
107
146
  };
108
147
  exports.parseCommentAndExt = parseCommentAndExt;
@@ -11,8 +11,8 @@ const converter_1 = require("../src/converter");
11
11
  const parseConverter = (text, config, accum) => {
12
12
  config.regexConverter ??= new RegExp(String.raw `;(?=(?:[^;]*?=>)?\s*(?:${config.variants.join('|')})\s*:|(?:\s|\0\d+[cn]\x7F)*$)`, 'u');
13
13
  const regex1 = /-\{/gu, regex2 = /-\{|\}-/gu, stack = [];
14
- let regex = regex1, mt = regex.exec(text);
15
- while (mt) {
14
+ let regex = regex1;
15
+ for (let mt = regex.exec(text); mt; mt = regex.exec(text)) {
16
16
  const { 0: syntax, index } = mt;
17
17
  if (syntax === '}-') {
18
18
  const top = stack.pop(), { length } = accum, str = text.slice(top.index + 2, index), i = str.indexOf('|'), [flags, raw] = i === -1 ? [[], str] : [str.slice(0, i).split(';'), str.slice(i + 1)], temp = raw.replace(/(&[#a-z\d]+);/giu, '$1\x01'), rules = temp.split(config.regexConverter)
@@ -30,7 +30,6 @@ const parseConverter = (text, config, accum) => {
30
30
  regex = regex2;
31
31
  regex.lastIndex = index + 2;
32
32
  }
33
- mt = regex.exec(text);
34
33
  }
35
34
  return text;
36
35
  };
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseExternalLinks = void 0;
4
4
  const string_1 = require("../util/string");
5
+ const replace_1 = require("../util/replace");
5
6
  const extLink_1 = require("../src/extLink");
6
7
  const magicLink_1 = require("../src/magicLink");
7
8
  /**
@@ -12,13 +13,17 @@ const magicLink_1 = require("../src/magicLink");
12
13
  * @param inFile 是否在图链中
13
14
  */
14
15
  const parseExternalLinks = (wikitext, config, accum, inFile) => {
15
- config.regexExternalLinks ??= new RegExp(String.raw `\[((?:\0\d+[cn]\x7F)*(?:\0\d+f\x7F|(?:(?:${config.protocol}|//)${string_1.extUrlCharFirst}|\0\d+m\x7F)${string_1.extUrlChar}(?=[[\]<>"\t${string_1.zs}]|\0\d)))([${string_1.zs}]*(?![${string_1.zs}]))([^\]\x01-\x08\x0A-\x1F\uFFFD]*)\]`, 'giu');
16
- return wikitext.replace(config.regexExternalLinks, (_, url, space, text) => {
17
- const { length } = accum, mt = /&[lg]t;/u.exec(url);
18
- if (mt) {
16
+ config.regexExternalLinks ??= new RegExp(String.raw `\[((?:\0\d+[cn]\x7F)*(?:\0\d+f\x7F|(?:(?:${config.protocol}|//)${string_1.extUrlCharFirst}|\0\d+m\x7F)${string_1.extUrlChar}(?=[[\]<>"\t${string_1.zs}]|\0\d)))([${string_1.zs}]*(?![${string_1.zs}]))`, 'giu');
17
+ const re = config.regexExternalLinks;
18
+ return (0, replace_1.safePatternReplace)(wikitext, re, ']', (text, [, url, space]) => {
19
+ if (/[\x01-\x08\x0A-\x1F\uFFFD]/u.test(text)) {
20
+ return false;
21
+ }
22
+ const { length } = accum, i = url.search(/&[lg]t;/u);
23
+ if (i !== -1) {
19
24
  space = '';
20
- text = url.slice(mt.index) + space + text;
21
- url = url.slice(0, mt.index);
25
+ text = url.slice(i) + space + text;
26
+ url = url.slice(0, i);
22
27
  }
23
28
  if (inFile) {
24
29
  // @ts-expect-error abstract class
@@ -11,23 +11,15 @@ const space = String.raw `[${string_1.zs}\t]|&nbsp;|&#0*160;|&#x0*a0;`, sp = `(?
11
11
  * @param accum
12
12
  */
13
13
  const parseMagicLinks = (wikitext, config, accum) => {
14
- if (!config.regexMagicLinks) {
15
- try {
16
- config.regexMagicLinks = new RegExp(String.raw `(^|[^\p{L}\p{N}_])(?:(?:${config.protocol})(${string_1.extUrlCharFirst}${string_1.extUrlChar})|${magicLinkPattern})`, 'giu');
17
- }
18
- catch /* c8 ignore start */ {
19
- config.regexMagicLinks = new RegExp(String.raw `(^|\W)(?:(?:${config.protocol})(${string_1.extUrlCharFirst}${string_1.extUrlChar})|${magicLinkPattern})`, 'giu');
20
- }
21
- /* c8 ignore stop */
22
- }
14
+ config.regexMagicLinks ??= new RegExp(String.raw `(^|[^\p{L}\p{N}_])(?:(?:${config.protocol})(${string_1.extUrlCharFirst}${string_1.extUrlChar})|${magicLinkPattern})`, 'giu');
23
15
  return wikitext.replace(config.regexMagicLinks, (m, lead, p1) => {
24
16
  let url = lead ? m.slice(lead.length) : m;
25
17
  if (p1) {
26
18
  let trail = '';
27
- const m2 = /&(?:[lg]t|nbsp|#x0*(?:3[ce]|a0)|#0*(?:6[02]|160));/iu.exec(url);
28
- if (m2) {
29
- trail = url.slice(m2.index);
30
- url = url.slice(0, m2.index);
19
+ const index = url.search(/&(?:[lg]t|nbsp|#x0*(?:3[ce]|a0)|#0*(?:6[02]|160));/iu);
20
+ if (index !== -1) {
21
+ trail = url.slice(index);
22
+ url = url.slice(0, index);
31
23
  }
32
24
  const sep = url.includes('(') ? /[^,;\\.:!?][,;\\.:!?]+$/u : /[^,;\\.:!?)][,;\\.:!?)]+$/u, sepChars = sep.exec(url);
33
25
  if (sepChars) {
@@ -100,7 +100,7 @@ const parseTable = ({ firstChild: { data }, type, name }, config, accum) => {
100
100
  else {
101
101
  top = pop(top, stack);
102
102
  const regex = cell === '!' ? /!!|(?:\||\0\d+!\x7F){2}|\0\d+\+\x7F/gu : /(?:\||\0\d+!\x7F){2}|\0\d+\+\x7F/gu;
103
- let mt = regex.exec(attr), lastIndex = 0, lastSyntax = `\n${spaces}${cell}`;
103
+ let lastIndex = 0, lastSyntax = `\n${spaces}${cell}`, mt;
104
104
  /**
105
105
  * 创建表格单元格
106
106
  * @param tr 当前表格行
@@ -111,11 +111,10 @@ const parseTable = ({ firstChild: { data }, type, name }, config, accum) => {
111
111
  tr.insertAt(td);
112
112
  return td;
113
113
  };
114
- while (mt) {
114
+ for (mt = regex.exec(attr); mt; mt = regex.exec(attr)) {
115
115
  newTd(top);
116
116
  ({ lastIndex } = regex);
117
117
  [lastSyntax] = mt;
118
- mt = regex.exec(attr);
119
118
  }
120
119
  stack.push(top, newTd(top));
121
120
  }
@@ -14,7 +14,7 @@ const index_2 = require("./index");
14
14
  const atom_1 = require("./atom");
15
15
  /* NOT FOR BROWSER ONLY */
16
16
  const document_1 = require("../lib/document");
17
- const insecureStyle = /expression|(?:accelerator|-o-link(?:-source)?|-o-replace)\s*:|(?:url|src|image(?:-set)?)\s*\(|attr\s*\([^)]+[\s,]url/u, evil = /(?:^|\s|\*\/)(?:java|vb)script(?:\W|$)/iu, complexTypes = new Set(['ext', 'arg', 'magic-word', 'template']), itemAttrs = new Set(['itemtype', 'itemid', 'itemref']), urlAttrs = new Set([
17
+ const insecureStyle = /expression|(?:accelerator|-o-link(?:-source)?|-o-replace)\s*:|(?:url|src|image(?:-set)?)\s*\(|attr\s*\((?:(?!attr\s*\()[^)])+[\s,]url/u, evil = /(?:^|\s|\*\/)(?:java|vb)script(?:\W|$)/iu, complexTypes = new Set(['ext', 'arg', 'magic-word', 'template']), itemAttrs = new Set(['itemtype', 'itemid', 'itemref']), urlAttrs = new Set([
18
18
  'about',
19
19
  'property',
20
20
  'resource',
@@ -22,16 +22,6 @@ exports.toAttributeType = toAttributeType;
22
22
  * @param type 属性类型
23
23
  */
24
24
  const toDirty = (type) => `${(0, exports.toAttributeType)(type)}-dirty`;
25
- const wordRegex = /* #__PURE__ */ (() => {
26
- try {
27
- // eslint-disable-next-line prefer-regex-literals
28
- return new RegExp(String.raw `[\p{L}\p{N}]`, 'u');
29
- }
30
- catch /* c8 ignore start */ {
31
- return /[^\W_]/u;
32
- }
33
- /* c8 ignore stop */
34
- })();
35
25
  const required = new Map([
36
26
  ['indicator', ['name']],
37
27
  ['langconvert', ['from', 'to']],
@@ -63,14 +53,14 @@ class AttributesToken extends index_2.Token {
63
53
  this.setAttribute('name', name);
64
54
  if (attr) {
65
55
  const regex = /([^\s/](?:(?!\0\d+~\x7F)[^\s/=])*)(?:((?:\s(?:\s|\0\d+[cn]\x7F)*)?(?:=|\0\d+~\x7F)(?:\s|\0\d+[cn]\x7F)*)(?:(["'])(.*?)(\3|$)|(\S*)))?/gsu;
66
- let out = '', mt = regex.exec(attr), lastIndex = 0;
56
+ let out = '', lastIndex = 0;
67
57
  const insertDirty = /** 插入无效属性 */ () => {
68
58
  if (out) {
69
59
  super.insertAt(new atom_1.AtomToken(out, toDirty(type), config, accum));
70
60
  out = '';
71
61
  }
72
62
  };
73
- while (mt) {
63
+ for (let mt = regex.exec(attr); mt; mt = regex.exec(attr)) {
74
64
  const { index, 0: full, 1: key, 2: equal, 3: quoteStart, 4: quoted, 5: quoteEnd, 6: unquoted } = mt;
75
65
  out += attr.slice(lastIndex, index);
76
66
  if (/^(?:[\w:]|\0\d+t\x7F)(?:[\w:.-]|\0\d+t\x7F)*$/u
@@ -85,7 +75,6 @@ class AttributesToken extends index_2.Token {
85
75
  out += full;
86
76
  }
87
77
  ({ lastIndex } = regex);
88
- mt = regex.exec(attr);
89
78
  }
90
79
  out += attr.slice(lastIndex);
91
80
  insertDirty();
@@ -185,7 +174,7 @@ class AttributesToken extends index_2.Token {
185
174
  }
186
175
  }
187
176
  else {
188
- const str = attr.text().trim(), severity = s[wordRegex.test(str) ? 1 : 2];
177
+ const str = attr.text().trim(), severity = s[/[\p{L}\p{N}]/u.test(str) ? 1 : 2];
189
178
  if (str && severity) {
190
179
  const e = (0, lint_1.generateForChild)(attr, rect, rules[0], 'invalid-attribute', severity);
191
180
  if (computeEditInfo) {
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CommentedToken = void 0;
4
+ const replace_1 = require("../util/replace");
4
5
  const index_1 = require("./index");
5
6
  const comment_1 = require("./nowiki/comment");
6
7
  /**
@@ -15,25 +16,19 @@ class CommentedToken extends index_1.Token {
15
16
  constructor(wikitext, config, accum = []) {
16
17
  super(undefined, config, accum, {});
17
18
  if (wikitext) {
18
- let i = wikitext.indexOf('<!--'), j = i !== -1 && wikitext.indexOf('-->', i + 4), lastIndex = 0;
19
- while (j !== false && j !== -1) {
20
- if (i > lastIndex) {
21
- this.insertAt(wikitext.slice(lastIndex, i));
22
- }
19
+ (0, replace_1.safeStringReplace)(wikitext, '<!--', '-->', false, inner => {
23
20
  // @ts-expect-error abstract class
24
- this.insertAt(new comment_1.CommentToken(wikitext.slice(i + 4, j), true, config, accum));
25
- lastIndex = j + 3;
26
- i = wikitext.indexOf('<!--', lastIndex);
27
- j = i !== -1 && wikitext.indexOf('-->', i + 4);
28
- }
29
- if (lastIndex < wikitext.length) {
30
- this.insertAt(wikitext.slice(lastIndex));
31
- }
21
+ this.insertAt(new comment_1.CommentToken(inner, true, config, accum));
22
+ return '';
23
+ }, s => {
24
+ this.insertAt(s);
25
+ return '';
26
+ });
32
27
  }
33
28
  }
34
29
  /** @private */
35
30
  lint(start = this.getAbsoluteIndex()) {
36
- LINT: return super.lint(start, new RegExp(String.raw `<\s*(?:\/\s*)?(${this.name})\b`, 'giu'));
31
+ LINT: return super.lint(start, new RegExp(String.raw `<\s*(?:/\s*)?(${this.name})\b`, 'giu'));
37
32
  }
38
33
  }
39
34
  exports.CommentedToken = CommentedToken;
package/dist/src/index.js CHANGED
@@ -1,48 +1,4 @@
1
1
  "use strict";
2
- // PHP解析器的步骤:
3
- // -2. 替换签名和`{{subst:}}`,参见Parser::preSaveTransform;这在revision中不可能保留,可以跳过
4
- // -1. 移除特定字符`\0`和`\x7F`,参见Parser::parse
5
- // 0. 重定向,参见WikitextContentHandler::extractRedirectTargetAndText
6
- // 1. 注释/扩展标签('<'相关),参见Preprocessor_Hash::buildDomTreeArrayFromText和Sanitizer::decodeTagAttributes
7
- // 2. 模板/模板变量/标题,注意rightmost法则,以及`-{`和`[[`可以破坏`{{`或`{{{`语法,
8
- // 参见Preprocessor_Hash::buildDomTreeArrayFromText
9
- // 3. HTML标签(允许不匹配),参见Sanitizer::internalRemoveHtmlTags
10
- // 4. 表格,参见Parser::handleTables
11
- // 5. 水平线、状态开关和余下的标题,参见Parser::internalParse
12
- // 6. 内链,含文件和分类,参见Parser::handleInternalLinks2
13
- // 7. `'`,参见Parser::doQuotes
14
- // 8. 外链,参见Parser::handleExternalLinks
15
- // 9. ISBN、RFC和自由外链,参见Parser::handleMagicLinks
16
- // 10. 段落和列表,参见BlockLevelPass::execute
17
- // 11. 转换,参见LanguageConverter::recursiveConvertTopLevel
18
- // \0\d+.\x7F标记Token:
19
- // !: `{{!}}`专用
20
- // {: `{{(!}}`专用
21
- // }: `{{!)}}`专用
22
- // -: `{{!-}}`专用
23
- // +: `{{!!}}`专用
24
- // ~: `{{=}}`专用
25
- // a: AttributeToken
26
- // b: TableToken
27
- // c: CommentToke
28
- // d: ListToken
29
- // e: ExtToken
30
- // f: ImageParameterToken内的MagicLinkToken
31
- // g: TranslateToken或OnlyincludeToken
32
- // h: HeadingToken
33
- // i: RFC/PMID/ISBN
34
- // l: LinkToken
35
- // m: `{{server}}`、`{{fullurl:}}`、`{{canonicalurl:}}`或`{{filepath:}}`
36
- // n: NoIncludeToken、IncludeToken、TvarToken、DoubleUnderscoreToken或`{{#vardefine:}}`
37
- // o: RedirectToken
38
- // q: QuoteToken
39
- // r: HrToken
40
- // s: `{{{|subst:}}}`
41
- // t: ArgToken或TranscludeToken
42
- // u: `__toc__`
43
- // v: ConverterToken
44
- // w: ExtLinkToken或free-ext-link
45
- // x: HtmlToken
46
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
47
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
48
4
  };
@@ -429,8 +385,7 @@ class Token extends element_1.AstElement {
429
385
  }
430
386
  if (!ignoreDisables) {
431
387
  const regex = new RegExp(String.raw `<!--\s*${configurationComment}-(disable(?:(?:-next)?-line)?|enable)(\s[\sa-z,-]*)?-->`, 'gu'), ignores = [];
432
- let mt = regex.exec(wikitext);
433
- while (mt) {
388
+ for (let mt = regex.exec(wikitext); mt; mt = regex.exec(wikitext)) {
434
389
  const { 1: type, index } = mt, detail = mt[2]?.trim();
435
390
  let line;
436
391
  if (type === 'disable-line' || type === 'disable-next-line') {
@@ -442,7 +397,6 @@ class Token extends element_1.AstElement {
442
397
  to: type === 'enable' ? regex.lastIndex : undefined,
443
398
  rules: detail ? new Set(detail.split(',').map(rule => rule.trim())) : undefined,
444
399
  });
445
- mt = regex.exec(wikitext);
446
400
  }
447
401
  errors = errors.filter(({ rule, startLine, startIndex }) => {
448
402
  const nearest = { pos: 0 };
@@ -25,8 +25,8 @@ const explode = (str) => {
25
25
  return [];
26
26
  }
27
27
  const regex = /-\{|\}-|\|/gu, exploded = [];
28
- let mt = regex.exec(str), depth = 0, lastIndex = 0;
29
- while (mt) {
28
+ let depth = 0, lastIndex = 0;
29
+ for (let mt = regex.exec(str); mt; mt = regex.exec(str)) {
30
30
  const { 0: match, index } = mt;
31
31
  if (match !== '|') {
32
32
  depth += match === '-{' ? 1 : -1;
@@ -35,7 +35,6 @@ const explode = (str) => {
35
35
  exploded.push(str.slice(lastIndex, index));
36
36
  ({ lastIndex } = regex);
37
37
  }
38
- mt = regex.exec(str);
39
38
  }
40
39
  exploded.push(str.slice(lastIndex));
41
40
  return exploded;
@@ -92,7 +92,7 @@ let GalleryImageToken = (() => {
92
92
  /** @private */
93
93
  getTitle(temporary) {
94
94
  const imagemap = this.type === 'imagemap-image';
95
- return this.normalizeTitle(this.firstChild.toString(), imagemap ? 0 : 6, { halfParsed: true, temporary, decode: !imagemap, page: '' });
95
+ return this.normalizeTitle(this.firstChild[imagemap ? 'toString' : 'text'](), imagemap ? 0 : 6, { halfParsed: true, temporary, decode: !imagemap, page: '' });
96
96
  }
97
97
  /** 判定无效的图片 */
98
98
  #lint() {
@@ -5,10 +5,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.GalleryToken = void 0;
7
7
  const lint_1 = require("../../util/lint");
8
+ const string_1 = require("../../util/string");
9
+ const replace_1 = require("../../util/replace");
8
10
  const index_1 = __importDefault(require("../../index"));
9
11
  const index_2 = require("./index");
10
12
  const galleryImage_1 = require("../link/galleryImage");
11
13
  const commentLine_1 = require("../nowiki/commentLine");
14
+ const comment_1 = require("../nowiki/comment");
12
15
  /**
13
16
  * `<gallery>`
14
17
  * @classdesc `{childNodes: (GalleryImageToken|CommentLineToken|AstText)[]}`
@@ -18,7 +21,12 @@ class GalleryToken extends index_2.MultiLineToken {
18
21
  constructor(inner, config, accum = []) {
19
22
  super(undefined, config, accum, {});
20
23
  if (inner) {
21
- for (const line of inner.split('\n')) {
24
+ for (let line of inner.split('\n')) {
25
+ line = (0, replace_1.safeStringReplace)(line, '<!--', '-->', false, p => {
26
+ // @ts-expect-error abstract class
27
+ new comment_1.CommentToken(p, true, config, accum);
28
+ return `\0${accum.length}c\x7F`;
29
+ });
22
30
  const matches = /^([^|]+)(?:\|(.*))?/u.exec(line);
23
31
  if (!matches) {
24
32
  super.insertAt((line.trim()
@@ -28,7 +36,7 @@ class GalleryToken extends index_2.MultiLineToken {
28
36
  continue;
29
37
  }
30
38
  const [, file, alt] = matches;
31
- if (this.#checkFile(file)) { // eslint-disable-line unicorn/prefer-ternary
39
+ if (this.#checkFile(file, true)) { // eslint-disable-line unicorn/prefer-ternary
32
40
  // @ts-expect-error abstract class
33
41
  super.insertAt(new galleryImage_1.GalleryImageToken('gallery', file, alt, config, accum));
34
42
  }
@@ -43,9 +51,10 @@ class GalleryToken extends index_2.MultiLineToken {
43
51
  /**
44
52
  * 检查文件名是否有效
45
53
  * @param file 文件名
54
+ * @param parsing 是否在解析过程中
46
55
  */
47
- #checkFile(file) {
48
- return this.normalizeTitle(file, 6, { halfParsed: true, temporary: true, decode: true, page: '' }).valid;
56
+ #checkFile(file, parsing) {
57
+ return this.normalizeTitle(parsing ? (0, string_1.removeComment)(file) : file, 6, { halfParsed: true, temporary: true, decode: true, page: '' }).valid;
49
58
  }
50
59
  /** @private */
51
60
  lint(start = this.getAbsoluteIndex(), re) {
@@ -40,13 +40,51 @@ class NestedToken extends index_2.Token {
40
40
  wikitext &&= (0, braces_1.parseBraces)(wikitext, config, accum);
41
41
  accum.splice(length, 1);
42
42
  }
43
- else {
44
- wikitext &&= wikitext.replace(regex, (_, name, attr, inner, closing) => {
45
- const str = `\0${accum.length + 1}e\x7F`;
46
- // @ts-expect-error abstract class
47
- new ext_1.ExtToken(name, attr, inner, closing, config, false, accum);
48
- return str;
49
- });
43
+ else if (wikitext) {
44
+ const noMoreTags = new Set();
45
+ regex.lastIndex = 0;
46
+ let lastIndex = 0, text = '';
47
+ for (let mt = regex.exec(wikitext); mt; mt = regex.exec(wikitext)) {
48
+ const i = wikitext.indexOf('>', regex.lastIndex);
49
+ if (i === -1) {
50
+ break;
51
+ }
52
+ const [substr, name] = mt, { index } = mt;
53
+ text += wikitext.slice(lastIndex, index);
54
+ ({ lastIndex } = regex);
55
+ let matched = true, attr, inner, closing;
56
+ if (wikitext[i - 1] === '/') {
57
+ attr = wikitext.slice(lastIndex, i - 1);
58
+ lastIndex = i + 1;
59
+ regex.lastIndex = lastIndex;
60
+ }
61
+ else if (noMoreTags.has(name.toLowerCase())) {
62
+ matched = false;
63
+ }
64
+ else {
65
+ const after = wikitext.slice(i + 1), m = new RegExp(String.raw `</(${name}\s*)>`, 'iu').exec(after);
66
+ if (m) {
67
+ inner = after.slice(0, m.index);
68
+ [, closing] = m;
69
+ lastIndex = i + 1 + m.index + m[0].length;
70
+ regex.lastIndex = lastIndex;
71
+ }
72
+ else {
73
+ matched = false;
74
+ noMoreTags.add(name.toLowerCase());
75
+ }
76
+ }
77
+ if (matched) {
78
+ const str = `\0${accum.length + 1}e\x7F`;
79
+ // @ts-expect-error abstract class
80
+ new ext_1.ExtToken(name, attr, inner, closing, config, false, accum);
81
+ text += str;
82
+ }
83
+ else {
84
+ text += substr;
85
+ }
86
+ }
87
+ wikitext = text + wikitext.slice(lastIndex);
50
88
  }
51
89
  wikitext &&= wikitext.replace(/(^|\0\d+.\x7F)([^\0]+)(?=$|\0\d+.\x7F)/gu, (_, lead, substr) => {
52
90
  // @ts-expect-error abstract class
@@ -8,7 +8,7 @@ const string_1 = require("../util/string");
8
8
  const lint_1 = require("../util/lint");
9
9
  const index_1 = __importDefault(require("../index"));
10
10
  const index_2 = require("./index");
11
- const linkRegex = /* #__PURE__ */ (() => new RegExp(`https?://${string_1.extUrlCharFirst}${string_1.extUrlChar}$`, 'iu'))();
11
+ const linkRegex = /* #__PURE__ */ (() => new RegExp(`^https?://${string_1.extUrlCharFirst}${string_1.extUrlChar}$`, 'u'))();
12
12
  /**
13
13
  * template or magic word parameter
14
14
  *
@@ -74,9 +74,9 @@ class ParameterToken extends index_2.Token {
74
74
  LINT: {
75
75
  const errors = super.lint(start, re), rule = 'unescaped', { lintConfig } = index_1.default, s = lintConfig.getSeverity(rule);
76
76
  if (s) {
77
- const { firstChild } = this, link = linkRegex.exec(firstChild.text())?.[0];
77
+ const { firstChild } = this, key = firstChild.text().toLowerCase(), i = Math.max(key.lastIndexOf('http://'), key.lastIndexOf('https://')), link = i !== -1 && key.slice(i);
78
78
  try {
79
- if (link && new URL(link).search) {
79
+ if (link && linkRegex.test(link) && new URL(link).search) {
80
80
  const e = (0, lint_1.generateForChild)(firstChild, { start }, rule, 'unescaped-query', s);
81
81
  e.startIndex = e.endIndex;
82
82
  e.startLine = e.endLine;
package/dist/src/pre.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PreToken = void 0;
4
4
  const constants_1 = require("../util/constants");
5
+ const replace_1 = require("../util/replace");
5
6
  const index_1 = require("./index");
6
7
  const noinclude_1 = require("./nowiki/noinclude");
7
8
  /**
@@ -14,29 +15,13 @@ class PreToken extends index_1.Token {
14
15
  }
15
16
  /** @class */
16
17
  constructor(wikitext, config, accum = []) {
17
- if (wikitext) {
18
- const opening = /<nowiki>/giu, closing = /<\/nowiki>/giu, { length } = opening.source;
19
- let i = opening.exec(wikitext);
20
- if (i) {
21
- closing.lastIndex = i.index + length;
22
- }
23
- let j = closing.exec(wikitext), lastIndex = 0, str = '';
24
- while (i && j) {
25
- // @ts-expect-error abstract class
26
- new noinclude_1.NoincludeToken(i[0], config, accum, true);
27
- // @ts-expect-error abstract class
28
- new noinclude_1.NoincludeToken(j[0], config, accum, true);
29
- str += `${wikitext.slice(lastIndex, i.index)}\0${accum.length - 1}n\x7F${wikitext.slice(i.index + length, j.index)}\0${accum.length}n\x7F`;
30
- lastIndex = j.index + length + 1;
31
- opening.lastIndex = lastIndex;
32
- i = opening.exec(wikitext);
33
- if (i) {
34
- closing.lastIndex = i.index + length;
35
- }
36
- j = closing.exec(wikitext);
37
- }
38
- wikitext = str + wikitext.slice(lastIndex);
39
- }
18
+ wikitext &&= (0, replace_1.safeStringReplace)(wikitext, '<nowiki>', '</nowiki>', true, (inner, opening, closing) => {
19
+ // @ts-expect-error abstract class
20
+ new noinclude_1.NoincludeToken(opening, config, accum, true);
21
+ // @ts-expect-error abstract class
22
+ new noinclude_1.NoincludeToken(closing, config, accum, true);
23
+ return `\0${accum.length - 1}n\x7F${inner}\0${accum.length}n\x7F`;
24
+ });
40
25
  super(wikitext, config, accum, {});
41
26
  this.setAttribute('stage', constants_1.MAX_STAGE - 1);
42
27
  }
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.CharinsertLineToken = void 0;
7
7
  const lint_1 = require("../../util/lint");
8
+ const replace_1 = require("../../util/replace");
8
9
  const rect_1 = require("../../lib/rect");
9
10
  const index_1 = __importDefault(require("../../index"));
10
11
  const index_2 = require("./index");
@@ -21,9 +22,9 @@ class CharinsertLineToken extends index_2.SingleLineToken {
21
22
  }
22
23
  /** @class */
23
24
  constructor(wikitext, config, accum) {
24
- super(wikitext?.replace(/<(nowiki)>(.*?)<\/(nowiki)>/giu, (_, opening, content, closing) => {
25
+ super(wikitext && (0, replace_1.safeStringReplace)(wikitext, '<nowiki>', '</nowiki>', true, (content, opening, closing) => {
25
26
  // @ts-expect-error abstract class
26
- new ext_1.ExtToken(opening, undefined, content, closing, config, false, accum);
27
+ new ext_1.ExtToken(opening.slice(1, -1), undefined, content, closing.slice(2, -1), config, false, accum);
27
28
  return `\0${accum.length - 2}e\x7F`;
28
29
  }), config, accum);
29
30
  }
@@ -37,22 +38,21 @@ class CharinsertLineToken extends index_2.SingleLineToken {
37
38
  const errors = [], str = this.childNodes.map(child => child.type === 'text'
38
39
  ? child.data
39
40
  : `<nowiki>${child.innerText.replace(/[^+]/gu, '_')}</nowiki>`).join(''), re = /(?:^|\s)\S+/gu, rect = new rect_1.BoundingRect(this, start), msg = index_1.default.msg('invalid-content', 'charinsert');
40
- let mt = re.exec(str);
41
- while (mt) {
41
+ for (let mt = re.exec(str); mt; mt = re.exec(str)) {
42
42
  const [item] = mt;
43
43
  let i = item.indexOf('+');
44
44
  if (i !== -1 && item.slice(0, i).trim()) {
45
45
  i = item.indexOf('+', i + 1);
46
46
  }
47
- if (i !== -1) {
48
- const e = (0, lint_1.generateForSelf)(this, rect, rule, msg, s), startOffset = mt.index + i, endOffset = mt.index + item.length;
49
- e.endIndex = start + endOffset;
50
- e.endCol = e.startCol + endOffset;
51
- e.startIndex += startOffset;
52
- e.startCol += startOffset;
53
- errors.push(e);
47
+ if (i === -1) {
48
+ continue;
54
49
  }
55
- mt = re.exec(str);
50
+ const e = (0, lint_1.generateForSelf)(this, rect, rule, msg, s), startOffset = mt.index + i, endOffset = mt.index + item.length;
51
+ e.endIndex = start + endOffset;
52
+ e.endCol = e.startCol + endOffset;
53
+ e.startIndex += startOffset;
54
+ e.startCol += startOffset;
55
+ errors.push(e);
56
56
  }
57
57
  return errors;
58
58
  }
@@ -152,14 +152,14 @@ let ExtToken = (() => {
152
152
  // NestedToken 依赖 ExtToken
153
153
  const { NestedToken } = require('../nested');
154
154
  // @ts-expect-error abstract class
155
- innerToken = new NestedToken(inner, /<(option|choicetemplate)(\s[^>]*?)?(?:\/>|>(.*?)<\/(\1)>)/gsu, ['option', 'choicetemplate'], newConfig, accum);
155
+ innerToken = new NestedToken(inner, /<(option|choicetemplate)(?=\s|\/?>)/gu, ['option', 'choicetemplate'], newConfig, accum);
156
156
  break;
157
157
  }
158
158
  case 'combobox': {
159
159
  // NestedToken 依赖 ExtToken
160
160
  const { NestedToken } = require('../nested');
161
161
  // @ts-expect-error abstract class
162
- innerToken = new NestedToken(inner, /<(combooption)(\s[^>]*?)?(?:\/>|>(.*?)<\/(combooption\s*)>)/gisu, ['combooption'], newConfig, accum);
162
+ innerToken = new NestedToken(inner, /<(combooption)(?=\s|\/?>)/gu, ['combooption'], newConfig, accum);
163
163
  break;
164
164
  }
165
165
  case 'gallery': {
@@ -22,19 +22,49 @@ class TranslateToken extends index_2.TagPairToken {
22
22
  */
23
23
  constructor(attr, inner, config, accum = []) {
24
24
  const attrToken = new syntax_1.SyntaxToken(attr, 'translate-attr', config, accum);
25
- inner = inner?.replace(/<tvar(\|[^>]+)>(.*?)<\/>/gsu, (_, p1, p2) => {
26
- // @ts-expect-error abstract class
27
- new tvar_1.TvarToken('tvar', p1, false, config, accum);
28
- // @ts-expect-error abstract class
29
- new tvar_1.TvarToken('', '', true, config, accum);
30
- return `\0${accum.length - 2}n\x7F${p2}\0${accum.length}n\x7F`;
31
- }).replace(/<(tvar)(\s+name\s*=(?:\s*(?:(["']).*?\3|[^"'\s>]+))?\s*)>(.*?)<\/(tvar)(\s*)>/gisu, (_, p1, p2, __, p3, p4, p5) => {
32
- // @ts-expect-error abstract class
33
- new tvar_1.TvarToken(p1, p2, false, config, accum);
34
- // @ts-expect-error abstract class
35
- new tvar_1.TvarToken(p4, p5, true, config, accum);
36
- return `\0${accum.length - 2}n\x7F${p3}\0${accum.length}n\x7F`;
37
- });
25
+ if (inner) {
26
+ let result = '', lastIndex = 0, i, j, k, index;
27
+ const update = /** @ignore */ () => {
28
+ i = inner.indexOf('<tvar|', lastIndex);
29
+ index = i + 6;
30
+ j = inner.indexOf('>', index);
31
+ k = inner.indexOf('</>', j + 1);
32
+ return [i, j === index ? -1 : j, k];
33
+ };
34
+ for ([i, j, k] = update(); i !== -1 && j !== -1 && k !== -1; [i, j, k] = update()) {
35
+ result += inner.slice(lastIndex, i);
36
+ lastIndex = k + 3;
37
+ // @ts-expect-error abstract class
38
+ new tvar_1.TvarToken('tvar', inner.slice(index - 1, j), false, config, accum);
39
+ // @ts-expect-error abstract class
40
+ new tvar_1.TvarToken('', '', true, config, accum);
41
+ result += `\0${accum.length - 2}n\x7F${inner.slice(j + 1, k)}\0${accum.length}n\x7F`;
42
+ }
43
+ inner = result + inner.slice(lastIndex);
44
+ result = '';
45
+ lastIndex = 0;
46
+ const start = /<(tvar)(\s+name\s*=)/giu, end = /<\/(tvar)(\s*)>/giu;
47
+ for (let mt = start.exec(inner); mt; mt = start.exec(inner)) {
48
+ i = inner.indexOf('>', start.lastIndex);
49
+ const name = inner.slice(start.lastIndex, i);
50
+ if (i === -1 || !/^(?:\s*(?:(["']).*?\1|[^"'\s>]+))?\s*$/su.test(name)) {
51
+ break;
52
+ }
53
+ end.lastIndex = i + 1;
54
+ const mt2 = end.exec(inner);
55
+ if (!mt2) {
56
+ break;
57
+ }
58
+ // @ts-expect-error abstract class
59
+ new tvar_1.TvarToken(mt[1], mt[2] + name, false, config, accum);
60
+ // @ts-expect-error abstract class
61
+ new tvar_1.TvarToken(mt2[1], mt2[2], true, config, accum);
62
+ result += `${inner.slice(lastIndex, mt.index)}\0${accum.length - 2}n\x7F${inner.slice(i + 1, mt2.index)}\0${accum.length}n\x7F`;
63
+ ({ lastIndex } = end);
64
+ start.lastIndex = lastIndex;
65
+ }
66
+ inner = result + inner.slice(lastIndex);
67
+ }
38
68
  const innerToken = new index_1.Token(inner, config, accum);
39
69
  innerToken.type = 'translate-inner';
40
70
  super('translate', attrToken, innerToken, 'translate', config, accum);
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.safePatternReplace = exports.safeStringReplace = void 0;
4
+ /**
5
+ * 使用`String#indexOf`避免正则表达式的回溯
6
+ * @param wikitext 源文本
7
+ * @param start 起始字符串
8
+ * @param end 结束字符串
9
+ * @param insensitive 是否忽略大小写
10
+ * @param matched 匹配到的内容处理函数
11
+ * @param unmatched 未匹配到的内容处理函数
12
+ */
13
+ const safeStringReplace = (wikitext, start, end, insensitive, matched, unmatched = (str) => str) => {
14
+ const text = insensitive ? wikitext.toLowerCase() : wikitext, lStart = start.length, lEnd = end.length;
15
+ let result = '', lastIndex = 0, i, j, index;
16
+ const update = /** @ignore */ () => {
17
+ i = text.indexOf(start, lastIndex);
18
+ index = i + lStart;
19
+ return [i, text.indexOf(end, index), index];
20
+ };
21
+ for ([i, j, index] = update(); i !== -1 && j !== -1; [i, j, index] = update()) {
22
+ result += unmatched(wikitext.slice(lastIndex, i));
23
+ lastIndex = j + lEnd;
24
+ result += matched(wikitext.slice(index, j), insensitive ? wikitext.slice(i, index) : start, insensitive ? wikitext.slice(j, lastIndex) : end);
25
+ }
26
+ return result + unmatched(wikitext.slice(lastIndex));
27
+ };
28
+ exports.safeStringReplace = safeStringReplace;
29
+ /**
30
+ * 使用`RegExp#exec`避免正则表达式的回溯
31
+ * @param wikitext 源文本
32
+ * @param start 起始正则表达式
33
+ * @param end 结束字符串
34
+ * @param matched 匹配到的内容处理函数
35
+ */
36
+ const safePatternReplace = (wikitext, start, end, matched) => {
37
+ let result = '', lastIndex = 0;
38
+ start.lastIndex = 0;
39
+ for (let mt = start.exec(wikitext); mt; mt = start.exec(wikitext)) {
40
+ const i = wikitext.indexOf(end, start.lastIndex);
41
+ if (i === -1) {
42
+ break;
43
+ }
44
+ const { index } = mt;
45
+ result += wikitext.slice(lastIndex, index);
46
+ lastIndex = i + end.length;
47
+ const r = matched(wikitext.slice(start.lastIndex, i), mt);
48
+ if (r === false) {
49
+ ({ lastIndex } = start);
50
+ result += wikitext.slice(index, lastIndex);
51
+ }
52
+ else {
53
+ result += r;
54
+ start.lastIndex = lastIndex;
55
+ }
56
+ }
57
+ return result + wikitext.slice(lastIndex);
58
+ };
59
+ exports.safePatternReplace = safePatternReplace;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wikilint",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "A Node.js linter for MediaWiki markup",
5
5
  "keywords": [
6
6
  "mediawiki",
@@ -75,7 +75,7 @@
75
75
  },
76
76
  "peerDependencies": {
77
77
  "mathoid-texvcjs": ">=0.4.0",
78
- "stylelint": ">=16.0.0",
78
+ "stylelint-config-recommended": ">=14.0.0",
79
79
  "vscode-css-languageservice": ">=3.0.0",
80
80
  "vscode-html-languageservice": ">=3.1.0",
81
81
  "vscode-json-languageservice": ">=3.2.0"
@@ -84,7 +84,7 @@
84
84
  "mathoid-texvcjs": {
85
85
  "optional": true
86
86
  },
87
- "stylelint": {
87
+ "stylelint-config-recommended": {
88
88
  "optional": true
89
89
  },
90
90
  "vscode-css-languageservice": {
@@ -100,7 +100,7 @@
100
100
  "dependencies": {
101
101
  "@bhsd/cm-util": "^2.4.2",
102
102
  "@bhsd/common": "^4.1.0",
103
- "@bhsd/stylelint-util": "^1.1.0",
103
+ "@bhsd/stylelint-util": "^2.0.0",
104
104
  "culori": "^4.0.2",
105
105
  "entities": "^8.1.0",
106
106
  "vscode-languageserver-types": "^3.18.5"
@@ -117,6 +117,7 @@
117
117
  "eslint": "^10.14.0",
118
118
  "mathoid-texvcjs": "^0.6.0",
119
119
  "stylelint": "^17.16.0",
120
+ "stylelint-config-recommended": "^18.0.0",
120
121
  "typescript": "^6.0.3",
121
122
  "vscode-css-languageservice": "^6.3.10",
122
123
  "vscode-html-languageservice": "^5.6.2",