@bhsd/codemirror-mediawiki 2.1.5 → 2.1.7
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/main.min.js +1 -1
- package/dist/main.min.js.map +7 -0
- package/mw/dist/base.js +3 -3
- package/package.json +7 -3
- package/src/codemirror.ts +386 -0
- package/src/config.ts +215 -0
- package/src/mediawiki.ts +1301 -0
package/src/mediawiki.ts
ADDED
|
@@ -0,0 +1,1301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author pastakhov, MusikAnimal and others
|
|
3
|
+
* @license GPL-2.0-or-later
|
|
4
|
+
* @link https://gerrit.wikimedia.org/g/mediawiki/extensions/CodeMirror
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {HighlightStyle, LanguageSupport, StreamLanguage, syntaxHighlighting} from '@codemirror/language';
|
|
8
|
+
import {Tag} from '@lezer/highlight';
|
|
9
|
+
import {modeConfig} from './config';
|
|
10
|
+
import * as plugins from './plugins';
|
|
11
|
+
import type {StreamParser, StringStream, TagStyle} from '@codemirror/language';
|
|
12
|
+
import type {Highlighter} from '@lezer/highlight';
|
|
13
|
+
|
|
14
|
+
declare type MimeTypes = 'mediawiki' | 'text/mediawiki';
|
|
15
|
+
|
|
16
|
+
declare type Tokenizer = (stream: StringStream, state: State) => string;
|
|
17
|
+
|
|
18
|
+
declare interface State {
|
|
19
|
+
tokenize: Tokenizer;
|
|
20
|
+
readonly stack: Tokenizer[];
|
|
21
|
+
readonly inHtmlTag: string[];
|
|
22
|
+
extName: string | false;
|
|
23
|
+
extMode: StreamParser<object> | false;
|
|
24
|
+
extState: object | false;
|
|
25
|
+
nTemplate: number;
|
|
26
|
+
nLink: number;
|
|
27
|
+
nExt: number;
|
|
28
|
+
lpar: boolean;
|
|
29
|
+
lbrack: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
declare interface Token {
|
|
33
|
+
pos: number;
|
|
34
|
+
readonly style: string;
|
|
35
|
+
readonly state: object;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface MwConfig {
|
|
39
|
+
readonly urlProtocols: string;
|
|
40
|
+
readonly tags: Record<string, true>;
|
|
41
|
+
readonly tagModes: Record<string, string>;
|
|
42
|
+
functionSynonyms: [Record<string, string>, Record<string, unknown>];
|
|
43
|
+
doubleUnderscore: [Record<string, unknown>, Record<string, unknown>];
|
|
44
|
+
variants?: string[];
|
|
45
|
+
img?: Record<string, string>;
|
|
46
|
+
nsid: Record<string, number>;
|
|
47
|
+
permittedHtmlTags?: string[];
|
|
48
|
+
implicitlyClosedHtmlTags?: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const copyState = (state: State): State => {
|
|
52
|
+
const newState = {} as State;
|
|
53
|
+
for (const [key, val] of Object.entries(state)) {
|
|
54
|
+
Object.assign(newState, {[key]: Array.isArray(val) ? [...val] : val});
|
|
55
|
+
}
|
|
56
|
+
return newState;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const span = document.createElement('span'); // used for isHtmlEntity()
|
|
60
|
+
|
|
61
|
+
const isHtmlEntity = (str: string): boolean => {
|
|
62
|
+
span.innerHTML = str;
|
|
63
|
+
return [...span.textContent!].length === 1;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Adapted from the original CodeMirror 5 stream parser by Pavel Astakhov
|
|
68
|
+
*/
|
|
69
|
+
class MediaWiki {
|
|
70
|
+
declare readonly config;
|
|
71
|
+
declare readonly urlProtocols;
|
|
72
|
+
declare isBold;
|
|
73
|
+
declare wasBold;
|
|
74
|
+
declare isItalic;
|
|
75
|
+
declare wasItalic;
|
|
76
|
+
declare firstSingleLetterWord: number | null;
|
|
77
|
+
declare firstMultiLetterWord: number | null;
|
|
78
|
+
declare firstSpace: number | null;
|
|
79
|
+
declare oldStyle: string | null;
|
|
80
|
+
declare oldTokens: Token[];
|
|
81
|
+
declare readonly tokenTable;
|
|
82
|
+
declare readonly permittedHtmlTags;
|
|
83
|
+
declare readonly implicitlyClosedHtmlTags;
|
|
84
|
+
|
|
85
|
+
constructor(config: MwConfig) {
|
|
86
|
+
this.config = config;
|
|
87
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
88
|
+
this.urlProtocols = new RegExp(`^(?:${config.urlProtocols})`, 'i');
|
|
89
|
+
this.isBold = false;
|
|
90
|
+
this.wasBold = false;
|
|
91
|
+
this.isItalic = false;
|
|
92
|
+
this.wasItalic = false;
|
|
93
|
+
this.firstSingleLetterWord = null;
|
|
94
|
+
this.firstMultiLetterWord = null;
|
|
95
|
+
this.firstSpace = null;
|
|
96
|
+
this.oldStyle = null;
|
|
97
|
+
this.oldTokens = [];
|
|
98
|
+
this.tokenTable = {...modeConfig.tokenTable};
|
|
99
|
+
this.permittedHtmlTags = new Set([
|
|
100
|
+
...modeConfig.permittedHtmlTags,
|
|
101
|
+
...config.permittedHtmlTags || [],
|
|
102
|
+
]);
|
|
103
|
+
this.implicitlyClosedHtmlTags = new Set([
|
|
104
|
+
...modeConfig.implicitlyClosedHtmlTags,
|
|
105
|
+
...config.implicitlyClosedHtmlTags || [],
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
// Dynamically register any tags that aren't already in CodeMirrorModeMediaWikiConfig
|
|
109
|
+
for (const tag of Object.keys(config.tags)) {
|
|
110
|
+
this.addTag(tag);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create RegExp for file links
|
|
116
|
+
* @internal
|
|
117
|
+
*/
|
|
118
|
+
get fileRegex(): RegExp {
|
|
119
|
+
const nsFile = Object.entries(this.config.nsid).filter(([, id]) => id === 6).map(([ns]) => ns).join('|');
|
|
120
|
+
return new RegExp(`^\\s*(?:${nsFile})\\s*:\\s*`, 'iu');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Register a tag in CodeMirror. The generated CSS class will be of the form 'cm-mw-tag-tagname'
|
|
125
|
+
* This is for internal use to dynamically register tags from other MediaWiki extensions.
|
|
126
|
+
*
|
|
127
|
+
* @see https://www.mediawiki.org/wiki/Extension:CodeMirror#Extension_integration
|
|
128
|
+
* @param tag
|
|
129
|
+
* @param parent
|
|
130
|
+
* @internal
|
|
131
|
+
*/
|
|
132
|
+
addTag(tag: string, parent?: Tag): void {
|
|
133
|
+
(this.tokenTable[`mw-tag-${tag}`] as Tag | undefined) ||= Tag.define(parent);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* This defines the actual CSS class assigned to each tag/token.
|
|
138
|
+
*
|
|
139
|
+
* @see https://codemirror.net/docs/ref/#language.TagStyle
|
|
140
|
+
*/
|
|
141
|
+
getTagStyles(): TagStyle[] {
|
|
142
|
+
return Object.keys(this.tokenTable).map(className => ({
|
|
143
|
+
tag: this.tokenTable[className]!,
|
|
144
|
+
class: `cm-${className}${className === 'templateName' ? ' cm-mw-pagename' : ''}`,
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
eatHtmlEntity(stream: StringStream, style: string): string { // eslint-disable-line class-methods-use-this
|
|
149
|
+
const entity = stream.match(/^(?:#x[a-f\d]+|#\d+|[a-z\d]+);/iu) as RegExpMatchArray | false;
|
|
150
|
+
return entity && isHtmlEntity(`&${entity[0]}`) ? modeConfig.tags.htmlEntity : style;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
makeStyle(style: string, state: State, endGround?: 'nTemplate' | 'nLink' | 'nExt'): string {
|
|
154
|
+
return this.makeLocalStyle(
|
|
155
|
+
`${style} ${this.isBold ? modeConfig.tags.strong : ''} ${this.isItalic ? modeConfig.tags.em : ''}`,
|
|
156
|
+
state,
|
|
157
|
+
endGround,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// eslint-disable-next-line class-methods-use-this
|
|
162
|
+
makeLocalStyle(style: string, state: State, endGround?: 'nTemplate' | 'nLink' | 'nExt'): string {
|
|
163
|
+
let ground = '';
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* List out token names in a comment for search purposes.
|
|
167
|
+
*
|
|
168
|
+
* Tokens used here include:
|
|
169
|
+
* - mw-ext-ground
|
|
170
|
+
* - mw-ext-link-ground
|
|
171
|
+
* - mw-ext2-ground
|
|
172
|
+
* - mw-ext2-link-ground
|
|
173
|
+
* - mw-ext3-ground
|
|
174
|
+
* - mw-ext3-link-ground
|
|
175
|
+
* - mw-link-ground
|
|
176
|
+
* - mw-template-ext-ground
|
|
177
|
+
* - mw-template-ext-link-ground
|
|
178
|
+
* - mw-template-ext2-ground
|
|
179
|
+
* - mw-template-ext2-link-ground
|
|
180
|
+
* - mw-template-ext3-ground
|
|
181
|
+
* - mw-template-ext3-link-ground
|
|
182
|
+
* - mw-template-link-ground
|
|
183
|
+
* - mw-template2-ext-ground
|
|
184
|
+
* - mw-template2-ext-link-ground
|
|
185
|
+
* - mw-template2-ext2-ground
|
|
186
|
+
* - mw-template2-ext2-link-ground
|
|
187
|
+
* - mw-template2-ext3-ground
|
|
188
|
+
* - mw-template2-ext3-link-ground
|
|
189
|
+
* - mw-template2-ground
|
|
190
|
+
* - mw-template2-link-ground
|
|
191
|
+
* - mw-template3-ext-ground
|
|
192
|
+
* - mw-template3-ext-link-ground
|
|
193
|
+
* - mw-template3-ext2-ground
|
|
194
|
+
* - mw-template3-ext2-link-ground
|
|
195
|
+
* - mw-template3-ext3-ground
|
|
196
|
+
* - mw-template3-ext3-link-ground
|
|
197
|
+
* - mw-template3-ground
|
|
198
|
+
* - mw-template3-link-ground
|
|
199
|
+
*
|
|
200
|
+
* NOTE: these should be defined in modeConfig.tokenTable()
|
|
201
|
+
* and modeConfig.highlightStyle()
|
|
202
|
+
*/
|
|
203
|
+
switch (state.nTemplate) {
|
|
204
|
+
case 0:
|
|
205
|
+
break;
|
|
206
|
+
case 1:
|
|
207
|
+
ground += '-template';
|
|
208
|
+
break;
|
|
209
|
+
case 2:
|
|
210
|
+
ground += '-template2';
|
|
211
|
+
break;
|
|
212
|
+
default:
|
|
213
|
+
ground += '-template3';
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
switch (state.nExt) {
|
|
217
|
+
case 0:
|
|
218
|
+
break;
|
|
219
|
+
case 1:
|
|
220
|
+
ground += '-ext';
|
|
221
|
+
break;
|
|
222
|
+
case 2:
|
|
223
|
+
ground += '-ext2';
|
|
224
|
+
break;
|
|
225
|
+
default:
|
|
226
|
+
ground += '-ext3';
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
if (state.nLink > 0) {
|
|
230
|
+
ground += '-link';
|
|
231
|
+
}
|
|
232
|
+
if (endGround) {
|
|
233
|
+
state[endGround]--;
|
|
234
|
+
}
|
|
235
|
+
return (ground && `mw${ground}-ground `) + style;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
inBlock(style: string, terminator: string, consumeLast = true): Tokenizer {
|
|
239
|
+
return (stream, state) => {
|
|
240
|
+
if (stream.skipTo(terminator)) {
|
|
241
|
+
if (consumeLast) {
|
|
242
|
+
stream.match(terminator);
|
|
243
|
+
}
|
|
244
|
+
state.tokenize = state.stack.pop()!;
|
|
245
|
+
} else {
|
|
246
|
+
stream.skipToEnd();
|
|
247
|
+
}
|
|
248
|
+
return this.makeLocalStyle(style, state);
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
eatEnd(style: string): Tokenizer {
|
|
253
|
+
return (stream, state) => {
|
|
254
|
+
stream.skipToEnd();
|
|
255
|
+
state.tokenize = state.stack.pop()!;
|
|
256
|
+
return this.makeLocalStyle(style, state);
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
inChar(char: string, style: string): Tokenizer {
|
|
261
|
+
return (stream, state) => {
|
|
262
|
+
if (stream.eat(char)) {
|
|
263
|
+
state.tokenize = state.stack.pop()!;
|
|
264
|
+
return this.makeLocalStyle(style, state);
|
|
265
|
+
} else if (!stream.skipTo(char)) {
|
|
266
|
+
stream.skipToEnd();
|
|
267
|
+
}
|
|
268
|
+
return this.makeLocalStyle(modeConfig.tags.error, state);
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
inSectionHeader(count: number): Tokenizer {
|
|
273
|
+
return (stream, state) => {
|
|
274
|
+
if (stream.match(/^[^&<[{~']+/u)) {
|
|
275
|
+
if (stream.eol()) {
|
|
276
|
+
stream.backUp(count);
|
|
277
|
+
state.tokenize = this.eatEnd(modeConfig.tags.sectionHeader);
|
|
278
|
+
} else if (stream.match(/^<!--(?!.*?-->.*?=)/u, false)) {
|
|
279
|
+
// T171074: handle trailing comments
|
|
280
|
+
stream.backUp(count);
|
|
281
|
+
state.tokenize = this.inBlock(modeConfig.tags.sectionHeader, '<!--', false);
|
|
282
|
+
}
|
|
283
|
+
return this.makeLocalStyle(modeConfig.tags.section, state);
|
|
284
|
+
}
|
|
285
|
+
return this.eatWikiText(modeConfig.tags.section)(stream, state);
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
inVariable(stream: StringStream, state: State): string {
|
|
290
|
+
if (stream.match(/^[^{}|]+/u)) {
|
|
291
|
+
return this.makeLocalStyle(modeConfig.tags.templateVariableName, state);
|
|
292
|
+
} else if (stream.eat('|')) {
|
|
293
|
+
state.tokenize = this.inVariableDefault(true);
|
|
294
|
+
return this.makeLocalStyle(modeConfig.tags.templateVariableDelimiter, state);
|
|
295
|
+
} else if (stream.match('}}}')) {
|
|
296
|
+
state.tokenize = state.stack.pop()!;
|
|
297
|
+
return this.makeLocalStyle(modeConfig.tags.templateVariableBracket, state);
|
|
298
|
+
} else if (stream.match('{{{')) {
|
|
299
|
+
state.stack.push(state.tokenize);
|
|
300
|
+
return this.makeLocalStyle(modeConfig.tags.templateVariableBracket, state);
|
|
301
|
+
}
|
|
302
|
+
stream.next();
|
|
303
|
+
return this.makeLocalStyle(modeConfig.tags.templateVariableName, state);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
inVariableDefault(isFirst: boolean): Tokenizer {
|
|
307
|
+
const style = modeConfig.tags[isFirst ? 'templateVariable' : 'comment'];
|
|
308
|
+
return (stream, state) => {
|
|
309
|
+
if (stream.match(/^[^{}[<&~|]+/u)) {
|
|
310
|
+
return this.makeStyle(style, state);
|
|
311
|
+
} else if (stream.match('}}}')) {
|
|
312
|
+
state.tokenize = state.stack.pop()!;
|
|
313
|
+
return this.makeLocalStyle(modeConfig.tags.templateVariableBracket, state);
|
|
314
|
+
} else if (stream.eat('|')) {
|
|
315
|
+
state.tokenize = this.inVariableDefault(false);
|
|
316
|
+
return this.makeLocalStyle(modeConfig.tags.templateVariableDelimiter, state);
|
|
317
|
+
}
|
|
318
|
+
return this.eatWikiText(style)(stream, state);
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
inParserFunctionName(stream: StringStream, state: State): string {
|
|
323
|
+
// FIXME: {{#name}} and {{uc}} are wrong, must have ':'
|
|
324
|
+
if (stream.match(/^[^:}{~|<>[\]]+/u)) {
|
|
325
|
+
return this.makeLocalStyle(modeConfig.tags.parserFunctionName, state);
|
|
326
|
+
} else if (stream.eat(':')) {
|
|
327
|
+
state.tokenize = this.inParserFunctionArguments.bind(this);
|
|
328
|
+
return this.makeLocalStyle(modeConfig.tags.parserFunctionDelimiter, state);
|
|
329
|
+
} else if (stream.match('}}')) {
|
|
330
|
+
state.tokenize = state.stack.pop()!;
|
|
331
|
+
return this.makeLocalStyle(modeConfig.tags.parserFunctionBracket, state, 'nExt');
|
|
332
|
+
}
|
|
333
|
+
return this.eatWikiText(modeConfig.tags.error)(stream, state);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
inParserFunctionArguments(stream: StringStream, state: State): string {
|
|
337
|
+
if (stream.match(/^[^|}{[<&~]+/u)) {
|
|
338
|
+
return this.makeLocalStyle(modeConfig.tags.parserFunction, state);
|
|
339
|
+
} else if (stream.eat('|')) {
|
|
340
|
+
return this.makeLocalStyle(modeConfig.tags.parserFunctionDelimiter, state);
|
|
341
|
+
} else if (stream.match('}}')) {
|
|
342
|
+
state.tokenize = state.stack.pop()!;
|
|
343
|
+
return this.makeLocalStyle(modeConfig.tags.parserFunctionBracket, state, 'nExt');
|
|
344
|
+
}
|
|
345
|
+
return this.eatWikiText(modeConfig.tags.parserFunction)(stream, state);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
inTemplatePageName(haveAte: boolean): Tokenizer {
|
|
349
|
+
return (stream, state) => {
|
|
350
|
+
if (stream.match(/^\s*\|\s*/u)) {
|
|
351
|
+
state.tokenize = this.inTemplateArgument(true);
|
|
352
|
+
return this.makeLocalStyle(modeConfig.tags.templateDelimiter, state);
|
|
353
|
+
} else if (stream.match(/^\s*\}\}/u)) {
|
|
354
|
+
state.tokenize = state.stack.pop()!;
|
|
355
|
+
return this.makeLocalStyle(modeConfig.tags.templateBracket, state, 'nTemplate');
|
|
356
|
+
} else if (stream.match(/^\s*<!--.*?-->/u)) {
|
|
357
|
+
return this.makeLocalStyle(modeConfig.tags.comment, state);
|
|
358
|
+
} else if (haveAte && stream.sol()) {
|
|
359
|
+
// @todo error message
|
|
360
|
+
state.nTemplate--;
|
|
361
|
+
state.tokenize = state.stack.pop()!;
|
|
362
|
+
return '';
|
|
363
|
+
} else if (stream.match(/^\s*[^\s|&~{}<>[\]]+/u)) {
|
|
364
|
+
state.tokenize = this.inTemplatePageName(true);
|
|
365
|
+
return this.makeLocalStyle(modeConfig.tags.templateName, state);
|
|
366
|
+
} else if (stream.match(/^(?:[<>[\]}]|\{(?!\{))/u)) {
|
|
367
|
+
return this.makeLocalStyle(modeConfig.tags.error, state);
|
|
368
|
+
}
|
|
369
|
+
return stream.eatSpace()
|
|
370
|
+
? this.makeLocalStyle(modeConfig.tags.templateName, state)
|
|
371
|
+
: this.eatWikiText(modeConfig.tags.templateName)(stream, state);
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
inTemplateArgument(expectArgName: boolean): Tokenizer {
|
|
376
|
+
return (stream, state) => {
|
|
377
|
+
if (expectArgName && stream.eatWhile(/[^=|}{[<&~]/u)) {
|
|
378
|
+
if (stream.eat('=')) {
|
|
379
|
+
state.tokenize = this.inTemplateArgument(false);
|
|
380
|
+
return this.makeLocalStyle(modeConfig.tags.templateArgumentName, state);
|
|
381
|
+
}
|
|
382
|
+
return this.makeLocalStyle(modeConfig.tags.template, state);
|
|
383
|
+
} else if (stream.eatWhile(/[^|}{[<&~]/u)) {
|
|
384
|
+
return this.makeLocalStyle(modeConfig.tags.template, state);
|
|
385
|
+
} else if (stream.eat('|')) {
|
|
386
|
+
state.tokenize = this.inTemplateArgument(true);
|
|
387
|
+
return this.makeLocalStyle(modeConfig.tags.templateDelimiter, state);
|
|
388
|
+
} else if (stream.match('}}')) {
|
|
389
|
+
state.tokenize = state.stack.pop()!;
|
|
390
|
+
return this.makeLocalStyle(modeConfig.tags.templateBracket, state, 'nTemplate');
|
|
391
|
+
}
|
|
392
|
+
return this.eatWikiText(modeConfig.tags.template)(stream, state);
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
eatExternalLinkProtocol(chars: number): Tokenizer {
|
|
397
|
+
return (stream, state) => {
|
|
398
|
+
for (let i = 0; i < chars; i++) {
|
|
399
|
+
stream.next();
|
|
400
|
+
}
|
|
401
|
+
if (stream.eol()) {
|
|
402
|
+
state.nLink--;
|
|
403
|
+
// @todo error message
|
|
404
|
+
state.tokenize = state.stack.pop()!;
|
|
405
|
+
} else {
|
|
406
|
+
state.tokenize = this.inExternalLink.bind(this);
|
|
407
|
+
}
|
|
408
|
+
return this.makeLocalStyle(modeConfig.tags.extLinkProtocol, state);
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
inExternalLink(stream: StringStream, state: State): string {
|
|
413
|
+
if (stream.sol()) {
|
|
414
|
+
state.nLink--;
|
|
415
|
+
// @todo error message
|
|
416
|
+
state.tokenize = state.stack.pop()!;
|
|
417
|
+
return '';
|
|
418
|
+
} else if (stream.match(/^\s*\]/u)) {
|
|
419
|
+
state.tokenize = state.stack.pop()!;
|
|
420
|
+
return this.makeLocalStyle(modeConfig.tags.extLinkBracket, state, 'nLink');
|
|
421
|
+
} else if (stream.eatSpace()) {
|
|
422
|
+
state.tokenize = this.inExternalLinkText.bind(this);
|
|
423
|
+
return this.makeLocalStyle('', state);
|
|
424
|
+
} else if (stream.match(/^[^\s\]{&~']+/u)) {
|
|
425
|
+
if (stream.peek() === "'") {
|
|
426
|
+
if (stream.match("''", false)) {
|
|
427
|
+
state.tokenize = this.inExternalLinkText.bind(this);
|
|
428
|
+
} else {
|
|
429
|
+
stream.next();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return this.makeLocalStyle(modeConfig.tags.extLink, state);
|
|
433
|
+
}
|
|
434
|
+
return this.eatWikiText(modeConfig.tags.extLink)(stream, state);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
inExternalLinkText(stream: StringStream, state: State): string {
|
|
438
|
+
if (stream.sol()) {
|
|
439
|
+
state.nLink--;
|
|
440
|
+
// @todo error message
|
|
441
|
+
state.tokenize = state.stack.pop()!;
|
|
442
|
+
return '';
|
|
443
|
+
} else if (stream.eat(']')) {
|
|
444
|
+
state.tokenize = state.stack.pop()!;
|
|
445
|
+
return this.makeLocalStyle(modeConfig.tags.extLinkBracket, state, 'nLink');
|
|
446
|
+
}
|
|
447
|
+
return stream.match(/^[^'\]{&~<]+/u)
|
|
448
|
+
? this.makeStyle(modeConfig.tags.extLinkText, state)
|
|
449
|
+
: this.eatWikiText(modeConfig.tags.extLinkText)(stream, state);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
inLink(file: boolean): Tokenizer {
|
|
453
|
+
return (stream, state) => {
|
|
454
|
+
if (stream.sol()) {
|
|
455
|
+
state.nLink--;
|
|
456
|
+
// @todo error message
|
|
457
|
+
state.tokenize = state.stack.pop()!;
|
|
458
|
+
return '';
|
|
459
|
+
} else if (stream.match(/^\s*#\s*/u)) {
|
|
460
|
+
state.tokenize = this.inLinkToSection(file);
|
|
461
|
+
return this.makeStyle(modeConfig.tags.link, state);
|
|
462
|
+
} else if (stream.match(/^\s*\|\s*/u)) {
|
|
463
|
+
state.tokenize = this.inLinkText(file);
|
|
464
|
+
return this.makeLocalStyle(modeConfig.tags.linkDelimiter, state);
|
|
465
|
+
} else if (stream.match(/^\s*\]\]/u)) {
|
|
466
|
+
state.tokenize = state.stack.pop()!;
|
|
467
|
+
return this.makeLocalStyle(modeConfig.tags.linkBracket, state, 'nLink');
|
|
468
|
+
} else if (stream.match(/^(?:[<>[\]}]|\{(?!\{))/u)) {
|
|
469
|
+
return this.makeStyle(modeConfig.tags.error, state);
|
|
470
|
+
}
|
|
471
|
+
const style = `${modeConfig.tags.linkPageName} ${modeConfig.tags.pageName}`;
|
|
472
|
+
return stream.match(/^[^#|[\]&~{}<>]+/u)
|
|
473
|
+
? this.makeStyle(style, state)
|
|
474
|
+
: this.eatWikiText(style)(stream, state);
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
inLinkToSection(file: boolean): Tokenizer {
|
|
479
|
+
return (stream, state) => {
|
|
480
|
+
if (stream.sol()) {
|
|
481
|
+
// @todo error message
|
|
482
|
+
state.nLink--;
|
|
483
|
+
state.tokenize = state.stack.pop()!;
|
|
484
|
+
return '';
|
|
485
|
+
}
|
|
486
|
+
// FIXME '{{' breaks links, example: [[z{{page]]
|
|
487
|
+
if (stream.match(/^[^|\]&~{}]+/u)) {
|
|
488
|
+
return this.makeStyle(modeConfig.tags.linkToSection, state);
|
|
489
|
+
} else if (stream.eat('|')) {
|
|
490
|
+
state.tokenize = this.inLinkText(file);
|
|
491
|
+
return this.makeLocalStyle(modeConfig.tags.linkDelimiter, state);
|
|
492
|
+
} else if (stream.match(']]')) {
|
|
493
|
+
state.tokenize = state.stack.pop()!;
|
|
494
|
+
return this.makeLocalStyle(modeConfig.tags.linkBracket, state, 'nLink');
|
|
495
|
+
}
|
|
496
|
+
return this.eatWikiText(modeConfig.tags.linkToSection)(stream, state);
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
inLinkText(file: boolean): Tokenizer {
|
|
501
|
+
let linkIsBold: boolean,
|
|
502
|
+
linkIsItalic: boolean;
|
|
503
|
+
|
|
504
|
+
return (stream, state) => {
|
|
505
|
+
const tmpstyle = `${modeConfig.tags.linkText} ${linkIsBold ? modeConfig.tags.strong : ''} ${
|
|
506
|
+
linkIsItalic ? modeConfig.tags.em : ''
|
|
507
|
+
}`;
|
|
508
|
+
if (stream.match(']]')) {
|
|
509
|
+
if (state.lbrack && stream.peek() === ']') {
|
|
510
|
+
stream.backUp(1);
|
|
511
|
+
state.lbrack = false;
|
|
512
|
+
return this.makeStyle(tmpstyle, state);
|
|
513
|
+
}
|
|
514
|
+
state.tokenize = state.stack.pop()!;
|
|
515
|
+
return this.makeLocalStyle(modeConfig.tags.linkBracket, state, 'nLink');
|
|
516
|
+
} else if (file && stream.eat('|')) {
|
|
517
|
+
return this.makeLocalStyle(modeConfig.tags.linkDelimiter, state);
|
|
518
|
+
} else if (stream.match("'''")) {
|
|
519
|
+
linkIsBold = !linkIsBold;
|
|
520
|
+
return this.makeLocalStyle(`${modeConfig.tags.linkText} ${modeConfig.tags.apostrophes}`, state);
|
|
521
|
+
} else if (stream.match("''")) {
|
|
522
|
+
linkIsItalic = !linkIsItalic;
|
|
523
|
+
return this.makeLocalStyle(`${modeConfig.tags.linkText} ${modeConfig.tags.apostrophes}`, state);
|
|
524
|
+
}
|
|
525
|
+
const mt = stream
|
|
526
|
+
.match(file ? /^(?:[^'\]{&~<|[]|\[(?!\[))+/u : /^[^'\]{&~<]+/u) as RegExpMatchArray | false;
|
|
527
|
+
if (mt && mt[0].includes('[')) {
|
|
528
|
+
state.lbrack = true;
|
|
529
|
+
}
|
|
530
|
+
return mt ? this.makeStyle(tmpstyle, state) : this.eatWikiText(tmpstyle)(stream, state);
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
eatTagName(chars: number, isCloseTag: boolean, isHtmlTag: boolean): Tokenizer {
|
|
535
|
+
return (stream, state) => {
|
|
536
|
+
let name = '';
|
|
537
|
+
for (let i = 0; i < chars; i++) {
|
|
538
|
+
name += stream.next();
|
|
539
|
+
}
|
|
540
|
+
stream.eatSpace();
|
|
541
|
+
name = name.toLowerCase();
|
|
542
|
+
|
|
543
|
+
if (isHtmlTag) {
|
|
544
|
+
state.tokenize = isCloseTag
|
|
545
|
+
? this.inChar('>', modeConfig.tags.htmlTagBracket)
|
|
546
|
+
: this.inHtmlTagAttribute(name);
|
|
547
|
+
return this.makeLocalStyle(modeConfig.tags.htmlTagName, state);
|
|
548
|
+
}
|
|
549
|
+
// it is the extension tag
|
|
550
|
+
state.tokenize = isCloseTag
|
|
551
|
+
? this.inChar('>', modeConfig.tags.extTagBracket)
|
|
552
|
+
: this.inExtTagAttribute(name);
|
|
553
|
+
return this.makeLocalStyle(modeConfig.tags.extTagName, state);
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
inHtmlTagAttribute(name: string): Tokenizer {
|
|
558
|
+
return (stream, state) => {
|
|
559
|
+
if (stream.match(/^[^>/<{]+/u)) {
|
|
560
|
+
return this.makeLocalStyle(modeConfig.tags.htmlTagAttribute, state);
|
|
561
|
+
} else if (stream.match(/^\/?>/u)) {
|
|
562
|
+
if (!this.implicitlyClosedHtmlTags.has(name)) {
|
|
563
|
+
state.inHtmlTag.push(name);
|
|
564
|
+
}
|
|
565
|
+
state.tokenize = state.stack.pop()!;
|
|
566
|
+
return this.makeLocalStyle(modeConfig.tags.htmlTagBracket, state);
|
|
567
|
+
}
|
|
568
|
+
return this.eatWikiText(modeConfig.tags.htmlTagAttribute)(stream, state);
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
eatNowiki(): Tokenizer {
|
|
573
|
+
return stream => {
|
|
574
|
+
if (stream.match(/^[^&]+/u)) {
|
|
575
|
+
return '';
|
|
576
|
+
}
|
|
577
|
+
// eat &
|
|
578
|
+
stream.next();
|
|
579
|
+
return this.eatHtmlEntity(stream, '');
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
inExtTagAttribute(name: string): Tokenizer {
|
|
584
|
+
return (stream, state) => {
|
|
585
|
+
if (stream.match(/^[^>/]+/u)) {
|
|
586
|
+
return this.makeLocalStyle(modeConfig.tags.extTagAttribute, state);
|
|
587
|
+
} else if (stream.eat('>')) {
|
|
588
|
+
state.extName = name;
|
|
589
|
+
// leverage the tagModes system for <nowiki> and <pre>
|
|
590
|
+
if (name === 'nowiki' || name === 'pre') {
|
|
591
|
+
// There's no actual processing within these tags (apart from HTML entities),
|
|
592
|
+
// so startState and copyState can be no-ops.
|
|
593
|
+
state.extMode = {
|
|
594
|
+
startState: () => ({}),
|
|
595
|
+
token: this.eatNowiki(),
|
|
596
|
+
};
|
|
597
|
+
state.extState = {};
|
|
598
|
+
} else if (name in this.config.tagModes) {
|
|
599
|
+
state.extMode = this[this.config.tagModes[name] as MimeTypes];
|
|
600
|
+
state.extState = state.extMode.startState!(0);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
state.tokenize = this.eatExtTagArea(name);
|
|
604
|
+
return this.makeLocalStyle(modeConfig.tags.extTagBracket, state);
|
|
605
|
+
} else if (stream.match('/>')) {
|
|
606
|
+
state.tokenize = state.stack.pop()!;
|
|
607
|
+
return this.makeLocalStyle(modeConfig.tags.extTagBracket, state);
|
|
608
|
+
}
|
|
609
|
+
return this.eatWikiText(modeConfig.tags.extTagAttribute)(stream, state);
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
eatExtTagArea(name: string): Tokenizer {
|
|
614
|
+
return (stream, state) => {
|
|
615
|
+
const from = stream.pos,
|
|
616
|
+
pattern = new RegExp(`</${name}\\s*(?:>|$)`, 'iu'),
|
|
617
|
+
m = pattern.exec(from ? stream.string.slice(from) : stream.string);
|
|
618
|
+
let origString: string | false = false;
|
|
619
|
+
|
|
620
|
+
if (m) {
|
|
621
|
+
if (m.index === 0) {
|
|
622
|
+
state.tokenize = this.eatExtCloseTag(name);
|
|
623
|
+
state.extName = false;
|
|
624
|
+
if (state.extMode) {
|
|
625
|
+
state.extMode = false;
|
|
626
|
+
state.extState = false;
|
|
627
|
+
}
|
|
628
|
+
return state.tokenize(stream, state);
|
|
629
|
+
}
|
|
630
|
+
const to = m.index + from;
|
|
631
|
+
origString = stream.string;
|
|
632
|
+
stream.string = origString.slice(0, to);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
state.stack.push(state.tokenize);
|
|
636
|
+
state.tokenize = this.inExtTokens(origString);
|
|
637
|
+
return state.tokenize(stream, state);
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
eatExtCloseTag(name: string): Tokenizer {
|
|
642
|
+
return (stream, state) => {
|
|
643
|
+
stream.next(); // eat <
|
|
644
|
+
stream.next(); // eat /
|
|
645
|
+
state.tokenize = this.eatTagName(name.length, true, false);
|
|
646
|
+
return this.makeLocalStyle(modeConfig.tags.extTagBracket, state);
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
inExtTokens(origString: string | false): Tokenizer { // eslint-disable-line class-methods-use-this
|
|
651
|
+
return (stream, state) => {
|
|
652
|
+
let ret: string;
|
|
653
|
+
if (state.extMode === false) {
|
|
654
|
+
ret = modeConfig.tags.extTag;
|
|
655
|
+
stream.skipToEnd();
|
|
656
|
+
} else {
|
|
657
|
+
ret = `mw-tag-${state.extName} ${state.extMode.token(stream, state.extState as State)}`;
|
|
658
|
+
}
|
|
659
|
+
if (stream.eol()) {
|
|
660
|
+
if (origString !== false) {
|
|
661
|
+
stream.string = origString;
|
|
662
|
+
}
|
|
663
|
+
state.tokenize = state.stack.pop()!;
|
|
664
|
+
}
|
|
665
|
+
return ret;
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
eatStartTable(stream: StringStream, state: State): string {
|
|
670
|
+
stream.match(/^(?:\{\||\{{3}\s*!\s*\}\})\s*/u);
|
|
671
|
+
state.tokenize = this.inTableDefinition.bind(this);
|
|
672
|
+
return this.makeLocalStyle(modeConfig.tags.tableBracket, state);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
inTableDefinition(stream: StringStream, state: State): string {
|
|
676
|
+
if (stream.sol()) {
|
|
677
|
+
state.tokenize = this.inTable.bind(this);
|
|
678
|
+
return this.inTable(stream, state);
|
|
679
|
+
}
|
|
680
|
+
return this.eatWikiText(modeConfig.tags.tableDefinition)(stream, state);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
inTableCaption(stream: StringStream, state: State): string {
|
|
684
|
+
if (stream.sol() && stream.match(/^\s*(?:[|!]|\{\{\s*!\s*\}\})/u, false)) {
|
|
685
|
+
state.tokenize = this.inTable.bind(this);
|
|
686
|
+
return this.inTable(stream, state);
|
|
687
|
+
} else if (stream.match(/^\s*(?:\||\{\{\s*!\s*\}\}){2}/u, false)) {
|
|
688
|
+
state.tokenize = this.inTableRow(false, false);
|
|
689
|
+
return '';
|
|
690
|
+
}
|
|
691
|
+
return this.eatWikiText(modeConfig.tags.tableCaption)(stream, state);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
inTable(stream: StringStream, state: State): string {
|
|
695
|
+
if (stream.sol()) {
|
|
696
|
+
stream.eatSpace();
|
|
697
|
+
if (stream.match(/^(?:\||\{\{\s*!\s*\}\})/u)) {
|
|
698
|
+
if (stream.match(/^-+\s*/u)) {
|
|
699
|
+
state.tokenize = this.inTableDefinition.bind(this);
|
|
700
|
+
return this.makeLocalStyle(modeConfig.tags.tableDelimiter, state);
|
|
701
|
+
} else if (stream.eat('+')) {
|
|
702
|
+
stream.eatSpace();
|
|
703
|
+
state.tokenize = this.inTableCaption.bind(this);
|
|
704
|
+
return this.makeLocalStyle(modeConfig.tags.tableDelimiter, state);
|
|
705
|
+
} else if (stream.eat('}')) {
|
|
706
|
+
state.tokenize = state.stack.pop()!;
|
|
707
|
+
return this.makeLocalStyle(modeConfig.tags.tableBracket, state);
|
|
708
|
+
}
|
|
709
|
+
stream.eatSpace();
|
|
710
|
+
state.tokenize = this.inTableRow(true, false);
|
|
711
|
+
return this.makeLocalStyle(modeConfig.tags.tableDelimiter, state);
|
|
712
|
+
} else if (stream.eat('!')) {
|
|
713
|
+
stream.eatSpace();
|
|
714
|
+
state.tokenize = this.inTableRow(true, true);
|
|
715
|
+
return this.makeLocalStyle(modeConfig.tags.tableDelimiter, state);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return this.eatWikiText('')(stream, state);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
inTableRow(isStart: boolean, isHead: boolean): Tokenizer {
|
|
722
|
+
return (stream, state) => {
|
|
723
|
+
if (stream.sol()) {
|
|
724
|
+
if (stream.match(/^\s*(?:[|!]|\{\{\s*!\s*\}\})/u, false)) {
|
|
725
|
+
state.tokenize = this.inTable.bind(this);
|
|
726
|
+
return this.inTable(stream, state);
|
|
727
|
+
}
|
|
728
|
+
} else if (stream.match(/^[^'|{[<&~!]+/u)) {
|
|
729
|
+
return this.makeStyle(isHead ? modeConfig.tags.strong : '', state);
|
|
730
|
+
} else if (
|
|
731
|
+
stream.match(/^(?:\||\{\{\s*!\s*\}\}){2}/u) || isHead && stream.match('!!')
|
|
732
|
+
|| isStart && stream.match(/^(?:\||\{\{\s*!\s*\}\})/u)
|
|
733
|
+
) {
|
|
734
|
+
this.isBold = false;
|
|
735
|
+
this.isItalic = false;
|
|
736
|
+
if (isStart) {
|
|
737
|
+
state.tokenize = this.inTableRow(false, isHead);
|
|
738
|
+
}
|
|
739
|
+
return this.makeLocalStyle(modeConfig.tags.tableDelimiter, state);
|
|
740
|
+
}
|
|
741
|
+
return this.eatWikiText(isHead ? modeConfig.tags.strong : '')(stream, state);
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
eatFreeExternalLinkProtocol(stream: StringStream, state: State): string {
|
|
746
|
+
stream.match(this.urlProtocols);
|
|
747
|
+
state.tokenize = this.inFreeExternalLink.bind(this);
|
|
748
|
+
return this.makeStyle(modeConfig.tags.freeExtLinkProtocol, state);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
inFreeExternalLink(stream: StringStream, state: State): string {
|
|
752
|
+
if (stream.eol()) {
|
|
753
|
+
// @todo error message
|
|
754
|
+
} else {
|
|
755
|
+
const mt = stream.match(/^[^\s{[\]<>~).,;:!?'"]*/u) as RegExpMatchArray;
|
|
756
|
+
state.lpar ||= mt[0].includes('(');
|
|
757
|
+
if (stream.peek() === '~') {
|
|
758
|
+
if (stream.match(/^~{1,2}(?!~)/u)) {
|
|
759
|
+
return this.makeStyle(modeConfig.tags.freeExtLink, state);
|
|
760
|
+
}
|
|
761
|
+
} else if (stream.peek() === '{') {
|
|
762
|
+
if (stream.match(/^\{(?!\{)/u)) {
|
|
763
|
+
return this.makeStyle(modeConfig.tags.freeExtLink, state);
|
|
764
|
+
}
|
|
765
|
+
} else if (stream.peek() === "'") {
|
|
766
|
+
if (stream.match(/^'(?!')/u)) {
|
|
767
|
+
return this.makeStyle(modeConfig.tags.freeExtLink, state);
|
|
768
|
+
}
|
|
769
|
+
} else if (state.lpar && stream.peek() === ')') {
|
|
770
|
+
stream.next();
|
|
771
|
+
return this.makeStyle(modeConfig.tags.freeExtLink, state);
|
|
772
|
+
} else if (stream.match(/^[).,;:!?]+(?=[^\s{[\]<>~).,;:!?'"]|~~?(?!~)|\{(?!\{)|'(?!'))/u)) {
|
|
773
|
+
return this.makeStyle(modeConfig.tags.freeExtLink, state);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
state.lpar = false;
|
|
777
|
+
state.tokenize = state.stack.pop()!;
|
|
778
|
+
return this.makeStyle(modeConfig.tags.freeExtLink, state);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
eatWikiText(style: string): Tokenizer {
|
|
782
|
+
return (stream, state) => {
|
|
783
|
+
let ch: string | void; // eslint-disable-line @typescript-eslint/no-invalid-void-type
|
|
784
|
+
const sol = stream.sol();
|
|
785
|
+
|
|
786
|
+
if (sol) {
|
|
787
|
+
if (stream.match('//')) {
|
|
788
|
+
return this.makeStyle(style, state);
|
|
789
|
+
// highlight free external links, see T108448
|
|
790
|
+
} else if (stream.match(this.urlProtocols)) {
|
|
791
|
+
state.stack.push(state.tokenize);
|
|
792
|
+
state.tokenize = this.inFreeExternalLink.bind(this);
|
|
793
|
+
return this.makeStyle(modeConfig.tags.freeExtLinkProtocol, state);
|
|
794
|
+
}
|
|
795
|
+
ch = stream.next();
|
|
796
|
+
switch (ch) {
|
|
797
|
+
case '-':
|
|
798
|
+
if (stream.match(/^-{3,}/u)) {
|
|
799
|
+
return modeConfig.tags.hr;
|
|
800
|
+
}
|
|
801
|
+
break;
|
|
802
|
+
case '=': {
|
|
803
|
+
const tmp = stream
|
|
804
|
+
.match(/^(={0,5})(.+?(=\1\s*)(?:<!--(?!.*-->.*\S).*)?)$/u) as RegExpMatchArray | false;
|
|
805
|
+
// Title
|
|
806
|
+
if (tmp) {
|
|
807
|
+
stream.backUp(tmp[2]!.length);
|
|
808
|
+
state.stack.push(state.tokenize);
|
|
809
|
+
state.tokenize = this.inSectionHeader(tmp[3]!.length);
|
|
810
|
+
return this.makeLocalStyle(`${modeConfig.tags.sectionHeader} ${
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Tokens used here include:
|
|
814
|
+
* - cm-mw-section-1
|
|
815
|
+
* - cm-mw-section-2
|
|
816
|
+
* - cm-mw-section-3
|
|
817
|
+
* - cm-mw-section-4
|
|
818
|
+
* - cm-mw-section-5
|
|
819
|
+
* - cm-mw-section-6
|
|
820
|
+
*/
|
|
821
|
+
(modeConfig.tags as Record<string, string>)[`sectionHeader${tmp[1]!.length + 1}`]
|
|
822
|
+
}`, state);
|
|
823
|
+
}
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
case '*':
|
|
827
|
+
case '#':
|
|
828
|
+
case ';':
|
|
829
|
+
// Just consume all nested list and indention syntax when there is more
|
|
830
|
+
stream.match(/^[*#;:]*/u);
|
|
831
|
+
return this.makeLocalStyle(modeConfig.tags.list, state);
|
|
832
|
+
case ':':
|
|
833
|
+
// Highlight indented tables :{|, bug T108454
|
|
834
|
+
if (stream.match(/^:*(?:\{\||\{{3}\s*!\s*\}\})/u, false)) {
|
|
835
|
+
state.stack.push(state.tokenize);
|
|
836
|
+
state.tokenize = this.eatStartTable.bind(this);
|
|
837
|
+
}
|
|
838
|
+
// Just consume all nested list and indention syntax when there is more
|
|
839
|
+
stream.match(/^[*#;:]*/u);
|
|
840
|
+
return this.makeLocalStyle(modeConfig.tags.list, state);
|
|
841
|
+
case ' ': {
|
|
842
|
+
// Leading spaces is valid syntax for tables, bug T108454
|
|
843
|
+
const mt = stream.match(/^\s*(:+\s*)?(?=\{\||\{{3}\s*!\s*\}\})/u) as RegExpMatchArray | false;
|
|
844
|
+
if (mt) {
|
|
845
|
+
if (mt[1]) { // ::{|
|
|
846
|
+
state.stack.push(state.tokenize);
|
|
847
|
+
state.tokenize = this.eatStartTable.bind(this);
|
|
848
|
+
return this.makeLocalStyle(modeConfig.tags.list, state);
|
|
849
|
+
}
|
|
850
|
+
stream.eat('{');
|
|
851
|
+
} else {
|
|
852
|
+
return modeConfig.tags.skipFormatting;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
// falls through
|
|
856
|
+
case '{':
|
|
857
|
+
if (stream.match(/^(?:\||\{\{\s*!\s*\}\})\s*/u)) {
|
|
858
|
+
state.stack.push(state.tokenize);
|
|
859
|
+
state.tokenize = this.inTableDefinition.bind(this);
|
|
860
|
+
return this.makeLocalStyle(modeConfig.tags.tableBracket, state);
|
|
861
|
+
}
|
|
862
|
+
break;
|
|
863
|
+
default:
|
|
864
|
+
// pass
|
|
865
|
+
}
|
|
866
|
+
} else {
|
|
867
|
+
ch = stream.next();
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
switch (ch) {
|
|
871
|
+
case '&':
|
|
872
|
+
return this.makeStyle(this.eatHtmlEntity(stream, style), state);
|
|
873
|
+
case "'":
|
|
874
|
+
// skip the irrelevant apostrophes ( >5 or =4 )
|
|
875
|
+
if (stream.match(/^'*(?='{5})/u) || stream.match(/^'''(?!')/u, false)) {
|
|
876
|
+
break;
|
|
877
|
+
} else if (stream.match("''")) { // bold
|
|
878
|
+
if (!(this.firstSingleLetterWord || stream.match("''", false))) {
|
|
879
|
+
this.prepareItalicForCorrection(stream);
|
|
880
|
+
}
|
|
881
|
+
this.isBold = !this.isBold;
|
|
882
|
+
return this.makeLocalStyle(modeConfig.tags.apostrophesBold, state);
|
|
883
|
+
} else if (stream.eat("'")) { // italic
|
|
884
|
+
this.isItalic = !this.isItalic;
|
|
885
|
+
return this.makeLocalStyle(modeConfig.tags.apostrophesItalic, state);
|
|
886
|
+
}
|
|
887
|
+
break;
|
|
888
|
+
case '[':
|
|
889
|
+
if (stream.eat('[')) { // Link Example: [[ Foo | Bar ]]
|
|
890
|
+
stream.eatSpace();
|
|
891
|
+
if (/[^\]|[]/u.test(stream.peek() || '')) {
|
|
892
|
+
state.nLink++;
|
|
893
|
+
state.stack.push(state.tokenize);
|
|
894
|
+
state.lbrack = false;
|
|
895
|
+
state.tokenize = this.inLink(Boolean(stream.match(this.fileRegex, false)));
|
|
896
|
+
return this.makeLocalStyle(modeConfig.tags.linkBracket, state);
|
|
897
|
+
}
|
|
898
|
+
} else {
|
|
899
|
+
const mt = stream.match(this.urlProtocols, false) as RegExpMatchArray | false;
|
|
900
|
+
if (mt) {
|
|
901
|
+
state.nLink++;
|
|
902
|
+
state.stack.push(state.tokenize);
|
|
903
|
+
state.tokenize = this.eatExternalLinkProtocol(mt[0].length);
|
|
904
|
+
return this.makeLocalStyle(modeConfig.tags.extLinkBracket, state);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
break;
|
|
908
|
+
case '{':
|
|
909
|
+
// Can't be a variable when it starts with more than 3 brackets (T108450) or
|
|
910
|
+
// a single { followed by a template. E.g. {{{!}} starts a table (T292967).
|
|
911
|
+
if (stream.match(/^\{\{(?!\{|[^{}]*\}\}(?!\}))/u)) {
|
|
912
|
+
stream.eatSpace();
|
|
913
|
+
state.stack.push(state.tokenize);
|
|
914
|
+
state.tokenize = this.inVariable.bind(this);
|
|
915
|
+
return this.makeLocalStyle(modeConfig.tags.templateVariableBracket, state);
|
|
916
|
+
} else if (stream.match(/^\{(?!\{(?!\{))\s*/u)) {
|
|
917
|
+
// Parser function
|
|
918
|
+
if (stream.peek() === '#') {
|
|
919
|
+
state.nExt++;
|
|
920
|
+
state.stack.push(state.tokenize);
|
|
921
|
+
state.tokenize = this.inParserFunctionName.bind(this);
|
|
922
|
+
return this.makeLocalStyle(modeConfig.tags.parserFunctionBracket, state);
|
|
923
|
+
}
|
|
924
|
+
// Check for parser function without '#'
|
|
925
|
+
const name = stream
|
|
926
|
+
.match(/^([^\s}[\]<{'|&:]+)(:|\s*)(\}\}?)?(.)?/u, false) as RegExpMatchArray | false;
|
|
927
|
+
if (name && (name[2] === ':' || name[4] === undefined || name[3] === '}}')
|
|
928
|
+
&& (
|
|
929
|
+
name[1]!.toLowerCase() in this.config.functionSynonyms[0]
|
|
930
|
+
|| name[1]! in this.config.functionSynonyms[1]
|
|
931
|
+
)
|
|
932
|
+
) {
|
|
933
|
+
state.nExt++;
|
|
934
|
+
state.stack.push(state.tokenize);
|
|
935
|
+
state.tokenize = this.inParserFunctionName.bind(this);
|
|
936
|
+
return this.makeLocalStyle(modeConfig.tags.parserFunctionBracket, state);
|
|
937
|
+
}
|
|
938
|
+
// Template
|
|
939
|
+
state.nTemplate++;
|
|
940
|
+
state.stack.push(state.tokenize);
|
|
941
|
+
state.tokenize = this.inTemplatePageName(false);
|
|
942
|
+
return this.makeLocalStyle(modeConfig.tags.templateBracket, state);
|
|
943
|
+
}
|
|
944
|
+
break;
|
|
945
|
+
case '<': {
|
|
946
|
+
if (stream.match('!--')) { // comment
|
|
947
|
+
state.stack.push(state.tokenize);
|
|
948
|
+
state.tokenize = this.inBlock(modeConfig.tags.comment, '-->');
|
|
949
|
+
return this.makeLocalStyle(modeConfig.tags.comment, state);
|
|
950
|
+
}
|
|
951
|
+
const isCloseTag = Boolean(stream.eat('/')),
|
|
952
|
+
mt = stream.match(/^[^>/\s.*,[\]{}$^+?|\\'`~<=!@#%&()-]+/u) as RegExpMatchArray | false;
|
|
953
|
+
if (mt) {
|
|
954
|
+
const tagname = mt[0]!.toLowerCase();
|
|
955
|
+
if (tagname in this.config.tags) {
|
|
956
|
+
// Parser function
|
|
957
|
+
if (isCloseTag) {
|
|
958
|
+
state.tokenize = this.inChar('>', modeConfig.tags.error);
|
|
959
|
+
return this.makeLocalStyle(modeConfig.tags.error, state);
|
|
960
|
+
}
|
|
961
|
+
stream.backUp(tagname.length);
|
|
962
|
+
state.stack.push(state.tokenize);
|
|
963
|
+
state.tokenize = this.eatTagName(tagname.length, isCloseTag, false);
|
|
964
|
+
return this.makeLocalStyle(modeConfig.tags.extTagBracket, state);
|
|
965
|
+
} else if (this.permittedHtmlTags.has(tagname)) {
|
|
966
|
+
// Html tag
|
|
967
|
+
if (isCloseTag && tagname !== state.inHtmlTag.pop()) {
|
|
968
|
+
state.tokenize = this.inChar('>', modeConfig.tags.error);
|
|
969
|
+
return this.makeLocalStyle(modeConfig.tags.error, state);
|
|
970
|
+
} else if (isCloseTag && this.implicitlyClosedHtmlTags.has(tagname)) {
|
|
971
|
+
return this.makeLocalStyle(modeConfig.tags.error, state);
|
|
972
|
+
}
|
|
973
|
+
stream.backUp(tagname.length);
|
|
974
|
+
state.stack.push(state.tokenize);
|
|
975
|
+
state.tokenize = this.eatTagName(tagname.length, isCloseTag, true);
|
|
976
|
+
return this.makeLocalStyle(modeConfig.tags.htmlTagBracket, state);
|
|
977
|
+
}
|
|
978
|
+
stream.backUp(tagname.length);
|
|
979
|
+
}
|
|
980
|
+
break;
|
|
981
|
+
}
|
|
982
|
+
case '~':
|
|
983
|
+
if (stream.match(/^~{2,4}/u)) {
|
|
984
|
+
return modeConfig.tags.signature;
|
|
985
|
+
}
|
|
986
|
+
break;
|
|
987
|
+
// Maybe double underscored Magic Word such as __TOC__
|
|
988
|
+
case '_': {
|
|
989
|
+
let tmp = 1;
|
|
990
|
+
// Optimize processing of many underscore symbols
|
|
991
|
+
while (stream.eat('_')) {
|
|
992
|
+
tmp++;
|
|
993
|
+
}
|
|
994
|
+
// Many underscore symbols
|
|
995
|
+
if (tmp > 2) {
|
|
996
|
+
if (!stream.eol()) {
|
|
997
|
+
// Leave last two underscore symbols for processing in next iteration
|
|
998
|
+
stream.backUp(2);
|
|
999
|
+
}
|
|
1000
|
+
// Optimization: skip regex function for EOL and backup-ed symbols
|
|
1001
|
+
return this.makeStyle(style, state);
|
|
1002
|
+
// Check on double underscore Magic Word
|
|
1003
|
+
} else if (tmp === 2) {
|
|
1004
|
+
// The same as the end of function except '_' inside and '__' at the end.
|
|
1005
|
+
const name = stream.match(/^\w+?__/u) as RegExpMatchArray | false;
|
|
1006
|
+
if (name) {
|
|
1007
|
+
if (
|
|
1008
|
+
`__${name[0].toLowerCase()}` in this.config.doubleUnderscore[0]
|
|
1009
|
+
|| `__${name[0]}` in this.config.doubleUnderscore[1]
|
|
1010
|
+
) {
|
|
1011
|
+
return modeConfig.tags.doubleUnderscore;
|
|
1012
|
+
} else if (!stream.eol()) {
|
|
1013
|
+
// Two underscore symbols at the end can be the
|
|
1014
|
+
// beginning of another double underscored Magic Word
|
|
1015
|
+
stream.backUp(2);
|
|
1016
|
+
}
|
|
1017
|
+
// Optimization: skip regex for EOL and backup-ed symbols
|
|
1018
|
+
return this.makeStyle(style, state);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1023
|
+
default:
|
|
1024
|
+
if (/\s/u.test(ch || '')) {
|
|
1025
|
+
stream.eatSpace();
|
|
1026
|
+
// highlight free external links, bug T108448
|
|
1027
|
+
if (stream.match(this.urlProtocols, false) && !stream.match('//')) {
|
|
1028
|
+
state.stack.push(state.tokenize);
|
|
1029
|
+
state.tokenize = this.eatFreeExternalLinkProtocol.bind(this);
|
|
1030
|
+
return this.makeStyle(style, state);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
break;
|
|
1034
|
+
}
|
|
1035
|
+
stream.match(/^[^\s_>}[\]<{'|&:~=]+/u);
|
|
1036
|
+
return this.makeStyle(style, state);
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Remembers position and status for rollbacking.
|
|
1042
|
+
* It is needed for changing from bold to italic with apostrophes before it, if required.
|
|
1043
|
+
*
|
|
1044
|
+
* @see https://phabricator.wikimedia.org/T108455
|
|
1045
|
+
*/
|
|
1046
|
+
prepareItalicForCorrection(stream: StringStream): void {
|
|
1047
|
+
// See Parser::doQuotes() in MediaWiki Core, it works similarly.
|
|
1048
|
+
// this.firstSingleLetterWord has maximum priority
|
|
1049
|
+
// this.firstMultiLetterWord has medium priority
|
|
1050
|
+
// this.firstSpace has low priority
|
|
1051
|
+
const end = stream.pos,
|
|
1052
|
+
str = stream.string.slice(0, end - 3),
|
|
1053
|
+
x1 = str.slice(-1),
|
|
1054
|
+
x2 = str.slice(-2, -1);
|
|
1055
|
+
|
|
1056
|
+
// this.firstSingleLetterWord always is undefined here
|
|
1057
|
+
if (x1 === ' ') {
|
|
1058
|
+
if (this.firstMultiLetterWord || this.firstSpace) {
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
this.firstSpace = end;
|
|
1062
|
+
} else if (x2 === ' ') {
|
|
1063
|
+
this.firstSingleLetterWord = end;
|
|
1064
|
+
} else if (this.firstMultiLetterWord) {
|
|
1065
|
+
return;
|
|
1066
|
+
} else {
|
|
1067
|
+
this.firstMultiLetterWord = end;
|
|
1068
|
+
}
|
|
1069
|
+
// remember bold and italic state for later restoration
|
|
1070
|
+
this.wasBold = this.isBold;
|
|
1071
|
+
this.wasItalic = this.isItalic;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* @see https://codemirror.net/docs/ref/#language.StreamParser
|
|
1076
|
+
*/
|
|
1077
|
+
get mediawiki(): StreamParser<State> {
|
|
1078
|
+
return {
|
|
1079
|
+
name: 'mediawiki',
|
|
1080
|
+
|
|
1081
|
+
startState: () => ({
|
|
1082
|
+
tokenize: this.eatWikiText(''),
|
|
1083
|
+
stack: [],
|
|
1084
|
+
inHtmlTag: [],
|
|
1085
|
+
extName: false,
|
|
1086
|
+
extMode: false,
|
|
1087
|
+
extState: false,
|
|
1088
|
+
nTemplate: 0,
|
|
1089
|
+
nLink: 0,
|
|
1090
|
+
nExt: 0,
|
|
1091
|
+
lpar: false,
|
|
1092
|
+
lbrack: false,
|
|
1093
|
+
}),
|
|
1094
|
+
|
|
1095
|
+
copyState: (state): State => {
|
|
1096
|
+
const newState = copyState(state);
|
|
1097
|
+
if (state.extMode && state.extMode.copyState) {
|
|
1098
|
+
newState.extState = state.extMode.copyState(state.extState as State);
|
|
1099
|
+
}
|
|
1100
|
+
return newState;
|
|
1101
|
+
},
|
|
1102
|
+
|
|
1103
|
+
token: (stream, state): string => {
|
|
1104
|
+
let style: string,
|
|
1105
|
+
p: number | null = null,
|
|
1106
|
+
t: Token,
|
|
1107
|
+
f: number | null,
|
|
1108
|
+
tmpTokens: Token[] = [];
|
|
1109
|
+
const readyTokens: Token[] = [];
|
|
1110
|
+
|
|
1111
|
+
if (this.oldTokens.length > 0) {
|
|
1112
|
+
// just send saved tokens till they exists
|
|
1113
|
+
t = this.oldTokens.shift()!;
|
|
1114
|
+
stream.pos = t.pos;
|
|
1115
|
+
return t.style;
|
|
1116
|
+
} else if (stream.sol()) {
|
|
1117
|
+
// reset bold and italic status in every new line
|
|
1118
|
+
this.isBold = false;
|
|
1119
|
+
this.isItalic = false;
|
|
1120
|
+
this.firstSingleLetterWord = null;
|
|
1121
|
+
this.firstMultiLetterWord = null;
|
|
1122
|
+
this.firstSpace = null;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
do {
|
|
1126
|
+
// get token style
|
|
1127
|
+
style = state.tokenize(stream, state);
|
|
1128
|
+
f = this.firstSingleLetterWord || this.firstMultiLetterWord || this.firstSpace;
|
|
1129
|
+
if (f) {
|
|
1130
|
+
// rollback point exists
|
|
1131
|
+
if (f !== p) {
|
|
1132
|
+
// new rollback point
|
|
1133
|
+
p = f;
|
|
1134
|
+
// it's not first rollback point
|
|
1135
|
+
if (tmpTokens.length > 0) {
|
|
1136
|
+
// save tokens
|
|
1137
|
+
readyTokens.push(...tmpTokens);
|
|
1138
|
+
tmpTokens = [];
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
// save token
|
|
1142
|
+
tmpTokens.push({
|
|
1143
|
+
pos: stream.pos,
|
|
1144
|
+
style,
|
|
1145
|
+
state: (state.extMode && state.extMode.copyState || copyState)(state),
|
|
1146
|
+
});
|
|
1147
|
+
} else {
|
|
1148
|
+
// rollback point does not exist
|
|
1149
|
+
// remember style before possible rollback point
|
|
1150
|
+
this.oldStyle = style;
|
|
1151
|
+
// just return token style
|
|
1152
|
+
return style;
|
|
1153
|
+
}
|
|
1154
|
+
} while (!stream.eol());
|
|
1155
|
+
|
|
1156
|
+
if (this.isBold && this.isItalic) {
|
|
1157
|
+
// needs to rollback
|
|
1158
|
+
// restore status
|
|
1159
|
+
this.isItalic = this.wasItalic;
|
|
1160
|
+
this.isBold = this.wasBold;
|
|
1161
|
+
this.firstSingleLetterWord = null;
|
|
1162
|
+
this.firstMultiLetterWord = null;
|
|
1163
|
+
this.firstSpace = null;
|
|
1164
|
+
if (readyTokens.length > 0) {
|
|
1165
|
+
// it contains tickets before the point of rollback
|
|
1166
|
+
// add one apostrophe, next token will be italic (two apostrophes)
|
|
1167
|
+
readyTokens[readyTokens.length - 1]!.pos++;
|
|
1168
|
+
// for sending tokens till the point of rollback
|
|
1169
|
+
this.oldTokens = readyTokens;
|
|
1170
|
+
} else {
|
|
1171
|
+
// there are no tickets before the point of rollback
|
|
1172
|
+
stream.pos = tmpTokens[0]!.pos - 2; // eat( "'" )
|
|
1173
|
+
// send saved Style
|
|
1174
|
+
return this.oldStyle || '';
|
|
1175
|
+
}
|
|
1176
|
+
} else {
|
|
1177
|
+
// do not need to rollback
|
|
1178
|
+
// send all saved tokens
|
|
1179
|
+
this.oldTokens = [
|
|
1180
|
+
...readyTokens,
|
|
1181
|
+
...tmpTokens,
|
|
1182
|
+
];
|
|
1183
|
+
}
|
|
1184
|
+
// return first saved token
|
|
1185
|
+
t = this.oldTokens.shift()!;
|
|
1186
|
+
stream.pos = t.pos;
|
|
1187
|
+
return t.style;
|
|
1188
|
+
},
|
|
1189
|
+
|
|
1190
|
+
blankLine: (state): void => {
|
|
1191
|
+
if (state.extMode && state.extMode.blankLine) {
|
|
1192
|
+
state.extMode.blankLine(state.extState as State, 0);
|
|
1193
|
+
}
|
|
1194
|
+
},
|
|
1195
|
+
|
|
1196
|
+
tokenTable: this.tokenTable,
|
|
1197
|
+
|
|
1198
|
+
languageData: {closeBrackets: {brackets: ['(', '[', '{', '"']}},
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
get 'text/mediawiki'(): StreamParser<State> {
|
|
1203
|
+
return this.mediawiki;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
for (const [language, parser] of Object.entries(plugins)) {
|
|
1208
|
+
Object.defineProperty(MediaWiki.prototype, language, {
|
|
1209
|
+
get() {
|
|
1210
|
+
return parser;
|
|
1211
|
+
},
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* Gets a LanguageSupport instance for the MediaWiki mode.
|
|
1217
|
+
* @param config Configuration for the MediaWiki mode
|
|
1218
|
+
*/
|
|
1219
|
+
export const mediawiki = (config: MwConfig): LanguageSupport => {
|
|
1220
|
+
const mode = new MediaWiki(config);
|
|
1221
|
+
const parser = mode.mediawiki;
|
|
1222
|
+
const lang = StreamLanguage.define(parser);
|
|
1223
|
+
const highlighter = syntaxHighlighting(HighlightStyle.define(mode.getTagStyles()) as Highlighter);
|
|
1224
|
+
return new LanguageSupport(lang, highlighter);
|
|
1225
|
+
};
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* Gets a LanguageSupport instance for the mixed MediaWiki-HTML mode.
|
|
1229
|
+
* @param config Configuration for the MediaWiki mode
|
|
1230
|
+
*/
|
|
1231
|
+
export const html = (config: MwConfig): LanguageSupport => mediawiki({
|
|
1232
|
+
...config,
|
|
1233
|
+
tags: {
|
|
1234
|
+
...config.tags,
|
|
1235
|
+
script: true,
|
|
1236
|
+
style: true,
|
|
1237
|
+
},
|
|
1238
|
+
tagModes: {
|
|
1239
|
+
...config.tagModes,
|
|
1240
|
+
script: 'javascript',
|
|
1241
|
+
style: 'css',
|
|
1242
|
+
},
|
|
1243
|
+
permittedHtmlTags: [
|
|
1244
|
+
'html',
|
|
1245
|
+
'base',
|
|
1246
|
+
'title',
|
|
1247
|
+
'menu',
|
|
1248
|
+
'a',
|
|
1249
|
+
'area',
|
|
1250
|
+
'audio',
|
|
1251
|
+
'map',
|
|
1252
|
+
'track',
|
|
1253
|
+
'video',
|
|
1254
|
+
'embed',
|
|
1255
|
+
'iframe',
|
|
1256
|
+
'object',
|
|
1257
|
+
'picture',
|
|
1258
|
+
'source',
|
|
1259
|
+
'canvas',
|
|
1260
|
+
'col',
|
|
1261
|
+
'colgroup',
|
|
1262
|
+
'tbody',
|
|
1263
|
+
'tfoot',
|
|
1264
|
+
'thead',
|
|
1265
|
+
'button',
|
|
1266
|
+
'datalist',
|
|
1267
|
+
'fieldset',
|
|
1268
|
+
'form',
|
|
1269
|
+
'input',
|
|
1270
|
+
'label',
|
|
1271
|
+
'legend',
|
|
1272
|
+
'meter',
|
|
1273
|
+
'optgroup',
|
|
1274
|
+
'option',
|
|
1275
|
+
'output',
|
|
1276
|
+
'progress',
|
|
1277
|
+
'select',
|
|
1278
|
+
'textarea',
|
|
1279
|
+
'details',
|
|
1280
|
+
'dialog',
|
|
1281
|
+
'slot',
|
|
1282
|
+
'template',
|
|
1283
|
+
'dir',
|
|
1284
|
+
'frame',
|
|
1285
|
+
'frameset',
|
|
1286
|
+
'marquee',
|
|
1287
|
+
'param',
|
|
1288
|
+
'xmp',
|
|
1289
|
+
],
|
|
1290
|
+
implicitlyClosedHtmlTags: [
|
|
1291
|
+
'area',
|
|
1292
|
+
'base',
|
|
1293
|
+
'col',
|
|
1294
|
+
'embed',
|
|
1295
|
+
'frame',
|
|
1296
|
+
'input',
|
|
1297
|
+
'param',
|
|
1298
|
+
'source',
|
|
1299
|
+
'track',
|
|
1300
|
+
],
|
|
1301
|
+
});
|