mirror of
https://github.com/DavidAnson/markdownlint.git
synced 2025-09-22 05:40:48 +02:00
941 lines
28 KiB
JavaScript
941 lines
28 KiB
JavaScript
// @ts-check
|
|
|
|
"use strict";
|
|
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const { URL } = require("url");
|
|
const markdownIt = require("markdown-it");
|
|
const rules = require("./rules");
|
|
const helpers = require("../helpers");
|
|
const cache = require("./cache");
|
|
|
|
const deprecatedRuleNames = [ "MD002" ];
|
|
|
|
// Validates the list of rules for structure and reuse
|
|
function validateRuleList(ruleList) {
|
|
let result = null;
|
|
if (ruleList.length === rules.length) {
|
|
// No need to validate if only using built-in rules
|
|
return result;
|
|
}
|
|
const allIds = {};
|
|
ruleList.forEach(function forRule(rule, index) {
|
|
const customIndex = index - rules.length;
|
|
function newError(property) {
|
|
return new Error(
|
|
"Property '" + property + "' of custom rule at index " +
|
|
customIndex + " is incorrect.");
|
|
}
|
|
[ "names", "tags" ].forEach(function forProperty(property) {
|
|
const value = rule[property];
|
|
if (!result &&
|
|
(!value || !Array.isArray(value) || (value.length === 0) ||
|
|
!value.every(helpers.isString) || value.some(helpers.isEmptyString))) {
|
|
result = newError(property);
|
|
}
|
|
});
|
|
[
|
|
[ "description", "string" ],
|
|
[ "function", "function" ]
|
|
].forEach(function forProperty(propertyInfo) {
|
|
const property = propertyInfo[0];
|
|
const value = rule[property];
|
|
if (!result && (!value || (typeof value !== propertyInfo[1]))) {
|
|
result = newError(property);
|
|
}
|
|
});
|
|
if (!result && rule.information) {
|
|
if (Object.getPrototypeOf(rule.information) !== URL.prototype) {
|
|
result = newError("information");
|
|
}
|
|
}
|
|
if (!result) {
|
|
rule.names.forEach(function forName(name) {
|
|
const nameUpper = name.toUpperCase();
|
|
if (!result && (allIds[nameUpper] !== undefined)) {
|
|
result = new Error("Name '" + name + "' of custom rule at index " +
|
|
customIndex + " is already used as a name or tag.");
|
|
}
|
|
allIds[nameUpper] = true;
|
|
});
|
|
rule.tags.forEach(function forTag(tag) {
|
|
const tagUpper = tag.toUpperCase();
|
|
if (!result && allIds[tagUpper]) {
|
|
result = new Error("Tag '" + tag + "' of custom rule at index " +
|
|
customIndex + " is already used as a name.");
|
|
}
|
|
allIds[tagUpper] = false;
|
|
});
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
|
|
// Class for results with toString for pretty display
|
|
function newResults(ruleList) {
|
|
function Results() {}
|
|
Results.prototype.toString = function toString(useAlias) {
|
|
const that = this;
|
|
let ruleNameToRule = null;
|
|
const results = [];
|
|
Object.keys(that).forEach(function forFile(file) {
|
|
const fileResults = that[file];
|
|
if (Array.isArray(fileResults)) {
|
|
fileResults.forEach(function forResult(result) {
|
|
const ruleMoniker = result.ruleNames ?
|
|
result.ruleNames.join("/") :
|
|
(result.ruleName + "/" + result.ruleAlias);
|
|
results.push(
|
|
file + ": " +
|
|
result.lineNumber + ": " +
|
|
ruleMoniker + " " +
|
|
result.ruleDescription +
|
|
(result.errorDetail ?
|
|
" [" + result.errorDetail + "]" :
|
|
"") +
|
|
(result.errorContext ?
|
|
" [Context: \"" + result.errorContext + "\"]" :
|
|
""));
|
|
});
|
|
} else {
|
|
if (!ruleNameToRule) {
|
|
ruleNameToRule = {};
|
|
ruleList.forEach(function forRule(rule) {
|
|
const ruleName = rule.names[0].toUpperCase();
|
|
ruleNameToRule[ruleName] = rule;
|
|
});
|
|
}
|
|
Object.keys(fileResults).forEach(function forRule(ruleName) {
|
|
const rule = ruleNameToRule[ruleName.toUpperCase()];
|
|
const ruleResults = fileResults[ruleName];
|
|
ruleResults.forEach(function forLine(lineNumber) {
|
|
const nameIndex = Math.min(useAlias ? 1 : 0, rule.names.length - 1);
|
|
const result =
|
|
file + ": " +
|
|
lineNumber + ": " +
|
|
rule.names[nameIndex] + " " +
|
|
rule.description;
|
|
results.push(result);
|
|
});
|
|
});
|
|
}
|
|
});
|
|
return results.join("\n");
|
|
};
|
|
return new Results();
|
|
}
|
|
|
|
// Remove front matter (if present at beginning of content)
|
|
function removeFrontMatter(content, frontMatter) {
|
|
let frontMatterLines = [];
|
|
if (frontMatter) {
|
|
const frontMatterMatch = content.match(frontMatter);
|
|
if (frontMatterMatch && !frontMatterMatch.index) {
|
|
const contentMatched = frontMatterMatch[0];
|
|
content = content.slice(contentMatched.length);
|
|
frontMatterLines = contentMatched.split(helpers.newLineRe);
|
|
if (frontMatterLines.length &&
|
|
(frontMatterLines[frontMatterLines.length - 1] === "")) {
|
|
frontMatterLines.length--;
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
"content": content,
|
|
"frontMatterLines": frontMatterLines
|
|
};
|
|
}
|
|
|
|
// Annotate tokens with line/lineNumber
|
|
function annotateTokens(tokens, lines) {
|
|
let tbodyMap = null;
|
|
tokens.forEach(function forToken(token) {
|
|
// Handle missing maps for table body
|
|
if (token.type === "tbody_open") {
|
|
tbodyMap = token.map.slice();
|
|
} else if ((token.type === "tr_close") && tbodyMap) {
|
|
tbodyMap[0]++;
|
|
} else if (token.type === "tbody_close") {
|
|
tbodyMap = null;
|
|
}
|
|
if (tbodyMap && !token.map) {
|
|
token.map = tbodyMap.slice();
|
|
}
|
|
// Update token metadata
|
|
if (token.map) {
|
|
token.line = lines[token.map[0]];
|
|
token.lineNumber = token.map[0] + 1;
|
|
// Trim bottom of token to exclude whitespace lines
|
|
while (token.map[1] && !((lines[token.map[1] - 1] || "").trim())) {
|
|
token.map[1]--;
|
|
}
|
|
// Annotate children with lineNumber
|
|
let lineNumber = token.lineNumber;
|
|
const codeSpanExtraLines = [];
|
|
helpers.forEachInlineCodeSpan(
|
|
token.content,
|
|
function handleInlineCodeSpan(code) {
|
|
codeSpanExtraLines.push(code.split(helpers.newLineRe).length - 1);
|
|
}
|
|
);
|
|
(token.children || []).forEach(function forChild(child) {
|
|
child.lineNumber = lineNumber;
|
|
child.line = lines[lineNumber - 1];
|
|
if ((child.type === "softbreak") || (child.type === "hardbreak")) {
|
|
lineNumber++;
|
|
} else if (child.type === "code_inline") {
|
|
lineNumber += codeSpanExtraLines.shift();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Map rule names/tags to canonical rule name
|
|
function mapAliasToRuleNames(ruleList) {
|
|
const aliasToRuleNames = {};
|
|
// const tagToRuleNames = {};
|
|
ruleList.forEach(function forRule(rule) {
|
|
const ruleName = rule.names[0].toUpperCase();
|
|
// The following is useful for updating README.md:
|
|
// console.log(
|
|
// "* **[" + ruleName + "](doc/Rules.md#" + ruleName.toLowerCase() +
|
|
// ")** *" + rule.names.slice(1).join("/") + "* - " + rule.description);
|
|
rule.names.forEach(function forName(name) {
|
|
const nameUpper = name.toUpperCase();
|
|
aliasToRuleNames[nameUpper] = [ ruleName ];
|
|
});
|
|
rule.tags.forEach(function forTag(tag) {
|
|
const tagUpper = tag.toUpperCase();
|
|
const ruleNames = aliasToRuleNames[tagUpper] || [];
|
|
ruleNames.push(ruleName);
|
|
aliasToRuleNames[tagUpper] = ruleNames;
|
|
// tagToRuleNames[tag] = ruleName;
|
|
});
|
|
});
|
|
// The following is useful for updating README.md:
|
|
// Object.keys(tagToRuleNames).sort().forEach(function forTag(tag) {
|
|
// console.log("* **" + tag + "** - " +
|
|
// aliasToRuleNames[tag.toUpperCase()].join(", "));
|
|
// });
|
|
return aliasToRuleNames;
|
|
}
|
|
|
|
// Apply (and normalize) config
|
|
function getEffectiveConfig(ruleList, config, aliasToRuleNames) {
|
|
const defaultKey = Object.keys(config).filter(
|
|
(key) => key.toUpperCase() === "DEFAULT"
|
|
);
|
|
const ruleDefault = (defaultKey.length === 0) || !!config[defaultKey[0]];
|
|
const effectiveConfig = {};
|
|
ruleList.forEach((rule) => {
|
|
const ruleName = rule.names[0].toUpperCase();
|
|
effectiveConfig[ruleName] = ruleDefault;
|
|
});
|
|
deprecatedRuleNames.forEach((ruleName) => {
|
|
effectiveConfig[ruleName] = false;
|
|
});
|
|
Object.keys(config).forEach((key) => {
|
|
let value = config[key];
|
|
if (value) {
|
|
if (!(value instanceof Object)) {
|
|
value = {};
|
|
}
|
|
} else {
|
|
value = false;
|
|
}
|
|
const keyUpper = key.toUpperCase();
|
|
(aliasToRuleNames[keyUpper] || []).forEach((ruleName) => {
|
|
effectiveConfig[ruleName] = value;
|
|
});
|
|
});
|
|
return effectiveConfig;
|
|
}
|
|
|
|
// Create mapping of enabled rules per line
|
|
function getEnabledRulesPerLineNumber(
|
|
ruleList, lines, frontMatterLines, noInlineConfig,
|
|
effectiveConfig, aliasToRuleNames) {
|
|
let enabledRules = {};
|
|
const allRuleNames = [];
|
|
ruleList.forEach((rule) => {
|
|
const ruleName = rule.names[0].toUpperCase();
|
|
allRuleNames.push(ruleName);
|
|
enabledRules[ruleName] = !!effectiveConfig[ruleName];
|
|
});
|
|
let capturedRules = enabledRules;
|
|
function forMatch(match, byLine) {
|
|
const action = match[1].toUpperCase();
|
|
if (action === "CAPTURE") {
|
|
if (byLine) {
|
|
capturedRules = { ...enabledRules };
|
|
}
|
|
} else if (action === "RESTORE") {
|
|
if (byLine) {
|
|
enabledRules = { ...capturedRules };
|
|
}
|
|
} else {
|
|
// action in [ENABLE, DISABLE, ENABLE-FILE, DISABLE-FILE]
|
|
const isfile = action.endsWith("-FILE");
|
|
if ((byLine && !isfile) || (!byLine && isfile)) {
|
|
const enabled = (action.startsWith("ENABLE"));
|
|
const items = match[2] ?
|
|
match[2].trim().toUpperCase().split(/\s+/) :
|
|
allRuleNames;
|
|
items.forEach((nameUpper) => {
|
|
(aliasToRuleNames[nameUpper] || []).forEach((ruleName) => {
|
|
enabledRules[ruleName] = enabled;
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
const enabledRulesPerLineNumber = new Array(1 + frontMatterLines.length);
|
|
[ false, true ].forEach((byLine) => {
|
|
lines.forEach((line) => {
|
|
if (!noInlineConfig) {
|
|
let match = helpers.inlineCommentRe.exec(line);
|
|
if (match) {
|
|
enabledRules = { ...enabledRules };
|
|
while (match) {
|
|
forMatch(match, byLine);
|
|
match = helpers.inlineCommentRe.exec(line);
|
|
}
|
|
}
|
|
}
|
|
if (byLine) {
|
|
enabledRulesPerLineNumber.push(enabledRules);
|
|
}
|
|
});
|
|
});
|
|
return enabledRulesPerLineNumber;
|
|
}
|
|
|
|
// Array.sort comparison for objects in errors array
|
|
function lineNumberComparison(a, b) {
|
|
return a.lineNumber - b.lineNumber;
|
|
}
|
|
|
|
// Function to return true for all inputs
|
|
function filterAllValues() {
|
|
return true;
|
|
}
|
|
|
|
// Function to return unique values from a sorted errors array
|
|
function uniqueFilterForSortedErrors(value, index, array) {
|
|
return (index === 0) || (value.lineNumber > array[index - 1].lineNumber);
|
|
}
|
|
|
|
// Lints a single string
|
|
function lintContent(
|
|
ruleList,
|
|
name,
|
|
content,
|
|
md,
|
|
config,
|
|
frontMatter,
|
|
handleRuleFailures,
|
|
noInlineConfig,
|
|
resultVersion,
|
|
callback) {
|
|
// Remove UTF-8 byte order marker (if present)
|
|
content = content.replace(/^\ufeff/, "");
|
|
// Remove front matter
|
|
const removeFrontMatterResult = removeFrontMatter(content, frontMatter);
|
|
const frontMatterLines = removeFrontMatterResult.frontMatterLines;
|
|
// Ignore the content of HTML comments
|
|
content = helpers.clearHtmlCommentText(removeFrontMatterResult.content);
|
|
// Parse content into tokens and lines
|
|
const tokens = md.parse(content, {});
|
|
const lines = content.split(helpers.newLineRe);
|
|
annotateTokens(tokens, lines);
|
|
const aliasToRuleNames = mapAliasToRuleNames(ruleList);
|
|
const effectiveConfig =
|
|
getEffectiveConfig(ruleList, config, aliasToRuleNames);
|
|
const enabledRulesPerLineNumber = getEnabledRulesPerLineNumber(
|
|
ruleList, lines, frontMatterLines, noInlineConfig,
|
|
effectiveConfig, aliasToRuleNames);
|
|
// Create parameters for rules
|
|
const params = {
|
|
name,
|
|
tokens,
|
|
lines,
|
|
frontMatterLines
|
|
};
|
|
cache.lineMetadata(helpers.getLineMetadata(params));
|
|
cache.flattenedLists(helpers.flattenLists(params));
|
|
// Function to run for each rule
|
|
const result = (resultVersion === 0) ? {} : [];
|
|
function forRule(rule) {
|
|
// Configure rule
|
|
const ruleNameFriendly = rule.names[0];
|
|
const ruleName = ruleNameFriendly.toUpperCase();
|
|
params.config = effectiveConfig[ruleName];
|
|
function throwError(property) {
|
|
throw new Error(
|
|
"Property '" + property + "' of onError parameter is incorrect.");
|
|
}
|
|
const errors = [];
|
|
function onError(errorInfo) {
|
|
if (!errorInfo ||
|
|
!helpers.isNumber(errorInfo.lineNumber) ||
|
|
(errorInfo.lineNumber < 1) ||
|
|
(errorInfo.lineNumber > lines.length)) {
|
|
throwError("lineNumber");
|
|
}
|
|
if (errorInfo.detail &&
|
|
!helpers.isString(errorInfo.detail)) {
|
|
throwError("detail");
|
|
}
|
|
if (errorInfo.context &&
|
|
!helpers.isString(errorInfo.context)) {
|
|
throwError("context");
|
|
}
|
|
if (errorInfo.range &&
|
|
(!Array.isArray(errorInfo.range) ||
|
|
(errorInfo.range.length !== 2) ||
|
|
!helpers.isNumber(errorInfo.range[0]) ||
|
|
(errorInfo.range[0] < 1) ||
|
|
!helpers.isNumber(errorInfo.range[1]) ||
|
|
(errorInfo.range[1] < 1) ||
|
|
((errorInfo.range[0] + errorInfo.range[1] - 1) >
|
|
lines[errorInfo.lineNumber - 1].length))) {
|
|
throwError("range");
|
|
}
|
|
const fixInfo = errorInfo.fixInfo;
|
|
const cleanFixInfo = {};
|
|
if (fixInfo) {
|
|
if (!helpers.isObject(fixInfo)) {
|
|
throwError("fixInfo");
|
|
}
|
|
if (fixInfo.lineNumber !== undefined) {
|
|
if ((!helpers.isNumber(fixInfo.lineNumber) ||
|
|
(fixInfo.lineNumber < 1) ||
|
|
(fixInfo.lineNumber > lines.length))) {
|
|
throwError("fixInfo.lineNumber");
|
|
}
|
|
cleanFixInfo.lineNumber =
|
|
fixInfo.lineNumber + frontMatterLines.length;
|
|
}
|
|
const effectiveLineNumber = fixInfo.lineNumber || errorInfo.lineNumber;
|
|
if (fixInfo.editColumn !== undefined) {
|
|
if ((!helpers.isNumber(fixInfo.editColumn) ||
|
|
(fixInfo.editColumn < 1) ||
|
|
(fixInfo.editColumn >
|
|
lines[effectiveLineNumber - 1].length + 1))) {
|
|
throwError("fixInfo.editColumn");
|
|
}
|
|
cleanFixInfo.editColumn = fixInfo.editColumn;
|
|
}
|
|
if (fixInfo.deleteCount !== undefined) {
|
|
if ((!helpers.isNumber(fixInfo.deleteCount) ||
|
|
(fixInfo.deleteCount < -1) ||
|
|
(fixInfo.deleteCount >
|
|
lines[effectiveLineNumber - 1].length))) {
|
|
throwError("fixInfo.deleteCount");
|
|
}
|
|
cleanFixInfo.deleteCount = fixInfo.deleteCount;
|
|
}
|
|
if (fixInfo.insertText !== undefined) {
|
|
if (!helpers.isString(fixInfo.insertText)) {
|
|
throwError("fixInfo.insertText");
|
|
}
|
|
cleanFixInfo.insertText = fixInfo.insertText;
|
|
}
|
|
}
|
|
errors.push({
|
|
"lineNumber": errorInfo.lineNumber + frontMatterLines.length,
|
|
"detail": errorInfo.detail || null,
|
|
"context": errorInfo.context || null,
|
|
"range": errorInfo.range || null,
|
|
"fixInfo": fixInfo ? cleanFixInfo : null
|
|
});
|
|
}
|
|
// Call (possibly external) rule function
|
|
if (handleRuleFailures) {
|
|
try {
|
|
rule.function(params, onError);
|
|
} catch (ex) {
|
|
onError({
|
|
"lineNumber": 1,
|
|
"detail": `This rule threw an exception: ${ex.message}`
|
|
});
|
|
}
|
|
} else {
|
|
rule.function(params, onError);
|
|
}
|
|
// Record any errors (significant performance benefit from length check)
|
|
if (errors.length) {
|
|
errors.sort(lineNumberComparison);
|
|
const filteredErrors = errors
|
|
.filter((resultVersion === 3) ?
|
|
filterAllValues :
|
|
uniqueFilterForSortedErrors)
|
|
.filter(function removeDisabledRules(error) {
|
|
return enabledRulesPerLineNumber[error.lineNumber][ruleName];
|
|
})
|
|
.map(function formatResults(error) {
|
|
if (resultVersion === 0) {
|
|
return error.lineNumber;
|
|
}
|
|
const errorObject = {};
|
|
errorObject.lineNumber = error.lineNumber;
|
|
if (resultVersion === 1) {
|
|
errorObject.ruleName = ruleNameFriendly;
|
|
errorObject.ruleAlias = rule.names[1] || rule.names[0];
|
|
} else {
|
|
errorObject.ruleNames = rule.names;
|
|
}
|
|
errorObject.ruleDescription = rule.description;
|
|
errorObject.ruleInformation =
|
|
rule.information ? rule.information.href : null;
|
|
errorObject.errorDetail = error.detail;
|
|
errorObject.errorContext = error.context;
|
|
errorObject.errorRange = error.range;
|
|
if (resultVersion === 3) {
|
|
errorObject.fixInfo = error.fixInfo;
|
|
}
|
|
return errorObject;
|
|
});
|
|
if (filteredErrors.length) {
|
|
if (resultVersion === 0) {
|
|
result[ruleNameFriendly] = filteredErrors;
|
|
} else {
|
|
Array.prototype.push.apply(result, filteredErrors);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Run all rules
|
|
try {
|
|
ruleList.forEach(forRule);
|
|
} catch (ex) {
|
|
cache.clear();
|
|
return callback(ex);
|
|
}
|
|
cache.clear();
|
|
return callback(null, result);
|
|
}
|
|
|
|
// Lints a single file
|
|
function lintFile(
|
|
ruleList,
|
|
file,
|
|
md,
|
|
config,
|
|
frontMatter,
|
|
handleRuleFailures,
|
|
noInlineConfig,
|
|
resultVersion,
|
|
synchronous,
|
|
callback) {
|
|
function lintContentWrapper(err, content) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
return lintContent(ruleList, file, content, md, config, frontMatter,
|
|
handleRuleFailures, noInlineConfig, resultVersion, callback);
|
|
}
|
|
// Make a/synchronous call to read file
|
|
if (synchronous) {
|
|
lintContentWrapper(null, fs.readFileSync(file, helpers.utf8Encoding));
|
|
} else {
|
|
fs.readFile(file, helpers.utf8Encoding, lintContentWrapper);
|
|
}
|
|
}
|
|
|
|
// Lints files and strings
|
|
function lintInput(options, synchronous, callback) {
|
|
// Normalize inputs
|
|
options = options || {};
|
|
callback = callback || function noop() {};
|
|
const ruleList = rules.concat(options.customRules || []);
|
|
const ruleErr = validateRuleList(ruleList);
|
|
if (ruleErr) {
|
|
return callback(ruleErr);
|
|
}
|
|
let files = [];
|
|
if (Array.isArray(options.files)) {
|
|
files = options.files.slice();
|
|
} else if (options.files) {
|
|
files = [ String(options.files) ];
|
|
}
|
|
const strings = options.strings || {};
|
|
const stringsKeys = Object.keys(strings);
|
|
const config = options.config || { "default": true };
|
|
const frontMatter = (options.frontMatter === undefined) ?
|
|
helpers.frontMatterRe : options.frontMatter;
|
|
const handleRuleFailures = !!options.handleRuleFailures;
|
|
const noInlineConfig = !!options.noInlineConfig;
|
|
const resultVersion = (options.resultVersion === undefined) ?
|
|
2 : options.resultVersion;
|
|
const md = markdownIt({ "html": true });
|
|
const markdownItPlugins = options.markdownItPlugins || [];
|
|
markdownItPlugins.forEach(function forPlugin(plugin) {
|
|
// @ts-ignore
|
|
md.use(...plugin);
|
|
});
|
|
const results = newResults(ruleList);
|
|
// Helper to lint the next string or file
|
|
/* eslint-disable consistent-return */
|
|
function lintNextItem() {
|
|
let iterating = true;
|
|
let item = null;
|
|
function lintNextItemCallback(err, result) {
|
|
if (err) {
|
|
iterating = false;
|
|
return callback(err);
|
|
}
|
|
results[item] = result;
|
|
return iterating || lintNextItem();
|
|
}
|
|
while (iterating) {
|
|
if ((item = stringsKeys.shift())) {
|
|
lintContent(
|
|
ruleList,
|
|
item,
|
|
strings[item] || "",
|
|
md,
|
|
config,
|
|
frontMatter,
|
|
handleRuleFailures,
|
|
noInlineConfig,
|
|
resultVersion,
|
|
lintNextItemCallback);
|
|
} else if ((item = files.shift())) {
|
|
iterating = synchronous;
|
|
lintFile(
|
|
ruleList,
|
|
item,
|
|
md,
|
|
config,
|
|
frontMatter,
|
|
handleRuleFailures,
|
|
noInlineConfig,
|
|
resultVersion,
|
|
synchronous,
|
|
lintNextItemCallback);
|
|
} else {
|
|
return callback(null, results);
|
|
}
|
|
}
|
|
}
|
|
return lintNextItem();
|
|
}
|
|
|
|
/**
|
|
* Lint specified Markdown files.
|
|
*
|
|
* @param {Options} options Configuration options.
|
|
* @param {LintCallback} callback Callback (err, result) function.
|
|
* @returns {void}
|
|
*/
|
|
function markdownlint(options, callback) {
|
|
return lintInput(options, false, callback);
|
|
}
|
|
|
|
/**
|
|
* Lint specified Markdown files synchronously.
|
|
*
|
|
* @param {Options} options Configuration options.
|
|
* @returns {LintResults} Results object.
|
|
*/
|
|
function markdownlintSync(options) {
|
|
let results = null;
|
|
lintInput(options, true, function callback(error, res) {
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
results = res;
|
|
});
|
|
return results;
|
|
}
|
|
|
|
// Parses the content of a configuration file
|
|
function parseConfiguration(name, content, parsers) {
|
|
let config = null;
|
|
let message = "";
|
|
const errors = [];
|
|
// Try each parser
|
|
(parsers || [ JSON.parse ]).every((parser) => {
|
|
try {
|
|
config = parser(content);
|
|
} catch (ex) {
|
|
errors.push(ex.message);
|
|
}
|
|
return !config;
|
|
});
|
|
// Message if unable to parse
|
|
if (!config) {
|
|
errors.unshift(`Unable to parse '${name}'`);
|
|
message = errors.join("; ");
|
|
}
|
|
return {
|
|
config,
|
|
message
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Read specified configuration file.
|
|
*
|
|
* @param {string} file Configuration file name.
|
|
* @param {ConfigurationParser[] | null} parsers Parsing function(s).
|
|
* @param {ReadConfigCallback} callback Callback (err, result) function.
|
|
* @returns {void}
|
|
*/
|
|
function readConfig(file, parsers, callback) {
|
|
if (!callback) {
|
|
// @ts-ignore
|
|
callback = parsers;
|
|
parsers = null;
|
|
}
|
|
// Read file
|
|
fs.readFile(file, helpers.utf8Encoding, (err, content) => {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
// Try to parse file
|
|
const { config, message } = parseConfiguration(file, content, parsers);
|
|
if (!config) {
|
|
return callback(new Error(message));
|
|
}
|
|
// Extend configuration
|
|
const configExtends = config.extends;
|
|
if (configExtends) {
|
|
delete config.extends;
|
|
const extendsFile = path.resolve(path.dirname(file), configExtends);
|
|
return readConfig(extendsFile, parsers, (errr, extendsConfig) => {
|
|
if (errr) {
|
|
return callback(errr);
|
|
}
|
|
return callback(null, {
|
|
...extendsConfig,
|
|
...config
|
|
});
|
|
});
|
|
}
|
|
return callback(null, config);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Read specified configuration file synchronously.
|
|
*
|
|
* @param {string} file Configuration file name.
|
|
* @param {ConfigurationParser[]} [parsers] Parsing function(s).
|
|
* @returns {Configuration} Configuration object.
|
|
*/
|
|
function readConfigSync(file, parsers) {
|
|
// Read file
|
|
const content = fs.readFileSync(file, helpers.utf8Encoding);
|
|
// Try to parse file
|
|
const { config, message } = parseConfiguration(file, content, parsers);
|
|
if (!config) {
|
|
throw new Error(message);
|
|
}
|
|
// Extend configuration
|
|
const configExtends = config.extends;
|
|
if (configExtends) {
|
|
delete config.extends;
|
|
return {
|
|
...readConfigSync(
|
|
path.resolve(path.dirname(file), configExtends),
|
|
parsers
|
|
),
|
|
...config
|
|
};
|
|
}
|
|
return config;
|
|
}
|
|
|
|
// Export a/synchronous APIs
|
|
markdownlint.sync = markdownlintSync;
|
|
markdownlint.readConfig = readConfig;
|
|
markdownlint.readConfigSync = readConfigSync;
|
|
module.exports = markdownlint;
|
|
|
|
// Type declarations
|
|
|
|
/**
|
|
* Function to implement rule logic.
|
|
*
|
|
* @callback RuleFunction
|
|
* @param {RuleParams} params Rule parameters.
|
|
* @param {RuleOnError} onError Error-reporting callback.
|
|
* @returns {void}
|
|
*/
|
|
|
|
/**
|
|
* Rule parameters.
|
|
*
|
|
* @typedef {Object} RuleParams
|
|
* @property {string} name File/string name.
|
|
* @property {MarkdownItToken[]} tokens markdown-it token objects.
|
|
* @property {string[]} lines File/string lines.
|
|
* @property {string[]} frontMatterLines Front matter lines.
|
|
* @property {RuleConfiguration} config Rule configuration.
|
|
*/
|
|
|
|
/**
|
|
* Markdown-It token.
|
|
*
|
|
* @typedef {Object} MarkdownItToken
|
|
* @property {string[][]} attrs HTML attributes.
|
|
* @property {boolean} block Block-level token.
|
|
* @property {MarkdownItToken[]} children Child nodes.
|
|
* @property {string} content Tag contents.
|
|
* @property {boolean} hidden Ignore element.
|
|
* @property {string} info Fence info.
|
|
* @property {number} level Nesting level.
|
|
* @property {number[]} map Beginning/ending line numbers.
|
|
* @property {string} markup Markup text.
|
|
* @property {Object} meta Arbitrary data.
|
|
* @property {number} nesting Level change.
|
|
* @property {string} tag HTML tag name.
|
|
* @property {string} type Token type.
|
|
* @property {number} lineNumber Line number (1-based).
|
|
* @property {string} line Line content.
|
|
*/
|
|
|
|
/**
|
|
* Error-reporting callback.
|
|
*
|
|
* @callback RuleOnError
|
|
* @param {RuleOnErrorInfo} onErrorInfo Error information.
|
|
* @returns {void}
|
|
*/
|
|
|
|
/**
|
|
* Fix information for RuleOnError callback.
|
|
*
|
|
* @typedef {Object} RuleOnErrorInfo
|
|
* @property {number} lineNumber Line number (1-based).
|
|
* @property {string} [details] Details about the error.
|
|
* @property {string} [context] Context for the error.
|
|
* @property {number[]} [range] Column number (1-based) and length.
|
|
* @property {RuleOnErrorFixInfo} [fixInfo] Fix information.
|
|
*/
|
|
|
|
/**
|
|
* Fix information for RuleOnErrorInfo.
|
|
*
|
|
* @typedef {Object} RuleOnErrorFixInfo
|
|
* @property {number} [lineNumber] Line number (1-based).
|
|
* @property {number} [editColumn] Column of the fix (1-based).
|
|
* @property {number} [deleteCount] Count of characters to delete.
|
|
* @property {string} [insertText] Text to insert (after deleting).
|
|
*/
|
|
|
|
/**
|
|
* Rule definition.
|
|
*
|
|
* @typedef {Object} Rule
|
|
* @property {string[]} names Rule name(s).
|
|
* @property {string} description Rule description.
|
|
* @property {URL} [information] Link to more information.
|
|
* @property {string[]} tags Rule tag(s).
|
|
* @property {RuleFunction} function Rule implementation.
|
|
*/
|
|
|
|
/**
|
|
* Configuration options.
|
|
*
|
|
* @typedef {Object} Options
|
|
* @property {string[] | string} [files] Files to lint.
|
|
* @property {Object.<string, string>} [strings] Strings to lint.
|
|
* @property {Configuration} [config] Configuration object.
|
|
* @property {Rule[] | Rule} [customRules] Custom rules.
|
|
* @property {RegExp} [frontMatter] Front matter pattern.
|
|
* @property {boolean} [handleRuleFailures] True to catch exceptions.
|
|
* @property {boolean} [noInlineConfig] True to ignore HTML directives.
|
|
* @property {number} [resultVersion] Results object version.
|
|
* @property {Plugin[]} [markdownItPlugins] Additional plugins.
|
|
*/
|
|
|
|
/**
|
|
* markdown-it plugin.
|
|
*
|
|
* @typedef {Array} Plugin
|
|
*/
|
|
|
|
/**
|
|
* Function to pretty-print lint results.
|
|
*
|
|
* @callback ToStringCallback
|
|
* @param {boolean} [ruleAliases] True to use rule aliases.
|
|
* @returns {string}
|
|
*/
|
|
|
|
/**
|
|
* Lint results (for resultVersion 3).
|
|
*
|
|
* @typedef {Object.<string, LintError[]>} LintResults
|
|
*/
|
|
// The following should be part of the LintResults typedef, but that causes
|
|
// TypeScript to "forget" about the string map.
|
|
// * @property {ToStringCallback} toString String representation.
|
|
// https://github.com/microsoft/TypeScript/issues/34911
|
|
|
|
/**
|
|
* Lint error.
|
|
*
|
|
* @typedef {Object} LintError
|
|
* @property {number} lineNumber Line number (1-based).
|
|
* @property {string[]} ruleNames Rule name(s).
|
|
* @property {string} ruleDescription Rule description.
|
|
* @property {string} ruleInformation Link to more information.
|
|
* @property {string} errorDetail Detail about the error.
|
|
* @property {string} errorContext Context for the error.
|
|
* @property {number[]} errorRange Column number (1-based) and length.
|
|
* @property {FixInfo} fixInfo Fix information.
|
|
*/
|
|
|
|
/**
|
|
* Fix information.
|
|
*
|
|
* @typedef {Object} FixInfo
|
|
* @property {number} [editColumn] Column of the fix (1-based).
|
|
* @property {number} [deleteCount] Count of characters to delete.
|
|
* @property {string} [insertText] Text to insert (after deleting).
|
|
*/
|
|
|
|
/**
|
|
* Called with the result of the lint operation.
|
|
*
|
|
* @callback LintCallback
|
|
* @param {Error | null} err Error object or null.
|
|
* @param {LintResults} [results] Lint results.
|
|
* @returns {void}
|
|
*/
|
|
|
|
/**
|
|
* Configuration object for linting rules. For a detailed schema, see
|
|
* {@link ../schema/markdownlint-config-schema.json}.
|
|
*
|
|
* @typedef {Object.<string, RuleConfiguration>} Configuration
|
|
*/
|
|
|
|
/**
|
|
* Rule configuration object.
|
|
*
|
|
* @typedef {boolean | Object} RuleConfiguration Rule configuration.
|
|
*/
|
|
|
|
/**
|
|
* Parses a configuration string and returns a configuration object.
|
|
*
|
|
* @callback ConfigurationParser
|
|
* @param {string} text Configuration string.
|
|
* @returns {Configuration}
|
|
*/
|
|
|
|
/**
|
|
* Called with the result of the readConfig operation.
|
|
*
|
|
* @callback ReadConfigCallback
|
|
* @param {Error | null} err Error object or null.
|
|
* @param {Configuration} [config] Configuration object.
|
|
* @returns {void}
|
|
*/
|