2015-02-23 23:39:20 -08:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
var fs = require("fs");
|
2015-04-13 08:47:15 -07:00
|
|
|
var md = require("markdown-it")({ "html": true });
|
2015-02-23 23:39:20 -08:00
|
|
|
var rules = require("./rules");
|
2015-03-08 23:08:43 -07:00
|
|
|
var shared = require("./shared");
|
2015-02-23 23:39:20 -08:00
|
|
|
|
2015-03-16 22:31:18 -07:00
|
|
|
// Mappings from rule to description and tag to rules
|
2015-09-26 16:55:33 -07:00
|
|
|
var allRuleNames = [];
|
2016-01-12 21:29:17 -08:00
|
|
|
var ruleNameToRule = {};
|
|
|
|
var idUpperToRuleNames = {};
|
2015-03-13 09:13:07 -07:00
|
|
|
rules.forEach(function forRule(rule) {
|
2015-09-26 16:55:33 -07:00
|
|
|
allRuleNames.push(rule.name);
|
2016-01-12 21:29:17 -08:00
|
|
|
ruleNameToRule[rule.name] = rule;
|
2015-03-14 22:34:28 -07:00
|
|
|
// The following is useful for updating README.md
|
2016-01-12 21:29:17 -08:00
|
|
|
// console.log("* **" + rule.name + "** *" +
|
|
|
|
// rule.aliases.join(", ") + "* - " + rule.desc);
|
2015-03-16 22:31:18 -07:00
|
|
|
rule.tags.forEach(function forTag(tag) {
|
2015-09-21 23:21:17 -07:00
|
|
|
var tagUpper = tag.toUpperCase();
|
2016-01-12 21:29:17 -08:00
|
|
|
var ruleNames = idUpperToRuleNames[tagUpper] || [];
|
|
|
|
ruleNames.push(rule.name);
|
|
|
|
idUpperToRuleNames[tagUpper] = ruleNames;
|
|
|
|
});
|
|
|
|
rule.aliases.forEach(function forAlias(alias) {
|
|
|
|
var aliasUpper = alias.toUpperCase();
|
|
|
|
idUpperToRuleNames[aliasUpper] = [ rule.name ];
|
2015-03-16 22:31:18 -07:00
|
|
|
});
|
2015-03-13 09:13:07 -07:00
|
|
|
});
|
2015-03-16 22:31:18 -07:00
|
|
|
// The following is useful for updating README.md
|
2016-01-12 21:29:17 -08:00
|
|
|
// Object.keys(idUpperToRuleNames).sort().forEach(function forTag(tag) {
|
|
|
|
// console.log("* **" + tag + "** - " + idUpperToRuleNames[tag].join(", "));
|
2015-03-16 22:31:18 -07:00
|
|
|
// });
|
2015-03-13 09:13:07 -07:00
|
|
|
|
2015-03-15 23:39:17 -07:00
|
|
|
// Class for results with toString for pretty display
|
2015-03-13 09:13:07 -07:00
|
|
|
function Results() { }
|
2016-01-12 21:29:17 -08:00
|
|
|
Results.prototype.toString = function resultsToString(useAlias) {
|
2015-09-26 22:22:22 -07:00
|
|
|
var that = this;
|
2015-03-13 09:13:07 -07:00
|
|
|
var results = [];
|
2015-09-26 22:22:22 -07:00
|
|
|
Object.keys(that).forEach(function forFile(file) {
|
|
|
|
var fileResults = that[file];
|
2016-01-12 21:29:17 -08:00
|
|
|
Object.keys(fileResults).forEach(function forRule(ruleName) {
|
|
|
|
var rule = ruleNameToRule[ruleName];
|
|
|
|
var ruleResults = fileResults[ruleName];
|
2015-03-13 09:13:07 -07:00
|
|
|
ruleResults.forEach(function forLine(lineNumber) {
|
|
|
|
var result =
|
2016-01-12 21:29:17 -08:00
|
|
|
file + ": " +
|
|
|
|
lineNumber + ": " +
|
|
|
|
(useAlias ? rule.aliases[0] : rule.name) + " " +
|
|
|
|
rule.desc;
|
2015-03-13 09:13:07 -07:00
|
|
|
results.push(result);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
return results.join("\n");
|
|
|
|
};
|
|
|
|
|
2015-03-15 23:39:17 -07:00
|
|
|
// Array.sort comparison for number objects
|
2015-02-24 23:35:34 -08:00
|
|
|
function numberComparison(a, b) {
|
|
|
|
return a - b;
|
|
|
|
}
|
|
|
|
|
2015-03-15 23:39:17 -07:00
|
|
|
// Function to return unique values from a sorted array
|
2015-02-24 23:35:34 -08:00
|
|
|
function uniqueFilterForSorted(value, index, array) {
|
|
|
|
return (index === 0) || (value > array[index - 1]);
|
|
|
|
}
|
|
|
|
|
2015-04-29 18:46:52 -07:00
|
|
|
// Lints a single string
|
2015-09-26 16:55:33 -07:00
|
|
|
function lintContent(content, config, frontMatter) { // eslint-disable-line
|
2015-07-25 22:18:30 -07:00
|
|
|
// Remove front matter (if present at beginning of content)
|
|
|
|
var frontMatterLines = 0;
|
|
|
|
if (frontMatter) {
|
2015-09-26 16:55:33 -07:00
|
|
|
var frontMatterMatch = content.match(frontMatter);
|
2015-07-25 22:18:30 -07:00
|
|
|
if (frontMatterMatch && !frontMatterMatch.index) {
|
|
|
|
var contentMatched = frontMatterMatch[0];
|
|
|
|
content = content.slice(contentMatched.length);
|
|
|
|
frontMatterLines = contentMatched.split(shared.newLineRe).length - 1;
|
|
|
|
}
|
2015-07-20 14:16:52 +02:00
|
|
|
}
|
2015-04-29 18:46:52 -07:00
|
|
|
// Parse content into tokens and lines
|
|
|
|
var tokens = md.parse(content, {});
|
|
|
|
var lines = content.split(shared.newLineRe);
|
2015-06-12 09:37:11 -07:00
|
|
|
var tokenLists = {};
|
2015-04-29 18:46:52 -07:00
|
|
|
// Annotate tokens with line/lineNumber
|
|
|
|
tokens.forEach(function forToken(token) {
|
|
|
|
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
|
|
|
|
var lineNumber = token.lineNumber;
|
|
|
|
(token.children || []).forEach(function forChild(child) {
|
|
|
|
child.lineNumber = lineNumber;
|
|
|
|
if ((child.type === "softbreak") || (child.type === "hardbreak")) {
|
|
|
|
lineNumber++;
|
2015-03-16 23:25:06 -07:00
|
|
|
}
|
|
|
|
});
|
2015-04-29 18:46:52 -07:00
|
|
|
}
|
2015-06-12 09:37:11 -07:00
|
|
|
if (!tokenLists[token.type]) {
|
|
|
|
tokenLists[token.type] = [];
|
|
|
|
}
|
|
|
|
tokenLists[token.type].push(token);
|
2015-04-29 18:46:52 -07:00
|
|
|
});
|
|
|
|
// Merge rules/tags and sanitize config
|
2015-09-21 23:21:17 -07:00
|
|
|
var defaultKey = Object.keys(config).filter(function forKey(key) {
|
|
|
|
return key.toUpperCase() === "DEFAULT";
|
|
|
|
});
|
|
|
|
var ruleDefault = (defaultKey.length === 0) || !!config[defaultKey[0]];
|
2015-04-29 18:46:52 -07:00
|
|
|
var mergedRules = {};
|
|
|
|
rules.forEach(function forRule(rule) {
|
|
|
|
mergedRules[rule.name] = ruleDefault;
|
|
|
|
});
|
|
|
|
Object.keys(config).forEach(function forKey(key) {
|
|
|
|
var value = config[key];
|
|
|
|
if (value) {
|
|
|
|
if (!(value instanceof Object)) {
|
|
|
|
value = {};
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
value = false;
|
|
|
|
}
|
2015-09-21 23:21:17 -07:00
|
|
|
var keyUpper = key.toUpperCase();
|
2016-01-12 21:29:17 -08:00
|
|
|
if (ruleNameToRule[keyUpper]) {
|
2015-09-21 23:21:17 -07:00
|
|
|
mergedRules[keyUpper] = value;
|
2016-01-12 21:29:17 -08:00
|
|
|
} else if (idUpperToRuleNames[keyUpper]) {
|
|
|
|
idUpperToRuleNames[keyUpper].forEach(function forRule(ruleName) {
|
2015-09-21 23:21:17 -07:00
|
|
|
mergedRules[ruleName] = value;
|
2015-02-24 18:40:37 -08:00
|
|
|
});
|
2015-02-23 23:39:20 -08:00
|
|
|
}
|
2015-04-29 18:46:52 -07:00
|
|
|
});
|
2015-09-26 16:55:33 -07:00
|
|
|
// Create mapping of enabled rules per line
|
|
|
|
var enabledRules = {};
|
|
|
|
rules.forEach(function forRule(rule) {
|
|
|
|
enabledRules[rule.name] = !!mergedRules[rule.name];
|
|
|
|
});
|
|
|
|
function forMatch(match) {
|
|
|
|
var enabled = match[1].toUpperCase() === "EN";
|
|
|
|
var items = match[2] ?
|
|
|
|
match[2].trim().toUpperCase().split(/\s+/) :
|
|
|
|
allRuleNames;
|
|
|
|
items.forEach(function forItem(nameUpper) {
|
2016-01-12 21:29:17 -08:00
|
|
|
if (ruleNameToRule[nameUpper]) {
|
2015-09-26 16:55:33 -07:00
|
|
|
enabledRules[nameUpper] = enabled;
|
2016-01-12 21:29:17 -08:00
|
|
|
} else if (idUpperToRuleNames[nameUpper]) {
|
|
|
|
idUpperToRuleNames[nameUpper].forEach(function forRule(ruleName) {
|
2015-09-26 16:55:33 -07:00
|
|
|
enabledRules[ruleName] = enabled;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
var enabledRulesPerLineNumber = [ null ];
|
|
|
|
lines.forEach(function forLine(line) {
|
|
|
|
var match = shared.inlineCommentRe.exec(line);
|
|
|
|
if (match) {
|
|
|
|
enabledRules = shared.clone(enabledRules);
|
|
|
|
while (match) {
|
|
|
|
forMatch(match);
|
|
|
|
match = shared.inlineCommentRe.exec(line);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
enabledRulesPerLineNumber.push(enabledRules);
|
|
|
|
});
|
|
|
|
// Create parameters for rules
|
|
|
|
var params = {
|
|
|
|
"tokens": tokens,
|
|
|
|
"tokenLists": tokenLists,
|
|
|
|
"lines": lines
|
|
|
|
};
|
|
|
|
// Run each rule
|
2015-04-29 18:46:52 -07:00
|
|
|
var result = {};
|
|
|
|
rules.forEach(function forRule(rule) {
|
2015-09-26 16:55:33 -07:00
|
|
|
// Configure rule
|
|
|
|
params.options = mergedRules[rule.name];
|
|
|
|
var errors = [];
|
|
|
|
rule.func(params, errors);
|
|
|
|
// Record any errors (significant performance benefit from length check)
|
|
|
|
if (errors.length) {
|
|
|
|
errors.sort(numberComparison);
|
|
|
|
var filteredErrors = errors
|
|
|
|
.filter(uniqueFilterForSorted)
|
|
|
|
.filter(function removeDisabledRules(lineNumber) {
|
|
|
|
return enabledRulesPerLineNumber[lineNumber][rule.name];
|
|
|
|
})
|
|
|
|
.map(function adjustLineNumbers(error) {
|
|
|
|
return error + frontMatterLines;
|
|
|
|
});
|
|
|
|
if (filteredErrors.length) {
|
|
|
|
result[rule.name] = filteredErrors;
|
2015-04-29 18:46:52 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Lints a single file
|
2015-07-25 22:18:30 -07:00
|
|
|
function lintFile(file, config, frontMatter, synchronous, callback) {
|
2015-04-29 18:46:52 -07:00
|
|
|
function lintContentWrapper(err, content) {
|
|
|
|
if (err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
2015-07-25 22:18:30 -07:00
|
|
|
var result = lintContent(content, config, frontMatter);
|
2015-04-29 18:46:52 -07:00
|
|
|
callback(null, result);
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Callback used as a sentinel by markdownlintSync
|
|
|
|
function markdownlintSynchronousCallback() {
|
|
|
|
// Unreachable; no code path in the synchronous case passes err
|
|
|
|
// if (err) {
|
|
|
|
// throw err; // Synchronous APIs throw
|
|
|
|
// }
|
2015-02-23 23:39:20 -08:00
|
|
|
}
|
|
|
|
|
2015-03-15 23:39:17 -07:00
|
|
|
/**
|
|
|
|
* Lint specified Markdown files according to configurable rules.
|
|
|
|
*
|
|
|
|
* @param {Object} options Configuration options.
|
2015-03-20 00:09:55 -07:00
|
|
|
* @param {Function} callback Callback (err, result) function.
|
2015-03-15 23:39:17 -07:00
|
|
|
* @returns {void}
|
|
|
|
*/
|
2015-03-20 00:09:55 -07:00
|
|
|
function markdownlint(options, 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() {};
|
2015-03-13 18:20:56 -07:00
|
|
|
var files = (options.files || []).slice();
|
2015-04-29 18:46:52 -07:00
|
|
|
var strings = options.strings || {};
|
2015-07-25 22:18:30 -07:00
|
|
|
var frontMatter = (options.frontMatter === undefined) ?
|
|
|
|
shared.frontMatterRe : options.frontMatter;
|
2015-03-02 22:45:43 -08:00
|
|
|
var config = options.config || { "default": true };
|
2015-03-20 00:09:55 -07:00
|
|
|
var synchronous = (callback === markdownlintSynchronousCallback);
|
2015-03-13 09:13:07 -07:00
|
|
|
var results = new Results();
|
2015-04-29 18:46:52 -07:00
|
|
|
// Helper to lint the next file in the array
|
|
|
|
function lintFilesArray() {
|
2015-02-24 18:40:37 -08:00
|
|
|
var file = files.shift();
|
|
|
|
if (file) {
|
2015-07-25 22:18:30 -07:00
|
|
|
lintFile(file, config, frontMatter, synchronous,
|
|
|
|
function lintedFile(err, result) {
|
|
|
|
if (err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
// Record errors and lint next file
|
|
|
|
results[file] = result;
|
|
|
|
lintFilesArray();
|
|
|
|
});
|
2015-02-24 18:40:37 -08:00
|
|
|
} else {
|
|
|
|
callback(null, results);
|
|
|
|
}
|
|
|
|
}
|
2015-04-29 18:46:52 -07:00
|
|
|
// Lint strings
|
|
|
|
Object.keys(strings).forEach(function forKey(key) {
|
2015-07-25 22:18:30 -07:00
|
|
|
var result = lintContent(strings[key] || "", config, frontMatter);
|
2015-04-29 18:46:52 -07:00
|
|
|
results[key] = result;
|
|
|
|
});
|
|
|
|
// Lint files
|
|
|
|
lintFilesArray();
|
|
|
|
// Return results
|
2015-03-20 00:09:55 -07:00
|
|
|
if (synchronous) {
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Lint specified Markdown files according to configurable rules.
|
|
|
|
*
|
|
|
|
* @param {Object} options Configuration options.
|
|
|
|
* @returns {Object} Result object.
|
|
|
|
*/
|
|
|
|
function markdownlintSync(options) {
|
|
|
|
return markdownlint(options, markdownlintSynchronousCallback);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Export a/synchronous APIs
|
|
|
|
module.exports = markdownlint;
|
|
|
|
module.exports.sync = markdownlintSync;
|