wikiparser-node 1.16.2 → 1.16.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 (45) hide show
  1. package/bundle/bundle.es7.js +26 -26
  2. package/bundle/bundle.lsp.js +38 -0
  3. package/bundle/bundle.min.js +35 -0
  4. package/coverage/badge.svg +1 -1
  5. package/data/signatures.json +43 -0
  6. package/dist/addon/token.js +107 -107
  7. package/dist/lib/element.d.ts +46 -1
  8. package/dist/lib/element.js +95 -1
  9. package/dist/lib/lsp.d.ts +16 -10
  10. package/dist/lib/lsp.js +331 -268
  11. package/dist/lib/node.d.ts +1 -1
  12. package/dist/lib/node.js +59 -27
  13. package/dist/lib/title.d.ts +5 -2
  14. package/dist/lib/title.js +25 -12
  15. package/dist/mixin/attributesParent.js +18 -18
  16. package/dist/parser/selector.js +1 -1
  17. package/dist/src/heading.js +7 -7
  18. package/dist/src/html.d.ts +1 -5
  19. package/dist/src/html.js +1 -5
  20. package/dist/src/imageParameter.d.ts +5 -2
  21. package/dist/src/imageParameter.js +6 -3
  22. package/dist/src/index.d.ts +1 -45
  23. package/dist/src/index.js +0 -93
  24. package/dist/src/link/base.js +8 -7
  25. package/dist/src/link/category.d.ts +2 -2
  26. package/dist/src/link/index.d.ts +2 -2
  27. package/dist/src/link/redirectTarget.d.ts +1 -1
  28. package/dist/src/magicLink.d.ts +5 -2
  29. package/dist/src/magicLink.js +22 -17
  30. package/dist/src/nowiki/listBase.js +1 -1
  31. package/dist/src/table/base.js +1 -1
  32. package/dist/src/tagPair/ext.js +1 -2
  33. package/dist/src/transclude.d.ts +5 -0
  34. package/dist/src/transclude.js +27 -5
  35. package/dist/util/html.js +5 -1
  36. package/extensions/dist/base.js +286 -0
  37. package/extensions/dist/codejar.js +56 -0
  38. package/extensions/dist/editor.js +159 -0
  39. package/extensions/dist/highlight.js +30 -0
  40. package/extensions/dist/lint.js +79 -0
  41. package/extensions/dist/lsp.js +74 -0
  42. package/extensions/editor.css +59 -0
  43. package/extensions/es7/base.js +1 -1
  44. package/extensions/ui.css +162 -0
  45. package/package.json +4 -4
package/dist/lib/lsp.js CHANGED
@@ -5,17 +5,32 @@ const path = require("path");
5
5
  const common_1 = require("@bhsd/common");
6
6
  const sharable_1 = require("../util/sharable");
7
7
  const lint_1 = require("../util/lint");
8
+ const string_1 = require("../util/string");
8
9
  const index_1 = require("../index");
9
- const element_1 = require("../lib/element");
10
10
  /* NOT FOR BROWSER */
11
11
  const constants_1 = require("../util/constants");
12
+ /* NOT FOR BROWSER ONLY END */
12
13
  exports.tasks = new WeakMap();
13
- const refTags = new Set(['ref']), referencesTags = new Set(['ref', 'references']), nameAttrs = new Set(['name', 'extends', 'follow']), groupAttrs = new Set(['group']);
14
+ const refTags = new Set(['ref']), referencesTags = new Set(['ref', 'references']), nameAttrs = new Set(['name', 'extends', 'follow']), groupAttrs = new Set(['group']), renameTypes = new Set([
15
+ 'arg-name',
16
+ 'template-name',
17
+ 'magic-word-name',
18
+ 'link-target',
19
+ 'parameter-key',
20
+ ]), referenceTypes = new Set([
21
+ 'ext',
22
+ 'html',
23
+ 'attr-key',
24
+ 'image-parameter',
25
+ 'heading-title',
26
+ 'heading',
27
+ ...renameTypes,
28
+ ]), plainTypes = new Set(['text', 'comment', 'noinclude', 'include']);
14
29
  /**
15
30
  * Check if all child nodes are plain text or comments.
16
31
  * @param childNodes child nodes
17
32
  */
18
- const isPlain = (childNodes) => childNodes.every(({ type }) => ['text', 'comment', 'noinclude', 'include'].includes(type));
33
+ const isPlain = (childNodes) => childNodes.every(({ type }) => plainTypes.has(type));
19
34
  /**
20
35
  * Get the position of a character in the document.
21
36
  * @param root root token
@@ -54,8 +69,9 @@ const createNodeRange = (token) => {
54
69
  * @param pos position
55
70
  * @param pos.line line number
56
71
  * @param pos.character character number
72
+ * @param extra extra text
57
73
  */
58
- const getCompletion = (words, kind, mt, { line, character }) => [...new Set(words)].map((w) => ({
74
+ const getCompletion = (words, kind, mt, { line, character }, extra) => [...new Set(words)].map((w) => ({
59
75
  label: w,
60
76
  kind,
61
77
  textEdit: {
@@ -63,38 +79,19 @@ const getCompletion = (words, kind, mt, { line, character }) => [...new Set(word
63
79
  start: { line, character: character - mt.length },
64
80
  end: { line, character },
65
81
  },
66
- newText: w,
82
+ newText: w + (extra ?? ''),
67
83
  },
68
84
  }));
69
85
  /**
70
- * Create the URL of a page.
71
- * @param page page name
72
- * @param ns namespace
73
- * @throws `RangeError` Invalid page name.
74
- * @throws `Error` Article path is not set.
75
- */
76
- const getUrl = (page, ns) => {
77
- const { title, fragment, valid } = index_1.default.normalizeTitle(page, ns), { articlePath } = index_1.default.getConfig();
78
- /* istanbul ignore next */
79
- if (!valid) {
80
- throw new RangeError('Invalid page name.');
81
- }
82
- else if (!articlePath) {
83
- throw new Error('Article path is not set.');
84
- }
85
- const encoded = encodeURIComponent(title) + (fragment === undefined ? '' : `#${encodeURIComponent(fragment)}`);
86
- return articlePath.includes('$1')
87
- ? articlePath.replace('$1', encoded)
88
- : articlePath + (articlePath.endsWith('/') ? '' : '/') + encoded;
89
- };
90
- /**
91
- * Get the token at the position from a word.
86
+ * Get the caret position at the position from a word.
92
87
  * @param root root token
93
88
  * @param pos position
89
+ * @param pos.line line number
90
+ * @param pos.character character number
94
91
  */
95
- const elementFromWord = (root, pos) => {
96
- const { line, character } = pos, index = root.indexFromPos(line, character), offset = Number(/[\w!#]/u.test(root.toString().charAt(index)));
97
- return root.elementFromIndex(index + offset);
92
+ const caretPositionFromWord = (root, { line, character }) => {
93
+ const index = root.indexFromPos(line, character);
94
+ return root.caretPositionFromIndex(index + Number(/\w/u.test(root.toString().charAt(index))));
98
95
  };
99
96
  /**
100
97
  * Get the attribute of a `<ref>` tag.
@@ -116,6 +113,15 @@ const getRefName = (token) => getRefAttr(token, refTags, nameAttrs);
116
113
  * @param token `group` attribute token
117
114
  */
118
115
  const getRefGroup = (token) => getRefAttr(token, referencesTags, groupAttrs);
116
+ /**
117
+ * Get the attribute of a `<ref>` tag.
118
+ * @param token extension token
119
+ * @param target attribute name
120
+ */
121
+ const getRefTagAttr = (token, target) => {
122
+ const attr = token?.getAttr(target);
123
+ return attr !== true && attr || false;
124
+ };
119
125
  /**
120
126
  * Get the effective name of a token.
121
127
  * @param token
@@ -138,6 +144,18 @@ const getName = (token) => {
138
144
  }
139
145
  };
140
146
  /* NOT FOR BROWSER ONLY */
147
+ /**
148
+ * Get the quick fix data.
149
+ * @param root root token
150
+ * @param fix lint error fix
151
+ * @param preferred whether it is a preferred fix
152
+ */
153
+ const getQuickFix = (root, fix, preferred = false) => ({
154
+ range: createRange(root, ...fix.range),
155
+ newText: fix.text,
156
+ title: `${preferred ? 'Fix' : 'Suggestion'}: ${fix.desc}`,
157
+ fix: preferred,
158
+ });
141
159
  /**
142
160
  * Get the end position of a section.
143
161
  * @param section section
@@ -180,7 +198,7 @@ class LanguageService {
180
198
  * - 否则开始新的解析
181
199
  */
182
200
  async #queue(text) {
183
- text = text.replace(/\r$/gmu, '');
201
+ text = (0, string_1.tidy)(text);
184
202
  if (this.#text === text && this.#config === index_1.default.config && !this.#running) {
185
203
  return this.#done;
186
204
  }
@@ -237,7 +255,7 @@ class LanguageService {
237
255
  if (type !== 'attr-value' && !isPlain(childNodes)) {
238
256
  return [];
239
257
  }
240
- return childNodes.filter((child) => child.type === 'text')
258
+ return childNodes.filter((child) => child.type === 'text').reverse()
241
259
  .flatMap(child => {
242
260
  const parts = (0, common_1.splitColors)(child.data, hsl).filter(([, , , isColor]) => isColor);
243
261
  if (parts.length === 0) {
@@ -277,7 +295,7 @@ class LanguageService {
277
295
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions, es-x/no-regexp-unicode-property-escapes
278
296
  /(?:<\/?(\w*)|(\{{2,4}|\[\[)\s*([^|{}<>[\]\s][^|{}<>[\]#]*)?|(__(?:(?!__)[\p{L}\d_])*)|(?<!\[)\[([a-z:/]*)|\[\[\s*(?:file|image)\s*:[^[\]{}<>]+\|([^[\]{}<>|=]*)|<(\w+)(?:\s(?:[^<>{}|=\s]+(?:\s*=\s*(?:[^\s"']\S*|(["']).*?\8))?(?=\s))*)?\s(\w*))$/iu;
279
297
  const re = new RegExp('(?:' // eslint-disable-line prefer-template
280
- + String.raw `<\/?(\w*)` // tag
298
+ + String.raw `<(\/?\w*)` // tag
281
299
  + '|'
282
300
  + String.raw `(\{{2,4}|\[\[)\s*([^|{}<>[\]\s][^|{}<>[\]#]*)?` // braces and brackets
283
301
  + '|'
@@ -315,9 +333,10 @@ class LanguageService {
315
333
  */
316
334
  async provideCompletionItems(text, position) {
317
335
  this.#checkSignature();
318
- const { re, allTags, functions, switches, protocols, params, tags, ext } = this.#prepareCompletionConfig(), { line, character } = position, mt = re.exec(text.split(/\r?\n/u, line + 1)[line]?.slice(0, character) ?? '');
336
+ const { re, allTags, functions, switches, protocols, params, tags, ext } = this.#prepareCompletionConfig(), { line, character } = position, curLine = text.split(/\r?\n/u, line + 1)[line], mt = re.exec(curLine?.slice(0, character) ?? '');
319
337
  if (mt?.[1] !== undefined) { // tag
320
- return getCompletion(allTags, 'Class', mt[1], position);
338
+ const closing = mt[1].startsWith('/');
339
+ return getCompletion(allTags, 'Class', mt[1].slice(closing ? 1 : 0), position, closing && !curLine?.slice(character).trim().startsWith('>') ? '>' : '');
321
340
  }
322
341
  else if (mt?.[4]) { // behavior switch
323
342
  return getCompletion(switches, 'Constant', mt[4], position);
@@ -326,34 +345,53 @@ class LanguageService {
326
345
  return getCompletion(protocols, 'Reference', mt[5], position);
327
346
  }
328
347
  const root = await this.#queue(text);
348
+ let cur;
329
349
  if (mt?.[2]) {
350
+ cur = root.elementFromPoint(mt.index + mt[2].length - 1, line);
330
351
  const match = mt[3] ?? '';
331
352
  if (mt[2] === '{{{') { // argument
332
- return getCompletion(root.querySelectorAll('arg').map(({ name }) => name).filter(Boolean), 'Variable', match, position);
353
+ return getCompletion(root.querySelectorAll('arg').filter(token => token.name && token !== cur)
354
+ .map(({ name }) => name), 'Variable', match, position);
333
355
  }
334
356
  const colon = match.startsWith(':'), str = colon ? match.slice(1).trimStart() : match;
335
357
  return mt[2] === '[['
336
358
  ? getCompletion(// link
337
- root.querySelectorAll('link,file,category,redirect-target').map(({ name }) => name), 'Folder', str, position)
359
+ root.querySelectorAll('link,file,category,redirect-target').filter(token => token !== cur).map(({ name }) => name), 'Folder', str, position)
338
360
  : [
339
361
  ...getCompletion(functions, 'Function', match, position),
340
362
  ...match.startsWith('#')
341
363
  ? []
342
- : getCompletion(root.querySelectorAll('template')
343
- .map(({ name }) => colon ? name : name.replace(/^Template:/u, '')), 'Folder', str, position),
364
+ : getCompletion(root.querySelectorAll('template').filter(token => token !== cur)
365
+ .map(token => {
366
+ const { name } = token;
367
+ if (colon) {
368
+ return name;
369
+ }
370
+ const { ns } = token.getAttribute('title');
371
+ if (ns === 0) {
372
+ return `:${name}`;
373
+ }
374
+ return ns === 10 ? name.slice(9) : name;
375
+ }), 'Folder', str, position),
344
376
  ];
345
377
  }
346
- const token = root.elementFromPoint(character, line), { type, parentNode } = token;
378
+ let type, parentNode;
379
+ if (mt?.[7] === undefined) {
380
+ cur = root.elementFromPoint(character, line);
381
+ ({ type, parentNode } = cur);
382
+ }
347
383
  if (mt?.[6] !== undefined || type === 'image-parameter') { // image parameter
348
- const match = mt?.[6]?.trimStart()
349
- ?? this.#text.slice(token.getAbsoluteIndex(), root.indexFromPos(position.line, position.character)).trimStart();
384
+ const index = root.indexFromPos(line, character), match = mt?.[6]?.trimStart()
385
+ ?? this.#text.slice(cur.getAbsoluteIndex(), index).trimStart(), equal = this.#text.charAt(index) === '=';
350
386
  return [
351
- ...getCompletion(params, 'Property', match, position),
352
- ...getCompletion(root.querySelectorAll('image-parameter#width').map(width => width.text()), 'Unit', match, position),
387
+ ...getCompletion(params, 'Property', match, position)
388
+ .filter(({ label }) => !equal || !/[= ]$/u.test(label)),
389
+ ...getCompletion(root.querySelectorAll('image-parameter#width').filter(token => token !== cur)
390
+ .map(width => width.text()), 'Unit', match, position),
353
391
  ];
354
392
  }
355
393
  else if (mt?.[7] !== undefined || type === 'attr-key') { // attribute key
356
- const tag = mt?.[7]?.toLowerCase() ?? parentNode.tag, key = mt?.[9] ?? token.toString();
394
+ const tag = mt?.[7]?.toLowerCase() ?? parentNode.tag, key = mt?.[9] ?? cur.toString();
357
395
  if (!tags.has(tag)) {
358
396
  return undefined;
359
397
  }
@@ -372,12 +410,27 @@ class LanguageService {
372
410
  ...getCompletion(['xmlns:'], 'Interface', key, position),
373
411
  ];
374
412
  }
375
- else if ((type === 'parameter-key' || type === 'parameter-value' && parentNode.anon)
376
- && parentNode.parentNode.type === 'template') { // parameter key
377
- const key = token.toString().trimStart();
413
+ else if (type === 'parameter-key' || type === 'parameter-value' && parentNode.anon) {
414
+ // parameter key
415
+ const transclusion = parentNode.parentNode;
416
+ if (transclusion.type === 'magic-word' && transclusion.name !== 'invoke') {
417
+ return undefined;
418
+ }
419
+ const key = cur.toString().trimStart(), [module, func] = transclusion.type === 'magic-word' ? transclusion.getModule() : [];
378
420
  return key
379
- ? getCompletion(root.querySelectorAll('parameter').filter(({ anon, parentNode: parent }) => !anon && parent.type === 'template'
380
- && parent.name === parentNode.parentNode.name).map(({ name }) => name), 'Variable', key, position)
421
+ ? getCompletion(root.querySelectorAll('parameter').filter(token => {
422
+ if (token === parentNode
423
+ || token.anon
424
+ || token.parentNode.type !== transclusion.type
425
+ || token.parentNode.name !== transclusion.name) {
426
+ return false;
427
+ }
428
+ else if (transclusion.type === 'template') {
429
+ return true;
430
+ }
431
+ const [m, f] = token.parentNode.getModule();
432
+ return m === module && f === func;
433
+ }).map(({ name }) => name), 'Variable', key, position, type === 'parameter-value' ? '=' : '')
381
434
  : undefined;
382
435
  }
383
436
  return undefined;
@@ -400,86 +453,41 @@ class LanguageService {
400
453
  source: 'WikiLint',
401
454
  code: rule,
402
455
  message,
456
+ /* NOT FOR BROWSER ONLY */
403
457
  data: [
404
- ...fix
405
- ? [
406
- {
407
- range: createRange(root, ...fix.range),
408
- newText: fix.text,
409
- title: `Fix: ${fix.desc}`,
410
- fix: true,
411
- },
412
- ]
413
- : [],
414
- ...suggestions
415
- ? suggestions.map(({ range, text, desc }) => ({
416
- range: createRange(root, ...range),
417
- newText: text,
418
- title: `Suggestion: ${desc}`,
419
- fix: false,
420
- }))
421
- : [],
458
+ ...fix ? [getQuickFix(root, fix, true)] : [],
459
+ ...suggestions ? suggestions.map(suggestion => getQuickFix(root, suggestion)) : [],
422
460
  ],
423
461
  }));
424
462
  }
425
- async #provideFoldingRangesOrDocumentSymbols(text, fold = true) {
463
+ /**
464
+ * 提供折叠范围
465
+ * @param text 源代码
466
+ */
467
+ async provideFoldingRanges(text) {
426
468
  this.#checkSignature();
427
- const ranges = [], symbols = [],
428
- /* NOT FOR BROWSER ONLY */
429
- names = new Set(), sections = new Array(6),
430
- /* NOT FOR BROWSER ONLY END */
431
- root = await this.#queue(text), lines = root.getLines(), { length } = lines, levels = new Array(6), tokens = root.querySelectorAll(fold ? 'heading-title,table,template,magic-word' : 'heading-title');
469
+ const root = await this.#queue(text), { length } = root.getLines(), ranges = [], levels = new Array(6), tokens = root.querySelectorAll('heading-title,table,template,magic-word');
470
+ for (const token of [...tokens].reverse()) { // 提高 getBoundingClientRect 的性能
471
+ token.getRelativeIndex();
472
+ }
432
473
  for (const token of tokens) {
433
- const { top, height,
434
- /* NOT FOR BROWSER ONLY */
435
- left, width, } = token.getBoundingClientRect();
474
+ const { top, height } = token.getBoundingClientRect();
436
475
  if (token.type === 'heading-title') {
437
476
  const { level } = token.parentNode;
438
- if (fold) {
439
- for (let i = level - 1; i < 6; i++) {
440
- const startLine = levels[i];
441
- if (startLine !== undefined && startLine < top - 1) {
442
- ranges.push({
443
- startLine,
444
- endLine: top - 1,
445
- kind: 'region',
446
- });
447
- }
448
- levels[i] = undefined;
477
+ for (let i = level - 1; i < 6; i++) {
478
+ const startLine = levels[i];
479
+ if (startLine !== undefined && startLine < top - 1) {
480
+ ranges.push({
481
+ startLine,
482
+ endLine: top - 1,
483
+ kind: 'region',
484
+ });
449
485
  }
450
- levels[level - 1] = top + height - 1; // 从标题的最后一行开始折叠
451
- /* NOT FOR BROWSER ONLY */
452
- }
453
- else {
454
- for (let i = level - 1; i < 6; i++) {
455
- getSectionEnd(sections[i], lines, top - 1);
456
- sections[i] = undefined;
457
- }
458
- const section = token.text().trim() || ' ', name = names.has(section)
459
- ? new Array(names.size).fill('').map((_, i) => `${section.trim()}_${i + 2}`)
460
- .find(s => !names.has(s))
461
- : section, container = sections.slice(0, level - 1).reverse().find(Boolean), selectionRange = {
462
- start: { line: top, character: left - level },
463
- end: (0, lint_1.getEndPos)(top, left, height, width + level),
464
- }, info = {
465
- name,
466
- kind: 15,
467
- range: { start: selectionRange.start },
468
- selectionRange,
469
- };
470
- names.add(name);
471
- sections[level - 1] = info;
472
- if (container) {
473
- container.children ??= [];
474
- container.children.push(info);
475
- }
476
- else {
477
- symbols.push(info);
478
- }
479
- /* NOT FOR BROWSER ONLY END */
486
+ levels[i] = undefined;
480
487
  }
488
+ levels[level - 1] = top + height - 1; // 从标题的最后一行开始折叠
481
489
  }
482
- else if (fold && height > 2) {
490
+ else if (height > 2) {
483
491
  ranges.push({
484
492
  startLine: top, // 从表格或模板的第一行开始折叠
485
493
  endLine: top + height - 2,
@@ -487,32 +495,16 @@ class LanguageService {
487
495
  });
488
496
  }
489
497
  }
490
- if (fold) {
491
- for (const startLine of levels) {
492
- if (startLine !== undefined && startLine < length - 1) {
493
- ranges.push({
494
- startLine,
495
- endLine: length - 1,
496
- kind: 'region',
497
- });
498
- }
499
- }
500
- /* NOT FOR BROWSER ONLY */
501
- }
502
- else {
503
- for (const section of sections) {
504
- getSectionEnd(section, lines, length - 1);
498
+ for (const startLine of levels) {
499
+ if (startLine !== undefined && startLine < length - 1) {
500
+ ranges.push({
501
+ startLine,
502
+ endLine: length - 1,
503
+ kind: 'region',
504
+ });
505
505
  }
506
- /* NOT FOR BROWSER ONLY END */
507
506
  }
508
- return fold ? ranges : symbols;
509
- }
510
- /**
511
- * 提供折叠范围
512
- * @param text 源代码
513
- */
514
- async provideFoldingRanges(text) {
515
- return this.#provideFoldingRangesOrDocumentSymbols(text);
507
+ return ranges;
516
508
  }
517
509
  /**
518
510
  * 提供链接
@@ -521,109 +513,79 @@ class LanguageService {
521
513
  async provideLinks(text) {
522
514
  this.#checkSignature();
523
515
  /^(?:http:\/\/|\/\/)/iu; // eslint-disable-line @typescript-eslint/no-unused-expressions
524
- const protocolRegex = new RegExp(`^(?:${index_1.default.getConfig().protocol}|//)`, 'iu'), selector = 'link-target,template-name,invoke-module,magic-link,ext-link-url,free-ext-link,attr-value,'
525
- + 'image-parameter#link';
526
- return (await this.#queue(text)).querySelectorAll(selector).flatMap((token) => {
516
+ const { articlePath, protocol } = index_1.default.getConfig(), absolute = articlePath?.includes('//'), protocolRegex = new RegExp(`^(?:${protocol}|//)`, 'iu');
517
+ return (await this.#queue(text))
518
+ .querySelectorAll(`magic-link,ext-link-url,free-ext-link,attr-value,image-parameter#link${absolute ? ',link-target,template-name,invoke-module' : ''}`).reverse()
519
+ .map((token) => {
527
520
  const { type, parentNode, firstChild, lastChild, childNodes } = token, { name, tag } = parentNode;
528
521
  if (!(type !== 'attr-value'
529
522
  || name === 'src' && ['templatestyles', 'img'].includes(tag)
530
523
  || name === 'cite' && ['blockquote', 'del', 'ins', 'q'].includes(tag))
531
524
  || !isPlain(childNodes)) {
532
- return [];
525
+ return false;
526
+ }
527
+ let target = childNodes.filter((node) => node.type === 'text')
528
+ .map(({ data }) => data).join('').trim();
529
+ if (!target) {
530
+ return false;
533
531
  }
534
- let target = type === 'image-parameter'
535
- ? element_1.AstElement.prototype.toString.call(token, true).trim()
536
- : token.toString(true).trim();
537
532
  try {
538
- if (type === 'magic-link') {
539
- if (target.startsWith('ISBN')) {
540
- target = getUrl(`Special:Booksources/${target.slice(4).replace(/[\p{Zs}\t-]/gu, '')}`);
541
- }
542
- else {
543
- target = target.startsWith('RFC')
544
- ? `https://tools.ietf.org/html/rfc${target.slice(3).trim()}`
545
- : `https://pubmed.ncbi.nlm.nih.gov/${target.slice(4).trim()}`;
533
+ if (token.is('magic-link')
534
+ || token.is('ext-link-url')
535
+ || token.is('free-ext-link')) {
536
+ target = token.getUrl(articlePath);
537
+ }
538
+ else if (type === 'link-target' && (parentNode.is('link')
539
+ || parentNode.is('redirect-target')
540
+ || parentNode.is('category'))) {
541
+ if (target.startsWith('/')) {
542
+ return false;
546
543
  }
544
+ target = parentNode.link.getUrl(articlePath);
547
545
  }
548
- else if (['link-target', 'template-name', 'invoke-module'].includes(type)
546
+ else if (type === 'template-name') {
547
+ target = parentNode.getAttribute('title').getUrl(articlePath);
548
+ }
549
+ else if (['link-target', 'invoke-module'].includes(type)
549
550
  || type === 'attr-value' && name === 'src' && tag === 'templatestyles'
550
551
  || type === 'image-parameter' && !protocolRegex.test(target)) {
551
- if (target.startsWith('/')) {
552
- return [];
552
+ if (!absolute || target.startsWith('/')) {
553
+ return false;
553
554
  }
554
555
  let ns = 0;
555
- if (type === 'template-name' || type === 'attr-value') {
556
+ if (type === 'attr-value') {
556
557
  ns = 10;
557
558
  }
558
559
  else if (type === 'invoke-module') {
559
560
  ns = 828;
560
561
  }
561
- target = getUrl(target, ns);
562
+ const title = index_1.default.normalizeTitle(target, ns);
563
+ /* istanbul ignore if */
564
+ if (!title.valid) {
565
+ return false;
566
+ }
567
+ target = title.getUrl();
562
568
  }
563
- if (target.startsWith('//')) {
569
+ if (typeof target === 'string' && target.startsWith('//')) {
564
570
  target = `https:${target}`;
565
571
  }
566
572
  target = new URL(target).href;
567
573
  if (type === 'image-parameter') {
568
- const rect = firstChild.getBoundingClientRect(), { top, left, height, width } = lastChild.getBoundingClientRect();
569
- return [
570
- {
571
- range: {
572
- start: { line: rect.top, character: rect.left },
573
- end: (0, lint_1.getEndPos)(top, left, height, width),
574
- },
575
- target,
574
+ const { top, left, height, width } = lastChild.getBoundingClientRect(), rect = firstChild.getBoundingClientRect();
575
+ return {
576
+ range: {
577
+ start: { line: rect.top, character: rect.left },
578
+ end: (0, lint_1.getEndPos)(top, left, height, width),
576
579
  },
577
- ];
580
+ target,
581
+ };
578
582
  }
579
- return [{ range: createNodeRange(token), target }];
583
+ return { range: createNodeRange(token), target };
580
584
  }
581
585
  catch {
582
- return [];
583
- }
584
- });
585
- }
586
- async #provideReferencesOrDefinition(text, position, usage) {
587
- this.#checkSignature();
588
- const renameTypes = [
589
- 'arg-name',
590
- 'template-name',
591
- 'magic-word-name',
592
- 'link-target',
593
- 'parameter-key',
594
- ], types = [
595
- 'ext',
596
- 'html',
597
- 'attr-key',
598
- 'image-parameter',
599
- 'heading-title',
600
- 'heading',
601
- ...renameTypes,
602
- ], root = await this.#queue(text), node = elementFromWord(root, position), { type } = node, refName = getRefName(node), refGroup = getRefGroup(node);
603
- if (usage === 2 && type === 'parameter-key' && /^[1-9]\d*$/u.test(node.parentNode.name)
604
- || !refName && (usage === 1
605
- || !refGroup && (usage === 0
606
- ? !types.includes(type)
607
- : !renameTypes.includes(type)
608
- || type === 'link-target' && !['link', 'redirect-target'].includes(node.parentNode.type)))) {
609
- return undefined;
610
- }
611
- else if (usage === 2) {
612
- return createNodeRange(node);
613
- }
614
- const name = getName(node), refs = root.querySelectorAll(type === 'heading-title' ? 'heading' : type).filter(token => {
615
- const { name: n, parentNode } = token.parentNode;
616
- if (usage === 1) {
617
- return getRefName(token) === refName
618
- && n === 'name' && parentNode.parentNode.innerText;
586
+ return false;
619
587
  }
620
- return type === 'attr-value'
621
- ? getRefName(token) === refName || getRefGroup(token) === refGroup
622
- : getName(token) === name;
623
- }).map((token) => ({
624
- range: createNodeRange(token.type === 'parameter-key' ? token.parentNode : token),
625
- }));
626
- return refs.length === 0 ? undefined : refs;
588
+ }).filter(Boolean);
627
589
  }
628
590
  /**
629
591
  * 提供引用
@@ -631,53 +593,87 @@ class LanguageService {
631
593
  * @param position 位置
632
594
  */
633
595
  async provideReferences(text, position) {
634
- return this.#provideReferencesOrDefinition(text, position, 0);
596
+ this.#checkSignature();
597
+ const root = await this.#queue(text), { offsetNode, offset } = caretPositionFromWord(root, position), element = offsetNode.type === 'text' ? offsetNode.parentNode : offsetNode, node = offset === 0 && (element.type === 'ext-attr-dirty' || element.type === 'html-attr-dirty')
598
+ ? element.parentNode.parentNode
599
+ : element, { type } = node, refName = getRefName(node), refGroup = getRefGroup(node);
600
+ if (!refName && !refGroup && !referenceTypes.has(type)) {
601
+ return undefined;
602
+ }
603
+ const name = getName(node), refs = root.querySelectorAll(type === 'heading-title' ? 'heading' : type).filter(token => type === 'attr-value'
604
+ ? getRefName(token) === refName || getRefGroup(token) === refGroup
605
+ : getName(token) === name).map((token) => ({
606
+ range: createNodeRange(token.type === 'parameter-key' ? token.parentNode : token),
607
+ }));
608
+ return refs.length === 0 ? undefined : refs;
635
609
  }
636
610
  /**
637
611
  * 提供定义
638
612
  * @param text 源代码
639
- * @param position 位置
613
+ * @param pos 位置
614
+ * @param pos.line 行号
615
+ * @param pos.character 列号
640
616
  */
641
- async provideDefinition(text, position) {
642
- return this.#provideReferencesOrDefinition(text, position, 1);
617
+ async provideDefinition(text, { line, character }) {
618
+ this.#checkSignature();
619
+ const root = await this.#queue(text), node = root.elementFromPoint(character, line), ext = node.is('ext') && node.name === 'ref' ? node : node.closest('ext#ref'), refName = getRefTagAttr(ext, 'name');
620
+ if (!refName) {
621
+ return undefined;
622
+ }
623
+ const refGroup = getRefTagAttr(ext, 'group'), refs = root.querySelectorAll('ext#ref').filter(token => token.innerText
624
+ && getRefTagAttr(token, 'name') === refName
625
+ && getRefTagAttr(token, 'group') === refGroup).map(({ lastChild }) => ({
626
+ range: createNodeRange(lastChild),
627
+ }));
628
+ return refs.length === 0 ? undefined : refs;
643
629
  }
644
630
  /**
645
631
  * 提供变量更名准备
646
632
  * @param text 源代码
647
- * @param position 位置
633
+ * @param pos 位置
634
+ * @param pos.line 行号
635
+ * @param pos.character 列号
648
636
  */
649
- async resolveRenameLocation(text, position) {
650
- return this.#provideReferencesOrDefinition(text, position, 2);
637
+ async resolveRenameLocation(text, { line, character }) {
638
+ this.#checkSignature();
639
+ const root = await this.#queue(text), node = root.elementFromPoint(character, line), { type } = node, refName = getRefName(node), refGroup = getRefGroup(node);
640
+ return !refName && !refGroup && (!renameTypes.has(type)
641
+ || type === 'parameter-key' && /^[1-9]\d*$/u.test(node.parentNode.name)
642
+ || type === 'link-target' && !['link', 'redirect-target'].includes(node.parentNode.type))
643
+ ? undefined
644
+ : createNodeRange(node);
651
645
  }
652
646
  /**
653
647
  * 变量更名
654
648
  * @param text 源代码
655
- * @param position 位置
649
+ * @param pos 位置
650
+ * @param pos.line 行号
651
+ * @param pos.character 列号
656
652
  * @param newName 新名称
657
653
  */
658
- async provideRenameEdits(text, position, newName) {
654
+ async provideRenameEdits(text, { line, character }, newName) {
659
655
  this.#checkSignature();
660
- const root = await this.#queue(text), node = elementFromWord(root, position), { type } = node, refName = getRefName(node), refGroup = getRefGroup(node);
661
- const name = getName(node), refs = root.querySelectorAll(type).filter(token => {
656
+ const root = await this.#queue(text), node = root.elementFromPoint(character, line), { type } = node, refName = getRefName(node), refNameGroup = refName && getRefTagAttr(node.parentNode.parentNode, 'group'), refGroup = getRefGroup(node), name = getName(node), refs = root.querySelectorAll(type).filter(token => {
662
657
  const { type: t } = token.parentNode;
663
658
  if (type === 'link-target' && t !== 'link' && t !== 'redirect-target') {
664
659
  return false;
665
660
  }
666
661
  return type === 'attr-value'
667
- ? getRefName(token) === refName || getRefGroup(token) === refGroup
662
+ ? getRefGroup(token) === refGroup
663
+ || getRefName(token) === refName
664
+ && getRefTagAttr(token.parentNode.parentNode, 'group') === refNameGroup
668
665
  : getName(token) === name;
669
666
  });
670
- if (refs.length === 0) {
671
- return undefined;
672
- }
673
- return {
674
- changes: {
675
- '': refs.map((ref) => ({
676
- range: createNodeRange(ref),
677
- newText: newName,
678
- })),
679
- },
680
- };
667
+ return refs.length === 0
668
+ ? undefined
669
+ : {
670
+ changes: {
671
+ '': refs.map((ref) => ({
672
+ range: createNodeRange(ref),
673
+ newText: newName,
674
+ })),
675
+ },
676
+ };
681
677
  }
682
678
  /**
683
679
  * 检索解析器函数
@@ -697,14 +693,33 @@ class LanguageService {
697
693
  return undefined;
698
694
  }
699
695
  this.#checkSignature();
700
- const token = elementFromWord(await this.#queue(text), position);
701
- let info, f;
696
+ const root = await this.#queue(text), { offsetNode, offset } = caretPositionFromWord(root, position), token = offsetNode.type === 'text' ? offsetNode.parentNode : offsetNode, { type, parentNode, length, name } = token;
697
+ let info, f, range;
702
698
  if (token.is('double-underscore')) {
699
+ if (offset === 0 && token.getBoundingClientRect().left > position.character) {
700
+ return undefined;
701
+ }
703
702
  info = this.data.behaviorSwitches.find(({ aliases }) => aliases.includes(token.innerText.toLowerCase()));
704
703
  }
705
- else if (token.type === 'magic-word-name') {
706
- info = this.#getParserFunction(token.parentNode.name);
707
- f = token.text().trim();
704
+ else if (type === 'magic-word-name') {
705
+ info = this.#getParserFunction(parentNode.name);
706
+ f = token.toString(true).trim();
707
+ }
708
+ else if (token.is('magic-word') && length === 1 && !token.modifier) {
709
+ info = this.#getParserFunction(name);
710
+ f = token.firstChild.toString(true).trim();
711
+ }
712
+ else if ((token.is('magic-word') || token.is('template'))
713
+ && token.modifier && offset >= 2 && token.getRelativeIndex(0) > offset) {
714
+ f = token.modifier.trim().slice(0, -1);
715
+ info = this.#getParserFunction(f.toLowerCase());
716
+ if (info) {
717
+ const aIndex = token.getAbsoluteIndex();
718
+ range = {
719
+ start: positionAt(root, aIndex + 2),
720
+ end: positionAt(root, aIndex + token.modifier.trimEnd().length + 1),
721
+ };
722
+ }
708
723
  }
709
724
  return info && {
710
725
  contents: {
@@ -714,7 +729,7 @@ class LanguageService {
714
729
  : '')
715
730
  + info.description,
716
731
  },
717
- range: createNodeRange(token),
732
+ range: range ?? createNodeRange(token),
718
733
  };
719
734
  }
720
735
  /**
@@ -732,32 +747,41 @@ class LanguageService {
732
747
  throw new Error('This is a regular language server!');
733
748
  }
734
749
  this.#signature = true;
735
- const { line, character } = position, curLine = text.split(/\r?\n/u, line + 1)[line], { lastChild } = await this.#queue(`${curLine.slice(0, character + /^[^{}<]*/u.exec(curLine.slice(character))[0].length)}}}`), { type, name, childNodes, firstChild } = lastChild;
736
- if (type !== 'magic-word') {
750
+ const { line, character } = position, curLine = text.split(/\r?\n/u, line + 1)[line], { lastChild } = await this.#queue(`${curLine.slice(0, character + /^[^{}<]*/u.exec(curLine.slice(character))[0].length)}}}`);
751
+ if (!lastChild.is('magic-word') || lastChild.length === 1) {
737
752
  return undefined;
738
753
  }
739
- const info = this.#getParserFunction(name);
754
+ const { name, childNodes, firstChild } = lastChild, info = this.#getParserFunction(name);
740
755
  if (!info?.signatures) {
741
756
  return undefined;
742
757
  }
743
- const f = firstChild.text().trim(), n = childNodes.length - 1, start = lastChild.getAbsoluteIndex();
744
- let activeParameter = childNodes.findIndex(child => child.getRelativeIndex() > character - start) - 2;
745
- if (activeParameter === -3) {
746
- activeParameter = n - 1;
747
- }
748
- const signatures = info.signatures.filter(params => (params.length >= n || params[params.length - 1]?.rest)
758
+ const n = childNodes.length - 1, candidates = info.signatures.filter(params => (params.length >= n || params[params.length - 1]?.rest)
749
759
  && params.every(({ label, const: c }, i) => {
750
- const p = c && i < n && childNodes[i + 1]?.text().trim();
751
- return !p || label.startsWith(p) || label.startsWith(p.toLowerCase());
752
- })).map((params) => ({
753
- label: `{{${f}${params.length === 0 ? '' : ':'}${params.map(({ label }) => label).join('|')}}}`,
754
- parameters: params.map(({ label, const: c }) => ({
755
- label,
756
- ...c ? { documentation: 'Predefined parameter' } : undefined,
760
+ const p = c && i < n && childNodes[i + 1]?.toString(true).trim();
761
+ return !p || label.toLowerCase().includes(p.toLowerCase());
762
+ }));
763
+ if (candidates.length === 0) {
764
+ return undefined;
765
+ }
766
+ let j = -1;
767
+ for (let cur = lastChild.getAbsoluteIndex() + lastChild.getAttribute('padding'); j < n; j++) {
768
+ cur += childNodes[j + 1].toString().length + 1;
769
+ if (cur > character) {
770
+ break;
771
+ }
772
+ }
773
+ const f = firstChild.toString(true).trim();
774
+ return {
775
+ signatures: candidates.map((params) => ({
776
+ label: `{{${f}${params.length === 0 ? '' : ':'}${params.map(({ label }) => label).join('|')}}}`,
777
+ parameters: params.map(({ label, const: c }) => ({
778
+ label,
779
+ ...c ? { documentation: 'Predefined parameter' } : undefined,
780
+ })),
781
+ ...params.length < n ? { activeParameter: Math.min(j, params.length - 1) } : undefined,
757
782
  })),
758
- ...params.length < n ? { activeParameter: Math.min(activeParameter, params.length - 1) } : undefined,
759
- }));
760
- return { signatures, activeParameter };
783
+ activeParameter: j,
784
+ };
761
785
  }
762
786
  /**
763
787
  * 提供 CodeLens
@@ -769,7 +793,7 @@ class LanguageService {
769
793
  for (const template of root.querySelectorAll('template,magic-word#invoke')) {
770
794
  const { type, childNodes } = template;
771
795
  hints.push(...childNodes.slice(type === 'template' ? 1 : 3).filter(({ anon }) => anon)
772
- .map((parameter) => ({
796
+ .reverse().map((parameter) => ({
773
797
  position: positionAt(root, parameter.getAbsoluteIndex()),
774
798
  label: `${parameter.name}=`,
775
799
  kind: 2,
@@ -796,7 +820,46 @@ class LanguageService {
796
820
  * @param text 源代码
797
821
  */
798
822
  async provideDocumentSymbols(text) {
799
- return this.#provideFoldingRangesOrDocumentSymbols(text, false);
823
+ this.#checkSignature();
824
+ const root = await this.#queue(text), lines = root.getLines(), { length } = lines, symbols = [], names = new Set(), sections = new Array(6), tokens = root.querySelectorAll('heading-title');
825
+ for (const token of [...tokens].reverse()) { // 提高 getBoundingClientRect 的性能
826
+ token.getRelativeIndex();
827
+ }
828
+ for (const token of tokens) {
829
+ const { top, height, left, width } = token.getBoundingClientRect();
830
+ if (token.type === 'heading-title') {
831
+ const { level } = token.parentNode;
832
+ for (let i = level - 1; i < 6; i++) {
833
+ getSectionEnd(sections[i], lines, top - 1);
834
+ sections[i] = undefined;
835
+ }
836
+ const section = token.text().trim() || ' ', name = names.has(section)
837
+ ? new Array(names.size).fill('').map((_, i) => `${section.trim()}_${i + 2}`)
838
+ .find(s => !names.has(s))
839
+ : section, container = sections.slice(0, level - 1).reverse().find(Boolean), selectionRange = {
840
+ start: { line: top, character: left - level },
841
+ end: (0, lint_1.getEndPos)(top, left, height, width + level),
842
+ }, info = {
843
+ name,
844
+ kind: 15,
845
+ range: { start: selectionRange.start },
846
+ selectionRange,
847
+ };
848
+ names.add(name);
849
+ sections[level - 1] = info;
850
+ if (container) {
851
+ container.children ??= [];
852
+ container.children.push(info);
853
+ }
854
+ else {
855
+ symbols.push(info);
856
+ }
857
+ }
858
+ }
859
+ for (const section of sections) {
860
+ getSectionEnd(section, lines, length - 1);
861
+ }
862
+ return symbols;
800
863
  }
801
864
  }
802
865
  exports.LanguageService = LanguageService;