@bhsd/codemirror-mediawiki 2.1.5 → 2.1.8

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,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
+ });