@bhsd/codemirror-mediawiki 2.1.11 → 2.1.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhsd/codemirror-mediawiki",
3
- "version": "2.1.11",
3
+ "version": "2.1.13",
4
4
  "description": "Modified CodeMirror mode based on wikimedia/mediawiki-extensions-CodeMirror",
5
5
  "keywords": [
6
6
  "mediawiki",
@@ -16,9 +16,10 @@
16
16
  "!/src/gh-page.ts",
17
17
  "!/src/plugins.ts",
18
18
  "!/src/*.d.ts",
19
+ "/mw/*.ts",
20
+ "!/mw/*.d.ts",
19
21
  "/dist/",
20
- "/mediawiki.css",
21
- "/mw/dist/"
22
+ "/mediawiki.css"
22
23
  ],
23
24
  "browser": "dist/main.min.js",
24
25
  "main": "./dist/main.min.js",
@@ -29,13 +30,13 @@
29
30
  },
30
31
  "scripts": {
31
32
  "build:core": "esbuild ./src/codemirror.ts --bundle --minify --target=es2018 --format=esm --sourcemap --outfile=dist/main.min.js && tsc --emitDeclarationOnly && rm dist/gh-page.d.ts",
32
- "build:mw": "bash build.sh mw/base.ts mw/dist/base.js",
33
+ "build:mw": "esbuild ./mw/base.ts --bundle --minify --target=es2018 --format=esm --sourcemap --outfile=dist/mw.min.js",
33
34
  "build:gh-page": "bash build.sh src/gh-page.ts gh-page.js",
34
35
  "build": "npm run build:core && npm run build:mw",
35
36
  "lint:ts": "tsc --noEmit && tsc --project mw/tsconfig.json --noEmit && eslint --cache .",
36
37
  "lint:css": "stylelint *.css",
37
38
  "lint": "npm run lint:ts && npm run lint:css",
38
- "server": "http-server .. -c-1 --cors &",
39
+ "server": "npm run test:end; http-server .. -c-1 --cors &",
39
40
  "test": "npm run build:core && npm run build:gh-page && npm run server",
40
41
  "test:end": "pkill -x http-server"
41
42
  },
@@ -52,24 +53,24 @@
52
53
  "@codemirror/state": "^6.3.3",
53
54
  "@codemirror/view": "^6.22.2",
54
55
  "@lezer/highlight": "^1.2.0",
55
- "@stylistic/eslint-plugin": "^1.5.3",
56
+ "@stylistic/eslint-plugin": "^1.5.4",
56
57
  "@stylistic/stylelint-plugin": "^2.0.0",
57
58
  "@types/jquery": "^3.5.29",
58
59
  "@types/oojs-ui": "^0.47.6",
59
- "@typescript-eslint/eslint-plugin": "^6.12.0",
60
- "@typescript-eslint/parser": "^6.12.0",
61
- "esbuild": "^0.19.7",
60
+ "@typescript-eslint/eslint-plugin": "^6.19.1",
61
+ "@typescript-eslint/parser": "^6.19.1",
62
+ "esbuild": "^0.19.12",
62
63
  "eslint": "^8.56.0",
63
- "eslint-plugin-es-x": "^7.3.0",
64
+ "eslint-plugin-es-x": "^7.5.0",
64
65
  "eslint-plugin-eslint-comments": "^3.2.0",
65
66
  "eslint-plugin-promise": "^6.1.1",
66
- "eslint-plugin-regexp": "^2.1.2",
67
+ "eslint-plugin-regexp": "^2.2.0",
67
68
  "eslint-plugin-unicorn": "^50.0.1",
68
69
  "http-server": "^14.1.0",
69
70
  "stylelint": "^16.1.0",
70
71
  "stylelint-config-recommended": "^14.0.0",
71
72
  "types-mediawiki": "^1.4.0",
72
- "typescript": "^5.1.6",
73
- "wikilint": "^2.3.2"
73
+ "typescript": "^5.3.3",
74
+ "wikilint": "^2.4.1"
74
75
  }
75
76
  }
package/src/codemirror.ts CHANGED
@@ -61,19 +61,20 @@ const avail: Record<string, [(config?: any) => Extension, Record<string, unknown
61
61
  {},
62
62
  ],
63
63
  };
64
+ const CDN = 'https://testingcf.jsdelivr.net';
64
65
 
65
66
  /**
66
67
  * 使用传统方法加载脚本
67
68
  * @param src 脚本地址
68
- * @param target 脚本全局变量名
69
+ * @param globalConst 脚本全局变量名
69
70
  */
70
- const loadScript = (src: string, target: string): Promise<void> => new Promise(resolve => {
71
- if (target in window) {
71
+ const loadScript = (src: string, globalConst: string): Promise<void> => new Promise(resolve => {
72
+ if (globalConst in window) {
72
73
  resolve();
73
74
  return;
74
75
  }
75
76
  const script = document.createElement('script');
76
- script.src = `https://testingcf.jsdelivr.net/${src}`;
77
+ script.src = `${CDN}/${src}`;
77
78
  script.onload = (): void => {
78
79
  resolve();
79
80
  };
@@ -90,13 +91,14 @@ const pos = (doc: Text, line: number, column: number): number => doc.line(line).
90
91
 
91
92
  export class CodeMirror6 {
92
93
  readonly #textarea;
93
- readonly #language;
94
- readonly #linter;
95
- readonly #extensions;
96
- readonly #indent;
97
94
  readonly #view;
95
+ readonly #language = new Compartment();
96
+ readonly #linter = new Compartment();
97
+ readonly #extensions = new Compartment();
98
+ readonly #indent = new Compartment();
98
99
  #lang;
99
- #visible = true;
100
+ #visible = false;
101
+ #preferred = new Set<string>();
100
102
 
101
103
  get textarea(): HTMLTextAreaElement {
102
104
  return this.#textarea;
@@ -122,10 +124,6 @@ export class CodeMirror6 {
122
124
  constructor(textarea: HTMLTextAreaElement, lang = 'plain', config?: unknown) {
123
125
  this.#textarea = textarea;
124
126
  this.#lang = lang;
125
- this.#language = new Compartment();
126
- this.#linter = new Compartment();
127
- this.#extensions = new Compartment();
128
- this.#indent = new Compartment();
129
127
  let timer: number | undefined;
130
128
  const extensions = [
131
129
  this.#language.of(languages[lang]!(config)),
@@ -162,25 +160,12 @@ export class CodeMirror6 {
162
160
  extensions,
163
161
  doc: textarea.value,
164
162
  });
165
- const {selectionStart, selectionEnd, scrollTop} = textarea,
166
- {fontSize, lineHeight} = getComputedStyle(textarea),
167
- hasFocus = document.activeElement === textarea;
163
+ const {fontSize, lineHeight} = getComputedStyle(textarea);
168
164
  textarea.parentNode!.insertBefore(this.#view.dom, textarea);
169
165
  this.#minHeight();
170
- this.#refresh();
171
- this.#view.dom.style.fontSize = fontSize;
166
+ this.#view.scrollDOM.style.fontSize = fontSize;
172
167
  this.#view.scrollDOM.style.lineHeight = lineHeight;
173
- this.#view.requestMeasure();
174
- this.#view.dispatch({
175
- selection: {anchor: selectionStart, head: selectionEnd},
176
- });
177
- textarea.style.display = 'none';
178
- if (hasFocus) {
179
- this.#view.focus();
180
- }
181
- requestAnimationFrame(() => {
182
- this.#view.scrollDOM.scrollTop = scrollTop;
183
- });
168
+ this.toggle(true);
184
169
  }
185
170
 
186
171
  /** 刷新编辑器高度 */
@@ -264,16 +249,23 @@ export class CodeMirror6 {
264
249
  * 添加扩展
265
250
  * @param names 扩展名
266
251
  */
267
- prefer(names: readonly string[]): void {
252
+ prefer(names: string[] | Record<string, boolean>): void {
253
+ if (Array.isArray(names)) {
254
+ this.#preferred = new Set(names.filter(name => avail[name]));
255
+ } else {
256
+ for (const [name, enable] of Object.entries(names)) {
257
+ if (enable && avail[name]) {
258
+ this.#preferred.add(name);
259
+ } else {
260
+ this.#preferred.delete(name);
261
+ }
262
+ }
263
+ }
268
264
  this.#view.dispatch({
269
265
  effects: [
270
- this.#extensions.reconfigure(names.map(name => {
271
- const option = avail[name];
272
- if (option) {
273
- const [extension, configs] = option;
274
- return extension(configs[this.#lang]);
275
- }
276
- return [];
266
+ this.#extensions.reconfigure([...this.#preferred].map(name => {
267
+ const [extension, configs] = avail[name]!;
268
+ return extension(configs[this.#lang]);
277
269
  })),
278
270
  ],
279
271
  });
@@ -293,11 +285,21 @@ export class CodeMirror6 {
293
285
  async getLinter(opt?: Record<string, unknown>): Promise<LintSource | undefined> {
294
286
  switch (this.#lang) {
295
287
  case 'mediawiki': {
296
- const CDN = 'npm/wikiparser-node@1.3.4-b/extensions/dist',
297
- src = `combine/${CDN}/base.min.js,${CDN}/lint.min.js`;
288
+ const REPO = 'npm/wikiparser-node@1.4.1-b',
289
+ DIR = `${REPO}/extensions/dist`,
290
+ src = `combine/${DIR}/base.min.js,${DIR}/lint.min.js`,
291
+ lang = opt?.['i18n'];
298
292
  await loadScript(src, 'wikiparse');
299
- const wikiLinter = new wikiparse.Linter(opt?.['include'] as boolean);
300
- return doc => wikiLinter.codemirror(doc.toString());
293
+ if (typeof lang === 'string') {
294
+ try {
295
+ const i18n: Record<string, string>
296
+ = await (await fetch(`${CDN}/${REPO}/i18n/${lang.toLowerCase()}.json`)).json();
297
+ wikiparse.setI18N(i18n);
298
+ } catch {}
299
+ }
300
+ const wikiLinter = new wikiparse.Linter(opt?.['include'] as boolean | undefined);
301
+ return async doc =>
302
+ (await wikiLinter.codemirror(doc.toString())).filter(({severity}) => severity === 'error');
301
303
  }
302
304
  case 'javascript': {
303
305
  await loadScript('npm/eslint-linter-browserify', 'eslint');
@@ -434,11 +436,39 @@ export class CodeMirror6 {
434
436
  */
435
437
  toggle(show = !this.#visible): void {
436
438
  if (show && !this.#visible) {
437
- this.setContent(this.#textarea.value);
439
+ const {value, selectionStart, selectionEnd, scrollTop} = this.#textarea,
440
+ hasFocus = document.activeElement === this.#textarea;
441
+ this.setContent(value);
438
442
  this.#refresh();
443
+ this.#view.dom.style.setProperty('display', '');
444
+ this.#textarea.style.display = 'none';
445
+ this.#view.requestMeasure();
446
+ this.#view.dispatch({
447
+ selection: {anchor: selectionStart, head: selectionEnd},
448
+ });
449
+ if (hasFocus) {
450
+ this.#view.focus();
451
+ }
452
+ requestAnimationFrame(() => {
453
+ this.#view.scrollDOM.scrollTop = scrollTop;
454
+ });
455
+ } else if (!show && this.#visible) {
456
+ const {state: {selection: {main: {from, to}}}, hasFocus} = this.#view,
457
+ {scrollDOM: {scrollTop}} = this.#view;
458
+ this.#view.dom.style.setProperty('display', 'none', 'important');
459
+ this.#textarea.style.display = '';
460
+ this.#textarea.setSelectionRange(
461
+ Math.min(from, to),
462
+ Math.max(from, to),
463
+ from > to ? 'backward' : 'forward',
464
+ );
465
+ if (hasFocus) {
466
+ this.#textarea.focus();
467
+ }
468
+ requestAnimationFrame(() => {
469
+ this.#textarea.scrollTop = scrollTop;
470
+ });
439
471
  }
440
472
  this.#visible = show;
441
- this.#view.dom.style.setProperty('display', show ? '' : 'none', 'important');
442
- this.#textarea.style.display = show ? 'none' : '';
443
473
  }
444
474
  }
package/src/mediawiki.ts CHANGED
@@ -951,7 +951,7 @@ class MediaWiki {
951
951
  const isCloseTag = Boolean(stream.eat('/')),
952
952
  mt = stream.match(/^[^>/\s.*,[\]{}$^+?|\\'`~<=!@#%&()-]+/u) as RegExpMatchArray | false;
953
953
  if (mt) {
954
- const tagname = mt[0]!.toLowerCase();
954
+ const tagname = mt[0].toLowerCase();
955
955
  if (tagname in this.config.tags) {
956
956
  // Parser function
957
957
  state.stack.push(state.tokenize);
package/mw/dist/base.js DELETED
@@ -1,265 +0,0 @@
1
- import { CodeMirror6 } from 'https://testingcf.jsdelivr.net/npm/@bhsd/codemirror-mediawiki@2.1.11/dist/main.min.js';
2
- (() => {
3
- var _a;
4
- mw.loader.load('https://testingcf.jsdelivr.net/npm/@bhsd/codemirror-mediawiki@2.1.11/mediawiki.min.css', 'text/css');
5
- mw.loader.addStyleTag('.wikiEditor-ui-toolbar{z-index:7}');
6
- const instances = new WeakMap();
7
- const getInstance = ($ele) => instances.get($ele[0]);
8
- $.valHooks['textarea'] = {
9
- get(elem) {
10
- const cm = instances.get(elem);
11
- return (cm === null || cm === void 0 ? void 0 : cm.visible) ? cm.view.state.doc.toString() : elem.value;
12
- },
13
- set(elem, value) {
14
- const cm = instances.get(elem);
15
- if (cm === null || cm === void 0 ? void 0 : cm.visible) {
16
- cm.setContent(value);
17
- }
18
- else {
19
- elem.value = value;
20
- }
21
- },
22
- };
23
- function getCaretPosition(option) {
24
- const { view: { state: { selection: { main } } } } = getInstance(this);
25
- return (option === null || option === void 0 ? void 0 : option.startAndEnd) ? [main.from, main.to] : main.head;
26
- }
27
- const textSelection = {
28
- getContents() {
29
- return getInstance(this).view.state.doc.toString();
30
- },
31
- setContents(content) {
32
- getInstance(this).setContent(content);
33
- return this;
34
- },
35
- getSelection() {
36
- const { view: { state } } = getInstance(this);
37
- return state.sliceDoc(state.selection.main.from, state.selection.main.to);
38
- },
39
- setSelection({ start, end }) {
40
- const { view } = getInstance(this);
41
- view.dispatch({
42
- selection: { anchor: start, head: end !== null && end !== void 0 ? end : start },
43
- });
44
- view.focus();
45
- return this;
46
- },
47
- replaceSelection(value) {
48
- const { view } = getInstance(this);
49
- view.dispatch(view.state.replaceSelection(value));
50
- return this;
51
- },
52
- getCaretPosition,
53
- scrollToCaretPosition() {
54
- getInstance(this).view.dispatch({ scrollIntoView: true });
55
- return this;
56
- },
57
- };
58
- const USING_LOCAL = mw.loader.getState('ext.CodeMirror') !== null, DATA_MODULE = mw.loader.getState('ext.CodeMirror.data') ? 'ext.CodeMirror.data' : 'ext.CodeMirror', ALL_SETTINGS_CACHE = (_a = JSON.parse(localStorage.getItem('InPageEditMwConfig'))) !== null && _a !== void 0 ? _a : {}, SITE_ID = `${mw.config.get('wgServerName')}${mw.config.get('wgScriptPath')}`, SITE_SETTINGS = ALL_SETTINGS_CACHE[SITE_ID], VALID = Number(SITE_SETTINGS === null || SITE_SETTINGS === void 0 ? void 0 : SITE_SETTINGS.time) > Date.now() - 86400 * 1000 * 30;
59
- const getConfig = (magicWords, rule, flip) => {
60
- const words = magicWords.filter(rule).filter(({ 'case-sensitive': i }) => i !== flip)
61
- .flatMap(({ aliases, name, 'case-sensitive': i }) => aliases.map(alias => ({
62
- alias: (i ? alias : alias.toLowerCase()).replace(/:$/u, ''),
63
- name,
64
- }))), obj = {};
65
- for (const { alias, name } of words) {
66
- obj[alias] = name;
67
- }
68
- return obj;
69
- };
70
- const getConfigPair = (magicWords, rule) => [true, false]
71
- .map(bool => getConfig(magicWords, rule, bool));
72
- const setConfig = (config) => {
73
- mw.config.set('extCodeMirrorConfig', config);
74
- };
75
- const getMwConfig = async () => {
76
- if (USING_LOCAL && !VALID) {
77
- await mw.loader.using(DATA_MODULE);
78
- }
79
- let config = mw.config.get('extCodeMirrorConfig');
80
- if (!config && VALID) {
81
- ({ config } = SITE_SETTINGS);
82
- setConfig(config);
83
- }
84
- const isIPE = config && Object.values(config.functionSynonyms[0]).includes(true);
85
- if ((config === null || config === void 0 ? void 0 : config.img) && config.variants && !isIPE) {
86
- return {
87
- ...config,
88
- nsid: mw.config.get('wgNamespaceIds'),
89
- };
90
- }
91
- const { query: { general: { variants }, magicwords, extensiontags, functionhooks, variables }, } = await new mw.Api().get({
92
- meta: 'siteinfo',
93
- siprop: [
94
- 'general',
95
- 'magicwords',
96
- ...config && !isIPE ? [] : ['extensiontags', 'functionhooks', 'variables'],
97
- ],
98
- formatversion: '2',
99
- });
100
- const others = new Set(['msg', 'raw', 'msgnw', 'subst', 'safesubst']);
101
- if (config && !isIPE) {
102
- const { functionSynonyms: [insensitive] } = config;
103
- if (!('subst' in insensitive)) {
104
- Object.assign(insensitive, getConfig(magicwords, ({ name }) => others.has(name)));
105
- }
106
- }
107
- else {
108
- config = {
109
- tagModes: {
110
- tab: 'text/mediawiki',
111
- indicator: 'text/mediawiki',
112
- poem: 'text/mediawiki',
113
- ref: 'text/mediawiki',
114
- option: 'text/mediawiki',
115
- combooption: 'text/mediawiki',
116
- tabs: 'text/mediawiki',
117
- poll: 'text/mediawiki',
118
- gallery: 'text/mediawiki',
119
- },
120
- tags: {},
121
- urlProtocols: mw.config.get('wgUrlProtocols'),
122
- };
123
- for (const tag of extensiontags) {
124
- config.tags[tag.slice(1, -1)] = true;
125
- }
126
- const functions = new Set([
127
- ...functionhooks,
128
- ...variables,
129
- ...others,
130
- ]);
131
- config.functionSynonyms = getConfigPair(magicwords, ({ name }) => functions.has(name));
132
- config.doubleUnderscore = getConfigPair(magicwords, ({ aliases }) => aliases.some(alias => /^__.+__$/u.test(alias)));
133
- }
134
- config.img = getConfig(magicwords, ({ name }) => name.startsWith('img_'));
135
- config.variants = variants ? variants.map(({ code }) => code) : [];
136
- config.nsid = mw.config.get('wgNamespaceIds');
137
- setConfig(config);
138
- ALL_SETTINGS_CACHE[SITE_ID] = { config: config, time: Date.now() };
139
- localStorage.setItem('InPageEditMwConfig', JSON.stringify(ALL_SETTINGS_CACHE));
140
- return config;
141
- };
142
- const linters = {};
143
- class CodeMirror extends CodeMirror6 {
144
- constructor(textarea, lang, config) {
145
- super(textarea, lang, config);
146
- instances.set(textarea, this);
147
- if (mw.loader.getState('jquery.textSelection') === 'ready') {
148
- $(textarea).data('jquery.textSelection', textSelection);
149
- }
150
- mw.hook('wiki-codemirror6').fire(this);
151
- }
152
- toggle(show = !this.visible) {
153
- super.toggle(show);
154
- $(this.textarea).data('jquery.textSelection', show && textSelection);
155
- }
156
- async getLinter(opt) {
157
- const linter = await super.getLinter(opt);
158
- linters[this.lang] = linter;
159
- return linter;
160
- }
161
- async defaultLint(on, optOrNs) {
162
- if (!on) {
163
- this.lint();
164
- return;
165
- }
166
- const { lang } = this;
167
- let opt;
168
- if (typeof optOrNs === 'number') {
169
- if (lang === 'mediawiki' && (optOrNs === 10 || optOrNs === 828 || optOrNs === 2)) {
170
- opt = { include: true };
171
- }
172
- else if (lang === 'javascript' && (optOrNs === 8 || optOrNs === 2300)) {
173
- opt = {
174
- env: { browser: true, es6: true },
175
- parserOptions: { ecmaVersion: 6 },
176
- };
177
- }
178
- }
179
- else {
180
- opt = optOrNs;
181
- }
182
- if (!(lang in linters)) {
183
- await this.getLinter(opt);
184
- if (lang === 'mediawiki') {
185
- const mwConfig = await getMwConfig(), config = {
186
- ...await wikiparse.getConfig(),
187
- ext: Object.keys(mwConfig.tags),
188
- namespaces: mw.config.get('wgFormattedNamespaces'),
189
- nsid: mwConfig.nsid,
190
- doubleUnderscore: mwConfig.doubleUnderscore.map(obj => Object.keys(obj).map(s => s.slice(2, -2))),
191
- variants: mwConfig.variants,
192
- protocol: mwConfig.urlProtocols.replace(/\\:/gu, ':'),
193
- };
194
- [config.parserFunction[0]] = mwConfig.functionSynonyms;
195
- if (!USING_LOCAL) {
196
- for (const [key, val] of Object.entries(mwConfig.functionSynonyms[0])) {
197
- if (!key.startsWith('#')) {
198
- config.parserFunction[0][`#${key}`] = val;
199
- }
200
- }
201
- }
202
- config.parserFunction[1] = [
203
- ...Object.keys(mwConfig.functionSynonyms[1]),
204
- '=',
205
- ];
206
- for (const key of Object.keys(mwConfig.img)) {
207
- config.img[key] = mwConfig.img[key].slice(4);
208
- }
209
- wikiparse.setConfig(config);
210
- }
211
- }
212
- else if (opt) {
213
- await this.getLinter(opt);
214
- }
215
- if (linters[lang]) {
216
- this.lint(linters[lang]);
217
- }
218
- }
219
- static async fromTextArea(textarea, lang) {
220
- const isWiki = lang === 'mediawiki' || lang === 'html', cm = new CodeMirror(textarea, isWiki ? undefined : lang);
221
- if (isWiki) {
222
- const config = await getMwConfig();
223
- cm.setLanguage(lang, config);
224
- }
225
- return cm;
226
- }
227
- }
228
- document.body.addEventListener('click', e => {
229
- if (e.target instanceof HTMLTextAreaElement && e.shiftKey && !instances.has(e.target)) {
230
- e.preventDefault();
231
- (async () => {
232
- var _a;
233
- const { wgAction, wgNamespaceNumber, wgPageContentModel } = mw.config.get();
234
- let lang;
235
- if (wgAction !== 'edit' && wgAction !== 'submit') {
236
- await mw.loader.using('oojs-ui-windows');
237
- lang = (_a = (await OO.ui.prompt('Language:') || undefined)) === null || _a === void 0 ? void 0 : _a.toLowerCase();
238
- }
239
- else {
240
- switch (wgPageContentModel) {
241
- case 'css':
242
- case 'sanitized-css':
243
- lang = 'css';
244
- break;
245
- case 'javascript':
246
- lang = 'javascript';
247
- break;
248
- case 'json':
249
- lang = 'json';
250
- break;
251
- case 'Scribunto':
252
- lang = 'lua';
253
- break;
254
- case 'wikitext':
255
- lang = wgNamespaceNumber === 274 ? 'html' : 'mediawiki';
256
- break;
257
- }
258
- }
259
- const cm = await CodeMirror.fromTextArea(e.target, lang);
260
- void cm.defaultLint(true, wgNamespaceNumber);
261
- })();
262
- }
263
- });
264
- Object.assign(window, { CodeMirror6: CodeMirror });
265
- })();