@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/README.md +58 -11
- package/dist/bidi.js +84 -0
- package/dist/codemirror.d.ts +2 -0
- package/dist/codemirror.js +608 -0
- package/dist/color.js +52 -0
- package/dist/config.js +119 -0
- package/dist/css.js +30 -0
- package/dist/escape.js +35 -0
- package/dist/fold.d.ts +0 -1
- package/dist/fold.js +445 -0
- package/dist/hover.js +52 -0
- package/dist/indent.js +50 -0
- package/dist/inlay.js +68 -0
- package/dist/javascript.js +5 -0
- package/dist/keybindings.js +35 -0
- package/dist/keymap.js +36 -0
- package/dist/linter.d.ts +9 -8
- package/dist/linter.js +134 -0
- package/dist/lua.js +428 -0
- package/dist/main.min.js +25 -29
- package/dist/matchBrackets.d.ts +7 -0
- package/dist/matchBrackets.js +58 -0
- package/dist/matchTag.js +139 -0
- package/dist/mediawiki.js +443 -0
- package/dist/mw.min.js +31 -35
- package/dist/{mwConfig.mjs → mwConfig.js} +6 -10
- package/dist/openLinks.js +97 -0
- package/dist/ref.js +85 -0
- package/dist/signature.js +69 -0
- package/dist/static.js +46 -0
- package/dist/statusBar.js +138 -0
- package/dist/token.js +1888 -0
- package/dist/wiki.min.js +33 -37
- package/i18n/en.json +1 -1
- package/i18n/zh-hans.json +1 -1
- package/i18n/zh-hant.json +1 -1
- package/mediawiki.css +1 -1
- package/package.json +19 -18
- package/dist/keybindings.d.mts +0 -15
- package/dist/keybindings.mjs +0 -34
- package/dist/linter.d.mts +0 -43
- package/dist/linter.mjs +0 -132
- /package/dist/{mwConfig.d.mts → mwConfig.d.ts} +0 -0
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
|
+
}
|