2017-12-15 22:55:51 -08:00
|
|
|
// @ts-check
|
|
|
|
|
2015-02-23 23:39:20 -08:00
|
|
|
"use strict";
|
|
|
|
|
2018-04-27 22:05:34 -07:00
|
|
|
const fs = require("fs");
|
|
|
|
const path = require("path");
|
|
|
|
const md = require("markdown-it")({ "html": true });
|
|
|
|
const rules = require("./rules");
|
|
|
|
const shared = require("./shared");
|
2015-02-23 23:39:20 -08:00
|
|
|
|
2018-02-25 16:04:13 -08:00
|
|
|
// Validates the list of rules for structure and reuse
|
|
|
|
function validateRuleList(ruleList) {
|
2018-04-27 22:05:34 -07:00
|
|
|
let result = null;
|
2018-02-25 16:04:13 -08:00
|
|
|
if (ruleList.length === rules.length) {
|
|
|
|
// No need to validate if only using built-in rules
|
|
|
|
return result;
|
|
|
|
}
|
2018-04-27 22:05:34 -07:00
|
|
|
const allIds = {};
|
2018-02-25 16:04:13 -08:00
|
|
|
ruleList.forEach(function forRule(rule, index) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const customIndex = index - rules.length;
|
2018-02-25 16:04:13 -08:00
|
|
|
function newError(property) {
|
|
|
|
return new Error(
|
|
|
|
"Property '" + property + "' of custom rule at index " +
|
|
|
|
customIndex + " is incorrect.");
|
|
|
|
}
|
|
|
|
[ "names", "tags" ].forEach(function forProperty(property) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const value = rule[property];
|
2018-02-25 16:04:13 -08:00
|
|
|
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) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const property = propertyInfo[0];
|
|
|
|
const value = rule[property];
|
2018-02-25 16:04:13 -08:00
|
|
|
if (!result && (!value || (typeof value !== propertyInfo[1]))) {
|
|
|
|
result = newError(property);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if (!result) {
|
|
|
|
rule.names.forEach(function forName(name) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const nameUpper = name.toUpperCase();
|
2018-02-25 16:04:13 -08:00
|
|
|
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) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const tagUpper = tag.toUpperCase();
|
2018-02-25 16:04:13 -08:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2015-03-15 23:39:17 -07:00
|
|
|
// Class for results with toString for pretty display
|
2018-02-25 16:04:13 -08:00
|
|
|
function newResults(ruleList) {
|
2018-02-15 21:35:58 -08:00
|
|
|
function Results() {}
|
|
|
|
Results.prototype.toString = function resultsToString(useAlias) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const that = this;
|
|
|
|
let ruleNameToRule = null;
|
|
|
|
const results = [];
|
2018-02-15 21:35:58 -08:00
|
|
|
Object.keys(that).forEach(function forFile(file) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const fileResults = that[file];
|
2018-02-15 21:35:58 -08:00
|
|
|
if (Array.isArray(fileResults)) {
|
|
|
|
fileResults.forEach(function forResult(result) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const ruleMoniker = result.ruleNames ?
|
2018-02-15 21:35:58 -08:00
|
|
|
result.ruleNames.join("/") :
|
|
|
|
(result.ruleName + "/" + result.ruleAlias);
|
|
|
|
results.push(
|
2016-10-16 21:46:02 -07:00
|
|
|
file + ": " +
|
2018-02-15 21:35:58 -08:00
|
|
|
result.lineNumber + ": " +
|
|
|
|
ruleMoniker + " " +
|
|
|
|
result.ruleDescription +
|
|
|
|
(result.errorDetail ?
|
|
|
|
" [" + result.errorDetail + "]" :
|
|
|
|
"") +
|
|
|
|
(result.errorContext ?
|
|
|
|
" [Context: \"" + result.errorContext + "\"]" :
|
|
|
|
""));
|
2016-10-16 21:46:02 -07:00
|
|
|
});
|
2018-02-15 21:35:58 -08:00
|
|
|
} else {
|
|
|
|
if (!ruleNameToRule) {
|
|
|
|
ruleNameToRule = {};
|
2018-02-25 16:04:13 -08:00
|
|
|
ruleList.forEach(function forRule(rule) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const ruleName = rule.names[0].toUpperCase();
|
2018-02-15 21:35:58 -08:00
|
|
|
ruleNameToRule[ruleName] = rule;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
Object.keys(fileResults).forEach(function forRule(ruleName) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const rule = ruleNameToRule[ruleName.toUpperCase()];
|
|
|
|
const ruleResults = fileResults[ruleName];
|
2018-02-15 21:35:58 -08:00
|
|
|
ruleResults.forEach(function forLine(lineNumber) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const nameIndex = Math.min(useAlias ? 1 : 0, rule.names.length - 1);
|
|
|
|
const result =
|
2018-02-15 21:35:58 -08:00
|
|
|
file + ": " +
|
|
|
|
lineNumber + ": " +
|
|
|
|
rule.names[nameIndex] + " " +
|
|
|
|
rule.description;
|
|
|
|
results.push(result);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return results.join("\n");
|
|
|
|
};
|
|
|
|
return new Results();
|
|
|
|
}
|
2015-03-13 09:13:07 -07:00
|
|
|
|
2018-02-05 21:26:07 -08:00
|
|
|
// Remove front matter (if present at beginning of content)
|
|
|
|
function removeFrontMatter(content, frontMatter) {
|
2018-04-27 22:05:34 -07:00
|
|
|
let frontMatterLines = [];
|
2015-07-25 22:18:30 -07:00
|
|
|
if (frontMatter) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const frontMatterMatch = content.match(frontMatter);
|
2015-07-25 22:18:30 -07:00
|
|
|
if (frontMatterMatch && !frontMatterMatch.index) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const contentMatched = frontMatterMatch[0];
|
2015-07-25 22:18:30 -07:00
|
|
|
content = content.slice(contentMatched.length);
|
2017-05-06 15:25:14 -07:00
|
|
|
frontMatterLines = contentMatched.split(shared.newLineRe);
|
|
|
|
if (frontMatterLines.length &&
|
|
|
|
(frontMatterLines[frontMatterLines.length - 1] === "")) {
|
|
|
|
frontMatterLines.length--;
|
|
|
|
}
|
2015-07-25 22:18:30 -07:00
|
|
|
}
|
2015-07-20 14:16:52 +02:00
|
|
|
}
|
2018-02-05 21:26:07 -08:00
|
|
|
return {
|
|
|
|
"content": content,
|
|
|
|
"frontMatterLines": frontMatterLines
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Annotate tokens with line/lineNumber
|
|
|
|
function annotateTokens(tokens, lines) {
|
2018-04-27 22:05:34 -07:00
|
|
|
let tbodyMap = null;
|
2015-04-29 18:46:52 -07:00
|
|
|
tokens.forEach(function forToken(token) {
|
2016-02-10 22:08:50 -08:00
|
|
|
// 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
|
2015-04-29 18:46:52 -07:00
|
|
|
if (token.map) {
|
|
|
|
token.line = lines[token.map[0]];
|
|
|
|
token.lineNumber = token.map[0] + 1;
|
|
|
|
// Trim bottom of token to exclude whitespace lines
|
2015-07-22 23:17:08 -07:00
|
|
|
while (token.map[1] && !(lines[token.map[1] - 1].trim())) {
|
2015-04-29 18:46:52 -07:00
|
|
|
token.map[1]--;
|
|
|
|
}
|
|
|
|
// Annotate children with lineNumber
|
2018-04-27 22:05:34 -07:00
|
|
|
let lineNumber = token.lineNumber;
|
2015-04-29 18:46:52 -07:00
|
|
|
(token.children || []).forEach(function forChild(child) {
|
|
|
|
child.lineNumber = lineNumber;
|
2018-01-16 21:27:38 -08:00
|
|
|
child.line = lines[lineNumber - 1];
|
2015-04-29 18:46:52 -07:00
|
|
|
if ((child.type === "softbreak") || (child.type === "hardbreak")) {
|
|
|
|
lineNumber++;
|
2015-03-16 23:25:06 -07:00
|
|
|
}
|
|
|
|
});
|
2015-04-29 18:46:52 -07:00
|
|
|
}
|
|
|
|
});
|
2018-02-05 21:26:07 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Map rule names/tags to canonical rule name
|
|
|
|
function mapAliasToRuleNames(ruleList) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const aliasToRuleNames = {};
|
|
|
|
// const tagToRuleNames = {};
|
2018-02-05 21:26:07 -08:00
|
|
|
ruleList.forEach(function forRule(rule) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const ruleName = rule.names[0].toUpperCase();
|
2018-02-05 21:26:07 -08:00
|
|
|
// The following is useful for updating README.md:
|
|
|
|
// console.log(
|
|
|
|
// "* **[" + ruleName + "](doc/Rules.md#" + ruleName.toLowerCase() +
|
2018-04-18 22:25:45 -07:00
|
|
|
// ")** *" + rule.names.slice(1).join("/") + "* - " + rule.description);
|
2018-02-05 21:26:07 -08:00
|
|
|
rule.names.forEach(function forName(name) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const nameUpper = name.toUpperCase();
|
2018-02-05 21:26:07 -08:00
|
|
|
aliasToRuleNames[nameUpper] = [ ruleName ];
|
|
|
|
});
|
|
|
|
rule.tags.forEach(function forTag(tag) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const tagUpper = tag.toUpperCase();
|
|
|
|
const ruleNames = aliasToRuleNames[tagUpper] || [];
|
2018-02-05 21:26:07 -08:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-02-15 21:35:58 -08:00
|
|
|
// Apply (and normalize) config
|
2018-02-25 16:04:13 -08:00
|
|
|
function getEffectiveConfig(ruleList, config, aliasToRuleNames) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const defaultKey = Object.keys(config).filter(function forKey(key) {
|
2015-09-21 23:21:17 -07:00
|
|
|
return key.toUpperCase() === "DEFAULT";
|
|
|
|
});
|
2018-04-27 22:05:34 -07:00
|
|
|
const ruleDefault = (defaultKey.length === 0) || !!config[defaultKey[0]];
|
|
|
|
const effectiveConfig = {};
|
2018-02-25 16:04:13 -08:00
|
|
|
ruleList.forEach(function forRule(rule) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const ruleName = rule.names[0].toUpperCase();
|
2018-02-15 21:35:58 -08:00
|
|
|
effectiveConfig[ruleName] = ruleDefault;
|
2015-04-29 18:46:52 -07:00
|
|
|
});
|
|
|
|
Object.keys(config).forEach(function forKey(key) {
|
2018-04-27 22:05:34 -07:00
|
|
|
let value = config[key];
|
2015-04-29 18:46:52 -07:00
|
|
|
if (value) {
|
|
|
|
if (!(value instanceof Object)) {
|
|
|
|
value = {};
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
value = false;
|
|
|
|
}
|
2018-04-27 22:05:34 -07:00
|
|
|
const keyUpper = key.toUpperCase();
|
2018-02-04 20:27:20 -08:00
|
|
|
(aliasToRuleNames[keyUpper] || []).forEach(function forRule(ruleName) {
|
2018-02-15 21:35:58 -08:00
|
|
|
effectiveConfig[ruleName] = value;
|
2018-02-04 20:27:20 -08:00
|
|
|
});
|
2015-04-29 18:46:52 -07:00
|
|
|
});
|
2018-02-15 21:35:58 -08:00
|
|
|
return effectiveConfig;
|
2018-02-05 21:26:07 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Create mapping of enabled rules per line
|
|
|
|
function getEnabledRulesPerLineNumber(
|
2018-02-25 16:04:13 -08:00
|
|
|
ruleList, lines, frontMatterLines, noInlineConfig,
|
2018-02-15 21:35:58 -08:00
|
|
|
effectiveConfig, aliasToRuleNames) {
|
2018-04-27 22:05:34 -07:00
|
|
|
let enabledRules = {};
|
|
|
|
const allRuleNames = [];
|
2018-02-25 16:04:13 -08:00
|
|
|
ruleList.forEach(function forRule(rule) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const ruleName = rule.names[0].toUpperCase();
|
2018-02-04 20:27:20 -08:00
|
|
|
allRuleNames.push(ruleName);
|
2018-02-15 21:35:58 -08:00
|
|
|
enabledRules[ruleName] = !!effectiveConfig[ruleName];
|
2015-09-26 16:55:33 -07:00
|
|
|
});
|
|
|
|
function forMatch(match) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const enabled = match[1].toUpperCase() === "EN";
|
|
|
|
const items = match[2] ?
|
2015-09-26 16:55:33 -07:00
|
|
|
match[2].trim().toUpperCase().split(/\s+/) :
|
|
|
|
allRuleNames;
|
|
|
|
items.forEach(function forItem(nameUpper) {
|
2018-02-04 20:27:20 -08:00
|
|
|
(aliasToRuleNames[nameUpper] || []).forEach(function forRule(ruleName) {
|
|
|
|
enabledRules[ruleName] = enabled;
|
|
|
|
});
|
2015-09-26 16:55:33 -07:00
|
|
|
});
|
|
|
|
}
|
2018-04-27 22:05:34 -07:00
|
|
|
const enabledRulesPerLineNumber = new Array(1 + frontMatterLines.length);
|
2015-09-26 16:55:33 -07:00
|
|
|
lines.forEach(function forLine(line) {
|
2017-05-21 22:58:10 -07:00
|
|
|
if (!noInlineConfig) {
|
2018-04-27 22:05:34 -07:00
|
|
|
let match = shared.inlineCommentRe.exec(line);
|
2017-05-21 22:58:10 -07:00
|
|
|
if (match) {
|
|
|
|
enabledRules = shared.clone(enabledRules);
|
|
|
|
while (match) {
|
|
|
|
forMatch(match);
|
|
|
|
match = shared.inlineCommentRe.exec(line);
|
|
|
|
}
|
2015-09-26 16:55:33 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
enabledRulesPerLineNumber.push(enabledRules);
|
|
|
|
});
|
2018-02-05 21:26:07 -08:00
|
|
|
return enabledRulesPerLineNumber;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Array.sort comparison for objects in errors array
|
|
|
|
function lineNumberComparison(a, b) {
|
|
|
|
return a.lineNumber - b.lineNumber;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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(
|
2018-02-25 16:04:13 -08:00
|
|
|
ruleList, content, config, frontMatter, noInlineConfig, resultVersion,
|
|
|
|
callback) {
|
2018-02-05 21:26:07 -08:00
|
|
|
// Remove UTF-8 byte order marker (if present)
|
2018-03-01 22:37:37 -08:00
|
|
|
content = content.replace(/^\ufeff/, "");
|
2018-02-05 21:26:07 -08:00
|
|
|
// Remove front matter
|
2018-04-27 22:05:34 -07:00
|
|
|
const removeFrontMatterResult = removeFrontMatter(content, frontMatter);
|
|
|
|
const frontMatterLines = removeFrontMatterResult.frontMatterLines;
|
2018-02-05 21:26:07 -08:00
|
|
|
// Ignore the content of HTML comments
|
2018-03-01 22:37:37 -08:00
|
|
|
content = shared.clearHtmlCommentText(removeFrontMatterResult.content);
|
2018-02-05 21:26:07 -08:00
|
|
|
// Parse content into tokens and lines
|
2018-04-27 22:05:34 -07:00
|
|
|
const tokens = md.parse(content, {});
|
|
|
|
const lines = content.split(shared.newLineRe);
|
2018-03-01 22:37:37 -08:00
|
|
|
annotateTokens(tokens, lines);
|
2018-04-27 22:05:34 -07:00
|
|
|
const aliasToRuleNames = mapAliasToRuleNames(ruleList);
|
|
|
|
const effectiveConfig =
|
|
|
|
getEffectiveConfig(ruleList, config, aliasToRuleNames);
|
|
|
|
const enabledRulesPerLineNumber = getEnabledRulesPerLineNumber(
|
2018-02-25 16:04:13 -08:00
|
|
|
ruleList, lines, frontMatterLines, noInlineConfig,
|
2018-02-15 21:35:58 -08:00
|
|
|
effectiveConfig, aliasToRuleNames);
|
2015-09-26 16:55:33 -07:00
|
|
|
// Create parameters for rules
|
2018-04-27 22:05:34 -07:00
|
|
|
const params = {
|
2015-09-26 16:55:33 -07:00
|
|
|
"tokens": tokens,
|
2017-05-06 15:25:14 -07:00
|
|
|
"lines": lines,
|
|
|
|
"frontMatterLines": frontMatterLines
|
2015-09-26 16:55:33 -07:00
|
|
|
};
|
2018-03-01 22:37:37 -08:00
|
|
|
shared.makeTokenCache(params);
|
2018-02-25 16:04:13 -08:00
|
|
|
// Function to run for each rule
|
2018-04-27 22:05:34 -07:00
|
|
|
const result = (resultVersion === 0) ? {} : [];
|
2018-02-25 16:04:13 -08:00
|
|
|
function forRule(rule) {
|
2015-09-26 16:55:33 -07:00
|
|
|
// Configure rule
|
2018-04-27 22:05:34 -07:00
|
|
|
const ruleNameFriendly = rule.names[0];
|
|
|
|
const ruleName = ruleNameFriendly.toUpperCase();
|
2018-02-15 21:35:58 -08:00
|
|
|
params.config = effectiveConfig[ruleName];
|
2018-02-27 21:14:02 -08:00
|
|
|
function throwError(property) {
|
|
|
|
throw new Error(
|
|
|
|
"Property '" + property + "' of onError parameter is incorrect.");
|
|
|
|
}
|
2018-04-27 22:05:34 -07:00
|
|
|
const errors = [];
|
2018-01-14 21:53:35 -08:00
|
|
|
function onError(errorInfo) {
|
2018-02-27 21:14:02 -08:00
|
|
|
if (!errorInfo ||
|
|
|
|
!errorInfo.lineNumber ||
|
|
|
|
!shared.isNumber(errorInfo.lineNumber)) {
|
|
|
|
throwError("lineNumber");
|
|
|
|
}
|
2018-03-17 22:11:01 -07:00
|
|
|
if (errorInfo.detail &&
|
|
|
|
!shared.isString(errorInfo.detail)) {
|
2018-02-27 21:14:02 -08:00
|
|
|
throwError("detail");
|
|
|
|
}
|
2018-03-17 22:11:01 -07:00
|
|
|
if (errorInfo.context &&
|
|
|
|
!shared.isString(errorInfo.context)) {
|
2018-02-27 21:14:02 -08:00
|
|
|
throwError("context");
|
|
|
|
}
|
|
|
|
if (errorInfo.range &&
|
|
|
|
(!Array.isArray(errorInfo.range) ||
|
|
|
|
(errorInfo.range.length !== 2) ||
|
|
|
|
!shared.isNumber(errorInfo.range[0]) ||
|
|
|
|
!shared.isNumber(errorInfo.range[1]))) {
|
|
|
|
throwError("range");
|
|
|
|
}
|
2016-10-16 21:46:02 -07:00
|
|
|
errors.push({
|
2018-01-14 21:53:35 -08:00
|
|
|
"lineNumber": errorInfo.lineNumber + frontMatterLines.length,
|
|
|
|
"detail": errorInfo.detail || null,
|
|
|
|
"context": errorInfo.context || null,
|
|
|
|
"range": errorInfo.range || null
|
2016-10-16 21:46:02 -07:00
|
|
|
});
|
|
|
|
}
|
2018-02-25 16:04:13 -08:00
|
|
|
// Call (possibly external) rule function
|
2018-01-18 21:34:30 -08:00
|
|
|
rule.function(params, onError);
|
2015-09-26 16:55:33 -07:00
|
|
|
// Record any errors (significant performance benefit from length check)
|
|
|
|
if (errors.length) {
|
2016-10-16 21:46:02 -07:00
|
|
|
errors.sort(lineNumberComparison);
|
2018-04-27 22:05:34 -07:00
|
|
|
const filteredErrors = errors
|
2016-10-16 21:46:02 -07:00
|
|
|
.filter(uniqueFilterForSortedErrors)
|
|
|
|
.filter(function removeDisabledRules(error) {
|
2018-01-12 23:21:06 -08:00
|
|
|
return enabledRulesPerLineNumber[error.lineNumber][ruleName];
|
2015-09-26 16:55:33 -07:00
|
|
|
})
|
2016-10-16 21:46:02 -07:00
|
|
|
.map(function formatResults(error) {
|
2017-07-05 21:53:21 -07:00
|
|
|
if (resultVersion === 0) {
|
|
|
|
return error.lineNumber;
|
|
|
|
}
|
2018-04-27 22:05:34 -07:00
|
|
|
const errorObject = {};
|
2018-01-12 23:21:06 -08:00
|
|
|
errorObject.lineNumber = error.lineNumber;
|
|
|
|
if (resultVersion === 1) {
|
2018-02-15 21:35:58 -08:00
|
|
|
errorObject.ruleName = ruleNameFriendly;
|
|
|
|
errorObject.ruleAlias = rule.names[1] || rule.names[0];
|
2018-01-12 23:21:06 -08:00
|
|
|
} else {
|
|
|
|
errorObject.ruleNames = rule.names;
|
|
|
|
}
|
2018-01-18 21:34:30 -08:00
|
|
|
errorObject.ruleDescription = rule.description;
|
2018-01-12 23:21:06 -08:00
|
|
|
errorObject.errorDetail = error.detail;
|
|
|
|
errorObject.errorContext = error.context;
|
2018-01-16 21:27:38 -08:00
|
|
|
errorObject.errorRange = error.range;
|
2018-01-12 23:21:06 -08:00
|
|
|
return errorObject;
|
2015-09-26 16:55:33 -07:00
|
|
|
});
|
|
|
|
if (filteredErrors.length) {
|
2017-07-05 21:53:21 -07:00
|
|
|
if (resultVersion === 0) {
|
2018-02-15 21:35:58 -08:00
|
|
|
result[ruleNameFriendly] = filteredErrors;
|
2017-07-05 21:53:21 -07:00
|
|
|
} else {
|
2018-02-15 21:35:58 -08:00
|
|
|
Array.prototype.push.apply(result, filteredErrors);
|
2016-10-16 21:46:02 -07:00
|
|
|
}
|
2015-04-29 18:46:52 -07:00
|
|
|
}
|
|
|
|
}
|
2018-02-25 16:04:13 -08:00
|
|
|
}
|
|
|
|
// Run all rules
|
|
|
|
try {
|
|
|
|
ruleList.forEach(forRule);
|
|
|
|
} catch (ex) {
|
2018-03-01 22:37:37 -08:00
|
|
|
shared.makeTokenCache(null);
|
2018-02-25 16:04:13 -08:00
|
|
|
return callback(ex);
|
|
|
|
}
|
2018-03-01 22:37:37 -08:00
|
|
|
shared.makeTokenCache(null);
|
2018-02-25 16:04:13 -08:00
|
|
|
callback(null, result);
|
2015-04-29 18:46:52 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Lints a single file
|
2016-10-16 21:46:02 -07:00
|
|
|
function lintFile(
|
2018-02-25 16:04:13 -08:00
|
|
|
ruleList,
|
2016-10-16 21:46:02 -07:00
|
|
|
file,
|
|
|
|
config,
|
|
|
|
frontMatter,
|
2017-05-21 22:58:10 -07:00
|
|
|
noInlineConfig,
|
2016-10-16 21:46:02 -07:00
|
|
|
resultVersion,
|
|
|
|
synchronous,
|
|
|
|
callback) {
|
2015-04-29 18:46:52 -07:00
|
|
|
function lintContentWrapper(err, content) {
|
|
|
|
if (err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
2018-02-25 16:04:13 -08:00
|
|
|
lintContent(ruleList, content, config, frontMatter, noInlineConfig,
|
|
|
|
resultVersion, callback);
|
2015-03-20 00:09:55 -07:00
|
|
|
}
|
|
|
|
// Make a/synchronous call to read file
|
|
|
|
if (synchronous) {
|
2015-04-29 18:46:52 -07:00
|
|
|
lintContentWrapper(null, fs.readFileSync(file, shared.utf8Encoding));
|
2015-03-20 00:09:55 -07:00
|
|
|
} else {
|
2015-04-29 18:46:52 -07:00
|
|
|
fs.readFile(file, shared.utf8Encoding, lintContentWrapper);
|
2015-03-20 00:09:55 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-15 22:55:51 -08:00
|
|
|
function lintInput(options, synchronous, callback) {
|
2015-03-15 23:39:17 -07:00
|
|
|
// Normalize inputs
|
2015-02-27 22:06:54 -08:00
|
|
|
options = options || {};
|
2015-03-11 21:13:21 -07:00
|
|
|
callback = callback || function noop() {};
|
2018-04-27 22:05:34 -07:00
|
|
|
const ruleList = rules.concat(options.customRules || []);
|
|
|
|
const ruleErr = validateRuleList(ruleList);
|
2018-02-25 16:04:13 -08:00
|
|
|
if (ruleErr) {
|
|
|
|
return callback(ruleErr);
|
|
|
|
}
|
2018-04-27 22:05:34 -07:00
|
|
|
let files = [];
|
2016-01-15 22:00:34 -08:00
|
|
|
if (Array.isArray(options.files)) {
|
|
|
|
files = options.files.slice();
|
|
|
|
} else if (options.files) {
|
|
|
|
files = [ String(options.files) ];
|
|
|
|
}
|
2018-04-27 22:05:34 -07:00
|
|
|
const strings = options.strings || {};
|
|
|
|
const stringsKeys = Object.keys(strings);
|
|
|
|
const config = options.config || { "default": true };
|
|
|
|
const frontMatter = (options.frontMatter === undefined) ?
|
2017-07-17 22:23:00 -07:00
|
|
|
shared.frontMatterRe : options.frontMatter;
|
2018-04-27 22:05:34 -07:00
|
|
|
const noInlineConfig = !!options.noInlineConfig;
|
|
|
|
const resultVersion = (options.resultVersion === undefined) ?
|
2018-01-12 23:21:06 -08:00
|
|
|
2 : options.resultVersion;
|
2018-04-27 22:05:34 -07:00
|
|
|
const results = newResults(ruleList);
|
2018-03-19 21:21:22 -07:00
|
|
|
// Helper to lint the next string or file
|
|
|
|
function lintNextItem() {
|
2018-04-27 22:05:34 -07:00
|
|
|
let iterating = true;
|
|
|
|
let item = null;
|
2018-03-19 21:21:22 -07:00
|
|
|
function lintNextItemCallback(err, result) {
|
|
|
|
if (err) {
|
|
|
|
iterating = false;
|
|
|
|
return callback(err);
|
|
|
|
}
|
2018-02-25 16:04:13 -08:00
|
|
|
results[item] = result;
|
2018-03-19 21:21:22 -07:00
|
|
|
if (!iterating) {
|
|
|
|
lintNextItem();
|
|
|
|
}
|
2018-02-25 16:04:13 -08:00
|
|
|
}
|
2018-03-19 21:21:22 -07:00
|
|
|
while (iterating) {
|
|
|
|
if ((item = stringsKeys.shift())) {
|
|
|
|
lintContent(
|
|
|
|
ruleList,
|
|
|
|
strings[item] || "",
|
|
|
|
config,
|
|
|
|
frontMatter,
|
|
|
|
noInlineConfig,
|
|
|
|
resultVersion,
|
|
|
|
lintNextItemCallback);
|
|
|
|
} else if ((item = files.shift())) {
|
|
|
|
iterating = synchronous;
|
|
|
|
lintFile(
|
|
|
|
ruleList,
|
|
|
|
item,
|
|
|
|
config,
|
|
|
|
frontMatter,
|
|
|
|
noInlineConfig,
|
|
|
|
resultVersion,
|
|
|
|
synchronous,
|
|
|
|
lintNextItemCallback);
|
|
|
|
} else {
|
|
|
|
return callback(null, results);
|
|
|
|
}
|
2015-02-24 18:40:37 -08:00
|
|
|
}
|
|
|
|
}
|
2018-02-25 16:04:13 -08:00
|
|
|
lintNextItem();
|
2017-12-15 22:55:51 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Lint specified Markdown files.
|
|
|
|
*
|
|
|
|
* @param {Object} options Configuration options.
|
|
|
|
* @param {Function} callback Callback (err, result) function.
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
function markdownlint(options, callback) {
|
|
|
|
return lintInput(options, false, callback);
|
2015-03-20 00:09:55 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-05-19 22:36:46 -07:00
|
|
|
* Lint specified Markdown files synchronously.
|
2015-03-20 00:09:55 -07:00
|
|
|
*
|
|
|
|
* @param {Object} options Configuration options.
|
|
|
|
* @returns {Object} Result object.
|
|
|
|
*/
|
|
|
|
function markdownlintSync(options) {
|
2018-04-27 22:05:34 -07:00
|
|
|
let results = null;
|
2017-12-15 22:55:51 -08:00
|
|
|
lintInput(options, true, function callback(error, res) {
|
2018-02-25 16:04:13 -08:00
|
|
|
if (error) {
|
|
|
|
throw error;
|
|
|
|
}
|
2017-12-15 22:55:51 -08:00
|
|
|
results = res;
|
|
|
|
});
|
|
|
|
return results;
|
2015-03-20 00:09:55 -07:00
|
|
|
}
|
|
|
|
|
2017-05-19 22:36:46 -07:00
|
|
|
/**
|
|
|
|
* Read specified configuration file.
|
|
|
|
*
|
|
|
|
* @param {String} file Configuration file name/path.
|
|
|
|
* @param {Function} callback Callback (err, result) function.
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
function readConfig(file, callback) {
|
|
|
|
// Read file
|
|
|
|
fs.readFile(file, shared.utf8Encoding, function handleFile(err, content) {
|
|
|
|
if (err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
// Parse file
|
2018-04-27 22:05:34 -07:00
|
|
|
let config = null;
|
2017-05-19 22:36:46 -07:00
|
|
|
try {
|
|
|
|
config = JSON.parse(content);
|
|
|
|
} catch (ex) {
|
|
|
|
return callback(ex);
|
|
|
|
}
|
|
|
|
if (config.extends) {
|
|
|
|
// Extend configuration
|
2018-04-27 22:05:34 -07:00
|
|
|
const extendsFile = path.resolve(path.dirname(file), config.extends);
|
2017-05-19 22:36:46 -07:00
|
|
|
readConfig(extendsFile, function handleConfig(errr, extendsConfig) {
|
|
|
|
if (errr) {
|
|
|
|
return callback(errr);
|
|
|
|
}
|
|
|
|
delete config.extends;
|
|
|
|
callback(null, shared.assign(extendsConfig, config));
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
callback(null, config);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Read specified configuration file synchronously.
|
|
|
|
*
|
|
|
|
* @param {String} file Configuration file name/path.
|
|
|
|
* @returns {Object} Configuration object.
|
|
|
|
*/
|
|
|
|
function readConfigSync(file) {
|
|
|
|
// Parse file
|
2018-04-27 22:05:34 -07:00
|
|
|
let config = JSON.parse(fs.readFileSync(file, shared.utf8Encoding));
|
2017-05-19 22:36:46 -07:00
|
|
|
if (config.extends) {
|
|
|
|
// Extend configuration
|
|
|
|
config = shared.assign(
|
|
|
|
readConfigSync(path.resolve(path.dirname(file), config.extends)),
|
|
|
|
config);
|
|
|
|
delete config.extends;
|
|
|
|
}
|
|
|
|
return config;
|
|
|
|
}
|
|
|
|
|
2015-03-20 00:09:55 -07:00
|
|
|
// Export a/synchronous APIs
|
2017-12-15 22:55:51 -08:00
|
|
|
markdownlint.sync = markdownlintSync;
|
|
|
|
markdownlint.readConfig = readConfig;
|
|
|
|
markdownlint.readConfigSync = readConfigSync;
|
2015-03-20 00:09:55 -07:00
|
|
|
module.exports = markdownlint;
|