diff --git a/lib/markdownlint.js b/lib/markdownlint.js index 1afca735..036c0cfc 100644 --- a/lib/markdownlint.js +++ b/lib/markdownlint.js @@ -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; diff --git a/lib/shared.js b/lib/shared.js index 472a60e5..a33daf0c 100644 --- a/lib/shared.js +++ b/lib/shared.js @@ -46,6 +46,16 @@ module.exports.clone = function clone(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 // This preserves the line/column information for the rest of the document // Trailing whitespace is avoided with a '\' character in the last column diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index 05ee0601..dacc4ec7 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -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) { test.expect(108); var tagToRules = {}; @@ -1561,15 +1535,16 @@ module.exports.customRulesV0 = function customRulesV0(test) { test.ifError(err); var expectedResult = {}; expectedResult[customRulesMd] = { - "blockquote": [ 12 ], + "any-blockquote": [ 12 ], "every-n-lines": [ 2, 4, 6, 10, 12 ], + "first-line": [ 1 ], "letters-E-X": [ 3, 7 ] }; test.deepEqual(actualResult, expectedResult, "Undetected issues."); var actualMessage = actualResult.toString(); var expectedMessage = - "./test/custom-rules.md: 12: blockquote" + - " Rule that reports an error for blockquotes\n" + + "./test/custom-rules.md: 12: any-blockquote" + + " Rule that reports an error for any blockquote\n" + "./test/custom-rules.md: 2: every-n-lines" + " Rule that reports an error every N lines\n" + "./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" + "./test/custom-rules.md: 12: every-n-lines" + " 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" + " Rule that reports an error for lines with the letters 'EX'\n" + "./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)."); actualMessage = actualResult.toString(true); expectedMessage = - "./test/custom-rules.md: 12: blockquote" + - " Rule that reports an error for blockquotes\n" + + "./test/custom-rules.md: 12: any-blockquote" + + " Rule that reports an error for any blockquote\n" + "./test/custom-rules.md: 2: every-n-lines" + " Rule that reports an error every N lines\n" + "./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" + "./test/custom-rules.md: 12: every-n-lines" + " 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" + " Rule that reports an error for lines with the letters 'EX'\n" + "./test/custom-rules.md: 7: letter-E-letter-X" + @@ -1621,9 +1600,9 @@ module.exports.customRulesV1 = function customRulesV1(test) { var expectedResult = {}; expectedResult[customRulesMd] = [ { "lineNumber": 12, - "ruleName": "blockquote", - "ruleAlias": "blockquote", - "ruleDescription": "Rule that reports an error for blockquotes", + "ruleName": "any-blockquote", + "ruleAlias": "any-blockquote", + "ruleDescription": "Rule that reports an error for any blockquote", "errorDetail": "Blockquote spans 1 line(s).", "errorContext": "> Block", "errorRange": null }, @@ -1662,6 +1641,13 @@ module.exports.customRulesV1 = function customRulesV1(test) { "errorDetail": "Line number 12", "errorContext": 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, "ruleName": "letters-E-X", "ruleAlias": "letter-E-letter-X", @@ -1682,8 +1668,8 @@ module.exports.customRulesV1 = function customRulesV1(test) { test.deepEqual(actualResult, expectedResult, "Undetected issues."); var actualMessage = actualResult.toString(); var expectedMessage = - "./test/custom-rules.md: 12: blockquote/blockquote" + - " Rule that reports an error for blockquotes" + + "./test/custom-rules.md: 12: any-blockquote/any-blockquote" + + " Rule that reports an error for any blockquote" + " [Blockquote spans 1 line(s).] [Context: \"> Block\"]\n" + "./test/custom-rules.md: 2: every-n-lines/every-n-lines" + " 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" + "./test/custom-rules.md: 12: every-n-lines/every-n-lines" + " 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" + " Rule that reports an error for lines with the letters 'EX'" + " [Context: \"text\"]\n" + @@ -1719,8 +1707,8 @@ module.exports.customRulesV2 = function customRulesV2(test) { var expectedResult = {}; expectedResult[customRulesMd] = [ { "lineNumber": 12, - "ruleNames": [ "blockquote" ], - "ruleDescription": "Rule that reports an error for blockquotes", + "ruleNames": [ "any-blockquote" ], + "ruleDescription": "Rule that reports an error for any blockquote", "errorDetail": "Blockquote spans 1 line(s).", "errorContext": "> Block", "errorRange": null }, @@ -1754,6 +1742,12 @@ module.exports.customRulesV2 = function customRulesV2(test) { "errorDetail": "Line number 12", "errorContext": 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, "ruleNames": [ "letters-E-X", "letter-E-letter-X", "contains-ex" ], "ruleDescription": @@ -1772,8 +1766,8 @@ module.exports.customRulesV2 = function customRulesV2(test) { test.deepEqual(actualResult, expectedResult, "Undetected issues."); var actualMessage = actualResult.toString(); var expectedMessage = - "./test/custom-rules.md: 12: blockquote" + - " Rule that reports an error for blockquotes" + + "./test/custom-rules.md: 12: any-blockquote" + + " Rule that reports an error for any blockquote" + " [Blockquote spans 1 line(s).] [Context: \"> Block\"]\n" + "./test/custom-rules.md: 2: every-n-lines" + " 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" + "./test/custom-rules.md: 12: every-n-lines" + " 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" + " Rule that reports an error for lines with the letters 'EX'" + " [Context: \"text\"]\n" + @@ -1815,11 +1811,200 @@ module.exports.customRulesConfig = function customRulesConfig(test) { test.ifError(err); var expectedResult = {}; expectedResult[customRulesMd] = { - "blockquote": [ 12 ], + "any-blockquote": [ 12 ], "every-n-lines": [ 3, 6, 12 ], + "first-line": [ 1 ], "letters-E-X": [ 7 ] }; test.deepEqual(actualResult, expectedResult, "Undetected issues."); 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(); + }); +}; diff --git a/test/rules/blockquote.js b/test/rules/any-blockquote.js similarity index 84% rename from test/rules/blockquote.js rename to test/rules/any-blockquote.js index c95901ec..ed8c02c1 100644 --- a/test/rules/blockquote.js +++ b/test/rules/any-blockquote.js @@ -3,8 +3,8 @@ "use strict"; module.exports = { - "names": [ "blockquote" ], - "description": "Rule that reports an error for blockquotes", + "names": [ "any-blockquote" ], + "description": "Rule that reports an error for any blockquote", "tags": [ "test" ], "function": function rule(params, onError) { params.tokens.filter(function filterToken(token) { diff --git a/test/rules/first-line.js b/test/rules/first-line.js new file mode 100644 index 00000000..27e1aa99 --- /dev/null +++ b/test/rules/first-line.js @@ -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 + }); + } +}; diff --git a/test/rules/rules.js b/test/rules/rules.js index bbba7028..fa7a7ff0 100644 --- a/test/rules/rules.js +++ b/test/rules/rules.js @@ -2,17 +2,21 @@ "use strict"; -var blockquote = require("./blockquote"); -module.exports.blockquote = blockquote; +var anyBlockquote = require("./any-blockquote"); +module.exports.anyBlockquote = anyBlockquote; var everyNLines = require("./every-n-lines"); module.exports.everyNLines = everyNLines; +var firstLine = require("./first-line"); +module.exports.firstLine = firstLine; + var lettersEX = require("./letters-E-X"); module.exports.lettersEX = lettersEX; module.exports.all = [ - blockquote, + anyBlockquote, everyNLines, + firstLine, lettersEX ];