wikilint 2.18.2 → 2.18.4

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 (48) 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 +12 -1
  10. package/dist/index.js +45 -28
  11. package/dist/lib/lsp.d.ts +1 -0
  12. package/dist/lib/lsp.js +19 -14
  13. package/dist/lib/text.js +2 -2
  14. package/dist/lib/title.d.ts +18 -4
  15. package/dist/lib/title.js +12 -4
  16. package/dist/parser/braces.js +79 -37
  17. package/dist/parser/commentAndExt.js +5 -16
  18. package/dist/parser/links.js +1 -1
  19. package/dist/parser/redirect.js +1 -3
  20. package/dist/src/arg.js +1 -1
  21. package/dist/src/attribute.js +1 -1
  22. package/dist/src/converter.js +1 -1
  23. package/dist/src/gallery.js +1 -1
  24. package/dist/src/heading.js +3 -3
  25. package/dist/src/imageParameter.js +9 -9
  26. package/dist/src/imagemap.js +3 -2
  27. package/dist/src/index.d.ts +1 -1
  28. package/dist/src/index.js +7 -5
  29. package/dist/src/link/base.js +1 -1
  30. package/dist/src/link/file.d.ts +2 -0
  31. package/dist/src/link/file.js +15 -13
  32. package/dist/src/link/galleryImage.js +1 -1
  33. package/dist/src/link/redirectTarget.js +1 -2
  34. package/dist/src/magicLink.js +1 -1
  35. package/dist/src/nested.js +5 -5
  36. package/dist/src/nowiki/index.js +3 -2
  37. package/dist/src/redirect.js +1 -2
  38. package/dist/src/syntax.d.ts +4 -2
  39. package/dist/src/syntax.js +4 -2
  40. package/dist/src/table/base.js +1 -1
  41. package/dist/src/table/index.d.ts +1 -0
  42. package/dist/src/table/index.js +3 -6
  43. package/dist/src/table/trBase.js +31 -14
  44. package/dist/src/transclude.js +14 -6
  45. package/dist/util/debug.js +11 -1
  46. package/dist/util/diff.js +1 -1
  47. package/dist/util/string.js +3 -4
  48. package/package.json +2 -2
@@ -1,16 +1,40 @@
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)().heap_size_limit * 0.9;
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
+ }, lbrack = String.raw `\[(?!\[)`, newline = String.raw `\n(?![=\0])`, openBraces = String.raw `|\{{2,}`, marks = new Map([['!', '!'], ['!!', '+'], ['(!', '{'], ['!)', '}'], ['!-', '-'], ['=', '~'], ['server', 'm']]), getExecRegex = (0, common_1.getRegex)(s => new RegExp(s, 'gmu'));
19
+ let reReplace;
20
+ /* NOT FOR BROWSER ONLY */
21
+ try {
22
+ reReplace = new RegExp(String.raw `(?<!\{)\{\{((?:[^\n{}[]|${lbrack}|${newline})*)\}\}` // eslint-disable-line prefer-template
23
+ + '|'
24
+ + String.raw `\{\{((?:[^\n{}[]|${lbrack}|${newline})*)\}\}(?!\})`
25
+ + '|'
26
+ + String.raw `\[\[(?:[^\n[\]{]|${newline})*\]\]`
27
+ + '|'
28
+ + String.raw `-\{(?:[^\n{}[]|${lbrack}|${newline})*\}-`, 'gu');
29
+ }
30
+ catch {
31
+ /* NOT FOR BROWSER ONLY END */
32
+ reReplace = new RegExp(String.raw `\{\{((?:[^\n{}[]|${lbrack}|${newline})*)\}\}(?!\})` // eslint-disable-line prefer-template
33
+ + '|'
34
+ + String.raw `\[\[(?:[^\n[\]{]|${newline})*\]\]`
35
+ + '|'
36
+ + String.raw `-\{(?:[^\n{}[]|${lbrack}|${newline})*\}-`, 'gu');
37
+ }
14
38
  /**
15
39
  * 获取模板或魔术字对应的字符
16
40
  * @param s 模板或魔术字名
@@ -33,7 +57,8 @@ const getSymbol = (s) => {
33
57
  * @param wikitext
34
58
  * @param config
35
59
  * @param accum
36
- * @throws TranscludeToken.constructor()
60
+ * @throws `RangeError` Maximum iteration exceeded
61
+ * @throws `TranscludeToken.constructor()`
37
62
  */
38
63
  const parseBraces = (wikitext, config, accum) => {
39
64
  const source = String.raw `${config.excludes?.includes('heading') ? '' : String.raw `^((?:\0\d+[cno]\x7F)*)={1,6}|`}\[\[|-\{(?!\{)`, { parserFunction: [, , , subst] } = config, stack = [], linkStack = [];
@@ -42,46 +67,61 @@ const parseBraces = (wikitext, config, accum) => {
42
67
  * @param s 不含内链的字符串
43
68
  */
44
69
  const restore = (s) => s.replace(/\0(\d+)\x7F/gu, (_, p1) => linkStack[p1]);
45
- wikitext = wikitext.replace(/\{\{([^\n{}[]*)\}\}(?!\})|\[\[[^\n[\]{]*\]\]/gu, (m, p1) => {
46
- if (p1 !== undefined) {
47
- try {
48
- const { length } = accum, parts = p1.split('|');
49
- // @ts-expect-error abstract class
50
- new transclude_1.TranscludeToken(parts[0], parts.slice(1).map(part => {
51
- const i = part.indexOf('=');
52
- return i === -1 ? [part] : [part.slice(0, i), part.slice(i + 1)];
53
- }), config, accum);
54
- return `\0${length}${getSymbol(parts[0])}\x7F`;
55
- }
56
- catch (e) {
57
- /* istanbul ignore if */
58
- if (!(e instanceof SyntaxError) || e.message !== 'Invalid template name') {
59
- throw e;
70
+ /**
71
+ * 填入模板内容
72
+ * @param text wikitext全文
73
+ * @param parts 模板参数
74
+ * @param lastIndex 匹配的起始位置
75
+ * @param index 匹配位置
76
+ */
77
+ const push = (text, parts, lastIndex, index) => {
78
+ parts[parts.length - 1].push(restore(text.slice(lastIndex, index)));
79
+ };
80
+ let replaced;
81
+ do {
82
+ if (replaced !== undefined) {
83
+ wikitext = replaced;
84
+ }
85
+ replaced = wikitext.replace(reReplace, (m, p1, p2) => {
86
+ if (p1 !== undefined || typeof p2 === 'string') {
87
+ try {
88
+ const { length } = accum, parts = (p1 ?? p2).split('|');
89
+ // @ts-expect-error abstract class
90
+ new transclude_1.TranscludeToken(restore(parts[0]), parts.slice(1).map(part => {
91
+ const i = part.indexOf('=');
92
+ return (i === -1 ? [part] : [part.slice(0, i), part.slice(i + 1)]).map(restore);
93
+ }), config, accum);
94
+ return `\0${length}${getSymbol(parts[0])}\x7F`;
95
+ }
96
+ catch (e) {
97
+ /* istanbul ignore if */
98
+ if (!(e instanceof SyntaxError) || e.message !== 'Invalid template name') {
99
+ throw e;
100
+ }
60
101
  }
61
102
  }
62
- }
63
- linkStack.push(m);
64
- return `\0${linkStack.length - 1}\x7F`;
65
- });
103
+ linkStack.push(restore(m));
104
+ return `\0${linkStack.length - 1}\x7F`;
105
+ });
106
+ } while (replaced !== wikitext);
107
+ wikitext = replaced;
66
108
  const lastBraces = wikitext.lastIndexOf('}}') - wikitext.length;
67
109
  let moreBraces = lastBraces + wikitext.length !== -1;
68
- let regex = new RegExp(source + (moreBraces ? openBraces : ''), 'gmu'), mt = regex.exec(wikitext), lastIndex;
110
+ let regex = getExecRegex(source + (moreBraces ? openBraces : '')), mt = regex.exec(wikitext), lastIndex;
69
111
  while (mt
70
112
  || lastIndex !== undefined && lastIndex <= wikitext.length
71
113
  && stack[stack.length - 1]?.[0]?.startsWith('=')) {
114
+ /* NOT FOR BROWSER ONLY */
115
+ if (process.memoryUsage().heapUsed > MAXHEAP) {
116
+ throw new RangeError('Maximum heap size exceeded');
117
+ }
118
+ /* NOT FOR BROWSER ONLY END */
72
119
  if (mt?.[1]) {
73
120
  const [, { length }] = mt;
74
121
  mt[0] = mt[0].slice(length);
75
122
  mt.index += length;
76
123
  }
77
124
  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
125
  if (syntax === ']]' || syntax === '}-') { // 情形1:闭合内链或转换
86
126
  lastIndex = curIndex + 2;
87
127
  }
@@ -92,17 +132,19 @@ const parseBraces = (wikitext, config, accum) => {
92
132
  const rmt = /^(={1,6})(.+)\1((?:\s|\0\d+[cn]\x7F)*)$/u
93
133
  .exec(wikitext.slice(index, curIndex));
94
134
  if (rmt) {
95
- wikitext = `${wikitext.slice(0, index)}\0${accum.length}h\x7F${wikitext.slice(curIndex)}`;
96
- lastIndex = index + 4 + String(accum.length).length;
97
135
  rmt[2] = restore(rmt[2]);
98
- // @ts-expect-error abstract class
99
- new heading_1.HeadingToken(rmt[1].length, rmt.slice(2), config, accum);
136
+ if (!rmt[2].includes('\n')) {
137
+ wikitext = `${wikitext.slice(0, index)}\0${accum.length}h\x7F${wikitext.slice(curIndex)}`;
138
+ lastIndex = index + 4 + String(accum.length).length;
139
+ // @ts-expect-error abstract class
140
+ new heading_1.HeadingToken(rmt[1].length, rmt.slice(2), config, accum);
141
+ }
100
142
  }
101
143
  }
102
144
  }
103
145
  else if (syntax === '|' || innerEqual) { // 情形3:模板内部,含行首单个'='
104
146
  lastIndex = curIndex + 1;
105
- push(wikitext);
147
+ push(wikitext, parts, topPos, curIndex);
106
148
  if (syntax === '|') {
107
149
  parts.push([]);
108
150
  }
@@ -113,7 +155,7 @@ const parseBraces = (wikitext, config, accum) => {
113
155
  else if (syntax.startsWith('}}')) { // 情形4:闭合模板
114
156
  const close = syntax.slice(0, Math.min(open.length, 3)), rest = open.length - close.length, { length } = accum;
115
157
  lastIndex = curIndex + close.length; // 这不是最终的lastIndex
116
- push(wikitext);
158
+ push(wikitext, parts, topPos, curIndex);
117
159
  let skip = false, ch = 't';
118
160
  if (close.length === 3) {
119
161
  const argParts = parts.map(part => part.join('=')), str = argParts.length > 1 && (0, string_1.removeComment)(argParts[1]).trim();
@@ -167,9 +209,9 @@ const parseBraces = (wikitext, config, accum) => {
167
209
  curTop = stack[stack.length - 1];
168
210
  }
169
211
  }
170
- regex = new RegExp(source
212
+ regex = getExecRegex(source
171
213
  + (moreBraces ? openBraces : '')
172
- + (curTop ? `|${closes[curTop[0][0]]}${curTop.findEqual ? '|=' : ''}` : ''), 'gmu');
214
+ + (curTop ? `|${closes[curTop[0][0]]}${curTop.findEqual ? '|=' : ''}` : ''));
173
215
  regex.lastIndex = lastIndex;
174
216
  mt = regex.exec(wikitext);
175
217
  }
@@ -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.getRegex)(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);
package/dist/src/arg.js CHANGED
@@ -61,7 +61,7 @@ class ArgToken extends index_1.Token {
61
61
  /** 设置name */
62
62
  #setName() {
63
63
  // eslint-disable-next-line no-unused-labels
64
- LSP: this.setAttribute('name', this.firstChild.toString(true).trim());
64
+ LSP: this.setAttribute('name', this.firstChild.text().trim());
65
65
  }
66
66
  /** @private */
67
67
  afterBuild() {
@@ -80,7 +80,7 @@ class AttributeToken extends index_2.Token {
80
80
  if (this.parentNode) {
81
81
  this.#tag = this.parentNode.name;
82
82
  }
83
- this.setAttribute('name', this.firstChild.toString(true).trim().toLowerCase());
83
+ this.setAttribute('name', this.firstChild.text().trim().toLowerCase());
84
84
  super.afterBuild();
85
85
  }
86
86
  /** @private */
@@ -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.text(), 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
  }