@bhsd/codemirror-mediawiki 2.9.1 → 2.9.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/mw/README.md DELETED
@@ -1,156 +0,0 @@
1
- <details>
2
- <summary>Expand</summary>
3
-
4
- - [Usage](#usage)
5
- - [Constructor](#constructor)
6
- - [Accessors](#accessors)
7
- - [Methods](#methods)
8
- - [defaultLint](#defaultlint)
9
- - [Static properties](#static-properties)
10
- - [version](#version)
11
- - [Static methods](#static-methods)
12
- - [fromTextArea](#fromtextarea)
13
- - [Extensions](#extensions)
14
- - [openLinks](#openlinks)
15
- - [wikiEditor](#wikieditor)
16
- - [save](#save)
17
-
18
- </details>
19
-
20
- # Usage
21
-
22
- You can download the code via CDN, for example:
23
-
24
- ```js
25
- // static import
26
- import {CodeMirror} from 'https://cdn.jsdelivr.net/npm/@bhsd/codemirror-mediawiki/dist/mw.min.js';
27
- ```
28
-
29
- or
30
-
31
- ```js
32
- import {CodeMirror} from 'https://unpkg.com/@bhsd/codemirror-mediawiki/dist/mw.min.js';
33
- ```
34
-
35
- or
36
-
37
- ```js
38
- // dynamic import
39
- const {CodeMirror} = await import('https://cdn.jsdelivr.net/npm/@bhsd/codemirror-mediawiki/dist/mw.min.js');
40
- ```
41
-
42
- or
43
-
44
- ```js
45
- const {CodeMirror} = await import('https://unpkg.com/@bhsd/codemirror-mediawiki/dist/mw.min.js');
46
- ```
47
-
48
- The script also loads the [styles](../mediawiki.css), adds a button to configure user preferences, and watches `Shift`-clicks of any textarea.
49
-
50
- # Constructor
51
-
52
- <details>
53
- <summary>Expand</summary>
54
-
55
- *version added: 2.2.2*
56
-
57
- The `CodeMirror` class extends the [`CodeMirror6`](../README.md#constructor) class with one more argument to specify the namespace.
58
-
59
- **param**: `HTMLTextAreaElement` the textarea element to be replaced by CodeMirror
60
- **param**: `string` the language mode to be used, default as plain text
61
- **param**: `number` the namespace id associated with the content, default as the current namespace
62
- **param**: `unknown` the optional language configuration
63
-
64
- ```js
65
- const cm = new CodeMirror6(textarea); // plain text
66
- const cm = new CodeMirror6(textarea, 'mediawiki', undefined, mwConfig);
67
- const cm = new CodeMirror6(textarea, 'html', 274, mwConfig); // mixed MediaWiki-HTML
68
- const cm = new CodeMirror6(textarea, 'css');
69
- const cm = new CodeMirror6(textarea, 'javascript');
70
- const cm = new CodeMirror6(textarea, 'json');
71
- const cm = new CodeMirror6(textarea, 'lua');
72
- ```
73
-
74
- </details>
75
-
76
- # Accessors
77
-
78
- The `CodeMirror` class inherits all the [accessors](../README.md#accessors) from the `CodeMirror6` class.
79
-
80
- # Methods
81
-
82
- The `CodeMirror` class inherits all the [methods](../README.md#methods) from the `CodeMirror6` class and addes more.
83
-
84
- ## defaultLint
85
-
86
- <details>
87
- <summary>Expand</summary>
88
-
89
- *version added: 2.1.9*
90
-
91
- **param**: `boolean` whether to start linting
92
- **param**: `Record<string, unknown> | number` the optional linter configuration or the namespace id
93
- Lint with a default linter.
94
-
95
- ```js
96
- cm.defaultLint(true, 0);
97
- ```
98
-
99
- </details>
100
-
101
- # Static properties
102
-
103
- ## version
104
-
105
- <details>
106
- <summary>Expand</summary>
107
-
108
- *version added: 2.6.3*
109
-
110
- **type**: `string`
111
- The version number.
112
- </details>
113
-
114
- # Static methods
115
-
116
- The `CodeMirror` class inherits all the [static methods](../README.md#static-methods) from the `CodeMirror6` class and addes more.
117
-
118
- ## fromTextArea
119
-
120
- <details>
121
- <summary>Expand</summary>
122
-
123
- *version added: 2.2.2*
124
-
125
- **param**: `HTMLTextAreaElement` the textarea element to be replaced by CodeMirror
126
- **param**: `string` the language mode to be used, default as plain text
127
- **param**: `number` the namespace id associated with the content, default as the current namespace
128
- Replace the textarea with CodeMirror editor.
129
-
130
- ```js
131
- CodeMirror6.fromTextArea(textarea, 'mediawiki');
132
- ```
133
-
134
- </details>
135
-
136
- # Extensions
137
-
138
- The `CodeMirror` class inherits all the [extensions](../README.md#extensions) from the `CodeMirror6` class and addes more.
139
-
140
- ## openLinks
141
-
142
- *version added: 2.1.15*
143
-
144
- CTRL/CMD-click opens a wikilink or template or external link in a new tab.
145
-
146
- ## wikiEditor
147
-
148
- *version added: 2.4.5*
149
-
150
- Load the WikiEditor toolbar. This extension can only be used before CodeMirror instantiation, which means it is inaccessible by the [`prefer`](../README.md#prefer) method.
151
-
152
- ## save
153
-
154
- *version added: 2.7.0*
155
-
156
- Save preferences as JSON on a user subpage (`Special:Mypage/codemirror-mediawiki.json`).
package/mw/base.ts DELETED
@@ -1,262 +0,0 @@
1
- import {CodeMirror6, CDN} from 'https://testingcf.jsdelivr.net/npm/@bhsd/codemirror-mediawiki@2.9.1/dist/main.min.js';
2
- import {getMwConfig, getParserConfig} from './config';
3
- import {openLinks} from './openLinks';
4
- import {instances, textSelection} from './textSelection';
5
- import {openPreference, prefs, indentKey, wikilintConfig, codeConfigs, loadJSON} from './preference';
6
- import {msg, setI18N, welcome, REPO_CDN, curVersion, localize} from './msg';
7
- import {wikiEditor} from './wikiEditor';
8
- import type {Diagnostic} from '@codemirror/lint';
9
- import type {Config, LintError} from 'wikiparser-node';
10
- import type {Linter} from 'eslint';
11
- import type {LintSource, MwConfig} from '../src/codemirror';
12
-
13
- // 每次新增插件都需要修改这里
14
- const baseVersion = '2.7',
15
- addons = ['save'];
16
-
17
- mw.loader.load(`${CDN}/${REPO_CDN}/mediawiki.min.css`, 'text/css');
18
-
19
- /**
20
- * jQuery.val overrides for CodeMirror.
21
- */
22
- $.valHooks['textarea'] = {
23
- get(elem: HTMLTextAreaElement): string {
24
- const cm = instances.get(elem);
25
- return cm?.visible ? cm.view.state.doc.toString() : elem.value;
26
- },
27
- set(elem: HTMLTextAreaElement, value: string): void {
28
- const cm = instances.get(elem);
29
- if (cm?.visible) {
30
- cm.setContent(value);
31
- } else {
32
- elem.value = value;
33
- }
34
- },
35
- };
36
-
37
- const linters: Record<string, LintSource | undefined> = {};
38
-
39
- /**
40
- * 判断是否为普通编辑器
41
- * @param textarea 文本框
42
- */
43
- const isEditor = (textarea: HTMLTextAreaElement): boolean => !textarea.closest('#cm-preference');
44
-
45
- /** 专用于MW环境的 CodeMirror 6 编辑器 */
46
- export class CodeMirror extends CodeMirror6 {
47
- static version = curVersion;
48
-
49
- ns;
50
-
51
- /**
52
- * @param textarea 文本框
53
- * @param lang 语言
54
- * @param ns 命名空间
55
- * @param config 语言设置
56
- */
57
- constructor(textarea: HTMLTextAreaElement, lang?: string, ns?: number, config?: unknown) {
58
- if (instances.get(textarea)?.visible) {
59
- throw new RangeError('The textarea has already been replaced by CodeMirror.');
60
- }
61
- super(textarea, lang, config);
62
- this.ns = ns;
63
- instances.set(textarea, this);
64
- if (mw.loader.getState('jquery.textSelection') === 'ready') {
65
- $(textarea).data('jquery.textSelection', textSelection);
66
- }
67
- if (isEditor(textarea)) {
68
- mw.hook('wiki-codemirror6').fire(this);
69
- if (textarea.id === 'wpTextbox1') {
70
- textarea.form?.addEventListener('submit', () => {
71
- const scrollTop = document.querySelector<HTMLInputElement>('#wpScrolltop');
72
- if (scrollTop) {
73
- scrollTop.value = String(this.view.scrollDOM.scrollTop);
74
- }
75
- });
76
- }
77
- }
78
- }
79
-
80
- override toggle(show = !this.visible): void {
81
- super.toggle(show);
82
- $(this.textarea).data('jquery.textSelection', show && textSelection);
83
- }
84
-
85
- override async getLinter(opt?: Record<string, unknown>): Promise<LintSource | undefined> {
86
- const linter = await super.getLinter(opt);
87
- linters[this.lang] = linter;
88
- return linter;
89
- }
90
-
91
- /**
92
- * 添加或移除默认 linter
93
- * @param on 是否添加
94
- * @param opt linter选项
95
- * @param ns 命名空间
96
- */
97
- defaultLint(on: boolean, opt: Record<string, unknown>): Promise<void>;
98
- defaultLint(on: boolean, ns?: number): Promise<void>;
99
- async defaultLint(on: boolean, optOrNs: Record<string, unknown> | number | undefined = this.ns): Promise<void> {
100
- if (!on) {
101
- this.lint();
102
- return;
103
- }
104
- const {lang} = this,
105
- eslint = codeConfigs.get('ESLint'),
106
- stylelint = codeConfigs.get('Stylelint');
107
- let opt: Record<string, unknown> | undefined;
108
- if (typeof optOrNs === 'number') {
109
- if (lang === 'mediawiki' && (optOrNs === 10 || optOrNs === 828 || optOrNs === 2)) {
110
- opt = {include: true};
111
- } else if (lang === 'javascript') {
112
- opt = {
113
- env: {browser: true, es2024: true, jquery: true},
114
- globals: {mw: 'readonly', mediaWiki: 'readonly', OO: 'readonly'},
115
- ...optOrNs === 8 || optOrNs === 2300 ? {parserOptions: {ecmaVersion: 8}} : {},
116
- ...eslint,
117
- } as Linter.Config as Record<string, unknown>;
118
- } else if (lang === 'css' && stylelint) {
119
- opt = stylelint;
120
- }
121
- } else {
122
- opt = optOrNs;
123
- }
124
- if (!(lang in linters)) {
125
- if (lang === 'mediawiki') {
126
- const i18n = mw.config.get('wgUserLanguage');
127
- if (['zh', 'zh-hans', 'zh-cn', 'zh-sg', 'zh-my'].includes(i18n)) {
128
- opt = {...opt, i18n: 'zh-hans'};
129
- } else if (['zh-hant', 'zh-tw', 'zh-hk', 'zh-mo'].includes(i18n)) {
130
- opt = {...opt, i18n: 'zh-hant'};
131
- }
132
- }
133
- await this.getLinter(opt);
134
- if (lang === 'mediawiki') {
135
- const [mwConfig, minConfig] = await Promise.all([getMwConfig(), wikiparse.getConfig()]);
136
- wikiparse.setConfig(getParserConfig(minConfig, mwConfig));
137
- }
138
- } else if (opt) {
139
- await this.getLinter(opt);
140
- }
141
- if (linters[lang]) {
142
- if (lang === 'mediawiki') {
143
- this.lint(
144
- async doc => (await linters[lang]!(doc) as (Diagnostic & {rule: LintError.Rule})[])
145
- .filter(({rule, severity}) => Number(wikilintConfig[rule]) > Number(severity === 'warning')),
146
- );
147
- } else {
148
- this.lint(linters[lang]);
149
- }
150
- }
151
- }
152
-
153
- override prefer(extensions: string[] | Record<string, boolean>): void {
154
- super.prefer(extensions);
155
- const hasExtension = Array.isArray(extensions)
156
- ? (ext: string): boolean => extensions.includes(ext)
157
- : (ext: string): boolean | undefined => extensions[ext],
158
- hasLint = hasExtension('lint');
159
- if (hasLint !== undefined) {
160
- void this.defaultLint(hasLint);
161
- }
162
- openLinks(this, hasExtension('openLinks'));
163
- if (!Array.isArray(extensions)) {
164
- for (const [k, v] of Object.entries(extensions)) {
165
- prefs[v ? 'add' : 'delete'](k);
166
- }
167
- }
168
- }
169
-
170
- /**
171
- * 将 textarea 替换为 CodeMirror
172
- * @param textarea textarea 元素
173
- * @param lang 语言
174
- * @param ns 命名空间
175
- */
176
- static async fromTextArea(textarea: HTMLTextAreaElement, lang?: string, ns?: number): Promise<CodeMirror> {
177
- if (prefs.has('wikiEditor') && isEditor(textarea)) {
178
- await wikiEditor($(textarea));
179
- }
180
- if (!lang && ns === undefined) {
181
- /* eslint-disable no-param-reassign */
182
- const {wgAction, wgNamespaceNumber, wgPageContentModel} = mw.config.get();
183
- if (wgAction === 'edit' || wgAction === 'submit') {
184
- ns = wgNamespaceNumber;
185
- lang = wgNamespaceNumber === 274 ? 'html' : wgPageContentModel.toLowerCase();
186
- } else {
187
- await mw.loader.using('oojs-ui-windows');
188
- lang = (await OO.ui.prompt(msg('contentmodel')) || undefined)?.toLowerCase();
189
- }
190
- if (lang && lang in langMap) {
191
- lang = langMap[lang];
192
- }
193
- /* eslint-enable no-param-reassign */
194
- }
195
- const isWiki = lang === 'mediawiki' || lang === 'html',
196
- cm = new CodeMirror(textarea, isWiki ? undefined : lang, ns);
197
- if (isWiki) {
198
- let config: MwConfig;
199
- if (mw.config.get('wgServerName').endsWith('.moegirl.org.cn')) {
200
- if (mw.config.exists('wikilintConfig')) {
201
- config = mw.config.get('extCodeMirrorConfig') as MwConfig;
202
- } else {
203
- const parserConfig: Config = await (await fetch(
204
- `${CDN}/npm/wikiparser-node@browser/config/moegirl.json`,
205
- )).json();
206
- mw.config.set('wikilintConfig', parserConfig);
207
- config = CodeMirror6.getMwConfig(parserConfig);
208
- mw.config.set('extCodeMirrorConfig', config);
209
- }
210
- } else {
211
- config = await getMwConfig();
212
- }
213
- cm.setLanguage(lang, config);
214
- }
215
- await loadJSON;
216
- cm.prefer([...prefs]);
217
- const indent = localStorage.getItem(indentKey);
218
- if (indent) {
219
- cm.setIndent(indent);
220
- }
221
- return cm;
222
- }
223
- }
224
-
225
- const langMap: Record<string, string> = {
226
- 'sanitized-css': 'css',
227
- js: 'javascript',
228
- scribunto: 'lua',
229
- wikitext: 'mediawiki',
230
- };
231
- document.body.addEventListener('click', e => {
232
- if (e.target instanceof HTMLTextAreaElement && e.shiftKey && !instances.has(e.target)) {
233
- e.preventDefault();
234
- void CodeMirror.fromTextArea(e.target);
235
- }
236
- });
237
-
238
- (async () => {
239
- const portletContainer: Record<string, string> = {
240
- minerva: 'page-actions-overflow',
241
- moeskin: 'ca-more-actions',
242
- citizen: 'p-tb',
243
- };
244
- await Promise.all([
245
- mw.loader.using('mediawiki.util'),
246
- setI18N(CDN),
247
- ]);
248
- mw.hook('wiki-codemirror6').add(localize);
249
- mw.util.addPortletLink(
250
- portletContainer[mw.config.get('skin')] || 'p-cactions',
251
- '#',
252
- msg('title'),
253
- 'cm-settings',
254
- ).addEventListener('click', e => {
255
- e.preventDefault();
256
- const textareas = [...document.querySelectorAll<HTMLTextAreaElement>('.cm-editor + textarea')];
257
- void openPreference(textareas.map(textarea => instances.get(textarea)));
258
- });
259
- void welcome(baseVersion, addons);
260
- })();
261
-
262
- Object.assign(window, {CodeMirror6: CodeMirror});
package/mw/config.ts DELETED
@@ -1,183 +0,0 @@
1
- import {setObject, getObject} from './msg';
2
- import {CodeMirror} from './base';
3
- import type {Config} from 'wikiparser-node';
4
- import type {MwConfig} from '../src/mediawiki';
5
-
6
- declare interface MagicWord {
7
- name: string;
8
- aliases: string[];
9
- 'case-sensitive': boolean;
10
- }
11
-
12
- // 和本地缓存有关的常数
13
- const USING_LOCAL = mw.loader.getState('ext.CodeMirror') !== null,
14
- DATA_MODULE = mw.loader.getState('ext.CodeMirror.data') ? 'ext.CodeMirror.data' : 'ext.CodeMirror',
15
- ALL_SETTINGS_CACHE: Record<string, {time: number, config: MwConfig}>
16
- = getObject('InPageEditMwConfig') || {},
17
- SITE_ID = `${mw.config.get('wgServerName')}${mw.config.get('wgScriptPath')}`,
18
- SITE_SETTINGS = ALL_SETTINGS_CACHE[SITE_ID],
19
- VALID = Number(SITE_SETTINGS?.time) > Date.now() - 86_400 * 1000 * 30;
20
-
21
- /**
22
- * 将魔术字信息转换为CodeMirror接受的设置
23
- * @param magicWords 完整魔术字列表
24
- * @param rule 过滤函数
25
- * @param flip 是否反向筛选对大小写敏感的魔术字
26
- */
27
- const getConfig = (
28
- magicWords: MagicWord[],
29
- rule: (word: MagicWord) => boolean,
30
- flip?: boolean,
31
- ): Record<string, string> => {
32
- const words = magicWords.filter(rule).filter(({'case-sensitive': i}) => i !== flip)
33
- .flatMap(({aliases, name, 'case-sensitive': i}) => aliases.map(alias => ({
34
- alias: (i ? alias : alias.toLowerCase()).replace(/:$/u, ''),
35
- name,
36
- }))),
37
- obj: Record<string, string> = {};
38
- for (const {alias, name} of words) {
39
- obj[alias] = name;
40
- }
41
- return obj;
42
- };
43
-
44
- /**
45
- * 将魔术字信息转换为CodeMirror接受的设置
46
- * @param magicWords 完整魔术字列表
47
- * @param rule 过滤函数
48
- */
49
- const getConfigPair = (
50
- magicWords: MagicWord[],
51
- rule: (word: MagicWord) => boolean,
52
- ): [Record<string, string>, Record<string, string>] => [true, false]
53
- .map(bool => getConfig(magicWords, rule, bool)) as [Record<string, string>, Record<string, string>];
54
-
55
- /**
56
- * 将设置保存到mw.config
57
- * @param config 设置
58
- */
59
- const setConfig = (config: MwConfig): void => {
60
- mw.config.set('extCodeMirrorConfig', config);
61
- };
62
-
63
- /** 加载CodeMirror的mediawiki模块需要的设置 */
64
- export const getMwConfig = async (): Promise<MwConfig> => {
65
- if (USING_LOCAL && !VALID) { // 只在localStorage过期时才会重新加载ext.CodeMirror.data
66
- await mw.loader.using(DATA_MODULE);
67
- }
68
-
69
- let config = mw.config.get('extCodeMirrorConfig') as MwConfig | null;
70
- if (!config && VALID) {
71
- ({config} = SITE_SETTINGS!);
72
- setConfig(config);
73
- }
74
- const isIPE = config && Object.values(config.functionSynonyms[0]).includes(true as unknown as string),
75
- nsid = mw.config.get('wgNamespaceIds'),
76
- tagModes = CodeMirror.mwTagModes;
77
- // 情形1:config已更新,可能来自localStorage
78
- if (config?.img && config.variants && !isIPE) {
79
- config.urlProtocols = config.urlProtocols.replace(/\\:/gu, ':');
80
- return {...config, nsid, tagModes};
81
- }
82
-
83
- // 以下情形均需要发送API请求
84
- // 情形2:localStorage未过期但不包含新设置
85
- // 情形3:新加载的 ext.CodeMirror.data
86
- // 情形4:`config === null`
87
- await mw.loader.using('mediawiki.api');
88
- const {
89
- query: {general: {variants}, magicwords, extensiontags, functionhooks, variables},
90
- }: {
91
- query: {
92
- general: {variants?: {code: string}[]};
93
- magicwords: MagicWord[];
94
- extensiontags: string[];
95
- functionhooks: string[];
96
- variables: string[];
97
- };
98
- } = await new mw.Api().get({
99
- meta: 'siteinfo',
100
- siprop: [
101
- 'general',
102
- 'magicwords',
103
- ...config && !isIPE ? [] : ['extensiontags', 'functionhooks', 'variables'],
104
- ],
105
- formatversion: '2',
106
- }) as any; // eslint-disable-line @typescript-eslint/no-explicit-any
107
- const others = new Set(['msg', 'raw', 'msgnw', 'subst', 'safesubst']);
108
-
109
- // 先处理魔术字和状态开关
110
- if (config && !isIPE) { // 情形2或3
111
- const {functionSynonyms: [insensitive]} = config;
112
- if (!('subst' in insensitive)) {
113
- Object.assign(insensitive, getConfig(magicwords, ({name}) => others.has(name)));
114
- }
115
- } else { // 情形4:`config === null`
116
- // @ts-expect-error incomplete properties
117
- config = {
118
- tagModes: {},
119
- tags: {},
120
- };
121
- for (const tag of extensiontags) {
122
- config!.tags[tag.slice(1, -1)] = true;
123
- }
124
- const functions = new Set([
125
- ...functionhooks,
126
- ...variables,
127
- ...others,
128
- ]);
129
- config!.functionSynonyms = getConfigPair(magicwords, ({name}) => functions.has(name));
130
- config!.doubleUnderscore = getConfigPair(
131
- magicwords,
132
- ({aliases}) => aliases.some(alias => /^__.+__$/u.test(alias)),
133
- );
134
- config!.fromApi = true;
135
- }
136
- config!.img = getConfig(magicwords, ({name}) => name.startsWith('img_'));
137
- config!.variants = variants ? variants.map(({code}) => code) : [];
138
- config!.nsid = nsid;
139
- config!.urlProtocols = mw.config.get('wgUrlProtocols').replace(/\\:/gu, ':');
140
- Object.assign(config!.tagModes, tagModes);
141
- setConfig(config!);
142
- ALL_SETTINGS_CACHE[SITE_ID] = {config: config!, time: Date.now()};
143
- setObject('InPageEditMwConfig', ALL_SETTINGS_CACHE);
144
- return config!;
145
- };
146
-
147
- /**
148
- * 将MwConfig转换为Config
149
- * @param minConfig 基础Config
150
- * @param mwConfig
151
- */
152
- export const getParserConfig = (minConfig: Config, mwConfig: MwConfig): Config => {
153
- if (mw.config.exists('wikilintConfig')) {
154
- return mw.config.get('wikilintConfig') as Config;
155
- }
156
- const config: Config = {
157
- ...minConfig,
158
- ext: Object.keys(mwConfig.tags),
159
- namespaces: mw.config.get('wgFormattedNamespaces'),
160
- nsid: mwConfig.nsid,
161
- doubleUnderscore: mwConfig.doubleUnderscore.map(
162
- obj => Object.keys(obj).map(s => s.slice(2, -2)),
163
- ) as [string[], string[]],
164
- variants: mwConfig.variants!,
165
- protocol: mwConfig.urlProtocols,
166
- };
167
- [config.parserFunction[0]] = mwConfig.functionSynonyms;
168
- if (!USING_LOCAL) {
169
- for (const [key, val] of Object.entries(mwConfig.functionSynonyms[0])) {
170
- if (!key.startsWith('#')) {
171
- config.parserFunction[0][`#${key}`] = val;
172
- }
173
- }
174
- }
175
- config.parserFunction[1] = [
176
- ...Object.keys(mwConfig.functionSynonyms[1]),
177
- '=',
178
- ];
179
- for (const [key, val] of Object.entries(mwConfig.img!)) {
180
- config.img[key] = val.slice(4);
181
- }
182
- return config;
183
- };