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

@ -53,6 +53,8 @@ A rule is implemented as an `Object` with four required properties:
- `context` is an optional `String` with relevant text surrounding the error location. - `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. - `range` is an optional `Array` with two `Number` values identifying the 1-based column and length of the error.
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).
## Examples ## Examples
- [Simple rules used by the project's test cases](../test/rules) - [Simple rules used by the project's test cases](../test/rules)

21
helpers/LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015-2019 David Anson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

50
helpers/README.md Normal file
View file

@ -0,0 +1,50 @@
# markdownlint-rule-helpers
> A collection of `markdownlint` helper functions for custom rules
## Overview
The [Markdown](https://en.wikipedia.org/wiki/Markdown) linter
[`markdownlint`](https://github.com/DavidAnson/markdownlint) offers a variety of built-in validation
[rules](https://github.com/DavidAnson/markdownlint/blob/master/doc/Rules.md) and supports the
creation of [custom rules](https://github.com/DavidAnson/markdownlint/blob/master/doc/CustomRules.md).
The internal rules share various helper functions; this package exposes those for reuse by custom rules.
## API
_Undocumented_ - This package exports the internal functions as-is. The APIs were not originally meant
to be public, are not officially supported, and may change from release to release. There are brief
descriptive comments above each function, but no [JSDoc](https://en.m.wikipedia.org/wiki/JSDoc)
annotations. That said, some of what's here will be useful to custom rule authors and may avoid
duplicating code.
## Example
```js
const { forEachLine, getLineMetadata } = require("markdownlint-rule-helpers");
module.exports = {
"names": [ "every-n-lines" ],
"description": "Rule that reports an error every N lines",
"tags": [ "test" ],
"function": (params, onError) => {
const n = params.config.n || 2;
forEachLine(getLineMetadata(params), (line, lineIndex) => {
const lineNumber = lineIndex + 1;
if ((lineNumber % n) === 0) {
onError({
"lineNumber": lineNumber,
"detail": "Line number " + lineNumber
});
}
});
}
};
```
See also: [`markdownlint` built-in rule implementations](https://github.com/DavidAnson/markdownlint/tree/master/lib).
## Tests
_None_ - The entire body of code is tested to 100% coverage by the core `markdownlint` project,
so there are no additional tests here.

18
helpers/package.json Normal file
View file

@ -0,0 +1,18 @@
{
"name": "markdownlint-rule-helpers",
"version": "0.1.0",
"description": "A collection of markdownlint helper functions for custom rules",
"main": "helpers.js",
"author": "David Anson (https://dlaa.me/)",
"license": "MIT",
"homepage": "https://github.com/DavidAnson/markdownlint",
"repository": {
"type": "git",
"url": "https://github.com/DavidAnson/markdownlint.git"
},
"bugs": "https://github.com/DavidAnson/markdownlint/issues",
"keywords": [
"markdownlint",
"markdownlint-rule"
]
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,16 +2,16 @@
"use strict"; "use strict";
const shared = require("./shared"); const { addErrorContext, filterTokens } = require("../helpers");
module.exports = { module.exports = {
"names": [ "MD040", "fenced-code-language" ], "names": [ "MD040", "fenced-code-language" ],
"description": "Fenced code blocks should have a language specified", "description": "Fenced code blocks should have a language specified",
"tags": [ "code", "language" ], "tags": [ "code", "language" ],
"function": function MD040(params, onError) { "function": function MD040(params, onError) {
shared.filterTokens(params, "fence", function forToken(token) { filterTokens(params, "fence", function forToken(token) {
if (!token.info.trim()) { 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"; "use strict";
const shared = require("./shared"); const { addErrorContext, frontMatterHasTitle } = require("../helpers");
module.exports = { module.exports = {
"names": [ "MD041", "first-line-heading", "first-line-h1" ], "names": [ "MD041", "first-line-heading", "first-line-h1" ],
@ -12,7 +12,7 @@ module.exports = {
const level = params.config.level || 1; const level = params.config.level || 1;
const tag = "h" + level; const tag = "h" + level;
const foundFrontMatterTitle = const foundFrontMatterTitle =
shared.frontMatterHasTitle( frontMatterHasTitle(
params.frontMatterLines, params.frontMatterLines,
params.config.front_matter_title params.config.front_matter_title
); );
@ -22,7 +22,7 @@ module.exports = {
return true; return true;
} }
if ((token.type !== "heading_open") || (token.tag !== tag)) { if ((token.type !== "heading_open") || (token.tag !== tag)) {
shared.addErrorContext(onError, token.lineNumber, token.line); addErrorContext(onError, token.lineNumber, token.line);
} }
return false; return false;
}); });

View file

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

View file

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

View file

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

View file

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

View file

@ -16,7 +16,7 @@
"test-cover": "istanbul cover node_modules/nodeunit/bin/nodeunit test/markdownlint-test.js", "test-cover": "istanbul cover node_modules/nodeunit/bin/nodeunit test/markdownlint-test.js",
"test-extra": "nodeunit test/markdownlint-test-extra.js", "test-extra": "nodeunit test/markdownlint-test-extra.js",
"debug": "node debug node_modules/nodeunit/bin/nodeunit", "debug": "node debug node_modules/nodeunit/bin/nodeunit",
"lint": "eslint lib test schema && eslint --env browser --global markdownit --global markdownlint --rule \"no-unused-vars: 0, no-extend-native: 0, max-statements: 0, no-console: 0, no-var: 0\" demo && eslint --rule \"no-console: 0, no-invalid-this: 0, no-shadow: 0, object-property-newline: 0\" example", "lint": "eslint lib helpers test schema && eslint --env browser --global markdownit --global markdownlint --rule \"no-unused-vars: 0, no-extend-native: 0, max-statements: 0, no-console: 0, no-var: 0\" demo && eslint --rule \"no-console: 0, no-invalid-this: 0, no-shadow: 0, object-property-newline: 0\" example",
"build-config-schema": "node schema/build-config-schema.js", "build-config-schema": "node schema/build-config-schema.js",
"build-demo": "cpy node_modules/markdown-it/dist/markdown-it.min.js demo && cd demo && rimraf markdownlint-browser.* && cpy file-header.js . --rename=markdownlint-browser.js && tsc --allowJs --resolveJsonModule --outDir ../lib-es3 ../lib/markdownlint.js && browserify ../lib-es3/lib/markdownlint.js --standalone markdownlint >> markdownlint-browser.js && uglifyjs markdownlint-browser.js --compress --mangle --comments --output markdownlint-browser.min.js", "build-demo": "cpy node_modules/markdown-it/dist/markdown-it.min.js demo && cd demo && rimraf markdownlint-browser.* && cpy file-header.js . --rename=markdownlint-browser.js && tsc --allowJs --resolveJsonModule --outDir ../lib-es3 ../lib/markdownlint.js && browserify ../lib-es3/lib/markdownlint.js --standalone markdownlint >> markdownlint-browser.js && uglifyjs markdownlint-browser.js --compress --mangle --comments --output markdownlint-browser.min.js",
"build-example": "npm install --no-save --ignore-scripts grunt grunt-cli gulp through2", "build-example": "npm install --no-save --ignore-scripts grunt grunt-cli gulp through2",

View file

@ -4,7 +4,7 @@ const fs = require("fs");
const path = require("path"); const path = require("path");
const glob = require("glob"); const glob = require("glob");
const markdownlint = require("../lib/markdownlint"); const markdownlint = require("../lib/markdownlint");
const shared = require("../lib/shared"); const { newLineRe, utf8Encoding } = require("../helpers");
module.exports.typeTestFiles = function typeTestFiles(test) { module.exports.typeTestFiles = function typeTestFiles(test) {
// Simulates typing each test file to validate handling of partial input // Simulates typing each test file to validate handling of partial input
@ -15,7 +15,7 @@ module.exports.typeTestFiles = function typeTestFiles(test) {
}, },
"resultVersion": 0 "resultVersion": 0
}); });
const contentLineCount = content.split(shared.newLineRe).length; const contentLineCount = content.split(newLineRe).length;
Object.keys(results.content).forEach(function forKey(ruleName) { Object.keys(results.content).forEach(function forKey(ruleName) {
results.content[ruleName].forEach(function forLine(line) { results.content[ruleName].forEach(function forLine(line) {
test.ok((line >= 1) && (line <= contentLineCount), test.ok((line >= 1) && (line <= contentLineCount),
@ -28,7 +28,7 @@ module.exports.typeTestFiles = function typeTestFiles(test) {
files.forEach(function forFile(file) { files.forEach(function forFile(file) {
if (/\.md$/.test(file)) { if (/\.md$/.test(file)) {
let content = fs.readFileSync( let content = fs.readFileSync(
path.join("./test", file), shared.utf8Encoding); path.join("./test", file), utf8Encoding);
while (content) { while (content) {
validate(file, content); validate(file, content);
content = content.slice(0, -1); content = content.slice(0, -1);

View file

@ -11,7 +11,7 @@ const pluginSup = require("markdown-it-sup");
const tv4 = require("tv4"); const tv4 = require("tv4");
const packageJson = require("../package.json"); const packageJson = require("../package.json");
const markdownlint = require("../lib/markdownlint"); const markdownlint = require("../lib/markdownlint");
const shared = require("../lib/shared"); const helpers = require("../helpers");
const rules = require("../lib/rules"); const rules = require("../lib/rules");
const customRules = require("./rules/rules.js"); const customRules = require("./rules/rules.js");
const defaultConfig = require("./markdownlint-test-default-config.json"); const defaultConfig = require("./markdownlint-test-default-config.json");
@ -39,7 +39,7 @@ function createTestForFile(file) {
const actualPromise = promisify(fs.stat, configFile) const actualPromise = promisify(fs.stat, configFile)
.then( .then(
function configFileExists() { function configFileExists() {
return promisify(fs.readFile, configFile, shared.utf8Encoding) return promisify(fs.readFile, configFile, helpers.utf8Encoding)
.then(JSON.parse); .then(JSON.parse);
}, },
function noConfigFile() { function noConfigFile() {
@ -48,7 +48,7 @@ function createTestForFile(file) {
.then( .then(
function lintWithConfig(config) { function lintWithConfig(config) {
const mergedConfig = const mergedConfig =
shared.assign(shared.clone(defaultConfig), config); helpers.assign(helpers.clone(defaultConfig), config);
return promisify(markdownlint, { return promisify(markdownlint, {
"files": [ file ], "files": [ file ],
"config": mergedConfig, "config": mergedConfig,
@ -56,7 +56,7 @@ function createTestForFile(file) {
}); });
}); });
const expectedPromise = detailedResults ? const expectedPromise = detailedResults ?
promisify(fs.readFile, resultsFile, shared.utf8Encoding) promisify(fs.readFile, resultsFile, helpers.utf8Encoding)
.then( .then(
function fileContents(contents) { function fileContents(contents) {
const errorObjects = JSON.parse(contents); const errorObjects = JSON.parse(contents);
@ -68,10 +68,10 @@ function createTestForFile(file) {
}); });
return errorObjects; return errorObjects;
}) : }) :
promisify(fs.readFile, file, shared.utf8Encoding) promisify(fs.readFile, file, helpers.utf8Encoding)
.then( .then(
function fileContents(contents) { function fileContents(contents) {
const lines = contents.split(shared.newLineRe); const lines = contents.split(helpers.newLineRe);
const results = {}; const results = {};
lines.forEach(function forLine(line, lineNum) { lines.forEach(function forLine(line, lineNum) {
const regex = /\{(MD\d+)(?::(\d+))?\}/g; const regex = /\{(MD\d+)(?::(\d+))?\}/g;
@ -114,7 +114,8 @@ module.exports.projectFiles = function projectFiles(test) {
const options = { const options = {
"files": [ "files": [
"README.md", "README.md",
"CONTRIBUTING.md" "CONTRIBUTING.md",
"helpers/README.md"
], ],
"noInlineConfig": true, "noInlineConfig": true,
"config": { "config": {
@ -126,7 +127,8 @@ module.exports.projectFiles = function projectFiles(test) {
test.ifError(err); test.ifError(err);
const expected = { const expected = {
"README.md": [], "README.md": [],
"CONTRIBUTING.md": [] "CONTRIBUTING.md": [],
"helpers/README.md": []
}; };
test.deepEqual(actual, expected, "Issue(s) with project files."); test.deepEqual(actual, expected, "Issue(s) with project files.");
test.done(); test.done();
@ -1098,7 +1100,7 @@ module.exports.readme = function readme(test) {
tagToRules[tag] = tagRules; tagToRules[tag] = tagRules;
}); });
}); });
fs.readFile("README.md", shared.utf8Encoding, fs.readFile("README.md", helpers.utf8Encoding,
function readFile(err, contents) { function readFile(err, contents) {
test.ifError(err); test.ifError(err);
const rulesLeft = rules.slice(); const rulesLeft = rules.slice();
@ -1156,7 +1158,7 @@ module.exports.readme = function readme(test) {
module.exports.doc = function doc(test) { module.exports.doc = function doc(test) {
test.expect(312); test.expect(312);
fs.readFile("doc/Rules.md", shared.utf8Encoding, fs.readFile("doc/Rules.md", helpers.utf8Encoding,
function readFile(err, contents) { function readFile(err, contents) {
test.ifError(err); test.ifError(err);
const rulesLeft = rules.slice(); const rulesLeft = rules.slice();
@ -1323,7 +1325,7 @@ function clearHtmlCommentTextValid(test) {
"<!--", "<!--",
" \\" " \\"
]; ];
const actual = shared.clearHtmlCommentText(validComments.join("\n")); const actual = helpers.clearHtmlCommentText(validComments.join("\n"));
const expected = validResult.join("\n"); const expected = validResult.join("\n");
test.equal(actual, expected); test.equal(actual, expected);
test.done(); test.done();
@ -1350,7 +1352,7 @@ function clearHtmlCommentTextInvalid(test) {
"<!--text--->", "<!--text--->",
"<!--te--xt-->" "<!--te--xt-->"
]; ];
const actual = shared.clearHtmlCommentText(invalidComments.join("\n")); const actual = helpers.clearHtmlCommentText(invalidComments.join("\n"));
const expected = invalidComments.join("\n"); const expected = invalidComments.join("\n");
test.equal(actual, expected); test.equal(actual, expected);
test.done(); test.done();
@ -1371,7 +1373,7 @@ function clearHtmlCommentTextNonGreedy(test) {
"<!-- --> -->", "<!-- --> -->",
"<!----> -->" "<!----> -->"
]; ];
const actual = shared.clearHtmlCommentText(nonGreedyComments.join("\n")); const actual = helpers.clearHtmlCommentText(nonGreedyComments.join("\n"));
const expected = nonGreedyResult.join("\n"); const expected = nonGreedyResult.join("\n");
test.equal(actual, expected); test.equal(actual, expected);
test.done(); test.done();
@ -1394,7 +1396,7 @@ function clearHtmlCommentTextEmbedded(test) {
"text<!-- markdownlint-disable MD010 -->text", "text<!-- markdownlint-disable MD010 -->text",
"text<!-- -->text" "text<!-- -->text"
]; ];
const actual = shared.clearHtmlCommentText(embeddedComments.join("\n")); const actual = helpers.clearHtmlCommentText(embeddedComments.join("\n"));
const expected = embeddedResult.join("\n"); const expected = embeddedResult.join("\n");
test.equal(actual, expected); test.equal(actual, expected);
test.done(); test.done();
@ -1421,7 +1423,7 @@ module.exports.isBlankLine = function isBlankLine(test) {
"> <!--text-->", "> <!--text-->",
">><!--text-->" ">><!--text-->"
]; ];
blankLines.forEach((line) => test.ok(shared.isBlankLine(line), line)); blankLines.forEach((line) => test.ok(helpers.isBlankLine(line), line));
const nonBlankLines = [ const nonBlankLines = [
"text", "text",
" text ", " text ",
@ -1432,7 +1434,7 @@ module.exports.isBlankLine = function isBlankLine(test) {
"<!--", "<!--",
"-->" "-->"
]; ];
nonBlankLines.forEach((line) => test.ok(!shared.isBlankLine(line), line)); nonBlankLines.forEach((line) => test.ok(!helpers.isBlankLine(line), line));
test.done(); test.done();
}; };
@ -1449,7 +1451,7 @@ module.exports.includesSorted = function includesSorted(test) {
]; ];
inputs.forEach((input) => { inputs.forEach((input) => {
for (let i = 0; i <= 21; i++) { for (let i = 0; i <= 21; i++) {
test.equal(shared.includesSorted(input, i), input.includes(i)); test.equal(helpers.includesSorted(input, i), input.includes(i));
} }
}); });
test.done(); test.done();
@ -1475,9 +1477,9 @@ module.exports.trimLeftRight = function trimLeftRight(test) {
]; ];
test.expect(inputs.length * 2); test.expect(inputs.length * 2);
inputs.forEach(function forInput(input) { inputs.forEach(function forInput(input) {
test.equal(shared.trimLeft(input), input.trimLeft(), test.equal(helpers.trimLeft(input), input.trimLeft(),
"trimLeft incorrect for '" + input + "'"); "trimLeft incorrect for '" + input + "'");
test.equal(shared.trimRight(input), input.trimRight(), test.equal(helpers.trimRight(input), input.trimRight(),
"trimRight incorrect for '" + input + "'"); "trimRight incorrect for '" + input + "'");
}); });
test.done(); test.done();
@ -1571,7 +1573,7 @@ module.exports.forEachInlineCodeSpan = function forEachInlineCodeSpan(test) {
]; ];
testCases.forEach((testCase) => { testCases.forEach((testCase) => {
const [ input, expecteds ] = testCase; const [ input, expecteds ] = testCase;
shared.forEachInlineCodeSpan(input, (code, line, column, ticks) => { helpers.forEachInlineCodeSpan(input, (code, line, column, ticks) => {
const [ expectedCode, expectedLine, expectedColumn, expectedTicks ] = const [ expectedCode, expectedLine, expectedColumn, expectedTicks ] =
expecteds.shift(); expecteds.shift();
test.equal(code, expectedCode, input); test.equal(code, expectedCode, input);
@ -1611,9 +1613,9 @@ module.exports.configMultiple = function configMultiple(test) {
markdownlint.readConfig("./test/config/config-grandparent.json", markdownlint.readConfig("./test/config/config-grandparent.json",
function callback(err, actual) { function callback(err, actual) {
test.ifError(err); test.ifError(err);
const expected = shared.assign( const expected = helpers.assign(
shared.assign( helpers.assign(
shared.assign({}, require("./config/config-child.json")), helpers.assign({}, require("./config/config-child.json")),
require("./config/config-parent.json")), require("./config/config-parent.json")),
require("./config/config-grandparent.json")); require("./config/config-grandparent.json"));
delete expected.extends; delete expected.extends;
@ -1689,9 +1691,9 @@ module.exports.configMultipleYaml = function configMultipleYaml(test) {
[ require("js-yaml").safeLoad ], [ require("js-yaml").safeLoad ],
function callback(err, actual) { function callback(err, actual) {
test.ifError(err); test.ifError(err);
const expected = shared.assign( const expected = helpers.assign(
shared.assign( helpers.assign(
shared.assign({}, require("./config/config-child.json")), helpers.assign({}, require("./config/config-child.json")),
require("./config/config-parent.json")), require("./config/config-parent.json")),
require("./config/config-grandparent.json")); require("./config/config-grandparent.json"));
delete expected.extends; delete expected.extends;
@ -1707,9 +1709,9 @@ module.exports.configMultipleHybrid = function configMultipleHybrid(test) {
[ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ], [ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ],
function callback(err, actual) { function callback(err, actual) {
test.ifError(err); test.ifError(err);
const expected = shared.assign( const expected = helpers.assign(
shared.assign( helpers.assign(
shared.assign({}, require("./config/config-child.json")), helpers.assign({}, require("./config/config-child.json")),
require("./config/config-parent.json")), require("./config/config-parent.json")),
require("./config/config-grandparent.json")); require("./config/config-grandparent.json"));
delete expected.extends; delete expected.extends;
@ -1756,9 +1758,9 @@ module.exports.configMultipleSync = function configMultipleSync(test) {
test.expect(1); test.expect(1);
const actual = const actual =
markdownlint.readConfigSync("./test/config/config-grandparent.json"); markdownlint.readConfigSync("./test/config/config-grandparent.json");
const expected = shared.assign( const expected = helpers.assign(
shared.assign( helpers.assign(
shared.assign({}, require("./config/config-child.json")), helpers.assign({}, require("./config/config-child.json")),
require("./config/config-parent.json")), require("./config/config-parent.json")),
require("./config/config-grandparent.json")); require("./config/config-grandparent.json"));
delete expected.extends; delete expected.extends;
@ -1837,9 +1839,9 @@ module.exports.configMultipleYamlSync = function configMultipleYamlSync(test) {
test.expect(1); test.expect(1);
const actual = markdownlint.readConfigSync( const actual = markdownlint.readConfigSync(
"./test/config/config-grandparent.yaml", [ require("js-yaml").safeLoad ]); "./test/config/config-grandparent.yaml", [ require("js-yaml").safeLoad ]);
const expected = shared.assign( const expected = helpers.assign(
shared.assign( helpers.assign(
shared.assign({}, require("./config/config-child.json")), helpers.assign({}, require("./config/config-child.json")),
require("./config/config-parent.json")), require("./config/config-parent.json")),
require("./config/config-grandparent.json")); require("./config/config-grandparent.json"));
delete expected.extends; delete expected.extends;
@ -1853,9 +1855,9 @@ function configMultipleHybridSync(test) {
const actual = markdownlint.readConfigSync( const actual = markdownlint.readConfigSync(
"./test/config/config-grandparent-hybrid.yaml", "./test/config/config-grandparent-hybrid.yaml",
[ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ]); [ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ]);
const expected = shared.assign( const expected = helpers.assign(
shared.assign( helpers.assign(
shared.assign({}, require("./config/config-child.json")), helpers.assign({}, require("./config/config-child.json")),
require("./config/config-parent.json")), require("./config/config-parent.json")),
require("./config/config-grandparent.json")); require("./config/config-grandparent.json"));
delete expected.extends; delete expected.extends;
@ -2266,7 +2268,7 @@ module.exports.customRulesBadProperty = function customRulesBadProperty(test) {
].forEach(function forProperty(property) { ].forEach(function forProperty(property) {
const propertyName = property[0]; const propertyName = property[0];
property[1].forEach(function forPropertyValue(propertyValue) { property[1].forEach(function forPropertyValue(propertyValue) {
const badRule = shared.clone(customRules.anyBlockquote); const badRule = helpers.clone(customRules.anyBlockquote);
badRule[propertyName] = propertyValue; badRule[propertyName] = propertyValue;
const options = { const options = {
"customRules": [ badRule ] "customRules": [ badRule ]

View file

@ -3,6 +3,7 @@
"use strict"; "use strict";
const { URL } = require("url"); const { URL } = require("url");
const { filterTokens } = require("../../helpers");
module.exports = { module.exports = {
"names": [ "any-blockquote" ], "names": [ "any-blockquote" ],
@ -12,10 +13,8 @@ module.exports = {
"/blob/master/test/rules/any-blockquote.js" "/blob/master/test/rules/any-blockquote.js"
), ),
"tags": [ "test" ], "tags": [ "test" ],
"function": function rule(params, onError) { "function": (params, onError) => {
params.tokens.filter(function filterToken(token) { filterTokens(params, "blockquote_open", (blockquote) => {
return token.type === "blockquote_open";
}).forEach(function forToken(blockquote) {
const lines = blockquote.map[1] - blockquote.map[0]; const lines = blockquote.map[1] - blockquote.map[0];
onError({ onError({
"lineNumber": blockquote.lineNumber, "lineNumber": blockquote.lineNumber,

View file

@ -2,13 +2,15 @@
"use strict"; "use strict";
const { forEachLine, getLineMetadata } = require("../../helpers");
module.exports = { module.exports = {
"names": [ "every-n-lines" ], "names": [ "every-n-lines" ],
"description": "Rule that reports an error every N lines", "description": "Rule that reports an error every N lines",
"tags": [ "test" ], "tags": [ "test" ],
"function": function rule(params, onError) { "function": (params, onError) => {
const n = params.config.n || 2; const n = params.config.n || 2;
params.lines.forEach(function forLine(line, lineIndex) { forEachLine(getLineMetadata(params), (line, lineIndex) => {
const lineNumber = lineIndex + 1; const lineNumber = lineIndex + 1;
if ((lineNumber % n) === 0) { if ((lineNumber % n) === 0) {
onError({ onError({