@bhsd/codemirror-mediawiki
Version:
Modified CodeMirror mode based on wikimedia/mediawiki-extensions-CodeMirror
189 lines (188 loc) • 7.82 kB
JavaScript
import { ensureSyntaxTree } from '@codemirror/language';
import { cssLanguage } from '@codemirror/lang-css';
import { javascriptLanguage } from '@codemirror/lang-javascript';
import { sanitizeInlineStyle } from '@bhsd/common';
import { getWikiLinter, getJsLinter, getCssLinter, getJsonLinter, getLuaLinter } from './linter';
import { posToIndex } from './hover';
/**
* 获取Linter选项
* @param opt Linter选项
* @param runtime 是否为运行时选项
*/
const getOpt = (opt, runtime) => typeof opt === 'function' ? opt(runtime) : opt;
/**
* 获取指定行列的位置
* @param doc 文档
* @param line 行号
* @param column 列号
* @param from 子语言起始位置
*/
const pos = (doc, line, column, from = 0) => {
if (from === 0) {
return posToIndex(doc, { line: line - 1, character: column - 1 });
}
const lineDesc = doc.lineAt(from);
return posToIndex(doc, {
line: lineDesc.number + line - 2,
character: (line === 1 ? from - lineDesc.from : 0) + column - 1,
});
};
const getRange = (doc, line, column, endLine, endColumn, f = 0, t = Infinity) => {
const start = pos(doc, line, column, f);
return {
from: start,
to: endLine === undefined ? Math.min(t, start + 1) : pos(doc, endLine, endColumn, f),
};
};
const wikiLintSource = async (wikiLint, text, opt, doc, f = 0, t) => (await wikiLint(text, opt))
.map(({ severity, code, message, range: r, from, to, data = [], source }) => ({
source: source,
severity: severity === 2 ? 'warning' : 'error',
message: source === 'Stylelint' ? message : `${message} (${code})`,
actions: data.map(({ title, range, newText }) => ({
name: title,
apply(view) {
view.dispatch({
changes: {
from: posToIndex(doc, range.start),
to: posToIndex(doc, range.end),
insert: newText,
},
});
},
})),
...from === undefined
? getRange(doc, r.start.line + 1, r.start.character + 1, r.end.line + 1, r.end.character + 1, f, t)
: { from: from + f, to: (to ?? from) + f },
}));
export const getWikiLintSource = async (opt, v) => {
const wikiLint = await getWikiLinter(await getOpt(opt), v);
const lintSource = async ({ doc }) => wikiLintSource(wikiLint, doc.toString(), await getOpt(opt, true), doc);
if (wikiLint.fixer) {
lintSource.fixer = (_, rule) => wikiLint.fixer('', rule);
}
return lintSource;
};
const jsLintSource = (esLint, code, opt, doc, f = 0, t) => esLint(code, opt)
.map(({ ruleId, message, severity, line, column, endLine, endColumn, fix, suggestions = [] }) => {
const diagnostic = {
source: 'ESLint',
message: message + (ruleId ? ` (${ruleId})` : ''),
severity: severity === 1 ? 'warning' : 'error',
...getRange(doc, line, column, endLine, endColumn, f, t),
};
if (fix || suggestions.length > 0) {
diagnostic.actions = [
...fix ? [{ name: 'fix', fix }] : [],
...suggestions.map(suggestion => ({ name: 'suggestion', fix: suggestion.fix })),
].map(({ name, fix: { range: [from, to], text } }) => ({
name,
apply(view) {
view.dispatch({ changes: { from: from + f, to: to + f, insert: text } });
},
}));
}
return diagnostic;
});
export const getJsLintSource = async (opt) => {
const esLint = await getJsLinter();
const lintSource = async ({ doc }) => jsLintSource(esLint, doc.toString(), await getOpt(opt), doc);
lintSource.fixer = (doc, rule) => esLint.fixer(doc.toString(), rule);
return lintSource;
};
const cssLintSource = async (styleLint, code, opt, doc, f = 0, t) => {
let option = opt ?? {};
if (!('extends' in option || 'rules' in option)) {
option = { rules: option };
}
return (await styleLint(code, option))
.map(({ text, severity, line, column, endLine, endColumn, fix }) => {
const diagnostic = {
source: 'Stylelint',
message: text,
severity,
...getRange(doc, line, column, endLine, endColumn, f, t),
};
if (fix) {
diagnostic.actions = [
{
name: 'fix',
apply(view) {
view.dispatch({
changes: { from: fix.range[0] + f, to: fix.range[1] + f, insert: fix.text },
});
},
},
];
}
return diagnostic;
});
};
export const getCssLintSource = async (opt) => {
const styleLint = await getCssLinter();
const lintSource = async ({ doc }) => cssLintSource(styleLint, doc.toString(), await getOpt(opt), doc);
lintSource.fixer = async (doc, rule) => styleLint.fixer(doc.toString(), rule);
return lintSource;
};
export const getVueLintSource = async (opt) => {
const styleLint = await getCssLinter(), esLint = await getJsLinter();
return async (state) => {
const { doc } = state, option = await getOpt(opt, true) ?? {}, js = option['js'], css = option['css'];
return [
...(await Promise.all(cssLanguage.findRegions(state).map(async ({ from, to }) => {
const node = ensureSyntaxTree(state, from)?.resolve(from, 1);
if (node?.name === 'AttributeValue') {
return (await cssLintSource(styleLint, `a {${sanitizeInlineStyle(state.sliceDoc(from, to))}}`, css, doc, from - 3, to + 1)).filter(({ from: f, to: t }) => f <= to && t >= from)
.map((diagnostic) => {
diagnostic.from = Math.max(diagnostic.from, from);
diagnostic.to = Math.min(diagnostic.to, to);
return diagnostic;
});
}
return node ? cssLintSource(styleLint, state.sliceDoc(from, to), css, doc, from, to) : [];
}))).flat(),
...javascriptLanguage.findRegions(state)
.flatMap(({ from, to }) => jsLintSource(esLint, state.sliceDoc(from, to), js, doc, from, to)),
];
};
};
export const getHTMLLintSource = async (opt, view, language) => {
const vueLintSource = await getVueLintSource(opt), wikiLint = await getWikiLinter({ include: false, ...await getOpt(opt) }, view);
return async (state) => {
const { doc } = state, option = await getOpt(opt, true) ?? {}, wiki = option['wiki'];
return [
...await vueLintSource(state),
...(await Promise.all(language.findRegions(state)
.map(({ from, to }) => wikiLintSource(wikiLint, state.sliceDoc(from, to), wiki, doc, from, to)))).flat(),
];
};
};
export const getJsonLintSource = () => {
const jsonLint = getJsonLinter();
return ({ doc }) => {
const [e] = jsonLint(doc.toString());
if (e) {
const { message, severity, line, column, position } = e;
let from = 0;
if (position) {
from = Number(position);
}
else if (line && column) {
from = pos(doc, Number(line), Number(column));
}
return [{ message, severity, from, to: from }];
}
return [];
};
};
export const getLuaLintSource = async () => {
const luaLint = await getLuaLinter();
return async ({ doc }) => (await luaLint(doc.toString()))
.map(({ line, column, end_column: endColumn, msg: message, severity }) => ({
source: 'Luacheck',
message,
severity: severity === 1 ? 'warning' : 'error',
from: pos(doc, line, column),
to: pos(doc, line, endColumn + 1),
}));
};