Merge fixInfo branch to introduce automatic fix ability for built-in and custom rules (fixes #80).

This commit is contained in:
David Anson 2019-09-28 17:16:08 -07:00
commit fa9e08cf53
63 changed files with 2247 additions and 266 deletions

View file

@ -416,15 +416,20 @@ Specifies which version of the `result` object to return (see the "Usage" sectio
below for examples).
Passing a `resultVersion` of `0` corresponds to the original, simple format where
each error is identified by rule name and line number. This is deprecated.
each error is identified by rule name and line number. *This is deprecated.*
Passing a `resultVersion` of `1` corresponds to a detailed format where each error
includes information about the line number, rule name, alias, description, as well
as any additional detail or context that is available. This is deprecated.
as any additional detail or context that is available. *This is deprecated.*
Passing a `resultVersion` of `2` corresponds to a detailed format where each error
includes information about the line number, rule names, description, as well as any
additional detail or context that is available. This is the default.
additional detail or context that is available. *This is the default.*
Passing a `resultVersion` of `3` corresponds to the detailed version `2` format
with additional information about how to fix automatically-fixable errors. In this
mode, all errors that occur on each line are reported (other versions report only
the first error for each rule).
##### options.markdownItPlugins

View file

@ -43,7 +43,8 @@ A rule is implemented as an `Object` with four required properties:
- `function` is a synchronous `Function` that implements the rule and is passed two parameters:
- `params` is an `Object` with properties that describe the content being analyzed:
- `name` is a `String` that identifies the input file/string.
- `tokens` is an `Array` of [`markdown-it` `Token` objects](https://markdown-it.github.io/markdown-it/#Token) with added `line` and `lineNumber` properties.
- `tokens` is an `Array` of [`markdown-it` `Token` objects](https://markdown-it.github.io/markdown-it/#Token)
with added `line` and `lineNumber` properties.
- `lines` is an `Array` of `String` values corresponding to the lines of the input file/string.
- `frontMatterLines` is an `Array` of `String` values corresponding to any front matter (not present in `lines`).
- `config` is an `Object` corresponding to the rule's entry in `options.config` (if present).
@ -52,6 +53,14 @@ A rule is implemented as an `Object` with four required properties:
- `details` is an optional `String` with information about what caused the error.
- `context` is an optional `String` with relevant text surrounding the error location.
- `range` is an optional `Array` with two `Number` values identifying the 1-based column and length of the error.
- `fixInfo` is an optional `Object` with information about how to fix the error (all properties are optional, but
at least one of `deleteCount` and `insertText` should be present; when applying a fix, the delete should be
performed before the insert):
- `lineNumber` is an optional `Number` specifying the 1-based line number of the edit.
- `editColumn` is an optional `Number` specifying the 1-based column number of the edit.
- `deleteCount` is an optional `Number` specifying the count of characters to delete.
- `insertText` is an optional `String` specifying the text to insert. `\n` is the platform-independent way to add
a line break; line breaks should be added at the beginning of a line instead of at the end).
The collection of helper functions shared by the built-in rules is available for use by custom rules in the [markdownlint-rule-helpers package](https://www.npmjs.com/package/markdownlint-rule-helpers).

View file

@ -2,9 +2,12 @@
"use strict";
const os = require("os");
// Regular expression for matching common newline characters
// See NEWLINES_RE in markdown-it/lib/rules_core/normalize.js
module.exports.newLineRe = /\r[\n\u0085]?|[\n\u2424\u2028\u0085]/;
const newLineRe = /\r\n?|\n/g;
module.exports.newLineRe = newLineRe;
// Regular expression for matching common front matter (YAML and TOML)
module.exports.frontMatterRe =
@ -18,8 +21,7 @@ const inlineCommentRe =
module.exports.inlineCommentRe = inlineCommentRe;
// Regular expressions for range matching
module.exports.atxHeadingSpaceRe = /^#+\s*\S/;
module.exports.bareUrlRe = /(?:http|ftp)s?:\/\/[^\s]*/i;
module.exports.bareUrlRe = /(?:http|ftp)s?:\/\/[^\s]*/ig;
module.exports.listItemMarkerRe = /^[\s>]*(?:[*+-]|\d+[.)])\s+/;
module.exports.orderedListItemMarkerRe = /^[\s>]*0*(\d+)[.)]/;
@ -44,6 +46,11 @@ module.exports.isEmptyString = function isEmptyString(str) {
return str.length === 0;
};
// Returns true iff the input is an object
module.exports.isObject = function isObject(obj) {
return (obj !== null) && (typeof obj === "object") && !Array.isArray(obj);
};
// Returns true iff the input line is blank (no content)
// Example: Contains nothing, whitespace, or comments
const blankLineRe = />|(?:<!--.*?-->)/g;
@ -312,7 +319,8 @@ module.exports.forEachInlineCodeSpan =
currentLine++;
currentColumn = 0;
} else if ((char === "\\") &&
((startIndex === -1) || (startColumn === -1))) {
((startIndex === -1) || (startColumn === -1)) &&
(input[index + 1] !== "\n")) {
// Escape character outside code, skip next
index++;
currentColumn += 2;
@ -331,19 +339,20 @@ module.exports.forEachInlineCodeSpan =
};
// Adds a generic error object via the onError callback
function addError(onError, lineNumber, detail, context, range) {
function addError(onError, lineNumber, detail, context, range, fixInfo) {
onError({
"lineNumber": lineNumber,
"detail": detail,
"context": context,
"range": range
lineNumber,
detail,
context,
range,
fixInfo
});
}
module.exports.addError = addError;
// Adds an error object with details conditionally via the onError callback
module.exports.addErrorDetailIf = function addErrorDetailIf(
onError, lineNumber, expected, actual, detail, context, range) {
onError, lineNumber, expected, actual, detail, context, range, fixInfo) {
if (expected !== actual) {
addError(
onError,
@ -351,24 +360,25 @@ module.exports.addErrorDetailIf = function addErrorDetailIf(
"Expected: " + expected + "; Actual: " + actual +
(detail ? "; " + detail : ""),
context,
range);
range,
fixInfo);
}
};
// Adds an error object with context via the onError callback
module.exports.addErrorContext =
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.addErrorContext = function addErrorContext(
onError, lineNumber, context, left, right, range, fixInfo) {
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, fixInfo);
};
// Returns a range object for a line by applying a RegExp
module.exports.rangeFromRegExp = function rangeFromRegExp(line, regexp) {
@ -396,3 +406,127 @@ module.exports.frontMatterHasTitle =
return !ignoreFrontMatter &&
frontMatterLines.some((line) => frontMatterTitleRe.test(line));
};
// Gets the most common line ending, falling back to platform default
function getPreferredLineEnding(input) {
let cr = 0;
let lf = 0;
let crlf = 0;
const endings = input.match(newLineRe) || [];
endings.forEach((ending) => {
// eslint-disable-next-line default-case
switch (ending) {
case "\r":
cr++;
break;
case "\n":
lf++;
break;
case "\r\n":
crlf++;
break;
}
});
let preferredLineEnding = null;
if (!cr && !lf && !crlf) {
preferredLineEnding = os.EOL;
} else if ((lf >= crlf) && (lf >= cr)) {
preferredLineEnding = "\n";
} else if (crlf >= cr) {
preferredLineEnding = "\r\n";
} else {
preferredLineEnding = "\r";
}
return preferredLineEnding;
}
module.exports.getPreferredLineEnding = getPreferredLineEnding;
// Normalizes the fields of a fixInfo object
function normalizeFixInfo(fixInfo, lineNumber) {
return {
"lineNumber": fixInfo.lineNumber || lineNumber,
"editColumn": fixInfo.editColumn || 1,
"deleteCount": fixInfo.deleteCount || 0,
"insertText": fixInfo.insertText || ""
};
}
// Fixes the specifide error on a line
function applyFix(line, fixInfo, lineEnding) {
const { editColumn, deleteCount, insertText } = normalizeFixInfo(fixInfo);
const editIndex = editColumn - 1;
return (deleteCount === -1) ?
null :
line.slice(0, editIndex) +
insertText.replace(/\n/g, lineEnding || "\n") +
line.slice(editIndex + deleteCount);
}
module.exports.applyFix = applyFix;
// Applies as many fixes as possible to the input lines
module.exports.applyFixes = function applyFixes(input, errors) {
const lineEnding = getPreferredLineEnding(input);
const lines = input.split(newLineRe);
// Normalize fixInfo objects
let fixInfos = errors
.filter((error) => error.fixInfo)
.map((error) => normalizeFixInfo(error.fixInfo, error.lineNumber));
// Sort bottom-to-top, line-deletes last, right-to-left, long-to-short
fixInfos.sort((a, b) => {
const aDeletingLine = (a.deleteCount === -1);
const bDeletingLine = (b.deleteCount === -1);
return (
(b.lineNumber - a.lineNumber) ||
(aDeletingLine ? 1 : (bDeletingLine ? -1 : 0)) ||
(b.editColumn - a.editColumn) ||
(b.insertText.length - a.insertText.length)
);
});
// Remove duplicate entries (needed for following collapse step)
let lastFixInfo = {};
fixInfos = fixInfos.filter((fixInfo) => {
const unique = (
(fixInfo.lineNumber !== lastFixInfo.lineNumber) ||
(fixInfo.editColumn !== lastFixInfo.editColumn) ||
(fixInfo.deleteCount !== lastFixInfo.deleteCount) ||
(fixInfo.insertText !== lastFixInfo.insertText)
);
lastFixInfo = fixInfo;
return unique;
});
// Collapse insert/no-delete and no-insert/delete for same line/column
lastFixInfo = {};
fixInfos.forEach((fixInfo) => {
if (
(fixInfo.lineNumber === lastFixInfo.lineNumber) &&
(fixInfo.editColumn === lastFixInfo.editColumn) &&
!fixInfo.insertText &&
(fixInfo.deleteCount > 0) &&
lastFixInfo.insertText &&
!lastFixInfo.deleteCount) {
fixInfo.insertText = lastFixInfo.insertText;
lastFixInfo.lineNumber = 0;
}
lastFixInfo = fixInfo;
});
fixInfos = fixInfos.filter((fixInfo) => fixInfo.lineNumber);
// Apply all (remaining/updated) fixes
let lastLineIndex = -1;
let lastEditIndex = -1;
fixInfos.forEach((fixInfo) => {
const { lineNumber, editColumn, deleteCount } = fixInfo;
const lineIndex = lineNumber - 1;
const editIndex = editColumn - 1;
if (
(lineIndex !== lastLineIndex) ||
((editIndex + deleteCount) < lastEditIndex) ||
(deleteCount === -1)
) {
lines[lineIndex] = applyFix(lines[lineIndex], fixInfo, lineEnding);
}
lastLineIndex = lineIndex;
lastEditIndex = editIndex;
});
// Return corrected input
return lines.filter((line) => line !== null).join(lineEnding);
};

View file

@ -172,11 +172,20 @@ function annotateTokens(tokens, lines) {
}
// Annotate children with lineNumber
let lineNumber = token.lineNumber;
const codeSpanExtraLines = [];
helpers.forEachInlineCodeSpan(
token.content,
function handleInlineCodeSpan(code) {
codeSpanExtraLines.push(code.split(helpers.newLineRe).length - 1);
}
);
(token.children || []).forEach(function forChild(child) {
child.lineNumber = lineNumber;
child.line = lines[lineNumber - 1];
if ((child.type === "softbreak") || (child.type === "hardbreak")) {
lineNumber++;
} else if (child.type === "code_inline") {
lineNumber += codeSpanExtraLines.shift();
}
});
}
@ -296,6 +305,11 @@ function lineNumberComparison(a, b) {
return a.lineNumber - b.lineNumber;
}
// Function to return true for all inputs
function filterAllValues() {
return true;
}
// Function to return unique values from a sorted errors array
function uniqueFilterForSortedErrors(value, index, array) {
return (index === 0) || (value.lineNumber > array[index - 1].lineNumber);
@ -377,11 +391,43 @@ function lintContent(
lines[errorInfo.lineNumber - 1].length))) {
throwError("range");
}
const fixInfo = errorInfo.fixInfo;
if (fixInfo) {
if (!helpers.isObject(fixInfo)) {
throwError("fixInfo");
}
if ((fixInfo.lineNumber !== undefined) &&
(!helpers.isNumber(fixInfo.lineNumber) ||
(fixInfo.lineNumber < 1) ||
(fixInfo.lineNumber > lines.length))) {
throwError("fixInfo.lineNumber");
}
const effectiveLineNumber = fixInfo.lineNumber || errorInfo.lineNumber;
if ((fixInfo.editColumn !== undefined) &&
(!helpers.isNumber(fixInfo.editColumn) ||
(fixInfo.editColumn < 1) ||
(fixInfo.editColumn >
lines[effectiveLineNumber - 1].length + 1))) {
throwError("fixInfo.editColumn");
}
if ((fixInfo.deleteCount !== undefined) &&
(!helpers.isNumber(fixInfo.deleteCount) ||
(fixInfo.deleteCount < -1) ||
(fixInfo.deleteCount >
lines[effectiveLineNumber - 1].length))) {
throwError("fixInfo.deleteCount");
}
if ((fixInfo.insertText !== undefined) &&
!helpers.isString(fixInfo.insertText)) {
throwError("fixInfo.insertText");
}
}
errors.push({
"lineNumber": errorInfo.lineNumber + frontMatterLines.length,
"detail": errorInfo.detail || null,
"context": errorInfo.context || null,
"range": errorInfo.range || null
"range": errorInfo.range || null,
"fixInfo": errorInfo.fixInfo || null
});
}
// Call (possibly external) rule function
@ -401,7 +447,9 @@ function lintContent(
if (errors.length) {
errors.sort(lineNumberComparison);
const filteredErrors = errors
.filter(uniqueFilterForSortedErrors)
.filter((resultVersion === 3) ?
filterAllValues :
uniqueFilterForSortedErrors)
.filter(function removeDisabledRules(error) {
return enabledRulesPerLineNumber[error.lineNumber][ruleName];
})
@ -423,6 +471,9 @@ function lintContent(
errorObject.errorDetail = error.detail;
errorObject.errorContext = error.context;
errorObject.errorRange = error.range;
if (resultVersion === 3) {
errorObject.fixInfo = error.fixInfo;
}
return errorObject;
});
if (filteredErrors.length) {

View file

@ -13,10 +13,19 @@ module.exports = {
"tags": [ "bullet", "ul", "indentation" ],
"function": function MD006(params, onError) {
flattenedLists().forEach((list) => {
if (list.unordered && !list.nesting) {
addErrorDetailIf(onError, list.open.lineNumber,
0, list.indent, null, null,
rangeFromRegExp(list.open.line, listItemMarkerRe));
if (list.unordered && !list.nesting && (list.indent !== 0)) {
const { lineNumber, line } = list.open;
addErrorDetailIf(
onError,
lineNumber,
0,
list.indent,
null,
null,
rangeFromRegExp(line, listItemMarkerRe),
{
"deleteCount": line.length - line.trimLeft().length
});
}
});
}

View file

@ -2,12 +2,10 @@
"use strict";
const { addError, filterTokens, forEachLine, includesSorted, rangeFromRegExp } =
const { addError, filterTokens, forEachLine, includesSorted } =
require("../helpers");
const { lineMetadata } = require("./cache");
const trailingSpaceRe = /\s+$/;
module.exports = {
"names": [ "MD009", "no-trailing-spaces" ],
"description": "Trailing spaces",
@ -34,14 +32,22 @@ module.exports = {
forEachLine(lineMetadata(), (line, lineIndex, inCode, onFence) => {
inFencedCode += onFence;
const lineNumber = lineIndex + 1;
if ((!inCode || inFencedCode) && trailingSpaceRe.test(line) &&
const trailingSpaces = line.length - line.trimRight().length;
if ((!inCode || inFencedCode) && trailingSpaces &&
!includesSorted(listItemLineNumbers, lineNumber)) {
const actual = line.length - line.trimRight().length;
if (expected !== actual) {
addError(onError, lineNumber,
if (expected !== trailingSpaces) {
const column = line.length - trailingSpaces + 1;
addError(
onError,
lineNumber,
"Expected: " + (expected === 0 ? "" : "0 or ") +
expected + "; Actual: " + actual,
null, rangeFromRegExp(line, trailingSpaceRe));
expected + "; Actual: " + trailingSpaces,
null,
[ column, trailingSpaces ],
{
"editColumn": column,
"deleteCount": trailingSpaces
});
}
}
});

View file

@ -2,10 +2,10 @@
"use strict";
const { addError, forEachLine, rangeFromRegExp } = require("../helpers");
const { addError, forEachLine } = require("../helpers");
const { lineMetadata } = require("./cache");
const tabRe = /\t+/;
const tabRe = /\t+/g;
module.exports = {
"names": [ "MD010", "no-hard-tabs" ],
@ -15,9 +15,23 @@ module.exports = {
const codeBlocks = params.config.code_blocks;
const includeCodeBlocks = (codeBlocks === undefined) ? true : !!codeBlocks;
forEachLine(lineMetadata(), (line, lineIndex, inCode) => {
if (tabRe.test(line) && (!inCode || includeCodeBlocks)) {
addError(onError, lineIndex + 1, "Column: " + (line.indexOf("\t") + 1),
null, rangeFromRegExp(line, tabRe));
if (!inCode || includeCodeBlocks) {
let match = null;
while ((match = tabRe.exec(line)) !== null) {
const column = match.index + 1;
const length = match[0].length;
addError(
onError,
lineIndex + 1,
"Column: " + column,
null,
[ column, length ],
{
"editColumn": column,
"deleteCount": length,
"insertText": "".padEnd(length)
});
}
}
});
}

View file

@ -2,20 +2,36 @@
"use strict";
const { addError, forEachInlineChild, rangeFromRegExp } = require("../helpers");
const { addError, forEachInlineChild, unescapeMarkdown } =
require("../helpers");
const reversedLinkRe = /\([^)]+\)\[[^\]^][^\]]*]/;
const reversedLinkRe = /\(([^)]+)\)\[([^\]^][^\]]*)]/g;
module.exports = {
"names": [ "MD011", "no-reversed-links" ],
"description": "Reversed link syntax",
"tags": [ "links" ],
"function": function MD011(params, onError) {
forEachInlineChild(params, "text", function forToken(token) {
const match = reversedLinkRe.exec(token.content);
if (match) {
addError(onError, token.lineNumber, match[0], null,
rangeFromRegExp(token.line, reversedLinkRe));
forEachInlineChild(params, "text", (token) => {
const { lineNumber, content } = token;
let match = null;
while ((match = reversedLinkRe.exec(content)) !== null) {
const [ reversedLink, linkText, linkDestination ] = match;
const line = params.lines[lineNumber - 1];
const column = unescapeMarkdown(line).indexOf(reversedLink) + 1;
const length = reversedLink.length;
addError(
onError,
lineNumber,
reversedLink,
null,
[ column, length ],
{
"editColumn": column,
"deleteCount": length,
"insertText": `[${linkText}](${linkDestination})`
}
);
}
});
}

View file

@ -15,7 +15,17 @@ module.exports = {
forEachLine(lineMetadata(), (line, lineIndex, inCode) => {
count = (inCode || line.trim().length) ? 0 : count + 1;
if (maximum < count) {
addErrorDetailIf(onError, lineIndex + 1, maximum, count);
addErrorDetailIf(
onError,
lineIndex + 1,
maximum,
count,
null,
null,
null,
{
"deleteCount": -1
});
}
});
}

View file

@ -2,8 +2,7 @@
"use strict";
const { addErrorContext, atxHeadingSpaceRe, forEachLine,
rangeFromRegExp } = require("../helpers");
const { addErrorContext, forEachLine } = require("../helpers");
const { lineMetadata } = require("./cache");
module.exports = {
@ -12,9 +11,20 @@ module.exports = {
"tags": [ "headings", "headers", "atx", "spaces" ],
"function": function MD018(params, onError) {
forEachLine(lineMetadata(), (line, lineIndex, inCode) => {
if (!inCode && /^#+[^#\s]/.test(line) && !/#$/.test(line)) {
addErrorContext(onError, lineIndex + 1, line.trim(), null,
null, rangeFromRegExp(line, atxHeadingSpaceRe));
if (!inCode && /^#+[^#\s]/.test(line) && !/#\s*$/.test(line)) {
const hashCount = /^#+/.exec(line)[0].length;
addErrorContext(
onError,
lineIndex + 1,
line.trim(),
null,
null,
[ 1, hashCount + 1 ],
{
"editColumn": hashCount + 1,
"insertText": " "
}
);
}
});
}

View file

@ -2,20 +2,37 @@
"use strict";
const { addErrorContext, atxHeadingSpaceRe, filterTokens, headingStyleFor,
rangeFromRegExp } = require("../helpers");
const { addErrorContext, filterTokens, headingStyleFor } =
require("../helpers");
module.exports = {
"names": [ "MD019", "no-multiple-space-atx" ],
"description": "Multiple spaces after hash on atx style heading",
"tags": [ "headings", "headers", "atx", "spaces" ],
"function": function MD019(params, onError) {
filterTokens(params, "heading_open", function forToken(token) {
if ((headingStyleFor(token) === "atx") &&
/^#+\s\s/.test(token.line)) {
addErrorContext(onError, token.lineNumber, token.line.trim(),
null, null,
rangeFromRegExp(token.line, atxHeadingSpaceRe));
filterTokens(params, "heading_open", (token) => {
if (headingStyleFor(token) === "atx") {
const { line, lineNumber } = token;
const match = /^(#+)(\s{2,})(?:\S)/.exec(line);
if (match) {
const [
,
{ "length": hashLength },
{ "length": spacesLength }
] = match;
addErrorContext(
onError,
lineNumber,
line.trim(),
null,
null,
[ 1, hashLength + spacesLength + 1 ],
{
"editColumn": hashLength + 1,
"deleteCount": spacesLength - 1
}
);
}
}
});
}

View file

@ -2,23 +2,59 @@
"use strict";
const { addErrorContext, forEachLine, rangeFromRegExp } = require("../helpers");
const { addErrorContext, forEachLine } = require("../helpers");
const { lineMetadata } = require("./cache");
const atxClosedHeadingNoSpaceRe = /(?:^#+[^#\s])|(?:[^#\s]#+\s*$)/;
module.exports = {
"names": [ "MD020", "no-missing-space-closed-atx" ],
"description": "No space inside hashes on closed atx style heading",
"tags": [ "headings", "headers", "atx_closed", "spaces" ],
"function": function MD020(params, onError) {
forEachLine(lineMetadata(), (line, lineIndex, inCode) => {
if (!inCode && /^#+[^#]*[^\\]#+$/.test(line)) {
const left = /^#+[^#\s]/.test(line);
const right = /[^#\s]#+$/.test(line);
if (left || right) {
addErrorContext(onError, lineIndex + 1, line.trim(), left,
right, rangeFromRegExp(line, atxClosedHeadingNoSpaceRe));
if (!inCode) {
const match =
/^(#+)(\s*)([^#]+?[^#\\])(\s*)((?:\\#)?)(#+)(\s*)$/.exec(line);
if (match) {
const [
,
leftHash,
{ "length": leftSpaceLength },
content,
{ "length": rightSpaceLength },
rightEscape,
rightHash,
{ "length": trailSpaceLength }
] = match;
const leftHashLength = leftHash.length;
const rightHashLength = rightHash.length;
const left = !leftSpaceLength;
const right = !rightSpaceLength || rightEscape;
const rightEscapeReplacement = rightEscape ? `${rightEscape} ` : "";
if (left || right) {
const range = left ?
[
1,
leftHashLength + 1
] :
[
line.length - trailSpaceLength - rightHashLength,
rightHashLength + 1
];
addErrorContext(
onError,
lineIndex + 1,
line.trim(),
left,
right,
range,
{
"editColumn": 1,
"deleteCount": line.length,
"insertText":
`${leftHash} ${content} ${rightEscapeReplacement}${rightHash}`
}
);
}
}
}
});

View file

@ -2,24 +2,57 @@
"use strict";
const { addErrorContext, filterTokens, headingStyleFor, rangeFromRegExp } =
const { addErrorContext, filterTokens, headingStyleFor } =
require("../helpers");
const atxClosedHeadingSpaceRe = /(?:^#+\s\s+?\S)|(?:\S\s\s+?#+\s*$)/;
module.exports = {
"names": [ "MD021", "no-multiple-space-closed-atx" ],
"description": "Multiple spaces inside hashes on closed atx style heading",
"tags": [ "headings", "headers", "atx_closed", "spaces" ],
"function": function MD021(params, onError) {
filterTokens(params, "heading_open", function forToken(token) {
filterTokens(params, "heading_open", (token) => {
if (headingStyleFor(token) === "atx_closed") {
const left = /^#+\s\s/.test(token.line);
const right = /\s\s#+$/.test(token.line);
if (left || right) {
addErrorContext(onError, token.lineNumber, token.line.trim(),
left, right,
rangeFromRegExp(token.line, atxClosedHeadingSpaceRe));
const { line, lineNumber } = token;
const match = /^(#+)(\s+)([^#]+?)(\s+)(#+)(\s*)$/.exec(line);
if (match) {
const [
,
leftHash,
{ "length": leftSpaceLength },
content,
{ "length": rightSpaceLength },
rightHash,
{ "length": trailSpaceLength }
] = match;
const left = leftSpaceLength > 1;
const right = rightSpaceLength > 1;
if (left || right) {
const length = line.length;
const leftHashLength = leftHash.length;
const rightHashLength = rightHash.length;
const range = left ?
[
1,
leftHashLength + leftSpaceLength + 1
] :
[
length - trailSpaceLength - rightHashLength - rightSpaceLength,
rightSpaceLength + rightHashLength + 1
];
addErrorContext(
onError,
lineNumber,
line.trim(),
left,
right,
range,
{
"editColumn": 1,
"deleteCount": length,
"insertText": `${leftHash} ${content} ${rightHash}`
}
);
}
}
}
});

View file

@ -20,20 +20,41 @@ module.exports = {
const { lines } = params;
filterTokens(params, "heading_open", (token) => {
const [ topIndex, nextIndex ] = token.map;
let actualAbove = 0;
for (let i = 0; i < linesAbove; i++) {
if (!isBlankLine(lines[topIndex - i - 1])) {
addErrorDetailIf(onError, topIndex + 1, linesAbove, i, "Above",
lines[topIndex].trim());
return;
if (isBlankLine(lines[topIndex - i - 1])) {
actualAbove++;
}
}
addErrorDetailIf(
onError,
topIndex + 1,
linesAbove,
actualAbove,
"Above",
lines[topIndex].trim(),
null,
{
"insertText": "".padEnd(linesAbove - actualAbove, "\n")
});
let actualBelow = 0;
for (let i = 0; i < linesBelow; i++) {
if (!isBlankLine(lines[nextIndex + i])) {
addErrorDetailIf(onError, topIndex + 1, linesBelow, i, "Below",
lines[topIndex].trim());
return;
if (isBlankLine(lines[nextIndex + i])) {
actualBelow++;
}
}
addErrorDetailIf(
onError,
topIndex + 1,
linesBelow,
actualBelow,
"Below",
lines[topIndex].trim(),
null,
{
"lineNumber": nextIndex + 1,
"insertText": "".padEnd(linesBelow - actualBelow, "\n")
});
});
}
};

View file

@ -2,8 +2,7 @@
"use strict";
const { addErrorContext, filterTokens, rangeFromRegExp } =
require("../helpers");
const { addErrorContext, filterTokens } = require("../helpers");
const spaceBeforeHeadingRe = /^((?:\s+)|(?:[>\s]+\s\s))[^>\s]/;
@ -13,9 +12,26 @@ module.exports = {
"tags": [ "headings", "headers", "spaces" ],
"function": function MD023(params, onError) {
filterTokens(params, "heading_open", function forToken(token) {
if (spaceBeforeHeadingRe.test(token.line)) {
addErrorContext(onError, token.lineNumber, token.line, null,
null, rangeFromRegExp(token.line, spaceBeforeHeadingRe));
const { lineNumber, line } = token;
const match = line.match(spaceBeforeHeadingRe);
if (match) {
const [ prefixAndFirstChar, prefix ] = match;
let deleteCount = prefix.length;
const prefixLengthNoSpace = prefix.trimRight().length;
if (prefixLengthNoSpace) {
deleteCount -= prefixLengthNoSpace - 1;
}
addErrorContext(
onError,
lineNumber,
line,
null,
null,
[ 1, prefixAndFirstChar.length ],
{
"editColumn": prefixLengthNoSpace + 1,
"deleteCount": deleteCount
});
}
});
}

View file

@ -2,8 +2,8 @@
"use strict";
const { addError, allPunctuation, escapeForRegExp, forEachHeading,
rangeFromRegExp } = require("../helpers");
const { addError, allPunctuation, escapeForRegExp, forEachHeading } =
require("../helpers");
module.exports = {
"names": [ "MD026", "no-trailing-punctuation" ],
@ -15,13 +15,26 @@ module.exports = {
punctuation = allPunctuation;
}
const trailingPunctuationRe =
new RegExp("[" + escapeForRegExp(punctuation) + "]$");
forEachHeading(params, (heading, content) => {
const match = trailingPunctuationRe.exec(content);
new RegExp("\\s*[" + escapeForRegExp(punctuation) + "]+$");
forEachHeading(params, (heading) => {
const { line, lineNumber } = heading;
const trimmedLine = line.replace(/[\s#]*$/, "");
const match = trailingPunctuationRe.exec(trimmedLine);
if (match) {
addError(onError, heading.lineNumber,
"Punctuation: '" + match[0] + "'", null,
rangeFromRegExp(heading.line, trailingPunctuationRe));
const fullMatch = match[0];
const column = match.index + 1;
const length = fullMatch.length;
addError(
onError,
lineNumber,
`Punctuation: '${fullMatch}'`,
null,
[ column, length ],
{
"editColumn": column,
"deleteCount": length
}
);
}
});
}

View file

@ -2,9 +2,9 @@
"use strict";
const { addErrorContext, newLineRe, rangeFromRegExp } = require("../helpers");
const { addErrorContext, newLineRe } = require("../helpers");
const spaceAfterBlockQuote = /^\s*(?:>\s+)+\S/;
const spaceAfterBlockQuoteRe = /^((?:\s*>)+)(\s{2,})\S/;
module.exports = {
"names": [ "MD027", "no-multiple-space-blockquote" ],
@ -13,31 +13,43 @@ module.exports = {
"function": function MD027(params, onError) {
let blockquoteNesting = 0;
let listItemNesting = 0;
params.tokens.forEach(function forToken(token) {
if (token.type === "blockquote_open") {
params.tokens.forEach((token) => {
const { content, lineNumber, type } = token;
if (type === "blockquote_open") {
blockquoteNesting++;
} else if (token.type === "blockquote_close") {
} else if (type === "blockquote_close") {
blockquoteNesting--;
} else if (token.type === "list_item_open") {
} else if (type === "list_item_open") {
listItemNesting++;
} else if (token.type === "list_item_close") {
} else if (type === "list_item_close") {
listItemNesting--;
} else if ((token.type === "inline") && (blockquoteNesting > 0)) {
const multipleSpaces = listItemNesting ?
/^(\s*>)+\s\s+>/.test(token.line) :
/^(\s*>)+\s\s/.test(token.line);
if (multipleSpaces) {
addErrorContext(onError, token.lineNumber, token.line, null,
null, rangeFromRegExp(token.line, spaceAfterBlockQuote));
}
token.content.split(newLineRe)
.forEach(function forLine(line, offset) {
if (/^\s/.test(line)) {
addErrorContext(onError, token.lineNumber + offset,
"> " + line, null, null,
rangeFromRegExp(line, spaceAfterBlockQuote));
} else if ((type === "inline") && blockquoteNesting) {
const lineCount = content.split(newLineRe).length;
for (let i = 0; i < lineCount; i++) {
const line = params.lines[lineNumber + i - 1];
const match = line.match(spaceAfterBlockQuoteRe);
if (match) {
const [
fullMatch,
{ "length": blockquoteLength },
{ "length": spaceLength }
] = match;
if (!listItemNesting || (fullMatch[fullMatch.length - 1] === ">")) {
addErrorContext(
onError,
lineNumber + i,
line,
null,
null,
[ 1, fullMatch.length ],
{
"editColumn": blockquoteLength + 1,
"deleteCount": spaceLength - 1
}
);
}
});
}
}
}
});
}

View file

@ -10,12 +10,29 @@ module.exports = {
"tags": [ "blockquote", "whitespace" ],
"function": function MD028(params, onError) {
let prevToken = {};
let prevLineNumber = null;
params.tokens.forEach(function forToken(token) {
if ((token.type === "blockquote_open") &&
(prevToken.type === "blockquote_close")) {
addError(onError, token.lineNumber - 1);
for (
let lineNumber = prevLineNumber;
lineNumber < token.lineNumber;
lineNumber++) {
addError(
onError,
lineNumber,
null,
null,
null,
{
"deleteCount": -1
});
}
}
prevToken = token;
if (token.type === "blockquote_open") {
prevLineNumber = token.map[1] + 1;
}
});
}
};

View file

@ -2,8 +2,7 @@
"use strict";
const { addErrorDetailIf, listItemMarkerRe, rangeFromRegExp } =
require("../helpers");
const { addErrorDetailIf } = require("../helpers");
const { flattenedLists } = require("./cache");
module.exports = {
@ -22,10 +21,27 @@ module.exports = {
(allSingle ? ulSingle : ulMulti) :
(allSingle ? olSingle : olMulti);
list.items.forEach((item) => {
const match = /^[\s>]*\S+(\s+)/.exec(item.line);
addErrorDetailIf(onError, item.lineNumber,
expectedSpaces, (match ? match[1].length : 0), null, null,
rangeFromRegExp(item.line, listItemMarkerRe));
const { line, lineNumber } = item;
const match = /^[\s>]*\S+(\s*)/.exec(line);
const [ { "length": matchLength }, { "length": actualSpaces } ] = match;
let fixInfo = null;
if ((expectedSpaces !== actualSpaces) && (line.length > matchLength)) {
fixInfo = {
"editColumn": matchLength - actualSpaces + 1,
"deleteCount": actualSpaces,
"insertText": "".padEnd(expectedSpaces)
};
}
addErrorDetailIf(
onError,
lineNumber,
expectedSpaces,
actualSpaces,
null,
null,
[ 1, matchLength ],
fixInfo
);
});
});
}

View file

@ -14,10 +14,22 @@ module.exports = {
const includeListItems = (listItems === undefined) ? true : !!listItems;
const { lines } = params;
forEachLine(lineMetadata(), (line, i, inCode, onFence, inTable, inItem) => {
if ((((onFence > 0) && !isBlankLine(lines[i - 1])) ||
((onFence < 0) && !isBlankLine(lines[i + 1]))) &&
(includeListItems || !inItem)) {
addErrorContext(onError, i + 1, lines[i].trim());
const onTopFence = (onFence > 0);
const onBottomFence = (onFence < 0);
if ((includeListItems || !inItem) &&
((onTopFence && !isBlankLine(lines[i - 1])) ||
(onBottomFence && !isBlankLine(lines[i + 1])))) {
addErrorContext(
onError,
i + 1,
lines[i].trim(),
null,
null,
null,
{
"lineNumber": i + (onTopFence ? 1 : 2),
"insertText": "\n"
});
}
});
}

View file

@ -5,6 +5,8 @@
const { addErrorContext, isBlankLine } = require("../helpers");
const { flattenedLists } = require("./cache");
const quotePrefixRe = /^[>\s]*/;
module.exports = {
"names": [ "MD032", "blanks-around-lists" ],
"description": "Lists should be surrounded by blank lines",
@ -14,11 +16,34 @@ module.exports = {
flattenedLists().filter((list) => !list.nesting).forEach((list) => {
const firstIndex = list.open.map[0];
if (!isBlankLine(lines[firstIndex - 1])) {
addErrorContext(onError, firstIndex + 1, lines[firstIndex].trim());
const line = lines[firstIndex];
const quotePrefix = line.match(quotePrefixRe)[0].trimRight();
addErrorContext(
onError,
firstIndex + 1,
line.trim(),
null,
null,
null,
{
"insertText": `${quotePrefix}\n`
});
}
const lastIndex = list.lastLineIndex - 1;
if (!isBlankLine(lines[lastIndex + 1])) {
addErrorContext(onError, lastIndex + 1, lines[lastIndex].trim());
const line = lines[lastIndex];
const quotePrefix = line.match(quotePrefixRe)[0].trimRight();
addErrorContext(
onError,
lastIndex + 1,
line.trim(),
null,
null,
null,
{
"lineNumber": lastIndex + 2,
"insertText": `${quotePrefix}\n`
});
}
});
}

View file

@ -18,15 +18,29 @@ module.exports = {
inLink = true;
} else if (type === "link_close") {
inLink = false;
} else if ((type === "text") && !inLink &&
(match = bareUrlRe.exec(content))) {
const [ bareUrl ] = match;
const index = line.indexOf(content);
const range = (index === -1) ? null : [
line.indexOf(content) + match.index + 1,
bareUrl.length
];
addErrorContext(onError, lineNumber, bareUrl, null, null, range);
} else if ((type === "text") && !inLink) {
while ((match = bareUrlRe.exec(content)) !== null) {
const [ bareUrl ] = match;
const index = line.indexOf(content);
const range = (index === -1) ? null : [
line.indexOf(content) + match.index + 1,
bareUrl.length
];
const fixInfo = range ? {
"editColumn": range[0],
"deleteCount": range[1],
"insertText": `<${bareUrl}>`
} : null;
addErrorContext(
onError,
lineNumber,
bareUrl,
null,
null,
range,
fixInfo
);
}
}
});
});

View file

@ -4,29 +4,49 @@
const { addErrorContext, forEachInlineChild } = require("../helpers");
const leftSpaceRe = /(?:^|\s)(\*\*?|__?)\s.*[^\\]\1/g;
const rightSpaceRe = /(?:^|[^\\])(\*\*?|__?).+\s\1(?:\s|$)/g;
module.exports = {
"names": [ "MD037", "no-space-in-emphasis" ],
"description": "Spaces inside emphasis markers",
"tags": [ "whitespace", "emphasis" ],
"function": function MD037(params, onError) {
forEachInlineChild(params, "text", (token) => {
let left = true;
let match = /(?:^|\s)(\*\*?|__?)\s.*[^\\]\1/.exec(token.content);
if (!match) {
left = false;
match = /(?:^|[^\\])(\*\*?|__?).+\s\1(?:\s|$)/.exec(token.content);
}
if (match) {
const fullText = match[0];
const line = params.lines[token.lineNumber - 1];
if (line.includes(fullText)) {
const text = fullText.trim();
const column = line.indexOf(text) + 1;
const length = text.length;
addErrorContext(onError, token.lineNumber,
text, left, !left, [ column, length ]);
const { content, lineNumber } = token;
const columnsReported = [];
[ leftSpaceRe, rightSpaceRe ].forEach((spaceRe, index) => {
let match = null;
while ((match = spaceRe.exec(content)) !== null) {
const [ fullText, marker ] = match;
const line = params.lines[lineNumber - 1];
if (line.includes(fullText)) {
const text = fullText.trim();
const column = line.indexOf(text) + 1;
if (!columnsReported.includes(column)) {
const length = text.length;
const markerLength = marker.length;
const emphasized =
text.slice(markerLength, length - markerLength);
const fixedText = `${marker}${emphasized.trim()}${marker}`;
addErrorContext(
onError,
lineNumber,
text,
index === 0,
index !== 0,
[ column, length ],
{
"editColumn": column,
"deleteCount": length,
"insertText": fixedText
}
);
columnsReported.push(column);
}
}
}
}
});
});
}
};

View file

@ -5,8 +5,8 @@
const { addErrorContext, filterTokens, forEachInlineCodeSpan, newLineRe } =
require("../helpers");
const startRe = /^\s([^`]|$)/;
const endRe = /[^`]\s$/;
const leftSpaceRe = /^\s([^`]|$)/;
const rightSpaceRe = /[^`]\s$/;
module.exports = {
"names": [ "MD038", "no-space-in-code" ],
@ -22,22 +22,42 @@ module.exports = {
let rangeIndex = columnIndex - tickCount;
let rangeLength = code.length + (2 * tickCount);
let rangeLineOffset = 0;
let fixIndex = columnIndex;
let fixLength = code.length;
const codeLines = code.split(newLineRe);
const left = startRe.test(code);
const right = !left && endRe.test(code);
const left = leftSpaceRe.test(code);
const right = !left && rightSpaceRe.test(code);
if (right && (codeLines.length > 1)) {
rangeIndex = 0;
rangeLineOffset = codeLines.length - 1;
fixIndex = 0;
}
if (left || right) {
const codeLinesRange = codeLines[rangeLineOffset];
if (codeLines.length > 1) {
rangeLength = codeLines[rangeLineOffset].length + tickCount;
rangeLength = codeLinesRange.length + tickCount;
fixLength = codeLinesRange.length;
}
const context = tokenLines[lineIndex + rangeLineOffset]
.substring(rangeIndex, rangeIndex + rangeLength);
const codeLinesRangeTrim = codeLinesRange.trim();
const fixText =
(codeLinesRangeTrim.startsWith("`") ? " " : "") +
codeLinesRangeTrim +
(codeLinesRangeTrim.endsWith("`") ? " " : "");
addErrorContext(
onError, token.lineNumber + lineIndex + rangeLineOffset,
context, left, right, [ rangeIndex + 1, rangeLength ]);
onError,
token.lineNumber + lineIndex + rangeLineOffset,
context,
left,
right,
[ rangeIndex + 1, rangeLength ],
{
"editColumn": fixIndex + 1,
"deleteCount": fixLength,
"insertText": fixText
}
);
}
});
}

View file

@ -2,8 +2,7 @@
"use strict";
const { addErrorContext, filterTokens, rangeFromRegExp } =
require("../helpers");
const { addErrorContext, filterTokens } = require("../helpers");
const spaceInLinkRe = /\[(?:\s+(?:[^\]]*?)\s*|(?:[^\]]*?)\s+)](?=\(\S*\))/;
@ -12,10 +11,13 @@ module.exports = {
"description": "Spaces inside link text",
"tags": [ "whitespace", "links" ],
"function": function MD039(params, onError) {
filterTokens(params, "inline", function forToken(token) {
filterTokens(params, "inline", (token) => {
const { children } = token;
let { lineNumber } = token;
let inLink = false;
let linkText = "";
token.children.forEach(function forChild(child) {
let lineIndex = 0;
children.forEach((child) => {
if (child.type === "link_open") {
inLink = true;
linkText = "";
@ -24,10 +26,28 @@ module.exports = {
const left = linkText.trimLeft().length !== linkText.length;
const right = linkText.trimRight().length !== linkText.length;
if (left || right) {
addErrorContext(onError, token.lineNumber,
"[" + linkText + "]", left, right,
rangeFromRegExp(token.line, spaceInLinkRe));
const line = params.lines[lineNumber - 1];
const match = line.slice(lineIndex).match(spaceInLinkRe);
const column = match.index + lineIndex + 1;
const length = match[0].length;
lineIndex = column + length - 1;
addErrorContext(
onError,
lineNumber,
`[${linkText}]`,
left,
right,
[ column, length ],
{
"editColumn": column + 1,
"deleteCount": length - 2,
"insertText": linkText.trim()
}
);
}
} else if (child.type === "softbreak") {
lineNumber++;
lineIndex = 0;
} else if (inLink) {
linkText += child.content;
}

View file

@ -29,9 +29,30 @@ module.exports = {
.replace(/^\W*/, "").replace(/\W*$/, "");
if (!names.includes(wordMatch)) {
const lineNumber = token.lineNumber + index + fenceOffset;
const range = [ match.index + 1, wordMatch.length ];
addErrorDetailIf(onError, lineNumber,
name, match[1], null, null, range);
const fullLine = params.lines[lineNumber - 1];
let matchIndex = match.index;
const matchLength = wordMatch.length;
const fullLineWord =
fullLine.slice(matchIndex, matchIndex + matchLength);
if (fullLineWord !== wordMatch) {
// Attempt to fix bad offset due to inline content
matchIndex = fullLine.indexOf(wordMatch);
}
const range = [ matchIndex + 1, matchLength ];
addErrorDetailIf(
onError,
lineNumber,
name,
match[1],
null,
null,
range,
{
"editColumn": matchIndex + 1,
"deleteCount": matchLength,
"insertText": name
}
);
}
}
}

View file

@ -12,7 +12,17 @@ module.exports = {
const lastLineNumber = params.lines.length;
const lastLine = params.lines[lastLineNumber - 1];
if (!isBlankLine(lastLine)) {
addError(onError, lastLineNumber);
addError(
onError,
lastLineNumber,
null,
null,
[ lastLine.length, 1 ],
{
"insertText": "\n",
"editColumn": lastLine.length + 1
}
);
}
}
};

View file

@ -0,0 +1,19 @@
# atx-heading-spacing-trailing-spaces
<!-- markdownlint-disable heading-style -->
##Heading 1 {MD018}
## Heading 2 {MD019}
##Heading 3 {MD020} ##
## Heading 4 {MD020}##
##Heading 5 {MD020}##
## Heading 5 {MD021} ##
## Heading 6 {MD021} ##
## Heading 7 {MD021} ##

View file

@ -26,6 +26,6 @@ Some text
Expected errors:
{MD028:5} {MD028:8} {MD028:10} {MD028:17}
{MD028:5} {MD028:7} {MD028:8} {MD028:10} {MD028:17}
{MD009:10} (trailing space is intentional)
{MD012:8} (multiple blank lines are intentional)

View file

@ -27,17 +27,17 @@ long line long line long line long line long line long line long line long line
# Heading 5 {MD019}
#Heading 6 {MD020} #
# Heading 7 {MD021} {MD022} {MD023} {MD003} #
# Heading 7 {MD021} {MD003} #
# Heading 8
# Heading 8
{MD024:34}
{MD024:35}
Note: Can not break MD025 and MD002 in the same file
# Heading 9 {MD026}.
# Heading 9 {MD023} {MD026}.
> {MD027}
@ -78,4 +78,7 @@ code fence without language {MD040:73} {MD046:73}
markdownLint {MD044}
![](image.jpg) {MD045} {MD047}
![](image.jpg) {MD045}
## Heading 10 {MD022}
EOF {MD047}

View file

@ -0,0 +1,30 @@
## One
#### Two
### Three ###
* Alpha
* Bravo
- Charlie
* Delta
* Echo
Text
Text text
1. One
2. Two
3. Three
4. Four
5. Five
6. Six
7. Seven
8. Eight
9. Nine
10. Ten
11. Eleven
12. Twelve

View file

@ -22,3 +22,5 @@ A (reversed)[link] example.
## Multiple spaces E ##
## Multiple spaces F ##
*Another* (reversed)[link] example.

View file

@ -0,0 +1,25 @@
# Top level heading
<!-- markdownlint-disable MD003 -->
A [reversed](link) example.
123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789
## 123456789 123456789 123456789 123456789 123456789 123456789
$ command with no output
## No space A
## Multiple spaces B
## No space C ##
## No space D ##
## Multiple spaces E ##
## Multiple spaces F ##
*Another* [reversed](link) example.

View file

@ -8,6 +8,15 @@
"errorContext": null,
"errorRange": [3, 16]
},
{
"lineNumber": 26,
"ruleNames": [ "MD011", "no-reversed-links" ],
"ruleDescription": "Reversed link syntax",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md011",
"errorDetail": "(reversed)[link]",
"errorContext": null,
"errorRange": [11, 16]
},
{
"lineNumber": 7,
"ruleNames": [ "MD012", "no-multiple-blanks" ],

View file

@ -0,0 +1,19 @@
# Heading
Text
# Heading
## Another heading
> Multiple spaces
> Blank line above
1. Alpha
3. Beta
> > Multiple spaces, multiple blockquotes
> >
> > > Multiple spaces, multiple blockquotes
> > >
> > > Multiple spaces, multiple blockquotes

View file

@ -78,7 +78,7 @@
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md027",
"errorDetail": null,
"errorContext": "> > > Multiple spaces, multip...",
"errorRange": [ 1, 8 ]
"errorRange": [ 1, 4 ]
},
{
"lineNumber": 9,

View file

@ -0,0 +1,13 @@
#
-
1.
- a
1. a
- a
1. a

View file

@ -6,7 +6,7 @@
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md030",
"errorDetail": "Expected: 1; Actual: 0",
"errorContext": null,
"errorRange": null
"errorRange": [1, 1]
},
{
"lineNumber": 5,
@ -15,7 +15,7 @@
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md030",
"errorDetail": "Expected: 1; Actual: 0",
"errorContext": null,
"errorRange": null
"errorRange": [1, 2]
},
{
"lineNumber": 11,

View file

@ -56,3 +56,5 @@ text text ```` code
span code
span```` text
text.
Text [ space](link) text [space ](link) text [ space ](link) text.

View file

@ -0,0 +1,62 @@
```js
debugger;
```
* List
Inline<hr/>HTML
Bare <https://example.com> link
---
***
*Emphasis*
Space *inside* emphasis
Space `inside` code span
Space [inside](link) text
```
```
space ``inside`` code
space `inside` of `code` elements
`space` inside `of` code `elements`
space ``inside`` of ``code`` elements
`` ` embedded backtick``
``embedded backtick` ``
some *space* in *some* emphasis
some *space* in *some* emphasis
some *space* in **some** emphasis
some _space_ in _some_ emphasis
some __space__ in __some__ emphasis
Text
text `code
span` text
text.
Text
text `code
span` text
text.
* List
---
Text
text ```code
span code
span code``` text
text
text text ````code
span code
span```` text
text.
Text [space](link) text [space](link) text [space](link) text.

View file

@ -33,7 +33,7 @@
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md033",
"errorDetail": "Element: hr",
"errorContext": null,
"errorRange": [7, 5]
"errorRange": [ 7, 5 ]
},
{
"lineNumber": 8,
@ -42,7 +42,7 @@
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md034",
"errorDetail": null,
"errorContext": "https://example.com",
"errorRange": [6, 19]
"errorRange": [ 6, 19 ]
},
{
"lineNumber": 11,
@ -69,7 +69,7 @@
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md037",
"errorDetail": null,
"errorContext": "* inside *",
"errorRange": [7, 10]
"errorRange": [ 7, 10 ]
},
{
"lineNumber": 31,
@ -123,7 +123,7 @@
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md038",
"errorDetail": null,
"errorContext": "` inside `",
"errorRange": [7, 10]
"errorRange": [ 7, 10 ]
},
{
"lineNumber": 24,
@ -222,7 +222,16 @@
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md039",
"errorDetail": null,
"errorContext": "[ inside ]",
"errorRange": [7, 10]
"errorRange": [ 7, 10 ]
},
{
"lineNumber": 60,
"ruleNames": [ "MD039", "no-space-in-links" ],
"ruleDescription": "Spaces inside link text",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md039",
"errorDetail": null,
"errorContext": "[ space]",
"errorRange": [ 6, 8 ]
},
{
"lineNumber": 21,

View file

@ -0,0 +1,27 @@
Not a heading
An [empty]() link
An [empty](#) link with fragment
An [empty](<>) link with angle brackets
This is a test file for the markdownlint package.
This is a paragraph
about markdownlint
that capitalizes the
name wrong twice:
markdownlint.
A [normal](link) and an [empty one]() and a [fragment](#one).
An image without alternate text ![](image.jpg)
```text
Fenced code
```
Indented code
Missing newline character

View file

@ -105,6 +105,6 @@
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md047",
"errorDetail": null,
"errorContext": null,
"errorRange": null
"errorRange": [ 25, 1 ]
}
]

View file

@ -0,0 +1,27 @@
# Blanks Around Headings
## Apple
Text
## Banana
Text
## Cherry
Text
## Durian ##
Text
---
Elderberry
----------
Text
## Fig

View file

@ -0,0 +1,31 @@
# Blanks Around Headings
## Apple
Text
## Banana
Text
## Cherry
Text
## Durian ##
Text
Elderberry
----------
Text
## Fig

View file

@ -0,0 +1,26 @@
# Blanks Around Headings
## Apple
Text
## Banana
Text
## Cherry
Text
## Durian ##
Text
---
Elderberry
----------
Text
## Fig

View file

@ -0,0 +1,6 @@
---
front: matter
---
Text
Text

View file

@ -0,0 +1,107 @@
# Detailed HTML Results
Text
<em>Block block</em>
Text <em>inline inline</em> text
Text
<strong>Block block</strong>
Text <strong>inline inline</strong> text
Text
<p>
Block
block <em>block</em> block
block
block <strong>block</strong> block
block
block <em>block</em> block <strong>block</strong> block
block <strong>block</strong> block <em>block</em> block
</p>
Text
<strong><em>Block</em> block</strong>
Text <strong><em>inline</em> inline</strong> text
Text
<em><strong>Block</strong> block</em>
Text <em><strong>inline</strong> inline</em> text
Text
Text <em>inline</em> text <strong>inline</strong> text <em>inline</em> text
Text <strong>inline</strong> text <em>inline</em> text <strong>inline</strong> text
Text
\<not>Block block\</not>
\\<problem>Block block\\</problem>
<not\>Block block</not\>
Text \<not>inline inline\</not> text
Text \\<problem>inline inline\\</problem> text
Text <not\>inline inline</not\> text
Text
> Text <em>inline inline</em> text
> text <strong>inline inline</strong> text
Text
Text <em>inline inline</em> text
text <strong>inline inline</strong> text
Text
```html
Text <em>inline inline</em> text
text <strong>inline inline</strong> text
```
Text
`<em>`
Text ``<em>`` text
Text `<em>` text ``<em>`` text ```<em>``` text
Text `<em>` text <em>inline</em> text
Text ``text <em> text`` text
Text
Text <a href="#anchor">inline</a> text
text <img src="src.png"/> text
Text
<name@example.com> is an email autolink.
Another email autolink: <first+last@ex.exa-mple.com>.
Text
<foo-bar-baz> is an HTML element.
But <foo.bar.baz> is not an autolink or HTML element.
And neither is <foo_bar>.
Nor <123abc>.
Text

View file

@ -24,3 +24,5 @@ Code https://example.com/code?type=fence code
Text <https://example.com/same> more text https://example.com/same still more text <https://example.com/same> done
Text <https://example.com/same> more \* text https://example.com/same more \[ text <https://example.com/same> done
Text https://example.com/first more text https://example.com/second still more text https://example.com/third done

View file

@ -0,0 +1,28 @@
# Detailed Link Results
Text <https://example.com/> text
Text <https://example.com/brackets> text <https://example.com/bare> text
Text <https://example.com/bare> text <https://example.com/brackets> text
Text `code https://example.com/code code` text <https://example.com/> text
> Text <https://example.com/brackets> text <https://example.com/bare> text
Text <https://example.com/dir>
text <https://example.com/file.txt>
text <https://example.com/dir/dir>
text <https://example.com/dir/dir/file?query=param>
```text
Code https://example.com/code?type=fence code
```
Code https://example.com/code?type=indent code
Text <https://example.com/same> more text <https://example.com/same> still more text <https://example.com/same> done
Text <https://example.com/same> more \* text https://example.com/same more \[ text <https://example.com/same> done
Text <https://example.com/first> more text <https://example.com/second> still more text <https://example.com/third> done

View file

@ -88,5 +88,14 @@
"errorDetail": null,
"errorContext": "https://example.com/same",
"errorRange": null
},
{
"lineNumber": 28,
"ruleNames": [ "MD034", "no-bare-urls" ],
"ruleDescription": "Bare URL used",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md034",
"errorDetail": null,
"errorContext": "https://example.com/first",
"errorRange": [ 6, 25 ]
}
]

View file

@ -0,0 +1,3 @@
# Ordered list examples
9. Item

View file

@ -0,0 +1,19 @@
# Ordered list examples
text
0. Item
0. Item
0. Item
text
1. Item
1. Item
1. Item
text
1. Item
2. Item
3. Item

View file

@ -23,3 +23,10 @@
## Heading/Full-Width {MD026}
## Heading/Full-Width {MD026}
<!-- markdownlint-disable heading-style -->
## Heading {MD026} alternate ? ##
Heading {MD026} alternate too ?
-------------------------------

View file

@ -0,0 +1,20 @@
# Headings Without Content
<!-- markdownlint-disable single-title heading-style -->
<!-- markdownlint-disable no-duplicate-heading no-trailing-spaces -->
#
#
#
#
##
##
##
##

View file

@ -2,6 +2,8 @@
Hard tab {MD010}
Hard tabs hard tabs {MD010}
<!-- Hard tab -->
<!--Hard tab-->

View file

@ -1,6 +0,0 @@
# Heading
```text
hello
world
```

View file

@ -66,3 +66,8 @@ text ` code {MD038}
span code
span` text
text.
"<!--"
-->
Text `code
code code `text` {MD038}

View file

@ -16,13 +16,11 @@
- one {MD032}
1. two {MD032}
1. three {MD032}
- four {MD032}
- three {MD032}
1. one {MD032}
- two {MD006} {MD032}
- three {MD032}
1. four {MD032}
1. three {MD032}
## Correct nesting, same type

File diff suppressed because it is too large Load diff

View file

@ -5,3 +5,30 @@ However, this shouldn't trigger inside code blocks:
myObj.getFiles("test")[0]
Nor inline code: `myobj.getFiles("test")[0]`
Two (issues)[https://www.example.com/one] in {MD011} {MD034}
the (same text)[https://www.example.com/two]. {MD011} {MD034}
<!-- markdownlint-disable line-length -->
Two (issues)[https://www.example.com/three] on the (same line)[https://www.example.com/four]. {MD011} {MD034}
`code code
code`
(reversed)[link] {MD011}
text
text `code
code code
code` text
text
text (reversed)[link] text {MD011}
## Escaped JavaScript Content
var IDENT_RE = '([a-zA-Z]|\\.[a-zA-Z.])[a-zA-Z0-9._]*'; {MD011}
begin: /\B(([\/.])[\w\-.\/=]+)+/, {MD011}
{begin: '%r\\(', end: '\\)[a-z]*'} {MD011}
return /(?:(?:(^|\/)[!.])|[*?+()|\[\]{}]|[+@]\()/.test(str); {MD011}

View file

@ -34,7 +34,7 @@ List with multiple paragraphs and incorrect spacing
* Foo {MD030}
Here is the second paragraph
Here is the second paragraph
* Bar {MD030}

View file

@ -30,3 +30,14 @@
The following shouldn't break anything:
[![Screenshot.png](/images/Screenshot.png)](/images/Screenshot.png)
function CodeButNotCode(input) {
return input.replace(/[- ]([a-z])/g, "one"); // {MD039}
}
function MoreCodeButNotCode(input) {
input = input.replace(/[- ]([a-z])/g, "two"); // {MD039}
input = input.toLowerCase();
input = input.replace(/[- ]([a-z])/g, "three"); // {MD039}
return input;
}