eslint-plugin-markdownlint
Version:
Eslint plugin for markdownlint
169 lines (146 loc) • 5 kB
JavaScript
/**
* @fileOverview Single rule checker.
* @author Paweł BB Drozd
*/
import { lint } from "markdownlint/sync";
import { allRulesDisabled, formatters, loadMarkdownlintConfig } from "./utils/index.js";
/**
* Check the rule.
* @param {string} ruleId - Rule Id
* @param {RuleContext} context
*/
// MD052 and MD053 use "parser: none" but depend on the micromark cache,
// which is only populated when at least one micromark-based rule runs.
// MD042 uses micromark and serves as the required companion.
const MICROMARK_CACHE_RULES = new Set(["md052", "md053"]);
const MICROMARK_COMPANION = "md042";
const check = (ruleId, context, src) => {
const needsCompanion = MICROMARK_CACHE_RULES.has(ruleId);
// Build rule options: config file options < ESLint rule options
const customConfigFile = context.settings?.markdownlint?.configFile;
const fileConfig = loadMarkdownlintConfig(
context.filename ?? context.getFilename?.() ?? "",
customConfigFile
);
const fileRuleOptions = fileConfig?.[ruleId.toUpperCase()] ?? fileConfig?.[ruleId];
const eslintOptions = context.options?.[0];
let ruleOptions;
if (eslintOptions != null) {
// ESLint options take precedence; merge on top of file config if it's an object
ruleOptions =
typeof fileRuleOptions === "object" && fileRuleOptions !== null
? Object.assign({}, fileRuleOptions, eslintOptions)
: Object.assign({}, eslintOptions);
} else if (fileRuleOptions != null) {
// No ESLint options — use config file options
ruleOptions =
typeof fileRuleOptions === "object"
? Object.assign({}, fileRuleOptions)
: fileRuleOptions;
} else {
ruleOptions = true;
}
const config = Object.assign({}, allRulesDisabled, {
[ruleId]: ruleOptions,
...(needsCompanion ? { [MICROMARK_COMPANION]: true } : {}),
});
const { text: allErrors } = lint({
strings: { text: src.getText() },
config,
});
const errors = needsCompanion
? allErrors.filter((e) => e.ruleNames[0].toLowerCase() === ruleId)
: allErrors;
return errors;
};
const getRange = (src, err) => {
const errorLine = (err.fixInfo.lineNumber || err.lineNumber) - 1;
const text = src.getText().split(/\r\n?|\n/g);
const charsBefore = text
.slice(0, errorLine)
.reduce((acc, line) => line.length + 1 + acc, 0);
let start, end;
if (err.fixInfo === null) {
start = charsBefore;
end = start + text[err.lineNumber - 1].length + 1;
} else if (
err.lineNumber === text.length &&
err.fixInfo &&
err.fixInfo.deleteCount < 0
) {
start = src.getIndexFromLoc({
line: err.lineNumber + err.fixInfo.deleteCount,
column: 0,
});
end = src.getIndexFromLoc({
line: err.lineNumber,
column: text[err.lineNumber - 1].length,
});
} else if (err.fixInfo && err.fixInfo.deleteCount < 0) {
start = src.getIndexFromLoc({ line: err.lineNumber, column: 0 });
const lineNumber = Math.min(err.lineNumber - err.fixInfo.deleteCount, text.length);
end = src.getIndexFromLoc({
line: lineNumber,
column: 0,
});
} else {
start = charsBefore + (err.fixInfo.editColumn || 1) - 1;
end = start + (err.fixInfo.deleteCount || 0);
}
return [start, end];
};
const md012Preprocessor = (errors) => {
const deleteLineErrors = errors.filter(
(err) => err.fixInfo && err.fixInfo.deleteCount === -1
);
const others = errors.filter((err) => !err.fixInfo || err.fixInfo.deleteCount !== -1);
const joinedErrors = [];
let line = null;
deleteLineErrors.forEach((dle) => {
if (!line) {
joinedErrors.push(dle);
line = dle.lineNumber;
} else if (dle.lineNumber === line + 1) {
joinedErrors[joinedErrors.length - 1].fixInfo.deleteCount +=
dle.fixInfo.deleteCount;
joinedErrors[joinedErrors.length - 1].errorDetail = dle.errorDetail;
line = dle.lineNumber;
}
});
return others.concat(joinedErrors);
};
const handleErrors = (errors, src) => {
if (errors.length && errors[0].ruleNames[0] === "MD012") {
errors = md012Preprocessor(errors);
}
return errors.map((err) => {
const error = {
loc: formatters.getLocation(err),
message: formatters.formatMessage(err),
};
if (err.fixInfo) {
error.fix = function (fixer) {
const range = getRange(src, err);
if (range[0] === range[1]) {
return fixer.insertTextBeforeRange(range, err.fixInfo.insertText || "");
}
return fixer.replaceTextRange(range, err.fixInfo.insertText || "");
};
}
return error;
});
};
const reportErrors = (context, errors) => {
errors.forEach((err) => context.report(err));
};
export default (ruleId) =>
function (context) {
return {
Program() {
const src = context.sourceCode;
const errors = check(ruleId, context, src);
const handledErrors = handleErrors(errors, src);
reportErrors(context, handledErrors);
},
};
};