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/.eslintrc.json +228 -0
- package/LICENSE +674 -0
- package/README.md +45 -0
- package/bump.sh +12 -0
- package/i18n/en.json +21 -0
- package/i18n/zh-hans.json +21 -0
- package/i18n/zh-hant.json +21 -0
- package/main.js +704 -0
- package/matchtags.js +308 -0
- package/package.json +24 -0
- package/search.js +187 -0
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
|
+
})();
|