wikiplus-highlight 2.8.1 → 2.13.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 CHANGED
@@ -8,13 +8,10 @@
8
8
  (async () => {
9
9
  'use strict';
10
10
 
11
- const version = '2.10',
12
- newAddon = 1;
11
+ const version = '2.13.1',
12
+ newAddon = 2;
13
13
 
14
- /**
15
- * polyfill for mw.storage
16
- * @type {{getObject: (key: string) => ?any, setObject: (key: string, value: any) => boolean}}
17
- */
14
+ /** @type {typeof mw.storage} */
18
15
  const storage = typeof mw.storage === 'object' && typeof mw.storage.getObject === 'function'
19
16
  ? mw.storage
20
17
  : {
@@ -30,9 +27,8 @@
30
27
  }
31
28
  },
32
29
  setObject(key, value) {
33
- let /** @type {string} */ json;
34
30
  try {
35
- json = JSON.stringify(value);
31
+ const json = JSON.stringify(value);
36
32
  return localStorage.setItem(key, json);
37
33
  } catch (e) {
38
34
  return false;
@@ -41,15 +37,22 @@
41
37
  };
42
38
  /**
43
39
  * polyfill for Object.fromEntries
44
- * @type {(entries: Iterable<[string, any]>) => Object<string, any>}
40
+ * @type {(entries: Iterable<[string, any]>) => Record<string, any>}
45
41
  */
46
42
  const fromEntries = Object.fromEntries || (entries => {
47
- const /** @type {Object<string, any>} */ obj = {};
43
+ const /** @type {Record<string, any>} */ obj = {};
48
44
  for (const [key, value] of entries) {
49
45
  obj[key] = value;
50
46
  }
51
47
  return obj;
52
48
  });
49
+ /**
50
+ * polyfill for Array.prototype.flat
51
+ * @type {function(this: any[][]): any[]}
52
+ */
53
+ const flat = Array.prototype.flat || function() {
54
+ return this.reduce((acc, cur) => acc.concat(cur), []);
55
+ };
53
56
 
54
57
  /**
55
58
  * 解析版本号
@@ -71,7 +74,6 @@
71
74
  * 获取I18N消息
72
75
  * @param {string} key 消息键,省略'wphl-'前缀
73
76
  * @param {string[]} args
74
- * @returns {string}
75
77
  */
76
78
  const msg = (key, ...args) => mw.msg(`wphl-${key}`, ...args);
77
79
  /**
@@ -79,7 +81,7 @@
79
81
  * @param {string[]} args
80
82
  */
81
83
  const notify = (...args) => () => {
82
- const /** @type {JQuery<HTMLParagraphElement>} */ $p = $('<p>', {html: msg(...args)});
84
+ const $p = $('<p>', {html: msg(...args)});
83
85
  mw.notify($p, {type: 'success', autoHideSeconds: 'long', tag: 'wikiplus-highlight'});
84
86
  return $p;
85
87
  };
@@ -90,13 +92,9 @@
90
92
  // 路径
91
93
  const CDN = '//fastly.jsdelivr.net',
92
94
  CM_CDN = 'npm/codemirror@5.65.3',
93
- MW_CDN = 'gh/bhsd-harry/codemirror-mediawiki@1.1.4',
95
+ MW_CDN = 'gh/bhsd-harry/codemirror-mediawiki@1.1.5',
94
96
  REPO_CDN = `npm/wikiplus-highlight@${majorVersion}`;
95
97
 
96
- /**
97
- * mw.config常数
98
- * @type {Object<string, string>}
99
- */
100
98
  const {
101
99
  wgPageName: page,
102
100
  wgNamespaceNumber: ns,
@@ -107,26 +105,15 @@
107
105
  skin,
108
106
  } = mw.config.values;
109
107
 
110
- /**
111
- * @typedef {object} mwConfig
112
- * @property {Object<string, string>} tagModes
113
- * @property {Object<string, boolean>} tags
114
- * @property {string} urlProtocols
115
- * @property {[Object<string, string>, Object<string, string>]} doubleUnderscore
116
- * @property {[Object<string, string>, Object<string, string>]} functionSynonyms
117
- * @property {string[]} redirect
118
- * @property {Object<string, string>} img
119
- */
120
-
121
108
  // 和本地缓存有关的常数
122
109
  const USING_LOCAL = mw.loader.getState('ext.CodeMirror') !== null,
123
- /** @type {Object<string, {time: number, config: mwConfig}>} */
110
+ /** @type {Record<string, {time: number, config: mwConfig}>} */
124
111
  ALL_SETTINGS_CACHE = storage.getObject('InPageEditMwConfig') || {},
125
112
  SITE_ID = `${server}${scriptPath}`,
126
113
  /** @type {{time: number, config: mwConfig}} */ SITE_SETTINGS = ALL_SETTINGS_CACHE[SITE_ID] || {},
127
114
  EXPIRED = SITE_SETTINGS.time < Date.now() - 86400 * 1000 * 30;
128
115
 
129
- const /** @type {Object<string, string>} */ CONTENTMODEL = {
116
+ const /** @type {Record<string, string>} */ CONTENTMODEL = {
130
117
  css: 'css',
131
118
  'sanitized-css': 'css',
132
119
  javascript: 'javascript',
@@ -134,7 +121,7 @@
134
121
  wikitext: 'mediawiki',
135
122
  };
136
123
 
137
- const /** @type {Object<string, string|[]>} */ MODE_LIST = USING_LOCAL
124
+ const MODE_LIST = USING_LOCAL
138
125
  ? {
139
126
  lib: 'ext.CodeMirror.lib',
140
127
  css: 'ext.CodeMirror.lib.mode.css',
@@ -157,27 +144,129 @@
157
144
  const ADDON_LIST = {
158
145
  searchcursor: `${CM_CDN}/addon/search/searchcursor.min.js`,
159
146
  search: `${REPO_CDN}/search.min.js`,
160
- activeLine: `${CM_CDN}/addon/selection/active-line.min.js`,
161
147
  markSelection: `${CM_CDN}/addon/selection/mark-selection.min.js`,
148
+ activeLine: `${CM_CDN}/addon/selection/active-line.min.js`,
162
149
  trailingspace: `${CM_CDN}/addon/edit/trailingspace.min.js`,
163
150
  matchBrackets: `${CM_CDN}/addon/edit/matchbrackets.min.js`,
151
+ closeBrackets: `${CM_CDN}/addon/edit/closebrackets.min.js`,
164
152
  matchTags: `${REPO_CDN}/matchtags.min.js`,
165
153
  fold: `${REPO_CDN}/fold.min.js`,
166
154
  };
167
155
 
156
+ /**
157
+ * @typedef {object} addon
158
+ * @property {string} option
159
+ * @property {string|string[]} addon
160
+ * @property {string} download
161
+ * @property {(mode: string, json: boolean) => any} complex
162
+ * @property {string[]} modes
163
+ * @property {boolean} only
164
+ */
165
+
166
+ const /** @type {addon[]} */ options = [
167
+ {option: 'styleSelectedText', addon: 'search', download: 'markSelection', only: true},
168
+ {option: 'styleActiveLine', addon: 'activeLine'},
169
+ {option: 'showTrailingSpace', addon: 'trailingspace'},
170
+ {
171
+ option: 'matchBrackets',
172
+ complex: (mode, json) => mode === 'mediawiki' || json
173
+ ? {bracketRegex: /[{}[\]]/}
174
+ : true,
175
+ },
176
+ {
177
+ option: 'autoCloseBrackets', addon: 'closeBrackets',
178
+ complex: (mode, json) => mode === 'mediawiki' || json
179
+ ? '()[]{}""'
180
+ : true,
181
+ },
182
+ {option: 'matchTags', addon: ['matchTags', 'fold'], modes: ['mediawiki', 'widget']},
183
+ {option: 'fold', modes: ['mediawiki', 'widget']},
184
+ ];
185
+
168
186
  const defaultAddons = ['search'],
169
187
  defaultIndent = '4';
170
188
  let /** @type {string[]} */ addons = storage.getObject('Wikiplus-highlight-addons') || defaultAddons,
171
189
  /** @type {string} */ indent = storage.getObject('Wikiplus-highlight-indent') || defaultIndent;
172
190
 
173
- // 用于contextMenu插件
174
- const /** @type {HTMLStyleElement} */ contextmenuStyle = document.getElementById('wphl-contextmenu')
175
- || mw.loader.addStyleTag('#Wikiplus-CodeMirror .cm-mw-template-name{cursor:pointer}');
176
- contextmenuStyle.id = 'wphl-contextmenu';
177
- contextmenuStyle.disabled = true;
191
+ /** @type {Record<string, string>} */
192
+ const entity = {'"': 'quot', "'": 'apos', '<': 'lt', '>': 'gt', '&': 'amp', ' ': 'nbsp'},
193
+ /** @type {(func: (str: string) => string) => (doc: CodeMirror.Editor) => void} */
194
+ convert = func => doc => {
195
+ doc.replaceSelection(doc.getSelection().split('\n').map(func).join('\n'), 'around');
196
+ },
197
+ escapeHTML = convert(str => str.split('').map(c => {
198
+ if (c in entity) {
199
+ return `&${entity[c]};`;
200
+ }
201
+ const code = c.charCodeAt();
202
+ return code < 256 ? `&#${code};` : `&#x${code.toString(16)};`;
203
+ }).join('')),
204
+ /** @type {function(typeof CodeMirror): boolean} */ isPc = ({keyMap}) => keyMap.default === keyMap.pcDefault,
205
+ extraKeysPc = {'Ctrl-/': escapeHTML, 'Ctrl-\\': convert(encodeURIComponent)},
206
+ extraKeysMac = {'Cmd-/': escapeHTML, 'Cmd-\\': convert(encodeURIComponent)};
207
+
208
+ /**
209
+ * contextMenu插件
210
+ * @param {CodeMirror.Editor} doc
211
+ * @param {string} mode
212
+ */
213
+ const handleContextMenu = async (doc, mode) => {
214
+ if (!['mediawiki', 'widget'].includes(mode) || !addons.includes('contextmenu')) {
215
+ return;
216
+ }
217
+ const $wrapper = $(doc.getWrapperElement()).addClass('CodeMirror-contextmenu'),
218
+ {functionSynonyms: [synonyms]} = mw.config.get('extCodeMirrorConfig') || {
219
+ functionSynonyms: [{
220
+ invoke: 'invoke',
221
+ 调用: 'invoke',
222
+ widget: 'widget',
223
+ 小工具: 'widget',
224
+ }],
225
+ };
226
+ /** @param {string} str */
227
+ const getSysnonyms = str => Object.keys(synonyms).filter(key => synonyms[key] === str)
228
+ .map(key => key.startsWith('#') ? key : `#${key}`);
229
+ const invoke = getSysnonyms('invoke'),
230
+ widget = getSysnonyms('widget');
231
+
232
+ await mw.loader.using('mediawiki.Title');
233
+ $wrapper.contextmenu(({pageX, pageY}) => {
234
+ const pos = doc.coordsChar({left: pageX, top: pageY}),
235
+ {line, ch} = pos,
236
+ type = doc.getTokenTypeAt(pos);
237
+ if (!/\bmw-(?:template-name|parserfunction)\b/.test(type)) {
238
+ return;
239
+ }
240
+ const tokens = doc.getLineTokens(line),
241
+ index = tokens.findIndex(({start, end}) => start < ch && end >= ch),
242
+ text = tokens[index].string.replace(/\u200e/g, '').replace(/_/g, ' ').trim();
243
+ if (/\bmw-template-name\b/.test(type)) {
244
+ const title = new mw.Title(text);
245
+ if (title.namespace !== 0 || text.startsWith(':')) {
246
+ open(title.getUrl(), '_blank');
247
+ } else {
248
+ open(mw.util.getUrl(`Template:${text}`), '_blank');
249
+ }
250
+ return false;
251
+ } else if (index < 2 || !/\bmw-parserfunction-delimiter\b/.test(tokens[index - 1].type)
252
+ || !/\bmw-parserfunction-name\b/.test(tokens[index - 2].type)
253
+ ) {
254
+ return;
255
+ }
256
+ const parserFunction = tokens[index - 2].string.trim().toLowerCase();
257
+ if (invoke.includes(parserFunction)) {
258
+ open(mw.util.getUrl(`Module:${text}`), '_blank');
259
+ } else if (widget.includes(parserFunction)) {
260
+ open(mw.util.getUrl(`Widget:${text}`, {action: 'edit'}), '_blank');
261
+ } else {
262
+ return;
263
+ }
264
+ return false;
265
+ });
266
+ };
178
267
 
179
- let /** @type {Object<string, string>} */ i18n = storage.getObject('Wikiplus-highlight-i18n'),
180
- /** @type {() => JQuery<HTMLParagraphElement>} */ welcome;
268
+ let /** @type {Record<string, string>} */ i18n = storage.getObject('Wikiplus-highlight-i18n'),
269
+ /** @type {() => JQuery<HTMLElement>} */ welcome;
181
270
  if (!i18n) { // 首次安装
182
271
  i18n = {};
183
272
  welcome = notify('welcome');
@@ -185,7 +274,7 @@
185
274
  welcome = notify(`welcome-${newAddon ? 'new-addon' : 'upgrade'}`, version, newAddon);
186
275
  }
187
276
 
188
- const /** @type {Object<string, string>} */ i18nLanguages = {
277
+ const /** @type {Record<string, string>} */ i18nLanguages = {
189
278
  zh: 'zh-hans', 'zh-hans': 'zh-hans', 'zh-cn': 'zh-hans', 'zh-my': 'zh-hans', 'zh-sg': 'zh-hans',
190
279
  'zh-hant': 'zh-hant', 'zh-tw': 'zh-hant', 'zh-hk': 'zh-hant', 'zh-mo': 'zh-hant',
191
280
  },
@@ -205,7 +294,7 @@
205
294
  mw.messages.set(i18n);
206
295
  };
207
296
 
208
- const /** @type {Promise<[void, void]>} */ i18nPromise = Promise.all([ // 提前加载I18N
297
+ const i18nPromise = Promise.all([ // 提前加载I18N
209
298
  mw.loader.using('mediawiki.util'),
210
299
  setI18N(),
211
300
  ]);
@@ -214,7 +303,6 @@
214
303
  * 下载脚本
215
304
  * @param {string[]} urls 脚本路径
216
305
  * @param {boolean} local 是否从本地下载
217
- * @returns {Promise<void>}
218
306
  */
219
307
  const getScript = (urls, local) => {
220
308
  if (urls.length === 0) {
@@ -229,7 +317,26 @@
229
317
  };
230
318
 
231
319
  // 以下进入CodeMirror相关内容
232
- let /** @type {CodeMirror.Editor} */ cm;
320
+ let /** @type {CodeMirror.EditorFromTextArea} */ cm;
321
+
322
+ /** @param {typeof CodeMirror} CM */
323
+ const getAddonScript = (CM, other = false) => {
324
+ const /** @type {string[]} */ addonScript = [];
325
+ for (const {option, addon = option, download = Array.isArray(addon) ? option : addon, only} of options) {
326
+ if (!(only && other) && !(option in CM.optionHandlers) && intersect(addon, addons)) {
327
+ addonScript.push(ADDON_LIST[download]);
328
+ }
329
+ }
330
+ return addonScript;
331
+ };
332
+
333
+ /**
334
+ * @param {array|any} arr1
335
+ * @param {array} arr2
336
+ */
337
+ const intersect = (arr1, arr2) => Array.isArray(arr1)
338
+ ? arr1.some(ele => arr2.includes(ele))
339
+ : arr2.includes(arr1);
233
340
 
234
341
  /**
235
342
  * 根据文本的高亮模式加载依赖项
@@ -243,7 +350,7 @@
243
350
 
244
351
  /**
245
352
  * 代替CodeMirror的局部变量
246
- * @type {CodeMirror}
353
+ * @type {typeof CodeMirror}
247
354
  */
248
355
  const CM = loaded
249
356
  ? window.CodeMirror
@@ -259,7 +366,7 @@
259
366
  mw.loader.load(`${CDN}/${MW_CDN}/mediawiki.min.css`, 'text/css');
260
367
  (USING_LOCAL ? externalScript : scripts).push(`${MW_CDN}/mediawiki.min.js`);
261
368
  }
262
- if (type === 'mediawiki' && typeof SITE_SETTINGS.config === 'object' && SITE_SETTINGS.config.tags.html) {
369
+ if (type === 'mediawiki' && SITE_SETTINGS.config && SITE_SETTINGS.config.tags.html) {
263
370
  // NamespaceHTML扩展自由度过高,所以这里一律当作允许<html>标签
264
371
  type = 'html'; // eslint-disable-line no-param-reassign
265
372
  }
@@ -275,24 +382,7 @@
275
382
  if (!CM.commands.findForward && addons.includes('search')) {
276
383
  addonScript.push(ADDON_LIST.search);
277
384
  }
278
- if (!CM.optionHandlers.styleActiveLine && addons.includes('activeLine')) {
279
- addonScript.push(ADDON_LIST.activeLine);
280
- }
281
- if (!CM.optionHandlers.styleSelectedText && addons.includes('search')) {
282
- addonScript.push(ADDON_LIST.markSelection);
283
- }
284
- if (!CM.optionHandlers.showTrailingSpace && addons.includes('trailingspace')) {
285
- addonScript.push(ADDON_LIST.trailingspace);
286
- }
287
- if (!CM.optionHandlers.matchBrackets && addons.includes('matchBrackets')) {
288
- addonScript.push(ADDON_LIST.matchBrackets);
289
- }
290
- if (!CM.optionHandlers.matchTags && ['matchTags', 'fold'].some(addon => addons.includes(addon))) {
291
- addonScript.push(ADDON_LIST.matchTags);
292
- }
293
- if (!CM.optionHandlers.fold && addons.includes('fold')) {
294
- addonScript.push(ADDON_LIST.fold);
295
- }
385
+ addonScript.push(...getAddonScript(CM));
296
386
  if (['widget', 'html'].includes(type)) {
297
387
  ['css', 'javascript', 'mediawiki', 'htmlmixed', 'xml'].forEach(lang => {
298
388
  if (!CM.modes[lang]) {
@@ -351,32 +441,35 @@
351
441
  await initModePromise;
352
442
  }
353
443
 
354
- let /** @type {mwConfig} */ config = mw.config.get('extCodeMirrorConfig');
444
+ let config = mw.config.get('extCodeMirrorConfig');
355
445
  if (!config && !EXPIRED && isLatest) {
356
446
  ({config} = SITE_SETTINGS);
447
+ if (config.tags.ref) { // fix a bug in InPageEdit-v2
448
+ config.tagModes.ref = 'text/mediawiki';
449
+ }
357
450
  mw.config.set('extCodeMirrorConfig', config);
358
451
  }
359
452
  if (config && config.redirect && config.img) { // 情形1:config已更新,可能来自localStorage
360
453
  return config;
361
- } else if (config) { // FIXME: 暂不需要redirect和img相关设置
454
+ } else if (config) { /** @todo 暂不需要redirect和img相关设置 */
362
455
  return config;
363
456
  }
364
457
 
365
458
  /**
366
- * @typedef {object} siteInfoQuery
459
+ * @typedef {object} ApiSiteInfoQuery
367
460
  * @property {{name: string, aliases: string[], 'case-sensitive': boolean}[]} magicwords
368
461
  * @property {string[]} extensiontags
369
462
  * @property {string[]} functionhooks
370
463
  * @property {string[]} variables
371
464
  */
372
465
 
373
- /**
466
+ /*
374
467
  * 以下情形均需要发送API请求
375
468
  * 情形2:localStorage未过期但不包含新设置
376
469
  * 情形3:新加载的 ext.CodeMirror.data
377
470
  * 情形4:config === null
378
471
  */
379
- const /** @type {{query: siteInfoQuery}} */ {
472
+ const /** @type {{query: ApiSiteInfoQuery}} */ {
380
473
  query: {magicwords, extensiontags, functionhooks, variables},
381
474
  } = await new mw.Api().get({
382
475
  meta: 'siteinfo',
@@ -389,13 +482,12 @@
389
482
  * @param {{aliases: string[], name: string}[]} words
390
483
  * @returns {{alias: string, name: string}[]}
391
484
  */
392
- const getAliases = words => words.flatMap(
393
- /** @param {{aliases: string[], name: string}} */
394
- ({aliases, name}) => aliases.map(alias => ({alias, name})),
485
+ const getAliases = words => flat.call(
486
+ words.map(({aliases, name}) => aliases.map(alias => ({alias, name}))),
395
487
  );
396
488
  /**
397
489
  * @param {{alias: string, name: string}[]} aliases
398
- * @returns {Object<string, string>}
490
+ * @returns {Record<string, string>}
399
491
  */
400
492
  const getConfig = aliases => fromEntries(
401
493
  aliases.map(({alias, name}) => [alias.replace(/:$/, ''), name]),
@@ -413,10 +505,8 @@
413
505
  ),
414
506
  urlProtocols: mw.config.get('wgUrlProtocols'),
415
507
  };
416
- /** @type {Set<string>} */
417
508
  const realMagicwords = new Set([...functionhooks, ...variables, ...otherMagicwords]),
418
509
  allMagicwords = magicwords.filter(
419
- /** @returns {boolean} */
420
510
  ({name, aliases}) => aliases.some(alias => /^__.+__$/.test(alias)) || realMagicwords.has(name),
421
511
  ),
422
512
  sensitive = getAliases(
@@ -437,15 +527,15 @@
437
527
  const {functionSynonyms: [insensitive]} = config;
438
528
  if (!insensitive.subst) {
439
529
  getAliases(
440
- magicwords.filter(/** @return {boolean} */ ({name}) => otherMagicwords.includes(name)),
530
+ magicwords.filter(({name}) => otherMagicwords.includes(name)),
441
531
  ).forEach(({alias, name}) => {
442
532
  insensitive[alias.replace(/:$/, '')] = name;
443
533
  });
444
534
  }
445
535
  }
446
- config.redirect = magicwords.find(/** @param {{name: string}} */ ({name}) => name === 'redirect').aliases;
536
+ config.redirect = magicwords.find(({name}) => name === 'redirect').aliases;
447
537
  config.img = getConfig(
448
- getAliases(magicwords.filter(/** @returns {boolean} */ ({name}) => name.startsWith('img_'))),
538
+ getAliases(magicwords.filter(({name}) => name.startsWith('img_'))),
449
539
  );
450
540
  mw.config.set('extCodeMirrorConfig', config);
451
541
  updateCachedConfig(config);
@@ -457,7 +547,7 @@
457
547
  if ([274, 828].includes(ns) && !page.endsWith('/doc')) {
458
548
  const pageMode = ns === 274 ? 'Widget' : 'Lua';
459
549
  await mw.loader.using(['oojs-ui-windows', 'oojs-ui.styles.icons-content']);
460
- const /** @type {boolean} */ bool = await OO.ui.confirm(msg('contentmodel'), {
550
+ const bool = await OO.ui.confirm(msg('contentmodel'), {
461
551
  actions: [
462
552
  {label: pageMode},
463
553
  {label: 'Wikitext', action: 'accept'},
@@ -478,7 +568,7 @@
478
568
  const renderEditor = async ($target, setting) => {
479
569
  const mode = setting ? 'javascript' : await getPageMode();
480
570
  const initModePromise = initMode(mode);
481
- const /** @type {[mwConfig]} */ [mwConfig] = await Promise.all([
571
+ const [mwConfig] = await Promise.all([
482
572
  getMwConfig(mode, initModePromise),
483
573
  initModePromise,
484
574
  ]);
@@ -503,7 +593,7 @@
503
593
  }
504
594
 
505
595
  const json = setting || contentmodel === 'json',
506
- /** @type {{name: string}} */ {name} = $.client.profile();
596
+ {name} = $.client.profile();
507
597
  cm = CodeMirror.fromTextArea($target[0], $.extend({
508
598
  inputStyle: name === 'safari' ? 'textarea' : 'contenteditable',
509
599
  lineNumbers: true,
@@ -511,17 +601,15 @@
511
601
  mode,
512
602
  mwConfig,
513
603
  json,
514
- styleActiveLine: addons.includes('activeLine'),
515
- styleSelectedText: addons.includes('search'),
516
- showTrailingSpace: addons.includes('trailingspace'),
517
- matchBrackets: addons.includes('matchBrackets') && (mode === 'mediawiki' || json
518
- ? {bracketRegex: /[{}[\]]/}
519
- : true
520
- ),
521
- matchTags: addons.includes('matchTags') && ['mediawiki', 'widget'].includes(mode),
522
- fold: addons.includes('fold') && ['mediawiki', 'widget'].includes(mode),
523
- }, mode === 'mediawiki'
524
- ? {}
604
+ }, fromEntries(
605
+ options.map(({option, addon = option, modes, complex = mod => !modes || modes.includes(mod)}) => {
606
+ const mainAddon = Array.isArray(addon) ? addon[0] : addon;
607
+ return [option, addons.includes(mainAddon) && complex(mode, json)];
608
+ }),
609
+ ), mode === 'mediawiki'
610
+ ? {
611
+ extraKeys: addons.includes('escape') && (isPc(CodeMirror) ? extraKeysPc : extraKeysMac),
612
+ }
525
613
  : {
526
614
  indentUnit: addons.includes('indentWithSpace') ? indent : defaultIndent,
527
615
  indentWithTabs: !addons.includes('indentWithSpace'),
@@ -530,44 +618,7 @@
530
618
  cm.setSize(null, height);
531
619
  cm.refresh();
532
620
 
533
- const wrapper = cm.getWrapperElement();
534
- wrapper.id = 'Wikiplus-CodeMirror';
535
- if (['mediawiki', 'widget'].includes(mode) && addons.includes('contextmenu')) {
536
- contextmenuStyle.disabled = false;
537
- const /** @type {mwConfig} */ {functionSynonyms: [synonyms]} = mw.config.get('extCodeMirrorConfig');
538
- /** @param {string} str */
539
- const getSysnonyms = str => Object.keys(synonyms).filter(key => synonyms[key] === str)
540
- .map(key => key.startsWith('#') ? key : `#${key}`);
541
- const invoke = getSysnonyms('invoke'),
542
- widget = getSysnonyms('widget');
543
-
544
- await mw.loader.using('mediawiki.Title');
545
- $(wrapper).on('contextmenu', '.cm-mw-template-name', function() {
546
- const /** @type {string} */ text = this.textContent.replace(/\u200e/g, '').trim(),
547
- /** @type {{namespace: number, getUrl: () => string}} */ title = new mw.Title(text);
548
- if (title.namespace !== 0 || text.startsWith(':')) {
549
- open(title.getUrl(), '_blank');
550
- } else {
551
- open(mw.util.getUrl(`Template:${text}`), '_blank');
552
- }
553
- return false;
554
- }).on(
555
- 'contextmenu',
556
- '.cm-mw-parserfunction-name + .cm-mw-parserfunction-delimiter + .cm-mw-parserfunction',
557
- function() {
558
- /** @type {string} */
559
- const parserFunction = this.previousSibling.previousSibling.textContent.trim().toLowerCase();
560
- if (invoke.includes(parserFunction)) {
561
- open(mw.util.getUrl(`Module:${this.textContent}`), '_blank');
562
- } else if (widget.includes(parserFunction)) {
563
- open(mw.util.getUrl(`Widget:${this.textContent}`, {action: 'edit'}), '_blank');
564
- }
565
- return false;
566
- },
567
- );
568
- } else {
569
- contextmenuStyle.disabled = true;
570
- }
621
+ handleContextMenu(cm, mode);
571
622
 
572
623
  $('#Wikiplus-Quickedit-Jump').children('a').attr('href', '#Wikiplus-CodeMirror');
573
624
 
@@ -579,18 +630,17 @@
579
630
  $('#Wikiplus-Quickedit-MinorEdit').click();
580
631
  $('#Wikiplus-Quickedit-Submit').triggerHandler('click');
581
632
  };
582
- cm.addKeyMap($.extend({
583
- 'Ctrl-S': submit,
584
- 'Cmd-S': submit,
585
- 'Shift-Ctrl-S': submitMinor,
586
- 'Shift-Cmd-S': submitMinor,
587
- }, Wikiplus.getSetting('esc_to_exit_quickedit')
588
- ? {
589
- Esc() {
590
- $('#Wikiplus-Quickedit-Back').triggerHandler('click');
591
- },
592
- }
593
- : {},
633
+ cm.addKeyMap($.extend(
634
+ isPc(CodeMirror)
635
+ ? {'Ctrl-S': submit, 'Shift-Ctrl-S': submitMinor}
636
+ : {'Cmd-S': submit, 'Shift-Cmd-S': submitMinor},
637
+ Wikiplus.getSetting('esc_to_exit_quickedit')
638
+ ? {
639
+ Esc() {
640
+ $('#Wikiplus-Quickedit-Back').triggerHandler('click');
641
+ },
642
+ }
643
+ : {},
594
644
  ));
595
645
  }
596
646
 
@@ -599,12 +649,8 @@
599
649
 
600
650
  // 监视 Wikiplus 编辑框
601
651
  const observer = new MutationObserver(records => {
602
- const $editArea = $(records.flatMap(
603
- /**
604
- * @param {{addedNodes: NodeList}}
605
- * @returns {Node[]}
606
- */
607
- ({addedNodes}) => [...addedNodes],
652
+ const $editArea = $(flat.call(
653
+ records.map(({addedNodes}) => [...addedNodes]),
608
654
  )).find('#Wikiplus-Quickedit, #Wikiplus-Setting-Input');
609
655
  if ($editArea.length === 0) {
610
656
  return;
@@ -614,7 +660,7 @@
614
660
  observer.observe(document.body, {childList: true});
615
661
 
616
662
  // 添加样式
617
- const /** @type {HTMLStyleElement} */ wphlStyle = document.getElementById('wphl-style') || mw.loader.addStyleTag(
663
+ const wphlStyle = document.getElementById('wphl-style') || mw.loader.addStyleTag(
618
664
  '#Wikiplus-Quickedit+.CodeMirror,#Wikiplus-Setting-Input+.CodeMirror'
619
665
  + '{border:1px solid #c8ccd1;line-height:1.3;clear:both}'
620
666
  + 'div.Wikiplus-InterBox{font-size:14px;z-index:100}'
@@ -623,7 +669,8 @@
623
669
  + 'div.CodeMirror span.CodeMirror-matchingbracket{box-shadow:0 0 0 2px #9aef98}'
624
670
  + 'div.CodeMirror span.CodeMirror-nonmatchingbracket{box-shadow:0 0 0 2px #eace64}'
625
671
  + '#Wikiplus-highlight-dialog .oo-ui-messageDialog-title{margin-bottom:0.28571429em}'
626
- + '#Wikiplus-highlight-dialog .oo-ui-flaggedElement-notice{font-weight:normal;margin:0}',
672
+ + '#Wikiplus-highlight-dialog .oo-ui-flaggedElement-notice{font-weight:normal;margin:0}'
673
+ + '.CodeMirror-contextmenu .cm-mw-template-name{cursor:pointer}',
627
674
  );
628
675
  wphlStyle.id = 'wphl-style';
629
676
 
@@ -639,10 +686,7 @@
639
686
  elem.value = value;
640
687
  },
641
688
  } = $.valHooks.textarea || {};
642
- /**
643
- * @param {HTMLTextAreaElement} elem
644
- * @returns {boolean}
645
- */
689
+ /** @param {HTMLTextAreaElement} elem */
646
690
  const isWikiplus = elem => ['Wikiplus-Quickedit', 'Wikiplus-Setting-Input'].includes(elem.id);
647
691
  $.valHooks.textarea = {
648
692
  get(elem) {
@@ -659,21 +703,12 @@
659
703
 
660
704
  await i18nPromise; // 以下内容依赖I18N
661
705
 
662
- /**
663
- * @typedef {object} OOUI.widget
664
- * @property {(data: any) => {closing: Promise<{action: string}>}} open
665
- * @property {() => string|string[]} getValue
666
- * @property {JQuery<HTMLDivElement>} $element
667
- * @property {(show: boolean) => OOUI.widget} toggle
668
- * @property {(windows: OOUI.widget[]) => void} addWindows
669
- */
670
-
671
706
  // 设置对话框
672
- let /** @type {OOUI.widget} */ dialog,
673
- /** @type {OOUI.widget} */ widget,
674
- /** @type {OOUI.widget} */ indentWidget,
675
- /** @type {OOUI.widget} */ field,
676
- /** @type {OOUI.widget} */ indentField;
707
+ let /** @type {OOUI.MessageDialog} */ dialog,
708
+ /** @type {OOUI.CheckboxMultiselectInputWidget} */ widget,
709
+ /** @type {OOUI.NumberInputWidget} */ indentWidget,
710
+ /** @type {OOUI.FieldLayout} */ field,
711
+ /** @type {OOUI.FieldLayout} */ indentField;
677
712
  const toggleIndent = (value = addons) => {
678
713
  indentField.toggle(value.includes('indentWithSpace'));
679
714
  };
@@ -681,7 +716,7 @@
681
716
  minerva: 'page-actions-overflow',
682
717
  citizen: 'p-actions',
683
718
  };
684
- const /** @type {JQuery<HTMLLIElement} */ $portlet = $(mw.util.addPortletLink(
719
+ const $portlet = $(mw.util.addPortletLink(
685
720
  portletContainer[skin] || 'p-cactions', '#', msg('portlet'), 'wphl-settings',
686
721
  )).click(async e => {
687
722
  e.preventDefault();
@@ -689,19 +724,17 @@
689
724
  await mw.loader.using(['oojs-ui-windows', 'oojs-ui.styles.icons-content']);
690
725
  // eslint-disable-next-line require-atomic-updates
691
726
  dialog = new OO.ui.MessageDialog({id: 'Wikiplus-highlight-dialog'});
692
- const /** @type {OOUI.widget} */ windowManager = new OO.ui.WindowManager();
727
+ const windowManager = new OO.ui.WindowManager();
693
728
  windowManager.$element.appendTo(document.body);
694
729
  windowManager.addWindows([dialog]);
695
730
  widget = new OO.ui.CheckboxMultiselectInputWidget({
696
731
  options: [
697
- {data: 'search', label: msg('addon-search')},
698
- {data: 'activeLine', label: msg('addon-active-line')},
699
- {data: 'trailingspace', label: msg('addon-trailingspace')},
700
- {data: 'matchBrackets', label: msg('addon-matchbrackets')},
701
- {data: 'matchTags', label: msg('addon-matchtags')},
702
- {data: 'fold', label: msg('addon-fold')},
703
- {data: 'contextmenu', label: msg('addon-contextmenu')},
704
- {data: 'indentWithSpace', label: msg('addon-indentwithspace')},
732
+ ...options.map(({option, addon = option}) => {
733
+ const mainAddon = Array.isArray(addon) ? addon[0] : addon;
734
+ return {data: mainAddon, label: msg(`addon-${mainAddon.toLowerCase()}`)};
735
+ }),
736
+ ...['escape', 'contextmenu', 'indentWithSpace', 'otherEditors']
737
+ .map(addon => ({data: addon, label: msg(`addon-${addon.toLowerCase()}`)})),
705
738
  ],
706
739
  value: addons,
707
740
  }).on('change', toggleIndent);
@@ -714,6 +747,9 @@
714
747
  indentField = new OO.ui.FieldLayout(indentWidget, {label: msg('addon-indent')});
715
748
  toggleIndent();
716
749
  }
750
+ const wikiplusLoaded = typeof window.Wikiplus === 'object';
751
+ widget.$element.find('.oo-ui-checkboxInputWidget').first().toggleClass('oo-ui-widget-enabled', wikiplusLoaded)
752
+ .children('input').prop('disabled', !wikiplusLoaded);
717
753
  dialog.open({
718
754
  title: msg('addon-title'),
719
755
  message: field.$element.add(indentField.$element).add(
@@ -746,4 +782,36 @@
746
782
  $('#wphl-settings').triggerHandler('click');
747
783
  });
748
784
  }
785
+
786
+ /** @param {CodeMirror.Editor} doc */
787
+ const handleOtherEditors = async doc => {
788
+ if (!addons.includes('otherEditors')) {
789
+ return;
790
+ }
791
+ let mode = doc.getOption('mode');
792
+ mode = mode === 'text/mediawiki' ? 'mediawiki' : mode;
793
+ const addonScript = getAddonScript(CodeMirror, true),
794
+ json = doc.getOption('json');
795
+ await getScript(addonScript);
796
+ for (const {
797
+ option, addon = option, modes, complex = (/** @type {string} */ mod) => !modes || modes.includes(mod),
798
+ } of options.filter(({only}) => !only)) {
799
+ const mainAddon = Array.isArray(addon) ? addon[0] : addon;
800
+ if (doc.getOption(option) === undefined && addons.includes(mainAddon)) {
801
+ doc.setOption(option, complex(mode, json));
802
+ }
803
+ }
804
+ if (mode !== 'mediawiki' && addons.includes('indentWithSpace')) {
805
+ doc.setOption('indentUnit', indent);
806
+ doc.setOption('indentWithTabs', false);
807
+ } else if (mode === 'mediawiki' && addons.includes('escape')) {
808
+ doc.addKeyMap(isPc(CodeMirror) ? extraKeysPc : extraKeysMac, true);
809
+ }
810
+ handleContextMenu(doc, mode);
811
+ };
812
+
813
+ mw.hook('InPageEdit.quickEdit.codemirror').add(
814
+ /** @param {{cm: CodeMirror.Editor}} */ ({cm: doc}) => handleOtherEditors(doc),
815
+ );
816
+ mw.hook('inspector').add(/** @param {CodeMirror.Editor} doc */ doc => handleOtherEditors(doc));
749
817
  })();