mirror of
https://github.com/DavidAnson/markdownlint.git
synced 2025-12-17 06:20:12 +01:00
Add support for authoring custom rules.
This commit is contained in:
parent
f24f98e146
commit
802c81f929
6 changed files with 375 additions and 97 deletions
|
|
@ -8,8 +8,63 @@ var md = require("markdown-it")({ "html": true });
|
|||
var rules = require("./rules");
|
||||
var shared = require("./shared");
|
||||
|
||||
// Validates the list of rules for structure and reuse
|
||||
function validateRuleList(ruleList) {
|
||||
var result = null;
|
||||
if (ruleList.length === rules.length) {
|
||||
// No need to validate if only using built-in rules
|
||||
return result;
|
||||
}
|
||||
var allIds = {};
|
||||
ruleList.forEach(function forRule(rule, index) {
|
||||
var 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) {
|
||||
var value = rule[property];
|
||||
if (!result &&
|
||||
(!value || !Array.isArray(value) || (value.length === 0) ||
|
||||
!value.every(shared.isString) || value.some(shared.isEmptyString))) {
|
||||
result = newError(property);
|
||||
}
|
||||
});
|
||||
[
|
||||
[ "description", "string" ],
|
||||
[ "function", "function" ]
|
||||
].forEach(function forProperty(propertyInfo) {
|
||||
var property = propertyInfo[0];
|
||||
var value = rule[property];
|
||||
if (!result && (!value || (typeof value !== propertyInfo[1]))) {
|
||||
result = newError(property);
|
||||
}
|
||||
});
|
||||
if (!result) {
|
||||
rule.names.forEach(function forName(name) {
|
||||
var 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) {
|
||||
var 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(ruleSet) {
|
||||
function newResults(ruleList) {
|
||||
function Results() {}
|
||||
Results.prototype.toString = function resultsToString(useAlias) {
|
||||
var that = this;
|
||||
|
|
@ -37,7 +92,7 @@ function newResults(ruleSet) {
|
|||
} else {
|
||||
if (!ruleNameToRule) {
|
||||
ruleNameToRule = {};
|
||||
ruleSet.forEach(function forRule(rule) {
|
||||
ruleList.forEach(function forRule(rule) {
|
||||
var ruleName = rule.names[0].toUpperCase();
|
||||
ruleNameToRule[ruleName] = rule;
|
||||
});
|
||||
|
|
@ -156,13 +211,13 @@ function mapAliasToRuleNames(ruleList) {
|
|||
}
|
||||
|
||||
// Apply (and normalize) config
|
||||
function getEffectiveConfig(ruleSet, config, aliasToRuleNames) {
|
||||
function getEffectiveConfig(ruleList, config, aliasToRuleNames) {
|
||||
var defaultKey = Object.keys(config).filter(function forKey(key) {
|
||||
return key.toUpperCase() === "DEFAULT";
|
||||
});
|
||||
var ruleDefault = (defaultKey.length === 0) || !!config[defaultKey[0]];
|
||||
var effectiveConfig = {};
|
||||
ruleSet.forEach(function forRule(rule) {
|
||||
ruleList.forEach(function forRule(rule) {
|
||||
var ruleName = rule.names[0].toUpperCase();
|
||||
effectiveConfig[ruleName] = ruleDefault;
|
||||
});
|
||||
|
|
@ -185,11 +240,11 @@ function getEffectiveConfig(ruleSet, config, aliasToRuleNames) {
|
|||
|
||||
// Create mapping of enabled rules per line
|
||||
function getEnabledRulesPerLineNumber(
|
||||
ruleSet, lines, frontMatterLines, noInlineConfig,
|
||||
ruleList, lines, frontMatterLines, noInlineConfig,
|
||||
effectiveConfig, aliasToRuleNames) {
|
||||
var enabledRules = {};
|
||||
var allRuleNames = [];
|
||||
ruleSet.forEach(function forRule(rule) {
|
||||
ruleList.forEach(function forRule(rule) {
|
||||
var ruleName = rule.names[0].toUpperCase();
|
||||
allRuleNames.push(ruleName);
|
||||
enabledRules[ruleName] = !!effectiveConfig[ruleName];
|
||||
|
|
@ -234,7 +289,8 @@ function uniqueFilterForSortedErrors(value, index, array) {
|
|||
|
||||
// Lints a single string
|
||||
function lintContent(
|
||||
ruleSet, content, config, frontMatter, noInlineConfig, resultVersion) {
|
||||
ruleList, content, config, frontMatter, noInlineConfig, resultVersion,
|
||||
callback) {
|
||||
// Remove UTF-8 byte order marker (if present)
|
||||
if (content.charCodeAt(0) === 0xfeff) {
|
||||
content = content.slice(1);
|
||||
|
|
@ -249,10 +305,10 @@ function lintContent(
|
|||
var tokens = md.parse(content, {});
|
||||
var lines = content.split(shared.newLineRe);
|
||||
var tokenLists = annotateTokens(tokens, lines);
|
||||
var aliasToRuleNames = mapAliasToRuleNames(ruleSet);
|
||||
var effectiveConfig = getEffectiveConfig(ruleSet, config, aliasToRuleNames);
|
||||
var aliasToRuleNames = mapAliasToRuleNames(ruleList);
|
||||
var effectiveConfig = getEffectiveConfig(ruleList, config, aliasToRuleNames);
|
||||
var enabledRulesPerLineNumber = getEnabledRulesPerLineNumber(
|
||||
ruleSet, lines, frontMatterLines, noInlineConfig,
|
||||
ruleList, lines, frontMatterLines, noInlineConfig,
|
||||
effectiveConfig, aliasToRuleNames);
|
||||
// Create parameters for rules
|
||||
var params = {
|
||||
|
|
@ -261,9 +317,9 @@ function lintContent(
|
|||
"lines": lines,
|
||||
"frontMatterLines": frontMatterLines
|
||||
};
|
||||
// Run each rule
|
||||
// Function to run for each rule
|
||||
var result = (resultVersion === 0) ? {} : [];
|
||||
ruleSet.forEach(function forRule(rule) {
|
||||
function forRule(rule) {
|
||||
// Configure rule
|
||||
var ruleNameFriendly = rule.names[0];
|
||||
var ruleName = ruleNameFriendly.toUpperCase();
|
||||
|
|
@ -277,6 +333,7 @@ function lintContent(
|
|||
"range": errorInfo.range || null
|
||||
});
|
||||
}
|
||||
// Call (possibly external) rule function
|
||||
rule.function(params, onError);
|
||||
// Record any errors (significant performance benefit from length check)
|
||||
if (errors.length) {
|
||||
|
|
@ -312,13 +369,19 @@ function lintContent(
|
|||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
// Run all rules
|
||||
try {
|
||||
ruleList.forEach(forRule);
|
||||
} catch (ex) {
|
||||
return callback(ex);
|
||||
}
|
||||
callback(null, result);
|
||||
}
|
||||
|
||||
// Lints a single file
|
||||
function lintFile(
|
||||
ruleSet,
|
||||
ruleList,
|
||||
file,
|
||||
config,
|
||||
frontMatter,
|
||||
|
|
@ -330,9 +393,8 @@ function lintFile(
|
|||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var result = lintContent(
|
||||
ruleSet, content, config, frontMatter, noInlineConfig, resultVersion);
|
||||
callback(null, result);
|
||||
lintContent(ruleList, content, config, frontMatter, noInlineConfig,
|
||||
resultVersion, callback);
|
||||
}
|
||||
// Make a/synchronous call to read file
|
||||
if (synchronous) {
|
||||
|
|
@ -346,7 +408,11 @@ function lintInput(options, synchronous, callback) {
|
|||
// Normalize inputs
|
||||
options = options || {};
|
||||
callback = callback || function noop() {};
|
||||
var ruleSet = rules.concat(options.customRules || []);
|
||||
var ruleList = rules.concat(options.customRules || []);
|
||||
var ruleErr = validateRuleList(ruleList);
|
||||
if (ruleErr) {
|
||||
return callback(ruleErr);
|
||||
}
|
||||
var files = [];
|
||||
if (Array.isArray(options.files)) {
|
||||
files = options.files.slice();
|
||||
|
|
@ -354,50 +420,46 @@ function lintInput(options, synchronous, callback) {
|
|||
files = [ String(options.files) ];
|
||||
}
|
||||
var strings = options.strings || {};
|
||||
var stringsKeys = Object.keys(strings);
|
||||
var config = options.config || { "default": true };
|
||||
var frontMatter = (options.frontMatter === undefined) ?
|
||||
shared.frontMatterRe : options.frontMatter;
|
||||
var noInlineConfig = !!options.noInlineConfig;
|
||||
var resultVersion = (options.resultVersion === undefined) ?
|
||||
2 : options.resultVersion;
|
||||
var results = newResults(ruleSet);
|
||||
// Helper to lint the next file in the array
|
||||
function lintFilesArray() {
|
||||
var file = files.shift();
|
||||
if (file) {
|
||||
var results = newResults(ruleList);
|
||||
// Helper to lint the next string or file item
|
||||
var item = null;
|
||||
function lintNextItem(err, result) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
} else if (result) {
|
||||
results[item] = result;
|
||||
}
|
||||
if ((item = stringsKeys.shift())) {
|
||||
lintContent(
|
||||
ruleList,
|
||||
strings[item] || "",
|
||||
config,
|
||||
frontMatter,
|
||||
noInlineConfig,
|
||||
resultVersion,
|
||||
lintNextItem);
|
||||
} else if ((item = files.shift())) {
|
||||
lintFile(
|
||||
ruleSet,
|
||||
file,
|
||||
ruleList,
|
||||
item,
|
||||
config,
|
||||
frontMatter,
|
||||
noInlineConfig,
|
||||
resultVersion,
|
||||
synchronous,
|
||||
function lintedFile(err, result) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
// Record errors and lint next file
|
||||
results[file] = result;
|
||||
lintFilesArray();
|
||||
});
|
||||
lintNextItem);
|
||||
} else {
|
||||
callback(null, results);
|
||||
}
|
||||
}
|
||||
// Lint strings
|
||||
Object.keys(strings).forEach(function forKey(key) {
|
||||
var result = lintContent(
|
||||
ruleSet,
|
||||
strings[key] || "",
|
||||
config,
|
||||
frontMatter,
|
||||
noInlineConfig,
|
||||
resultVersion);
|
||||
results[key] = result;
|
||||
});
|
||||
// Lint files
|
||||
lintFilesArray();
|
||||
lintNextItem();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -420,10 +482,9 @@ function markdownlint(options, callback) {
|
|||
function markdownlintSync(options) {
|
||||
var results = null;
|
||||
lintInput(options, true, function callback(error, res) {
|
||||
// Unreachable; no code path in the synchronous case passes error
|
||||
// if (error) {
|
||||
// throw error;
|
||||
// }
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
results = res;
|
||||
});
|
||||
return results;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue