mirror of
https://github.com/DavidAnson/markdownlint.git
synced 2025-12-16 22:10:13 +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 rules = require("./rules");
|
||||||
var shared = require("./shared");
|
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
|
// Class for results with toString for pretty display
|
||||||
function newResults(ruleSet) {
|
function newResults(ruleList) {
|
||||||
function Results() {}
|
function Results() {}
|
||||||
Results.prototype.toString = function resultsToString(useAlias) {
|
Results.prototype.toString = function resultsToString(useAlias) {
|
||||||
var that = this;
|
var that = this;
|
||||||
|
|
@ -37,7 +92,7 @@ function newResults(ruleSet) {
|
||||||
} else {
|
} else {
|
||||||
if (!ruleNameToRule) {
|
if (!ruleNameToRule) {
|
||||||
ruleNameToRule = {};
|
ruleNameToRule = {};
|
||||||
ruleSet.forEach(function forRule(rule) {
|
ruleList.forEach(function forRule(rule) {
|
||||||
var ruleName = rule.names[0].toUpperCase();
|
var ruleName = rule.names[0].toUpperCase();
|
||||||
ruleNameToRule[ruleName] = rule;
|
ruleNameToRule[ruleName] = rule;
|
||||||
});
|
});
|
||||||
|
|
@ -156,13 +211,13 @@ function mapAliasToRuleNames(ruleList) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply (and normalize) config
|
// Apply (and normalize) config
|
||||||
function getEffectiveConfig(ruleSet, config, aliasToRuleNames) {
|
function getEffectiveConfig(ruleList, config, aliasToRuleNames) {
|
||||||
var defaultKey = Object.keys(config).filter(function forKey(key) {
|
var defaultKey = Object.keys(config).filter(function forKey(key) {
|
||||||
return key.toUpperCase() === "DEFAULT";
|
return key.toUpperCase() === "DEFAULT";
|
||||||
});
|
});
|
||||||
var ruleDefault = (defaultKey.length === 0) || !!config[defaultKey[0]];
|
var ruleDefault = (defaultKey.length === 0) || !!config[defaultKey[0]];
|
||||||
var effectiveConfig = {};
|
var effectiveConfig = {};
|
||||||
ruleSet.forEach(function forRule(rule) {
|
ruleList.forEach(function forRule(rule) {
|
||||||
var ruleName = rule.names[0].toUpperCase();
|
var ruleName = rule.names[0].toUpperCase();
|
||||||
effectiveConfig[ruleName] = ruleDefault;
|
effectiveConfig[ruleName] = ruleDefault;
|
||||||
});
|
});
|
||||||
|
|
@ -185,11 +240,11 @@ function getEffectiveConfig(ruleSet, config, aliasToRuleNames) {
|
||||||
|
|
||||||
// Create mapping of enabled rules per line
|
// Create mapping of enabled rules per line
|
||||||
function getEnabledRulesPerLineNumber(
|
function getEnabledRulesPerLineNumber(
|
||||||
ruleSet, lines, frontMatterLines, noInlineConfig,
|
ruleList, lines, frontMatterLines, noInlineConfig,
|
||||||
effectiveConfig, aliasToRuleNames) {
|
effectiveConfig, aliasToRuleNames) {
|
||||||
var enabledRules = {};
|
var enabledRules = {};
|
||||||
var allRuleNames = [];
|
var allRuleNames = [];
|
||||||
ruleSet.forEach(function forRule(rule) {
|
ruleList.forEach(function forRule(rule) {
|
||||||
var ruleName = rule.names[0].toUpperCase();
|
var ruleName = rule.names[0].toUpperCase();
|
||||||
allRuleNames.push(ruleName);
|
allRuleNames.push(ruleName);
|
||||||
enabledRules[ruleName] = !!effectiveConfig[ruleName];
|
enabledRules[ruleName] = !!effectiveConfig[ruleName];
|
||||||
|
|
@ -234,7 +289,8 @@ function uniqueFilterForSortedErrors(value, index, array) {
|
||||||
|
|
||||||
// Lints a single string
|
// Lints a single string
|
||||||
function lintContent(
|
function lintContent(
|
||||||
ruleSet, content, config, frontMatter, noInlineConfig, resultVersion) {
|
ruleList, content, config, frontMatter, noInlineConfig, resultVersion,
|
||||||
|
callback) {
|
||||||
// Remove UTF-8 byte order marker (if present)
|
// Remove UTF-8 byte order marker (if present)
|
||||||
if (content.charCodeAt(0) === 0xfeff) {
|
if (content.charCodeAt(0) === 0xfeff) {
|
||||||
content = content.slice(1);
|
content = content.slice(1);
|
||||||
|
|
@ -249,10 +305,10 @@ function lintContent(
|
||||||
var tokens = md.parse(content, {});
|
var tokens = md.parse(content, {});
|
||||||
var lines = content.split(shared.newLineRe);
|
var lines = content.split(shared.newLineRe);
|
||||||
var tokenLists = annotateTokens(tokens, lines);
|
var tokenLists = annotateTokens(tokens, lines);
|
||||||
var aliasToRuleNames = mapAliasToRuleNames(ruleSet);
|
var aliasToRuleNames = mapAliasToRuleNames(ruleList);
|
||||||
var effectiveConfig = getEffectiveConfig(ruleSet, config, aliasToRuleNames);
|
var effectiveConfig = getEffectiveConfig(ruleList, config, aliasToRuleNames);
|
||||||
var enabledRulesPerLineNumber = getEnabledRulesPerLineNumber(
|
var enabledRulesPerLineNumber = getEnabledRulesPerLineNumber(
|
||||||
ruleSet, lines, frontMatterLines, noInlineConfig,
|
ruleList, lines, frontMatterLines, noInlineConfig,
|
||||||
effectiveConfig, aliasToRuleNames);
|
effectiveConfig, aliasToRuleNames);
|
||||||
// Create parameters for rules
|
// Create parameters for rules
|
||||||
var params = {
|
var params = {
|
||||||
|
|
@ -261,9 +317,9 @@ function lintContent(
|
||||||
"lines": lines,
|
"lines": lines,
|
||||||
"frontMatterLines": frontMatterLines
|
"frontMatterLines": frontMatterLines
|
||||||
};
|
};
|
||||||
// Run each rule
|
// Function to run for each rule
|
||||||
var result = (resultVersion === 0) ? {} : [];
|
var result = (resultVersion === 0) ? {} : [];
|
||||||
ruleSet.forEach(function forRule(rule) {
|
function forRule(rule) {
|
||||||
// Configure rule
|
// Configure rule
|
||||||
var ruleNameFriendly = rule.names[0];
|
var ruleNameFriendly = rule.names[0];
|
||||||
var ruleName = ruleNameFriendly.toUpperCase();
|
var ruleName = ruleNameFriendly.toUpperCase();
|
||||||
|
|
@ -277,6 +333,7 @@ function lintContent(
|
||||||
"range": errorInfo.range || null
|
"range": errorInfo.range || null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Call (possibly external) rule function
|
||||||
rule.function(params, onError);
|
rule.function(params, onError);
|
||||||
// Record any errors (significant performance benefit from length check)
|
// Record any errors (significant performance benefit from length check)
|
||||||
if (errors.length) {
|
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
|
// Lints a single file
|
||||||
function lintFile(
|
function lintFile(
|
||||||
ruleSet,
|
ruleList,
|
||||||
file,
|
file,
|
||||||
config,
|
config,
|
||||||
frontMatter,
|
frontMatter,
|
||||||
|
|
@ -330,9 +393,8 @@ function lintFile(
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
var result = lintContent(
|
lintContent(ruleList, content, config, frontMatter, noInlineConfig,
|
||||||
ruleSet, content, config, frontMatter, noInlineConfig, resultVersion);
|
resultVersion, callback);
|
||||||
callback(null, result);
|
|
||||||
}
|
}
|
||||||
// Make a/synchronous call to read file
|
// Make a/synchronous call to read file
|
||||||
if (synchronous) {
|
if (synchronous) {
|
||||||
|
|
@ -346,7 +408,11 @@ function lintInput(options, synchronous, callback) {
|
||||||
// Normalize inputs
|
// Normalize inputs
|
||||||
options = options || {};
|
options = options || {};
|
||||||
callback = callback || function noop() {};
|
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 = [];
|
var files = [];
|
||||||
if (Array.isArray(options.files)) {
|
if (Array.isArray(options.files)) {
|
||||||
files = options.files.slice();
|
files = options.files.slice();
|
||||||
|
|
@ -354,50 +420,46 @@ function lintInput(options, synchronous, callback) {
|
||||||
files = [ String(options.files) ];
|
files = [ String(options.files) ];
|
||||||
}
|
}
|
||||||
var strings = options.strings || {};
|
var strings = options.strings || {};
|
||||||
|
var stringsKeys = Object.keys(strings);
|
||||||
var config = options.config || { "default": true };
|
var config = options.config || { "default": true };
|
||||||
var frontMatter = (options.frontMatter === undefined) ?
|
var frontMatter = (options.frontMatter === undefined) ?
|
||||||
shared.frontMatterRe : options.frontMatter;
|
shared.frontMatterRe : options.frontMatter;
|
||||||
var noInlineConfig = !!options.noInlineConfig;
|
var noInlineConfig = !!options.noInlineConfig;
|
||||||
var resultVersion = (options.resultVersion === undefined) ?
|
var resultVersion = (options.resultVersion === undefined) ?
|
||||||
2 : options.resultVersion;
|
2 : options.resultVersion;
|
||||||
var results = newResults(ruleSet);
|
var results = newResults(ruleList);
|
||||||
// Helper to lint the next file in the array
|
// Helper to lint the next string or file item
|
||||||
function lintFilesArray() {
|
var item = null;
|
||||||
var file = files.shift();
|
function lintNextItem(err, result) {
|
||||||
if (file) {
|
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(
|
lintFile(
|
||||||
ruleSet,
|
ruleList,
|
||||||
file,
|
item,
|
||||||
config,
|
config,
|
||||||
frontMatter,
|
frontMatter,
|
||||||
noInlineConfig,
|
noInlineConfig,
|
||||||
resultVersion,
|
resultVersion,
|
||||||
synchronous,
|
synchronous,
|
||||||
function lintedFile(err, result) {
|
lintNextItem);
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
// Record errors and lint next file
|
|
||||||
results[file] = result;
|
|
||||||
lintFilesArray();
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
callback(null, results);
|
callback(null, results);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Lint strings
|
lintNextItem();
|
||||||
Object.keys(strings).forEach(function forKey(key) {
|
|
||||||
var result = lintContent(
|
|
||||||
ruleSet,
|
|
||||||
strings[key] || "",
|
|
||||||
config,
|
|
||||||
frontMatter,
|
|
||||||
noInlineConfig,
|
|
||||||
resultVersion);
|
|
||||||
results[key] = result;
|
|
||||||
});
|
|
||||||
// Lint files
|
|
||||||
lintFilesArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -420,10 +482,9 @@ function markdownlint(options, callback) {
|
||||||
function markdownlintSync(options) {
|
function markdownlintSync(options) {
|
||||||
var results = null;
|
var results = null;
|
||||||
lintInput(options, true, function callback(error, res) {
|
lintInput(options, true, function callback(error, res) {
|
||||||
// Unreachable; no code path in the synchronous case passes error
|
if (error) {
|
||||||
// if (error) {
|
throw error;
|
||||||
// throw error;
|
}
|
||||||
// }
|
|
||||||
results = res;
|
results = res;
|
||||||
});
|
});
|
||||||
return results;
|
return results;
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,16 @@ module.exports.clone = function clone(obj) {
|
||||||
return assign({}, obj);
|
return assign({}, obj);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Returns true iff the input is a string
|
||||||
|
module.exports.isString = function isString(obj) {
|
||||||
|
return typeof obj === "string";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns true iff the input string is empty
|
||||||
|
module.exports.isEmptyString = function isEmptyString(str) {
|
||||||
|
return str.length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
// Replaces the text of all properly-formatted HTML comments with whitespace
|
// Replaces the text of all properly-formatted HTML comments with whitespace
|
||||||
// This preserves the line/column information for the rest of the document
|
// This preserves the line/column information for the rest of the document
|
||||||
// Trailing whitespace is avoided with a '\' character in the last column
|
// Trailing whitespace is avoided with a '\' character in the last column
|
||||||
|
|
|
||||||
|
|
@ -1023,32 +1023,6 @@ module.exports.missingStringValue = function missingStringValue(test) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.ruleNamesUpperCase = function ruleNamesUpperCase(test) {
|
|
||||||
test.expect(41);
|
|
||||||
rules.forEach(function forRule(rule) {
|
|
||||||
var ruleName = rule.names[0];
|
|
||||||
test.equal(ruleName, ruleName.toUpperCase(), "Rule name not upper-case.");
|
|
||||||
});
|
|
||||||
test.done();
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.uniqueNames = function uniqueNames(test) {
|
|
||||||
test.expect(164);
|
|
||||||
var tags = [];
|
|
||||||
rules.forEach(function forRule(rule) {
|
|
||||||
Array.prototype.push.apply(tags, rule.tags);
|
|
||||||
});
|
|
||||||
var names = [];
|
|
||||||
rules.forEach(function forRule(rule) {
|
|
||||||
rule.names.forEach(function forAlias(name) {
|
|
||||||
test.ok(tags.indexOf(name) === -1, "Name not unique in tags.");
|
|
||||||
test.ok(names.indexOf(name) === -1, "Name not unique in names.");
|
|
||||||
names.push(name);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test.done();
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.readme = function readme(test) {
|
module.exports.readme = function readme(test) {
|
||||||
test.expect(108);
|
test.expect(108);
|
||||||
var tagToRules = {};
|
var tagToRules = {};
|
||||||
|
|
@ -1561,15 +1535,16 @@ module.exports.customRulesV0 = function customRulesV0(test) {
|
||||||
test.ifError(err);
|
test.ifError(err);
|
||||||
var expectedResult = {};
|
var expectedResult = {};
|
||||||
expectedResult[customRulesMd] = {
|
expectedResult[customRulesMd] = {
|
||||||
"blockquote": [ 12 ],
|
"any-blockquote": [ 12 ],
|
||||||
"every-n-lines": [ 2, 4, 6, 10, 12 ],
|
"every-n-lines": [ 2, 4, 6, 10, 12 ],
|
||||||
|
"first-line": [ 1 ],
|
||||||
"letters-E-X": [ 3, 7 ]
|
"letters-E-X": [ 3, 7 ]
|
||||||
};
|
};
|
||||||
test.deepEqual(actualResult, expectedResult, "Undetected issues.");
|
test.deepEqual(actualResult, expectedResult, "Undetected issues.");
|
||||||
var actualMessage = actualResult.toString();
|
var actualMessage = actualResult.toString();
|
||||||
var expectedMessage =
|
var expectedMessage =
|
||||||
"./test/custom-rules.md: 12: blockquote" +
|
"./test/custom-rules.md: 12: any-blockquote" +
|
||||||
" Rule that reports an error for blockquotes\n" +
|
" Rule that reports an error for any blockquote\n" +
|
||||||
"./test/custom-rules.md: 2: every-n-lines" +
|
"./test/custom-rules.md: 2: every-n-lines" +
|
||||||
" Rule that reports an error every N lines\n" +
|
" Rule that reports an error every N lines\n" +
|
||||||
"./test/custom-rules.md: 4: every-n-lines" +
|
"./test/custom-rules.md: 4: every-n-lines" +
|
||||||
|
|
@ -1580,6 +1555,8 @@ module.exports.customRulesV0 = function customRulesV0(test) {
|
||||||
" Rule that reports an error every N lines\n" +
|
" Rule that reports an error every N lines\n" +
|
||||||
"./test/custom-rules.md: 12: every-n-lines" +
|
"./test/custom-rules.md: 12: every-n-lines" +
|
||||||
" Rule that reports an error every N lines\n" +
|
" Rule that reports an error every N lines\n" +
|
||||||
|
"./test/custom-rules.md: 1: first-line" +
|
||||||
|
" Rule that reports an error for the first line\n" +
|
||||||
"./test/custom-rules.md: 3: letters-E-X" +
|
"./test/custom-rules.md: 3: letters-E-X" +
|
||||||
" Rule that reports an error for lines with the letters 'EX'\n" +
|
" Rule that reports an error for lines with the letters 'EX'\n" +
|
||||||
"./test/custom-rules.md: 7: letters-E-X" +
|
"./test/custom-rules.md: 7: letters-E-X" +
|
||||||
|
|
@ -1587,8 +1564,8 @@ module.exports.customRulesV0 = function customRulesV0(test) {
|
||||||
test.equal(actualMessage, expectedMessage, "Incorrect message (name).");
|
test.equal(actualMessage, expectedMessage, "Incorrect message (name).");
|
||||||
actualMessage = actualResult.toString(true);
|
actualMessage = actualResult.toString(true);
|
||||||
expectedMessage =
|
expectedMessage =
|
||||||
"./test/custom-rules.md: 12: blockquote" +
|
"./test/custom-rules.md: 12: any-blockquote" +
|
||||||
" Rule that reports an error for blockquotes\n" +
|
" Rule that reports an error for any blockquote\n" +
|
||||||
"./test/custom-rules.md: 2: every-n-lines" +
|
"./test/custom-rules.md: 2: every-n-lines" +
|
||||||
" Rule that reports an error every N lines\n" +
|
" Rule that reports an error every N lines\n" +
|
||||||
"./test/custom-rules.md: 4: every-n-lines" +
|
"./test/custom-rules.md: 4: every-n-lines" +
|
||||||
|
|
@ -1599,6 +1576,8 @@ module.exports.customRulesV0 = function customRulesV0(test) {
|
||||||
" Rule that reports an error every N lines\n" +
|
" Rule that reports an error every N lines\n" +
|
||||||
"./test/custom-rules.md: 12: every-n-lines" +
|
"./test/custom-rules.md: 12: every-n-lines" +
|
||||||
" Rule that reports an error every N lines\n" +
|
" Rule that reports an error every N lines\n" +
|
||||||
|
"./test/custom-rules.md: 1: first-line" +
|
||||||
|
" Rule that reports an error for the first line\n" +
|
||||||
"./test/custom-rules.md: 3: letter-E-letter-X" +
|
"./test/custom-rules.md: 3: letter-E-letter-X" +
|
||||||
" Rule that reports an error for lines with the letters 'EX'\n" +
|
" Rule that reports an error for lines with the letters 'EX'\n" +
|
||||||
"./test/custom-rules.md: 7: letter-E-letter-X" +
|
"./test/custom-rules.md: 7: letter-E-letter-X" +
|
||||||
|
|
@ -1621,9 +1600,9 @@ module.exports.customRulesV1 = function customRulesV1(test) {
|
||||||
var expectedResult = {};
|
var expectedResult = {};
|
||||||
expectedResult[customRulesMd] = [
|
expectedResult[customRulesMd] = [
|
||||||
{ "lineNumber": 12,
|
{ "lineNumber": 12,
|
||||||
"ruleName": "blockquote",
|
"ruleName": "any-blockquote",
|
||||||
"ruleAlias": "blockquote",
|
"ruleAlias": "any-blockquote",
|
||||||
"ruleDescription": "Rule that reports an error for blockquotes",
|
"ruleDescription": "Rule that reports an error for any blockquote",
|
||||||
"errorDetail": "Blockquote spans 1 line(s).",
|
"errorDetail": "Blockquote spans 1 line(s).",
|
||||||
"errorContext": "> Block",
|
"errorContext": "> Block",
|
||||||
"errorRange": null },
|
"errorRange": null },
|
||||||
|
|
@ -1662,6 +1641,13 @@ module.exports.customRulesV1 = function customRulesV1(test) {
|
||||||
"errorDetail": "Line number 12",
|
"errorDetail": "Line number 12",
|
||||||
"errorContext": null,
|
"errorContext": null,
|
||||||
"errorRange": null },
|
"errorRange": null },
|
||||||
|
{ "lineNumber": 1,
|
||||||
|
"ruleName": "first-line",
|
||||||
|
"ruleAlias": "first-line",
|
||||||
|
"ruleDescription": "Rule that reports an error for the first line",
|
||||||
|
"errorDetail": null,
|
||||||
|
"errorContext": null,
|
||||||
|
"errorRange": null },
|
||||||
{ "lineNumber": 3,
|
{ "lineNumber": 3,
|
||||||
"ruleName": "letters-E-X",
|
"ruleName": "letters-E-X",
|
||||||
"ruleAlias": "letter-E-letter-X",
|
"ruleAlias": "letter-E-letter-X",
|
||||||
|
|
@ -1682,8 +1668,8 @@ module.exports.customRulesV1 = function customRulesV1(test) {
|
||||||
test.deepEqual(actualResult, expectedResult, "Undetected issues.");
|
test.deepEqual(actualResult, expectedResult, "Undetected issues.");
|
||||||
var actualMessage = actualResult.toString();
|
var actualMessage = actualResult.toString();
|
||||||
var expectedMessage =
|
var expectedMessage =
|
||||||
"./test/custom-rules.md: 12: blockquote/blockquote" +
|
"./test/custom-rules.md: 12: any-blockquote/any-blockquote" +
|
||||||
" Rule that reports an error for blockquotes" +
|
" Rule that reports an error for any blockquote" +
|
||||||
" [Blockquote spans 1 line(s).] [Context: \"> Block\"]\n" +
|
" [Blockquote spans 1 line(s).] [Context: \"> Block\"]\n" +
|
||||||
"./test/custom-rules.md: 2: every-n-lines/every-n-lines" +
|
"./test/custom-rules.md: 2: every-n-lines/every-n-lines" +
|
||||||
" Rule that reports an error every N lines [Line number 2]\n" +
|
" Rule that reports an error every N lines [Line number 2]\n" +
|
||||||
|
|
@ -1695,6 +1681,8 @@ module.exports.customRulesV1 = function customRulesV1(test) {
|
||||||
" Rule that reports an error every N lines [Line number 10]\n" +
|
" Rule that reports an error every N lines [Line number 10]\n" +
|
||||||
"./test/custom-rules.md: 12: every-n-lines/every-n-lines" +
|
"./test/custom-rules.md: 12: every-n-lines/every-n-lines" +
|
||||||
" Rule that reports an error every N lines [Line number 12]\n" +
|
" Rule that reports an error every N lines [Line number 12]\n" +
|
||||||
|
"./test/custom-rules.md: 1: first-line/first-line" +
|
||||||
|
" Rule that reports an error for the first line\n" +
|
||||||
"./test/custom-rules.md: 3: letters-E-X/letter-E-letter-X" +
|
"./test/custom-rules.md: 3: letters-E-X/letter-E-letter-X" +
|
||||||
" Rule that reports an error for lines with the letters 'EX'" +
|
" Rule that reports an error for lines with the letters 'EX'" +
|
||||||
" [Context: \"text\"]\n" +
|
" [Context: \"text\"]\n" +
|
||||||
|
|
@ -1719,8 +1707,8 @@ module.exports.customRulesV2 = function customRulesV2(test) {
|
||||||
var expectedResult = {};
|
var expectedResult = {};
|
||||||
expectedResult[customRulesMd] = [
|
expectedResult[customRulesMd] = [
|
||||||
{ "lineNumber": 12,
|
{ "lineNumber": 12,
|
||||||
"ruleNames": [ "blockquote" ],
|
"ruleNames": [ "any-blockquote" ],
|
||||||
"ruleDescription": "Rule that reports an error for blockquotes",
|
"ruleDescription": "Rule that reports an error for any blockquote",
|
||||||
"errorDetail": "Blockquote spans 1 line(s).",
|
"errorDetail": "Blockquote spans 1 line(s).",
|
||||||
"errorContext": "> Block",
|
"errorContext": "> Block",
|
||||||
"errorRange": null },
|
"errorRange": null },
|
||||||
|
|
@ -1754,6 +1742,12 @@ module.exports.customRulesV2 = function customRulesV2(test) {
|
||||||
"errorDetail": "Line number 12",
|
"errorDetail": "Line number 12",
|
||||||
"errorContext": null,
|
"errorContext": null,
|
||||||
"errorRange": null },
|
"errorRange": null },
|
||||||
|
{ "lineNumber": 1,
|
||||||
|
"ruleNames": [ "first-line" ],
|
||||||
|
"ruleDescription": "Rule that reports an error for the first line",
|
||||||
|
"errorDetail": null,
|
||||||
|
"errorContext": null,
|
||||||
|
"errorRange": null },
|
||||||
{ "lineNumber": 3,
|
{ "lineNumber": 3,
|
||||||
"ruleNames": [ "letters-E-X", "letter-E-letter-X", "contains-ex" ],
|
"ruleNames": [ "letters-E-X", "letter-E-letter-X", "contains-ex" ],
|
||||||
"ruleDescription":
|
"ruleDescription":
|
||||||
|
|
@ -1772,8 +1766,8 @@ module.exports.customRulesV2 = function customRulesV2(test) {
|
||||||
test.deepEqual(actualResult, expectedResult, "Undetected issues.");
|
test.deepEqual(actualResult, expectedResult, "Undetected issues.");
|
||||||
var actualMessage = actualResult.toString();
|
var actualMessage = actualResult.toString();
|
||||||
var expectedMessage =
|
var expectedMessage =
|
||||||
"./test/custom-rules.md: 12: blockquote" +
|
"./test/custom-rules.md: 12: any-blockquote" +
|
||||||
" Rule that reports an error for blockquotes" +
|
" Rule that reports an error for any blockquote" +
|
||||||
" [Blockquote spans 1 line(s).] [Context: \"> Block\"]\n" +
|
" [Blockquote spans 1 line(s).] [Context: \"> Block\"]\n" +
|
||||||
"./test/custom-rules.md: 2: every-n-lines" +
|
"./test/custom-rules.md: 2: every-n-lines" +
|
||||||
" Rule that reports an error every N lines [Line number 2]\n" +
|
" Rule that reports an error every N lines [Line number 2]\n" +
|
||||||
|
|
@ -1785,6 +1779,8 @@ module.exports.customRulesV2 = function customRulesV2(test) {
|
||||||
" Rule that reports an error every N lines [Line number 10]\n" +
|
" Rule that reports an error every N lines [Line number 10]\n" +
|
||||||
"./test/custom-rules.md: 12: every-n-lines" +
|
"./test/custom-rules.md: 12: every-n-lines" +
|
||||||
" Rule that reports an error every N lines [Line number 12]\n" +
|
" Rule that reports an error every N lines [Line number 12]\n" +
|
||||||
|
"./test/custom-rules.md: 1: first-line" +
|
||||||
|
" Rule that reports an error for the first line\n" +
|
||||||
"./test/custom-rules.md: 3: letters-E-X/letter-E-letter-X/contains-ex" +
|
"./test/custom-rules.md: 3: letters-E-X/letter-E-letter-X/contains-ex" +
|
||||||
" Rule that reports an error for lines with the letters 'EX'" +
|
" Rule that reports an error for lines with the letters 'EX'" +
|
||||||
" [Context: \"text\"]\n" +
|
" [Context: \"text\"]\n" +
|
||||||
|
|
@ -1815,11 +1811,200 @@ module.exports.customRulesConfig = function customRulesConfig(test) {
|
||||||
test.ifError(err);
|
test.ifError(err);
|
||||||
var expectedResult = {};
|
var expectedResult = {};
|
||||||
expectedResult[customRulesMd] = {
|
expectedResult[customRulesMd] = {
|
||||||
"blockquote": [ 12 ],
|
"any-blockquote": [ 12 ],
|
||||||
"every-n-lines": [ 3, 6, 12 ],
|
"every-n-lines": [ 3, 6, 12 ],
|
||||||
|
"first-line": [ 1 ],
|
||||||
"letters-E-X": [ 7 ]
|
"letters-E-X": [ 7 ]
|
||||||
};
|
};
|
||||||
test.deepEqual(actualResult, expectedResult, "Undetected issues.");
|
test.deepEqual(actualResult, expectedResult, "Undetected issues.");
|
||||||
test.done();
|
test.done();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports.customRulesBadProperty = function customRulesBadProperty(test) {
|
||||||
|
test.expect(76);
|
||||||
|
[
|
||||||
|
[ "names", [ null, "string", [], [ null ], [ "" ], [ "string", 10 ] ] ],
|
||||||
|
[ "description", [ null, 10, "", [] ] ],
|
||||||
|
[ "tags", [ null, "string", [], [ null ], [ "" ], [ "string", 10 ] ] ],
|
||||||
|
[ "function", [ null, "string", [] ] ]
|
||||||
|
].forEach(function forProperty(property) {
|
||||||
|
var propertyName = property[0];
|
||||||
|
property[1].forEach(function forPropertyValue(propertyValue) {
|
||||||
|
var badRule = shared.clone(customRules.anyBlockquote);
|
||||||
|
badRule[propertyName] = propertyValue;
|
||||||
|
var options = {
|
||||||
|
"customRules": [ badRule ]
|
||||||
|
};
|
||||||
|
test.throws(function badRuleCall() {
|
||||||
|
markdownlint.sync(options);
|
||||||
|
}, function testError(err) {
|
||||||
|
test.ok(err, "Did not get an error for missing property.");
|
||||||
|
test.ok(err instanceof Error, "Error not instance of Error.");
|
||||||
|
test.equal(err.message,
|
||||||
|
"Property '" + propertyName +
|
||||||
|
"' of custom rule at index 0 is incorrect.",
|
||||||
|
"Incorrect message for missing property.");
|
||||||
|
return true;
|
||||||
|
}, "Did not get exception for missing property.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test.done();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.customRulesUsedNameName =
|
||||||
|
function customRulesUsedNameName(test) {
|
||||||
|
test.expect(4);
|
||||||
|
markdownlint({
|
||||||
|
"customRules": [
|
||||||
|
{
|
||||||
|
"names": [ "name", "NO-missing-SPACE-atx" ],
|
||||||
|
"description": "description",
|
||||||
|
"tags": [ "tag" ],
|
||||||
|
"function": function noop() {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, function callback(err, result) {
|
||||||
|
test.ok(err, "Did not get an error for duplicate name.");
|
||||||
|
test.ok(err instanceof Error, "Error not instance of Error.");
|
||||||
|
test.equal(err.message,
|
||||||
|
"Name 'NO-missing-SPACE-atx' of custom rule at index 0 is " +
|
||||||
|
"already used as a name or tag.",
|
||||||
|
"Incorrect message for duplicate name.");
|
||||||
|
test.ok(!result, "Got result for duplicate name.");
|
||||||
|
test.done();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.customRulesUsedNameTag =
|
||||||
|
function customRulesUsedNameTag(test) {
|
||||||
|
test.expect(4);
|
||||||
|
markdownlint({
|
||||||
|
"customRules": [
|
||||||
|
{
|
||||||
|
"names": [ "name", "HtMl" ],
|
||||||
|
"description": "description",
|
||||||
|
"tags": [ "tag" ],
|
||||||
|
"function": function noop() {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, function callback(err, result) {
|
||||||
|
test.ok(err, "Did not get an error for duplicate name.");
|
||||||
|
test.ok(err instanceof Error, "Error not instance of Error.");
|
||||||
|
test.equal(err.message,
|
||||||
|
"Name 'HtMl' of custom rule at index 0 is already used as a name or tag.",
|
||||||
|
"Incorrect message for duplicate name.");
|
||||||
|
test.ok(!result, "Got result for duplicate name.");
|
||||||
|
test.done();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.customRulesUsedTagName =
|
||||||
|
function customRulesUsedTagName(test) {
|
||||||
|
test.expect(4);
|
||||||
|
markdownlint({
|
||||||
|
"customRules": [
|
||||||
|
{
|
||||||
|
"names": [ "filler" ],
|
||||||
|
"description": "description",
|
||||||
|
"tags": [ "tag" ],
|
||||||
|
"function": function noop() {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [ "name" ],
|
||||||
|
"description": "description",
|
||||||
|
"tags": [ "tag", "NO-missing-SPACE-atx" ],
|
||||||
|
"function": function noop() {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, function callback(err, result) {
|
||||||
|
test.ok(err, "Did not get an error for duplicate tag.");
|
||||||
|
test.ok(err instanceof Error, "Error not instance of Error.");
|
||||||
|
test.equal(err.message,
|
||||||
|
"Tag 'NO-missing-SPACE-atx' of custom rule at index 1 is " +
|
||||||
|
"already used as a name.",
|
||||||
|
"Incorrect message for duplicate name.");
|
||||||
|
test.ok(!result, "Got result for duplicate tag.");
|
||||||
|
test.done();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.customRulesThrowForFile =
|
||||||
|
function customRulesThrowForFile(test) {
|
||||||
|
test.expect(4);
|
||||||
|
var exceptionMessage = "Test exception message";
|
||||||
|
markdownlint({
|
||||||
|
"customRules": [
|
||||||
|
{
|
||||||
|
"names": [ "name" ],
|
||||||
|
"description": "description",
|
||||||
|
"tags": [ "tag" ],
|
||||||
|
"function": function throws() {
|
||||||
|
throw new Error(exceptionMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"files": [ "./test/custom-rules.md" ]
|
||||||
|
}, function callback(err, result) {
|
||||||
|
test.ok(err, "Did not get an error for function thrown.");
|
||||||
|
test.ok(err instanceof Error, "Error not instance of Error.");
|
||||||
|
test.equal(err.message, exceptionMessage,
|
||||||
|
"Incorrect message for function thrown.");
|
||||||
|
test.ok(!result, "Got result for function thrown.");
|
||||||
|
test.done();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.customRulesThrowForFileSync =
|
||||||
|
function customRulesThrowForFileSync(test) {
|
||||||
|
test.expect(4);
|
||||||
|
var exceptionMessage = "Test exception message";
|
||||||
|
test.throws(function customRuleThrowsCall() {
|
||||||
|
markdownlint.sync({
|
||||||
|
"customRules": [
|
||||||
|
{
|
||||||
|
"names": [ "name" ],
|
||||||
|
"description": "description",
|
||||||
|
"tags": [ "tag" ],
|
||||||
|
"function": function throws() {
|
||||||
|
throw new Error(exceptionMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"files": [ "./test/custom-rules.md" ]
|
||||||
|
});
|
||||||
|
}, function testError(err) {
|
||||||
|
test.ok(err, "Did not get an error for function thrown.");
|
||||||
|
test.ok(err instanceof Error, "Error not instance of Error.");
|
||||||
|
test.equal(err.message, exceptionMessage,
|
||||||
|
"Incorrect message for function thrown.");
|
||||||
|
return true;
|
||||||
|
}, "Did not get exception for function thrown.");
|
||||||
|
test.done();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.customRulesThrowForString =
|
||||||
|
function customRulesThrowForString(test) {
|
||||||
|
test.expect(4);
|
||||||
|
var exceptionMessage = "Test exception message";
|
||||||
|
markdownlint({
|
||||||
|
"customRules": [
|
||||||
|
{
|
||||||
|
"names": [ "name" ],
|
||||||
|
"description": "description",
|
||||||
|
"tags": [ "tag" ],
|
||||||
|
"function": function throws() {
|
||||||
|
throw new Error(exceptionMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"strings": [ "String" ]
|
||||||
|
}, function callback(err, result) {
|
||||||
|
test.ok(err, "Did not get an error for function thrown.");
|
||||||
|
test.ok(err instanceof Error, "Error not instance of Error.");
|
||||||
|
test.equal(err.message, exceptionMessage,
|
||||||
|
"Incorrect message for function thrown.");
|
||||||
|
test.ok(!result, "Got result for function thrown.");
|
||||||
|
test.done();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"names": [ "blockquote" ],
|
"names": [ "any-blockquote" ],
|
||||||
"description": "Rule that reports an error for blockquotes",
|
"description": "Rule that reports an error for any blockquote",
|
||||||
"tags": [ "test" ],
|
"tags": [ "test" ],
|
||||||
"function": function rule(params, onError) {
|
"function": function rule(params, onError) {
|
||||||
params.tokens.filter(function filterToken(token) {
|
params.tokens.filter(function filterToken(token) {
|
||||||
18
test/rules/first-line.js
Normal file
18
test/rules/first-line.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
"names": [ "first-line" ],
|
||||||
|
"description": "Rule that reports an error for the first line",
|
||||||
|
"tags": [ "test" ],
|
||||||
|
"function": function rule(params, onError) {
|
||||||
|
// Unconditionally report an error for line 1
|
||||||
|
onError({
|
||||||
|
"lineNumber": 1,
|
||||||
|
"detail": null,
|
||||||
|
"context": null,
|
||||||
|
"range": null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -2,17 +2,21 @@
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var blockquote = require("./blockquote");
|
var anyBlockquote = require("./any-blockquote");
|
||||||
module.exports.blockquote = blockquote;
|
module.exports.anyBlockquote = anyBlockquote;
|
||||||
|
|
||||||
var everyNLines = require("./every-n-lines");
|
var everyNLines = require("./every-n-lines");
|
||||||
module.exports.everyNLines = everyNLines;
|
module.exports.everyNLines = everyNLines;
|
||||||
|
|
||||||
|
var firstLine = require("./first-line");
|
||||||
|
module.exports.firstLine = firstLine;
|
||||||
|
|
||||||
var lettersEX = require("./letters-E-X");
|
var lettersEX = require("./letters-E-X");
|
||||||
module.exports.lettersEX = lettersEX;
|
module.exports.lettersEX = lettersEX;
|
||||||
|
|
||||||
module.exports.all = [
|
module.exports.all = [
|
||||||
blockquote,
|
anyBlockquote,
|
||||||
everyNLines,
|
everyNLines,
|
||||||
|
firstLine,
|
||||||
lettersEX
|
lettersEX
|
||||||
];
|
];
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue