@bhsd/codemirror-mediawiki 2.9.1 → 2.9.3
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/dist/main.min.js +11 -11
- package/dist/mw.min.js +4 -4
- package/dist/mw.min.js.map +4 -4
- package/i18n/en.json +1 -1
- package/i18n/zh-hans.json +1 -1
- package/i18n/zh-hant.json +1 -1
- package/package.json +1 -3
- package/mw/README.md +0 -156
- package/mw/base.ts +0 -262
- package/mw/config.ts +0 -183
- package/mw/msg.ts +0 -144
- package/mw/openLinks.ts +0 -100
- package/mw/preference.ts +0 -281
- package/mw/textSelection.ts +0 -122
- package/mw/wikiEditor.ts +0 -30
package/mw/msg.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import type {CodeMirror} from './base';
|
|
2
|
-
|
|
3
|
-
export const REPO_CDN = 'npm/@bhsd/codemirror-mediawiki@2.9.1',
|
|
4
|
-
curVersion = REPO_CDN.slice(REPO_CDN.lastIndexOf('@') + 1);
|
|
5
|
-
|
|
6
|
-
const {vendor, userAgent, maxTouchPoints, platform} = navigator;
|
|
7
|
-
|
|
8
|
-
export const isMac = vendor.includes('Apple Computer') && (userAgent.includes('Mobile/') || maxTouchPoints > 2)
|
|
9
|
-
|| platform.includes('Mac');
|
|
10
|
-
|
|
11
|
-
const storageKey = 'codemirror-mediawiki-i18n',
|
|
12
|
-
languages: Record<string, string> = {
|
|
13
|
-
zh: 'zh-hans',
|
|
14
|
-
'zh-hans': 'zh-hans',
|
|
15
|
-
'zh-cn': 'zh-hans',
|
|
16
|
-
'zh-my': 'zh-hans',
|
|
17
|
-
'zh-sg': 'zh-hans',
|
|
18
|
-
'zh-hant': 'zh-hant',
|
|
19
|
-
'zh-tw': 'zh-hant',
|
|
20
|
-
'zh-hk': 'zh-hant',
|
|
21
|
-
'zh-mo': 'zh-hant',
|
|
22
|
-
},
|
|
23
|
-
lang = languages[mw.config.get('wgUserLanguage')] || 'en';
|
|
24
|
-
|
|
25
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
-
export const getObject = (key: string): any => JSON.parse(String(localStorage.getItem(key)));
|
|
27
|
-
export const setObject = (key: string, value: unknown): void => {
|
|
28
|
-
localStorage.setItem(key, JSON.stringify(value));
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/** 预存的I18N,可以用于判断是否是首次安装 */
|
|
32
|
-
export const i18n: Record<string, string> = getObject(storageKey) || {};
|
|
33
|
-
|
|
34
|
-
const {version} = i18n;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* 加载 I18N
|
|
38
|
-
* @param CDN CDN地址
|
|
39
|
-
*/
|
|
40
|
-
export const setI18N = async (CDN: string): Promise<void> => {
|
|
41
|
-
if (i18n['lang'] !== lang || version !== curVersion) {
|
|
42
|
-
try {
|
|
43
|
-
Object.assign(i18n, await (await fetch(`${CDN}/${REPO_CDN}/i18n/${lang}.json`)).json());
|
|
44
|
-
setObject(storageKey, i18n);
|
|
45
|
-
} catch (e) {
|
|
46
|
-
void mw.notify(msg('i18n-failed', lang), {type: 'error'});
|
|
47
|
-
console.error(e);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
for (const [k, v] of Object.entries(i18n)) {
|
|
51
|
-
if (!k.endsWith('-mac')) {
|
|
52
|
-
mw.messages.set(`cm-mw-${k}`, v);
|
|
53
|
-
} else if (isMac) {
|
|
54
|
-
mw.messages.set(`cm-mw-${k.slice(0, -4)}`, v);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* 获取I18N消息
|
|
61
|
-
* @param key 消息键,省略`cm-mw-`前缀
|
|
62
|
-
* @param args 替换`$1`等的参数
|
|
63
|
-
*/
|
|
64
|
-
export const msg = (key: string, ...args: string[]): string => mw.msg(`cm-mw-${key}`, ...args);
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* 为所有链接添加`target="_blank"`
|
|
68
|
-
* @param $dom 容器
|
|
69
|
-
*/
|
|
70
|
-
const blankTarget = ($dom: JQuery<HTMLElement>): JQuery<HTMLElement> => {
|
|
71
|
-
$dom.find('a').add($dom.filter('a')).attr('target', '_blank');
|
|
72
|
-
return $dom;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* 解析I18N消息
|
|
77
|
-
* @param key 消息键,省略`cm-mw-`前缀
|
|
78
|
-
* @param text 是否输出为文本
|
|
79
|
-
*/
|
|
80
|
-
function parseMsg(key: string): JQuery<HTMLElement>;
|
|
81
|
-
function parseMsg(key: string, text: true): string;
|
|
82
|
-
function parseMsg(key: string, text?: boolean): string | JQuery<HTMLElement> {
|
|
83
|
-
const message = mw.message(`cm-mw-${key}`);
|
|
84
|
-
return text ? message.parse() : blankTarget(message.parseDom());
|
|
85
|
-
}
|
|
86
|
-
export {parseMsg};
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* 解析版本号
|
|
90
|
-
* @param v 版本号
|
|
91
|
-
*/
|
|
92
|
-
const parseVersion = (v: string): [number, number] => v.split('.', 2).map(Number) as [number, number];
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* 创建气泡提示消息
|
|
96
|
-
* @param key 消息键,省略`cm-mw-`前缀
|
|
97
|
-
* @param args 替换`$1`等的参数
|
|
98
|
-
*/
|
|
99
|
-
const notify = async (key: string, ...args: string[]): Promise<JQuery<HTMLElement>> => {
|
|
100
|
-
const $p = blankTarget($('<p>', {html: msg(key, ...args)}));
|
|
101
|
-
await mw.notify($p, {type: 'success', autoHideSeconds: 'long'});
|
|
102
|
-
return $p;
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* 欢迎消息
|
|
107
|
-
* @param baseVersion 首次加入新插件的版本
|
|
108
|
-
* @param addons 新插件
|
|
109
|
-
*/
|
|
110
|
-
export const welcome = async (baseVersion: string, addons: string[]): Promise<void> => {
|
|
111
|
-
let notification: JQuery<HTMLElement> | undefined;
|
|
112
|
-
if (!version) { // 首次安装
|
|
113
|
-
notification = await notify('welcome');
|
|
114
|
-
} else if (addons.length > 0) { // 更新版本
|
|
115
|
-
const [baseMajor, baseMinor] = parseVersion(baseVersion),
|
|
116
|
-
[major, minor] = parseVersion(version);
|
|
117
|
-
if (major < baseMajor || major === baseMajor && minor < baseMinor) {
|
|
118
|
-
notification = await notify(
|
|
119
|
-
'welcome-addons',
|
|
120
|
-
curVersion,
|
|
121
|
-
String(addons.length),
|
|
122
|
-
addons.map(addon => `<li>${parseMsg(`addon-${addon}`, true)}</li>`).join(''),
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
notification?.find('#settings').click(e => {
|
|
127
|
-
e.preventDefault();
|
|
128
|
-
document.getElementById('cm-settings')!.dispatchEvent(new MouseEvent('click'));
|
|
129
|
-
});
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* 本地化
|
|
134
|
-
* @param cm
|
|
135
|
-
*/
|
|
136
|
-
export const localize = (cm: CodeMirror): void => {
|
|
137
|
-
const obj: Record<string, string> = {};
|
|
138
|
-
for (const [k, v] of Object.entries(i18n)) {
|
|
139
|
-
if (k.startsWith('phrase-')) {
|
|
140
|
-
obj[k.slice(7).replace(/-/gu, ' ')] = v;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
cm.localize(obj);
|
|
144
|
-
};
|
package/mw/openLinks.ts
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import {isMac} from './msg';
|
|
2
|
-
import type {SyntaxNode} from '@lezer/common';
|
|
3
|
-
import type {CodeMirror} from './base';
|
|
4
|
-
|
|
5
|
-
declare type MouseEventListener = (e: MouseEvent) => void;
|
|
6
|
-
|
|
7
|
-
const modKey = isMac ? 'metaKey' : 'ctrlKey',
|
|
8
|
-
handlers = new WeakMap<CodeMirror, MouseEventListener>();
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* 获取节点的名称
|
|
12
|
-
* @param node 语法树节点
|
|
13
|
-
*/
|
|
14
|
-
function getName(node: SyntaxNode): string;
|
|
15
|
-
function getName(node: null): undefined;
|
|
16
|
-
function getName(node: SyntaxNode | null): string | undefined {
|
|
17
|
-
return node?.name.replace(/_+/gu, ' ').trim();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* 查找连续同名节点
|
|
22
|
-
* @param node 起始节点
|
|
23
|
-
* @param dir 方向
|
|
24
|
-
* @param name 节点名称
|
|
25
|
-
*/
|
|
26
|
-
const search = (node: SyntaxNode, dir: 'prevSibling' | 'nextSibling', name = getName(node)): SyntaxNode => {
|
|
27
|
-
while (getName(node[dir]!) === name) {
|
|
28
|
-
node = node[dir]!; // eslint-disable-line no-param-reassign
|
|
29
|
-
}
|
|
30
|
-
return node;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 点击时在新页面打开链接、模板等
|
|
35
|
-
* @param cm
|
|
36
|
-
* @param e 点击事件
|
|
37
|
-
*/
|
|
38
|
-
const getHandler = (cm: CodeMirror): MouseEventListener => {
|
|
39
|
-
if (handlers.has(cm)) {
|
|
40
|
-
return handlers.get(cm)!;
|
|
41
|
-
}
|
|
42
|
-
const handler: MouseEventListener = (e): void => {
|
|
43
|
-
if (!e[modKey]) {
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
const {view} = cm,
|
|
47
|
-
{state} = view,
|
|
48
|
-
node = cm.getNodeAt(view.posAtCoords(e)!);
|
|
49
|
-
if (!node) {
|
|
50
|
-
// pass
|
|
51
|
-
} else if (node.name.includes('mw-pagename')) {
|
|
52
|
-
e.preventDefault();
|
|
53
|
-
e.stopPropagation();
|
|
54
|
-
const name = getName(node);
|
|
55
|
-
let page = state.sliceDoc(
|
|
56
|
-
search(node, 'prevSibling', name).from,
|
|
57
|
-
search(node, 'nextSibling', name).to,
|
|
58
|
-
).trim();
|
|
59
|
-
if (page.startsWith('/')) {
|
|
60
|
-
page = `:${mw.config.get('wgPageName')}${page}`;
|
|
61
|
-
}
|
|
62
|
-
let ns = 0;
|
|
63
|
-
if (name.includes('-template-name')) {
|
|
64
|
-
ns = 10;
|
|
65
|
-
} else if (name.includes('-parserfunction')) {
|
|
66
|
-
ns = 828;
|
|
67
|
-
}
|
|
68
|
-
open(new mw.Title(page, ns).getUrl(undefined), '_blank');
|
|
69
|
-
} else if (/-extlink-protocol/u.test(node.name)) {
|
|
70
|
-
e.preventDefault();
|
|
71
|
-
open(state.sliceDoc(node.from, search(node.nextSibling!, 'nextSibling').to), '_blank');
|
|
72
|
-
} else if (/-extlink(?:_|$)/u.test(node.name)) {
|
|
73
|
-
e.preventDefault();
|
|
74
|
-
const name = getName(node),
|
|
75
|
-
prev = search(node, 'prevSibling', name).prevSibling!,
|
|
76
|
-
next = search(node, 'nextSibling', name);
|
|
77
|
-
open(state.sliceDoc(prev.from, next.to), '_blank');
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
handlers.set(cm, handler);
|
|
81
|
-
return handler;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* 添加或移除打开链接的事件
|
|
86
|
-
* @param cm
|
|
87
|
-
* @param on 是否添加
|
|
88
|
-
*/
|
|
89
|
-
export const openLinks = (cm: CodeMirror, on?: boolean): void => {
|
|
90
|
-
const {view: {contentDOM}} = cm,
|
|
91
|
-
handler = getHandler(cm);
|
|
92
|
-
if (on) {
|
|
93
|
-
mw.loader.load('mediawiki.Title');
|
|
94
|
-
contentDOM.addEventListener('mousedown', handler, {capture: true});
|
|
95
|
-
contentDOM.style.setProperty('--codemirror-cursor', 'pointer');
|
|
96
|
-
} else if (on === false) {
|
|
97
|
-
contentDOM.removeEventListener('mousedown', handler, {capture: true});
|
|
98
|
-
contentDOM.style.removeProperty('--codemirror-cursor');
|
|
99
|
-
}
|
|
100
|
-
};
|
package/mw/preference.ts
DELETED
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
import {rules} from 'wikiparser-node/dist/base';
|
|
2
|
-
import {CodeMirror} from './base';
|
|
3
|
-
import {msg, parseMsg, i18n, setObject, getObject} from './msg';
|
|
4
|
-
import {instances} from './textSelection';
|
|
5
|
-
import type {LintError} from 'wikiparser-node';
|
|
6
|
-
import type {ApiEditPageParams, ApiQueryRevisionsParams} from 'types-mediawiki/api_params';
|
|
7
|
-
|
|
8
|
-
const storageKey = 'codemirror-mediawiki-addons',
|
|
9
|
-
wikilintKey = 'codemirror-mediawiki-wikilint',
|
|
10
|
-
codeKeys = ['ESLint', 'Stylelint'] as const,
|
|
11
|
-
user = mw.config.get('wgUserName'),
|
|
12
|
-
userPage = user && `User:${user}/codemirror-mediawiki.json`;
|
|
13
|
-
|
|
14
|
-
declare type codeKey = typeof codeKeys[number];
|
|
15
|
-
|
|
16
|
-
declare type Preferences = {
|
|
17
|
-
addons?: string[];
|
|
18
|
-
indent?: string;
|
|
19
|
-
wikilint?: Record<LintError.Rule, RuleState>;
|
|
20
|
-
} & Record<codeKey, unknown>;
|
|
21
|
-
|
|
22
|
-
declare interface MediaWikiPage {
|
|
23
|
-
readonly revisions?: {
|
|
24
|
-
readonly content: string;
|
|
25
|
-
}[];
|
|
26
|
-
}
|
|
27
|
-
declare interface MediaWikiResponse {
|
|
28
|
-
readonly query: {
|
|
29
|
-
readonly pages: MediaWikiPage[];
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const enum RuleState {
|
|
34
|
-
off = '0',
|
|
35
|
-
error = '1',
|
|
36
|
-
on = '2',
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const indentKey = 'codemirror-mediawiki-indent',
|
|
40
|
-
prefs = new Set<string>(getObject(storageKey) as string[] | null),
|
|
41
|
-
wikilintConfig = (getObject(wikilintKey) || {}) as Record<LintError.Rule, RuleState | undefined>,
|
|
42
|
-
codeConfigs = new Map(codeKeys.map(k => [k, getObject(`codemirror-mediawiki-${k}`)]));
|
|
43
|
-
|
|
44
|
-
// OOUI组件
|
|
45
|
-
let dialog: OO.ui.MessageDialog | undefined,
|
|
46
|
-
layout: OO.ui.IndexLayout,
|
|
47
|
-
widget: OO.ui.CheckboxMultiselectInputWidget,
|
|
48
|
-
indentWidget: OO.ui.TextInputWidget,
|
|
49
|
-
indent = localStorage.getItem(indentKey) || '';
|
|
50
|
-
const widgets: Partial<Record<codeKey, OO.ui.MultilineTextInputWidget>> = {},
|
|
51
|
-
wikilintWidgets = new Map<LintError.Rule, OO.ui.DropdownInputWidget>();
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* 处理Api请求错误
|
|
55
|
-
* @param code 错误代码
|
|
56
|
-
* @param e 错误信息
|
|
57
|
-
*/
|
|
58
|
-
const apiErr = (code: string, e: any): void => { // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
59
|
-
const message = code === 'http' || code === 'okay-but-empty'
|
|
60
|
-
? `MediaWiki API request failed: ${code}`
|
|
61
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
62
|
-
: $('<ul>', {html: (e.errors as {html: string}[]).map(({html}) => $('<li>', {html}))});
|
|
63
|
-
void mw.notify(message as string | HTMLElement[], {type: 'error', autoHideSeconds: 'long'});
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const api = (async () => {
|
|
67
|
-
if (user) {
|
|
68
|
-
await mw.loader.using('mediawiki.api');
|
|
69
|
-
return new mw.Api({parameters: {errorformat: 'html', formatversion: '2'}});
|
|
70
|
-
}
|
|
71
|
-
return undefined;
|
|
72
|
-
})();
|
|
73
|
-
|
|
74
|
-
export const loadJSON = (async () => {
|
|
75
|
-
if (!user) {
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
const params: ApiQueryRevisionsParams = {
|
|
79
|
-
action: 'query',
|
|
80
|
-
prop: 'revisions',
|
|
81
|
-
titles: userPage,
|
|
82
|
-
rvprop: 'content',
|
|
83
|
-
rvlimit: 1,
|
|
84
|
-
};
|
|
85
|
-
(await api)!.get(params as Record<string, string>).then( // eslint-disable-line promise/prefer-await-to-then
|
|
86
|
-
res => {
|
|
87
|
-
const {query: {pages: [page]}} = res as MediaWikiResponse;
|
|
88
|
-
if (page?.revisions) {
|
|
89
|
-
const json: Preferences = JSON.parse(page.revisions[0]!.content);
|
|
90
|
-
if (!json.addons?.includes('save')) {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
prefs.clear();
|
|
94
|
-
for (const option of json.addons) {
|
|
95
|
-
prefs.add(option);
|
|
96
|
-
}
|
|
97
|
-
if (json.indent) {
|
|
98
|
-
localStorage.setItem(indentKey, json.indent);
|
|
99
|
-
}
|
|
100
|
-
for (const key of codeKeys) {
|
|
101
|
-
if (json[key]) {
|
|
102
|
-
codeConfigs.set(key, json[key]);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
if (json.wikilint) {
|
|
106
|
-
Object.assign(wikilintConfig, json.wikilint);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
apiErr,
|
|
111
|
-
);
|
|
112
|
-
})();
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* 打开设置对话框
|
|
116
|
-
* @param editors CodeMirror实例
|
|
117
|
-
*/
|
|
118
|
-
export const openPreference = async (editors: (CodeMirror | undefined)[]): Promise<void> => {
|
|
119
|
-
await mw.loader.using([
|
|
120
|
-
'oojs-ui-windows',
|
|
121
|
-
'oojs-ui-widgets',
|
|
122
|
-
'oojs-ui.styles.icons-content',
|
|
123
|
-
'mediawiki.jqueryMsg',
|
|
124
|
-
]);
|
|
125
|
-
await loadJSON;
|
|
126
|
-
if (dialog) {
|
|
127
|
-
widget.setValue([...prefs] as unknown as string);
|
|
128
|
-
indentWidget.setValue(indent);
|
|
129
|
-
} else {
|
|
130
|
-
dialog = new OO.ui.MessageDialog({id: 'cm-preference'});
|
|
131
|
-
const windowManager = new OO.ui.WindowManager();
|
|
132
|
-
windowManager.$element.appendTo(document.body);
|
|
133
|
-
windowManager.addWindows([dialog]);
|
|
134
|
-
layout = new OO.ui.IndexLayout();
|
|
135
|
-
const panelMain = new OO.ui.TabPanelLayout('main', {label: msg('title')}),
|
|
136
|
-
panelWikilint = new OO.ui.TabPanelLayout('wikilint', {label: 'WikiLint'}),
|
|
137
|
-
panels: Partial<Record<codeKey, OO.ui.TabPanelLayout>> = {};
|
|
138
|
-
for (const key of codeKeys) {
|
|
139
|
-
const c = codeConfigs.get(key);
|
|
140
|
-
widgets[key] = new OO.ui.MultilineTextInputWidget({
|
|
141
|
-
value: c ? JSON.stringify(c, null, indent || '\t') : '',
|
|
142
|
-
});
|
|
143
|
-
const codeField = new OO.ui.FieldLayout(widgets[key]!, {label: msg(`${key}-config`), align: 'top'}),
|
|
144
|
-
panel = new OO.ui.TabPanelLayout(key, {label: key, $content: codeField.$element});
|
|
145
|
-
panel.on('active', active => {
|
|
146
|
-
const [textarea] = panel.$element.find('textarea') as unknown as [HTMLTextAreaElement];
|
|
147
|
-
if (active && !instances.has(textarea)) {
|
|
148
|
-
void CodeMirror.fromTextArea(textarea, 'json');
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
panels[key] = panel;
|
|
152
|
-
}
|
|
153
|
-
layout.addTabPanels([panelMain, panelWikilint, ...Object.values(panels)], 0);
|
|
154
|
-
widget = new OO.ui.CheckboxMultiselectInputWidget({
|
|
155
|
-
options: [
|
|
156
|
-
{disabled: true},
|
|
157
|
-
...Object.keys(i18n)
|
|
158
|
-
.filter(k => k !== 'addon-indent' && k.startsWith('addon-') && !k.endsWith('-mac'))
|
|
159
|
-
.map(k => ({
|
|
160
|
-
data: k.slice(6),
|
|
161
|
-
label: parseMsg(k),
|
|
162
|
-
disabled: k === 'addon-wikiEditor' && !mw.loader.getState('ext.wikiEditor')
|
|
163
|
-
|| k === 'addon-save' && !user,
|
|
164
|
-
})),
|
|
165
|
-
],
|
|
166
|
-
value: [...prefs] as unknown as string,
|
|
167
|
-
});
|
|
168
|
-
indentWidget = new OO.ui.TextInputWidget({value: indent, placeholder: '\\t'});
|
|
169
|
-
const field = new OO.ui.FieldLayout(widget, {
|
|
170
|
-
label: msg('label'),
|
|
171
|
-
align: 'top',
|
|
172
|
-
}),
|
|
173
|
-
indentField = new OO.ui.FieldLayout(indentWidget, {label: msg('addon-indent')});
|
|
174
|
-
panelMain.$element.append(
|
|
175
|
-
field.$element,
|
|
176
|
-
indentField.$element,
|
|
177
|
-
$('<p>', {html: msg('feedback', 'codemirror-mediawiki')}),
|
|
178
|
-
);
|
|
179
|
-
panelWikilint.$element.append(
|
|
180
|
-
...rules.map(rule => {
|
|
181
|
-
const state = rule === 'no-arg' ? RuleState.off : RuleState.error,
|
|
182
|
-
dropdown = new OO.ui.DropdownInputWidget({
|
|
183
|
-
options: [
|
|
184
|
-
{data: RuleState.off, label: msg('wikilint-off')},
|
|
185
|
-
{data: RuleState.error, label: msg('wikilint-error')},
|
|
186
|
-
{data: RuleState.on, label: msg('wikilint-on')},
|
|
187
|
-
],
|
|
188
|
-
value: wikilintConfig[rule] || state,
|
|
189
|
-
}),
|
|
190
|
-
f = new OO.ui.FieldLayout(dropdown, {label: rule});
|
|
191
|
-
wikilintWidgets.set(rule, dropdown);
|
|
192
|
-
wikilintConfig[rule] ||= state;
|
|
193
|
-
return f.$element;
|
|
194
|
-
}),
|
|
195
|
-
$('<p>', {html: msg('feedback', 'wikiparser-node')}),
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const data = await (dialog.open({
|
|
200
|
-
message: layout!.$element,
|
|
201
|
-
actions: [
|
|
202
|
-
{action: 'reject', label: mw.msg('ooui-dialog-message-reject')},
|
|
203
|
-
{action: 'accept', label: mw.msg('ooui-dialog-message-accept'), flags: 'progressive'},
|
|
204
|
-
],
|
|
205
|
-
size: 'medium',
|
|
206
|
-
}).closing as unknown as Promise<{action?: unknown} | undefined>);
|
|
207
|
-
if (typeof data === 'object' && data.action === 'accept') {
|
|
208
|
-
// 缩进
|
|
209
|
-
const oldIndent = indent,
|
|
210
|
-
save = prefs.has('save');
|
|
211
|
-
indent = indentWidget.getValue(); // eslint-disable-line require-atomic-updates
|
|
212
|
-
let changed = indent !== oldIndent;
|
|
213
|
-
if (changed) {
|
|
214
|
-
for (const cm of editors) {
|
|
215
|
-
cm?.setIndent(indent || '\t');
|
|
216
|
-
}
|
|
217
|
-
localStorage.setItem(indentKey, indent);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// WikiLint
|
|
221
|
-
for (const [rule, dropdown] of wikilintWidgets) {
|
|
222
|
-
const val = dropdown.getValue() as RuleState;
|
|
223
|
-
changed ||= val !== wikilintConfig[rule];
|
|
224
|
-
wikilintConfig[rule] = val;
|
|
225
|
-
}
|
|
226
|
-
setObject(wikilintKey, wikilintConfig);
|
|
227
|
-
|
|
228
|
-
// ESLint & Stylelint
|
|
229
|
-
const jsonErrors: string[] = [];
|
|
230
|
-
for (const key of codeKeys) {
|
|
231
|
-
try {
|
|
232
|
-
const config = JSON.parse(widgets[key]!.getValue().trim() || 'null');
|
|
233
|
-
changed ||= JSON.stringify(config) !== JSON.stringify(codeConfigs.get(key));
|
|
234
|
-
codeConfigs.set(key, config);
|
|
235
|
-
setObject(`codemirror-mediawiki-${key}`, config);
|
|
236
|
-
} catch {
|
|
237
|
-
jsonErrors.push(key);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
if (jsonErrors.length > 0) {
|
|
241
|
-
void OO.ui.alert(msg('json-error', jsonErrors.join(msg('and'))));
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// 插件
|
|
245
|
-
const value = widget.getValue() as unknown as string[];
|
|
246
|
-
if (value.length !== prefs.size || !value.every(option => prefs.has(option))) {
|
|
247
|
-
changed = true;
|
|
248
|
-
prefs.clear();
|
|
249
|
-
for (const option of value) {
|
|
250
|
-
prefs.add(option);
|
|
251
|
-
}
|
|
252
|
-
for (const cm of editors) {
|
|
253
|
-
cm?.prefer(value);
|
|
254
|
-
}
|
|
255
|
-
setObject(storageKey, value);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// 保存至用户子页面
|
|
259
|
-
if (changed && user && (save || prefs.has('save'))) {
|
|
260
|
-
const params: ApiEditPageParams = {
|
|
261
|
-
action: 'edit',
|
|
262
|
-
title: userPage,
|
|
263
|
-
text: JSON.stringify({
|
|
264
|
-
addons: [...prefs],
|
|
265
|
-
indent,
|
|
266
|
-
wikilint: wikilintConfig,
|
|
267
|
-
ESLint: codeConfigs.get('ESLint'),
|
|
268
|
-
Stylelint: codeConfigs.get('Stylelint'),
|
|
269
|
-
} as Preferences),
|
|
270
|
-
summary: msg('save-summary'),
|
|
271
|
-
};
|
|
272
|
-
// eslint-disable-next-line promise/prefer-await-to-then
|
|
273
|
-
(await api)!.postWithToken('csrf', params as Record<string, string>).then(
|
|
274
|
-
() => {
|
|
275
|
-
void mw.notify(parseMsg('save-success'), {type: 'success'});
|
|
276
|
-
},
|
|
277
|
-
apiErr,
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
};
|
package/mw/textSelection.ts
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import {CodeMirror} from './base';
|
|
2
|
-
|
|
3
|
-
export const instances = new WeakMap<HTMLTextAreaElement, CodeMirror>();
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 获取CodeMirror实例
|
|
7
|
-
* @param $ele textarea元素的jQuery对象
|
|
8
|
-
*/
|
|
9
|
-
const getInstance = ($ele: JQuery<HTMLTextAreaElement>): CodeMirror => instances.get($ele[0]!)!;
|
|
10
|
-
|
|
11
|
-
declare interface EncapsulateOptions {
|
|
12
|
-
pre?: string;
|
|
13
|
-
peri?: string;
|
|
14
|
-
post?: string;
|
|
15
|
-
ownline?: boolean;
|
|
16
|
-
replace?: boolean;
|
|
17
|
-
selectPeri?: boolean;
|
|
18
|
-
splitlines?: boolean;
|
|
19
|
-
selectionStart?: number;
|
|
20
|
-
selectionEnd?: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* jQuery.textSelection overrides for CodeMirror.
|
|
25
|
-
* See jQuery.textSelection.js for method documentation
|
|
26
|
-
*/
|
|
27
|
-
export const textSelection = {
|
|
28
|
-
getContents(this: JQuery<HTMLTextAreaElement>): string {
|
|
29
|
-
return getInstance(this).view.state.doc.toString();
|
|
30
|
-
},
|
|
31
|
-
setContents(this: JQuery<HTMLTextAreaElement>, content: string): JQuery<HTMLTextAreaElement> {
|
|
32
|
-
getInstance(this).setContent(content);
|
|
33
|
-
return this;
|
|
34
|
-
},
|
|
35
|
-
getSelection(this: JQuery<HTMLTextAreaElement>): string {
|
|
36
|
-
const {view: {state}} = getInstance(this),
|
|
37
|
-
{selection: {main: {from, to}}} = state;
|
|
38
|
-
return state.sliceDoc(from, to);
|
|
39
|
-
},
|
|
40
|
-
setSelection(
|
|
41
|
-
this: JQuery<HTMLTextAreaElement>,
|
|
42
|
-
{start, end = start}: {start: number, end?: number},
|
|
43
|
-
): JQuery<HTMLTextAreaElement> {
|
|
44
|
-
getInstance(this).view.dispatch({
|
|
45
|
-
selection: {anchor: start, head: end},
|
|
46
|
-
});
|
|
47
|
-
return this;
|
|
48
|
-
},
|
|
49
|
-
replaceSelection(this: JQuery<HTMLTextAreaElement>, value: string): JQuery<HTMLTextAreaElement> {
|
|
50
|
-
const {view} = getInstance(this);
|
|
51
|
-
view.dispatch(view.state.replaceSelection(value));
|
|
52
|
-
return this;
|
|
53
|
-
},
|
|
54
|
-
encapsulateSelection(this: JQuery<HTMLTextAreaElement>, {
|
|
55
|
-
pre = '',
|
|
56
|
-
peri = '',
|
|
57
|
-
post = '',
|
|
58
|
-
ownline,
|
|
59
|
-
replace,
|
|
60
|
-
selectPeri = true,
|
|
61
|
-
splitlines,
|
|
62
|
-
selectionStart,
|
|
63
|
-
selectionEnd = selectionStart,
|
|
64
|
-
}: EncapsulateOptions): JQuery<HTMLTextAreaElement> {
|
|
65
|
-
const {view} = getInstance(this),
|
|
66
|
-
{state} = view;
|
|
67
|
-
const handleOwnline = (from: number, to: number, text: string): [string, number, number] => {
|
|
68
|
-
let start = 0,
|
|
69
|
-
end = 0;
|
|
70
|
-
if (ownline) {
|
|
71
|
-
if (from > 0 && !/[\n\r]/u.test(state.sliceDoc(from - 1, from))) {
|
|
72
|
-
text = `\n${text}`; // eslint-disable-line no-param-reassign
|
|
73
|
-
start = 1;
|
|
74
|
-
}
|
|
75
|
-
if (!/[\n\r]/u.test(state.sliceDoc(to, to + 1))) {
|
|
76
|
-
text += '\n'; // eslint-disable-line no-param-reassign
|
|
77
|
-
end = 1;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return [text, start, end];
|
|
81
|
-
};
|
|
82
|
-
if (ownline && replace && !pre && !post && selectionStart === undefined && /^\s*=.*=\s*$/u.test(peri)) {
|
|
83
|
-
// 单独处理改变标题层级
|
|
84
|
-
const {selection: {main: {from, to}}} = state,
|
|
85
|
-
[insertText] = handleOwnline(from, to, peri);
|
|
86
|
-
view.dispatch({
|
|
87
|
-
changes: {from, to, insert: insertText},
|
|
88
|
-
selection: {anchor: from + insertText.length},
|
|
89
|
-
});
|
|
90
|
-
return this;
|
|
91
|
-
}
|
|
92
|
-
CodeMirror.replaceSelections(view, (_, {from, to}) => {
|
|
93
|
-
if (selectionStart !== undefined) {
|
|
94
|
-
/* eslint-disable no-param-reassign */
|
|
95
|
-
from = selectionStart;
|
|
96
|
-
to = selectionEnd!;
|
|
97
|
-
/* eslint-enable no-param-reassign */
|
|
98
|
-
}
|
|
99
|
-
const isSample = selectPeri && from === to,
|
|
100
|
-
selText = replace || from === to ? peri : state.sliceDoc(from, to),
|
|
101
|
-
[insertText, start, end] = handleOwnline(
|
|
102
|
-
from,
|
|
103
|
-
to,
|
|
104
|
-
splitlines
|
|
105
|
-
? selText.split('\n').map(line => `${pre}${line}${post}`).join('\n')
|
|
106
|
-
: `${pre}${selText}${post}`,
|
|
107
|
-
),
|
|
108
|
-
head = from + insertText.length;
|
|
109
|
-
return isSample ? [insertText, from + pre.length + start, head - post.length - end] : [insertText, head];
|
|
110
|
-
});
|
|
111
|
-
return this;
|
|
112
|
-
},
|
|
113
|
-
getCaretPosition(this: JQuery<HTMLTextAreaElement>, option?: {startAndEnd?: boolean}): [number, number] | number {
|
|
114
|
-
const {view: {state: {selection: {main: {from, to, head}}}}} = getInstance(this);
|
|
115
|
-
return option?.startAndEnd ? [from, to] : head;
|
|
116
|
-
},
|
|
117
|
-
scrollToCaretPosition(this: JQuery<HTMLTextAreaElement>): JQuery<HTMLTextAreaElement> {
|
|
118
|
-
const cm = getInstance(this);
|
|
119
|
-
cm.scrollTo();
|
|
120
|
-
return this;
|
|
121
|
-
},
|
|
122
|
-
};
|
package/mw/wikiEditor.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import {msg} from './msg';
|
|
2
|
-
import {prefs} from './preference';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 添加WikiEditor工具栏
|
|
6
|
-
* @param $textarea 文本框
|
|
7
|
-
*/
|
|
8
|
-
export const wikiEditor = async ($textarea: JQuery<HTMLTextAreaElement>): Promise<void> => {
|
|
9
|
-
if (!mw.loader.getState('ext.wikiEditor')) {
|
|
10
|
-
prefs.delete('wikiEditor');
|
|
11
|
-
void mw.notify(msg('no-wikiEditor'), {type: 'error'});
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
await mw.loader.using('ext.wikiEditor');
|
|
15
|
-
if ($textarea.data('wikiEditorContext')) {
|
|
16
|
-
return;
|
|
17
|
-
} else if (typeof mw.addWikiEditor === 'function') { // MW >= 1.34
|
|
18
|
-
mw.addWikiEditor($textarea);
|
|
19
|
-
} else { // MW <= 1.33
|
|
20
|
-
const {wikiEditor: {modules: {dialogs: {config}}}} = $;
|
|
21
|
-
$textarea.wikiEditor('addModule', {
|
|
22
|
-
...$.wikiEditor.modules.toolbar.config.getDefaultConfig(),
|
|
23
|
-
...config.getDefaultConfig(),
|
|
24
|
-
});
|
|
25
|
-
config.replaceIcons($textarea);
|
|
26
|
-
}
|
|
27
|
-
await new Promise(resolve => { // MW >= 1.21
|
|
28
|
-
$textarea.on('wikiEditor-toolbar-doneInitialSections', resolve);
|
|
29
|
-
});
|
|
30
|
-
};
|