@bhsd/codemirror-mediawiki 2.28.2 → 2.29.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.
@@ -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
+ };
@@ -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
+ });