wikiplus-highlight 2.60.3 → 3.0.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/main.js DELETED
@@ -1,1006 +0,0 @@
1
- /**
2
- * @name Wikiplus-highlight Wikiplus编辑器的CodeMirror语法高亮扩展
3
- * @author Bhsd <https://github.com/bhsd-harry>
4
- * @author 机智的小鱼君 <https://github.com/Dragon-Fish>
5
- * @license GPL-3.0
6
- */
7
-
8
- (async () => {
9
- 'use strict';
10
-
11
- if (mw.libs.wphl && mw.libs.wphl.version) {
12
- return;
13
- }
14
- mw.libs.wphl = mw.libs.wphl || {}; // 开始加载
15
-
16
- const version = '2.60.3',
17
- newAddon = 0;
18
-
19
- /** @type {typeof mw.storage} */
20
- const storage = typeof mw.storage === 'object' && typeof mw.storage.getObject === 'function'
21
- ? mw.storage
22
- : {
23
- /** @override */
24
- getObject(key) {
25
- const json = localStorage.getItem(key);
26
- if (json === false) {
27
- return false;
28
- }
29
- try {
30
- return JSON.parse(json);
31
- } catch (e) {
32
- return null;
33
- }
34
- },
35
- /** @override */
36
- setObject(key, value) {
37
- try {
38
- return localStorage.setItem(key, JSON.stringify(value));
39
- } catch (e) {
40
- return false;
41
- }
42
- },
43
- };
44
-
45
- Object.fromEntries = Object.fromEntries || (entries => {
46
- const /** @type {Record<string, T>} */ obj = {};
47
- for (const [key, value] of entries) {
48
- obj[key] = value;
49
- }
50
- return obj;
51
- });
52
-
53
- /**
54
- * 解析版本号
55
- * @param {string} str 版本号
56
- */
57
- const getVersion = (str = version) => str.split('.').map(Number);
58
-
59
- /**
60
- * 比较版本号
61
- * @param {string} a 版本号1
62
- * @param {string} b 版本号2
63
- * @returns {boolean} `a`的版本号是否小于`b`的版本号
64
- */
65
- const cmpVersion = (a, b) => {
66
- const [a0, a1] = getVersion(a),
67
- [b0, b1] = getVersion(b);
68
- return a0 < b0 || a0 === b0 && a1 < b1;
69
- };
70
-
71
- /**
72
- * 获取I18N消息
73
- * @param {string} key 消息键,省略`wphl-`前缀
74
- * @param {string[]} args 替换`$1`等的参数
75
- */
76
- const msg = (key, ...args) => mw.msg(`wphl-${key}`, ...args);
77
-
78
- /**
79
- * 生成JQuery的I18N消息
80
- * @param {string[]} args 替换`$1`等的参数
81
- */
82
- const htmlMsg = (...args) => $($.parseHTML(msg(...args)));
83
-
84
- /**
85
- * 提示消息
86
- * @param {string[]} args 替换`$1`等的参数
87
- */
88
- const notify = (...args) => () => {
89
- const $p = $('<p>', {html: msg(...args)});
90
- mw.notify($p, {type: 'success', autoHideSeconds: 'long', tag: 'wikiplus-highlight'});
91
- return $p;
92
- };
93
-
94
- // 插件和I18N依主版本号
95
- const majorVersion = getVersion().slice(0, 2).join('.');
96
-
97
- // 路径
98
- const CDN = '//testingcf.jsdelivr.net',
99
- CM_CDN = 'npm/codemirror@5.65.3',
100
- MW_CDN = 'npm/@bhsd/codemirror-mediawiki@1.1.11',
101
- PARSER_CDN = 'npm/wikiparser-node@1.1.1-b',
102
- REPO_CDN = `npm/wikiplus-highlight@${majorVersion}`;
103
-
104
- const {config: {values: {
105
- wgPageName: page,
106
- wgNamespaceNumber: ns,
107
- wgPageContentModel: contentmodel,
108
- wgServerName: server,
109
- wgScriptPath: scriptPath,
110
- wgUserLanguage: userLang,
111
- skin,
112
- }}} = mw;
113
-
114
- // 和本地缓存有关的常数
115
- const USING_LOCAL = mw.loader.getState('ext.CodeMirror') !== null,
116
- DATA_MODULE = mw.loader.getState('ext.CodeMirror.data') ? 'ext.CodeMirror.data' : 'ext.CodeMirror',
117
- /** @type {Record<string, {time: number, config: mwConfig}>} */
118
- ALL_SETTINGS_CACHE = storage.getObject('InPageEditMwConfig') || {},
119
- SITE_ID = `${server}${scriptPath}`,
120
- /** @type {{time: number, config: mwConfig}} */ SITE_SETTINGS = ALL_SETTINGS_CACHE[SITE_ID] || {},
121
- EXPIRED = !(SITE_SETTINGS.time > Date.now() - 86400 * 1000 * 30);
122
-
123
- const /** @type {Record<string, string>} */ CONTENTMODEL = {
124
- css: 'css',
125
- 'sanitized-css': 'css',
126
- javascript: 'javascript',
127
- json: 'javascript',
128
- wikitext: 'mediawiki',
129
- };
130
-
131
- const MODE_LIST = USING_LOCAL
132
- ? {
133
- lib: 'ext.CodeMirror.lib',
134
- css: 'ext.CodeMirror.lib.mode.css',
135
- javascript: 'ext.CodeMirror.lib.mode.javascript',
136
- lua: `${CM_CDN}/mode/lua/lua.min.js`,
137
- mediawiki: EXPIRED ? DATA_MODULE : [],
138
- htmlmixed: 'ext.CodeMirror.lib.mode.htmlmixed',
139
- xml: [],
140
- }
141
- : {
142
- lib: `${CM_CDN}/lib/codemirror.min.js`,
143
- css: `${CM_CDN}/mode/css/css.min.js`,
144
- javascript: `${CM_CDN}/mode/javascript/javascript.min.js`,
145
- lua: `${CM_CDN}/mode/lua/lua.min.js`,
146
- mediawiki: [],
147
- htmlmixed: `${CM_CDN}/mode/htmlmixed/htmlmixed.min.js`,
148
- xml: `${CM_CDN}/mode/xml/xml.min.js`,
149
- };
150
-
151
- const ADDON_LIST = {
152
- searchcursor: `${CM_CDN}/addon/search/searchcursor.min.js`,
153
- search: `${REPO_CDN}/search.min.js`,
154
- markSelection: `${CM_CDN}/addon/selection/mark-selection.min.js`,
155
- activeLine: `${CM_CDN}/addon/selection/active-line.min.js`,
156
- trailingspace: `${CM_CDN}/addon/edit/trailingspace.min.js`,
157
- matchBrackets: `${CM_CDN}/addon/edit/matchbrackets.min.js`,
158
- closeBrackets: `${CM_CDN}/addon/edit/closebrackets.min.js`,
159
- matchTags: `${REPO_CDN}/matchtags.min.js`,
160
- fold: `${REPO_CDN}/fold.min.js`,
161
- wikiEditor: 'ext.wikiEditor',
162
- contextmenu: 'mediawiki.Title',
163
- lint: `${CM_CDN}/addon/lint/lint.min.js`,
164
- annotateScrollbar: `${CM_CDN}/addon/scroll/annotatescrollbar.min.js`,
165
- parser: `${PARSER_CDN}/extensions/dist/base.min.js`,
166
- linter: `${PARSER_CDN}/extensions/dist/lint.min.js`,
167
- lintWikitext: `${REPO_CDN}/lint.min.js`,
168
- };
169
-
170
- const /** @type {addon[]} */ options = [
171
- {
172
- option: 'styleSelectedText',
173
- addon: 'search',
174
- download: 'markSelection',
175
- only: true,
176
- /** @implements */ complex: () => !addons.has('wikiEditor'),
177
- },
178
- {option: 'styleActiveLine', addon: 'activeLine'},
179
- {option: 'showTrailingSpace', addon: 'trailingspace'},
180
- {
181
- option: 'matchBrackets',
182
- /** @implements */ complex: (mode, json) => mode === 'mediawiki' || json
183
- ? {bracketRegex: /[{}[\]]/u}
184
- : true,
185
- },
186
- {
187
- option: 'autoCloseBrackets',
188
- addon: 'closeBrackets',
189
- /** @implements */ complex: (mode, json) => mode === 'mediawiki' || json
190
- ? '()[]{}""'
191
- : true,
192
- },
193
- {option: 'matchTags', addon: ['matchTags', 'fold'], modes: new Set(['mediawiki', 'widget'])},
194
- {option: 'fold', modes: new Set(['mediawiki', 'widget'])},
195
- ];
196
-
197
- const defaultAddons = ['search'],
198
- defaultIndent = 4,
199
- /** @type {Set<string>} */ addons = new Set(storage.getObject('Wikiplus-highlight-addons') || defaultAddons);
200
- let /** @type {number} */ indent = storage.getObject('Wikiplus-highlight-indent') || defaultIndent;
201
-
202
- /** @type {Record<string, string>} */
203
- const entity = {'"': 'quot', "'": 'apos', '<': 'lt', '>': 'gt', '&': 'amp', ' ': 'nbsp'},
204
- /** @type {(func: (str: string) => string) => (doc: CodeMirror.Editor) => void} */
205
- convert = func => doc => {
206
- doc.replaceSelections(
207
- doc.getSelections().map(selection => selection.split('\n').map(func).join('\n')),
208
- 'around',
209
- );
210
- },
211
- escapeHTML = convert(str => [...str].map(c => {
212
- if (c in entity) {
213
- return `&${entity[c]};`;
214
- }
215
- const code = c.codePointAt();
216
- return code < 256 ? `&#${code};` : `&#x${code.toString(16)};`;
217
- }).join('')),
218
- escapeURI = convert(str => {
219
- if (str.includes('%')) {
220
- try {
221
- return decodeURIComponent(str);
222
- } catch (e) {}
223
- }
224
- return encodeURIComponent(str);
225
- }),
226
- escapeHash = convert(str => {
227
- try {
228
- return decodeURIComponent(str.replace(/\.([\da-f]{2})/giu, '%$1'));
229
- } catch (e) {
230
- return str;
231
- }
232
- }),
233
- /** @type {(cm: typeof CodeMirror) => boolean} */ isPc = ({keyMap}) => keyMap.default === keyMap.pcDefault,
234
- /** @type {(cm: typeof CodeMirror) => Record<string, (doc: CodeMirror.Editor) => void} */ extraKeys = CM => {
235
- const ctrl = isPc(CM) ? 'Ctrl' : 'Cmd';
236
- return {[`${ctrl}-/`]: escapeHTML, [`${ctrl}-\\`]: escapeURI, [`Shift-${ctrl}-\\`]: escapeHash};
237
- };
238
-
239
- /**
240
- * contextMenu插件
241
- * @param {CodeMirror.Editor} doc CodeMirror编辑区
242
- * @param {string} mode 高亮模式
243
- */
244
- const handleContextMenu = (doc, mode) => {
245
- if (mode !== 'mediawiki' && mode !== 'widget' || !addons.has('contextmenu')) {
246
- return;
247
- }
248
- const $wrapper = $(doc.getWrapperElement()).addClass('CodeMirror-contextmenu'),
249
- {functionSynonyms: [synonyms]} = mw.config.get('extCodeMirrorConfig') || {
250
- functionSynonyms: [{invoke: 'invoke', 调用: 'invoke', widget: 'widget', 小工具: 'widget'}],
251
- };
252
-
253
- /**
254
- * 生成别名映射表
255
- * @param {string} str 别名
256
- */
257
- const getSysnonyms = str => new Set(Object.keys(synonyms).filter(key => synonyms[key] === str)
258
- .map(key => key.startsWith('#') ? key : `#${key}`));
259
- const invoke = getSysnonyms('invoke'),
260
- widget = getSysnonyms('widget');
261
-
262
- $wrapper.contextmenu(({pageX, pageY}) => {
263
- const pos = doc.coordsChar({left: pageX, top: pageY}),
264
- {line, ch} = pos,
265
- curType = doc.getTokenTypeAt(pos);
266
- if (!/\bmw-(?:template-name|parserfunction)\b/u.test(curType)) {
267
- return undefined;
268
- }
269
- const tokens = doc.getLineTokens(line);
270
- for (let i = tokens.length - 1; i > 0; i--) {
271
- const {type, end, string} = tokens[i];
272
- if (tokens[i - 1].type === type) {
273
- tokens[i - 1].end = end;
274
- tokens[i - 1].string += string;
275
- tokens.splice(i, 1);
276
- }
277
- }
278
- const index = tokens.findIndex(({start, end}) => start < ch && end >= ch),
279
- text = tokens[index].string.replace(/\u200E/gu, '').replace(/_/gu, ' ').trim();
280
- if (/\bmw-template-name\b/u.test(curType)) {
281
- const title = new mw.Title(text);
282
- if (title.namespace !== 0 || text.startsWith(':')) {
283
- open(title.getUrl(), '_blank');
284
- } else {
285
- open(mw.util.getUrl(`Template:${text}`), '_blank');
286
- }
287
- return false;
288
- } else if (index < 2 || !/\bmw-parserfunction-delimiter\b/u.test(tokens[index - 1].type)
289
- || !/\bmw-parserfunction-name\b/u.test(tokens[index - 2].type)
290
- ) {
291
- return undefined;
292
- }
293
- const parserFunction = tokens[index - 2].string.trim().toLowerCase();
294
- if (invoke.has(parserFunction)) {
295
- open(mw.util.getUrl(`Module:${text}`), '_blank');
296
- } else if (widget.has(parserFunction)) {
297
- open(mw.util.getUrl(`Widget:${text}`, {action: 'edit'}), '_blank');
298
- } else {
299
- return undefined;
300
- }
301
- return false;
302
- });
303
- };
304
-
305
- const /** @type {Record<string, string>} */ i18n = storage.getObject('Wikiplus-highlight-i18n') || {};
306
- let /** @type {() => JQuery<HTMLElement>} */ welcome;
307
- if (!i18n['wphl-version']) { // 首次安装
308
- welcome = notify('welcome');
309
- } else if (cmpVersion(i18n['wphl-version'], version)) { // 更新版本
310
- welcome = notify(`welcome-${newAddon ? 'new-addon' : 'upgrade'}`, version, newAddon);
311
- }
312
-
313
- const /** @type {Record<string, string>} */ i18nLanguages = {
314
- zh: 'zh-hans',
315
- 'zh-hans': 'zh-hans',
316
- 'zh-cn': 'zh-hans',
317
- 'zh-my': 'zh-hans',
318
- 'zh-sg': 'zh-hans',
319
- 'zh-hant': 'zh-hant',
320
- 'zh-tw': 'zh-hant',
321
- 'zh-hk': 'zh-hant',
322
- 'zh-mo': 'zh-hant',
323
- ka: 'ka',
324
- },
325
- i18nLang = i18nLanguages[userLang] || 'en',
326
- I18N_CDN = `${CDN}/${REPO_CDN}/i18n/${i18nLang}.json`,
327
- isLatest = i18n['wphl-version'] === majorVersion;
328
-
329
- /** 加载 I18N */
330
- const setI18N = async () => {
331
- if (!isLatest || i18n['wphl-lang'] !== i18nLang) {
332
- Object.assign(i18n, await $.ajax(`${I18N_CDN}`, { // eslint-disable-line require-atomic-updates
333
- dataType: 'json',
334
- cache: true,
335
- }));
336
- storage.setObject('Wikiplus-highlight-i18n', i18n);
337
- }
338
- mw.messages.set(i18n);
339
- };
340
-
341
- const i18nPromise = Promise.all([ // 提前加载I18N
342
- mw.loader.using('mediawiki.util'),
343
- setI18N(),
344
- ]);
345
-
346
- /**
347
- * 下载MW扩展脚本
348
- * @param {string[]} exts CodeMirror扩展模块
349
- */
350
- const getInternalScript = exts => exts.length > 0 ? mw.loader.using(exts) : Promise.resolve();
351
-
352
- /**
353
- * 下载外部脚本
354
- * @param {string[]} urls CodeMirror脚本网址
355
- */
356
- const getExternalScript = urls => urls.length > 0
357
- ? $.ajax(`${CDN}/${urls.length > 1 ? 'combine/' : ''}${urls.join()}`, {dataType: 'script', cache: true})
358
- : Promise.resolve();
359
-
360
- /**
361
- * 下载脚本
362
- * @param {string[]} urls 脚本路径
363
- * @param {boolean|undefined} local 是否先从本地下载
364
- */
365
- const getScript = async (urls, local) => {
366
- const internal = urls.filter(url => !url.includes('/')),
367
- external = urls.filter(url => url.includes('/'));
368
- if (local === true) {
369
- await getInternalScript(internal);
370
- return getExternalScript(external);
371
- } else if (local === false) {
372
- await getExternalScript(external);
373
- return getInternalScript(internal);
374
- }
375
- return Promise.all([getInternalScript(internal), getExternalScript(external)]);
376
- };
377
-
378
- // 以下进入CodeMirror相关内容
379
- let /** @type {CodeMirror.EditorFromTextArea} */ cm;
380
-
381
- /**
382
- * 生成需要的插件列表
383
- * @param {typeof CodeMirror} CM CodeMirror
384
- * @param {boolean} other 是否用于Wikiplus以外的textarea
385
- */
386
- const getAddonScript = (CM, other = false) => {
387
- const /** @type {string[]} */ addonScript = [];
388
- for (const {option, addon = option, download = Array.isArray(addon) ? option : addon, only} of options) {
389
- if (!(only && other) && !(option in CM.optionHandlers) && intersect(addon, addons)) {
390
- addonScript.push(ADDON_LIST[download]);
391
- }
392
- }
393
- return addonScript;
394
- };
395
-
396
- /**
397
- * 交集
398
- * @param {T[]|T} arr 集合1(可重)
399
- * @param {Set<T>} set 集合2
400
- * @template T
401
- */
402
- const intersect = (arr, set) => Array.isArray(arr)
403
- ? arr.some(ele => set.has(ele))
404
- : set.has(arr);
405
-
406
- /**
407
- * 根据文本的高亮模式加载依赖项
408
- * @param {string} type 高亮模式
409
- */
410
- const initMode = type => {
411
- let /** @type {string[]} */ scripts = [];
412
- const loaded = typeof window.CodeMirror === 'function';
413
-
414
- /**
415
- * 代替`CodeMirror`的局部变量
416
- * @type {typeof CodeMirror}
417
- */
418
- const CM = loaded
419
- ? window.CodeMirror
420
- : {modes: {}, prototype: {}, commands: {}, optionHandlers: {}, helpers: {}};
421
-
422
- // lib
423
- if (!loaded) {
424
- scripts.push(MODE_LIST.lib);
425
- if (!USING_LOCAL) {
426
- mw.loader.load(`${CDN}/${CM_CDN}/lib/codemirror.min.css`, 'text/css');
427
- }
428
- }
429
-
430
- // modes
431
- if (type === 'mediawiki' && SITE_SETTINGS.config && SITE_SETTINGS.config.tags.html) {
432
- // NamespaceHTML扩展自由度过高,所以这里一律当作允许<html>标签
433
- type = 'html'; // eslint-disable-line no-param-reassign
434
- }
435
- if ((type === 'mediawiki' || type === 'widget') && !CM.modes.mediawiki) {
436
- // 总是外部样式表和外部脚本
437
- mw.loader.load(`${CDN}/${MW_CDN}/mediawiki.min.css`, 'text/css');
438
- scripts.push(`${MW_CDN}/mediawiki.min.js`);
439
- }
440
- if (type === 'widget' || type === 'html') {
441
- for (const lang of ['css', 'javascript', 'mediawiki', 'htmlmixed', 'xml']) {
442
- if (!CM.modes[lang]) {
443
- scripts = scripts.concat(MODE_LIST[lang]);
444
- }
445
- }
446
- } else {
447
- scripts = scripts.concat(MODE_LIST[type]);
448
- }
449
-
450
- // addons
451
- if (!CM.prototype.getSearchCursor && addons.has('search') && !addons.has('wikiEditor')) {
452
- scripts.push(ADDON_LIST.searchcursor);
453
- }
454
- if (!CM.prototype.annotateScrollbar && type === 'mediawiki' && addons.has('lint')) {
455
- scripts.push(ADDON_LIST.annotateScrollbar);
456
- }
457
- if (!CM.commands.find && addons.has('search') && !addons.has('wikiEditor')) {
458
- scripts.push(ADDON_LIST.search);
459
- }
460
- if (!window.wikiparse && type === 'mediawiki' && addons.has('lint')) {
461
- scripts.push(ADDON_LIST.parser, ADDON_LIST.linter);
462
- }
463
- if (!CM.optionHandlers.lint && type === 'mediawiki' && addons.has('lint')) {
464
- mw.loader.load(`${CDN}/${CM_CDN}/addon/lint/lint.min.css`, 'text/css');
465
- scripts.push(ADDON_LIST.lint);
466
- }
467
- if (!(CM.helpers.lint && CM.helpers.lint.mediawiki) && type === 'mediawiki' && addons.has('lint')) {
468
- scripts.push(ADDON_LIST.lintWikitext);
469
- }
470
- if (addons.has('wikiEditor')) {
471
- const state = mw.loader.getState('ext.wikiEditor');
472
- if (!state) {
473
- addons.delete('wikiEditor');
474
- } else if (state !== 'ready') {
475
- scripts.push(ADDON_LIST.wikiEditor);
476
- }
477
- }
478
- if (mw.loader.getState('mediawiki.Title') !== 'ready' && addons.has('contextmenu')) {
479
- scripts.push(ADDON_LIST.contextmenu);
480
- }
481
- scripts.push(...getAddonScript(CM));
482
-
483
- return getScript(scripts, loaded ? undefined : USING_LOCAL);
484
- };
485
-
486
- /**
487
- * 更新缓存的设置数据
488
- * @param {mwConfig} config wikitext设置
489
- */
490
- const updateCachedConfig = config => {
491
- ALL_SETTINGS_CACHE[SITE_ID] = {config, time: Date.now()};
492
- storage.setObject('InPageEditMwConfig', ALL_SETTINGS_CACHE);
493
- };
494
-
495
- /**
496
- * 展开别名列表
497
- * @param {{aliases: string[], name: string}[]} words 原名
498
- */
499
- const getAliases = words => words.flatMap(({aliases, name}) => aliases.map(alias => ({alias, name})));
500
-
501
- /**
502
- * 将别名信息转换为CodeMirror接受的设置
503
- * @param {{alias: string, name: string}[]} aliases 别名
504
- * @returns {Record<string, string>}
505
- */
506
- const getConfig = aliases => Object.fromEntries(
507
- aliases.map(({alias, name}) => [alias.replace(/:$/u, ''), name]),
508
- );
509
-
510
- /**
511
- * 高亮扩展标签内部
512
- * @param {mwConfig} config 设置
513
- */
514
- const setPlainMode = config => {
515
- const tags = ['indicator', 'poem', 'ref', 'tabs', 'tab', 'poll'];
516
- for (const tag of tags) {
517
- if (config.tags[tag]) {
518
- config.tagModes[tag] = 'text/mediawiki';
519
- }
520
- }
521
- };
522
-
523
- /**
524
- * 加载CodeMirror的mediawiki模块需要的设置数据
525
- * @param {string} type 高亮模式
526
- * @param {Promise<void>} initModePromise 使用本地CodeMirror扩展时大部分数据来自ext.CodeMirror.data模块
527
- */
528
- const getMwConfig = async (type, initModePromise) => {
529
- if (type !== 'mediawiki' && type !== 'widget') {
530
- return undefined;
531
- }
532
-
533
- if (USING_LOCAL && EXPIRED) { // 只在localStorage过期时才会重新加载ext.CodeMirror.data
534
- await initModePromise;
535
- }
536
-
537
- let config = mw.config.get('extCodeMirrorConfig');
538
- if (!config && !EXPIRED && isLatest) {
539
- ({config} = SITE_SETTINGS);
540
- setPlainMode(config);
541
- mw.config.set('extCodeMirrorConfig', config);
542
- }
543
- const isIPE = config && Object.values(config.functionSynonyms[0]).includes(true);
544
- // 情形1:config已更新,可能来自localStorage
545
- if (config && config.redirect && config.img && config.variants && !isIPE) {
546
- return config;
547
- }
548
-
549
- // 以下情形均需要发送API请求
550
- // 情形2:localStorage未过期但不包含新设置
551
- // 情形3:新加载的 ext.CodeMirror.data
552
- // 情形4:`config === null`
553
- const {
554
- query: {general: {variants}, magicwords, extensiontags, functionhooks, variables},
555
- } = await new mw.Api().get({
556
- meta: 'siteinfo',
557
- siprop: `general|magicwords${config && !isIPE ? '' : '|extensiontags|functionhooks|variables'}`,
558
- formatversion: 2,
559
- });
560
- const otherMagicwords = new Set(['msg', 'raw', 'msgnw', 'subst', 'safesubst']);
561
-
562
- if (config && !isIPE) { // 情形2或3
563
- const {functionSynonyms: [insensitive]} = config;
564
- if (!insensitive.subst) {
565
- const aliases = getAliases(magicwords.filter(({name}) => otherMagicwords.has(name)));
566
- for (const {alias, name} of aliases) {
567
- insensitive[alias.replace(/:$/u, '')] = name;
568
- }
569
- }
570
- } else { // 情形4:`config === null`
571
- config = {
572
- tagModes: {
573
- pre: 'mw-tag-pre',
574
- nowiki: 'mw-tag-nowiki',
575
- ref: 'text/mediawiki',
576
- },
577
- tags: Object.fromEntries(
578
- extensiontags.map(tag => [tag.slice(1, -1), true]),
579
- ),
580
- urlProtocols: mw.config.get('wgUrlProtocols'),
581
- };
582
- const realMagicwords = new Set([...functionhooks, ...variables, ...otherMagicwords]),
583
- allMagicwords = magicwords.filter(
584
- ({name, aliases}) => aliases.some(alias => /^__.+__$/u.test(alias)) || realMagicwords.has(name),
585
- ),
586
- sensitive = getAliases(
587
- allMagicwords.filter(word => word['case-sensitive']),
588
- ),
589
- insensitive = getAliases(
590
- allMagicwords.filter(word => !word['case-sensitive']),
591
- ).map(({alias, name}) => ({alias: alias.toLowerCase(), name}));
592
- config.doubleUnderscore = [
593
- getConfig(insensitive.filter(({alias}) => /^__.+__$/u.test(alias))),
594
- getConfig(sensitive.filter(({alias}) => /^__.+__$/u.test(alias))),
595
- ];
596
- config.functionSynonyms = [
597
- getConfig(insensitive.filter(({alias}) => !/^__.+__|^#$/u.test(alias))),
598
- getConfig(sensitive.filter(({alias}) => !/^__.+__|^#$/u.test(alias))),
599
- ];
600
- }
601
- config.redirect = magicwords.find(({name}) => name === 'redirect').aliases;
602
- config.img = getConfig(
603
- getAliases(magicwords.filter(({name}) => name.startsWith('img_'))),
604
- );
605
- config.variants = variants ? variants.map(({code}) => code) : [];
606
- setPlainMode(config);
607
- mw.config.set('extCodeMirrorConfig', config);
608
- updateCachedConfig(config);
609
- return config;
610
- };
611
-
612
- /** 检查页面语言类型 */
613
- const getPageMode = async () => {
614
- if (page.endsWith('/doc')) {
615
- return 'mediawiki';
616
- } else if (ns !== 274 && ns !== 828) {
617
- return CONTENTMODEL[contentmodel];
618
- }
619
- const pageMode = ns === 274 ? 'Widget' : 'Lua';
620
- await mw.loader.using(['oojs-ui-windows', 'oojs-ui.styles.icons-content']);
621
- const bool = await OO.ui.confirm(msg('contentmodel'), {
622
- actions: [{label: pageMode}, {label: 'Wikitext', action: 'accept'}],
623
- });
624
- return bool ? 'mediawiki' : pageMode.toLowerCase();
625
- };
626
-
627
- /**
628
- * jQuery.textSelection overrides for CodeMirror.
629
- * See jQuery.textSelection.js for method documentation
630
- */
631
- const cmTextSelection = {
632
- /** @override */ getContents() {
633
- return cm.getValue();
634
- },
635
- /** @override */ setContents(content) {
636
- cm.setValue(content);
637
- return this;
638
- },
639
- /** @override */ getSelection() {
640
- return cm.getSelection();
641
- },
642
- /** @override */ setSelection(option) {
643
- cm.setSelection(
644
- cm.posFromIndex(option.start),
645
- 'end' in option ? cm.posFromIndex(option.end) : undefined,
646
- );
647
- cm.focus();
648
- return this;
649
- },
650
- /** @override */ replaceSelection(value) {
651
- cm.replaceSelection(value);
652
- return this;
653
- },
654
- /** @override */ getCaretPosition(option) {
655
- const caretPos = cm.indexFromPos(cm.getCursor('from')),
656
- endPos = cm.indexFromPos(cm.getCursor('to'));
657
- return option.startAndEnd ? [caretPos, endPos] : caretPos;
658
- },
659
- /** @override */ scrollToCaretPosition() {
660
- cm.scrollIntoView();
661
- return this;
662
- },
663
- };
664
-
665
- /**
666
- * 渲染编辑器
667
- * @param {JQuery<HTMLTextAreaElement>} $target 目标编辑框
668
- * @param {boolean} setting 是否是Wikiplus设置(使用json语法)
669
- */
670
- const renderEditor = async ($target, setting) => {
671
- const mode = setting ? 'javascript' : await getPageMode(),
672
- initModePromise = initMode(mode),
673
- [mwConfig] = await Promise.all([
674
- getMwConfig(mode, initModePromise),
675
- initModePromise,
676
- ]);
677
-
678
- if (!setting && addons.has('wikiEditor')) {
679
- try {
680
- if (typeof mw.addWikiEditor === 'function') {
681
- mw.addWikiEditor($target);
682
- } else {
683
- const {wikiEditor: {modules: {dialogs: {config}}}} = $;
684
- $target.wikiEditor('addModule', {
685
- ...$.wikiEditor.modules.toolbar.config.getDefaultConfig(),
686
- ...config.getDefaultConfig(),
687
- });
688
- config.replaceIcons($target);
689
- }
690
- } catch (e) {
691
- addons.delete('wikiEditor');
692
- mw.notify('WikiEditor工具栏加载失败。', {type: 'error'});
693
- console.error(e);
694
- }
695
- }
696
-
697
- if (mode === 'mediawiki' && mwConfig.tags.html) {
698
- mwConfig.tagModes.html = 'htmlmixed';
699
- await initMode('html'); // 如果已经缓存过`mwConfig`,这一步什么都不会发生
700
- } else if (mode === 'widget' && !CodeMirror.mimeModes.widget) { // 到这里CodeMirror已确定加载完毕
701
- CodeMirror.defineMIME('widget', {name: 'htmlmixed', tags: {noinclude: [[null, null, 'mediawiki']]}});
702
- }
703
-
704
- // 储存初始高度
705
- const height = $target.height();
706
-
707
- if (cm) {
708
- cm.toTextArea();
709
- }
710
-
711
- const json = setting || contentmodel === 'json';
712
- cm = CodeMirror.fromTextArea($target[0], $.extend(
713
- {
714
- inputStyle: 'contenteditable',
715
- lineNumbers: !/Android\b/u.test(navigator.userAgent),
716
- lineWrapping: true,
717
- mode,
718
- mwConfig,
719
- json,
720
- },
721
- Object.fromEntries(
722
- options.map(({option, addon = option, modes, complex = mod => !modes || modes.has(mod)}) => {
723
- const mainAddon = Array.isArray(addon) ? addon[0] : addon;
724
- return [option, addons.has(mainAddon) && complex(mode, json)];
725
- }),
726
- ),
727
- mode === 'mediawiki'
728
- ? {
729
- extraKeys: addons.has('escape') && extraKeys(CodeMirror),
730
- }
731
- : {
732
- indentUnit: addons.has('indentWithSpace') ? indent : defaultIndent,
733
- indentWithTabs: !addons.has('indentWithSpace'),
734
- },
735
- ));
736
- cm.setSize(null, height);
737
- cm.getWrapperElement().id = 'Wikiplus-CodeMirror';
738
-
739
- if ($.fn.textSelection) {
740
- $target.textSelection('register', cmTextSelection);
741
- }
742
-
743
- const ctrl = isPc(CodeMirror) ? 'Ctrl' : 'Cmd';
744
- if (addons.has('wikiEditor')) {
745
- const context = $target.data('wikiEditorContext');
746
- cm.addKeyMap({/** 替代CodeMirror的搜索功能 */ [`${ctrl}-F`]() {
747
- $.wikiEditor.modules.dialogs.api.openDialog(context, 'search-and-replace');
748
- }});
749
- }
750
-
751
- handleContextMenu(cm, mode);
752
-
753
- $('#Wikiplus-Quickedit-Jump').children('a').attr('href', '#Wikiplus-CodeMirror');
754
-
755
- if (!setting) { // 普通Wikiplus编辑区
756
- const settings = storage.getObject('Wikiplus_Settings'),
757
- escToExitQuickEdit = settings && settings.esc_to_exit_quickedit || settings.escToExitQuickEdit,
758
- submit = /** 提交编辑 */ () => {
759
- $('#Wikiplus-Quickedit-Submit').triggerHandler('click');
760
- },
761
- submitMinor = /** 提交小编辑 */ () => {
762
- $('#Wikiplus-Quickedit-MinorEdit').click();
763
- $('#Wikiplus-Quickedit-Submit').triggerHandler('click');
764
- };
765
- cm.addKeyMap($.extend(
766
- {[`${ctrl}-S`]: submit, [`Shift-${ctrl}-S`]: submitMinor},
767
- escToExitQuickEdit === true || escToExitQuickEdit === 'true'
768
- ? {
769
- /** 按下Esc键退出编辑 */ Esc() {
770
- $('#Wikiplus-Quickedit-Back').triggerHandler('click');
771
- },
772
- }
773
- : {},
774
- ));
775
- }
776
-
777
- cm.refresh();
778
- mw.hook('wiki-codemirror').fire(cm);
779
- };
780
-
781
- const {body} = document;
782
-
783
- // 监视 Wikiplus 编辑框
784
- const observer = new MutationObserver(records => {
785
- const $editArea = $(records.flatMap(({addedNodes}) => [...addedNodes]))
786
- .find('#Wikiplus-Quickedit, #Wikiplus-Setting-Input');
787
- if ($editArea.length > 0) {
788
- renderEditor($editArea, $editArea.attr('id') === 'Wikiplus-Setting-Input');
789
- }
790
- });
791
- observer.observe(body, {childList: true});
792
-
793
- $(body).on(
794
- 'keydown.wphl',
795
- '.ui-dialog',
796
- /** @this {HTMLBodyElement} */
797
- function(e) {
798
- if (e.key === 'Escape') {
799
- /** @type {{$textarea: JQuery<HTMLTextAreaElement>}} */
800
- const context = $(this).children('.ui-dialog-content').data('context');
801
- if (context && context.$textarea && context.$textarea.attr('id') === 'Wikiplus-Quickedit') {
802
- e.stopPropagation();
803
- }
804
- }
805
- },
806
- );
807
-
808
- /**
809
- * 是否是Wikiplus编辑区
810
- * @param {HTMLTextAreaElement} elem textarea元素
811
- */
812
- const isWikiplus = elem => elem.id === 'Wikiplus-Quickedit' || elem.id === 'Wikiplus-Setting-Input';
813
- $.valHooks.textarea = {
814
- /** @override */ get(elem) {
815
- return isWikiplus(elem) && cm ? cm.getValue() : elem.value;
816
- },
817
- /** @override */ set(elem, value) {
818
- if (isWikiplus(elem) && cm) {
819
- cm.setValue(value);
820
- } else {
821
- elem.value = value;
822
- }
823
- },
824
- };
825
-
826
- await i18nPromise; // 以下内容依赖I18N
827
-
828
- // 设置对话框
829
- let /** @type {OOUI.MessageDialog} */ dialog,
830
- /** @type {OOUI.CheckboxMultiselectInputWidget} */ widget,
831
- /** @type {OOUI.CheckboxMultioptionWidget} */ searchWidget,
832
- /** @type {OOUI.CheckboxMultioptionWidget} */ wikiEditorWidget,
833
- /** @type {OOUI.NumberInputWidget} */ indentWidget,
834
- /** @type {OOUI.FieldLayout} */ field,
835
- /** @type {OOUI.FieldLayout} */ indentField;
836
- /**
837
- * 显示/隐藏缩进大小选项
838
- * @param {string[]} value 加载的插件
839
- */
840
- const toggleIndent = (value = [...addons]) => {
841
- indentField.toggle(value.includes('indentWithSpace'));
842
- };
843
- const portletContainer = {
844
- minerva: 'page-actions-overflow',
845
- moeskin: 'ca-more-actions',
846
- };
847
- const $portlet = $(mw.util.addPortletLink(
848
- portletContainer[skin] || 'p-cactions', '#', msg('portlet'), 'wphl-settings',
849
- )).click(async () => {
850
- if (dialog) {
851
- widget.setValue([...addons]);
852
- indentWidget.setValue(indent);
853
- } else {
854
- await mw.loader.using(['oojs-ui-windows', 'oojs-ui.styles.icons-content']);
855
- // eslint-disable-next-line require-atomic-updates
856
- dialog = new OO.ui.MessageDialog({id: 'Wikiplus-highlight-dialog'});
857
- const windowManager = new OO.ui.WindowManager();
858
- windowManager.$element.appendTo(body);
859
- windowManager.addWindows([dialog]);
860
- widget = new OO.ui.CheckboxMultiselectInputWidget({
861
- options: [
862
- ...options.map(({option, addon = option}) => {
863
- const mainAddon = Array.isArray(addon) ? addon[0] : addon;
864
- return {data: mainAddon, label: htmlMsg(`addon-${mainAddon.toLowerCase()}`)};
865
- }),
866
- ...['wikiEditor', 'escape', 'contextmenu', 'lint', 'indentWithSpace', 'otherEditors']
867
- .map(addon => ({data: addon, label: htmlMsg(`addon-${addon.toLowerCase()}`)})),
868
- ],
869
- value: [...addons],
870
- }).on('change', toggleIndent);
871
- const {checkboxMultiselectWidget} = widget;
872
- searchWidget = checkboxMultiselectWidget.findItemFromData('search');
873
- wikiEditorWidget = checkboxMultiselectWidget.findItemFromData('wikiEditor');
874
- indentWidget = new OO.ui.NumberInputWidget({min: 0, value: indent});
875
- field = new OO.ui.FieldLayout(widget, {
876
- label: msg('addon-label'),
877
- notices: [msg('addon-notice')],
878
- align: 'top',
879
- });
880
- indentField = new OO.ui.FieldLayout(indentWidget, {label: msg('addon-indent')});
881
- toggleIndent();
882
- Object.assign(mw.libs.wphl, {widget, indentWidget});
883
- }
884
- const wikiplusLoaded = typeof window.Wikiplus === 'object' || typeof window._WikiplusPages === 'object';
885
- searchWidget.setDisabled(!wikiplusLoaded);
886
- wikiEditorWidget.setDisabled(!wikiplusLoaded || !mw.loader.getState('ext.wikiEditor'));
887
- const data = await dialog.open({
888
- title: msg('addon-title'),
889
- message: field.$element.add(indentField.$element).add(
890
- $('<p>', {html: msg('feedback')}),
891
- ),
892
- actions: [
893
- {action: 'reject', label: mw.msg('ooui-dialog-message-reject')},
894
- {action: 'accept', label: mw.msg('ooui-dialog-message-accept'), flags: 'progressive'},
895
- ],
896
- size: i18nLang === 'en' || skin === 'minerva' ? 'medium' : 'small',
897
- }).closing;
898
- field.$element.detach();
899
- indentField.$element.detach();
900
- if (typeof data === 'object' && data.action === 'accept') {
901
- /* eslint-disable require-atomic-updates */
902
- const value = widget.getValue();
903
- addons.clear();
904
- for (const addon of value) {
905
- addons.add(addon);
906
- }
907
- indent = Number(indentWidget.getValue());
908
- storage.setObject('Wikiplus-highlight-addons', value);
909
- storage.setObject('Wikiplus-highlight-indent', indent);
910
- /* eslint-enable require-atomic-updates */
911
- }
912
- });
913
- if (skin === 'minerva') {
914
- $portlet.find('.mw-ui-icon').addClass('mw-ui-icon-minerva-settings');
915
- }
916
-
917
- // 发送欢迎提示
918
- if (typeof welcome === 'function') {
919
- welcome().find('#wphl-settings-notify').click(e => {
920
- e.preventDefault();
921
- $('#wphl-settings').triggerHandler('click');
922
- });
923
- }
924
-
925
- /**
926
- * 处理非Wikiplus编辑器
927
- * @param {CodeMirror.Editor} doc CodeMirror编辑区
928
- */
929
- const handleOtherEditors = async doc => {
930
- if (!addons.has('otherEditors')) {
931
- return;
932
- }
933
- let mode = doc.getOption('mode');
934
- mode = mode === 'text/mediawiki' ? 'mediawiki' : mode;
935
- const addonScript = getAddonScript(CodeMirror, true),
936
- json = doc.getOption('json'),
937
- {prototype, optionHandlers, helpers: {lint}} = CodeMirror;
938
- if (!prototype.annotateScrollbar && mode === 'mediawiki' && addons.has('lint')) {
939
- addonScript.push(ADDON_LIST.annotateScrollbar);
940
- }
941
- if (!window.wikiparse && mode === 'mediawiki' && addons.has('lint')) {
942
- addonScript.push(ADDON_LIST.parser);
943
- }
944
- if (!optionHandlers.lint && mode === 'mediawiki' && addons.has('lint')) {
945
- mw.loader.load(`${CDN}/${CM_CDN}/addon/lint/lint.min.css`, 'text/css');
946
- addonScript.push(ADDON_LIST.lint);
947
- }
948
- if (!(lint && lint.mediawiki) && mode === 'mediawiki' && addons.has('lint')) {
949
- addonScript.push(ADDON_LIST.lintWikitext);
950
- }
951
- await getScript(addonScript);
952
- for (const {
953
- option, addon = option, modes, complex = (/** @type {string} */ mod) => !modes || modes.has(mod),
954
- } of options.filter(({only}) => !only)) {
955
- const mainAddon = Array.isArray(addon) ? addon[0] : addon;
956
- if (doc.getOption(option) === undefined && addons.has(mainAddon)) {
957
- doc.setOption(option, complex(mode, json));
958
- }
959
- }
960
- if (mode === 'mediawiki' && addons.has('escape')) {
961
- doc.addKeyMap(extraKeys(CodeMirror), true);
962
- } else if (mode !== 'mediawiki' && addons.has('indentWithSpace')) {
963
- doc.setOption('indentUnit', indent);
964
- doc.setOption('indentWithTabs', false);
965
- }
966
- handleContextMenu(doc, mode);
967
- };
968
-
969
- mw.hook('InPageEdit.quickEdit.codemirror').add(
970
- /** @param {{cm: CodeMirror.Editor}} */ ({cm: doc}) => handleOtherEditors(doc),
971
- );
972
- mw.hook('inspector').add(/** @param {CodeMirror.Editor} doc */ doc => handleOtherEditors(doc));
973
- mw.hook('wiki-codemirror').add(/** @param {CodeMirror.Editor} doc */ doc => {
974
- if (!doc.getTextArea || !isWikiplus(doc.getTextArea())) {
975
- handleOtherEditors(doc);
976
- }
977
- });
978
-
979
- mw.loader.load(`${CDN}/${REPO_CDN}/styles.min.css`, 'text/css');
980
-
981
- Object.assign(mw.libs.wphl, {
982
- version,
983
- options,
984
- addons,
985
- i18n,
986
- i18nLang,
987
- storage,
988
- $portlet,
989
- CDN,
990
- PARSER_CDN,
991
- USING_LOCAL,
992
- MODE_LIST,
993
- ADDON_LIST,
994
- msg,
995
- htmlMsg,
996
- escapeHTML,
997
- handleContextMenu,
998
- setI18N,
999
- getAddonScript,
1000
- updateCachedConfig,
1001
- getMwConfig,
1002
- renderEditor,
1003
- handleOtherEditors,
1004
- isPc,
1005
- }); // 加载完毕
1006
- })();