@bhsd/codemirror-mediawiki 2.28.0 → 2.29.0

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/indent.js ADDED
@@ -0,0 +1,50 @@
1
+ export const noDetectionLangs = new Set(['plain', 'mediawiki', 'html']);
2
+ /**
3
+ * 检测文本的缩进方式
4
+ * @param text 文本内容
5
+ * @param defaultIndent 默认缩进方式
6
+ * @param lang 语言
7
+ */
8
+ export const detectIndent = (text, defaultIndent, lang) => {
9
+ if (noDetectionLangs.has(lang)) {
10
+ return defaultIndent;
11
+ }
12
+ const lineSpaces = [];
13
+ let tabLines = 0;
14
+ for (const line of text.split('\n')) {
15
+ if (!line.trim()) {
16
+ continue;
17
+ }
18
+ let tabs = 0, spaces = 0;
19
+ for (const char of line) {
20
+ if (char === '\t') {
21
+ tabs++;
22
+ }
23
+ else if (char === ' ') {
24
+ spaces++;
25
+ }
26
+ else {
27
+ break;
28
+ }
29
+ }
30
+ if (tabs && tabs * 8 >= spaces) {
31
+ tabLines++;
32
+ }
33
+ if (spaces > 1 && spaces >= tabs * 2) {
34
+ lineSpaces.push(spaces);
35
+ }
36
+ }
37
+ const { length } = lineSpaces;
38
+ if (tabLines > length) {
39
+ return '\t';
40
+ }
41
+ else if (tabLines === length) {
42
+ return defaultIndent;
43
+ }
44
+ for (let i = Math.min(...lineSpaces, 8); i > 2; i--) {
45
+ if (lineSpaces.every(s => s % i === 0)) {
46
+ return ' '.repeat(i);
47
+ }
48
+ }
49
+ return ' ';
50
+ };
package/dist/inlay.js ADDED
@@ -0,0 +1,68 @@
1
+ import { StateField, StateEffect } from '@codemirror/state';
2
+ import { Decoration, EditorView, WidgetType, ViewPlugin } from '@codemirror/view';
3
+ import { getLSP } from '@bhsd/common';
4
+ import { posToIndex } from './hover';
5
+ class InlayHintWidget extends WidgetType {
6
+ constructor(label) {
7
+ super();
8
+ this.label = label;
9
+ }
10
+ toDOM() {
11
+ const element = document.createElement('span');
12
+ element.textContent = this.label;
13
+ element.className = 'cm-inlay-hint';
14
+ return element;
15
+ }
16
+ }
17
+ const stateEffect = StateEffect.define(), field = StateField.define({
18
+ create() {
19
+ return Decoration.none;
20
+ },
21
+ update(deco, { state: { doc }, effects }) {
22
+ const str = doc.toString();
23
+ for (const effect of effects) {
24
+ if (effect.is(stateEffect)) {
25
+ const { value: { text, inlayHints } } = effect;
26
+ if (str === text) {
27
+ return inlayHints
28
+ ? Decoration.set(inlayHints.reverse()
29
+ .map(({ position, label }) => [posToIndex(doc, position), label])
30
+ .sort(([a], [b]) => a - b)
31
+ .map(([index, label]) => Decoration.widget({ widget: new InlayHintWidget(label) }).range(index)))
32
+ : Decoration.none;
33
+ }
34
+ }
35
+ }
36
+ return deco;
37
+ },
38
+ provide(f) {
39
+ return EditorView.decorations.from(f);
40
+ },
41
+ });
42
+ const updateField = async ({ view, docChanged }) => {
43
+ if (docChanged) {
44
+ const text = view.state.doc.toString();
45
+ view.dispatch({
46
+ effects: stateEffect.of({
47
+ text,
48
+ inlayHints: await getLSP(view)?.provideInlayHints(text),
49
+ }),
50
+ });
51
+ }
52
+ };
53
+ export default (cm) => [
54
+ field,
55
+ ViewPlugin.fromClass(class {
56
+ constructor(view) {
57
+ const timer = setInterval(() => {
58
+ if (getLSP(view, false, cm.getWikiConfig)) {
59
+ clearInterval(timer);
60
+ void updateField({ view, docChanged: true });
61
+ }
62
+ }, 100);
63
+ }
64
+ update(update) {
65
+ void updateField(update);
66
+ }
67
+ }),
68
+ ];
@@ -0,0 +1,5 @@
1
+ import { javascript as js, javascriptLanguage, scopeCompletionSource } from '@codemirror/lang-javascript';
2
+ export default () => [
3
+ js(),
4
+ javascriptLanguage.data.of({ autocomplete: scopeCompletionSource(globalThis) }),
5
+ ];
@@ -0,0 +1,35 @@
1
+ export const keybindings = [
2
+ { key: 'Ctrl-8', pre: '<blockquote>', post: '</blockquote>', desc: 'blockquote' },
3
+ { key: 'Mod-.', pre: '<sup>', post: '</sup>', desc: 'sup' },
4
+ { key: 'Mod-,', pre: '<sub>', post: '</sub>', desc: 'sub' },
5
+ { key: 'Mod-Shift-6', pre: '<code>', post: '</code>', desc: 'code' },
6
+ { key: 'Ctrl-Shift-5', pre: '<s>', post: '</s>', desc: 's' },
7
+ { key: 'Mod-u', pre: '<u>', post: '</u>', desc: 'u' },
8
+ { key: 'Mod-k', pre: '[[', post: ']]', desc: 'link' },
9
+ { key: 'Mod-i', pre: "''", post: "''", desc: 'italic' },
10
+ { key: 'Mod-b', pre: "'''", post: "'''", desc: 'bold' },
11
+ { key: 'Mod-Shift-k', pre: '<ref>', post: '</ref>', desc: 'ref' },
12
+ { key: 'Mod-/', pre: '<!-- ', post: ' -->', desc: 'comment' },
13
+ { key: 'Ctrl-0', splitlines: true, desc: 'heading 0' },
14
+ ...new Array(6).fill(0).map((_, i) => ({
15
+ key: `Ctrl-${i + 1}`,
16
+ pre: `${'='.repeat(i + 1)} `,
17
+ post: ` ${'='.repeat(i + 1)}`,
18
+ splitlines: true,
19
+ desc: `heading ${i + 1}`,
20
+ })),
21
+ { key: 'Ctrl-7', pre: ' ', splitlines: true, desc: 'pre' },
22
+ ];
23
+ /**
24
+ * 将文本各行包裹在指定的前后缀中
25
+ * @param text 跨行文本
26
+ * @param pre 前缀
27
+ * @param post 后缀
28
+ */
29
+ export const encapsulateLines = (text, pre, post) => {
30
+ const lines = text.split('\n');
31
+ return lines.map(line => {
32
+ const str = (/^(={1,6})(.+)\1\s*$/u.exec(line)?.[2] ?? line).trim();
33
+ return pre === ' ' || lines.length === 1 || line.trim() ? pre + str + post : str;
34
+ }).join('\n');
35
+ };
package/dist/keymap.js ADDED
@@ -0,0 +1,36 @@
1
+ import { keymap } from '@codemirror/view';
2
+ import { EditorSelection } from '@codemirror/state';
3
+ import { keybindings, encapsulateLines } from './keybindings';
4
+ /**
5
+ * 生成keymap
6
+ * @param opt 快捷键设置
7
+ * @param opt.key 键名
8
+ * @param opt.pre 前缀
9
+ * @param opt.post 后缀
10
+ * @param opt.splitlines 是否分行
11
+ */
12
+ const getKeymap = ({ key, pre = '', post = '', splitlines }) => ({
13
+ key,
14
+ run(view) {
15
+ const { state } = view;
16
+ view.dispatch(state.changeByRange(({ from, to }) => {
17
+ if (splitlines) {
18
+ const start = state.doc.lineAt(from).from, end = state.doc.lineAt(to).to, insert = encapsulateLines(state.sliceDoc(start, end), pre, post);
19
+ return {
20
+ range: EditorSelection.range(start, start + insert.length),
21
+ changes: { from: start, to: end, insert },
22
+ };
23
+ }
24
+ const insert = pre + state.sliceDoc(from, to) + post, head = from + insert.length;
25
+ return {
26
+ range: from === to
27
+ ? EditorSelection.range(from + pre.length, head - post.length)
28
+ : EditorSelection.range(head, head),
29
+ changes: { from, to, insert },
30
+ };
31
+ }));
32
+ return true;
33
+ },
34
+ preventDefault: true,
35
+ });
36
+ export default keymap.of(keybindings.map(getKeymap));
package/dist/linter.d.ts CHANGED
@@ -3,7 +3,7 @@ import type { Linter } from 'eslint';
3
3
  import type { Warning } from 'stylelint';
4
4
  import type { Diagnostic } from 'luacheck-browserify';
5
5
  export type Option = Record<string, unknown> | null | undefined;
6
- export type LiveOption = (runtime?: true) => Option;
6
+ export type LiveOption = (runtime?: boolean) => Option;
7
7
  declare type getLinter<T> = () => (text: string) => T;
8
8
  declare type asyncLinter<T, S = Record<string, unknown>> = ((text: string, config?: Option) => T) & {
9
9
  config?: S;
@@ -19,25 +19,26 @@ declare interface MixedDiagnostic extends Omit<DiagnosticBase, 'range'> {
19
19
  from?: number;
20
20
  to?: number;
21
21
  }
22
+ declare interface JsonError {
23
+ message: string;
24
+ severity: 'error';
25
+ line: string | undefined;
26
+ column: string | undefined;
27
+ position: string | undefined;
28
+ }
22
29
  /**
23
30
  * 获取 Wikitext LSP
24
31
  * @param opt 选项
25
32
  * @param obj 对象
26
33
  */
27
34
  export declare const getWikiLinter: getAsyncLinter<Promise<MixedDiagnostic[]>, Option, object>;
35
+ export declare const jsConfig: Linter.Config<Linter.RulesRecord, Linter.RulesRecord>;
28
36
  /** 获取 ESLint */
29
37
  export declare const getJsLinter: getAsyncLinter<Linter.LintMessage[]>;
30
38
  /** 获取 Stylelint */
31
39
  export declare const getCssLinter: getAsyncLinter<Promise<Warning[]>>;
32
40
  /** 获取 Luacheck */
33
41
  export declare const getLuaLinter: getAsyncLinter<Promise<Diagnostic[]>>;
34
- declare interface JsonError {
35
- message: string;
36
- severity: 'error';
37
- line: string | undefined;
38
- column: string | undefined;
39
- position: string | undefined;
40
- }
41
42
  /** JSON.parse */
42
43
  export declare const getJsonLinter: getLinter<JsonError[]>;
43
44
  export {};
package/dist/linter.js ADDED
@@ -0,0 +1,134 @@
1
+ /* eslint-disable unicorn/no-unreadable-iife */
2
+ import { loadScript, getWikiparse, getLSP, sanitizeInlineStyle } from '@bhsd/common';
3
+ import { styleLint } from '@bhsd/common/dist/stylelint';
4
+ /**
5
+ * 计算位置
6
+ * @param range 范围
7
+ * @param line 行号
8
+ * @param column 列号
9
+ */
10
+ const offsetAt = (range, line, column) => {
11
+ if (line === -2) {
12
+ return range[0];
13
+ }
14
+ return line === 0 ? range[1] : range[0] + column;
15
+ };
16
+ /**
17
+ * 获取 Wikitext LSP
18
+ * @param opt 选项
19
+ * @param obj 对象
20
+ */
21
+ export const getWikiLinter = async (opt, obj) => {
22
+ await getWikiparse(opt?.['getConfig'], opt?.['i18n']);
23
+ const lsp = getLSP(obj, opt?.['include']);
24
+ return async (text, config) => {
25
+ const diagnostics = (await lsp.provideDiagnostics(text)).filter(({ code, severity }) => Number(config?.[code] ?? 2) > Number(severity === 2)), tokens = 'findStyleTokens' in lsp && config?.['invalid-css'] !== '0' ? await lsp.findStyleTokens() : [];
26
+ if (tokens.length === 0) {
27
+ return diagnostics;
28
+ }
29
+ const cssLint = await getCssLinter();
30
+ return [
31
+ ...diagnostics,
32
+ ...(await cssLint(tokens.map(({ childNodes, type, tag }, i) => `${type === 'ext-attr' ? 'div' : tag}#${i}{\n${sanitizeInlineStyle(childNodes[1].childNodes[0].data)
33
+ .replace(/\n/gu, ' ')}\n}`).join('\n'))).map(({ line, column, endLine, endColumn, rule, severity, text: message }) => {
34
+ const i = Math.ceil(line / 3), { range } = tokens[i - 1].childNodes[1].childNodes[0], from = offsetAt(range, line - 3 * i, column - 1);
35
+ return {
36
+ from,
37
+ to: endLine === undefined ? from : offsetAt(range, endLine - 3 * i, endColumn - 1),
38
+ severity: severity === 'error' ? 1 : 2,
39
+ source: 'Stylelint',
40
+ code: rule,
41
+ message,
42
+ };
43
+ }),
44
+ ];
45
+ };
46
+ };
47
+ export const jsConfig = /* #__PURE__ */ (() => ({
48
+ env: { browser: true, es2024: true, jquery: true },
49
+ globals: {
50
+ mw: 'readonly',
51
+ mediaWiki: 'readonly',
52
+ OO: 'readonly',
53
+ addOnloadHook: 'readonly',
54
+ importScriptURI: 'readonly',
55
+ importScript: 'readonly',
56
+ importStylesheet: 'readonly',
57
+ importStylesheetURI: 'readonly',
58
+ },
59
+ }))();
60
+ /** 获取 ESLint */
61
+ export const getJsLinter = async () => {
62
+ await loadScript('npm/@bhsd/eslint-browserify', 'eslint');
63
+ /** @see https://www.npmjs.com/package/@codemirror/lang-javascript */
64
+ const esLinter = new eslint.Linter(), conf = {
65
+ env: { browser: true, es2024: true },
66
+ parserOptions: { ecmaVersion: 15, sourceType: 'module' },
67
+ }, recommended = {};
68
+ for (const [name, { meta }] of esLinter.getRules()) {
69
+ if (meta?.docs?.recommended) {
70
+ recommended[name] = 2;
71
+ }
72
+ }
73
+ const linter = (text, opt) => {
74
+ const config = { ...conf, ...opt };
75
+ if (!('rules' in config)
76
+ || config.extends === 'eslint:recommended'
77
+ || Array.isArray(config.extends) && config.extends.includes('eslint:recommended')) {
78
+ config.rules = { ...recommended, ...config.rules };
79
+ }
80
+ delete config.extends;
81
+ linter.config = config;
82
+ return esLinter.verify(text, config);
83
+ };
84
+ linter.fixer = (code, rule) => esLinter.verifyAndFix(code, rule ? { ...linter.config, rules: { [rule]: linter.config.rules?.[rule] ?? 2 } } : linter.config).output;
85
+ return linter;
86
+ };
87
+ /** 获取 Stylelint */
88
+ export const getCssLinter = async () => {
89
+ await loadScript('npm/@bhsd/stylelint-browserify', 'stylelint');
90
+ const linter = async (code, opt) => {
91
+ const warnings = await styleLint(stylelint, code, opt);
92
+ if (opt && 'rules' in opt) {
93
+ linter.config = opt;
94
+ }
95
+ return warnings;
96
+ };
97
+ linter.fixer = (code, rule) => {
98
+ if (!linter.config) {
99
+ throw new Error('Fixer unavailable!');
100
+ }
101
+ return styleLint(stylelint, code, rule ? { extends: [], rules: { [rule]: linter.config.rules?.[rule] ?? true } } : linter.config, true);
102
+ };
103
+ return linter;
104
+ };
105
+ /** 获取 Luacheck */
106
+ export const getLuaLinter = async () => {
107
+ await loadScript('npm/luacheck-browserify', 'luacheck');
108
+ // eslint-disable-next-line @typescript-eslint/await-thenable
109
+ const luachecker = await luacheck(undefined);
110
+ return async (text) => (await luachecker.queue(text)).filter(({ severity }) => severity);
111
+ };
112
+ /** JSON.parse */
113
+ export const getJsonLinter = () => str => {
114
+ try {
115
+ if (str.trim()) {
116
+ JSON.parse(str);
117
+ }
118
+ }
119
+ catch (e) {
120
+ if (e instanceof SyntaxError) {
121
+ const { message } = e, line = /\bline (\d+)/u.exec(message)?.[1], column = /\bcolumn (\d+)/u.exec(message)?.[1], position = /\bposition (\d+)/u.exec(message)?.[1];
122
+ return [
123
+ {
124
+ message,
125
+ severity: 'error',
126
+ line,
127
+ column,
128
+ position,
129
+ },
130
+ ];
131
+ }
132
+ }
133
+ return [];
134
+ };