@bhsd/codemirror-mediawiki 2.1.5 → 2.1.8

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/dist/base.js CHANGED
@@ -1,8 +1,9 @@
1
- import { CodeMirror6 } from 'https://testingcf.jsdelivr.net/npm/@bhsd/codemirror-mediawiki@2.1.5/dist/main.min.js';
1
+ import { CodeMirror6 } from 'https://testingcf.jsdelivr.net/npm/@bhsd/codemirror-mediawiki@2.1.8/dist/main.min.js';
2
2
  (() => {
3
3
  var _a;
4
- mw.loader.load('https://testingcf.jsdelivr.net/npm/@bhsd/codemirror-mediawiki@2.1.5/mediawiki.min.css', 'text/css');
4
+ mw.loader.load('https://testingcf.jsdelivr.net/npm/@bhsd/codemirror-mediawiki@2.1.8/mediawiki.min.css', 'text/css');
5
5
  const instances = new WeakMap();
6
+ const getInstance = ($ele) => instances.get($ele[0]);
6
7
  $.valHooks['textarea'] = {
7
8
  get(elem) {
8
9
  const cm = instances.get(elem);
@@ -11,9 +12,7 @@ import { CodeMirror6 } from 'https://testingcf.jsdelivr.net/npm/@bhsd/codemirror
11
12
  set(elem, value) {
12
13
  const cm = instances.get(elem);
13
14
  if (cm === null || cm === void 0 ? void 0 : cm.visible) {
14
- cm.view.dispatch({
15
- changes: { from: 0, to: cm.view.state.doc.length, insert: value },
16
- });
15
+ cm.setContent(value);
17
16
  }
18
17
  else {
19
18
  elem.value = value;
@@ -21,26 +20,23 @@ import { CodeMirror6 } from 'https://testingcf.jsdelivr.net/npm/@bhsd/codemirror
21
20
  },
22
21
  };
23
22
  function getCaretPosition(option) {
24
- const { view: { state: { selection: { main } } } } = instances.get(this[0]);
23
+ const { view: { state: { selection: { main } } } } = getInstance(this);
25
24
  return (option === null || option === void 0 ? void 0 : option.startAndEnd) ? [main.from, main.to] : main.head;
26
25
  }
27
26
  const textSelection = {
28
27
  getContents() {
29
- return instances.get(this[0]).view.state.doc.toString();
28
+ return getInstance(this).view.state.doc.toString();
30
29
  },
31
30
  setContents(content) {
32
- const { view } = instances.get(this[0]);
33
- view.dispatch({
34
- changes: { from: 0, to: view.state.doc.length, insert: content },
35
- });
31
+ getInstance(this).setContent(content);
36
32
  return this;
37
33
  },
38
34
  getSelection() {
39
- const { view: { state } } = instances.get(this[0]);
35
+ const { view: { state } } = getInstance(this);
40
36
  return state.sliceDoc(state.selection.main.from, state.selection.main.to);
41
37
  },
42
38
  setSelection({ start, end }) {
43
- const { view } = instances.get(this[0]);
39
+ const { view } = getInstance(this);
44
40
  view.dispatch({
45
41
  selection: { anchor: start, head: end !== null && end !== void 0 ? end : start },
46
42
  });
@@ -48,36 +44,44 @@ import { CodeMirror6 } from 'https://testingcf.jsdelivr.net/npm/@bhsd/codemirror
48
44
  return this;
49
45
  },
50
46
  replaceSelection(value) {
51
- const { view } = instances.get(this[0]);
47
+ const { view } = getInstance(this);
52
48
  view.dispatch(view.state.replaceSelection(value));
53
49
  return this;
54
50
  },
55
51
  getCaretPosition,
56
52
  scrollToCaretPosition() {
57
- instances.get(this[0]).view.dispatch({ scrollIntoView: true });
53
+ getInstance(this).view.dispatch({ scrollIntoView: true });
58
54
  return this;
59
55
  },
60
56
  };
61
- 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], EXPIRED = !(SITE_SETTINGS && SITE_SETTINGS.time > Date.now() - 86400 * 1000 * 30);
62
- const getAliases = (words) => words.flatMap(({ aliases, name }) => aliases.map(alias => ({ alias, name })));
63
- const getConfig = (aliases) => {
64
- const config = {};
65
- for (const { alias, name } of aliases) {
66
- config[alias.replace(/:$/u, '')] = name;
57
+ 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 = (SITE_SETTINGS === null || SITE_SETTINGS === void 0 ? void 0 : SITE_SETTINGS.time) > Date.now() - 86400 * 1000 * 30;
58
+ const getConfig = (magicWords, rule, flip) => {
59
+ const words = magicWords.filter(rule).filter(({ 'case-sensitive': i }) => i !== flip)
60
+ .flatMap(({ aliases, name, 'case-sensitive': i }) => aliases.map(alias => ({
61
+ alias: (i ? alias : alias.toLowerCase()).replace(/:$/u, ''),
62
+ name,
63
+ }))), obj = {};
64
+ for (const { alias, name } of words) {
65
+ obj[alias] = name;
67
66
  }
68
- return config;
67
+ return obj;
68
+ };
69
+ const getConfigPair = (magicWords, rule) => [true, false]
70
+ .map(bool => getConfig(magicWords, rule, bool));
71
+ const setConfig = (config) => {
72
+ mw.config.set('extCodeMirrorConfig', config);
69
73
  };
70
74
  const getMwConfig = async () => {
71
- if (USING_LOCAL && EXPIRED) {
75
+ if (USING_LOCAL && !VALID) {
72
76
  await mw.loader.using(DATA_MODULE);
73
77
  }
74
78
  let config = mw.config.get('extCodeMirrorConfig');
75
- if (!config && !EXPIRED) {
79
+ if (!config && VALID) {
76
80
  ({ config } = SITE_SETTINGS);
77
- mw.config.set('extCodeMirrorConfig', config);
81
+ setConfig(config);
78
82
  }
79
83
  const isIPE = config && Object.values(config.functionSynonyms[0]).includes(true);
80
- if (config && config.img && config.variants && !isIPE) {
84
+ if ((config === null || config === void 0 ? void 0 : config.img) && config.variants && !isIPE) {
81
85
  return {
82
86
  ...config,
83
87
  nsid: mw.config.get('wgNamespaceIds'),
@@ -92,20 +96,25 @@ import { CodeMirror6 } from 'https://testingcf.jsdelivr.net/npm/@bhsd/codemirror
92
96
  ],
93
97
  formatversion: '2',
94
98
  });
95
- const otherMagicwords = new Set(['msg', 'raw', 'msgnw', 'subst', 'safesubst']);
99
+ const others = new Set(['msg', 'raw', 'msgnw', 'subst', 'safesubst']);
96
100
  if (config && !isIPE) {
97
101
  const { functionSynonyms: [insensitive] } = config;
98
102
  if (!('subst' in insensitive)) {
99
- const aliases = getAliases(magicwords.filter(({ name }) => otherMagicwords.has(name)));
100
- for (const { alias, name } of aliases) {
101
- insensitive[alias.replace(/:$/u, '')] = name;
102
- }
103
+ Object.assign(insensitive, getConfig(magicwords, ({ name }) => others.has(name)));
103
104
  }
104
105
  }
105
106
  else {
106
107
  config = {
107
108
  tagModes: {
109
+ tab: 'text/mediawiki',
110
+ indicator: 'text/mediawiki',
111
+ poem: 'text/mediawiki',
108
112
  ref: 'text/mediawiki',
113
+ option: 'text/mediawiki',
114
+ combooption: 'text/mediawiki',
115
+ tabs: 'text/mediawiki',
116
+ poll: 'text/mediawiki',
117
+ gallery: 'text/mediawiki',
109
118
  },
110
119
  tags: {},
111
120
  urlProtocols: mw.config.get('wgUrlProtocols'),
@@ -113,24 +122,18 @@ import { CodeMirror6 } from 'https://testingcf.jsdelivr.net/npm/@bhsd/codemirror
113
122
  for (const tag of extensiontags) {
114
123
  config.tags[tag.slice(1, -1)] = true;
115
124
  }
116
- const realMagicwords = new Set([
125
+ const functions = new Set([
117
126
  ...functionhooks,
118
127
  ...variables,
119
- ...otherMagicwords,
120
- ]), allMagicwords = magicwords.filter(({ name, aliases }) => aliases.some(alias => /^__.+__$/u.test(alias)) || realMagicwords.has(name)), sensitive = getAliases(allMagicwords.filter(word => word['case-sensitive'])), insensitive = getAliases(allMagicwords.filter(word => !word['case-sensitive'])).map(({ alias, name }) => ({ alias: alias.toLowerCase(), name }));
121
- config.doubleUnderscore = [
122
- getConfig(insensitive.filter(({ alias }) => /^__.+__$/u.test(alias))),
123
- getConfig(sensitive.filter(({ alias }) => /^__.+__$/u.test(alias))),
124
- ];
125
- config.functionSynonyms = [
126
- getConfig(insensitive.filter(({ alias }) => !/^__.+__|^#$/u.test(alias))),
127
- getConfig(sensitive.filter(({ alias }) => !/^__.+__|^#$/u.test(alias))),
128
- ];
128
+ ...others,
129
+ ]);
130
+ config.functionSynonyms = getConfigPair(magicwords, ({ name }) => functions.has(name));
131
+ config.doubleUnderscore = getConfigPair(magicwords, ({ aliases }) => aliases.some(alias => /^__.+__$/u.test(alias)));
129
132
  }
130
- config.img = getConfig(getAliases(magicwords.filter(({ name }) => name.startsWith('img_'))));
133
+ config.img = getConfig(magicwords, ({ name }) => name.startsWith('img_'));
131
134
  config.variants = variants ? variants.map(({ code }) => code) : [];
132
135
  config.nsid = mw.config.get('wgNamespaceIds');
133
- mw.config.set('extCodeMirrorConfig', config);
136
+ setConfig(config);
134
137
  ALL_SETTINGS_CACHE[SITE_ID] = { config: config, time: Date.now() };
135
138
  localStorage.setItem('InPageEditMwConfig', JSON.stringify(ALL_SETTINGS_CACHE));
136
139
  return config;
@@ -149,6 +152,11 @@ import { CodeMirror6 } from 'https://testingcf.jsdelivr.net/npm/@bhsd/codemirror
149
152
  super.toggle(show);
150
153
  $(this.textarea).data('jquery.textSelection', show && textSelection);
151
154
  }
155
+ async getLinter(opt) {
156
+ const linter = await super.getLinter(opt);
157
+ linters[this.lang] = linter;
158
+ return linter;
159
+ }
152
160
  async defaultLint(on, opt) {
153
161
  if (!on) {
154
162
  this.lint();
@@ -156,13 +164,13 @@ import { CodeMirror6 } from 'https://testingcf.jsdelivr.net/npm/@bhsd/codemirror
156
164
  }
157
165
  const { lang } = this;
158
166
  if (!(lang in linters)) {
159
- linters[lang] = await this.getLinter(opt);
160
- if (this.lang === 'mediawiki' || this.lang === 'html') {
167
+ await this.getLinter(opt);
168
+ if (lang === 'mediawiki') {
161
169
  const mwConfig = await getMwConfig(), config = {
162
170
  ...await wikiparse.getConfig(),
163
171
  ext: Object.keys(mwConfig.tags),
164
172
  namespaces: mw.config.get('wgFormattedNamespaces'),
165
- nsid: mw.config.get('wgNamespaceIds'),
173
+ nsid: mwConfig.nsid,
166
174
  doubleUnderscore: mwConfig.doubleUnderscore.map(obj => Object.keys(obj).map(s => s.slice(2, -2))),
167
175
  variants: mwConfig.variants,
168
176
  protocol: mwConfig.urlProtocols.replace(/\\:/gu, ':'),
@@ -185,6 +193,9 @@ import { CodeMirror6 } from 'https://testingcf.jsdelivr.net/npm/@bhsd/codemirror
185
193
  wikiparse.setConfig(config);
186
194
  }
187
195
  }
196
+ else if (opt) {
197
+ await this.getLinter(opt);
198
+ }
188
199
  if (linters[lang]) {
189
200
  this.lint(linters[lang]);
190
201
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhsd/codemirror-mediawiki",
3
- "version": "2.1.5",
3
+ "version": "2.1.8",
4
4
  "description": "Modified CodeMirror mode based on wikimedia/mediawiki-extensions-CodeMirror",
5
5
  "keywords": [
6
6
  "mediawiki",
@@ -12,7 +12,11 @@
12
12
  },
13
13
  "license": "GPL-2.0",
14
14
  "files":[
15
- "/dist/*.[jt]s",
15
+ "/src/",
16
+ "!/src/gh-page.ts",
17
+ "!/src/plugins.ts",
18
+ "!/src/*.d.ts",
19
+ "/dist/",
16
20
  "/mediawiki.css",
17
21
  "/mw/dist/"
18
22
  ],
@@ -66,6 +70,6 @@
66
70
  "stylelint-config-recommended": "^14.0.0",
67
71
  "types-mediawiki": "^1.4.0",
68
72
  "typescript": "^5.1.6",
69
- "wikilint": "^2.3.0"
73
+ "wikilint": "^2.3.2"
70
74
  }
71
75
  }
@@ -0,0 +1,419 @@
1
+ import {Compartment, EditorState} from '@codemirror/state';
2
+ import {
3
+ EditorView,
4
+ lineNumbers,
5
+ keymap,
6
+ highlightSpecialChars,
7
+ highlightActiveLine,
8
+ highlightWhitespace,
9
+ highlightTrailingWhitespace,
10
+ } from '@codemirror/view';
11
+ import {
12
+ syntaxHighlighting,
13
+ defaultHighlightStyle,
14
+ indentOnInput,
15
+ StreamLanguage,
16
+ LanguageSupport,
17
+ bracketMatching,
18
+ indentUnit,
19
+ } from '@codemirror/language';
20
+ import {defaultKeymap, historyKeymap, history} from '@codemirror/commands';
21
+ import {searchKeymap} from '@codemirror/search';
22
+ import {linter, lintGutter, openLintPanel, closeLintPanel} from '@codemirror/lint';
23
+ import {closeBrackets} from '@codemirror/autocomplete';
24
+ import {mediawiki, html} from './mediawiki';
25
+ import * as plugins from './plugins';
26
+ import type {ViewPlugin} from '@codemirror/view';
27
+ import type {Extension, Text} from '@codemirror/state';
28
+ import type {Diagnostic} from '@codemirror/lint';
29
+ import type {Highlighter} from '@lezer/highlight';
30
+ import type {Linter} from 'eslint';
31
+
32
+ export type {MwConfig} from './mediawiki';
33
+ export type LintSource = (doc: Text) => Diagnostic[] | Promise<Diagnostic[]>;
34
+
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ const languages: Record<string, (config?: any) => LanguageSupport | []> = {
37
+ plain: () => [],
38
+ mediawiki,
39
+ html,
40
+ };
41
+ for (const [language, parser] of Object.entries(plugins)) {
42
+ languages[language] = (): LanguageSupport => new LanguageSupport(StreamLanguage.define(parser));
43
+ }
44
+ const linters: Record<string, Extension> = {};
45
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
+ const avail: Record<string, [ (config?: any) => Extension, Record<string, unknown> ]> = {
47
+ highlightSpecialChars: [highlightSpecialChars, {}],
48
+ highlightActiveLine: [highlightActiveLine, {}],
49
+ highlightWhitespace: [highlightWhitespace, {}],
50
+ highlightTrailingWhitespace: [highlightTrailingWhitespace, {}],
51
+ bracketMatching: [bracketMatching, {mediawiki: {brackets: '[]{}'}}],
52
+ closeBrackets: [closeBrackets, {}],
53
+ };
54
+
55
+ /**
56
+ * 使用传统方法加载脚本
57
+ * @param src 脚本地址
58
+ * @param target 脚本全局变量名
59
+ */
60
+ const loadScript = (src: string, target: string): Promise<void> => new Promise(resolve => {
61
+ if (target in window) {
62
+ resolve();
63
+ return;
64
+ }
65
+ const script = document.createElement('script');
66
+ script.src = `https://testingcf.jsdelivr.net/${src}`;
67
+ script.onload = (): void => {
68
+ resolve();
69
+ };
70
+ document.head.append(script);
71
+ });
72
+
73
+ /**
74
+ * 获取指定行列的位置
75
+ * @param doc 文档
76
+ * @param line 行号
77
+ * @param column 列号
78
+ */
79
+ const pos = (doc: Text, line: number, column: number): number => doc.line(line).from + column - 1;
80
+
81
+ export class CodeMirror6 {
82
+ readonly #textarea;
83
+ readonly #language;
84
+ readonly #linter;
85
+ readonly #extensions;
86
+ readonly #indent;
87
+ readonly #view;
88
+ #lang;
89
+ #visible = true;
90
+
91
+ get textarea(): HTMLTextAreaElement {
92
+ return this.#textarea;
93
+ }
94
+
95
+ get view(): EditorView {
96
+ return this.#view;
97
+ }
98
+
99
+ get lang(): string {
100
+ return this.#lang;
101
+ }
102
+
103
+ get visible(): boolean {
104
+ return this.#visible;
105
+ }
106
+
107
+ /**
108
+ * @param textarea 文本框
109
+ * @param lang 语言
110
+ * @param config 语言设置
111
+ */
112
+ constructor(textarea: HTMLTextAreaElement, lang = 'plain', config?: unknown) {
113
+ this.#textarea = textarea;
114
+ this.#lang = lang;
115
+ this.#language = new Compartment();
116
+ this.#linter = new Compartment();
117
+ this.#extensions = new Compartment();
118
+ this.#indent = new Compartment();
119
+ let timer: number | undefined;
120
+ const extensions = [
121
+ this.#language.of(languages[lang]!(config)),
122
+ this.#linter.of([]),
123
+ this.#extensions.of([]),
124
+ this.#indent.of(indentUnit.of('\t')),
125
+ syntaxHighlighting(defaultHighlightStyle as Highlighter),
126
+ EditorView.contentAttributes.of({
127
+ accesskey: textarea.accessKey,
128
+ dir: textarea.dir,
129
+ lang: textarea.lang,
130
+ }),
131
+ EditorState.readOnly.of(textarea.readOnly),
132
+ lineNumbers(),
133
+ EditorView.lineWrapping,
134
+ history(),
135
+ indentOnInput(),
136
+ keymap.of([
137
+ ...defaultKeymap,
138
+ ...historyKeymap,
139
+ ...searchKeymap,
140
+ ]),
141
+ EditorView.updateListener.of(({state: {doc}, docChanged}) => {
142
+ if (docChanged) {
143
+ clearTimeout(timer);
144
+ timer = window.setTimeout(() => {
145
+ textarea.value = doc.toString();
146
+ }, 400);
147
+ }
148
+ }),
149
+ ];
150
+ this.#view = new EditorView({
151
+ extensions,
152
+ doc: textarea.value,
153
+ });
154
+ const {selectionStart, selectionEnd, scrollTop} = textarea,
155
+ {fontSize, lineHeight} = getComputedStyle(textarea),
156
+ hasFocus = document.activeElement === textarea;
157
+ textarea.parentNode!.insertBefore(this.#view.dom, textarea);
158
+ this.#minHeight();
159
+ this.#refresh();
160
+ this.#view.dom.style.fontSize = fontSize;
161
+ this.#view.scrollDOM.style.lineHeight = lineHeight;
162
+ this.#view.requestMeasure();
163
+ this.#view.dispatch({
164
+ selection: {anchor: selectionStart, head: selectionEnd},
165
+ });
166
+ textarea.style.display = 'none';
167
+ if (hasFocus) {
168
+ this.#view.focus();
169
+ }
170
+ requestAnimationFrame(() => {
171
+ this.#view.scrollDOM.scrollTop = scrollTop;
172
+ });
173
+ }
174
+
175
+ /** 刷新编辑器高度 */
176
+ #refresh(): void {
177
+ const {offsetHeight} = this.#textarea;
178
+ this.#view.dom.style.height = offsetHeight ? `${offsetHeight}px` : this.#textarea.style.height;
179
+ }
180
+
181
+ /**
182
+ * 设置编辑器最小高度
183
+ * @param linting 是否启用语法检查
184
+ */
185
+ #minHeight(linting?: boolean): void {
186
+ this.#view.dom.style.minHeight = linting ? 'calc(100px + 2em)' : '2em';
187
+ }
188
+
189
+ /**
190
+ * 设置语言
191
+ * @param lang 语言
192
+ * @param config 语言设置
193
+ */
194
+ setLanguage(lang = 'plain', config?: unknown): void {
195
+ this.#view.dispatch({
196
+ effects: [
197
+ this.#language.reconfigure(languages[lang]!(config)),
198
+ this.#linter.reconfigure(linters[lang] || []),
199
+ ],
200
+ });
201
+ this.#lang = lang;
202
+ (linters[lang] ? openLintPanel : closeLintPanel)(this.#view);
203
+ }
204
+
205
+ /**
206
+ * 开始语法检查
207
+ * @param lintSource 语法检查函数
208
+ */
209
+ lint(lintSource?: LintSource): void {
210
+ const linterExtension = lintSource
211
+ ? [
212
+ linter(view => lintSource(view.state.doc)),
213
+ lintGutter(),
214
+ ]
215
+ : [];
216
+ if (lintSource) {
217
+ linters[this.#lang] = linterExtension;
218
+ this.#minHeight(true);
219
+ } else {
220
+ delete linters[this.#lang];
221
+ this.#minHeight();
222
+ }
223
+ this.#view.dispatch({
224
+ effects: [this.#linter.reconfigure(linterExtension)],
225
+ });
226
+ (lintSource ? openLintPanel : closeLintPanel)(this.#view);
227
+ }
228
+
229
+ /** 立即更新语法检查 */
230
+ update(): void {
231
+ const extension = this.#linter.get(this.#view.state) as [[ unknown, ViewPlugin<{
232
+ set: boolean;
233
+ force(): void;
234
+ }> ]] | [];
235
+ if (extension.length > 0) {
236
+ const plugin = this.#view.plugin(extension[0]![1])!;
237
+ plugin.set = true;
238
+ plugin.force();
239
+ }
240
+ }
241
+
242
+ /**
243
+ * 添加扩展
244
+ * @param names 扩展名
245
+ */
246
+ prefer(names: readonly string[]): void {
247
+ this.#view.dispatch({
248
+ effects: [
249
+ this.#extensions.reconfigure(names.map(name => {
250
+ const [extension, configs] = avail[name]!;
251
+ return extension(configs[this.#lang]);
252
+ })),
253
+ ],
254
+ });
255
+ }
256
+
257
+ /**
258
+ * 设置缩进
259
+ * @param indent 缩进字符串
260
+ */
261
+ setIndent(indent: string): void {
262
+ this.#view.dispatch({
263
+ effects: [this.#indent.reconfigure(indentUnit.of(indent))],
264
+ });
265
+ }
266
+
267
+ /** 获取默认linter */
268
+ async getLinter(opt?: Record<string, unknown>): Promise<LintSource | undefined> {
269
+ switch (this.#lang) {
270
+ case 'mediawiki': {
271
+ const CDN = 'npm/wikiparser-node@1.3.4-b/extensions/dist',
272
+ src = `combine/${CDN}/base.min.js,${CDN}/lint.min.js`;
273
+ await loadScript(src, 'wikiparse');
274
+ const wikiLinter = new wikiparse.Linter(opt?.['include'] as boolean);
275
+ return doc => wikiLinter.codemirror(doc.toString());
276
+ }
277
+ case 'javascript': {
278
+ await loadScript('npm/eslint-linter-browserify', 'eslint');
279
+ /** @see https://npmjs.com/package/@codemirror/lang-javascript */
280
+ const esLinter = new eslint.Linter(),
281
+ conf: Linter.Config = {
282
+ env: {
283
+ browser: true,
284
+ es2018: true,
285
+ },
286
+ parserOptions: {
287
+ ecmaVersion: 9,
288
+ sourceType: 'module',
289
+ },
290
+ rules: {},
291
+ ...opt,
292
+ };
293
+ for (const [name, {meta}] of esLinter.getRules()) {
294
+ if (meta?.docs?.recommended) {
295
+ conf.rules![name] ??= 2;
296
+ }
297
+ }
298
+ return doc => esLinter.verify(doc.toString(), conf)
299
+ .map(({message, severity, line, column, endLine, endColumn}) => {
300
+ const from = pos(doc, line, column);
301
+ return {
302
+ message,
303
+ severity: severity === 1 ? 'warning' : 'error',
304
+ from,
305
+ to: endLine === undefined ? from + 1 : pos(doc, endLine, endColumn!),
306
+ };
307
+ });
308
+ }
309
+ case 'css': {
310
+ await loadScript('gh/openstyles/stylelint-bundle/dist/stylelint-bundle.min.js', 'stylelint');
311
+ /** @see https://npmjs.com/package/stylelint-config-recommended */
312
+ const conf = {
313
+ rules: {
314
+ 'annotation-no-unknown': true,
315
+ 'at-rule-no-unknown': true,
316
+ 'block-no-empty': true,
317
+ 'color-no-invalid-hex': true,
318
+ 'comment-no-empty': true,
319
+ 'custom-property-no-missing-var-function': true,
320
+ 'declaration-block-no-duplicate-custom-properties': true,
321
+ 'declaration-block-no-duplicate-properties': [
322
+ true,
323
+ {
324
+ ignore: ['consecutive-duplicates-with-different-syntaxes'],
325
+ },
326
+ ],
327
+ 'declaration-block-no-shorthand-property-overrides': true,
328
+ 'font-family-no-duplicate-names': true,
329
+ 'font-family-no-missing-generic-family-keyword': true,
330
+ 'function-calc-no-unspaced-operator': true,
331
+ 'function-linear-gradient-no-nonstandard-direction': true,
332
+ 'function-no-unknown': true,
333
+ 'keyframe-block-no-duplicate-selectors': true,
334
+ 'keyframe-declaration-no-important': true,
335
+ 'media-feature-name-no-unknown': true,
336
+ 'media-query-no-invalid': true,
337
+ 'named-grid-areas-no-invalid': true,
338
+ 'no-descending-specificity': true,
339
+ 'no-duplicate-at-import-rules': true,
340
+ 'no-duplicate-selectors': true,
341
+ 'no-empty-source': true,
342
+ 'no-invalid-double-slash-comments': true,
343
+ 'no-invalid-position-at-import-rule': true,
344
+ 'no-irregular-whitespace': true,
345
+ 'property-no-unknown': true,
346
+ 'selector-anb-no-unmatchable': true,
347
+ 'selector-pseudo-class-no-unknown': true,
348
+ 'selector-pseudo-element-no-unknown': true,
349
+ 'selector-type-no-unknown': [
350
+ true,
351
+ {
352
+ ignore: ['custom-elements'],
353
+ },
354
+ ],
355
+ 'string-no-newline': true,
356
+ 'unit-no-unknown': true,
357
+ ...opt?.['rules'] as Record<string, unknown>,
358
+ },
359
+ };
360
+ return async doc => {
361
+ const {results} = await stylelint.lint({code: doc.toString(), config: conf});
362
+ return results.flatMap(({warnings}) => warnings)
363
+ .map(({text, severity, line, column, endLine, endColumn}) => ({
364
+ message: text,
365
+ severity,
366
+ from: pos(doc, line, column),
367
+ to: endLine === undefined ? doc.line(line).to : pos(doc, endLine, endColumn!),
368
+ }));
369
+ };
370
+ }
371
+ case 'lua':
372
+ await loadScript('npm/luaparse', 'luaparse');
373
+ /** @see https://github.com/ajaxorg/ace/blob/master/lib/ace/mode/lua_worker.js */
374
+ return doc => {
375
+ try {
376
+ luaparse.parse(doc.toString());
377
+ } catch (e) {
378
+ if (e instanceof luaparse.SyntaxError) {
379
+ return [
380
+ {
381
+ message: e.message,
382
+ severity: 'error',
383
+ from: e.index,
384
+ to: e.index,
385
+ },
386
+ ];
387
+ }
388
+ }
389
+ return [];
390
+ };
391
+ default:
392
+ return undefined;
393
+ }
394
+ }
395
+
396
+ /**
397
+ * 重设编辑器内容
398
+ * @param content 新内容
399
+ */
400
+ setContent(content: string): void {
401
+ this.#view.dispatch({
402
+ changes: {from: 0, to: this.#view.state.doc.length, insert: content},
403
+ });
404
+ }
405
+
406
+ /**
407
+ * 在编辑器和文本框之间切换
408
+ * @param show 是否显示编辑器
409
+ */
410
+ toggle(show = !this.#visible): void {
411
+ if (show && !this.#visible) {
412
+ this.setContent(this.#textarea.value);
413
+ this.#refresh();
414
+ }
415
+ this.#visible = show;
416
+ this.#view.dom.style.setProperty('display', show ? '' : 'none', 'important');
417
+ this.#textarea.style.display = show ? 'none' : '';
418
+ }
419
+ }