diff --git a/test/markdownlint-test-custom-rules.js b/test/markdownlint-test-custom-rules.js new file mode 100644 index 00000000..3598159d --- /dev/null +++ b/test/markdownlint-test-custom-rules.js @@ -0,0 +1,4087 @@ +// @ts-check + +"use strict"; + +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const { promisify } = require("util"); +const md = require("markdown-it")(); +const pluginInline = require("markdown-it-for-inline"); +const pluginKatex = require("@iktakahiro/markdown-it-katex"); +const pluginSub = require("markdown-it-sub"); +const pluginSup = require("markdown-it-sup"); +const tape = require("tape"); +require("tape-player"); +const tv4 = require("tv4"); +const packageJson = require("../package.json"); +const markdownlint = require("../lib/markdownlint"); +const helpers = require("../helpers"); +const rules = require("../lib/rules"); +const customRules = require("./rules/rules.js"); +const defaultConfig = require("./markdownlint-test-default-config.json"); +const configSchema = require("../schema/markdownlint-config-schema.json"); +const homepage = packageJson.homepage; +const version = packageJson.version; + +const deprecatedRuleNames = new Set([ "MD002", "MD006" ]); + +/** + * Create a test function for the specified test file. + * + * @param {string} file Test file relative path. + * @returns {Function} Test function. + */ +function createTestForFile(file) { + const markdownlintPromise = promisify(markdownlint); + return function testForFile(test) { + const detailedResults = /[/\\]detailed-results-/.test(file); + test.plan(detailedResults ? 3 : 2); + const resultsFile = file.replace(/\.md$/, ".results.json"); + const fixedFile = file.replace(/\.md$/, ".md.fixed"); + const configFile = file.replace(/\.md$/, ".json"); + let mergedConfig = null; + const actualPromise = fs.promises.stat(configFile) + .then( + function configFileExists() { + return fs.promises.readFile(configFile, helpers.utf8Encoding) + .then(JSON.parse); + }, + function noConfigFile() { + return {}; + }) + .then( + function lintWithConfig(config) { + mergedConfig = { + ...defaultConfig, + ...config + }; + return markdownlintPromise({ + "files": [ file ], + "config": mergedConfig, + "resultVersion": detailedResults ? 2 : 3 + }); + }) + .then( + function diffFixedFiles(resultVersion2or3) { + return detailedResults ? + Promise.all([ + markdownlintPromise({ + "files": [ file ], + "config": mergedConfig, + "resultVersion": 3 + }), + fs.promises.readFile(file, helpers.utf8Encoding), + fs.promises.readFile(fixedFile, helpers.utf8Encoding) + ]) + .then(function validateApplyFixes(fulfillments) { + const [ resultVersion3, content, expected ] = fulfillments; + const errors = resultVersion3[file]; + const actual = helpers.applyFixes(content, errors); + // Uncomment the following line to update *.md.fixed files + // fs.writeFileSync(fixedFile, actual, helpers.utf8Encoding); + test.equal(actual, expected, + "Unexpected output from applyFixes."); + return resultVersion2or3; + }) : + resultVersion2or3; + } + ) + .then( + function convertResultVersion2To0(resultVersion2or3) { + const result0 = {}; + const result2or3 = resultVersion2or3[file]; + result2or3.forEach(function forResult(result) { + const ruleName = result.ruleNames[0]; + const lineNumbers = result0[ruleName] || []; + if (!lineNumbers.includes(result.lineNumber)) { + lineNumbers.push(result.lineNumber); + } + result0[ruleName] = lineNumbers; + }); + return [ result0, result2or3 ]; + } + ); + const expectedPromise = detailedResults ? + fs.promises.readFile(resultsFile, helpers.utf8Encoding) + .then( + function fileContents(contents) { + // @ts-ignore + const errorObjects = JSON.parse(contents); + errorObjects.forEach(function forObject(errorObject) { + if (errorObject.ruleInformation) { + errorObject.ruleInformation = + errorObject.ruleInformation.replace("v0.0.0", `v${version}`); + } + }); + return errorObjects; + }) : + fs.promises.readFile(file, helpers.utf8Encoding) + .then( + function fileContents(contents) { + // @ts-ignore + const lines = contents.split(helpers.newLineRe); + const results = {}; + lines.forEach(function forLine(line, lineNum) { + const regex = /\{(MD\d+)(?::(\d+))?\}/g; + let match = null; + while ((match = regex.exec(line))) { + const rule = match[1]; + const errors = results[rule] || []; + errors.push( + match[2] ? + Number.parseInt(match[2], 10) : + lineNum + 1 + ); + results[rule] = errors; + } + }); + const sortedResults = {}; + Object.keys(results).sort().forEach(function forKey(key) { + sortedResults[key] = results[key]; + }); + return sortedResults; + }); + Promise.all([ actualPromise, expectedPromise ]) + .then( + function compareResults(fulfillments) { + const [ [ actual0, actual2or3 ], expected ] = fulfillments; + const actual = detailedResults ? actual2or3 : actual0; + test.deepEqual(actual, expected, "Line numbers are not correct."); + return actual2or3; + }) + .then( + function verifyFixes(errors) { + if (detailedResults) { + return test.ok(true); + } + return fs.promises.readFile(file, helpers.utf8Encoding) + .then( + function applyFixes(content) { + const corrections = helpers.applyFixes(content, errors); + return markdownlintPromise({ + "strings": { + "input": corrections + }, + "config": mergedConfig, + "resultVersion": 3 + }); + }) + .then( + function checkFixes(newErrors) { + const unfixed = newErrors.input + .filter((error) => !!error.fixInfo); + test.deepEqual(unfixed, [], "Fixable error was not fixed."); + } + ); + }) + .catch() + .then(test.done); + }; +} + +fs.readdirSync("./test") + .filter((file) => /\.md$/.test(file)) + // @ts-ignore + .forEach((file) => tape(file, createTestForFile(path.join("./test", file)))); + +tape("projectFilesNoInlineConfig", (test) => { + test.plan(2); + const options = { + "files": [ + "README.md", + "CONTRIBUTING.md", + "doc/CustomRules.md", + "helpers/README.md" + ], + "noInlineConfig": true, + "config": { + "line-length": { "line_length": 150 }, + "no-duplicate-heading": false + } + }; + markdownlint(options, function callback(err, actual) { + test.ifError(err); + const expected = { + "README.md": [], + "CONTRIBUTING.md": [], + "doc/CustomRules.md": [], + "helpers/README.md": [] + }; + test.deepEqual(actual, expected, "Issue(s) with project files."); + test.end(); + }); +}); + +tape("projectFilesInlineConfig", (test) => { + test.plan(2); + const options = { + "files": [ "doc/Rules.md" ], + "config": { + "line-length": { "line_length": 150 }, + "no-inline-html": false + } + }; + markdownlint(options, function callback(err, actual) { + test.ifError(err); + const expected = { + "doc/Rules.md": [] + }; + test.deepEqual(actual, expected, "Issue(s) with project files."); + test.end(); + }); +}); + +tape("resultObjectToStringNotEnumerable", (test) => { + test.plan(2); + const options = { + "strings": { + "string": "# Heading" + } + }; + markdownlint(options, function callback(err, result) { + test.ifError(err); + // eslint-disable-next-line guard-for-in + for (const property in result) { + test.notEqual(property, "toString", "Function should not enumerate."); + } + test.end(); + }); +}); + +tape("resultFormattingV0", (test) => { + test.plan(4); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": true, + "MD041": false + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD002": [ 3 ], + "MD018": [ 1 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": { + "MD002": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + let actualMessage = actualResult.toString(); + let expectedMessage = + "./test/atx_heading_spacing.md: 3: MD002" + + " First heading should be a top level heading\n" + + "./test/atx_heading_spacing.md: 1: MD018" + + " No space after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 3: MD019" + + " Multiple spaces after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 5: MD019" + + " Multiple spaces after hash on atx style heading\n" + + "./test/first_heading_bad_atx.md: 1: MD002" + + " First heading should be a top level heading"; + test.equal(actualMessage, expectedMessage, "Incorrect message (name)."); + // @ts-ignore + actualMessage = actualResult.toString(true); + expectedMessage = + "./test/atx_heading_spacing.md: 3: first-heading-h1" + + " First heading should be a top level heading\n" + + "./test/atx_heading_spacing.md: 1: no-missing-space-atx" + + " No space after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 3: no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 5: no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading\n" + + "./test/first_heading_bad_atx.md: 1: first-heading-h1" + + " First heading should be a top level heading"; + test.equal(actualMessage, expectedMessage, "Incorrect message (alias)."); + test.end(); + }); +}); + +tape("resultFormattingSyncV0", (test) => { + test.plan(3); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": true, + "MD041": false + }, + "resultVersion": 0 + }; + const actualResult = markdownlint.sync(options); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD002": [ 3 ], + "MD018": [ 1 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": { + "MD002": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + let actualMessage = actualResult.toString(); + let expectedMessage = + "./test/atx_heading_spacing.md: 3: MD002" + + " First heading should be a top level heading\n" + + "./test/atx_heading_spacing.md: 1: MD018" + + " No space after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 3: MD019" + + " Multiple spaces after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 5: MD019" + + " Multiple spaces after hash on atx style heading\n" + + "./test/first_heading_bad_atx.md: 1: MD002" + + " First heading should be a top level heading"; + test.equal(actualMessage, expectedMessage, "Incorrect message (name)."); + // @ts-ignore + actualMessage = actualResult.toString(true); + expectedMessage = + "./test/atx_heading_spacing.md: 3: first-heading-h1" + + " First heading should be a top level heading\n" + + "./test/atx_heading_spacing.md: 1: no-missing-space-atx" + + " No space after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 3: no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 5: no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading\n" + + "./test/first_heading_bad_atx.md: 1: first-heading-h1" + + " First heading should be a top level heading"; + test.equal(actualMessage, expectedMessage, "Incorrect message (alias)."); + test.end(); +}); + +tape("resultFormattingV1", (test) => { + test.plan(3); + const options = { + "strings": { + "truncate": + "# Multiple spaces inside hashes on closed atx style heading #\n" + }, + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": true, + "MD041": false + }, + "resultVersion": 1 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "truncate": [ + { "lineNumber": 1, + "ruleName": "MD021", + "ruleAlias": "no-multiple-space-closed-atx", + "ruleDescription": + "Multiple spaces inside hashes on closed atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md021`, + "errorDetail": null, + "errorContext": "# Multiple spa...tyle heading #", + "errorRange": [ 1, 4 ] } + ], + "./test/atx_heading_spacing.md": [ + { "lineNumber": 3, + "ruleName": "MD002", + "ruleAlias": "first-heading-h1", + "ruleDescription": "First heading should be a top level heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md002`, + "errorDetail": "Expected: h1; Actual: h2", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 1, + "ruleName": "MD018", + "ruleAlias": "no-missing-space-atx", + "ruleDescription": "No space after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md018`, + "errorDetail": null, + "errorContext": "#Heading 1 {MD018}", + "errorRange": [ 1, 2 ] }, + { "lineNumber": 3, + "ruleName": "MD019", + "ruleAlias": "no-multiple-space-atx", + "ruleDescription": "Multiple spaces after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md019`, + "errorDetail": null, + "errorContext": "## Heading 2 {MD019}", + "errorRange": [ 1, 5 ] }, + { "lineNumber": 5, + "ruleName": "MD019", + "ruleAlias": "no-multiple-space-atx", + "ruleDescription": "Multiple spaces after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md019`, + "errorDetail": null, + "errorContext": "## Heading 3 {MD019}", + "errorRange": [ 1, 6 ] } + ], + "./test/first_heading_bad_atx.md": [ + { "lineNumber": 1, + "ruleName": "MD002", + "ruleAlias": "first-heading-h1", + "ruleDescription": "First heading should be a top level heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md002`, + "errorDetail": "Expected: h1; Actual: h2", + "errorContext": null, + "errorRange": null } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "./test/atx_heading_spacing.md: 3: MD002/first-heading-h1" + + " First heading should be a top level heading" + + " [Expected: h1; Actual: h2]\n" + + "./test/atx_heading_spacing.md: 1: MD018/no-missing-space-atx" + + " No space after hash on atx style heading" + + " [Context: \"#Heading 1 {MD018}\"]\n" + + "./test/atx_heading_spacing.md: 3: MD019/no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading" + + " [Context: \"## Heading 2 {MD019}\"]\n" + + "./test/atx_heading_spacing.md: 5: MD019/no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading" + + " [Context: \"## Heading 3 {MD019}\"]\n" + + "./test/first_heading_bad_atx.md: 1: MD002/first-heading-h1" + + " First heading should be a top level heading" + + " [Expected: h1; Actual: h2]\n" + + "truncate: 1: MD021/no-multiple-space-closed-atx" + + " Multiple spaces inside hashes on closed atx style heading" + + " [Context: \"# Multiple spa...tyle heading #\"]"; + test.equal(actualMessage, expectedMessage, "Incorrect message."); + test.end(); + }); +}); + +tape("resultFormattingV2", (test) => { + test.plan(3); + const options = { + "strings": { + "truncate": + "# Multiple spaces inside hashes on closed atx style heading #\n" + }, + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": true, + "MD041": false + } + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "truncate": [ + { "lineNumber": 1, + "ruleNames": [ "MD021", "no-multiple-space-closed-atx" ], + "ruleDescription": + "Multiple spaces inside hashes on closed atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md021`, + "errorDetail": null, + "errorContext": "# Multiple spa...tyle heading #", + "errorRange": [ 1, 4 ] } + ], + "./test/atx_heading_spacing.md": [ + { "lineNumber": 3, + "ruleNames": [ "MD002", "first-heading-h1", "first-header-h1" ], + "ruleDescription": "First heading should be a top level heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md002`, + "errorDetail": "Expected: h1; Actual: h2", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 1, + "ruleNames": [ "MD018", "no-missing-space-atx" ], + "ruleDescription": "No space after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md018`, + "errorDetail": null, + "errorContext": "#Heading 1 {MD018}", + "errorRange": [ 1, 2 ] }, + { "lineNumber": 3, + "ruleNames": [ "MD019", "no-multiple-space-atx" ], + "ruleDescription": "Multiple spaces after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md019`, + "errorDetail": null, + "errorContext": "## Heading 2 {MD019}", + "errorRange": [ 1, 5 ] }, + { "lineNumber": 5, + "ruleNames": [ "MD019", "no-multiple-space-atx" ], + "ruleDescription": "Multiple spaces after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md019`, + "errorDetail": null, + "errorContext": "## Heading 3 {MD019}", + "errorRange": [ 1, 6 ] } + ], + "./test/first_heading_bad_atx.md": [ + { "lineNumber": 1, + "ruleNames": [ "MD002", "first-heading-h1", "first-header-h1" ], + "ruleDescription": "First heading should be a top level heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md002`, + "errorDetail": "Expected: h1; Actual: h2", + "errorContext": null, + "errorRange": null } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "./test/atx_heading_spacing.md: 3:" + + " MD002/first-heading-h1/first-header-h1" + + " First heading should be a top level heading" + + " [Expected: h1; Actual: h2]\n" + + "./test/atx_heading_spacing.md: 1: MD018/no-missing-space-atx" + + " No space after hash on atx style heading" + + " [Context: \"#Heading 1 {MD018}\"]\n" + + "./test/atx_heading_spacing.md: 3: MD019/no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading" + + " [Context: \"## Heading 2 {MD019}\"]\n" + + "./test/atx_heading_spacing.md: 5: MD019/no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading" + + " [Context: \"## Heading 3 {MD019}\"]\n" + + "./test/first_heading_bad_atx.md: 1:" + + " MD002/first-heading-h1/first-header-h1" + + " First heading should be a top level heading" + + " [Expected: h1; Actual: h2]\n" + + "truncate: 1: MD021/no-multiple-space-closed-atx" + + " Multiple spaces inside hashes on closed atx style heading" + + " [Context: \"# Multiple spa...tyle heading #\"]"; + test.equal(actualMessage, expectedMessage, "Incorrect message."); + test.end(); + }); +}); + +tape("resultFormattingV3", (test) => { + test.plan(3); + const options = { + "strings": { + "input": + "# Heading \n" + + "\n" + + "Text\ttext\t\ttext\n" + + "Text * emphasis * text" + }, + "resultVersion": 3 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { + "lineNumber": 1, + "ruleNames": [ "MD009", "no-trailing-spaces" ], + "ruleDescription": "Trailing spaces", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md009`, + "errorDetail": "Expected: 0 or 2; Actual: 3", + "errorContext": null, + "errorRange": [ 10, 3 ], + "fixInfo": { + "editColumn": 10, + "deleteCount": 3 + } + }, + { + "lineNumber": 3, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 5", + "errorContext": null, + "errorRange": [ 5, 1 ], + "fixInfo": { + "editColumn": 5, + "deleteCount": 1, + "insertText": " " + } + }, + { + "lineNumber": 3, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 10", + "errorContext": null, + "errorRange": [ 10, 2 ], + "fixInfo": { + "editColumn": 10, + "deleteCount": 2, + "insertText": " " + } + }, + { + "lineNumber": 4, + "ruleNames": [ "MD037", "no-space-in-emphasis" ], + "ruleDescription": "Spaces inside emphasis markers", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md037`, + "errorDetail": null, + "errorContext": "* emphasis *", + "errorRange": [ 6, 12 ], + "fixInfo": { + "editColumn": 6, + "deleteCount": 12, + "insertText": "*emphasis*" + } + }, + { + "lineNumber": 4, + "ruleNames": [ "MD047", "single-trailing-newline" ], + "ruleDescription": "Files should end with a single newline character", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md047`, + "errorDetail": null, + "errorContext": null, + "errorRange": [ 22, 1 ], + "fixInfo": { + "insertText": "\n", + "editColumn": 23 + } + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "input: 1: MD009/no-trailing-spaces" + + " Trailing spaces [Expected: 0 or 2; Actual: 3]\n" + + "input: 3: MD010/no-hard-tabs" + + " Hard tabs [Column: 5]\n" + + "input: 3: MD010/no-hard-tabs" + + " Hard tabs [Column: 10]\n" + + "input: 4: MD037/no-space-in-emphasis" + + " Spaces inside emphasis markers [Context: \"* emphasis *\"]\n" + + "input: 4: MD047/single-trailing-newline" + + " Files should end with a single newline character"; + test.equal(actualMessage, expectedMessage, "Incorrect message."); + test.end(); + }); +}); + +tape("onePerLineResultVersion0", (test) => { + test.plan(2); + const options = { + "strings": { + "input": "# Heading\theading\t\theading\n" + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": { + "MD010": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("onePerLineResultVersion1", (test) => { + test.plan(2); + const options = { + "strings": { + "input": "# Heading\theading\t\theading\n" + }, + "resultVersion": 1 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { + "lineNumber": 1, + "ruleName": "MD010", + "ruleAlias": "no-hard-tabs", + "ruleDescription": "Hard tabs", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 10", + "errorContext": null, + "errorRange": [ 10, 1 ] + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("onePerLineResultVersion2", (test) => { + test.plan(2); + const options = { + "strings": { + "input": "# Heading\theading\t\theading\n" + }, + "resultVersion": 2 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { + "lineNumber": 1, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 10", + "errorContext": null, + "errorRange": [ 10, 1 ] + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("manyPerLineResultVersion3", (test) => { + test.plan(2); + const options = { + "strings": { + "input": "# Heading\theading\t\theading\n" + }, + "resultVersion": 3 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { + "lineNumber": 1, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 10", + "errorContext": null, + "errorRange": [ 10, 1 ], + "fixInfo": { + "editColumn": 10, + "deleteCount": 1, + "insertText": " " + } + }, + { + "lineNumber": 1, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 18", + "errorContext": null, + "errorRange": [ 18, 2 ], + "fixInfo": { + "editColumn": 18, + "deleteCount": 2, + "insertText": " " + } + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("frontMatterResultVersion3", (test) => { + test.plan(2); + const options = { + "strings": { + "input": "---\n---\n# Heading\nText\n" + }, + "resultVersion": 3 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { + "lineNumber": 3, + "ruleNames": + [ "MD022", "blanks-around-headings", "blanks-around-headers" ], + "ruleDescription": "Headings should be surrounded by blank lines", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md022`, + "errorDetail": "Expected: 1; Actual: 0; Below", + "errorContext": "# Heading", + "errorRange": null, + "fixInfo": { + "lineNumber": 4, + "insertText": "\n" + } + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("stringInputLineEndings", (test) => { + test.plan(2); + const options = { + "strings": { + "cr": "One\rTwo\r#Three\n", + "lf": "One\nTwo\n#Three\n", + "crlf": "One\r\nTwo\r\n#Three\n", + "mixed": "One\rTwo\n#Three\n" + }, + "config": defaultConfig, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "cr": { "MD018": [ 3 ] }, + "lf": { "MD018": [ 3 ] }, + "crlf": { "MD018": [ 3 ] }, + "mixed": { "MD018": [ 3 ] } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("inputOnlyNewline", (test) => { + test.plan(2); + const options = { + "strings": { + "cr": "\r", + "lf": "\n", + "crlf": "\r\n" + }, + "config": { + "default": false + } + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "cr": [], + "lf": [], + "crlf": [] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("defaultTrue", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "default": true + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD018": [ 1 ], + "MD019": [ 3, 5 ], + "MD041": [ 1 ] + }, + "./test/first_heading_bad_atx.md": { + "MD041": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("defaultFalse", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "default": false + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": {}, + "./test/first_heading_bad_atx.md": {} + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("defaultUndefined", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": {}, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD018": [ 1 ], + "MD019": [ 3, 5 ], + "MD041": [ 1 ] + }, + "./test/first_heading_bad_atx.md": { + "MD041": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("disableRules", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": false, + "default": true, + "MD019": false, + "first-line-h1": false + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD018": [ 1 ] + }, + "./test/first_heading_bad_atx.md": {} + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("enableRules", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": true, + "default": false, + "no-multiple-space-atx": true + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD002": [ 3 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": { + "MD002": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("enableRulesMixedCase", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "Md002": true, + "DeFaUlT": false, + "nO-mUlTiPlE-sPaCe-AtX": true + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD002": [ 3 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": { + "MD002": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("disableTag", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "default": true, + "spaces": false + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD041": [ 1 ] + }, + "./test/first_heading_bad_atx.md": { + "MD041": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("enableTag", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "default": false, + "spaces": true, + "notatag": true + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD018": [ 1 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": {} + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("enableTagMixedCase", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "DeFaUlT": false, + "SpAcEs": true, + "NoTaTaG": true + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD018": [ 1 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": {} + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("styleFiles", (test) => { + test.plan(4); + fs.readdir("./style", function readdir(err, files) { + test.ifError(err); + files.forEach(function forFile(file) { + test.ok(require(path.join("../style", file)), "Unable to load/parse."); + }); + test.end(); + }); +}); + +tape("styleAll", (test) => { + test.plan(2); + const options = { + "files": [ "./test/break-all-the-rules.md" ], + "config": require("../style/all.json"), + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/break-all-the-rules.md": { + "MD001": [ 3 ], + "MD003": [ 5, 31 ], + "MD004": [ 8 ], + "MD005": [ 12 ], + "MD007": [ 8, 11 ], + "MD009": [ 14 ], + "MD010": [ 14 ], + "MD011": [ 16 ], + "MD012": [ 18 ], + "MD013": [ 21 ], + "MD014": [ 23 ], + "MD018": [ 25 ], + "MD019": [ 27 ], + "MD020": [ 29 ], + "MD021": [ 31 ], + "MD022": [ 86 ], + "MD023": [ 40 ], + "MD024": [ 35 ], + "MD026": [ 40 ], + "MD027": [ 42 ], + "MD028": [ 43 ], + "MD029": [ 47 ], + "MD030": [ 8 ], + "MD031": [ 50 ], + "MD032": [ 7, 8, 51 ], + "MD033": [ 55 ], + "MD034": [ 57 ], + "MD035": [ 61 ], + "MD036": [ 65 ], + "MD037": [ 67 ], + "MD038": [ 69 ], + "MD039": [ 71 ], + "MD040": [ 73 ], + "MD041": [ 1 ], + "MD042": [ 81 ], + "MD045": [ 85 ], + "MD046": [ 49, 73, 77 ], + "MD047": [ 88 ], + "MD048": [ 77 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("styleRelaxed", (test) => { + test.plan(2); + const options = { + "files": [ "./test/break-all-the-rules.md" ], + "config": require("../style/relaxed.json"), + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/break-all-the-rules.md": { + "MD001": [ 3 ], + "MD003": [ 5, 31 ], + "MD004": [ 8 ], + "MD005": [ 12 ], + "MD011": [ 16 ], + "MD014": [ 23 ], + "MD018": [ 25 ], + "MD019": [ 27 ], + "MD020": [ 29 ], + "MD021": [ 31 ], + "MD022": [ 86 ], + "MD023": [ 40 ], + "MD024": [ 35 ], + "MD026": [ 40 ], + "MD029": [ 47 ], + "MD031": [ 50 ], + "MD032": [ 7, 8, 51 ], + "MD035": [ 61 ], + "MD036": [ 65 ], + "MD042": [ 81 ], + "MD045": [ 85 ], + "MD046": [ 49, 73, 77 ], + "MD047": [ 88 ], + "MD048": [ 77 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("nullFrontMatter", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "content": "---\n\t\n---\n# Heading\n" + }, + "frontMatter": null, + "config": { + "default": false, + "MD010": true + }, + "resultVersion": 0 + }, function callback(err, result) { + test.ifError(err); + const expectedResult = { + "content": { "MD010": [ 2 ] } + }; + test.deepEqual(result, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customFrontMatter", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "content": "\n\t\n\n# Heading\n" + }, + "frontMatter": /[^]*<\/head>/, + "config": { + "default": false, + "MD010": true + } + }, function callback(err, result) { + test.ifError(err); + const expectedResult = { + "content": [] + }; + test.deepEqual(result, expectedResult, "Did not get empty results."); + test.end(); + }); +}); + +tape("noInlineConfig", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "content": [ + "# Heading", + "", + "\tTab", + "", + "", + "", + "\tTab", + "", + "", + "", + "\tTab\n" + ].join("\n") + }, + "noInlineConfig": true, + "resultVersion": 0 + }, function callback(err, result) { + test.ifError(err); + const expectedResult = { + "content": { + "MD010": [ 3, 7, 11 ] + } + }; + test.deepEqual(result, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("readmeHeadings", (test) => { + test.plan(2); + markdownlint({ + "files": "README.md", + "noInlineConfig": true, + "config": { + "default": false, + "MD013": { + "line_length": 150 + }, + "MD043": { + "headings": [ + "# markdownlint", + "## Install", + "## Overview", + "### Related", + "## Demonstration", + "## Rules / Aliases", + "## Tags", + "## Configuration", + "## API", + "### Linting", + "#### options", + "##### options.customRules", + "##### options.files", + "##### options.strings", + "##### options.config", + "##### options.frontMatter", + "##### options.handleRuleFailures", + "##### options.noInlineConfig", + "##### options.resultVersion", + "##### options.markdownItPlugins", + "#### callback", + "#### result", + "### Config", + "#### file", + "#### parsers", + "#### callback", + "#### result", + "## Usage", + "## Browser", + "## Examples", + "## Contributing", + "## History" + ] + } + } + }, function callback(err, result) { + test.ifError(err); + const expected = { "README.md": [] }; + test.deepEqual(result, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("filesArrayNotModified", (test) => { + test.plan(2); + const files = [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ]; + const expectedFiles = files.slice(); + markdownlint({ "files": files }, function callback(err) { + test.ifError(err); + test.deepEqual(files, expectedFiles, "Files modified."); + test.end(); + }); +}); + +tape("filesArrayAsString", (test) => { + test.plan(2); + markdownlint({ + "files": "README.md", + "noInlineConfig": true, + "config": { + "MD013": { "line_length": 150 }, + "MD024": false + } + }, function callback(err, actual) { + test.ifError(err); + const expected = { "README.md": [] }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("missingOptions", (test) => { + test.plan(2); + markdownlint(null, function callback(err, result) { + test.ifError(err); + test.deepEqual( + result, + {}, + "Did not get empty result for missing options." + ); + test.end(); + }); +}); + +tape("missingFilesAndStrings", (test) => { + test.plan(2); + markdownlint({}, function callback(err, result) { + test.ifError(err); + test.ok(result, "Did not get result for missing files/strings."); + test.end(); + }); +}); + +tape("missingCallback", (test) => { + test.plan(0); + // @ts-ignore + markdownlint(); + test.end(); +}); + +tape("badFile", (test) => { + test.plan(4); + markdownlint({ + "files": [ "./badFile" ] + }, function callback(err, result) { + test.ok(err, "Did not get an error for bad file."); + test.ok(err instanceof Error, "Error not instance of Error."); + // @ts-ignore + test.equal(err.code, "ENOENT", "Error code for bad file not ENOENT."); + test.ok(!result, "Got result for bad file."); + test.end(); + }); +}); + +tape("badFileSync", (test) => { + test.plan(1); + test.throws( + function badFileCall() { + markdownlint.sync({ + "files": [ "./badFile" ] + }); + }, + /ENOENT/, + "Did not get correct exception for bad file." + ); + test.end(); +}); + +tape("missingStringValue", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "undefined": undefined, + "null": null, + "empty": "" + }, + "config": defaultConfig + }, function callback(err, result) { + test.ifError(err); + const expectedResult = { + "undefined": [], + "null": [], + "empty": [] + }; + test.deepEqual(result, expectedResult, "Did not get empty results."); + test.end(); + }); +}); + +tape("readme", (test) => { + test.plan(115); + const tagToRules = {}; + rules.forEach(function forRule(rule) { + rule.tags.forEach(function forTag(tag) { + const tagRules = tagToRules[tag] || []; + tagRules.push(rule.names[0]); + tagToRules[tag] = tagRules; + }); + }); + fs.readFile("README.md", helpers.utf8Encoding, + function readFile(err, contents) { + test.ifError(err); + const rulesLeft = rules.slice(); + let seenRelated = false; + let seenRules = false; + let inRules = false; + let seenTags = false; + let inTags = false; + md.parse(contents, {}).forEach(function forToken(token) { + if ( + (token.type === "bullet_list_open") && + (token.level === 0) + ) { + if (!seenRelated) { + seenRelated = true; + } else if (seenRelated && !seenRules) { + seenRules = true; + inRules = true; + } else if (seenRelated && seenRules && !seenTags) { + seenTags = true; + inTags = true; + } + } else if ( + (token.type === "bullet_list_close") && + (token.level === 0) + ) { + inRules = false; + inTags = false; + } else if (token.type === "inline") { + if (inRules) { + const rule = rulesLeft.shift(); + test.ok(rule, + "Missing rule implementation for " + token.content + "."); + if (rule) { + const ruleName = rule.names[0]; + const ruleAliases = rule.names.slice(1); + let expected = "**[" + ruleName + "](doc/Rules.md#" + + ruleName.toLowerCase() + ")** *" + + ruleAliases.join("/") + "* - " + rule.description; + if (deprecatedRuleNames.has(ruleName)) { + expected = "~~" + expected + "~~"; + } + test.equal(token.content, expected, "Rule mismatch."); + } + } else if (inTags) { + const parts = + token.content.replace(/\*\*/g, "").split(/ - |, |,\n/); + const tag = parts.shift(); + test.deepEqual(parts, tagToRules[tag] || [], + "Rule mismatch for tag " + tag + "."); + delete tagToRules[tag]; + } + } + }); + const ruleLeft = rulesLeft.shift(); + test.ok(!ruleLeft, + "Missing rule documentation for " + + (ruleLeft || "[NO RULE]").toString() + "."); + const tagLeft = Object.keys(tagToRules).shift(); + test.ok(!tagLeft, "Undocumented tag " + tagLeft + "."); + test.end(); + }); +}); + +tape("rules", (test) => { + test.plan(336); + fs.readFile("doc/Rules.md", helpers.utf8Encoding, + (err, contents) => { + test.ifError(err); + const rulesLeft = rules.slice(); + let inHeading = false; + let rule = null; + let ruleHasTags = true; + let ruleHasAliases = true; + let ruleUsesParams = null; + const tagAliasParameterRe = /, |: | /; + // eslint-disable-next-line func-style + const testTagsAliasesParams = (r) => { + r = r || "[NO RULE]"; + test.ok(ruleHasTags, + "Missing tags for rule " + r.names + "."); + test.ok(ruleHasAliases, + "Missing aliases for rule " + r.names + "."); + test.ok(!ruleUsesParams, + "Missing parameters for rule " + r.names + "."); + }; + md.parse(contents, {}).forEach(function forToken(token) { + if ((token.type === "heading_open") && (token.tag === "h2")) { + inHeading = true; + } else if (token.type === "heading_close") { + inHeading = false; + } else if (token.type === "inline") { + if (inHeading) { + testTagsAliasesParams(rule); + rule = rulesLeft.shift(); + ruleHasTags = false; + ruleHasAliases = false; + test.ok(rule, + "Missing rule implementation for " + token.content + "."); + const ruleName = rule.names[0]; + let headingContent = ruleName + " - " + rule.description; + if (deprecatedRuleNames.has(ruleName)) { + headingContent = "~~" + headingContent + "~~"; + } + test.equal(token.content, + headingContent, + "Rule mismatch."); + ruleUsesParams = rule.function.toString() + .match(/params\.config\.[_a-z]*/gi); + if (ruleUsesParams) { + ruleUsesParams = ruleUsesParams.map(function forUse(use) { + return use.split(".").pop(); + }); + ruleUsesParams.sort(); + } + } else if (token.content.startsWith("Tags: ") && rule) { + test.deepEqual(token.content.split(tagAliasParameterRe).slice(1), + rule.tags, "Tag mismatch for rule " + rule.names + "."); + ruleHasTags = true; + } else if (token.content.startsWith("Aliases: ") && rule) { + test.deepEqual(token.content.split(tagAliasParameterRe).slice(1), + rule.names.slice(1), + "Alias mismatch for rule " + rule.names + "."); + ruleHasAliases = true; + } else if (token.content.startsWith("Parameters: ") && rule) { + let inDetails = false; + const parameters = token.content.split(tagAliasParameterRe) + .slice(1) + .filter(function forPart(part) { + inDetails = inDetails || (part[0] === "("); + return !inDetails; + }); + parameters.sort(); + test.deepEqual(parameters, ruleUsesParams, + "Missing parameter for rule " + rule.names); + ruleUsesParams = null; + } + } + }); + const ruleLeft = rulesLeft.shift(); + test.ok(!ruleLeft, + "Missing rule documentation for " + + (ruleLeft || { "names": "[NO RULE]" }).names + "."); + if (rule) { + testTagsAliasesParams(rule); + } + test.end(); + }); +}); + +tape("validateConfigSchema", (test) => { + const jsonFileRe = /\.json$/i; + const resultsFileRe = /\.results\.json$/i; + const jsConfigFileRe = /^jsconfig\.json$/i; + const wrongTypesFileRe = /wrong-types-in-config-file.json$/i; + const testDirectory = __dirname; + const testFiles = fs.readdirSync(testDirectory); + testFiles.filter(function filterFile(file) { + return jsonFileRe.test(file) && + !resultsFileRe.test(file) && + !jsConfigFileRe.test(file) && + !wrongTypesFileRe.test(file); + }).forEach(function forFile(file) { + const data = fs.readFileSync( + path.join(testDirectory, file), + helpers.utf8Encoding + ); + test.ok( + tv4.validate(JSON.parse(data), configSchema), + file + "\n" + JSON.stringify(tv4.error, null, 2)); + }); + test.end(); +}); + +tape("clearHtmlCommentTextValid", (test) => { + test.plan(1); + const validComments = [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "texttext", + "texttext", + "texttext", + "", + "texttexttext", + "texttext", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "texttext", + "texttext", + "texttext", + "", + "texttexttext", + "texttext", + "", + "", + "-->", + "-->", + "", + "", + "", + "-->", + " -->", + "-->", + "text-->", + "text-->", + "", + "" + ]; + const actual = helpers.clearHtmlCommentText(invalidComments.join("\n")); + const expected = invalidComments.join("\n"); + test.equal(actual, expected); + test.end(); +}); + +tape("clearHtmlCommentTextNonGreedy", (test) => { + test.plan(1); + const nonGreedyComments = [ + " -->", + " -->", + " -->", + " -->" + ]; + const nonGreedyResult = [ + " -->", + " -->", + " -->", + " -->" + ]; + const actual = helpers.clearHtmlCommentText(nonGreedyComments.join("\n")); + const expected = nonGreedyResult.join("\n"); + test.equal(actual, expected); + test.end(); +}); + +tape("clearHtmlCommentTextEmbedded", (test) => { + test.plan(1); + const embeddedComments = [ + "texttext", + "", + "texttext", + "texttext", + "texttext" + ]; + const embeddedResult = [ + "texttext", + "", + "texttext", + "texttext", + "texttext" + ]; + const actual = helpers.clearHtmlCommentText(embeddedComments.join("\n")); + const expected = embeddedResult.join("\n"); + test.equal(actual, expected); + test.end(); +}); + +tape("unescapeMarkdown", (test) => { + test.plan(7); + // Test cases from https://spec.commonmark.org/0.29/#backslash-escapes + const testCases = [ + [ + "\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;" + + "\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~", + "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" + ], + [ + "\\→\\A\\a\\ \\3\\φ\\«", + "\\→\\A\\a\\ \\3\\φ\\«" + ], + [ + `\\*not emphasized* +\\
not a tag +\\[not a link](/foo) +\\\`not code\` +1\\. not a list +\\* not a list +\\# not a heading +\\[foo]: /url "not a reference" +\\ö not a character entity`, + `*not emphasized* +
not a tag +[not a link](/foo) +\`not code\` +1. not a list +* not a list +# not a heading +[foo]: /url "not a reference" +ö not a character entity` + ], + [ + "\\\\*emphasis*", + "\\*emphasis*" + ], + [ + `foo\\ +bar`, + `foo\\ +bar` + ], + [ + "Text \\<", + "Text _", + "_" + ], + [ + "Text \\\\<", + "Text _<", + "_" + ] + ]; + testCases.forEach(function forTestCase(testCase) { + const [ markdown, expected, replacement ] = testCase; + const actual = helpers.unescapeMarkdown(markdown, replacement); + test.equal(actual, expected); + }); + test.end(); +}); + +tape("isBlankLine", (test) => { + test.plan(25); + const blankLines = [ + null, + "", + " ", + " ", + "\t\t\t", + "\r", + "\n", + "\t\r\n", + " ", + "", + "", + "\t", + ">", + "> ", + "> > > \t", + "> ", + ">>" + ]; + blankLines.forEach((line) => test.ok(helpers.isBlankLine(line), line)); + const nonBlankLines = [ + "text", + " text ", + ".", + "> .", + " text", + "", + "" + ]; + nonBlankLines.forEach((line) => test.ok(!helpers.isBlankLine(line), line)); + test.end(); +}); + +tape("includesSorted", (test) => { + test.plan(154); + const inputs = [ + [ ], + [ 8 ], + [ 7, 11 ], + [ 0, 1, 2, 3, 5, 8, 13 ], + [ 2, 3, 5, 7, 11, 13, 17, 19 ], + [ 1, 3, 5, 7, 9, 11, 13, 15, 17, 19 ], + [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 ] + ]; + inputs.forEach((input) => { + for (let i = 0; i <= 21; i++) { + test.equal(helpers.includesSorted(input, i), input.includes(i)); + } + }); + test.end(); +}); + +tape("forEachInlineCodeSpan", (test) => { + test.plan(99); + const testCases = + [ + { + "input": "`code`", + "expecteds": [ [ "code", 0, 1, 1 ] ] + }, + { + "input": "text `code` text", + "expecteds": [ [ "code", 0, 6, 1 ] ] + }, + { + "input": "text `code` text `edoc`", + "expecteds": [ + [ "code", 0, 6, 1 ], + [ "edoc", 0, 18, 1 ] + ] + }, + { + "input": "text `code` text `edoc` text", + "expecteds": [ + [ "code", 0, 6, 1 ], + [ "edoc", 0, 18, 1 ] + ] + }, + { + "input": "text ``code`code`` text", + "expecteds": [ [ "code`code", 0, 7, 2 ] ] + }, + { + "input": "`code `` code`", + "expecteds": [ [ "code `` code", 0, 1, 1 ] ] + }, + { + "input": "`code\\`text`", + "expecteds": [ [ "code\\", 0, 1, 1 ] ] + }, + { + "input": "``\ncode\n``", + "expecteds": [ [ "\ncode\n", 0, 2, 2 ] ] + }, + { + "input": "text\n`code`\ntext", + "expecteds": [ [ "code", 1, 1, 1 ] ] + }, + { + "input": "text\ntext\n`code`\ntext\n`edoc`\ntext", + "expecteds": [ + [ "code", 2, 1, 1 ], + [ "edoc", 4, 1, 1 ] + ] + }, + { + "input": "text `code\nedoc` text", + "expecteds": [ [ "code\nedoc", 0, 6, 1 ] ] + }, + { + "input": "> text `code` text", + "expecteds": [ [ "code", 0, 8, 1 ] ] + }, + { + "input": "> text\n> `code`\n> text", + "expecteds": [ [ "code", 1, 3, 1 ] ] + }, + { + "input": "> text\n> `code\n> edoc`\n> text", + "expecteds": [ [ "code\n> edoc", 1, 3, 1 ] ] + }, + { + "input": "```text``", + "expecteds": [] + }, + { + "input": "text `text text", + "expecteds": [] + }, + { + "input": "`text``code``", + "expecteds": [ [ "code", 0, 7, 2 ] ] + }, + { + "input": "text \\` text `code`", + "expecteds": [ [ "code", 0, 14, 1 ] ] + }, + { + "input": "text\\\n`code`", + "expecteds": [ [ "code", 1, 1, 1 ] ] + } + ]; + testCases.forEach((testCase) => { + const { input, expecteds } = testCase; + helpers.forEachInlineCodeSpan(input, (code, line, column, ticks) => { + const [ expectedCode, expectedLine, expectedColumn, expectedTicks ] = + expecteds.shift(); + test.equal(code, expectedCode, input); + test.equal(line, expectedLine, input); + test.equal(column, expectedColumn, input); + test.equal(ticks, expectedTicks, input); + }); + test.equal(expecteds.length, 0, "length"); + }); + test.end(); +}); + +tape("getPreferredLineEnding", (test) => { + test.plan(17); + const testCases = [ + [ "", os.EOL ], + [ "\r", "\r" ], + [ "\n", "\n" ], + [ "\r\n", "\r\n" ], + [ "t\rt\nt", "\n" ], + [ "t\nt\rt", "\n" ], + [ "t\r\nt\nt", "\n" ], + [ "t\nt\r\nt", "\n" ], + [ "t\r\nt\rt", "\r\n" ], + [ "t\rt\r\nt", "\r\n" ], + [ "t\r\nt\rt\nt", "\n" ], + [ "t\r\nt\r\nt\r\nt", "\r\n" ], + [ "t\nt\nt\nt", "\n" ], + [ "t\rt\rt\rt", "\r" ], + [ "t\r\nt\nt\r\nt", "\r\n" ], + [ "t\nt\r\nt\nt", "\n" ], + [ "t\rt\t\rt", "\r" ] + ]; + testCases.forEach((testCase) => { + const [ input, expected ] = testCase; + const actual = helpers.getPreferredLineEnding(input); + test.equal(actual, expected, "Incorrect line ending returned."); + }); + test.end(); +}); + +tape("applyFix", (test) => { + test.plan(4); + const testCases = [ + [ + "Hello world.", + { + "editColumn": 12, + "deleteCount": 1 + }, + undefined, + "Hello world" + ], + [ + "Hello world.", + { + "editColumn": 13, + "insertText": "\n" + }, + undefined, + "Hello world.\n" + ], + [ + "Hello world.", + { + "editColumn": 13, + "insertText": "\n" + }, + "\n", + "Hello world.\n" + ], + [ + "Hello world.", + { + "editColumn": 13, + "insertText": "\n" + }, + "\r\n", + "Hello world.\r\n" + ] + ]; + testCases.forEach((testCase) => { + const [ line, fixInfo, lineEnding, expected ] = testCase; + // @ts-ignore + const actual = helpers.applyFix(line, fixInfo, lineEnding); + test.equal(actual, expected, "Incorrect fix applied."); + }); + test.end(); +}); + +tape("applyFixes", (test) => { + test.plan(28); + const testCases = [ + [ + "Hello world.", + [], + "Hello world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": {} + } + ], + "Hello world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "insertText": "Very " + } + } + ], + "Very Hello world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 7, + "insertText": "big " + } + } + ], + "Hello big world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "deleteCount": 6 + } + } + ], + "world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 7, + "deleteCount": 5, + "insertText": "there" + } + } + ], + "Hello there." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 12, + "deleteCount": 1 + } + }, + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 6, + "deleteCount": 1 + } + } + ], + "Helloworld" + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 13, + "insertText": " Hi." + } + } + ], + "Hello world. Hi." + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 1, + "fixInfo": { + "deleteCount": -1 + } + } + ], + "world" + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 2, + "fixInfo": { + "deleteCount": -1 + } + } + ], + "Hello" + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 2, + "fixInfo": { + "lineNumber": 1, + "deleteCount": -1 + } + } + ], + "world" + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 1, + "fixInfo": { + "lineNumber": 2, + "deleteCount": -1 + } + } + ], + "Hello" + ], + [ + "Hello world", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 4, + "deleteCount": 1 + } + }, + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 10, + "deleteCount": 1 + } + } + ], + "Helo word" + ], + [ + "Hello world", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 10, + "deleteCount": 1 + } + }, + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 4, + "deleteCount": 1 + } + } + ], + "Helo word" + ], + [ + "Hello\nworld", + [ + { + "fixInfo": { + "lineNumber": 1, + "deleteCount": -1 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "insertText": "Big " + } + } + ], + "world" + ], + [ + "Hello\nworld", + [ + { + "fixInfo": { + "lineNumber": 1, + "deleteCount": -1 + } + }, + { + "fixInfo": { + "lineNumber": 2, + "deleteCount": -1 + } + } + ], + "" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "insertText": "aa" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "insertText": "b" + } + } + ], + "aaHello world" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "insertText": "a" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "insertText": "bb" + } + } + ], + "bbHello world" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 6, + "insertText": " big" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1 + } + } + ], + "Hello big orld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 8, + "deleteCount": 2 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 2 + } + } + ], + "Hello wld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 2 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 8, + "deleteCount": 2 + } + } + ], + "Hello wld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1, + "insertText": "z" + } + } + ], + "Hello zorld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "insertText": "z" + } + } + ], + "Hello zorld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "insertText": "z" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1 + } + } + ], + "Hello zorld" + ], + [ + "Hello\nworld\nhello\rworld", + [ + { + "fixInfo": { + "lineNumber": 4, + "editColumn": 6, + "insertText": "\n" + } + } + ], + "Hello\nworld\nhello\nworld\n" + ], + [ + "Hello\r\nworld\r\nhello\nworld", + [ + { + "fixInfo": { + "lineNumber": 4, + "editColumn": 6, + "insertText": "\n" + } + } + ], + "Hello\r\nworld\r\nhello\r\nworld\r\n" + ], + [ + "Hello\rworld\rhello\nworld", + [ + { + "fixInfo": { + "lineNumber": 4, + "editColumn": 6, + "insertText": "\n" + } + } + ], + "Hello\rworld\rhello\rworld\r" + ], + [ + "Hello\r\nworld", + [ + { + "lineNumber": 2, + "fixInfo": { + "editColumn": 6, + "insertText": "\n\n" + } + } + ], + "Hello\r\nworld\r\n\r\n" + ] + ]; + testCases.forEach((testCase) => { + const [ input, errors, expected ] = testCase; + const actual = helpers.applyFixes(input, errors); + test.equal(actual, expected, "Incorrect fix applied."); + }); + test.end(); +}); + +tape("configSingle", (test) => { + test.plan(2); + markdownlint.readConfig("./test/config/config-child.json", + function callback(err, actual) { + test.ifError(err); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configAbsolute", (test) => { + test.plan(2); + markdownlint.readConfig(path.join(__dirname, "config", "config-child.json"), + function callback(err, actual) { + test.ifError(err); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configMultiple", (test) => { + test.plan(2); + markdownlint.readConfig("./test/config/config-grandparent.json", + function callback(err, actual) { + test.ifError(err); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configBadFile", (test) => { + test.plan(4); + markdownlint.readConfig("./test/config/config-badfile.json", + function callback(err, result) { + test.ok(err, "Did not get an error for bad file."); + test.ok(err instanceof Error, "Error not instance of Error."); + // @ts-ignore + test.equal(err.code, "ENOENT", "Error code for bad file not ENOENT."); + test.ok(!result, "Got result for bad file."); + test.end(); + }); +}); + +tape("configBadChildFile", (test) => { + test.plan(4); + markdownlint.readConfig("./test/config/config-badchildfile.json", + function callback(err, result) { + test.ok(err, "Did not get an error for bad child file."); + test.ok(err instanceof Error, "Error not instance of Error."); + // @ts-ignore + test.equal(err.code, "ENOENT", + "Error code for bad child file not ENOENT."); + test.ok(!result, "Got result for bad child file."); + test.end(); + }); +}); + +tape("configBadJson", (test) => { + test.plan(3); + markdownlint.readConfig("./test/config/config-badjson.json", + function callback(err, result) { + test.ok(err, "Did not get an error for bad JSON."); + test.ok(err instanceof Error, "Error not instance of Error."); + test.ok(!result, "Got result for bad JSON."); + test.end(); + }); +}); + +tape("configBadChildJson", (test) => { + test.plan(3); + markdownlint.readConfig("./test/config/config-badchildjson.json", + function callback(err, result) { + test.ok(err, "Did not get an error for bad child JSON."); + test.ok(err instanceof Error, "Error not instance of Error."); + test.ok(!result, "Got result for bad child JSON."); + test.end(); + }); +}); + +tape("configSingleYaml", (test) => { + test.plan(2); + markdownlint.readConfig( + "./test/config/config-child.yaml", + [ require("js-yaml").safeLoad ], + function callback(err, actual) { + test.ifError(err); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configMultipleYaml", (test) => { + test.plan(2); + markdownlint.readConfig( + "./test/config/config-grandparent.yaml", + [ require("js-yaml").safeLoad ], + function callback(err, actual) { + test.ifError(err); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configMultipleHybrid", (test) => { + test.plan(2); + markdownlint.readConfig( + "./test/config/config-grandparent-hybrid.yaml", + [ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ], + function callback(err, actual) { + test.ifError(err); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepLooseEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configBadHybrid", (test) => { + test.plan(4); + markdownlint.readConfig( + "./test/config/config-badcontent.txt", + [ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ], + function callback(err, result) { + test.ok(err, "Did not get an error for bad child JSON."); + test.ok(err instanceof Error, "Error not instance of Error."); + test.ok(err.message.match( + // eslint-disable-next-line max-len + /^Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+; Expected [^;]+ or end of input but "\S+" found.; end of the stream or a document separator is expected at line \d+, column \d+:[^;]*$/ + ), "Error message unexpected."); + test.ok(!result, "Got result for bad child JSON."); + test.end(); + }); +}); + +tape("configSingleSync", (test) => { + test.plan(1); + const actual = markdownlint.readConfigSync("./test/config/config-child.json"); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configAbsoluteSync", (test) => { + test.plan(1); + const actual = markdownlint.readConfigSync( + path.join(__dirname, "config", "config-child.json")); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configMultipleSync", (test) => { + test.plan(1); + const actual = + markdownlint.readConfigSync("./test/config/config-grandparent.json"); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configBadFileSync", (test) => { + test.plan(1); + test.throws( + function badFileCall() { + markdownlint.readConfigSync("./test/config/config-badfile.json"); + }, + /ENOENT/, + "Did not get correct exception for bad file." + ); + test.end(); +}); + +tape("configBadChildFileSync", (test) => { + test.plan(1); + test.throws( + function badChildFileCall() { + markdownlint.readConfigSync("./test/config/config-badchildfile.json"); + }, + /ENOENT/, + "Did not get correct exception for bad child file." + ); + test.end(); +}); + +tape("configBadJsonSync", (test) => { + test.plan(1); + test.throws( + function badJsonCall() { + markdownlint.readConfigSync("./test/config/config-badjson.json"); + }, + /Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+/, + "Did not get correct exception for bad JSON." + ); + test.end(); +}); + +tape("configBadChildJsonSync", (test) => { + test.plan(1); + test.throws( + function badChildJsonCall() { + markdownlint.readConfigSync("./test/config/config-badchildjson.json"); + }, + /Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+/, + "Did not get correct exception for bad child JSON." + ); + test.end(); +}); + +tape("configSingleYamlSync", (test) => { + test.plan(1); + const actual = markdownlint.readConfigSync( + "./test/config/config-child.yaml", [ require("js-yaml").safeLoad ]); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configMultipleYamlSync", (test) => { + test.plan(1); + const actual = markdownlint.readConfigSync( + "./test/config/config-grandparent.yaml", [ require("js-yaml").safeLoad ]); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configMultipleHybridSync", (test) => { + test.plan(1); + const actual = markdownlint.readConfigSync( + "./test/config/config-grandparent-hybrid.yaml", + [ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ]); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepLooseEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configBadHybridSync", (test) => { + test.plan(1); + test.throws( + function badHybridCall() { + markdownlint.readConfigSync( + "./test/config/config-badcontent.txt", + [ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ]); + }, + // eslint-disable-next-line max-len + /Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+; Expected [^;]+ or end of input but "\S+" found.; end of the stream or a document separator is expected at line \d+, column \d+:[^;]*/, + "Did not get correct exception for bad content." + ); + test.end(); +}); + +tape("allBuiltInRulesHaveValidUrl", (test) => { + test.plan(132); + rules.forEach(function forRule(rule) { + test.ok(rule.information); + test.ok(Object.getPrototypeOf(rule.information) === URL.prototype); + const name = rule.names[0].toLowerCase(); + test.equal( + rule.information.href, + `${homepage}/blob/v${version}/doc/Rules.md#${name}` + ); + }); + test.end(); +}); + +tape("someCustomRulesHaveValidUrl", (test) => { + test.plan(7); + customRules.all.forEach(function forRule(rule) { + test.ok(!rule.information || + (Object.getPrototypeOf(rule.information) === URL.prototype)); + if (rule === customRules.anyBlockquote) { + test.equal( + rule.information.href, + `${homepage}/blob/main/test/rules/any-blockquote.js` + ); + } else if (rule === customRules.lettersEX) { + test.equal( + rule.information.href, + `${homepage}/blob/main/test/rules/letters-E-X.js` + ); + } + }); + test.end(); +}); + +tape("customRulesV0", (test) => { + test.plan(4); + const customRulesMd = "./test/custom-rules.md"; + const options = { + "customRules": customRules.all, + "files": [ customRulesMd ], + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = {}; + expectedResult[customRulesMd] = { + "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."); + let actualMessage = actualResult.toString(); + let expectedMessage = + "./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" + + " Rule that reports an error every N lines\n" + + "./test/custom-rules.md: 6: every-n-lines" + + " Rule that reports an error every N lines\n" + + "./test/custom-rules.md: 10: every-n-lines" + + " 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" + + " Rule that reports an error for lines with the letters 'EX'"; + test.equal(actualMessage, expectedMessage, "Incorrect message (name)."); + // @ts-ignore + actualMessage = actualResult.toString(true); + expectedMessage = + "./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" + + " Rule that reports an error every N lines\n" + + "./test/custom-rules.md: 6: every-n-lines" + + " Rule that reports an error every N lines\n" + + "./test/custom-rules.md: 10: every-n-lines" + + " 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" + + " Rule that reports an error for lines with the letters 'EX'"; + test.equal(actualMessage, expectedMessage, "Incorrect message (alias)."); + test.end(); + }); +}); + +tape("customRulesV1", (test) => { + test.plan(3); + const customRulesMd = "./test/custom-rules.md"; + const options = { + "customRules": customRules.all, + "files": [ customRulesMd ], + "resultVersion": 1 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = {}; + expectedResult[customRulesMd] = [ + { "lineNumber": 12, + "ruleName": "any-blockquote", + "ruleAlias": "any-blockquote", + "ruleDescription": "Rule that reports an error for any blockquote", + "ruleInformation": + `${homepage}/blob/main/test/rules/any-blockquote.js`, + "errorDetail": "Blockquote spans 1 line(s).", + "errorContext": "> Block", + "errorRange": null }, + { "lineNumber": 2, + "ruleName": "every-n-lines", + "ruleAlias": "every-n-lines", + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 2", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 4, + "ruleName": "every-n-lines", + "ruleAlias": "every-n-lines", + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 4", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 6, + "ruleName": "every-n-lines", + "ruleAlias": "every-n-lines", + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 6", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 10, + "ruleName": "every-n-lines", + "ruleAlias": "every-n-lines", + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 10", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 12, + "ruleName": "every-n-lines", + "ruleAlias": "every-n-lines", + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "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", + "ruleInformation": null, + "errorDetail": null, + "errorContext": null, + "errorRange": null }, + { "lineNumber": 3, + "ruleName": "letters-E-X", + "ruleAlias": "letter-E-letter-X", + "ruleDescription": + "Rule that reports an error for lines with the letters 'EX'", + "ruleInformation": `${homepage}/blob/main/test/rules/letters-E-X.js`, + "errorDetail": null, + "errorContext": "text", + "errorRange": null }, + { "lineNumber": 7, + "ruleName": "letters-E-X", + "ruleAlias": "letter-E-letter-X", + "ruleDescription": + "Rule that reports an error for lines with the letters 'EX'", + "ruleInformation": `${homepage}/blob/main/test/rules/letters-E-X.js`, + "errorDetail": null, + "errorContext": "text", + "errorRange": null } + ]; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "./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" + + "./test/custom-rules.md: 4: every-n-lines/every-n-lines" + + " Rule that reports an error every N lines [Line number 4]\n" + + "./test/custom-rules.md: 6: every-n-lines/every-n-lines" + + " Rule that reports an error every N lines [Line number 6]\n" + + "./test/custom-rules.md: 10: every-n-lines/every-n-lines" + + " 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" + + "./test/custom-rules.md: 7: letters-E-X/letter-E-letter-X" + + " Rule that reports an error for lines with the letters 'EX'" + + " [Context: \"text\"]"; + test.equal(actualMessage, expectedMessage, "Incorrect message."); + test.end(); + }); +}); + +tape("customRulesV2", (test) => { + test.plan(3); + const customRulesMd = "./test/custom-rules.md"; + const options = { + "customRules": customRules.all, + "files": [ customRulesMd ], + "resultVersion": 2 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = {}; + expectedResult[customRulesMd] = [ + { "lineNumber": 12, + "ruleNames": [ "any-blockquote" ], + "ruleDescription": "Rule that reports an error for any blockquote", + "ruleInformation": + `${homepage}/blob/main/test/rules/any-blockquote.js`, + "errorDetail": "Blockquote spans 1 line(s).", + "errorContext": "> Block", + "errorRange": null }, + { "lineNumber": 2, + "ruleNames": [ "every-n-lines" ], + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 2", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 4, + "ruleNames": [ "every-n-lines" ], + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 4", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 6, + "ruleNames": [ "every-n-lines" ], + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 6", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 10, + "ruleNames": [ "every-n-lines" ], + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 10", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 12, + "ruleNames": [ "every-n-lines" ], + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 12", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 1, + "ruleNames": [ "first-line" ], + "ruleDescription": "Rule that reports an error for the first line", + "ruleInformation": null, + "errorDetail": null, + "errorContext": null, + "errorRange": null }, + { "lineNumber": 3, + "ruleNames": [ "letters-E-X", "letter-E-letter-X", "contains-ex" ], + "ruleDescription": + "Rule that reports an error for lines with the letters 'EX'", + "ruleInformation": `${homepage}/blob/main/test/rules/letters-E-X.js`, + "errorDetail": null, + "errorContext": "text", + "errorRange": null }, + { "lineNumber": 7, + "ruleNames": [ "letters-E-X", "letter-E-letter-X", "contains-ex" ], + "ruleDescription": + "Rule that reports an error for lines with the letters 'EX'", + "ruleInformation": `${homepage}/blob/main/test/rules/letters-E-X.js`, + "errorDetail": null, + "errorContext": "text", + "errorRange": null } + ]; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "./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" + + "./test/custom-rules.md: 4: every-n-lines" + + " Rule that reports an error every N lines [Line number 4]\n" + + "./test/custom-rules.md: 6: every-n-lines" + + " Rule that reports an error every N lines [Line number 6]\n" + + "./test/custom-rules.md: 10: every-n-lines" + + " 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" + + "./test/custom-rules.md: 7: letters-E-X/letter-E-letter-X/contains-ex" + + " Rule that reports an error for lines with the letters 'EX'" + + " [Context: \"text\"]"; + test.equal(actualMessage, expectedMessage, "Incorrect message."); + test.end(); + }); +}); + +tape("customRulesConfig", (test) => { + test.plan(2); + const customRulesMd = "./test/custom-rules.md"; + const options = { + "customRules": customRules.all, + "files": [ customRulesMd ], + "config": { + "blockquote": true, + "every-n-lines": { + "n": 3 + }, + "letters-e-x": false + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = {}; + expectedResult[customRulesMd] = { + "any-blockquote": [ 12 ], + "every-n-lines": [ 3, 6, 12 ], + "first-line": [ 1 ], + "letters-E-X": [ 7 ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesNpmPackage", (test) => { + test.plan(2); + const options = { + "customRules": [ require("./rules/npm") ], + "strings": { + "string": "# Text\n\n---\n\nText\n" + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = {}; + expectedResult.string = { + "sample-rule": [ 3 ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesBadProperty", (test) => { + test.plan(23); + [ + { + "propertyName": "names", + "propertyValues": + [ null, "string", [], [ null ], [ "" ], [ "string", 10 ] ] + }, + { + "propertyName": "description", + "propertyValues": [ null, 10, "", [] ] + }, + { + "propertyName": "information", + "propertyValues": [ 10, [], "string", "https://example.com" ] + }, + { + "propertyName": "tags", + "propertyValues": + [ null, "string", [], [ null ], [ "" ], [ "string", 10 ] ] + }, + { + "propertyName": "function", + "propertyValues": [ null, "string", [] ] + } + ].forEach(function forTestCase(testCase) { + const { propertyName, propertyValues } = testCase; + propertyValues.forEach(function forPropertyValue(propertyValue) { + const badRule = { ...customRules.anyBlockquote }; + badRule[propertyName] = propertyValue; + const options = { + "customRules": [ badRule ] + }; + test.throws( + function badRuleCall() { + markdownlint.sync(options); + }, + new RegExp( + `Property '${propertyName}' of custom rule at index 0 is incorrect.` + ), + "Did not get correct exception for missing property." + ); + }); + }); + test.end(); +}); + +tape("customRulesUsedNameName", (test) => { + test.plan(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.end(); + }); +}); + +tape("customRulesUsedNameTag", (test) => { + test.plan(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.end(); + }); +}); + +tape("customRulesUsedTagName", (test) => { + test.plan(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.end(); + }); +}); + +tape("customRulesThrowForFile", (test) => { + test.plan(4); + const 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.end(); + }); +}); + +tape("customRulesThrowForFileSync", (test) => { + test.plan(1); + const 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" ] + }); + }, + new RegExp(exceptionMessage), + "Did not get correct exception for function thrown." + ); + test.end(); +}); + +tape("customRulesThrowForString", (test) => { + test.plan(4); + const exceptionMessage = "Test exception message"; + markdownlint({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function throws() { + throw new Error(exceptionMessage); + } + } + ], + "strings": { + "string": "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.end(); + }); +}); + +tape("customRulesOnErrorNull", (test) => { + test.plan(1); + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorNull(params, onError) { + onError(null); + } + } + ], + "strings": { + "string": "String" + } + }; + test.throws( + function nullErrorCall() { + markdownlint.sync(options); + }, + /Property 'lineNumber' of onError parameter is incorrect./, + "Did not get correct exception for null object." + ); + test.end(); +}); + +tape("customRulesOnErrorBad", (test) => { + test.plan(21); + [ + { + "propertyName": "lineNumber", + "subPropertyName": null, + "propertyValues": [ null, "string" ] + }, + { + "propertyName": "detail", + "subPropertyName": null, + "propertyValues": [ 10, [] ] + }, + { + "propertyName": "context", + "subPropertyName": null, + "propertyValues": [ 10, [] ] + }, + { + "propertyName": "range", + "subPropertyName": null, + "propertyValues": [ 10, [], [ 10 ], [ 10, null ], [ 10, 11, 12 ] ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": null, + "propertyValues": [ 10, "string" ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "lineNumber", + "propertyValues": [ null, "string" ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "editColumn", + "propertyValues": [ null, "string" ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "deleteCount", + "propertyValues": [ null, "string" ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "insertText", + "propertyValues": [ 10, [] ] + } + ].forEach(function forTestCase(testCase) { + const { propertyName, subPropertyName, propertyValues } = testCase; + propertyValues.forEach(function forPropertyValue(propertyValue) { + const badObject = { + "lineNumber": 1 + }; + let propertyNames = null; + if (subPropertyName) { + badObject[propertyName] = {}; + badObject[propertyName][subPropertyName] = propertyValue; + propertyNames = `${propertyName}.${subPropertyName}`; + } else { + badObject[propertyName] = propertyValue; + propertyNames = propertyName; + } + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorBad(params, onError) { + onError(badObject); + } + } + ], + "strings": { + "string": "String" + } + }; + test.throws( + function badErrorCall() { + markdownlint.sync(options); + }, + new RegExp( + `Property '${propertyNames}' of onError parameter is incorrect.` + ), + "Did not get correct exception for bad object." + ); + }); + }); + test.end(); +}); + +tape("customRulesOnErrorInvalid", (test) => { + test.plan(17); + [ + { + "propertyName": "lineNumber", + "subPropertyName": null, + "propertyValues": [ -1, 0, 3, 4 ] + }, + { + "propertyName": "range", + "subPropertyName": null, + "propertyValues": [ [ 0, 1 ], [ 1, 0 ], [ 5, 1 ], [ 1, 5 ], [ 4, 2 ] ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "lineNumber", + "propertyValues": [ -1, 0, 3, 4 ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "editColumn", + "propertyValues": [ 0, 6 ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "deleteCount", + "propertyValues": [ -2, 5 ] + } + ].forEach(function forTestCase(testCase) { + const { propertyName, subPropertyName, propertyValues } = testCase; + propertyValues.forEach(function forPropertyValue(propertyValue) { + const badObject = { + "lineNumber": 1 + }; + let propertyNames = null; + if (subPropertyName) { + badObject[propertyName] = {}; + badObject[propertyName][subPropertyName] = propertyValue; + propertyNames = `${propertyName}.${subPropertyName}`; + } else { + badObject[propertyName] = propertyValue; + propertyNames = propertyName; + } + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorInvalid(params, onError) { + onError(badObject); + } + } + ], + "strings": { + "string": "Text\ntext" + } + }; + test.throws( + function invalidErrorCall() { + markdownlint.sync(options); + }, + new RegExp( + `Property '${propertyNames}' of onError parameter is incorrect.` + ), + "Did not get correct exception for invalid object." + ); + }); + }); + test.end(); +}); + +tape("customRulesOnErrorValid", (test) => { + test.plan(24); + [ + { + "propertyName": "lineNumber", + "subPropertyName": null, + "propertyValues": [ 1, 2 ] + }, + { + "propertyName": "range", + "subPropertyName": null, + "propertyValues": [ [ 1, 1 ], [ 1, 4 ], [ 2, 2 ], [ 3, 2 ], [ 4, 1 ] ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "lineNumber", + "propertyValues": [ 1, 2 ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "editColumn", + "propertyValues": [ 1, 2, 4, 5 ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "deleteCount", + "propertyValues": [ -1, 0, 1, 4 ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "insertText", + "propertyValues": + [ "", "1", "123456", "\n", "\nText", "Text\n", "\nText\n" ] + } + ].forEach(function forTestCase(testCase) { + const { propertyName, subPropertyName, propertyValues } = testCase; + propertyValues.forEach(function forPropertyValue(propertyValue) { + const goodObject = { + "lineNumber": 1 + }; + if (subPropertyName) { + goodObject[propertyName] = {}; + goodObject[propertyName][subPropertyName] = propertyValue; + } else { + goodObject[propertyName] = propertyValue; + } + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorValid(params, onError) { + onError(goodObject); + } + } + ], + "strings": { + "string": "Text\ntext" + } + }; + markdownlint.sync(options); + test.ok(true); + }); + }); + test.end(); +}); + +tape("customRulesOnErrorLazy", (test) => { + test.plan(2); + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorLazy(params, onError) { + onError({ + "lineNumber": 1, + "detail": "", + "context": "", + "range": [ 1, 1 ] + }); + } + } + ], + "strings": { + "string": "# Heading\n" + } + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "string": [ + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": null, + "errorDetail": null, + "errorContext": null, + "errorRange": [ 1, 1 ] + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesOnErrorModified", (test) => { + test.plan(2); + const errorObject = { + "lineNumber": 1, + "detail": "detail", + "context": "context", + "range": [ 1, 2 ], + "fixInfo": { + "editColumn": 1, + "deleteCount": 2, + "insertText": "text" + } + }; + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorModified(params, onError) { + onError(errorObject); + errorObject.lineNumber = 2; + errorObject.detail = "changed"; + errorObject.context = "changed"; + errorObject.range[1] = 3; + errorObject.fixInfo.editColumn = 2; + errorObject.fixInfo.deleteCount = 3; + errorObject.fixInfo.insertText = "changed"; + } + } + ], + "strings": { + "string": "# Heading\n" + }, + "resultVersion": 3 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "string": [ + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": null, + "errorDetail": "detail", + "errorContext": "context", + "errorRange": [ 1, 2 ], + "fixInfo": { + "editColumn": 1, + "deleteCount": 2, + "insertText": "text" + } + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesThrowForFileHandled", (test) => { + test.plan(2); + const exceptionMessage = "Test exception message"; + markdownlint({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function throws() { + throw new Error(exceptionMessage); + } + } + ], + "files": [ "./test/custom-rules.md" ], + "handleRuleFailures": true + }, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/custom-rules.md": [ + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": null, + "errorDetail": + `This rule threw an exception: ${exceptionMessage}`, + "errorContext": null, + "errorRange": null + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesThrowForStringHandled", (test) => { + test.plan(2); + const exceptionMessage = "Test exception message"; + const informationUrl = "https://example.com/rule"; + markdownlint({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "information": new URL(informationUrl), + "tags": [ "tag" ], + "function": function throws() { + throw new Error(exceptionMessage); + } + } + ], + "strings": { + "string": "String\n" + }, + "handleRuleFailures": true + }, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "string": [ + { + "lineNumber": 1, + "ruleNames": [ "MD041", "first-line-heading", "first-line-h1" ], + "ruleDescription": + "First line in file should be a top level heading", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md041`, + "errorDetail": null, + "errorContext": "String", + "errorRange": null + }, + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": informationUrl, + "errorDetail": + `This rule threw an exception: ${exceptionMessage}`, + "errorContext": null, + "errorRange": null + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesOnErrorInvalidHandled", (test) => { + test.plan(2); + markdownlint({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorInvalid(params, onError) { + onError({ + "lineNumber": 13, + "details": "N/A" + }); + } + } + ], + "strings": { + "string": "# Heading\n" + }, + "handleRuleFailures": true + }, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "string": [ + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": null, + "errorDetail": "This rule threw an exception: " + + "Property 'lineNumber' of onError parameter is incorrect.", + "errorContext": null, + "errorRange": null + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesFileName", (test) => { + test.plan(2); + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function stringName(params) { + test.equal(params.name, "doc/CustomRules.md", "Incorrect file name"); + } + } + ], + "files": "doc/CustomRules.md" + }; + markdownlint(options, function callback(err) { + test.ifError(err); + test.end(); + }); +}); + +tape("customRulesStringName", (test) => { + test.plan(2); + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function stringName(params) { + test.equal(params.name, "string", "Incorrect string name"); + } + } + ], + "strings": { + "string": "# Heading" + } + }; + markdownlint(options, function callback(err) { + test.ifError(err); + test.end(); + }); +}); + +tape("customRulesDoc", (test) => { + test.plan(2); + markdownlint({ + "files": "doc/CustomRules.md", + "config": { + "MD013": { "line_length": 200 } + } + }, function callback(err, actual) { + test.ifError(err); + const expected = { "doc/CustomRules.md": [] }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("customRulesLintJavaScript", (test) => { + test.plan(2); + const options = { + "customRules": customRules.lintJavaScript, + "files": "test/lint-javascript.md" + }; + markdownlint(options, (err, actual) => { + test.ifError(err); + const expected = { + "test/lint-javascript.md": [ + { + "lineNumber": 10, + "ruleNames": [ "lint-javascript" ], + "ruleDescription": "Rule that lints JavaScript code", + "ruleInformation": null, + "errorDetail": "Unexpected var, use let or const instead.", + "errorContext": "var x = 0;", + "errorRange": null + }, + { + "lineNumber": 12, + "ruleNames": [ "lint-javascript" ], + "ruleDescription": "Rule that lints JavaScript code", + "ruleInformation": null, + "errorDetail": "Unexpected console statement.", + "errorContext": "console.log(x);", + "errorRange": null + } + ] + }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("markdownItPluginsSingle", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "string": "# Heading\n\nText [ link ](https://example.com)\n" + }, + "markdownItPlugins": [ + [ + pluginInline, + "trim_text_plugin", + "text", + function iterator(tokens, index) { + tokens[index].content = tokens[index].content.trim(); + } + ] + ] + }, function callback(err, actual) { + test.ifError(err); + const expected = { "string": [] }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("markdownItPluginsMultiple", (test) => { + test.plan(4); + markdownlint({ + "strings": { + "string": "# Heading\n\nText H~2~0 text 29^th^ text\n" + }, + "markdownItPlugins": [ + [ pluginSub ], + [ pluginSup ], + [ pluginInline, "check_sub_plugin", "sub_open", test.ok ], + [ pluginInline, "check_sup_plugin", "sup_open", test.ok ] + ] + }, function callback(err, actual) { + test.ifError(err); + const expected = { "string": [] }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("markdownItPluginsMathjax", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "string": + "# Heading\n" + + "\n" + + "$1 *2* 3$\n" + + "\n" + + "$$1 *2* 3$$\n" + + "\n" + + "$$1\n" + + "+ 2\n" + + "+ 3$$\n" + }, + "markdownItPlugins": [ [ pluginKatex ] ] + }, function callback(err, actual) { + test.ifError(err); + const expected = { "string": [] }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("markdownItPluginsMathjaxIssue166", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "string": +`## Heading + +$$ +1 +$$$$ +2 +$$\n` + }, + "markdownItPlugins": [ [ pluginKatex ] ], + "resultVersion": 0 + }, function callback(err, actual) { + test.ifError(err); + const expected = { + "string": { + "MD041": [ 1 ] + } + }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); diff --git a/test/markdownlint-test-helpers.js b/test/markdownlint-test-helpers.js new file mode 100644 index 00000000..3598159d --- /dev/null +++ b/test/markdownlint-test-helpers.js @@ -0,0 +1,4087 @@ +// @ts-check + +"use strict"; + +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const { promisify } = require("util"); +const md = require("markdown-it")(); +const pluginInline = require("markdown-it-for-inline"); +const pluginKatex = require("@iktakahiro/markdown-it-katex"); +const pluginSub = require("markdown-it-sub"); +const pluginSup = require("markdown-it-sup"); +const tape = require("tape"); +require("tape-player"); +const tv4 = require("tv4"); +const packageJson = require("../package.json"); +const markdownlint = require("../lib/markdownlint"); +const helpers = require("../helpers"); +const rules = require("../lib/rules"); +const customRules = require("./rules/rules.js"); +const defaultConfig = require("./markdownlint-test-default-config.json"); +const configSchema = require("../schema/markdownlint-config-schema.json"); +const homepage = packageJson.homepage; +const version = packageJson.version; + +const deprecatedRuleNames = new Set([ "MD002", "MD006" ]); + +/** + * Create a test function for the specified test file. + * + * @param {string} file Test file relative path. + * @returns {Function} Test function. + */ +function createTestForFile(file) { + const markdownlintPromise = promisify(markdownlint); + return function testForFile(test) { + const detailedResults = /[/\\]detailed-results-/.test(file); + test.plan(detailedResults ? 3 : 2); + const resultsFile = file.replace(/\.md$/, ".results.json"); + const fixedFile = file.replace(/\.md$/, ".md.fixed"); + const configFile = file.replace(/\.md$/, ".json"); + let mergedConfig = null; + const actualPromise = fs.promises.stat(configFile) + .then( + function configFileExists() { + return fs.promises.readFile(configFile, helpers.utf8Encoding) + .then(JSON.parse); + }, + function noConfigFile() { + return {}; + }) + .then( + function lintWithConfig(config) { + mergedConfig = { + ...defaultConfig, + ...config + }; + return markdownlintPromise({ + "files": [ file ], + "config": mergedConfig, + "resultVersion": detailedResults ? 2 : 3 + }); + }) + .then( + function diffFixedFiles(resultVersion2or3) { + return detailedResults ? + Promise.all([ + markdownlintPromise({ + "files": [ file ], + "config": mergedConfig, + "resultVersion": 3 + }), + fs.promises.readFile(file, helpers.utf8Encoding), + fs.promises.readFile(fixedFile, helpers.utf8Encoding) + ]) + .then(function validateApplyFixes(fulfillments) { + const [ resultVersion3, content, expected ] = fulfillments; + const errors = resultVersion3[file]; + const actual = helpers.applyFixes(content, errors); + // Uncomment the following line to update *.md.fixed files + // fs.writeFileSync(fixedFile, actual, helpers.utf8Encoding); + test.equal(actual, expected, + "Unexpected output from applyFixes."); + return resultVersion2or3; + }) : + resultVersion2or3; + } + ) + .then( + function convertResultVersion2To0(resultVersion2or3) { + const result0 = {}; + const result2or3 = resultVersion2or3[file]; + result2or3.forEach(function forResult(result) { + const ruleName = result.ruleNames[0]; + const lineNumbers = result0[ruleName] || []; + if (!lineNumbers.includes(result.lineNumber)) { + lineNumbers.push(result.lineNumber); + } + result0[ruleName] = lineNumbers; + }); + return [ result0, result2or3 ]; + } + ); + const expectedPromise = detailedResults ? + fs.promises.readFile(resultsFile, helpers.utf8Encoding) + .then( + function fileContents(contents) { + // @ts-ignore + const errorObjects = JSON.parse(contents); + errorObjects.forEach(function forObject(errorObject) { + if (errorObject.ruleInformation) { + errorObject.ruleInformation = + errorObject.ruleInformation.replace("v0.0.0", `v${version}`); + } + }); + return errorObjects; + }) : + fs.promises.readFile(file, helpers.utf8Encoding) + .then( + function fileContents(contents) { + // @ts-ignore + const lines = contents.split(helpers.newLineRe); + const results = {}; + lines.forEach(function forLine(line, lineNum) { + const regex = /\{(MD\d+)(?::(\d+))?\}/g; + let match = null; + while ((match = regex.exec(line))) { + const rule = match[1]; + const errors = results[rule] || []; + errors.push( + match[2] ? + Number.parseInt(match[2], 10) : + lineNum + 1 + ); + results[rule] = errors; + } + }); + const sortedResults = {}; + Object.keys(results).sort().forEach(function forKey(key) { + sortedResults[key] = results[key]; + }); + return sortedResults; + }); + Promise.all([ actualPromise, expectedPromise ]) + .then( + function compareResults(fulfillments) { + const [ [ actual0, actual2or3 ], expected ] = fulfillments; + const actual = detailedResults ? actual2or3 : actual0; + test.deepEqual(actual, expected, "Line numbers are not correct."); + return actual2or3; + }) + .then( + function verifyFixes(errors) { + if (detailedResults) { + return test.ok(true); + } + return fs.promises.readFile(file, helpers.utf8Encoding) + .then( + function applyFixes(content) { + const corrections = helpers.applyFixes(content, errors); + return markdownlintPromise({ + "strings": { + "input": corrections + }, + "config": mergedConfig, + "resultVersion": 3 + }); + }) + .then( + function checkFixes(newErrors) { + const unfixed = newErrors.input + .filter((error) => !!error.fixInfo); + test.deepEqual(unfixed, [], "Fixable error was not fixed."); + } + ); + }) + .catch() + .then(test.done); + }; +} + +fs.readdirSync("./test") + .filter((file) => /\.md$/.test(file)) + // @ts-ignore + .forEach((file) => tape(file, createTestForFile(path.join("./test", file)))); + +tape("projectFilesNoInlineConfig", (test) => { + test.plan(2); + const options = { + "files": [ + "README.md", + "CONTRIBUTING.md", + "doc/CustomRules.md", + "helpers/README.md" + ], + "noInlineConfig": true, + "config": { + "line-length": { "line_length": 150 }, + "no-duplicate-heading": false + } + }; + markdownlint(options, function callback(err, actual) { + test.ifError(err); + const expected = { + "README.md": [], + "CONTRIBUTING.md": [], + "doc/CustomRules.md": [], + "helpers/README.md": [] + }; + test.deepEqual(actual, expected, "Issue(s) with project files."); + test.end(); + }); +}); + +tape("projectFilesInlineConfig", (test) => { + test.plan(2); + const options = { + "files": [ "doc/Rules.md" ], + "config": { + "line-length": { "line_length": 150 }, + "no-inline-html": false + } + }; + markdownlint(options, function callback(err, actual) { + test.ifError(err); + const expected = { + "doc/Rules.md": [] + }; + test.deepEqual(actual, expected, "Issue(s) with project files."); + test.end(); + }); +}); + +tape("resultObjectToStringNotEnumerable", (test) => { + test.plan(2); + const options = { + "strings": { + "string": "# Heading" + } + }; + markdownlint(options, function callback(err, result) { + test.ifError(err); + // eslint-disable-next-line guard-for-in + for (const property in result) { + test.notEqual(property, "toString", "Function should not enumerate."); + } + test.end(); + }); +}); + +tape("resultFormattingV0", (test) => { + test.plan(4); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": true, + "MD041": false + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD002": [ 3 ], + "MD018": [ 1 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": { + "MD002": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + let actualMessage = actualResult.toString(); + let expectedMessage = + "./test/atx_heading_spacing.md: 3: MD002" + + " First heading should be a top level heading\n" + + "./test/atx_heading_spacing.md: 1: MD018" + + " No space after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 3: MD019" + + " Multiple spaces after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 5: MD019" + + " Multiple spaces after hash on atx style heading\n" + + "./test/first_heading_bad_atx.md: 1: MD002" + + " First heading should be a top level heading"; + test.equal(actualMessage, expectedMessage, "Incorrect message (name)."); + // @ts-ignore + actualMessage = actualResult.toString(true); + expectedMessage = + "./test/atx_heading_spacing.md: 3: first-heading-h1" + + " First heading should be a top level heading\n" + + "./test/atx_heading_spacing.md: 1: no-missing-space-atx" + + " No space after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 3: no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 5: no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading\n" + + "./test/first_heading_bad_atx.md: 1: first-heading-h1" + + " First heading should be a top level heading"; + test.equal(actualMessage, expectedMessage, "Incorrect message (alias)."); + test.end(); + }); +}); + +tape("resultFormattingSyncV0", (test) => { + test.plan(3); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": true, + "MD041": false + }, + "resultVersion": 0 + }; + const actualResult = markdownlint.sync(options); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD002": [ 3 ], + "MD018": [ 1 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": { + "MD002": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + let actualMessage = actualResult.toString(); + let expectedMessage = + "./test/atx_heading_spacing.md: 3: MD002" + + " First heading should be a top level heading\n" + + "./test/atx_heading_spacing.md: 1: MD018" + + " No space after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 3: MD019" + + " Multiple spaces after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 5: MD019" + + " Multiple spaces after hash on atx style heading\n" + + "./test/first_heading_bad_atx.md: 1: MD002" + + " First heading should be a top level heading"; + test.equal(actualMessage, expectedMessage, "Incorrect message (name)."); + // @ts-ignore + actualMessage = actualResult.toString(true); + expectedMessage = + "./test/atx_heading_spacing.md: 3: first-heading-h1" + + " First heading should be a top level heading\n" + + "./test/atx_heading_spacing.md: 1: no-missing-space-atx" + + " No space after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 3: no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 5: no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading\n" + + "./test/first_heading_bad_atx.md: 1: first-heading-h1" + + " First heading should be a top level heading"; + test.equal(actualMessage, expectedMessage, "Incorrect message (alias)."); + test.end(); +}); + +tape("resultFormattingV1", (test) => { + test.plan(3); + const options = { + "strings": { + "truncate": + "# Multiple spaces inside hashes on closed atx style heading #\n" + }, + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": true, + "MD041": false + }, + "resultVersion": 1 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "truncate": [ + { "lineNumber": 1, + "ruleName": "MD021", + "ruleAlias": "no-multiple-space-closed-atx", + "ruleDescription": + "Multiple spaces inside hashes on closed atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md021`, + "errorDetail": null, + "errorContext": "# Multiple spa...tyle heading #", + "errorRange": [ 1, 4 ] } + ], + "./test/atx_heading_spacing.md": [ + { "lineNumber": 3, + "ruleName": "MD002", + "ruleAlias": "first-heading-h1", + "ruleDescription": "First heading should be a top level heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md002`, + "errorDetail": "Expected: h1; Actual: h2", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 1, + "ruleName": "MD018", + "ruleAlias": "no-missing-space-atx", + "ruleDescription": "No space after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md018`, + "errorDetail": null, + "errorContext": "#Heading 1 {MD018}", + "errorRange": [ 1, 2 ] }, + { "lineNumber": 3, + "ruleName": "MD019", + "ruleAlias": "no-multiple-space-atx", + "ruleDescription": "Multiple spaces after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md019`, + "errorDetail": null, + "errorContext": "## Heading 2 {MD019}", + "errorRange": [ 1, 5 ] }, + { "lineNumber": 5, + "ruleName": "MD019", + "ruleAlias": "no-multiple-space-atx", + "ruleDescription": "Multiple spaces after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md019`, + "errorDetail": null, + "errorContext": "## Heading 3 {MD019}", + "errorRange": [ 1, 6 ] } + ], + "./test/first_heading_bad_atx.md": [ + { "lineNumber": 1, + "ruleName": "MD002", + "ruleAlias": "first-heading-h1", + "ruleDescription": "First heading should be a top level heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md002`, + "errorDetail": "Expected: h1; Actual: h2", + "errorContext": null, + "errorRange": null } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "./test/atx_heading_spacing.md: 3: MD002/first-heading-h1" + + " First heading should be a top level heading" + + " [Expected: h1; Actual: h2]\n" + + "./test/atx_heading_spacing.md: 1: MD018/no-missing-space-atx" + + " No space after hash on atx style heading" + + " [Context: \"#Heading 1 {MD018}\"]\n" + + "./test/atx_heading_spacing.md: 3: MD019/no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading" + + " [Context: \"## Heading 2 {MD019}\"]\n" + + "./test/atx_heading_spacing.md: 5: MD019/no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading" + + " [Context: \"## Heading 3 {MD019}\"]\n" + + "./test/first_heading_bad_atx.md: 1: MD002/first-heading-h1" + + " First heading should be a top level heading" + + " [Expected: h1; Actual: h2]\n" + + "truncate: 1: MD021/no-multiple-space-closed-atx" + + " Multiple spaces inside hashes on closed atx style heading" + + " [Context: \"# Multiple spa...tyle heading #\"]"; + test.equal(actualMessage, expectedMessage, "Incorrect message."); + test.end(); + }); +}); + +tape("resultFormattingV2", (test) => { + test.plan(3); + const options = { + "strings": { + "truncate": + "# Multiple spaces inside hashes on closed atx style heading #\n" + }, + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": true, + "MD041": false + } + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "truncate": [ + { "lineNumber": 1, + "ruleNames": [ "MD021", "no-multiple-space-closed-atx" ], + "ruleDescription": + "Multiple spaces inside hashes on closed atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md021`, + "errorDetail": null, + "errorContext": "# Multiple spa...tyle heading #", + "errorRange": [ 1, 4 ] } + ], + "./test/atx_heading_spacing.md": [ + { "lineNumber": 3, + "ruleNames": [ "MD002", "first-heading-h1", "first-header-h1" ], + "ruleDescription": "First heading should be a top level heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md002`, + "errorDetail": "Expected: h1; Actual: h2", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 1, + "ruleNames": [ "MD018", "no-missing-space-atx" ], + "ruleDescription": "No space after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md018`, + "errorDetail": null, + "errorContext": "#Heading 1 {MD018}", + "errorRange": [ 1, 2 ] }, + { "lineNumber": 3, + "ruleNames": [ "MD019", "no-multiple-space-atx" ], + "ruleDescription": "Multiple spaces after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md019`, + "errorDetail": null, + "errorContext": "## Heading 2 {MD019}", + "errorRange": [ 1, 5 ] }, + { "lineNumber": 5, + "ruleNames": [ "MD019", "no-multiple-space-atx" ], + "ruleDescription": "Multiple spaces after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md019`, + "errorDetail": null, + "errorContext": "## Heading 3 {MD019}", + "errorRange": [ 1, 6 ] } + ], + "./test/first_heading_bad_atx.md": [ + { "lineNumber": 1, + "ruleNames": [ "MD002", "first-heading-h1", "first-header-h1" ], + "ruleDescription": "First heading should be a top level heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md002`, + "errorDetail": "Expected: h1; Actual: h2", + "errorContext": null, + "errorRange": null } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "./test/atx_heading_spacing.md: 3:" + + " MD002/first-heading-h1/first-header-h1" + + " First heading should be a top level heading" + + " [Expected: h1; Actual: h2]\n" + + "./test/atx_heading_spacing.md: 1: MD018/no-missing-space-atx" + + " No space after hash on atx style heading" + + " [Context: \"#Heading 1 {MD018}\"]\n" + + "./test/atx_heading_spacing.md: 3: MD019/no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading" + + " [Context: \"## Heading 2 {MD019}\"]\n" + + "./test/atx_heading_spacing.md: 5: MD019/no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading" + + " [Context: \"## Heading 3 {MD019}\"]\n" + + "./test/first_heading_bad_atx.md: 1:" + + " MD002/first-heading-h1/first-header-h1" + + " First heading should be a top level heading" + + " [Expected: h1; Actual: h2]\n" + + "truncate: 1: MD021/no-multiple-space-closed-atx" + + " Multiple spaces inside hashes on closed atx style heading" + + " [Context: \"# Multiple spa...tyle heading #\"]"; + test.equal(actualMessage, expectedMessage, "Incorrect message."); + test.end(); + }); +}); + +tape("resultFormattingV3", (test) => { + test.plan(3); + const options = { + "strings": { + "input": + "# Heading \n" + + "\n" + + "Text\ttext\t\ttext\n" + + "Text * emphasis * text" + }, + "resultVersion": 3 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { + "lineNumber": 1, + "ruleNames": [ "MD009", "no-trailing-spaces" ], + "ruleDescription": "Trailing spaces", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md009`, + "errorDetail": "Expected: 0 or 2; Actual: 3", + "errorContext": null, + "errorRange": [ 10, 3 ], + "fixInfo": { + "editColumn": 10, + "deleteCount": 3 + } + }, + { + "lineNumber": 3, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 5", + "errorContext": null, + "errorRange": [ 5, 1 ], + "fixInfo": { + "editColumn": 5, + "deleteCount": 1, + "insertText": " " + } + }, + { + "lineNumber": 3, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 10", + "errorContext": null, + "errorRange": [ 10, 2 ], + "fixInfo": { + "editColumn": 10, + "deleteCount": 2, + "insertText": " " + } + }, + { + "lineNumber": 4, + "ruleNames": [ "MD037", "no-space-in-emphasis" ], + "ruleDescription": "Spaces inside emphasis markers", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md037`, + "errorDetail": null, + "errorContext": "* emphasis *", + "errorRange": [ 6, 12 ], + "fixInfo": { + "editColumn": 6, + "deleteCount": 12, + "insertText": "*emphasis*" + } + }, + { + "lineNumber": 4, + "ruleNames": [ "MD047", "single-trailing-newline" ], + "ruleDescription": "Files should end with a single newline character", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md047`, + "errorDetail": null, + "errorContext": null, + "errorRange": [ 22, 1 ], + "fixInfo": { + "insertText": "\n", + "editColumn": 23 + } + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "input: 1: MD009/no-trailing-spaces" + + " Trailing spaces [Expected: 0 or 2; Actual: 3]\n" + + "input: 3: MD010/no-hard-tabs" + + " Hard tabs [Column: 5]\n" + + "input: 3: MD010/no-hard-tabs" + + " Hard tabs [Column: 10]\n" + + "input: 4: MD037/no-space-in-emphasis" + + " Spaces inside emphasis markers [Context: \"* emphasis *\"]\n" + + "input: 4: MD047/single-trailing-newline" + + " Files should end with a single newline character"; + test.equal(actualMessage, expectedMessage, "Incorrect message."); + test.end(); + }); +}); + +tape("onePerLineResultVersion0", (test) => { + test.plan(2); + const options = { + "strings": { + "input": "# Heading\theading\t\theading\n" + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": { + "MD010": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("onePerLineResultVersion1", (test) => { + test.plan(2); + const options = { + "strings": { + "input": "# Heading\theading\t\theading\n" + }, + "resultVersion": 1 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { + "lineNumber": 1, + "ruleName": "MD010", + "ruleAlias": "no-hard-tabs", + "ruleDescription": "Hard tabs", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 10", + "errorContext": null, + "errorRange": [ 10, 1 ] + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("onePerLineResultVersion2", (test) => { + test.plan(2); + const options = { + "strings": { + "input": "# Heading\theading\t\theading\n" + }, + "resultVersion": 2 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { + "lineNumber": 1, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 10", + "errorContext": null, + "errorRange": [ 10, 1 ] + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("manyPerLineResultVersion3", (test) => { + test.plan(2); + const options = { + "strings": { + "input": "# Heading\theading\t\theading\n" + }, + "resultVersion": 3 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { + "lineNumber": 1, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 10", + "errorContext": null, + "errorRange": [ 10, 1 ], + "fixInfo": { + "editColumn": 10, + "deleteCount": 1, + "insertText": " " + } + }, + { + "lineNumber": 1, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 18", + "errorContext": null, + "errorRange": [ 18, 2 ], + "fixInfo": { + "editColumn": 18, + "deleteCount": 2, + "insertText": " " + } + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("frontMatterResultVersion3", (test) => { + test.plan(2); + const options = { + "strings": { + "input": "---\n---\n# Heading\nText\n" + }, + "resultVersion": 3 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { + "lineNumber": 3, + "ruleNames": + [ "MD022", "blanks-around-headings", "blanks-around-headers" ], + "ruleDescription": "Headings should be surrounded by blank lines", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md022`, + "errorDetail": "Expected: 1; Actual: 0; Below", + "errorContext": "# Heading", + "errorRange": null, + "fixInfo": { + "lineNumber": 4, + "insertText": "\n" + } + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("stringInputLineEndings", (test) => { + test.plan(2); + const options = { + "strings": { + "cr": "One\rTwo\r#Three\n", + "lf": "One\nTwo\n#Three\n", + "crlf": "One\r\nTwo\r\n#Three\n", + "mixed": "One\rTwo\n#Three\n" + }, + "config": defaultConfig, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "cr": { "MD018": [ 3 ] }, + "lf": { "MD018": [ 3 ] }, + "crlf": { "MD018": [ 3 ] }, + "mixed": { "MD018": [ 3 ] } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("inputOnlyNewline", (test) => { + test.plan(2); + const options = { + "strings": { + "cr": "\r", + "lf": "\n", + "crlf": "\r\n" + }, + "config": { + "default": false + } + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "cr": [], + "lf": [], + "crlf": [] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("defaultTrue", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "default": true + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD018": [ 1 ], + "MD019": [ 3, 5 ], + "MD041": [ 1 ] + }, + "./test/first_heading_bad_atx.md": { + "MD041": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("defaultFalse", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "default": false + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": {}, + "./test/first_heading_bad_atx.md": {} + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("defaultUndefined", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": {}, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD018": [ 1 ], + "MD019": [ 3, 5 ], + "MD041": [ 1 ] + }, + "./test/first_heading_bad_atx.md": { + "MD041": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("disableRules", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": false, + "default": true, + "MD019": false, + "first-line-h1": false + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD018": [ 1 ] + }, + "./test/first_heading_bad_atx.md": {} + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("enableRules", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": true, + "default": false, + "no-multiple-space-atx": true + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD002": [ 3 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": { + "MD002": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("enableRulesMixedCase", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "Md002": true, + "DeFaUlT": false, + "nO-mUlTiPlE-sPaCe-AtX": true + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD002": [ 3 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": { + "MD002": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("disableTag", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "default": true, + "spaces": false + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD041": [ 1 ] + }, + "./test/first_heading_bad_atx.md": { + "MD041": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("enableTag", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "default": false, + "spaces": true, + "notatag": true + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD018": [ 1 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": {} + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("enableTagMixedCase", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "DeFaUlT": false, + "SpAcEs": true, + "NoTaTaG": true + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD018": [ 1 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": {} + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("styleFiles", (test) => { + test.plan(4); + fs.readdir("./style", function readdir(err, files) { + test.ifError(err); + files.forEach(function forFile(file) { + test.ok(require(path.join("../style", file)), "Unable to load/parse."); + }); + test.end(); + }); +}); + +tape("styleAll", (test) => { + test.plan(2); + const options = { + "files": [ "./test/break-all-the-rules.md" ], + "config": require("../style/all.json"), + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/break-all-the-rules.md": { + "MD001": [ 3 ], + "MD003": [ 5, 31 ], + "MD004": [ 8 ], + "MD005": [ 12 ], + "MD007": [ 8, 11 ], + "MD009": [ 14 ], + "MD010": [ 14 ], + "MD011": [ 16 ], + "MD012": [ 18 ], + "MD013": [ 21 ], + "MD014": [ 23 ], + "MD018": [ 25 ], + "MD019": [ 27 ], + "MD020": [ 29 ], + "MD021": [ 31 ], + "MD022": [ 86 ], + "MD023": [ 40 ], + "MD024": [ 35 ], + "MD026": [ 40 ], + "MD027": [ 42 ], + "MD028": [ 43 ], + "MD029": [ 47 ], + "MD030": [ 8 ], + "MD031": [ 50 ], + "MD032": [ 7, 8, 51 ], + "MD033": [ 55 ], + "MD034": [ 57 ], + "MD035": [ 61 ], + "MD036": [ 65 ], + "MD037": [ 67 ], + "MD038": [ 69 ], + "MD039": [ 71 ], + "MD040": [ 73 ], + "MD041": [ 1 ], + "MD042": [ 81 ], + "MD045": [ 85 ], + "MD046": [ 49, 73, 77 ], + "MD047": [ 88 ], + "MD048": [ 77 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("styleRelaxed", (test) => { + test.plan(2); + const options = { + "files": [ "./test/break-all-the-rules.md" ], + "config": require("../style/relaxed.json"), + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/break-all-the-rules.md": { + "MD001": [ 3 ], + "MD003": [ 5, 31 ], + "MD004": [ 8 ], + "MD005": [ 12 ], + "MD011": [ 16 ], + "MD014": [ 23 ], + "MD018": [ 25 ], + "MD019": [ 27 ], + "MD020": [ 29 ], + "MD021": [ 31 ], + "MD022": [ 86 ], + "MD023": [ 40 ], + "MD024": [ 35 ], + "MD026": [ 40 ], + "MD029": [ 47 ], + "MD031": [ 50 ], + "MD032": [ 7, 8, 51 ], + "MD035": [ 61 ], + "MD036": [ 65 ], + "MD042": [ 81 ], + "MD045": [ 85 ], + "MD046": [ 49, 73, 77 ], + "MD047": [ 88 ], + "MD048": [ 77 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("nullFrontMatter", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "content": "---\n\t\n---\n# Heading\n" + }, + "frontMatter": null, + "config": { + "default": false, + "MD010": true + }, + "resultVersion": 0 + }, function callback(err, result) { + test.ifError(err); + const expectedResult = { + "content": { "MD010": [ 2 ] } + }; + test.deepEqual(result, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customFrontMatter", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "content": "\n\t\n\n# Heading\n" + }, + "frontMatter": /[^]*<\/head>/, + "config": { + "default": false, + "MD010": true + } + }, function callback(err, result) { + test.ifError(err); + const expectedResult = { + "content": [] + }; + test.deepEqual(result, expectedResult, "Did not get empty results."); + test.end(); + }); +}); + +tape("noInlineConfig", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "content": [ + "# Heading", + "", + "\tTab", + "", + "", + "", + "\tTab", + "", + "", + "", + "\tTab\n" + ].join("\n") + }, + "noInlineConfig": true, + "resultVersion": 0 + }, function callback(err, result) { + test.ifError(err); + const expectedResult = { + "content": { + "MD010": [ 3, 7, 11 ] + } + }; + test.deepEqual(result, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("readmeHeadings", (test) => { + test.plan(2); + markdownlint({ + "files": "README.md", + "noInlineConfig": true, + "config": { + "default": false, + "MD013": { + "line_length": 150 + }, + "MD043": { + "headings": [ + "# markdownlint", + "## Install", + "## Overview", + "### Related", + "## Demonstration", + "## Rules / Aliases", + "## Tags", + "## Configuration", + "## API", + "### Linting", + "#### options", + "##### options.customRules", + "##### options.files", + "##### options.strings", + "##### options.config", + "##### options.frontMatter", + "##### options.handleRuleFailures", + "##### options.noInlineConfig", + "##### options.resultVersion", + "##### options.markdownItPlugins", + "#### callback", + "#### result", + "### Config", + "#### file", + "#### parsers", + "#### callback", + "#### result", + "## Usage", + "## Browser", + "## Examples", + "## Contributing", + "## History" + ] + } + } + }, function callback(err, result) { + test.ifError(err); + const expected = { "README.md": [] }; + test.deepEqual(result, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("filesArrayNotModified", (test) => { + test.plan(2); + const files = [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ]; + const expectedFiles = files.slice(); + markdownlint({ "files": files }, function callback(err) { + test.ifError(err); + test.deepEqual(files, expectedFiles, "Files modified."); + test.end(); + }); +}); + +tape("filesArrayAsString", (test) => { + test.plan(2); + markdownlint({ + "files": "README.md", + "noInlineConfig": true, + "config": { + "MD013": { "line_length": 150 }, + "MD024": false + } + }, function callback(err, actual) { + test.ifError(err); + const expected = { "README.md": [] }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("missingOptions", (test) => { + test.plan(2); + markdownlint(null, function callback(err, result) { + test.ifError(err); + test.deepEqual( + result, + {}, + "Did not get empty result for missing options." + ); + test.end(); + }); +}); + +tape("missingFilesAndStrings", (test) => { + test.plan(2); + markdownlint({}, function callback(err, result) { + test.ifError(err); + test.ok(result, "Did not get result for missing files/strings."); + test.end(); + }); +}); + +tape("missingCallback", (test) => { + test.plan(0); + // @ts-ignore + markdownlint(); + test.end(); +}); + +tape("badFile", (test) => { + test.plan(4); + markdownlint({ + "files": [ "./badFile" ] + }, function callback(err, result) { + test.ok(err, "Did not get an error for bad file."); + test.ok(err instanceof Error, "Error not instance of Error."); + // @ts-ignore + test.equal(err.code, "ENOENT", "Error code for bad file not ENOENT."); + test.ok(!result, "Got result for bad file."); + test.end(); + }); +}); + +tape("badFileSync", (test) => { + test.plan(1); + test.throws( + function badFileCall() { + markdownlint.sync({ + "files": [ "./badFile" ] + }); + }, + /ENOENT/, + "Did not get correct exception for bad file." + ); + test.end(); +}); + +tape("missingStringValue", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "undefined": undefined, + "null": null, + "empty": "" + }, + "config": defaultConfig + }, function callback(err, result) { + test.ifError(err); + const expectedResult = { + "undefined": [], + "null": [], + "empty": [] + }; + test.deepEqual(result, expectedResult, "Did not get empty results."); + test.end(); + }); +}); + +tape("readme", (test) => { + test.plan(115); + const tagToRules = {}; + rules.forEach(function forRule(rule) { + rule.tags.forEach(function forTag(tag) { + const tagRules = tagToRules[tag] || []; + tagRules.push(rule.names[0]); + tagToRules[tag] = tagRules; + }); + }); + fs.readFile("README.md", helpers.utf8Encoding, + function readFile(err, contents) { + test.ifError(err); + const rulesLeft = rules.slice(); + let seenRelated = false; + let seenRules = false; + let inRules = false; + let seenTags = false; + let inTags = false; + md.parse(contents, {}).forEach(function forToken(token) { + if ( + (token.type === "bullet_list_open") && + (token.level === 0) + ) { + if (!seenRelated) { + seenRelated = true; + } else if (seenRelated && !seenRules) { + seenRules = true; + inRules = true; + } else if (seenRelated && seenRules && !seenTags) { + seenTags = true; + inTags = true; + } + } else if ( + (token.type === "bullet_list_close") && + (token.level === 0) + ) { + inRules = false; + inTags = false; + } else if (token.type === "inline") { + if (inRules) { + const rule = rulesLeft.shift(); + test.ok(rule, + "Missing rule implementation for " + token.content + "."); + if (rule) { + const ruleName = rule.names[0]; + const ruleAliases = rule.names.slice(1); + let expected = "**[" + ruleName + "](doc/Rules.md#" + + ruleName.toLowerCase() + ")** *" + + ruleAliases.join("/") + "* - " + rule.description; + if (deprecatedRuleNames.has(ruleName)) { + expected = "~~" + expected + "~~"; + } + test.equal(token.content, expected, "Rule mismatch."); + } + } else if (inTags) { + const parts = + token.content.replace(/\*\*/g, "").split(/ - |, |,\n/); + const tag = parts.shift(); + test.deepEqual(parts, tagToRules[tag] || [], + "Rule mismatch for tag " + tag + "."); + delete tagToRules[tag]; + } + } + }); + const ruleLeft = rulesLeft.shift(); + test.ok(!ruleLeft, + "Missing rule documentation for " + + (ruleLeft || "[NO RULE]").toString() + "."); + const tagLeft = Object.keys(tagToRules).shift(); + test.ok(!tagLeft, "Undocumented tag " + tagLeft + "."); + test.end(); + }); +}); + +tape("rules", (test) => { + test.plan(336); + fs.readFile("doc/Rules.md", helpers.utf8Encoding, + (err, contents) => { + test.ifError(err); + const rulesLeft = rules.slice(); + let inHeading = false; + let rule = null; + let ruleHasTags = true; + let ruleHasAliases = true; + let ruleUsesParams = null; + const tagAliasParameterRe = /, |: | /; + // eslint-disable-next-line func-style + const testTagsAliasesParams = (r) => { + r = r || "[NO RULE]"; + test.ok(ruleHasTags, + "Missing tags for rule " + r.names + "."); + test.ok(ruleHasAliases, + "Missing aliases for rule " + r.names + "."); + test.ok(!ruleUsesParams, + "Missing parameters for rule " + r.names + "."); + }; + md.parse(contents, {}).forEach(function forToken(token) { + if ((token.type === "heading_open") && (token.tag === "h2")) { + inHeading = true; + } else if (token.type === "heading_close") { + inHeading = false; + } else if (token.type === "inline") { + if (inHeading) { + testTagsAliasesParams(rule); + rule = rulesLeft.shift(); + ruleHasTags = false; + ruleHasAliases = false; + test.ok(rule, + "Missing rule implementation for " + token.content + "."); + const ruleName = rule.names[0]; + let headingContent = ruleName + " - " + rule.description; + if (deprecatedRuleNames.has(ruleName)) { + headingContent = "~~" + headingContent + "~~"; + } + test.equal(token.content, + headingContent, + "Rule mismatch."); + ruleUsesParams = rule.function.toString() + .match(/params\.config\.[_a-z]*/gi); + if (ruleUsesParams) { + ruleUsesParams = ruleUsesParams.map(function forUse(use) { + return use.split(".").pop(); + }); + ruleUsesParams.sort(); + } + } else if (token.content.startsWith("Tags: ") && rule) { + test.deepEqual(token.content.split(tagAliasParameterRe).slice(1), + rule.tags, "Tag mismatch for rule " + rule.names + "."); + ruleHasTags = true; + } else if (token.content.startsWith("Aliases: ") && rule) { + test.deepEqual(token.content.split(tagAliasParameterRe).slice(1), + rule.names.slice(1), + "Alias mismatch for rule " + rule.names + "."); + ruleHasAliases = true; + } else if (token.content.startsWith("Parameters: ") && rule) { + let inDetails = false; + const parameters = token.content.split(tagAliasParameterRe) + .slice(1) + .filter(function forPart(part) { + inDetails = inDetails || (part[0] === "("); + return !inDetails; + }); + parameters.sort(); + test.deepEqual(parameters, ruleUsesParams, + "Missing parameter for rule " + rule.names); + ruleUsesParams = null; + } + } + }); + const ruleLeft = rulesLeft.shift(); + test.ok(!ruleLeft, + "Missing rule documentation for " + + (ruleLeft || { "names": "[NO RULE]" }).names + "."); + if (rule) { + testTagsAliasesParams(rule); + } + test.end(); + }); +}); + +tape("validateConfigSchema", (test) => { + const jsonFileRe = /\.json$/i; + const resultsFileRe = /\.results\.json$/i; + const jsConfigFileRe = /^jsconfig\.json$/i; + const wrongTypesFileRe = /wrong-types-in-config-file.json$/i; + const testDirectory = __dirname; + const testFiles = fs.readdirSync(testDirectory); + testFiles.filter(function filterFile(file) { + return jsonFileRe.test(file) && + !resultsFileRe.test(file) && + !jsConfigFileRe.test(file) && + !wrongTypesFileRe.test(file); + }).forEach(function forFile(file) { + const data = fs.readFileSync( + path.join(testDirectory, file), + helpers.utf8Encoding + ); + test.ok( + tv4.validate(JSON.parse(data), configSchema), + file + "\n" + JSON.stringify(tv4.error, null, 2)); + }); + test.end(); +}); + +tape("clearHtmlCommentTextValid", (test) => { + test.plan(1); + const validComments = [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "texttext", + "texttext", + "texttext", + "", + "texttexttext", + "texttext", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "texttext", + "texttext", + "texttext", + "", + "texttexttext", + "texttext", + "", + "", + "-->", + "-->", + "", + "", + "", + "-->", + " -->", + "-->", + "text-->", + "text-->", + "", + "" + ]; + const actual = helpers.clearHtmlCommentText(invalidComments.join("\n")); + const expected = invalidComments.join("\n"); + test.equal(actual, expected); + test.end(); +}); + +tape("clearHtmlCommentTextNonGreedy", (test) => { + test.plan(1); + const nonGreedyComments = [ + " -->", + " -->", + " -->", + " -->" + ]; + const nonGreedyResult = [ + " -->", + " -->", + " -->", + " -->" + ]; + const actual = helpers.clearHtmlCommentText(nonGreedyComments.join("\n")); + const expected = nonGreedyResult.join("\n"); + test.equal(actual, expected); + test.end(); +}); + +tape("clearHtmlCommentTextEmbedded", (test) => { + test.plan(1); + const embeddedComments = [ + "texttext", + "", + "texttext", + "texttext", + "texttext" + ]; + const embeddedResult = [ + "texttext", + "", + "texttext", + "texttext", + "texttext" + ]; + const actual = helpers.clearHtmlCommentText(embeddedComments.join("\n")); + const expected = embeddedResult.join("\n"); + test.equal(actual, expected); + test.end(); +}); + +tape("unescapeMarkdown", (test) => { + test.plan(7); + // Test cases from https://spec.commonmark.org/0.29/#backslash-escapes + const testCases = [ + [ + "\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;" + + "\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~", + "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" + ], + [ + "\\→\\A\\a\\ \\3\\φ\\«", + "\\→\\A\\a\\ \\3\\φ\\«" + ], + [ + `\\*not emphasized* +\\
not a tag +\\[not a link](/foo) +\\\`not code\` +1\\. not a list +\\* not a list +\\# not a heading +\\[foo]: /url "not a reference" +\\ö not a character entity`, + `*not emphasized* +
not a tag +[not a link](/foo) +\`not code\` +1. not a list +* not a list +# not a heading +[foo]: /url "not a reference" +ö not a character entity` + ], + [ + "\\\\*emphasis*", + "\\*emphasis*" + ], + [ + `foo\\ +bar`, + `foo\\ +bar` + ], + [ + "Text \\<", + "Text _", + "_" + ], + [ + "Text \\\\<", + "Text _<", + "_" + ] + ]; + testCases.forEach(function forTestCase(testCase) { + const [ markdown, expected, replacement ] = testCase; + const actual = helpers.unescapeMarkdown(markdown, replacement); + test.equal(actual, expected); + }); + test.end(); +}); + +tape("isBlankLine", (test) => { + test.plan(25); + const blankLines = [ + null, + "", + " ", + " ", + "\t\t\t", + "\r", + "\n", + "\t\r\n", + " ", + "", + "", + "\t", + ">", + "> ", + "> > > \t", + "> ", + ">>" + ]; + blankLines.forEach((line) => test.ok(helpers.isBlankLine(line), line)); + const nonBlankLines = [ + "text", + " text ", + ".", + "> .", + " text", + "", + "" + ]; + nonBlankLines.forEach((line) => test.ok(!helpers.isBlankLine(line), line)); + test.end(); +}); + +tape("includesSorted", (test) => { + test.plan(154); + const inputs = [ + [ ], + [ 8 ], + [ 7, 11 ], + [ 0, 1, 2, 3, 5, 8, 13 ], + [ 2, 3, 5, 7, 11, 13, 17, 19 ], + [ 1, 3, 5, 7, 9, 11, 13, 15, 17, 19 ], + [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 ] + ]; + inputs.forEach((input) => { + for (let i = 0; i <= 21; i++) { + test.equal(helpers.includesSorted(input, i), input.includes(i)); + } + }); + test.end(); +}); + +tape("forEachInlineCodeSpan", (test) => { + test.plan(99); + const testCases = + [ + { + "input": "`code`", + "expecteds": [ [ "code", 0, 1, 1 ] ] + }, + { + "input": "text `code` text", + "expecteds": [ [ "code", 0, 6, 1 ] ] + }, + { + "input": "text `code` text `edoc`", + "expecteds": [ + [ "code", 0, 6, 1 ], + [ "edoc", 0, 18, 1 ] + ] + }, + { + "input": "text `code` text `edoc` text", + "expecteds": [ + [ "code", 0, 6, 1 ], + [ "edoc", 0, 18, 1 ] + ] + }, + { + "input": "text ``code`code`` text", + "expecteds": [ [ "code`code", 0, 7, 2 ] ] + }, + { + "input": "`code `` code`", + "expecteds": [ [ "code `` code", 0, 1, 1 ] ] + }, + { + "input": "`code\\`text`", + "expecteds": [ [ "code\\", 0, 1, 1 ] ] + }, + { + "input": "``\ncode\n``", + "expecteds": [ [ "\ncode\n", 0, 2, 2 ] ] + }, + { + "input": "text\n`code`\ntext", + "expecteds": [ [ "code", 1, 1, 1 ] ] + }, + { + "input": "text\ntext\n`code`\ntext\n`edoc`\ntext", + "expecteds": [ + [ "code", 2, 1, 1 ], + [ "edoc", 4, 1, 1 ] + ] + }, + { + "input": "text `code\nedoc` text", + "expecteds": [ [ "code\nedoc", 0, 6, 1 ] ] + }, + { + "input": "> text `code` text", + "expecteds": [ [ "code", 0, 8, 1 ] ] + }, + { + "input": "> text\n> `code`\n> text", + "expecteds": [ [ "code", 1, 3, 1 ] ] + }, + { + "input": "> text\n> `code\n> edoc`\n> text", + "expecteds": [ [ "code\n> edoc", 1, 3, 1 ] ] + }, + { + "input": "```text``", + "expecteds": [] + }, + { + "input": "text `text text", + "expecteds": [] + }, + { + "input": "`text``code``", + "expecteds": [ [ "code", 0, 7, 2 ] ] + }, + { + "input": "text \\` text `code`", + "expecteds": [ [ "code", 0, 14, 1 ] ] + }, + { + "input": "text\\\n`code`", + "expecteds": [ [ "code", 1, 1, 1 ] ] + } + ]; + testCases.forEach((testCase) => { + const { input, expecteds } = testCase; + helpers.forEachInlineCodeSpan(input, (code, line, column, ticks) => { + const [ expectedCode, expectedLine, expectedColumn, expectedTicks ] = + expecteds.shift(); + test.equal(code, expectedCode, input); + test.equal(line, expectedLine, input); + test.equal(column, expectedColumn, input); + test.equal(ticks, expectedTicks, input); + }); + test.equal(expecteds.length, 0, "length"); + }); + test.end(); +}); + +tape("getPreferredLineEnding", (test) => { + test.plan(17); + const testCases = [ + [ "", os.EOL ], + [ "\r", "\r" ], + [ "\n", "\n" ], + [ "\r\n", "\r\n" ], + [ "t\rt\nt", "\n" ], + [ "t\nt\rt", "\n" ], + [ "t\r\nt\nt", "\n" ], + [ "t\nt\r\nt", "\n" ], + [ "t\r\nt\rt", "\r\n" ], + [ "t\rt\r\nt", "\r\n" ], + [ "t\r\nt\rt\nt", "\n" ], + [ "t\r\nt\r\nt\r\nt", "\r\n" ], + [ "t\nt\nt\nt", "\n" ], + [ "t\rt\rt\rt", "\r" ], + [ "t\r\nt\nt\r\nt", "\r\n" ], + [ "t\nt\r\nt\nt", "\n" ], + [ "t\rt\t\rt", "\r" ] + ]; + testCases.forEach((testCase) => { + const [ input, expected ] = testCase; + const actual = helpers.getPreferredLineEnding(input); + test.equal(actual, expected, "Incorrect line ending returned."); + }); + test.end(); +}); + +tape("applyFix", (test) => { + test.plan(4); + const testCases = [ + [ + "Hello world.", + { + "editColumn": 12, + "deleteCount": 1 + }, + undefined, + "Hello world" + ], + [ + "Hello world.", + { + "editColumn": 13, + "insertText": "\n" + }, + undefined, + "Hello world.\n" + ], + [ + "Hello world.", + { + "editColumn": 13, + "insertText": "\n" + }, + "\n", + "Hello world.\n" + ], + [ + "Hello world.", + { + "editColumn": 13, + "insertText": "\n" + }, + "\r\n", + "Hello world.\r\n" + ] + ]; + testCases.forEach((testCase) => { + const [ line, fixInfo, lineEnding, expected ] = testCase; + // @ts-ignore + const actual = helpers.applyFix(line, fixInfo, lineEnding); + test.equal(actual, expected, "Incorrect fix applied."); + }); + test.end(); +}); + +tape("applyFixes", (test) => { + test.plan(28); + const testCases = [ + [ + "Hello world.", + [], + "Hello world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": {} + } + ], + "Hello world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "insertText": "Very " + } + } + ], + "Very Hello world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 7, + "insertText": "big " + } + } + ], + "Hello big world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "deleteCount": 6 + } + } + ], + "world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 7, + "deleteCount": 5, + "insertText": "there" + } + } + ], + "Hello there." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 12, + "deleteCount": 1 + } + }, + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 6, + "deleteCount": 1 + } + } + ], + "Helloworld" + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 13, + "insertText": " Hi." + } + } + ], + "Hello world. Hi." + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 1, + "fixInfo": { + "deleteCount": -1 + } + } + ], + "world" + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 2, + "fixInfo": { + "deleteCount": -1 + } + } + ], + "Hello" + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 2, + "fixInfo": { + "lineNumber": 1, + "deleteCount": -1 + } + } + ], + "world" + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 1, + "fixInfo": { + "lineNumber": 2, + "deleteCount": -1 + } + } + ], + "Hello" + ], + [ + "Hello world", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 4, + "deleteCount": 1 + } + }, + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 10, + "deleteCount": 1 + } + } + ], + "Helo word" + ], + [ + "Hello world", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 10, + "deleteCount": 1 + } + }, + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 4, + "deleteCount": 1 + } + } + ], + "Helo word" + ], + [ + "Hello\nworld", + [ + { + "fixInfo": { + "lineNumber": 1, + "deleteCount": -1 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "insertText": "Big " + } + } + ], + "world" + ], + [ + "Hello\nworld", + [ + { + "fixInfo": { + "lineNumber": 1, + "deleteCount": -1 + } + }, + { + "fixInfo": { + "lineNumber": 2, + "deleteCount": -1 + } + } + ], + "" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "insertText": "aa" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "insertText": "b" + } + } + ], + "aaHello world" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "insertText": "a" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "insertText": "bb" + } + } + ], + "bbHello world" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 6, + "insertText": " big" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1 + } + } + ], + "Hello big orld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 8, + "deleteCount": 2 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 2 + } + } + ], + "Hello wld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 2 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 8, + "deleteCount": 2 + } + } + ], + "Hello wld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1, + "insertText": "z" + } + } + ], + "Hello zorld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "insertText": "z" + } + } + ], + "Hello zorld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "insertText": "z" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1 + } + } + ], + "Hello zorld" + ], + [ + "Hello\nworld\nhello\rworld", + [ + { + "fixInfo": { + "lineNumber": 4, + "editColumn": 6, + "insertText": "\n" + } + } + ], + "Hello\nworld\nhello\nworld\n" + ], + [ + "Hello\r\nworld\r\nhello\nworld", + [ + { + "fixInfo": { + "lineNumber": 4, + "editColumn": 6, + "insertText": "\n" + } + } + ], + "Hello\r\nworld\r\nhello\r\nworld\r\n" + ], + [ + "Hello\rworld\rhello\nworld", + [ + { + "fixInfo": { + "lineNumber": 4, + "editColumn": 6, + "insertText": "\n" + } + } + ], + "Hello\rworld\rhello\rworld\r" + ], + [ + "Hello\r\nworld", + [ + { + "lineNumber": 2, + "fixInfo": { + "editColumn": 6, + "insertText": "\n\n" + } + } + ], + "Hello\r\nworld\r\n\r\n" + ] + ]; + testCases.forEach((testCase) => { + const [ input, errors, expected ] = testCase; + const actual = helpers.applyFixes(input, errors); + test.equal(actual, expected, "Incorrect fix applied."); + }); + test.end(); +}); + +tape("configSingle", (test) => { + test.plan(2); + markdownlint.readConfig("./test/config/config-child.json", + function callback(err, actual) { + test.ifError(err); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configAbsolute", (test) => { + test.plan(2); + markdownlint.readConfig(path.join(__dirname, "config", "config-child.json"), + function callback(err, actual) { + test.ifError(err); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configMultiple", (test) => { + test.plan(2); + markdownlint.readConfig("./test/config/config-grandparent.json", + function callback(err, actual) { + test.ifError(err); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configBadFile", (test) => { + test.plan(4); + markdownlint.readConfig("./test/config/config-badfile.json", + function callback(err, result) { + test.ok(err, "Did not get an error for bad file."); + test.ok(err instanceof Error, "Error not instance of Error."); + // @ts-ignore + test.equal(err.code, "ENOENT", "Error code for bad file not ENOENT."); + test.ok(!result, "Got result for bad file."); + test.end(); + }); +}); + +tape("configBadChildFile", (test) => { + test.plan(4); + markdownlint.readConfig("./test/config/config-badchildfile.json", + function callback(err, result) { + test.ok(err, "Did not get an error for bad child file."); + test.ok(err instanceof Error, "Error not instance of Error."); + // @ts-ignore + test.equal(err.code, "ENOENT", + "Error code for bad child file not ENOENT."); + test.ok(!result, "Got result for bad child file."); + test.end(); + }); +}); + +tape("configBadJson", (test) => { + test.plan(3); + markdownlint.readConfig("./test/config/config-badjson.json", + function callback(err, result) { + test.ok(err, "Did not get an error for bad JSON."); + test.ok(err instanceof Error, "Error not instance of Error."); + test.ok(!result, "Got result for bad JSON."); + test.end(); + }); +}); + +tape("configBadChildJson", (test) => { + test.plan(3); + markdownlint.readConfig("./test/config/config-badchildjson.json", + function callback(err, result) { + test.ok(err, "Did not get an error for bad child JSON."); + test.ok(err instanceof Error, "Error not instance of Error."); + test.ok(!result, "Got result for bad child JSON."); + test.end(); + }); +}); + +tape("configSingleYaml", (test) => { + test.plan(2); + markdownlint.readConfig( + "./test/config/config-child.yaml", + [ require("js-yaml").safeLoad ], + function callback(err, actual) { + test.ifError(err); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configMultipleYaml", (test) => { + test.plan(2); + markdownlint.readConfig( + "./test/config/config-grandparent.yaml", + [ require("js-yaml").safeLoad ], + function callback(err, actual) { + test.ifError(err); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configMultipleHybrid", (test) => { + test.plan(2); + markdownlint.readConfig( + "./test/config/config-grandparent-hybrid.yaml", + [ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ], + function callback(err, actual) { + test.ifError(err); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepLooseEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configBadHybrid", (test) => { + test.plan(4); + markdownlint.readConfig( + "./test/config/config-badcontent.txt", + [ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ], + function callback(err, result) { + test.ok(err, "Did not get an error for bad child JSON."); + test.ok(err instanceof Error, "Error not instance of Error."); + test.ok(err.message.match( + // eslint-disable-next-line max-len + /^Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+; Expected [^;]+ or end of input but "\S+" found.; end of the stream or a document separator is expected at line \d+, column \d+:[^;]*$/ + ), "Error message unexpected."); + test.ok(!result, "Got result for bad child JSON."); + test.end(); + }); +}); + +tape("configSingleSync", (test) => { + test.plan(1); + const actual = markdownlint.readConfigSync("./test/config/config-child.json"); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configAbsoluteSync", (test) => { + test.plan(1); + const actual = markdownlint.readConfigSync( + path.join(__dirname, "config", "config-child.json")); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configMultipleSync", (test) => { + test.plan(1); + const actual = + markdownlint.readConfigSync("./test/config/config-grandparent.json"); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configBadFileSync", (test) => { + test.plan(1); + test.throws( + function badFileCall() { + markdownlint.readConfigSync("./test/config/config-badfile.json"); + }, + /ENOENT/, + "Did not get correct exception for bad file." + ); + test.end(); +}); + +tape("configBadChildFileSync", (test) => { + test.plan(1); + test.throws( + function badChildFileCall() { + markdownlint.readConfigSync("./test/config/config-badchildfile.json"); + }, + /ENOENT/, + "Did not get correct exception for bad child file." + ); + test.end(); +}); + +tape("configBadJsonSync", (test) => { + test.plan(1); + test.throws( + function badJsonCall() { + markdownlint.readConfigSync("./test/config/config-badjson.json"); + }, + /Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+/, + "Did not get correct exception for bad JSON." + ); + test.end(); +}); + +tape("configBadChildJsonSync", (test) => { + test.plan(1); + test.throws( + function badChildJsonCall() { + markdownlint.readConfigSync("./test/config/config-badchildjson.json"); + }, + /Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+/, + "Did not get correct exception for bad child JSON." + ); + test.end(); +}); + +tape("configSingleYamlSync", (test) => { + test.plan(1); + const actual = markdownlint.readConfigSync( + "./test/config/config-child.yaml", [ require("js-yaml").safeLoad ]); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configMultipleYamlSync", (test) => { + test.plan(1); + const actual = markdownlint.readConfigSync( + "./test/config/config-grandparent.yaml", [ require("js-yaml").safeLoad ]); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configMultipleHybridSync", (test) => { + test.plan(1); + const actual = markdownlint.readConfigSync( + "./test/config/config-grandparent-hybrid.yaml", + [ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ]); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepLooseEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configBadHybridSync", (test) => { + test.plan(1); + test.throws( + function badHybridCall() { + markdownlint.readConfigSync( + "./test/config/config-badcontent.txt", + [ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ]); + }, + // eslint-disable-next-line max-len + /Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+; Expected [^;]+ or end of input but "\S+" found.; end of the stream or a document separator is expected at line \d+, column \d+:[^;]*/, + "Did not get correct exception for bad content." + ); + test.end(); +}); + +tape("allBuiltInRulesHaveValidUrl", (test) => { + test.plan(132); + rules.forEach(function forRule(rule) { + test.ok(rule.information); + test.ok(Object.getPrototypeOf(rule.information) === URL.prototype); + const name = rule.names[0].toLowerCase(); + test.equal( + rule.information.href, + `${homepage}/blob/v${version}/doc/Rules.md#${name}` + ); + }); + test.end(); +}); + +tape("someCustomRulesHaveValidUrl", (test) => { + test.plan(7); + customRules.all.forEach(function forRule(rule) { + test.ok(!rule.information || + (Object.getPrototypeOf(rule.information) === URL.prototype)); + if (rule === customRules.anyBlockquote) { + test.equal( + rule.information.href, + `${homepage}/blob/main/test/rules/any-blockquote.js` + ); + } else if (rule === customRules.lettersEX) { + test.equal( + rule.information.href, + `${homepage}/blob/main/test/rules/letters-E-X.js` + ); + } + }); + test.end(); +}); + +tape("customRulesV0", (test) => { + test.plan(4); + const customRulesMd = "./test/custom-rules.md"; + const options = { + "customRules": customRules.all, + "files": [ customRulesMd ], + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = {}; + expectedResult[customRulesMd] = { + "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."); + let actualMessage = actualResult.toString(); + let expectedMessage = + "./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" + + " Rule that reports an error every N lines\n" + + "./test/custom-rules.md: 6: every-n-lines" + + " Rule that reports an error every N lines\n" + + "./test/custom-rules.md: 10: every-n-lines" + + " 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" + + " Rule that reports an error for lines with the letters 'EX'"; + test.equal(actualMessage, expectedMessage, "Incorrect message (name)."); + // @ts-ignore + actualMessage = actualResult.toString(true); + expectedMessage = + "./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" + + " Rule that reports an error every N lines\n" + + "./test/custom-rules.md: 6: every-n-lines" + + " Rule that reports an error every N lines\n" + + "./test/custom-rules.md: 10: every-n-lines" + + " 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" + + " Rule that reports an error for lines with the letters 'EX'"; + test.equal(actualMessage, expectedMessage, "Incorrect message (alias)."); + test.end(); + }); +}); + +tape("customRulesV1", (test) => { + test.plan(3); + const customRulesMd = "./test/custom-rules.md"; + const options = { + "customRules": customRules.all, + "files": [ customRulesMd ], + "resultVersion": 1 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = {}; + expectedResult[customRulesMd] = [ + { "lineNumber": 12, + "ruleName": "any-blockquote", + "ruleAlias": "any-blockquote", + "ruleDescription": "Rule that reports an error for any blockquote", + "ruleInformation": + `${homepage}/blob/main/test/rules/any-blockquote.js`, + "errorDetail": "Blockquote spans 1 line(s).", + "errorContext": "> Block", + "errorRange": null }, + { "lineNumber": 2, + "ruleName": "every-n-lines", + "ruleAlias": "every-n-lines", + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 2", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 4, + "ruleName": "every-n-lines", + "ruleAlias": "every-n-lines", + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 4", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 6, + "ruleName": "every-n-lines", + "ruleAlias": "every-n-lines", + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 6", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 10, + "ruleName": "every-n-lines", + "ruleAlias": "every-n-lines", + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 10", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 12, + "ruleName": "every-n-lines", + "ruleAlias": "every-n-lines", + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "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", + "ruleInformation": null, + "errorDetail": null, + "errorContext": null, + "errorRange": null }, + { "lineNumber": 3, + "ruleName": "letters-E-X", + "ruleAlias": "letter-E-letter-X", + "ruleDescription": + "Rule that reports an error for lines with the letters 'EX'", + "ruleInformation": `${homepage}/blob/main/test/rules/letters-E-X.js`, + "errorDetail": null, + "errorContext": "text", + "errorRange": null }, + { "lineNumber": 7, + "ruleName": "letters-E-X", + "ruleAlias": "letter-E-letter-X", + "ruleDescription": + "Rule that reports an error for lines with the letters 'EX'", + "ruleInformation": `${homepage}/blob/main/test/rules/letters-E-X.js`, + "errorDetail": null, + "errorContext": "text", + "errorRange": null } + ]; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "./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" + + "./test/custom-rules.md: 4: every-n-lines/every-n-lines" + + " Rule that reports an error every N lines [Line number 4]\n" + + "./test/custom-rules.md: 6: every-n-lines/every-n-lines" + + " Rule that reports an error every N lines [Line number 6]\n" + + "./test/custom-rules.md: 10: every-n-lines/every-n-lines" + + " 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" + + "./test/custom-rules.md: 7: letters-E-X/letter-E-letter-X" + + " Rule that reports an error for lines with the letters 'EX'" + + " [Context: \"text\"]"; + test.equal(actualMessage, expectedMessage, "Incorrect message."); + test.end(); + }); +}); + +tape("customRulesV2", (test) => { + test.plan(3); + const customRulesMd = "./test/custom-rules.md"; + const options = { + "customRules": customRules.all, + "files": [ customRulesMd ], + "resultVersion": 2 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = {}; + expectedResult[customRulesMd] = [ + { "lineNumber": 12, + "ruleNames": [ "any-blockquote" ], + "ruleDescription": "Rule that reports an error for any blockquote", + "ruleInformation": + `${homepage}/blob/main/test/rules/any-blockquote.js`, + "errorDetail": "Blockquote spans 1 line(s).", + "errorContext": "> Block", + "errorRange": null }, + { "lineNumber": 2, + "ruleNames": [ "every-n-lines" ], + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 2", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 4, + "ruleNames": [ "every-n-lines" ], + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 4", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 6, + "ruleNames": [ "every-n-lines" ], + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 6", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 10, + "ruleNames": [ "every-n-lines" ], + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 10", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 12, + "ruleNames": [ "every-n-lines" ], + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 12", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 1, + "ruleNames": [ "first-line" ], + "ruleDescription": "Rule that reports an error for the first line", + "ruleInformation": null, + "errorDetail": null, + "errorContext": null, + "errorRange": null }, + { "lineNumber": 3, + "ruleNames": [ "letters-E-X", "letter-E-letter-X", "contains-ex" ], + "ruleDescription": + "Rule that reports an error for lines with the letters 'EX'", + "ruleInformation": `${homepage}/blob/main/test/rules/letters-E-X.js`, + "errorDetail": null, + "errorContext": "text", + "errorRange": null }, + { "lineNumber": 7, + "ruleNames": [ "letters-E-X", "letter-E-letter-X", "contains-ex" ], + "ruleDescription": + "Rule that reports an error for lines with the letters 'EX'", + "ruleInformation": `${homepage}/blob/main/test/rules/letters-E-X.js`, + "errorDetail": null, + "errorContext": "text", + "errorRange": null } + ]; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "./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" + + "./test/custom-rules.md: 4: every-n-lines" + + " Rule that reports an error every N lines [Line number 4]\n" + + "./test/custom-rules.md: 6: every-n-lines" + + " Rule that reports an error every N lines [Line number 6]\n" + + "./test/custom-rules.md: 10: every-n-lines" + + " 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" + + "./test/custom-rules.md: 7: letters-E-X/letter-E-letter-X/contains-ex" + + " Rule that reports an error for lines with the letters 'EX'" + + " [Context: \"text\"]"; + test.equal(actualMessage, expectedMessage, "Incorrect message."); + test.end(); + }); +}); + +tape("customRulesConfig", (test) => { + test.plan(2); + const customRulesMd = "./test/custom-rules.md"; + const options = { + "customRules": customRules.all, + "files": [ customRulesMd ], + "config": { + "blockquote": true, + "every-n-lines": { + "n": 3 + }, + "letters-e-x": false + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = {}; + expectedResult[customRulesMd] = { + "any-blockquote": [ 12 ], + "every-n-lines": [ 3, 6, 12 ], + "first-line": [ 1 ], + "letters-E-X": [ 7 ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesNpmPackage", (test) => { + test.plan(2); + const options = { + "customRules": [ require("./rules/npm") ], + "strings": { + "string": "# Text\n\n---\n\nText\n" + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = {}; + expectedResult.string = { + "sample-rule": [ 3 ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesBadProperty", (test) => { + test.plan(23); + [ + { + "propertyName": "names", + "propertyValues": + [ null, "string", [], [ null ], [ "" ], [ "string", 10 ] ] + }, + { + "propertyName": "description", + "propertyValues": [ null, 10, "", [] ] + }, + { + "propertyName": "information", + "propertyValues": [ 10, [], "string", "https://example.com" ] + }, + { + "propertyName": "tags", + "propertyValues": + [ null, "string", [], [ null ], [ "" ], [ "string", 10 ] ] + }, + { + "propertyName": "function", + "propertyValues": [ null, "string", [] ] + } + ].forEach(function forTestCase(testCase) { + const { propertyName, propertyValues } = testCase; + propertyValues.forEach(function forPropertyValue(propertyValue) { + const badRule = { ...customRules.anyBlockquote }; + badRule[propertyName] = propertyValue; + const options = { + "customRules": [ badRule ] + }; + test.throws( + function badRuleCall() { + markdownlint.sync(options); + }, + new RegExp( + `Property '${propertyName}' of custom rule at index 0 is incorrect.` + ), + "Did not get correct exception for missing property." + ); + }); + }); + test.end(); +}); + +tape("customRulesUsedNameName", (test) => { + test.plan(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.end(); + }); +}); + +tape("customRulesUsedNameTag", (test) => { + test.plan(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.end(); + }); +}); + +tape("customRulesUsedTagName", (test) => { + test.plan(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.end(); + }); +}); + +tape("customRulesThrowForFile", (test) => { + test.plan(4); + const 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.end(); + }); +}); + +tape("customRulesThrowForFileSync", (test) => { + test.plan(1); + const 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" ] + }); + }, + new RegExp(exceptionMessage), + "Did not get correct exception for function thrown." + ); + test.end(); +}); + +tape("customRulesThrowForString", (test) => { + test.plan(4); + const exceptionMessage = "Test exception message"; + markdownlint({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function throws() { + throw new Error(exceptionMessage); + } + } + ], + "strings": { + "string": "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.end(); + }); +}); + +tape("customRulesOnErrorNull", (test) => { + test.plan(1); + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorNull(params, onError) { + onError(null); + } + } + ], + "strings": { + "string": "String" + } + }; + test.throws( + function nullErrorCall() { + markdownlint.sync(options); + }, + /Property 'lineNumber' of onError parameter is incorrect./, + "Did not get correct exception for null object." + ); + test.end(); +}); + +tape("customRulesOnErrorBad", (test) => { + test.plan(21); + [ + { + "propertyName": "lineNumber", + "subPropertyName": null, + "propertyValues": [ null, "string" ] + }, + { + "propertyName": "detail", + "subPropertyName": null, + "propertyValues": [ 10, [] ] + }, + { + "propertyName": "context", + "subPropertyName": null, + "propertyValues": [ 10, [] ] + }, + { + "propertyName": "range", + "subPropertyName": null, + "propertyValues": [ 10, [], [ 10 ], [ 10, null ], [ 10, 11, 12 ] ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": null, + "propertyValues": [ 10, "string" ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "lineNumber", + "propertyValues": [ null, "string" ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "editColumn", + "propertyValues": [ null, "string" ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "deleteCount", + "propertyValues": [ null, "string" ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "insertText", + "propertyValues": [ 10, [] ] + } + ].forEach(function forTestCase(testCase) { + const { propertyName, subPropertyName, propertyValues } = testCase; + propertyValues.forEach(function forPropertyValue(propertyValue) { + const badObject = { + "lineNumber": 1 + }; + let propertyNames = null; + if (subPropertyName) { + badObject[propertyName] = {}; + badObject[propertyName][subPropertyName] = propertyValue; + propertyNames = `${propertyName}.${subPropertyName}`; + } else { + badObject[propertyName] = propertyValue; + propertyNames = propertyName; + } + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorBad(params, onError) { + onError(badObject); + } + } + ], + "strings": { + "string": "String" + } + }; + test.throws( + function badErrorCall() { + markdownlint.sync(options); + }, + new RegExp( + `Property '${propertyNames}' of onError parameter is incorrect.` + ), + "Did not get correct exception for bad object." + ); + }); + }); + test.end(); +}); + +tape("customRulesOnErrorInvalid", (test) => { + test.plan(17); + [ + { + "propertyName": "lineNumber", + "subPropertyName": null, + "propertyValues": [ -1, 0, 3, 4 ] + }, + { + "propertyName": "range", + "subPropertyName": null, + "propertyValues": [ [ 0, 1 ], [ 1, 0 ], [ 5, 1 ], [ 1, 5 ], [ 4, 2 ] ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "lineNumber", + "propertyValues": [ -1, 0, 3, 4 ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "editColumn", + "propertyValues": [ 0, 6 ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "deleteCount", + "propertyValues": [ -2, 5 ] + } + ].forEach(function forTestCase(testCase) { + const { propertyName, subPropertyName, propertyValues } = testCase; + propertyValues.forEach(function forPropertyValue(propertyValue) { + const badObject = { + "lineNumber": 1 + }; + let propertyNames = null; + if (subPropertyName) { + badObject[propertyName] = {}; + badObject[propertyName][subPropertyName] = propertyValue; + propertyNames = `${propertyName}.${subPropertyName}`; + } else { + badObject[propertyName] = propertyValue; + propertyNames = propertyName; + } + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorInvalid(params, onError) { + onError(badObject); + } + } + ], + "strings": { + "string": "Text\ntext" + } + }; + test.throws( + function invalidErrorCall() { + markdownlint.sync(options); + }, + new RegExp( + `Property '${propertyNames}' of onError parameter is incorrect.` + ), + "Did not get correct exception for invalid object." + ); + }); + }); + test.end(); +}); + +tape("customRulesOnErrorValid", (test) => { + test.plan(24); + [ + { + "propertyName": "lineNumber", + "subPropertyName": null, + "propertyValues": [ 1, 2 ] + }, + { + "propertyName": "range", + "subPropertyName": null, + "propertyValues": [ [ 1, 1 ], [ 1, 4 ], [ 2, 2 ], [ 3, 2 ], [ 4, 1 ] ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "lineNumber", + "propertyValues": [ 1, 2 ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "editColumn", + "propertyValues": [ 1, 2, 4, 5 ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "deleteCount", + "propertyValues": [ -1, 0, 1, 4 ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "insertText", + "propertyValues": + [ "", "1", "123456", "\n", "\nText", "Text\n", "\nText\n" ] + } + ].forEach(function forTestCase(testCase) { + const { propertyName, subPropertyName, propertyValues } = testCase; + propertyValues.forEach(function forPropertyValue(propertyValue) { + const goodObject = { + "lineNumber": 1 + }; + if (subPropertyName) { + goodObject[propertyName] = {}; + goodObject[propertyName][subPropertyName] = propertyValue; + } else { + goodObject[propertyName] = propertyValue; + } + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorValid(params, onError) { + onError(goodObject); + } + } + ], + "strings": { + "string": "Text\ntext" + } + }; + markdownlint.sync(options); + test.ok(true); + }); + }); + test.end(); +}); + +tape("customRulesOnErrorLazy", (test) => { + test.plan(2); + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorLazy(params, onError) { + onError({ + "lineNumber": 1, + "detail": "", + "context": "", + "range": [ 1, 1 ] + }); + } + } + ], + "strings": { + "string": "# Heading\n" + } + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "string": [ + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": null, + "errorDetail": null, + "errorContext": null, + "errorRange": [ 1, 1 ] + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesOnErrorModified", (test) => { + test.plan(2); + const errorObject = { + "lineNumber": 1, + "detail": "detail", + "context": "context", + "range": [ 1, 2 ], + "fixInfo": { + "editColumn": 1, + "deleteCount": 2, + "insertText": "text" + } + }; + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorModified(params, onError) { + onError(errorObject); + errorObject.lineNumber = 2; + errorObject.detail = "changed"; + errorObject.context = "changed"; + errorObject.range[1] = 3; + errorObject.fixInfo.editColumn = 2; + errorObject.fixInfo.deleteCount = 3; + errorObject.fixInfo.insertText = "changed"; + } + } + ], + "strings": { + "string": "# Heading\n" + }, + "resultVersion": 3 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "string": [ + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": null, + "errorDetail": "detail", + "errorContext": "context", + "errorRange": [ 1, 2 ], + "fixInfo": { + "editColumn": 1, + "deleteCount": 2, + "insertText": "text" + } + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesThrowForFileHandled", (test) => { + test.plan(2); + const exceptionMessage = "Test exception message"; + markdownlint({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function throws() { + throw new Error(exceptionMessage); + } + } + ], + "files": [ "./test/custom-rules.md" ], + "handleRuleFailures": true + }, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/custom-rules.md": [ + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": null, + "errorDetail": + `This rule threw an exception: ${exceptionMessage}`, + "errorContext": null, + "errorRange": null + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesThrowForStringHandled", (test) => { + test.plan(2); + const exceptionMessage = "Test exception message"; + const informationUrl = "https://example.com/rule"; + markdownlint({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "information": new URL(informationUrl), + "tags": [ "tag" ], + "function": function throws() { + throw new Error(exceptionMessage); + } + } + ], + "strings": { + "string": "String\n" + }, + "handleRuleFailures": true + }, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "string": [ + { + "lineNumber": 1, + "ruleNames": [ "MD041", "first-line-heading", "first-line-h1" ], + "ruleDescription": + "First line in file should be a top level heading", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md041`, + "errorDetail": null, + "errorContext": "String", + "errorRange": null + }, + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": informationUrl, + "errorDetail": + `This rule threw an exception: ${exceptionMessage}`, + "errorContext": null, + "errorRange": null + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesOnErrorInvalidHandled", (test) => { + test.plan(2); + markdownlint({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorInvalid(params, onError) { + onError({ + "lineNumber": 13, + "details": "N/A" + }); + } + } + ], + "strings": { + "string": "# Heading\n" + }, + "handleRuleFailures": true + }, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "string": [ + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": null, + "errorDetail": "This rule threw an exception: " + + "Property 'lineNumber' of onError parameter is incorrect.", + "errorContext": null, + "errorRange": null + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesFileName", (test) => { + test.plan(2); + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function stringName(params) { + test.equal(params.name, "doc/CustomRules.md", "Incorrect file name"); + } + } + ], + "files": "doc/CustomRules.md" + }; + markdownlint(options, function callback(err) { + test.ifError(err); + test.end(); + }); +}); + +tape("customRulesStringName", (test) => { + test.plan(2); + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function stringName(params) { + test.equal(params.name, "string", "Incorrect string name"); + } + } + ], + "strings": { + "string": "# Heading" + } + }; + markdownlint(options, function callback(err) { + test.ifError(err); + test.end(); + }); +}); + +tape("customRulesDoc", (test) => { + test.plan(2); + markdownlint({ + "files": "doc/CustomRules.md", + "config": { + "MD013": { "line_length": 200 } + } + }, function callback(err, actual) { + test.ifError(err); + const expected = { "doc/CustomRules.md": [] }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("customRulesLintJavaScript", (test) => { + test.plan(2); + const options = { + "customRules": customRules.lintJavaScript, + "files": "test/lint-javascript.md" + }; + markdownlint(options, (err, actual) => { + test.ifError(err); + const expected = { + "test/lint-javascript.md": [ + { + "lineNumber": 10, + "ruleNames": [ "lint-javascript" ], + "ruleDescription": "Rule that lints JavaScript code", + "ruleInformation": null, + "errorDetail": "Unexpected var, use let or const instead.", + "errorContext": "var x = 0;", + "errorRange": null + }, + { + "lineNumber": 12, + "ruleNames": [ "lint-javascript" ], + "ruleDescription": "Rule that lints JavaScript code", + "ruleInformation": null, + "errorDetail": "Unexpected console statement.", + "errorContext": "console.log(x);", + "errorRange": null + } + ] + }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("markdownItPluginsSingle", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "string": "# Heading\n\nText [ link ](https://example.com)\n" + }, + "markdownItPlugins": [ + [ + pluginInline, + "trim_text_plugin", + "text", + function iterator(tokens, index) { + tokens[index].content = tokens[index].content.trim(); + } + ] + ] + }, function callback(err, actual) { + test.ifError(err); + const expected = { "string": [] }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("markdownItPluginsMultiple", (test) => { + test.plan(4); + markdownlint({ + "strings": { + "string": "# Heading\n\nText H~2~0 text 29^th^ text\n" + }, + "markdownItPlugins": [ + [ pluginSub ], + [ pluginSup ], + [ pluginInline, "check_sub_plugin", "sub_open", test.ok ], + [ pluginInline, "check_sup_plugin", "sup_open", test.ok ] + ] + }, function callback(err, actual) { + test.ifError(err); + const expected = { "string": [] }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("markdownItPluginsMathjax", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "string": + "# Heading\n" + + "\n" + + "$1 *2* 3$\n" + + "\n" + + "$$1 *2* 3$$\n" + + "\n" + + "$$1\n" + + "+ 2\n" + + "+ 3$$\n" + }, + "markdownItPlugins": [ [ pluginKatex ] ] + }, function callback(err, actual) { + test.ifError(err); + const expected = { "string": [] }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("markdownItPluginsMathjaxIssue166", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "string": +`## Heading + +$$ +1 +$$$$ +2 +$$\n` + }, + "markdownItPlugins": [ [ pluginKatex ] ], + "resultVersion": 0 + }, function callback(err, actual) { + test.ifError(err); + const expected = { + "string": { + "MD041": [ 1 ] + } + }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); diff --git a/test/markdownlint-test-result-object.js b/test/markdownlint-test-result-object.js new file mode 100644 index 00000000..3598159d --- /dev/null +++ b/test/markdownlint-test-result-object.js @@ -0,0 +1,4087 @@ +// @ts-check + +"use strict"; + +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const { promisify } = require("util"); +const md = require("markdown-it")(); +const pluginInline = require("markdown-it-for-inline"); +const pluginKatex = require("@iktakahiro/markdown-it-katex"); +const pluginSub = require("markdown-it-sub"); +const pluginSup = require("markdown-it-sup"); +const tape = require("tape"); +require("tape-player"); +const tv4 = require("tv4"); +const packageJson = require("../package.json"); +const markdownlint = require("../lib/markdownlint"); +const helpers = require("../helpers"); +const rules = require("../lib/rules"); +const customRules = require("./rules/rules.js"); +const defaultConfig = require("./markdownlint-test-default-config.json"); +const configSchema = require("../schema/markdownlint-config-schema.json"); +const homepage = packageJson.homepage; +const version = packageJson.version; + +const deprecatedRuleNames = new Set([ "MD002", "MD006" ]); + +/** + * Create a test function for the specified test file. + * + * @param {string} file Test file relative path. + * @returns {Function} Test function. + */ +function createTestForFile(file) { + const markdownlintPromise = promisify(markdownlint); + return function testForFile(test) { + const detailedResults = /[/\\]detailed-results-/.test(file); + test.plan(detailedResults ? 3 : 2); + const resultsFile = file.replace(/\.md$/, ".results.json"); + const fixedFile = file.replace(/\.md$/, ".md.fixed"); + const configFile = file.replace(/\.md$/, ".json"); + let mergedConfig = null; + const actualPromise = fs.promises.stat(configFile) + .then( + function configFileExists() { + return fs.promises.readFile(configFile, helpers.utf8Encoding) + .then(JSON.parse); + }, + function noConfigFile() { + return {}; + }) + .then( + function lintWithConfig(config) { + mergedConfig = { + ...defaultConfig, + ...config + }; + return markdownlintPromise({ + "files": [ file ], + "config": mergedConfig, + "resultVersion": detailedResults ? 2 : 3 + }); + }) + .then( + function diffFixedFiles(resultVersion2or3) { + return detailedResults ? + Promise.all([ + markdownlintPromise({ + "files": [ file ], + "config": mergedConfig, + "resultVersion": 3 + }), + fs.promises.readFile(file, helpers.utf8Encoding), + fs.promises.readFile(fixedFile, helpers.utf8Encoding) + ]) + .then(function validateApplyFixes(fulfillments) { + const [ resultVersion3, content, expected ] = fulfillments; + const errors = resultVersion3[file]; + const actual = helpers.applyFixes(content, errors); + // Uncomment the following line to update *.md.fixed files + // fs.writeFileSync(fixedFile, actual, helpers.utf8Encoding); + test.equal(actual, expected, + "Unexpected output from applyFixes."); + return resultVersion2or3; + }) : + resultVersion2or3; + } + ) + .then( + function convertResultVersion2To0(resultVersion2or3) { + const result0 = {}; + const result2or3 = resultVersion2or3[file]; + result2or3.forEach(function forResult(result) { + const ruleName = result.ruleNames[0]; + const lineNumbers = result0[ruleName] || []; + if (!lineNumbers.includes(result.lineNumber)) { + lineNumbers.push(result.lineNumber); + } + result0[ruleName] = lineNumbers; + }); + return [ result0, result2or3 ]; + } + ); + const expectedPromise = detailedResults ? + fs.promises.readFile(resultsFile, helpers.utf8Encoding) + .then( + function fileContents(contents) { + // @ts-ignore + const errorObjects = JSON.parse(contents); + errorObjects.forEach(function forObject(errorObject) { + if (errorObject.ruleInformation) { + errorObject.ruleInformation = + errorObject.ruleInformation.replace("v0.0.0", `v${version}`); + } + }); + return errorObjects; + }) : + fs.promises.readFile(file, helpers.utf8Encoding) + .then( + function fileContents(contents) { + // @ts-ignore + const lines = contents.split(helpers.newLineRe); + const results = {}; + lines.forEach(function forLine(line, lineNum) { + const regex = /\{(MD\d+)(?::(\d+))?\}/g; + let match = null; + while ((match = regex.exec(line))) { + const rule = match[1]; + const errors = results[rule] || []; + errors.push( + match[2] ? + Number.parseInt(match[2], 10) : + lineNum + 1 + ); + results[rule] = errors; + } + }); + const sortedResults = {}; + Object.keys(results).sort().forEach(function forKey(key) { + sortedResults[key] = results[key]; + }); + return sortedResults; + }); + Promise.all([ actualPromise, expectedPromise ]) + .then( + function compareResults(fulfillments) { + const [ [ actual0, actual2or3 ], expected ] = fulfillments; + const actual = detailedResults ? actual2or3 : actual0; + test.deepEqual(actual, expected, "Line numbers are not correct."); + return actual2or3; + }) + .then( + function verifyFixes(errors) { + if (detailedResults) { + return test.ok(true); + } + return fs.promises.readFile(file, helpers.utf8Encoding) + .then( + function applyFixes(content) { + const corrections = helpers.applyFixes(content, errors); + return markdownlintPromise({ + "strings": { + "input": corrections + }, + "config": mergedConfig, + "resultVersion": 3 + }); + }) + .then( + function checkFixes(newErrors) { + const unfixed = newErrors.input + .filter((error) => !!error.fixInfo); + test.deepEqual(unfixed, [], "Fixable error was not fixed."); + } + ); + }) + .catch() + .then(test.done); + }; +} + +fs.readdirSync("./test") + .filter((file) => /\.md$/.test(file)) + // @ts-ignore + .forEach((file) => tape(file, createTestForFile(path.join("./test", file)))); + +tape("projectFilesNoInlineConfig", (test) => { + test.plan(2); + const options = { + "files": [ + "README.md", + "CONTRIBUTING.md", + "doc/CustomRules.md", + "helpers/README.md" + ], + "noInlineConfig": true, + "config": { + "line-length": { "line_length": 150 }, + "no-duplicate-heading": false + } + }; + markdownlint(options, function callback(err, actual) { + test.ifError(err); + const expected = { + "README.md": [], + "CONTRIBUTING.md": [], + "doc/CustomRules.md": [], + "helpers/README.md": [] + }; + test.deepEqual(actual, expected, "Issue(s) with project files."); + test.end(); + }); +}); + +tape("projectFilesInlineConfig", (test) => { + test.plan(2); + const options = { + "files": [ "doc/Rules.md" ], + "config": { + "line-length": { "line_length": 150 }, + "no-inline-html": false + } + }; + markdownlint(options, function callback(err, actual) { + test.ifError(err); + const expected = { + "doc/Rules.md": [] + }; + test.deepEqual(actual, expected, "Issue(s) with project files."); + test.end(); + }); +}); + +tape("resultObjectToStringNotEnumerable", (test) => { + test.plan(2); + const options = { + "strings": { + "string": "# Heading" + } + }; + markdownlint(options, function callback(err, result) { + test.ifError(err); + // eslint-disable-next-line guard-for-in + for (const property in result) { + test.notEqual(property, "toString", "Function should not enumerate."); + } + test.end(); + }); +}); + +tape("resultFormattingV0", (test) => { + test.plan(4); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": true, + "MD041": false + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD002": [ 3 ], + "MD018": [ 1 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": { + "MD002": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + let actualMessage = actualResult.toString(); + let expectedMessage = + "./test/atx_heading_spacing.md: 3: MD002" + + " First heading should be a top level heading\n" + + "./test/atx_heading_spacing.md: 1: MD018" + + " No space after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 3: MD019" + + " Multiple spaces after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 5: MD019" + + " Multiple spaces after hash on atx style heading\n" + + "./test/first_heading_bad_atx.md: 1: MD002" + + " First heading should be a top level heading"; + test.equal(actualMessage, expectedMessage, "Incorrect message (name)."); + // @ts-ignore + actualMessage = actualResult.toString(true); + expectedMessage = + "./test/atx_heading_spacing.md: 3: first-heading-h1" + + " First heading should be a top level heading\n" + + "./test/atx_heading_spacing.md: 1: no-missing-space-atx" + + " No space after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 3: no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 5: no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading\n" + + "./test/first_heading_bad_atx.md: 1: first-heading-h1" + + " First heading should be a top level heading"; + test.equal(actualMessage, expectedMessage, "Incorrect message (alias)."); + test.end(); + }); +}); + +tape("resultFormattingSyncV0", (test) => { + test.plan(3); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": true, + "MD041": false + }, + "resultVersion": 0 + }; + const actualResult = markdownlint.sync(options); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD002": [ 3 ], + "MD018": [ 1 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": { + "MD002": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + let actualMessage = actualResult.toString(); + let expectedMessage = + "./test/atx_heading_spacing.md: 3: MD002" + + " First heading should be a top level heading\n" + + "./test/atx_heading_spacing.md: 1: MD018" + + " No space after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 3: MD019" + + " Multiple spaces after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 5: MD019" + + " Multiple spaces after hash on atx style heading\n" + + "./test/first_heading_bad_atx.md: 1: MD002" + + " First heading should be a top level heading"; + test.equal(actualMessage, expectedMessage, "Incorrect message (name)."); + // @ts-ignore + actualMessage = actualResult.toString(true); + expectedMessage = + "./test/atx_heading_spacing.md: 3: first-heading-h1" + + " First heading should be a top level heading\n" + + "./test/atx_heading_spacing.md: 1: no-missing-space-atx" + + " No space after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 3: no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 5: no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading\n" + + "./test/first_heading_bad_atx.md: 1: first-heading-h1" + + " First heading should be a top level heading"; + test.equal(actualMessage, expectedMessage, "Incorrect message (alias)."); + test.end(); +}); + +tape("resultFormattingV1", (test) => { + test.plan(3); + const options = { + "strings": { + "truncate": + "# Multiple spaces inside hashes on closed atx style heading #\n" + }, + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": true, + "MD041": false + }, + "resultVersion": 1 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "truncate": [ + { "lineNumber": 1, + "ruleName": "MD021", + "ruleAlias": "no-multiple-space-closed-atx", + "ruleDescription": + "Multiple spaces inside hashes on closed atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md021`, + "errorDetail": null, + "errorContext": "# Multiple spa...tyle heading #", + "errorRange": [ 1, 4 ] } + ], + "./test/atx_heading_spacing.md": [ + { "lineNumber": 3, + "ruleName": "MD002", + "ruleAlias": "first-heading-h1", + "ruleDescription": "First heading should be a top level heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md002`, + "errorDetail": "Expected: h1; Actual: h2", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 1, + "ruleName": "MD018", + "ruleAlias": "no-missing-space-atx", + "ruleDescription": "No space after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md018`, + "errorDetail": null, + "errorContext": "#Heading 1 {MD018}", + "errorRange": [ 1, 2 ] }, + { "lineNumber": 3, + "ruleName": "MD019", + "ruleAlias": "no-multiple-space-atx", + "ruleDescription": "Multiple spaces after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md019`, + "errorDetail": null, + "errorContext": "## Heading 2 {MD019}", + "errorRange": [ 1, 5 ] }, + { "lineNumber": 5, + "ruleName": "MD019", + "ruleAlias": "no-multiple-space-atx", + "ruleDescription": "Multiple spaces after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md019`, + "errorDetail": null, + "errorContext": "## Heading 3 {MD019}", + "errorRange": [ 1, 6 ] } + ], + "./test/first_heading_bad_atx.md": [ + { "lineNumber": 1, + "ruleName": "MD002", + "ruleAlias": "first-heading-h1", + "ruleDescription": "First heading should be a top level heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md002`, + "errorDetail": "Expected: h1; Actual: h2", + "errorContext": null, + "errorRange": null } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "./test/atx_heading_spacing.md: 3: MD002/first-heading-h1" + + " First heading should be a top level heading" + + " [Expected: h1; Actual: h2]\n" + + "./test/atx_heading_spacing.md: 1: MD018/no-missing-space-atx" + + " No space after hash on atx style heading" + + " [Context: \"#Heading 1 {MD018}\"]\n" + + "./test/atx_heading_spacing.md: 3: MD019/no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading" + + " [Context: \"## Heading 2 {MD019}\"]\n" + + "./test/atx_heading_spacing.md: 5: MD019/no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading" + + " [Context: \"## Heading 3 {MD019}\"]\n" + + "./test/first_heading_bad_atx.md: 1: MD002/first-heading-h1" + + " First heading should be a top level heading" + + " [Expected: h1; Actual: h2]\n" + + "truncate: 1: MD021/no-multiple-space-closed-atx" + + " Multiple spaces inside hashes on closed atx style heading" + + " [Context: \"# Multiple spa...tyle heading #\"]"; + test.equal(actualMessage, expectedMessage, "Incorrect message."); + test.end(); + }); +}); + +tape("resultFormattingV2", (test) => { + test.plan(3); + const options = { + "strings": { + "truncate": + "# Multiple spaces inside hashes on closed atx style heading #\n" + }, + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": true, + "MD041": false + } + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "truncate": [ + { "lineNumber": 1, + "ruleNames": [ "MD021", "no-multiple-space-closed-atx" ], + "ruleDescription": + "Multiple spaces inside hashes on closed atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md021`, + "errorDetail": null, + "errorContext": "# Multiple spa...tyle heading #", + "errorRange": [ 1, 4 ] } + ], + "./test/atx_heading_spacing.md": [ + { "lineNumber": 3, + "ruleNames": [ "MD002", "first-heading-h1", "first-header-h1" ], + "ruleDescription": "First heading should be a top level heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md002`, + "errorDetail": "Expected: h1; Actual: h2", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 1, + "ruleNames": [ "MD018", "no-missing-space-atx" ], + "ruleDescription": "No space after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md018`, + "errorDetail": null, + "errorContext": "#Heading 1 {MD018}", + "errorRange": [ 1, 2 ] }, + { "lineNumber": 3, + "ruleNames": [ "MD019", "no-multiple-space-atx" ], + "ruleDescription": "Multiple spaces after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md019`, + "errorDetail": null, + "errorContext": "## Heading 2 {MD019}", + "errorRange": [ 1, 5 ] }, + { "lineNumber": 5, + "ruleNames": [ "MD019", "no-multiple-space-atx" ], + "ruleDescription": "Multiple spaces after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md019`, + "errorDetail": null, + "errorContext": "## Heading 3 {MD019}", + "errorRange": [ 1, 6 ] } + ], + "./test/first_heading_bad_atx.md": [ + { "lineNumber": 1, + "ruleNames": [ "MD002", "first-heading-h1", "first-header-h1" ], + "ruleDescription": "First heading should be a top level heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md002`, + "errorDetail": "Expected: h1; Actual: h2", + "errorContext": null, + "errorRange": null } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "./test/atx_heading_spacing.md: 3:" + + " MD002/first-heading-h1/first-header-h1" + + " First heading should be a top level heading" + + " [Expected: h1; Actual: h2]\n" + + "./test/atx_heading_spacing.md: 1: MD018/no-missing-space-atx" + + " No space after hash on atx style heading" + + " [Context: \"#Heading 1 {MD018}\"]\n" + + "./test/atx_heading_spacing.md: 3: MD019/no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading" + + " [Context: \"## Heading 2 {MD019}\"]\n" + + "./test/atx_heading_spacing.md: 5: MD019/no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading" + + " [Context: \"## Heading 3 {MD019}\"]\n" + + "./test/first_heading_bad_atx.md: 1:" + + " MD002/first-heading-h1/first-header-h1" + + " First heading should be a top level heading" + + " [Expected: h1; Actual: h2]\n" + + "truncate: 1: MD021/no-multiple-space-closed-atx" + + " Multiple spaces inside hashes on closed atx style heading" + + " [Context: \"# Multiple spa...tyle heading #\"]"; + test.equal(actualMessage, expectedMessage, "Incorrect message."); + test.end(); + }); +}); + +tape("resultFormattingV3", (test) => { + test.plan(3); + const options = { + "strings": { + "input": + "# Heading \n" + + "\n" + + "Text\ttext\t\ttext\n" + + "Text * emphasis * text" + }, + "resultVersion": 3 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { + "lineNumber": 1, + "ruleNames": [ "MD009", "no-trailing-spaces" ], + "ruleDescription": "Trailing spaces", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md009`, + "errorDetail": "Expected: 0 or 2; Actual: 3", + "errorContext": null, + "errorRange": [ 10, 3 ], + "fixInfo": { + "editColumn": 10, + "deleteCount": 3 + } + }, + { + "lineNumber": 3, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 5", + "errorContext": null, + "errorRange": [ 5, 1 ], + "fixInfo": { + "editColumn": 5, + "deleteCount": 1, + "insertText": " " + } + }, + { + "lineNumber": 3, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 10", + "errorContext": null, + "errorRange": [ 10, 2 ], + "fixInfo": { + "editColumn": 10, + "deleteCount": 2, + "insertText": " " + } + }, + { + "lineNumber": 4, + "ruleNames": [ "MD037", "no-space-in-emphasis" ], + "ruleDescription": "Spaces inside emphasis markers", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md037`, + "errorDetail": null, + "errorContext": "* emphasis *", + "errorRange": [ 6, 12 ], + "fixInfo": { + "editColumn": 6, + "deleteCount": 12, + "insertText": "*emphasis*" + } + }, + { + "lineNumber": 4, + "ruleNames": [ "MD047", "single-trailing-newline" ], + "ruleDescription": "Files should end with a single newline character", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md047`, + "errorDetail": null, + "errorContext": null, + "errorRange": [ 22, 1 ], + "fixInfo": { + "insertText": "\n", + "editColumn": 23 + } + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "input: 1: MD009/no-trailing-spaces" + + " Trailing spaces [Expected: 0 or 2; Actual: 3]\n" + + "input: 3: MD010/no-hard-tabs" + + " Hard tabs [Column: 5]\n" + + "input: 3: MD010/no-hard-tabs" + + " Hard tabs [Column: 10]\n" + + "input: 4: MD037/no-space-in-emphasis" + + " Spaces inside emphasis markers [Context: \"* emphasis *\"]\n" + + "input: 4: MD047/single-trailing-newline" + + " Files should end with a single newline character"; + test.equal(actualMessage, expectedMessage, "Incorrect message."); + test.end(); + }); +}); + +tape("onePerLineResultVersion0", (test) => { + test.plan(2); + const options = { + "strings": { + "input": "# Heading\theading\t\theading\n" + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": { + "MD010": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("onePerLineResultVersion1", (test) => { + test.plan(2); + const options = { + "strings": { + "input": "# Heading\theading\t\theading\n" + }, + "resultVersion": 1 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { + "lineNumber": 1, + "ruleName": "MD010", + "ruleAlias": "no-hard-tabs", + "ruleDescription": "Hard tabs", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 10", + "errorContext": null, + "errorRange": [ 10, 1 ] + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("onePerLineResultVersion2", (test) => { + test.plan(2); + const options = { + "strings": { + "input": "# Heading\theading\t\theading\n" + }, + "resultVersion": 2 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { + "lineNumber": 1, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 10", + "errorContext": null, + "errorRange": [ 10, 1 ] + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("manyPerLineResultVersion3", (test) => { + test.plan(2); + const options = { + "strings": { + "input": "# Heading\theading\t\theading\n" + }, + "resultVersion": 3 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { + "lineNumber": 1, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 10", + "errorContext": null, + "errorRange": [ 10, 1 ], + "fixInfo": { + "editColumn": 10, + "deleteCount": 1, + "insertText": " " + } + }, + { + "lineNumber": 1, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 18", + "errorContext": null, + "errorRange": [ 18, 2 ], + "fixInfo": { + "editColumn": 18, + "deleteCount": 2, + "insertText": " " + } + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("frontMatterResultVersion3", (test) => { + test.plan(2); + const options = { + "strings": { + "input": "---\n---\n# Heading\nText\n" + }, + "resultVersion": 3 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { + "lineNumber": 3, + "ruleNames": + [ "MD022", "blanks-around-headings", "blanks-around-headers" ], + "ruleDescription": "Headings should be surrounded by blank lines", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md022`, + "errorDetail": "Expected: 1; Actual: 0; Below", + "errorContext": "# Heading", + "errorRange": null, + "fixInfo": { + "lineNumber": 4, + "insertText": "\n" + } + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("stringInputLineEndings", (test) => { + test.plan(2); + const options = { + "strings": { + "cr": "One\rTwo\r#Three\n", + "lf": "One\nTwo\n#Three\n", + "crlf": "One\r\nTwo\r\n#Three\n", + "mixed": "One\rTwo\n#Three\n" + }, + "config": defaultConfig, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "cr": { "MD018": [ 3 ] }, + "lf": { "MD018": [ 3 ] }, + "crlf": { "MD018": [ 3 ] }, + "mixed": { "MD018": [ 3 ] } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("inputOnlyNewline", (test) => { + test.plan(2); + const options = { + "strings": { + "cr": "\r", + "lf": "\n", + "crlf": "\r\n" + }, + "config": { + "default": false + } + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "cr": [], + "lf": [], + "crlf": [] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("defaultTrue", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "default": true + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD018": [ 1 ], + "MD019": [ 3, 5 ], + "MD041": [ 1 ] + }, + "./test/first_heading_bad_atx.md": { + "MD041": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("defaultFalse", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "default": false + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": {}, + "./test/first_heading_bad_atx.md": {} + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("defaultUndefined", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": {}, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD018": [ 1 ], + "MD019": [ 3, 5 ], + "MD041": [ 1 ] + }, + "./test/first_heading_bad_atx.md": { + "MD041": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("disableRules", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": false, + "default": true, + "MD019": false, + "first-line-h1": false + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD018": [ 1 ] + }, + "./test/first_heading_bad_atx.md": {} + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("enableRules", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": true, + "default": false, + "no-multiple-space-atx": true + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD002": [ 3 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": { + "MD002": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("enableRulesMixedCase", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "Md002": true, + "DeFaUlT": false, + "nO-mUlTiPlE-sPaCe-AtX": true + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD002": [ 3 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": { + "MD002": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("disableTag", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "default": true, + "spaces": false + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD041": [ 1 ] + }, + "./test/first_heading_bad_atx.md": { + "MD041": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("enableTag", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "default": false, + "spaces": true, + "notatag": true + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD018": [ 1 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": {} + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("enableTagMixedCase", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "DeFaUlT": false, + "SpAcEs": true, + "NoTaTaG": true + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD018": [ 1 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": {} + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("styleFiles", (test) => { + test.plan(4); + fs.readdir("./style", function readdir(err, files) { + test.ifError(err); + files.forEach(function forFile(file) { + test.ok(require(path.join("../style", file)), "Unable to load/parse."); + }); + test.end(); + }); +}); + +tape("styleAll", (test) => { + test.plan(2); + const options = { + "files": [ "./test/break-all-the-rules.md" ], + "config": require("../style/all.json"), + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/break-all-the-rules.md": { + "MD001": [ 3 ], + "MD003": [ 5, 31 ], + "MD004": [ 8 ], + "MD005": [ 12 ], + "MD007": [ 8, 11 ], + "MD009": [ 14 ], + "MD010": [ 14 ], + "MD011": [ 16 ], + "MD012": [ 18 ], + "MD013": [ 21 ], + "MD014": [ 23 ], + "MD018": [ 25 ], + "MD019": [ 27 ], + "MD020": [ 29 ], + "MD021": [ 31 ], + "MD022": [ 86 ], + "MD023": [ 40 ], + "MD024": [ 35 ], + "MD026": [ 40 ], + "MD027": [ 42 ], + "MD028": [ 43 ], + "MD029": [ 47 ], + "MD030": [ 8 ], + "MD031": [ 50 ], + "MD032": [ 7, 8, 51 ], + "MD033": [ 55 ], + "MD034": [ 57 ], + "MD035": [ 61 ], + "MD036": [ 65 ], + "MD037": [ 67 ], + "MD038": [ 69 ], + "MD039": [ 71 ], + "MD040": [ 73 ], + "MD041": [ 1 ], + "MD042": [ 81 ], + "MD045": [ 85 ], + "MD046": [ 49, 73, 77 ], + "MD047": [ 88 ], + "MD048": [ 77 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("styleRelaxed", (test) => { + test.plan(2); + const options = { + "files": [ "./test/break-all-the-rules.md" ], + "config": require("../style/relaxed.json"), + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/break-all-the-rules.md": { + "MD001": [ 3 ], + "MD003": [ 5, 31 ], + "MD004": [ 8 ], + "MD005": [ 12 ], + "MD011": [ 16 ], + "MD014": [ 23 ], + "MD018": [ 25 ], + "MD019": [ 27 ], + "MD020": [ 29 ], + "MD021": [ 31 ], + "MD022": [ 86 ], + "MD023": [ 40 ], + "MD024": [ 35 ], + "MD026": [ 40 ], + "MD029": [ 47 ], + "MD031": [ 50 ], + "MD032": [ 7, 8, 51 ], + "MD035": [ 61 ], + "MD036": [ 65 ], + "MD042": [ 81 ], + "MD045": [ 85 ], + "MD046": [ 49, 73, 77 ], + "MD047": [ 88 ], + "MD048": [ 77 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("nullFrontMatter", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "content": "---\n\t\n---\n# Heading\n" + }, + "frontMatter": null, + "config": { + "default": false, + "MD010": true + }, + "resultVersion": 0 + }, function callback(err, result) { + test.ifError(err); + const expectedResult = { + "content": { "MD010": [ 2 ] } + }; + test.deepEqual(result, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customFrontMatter", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "content": "\n\t\n\n# Heading\n" + }, + "frontMatter": /[^]*<\/head>/, + "config": { + "default": false, + "MD010": true + } + }, function callback(err, result) { + test.ifError(err); + const expectedResult = { + "content": [] + }; + test.deepEqual(result, expectedResult, "Did not get empty results."); + test.end(); + }); +}); + +tape("noInlineConfig", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "content": [ + "# Heading", + "", + "\tTab", + "", + "", + "", + "\tTab", + "", + "", + "", + "\tTab\n" + ].join("\n") + }, + "noInlineConfig": true, + "resultVersion": 0 + }, function callback(err, result) { + test.ifError(err); + const expectedResult = { + "content": { + "MD010": [ 3, 7, 11 ] + } + }; + test.deepEqual(result, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("readmeHeadings", (test) => { + test.plan(2); + markdownlint({ + "files": "README.md", + "noInlineConfig": true, + "config": { + "default": false, + "MD013": { + "line_length": 150 + }, + "MD043": { + "headings": [ + "# markdownlint", + "## Install", + "## Overview", + "### Related", + "## Demonstration", + "## Rules / Aliases", + "## Tags", + "## Configuration", + "## API", + "### Linting", + "#### options", + "##### options.customRules", + "##### options.files", + "##### options.strings", + "##### options.config", + "##### options.frontMatter", + "##### options.handleRuleFailures", + "##### options.noInlineConfig", + "##### options.resultVersion", + "##### options.markdownItPlugins", + "#### callback", + "#### result", + "### Config", + "#### file", + "#### parsers", + "#### callback", + "#### result", + "## Usage", + "## Browser", + "## Examples", + "## Contributing", + "## History" + ] + } + } + }, function callback(err, result) { + test.ifError(err); + const expected = { "README.md": [] }; + test.deepEqual(result, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("filesArrayNotModified", (test) => { + test.plan(2); + const files = [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ]; + const expectedFiles = files.slice(); + markdownlint({ "files": files }, function callback(err) { + test.ifError(err); + test.deepEqual(files, expectedFiles, "Files modified."); + test.end(); + }); +}); + +tape("filesArrayAsString", (test) => { + test.plan(2); + markdownlint({ + "files": "README.md", + "noInlineConfig": true, + "config": { + "MD013": { "line_length": 150 }, + "MD024": false + } + }, function callback(err, actual) { + test.ifError(err); + const expected = { "README.md": [] }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("missingOptions", (test) => { + test.plan(2); + markdownlint(null, function callback(err, result) { + test.ifError(err); + test.deepEqual( + result, + {}, + "Did not get empty result for missing options." + ); + test.end(); + }); +}); + +tape("missingFilesAndStrings", (test) => { + test.plan(2); + markdownlint({}, function callback(err, result) { + test.ifError(err); + test.ok(result, "Did not get result for missing files/strings."); + test.end(); + }); +}); + +tape("missingCallback", (test) => { + test.plan(0); + // @ts-ignore + markdownlint(); + test.end(); +}); + +tape("badFile", (test) => { + test.plan(4); + markdownlint({ + "files": [ "./badFile" ] + }, function callback(err, result) { + test.ok(err, "Did not get an error for bad file."); + test.ok(err instanceof Error, "Error not instance of Error."); + // @ts-ignore + test.equal(err.code, "ENOENT", "Error code for bad file not ENOENT."); + test.ok(!result, "Got result for bad file."); + test.end(); + }); +}); + +tape("badFileSync", (test) => { + test.plan(1); + test.throws( + function badFileCall() { + markdownlint.sync({ + "files": [ "./badFile" ] + }); + }, + /ENOENT/, + "Did not get correct exception for bad file." + ); + test.end(); +}); + +tape("missingStringValue", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "undefined": undefined, + "null": null, + "empty": "" + }, + "config": defaultConfig + }, function callback(err, result) { + test.ifError(err); + const expectedResult = { + "undefined": [], + "null": [], + "empty": [] + }; + test.deepEqual(result, expectedResult, "Did not get empty results."); + test.end(); + }); +}); + +tape("readme", (test) => { + test.plan(115); + const tagToRules = {}; + rules.forEach(function forRule(rule) { + rule.tags.forEach(function forTag(tag) { + const tagRules = tagToRules[tag] || []; + tagRules.push(rule.names[0]); + tagToRules[tag] = tagRules; + }); + }); + fs.readFile("README.md", helpers.utf8Encoding, + function readFile(err, contents) { + test.ifError(err); + const rulesLeft = rules.slice(); + let seenRelated = false; + let seenRules = false; + let inRules = false; + let seenTags = false; + let inTags = false; + md.parse(contents, {}).forEach(function forToken(token) { + if ( + (token.type === "bullet_list_open") && + (token.level === 0) + ) { + if (!seenRelated) { + seenRelated = true; + } else if (seenRelated && !seenRules) { + seenRules = true; + inRules = true; + } else if (seenRelated && seenRules && !seenTags) { + seenTags = true; + inTags = true; + } + } else if ( + (token.type === "bullet_list_close") && + (token.level === 0) + ) { + inRules = false; + inTags = false; + } else if (token.type === "inline") { + if (inRules) { + const rule = rulesLeft.shift(); + test.ok(rule, + "Missing rule implementation for " + token.content + "."); + if (rule) { + const ruleName = rule.names[0]; + const ruleAliases = rule.names.slice(1); + let expected = "**[" + ruleName + "](doc/Rules.md#" + + ruleName.toLowerCase() + ")** *" + + ruleAliases.join("/") + "* - " + rule.description; + if (deprecatedRuleNames.has(ruleName)) { + expected = "~~" + expected + "~~"; + } + test.equal(token.content, expected, "Rule mismatch."); + } + } else if (inTags) { + const parts = + token.content.replace(/\*\*/g, "").split(/ - |, |,\n/); + const tag = parts.shift(); + test.deepEqual(parts, tagToRules[tag] || [], + "Rule mismatch for tag " + tag + "."); + delete tagToRules[tag]; + } + } + }); + const ruleLeft = rulesLeft.shift(); + test.ok(!ruleLeft, + "Missing rule documentation for " + + (ruleLeft || "[NO RULE]").toString() + "."); + const tagLeft = Object.keys(tagToRules).shift(); + test.ok(!tagLeft, "Undocumented tag " + tagLeft + "."); + test.end(); + }); +}); + +tape("rules", (test) => { + test.plan(336); + fs.readFile("doc/Rules.md", helpers.utf8Encoding, + (err, contents) => { + test.ifError(err); + const rulesLeft = rules.slice(); + let inHeading = false; + let rule = null; + let ruleHasTags = true; + let ruleHasAliases = true; + let ruleUsesParams = null; + const tagAliasParameterRe = /, |: | /; + // eslint-disable-next-line func-style + const testTagsAliasesParams = (r) => { + r = r || "[NO RULE]"; + test.ok(ruleHasTags, + "Missing tags for rule " + r.names + "."); + test.ok(ruleHasAliases, + "Missing aliases for rule " + r.names + "."); + test.ok(!ruleUsesParams, + "Missing parameters for rule " + r.names + "."); + }; + md.parse(contents, {}).forEach(function forToken(token) { + if ((token.type === "heading_open") && (token.tag === "h2")) { + inHeading = true; + } else if (token.type === "heading_close") { + inHeading = false; + } else if (token.type === "inline") { + if (inHeading) { + testTagsAliasesParams(rule); + rule = rulesLeft.shift(); + ruleHasTags = false; + ruleHasAliases = false; + test.ok(rule, + "Missing rule implementation for " + token.content + "."); + const ruleName = rule.names[0]; + let headingContent = ruleName + " - " + rule.description; + if (deprecatedRuleNames.has(ruleName)) { + headingContent = "~~" + headingContent + "~~"; + } + test.equal(token.content, + headingContent, + "Rule mismatch."); + ruleUsesParams = rule.function.toString() + .match(/params\.config\.[_a-z]*/gi); + if (ruleUsesParams) { + ruleUsesParams = ruleUsesParams.map(function forUse(use) { + return use.split(".").pop(); + }); + ruleUsesParams.sort(); + } + } else if (token.content.startsWith("Tags: ") && rule) { + test.deepEqual(token.content.split(tagAliasParameterRe).slice(1), + rule.tags, "Tag mismatch for rule " + rule.names + "."); + ruleHasTags = true; + } else if (token.content.startsWith("Aliases: ") && rule) { + test.deepEqual(token.content.split(tagAliasParameterRe).slice(1), + rule.names.slice(1), + "Alias mismatch for rule " + rule.names + "."); + ruleHasAliases = true; + } else if (token.content.startsWith("Parameters: ") && rule) { + let inDetails = false; + const parameters = token.content.split(tagAliasParameterRe) + .slice(1) + .filter(function forPart(part) { + inDetails = inDetails || (part[0] === "("); + return !inDetails; + }); + parameters.sort(); + test.deepEqual(parameters, ruleUsesParams, + "Missing parameter for rule " + rule.names); + ruleUsesParams = null; + } + } + }); + const ruleLeft = rulesLeft.shift(); + test.ok(!ruleLeft, + "Missing rule documentation for " + + (ruleLeft || { "names": "[NO RULE]" }).names + "."); + if (rule) { + testTagsAliasesParams(rule); + } + test.end(); + }); +}); + +tape("validateConfigSchema", (test) => { + const jsonFileRe = /\.json$/i; + const resultsFileRe = /\.results\.json$/i; + const jsConfigFileRe = /^jsconfig\.json$/i; + const wrongTypesFileRe = /wrong-types-in-config-file.json$/i; + const testDirectory = __dirname; + const testFiles = fs.readdirSync(testDirectory); + testFiles.filter(function filterFile(file) { + return jsonFileRe.test(file) && + !resultsFileRe.test(file) && + !jsConfigFileRe.test(file) && + !wrongTypesFileRe.test(file); + }).forEach(function forFile(file) { + const data = fs.readFileSync( + path.join(testDirectory, file), + helpers.utf8Encoding + ); + test.ok( + tv4.validate(JSON.parse(data), configSchema), + file + "\n" + JSON.stringify(tv4.error, null, 2)); + }); + test.end(); +}); + +tape("clearHtmlCommentTextValid", (test) => { + test.plan(1); + const validComments = [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "texttext", + "texttext", + "texttext", + "", + "texttexttext", + "texttext", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "texttext", + "texttext", + "texttext", + "", + "texttexttext", + "texttext", + "", + "", + "-->", + "-->", + "", + "", + "", + "-->", + " -->", + "-->", + "text-->", + "text-->", + "", + "" + ]; + const actual = helpers.clearHtmlCommentText(invalidComments.join("\n")); + const expected = invalidComments.join("\n"); + test.equal(actual, expected); + test.end(); +}); + +tape("clearHtmlCommentTextNonGreedy", (test) => { + test.plan(1); + const nonGreedyComments = [ + " -->", + " -->", + " -->", + " -->" + ]; + const nonGreedyResult = [ + " -->", + " -->", + " -->", + " -->" + ]; + const actual = helpers.clearHtmlCommentText(nonGreedyComments.join("\n")); + const expected = nonGreedyResult.join("\n"); + test.equal(actual, expected); + test.end(); +}); + +tape("clearHtmlCommentTextEmbedded", (test) => { + test.plan(1); + const embeddedComments = [ + "texttext", + "", + "texttext", + "texttext", + "texttext" + ]; + const embeddedResult = [ + "texttext", + "", + "texttext", + "texttext", + "texttext" + ]; + const actual = helpers.clearHtmlCommentText(embeddedComments.join("\n")); + const expected = embeddedResult.join("\n"); + test.equal(actual, expected); + test.end(); +}); + +tape("unescapeMarkdown", (test) => { + test.plan(7); + // Test cases from https://spec.commonmark.org/0.29/#backslash-escapes + const testCases = [ + [ + "\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;" + + "\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~", + "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" + ], + [ + "\\→\\A\\a\\ \\3\\φ\\«", + "\\→\\A\\a\\ \\3\\φ\\«" + ], + [ + `\\*not emphasized* +\\
not a tag +\\[not a link](/foo) +\\\`not code\` +1\\. not a list +\\* not a list +\\# not a heading +\\[foo]: /url "not a reference" +\\ö not a character entity`, + `*not emphasized* +
not a tag +[not a link](/foo) +\`not code\` +1. not a list +* not a list +# not a heading +[foo]: /url "not a reference" +ö not a character entity` + ], + [ + "\\\\*emphasis*", + "\\*emphasis*" + ], + [ + `foo\\ +bar`, + `foo\\ +bar` + ], + [ + "Text \\<", + "Text _", + "_" + ], + [ + "Text \\\\<", + "Text _<", + "_" + ] + ]; + testCases.forEach(function forTestCase(testCase) { + const [ markdown, expected, replacement ] = testCase; + const actual = helpers.unescapeMarkdown(markdown, replacement); + test.equal(actual, expected); + }); + test.end(); +}); + +tape("isBlankLine", (test) => { + test.plan(25); + const blankLines = [ + null, + "", + " ", + " ", + "\t\t\t", + "\r", + "\n", + "\t\r\n", + " ", + "", + "", + "\t", + ">", + "> ", + "> > > \t", + "> ", + ">>" + ]; + blankLines.forEach((line) => test.ok(helpers.isBlankLine(line), line)); + const nonBlankLines = [ + "text", + " text ", + ".", + "> .", + " text", + "", + "" + ]; + nonBlankLines.forEach((line) => test.ok(!helpers.isBlankLine(line), line)); + test.end(); +}); + +tape("includesSorted", (test) => { + test.plan(154); + const inputs = [ + [ ], + [ 8 ], + [ 7, 11 ], + [ 0, 1, 2, 3, 5, 8, 13 ], + [ 2, 3, 5, 7, 11, 13, 17, 19 ], + [ 1, 3, 5, 7, 9, 11, 13, 15, 17, 19 ], + [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 ] + ]; + inputs.forEach((input) => { + for (let i = 0; i <= 21; i++) { + test.equal(helpers.includesSorted(input, i), input.includes(i)); + } + }); + test.end(); +}); + +tape("forEachInlineCodeSpan", (test) => { + test.plan(99); + const testCases = + [ + { + "input": "`code`", + "expecteds": [ [ "code", 0, 1, 1 ] ] + }, + { + "input": "text `code` text", + "expecteds": [ [ "code", 0, 6, 1 ] ] + }, + { + "input": "text `code` text `edoc`", + "expecteds": [ + [ "code", 0, 6, 1 ], + [ "edoc", 0, 18, 1 ] + ] + }, + { + "input": "text `code` text `edoc` text", + "expecteds": [ + [ "code", 0, 6, 1 ], + [ "edoc", 0, 18, 1 ] + ] + }, + { + "input": "text ``code`code`` text", + "expecteds": [ [ "code`code", 0, 7, 2 ] ] + }, + { + "input": "`code `` code`", + "expecteds": [ [ "code `` code", 0, 1, 1 ] ] + }, + { + "input": "`code\\`text`", + "expecteds": [ [ "code\\", 0, 1, 1 ] ] + }, + { + "input": "``\ncode\n``", + "expecteds": [ [ "\ncode\n", 0, 2, 2 ] ] + }, + { + "input": "text\n`code`\ntext", + "expecteds": [ [ "code", 1, 1, 1 ] ] + }, + { + "input": "text\ntext\n`code`\ntext\n`edoc`\ntext", + "expecteds": [ + [ "code", 2, 1, 1 ], + [ "edoc", 4, 1, 1 ] + ] + }, + { + "input": "text `code\nedoc` text", + "expecteds": [ [ "code\nedoc", 0, 6, 1 ] ] + }, + { + "input": "> text `code` text", + "expecteds": [ [ "code", 0, 8, 1 ] ] + }, + { + "input": "> text\n> `code`\n> text", + "expecteds": [ [ "code", 1, 3, 1 ] ] + }, + { + "input": "> text\n> `code\n> edoc`\n> text", + "expecteds": [ [ "code\n> edoc", 1, 3, 1 ] ] + }, + { + "input": "```text``", + "expecteds": [] + }, + { + "input": "text `text text", + "expecteds": [] + }, + { + "input": "`text``code``", + "expecteds": [ [ "code", 0, 7, 2 ] ] + }, + { + "input": "text \\` text `code`", + "expecteds": [ [ "code", 0, 14, 1 ] ] + }, + { + "input": "text\\\n`code`", + "expecteds": [ [ "code", 1, 1, 1 ] ] + } + ]; + testCases.forEach((testCase) => { + const { input, expecteds } = testCase; + helpers.forEachInlineCodeSpan(input, (code, line, column, ticks) => { + const [ expectedCode, expectedLine, expectedColumn, expectedTicks ] = + expecteds.shift(); + test.equal(code, expectedCode, input); + test.equal(line, expectedLine, input); + test.equal(column, expectedColumn, input); + test.equal(ticks, expectedTicks, input); + }); + test.equal(expecteds.length, 0, "length"); + }); + test.end(); +}); + +tape("getPreferredLineEnding", (test) => { + test.plan(17); + const testCases = [ + [ "", os.EOL ], + [ "\r", "\r" ], + [ "\n", "\n" ], + [ "\r\n", "\r\n" ], + [ "t\rt\nt", "\n" ], + [ "t\nt\rt", "\n" ], + [ "t\r\nt\nt", "\n" ], + [ "t\nt\r\nt", "\n" ], + [ "t\r\nt\rt", "\r\n" ], + [ "t\rt\r\nt", "\r\n" ], + [ "t\r\nt\rt\nt", "\n" ], + [ "t\r\nt\r\nt\r\nt", "\r\n" ], + [ "t\nt\nt\nt", "\n" ], + [ "t\rt\rt\rt", "\r" ], + [ "t\r\nt\nt\r\nt", "\r\n" ], + [ "t\nt\r\nt\nt", "\n" ], + [ "t\rt\t\rt", "\r" ] + ]; + testCases.forEach((testCase) => { + const [ input, expected ] = testCase; + const actual = helpers.getPreferredLineEnding(input); + test.equal(actual, expected, "Incorrect line ending returned."); + }); + test.end(); +}); + +tape("applyFix", (test) => { + test.plan(4); + const testCases = [ + [ + "Hello world.", + { + "editColumn": 12, + "deleteCount": 1 + }, + undefined, + "Hello world" + ], + [ + "Hello world.", + { + "editColumn": 13, + "insertText": "\n" + }, + undefined, + "Hello world.\n" + ], + [ + "Hello world.", + { + "editColumn": 13, + "insertText": "\n" + }, + "\n", + "Hello world.\n" + ], + [ + "Hello world.", + { + "editColumn": 13, + "insertText": "\n" + }, + "\r\n", + "Hello world.\r\n" + ] + ]; + testCases.forEach((testCase) => { + const [ line, fixInfo, lineEnding, expected ] = testCase; + // @ts-ignore + const actual = helpers.applyFix(line, fixInfo, lineEnding); + test.equal(actual, expected, "Incorrect fix applied."); + }); + test.end(); +}); + +tape("applyFixes", (test) => { + test.plan(28); + const testCases = [ + [ + "Hello world.", + [], + "Hello world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": {} + } + ], + "Hello world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "insertText": "Very " + } + } + ], + "Very Hello world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 7, + "insertText": "big " + } + } + ], + "Hello big world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "deleteCount": 6 + } + } + ], + "world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 7, + "deleteCount": 5, + "insertText": "there" + } + } + ], + "Hello there." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 12, + "deleteCount": 1 + } + }, + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 6, + "deleteCount": 1 + } + } + ], + "Helloworld" + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 13, + "insertText": " Hi." + } + } + ], + "Hello world. Hi." + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 1, + "fixInfo": { + "deleteCount": -1 + } + } + ], + "world" + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 2, + "fixInfo": { + "deleteCount": -1 + } + } + ], + "Hello" + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 2, + "fixInfo": { + "lineNumber": 1, + "deleteCount": -1 + } + } + ], + "world" + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 1, + "fixInfo": { + "lineNumber": 2, + "deleteCount": -1 + } + } + ], + "Hello" + ], + [ + "Hello world", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 4, + "deleteCount": 1 + } + }, + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 10, + "deleteCount": 1 + } + } + ], + "Helo word" + ], + [ + "Hello world", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 10, + "deleteCount": 1 + } + }, + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 4, + "deleteCount": 1 + } + } + ], + "Helo word" + ], + [ + "Hello\nworld", + [ + { + "fixInfo": { + "lineNumber": 1, + "deleteCount": -1 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "insertText": "Big " + } + } + ], + "world" + ], + [ + "Hello\nworld", + [ + { + "fixInfo": { + "lineNumber": 1, + "deleteCount": -1 + } + }, + { + "fixInfo": { + "lineNumber": 2, + "deleteCount": -1 + } + } + ], + "" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "insertText": "aa" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "insertText": "b" + } + } + ], + "aaHello world" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "insertText": "a" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "insertText": "bb" + } + } + ], + "bbHello world" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 6, + "insertText": " big" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1 + } + } + ], + "Hello big orld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 8, + "deleteCount": 2 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 2 + } + } + ], + "Hello wld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 2 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 8, + "deleteCount": 2 + } + } + ], + "Hello wld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1, + "insertText": "z" + } + } + ], + "Hello zorld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "insertText": "z" + } + } + ], + "Hello zorld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "insertText": "z" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1 + } + } + ], + "Hello zorld" + ], + [ + "Hello\nworld\nhello\rworld", + [ + { + "fixInfo": { + "lineNumber": 4, + "editColumn": 6, + "insertText": "\n" + } + } + ], + "Hello\nworld\nhello\nworld\n" + ], + [ + "Hello\r\nworld\r\nhello\nworld", + [ + { + "fixInfo": { + "lineNumber": 4, + "editColumn": 6, + "insertText": "\n" + } + } + ], + "Hello\r\nworld\r\nhello\r\nworld\r\n" + ], + [ + "Hello\rworld\rhello\nworld", + [ + { + "fixInfo": { + "lineNumber": 4, + "editColumn": 6, + "insertText": "\n" + } + } + ], + "Hello\rworld\rhello\rworld\r" + ], + [ + "Hello\r\nworld", + [ + { + "lineNumber": 2, + "fixInfo": { + "editColumn": 6, + "insertText": "\n\n" + } + } + ], + "Hello\r\nworld\r\n\r\n" + ] + ]; + testCases.forEach((testCase) => { + const [ input, errors, expected ] = testCase; + const actual = helpers.applyFixes(input, errors); + test.equal(actual, expected, "Incorrect fix applied."); + }); + test.end(); +}); + +tape("configSingle", (test) => { + test.plan(2); + markdownlint.readConfig("./test/config/config-child.json", + function callback(err, actual) { + test.ifError(err); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configAbsolute", (test) => { + test.plan(2); + markdownlint.readConfig(path.join(__dirname, "config", "config-child.json"), + function callback(err, actual) { + test.ifError(err); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configMultiple", (test) => { + test.plan(2); + markdownlint.readConfig("./test/config/config-grandparent.json", + function callback(err, actual) { + test.ifError(err); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configBadFile", (test) => { + test.plan(4); + markdownlint.readConfig("./test/config/config-badfile.json", + function callback(err, result) { + test.ok(err, "Did not get an error for bad file."); + test.ok(err instanceof Error, "Error not instance of Error."); + // @ts-ignore + test.equal(err.code, "ENOENT", "Error code for bad file not ENOENT."); + test.ok(!result, "Got result for bad file."); + test.end(); + }); +}); + +tape("configBadChildFile", (test) => { + test.plan(4); + markdownlint.readConfig("./test/config/config-badchildfile.json", + function callback(err, result) { + test.ok(err, "Did not get an error for bad child file."); + test.ok(err instanceof Error, "Error not instance of Error."); + // @ts-ignore + test.equal(err.code, "ENOENT", + "Error code for bad child file not ENOENT."); + test.ok(!result, "Got result for bad child file."); + test.end(); + }); +}); + +tape("configBadJson", (test) => { + test.plan(3); + markdownlint.readConfig("./test/config/config-badjson.json", + function callback(err, result) { + test.ok(err, "Did not get an error for bad JSON."); + test.ok(err instanceof Error, "Error not instance of Error."); + test.ok(!result, "Got result for bad JSON."); + test.end(); + }); +}); + +tape("configBadChildJson", (test) => { + test.plan(3); + markdownlint.readConfig("./test/config/config-badchildjson.json", + function callback(err, result) { + test.ok(err, "Did not get an error for bad child JSON."); + test.ok(err instanceof Error, "Error not instance of Error."); + test.ok(!result, "Got result for bad child JSON."); + test.end(); + }); +}); + +tape("configSingleYaml", (test) => { + test.plan(2); + markdownlint.readConfig( + "./test/config/config-child.yaml", + [ require("js-yaml").safeLoad ], + function callback(err, actual) { + test.ifError(err); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configMultipleYaml", (test) => { + test.plan(2); + markdownlint.readConfig( + "./test/config/config-grandparent.yaml", + [ require("js-yaml").safeLoad ], + function callback(err, actual) { + test.ifError(err); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configMultipleHybrid", (test) => { + test.plan(2); + markdownlint.readConfig( + "./test/config/config-grandparent-hybrid.yaml", + [ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ], + function callback(err, actual) { + test.ifError(err); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepLooseEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configBadHybrid", (test) => { + test.plan(4); + markdownlint.readConfig( + "./test/config/config-badcontent.txt", + [ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ], + function callback(err, result) { + test.ok(err, "Did not get an error for bad child JSON."); + test.ok(err instanceof Error, "Error not instance of Error."); + test.ok(err.message.match( + // eslint-disable-next-line max-len + /^Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+; Expected [^;]+ or end of input but "\S+" found.; end of the stream or a document separator is expected at line \d+, column \d+:[^;]*$/ + ), "Error message unexpected."); + test.ok(!result, "Got result for bad child JSON."); + test.end(); + }); +}); + +tape("configSingleSync", (test) => { + test.plan(1); + const actual = markdownlint.readConfigSync("./test/config/config-child.json"); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configAbsoluteSync", (test) => { + test.plan(1); + const actual = markdownlint.readConfigSync( + path.join(__dirname, "config", "config-child.json")); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configMultipleSync", (test) => { + test.plan(1); + const actual = + markdownlint.readConfigSync("./test/config/config-grandparent.json"); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configBadFileSync", (test) => { + test.plan(1); + test.throws( + function badFileCall() { + markdownlint.readConfigSync("./test/config/config-badfile.json"); + }, + /ENOENT/, + "Did not get correct exception for bad file." + ); + test.end(); +}); + +tape("configBadChildFileSync", (test) => { + test.plan(1); + test.throws( + function badChildFileCall() { + markdownlint.readConfigSync("./test/config/config-badchildfile.json"); + }, + /ENOENT/, + "Did not get correct exception for bad child file." + ); + test.end(); +}); + +tape("configBadJsonSync", (test) => { + test.plan(1); + test.throws( + function badJsonCall() { + markdownlint.readConfigSync("./test/config/config-badjson.json"); + }, + /Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+/, + "Did not get correct exception for bad JSON." + ); + test.end(); +}); + +tape("configBadChildJsonSync", (test) => { + test.plan(1); + test.throws( + function badChildJsonCall() { + markdownlint.readConfigSync("./test/config/config-badchildjson.json"); + }, + /Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+/, + "Did not get correct exception for bad child JSON." + ); + test.end(); +}); + +tape("configSingleYamlSync", (test) => { + test.plan(1); + const actual = markdownlint.readConfigSync( + "./test/config/config-child.yaml", [ require("js-yaml").safeLoad ]); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configMultipleYamlSync", (test) => { + test.plan(1); + const actual = markdownlint.readConfigSync( + "./test/config/config-grandparent.yaml", [ require("js-yaml").safeLoad ]); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configMultipleHybridSync", (test) => { + test.plan(1); + const actual = markdownlint.readConfigSync( + "./test/config/config-grandparent-hybrid.yaml", + [ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ]); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepLooseEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configBadHybridSync", (test) => { + test.plan(1); + test.throws( + function badHybridCall() { + markdownlint.readConfigSync( + "./test/config/config-badcontent.txt", + [ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ]); + }, + // eslint-disable-next-line max-len + /Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+; Expected [^;]+ or end of input but "\S+" found.; end of the stream or a document separator is expected at line \d+, column \d+:[^;]*/, + "Did not get correct exception for bad content." + ); + test.end(); +}); + +tape("allBuiltInRulesHaveValidUrl", (test) => { + test.plan(132); + rules.forEach(function forRule(rule) { + test.ok(rule.information); + test.ok(Object.getPrototypeOf(rule.information) === URL.prototype); + const name = rule.names[0].toLowerCase(); + test.equal( + rule.information.href, + `${homepage}/blob/v${version}/doc/Rules.md#${name}` + ); + }); + test.end(); +}); + +tape("someCustomRulesHaveValidUrl", (test) => { + test.plan(7); + customRules.all.forEach(function forRule(rule) { + test.ok(!rule.information || + (Object.getPrototypeOf(rule.information) === URL.prototype)); + if (rule === customRules.anyBlockquote) { + test.equal( + rule.information.href, + `${homepage}/blob/main/test/rules/any-blockquote.js` + ); + } else if (rule === customRules.lettersEX) { + test.equal( + rule.information.href, + `${homepage}/blob/main/test/rules/letters-E-X.js` + ); + } + }); + test.end(); +}); + +tape("customRulesV0", (test) => { + test.plan(4); + const customRulesMd = "./test/custom-rules.md"; + const options = { + "customRules": customRules.all, + "files": [ customRulesMd ], + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = {}; + expectedResult[customRulesMd] = { + "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."); + let actualMessage = actualResult.toString(); + let expectedMessage = + "./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" + + " Rule that reports an error every N lines\n" + + "./test/custom-rules.md: 6: every-n-lines" + + " Rule that reports an error every N lines\n" + + "./test/custom-rules.md: 10: every-n-lines" + + " 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" + + " Rule that reports an error for lines with the letters 'EX'"; + test.equal(actualMessage, expectedMessage, "Incorrect message (name)."); + // @ts-ignore + actualMessage = actualResult.toString(true); + expectedMessage = + "./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" + + " Rule that reports an error every N lines\n" + + "./test/custom-rules.md: 6: every-n-lines" + + " Rule that reports an error every N lines\n" + + "./test/custom-rules.md: 10: every-n-lines" + + " 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" + + " Rule that reports an error for lines with the letters 'EX'"; + test.equal(actualMessage, expectedMessage, "Incorrect message (alias)."); + test.end(); + }); +}); + +tape("customRulesV1", (test) => { + test.plan(3); + const customRulesMd = "./test/custom-rules.md"; + const options = { + "customRules": customRules.all, + "files": [ customRulesMd ], + "resultVersion": 1 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = {}; + expectedResult[customRulesMd] = [ + { "lineNumber": 12, + "ruleName": "any-blockquote", + "ruleAlias": "any-blockquote", + "ruleDescription": "Rule that reports an error for any blockquote", + "ruleInformation": + `${homepage}/blob/main/test/rules/any-blockquote.js`, + "errorDetail": "Blockquote spans 1 line(s).", + "errorContext": "> Block", + "errorRange": null }, + { "lineNumber": 2, + "ruleName": "every-n-lines", + "ruleAlias": "every-n-lines", + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 2", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 4, + "ruleName": "every-n-lines", + "ruleAlias": "every-n-lines", + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 4", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 6, + "ruleName": "every-n-lines", + "ruleAlias": "every-n-lines", + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 6", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 10, + "ruleName": "every-n-lines", + "ruleAlias": "every-n-lines", + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 10", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 12, + "ruleName": "every-n-lines", + "ruleAlias": "every-n-lines", + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "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", + "ruleInformation": null, + "errorDetail": null, + "errorContext": null, + "errorRange": null }, + { "lineNumber": 3, + "ruleName": "letters-E-X", + "ruleAlias": "letter-E-letter-X", + "ruleDescription": + "Rule that reports an error for lines with the letters 'EX'", + "ruleInformation": `${homepage}/blob/main/test/rules/letters-E-X.js`, + "errorDetail": null, + "errorContext": "text", + "errorRange": null }, + { "lineNumber": 7, + "ruleName": "letters-E-X", + "ruleAlias": "letter-E-letter-X", + "ruleDescription": + "Rule that reports an error for lines with the letters 'EX'", + "ruleInformation": `${homepage}/blob/main/test/rules/letters-E-X.js`, + "errorDetail": null, + "errorContext": "text", + "errorRange": null } + ]; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "./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" + + "./test/custom-rules.md: 4: every-n-lines/every-n-lines" + + " Rule that reports an error every N lines [Line number 4]\n" + + "./test/custom-rules.md: 6: every-n-lines/every-n-lines" + + " Rule that reports an error every N lines [Line number 6]\n" + + "./test/custom-rules.md: 10: every-n-lines/every-n-lines" + + " 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" + + "./test/custom-rules.md: 7: letters-E-X/letter-E-letter-X" + + " Rule that reports an error for lines with the letters 'EX'" + + " [Context: \"text\"]"; + test.equal(actualMessage, expectedMessage, "Incorrect message."); + test.end(); + }); +}); + +tape("customRulesV2", (test) => { + test.plan(3); + const customRulesMd = "./test/custom-rules.md"; + const options = { + "customRules": customRules.all, + "files": [ customRulesMd ], + "resultVersion": 2 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = {}; + expectedResult[customRulesMd] = [ + { "lineNumber": 12, + "ruleNames": [ "any-blockquote" ], + "ruleDescription": "Rule that reports an error for any blockquote", + "ruleInformation": + `${homepage}/blob/main/test/rules/any-blockquote.js`, + "errorDetail": "Blockquote spans 1 line(s).", + "errorContext": "> Block", + "errorRange": null }, + { "lineNumber": 2, + "ruleNames": [ "every-n-lines" ], + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 2", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 4, + "ruleNames": [ "every-n-lines" ], + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 4", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 6, + "ruleNames": [ "every-n-lines" ], + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 6", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 10, + "ruleNames": [ "every-n-lines" ], + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 10", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 12, + "ruleNames": [ "every-n-lines" ], + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 12", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 1, + "ruleNames": [ "first-line" ], + "ruleDescription": "Rule that reports an error for the first line", + "ruleInformation": null, + "errorDetail": null, + "errorContext": null, + "errorRange": null }, + { "lineNumber": 3, + "ruleNames": [ "letters-E-X", "letter-E-letter-X", "contains-ex" ], + "ruleDescription": + "Rule that reports an error for lines with the letters 'EX'", + "ruleInformation": `${homepage}/blob/main/test/rules/letters-E-X.js`, + "errorDetail": null, + "errorContext": "text", + "errorRange": null }, + { "lineNumber": 7, + "ruleNames": [ "letters-E-X", "letter-E-letter-X", "contains-ex" ], + "ruleDescription": + "Rule that reports an error for lines with the letters 'EX'", + "ruleInformation": `${homepage}/blob/main/test/rules/letters-E-X.js`, + "errorDetail": null, + "errorContext": "text", + "errorRange": null } + ]; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "./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" + + "./test/custom-rules.md: 4: every-n-lines" + + " Rule that reports an error every N lines [Line number 4]\n" + + "./test/custom-rules.md: 6: every-n-lines" + + " Rule that reports an error every N lines [Line number 6]\n" + + "./test/custom-rules.md: 10: every-n-lines" + + " 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" + + "./test/custom-rules.md: 7: letters-E-X/letter-E-letter-X/contains-ex" + + " Rule that reports an error for lines with the letters 'EX'" + + " [Context: \"text\"]"; + test.equal(actualMessage, expectedMessage, "Incorrect message."); + test.end(); + }); +}); + +tape("customRulesConfig", (test) => { + test.plan(2); + const customRulesMd = "./test/custom-rules.md"; + const options = { + "customRules": customRules.all, + "files": [ customRulesMd ], + "config": { + "blockquote": true, + "every-n-lines": { + "n": 3 + }, + "letters-e-x": false + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = {}; + expectedResult[customRulesMd] = { + "any-blockquote": [ 12 ], + "every-n-lines": [ 3, 6, 12 ], + "first-line": [ 1 ], + "letters-E-X": [ 7 ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesNpmPackage", (test) => { + test.plan(2); + const options = { + "customRules": [ require("./rules/npm") ], + "strings": { + "string": "# Text\n\n---\n\nText\n" + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = {}; + expectedResult.string = { + "sample-rule": [ 3 ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesBadProperty", (test) => { + test.plan(23); + [ + { + "propertyName": "names", + "propertyValues": + [ null, "string", [], [ null ], [ "" ], [ "string", 10 ] ] + }, + { + "propertyName": "description", + "propertyValues": [ null, 10, "", [] ] + }, + { + "propertyName": "information", + "propertyValues": [ 10, [], "string", "https://example.com" ] + }, + { + "propertyName": "tags", + "propertyValues": + [ null, "string", [], [ null ], [ "" ], [ "string", 10 ] ] + }, + { + "propertyName": "function", + "propertyValues": [ null, "string", [] ] + } + ].forEach(function forTestCase(testCase) { + const { propertyName, propertyValues } = testCase; + propertyValues.forEach(function forPropertyValue(propertyValue) { + const badRule = { ...customRules.anyBlockquote }; + badRule[propertyName] = propertyValue; + const options = { + "customRules": [ badRule ] + }; + test.throws( + function badRuleCall() { + markdownlint.sync(options); + }, + new RegExp( + `Property '${propertyName}' of custom rule at index 0 is incorrect.` + ), + "Did not get correct exception for missing property." + ); + }); + }); + test.end(); +}); + +tape("customRulesUsedNameName", (test) => { + test.plan(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.end(); + }); +}); + +tape("customRulesUsedNameTag", (test) => { + test.plan(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.end(); + }); +}); + +tape("customRulesUsedTagName", (test) => { + test.plan(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.end(); + }); +}); + +tape("customRulesThrowForFile", (test) => { + test.plan(4); + const 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.end(); + }); +}); + +tape("customRulesThrowForFileSync", (test) => { + test.plan(1); + const 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" ] + }); + }, + new RegExp(exceptionMessage), + "Did not get correct exception for function thrown." + ); + test.end(); +}); + +tape("customRulesThrowForString", (test) => { + test.plan(4); + const exceptionMessage = "Test exception message"; + markdownlint({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function throws() { + throw new Error(exceptionMessage); + } + } + ], + "strings": { + "string": "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.end(); + }); +}); + +tape("customRulesOnErrorNull", (test) => { + test.plan(1); + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorNull(params, onError) { + onError(null); + } + } + ], + "strings": { + "string": "String" + } + }; + test.throws( + function nullErrorCall() { + markdownlint.sync(options); + }, + /Property 'lineNumber' of onError parameter is incorrect./, + "Did not get correct exception for null object." + ); + test.end(); +}); + +tape("customRulesOnErrorBad", (test) => { + test.plan(21); + [ + { + "propertyName": "lineNumber", + "subPropertyName": null, + "propertyValues": [ null, "string" ] + }, + { + "propertyName": "detail", + "subPropertyName": null, + "propertyValues": [ 10, [] ] + }, + { + "propertyName": "context", + "subPropertyName": null, + "propertyValues": [ 10, [] ] + }, + { + "propertyName": "range", + "subPropertyName": null, + "propertyValues": [ 10, [], [ 10 ], [ 10, null ], [ 10, 11, 12 ] ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": null, + "propertyValues": [ 10, "string" ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "lineNumber", + "propertyValues": [ null, "string" ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "editColumn", + "propertyValues": [ null, "string" ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "deleteCount", + "propertyValues": [ null, "string" ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "insertText", + "propertyValues": [ 10, [] ] + } + ].forEach(function forTestCase(testCase) { + const { propertyName, subPropertyName, propertyValues } = testCase; + propertyValues.forEach(function forPropertyValue(propertyValue) { + const badObject = { + "lineNumber": 1 + }; + let propertyNames = null; + if (subPropertyName) { + badObject[propertyName] = {}; + badObject[propertyName][subPropertyName] = propertyValue; + propertyNames = `${propertyName}.${subPropertyName}`; + } else { + badObject[propertyName] = propertyValue; + propertyNames = propertyName; + } + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorBad(params, onError) { + onError(badObject); + } + } + ], + "strings": { + "string": "String" + } + }; + test.throws( + function badErrorCall() { + markdownlint.sync(options); + }, + new RegExp( + `Property '${propertyNames}' of onError parameter is incorrect.` + ), + "Did not get correct exception for bad object." + ); + }); + }); + test.end(); +}); + +tape("customRulesOnErrorInvalid", (test) => { + test.plan(17); + [ + { + "propertyName": "lineNumber", + "subPropertyName": null, + "propertyValues": [ -1, 0, 3, 4 ] + }, + { + "propertyName": "range", + "subPropertyName": null, + "propertyValues": [ [ 0, 1 ], [ 1, 0 ], [ 5, 1 ], [ 1, 5 ], [ 4, 2 ] ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "lineNumber", + "propertyValues": [ -1, 0, 3, 4 ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "editColumn", + "propertyValues": [ 0, 6 ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "deleteCount", + "propertyValues": [ -2, 5 ] + } + ].forEach(function forTestCase(testCase) { + const { propertyName, subPropertyName, propertyValues } = testCase; + propertyValues.forEach(function forPropertyValue(propertyValue) { + const badObject = { + "lineNumber": 1 + }; + let propertyNames = null; + if (subPropertyName) { + badObject[propertyName] = {}; + badObject[propertyName][subPropertyName] = propertyValue; + propertyNames = `${propertyName}.${subPropertyName}`; + } else { + badObject[propertyName] = propertyValue; + propertyNames = propertyName; + } + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorInvalid(params, onError) { + onError(badObject); + } + } + ], + "strings": { + "string": "Text\ntext" + } + }; + test.throws( + function invalidErrorCall() { + markdownlint.sync(options); + }, + new RegExp( + `Property '${propertyNames}' of onError parameter is incorrect.` + ), + "Did not get correct exception for invalid object." + ); + }); + }); + test.end(); +}); + +tape("customRulesOnErrorValid", (test) => { + test.plan(24); + [ + { + "propertyName": "lineNumber", + "subPropertyName": null, + "propertyValues": [ 1, 2 ] + }, + { + "propertyName": "range", + "subPropertyName": null, + "propertyValues": [ [ 1, 1 ], [ 1, 4 ], [ 2, 2 ], [ 3, 2 ], [ 4, 1 ] ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "lineNumber", + "propertyValues": [ 1, 2 ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "editColumn", + "propertyValues": [ 1, 2, 4, 5 ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "deleteCount", + "propertyValues": [ -1, 0, 1, 4 ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "insertText", + "propertyValues": + [ "", "1", "123456", "\n", "\nText", "Text\n", "\nText\n" ] + } + ].forEach(function forTestCase(testCase) { + const { propertyName, subPropertyName, propertyValues } = testCase; + propertyValues.forEach(function forPropertyValue(propertyValue) { + const goodObject = { + "lineNumber": 1 + }; + if (subPropertyName) { + goodObject[propertyName] = {}; + goodObject[propertyName][subPropertyName] = propertyValue; + } else { + goodObject[propertyName] = propertyValue; + } + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorValid(params, onError) { + onError(goodObject); + } + } + ], + "strings": { + "string": "Text\ntext" + } + }; + markdownlint.sync(options); + test.ok(true); + }); + }); + test.end(); +}); + +tape("customRulesOnErrorLazy", (test) => { + test.plan(2); + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorLazy(params, onError) { + onError({ + "lineNumber": 1, + "detail": "", + "context": "", + "range": [ 1, 1 ] + }); + } + } + ], + "strings": { + "string": "# Heading\n" + } + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "string": [ + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": null, + "errorDetail": null, + "errorContext": null, + "errorRange": [ 1, 1 ] + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesOnErrorModified", (test) => { + test.plan(2); + const errorObject = { + "lineNumber": 1, + "detail": "detail", + "context": "context", + "range": [ 1, 2 ], + "fixInfo": { + "editColumn": 1, + "deleteCount": 2, + "insertText": "text" + } + }; + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorModified(params, onError) { + onError(errorObject); + errorObject.lineNumber = 2; + errorObject.detail = "changed"; + errorObject.context = "changed"; + errorObject.range[1] = 3; + errorObject.fixInfo.editColumn = 2; + errorObject.fixInfo.deleteCount = 3; + errorObject.fixInfo.insertText = "changed"; + } + } + ], + "strings": { + "string": "# Heading\n" + }, + "resultVersion": 3 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "string": [ + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": null, + "errorDetail": "detail", + "errorContext": "context", + "errorRange": [ 1, 2 ], + "fixInfo": { + "editColumn": 1, + "deleteCount": 2, + "insertText": "text" + } + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesThrowForFileHandled", (test) => { + test.plan(2); + const exceptionMessage = "Test exception message"; + markdownlint({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function throws() { + throw new Error(exceptionMessage); + } + } + ], + "files": [ "./test/custom-rules.md" ], + "handleRuleFailures": true + }, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/custom-rules.md": [ + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": null, + "errorDetail": + `This rule threw an exception: ${exceptionMessage}`, + "errorContext": null, + "errorRange": null + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesThrowForStringHandled", (test) => { + test.plan(2); + const exceptionMessage = "Test exception message"; + const informationUrl = "https://example.com/rule"; + markdownlint({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "information": new URL(informationUrl), + "tags": [ "tag" ], + "function": function throws() { + throw new Error(exceptionMessage); + } + } + ], + "strings": { + "string": "String\n" + }, + "handleRuleFailures": true + }, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "string": [ + { + "lineNumber": 1, + "ruleNames": [ "MD041", "first-line-heading", "first-line-h1" ], + "ruleDescription": + "First line in file should be a top level heading", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md041`, + "errorDetail": null, + "errorContext": "String", + "errorRange": null + }, + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": informationUrl, + "errorDetail": + `This rule threw an exception: ${exceptionMessage}`, + "errorContext": null, + "errorRange": null + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesOnErrorInvalidHandled", (test) => { + test.plan(2); + markdownlint({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorInvalid(params, onError) { + onError({ + "lineNumber": 13, + "details": "N/A" + }); + } + } + ], + "strings": { + "string": "# Heading\n" + }, + "handleRuleFailures": true + }, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "string": [ + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": null, + "errorDetail": "This rule threw an exception: " + + "Property 'lineNumber' of onError parameter is incorrect.", + "errorContext": null, + "errorRange": null + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesFileName", (test) => { + test.plan(2); + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function stringName(params) { + test.equal(params.name, "doc/CustomRules.md", "Incorrect file name"); + } + } + ], + "files": "doc/CustomRules.md" + }; + markdownlint(options, function callback(err) { + test.ifError(err); + test.end(); + }); +}); + +tape("customRulesStringName", (test) => { + test.plan(2); + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function stringName(params) { + test.equal(params.name, "string", "Incorrect string name"); + } + } + ], + "strings": { + "string": "# Heading" + } + }; + markdownlint(options, function callback(err) { + test.ifError(err); + test.end(); + }); +}); + +tape("customRulesDoc", (test) => { + test.plan(2); + markdownlint({ + "files": "doc/CustomRules.md", + "config": { + "MD013": { "line_length": 200 } + } + }, function callback(err, actual) { + test.ifError(err); + const expected = { "doc/CustomRules.md": [] }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("customRulesLintJavaScript", (test) => { + test.plan(2); + const options = { + "customRules": customRules.lintJavaScript, + "files": "test/lint-javascript.md" + }; + markdownlint(options, (err, actual) => { + test.ifError(err); + const expected = { + "test/lint-javascript.md": [ + { + "lineNumber": 10, + "ruleNames": [ "lint-javascript" ], + "ruleDescription": "Rule that lints JavaScript code", + "ruleInformation": null, + "errorDetail": "Unexpected var, use let or const instead.", + "errorContext": "var x = 0;", + "errorRange": null + }, + { + "lineNumber": 12, + "ruleNames": [ "lint-javascript" ], + "ruleDescription": "Rule that lints JavaScript code", + "ruleInformation": null, + "errorDetail": "Unexpected console statement.", + "errorContext": "console.log(x);", + "errorRange": null + } + ] + }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("markdownItPluginsSingle", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "string": "# Heading\n\nText [ link ](https://example.com)\n" + }, + "markdownItPlugins": [ + [ + pluginInline, + "trim_text_plugin", + "text", + function iterator(tokens, index) { + tokens[index].content = tokens[index].content.trim(); + } + ] + ] + }, function callback(err, actual) { + test.ifError(err); + const expected = { "string": [] }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("markdownItPluginsMultiple", (test) => { + test.plan(4); + markdownlint({ + "strings": { + "string": "# Heading\n\nText H~2~0 text 29^th^ text\n" + }, + "markdownItPlugins": [ + [ pluginSub ], + [ pluginSup ], + [ pluginInline, "check_sub_plugin", "sub_open", test.ok ], + [ pluginInline, "check_sup_plugin", "sup_open", test.ok ] + ] + }, function callback(err, actual) { + test.ifError(err); + const expected = { "string": [] }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("markdownItPluginsMathjax", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "string": + "# Heading\n" + + "\n" + + "$1 *2* 3$\n" + + "\n" + + "$$1 *2* 3$$\n" + + "\n" + + "$$1\n" + + "+ 2\n" + + "+ 3$$\n" + }, + "markdownItPlugins": [ [ pluginKatex ] ] + }, function callback(err, actual) { + test.ifError(err); + const expected = { "string": [] }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("markdownItPluginsMathjaxIssue166", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "string": +`## Heading + +$$ +1 +$$$$ +2 +$$\n` + }, + "markdownItPlugins": [ [ pluginKatex ] ], + "resultVersion": 0 + }, function callback(err, actual) { + test.ifError(err); + const expected = { + "string": { + "MD041": [ 1 ] + } + }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); diff --git a/test/markdownlint-test-scenarios.js b/test/markdownlint-test-scenarios.js new file mode 100644 index 00000000..3598159d --- /dev/null +++ b/test/markdownlint-test-scenarios.js @@ -0,0 +1,4087 @@ +// @ts-check + +"use strict"; + +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const { promisify } = require("util"); +const md = require("markdown-it")(); +const pluginInline = require("markdown-it-for-inline"); +const pluginKatex = require("@iktakahiro/markdown-it-katex"); +const pluginSub = require("markdown-it-sub"); +const pluginSup = require("markdown-it-sup"); +const tape = require("tape"); +require("tape-player"); +const tv4 = require("tv4"); +const packageJson = require("../package.json"); +const markdownlint = require("../lib/markdownlint"); +const helpers = require("../helpers"); +const rules = require("../lib/rules"); +const customRules = require("./rules/rules.js"); +const defaultConfig = require("./markdownlint-test-default-config.json"); +const configSchema = require("../schema/markdownlint-config-schema.json"); +const homepage = packageJson.homepage; +const version = packageJson.version; + +const deprecatedRuleNames = new Set([ "MD002", "MD006" ]); + +/** + * Create a test function for the specified test file. + * + * @param {string} file Test file relative path. + * @returns {Function} Test function. + */ +function createTestForFile(file) { + const markdownlintPromise = promisify(markdownlint); + return function testForFile(test) { + const detailedResults = /[/\\]detailed-results-/.test(file); + test.plan(detailedResults ? 3 : 2); + const resultsFile = file.replace(/\.md$/, ".results.json"); + const fixedFile = file.replace(/\.md$/, ".md.fixed"); + const configFile = file.replace(/\.md$/, ".json"); + let mergedConfig = null; + const actualPromise = fs.promises.stat(configFile) + .then( + function configFileExists() { + return fs.promises.readFile(configFile, helpers.utf8Encoding) + .then(JSON.parse); + }, + function noConfigFile() { + return {}; + }) + .then( + function lintWithConfig(config) { + mergedConfig = { + ...defaultConfig, + ...config + }; + return markdownlintPromise({ + "files": [ file ], + "config": mergedConfig, + "resultVersion": detailedResults ? 2 : 3 + }); + }) + .then( + function diffFixedFiles(resultVersion2or3) { + return detailedResults ? + Promise.all([ + markdownlintPromise({ + "files": [ file ], + "config": mergedConfig, + "resultVersion": 3 + }), + fs.promises.readFile(file, helpers.utf8Encoding), + fs.promises.readFile(fixedFile, helpers.utf8Encoding) + ]) + .then(function validateApplyFixes(fulfillments) { + const [ resultVersion3, content, expected ] = fulfillments; + const errors = resultVersion3[file]; + const actual = helpers.applyFixes(content, errors); + // Uncomment the following line to update *.md.fixed files + // fs.writeFileSync(fixedFile, actual, helpers.utf8Encoding); + test.equal(actual, expected, + "Unexpected output from applyFixes."); + return resultVersion2or3; + }) : + resultVersion2or3; + } + ) + .then( + function convertResultVersion2To0(resultVersion2or3) { + const result0 = {}; + const result2or3 = resultVersion2or3[file]; + result2or3.forEach(function forResult(result) { + const ruleName = result.ruleNames[0]; + const lineNumbers = result0[ruleName] || []; + if (!lineNumbers.includes(result.lineNumber)) { + lineNumbers.push(result.lineNumber); + } + result0[ruleName] = lineNumbers; + }); + return [ result0, result2or3 ]; + } + ); + const expectedPromise = detailedResults ? + fs.promises.readFile(resultsFile, helpers.utf8Encoding) + .then( + function fileContents(contents) { + // @ts-ignore + const errorObjects = JSON.parse(contents); + errorObjects.forEach(function forObject(errorObject) { + if (errorObject.ruleInformation) { + errorObject.ruleInformation = + errorObject.ruleInformation.replace("v0.0.0", `v${version}`); + } + }); + return errorObjects; + }) : + fs.promises.readFile(file, helpers.utf8Encoding) + .then( + function fileContents(contents) { + // @ts-ignore + const lines = contents.split(helpers.newLineRe); + const results = {}; + lines.forEach(function forLine(line, lineNum) { + const regex = /\{(MD\d+)(?::(\d+))?\}/g; + let match = null; + while ((match = regex.exec(line))) { + const rule = match[1]; + const errors = results[rule] || []; + errors.push( + match[2] ? + Number.parseInt(match[2], 10) : + lineNum + 1 + ); + results[rule] = errors; + } + }); + const sortedResults = {}; + Object.keys(results).sort().forEach(function forKey(key) { + sortedResults[key] = results[key]; + }); + return sortedResults; + }); + Promise.all([ actualPromise, expectedPromise ]) + .then( + function compareResults(fulfillments) { + const [ [ actual0, actual2or3 ], expected ] = fulfillments; + const actual = detailedResults ? actual2or3 : actual0; + test.deepEqual(actual, expected, "Line numbers are not correct."); + return actual2or3; + }) + .then( + function verifyFixes(errors) { + if (detailedResults) { + return test.ok(true); + } + return fs.promises.readFile(file, helpers.utf8Encoding) + .then( + function applyFixes(content) { + const corrections = helpers.applyFixes(content, errors); + return markdownlintPromise({ + "strings": { + "input": corrections + }, + "config": mergedConfig, + "resultVersion": 3 + }); + }) + .then( + function checkFixes(newErrors) { + const unfixed = newErrors.input + .filter((error) => !!error.fixInfo); + test.deepEqual(unfixed, [], "Fixable error was not fixed."); + } + ); + }) + .catch() + .then(test.done); + }; +} + +fs.readdirSync("./test") + .filter((file) => /\.md$/.test(file)) + // @ts-ignore + .forEach((file) => tape(file, createTestForFile(path.join("./test", file)))); + +tape("projectFilesNoInlineConfig", (test) => { + test.plan(2); + const options = { + "files": [ + "README.md", + "CONTRIBUTING.md", + "doc/CustomRules.md", + "helpers/README.md" + ], + "noInlineConfig": true, + "config": { + "line-length": { "line_length": 150 }, + "no-duplicate-heading": false + } + }; + markdownlint(options, function callback(err, actual) { + test.ifError(err); + const expected = { + "README.md": [], + "CONTRIBUTING.md": [], + "doc/CustomRules.md": [], + "helpers/README.md": [] + }; + test.deepEqual(actual, expected, "Issue(s) with project files."); + test.end(); + }); +}); + +tape("projectFilesInlineConfig", (test) => { + test.plan(2); + const options = { + "files": [ "doc/Rules.md" ], + "config": { + "line-length": { "line_length": 150 }, + "no-inline-html": false + } + }; + markdownlint(options, function callback(err, actual) { + test.ifError(err); + const expected = { + "doc/Rules.md": [] + }; + test.deepEqual(actual, expected, "Issue(s) with project files."); + test.end(); + }); +}); + +tape("resultObjectToStringNotEnumerable", (test) => { + test.plan(2); + const options = { + "strings": { + "string": "# Heading" + } + }; + markdownlint(options, function callback(err, result) { + test.ifError(err); + // eslint-disable-next-line guard-for-in + for (const property in result) { + test.notEqual(property, "toString", "Function should not enumerate."); + } + test.end(); + }); +}); + +tape("resultFormattingV0", (test) => { + test.plan(4); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": true, + "MD041": false + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD002": [ 3 ], + "MD018": [ 1 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": { + "MD002": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + let actualMessage = actualResult.toString(); + let expectedMessage = + "./test/atx_heading_spacing.md: 3: MD002" + + " First heading should be a top level heading\n" + + "./test/atx_heading_spacing.md: 1: MD018" + + " No space after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 3: MD019" + + " Multiple spaces after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 5: MD019" + + " Multiple spaces after hash on atx style heading\n" + + "./test/first_heading_bad_atx.md: 1: MD002" + + " First heading should be a top level heading"; + test.equal(actualMessage, expectedMessage, "Incorrect message (name)."); + // @ts-ignore + actualMessage = actualResult.toString(true); + expectedMessage = + "./test/atx_heading_spacing.md: 3: first-heading-h1" + + " First heading should be a top level heading\n" + + "./test/atx_heading_spacing.md: 1: no-missing-space-atx" + + " No space after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 3: no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 5: no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading\n" + + "./test/first_heading_bad_atx.md: 1: first-heading-h1" + + " First heading should be a top level heading"; + test.equal(actualMessage, expectedMessage, "Incorrect message (alias)."); + test.end(); + }); +}); + +tape("resultFormattingSyncV0", (test) => { + test.plan(3); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": true, + "MD041": false + }, + "resultVersion": 0 + }; + const actualResult = markdownlint.sync(options); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD002": [ 3 ], + "MD018": [ 1 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": { + "MD002": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + let actualMessage = actualResult.toString(); + let expectedMessage = + "./test/atx_heading_spacing.md: 3: MD002" + + " First heading should be a top level heading\n" + + "./test/atx_heading_spacing.md: 1: MD018" + + " No space after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 3: MD019" + + " Multiple spaces after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 5: MD019" + + " Multiple spaces after hash on atx style heading\n" + + "./test/first_heading_bad_atx.md: 1: MD002" + + " First heading should be a top level heading"; + test.equal(actualMessage, expectedMessage, "Incorrect message (name)."); + // @ts-ignore + actualMessage = actualResult.toString(true); + expectedMessage = + "./test/atx_heading_spacing.md: 3: first-heading-h1" + + " First heading should be a top level heading\n" + + "./test/atx_heading_spacing.md: 1: no-missing-space-atx" + + " No space after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 3: no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading\n" + + "./test/atx_heading_spacing.md: 5: no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading\n" + + "./test/first_heading_bad_atx.md: 1: first-heading-h1" + + " First heading should be a top level heading"; + test.equal(actualMessage, expectedMessage, "Incorrect message (alias)."); + test.end(); +}); + +tape("resultFormattingV1", (test) => { + test.plan(3); + const options = { + "strings": { + "truncate": + "# Multiple spaces inside hashes on closed atx style heading #\n" + }, + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": true, + "MD041": false + }, + "resultVersion": 1 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "truncate": [ + { "lineNumber": 1, + "ruleName": "MD021", + "ruleAlias": "no-multiple-space-closed-atx", + "ruleDescription": + "Multiple spaces inside hashes on closed atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md021`, + "errorDetail": null, + "errorContext": "# Multiple spa...tyle heading #", + "errorRange": [ 1, 4 ] } + ], + "./test/atx_heading_spacing.md": [ + { "lineNumber": 3, + "ruleName": "MD002", + "ruleAlias": "first-heading-h1", + "ruleDescription": "First heading should be a top level heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md002`, + "errorDetail": "Expected: h1; Actual: h2", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 1, + "ruleName": "MD018", + "ruleAlias": "no-missing-space-atx", + "ruleDescription": "No space after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md018`, + "errorDetail": null, + "errorContext": "#Heading 1 {MD018}", + "errorRange": [ 1, 2 ] }, + { "lineNumber": 3, + "ruleName": "MD019", + "ruleAlias": "no-multiple-space-atx", + "ruleDescription": "Multiple spaces after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md019`, + "errorDetail": null, + "errorContext": "## Heading 2 {MD019}", + "errorRange": [ 1, 5 ] }, + { "lineNumber": 5, + "ruleName": "MD019", + "ruleAlias": "no-multiple-space-atx", + "ruleDescription": "Multiple spaces after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md019`, + "errorDetail": null, + "errorContext": "## Heading 3 {MD019}", + "errorRange": [ 1, 6 ] } + ], + "./test/first_heading_bad_atx.md": [ + { "lineNumber": 1, + "ruleName": "MD002", + "ruleAlias": "first-heading-h1", + "ruleDescription": "First heading should be a top level heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md002`, + "errorDetail": "Expected: h1; Actual: h2", + "errorContext": null, + "errorRange": null } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "./test/atx_heading_spacing.md: 3: MD002/first-heading-h1" + + " First heading should be a top level heading" + + " [Expected: h1; Actual: h2]\n" + + "./test/atx_heading_spacing.md: 1: MD018/no-missing-space-atx" + + " No space after hash on atx style heading" + + " [Context: \"#Heading 1 {MD018}\"]\n" + + "./test/atx_heading_spacing.md: 3: MD019/no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading" + + " [Context: \"## Heading 2 {MD019}\"]\n" + + "./test/atx_heading_spacing.md: 5: MD019/no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading" + + " [Context: \"## Heading 3 {MD019}\"]\n" + + "./test/first_heading_bad_atx.md: 1: MD002/first-heading-h1" + + " First heading should be a top level heading" + + " [Expected: h1; Actual: h2]\n" + + "truncate: 1: MD021/no-multiple-space-closed-atx" + + " Multiple spaces inside hashes on closed atx style heading" + + " [Context: \"# Multiple spa...tyle heading #\"]"; + test.equal(actualMessage, expectedMessage, "Incorrect message."); + test.end(); + }); +}); + +tape("resultFormattingV2", (test) => { + test.plan(3); + const options = { + "strings": { + "truncate": + "# Multiple spaces inside hashes on closed atx style heading #\n" + }, + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": true, + "MD041": false + } + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "truncate": [ + { "lineNumber": 1, + "ruleNames": [ "MD021", "no-multiple-space-closed-atx" ], + "ruleDescription": + "Multiple spaces inside hashes on closed atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md021`, + "errorDetail": null, + "errorContext": "# Multiple spa...tyle heading #", + "errorRange": [ 1, 4 ] } + ], + "./test/atx_heading_spacing.md": [ + { "lineNumber": 3, + "ruleNames": [ "MD002", "first-heading-h1", "first-header-h1" ], + "ruleDescription": "First heading should be a top level heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md002`, + "errorDetail": "Expected: h1; Actual: h2", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 1, + "ruleNames": [ "MD018", "no-missing-space-atx" ], + "ruleDescription": "No space after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md018`, + "errorDetail": null, + "errorContext": "#Heading 1 {MD018}", + "errorRange": [ 1, 2 ] }, + { "lineNumber": 3, + "ruleNames": [ "MD019", "no-multiple-space-atx" ], + "ruleDescription": "Multiple spaces after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md019`, + "errorDetail": null, + "errorContext": "## Heading 2 {MD019}", + "errorRange": [ 1, 5 ] }, + { "lineNumber": 5, + "ruleNames": [ "MD019", "no-multiple-space-atx" ], + "ruleDescription": "Multiple spaces after hash on atx style heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md019`, + "errorDetail": null, + "errorContext": "## Heading 3 {MD019}", + "errorRange": [ 1, 6 ] } + ], + "./test/first_heading_bad_atx.md": [ + { "lineNumber": 1, + "ruleNames": [ "MD002", "first-heading-h1", "first-header-h1" ], + "ruleDescription": "First heading should be a top level heading", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md002`, + "errorDetail": "Expected: h1; Actual: h2", + "errorContext": null, + "errorRange": null } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "./test/atx_heading_spacing.md: 3:" + + " MD002/first-heading-h1/first-header-h1" + + " First heading should be a top level heading" + + " [Expected: h1; Actual: h2]\n" + + "./test/atx_heading_spacing.md: 1: MD018/no-missing-space-atx" + + " No space after hash on atx style heading" + + " [Context: \"#Heading 1 {MD018}\"]\n" + + "./test/atx_heading_spacing.md: 3: MD019/no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading" + + " [Context: \"## Heading 2 {MD019}\"]\n" + + "./test/atx_heading_spacing.md: 5: MD019/no-multiple-space-atx" + + " Multiple spaces after hash on atx style heading" + + " [Context: \"## Heading 3 {MD019}\"]\n" + + "./test/first_heading_bad_atx.md: 1:" + + " MD002/first-heading-h1/first-header-h1" + + " First heading should be a top level heading" + + " [Expected: h1; Actual: h2]\n" + + "truncate: 1: MD021/no-multiple-space-closed-atx" + + " Multiple spaces inside hashes on closed atx style heading" + + " [Context: \"# Multiple spa...tyle heading #\"]"; + test.equal(actualMessage, expectedMessage, "Incorrect message."); + test.end(); + }); +}); + +tape("resultFormattingV3", (test) => { + test.plan(3); + const options = { + "strings": { + "input": + "# Heading \n" + + "\n" + + "Text\ttext\t\ttext\n" + + "Text * emphasis * text" + }, + "resultVersion": 3 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { + "lineNumber": 1, + "ruleNames": [ "MD009", "no-trailing-spaces" ], + "ruleDescription": "Trailing spaces", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md009`, + "errorDetail": "Expected: 0 or 2; Actual: 3", + "errorContext": null, + "errorRange": [ 10, 3 ], + "fixInfo": { + "editColumn": 10, + "deleteCount": 3 + } + }, + { + "lineNumber": 3, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 5", + "errorContext": null, + "errorRange": [ 5, 1 ], + "fixInfo": { + "editColumn": 5, + "deleteCount": 1, + "insertText": " " + } + }, + { + "lineNumber": 3, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 10", + "errorContext": null, + "errorRange": [ 10, 2 ], + "fixInfo": { + "editColumn": 10, + "deleteCount": 2, + "insertText": " " + } + }, + { + "lineNumber": 4, + "ruleNames": [ "MD037", "no-space-in-emphasis" ], + "ruleDescription": "Spaces inside emphasis markers", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md037`, + "errorDetail": null, + "errorContext": "* emphasis *", + "errorRange": [ 6, 12 ], + "fixInfo": { + "editColumn": 6, + "deleteCount": 12, + "insertText": "*emphasis*" + } + }, + { + "lineNumber": 4, + "ruleNames": [ "MD047", "single-trailing-newline" ], + "ruleDescription": "Files should end with a single newline character", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md047`, + "errorDetail": null, + "errorContext": null, + "errorRange": [ 22, 1 ], + "fixInfo": { + "insertText": "\n", + "editColumn": 23 + } + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "input: 1: MD009/no-trailing-spaces" + + " Trailing spaces [Expected: 0 or 2; Actual: 3]\n" + + "input: 3: MD010/no-hard-tabs" + + " Hard tabs [Column: 5]\n" + + "input: 3: MD010/no-hard-tabs" + + " Hard tabs [Column: 10]\n" + + "input: 4: MD037/no-space-in-emphasis" + + " Spaces inside emphasis markers [Context: \"* emphasis *\"]\n" + + "input: 4: MD047/single-trailing-newline" + + " Files should end with a single newline character"; + test.equal(actualMessage, expectedMessage, "Incorrect message."); + test.end(); + }); +}); + +tape("onePerLineResultVersion0", (test) => { + test.plan(2); + const options = { + "strings": { + "input": "# Heading\theading\t\theading\n" + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": { + "MD010": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("onePerLineResultVersion1", (test) => { + test.plan(2); + const options = { + "strings": { + "input": "# Heading\theading\t\theading\n" + }, + "resultVersion": 1 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { + "lineNumber": 1, + "ruleName": "MD010", + "ruleAlias": "no-hard-tabs", + "ruleDescription": "Hard tabs", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 10", + "errorContext": null, + "errorRange": [ 10, 1 ] + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("onePerLineResultVersion2", (test) => { + test.plan(2); + const options = { + "strings": { + "input": "# Heading\theading\t\theading\n" + }, + "resultVersion": 2 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { + "lineNumber": 1, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 10", + "errorContext": null, + "errorRange": [ 10, 1 ] + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("manyPerLineResultVersion3", (test) => { + test.plan(2); + const options = { + "strings": { + "input": "# Heading\theading\t\theading\n" + }, + "resultVersion": 3 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { + "lineNumber": 1, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 10", + "errorContext": null, + "errorRange": [ 10, 1 ], + "fixInfo": { + "editColumn": 10, + "deleteCount": 1, + "insertText": " " + } + }, + { + "lineNumber": 1, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 18", + "errorContext": null, + "errorRange": [ 18, 2 ], + "fixInfo": { + "editColumn": 18, + "deleteCount": 2, + "insertText": " " + } + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("frontMatterResultVersion3", (test) => { + test.plan(2); + const options = { + "strings": { + "input": "---\n---\n# Heading\nText\n" + }, + "resultVersion": 3 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { + "lineNumber": 3, + "ruleNames": + [ "MD022", "blanks-around-headings", "blanks-around-headers" ], + "ruleDescription": "Headings should be surrounded by blank lines", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md022`, + "errorDetail": "Expected: 1; Actual: 0; Below", + "errorContext": "# Heading", + "errorRange": null, + "fixInfo": { + "lineNumber": 4, + "insertText": "\n" + } + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("stringInputLineEndings", (test) => { + test.plan(2); + const options = { + "strings": { + "cr": "One\rTwo\r#Three\n", + "lf": "One\nTwo\n#Three\n", + "crlf": "One\r\nTwo\r\n#Three\n", + "mixed": "One\rTwo\n#Three\n" + }, + "config": defaultConfig, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "cr": { "MD018": [ 3 ] }, + "lf": { "MD018": [ 3 ] }, + "crlf": { "MD018": [ 3 ] }, + "mixed": { "MD018": [ 3 ] } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("inputOnlyNewline", (test) => { + test.plan(2); + const options = { + "strings": { + "cr": "\r", + "lf": "\n", + "crlf": "\r\n" + }, + "config": { + "default": false + } + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "cr": [], + "lf": [], + "crlf": [] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("defaultTrue", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "default": true + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD018": [ 1 ], + "MD019": [ 3, 5 ], + "MD041": [ 1 ] + }, + "./test/first_heading_bad_atx.md": { + "MD041": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("defaultFalse", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "default": false + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": {}, + "./test/first_heading_bad_atx.md": {} + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("defaultUndefined", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": {}, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD018": [ 1 ], + "MD019": [ 3, 5 ], + "MD041": [ 1 ] + }, + "./test/first_heading_bad_atx.md": { + "MD041": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("disableRules", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": false, + "default": true, + "MD019": false, + "first-line-h1": false + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD018": [ 1 ] + }, + "./test/first_heading_bad_atx.md": {} + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("enableRules", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "MD002": true, + "default": false, + "no-multiple-space-atx": true + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD002": [ 3 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": { + "MD002": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("enableRulesMixedCase", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "Md002": true, + "DeFaUlT": false, + "nO-mUlTiPlE-sPaCe-AtX": true + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD002": [ 3 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": { + "MD002": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("disableTag", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "default": true, + "spaces": false + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD041": [ 1 ] + }, + "./test/first_heading_bad_atx.md": { + "MD041": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("enableTag", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "default": false, + "spaces": true, + "notatag": true + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD018": [ 1 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": {} + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("enableTagMixedCase", (test) => { + test.plan(2); + const options = { + "files": [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ], + "config": { + "DeFaUlT": false, + "SpAcEs": true, + "NoTaTaG": true + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/atx_heading_spacing.md": { + "MD018": [ 1 ], + "MD019": [ 3, 5 ] + }, + "./test/first_heading_bad_atx.md": {} + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("styleFiles", (test) => { + test.plan(4); + fs.readdir("./style", function readdir(err, files) { + test.ifError(err); + files.forEach(function forFile(file) { + test.ok(require(path.join("../style", file)), "Unable to load/parse."); + }); + test.end(); + }); +}); + +tape("styleAll", (test) => { + test.plan(2); + const options = { + "files": [ "./test/break-all-the-rules.md" ], + "config": require("../style/all.json"), + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/break-all-the-rules.md": { + "MD001": [ 3 ], + "MD003": [ 5, 31 ], + "MD004": [ 8 ], + "MD005": [ 12 ], + "MD007": [ 8, 11 ], + "MD009": [ 14 ], + "MD010": [ 14 ], + "MD011": [ 16 ], + "MD012": [ 18 ], + "MD013": [ 21 ], + "MD014": [ 23 ], + "MD018": [ 25 ], + "MD019": [ 27 ], + "MD020": [ 29 ], + "MD021": [ 31 ], + "MD022": [ 86 ], + "MD023": [ 40 ], + "MD024": [ 35 ], + "MD026": [ 40 ], + "MD027": [ 42 ], + "MD028": [ 43 ], + "MD029": [ 47 ], + "MD030": [ 8 ], + "MD031": [ 50 ], + "MD032": [ 7, 8, 51 ], + "MD033": [ 55 ], + "MD034": [ 57 ], + "MD035": [ 61 ], + "MD036": [ 65 ], + "MD037": [ 67 ], + "MD038": [ 69 ], + "MD039": [ 71 ], + "MD040": [ 73 ], + "MD041": [ 1 ], + "MD042": [ 81 ], + "MD045": [ 85 ], + "MD046": [ 49, 73, 77 ], + "MD047": [ 88 ], + "MD048": [ 77 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("styleRelaxed", (test) => { + test.plan(2); + const options = { + "files": [ "./test/break-all-the-rules.md" ], + "config": require("../style/relaxed.json"), + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/break-all-the-rules.md": { + "MD001": [ 3 ], + "MD003": [ 5, 31 ], + "MD004": [ 8 ], + "MD005": [ 12 ], + "MD011": [ 16 ], + "MD014": [ 23 ], + "MD018": [ 25 ], + "MD019": [ 27 ], + "MD020": [ 29 ], + "MD021": [ 31 ], + "MD022": [ 86 ], + "MD023": [ 40 ], + "MD024": [ 35 ], + "MD026": [ 40 ], + "MD029": [ 47 ], + "MD031": [ 50 ], + "MD032": [ 7, 8, 51 ], + "MD035": [ 61 ], + "MD036": [ 65 ], + "MD042": [ 81 ], + "MD045": [ 85 ], + "MD046": [ 49, 73, 77 ], + "MD047": [ 88 ], + "MD048": [ 77 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("nullFrontMatter", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "content": "---\n\t\n---\n# Heading\n" + }, + "frontMatter": null, + "config": { + "default": false, + "MD010": true + }, + "resultVersion": 0 + }, function callback(err, result) { + test.ifError(err); + const expectedResult = { + "content": { "MD010": [ 2 ] } + }; + test.deepEqual(result, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customFrontMatter", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "content": "\n\t\n\n# Heading\n" + }, + "frontMatter": /[^]*<\/head>/, + "config": { + "default": false, + "MD010": true + } + }, function callback(err, result) { + test.ifError(err); + const expectedResult = { + "content": [] + }; + test.deepEqual(result, expectedResult, "Did not get empty results."); + test.end(); + }); +}); + +tape("noInlineConfig", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "content": [ + "# Heading", + "", + "\tTab", + "", + "", + "", + "\tTab", + "", + "", + "", + "\tTab\n" + ].join("\n") + }, + "noInlineConfig": true, + "resultVersion": 0 + }, function callback(err, result) { + test.ifError(err); + const expectedResult = { + "content": { + "MD010": [ 3, 7, 11 ] + } + }; + test.deepEqual(result, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("readmeHeadings", (test) => { + test.plan(2); + markdownlint({ + "files": "README.md", + "noInlineConfig": true, + "config": { + "default": false, + "MD013": { + "line_length": 150 + }, + "MD043": { + "headings": [ + "# markdownlint", + "## Install", + "## Overview", + "### Related", + "## Demonstration", + "## Rules / Aliases", + "## Tags", + "## Configuration", + "## API", + "### Linting", + "#### options", + "##### options.customRules", + "##### options.files", + "##### options.strings", + "##### options.config", + "##### options.frontMatter", + "##### options.handleRuleFailures", + "##### options.noInlineConfig", + "##### options.resultVersion", + "##### options.markdownItPlugins", + "#### callback", + "#### result", + "### Config", + "#### file", + "#### parsers", + "#### callback", + "#### result", + "## Usage", + "## Browser", + "## Examples", + "## Contributing", + "## History" + ] + } + } + }, function callback(err, result) { + test.ifError(err); + const expected = { "README.md": [] }; + test.deepEqual(result, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("filesArrayNotModified", (test) => { + test.plan(2); + const files = [ + "./test/atx_heading_spacing.md", + "./test/first_heading_bad_atx.md" + ]; + const expectedFiles = files.slice(); + markdownlint({ "files": files }, function callback(err) { + test.ifError(err); + test.deepEqual(files, expectedFiles, "Files modified."); + test.end(); + }); +}); + +tape("filesArrayAsString", (test) => { + test.plan(2); + markdownlint({ + "files": "README.md", + "noInlineConfig": true, + "config": { + "MD013": { "line_length": 150 }, + "MD024": false + } + }, function callback(err, actual) { + test.ifError(err); + const expected = { "README.md": [] }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("missingOptions", (test) => { + test.plan(2); + markdownlint(null, function callback(err, result) { + test.ifError(err); + test.deepEqual( + result, + {}, + "Did not get empty result for missing options." + ); + test.end(); + }); +}); + +tape("missingFilesAndStrings", (test) => { + test.plan(2); + markdownlint({}, function callback(err, result) { + test.ifError(err); + test.ok(result, "Did not get result for missing files/strings."); + test.end(); + }); +}); + +tape("missingCallback", (test) => { + test.plan(0); + // @ts-ignore + markdownlint(); + test.end(); +}); + +tape("badFile", (test) => { + test.plan(4); + markdownlint({ + "files": [ "./badFile" ] + }, function callback(err, result) { + test.ok(err, "Did not get an error for bad file."); + test.ok(err instanceof Error, "Error not instance of Error."); + // @ts-ignore + test.equal(err.code, "ENOENT", "Error code for bad file not ENOENT."); + test.ok(!result, "Got result for bad file."); + test.end(); + }); +}); + +tape("badFileSync", (test) => { + test.plan(1); + test.throws( + function badFileCall() { + markdownlint.sync({ + "files": [ "./badFile" ] + }); + }, + /ENOENT/, + "Did not get correct exception for bad file." + ); + test.end(); +}); + +tape("missingStringValue", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "undefined": undefined, + "null": null, + "empty": "" + }, + "config": defaultConfig + }, function callback(err, result) { + test.ifError(err); + const expectedResult = { + "undefined": [], + "null": [], + "empty": [] + }; + test.deepEqual(result, expectedResult, "Did not get empty results."); + test.end(); + }); +}); + +tape("readme", (test) => { + test.plan(115); + const tagToRules = {}; + rules.forEach(function forRule(rule) { + rule.tags.forEach(function forTag(tag) { + const tagRules = tagToRules[tag] || []; + tagRules.push(rule.names[0]); + tagToRules[tag] = tagRules; + }); + }); + fs.readFile("README.md", helpers.utf8Encoding, + function readFile(err, contents) { + test.ifError(err); + const rulesLeft = rules.slice(); + let seenRelated = false; + let seenRules = false; + let inRules = false; + let seenTags = false; + let inTags = false; + md.parse(contents, {}).forEach(function forToken(token) { + if ( + (token.type === "bullet_list_open") && + (token.level === 0) + ) { + if (!seenRelated) { + seenRelated = true; + } else if (seenRelated && !seenRules) { + seenRules = true; + inRules = true; + } else if (seenRelated && seenRules && !seenTags) { + seenTags = true; + inTags = true; + } + } else if ( + (token.type === "bullet_list_close") && + (token.level === 0) + ) { + inRules = false; + inTags = false; + } else if (token.type === "inline") { + if (inRules) { + const rule = rulesLeft.shift(); + test.ok(rule, + "Missing rule implementation for " + token.content + "."); + if (rule) { + const ruleName = rule.names[0]; + const ruleAliases = rule.names.slice(1); + let expected = "**[" + ruleName + "](doc/Rules.md#" + + ruleName.toLowerCase() + ")** *" + + ruleAliases.join("/") + "* - " + rule.description; + if (deprecatedRuleNames.has(ruleName)) { + expected = "~~" + expected + "~~"; + } + test.equal(token.content, expected, "Rule mismatch."); + } + } else if (inTags) { + const parts = + token.content.replace(/\*\*/g, "").split(/ - |, |,\n/); + const tag = parts.shift(); + test.deepEqual(parts, tagToRules[tag] || [], + "Rule mismatch for tag " + tag + "."); + delete tagToRules[tag]; + } + } + }); + const ruleLeft = rulesLeft.shift(); + test.ok(!ruleLeft, + "Missing rule documentation for " + + (ruleLeft || "[NO RULE]").toString() + "."); + const tagLeft = Object.keys(tagToRules).shift(); + test.ok(!tagLeft, "Undocumented tag " + tagLeft + "."); + test.end(); + }); +}); + +tape("rules", (test) => { + test.plan(336); + fs.readFile("doc/Rules.md", helpers.utf8Encoding, + (err, contents) => { + test.ifError(err); + const rulesLeft = rules.slice(); + let inHeading = false; + let rule = null; + let ruleHasTags = true; + let ruleHasAliases = true; + let ruleUsesParams = null; + const tagAliasParameterRe = /, |: | /; + // eslint-disable-next-line func-style + const testTagsAliasesParams = (r) => { + r = r || "[NO RULE]"; + test.ok(ruleHasTags, + "Missing tags for rule " + r.names + "."); + test.ok(ruleHasAliases, + "Missing aliases for rule " + r.names + "."); + test.ok(!ruleUsesParams, + "Missing parameters for rule " + r.names + "."); + }; + md.parse(contents, {}).forEach(function forToken(token) { + if ((token.type === "heading_open") && (token.tag === "h2")) { + inHeading = true; + } else if (token.type === "heading_close") { + inHeading = false; + } else if (token.type === "inline") { + if (inHeading) { + testTagsAliasesParams(rule); + rule = rulesLeft.shift(); + ruleHasTags = false; + ruleHasAliases = false; + test.ok(rule, + "Missing rule implementation for " + token.content + "."); + const ruleName = rule.names[0]; + let headingContent = ruleName + " - " + rule.description; + if (deprecatedRuleNames.has(ruleName)) { + headingContent = "~~" + headingContent + "~~"; + } + test.equal(token.content, + headingContent, + "Rule mismatch."); + ruleUsesParams = rule.function.toString() + .match(/params\.config\.[_a-z]*/gi); + if (ruleUsesParams) { + ruleUsesParams = ruleUsesParams.map(function forUse(use) { + return use.split(".").pop(); + }); + ruleUsesParams.sort(); + } + } else if (token.content.startsWith("Tags: ") && rule) { + test.deepEqual(token.content.split(tagAliasParameterRe).slice(1), + rule.tags, "Tag mismatch for rule " + rule.names + "."); + ruleHasTags = true; + } else if (token.content.startsWith("Aliases: ") && rule) { + test.deepEqual(token.content.split(tagAliasParameterRe).slice(1), + rule.names.slice(1), + "Alias mismatch for rule " + rule.names + "."); + ruleHasAliases = true; + } else if (token.content.startsWith("Parameters: ") && rule) { + let inDetails = false; + const parameters = token.content.split(tagAliasParameterRe) + .slice(1) + .filter(function forPart(part) { + inDetails = inDetails || (part[0] === "("); + return !inDetails; + }); + parameters.sort(); + test.deepEqual(parameters, ruleUsesParams, + "Missing parameter for rule " + rule.names); + ruleUsesParams = null; + } + } + }); + const ruleLeft = rulesLeft.shift(); + test.ok(!ruleLeft, + "Missing rule documentation for " + + (ruleLeft || { "names": "[NO RULE]" }).names + "."); + if (rule) { + testTagsAliasesParams(rule); + } + test.end(); + }); +}); + +tape("validateConfigSchema", (test) => { + const jsonFileRe = /\.json$/i; + const resultsFileRe = /\.results\.json$/i; + const jsConfigFileRe = /^jsconfig\.json$/i; + const wrongTypesFileRe = /wrong-types-in-config-file.json$/i; + const testDirectory = __dirname; + const testFiles = fs.readdirSync(testDirectory); + testFiles.filter(function filterFile(file) { + return jsonFileRe.test(file) && + !resultsFileRe.test(file) && + !jsConfigFileRe.test(file) && + !wrongTypesFileRe.test(file); + }).forEach(function forFile(file) { + const data = fs.readFileSync( + path.join(testDirectory, file), + helpers.utf8Encoding + ); + test.ok( + tv4.validate(JSON.parse(data), configSchema), + file + "\n" + JSON.stringify(tv4.error, null, 2)); + }); + test.end(); +}); + +tape("clearHtmlCommentTextValid", (test) => { + test.plan(1); + const validComments = [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "texttext", + "texttext", + "texttext", + "", + "texttexttext", + "texttext", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "texttext", + "texttext", + "texttext", + "", + "texttexttext", + "texttext", + "", + "", + "-->", + "-->", + "", + "", + "", + "-->", + " -->", + "-->", + "text-->", + "text-->", + "", + "" + ]; + const actual = helpers.clearHtmlCommentText(invalidComments.join("\n")); + const expected = invalidComments.join("\n"); + test.equal(actual, expected); + test.end(); +}); + +tape("clearHtmlCommentTextNonGreedy", (test) => { + test.plan(1); + const nonGreedyComments = [ + " -->", + " -->", + " -->", + " -->" + ]; + const nonGreedyResult = [ + " -->", + " -->", + " -->", + " -->" + ]; + const actual = helpers.clearHtmlCommentText(nonGreedyComments.join("\n")); + const expected = nonGreedyResult.join("\n"); + test.equal(actual, expected); + test.end(); +}); + +tape("clearHtmlCommentTextEmbedded", (test) => { + test.plan(1); + const embeddedComments = [ + "texttext", + "", + "texttext", + "texttext", + "texttext" + ]; + const embeddedResult = [ + "texttext", + "", + "texttext", + "texttext", + "texttext" + ]; + const actual = helpers.clearHtmlCommentText(embeddedComments.join("\n")); + const expected = embeddedResult.join("\n"); + test.equal(actual, expected); + test.end(); +}); + +tape("unescapeMarkdown", (test) => { + test.plan(7); + // Test cases from https://spec.commonmark.org/0.29/#backslash-escapes + const testCases = [ + [ + "\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;" + + "\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~", + "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" + ], + [ + "\\→\\A\\a\\ \\3\\φ\\«", + "\\→\\A\\a\\ \\3\\φ\\«" + ], + [ + `\\*not emphasized* +\\
not a tag +\\[not a link](/foo) +\\\`not code\` +1\\. not a list +\\* not a list +\\# not a heading +\\[foo]: /url "not a reference" +\\ö not a character entity`, + `*not emphasized* +
not a tag +[not a link](/foo) +\`not code\` +1. not a list +* not a list +# not a heading +[foo]: /url "not a reference" +ö not a character entity` + ], + [ + "\\\\*emphasis*", + "\\*emphasis*" + ], + [ + `foo\\ +bar`, + `foo\\ +bar` + ], + [ + "Text \\<", + "Text _", + "_" + ], + [ + "Text \\\\<", + "Text _<", + "_" + ] + ]; + testCases.forEach(function forTestCase(testCase) { + const [ markdown, expected, replacement ] = testCase; + const actual = helpers.unescapeMarkdown(markdown, replacement); + test.equal(actual, expected); + }); + test.end(); +}); + +tape("isBlankLine", (test) => { + test.plan(25); + const blankLines = [ + null, + "", + " ", + " ", + "\t\t\t", + "\r", + "\n", + "\t\r\n", + " ", + "", + "", + "\t", + ">", + "> ", + "> > > \t", + "> ", + ">>" + ]; + blankLines.forEach((line) => test.ok(helpers.isBlankLine(line), line)); + const nonBlankLines = [ + "text", + " text ", + ".", + "> .", + " text", + "", + "" + ]; + nonBlankLines.forEach((line) => test.ok(!helpers.isBlankLine(line), line)); + test.end(); +}); + +tape("includesSorted", (test) => { + test.plan(154); + const inputs = [ + [ ], + [ 8 ], + [ 7, 11 ], + [ 0, 1, 2, 3, 5, 8, 13 ], + [ 2, 3, 5, 7, 11, 13, 17, 19 ], + [ 1, 3, 5, 7, 9, 11, 13, 15, 17, 19 ], + [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 ] + ]; + inputs.forEach((input) => { + for (let i = 0; i <= 21; i++) { + test.equal(helpers.includesSorted(input, i), input.includes(i)); + } + }); + test.end(); +}); + +tape("forEachInlineCodeSpan", (test) => { + test.plan(99); + const testCases = + [ + { + "input": "`code`", + "expecteds": [ [ "code", 0, 1, 1 ] ] + }, + { + "input": "text `code` text", + "expecteds": [ [ "code", 0, 6, 1 ] ] + }, + { + "input": "text `code` text `edoc`", + "expecteds": [ + [ "code", 0, 6, 1 ], + [ "edoc", 0, 18, 1 ] + ] + }, + { + "input": "text `code` text `edoc` text", + "expecteds": [ + [ "code", 0, 6, 1 ], + [ "edoc", 0, 18, 1 ] + ] + }, + { + "input": "text ``code`code`` text", + "expecteds": [ [ "code`code", 0, 7, 2 ] ] + }, + { + "input": "`code `` code`", + "expecteds": [ [ "code `` code", 0, 1, 1 ] ] + }, + { + "input": "`code\\`text`", + "expecteds": [ [ "code\\", 0, 1, 1 ] ] + }, + { + "input": "``\ncode\n``", + "expecteds": [ [ "\ncode\n", 0, 2, 2 ] ] + }, + { + "input": "text\n`code`\ntext", + "expecteds": [ [ "code", 1, 1, 1 ] ] + }, + { + "input": "text\ntext\n`code`\ntext\n`edoc`\ntext", + "expecteds": [ + [ "code", 2, 1, 1 ], + [ "edoc", 4, 1, 1 ] + ] + }, + { + "input": "text `code\nedoc` text", + "expecteds": [ [ "code\nedoc", 0, 6, 1 ] ] + }, + { + "input": "> text `code` text", + "expecteds": [ [ "code", 0, 8, 1 ] ] + }, + { + "input": "> text\n> `code`\n> text", + "expecteds": [ [ "code", 1, 3, 1 ] ] + }, + { + "input": "> text\n> `code\n> edoc`\n> text", + "expecteds": [ [ "code\n> edoc", 1, 3, 1 ] ] + }, + { + "input": "```text``", + "expecteds": [] + }, + { + "input": "text `text text", + "expecteds": [] + }, + { + "input": "`text``code``", + "expecteds": [ [ "code", 0, 7, 2 ] ] + }, + { + "input": "text \\` text `code`", + "expecteds": [ [ "code", 0, 14, 1 ] ] + }, + { + "input": "text\\\n`code`", + "expecteds": [ [ "code", 1, 1, 1 ] ] + } + ]; + testCases.forEach((testCase) => { + const { input, expecteds } = testCase; + helpers.forEachInlineCodeSpan(input, (code, line, column, ticks) => { + const [ expectedCode, expectedLine, expectedColumn, expectedTicks ] = + expecteds.shift(); + test.equal(code, expectedCode, input); + test.equal(line, expectedLine, input); + test.equal(column, expectedColumn, input); + test.equal(ticks, expectedTicks, input); + }); + test.equal(expecteds.length, 0, "length"); + }); + test.end(); +}); + +tape("getPreferredLineEnding", (test) => { + test.plan(17); + const testCases = [ + [ "", os.EOL ], + [ "\r", "\r" ], + [ "\n", "\n" ], + [ "\r\n", "\r\n" ], + [ "t\rt\nt", "\n" ], + [ "t\nt\rt", "\n" ], + [ "t\r\nt\nt", "\n" ], + [ "t\nt\r\nt", "\n" ], + [ "t\r\nt\rt", "\r\n" ], + [ "t\rt\r\nt", "\r\n" ], + [ "t\r\nt\rt\nt", "\n" ], + [ "t\r\nt\r\nt\r\nt", "\r\n" ], + [ "t\nt\nt\nt", "\n" ], + [ "t\rt\rt\rt", "\r" ], + [ "t\r\nt\nt\r\nt", "\r\n" ], + [ "t\nt\r\nt\nt", "\n" ], + [ "t\rt\t\rt", "\r" ] + ]; + testCases.forEach((testCase) => { + const [ input, expected ] = testCase; + const actual = helpers.getPreferredLineEnding(input); + test.equal(actual, expected, "Incorrect line ending returned."); + }); + test.end(); +}); + +tape("applyFix", (test) => { + test.plan(4); + const testCases = [ + [ + "Hello world.", + { + "editColumn": 12, + "deleteCount": 1 + }, + undefined, + "Hello world" + ], + [ + "Hello world.", + { + "editColumn": 13, + "insertText": "\n" + }, + undefined, + "Hello world.\n" + ], + [ + "Hello world.", + { + "editColumn": 13, + "insertText": "\n" + }, + "\n", + "Hello world.\n" + ], + [ + "Hello world.", + { + "editColumn": 13, + "insertText": "\n" + }, + "\r\n", + "Hello world.\r\n" + ] + ]; + testCases.forEach((testCase) => { + const [ line, fixInfo, lineEnding, expected ] = testCase; + // @ts-ignore + const actual = helpers.applyFix(line, fixInfo, lineEnding); + test.equal(actual, expected, "Incorrect fix applied."); + }); + test.end(); +}); + +tape("applyFixes", (test) => { + test.plan(28); + const testCases = [ + [ + "Hello world.", + [], + "Hello world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": {} + } + ], + "Hello world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "insertText": "Very " + } + } + ], + "Very Hello world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 7, + "insertText": "big " + } + } + ], + "Hello big world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "deleteCount": 6 + } + } + ], + "world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 7, + "deleteCount": 5, + "insertText": "there" + } + } + ], + "Hello there." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 12, + "deleteCount": 1 + } + }, + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 6, + "deleteCount": 1 + } + } + ], + "Helloworld" + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 13, + "insertText": " Hi." + } + } + ], + "Hello world. Hi." + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 1, + "fixInfo": { + "deleteCount": -1 + } + } + ], + "world" + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 2, + "fixInfo": { + "deleteCount": -1 + } + } + ], + "Hello" + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 2, + "fixInfo": { + "lineNumber": 1, + "deleteCount": -1 + } + } + ], + "world" + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 1, + "fixInfo": { + "lineNumber": 2, + "deleteCount": -1 + } + } + ], + "Hello" + ], + [ + "Hello world", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 4, + "deleteCount": 1 + } + }, + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 10, + "deleteCount": 1 + } + } + ], + "Helo word" + ], + [ + "Hello world", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 10, + "deleteCount": 1 + } + }, + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 4, + "deleteCount": 1 + } + } + ], + "Helo word" + ], + [ + "Hello\nworld", + [ + { + "fixInfo": { + "lineNumber": 1, + "deleteCount": -1 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "insertText": "Big " + } + } + ], + "world" + ], + [ + "Hello\nworld", + [ + { + "fixInfo": { + "lineNumber": 1, + "deleteCount": -1 + } + }, + { + "fixInfo": { + "lineNumber": 2, + "deleteCount": -1 + } + } + ], + "" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "insertText": "aa" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "insertText": "b" + } + } + ], + "aaHello world" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "insertText": "a" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "insertText": "bb" + } + } + ], + "bbHello world" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 6, + "insertText": " big" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1 + } + } + ], + "Hello big orld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 8, + "deleteCount": 2 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 2 + } + } + ], + "Hello wld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 2 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 8, + "deleteCount": 2 + } + } + ], + "Hello wld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1, + "insertText": "z" + } + } + ], + "Hello zorld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "insertText": "z" + } + } + ], + "Hello zorld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "insertText": "z" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1 + } + } + ], + "Hello zorld" + ], + [ + "Hello\nworld\nhello\rworld", + [ + { + "fixInfo": { + "lineNumber": 4, + "editColumn": 6, + "insertText": "\n" + } + } + ], + "Hello\nworld\nhello\nworld\n" + ], + [ + "Hello\r\nworld\r\nhello\nworld", + [ + { + "fixInfo": { + "lineNumber": 4, + "editColumn": 6, + "insertText": "\n" + } + } + ], + "Hello\r\nworld\r\nhello\r\nworld\r\n" + ], + [ + "Hello\rworld\rhello\nworld", + [ + { + "fixInfo": { + "lineNumber": 4, + "editColumn": 6, + "insertText": "\n" + } + } + ], + "Hello\rworld\rhello\rworld\r" + ], + [ + "Hello\r\nworld", + [ + { + "lineNumber": 2, + "fixInfo": { + "editColumn": 6, + "insertText": "\n\n" + } + } + ], + "Hello\r\nworld\r\n\r\n" + ] + ]; + testCases.forEach((testCase) => { + const [ input, errors, expected ] = testCase; + const actual = helpers.applyFixes(input, errors); + test.equal(actual, expected, "Incorrect fix applied."); + }); + test.end(); +}); + +tape("configSingle", (test) => { + test.plan(2); + markdownlint.readConfig("./test/config/config-child.json", + function callback(err, actual) { + test.ifError(err); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configAbsolute", (test) => { + test.plan(2); + markdownlint.readConfig(path.join(__dirname, "config", "config-child.json"), + function callback(err, actual) { + test.ifError(err); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configMultiple", (test) => { + test.plan(2); + markdownlint.readConfig("./test/config/config-grandparent.json", + function callback(err, actual) { + test.ifError(err); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configBadFile", (test) => { + test.plan(4); + markdownlint.readConfig("./test/config/config-badfile.json", + function callback(err, result) { + test.ok(err, "Did not get an error for bad file."); + test.ok(err instanceof Error, "Error not instance of Error."); + // @ts-ignore + test.equal(err.code, "ENOENT", "Error code for bad file not ENOENT."); + test.ok(!result, "Got result for bad file."); + test.end(); + }); +}); + +tape("configBadChildFile", (test) => { + test.plan(4); + markdownlint.readConfig("./test/config/config-badchildfile.json", + function callback(err, result) { + test.ok(err, "Did not get an error for bad child file."); + test.ok(err instanceof Error, "Error not instance of Error."); + // @ts-ignore + test.equal(err.code, "ENOENT", + "Error code for bad child file not ENOENT."); + test.ok(!result, "Got result for bad child file."); + test.end(); + }); +}); + +tape("configBadJson", (test) => { + test.plan(3); + markdownlint.readConfig("./test/config/config-badjson.json", + function callback(err, result) { + test.ok(err, "Did not get an error for bad JSON."); + test.ok(err instanceof Error, "Error not instance of Error."); + test.ok(!result, "Got result for bad JSON."); + test.end(); + }); +}); + +tape("configBadChildJson", (test) => { + test.plan(3); + markdownlint.readConfig("./test/config/config-badchildjson.json", + function callback(err, result) { + test.ok(err, "Did not get an error for bad child JSON."); + test.ok(err instanceof Error, "Error not instance of Error."); + test.ok(!result, "Got result for bad child JSON."); + test.end(); + }); +}); + +tape("configSingleYaml", (test) => { + test.plan(2); + markdownlint.readConfig( + "./test/config/config-child.yaml", + [ require("js-yaml").safeLoad ], + function callback(err, actual) { + test.ifError(err); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configMultipleYaml", (test) => { + test.plan(2); + markdownlint.readConfig( + "./test/config/config-grandparent.yaml", + [ require("js-yaml").safeLoad ], + function callback(err, actual) { + test.ifError(err); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configMultipleHybrid", (test) => { + test.plan(2); + markdownlint.readConfig( + "./test/config/config-grandparent-hybrid.yaml", + [ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ], + function callback(err, actual) { + test.ifError(err); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepLooseEqual(actual, expected, "Config object not correct."); + test.end(); + }); +}); + +tape("configBadHybrid", (test) => { + test.plan(4); + markdownlint.readConfig( + "./test/config/config-badcontent.txt", + [ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ], + function callback(err, result) { + test.ok(err, "Did not get an error for bad child JSON."); + test.ok(err instanceof Error, "Error not instance of Error."); + test.ok(err.message.match( + // eslint-disable-next-line max-len + /^Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+; Expected [^;]+ or end of input but "\S+" found.; end of the stream or a document separator is expected at line \d+, column \d+:[^;]*$/ + ), "Error message unexpected."); + test.ok(!result, "Got result for bad child JSON."); + test.end(); + }); +}); + +tape("configSingleSync", (test) => { + test.plan(1); + const actual = markdownlint.readConfigSync("./test/config/config-child.json"); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configAbsoluteSync", (test) => { + test.plan(1); + const actual = markdownlint.readConfigSync( + path.join(__dirname, "config", "config-child.json")); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configMultipleSync", (test) => { + test.plan(1); + const actual = + markdownlint.readConfigSync("./test/config/config-grandparent.json"); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configBadFileSync", (test) => { + test.plan(1); + test.throws( + function badFileCall() { + markdownlint.readConfigSync("./test/config/config-badfile.json"); + }, + /ENOENT/, + "Did not get correct exception for bad file." + ); + test.end(); +}); + +tape("configBadChildFileSync", (test) => { + test.plan(1); + test.throws( + function badChildFileCall() { + markdownlint.readConfigSync("./test/config/config-badchildfile.json"); + }, + /ENOENT/, + "Did not get correct exception for bad child file." + ); + test.end(); +}); + +tape("configBadJsonSync", (test) => { + test.plan(1); + test.throws( + function badJsonCall() { + markdownlint.readConfigSync("./test/config/config-badjson.json"); + }, + /Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+/, + "Did not get correct exception for bad JSON." + ); + test.end(); +}); + +tape("configBadChildJsonSync", (test) => { + test.plan(1); + test.throws( + function badChildJsonCall() { + markdownlint.readConfigSync("./test/config/config-badchildjson.json"); + }, + /Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+/, + "Did not get correct exception for bad child JSON." + ); + test.end(); +}); + +tape("configSingleYamlSync", (test) => { + test.plan(1); + const actual = markdownlint.readConfigSync( + "./test/config/config-child.yaml", [ require("js-yaml").safeLoad ]); + const expected = require("./config/config-child.json"); + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configMultipleYamlSync", (test) => { + test.plan(1); + const actual = markdownlint.readConfigSync( + "./test/config/config-grandparent.yaml", [ require("js-yaml").safeLoad ]); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configMultipleHybridSync", (test) => { + test.plan(1); + const actual = markdownlint.readConfigSync( + "./test/config/config-grandparent-hybrid.yaml", + [ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ]); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + test.deepLooseEqual(actual, expected, "Config object not correct."); + test.end(); +}); + +tape("configBadHybridSync", (test) => { + test.plan(1); + test.throws( + function badHybridCall() { + markdownlint.readConfigSync( + "./test/config/config-badcontent.txt", + [ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ]); + }, + // eslint-disable-next-line max-len + /Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+; Expected [^;]+ or end of input but "\S+" found.; end of the stream or a document separator is expected at line \d+, column \d+:[^;]*/, + "Did not get correct exception for bad content." + ); + test.end(); +}); + +tape("allBuiltInRulesHaveValidUrl", (test) => { + test.plan(132); + rules.forEach(function forRule(rule) { + test.ok(rule.information); + test.ok(Object.getPrototypeOf(rule.information) === URL.prototype); + const name = rule.names[0].toLowerCase(); + test.equal( + rule.information.href, + `${homepage}/blob/v${version}/doc/Rules.md#${name}` + ); + }); + test.end(); +}); + +tape("someCustomRulesHaveValidUrl", (test) => { + test.plan(7); + customRules.all.forEach(function forRule(rule) { + test.ok(!rule.information || + (Object.getPrototypeOf(rule.information) === URL.prototype)); + if (rule === customRules.anyBlockquote) { + test.equal( + rule.information.href, + `${homepage}/blob/main/test/rules/any-blockquote.js` + ); + } else if (rule === customRules.lettersEX) { + test.equal( + rule.information.href, + `${homepage}/blob/main/test/rules/letters-E-X.js` + ); + } + }); + test.end(); +}); + +tape("customRulesV0", (test) => { + test.plan(4); + const customRulesMd = "./test/custom-rules.md"; + const options = { + "customRules": customRules.all, + "files": [ customRulesMd ], + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = {}; + expectedResult[customRulesMd] = { + "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."); + let actualMessage = actualResult.toString(); + let expectedMessage = + "./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" + + " Rule that reports an error every N lines\n" + + "./test/custom-rules.md: 6: every-n-lines" + + " Rule that reports an error every N lines\n" + + "./test/custom-rules.md: 10: every-n-lines" + + " 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" + + " Rule that reports an error for lines with the letters 'EX'"; + test.equal(actualMessage, expectedMessage, "Incorrect message (name)."); + // @ts-ignore + actualMessage = actualResult.toString(true); + expectedMessage = + "./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" + + " Rule that reports an error every N lines\n" + + "./test/custom-rules.md: 6: every-n-lines" + + " Rule that reports an error every N lines\n" + + "./test/custom-rules.md: 10: every-n-lines" + + " 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" + + " Rule that reports an error for lines with the letters 'EX'"; + test.equal(actualMessage, expectedMessage, "Incorrect message (alias)."); + test.end(); + }); +}); + +tape("customRulesV1", (test) => { + test.plan(3); + const customRulesMd = "./test/custom-rules.md"; + const options = { + "customRules": customRules.all, + "files": [ customRulesMd ], + "resultVersion": 1 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = {}; + expectedResult[customRulesMd] = [ + { "lineNumber": 12, + "ruleName": "any-blockquote", + "ruleAlias": "any-blockquote", + "ruleDescription": "Rule that reports an error for any blockquote", + "ruleInformation": + `${homepage}/blob/main/test/rules/any-blockquote.js`, + "errorDetail": "Blockquote spans 1 line(s).", + "errorContext": "> Block", + "errorRange": null }, + { "lineNumber": 2, + "ruleName": "every-n-lines", + "ruleAlias": "every-n-lines", + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 2", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 4, + "ruleName": "every-n-lines", + "ruleAlias": "every-n-lines", + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 4", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 6, + "ruleName": "every-n-lines", + "ruleAlias": "every-n-lines", + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 6", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 10, + "ruleName": "every-n-lines", + "ruleAlias": "every-n-lines", + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 10", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 12, + "ruleName": "every-n-lines", + "ruleAlias": "every-n-lines", + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "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", + "ruleInformation": null, + "errorDetail": null, + "errorContext": null, + "errorRange": null }, + { "lineNumber": 3, + "ruleName": "letters-E-X", + "ruleAlias": "letter-E-letter-X", + "ruleDescription": + "Rule that reports an error for lines with the letters 'EX'", + "ruleInformation": `${homepage}/blob/main/test/rules/letters-E-X.js`, + "errorDetail": null, + "errorContext": "text", + "errorRange": null }, + { "lineNumber": 7, + "ruleName": "letters-E-X", + "ruleAlias": "letter-E-letter-X", + "ruleDescription": + "Rule that reports an error for lines with the letters 'EX'", + "ruleInformation": `${homepage}/blob/main/test/rules/letters-E-X.js`, + "errorDetail": null, + "errorContext": "text", + "errorRange": null } + ]; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "./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" + + "./test/custom-rules.md: 4: every-n-lines/every-n-lines" + + " Rule that reports an error every N lines [Line number 4]\n" + + "./test/custom-rules.md: 6: every-n-lines/every-n-lines" + + " Rule that reports an error every N lines [Line number 6]\n" + + "./test/custom-rules.md: 10: every-n-lines/every-n-lines" + + " 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" + + "./test/custom-rules.md: 7: letters-E-X/letter-E-letter-X" + + " Rule that reports an error for lines with the letters 'EX'" + + " [Context: \"text\"]"; + test.equal(actualMessage, expectedMessage, "Incorrect message."); + test.end(); + }); +}); + +tape("customRulesV2", (test) => { + test.plan(3); + const customRulesMd = "./test/custom-rules.md"; + const options = { + "customRules": customRules.all, + "files": [ customRulesMd ], + "resultVersion": 2 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = {}; + expectedResult[customRulesMd] = [ + { "lineNumber": 12, + "ruleNames": [ "any-blockquote" ], + "ruleDescription": "Rule that reports an error for any blockquote", + "ruleInformation": + `${homepage}/blob/main/test/rules/any-blockquote.js`, + "errorDetail": "Blockquote spans 1 line(s).", + "errorContext": "> Block", + "errorRange": null }, + { "lineNumber": 2, + "ruleNames": [ "every-n-lines" ], + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 2", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 4, + "ruleNames": [ "every-n-lines" ], + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 4", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 6, + "ruleNames": [ "every-n-lines" ], + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 6", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 10, + "ruleNames": [ "every-n-lines" ], + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 10", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 12, + "ruleNames": [ "every-n-lines" ], + "ruleDescription": "Rule that reports an error every N lines", + "ruleInformation": null, + "errorDetail": "Line number 12", + "errorContext": null, + "errorRange": null }, + { "lineNumber": 1, + "ruleNames": [ "first-line" ], + "ruleDescription": "Rule that reports an error for the first line", + "ruleInformation": null, + "errorDetail": null, + "errorContext": null, + "errorRange": null }, + { "lineNumber": 3, + "ruleNames": [ "letters-E-X", "letter-E-letter-X", "contains-ex" ], + "ruleDescription": + "Rule that reports an error for lines with the letters 'EX'", + "ruleInformation": `${homepage}/blob/main/test/rules/letters-E-X.js`, + "errorDetail": null, + "errorContext": "text", + "errorRange": null }, + { "lineNumber": 7, + "ruleNames": [ "letters-E-X", "letter-E-letter-X", "contains-ex" ], + "ruleDescription": + "Rule that reports an error for lines with the letters 'EX'", + "ruleInformation": `${homepage}/blob/main/test/rules/letters-E-X.js`, + "errorDetail": null, + "errorContext": "text", + "errorRange": null } + ]; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "./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" + + "./test/custom-rules.md: 4: every-n-lines" + + " Rule that reports an error every N lines [Line number 4]\n" + + "./test/custom-rules.md: 6: every-n-lines" + + " Rule that reports an error every N lines [Line number 6]\n" + + "./test/custom-rules.md: 10: every-n-lines" + + " 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" + + "./test/custom-rules.md: 7: letters-E-X/letter-E-letter-X/contains-ex" + + " Rule that reports an error for lines with the letters 'EX'" + + " [Context: \"text\"]"; + test.equal(actualMessage, expectedMessage, "Incorrect message."); + test.end(); + }); +}); + +tape("customRulesConfig", (test) => { + test.plan(2); + const customRulesMd = "./test/custom-rules.md"; + const options = { + "customRules": customRules.all, + "files": [ customRulesMd ], + "config": { + "blockquote": true, + "every-n-lines": { + "n": 3 + }, + "letters-e-x": false + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = {}; + expectedResult[customRulesMd] = { + "any-blockquote": [ 12 ], + "every-n-lines": [ 3, 6, 12 ], + "first-line": [ 1 ], + "letters-E-X": [ 7 ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesNpmPackage", (test) => { + test.plan(2); + const options = { + "customRules": [ require("./rules/npm") ], + "strings": { + "string": "# Text\n\n---\n\nText\n" + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = {}; + expectedResult.string = { + "sample-rule": [ 3 ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesBadProperty", (test) => { + test.plan(23); + [ + { + "propertyName": "names", + "propertyValues": + [ null, "string", [], [ null ], [ "" ], [ "string", 10 ] ] + }, + { + "propertyName": "description", + "propertyValues": [ null, 10, "", [] ] + }, + { + "propertyName": "information", + "propertyValues": [ 10, [], "string", "https://example.com" ] + }, + { + "propertyName": "tags", + "propertyValues": + [ null, "string", [], [ null ], [ "" ], [ "string", 10 ] ] + }, + { + "propertyName": "function", + "propertyValues": [ null, "string", [] ] + } + ].forEach(function forTestCase(testCase) { + const { propertyName, propertyValues } = testCase; + propertyValues.forEach(function forPropertyValue(propertyValue) { + const badRule = { ...customRules.anyBlockquote }; + badRule[propertyName] = propertyValue; + const options = { + "customRules": [ badRule ] + }; + test.throws( + function badRuleCall() { + markdownlint.sync(options); + }, + new RegExp( + `Property '${propertyName}' of custom rule at index 0 is incorrect.` + ), + "Did not get correct exception for missing property." + ); + }); + }); + test.end(); +}); + +tape("customRulesUsedNameName", (test) => { + test.plan(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.end(); + }); +}); + +tape("customRulesUsedNameTag", (test) => { + test.plan(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.end(); + }); +}); + +tape("customRulesUsedTagName", (test) => { + test.plan(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.end(); + }); +}); + +tape("customRulesThrowForFile", (test) => { + test.plan(4); + const 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.end(); + }); +}); + +tape("customRulesThrowForFileSync", (test) => { + test.plan(1); + const 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" ] + }); + }, + new RegExp(exceptionMessage), + "Did not get correct exception for function thrown." + ); + test.end(); +}); + +tape("customRulesThrowForString", (test) => { + test.plan(4); + const exceptionMessage = "Test exception message"; + markdownlint({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function throws() { + throw new Error(exceptionMessage); + } + } + ], + "strings": { + "string": "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.end(); + }); +}); + +tape("customRulesOnErrorNull", (test) => { + test.plan(1); + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorNull(params, onError) { + onError(null); + } + } + ], + "strings": { + "string": "String" + } + }; + test.throws( + function nullErrorCall() { + markdownlint.sync(options); + }, + /Property 'lineNumber' of onError parameter is incorrect./, + "Did not get correct exception for null object." + ); + test.end(); +}); + +tape("customRulesOnErrorBad", (test) => { + test.plan(21); + [ + { + "propertyName": "lineNumber", + "subPropertyName": null, + "propertyValues": [ null, "string" ] + }, + { + "propertyName": "detail", + "subPropertyName": null, + "propertyValues": [ 10, [] ] + }, + { + "propertyName": "context", + "subPropertyName": null, + "propertyValues": [ 10, [] ] + }, + { + "propertyName": "range", + "subPropertyName": null, + "propertyValues": [ 10, [], [ 10 ], [ 10, null ], [ 10, 11, 12 ] ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": null, + "propertyValues": [ 10, "string" ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "lineNumber", + "propertyValues": [ null, "string" ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "editColumn", + "propertyValues": [ null, "string" ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "deleteCount", + "propertyValues": [ null, "string" ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "insertText", + "propertyValues": [ 10, [] ] + } + ].forEach(function forTestCase(testCase) { + const { propertyName, subPropertyName, propertyValues } = testCase; + propertyValues.forEach(function forPropertyValue(propertyValue) { + const badObject = { + "lineNumber": 1 + }; + let propertyNames = null; + if (subPropertyName) { + badObject[propertyName] = {}; + badObject[propertyName][subPropertyName] = propertyValue; + propertyNames = `${propertyName}.${subPropertyName}`; + } else { + badObject[propertyName] = propertyValue; + propertyNames = propertyName; + } + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorBad(params, onError) { + onError(badObject); + } + } + ], + "strings": { + "string": "String" + } + }; + test.throws( + function badErrorCall() { + markdownlint.sync(options); + }, + new RegExp( + `Property '${propertyNames}' of onError parameter is incorrect.` + ), + "Did not get correct exception for bad object." + ); + }); + }); + test.end(); +}); + +tape("customRulesOnErrorInvalid", (test) => { + test.plan(17); + [ + { + "propertyName": "lineNumber", + "subPropertyName": null, + "propertyValues": [ -1, 0, 3, 4 ] + }, + { + "propertyName": "range", + "subPropertyName": null, + "propertyValues": [ [ 0, 1 ], [ 1, 0 ], [ 5, 1 ], [ 1, 5 ], [ 4, 2 ] ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "lineNumber", + "propertyValues": [ -1, 0, 3, 4 ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "editColumn", + "propertyValues": [ 0, 6 ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "deleteCount", + "propertyValues": [ -2, 5 ] + } + ].forEach(function forTestCase(testCase) { + const { propertyName, subPropertyName, propertyValues } = testCase; + propertyValues.forEach(function forPropertyValue(propertyValue) { + const badObject = { + "lineNumber": 1 + }; + let propertyNames = null; + if (subPropertyName) { + badObject[propertyName] = {}; + badObject[propertyName][subPropertyName] = propertyValue; + propertyNames = `${propertyName}.${subPropertyName}`; + } else { + badObject[propertyName] = propertyValue; + propertyNames = propertyName; + } + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorInvalid(params, onError) { + onError(badObject); + } + } + ], + "strings": { + "string": "Text\ntext" + } + }; + test.throws( + function invalidErrorCall() { + markdownlint.sync(options); + }, + new RegExp( + `Property '${propertyNames}' of onError parameter is incorrect.` + ), + "Did not get correct exception for invalid object." + ); + }); + }); + test.end(); +}); + +tape("customRulesOnErrorValid", (test) => { + test.plan(24); + [ + { + "propertyName": "lineNumber", + "subPropertyName": null, + "propertyValues": [ 1, 2 ] + }, + { + "propertyName": "range", + "subPropertyName": null, + "propertyValues": [ [ 1, 1 ], [ 1, 4 ], [ 2, 2 ], [ 3, 2 ], [ 4, 1 ] ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "lineNumber", + "propertyValues": [ 1, 2 ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "editColumn", + "propertyValues": [ 1, 2, 4, 5 ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "deleteCount", + "propertyValues": [ -1, 0, 1, 4 ] + }, + { + "propertyName": "fixInfo", + "subPropertyName": "insertText", + "propertyValues": + [ "", "1", "123456", "\n", "\nText", "Text\n", "\nText\n" ] + } + ].forEach(function forTestCase(testCase) { + const { propertyName, subPropertyName, propertyValues } = testCase; + propertyValues.forEach(function forPropertyValue(propertyValue) { + const goodObject = { + "lineNumber": 1 + }; + if (subPropertyName) { + goodObject[propertyName] = {}; + goodObject[propertyName][subPropertyName] = propertyValue; + } else { + goodObject[propertyName] = propertyValue; + } + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorValid(params, onError) { + onError(goodObject); + } + } + ], + "strings": { + "string": "Text\ntext" + } + }; + markdownlint.sync(options); + test.ok(true); + }); + }); + test.end(); +}); + +tape("customRulesOnErrorLazy", (test) => { + test.plan(2); + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorLazy(params, onError) { + onError({ + "lineNumber": 1, + "detail": "", + "context": "", + "range": [ 1, 1 ] + }); + } + } + ], + "strings": { + "string": "# Heading\n" + } + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "string": [ + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": null, + "errorDetail": null, + "errorContext": null, + "errorRange": [ 1, 1 ] + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesOnErrorModified", (test) => { + test.plan(2); + const errorObject = { + "lineNumber": 1, + "detail": "detail", + "context": "context", + "range": [ 1, 2 ], + "fixInfo": { + "editColumn": 1, + "deleteCount": 2, + "insertText": "text" + } + }; + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorModified(params, onError) { + onError(errorObject); + errorObject.lineNumber = 2; + errorObject.detail = "changed"; + errorObject.context = "changed"; + errorObject.range[1] = 3; + errorObject.fixInfo.editColumn = 2; + errorObject.fixInfo.deleteCount = 3; + errorObject.fixInfo.insertText = "changed"; + } + } + ], + "strings": { + "string": "# Heading\n" + }, + "resultVersion": 3 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "string": [ + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": null, + "errorDetail": "detail", + "errorContext": "context", + "errorRange": [ 1, 2 ], + "fixInfo": { + "editColumn": 1, + "deleteCount": 2, + "insertText": "text" + } + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesThrowForFileHandled", (test) => { + test.plan(2); + const exceptionMessage = "Test exception message"; + markdownlint({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function throws() { + throw new Error(exceptionMessage); + } + } + ], + "files": [ "./test/custom-rules.md" ], + "handleRuleFailures": true + }, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "./test/custom-rules.md": [ + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": null, + "errorDetail": + `This rule threw an exception: ${exceptionMessage}`, + "errorContext": null, + "errorRange": null + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesThrowForStringHandled", (test) => { + test.plan(2); + const exceptionMessage = "Test exception message"; + const informationUrl = "https://example.com/rule"; + markdownlint({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "information": new URL(informationUrl), + "tags": [ "tag" ], + "function": function throws() { + throw new Error(exceptionMessage); + } + } + ], + "strings": { + "string": "String\n" + }, + "handleRuleFailures": true + }, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "string": [ + { + "lineNumber": 1, + "ruleNames": [ "MD041", "first-line-heading", "first-line-h1" ], + "ruleDescription": + "First line in file should be a top level heading", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md041`, + "errorDetail": null, + "errorContext": "String", + "errorRange": null + }, + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": informationUrl, + "errorDetail": + `This rule threw an exception: ${exceptionMessage}`, + "errorContext": null, + "errorRange": null + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesOnErrorInvalidHandled", (test) => { + test.plan(2); + markdownlint({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorInvalid(params, onError) { + onError({ + "lineNumber": 13, + "details": "N/A" + }); + } + } + ], + "strings": { + "string": "# Heading\n" + }, + "handleRuleFailures": true + }, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "string": [ + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": null, + "errorDetail": "This rule threw an exception: " + + "Property 'lineNumber' of onError parameter is incorrect.", + "errorContext": null, + "errorRange": null + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.end(); + }); +}); + +tape("customRulesFileName", (test) => { + test.plan(2); + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function stringName(params) { + test.equal(params.name, "doc/CustomRules.md", "Incorrect file name"); + } + } + ], + "files": "doc/CustomRules.md" + }; + markdownlint(options, function callback(err) { + test.ifError(err); + test.end(); + }); +}); + +tape("customRulesStringName", (test) => { + test.plan(2); + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function stringName(params) { + test.equal(params.name, "string", "Incorrect string name"); + } + } + ], + "strings": { + "string": "# Heading" + } + }; + markdownlint(options, function callback(err) { + test.ifError(err); + test.end(); + }); +}); + +tape("customRulesDoc", (test) => { + test.plan(2); + markdownlint({ + "files": "doc/CustomRules.md", + "config": { + "MD013": { "line_length": 200 } + } + }, function callback(err, actual) { + test.ifError(err); + const expected = { "doc/CustomRules.md": [] }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("customRulesLintJavaScript", (test) => { + test.plan(2); + const options = { + "customRules": customRules.lintJavaScript, + "files": "test/lint-javascript.md" + }; + markdownlint(options, (err, actual) => { + test.ifError(err); + const expected = { + "test/lint-javascript.md": [ + { + "lineNumber": 10, + "ruleNames": [ "lint-javascript" ], + "ruleDescription": "Rule that lints JavaScript code", + "ruleInformation": null, + "errorDetail": "Unexpected var, use let or const instead.", + "errorContext": "var x = 0;", + "errorRange": null + }, + { + "lineNumber": 12, + "ruleNames": [ "lint-javascript" ], + "ruleDescription": "Rule that lints JavaScript code", + "ruleInformation": null, + "errorDetail": "Unexpected console statement.", + "errorContext": "console.log(x);", + "errorRange": null + } + ] + }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("markdownItPluginsSingle", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "string": "# Heading\n\nText [ link ](https://example.com)\n" + }, + "markdownItPlugins": [ + [ + pluginInline, + "trim_text_plugin", + "text", + function iterator(tokens, index) { + tokens[index].content = tokens[index].content.trim(); + } + ] + ] + }, function callback(err, actual) { + test.ifError(err); + const expected = { "string": [] }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("markdownItPluginsMultiple", (test) => { + test.plan(4); + markdownlint({ + "strings": { + "string": "# Heading\n\nText H~2~0 text 29^th^ text\n" + }, + "markdownItPlugins": [ + [ pluginSub ], + [ pluginSup ], + [ pluginInline, "check_sub_plugin", "sub_open", test.ok ], + [ pluginInline, "check_sup_plugin", "sup_open", test.ok ] + ] + }, function callback(err, actual) { + test.ifError(err); + const expected = { "string": [] }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("markdownItPluginsMathjax", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "string": + "# Heading\n" + + "\n" + + "$1 *2* 3$\n" + + "\n" + + "$$1 *2* 3$$\n" + + "\n" + + "$$1\n" + + "+ 2\n" + + "+ 3$$\n" + }, + "markdownItPlugins": [ [ pluginKatex ] ] + }, function callback(err, actual) { + test.ifError(err); + const expected = { "string": [] }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +}); + +tape("markdownItPluginsMathjaxIssue166", (test) => { + test.plan(2); + markdownlint({ + "strings": { + "string": +`## Heading + +$$ +1 +$$$$ +2 +$$\n` + }, + "markdownItPlugins": [ [ pluginKatex ] ], + "resultVersion": 0 + }, function callback(err, actual) { + test.ifError(err); + const expected = { + "string": { + "MD041": [ 1 ] + } + }; + test.deepEqual(actual, expected, "Unexpected issues."); + test.end(); + }); +});