wikiplus-highlight 2.7.1

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 ADDED
@@ -0,0 +1,704 @@
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
+ const version = '2.7.1';
12
+
13
+ /**
14
+ * polyfill for mw.storage
15
+ * @type {Object.<string, function>}
16
+ */
17
+ const storage = typeof mw.storage === 'object' && typeof mw.storage.getObject === 'function'
18
+ ? mw.storage
19
+ : {
20
+ getObject(key) {
21
+ const json = localStorage.getItem(key);
22
+ if (json === false) {
23
+ return false;
24
+ }
25
+ try {
26
+ return JSON.parse(json);
27
+ } catch (e) {
28
+ return null;
29
+ }
30
+ },
31
+ setObject(key, value) {
32
+ let json;
33
+ try {
34
+ json = JSON.stringify(value);
35
+ return localStorage.setItem(key, json);
36
+ } catch (e) {
37
+ return false;
38
+ }
39
+ },
40
+ };
41
+ /**
42
+ * polyfill for Object.fromEntries
43
+ * @type {function}
44
+ * @param {Array.<[string, any]>} entries
45
+ * @returns {Object}
46
+ */
47
+ const fromEntries = Object.fromEntries || (entries => {
48
+ const obj = {};
49
+ for (const [key, value] of entries) {
50
+ obj[key] = value;
51
+ }
52
+ return obj;
53
+ });
54
+
55
+ /**
56
+ * 解析版本号
57
+ * @param {string} str 版本字符串
58
+ * @returns {number[]}
59
+ */
60
+ const getVersion = str => str.split('.').map(s => Number(s));
61
+ /**
62
+ * 比较版本号
63
+ * @param {string} a
64
+ * @param {string} b
65
+ * @returns {boolean} a的版本号是否小于b的版本号
66
+ */
67
+ const cmpVersion = (a, b) => {
68
+ const [a0, a1] = getVersion(a),
69
+ [b0, b1] = getVersion(b);
70
+ return a0 < b0 || a0 === b0 && a1 < b1;
71
+ };
72
+ /**
73
+ * 获取I18N消息
74
+ * @param {string} key 消息键,省略'wphl-'前缀
75
+ * @param {string[]} args
76
+ * @returns {string}
77
+ */
78
+ const msg = (key, ...args) => mw.msg(`wphl-${key}`, ...args);
79
+ /**
80
+ * 提示消息
81
+ * @param {string[]} args
82
+ * @return {function: jQuery.<HTMLParagraphElement>}
83
+ */
84
+ const notify = (...args) => () => {
85
+ const $p = $('<p>', {html: msg(...args)});
86
+ mw.notify($p, {type: 'success', autoHideSeconds: 'long'});
87
+ return $p;
88
+ };
89
+
90
+ // 插件和I18N依主版本号
91
+ const majorVersion = getVersion(version).slice(0, 2).join('.');
92
+
93
+ // 路径
94
+ const CDN = '//fastly.jsdelivr.net',
95
+ CM_CDN = 'npm/codemirror@5.65.3',
96
+ MW_CDN = 'gh/bhsd-harry/codemirror-mediawiki@1.0',
97
+ REPO_CDN = `gh/bhsd-harry/Wikiplus-highlight@${majorVersion}`;
98
+
99
+ // mw.config常数
100
+ const {
101
+ wgPageName: page,
102
+ wgNamespaceNumber: ns,
103
+ wgPageContentModel: contentmodel,
104
+ wgServerName: server,
105
+ wgScriptPath: scriptPath,
106
+ wgUserLanguage: userLang,
107
+ skin,
108
+ } = mw.config.values;
109
+
110
+ // 和本地缓存有关的常数
111
+ const USING_LOCAL = mw.loader.getState('ext.CodeMirror') !== null,
112
+ ALL_SETTINGS_CACHE = storage.getObject('InPageEditMwConfig') || {}, // @type {?Object.<string, Object>}
113
+ SITE_ID = `${server}${scriptPath}`,
114
+ SITE_SETTINGS = ALL_SETTINGS_CACHE[SITE_ID] || {}, // @type {?Object}
115
+ EXPIRED = SITE_SETTINGS.time < Date.now() - 86400 * 1000 * 30;
116
+
117
+ const CONTENTMODEL = {
118
+ css: 'css',
119
+ 'sanitized-css': 'css',
120
+ javascript: 'javascript',
121
+ json: 'javascript',
122
+ wikitext: 'mediawiki',
123
+ };
124
+
125
+ const MODE_LIST = USING_LOCAL
126
+ ? {
127
+ lib: 'ext.CodeMirror.lib',
128
+ css: 'ext.CodeMirror.lib.mode.css',
129
+ javascript: 'ext.CodeMirror.lib.mode.javascript',
130
+ lua: `${CM_CDN}/mode/lua/lua.min.js`,
131
+ mediawiki: EXPIRED ? 'ext.CodeMirror.data' : [],
132
+ htmlmixed: 'ext.CodeMirror.lib.mode.htmlmixed',
133
+ xml: [],
134
+ }
135
+ : {
136
+ lib: `${CM_CDN}/lib/codemirror.min.js`,
137
+ css: `${CM_CDN}/mode/css/css.min.js`,
138
+ javascript: `${CM_CDN}/mode/javascript/javascript.min.js`,
139
+ lua: `${CM_CDN}/mode/lua/lua.min.js`,
140
+ mediawiki: [],
141
+ htmlmixed: `${CM_CDN}/mode/htmlmixed/htmlmixed.min.js`,
142
+ xml: `${CM_CDN}/mode/xml/xml.min.js`,
143
+ };
144
+
145
+ const ADDON_LIST = {
146
+ searchcursor: `${CM_CDN}/addon/search/searchcursor.min.js`,
147
+ search: `${REPO_CDN}/search.min.js`,
148
+ activeLine: `${CM_CDN}/addon/selection/active-line.min.js`,
149
+ markSelection: `${CM_CDN}/addon/selection/mark-selection.min.js`,
150
+ trailingspace: `${CM_CDN}/addon/edit/trailingspace.min.js`,
151
+ matchBrackets: `${CM_CDN}/addon/edit/matchbrackets.min.js`,
152
+ matchTags: `${REPO_CDN}/matchtags.min.js`,
153
+ };
154
+
155
+ const defaultAddons = ['search'],
156
+ defaultIndent = '4';
157
+ let addons = storage.getObject('Wikiplus-highlight-addons') || defaultAddons, // @type {?string[]}
158
+ indent = storage.getObject('Wikiplus-highlight-indent') || defaultIndent; // @type {string}
159
+
160
+ // 用于contextMenu插件
161
+ const contextmenuStyle = document.getElementById('wphl-contextmenu') // @type {?HTMLStyleElement}
162
+ || mw.loader.addStyleTag('#Wikiplus-CodeMirror .cm-mw-template-name{cursor:pointer}');
163
+ contextmenuStyle.id = 'wphl-contextmenu';
164
+ contextmenuStyle.disabled = true;
165
+
166
+ let i18n = storage.getObject('Wikiplus-highlight-i18n'), // @type {?Object.<string, string>}
167
+ welcome; // @type {function: jQuery.<?HTMLParagraphElement>}
168
+ if (!i18n) { // 首次安装
169
+ i18n = {};
170
+ welcome = notify('welcome');
171
+ } else if (cmpVersion(i18n['wphl-version'], version)) { // 更新版本
172
+ welcome = notify('welcome-upgrade', version);
173
+ }
174
+
175
+ const i18nLanguages = {
176
+ zh: 'zh-hans', 'zh-hans': 'zh-hans', 'zh-cn': 'zh-hans', 'zh-my': 'zh-hans', 'zh-sg': 'zh-hans',
177
+ 'zh-hant': 'zh-hant', 'zh-tw': 'zh-hant', 'zh-hk': 'zh-hant', 'zh-mo': 'zh-hant',
178
+ },
179
+ i18nLang = i18nLanguages[userLang] || 'en', // @type {?string}
180
+ I18N_CDN = `${CDN}/${REPO_CDN}/i18n/${i18nLang}.json`,
181
+ isLatest = i18n['wphl-version'] === majorVersion;
182
+
183
+ /**
184
+ * 加载 I18N
185
+ * @returns {promise}
186
+ */
187
+ const setI18N = async () => {
188
+ if (!isLatest || i18n['wphl-lang'] !== i18nLang) {
189
+ i18n = await $.ajax(`${I18N_CDN}`, { // eslint-disable-line require-atomic-updates
190
+ dataType: 'json',
191
+ cache: true,
192
+ });
193
+ storage.setObject('Wikiplus-highlight-i18n', i18n);
194
+ }
195
+ mw.messages.set(i18n);
196
+ };
197
+
198
+ const i18nPromise = Promise.all([ // 提前加载I18N
199
+ mw.loader.using('mediawiki.util'),
200
+ setI18N(),
201
+ ]);
202
+
203
+ /**
204
+ * 下载脚本
205
+ * @param {string[]} urls 脚本路径
206
+ * @param {boolean=false} local 是否从本地下载
207
+ * @returns {promise}
208
+ */
209
+ const getScript = (urls, local) => {
210
+ if (urls.length === 0) {
211
+ return;
212
+ }
213
+ return local
214
+ ? mw.loader.using(urls)
215
+ : $.ajax(`${CDN}/${urls.length > 1 ? 'combine/' : ''}${urls.join()}`, {
216
+ dataType: 'script',
217
+ cache: true,
218
+ });
219
+ };
220
+
221
+ // 以下进入CodeMirror相关内容
222
+ let cm;
223
+
224
+ /**
225
+ * 根据文本的高亮模式加载依赖项
226
+ * @param {string} type
227
+ * @returns {promise}
228
+ */
229
+ const initMode = async type => {
230
+ let scripts = [];
231
+ const externalScript = [],
232
+ addonScript = [],
233
+ loaded = typeof window.CodeMirror === 'function';
234
+
235
+ /**
236
+ * 代替CodeMirror的局部变量
237
+ * @type {(function|Object.<string, Object>)}
238
+ */
239
+ const CM = loaded
240
+ ? window.CodeMirror
241
+ : {
242
+ modes: {},
243
+ prototype: {},
244
+ commands: {},
245
+ optionHandlers: {},
246
+ };
247
+
248
+ if (['mediawiki', 'widget'].includes(type) && !CM.modes.mediawiki) {
249
+ // 总是外部样式表和外部脚本
250
+ mw.loader.load(`${CDN}/${MW_CDN}/mediawiki.min.css`, 'text/css');
251
+ (USING_LOCAL ? externalScript : scripts).push(`${MW_CDN}/mediawiki.min.js`);
252
+ }
253
+ if (type === 'mediawiki' && typeof SITE_SETTINGS.config === 'object' && SITE_SETTINGS.config.tags.html) {
254
+ // NamespaceHTML扩展自由度过高,所以这里一律当作允许<html>标签
255
+ type = 'html'; // eslint-disable-line no-param-reassign
256
+ }
257
+ if (!loaded) {
258
+ (USING_LOCAL ? scripts : addonScript).push(MODE_LIST.lib);
259
+ if (!USING_LOCAL) {
260
+ mw.loader.load(`${CDN}/${CM_CDN}/lib/codemirror.min.css`, 'text/css');
261
+ }
262
+ }
263
+ if (!CM.prototype.getSearchCursor && addons.includes('search')) {
264
+ addonScript.push(ADDON_LIST.searchcursor);
265
+ }
266
+ if (!CM.commands.findForward && addons.includes('search')) {
267
+ addonScript.push(ADDON_LIST.search);
268
+ }
269
+ if (!CM.optionHandlers.styleActiveLine && addons.includes('activeLine')) {
270
+ addonScript.push(ADDON_LIST.activeLine);
271
+ }
272
+ if (!CM.optionHandlers.styleSelectedText && addons.includes('search')) {
273
+ addonScript.push(ADDON_LIST.markSelection);
274
+ }
275
+ if (!CM.optionHandlers.showTrailingSpace && addons.includes('trailingspace')) {
276
+ addonScript.push(ADDON_LIST.trailingspace);
277
+ }
278
+ if (!CM.optionHandlers.matchBrackets && addons.includes('matchBrackets')) {
279
+ addonScript.push(ADDON_LIST.matchBrackets);
280
+ }
281
+ if (!CM.optionHandlers.matchTags && addons.includes('matchTags')) {
282
+ addonScript.push(ADDON_LIST.matchTags);
283
+ }
284
+ if (['widget', 'html'].includes(type)) {
285
+ ['css', 'javascript', 'mediawiki', 'htmlmixed', 'xml'].forEach(lang => {
286
+ if (!CM.modes[lang]) {
287
+ scripts = scripts.concat(MODE_LIST[lang]);
288
+ }
289
+ });
290
+ } else if (!CM.modes[type]) {
291
+ if (type === 'lua') { // CodeMirror扩展没有提供Lua模式,必须从外部下载
292
+ (USING_LOCAL ? externalScript : scripts).push(MODE_LIST.lua);
293
+ } else {
294
+ scripts = scripts.concat(MODE_LIST[type]);
295
+ }
296
+ }
297
+
298
+ if (loaded) {
299
+ await Promise.all([
300
+ getScript(scripts, USING_LOCAL), // CodeMirror modes
301
+ getScript(externalScript), // external Lua mode when using local lib
302
+ getScript(addonScript), // external addons
303
+ ]);
304
+ } else if (USING_LOCAL) {
305
+ await getScript(scripts, true); // local CodeMirror lib and modes
306
+ await Promise.all([
307
+ getScript(externalScript), // external Lua mode
308
+ getScript(addonScript), // external addons
309
+ ]);
310
+ } else {
311
+ await getScript(addonScript); // external CodeMirror lib and addons
312
+ await getScript(scripts); // external modes, including Lua
313
+ }
314
+ };
315
+
316
+ /**
317
+ * 更新缓存的设置数据
318
+ * @param {Object} config
319
+ */
320
+ const updateCachedConfig = config => {
321
+ ALL_SETTINGS_CACHE[SITE_ID] = {
322
+ config,
323
+ time: Date.now(),
324
+ };
325
+ storage.setObject('InPageEditMwConfig', ALL_SETTINGS_CACHE);
326
+ };
327
+
328
+ /**
329
+ * 加载CodeMirror的mediawiki模块需要的设置数据
330
+ * @param {string} type
331
+ * @param {promise} initModePromise 使用本地CodeMirror扩展时大部分数据来自ext.CodeMirror.data模块
332
+ * @returns {promise}
333
+ */
334
+ const getMwConfig = async (type, initModePromise) => {
335
+ if (!['mediawiki', 'widget'].includes(type)) {
336
+ return;
337
+ }
338
+
339
+ if (USING_LOCAL && EXPIRED) { // 只在localStorage过期时才会重新加载ext.CodeMirror.data
340
+ await initModePromise;
341
+ }
342
+
343
+ let config = mw.config.get('extCodeMirrorConfig');
344
+ if (!config && !EXPIRED && isLatest) {
345
+ ({config} = SITE_SETTINGS);
346
+ mw.config.set('extCodeMirrorConfig', config);
347
+ }
348
+ if (config && config.redirect && config.img) { // 情形1:config已更新,可能来自localStorage
349
+ return config;
350
+ } else if (config) { // FIXME: 暂不需要redirect和img相关设置
351
+ return config;
352
+ }
353
+
354
+ /**
355
+ * 以下情形均需要发送API请求
356
+ * 情形2:localStorage未过期但不包含新设置
357
+ * 情形3:新加载的 ext.CodeMirror.data
358
+ * 情形4:config === null
359
+ */
360
+ const {
361
+ query: {magicwords, extensiontags, functionhooks, variables},
362
+ } = await new mw.Api().get({
363
+ meta: 'siteinfo',
364
+ siprop: config ? 'magicwords' : 'magicwords|extensiontags|functionhooks|variables',
365
+ formatversion: 2,
366
+ });
367
+ const otherMagicwords = ['msg', 'raw', 'msgnw', 'subst', 'safesubst'];
368
+
369
+ /**
370
+ * @param {Object[]} words
371
+ * @property <string[]> aliases
372
+ * @property <string> name
373
+ * @returns {Object.<('alias'|'name'), string>[]}
374
+ */
375
+ const getAliases = words => words.flatMap(({aliases, name}) => aliases.map(alias => ({alias, name})));
376
+ /**
377
+ * @param {Object.<('alias'|'name'), string>[]} aliases
378
+ * @returns {Object.<string, string>}
379
+ */
380
+ const getConfig = aliases => fromEntries(
381
+ aliases.map(({alias, name}) => [alias.replace(/:$/, ''), name]),
382
+ );
383
+
384
+ if (!config) { // 情形4:config === null
385
+ config = {
386
+ tagModes: {
387
+ pre: 'mw-tag-pre',
388
+ nowiki: 'mw-tag-nowiki',
389
+ ref: 'text/mediawiki',
390
+ },
391
+ tags: fromEntries(
392
+ extensiontags.map(tag => [tag.slice(1, -1), true]),
393
+ ),
394
+ urlProtocols: mw.config.get('wgUrlProtocols'),
395
+ };
396
+ const realMagicwords = new Set([...functionhooks, ...variables, ...otherMagicwords]),
397
+ allMagicwords = magicwords.filter(({name, aliases}) =>
398
+ aliases.some(alias => /^__.+__$/.test(alias)) || realMagicwords.has(name),
399
+ ),
400
+ sensitive = getAliases(
401
+ allMagicwords.filter(word => word['case-sensitive']),
402
+ ),
403
+ insensitive = getAliases(
404
+ allMagicwords.filter(word => !word['case-sensitive']),
405
+ ).map(({alias, name}) => ({alias: alias.toLowerCase(), name}));
406
+ config.doubleUnderscore = [
407
+ getConfig(insensitive.filter(({alias}) => /^__.+__$/.test(alias))),
408
+ getConfig(sensitive.filter(({alias}) => /^__.+__$/.test(alias))),
409
+ ];
410
+ config.functionSynonyms = [
411
+ getConfig(insensitive.filter(({alias}) => !/^__.+__|^#$/.test(alias))),
412
+ getConfig(sensitive.filter(({alias}) => !/^__.+__|^#$/.test(alias))),
413
+ ];
414
+ } else { // 情形2或3
415
+ const {functionSynonyms: [insensitive]} = config;
416
+ if (!insensitive.subst) {
417
+ getAliases(
418
+ magicwords.filter(({name}) => otherMagicwords.includes(name)),
419
+ ).forEach(({alias, name}) => {
420
+ insensitive[alias.replace(/:$/, '')] = name;
421
+ });
422
+ }
423
+ }
424
+ config.redirect = magicwords.find(({name}) => name === 'redirect').aliases;
425
+ config.img = getConfig(
426
+ getAliases(magicwords.filter(({name}) => name.startsWith('img_'))),
427
+ );
428
+ mw.config.set('extCodeMirrorConfig', config);
429
+ updateCachedConfig(config);
430
+ return config;
431
+ };
432
+
433
+ /**
434
+ * 检查页面语言类型
435
+ * @returns {promise}
436
+ */
437
+ const getPageMode = async () => {
438
+ if ([274, 828].includes(ns) && !page.endsWith('/doc')) {
439
+ const pageMode = ns === 274 ? 'Widget' : 'Lua';
440
+ await mw.loader.using(['oojs-ui-windows', 'oojs-ui.styles.icons-content']);
441
+ const bool = await OO.ui.confirm(msg('contentmodel'), {
442
+ actions: [
443
+ {label: pageMode},
444
+ {label: 'Wikitext', action: 'accept'},
445
+ ],
446
+ });
447
+ return bool ? 'mediawiki' : pageMode.toLowerCase();
448
+ } else if (page.endsWith('/doc')) {
449
+ return 'mediawiki';
450
+ }
451
+ return CONTENTMODEL[contentmodel];
452
+ };
453
+
454
+ /**
455
+ * 渲染编辑器
456
+ * @param {jQuery.<HTMLTextAreaElement>} $target 目标编辑框
457
+ * @param {boolean} setting 是否是Wikiplus设置(使用json语法)
458
+ */
459
+ const renderEditor = async ($target, setting) => {
460
+ const mode = setting ? 'javascript' : await getPageMode();
461
+ const initModePromise = initMode(mode);
462
+ const [mwConfig] = await Promise.all([
463
+ getMwConfig(mode, initModePromise),
464
+ initModePromise,
465
+ ]);
466
+
467
+ if (mode === 'mediawiki' && mwConfig.tags.html) {
468
+ mwConfig.tagModes.html = 'htmlmixed';
469
+ await initMode('html'); // 如果已经缓存过mwConfig,这一步什么都不会发生
470
+ } else if (mode === 'widget' && !CodeMirror.mimeModes.widget) { // 到这里CodeMirror已确定加载完毕
471
+ CodeMirror.defineMIME('widget', {
472
+ name: 'htmlmixed',
473
+ tags: {
474
+ noinclude: [[null, null, 'mediawiki']],
475
+ },
476
+ });
477
+ }
478
+
479
+ // 储存初始高度
480
+ const height = $target.height();
481
+
482
+ if (cm) {
483
+ cm.toTextArea();
484
+ }
485
+
486
+ const json = setting || contentmodel === 'json',
487
+ {name} = $.client.profile();
488
+ cm = CodeMirror.fromTextArea($target[0], $.extend({
489
+ inputStyle: name === 'safari' ? 'textarea' : 'contenteditable',
490
+ lineNumbers: true,
491
+ lineWrapping: true,
492
+ mode,
493
+ mwConfig,
494
+ json,
495
+ styleActiveLine: addons.includes('activeLine'),
496
+ styleSelectedText: addons.includes('search'),
497
+ showTrailingSpace: addons.includes('trailingspace'),
498
+ matchBrackets: addons.includes('matchBrackets') && (mode === 'mediawiki' || json
499
+ ? {bracketRegex: /[{}[\]]/}
500
+ : true
501
+ ),
502
+ matchTags: addons.includes('matchBrackets') && ['mediawiki', 'widget'].includes(mode),
503
+ }, mode === 'mediawiki'
504
+ ? {}
505
+ : {
506
+ indentUnit: addons.includes('indentWithSpace') ? indent : defaultIndent,
507
+ indentWithTabs: !addons.includes('indentWithSpace'),
508
+ },
509
+ ));
510
+ cm.setSize(null, height);
511
+ cm.refresh();
512
+
513
+ const wrapper = cm.getWrapperElement();
514
+ wrapper.id = 'Wikiplus-CodeMirror';
515
+ if (['mediawiki', 'widget'].includes(mode) && addons.includes('contextmenu')) {
516
+ contextmenuStyle.disabled = false;
517
+ const {functionSynonyms: [synonyms]} = mw.config.get('extCodeMirrorConfig');
518
+ /**
519
+ * @param {string} name
520
+ * @returns {string[]}
521
+ */
522
+ const getSysnonyms = name => Object.keys(synonyms).filter(key => synonyms[key] === name)
523
+ .map(key => key.startsWith('#') ? key : `#${key}`);
524
+ const invoke = getSysnonyms('invoke'),
525
+ widget = getSysnonyms('widget');
526
+
527
+ await mw.loader.using('mediawiki.Title');
528
+ $(wrapper).on('contextmenu', '.cm-mw-template-name', function() {
529
+ const text = this.textContent.replace(/\u200e/g, '').trim(),
530
+ title = new mw.Title(text);
531
+ if (title.namespace !== 0 || text.startsWith(':')) {
532
+ open(title.getUrl(), '_blank');
533
+ } else {
534
+ open(mw.util.getUrl(`Template:${text}`), '_blank');
535
+ }
536
+ return false;
537
+ }).on(
538
+ 'contextmenu',
539
+ '.cm-mw-parserfunction-name + .cm-mw-parserfunction-delimiter + .cm-mw-parserfunction',
540
+ function() {
541
+ const parserFunction = this.previousSibling.previousSibling.textContent.trim().toLowerCase();
542
+ if (invoke.includes(parserFunction)) {
543
+ open(mw.util.getUrl(`Module:${this.textContent}`), '_blank');
544
+ } else if (widget.includes(parserFunction)) {
545
+ open(mw.util.getUrl(`Widget:${this.textContent}`, {action: 'edit'}), '_blank');
546
+ }
547
+ return false;
548
+ },
549
+ );
550
+ } else {
551
+ contextmenuStyle.disabled = true;
552
+ }
553
+
554
+ $('#Wikiplus-Quickedit-Jump').children('a').attr('href', '#Wikiplus-CodeMirror');
555
+
556
+ if (!setting) { // 普通Wikiplus编辑区
557
+ const submit = () => {
558
+ $('#Wikiplus-Quickedit-Submit').triggerHandler('click');
559
+ },
560
+ submitMinor = () => {
561
+ $('#Wikiplus-Quickedit-MinorEdit').click();
562
+ $('#Wikiplus-Quickedit-Submit').triggerHandler('click');
563
+ };
564
+ cm.addKeyMap($.extend({
565
+ 'Ctrl-S': submit,
566
+ 'Cmd-S': submit,
567
+ 'Shift-Ctrl-S': submitMinor,
568
+ 'Shift-Cmd-S': submitMinor,
569
+ }, Wikiplus.getSetting('esc_to_exit_quickedit')
570
+ ? {
571
+ Esc() {
572
+ $('#Wikiplus-Quickedit-Back').triggerHandler('click');
573
+ },
574
+ }
575
+ : {},
576
+ ));
577
+ }
578
+
579
+ mw.hook('wiki-codemirror').fire(cm);
580
+ };
581
+
582
+ // 监视 Wikiplus 编辑框
583
+ const observer = new MutationObserver(records => {
584
+ const $editArea = $(records.flatMap(({addedNodes}) => [...addedNodes]))
585
+ .find('#Wikiplus-Quickedit, #Wikiplus-Setting-Input');
586
+ if ($editArea.length === 0) {
587
+ return;
588
+ }
589
+ renderEditor($editArea, $editArea.attr('id') === 'Wikiplus-Setting-Input');
590
+ });
591
+ observer.observe(document.body, {childList: true});
592
+
593
+ // 添加样式
594
+ const wphlStyle = document.getElementById('wphl-style') || mw.loader.addStyleTag(
595
+ '#Wikiplus-Quickedit+.CodeMirror,#Wikiplus-Setting-Input+.CodeMirror{border:1px solid #c8ccd1;line-height:1.3;clear:both}'
596
+ + 'div.Wikiplus-InterBox{font-size:14px;z-index:100}'
597
+ + '.skin-minerva .Wikiplus-InterBox{font-size:16px}'
598
+ + '.cm-trailingspace{text-decoration:underline wavy red}'
599
+ + 'div.CodeMirror span.CodeMirror-matchingbracket{box-shadow:0 0 0 2px #9aef98}'
600
+ + 'div.CodeMirror span.CodeMirror-nonmatchingbracket{box-shadow:0 0 0 2px #eace64}'
601
+ + '#Wikiplus-highlight-dialog .oo-ui-messageDialog-title{margin-bottom:0.28571429em}'
602
+ + '#Wikiplus-highlight-dialog .oo-ui-flaggedElement-notice{font-weight:normal;margin:0}',
603
+ );
604
+ wphlStyle.id = 'wphl-style';
605
+
606
+ // 对编辑框调用jQuery.val方法时从CodeMirror获取文本
607
+ const {
608
+ get = function(elem) {
609
+ return elem.value;
610
+ },
611
+ set = function(elem, value) {
612
+ elem.value = value;
613
+ },
614
+ } = $.valHooks.textarea || {}; // @type {?Object.<string, function>}
615
+ const isWikiplus = elem => ['Wikiplus-Quickedit', 'Wikiplus-Setting-Input'].includes(elem.id);
616
+ $.valHooks.textarea = {
617
+ get(elem) {
618
+ return isWikiplus(elem) && cm ? cm.getValue() : get(elem);
619
+ },
620
+ set(elem, value) {
621
+ if (isWikiplus(elem) && cm) {
622
+ cm.setValue(value);
623
+ } else {
624
+ set(elem, value);
625
+ }
626
+ },
627
+ };
628
+
629
+ await i18nPromise; // 以下内容依赖I18N
630
+
631
+ // 设置对话框
632
+ let dialog, widget, indentWidget, field, indentField;
633
+ const toggleIndent = (value = addons) => {
634
+ indentField.toggle(value.includes('indentWithSpace'));
635
+ };
636
+ const portletContainer = {
637
+ minerva: 'page-actions-overflow',
638
+ citizen: 'p-actions',
639
+ };
640
+ const $portlet = $(mw.util.addPortletLink(
641
+ portletContainer[skin] || 'p-cactions', '#', msg('portlet'), 'wphl-settings',
642
+ )).click(async e => {
643
+ e.preventDefault();
644
+ if (!dialog) {
645
+ await mw.loader.using(['oojs-ui-windows', 'oojs-ui.styles.icons-content']);
646
+ // eslint-disable-next-line require-atomic-updates
647
+ dialog = new OO.ui.MessageDialog({id: 'Wikiplus-highlight-dialog'});
648
+ const windowManager = new OO.ui.WindowManager();
649
+ windowManager.$element.appendTo(document.body);
650
+ windowManager.addWindows([dialog]);
651
+ widget = new OO.ui.CheckboxMultiselectInputWidget({
652
+ options: [
653
+ {data: 'search', label: msg('addon-search')},
654
+ {data: 'activeLine', label: msg('addon-active-line')},
655
+ {data: 'trailingspace', label: msg('addon-trailingspace')},
656
+ {data: 'matchBrackets', label: msg('addon-matchbrackets')},
657
+ {data: 'matchTags', label: msg('addon-matchtags')},
658
+ {data: 'contextmenu', label: msg('addon-contextmenu')},
659
+ {data: 'indentWithSpace', label: msg('addon-indentwithspace')},
660
+ ],
661
+ value: addons,
662
+ }).on('change', toggleIndent);
663
+ indentWidget = new OO.ui.NumberInputWidget({min: 0, value: indent});
664
+ field = new OO.ui.FieldLayout(widget, {
665
+ label: msg('addon-label'),
666
+ notices: [msg('addon-notice')],
667
+ align: 'top',
668
+ });
669
+ indentField = new OO.ui.FieldLayout(indentWidget, {label: msg('addon-indent')});
670
+ toggleIndent();
671
+ }
672
+ dialog.open({
673
+ title: msg('addon-title'),
674
+ message: field.$element.add(indentField.$element).add(
675
+ $('<p>', {html: msg('feedback')}),
676
+ ),
677
+ actions: [
678
+ {action: 'reject', label: mw.msg('ooui-dialog-message-reject')},
679
+ {action: 'accept', label: mw.msg('ooui-dialog-message-accept'), flags: 'progressive'},
680
+ ],
681
+ size: i18nLang === 'en' ? 'medium' : 'small',
682
+ }).closing.then(data => {
683
+ field.$element.detach();
684
+ indentField.$element.detach();
685
+ if (typeof data === 'object' && data.action === 'accept') {
686
+ addons = widget.getValue();
687
+ indent = indentWidget.getValue();
688
+ storage.setObject('Wikiplus-highlight-addons', addons);
689
+ storage.setObject('Wikiplus-highlight-indent', indent);
690
+ }
691
+ });
692
+ });
693
+ if (skin === 'minerva') {
694
+ $portlet.find('a').addClass('mw-ui-icon-minerva-settings');
695
+ }
696
+
697
+ // 发送欢迎提示
698
+ if (typeof welcome === 'function') {
699
+ welcome().find('#wphl-settings-notify').click(e => {
700
+ e.preventDefault();
701
+ $('#wphl-settings').triggerHandler('click');
702
+ });
703
+ }
704
+ })();