diff --git a/README.md b/README.md index 78f8f51b..5c37b165 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,25 @@ responsibility. Example: `[ "one.md", "dir/two.md" ]` +#### options.strings + +Type: `Object` mapping `String` to `String` + +Map of identifiers to strings for linting. + +When Markdown content is not available as files, it can be passed as strings. +The keys of the `strings` object are used to identify each input value in the +`result` summary. + +Example: + +```json +{ + "readme": "# README\n...", + "changelog": "# CHANGELOG\n..." +} +``` + #### options.config Type: `Object` mapping `String` to `Boolean | Object` @@ -205,7 +224,11 @@ Invoke `markdownlint` and use the `result` object's `toString` method: var markdownlint = require("markdownlint"); var options = { - "files": [ "good.md", "bad.md" ] + "files": [ "good.md", "bad.md" ], + "strings": { + "good.string": "# good.string\n\nThis string passes all rules.", + "bad.string": "#bad.string\n\n#This string fails\tsome rules." + } }; markdownlint(options, function callback(err, result) { @@ -225,6 +248,9 @@ console.log(result.toString()); Output of both calls: ```text +bad.string: 3: MD010 Hard tabs +bad.string: 1: MD018 No space after hash on atx style header +bad.string: 3: MD018 No space after hash on atx style header bad.md: 3: MD010 Hard tabs bad.md: 1: MD018 No space after hash on atx style header bad.md: 3: MD018 No space after hash on atx style header @@ -244,6 +270,11 @@ Output: ```json { + "good.string": {}, + "bad.string": { + "MD010": [ 3 ], + "MD018": [ 1, 3 ] + }, "good.md": {}, "bad.md": { "MD010": [ 3 ], @@ -331,6 +362,7 @@ bad.md: 3: MD018 No space after hash on atx style header * 0.0.2 - Improve documentation, tests, and code. * 0.0.3 - Add synchronous API, improve documentation and code. * 0.0.4 - Add tests MD033-MD040, update dependencies. +* *PENDING* - Add `strings` option to enable file-less scenarios. [npm-image]: https://img.shields.io/npm/v/markdownlint.svg [npm-url]: https://www.npmjs.com/package/markdownlint diff --git a/example/standalone.js b/example/standalone.js index 5d5b9202..f283bb20 100644 --- a/example/standalone.js +++ b/example/standalone.js @@ -3,7 +3,11 @@ var markdownlint = require("../lib/markdownlint"); var options = { - "files": [ "good.md", "bad.md" ] + "files": [ "good.md", "bad.md" ], + "strings": { + "good.string": "# good.string\n\nThis string passes all rules.", + "bad.string": "#bad.string\n\n#This string fails\tsome rules." + } }; // Uses result.toString for pretty formatting diff --git a/lib/markdownlint.js b/lib/markdownlint.js index 4b70793d..97e8ba88 100644 --- a/lib/markdownlint.js +++ b/lib/markdownlint.js @@ -53,86 +53,90 @@ function uniqueFilterForSorted(value, index, array) { return (index === 0) || (value > array[index - 1]); } +// Lints a single string +function lintContent(content, config) { + // Parse content into tokens and lines + var tokens = md.parse(content, {}); + var lines = content.split(shared.newLineRe); + // 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 + while (!(lines[token.map[1] - 1].trim())) { + 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++; + } + }); + } + }); + // Create parameters for rules + var params = { + "tokens": tokens, + "lines": lines + }; + // Merge rules/tags and sanitize config + var mergedRules = {}; + var ruleDefault = (config.default === undefined) || !!config.default; + 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; + } + if (ruleToDescription[key]) { + mergedRules[key] = value; + } else if (tagToRules[key]) { + tagToRules[key].forEach(function forRule(rule) { + mergedRules[rule] = value; + }); + } + }); + // Run each enabled rule + var result = {}; + rules.forEach(function forRule(rule) { + if (mergedRules[rule.name]) { + // Configure rule + params.options = mergedRules[rule.name]; + var errors = []; + rule.func(params, errors); + // Record any errors + if (errors.length) { + errors.sort(numberComparison); + result[rule.name] = errors.filter(uniqueFilterForSorted); + } + } + }); + return result; +} + // Lints a single file function lintFile(file, config, synchronous, callback) { - // Callback for read file API - function readFile(err, contents) { + function lintContentWrapper(err, content) { if (err) { - callback(err); - } else { - // Parse file into tokens and lines - var tokens = md.parse(contents, {}); - var lines = contents.split(shared.newLineRe); - // 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 - while (!(lines[token.map[1] - 1].trim())) { - 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++; - } - }); - } - }); - // Create parameters for rules - var params = { - "tokens": tokens, - "lines": lines - }; - // Merge rules/tags and sanitize config - var mergedRules = {}; - var ruleDefault = (config.default === undefined) || !!config.default; - 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; - } - if (ruleToDescription[key]) { - mergedRules[key] = value; - } else if (tagToRules[key]) { - tagToRules[key].forEach(function forRule(rule) { - mergedRules[rule] = value; - }); - } - }); - // Run each enabled rule - var result = {}; - rules.forEach(function forRule(rule) { - if (mergedRules[rule.name]) { - // Configure rule - params.options = mergedRules[rule.name]; - var errors = []; - rule.func(params, errors); - // Record any errors - if (errors.length) { - errors.sort(numberComparison); - result[rule.name] = errors.filter(uniqueFilterForSorted); - } - } - }); - callback(null, result); + return callback(err); } + var result = lintContent(content, config); + callback(null, result); } // Make a/synchronous call to read file if (synchronous) { - readFile(null, fs.readFileSync(file, shared.utf8Encoding)); + lintContentWrapper(null, fs.readFileSync(file, shared.utf8Encoding)); } else { - fs.readFile(file, shared.utf8Encoding, readFile); + fs.readFile(file, shared.utf8Encoding, lintContentWrapper); } } @@ -156,27 +160,34 @@ function markdownlint(options, callback) { options = options || {}; callback = callback || function noop() {}; var files = (options.files || []).slice(); + var strings = options.strings || {}; var config = options.config || { "default": true }; var synchronous = (callback === markdownlintSynchronousCallback); var results = new Results(); - // Lint each input file - function lintFiles() { + // Helper to lint the next file in the array + function lintFilesArray() { var file = files.shift(); if (file) { lintFile(file, config, synchronous, function lintedFile(err, result) { if (err) { - callback(err); - } else { - // Record errors and lint next file - results[file] = result; - lintFiles(); + return callback(err); } + // Record errors and lint next file + results[file] = result; + lintFilesArray(); }); } else { callback(null, results); } } - lintFiles(); + // Lint strings + Object.keys(strings).forEach(function forKey(key) { + var result = lintContent(strings[key] || "", config); + results[key] = result; + }); + // Lint files + lintFilesArray(); + // Return results if (synchronous) { return results; } diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index a611df03..3c202e67 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -158,6 +158,27 @@ module.exports.resultFormattingSync = function resultFormattingSync(test) { test.done(); }; +module.exports.stringInputLineEndings = function stringInputLineEndings(test) { + test.expect(2); + var options = { + "strings": { + "lf": "One\nTwo\n#Three", + "crlf": "One\r\nTwo\r\n#Three", + "mixed": "One\r\nTwo\n#Three" + } + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + var expectedResult = { + "lf": { "MD018": [ 3 ] }, + "crlf": { "MD018": [ 3 ] }, + "mixed": { "MD018": [ 3 ] } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.done(); + }); +}; + module.exports.defaultTrue = function defaultTrue(test) { test.expect(2); var options = { @@ -442,7 +463,7 @@ module.exports.styleRelaxed = function styleRelaxed(test) { }); }; -module.exports.filesNotModified = function filesNotModified(test) { +module.exports.filesArrayNotModified = function filesArrayNotModified(test) { test.expect(2); var files = [ "./test/atx_header_spacing.md", @@ -460,16 +481,16 @@ module.exports.missingOptions = function missingOptions(test) { test.expect(2); markdownlint(null, function callback(err, result) { test.ifError(err); - test.ok(result, "Did not get result for missing options."); + test.deepEqual(result, {}, "Did not get empty result for missing options."); test.done(); }); }; -module.exports.missingFiles = function missingFiles(test) { +module.exports.missingFilesAndStrings = function missingFilesAndStrings(test) { test.expect(2); markdownlint({}, function callback(err, result) { test.ifError(err); - test.ok(result, "Did not get result for missing files."); + test.ok(result, "Did not get result for missing files/strings."); test.done(); }); }; @@ -494,12 +515,13 @@ module.exports.badFile = function badFile(test) { }; module.exports.badFileSync = function badFileSync(test) { - test.expect(3); + test.expect(4); test.throws(function badFileCall() { markdownlint.sync({ "files": [ "./badFile" ] }); }, function testError(err) { + test.ok(err, "Did not get an error for bad file."); test.ok(err instanceof Error, "Error not instance of Error."); test.equal(err.code, "ENOENT", "Error code for bad file not ENOENT."); return true; @@ -507,6 +529,26 @@ module.exports.badFileSync = function badFileSync(test) { test.done(); }; +module.exports.missingStringValue = function missingStringValue(test) { + test.expect(2); + markdownlint({ + "strings": { + "undefined": undefined, + "null": null, + "empty": "" + } + }, function callback(err, result) { + test.ifError(err); + var expectedResult = { + "undefined": {}, + "null": {}, + "empty": {} + }; + test.deepEqual(result, expectedResult, "Did not get empty results."); + test.done(); + }); +}; + module.exports.readme = function readme(test) { test.expect(95); var tagToRules = {};