@bhsd/codemirror-mediawiki 2.3.1 → 2.3.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/plugins.d.ts DELETED
@@ -1,3 +0,0 @@
1
- export { css } from '@codemirror/legacy-modes/mode/css';
2
- export { javascript, json } from '@codemirror/legacy-modes/mode/javascript';
3
- export { lua } from '@codemirror/legacy-modes/mode/lua';
package/src/codemirror.ts DELETED
@@ -1,521 +0,0 @@
1
- import {
2
- EditorView,
3
- lineNumbers,
4
- keymap,
5
- highlightSpecialChars,
6
- highlightActiveLine,
7
- highlightWhitespace,
8
- highlightTrailingWhitespace,
9
- drawSelection,
10
- } from '@codemirror/view';
11
- import {Compartment, EditorState, EditorSelection} from '@codemirror/state';
12
- import {
13
- syntaxHighlighting,
14
- defaultHighlightStyle,
15
- indentOnInput,
16
- StreamLanguage,
17
- LanguageSupport,
18
- bracketMatching,
19
- indentUnit,
20
- codeFolding,
21
- } from '@codemirror/language';
22
- import {defaultKeymap, historyKeymap, history} from '@codemirror/commands';
23
- import {searchKeymap} from '@codemirror/search';
24
- import {linter, lintGutter, openLintPanel, closeLintPanel, lintKeymap} from '@codemirror/lint';
25
- import {closeBrackets} from '@codemirror/autocomplete';
26
- import {mediawiki, html} from './mediawiki';
27
- import {keyMap} from './escape';
28
- import {fold, cursorTooltipField, handler, cursorTooltipTheme} from './fold';
29
- import * as plugins from './plugins';
30
- import type {ViewPlugin, KeyBinding} from '@codemirror/view';
31
- import type {Extension, Text, StateEffect} from '@codemirror/state';
32
- import type {Diagnostic} from '@codemirror/lint';
33
- import type {Highlighter} from '@lezer/highlight';
34
- import type {Linter} from 'eslint';
35
-
36
- export type {MwConfig} from './mediawiki';
37
- export type LintSource = (doc: Text) => Diagnostic[] | Promise<Diagnostic[]>;
38
-
39
- declare type LintExtension = [unknown, ViewPlugin<{set: boolean, force(): void}>];
40
-
41
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
- const languages: Record<string, (config?: any) => LanguageSupport | []> = {
43
- plain: () => [],
44
- mediawiki,
45
- html,
46
- };
47
- for (const [language, parser] of Object.entries(plugins)) {
48
- languages[language] = (): LanguageSupport => new LanguageSupport(StreamLanguage.define(parser));
49
- }
50
- const linters: Record<string, Extension> = {};
51
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
- const avail: Record<string, [(config?: any) => Extension, Record<string, unknown>]> = {
53
- highlightSpecialChars: [highlightSpecialChars, {}],
54
- highlightActiveLine: [highlightActiveLine, {}],
55
- highlightWhitespace: [highlightWhitespace, {}],
56
- highlightTrailingWhitespace: [highlightTrailingWhitespace, {}],
57
- bracketMatching: [bracketMatching, {mediawiki: {brackets: '[]{}'}}],
58
- closeBrackets: [closeBrackets, {}],
59
- codeFolding: [
60
- (flag: boolean): Extension => flag
61
- ? [
62
- codeFolding(),
63
- cursorTooltipField,
64
- cursorTooltipTheme,
65
- keymap.of([{key: 'Ctrl-Shift-[', mac: 'Cmd-Alt-[', run: fold}]),
66
- ]
67
- : [],
68
- {mediawiki: true},
69
- ],
70
- allowMultipleSelections: [
71
- (): Extension => [
72
- EditorState.allowMultipleSelections.of(true),
73
- drawSelection(),
74
- ],
75
- {},
76
- ],
77
- escape: [
78
- (keys: KeyBinding[] = []): Extension => keymap.of(keys),
79
- {mediawiki: keyMap},
80
- ],
81
- };
82
-
83
- export const CDN = 'https://testingcf.jsdelivr.net';
84
-
85
- /**
86
- * 使用传统方法加载脚本
87
- * @param src 脚本地址
88
- * @param globalConst 脚本全局变量名
89
- */
90
- const loadScript = (src: string, globalConst: string): Promise<void> => new Promise(resolve => {
91
- if (globalConst in window) {
92
- resolve();
93
- return;
94
- }
95
- const script = document.createElement('script');
96
- script.src = `${CDN}/${src}`;
97
- script.onload = (): void => {
98
- resolve();
99
- };
100
- document.head.append(script);
101
- });
102
-
103
- /**
104
- * 获取指定行列的位置
105
- * @param doc 文档
106
- * @param line 行号
107
- * @param column 列号
108
- */
109
- const pos = (doc: Text, line: number, column: number): number => doc.line(line).from + column - 1;
110
-
111
- export class CodeMirror6 {
112
- readonly #textarea;
113
- readonly #view;
114
- readonly #language = new Compartment();
115
- readonly #linter = new Compartment();
116
- readonly #extensions = new Compartment();
117
- readonly #indent = new Compartment();
118
- readonly #extraKeys = new Compartment();
119
- #lang;
120
- #visible = false;
121
- #preferred = new Set<string>();
122
-
123
- get textarea(): HTMLTextAreaElement {
124
- return this.#textarea;
125
- }
126
-
127
- get view(): EditorView {
128
- return this.#view;
129
- }
130
-
131
- get lang(): string {
132
- return this.#lang;
133
- }
134
-
135
- get visible(): boolean {
136
- return this.#visible;
137
- }
138
-
139
- /**
140
- * @param textarea 文本框
141
- * @param lang 语言
142
- * @param config 语言设置
143
- */
144
- constructor(textarea: HTMLTextAreaElement, lang = 'plain', config?: unknown) {
145
- this.#textarea = textarea;
146
- this.#lang = lang;
147
- let timer: number | undefined;
148
- const extensions = [
149
- this.#language.of(languages[lang]!(config)),
150
- this.#linter.of([]),
151
- this.#extensions.of([]),
152
- this.#indent.of(indentUnit.of('\t')),
153
- this.#extraKeys.of([]),
154
- syntaxHighlighting(defaultHighlightStyle as Highlighter),
155
- EditorView.contentAttributes.of({
156
- accesskey: textarea.accessKey,
157
- dir: textarea.dir,
158
- lang: textarea.lang,
159
- }),
160
- EditorState.readOnly.of(textarea.readOnly),
161
- lineNumbers(),
162
- EditorView.lineWrapping,
163
- history(),
164
- indentOnInput(),
165
- keymap.of([
166
- ...defaultKeymap,
167
- ...historyKeymap,
168
- ...searchKeymap,
169
- ...lintKeymap,
170
- ]),
171
- EditorView.updateListener.of(({state: {doc}, docChanged}) => {
172
- if (docChanged) {
173
- clearTimeout(timer);
174
- timer = window.setTimeout(() => {
175
- textarea.value = doc.toString();
176
- }, 400);
177
- }
178
- }),
179
- ];
180
- this.#view = new EditorView({
181
- extensions,
182
- doc: textarea.value,
183
- });
184
- const {fontSize, lineHeight} = getComputedStyle(textarea);
185
- textarea.parentNode!.insertBefore(this.#view.dom, textarea);
186
- this.#minHeight();
187
- this.#view.scrollDOM.style.fontSize = fontSize;
188
- this.#view.scrollDOM.style.lineHeight = lineHeight;
189
- this.toggle(true);
190
- this.#view.dom.addEventListener('click', handler(this.#view));
191
- }
192
-
193
- /**
194
- * 修改扩展
195
- * @param effects 扩展变动
196
- */
197
- #effects(effects: StateEffect<unknown> | StateEffect<unknown>[]): void {
198
- this.#view.dispatch({effects});
199
- }
200
-
201
- /** 刷新编辑器高度 */
202
- #refresh(): void {
203
- const {offsetHeight} = this.#textarea;
204
- this.#view.dom.style.height = offsetHeight ? `${offsetHeight}px` : this.#textarea.style.height;
205
- }
206
-
207
- /**
208
- * 设置编辑器最小高度
209
- * @param linting 是否启用语法检查
210
- */
211
- #minHeight(linting?: boolean): void {
212
- this.#view.dom.style.minHeight = linting ? 'calc(100px + 2em)' : '2em';
213
- }
214
-
215
- /**
216
- * 开关语法检查面板
217
- * @param show 是否显示
218
- */
219
- #toggleLintPanel(show: boolean): void {
220
- (show ? openLintPanel : closeLintPanel)(this.#view);
221
- document.querySelector<HTMLUListElement>('.cm-panel-lint ul')?.blur();
222
- this.#minHeight(show);
223
- }
224
-
225
- /** 获取语法检查扩展 */
226
- #getLintExtension(): LintExtension | undefined {
227
- return (this.#linter.get(this.#view.state) as LintExtension[])[0];
228
- }
229
-
230
- /**
231
- * 设置语言
232
- * @param lang 语言
233
- * @param config 语言设置
234
- */
235
- setLanguage(lang = 'plain', config?: unknown): void {
236
- this.#effects([
237
- this.#language.reconfigure(languages[lang]!(config)),
238
- this.#linter.reconfigure(linters[lang] || []),
239
- ]);
240
- this.#lang = lang;
241
- this.#toggleLintPanel(Boolean(linters[lang]));
242
- this.prefer({});
243
- }
244
-
245
- /**
246
- * 开始语法检查
247
- * @param lintSource 语法检查函数
248
- */
249
- lint(lintSource?: LintSource): void {
250
- const linterExtension = lintSource
251
- ? [
252
- linter(view => lintSource(view.state.doc)),
253
- lintGutter(),
254
- ]
255
- : [];
256
- if (lintSource) {
257
- linters[this.#lang] = linterExtension;
258
- } else {
259
- delete linters[this.#lang];
260
- }
261
- this.#effects(this.#linter.reconfigure(linterExtension));
262
- this.#toggleLintPanel(Boolean(lintSource));
263
- }
264
-
265
- /** 立即更新语法检查 */
266
- update(): void {
267
- const extension = this.#getLintExtension();
268
- if (extension) {
269
- const plugin = this.#view.plugin(extension[1])!;
270
- plugin.set = true;
271
- plugin.force();
272
- }
273
- }
274
-
275
- /**
276
- * 添加扩展
277
- * @param names 扩展名
278
- */
279
- prefer(names: string[] | Record<string, boolean>): void {
280
- if (Array.isArray(names)) {
281
- this.#preferred = new Set(names.filter(name => avail[name]));
282
- } else {
283
- for (const [name, enable] of Object.entries(names)) {
284
- if (enable && avail[name]) {
285
- this.#preferred.add(name);
286
- } else {
287
- this.#preferred.delete(name);
288
- }
289
- }
290
- }
291
- this.#effects(
292
- this.#extensions.reconfigure([...this.#preferred].map(name => {
293
- const [extension, configs] = avail[name]!;
294
- return extension(configs[this.#lang]);
295
- })),
296
- );
297
- }
298
-
299
- /**
300
- * 设置缩进
301
- * @param indent 缩进字符串
302
- */
303
- setIndent(indent: string): void {
304
- this.#effects(this.#indent.reconfigure(indentUnit.of(indent)));
305
- }
306
-
307
- /** 获取默认linter */
308
- async getLinter(opt?: Record<string, unknown>): Promise<LintSource | undefined> {
309
- switch (this.#lang) {
310
- case 'mediawiki': {
311
- const REPO = 'npm/wikiparser-node@1.4.3-b',
312
- DIR = `${REPO}/extensions/dist`,
313
- src = `combine/${DIR}/base.min.js,${DIR}/lint.min.js`,
314
- lang = opt?.['i18n'];
315
- await loadScript(src, 'wikiparse');
316
- if (typeof lang === 'string') {
317
- try {
318
- const i18n: Record<string, string>
319
- = await (await fetch(`${CDN}/${REPO}/i18n/${lang.toLowerCase()}.json`)).json();
320
- wikiparse.setI18N(i18n);
321
- } catch {}
322
- }
323
- const wikiLinter = new wikiparse.Linter(opt?.['include'] as boolean | undefined);
324
- return async doc =>
325
- (await wikiLinter.codemirror(doc.toString())).filter(({severity}) => severity === 'error');
326
- }
327
- case 'javascript': {
328
- await loadScript('npm/eslint-linter-browserify', 'eslint');
329
- /** @see https://npmjs.com/package/@codemirror/lang-javascript */
330
- const esLinter = new eslint.Linter(),
331
- conf: Linter.Config = {
332
- env: {
333
- browser: true,
334
- es2024: true,
335
- },
336
- parserOptions: {
337
- ecmaVersion: 15,
338
- sourceType: 'module',
339
- },
340
- rules: {},
341
- ...opt,
342
- };
343
- for (const [name, {meta}] of esLinter.getRules()) {
344
- if (meta?.docs?.recommended) {
345
- conf.rules![name] ??= 2;
346
- }
347
- }
348
- return doc => esLinter.verify(doc.toString(), conf)
349
- .map(({message, severity, line, column, endLine, endColumn}) => {
350
- const from = pos(doc, line, column);
351
- return {
352
- message,
353
- severity: severity === 1 ? 'warning' : 'error',
354
- from,
355
- to: endLine === undefined ? from + 1 : pos(doc, endLine, endColumn!),
356
- };
357
- });
358
- }
359
- case 'css': {
360
- await loadScript('gh/openstyles/stylelint-bundle/dist/stylelint-bundle.min.js', 'stylelint');
361
- /** @see https://npmjs.com/package/stylelint-config-recommended */
362
- const conf = {
363
- rules: {
364
- 'annotation-no-unknown': true,
365
- 'at-rule-no-unknown': true,
366
- 'block-no-empty': true,
367
- 'color-no-invalid-hex': true,
368
- 'comment-no-empty': true,
369
- 'custom-property-no-missing-var-function': true,
370
- 'declaration-block-no-duplicate-custom-properties': true,
371
- 'declaration-block-no-duplicate-properties': [
372
- true,
373
- {
374
- ignore: ['consecutive-duplicates-with-different-syntaxes'],
375
- },
376
- ],
377
- 'declaration-block-no-shorthand-property-overrides': true,
378
- 'font-family-no-duplicate-names': true,
379
- 'font-family-no-missing-generic-family-keyword': true,
380
- 'function-calc-no-unspaced-operator': true,
381
- 'function-linear-gradient-no-nonstandard-direction': true,
382
- 'function-no-unknown': true,
383
- 'keyframe-block-no-duplicate-selectors': true,
384
- 'keyframe-declaration-no-important': true,
385
- 'media-feature-name-no-unknown': true,
386
- 'media-query-no-invalid': true,
387
- 'named-grid-areas-no-invalid': true,
388
- 'no-descending-specificity': true,
389
- 'no-duplicate-at-import-rules': true,
390
- 'no-duplicate-selectors': true,
391
- 'no-empty-source': true,
392
- 'no-invalid-double-slash-comments': true,
393
- 'no-invalid-position-at-import-rule': true,
394
- 'no-irregular-whitespace': true,
395
- 'property-no-unknown': true,
396
- 'selector-anb-no-unmatchable': true,
397
- 'selector-pseudo-class-no-unknown': true,
398
- 'selector-pseudo-element-no-unknown': true,
399
- 'selector-type-no-unknown': [
400
- true,
401
- {
402
- ignore: ['custom-elements'],
403
- },
404
- ],
405
- 'string-no-newline': true,
406
- 'unit-no-unknown': true,
407
- ...opt?.['rules'] as Record<string, unknown>,
408
- },
409
- };
410
- return async doc => {
411
- const {results} = await stylelint.lint({code: doc.toString(), config: conf});
412
- return results.flatMap(({warnings}) => warnings)
413
- .map(({text, severity, line, column, endLine, endColumn}) => ({
414
- message: text,
415
- severity,
416
- from: pos(doc, line, column),
417
- to: endLine === undefined ? doc.line(line).to : pos(doc, endLine, endColumn!),
418
- }));
419
- };
420
- }
421
- case 'lua':
422
- await loadScript('npm/luaparse', 'luaparse');
423
- /** @see https://github.com/ajaxorg/ace/blob/master/lib/ace/mode/lua_worker.js */
424
- return doc => {
425
- try {
426
- luaparse.parse(doc.toString());
427
- } catch (e) {
428
- if (e instanceof luaparse.SyntaxError) {
429
- return [
430
- {
431
- message: e.message,
432
- severity: 'error',
433
- from: e.index,
434
- to: e.index,
435
- },
436
- ];
437
- }
438
- }
439
- return [];
440
- };
441
- default:
442
- return undefined;
443
- }
444
- }
445
-
446
- /**
447
- * 重设编辑器内容
448
- * @param content 新内容
449
- */
450
- setContent(content: string): void {
451
- this.#view.dispatch({
452
- changes: {from: 0, to: this.#view.state.doc.length, insert: content},
453
- });
454
- }
455
-
456
- /**
457
- * 在编辑器和文本框之间切换
458
- * @param show 是否显示编辑器
459
- */
460
- toggle(show = !this.#visible): void {
461
- if (show && !this.#visible) {
462
- const {value, selectionStart, selectionEnd, scrollTop} = this.#textarea,
463
- hasFocus = document.activeElement === this.#textarea;
464
- this.setContent(value);
465
- this.#refresh();
466
- this.#view.dom.style.setProperty('display', '');
467
- this.#textarea.style.display = 'none';
468
- this.#view.requestMeasure();
469
- this.#view.dispatch({
470
- selection: {anchor: selectionStart, head: selectionEnd},
471
- });
472
- if (hasFocus) {
473
- this.#view.focus();
474
- }
475
- requestAnimationFrame(() => {
476
- this.#view.scrollDOM.scrollTop = scrollTop;
477
- });
478
- } else if (!show && this.#visible) {
479
- const {state: {selection: {main: {from, to}}}, hasFocus} = this.#view,
480
- {scrollDOM: {scrollTop}} = this.#view;
481
- this.#view.dom.style.setProperty('display', 'none', 'important');
482
- this.#textarea.style.display = '';
483
- this.#textarea.setSelectionRange(
484
- Math.min(from, to),
485
- Math.max(from, to),
486
- from > to ? 'backward' : 'forward',
487
- );
488
- if (hasFocus) {
489
- this.#textarea.focus();
490
- }
491
- requestAnimationFrame(() => {
492
- this.#textarea.scrollTop = scrollTop;
493
- });
494
- }
495
- this.#visible = show;
496
- }
497
-
498
- /**
499
- * 添加额外快捷键
500
- * @param keys 快捷键
501
- */
502
- extraKeys(keys: KeyBinding[]): void {
503
- this.#effects(this.#extraKeys.reconfigure(keymap.of(keys)));
504
- }
505
-
506
- /**
507
- * 替换选中内容
508
- * @param view EditorView
509
- * @param func 替换函数
510
- */
511
- static replaceSelections(view: EditorView, func: (str: string) => string): void {
512
- const {state} = view;
513
- view.dispatch(state.changeByRange(({from, to}) => {
514
- const insert = func(state.sliceDoc(from, to));
515
- return {
516
- range: EditorSelection.range(from, from + insert.length),
517
- changes: {from, to, insert},
518
- };
519
- }));
520
- }
521
- }