Move error reporting helpers out of core to simplify API for rule.func.

This commit is contained in:
David Anson 2018-01-14 21:53:35 -08:00
parent 6319cbba3e
commit 25b6e6f2f3
2 changed files with 148 additions and 136 deletions

View file

@ -232,50 +232,15 @@ function lintContent(
var ruleName = rule.names[0]; var ruleName = rule.names[0];
params.options = mergedRules[ruleName]; params.options = mergedRules[ruleName];
var errors = []; var errors = [];
function addError(lineNumber, detail, context, range) { function onError(errorInfo) {
errors.push({ errors.push({
"lineNumber": lineNumber + frontMatterLines.length, "lineNumber": errorInfo.lineNumber + frontMatterLines.length,
"detail": detail || null, "detail": errorInfo.detail || null,
"context": context || null, "context": errorInfo.context || null,
"range": range || null "range": errorInfo.range || null
}); });
} }
var errorHelpers = { rule.func(params, onError);
"add": function add(lineNumber) {
addError(lineNumber);
},
"addDetail": function addDetail(lineNumber, detail) {
addError(lineNumber, detail);
},
"addDetailIf":
function addDetailIf(lineNumber, expected, actual, detail, range) {
if (expected !== actual) {
addError(
lineNumber,
"Expected: " + expected + "; Actual: " + actual +
(detail ? "; " + detail : ""),
null,
range);
}
},
"addContext":
function addContext(lineNumber, context, left, right, range) {
if (context.length <= 30) {
// Nothing to do
} else if (left && right) {
context = context.substr(0, 15) + "..." + context.substr(-15);
} else if (right) {
context = "..." + context.substr(-30);
} else {
context = context.substr(0, 30) + "...";
}
addError(lineNumber, null, context, range);
},
"count": function count() {
return errors.length;
}
};
rule.func(params, errorHelpers);
// Record any errors (significant performance benefit from length check) // Record any errors (significant performance benefit from length check)
if (errors.length) { if (errors.length) {
errors.sort(lineNumberComparison); errors.sort(lineNumberComparison);

View file

@ -181,18 +181,56 @@ function flattenLists(params) {
return params.flattenLists; return params.flattenLists;
} }
// Adds a generic error object via the onError callback
function addError(onError, lineNumber, detail, context, range) {
onError({
"lineNumber": lineNumber,
"detail": detail,
"context": context,
"range": range
});
}
// Adds an error object with details conditionally via the onError callback
function addErrorDetailIf(
onError, lineNumber, expected, actual, detail, range) {
if (expected !== actual) {
addError(
onError,
lineNumber,
"Expected: " + expected + "; Actual: " + actual +
(detail ? "; " + detail : ""),
null,
range);
}
}
// Adds an error object with context via the onError callback
function addErrorContext(onError, lineNumber, context, left, right, range) {
if (context.length <= 30) {
// Nothing to do
} else if (left && right) {
context = context.substr(0, 15) + "..." + context.substr(-15);
} else if (right) {
context = "..." + context.substr(-30);
} else {
context = context.substr(0, 30) + "...";
}
addError(onError, lineNumber, null, context, range);
}
module.exports = [ module.exports = [
{ {
"names": [ "MD001", "header-increment" ], "names": [ "MD001", "header-increment" ],
"desc": "Header levels should only increment by one level at a time", "desc": "Header levels should only increment by one level at a time",
"tags": [ "headers" ], "tags": [ "headers" ],
"regexp": null, "regexp": null,
"func": function MD001(params, errors) { "func": function MD001(params, onError) {
var prevLevel = 0; var prevLevel = 0;
filterTokens(params, "heading_open", function forToken(token) { filterTokens(params, "heading_open", function forToken(token) {
var level = parseInt(token.tag.slice(1), 10); var level = parseInt(token.tag.slice(1), 10);
if (prevLevel && (level > prevLevel)) { if (prevLevel && (level > prevLevel)) {
errors.addDetailIf(token.lineNumber, addErrorDetailIf(onError, token.lineNumber,
"h" + (prevLevel + 1), "h" + level); "h" + (prevLevel + 1), "h" + level);
} }
prevLevel = level; prevLevel = level;
@ -205,12 +243,12 @@ module.exports = [
"desc": "First header should be a top level header", "desc": "First header should be a top level header",
"tags": [ "headers" ], "tags": [ "headers" ],
"regexp": null, "regexp": null,
"func": function MD002(params, errors) { "func": function MD002(params, onError) {
var level = params.options.level || 1; var level = params.options.level || 1;
var tag = "h" + level; var tag = "h" + level;
params.tokens.every(function forToken(token) { params.tokens.every(function forToken(token) {
if (token.type === "heading_open") { if (token.type === "heading_open") {
errors.addDetailIf(token.lineNumber, tag, token.tag); addErrorDetailIf(onError, token.lineNumber, tag, token.tag);
return false; return false;
} }
return true; return true;
@ -223,7 +261,7 @@ module.exports = [
"desc": "Header style", "desc": "Header style",
"tags": [ "headers" ], "tags": [ "headers" ],
"regexp": null, "regexp": null,
"func": function MD003(params, errors) { "func": function MD003(params, onError) {
var style = params.options.style || "consistent"; var style = params.options.style || "consistent";
filterTokens(params, "heading_open", function forToken(token) { filterTokens(params, "heading_open", function forToken(token) {
var styleForToken = headingStyleFor(token); var styleForToken = headingStyleFor(token);
@ -247,7 +285,8 @@ module.exports = [
} else if (style === "setext_with_atx_closed") { } else if (style === "setext_with_atx_closed") {
expected = h12 ? "setext" : "atx_closed"; expected = h12 ? "setext" : "atx_closed";
} }
errors.addDetailIf(token.lineNumber, expected, styleForToken); addErrorDetailIf(onError, token.lineNumber,
expected, styleForToken);
} }
} }
}); });
@ -259,7 +298,7 @@ module.exports = [
"desc": "Unordered list style", "desc": "Unordered list style",
"tags": [ "bullet", "ul" ], "tags": [ "bullet", "ul" ],
"regexp": listItemMarkerRe, "regexp": listItemMarkerRe,
"func": function MD004(params, errors) { "func": function MD004(params, onError) {
var style = params.options.style || "consistent"; var style = params.options.style || "consistent";
var expectedStyle = style; var expectedStyle = style;
var nestingStyles = []; var nestingStyles = [];
@ -276,11 +315,12 @@ module.exports = [
(itemStyle !== nestingStyles[nesting - 1])) { (itemStyle !== nestingStyles[nesting - 1])) {
nestingStyles[nesting] = itemStyle; nestingStyles[nesting] = itemStyle;
} else { } else {
errors.addDetailIf(item.lineNumber, addErrorDetailIf(onError, item.lineNumber,
nestingStyles[nesting], itemStyle); nestingStyles[nesting], itemStyle);
} }
} else { } else {
errors.addDetailIf(item.lineNumber, expectedStyle, itemStyle); addErrorDetailIf(onError, item.lineNumber,
expectedStyle, itemStyle);
} }
}); });
} }
@ -293,11 +333,11 @@ module.exports = [
"desc": "Inconsistent indentation for list items at the same level", "desc": "Inconsistent indentation for list items at the same level",
"tags": [ "bullet", "ul", "indentation" ], "tags": [ "bullet", "ul", "indentation" ],
"regexp": listItemMarkerRe, "regexp": listItemMarkerRe,
"func": function MD005(params, errors) { "func": function MD005(params, onError) {
flattenLists(params).forEach(function forList(list) { flattenLists(params).forEach(function forList(list) {
var indent = indentFor(list.items[0]); var indent = indentFor(list.items[0]);
list.items.forEach(function forItem(item) { list.items.forEach(function forItem(item) {
errors.addDetailIf(item.lineNumber, indent, indentFor(item)); addErrorDetailIf(onError, item.lineNumber, indent, indentFor(item));
}); });
}); });
} }
@ -308,10 +348,11 @@ module.exports = [
"desc": "Consider starting bulleted lists at the beginning of the line", "desc": "Consider starting bulleted lists at the beginning of the line",
"tags": [ "bullet", "ul", "indentation" ], "tags": [ "bullet", "ul", "indentation" ],
"regexp": listItemMarkerRe, "regexp": listItemMarkerRe,
"func": function MD006(params, errors) { "func": function MD006(params, onError) {
flattenLists(params).forEach(function forList(list) { flattenLists(params).forEach(function forList(list) {
if (list.unordered && !list.nesting) { if (list.unordered && !list.nesting) {
errors.addDetailIf(list.open.lineNumber, 0, indentFor(list.open)); addErrorDetailIf(onError, list.open.lineNumber,
0, indentFor(list.open));
} }
}); });
} }
@ -322,14 +363,14 @@ module.exports = [
"desc": "Unordered list indentation", "desc": "Unordered list indentation",
"tags": [ "bullet", "ul", "indentation" ], "tags": [ "bullet", "ul", "indentation" ],
"regexp": listItemMarkerRe, "regexp": listItemMarkerRe,
"func": function MD007(params, errors) { "func": function MD007(params, onError) {
var optionsIndent = params.options.indent || 2; var optionsIndent = params.options.indent || 2;
var prevIndent = 0; var prevIndent = 0;
flattenLists(params).forEach(function forList(list) { flattenLists(params).forEach(function forList(list) {
if (list.unordered && list.parentsUnordered) { if (list.unordered && list.parentsUnordered) {
var indent = indentFor(list.open); var indent = indentFor(list.open);
if (indent > prevIndent) { if (indent > prevIndent) {
errors.addDetailIf(list.open.lineNumber, addErrorDetailIf(onError, list.open.lineNumber,
prevIndent + optionsIndent, indent); prevIndent + optionsIndent, indent);
} }
prevIndent = indent; prevIndent = indent;
@ -343,7 +384,7 @@ module.exports = [
"desc": "Trailing spaces", "desc": "Trailing spaces",
"tags": [ "whitespace" ], "tags": [ "whitespace" ],
"regexp": trailingSpaceRe, "regexp": trailingSpaceRe,
"func": function MD009(params, errors) { "func": function MD009(params, onError) {
var brSpaces = params.options.br_spaces || 0; var brSpaces = params.options.br_spaces || 0;
var listItemEmptyLines = params.options.list_item_empty_lines; var listItemEmptyLines = params.options.list_item_empty_lines;
var allowListItemEmptyLines = var allowListItemEmptyLines =
@ -361,7 +402,7 @@ module.exports = [
if (trailingSpaceRe.test(line) && if (trailingSpaceRe.test(line) &&
(listItemLineNumbers.indexOf(lineNumber) === -1)) { (listItemLineNumbers.indexOf(lineNumber) === -1)) {
var expected = (brSpaces < 2) ? 0 : brSpaces; var expected = (brSpaces < 2) ? 0 : brSpaces;
errors.addDetailIf(lineNumber, addErrorDetailIf(onError, lineNumber,
expected, line.length - shared.trimRight(line).length); expected, line.length - shared.trimRight(line).length);
} }
}); });
@ -373,12 +414,12 @@ module.exports = [
"desc": "Hard tabs", "desc": "Hard tabs",
"tags": [ "whitespace", "hard_tab" ], "tags": [ "whitespace", "hard_tab" ],
"regexp": tabRe, "regexp": tabRe,
"func": function MD010(params, errors) { "func": function MD010(params, onError) {
var codeBlocks = params.options.code_blocks; var codeBlocks = params.options.code_blocks;
var includeCodeBlocks = (codeBlocks === undefined) ? true : !!codeBlocks; var includeCodeBlocks = (codeBlocks === undefined) ? true : !!codeBlocks;
forEachLine(params, function forLine(line, lineIndex, inCode) { forEachLine(params, function forLine(line, lineIndex, inCode) {
if (tabRe.test(line) && (!inCode || includeCodeBlocks)) { if (tabRe.test(line) && (!inCode || includeCodeBlocks)) {
errors.addDetail(lineIndex + 1, addError(onError, lineIndex + 1,
"Column: " + (line.indexOf("\t") + 1)); "Column: " + (line.indexOf("\t") + 1));
} }
}); });
@ -390,11 +431,11 @@ module.exports = [
"desc": "Reversed link syntax", "desc": "Reversed link syntax",
"tags": [ "links" ], "tags": [ "links" ],
"regexp": reversedLinkRe, "regexp": reversedLinkRe,
"func": function MD011(params, errors) { "func": function MD011(params, onError) {
forEachInlineChild(params, "text", function forToken(token) { forEachInlineChild(params, "text", function forToken(token) {
var match = reversedLinkRe.exec(token.content); var match = reversedLinkRe.exec(token.content);
if (match) { if (match) {
errors.addDetail(token.lineNumber, match[0]); addError(onError, token.lineNumber, match[0]);
} }
}); });
} }
@ -405,13 +446,13 @@ module.exports = [
"desc": "Multiple consecutive blank lines", "desc": "Multiple consecutive blank lines",
"tags": [ "whitespace", "blank_lines" ], "tags": [ "whitespace", "blank_lines" ],
"regexp": null, "regexp": null,
"func": function MD012(params, errors) { "func": function MD012(params, onError) {
var maximum = params.options.maximum || 1; var maximum = params.options.maximum || 1;
var count = 0; var count = 0;
forEachLine(params, function forLine(line, lineIndex, inCode) { forEachLine(params, function forLine(line, lineIndex, inCode) {
count = (inCode || line.trim().length) ? 0 : count + 1; count = (inCode || line.trim().length) ? 0 : count + 1;
if (maximum < count) { if (maximum < count) {
errors.addDetailIf(lineIndex + 1, maximum, count); addErrorDetailIf(onError, lineIndex + 1, maximum, count);
} }
}); });
} }
@ -422,7 +463,7 @@ module.exports = [
"desc": "Line length", "desc": "Line length",
"tags": [ "line_length" ], "tags": [ "line_length" ],
"regexp": longLineReFunc, "regexp": longLineReFunc,
"func": function MD013(params, errors) { "func": function MD013(params, onError) {
var lineLength = params.options.line_length || defaultLineLength; var lineLength = params.options.line_length || defaultLineLength;
var codeBlocks = params.options.code_blocks; var codeBlocks = params.options.code_blocks;
var includeCodeBlocks = (codeBlocks === undefined) ? true : !!codeBlocks; var includeCodeBlocks = (codeBlocks === undefined) ? true : !!codeBlocks;
@ -468,7 +509,7 @@ module.exports = [
(linkOnlyLineNumbers.indexOf(lineNumber) < 0) && (linkOnlyLineNumbers.indexOf(lineNumber) < 0) &&
longLineRe.test(line) && longLineRe.test(line) &&
!labelRe.test(line)) { !labelRe.test(line)) {
errors.addDetailIf(lineNumber, lineLength, line.length); addErrorDetailIf(onError, lineNumber, lineLength, line.length);
} }
}); });
} }
@ -479,7 +520,7 @@ module.exports = [
"desc": "Dollar signs used before commands without showing output", "desc": "Dollar signs used before commands without showing output",
"tags": [ "code" ], "tags": [ "code" ],
"regexp": dollarCommandRe, "regexp": dollarCommandRe,
"func": function MD014(params, errors) { "func": function MD014(params, onError) {
[ "code_block", "fence" ].forEach(function forType(type) { [ "code_block", "fence" ].forEach(function forType(type) {
filterTokens(params, type, function forToken(token) { filterTokens(params, type, function forToken(token) {
var allBlank = true; var allBlank = true;
@ -487,7 +528,7 @@ module.exports = [
.every(function forLine(line) { .every(function forLine(line) {
return !line || (allBlank = false) || dollarCommandRe.test(line); return !line || (allBlank = false) || dollarCommandRe.test(line);
}) && !allBlank) { }) && !allBlank) {
errors.addContext(token.lineNumber, addErrorContext(onError, token.lineNumber,
token.content.split(shared.newLineRe)[0].trim()); token.content.split(shared.newLineRe)[0].trim());
} }
}); });
@ -500,10 +541,10 @@ module.exports = [
"desc": "No space after hash on atx style header", "desc": "No space after hash on atx style header",
"tags": [ "headers", "atx", "spaces" ], "tags": [ "headers", "atx", "spaces" ],
"regexp": atxHeaderSpaceRe, "regexp": atxHeaderSpaceRe,
"func": function MD018(params, errors) { "func": function MD018(params, onError) {
forEachLine(params, function forLine(line, lineIndex, inCode) { forEachLine(params, function forLine(line, lineIndex, inCode) {
if (!inCode && /^#+[^#\s]/.test(line) && !/#$/.test(line)) { if (!inCode && /^#+[^#\s]/.test(line) && !/#$/.test(line)) {
errors.addContext(lineIndex + 1, line.trim()); addErrorContext(onError, lineIndex + 1, line.trim());
} }
}); });
} }
@ -514,11 +555,11 @@ module.exports = [
"desc": "Multiple spaces after hash on atx style header", "desc": "Multiple spaces after hash on atx style header",
"tags": [ "headers", "atx", "spaces" ], "tags": [ "headers", "atx", "spaces" ],
"regexp": atxHeaderSpaceRe, "regexp": atxHeaderSpaceRe,
"func": function MD019(params, errors) { "func": function MD019(params, onError) {
filterTokens(params, "heading_open", function forToken(token) { filterTokens(params, "heading_open", function forToken(token) {
if ((headingStyleFor(token) === "atx") && if ((headingStyleFor(token) === "atx") &&
/^#+\s\s/.test(token.line)) { /^#+\s\s/.test(token.line)) {
errors.addContext(token.lineNumber, token.line.trim()); addErrorContext(onError, token.lineNumber, token.line.trim());
} }
}); });
} }
@ -529,13 +570,13 @@ module.exports = [
"desc": "No space inside hashes on closed atx style header", "desc": "No space inside hashes on closed atx style header",
"tags": [ "headers", "atx_closed", "spaces" ], "tags": [ "headers", "atx_closed", "spaces" ],
"regexp": atxClosedHeaderNoSpaceRe, "regexp": atxClosedHeaderNoSpaceRe,
"func": function MD020(params, errors) { "func": function MD020(params, onError) {
forEachLine(params, function forLine(line, lineIndex, inCode) { forEachLine(params, function forLine(line, lineIndex, inCode) {
if (!inCode && /^#+[^#]*[^\\]#+$/.test(line)) { if (!inCode && /^#+[^#]*[^\\]#+$/.test(line)) {
var left = /^#+[^#\s]/.test(line); var left = /^#+[^#\s]/.test(line);
var right = /[^#\s]#+$/.test(line); var right = /[^#\s]#+$/.test(line);
if (left || right) { if (left || right) {
errors.addContext(lineIndex + 1, line.trim(), left, right); addErrorContext(onError, lineIndex + 1, line.trim(), left, right);
} }
} }
}); });
@ -547,13 +588,14 @@ module.exports = [
"desc": "Multiple spaces inside hashes on closed atx style header", "desc": "Multiple spaces inside hashes on closed atx style header",
"tags": [ "headers", "atx_closed", "spaces" ], "tags": [ "headers", "atx_closed", "spaces" ],
"regexp": atxClosedHeaderSpaceRe, "regexp": atxClosedHeaderSpaceRe,
"func": function MD021(params, errors) { "func": function MD021(params, onError) {
filterTokens(params, "heading_open", function forToken(token) { filterTokens(params, "heading_open", function forToken(token) {
if (headingStyleFor(token) === "atx_closed") { if (headingStyleFor(token) === "atx_closed") {
var left = /^#+\s\s/.test(token.line); var left = /^#+\s\s/.test(token.line);
var right = /\s\s#+$/.test(token.line); var right = /\s\s#+$/.test(token.line);
if (left || right) { if (left || right) {
errors.addContext(token.lineNumber, token.line.trim(), left, right); addErrorContext(onError, token.lineNumber,
token.line.trim(), left, right);
} }
} }
}); });
@ -565,14 +607,14 @@ module.exports = [
"desc": "Headers should be surrounded by blank lines", "desc": "Headers should be surrounded by blank lines",
"tags": [ "headers", "blank_lines" ], "tags": [ "headers", "blank_lines" ],
"regexp": null, "regexp": null,
"func": function MD022(params, errors) { "func": function MD022(params, onError) {
var prevHeadingLineNumber = 0; var prevHeadingLineNumber = 0;
var prevMaxLineIndex = -1; var prevMaxLineIndex = -1;
var needBlankLine = false; var needBlankLine = false;
params.tokens.forEach(function forToken(token) { params.tokens.forEach(function forToken(token) {
if (token.type === "heading_open") { if (token.type === "heading_open") {
if ((token.map[0] - prevMaxLineIndex) === 0) { if ((token.map[0] - prevMaxLineIndex) === 0) {
errors.addContext(token.lineNumber, token.line.trim()); addErrorContext(onError, token.lineNumber, token.line.trim());
} }
} else if (token.type === "heading_close") { } else if (token.type === "heading_close") {
needBlankLine = true; needBlankLine = true;
@ -580,7 +622,7 @@ module.exports = [
if (token.map) { if (token.map) {
if (needBlankLine) { if (needBlankLine) {
if ((token.map[0] - prevMaxLineIndex) === 0) { if ((token.map[0] - prevMaxLineIndex) === 0) {
errors.addContext(prevHeadingLineNumber, addErrorContext(onError, prevHeadingLineNumber,
params.lines[prevHeadingLineNumber - 1].trim()); params.lines[prevHeadingLineNumber - 1].trim());
} }
needBlankLine = false; needBlankLine = false;
@ -599,10 +641,10 @@ module.exports = [
"desc": "Headers must start at the beginning of the line", "desc": "Headers must start at the beginning of the line",
"tags": [ "headers", "spaces" ], "tags": [ "headers", "spaces" ],
"regexp": spaceBeforeHeaderRe, "regexp": spaceBeforeHeaderRe,
"func": function MD023(params, errors) { "func": function MD023(params, onError) {
filterTokens(params, "heading_open", function forToken(token) { filterTokens(params, "heading_open", function forToken(token) {
if (spaceBeforeHeaderRe.test(token.line)) { if (spaceBeforeHeaderRe.test(token.line)) {
errors.addContext(token.lineNumber, token.line); addErrorContext(onError, token.lineNumber, token.line);
} }
}); });
} }
@ -613,13 +655,13 @@ module.exports = [
"desc": "Multiple headers with the same content", "desc": "Multiple headers with the same content",
"tags": [ "headers" ], "tags": [ "headers" ],
"regexp": null, "regexp": null,
"func": function MD024(params, errors) { "func": function MD024(params, onError) {
var knownContent = []; var knownContent = [];
forEachHeading(params, function forHeading(heading, content) { forEachHeading(params, function forHeading(heading, content) {
if (knownContent.indexOf(content) === -1) { if (knownContent.indexOf(content) === -1) {
knownContent.push(content); knownContent.push(content);
} else { } else {
errors.addContext(heading.lineNumber, heading.line.trim()); addErrorContext(onError, heading.lineNumber, heading.line.trim());
} }
}); });
} }
@ -630,14 +672,14 @@ module.exports = [
"desc": "Multiple top level headers in the same document", "desc": "Multiple top level headers in the same document",
"tags": [ "headers" ], "tags": [ "headers" ],
"regexp": null, "regexp": null,
"func": function MD025(params, errors) { "func": function MD025(params, onError) {
var level = params.options.level || 1; var level = params.options.level || 1;
var tag = "h" + level; var tag = "h" + level;
var hasTopLevelHeading = false; var hasTopLevelHeading = false;
filterTokens(params, "heading_open", function forToken(token) { filterTokens(params, "heading_open", function forToken(token) {
if (token.tag === tag) { if (token.tag === tag) {
if (hasTopLevelHeading) { if (hasTopLevelHeading) {
errors.addContext(token.lineNumber, token.line.trim()); addErrorContext(onError, token.lineNumber, token.line.trim());
} else if (token.lineNumber === 1) { } else if (token.lineNumber === 1) {
hasTopLevelHeading = true; hasTopLevelHeading = true;
} }
@ -651,13 +693,13 @@ module.exports = [
"desc": "Trailing punctuation in header", "desc": "Trailing punctuation in header",
"tags": [ "headers" ], "tags": [ "headers" ],
"regexp": trailingPunctuationRe, "regexp": trailingPunctuationRe,
"func": function MD026(params, errors) { "func": function MD026(params, onError) {
var punctuation = params.options.punctuation || ".,;:!?"; var punctuation = params.options.punctuation || ".,;:!?";
var re = new RegExp("[" + punctuation + "]$"); var re = new RegExp("[" + punctuation + "]$");
forEachHeading(params, function forHeading(heading, content) { forEachHeading(params, function forHeading(heading, content) {
var match = re.exec(content); var match = re.exec(content);
if (match) { if (match) {
errors.addDetail(heading.lineNumber, addError(onError, heading.lineNumber,
"Punctuation: '" + match[0] + "'"); "Punctuation: '" + match[0] + "'");
} }
}); });
@ -669,7 +711,7 @@ module.exports = [
"desc": "Multiple spaces after blockquote symbol", "desc": "Multiple spaces after blockquote symbol",
"tags": [ "blockquote", "whitespace", "indentation" ], "tags": [ "blockquote", "whitespace", "indentation" ],
"regexp": spaceAfterBlockQuote, "regexp": spaceAfterBlockQuote,
"func": function MD027(params, errors) { "func": function MD027(params, onError) {
var blockquoteNesting = 0; var blockquoteNesting = 0;
var listItemNesting = 0; var listItemNesting = 0;
params.tokens.forEach(function forToken(token) { params.tokens.forEach(function forToken(token) {
@ -686,12 +728,13 @@ module.exports = [
/^(\s*>)+\s\s+>/.test(token.line) : /^(\s*>)+\s\s+>/.test(token.line) :
/^(\s*>)+\s\s/.test(token.line); /^(\s*>)+\s\s/.test(token.line);
if (multipleSpaces) { if (multipleSpaces) {
errors.addContext(token.lineNumber, token.line); addErrorContext(onError, token.lineNumber, token.line);
} }
token.content.split(shared.newLineRe) token.content.split(shared.newLineRe)
.forEach(function forLine(line, offset) { .forEach(function forLine(line, offset) {
if (/^\s/.test(line)) { if (/^\s/.test(line)) {
errors.addContext(token.lineNumber + offset, "> " + line); addErrorContext(onError, token.lineNumber + offset,
"> " + line);
} }
}); });
} }
@ -704,12 +747,12 @@ module.exports = [
"desc": "Blank line inside blockquote", "desc": "Blank line inside blockquote",
"tags": [ "blockquote", "whitespace" ], "tags": [ "blockquote", "whitespace" ],
"regexp": null, "regexp": null,
"func": function MD028(params, errors) { "func": function MD028(params, onError) {
var prevToken = {}; var prevToken = {};
params.tokens.forEach(function forToken(token) { params.tokens.forEach(function forToken(token) {
if ((token.type === "blockquote_open") && if ((token.type === "blockquote_open") &&
(prevToken.type === "blockquote_close")) { (prevToken.type === "blockquote_close")) {
errors.add(token.lineNumber - 1); addError(onError, token.lineNumber - 1);
} }
prevToken = token; prevToken = token;
}); });
@ -721,7 +764,7 @@ module.exports = [
"desc": "Ordered list item prefix", "desc": "Ordered list item prefix",
"tags": [ "ol" ], "tags": [ "ol" ],
"regexp": listItemMarkerRe, "regexp": listItemMarkerRe,
"func": function MD029(params, errors) { "func": function MD029(params, onError) {
var style = params.options.style || "one_or_ordered"; var style = params.options.style || "one_or_ordered";
var numberRe = /^[\s>]*([^.)]*)[.)]/; var numberRe = /^[\s>]*([^.)]*)[.)]/;
flattenLists(params).forEach(function forList(list) { flattenLists(params).forEach(function forList(list) {
@ -735,7 +778,7 @@ module.exports = [
var number = 1; var number = 1;
list.items.forEach(function forItem(item) { list.items.forEach(function forItem(item) {
var match = numberRe.exec(item.line); var match = numberRe.exec(item.line);
errors.addDetailIf(item.lineNumber, addErrorDetailIf(onError, item.lineNumber,
String(number), !match || match[1], String(number), !match || match[1],
"Style: " + (listStyle === "one" ? "1/1/1" : "1/2/3")); "Style: " + (listStyle === "one" ? "1/1/1" : "1/2/3"));
if (listStyle === "ordered") { if (listStyle === "ordered") {
@ -752,7 +795,7 @@ module.exports = [
"desc": "Spaces after list markers", "desc": "Spaces after list markers",
"tags": [ "ol", "ul", "whitespace" ], "tags": [ "ol", "ul", "whitespace" ],
"regexp": listItemMarkerRe, "regexp": listItemMarkerRe,
"func": function MD030(params, errors) { "func": function MD030(params, onError) {
var ulSingle = params.options.ul_single || 1; var ulSingle = params.options.ul_single || 1;
var olSingle = params.options.ol_single || 1; var olSingle = params.options.ol_single || 1;
var ulMulti = params.options.ul_multi || 1; var ulMulti = params.options.ul_multi || 1;
@ -765,7 +808,7 @@ module.exports = [
(allSingle ? olSingle : olMulti); (allSingle ? olSingle : olMulti);
list.items.forEach(function forItem(item) { list.items.forEach(function forItem(item) {
var match = /^[\s>]*\S+(\s+)/.exec(item.line); var match = /^[\s>]*\S+(\s+)/.exec(item.line);
errors.addDetailIf(item.lineNumber, addErrorDetailIf(onError, item.lineNumber,
expectedSpaces, (match ? match[1].length : 0)); expectedSpaces, (match ? match[1].length : 0));
}); });
}); });
@ -777,12 +820,12 @@ module.exports = [
"desc": "Fenced code blocks should be surrounded by blank lines", "desc": "Fenced code blocks should be surrounded by blank lines",
"tags": [ "code", "blank_lines" ], "tags": [ "code", "blank_lines" ],
"regexp": null, "regexp": null,
"func": function MD031(params, errors) { "func": function MD031(params, onError) {
var lines = params.lines; var lines = params.lines;
forEachLine(params, function forLine(line, i, inCode, onFence) { forEachLine(params, function forLine(line, i, inCode, onFence) {
if (((onFence > 0) && (i - 1 >= 0) && lines[i - 1].length) || if (((onFence > 0) && (i - 1 >= 0) && lines[i - 1].length) ||
((onFence < 0) && (i + 1 < lines.length) && lines[i + 1].length)) { ((onFence < 0) && (i + 1 < lines.length) && lines[i + 1].length)) {
errors.addContext(i + 1, lines[i].trim()); addErrorContext(onError, i + 1, lines[i].trim());
} }
}); });
} }
@ -793,7 +836,7 @@ module.exports = [
"desc": "Lists should be surrounded by blank lines", "desc": "Lists should be surrounded by blank lines",
"tags": [ "bullet", "ul", "ol", "blank_lines" ], "tags": [ "bullet", "ul", "ol", "blank_lines" ],
"regexp": null, "regexp": null,
"func": function MD032(params, errors) { "func": function MD032(params, onError) {
var blankOrListRe = /^[\s>]*($|\s)/; var blankOrListRe = /^[\s>]*($|\s)/;
var inList = false; var inList = false;
var prevLine = ""; var prevLine = "";
@ -804,12 +847,12 @@ module.exports = [
if (listMarker && !inList && !blankOrListRe.test(prevLine)) { if (listMarker && !inList && !blankOrListRe.test(prevLine)) {
// Check whether this list prefix can interrupt a paragraph // Check whether this list prefix can interrupt a paragraph
if (listItemMarkerInterruptsRe.test(lineTrim)) { if (listItemMarkerInterruptsRe.test(lineTrim)) {
errors.addContext(lineIndex + 1, lineTrim); addErrorContext(onError, lineIndex + 1, lineTrim);
} else { } else {
listMarker = false; listMarker = false;
} }
} else if (!listMarker && inList && !blankOrListRe.test(line)) { } else if (!listMarker && inList && !blankOrListRe.test(line)) {
errors.addContext(lineIndex, lineTrim); addErrorContext(onError, lineIndex, lineTrim);
} }
inList = listMarker; inList = listMarker;
} }
@ -823,7 +866,7 @@ module.exports = [
"desc": "Inline HTML", "desc": "Inline HTML",
"tags": [ "html" ], "tags": [ "html" ],
"regexp": htmlRe, "regexp": htmlRe,
"func": function MD033(params, errors) { "func": function MD033(params, onError) {
var allowedElements = (params.options.allowed_elements || []) var allowedElements = (params.options.allowed_elements || [])
.map(function forElement(element) { .map(function forElement(element) {
return element.toLowerCase(); return element.toLowerCase();
@ -842,7 +885,7 @@ module.exports = [
return allowedElements.indexOf(element) === -1; return allowedElements.indexOf(element) === -1;
}); });
if (allowed.length) { if (allowed.length) {
errors.addDetail(token.lineNumber + offset, addError(onError, token.lineNumber + offset,
"Element: " + allowed[0]); "Element: " + allowed[0]);
} }
}); });
@ -857,7 +900,7 @@ module.exports = [
"desc": "Bare URL used", "desc": "Bare URL used",
"tags": [ "links", "url" ], "tags": [ "links", "url" ],
"regexp": bareUrlRe, "regexp": bareUrlRe,
"func": function MD034(params, errors) { "func": function MD034(params, onError) {
filterTokens(params, "inline", function forToken(token) { filterTokens(params, "inline", function forToken(token) {
var inLink = false; var inLink = false;
token.children.forEach(function forChild(child) { token.children.forEach(function forChild(child) {
@ -869,7 +912,7 @@ module.exports = [
} else if ((child.type === "text") && } else if ((child.type === "text") &&
!inLink && !inLink &&
(match = bareUrlRe.exec(child.content))) { (match = bareUrlRe.exec(child.content))) {
errors.addContext(child.lineNumber, match[0]); addErrorContext(onError, child.lineNumber, match[0]);
} }
}); });
}); });
@ -881,14 +924,14 @@ module.exports = [
"desc": "Horizontal rule style", "desc": "Horizontal rule style",
"tags": [ "hr" ], "tags": [ "hr" ],
"regexp": null, "regexp": null,
"func": function MD035(params, errors) { "func": function MD035(params, onError) {
var style = params.options.style || "consistent"; var style = params.options.style || "consistent";
filterTokens(params, "hr", function forToken(token) { filterTokens(params, "hr", function forToken(token) {
var lineTrim = token.line.trim(); var lineTrim = token.line.trim();
if (style === "consistent") { if (style === "consistent") {
style = lineTrim; style = lineTrim;
} }
errors.addDetailIf(token.lineNumber, style, lineTrim); addErrorDetailIf(onError, token.lineNumber, style, lineTrim);
}); });
} }
}, },
@ -898,7 +941,7 @@ module.exports = [
"desc": "Emphasis used instead of a header", "desc": "Emphasis used instead of a header",
"tags": [ "headers", "emphasis" ], "tags": [ "headers", "emphasis" ],
"regexp": null, "regexp": null,
"func": function MD036(params, errors) { "func": function MD036(params, onError) {
var punctuation = params.options.punctuation || ".,;:!?"; var punctuation = params.options.punctuation || ".,;:!?";
var re = new RegExp("[" + punctuation + "]$"); var re = new RegExp("[" + punctuation + "]$");
function base(token) { function base(token) {
@ -914,7 +957,7 @@ module.exports = [
(children[0].type === "em_open")) && (children[0].type === "em_open")) &&
(children[1].type === "text") && (children[1].type === "text") &&
!re.test(children[1].content)) { !re.test(children[1].content)) {
errors.addContext(t.lineNumber, children[1].content); addErrorContext(onError, t.lineNumber, children[1].content);
} }
return base; return base;
}; };
@ -947,7 +990,7 @@ module.exports = [
"desc": "Spaces inside emphasis markers", "desc": "Spaces inside emphasis markers",
"tags": [ "whitespace", "emphasis" ], "tags": [ "whitespace", "emphasis" ],
"regexp": null, "regexp": null,
"func": function MD037(params, errors) { "func": function MD037(params, onError) {
forEachInlineChild(params, "text", function forToken(token) { forEachInlineChild(params, "text", function forToken(token) {
var left = true; var left = true;
var match = /\s(\*\*?|__?)\s.+\1/.exec(token.content); var match = /\s(\*\*?|__?)\s.+\1/.exec(token.content);
@ -960,8 +1003,8 @@ module.exports = [
var line = params.lines[token.lineNumber - 1]; var line = params.lines[token.lineNumber - 1];
var column = line.indexOf(text) + 1; var column = line.indexOf(text) + 1;
var length = text.length; var length = text.length;
errors.addContext( addErrorContext(onError, token.lineNumber,
token.lineNumber, text, left, !left, [ column, length ]); text, left, !left, [ column, length ]);
} }
}); });
} }
@ -972,7 +1015,7 @@ module.exports = [
"desc": "Spaces inside code span elements", "desc": "Spaces inside code span elements",
"tags": [ "whitespace", "code" ], "tags": [ "whitespace", "code" ],
"regexp": null, "regexp": null,
"func": function MD038(params, errors) { "func": function MD038(params, onError) {
var inlineCodeSpansRe = /(?:^|[^\\])((`+)((?:.*?[^`])|)\2(?!`))/g; var inlineCodeSpansRe = /(?:^|[^\\])((`+)((?:.*?[^`])|)\2(?!`))/g;
forEachInlineChild(params, "code_inline", forEachInlineChild(params, "code_inline",
function forToken(token) { function forToken(token) {
@ -985,11 +1028,11 @@ module.exports = [
var column = match.index + 1 + (match[0].length - length); var column = match.index + 1 + (match[0].length - length);
var range = [ column, length ]; var range = [ column, length ];
if (/^\s([^`]|$)/.test(content)) { if (/^\s([^`]|$)/.test(content)) {
errors.addContext( addErrorContext(onError, token.lineNumber,
token.lineNumber, inlineCodeSpan, true, false, range); inlineCodeSpan, true, false, range);
} else if (/[^`]\s$/.test(content)) { } else if (/[^`]\s$/.test(content)) {
errors.addContext( addErrorContext(onError, token.lineNumber,
token.lineNumber, inlineCodeSpan, false, true, range); inlineCodeSpan, false, true, range);
} }
} }
}); });
@ -1001,7 +1044,7 @@ module.exports = [
"desc": "Spaces inside link text", "desc": "Spaces inside link text",
"tags": [ "whitespace", "links" ], "tags": [ "whitespace", "links" ],
"regexp": spaceInsideLinkRe, "regexp": spaceInsideLinkRe,
"func": function MD039(params, errors) { "func": function MD039(params, onError) {
filterTokens(params, "inline", function forToken(token) { filterTokens(params, "inline", function forToken(token) {
var inLink = false; var inLink = false;
var linkText = ""; var linkText = "";
@ -1014,8 +1057,8 @@ module.exports = [
var left = shared.trimLeft(linkText).length !== linkText.length; var left = shared.trimLeft(linkText).length !== linkText.length;
var right = shared.trimRight(linkText).length !== linkText.length; var right = shared.trimRight(linkText).length !== linkText.length;
if (left || right) { if (left || right) {
errors.addContext( addErrorContext(onError, token.lineNumber,
token.lineNumber, "[" + linkText + "]", left, right); "[" + linkText + "]", left, right);
} }
} else if (inLink) { } else if (inLink) {
linkText += child.content; linkText += child.content;
@ -1030,10 +1073,10 @@ module.exports = [
"desc": "Fenced code blocks should have a language specified", "desc": "Fenced code blocks should have a language specified",
"tags": [ "code", "language" ], "tags": [ "code", "language" ],
"regexp": null, "regexp": null,
"func": function MD040(params, errors) { "func": function MD040(params, onError) {
filterTokens(params, "fence", function forToken(token) { filterTokens(params, "fence", function forToken(token) {
if (!token.info.trim()) { if (!token.info.trim()) {
errors.addContext(token.lineNumber, token.line); addErrorContext(onError, token.lineNumber, token.line);
} }
}); });
} }
@ -1044,7 +1087,7 @@ module.exports = [
"desc": "First line in file should be a top level header", "desc": "First line in file should be a top level header",
"tags": [ "headers" ], "tags": [ "headers" ],
"regexp": null, "regexp": null,
"func": function MD041(params, errors) { "func": function MD041(params, onError) {
var level = params.options.level || 1; var level = params.options.level || 1;
var frontMatterTitle = params.options.front_matter_title; var frontMatterTitle = params.options.front_matter_title;
var tag = "h" + level; var tag = "h" + level;
@ -1054,7 +1097,7 @@ module.exports = [
if (token.type === "heading_open") { if (token.type === "heading_open") {
if (!((token.lineNumber === 1) || (index > 0)) || if (!((token.lineNumber === 1) || (index > 0)) ||
(token.tag !== tag)) { (token.tag !== tag)) {
errors.addContext(token.lineNumber, token.line); addErrorContext(onError, token.lineNumber, token.line);
} }
return false; return false;
} else if (token.type === "html_block") { } else if (token.type === "html_block") {
@ -1064,7 +1107,7 @@ module.exports = [
!params.frontMatterLines.some(function forLine(line) { !params.frontMatterLines.some(function forLine(line) {
return frontMatterTitleRe.test(line); return frontMatterTitleRe.test(line);
})) { })) {
errors.addContext(token.lineNumber, token.line); addErrorContext(onError, token.lineNumber, token.line);
} }
return false; return false;
}); });
@ -1076,7 +1119,7 @@ module.exports = [
"desc": "No empty links", "desc": "No empty links",
"tags": [ "links" ], "tags": [ "links" ],
"regexp": emptyLinkRe, "regexp": emptyLinkRe,
"func": function MD042(params, errors) { "func": function MD042(params, onError) {
filterTokens(params, "inline", function forToken(token) { filterTokens(params, "inline", function forToken(token) {
var inLink = false; var inLink = false;
var linkText = ""; var linkText = "";
@ -1093,7 +1136,8 @@ module.exports = [
} else if (child.type === "link_close") { } else if (child.type === "link_close") {
inLink = false; inLink = false;
if (emptyLink) { if (emptyLink) {
errors.addContext(child.lineNumber, "[" + linkText + "]()"); addErrorContext(onError, child.lineNumber,
"[" + linkText + "]()");
} }
} else if (inLink) { } else if (inLink) {
linkText += child.content; linkText += child.content;
@ -1108,7 +1152,7 @@ module.exports = [
"desc": "Required header structure", "desc": "Required header structure",
"tags": [ "headers" ], "tags": [ "headers" ],
"regexp": null, "regexp": null,
"func": function MD043(params, errors) { "func": function MD043(params, onError) {
var requiredHeaders = params.options.headers; var requiredHeaders = params.options.headers;
if (requiredHeaders) { if (requiredHeaders) {
var levels = {}; var levels = {};
@ -1117,8 +1161,9 @@ module.exports = [
}); });
var i = 0; var i = 0;
var optional = false; var optional = false;
var errorCount = 0;
forEachHeading(params, function forHeading(heading, content) { forEachHeading(params, function forHeading(heading, content) {
if (!errors.count()) { if (!errorCount) {
var actual = levels[heading.tag] + " " + content; var actual = levels[heading.tag] + " " + content;
var expected = requiredHeaders[i++] || "[None]"; var expected = requiredHeaders[i++] || "[None]";
if (expected === "*") { if (expected === "*") {
@ -1128,12 +1173,13 @@ module.exports = [
} else if (optional) { } else if (optional) {
i--; i--;
} else { } else {
errors.addDetailIf(heading.lineNumber, expected, actual); addErrorDetailIf(onError, heading.lineNumber, expected, actual);
errorCount++;
} }
} }
}); });
if ((i < requiredHeaders.length) && !errors.count()) { if ((i < requiredHeaders.length) && !errorCount) {
errors.addContext(params.lines.length, requiredHeaders[i]); addErrorContext(onError, params.lines.length, requiredHeaders[i]);
} }
} }
} }
@ -1144,7 +1190,7 @@ module.exports = [
"desc": "Proper names should have the correct capitalization", "desc": "Proper names should have the correct capitalization",
"tags": [ "spelling" ], "tags": [ "spelling" ],
"regexp": null, "regexp": null,
"func": function MD044(params, errors) { "func": function MD044(params, onError) {
var names = params.options.names || []; var names = params.options.names || [];
var codeBlocks = params.options.code_blocks; var codeBlocks = params.options.code_blocks;
var includeCodeBlocks = (codeBlocks === undefined) ? true : !!codeBlocks; var includeCodeBlocks = (codeBlocks === undefined) ? true : !!codeBlocks;
@ -1165,7 +1211,8 @@ module.exports = [
if (names.indexOf(wordMatch) === -1) { if (names.indexOf(wordMatch) === -1) {
var lineNumber = token.lineNumber + index + fenceOffset; var lineNumber = token.lineNumber + index + fenceOffset;
var range = [ match.index + 1, wordMatch.length ]; var range = [ match.index + 1, wordMatch.length ];
errors.addDetailIf(lineNumber, name, match[1], null, range); addErrorDetailIf(onError, lineNumber,
name, match[1], null, range);
} }
} }
} }
@ -1185,10 +1232,10 @@ module.exports = [
"desc": "Images should have alternate text (alt text)", "desc": "Images should have alternate text (alt text)",
"tags": [ "accessibility", "images" ], "tags": [ "accessibility", "images" ],
"regexp": null, "regexp": null,
"func": function MD045(params, errors) { "func": function MD045(params, onError) {
forEachInlineChild(params, "image", function forToken(token) { forEachInlineChild(params, "image", function forToken(token) {
if (token.content === "") { if (token.content === "") {
errors.add(token.lineNumber); addError(onError, token.lineNumber);
} }
}); });
} }