wikiparser-node 1.17.1 → 1.18.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.
package/dist/lib/lsp.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.LanguageService = exports.tasks = void 0;
6
+ exports.LanguageService = exports.isAttr = exports.tasks = void 0;
7
7
  const common_1 = require("@bhsd/common");
8
8
  const sharable_1 = require("../util/sharable");
9
9
  const lint_1 = require("../util/lint");
@@ -13,15 +13,19 @@ const index_1 = __importDefault(require("../index"));
13
13
  const constants_1 = require("../util/constants");
14
14
  /* NOT FOR BROWSER END */
15
15
  /* NOT FOR BROWSER ONLY */
16
+ const fs_1 = __importDefault(require("fs"));
16
17
  const path_1 = __importDefault(require("path"));
18
+ const util_1 = __importDefault(require("util"));
19
+ const child_process_1 = require("child_process");
20
+ const crypto_1 = require("crypto");
17
21
  const stylelint_1 = require("@bhsd/common/dist/stylelint");
22
+ const config_1 = __importDefault(require("../bin/config"));
18
23
  const document_1 = require("./document");
19
- /* NOT FOR BROWSER ONLY */
20
24
  /** @see https://www.npmjs.com/package/stylelint-config-recommended */
21
25
  const cssRules = {
22
26
  'block-no-empty': null,
23
27
  'property-no-unknown': null,
24
- }, jsonSelector = document_1.jsonTags.map(s => `ext-inner#${s}`).join();
28
+ }, jsonSelector = document_1.jsonTags.map(s => `ext-inner#${s}`).join(), scores = new Map();
25
29
  /* NOT FOR BROWSER ONLY END */
26
30
  exports.tasks = new WeakMap();
27
31
  const refTags = new Set(['ref']), referencesTags = new Set(['ref', 'references']), nameAttrs = new Set(['name', 'extends', 'follow']), groupAttrs = new Set(['group']), renameTypes = new Set([
@@ -39,11 +43,30 @@ const refTags = new Set(['ref']), referencesTags = new Set(['ref', 'references']
39
43
  'heading',
40
44
  ...renameTypes,
41
45
  ]), plainTypes = new Set(['text', 'comment', 'noinclude', 'include']), cssSelector = ['ext', 'html', 'table'].map(s => `${s}-attr#style`).join();
46
+ /**
47
+ * Check if a token is a plain attribute.
48
+ * @param token
49
+ * @param token.type
50
+ * @param token.parentNode
51
+ * @param token.length
52
+ * @param token.firstChild
53
+ * @param style whether it is a style attribute
54
+ */
55
+ const isAttr = ({ type, parentNode, length, firstChild }, style) => type === 'attr-value' && length === 1 && firstChild.type === 'text'
56
+ && (!style
57
+ || parentNode.name === 'style'
58
+ && Boolean(document_1.cssLSP));
59
+ exports.isAttr = isAttr;
60
+ /**
61
+ * Check if a token is an HTML attribute.
62
+ * @param token
63
+ */
64
+ const isHtmlAttr = (token) => token.type === 'html-attr' || token.type === 'table-attr';
42
65
  /**
43
66
  * Check if all child nodes are plain text or comments.
44
- * @param childNodes child nodes
67
+ * @param token
45
68
  */
46
- const isPlain = (childNodes) => childNodes.every(({ type }) => plainTypes.has(type));
69
+ const isPlain = (token) => token.childNodes.every(({ type }) => plainTypes.has(type));
47
70
  /**
48
71
  * Get the position of a character in the document.
49
72
  * @param root root token
@@ -83,28 +106,39 @@ const createNodeRange = (token) => {
83
106
  * @param pos.line line number
84
107
  * @param pos.character character number
85
108
  * @param extra extra text
109
+ * @param getDoc documentation method
86
110
  */
87
- const getCompletion = (words, kind, mt, { line, character }, extra) => [...new Set(words)].map((w) => ({
88
- label: w,
89
- kind,
90
- textEdit: {
91
- range: {
92
- start: { line, character: character - mt.length },
93
- end: { line, character },
111
+ const getCompletion = (words, kind, mt, { line, character }, extra, getDoc) => [...new Set(words)].map((w) => {
112
+ const doc = getDoc?.(w)?.description;
113
+ return {
114
+ label: w,
115
+ kind,
116
+ textEdit: {
117
+ range: {
118
+ start: { line, character: character - mt.length },
119
+ end: { line, character },
120
+ },
121
+ newText: w + (extra ?? ''),
94
122
  },
95
- newText: w + (extra ?? ''),
96
- },
97
- }));
123
+ ...doc && {
124
+ documentation: {
125
+ kind: 'markdown',
126
+ value: doc,
127
+ },
128
+ },
129
+ };
130
+ });
98
131
  /**
99
132
  * Get the caret position at the position from a word.
100
133
  * @param root root token
134
+ * @param text source code
101
135
  * @param pos position
102
136
  * @param pos.line line number
103
137
  * @param pos.character character number
104
138
  */
105
- const caretPositionFromWord = (root, { line, character }) => {
139
+ const caretPositionFromWord = (root, text, { line, character }) => {
106
140
  const index = root.indexFromPos(line, character);
107
- return root.caretPositionFromIndex(index + Number(/\w/u.test(root.toString().charAt(index))));
141
+ return root.caretPositionFromIndex(index + Number(/\w/u.test(text.charAt(index))));
108
142
  };
109
143
  /**
110
144
  * Get the attribute of a `<ref>` tag.
@@ -170,15 +204,13 @@ const getQuickFix = (root, fix, preferred = false) => ({
170
204
  });
171
205
  /* NOT FOR BROWSER ONLY */
172
206
  /**
173
- * Get the position of a Stylelint error
174
- * @param rect bounding client rect of the token
175
- * @param bottom bottom of the style block
176
- * @param line line number
177
- * @param column column number
207
+ * Correct the position of an error.
208
+ * @param height
209
+ * @param width
210
+ * @param line 0-based line number
211
+ * @param column 0-based column number
178
212
  */
179
- const getStylelintPos = (rect, bottom, line, column) => {
180
- const { top, left, height, width } = rect, start = bottom - height - 1;
181
- line -= start;
213
+ const adjustPos = (height, width, line, column) => {
182
214
  if (line === 0) {
183
215
  line = 1;
184
216
  column = 0;
@@ -187,8 +219,39 @@ const getStylelintPos = (rect, bottom, line, column) => {
187
219
  line = height;
188
220
  column = width;
189
221
  }
222
+ return [line, column];
223
+ };
224
+ /**
225
+ * Get the position of a Stylelint error.
226
+ * @param rect bounding client rect of the token
227
+ * @param bottom bottom of the style block
228
+ * @param line line number
229
+ * @param column column number
230
+ */
231
+ const getStylelintPos = (rect, bottom, line, column) => {
232
+ const { top, left, height, width } = rect, start = bottom - height - 1;
233
+ line -= start;
234
+ [line, column] = adjustPos(height, width, line, column);
190
235
  return (0, lint_1.getEndPos)(top, left, line, column);
191
236
  };
237
+ /**
238
+ * Convert LilyPond errors to VSCode diagnostics.
239
+ * @param root root token
240
+ * @param token `<score>` extension token
241
+ * @param errors LilyPond errors
242
+ */
243
+ const getLilyPondDiagnostics = (root, token, errors) => {
244
+ const { top, left } = root.posFromIndex(token.lastChild.getAbsoluteIndex());
245
+ return errors.map(({ line, col, message }) => {
246
+ const pos = (0, lint_1.getEndPos)(top, left, line, col);
247
+ return {
248
+ range: { start: pos, end: pos },
249
+ severity: 1,
250
+ source: 'LilyPond',
251
+ message,
252
+ };
253
+ });
254
+ };
192
255
  /**
193
256
  * Get the end position of a section.
194
257
  * @param section section
@@ -215,23 +278,49 @@ class LanguageService {
215
278
  #completionConfig;
216
279
  include = true;
217
280
  /** @private */
281
+ config;
282
+ /** @private */
218
283
  data;
284
+ /* NOT FOR BROWSER ONLY */
285
+ lilypond;
286
+ /** @private */
287
+ lilypondData;
288
+ /* NOT FOR BROWSER ONLY END */
219
289
  /** @param uri 任务标识 */
220
290
  constructor(uri) {
221
291
  exports.tasks.set(uri, this);
222
- /* NOT FOR BROWSER ONLY */
223
- Object.defineProperty(this, 'data', {
224
- value: require(path_1.default.join('..', '..', 'data', 'signatures')),
225
- enumerable: false,
292
+ Object.defineProperties(this, {
293
+ config: { enumerable: false },
294
+ /* NOT FOR BROWSER ONLY */
295
+ data: {
296
+ value: require(path_1.default.join('..', '..', 'data', 'signatures')),
297
+ enumerable: false,
298
+ },
299
+ lilypondData: {
300
+ value: require(path_1.default.join('..', '..', 'data', 'ext', 'score')),
301
+ enumerable: false,
302
+ },
226
303
  });
227
304
  }
228
305
  /** @implements */
229
306
  destroy() {
230
307
  Object.setPrototypeOf(this, null);
308
+ /* NOT FOR BROWSER ONLY */
309
+ const dir = path_1.default.join(__dirname, 'lilypond');
310
+ if (fs_1.default.existsSync(dir)) {
311
+ for (const file of fs_1.default.readdirSync(dir)) {
312
+ (async () => {
313
+ try {
314
+ await fs_1.default.promises.unlink(path_1.default.join(dir, file));
315
+ }
316
+ catch { }
317
+ })();
318
+ }
319
+ }
231
320
  }
232
321
  /** 检查解析设置有无更新 */
233
322
  #checkConfig() {
234
- return this.#config === index_1.default.config && this.#include === this.include;
323
+ return this.#config === this.config && this.#include === this.include;
235
324
  }
236
325
  /**
237
326
  * 提交解析任务
@@ -257,10 +346,10 @@ class LanguageService {
257
346
  * - 总是返回最新的解析结果
258
347
  */
259
348
  async #parse() {
260
- const config = index_1.default.getConfig();
261
- this.#config = index_1.default.config;
349
+ this.config ??= index_1.default.getConfig();
350
+ this.#config = this.config;
262
351
  this.#include = this.include;
263
- const text = this.#text, root = await index_1.default.partialParse(text, () => this.#text, this.include, config);
352
+ const text = this.#text, root = await index_1.default.partialParse(text, () => this.#text, this.include, this.config);
264
353
  if (this.#checkConfig() && this.#text === text) {
265
354
  this.#done = root;
266
355
  this.#running = undefined;
@@ -295,10 +384,10 @@ class LanguageService {
295
384
  * - 总是返回最新的解析结果
296
385
  */
297
386
  async #parseSignature() {
298
- const config = index_1.default.getConfig();
299
- this.#config = index_1.default.config;
387
+ this.config ??= index_1.default.getConfig();
388
+ this.#config = this.config;
300
389
  this.#include = this.include;
301
- const text = this.#text2, root = await index_1.default.partialParse(text, () => this.#text2, this.include, config);
390
+ const text = this.#text2, root = await index_1.default.partialParse(text, () => this.#text2, this.include, this.config);
302
391
  if (this.#checkConfig() && this.#text2 === text) {
303
392
  this.#done2 = root;
304
393
  this.#running2 = undefined;
@@ -319,14 +408,38 @@ class LanguageService {
319
408
  */
320
409
  async provideDocumentColors(rgba, text, hsl = true) {
321
410
  const root = await this.#queue(text);
411
+ /* NOT FOR BROWSER ONLY */
412
+ let colors;
413
+ try {
414
+ colors = new RegExp(String.raw `\b${Object.keys((await import('color-name')).default).join('|')}\b`, 'giu');
415
+ }
416
+ catch { }
417
+ /* NOT FOR BROWSER ONLY END */
322
418
  return root.querySelectorAll('attr-value,parameter-value,arg-default').reverse()
323
- .flatMap(({ type, childNodes }) => {
324
- if (type !== 'attr-value' && !isPlain(childNodes)) {
419
+ .flatMap(token => {
420
+ const { type, childNodes,
421
+ /* NOT FOR BROWSER ONLY */
422
+ parentNode, } = token;
423
+ if (type !== 'attr-value' && !isPlain(token)) {
325
424
  return [];
425
+ /* NOT FOR BROWSER ONLY */
326
426
  }
427
+ else if ((0, exports.isAttr)(token, true)) {
428
+ const textDoc = new document_1.EmbeddedCSSDocument(root, token);
429
+ return document_1.cssLSP.findDocumentColors(textDoc, textDoc.styleSheet);
430
+ /* NOT FOR BROWSER ONLY END */
431
+ }
432
+ /* NOT FOR BROWSER ONLY */
433
+ const isStyle = colors && type === 'attr-value' && parentNode.name === 'style';
434
+ /* NOT FOR BROWSER ONLY END */
327
435
  return childNodes.filter((child) => child.type === 'text').reverse()
328
436
  .flatMap(child => {
329
- const parts = (0, common_1.splitColors)(child.data, hsl).filter(([, , , isColor]) => isColor);
437
+ const { data } = child, parts = (0, common_1.splitColors)(data, hsl).filter(([, , , isColor]) => isColor);
438
+ /* NOT FOR BROWSER ONLY */
439
+ if (isStyle) {
440
+ parts.push(...[...data.matchAll(colors)].map(({ index, 0: s }) => [s, index, index + s.length, true]));
441
+ }
442
+ /* NOT FOR BROWSER ONLY END */
330
443
  if (parts.length === 0) {
331
444
  return [];
332
445
  }
@@ -364,8 +477,9 @@ class LanguageService {
364
477
  }
365
478
  /** 准备自动补全设置 */
366
479
  #prepareCompletionConfig() {
367
- if (!this.#completionConfig) {
368
- const { nsid, ext, html, parserFunction: [insensitive, sensitive, ...other], doubleUnderscore, protocol, img, } = index_1.default.getConfig(), tags = new Set([ext, html].flat(2));
480
+ if (!this.#completionConfig || this.#completionConfig[1] !== this.config) {
481
+ this.config ??= index_1.default.getConfig();
482
+ const { nsid, ext, html, parserFunction: [insensitive, sensitive, ...other], doubleUnderscore, protocol, img, } = this.config, tags = new Set([ext, html].flat(2));
369
483
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions, es-x/no-regexp-unicode-property-escapes
370
484
  /(?:<\/?(\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;
371
485
  const re = new RegExp('(?:' // eslint-disable-line prefer-template
@@ -382,23 +496,27 @@ class LanguageService {
382
496
  // attribute key
383
497
  + String.raw `<(\w+)(?:\s(?:[^<>{}|=\s]+(?:\s*=\s*(?:[^\s"']\S*|(["']).*?\8))?(?=\s))*)?\s(\w*)`
384
498
  + ')$', 'iu');
385
- this.#completionConfig = {
386
- re,
387
- ext,
388
- tags,
389
- allTags: [...tags, 'onlyinclude', 'includeonly', 'noinclude'],
390
- functions: [
391
- Object.keys(insensitive),
392
- Array.isArray(sensitive) ? /* istanbul ignore next */ sensitive : Object.keys(sensitive),
393
- other,
394
- ].flat(2),
395
- switches: doubleUnderscore.slice(0, 2).flat().map(w => `__${w}__`),
396
- protocols: protocol.split('|'),
397
- params: Object.keys(img).filter(k => k.endsWith('$1') || !k.includes('$1'))
398
- .map(k => k.replace(/\$1$/u, '')),
399
- };
499
+ this.#completionConfig = [
500
+ {
501
+ re,
502
+ ext,
503
+ tags,
504
+ allTags: [...tags, 'onlyinclude', 'includeonly', 'noinclude'],
505
+ functions: [
506
+ Object.keys(insensitive),
507
+ Array.isArray(sensitive) ? /* istanbul ignore next */ sensitive : Object.keys(sensitive),
508
+ other,
509
+ ].flat(2),
510
+ switches: doubleUnderscore.slice(0, 2).flat().map(w => `__${w}__`),
511
+ protocols: protocol.split('|'),
512
+ params: Object.keys(img)
513
+ .filter(k => k.endsWith('$1') || !k.includes('$1'))
514
+ .map(k => k.replace(/\$1$/u, '')),
515
+ },
516
+ this.config,
517
+ ];
400
518
  }
401
- return this.#completionConfig;
519
+ return this.#completionConfig[0];
402
520
  }
403
521
  /**
404
522
  * Provide auto-completion
@@ -408,13 +526,25 @@ class LanguageService {
408
526
  * @param position 位置
409
527
  */
410
528
  async provideCompletionItems(text, position) {
411
- 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) ?? '');
529
+ 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) ?? ''), [, , iAlias = {}, sAlias = {}] = this.config.doubleUnderscore;
412
530
  if (mt?.[1] !== undefined) { // tag
413
531
  const closing = mt[1].startsWith('/');
414
532
  return getCompletion(allTags, 'Class', mt[1].slice(closing ? 1 : 0), position, closing && !curLine?.slice(character).trim().startsWith('>') ? '>' : '');
415
533
  }
416
534
  else if (mt?.[4]) { // behavior switch
417
- return getCompletion(switches, 'Constant', mt[4], position);
535
+ return getCompletion(switches, 'Constant', mt[4], position, '', name => {
536
+ if (!this.data) {
537
+ return undefined;
538
+ }
539
+ name = name.slice(2, -2);
540
+ if (name in iAlias) {
541
+ name = iAlias[name];
542
+ }
543
+ else if (name in sAlias) {
544
+ name = sAlias[name];
545
+ }
546
+ return this.#getBehaviorSwitch(name.toLowerCase());
547
+ });
418
548
  }
419
549
  else if (mt?.[5] !== undefined) { // protocol
420
550
  return getCompletion(protocols, 'Reference', mt[5], position);
@@ -428,12 +558,23 @@ class LanguageService {
428
558
  return getCompletion(root.querySelectorAll('arg').filter(token => token.name && token !== cur)
429
559
  .map(({ name }) => name), 'Variable', match, position);
430
560
  }
431
- const colon = match.startsWith(':'), str = colon ? match.slice(1).trimStart() : match;
561
+ const [insensitive, sensitive] = this.config.parserFunction, isOld = Array.isArray(sensitive), colon = match.startsWith(':'), str = colon ? match.slice(1).trimStart() : match;
432
562
  return mt[2] === '[['
433
563
  ? getCompletion(// link
434
564
  root.querySelectorAll('link,file,category,redirect-target').filter(token => token !== cur).map(({ name }) => name), 'Folder', str, position)
435
565
  : [
436
- ...getCompletion(functions, 'Function', match, position),
566
+ ...getCompletion(functions, 'Function', match, position, '', name => {
567
+ if (!this.data) {
568
+ return undefined;
569
+ }
570
+ else if (name in insensitive) {
571
+ name = insensitive[name];
572
+ }
573
+ else if (!isOld && name in sensitive) {
574
+ name = sensitive[name];
575
+ }
576
+ return this.#getParserFunction(name.toLowerCase());
577
+ }),
437
578
  ...match.startsWith('#')
438
579
  ? []
439
580
  : getCompletion(root.querySelectorAll('template').filter(token => token !== cur)
@@ -493,7 +634,7 @@ class LanguageService {
493
634
  if (t === 'magic-word' && n !== 'invoke') {
494
635
  return undefined;
495
636
  }
496
- const index = root.indexFromPos(line, character), key = this.#text.slice(cur.getAbsoluteIndex(), index).trimStart(), [module, func] = t === 'magic-word' ? transclusion.getModule() : [];
637
+ const key = this.#text.slice(cur.getAbsoluteIndex(), root.indexFromPos(line, character)).trimStart(), [module, func] = t === 'magic-word' ? transclusion.getModule() : [];
497
638
  return key
498
639
  ? getCompletion(root.querySelectorAll('parameter').filter(token => {
499
640
  if (token === parentNode
@@ -511,7 +652,7 @@ class LanguageService {
511
652
  : undefined;
512
653
  /* NOT FOR BROWSER ONLY */
513
654
  }
514
- else if (document_1.cssLSP && type === 'attr-value' && parentNode.name === 'style' && cur.length === 1) {
655
+ else if ((0, exports.isAttr)(cur, true)) {
515
656
  const textDoc = new document_1.EmbeddedCSSDocument(root, cur);
516
657
  return document_1.cssLSP.doComplete(textDoc, position, textDoc.styleSheet).items.map((item) => ({
517
658
  ...item,
@@ -524,8 +665,37 @@ class LanguageService {
524
665
  else if (document_1.jsonLSP && type === 'ext-inner' && document_1.jsonTags.includes(cur.name)) {
525
666
  const textDoc = new document_1.EmbeddedJSONDocument(root, cur);
526
667
  return (await document_1.jsonLSP.doComplete(textDoc, position, textDoc.jsonDoc))?.items;
668
+ }
669
+ else if (type === 'ext-inner' && cur.name === 'score') {
670
+ const lang = parentNode.getAttr('lang');
671
+ if (lang !== undefined && lang !== 'lilypond') {
672
+ return undefined;
673
+ }
674
+ const before = this.#text.slice(cur.getAbsoluteIndex(), root.indexFromPos(line, character)), comment = before.lastIndexOf('%');
675
+ if (comment !== -1
676
+ && (before.charAt(comment + 1) === '{' || !before.slice(comment).includes('\n'))) {
677
+ return undefined;
678
+ }
679
+ const word = /\\?\b(?:\w|\b(?:->?|\.)|\bly:)+$/u.exec(curLine.slice(0, character))?.[0];
680
+ if (word) {
681
+ const { lilypondData } = this;
682
+ return word.startsWith('\\')
683
+ ? getCompletion(lilypondData.filter(w => w.startsWith('\\')), 'Function', word, position)
684
+ : [
685
+ ...getCompletion(lilypondData.filter(w => /^[a-z]/u.test(w)), 'Variable', word, position),
686
+ ...getCompletion(lilypondData.filter(w => /^[A-Z]/u.test(w)), 'Class', word, position),
687
+ ];
688
+ }
527
689
  /* NOT FOR BROWSER ONLY END */
528
690
  }
691
+ else if ((0, exports.isAttr)(cur) && isHtmlAttr(parentNode)) {
692
+ const data = lint_1.htmlData.provideValues(parentNode.tag, parentNode.name);
693
+ if (data.length === 0) {
694
+ return undefined;
695
+ }
696
+ const val = this.#text.slice(cur.getAbsoluteIndex(), root.indexFromPos(line, character)).trimStart();
697
+ return getCompletion(data.map(({ name }) => name), 'Value', val, position);
698
+ }
529
699
  return undefined;
530
700
  }
531
701
  /**
@@ -559,13 +729,13 @@ class LanguageService {
559
729
  ],
560
730
  })),
561
731
  /* eslint-disable @stylistic/operator-linebreak */
562
- cssDiagnostics = document_1.stylelint ?
732
+ cssDiagnostics = await document_1.stylelint ?
563
733
  await (async () => {
564
734
  const tokens = this.findStyleTokens();
565
735
  if (tokens.length === 0) {
566
736
  return [];
567
737
  }
568
- const cssErrors = await (0, stylelint_1.styleLint)(await document_1.stylelint, tokens.map(({ type, tag, lastChild }, i) => `${type === 'ext-attr' ? 'div' : tag}#${i}{\n${(0, common_1.sanitizeInlineStyle)(lastChild.toString())}\n}`).join('\n'), cssRules);
738
+ const cssErrors = await (0, stylelint_1.styleLint)((await document_1.stylelint), tokens.map(({ type, tag, lastChild }, i) => `${type === 'ext-attr' ? 'div' : tag}#${i}{\n${(0, common_1.sanitizeInlineStyle)(lastChild.toString())}\n}`).join('\n'), cssRules);
569
739
  if (cssErrors.length === 0) {
570
740
  return [];
571
741
  }
@@ -602,7 +772,58 @@ class LanguageService {
602
772
  })) :
603
773
  [];
604
774
  /* eslint-enable @stylistic/operator-linebreak */
605
- return [diagnostics, cssDiagnostics, jsonDiagnostics].flat(2);
775
+ /* NOT FOR BROWSER ONLY */
776
+ let lilypondDiagnostics = [];
777
+ if (this.lilypond) {
778
+ const tokens = root.querySelectorAll('ext#score').filter(token => {
779
+ const lang = token.getAttr('lang');
780
+ return (lang === undefined || lang === 'lilypond') && token.innerText;
781
+ });
782
+ if (tokens.length > 0) {
783
+ const dir = path_1.default.join(__dirname, 'lilypond');
784
+ if (!fs_1.default.existsSync(dir)) {
785
+ fs_1.default.mkdirSync(dir);
786
+ }
787
+ lilypondDiagnostics = await Promise.all(tokens.map(async (token) => {
788
+ const { innerText } = token, score = `showLastLength = R1${token.getAttr('raw') === undefined ? ` \\score {\n${innerText}\n}` : `\n${innerText}`}`;
789
+ if (scores.has(score)) {
790
+ return getLilyPondDiagnostics(root, token, scores.get(score));
791
+ }
792
+ const hash = (0, crypto_1.createHash)('sha256');
793
+ hash.update(score);
794
+ const file = path_1.default.join(dir, `${hash.digest('hex')}.ly`);
795
+ fs_1.default.writeFileSync(file, score);
796
+ try {
797
+ await util_1.default.promisify(child_process_1.execFile)(this.lilypond, ['-s', '-o', dir, file]);
798
+ scores.set(score, []);
799
+ }
800
+ catch (e) {
801
+ const { stderr } = e;
802
+ if (stderr) {
803
+ const re = new RegExp(String.raw `^${file}:(\d+):(\d+): error: (.+)$`, 'gmu'), lilypondErrors = [...stderr.matchAll(re)].map(([, line, col, msg]) => {
804
+ const { offsetHeight, offsetWidth } = token.lastChild, pos = adjustPos(offsetHeight, offsetWidth, Number(line) - 1, Number(col) - 1);
805
+ return {
806
+ line: pos[0],
807
+ col: pos[1],
808
+ message: msg,
809
+ };
810
+ });
811
+ scores.set(score, lilypondErrors);
812
+ return getLilyPondDiagnostics(root, token, lilypondErrors);
813
+ }
814
+ }
815
+ return [];
816
+ }));
817
+ }
818
+ }
819
+ /* NOT FOR BROWSER ONLY END */
820
+ return [
821
+ diagnostics,
822
+ cssDiagnostics,
823
+ jsonDiagnostics,
824
+ /* NOT FOR BROWSER ONLY */
825
+ lilypondDiagnostics,
826
+ ].flat(2);
606
827
  }
607
828
  /**
608
829
  * Provide folding ranges
@@ -668,8 +889,9 @@ class LanguageService {
668
889
  * @param text source Wikitext / 源代码
669
890
  */
670
891
  async provideLinks(text) {
892
+ this.config ??= index_1.default.getConfig();
671
893
  /^(?:http:\/\/|\/\/)/iu; // eslint-disable-line @typescript-eslint/no-unused-expressions
672
- const { articlePath, protocol } = index_1.default.getConfig(), absolute = articlePath?.includes('//'), protocolRegex = new RegExp(`^(?:${protocol}|//)`, 'iu');
894
+ const { articlePath, protocol } = this.config, absolute = articlePath?.includes('//'), protocolRegex = new RegExp(`^(?:${protocol}|//)`, 'iu');
673
895
  return (await this.#queue(text))
674
896
  .querySelectorAll(`magic-link,ext-link-url,free-ext-link,attr-value,image-parameter#link${absolute ? ',link-target,template-name,invoke-module' : ''}`)
675
897
  .reverse()
@@ -678,7 +900,7 @@ class LanguageService {
678
900
  if (!(type !== 'attr-value'
679
901
  || name === 'src' && ['templatestyles', 'img'].includes(tag)
680
902
  || name === 'cite' && ['blockquote', 'del', 'ins', 'q'].includes(tag))
681
- || !isPlain(childNodes)) {
903
+ || !isPlain(token)) {
682
904
  return false;
683
905
  }
684
906
  let target = childNodes.filter((node) => node.type === 'text')
@@ -718,8 +940,7 @@ class LanguageService {
718
940
  else if (type === 'invoke-module') {
719
941
  ns = 828;
720
942
  }
721
- const title = index_1.default
722
- .normalizeTitle(target, ns, false, undefined, true);
943
+ const title = index_1.default.normalizeTitle(target, ns, false, this.config, true);
723
944
  /* istanbul ignore if */
724
945
  if (!title.valid) {
725
946
  return false;
@@ -756,7 +977,7 @@ class LanguageService {
756
977
  * @param position 位置
757
978
  */
758
979
  async provideReferences(text, position) {
759
- 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')
980
+ const root = await this.#queue(text), { offsetNode, offset } = caretPositionFromWord(root, this.#text, position), element = offsetNode.type === 'text' ? offsetNode.parentNode : offsetNode, node = offset === 0 && (element.type === 'ext-attr-dirty' || element.type === 'html-attr-dirty')
760
981
  ? element.parentNode.parentNode
761
982
  : element, { type } = node, refName = getRefName(node), refGroup = getRefGroup(node);
762
983
  if (!refName && !refGroup && !referenceTypes.has(type)) {
@@ -836,6 +1057,13 @@ class LanguageService {
836
1057
  },
837
1058
  };
838
1059
  }
1060
+ /**
1061
+ * 检索状态开关
1062
+ * @param name 魔术字名
1063
+ */
1064
+ #getBehaviorSwitch(name) {
1065
+ return this.data.behaviorSwitches.find(({ aliases }) => aliases.includes(name));
1066
+ }
839
1067
  /**
840
1068
  * 检索解析器函数
841
1069
  * @param name 函数名
@@ -856,40 +1084,74 @@ class LanguageService {
856
1084
  if (!this.data) {
857
1085
  return undefined;
858
1086
  }
859
- const root = await this.#queue(text), { offsetNode, offset } = caretPositionFromWord(root, position), token = offsetNode.type === 'text' ? offsetNode.parentNode : offsetNode, { type, parentNode, length, name } = token;
1087
+ const root = await this.#queue(text);
1088
+ let { offsetNode, offset } = caretPositionFromWord(root, this.#text, position);
1089
+ if (offsetNode.type === 'text') {
1090
+ offset += offsetNode.getRelativeIndex();
1091
+ offsetNode = offsetNode.parentNode;
1092
+ }
1093
+ const { type, parentNode, length, name } = offsetNode;
860
1094
  let info, f, range;
861
- if (token.is('double-underscore') && offset > 0) {
862
- info = this.data.behaviorSwitches.find(({ aliases }) => aliases.includes(token.innerText.toLowerCase()));
1095
+ if (offsetNode.is('double-underscore') && offset > 0) {
1096
+ info = this.#getBehaviorSwitch(offsetNode.name);
863
1097
  }
864
1098
  else if (type === 'magic-word-name') {
865
1099
  info = this.#getParserFunction(parentNode.name);
866
- f = token.toString(true).trim();
1100
+ f = offsetNode.toString(true).trim();
867
1101
  }
868
- else if (token.is('magic-word') && !token.modifier && length === 1
869
- && (offset > 0 || root.posFromIndex(token.getAbsoluteIndex()).left === position.character)) {
1102
+ else if (offsetNode.is('magic-word') && !offsetNode.modifier && length === 1
1103
+ && (offset > 0 || root.posFromIndex(offsetNode.getAbsoluteIndex()).left === position.character)) {
870
1104
  info = this.#getParserFunction(name);
871
- f = token.firstChild.toString(true).trim();
1105
+ f = offsetNode.firstChild.toString(true).trim();
872
1106
  }
873
- else if ((token.is('magic-word') || token.is('template'))
874
- && token.modifier && offset >= 2 && token.getRelativeIndex(0) > offset) {
875
- f = token.modifier.trim().slice(0, -1);
1107
+ else if ((offsetNode.is('magic-word') || offsetNode.is('template'))
1108
+ && offsetNode.modifier && offset >= 2 && offsetNode.getRelativeIndex(0) > offset) {
1109
+ f = offsetNode.modifier.trim().slice(0, -1);
876
1110
  info = this.#getParserFunction(f.toLowerCase());
877
1111
  if (info) {
878
- const aIndex = token.getAbsoluteIndex();
1112
+ const aIndex = offsetNode.getAbsoluteIndex();
879
1113
  range = {
880
1114
  start: positionAt(root, aIndex + 2),
881
- end: positionAt(root, aIndex + token.modifier.trimEnd().length + 1),
1115
+ end: positionAt(root, aIndex + offsetNode.modifier.trimEnd().length + 1),
882
1116
  };
883
1117
  }
884
1118
  /* NOT FOR BROWSER ONLY */
885
1119
  }
886
- else if (document_1.cssLSP && type === 'attr-value' && length === 1 && parentNode.name === 'style') {
887
- const textDoc = new document_1.EmbeddedCSSDocument(root, token);
1120
+ else if ((0, exports.isAttr)(offsetNode, true)) {
1121
+ const textDoc = new document_1.EmbeddedCSSDocument(root, offsetNode);
888
1122
  return document_1.cssLSP.doHover(textDoc, position, textDoc.styleSheet) ?? undefined;
889
1123
  }
890
1124
  else if (document_1.jsonLSP && type === 'ext-inner' && document_1.jsonTags.includes(name)) {
891
- const textDoc = new document_1.EmbeddedJSONDocument(root, token);
1125
+ const textDoc = new document_1.EmbeddedJSONDocument(root, offsetNode);
892
1126
  return await document_1.jsonLSP.doHover(textDoc, position, textDoc.jsonDoc) ?? undefined;
1127
+ }
1128
+ else if (lint_1.htmlData.provideTags && lint_1.htmlData.provideAttributes) {
1129
+ if (type === 'html' && offset <= offsetNode.getRelativeIndex(0)
1130
+ || type === 'html-attr-dirty' && offset === 0 && parentNode.firstChild === offsetNode) {
1131
+ const token = type === 'html' ? offsetNode : parentNode.parentNode, data = lint_1.htmlData.provideTags().find(({ name: n }) => n === token.name);
1132
+ if (data?.description) {
1133
+ const start = positionAt(root, token.getAbsoluteIndex());
1134
+ return {
1135
+ contents: data.description,
1136
+ range: {
1137
+ start,
1138
+ end: {
1139
+ line: start.line,
1140
+ character: start.character + token.getRelativeIndex(0),
1141
+ },
1142
+ },
1143
+ };
1144
+ }
1145
+ }
1146
+ else if (type === 'attr-key' && isHtmlAttr(parentNode)) {
1147
+ const data = lint_1.htmlData.provideAttributes(parentNode.tag).find(({ name: n }) => n === parentNode.name);
1148
+ if (data?.description) {
1149
+ return {
1150
+ contents: data.description,
1151
+ range: createNodeRange(offsetNode),
1152
+ };
1153
+ }
1154
+ }
893
1155
  /* NOT FOR BROWSER ONLY END */
894
1156
  }
895
1157
  return info && {
@@ -900,7 +1162,7 @@ class LanguageService {
900
1162
  : '')
901
1163
  + info.description,
902
1164
  },
903
- range: range ?? createNodeRange(token),
1165
+ range: range ?? createNodeRange(offsetNode),
904
1166
  };
905
1167
  }
906
1168
  /**
@@ -973,8 +1235,7 @@ class LanguageService {
973
1235
  }
974
1236
  /** @private */
975
1237
  findStyleTokens() {
976
- return this.#done.querySelectorAll(cssSelector)
977
- .filter(({ lastChild: { length, firstChild } }) => length === 1 && firstChild.type === 'text');
1238
+ return this.#done.querySelectorAll(cssSelector).filter(({ lastChild }) => (0, exports.isAttr)(lastChild));
978
1239
  }
979
1240
  /* NOT FOR BROWSER ONLY */
980
1241
  /**
@@ -1036,6 +1297,28 @@ class LanguageService {
1036
1297
  }
1037
1298
  return symbols;
1038
1299
  }
1300
+ /**
1301
+ * Set the target Wikipedia
1302
+ *
1303
+ * 设置目标维基百科
1304
+ * @param wiki Wikipedia URL / 维基百科网址
1305
+ * @throws `RangeError` 不是有效的维基百科网址
1306
+ */
1307
+ async setTargetWikipedia(wiki) {
1308
+ const mt = /^https?:\/\/([^./]+)\.wikipedia\.org/iu.exec(wiki);
1309
+ if (!mt) {
1310
+ throw new RangeError('Invalid Wikipedia URL!');
1311
+ }
1312
+ const site = `${mt[1].toLowerCase()}wiki`;
1313
+ try {
1314
+ const config = require(path_1.default.join('..', '..', 'config', site));
1315
+ this.config = index_1.default.getConfig(config);
1316
+ }
1317
+ catch {
1318
+ this.config = index_1.default.getConfig(await (0, config_1.default)(site, `${mt[0]}/w`));
1319
+ }
1320
+ Object.assign(this.config, { articlePath: `${mt[0]}/wiki/` });
1321
+ }
1039
1322
  }
1040
1323
  exports.LanguageService = LanguageService;
1041
1324
  constants_1.classes['LanguageService'] = __filename;