@bhsd/codemirror-mediawiki 2.28.0 → 2.29.0
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/README.md +58 -11
- package/dist/bidi.js +84 -0
- package/dist/codemirror.d.ts +2 -0
- package/dist/codemirror.js +608 -0
- package/dist/color.js +52 -0
- package/dist/config.js +119 -0
- package/dist/css.js +30 -0
- package/dist/escape.js +35 -0
- package/dist/fold.d.ts +0 -1
- package/dist/fold.js +445 -0
- package/dist/hover.js +52 -0
- package/dist/indent.js +50 -0
- package/dist/inlay.js +68 -0
- package/dist/javascript.js +5 -0
- package/dist/keybindings.js +35 -0
- package/dist/keymap.js +36 -0
- package/dist/linter.d.ts +9 -8
- package/dist/linter.js +134 -0
- package/dist/lua.js +428 -0
- package/dist/main.min.js +25 -29
- package/dist/matchBrackets.d.ts +7 -0
- package/dist/matchBrackets.js +58 -0
- package/dist/matchTag.js +139 -0
- package/dist/mediawiki.js +443 -0
- package/dist/mw.min.js +31 -35
- package/dist/{mwConfig.mjs → mwConfig.js} +6 -10
- package/dist/openLinks.js +97 -0
- package/dist/ref.js +85 -0
- package/dist/signature.js +69 -0
- package/dist/static.js +46 -0
- package/dist/statusBar.js +138 -0
- package/dist/token.js +1888 -0
- package/dist/wiki.min.js +33 -37
- package/i18n/en.json +1 -1
- package/i18n/zh-hans.json +1 -1
- package/i18n/zh-hant.json +1 -1
- package/mediawiki.css +1 -1
- package/package.json +19 -18
- package/dist/keybindings.d.mts +0 -15
- package/dist/keybindings.mjs +0 -34
- package/dist/linter.d.mts +0 -43
- package/dist/linter.mjs +0 -132
- /package/dist/{mwConfig.d.mts → mwConfig.d.ts} +0 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Extension, EditorState } from '@codemirror/state';
|
|
2
|
+
import type { Config, MatchResult } from '@codemirror/language';
|
|
3
|
+
import type { SyntaxNode } from '@lezer/common';
|
|
4
|
+
export declare const findEnclosingBrackets: (node: SyntaxNode, pos: number, brackets: string) => MatchResult | undefined;
|
|
5
|
+
export declare const findEnclosingPlainBrackets: (state: EditorState, pos: number, config: Required<Config>) => MatchResult | null;
|
|
6
|
+
declare const _default: (configs: Config) => Extension;
|
|
7
|
+
export default _default;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Decoration } from '@codemirror/view';
|
|
2
|
+
import { bracketMatching, matchBrackets, syntaxTree } from '@codemirror/language';
|
|
3
|
+
export const findEnclosingBrackets = (node, pos, brackets) => {
|
|
4
|
+
let parent = node;
|
|
5
|
+
while (parent) {
|
|
6
|
+
const { firstChild, lastChild } = parent;
|
|
7
|
+
if (firstChild && lastChild) {
|
|
8
|
+
const i = brackets.indexOf(firstChild.name), j = brackets.indexOf(lastChild.name);
|
|
9
|
+
if (i !== -1 && j !== -1 && i % 2 === 0 && j % 2 === 1 && firstChild.from < pos && lastChild.to > pos) {
|
|
10
|
+
return { start: firstChild, end: lastChild, matched: true };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
({ parent } = parent); // eslint-disable-line no-param-reassign
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
};
|
|
17
|
+
export const findEnclosingPlainBrackets = (state, pos, config) => {
|
|
18
|
+
const { brackets, maxScanDistance } = config, re = new RegExp(`[${
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-spread
|
|
20
|
+
[...brackets].filter((_, i) => i % 2).map(c => c === ']' ? String.raw `\]` : c).join('')}]`, 'gu'), str = state.sliceDoc(pos, pos + maxScanDistance);
|
|
21
|
+
let mt = re.exec(str);
|
|
22
|
+
while (mt) {
|
|
23
|
+
const result = matchBrackets(state, pos + mt.index + 1, -1, config), left = result?.end?.to;
|
|
24
|
+
if (left !== undefined && left <= pos) {
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
mt = re.exec(str);
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
};
|
|
31
|
+
export default (configs) => {
|
|
32
|
+
const extension = bracketMatching(configs), [{ facet }, [field]] = extension;
|
|
33
|
+
Object.assign(field, {
|
|
34
|
+
updateF(value, { state, docChanged, selection }) {
|
|
35
|
+
if (!docChanged && !selection) {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
const decorations = [], config = state.facet(facet), { afterCursor, brackets, renderMatch } = config;
|
|
39
|
+
for (const { empty, head } of state.selection.ranges) {
|
|
40
|
+
if (!empty) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const tree = syntaxTree(state), match = matchBrackets(state, head, -1, config)
|
|
44
|
+
|| head > 0 && matchBrackets(state, head - 1, 1, config)
|
|
45
|
+
|| afterCursor && (matchBrackets(state, head, 1, config)
|
|
46
|
+
|| head < state.doc.length && matchBrackets(state, head + 1, -1, config))
|
|
47
|
+
|| findEnclosingBrackets(tree.resolveInner(head, -1), head, brackets)
|
|
48
|
+
|| afterCursor && findEnclosingBrackets(tree.resolveInner(head, 1), head, brackets)
|
|
49
|
+
|| findEnclosingPlainBrackets(state, head, config);
|
|
50
|
+
if (match) {
|
|
51
|
+
decorations.push(...renderMatch(match, state));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return Decoration.set(decorations, true);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
return extension;
|
|
58
|
+
};
|
package/dist/matchTag.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Decoration, EditorView } from '@codemirror/view';
|
|
2
|
+
import { StateField } from '@codemirror/state';
|
|
3
|
+
import { ensureSyntaxTree } from '@codemirror/language';
|
|
4
|
+
import { voidHtmlTags, selfClosingTags } from './config';
|
|
5
|
+
class Tag {
|
|
6
|
+
get closing() {
|
|
7
|
+
return isClosing(this.first, this.type, this.state, true);
|
|
8
|
+
}
|
|
9
|
+
get selfClosing() {
|
|
10
|
+
return voidHtmlTags.includes(this.name)
|
|
11
|
+
|| (this.type === 'ext' || selfClosingTags.includes(this.name))
|
|
12
|
+
&& isClosing(this.last, this.type, this.state);
|
|
13
|
+
}
|
|
14
|
+
get from() {
|
|
15
|
+
const { first: { from, to }, state } = this;
|
|
16
|
+
return from + state.sliceDoc(from, to).lastIndexOf('<');
|
|
17
|
+
}
|
|
18
|
+
get to() {
|
|
19
|
+
const { last: { from, to }, state } = this;
|
|
20
|
+
return from + state.sliceDoc(from, to).indexOf('>') + 1;
|
|
21
|
+
}
|
|
22
|
+
constructor(type, name, first, last, state) {
|
|
23
|
+
this.type = type;
|
|
24
|
+
this.name = name;
|
|
25
|
+
this.first = first;
|
|
26
|
+
this.last = last;
|
|
27
|
+
this.state = state;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const isTag = ({ name }) => /-(?:ext|html)tag-(?!bracket)/u.test(name), isTagComponent = (s) => {
|
|
31
|
+
const reHtml = new RegExp(`-htmltag-${s}`, 'u'), reExt = new RegExp(`-exttag-${s}`, 'u');
|
|
32
|
+
return ({ name }, type) => (type === 'ext' ? reExt : reHtml).test(name);
|
|
33
|
+
}, isBracket = isTagComponent('bracket'), isName = isTagComponent('name'), isClosing = (node, type, state, first) => isBracket(node, type)
|
|
34
|
+
&& state.sliceDoc(node.from, node.to)[first ? 'endsWith' : 'startsWith']('/'), getName = (state, { from, to }) => state.sliceDoc(from, to).trim().toLowerCase();
|
|
35
|
+
/**
|
|
36
|
+
* 获取标签信息,破损的HTML标签会返回`null`
|
|
37
|
+
* @param state
|
|
38
|
+
* @param node 语法树节点
|
|
39
|
+
*/
|
|
40
|
+
export const getTag = (state, node) => {
|
|
41
|
+
const type = node.name.includes('exttag') ? 'ext' : 'html';
|
|
42
|
+
let { nextSibling, prevSibling } = node, nameNode = isName(node, type) ? node : null;
|
|
43
|
+
while (nextSibling && !isBracket(nextSibling, type)) {
|
|
44
|
+
({ nextSibling } = nextSibling);
|
|
45
|
+
}
|
|
46
|
+
if (!nextSibling
|
|
47
|
+
|| isBracket(nextSibling, type) && state.sliceDoc(nextSibling.from, nextSibling.from + 1) === '<') {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
while (prevSibling && !isBracket(prevSibling, type)) {
|
|
51
|
+
nameNode ??= isName(prevSibling, type) ? prevSibling : null;
|
|
52
|
+
({ prevSibling } = prevSibling);
|
|
53
|
+
}
|
|
54
|
+
const name = getName(state, nameNode);
|
|
55
|
+
return new Tag(type, name, prevSibling, nextSibling, state);
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* 搜索匹配的标签
|
|
59
|
+
* @param state
|
|
60
|
+
* @param origin 起始标签
|
|
61
|
+
*/
|
|
62
|
+
const searchTag = (state, origin) => {
|
|
63
|
+
const { type, name, closing } = origin, siblingGetter = closing ? 'prevSibling' : 'nextSibling', endGetter = closing ? 'first' : 'last';
|
|
64
|
+
let stack = closing ? -1 : 1, sibling = origin[endGetter][siblingGetter];
|
|
65
|
+
while (sibling) {
|
|
66
|
+
if (isName(sibling, type) && getName(state, sibling) === name) {
|
|
67
|
+
const tag = getTag(state, sibling);
|
|
68
|
+
if (tag) {
|
|
69
|
+
if (tag.closing) {
|
|
70
|
+
stack--;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
stack += tag.selfClosing ? 0 : 1;
|
|
74
|
+
}
|
|
75
|
+
if (stack === 0) {
|
|
76
|
+
return tag;
|
|
77
|
+
}
|
|
78
|
+
sibling = tag[endGetter];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
sibling = sibling[siblingGetter];
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* 匹配标签
|
|
87
|
+
* @param state
|
|
88
|
+
* @param pos 位置
|
|
89
|
+
*/
|
|
90
|
+
export const matchTag = (state, pos) => {
|
|
91
|
+
const tree = ensureSyntaxTree(state, pos);
|
|
92
|
+
if (!tree) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
let node = tree.resolve(pos, -1);
|
|
96
|
+
if (!isTag(node)) {
|
|
97
|
+
node = tree.resolve(pos, 1);
|
|
98
|
+
if (!isTag(node)) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const start = getTag(state, node);
|
|
103
|
+
if (!start) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
else if (start.selfClosing) {
|
|
107
|
+
return { matched: true, start };
|
|
108
|
+
}
|
|
109
|
+
const end = searchTag(state, start);
|
|
110
|
+
return end ? { matched: true, start, end } : { matched: false, start };
|
|
111
|
+
};
|
|
112
|
+
const matchingMark = Decoration.mark({ class: 'cm-matchingTag' }), nonmatchingMark = Decoration.mark({ class: 'cm-nonmatchingTag' });
|
|
113
|
+
export default StateField.define({
|
|
114
|
+
create() {
|
|
115
|
+
return Decoration.none;
|
|
116
|
+
},
|
|
117
|
+
update(deco, { docChanged, selection, state }) {
|
|
118
|
+
if (!docChanged && !selection) {
|
|
119
|
+
return deco;
|
|
120
|
+
}
|
|
121
|
+
const decorations = [];
|
|
122
|
+
for (const range of state.selection.ranges) {
|
|
123
|
+
if (range.empty) {
|
|
124
|
+
const match = matchTag(state, range.head);
|
|
125
|
+
if (match) {
|
|
126
|
+
const mark = match.matched ? matchingMark : nonmatchingMark, { start: { from, to, closing }, end } = match;
|
|
127
|
+
decorations.push(mark.range(from, to));
|
|
128
|
+
if (end) {
|
|
129
|
+
decorations[closing ? 'unshift' : 'push'](mark.range(end.from, end.to));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return Decoration.set(decorations);
|
|
135
|
+
},
|
|
136
|
+
provide(f) {
|
|
137
|
+
return EditorView.decorations.from(f);
|
|
138
|
+
},
|
|
139
|
+
});
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author MusikAnimal, Bhsd and others
|
|
3
|
+
* @license GPL-2.0-or-later
|
|
4
|
+
* @see https://gerrit.wikimedia.org/g/mediawiki/extensions/CodeMirror
|
|
5
|
+
*/
|
|
6
|
+
import { HighlightStyle, LanguageSupport, StreamLanguage, syntaxHighlighting, syntaxTree, } from '@codemirror/language';
|
|
7
|
+
import { insertCompletionText, pickedCompletion } from '@codemirror/autocomplete';
|
|
8
|
+
import { commonHtmlAttrs, htmlAttrs, extAttrs } from 'wikiparser-node/dist/util/sharable.mjs';
|
|
9
|
+
import { MediaWiki } from './token';
|
|
10
|
+
import { htmlTags, tokens } from './config';
|
|
11
|
+
import { braceStackUpdate } from './fold';
|
|
12
|
+
const wmf = /\.(?:wiktionary|wiki(?:pedia|books|news|quote|source|versity|voyage))\.org$/u;
|
|
13
|
+
/**
|
|
14
|
+
* 检查首字母大小写并插入正确的自动填充内容
|
|
15
|
+
* @param view
|
|
16
|
+
* @param completion 自动填充内容
|
|
17
|
+
* @param from 起始位置
|
|
18
|
+
* @param to 结束位置
|
|
19
|
+
*/
|
|
20
|
+
const apply = (view, completion, from, to) => {
|
|
21
|
+
let { label } = completion;
|
|
22
|
+
const initial = label.charAt(0).toLowerCase();
|
|
23
|
+
if (view.state.sliceDoc(from, from + 1) === initial) {
|
|
24
|
+
label = initial + label.slice(1);
|
|
25
|
+
}
|
|
26
|
+
view.dispatch({
|
|
27
|
+
...insertCompletionText(view.state, label, from, to),
|
|
28
|
+
annotations: pickedCompletion.of(completion),
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* 判断节点是否包含指定类型
|
|
33
|
+
* @param types 节点类型
|
|
34
|
+
* @param names 指定类型
|
|
35
|
+
*/
|
|
36
|
+
export const hasTag = (types, names) => (Array.isArray(names) ? names : [names]).some(name => types.has(name in tokens ? tokens[name] : name));
|
|
37
|
+
export class FullMediaWiki extends MediaWiki {
|
|
38
|
+
constructor(config) {
|
|
39
|
+
super(config);
|
|
40
|
+
const { urlProtocols, nsid, functionSynonyms, doubleUnderscore, } = config;
|
|
41
|
+
this.nsRegex = new RegExp(String.raw `^(${Object.keys(nsid).filter(Boolean).join('|').replace(/_/gu, ' ')})\s*:\s*`, 'iu');
|
|
42
|
+
this.functionSynonyms = functionSynonyms.flatMap((obj, i) => Object.keys(obj).map((label) => ({
|
|
43
|
+
type: i ? 'constant' : 'function',
|
|
44
|
+
label,
|
|
45
|
+
})));
|
|
46
|
+
this.doubleUnderscore = doubleUnderscore.flatMap(Object.keys).map((label) => ({
|
|
47
|
+
type: 'constant',
|
|
48
|
+
label,
|
|
49
|
+
}));
|
|
50
|
+
this.extTags = this.tags.map((label) => ({ type: 'type', label }));
|
|
51
|
+
this.htmlTags = htmlTags.filter(tag => !this.tags.includes(tag)).map((label) => ({
|
|
52
|
+
type: 'type',
|
|
53
|
+
label,
|
|
54
|
+
}));
|
|
55
|
+
this.protocols = urlProtocols.split('|').map((label) => ({
|
|
56
|
+
type: 'namespace',
|
|
57
|
+
label: label.replace(/\\\//gu, '/'),
|
|
58
|
+
}));
|
|
59
|
+
this.imgKeys = this.img.map((label) => label.endsWith('$1')
|
|
60
|
+
? { type: 'property', label: label.slice(0, -2), detail: '$1' }
|
|
61
|
+
: { type: 'keyword', label });
|
|
62
|
+
this.htmlAttrs = [
|
|
63
|
+
...[...commonHtmlAttrs].map((label) => ({ type: 'property', label })),
|
|
64
|
+
{ type: 'variable', label: 'data-', detail: '*' },
|
|
65
|
+
{ type: 'namespace', label: 'xmlns:', detail: '*' },
|
|
66
|
+
];
|
|
67
|
+
this.elementAttrs = new Map(Object.entries(htmlAttrs).map(([key, value]) => [
|
|
68
|
+
key,
|
|
69
|
+
[...value].map((label) => ({ type: 'property', label })),
|
|
70
|
+
]));
|
|
71
|
+
this.extAttrs = new Map(Object.entries(extAttrs).map(([key, value]) => [
|
|
72
|
+
key,
|
|
73
|
+
[...value].map((label) => ({ type: 'property', label })),
|
|
74
|
+
]));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* This defines the actual CSS class assigned to each tag/token.
|
|
78
|
+
*
|
|
79
|
+
* @see https://codemirror.net/docs/ref/#language.TagStyle
|
|
80
|
+
*/
|
|
81
|
+
getTagStyles() {
|
|
82
|
+
return Object.keys(this.tokenTable).map((className) => ({
|
|
83
|
+
tag: this.tokenTable[className],
|
|
84
|
+
class: `cm-${className}`,
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
mediawiki(tags) {
|
|
88
|
+
const parser = super.mediawiki(tags);
|
|
89
|
+
parser.languageData = {
|
|
90
|
+
closeBrackets: { brackets: ['(', '[', '{', '"'], before: ')]}>' },
|
|
91
|
+
autocomplete: this.completionSource,
|
|
92
|
+
};
|
|
93
|
+
return parser;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 提供链接建议
|
|
97
|
+
* @param str 搜索字符串,开头不包含` `,但可能包含`_`
|
|
98
|
+
* @param ns 命名空间
|
|
99
|
+
*/
|
|
100
|
+
async #linkSuggest(str, ns = 0) {
|
|
101
|
+
const { config: { linkSuggest, nsid }, nsRegex } = this;
|
|
102
|
+
if (typeof linkSuggest !== 'function' || /[|{}<>[\]#]/u.test(str)) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
let subpage = false, search = str, offset = 0;
|
|
106
|
+
/* eslint-disable no-param-reassign */
|
|
107
|
+
if (search.startsWith('/')) {
|
|
108
|
+
ns = 0;
|
|
109
|
+
subpage = true;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
search = search.replace(/_/gu, ' ');
|
|
113
|
+
const mt = /^\s*/u.exec(search);
|
|
114
|
+
[{ length: offset }] = mt;
|
|
115
|
+
search = search.slice(offset);
|
|
116
|
+
if (search.startsWith(':')) {
|
|
117
|
+
const [{ length }] = /^:\s*/u.exec(search);
|
|
118
|
+
offset += length;
|
|
119
|
+
search = search.slice(length);
|
|
120
|
+
ns = 0;
|
|
121
|
+
}
|
|
122
|
+
if (!search) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
const mt2 = nsRegex.exec(search);
|
|
126
|
+
if (mt2) {
|
|
127
|
+
const [{ length }, prefix] = mt2;
|
|
128
|
+
ns = nsid[prefix.replace(/ /gu, '_').toLowerCase()] || 1;
|
|
129
|
+
offset += length;
|
|
130
|
+
search = `${ns === -2 ? 'File' : prefix}:${search.slice(length)}`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/* eslint-enable no-param-reassign */
|
|
134
|
+
const underscore = str.slice(offset).includes('_');
|
|
135
|
+
return {
|
|
136
|
+
offset,
|
|
137
|
+
options: (await linkSuggest(search, ns, subpage)).map(([label]) => ({
|
|
138
|
+
type: 'text',
|
|
139
|
+
label: underscore ? label.replace(/ /gu, '_') : label,
|
|
140
|
+
})),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 提供模板参数建议
|
|
145
|
+
* @param search 搜索字符串
|
|
146
|
+
* @param page 模板名,可包含`_`、`:`等
|
|
147
|
+
* @param equal 是否有等号
|
|
148
|
+
*/
|
|
149
|
+
async #paramSuggest(search, page, equal) {
|
|
150
|
+
const { config: { paramSuggest } } = this;
|
|
151
|
+
return page && typeof paramSuggest === 'function' && !/[|{}<>[\]]/u.test(page)
|
|
152
|
+
? {
|
|
153
|
+
offset: /^\s*/u.exec(search)[0].length,
|
|
154
|
+
options: (await paramSuggest(page))
|
|
155
|
+
.map(([key, detail]) => ({ type: 'variable', label: key + equal, detail })),
|
|
156
|
+
}
|
|
157
|
+
: undefined;
|
|
158
|
+
}
|
|
159
|
+
/** 自动补全魔术字和标签名 */
|
|
160
|
+
get completionSource() {
|
|
161
|
+
return async (context) => {
|
|
162
|
+
const { state, pos, explicit } = context, node = syntaxTree(state).resolve(pos, -1), types = new Set(node.name.split('_')), isParserFunction = hasTag(types, 'parserFunctionName'),
|
|
163
|
+
/** 开头不包含` `,但可能包含`_` */ search = state.sliceDoc(node.from, pos).trimStart(), start = pos - search.length;
|
|
164
|
+
let { prevSibling } = node;
|
|
165
|
+
if (explicit || isParserFunction && search.includes('#') || wmf.test(location.hostname)) {
|
|
166
|
+
const validFor = /^[^|{}<>[\]#]*$/u;
|
|
167
|
+
if (isParserFunction || hasTag(types, 'templateName')) {
|
|
168
|
+
const options = search.includes(':') ? [] : [...this.functionSynonyms], suggestions = await this.#linkSuggest(search, 10) ?? { offset: 0, options: [] };
|
|
169
|
+
options.push(...suggestions.options);
|
|
170
|
+
return options.length === 0
|
|
171
|
+
? null
|
|
172
|
+
: {
|
|
173
|
+
from: start + suggestions.offset,
|
|
174
|
+
options,
|
|
175
|
+
validFor,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
else if (explicit && hasTag(types, 'templateBracket') && context.matchBefore(/\{\{$/u)) {
|
|
179
|
+
return {
|
|
180
|
+
from: pos,
|
|
181
|
+
options: this.functionSynonyms,
|
|
182
|
+
validFor,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const isPage = hasTag(types, 'pageName') && hasTag(types, 'parserFunction') || 0;
|
|
186
|
+
if (isPage && search.trim() || hasTag(types, 'linkPageName')) {
|
|
187
|
+
let prefix = '';
|
|
188
|
+
if (isPage) {
|
|
189
|
+
prefix = this.autocompleteNamespaces[[...types].find(t => t.startsWith('mw-function-'))
|
|
190
|
+
.slice(12)];
|
|
191
|
+
}
|
|
192
|
+
const suggestions = await this.#linkSuggest(prefix + search);
|
|
193
|
+
if (!suggestions) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
else if (!isPage) {
|
|
197
|
+
suggestions.options = suggestions.options.map((option) => ({ ...option, apply }));
|
|
198
|
+
}
|
|
199
|
+
else if (prefix === 'Module:') {
|
|
200
|
+
suggestions.options = suggestions.options
|
|
201
|
+
.filter(({ label }) => !label.endsWith('/doc'));
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
// eslint-disable-next-line unicorn/explicit-length-check
|
|
205
|
+
from: start + suggestions.offset - (isPage && prefix.length),
|
|
206
|
+
options: suggestions.options,
|
|
207
|
+
validFor,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const isArgument = hasTag(types, 'templateArgumentName'), prevIsDelimiter = prevSibling?.name.includes(tokens.templateDelimiter), isDelimiter = hasTag(types, 'templateDelimiter')
|
|
211
|
+
|| hasTag(types, 'templateBracket') && prevIsDelimiter;
|
|
212
|
+
if (this.tags.includes('templatedata')
|
|
213
|
+
&& (isDelimiter
|
|
214
|
+
|| isArgument && !search.includes('=')
|
|
215
|
+
|| hasTag(types, 'template') && prevIsDelimiter)) {
|
|
216
|
+
let stack = -1,
|
|
217
|
+
/** 可包含`_`、`:`等 */ page = '';
|
|
218
|
+
while (prevSibling) {
|
|
219
|
+
const { name, from, to } = prevSibling;
|
|
220
|
+
if (name.includes(tokens.templateBracket)) {
|
|
221
|
+
const [lbrace, rbrace] = braceStackUpdate(state, prevSibling);
|
|
222
|
+
stack += lbrace;
|
|
223
|
+
if (stack >= 0) {
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
stack += rbrace;
|
|
227
|
+
}
|
|
228
|
+
else if (stack === -1 && name.includes(tokens.templateName)) {
|
|
229
|
+
page = state.sliceDoc(from, to) + page;
|
|
230
|
+
}
|
|
231
|
+
else if (page && !name.includes(tokens.comment)) {
|
|
232
|
+
prevSibling = null;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
({ prevSibling } = prevSibling);
|
|
236
|
+
}
|
|
237
|
+
if (prevSibling && page) {
|
|
238
|
+
const equal = isArgument && state.sliceDoc(pos, node.to).trim() === '=' ? '' : '=', suggestions = await this.#paramSuggest(isDelimiter ? '' : search, page, equal);
|
|
239
|
+
if (suggestions && suggestions.options.length > 0) {
|
|
240
|
+
return {
|
|
241
|
+
from: isDelimiter ? pos : start + suggestions.offset,
|
|
242
|
+
options: suggestions.options,
|
|
243
|
+
validFor: /^[^|{}=]*$/u,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const isTagName = hasTag(types, ['htmlTagName', 'extTagName']), explicitMatch = explicit && context.matchBefore(/\s$/u), validForAttr = /^[a-z]*$/iu;
|
|
250
|
+
if (isTagName && explicitMatch
|
|
251
|
+
|| hasTag(types, ['htmlTagAttribute', 'extTagAttribute', 'tableDefinition'])) {
|
|
252
|
+
const tagName = isTagName ? search.trim() : /mw-(?:ext|html)-([a-z]+)/u.exec(node.name)[1], mt = explicitMatch || context.matchBefore(hasTag(types, 'tableDefinition') ? /[\s|-][a-z]+$/iu : /\s[a-z]+$/iu);
|
|
253
|
+
return mt && (mt.from < start || /^\s/u.test(mt.text))
|
|
254
|
+
? {
|
|
255
|
+
from: mt.from + 1,
|
|
256
|
+
options: [
|
|
257
|
+
...tagName === 'meta' || tagName === 'link'
|
|
258
|
+
|| tagName in this.config.tags && !this.elementAttrs.has(tagName)
|
|
259
|
+
? []
|
|
260
|
+
: this.htmlAttrs,
|
|
261
|
+
...this.elementAttrs.get(tagName) ?? [],
|
|
262
|
+
...this.extAttrs.get(tagName) ?? [],
|
|
263
|
+
],
|
|
264
|
+
validFor: validForAttr,
|
|
265
|
+
}
|
|
266
|
+
: null;
|
|
267
|
+
}
|
|
268
|
+
else if (explicit && hasTag(types, ['tableTd', 'tableTh', 'tableCaption'])) {
|
|
269
|
+
const [, tagName] = /mw-table-([a-z]+)/u.exec(node.name), mt = context.matchBefore(/[\s|!+][a-z]*$/iu);
|
|
270
|
+
if (mt && (mt.from < start || /^\s/u.test(mt.text))) {
|
|
271
|
+
return {
|
|
272
|
+
from: mt.from + 1,
|
|
273
|
+
options: [
|
|
274
|
+
...this.htmlAttrs,
|
|
275
|
+
...this.elementAttrs.get(tagName) ?? [],
|
|
276
|
+
],
|
|
277
|
+
validFor: validForAttr,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else if (hasTag(types, [
|
|
282
|
+
'comment',
|
|
283
|
+
'templateVariableName',
|
|
284
|
+
'templateName',
|
|
285
|
+
'linkPageName',
|
|
286
|
+
'linkToSection',
|
|
287
|
+
'extLink',
|
|
288
|
+
])) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
let mt = context.matchBefore(/__(?:(?!__)[\p{L}\p{N}_])*$/u);
|
|
292
|
+
if (mt) {
|
|
293
|
+
return {
|
|
294
|
+
from: mt.from,
|
|
295
|
+
options: this.doubleUnderscore,
|
|
296
|
+
validFor: /^[\p{L}\p{N}]*$/u,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
mt = context.matchBefore(/<\/?[a-z\d]*$/iu);
|
|
300
|
+
const extTags = [...types].filter(t => t.startsWith('mw-tag-'))
|
|
301
|
+
.map(s => s.slice(7));
|
|
302
|
+
if (mt && (explicit || mt.to - mt.from > 1)) {
|
|
303
|
+
const validFor = /^[a-z\d]*$/iu;
|
|
304
|
+
if (mt.text[1] === '/') {
|
|
305
|
+
const mt2 = context
|
|
306
|
+
.matchBefore(/<[a-z\d]+(?:\s[^<>]*)?>(?:(?!<\/?[a-z]).)*<\/[a-z\d]*$/iu), target = /^<([a-z\d]+)/iu.exec(mt2?.text ?? '')?.[1].toLowerCase(), extTag = extTags[extTags.length - 1], closed = /^\s*>/u.test(state.sliceDoc(pos)), options = [
|
|
307
|
+
...this.htmlTags.filter(({ label }) => !this.voidHtmlTags.has(label)),
|
|
308
|
+
...extTag ? [{ type: 'type', label: extTag, boost: 50 }] : [],
|
|
309
|
+
], i = this.permittedHtmlTags.has(target) && options.findIndex(({ label }) => label === target);
|
|
310
|
+
if (i !== false && i !== -1) {
|
|
311
|
+
options.splice(i, 1, { type: 'type', label: target, boost: 99 });
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
from: mt.from + 2,
|
|
315
|
+
options: closed
|
|
316
|
+
? options
|
|
317
|
+
: options.map((option) => ({ ...option, apply: `${option.label}>` })),
|
|
318
|
+
validFor,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
from: mt.from + 1,
|
|
323
|
+
options: [
|
|
324
|
+
...this.htmlTags,
|
|
325
|
+
...this.extTags.filter(({ label }) => !extTags.includes(label)),
|
|
326
|
+
],
|
|
327
|
+
validFor,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const isDelimiter = explicit && hasTag(types, 'fileDelimiter');
|
|
331
|
+
if (isDelimiter
|
|
332
|
+
|| hasTag(types, 'fileText')
|
|
333
|
+
&& prevSibling?.name.includes(tokens.fileDelimiter)
|
|
334
|
+
&& !search.includes('[')) {
|
|
335
|
+
const equal = state.sliceDoc(pos, pos + 1) === '=';
|
|
336
|
+
return {
|
|
337
|
+
from: isDelimiter ? pos : prevSibling.to,
|
|
338
|
+
options: equal
|
|
339
|
+
? this.imgKeys.map((option) => ({
|
|
340
|
+
...option,
|
|
341
|
+
apply: option.label.replace(/=$/u, ''),
|
|
342
|
+
}))
|
|
343
|
+
: this.imgKeys,
|
|
344
|
+
validFor: /^[^|{}<>[\]$]*$/u,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
else if (!hasTag(types, ['linkText', 'extLinkText'])) {
|
|
348
|
+
mt = context.matchBefore(/(?:^|[^[])\[[a-z:/]*$/iu);
|
|
349
|
+
if (mt && (explicit || !mt.text.endsWith('['))) {
|
|
350
|
+
return {
|
|
351
|
+
from: mt.from + (mt.text[1] === '[' ? 2 : 1),
|
|
352
|
+
options: this.protocols,
|
|
353
|
+
validFor: /^[a-z:/]*$/iu,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return null;
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Gets a LanguageSupport instance for the MediaWiki mode.
|
|
363
|
+
* @param config Configuration for the MediaWiki mode
|
|
364
|
+
*/
|
|
365
|
+
export const mediawiki = (config) => {
|
|
366
|
+
const mode = new FullMediaWiki(config), lang = StreamLanguage.define(mode.mediawiki()), highlighter = syntaxHighlighting(HighlightStyle.define(mode.getTagStyles()));
|
|
367
|
+
return new LanguageSupport(lang, highlighter);
|
|
368
|
+
};
|
|
369
|
+
/**
|
|
370
|
+
* Gets a LanguageSupport instance for the mixed MediaWiki-HTML mode.
|
|
371
|
+
* @param config Configuration for the MediaWiki mode
|
|
372
|
+
*/
|
|
373
|
+
export const html = (config) => mediawiki({
|
|
374
|
+
...config,
|
|
375
|
+
tags: {
|
|
376
|
+
...config.tags,
|
|
377
|
+
script: true,
|
|
378
|
+
style: true,
|
|
379
|
+
},
|
|
380
|
+
tagModes: {
|
|
381
|
+
...config.tagModes,
|
|
382
|
+
script: 'javascript',
|
|
383
|
+
style: 'css',
|
|
384
|
+
},
|
|
385
|
+
permittedHtmlTags: [
|
|
386
|
+
'html',
|
|
387
|
+
'base',
|
|
388
|
+
'title',
|
|
389
|
+
'menu',
|
|
390
|
+
'a',
|
|
391
|
+
'area',
|
|
392
|
+
'audio',
|
|
393
|
+
'map',
|
|
394
|
+
'track',
|
|
395
|
+
'video',
|
|
396
|
+
'embed',
|
|
397
|
+
'iframe',
|
|
398
|
+
'object',
|
|
399
|
+
'picture',
|
|
400
|
+
'source',
|
|
401
|
+
'canvas',
|
|
402
|
+
'col',
|
|
403
|
+
'colgroup',
|
|
404
|
+
'tbody',
|
|
405
|
+
'tfoot',
|
|
406
|
+
'thead',
|
|
407
|
+
'button',
|
|
408
|
+
'datalist',
|
|
409
|
+
'fieldset',
|
|
410
|
+
'form',
|
|
411
|
+
'input',
|
|
412
|
+
'label',
|
|
413
|
+
'legend',
|
|
414
|
+
'meter',
|
|
415
|
+
'optgroup',
|
|
416
|
+
'option',
|
|
417
|
+
'output',
|
|
418
|
+
'progress',
|
|
419
|
+
'select',
|
|
420
|
+
'textarea',
|
|
421
|
+
'details',
|
|
422
|
+
'dialog',
|
|
423
|
+
'slot',
|
|
424
|
+
'template',
|
|
425
|
+
'dir',
|
|
426
|
+
'frame',
|
|
427
|
+
'frameset',
|
|
428
|
+
'marquee',
|
|
429
|
+
'param',
|
|
430
|
+
'xmp',
|
|
431
|
+
],
|
|
432
|
+
implicitlyClosedHtmlTags: [
|
|
433
|
+
'area',
|
|
434
|
+
'base',
|
|
435
|
+
'col',
|
|
436
|
+
'embed',
|
|
437
|
+
'frame',
|
|
438
|
+
'input',
|
|
439
|
+
'param',
|
|
440
|
+
'source',
|
|
441
|
+
'track',
|
|
442
|
+
],
|
|
443
|
+
});
|