@bhsd/codemirror-mediawiki 2.28.0 → 2.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/token.js ADDED
@@ -0,0 +1,1888 @@
1
+ /**
2
+ * @author pastakhov, MusikAnimal, Bhsd and others
3
+ * @license GPL-2.0-or-later
4
+ * @see https://gerrit.wikimedia.org/g/mediawiki/extensions/CodeMirror
5
+ */
6
+ import { Tag } from '@lezer/highlight';
7
+ import { decodeHTML, getRegex } from '@bhsd/common';
8
+ import { otherParserFunctions } from '@bhsd/common/dist/cm';
9
+ import { css } from '@codemirror/legacy-modes/mode/css';
10
+ import { javascript, json } from '@codemirror/legacy-modes/mode/javascript';
11
+ import { htmlTags, voidHtmlTags, selfClosingTags, tokenTable, tokens } from './config';
12
+ class MediaWikiData {
13
+ constructor(tags) {
14
+ this.tags = tags.includes('translate') ? tags.filter(tag => tag !== 'tvar') : tags;
15
+ this.firstSingleLetterWord = null;
16
+ this.firstMultiLetterWord = null;
17
+ this.firstSpace = null;
18
+ this.readyTokens = [];
19
+ this.oldToken = null;
20
+ this.mark = null;
21
+ }
22
+ }
23
+ /**
24
+ * 比较两个嵌套状态是否相同
25
+ * @param a
26
+ * @param b
27
+ * @param shallow 是否浅比较
28
+ */
29
+ const cmpNesting = (a, b, shallow) => a.nTemplate === b.nTemplate
30
+ && a.nExt === b.nExt
31
+ && a.nVar === b.nVar
32
+ && a.nLink === b.nLink
33
+ && a.nExtLink === b.nExtLink
34
+ && a.extName === b.extName
35
+ && (shallow || a.extName !== 'mediawiki' || cmpNesting(a.extState, b.extState));
36
+ /**
37
+ * 浅复制嵌套状态
38
+ * @param a
39
+ * @param b
40
+ */
41
+ const copyNesting = (a, b) => {
42
+ a.nTemplate = b.nTemplate;
43
+ a.nExt = b.nExt;
44
+ a.nVar = b.nVar;
45
+ a.nLink = b.nLink;
46
+ a.nExtLink = b.nExtLink;
47
+ a.extName = b.extName;
48
+ };
49
+ const simpleToken = (stream, state) => {
50
+ const style = state.tokenize(stream, state);
51
+ return Array.isArray(style) ? style[0] : style;
52
+ };
53
+ const startState = (tokenize, tags, sof = false) => ({
54
+ tokenize,
55
+ stack: [],
56
+ inHtmlTag: [],
57
+ extName: false,
58
+ extMode: false,
59
+ extState: false,
60
+ nTemplate: 0,
61
+ nExt: 0,
62
+ nVar: 0,
63
+ nLink: 0,
64
+ nExtLink: 0,
65
+ lbrack: false,
66
+ bold: false,
67
+ italic: false,
68
+ dt: { n: 0, html: 0 },
69
+ sof,
70
+ redirect: false,
71
+ imgLink: false,
72
+ data: new MediaWikiData(tags),
73
+ });
74
+ /**
75
+ * 复制 StreamParser 状态
76
+ * @param state
77
+ */
78
+ const copyState = (state) => {
79
+ const result = { ...state };
80
+ for (const [key, val] of Object.entries(state)) {
81
+ if (Array.isArray(val)) {
82
+ // @ts-expect-error initial value
83
+ result[key] = [...val];
84
+ }
85
+ else if (key === 'extState') {
86
+ result[key] = (state.extName && state.extMode && state.extMode.copyState || copyState)(val);
87
+ }
88
+ else if (key !== 'data' && val && typeof val === 'object') {
89
+ // @ts-expect-error initial value
90
+ result[key] = { ...val }; // eslint-disable-line @typescript-eslint/no-misused-spread
91
+ }
92
+ }
93
+ return result;
94
+ };
95
+ /**
96
+ * 判断字符串是否为 HTML 实体
97
+ * @param str 字符串
98
+ */
99
+ const isHtmlEntity = (str) =>
100
+ // eslint-disable-next-line @typescript-eslint/no-misused-spread
101
+ typeof document !== 'object' || str.startsWith('#') || [...decodeHTML(`&${str}`)].length === 1;
102
+ /**
103
+ * 更新内部 Tokenizer
104
+ * @param state
105
+ * @param tokenizer
106
+ */
107
+ const chain = (state, tokenizer) => {
108
+ state.stack.unshift(state.tokenize);
109
+ state.tokenize = tokenizer;
110
+ };
111
+ /**
112
+ * 更新内部 Tokenizer
113
+ * @param state
114
+ */
115
+ const pop = (state) => {
116
+ state.tokenize = state.stack.shift();
117
+ };
118
+ /**
119
+ * 是否为行首语法
120
+ * @param stream
121
+ * @param table 是否允许表格
122
+ * @param file 是否为文件
123
+ */
124
+ const isSolSyntax = (stream, table, file) => stream.sol() && (table && stream.match(/^\s*(?::+\s*)?\{\|/u, false)
125
+ || stream.match(/^(?:-{4}|=)/u, false)
126
+ || !file && /[*#;:]/u.test(stream.peek() || ''));
127
+ /**
128
+ * 获取负向先行断言
129
+ * @param chars
130
+ * @param comment 是否仅排除注释
131
+ */
132
+ const lookahead = (chars, comment) => {
133
+ const table = {
134
+ "'": "'(?!')",
135
+ '{': String.raw `\{(?!\{)`,
136
+ '}': String.raw `\}(?!\})`,
137
+ '<': comment ? '<(?!!--)' : '<(?!!--|/?[a-z])',
138
+ '~': '~~?(?!~)',
139
+ _: '_(?!_)',
140
+ '[': String.raw `\[(?!\[)`,
141
+ ']': String.raw `\](?!\])`,
142
+ '/': '/(?!>)',
143
+ '-': String.raw `-(?!\{(?!\{))`,
144
+ };
145
+ if (typeof comment === 'object') {
146
+ const { data: { tags } } = comment;
147
+ table['<'] = String.raw `<(?!!--${tags.includes('onlyinclude') ? '|onlyinclude>' : ''}|(?:${tags.filter(tag => tag !== 'onlyinclude').join('|')})(?:[\s/>]|$))`;
148
+ }
149
+ // eslint-disable-next-line @typescript-eslint/no-misused-spread
150
+ return [...chars].map(ch => table[ch]).join('|');
151
+ };
152
+ /**
153
+ * 是否需要检查冒号
154
+ * @param state
155
+ */
156
+ const needColon = (state) => {
157
+ const { dt } = state;
158
+ return Boolean(dt.n) && dt.html === 0 && !state.bold && !state.italic && cmpNesting(dt, state, true);
159
+ };
160
+ /**
161
+ * 获取外部链接正则表达式
162
+ * @param punctuations 标点符号
163
+ */
164
+ const getUrlRegex = (punctuations = '') => {
165
+ const chars = "~{'";
166
+ return String.raw `[^&${chars}\p{Zs}[\]<>"${punctuations}]|&(?![lg]t;)|${lookahead(chars)}`;
167
+ };
168
+ /**
169
+ * 获取标点符号
170
+ * @param lpar 是否包含左括号
171
+ */
172
+ const getPunctuations = (lpar) => String.raw `.,;:!?\\${lpar ? '' : ')'}`;
173
+ const getTokenizer = (method, context) => function (...args) {
174
+ const tokenizer = method.apply(this, args);
175
+ Object.defineProperty(tokenizer, 'name', { value: context.name });
176
+ tokenizer.args = args;
177
+ return tokenizer;
178
+ };
179
+ const makeFullStyle = (style, state) => (typeof style === 'string'
180
+ ? style
181
+ : `${style[0]} ${state.bold || state.dt?.n ? tokens.strong : ''} ${state.italic ? tokens.em : ''}`).trim().replace(/\s{2,}/gu, ' ') || ' ';
182
+ const makeLocalStyle = (style, state, endGround) => {
183
+ let ground = '';
184
+ switch (state.nTemplate) {
185
+ case 0:
186
+ break;
187
+ case 1:
188
+ ground += '-template';
189
+ break;
190
+ case 2:
191
+ ground += '-template2';
192
+ break;
193
+ default:
194
+ ground += '-template3';
195
+ break;
196
+ }
197
+ switch (state.nExt) {
198
+ case 0:
199
+ break;
200
+ case 1:
201
+ ground += '-ext';
202
+ break;
203
+ case 2:
204
+ ground += '-ext2';
205
+ break;
206
+ default:
207
+ ground += '-ext3';
208
+ break;
209
+ }
210
+ if (state.nLink || state.nExtLink) {
211
+ ground += '-link';
212
+ }
213
+ if (endGround) {
214
+ state[endGround]--;
215
+ const { dt } = state;
216
+ if (dt?.n && state[endGround] < dt[endGround]) {
217
+ dt.n = 0;
218
+ }
219
+ }
220
+ return (ground && `mw${ground}-ground `) + style;
221
+ };
222
+ const makeLocalTagStyle = (tag, state, endGround) => makeLocalStyle(tokens[tag], state, endGround);
223
+ const makeStyle = (style, state, endGround) => [makeLocalStyle(style, state, endGround)];
224
+ const makeTagStyle = (tag, state, endGround) => makeStyle(tokens[tag], state, endGround);
225
+ /**
226
+ * Remembers position and status for rollbacking.
227
+ * It is needed for changing from bold to italic with apostrophes before it, if required.
228
+ *
229
+ * @see https://phabricator.wikimedia.org/T108455
230
+ * @param stream
231
+ * @param state
232
+ */
233
+ const prepareItalicForCorrection = (stream, state) => {
234
+ // See Parser::doQuotes() in MediaWiki Core, it works similarly.
235
+ // firstSingleLetterWord has maximum priority
236
+ // firstMultiLetterWord has medium priority
237
+ // firstSpace has low priority
238
+ const end = stream.pos, str = stream.string.slice(0, end - 3), x1 = str.slice(-1), x2 = str.slice(-2, -1), { data } = state;
239
+ // firstSingleLetterWord always is undefined here
240
+ if (x1 === ' ') {
241
+ if (data.firstMultiLetterWord || data.firstSpace) {
242
+ return;
243
+ }
244
+ data.firstSpace = end;
245
+ }
246
+ else if (x2 === ' ') {
247
+ data.firstSingleLetterWord = end;
248
+ }
249
+ else if (data.firstMultiLetterWord) {
250
+ return;
251
+ }
252
+ else {
253
+ data.firstMultiLetterWord = end;
254
+ }
255
+ data.mark = end;
256
+ };
257
+ /**
258
+ * 获取`|`的正则表达式
259
+ * @param isTemplate 是否在模板中
260
+ */
261
+ const getPipe = (isTemplate) => String.raw `(?:${isTemplate ? '' : String.raw `\||`}\{(?:\{\s*|\s*\()!\s*\}\})`;
262
+ /**
263
+ * 计算标签属性的引号
264
+ * @param stream StringStream
265
+ */
266
+ const getQuote = (stream) => {
267
+ const peek = stream.peek();
268
+ return peek === "'" || peek === '"' ? peek.repeat(2) : '';
269
+ };
270
+ /**
271
+ * 是否需要模板参数的等号
272
+ * @param t Tokenizer
273
+ */
274
+ const getEqual = (t) => t.name === 'inTemplateArgument' && t.args[0] ? '=' : '';
275
+ /**
276
+ * 下一个字符是否为空白字符
277
+ * @param stream StringStream
278
+ * @param sol 是否在行首
279
+ */
280
+ const peekSpace = (stream, sol) => {
281
+ if (sol && stream.sol()) {
282
+ return true;
283
+ }
284
+ const peek = stream.peek();
285
+ return Boolean(peek && !peek.trim());
286
+ };
287
+ const syntaxHighlight = new Set(['syntaxhighlight', 'source', 'pre']), pageFunctions = new Set([
288
+ 'subst',
289
+ 'safesubst',
290
+ 'raw',
291
+ 'msg',
292
+ 'filepath',
293
+ 'localurl',
294
+ 'localurle',
295
+ 'fullurl',
296
+ 'fullurle',
297
+ 'canonicalurl',
298
+ 'canonicalurle',
299
+ 'int',
300
+ 'msgnw',
301
+ ]), headerRegex = new RegExp(`^(?:[^&[<{~'-]|${lookahead("<{~'-")})+`, 'iu'), templateRegex = new RegExp(`^(?:[^|{}<]|${lookahead('{}<', true)})+`, 'u'), argumentRegex = new RegExp(`^(?:[^|[&:}{<~'_-]|${lookahead("}{<~'_-")})+`, 'iu'), styleRegex = new RegExp(`^(?:[^|[&}{<~'_-]|${lookahead("}{<~'_-")})+`, 'iu'), wikiRegex = new RegExp(`^(?:[^&'{[<~_:-]|${lookahead("'{[<~_-")})+`, 'u'), tableDefinitionRegex = new RegExp(`^(?:[^&={<]|${lookahead('{<')})+`, 'iu'), extLinkChars = "[{'<-", tableDefinitionChars = '{<', tableCellChars = "'<~_{-", htmlAttrChars = '{/', freeRegex = [false, true].map(lpar => {
302
+ const punctuations = getPunctuations(lpar), source = getUrlRegex(punctuations);
303
+ return new RegExp(`^(?:${source}|[${punctuations}]+(?=${source}))*`, 'u');
304
+ }), indentedTableRegex = [false, true].map(isTemplate => new RegExp(String.raw `^:*\s*(?=\{${getPipe(isTemplate)})`, 'u')), tableRegex = [false, true]
305
+ .map(isTemplate => new RegExp(String.raw `^${getPipe(isTemplate)}\s*`, 'u')), spacedTableRegex = [false, true].map(isTemplate => new RegExp(String.raw `^\s*(:+\s*)?(?=\{${getPipe(isTemplate)})`, 'u')), linkTextRegex = [false, true].map(file => {
306
+ const chars = `]'{<${file ? '~' : '['}-`;
307
+ return new RegExp(`^(?:[^&${file ? '[|' : ''}\\${chars}]|${lookahead(chars)})+`, 'iu');
308
+ }), linkErrorRegex = [
309
+ new RegExp(String.raw `^(?:[<>{}]|%(?:3[ce]|[57][bd])|${lookahead('[]')})+`, 'iu'),
310
+ new RegExp(String.raw `^(?:\}|${lookahead('[]{')})+`, 'u'),
311
+ new RegExp(String.raw `^(?:[>}]|%(?:3[ce]|[57][bd])|${lookahead('[]{<')})+`, 'iu'),
312
+ ], tableDefinitionValueRegex = ['', '='].map(equal => new RegExp(String.raw `^(?:[^\s&${tableDefinitionChars}${equal}]|${lookahead(tableDefinitionChars)})+`, 'iu')), variableRegex = [false, true].map(isDefault => new RegExp(String.raw `^(?:[^|{}<${isDefault ? "[&~'_:-" : ''}]|\}(?!\}\})|${isDefault ? lookahead("{<~'_-") : lookahead('{<', true)})+`, 'iu')), parserFunctionRegex = ['', '[&', '[&:'].map(s => getRegex(chars => new RegExp(`^(?:[^|${s}${chars}]|${lookahead(chars)})+`, 'iu'))), getExtLinkTextRegex = getRegex(pipe => new RegExp(String.raw `^(?:[^\]&${pipe}${extLinkChars}]|${lookahead(extLinkChars)})+`, 'iu')), getExtLinkRegex = getRegex(pipe => new RegExp(`^(?:${getUrlRegex(pipe)})+`, 'u')), getTableDefinitionRegex = getRegex(s => new RegExp(`^(?:[^&${tableDefinitionChars}${s}]|${lookahead(tableDefinitionChars)})+`, 'iu')), getTableCellRegex = getRegex(s => new RegExp(`^(?:[^[&${s}${tableCellChars}]|${lookahead(tableCellChars)})+`, 'iu')), getHtmlAttrRegex = getRegex(s => new RegExp(`^(?:[^<>&${htmlAttrChars}${s}]|${lookahead(htmlAttrChars)})+`, 'u')), getHtmlAttrKeyRegex = getRegex(pipe => new RegExp(`^(?:[^<>&={/${pipe}]|${lookahead('{/')})+`, 'u')), getExtAttrRegex = getRegex(s => new RegExp(`^(?:[^>/${s}]|${lookahead('/')})+`, 'u')), getExtTagCloseRegex = getRegex(name => name === 'onlyinclude'
313
+ ? /<\/onlyinclude(?:>|$)/u
314
+ : new RegExp(String.raw `</${name}\s*(?:>|$)`, 'iu')), getNestedRegex = getRegex(tag => new RegExp(String.raw `^(?:[^<]|<(?!${tag}(?:[\s/>]|$)))+`, 'iu'));
315
+ /** Adapted from the original CodeMirror 5 stream parser by Pavel Astakhov */
316
+ export class MediaWiki {
317
+ constructor(config) {
318
+ const { urlProtocols, permittedHtmlTags, implicitlyClosedHtmlTags, tags, nsid, variants, redirection = ['#REDIRECT'], img = {}, } = config;
319
+ this.config = config;
320
+ this.tokenTable = { ...tokenTable };
321
+ this.hiddenTable = {};
322
+ this.permittedHtmlTags = new Set([
323
+ ...htmlTags,
324
+ ...permittedHtmlTags ?? [],
325
+ ]);
326
+ this.voidHtmlTags = new Set([
327
+ ...voidHtmlTags,
328
+ ...implicitlyClosedHtmlTags ?? [],
329
+ ]);
330
+ this.urlProtocols = new RegExp(String.raw `^(?:${urlProtocols})(?=[^\p{Zs}[\]<>"])`, 'iu');
331
+ this.linkRegex = new RegExp(String.raw `^\[(?!${urlProtocols})\s*`, 'iu');
332
+ this.fileRegex = new RegExp(String.raw `^(?:${Object.entries(nsid).filter(([, id]) => id === 6).map(([ns]) => ns).join('|')})\s*:`, 'iu');
333
+ this.redirectRegex = new RegExp(String.raw `^\s*(?:${redirection.join('|')})(\s*:)?\s*(?=\[\[|$)`, 'iu');
334
+ this.img = Object.keys(img).filter(word => !/\$1./u.test(word));
335
+ this.imgRegex = new RegExp(String.raw `^(?:${this.img.filter(word => word.endsWith('$1')).map(word => word.slice(0, -2))
336
+ .join('|')}|(?:${this.img.filter(word => !word.endsWith('$1')).join('|')}|(?:\d+x?|\d*x\d+)\s*(?:px)?px)\s*(?=\||\]\]|$))`, 'u');
337
+ this.tags = [...Object.keys(tags), 'includeonly', 'noinclude', 'onlyinclude'];
338
+ this.convertRegex = new RegExp(String.raw `^(?:[^}|;&='{[<~_-]|\}(?!-)|=(?!>)|\[(?!\[|${urlProtocols})|${lookahead("'{<~_-")})+`, 'u');
339
+ this.convertSemicolon = variants && new RegExp(String.raw `^;\s*(?=(?:[^;]*?=>\s*)?(?:${variants.join('|')})\s*:|(?:$|\}-))`, 'u');
340
+ this.convertLang = variants
341
+ && new RegExp(String.raw `^(?:=>\s*)?(?:${variants.join('|')})\s*:`, 'u');
342
+ this.hasVariants = Boolean(variants?.length);
343
+ this.preRegex = [false, true].map(begin => new RegExp(String.raw `^(?:[^<&-]|-${this.hasVariants ? String.raw `(?!\{)` : ''}|<(?!${begin ? '/' : ''}nowiki>))+`, 'iu'));
344
+ this.autocompleteNamespaces = {
345
+ 0: '',
346
+ 6: 'File:',
347
+ 8: 'MediaWiki:',
348
+ 10: 'Template:',
349
+ 274: 'Widget:',
350
+ 828: 'Module:',
351
+ };
352
+ this.registerGroundTokens();
353
+ }
354
+ /**
355
+ * Dynamically register a token in CodeMirror.
356
+ * This is solely for use by this.addTag() and CodeMirrorModeMediaWiki.makeLocalStyle().
357
+ *
358
+ * @param token
359
+ * @param hidden Whether the token is not highlighted
360
+ * @param parent
361
+ */
362
+ addToken(token, hidden = false, parent) {
363
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
364
+ this[hidden ? 'hiddenTable' : 'tokenTable'][`mw-${token}`] ??= Tag.define(parent);
365
+ }
366
+ /**
367
+ * Register the ground tokens. These aren't referenced directly in the StreamParser, nor do
368
+ * they have a parent Tag, so we don't need them as constants like we do for other tokens.
369
+ * See makeLocalStyle() for how these tokens are used.
370
+ */
371
+ registerGroundTokens() {
372
+ const grounds = [
373
+ 'ext',
374
+ 'ext-link',
375
+ 'ext2',
376
+ 'ext2-link',
377
+ 'ext3',
378
+ 'ext3-link',
379
+ 'link',
380
+ 'template-ext',
381
+ 'template-ext-link',
382
+ 'template-ext2',
383
+ 'template-ext2-link',
384
+ 'template-ext3',
385
+ 'template-ext3-link',
386
+ 'template',
387
+ 'template-link',
388
+ 'template2-ext',
389
+ 'template2-ext-link',
390
+ 'template2-ext2',
391
+ 'template2-ext2-link',
392
+ 'template2-ext3',
393
+ 'template2-ext3-link',
394
+ 'template2',
395
+ 'template2-link',
396
+ 'template3-ext',
397
+ 'template3-ext-link',
398
+ 'template3-ext2',
399
+ 'template3-ext2-link',
400
+ 'template3-ext3',
401
+ 'template3-ext3-link',
402
+ 'template3',
403
+ 'template3-link',
404
+ ];
405
+ for (const ground of grounds) {
406
+ this.addToken(`${ground}-ground`);
407
+ }
408
+ for (let i = 1; i < 7; i++) {
409
+ this.addToken(`section--${i}`);
410
+ }
411
+ for (const tag of this.tags) {
412
+ this.addToken(`tag-${tag}`, tag !== 'nowiki' && tag !== 'pre');
413
+ this.addToken(`ext-${tag}`, true);
414
+ }
415
+ for (const tag of this.permittedHtmlTags) {
416
+ this.addToken(`html-${tag}`, true);
417
+ }
418
+ for (const i in this.autocompleteNamespaces) {
419
+ if (Number.isInteger(Number(i))) {
420
+ this.addToken(`function-${i}`, true);
421
+ }
422
+ }
423
+ }
424
+ @getTokenizer
425
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this
426
+ inChars({ length }, tag) {
427
+ return (stream, state) => {
428
+ stream.pos += length;
429
+ pop(state);
430
+ return makeLocalTagStyle(tag, state);
431
+ };
432
+ }
433
+ @getTokenizer
434
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this
435
+ inStr(str, tag, errorTag = 'error') {
436
+ return (stream, state) => {
437
+ if (stream.match(str, Boolean(tag))) {
438
+ pop(state);
439
+ return tag ? makeLocalTagStyle(tag, state) : '';
440
+ }
441
+ else if (!stream.skipTo(str)) {
442
+ stream.skipToEnd();
443
+ }
444
+ return makeLocalTagStyle(errorTag, state);
445
+ };
446
+ }
447
+ @getTokenizer
448
+ eatWikiText(style) {
449
+ if (style in tokens) {
450
+ style = tokens[style]; // eslint-disable-line no-param-reassign
451
+ }
452
+ const regex = /^(?:(?:RFC|PMID)[\p{Zs}\t]+\d+|ISBN[\p{Zs}\t]+(?:97[89][\p{Zs}\t-]?)?(?:\d[\p{Zs}\t-]?){9}[\dxX])\b/u;
453
+ return (stream, state) => {
454
+ let ch;
455
+ if (stream.eol()) {
456
+ return '';
457
+ }
458
+ else if (stream.sol()) {
459
+ if (state.sof) {
460
+ if (stream.match(/^\s+$/u)) {
461
+ return '';
462
+ }
463
+ state.sof = false;
464
+ const mt = stream.match(this.redirectRegex);
465
+ if (mt) {
466
+ state.redirect = { colon: !mt[1] };
467
+ return tokens.redirect;
468
+ }
469
+ }
470
+ else if (state.redirect) {
471
+ if (stream.match(/^\s+(?=$|\[\[)/u)) {
472
+ return '';
473
+ }
474
+ else if (state.redirect.colon && stream.match(/^\s*:\s*(?=$|\[\[)/u)) {
475
+ state.redirect.colon = false;
476
+ return tokens.redirect;
477
+ }
478
+ state.redirect = false;
479
+ }
480
+ ch = stream.next();
481
+ const isTemplate = ['inTemplateArgument', 'inParserFunctionArgument', 'inVariable']
482
+ .includes(state.tokenize.name);
483
+ switch (ch) {
484
+ case '#':
485
+ case ';':
486
+ case '*':
487
+ stream.backUp(1);
488
+ return this.eatList(stream, state);
489
+ case ':':
490
+ // Highlight indented tables :{|, bug T108454
491
+ if (stream.match(indentedTableRegex[isTemplate ? 1 : 0])) {
492
+ chain(state, this.eatStartTable);
493
+ return makeLocalTagStyle('list', state);
494
+ }
495
+ return this.eatList(stream, state);
496
+ case '=': {
497
+ const tmp = stream
498
+ .match(/^(={0,5})(.+?(=\1\s*)(?:<!--(?!.*-->\s*\S).*)?)$/u);
499
+ // Title
500
+ if (tmp) {
501
+ stream.backUp(tmp[2].length);
502
+ chain(state, this.inSectionHeader(tmp[3]));
503
+ return makeLocalStyle(`${tokens.sectionHeader} mw-section--${tmp[1].length + 1}`, state);
504
+ }
505
+ break;
506
+ }
507
+ case '{':
508
+ if (stream.match(tableRegex[isTemplate ? 1 : 0])) {
509
+ chain(state, this.inTableDefinition());
510
+ return makeLocalTagStyle('tableBracket', state);
511
+ }
512
+ break;
513
+ case '-':
514
+ if (stream.match(/^-{3,}/u)) {
515
+ return tokens.hr;
516
+ }
517
+ break;
518
+ default:
519
+ if (!ch.trim()) {
520
+ // Leading spaces is valid syntax for tables, bug T108454
521
+ const mt = stream.match(spacedTableRegex[isTemplate ? 1 : 0]);
522
+ if (mt) {
523
+ chain(state, this.eatStartTable);
524
+ return makeLocalStyle(mt[1] ? tokens.list : '', state);
525
+ }
526
+ else if (ch === ' '
527
+ && !/^ \s*(?=<!--)(?:\s|<!--(?:(?!-->).)*-->)+$/u.test(stream.string)) {
528
+ /** @todo indent-pre is sometimes suppressed */
529
+ return tokens.skipFormatting;
530
+ }
531
+ }
532
+ }
533
+ }
534
+ else {
535
+ ch = stream.next();
536
+ }
537
+ switch (ch) {
538
+ case '~':
539
+ if (stream.match(/^~{2,4}/u)) {
540
+ return tokens.signature;
541
+ }
542
+ break;
543
+ case '<': {
544
+ if (stream.match('!--')) { // comment
545
+ chain(state, this.inComment);
546
+ return makeLocalTagStyle('comment', state);
547
+ }
548
+ const isCloseTag = Boolean(stream.eat('/')), mt = stream.match(/^([a-z][^\s/>]*)>?/iu, false);
549
+ if (mt) {
550
+ const tagname = mt[1].toLowerCase();
551
+ if ((mt[0] === 'onlyinclude>' || tagname !== 'onlyinclude')
552
+ && state.data.tags.includes(tagname)) {
553
+ // Extension tag
554
+ return this.eatExtTag(tagname, isCloseTag, state);
555
+ }
556
+ else if (this.permittedHtmlTags.has(tagname)) {
557
+ // Html tag
558
+ return this.eatHtmlTag(tagname, isCloseTag, state);
559
+ }
560
+ }
561
+ break;
562
+ }
563
+ case '{': {
564
+ // Can't be a variable when it starts with more than 3 brackets (T108450) or
565
+ // a single { followed by a template. E.g. {{{!}} starts a table (T292967).
566
+ if (stream.match(/^\{\{(?!\{|[^{}]*\}\}(?!\}))\s*/u)) {
567
+ state.nVar++;
568
+ chain(state, this.inVariable());
569
+ return makeLocalTagStyle('templateVariableBracket', state);
570
+ }
571
+ const mt = stream.match(/^\{(?!\{(?!\{))/u);
572
+ if (mt) {
573
+ return this.eatTransclusion(stream, state) ?? makeStyle(style, state);
574
+ }
575
+ break;
576
+ }
577
+ case '_': {
578
+ const { pos } = stream;
579
+ stream.eatWhile('_');
580
+ switch (stream.pos - pos) {
581
+ case 0:
582
+ break;
583
+ case 1:
584
+ return this.eatDoubleUnderscore(style, stream, state);
585
+ default:
586
+ if (!stream.eol()) {
587
+ stream.backUp(2);
588
+ }
589
+ return makeStyle(style, state);
590
+ }
591
+ break;
592
+ }
593
+ case '[':
594
+ // Link Example: [[ Foo | Bar ]]
595
+ if (stream.match(this.linkRegex)) {
596
+ const { redirect } = state;
597
+ if (redirect || /[^[\]|]/u.test(stream.peek() || '')) {
598
+ state.nLink++;
599
+ state.lbrack = undefined;
600
+ chain(state, this.inLink(!redirect && Boolean(stream.match(this.fileRegex, false))));
601
+ return makeLocalTagStyle('linkBracket', state);
602
+ }
603
+ else if (stream.match(']]')) {
604
+ return makeStyle(style, state);
605
+ }
606
+ }
607
+ else {
608
+ const mt = stream.match(this.urlProtocols, false);
609
+ if (mt) {
610
+ state.nExtLink++;
611
+ chain(state, this.eatExternalLinkProtocol(mt[0], false));
612
+ return makeLocalTagStyle('extLinkBracket', state);
613
+ }
614
+ }
615
+ break;
616
+ case "'": {
617
+ const result = this.eatApostrophes(state)(stream, state);
618
+ if (result) {
619
+ return result;
620
+ }
621
+ break;
622
+ }
623
+ case ':':
624
+ if (needColon(state)) {
625
+ state.dt.n--;
626
+ return makeLocalTagStyle('list', state);
627
+ }
628
+ break;
629
+ case '&':
630
+ return makeStyle(this.eatEntity(stream, style), state);
631
+ case '-':
632
+ if (this.config.variants?.length && stream.match(/^\{(?!\{)\s*/u)) {
633
+ chain(state, this.inConvert(style, true));
634
+ return makeLocalTagStyle('convertBracket', state);
635
+ }
636
+ // no default
637
+ }
638
+ if (state.stack.length === 0) {
639
+ if (ch !== '_') {
640
+ // highlight free external links, bug T108448
641
+ if (/[\p{L}\p{N}]/u.test(ch)) {
642
+ stream.backUp(1);
643
+ }
644
+ else {
645
+ stream.eatWhile(/[^\p{L}\p{N}_&'{[<~:-]/u);
646
+ }
647
+ const mt = stream.match(this.urlProtocols, false);
648
+ if (mt) {
649
+ chain(state, this.eatExternalLinkProtocol(mt[0]));
650
+ return makeStyle(style, state);
651
+ }
652
+ const mtMagic = stream.match(regex, false);
653
+ if (mtMagic) {
654
+ chain(state, this.inChars(mtMagic[0], 'magicLink'));
655
+ return makeStyle(style, state);
656
+ }
657
+ }
658
+ stream.eatWhile(/[\p{L}\p{N}]/u);
659
+ }
660
+ return makeStyle(style, state);
661
+ };
662
+ }
663
+ @getTokenizer
664
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this
665
+ eatApostrophes(obj) {
666
+ return (stream, state) => {
667
+ // skip the irrelevant apostrophes ( >5 or =4 )
668
+ if (stream.match(/^'*(?='{5})/u) || stream.match(/^'''(?!')/u, false)) {
669
+ return false;
670
+ }
671
+ else if (stream.match("''''")) { // bold italic
672
+ obj.bold = !obj.bold;
673
+ obj.italic = !obj.italic;
674
+ return makeLocalTagStyle('apostrophes', state);
675
+ }
676
+ else if (stream.match("''")) { // bold
677
+ if (obj === state && state.data.firstSingleLetterWord === null) {
678
+ prepareItalicForCorrection(stream, state);
679
+ }
680
+ obj.bold = !obj.bold;
681
+ return makeLocalTagStyle('apostrophes', state);
682
+ }
683
+ else if (stream.eat("'")) { // italic
684
+ obj.italic = !obj.italic;
685
+ return makeLocalTagStyle('apostrophes', state);
686
+ }
687
+ return false;
688
+ };
689
+ }
690
+ @getTokenizer
691
+ eatExternalLinkProtocol({ length }, free = true) {
692
+ return (stream, state) => {
693
+ stream.pos += length;
694
+ state.tokenize = free ? this.eatFreeExternalLink : this.inExternalLink();
695
+ return makeLocalTagStyle(free ? 'freeExtLinkProtocol' : 'extLinkProtocol', state);
696
+ };
697
+ }
698
+ @getTokenizer
699
+ inExternalLink(text) {
700
+ return (stream, state) => {
701
+ const t = state.stack[0], equal = getEqual(t), isNested = ['inTemplateArgument', 'inParserFunctionArgument', 'inVariable', 'inTableCell']
702
+ .includes(t.name), pipe = (isNested ? '|' : '') + equal, peek = stream.peek();
703
+ if (stream.sol()
704
+ || stream.match(/^\p{Zs}*\]/u)
705
+ || isNested && peek === '|'
706
+ || equal && peek === '=') {
707
+ pop(state);
708
+ return makeLocalTagStyle('extLinkBracket', state, 'nExtLink');
709
+ }
710
+ else if (text) {
711
+ return stream.match(getExtLinkTextRegex(pipe))
712
+ ? makeTagStyle('extLinkText', state)
713
+ : this.eatWikiText('extLinkText')(stream, state);
714
+ }
715
+ else if (stream.match(getExtLinkRegex(pipe))) {
716
+ return makeLocalTagStyle('extLink', state);
717
+ }
718
+ state.tokenize = this.inExternalLink(true);
719
+ return '';
720
+ };
721
+ }
722
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this
723
+ eatFreeExternalLink(stream, state) {
724
+ const mt = stream.match(freeRegex[0]);
725
+ if (!stream.eol() && mt[0].includes('(') && getPunctuations().includes(stream.peek())) {
726
+ stream.match(freeRegex[1]);
727
+ }
728
+ pop(state);
729
+ return makeTagStyle('freeExtLink', state);
730
+ }
731
+ @getTokenizer
732
+ inLink(file, section) {
733
+ const style = section ? tokens[file ? 'error' : 'linkToSection'] : `${tokens.linkPageName} ${tokens.pageName}`, re = section
734
+ ? /^(?:[^|<[\]{}]|<(?!!--|\/?[a-z]))+/iu
735
+ : /^(?:&#(?:\d+|x[a-f\d]+);|[^#|<>[\]{}%]|%(?!3[ce]|[57][bd]))+/iu;
736
+ let lt;
737
+ return (stream, state) => {
738
+ if (stream.sol()
739
+ || lt && stream.pos > lt
740
+ || stream.match(/^\s*\]\]/u)
741
+ || stream.match(/^\[\[/u, false)) {
742
+ state.redirect = false;
743
+ state.lbrack = false;
744
+ pop(state);
745
+ return makeLocalTagStyle('linkBracket', state, 'nLink');
746
+ }
747
+ lt = undefined;
748
+ const space = stream.eatSpace(), { redirect } = state;
749
+ if (!section && stream.match(/^#\s*/u)) {
750
+ state.tokenize = this.inLink(file, true);
751
+ return makeTagStyle(file ? 'error' : 'linkToSection', state);
752
+ }
753
+ else if (stream.match(/^\|\s*/u)) {
754
+ state.tokenize = this.inLinkText(file);
755
+ let s = redirect ? 'error' : 'linkDelimiter';
756
+ if (file) {
757
+ s = 'fileDelimiter';
758
+ this.toEatImageParameter(stream, state);
759
+ }
760
+ return makeLocalTagStyle(s, state);
761
+ }
762
+ let regex;
763
+ if (redirect) {
764
+ [regex] = linkErrorRegex;
765
+ }
766
+ else if (section) {
767
+ [, regex] = linkErrorRegex;
768
+ }
769
+ else {
770
+ [, , regex] = linkErrorRegex;
771
+ }
772
+ if (stream.match(regex)) {
773
+ return makeTagStyle('error', state);
774
+ }
775
+ else if (redirect) {
776
+ stream.match(/^(?:[^|<>[\]{}%]|%(?!3[ce]|[57][bd]))+/iu);
777
+ return makeStyle(style, state);
778
+ }
779
+ else if (stream.match(re) || space) {
780
+ return makeStyle(style, state);
781
+ }
782
+ else if (stream.match(/^<(?!(?:includeonly|noinclude)(?:\/?>|\s|$))[/a-z]/iu, false)
783
+ && !stream.match(/^<onlyinclude>/u, false)) {
784
+ lt = stream.pos + 1;
785
+ }
786
+ return this.eatWikiText(section ? style : 'error')(stream, state);
787
+ };
788
+ }
789
+ @getTokenizer
790
+ inLinkText(file, gallery) {
791
+ const linkState = { bold: false, italic: false }, regex = linkTextRegex[file ? 1 : 0];
792
+ return (stream, state) => {
793
+ const tmpstyle = `${tokens[file ? 'fileText' : 'linkText']} ${linkState.bold ? tokens.strong : ''} ${linkState.italic ? tokens.em : ''} ${file && state.imgLink ? tokens.pageName : ''}`, { redirect, lbrack } = state, closing = stream.match(']]');
794
+ if (closing || !file && stream.match('[[', false)) {
795
+ if (gallery) {
796
+ return makeStyle(tmpstyle, state);
797
+ }
798
+ else if (closing && !redirect && lbrack && stream.peek() === ']') {
799
+ stream.backUp(1);
800
+ state.lbrack = false;
801
+ return makeStyle(tmpstyle, state);
802
+ }
803
+ state.redirect = false;
804
+ state.lbrack = false;
805
+ pop(state);
806
+ return makeLocalTagStyle('linkBracket', state, 'nLink');
807
+ }
808
+ else if (redirect) {
809
+ if (!stream.skipTo(']]')) {
810
+ stream.skipToEnd();
811
+ }
812
+ return makeLocalTagStyle('error', state);
813
+ }
814
+ else if (file && stream.match(/^\|\s*/u)) {
815
+ this.toEatImageParameter(stream, state);
816
+ return makeLocalTagStyle('fileDelimiter', state);
817
+ }
818
+ else if (stream.match(/^'(?=')/u)) {
819
+ return this.eatApostrophes(linkState)(stream, state) || makeStyle(tmpstyle, state);
820
+ }
821
+ else if (file && isSolSyntax(stream, true, true)
822
+ || stream.sol() && stream.match('{|', false)) {
823
+ return this.eatWikiText(tmpstyle)(stream, state);
824
+ }
825
+ const mt = stream.match(regex);
826
+ if (lbrack === undefined && mt?.[0].includes('[')) {
827
+ state.lbrack = true;
828
+ }
829
+ return mt ? makeStyle(tmpstyle, state) : this.eatWikiText(tmpstyle)(stream, state);
830
+ };
831
+ }
832
+ toEatImageParameter(stream, state) {
833
+ state.imgLink = false;
834
+ const mt = stream.match(this.imgRegex, false);
835
+ if (mt) {
836
+ if (this.config.img?.[`${mt[0]}$1`] === 'img_link') {
837
+ state.imgLink = true;
838
+ }
839
+ chain(state, this.inChars(mt[0], 'imageParameter'));
840
+ }
841
+ }
842
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this
843
+ eatList(stream, state) {
844
+ const mt = stream.match(/^[*#;:]*/u), { dt } = state;
845
+ if (mt[0].includes(';')) {
846
+ dt.n = mt[0].split(';').length - 1;
847
+ copyNesting(dt, state);
848
+ }
849
+ return makeLocalTagStyle('list', state);
850
+ }
851
+ eatDoubleUnderscore(style, stream, state) {
852
+ const { config: { doubleUnderscore } } = this, name = stream.match(/^[\p{L}\p{N}_]+?__/u);
853
+ if (name) {
854
+ if (Object.prototype.hasOwnProperty.call(doubleUnderscore[0], `__${name[0].toLowerCase()}`)
855
+ || Object.prototype.hasOwnProperty.call(doubleUnderscore[1], `__${name[0]}`)) {
856
+ return tokens.doubleUnderscore;
857
+ }
858
+ else if (!stream.eol()) {
859
+ // Two underscore symbols at the end can be the beginning of another double underscored Magic Word
860
+ stream.backUp(2);
861
+ }
862
+ }
863
+ return makeStyle(style, state);
864
+ }
865
+ @getTokenizer
866
+ get eatStartTable() {
867
+ return (stream, state) => {
868
+ stream.match(/^(?:\{\||\{\{(?:\{\s*|\s*\()!\s*\}\})\s*/u);
869
+ state.tokenize = this.inTableDefinition();
870
+ return makeLocalTagStyle('tableBracket', state);
871
+ };
872
+ }
873
+ @getTokenizer
874
+ inTableDefinition(tr, quote) {
875
+ const style = quote === undefined
876
+ ? `${tokens.tableDefinition} mw-html-${tr ? 'tr' : 'table'}`
877
+ : tokens.tableDefinitionValue;
878
+ return (stream, state) => {
879
+ if (stream.sol()) {
880
+ state.tokenize = this.inTable;
881
+ return '';
882
+ }
883
+ const t = state.stack[0], equal = getEqual(t);
884
+ if (equal && stream.peek() === '=') {
885
+ pop(state);
886
+ return '';
887
+ }
888
+ else if (stream.match(/^(?:&|\{\{|<(?:!--|\/?[a-z]))/iu, false)) {
889
+ return this.eatWikiText(style)(stream, state);
890
+ }
891
+ else if (quote) { // 有引号的属性值
892
+ if (stream.eat(quote[0])) {
893
+ state.tokenize = this.inTableDefinition(tr, quote[1]);
894
+ }
895
+ else {
896
+ stream.match(getTableDefinitionRegex(equal + quote[0]));
897
+ }
898
+ return makeLocalStyle(style, state);
899
+ }
900
+ else if (quote === '') { // 无引号的属性值
901
+ if (peekSpace(stream)) {
902
+ state.tokenize = this.inTableDefinition(tr);
903
+ return '';
904
+ }
905
+ stream.match(tableDefinitionValueRegex[equal ? 1 : 0]);
906
+ return makeLocalStyle(style, state);
907
+ }
908
+ else if (stream.match(/^=\s*/u)) {
909
+ state.tokenize = this.inTableDefinition(tr, getQuote(stream));
910
+ return makeLocalStyle(style, state);
911
+ }
912
+ stream.match(tableDefinitionRegex);
913
+ return makeLocalStyle(style, state);
914
+ };
915
+ }
916
+ @getTokenizer
917
+ get inTable() {
918
+ return (stream, state) => {
919
+ if (stream.sol()) {
920
+ stream.eatSpace();
921
+ const mt = stream.match(/^(?:\||\{\{\s*!([!)+-])?\s*\}\})/u);
922
+ if (mt) {
923
+ if (mt[1] === '-' || !mt[1] && stream.eat('-')) {
924
+ stream.match(/^-*\s*/u);
925
+ state.tokenize = this.inTableDefinition(true);
926
+ return makeLocalTagStyle('tableDelimiter', state);
927
+ }
928
+ else if (mt[1] === '+' || !mt[1] && stream.match(/^\+\s*/u)) {
929
+ state.tokenize = this.inTableCell(tokens.tableCaption);
930
+ return makeLocalTagStyle('tableDelimiter', state);
931
+ }
932
+ else if (mt[1] === ')' || !mt[1] && stream.eat('}')) {
933
+ pop(state);
934
+ return makeLocalTagStyle('tableBracket', state);
935
+ }
936
+ stream.eatSpace();
937
+ state.tokenize = this.inTableCell(tokens.tableTd, mt[1] !== '!');
938
+ return makeLocalTagStyle('tableDelimiter', state);
939
+ }
940
+ else if (stream.match(/^!\s*/u)) {
941
+ state.tokenize = this.inTableCell(tokens.tableTh);
942
+ return makeLocalTagStyle('tableDelimiter', state);
943
+ }
944
+ else if (isSolSyntax(stream, true)) {
945
+ return this.eatWikiText('error')(stream, state);
946
+ }
947
+ }
948
+ return stream.match(wikiRegex)
949
+ ? makeTagStyle('error', state)
950
+ : this.eatWikiText('error')(stream, state);
951
+ };
952
+ }
953
+ @getTokenizer
954
+ inTableCell(style, needAttr = true, firstLine = true) {
955
+ return (stream, state) => {
956
+ if (stream.sol()) {
957
+ if (stream.match(/^\s*(?:[|!]|\{\{\s*![!)+-]?\s*\}\})/u, false)) {
958
+ state.tokenize = this.inTable;
959
+ return '';
960
+ }
961
+ else if (firstLine) {
962
+ state.tokenize = this.inTableCell(style, false, false);
963
+ return '';
964
+ }
965
+ else if (isSolSyntax(stream, true)) {
966
+ return this.eatWikiText(style)(stream, state);
967
+ }
968
+ }
969
+ if (firstLine) {
970
+ if (stream.match(/^(?:(?:\||\{\{\s*!\s*\}\}){2}|\{\{\s*!!\s*\}\})\s*/u)
971
+ || style === tokens.tableTh && stream.match(/^!!\s*/u)) {
972
+ state.bold = false;
973
+ state.italic = false;
974
+ if (!needAttr) {
975
+ state.tokenize = this.inTableCell(style);
976
+ }
977
+ return makeLocalTagStyle('tableDelimiter', state);
978
+ }
979
+ else if (needAttr && stream.match(/^(?:\||\{\{\s*!\s*\}\})\s*/u)) {
980
+ state.bold = false;
981
+ state.italic = false;
982
+ state.tokenize = this.inTableCell(style, false);
983
+ return makeLocalTagStyle('tableDelimiter2', state);
984
+ }
985
+ else if (needAttr && stream.match('[[', false)) {
986
+ state.tokenize = this.inTableCell(style, false);
987
+ }
988
+ }
989
+ const t = state.stack[0], equal = getEqual(t);
990
+ if (equal && stream.peek() === '=') {
991
+ pop(state);
992
+ return '';
993
+ }
994
+ return stream.match(getTableCellRegex((firstLine ? '|!' : ':') + equal))
995
+ ? makeStyle(style, state)
996
+ : this.eatWikiText(style)(stream, state);
997
+ };
998
+ }
999
+ @getTokenizer
1000
+ inSectionHeader(str) {
1001
+ return (stream, state) => {
1002
+ if (stream.sol()) {
1003
+ pop(state);
1004
+ return '';
1005
+ }
1006
+ else if (stream.match(headerRegex)) {
1007
+ if (stream.eol()) {
1008
+ stream.backUp(str.length);
1009
+ state.tokenize = this.inStr(str, 'sectionHeader');
1010
+ }
1011
+ else if (stream.match(/^<!--(?!.*?-->.*?=)/u, false)) {
1012
+ // T171074: handle trailing comments
1013
+ stream.backUp(str.length);
1014
+ state.tokenize = this.inStr('<!--', false, 'sectionHeader');
1015
+ }
1016
+ return makeLocalTagStyle('section', state);
1017
+ }
1018
+ return this.eatWikiText('section')(stream, state);
1019
+ };
1020
+ }
1021
+ @getTokenizer
1022
+ get inComment() {
1023
+ return this.inStr('-->', 'comment', 'comment');
1024
+ }
1025
+ eatExtTag(tagname, isCloseTag, state) {
1026
+ if (isCloseTag) {
1027
+ chain(state, this.inStr('>', 'error'));
1028
+ return makeLocalTagStyle('error', state);
1029
+ }
1030
+ chain(state, this.eatTagName(tagname));
1031
+ return makeLocalTagStyle('extTagBracket', state);
1032
+ }
1033
+ eatHtmlTag(tagname, isCloseTag, state) {
1034
+ if (isCloseTag) {
1035
+ const { dt, inHtmlTag } = state;
1036
+ if (dt.n && dt.html) {
1037
+ dt.html--;
1038
+ }
1039
+ if (tagname === inHtmlTag[0]) {
1040
+ inHtmlTag.shift();
1041
+ }
1042
+ else {
1043
+ chain(state, this.inStr('>', 'error'));
1044
+ const i = inHtmlTag.lastIndexOf(tagname);
1045
+ if (i !== -1) {
1046
+ inHtmlTag.splice(i, 1);
1047
+ }
1048
+ return makeLocalTagStyle('error', state);
1049
+ }
1050
+ }
1051
+ chain(state, this.eatTagName(tagname, isCloseTag, true));
1052
+ return makeLocalTagStyle('htmlTagBracket', state);
1053
+ }
1054
+ @getTokenizer
1055
+ eatTagName(name, isCloseTag, isHtmlTag) {
1056
+ return (stream, state) => {
1057
+ stream.match(name, true, true);
1058
+ stream.eatSpace();
1059
+ if (isHtmlTag) {
1060
+ state.tokenize = isCloseTag
1061
+ ? this.inStr('>', 'htmlTagBracket')
1062
+ : this.inHtmlTagAttribute(name);
1063
+ return makeLocalTagStyle('htmlTagName', state);
1064
+ }
1065
+ // it is the extension tag
1066
+ state.tokenize = isCloseTag ? this.inStr('>', 'extTagBracket') : this.inExtTagAttribute(name);
1067
+ return makeLocalTagStyle('extTagName', state);
1068
+ };
1069
+ }
1070
+ @getTokenizer
1071
+ inHtmlTagAttribute(name, quote) {
1072
+ const style = quote === undefined
1073
+ ? `${tokens.htmlTagAttribute} mw-html-${name}`
1074
+ : tokens.htmlTagAttributeValue;
1075
+ return (stream, state) => {
1076
+ if (stream.match(new RegExp(`^${lookahead('<', state)}`, 'iu'), false)) {
1077
+ pop(state);
1078
+ return '';
1079
+ }
1080
+ const mt = stream.match(/^\/?>/u);
1081
+ if (mt) {
1082
+ if (!this.voidHtmlTags.has(name) && (mt[0] === '>' || !selfClosingTags.includes(name))) {
1083
+ state.inHtmlTag.unshift(name);
1084
+ state.dt.html++;
1085
+ }
1086
+ pop(state);
1087
+ return makeLocalTagStyle('htmlTagBracket', state);
1088
+ }
1089
+ const t = state.stack[0], pipe = (['inTemplateArgument', 'inParserFunctionArgument', 'inVariable'].includes(t.name) ? '|' : '')
1090
+ + getEqual(t);
1091
+ if (pipe.includes(stream.peek() ?? '')) {
1092
+ pop(state);
1093
+ return makeLocalTagStyle('htmlTagBracket', state);
1094
+ }
1095
+ else if (stream.match(/^(?:[&<]|\{\{)/u, false)) {
1096
+ return this.eatWikiText(style)(stream, state);
1097
+ }
1098
+ else if (quote) { // 有引号的属性值
1099
+ if (stream.eat(quote[0])) {
1100
+ state.tokenize = this.inHtmlTagAttribute(name, quote[1]);
1101
+ }
1102
+ else {
1103
+ stream.match(getHtmlAttrRegex(pipe + quote[0]));
1104
+ }
1105
+ return makeLocalStyle(style, state);
1106
+ }
1107
+ else if (quote === '') { // 无引号的属性值
1108
+ if (peekSpace(stream, true)) {
1109
+ state.tokenize = this.inHtmlTagAttribute(name);
1110
+ return '';
1111
+ }
1112
+ stream.match(getHtmlAttrRegex(String.raw `\s${pipe}`));
1113
+ return makeLocalStyle(style, state);
1114
+ }
1115
+ else if (stream.match(/^=\s*/u)) {
1116
+ state.tokenize = this.inHtmlTagAttribute(name, getQuote(stream));
1117
+ return makeLocalStyle(style, state);
1118
+ }
1119
+ stream.match(getHtmlAttrKeyRegex(pipe));
1120
+ return makeLocalStyle(style, state);
1121
+ };
1122
+ }
1123
+ @getTokenizer
1124
+ inExtTagAttribute(name, quote, isLang, isPage) {
1125
+ const style = `${tokens.extTagAttribute} mw-ext-${name}`;
1126
+ const advance = (stream, state, re) => {
1127
+ const mt = stream.match(re);
1128
+ if (isLang) {
1129
+ switch (mt[0].trim().toLowerCase()) {
1130
+ case 'js':
1131
+ case 'javascript':
1132
+ state.extMode = javascript;
1133
+ break;
1134
+ case 'css':
1135
+ state.extMode = css;
1136
+ break;
1137
+ case 'json':
1138
+ state.extMode = json;
1139
+ // no default
1140
+ }
1141
+ }
1142
+ return makeLocalStyle(tokens.extTagAttributeValue + (isPage ? ` ${tokens.pageName}` : ''), state);
1143
+ };
1144
+ return (stream, state) => {
1145
+ if (stream.eat('>')) {
1146
+ const { config: { tagModes } } = this;
1147
+ state.extName = name;
1148
+ state.extMode ||= name in tagModes
1149
+ && this[tagModes[name]](state.data.tags.filter(tag => tag !== name));
1150
+ if (state.extMode) {
1151
+ state.extState = state.extMode.startState(0);
1152
+ }
1153
+ state.tokenize = this.eatExtTagArea(name);
1154
+ return makeLocalTagStyle('extTagBracket', state);
1155
+ }
1156
+ else if (stream.match('/>')) {
1157
+ state.extMode = false;
1158
+ pop(state);
1159
+ return makeLocalTagStyle('extTagBracket', state);
1160
+ }
1161
+ else if (quote) { // 有引号的属性值
1162
+ if (stream.eat(quote[0])) {
1163
+ const [, remains] = quote;
1164
+ state.tokenize = this.inExtTagAttribute(name, remains, isLang && Boolean(remains), isPage && Boolean(remains));
1165
+ return makeLocalTagStyle('extTagAttributeValue', state);
1166
+ }
1167
+ return advance(stream, state, getExtAttrRegex(quote[0]));
1168
+ }
1169
+ else if (quote === '') { // 无引号的属性值
1170
+ if (peekSpace(stream, true)) {
1171
+ state.tokenize = this.inExtTagAttribute(name);
1172
+ return '';
1173
+ }
1174
+ return advance(stream, state, /^(?:[^>/\s]|\/(?!>))+/u);
1175
+ }
1176
+ else if (stream.match(/^=\s*/u)) {
1177
+ state.tokenize = this.inExtTagAttribute(name, getQuote(stream), isLang, isPage);
1178
+ return makeLocalStyle(style, state);
1179
+ }
1180
+ const mt = stream.match(/^(?:[^>/=]|\/(?!>))+/u);
1181
+ if (stream.peek() === '=') {
1182
+ state.tokenize = this.inExtTagAttribute(name, undefined, syntaxHighlight.has(name) && /(?:^|\s)lang\s*$/iu.test(mt[0]), name === 'templatestyles' && /(?:^|\s)src\s*$/iu.test(mt[0]));
1183
+ }
1184
+ return makeLocalStyle(style, state);
1185
+ };
1186
+ }
1187
+ @getTokenizer
1188
+ eatExtTagArea(name) {
1189
+ return (stream, state) => {
1190
+ const { pos } = stream, i = stream.string.slice(pos).search(getExtTagCloseRegex(name));
1191
+ if (i === 0) {
1192
+ stream.match('</');
1193
+ state.tokenize = this.eatTagName(name, true);
1194
+ state.extName = false;
1195
+ state.extMode = false;
1196
+ state.extState = false;
1197
+ return makeLocalTagStyle('extTagBracket', state);
1198
+ }
1199
+ let origString = '';
1200
+ if (i !== -1) {
1201
+ origString = stream.string;
1202
+ stream.string = origString.slice(0, pos + i);
1203
+ }
1204
+ chain(state, this.inExtTokens(origString));
1205
+ return '';
1206
+ };
1207
+ }
1208
+ @getTokenizer
1209
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this
1210
+ inExtTokens(origString) {
1211
+ return (stream, state) => {
1212
+ let ret;
1213
+ if (state.extMode === false) {
1214
+ ret = `mw-tag-${state.extName} ${tokens.extTag}`;
1215
+ stream.skipToEnd();
1216
+ }
1217
+ else {
1218
+ ret = `mw-tag-${state.extName} ${state.extMode.token(stream, state.extState) ?? ''}`;
1219
+ }
1220
+ if (stream.eol()) {
1221
+ if (origString) {
1222
+ stream.string = origString;
1223
+ }
1224
+ pop(state);
1225
+ }
1226
+ return ret;
1227
+ };
1228
+ }
1229
+ @getTokenizer
1230
+ inVariable(pos = 0) {
1231
+ let tag = 'comment';
1232
+ if (pos === 0) {
1233
+ tag = 'templateVariableName';
1234
+ }
1235
+ else if (pos === 1) {
1236
+ tag = 'templateVariable';
1237
+ }
1238
+ const re = variableRegex[pos === 1 ? 1 : 0];
1239
+ return (stream, state) => {
1240
+ const sol = stream.sol();
1241
+ stream.eatSpace();
1242
+ if (stream.eol()) {
1243
+ return makeLocalStyle('', state);
1244
+ }
1245
+ else if (stream.eat('|')) {
1246
+ if (pos < 2) {
1247
+ state.tokenize = this.inVariable(pos + 1);
1248
+ }
1249
+ return makeLocalTagStyle('templateVariableDelimiter', state);
1250
+ }
1251
+ else if (stream.match(/^\}{2,3}/u)) {
1252
+ pop(state);
1253
+ return makeLocalTagStyle('templateVariableBracket', state, 'nVar');
1254
+ }
1255
+ else if (stream.match('<!--')) {
1256
+ chain(state, this.inComment);
1257
+ return makeLocalTagStyle('comment', state);
1258
+ }
1259
+ else if (pos === 0 && sol) {
1260
+ state.nVar--;
1261
+ pop(state);
1262
+ stream.pos = 0;
1263
+ return '';
1264
+ }
1265
+ return pos === 1 && isSolSyntax(stream) || !stream.match(re)
1266
+ ? this.eatWikiText(tag)(stream, state)
1267
+ : (pos === 1 ? makeTagStyle : makeLocalTagStyle)(tag, state);
1268
+ };
1269
+ }
1270
+ eatTransclusion(stream, state) {
1271
+ const [{ length }] = stream.match(/^\s*/u);
1272
+ // Parser function
1273
+ if (stream.peek() === '#') {
1274
+ stream.backUp(length);
1275
+ state.nExt++;
1276
+ chain(state, this.inParserFunctionName());
1277
+ return makeLocalTagStyle('parserFunctionBracket', state);
1278
+ }
1279
+ // Check for parser function without '#'
1280
+ const name = stream.match(/^([^}<{|::]+)(.?)/u, false);
1281
+ if (name) {
1282
+ const [, f, delimiter] = name, fullWidth = delimiter === ':';
1283
+ let ff = f;
1284
+ if (fullWidth) {
1285
+ ff += ':';
1286
+ }
1287
+ else if (delimiter !== ':') {
1288
+ ff = f.trim();
1289
+ }
1290
+ const ffLower = ff.toLowerCase(), { config: { functionSynonyms, variableIDs, functionHooks } } = this, canonicalName = Object.prototype.hasOwnProperty.call(functionSynonyms[1], ff)
1291
+ && functionSynonyms[1][ff]
1292
+ || Object.prototype.hasOwnProperty.call(functionSynonyms[0], ffLower)
1293
+ && functionSynonyms[0][ffLower];
1294
+ if ((!delimiter || fullWidth || delimiter === ':' || delimiter === '}')
1295
+ && canonicalName
1296
+ && (fullWidth || delimiter === ':' || !variableIDs || variableIDs.includes(canonicalName))
1297
+ && (!fullWidth && delimiter !== ':'
1298
+ || !functionHooks
1299
+ || functionHooks.includes(canonicalName) || otherParserFunctions.has(canonicalName))) {
1300
+ stream.backUp(length);
1301
+ state.nExt++;
1302
+ chain(state, this.inParserFunctionName());
1303
+ return makeLocalTagStyle('parserFunctionBracket', state);
1304
+ }
1305
+ }
1306
+ if (stream.match('}}')) {
1307
+ return undefined;
1308
+ }
1309
+ // Template
1310
+ stream.backUp(length);
1311
+ state.nTemplate++;
1312
+ chain(state, this.inTemplatePageName());
1313
+ return makeLocalTagStyle('templateBracket', state);
1314
+ }
1315
+ @getTokenizer
1316
+ inParserFunctionName(invoke, n, ns, subst) {
1317
+ return (stream, state) => {
1318
+ const sol = stream.sol(), space = stream.eatSpace();
1319
+ if (stream.eol()) {
1320
+ return makeLocalStyle('', state);
1321
+ }
1322
+ else if (stream.eat('}')) {
1323
+ pop(state);
1324
+ return makeLocalTagStyle(stream.eat('}') ? 'parserFunctionBracket' : 'error', state, 'nExt');
1325
+ }
1326
+ else if (stream.match('<!--')) {
1327
+ chain(state, this.inComment);
1328
+ return makeLocalTagStyle('comment', state);
1329
+ }
1330
+ else if (sol) {
1331
+ state.nExt--;
1332
+ pop(state);
1333
+ stream.pos = 0;
1334
+ return '';
1335
+ }
1336
+ const ch = stream.eat(/[::|]/u);
1337
+ if (ch) {
1338
+ state.tokenize = subst && stream.match(/^\s*#/u, false)
1339
+ ? this.inParserFunctionName()
1340
+ : this.inParserFunctionArgument(invoke, n, ns);
1341
+ return makeLocalTagStyle(space || ch === '|' ? 'error' : 'parserFunctionDelimiter', state);
1342
+ }
1343
+ const mt = stream.match(/^(?:[^::}{|<>[\]\s]|\s(?![::]))+/u);
1344
+ if (mt) {
1345
+ const name = mt[0].trim().toLowerCase() + (stream.peek() === ':' ? ':' : ''), { config: { functionSynonyms: [insensitive] } } = this;
1346
+ if (name.startsWith('#')) {
1347
+ switch (insensitive[name] ?? insensitive[name.slice(1)]) {
1348
+ case 'invoke':
1349
+ state.tokenize = this.inParserFunctionName(2);
1350
+ break;
1351
+ case 'widget':
1352
+ state.tokenize = this.inParserFunctionName(1);
1353
+ break;
1354
+ case 'switch':
1355
+ state.tokenize = this.inParserFunctionName(undefined, 1);
1356
+ break;
1357
+ case 'tag':
1358
+ state.tokenize = this.inParserFunctionName(undefined, 2);
1359
+ break;
1360
+ case 'ifexist':
1361
+ case 'lst':
1362
+ case 'lstx':
1363
+ case 'lsth':
1364
+ state.tokenize = this.inParserFunctionName(Infinity);
1365
+ // no default
1366
+ }
1367
+ }
1368
+ else {
1369
+ const canonicalName = insensitive[name];
1370
+ if (pageFunctions.has(canonicalName)) {
1371
+ let namespace = 0;
1372
+ switch (canonicalName) {
1373
+ case 'filepath':
1374
+ namespace = 6;
1375
+ break;
1376
+ case 'int':
1377
+ namespace = 8;
1378
+ break;
1379
+ case 'raw':
1380
+ case 'msg':
1381
+ case 'msgnw':
1382
+ namespace = 10;
1383
+ // no default
1384
+ }
1385
+ state.tokenize = canonicalName === 'subst' || canonicalName === 'safesubst'
1386
+ ? this.inParserFunctionName(invoke, n, ns, true)
1387
+ : this.inParserFunctionName(Infinity, Infinity, namespace);
1388
+ }
1389
+ }
1390
+ return makeLocalTagStyle('parserFunctionName', state);
1391
+ }
1392
+ pop(state);
1393
+ return makeLocalStyle('', state, 'nExt');
1394
+ };
1395
+ }
1396
+ @getTokenizer
1397
+ inTemplatePageName(haveEaten, anchor) {
1398
+ const style = anchor ? tokens.error : `${tokens.templateName} ${tokens.pageName}`, chars = '{}<', re = anchor ? templateRegex : /^(?:&#(?:\d+|x[a-f\d]+);|[^|{}<>[\]#%]|%(?![\da-f]{2}))+/iu;
1399
+ return (stream, state) => {
1400
+ const sol = stream.sol(), space = stream.eatSpace();
1401
+ if (stream.eol()) {
1402
+ return makeLocalStyle('', state);
1403
+ }
1404
+ else if (stream.match('}}')) {
1405
+ pop(state);
1406
+ return makeLocalTagStyle('templateBracket', state, 'nTemplate');
1407
+ }
1408
+ else if (stream.match('<!--')) {
1409
+ chain(state, this.inComment);
1410
+ return makeLocalTagStyle('comment', state);
1411
+ }
1412
+ else if (stream.match(/^<\/?onlyinclude>/u)
1413
+ || stream.match(/^<(?:(?:includeonly|noinclude)(?:\s[^>]*)?\/?>|\/(?:includeonly|noinclude)\s*>)/iu)) {
1414
+ return makeLocalTagStyle('comment', state);
1415
+ }
1416
+ else if (stream.eat('|')) {
1417
+ state.tokenize = this.inTemplateArgument(true);
1418
+ return makeLocalTagStyle('templateDelimiter', state);
1419
+ }
1420
+ else if (haveEaten && sol) {
1421
+ state.nTemplate--;
1422
+ pop(state);
1423
+ stream.pos = 0;
1424
+ return '';
1425
+ }
1426
+ else if (!anchor && stream.eat('#')) {
1427
+ state.tokenize = this.inTemplatePageName(true, true);
1428
+ return makeLocalTagStyle('error', state);
1429
+ }
1430
+ else if (!anchor
1431
+ && stream.match(new RegExp(String.raw `^(?:[>[\]]|%[\da-f]{2}|${lookahead(chars, state)})+`, 'iu'))) {
1432
+ return makeLocalTagStyle('error', state);
1433
+ }
1434
+ else if (!anchor && stream.peek() === '<') {
1435
+ pop(state);
1436
+ return makeLocalStyle('', state, 'nTemplate');
1437
+ }
1438
+ else if (space && !haveEaten) {
1439
+ return makeLocalStyle('', state);
1440
+ }
1441
+ else if (stream.match(re)) {
1442
+ if (!haveEaten) {
1443
+ state.tokenize = this.inTemplatePageName(true, anchor);
1444
+ }
1445
+ return makeLocalStyle(style, state);
1446
+ }
1447
+ return space
1448
+ ? makeLocalStyle(style, state)
1449
+ : this.eatWikiText(style)(stream, state);
1450
+ };
1451
+ }
1452
+ @getTokenizer
1453
+ inParserFunctionArgument(module, n = module ?? Infinity, ns = 0) {
1454
+ if (n === 0) {
1455
+ return this.inTemplateArgument(true, true);
1456
+ }
1457
+ const chars = n === 2 ? '}{<' : "}{<~'_-"; // `#invoke`/`#tag`
1458
+ let style = `${tokens.parserFunction} ${module ? tokens.pageName : ''}`;
1459
+ switch (module) {
1460
+ case 1:
1461
+ style += ' mw-function-274';
1462
+ break;
1463
+ case 2:
1464
+ style += ' mw-function-828';
1465
+ break;
1466
+ case Infinity:
1467
+ style += ` mw-function-${ns}`;
1468
+ // no default
1469
+ }
1470
+ return (stream, state) => {
1471
+ if (stream.eat('|')) {
1472
+ if (module) {
1473
+ state.tokenize = this.inParserFunctionArgument(undefined, module - 1);
1474
+ }
1475
+ else if (n !== Infinity) {
1476
+ state.tokenize = this.inParserFunctionArgument(undefined, n - 1);
1477
+ }
1478
+ return makeLocalTagStyle('parserFunctionDelimiter', state);
1479
+ }
1480
+ else if (stream.match('}}')) {
1481
+ pop(state);
1482
+ return makeLocalTagStyle('parserFunctionBracket', state, 'nExt');
1483
+ }
1484
+ return !isSolSyntax(stream)
1485
+ && stream.match(parserFunctionRegex[module ? 0 : Number(needColon(state)) + 1](chars))
1486
+ ? makeLocalStyle(style, state)
1487
+ : this.eatWikiText('parserFunction')(stream, state);
1488
+ };
1489
+ }
1490
+ @getTokenizer
1491
+ inTemplateArgument(expectName, parserFunction) {
1492
+ const tag = parserFunction ? 'parserFunction' : 'template';
1493
+ return (stream, state) => {
1494
+ const space = stream.eatSpace();
1495
+ if (stream.eol()) {
1496
+ return makeLocalTagStyle(tag, state);
1497
+ }
1498
+ else if (stream.eat('|')) {
1499
+ if (!expectName) {
1500
+ state.tokenize = this.inTemplateArgument(true, parserFunction);
1501
+ }
1502
+ return makeLocalTagStyle(parserFunction ? 'parserFunctionDelimiter' : 'templateDelimiter', state);
1503
+ }
1504
+ else if (stream.match('}}', false)) {
1505
+ if (space) {
1506
+ return makeLocalTagStyle(tag, state);
1507
+ }
1508
+ stream.pos += 2;
1509
+ pop(state);
1510
+ return makeLocalTagStyle(parserFunction ? 'parserFunctionBracket' : 'templateBracket', state, parserFunction ? 'nExt' : 'nTemplate');
1511
+ }
1512
+ else if (stream.sol() && stream.peek() === '=') {
1513
+ const style = this.eatWikiText(tag)(stream, state);
1514
+ if (style.includes(tokens.sectionHeader)) {
1515
+ return style;
1516
+ }
1517
+ stream.pos = 0;
1518
+ }
1519
+ if (expectName
1520
+ && stream.match(new RegExp(`^(?:[^=|}{[<]|${lookahead('}{[<', state)})*=`, 'iu'))) {
1521
+ state.tokenize = this.inTemplateArgument(false, parserFunction);
1522
+ return makeLocalTagStyle('templateArgumentName', state);
1523
+ }
1524
+ else if (isSolSyntax(stream) && stream.peek() !== '=') {
1525
+ return this.eatWikiText(tag)(stream, state);
1526
+ }
1527
+ return stream.match(needColon(state) ? argumentRegex : styleRegex) || space
1528
+ ? makeLocalTagStyle(tag, state)
1529
+ : this.eatWikiText(tag)(stream, state);
1530
+ };
1531
+ }
1532
+ @getTokenizer
1533
+ inConvert(style, needFlag, needLang = true, plain) {
1534
+ return (stream, state) => {
1535
+ const space = stream.eatSpace();
1536
+ if (stream.match('}-')) {
1537
+ pop(state);
1538
+ return makeLocalTagStyle('convertBracket', state);
1539
+ }
1540
+ else if (needFlag && stream.match(/^[;\sa-z-]*(?=\|)/iu)) {
1541
+ chain(state, this.inConvert(style, false, true, plain));
1542
+ state.tokenize = this.inStr('|', 'convertDelimiter');
1543
+ return makeLocalTagStyle('convertFlag', state);
1544
+ }
1545
+ else if (stream.match(this.convertSemicolon)) {
1546
+ if (needFlag || !needLang) {
1547
+ state.tokenize = this.inConvert(style, false, true, plain);
1548
+ }
1549
+ return makeLocalTagStyle('convertDelimiter', state);
1550
+ }
1551
+ else if (needLang && stream.match(this.convertLang)) {
1552
+ state.tokenize = this.inConvert(style, false, false, plain);
1553
+ return makeLocalTagStyle('convertLang', state);
1554
+ }
1555
+ else if (plain) {
1556
+ if (stream.match('-{', false)) {
1557
+ return this.eatWikiText(style)(stream, state);
1558
+ }
1559
+ stream.match(/^(?:(?:[^};=-]|\}(?!-)|=(?!>)|-(?!\{))+|;|=>)/u);
1560
+ return makeStyle(style, state);
1561
+ }
1562
+ return !isSolSyntax(stream, true) && stream.match(this.convertRegex) || space
1563
+ ? makeStyle(style, state)
1564
+ : this.eatWikiText(style)(stream, state);
1565
+ };
1566
+ }
1567
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this
1568
+ eatEntity(stream, style) {
1569
+ const entity = stream.match(/^(?:#x[a-f\d]+|#\d+|[a-z\d]+);/iu);
1570
+ return entity && isHtmlEntity(entity[0]) ? tokens.htmlEntity : style;
1571
+ }
1572
+ /**
1573
+ * main entry
1574
+ *
1575
+ * @see https://codemirror.net/docs/ref/#language.StreamParser
1576
+ *
1577
+ * @param tags
1578
+ */
1579
+ mediawiki(tags) {
1580
+ return {
1581
+ startState: () => startState(this.eatWikiText(''), tags ?? this.tags, tags === undefined),
1582
+ copyState,
1583
+ token(stream, state) {
1584
+ const { data } = state, { readyTokens } = data;
1585
+ let { oldToken } = data;
1586
+ while (oldToken
1587
+ && (
1588
+ // 如果 PartialParse 的起点位于当前位置之后
1589
+ stream.pos > oldToken.pos
1590
+ || stream.pos === oldToken.pos && state.tokenize !== oldToken.state.tokenize)) {
1591
+ oldToken = readyTokens.shift();
1592
+ }
1593
+ if (
1594
+ // 检查起点
1595
+ stream.pos === oldToken?.pos
1596
+ && stream.string === oldToken.string
1597
+ && cmpNesting(state, oldToken.state)) {
1598
+ const { pos, string, state: { bold, italic, ...other }, style } = readyTokens[0];
1599
+ Object.assign(state, other);
1600
+ if (!(state.extName && state.extMode)
1601
+ && state.nLink === 0
1602
+ && typeof style === 'string'
1603
+ && style.includes(tokens.apostrophes)) {
1604
+ if (data.mark === pos) {
1605
+ // rollback
1606
+ data.mark = null;
1607
+ // add one apostrophe, next token will be italic (two apostrophes)
1608
+ stream.string = string.slice(0, pos - 2);
1609
+ const s = state.tokenize(stream, state);
1610
+ stream.string = string;
1611
+ oldToken.pos++;
1612
+ data.oldToken = oldToken;
1613
+ return makeFullStyle(s, state);
1614
+ }
1615
+ const length = pos - stream.pos;
1616
+ if (length !== 3) {
1617
+ state.italic = !state.italic;
1618
+ }
1619
+ if (length !== 2) {
1620
+ state.bold = !state.bold;
1621
+ }
1622
+ }
1623
+ else if (typeof style === 'string' && style.includes(tokens.tableDelimiter)) {
1624
+ state.bold = false;
1625
+ state.italic = false;
1626
+ }
1627
+ // return first saved token
1628
+ data.oldToken = readyTokens.shift();
1629
+ stream.pos = pos;
1630
+ stream.string = string;
1631
+ return makeFullStyle(style, state);
1632
+ }
1633
+ else if (stream.sol()) {
1634
+ // reset bold and italic status in every new line
1635
+ state.bold = false;
1636
+ state.italic = false;
1637
+ state.dt.n = 0;
1638
+ state.dt.html = 0;
1639
+ data.firstSingleLetterWord = null;
1640
+ data.firstMultiLetterWord = null;
1641
+ data.firstSpace = null;
1642
+ if (state.tokenize.name === 'inExtTokens') {
1643
+ pop(state); // dispose inExtTokens
1644
+ pop(state); // dispose eatExtTagArea
1645
+ state.extName = false;
1646
+ state.extMode = false;
1647
+ state.extState = false;
1648
+ }
1649
+ }
1650
+ readyTokens.length = 0;
1651
+ data.mark = null;
1652
+ data.oldToken = { pos: stream.pos, string: stream.string, state: copyState(state), style: '' };
1653
+ const { start } = stream;
1654
+ do {
1655
+ // get token style
1656
+ stream.start = stream.pos;
1657
+ const char = stream.peek(), style = state.tokenize(stream, state);
1658
+ if (typeof style === 'string' && style.includes(tokens.templateArgumentName)) {
1659
+ for (let i = readyTokens.length - 1; i >= 0; i--) {
1660
+ const token = readyTokens[i];
1661
+ if (cmpNesting(state, token.state, true)) {
1662
+ const types = typeof token.style === 'string' && token.style.split(' '), j = types && types.indexOf(tokens.template);
1663
+ if (j !== false && j !== -1) {
1664
+ types[j] = tokens.templateArgumentName;
1665
+ token.style = types.join(' ');
1666
+ }
1667
+ else if (types && types.includes(tokens.templateDelimiter)) {
1668
+ break;
1669
+ }
1670
+ }
1671
+ }
1672
+ }
1673
+ else if (typeof style === 'string' && style.includes(tokens.tableDelimiter2)) {
1674
+ for (let i = readyTokens.length - 1; i >= 0; i--) {
1675
+ const token = readyTokens[i];
1676
+ if (cmpNesting(state, token.state, true)) {
1677
+ const { style: s } = token, local = typeof s === 'string', type = !local
1678
+ && s[0].split(' ')
1679
+ .find(t => t && !t.endsWith('-ground'));
1680
+ if (type && type.startsWith('mw-table-')) {
1681
+ token.style = `${s[0].replace('mw-table-', 'mw-html-')} ${tokens.tableDefinition}`;
1682
+ }
1683
+ else if (local && s.includes(tokens.tableDelimiter)) {
1684
+ break;
1685
+ }
1686
+ }
1687
+ }
1688
+ }
1689
+ else if (char === '|' && typeof style === 'string' && style.includes(tokens.convertDelimiter)) {
1690
+ let count = 0;
1691
+ for (let i = readyTokens.length - 1; i >= 0; i--) {
1692
+ const token = readyTokens[i];
1693
+ if (cmpNesting(state, token.state, true)) {
1694
+ const { style: s } = token;
1695
+ if (typeof s === 'string' && s.includes(tokens.convertBracket)) {
1696
+ count += token.char === '-' ? 1 : -1;
1697
+ if (count === 1) {
1698
+ break;
1699
+ }
1700
+ }
1701
+ else if (typeof s === 'object') {
1702
+ token.style = s[0]
1703
+ + (s[0].includes(tokens.convertFlag) ? '' : ` ${tokens.convertFlag}`);
1704
+ }
1705
+ }
1706
+ }
1707
+ }
1708
+ // save token
1709
+ readyTokens.push({ pos: stream.pos, char, string: stream.string, state: copyState(state), style });
1710
+ } while ( /** @todo should end at table delimiter as well */!stream.eol());
1711
+ if (!state.bold || !state.italic) {
1712
+ // no need to rollback
1713
+ data.mark = null;
1714
+ }
1715
+ stream.start = start;
1716
+ stream.pos = data.oldToken.pos;
1717
+ stream.string = data.oldToken.string;
1718
+ Object.assign(state, data.oldToken.state);
1719
+ return '';
1720
+ },
1721
+ blankLine(state) {
1722
+ if (state.extName && state.extMode && state.extMode.blankLine) {
1723
+ state.extMode.blankLine(state.extState, 0);
1724
+ }
1725
+ },
1726
+ indent(state, textAfter, context) {
1727
+ return state.extName && state.extMode && state.extMode.indent
1728
+ ? state.extMode.indent(state.extState, textAfter, context)
1729
+ : null;
1730
+ },
1731
+ ...tags
1732
+ ? undefined
1733
+ : {
1734
+ tokenTable: {
1735
+ ...this.tokenTable,
1736
+ ...this.hiddenTable,
1737
+ '': Tag.define(),
1738
+ },
1739
+ },
1740
+ };
1741
+ }
1742
+ 'text/mediawiki'(tags) {
1743
+ return this.mediawiki(tags);
1744
+ }
1745
+ 'text/nowiki'() {
1746
+ return {
1747
+ startState() {
1748
+ return {};
1749
+ },
1750
+ token: (stream) => {
1751
+ if (stream.eatWhile(/[^&]/u)) {
1752
+ return '';
1753
+ }
1754
+ // eat &
1755
+ stream.next();
1756
+ return this.eatEntity(stream, '');
1757
+ },
1758
+ };
1759
+ }
1760
+ @getTokenizer
1761
+ inPre(begin) {
1762
+ return (stream, state) => {
1763
+ if (stream.match(begin ? /^<\/nowiki>/iu : /^<nowiki>/iu)) {
1764
+ state.tokenize = this.inPre(!begin);
1765
+ return tokens.comment;
1766
+ }
1767
+ else if (this.hasVariants && stream.match('-{')) {
1768
+ chain(state, this.inConvert('', true, true, true));
1769
+ return tokens.convertBracket;
1770
+ }
1771
+ else if (stream.eat('&')) {
1772
+ return this.eatEntity(stream, '');
1773
+ }
1774
+ stream.match(this.preRegex[begin ? 1 : 0]);
1775
+ return '';
1776
+ };
1777
+ }
1778
+ 'text/pre'() {
1779
+ return {
1780
+ startState: () => startState(this.inPre(), []),
1781
+ token: simpleToken,
1782
+ };
1783
+ }
1784
+ @getTokenizer
1785
+ inNested(tag) {
1786
+ const re = tag === 'ref' ? /^(?:\{|(?:[^<{]|\{(?!\{)|<(?!!--|ref(?:[\s/>]|$)))+)/iu : getNestedRegex(tag);
1787
+ return (stream, state) => {
1788
+ if (tag === 'ref') {
1789
+ if (stream.match('<!--')) {
1790
+ chain(state, this.inComment);
1791
+ return makeLocalTagStyle('comment', state);
1792
+ }
1793
+ else if (stream.match(/^\{{3}(?!\{|[^{}]*\}\}(?!\}))\s*/u)) {
1794
+ chain(state, this.inVariable());
1795
+ return tokens.templateVariableBracket;
1796
+ }
1797
+ const mt = stream.match(/^\{\{(?!\{(?!\{))/u);
1798
+ if (mt) {
1799
+ return this.eatTransclusion(stream, state) ?? tokens.comment;
1800
+ }
1801
+ }
1802
+ if (stream.match(re)) {
1803
+ return tokens.comment;
1804
+ }
1805
+ stream.eat('<');
1806
+ chain(state, this.eatTagName(tag));
1807
+ return tokens.extTagBracket;
1808
+ };
1809
+ }
1810
+ 'text/references'(tags) {
1811
+ return {
1812
+ startState: () => startState(this.inNested('ref'), tags),
1813
+ token: simpleToken,
1814
+ };
1815
+ }
1816
+ 'text/choose'(tags) {
1817
+ return {
1818
+ startState: () => startState(this.inNested('option'), tags),
1819
+ token: simpleToken,
1820
+ };
1821
+ }
1822
+ 'text/combobox'(tags) {
1823
+ return {
1824
+ startState: () => startState(this.inNested('combooption'), tags),
1825
+ token: simpleToken,
1826
+ };
1827
+ }
1828
+ @getTokenizer
1829
+ get inInputbox() {
1830
+ return (stream, state) => {
1831
+ if (stream.match('<!--')) {
1832
+ chain(state, this.inComment);
1833
+ return tokens.comment;
1834
+ }
1835
+ /** @todo braces should also be parsed */
1836
+ stream.match(/^(?:[^<]|<(?!!--))+/u);
1837
+ return '';
1838
+ };
1839
+ }
1840
+ 'text/inputbox'() {
1841
+ return {
1842
+ startState: () => startState(this.inInputbox, []),
1843
+ token: simpleToken,
1844
+ };
1845
+ }
1846
+ @getTokenizer
1847
+ inGallery(section) {
1848
+ const style = section ? tokens.error : `${tokens.linkPageName} ${tokens.pageName}`, regex = section ? /^(?:[[}\]]|\{(?!\{))+/u : /^(?:[>[}\]]|\{(?!\{)|<(?!!--))+/u, re = section ? /^(?:[^|<[\]{}]|<(?!!--))+/u : /^(?:&#(?:\d+|x[a-f\d]+);|[^#|<>[\]{}])+/u;
1849
+ return (stream, state) => {
1850
+ const space = stream.eatSpace();
1851
+ if (!section && stream.match(/^#\s*/u)) {
1852
+ state.tokenize = this.inGallery(true);
1853
+ return makeTagStyle('error', state);
1854
+ }
1855
+ else if (stream.match(/^\|\s*/u)) {
1856
+ state.tokenize = this.inLinkText(true, true);
1857
+ this.toEatImageParameter(stream, state);
1858
+ return makeLocalTagStyle('fileDelimiter', state);
1859
+ }
1860
+ else if (stream.match(regex)) {
1861
+ return makeTagStyle('error', state);
1862
+ }
1863
+ return stream.match(re) || space
1864
+ ? makeStyle(style, state)
1865
+ : this.eatWikiText(section ? style : 'error')(stream, state);
1866
+ };
1867
+ }
1868
+ 'text/gallery'(tags) {
1869
+ return {
1870
+ startState: () => startState(this.inGallery(), tags),
1871
+ token: (stream, state) => {
1872
+ if (stream.sol()) {
1873
+ Object.assign(state, startState(this.inGallery(), state.data.tags));
1874
+ }
1875
+ return simpleToken(stream, state);
1876
+ },
1877
+ };
1878
+ }
1879
+ javascript() {
1880
+ return javascript;
1881
+ }
1882
+ css() {
1883
+ return css;
1884
+ }
1885
+ json() {
1886
+ return json;
1887
+ }
1888
+ }