@bhsd/codemirror-mediawiki 3.9.2 → 3.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +147 -87
  2. package/dist/bidi.d.ts +9 -8
  3. package/dist/bidi.js +38 -23
  4. package/dist/codemirror.d.ts +7 -0
  5. package/dist/codemirror.js +22 -9
  6. package/dist/color.d.ts +8 -5
  7. package/dist/color.js +5 -1
  8. package/dist/config.d.ts +1 -0
  9. package/dist/config.js +1 -0
  10. package/dist/constants.d.ts +3 -2
  11. package/dist/constants.js +3 -2
  12. package/dist/css.d.ts +5 -0
  13. package/dist/css.js +14 -6
  14. package/dist/escape.d.ts +22 -2
  15. package/dist/escape.js +44 -25
  16. package/dist/fold.d.ts +57 -5
  17. package/dist/fold.js +149 -58
  18. package/dist/hover.d.ts +16 -3
  19. package/dist/hover.js +84 -67
  20. package/dist/html.js +17 -12
  21. package/dist/indent.d.ts +7 -0
  22. package/dist/indent.js +7 -1
  23. package/dist/index.d.ts +54 -16
  24. package/dist/index.js +91 -38
  25. package/dist/inlay.d.ts +1 -1
  26. package/dist/inlay.js +26 -25
  27. package/dist/javascript.d.ts +10 -1
  28. package/dist/javascript.js +50 -2
  29. package/dist/keybindings.d.ts +1 -0
  30. package/dist/keybindings.js +1 -0
  31. package/dist/keymap.d.ts +11 -0
  32. package/dist/keymap.js +3 -4
  33. package/dist/linter.d.ts +31 -2
  34. package/dist/linter.js +10 -3
  35. package/dist/lintsource.d.ts +47 -3
  36. package/dist/lintsource.js +50 -11
  37. package/dist/lua.d.ts +0 -2
  38. package/dist/lua.js +27 -10
  39. package/dist/main.min.js +31 -29
  40. package/dist/matchBrackets.d.ts +16 -0
  41. package/dist/matchBrackets.js +16 -0
  42. package/dist/matchTag.d.ts +5 -2
  43. package/dist/matchTag.js +11 -7
  44. package/dist/mediawiki.d.ts +15 -2
  45. package/dist/mediawiki.js +59 -45
  46. package/dist/mw.min.js +33 -37
  47. package/dist/mwConfig.js +2 -2
  48. package/dist/openLinks.d.ts +12 -2
  49. package/dist/openLinks.js +64 -54
  50. package/dist/ref.d.ts +16 -2
  51. package/dist/ref.js +110 -95
  52. package/dist/signature.d.ts +7 -1
  53. package/dist/signature.js +53 -49
  54. package/dist/static.d.ts +4 -0
  55. package/dist/static.js +4 -0
  56. package/dist/statusBar.js +9 -8
  57. package/dist/theme.d.ts +1 -0
  58. package/dist/theme.js +8 -0
  59. package/dist/token.d.ts +29 -7
  60. package/dist/token.js +33 -18
  61. package/dist/util.d.ts +25 -2
  62. package/dist/util.js +47 -1
  63. package/dist/wiki.min.js +32 -36
  64. package/i18n/en.json +2 -2
  65. package/i18n/zh-hans.json +2 -2
  66. package/i18n/zh-hant.json +2 -2
  67. package/package.json +15 -13
@@ -5,9 +5,25 @@ export interface Selection {
5
5
  anchor: number;
6
6
  head: number;
7
7
  }
8
+ /**
9
+ * @ignore
10
+ * @test
11
+ */
8
12
  export declare const findEnclosingBrackets: (node: SyntaxNode, pos: number, brackets: string) => MatchResult | undefined;
13
+ /**
14
+ * @ignore
15
+ * @test
16
+ */
9
17
  export declare const findEnclosingPlainBrackets: (state: EditorState, pos: number, config: Required<Config>) => MatchResult | null;
18
+ /**
19
+ * @ignore
20
+ * @test
21
+ */
10
22
  export declare const trySelectMatchingBrackets: (state: EditorState, pos: number, dir: 1 | -1, config?: Config, inside?: boolean) => Selection | false;
23
+ /**
24
+ * @ignore
25
+ * @test
26
+ */
11
27
  export declare const selectMatchingBrackets: (state: EditorState, pos: number, config?: Config) => Selection | false;
12
28
  declare const _default: (configs?: Config) => Extension;
13
29
  export default _default;
@@ -1,5 +1,9 @@
1
1
  import { Decoration, EditorView } from '@codemirror/view';
2
2
  import { bracketMatching, matchBrackets, syntaxTree } from '@codemirror/language';
3
+ /**
4
+ * @ignore
5
+ * @test
6
+ */
3
7
  export const findEnclosingBrackets = (node, pos, brackets) => {
4
8
  let parent = node;
5
9
  while (parent) {
@@ -14,6 +18,10 @@ export const findEnclosingBrackets = (node, pos, brackets) => {
14
18
  }
15
19
  return undefined;
16
20
  };
21
+ /**
22
+ * @ignore
23
+ * @test
24
+ */
17
25
  export const findEnclosingPlainBrackets = (state, pos, config) => {
18
26
  const { brackets, maxScanDistance } = config, re = new RegExp(`[${[...brackets].filter((_, i) => i % 2).map(c => c === ']' ? String.raw `\]` : c).join('')}]`, 'gu'), str = state.sliceDoc(pos, pos + maxScanDistance);
19
27
  let mt = re.exec(str);
@@ -26,6 +34,10 @@ export const findEnclosingPlainBrackets = (state, pos, config) => {
26
34
  }
27
35
  return null;
28
36
  };
37
+ /**
38
+ * @ignore
39
+ * @test
40
+ */
29
41
  export const trySelectMatchingBrackets = (state, pos, dir, config, inside = false) => {
30
42
  if (pos < 0) {
31
43
  return false;
@@ -36,6 +48,10 @@ export const trySelectMatchingBrackets = (state, pos, dir, config, inside = fals
36
48
  head: match.end[rightInside ? 'from' : 'to'],
37
49
  };
38
50
  };
51
+ /**
52
+ * @ignore
53
+ * @test
54
+ */
39
55
  export const selectMatchingBrackets = (state, pos, config) => trySelectMatchingBrackets(state, pos, -1, config)
40
56
  || trySelectMatchingBrackets(state, pos, 1, config)
41
57
  || trySelectMatchingBrackets(state, pos + 1, -1, config, true)
@@ -4,11 +4,12 @@ import type { EditorState } from '@codemirror/state';
4
4
  import type { MatchResult } from '@codemirror/language';
5
5
  import type { SyntaxNode } from '@lezer/common';
6
6
  declare type TagType = 'ext' | 'html';
7
- declare interface TagMatchResult extends MatchResult {
7
+ export interface TagMatchResult extends MatchResult {
8
8
  start: Tag;
9
9
  end?: Tag;
10
10
  }
11
- declare class Tag {
11
+ /** @test */
12
+ export declare class Tag {
12
13
  readonly type: TagType;
13
14
  readonly name: string;
14
15
  readonly first: SyntaxNode;
@@ -24,12 +25,14 @@ declare class Tag {
24
25
  * 获取标签信息,破损的HTML标签会返回`null`
25
26
  * @param state
26
27
  * @param node 语法树节点
28
+ * @test
27
29
  */
28
30
  export declare const getTag: (state: EditorState, node: SyntaxNode) => Tag | null;
29
31
  /**
30
32
  * 匹配标签
31
33
  * @param state
32
34
  * @param pos 位置
35
+ * @test
33
36
  */
34
37
  export declare const matchTag: (state: EditorState, pos: number) => TagMatchResult | null;
35
38
  declare const _default: StateField<DecorationSet>;
package/dist/matchTag.js CHANGED
@@ -3,7 +3,9 @@ import { StateField } from '@codemirror/state';
3
3
  import { ensureSyntaxTree } from '@codemirror/language';
4
4
  import { voidHtmlTags, selfClosingTags } from './config.js';
5
5
  import { matchingCls, nonmatchingCls } from './constants.js';
6
- class Tag {
6
+ import { sliceDoc } from './util.js';
7
+ /** @test */
8
+ export class Tag {
7
9
  get closing() {
8
10
  return isClosing(this.first, this.type, this.state, true);
9
11
  }
@@ -32,13 +34,17 @@ const isTag = ({ name }) => /-(?:ext|html)tag-(?!bracket)/u.test(name), isTagCom
32
34
  const reHtml = new RegExp(`-htmltag-${s}`, 'u'), reExt = new RegExp(`-exttag-${s}`, 'u');
33
35
  return ({ name }, type) => (type === 'ext' ? reExt : reHtml).test(name);
34
36
  }, isBracket = isTagComponent('bracket'), isName = isTagComponent('name'), isClosing = (node, type, state, first) => isBracket(node, type)
35
- && state.sliceDoc(node.from, node.to)[first ? 'endsWith' : 'startsWith']('/'), getName = (state, { from, to }) => state.sliceDoc(from, to).trim().toLowerCase();
37
+ && sliceDoc(state, node)[first ? 'endsWith' : 'startsWith']('/'), getName = (state, node) => sliceDoc(state, node).trim().toLowerCase();
36
38
  /**
37
39
  * 获取标签信息,破损的HTML标签会返回`null`
38
40
  * @param state
39
41
  * @param node 语法树节点
42
+ * @test
40
43
  */
41
44
  export const getTag = (state, node) => {
45
+ if (!isTag(node)) {
46
+ return null;
47
+ }
42
48
  const type = node.name.includes('exttag') ? 'ext' : 'html';
43
49
  let { nextSibling, prevSibling } = node, nameNode = isName(node, type) ? node : null;
44
50
  while (nextSibling && !isBracket(nextSibling, type)) {
@@ -87,18 +93,16 @@ const searchTag = (state, origin) => {
87
93
  * 匹配标签
88
94
  * @param state
89
95
  * @param pos 位置
96
+ * @test
90
97
  */
91
98
  export const matchTag = (state, pos) => {
92
99
  const tree = ensureSyntaxTree(state, pos);
93
100
  if (!tree) {
94
101
  return null;
95
102
  }
96
- let node = tree.resolve(pos, -1);
103
+ let node = tree.resolveInner(pos, -1);
97
104
  if (!isTag(node)) {
98
- node = tree.resolve(pos, 1);
99
- if (!isTag(node)) {
100
- return null;
101
- }
105
+ node = tree.resolveInner(pos, 1);
102
106
  }
103
107
  const start = getTag(state, node);
104
108
  if (!start) {
@@ -4,6 +4,7 @@
4
4
  * @see https://gerrit.wikimedia.org/g/mediawiki/extensions/CodeMirror
5
5
  */
6
6
  import { LanguageSupport } from '@codemirror/language';
7
+ import { EditorView } from '@codemirror/view';
7
8
  import { MediaWiki } from './token.js';
8
9
  import type { StreamParser, TagStyle } from '@codemirror/language';
9
10
  import type { CompletionSource, Completion } from '@codemirror/autocomplete';
@@ -13,8 +14,19 @@ import type { MwConfig } from './token';
13
14
  * @param name 节点名称
14
15
  */
15
16
  export declare const isWikiLink: (name: string) => boolean;
17
+ /**
18
+ * 检查首字母大小写并插入正确的自动填充内容
19
+ * @param view
20
+ * @param completion 自动填充内容
21
+ * @param from 起始位置
22
+ * @param to 结束位置
23
+ * @test
24
+ */
25
+ export declare const apply: (view: EditorView, completion: Completion, from: number, to: number) => void;
26
+ /** @test */
16
27
  export declare class FullMediaWiki extends MediaWiki {
17
28
  #private;
29
+ readonly templatedata: boolean;
18
30
  readonly nsRegex: RegExp;
19
31
  readonly functionSynonyms: Completion[];
20
32
  readonly doubleUnderscore: Completion[];
@@ -25,7 +37,7 @@ export declare class FullMediaWiki extends MediaWiki {
25
37
  readonly htmlAttrs: Completion[];
26
38
  readonly elementAttrs: Map<string | undefined, Completion[]>;
27
39
  readonly extAttrs: Map<string, Completion[]>;
28
- constructor(config: MwConfig);
40
+ constructor(config: MwConfig, templatedata?: boolean);
29
41
  /**
30
42
  * This defines the actual CSS class assigned to each tag/token.
31
43
  *
@@ -39,5 +51,6 @@ export declare class FullMediaWiki extends MediaWiki {
39
51
  /**
40
52
  * Get a LanguageSupport instance for the MediaWiki mode.
41
53
  * @param config Configuration for the MediaWiki mode
54
+ * @param templatedata Whether to enable template parameter autocompletion
42
55
  */
43
- export declare const mediawikiBase: (config: MwConfig) => LanguageSupport;
56
+ export declare const mediawikiBase: (config: MwConfig, templatedata?: boolean) => LanguageSupport;
package/dist/mediawiki.js CHANGED
@@ -9,9 +9,10 @@ import { insertCompletionText, pickedCompletion } from '@codemirror/autocomplete
9
9
  import { isUnderscore } from '@bhsd/cm-util';
10
10
  import { commonHtmlAttrs, htmlAttrs, extAttrs } from 'wikiparser-node/dist/util/sharable.mjs';
11
11
  import { htmlTags, tokens } from './config.js';
12
- import { isWMF, isolateSelector, ltrSelector } from './constants.js';
12
+ import { hoverSelector, isWMF, } from './constants.js';
13
13
  import { MediaWiki } from './token.js';
14
- import { hasTag, braceStackUpdate, leadingSpaces, } from './util.js';
14
+ import { hasTag, leadingSpaces, findTemplateName, } from './util.js';
15
+ const ranks = { Required: 1, Suggested: 2, Optional: 3, Deprecated: 4 };
15
16
  /**
16
17
  * 是否是普通维基链接
17
18
  * @param name 节点名称
@@ -23,8 +24,9 @@ export const isWikiLink = (name) => /mw-[\w-]*link-ground/u.test(name);
23
24
  * @param completion 自动填充内容
24
25
  * @param from 起始位置
25
26
  * @param to 结束位置
27
+ * @test
26
28
  */
27
- const apply = (view, completion, from, to) => {
29
+ export const apply = (view, completion, from, to) => {
28
30
  let { label } = completion, selection;
29
31
  const initial = label.charAt(0).toLowerCase(), { state } = view, after = state.sliceDoc(to);
30
32
  if (state.sliceDoc(from, from + 1) === initial) {
@@ -40,10 +42,12 @@ const apply = (view, completion, from, to) => {
40
42
  selection,
41
43
  });
42
44
  };
45
+ /** @test */
43
46
  export class FullMediaWiki extends MediaWiki {
44
- constructor(config) {
47
+ constructor(config, templatedata = false) {
45
48
  super(config);
46
49
  const { urlProtocols, nsid, functionSynonyms, doubleUnderscore, } = config;
50
+ this.templatedata = templatedata;
47
51
  this.nsRegex = new RegExp(String.raw `^(${Object.keys(nsid).filter(ns => ns !== '').join('|')
48
52
  .replace(/_/gu, ' ')})\s*:\s*`, 'iu');
49
53
  this.functionSynonyms = functionSynonyms.flatMap((obj, i) => Object.keys(obj).map((label) => ({
@@ -95,7 +99,6 @@ export class FullMediaWiki extends MediaWiki {
95
99
  const parser = super.mediawiki(tags);
96
100
  parser.languageData = {
97
101
  closeBrackets: { brackets: ['(', '[', '{', '"'], before: ')]}>' },
98
- autocomplete: this.completionSource,
99
102
  };
100
103
  return parser;
101
104
  }
@@ -155,22 +158,29 @@ export class FullMediaWiki extends MediaWiki {
155
158
  return result?.length
156
159
  ? {
157
160
  offset: leadingSpaces(search).length,
158
- options: result.map(([key, detail]) => ({ type: 'variable', label: key + equal, detail })),
161
+ options: result.flatMap(([keys, detail, info, name]) => keys.map((key) => ({
162
+ type: 'variable',
163
+ label: key + equal,
164
+ section: { name: name, rank: ranks[name] },
165
+ ...detail && { detail },
166
+ ...info && { info },
167
+ }))),
159
168
  }
160
169
  : undefined;
161
170
  }
162
171
  /** 自动补全魔术字和标签名 */
163
172
  get completionSource() {
164
173
  return async (context) => {
165
- const { state, pos, explicit } = context, node = syntaxTree(state).resolve(pos, -1), { name: n, from: f, to: t, } = node, types = new Set(n.split('_')), isParserFunction = hasTag(types, 'parserFunctionName'),
174
+ const { state, pos, explicit } = context, node = syntaxTree(state).resolveInner(pos, -1), { name: n, prevSibling, from: f, to: t, } = node, types = new Set(n.split('_')), isParserFunction = hasTag(types, 'parserFunctionName'),
166
175
  /** 开头不包含` `,但可能包含`_` */ search = state.sliceDoc(f, pos).trimStart(), start = pos - search.length;
167
- let { prevSibling } = node;
168
- if (explicit || isParserFunction && search.includes('#') || isWMF) {
176
+ // 需要opensearch API的建议,只在显式触发时或WMF网站上提供
177
+ if (explicit || isWMF || isParserFunction && search.includes('#')) {
169
178
  const obj = isWMF
170
179
  ? null
171
180
  : {
172
181
  validFor: /^[^|{}<>[\]#]*$/u,
173
182
  };
183
+ // 模板名
174
184
  if (isParserFunction || hasTag(types, 'templateName')) {
175
185
  const options = search.includes(':') ? [] : [...this.functionSynonyms], suggestions = await this.#linkSuggest(search, 10) ?? { offset: 0, options: [] };
176
186
  options.push(...suggestions.options);
@@ -189,8 +199,12 @@ export class FullMediaWiki extends MediaWiki {
189
199
  ...obj,
190
200
  };
191
201
  }
202
+ // 页面名
192
203
  const isPage = hasTag(types, 'pageName') && hasTag(types, 'parserFunction') || 0;
193
204
  if (isPage && search.trim() || hasTag(types, 'linkPageName')) {
205
+ if (!this.config.linkSuggest) {
206
+ return null;
207
+ }
194
208
  const isLink = isWikiLink(n);
195
209
  let prefix = '', ns = 0;
196
210
  if (isPage) {
@@ -218,34 +232,18 @@ export class FullMediaWiki extends MediaWiki {
218
232
  ...obj,
219
233
  };
220
234
  }
235
+ }
236
+ // 需要TemplateData API的建议,只在显式触发时提供
237
+ if (this.config.paramSuggest
238
+ && (explicit || this.templatedata)
239
+ && this.tags.includes('templatedata')) {
221
240
  const isArgument = hasTag(types, 'templateArgumentName'), prevIsDelimiter = prevSibling?.name.includes(tokens.templateDelimiter), isDelimiter = hasTag(types, 'templateDelimiter')
222
241
  || hasTag(types, 'templateBracket') && prevIsDelimiter;
223
- if (this.tags.includes('templatedata')
224
- && (isDelimiter
225
- || isArgument && !search.includes('=')
226
- || hasTag(types, 'template') && prevIsDelimiter)) {
227
- let stack = -1,
228
- /** 可包含`_`、`:`等 */ page = '';
229
- while (prevSibling) {
230
- const { name, from, to } = prevSibling;
231
- if (name.includes(tokens.templateBracket)) {
232
- const [lbrace, rbrace] = braceStackUpdate(state, prevSibling);
233
- stack += lbrace;
234
- if (stack >= 0) {
235
- break;
236
- }
237
- stack += rbrace;
238
- }
239
- else if (stack === -1 && name.includes(tokens.templateName)) {
240
- page = state.sliceDoc(from, to) + page;
241
- }
242
- else if (page && !name.includes(tokens.comment)) {
243
- prevSibling = null;
244
- break;
245
- }
246
- ({ prevSibling } = prevSibling);
247
- }
248
- if (prevSibling && page) {
242
+ if (isDelimiter
243
+ || isArgument && !search.includes('=')
244
+ || hasTag(types, 'template') && prevIsDelimiter) {
245
+ const page = findTemplateName(state, node);
246
+ if (page) {
249
247
  const equal = isArgument && state.sliceDoc(pos, t).trim() === '=' ? '' : '=', suggestions = await this.#paramSuggest(isDelimiter ? '' : search, page, equal);
250
248
  if (suggestions && suggestions.options.length > 0) {
251
249
  return {
@@ -293,10 +291,12 @@ export class FullMediaWiki extends MediaWiki {
293
291
  'comment',
294
292
  'templateVariableName',
295
293
  'templateName',
294
+ 'parserFunctionName',
296
295
  'linkPageName',
297
296
  'linkToSection',
298
297
  'extLink',
299
298
  ])) {
299
+ // 不可能是状态开关、标签、协议或图片参数名
300
300
  return null;
301
301
  }
302
302
  let mt = context.matchBefore(/__(?:(?!__)[\p{L}\p{N}_])*$/u);
@@ -374,7 +374,7 @@ const getSelector = (cls, prefix = '') => typeof prefix === 'string'
374
374
  : prefix.map(p => getSelector(cls, p)).join();
375
375
  const getGround = (type, ground) => ground ? `${type}${ground === 1 ? '' : ground}-` : '';
376
376
  const getGrounds = (grounds, r, g, b, a) => ({
377
- [grounds.map(([template, ext, link]) => `.cm-mw-${getGround('template', template)}${getGround('exttag', ext)}${getGround('link', link)}ground`).join()]: {
377
+ [grounds.map(([template, ext, link]) => `.cm-mw-${getGround('template', template)}${getGround('ext', ext)}${getGround('link', link)}ground`).join()]: {
378
378
  backgroundColor: `rgb(${r},${g},${b},${a})`,
379
379
  },
380
380
  });
@@ -429,7 +429,7 @@ const theme = /* @__PURE__ */ EditorView.theme({
429
429
  color: 'var(--cm-tpl)',
430
430
  fontWeight: 'bold',
431
431
  },
432
- '.cm-mw-template-argument-name': {
432
+ [getSelector(['-argument-name'], ['template', 'parserfunction'])]: {
433
433
  color: 'var(--cm-arg)',
434
434
  fontWeight: 'normal',
435
435
  },
@@ -507,19 +507,33 @@ const theme = /* @__PURE__ */ EditorView.theme({
507
507
  '.cm-mw-tag-ref': {
508
508
  backgroundColor: 'var(--cm-ref)',
509
509
  },
510
- [`${isolateSelector}, &[dir="rtl"] .cm-mw-template-name`]: {
511
- unicodeBidi: 'isolate',
510
+ // hover tooltip and signature tooltip
511
+ [hoverSelector]: {
512
+ padding: '2px 5px',
513
+ width: 'max-content',
514
+ maxWidth: '60vw',
515
+ maxHeight: '60vh',
516
+ overflowY: 'auto',
517
+ },
518
+ [`${hoverSelector} *`]: {
519
+ marginTop: '0!important',
520
+ marginBottom: '0!important',
512
521
  },
513
- [ltrSelector]: {
514
- direction: 'ltr',
515
- display: 'inline-block',
522
+ [`${hoverSelector}>div`]: {
523
+ fontSize: '90%',
524
+ lineHeight: 1.4,
516
525
  },
517
526
  });
518
527
  /**
519
528
  * Get a LanguageSupport instance for the MediaWiki mode.
520
529
  * @param config Configuration for the MediaWiki mode
530
+ * @param templatedata Whether to enable template parameter autocompletion
521
531
  */
522
- export const mediawikiBase = (config) => {
523
- const mode = new FullMediaWiki(config), lang = StreamLanguage.define(mode.mediawiki());
524
- return new LanguageSupport(lang, [syntaxHighlighting(HighlightStyle.define(mode.getTagStyles())), theme]);
532
+ export const mediawikiBase = (config, templatedata) => {
533
+ const mode = new FullMediaWiki(config, templatedata), lang = StreamLanguage.define(mode.mediawiki());
534
+ return new LanguageSupport(lang, [
535
+ syntaxHighlighting(HighlightStyle.define(mode.getTagStyles())),
536
+ theme,
537
+ lang.data.of({ autocomplete: mode.completionSource }),
538
+ ]);
525
539
  };