@bhsd/codemirror-mediawiki 2.3.1 → 2.3.3

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