wikiparser-node 1.13.9 → 1.14.1

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 (51) hide show
  1. package/README.md +2 -0
  2. package/bundle/bundle.es7.js +37 -0
  3. package/bundle/bundle.min.js +30 -30
  4. package/dist/addon/transclude.js +10 -1
  5. package/dist/base.d.ts +1 -3
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js +2 -2
  8. package/dist/lib/element.js +3 -1
  9. package/dist/lib/text.js +38 -27
  10. package/dist/lib/title.js +12 -9
  11. package/dist/parser/commentAndExt.js +2 -2
  12. package/dist/parser/externalLinks.js +2 -2
  13. package/dist/parser/html.js +2 -1
  14. package/dist/parser/links.js +3 -3
  15. package/dist/parser/magicLinks.js +10 -3
  16. package/dist/parser/selector.js +2 -2
  17. package/dist/src/arg.js +3 -11
  18. package/dist/src/attribute.js +8 -19
  19. package/dist/src/attributes.js +30 -12
  20. package/dist/src/converterFlags.js +1 -7
  21. package/dist/src/extLink.js +1 -0
  22. package/dist/src/gallery.js +2 -10
  23. package/dist/src/heading.js +41 -7
  24. package/dist/src/html.js +47 -27
  25. package/dist/src/imageParameter.js +6 -4
  26. package/dist/src/imagemap.js +8 -1
  27. package/dist/src/index.d.ts +1 -1
  28. package/dist/src/index.js +12 -24
  29. package/dist/src/link/base.js +8 -5
  30. package/dist/src/link/file.js +5 -1
  31. package/dist/src/link/galleryImage.js +3 -1
  32. package/dist/src/magicLink.js +7 -21
  33. package/dist/src/nested.js +3 -11
  34. package/dist/src/nowiki/comment.js +1 -1
  35. package/dist/src/nowiki/quote.js +10 -8
  36. package/dist/src/paramTag/index.js +1 -7
  37. package/dist/src/parameter.js +1 -1
  38. package/dist/src/table/index.js +3 -1
  39. package/dist/src/tagPair/ext.js +3 -3
  40. package/dist/src/tagPair/include.js +2 -14
  41. package/dist/src/transclude.js +11 -3
  42. package/dist/util/sharable.d.mts +1 -0
  43. package/dist/util/sharable.mjs +168 -0
  44. package/dist/util/string.js +3 -2
  45. package/extensions/dist/base.js +3 -3
  46. package/extensions/es7/base.js +166 -0
  47. package/extensions/es7/lint.js +87 -0
  48. package/extensions/typings.d.ts +52 -0
  49. package/i18n/zh-hans.json +1 -1
  50. package/i18n/zh-hant.json +1 -1
  51. package/package.json +15 -11
@@ -94,7 +94,16 @@ transclude_1.TranscludeToken.prototype.fixDuplication =
94
94
  if (args.length <= 1) {
95
95
  continue;
96
96
  }
97
- const values = Map.groupBy(args, (arg) => arg.getValue().trim());
97
+ const values = new Map();
98
+ for (const arg of args) {
99
+ const val = arg.getValue().trim();
100
+ if (values.has(val)) {
101
+ values.get(val).push(arg);
102
+ }
103
+ else {
104
+ values.set(val, [arg]);
105
+ }
106
+ }
98
107
  let noMoreAnon = anonCount === 0 || !key.trim() || isNaN(key);
99
108
  const emptyArgs = values.get('') ?? [], duplicatedArgs = [...values].filter(([val, { length }]) => val && length > 1).flatMap(([, curArgs]) => {
100
109
  const anonIndex = noMoreAnon ? -1 : curArgs.findIndex(({ anon }) => anon);
package/dist/base.d.ts CHANGED
@@ -63,9 +63,7 @@ export interface LintError {
63
63
  endLine: number;
64
64
  endCol: number;
65
65
  fix?: LintError.Fix;
66
- suggestions?: (LintError.Fix & {
67
- desc: string;
68
- })[];
66
+ suggestions?: LintError.Fix[];
69
67
  }
70
68
  export type AST = Record<string, string | number | boolean> & {
71
69
  range: [number, number];
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Config, LintError, TokenTypes, Parser as ParserBase, Stage } from './base';
1
+ import type { Config, LintError, TokenTypes, Parser as ParserBase, Stage, AST } from './base';
2
2
  import type { Title } from './lib/title';
3
3
  import type { Token } from './internal';
4
4
  declare interface Parser extends ParserBase {
@@ -28,6 +28,6 @@ declare const Parser: Parser;
28
28
  // @ts-expect-error mixed export styles
29
29
  export = Parser;
30
30
  export default Parser;
31
- export type { Config, LintError, TokenTypes };
31
+ export type { Config, LintError, TokenTypes, AST, };
32
32
  export type * from './internal';
33
33
  declare global { type Acceptable = unknown; }
package/dist/index.js CHANGED
@@ -222,8 +222,8 @@ const Parser = {
222
222
  catch { }
223
223
  }
224
224
  for (const [name, filePath] of entries) {
225
- if (name in globalThis) {
226
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
225
+ if (name in globalThis) { // eslint-disable-line es-x/no-global-this
226
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, es-x/no-global-this
227
227
  Object.assign(globalThis, { [name]: require(filePath)[name] });
228
228
  }
229
229
  }
@@ -260,7 +260,9 @@ class AstElement extends node_1.AstNode {
260
260
  print(opt = {}) {
261
261
  const cl = opt.class;
262
262
  return this.toString()
263
- ? `${cl === '' ? '' : `<span class="wpb-${cl ?? this.type}">`}${(0, string_1.print)(this.childNodes, opt)}${cl === '' ? '' : '</span>'}`
263
+ ? (cl === '' ? '' : `<span class="wpb-${cl ?? this.type}">`)
264
+ + (0, string_1.print)(this.childNodes, opt)
265
+ + (cl === '' ? '' : '</span>')
264
266
  : '';
265
267
  }
266
268
  /**
package/dist/lib/text.js CHANGED
@@ -9,11 +9,11 @@ const constants_1 = require("../util/constants");
9
9
  const debug_1 = require("../util/debug");
10
10
  const html_1 = require("../util/html");
11
11
  /* NOT FOR BROWSER END */
12
- /* eslint-disable @typescript-eslint/no-unused-expressions */
12
+ /* eslint-disable @typescript-eslint/no-unused-expressions, es-x/no-regexp-unicode-property-escapes */
13
13
  /<\s*(?:\/\s*)?([a-z]\w*)|\{+|\}+|\[{2,}|\[(?![^[]*?\])|((?:^|\])[^[]*?)\]+|https?[:/]\/+/giu;
14
14
  /^https?:\/\/(?:\[[\da-f:.]+\]|[^[\]<>"\t\n\p{Zs}])[^[\]<>"\t\n\p{Zs}]*\.(?:gif|png|jpg|jpeg)$/iu;
15
- /* eslint-enable @typescript-eslint/no-unused-expressions */
16
- const sp = String.raw `[\p{Zs}\t]*`, source = String.raw `<\s*(?:/\s*)?([a-z]\w*)|\{+|\}+|\[{2,}|\[(?![^[]*?\])|((?:^|\])[^[]*?)\]+|(?:rfc|pmid)(?=[-::]?${sp}\d)|isbn(?=[-::]?${sp}(?:\d(?:${sp}|-)){6})`, errorSyntax = new RegExp(String.raw `${source}|https?[:/]/+`, 'giu'), errorSyntaxUrl = new RegExp(source, 'giu'), extImage = new RegExp(String.raw `^https?://${string_1.extUrlCharFirst}${string_1.extUrlChar}\.(?:gif|png|jpg|jpeg)$`, 'iu'), noLinkTypes = new Set(['attr-value', 'ext-link-text', 'link-text']), regexes = {
15
+ /* eslint-enable @typescript-eslint/no-unused-expressions, es-x/no-regexp-unicode-property-escapes */
16
+ const sp = String.raw `[${string_1.zs}\t]*`, source = String.raw `<\s*(?:/\s*)?([a-z]\w*)|\{+|\}+|\[{2,}|\[(?![^[]*?\])|((?:^|\])[^[]*?)\]+|(?:rfc|pmid)(?=[-::]?${sp}\d)|isbn(?=[-::]?${sp}(?:\d(?:${sp}|-)){6})`, errorSyntax = new RegExp(String.raw `${source}|https?[:/]/+`, 'giu'), errorSyntaxUrl = new RegExp(source, 'giu'), extImage = new RegExp(String.raw `^https?://${string_1.extUrlCharFirst}${string_1.extUrlChar}\.(?:gif|png|jpg|jpeg)$`, 'iu'), noLinkTypes = new Set(['attr-value', 'ext-link-text', 'link-text']), regexes = {
17
17
  '[': /[[\]]/u,
18
18
  '{': /[{}]/u,
19
19
  ']': /[[\]](?=[^[\]]*$)/u,
@@ -80,6 +80,14 @@ const sp = String.raw `[\p{Zs}\t]*`, source = String.raw `<\s*(?:/\s*)?([a-z]\w*
80
80
  'param',
81
81
  'xmp',
82
82
  ];
83
+ let wordRegex;
84
+ try {
85
+ // eslint-disable-next-line prefer-regex-literals, es-x/no-regexp-unicode-property-escapes
86
+ wordRegex = new RegExp(String.raw `[\p{L}\d_]`, 'u');
87
+ }
88
+ catch {
89
+ wordRegex = /\w/u;
90
+ }
83
91
  /** 文本节点 */
84
92
  class AstText extends node_1.AstNode {
85
93
  data = '';
@@ -148,7 +156,16 @@ class AstText extends node_1.AstNode {
148
156
  return [];
149
157
  }
150
158
  errorRegex.lastIndex = 0;
151
- const errors = [], nextType = nextSibling?.type, nextName = nextSibling?.name, previousType = previousSibling?.type, root = this.getRootNode(), { ext, html } = root.getAttribute('config'), { top, left } = root.posFromIndex(start), tags = new Set(['onlyinclude', 'noinclude', 'includeonly', ext, html, disallowedTags].flat(2));
159
+ const errors = [], nextType = nextSibling?.type, nextName = nextSibling?.name, previousType = previousSibling?.type, root = this.getRootNode(), rootStr = root.toString(), { ext, html } = root.getAttribute('config'), { top, left } = root.posFromIndex(start), tags = new Set([
160
+ 'onlyinclude',
161
+ 'noinclude',
162
+ 'includeonly',
163
+ ...ext,
164
+ ...html[0],
165
+ ...html[1],
166
+ ...html[2],
167
+ ...disallowedTags,
168
+ ]);
152
169
  for (let mt = errorRegex.exec(data); mt; mt = errorRegex.exec(data)) {
153
170
  const [, tag, prefix] = mt;
154
171
  let { index } = mt, error = mt[0].toLowerCase();
@@ -168,7 +185,7 @@ class AstText extends node_1.AstNode {
168
185
  else if (char === ']' && (index || length > 1)) {
169
186
  errorRegex.lastIndex--;
170
187
  }
171
- const startIndex = start + index, endIndex = startIndex + length, rootStr = root.toString(), nextChar = rootStr[endIndex], previousChar = rootStr[startIndex - 1], severity = length > 1 && !(char === '<' && !/[\s/>]/u.test(nextChar ?? '')
188
+ const startIndex = start + index, endIndex = startIndex + length, nextChar = rootStr[endIndex], previousChar = rootStr[startIndex - 1], severity = length > 1 && !(char === '<' && !/[\s/>]/u.test(nextChar ?? '')
172
189
  || isHtmlAttrVal && (char === '[' || char === ']')
173
190
  || magicLink && type === 'parameter-value')
174
191
  || char === '{' && (nextChar === char || previousChar === '-')
@@ -213,39 +230,31 @@ class AstText extends node_1.AstNode {
213
230
  endCol: startCol + length,
214
231
  };
215
232
  if (char === '<') {
216
- e.suggestions = [
217
- {
218
- desc: 'escape',
219
- range: [startIndex, startIndex + 1],
220
- text: '&lt;',
221
- },
222
- ];
233
+ e.suggestions = [{ desc: 'escape', range: [startIndex, startIndex + 1], text: '&lt;' }];
223
234
  }
224
235
  else if (char === 'h'
225
236
  && !(type === 'ext-link-text' || type === 'link-text')
226
- && /[\p{L}\d_]/u.test(previousChar || '')) {
227
- e.suggestions = [
228
- {
229
- desc: 'whitespace',
230
- range: [startIndex, startIndex],
231
- text: ' ',
232
- },
233
- ];
237
+ && wordRegex.test(previousChar || '')) {
238
+ e.suggestions = [{ desc: 'whitespace', range: [startIndex, startIndex], text: ' ' }];
234
239
  }
235
240
  else if (char === '[' && type === 'ext-link-text') {
236
241
  const i = parentNode.getAbsoluteIndex() + parentNode.toString().length;
237
- e.suggestions = [
238
- {
239
- desc: 'escape',
240
- range: [i, i + 1],
241
- text: '&#93;',
242
- },
243
- ];
242
+ e.suggestions = [{ desc: 'escape', range: [i, i + 1], text: '&#93;' }];
244
243
  }
245
244
  else if (char === ']' && previousType === 'free-ext-link' && severity === 'error') {
246
245
  const i = start - previousSibling.toString().length;
247
246
  e.fix = { range: [i, i], text: '[', desc: 'left bracket' };
248
247
  }
248
+ else if (magicLink) {
249
+ e.suggestions = [
250
+ ...mt[0] === error
251
+ ? []
252
+ : [{ desc: 'uppercase', range: [startIndex, endIndex], text: error }],
253
+ ...nextChar === ':' || nextChar === ':'
254
+ ? [{ desc: 'whitespace', range: [endIndex, endIndex + 1], text: ' ' }]
255
+ : [],
256
+ ];
257
+ }
249
258
  errors.push(e);
250
259
  }
251
260
  return errors;
@@ -378,6 +387,7 @@ class AstText extends node_1.AstNode {
378
387
  }
379
388
  }
380
389
  else if (mt && nextSibling.type === 'category') {
390
+ // eslint-disable-next-line es-x/no-string-prototype-trimstart-trimend
381
391
  const trimmed = this.data.trimEnd();
382
392
  if (this.data !== trimmed) {
383
393
  const { length } = trimmed;
@@ -398,6 +408,7 @@ class AstText extends node_1.AstNode {
398
408
  }
399
409
  }
400
410
  else {
411
+ // eslint-disable-next-line es-x/no-string-prototype-trimstart-trimend
401
412
  this.#setData(this.data.trimEnd());
402
413
  }
403
414
  for (const space of spaces) {
package/dist/lib/title.js CHANGED
@@ -78,6 +78,7 @@ class Title {
78
78
  }
79
79
  catch { }
80
80
  }
81
+ // eslint-disable-next-line es-x/no-string-prototype-trimstart-trimend
81
82
  this.#fragment = (0, string_1.decodeHtml)(fragment).replace(/[_ ]+/gu, ' ').trimEnd().replaceAll(' ', '_');
82
83
  }
83
84
  }
@@ -129,7 +130,7 @@ class Title {
129
130
  }
130
131
  const i = title.indexOf('#');
131
132
  if (i !== -1) {
132
- let fragment = title.slice(i + 1).trimEnd();
133
+ let fragment = title.slice(i).trim().slice(1);
133
134
  if (fragment.includes('%')) {
134
135
  try {
135
136
  fragment = (0, string_1.rawurldecode)(fragment);
@@ -155,11 +156,12 @@ class Title {
155
156
  }
156
157
  /** @private */
157
158
  toString(display) {
158
- return `${display ? this.title.replace(/_/gu, ' ') : this.title}${this.#fragment === undefined
159
- && this.#redirectFragment === undefined
160
- ? ''
161
- : `#${this.#fragment
162
- ?? this.#redirectFragment}`}`;
159
+ return (display ? this.title.replace(/_/gu, ' ') : this.title)
160
+ + (this.#fragment === undefined
161
+ && this.#redirectFragment === undefined
162
+ ? ''
163
+ : `#${this.#fragment
164
+ ?? this.#redirectFragment}`);
163
165
  }
164
166
  /** 检测是否是重定向 */
165
167
  getRedirection() {
@@ -236,9 +238,10 @@ class Title {
236
238
  getUrl() {
237
239
  const { title, fragment } = this;
238
240
  if (title) {
239
- return this.#path.replace('$1', `${encodeURIComponent(title)}${fragment === undefined && this.#redirectFragment === undefined
240
- ? ''
241
- : `#${encodeURIComponent(fragment ?? this.#redirectFragment)}`}`);
241
+ return this.#path.replace('$1', encodeURIComponent(title)
242
+ + (fragment === undefined && this.#redirectFragment === undefined
243
+ ? ''
244
+ : `#${encodeURIComponent(fragment ?? this.#redirectFragment)}`));
242
245
  }
243
246
  return fragment === undefined ? '' : `#${encodeURIComponent(fragment)}`;
244
247
  }
@@ -56,10 +56,10 @@ const parseCommentAndExt = (wikitext, config, accum, includeOnly) => {
56
56
  }
57
57
  }
58
58
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
59
- /<!--.*?(?:-->|$)|<foo(?:\s[^>]*)?\/?>|<\/foo\s*>|<(bar)(\s[^>]*?)?(?:\/>|>(.*?)<\/(\1\s*)>)|<(baz)(\s[^>]*?)?(?:\/>|>(.*?)(?:<\/(baz\s*)>|$))/gisu;
59
+ /<!--[\s\S]*?(?:-->|$)|<foo(?:\s[^>]*)?\/?>|<\/foo\s*>|<(bar)(\s[^>]*?)?(?:\/>|>([\s\S]*?)<\/(\1\s*)>)|<(baz)(\s[^>]*?)?(?:\/>|>([\s\S]*?)(?:<\/(baz\s*)>|$))/giu;
60
60
  const ext = config.ext.join('|'), noincludeRegex = includeOnly ? 'includeonly' : '(?:no|only)include', includeRegex = includeOnly ? 'noinclude' : 'includeonly',
61
61
  /** Never cached due to the possibility of nested extension tags */
62
- regex = new RegExp(String.raw `<!--.*?(?:-->|$)|<${noincludeRegex}(?:\s[^>]*)?/?>|</${noincludeRegex}\s*>|<(${ext})(\s[^>]*?)?(?:/>|>(.*?)</(\1\s*)>)|<(${includeRegex})(\s[^>]*?)?(?:/>|>(.*?)(?:</(${includeRegex}\s*)>|$))`, 'gisu');
62
+ regex = new RegExp(String.raw `<!--[\s\S]*?(?:-->|$)|<${noincludeRegex}(?:\s[^>]*)?/?>|</${noincludeRegex}\s*>|<(${ext})(\s[^>]*?)?(?:/>|>([\s\S]*?)</(\1\s*)>)|<(${includeRegex})(\s[^>]*?)?(?:/>|>([\s\S]*?)(?:</(${includeRegex}\s*)>|$))`, 'giu');
63
63
  return wikitext.replace(regex, (substr, name, attr, inner, closing, include, includeAttr, includeInner, includeClosing) => {
64
64
  const l = accum.length;
65
65
  let ch = 'n';
@@ -15,9 +15,9 @@ const constants_1 = require("../util/constants");
15
15
  * @param inFile 是否在图链中
16
16
  */
17
17
  const parseExternalLinks = (wikitext, config, accum, inFile) => {
18
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
18
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions, es-x/no-regexp-unicode-property-escapes
19
19
  /\[((?:ftp:\/\/|\/\/)(?:\[[\da-f:.]+\]|[^[\]<>"\t\n\p{Zs}])[^[\]<>"\t\n\p{Zs}]*(?=[[\]<>"\t\p{Zs}]|\0\d))(\p{Zs}*(?!\p{Zs}))([^\]\n]*)\]/giu;
20
- config.regexExternalLinks ??= new RegExp(String.raw `\[(\0\d+f\x7F|(?:(?:${config.protocol}|//)${string_1.extUrlCharFirst}|\0\d+m\x7F)${string_1.extUrlChar}(?=[[\]<>"\t\p{Zs}]|\0\d))(\p{Zs}*(?!\p{Zs}))([^\]\x01-\x08\x0A-\x1F\uFFFD]*)\]`, 'giu');
20
+ config.regexExternalLinks ??= new RegExp(String.raw `\[(\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');
21
21
  return wikitext.replace(config.regexExternalLinks, (_, url, space, text) => {
22
22
  const { length } = accum, mt = /&[lg]t;/u.exec(url);
23
23
  if (mt) {
@@ -14,7 +14,8 @@ const regex = /^(\/?)([a-z][^\s/>]*)((?:\s|\/(?!>))[^>]*?)?(\/?>)([^<]*)$/iu;
14
14
  * @param accum
15
15
  */
16
16
  const parseHtml = (wikitext, config, accum) => {
17
- config.htmlElements ??= new Set(config.html.flat());
17
+ const { html } = config;
18
+ config.htmlElements ??= new Set([...html[0], ...html[1], ...html[2]]);
18
19
  const bits = wikitext.split('<');
19
20
  let text = bits.shift();
20
21
  for (const x of bits) {
@@ -10,7 +10,7 @@ const category_1 = require("../src/link/category");
10
10
  /* NOT FOR BROWSER */
11
11
  const constants_1 = require("../util/constants");
12
12
  /* NOT FOR BROWSER END */
13
- const regexImg = /^((?:(?!\0\d+!\x7F)[^\n[\]{}|])+)(\||\0\d+!\x7F)(.*)$/su;
13
+ const regexImg = /^((?:(?!\0\d+!\x7F)[^\n[\]{}|])+)(\||\0\d+!\x7F)([\s\S]*)$/u;
14
14
  /**
15
15
  * 解析内部链接
16
16
  * @param wikitext
@@ -22,8 +22,8 @@ const parseLinks = (wikitext, config, accum) => {
22
22
  /^\s*(?:ftp:\/\/|\/\/)/iu;
23
23
  config.regexLinks ??= new RegExp(String.raw `^\s*(?:${config.protocol}|//)`, 'iu');
24
24
  const regex = true // eslint-disable-line no-constant-condition, @typescript-eslint/no-unnecessary-condition
25
- ? /^((?:(?!\0\d+!\x7F)[^\n[\]{}|])+)(?:(\||\0\d+!\x7F)(.*?[^\]]))?\]\](.*)$/su
26
- : /^((?:(?!\0\d+!\x7F)[^\n[\]{}|])+)(?:(\||\0\d+!\x7F)(.*?[^\]])?)?\]\](.*)$/su, bits = wikitext.split('[[');
25
+ ? /^((?:(?!\0\d+!\x7F)[^\n[\]{}|])+)(?:(\||\0\d+!\x7F)([\s\S]*?[^\]]))?\]\]([\s\S]*)$/u
26
+ : /^((?:(?!\0\d+!\x7F)[^\n[\]{}|])+)(?:(\||\0\d+!\x7F)([\s\S]*?[^\]])?)?\]\]([\s\S]*)$/u, bits = wikitext.split('[[');
27
27
  let s = bits.shift();
28
28
  for (let i = 0; i < bits.length; i++) {
29
29
  let mightBeImg = false, link, delimiter, text, after;
@@ -6,7 +6,7 @@ const magicLink_1 = require("../src/magicLink");
6
6
  /* NOT FOR BROWSER */
7
7
  const constants_1 = require("../util/constants");
8
8
  /* NOT FOR BROWSER END */
9
- const space = String.raw `[\p{Zs}\t]|&nbsp;|&#0*160;|&#x0*a0;`, sp = `(?:${space})+`, spdash = `(?:${space}|-)`, magicLinkPattern = String.raw `(?:RFC|PMID)${sp}\d+\b|ISBN${sp}(?:97[89]${spdash}?)?(?:\d${spdash}?){9}[\dx]\b`;
9
+ const space = String.raw `[${string_1.zs}\t]|&nbsp;|&#0*160;|&#x0*a0;`, sp = `(?:${space})+`, spdash = `(?:${space}|-)`, magicLinkPattern = String.raw `(?:RFC|PMID)${sp}\d+\b|ISBN${sp}(?:97[89]${spdash}?)?(?:\d${spdash}?){9}[\dx]\b`;
10
10
  /**
11
11
  * 解析自由外链
12
12
  * @param wikitext
@@ -14,9 +14,16 @@ const space = String.raw `[\p{Zs}\t]|&nbsp;|&#0*160;|&#x0*a0;`, sp = `(?:${space
14
14
  * @param accum
15
15
  */
16
16
  const parseMagicLinks = (wikitext, config, accum) => {
17
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
17
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions, es-x/no-regexp-unicode-property-escapes
18
18
  /(^|[^\p{L}\d_])(?:(?:ftp:\/\/|http:\/\/)((?:\[[\da-f:.]+\]|[^[\]<>"\t\n\p{Zs}])[^[\]<>"\0\t\n\p{Zs}]*)|(?:rfc|pmid)[\p{Zs}\t]+\d+\b|isbn[\p{Zs}\t]+(?:97[89][\p{Zs}\t-]?)?(?:\d[\p{Zs}\t-]?){9}[\dx]\b)/giu;
19
- config.regexMagicLinks ??= new RegExp(String.raw `(^|[^\p{L}\d_])(?:(?:${config.protocol})(${string_1.extUrlCharFirst}${string_1.extUrlChar})|${magicLinkPattern})`, 'giu');
19
+ if (!config.regexMagicLinks) {
20
+ try {
21
+ config.regexMagicLinks = new RegExp(String.raw `(^|[^\p{L}\d_])(?:(?:${config.protocol})(${string_1.extUrlCharFirst}${string_1.extUrlChar})|${magicLinkPattern})`, 'giu');
22
+ }
23
+ catch {
24
+ config.regexMagicLinks = new RegExp(String.raw `(^|\W)(?:(?:${config.protocol})(${string_1.extUrlCharFirst}${string_1.extUrlChar})|${magicLinkPattern})`, 'giu');
25
+ }
26
+ }
20
27
  return wikitext.replace(config.regexMagicLinks, (m, lead, p1) => {
21
28
  let url = lead ? m.slice(lead.length) : m;
22
29
  if (p1) {
@@ -161,7 +161,7 @@ const matches = (token, step, scope, has) => {
161
161
  }
162
162
  else if (selector.length === 4) { // 情形2:属性选择器
163
163
  const [key, equal, val = '', i] = selector, isAttr = typeof token.hasAttr === 'function' && typeof token.getAttr === 'function';
164
- if (!(key in token) && (!isAttr || !token.hasAttr(key))) {
164
+ if (!(key in token || isAttr && token.hasAttr(key))) {
165
165
  return equal === '!=';
166
166
  }
167
167
  const v = toCase(val, i), thisVal = getAttr(token, key);
@@ -173,7 +173,7 @@ const matches = (token, step, scope, has) => {
173
173
  return Boolean(thisVals?.[Symbol.iterator])
174
174
  && [...thisVals].some(w => typeof w === 'string' && toCase(w, i) === v);
175
175
  }
176
- else if (!primitives.has(typeof thisVal) && !(thisVal instanceof title_1.Title)) {
176
+ else if (!(primitives.has(typeof thisVal) || thisVal instanceof title_1.Title)) {
177
177
  throw new RangeError(`The complex attribute ${key} cannot be used in a selector!`);
178
178
  }
179
179
  const stringVal = toCase(String(thisVal), i);
package/dist/src/arg.js CHANGED
@@ -95,7 +95,7 @@ class ArgToken extends index_2.Token {
95
95
  if (!this.getAttribute('include')) {
96
96
  const e = (0, lint_1.generateForSelf)(this, { start }, 'no-arg', 'unexpected template argument');
97
97
  if (argDefault) {
98
- e.fix = { range: [start, e.endIndex], text: argDefault.text(), desc: 'expand' };
98
+ e.suggestions = [{ range: [start, e.endIndex], text: argDefault.text(), desc: 'expand' }];
99
99
  }
100
100
  return [e];
101
101
  }
@@ -110,16 +110,8 @@ class ArgToken extends index_2.Token {
110
110
  e.startIndex--;
111
111
  e.startCol--;
112
112
  e.suggestions = [
113
- {
114
- desc: 'remove',
115
- range: [e.startIndex, e.endIndex],
116
- text: '',
117
- },
118
- {
119
- desc: 'escape',
120
- range: [e.startIndex, e.startIndex + 1],
121
- text: '{{!}}',
122
- },
113
+ { desc: 'remove', range: [e.startIndex, e.endIndex], text: '' },
114
+ { desc: 'escape', range: [e.startIndex, e.startIndex + 1], text: '{{!}}' },
123
115
  ];
124
116
  return e;
125
117
  }));
@@ -180,16 +180,10 @@ let AttributeToken = (() => {
180
180
  const e = (0, lint_1.generateForChild)(lastChild, rect, 'unclosed-quote', index_1.default.msg('unclosed $1', 'quotes'), 'warning');
181
181
  e.startIndex--;
182
182
  e.startCol--;
183
- const fix = { range: [e.endIndex, e.endIndex], text: this.#quotes[0], desc: 'close' };
184
- if (lastChild.childNodes.some(({ type: t, data }) => t === 'text' && /\s/u.test(data))) {
185
- e.suggestions = [fix];
186
- }
187
- else {
188
- e.fix = fix;
189
- }
183
+ e.suggestions = [{ range: [e.endIndex, e.endIndex], text: this.#quotes[0], desc: 'close' }];
190
184
  errors.push(e);
191
185
  }
192
- const attrs = sharable_1.extAttrs[tag], attrs2 = sharable_1.htmlAttrs[tag];
186
+ const attrs = sharable_1.extAttrs[tag], attrs2 = sharable_1.htmlAttrs[tag], { length } = this.toString();
193
187
  if (!attrs?.has(name)
194
188
  && !attrs2?.has(name)
195
189
  // 不是未定义的扩展标签或包含嵌入的HTML标签
@@ -197,7 +191,10 @@ let AttributeToken = (() => {
197
191
  && (type === 'ext-attr' && !attrs2
198
192
  || !/^(?:xmlns:[\w:.-]+|data-(?!ooui|mw|parsoid)[^:]*)$/u.test(name)
199
193
  && (tag === 'meta' || tag === 'link' || !sharable_1.commonHtmlAttrs.has(name)))) {
200
- errors.push((0, lint_1.generateForChild)(firstChild, rect, 'illegal-attr', 'illegal attribute name'));
194
+ errors.push({
195
+ ...(0, lint_1.generateForChild)(firstChild, rect, 'illegal-attr', 'illegal attribute name'),
196
+ suggestions: [{ desc: 'remove', range: [start, start + length], text: '' }],
197
+ });
201
198
  }
202
199
  else if (sharable_1.obsoleteAttrs[tag]?.has(name)) {
203
200
  errors.push((0, lint_1.generateForChild)(firstChild, rect, 'obsolete-attr', 'obsolete attribute', 'warning'));
@@ -208,16 +205,8 @@ let AttributeToken = (() => {
208
205
  else if (name === 'tabindex' && typeof value === 'string' && value !== '0') {
209
206
  const e = (0, lint_1.generateForChild)(lastChild, rect, 'illegal-attr', 'nonzero tabindex');
210
207
  e.suggestions = [
211
- {
212
- desc: 'remove',
213
- range: [start, e.endIndex],
214
- text: '',
215
- },
216
- {
217
- desc: '0 tabindex',
218
- range: [e.startIndex, e.endIndex],
219
- text: '0',
220
- },
208
+ { desc: 'remove', range: [start, start + length], text: '' },
209
+ { desc: '0 tabindex', range: [e.startIndex, e.endIndex], text: '0' },
221
210
  ];
222
211
  errors.push(e);
223
212
  }
@@ -23,6 +23,14 @@ const toAttributeType = (type) => type.slice(0, -1);
23
23
  * @param type 属性类型
24
24
  */
25
25
  const toDirty = (type) => `${toAttributeType(type)}-dirty`;
26
+ let wordRegex;
27
+ try {
28
+ // eslint-disable-next-line prefer-regex-literals, es-x/no-regexp-unicode-property-escapes
29
+ wordRegex = new RegExp(String.raw `[\p{L}\d]`, 'u');
30
+ }
31
+ catch {
32
+ wordRegex = /[^\W_]/u;
33
+ }
26
34
  /**
27
35
  * 扩展和HTML标签属性
28
36
  * @classdesc `{childNodes: ...AtomToken|AttributeToken}`
@@ -104,7 +112,7 @@ class AttributesToken extends index_2.Token {
104
112
  this.#type = type;
105
113
  this.setAttribute('name', name);
106
114
  if (attr) {
107
- const regex = /([^\s/](?:(?!\0\d+~\x7F)[^\s/=])*)(?:((?:\s(?:\s|\0\d+[cn]\x7F)*)?(?:=|\0\d+~\x7F)(?:\s|\0\d+[cn]\x7F)*)(?:(["'])(.*?)(\3|$)|(\S*)))?/gsu;
115
+ const regex = /([^\s/](?:(?!\0\d+~\x7F)[^\s/=])*)(?:((?:\s(?:\s|\0\d+[cn]\x7F)*)?(?:=|\0\d+~\x7F)(?:\s|\0\d+[cn]\x7F)*)(?:(["'])([\s\S]*?)(\3|$)|(\S*)))?/gu;
108
116
  let out = '', mt = regex.exec(attr), lastIndex = 0;
109
117
  const insertDirty = /** 插入无效属性 */ () => {
110
118
  if (out) {
@@ -168,8 +176,11 @@ class AttributesToken extends index_2.Token {
168
176
  lint(start = this.getAbsoluteIndex(), re) {
169
177
  const errors = super.lint(start, re), { parentNode, childNodes } = this, attrs = new Map(), duplicated = new Set(), rect = new rect_1.BoundingRect(this, start);
170
178
  if (parentNode?.type === 'html' && parentNode.closing && this.text().trim()) {
171
- const e = (0, lint_1.generateForSelf)(this, rect, 'no-ignored', 'attributes of a closing tag');
172
- e.fix = { range: [start, e.endIndex], text: '', desc: 'remove' };
179
+ const e = (0, lint_1.generateForSelf)(this, rect, 'no-ignored', 'attributes of a closing tag'), index = parentNode.getAbsoluteIndex();
180
+ e.suggestions = [
181
+ { desc: 'remove', range: [start, e.endIndex], text: '' },
182
+ { desc: 'open', range: [index + 1, index + 2], text: '' },
183
+ ];
173
184
  errors.push(e);
174
185
  }
175
186
  for (const attr of childNodes) {
@@ -186,21 +197,28 @@ class AttributesToken extends index_2.Token {
186
197
  else {
187
198
  const str = attr.text().trim();
188
199
  if (str) {
189
- const e = (0, lint_1.generateForChild)(attr, rect, 'no-ignored', 'containing invalid attribute', /[\p{L}\d]/u.test(str) ? 'error' : 'warning');
190
- e.suggestions = [
191
- {
192
- desc: 'remove',
193
- range: [e.startIndex, e.endIndex],
194
- text: ' ',
195
- },
196
- ];
200
+ const e = (0, lint_1.generateForChild)(attr, rect, 'no-ignored', 'containing invalid attribute', wordRegex.test(str) ? 'error' : 'warning');
201
+ e.suggestions = [{ desc: 'remove', range: [e.startIndex, e.endIndex], text: ' ' }];
197
202
  errors.push(e);
198
203
  }
199
204
  }
200
205
  }
201
206
  if (duplicated.size > 0) {
202
207
  for (const key of duplicated) {
203
- errors.push(...attrs.get(key).map(attr => (0, lint_1.generateForChild)(attr, rect, 'no-duplicate', index_1.default.msg('duplicated $1 attribute', key))));
208
+ const pairs = attrs.get(key).map(attr => {
209
+ const value = attr.getValue();
210
+ return [attr, value === true ? '' : value];
211
+ });
212
+ errors.push(...pairs.map(([attr, value], i) => {
213
+ const e = (0, lint_1.generateForChild)(attr, rect, 'no-duplicate', index_1.default.msg('duplicated $1 attribute', key)), remove = { desc: 'remove', range: [e.startIndex, e.endIndex], text: '' };
214
+ if (!value || pairs.slice(0, i).some(([, v]) => v === value)) {
215
+ e.fix = remove;
216
+ }
217
+ else {
218
+ e.suggestions = [remove];
219
+ }
220
+ return e;
221
+ }));
204
222
  }
205
223
  }
206
224
  return errors;
@@ -91,13 +91,7 @@ class ConverterFlagsToken extends index_2.Token {
91
91
  e.fix = { range: [e.startIndex, e.endIndex], text: flag.toUpperCase(), desc: 'uppercase' };
92
92
  }
93
93
  else {
94
- e.suggestions = [
95
- {
96
- desc: 'remove',
97
- range: [e.startIndex - (i && 1), e.endIndex],
98
- text: '',
99
- },
100
- ];
94
+ e.suggestions = [{ desc: 'remove', range: [e.startIndex - (i && 1), e.endIndex], text: '' }];
101
95
  }
102
96
  errors.push(e);
103
97
  }
@@ -168,6 +168,7 @@ let ExtLinkToken = (() => {
168
168
  && length > 1
169
169
  && (firstChild?.type === 'text' || firstChild?.type === 'converter')
170
170
  // 都替换成`<`肯定不对,但无妨
171
+ // eslint-disable-next-line es-x/no-regexp-unicode-property-escapes
171
172
  && /^[^[\]<>"\0-\x1F\x7F\p{Zs}\uFFFD]/u.test(lastChild.text().replace(/&[lg]t;/u, '<'))) {
172
173
  this.#space = ' ';
173
174
  }
@@ -92,16 +92,8 @@ class GalleryToken extends index_2.Token {
92
92
  startCol,
93
93
  endCol: startCol + length,
94
94
  suggestions: [
95
- {
96
- desc: 'remove',
97
- range: [start, endIndex],
98
- text: '',
99
- },
100
- {
101
- desc: 'comment',
102
- range: [start, endIndex],
103
- text: `<!--${str}-->`,
104
- },
95
+ { desc: 'remove', range: [start, endIndex], text: '' },
96
+ { desc: 'comment', range: [start, endIndex], text: `<!--${str}-->` },
105
97
  ],
106
98
  });
107
99
  }
@@ -135,21 +135,55 @@ let HeadingToken = (() => {
135
135
  }
136
136
  /** @private */
137
137
  lint(start = this.getAbsoluteIndex(), re) {
138
- const errors = super.lint(start, re), { firstChild, level } = this, innerStr = firstChild.toString(), quotes = firstChild.childNodes.filter((0, debug_1.isToken)('quote')), boldQuotes = quotes.filter(({ bold }) => bold), italicQuotes = quotes.filter(({ italic }) => italic), rect = new rect_1.BoundingRect(this, start);
138
+ const errors = super.lint(start, re), { firstChild, level } = this, innerStr = firstChild.toString(), unbalancedStart = innerStr.startsWith('='), unbalanced = unbalancedStart || innerStr.endsWith('='), quotes = firstChild.childNodes.filter((0, debug_1.isToken)('quote')), boldQuotes = quotes.filter(({ bold }) => bold), italicQuotes = quotes.filter(({ italic }) => italic), rect = new rect_1.BoundingRect(this, start);
139
139
  if (this.level === 1) {
140
- errors.push((0, lint_1.generateForChild)(firstChild, rect, 'h1', '<h1>'));
140
+ const e = (0, lint_1.generateForChild)(firstChild, rect, 'h1', '<h1>');
141
+ if (!unbalanced) {
142
+ e.suggestions = [{ desc: 'h2', range: [e.startIndex, e.endIndex], text: `=${innerStr}=` }];
143
+ }
144
+ errors.push(e);
141
145
  }
142
- if (innerStr.startsWith('=') || innerStr.endsWith('=')) {
143
- errors.push((0, lint_1.generateForChild)(firstChild, rect, 'unbalanced-header', index_1.default.msg('unbalanced $1 in a section header', '"="')));
146
+ if (unbalanced) {
147
+ const e = (0, lint_1.generateForChild)(firstChild, rect, 'unbalanced-header', index_1.default.msg('unbalanced $1 in a section header', '"="'));
148
+ if (innerStr === '=') {
149
+ //
150
+ }
151
+ else if (unbalancedStart) {
152
+ const [extra] = /^=+/u.exec(innerStr);
153
+ e.suggestions = [
154
+ { desc: `h${level}`, range: [e.startIndex, e.startIndex + extra.length], text: '' },
155
+ { desc: `h${level + extra.length}`, range: [e.endIndex, e.endIndex], text: extra },
156
+ ];
157
+ }
158
+ else {
159
+ const extra = /[^=](=+)$/u.exec(innerStr)[1];
160
+ e.suggestions = [
161
+ { desc: `h${level}`, range: [e.endIndex - extra.length, e.endIndex], text: '' },
162
+ { desc: `h${level + extra.length}`, range: [e.startIndex, e.startIndex], text: extra },
163
+ ];
164
+ }
165
+ errors.push(e);
144
166
  }
145
167
  if (this.closest('html-attrs,table-attrs')) {
146
168
  errors.push((0, lint_1.generateForSelf)(this, rect, 'parsing-order', 'section header in a HTML tag'));
147
169
  }
170
+ const rootStr = this.getRootNode().toString();
148
171
  if (boldQuotes.length % 2) {
149
- errors.push((0, lint_1.generateForChild)(boldQuotes[boldQuotes.length - 1], { ...rect, start: start + level, left: rect.left + level }, 'format-leakage', index_1.default.msg('unbalanced $1 in a section header', 'bold apostrophes')));
172
+ const e = (0, lint_1.generateForChild)(boldQuotes[boldQuotes.length - 1], { ...rect, start: start + level, left: rect.left + level }, 'format-leakage', index_1.default.msg('unbalanced $1 in a section header', 'bold apostrophes')), end = start + level + innerStr.length;
173
+ if (rootStr.slice(e.endIndex, end).trim()) {
174
+ e.suggestions = [{ desc: 'close', range: [end, end], text: "'''" }];
175
+ }
176
+ else {
177
+ e.fix = { desc: 'remove', range: [e.startIndex, e.endIndex], text: '' };
178
+ }
179
+ errors.push(e);
150
180
  }
151
181
  if (italicQuotes.length % 2) {
152
- errors.push((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')));
182
+ 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;
183
+ e.fix = rootStr.slice(e.endIndex, end).trim()
184
+ ? { desc: 'close', range: [end, end], text: "''" }
185
+ : { desc: 'remove', range: [e.startIndex, e.endIndex], text: '' };
186
+ errors.push(e);
153
187
  }
154
188
  return errors;
155
189
  }
@@ -205,7 +239,7 @@ let HeadingToken = (() => {
205
239
  if (headings?.has(lcId)) {
206
240
  let i = 2;
207
241
  for (; headings.has(`${lcId}_${i}`); i++) {
208
- // pass
242
+ //
209
243
  }
210
244
  id = `${id}_${i}`;
211
245
  headings.add(`${lcId}_${i}`);