Expose shared.js helper code for custom rule authors (fixes #134).

This commit is contained in:
David Anson 2019-04-13 11:18:57 -07:00
parent f614f3e1ce
commit 7e980401b8
52 changed files with 283 additions and 184 deletions

View file

@ -7,7 +7,7 @@ const path = require("path");
const { URL } = require("url");
const markdownIt = require("markdown-it");
const rules = require("./rules");
const shared = require("./shared");
const helpers = require("../helpers");
const cache = require("./cache");
const deprecatedRuleNames = [ "MD002" ];
@ -31,7 +31,7 @@ function validateRuleList(ruleList) {
const value = rule[property];
if (!result &&
(!value || !Array.isArray(value) || (value.length === 0) ||
!value.every(shared.isString) || value.some(shared.isEmptyString))) {
!value.every(helpers.isString) || value.some(helpers.isEmptyString))) {
result = newError(property);
}
});
@ -134,7 +134,7 @@ function removeFrontMatter(content, frontMatter) {
if (frontMatterMatch && !frontMatterMatch.index) {
const contentMatched = frontMatterMatch[0];
content = content.slice(contentMatched.length);
frontMatterLines = contentMatched.split(shared.newLineRe);
frontMatterLines = contentMatched.split(helpers.newLineRe);
if (frontMatterLines.length &&
(frontMatterLines[frontMatterLines.length - 1] === "")) {
frontMatterLines.length--;
@ -269,12 +269,12 @@ function getEnabledRulesPerLineNumber(
const enabledRulesPerLineNumber = new Array(1 + frontMatterLines.length);
lines.forEach(function forLine(line) {
if (!noInlineConfig) {
let match = shared.inlineCommentRe.exec(line);
let match = helpers.inlineCommentRe.exec(line);
if (match) {
enabledRules = shared.clone(enabledRules);
enabledRules = helpers.clone(enabledRules);
while (match) {
forMatch(match);
match = shared.inlineCommentRe.exec(line);
match = helpers.inlineCommentRe.exec(line);
}
}
}
@ -310,10 +310,10 @@ function lintContent(
const removeFrontMatterResult = removeFrontMatter(content, frontMatter);
const frontMatterLines = removeFrontMatterResult.frontMatterLines;
// Ignore the content of HTML comments
content = shared.clearHtmlCommentText(removeFrontMatterResult.content);
content = helpers.clearHtmlCommentText(removeFrontMatterResult.content);
// Parse content into tokens and lines
const tokens = md.parse(content, {});
const lines = content.split(shared.newLineRe);
const lines = content.split(helpers.newLineRe);
annotateTokens(tokens, lines);
const aliasToRuleNames = mapAliasToRuleNames(ruleList);
const effectiveConfig =
@ -328,8 +328,8 @@ function lintContent(
lines,
frontMatterLines
};
cache.lineMetadata(shared.getLineMetadata(params));
cache.flattenedLists(shared.flattenLists(params));
cache.lineMetadata(helpers.getLineMetadata(params));
cache.flattenedLists(helpers.flattenLists(params));
// Function to run for each rule
const result = (resultVersion === 0) ? {} : [];
function forRule(rule) {
@ -345,22 +345,22 @@ function lintContent(
function onError(errorInfo) {
if (!errorInfo ||
!errorInfo.lineNumber ||
!shared.isNumber(errorInfo.lineNumber)) {
!helpers.isNumber(errorInfo.lineNumber)) {
throwError("lineNumber");
}
if (errorInfo.detail &&
!shared.isString(errorInfo.detail)) {
!helpers.isString(errorInfo.detail)) {
throwError("detail");
}
if (errorInfo.context &&
!shared.isString(errorInfo.context)) {
!helpers.isString(errorInfo.context)) {
throwError("context");
}
if (errorInfo.range &&
(!Array.isArray(errorInfo.range) ||
(errorInfo.range.length !== 2) ||
!shared.isNumber(errorInfo.range[0]) ||
!shared.isNumber(errorInfo.range[1]))) {
!helpers.isNumber(errorInfo.range[0]) ||
!helpers.isNumber(errorInfo.range[1]))) {
throwError("range");
}
errors.push({
@ -440,9 +440,9 @@ function lintFile(
}
// Make a/synchronous call to read file
if (synchronous) {
lintContentWrapper(null, fs.readFileSync(file, shared.utf8Encoding));
lintContentWrapper(null, fs.readFileSync(file, helpers.utf8Encoding));
} else {
fs.readFile(file, shared.utf8Encoding, lintContentWrapper);
fs.readFile(file, helpers.utf8Encoding, lintContentWrapper);
}
}
@ -466,7 +466,7 @@ function lintInput(options, synchronous, callback) {
const stringsKeys = Object.keys(strings);
const config = options.config || { "default": true };
const frontMatter = (options.frontMatter === undefined) ?
shared.frontMatterRe : options.frontMatter;
helpers.frontMatterRe : options.frontMatter;
const noInlineConfig = !!options.noInlineConfig;
const resultVersion = (options.resultVersion === undefined) ?
2 : options.resultVersion;
@ -590,7 +590,7 @@ function readConfig(file, parsers, callback) {
parsers = null;
}
// Read file
fs.readFile(file, shared.utf8Encoding, (err, content) => {
fs.readFile(file, helpers.utf8Encoding, (err, content) => {
if (err) {
return callback(err);
}
@ -608,7 +608,7 @@ function readConfig(file, parsers, callback) {
if (errr) {
return callback(errr);
}
return callback(null, shared.assign(extendsConfig, config));
return callback(null, helpers.assign(extendsConfig, config));
});
}
return callback(null, config);
@ -624,7 +624,7 @@ function readConfig(file, parsers, callback) {
*/
function readConfigSync(file, parsers) {
// Read file
const content = fs.readFileSync(file, shared.utf8Encoding);
const content = fs.readFileSync(file, helpers.utf8Encoding);
// Try to parse file
const { config, message } = parseConfiguration(file, content, parsers);
if (!config) {
@ -634,7 +634,7 @@ function readConfigSync(file, parsers) {
const configExtends = config.extends;
if (configExtends) {
delete config.extends;
return shared.assign(
return helpers.assign(
readConfigSync(path.resolve(path.dirname(file), configExtends), parsers),
config);
}

View file

@ -2,7 +2,7 @@
"use strict";
const shared = require("./shared");
const { addErrorDetailIf, filterTokens } = require("../helpers");
module.exports = {
"names": [ "MD001", "heading-increment", "header-increment" ],
@ -10,10 +10,10 @@ module.exports = {
"tags": [ "headings", "headers" ],
"function": function MD001(params, onError) {
let prevLevel = 0;
shared.filterTokens(params, "heading_open", function forToken(token) {
filterTokens(params, "heading_open", function forToken(token) {
const level = parseInt(token.tag.slice(1), 10);
if (prevLevel && (level > prevLevel)) {
shared.addErrorDetailIf(onError, token.lineNumber,
addErrorDetailIf(onError, token.lineNumber,
"h" + (prevLevel + 1), "h" + level);
}
prevLevel = level;

View file

@ -2,7 +2,7 @@
"use strict";
const shared = require("./shared");
const { addErrorDetailIf } = require("../helpers");
module.exports = {
"names": [ "MD002", "first-heading-h1", "first-header-h1" ],
@ -13,7 +13,7 @@ module.exports = {
const tag = "h" + level;
params.tokens.every(function forToken(token) {
if (token.type === "heading_open") {
shared.addErrorDetailIf(onError, token.lineNumber, tag, token.tag);
addErrorDetailIf(onError, token.lineNumber, tag, token.tag);
return false;
}
return true;

View file

@ -2,7 +2,8 @@
"use strict";
const shared = require("./shared");
const { addErrorDetailIf, filterTokens, headingStyleFor } =
require("../helpers");
module.exports = {
"names": [ "MD003", "heading-style", "header-style" ],
@ -10,8 +11,8 @@ module.exports = {
"tags": [ "headings", "headers" ],
"function": function MD003(params, onError) {
let style = params.config.style || "consistent";
shared.filterTokens(params, "heading_open", function forToken(token) {
const styleForToken = shared.headingStyleFor(token);
filterTokens(params, "heading_open", function forToken(token) {
const styleForToken = headingStyleFor(token);
if (style === "consistent") {
style = styleForToken;
}
@ -32,7 +33,7 @@ module.exports = {
} else if (style === "setext_with_atx_closed") {
expected = h12 ? "setext" : "atx_closed";
}
shared.addErrorDetailIf(onError, token.lineNumber,
addErrorDetailIf(onError, token.lineNumber,
expected, styleForToken);
}
}

View file

@ -3,7 +3,7 @@
"use strict";
const { addErrorDetailIf, listItemMarkerRe,
rangeFromRegExp } = require("./shared");
rangeFromRegExp } = require("../helpers");
const { flattenedLists } = require("./cache");
// Returns the unordered list style for a list item token

View file

@ -3,7 +3,7 @@
"use strict";
const { addError, addErrorDetailIf, indentFor, listItemMarkerRe,
orderedListItemMarkerRe, rangeFromRegExp } = require("./shared");
orderedListItemMarkerRe, rangeFromRegExp } = require("../helpers");
const { flattenedLists } = require("./cache");
module.exports = {

View file

@ -3,7 +3,7 @@
"use strict";
const { addErrorDetailIf, listItemMarkerRe, rangeFromRegExp } =
require("./shared");
require("../helpers");
const { flattenedLists } = require("./cache");
module.exports = {

View file

@ -3,7 +3,7 @@
"use strict";
const { addErrorDetailIf, listItemMarkerRe, rangeFromRegExp } =
require("./shared");
require("../helpers");
const { flattenedLists } = require("./cache");
module.exports = {

View file

@ -3,7 +3,7 @@
"use strict";
const { addError, filterTokens, forEachLine, includesSorted, rangeFromRegExp,
trimRight } = require("./shared");
trimRight } = require("../helpers");
const { lineMetadata } = require("./cache");
const trailingSpaceRe = /\s+$/;

View file

@ -2,7 +2,7 @@
"use strict";
const { addError, forEachLine, rangeFromRegExp } = require("./shared");
const { addError, forEachLine, rangeFromRegExp } = require("../helpers");
const { lineMetadata } = require("./cache");
const tabRe = /\t+/;

View file

@ -2,7 +2,7 @@
"use strict";
const shared = require("./shared");
const { addError, forEachInlineChild, rangeFromRegExp } = require("../helpers");
const reversedLinkRe = /\([^)]+\)\[[^\]^][^\]]*]/;
@ -11,11 +11,11 @@ module.exports = {
"description": "Reversed link syntax",
"tags": [ "links" ],
"function": function MD011(params, onError) {
shared.forEachInlineChild(params, "text", function forToken(token) {
forEachInlineChild(params, "text", function forToken(token) {
const match = reversedLinkRe.exec(token.content);
if (match) {
shared.addError(onError, token.lineNumber, match[0], null,
shared.rangeFromRegExp(token.line, reversedLinkRe));
addError(onError, token.lineNumber, match[0], null,
rangeFromRegExp(token.line, reversedLinkRe));
}
});
}

View file

@ -2,7 +2,7 @@
"use strict";
const { addErrorDetailIf, forEachLine } = require("./shared");
const { addErrorDetailIf, forEachLine } = require("../helpers");
const { lineMetadata } = require("./cache");
module.exports = {

View file

@ -3,7 +3,7 @@
"use strict";
const { addErrorDetailIf, filterTokens, forEachHeading, forEachLine,
includesSorted, rangeFromRegExp } = require("./shared");
includesSorted, rangeFromRegExp } = require("../helpers");
const { lineMetadata } = require("./cache");
const longLineRePrefix = "^(.{";

View file

@ -2,7 +2,8 @@
"use strict";
const shared = require("./shared");
const { addErrorContext, filterTokens, newLineRe, rangeFromRegExp } =
require("../helpers");
const dollarCommandRe = /^(\s*)(\$\s)/;
@ -12,15 +13,15 @@ module.exports = {
"tags": [ "code" ],
"function": function MD014(params, onError) {
[ "code_block", "fence" ].forEach(function forType(type) {
shared.filterTokens(params, type, function forToken(token) {
filterTokens(params, type, function forToken(token) {
let allBlank = true;
if (token.content && token.content.split(shared.newLineRe)
if (token.content && token.content.split(newLineRe)
.every(function forLine(line) {
return !line || (allBlank = false) || dollarCommandRe.test(line);
}) && !allBlank) {
shared.addErrorContext(onError, token.lineNumber,
token.content.split(shared.newLineRe)[0].trim(), null, null,
shared.rangeFromRegExp(token.line, dollarCommandRe));
addErrorContext(onError, token.lineNumber,
token.content.split(newLineRe)[0].trim(), null, null,
rangeFromRegExp(token.line, dollarCommandRe));
}
});
});

View file

@ -3,7 +3,7 @@
"use strict";
const { addErrorContext, atxHeadingSpaceRe, forEachLine,
rangeFromRegExp } = require("./shared");
rangeFromRegExp } = require("../helpers");
const { lineMetadata } = require("./cache");
module.exports = {

View file

@ -2,19 +2,20 @@
"use strict";
const shared = require("./shared");
const { addErrorContext, atxHeadingSpaceRe, filterTokens, headingStyleFor,
rangeFromRegExp } = 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) {
shared.filterTokens(params, "heading_open", function forToken(token) {
if ((shared.headingStyleFor(token) === "atx") &&
filterTokens(params, "heading_open", function forToken(token) {
if ((headingStyleFor(token) === "atx") &&
/^#+\s\s/.test(token.line)) {
shared.addErrorContext(onError, token.lineNumber, token.line.trim(),
addErrorContext(onError, token.lineNumber, token.line.trim(),
null, null,
shared.rangeFromRegExp(token.line, shared.atxHeadingSpaceRe));
rangeFromRegExp(token.line, atxHeadingSpaceRe));
}
});
}

View file

@ -2,7 +2,7 @@
"use strict";
const { addErrorContext, forEachLine, rangeFromRegExp } = require("./shared");
const { addErrorContext, forEachLine, rangeFromRegExp } = require("../helpers");
const { lineMetadata } = require("./cache");
const atxClosedHeadingNoSpaceRe = /(?:^#+[^#\s])|(?:[^#\s]#+\s*$)/;

View file

@ -2,7 +2,8 @@
"use strict";
const shared = require("./shared");
const { addErrorContext, filterTokens, headingStyleFor, rangeFromRegExp } =
require("../helpers");
const atxClosedHeadingSpaceRe = /(?:^#+\s\s+?\S)|(?:\S\s\s+?#+\s*$)/;
@ -11,14 +12,14 @@ module.exports = {
"description": "Multiple spaces inside hashes on closed atx style heading",
"tags": [ "headings", "headers", "atx_closed", "spaces" ],
"function": function MD021(params, onError) {
shared.filterTokens(params, "heading_open", function forToken(token) {
if (shared.headingStyleFor(token) === "atx_closed") {
filterTokens(params, "heading_open", function forToken(token) {
if (headingStyleFor(token) === "atx_closed") {
const left = /^#+\s\s/.test(token.line);
const right = /\s\s#+$/.test(token.line);
if (left || right) {
shared.addErrorContext(onError, token.lineNumber, token.line.trim(),
addErrorContext(onError, token.lineNumber, token.line.trim(),
left, right,
shared.rangeFromRegExp(token.line, atxClosedHeadingSpaceRe));
rangeFromRegExp(token.line, atxClosedHeadingSpaceRe));
}
}
});

View file

@ -2,8 +2,7 @@
"use strict";
const shared = require("./shared");
const { addErrorDetailIf, filterTokens, isBlankLine } = shared;
const { addErrorDetailIf, filterTokens, isBlankLine } = require("../helpers");
module.exports = {
"names": [ "MD022", "blanks-around-headings", "blanks-around-headers" ],

View file

@ -2,7 +2,8 @@
"use strict";
const shared = require("./shared");
const { addErrorContext, filterTokens, rangeFromRegExp } =
require("../helpers");
const spaceBeforeHeadingRe = /^((?:\s+)|(?:[>\s]+\s\s))[^>\s]/;
@ -11,10 +12,10 @@ module.exports = {
"description": "Headings must start at the beginning of the line",
"tags": [ "headings", "headers", "spaces" ],
"function": function MD023(params, onError) {
shared.filterTokens(params, "heading_open", function forToken(token) {
filterTokens(params, "heading_open", function forToken(token) {
if (spaceBeforeHeadingRe.test(token.line)) {
shared.addErrorContext(onError, token.lineNumber, token.line, null,
null, shared.rangeFromRegExp(token.line, spaceBeforeHeadingRe));
addErrorContext(onError, token.lineNumber, token.line, null,
null, rangeFromRegExp(token.line, spaceBeforeHeadingRe));
}
});
}

View file

@ -2,8 +2,7 @@
"use strict";
const shared = require("./shared");
const { addErrorContext, forEachHeading } = shared;
const { addErrorContext, forEachHeading } = require("../helpers");
module.exports = {
"names": [ "MD024", "no-duplicate-heading", "no-duplicate-header" ],

View file

@ -2,7 +2,8 @@
"use strict";
const shared = require("./shared");
const { addErrorContext, filterTokens, frontMatterHasTitle } =
require("../helpers");
module.exports = {
"names": [ "MD025", "single-title", "single-h1" ],
@ -12,15 +13,15 @@ module.exports = {
const level = params.config.level || 1;
const tag = "h" + level;
const foundFrontMatterTitle =
shared.frontMatterHasTitle(
frontMatterHasTitle(
params.frontMatterLines,
params.config.front_matter_title
);
let hasTopLevelHeading = false;
shared.filterTokens(params, "heading_open", function forToken(token) {
filterTokens(params, "heading_open", function forToken(token) {
if (token.tag === tag) {
if (hasTopLevelHeading || foundFrontMatterTitle) {
shared.addErrorContext(onError, token.lineNumber,
addErrorContext(onError, token.lineNumber,
token.line.trim());
} else if (token.lineNumber === 1) {
hasTopLevelHeading = true;

View file

@ -2,7 +2,7 @@
"use strict";
const shared = require("./shared");
const { addError, forEachHeading, rangeFromRegExp } = require("../helpers");
module.exports = {
"names": [ "MD026", "no-trailing-punctuation" ],
@ -11,12 +11,12 @@ module.exports = {
"function": function MD026(params, onError) {
const punctuation = params.config.punctuation || ".,;:!?";
const trailingPunctuationRe = new RegExp("[" + punctuation + "]$");
shared.forEachHeading(params, function forHeading(heading, content) {
forEachHeading(params, function forHeading(heading, content) {
const match = trailingPunctuationRe.exec(content);
if (match) {
shared.addError(onError, heading.lineNumber,
addError(onError, heading.lineNumber,
"Punctuation: '" + match[0] + "'", null,
shared.rangeFromRegExp(heading.line, trailingPunctuationRe));
rangeFromRegExp(heading.line, trailingPunctuationRe));
}
});
}

View file

@ -2,7 +2,7 @@
"use strict";
const shared = require("./shared");
const { addErrorContext, newLineRe, rangeFromRegExp } = require("../helpers");
const spaceAfterBlockQuote = /^\s*(?:>\s+)+\S/;
@ -27,15 +27,15 @@ module.exports = {
/^(\s*>)+\s\s+>/.test(token.line) :
/^(\s*>)+\s\s/.test(token.line);
if (multipleSpaces) {
shared.addErrorContext(onError, token.lineNumber, token.line, null,
null, shared.rangeFromRegExp(token.line, spaceAfterBlockQuote));
addErrorContext(onError, token.lineNumber, token.line, null,
null, rangeFromRegExp(token.line, spaceAfterBlockQuote));
}
token.content.split(shared.newLineRe)
token.content.split(newLineRe)
.forEach(function forLine(line, offset) {
if (/^\s/.test(line)) {
shared.addErrorContext(onError, token.lineNumber + offset,
addErrorContext(onError, token.lineNumber + offset,
"> " + line, null, null,
shared.rangeFromRegExp(line, spaceAfterBlockQuote));
rangeFromRegExp(line, spaceAfterBlockQuote));
}
});
}

View file

@ -2,7 +2,7 @@
"use strict";
const shared = require("./shared");
const { addError } = require("../helpers");
module.exports = {
"names": [ "MD028", "no-blanks-blockquote" ],
@ -13,7 +13,7 @@ module.exports = {
params.tokens.forEach(function forToken(token) {
if ((token.type === "blockquote_open") &&
(prevToken.type === "blockquote_close")) {
shared.addError(onError, token.lineNumber - 1);
addError(onError, token.lineNumber - 1);
}
prevToken = token;
});

View file

@ -3,7 +3,7 @@
"use strict";
const { addErrorDetailIf, listItemMarkerRe, orderedListItemMarkerRe,
rangeFromRegExp } = require("./shared");
rangeFromRegExp } = require("../helpers");
const { flattenedLists } = require("./cache");
const listStyleExamples = {

View file

@ -3,7 +3,7 @@
"use strict";
const { addErrorDetailIf, listItemMarkerRe, rangeFromRegExp } =
require("./shared");
require("../helpers");
const { flattenedLists } = require("./cache");
module.exports = {

View file

@ -2,7 +2,7 @@
"use strict";
const { addErrorContext, forEachLine, isBlankLine } = require("./shared");
const { addErrorContext, forEachLine, isBlankLine } = require("../helpers");
const { lineMetadata } = require("./cache");
module.exports = {

View file

@ -2,7 +2,7 @@
"use strict";
const { addErrorContext, isBlankLine } = require("./shared");
const { addErrorContext, isBlankLine } = require("../helpers");
const { flattenedLists } = require("./cache");
module.exports = {

View file

@ -2,10 +2,8 @@
"use strict";
const shared = require("./shared");
const {
addError, filterTokens, forEachInlineChild, newLineRe, rangeFromRegExp
} = shared;
const { addError, filterTokens, forEachInlineChild, newLineRe,
rangeFromRegExp } = require("../helpers");
const htmlRe = /<[^>]*>/;

View file

@ -2,14 +2,15 @@
"use strict";
const shared = require("./shared");
const { addErrorContext, bareUrlRe, filterTokens, rangeFromRegExp } =
require("../helpers");
module.exports = {
"names": [ "MD034", "no-bare-urls" ],
"description": "Bare URL used",
"tags": [ "links", "url" ],
"function": function MD034(params, onError) {
shared.filterTokens(params, "inline", function forToken(token) {
filterTokens(params, "inline", function forToken(token) {
let inLink = false;
token.children.forEach(function forChild(child) {
let match = null;
@ -19,9 +20,9 @@ module.exports = {
inLink = false;
} else if ((child.type === "text") &&
!inLink &&
(match = shared.bareUrlRe.exec(child.content))) {
shared.addErrorContext(onError, child.lineNumber, match[0], null,
null, shared.rangeFromRegExp(child.line, shared.bareUrlRe));
(match = bareUrlRe.exec(child.content))) {
addErrorContext(onError, child.lineNumber, match[0], null,
null, rangeFromRegExp(child.line, bareUrlRe));
}
});
});

View file

@ -2,7 +2,7 @@
"use strict";
const shared = require("./shared");
const { addErrorDetailIf, filterTokens } = require("../helpers");
module.exports = {
"names": [ "MD035", "hr-style" ],
@ -10,12 +10,12 @@ module.exports = {
"tags": [ "hr" ],
"function": function MD035(params, onError) {
let style = params.config.style || "consistent";
shared.filterTokens(params, "hr", function forToken(token) {
filterTokens(params, "hr", function forToken(token) {
const lineTrim = token.line.trim();
if (style === "consistent") {
style = lineTrim;
}
shared.addErrorDetailIf(onError, token.lineNumber, style, lineTrim);
addErrorDetailIf(onError, token.lineNumber, style, lineTrim);
});
}
};

View file

@ -2,7 +2,7 @@
"use strict";
const shared = require("./shared");
const { addErrorContext } = require("../helpers");
module.exports = {
"names": [ "MD036", "no-emphasis-as-heading", "no-emphasis-as-header" ],
@ -24,7 +24,7 @@ module.exports = {
(children[0].type === "em_open")) &&
(children[1].type === "text") &&
!re.test(children[1].content)) {
shared.addErrorContext(onError, t.lineNumber,
addErrorContext(onError, t.lineNumber,
children[1].content);
}
return base;

View file

@ -2,8 +2,7 @@
"use strict";
const shared = require("./shared");
const { addErrorContext, forEachInlineChild } = shared;
const { addErrorContext, forEachInlineChild } = require("../helpers");
module.exports = {
"names": [ "MD037", "no-space-in-emphasis" ],

View file

@ -2,7 +2,8 @@
"use strict";
const shared = require("./shared");
const { addErrorContext, filterTokens, forEachInlineCodeSpan, newLineRe } =
require("../helpers");
const startRe = /^\s([^`]|$)/;
const endRe = /[^`]\s$/;
@ -12,16 +13,16 @@ module.exports = {
"description": "Spaces inside code span elements",
"tags": [ "whitespace", "code" ],
"function": function MD038(params, onError) {
shared.filterTokens(params, "inline", (token) => {
filterTokens(params, "inline", (token) => {
if (token.children.some((child) => child.type === "code_inline")) {
const tokenLines = params.lines.slice(token.map[0], token.map[1]);
shared.forEachInlineCodeSpan(
forEachInlineCodeSpan(
tokenLines.join("\n"),
(code, lineIndex, columnIndex, tickCount) => {
let rangeIndex = columnIndex - tickCount;
let rangeLength = code.length + (2 * tickCount);
let rangeLineOffset = 0;
const codeLines = code.split(shared.newLineRe);
const codeLines = code.split(newLineRe);
const left = startRe.test(code);
const right = !left && endRe.test(code);
if (right && (codeLines.length > 1)) {
@ -34,7 +35,7 @@ module.exports = {
}
const context = tokenLines[lineIndex + rangeLineOffset]
.substring(rangeIndex, rangeIndex + rangeLength);
shared.addErrorContext(
addErrorContext(
onError, token.lineNumber + lineIndex + rangeLineOffset,
context, left, right, [ rangeIndex + 1, rangeLength ]);
}

View file

@ -2,7 +2,8 @@
"use strict";
const shared = require("./shared");
const { addErrorContext, filterTokens, rangeFromRegExp, trimLeft, trimRight } =
require("../helpers");
const spaceInLinkRe = /\[(?:\s+(?:[^\]]*?)\s*|(?:[^\]]*?)\s+)](?=\(\S*\))/;
@ -11,7 +12,7 @@ module.exports = {
"description": "Spaces inside link text",
"tags": [ "whitespace", "links" ],
"function": function MD039(params, onError) {
shared.filterTokens(params, "inline", function forToken(token) {
filterTokens(params, "inline", function forToken(token) {
let inLink = false;
let linkText = "";
token.children.forEach(function forChild(child) {
@ -20,12 +21,12 @@ module.exports = {
linkText = "";
} else if (child.type === "link_close") {
inLink = false;
const left = shared.trimLeft(linkText).length !== linkText.length;
const right = shared.trimRight(linkText).length !== linkText.length;
const left = trimLeft(linkText).length !== linkText.length;
const right = trimRight(linkText).length !== linkText.length;
if (left || right) {
shared.addErrorContext(onError, token.lineNumber,
addErrorContext(onError, token.lineNumber,
"[" + linkText + "]", left, right,
shared.rangeFromRegExp(token.line, spaceInLinkRe));
rangeFromRegExp(token.line, spaceInLinkRe));
}
} else if (inLink) {
linkText += child.content;

View file

@ -2,16 +2,16 @@
"use strict";
const shared = require("./shared");
const { addErrorContext, filterTokens } = require("../helpers");
module.exports = {
"names": [ "MD040", "fenced-code-language" ],
"description": "Fenced code blocks should have a language specified",
"tags": [ "code", "language" ],
"function": function MD040(params, onError) {
shared.filterTokens(params, "fence", function forToken(token) {
filterTokens(params, "fence", function forToken(token) {
if (!token.info.trim()) {
shared.addErrorContext(onError, token.lineNumber, token.line);
addErrorContext(onError, token.lineNumber, token.line);
}
});
}

View file

@ -2,7 +2,7 @@
"use strict";
const shared = require("./shared");
const { addErrorContext, frontMatterHasTitle } = require("../helpers");
module.exports = {
"names": [ "MD041", "first-line-heading", "first-line-h1" ],
@ -12,7 +12,7 @@ module.exports = {
const level = params.config.level || 1;
const tag = "h" + level;
const foundFrontMatterTitle =
shared.frontMatterHasTitle(
frontMatterHasTitle(
params.frontMatterLines,
params.config.front_matter_title
);
@ -22,7 +22,7 @@ module.exports = {
return true;
}
if ((token.type !== "heading_open") || (token.tag !== tag)) {
shared.addErrorContext(onError, token.lineNumber, token.line);
addErrorContext(onError, token.lineNumber, token.line);
}
return false;
});

View file

@ -2,7 +2,8 @@
"use strict";
const shared = require("./shared");
const { addErrorContext, filterTokens, rangeFromRegExp } =
require("../helpers");
const emptyLinkRe = /\[[^\]]*](?:\((?:#?|(?:<>))\))/;
@ -11,7 +12,7 @@ module.exports = {
"description": "No empty links",
"tags": [ "links" ],
"function": function MD042(params, onError) {
shared.filterTokens(params, "inline", function forToken(token) {
filterTokens(params, "inline", function forToken(token) {
let inLink = false;
let linkText = "";
let emptyLink = false;
@ -27,9 +28,9 @@ module.exports = {
} else if (child.type === "link_close") {
inLink = false;
if (emptyLink) {
shared.addErrorContext(onError, child.lineNumber,
addErrorContext(onError, child.lineNumber,
"[" + linkText + "]()", null, null,
shared.rangeFromRegExp(child.line, emptyLinkRe));
rangeFromRegExp(child.line, emptyLinkRe));
}
} else if (inLink) {
linkText += child.content;

View file

@ -2,7 +2,8 @@
"use strict";
const shared = require("./shared");
const { addErrorContext, addErrorDetailIf, forEachHeading } =
require("../helpers");
module.exports = {
"names": [ "MD043", "required-headings", "required-headers" ],
@ -18,7 +19,7 @@ module.exports = {
let i = 0;
let optional = false;
let errorCount = 0;
shared.forEachHeading(params, function forHeading(heading, content) {
forEachHeading(params, function forHeading(heading, content) {
if (!errorCount) {
const actual = levels[heading.tag] + " " + content;
const expected = requiredHeadings[i++] || "[None]";
@ -29,14 +30,14 @@ module.exports = {
} else if (optional) {
i--;
} else {
shared.addErrorDetailIf(onError, heading.lineNumber,
addErrorDetailIf(onError, heading.lineNumber,
expected, actual);
errorCount++;
}
}
});
if ((i < requiredHeadings.length) && !errorCount) {
shared.addErrorContext(onError, params.lines.length,
addErrorContext(onError, params.lines.length,
requiredHeadings[i]);
}
}

View file

@ -2,9 +2,8 @@
"use strict";
const shared = require("./shared");
const { addErrorDetailIf, bareUrlRe, escapeForRegExp, filterTokens,
forEachInlineChild, newLineRe } = shared;
forEachInlineChild, newLineRe } = require("../helpers");
module.exports = {
"names": [ "MD044", "proper-names" ],

View file

@ -2,16 +2,16 @@
"use strict";
const shared = require("./shared");
const { addError, forEachInlineChild } = require("../helpers");
module.exports = {
"names": [ "MD045", "no-alt-text" ],
"description": "Images should have alternate text (alt text)",
"tags": [ "accessibility", "images" ],
"function": function MD045(params, onError) {
shared.forEachInlineChild(params, "image", function forToken(token) {
forEachInlineChild(params, "image", function forToken(token) {
if (token.content === "") {
shared.addError(onError, token.lineNumber);
addError(onError, token.lineNumber);
}
});
}

View file

@ -1,385 +0,0 @@
// @ts-check
"use strict";
// 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]/;
// Regular expression for matching common front matter (YAML and TOML)
module.exports.frontMatterRe =
/((^---$[^]*?^---$)|(^\+\+\+$[^]*?^(\+\+\+|\.\.\.)$))(\r\n|\r|\n|$)/m;
// Regular expression for matching inline disable/enable comments
const inlineCommentRe =
/<!--\s*markdownlint-(dis|en)able((?:\s+[a-z0-9_-]+)*)\s*-->/ig;
module.exports.inlineCommentRe = inlineCommentRe;
// Regular expressions for range matching
module.exports.atxHeadingSpaceRe = /^#+\s*\S/;
module.exports.bareUrlRe = /(?:http|ftp)s?:\/\/[^\s]*/i;
module.exports.listItemMarkerRe = /^[\s>]*(?:[*+-]|\d+[.)])\s+/;
module.exports.orderedListItemMarkerRe = /^[\s>]*0*(\d+)[.)]/;
// readFile options for reading with the UTF-8 encoding
module.exports.utf8Encoding = { "encoding": "utf8" };
// Trims whitespace from the left (start) of a string
function trimLeft(str) {
return str.replace(/^\s*/, "");
}
module.exports.trimLeft = trimLeft;
// Trims whitespace from the right (end) of a string
module.exports.trimRight = function trimRight(str) {
return str.replace(/\s*$/, "");
};
// Applies key/value pairs from src to dst, returning dst
function assign(dst, src) {
Object.keys(src).forEach(function forKey(key) {
dst[key] = src[key];
});
return dst;
}
module.exports.assign = assign;
// Clones the key/value pairs of obj, returning the clone
module.exports.clone = function clone(obj) {
return assign({}, obj);
};
// Returns true iff the input is a number
module.exports.isNumber = function isNumber(obj) {
return typeof obj === "number";
};
// Returns true iff the input is a string
module.exports.isString = function isString(obj) {
return typeof obj === "string";
};
// Returns true iff the input string is empty
module.exports.isEmptyString = function isEmptyString(str) {
return str.length === 0;
};
// Returns true iff the input line is blank (no content)
// Example: Contains nothing, whitespace, or comments
const blankLineRe = />|(?:<!--.*?-->)/g;
module.exports.isBlankLine = function isBlankLine(line) {
return !line || !line.trim() || !line.replace(blankLineRe, "").trim();
};
// Returns true iff the sorted array contains the specified element
module.exports.includesSorted = function includesSorted(array, element) {
let left = 0;
let right = array.length - 1;
while (left <= right) {
/* eslint-disable no-bitwise */
const mid = (left + right) >> 1;
if (array[mid] < element) {
left = mid + 1;
} else if (array[mid] > element) {
right = mid - 1;
} else {
return true;
}
}
return false;
};
// Replaces the text of all properly-formatted HTML comments with whitespace
// This preserves the line/column information for the rest of the document
// Trailing whitespace is avoided with a '\' character in the last column
// See https://www.w3.org/TR/html5/syntax.html#comments for details
const htmlCommentBegin = "<!--";
const htmlCommentEnd = "-->";
module.exports.clearHtmlCommentText = function clearHtmlCommentText(text) {
let i = 0;
while ((i = text.indexOf(htmlCommentBegin, i)) !== -1) {
let j = text.indexOf(htmlCommentEnd, i);
if (j === -1) {
j = text.length;
text += "\\";
}
const comment = text.slice(i + htmlCommentBegin.length, j);
if ((comment.length > 0) &&
(comment[0] !== ">") &&
(comment[comment.length - 1] !== "-") &&
!comment.includes("--") &&
(text.slice(i, j + htmlCommentEnd.length)
.search(inlineCommentRe) === -1)) {
const blanks = comment
.replace(/[^\r\n]/g, " ")
.replace(/ ([\r\n])/g, "\\$1");
text = text.slice(0, i + htmlCommentBegin.length) +
blanks + text.slice(j);
}
i = j + htmlCommentEnd.length;
}
return text;
};
// Escapes a string for use in a RegExp
module.exports.escapeForRegExp = function escapeForRegExp(str) {
return str.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
};
// Returns the indent for a token
function indentFor(token) {
const line = token.line.replace(/^[\s>]*(> |>)/, "");
return line.length - trimLeft(line).length;
}
module.exports.indentFor = indentFor;
// Returns the heading style for a heading token
module.exports.headingStyleFor = function headingStyleFor(token) {
if ((token.map[1] - token.map[0]) === 1) {
if (/[^\\]#\s*$/.test(token.line)) {
return "atx_closed";
}
return "atx";
}
return "setext";
};
// Calls the provided function for each matching token
function filterTokens(params, type, handler) {
params.tokens.forEach(function forToken(token) {
if (token.type === type) {
handler(token);
}
});
}
module.exports.filterTokens = filterTokens;
// Get line metadata array
module.exports.getLineMetadata = function getLineMetadata(params) {
const lineMetadata = params.lines.map(function mapLine(line, index) {
return [ line, index, false, 0, false ];
});
filterTokens(params, "fence", function forToken(token) {
lineMetadata[token.map[0]][3] = 1;
lineMetadata[token.map[1] - 1][3] = -1;
for (let i = token.map[0] + 1; i < token.map[1] - 1; i++) {
lineMetadata[i][2] = true;
}
});
filterTokens(params, "code_block", function forToken(token) {
for (let i = token.map[0]; i < token.map[1]; i++) {
lineMetadata[i][2] = true;
}
});
filterTokens(params, "table_open", function forToken(token) {
for (let i = token.map[0]; i < token.map[1]; i++) {
lineMetadata[i][4] = true;
}
});
return lineMetadata;
};
// Calls the provided function for each line (with context)
module.exports.forEachLine = function forEachLine(lineMetadata, handler) {
lineMetadata.forEach(function forMetadata(metadata) {
// Parameters: line, lineIndex, inCode, onFence, inTable
handler(...metadata);
});
};
// Returns (nested) lists as a flat array (in order)
module.exports.flattenLists = function flattenLists(params) {
const flattenedLists = [];
const stack = [];
let current = null;
let lastWithMap = { "map": [ 0, 1 ] };
params.tokens.forEach(function forToken(token) {
if ((token.type === "bullet_list_open") ||
(token.type === "ordered_list_open")) {
// Save current context and start a new one
stack.push(current);
current = {
"unordered": (token.type === "bullet_list_open"),
"parentsUnordered": !current ||
(current.unordered && current.parentsUnordered),
"open": token,
"indent": indentFor(token),
"parentIndent": (current && current.indent) || 0,
"items": [],
"nesting": stack.length - 1,
"lastLineIndex": -1,
"insert": flattenedLists.length
};
} else if ((token.type === "bullet_list_close") ||
(token.type === "ordered_list_close")) {
// Finalize current context and restore previous
current.lastLineIndex = lastWithMap.map[1];
flattenedLists.splice(current.insert, 0, current);
delete current.insert;
current = stack.pop();
} else if (token.type === "list_item_open") {
// Add list item
current.items.push(token);
} else if (token.map) {
// Track last token with map
lastWithMap = token;
}
});
return flattenedLists;
};
// Calls the provided function for each specified inline child token
module.exports.forEachInlineChild =
function forEachInlineChild(params, type, handler) {
filterTokens(params, "inline", function forToken(token) {
token.children.forEach(function forChild(child) {
if (child.type === type) {
handler(child, token);
}
});
});
};
// Calls the provided function for each heading's content
module.exports.forEachHeading = function forEachHeading(params, handler) {
let heading = null;
params.tokens.forEach(function forToken(token) {
if (token.type === "heading_open") {
heading = token;
} else if (token.type === "heading_close") {
heading = null;
} else if ((token.type === "inline") && heading) {
handler(heading, token.content);
}
});
};
// Calls the provided function for each inline code span's content
module.exports.forEachInlineCodeSpan =
function forEachInlineCodeSpan(input, handler) {
let currentLine = 0;
let currentColumn = 0;
let index = 0;
while (index < input.length) {
let startIndex = -1;
let startLine = -1;
let startColumn = -1;
let tickCount = 0;
let currentTicks = 0;
// Deliberate <= so trailing 0 completes the last span (ex: "text `code`")
for (; index <= input.length; index++) {
const char = input[index];
if (char === "`") {
// Count backticks at start or end of code span
currentTicks++;
if ((startIndex === -1) || (startColumn === -1)) {
startIndex = index + 1;
}
} else {
if ((startIndex >= 0) &&
(startColumn >= 0) &&
(tickCount === currentTicks)) {
// Found end backticks; invoke callback for code span
handler(
input.substring(startIndex, index - currentTicks),
startLine, startColumn, tickCount);
startIndex = -1;
startColumn = -1;
} else if ((startIndex >= 0) && (startColumn === -1)) {
// Found start backticks
tickCount = currentTicks;
startLine = currentLine;
startColumn = currentColumn;
}
// Not in backticks
currentTicks = 0;
}
if (char === "\n") {
// On next line
currentLine++;
currentColumn = 0;
} else if ((char === "\\") &&
((startIndex === -1) || (startColumn === -1))) {
// Escape character outside code, skip next
index++;
currentColumn += 2;
} else {
// On next column
currentColumn++;
}
}
if (startIndex >= 0) {
// Restart loop after unmatched start backticks (ex: "`text``code``")
index = startIndex;
currentLine = startLine;
currentColumn = startColumn;
}
}
};
// Adds a generic error object via the onError callback
function addError(onError, lineNumber, detail, context, range) {
onError({
"lineNumber": lineNumber,
"detail": detail,
"context": context,
"range": range
});
}
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) {
if (expected !== actual) {
addError(
onError,
lineNumber,
"Expected: " + expected + "; Actual: " + actual +
(detail ? "; " + detail : ""),
context,
range);
}
};
// 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);
};
// Returns a range object for a line by applying a RegExp
module.exports.rangeFromRegExp = function rangeFromRegExp(line, regexp) {
let range = null;
const match = line.match(regexp);
if (match) {
let column = match.index + 1;
let length = match[0].length;
if (match[2]) {
column += match[1].length;
length -= match[1].length;
}
range = [ column, length ];
}
return range;
};
// Determines if the front matter includes a title
module.exports.frontMatterHasTitle =
function frontMatterHasTitle(frontMatterLines, frontMatterTitlePattern) {
const ignoreFrontMatter =
(frontMatterTitlePattern !== undefined) && !frontMatterTitlePattern;
const frontMatterTitleRe =
new RegExp(frontMatterTitlePattern || "^\\s*title\\s*[:=]", "i");
return !ignoreFrontMatter &&
frontMatterLines.some((line) => frontMatterTitleRe.test(line));
};