Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

markdownlint

Package Overview
Dependencies
Maintainers
1
Versions
71
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

markdownlint - npm Package Compare versions

Comparing version 0.16.0 to 0.17.0

demo/markdownlint-browser.js

11

doc/CustomRules.md

@@ -46,3 +46,4 @@ # Custom Rules

- `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.

@@ -56,2 +57,10 @@ - `frontMatterLines` is an `Array` of `String` values corresponding to any front matter (not present in `lines`).

- `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).

@@ -58,0 +67,0 @@ 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).

11

doc/Rules.md

@@ -1098,13 +1098,2 @@

Note: List items without hanging indents are a violation of this rule; list
items with hanging indents are okay:
```markdown
* This is
not okay
* This is
okay
```
<a name="md033"></a>

@@ -1111,0 +1100,0 @@

@@ -5,5 +5,8 @@ // @ts-check

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;

@@ -22,4 +25,3 @@ // Regular expression for matching common front matter (YAML and TOML)

// 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+/;

@@ -49,2 +51,7 @@ module.exports.orderedListItemMarkerRe = /^[\s>]*0*(\d+)[.)]/;

// 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)

@@ -318,3 +325,4 @@ // Example: Contains nothing, whitespace, or comments

} else if ((char === "\\") &&
((startIndex === -1) || (startColumn === -1))) {
((startIndex === -1) || (startColumn === -1)) &&
(input[index + 1] !== "\n")) {
// Escape character outside code, skip next

@@ -338,8 +346,9 @@ index++;

// 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
});

@@ -351,3 +360,3 @@ }

module.exports.addErrorDetailIf = function addErrorDetailIf(
onError, lineNumber, expected, actual, detail, context, range) {
onError, lineNumber, expected, actual, detail, context, range, fixInfo) {
if (expected !== actual) {

@@ -360,3 +369,4 @@ addError(

context,
range);
range,
fixInfo);
}

@@ -366,15 +376,15 @@ };

// 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);
};

@@ -407,1 +417,125 @@ // Returns a range object for a line by applying a RegExp

};
// 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);
};
{
"name": "markdownlint-rule-helpers",
"version": "0.4.0",
"version": "0.5.0",
"description": "A collection of markdownlint helper functions for custom rules",

@@ -5,0 +5,0 @@ "main": "helpers.js",

@@ -175,2 +175,9 @@ // @ts-check

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) {

@@ -181,2 +188,4 @@ child.lineNumber = lineNumber;

lineNumber++;
} else if (child.type === "code_inline") {
lineNumber += codeSpanExtraLines.shift();
}

@@ -301,2 +310,7 @@ });

// Function to return true for all inputs
function filterAllValues() {
return true;
}
// Function to return unique values from a sorted errors array

@@ -383,2 +397,33 @@ function uniqueFilterForSortedErrors(value, index, array) {

}
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({

@@ -388,3 +433,4 @@ "lineNumber": errorInfo.lineNumber + frontMatterLines.length,

"context": errorInfo.context || null,
"range": errorInfo.range || null
"range": errorInfo.range || null,
"fixInfo": errorInfo.fixInfo || null
});

@@ -409,3 +455,5 @@ }

const filteredErrors = errors
.filter(uniqueFilterForSortedErrors)
.filter((resultVersion === 3) ?
filterAllValues :
uniqueFilterForSortedErrors)
.filter(function removeDisabledRules(error) {

@@ -432,2 +480,5 @@ return enabledRulesPerLineNumber[error.lineNumber][ruleName];

errorObject.errorRange = error.range;
if (resultVersion === 3) {
errorObject.fixInfo = error.fixInfo;
}
return errorObject;

@@ -434,0 +485,0 @@ });

@@ -16,6 +16,15 @@ // @ts-check

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
});
}

@@ -22,0 +31,0 @@ });

@@ -5,8 +5,6 @@ // @ts-check

const { addError, filterTokens, forEachLine, includesSorted, rangeFromRegExp } =
const { addError, filterTokens, forEachLine, includesSorted } =
require("../helpers");
const { lineMetadata } = require("./cache");
const trailingSpaceRe = /\s+$/;
module.exports = {

@@ -38,10 +36,18 @@ "names": [ "MD009", "no-trailing-spaces" ],

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
});
}

@@ -48,0 +54,0 @@ }

@@ -5,6 +5,6 @@ // @ts-check

const { addError, forEachLine, rangeFromRegExp } = require("../helpers");
const { addError, forEachLine } = require("../helpers");
const { lineMetadata } = require("./cache");
const tabRe = /\t+/;
const tabRe = /\t+/g;

@@ -19,5 +19,19 @@ module.exports = {

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)
});
}
}

@@ -24,0 +38,0 @@ });

@@ -5,5 +5,6 @@ // @ts-check

const { addError, forEachInlineChild, rangeFromRegExp } = require("../helpers");
const { addError, forEachInlineChild, unescapeMarkdown } =
require("../helpers");
const reversedLinkRe = /\([^)]+\)\[[^\]^][^\]]*]/;
const reversedLinkRe = /\(([^)]+)\)\[([^\]^][^\]]*)]/g;

@@ -15,7 +16,22 @@ module.exports = {

"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})`
}
);
}

@@ -22,0 +38,0 @@ });

@@ -18,3 +18,13 @@ // @ts-check

if (maximum < count) {
addErrorDetailIf(onError, lineIndex + 1, maximum, count);
addErrorDetailIf(
onError,
lineIndex + 1,
maximum,
count,
null,
null,
null,
{
"deleteCount": -1
});
}

@@ -21,0 +31,0 @@ });

@@ -5,7 +5,24 @@ // @ts-check

const { addErrorContext, filterTokens, newLineRe, rangeFromRegExp } =
require("../helpers");
const { addErrorContext, filterTokens } = require("../helpers");
const dollarCommandRe = /^(\s*)(\$\s)/;
const dollarCommandRe = /^(\s*)(\$\s+)/;
function addErrorIfPreviousWasCommand(onError, previous) {
if (previous) {
const { lineNumber, lineTrim, column, length } = previous;
addErrorContext(
onError,
lineNumber,
lineTrim,
null,
null,
[ column, length ],
{
"editColumn": column,
"deleteCount": length
}
);
}
}
module.exports = {

@@ -16,13 +33,21 @@ "names": [ "MD014", "commands-show-output" ],

"function": function MD014(params, onError) {
[ "code_block", "fence" ].forEach(function forType(type) {
filterTokens(params, type, function forToken(token) {
let allBlank = true;
if (token.content && token.content.split(newLineRe)
.every(function forLine(line) {
return !line || (allBlank = false) || dollarCommandRe.test(line);
}) && !allBlank) {
addErrorContext(onError, token.lineNumber,
token.content.split(newLineRe)[0].trim(), null, null,
rangeFromRegExp(token.line, dollarCommandRe));
[ "code_block", "fence" ].forEach((type) => {
filterTokens(params, type, (token) => {
let previous = null;
const margin = (token.type === "fence") ? 1 : 0;
for (let i = token.map[0] + margin; i < token.map[1] - margin; i++) {
const line = params.lines[i];
const lineTrim = line.trim();
const match = dollarCommandRe.exec(line);
if (!lineTrim || match) {
addErrorIfPreviousWasCommand(onError, previous);
}
previous = match ? {
"lineNumber": i + 1,
"lineTrim": lineTrim,
"column": match[1].length + 1,
"length": match[2].length
} : null;
}
addErrorIfPreviousWasCommand(onError, previous);
});

@@ -29,0 +54,0 @@ });

@@ -5,4 +5,3 @@ // @ts-check

const { addErrorContext, atxHeadingSpaceRe, forEachLine,
rangeFromRegExp } = require("../helpers");
const { addErrorContext, forEachLine } = require("../helpers");
const { lineMetadata } = require("./cache");

@@ -16,5 +15,16 @@

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": " "
}
);
}

@@ -21,0 +31,0 @@ });

@@ -5,4 +5,4 @@ // @ts-check

const { addErrorContext, atxHeadingSpaceRe, filterTokens, headingStyleFor,
rangeFromRegExp } = require("../helpers");
const { addErrorContext, filterTokens, headingStyleFor } =
require("../helpers");

@@ -14,8 +14,25 @@ module.exports = {

"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
}
);
}
}

@@ -22,0 +39,0 @@ });

@@ -5,7 +5,5 @@ // @ts-check

const { addErrorContext, forEachLine, rangeFromRegExp } = require("../helpers");
const { addErrorContext, forEachLine } = require("../helpers");
const { lineMetadata } = require("./cache");
const atxClosedHeadingNoSpaceRe = /(?:^#+[^#\s])|(?:[^#\s]#+\s*$)/;
module.exports = {

@@ -17,8 +15,46 @@ "names": [ "MD020", "no-missing-space-closed-atx" ],

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}`
}
);
}
}

@@ -25,0 +61,0 @@ }

@@ -5,7 +5,5 @@ // @ts-check

const { addErrorContext, filterTokens, headingStyleFor, rangeFromRegExp } =
const { addErrorContext, filterTokens, headingStyleFor } =
require("../helpers");
const atxClosedHeadingSpaceRe = /(?:^#+\s\s+?\S)|(?:\S\s\s+?#+\s*$)/;
module.exports = {

@@ -16,10 +14,45 @@ "names": [ "MD021", "no-multiple-space-closed-atx" ],

"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}`
}
);
}
}

@@ -26,0 +59,0 @@ }

@@ -23,18 +23,39 @@ // @ts-check

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")
});
});
}
};

@@ -5,4 +5,3 @@ // @ts-check

const { addErrorContext, filterTokens, rangeFromRegExp } =
require("../helpers");
const { addErrorContext, filterTokens } = require("../helpers");

@@ -17,5 +16,22 @@ const spaceBeforeHeadingRe = /^((?:\s+)|(?:[>\s]+\s\s))[^>\s]/;

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
});
}

@@ -22,0 +38,0 @@ });

@@ -5,4 +5,4 @@ // @ts-check

const { addError, allPunctuation, escapeForRegExp, forEachHeading,
rangeFromRegExp } = require("../helpers");
const { addError, allPunctuation, escapeForRegExp, forEachHeading } =
require("../helpers");

@@ -19,9 +19,22 @@ module.exports = {

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
}
);
}

@@ -28,0 +41,0 @@ });

@@ -5,5 +5,5 @@ // @ts-check

const { addErrorContext, newLineRe, rangeFromRegExp } = require("../helpers");
const { addErrorContext, newLineRe } = require("../helpers");
const spaceAfterBlockQuote = /^\s*(?:>\s+)+\S/;
const spaceAfterBlockQuoteRe = /^((?:\s*>)+)(\s{2,})\S/;

@@ -17,27 +17,39 @@ module.exports = {

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));
} 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
}
);
}
}
}
token.content.split(newLineRe)
.forEach(function forLine(line, offset) {
if (/^\s/.test(line)) {
addErrorContext(onError, token.lineNumber + offset,
"> " + line, null, null,
rangeFromRegExp(line, spaceAfterBlockQuote));
}
});
}

@@ -44,0 +56,0 @@ });

@@ -13,10 +13,27 @@ // @ts-check

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;
}
});
}
};

@@ -5,4 +5,3 @@ // @ts-check

const { addErrorDetailIf, listItemMarkerRe, rangeFromRegExp } =
require("../helpers");
const { addErrorDetailIf } = require("../helpers");
const { flattenedLists } = require("./cache");

@@ -26,6 +25,23 @@

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
);
});

@@ -32,0 +48,0 @@ });

@@ -17,6 +17,18 @@ // @ts-check

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"
});
}

@@ -23,0 +35,0 @@ });

@@ -8,2 +8,4 @@ // @ts-check

const quotePrefixRe = /^[>\s]*/;
module.exports = {

@@ -18,7 +20,30 @@ "names": [ "MD032", "blanks-around-lists" ],

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`
});
}

@@ -25,0 +50,0 @@ });

@@ -21,11 +21,25 @@ // @ts-check

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
);
}
}

@@ -32,0 +46,0 @@ });

@@ -7,2 +7,5 @@ // @ts-check

const leftSpaceRe = /(?:^|\s)(\*\*?\*?|__?_?)\s.*[^\\]\1/g;
const rightSpaceRe = /(?:^|[^\\])(\*\*?\*?|__?_?).+\s\1(?:\s|$)/g;
module.exports = {

@@ -14,21 +17,38 @@ "names": [ "MD037", "no-space-in-emphasis" ],

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);
}
}
}
}
});
});
}
};

@@ -8,4 +8,4 @@ // @ts-check

const startRe = /^\s([^`]|$)/;
const endRe = /[^`]\s$/;
const leftSpaceRe = /^\s([^`]|$)/;
const rightSpaceRe = /[^`]\s$/;

@@ -26,18 +26,38 @@ module.exports = {

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
}
);
}

@@ -44,0 +64,0 @@ });

@@ -5,4 +5,3 @@ // @ts-check

const { addErrorContext, filterTokens, rangeFromRegExp } =
require("../helpers");
const { addErrorContext, filterTokens } = require("../helpers");

@@ -16,10 +15,14 @@ const spaceInLinkRe = /\[(?:\s+(?:[^\]]*?)\s*|(?:[^\]]*?)\s+)](?=\(\S*\))/;

"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) {
if (child.type === "link_open") {
let lineIndex = 0;
children.forEach((child) => {
const { content, type } = child;
if (type === "link_open") {
inLink = true;
linkText = "";
} else if (child.type === "link_close") {
} else if (type === "link_close") {
inLink = false;

@@ -29,8 +32,26 @@ const left = linkText.trimLeft().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 ((type === "softbreak") || (type === "hardbreak")) {
lineNumber++;
lineIndex = 0;
} else if (inLink) {
linkText += child.content;
linkText += content;
}

@@ -37,0 +58,0 @@ });

@@ -32,5 +32,26 @@ // @ts-check

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
}
);
}

@@ -37,0 +58,0 @@ }

@@ -15,5 +15,15 @@ // @ts-check

if (!isBlankLine(lastLine)) {
addError(onError, lastLineNumber);
addError(
onError,
lastLineNumber,
null,
null,
[ lastLine.length, 1 ],
{
"insertText": "\n",
"editColumn": lastLine.length + 1
}
);
}
}
};
{
"name": "markdownlint",
"version": "0.16.0",
"version": "0.17.0",
"description": "A Node.js style checker and lint tool for Markdown/CommonMark files.",

@@ -30,9 +30,9 @@ "main": "lib/markdownlint.js",

"dependencies": {
"markdown-it": "9.0.1"
"markdown-it": "10.0.0"
},
"devDependencies": {
"@types/node": "~12.6.9",
"browserify": "~16.3.0",
"@types/node": "~12.7.11",
"browserify": "~16.5.0",
"cpy-cli": "~2.0.0",
"eslint": "~6.1.0",
"eslint": "~6.5.1",
"glob": "~7.1.4",

@@ -44,9 +44,9 @@ "js-yaml": "~3.13.1",

"markdown-it-sup": "~1.0.0",
"markdownlint-rule-helpers": "~0.3.0",
"markdownlint-rule-helpers": "~0.4.0",
"nodeunit": "~0.11.3",
"nyc": "~14.1.1",
"rimraf": "~2.6.3",
"rimraf": "~3.0.0",
"toml": "~3.0.0",
"tv4": "~1.3.0",
"typescript": "~3.5.3",
"typescript": "~3.6.3",
"uglify-js": "~3.6.0"

@@ -53,0 +53,0 @@ },

@@ -419,12 +419,17 @@ # markdownlint

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

@@ -734,2 +739,3 @@

* [Boostnote](https://boostnote.io/) ([Search repository](https://github.com/BoostIO/Boostnote/search?q=markdownlint))
* [CodiMD](https://github.com/hackmdio/codimd) ([Search repository](https://github.com/hackmdio/codimd/search?q=markdownlint))
* [ESLint](https://eslint.org/) ([Search repository](https://github.com/eslint/eslint/search?q=markdownlint))

@@ -801,2 +807,5 @@ * [Garden React Components](https://garden.zendesk.com/react-components/) ([Search repository](https://github.com/zendeskgarden/react-components/search?q=markdownlint))

update dependencies.
* 0.17.0 - Add `resultVersion` 3 to support fix information for default and custom rules,
add fix information for 24 rules, update newline handling to match latest
CommonMark specification, improve MD014/MD037/MD039, update dependencies.

@@ -803,0 +812,0 @@ [npm-image]: https://img.shields.io/npm/v/markdownlint.svg

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc