wikilint 2.18.2 → 2.18.3

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.
Files changed (43) hide show
  1. package/README.md +1 -1
  2. package/bin/cli.js +1 -1
  3. package/config/minimum.json +7 -0
  4. package/dist/base.d.mts +2 -0
  5. package/dist/base.d.ts +2 -0
  6. package/dist/base.js +1 -0
  7. package/dist/base.mjs +2 -1
  8. package/dist/bin/config.js +3 -2
  9. package/dist/index.d.ts +3 -1
  10. package/dist/index.js +38 -28
  11. package/dist/lib/lsp.d.ts +1 -0
  12. package/dist/lib/lsp.js +15 -9
  13. package/dist/lib/title.d.ts +18 -4
  14. package/dist/lib/title.js +12 -4
  15. package/dist/parser/braces.js +29 -15
  16. package/dist/parser/commentAndExt.js +5 -16
  17. package/dist/parser/links.js +1 -1
  18. package/dist/parser/redirect.js +1 -3
  19. package/dist/src/converter.js +1 -1
  20. package/dist/src/gallery.js +1 -1
  21. package/dist/src/heading.js +3 -3
  22. package/dist/src/imageParameter.js +9 -9
  23. package/dist/src/imagemap.js +3 -2
  24. package/dist/src/index.d.ts +1 -1
  25. package/dist/src/index.js +7 -5
  26. package/dist/src/link/base.js +1 -1
  27. package/dist/src/link/file.d.ts +2 -0
  28. package/dist/src/link/file.js +15 -13
  29. package/dist/src/link/galleryImage.js +1 -1
  30. package/dist/src/link/redirectTarget.js +1 -2
  31. package/dist/src/magicLink.js +1 -1
  32. package/dist/src/nested.js +5 -5
  33. package/dist/src/nowiki/index.js +3 -2
  34. package/dist/src/redirect.js +1 -2
  35. package/dist/src/syntax.d.ts +4 -2
  36. package/dist/src/syntax.js +4 -2
  37. package/dist/src/table/base.js +1 -1
  38. package/dist/src/table/index.js +2 -5
  39. package/dist/src/table/trBase.js +3 -1
  40. package/dist/src/transclude.js +3 -3
  41. package/dist/util/debug.js +11 -1
  42. package/dist/util/string.js +3 -4
  43. package/package.json +2 -2
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  # WikiLint
8
8
 
9
- This is a minimal version of [WikiParser-Node](https://www.npmjs.com/package/wikiparser-node) customized for [eslint-plugin-wikitext](https://www.npmjs.com/package/eslint-plugin-wikitext).
9
+ This is a minimal version of [WikiParser-Node](https://www.npmjs.com/package/wikiparser-node). The [WikiParser Language Server](https://marketplace.visualstudio.com/items?itemName=Bhsd.vscode-extension-wikiparser) VSCode extension is written based on this package.
10
10
 
11
11
  You can also directly lint Wikitext articles in the command line using this package:
12
12
 
package/bin/cli.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
- require('../dist/bin/cli.js');
3
+ require('../dist/bin/cli.js'); // eslint-disable-line n/no-missing-require
@@ -98,6 +98,13 @@
98
98
  ],
99
99
  "parserFunction": [
100
100
  {
101
+ "msgnw": "msgnw",
102
+ "pageid": "pageid",
103
+ "articlepath": "articlepath",
104
+ "server": "server",
105
+ "servername": "servername",
106
+ "scriptpath": "scriptpath",
107
+ "stylepath": "stylepath",
101
108
  "#language": "language",
102
109
  "#special": "special",
103
110
  "#speciale": "speciale",
package/dist/base.d.mts CHANGED
@@ -41,6 +41,7 @@ export declare const stages: {
41
41
  list: number;
42
42
  dd: number;
43
43
  converter: number;
44
+ 'list-range': number;
44
45
  };
45
46
  export type Stage = keyof typeof stages;
46
47
  export declare const rules: readonly ["bold-header", "format-leakage", "fostered-content", "h1", "illegal-attr", "insecure-style", "invalid-gallery", "invalid-imagemap", "invalid-invoke", "invalid-isbn", "lonely-apos", "lonely-bracket", "lonely-http", "nested-link", "no-arg", "no-duplicate", "no-ignored", "obsolete-attr", "obsolete-tag", "parsing-order", "pipe-like", "table-layout", "tag-like", "unbalanced-header", "unclosed-comment", "unclosed-quote", "unclosed-table", "unescaped", "unknown-page", "unmatched-tag", "unterminated-url", "url-encoding", "var-anchor", "void-ext", "invalid-css"];
@@ -271,6 +272,7 @@ export interface Parser {
271
272
  *
272
273
  * 创建语言服务
273
274
  * @param uri document URI / 文档标识
275
+ * @since v1.16.1
274
276
  */
275
277
  createLanguageService(uri: object): LanguageService;
276
278
  }
package/dist/base.d.ts CHANGED
@@ -41,6 +41,7 @@ export declare const stages: {
41
41
  list: number;
42
42
  dd: number;
43
43
  converter: number;
44
+ 'list-range': number;
44
45
  };
45
46
  export type Stage = keyof typeof stages;
46
47
  export declare const rules: readonly ["bold-header", "format-leakage", "fostered-content", "h1", "illegal-attr", "insecure-style", "invalid-gallery", "invalid-imagemap", "invalid-invoke", "invalid-isbn", "lonely-apos", "lonely-bracket", "lonely-http", "nested-link", "no-arg", "no-duplicate", "no-ignored", "obsolete-attr", "obsolete-tag", "parsing-order", "pipe-like", "table-layout", "tag-like", "unbalanced-header", "unclosed-comment", "unclosed-quote", "unclosed-table", "unescaped", "unknown-page", "unmatched-tag", "unterminated-url", "url-encoding", "var-anchor", "void-ext", "invalid-css"];
@@ -271,6 +272,7 @@ export interface Parser {
271
272
  *
272
273
  * 创建语言服务
273
274
  * @param uri document URI / 文档标识
275
+ * @since v1.16.1
274
276
  */
275
277
  createLanguageService(uri: object): LanguageService;
276
278
  }
package/dist/base.js CHANGED
@@ -27,6 +27,7 @@ exports.stages = (() => {
27
27
  list: 10,
28
28
  dd: 10,
29
29
  converter: 11,
30
+ 'list-range': 11,
30
31
  };
31
32
  Object.setPrototypeOf(obj, null);
32
33
  return obj;
package/dist/base.mjs CHANGED
@@ -23,7 +23,8 @@ const stages = /* @__PURE__ */ (() => {
23
23
  "magic-link": 9,
24
24
  list: 10,
25
25
  dd: 10,
26
- converter: 11
26
+ converter: 11,
27
+ "list-range": 11
27
28
  };
28
29
  Object.setPrototypeOf(obj, null);
29
30
  return obj;
@@ -81,8 +81,9 @@ let mwConfig;
81
81
  * @param site site nickname
82
82
  * @param url script path
83
83
  * @param force whether to overwrite the existing configuration
84
+ * @param internal for internal use
84
85
  */
85
- exports.default = async (site, url, force) => {
86
+ exports.default = async (site, url, force, internal) => {
86
87
  if (!site || !url) {
87
88
  console.error('Usage: npx getParserConfig <site> <script path> [force]');
88
89
  process.exit(1);
@@ -133,7 +134,7 @@ exports.default = async (site, url, force) => {
133
134
  if (force || !fs_1.default.existsSync(file)) {
134
135
  fs_1.default.writeFileSync(file, `${JSON.stringify(config, null, '\t')}\n`);
135
136
  }
136
- else {
137
+ else if (!internal) {
137
138
  assert_1.default.deepStrictEqual(arrToObj(require(file)), arrToObj(config));
138
139
  }
139
140
  return config;
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import type { Config, LintError, TokenTypes, Parser as ParserBase, Stage } from './base';
2
- import type { Title } from './lib/title';
2
+ import type { Title, TitleOptions } from './lib/title';
3
3
  import type { LanguageService, QuickFixData } from './lib/lsp';
4
4
  import type { Token } from './internal';
5
5
  declare interface Parser extends ParserBase {
6
+ /** @since v1.5.1 */
6
7
  rules: readonly LintError.Rule[];
7
8
  /**
8
9
  * Normalize page title
@@ -19,6 +20,7 @@ declare interface Parser extends ParserBase {
19
20
  *
20
21
  * 创建语言服务
21
22
  * @param uri document URI / 文档标识
23
+ * @since v1.16.1
22
24
  */
23
25
  createLanguageService(uri: object): LanguageService;
24
26
  }
package/dist/index.js CHANGED
@@ -25,6 +25,9 @@ const Parser = {
25
25
  config: 'default',
26
26
  i18n: undefined,
27
27
  rules: base_1.rules,
28
+ /* NOT FOR BROWSER ONLY */
29
+ lintCSS: true,
30
+ /* NOT FOR BROWSER ONLY END */
28
31
  /** @implements */
29
32
  getConfig(config) {
30
33
  /* NOT FOR BROWSER ONLY */
@@ -39,7 +42,7 @@ const Parser = {
39
42
  return this.getConfig();
40
43
  }
41
44
  /* NOT FOR BROWSER ONLY END */
42
- const parserConfig = config ?? this.config, { doubleUnderscore } = parserConfig;
45
+ const parserConfig = config ?? this.config, { doubleUnderscore, } = parserConfig;
43
46
  for (let i = 0; i < 2; i++) {
44
47
  if (doubleUnderscore.length > i + 2 && doubleUnderscore[i].length === 0) {
45
48
  doubleUnderscore[i] = Object.keys(doubleUnderscore[i + 2]);
@@ -61,11 +64,11 @@ const Parser = {
61
64
  return msg && (this.i18n?.[msg] ?? msg).replace('$1', this.msg(arg));
62
65
  },
63
66
  /** @implements */
64
- normalizeTitle(title, defaultNs = 0, include, config = Parser.getConfig(), temporary = false, halfParsed, decode = false, selfLink = false) {
67
+ normalizeTitle(title, defaultNs = 0, include, config = Parser.getConfig(), opt) {
65
68
  const { Title } = require('./lib/title');
66
69
  let titleObj;
67
- if (halfParsed) {
68
- titleObj = new Title(title, defaultNs, config, temporary, decode, selfLink);
70
+ if (opt?.halfParsed) {
71
+ titleObj = new Title(title, defaultNs, config, opt);
69
72
  }
70
73
  else {
71
74
  const { Token } = require('./src/index');
@@ -73,7 +76,7 @@ const Parser = {
73
76
  const root = new Token(title, config);
74
77
  root.type = 'root';
75
78
  root.parseOnce(0, include).parseOnce();
76
- const t = new Title(root.toString(), defaultNs, config, temporary, decode, selfLink);
79
+ const t = new Title(root.toString(), defaultNs, config, opt);
77
80
  for (const key of ['main', 'fragment']) {
78
81
  const str = t[key];
79
82
  if (str?.includes('\0')) {
@@ -94,8 +97,9 @@ const Parser = {
94
97
  /** @implements */
95
98
  parse(wikitext, include, maxStage = constants_1.MAX_STAGE, config = Parser.getConfig()) {
96
99
  wikitext = (0, string_1.tidy)(wikitext);
100
+ let types;
97
101
  if (typeof maxStage !== 'number') {
98
- const types = Array.isArray(maxStage) ? maxStage : [maxStage];
102
+ types = Array.isArray(maxStage) ? maxStage : [maxStage];
99
103
  maxStage = Math.max(...types.map(t => base_1.stages[t] || constants_1.MAX_STAGE));
100
104
  }
101
105
  const { Token } = require('./src/index');
@@ -132,28 +136,34 @@ const Parser = {
132
136
  const token = new Token((0, string_1.tidy)(wikitext), config);
133
137
  token.type = 'root';
134
138
  let i = 0;
135
- await new Promise(resolve => {
136
- const /** @ignore */ check = () => {
137
- if (watch() === wikitext) {
138
- i++;
139
- set(parseOnce, 0);
140
- }
141
- else {
142
- resolve();
143
- }
144
- },
145
- /** @ignore */ parseOnce = () => {
146
- if (i === constants_1.MAX_STAGE + 1) {
147
- token.afterBuild();
148
- resolve();
149
- }
150
- else {
151
- token[i === constants_1.MAX_STAGE ? 'build' : 'parseOnce'](i, include);
152
- check();
153
- }
154
- };
155
- set(parseOnce, 0);
156
- });
139
+ try {
140
+ await new Promise(resolve => {
141
+ const /** @ignore */ check = () => {
142
+ if (watch() === wikitext) {
143
+ i++;
144
+ set(parseOnce, 0);
145
+ }
146
+ else {
147
+ resolve();
148
+ }
149
+ },
150
+ /** @ignore */ parseOnce = () => {
151
+ if (i === constants_1.MAX_STAGE + 1) {
152
+ token.afterBuild();
153
+ resolve();
154
+ }
155
+ else {
156
+ token[i === constants_1.MAX_STAGE ? 'build' : 'parseOnce'](i, include);
157
+ check();
158
+ }
159
+ };
160
+ set(parseOnce, 0);
161
+ });
162
+ }
163
+ catch (e) /* istanbul ignore next */ {
164
+ debug_1.Shadow.running = running;
165
+ throw e;
166
+ }
157
167
  debug_1.Shadow.running = running;
158
168
  return token;
159
169
  },
package/dist/lib/lsp.d.ts CHANGED
@@ -20,6 +20,7 @@ export declare const isAttr: ({ type, parentNode, length, firstChild }: Token, s
20
20
  /** VSCode-style language service */
21
21
  export declare class LanguageService implements LanguageServiceBase {
22
22
  #private;
23
+ /** @since v1.17.1 */
23
24
  include: boolean;
24
25
  lilypond: string;
25
26
  /** @param uri 任务标识 */
package/dist/lib/lsp.js CHANGED
@@ -23,6 +23,7 @@ const cssRules = {
23
23
  'block-no-empty': null,
24
24
  'property-no-unknown': null,
25
25
  }, jsonSelector = document_1.jsonTags.map(s => `ext-inner#${s}`).join(), scores = new Map();
26
+ let colors;
26
27
  /* NOT FOR BROWSER ONLY END */
27
28
  exports.tasks = new WeakMap();
28
29
  const refTags = new Set(['ref']), referencesTags = new Set(['ref', 'references']), nameAttrs = new Set(['name', 'extends', 'follow']), groupAttrs = new Set(['group']), renameTypes = new Set([
@@ -40,6 +41,7 @@ const refTags = new Set(['ref']), referencesTags = new Set(['ref', 'references']
40
41
  'magic-word-name',
41
42
  ...renameTypes,
42
43
  ]), plainTypes = new Set(['text', 'comment', 'noinclude', 'include']), cssSelector = ['ext', 'html', 'table'].map(s => `${s}-attr#style`).join();
44
+ const getLinkRegex = (0, common_1.getRegex)(protocol => new RegExp(`^(?:${protocol}|//)`, 'iu'));
43
45
  /**
44
46
  * Check if a token is a plain attribute.
45
47
  * @param token
@@ -273,6 +275,7 @@ class LanguageService {
273
275
  #config;
274
276
  #include;
275
277
  #completionConfig;
278
+ /** @since v1.17.1 */
276
279
  include = true;
277
280
  /** @private */
278
281
  config;
@@ -406,11 +409,14 @@ class LanguageService {
406
409
  async provideDocumentColors(rgba, text, hsl = true) {
407
410
  const root = await this.#queue(text);
408
411
  /* NOT FOR BROWSER ONLY */
409
- let colors;
412
+ /* eslint-disable require-atomic-updates */
410
413
  try {
411
- colors = new RegExp(String.raw `\b${Object.keys((await import('color-name')).default).join('|')}\b`, 'giu');
414
+ colors ??= new RegExp(String.raw `\b${Object.keys((await import('color-name')).default).join('|')}\b`, 'giu');
412
415
  }
413
- catch { }
416
+ catch {
417
+ colors = false;
418
+ }
419
+ /* eslint-enable require-atomic-updates */
414
420
  /* NOT FOR BROWSER ONLY END */
415
421
  return root.querySelectorAll('attr-value,parameter-value,arg-default').reverse()
416
422
  .flatMap(token => {
@@ -637,7 +643,7 @@ class LanguageService {
637
643
  if (t === 'magic-word' && n !== 'invoke') {
638
644
  return undefined;
639
645
  }
640
- const key = this.#text.slice(cur.getAbsoluteIndex(), root.indexFromPos(line, character)).trimStart(), [module, func] = t === 'magic-word' ? transclusion.getModule() : [];
646
+ const key = this.#text.slice(cur.getAbsoluteIndex(), root.indexFromPos(line, character)).trimStart(), [mod, func] = t === 'magic-word' ? transclusion.getModule() : [];
641
647
  return key
642
648
  ? getCompletion(root.querySelectorAll('parameter').filter(token => {
643
649
  if (token === parentNode
@@ -650,7 +656,7 @@ class LanguageService {
650
656
  return true;
651
657
  }
652
658
  const [m, f] = token.parentNode.getModule();
653
- return m === module && f === func;
659
+ return m === mod && f === func;
654
660
  }).map(({ name }) => name), 'Variable', key, position, type === 'parameter-value' ? '=' : '')
655
661
  : undefined;
656
662
  /* NOT FOR BROWSER ONLY */
@@ -893,7 +899,7 @@ class LanguageService {
893
899
  */
894
900
  async provideLinks(text) {
895
901
  this.config ??= index_1.default.getConfig();
896
- const { articlePath, protocol } = this.config, absolute = articlePath?.includes('//'), protocolRegex = new RegExp(`^(?:${protocol}|//)`, 'iu');
902
+ const { articlePath, protocol } = this.config, absolute = articlePath?.includes('//'), protocolRegex = getLinkRegex(protocol);
897
903
  return (await this.#queue(text))
898
904
  .querySelectorAll(`magic-link,ext-link-url,free-ext-link,attr-value,image-parameter#link${absolute ? ',link-target,template-name,invoke-module,magic-word#filepath,magic-word#widget' : ''}`)
899
905
  .reverse()
@@ -953,8 +959,8 @@ class LanguageService {
953
959
  ns = name === 'filepath' ? 6 : 274;
954
960
  // no default
955
961
  }
956
- const title = index_1.default.normalizeTitle(target, ns, false, this.config, true);
957
- /* istanbul ignore if */
962
+ const title = index_1.default
963
+ .normalizeTitle(target, ns, false, this.config, { temporary: true });
958
964
  if (!title.valid) {
959
965
  return false;
960
966
  }
@@ -1331,7 +1337,7 @@ class LanguageService {
1331
1337
  this.config = index_1.default.getConfig(config);
1332
1338
  }
1333
1339
  catch {
1334
- this.config = index_1.default.getConfig(await (0, config_1.default)(site, `${mt[0]}/w`));
1340
+ this.config = index_1.default.getConfig(await (0, config_1.default)(site, `${mt[0]}/w`, false, true));
1335
1341
  }
1336
1342
  Object.assign(this.config, { articlePath: `${mt[0]}/wiki/` });
1337
1343
  }
@@ -1,4 +1,10 @@
1
1
  import type { Config } from '../base';
2
+ export interface TitleOptions {
3
+ temporary?: boolean | undefined;
4
+ decode?: boolean | undefined;
5
+ selfLink?: boolean | undefined;
6
+ halfParsed?: boolean | undefined;
7
+ }
2
8
  /**
3
9
  * title object of a MediaWiki page
4
10
  *
@@ -18,21 +24,28 @@ export declare class Title {
18
24
  get prefix(): string;
19
25
  /** full title / 完整标题 */
20
26
  get title(): string;
21
- /** file extension / 扩展名 */
27
+ /**
28
+ * file extension / 扩展名
29
+ * @since v1.1.0
30
+ */
22
31
  get extension(): string | undefined;
23
32
  /**
24
33
  * @see MediaWikiTitleCodec::splitTitleString
25
34
  *
26
35
  * @param title 标题(含或不含命名空间前缀)
27
36
  * @param defaultNs 命名空间
28
- * @param decode 是否需要解码
29
- * @param selfLink 是否允许selfLink
37
+ * @param config
38
+ * @param opt 选项
39
+ * @param opt.temporary 是否是临时标题
40
+ * @param opt.decode 是否需要解码
41
+ * @param opt.selfLink 是否允许selfLink
30
42
  */
31
- constructor(title: string, defaultNs: number, config: Config, temporary: boolean, decode: boolean, selfLink: boolean);
43
+ constructor(title: string, defaultNs: number, config: Config, { temporary, decode, selfLink }?: TitleOptions);
32
44
  /**
33
45
  * Check if the title is a redirect
34
46
  *
35
47
  * 检测是否是重定向
48
+ * @since v1.12.2
36
49
  */
37
50
  getRedirection(): [boolean, string];
38
51
  /**
@@ -40,6 +53,7 @@ export declare class Title {
40
53
  *
41
54
  * 生成URL
42
55
  * @param articlePath article path / 条目路径
56
+ * @since v1.10.0
43
57
  */
44
58
  getUrl(articlePath?: string): string;
45
59
  }
package/dist/lib/title.js CHANGED
@@ -41,7 +41,10 @@ class Title {
41
41
  get title() {
42
42
  return this.getRedirection()[1];
43
43
  }
44
- /** file extension / 扩展名 */
44
+ /**
45
+ * file extension / 扩展名
46
+ * @since v1.1.0
47
+ */
45
48
  get extension() {
46
49
  const { main } = this, i = main.lastIndexOf('.');
47
50
  return i === -1 ? undefined : main.slice(i + 1).toLowerCase();
@@ -51,10 +54,13 @@ class Title {
51
54
  *
52
55
  * @param title 标题(含或不含命名空间前缀)
53
56
  * @param defaultNs 命名空间
54
- * @param decode 是否需要解码
55
- * @param selfLink 是否允许selfLink
57
+ * @param config
58
+ * @param opt 选项
59
+ * @param opt.temporary 是否是临时标题
60
+ * @param opt.decode 是否需要解码
61
+ * @param opt.selfLink 是否允许selfLink
56
62
  */
57
- constructor(title, defaultNs, config, temporary, decode, selfLink) {
63
+ constructor(title, defaultNs, config, { temporary, decode, selfLink } = {}) {
58
64
  const subpage = title.trim().startsWith('../');
59
65
  if (decode && title.includes('%')) {
60
66
  try {
@@ -116,6 +122,7 @@ class Title {
116
122
  * Check if the title is a redirect
117
123
  *
118
124
  * 检测是否是重定向
125
+ * @since v1.12.2
119
126
  */
120
127
  getRedirection() {
121
128
  // eslint-disable-next-line @typescript-eslint/prefer-destructuring
@@ -133,6 +140,7 @@ class Title {
133
140
  *
134
141
  * 生成URL
135
142
  * @param articlePath article path / 条目路径
143
+ * @since v1.10.0
136
144
  */
137
145
  getUrl(articlePath) {
138
146
  LSP: { // eslint-disable-line no-unused-labels
@@ -1,16 +1,21 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseBraces = void 0;
4
+ const common_1 = require("@bhsd/common");
4
5
  const string_1 = require("../util/string");
5
6
  const heading_1 = require("../src/heading");
6
7
  const transclude_1 = require("../src/transclude");
7
8
  const arg_1 = require("../src/arg");
9
+ /* NOT FOR BROWSER ONLY */
10
+ const v8_1 = require("v8");
11
+ const MAXHEAP = (0, v8_1.getHeapStatistics)().total_available_size * 0.9, MAXITER = MAXHEAP / 2e5;
12
+ /* NOT FOR BROWSER ONLY END */
8
13
  const closes = {
9
14
  '=': String.raw `\n(?!(?:[^\S\n]|\0\d+[cn]\x7F)*\n)`,
10
15
  '{': String.raw `\}{2,}|\|`,
11
16
  '-': String.raw `\}-`,
12
17
  '[': String.raw `\]\]`,
13
- }, openBraces = String.raw `|\{{2,}`, marks = new Map([['!', '!'], ['!!', '+'], ['(!', '{'], ['!)', '}'], ['!-', '-'], ['=', '~'], ['server', 'm']]);
18
+ }, openBraces = String.raw `|\{{2,}`, marks = new Map([['!', '!'], ['!!', '+'], ['(!', '{'], ['!)', '}'], ['!-', '-'], ['=', '~'], ['server', 'm']]), getExecRegex = (0, common_1.getRegex)(s => new RegExp(s, 'gmu'));
14
19
  /**
15
20
  * 获取模板或魔术字对应的字符
16
21
  * @param s 模板或魔术字名
@@ -33,7 +38,8 @@ const getSymbol = (s) => {
33
38
  * @param wikitext
34
39
  * @param config
35
40
  * @param accum
36
- * @throws TranscludeToken.constructor()
41
+ * @throws `RangeError` Maximum iteration exceeded
42
+ * @throws `TranscludeToken.constructor()`
37
43
  */
38
44
  const parseBraces = (wikitext, config, accum) => {
39
45
  const source = String.raw `${config.excludes?.includes('heading') ? '' : String.raw `^((?:\0\d+[cno]\x7F)*)={1,6}|`}\[\[|-\{(?!\{)`, { parserFunction: [, , , subst] } = config, stack = [], linkStack = [];
@@ -42,7 +48,17 @@ const parseBraces = (wikitext, config, accum) => {
42
48
  * @param s 不含内链的字符串
43
49
  */
44
50
  const restore = (s) => s.replace(/\0(\d+)\x7F/gu, (_, p1) => linkStack[p1]);
45
- wikitext = wikitext.replace(/\{\{([^\n{}[]*)\}\}(?!\})|\[\[[^\n[\]{]*\]\]/gu, (m, p1) => {
51
+ /**
52
+ * 填入模板内容
53
+ * @param text wikitext全文
54
+ * @param parts 模板参数
55
+ * @param lastIndex 匹配的起始位置
56
+ * @param index 匹配位置
57
+ */
58
+ const push = (text, parts, lastIndex, index) => {
59
+ parts[parts.length - 1].push(restore(text.slice(lastIndex, index)));
60
+ };
61
+ wikitext = wikitext.replace(/\{\{([^\n{}[]*)\}\}(?!\})|\[\[[^\n[\]{]*\]\]|-\{[^\n{}[]*\}-/gu, (m, p1) => {
46
62
  if (p1 !== undefined) {
47
63
  try {
48
64
  const { length } = accum, parts = p1.split('|');
@@ -65,23 +81,21 @@ const parseBraces = (wikitext, config, accum) => {
65
81
  });
66
82
  const lastBraces = wikitext.lastIndexOf('}}') - wikitext.length;
67
83
  let moreBraces = lastBraces + wikitext.length !== -1;
68
- let regex = new RegExp(source + (moreBraces ? openBraces : ''), 'gmu'), mt = regex.exec(wikitext), lastIndex;
84
+ let regex = getExecRegex(source + (moreBraces ? openBraces : '')), mt = regex.exec(wikitext), iter = 0, lastIndex;
69
85
  while (mt
70
86
  || lastIndex !== undefined && lastIndex <= wikitext.length
71
87
  && stack[stack.length - 1]?.[0]?.startsWith('=')) {
88
+ /* NOT FOR BROWSER ONLY */
89
+ if (iter++ > MAXITER && process.memoryUsage().heapUsed > MAXHEAP) {
90
+ throw new RangeError('Maximum iteration exceeded');
91
+ }
92
+ /* NOT FOR BROWSER ONLY END */
72
93
  if (mt?.[1]) {
73
94
  const [, { length }] = mt;
74
95
  mt[0] = mt[0].slice(length);
75
96
  mt.index += length;
76
97
  }
77
98
  const { 0: syntax, index: curIndex } = mt ?? { 0: '\n', index: wikitext.length }, top = stack.pop() ?? {}, { 0: open, index, parts, findEqual: topFindEqual, pos: topPos } = top, innerEqual = syntax === '=' && topFindEqual;
78
- /**
79
- * 填入模板内容
80
- * @param text wikitext全文
81
- */
82
- const push = (text) => {
83
- parts[parts.length - 1].push(restore(text.slice(topPos, curIndex)));
84
- };
85
99
  if (syntax === ']]' || syntax === '}-') { // 情形1:闭合内链或转换
86
100
  lastIndex = curIndex + 2;
87
101
  }
@@ -102,7 +116,7 @@ const parseBraces = (wikitext, config, accum) => {
102
116
  }
103
117
  else if (syntax === '|' || innerEqual) { // 情形3:模板内部,含行首单个'='
104
118
  lastIndex = curIndex + 1;
105
- push(wikitext);
119
+ push(wikitext, parts, topPos, curIndex);
106
120
  if (syntax === '|') {
107
121
  parts.push([]);
108
122
  }
@@ -113,7 +127,7 @@ const parseBraces = (wikitext, config, accum) => {
113
127
  else if (syntax.startsWith('}}')) { // 情形4:闭合模板
114
128
  const close = syntax.slice(0, Math.min(open.length, 3)), rest = open.length - close.length, { length } = accum;
115
129
  lastIndex = curIndex + close.length; // 这不是最终的lastIndex
116
- push(wikitext);
130
+ push(wikitext, parts, topPos, curIndex);
117
131
  let skip = false, ch = 't';
118
132
  if (close.length === 3) {
119
133
  const argParts = parts.map(part => part.join('=')), str = argParts.length > 1 && (0, string_1.removeComment)(argParts[1]).trim();
@@ -167,9 +181,9 @@ const parseBraces = (wikitext, config, accum) => {
167
181
  curTop = stack[stack.length - 1];
168
182
  }
169
183
  }
170
- regex = new RegExp(source
184
+ regex = getExecRegex(source
171
185
  + (moreBraces ? openBraces : '')
172
- + (curTop ? `|${closes[curTop[0][0]]}${curTop.findEqual ? '|=' : ''}` : ''), 'gmu');
186
+ + (curTop ? `|${closes[curTop[0][0]]}${curTop.findEqual ? '|=' : ''}` : ''));
173
187
  regex.lastIndex = lastIndex;
174
188
  mt = regex.exec(wikitext);
175
189
  }
@@ -1,27 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseCommentAndExt = void 0;
4
+ const common_1 = require("@bhsd/common");
4
5
  const onlyinclude_1 = require("../src/onlyinclude");
5
6
  const noinclude_1 = require("../src/nowiki/noinclude");
6
7
  const include_1 = require("../src/tagPair/include");
7
8
  const ext_1 = require("../src/tagPair/ext");
8
9
  const comment_1 = require("../src/nowiki/comment");
9
- const onlyincludeLeft = '<onlyinclude>', onlyincludeRight = '</onlyinclude>', { length } = onlyincludeLeft, regexInclude = new WeakMap(), regexNoinclude = new WeakMap();
10
- /**
11
- * 获取正则表达式
12
- * @param ext 扩展标签
13
- * @param includeOnly 是否嵌入
14
- */
15
- const getRegex = (ext, includeOnly) => {
16
- const regex = includeOnly ? regexInclude : regexNoinclude;
17
- if (regex.has(ext)) {
18
- return regex.get(ext);
19
- }
10
+ const onlyincludeLeft = '<onlyinclude>', onlyincludeRight = '</onlyinclude>', { length } = onlyincludeLeft, getRegex = [false, true].map(includeOnly => {
20
11
  const noincludeRegex = includeOnly ? 'includeonly' : '(?:no|only)include', includeRegex = includeOnly ? 'noinclude' : 'includeonly';
21
- const re = new RegExp(String.raw `<!--[\s\S]*?(?:-->|$)|<${noincludeRegex}(?:\s[^>]*)?/?>|</${noincludeRegex}\s*>|<(${ext.join('|')})(\s[^>]*?)?(?:/>|>([\s\S]*?)</(\1\s*)>)|<(${includeRegex})(\s[^>]*?)?(?:/>|>([\s\S]*?)(?:</(${includeRegex}\s*)>|$))`, 'giu');
22
- regex.set(ext, re);
23
- return re;
24
- };
12
+ return (0, common_1.getObjRegex)(ext => new RegExp(String.raw `<!--[\s\S]*?(?:-->|$)|<${noincludeRegex}(?:\s[^>]*)?/?>|</${noincludeRegex}\s*>|<(${ext.join('|')})(\s[^>]*?)?(?:/>|>([\s\S]*?)</(\1\s*)>)|<(${includeRegex})(\s[^>]*?)?(?:/>|>([\s\S]*?)(?:</(${includeRegex}\s*)>|$))`, 'giu'));
13
+ });
25
14
  /**
26
15
  * 更新`<onlyinclude>`和`</onlyinclude>`的位置
27
16
  * @param wikitext
@@ -67,7 +56,7 @@ const parseCommentAndExt = (wikitext, config, accum, includeOnly) => {
67
56
  return str;
68
57
  }
69
58
  }
70
- return wikitext.replace(getRegex(config.ext, includeOnly), (substr, name, attr, inner, closing, include, includeAttr, includeInner, includeClosing) => {
59
+ return wikitext.replace(getRegex[includeOnly ? 1 : 0](config.ext), (substr, name, attr, inner, closing, include, includeAttr, includeInner, includeClosing) => {
71
60
  const l = accum.length;
72
61
  let ch = 'n';
73
62
  if (name) {
@@ -50,7 +50,7 @@ const parseLinks = (wikitext, config, accum, tidy) => {
50
50
  s += `[[${x}`;
51
51
  continue;
52
52
  }
53
- const { ns, valid, } = index_1.default.normalizeTitle(link, 0, false, config, true, true, true, true);
53
+ const { ns, valid, } = index_1.default.normalizeTitle(link, 0, false, config, { halfParsed: true, temporary: true, decode: true, selfLink: true });
54
54
  if (!valid) {
55
55
  s += `[[${x}`;
56
56
  continue;
@@ -16,9 +16,7 @@ const parseRedirect = (text, config, accum) => {
16
16
  config.regexRedirect ??= new RegExp(String.raw `^(\s*)((?:${config.redirection.join('|')})\s*(?::\s*)?)\[\[([^\n|\]]+)(\|.*?)?\]\](\s*)`, 'iu');
17
17
  const mt = config.regexRedirect.exec(text);
18
18
  if (mt
19
- && index_1.default
20
- .normalizeTitle(mt[3], 0, false, config, true, true, true)
21
- .valid) {
19
+ && index_1.default.normalizeTitle(mt[3], 0, false, config, { halfParsed: true, temporary: true, decode: true }).valid) {
22
20
  text = `\0${accum.length}o\x7F${text.slice(mt[0].length)}`;
23
21
  // @ts-expect-error abstract class
24
22
  new redirect_1.RedirectToken(...mt.slice(1), config, accum);
@@ -22,7 +22,7 @@ class ConverterToken extends index_1.Token {
22
22
  constructor(flags, rules, config, accum = []) {
23
23
  super(undefined, config, accum);
24
24
  // @ts-expect-error abstract class
25
- this.append(new converterFlags_1.ConverterFlagsToken(flags, config, accum));
25
+ this.insertAt(new converterFlags_1.ConverterFlagsToken(flags, config, accum));
26
26
  const [firstRule] = rules, hasColon = firstRule.includes(':'),
27
27
  // @ts-expect-error abstract class
28
28
  firstRuleToken = new converterRule_1.ConverterRuleToken(firstRule, hasColon, config, accum);
@@ -44,7 +44,7 @@ class GalleryToken extends index_2.Token {
44
44
  * @param file 文件名
45
45
  */
46
46
  #checkFile(file) {
47
- return this.normalizeTitle(file, 6, true, true, true).valid;
47
+ return this.normalizeTitle(file, 6, { halfParsed: true, temporary: true, decode: true }).valid;
48
48
  }
49
49
  /** @private */
50
50
  toString(skip) {
@@ -35,7 +35,7 @@ class HeadingToken extends index_2.Token {
35
35
  const token = new index_2.Token(input[0], config, accum);
36
36
  token.type = 'heading-title';
37
37
  token.setAttribute('stage', 2);
38
- const trail = new syntax_1.SyntaxToken(input[1], /^\s*$/u, 'heading-trail', config, accum, {});
38
+ const trail = new syntax_1.SyntaxToken(input[1], 'heading-trail', config, accum);
39
39
  this.append(token, trail);
40
40
  }
41
41
  /** 标题格式的等号 */
@@ -102,7 +102,7 @@ class HeadingToken extends index_2.Token {
102
102
  left: rect.left + level,
103
103
  }, 'format-leakage', index_1.default.msg('unbalanced $1 in a section header', 'bold apostrophes')), end = start + level + innerStr.length;
104
104
  if (rootStr.slice(e.endIndex, end).trim()) {
105
- e.suggestions = [{ desc: 'close', range: [end, end], text: "'''" }];
105
+ e.suggestions = [{ desc: 'close', range: [end, end], text: `'''` }];
106
106
  }
107
107
  else {
108
108
  e.fix = { desc: 'remove', range: [e.startIndex, e.endIndex], text: '' };
@@ -112,7 +112,7 @@ class HeadingToken extends index_2.Token {
112
112
  if (italicQuotes.length % 2) {
113
113
  const e = (0, lint_1.generateForChild)(italicQuotes[italicQuotes.length - 1], { start: start + level }, 'format-leakage', index_1.default.msg('unbalanced $1 in a section header', 'italic apostrophes')), end = start + level + innerStr.length;
114
114
  e.fix = rootStr.slice(e.endIndex, end).trim()
115
- ? { desc: 'close', range: [end, end], text: "''" }
115
+ ? { desc: 'close', range: [end, end], text: `''` }
116
116
  : { desc: 'remove', range: [e.startIndex, e.endIndex], text: '' };
117
117
  errors.push(e);
118
118
  }
@@ -4,10 +4,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.ImageParameterToken = exports.galleryParams = void 0;
7
+ const common_1 = require("@bhsd/common");
7
8
  const string_1 = require("../util/string");
8
9
  const lint_1 = require("../util/lint");
9
10
  const index_1 = __importDefault(require("../index"));
10
11
  const index_2 = require("./index");
12
+ const getUrlLikeRegex = (0, common_1.getRegex)(protocol => new RegExp(String.raw `^(?:${protocol}|//|\0\d+m\x7F)`, 'iu'));
13
+ const getUrlRegex = (0, common_1.getRegex)(protocol => new RegExp(String.raw `^(?:(?:${protocol}|//)${string_1.extUrlCharFirst}|\0\d+m\x7F)${string_1.extUrlChar}$`, 'iu'));
14
+ const getSyntaxRegex = (0, common_1.getRegex)(syntax => new RegExp(String.raw `^(\s*(?!\s))${syntax.replace('$1', '(.*)')}${syntax.endsWith('$1') ? '(?=$|\n)' : ''}(\s*)$`, 'u'));
11
15
  exports.galleryParams = new Set(['alt', 'link', 'lang', 'page', 'caption']);
12
16
  function validate(key, val, config, halfParsed, ext) {
13
17
  val = val.trim();
@@ -19,15 +23,13 @@ function validate(key, val, config, halfParsed, ext) {
19
23
  if (!value) {
20
24
  return val;
21
25
  }
22
- const re1 = new RegExp(String.raw `^(?:${config.protocol}|//|\0\d+m\x7F)`, 'iu');
23
- const re2 = new RegExp(String.raw `^(?:(?:${config.protocol}|//)${string_1.extUrlCharFirst}|\0\d+m\x7F)${string_1.extUrlChar}$`, 'iu');
24
- if (re1.test(value)) {
25
- return re2.test(value) && val;
26
+ else if (getUrlLikeRegex(config.protocol).test(value)) {
27
+ return getUrlRegex(config.protocol).test(value) && val;
26
28
  }
27
29
  else if (value.startsWith('[[') && value.endsWith(']]')) {
28
30
  value = value.slice(2, -2);
29
31
  }
30
- const title = index_1.default.normalizeTitle(value, 0, false, config, false, halfParsed, true, true);
32
+ const title = index_1.default.normalizeTitle(value, 0, false, config, { halfParsed, decode: true, selfLink: true });
31
33
  return title.valid && title;
32
34
  }
33
35
  case 'lang':
@@ -59,10 +61,8 @@ class ImageParameterToken extends index_2.Token {
59
61
  /** @param str 图片参数 */
60
62
  constructor(str, extension, config, accum) {
61
63
  let mt;
62
- const regexes = Object.entries(config.img).map(([syntax, param]) => {
63
- const re = new RegExp(String.raw `^(\s*(?!\s))${syntax.replace('$1', '(.*)')}${syntax.endsWith('$1') ? '(?=$|\n)' : ''}(\s*)$`, 'u');
64
- return [syntax, param, re];
65
- }), param = regexes.find(([, key, regex]) => {
64
+ const regexes = Object.entries(config.img)
65
+ .map(([syntax, param]) => [syntax, param, getSyntaxRegex(syntax)]), param = regexes.find(([, key, regex]) => {
66
66
  mt = regex.exec(str);
67
67
  return mt
68
68
  && (mt.length !== 4
@@ -38,7 +38,7 @@ class ImagemapToken extends index_2.Token {
38
38
  //
39
39
  }
40
40
  else if (first) {
41
- const pipe = line.indexOf('|'), file = pipe === -1 ? line : line.slice(0, pipe), { valid, ns, } = this.normalizeTitle(file, 0, true, true);
41
+ const pipe = line.indexOf('|'), file = pipe === -1 ? line : line.slice(0, pipe), { valid, ns, } = this.normalizeTitle(file, 0, { halfParsed: true, temporary: true });
42
42
  if (valid
43
43
  && ns === 6) {
44
44
  // @ts-expect-error abstract class
@@ -59,7 +59,8 @@ class ImagemapToken extends index_2.Token {
59
59
  const i = line.indexOf('['), substr = line.slice(i), mtIn = /^\[\[([^|]+)(?:\|([^\]]+))?\]\][\w\s]*$/u
60
60
  .exec(substr);
61
61
  if (mtIn) {
62
- if (this.normalizeTitle(mtIn[1], 0, true, true, false, true).valid) {
62
+ if (this.normalizeTitle(mtIn[1], 0, { halfParsed: true, temporary: true, selfLink: true })
63
+ .valid) {
63
64
  // @ts-expect-error abstract class
64
65
  super.insertAt(new imagemapLink_1.ImagemapLinkToken(line.slice(0, i), mtIn.slice(1), substr.slice(substr.indexOf(']]') + 2), config, accum));
65
66
  continue;
@@ -2,7 +2,7 @@ import Parser from '../index';
2
2
  import { AstElement } from '../lib/element';
3
3
  import { AstText } from '../lib/text';
4
4
  import type { LintError, TokenTypes } from '../base';
5
- import type { Title } from '../lib/title';
5
+ import type { Title, TitleOptions } from '../lib/title';
6
6
  import type { AstNodes } from '../internal';
7
7
  /**
8
8
  * base class for all tokens
package/dist/src/index.js CHANGED
@@ -49,6 +49,7 @@ exports.Token = void 0;
49
49
  const string_1 = require("../util/string");
50
50
  const constants_1 = require("../util/constants");
51
51
  const lint_1 = require("../util/lint");
52
+ const debug_1 = require("../util/debug");
52
53
  const index_1 = __importDefault(require("../index"));
53
54
  const element_1 = require("../lib/element");
54
55
  const text_1 = require("../lib/text");
@@ -168,7 +169,7 @@ class Token extends element_1.AstElement {
168
169
  this.#stage = constants_1.MAX_STAGE;
169
170
  const { length, firstChild } = this, str = firstChild?.toString();
170
171
  if (length === 1 && firstChild.type === 'text' && str.includes('\0')) {
171
- this.replaceChildren(...this.buildFromStr(str));
172
+ (0, debug_1.setChildNodes)(this, 0, 1, this.buildFromStr(str));
172
173
  this.normalize();
173
174
  if (this.type === 'root') {
174
175
  for (const token of this.#accum) {
@@ -317,6 +318,8 @@ class Token extends element_1.AstElement {
317
318
  return this.#accum;
318
319
  case 'built':
319
320
  return this.#built;
321
+ case 'stage':
322
+ return this.#stage;
320
323
  default:
321
324
  return super.getAttribute(key);
322
325
  }
@@ -344,9 +347,8 @@ class Token extends element_1.AstElement {
344
347
  return token;
345
348
  }
346
349
  /** @private */
347
- normalizeTitle(title, defaultNs = 0, temporary, halfParsed, decode, selfLink) {
348
- return index_1.default
349
- .normalizeTitle(title, defaultNs, this.#include, this.#config, temporary, halfParsed, decode, selfLink);
350
+ normalizeTitle(title, defaultNs = 0, opt) {
351
+ return index_1.default.normalizeTitle(title, defaultNs, this.#include, this.#config, opt);
350
352
  }
351
353
  /** @private */
352
354
  lint(start = this.getAbsoluteIndex(), re) {
@@ -423,7 +425,7 @@ class Token extends element_1.AstElement {
423
425
  });
424
426
  /* NOT FOR BROWSER ONLY */
425
427
  }
426
- else if ((0, lsp_1.isAttr)(this, true)) {
428
+ else if (index_1.default.lintCSS && (0, lsp_1.isAttr)(this, true)) {
427
429
  const root = this.getRootNode(), textDoc = new document_1.EmbeddedCSSDocument(root, this);
428
430
  errors.push(...document_1.cssLSP.doValidation(textDoc, textDoc.styleSheet)
429
431
  .filter(({ code }) => code !== 'css-ruleorselectorexpected')
@@ -132,7 +132,7 @@ class LinkBaseToken extends index_2.Token {
132
132
  }
133
133
  /** @private */
134
134
  getTitle(temporary, halfParsed) {
135
- return this.normalizeTitle(this.firstChild.toString(true), 0, temporary, halfParsed, true, true);
135
+ return this.normalizeTitle(this.firstChild.toString(true), 0, { halfParsed, temporary, decode: true, selfLink: true });
136
136
  }
137
137
  }
138
138
  exports.LinkBaseToken = LinkBaseToken;
@@ -13,6 +13,8 @@ export declare abstract class FileToken extends LinkBaseToken {
13
13
  readonly childNodes: readonly [AtomToken, ...ImageParameterToken[]];
14
14
  abstract get lastChild(): AtomToken | ImageParameterToken;
15
15
  get type(): 'file' | 'gallery-image' | 'imagemap-image';
16
+ /** file extension / 扩展名 */
17
+ get extension(): string | undefined;
16
18
  /**
17
19
  * @param link 文件名
18
20
  * @param text 图片参数
@@ -4,7 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.FileToken = void 0;
7
- const string_1 = require("../../util/string");
8
7
  const lint_1 = require("../../util/lint");
9
8
  const rect_1 = require("../../lib/rect");
10
9
  const index_1 = __importDefault(require("../../index"));
@@ -15,24 +14,21 @@ const frame = new Map([
15
14
  ['frameless', 'Frameless'],
16
15
  ['framed', 'Frame'],
17
16
  ['thumbnail', 'Thumb'],
18
- ]), horizAlign = new Set(['left', 'right', 'center', 'none']), vertAlign = new Set(['baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom']);
17
+ ]), horizAlign = new Set(['left', 'right', 'center', 'none']), vertAlign = new Set(['baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom']), extensions = new Set(['tiff', 'tif', 'png', 'gif', 'jpg', 'jpeg', 'webp', 'xcf', 'pdf', 'svg', 'djvu']);
19
18
  /**
20
19
  * a more sophisticated string-explode function
21
- * @param start start syntax of a nested AST node
22
- * @param end end syntax of a nested AST node
23
- * @param separator syntax for explosion
24
20
  * @param str string to be exploded
25
21
  */
26
- const explode = (start, end, separator, str) => {
22
+ const explode = (str) => {
27
23
  if (str === undefined) {
28
24
  return [];
29
25
  }
30
- const regex = new RegExp(`${[start, end, separator].map(string_1.escapeRegExp).join('|')}`, 'gu'), exploded = [];
26
+ const regex = /-\{|\}-|\|/gu, exploded = [];
31
27
  let mt = regex.exec(str), depth = 0, lastIndex = 0;
32
28
  while (mt) {
33
29
  const { 0: match, index } = mt;
34
- if (match !== separator) {
35
- depth += match === start ? 1 : -1;
30
+ if (match !== '|') {
31
+ depth += match === '-{' ? 1 : -1;
36
32
  }
37
33
  else if (depth === 0) {
38
34
  exploded.push(str.slice(lastIndex, index));
@@ -53,6 +49,10 @@ class FileToken extends base_1.LinkBaseToken {
53
49
  get type() {
54
50
  return 'file';
55
51
  }
52
+ /** file extension / 扩展名 */
53
+ get extension() {
54
+ return this.getAttribute('title').extension;
55
+ }
56
56
  /**
57
57
  * @param link 文件名
58
58
  * @param text 图片参数
@@ -61,7 +61,7 @@ class FileToken extends base_1.LinkBaseToken {
61
61
  constructor(link, text, config = index_1.default.getConfig(), accum = [], delimiter = '|') {
62
62
  super(link, undefined, config, accum, delimiter);
63
63
  const { extension } = this.getTitle(true, true);
64
- this.append(...explode('-{', '}-', '|', text).map(
64
+ this.append(...explode(text).map(
65
65
  // @ts-expect-error abstract class
66
66
  (part) => new imageParameter_1.ImageParameterToken(part, extension, config, accum)));
67
67
  }
@@ -92,12 +92,14 @@ class FileToken extends base_1.LinkBaseToken {
92
92
  * 图片参数到语法错误的映射
93
93
  * @param msg 消息键
94
94
  * @param p1 替换$1
95
+ * @param severity 错误等级
95
96
  */
96
- const generate = (msg, p1) => (arg) => {
97
- const e = (0, lint_1.generateForChild)(arg, rect, 'no-duplicate', index_1.default.msg(`${msg} image $1 parameter`, p1));
97
+ const generate = (msg, p1, severity) => (arg) => {
98
+ const e = (0, lint_1.generateForChild)(arg, rect, 'no-duplicate', index_1.default.msg(`${msg} image $1 parameter`, p1), severity);
98
99
  e.suggestions = [{ desc: 'remove', range: [e.startIndex - 1, e.endIndex], text: '' }];
99
100
  return e;
100
101
  };
102
+ const { extension } = this;
101
103
  for (const key of keys) {
102
104
  if (key === 'invalid' || key === 'width' && unscaled) {
103
105
  continue;
@@ -110,7 +112,7 @@ class FileToken extends base_1.LinkBaseToken {
110
112
  ];
111
113
  }
112
114
  if (relevantArgs.length > 1) {
113
- errors.push(...relevantArgs.map(generate('duplicated', key)));
115
+ errors.push(...relevantArgs.map(generate('duplicated', key, key === 'caption' && extension && !extensions.has(extension) ? 'warning' : 'error')));
114
116
  }
115
117
  }
116
118
  if (frameKeys.length > 1) {
@@ -43,7 +43,7 @@ class GalleryImageToken extends file_1.FileToken {
43
43
  /** @private */
44
44
  getTitle(temporary) {
45
45
  const imagemap = this.type === 'imagemap-image';
46
- return this.normalizeTitle(this.firstChild.toString(), imagemap ? 0 : 6, temporary, true, !imagemap);
46
+ return this.normalizeTitle(this.firstChild.toString(), imagemap ? 0 : 6, { halfParsed: true, temporary, decode: !imagemap });
47
47
  }
48
48
  /** @private */
49
49
  getAttribute(key) {
@@ -27,8 +27,7 @@ class RedirectTargetToken extends base_1.LinkBaseToken {
27
27
  }
28
28
  /** @private */
29
29
  getTitle() {
30
- return this
31
- .normalizeTitle(this.firstChild.toString(), 0, false, true, true);
30
+ return this.normalizeTitle(this.firstChild.toString(), 0, { halfParsed: true, decode: true });
32
31
  }
33
32
  /** @private */
34
33
  lint(start = this.getAbsoluteIndex()) {
@@ -95,7 +95,7 @@ class MagicLinkToken extends index_2.Token {
95
95
  if (type === 'magic-link') {
96
96
  if (link.startsWith('ISBN')) {
97
97
  return this
98
- .normalizeTitle(`BookSources/${link.slice(5)}`, -1, true)
98
+ .normalizeTitle(`BookSources/${link.slice(5)}`, -1, { temporary: true })
99
99
  .getUrl(articlePath);
100
100
  }
101
101
  link = link.startsWith('RFC')
@@ -12,7 +12,10 @@ const index_1 = __importDefault(require("../index"));
12
12
  const index_2 = require("./index");
13
13
  const ext_1 = require("./tagPair/ext");
14
14
  const noinclude_1 = require("./nowiki/noinclude");
15
- const childTypes = new Set(['comment', 'include', 'arg', 'template', 'magic-word']);
15
+ const childTypes = new Set(['comment', 'include', 'arg', 'template', 'magic-word']), lintRegex = [false, true].map(article => {
16
+ const noinclude = article ? 'includeonly' : 'noinclude';
17
+ return new RegExp(String.raw `^(?:<${noinclude}(?:\s[^>]*)?/?>|</${noinclude}\s*>)$`, 'iu');
18
+ });
16
19
  /**
17
20
  * extension tag that has a nested structure
18
21
  *
@@ -56,10 +59,7 @@ class NestedToken extends index_2.Token {
56
59
  }
57
60
  /** @private */
58
61
  lint(start = this.getAbsoluteIndex(), re) {
59
- const rect = new rect_1.BoundingRect(this, start), noinclude = this.#regex ? 'includeonly' : 'noinclude';
60
- const regex = typeof this.#regex === 'boolean'
61
- ? new RegExp(String.raw `^(?:<${noinclude}(?:\s[^>]*)?/?>|</${noinclude}\s*>)$`, 'iu')
62
- : /^<!--[\s\S]*-->$/u;
62
+ const rect = new rect_1.BoundingRect(this, start), regex = typeof this.#regex === 'boolean' ? lintRegex[this.#regex ? 1 : 0] : /^<!--[\s\S]*-->$/u;
63
63
  return [
64
64
  ...super.lint(start, re),
65
65
  ...this.childNodes.filter(child => {
@@ -4,9 +4,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.NowikiToken = void 0;
7
+ const common_1 = require("@bhsd/common");
7
8
  const lint_1 = require("../../util/lint");
8
9
  const index_1 = __importDefault(require("../../index"));
9
10
  const base_1 = require("./base");
11
+ const getLintRegex = (0, common_1.getRegex)(name => new RegExp(String.raw `<\s*(?:/\s*)${name === 'nowiki' ? '' : '?'}(${name})\b`, 'giu'));
10
12
  /**
11
13
  * text-only token inside an extension tag
12
14
  *
@@ -24,8 +26,7 @@ class NowikiToken extends base_1.NowikiBaseToken {
24
26
  e.fix = { range: [start, e.endIndex], text: '', desc: 'empty' };
25
27
  return [e];
26
28
  }
27
- const re = new RegExp(String.raw `<\s*(?:/\s*)${name === 'nowiki' ? '' : '?'}(${name})\b`, 'giu');
28
- return super.lint(start, re);
29
+ return super.lint(start, getLintRegex(name));
29
30
  }
30
31
  }
31
32
  exports.NowikiToken = NowikiToken;
@@ -76,8 +76,7 @@ let RedirectToken = (() => {
76
76
  super(undefined, config, accum);
77
77
  this.#pre = pre;
78
78
  this.#post = post;
79
- const pattern = new RegExp(String.raw `^(?:${config.redirection.join('|')})\s*(?::\s*)?$`, 'iu');
80
- this.append(new syntax_1.SyntaxToken(syntax, pattern, 'redirect-syntax', config, accum, {}),
79
+ this.append(new syntax_1.SyntaxToken(syntax, 'redirect-syntax', config, accum),
81
80
  // @ts-expect-error abstract class
82
81
  new redirectTarget_1.RedirectTargetToken(link, text?.slice(1), config, accum));
83
82
  }
@@ -9,7 +9,9 @@ declare type SyntaxTypes = 'heading-trail' | 'magic-word-name' | 'table-syntax'
9
9
  export declare class SyntaxToken extends Token {
10
10
  #private;
11
11
  get type(): SyntaxTypes;
12
- /** @param pattern 语法正则 */
13
- constructor(wikitext: string | undefined, pattern: RegExp, type: SyntaxTypes, config?: Config, accum?: Token[], acceptable?: Acceptable);
12
+ /**
13
+ * @class
14
+ */
15
+ constructor(wikitext: string | undefined, type: SyntaxTypes, config?: Config, accum?: Token[], acceptable?: Acceptable);
14
16
  }
15
17
  export {};
@@ -12,8 +12,10 @@ class SyntaxToken extends index_1.Token {
12
12
  get type() {
13
13
  return this.#type;
14
14
  }
15
- /** @param pattern 语法正则 */
16
- constructor(wikitext, pattern, type, config, accum, acceptable) {
15
+ /**
16
+ * @class
17
+ */
18
+ constructor(wikitext, type, config, accum, acceptable) {
17
19
  super(wikitext, config, accum, acceptable);
18
20
  this.#type = type;
19
21
  }
@@ -24,7 +24,7 @@ class TableBaseToken extends (0, attributesParent_1.attributesParent)(1)(index_2
24
24
  */
25
25
  constructor(pattern, syntax, type, attr, config = index_1.default.getConfig(), accum = [], acceptable) {
26
26
  super(undefined, config, accum, acceptable);
27
- this.append(new syntax_1.SyntaxToken(syntax, pattern, 'table-syntax', config, accum, {}),
27
+ this.append(new syntax_1.SyntaxToken(syntax, 'table-syntax', config, accum),
28
28
  // @ts-expect-error abstract class
29
29
  new attributes_1.AttributesToken(attr, 'table-attrs', type, config, accum));
30
30
  }
@@ -10,7 +10,6 @@ const rect_1 = require("../../lib/rect");
10
10
  const index_1 = __importDefault(require("../../index"));
11
11
  const trBase_1 = require("./trBase");
12
12
  const syntax_1 = require("../syntax");
13
- const closingPattern = /^\n[^\S\n]*(?:\|\}|\{\{\s*!\s*\}\}\}|\{\{\s*!\)\s*\}\})$/u;
14
13
  /**
15
14
  * 生成一个指定长度的空数组
16
15
  * @param n 数组长度
@@ -91,11 +90,9 @@ class TableToken extends trBase_1.TrBaseToken {
91
90
  * @param halfParsed
92
91
  */
93
92
  close(syntax = '\n|}', halfParsed) {
94
- const config = this.getAttribute('config'), accum = this.getAttribute('accum'), inner = halfParsed
95
- ? [syntax]
96
- : index_1.default.parse(syntax, this.getAttribute('include'), 2, config).childNodes;
93
+ const config = this.getAttribute('config'), accum = this.getAttribute('accum'), inner = [syntax];
97
94
  debug_1.Shadow.run(() => {
98
- const token = new syntax_1.SyntaxToken(undefined, closingPattern, 'table-syntax', config, accum);
95
+ const token = new syntax_1.SyntaxToken(undefined, 'table-syntax', config, accum);
99
96
  super.insertAt(token);
100
97
  });
101
98
  this.lastChild.replaceChildren(...inner);
@@ -4,6 +4,7 @@ exports.TrBaseToken = void 0;
4
4
  const lint_1 = require("../../util/lint");
5
5
  const base_1 = require("./base");
6
6
  const td_1 = require("./td");
7
+ const tableTags = new Set(['tr', 'td', 'th', 'caption']);
7
8
  /**
8
9
  * table row or table
9
10
  *
@@ -19,7 +20,8 @@ class TrBaseToken extends base_1.TableBaseToken {
19
20
  const first = inter.childNodes.find(child => child.text().trim()), tdPattern = /^\s*(?:!|\{\{\s*![!-]?\s*\}\})/u;
20
21
  if (!first
21
22
  || tdPattern.test(first.toString())
22
- || first.is('arg') && tdPattern.test(first.default || '')) {
23
+ || first.is('arg') && tdPattern.test(first.default || '')
24
+ || first.is('html') && tableTags.has(first.name)) {
23
25
  return errors;
24
26
  }
25
27
  else if (first.is('magic-word')) {
@@ -66,7 +66,7 @@ class TranscludeToken extends index_1.Token {
66
66
  if (fullWidth) {
67
67
  this.#colon = ':';
68
68
  }
69
- const pattern = new RegExp(String.raw `^\s*${name}\s*$`, isSensitive ? 'u' : 'iu'), token = new syntax_1.SyntaxToken(magicWord, pattern, 'magic-word-name', config, accum, {});
69
+ const token = new syntax_1.SyntaxToken(magicWord, 'magic-word-name', config, accum);
70
70
  super.insertAt(token);
71
71
  if (arg !== false) {
72
72
  parts.unshift([arg]);
@@ -85,7 +85,7 @@ class TranscludeToken extends index_1.Token {
85
85
  }
86
86
  if (this.type === 'template') {
87
87
  const name = (0, string_1.removeComment)(title).trim();
88
- if (!this.normalizeTitle(name, 10, true, true).valid) {
88
+ if (!this.normalizeTitle(name, 10, { halfParsed: true, temporary: true }).valid) {
89
89
  accum.pop();
90
90
  throw new SyntaxError('Invalid template name');
91
91
  }
@@ -144,7 +144,7 @@ class TranscludeToken extends index_1.Token {
144
144
  }
145
145
  /** 获取模板或模块名 */
146
146
  #getTitle() {
147
- const isTemplate = this.type === 'template', title = this.normalizeTitle(this.childNodes[isTemplate ? 0 : 1].toString(true), isTemplate ? 10 : 828, true);
147
+ const isTemplate = this.type === 'template', title = this.normalizeTitle(this.childNodes[isTemplate ? 0 : 1].toString(true), isTemplate ? 10 : 828, { temporary: true });
148
148
  return title;
149
149
  }
150
150
  /**
@@ -37,7 +37,14 @@ exports.isToken = isToken;
37
37
  * @param inserted 插入的子节点
38
38
  */
39
39
  const setChildNodes = (parent, position, deleteCount, inserted = []) => {
40
- const nodes = parent.getChildNodes(), removed = nodes.splice(position, deleteCount, ...inserted);
40
+ let nodes = parent.getChildNodes(), removed;
41
+ if (nodes.length === deleteCount) {
42
+ removed = nodes;
43
+ nodes = inserted;
44
+ }
45
+ else {
46
+ removed = nodes.splice(position, deleteCount, ...inserted);
47
+ }
41
48
  for (let i = 0; i < inserted.length; i++) {
42
49
  const node = inserted[i];
43
50
  node.setAttribute('parentNode', parent);
@@ -46,6 +53,9 @@ const setChildNodes = (parent, position, deleteCount, inserted = []) => {
46
53
  }
47
54
  nodes[position - 1]?.setAttribute('nextSibling', nodes[position]);
48
55
  nodes[position + inserted.length]?.setAttribute('previousSibling', nodes[position + inserted.length - 1]);
56
+ if (nodes === inserted) {
57
+ parent.setAttribute('childNodes', nodes);
58
+ }
49
59
  return removed;
50
60
  };
51
61
  exports.setChildNodes = setChildNodes;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.noWrap = exports.decodeNumber = exports.decodeHtml = exports.text = exports.escapeRegExp = exports.removeComment = exports.tidy = exports.extUrlChar = exports.extUrlCharFirst = exports.zs = exports.rawurldecode = void 0;
3
+ exports.noWrap = exports.decodeNumber = exports.decodeHtml = exports.decodeHtmlBasic = exports.text = exports.escapeRegExp = exports.removeComment = exports.tidy = exports.extUrlChar = exports.extUrlCharFirst = exports.zs = exports.rawurldecode = void 0;
4
4
  var common_1 = require("@bhsd/common");
5
5
  Object.defineProperty(exports, "rawurldecode", { enumerable: true, get: function () { return common_1.rawurldecode; } });
6
6
  exports.zs = String.raw ` \xA0\u1680\u2000-\u200A\u202F\u205F\u3000`;
@@ -27,9 +27,8 @@ exports.escapeRegExp = factory(/[\\{}()|.?*+^$[\]]/gu, String.raw `\$&`);
27
27
  const text = (childNodes, separator = '') => childNodes.map(child => typeof child === 'string' ? child : child.text()).join(separator);
28
28
  exports.text = text;
29
29
  const names = { lt: '<', gt: '>', lbrack: '[', rbrack: ']', lbrace: '{', rbrace: '}', nbsp: ' ', amp: '&', quot: '"' };
30
- /* istanbul ignore next */
31
30
  /** decode HTML entities */
32
- const decodeHtmlBasic = factory(/&(?:#(\d+|[Xx][\da-fA-F]+)|([lg]t|[LG]T|[lr]brac[ke]|nbsp|amp|AMP|quot|QUOT));/gu, (_, code, name) => code
31
+ exports.decodeHtmlBasic = factory(/&(?:#(\d+|[Xx][\da-fA-F]+)|([lg]t|[LG]T|[lr]brac[ke]|nbsp|amp|AMP|quot|QUOT));/gu, (_, code, name) => code
33
32
  ? String.fromCodePoint(Number((/^x/iu.test(code) ? '0' : '') + code))
34
33
  : names[name.toLowerCase()]);
35
34
  /**
@@ -48,7 +47,7 @@ const decodeHtml = (str) => {
48
47
  }
49
48
  /* istanbul ignore next */
50
49
  /* NOT FOR BROWSER ONLY END */
51
- return decodeHtmlBasic(str);
50
+ return (0, exports.decodeHtmlBasic)(str);
52
51
  };
53
52
  exports.decodeHtml = decodeHtml;
54
53
  /** decode numbered HTML entities */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wikilint",
3
- "version": "2.18.2",
3
+ "version": "2.18.3",
4
4
  "description": "A Node.js linter for MediaWiki markup",
5
5
  "keywords": [
6
6
  "mediawiki",
@@ -65,7 +65,7 @@
65
65
  ]
66
66
  },
67
67
  "dependencies": {
68
- "@bhsd/common": "^0.8.1",
68
+ "@bhsd/common": "^0.9.0",
69
69
  "vscode-languageserver-types": "^3.17.5"
70
70
  },
71
71
  "optionalDependencies": {