diff --git a/README.md b/README.md index 1d21710e..243a6a66 100644 --- a/README.md +++ b/README.md @@ -408,19 +408,22 @@ And the `result` object becomes: "ruleAlias": "no-hard-tabs", "ruleDescription": "Hard tabs", "errorDetail": "Column: 17", - "errorContext": null }, + "errorContext": null, + "errorRange": [ 17, 1 ] }, { "lineNumber": 1, "ruleName": "MD018", "ruleAlias": "no-missing-space-atx", "ruleDescription": "No space after hash on atx style header", "errorDetail": null, - "errorContext": "#bad.md" }, + "errorContext": "#bad.md", + "errorRange": [ 1, 2 ] }, { "lineNumber": 3, "ruleName": "MD018", "ruleAlias": "no-missing-space-atx", "ruleDescription": "No space after hash on atx style header", "errorDetail": null, - "errorContext": "#This file fails\tsome rules." } + "errorContext": "#This file fails\tsome rules.", + "errorRange": [ 1, 2 ] } ] } ``` diff --git a/demo/default.js b/demo/default.js index e8693b5b..ceb5dd60 100644 --- a/demo/default.js +++ b/demo/default.js @@ -67,6 +67,12 @@ sanitize(result.errorContext) + "\"]" : ""); + /* + (result.errorRange ? + " " + + lines[result.lineNumber - 1].substr( + result.errorRange[0] - 1, result.errorRange[1]) + + "" : + ""); */ }).join("
"); } diff --git a/example/standalone.js b/example/standalone.js index edf77ab0..e2ee29f0 100644 --- a/example/standalone.js +++ b/example/standalone.js @@ -20,7 +20,7 @@ markdownlint(options, function callback(err, result) { // Examines the result object directly markdownlint(options, function callback(err, result) { if (!err) { - console.dir(result, { "colors": true }); + console.dir(result, { "colors": true, "depth": null }); } }); @@ -28,7 +28,7 @@ markdownlint(options, function callback(err, result) { options.resultVersion = 1; markdownlint(options, function callback(err, result) { if (!err) { - console.dir(result, { "colors": true }); + console.dir(result, { "colors": true, "depth": null }); } }); diff --git a/lib/markdownlint.js b/lib/markdownlint.js index 33917656..b68d7e21 100644 --- a/lib/markdownlint.js +++ b/lib/markdownlint.js @@ -245,13 +245,31 @@ function lintContent(content, config, frontMatter, resultVersion) { }) .map(function formatResults(error) { if (resultVersion === 1) { + var range = null; + var regexp = rule.regexp; + if (regexp) { + if (typeof regexp === "function") { + regexp = regexp(params.options); + } + var match = lines[error.lineNumber - 1].match(regexp); + if (match) { + var column = match.index + 1; + var length = match[0].length; + if (match[2]) { + column += match[1].length; + length -= match[1].length; + } + range = [ column, length ]; + } + } return { "lineNumber": error.lineNumber, "ruleName": rule.name, "ruleAlias": rule.aliases[0], "ruleDescription": rule.desc, "errorDetail": error.detail, - "errorContext": error.context + "errorContext": error.context, + "errorRange": range }; } return error.lineNumber; diff --git a/lib/rules.js b/lib/rules.js index 3a991f11..cc9b44e7 100644 --- a/lib/rules.js +++ b/lib/rules.js @@ -2,6 +2,30 @@ var shared = require("./shared"); +// Range regular expressions +var atxClosedHeaderNoSpaceRe = /(?:^#+\S)|(?:\S#+\s*$)/; +var atxClosedHeaderSpaceRe = /(?:^#+\s\s+?\S)|(?:\S\s\s+?#+\s*$)/; +var atxHeaderSpaceRe = /^#+\s*\S/; +var bareUrlRe = /(?:http|ftp)s?:\/\/[^\s]*/; +var dollarCommandRe = /^(\s*)(\$\s)/; +var emptyLinkRe = /\[[^\]]*\](?=(?:\((?:#?|(?:<>))\))|(?:\[[^\]]*\]))/; +var htmlRe = /<[^>]*>/; +var listItemMarkerRe = /^[\s>]*(?:[\*\+\-]|\d+\.)\s+/; +var reversedLinkRe = /\([^)]+\)\[[^\]^][^\]]*\]/; +var spaceAfterBlockQuote = />\s+\S/; +var spaceBeforeHeaderRe = /^\s+\S/; +var spaceInsideCodeRe = /`(?:(?:\s[^`]*)|(?:[^`]*\s))`/; +var spaceInsideEmphasisRe = /(\*\*?|__?)(?:(?:\s.+)|(?:.+\s))\1/; +var spaceInsideLinkRe = /\[(?:(?:\s[^\]]*)|(?:[^\]]*\s))\](?=\(\S*\))/; +var tabRe = /\t+/; +var trailingPunctuationRe = /.$/; +var trailingSpaceRe = /\s+$/; +var defaultLineLength = 80; +function longLineReFunc(options) { + var lineLength = options.line_length || defaultLineLength; + return new RegExp("^(.{" + lineLength + "})(.*\\s.*)$"); +} + // Escapes a string for use in a RegExp function escapeForRegExp(str) { return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); @@ -162,6 +186,7 @@ module.exports = [ "desc": "Header levels should only increment by one level at a time", "tags": [ "headers" ], "aliases": [ "header-increment" ], + "regexp": null, "func": function MD001(params, errors) { var prevLevel = 0; filterTokens(params, "heading_open", function forToken(token) { @@ -180,6 +205,7 @@ module.exports = [ "desc": "First header should be a top level header", "tags": [ "headers" ], "aliases": [ "first-header-h1" ], + "regexp": null, "func": function MD002(params, errors) { var level = params.options.level || 1; var tag = "h" + level; @@ -198,6 +224,7 @@ module.exports = [ "desc": "Header style", "tags": [ "headers" ], "aliases": [ "header-style" ], + "regexp": null, "func": function MD003(params, errors) { var style = params.options.style || "consistent"; filterTokens(params, "heading_open", function forToken(token) { @@ -234,6 +261,7 @@ module.exports = [ "desc": "Unordered list style", "tags": [ "bullet", "ul" ], "aliases": [ "ul-style" ], + "regexp": listItemMarkerRe, "func": function MD004(params, errors) { var style = params.options.style || "consistent"; var expectedStyle = style; @@ -268,6 +296,7 @@ module.exports = [ "desc": "Inconsistent indentation for list items at the same level", "tags": [ "bullet", "ul", "indentation" ], "aliases": [ "list-indent" ], + "regexp": listItemMarkerRe, "func": function MD005(params, errors) { flattenLists(params).forEach(function forList(list) { var indent = indentFor(list.items[0]); @@ -283,6 +312,7 @@ module.exports = [ "desc": "Consider starting bulleted lists at the beginning of the line", "tags": [ "bullet", "ul", "indentation" ], "aliases": [ "ul-start-left" ], + "regexp": listItemMarkerRe, "func": function MD006(params, errors) { flattenLists(params).forEach(function forList(list) { if (list.unordered && !list.nesting) { @@ -297,6 +327,7 @@ module.exports = [ "desc": "Unordered list indentation", "tags": [ "bullet", "ul", "indentation" ], "aliases": [ "ul-indent" ], + "regexp": listItemMarkerRe, "func": function MD007(params, errors) { var optionsIndent = params.options.indent || 2; var prevIndent = 0; @@ -318,10 +349,11 @@ module.exports = [ "desc": "Trailing spaces", "tags": [ "whitespace" ], "aliases": [ "no-trailing-spaces" ], + "regexp": trailingSpaceRe, "func": function MD009(params, errors) { var brSpaces = params.options.br_spaces || 0; params.lines.forEach(function forLine(line, lineIndex) { - if (/\s$/.test(line)) { + if (trailingSpaceRe.test(line)) { var expected = (brSpaces < 2) ? 0 : brSpaces; errors.addDetailIf(lineIndex + 1, expected, line.length - line.trimRight().length); @@ -335,11 +367,12 @@ module.exports = [ "desc": "Hard tabs", "tags": [ "whitespace", "hard_tab" ], "aliases": [ "no-hard-tabs" ], + "regexp": tabRe, "func": function MD010(params, errors) { var codeBlocks = params.options.code_blocks; var includeCodeBlocks = (codeBlocks === undefined) ? true : !!codeBlocks; forEachLine(params, function forLine(line, lineIndex, inCode) { - if (/\t/.test(line) && (!inCode || includeCodeBlocks)) { + if (tabRe.test(line) && (!inCode || includeCodeBlocks)) { errors.addDetail(lineIndex + 1, "Column: " + (line.indexOf("\t") + 1)); } @@ -352,9 +385,10 @@ module.exports = [ "desc": "Reversed link syntax", "tags": [ "links" ], "aliases": [ "no-reversed-links" ], + "regexp": reversedLinkRe, "func": function MD011(params, errors) { forEachInlineChild(params, "text", function forToken(token) { - var match = /\([^)]+\)\[[^\]^][^\]]*\]/.exec(token.content); + var match = reversedLinkRe.exec(token.content); if (match) { errors.addDetail(token.lineNumber, match[0]); } @@ -367,6 +401,7 @@ module.exports = [ "desc": "Multiple consecutive blank lines", "tags": [ "whitespace", "blank_lines" ], "aliases": [ "no-multiple-blanks" ], + "regexp": null, "func": function MD012(params, errors) { var maximum = params.options.maximum || 1; var count = 0; @@ -384,13 +419,14 @@ module.exports = [ "desc": "Line length", "tags": [ "line_length" ], "aliases": [ "line-length" ], + "regexp": longLineReFunc, "func": function MD013(params, errors) { - var lineLength = params.options.line_length || 80; + var lineLength = params.options.line_length || defaultLineLength; var codeBlocks = params.options.code_blocks; var includeCodeBlocks = (codeBlocks === undefined) ? true : !!codeBlocks; var tables = params.options.tables; var includeTables = (tables === undefined) ? true : !!tables; - var re = new RegExp("^.{" + lineLength + "}.*\\s"); + var re = longLineReFunc(params.options); forEachLine(params, function forLine(line, lineIndex, inCode, onFence, inTable) { if ((includeCodeBlocks || !inCode) && @@ -407,13 +443,14 @@ module.exports = [ "desc": "Dollar signs used before commands without showing output", "tags": [ "code" ], "aliases": [ "commands-show-output" ], + "regexp": dollarCommandRe, "func": function MD014(params, errors) { [ "code_block", "fence" ].forEach(function forType(type) { filterTokens(params, type, function forToken(token) { var allBlank = true; if (token.content && token.content.split(shared.newLineRe) .every(function forLine(line) { - return !line || (allBlank = false) || /^\$\s/.test(line); + return !line || (allBlank = false) || dollarCommandRe.test(line); }) && !allBlank) { errors.addContext(token.lineNumber, token.content.split(shared.newLineRe)[0].trim()); @@ -428,6 +465,7 @@ module.exports = [ "desc": "No space after hash on atx style header", "tags": [ "headers", "atx", "spaces" ], "aliases": [ "no-missing-space-atx" ], + "regexp": atxHeaderSpaceRe, "func": function MD018(params, errors) { forEachLine(params, function forLine(line, lineIndex, inCode) { if (!inCode && /^#+[^#\s]/.test(line) && !/#$/.test(line)) { @@ -442,6 +480,7 @@ module.exports = [ "desc": "Multiple spaces after hash on atx style header", "tags": [ "headers", "atx", "spaces" ], "aliases": [ "no-multiple-space-atx" ], + "regexp": atxHeaderSpaceRe, "func": function MD019(params, errors) { filterTokens(params, "heading_open", function forToken(token) { if ((headingStyleFor(token) === "atx") && @@ -457,6 +496,7 @@ module.exports = [ "desc": "No space inside hashes on closed atx style header", "tags": [ "headers", "atx_closed", "spaces" ], "aliases": [ "no-missing-space-closed-atx" ], + "regexp": atxClosedHeaderNoSpaceRe, "func": function MD020(params, errors) { forEachLine(params, function forLine(line, lineIndex, inCode) { if (!inCode && /^#+[^#]*[^\\]#+$/.test(line)) { @@ -475,6 +515,7 @@ module.exports = [ "desc": "Multiple spaces inside hashes on closed atx style header", "tags": [ "headers", "atx_closed", "spaces" ], "aliases": [ "no-multiple-space-closed-atx" ], + "regexp": atxClosedHeaderSpaceRe, "func": function MD021(params, errors) { filterTokens(params, "heading_open", function forToken(token) { if (headingStyleFor(token) === "atx_closed") { @@ -493,6 +534,7 @@ module.exports = [ "desc": "Headers should be surrounded by blank lines", "tags": [ "headers", "blank_lines" ], "aliases": [ "blanks-around-headers" ], + "regexp": null, "func": function MD022(params, errors) { var prevHeadingLineNumber = 0; var prevMaxLineIndex = -1; @@ -527,9 +569,10 @@ module.exports = [ "desc": "Headers must start at the beginning of the line", "tags": [ "headers", "spaces" ], "aliases": [ "header-start-left" ], + "regexp": spaceBeforeHeaderRe, "func": function MD023(params, errors) { filterTokens(params, "heading_open", function forToken(token) { - if (/^\s/.test(token.line)) { + if (spaceBeforeHeaderRe.test(token.line)) { errors.addContext(token.lineNumber, token.line); } }); @@ -541,6 +584,7 @@ module.exports = [ "desc": "Multiple headers with the same content", "tags": [ "headers" ], "aliases": [ "no-duplicate-header" ], + "regexp": null, "func": function MD024(params, errors) { var knownContent = []; forEachHeading(params, function forHeading(heading, content) { @@ -558,6 +602,7 @@ module.exports = [ "desc": "Multiple top level headers in the same document", "tags": [ "headers" ], "aliases": [ "single-h1" ], + "regexp": null, "func": function MD025(params, errors) { var level = params.options.level || 1; var tag = "h" + level; @@ -579,6 +624,7 @@ module.exports = [ "desc": "Trailing punctuation in header", "tags": [ "headers" ], "aliases": [ "no-trailing-punctuation" ], + "regexp": trailingPunctuationRe, "func": function MD026(params, errors) { var punctuation = params.options.punctuation || ".,;:!?"; var re = new RegExp("[" + punctuation + "]$"); @@ -597,6 +643,7 @@ module.exports = [ "desc": "Multiple spaces after blockquote symbol", "tags": [ "blockquote", "whitespace", "indentation" ], "aliases": [ "no-multiple-space-blockquote" ], + "regexp": spaceAfterBlockQuote, "func": function MD027(params, errors) { var blockquoteNesting = 0; var listItemNesting = 0; @@ -632,6 +679,7 @@ module.exports = [ "desc": "Blank line inside blockquote", "tags": [ "blockquote", "whitespace" ], "aliases": [ "no-blanks-blockquote" ], + "regexp": null, "func": function MD028(params, errors) { var prevToken = {}; params.tokens.forEach(function forToken(token) { @@ -649,6 +697,7 @@ module.exports = [ "desc": "Ordered list item prefix", "tags": [ "ol" ], "aliases": [ "ol-prefix" ], + "regexp": listItemMarkerRe, "func": function MD029(params, errors) { var style = params.options.style || "one"; flattenLists(params).forEach(function forList(list) { @@ -672,6 +721,7 @@ module.exports = [ "desc": "Spaces after list markers", "tags": [ "ol", "ul", "whitespace" ], "aliases": [ "list-marker-space" ], + "regexp": listItemMarkerRe, "func": function MD030(params, errors) { var ulSingle = params.options.ul_single || 1; var olSingle = params.options.ol_single || 1; @@ -697,6 +747,7 @@ module.exports = [ "desc": "Fenced code blocks should be surrounded by blank lines", "tags": [ "code", "blank_lines" ], "aliases": [ "blanks-around-fences" ], + "regexp": null, "func": function MD031(params, errors) { var lines = params.lines; forEachLine(params, function forLine(line, i, inCode, onFence) { @@ -713,13 +764,14 @@ module.exports = [ "desc": "Lists should be surrounded by blank lines", "tags": [ "bullet", "ul", "ol", "blank_lines" ], "aliases": [ "blanks-around-lists" ], + "regexp": null, "func": function MD032(params, errors) { var inList = false; var prevLine = ""; forEachLine(params, function forLine(line, lineIndex, inCode, onFence) { if (!inCode || onFence) { var lineTrim = line.trim(); - var listMarker = /^([\*\+\-]|(\d+\.))\s/.test(lineTrim); + var listMarker = listItemMarkerRe.test(lineTrim); if (listMarker && !inList && !/^($|\s)/.test(prevLine)) { errors.addContext(lineIndex + 1, lineTrim); } else if (!listMarker && inList && !/^($|\s)/.test(line)) { @@ -737,6 +789,7 @@ module.exports = [ "desc": "Inline HTML", "tags": [ "html" ], "aliases": [ "no-inline-html" ], + "regexp": htmlRe, "func": function MD033(params, errors) { var allowedElements = (params.options.allowed_elements || []) .map(function forElement(element) { @@ -771,6 +824,7 @@ module.exports = [ "desc": "Bare URL used", "tags": [ "links", "url" ], "aliases": [ "no-bare-urls" ], + "regexp": bareUrlRe, "func": function MD034(params, errors) { filterTokens(params, "inline", function forToken(token) { var inLink = false; @@ -782,7 +836,7 @@ module.exports = [ inLink = false; } else if ((child.type === "text") && !inLink && - (match = /(http|ftp)s?:\/\/[^\s]*/.exec(child.content))) { + (match = bareUrlRe.exec(child.content))) { errors.addContext(child.lineNumber, match[0]); } }); @@ -795,6 +849,7 @@ module.exports = [ "desc": "Horizontal rule style", "tags": [ "hr" ], "aliases": [ "hr-style" ], + "regexp": null, "func": function MD035(params, errors) { var style = params.options.style || "consistent"; filterTokens(params, "hr", function forToken(token) { @@ -812,6 +867,7 @@ module.exports = [ "desc": "Emphasis used instead of a header", "tags": [ "headers", "emphasis" ], "aliases": [ "no-emphasis-as-header" ], + "regexp": null, "func": function MD036(params, errors) { var punctuation = params.options.punctuation || ".,;:!?"; var re = new RegExp("[" + punctuation + "]$"); @@ -853,6 +909,7 @@ module.exports = [ "desc": "Spaces inside emphasis markers", "tags": [ "whitespace", "emphasis" ], "aliases": [ "no-space-in-emphasis" ], + "regexp": spaceInsideEmphasisRe, "func": function MD037(params, errors) { forEachInlineChild(params, "text", function forToken(token) { var left = /\s(\*\*?|__?)\s.+\1/.exec(token.content); @@ -871,6 +928,7 @@ module.exports = [ "desc": "Spaces inside code span elements", "tags": [ "whitespace", "code" ], "aliases": [ "no-space-in-code" ], + "regexp": spaceInsideCodeRe, "func": function MD038(params, errors) { forEachInlineChild(params, "code_inline", function forToken(token, inline) { @@ -893,6 +951,7 @@ module.exports = [ "desc": "Spaces inside link text", "tags": [ "whitespace", "links" ], "aliases": [ "no-space-in-links" ], + "regexp": spaceInsideLinkRe, "func": function MD039(params, errors) { filterTokens(params, "inline", function forToken(token) { var inLink = false; @@ -922,6 +981,7 @@ module.exports = [ "desc": "Fenced code blocks should have a language specified", "tags": [ "code", "language" ], "aliases": [ "fenced-code-language" ], + "regexp": null, "func": function MD040(params, errors) { filterTokens(params, "fence", function forToken(token) { if (!token.info.trim()) { @@ -936,6 +996,7 @@ module.exports = [ "desc": "First line in file should be a top level header", "tags": [ "headers" ], "aliases": [ "first-line-h1" ], + "regexp": null, "func": function MD041(params, errors) { var level = params.options.level || 1; var tag = "h" + level; @@ -962,6 +1023,7 @@ module.exports = [ "desc": "No empty links", "tags": [ "links" ], "aliases": [ "no-empty-links" ], + "regexp": emptyLinkRe, "func": function MD042(params, errors) { filterTokens(params, "inline", function forToken(token) { var inLink = false; @@ -994,6 +1056,7 @@ module.exports = [ "desc": "Required header structure", "tags": [ "headers" ], "aliases": [ "required-headers" ], + "regexp": null, "func": function MD043(params, errors) { var requiredHeaders = params.options.headers; if (requiredHeaders) { diff --git a/package.json b/package.json index 1dfddb20..6a6cd1b3 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "test-cover": "istanbul cover node_modules/nodeunit/bin/nodeunit test/markdownlint-test.js", "test-extra": "nodeunit test/markdownlint-test-extra.js", "debug": "node debug node_modules/nodeunit/bin/nodeunit", - "lint": "eslint lib test schema && eslint --env browser --global markdownit --global markdownlint --rule \"no-unused-vars: 0, no-extend-native: 0, max-statements: 0, no-console: 0\" demo && eslint --rule \"no-console: 0, no-shadow: 0\" example", + "lint": "eslint lib test schema && eslint --env browser --global markdownit --global markdownlint --rule \"no-unused-vars: 0, no-extend-native: 0, max-statements: 0, no-console: 0\" demo && eslint --rule \"no-console: 0, no-shadow: 0, object-property-newline: 0\" example", "build-config-schema": "node schema/build-config-schema.js", "build-demo": "cpy node_modules/markdown-it/dist/markdown-it.min.js demo && cd demo && rimraf markdownlint-browser.* && cpy file-header.js . --rename=markdownlint-browser.js && browserify browser-polyfills.js ../lib/markdownlint.js --standalone markdownlint >> markdownlint-browser.js && uglifyjs markdownlint-browser.js --compress --mangle --comments --output markdownlint-browser.min.js", "build-example": "npm install --ignore-scripts grunt grunt-cli gulp through2", diff --git a/test/detailed-results-MD001-MD010.results.json b/test/detailed-results-MD001-MD010.results.json index 2248fae9..356fd9e3 100644 --- a/test/detailed-results-MD001-MD010.results.json +++ b/test/detailed-results-MD001-MD010.results.json @@ -5,7 +5,8 @@ "ruleAlias": "header-increment", "ruleDescription": "Header levels should only increment by one level at a time", "errorDetail": "Expected: h3; Actual: h4", - "errorContext": null + "errorContext": null, + "errorRange": null }, { "lineNumber": 1, @@ -13,7 +14,8 @@ "ruleAlias": "first-header-h1", "ruleDescription": "First header should be a top level header", "errorDetail": "Expected: h1; Actual: h2", - "errorContext": null + "errorContext": null, + "errorRange": null }, { "lineNumber": 5, @@ -21,7 +23,8 @@ "ruleAlias": "header-style", "ruleDescription": "Header style", "errorDetail": "Expected: atx; Actual: atx_closed", - "errorContext": null + "errorContext": null, + "errorRange": null }, { "lineNumber": 10, @@ -29,7 +32,8 @@ "ruleAlias": "ul-style", "ruleDescription": "Unordered list style", "errorDetail": "Expected: asterisk; Actual: dash", - "errorContext": null + "errorContext": null, + "errorRange": [1, 2] }, { "lineNumber": 8, @@ -37,7 +41,8 @@ "ruleAlias": "list-indent", "ruleDescription": "Inconsistent indentation for list items at the same level", "errorDetail": "Expected: 0; Actual: 1", - "errorContext": null + "errorContext": null, + "errorRange": [1, 3] }, { "lineNumber": 12, @@ -45,7 +50,8 @@ "ruleAlias": "ul-start-left", "ruleDescription": "Consider starting bulleted lists at the beginning of the line", "errorDetail": "Expected: 0; Actual: 1", - "errorContext": null + "errorContext": null, + "errorRange": [1, 3] }, { "lineNumber": 12, @@ -53,7 +59,8 @@ "ruleAlias": "ul-indent", "ruleDescription": "Unordered list indentation", "errorDetail": "Expected: 2; Actual: 1", - "errorContext": null + "errorContext": null, + "errorRange": [1, 3] }, { "lineNumber": 15, @@ -61,7 +68,8 @@ "ruleAlias": "no-trailing-spaces", "ruleDescription": "Trailing spaces", "errorDetail": "Expected: 0; Actual: 1", - "errorContext": null + "errorContext": null, + "errorRange": [5, 1] }, { "lineNumber": 17, @@ -69,6 +77,7 @@ "ruleAlias": "no-hard-tabs", "ruleDescription": "Hard tabs", "errorDetail": "Column: 5", - "errorContext": null + "errorContext": null, + "errorRange": [5, 1] } ] \ No newline at end of file diff --git a/test/detailed-results-MD011-MD021.results.json b/test/detailed-results-MD011-MD021.results.json index d33eda0c..98bce8b9 100644 --- a/test/detailed-results-MD011-MD021.results.json +++ b/test/detailed-results-MD011-MD021.results.json @@ -5,7 +5,8 @@ "ruleAlias": "no-reversed-links", "ruleDescription": "Reversed link syntax", "errorDetail": "(reversed)[link]", - "errorContext": null + "errorContext": null, + "errorRange": [3, 16] }, { "lineNumber": 5, @@ -13,7 +14,8 @@ "ruleAlias": "no-multiple-blanks", "ruleDescription": "Multiple consecutive blank lines", "errorDetail": "Expected: 1; Actual: 2", - "errorContext": null + "errorContext": null, + "errorRange": null }, { "lineNumber": 6, @@ -21,7 +23,8 @@ "ruleAlias": "line-length", "ruleDescription": "Line length", "errorDetail": "Expected: 80; Actual: 99", - "errorContext": null + "errorContext": null, + "errorRange": [81, 19] }, { "lineNumber": 8, @@ -29,7 +32,8 @@ "ruleAlias": "commands-show-output", "ruleDescription": "Dollar signs used before commands without showing output", "errorDetail": null, - "errorContext": "$ command with no output" + "errorContext": "$ command with no output", + "errorRange": [5, 2] }, { "lineNumber": 10, @@ -37,7 +41,8 @@ "ruleAlias": "no-missing-space-atx", "ruleDescription": "No space after hash on atx style header", "errorDetail": null, - "errorContext": "#No space A" + "errorContext": "#No space A", + "errorRange": [1, 2] }, { "lineNumber": 12, @@ -45,7 +50,8 @@ "ruleAlias": "no-multiple-space-atx", "ruleDescription": "Multiple spaces after hash on atx style header", "errorDetail": null, - "errorContext": "# Multiple spaces A" + "errorContext": "# Multiple spaces A", + "errorRange": [1, 4] }, { "lineNumber": 14, @@ -53,7 +59,8 @@ "ruleAlias": "no-missing-space-closed-atx", "ruleDescription": "No space inside hashes on closed atx style header", "errorDetail": null, - "errorContext": "#No space B#" + "errorContext": "#No space B#", + "errorRange": [1, 2] }, { "lineNumber": 16, @@ -61,6 +68,7 @@ "ruleAlias": "no-multiple-space-closed-atx", "ruleDescription": "Multiple spaces inside hashes on closed atx style header", "errorDetail": null, - "errorContext": "# Multiple spaces B #" + "errorContext": "# Multiple spaces B #", + "errorRange": [1, 4] } ] \ No newline at end of file diff --git a/test/detailed-results-MD022-MD030.results.json b/test/detailed-results-MD022-MD030.results.json index 197651db..3d7b75a4 100644 --- a/test/detailed-results-MD022-MD030.results.json +++ b/test/detailed-results-MD022-MD030.results.json @@ -5,7 +5,8 @@ "ruleAlias": "blanks-around-headers", "ruleDescription": "Headers should be surrounded by blank lines", "errorDetail": null, - "errorContext": "# Heading" + "errorContext": "# Heading", + "errorRange": null }, { "lineNumber": 1, @@ -13,7 +14,8 @@ "ruleAlias": "header-start-left", "ruleDescription": "Headers must start at the beginning of the line", "errorDetail": null, - "errorContext": " # Heading" + "errorContext": " # Heading", + "errorRange": [1, 2] }, { "lineNumber": 4, @@ -21,7 +23,8 @@ "ruleAlias": "no-duplicate-header", "ruleDescription": "Multiple headers with the same content", "errorDetail": null, - "errorContext": "# Heading" + "errorContext": "# Heading", + "errorRange": null }, { "lineNumber": 4, @@ -29,7 +32,8 @@ "ruleAlias": "single-h1", "ruleDescription": "Multiple top level headers in the same document", "errorDetail": null, - "errorContext": "# Heading" + "errorContext": "# Heading", + "errorRange": null }, { "lineNumber": 6, @@ -37,7 +41,8 @@ "ruleAlias": "no-trailing-punctuation", "ruleDescription": "Trailing punctuation in header", "errorDetail": "Punctuation: '.'", - "errorContext": null + "errorContext": null, + "errorRange": [19, 1] }, { "lineNumber": 8, @@ -45,7 +50,8 @@ "ruleAlias": "no-multiple-space-blockquote", "ruleDescription": "Multiple spaces after blockquote symbol", "errorDetail": null, - "errorContext": "> Multiple spaces" + "errorContext": "> Multiple spaces", + "errorRange": [1, 4] }, { "lineNumber": 9, @@ -53,7 +59,8 @@ "ruleAlias": "no-blanks-blockquote", "ruleDescription": "Blank line inside blockquote", "errorDetail": null, - "errorContext": null + "errorContext": null, + "errorRange": null }, { "lineNumber": 13, @@ -61,7 +68,8 @@ "ruleAlias": "ol-prefix", "ruleDescription": "Ordered list item prefix", "errorDetail": "Expected: 1; Actual: 2", - "errorContext": null + "errorContext": null, + "errorRange": [1, 4] }, { "lineNumber": 13, @@ -69,6 +77,7 @@ "ruleAlias": "list-marker-space", "ruleDescription": "Spaces after list markers", "errorDetail": "Expected: 1; Actual: 2", - "errorContext": null + "errorContext": null, + "errorRange": [1, 4] } ] \ No newline at end of file diff --git a/test/detailed-results-MD031-MD040.results.json b/test/detailed-results-MD031-MD040.results.json index 2d258f0e..fd919f59 100644 --- a/test/detailed-results-MD031-MD040.results.json +++ b/test/detailed-results-MD031-MD040.results.json @@ -5,7 +5,8 @@ "ruleAlias": "blanks-around-fences", "ruleDescription": "Fenced code blocks should be surrounded by blank lines", "errorDetail": null, - "errorContext": "```" + "errorContext": "```", + "errorRange": null }, { "lineNumber": 4, @@ -13,7 +14,8 @@ "ruleAlias": "blanks-around-lists", "ruleDescription": "Lists should be surrounded by blank lines", "errorDetail": null, - "errorContext": "* List" + "errorContext": "* List", + "errorRange": null }, { "lineNumber": 6, @@ -21,7 +23,8 @@ "ruleAlias": "no-inline-html", "ruleDescription": "Inline HTML", "errorDetail": "Element: hr", - "errorContext": null + "errorContext": null, + "errorRange": [7, 5] }, { "lineNumber": 8, @@ -29,7 +32,8 @@ "ruleAlias": "no-bare-urls", "ruleDescription": "Bare URL used", "errorDetail": null, - "errorContext": "http://example.com" + "errorContext": "http://example.com", + "errorRange": [6, 18] }, { "lineNumber": 11, @@ -37,7 +41,8 @@ "ruleAlias": "hr-style", "ruleDescription": "Horizontal rule style", "errorDetail": "Expected: ---; Actual: ***", - "errorContext": null + "errorContext": null, + "errorRange": null }, { "lineNumber": 13, @@ -45,7 +50,8 @@ "ruleAlias": "no-emphasis-as-header", "ruleDescription": "Emphasis used instead of a header", "errorDetail": null, - "errorContext": "Emphasis" + "errorContext": "Emphasis", + "errorRange": null }, { "lineNumber": 15, @@ -53,7 +59,8 @@ "ruleAlias": "no-space-in-emphasis", "ruleDescription": "Spaces inside emphasis markers", "errorDetail": null, - "errorContext": "* inside *" + "errorContext": "* inside *", + "errorRange": [7, 10] }, { "lineNumber": 17, @@ -61,7 +68,8 @@ "ruleAlias": "no-space-in-code", "ruleDescription": "Spaces inside code span elements", "errorDetail": null, - "errorContext": "` inside `" + "errorContext": "` inside `", + "errorRange": [7, 10] }, { "lineNumber": 19, @@ -69,7 +77,8 @@ "ruleAlias": "no-space-in-links", "ruleDescription": "Spaces inside link text", "errorDetail": null, - "errorContext": "[ inside ]" + "errorContext": "[ inside ]", + "errorRange": [7, 10] }, { "lineNumber": 21, @@ -77,6 +86,7 @@ "ruleAlias": "fenced-code-language", "ruleDescription": "Fenced code blocks should have a language specified", "errorDetail": null, - "errorContext": "```" + "errorContext": "```", + "errorRange": null } ] \ No newline at end of file diff --git a/test/detailed-results-MD041-MD043.results.json b/test/detailed-results-MD041-MD043.results.json index 426894f5..1b90f8d3 100644 --- a/test/detailed-results-MD041-MD043.results.json +++ b/test/detailed-results-MD041-MD043.results.json @@ -5,7 +5,8 @@ "ruleAlias": "first-line-h1", "ruleDescription": "First line in file should be a top level header", "errorDetail": null, - "errorContext": "Not a header" + "errorContext": "Not a header", + "errorRange": null }, { "lineNumber": 3, @@ -13,7 +14,8 @@ "ruleAlias": "no-empty-links", "ruleDescription": "No empty links", "errorDetail": null, - "errorContext": "[empty]" + "errorContext": "[empty]", + "errorRange": [4, 7] }, { "lineNumber": 4, @@ -21,6 +23,7 @@ "ruleAlias": "required-headers", "ruleDescription": "Required header structure", "errorDetail": null, - "errorContext": "# Header" + "errorContext": "# Header", + "errorRange": null } ] \ No newline at end of file diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index a9159666..ef119390 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -220,7 +220,8 @@ module.exports.resultFormattingV1 = function resultFormattingV1(test) { "ruleDescription": "Multiple spaces inside hashes on closed atx style header", "errorDetail": null, - "errorContext": "# Multiple spa...style header #" } + "errorContext": "# Multiple spa...style header #", + "errorRange": [ 1, 4 ] } ], "./test/atx_header_spacing.md": [ { "lineNumber": 3, @@ -228,25 +229,29 @@ module.exports.resultFormattingV1 = function resultFormattingV1(test) { "ruleAlias": "first-header-h1", "ruleDescription": "First header should be a top level header", "errorDetail": "Expected: h1; Actual: h2", - "errorContext": null }, + "errorContext": null, + "errorRange": null }, { "lineNumber": 1, "ruleName": "MD018", "ruleAlias": "no-missing-space-atx", "ruleDescription": "No space after hash on atx style header", "errorDetail": null, - "errorContext": "#Header 1 {MD018}" }, + "errorContext": "#Header 1 {MD018}", + "errorRange": [ 1, 2 ] }, { "lineNumber": 3, "ruleName": "MD019", "ruleAlias": "no-multiple-space-atx", "ruleDescription": "Multiple spaces after hash on atx style header", "errorDetail": null, - "errorContext": "## Header 2 {MD019}" }, + "errorContext": "## Header 2 {MD019}", + "errorRange": [ 1, 5 ] }, { "lineNumber": 5, "ruleName": "MD019", "ruleAlias": "no-multiple-space-atx", "ruleDescription": "Multiple spaces after hash on atx style header", "errorDetail": null, - "errorContext": "## Header 3 {MD019}" } + "errorContext": "## Header 3 {MD019}", + "errorRange": [ 1, 6 ] } ], "./test/first_header_bad_atx.md": [ { "lineNumber": 1, @@ -254,7 +259,8 @@ module.exports.resultFormattingV1 = function resultFormattingV1(test) { "ruleAlias": "first-header-h1", "ruleDescription": "First header should be a top level header", "errorDetail": "Expected: h1; Actual: h2", - "errorContext": null } + "errorContext": null, + "errorRange": null } ] }; test.deepEqual(actualResult, expectedResult, "Undetected issues."); @@ -283,6 +289,38 @@ module.exports.resultFormattingV1 = function resultFormattingV1(test) { }); }; +module.exports.resultFormattingV1BadRegExp = function resultFormattingV1(test) { + test.expect(3); + var md010 = rules[8]; + test.equal(md010.name, "MD010", "Wrong rule."); + var md010RegExp = md010.regexp; + md010.regexp = /X/; + var options = { + "strings": { + "tab": "\t." + }, + "config": defaultConfig, + "resultVersion": 1 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + var expectedResult = { + "tab": [ + { "lineNumber": 1, + "ruleName": "MD010", + "ruleAlias": "no-hard-tabs", + "ruleDescription": "Hard tabs", + "errorDetail": "Column: 1", + "errorContext": null, + "errorRange": null } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + md010.regexp = md010RegExp; + test.done(); + }); +}; + module.exports.stringInputLineEndings = function stringInputLineEndings(test) { test.expect(2); var options = {