wikilint 2.18.0 → 2.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");
@@ -16,6 +16,7 @@ const util_1 = __importDefault(require("util"));
16
16
  const child_process_1 = require("child_process");
17
17
  const crypto_1 = require("crypto");
18
18
  const stylelint_1 = require("@bhsd/common/dist/stylelint");
19
+ const config_1 = __importDefault(require("../bin/config"));
19
20
  const document_1 = require("./document");
20
21
  /** @see https://www.npmjs.com/package/stylelint-config-recommended */
21
22
  const cssRules = {
@@ -39,11 +40,30 @@ const refTags = new Set(['ref']), referencesTags = new Set(['ref', 'references']
39
40
  'heading',
40
41
  ...renameTypes,
41
42
  ]), plainTypes = new Set(['text', 'comment', 'noinclude', 'include']), cssSelector = ['ext', 'html', 'table'].map(s => `${s}-attr#style`).join();
43
+ /**
44
+ * Check if a token is a plain attribute.
45
+ * @param token
46
+ * @param token.type
47
+ * @param token.parentNode
48
+ * @param token.length
49
+ * @param token.firstChild
50
+ * @param style whether it is a style attribute
51
+ */
52
+ const isAttr = ({ type, parentNode, length, firstChild }, style) => type === 'attr-value' && length === 1 && firstChild.type === 'text'
53
+ && (!style
54
+ || parentNode.name === 'style'
55
+ && Boolean(document_1.cssLSP));
56
+ exports.isAttr = isAttr;
57
+ /**
58
+ * Check if a token is an HTML attribute.
59
+ * @param token
60
+ */
61
+ const isHtmlAttr = (token) => token.type === 'html-attr' || token.type === 'table-attr';
42
62
  /**
43
63
  * Check if all child nodes are plain text or comments.
44
- * @param childNodes child nodes
64
+ * @param token
45
65
  */
46
- const isPlain = (childNodes) => childNodes.every(({ type }) => plainTypes.has(type));
66
+ const isPlain = (token) => token.childNodes.every(({ type }) => plainTypes.has(type));
47
67
  /**
48
68
  * Get the position of a character in the document.
49
69
  * @param root root token
@@ -213,11 +233,12 @@ const getStylelintPos = (rect, bottom, line, column) => {
213
233
  };
214
234
  /**
215
235
  * Convert LilyPond errors to VSCode diagnostics.
236
+ * @param root root token
216
237
  * @param token `<score>` extension token
217
238
  * @param errors LilyPond errors
218
239
  */
219
- const getLilyPondDiagnostics = (token, errors) => {
220
- const { top, left } = token.lastChild.getBoundingClientRect();
240
+ const getLilyPondDiagnostics = (root, token, errors) => {
241
+ const { top, left } = root.posFromIndex(token.lastChild.getAbsoluteIndex());
221
242
  return errors.map(({ line, col, message }) => {
222
243
  const pos = (0, lint_1.getEndPos)(top, left, line, col);
223
244
  return {
@@ -254,16 +275,20 @@ class LanguageService {
254
275
  #completionConfig;
255
276
  include = true;
256
277
  /** @private */
278
+ config;
279
+ /** @private */
257
280
  data;
258
281
  /* NOT FOR BROWSER ONLY */
259
282
  lilypond;
283
+ /** @private */
260
284
  lilypondData;
261
285
  /* NOT FOR BROWSER ONLY END */
262
286
  /** @param uri 任务标识 */
263
287
  constructor(uri) {
264
288
  exports.tasks.set(uri, this);
265
- /* NOT FOR BROWSER ONLY */
266
289
  Object.defineProperties(this, {
290
+ config: { enumerable: false },
291
+ /* NOT FOR BROWSER ONLY */
267
292
  data: {
268
293
  value: require(path_1.default.join('..', '..', 'data', 'signatures')),
269
294
  enumerable: false,
@@ -281,16 +306,18 @@ class LanguageService {
281
306
  const dir = path_1.default.join(__dirname, 'lilypond');
282
307
  if (fs_1.default.existsSync(dir)) {
283
308
  for (const file of fs_1.default.readdirSync(dir)) {
284
- try {
285
- void fs_1.default.promises.unlink(path_1.default.join(dir, file));
286
- }
287
- catch { }
309
+ (async () => {
310
+ try {
311
+ await fs_1.default.promises.unlink(path_1.default.join(dir, file));
312
+ }
313
+ catch { }
314
+ })();
288
315
  }
289
316
  }
290
317
  }
291
318
  /** 检查解析设置有无更新 */
292
319
  #checkConfig() {
293
- return this.#config === index_1.default.config && this.#include === this.include;
320
+ return this.#config === this.config && this.#include === this.include;
294
321
  }
295
322
  /**
296
323
  * 提交解析任务
@@ -316,10 +343,10 @@ class LanguageService {
316
343
  * - 总是返回最新的解析结果
317
344
  */
318
345
  async #parse() {
319
- const config = index_1.default.getConfig();
320
- this.#config = index_1.default.config;
346
+ this.config ??= index_1.default.getConfig();
347
+ this.#config = this.config;
321
348
  this.#include = this.include;
322
- const text = this.#text, root = await index_1.default.partialParse(text, () => this.#text, this.include, config);
349
+ const text = this.#text, root = await index_1.default.partialParse(text, () => this.#text, this.include, this.config);
323
350
  if (this.#checkConfig() && this.#text === text) {
324
351
  this.#done = root;
325
352
  this.#running = undefined;
@@ -354,10 +381,10 @@ class LanguageService {
354
381
  * - 总是返回最新的解析结果
355
382
  */
356
383
  async #parseSignature() {
357
- const config = index_1.default.getConfig();
358
- this.#config = index_1.default.config;
384
+ this.config ??= index_1.default.getConfig();
385
+ this.#config = this.config;
359
386
  this.#include = this.include;
360
- const text = this.#text2, root = await index_1.default.partialParse(text, () => this.#text2, this.include, config);
387
+ const text = this.#text2, root = await index_1.default.partialParse(text, () => this.#text2, this.include, this.config);
361
388
  if (this.#checkConfig() && this.#text2 === text) {
362
389
  this.#done2 = root;
363
390
  this.#running2 = undefined;
@@ -378,14 +405,38 @@ class LanguageService {
378
405
  */
379
406
  async provideDocumentColors(rgba, text, hsl = true) {
380
407
  const root = await this.#queue(text);
408
+ /* NOT FOR BROWSER ONLY */
409
+ let colors;
410
+ try {
411
+ colors = new RegExp(String.raw `\b${Object.keys((await import('color-name')).default).join('|')}\b`, 'giu');
412
+ }
413
+ catch { }
414
+ /* NOT FOR BROWSER ONLY END */
381
415
  return root.querySelectorAll('attr-value,parameter-value,arg-default').reverse()
382
- .flatMap(({ type, childNodes }) => {
383
- if (type !== 'attr-value' && !isPlain(childNodes)) {
416
+ .flatMap(token => {
417
+ const { type, childNodes,
418
+ /* NOT FOR BROWSER ONLY */
419
+ parentNode, } = token;
420
+ if (type !== 'attr-value' && !isPlain(token)) {
384
421
  return [];
422
+ /* NOT FOR BROWSER ONLY */
423
+ }
424
+ else if ((0, exports.isAttr)(token, true)) {
425
+ const textDoc = new document_1.EmbeddedCSSDocument(root, token);
426
+ return document_1.cssLSP.findDocumentColors(textDoc, textDoc.styleSheet);
427
+ /* NOT FOR BROWSER ONLY END */
385
428
  }
429
+ /* NOT FOR BROWSER ONLY */
430
+ const isStyle = colors && type === 'attr-value' && parentNode.name === 'style';
431
+ /* NOT FOR BROWSER ONLY END */
386
432
  return childNodes.filter((child) => child.type === 'text').reverse()
387
433
  .flatMap(child => {
388
- const parts = (0, common_1.splitColors)(child.data, hsl).filter(([, , , isColor]) => isColor);
434
+ const { data } = child, parts = (0, common_1.splitColors)(data, hsl).filter(([, , , isColor]) => isColor);
435
+ /* NOT FOR BROWSER ONLY */
436
+ if (isStyle) {
437
+ parts.push(...[...data.matchAll(colors)].map(({ index, 0: s }) => [s, index, index + s.length, true]));
438
+ }
439
+ /* NOT FOR BROWSER ONLY END */
389
440
  if (parts.length === 0) {
390
441
  return [];
391
442
  }
@@ -423,8 +474,9 @@ class LanguageService {
423
474
  }
424
475
  /** 准备自动补全设置 */
425
476
  #prepareCompletionConfig() {
426
- if (!this.#completionConfig) {
427
- const { nsid, ext, html, parserFunction: [insensitive, sensitive, ...other], doubleUnderscore, protocol, img, } = index_1.default.getConfig(), tags = new Set([ext, html].flat(2));
477
+ if (!this.#completionConfig || this.#completionConfig[1] !== this.config) {
478
+ this.config ??= index_1.default.getConfig();
479
+ const { nsid, ext, html, parserFunction: [insensitive, sensitive, ...other], doubleUnderscore, protocol, img, } = this.config, tags = new Set([ext, html].flat(2));
428
480
  const re = new RegExp('(?:' // eslint-disable-line prefer-template
429
481
  + String.raw `<(\/?\w*)` // tag
430
482
  + '|'
@@ -439,23 +491,27 @@ class LanguageService {
439
491
  // attribute key
440
492
  + String.raw `<(\w+)(?:\s(?:[^<>{}|=\s]+(?:\s*=\s*(?:[^\s"']\S*|(["']).*?\8))?(?=\s))*)?\s(\w*)`
441
493
  + ')$', 'iu');
442
- this.#completionConfig = {
443
- re,
444
- ext,
445
- tags,
446
- allTags: [...tags, 'onlyinclude', 'includeonly', 'noinclude'],
447
- functions: [
448
- Object.keys(insensitive),
449
- Array.isArray(sensitive) ? /* istanbul ignore next */ sensitive : Object.keys(sensitive),
450
- other,
451
- ].flat(2),
452
- switches: doubleUnderscore.slice(0, 2).flat().map(w => `__${w}__`),
453
- protocols: protocol.split('|'),
454
- params: Object.keys(img).filter(k => k.endsWith('$1') || !k.includes('$1'))
455
- .map(k => k.replace(/\$1$/u, '')),
456
- };
494
+ this.#completionConfig = [
495
+ {
496
+ re,
497
+ ext,
498
+ tags,
499
+ allTags: [...tags, 'onlyinclude', 'includeonly', 'noinclude'],
500
+ functions: [
501
+ Object.keys(insensitive),
502
+ Array.isArray(sensitive) ? /* istanbul ignore next */ sensitive : Object.keys(sensitive),
503
+ other,
504
+ ].flat(2),
505
+ switches: doubleUnderscore.slice(0, 2).flat().map(w => `__${w}__`),
506
+ protocols: protocol.split('|'),
507
+ params: Object.keys(img)
508
+ .filter(k => k.endsWith('$1') || !k.includes('$1'))
509
+ .map(k => k.replace(/\$1$/u, '')),
510
+ },
511
+ this.config,
512
+ ];
457
513
  }
458
- return this.#completionConfig;
514
+ return this.#completionConfig[0];
459
515
  }
460
516
  /**
461
517
  * Provide auto-completion
@@ -465,13 +521,25 @@ class LanguageService {
465
521
  * @param position 位置
466
522
  */
467
523
  async provideCompletionItems(text, position) {
468
- 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) ?? '');
524
+ 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;
469
525
  if (mt?.[1] !== undefined) { // tag
470
526
  const closing = mt[1].startsWith('/');
471
527
  return getCompletion(allTags, 'Class', mt[1].slice(closing ? 1 : 0), position, closing && !curLine?.slice(character).trim().startsWith('>') ? '>' : '');
472
528
  }
473
529
  else if (mt?.[4]) { // behavior switch
474
- return getCompletion(switches, 'Constant', mt[4], position, '', name => this.data && this.#getBehaviorSwitch(name.slice(2, -2)));
530
+ return getCompletion(switches, 'Constant', mt[4], position, '', name => {
531
+ if (!this.data) {
532
+ return undefined;
533
+ }
534
+ name = name.slice(2, -2);
535
+ if (name in iAlias) {
536
+ name = iAlias[name];
537
+ }
538
+ else if (name in sAlias) {
539
+ name = sAlias[name];
540
+ }
541
+ return this.#getBehaviorSwitch(name.toLowerCase());
542
+ });
475
543
  }
476
544
  else if (mt?.[5] !== undefined) { // protocol
477
545
  return getCompletion(protocols, 'Reference', mt[5], position);
@@ -485,12 +553,23 @@ class LanguageService {
485
553
  return getCompletion(root.querySelectorAll('arg').filter(token => token.name && token !== cur)
486
554
  .map(({ name }) => name), 'Variable', match, position);
487
555
  }
488
- const colon = match.startsWith(':'), str = colon ? match.slice(1).trimStart() : match;
556
+ const [insensitive, sensitive] = this.config.parserFunction, isOld = Array.isArray(sensitive), colon = match.startsWith(':'), str = colon ? match.slice(1).trimStart() : match;
489
557
  return mt[2] === '[['
490
558
  ? getCompletion(// link
491
559
  root.querySelectorAll('link,file,category,redirect-target').filter(token => token !== cur).map(({ name }) => name), 'Folder', str, position)
492
560
  : [
493
- ...getCompletion(functions, 'Function', match, position, '', name => this.data && this.#getParserFunction(name.replace(/^#/u, '').toLowerCase())),
561
+ ...getCompletion(functions, 'Function', match, position, '', name => {
562
+ if (!this.data) {
563
+ return undefined;
564
+ }
565
+ else if (name in insensitive) {
566
+ name = insensitive[name];
567
+ }
568
+ else if (!isOld && name in sensitive) {
569
+ name = sensitive[name];
570
+ }
571
+ return this.#getParserFunction(name.toLowerCase());
572
+ }),
494
573
  ...match.startsWith('#')
495
574
  ? []
496
575
  : getCompletion(root.querySelectorAll('template').filter(token => token !== cur)
@@ -550,7 +629,7 @@ class LanguageService {
550
629
  if (t === 'magic-word' && n !== 'invoke') {
551
630
  return undefined;
552
631
  }
553
- const index = root.indexFromPos(line, character), key = this.#text.slice(cur.getAbsoluteIndex(), index).trimStart(), [module, func] = t === 'magic-word' ? transclusion.getModule() : [];
632
+ const key = this.#text.slice(cur.getAbsoluteIndex(), root.indexFromPos(line, character)).trimStart(), [module, func] = t === 'magic-word' ? transclusion.getModule() : [];
554
633
  return key
555
634
  ? getCompletion(root.querySelectorAll('parameter').filter(token => {
556
635
  if (token === parentNode
@@ -568,7 +647,7 @@ class LanguageService {
568
647
  : undefined;
569
648
  /* NOT FOR BROWSER ONLY */
570
649
  }
571
- else if (document_1.cssLSP && type === 'attr-value' && parentNode.name === 'style' && cur.length === 1) {
650
+ else if ((0, exports.isAttr)(cur, true)) {
572
651
  const textDoc = new document_1.EmbeddedCSSDocument(root, cur);
573
652
  return document_1.cssLSP.doComplete(textDoc, position, textDoc.styleSheet).items.map((item) => ({
574
653
  ...item,
@@ -587,7 +666,7 @@ class LanguageService {
587
666
  if (lang !== undefined && lang !== 'lilypond') {
588
667
  return undefined;
589
668
  }
590
- const j = root.indexFromPos(line, character), i = cur.getAbsoluteIndex(), before = this.#text.slice(i, j), comment = before.lastIndexOf('%');
669
+ const before = this.#text.slice(cur.getAbsoluteIndex(), root.indexFromPos(line, character)), comment = before.lastIndexOf('%');
591
670
  if (comment !== -1
592
671
  && (before.charAt(comment + 1) === '{' || !before.slice(comment).includes('\n'))) {
593
672
  return undefined;
@@ -604,6 +683,14 @@ class LanguageService {
604
683
  }
605
684
  /* NOT FOR BROWSER ONLY END */
606
685
  }
686
+ else if ((0, exports.isAttr)(cur) && isHtmlAttr(parentNode)) {
687
+ const data = lint_1.htmlData.provideValues(parentNode.tag, parentNode.name);
688
+ if (data.length === 0) {
689
+ return undefined;
690
+ }
691
+ const val = this.#text.slice(cur.getAbsoluteIndex(), root.indexFromPos(line, character)).trimStart();
692
+ return getCompletion(data.map(({ name }) => name), 'Value', val, position);
693
+ }
607
694
  return undefined;
608
695
  }
609
696
  /**
@@ -695,7 +782,7 @@ class LanguageService {
695
782
  lilypondDiagnostics = await Promise.all(tokens.map(async (token) => {
696
783
  const { innerText } = token, score = `showLastLength = R1${token.getAttr('raw') === undefined ? ` \\score {\n${innerText}\n}` : `\n${innerText}`}`;
697
784
  if (scores.has(score)) {
698
- return getLilyPondDiagnostics(token, scores.get(score));
785
+ return getLilyPondDiagnostics(root, token, scores.get(score));
699
786
  }
700
787
  const hash = (0, crypto_1.createHash)('sha256');
701
788
  hash.update(score);
@@ -717,7 +804,7 @@ class LanguageService {
717
804
  };
718
805
  });
719
806
  scores.set(score, lilypondErrors);
720
- return getLilyPondDiagnostics(token, lilypondErrors);
807
+ return getLilyPondDiagnostics(root, token, lilypondErrors);
721
808
  }
722
809
  }
723
810
  return [];
@@ -797,7 +884,8 @@ class LanguageService {
797
884
  * @param text source Wikitext / 源代码
798
885
  */
799
886
  async provideLinks(text) {
800
- const { articlePath, protocol } = index_1.default.getConfig(), absolute = articlePath?.includes('//'), protocolRegex = new RegExp(`^(?:${protocol}|//)`, 'iu');
887
+ this.config ??= index_1.default.getConfig();
888
+ const { articlePath, protocol } = this.config, absolute = articlePath?.includes('//'), protocolRegex = new RegExp(`^(?:${protocol}|//)`, 'iu');
801
889
  return (await this.#queue(text))
802
890
  .querySelectorAll(`magic-link,ext-link-url,free-ext-link,attr-value,image-parameter#link${absolute ? ',link-target,template-name,invoke-module' : ''}`)
803
891
  .reverse()
@@ -806,7 +894,7 @@ class LanguageService {
806
894
  if (!(type !== 'attr-value'
807
895
  || name === 'src' && ['templatestyles', 'img'].includes(tag)
808
896
  || name === 'cite' && ['blockquote', 'del', 'ins', 'q'].includes(tag))
809
- || !isPlain(childNodes)) {
897
+ || !isPlain(token)) {
810
898
  return false;
811
899
  }
812
900
  let target = childNodes.filter((node) => node.type === 'text')
@@ -846,8 +934,7 @@ class LanguageService {
846
934
  else if (type === 'invoke-module') {
847
935
  ns = 828;
848
936
  }
849
- const title = index_1.default
850
- .normalizeTitle(target, ns, false, undefined, true);
937
+ const title = index_1.default.normalizeTitle(target, ns, false, this.config, true);
851
938
  /* istanbul ignore if */
852
939
  if (!title.valid) {
853
940
  return false;
@@ -991,40 +1078,74 @@ class LanguageService {
991
1078
  if (!this.data) {
992
1079
  return undefined;
993
1080
  }
994
- const root = await this.#queue(text), { offsetNode, offset } = caretPositionFromWord(root, this.#text, position), token = offsetNode.type === 'text' ? offsetNode.parentNode : offsetNode, { type, parentNode, length, name } = token;
1081
+ const root = await this.#queue(text);
1082
+ let { offsetNode, offset } = caretPositionFromWord(root, this.#text, position);
1083
+ if (offsetNode.type === 'text') {
1084
+ offset += offsetNode.getRelativeIndex();
1085
+ offsetNode = offsetNode.parentNode;
1086
+ }
1087
+ const { type, parentNode, length, name } = offsetNode;
995
1088
  let info, f, range;
996
- if (token.is('double-underscore') && offset > 0) {
997
- info = this.#getBehaviorSwitch(token.innerText.toLowerCase());
1089
+ if (offsetNode.is('double-underscore') && offset > 0) {
1090
+ info = this.#getBehaviorSwitch(offsetNode.name);
998
1091
  }
999
1092
  else if (type === 'magic-word-name') {
1000
1093
  info = this.#getParserFunction(parentNode.name);
1001
- f = token.toString(true).trim();
1094
+ f = offsetNode.toString(true).trim();
1002
1095
  }
1003
- else if (token.is('magic-word') && !token.modifier && length === 1
1004
- && (offset > 0 || root.posFromIndex(token.getAbsoluteIndex()).left === position.character)) {
1096
+ else if (offsetNode.is('magic-word') && !offsetNode.modifier && length === 1
1097
+ && (offset > 0 || root.posFromIndex(offsetNode.getAbsoluteIndex()).left === position.character)) {
1005
1098
  info = this.#getParserFunction(name);
1006
- f = token.firstChild.toString(true).trim();
1099
+ f = offsetNode.firstChild.toString(true).trim();
1007
1100
  }
1008
- else if ((token.is('magic-word') || token.is('template'))
1009
- && token.modifier && offset >= 2 && token.getRelativeIndex(0) > offset) {
1010
- f = token.modifier.trim().slice(0, -1);
1101
+ else if ((offsetNode.is('magic-word') || offsetNode.is('template'))
1102
+ && offsetNode.modifier && offset >= 2 && offsetNode.getRelativeIndex(0) > offset) {
1103
+ f = offsetNode.modifier.trim().slice(0, -1);
1011
1104
  info = this.#getParserFunction(f.toLowerCase());
1012
1105
  if (info) {
1013
- const aIndex = token.getAbsoluteIndex();
1106
+ const aIndex = offsetNode.getAbsoluteIndex();
1014
1107
  range = {
1015
1108
  start: positionAt(root, aIndex + 2),
1016
- end: positionAt(root, aIndex + token.modifier.trimEnd().length + 1),
1109
+ end: positionAt(root, aIndex + offsetNode.modifier.trimEnd().length + 1),
1017
1110
  };
1018
1111
  }
1019
1112
  /* NOT FOR BROWSER ONLY */
1020
1113
  }
1021
- else if (document_1.cssLSP && type === 'attr-value' && length === 1 && parentNode.name === 'style') {
1022
- const textDoc = new document_1.EmbeddedCSSDocument(root, token);
1114
+ else if ((0, exports.isAttr)(offsetNode, true)) {
1115
+ const textDoc = new document_1.EmbeddedCSSDocument(root, offsetNode);
1023
1116
  return document_1.cssLSP.doHover(textDoc, position, textDoc.styleSheet) ?? undefined;
1024
1117
  }
1025
1118
  else if (document_1.jsonLSP && type === 'ext-inner' && document_1.jsonTags.includes(name)) {
1026
- const textDoc = new document_1.EmbeddedJSONDocument(root, token);
1119
+ const textDoc = new document_1.EmbeddedJSONDocument(root, offsetNode);
1027
1120
  return await document_1.jsonLSP.doHover(textDoc, position, textDoc.jsonDoc) ?? undefined;
1121
+ }
1122
+ else if (lint_1.htmlData.provideTags && lint_1.htmlData.provideAttributes) {
1123
+ if (type === 'html' && offset <= offsetNode.getRelativeIndex(0)
1124
+ || type === 'html-attr-dirty' && offset === 0 && parentNode.firstChild === offsetNode) {
1125
+ const token = type === 'html' ? offsetNode : parentNode.parentNode, data = lint_1.htmlData.provideTags().find(({ name: n }) => n === token.name);
1126
+ if (data?.description) {
1127
+ const start = positionAt(root, token.getAbsoluteIndex());
1128
+ return {
1129
+ contents: data.description,
1130
+ range: {
1131
+ start,
1132
+ end: {
1133
+ line: start.line,
1134
+ character: start.character + token.getRelativeIndex(0),
1135
+ },
1136
+ },
1137
+ };
1138
+ }
1139
+ }
1140
+ else if (type === 'attr-key' && isHtmlAttr(parentNode)) {
1141
+ const data = lint_1.htmlData.provideAttributes(parentNode.tag).find(({ name: n }) => n === parentNode.name);
1142
+ if (data?.description) {
1143
+ return {
1144
+ contents: data.description,
1145
+ range: createNodeRange(offsetNode),
1146
+ };
1147
+ }
1148
+ }
1028
1149
  /* NOT FOR BROWSER ONLY END */
1029
1150
  }
1030
1151
  return info && {
@@ -1035,7 +1156,7 @@ class LanguageService {
1035
1156
  : '')
1036
1157
  + info.description,
1037
1158
  },
1038
- range: range ?? createNodeRange(token),
1159
+ range: range ?? createNodeRange(offsetNode),
1039
1160
  };
1040
1161
  }
1041
1162
  /**
@@ -1108,8 +1229,7 @@ class LanguageService {
1108
1229
  }
1109
1230
  /** @private */
1110
1231
  findStyleTokens() {
1111
- return this.#done.querySelectorAll(cssSelector)
1112
- .filter(({ lastChild: { length, firstChild } }) => length === 1 && firstChild.type === 'text');
1232
+ return this.#done.querySelectorAll(cssSelector).filter(({ lastChild }) => (0, exports.isAttr)(lastChild));
1113
1233
  }
1114
1234
  /* NOT FOR BROWSER ONLY */
1115
1235
  /**
@@ -1171,5 +1291,27 @@ class LanguageService {
1171
1291
  }
1172
1292
  return symbols;
1173
1293
  }
1294
+ /**
1295
+ * Set the target Wikipedia
1296
+ *
1297
+ * 设置目标维基百科
1298
+ * @param wiki Wikipedia URL / 维基百科网址
1299
+ * @throws `RangeError` 不是有效的维基百科网址
1300
+ */
1301
+ async setTargetWikipedia(wiki) {
1302
+ const mt = /^https?:\/\/([^./]+)\.wikipedia\.org/iu.exec(wiki);
1303
+ if (!mt) {
1304
+ throw new RangeError('Invalid Wikipedia URL!');
1305
+ }
1306
+ const site = `${mt[1].toLowerCase()}wiki`;
1307
+ try {
1308
+ const config = require(path_1.default.join('..', '..', 'config', site));
1309
+ this.config = index_1.default.getConfig(config);
1310
+ }
1311
+ catch {
1312
+ this.config = index_1.default.getConfig(await (0, config_1.default)(site, `${mt[0]}/w`));
1313
+ }
1314
+ Object.assign(this.config, { articlePath: `${mt[0]}/wiki/` });
1315
+ }
1174
1316
  }
1175
1317
  exports.LanguageService = LanguageService;
@@ -12,7 +12,7 @@ const rect_1 = require("../lib/rect");
12
12
  const index_1 = __importDefault(require("../index"));
13
13
  const index_2 = require("./index");
14
14
  const atom_1 = require("./atom");
15
- const insecureStyle = /expression|(?:accelerator|-o-link(?:-source)?|-o-replace)\s*:|(?:url|image(?:-set)?)\s*\(|attr\s*\([^)]+[\s,]url/u;
15
+ const insecureStyle = /expression|(?:accelerator|-o-link(?:-source)?|-o-replace)\s*:|(?:url|image(?:-set)?)\s*\(|attr\s*\([^)]+[\s,]url/u, complexTypes = new Set(['ext', 'arg', 'magic-word', 'template']);
16
16
  /**
17
17
  * attribute of extension and HTML tags
18
18
  *
@@ -118,9 +118,6 @@ class AttributeToken extends index_2.Token {
118
118
  e.suggestions = [{ desc: 'remove', range: [start, start + length], text: '' }];
119
119
  errors.push(e);
120
120
  }
121
- else if (sharable_1.obsoleteAttrs[tag]?.has(name)) {
122
- errors.push((0, lint_1.generateForChild)(firstChild, rect, 'obsolete-attr', 'obsolete attribute', 'warning'));
123
- }
124
121
  else if (name === 'style' && typeof value === 'string' && insecureStyle.test(value)) {
125
122
  errors.push((0, lint_1.generateForChild)(lastChild, rect, 'insecure-style', 'insecure style'));
126
123
  }
@@ -132,6 +129,15 @@ class AttributeToken extends index_2.Token {
132
129
  ];
133
130
  errors.push(e);
134
131
  }
132
+ else if (type !== 'ext-attr' && !lastChild.childNodes.some(({ type: t }) => complexTypes.has(t))) {
133
+ const data = lint_1.htmlData.provideValues(tag, name), v = String(value).toLowerCase();
134
+ if (data.length > 0 && data.every(({ name: n }) => n !== v)) {
135
+ errors.push((0, lint_1.generateForChild)(lastChild, rect, 'illegal-attr', 'illegal attribute value', 'warning'));
136
+ }
137
+ }
138
+ if (sharable_1.obsoleteAttrs[tag]?.has(name)) {
139
+ errors.push((0, lint_1.generateForChild)(firstChild, rect, 'obsolete-attr', 'obsolete attribute', 'warning'));
140
+ }
135
141
  return errors;
136
142
  }
137
143
  /**
package/dist/src/index.js CHANGED
@@ -54,6 +54,7 @@ const element_1 = require("../lib/element");
54
54
  const text_1 = require("../lib/text");
55
55
  /* NOT FOR BROWSER ONLY */
56
56
  const document_1 = require("../lib/document");
57
+ const lsp_1 = require("../lib/lsp");
57
58
  /**
58
59
  * base class for all tokens
59
60
  *
@@ -422,10 +423,10 @@ class Token extends element_1.AstElement {
422
423
  });
423
424
  /* NOT FOR BROWSER ONLY */
424
425
  }
425
- else if (document_1.cssLSP && this.type === 'attr-value' && this.length === 1 && this.firstChild.type === 'text'
426
- && this.parentNode.name === 'style') {
426
+ else if ((0, lsp_1.isAttr)(this, true)) {
427
427
  const root = this.getRootNode(), textDoc = new document_1.EmbeddedCSSDocument(root, this);
428
428
  errors.push(...document_1.cssLSP.doValidation(textDoc, textDoc.styleSheet)
429
+ .filter(({ code }) => code !== 'css-ruleorselectorexpected')
429
430
  .map(({ range: { start: { line, character }, end }, message, severity, code }) => ({
430
431
  code: code,
431
432
  rule: 'invalid-css',
@@ -7,6 +7,7 @@ import type { Token } from '../index';
7
7
  * 状态开关
8
8
  */
9
9
  export declare abstract class DoubleUnderscoreToken extends NowikiBaseToken {
10
+ readonly name: string;
10
11
  get type(): 'double-underscore';
11
12
  /**
12
13
  * @param word 状态开关名
@@ -66,6 +66,8 @@ let DoubleUnderscoreToken = (() => {
66
66
  */
67
67
  constructor(word, sensitive, config, accum) {
68
68
  super(word, config, accum);
69
+ const lc = word.toLowerCase(), { doubleUnderscore: [, , iAlias, sAlias] } = config;
70
+ this.setAttribute('name', (sensitive ? sAlias?.[word]?.toLowerCase() : iAlias?.[lc]) ?? lc);
69
71
  }
70
72
  /** @private */
71
73
  getAttribute(key) {
package/dist/util/diff.js CHANGED
@@ -73,7 +73,7 @@ const diff = async (oldStr, newStr, uid) => {
73
73
  newFile,
74
74
  ]);
75
75
  console.log(stdout?.split('\n').slice(4).join('\n'));
76
- await Promise.all([promises_1.default.unlink(oldFile), promises_1.default.unlink(newFile)]);
76
+ await Promise.allSettled([promises_1.default.unlink(oldFile), promises_1.default.unlink(newFile)]);
77
77
  };
78
78
  exports.diff = diff;
79
79
  /* istanbul ignore next */
package/dist/util/lint.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.cache = exports.generateForSelf = exports.generateForChild = exports.getEndPos = void 0;
6
+ exports.htmlData = exports.cache = exports.generateForSelf = exports.generateForChild = exports.getEndPos = void 0;
7
7
  const debug_1 = require("./debug");
8
8
  const rect_1 = require("../lib/rect");
9
9
  const index_1 = __importDefault(require("../index"));
@@ -61,3 +61,33 @@ const cache = (store, compute, update) => {
61
61
  return result;
62
62
  };
63
63
  exports.cache = cache;
64
+ let htmlData;
65
+ try {
66
+ exports.htmlData = htmlData = require('vscode-html-languageservice')
67
+ .getDefaultHTMLDataProvider();
68
+ }
69
+ catch {
70
+ /**
71
+ * 获取HTML属性值可选列表
72
+ * @param tag 标签名
73
+ * @param attribute 属性名
74
+ */
75
+ const provideValues = (tag, attribute) => {
76
+ if (tag === 'ol' && attribute === 'type') {
77
+ return ['1', 'a', 'A', 'i', 'I'];
78
+ }
79
+ else if (tag === 'th' && attribute === 'scope') {
80
+ return ['row', 'col', 'rowgroup', 'colgroup'];
81
+ }
82
+ else if (attribute === 'dir') {
83
+ return ['ltr', 'rtl', 'auto'];
84
+ }
85
+ return attribute === 'aria-hidden' ? ['true', 'false'] : [];
86
+ };
87
+ exports.htmlData = htmlData = {
88
+ /** @implements */
89
+ provideValues(tag, attribute) {
90
+ return provideValues(tag, attribute).map(value => ({ name: value }));
91
+ },
92
+ };
93
+ }
package/i18n/zh-hans.json CHANGED
@@ -21,6 +21,7 @@
21
21
  "HTML comment": "HTML注释",
22
22
  "HTML tag in table attributes": "表格属性中的HTML标签",
23
23
  "illegal attribute name": "非法的属性名",
24
+ "illegal attribute value": "非法的属性值",
24
25
  "illegal module name": "非法的模块名称",
25
26
  "inconsistent table layout": "不一致的表格布局",
26
27
  "insecure style": "不安全的样式",
package/i18n/zh-hant.json CHANGED
@@ -21,6 +21,7 @@
21
21
  "HTML comment": "HTML註釋",
22
22
  "HTML tag in table attributes": "表格屬性中的HTML標籤",
23
23
  "illegal attribute name": "非法的屬性名",
24
+ "illegal attribute value": "非法的屬性值",
24
25
  "illegal module name": "非法的模組名稱",
25
26
  "inconsistent table layout": "不一致的表格佈局",
26
27
  "insecure style": "不安全的樣式",