wikiplus-highlight 2.20.0 → 2.22.2

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
@@ -13,13 +13,14 @@
13
13
  }
14
14
  mw.libs.wphl = {}; // 开始加载
15
15
 
16
- const version = '2.20',
16
+ const version = '2.22.2',
17
17
  newAddon = 0;
18
18
 
19
19
  /** @type {typeof mw.storage} */
20
20
  const storage = typeof mw.storage === 'object' && typeof mw.storage.getObject === 'function'
21
21
  ? mw.storage
22
22
  : {
23
+ /** @override */
23
24
  getObject(key) {
24
25
  const json = localStorage.getItem(key);
25
26
  if (json === false) {
@@ -31,6 +32,7 @@
31
32
  return null;
32
33
  }
33
34
  },
35
+ /** @override */
34
36
  setObject(key, value) {
35
37
  try {
36
38
  return localStorage.setItem(key, JSON.stringify(value));
@@ -39,18 +41,20 @@
39
41
  }
40
42
  },
41
43
  };
44
+
42
45
  /**
43
46
  * polyfill for `Object.fromEntries`
44
47
  * @type {(entries: Iterable<[string, T]>) => Record<string, T>}
45
48
  * @template T
46
49
  */
47
- const fromEntries = Object.fromEntries || (entries => {
50
+ const fromEntries = Object.fromEntries || (entries => { // eslint-disable-line es-x/no-object-fromentries
48
51
  const /** @type {Record<string, T>} */ obj = {};
49
52
  for (const [key, value] of entries) {
50
53
  obj[key] = value;
51
54
  }
52
55
  return obj;
53
56
  });
57
+
54
58
  /**
55
59
  * polyfill for `Array.prototype.flat`
56
60
  * @type {(arr: HTMLElement[][]) => HTMLElement[]}
@@ -59,16 +63,21 @@
59
63
  if (typeof arr.flat === 'function') {
60
64
  return arr.flat();
61
65
  }
66
+ // eslint-disable-next-line unicorn/no-array-reduce
62
67
  return arr.reduce((acc, cur) => acc.concat(cur), []);
63
68
  };
64
69
 
65
- /** 解析版本号 */
66
- const getVersion = (str = version) => str.split('.').map(s => Number(s));
70
+ /**
71
+ * 解析版本号
72
+ * @param {string} str 版本号
73
+ */
74
+ const getVersion = (str = version) => str.split('.').map(Number);
75
+
67
76
  /**
68
77
  * 比较版本号
69
- * @param {string} a
70
- * @param {string} b
71
- * @returns `a`的版本号是否小于`b`的版本号
78
+ * @param {string} a 版本号1
79
+ * @param {string} b 版本号2
80
+ * @returns {boolean} `a`的版本号是否小于`b`的版本号
72
81
  */
73
82
  const cmpVersion = (a, b) => {
74
83
  const [a0, a1] = getVersion(a),
@@ -79,18 +88,19 @@
79
88
  /**
80
89
  * 获取I18N消息
81
90
  * @param {string} key 消息键,省略`wphl-`前缀
82
- * @param {string[]} args
91
+ * @param {string[]} args 替换`$1`等的参数
83
92
  */
84
93
  const msg = (key, ...args) => mw.msg(`wphl-${key}`, ...args);
94
+
85
95
  /**
86
96
  * 生成JQuery的I18N消息
87
- * @param {string[]} args
97
+ * @param {string[]} args 替换`$1`等的参数
88
98
  */
89
99
  const htmlMsg = (...args) => $($.parseHTML(msg(...args)));
90
100
 
91
101
  /**
92
102
  * 提示消息
93
- * @param {string[]} args
103
+ * @param {string[]} args 替换`$1`等的参数
94
104
  */
95
105
  const notify = (...args) => () => {
96
106
  const $p = $('<p>', {html: msg(...args)});
@@ -104,10 +114,10 @@
104
114
  // 路径
105
115
  const CDN = '//fastly.jsdelivr.net',
106
116
  CM_CDN = 'npm/codemirror@5.65.3',
107
- MW_CDN = 'gh/bhsd-harry/codemirror-mediawiki@1.1.5',
117
+ MW_CDN = 'gh/bhsd-harry/codemirror-mediawiki@1.1.6',
108
118
  REPO_CDN = `npm/wikiplus-highlight@${majorVersion}`;
109
119
 
110
- const {
120
+ const {config: {values: {
111
121
  wgPageName: page,
112
122
  wgNamespaceNumber: ns,
113
123
  wgPageContentModel: contentmodel,
@@ -115,7 +125,7 @@
115
125
  wgScriptPath: scriptPath,
116
126
  wgUserLanguage: userLang,
117
127
  skin,
118
- } = mw.config.values;
128
+ }}} = mw;
119
129
 
120
130
  // 和本地缓存有关的常数
121
131
  const USING_LOCAL = mw.loader.getState('ext.CodeMirror') !== null,
@@ -169,30 +179,34 @@
169
179
 
170
180
  /**
171
181
  * @typedef {object} addon
172
- * @property {string} option
173
- * @property {string|string[]} addon
174
- * @property {string} download
175
- * @property {(mode: string, json: boolean) => any} complex
176
- * @property {string[]} modes
177
- * @property {boolean} only
182
+ * @property {string} option 对应的CodeMirror选项
183
+ * @property {string|string[]} addon 对应的Wikiplus-highlight插件
184
+ * @property {string} download 需要下载的CodeMirror扩展
185
+ * @property {(mode: string, json: boolean) => any} complex 插件加载条件
186
+ * @property {string[]} modes 使用的高亮模式
187
+ * @property {boolean} only 是否仅用于Wikiplus
178
188
  */
179
189
 
180
190
  const /** @type {addon[]} */ options = [
181
191
  {
182
- option: 'styleSelectedText', addon: 'search', download: 'markSelection', only: true,
183
- complex: () => !addons.has('wikiEditor'),
192
+ option: 'styleSelectedText',
193
+ addon: 'search',
194
+ download: 'markSelection',
195
+ only: true,
196
+ /** @implements */ complex: () => !addons.has('wikiEditor'),
184
197
  },
185
198
  {option: 'styleActiveLine', addon: 'activeLine'},
186
199
  {option: 'showTrailingSpace', addon: 'trailingspace'},
187
200
  {
188
201
  option: 'matchBrackets',
189
- complex: (mode, json) => mode === 'mediawiki' || json
190
- ? {bracketRegex: /[{}[\]]/}
202
+ /** @implements */ complex: (mode, json) => mode === 'mediawiki' || json
203
+ ? {bracketRegex: /[{}[\]]/u}
191
204
  : true,
192
205
  },
193
206
  {
194
- option: 'autoCloseBrackets', addon: 'closeBrackets',
195
- complex: (mode, json) => mode === 'mediawiki' || json
207
+ option: 'autoCloseBrackets',
208
+ addon: 'closeBrackets',
209
+ /** @implements */ complex: (mode, json) => mode === 'mediawiki' || json
196
210
  ? '()[]{}""'
197
211
  : true,
198
212
  },
@@ -211,11 +225,11 @@
211
225
  convert = func => doc => {
212
226
  doc.replaceSelection(doc.getSelection().split('\n').map(func).join('\n'), 'around');
213
227
  },
214
- escapeHTML = convert(str => str.split('').map(c => {
228
+ escapeHTML = convert(str => [...str].map(c => {
215
229
  if (c in entity) {
216
230
  return `&${entity[c]};`;
217
231
  }
218
- const code = c.charCodeAt();
232
+ const code = c.codePointAt();
219
233
  return code < 256 ? `&#${code};` : `&#x${code.toString(16)};`;
220
234
  }).join('')),
221
235
  /** @type {function(typeof CodeMirror): boolean} */ isPc = ({keyMap}) => keyMap.default === keyMap.pcDefault,
@@ -224,18 +238,22 @@
224
238
 
225
239
  /**
226
240
  * contextMenu插件
227
- * @param {CodeMirror.Editor} doc
228
- * @param {string} mode
241
+ * @param {CodeMirror.Editor} doc CodeMirror编辑区
242
+ * @param {string} mode 高亮模式
229
243
  */
230
244
  const handleContextMenu = (doc, mode) => {
231
- if (!['mediawiki', 'widget'].includes(mode) || !addons.has('contextmenu')) {
245
+ if (mode !== 'mediawiki' && mode !== 'widget' || !addons.has('contextmenu')) {
232
246
  return;
233
247
  }
234
248
  const $wrapper = $(doc.getWrapperElement()).addClass('CodeMirror-contextmenu'),
235
249
  {functionSynonyms: [synonyms]} = mw.config.get('extCodeMirrorConfig') || {
236
250
  functionSynonyms: [{invoke: 'invoke', 调用: 'invoke', widget: 'widget', 小工具: 'widget'}],
237
251
  };
238
- /** @param {string} str */
252
+
253
+ /**
254
+ * 生成别名映射表
255
+ * @param {string} str 别名
256
+ */
239
257
  const getSysnonyms = str => Object.keys(synonyms).filter(key => synonyms[key] === str)
240
258
  .map(key => key.startsWith('#') ? key : `#${key}`);
241
259
  const invoke = getSysnonyms('invoke'),
@@ -245,20 +263,21 @@
245
263
  const pos = doc.coordsChar({left: pageX, top: pageY}),
246
264
  {line, ch} = pos,
247
265
  curType = doc.getTokenTypeAt(pos);
248
- if (!/\bmw-(?:template-name|parserfunction)\b/.test(curType)) {
249
- return;
266
+ if (!/\bmw-(?:template-name|parserfunction)\b/u.test(curType)) {
267
+ return undefined;
250
268
  }
251
269
  const tokens = doc.getLineTokens(line);
252
- for (const [i, {type, end, string}] of [...tokens.entries()].reverse()) {
253
- if (i > 0 && tokens[i - 1].type === type) {
270
+ for (let i = tokens.length - 1; i > 0; i--) {
271
+ const {type, end, string} = tokens[i];
272
+ if (tokens[i - 1].type === type) {
254
273
  tokens[i - 1].end = end;
255
274
  tokens[i - 1].string += string;
256
275
  tokens.splice(i, 1);
257
276
  }
258
277
  }
259
278
  const index = tokens.findIndex(({start, end}) => start < ch && end >= ch),
260
- text = tokens[index].string.replace(/\u200e/g, '').replace(/_/g, ' ').trim();
261
- if (/\bmw-template-name\b/.test(curType)) {
279
+ text = tokens[index].string.replace(/\u200E/gu, '').replace(/_/gu, ' ').trim();
280
+ if (/\bmw-template-name\b/u.test(curType)) {
262
281
  const title = new mw.Title(text);
263
282
  if (title.namespace !== 0 || text.startsWith(':')) {
264
283
  open(title.getUrl(), '_blank');
@@ -266,10 +285,10 @@
266
285
  open(mw.util.getUrl(`Template:${text}`), '_blank');
267
286
  }
268
287
  return false;
269
- } else if (index < 2 || !/\bmw-parserfunction-delimiter\b/.test(tokens[index - 1].type)
270
- || !/\bmw-parserfunction-name\b/.test(tokens[index - 2].type)
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)
271
290
  ) {
272
- return;
291
+ return undefined;
273
292
  }
274
293
  const parserFunction = tokens[index - 2].string.trim().toLowerCase();
275
294
  if (invoke.includes(parserFunction)) {
@@ -277,7 +296,7 @@
277
296
  } else if (widget.includes(parserFunction)) {
278
297
  open(mw.util.getUrl(`Widget:${text}`, {action: 'edit'}), '_blank');
279
298
  } else {
280
- return;
299
+ return undefined;
281
300
  }
282
301
  return false;
283
302
  });
@@ -293,8 +312,16 @@
293
312
  }
294
313
 
295
314
  const /** @type {Record<string, string>} */ i18nLanguages = {
296
- zh: 'zh-hans', 'zh-hans': 'zh-hans', 'zh-cn': 'zh-hans', 'zh-my': 'zh-hans', 'zh-sg': 'zh-hans',
297
- 'zh-hant': 'zh-hant', 'zh-tw': 'zh-hant', 'zh-hk': 'zh-hant', 'zh-mo': 'zh-hant', ka: 'ka',
315
+ zh: 'zh-hans',
316
+ 'zh-hans': 'zh-hans',
317
+ 'zh-cn': 'zh-hans',
318
+ 'zh-my': 'zh-hans',
319
+ 'zh-sg': 'zh-hans',
320
+ 'zh-hant': 'zh-hant',
321
+ 'zh-tw': 'zh-hant',
322
+ 'zh-hk': 'zh-hant',
323
+ 'zh-mo': 'zh-hant',
324
+ ka: 'ka',
298
325
  },
299
326
  i18nLang = i18nLanguages[userLang] || 'en',
300
327
  I18N_CDN = `${CDN}/${REPO_CDN}/i18n/${i18nLang}.json`,
@@ -319,14 +346,15 @@
319
346
 
320
347
  /**
321
348
  * 下载MW扩展脚本
322
- * @param {string[]} exts
349
+ * @param {string[]} exts CodeMirror扩展模块
323
350
  */
324
- const getInternalScript = exts => exts.length ? mw.loader.using(exts) : Promise.resolve();
351
+ const getInternalScript = exts => exts.length > 0 ? mw.loader.using(exts) : Promise.resolve();
352
+
325
353
  /**
326
354
  * 下载外部脚本
327
- * @param {string[]} urls
355
+ * @param {string[]} urls CodeMirror脚本网址
328
356
  */
329
- const getExternalScript = urls => urls.length
357
+ const getExternalScript = urls => urls.length > 0
330
358
  ? $.ajax(`${CDN}/${urls.length > 1 ? 'combine/' : ''}${urls.join()}`, {dataType: 'script', cache: true})
331
359
  : Promise.resolve();
332
360
 
@@ -351,7 +379,11 @@
351
379
  // 以下进入CodeMirror相关内容
352
380
  let /** @type {CodeMirror.EditorFromTextArea} */ cm;
353
381
 
354
- /** @param {typeof CodeMirror} CM */
382
+ /**
383
+ * 生成需要的插件列表
384
+ * @param {typeof CodeMirror} CM CodeMirror
385
+ * @param {boolean} other 是否用于Wikiplus以外的textarea
386
+ */
355
387
  const getAddonScript = (CM, other = false) => {
356
388
  const /** @type {string[]} */ addonScript = [];
357
389
  for (const {option, addon = option, download = Array.isArray(addon) ? option : addon, only} of options) {
@@ -363,8 +395,9 @@
363
395
  };
364
396
 
365
397
  /**
366
- * @param {T[]|T} arr
367
- * @param {Set<T>} set
398
+ * 交集
399
+ * @param {T[]|T} arr 集合1(可重)
400
+ * @param {Set<T>} set 集合2
368
401
  * @template T
369
402
  */
370
403
  const intersect = (arr, set) => Array.isArray(arr)
@@ -373,7 +406,7 @@
373
406
 
374
407
  /**
375
408
  * 根据文本的高亮模式加载依赖项
376
- * @param {string} type
409
+ * @param {string} type 高亮模式
377
410
  */
378
411
  const initMode = type => {
379
412
  let /** @type {string[]} */ scripts = [];
@@ -398,12 +431,12 @@
398
431
  // NamespaceHTML扩展自由度过高,所以这里一律当作允许<html>标签
399
432
  type = 'html'; // eslint-disable-line no-param-reassign
400
433
  }
401
- if (['mediawiki', 'widget'].includes(type) && !CM.modes.mediawiki) {
434
+ if ((type === 'mediawiki' || type === 'widget') && !CM.modes.mediawiki) {
402
435
  // 总是外部样式表和外部脚本
403
436
  mw.loader.load(`${CDN}/${MW_CDN}/mediawiki.min.css`, 'text/css');
404
437
  scripts.push(`${MW_CDN}/mediawiki.min.js`);
405
438
  }
406
- if (['widget', 'html'].includes(type)) {
439
+ if (type === 'widget' || type === 'html') {
407
440
  for (const lang of ['css', 'javascript', 'mediawiki', 'htmlmixed', 'xml']) {
408
441
  if (!CM.modes[lang]) {
409
442
  scripts = scripts.concat(MODE_LIST[lang]);
@@ -438,21 +471,39 @@
438
471
 
439
472
  /**
440
473
  * 更新缓存的设置数据
441
- * @param {mwConfig} config
474
+ * @param {mwConfig} config wikitext设置
442
475
  */
443
476
  const updateCachedConfig = config => {
444
477
  ALL_SETTINGS_CACHE[SITE_ID] = {config, time: Date.now()};
445
478
  storage.setObject('InPageEditMwConfig', ALL_SETTINGS_CACHE);
446
479
  };
447
480
 
481
+ /**
482
+ * 展开别名列表
483
+ * @param {{aliases: string[], name: string}[]} words 原名
484
+ * @returns {{alias: string, name: string}[]}
485
+ */
486
+ const getAliases = words => flatten(
487
+ words.map(({aliases, name}) => aliases.map(alias => ({alias, name}))),
488
+ );
489
+
490
+ /**
491
+ * 将别名信息转换为CodeMirror接受的设置
492
+ * @param {{alias: string, name: string}[]} aliases 别名
493
+ * @returns {Record<string, string>}
494
+ */
495
+ const getConfig = aliases => fromEntries(
496
+ aliases.map(({alias, name}) => [alias.replace(/:$/u, ''), name]),
497
+ );
498
+
448
499
  /**
449
500
  * 加载CodeMirror的mediawiki模块需要的设置数据
450
- * @param {string} type
501
+ * @param {string} type 高亮模式
451
502
  * @param {Promise<void>} initModePromise 使用本地CodeMirror扩展时大部分数据来自ext.CodeMirror.data模块
452
503
  */
453
504
  const getMwConfig = async (type, initModePromise) => {
454
- if (!['mediawiki', 'widget'].includes(type)) {
455
- return;
505
+ if (type !== 'mediawiki' && type !== 'widget') {
506
+ return undefined;
456
507
  }
457
508
 
458
509
  if (USING_LOCAL && EXPIRED) { // 只在localStorage过期时才会重新加载ext.CodeMirror.data
@@ -486,22 +537,15 @@
486
537
  });
487
538
  const otherMagicwords = ['msg', 'raw', 'msgnw', 'subst', 'safesubst'];
488
539
 
489
- /**
490
- * @param {{aliases: string[], name: string}[]} words
491
- * @returns {{alias: string, name: string}[]}
492
- */
493
- const getAliases = words => flatten(
494
- words.map(({aliases, name}) => aliases.map(alias => ({alias, name}))),
495
- );
496
- /**
497
- * @param {{alias: string, name: string}[]} aliases
498
- * @returns {Record<string, string>}
499
- */
500
- const getConfig = aliases => fromEntries(
501
- aliases.map(({alias, name}) => [alias.replace(/:$/, ''), name]),
502
- );
503
-
504
- if (!config) { // 情形4:`config === null`
540
+ if (config) { // 情形2或3
541
+ const {functionSynonyms: [insensitive]} = config;
542
+ if (!insensitive.subst) {
543
+ const aliases = getAliases(magicwords.filter(({name}) => otherMagicwords.includes(name)));
544
+ for (const {alias, name} of aliases) {
545
+ insensitive[alias.replace(/:$/u, '')] = name;
546
+ }
547
+ }
548
+ } else { // 情形4:`config === null`
505
549
  config = {
506
550
  tagModes: {
507
551
  pre: 'mw-tag-pre',
@@ -515,7 +559,7 @@
515
559
  };
516
560
  const realMagicwords = new Set([...functionhooks, ...variables, ...otherMagicwords]),
517
561
  allMagicwords = magicwords.filter(
518
- ({name, aliases}) => aliases.some(alias => /^__.+__$/.test(alias)) || realMagicwords.has(name),
562
+ ({name, aliases}) => aliases.some(alias => /^__.+__$/u.test(alias)) || realMagicwords.has(name),
519
563
  ),
520
564
  sensitive = getAliases(
521
565
  allMagicwords.filter(word => word['case-sensitive']),
@@ -524,22 +568,13 @@
524
568
  allMagicwords.filter(word => !word['case-sensitive']),
525
569
  ).map(({alias, name}) => ({alias: alias.toLowerCase(), name}));
526
570
  config.doubleUnderscore = [
527
- getConfig(insensitive.filter(({alias}) => /^__.+__$/.test(alias))),
528
- getConfig(sensitive.filter(({alias}) => /^__.+__$/.test(alias))),
571
+ getConfig(insensitive.filter(({alias}) => /^__.+__$/u.test(alias))),
572
+ getConfig(sensitive.filter(({alias}) => /^__.+__$/u.test(alias))),
529
573
  ];
530
574
  config.functionSynonyms = [
531
- getConfig(insensitive.filter(({alias}) => !/^__.+__|^#$/.test(alias))),
532
- getConfig(sensitive.filter(({alias}) => !/^__.+__|^#$/.test(alias))),
575
+ getConfig(insensitive.filter(({alias}) => !/^__.+__|^#$/u.test(alias))),
576
+ getConfig(sensitive.filter(({alias}) => !/^__.+__|^#$/u.test(alias))),
533
577
  ];
534
- } else { // 情形2或3
535
- const {functionSynonyms: [insensitive]} = config;
536
- if (!insensitive.subst) {
537
- getAliases(
538
- magicwords.filter(({name}) => otherMagicwords.includes(name)),
539
- ).forEach(({alias, name}) => {
540
- insensitive[alias.replace(/:$/, '')] = name;
541
- });
542
- }
543
578
  }
544
579
  config.redirect = magicwords.find(({name}) => name === 'redirect').aliases;
545
580
  config.img = getConfig(
@@ -552,7 +587,7 @@
552
587
 
553
588
  /** 检查页面语言类型 */
554
589
  const getPageMode = async () => {
555
- if ([274, 828].includes(ns) && !page.endsWith('/doc')) {
590
+ if ((ns === 274 || ns === 828) && !page.endsWith('/doc')) {
556
591
  const pageMode = ns === 274 ? 'Widget' : 'Lua';
557
592
  await mw.loader.using(['oojs-ui-windows', 'oojs-ui.styles.icons-content']);
558
593
  const bool = await OO.ui.confirm(msg('contentmodel'), {
@@ -570,17 +605,17 @@
570
605
  * See jQuery.textSelection.js for method documentation
571
606
  */
572
607
  const cmTextSelection = {
573
- getContents() {
608
+ /** @override */ getContents() {
574
609
  return cm.getValue();
575
610
  },
576
- setContents(content) {
611
+ /** @override */ setContents(content) {
577
612
  cm.setValue(content);
578
613
  return this;
579
614
  },
580
- getSelection() {
615
+ /** @override */ getSelection() {
581
616
  return cm.getSelection();
582
617
  },
583
- setSelection(option) {
618
+ /** @override */ setSelection(option) {
584
619
  cm.setSelection(
585
620
  cm.posFromIndex(option.start),
586
621
  'end' in option ? cm.posFromIndex(option.end) : undefined,
@@ -588,11 +623,11 @@
588
623
  cm.focus();
589
624
  return this;
590
625
  },
591
- replaceSelection(value) {
626
+ /** @override */ replaceSelection(value) {
592
627
  cm.replaceSelection(value);
593
628
  return this;
594
629
  },
595
- getCaretPosition(option) {
630
+ /** @override */ getCaretPosition(option) {
596
631
  const caretPos = cm.indexFromPos(cm.getCursor('from')),
597
632
  endPos = cm.indexFromPos(cm.getCursor('to'));
598
633
  if (option.startAndEnd) {
@@ -600,7 +635,7 @@
600
635
  }
601
636
  return caretPos;
602
637
  },
603
- scrollToCaretPosition() {
638
+ /** @override */ scrollToCaretPosition() {
604
639
  cm.scrollIntoView();
605
640
  return this;
606
641
  },
@@ -624,7 +659,7 @@
624
659
  if (typeof mw.addWikiEditor === 'function') {
625
660
  mw.addWikiEditor($target);
626
661
  } else {
627
- const {config} = $.wikiEditor.modules.dialogs;
662
+ const {wikiEditor: {modules: {dialogs: {config}}}} = $;
628
663
  $target.wikiEditor('addModule', {
629
664
  ...$.wikiEditor.modules.toolbar.config.getDefaultConfig(),
630
665
  ...config.getDefaultConfig(),
@@ -653,29 +688,31 @@
653
688
  }
654
689
 
655
690
  const json = setting || contentmodel === 'json';
656
- cm = CodeMirror.fromTextArea($target[0], $.extend({
657
- inputStyle: 'contenteditable',
658
- lineNumbers: !/Android\b/.test(navigator.userAgent),
659
- lineWrapping: true,
660
- mode,
661
- mwConfig,
662
- json,
663
- }, fromEntries(
664
- options.map(({option, addon = option, modes, complex = mod => !modes || modes.includes(mod)}) => {
665
- const mainAddon = Array.isArray(addon) ? addon[0] : addon;
666
- return [option, addons.has(mainAddon) && complex(mode, json)];
667
- }),
668
- ), mode === 'mediawiki'
669
- ? {
670
- extraKeys: addons.has('escape') && (isPc(CodeMirror) ? extraKeysPc : extraKeysMac),
671
- }
672
- : {
673
- indentUnit: addons.has('indentWithSpace') ? indent : defaultIndent,
674
- indentWithTabs: !addons.has('indentWithSpace'),
691
+ cm = CodeMirror.fromTextArea($target[0], $.extend(
692
+ {
693
+ inputStyle: 'contenteditable',
694
+ lineNumbers: !/Android\b/u.test(navigator.userAgent),
695
+ lineWrapping: true,
696
+ mode,
697
+ mwConfig,
698
+ json,
675
699
  },
700
+ fromEntries(
701
+ options.map(({option, addon = option, modes, complex = mod => !modes || modes.includes(mod)}) => {
702
+ const mainAddon = Array.isArray(addon) ? addon[0] : addon;
703
+ return [option, addons.has(mainAddon) && complex(mode, json)];
704
+ }),
705
+ ),
706
+ mode === 'mediawiki'
707
+ ? {
708
+ extraKeys: addons.has('escape') && (isPc(CodeMirror) ? extraKeysPc : extraKeysMac),
709
+ }
710
+ : {
711
+ indentUnit: addons.has('indentWithSpace') ? indent : defaultIndent,
712
+ indentWithTabs: !addons.has('indentWithSpace'),
713
+ },
676
714
  ));
677
715
  cm.setSize(null, height);
678
- cm.refresh();
679
716
  cm.getWrapperElement().id = 'Wikiplus-CodeMirror';
680
717
 
681
718
  if ($.fn.textSelection) {
@@ -685,7 +722,7 @@
685
722
  if (addons.has('wikiEditor')) {
686
723
  const context = $target.data('wikiEditorContext'),
687
724
  {keyMap} = CodeMirror,
688
- callback = () => {
725
+ callback = /** 替代CodeMirror的Ctrl/Cmd-F快捷键 */ () => {
689
726
  $.wikiEditor.modules.dialogs.api.openDialog(context, 'search-and-replace');
690
727
  };
691
728
  cm.addKeyMap(keyMap.default === keyMap.pcDefault ? {'Ctrl-F': callback} : {'Cmd-F': callback});
@@ -699,15 +736,16 @@
699
736
  const /** @type {Wikiplus} */ Wikiplus = typeof window.Wikiplus === 'object'
700
737
  ? window.Wikiplus
701
738
  : {
702
- getSetting(key) {
739
+ /** @override */ getSetting(key) {
703
740
  const settings = storage.getObject('Wikiplus_Settings');
704
741
  return settings && settings[key];
705
742
  },
706
743
  },
707
- submit = () => {
744
+ escToExitQuickEdit = Wikiplus.getSetting('esc_to_exit_quickedit'),
745
+ submit = /** 提交编辑 */ () => {
708
746
  $('#Wikiplus-Quickedit-Submit').triggerHandler('click');
709
747
  },
710
- submitMinor = () => {
748
+ submitMinor = /** 提交小编辑 */ () => {
711
749
  $('#Wikiplus-Quickedit-MinorEdit').click();
712
750
  $('#Wikiplus-Quickedit-Submit').triggerHandler('click');
713
751
  };
@@ -715,9 +753,9 @@
715
753
  isPc(CodeMirror)
716
754
  ? {'Ctrl-S': submit, 'Shift-Ctrl-S': submitMinor}
717
755
  : {'Cmd-S': submit, 'Shift-Cmd-S': submitMinor},
718
- [true, 'true'].includes(Wikiplus.getSetting('esc_to_exit_quickedit'))
756
+ escToExitQuickEdit === true || escToExitQuickEdit === 'true'
719
757
  ? {
720
- Esc() {
758
+ /** 按下Esc键退出编辑 */ Esc() {
721
759
  $('#Wikiplus-Quickedit-Back').triggerHandler('click');
722
760
  },
723
761
  }
@@ -725,6 +763,7 @@
725
763
  ));
726
764
  }
727
765
 
766
+ cm.refresh();
728
767
  mw.hook('wiki-codemirror').fire(cm);
729
768
  };
730
769
 
@@ -742,18 +781,23 @@
742
781
  });
743
782
  observer.observe(body, {childList: true});
744
783
 
745
- $(body).on('keydown.wphl', '.ui-dialog', function(e) {
746
- if (e.key === 'Escape') {
747
- /** @type {{$textarea: JQuery<HTMLTextAreaElement>}} */
748
- const context = $(this).children('.ui-dialog-content').data('context');
749
- if (context && context.$textarea && context.$textarea.attr('id') === 'Wikiplus-Quickedit') {
750
- e.stopPropagation();
784
+ $(body).on(
785
+ 'keydown.wphl',
786
+ '.ui-dialog',
787
+ /** @this {HTMLBodyElement} */
788
+ function(e) {
789
+ if (e.key === 'Escape') {
790
+ /** @type {{$textarea: JQuery<HTMLTextAreaElement>}} */
791
+ const context = $(this).children('.ui-dialog-content').data('context');
792
+ if (context && context.$textarea && context.$textarea.attr('id') === 'Wikiplus-Quickedit') {
793
+ e.stopPropagation();
794
+ }
751
795
  }
752
- }
753
- });
796
+ },
797
+ );
754
798
 
755
799
  // 添加样式
756
- const wphlStyle = document.getElementById('wphl-style') || mw.loader.addStyleTag(
800
+ const wphlStyle = document.querySelector('#wphl-style') || mw.loader.addStyleTag(
757
801
  '#Wikiplus-CodeMirror{border:1px solid #c8ccd1;line-height:1.3;clear:both;'
758
802
  + '-moz-user-select:auto;-webkit-user-select:auto;user-select:auto}' // fix mobile select
759
803
  + '#Wikiplus-CodeMirror .CodeMirror-gutter-wrapper{'
@@ -765,7 +809,8 @@
765
809
  + 'div.CodeMirror span.CodeMirror-nonmatchingbracket{box-shadow:0 0 0 2px #eace64}'
766
810
  + '#Wikiplus-highlight-dialog .oo-ui-messageDialog-title{margin-bottom:0.28571429em}'
767
811
  + '#Wikiplus-highlight-dialog .oo-ui-flaggedElement-notice{font-weight:normal;margin:0}'
768
- + '.CodeMirror-contextmenu .cm-mw-template-name{cursor:pointer}',
812
+ + '.CodeMirror-contextmenu .cm-mw-template-name{cursor:pointer}'
813
+ + '.skin-moeskin #ca-more-actions li>a{display:inline-block;padding:0.4rem 0.8rem;line-height:1.5}',
769
814
  );
770
815
  wphlStyle.id = 'wphl-style';
771
816
 
@@ -774,21 +819,22 @@
774
819
  * @type {{get: (elem: HTMLTextAreaElement) => string, set: (elem: HTMLTextAreaElement, value: string) => void}}
775
820
  */
776
821
  const {
777
- get = function(elem) {
778
- return elem.value;
779
- },
780
- set = function(elem, value) {
822
+ get = elem => elem.value,
823
+ set = (elem, value) => {
781
824
  elem.value = value;
782
825
  },
783
826
  } = $.valHooks.textarea || {};
784
827
 
785
- /** @param {HTMLTextAreaElement} elem */
786
- const isWikiplus = elem => ['Wikiplus-Quickedit', 'Wikiplus-Setting-Input'].includes(elem.id);
828
+ /**
829
+ * 是否是Wikiplus编辑区
830
+ * @param {HTMLTextAreaElement} elem textarea元素
831
+ */
832
+ const isWikiplus = elem => elem.id === 'Wikiplus-Quickedit' || elem.id === 'Wikiplus-Setting-Input';
787
833
  $.valHooks.textarea = {
788
- get(elem) {
834
+ /** @override */ get(elem) {
789
835
  return isWikiplus(elem) && cm ? cm.getValue() : get(elem);
790
836
  },
791
- set(elem, value) {
837
+ /** @override */ set(elem, value) {
792
838
  if (isWikiplus(elem) && cm) {
793
839
  cm.setValue(value);
794
840
  } else {
@@ -807,18 +853,26 @@
807
853
  /** @type {OOUI.NumberInputWidget} */ indentWidget,
808
854
  /** @type {OOUI.FieldLayout} */ field,
809
855
  /** @type {OOUI.FieldLayout} */ indentField;
856
+ /**
857
+ * 显示/隐藏缩进大小选项
858
+ * @param {string[]} value 加载的插件
859
+ */
810
860
  const toggleIndent = (value = [...addons]) => {
811
861
  indentField.toggle(value.includes('indentWithSpace'));
812
862
  };
813
863
  const portletContainer = {
814
864
  minerva: 'page-actions-overflow',
815
865
  citizen: 'p-actions',
866
+ moeskin: 'ca-more-actions',
816
867
  };
817
868
  const $portlet = $(mw.util.addPortletLink(
818
869
  portletContainer[skin] || 'p-cactions', '#', msg('portlet'), 'wphl-settings',
819
870
  )).click(async e => {
820
871
  e.preventDefault();
821
- if (!dialog) {
872
+ if (dialog) {
873
+ widget.setValue([...addons]);
874
+ indentWidget.setValue(indent);
875
+ } else {
822
876
  await mw.loader.using(['oojs-ui-windows', 'oojs-ui.styles.icons-content']);
823
877
  // eslint-disable-next-line require-atomic-updates
824
878
  dialog = new OO.ui.MessageDialog({id: 'Wikiplus-highlight-dialog'});
@@ -848,14 +902,11 @@
848
902
  indentField = new OO.ui.FieldLayout(indentWidget, {label: msg('addon-indent')});
849
903
  toggleIndent();
850
904
  Object.assign(mw.libs.wphl, {widget, indentWidget});
851
- } else {
852
- widget.setValue([...addons]);
853
- indentWidget.setValue(indent);
854
905
  }
855
906
  const wikiplusLoaded = typeof window.Wikiplus === 'object' || typeof window.Pages === 'object';
856
907
  searchWidget.setDisabled(!wikiplusLoaded);
857
908
  wikiEditorWidget.setDisabled(!wikiplusLoaded || !mw.loader.getState('ext.wikiEditor'));
858
- dialog.open({
909
+ const data = await dialog.open({
859
910
  title: msg('addon-title'),
860
911
  message: field.$element.add(indentField.$element).add(
861
912
  $('<p>', {html: msg('feedback')}),
@@ -865,17 +916,18 @@
865
916
  {action: 'accept', label: mw.msg('ooui-dialog-message-accept'), flags: 'progressive'},
866
917
  ],
867
918
  size: i18nLang === 'en' || skin === 'minerva' ? 'medium' : 'small',
868
- }).closing.then(data => {
869
- field.$element.detach();
870
- indentField.$element.detach();
871
- if (typeof data === 'object' && data.action === 'accept') {
872
- const value = widget.getValue();
873
- addons = new Set(value);
874
- indent = Number(indentWidget.getValue());
875
- storage.setObject('Wikiplus-highlight-addons', value);
876
- storage.setObject('Wikiplus-highlight-indent', indent);
877
- }
878
- });
919
+ }).closing;
920
+ field.$element.detach();
921
+ indentField.$element.detach();
922
+ if (typeof data === 'object' && data.action === 'accept') {
923
+ /* eslint-disable require-atomic-updates */
924
+ const value = widget.getValue();
925
+ addons = new Set(value);
926
+ indent = Number(indentWidget.getValue());
927
+ storage.setObject('Wikiplus-highlight-addons', value);
928
+ storage.setObject('Wikiplus-highlight-indent', indent);
929
+ /* eslint-enable require-atomic-updates */
930
+ }
879
931
  });
880
932
  if (skin === 'minerva') {
881
933
  $portlet.find('.mw-ui-icon').addClass('mw-ui-icon-minerva-settings');
@@ -889,7 +941,10 @@
889
941
  });
890
942
  }
891
943
 
892
- /** @param {CodeMirror.Editor} doc */
944
+ /**
945
+ * 处理非Wikiplus编辑器
946
+ * @param {CodeMirror.Editor} doc CodeMirror编辑区
947
+ */
893
948
  const handleOtherEditors = async doc => {
894
949
  if (!addons.has('otherEditors')) {
895
950
  return;
@@ -922,8 +977,25 @@
922
977
  mw.hook('inspector').add(/** @param {CodeMirror.Editor} doc */ doc => handleOtherEditors(doc));
923
978
 
924
979
  mw.libs.wphl = {
925
- version, options, addons, i18n, i18nLang, wphlStyle, $portlet, USING_LOCAL, MODE_LIST, ADDON_LIST,
926
- msg, htmlMsg, escapeHTML, handleContextMenu, setI18N, getAddonScript,
927
- updateCachedConfig, getMwConfig, renderEditor, handleOtherEditors,
980
+ version,
981
+ options,
982
+ addons,
983
+ i18n,
984
+ i18nLang,
985
+ wphlStyle,
986
+ $portlet,
987
+ USING_LOCAL,
988
+ MODE_LIST,
989
+ ADDON_LIST,
990
+ msg,
991
+ htmlMsg,
992
+ escapeHTML,
993
+ handleContextMenu,
994
+ setI18N,
995
+ getAddonScript,
996
+ updateCachedConfig,
997
+ getMwConfig,
998
+ renderEditor,
999
+ handleOtherEditors,
928
1000
  }; // 加载完毕
929
1001
  })();