Merge branch 'next' into main

This commit is contained in:
David Anson 2021-12-27 18:51:01 -08:00
commit 02707cf270
91 changed files with 3640 additions and 1502 deletions

View file

@ -1,6 +0,0 @@
demo/markdown-it.min.js
demo/markdownlint-browser.js
demo/markdownlint-browser.min.js
demo/markdownlint-rule-helpers-browser.js
demo/markdownlint-rule-helpers-browser.min.js
example/typescript/type-check.js

View file

@ -1,27 +1,60 @@
{
"parserOptions": {
"ecmaVersion": 2019
},
"env": {
"node": true,
"es6": true
},
"extends": [
"eslint:all",
"plugin:jsdoc/recommended"
],
"ignorePatterns": [
"demo/markdown-it.min.js",
"demo/markdownlint-browser.js",
"demo/markdownlint-browser.min.js",
"demo/markdownlint-rule-helpers-browser.js",
"demo/markdownlint-rule-helpers-browser.min.js",
"example/typescript/type-check.js",
"test-repos/"
],
"overrides": [
{
"files": [
"demo/*.js"
],
"env": {
"browser": true
},
"rules": {
"jsdoc/require-jsdoc": "off",
"unicorn/prefer-query-selector": "off",
"unicorn/prefer-add-event-listener": "off",
"no-console": "off",
"no-shadow": "off",
"no-var": "off"
}
},
{
"files": [
"example/*.js"
],
"rules": {
"node/no-missing-require": "off",
"node/no-extraneous-require": "off",
"no-console": "off",
"no-invalid-this": "off",
"no-shadow": "off",
"object-property-newline": "off"
}
}
],
"parserOptions": {
"ecmaVersion": 2020
},
"plugins": [
"jsdoc",
"node",
"unicorn"
],
"extends": [
"eslint:all",
"plugin:jsdoc/recommended"
],
"settings": {
"jsdoc": {
"preferredTypes": {
"object": "Object"
}
}
},
"reportUnusedDisableDirectives": true,
"rules": {
"array-bracket-spacing": ["error", "always"],
@ -29,7 +62,7 @@
"capitalized-comments": "off",
"complexity": "off",
"dot-location": ["error", "property"],
"func-style": ["error", "declaration"],
"func-style": "off",
"function-call-argument-newline": "off",
"function-paren-newline": "off",
"global-require": "off",
@ -72,7 +105,7 @@
"jsdoc/check-access": "error",
"jsdoc/check-alignment": "error",
"jsdoc/check-examples": "error",
"jsdoc/check-examples": "off",
"jsdoc/check-indentation": "error",
"jsdoc/check-line-alignment": "error",
"jsdoc/check-param-names": "error",
@ -174,11 +207,14 @@
"unicorn/no-array-method-this-argument": "error",
"unicorn/no-array-push-push": "error",
"unicorn/no-array-reduce": "error",
"unicorn/no-await-expression-member": "error",
"unicorn/no-console-spaces": "error",
"unicorn/no-document-cookie": "error",
"unicorn/no-empty-file": "error",
"unicorn/no-for-loop": "error",
"unicorn/no-hex-escape": "error",
"unicorn/no-instanceof-array": "error",
"unicorn/no-invalid-remove-event-listener": "error",
"unicorn/no-keyword-prefix": "off",
"unicorn/no-lonely-if": "error",
"unicorn/no-nested-ternary": "error",
@ -192,6 +228,7 @@
"unicorn/no-unreadable-array-destructuring": "error",
"unicorn/no-unsafe-regex": "off",
"unicorn/no-unused-properties": "error",
"unicorn/no-useless-fallback-in-spread": "error",
"unicorn/no-useless-length-check": "error",
"unicorn/no-useless-spread": "error",
"unicorn/no-useless-undefined": "error",
@ -205,12 +242,14 @@
"unicorn/prefer-array-index-of": "error",
"unicorn/prefer-array-some": "error",
"unicorn/prefer-at": "off",
"unicorn/prefer-code-point": "error",
"unicorn/prefer-date-now": "error",
"unicorn/prefer-default-parameters": "error",
"unicorn/prefer-dom-node-append": "error",
"unicorn/prefer-dom-node-dataset": "error",
"unicorn/prefer-dom-node-remove": "error",
"unicorn/prefer-dom-node-text-content": "error",
"unicorn/prefer-export-from": "error",
"unicorn/prefer-includes": "error",
"unicorn/prefer-keyboard-event-key": "error",
"unicorn/prefer-math-trunc": "error",
@ -241,37 +280,14 @@
"unicorn/require-number-to-fixed-digits-argument": "error",
"unicorn/require-post-message-target-origin": "error",
"unicorn/string-content": "error",
"unicorn/template-indent": "error",
"unicorn/throw-new-error": "error"
},
"overrides": [
{
"files": [
"demo/*.js"
],
"env": {
"browser": true
},
"rules": {
"jsdoc/require-jsdoc": "off",
"unicorn/prefer-query-selector": "off",
"unicorn/prefer-add-event-listener": "off",
"no-console": "off",
"no-shadow": "off",
"no-var": "off"
}
},
{
"files": [
"example/*.js"
],
"rules": {
"node/no-missing-require": "off",
"node/no-extraneous-require": "off",
"no-console": "off",
"no-invalid-this": "off",
"no-shadow": "off",
"object-property-newline": "off"
"settings": {
"jsdoc": {
"preferredTypes": {
"object": "Object"
}
}
]
}
}

View file

@ -27,8 +27,8 @@ jobs:
- name: Install Dependencies
run: npm install --no-package-lock
- name: Run All Validations
if: ${{ matrix.node-version != '10.x' && matrix.node-version != '12.x' }}
if: ${{ matrix.node-version != '12.x' }}
run: npm run ci
- name: Run Tests Only
if: ${{ matrix.node-version == '10.x' || matrix.node-version == '12.x' }}
if: ${{ matrix.node-version == '12.x' }}
run: npm run test

View file

@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
node-version: [12.x]
node-version: [16.x]
steps:
- uses: actions/checkout@v2

View file

@ -23,7 +23,7 @@ Try to break the new code now, or else it will get broken later.
Run tests before sending a pull request via `npm test` in the [usual manner](https://docs.npmjs.com/misc/scripts).
Tests should all pass on all platforms.
The test runner is [tape](https://www.npmjs.com/package/tape) and test cases are located in `test/markdownlint-test*.js`.
The test runner is [AVA](https://github.com/avajs/ava) and test cases are located in `test/markdownlint-test*.js`.
When running tests, `test/*.md` files are enumerated, linted, and fail if any violations are missing a corresponding `{MD###}` marker in the test file.
For example, the line `### Heading {MD001}` is expected to trigger the rule `MD001`.
For cases where the marker text can not be present on the same line, the syntax `{MD###:#}` can be used to include a line number.
@ -33,7 +33,8 @@ Lint before sending a pull request by running `npm run lint`.
There should be no issues.
Run a full continuous integration pass before sending a pull request via `npm run ci`.
Code coverage should remain at 100%.
Code coverage should always be 100%.
As part of a continuous integration run, generated files may get updated and fail the run - commit them to the repository and rerun continuous integration.
Pull requests should contain a single commit.
If necessary, squash multiple commits before creating the pull request and when making changes.
@ -43,9 +44,18 @@ Open pull requests against the `next` branch.
That's where the latest changes are staged for the next release.
Include the text "(fixes #??)" at the end of the commit message so the pull request will be associated with the relevant issue.
End commit messages with a period (`.`).
Do not include `package-lock.json` in the pull request.
Once accepted, the tag `fixed in next` will be added to the issue.
When the commit is merged to the main branch during the release process, the issue will be closed automatically.
(See [Closing issues using keywords](https://help.github.com/articles/closing-issues-using-keywords/) for details.)
Please refrain from using slang or meaningless placeholder words.
Sample content can be "text", "code", "heading", or the like.
Sample URLs should use [example.com](https://en.wikipedia.org/wiki/Example.com) which is safe for this purpose.
Profanity is not allowed.
In order to maintain the permissive MIT license this project uses, all contributions must be your own and released under that license.
Code you add should be an original work and should not be copied from elsewhere.
Taking code from a different project, Stack Overflow, or the like is not allowed.
The use of tools such as GitHub Copilot that generate code from other projects is not allowed.
Thank you!

View file

@ -33,8 +33,10 @@ and test cases came directly from that project.
### Related
* CLI
* [markdownlint-cli command-line interface for Node.js](https://github.com/igorshubovych/markdownlint-cli)
* [markdownlint-cli2 command-line interface for Node.js](https://github.com/DavidAnson/markdownlint-cli2)
* [markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli)
command-line interface for Node.js ([works with pre-commit](https://github.com/igorshubovych/markdownlint-cli#use-with-pre-commit))
* [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2)
command-line interface for Node.js ([works with pre-commit](https://github.com/DavidAnson/markdownlint-cli2#pre-commit))
* GitHub
* [GitHub Super-Linter Action](https://github.com/github/super-linter)
* [GitHub Actions problem matcher for markdownlint-cli](https://github.com/xt0rted/markdownlint-problem-matcher)
@ -102,6 +104,8 @@ playground for learning and exploring.
* **[MD046](doc/Rules.md#md046)** *code-block-style* - Code block style
* **[MD047](doc/Rules.md#md047)** *single-trailing-newline* - Files should end with a single newline character
* **[MD048](doc/Rules.md#md048)** *code-fence-style* - Code fence style
* **[MD049](doc/Rules.md#md049)** *emphasis-style* - Emphasis style should be consistent
* **[MD050](doc/Rules.md#md050)** *strong-style* - Strong style should be consistent
<!-- markdownlint-restore -->
@ -125,7 +129,7 @@ rules at once.
* **blockquote** - MD027, MD028
* **bullet** - MD004, MD005, MD006, MD007, MD032
* **code** - MD014, MD031, MD038, MD040, MD046, MD048
* **emphasis** - MD036, MD037
* **emphasis** - MD036, MD037, MD049, MD050
* **hard_tab** - MD010
* **headers** - MD001, MD002, MD003, MD018, MD019, MD020, MD021, MD022,
MD023, MD024, MD025, MD026, MD036, MD041, MD043
@ -163,10 +167,12 @@ appear in the final markup):
* Disable all rules: `<!-- markdownlint-disable -->`
* Enable all rules: `<!-- markdownlint-enable -->`
* Disable all rules for the next line only: `<!-- markdownlint-disable-next-line -->`
* Disable all rules for the next line only:
`<!-- markdownlint-disable-next-line -->`
* Disable one or more rules by name: `<!-- markdownlint-disable MD001 MD005 -->`
* Enable one or more rules by name: `<!-- markdownlint-enable MD001 MD005 -->`
* Disable one or more rules by name for the next line only: `<!-- markdownlint-disable-next-line MD001 MD005 -->`
* Disable one or more rules by name for the next line only:
`<!-- markdownlint-disable-next-line MD001 MD005 -->`
* Capture the current rule configuration: `<!-- markdownlint-capture -->`
* Restore the captured rule configuration: `<!-- markdownlint-restore -->`
@ -229,7 +235,7 @@ more rules for a file, the following more advanced syntax is supported:
For example:
```markdown
<!-- markdownlint-configure-file { "MD013": { "line_length": 70 } } -->
<!-- markdownlint-configure-file { "MD013": { "code_blocks": false } } -->
```
or
@ -859,7 +865,7 @@ const results = window.markdownlint.sync(options).toString();
## Examples
For ideas how to integrate `markdownlint` into your workflow, refer to the
following projects or one of the tools in the [Related section](#Related):
following projects or one of the tools in the [Related section](#related):
* [.NET Documentation](https://docs.microsoft.com/en-us/dotnet/) ([Search repository](https://github.com/dotnet/docs/search?q=markdownlint))
* [ally.js](https://allyjs.io/) ([Search repository](https://github.com/medialize/ally.js/search?q=markdownlint))
@ -870,6 +876,7 @@ following projects or one of the tools in the [Related section](#Related):
* [MDN Web Docs](https://developer.mozilla.org/) ([Search repository](https://github.com/mdn/content/search?q=markdownlint))
* [MkDocs](https://www.mkdocs.org/) ([Search repository](https://github.com/mkdocs/mkdocs/search?q=markdownlint))
* [Mocha](https://mochajs.org/) ([Search repository](https://github.com/mochajs/mocha/search?q=markdownlint))
* [Pi-hole documentation](https://docs.pi-hole.net) ([Search repository](https://github.com/pi-hole/docs/search?q=markdownlint))
* [Reactable](https://glittershark.github.io/reactable/) ([Search repository](https://github.com/glittershark/reactable/search?q=markdownlint))
* [Sinon.JS](https://sinonjs.org/) ([Search repository](https://github.com/sinonjs/sinon/search?q=markdownlint))
* [TestCafe](https://devexpress.github.io/testcafe/) ([Search repository](https://github.com/DevExpress/testcafe/search?q=markdownlint))
@ -968,6 +975,9 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for more information.
* 0.24.0 - Remove support for end-of-life Node version 10, add support for custom file system
module, improve MD010/MD011/MD037/MD043/MD044, improve TypeScript declaration file
and JSON schema, update dependencies.
* 0.25.0 - Add MD049/MD050 for consistent emphasis/strong style (both auto-fixable), improve
MD007/MD010/MD032/MD033/MD035/MD037/MD039, support asynchronous custom rules,
improve performance, improve CI process, reduce dependencies, update dependencies.
[npm-image]: https://img.shields.io/npm/v/markdownlint.svg
[npm-url]: https://www.npmjs.com/package/markdownlint

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,7 @@
{
"compilerOptions": {
"allowJs": true,
"outDir": "unused",
"resolveJsonModule": true
"outDir": "unused"
},
"include": [
"../lib/*.js"

View file

@ -22,7 +22,8 @@ function config(options) {
{
"loader": "ts-loader",
"options": {
"configFile": "../demo/tsconfig.json"
"configFile": "../demo/tsconfig.json",
"transpileOnly": true
}
}
]
@ -43,7 +44,6 @@ function config(options) {
"resolve": {
"fallback": {
"fs": false,
"os": false,
"path": false,
"util": false
}

View file

@ -41,7 +41,8 @@ A rule is implemented as an `Object` with one optional and four required propert
- `description` is a required `String` value that describes the rule in output messages.
- `information` is an optional (absolute) `URL` of a link to more information about the rule.
- `tags` is a required `Array` of `String` values that groups related rules for easier customization.
- `function` is a required synchronous `Function` that implements the rule and is passed two parameters:
- `asynchronous` is an optional `Boolean` value that indicates whether the rule returns a `Promise` and runs asynchronously.
- `function` is a required `Function` that implements the rule and is passed two parameters:
- `params` is an `Object` with properties that describe the content being analyzed:
- `name` is a `String` that identifies the input file/string.
- `tokens` is an `Array` of [`markdown-it` `Token` objects](https://markdown-it.github.io/markdown-it/#Token)
@ -51,7 +52,7 @@ A rule is implemented as an `Object` with one optional and four required propert
- `config` is an `Object` corresponding to the rule's entry in `options.config` (if present).
- `onError` is a function that takes a single `Object` parameter with one required and four optional properties:
- `lineNumber` is a required `Number` specifying the 1-based line number of the error.
- `details` is an optional `String` with information about what caused the error.
- `detail` is an optional `String` with information about what caused the error.
- `context` is an optional `String` with relevant text surrounding the error location.
- `range` is an optional `Array` with two `Number` values identifying the 1-based column and length of the error.
- `fixInfo` is an optional `Object` with information about how to fix the error (all properties are optional, but
@ -66,15 +67,25 @@ A rule is implemented as an `Object` with one optional and four required propert
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).
### Asynchronous Rules
If a rule needs to perform asynchronous operations (such as fetching a network resource), it can specify the value `true` for its `asynchronous` property.
Asynchronous rules should return a `Promise` from their `function` implementation that is resolved when the rule completes.
(The value passed to `resolve(...)` is ignored.)
Linting violations from asynchronous rules are reported via the `onError` function just like for synchronous rules.
**Note**: Asynchronous rules cannot be referenced in a synchronous calling context (i.e., `markdownlint.sync(...)`).
Attempting to do so throws an exception.
## Examples
- [Simple rules used by the project's test cases](../test/rules)
- [Code for all `markdownlint` built-in rules](../lib)
- [Package configuration for publishing to npm](../test/rules/npm)
- Packages should export a single rule object or an `Array` of rule objects
- [Custom rules from the Microsoft/vscode-docs-authoring repository](https://github.com/microsoft/vscode-docs-authoring/tree/master/packages/docs-linting/markdownlint-custom-rules)
- [Custom rules from the Microsoft/vscode-docs-authoring repository](https://github.com/microsoft/vscode-docs-authoring/tree/main/packages/docs-linting/markdownlint-custom-rules)
- [Custom rules from the axibase/docs-util repository](https://github.com/axibase/docs-util/tree/master/linting-rules)
- [Custom rules from the webhintio/hint repository](https://github.com/webhintio/hint/blob/master/scripts/lint-markdown.js)
- [Custom rules from the webhintio/hint repository](https://github.com/webhintio/hint/blob/main/scripts/lint-markdown.js)
## References

View file

@ -292,7 +292,11 @@ Tags: bullet, ul, indentation
Aliases: ul-indent
Parameters: indent, start_indented (number; default 2, boolean; default false)
<!-- markdownlint-disable line-length -->
Parameters: indent, start_indented, start_indent (number; default 2, boolean; default false, number; defaults to indent)
<!-- markdownlint-restore -->
Fixable: Most violations can be fixed by tooling
@ -319,7 +323,9 @@ rule).
The `start_indented` parameter allows the first level of lists to be indented by
the configured number of spaces rather than starting at zero (the inverse of
MD006).
MD006). The `start_indent` parameter allows the first level of lists to be indented
by a different number of spaces than the rest (ignored when `start_indented` is not
set).
Rationale: Indenting by 2 spaces allows the content of a nested list to be in
line with the start of the content of the parent list when a single space is
@ -418,12 +424,14 @@ Some text
* Spaces used to indent the list item instead
```
You have the option to exclude this rule for code blocks. To do so, set the
`code_blocks` parameter to `false`. Code blocks are included by default since
handling of tabs by tools is often inconsistent (ex: using 4 vs. 8 spaces).
You have the option to exclude this rule for code blocks and spans. To do so,
set the `code_blocks` parameter to `false`. Code blocks and spans are included
by default since handling of tabs by Markdown tools can be inconsistent (e.g.,
using 4 vs. 8 spaces).
If you would like the fixer to change tabs to x spaces, then configure the `spaces_per_tab`
parameter to the number x. The default value would be 1.
By default, violations of this rule are fixed by replacing the tab with 1 space
character. To use a different number of spaces, set the `spaces_per_tab`
parameter to the desired value.
Rationale: Hard tabs are often rendered inconsistently by different editors and
can be harder to work with than spaces.
@ -1331,20 +1339,20 @@ For more information, see <https://www.example.com/>.
```
Note: To use a bare URL without it being converted into a link, enclose it in
a code block, otherwise in some markdown parsers it _will_ be converted:
a code block, otherwise in some markdown parsers it *will* be converted:
```markdown
`https://www.example.com`
```
Note: The following scenario does _not_ trigger this rule to avoid conflicts
Note: The following scenario does *not* trigger this rule to avoid conflicts
with `MD011`/`no-reversed-links`:
```markdown
[https://www.example.com]
```
The use of quotes around a bare link will _not_ trigger this rule, either:
The use of quotes around a bare link will *not* trigger this rule, either:
```markdown
"https://www.example.com"
@ -1362,8 +1370,8 @@ Tags: hr
Aliases: hr-style
Parameters: style ("consistent", "---", "***", or other string specifying the
horizontal rule; default "consistent")
Parameters: style ("consistent", "---", "***", "___", or other string specifying
the horizontal rule; default "consistent")
This rule is triggered when inconsistent styles of horizontal rules are used
in the document:
@ -1762,7 +1770,8 @@ the proper capitalization, specify the desired letter case in the `names` array:
]
```
Set the `code_blocks` parameter to `false` to disable this rule for code blocks.
Set the `code_blocks` parameter to `false` to disable this rule for code blocks
and spans.
Rationale: Incorrect capitalization of proper names is usually a mistake.
@ -1911,3 +1920,67 @@ The configured list style can be a specific symbol to use (backtick, tilde), or
can require that usage be consistent within the document.
Rationale: Consistent formatting makes it easier to understand a document.
<a name="md049"></a>
## MD049 - Emphasis style should be consistent
Tags: emphasis
Aliases: emphasis-style
Parameters: style ("consistent", "asterisk", "underscore"; default "consistent")
Fixable: Most violations can be fixed by tooling
This rule is triggered when the symbols used in the document for emphasis do not
match the configured emphasis style:
```markdown
*Text*
_Text_
```
To fix this issue, use the configured emphasis style throughout the document:
```markdown
*Text*
*Text*
```
The configured emphasis style can be a specific symbol to use ("asterisk",
"underscore"), or can require that usage be consistent within the document.
Rationale: Consistent formatting makes it easier to understand a document.
<a name="md050"></a>
## MD050 - Strong style should be consistent
Tags: emphasis
Aliases: strong-style
Parameters: style ("consistent", "asterisk", "underscore"; default "consistent")
Fixable: Most violations can be fixed by tooling
This rule is triggered when the symbols used in the document for strong do not
match the configured strong style:
```markdown
**Text**
__Text__
```
To fix this issue, use the configured strong style throughout the document:
```markdown
**Text**
**Text**
```
The configured strong style can be a specific symbol to use ("asterisk",
"underscore"), or can require that usage be consistent within the document.
Rationale: Consistent formatting makes it easier to understand a document.

View file

@ -139,7 +139,7 @@ const testRule = {
let ruleOnErrorInfo: markdownlint.RuleOnErrorInfo;
ruleOnErrorInfo = {
"lineNumber": 1,
"details": "details",
"detail": "detail",
"context": "context",
"range": [ 1, 2 ],
"fixInfo": {

View file

@ -2,8 +2,6 @@
"use strict";
const os = require("os");
// Regular expression for matching common newline characters
// See NEWLINES_RE in markdown-it/lib/rules_core/normalize.js
const newLineRe = /\r\n?|\n/g;
@ -63,12 +61,20 @@ module.exports.isObject = function isObject(obj) {
};
// Returns true iff the input line is blank (no content)
// Example: Contains nothing, whitespace, or comments
const blankLineRe = />|(?:<!--.*?-->)/g;
// Example: Contains nothing, whitespace, or comment (unclosed start/end okay)
module.exports.isBlankLine = function isBlankLine(line) {
// Call to String.replace follows best practices and is not a security check
// False-positive for js/incomplete-multi-character-sanitization
return !line || !line.trim() || !line.replace(blankLineRe, "").trim();
return (
!line ||
!line.trim() ||
!line
.replace(/<!--.*?-->/g, "")
.replace(/<!--.*$/g, "")
.replace(/^.*-->/g, "")
.replace(/>/g, "")
.trim()
);
};
/**
@ -181,6 +187,22 @@ module.exports.fencedCodeBlockStyleFor =
}
};
/**
* Return the string representation of a emphasis or strong markup character.
*
* @param {string} markup Emphasis or strong string.
* @returns {string} String representation.
*/
module.exports.emphasisOrStrongStyleFor =
function emphasisOrStrongStyleFor(markup) {
switch (markup[0]) {
case "*":
return "asterisk";
default:
return "underscore";
}
};
/**
* Return the number of characters of indent for a token.
*
@ -252,6 +274,7 @@ function isMathBlock(token) {
!token.type.endsWith("_end")
);
}
module.exports.isMathBlock = isMathBlock;
// Get line metadata array
module.exports.getLineMetadata = function getLineMetadata(params) {
@ -293,14 +316,20 @@ module.exports.getLineMetadata = function getLineMetadata(params) {
return lineMetadata;
};
// Calls the provided function for each line (with context)
module.exports.forEachLine = function forEachLine(lineMetadata, handler) {
/**
* Calls the provided function for each line.
*
* @param {Object} lineMetadata Line metadata object.
* @param {Function} handler Function taking (line, lineIndex, inCode, onFence,
* inTable, inItem, inBreak, inMath).
* @returns {void}
*/
function forEachLine(lineMetadata, handler) {
lineMetadata.forEach(function forMetadata(metadata) {
// Parameters:
// line, lineIndex, inCode, onFence, inTable, inItem, inBreak, inMath
handler(...metadata);
});
};
}
module.exports.forEachLine = forEachLine;
// Returns (nested) lists as a flat array (in order)
module.exports.flattenLists = function flattenLists(tokens) {
@ -311,10 +340,6 @@ module.exports.flattenLists = function flattenLists(tokens) {
const nestingStack = [];
let lastWithMap = { "map": [ 0, 1 ] };
tokens.forEach((token) => {
if (isMathBlock(token) && token.map[1]) {
// markdown-it-texmath plugin does not account for math_block_end
token.map[1]++;
}
if ((token.type === "bullet_list_open") ||
(token.type === "ordered_list_open")) {
// Save current context and start a new one
@ -386,7 +411,8 @@ module.exports.forEachHeading = function forEachHeading(params, handler) {
* Calls the provided function for each inline code span's content.
*
* @param {string} input Markdown content.
* @param {Function} handler Callback function.
* @param {Function} handler Callback function taking (code, lineIndex,
* columnIndex, ticks).
* @returns {void}
*/
function forEachInlineCodeSpan(input, handler) {
@ -521,26 +547,39 @@ module.exports.addErrorContext = function addErrorContext(
};
/**
* Returns an array of code span ranges.
* Returns an array of code block and span content ranges.
*
* @param {string[]} lines Lines to scan for code span ranges.
* @returns {number[][]} Array of ranges (line, index, length).
* @param {Object} params RuleParams instance.
* @param {Object} lineMetadata Line metadata object.
* @returns {number[][]} Array of ranges (lineIndex, columnIndex, length).
*/
module.exports.inlineCodeSpanRanges = (lines) => {
module.exports.codeBlockAndSpanRanges = (params, lineMetadata) => {
const exclusions = [];
forEachInlineCodeSpan(
lines.join("\n"),
(code, lineIndex, columnIndex) => {
const codeLines = code.split(newLineRe);
// eslint-disable-next-line unicorn/no-for-loop
for (let i = 0; i < codeLines.length; i++) {
exclusions.push(
[ lineIndex + i, columnIndex, codeLines[i].length ]
);
columnIndex = 0;
}
// Add code block ranges (excludes fences)
forEachLine(lineMetadata, (line, lineIndex, inCode, onFence) => {
if (inCode && !onFence) {
exclusions.push([ lineIndex, 0, line.length ]);
}
);
});
// Add code span ranges (excludes ticks)
filterTokens(params, "inline", (token) => {
if (token.children.some((child) => child.type === "code_inline")) {
const tokenLines = params.lines.slice(token.map[0], token.map[1]);
forEachInlineCodeSpan(
tokenLines.join("\n"),
(code, lineIndex, columnIndex) => {
const codeLines = code.split(newLineRe);
for (const [ i, line ] of codeLines.entries()) {
exclusions.push([
token.lineNumber - 1 + lineIndex + i,
i ? 0 : columnIndex,
line.length
]);
}
}
);
}
});
return exclusions;
};
@ -596,6 +635,18 @@ module.exports.frontMatterHasTitle =
function emphasisMarkersInContent(params) {
const { lines } = params;
const byLine = new Array(lines.length);
// Search links
lines.forEach((tokenLine, tokenLineIndex) => {
const inLine = [];
let linkMatch = null;
while ((linkMatch = linkRe.exec(tokenLine))) {
let markerMatch = null;
while ((markerMatch = emphasisMarkersRe.exec(linkMatch[0]))) {
inLine.push(linkMatch.index + markerMatch.index);
}
}
byLine[tokenLineIndex] = inLine;
});
// Search code spans
filterTokens(params, "inline", (token) => {
const { children, lineNumber, map } = token;
@ -606,31 +657,19 @@ function emphasisMarkersInContent(params) {
(code, lineIndex, column, tickCount) => {
const codeLines = code.split(newLineRe);
codeLines.forEach((codeLine, codeLineIndex) => {
const byLineIndex = lineNumber - 1 + lineIndex + codeLineIndex;
const inLine = byLine[byLineIndex];
const codeLineOffset = codeLineIndex ? 0 : column - 1 + tickCount;
let match = null;
while ((match = emphasisMarkersRe.exec(codeLine))) {
const byLineIndex = lineNumber - 1 + lineIndex + codeLineIndex;
const inLine = byLine[byLineIndex] || [];
const codeLineOffset = codeLineIndex ? 0 : column - 1 + tickCount;
inLine.push(codeLineOffset + match.index);
byLine[byLineIndex] = inLine;
}
byLine[byLineIndex] = inLine;
});
}
);
}
});
// Search links
lines.forEach((tokenLine, tokenLineIndex) => {
let linkMatch = null;
while ((linkMatch = linkRe.exec(tokenLine))) {
let markerMatch = null;
while ((markerMatch = emphasisMarkersRe.exec(linkMatch[0]))) {
const inLine = byLine[tokenLineIndex] || [];
inLine.push(linkMatch.index + markerMatch.index);
byLine[tokenLineIndex] = inLine;
}
}
});
return byLine;
}
module.exports.emphasisMarkersInContent = emphasisMarkersInContent;
@ -639,9 +678,10 @@ module.exports.emphasisMarkersInContent = emphasisMarkersInContent;
* Gets the most common line ending, falling back to the platform default.
*
* @param {string} input Markdown content to analyze.
* @param {string} [platform] Platform identifier (process.platform).
* @returns {string} Preferred line ending.
*/
function getPreferredLineEnding(input) {
function getPreferredLineEnding(input, platform) {
let cr = 0;
let lf = 0;
let crlf = 0;
@ -662,7 +702,8 @@ function getPreferredLineEnding(input) {
});
let preferredLineEnding = null;
if (!cr && !lf && !crlf) {
preferredLineEnding = os.EOL;
preferredLineEnding =
((platform || process.platform) === "win32") ? "\r\n" : "\n";
} else if ((lf >= crlf) && (lf >= cr)) {
preferredLineEnding = "\n";
} else if (crlf >= cr) {
@ -777,3 +818,80 @@ module.exports.applyFixes = function applyFixes(input, errors) {
// Return corrected input
return lines.filter((line) => line !== null).join(lineEnding);
};
/**
* Gets the range and fixInfo values for reporting an error if the expected
* text is found on the specified line.
*
* @param {string[]} lines Lines of Markdown content.
* @param {number} lineIndex Line index to check.
* @param {string} search Text to search for.
* @param {string} replace Text to replace with.
* @returns {Object} Range and fixInfo wrapper.
*/
function getRangeAndFixInfoIfFound(lines, lineIndex, search, replace) {
let range = null;
let fixInfo = null;
const searchIndex = lines[lineIndex].indexOf(search);
if (searchIndex !== -1) {
const column = searchIndex + 1;
const length = search.length;
range = [ column, length ];
fixInfo = {
"editColumn": column,
"deleteCount": length,
"insertText": replace
};
}
return {
range,
fixInfo
};
}
module.exports.getRangeAndFixInfoIfFound = getRangeAndFixInfoIfFound;
/**
* Gets the next (subsequent) child token if it is of the expected type.
*
* @param {Object} parentToken Parent token.
* @param {Object} childToken Child token basis.
* @param {string} nextType Token type of next token.
* @param {string} nextNextType Token type of next-next token.
* @returns {Object} Next token.
*/
function getNextChildToken(parentToken, childToken, nextType, nextNextType) {
const { children } = parentToken;
const index = children.indexOf(childToken);
if (
(index !== -1) &&
(children.length > index + 2) &&
(children[index + 1].type === nextType) &&
(children[index + 2].type === nextNextType)
) {
return children[index + 1];
}
return null;
}
module.exports.getNextChildToken = getNextChildToken;
/**
* Calls Object.freeze() on an object and its children.
*
* @param {Object} obj Object to deep freeze.
* @returns {Object} Object passed to the function.
*/
function deepFreeze(obj) {
const pending = [ obj ];
let current = null;
while ((current = pending.shift())) {
Object.freeze(current);
for (const name of Object.getOwnPropertyNames(current)) {
const value = current[name];
if (value && (typeof value === "object")) {
pending.push(value);
}
}
}
return obj;
}
module.exports.deepFreeze = deepFreeze;

View file

@ -1,6 +1,6 @@
{
"name": "markdownlint-rule-helpers",
"version": "0.15.0",
"version": "0.16.0",
"description": "A collection of markdownlint helper functions for custom rules",
"main": "helpers.js",
"author": "David Anson (https://dlaa.me/)",

View file

@ -2,6 +2,14 @@
"use strict";
let codeBlockAndSpanRanges = null;
module.exports.codeBlockAndSpanRanges = (value) => {
if (value) {
codeBlockAndSpanRanges = value;
}
return codeBlockAndSpanRanges;
};
let flattenedLists = null;
module.exports.flattenedLists = (value) => {
if (value) {
@ -10,14 +18,6 @@ module.exports.flattenedLists = (value) => {
return flattenedLists;
};
let inlineCodeSpanRanges = null;
module.exports.inlineCodeSpanRanges = (value) => {
if (value) {
inlineCodeSpanRanges = value;
}
return inlineCodeSpanRanges;
};
let lineMetadata = null;
module.exports.lineMetadata = (value) => {
if (value) {
@ -27,7 +27,7 @@ module.exports.lineMetadata = (value) => {
};
module.exports.clear = () => {
codeBlockAndSpanRanges = null;
flattenedLists = null;
inlineCodeSpanRanges = null;
lineMetadata = null;
};

7
lib/constants.js Normal file
View file

@ -0,0 +1,7 @@
// @ts-check
"use strict";
module.exports.deprecatedRuleNames = [ "MD002", "MD006" ];
module.exports.homepage = "https://github.com/DavidAnson/markdownlint";
module.exports.version = "0.25.0";

View file

@ -206,9 +206,9 @@ type RuleOnErrorInfo = {
*/
lineNumber: number;
/**
* Details about the error.
* Detail about the error.
*/
details?: string;
detail?: string;
/**
* Context for the error.
*/
@ -263,6 +263,10 @@ type Rule = {
* Rule tag(s).
*/
tags: string[];
/**
* True if asynchronous.
*/
asynchronous?: boolean;
/**
* Rule implementation.
*/

View file

@ -5,6 +5,7 @@
const path = require("path");
const { promisify } = require("util");
const markdownIt = require("markdown-it");
const { deprecatedRuleNames } = require("./constants");
const rules = require("./rules");
const helpers = require("../helpers");
const cache = require("./cache");
@ -14,15 +15,14 @@ const cache = require("./cache");
const dynamicRequire = (typeof __non_webpack_require__ === "undefined") ? require : /* c8 ignore next */ __non_webpack_require__;
// Capture native require implementation for dynamic loading of modules
const deprecatedRuleNames = [ "MD002", "MD006" ];
/**
* Validate the list of rules for structure and reuse.
*
* @param {Rule[]} ruleList List of rules.
* @param {boolean} synchronous Whether to execute synchronously.
* @returns {string} Error message if validation fails.
*/
function validateRuleList(ruleList) {
function validateRuleList(ruleList, synchronous) {
let result = null;
if (ruleList.length === rules.length) {
// No need to validate if only using built-in rules
@ -62,6 +62,19 @@ function validateRuleList(ruleList) {
) {
result = newError("information");
}
if (
!result &&
(rule.asynchronous !== undefined) &&
(typeof rule.asynchronous !== "boolean")
) {
result = newError("asynchronous");
}
if (!result && rule.asynchronous && synchronous) {
result = new Error(
"Custom rule " + rule.names.join("/") + " at index " + customIndex +
" is asynchronous and can not be used in a synchronous context."
);
}
if (!result) {
rule.names.forEach(function forName(name) {
const nameUpper = name.toUpperCase();
@ -182,27 +195,21 @@ function removeFrontMatter(content, frontMatter) {
* @returns {void}
*/
function annotateTokens(tokens, lines) {
let tableMap = null;
let trMap = null;
tokens.forEach(function forToken(token) {
// Handle missing maps for table head/body
if (
(token.type === "thead_open") ||
(token.type === "tbody_open")
) {
tableMap = [ ...token.map ];
} else if (
(token.type === "tr_close") &&
tableMap
) {
tableMap[0]++;
} else if (
(token.type === "thead_close") ||
(token.type === "tbody_close")
) {
tableMap = null;
// Provide missing maps for table content
if (token.type === "tr_open") {
trMap = token.map;
} else if (token.type === "tr_close") {
trMap = null;
}
if (tableMap && !token.map) {
token.map = [ ...tableMap ];
if (!token.map && trMap) {
token.map = [ ...trMap ];
}
// Adjust maps for math blocks
if (helpers.isMathBlock(token) && token.map[1]) {
// markdown-it-texmath plugin does not account for math_block_end
token.map[1]++;
}
// Update token metadata
if (token.map) {
@ -335,8 +342,7 @@ function getEnabledRulesPerLineNumber(
const enabledRulesPerLineNumber = new Array(1 + frontMatterLines.length);
// Helper functions
// eslint-disable-next-line jsdoc/require-jsdoc
function handleInlineConfig(perLine, forEachMatch, forEachLine) {
const input = perLine ? lines : [ lines.join("\n") ];
function handleInlineConfig(input, forEachMatch, forEachLine) {
input.forEach((line, lineIndex) => {
if (!noInlineConfig) {
let match = null;
@ -367,6 +373,7 @@ function getEnabledRulesPerLineNumber(
}
// eslint-disable-next-line jsdoc/require-jsdoc
function applyEnableDisable(action, parameter, state) {
state = { ...state };
const enabled = (action.startsWith("ENABLE"));
const items = parameter ?
parameter.trim().toUpperCase().split(/\s+/) :
@ -376,40 +383,42 @@ function getEnabledRulesPerLineNumber(
state[ruleName] = enabled;
});
});
return state;
}
// eslint-disable-next-line jsdoc/require-jsdoc
function enableDisableFile(action, parameter) {
if ((action === "ENABLE-FILE") || (action === "DISABLE-FILE")) {
applyEnableDisable(action, parameter, enabledRules);
enabledRules = applyEnableDisable(action, parameter, enabledRules);
}
}
// eslint-disable-next-line jsdoc/require-jsdoc
function captureRestoreEnableDisable(action, parameter) {
if (action === "CAPTURE") {
capturedRules = { ...enabledRules };
capturedRules = enabledRules;
} else if (action === "RESTORE") {
enabledRules = { ...capturedRules };
enabledRules = capturedRules;
} else if ((action === "ENABLE") || (action === "DISABLE")) {
enabledRules = { ...enabledRules };
applyEnableDisable(action, parameter, enabledRules);
enabledRules = applyEnableDisable(action, parameter, enabledRules);
}
}
// eslint-disable-next-line jsdoc/require-jsdoc
function updateLineState() {
enabledRulesPerLineNumber.push({ ...enabledRules });
enabledRulesPerLineNumber.push(enabledRules);
}
// eslint-disable-next-line jsdoc/require-jsdoc
function disableNextLine(action, parameter, lineNumber) {
if (action === "DISABLE-NEXT-LINE") {
applyEnableDisable(
action,
parameter,
enabledRulesPerLineNumber[lineNumber + 1] || {}
);
const nextLineNumber = frontMatterLines.length + lineNumber + 1;
enabledRulesPerLineNumber[nextLineNumber] =
applyEnableDisable(
action,
parameter,
enabledRulesPerLineNumber[nextLineNumber] || {}
);
}
}
// Handle inline comments
handleInlineConfig(false, configureFile);
handleInlineConfig([ lines.join("\n") ], configureFile);
const effectiveConfig = getEffectiveConfig(
ruleList, config, aliasToRuleNames);
ruleList.forEach((rule) => {
@ -418,9 +427,9 @@ function getEnabledRulesPerLineNumber(
enabledRules[ruleName] = !!effectiveConfig[ruleName];
});
capturedRules = enabledRules;
handleInlineConfig(true, enableDisableFile);
handleInlineConfig(true, captureRestoreEnableDisable, updateLineState);
handleInlineConfig(true, disableNextLine);
handleInlineConfig(lines, enableDisableFile);
handleInlineConfig(lines, captureRestoreEnableDisable, updateLineState);
handleInlineConfig(lines, disableNextLine);
// Return results
return {
effectiveConfig,
@ -428,38 +437,6 @@ function getEnabledRulesPerLineNumber(
};
}
/**
* Compare function for Array.prototype.sort for ascending order of errors.
*
* @param {LintError} a First error.
* @param {LintError} b Second error.
* @returns {number} Positive value if a>b, negative value if b<a, 0 otherwise.
*/
function lineNumberComparison(a, b) {
return a.lineNumber - b.lineNumber;
}
/**
* Filter function to include everything.
*
* @returns {boolean} True.
*/
function filterAllValues() {
return true;
}
/**
* Function to return unique values from a sorted errors array.
*
* @param {LintError} value Error instance.
* @param {number} index Index in array.
* @param {LintError[]} array Array of errors.
* @returns {boolean} Filter value.
*/
function uniqueFilterForSortedErrors(value, index, array) {
return (index === 0) || (value.lineNumber > array[index - 1].lineNumber);
}
/**
* Lints a string containing Markdown content.
*
@ -509,28 +486,28 @@ function lintContent(
);
// Create parameters for rules
const params = {
name,
tokens,
lines,
frontMatterLines
"name": helpers.deepFreeze(name),
"tokens": helpers.deepFreeze(tokens),
"lines": helpers.deepFreeze(lines),
"frontMatterLines": helpers.deepFreeze(frontMatterLines)
};
cache.lineMetadata(helpers.getLineMetadata(params));
cache.flattenedLists(helpers.flattenLists(params.tokens));
cache.inlineCodeSpanRanges(helpers.inlineCodeSpanRanges(params.lines));
cache.codeBlockAndSpanRanges(
helpers.codeBlockAndSpanRanges(params, cache.lineMetadata())
);
// Function to run for each rule
const result = (resultVersion === 0) ? {} : [];
let results = [];
// eslint-disable-next-line jsdoc/require-jsdoc
function forRule(rule) {
// Configure rule
const ruleNameFriendly = rule.names[0];
const ruleName = ruleNameFriendly.toUpperCase();
const ruleName = rule.names[0].toUpperCase();
params.config = effectiveConfig[ruleName];
// eslint-disable-next-line jsdoc/require-jsdoc
function throwError(property) {
throw new Error(
"Property '" + property + "' of onError parameter is incorrect.");
}
const errors = [];
// eslint-disable-next-line jsdoc/require-jsdoc
function onError(errorInfo) {
if (!errorInfo ||
@ -539,6 +516,10 @@ function lintContent(
(errorInfo.lineNumber > lines.length)) {
throwError("lineNumber");
}
const lineNumber = errorInfo.lineNumber + frontMatterLines.length;
if (!enabledRulesPerLineNumber[lineNumber][ruleName]) {
return;
}
if (errorInfo.detail &&
!helpers.isString(errorInfo.detail)) {
throwError("detail");
@ -549,12 +530,12 @@ function lintContent(
}
if (errorInfo.range &&
(!Array.isArray(errorInfo.range) ||
(errorInfo.range.length !== 2) ||
!helpers.isNumber(errorInfo.range[0]) ||
(errorInfo.range[0] < 1) ||
!helpers.isNumber(errorInfo.range[1]) ||
(errorInfo.range[1] < 1) ||
((errorInfo.range[0] + errorInfo.range[1] - 1) >
(errorInfo.range.length !== 2) ||
!helpers.isNumber(errorInfo.range[0]) ||
(errorInfo.range[0] < 1) ||
!helpers.isNumber(errorInfo.range[1]) ||
(errorInfo.range[1] < 1) ||
((errorInfo.range[0] + errorInfo.range[1] - 1) >
lines[errorInfo.lineNumber - 1].length))) {
throwError("range");
}
@ -599,78 +580,114 @@ function lintContent(
cleanFixInfo.insertText = fixInfo.insertText;
}
}
errors.push({
"lineNumber": errorInfo.lineNumber + frontMatterLines.length,
"detail": errorInfo.detail || null,
"context": errorInfo.context || null,
"range": errorInfo.range ? [ ...errorInfo.range ] : null,
results.push({
lineNumber,
"ruleName": rule.names[0],
"ruleNames": rule.names,
"ruleDescription": rule.description,
"ruleInformation": rule.information ? rule.information.href : null,
"errorDetail": errorInfo.detail || null,
"errorContext": errorInfo.context || null,
"errorRange": errorInfo.range ? [ ...errorInfo.range ] : null,
"fixInfo": fixInfo ? cleanFixInfo : null
});
}
// Call (possibly external) rule function
if (handleRuleFailures) {
try {
rule.function(params, onError);
} catch (error) {
onError({
"lineNumber": 1,
"detail": `This rule threw an exception: ${error.message}`
});
// Call (possibly external) rule function to report errors
const catchCallsOnError = (error) => onError({
"lineNumber": 1,
"detail": `This rule threw an exception: ${error.message || error}`
});
const invokeRuleFunction = () => rule.function(params, onError);
if (rule.asynchronous) {
// Asynchronous rule, ensure it returns a Promise
const ruleFunctionPromise =
Promise.resolve().then(invokeRuleFunction);
return handleRuleFailures ?
ruleFunctionPromise.catch(catchCallsOnError) :
ruleFunctionPromise;
}
// Synchronous rule
try {
invokeRuleFunction();
} catch (error) {
if (handleRuleFailures) {
catchCallsOnError(error);
} else {
throw error;
}
}
return null;
}
// eslint-disable-next-line jsdoc/require-jsdoc
function formatResults() {
// Sort results by rule name by line number
results.sort((a, b) => (
a.ruleName.localeCompare(b.ruleName) ||
a.lineNumber - b.lineNumber
));
if (resultVersion < 3) {
// Remove fixInfo and multiple errors for the same rule and line number
const noPrevious = {
"ruleName": null,
"lineNumber": -1
};
results = results.filter((error, index, array) => {
delete error.fixInfo;
const previous = array[index - 1] || noPrevious;
return (
(error.ruleName !== previous.ruleName) ||
(error.lineNumber !== previous.lineNumber)
);
});
}
if (resultVersion === 0) {
// Return a dictionary of rule->[line numbers]
const dictionary = {};
for (const error of results) {
const ruleLines = dictionary[error.ruleName] || [];
ruleLines.push(error.lineNumber);
dictionary[error.ruleName] = ruleLines;
}
// @ts-ignore
results = dictionary;
} else if (resultVersion === 1) {
// Use ruleAlias instead of ruleNames
for (const error of results) {
error.ruleAlias = error.ruleNames[1] || error.ruleName;
delete error.ruleNames;
}
} else {
rule.function(params, onError);
}
// Record any errors (significant performance benefit from length check)
if (errors.length > 0) {
errors.sort(lineNumberComparison);
const filteredErrors = errors
.filter((resultVersion === 3) ?
filterAllValues :
uniqueFilterForSortedErrors)
.filter(function removeDisabledRules(error) {
return enabledRulesPerLineNumber[error.lineNumber][ruleName];
})
.map(function formatResults(error) {
if (resultVersion === 0) {
return error.lineNumber;
}
const errorObject = {};
errorObject.lineNumber = error.lineNumber;
if (resultVersion === 1) {
errorObject.ruleName = ruleNameFriendly;
errorObject.ruleAlias = rule.names[1] || rule.names[0];
} else {
errorObject.ruleNames = rule.names;
}
errorObject.ruleDescription = rule.description;
errorObject.ruleInformation =
rule.information ? rule.information.href : null;
errorObject.errorDetail = error.detail;
errorObject.errorContext = error.context;
errorObject.errorRange = error.range;
if (resultVersion === 3) {
errorObject.fixInfo = error.fixInfo;
}
return errorObject;
});
if (filteredErrors.length > 0) {
if (resultVersion === 0) {
result[ruleNameFriendly] = filteredErrors;
} else {
Array.prototype.push.apply(result, filteredErrors);
}
// resultVersion 2 or 3: Remove unwanted ruleName
for (const error of results) {
delete error.ruleName;
}
}
return results;
}
// Run all rules
const ruleListAsync = ruleList.filter((rule) => rule.asynchronous);
const ruleListSync = ruleList.filter((rule) => !rule.asynchronous);
const ruleListAsyncFirst = [
...ruleListAsync,
...ruleListSync
];
const callbackSuccess = () => callback(null, formatResults());
const callbackError =
(error) => callback(error instanceof Error ? error : new Error(error));
try {
ruleList.forEach(forRule);
const ruleResults = ruleListAsyncFirst.map(forRule);
if (ruleListAsync.length > 0) {
Promise.all(ruleResults.slice(0, ruleListAsync.length))
.then(callbackSuccess)
.catch(callbackError);
} else {
callbackSuccess();
}
} catch (error) {
callbackError(error);
} finally {
cache.clear();
return callback(error);
}
cache.clear();
return callback(null, result);
}
/**
@ -731,7 +748,7 @@ function lintInput(options, synchronous, callback) {
callback = callback || function noop() {};
// eslint-disable-next-line unicorn/prefer-spread
const ruleList = rules.concat(options.customRules || []);
const ruleErr = validateRuleList(ruleList);
const ruleErr = validateRuleList(ruleList, synchronous);
if (ruleErr) {
return callback(ruleErr);
}
@ -759,62 +776,32 @@ function lintInput(options, synchronous, callback) {
const fs = options.fs || require("fs");
const results = newResults(ruleList);
let done = false;
// Linting of strings is always synchronous
let syncItem = null;
// eslint-disable-next-line jsdoc/require-jsdoc
function syncCallback(err, result) {
if (err) {
done = true;
return callback(err);
}
results[syncItem] = result;
return null;
}
while (!done && (syncItem = stringsKeys.shift())) {
lintContent(
ruleList,
syncItem,
strings[syncItem] || "",
md,
config,
frontMatter,
handleRuleFailures,
noInlineConfig,
resultVersion,
syncCallback
);
}
if (synchronous) {
// Lint files synchronously
while (!done && (syncItem = files.shift())) {
lintFile(
ruleList,
syncItem,
md,
config,
frontMatter,
handleRuleFailures,
noInlineConfig,
resultVersion,
fs,
synchronous,
syncCallback
);
}
return done || callback(null, results);
}
// Lint files asynchronously
let concurrency = 0;
// eslint-disable-next-line jsdoc/require-jsdoc
function lintConcurrently() {
const asyncItem = files.shift();
function lintWorker() {
let currentItem = null;
// eslint-disable-next-line jsdoc/require-jsdoc
function lintWorkerCallback(err, result) {
concurrency--;
if (err) {
done = true;
return callback(err);
}
results[currentItem] = result;
if (!synchronous) {
lintWorker();
}
return null;
}
if (done) {
// Nothing to do
} else if (asyncItem) {
// Abort for error or nothing left to do
} else if (files.length > 0) {
// Lint next file
concurrency++;
currentItem = files.shift();
lintFile(
ruleList,
asyncItem,
currentItem,
md,
config,
frontMatter,
@ -823,34 +810,48 @@ function lintInput(options, synchronous, callback) {
resultVersion,
fs,
synchronous,
(err, result) => {
concurrency--;
if (err) {
done = true;
return callback(err);
}
results[asyncItem] = result;
lintConcurrently();
return null;
}
lintWorkerCallback
);
} else if (stringsKeys.length > 0) {
// Lint next string
concurrency++;
currentItem = stringsKeys.shift();
lintContent(
ruleList,
currentItem,
strings[currentItem] || "",
md,
config,
frontMatter,
handleRuleFailures,
noInlineConfig,
resultVersion,
lintWorkerCallback
);
} else if (concurrency === 0) {
// Finish
done = true;
return callback(null, results);
}
return null;
}
// Testing on a Raspberry Pi 4 Model B with an artificial 5ms file access
// delay suggests that a concurrency factor of 8 can eliminate the impact
// of that delay (i.e., total time is the same as with no delay).
lintConcurrently();
lintConcurrently();
lintConcurrently();
lintConcurrently();
lintConcurrently();
lintConcurrently();
lintConcurrently();
lintConcurrently();
if (synchronous) {
while (!done) {
lintWorker();
}
} else {
// Testing on a Raspberry Pi 4 Model B with an artificial 5ms file access
// delay suggests that a concurrency factor of 8 can eliminate the impact
// of that delay (i.e., total time is the same as with no delay).
lintWorker();
lintWorker();
lintWorker();
lintWorker();
lintWorker();
lintWorker();
lintWorker();
lintWorker();
}
return null;
}
@ -906,12 +907,13 @@ function parseConfiguration(name, content, parsers) {
let config = null;
let message = "";
const errors = [];
let index = 0;
// Try each parser
(parsers || [ JSON.parse ]).every((parser) => {
try {
config = parser(content);
} catch (error) {
errors.push(error.message);
errors.push(`Parser ${index++}: ${error.message}`);
}
return !config;
});
@ -1102,7 +1104,7 @@ function readConfigSync(file, parsers, fs) {
* @returns {string} SemVer string.
*/
function getVersion() {
return require("../package.json").version;
return require("./constants").version;
}
// Export a/synchronous/Promise APIs
@ -1172,7 +1174,7 @@ module.exports = markdownlint;
*
* @typedef {Object} RuleOnErrorInfo
* @property {number} lineNumber Line number (1-based).
* @property {string} [details] Details about the error.
* @property {string} [detail] Detail about the error.
* @property {string} [context] Context for the error.
* @property {number[]} [range] Column number (1-based) and length.
* @property {RuleOnErrorFixInfo} [fixInfo] Fix information.
@ -1196,6 +1198,7 @@ module.exports = markdownlint;
* @property {string} description Rule description.
* @property {URL} [information] Link to more information.
* @property {string[]} tags Rule tag(s).
* @property {boolean} [asynchronous] True if asynchronous.
* @property {RuleFunction} function Rule implementation.
*/

View file

@ -13,12 +13,14 @@ module.exports = {
"function": function MD007(params, onError) {
const indent = Number(params.config.indent || 2);
const startIndented = !!params.config.start_indented;
const startIndent = Number(params.config.start_indent || indent);
flattenedLists().forEach((list) => {
if (list.unordered && list.parentsUnordered) {
list.items.forEach((item) => {
const { lineNumber, line } = item;
const expectedNesting = list.nesting + (startIndented ? 1 : 0);
const expectedIndent = expectedNesting * indent;
const expectedIndent =
(startIndented ? startIndent : 0) +
(list.nesting * indent);
const actualIndent = indentFor(item);
let range = null;
let editColumn = 1;

View file

@ -2,8 +2,8 @@
"use strict";
const { addError, forEachLine } = require("../helpers");
const { lineMetadata } = require("./cache");
const { addError, forEachLine, overlapsAnyRange } = require("../helpers");
const { codeBlockAndSpanRanges, lineMetadata } = require("./cache");
const tabRe = /\t+/g;
@ -13,28 +13,33 @@ module.exports = {
"tags": [ "whitespace", "hard_tab" ],
"function": function MD010(params, onError) {
const codeBlocks = params.config.code_blocks;
const includeCodeBlocks = (codeBlocks === undefined) ? true : !!codeBlocks;
const includeCode = (codeBlocks === undefined) ? true : !!codeBlocks;
const spacesPerTab = params.config.spaces_per_tab;
const spaceMultiplier = (spacesPerTab === undefined) ?
1 :
Math.max(0, Number(spacesPerTab));
const exclusions = includeCode ? [] : codeBlockAndSpanRanges();
forEachLine(lineMetadata(), (line, lineIndex, inCode) => {
if (!inCode || includeCodeBlocks) {
if (includeCode || !inCode) {
let match = null;
while ((match = tabRe.exec(line)) !== null) {
const column = match.index + 1;
const { index } = match;
const column = index + 1;
const length = match[0].length;
addError(
onError,
lineIndex + 1,
"Column: " + column,
null,
[ column, length ],
{
"editColumn": column,
"deleteCount": length,
"insertText": "".padEnd(length * spaceMultiplier)
});
if (!overlapsAnyRange(exclusions, lineIndex, index, length)) {
addError(
onError,
lineIndex + 1,
"Column: " + column,
null,
[ column, length ],
{
"editColumn": column,
"deleteCount": length,
"insertText": "".padEnd(length * spaceMultiplier)
}
);
}
}
}
});

View file

@ -3,7 +3,7 @@
"use strict";
const { addError, forEachLine, overlapsAnyRange } = require("../helpers");
const { inlineCodeSpanRanges, lineMetadata } = require("./cache");
const { codeBlockAndSpanRanges, lineMetadata } = require("./cache");
const reversedLinkRe =
/(^|[^\\])\(([^)]+)\)\[([^\]^][^\]]*)](?!\()/g;
@ -13,7 +13,7 @@ module.exports = {
"description": "Reversed link syntax",
"tags": [ "links" ],
"function": function MD011(params, onError) {
const exclusions = inlineCodeSpanRanges();
const exclusions = codeBlockAndSpanRanges();
forEachLine(lineMetadata(), (line, lineIndex, inCode, onFence) => {
if (!inCode && !onFence) {
let match = null;

View file

@ -2,12 +2,13 @@
"use strict";
const { addError, forEachLine, unescapeMarkdown } = require("../helpers");
const { lineMetadata } = require("./cache");
const {
addError, forEachLine, overlapsAnyRange, unescapeMarkdown
} = require("../helpers");
const { codeBlockAndSpanRanges, lineMetadata } = require("./cache");
const htmlElementRe = /<(([A-Za-z][A-Za-z0-9-]*)(?:\s[^>]*)?)\/?>/g;
const linkDestinationRe = /]\(\s*$/;
const inlineCodeRe = /^[^`]*(`+[^`]+`+[^`]+)*`+[^`]*$/;
// See https://spec.commonmark.org/0.29/#autolinks
const emailAddressRe =
// eslint-disable-next-line max-len
@ -21,19 +22,22 @@ module.exports = {
let allowedElements = params.config.allowed_elements;
allowedElements = Array.isArray(allowedElements) ? allowedElements : [];
allowedElements = allowedElements.map((element) => element.toLowerCase());
const exclusions = codeBlockAndSpanRanges();
forEachLine(lineMetadata(), (line, lineIndex, inCode) => {
let match = null;
// eslint-disable-next-line no-unmodified-loop-condition
while (!inCode && ((match = htmlElementRe.exec(line)) !== null)) {
const [ tag, content, element ] = match;
if (!allowedElements.includes(element.toLowerCase()) &&
if (
!allowedElements.includes(element.toLowerCase()) &&
!tag.endsWith("\\>") &&
!emailAddressRe.test(content)) {
!emailAddressRe.test(content) &&
!overlapsAnyRange(exclusions, lineIndex, match.index, match[0].length)
) {
const prefix = line.substring(0, match.index);
if (!linkDestinationRe.test(prefix) && !inlineCodeRe.test(prefix)) {
if (!linkDestinationRe.test(prefix)) {
const unescaped = unescapeMarkdown(prefix + "<", "_");
if (!unescaped.endsWith("_") &&
((unescaped + "`").match(/`/g).length % 2)) {
if (!unescaped.endsWith("_")) {
addError(onError, lineIndex + 1, "Element: " + element,
null, [ match.index + 1, tag.length ]);
}

View file

@ -10,12 +10,12 @@ module.exports = {
"tags": [ "hr" ],
"function": function MD035(params, onError) {
let style = String(params.config.style || "consistent");
filterTokens(params, "hr", function forToken(token) {
const lineTrim = token.line.trim();
filterTokens(params, "hr", (token) => {
const { lineNumber, markup } = token;
if (style === "consistent") {
style = lineTrim;
style = markup;
}
addErrorDetailIf(onError, token.lineNumber, style, lineTrim);
addErrorDetailIf(onError, lineNumber, style, markup);
});
}
};

View file

@ -7,6 +7,7 @@ const { addErrorContext, emphasisMarkersInContent, forEachLine, isBlankLine } =
const { lineMetadata } = require("./cache");
const emphasisRe = /(^|[^\\]|\\\\)(?:(\*\*?\*?)|(__?_?))/g;
const embeddedUnderscoreRe = /([A-Za-z0-9])_([A-Za-z0-9])/g;
const asteriskListItemMarkerRe = /^([\s>]*)\*(\s+)/;
const leftSpaceRe = /^\s+/;
const rightSpaceRe = /\s+$/;
@ -98,14 +99,15 @@ module.exports = {
// Emphasis has no meaning here
return;
}
let patchedLine = line.replace(embeddedUnderscoreRe, "$1 $2");
if (onItemStart) {
// Trim overlapping '*' list item marker
line = line.replace(asteriskListItemMarkerRe, "$1 $2");
patchedLine = patchedLine.replace(asteriskListItemMarkerRe, "$1 $2");
}
let match = null;
// Match all emphasis-looking runs in the line...
while ((match = emphasisRe.exec(line))) {
const ignoreMarkersForLine = ignoreMarkersByLine[lineIndex] || [];
while ((match = emphasisRe.exec(patchedLine))) {
const ignoreMarkersForLine = ignoreMarkersByLine[lineIndex];
const matchIndex = match.index + match[1].length;
if (ignoreMarkersForLine.includes(matchIndex)) {
// Ignore emphasis markers inside code spans and links

View file

@ -4,7 +4,8 @@
const { addErrorContext, filterTokens } = require("../helpers");
const spaceInLinkRe = /\[(?:\s+(?:[^\]]*?)\s*|(?:[^\]]*?)\s+)](?=\(\S*\))/;
const spaceInLinkRe =
/\[(?:\s+(?:[^\]]*?)\s*|(?:[^\]]*?)\s+)](?=((?:\([^)]*\))|(?:\[[^\]]*\])))/;
module.exports = {
"names": [ "MD039", "no-space-in-links" ],

View file

@ -20,7 +20,6 @@ module.exports = {
let matchAny = false;
let hasError = false;
let anyHeadings = false;
// eslint-disable-next-line func-style
const getExpected = () => requiredHeadings[i++] || "[None]";
forEachHeading(params, (heading, content) => {
if (!hasError) {

View file

@ -4,7 +4,7 @@
const { addErrorDetailIf, bareUrlRe, escapeForRegExp, forEachLine,
overlapsAnyRange, linkRe, linkReferenceRe } = require("../helpers");
const { inlineCodeSpanRanges, lineMetadata } = require("./cache");
const { codeBlockAndSpanRanges, lineMetadata } = require("./cache");
module.exports = {
"names": [ "MD044", "proper-names" ],
@ -36,7 +36,7 @@ module.exports = {
}
});
if (!includeCodeBlocks) {
exclusions.push(...inlineCodeSpanRanges());
exclusions.push(...codeBlockAndSpanRanges());
}
for (const name of names) {
const escapedName = escapeForRegExp(name);

45
lib/md049.js Normal file
View file

@ -0,0 +1,45 @@
// @ts-check
"use strict";
const { addError, emphasisOrStrongStyleFor, forEachInlineChild,
getNextChildToken, getRangeAndFixInfoIfFound } = require("../helpers");
module.exports = {
"names": [ "MD049", "emphasis-style" ],
"description": "Emphasis style should be consistent",
"tags": [ "emphasis" ],
"function": function MD049(params, onError) {
let expectedStyle = String(params.config.style || "consistent");
forEachInlineChild(params, "em_open", (token, parent) => {
const { lineNumber, markup } = token;
const markupStyle = emphasisOrStrongStyleFor(markup);
if (expectedStyle === "consistent") {
expectedStyle = markupStyle;
}
if (expectedStyle !== markupStyle) {
let rangeAndFixInfo = {};
const contentToken = getNextChildToken(
parent, token, "text", "em_close"
);
if (contentToken) {
const { content } = contentToken;
const actual = `${markup}${content}${markup}`;
const expectedMarkup = (expectedStyle === "asterisk") ? "*" : "_";
const expected = `${expectedMarkup}${content}${expectedMarkup}`;
rangeAndFixInfo = getRangeAndFixInfoIfFound(
params.lines, lineNumber - 1, actual, expected
);
}
addError(
onError,
lineNumber,
`Expected: ${expectedStyle}; Actual: ${markupStyle}`,
null,
rangeAndFixInfo.range,
rangeAndFixInfo.fixInfo
);
}
});
}
};

45
lib/md050.js Normal file
View file

@ -0,0 +1,45 @@
// @ts-check
"use strict";
const { addError, emphasisOrStrongStyleFor, forEachInlineChild,
getNextChildToken, getRangeAndFixInfoIfFound } = require("../helpers");
module.exports = {
"names": [ "MD050", "strong-style" ],
"description": "Strong style should be consistent",
"tags": [ "emphasis" ],
"function": function MD050(params, onError) {
let expectedStyle = String(params.config.style || "consistent");
forEachInlineChild(params, "strong_open", (token, parent) => {
const { lineNumber, markup } = token;
const markupStyle = emphasisOrStrongStyleFor(markup);
if (expectedStyle === "consistent") {
expectedStyle = markupStyle;
}
if (expectedStyle !== markupStyle) {
let rangeAndFixInfo = {};
const contentToken = getNextChildToken(
parent, token, "text", "strong_close"
);
if (contentToken) {
const { content } = contentToken;
const actual = `${markup}${content}${markup}`;
const expectedMarkup = (expectedStyle === "asterisk") ? "**" : "__";
const expected = `${expectedMarkup}${content}${expectedMarkup}`;
rangeAndFixInfo = getRangeAndFixInfoIfFound(
params.lines, lineNumber - 1, actual, expected
);
}
addError(
onError,
lineNumber,
`Expected: ${expectedStyle}; Actual: ${markupStyle}`,
null,
rangeAndFixInfo.range,
rangeAndFixInfo.fixInfo
);
}
});
}
};

View file

@ -2,9 +2,7 @@
"use strict";
const packageJson = require("../package.json");
const homepage = packageJson.homepage;
const version = packageJson.version;
const { homepage, version } = require("./constants");
const rules = [
require("./md001"),
@ -50,7 +48,9 @@ const rules = [
require("./md045"),
require("./md046"),
require("./md047"),
require("./md048")
require("./md048"),
require("./md049"),
require("./md050")
];
rules.forEach((rule) => {
const name = rule.names[0].toLowerCase();

View file

@ -1,6 +1,6 @@
{
"name": "markdownlint",
"version": "0.24.0",
"version": "0.25.0",
"description": "A Node.js style checker and lint tool for Markdown/CommonMark files.",
"main": "lib/markdownlint.js",
"types": "lib/markdownlint.d.ts",
@ -19,7 +19,7 @@
"build-declaration": "tsc --allowJs --declaration --emitDeclarationOnly --resolveJsonModule lib/markdownlint.js && node scripts delete 'lib/{c,md,r}*.d.ts' 'helpers/*.d.ts'",
"build-demo": "node scripts copy node_modules/markdown-it/dist/markdown-it.min.js demo/markdown-it.min.js && cd demo && webpack --no-stats",
"build-example": "npm install --no-save --ignore-scripts grunt grunt-cli gulp through2",
"ci": "npm-run-all --continue-on-error --parallel declaration lint --parallel build-config build-demo test-cover && git diff --exit-code",
"ci": "npm-run-all --continue-on-error --parallel build-config lint serial-declaration-demo test-cover && git diff --exit-code",
"clone-test-repos-dotnet-docs": "cd test-repos && git clone https://github.com/dotnet/docs dotnet-docs --depth 1 --no-tags --quiet",
"clone-test-repos-eslint-eslint": "cd test-repos && git clone https://github.com/eslint/eslint eslint-eslint --depth 1 --no-tags --quiet",
"clone-test-repos-mkdocs-mkdocs": "cd test-repos && git clone https://github.com/mkdocs/mkdocs mkdocs-mkdocs --depth 1 --no-tags --quiet",
@ -32,41 +32,46 @@
"clone-test-repos-large": "npm run clone-test-repos && cd test-repos && npm run clone-test-repos-dotnet-docs && npm run clone-test-repos-v8-v8-dev",
"declaration": "npm run build-declaration && npm run test-declaration",
"example": "cd example && node standalone.js && grunt markdownlint --force && gulp markdownlint",
"docker-npm-install": "docker run --rm --tty --name npm-install --volume $PWD:/home/workdir --workdir /home/workdir --user node node:16 npm install",
"docker-npm-run-upgrade": "docker run --rm --tty --name npm-run-upgrade --volume $PWD:/home/workdir --workdir /home/workdir --user node node:16 npm run upgrade",
"lint": "eslint --max-warnings 0 .",
"lint-test-repos": "ava --timeout=5m test/markdownlint-test-repos.js",
"test": "ava test/markdownlint-test.js test/markdownlint-test-custom-rules.js test/markdownlint-test-helpers.js test/markdownlint-test-result-object.js test/markdownlint-test-scenarios.js",
"serial-declaration-demo": "npm run build-declaration && npm-run-all --continue-on-error --parallel build-demo test-declaration",
"test": "ava test/markdownlint-test.js test/markdownlint-test-config.js test/markdownlint-test-custom-rules.js test/markdownlint-test-helpers.js test/markdownlint-test-result-object.js test/markdownlint-test-scenarios.js",
"test-cover": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 npm test",
"test-declaration": "cd example/typescript && tsc && node type-check.js",
"test-extra": "ava --timeout=5m test/markdownlint-test-extra.js"
"test-extra": "ava --timeout=5m test/markdownlint-test-extra-parse.js test/markdownlint-test-extra-type.js",
"upgrade": "npx npm-check-updates --upgrade"
},
"engines": {
"node": ">=10"
"node": ">=12"
},
"dependencies": {
"markdown-it": "12.2.0"
"markdown-it": "12.3.0"
},
"devDependencies": {
"ava": "~3.15.0",
"c8": "~7.8.0",
"eslint": "~7.32.0",
"eslint-plugin-jsdoc": "~36.0.7",
"c8": "~7.10.0",
"eslint": "~8.5.0",
"eslint-plugin-jsdoc": "~37.4.0",
"eslint-plugin-node": "~11.1.0",
"eslint-plugin-unicorn": "~35.0.0",
"globby": "~11.0.4",
"eslint-plugin-unicorn": "~39.0.0",
"globby": "~12.0.2",
"js-yaml": "~4.1.0",
"markdown-it-for-inline": "~0.1.1",
"markdown-it-sub": "~1.0.0",
"markdown-it-sup": "~1.0.0",
"markdown-it-texmath": "~0.9.1",
"markdownlint-rule-helpers": "~0.14.0",
"markdown-it-texmath": "~0.9.7",
"markdownlint-rule-github-internal-links": "~0.1.0",
"markdownlint-rule-helpers": "~0.15.0",
"npm-run-all": "~4.1.5",
"strip-json-comments": "~3.1.1",
"strip-json-comments": "~4.0.0",
"toml": "~3.0.0",
"ts-loader": "~9.2.5",
"ts-loader": "~9.2.6",
"tv4": "~1.3.0",
"typescript": "~4.3.5",
"webpack": "~5.51.1",
"webpack-cli": "~4.8.0"
"typescript": "~4.5.4",
"webpack": "~5.65.0",
"webpack-cli": "~4.9.1"
},
"keywords": [
"markdown",

View file

@ -39,7 +39,9 @@
// Spaces for indent
"indent": 2,
// Whether to indent the first level of the list
"start_indented": false
"start_indented": false,
// Spaces for first level indent (when start_indented is set)
"start_indent": 2
},
// MD009/no-trailing-spaces - Trailing spaces
@ -248,5 +250,17 @@
"MD048": {
// Code fence style
"style": "consistent"
},
// MD049/emphasis-style - Emphasis style should be consistent
"MD049": {
// Emphasis style should be consistent
"style": "consistent"
},
// MD050/strong-style - Strong style should be consistent
"MD050": {
// Strong style should be consistent
"style": "consistent"
}
}

View file

@ -36,6 +36,8 @@ MD007:
indent: 2
# Whether to indent the first level of the list
start_indented: false
# Spaces for first level indent (when start_indented is set)
start_indent: 2
# MD009/no-trailing-spaces - Trailing spaces
MD009:
@ -224,4 +226,14 @@ MD047: true
# MD048/code-fence-style - Code fence style
MD048:
# Code fence style
style: "consistent"
# MD049/emphasis-style - Emphasis style should be consistent
MD049:
# Emphasis style should be consistent
style: "consistent"
# MD050/strong-style - Strong style should be consistent
MD050:
# Strong style should be consistent
style: "consistent"

View file

@ -107,6 +107,12 @@ rules.forEach(function forRule(rule) {
"description": "Whether to indent the first level of the list",
"type": "boolean",
"default": false
},
"start_indent": {
"description":
"Spaces for first level indent (when start_indented is set)",
"type": "integer",
"default": 2
}
};
break;
@ -396,6 +402,34 @@ rules.forEach(function forRule(rule) {
}
};
break;
case "MD049":
scheme.properties = {
"style": {
"description": "Emphasis style should be consistent",
"type": "string",
"enum": [
"consistent",
"asterisk",
"underscore"
],
"default": "consistent"
}
};
break;
case "MD050":
scheme.properties = {
"style": {
"description": "Strong style should be consistent",
"type": "string",
"enum": [
"consistent",
"asterisk",
"underscore"
],
"default": "consistent"
}
};
break;
default:
custom = false;
break;

View file

@ -238,6 +238,11 @@
"description": "Whether to indent the first level of the list",
"type": "boolean",
"default": false
},
"start_indent": {
"description": "Spaces for first level indent (when start_indented is set)",
"type": "integer",
"default": 2
}
},
"additionalProperties": false
@ -259,6 +264,11 @@
"description": "Whether to indent the first level of the list",
"type": "boolean",
"default": false
},
"start_indent": {
"description": "Spaces for first level indent (when start_indented is set)",
"type": "integer",
"default": 2
}
},
"additionalProperties": false
@ -1439,6 +1449,90 @@
},
"additionalProperties": false
},
"MD049": {
"description": "MD049/emphasis-style - Emphasis style should be consistent",
"type": [
"boolean",
"object"
],
"default": true,
"properties": {
"style": {
"description": "Emphasis style should be consistent",
"type": "string",
"enum": [
"consistent",
"asterisk",
"underscore"
],
"default": "consistent"
}
},
"additionalProperties": false
},
"emphasis-style": {
"description": "MD049/emphasis-style - Emphasis style should be consistent",
"type": [
"boolean",
"object"
],
"default": true,
"properties": {
"style": {
"description": "Emphasis style should be consistent",
"type": "string",
"enum": [
"consistent",
"asterisk",
"underscore"
],
"default": "consistent"
}
},
"additionalProperties": false
},
"MD050": {
"description": "MD050/strong-style - Strong style should be consistent",
"type": [
"boolean",
"object"
],
"default": true,
"properties": {
"style": {
"description": "Strong style should be consistent",
"type": "string",
"enum": [
"consistent",
"asterisk",
"underscore"
],
"default": "consistent"
}
},
"additionalProperties": false
},
"strong-style": {
"description": "MD050/strong-style - Strong style should be consistent",
"type": [
"boolean",
"object"
],
"default": true,
"properties": {
"style": {
"description": "Strong style should be consistent",
"type": "string",
"enum": [
"consistent",
"asterisk",
"underscore"
],
"default": "consistent"
}
},
"additionalProperties": false
},
"headings": {
"description": "headings - MD001, MD002, MD003, MD018, MD019, MD020, MD021, MD022, MD023, MD024, MD025, MD026, MD036, MD041, MD043",
"type": "boolean",
@ -1535,7 +1629,7 @@
"default": true
},
"emphasis": {
"description": "emphasis - MD036, MD037",
"description": "emphasis - MD036, MD037, MD049, MD050",
"type": "boolean",
"default": true
},

View file

@ -2,20 +2,27 @@
"use strict";
const fs = require("fs");
const globby = require("globby");
const fs = require("fs").promises;
const [ command, ...args ] = process.argv.slice(2);
if (command === "copy") {
const [ src, dest ] = args;
fs.copyFileSync(src, dest);
} else if (command === "delete") {
for (const arg of args) {
for (const file of globby.sync(arg)) {
fs.unlinkSync(file);
}
// eslint-disable-next-line unicorn/prefer-top-level-await
(async() => {
if (command === "copy") {
const [ src, dest ] = args;
await fs.copyFile(src, dest);
} else if (command === "delete") {
// eslint-disable-next-line node/no-unsupported-features/es-syntax
const { globby } = await import("globby");
await Promise.all(
args.flatMap(
(glob) => globby(glob)
.then(
(files) => files.map((file) => fs.unlink(file))
)
)
);
} else {
throw new Error(`Unsupported command: ${command}`);
}
} else {
throw new Error(`Unsupported command: ${command}`);
}
})();

View file

@ -64,7 +64,7 @@ https://example.com/page {MD034}
_Section {MD036} Heading_
Emphasis *with * space {MD037}
Emphasis _with _ space {MD037}
Code `with ` space {MD038}
@ -85,4 +85,12 @@ markdownLint {MD044}
![](image.jpg) {MD045}
## Heading 10 {MD022}
Emphasis _with_ underscore style
Emphasis *with* different style {MD049}
Strong __with__ underscore style
Strong **with** different style {MD050}
EOF {MD047}

View file

@ -0,0 +1,38 @@
# Code Blocks and Spans {MD044}
Text CODE text {MD044}
Text `CODE` text
```lang
CODE
CODE
```
`CODE` text `CODE`
CODE
CODE
Text `CODE
CODE` text
text text
text `CODE
CODE CODE
CODE` text
Text `CODE {MD044}
Text `CODE {MD044}
<!-- markdownlint-configure-file {
"proper-names": {
"names": [
"code"
],
"code_blocks": false
},
"code-block-style": false
} -->

View file

@ -0,0 +1,33 @@
# Code With Tabs Allowed
Text text {MD010}
Text `code code` text
Text ` code` text
Text `code ` text
Text `code code
code code
code code` text
console.log(" ");
```js
console.log(" ");
```
```j s {MD010}
console.log(" ");
```
console.log("");
<!-- markdownlint-configure-file {
"code-block-style": false,
"no-space-in-code": false,
"no-hard-tabs": {
"code_blocks": false
}
} -->

View file

@ -0,0 +1,33 @@
# Code With Tabs Blocked
Text text {MD010}
Text `code code` text {MD010}
Text ` code` text {MD010}
Text `code ` text {MD010}
Text `code code
code code {MD010}
code code` text
console.log(" "); // {MD010}
```js
console.log(" "); // {MD010}
```
```j s {MD010}
console.log(" "); // {MD010}
```
console.log(""); // {MD010}
<!-- markdownlint-configure-file {
"code-block-style": false,
"no-space-in-code": false,
"no-hard-tabs": {
"code_blocks": true
}
} -->

View file

@ -1,4 +1,5 @@
{
"default": true,
"MD041": false
"MD041": false,
"MD049": false
}

View file

@ -28,4 +28,18 @@ Fenced code
Fenced code
~~~
Mixed *emphasis* on _this_ line *with* multiple _issues_
Mixed __strong emphasis__ on **this** line __with__ multiple **issues**
Inconsistent
emphasis _text
spanning_ many
lines
Inconsistent
strong **emphasis
spanning** many
lines
Missing newline character

View file

@ -28,4 +28,18 @@ Fenced code
Fenced code
~~~
Mixed *emphasis* on *this* line *with* multiple *issues*
Mixed __strong emphasis__ on __this__ line __with__ multiple __issues__
Inconsistent
emphasis _text
spanning_ many
lines
Inconsistent
strong **emphasis
spanning** many
lines
Missing newline character

View file

@ -78,7 +78,7 @@
"fixInfo": null
},
{
"lineNumber": 31,
"lineNumber": 45,
"ruleNames": [
"MD043",
"required-headings",
@ -178,7 +178,7 @@
"fixInfo": null
},
{
"lineNumber": 31,
"lineNumber": 45,
"ruleNames": [
"MD047",
"single-trailing-newline"
@ -208,5 +208,111 @@
"errorContext": null,
"errorRange": null,
"fixInfo": null
},
{
"errorContext": null,
"errorDetail": "Expected: asterisk; Actual: underscore",
"errorRange": [
21,
6
],
"fixInfo": {
"deleteCount": 6,
"editColumn": 21,
"insertText": "*this*"
},
"lineNumber": 31,
"ruleDescription": "Emphasis style should be consistent",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md049",
"ruleNames": [
"MD049",
"emphasis-style"
]
},
{
"errorContext": null,
"errorDetail": "Expected: asterisk; Actual: underscore",
"errorRange": [
49,
8
],
"fixInfo": {
"deleteCount": 8,
"editColumn": 49,
"insertText": "*issues*"
},
"lineNumber": 31,
"ruleDescription": "Emphasis style should be consistent",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md049",
"ruleNames": [
"MD049",
"emphasis-style"
]
},
{
"errorContext": null,
"errorDetail": "Expected: asterisk; Actual: underscore",
"errorRange": null,
"fixInfo": null,
"lineNumber": 36,
"ruleDescription": "Emphasis style should be consistent",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md049",
"ruleNames": [
"MD049",
"emphasis-style"
]
},
{
"errorContext": null,
"errorDetail": "Expected: underscore; Actual: asterisk",
"errorRange": [
30,
8
],
"fixInfo": {
"deleteCount": 8,
"editColumn": 30,
"insertText": "__this__"
},
"lineNumber": 33,
"ruleDescription": "Strong style should be consistent",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md050",
"ruleNames": [
"MD050",
"strong-style"
]
},
{
"errorContext": null,
"errorDetail": "Expected: underscore; Actual: asterisk",
"errorRange": [
62,
10
],
"fixInfo": {
"deleteCount": 10,
"editColumn": 62,
"insertText": "__issues__"
},
"lineNumber": 33,
"ruleDescription": "Strong style should be consistent",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md050",
"ruleNames": [
"MD050",
"strong-style"
]
},
{
"errorContext": null,
"errorDetail": "Expected: underscore; Actual: asterisk",
"errorRange": null,
"fixInfo": null,
"lineNumber": 41,
"ruleDescription": "Strong style should be consistent",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md050",
"ruleNames": [
"MD050",
"strong-style"
]
}
]

View file

@ -28,3 +28,17 @@ Text <https://example.com/same> more \* text https://example.com/same more \[ te
Text https://example.com/first more text https://example.com/second still more text https://example.com/third done
(Incorrect link syntax)[https://www.example.com/]
Text [link ](https://example.com/) text.
Text [ link](https://example.com/) text.
Text [ link ](https://example.com/) text.
Text [link ][reference] text.
Text [ link][reference] text.
Text [ link ][reference] text.
[reference]: https://example.com/

View file

@ -28,3 +28,17 @@ Text <https://example.com/same> more \* text https://example.com/same more \[ te
Text <https://example.com/first> more text <https://example.com/second> still more text <https://example.com/third> done
[Incorrect link syntax](https://www.example.com/)
Text [link](https://example.com/) text.
Text [link](https://example.com/) text.
Text [link](https://example.com/) text.
Text [link][reference] text.
Text [link][reference] text.
Text [link][reference] text.
[reference]: https://example.com/

View file

@ -271,5 +271,125 @@
"deleteCount": 25,
"insertText": "<https://example.com/third>"
}
},
{
"lineNumber": 32,
"ruleNames": [
"MD039",
"no-space-in-links"
],
"ruleDescription": "Spaces inside link text",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md039",
"errorDetail": null,
"errorContext": "[link ]",
"errorRange": [
6,
7
],
"fixInfo": {
"editColumn": 7,
"deleteCount": 5,
"insertText": "link"
}
},
{
"lineNumber": 34,
"ruleNames": [
"MD039",
"no-space-in-links"
],
"ruleDescription": "Spaces inside link text",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md039",
"errorDetail": null,
"errorContext": "[ link]",
"errorRange": [
6,
7
],
"fixInfo": {
"editColumn": 7,
"deleteCount": 5,
"insertText": "link"
}
},
{
"lineNumber": 36,
"ruleNames": [
"MD039",
"no-space-in-links"
],
"ruleDescription": "Spaces inside link text",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md039",
"errorDetail": null,
"errorContext": "[ link ]",
"errorRange": [
6,
8
],
"fixInfo": {
"editColumn": 7,
"deleteCount": 6,
"insertText": "link"
}
},
{
"lineNumber": 38,
"ruleNames": [
"MD039",
"no-space-in-links"
],
"ruleDescription": "Spaces inside link text",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md039",
"errorDetail": null,
"errorContext": "[link ]",
"errorRange": [
6,
7
],
"fixInfo": {
"editColumn": 7,
"deleteCount": 5,
"insertText": "link"
}
},
{
"lineNumber": 40,
"ruleNames": [
"MD039",
"no-space-in-links"
],
"ruleDescription": "Spaces inside link text",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md039",
"errorDetail": null,
"errorContext": "[ link]",
"errorRange": [
6,
7
],
"fixInfo": {
"editColumn": 7,
"deleteCount": 5,
"insertText": "link"
}
},
{
"lineNumber": 42,
"ruleNames": [
"MD039",
"no-space-in-links"
],
"ruleDescription": "Spaces inside link text",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md039",
"errorDetail": null,
"errorContext": "[ link ]",
"errorRange": [
6,
8
],
"fixInfo": {
"editColumn": 7,
"deleteCount": 6,
"insertText": "link"
}
}
]

View file

@ -8,11 +8,11 @@ Text
Text
> *Text*
> *Text* {MD049}
Text
> *Text text text*
> *Text text text* {MD049}
Text

View file

@ -9,7 +9,7 @@ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore
eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt
in culpa qui officia deserunt mollit anim id est laborum.
__Section 1.1: another section {MD036}__
__Section 1.1: another section {MD036} {MD050}__
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
@ -27,7 +27,7 @@ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore
eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt
in culpa qui officia deserunt mollit anim id est laborum.
_Section 3: oh no more sections {MD036}_
_Section 3: oh no more sections {MD036} {MD049}_
This is a normal paragraph
**that just happens to have emphasized text in**

View file

@ -0,0 +1,6 @@
{
"default": true,
"MD049": {
"style": "asterisk"
}
}

View file

@ -0,0 +1,5 @@
# Emphasis style asterisk
This is *fine*
This is _not_ {MD049}

View file

@ -0,0 +1,6 @@
{
"default": true,
"MD049": {
"style": "underscore"
}
}

View file

@ -0,0 +1,5 @@
# Emphasis style underscore
This is _fine_
This is *not* {MD049}

View file

@ -8,3 +8,5 @@
[test **test** test](www.test.com)
[test __test__ test](www.test.com)
[this should not raise](www.shouldnotraise.com)
<!-- markdownlint-disable-file MD049 MD050 -->

View file

@ -0,0 +1,20 @@
---
front: matter
---
# Front Matter with Disable-Next-Line
<!-- markdownlint-disable-next-line no-inline-html -->
<hr/>
<hr/> {MD033}
<!-- markdownlint-disable-next-line -->
<hr/>
<hr/> {MD033}
<hr/> {MD033}
<!-- markdownlint-disable-next-line -->
<hr/>
<hr/> {MD033}
<hr/> {MD033}

View file

@ -0,0 +1,39 @@
# HR in Blockquote, Dash
---
***
___
{MD035:5} {MD035:7}
> Text
>
> ---
>
> ***
>
> ___
>
> Text
{MD035:15} {MD035:17}
- - -
> Text
>
> > Text
> >
> > ---
> >
> > ***
> >
> > ___
> >
> > Text
>
> Text
{MD035:31} {MD035:33}

View file

@ -0,0 +1,39 @@
# HR in Blockquote, Star
***
___
---
{MD035:5} {MD035:7}
> Text
>
> ---
>
> ***
>
> ___
>
> Text
{MD035:13} {MD035:17}
* * *
> Text
>
> > Text
> >
> > ---
> >
> > ***
> >
> > ___
> >
> > Text
>
> Text
{MD035:29} {MD035:33}

View file

@ -0,0 +1,39 @@
# HR in Blockquote, Under
___
---
***
{MD035:5} {MD035:7}
> Text
>
> ---
>
> ***
>
> ___
>
> Text
{MD035:13} {MD035:15}
_ _ _
> Text
>
> > Text
> >
> > ---
> >
> > ***
> >
> > ___
> >
> > Text
>
> Text
{MD035:29} {MD035:31}

View file

@ -20,5 +20,4 @@ _____
***
{MD035:3} {MD035:5} {MD035:7} {MD035:11} {MD035:13} {MD035:15} {MD035:17}
{MD035:19} {MD035:21}
{MD035:3} {MD035:5} {MD035:7} {MD035:13} {MD035:15} {MD035:17} {MD035:19} {MD035:21}

View file

@ -20,5 +20,4 @@ _____
***
{MD035:5} {MD035:7} {MD035:9} {MD035:11} {MD035:13} {MD035:15} {MD035:17}
{MD035:19}
{MD035:7} {MD035:9} {MD035:11} {MD035:13} {MD035:15} {MD035:17} {MD035:19}

View file

@ -20,5 +20,4 @@ _____
***
{MD035:5} {MD035:7} {MD035:9} {MD035:11} {MD035:13} {MD035:15} {MD035:17}
{MD035:19}
{MD035:7} {MD035:9} {MD035:11} {MD035:13} {MD035:15} {MD035:17} {MD035:19}

View file

@ -22,6 +22,63 @@ Text \` text `<code>` text \` text `<code>` text
Text \`\` text `<code>` text
Text `<code>` text \` text `<code>` text
## Elements in multiple line code spans
Text `code
<element/>`
`code
<element/>`
`code
<element/>` text
Text `code
code
<element/>
<element/>`
``code ``` ```` `
<code>code
</code>``
Text `code
</element>
code` text
Text `code code
code <element>` text
Text `code <element>
code code` text
Text `code code
code <element> code
code code` text
Text ````code code
code <element> code
code code```` text
Text `code code
code <element>` text
text `code code
code code` text
Text `code code
code code` text
text `code code
code <element>` text
Text `code code
code <element>` text
text `code code
code <element>` text
Text `code code
code` text <element> text `code {MD033}
code code` text
## Slash in element name
Text **\<base directory>\another\directory\\<slash/directory>** text
@ -39,3 +96,25 @@ Text **\<base directory>\another\directory\\<slash/directory>** text
<a href="https://example.com" target="_blank">Google</a> {MD033}
<a href="https://example.com:9999" target="_blank">Google</a> {MD033}
## Unterminated code span followed by element in code span
Text text `text text
Text `<element>` text
Text
text `text
text
Text `code <element> code` text
```lang
code {MD046:112}
<element>
```
Text `code <element> code` text
Text <element> text {MD033}

View file

@ -10,7 +10,7 @@
[This link has `code` and right space ](link) {MD039}
[ This link has _emphasis_ and left space](link) {MD039}
[ This link has *emphasis* and left space](link) {MD039}
[This](link) line has [multiple](link) links.

View file

@ -0,0 +1,35 @@
# List Indentation start_indent/indent
* item 1
* item 2
* item 2.1
* item 2.2
* item 2.2.1
* item 2.3
* item 2.3.1 {MD007}
* item 3
* item 4 {MD005} {MD007}
Text
* item 1 {MD007}
* item 2 {MD007}
* item 2.1
* item 2.2
* item 2.2.1
Text
* item 1
* item 2
* item 2.1 {MD007}
* item 2.2 {MD007}
* item 2.2.1 {MD007}
<!-- markdownlint-configure-file {
"ul-indent": {
"indent": 3,
"start_indented": true,
"start_indent": 1
}
} -->

View file

@ -0,0 +1,34 @@
# List Indentation start_indent/no indent
* item 1
* item 2
* item 2.1
* item 2.2
* item 2.2.1
* item 2.3
* item 2.3.1 {MD007}
* item 3
* item 4 {MD005} {MD007}
Text
* item 1 {MD007}
* item 2 {MD007}
* item 2.1 {MD007}
* item 2.2 {MD007}
* item 2.2.1 {MD007}
Text
* item 1
* item 2
* item 2.1 {MD007}
* item 2.2 {MD007}
* item 2.2.1 {MD007}
<!-- markdownlint-configure-file {
"ul-indent": {
"start_indented": true,
"start_indent": 1
}
} -->

View file

@ -0,0 +1,46 @@
# List Indentation - Start Indented/No Indent
* item 1
* item 2
* item 2.1
* item 2.2
* item 2.2.1
* item 2.3
* item 3
## Disallowed List Indentation - Starts at Zero
* item 1 {MD007}
* item 2 {MD007}
* item 2.1 {MD007}
* item 2.2 {MD007}
* item 2.2.1 {MD007}
* item 2.3 {MD007}
* item 3 {MD007}
## Disallowed List Indentation - Starts at One
* item 1 {MD007}
* item 2 {MD007}
* item 2.1 {MD007}
* item 2.2 {MD007}
* item 2.2.1 {MD007}
* item 2.3 {MD007}
* item 2.3.1
* item 3 {MD007}
## Disallowed List Indentation - Starts at Three
* item 1 {MD007}
* item 2 {MD007}
* item 2.1 {MD007}
* item 2.2 {MD007}
* item 2.2.1 {MD007}
* item 2.3 {MD007}
* item 3 {MD007}
<!-- markdownlint-configure-file {
"ul-indent": {
"start_indented": true
}
} -->

View file

@ -0,0 +1,38 @@
# Lists with Commented Items
Text
- item <!-- comment -->
- item <!-- comment -->
<!--
- commented subitem: description
- commented subitem: description
-->
- item <!-- comment -->
- item <!-- comment -->
Text
- item <!-- comment -->
- item <!-- comment -->
<!-- - commented subitem: description
- commented subitem: description -->
- item <!-- comment -->
- item <!-- comment -->
Text
- item <!-- comment -->
<!-- - commented subitem: description -->
- item <!-- comment -->
Text
- item <!-- comment -->
- item <!-- comment -->
<!-- - commented subitem: description -->
<!-- - commented subitem: description -->
- item <!-- comment -->
- item <!-- comment -->
Text

View file

@ -32,15 +32,15 @@ This long line includes a simple [reference][label] link and is long enough to v
*[This long line is comprised of an emphasized link](https://example.com "This is the long link's title")*
_[This long line is comprised of an emphasized link](https://example.com "This is the long link's title")_
_[This long line is comprised of an emphasized link {MD049}](https://example.com "This is the long link's title")_
**[This long line is comprised of a bolded link](https://example.com "This is the long link's title")**
__[This long line is comprised of a bolded link](https://example.com "This is the long link's title")__
__[This long line is comprised of a bolded link {MD050}](https://example.com "This is the long link's title")__
_**[This long line is comprised of an emphasized and bolded link](https://example.com "This is the long link's title")**_
_**[This long line is comprised of an emphasized and bolded link {MD049}](https://example.com "This is the long link's title")**_
**_[This long line is comprised of an emphasized and bolded link](https://example.com "This is the long link's title")_**
**_[This long line is comprised of an emphasized and bolded link {MD049}](https://example.com "This is the long link's title")_**
*[](https://example.com "This long line is comprised of an emphasized link with empty text and a non-empty title")*

View file

@ -0,0 +1,484 @@
// @ts-check
"use strict";
const path = require("path");
const test = require("ava").default;
const markdownlint = require("../lib/markdownlint");
test.cb("configSingle", (t) => {
t.plan(2);
markdownlint.readConfig("./test/config/config-child.json",
function callback(err, actual) {
t.falsy(err);
const expected = require("./config/config-child.json");
t.deepEqual(actual, expected, "Config object not correct.");
t.end();
});
});
test.cb("configAbsolute", (t) => {
t.plan(2);
markdownlint.readConfig(path.join(__dirname, "config", "config-child.json"),
function callback(err, actual) {
t.falsy(err);
const expected = require("./config/config-child.json");
t.deepEqual(actual, expected, "Config object not correct.");
t.end();
});
});
test.cb("configMultiple", (t) => {
t.plan(2);
markdownlint.readConfig("./test/config/config-grandparent.json",
function callback(err, actual) {
t.falsy(err);
const expected = {
...require("./config/config-child.json"),
...require("./config/config-parent.json"),
...require("./config/config-grandparent.json")
};
delete expected.extends;
t.deepEqual(actual, expected, "Config object not correct.");
t.end();
});
});
test.cb("configMultipleWithRequireResolve", (t) => {
t.plan(2);
markdownlint.readConfig("./test/config/config-packageparent.json",
function callback(err, actual) {
t.falsy(err);
const expected = {
...require("./node_modules/pseudo-package/config-frompackage.json"),
...require("./config/config-packageparent.json")
};
delete expected.extends;
t.deepEqual(actual, expected, "Config object not correct.");
t.end();
});
});
test.cb("configCustomFileSystem", (t) => {
t.plan(5);
const file = path.resolve("/dir/file.json");
const extended = path.resolve("/dir/extended.json");
const fileContent = {
"extends": extended,
"default": true,
"MD001": false
};
const extendedContent = {
"MD001": true,
"MD002": true
};
const fsApi = {
"access": (p, m, cb) => {
t.is(p, extended);
return (cb || m)();
},
"readFile": (p, o, cb) => {
switch (p) {
case file:
t.is(p, file);
return cb(null, JSON.stringify(fileContent));
case extended:
t.is(p, extended);
return cb(null, JSON.stringify(extendedContent));
default:
return t.fail();
}
}
};
markdownlint.readConfig(
file,
null,
fsApi,
function callback(err, actual) {
t.falsy(err);
const expected = {
...extendedContent,
...fileContent
};
delete expected.extends;
t.deepEqual(actual, expected, "Config object not correct.");
t.end();
});
});
test.cb("configBadFile", (t) => {
t.plan(4);
markdownlint.readConfig("./test/config/config-badfile.json",
function callback(err, result) {
t.truthy(err, "Did not get an error for bad file.");
t.true(err instanceof Error, "Error not instance of Error.");
// @ts-ignore
t.is(err.code, "ENOENT", "Error code for bad file not ENOENT.");
t.true(!result, "Got result for bad file.");
t.end();
});
});
test.cb("configBadChildFile", (t) => {
t.plan(4);
markdownlint.readConfig("./test/config/config-badchildfile.json",
function callback(err, result) {
t.truthy(err, "Did not get an error for bad child file.");
t.true(err instanceof Error, "Error not instance of Error.");
// @ts-ignore
t.is(err.code, "ENOENT",
"Error code for bad child file not ENOENT.");
t.true(!result, "Got result for bad child file.");
t.end();
});
});
test.cb("configBadChildPackage", (t) => {
t.plan(4);
markdownlint.readConfig("./test/config/config-badchildpackage.json",
function callback(err, result) {
t.truthy(err, "Did not get an error for bad child package.");
t.true(err instanceof Error, "Error not instance of Error.");
// @ts-ignore
t.is(err.code, "ENOENT",
"Error code for bad child package not ENOENT.");
t.true(!result, "Got result for bad child package.");
t.end();
});
});
test.cb("configBadJson", (t) => {
t.plan(3);
markdownlint.readConfig("./test/config/config-badjson.json",
function callback(err, result) {
t.truthy(err, "Did not get an error for bad JSON.");
t.true(err instanceof Error, "Error not instance of Error.");
t.true(!result, "Got result for bad JSON.");
t.end();
});
});
test.cb("configBadChildJson", (t) => {
t.plan(3);
markdownlint.readConfig("./test/config/config-badchildjson.json",
function callback(err, result) {
t.truthy(err, "Did not get an error for bad child JSON.");
t.true(err instanceof Error, "Error not instance of Error.");
t.true(!result, "Got result for bad child JSON.");
t.end();
});
});
test.cb("configSingleYaml", (t) => {
t.plan(2);
markdownlint.readConfig(
"./test/config/config-child.yaml",
// @ts-ignore
[ require("js-yaml").load ],
function callback(err, actual) {
t.falsy(err);
const expected = require("./config/config-child.json");
t.deepEqual(actual, expected, "Config object not correct.");
t.end();
});
});
test.cb("configMultipleYaml", (t) => {
t.plan(2);
markdownlint.readConfig(
"./test/config/config-grandparent.yaml",
// @ts-ignore
[ require("js-yaml").load ],
function callback(err, actual) {
t.falsy(err);
const expected = {
...require("./config/config-child.json"),
...require("./config/config-parent.json"),
...require("./config/config-grandparent.json")
};
delete expected.extends;
t.deepEqual(actual, expected, "Config object not correct.");
t.end();
});
});
test.cb("configMultipleHybrid", (t) => {
t.plan(2);
markdownlint.readConfig(
"./test/config/config-grandparent-hybrid.yaml",
// @ts-ignore
[ JSON.parse, require("toml").parse, require("js-yaml").load ],
function callback(err, actual) {
t.falsy(err);
const expected = {
...require("./config/config-child.json"),
...require("./config/config-parent.json"),
...require("./config/config-grandparent.json")
};
delete expected.extends;
t.like(actual, expected, "Config object not correct.");
t.end();
});
});
test.cb("configBadHybrid", (t) => {
t.plan(4);
markdownlint.readConfig(
"./test/config/config-badcontent.txt",
// @ts-ignore
[ JSON.parse, require("toml").parse, require("js-yaml").load ],
function callback(err, result) {
t.truthy(err, "Did not get an error for bad child JSON.");
t.true(err instanceof Error, "Error not instance of Error.");
t.truthy(err.message.match(
// eslint-disable-next-line max-len
/^Unable to parse '[^']*'; Parser \d+: Unexpected token \S+ in JSON at position \d+;/
), "Error message unexpected.");
t.true(!result, "Got result for bad child JSON.");
t.end();
});
});
test("configSingleSync", (t) => {
t.plan(1);
const actual = markdownlint.readConfigSync("./test/config/config-child.json");
const expected = require("./config/config-child.json");
t.deepEqual(actual, expected, "Config object not correct.");
});
test("configAbsoluteSync", (t) => {
t.plan(1);
const actual = markdownlint.readConfigSync(
path.join(__dirname, "config", "config-child.json"));
const expected = require("./config/config-child.json");
t.deepEqual(actual, expected, "Config object not correct.");
});
test("configMultipleSync", (t) => {
t.plan(1);
const actual =
markdownlint.readConfigSync("./test/config/config-grandparent.json");
const expected = {
...require("./config/config-child.json"),
...require("./config/config-parent.json"),
...require("./config/config-grandparent.json")
};
delete expected.extends;
t.deepEqual(actual, expected, "Config object not correct.");
});
test("configBadFileSync", (t) => {
t.plan(1);
t.throws(
function badFileCall() {
markdownlint.readConfigSync("./test/config/config-badfile.json");
},
{
"message": /ENOENT/
},
"Did not get correct exception for bad file."
);
});
test("configBadChildFileSync", (t) => {
t.plan(1);
t.throws(
function badChildFileCall() {
markdownlint.readConfigSync("./test/config/config-badchildfile.json");
},
{
"message": /ENOENT/
},
"Did not get correct exception for bad child file."
);
});
test("configBadJsonSync", (t) => {
t.plan(1);
t.throws(
function badJsonCall() {
markdownlint.readConfigSync("./test/config/config-badjson.json");
},
{
"message":
// eslint-disable-next-line max-len
/Unable to parse '[^']*'; Parser \d+: Unexpected token \S+ in JSON at position \d+/
},
"Did not get correct exception for bad JSON."
);
});
test("configBadChildJsonSync", (t) => {
t.plan(1);
t.throws(
function badChildJsonCall() {
markdownlint.readConfigSync("./test/config/config-badchildjson.json");
},
{
"message":
// eslint-disable-next-line max-len
/Unable to parse '[^']*'; Parser \d+: Unexpected token \S+ in JSON at position \d+/
},
"Did not get correct exception for bad child JSON."
);
});
test("configSingleYamlSync", (t) => {
t.plan(1);
const actual = markdownlint.readConfigSync(
// @ts-ignore
"./test/config/config-child.yaml", [ require("js-yaml").load ]);
const expected = require("./config/config-child.json");
t.deepEqual(actual, expected, "Config object not correct.");
});
test("configMultipleYamlSync", (t) => {
t.plan(1);
const actual = markdownlint.readConfigSync(
// @ts-ignore
"./test/config/config-grandparent.yaml", [ require("js-yaml").load ]);
const expected = {
...require("./config/config-child.json"),
...require("./config/config-parent.json"),
...require("./config/config-grandparent.json")
};
delete expected.extends;
t.deepEqual(actual, expected, "Config object not correct.");
});
test("configMultipleHybridSync", (t) => {
t.plan(1);
const actual = markdownlint.readConfigSync(
"./test/config/config-grandparent-hybrid.yaml",
// @ts-ignore
[ JSON.parse, require("toml").parse, require("js-yaml").load ]);
const expected = {
...require("./config/config-child.json"),
...require("./config/config-parent.json"),
...require("./config/config-grandparent.json")
};
delete expected.extends;
t.like(actual, expected, "Config object not correct.");
});
test("configCustomFileSystemSync", (t) => {
t.plan(4);
const file = path.resolve("/dir/file.json");
const extended = path.resolve("/dir/extended.json");
const fileContent = {
"extends": extended,
"default": true,
"MD001": false
};
const extendedContent = {
"MD001": true,
"MD002": true
};
const fsApi = {
"accessSync": (p) => {
t.is(p, extended);
},
"readFileSync": (p) => {
switch (p) {
case file:
t.is(p, file);
return JSON.stringify(fileContent);
case extended:
t.is(p, extended);
return JSON.stringify(extendedContent);
default:
return t.fail();
}
}
};
const actual = markdownlint.readConfigSync(file, null, fsApi);
const expected = {
...extendedContent,
...fileContent
};
delete expected.extends;
t.deepEqual(actual, expected, "Config object not correct.");
});
test("configBadHybridSync", (t) => {
t.plan(1);
t.throws(
function badHybridCall() {
markdownlint.readConfigSync(
"./test/config/config-badcontent.txt",
// @ts-ignore
[ JSON.parse, require("toml").parse, require("js-yaml").load ]);
},
{
// eslint-disable-next-line max-len
"message": /^Unable to parse '[^']*'; Parser \d+: Unexpected token \S+ in JSON at position \d+;/
},
"Did not get correct exception for bad content."
);
});
test.cb("configSinglePromise", (t) => {
t.plan(1);
markdownlint.promises.readConfig("./test/config/config-child.json")
.then((actual) => {
const expected = require("./config/config-child.json");
t.deepEqual(actual, expected, "Config object not correct.");
t.end();
});
});
test.cb("configCustomFileSystemPromise", (t) => {
t.plan(4);
const file = path.resolve("/dir/file.json");
const extended = path.resolve("/dir/extended.json");
const fileContent = {
"extends": extended,
"default": true,
"MD001": false
};
const extendedContent = {
"MD001": true,
"MD002": true
};
const fsApi = {
"access": (p, m, cb) => {
t.is(p, extended);
return (cb || m)();
},
"readFile": (p, o, cb) => {
switch (p) {
case file:
t.is(p, file);
return cb(null, JSON.stringify(fileContent));
case extended:
t.is(p, extended);
return cb(null, JSON.stringify(extendedContent));
default:
return t.fail();
}
}
};
markdownlint.promises.readConfig(file, null, fsApi)
.then((actual) => {
const expected = {
...extendedContent,
...fileContent
};
delete expected.extends;
t.deepEqual(actual, expected, "Config object not correct.");
t.end();
});
});
test.cb("configBadFilePromise", (t) => {
t.plan(2);
markdownlint.promises.readConfig("./test/config/config-badfile.json")
.then(
null,
(error) => {
t.truthy(error, "Did not get an error for bad JSON.");
t.true(error instanceof Error, "Error not instance of Error.");
t.end();
}
);
});

View file

@ -2,12 +2,11 @@
"use strict";
const fs = require("fs").promises;
const test = require("ava").default;
const packageJson = require("../package.json");
const markdownlint = require("../lib/markdownlint");
const customRules = require("./rules/rules.js");
const homepage = packageJson.homepage;
const version = packageJson.version;
const { homepage, version } = require("../package.json");
test.cb("customRulesV0", (t) => {
t.plan(4);
@ -328,7 +327,7 @@ test.cb("customRulesConfig", (t) => {
});
});
test("customRulesNpmPackage", (t) => {
test.cb("customRulesNpmPackage", (t) => {
t.plan(2);
const options = {
"customRules": [ require("./rules/npm") ],
@ -345,11 +344,12 @@ test("customRulesNpmPackage", (t) => {
};
// @ts-ignore
t.deepEqual(actualResult, expectedResult, "Undetected issues.");
t.end();
});
});
test("customRulesBadProperty", (t) => {
t.plan(23);
t.plan(27);
[
{
"propertyName": "names",
@ -364,6 +364,10 @@ test("customRulesBadProperty", (t) => {
"propertyName": "information",
"propertyValues": [ 10, [], "string", "https://example.com" ]
},
{
"propertyName": "asynchronous",
"propertyValues": [ null, 10, "", [] ]
},
{
"propertyName": "tags",
"propertyValues":
@ -395,7 +399,7 @@ test("customRulesBadProperty", (t) => {
});
});
test("customRulesUsedNameName", (t) => {
test.cb("customRulesUsedNameName", (t) => {
t.plan(4);
markdownlint({
"customRules": [
@ -414,10 +418,11 @@ test("customRulesUsedNameName", (t) => {
"already used as a name or tag.",
"Incorrect message for duplicate name.");
t.true(!result, "Got result for duplicate name.");
t.end();
});
});
test("customRulesUsedNameTag", (t) => {
test.cb("customRulesUsedNameTag", (t) => {
t.plan(4);
markdownlint({
"customRules": [
@ -435,10 +440,11 @@ test("customRulesUsedNameTag", (t) => {
"Name 'HtMl' of custom rule at index 0 is already used as a name or tag.",
"Incorrect message for duplicate name.");
t.true(!result, "Got result for duplicate name.");
t.end();
});
});
test("customRulesUsedTagName", (t) => {
test.cb("customRulesUsedTagName", (t) => {
t.plan(4);
markdownlint({
"customRules": [
@ -463,6 +469,7 @@ test("customRulesUsedTagName", (t) => {
"already used as a name.",
"Incorrect message for duplicate name.");
t.true(!result, "Got result for duplicate tag.");
t.end();
});
});
@ -517,7 +524,7 @@ test("customRulesThrowForFileSync", (t) => {
);
});
test("customRulesThrowForString", (t) => {
test.cb("customRulesThrowForString", (t) => {
t.plan(4);
const exceptionMessage = "Test exception message";
markdownlint({
@ -540,10 +547,69 @@ test("customRulesThrowForString", (t) => {
t.is(err.message, exceptionMessage,
"Incorrect message for function thrown.");
t.true(!result, "Got result for function thrown.");
t.end();
});
});
test("customRulesOnErrorNull", (t) => {
test("customRulesThrowForStringSync", (t) => {
t.plan(1);
const exceptionMessage = "Test exception message";
t.throws(
function customRuleThrowsCall() {
markdownlint.sync({
"customRules": [
{
"names": [ "name" ],
"description": "description",
"tags": [ "tag" ],
"function": function throws() {
throw new Error(exceptionMessage);
}
}
],
"strings": {
"string": "String"
}
});
},
{
"message": exceptionMessage
},
"Did not get correct exception for function thrown."
);
});
test.cb("customRulesOnErrorNull", (t) => {
t.plan(4);
markdownlint({
"customRules": [
{
"names": [ "name" ],
"description": "description",
"tags": [ "tag" ],
"function": function onErrorNull(params, onError) {
onError(null);
}
}
],
"strings": {
"string": "String"
}
},
function callback(err, result) {
t.truthy(err, "Did not get an error for function thrown.");
t.true(err instanceof Error, "Error not instance of Error.");
t.is(
err.message,
"Property 'lineNumber' of onError parameter is incorrect.",
"Did not get correct exception for null object."
);
t.true(!result, "Got result for function thrown.");
t.end();
});
});
test("customRulesOnErrorNullSync", (t) => {
t.plan(1);
const options = {
"customRules": [
@ -802,7 +868,7 @@ test("customRulesOnErrorValid", (t) => {
});
});
test("customRulesOnErrorLazy", (t) => {
test.cb("customRulesOnErrorLazy", (t) => {
t.plan(2);
const options = {
"customRules": [
@ -840,10 +906,11 @@ test("customRulesOnErrorLazy", (t) => {
]
};
t.deepEqual(actualResult, expectedResult, "Undetected issues.");
t.end();
});
});
test("customRulesOnErrorModified", (t) => {
test.cb("customRulesOnErrorModified", (t) => {
t.plan(2);
const errorObject = {
"lineNumber": 1,
@ -900,98 +967,11 @@ test("customRulesOnErrorModified", (t) => {
]
};
t.deepEqual(actualResult, expectedResult, "Undetected issues.");
});
});
test.cb("customRulesThrowForFileHandled", (t) => {
t.plan(2);
const exceptionMessage = "Test exception message";
markdownlint({
"customRules": [
{
"names": [ "name" ],
"description": "description",
"tags": [ "tag" ],
"function": function throws() {
throw new Error(exceptionMessage);
}
}
],
"files": [ "./test/custom-rules.md" ],
"handleRuleFailures": true
}, function callback(err, actualResult) {
t.falsy(err);
const expectedResult = {
"./test/custom-rules.md": [
{
"lineNumber": 1,
"ruleNames": [ "name" ],
"ruleDescription": "description",
"ruleInformation": null,
"errorDetail":
`This rule threw an exception: ${exceptionMessage}`,
"errorContext": null,
"errorRange": null
}
]
};
t.deepEqual(actualResult, expectedResult, "Undetected issues.");
t.end();
});
});
test("customRulesThrowForStringHandled", (t) => {
t.plan(2);
const exceptionMessage = "Test exception message";
const informationUrl = "https://example.com/rule";
markdownlint({
"customRules": [
{
"names": [ "name" ],
"description": "description",
"information": new URL(informationUrl),
"tags": [ "tag" ],
"function": function throws() {
throw new Error(exceptionMessage);
}
}
],
"strings": {
"string": "String\n"
},
"handleRuleFailures": true
}, function callback(err, actualResult) {
t.falsy(err);
const expectedResult = {
"string": [
{
"lineNumber": 1,
"ruleNames": [ "MD041", "first-line-heading", "first-line-h1" ],
"ruleDescription":
"First line in a file should be a top-level heading",
"ruleInformation":
`${homepage}/blob/v${version}/doc/Rules.md#md041`,
"errorDetail": null,
"errorContext": "String",
"errorRange": null
},
{
"lineNumber": 1,
"ruleNames": [ "name" ],
"ruleDescription": "description",
"ruleInformation": informationUrl,
"errorDetail":
`This rule threw an exception: ${exceptionMessage}`,
"errorContext": null,
"errorRange": null
}
]
};
t.deepEqual(actualResult, expectedResult, "Undetected issues.");
});
});
test("customRulesOnErrorInvalidHandled", (t) => {
test.cb("customRulesOnErrorInvalidHandled", (t) => {
t.plan(2);
markdownlint({
"customRules": [
@ -1001,8 +981,7 @@ test("customRulesOnErrorInvalidHandled", (t) => {
"tags": [ "tag" ],
"function": function onErrorInvalid(params, onError) {
onError({
"lineNumber": 13,
"details": "N/A"
"lineNumber": 13
});
}
}
@ -1028,9 +1007,48 @@ test("customRulesOnErrorInvalidHandled", (t) => {
]
};
t.deepEqual(actualResult, expectedResult, "Undetected issues.");
t.end();
});
});
test("customRulesOnErrorInvalidHandledSync", (t) => {
t.plan(1);
const actualResult = markdownlint.sync({
"customRules": [
{
"names": [ "name" ],
"description": "description",
"tags": [ "tag" ],
"function": function onErrorInvalid(params, onError) {
onError({
"lineNumber": 13,
"detail": "N/A"
});
}
}
],
"strings": {
"string": "# Heading\n"
},
"handleRuleFailures": true
});
const expectedResult = {
"string": [
{
"lineNumber": 1,
"ruleNames": [ "name" ],
"ruleDescription": "description",
"ruleInformation": null,
"errorDetail": "This rule threw an exception: " +
"Property 'lineNumber' of onError parameter is incorrect.",
"errorContext": null,
"errorRange": null
}
]
};
t.deepEqual(actualResult, expectedResult, "Undetected issues.");
});
test.cb("customRulesFileName", (t) => {
t.plan(2);
const options = {
@ -1052,7 +1070,7 @@ test.cb("customRulesFileName", (t) => {
});
});
test("customRulesStringName", (t) => {
test.cb("customRulesStringName", (t) => {
t.plan(2);
const options = {
"customRules": [
@ -1071,6 +1089,7 @@ test("customRulesStringName", (t) => {
};
markdownlint(options, function callback(err) {
t.falsy(err);
t.end();
});
});
@ -1123,3 +1142,385 @@ test.cb("customRulesLintJavaScript", (t) => {
t.end();
});
});
test("customRulesAsyncThrowsInSyncContext", (t) => {
t.plan(1);
const options = {
"customRules": [
{
"names": [ "name1", "name2" ],
"description": "description",
"tags": [ "tag" ],
"asynchronous": true,
"function": () => {}
}
],
"strings": {
"string": "Unused"
}
};
t.throws(
() => markdownlint.sync(options),
{
"message": "Custom rule name1/name2 at index 0 is asynchronous and " +
"can not be used in a synchronous context."
},
"Did not get correct exception for async rule in sync context."
);
});
test("customRulesAsyncReadFiles", (t) => {
t.plan(3);
const options = {
"customRules": [
{
"names": [ "name1" ],
"description": "description1",
"information": new URL("https://example.com/asyncRule1"),
"tags": [ "tag" ],
"asynchronous": true,
"function":
(params, onError) => fs.readFile(__filename, "utf8").then(
(content) => {
t.true(content.length > 0);
onError({
"lineNumber": 1,
"detail": "detail1",
"context": "context1",
"range": [ 2, 3 ]
});
}
)
},
{
"names": [ "name2" ],
"description": "description2",
"tags": [ "tag" ],
"asynchronous": true,
"function":
async(params, onError) => {
const content = await fs.readFile(__filename, "utf8");
t.true(content.length > 0);
onError({
"lineNumber": 1,
"detail": "detail2",
"context": "context2"
});
}
}
],
"strings": {
"string": "# Heading"
}
};
const expected = {
"string": [
{
"lineNumber": 1,
"ruleNames": [ "MD047", "single-trailing-newline" ],
"ruleDescription": "Files should end with a single newline character",
"ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md047`,
"errorDetail": null,
"errorContext": null,
"errorRange": [ 9, 1 ]
},
{
"lineNumber": 1,
"ruleNames": [ "name1" ],
"ruleDescription": "description1",
"ruleInformation": "https://example.com/asyncRule1",
"errorDetail": "detail1",
"errorContext": "context1",
"errorRange": [ 2, 3 ]
},
{
"lineNumber": 1,
"ruleNames": [ "name2" ],
"ruleDescription": "description2",
"ruleInformation": null,
"errorDetail": "detail2",
"errorContext": "context2",
"errorRange": null
}
]
};
return markdownlint.promises.markdownlint(options)
.then((actual) => t.deepEqual(actual, expected, "Unexpected issues."));
});
test("customRulesAsyncIgnoresSyncReturn", (t) => {
t.plan(1);
const options = {
"customRules": [
{
"names": [ "sync" ],
"description": "description",
"information": new URL("https://example.com/asyncRule"),
"tags": [ "tag" ],
"asynchronous": false,
"function": () => new Promise(() => {
// Never resolves
})
},
{
"names": [ "async" ],
"description": "description",
"information": new URL("https://example.com/asyncRule"),
"tags": [ "tag" ],
"asynchronous": true,
"function": (params, onError) => new Promise((resolve) => {
onError({ "lineNumber": 1 });
resolve();
})
}
],
"strings": {
"string": "# Heading"
}
};
const expected = {
"string": [
{
"lineNumber": 1,
"ruleNames": [ "async" ],
"ruleDescription": "description",
"ruleInformation": "https://example.com/asyncRule",
"errorDetail": null,
"errorContext": null,
"errorRange": null
},
{
"lineNumber": 1,
"ruleNames": [ "MD047", "single-trailing-newline" ],
"ruleDescription": "Files should end with a single newline character",
"ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md047`,
"errorDetail": null,
"errorContext": null,
"errorRange": [ 9, 1 ]
}
]
};
return markdownlint.promises.markdownlint(options)
.then((actual) => t.deepEqual(actual, expected, "Unexpected issues."));
});
const errorMessage = "Custom error message.";
const stringScenarios = [
[
"Files",
[ "./test/custom-rules.md" ],
null
],
[
"Strings",
null,
{ "./test/custom-rules.md": "# Heading\n" }
]
];
[
[
"customRulesThrowString",
() => {
throw errorMessage;
}
],
[
"customRulesThrowError",
() => {
throw new Error(errorMessage);
}
]
].forEach((flavor) => {
const [ name, func ] = flavor;
const customRule = [
{
"names": [ "name" ],
"description": "description",
"tags": [ "tag" ],
"function": func
}
];
const expectedResult = {
"./test/custom-rules.md": [
{
"lineNumber": 1,
"ruleNames": [ "name" ],
"ruleDescription": "description",
"ruleInformation": null,
"errorDetail": `This rule threw an exception: ${errorMessage}`,
"errorContext": null,
"errorRange": null
}
]
};
stringScenarios.forEach((inputs) => {
const [ subname, files, strings ] = inputs;
test.cb(`${name}${subname}UnhandledAsync`, (t) => {
t.plan(4);
markdownlint({
// @ts-ignore
"customRules": customRule,
// @ts-ignore
files,
// @ts-ignore
strings
}, function callback(err, result) {
t.truthy(err, "Did not get an error for exception.");
t.true(err instanceof Error, "Error not instance of Error.");
t.is(err.message, errorMessage, "Incorrect message for exception.");
t.true(!result, "Got result for exception.");
t.end();
});
});
test.cb(`${name}${subname}HandledAsync`, (t) => {
t.plan(2);
markdownlint({
// @ts-ignore
"customRules": customRule,
// @ts-ignore
files,
// @ts-ignore
strings,
"handleRuleFailures": true
}, function callback(err, actualResult) {
t.falsy(err);
t.deepEqual(actualResult, expectedResult, "Undetected issues.");
t.end();
});
});
test(`${name}${subname}UnhandledSync`, (t) => {
t.plan(1);
t.throws(
() => markdownlint.sync({
// @ts-ignore
"customRules": customRule,
// @ts-ignore
files,
// @ts-ignore
strings
}),
{
"message": errorMessage
},
"Unexpected exception."
);
});
test(`${name}${subname}HandledSync`, (t) => {
t.plan(1);
const actualResult = markdownlint.sync({
// @ts-ignore
"customRules": customRule,
// @ts-ignore
files,
// @ts-ignore
strings,
"handleRuleFailures": true
});
t.deepEqual(actualResult, expectedResult, "Undetected issues.");
});
});
});
[
[
"customRulesAsyncExceptionString",
() => {
throw errorMessage;
}
],
[
"customRulesAsyncExceptionError",
() => {
throw new Error(errorMessage);
}
],
[
"customRulesAsyncDeferredString",
() => fs.readFile(__filename, "utf8").then(
() => {
throw errorMessage;
}
)
],
[
"customRulesAsyncDeferredError",
() => fs.readFile(__filename, "utf8").then(
() => {
throw new Error(errorMessage);
}
)
],
[
"customRulesAsyncRejectString",
() => Promise.reject(errorMessage)
],
[
"customRulesAsyncRejectError",
() => Promise.reject(new Error(errorMessage))
]
].forEach((flavor) => {
const [ name, func ] = flavor;
const customRule = {
"names": [ "name" ],
"description": "description",
"tags": [ "tag" ],
"asynchronous": true,
"function": func
};
stringScenarios.forEach((inputs) => {
const [ subname, files, strings ] = inputs;
test.cb(`${name}${subname}Unhandled`, (t) => {
t.plan(4);
markdownlint({
// @ts-ignore
"customRules": [ customRule ],
// @ts-ignore
files,
// @ts-ignore
strings
}, function callback(err, result) {
t.truthy(err, "Did not get an error for rejection.");
t.true(err instanceof Error, "Error not instance of Error.");
t.is(err.message, errorMessage, "Incorrect message for rejection.");
t.true(!result, "Got result for rejection.");
t.end();
});
});
test.cb(`${name}${subname}Handled`, (t) => {
t.plan(2);
markdownlint({
// @ts-ignore
"customRules": [ customRule ],
// @ts-ignore
files,
// @ts-ignore
strings,
"handleRuleFailures": true
}, function callback(err, actualResult) {
t.falsy(err);
const expectedResult = {
"./test/custom-rules.md": [
{
"lineNumber": 1,
"ruleNames": [ "name" ],
"ruleDescription": "description",
"ruleInformation": null,
"errorDetail": `This rule threw an exception: ${errorMessage}`,
"errorContext": null,
"errorRange": null
}
]
};
t.deepEqual(actualResult, expectedResult, "Undetected issues.");
t.end();
});
});
});
});

View file

@ -0,0 +1,16 @@
// @ts-check
"use strict";
const test = require("ava").default;
const markdownlint = require("../lib/markdownlint");
// Parses all Markdown files in all package dependencies
test("parseAllFiles", async(t) => {
t.plan(1);
// eslint-disable-next-line node/no-unsupported-features/es-syntax
const { globby } = await import("globby");
const files = await globby("**/*.{md,markdown}");
await markdownlint.promises.markdownlint({ files });
t.pass();
});

View file

@ -4,7 +4,6 @@
const fs = require("fs");
const path = require("path");
const globby = require("globby");
const test = require("ava").default;
const markdownlint = require("../lib/markdownlint");
@ -27,15 +26,3 @@ files.filter((file) => /\.md$/.test(file)).forEach((file) => {
t.pass();
});
});
// Parses all Markdown files in all package dependencies
test.cb("parseAllFiles", (t) => {
t.plan(1);
const options = {
"files": globby.sync("**/*.{md,markdown}")
};
markdownlint(options, (err) => {
t.falsy(err);
t.end();
});
});

View file

@ -226,7 +226,7 @@ bar`
});
test("isBlankLine", (t) => {
t.plan(25);
t.plan(29);
const blankLines = [
null,
"",
@ -244,7 +244,11 @@ test("isBlankLine", (t) => {
"> ",
"> > > \t",
"> <!--text-->",
">><!--text-->"
">><!--text-->",
"<!--",
" <!-- text",
"text --> ",
"-->"
];
blankLines.forEach((line) => t.true(helpers.isBlankLine(line), line || ""));
const nonBlankLines = [
@ -253,9 +257,9 @@ test("isBlankLine", (t) => {
".",
"> .",
"<!--text--> text",
"<!--->",
"<!--",
"-->"
"text <!--text-->",
"text <!--",
"--> text"
];
nonBlankLines.forEach((line) => t.true(!helpers.isBlankLine(line), line));
});
@ -383,7 +387,7 @@ test("forEachInlineCodeSpan", (t) => {
});
test("getPreferredLineEnding", (t) => {
t.plan(17);
t.plan(19);
const testCases = [
[ "", os.EOL ],
[ "\r", "\r" ],
@ -408,6 +412,16 @@ test("getPreferredLineEnding", (t) => {
const actual = helpers.getPreferredLineEnding(input);
t.is(actual, expected, "Incorrect line ending returned.");
});
t.is(
helpers.getPreferredLineEnding("", "linux"),
"\n",
"Incorrect line ending for linux"
);
t.is(
helpers.getPreferredLineEnding("", "win32"),
"\r\n",
"Incorrect line ending for win32"
);
});
test("applyFix", (t) => {
@ -934,3 +948,37 @@ test("applyFixes", (t) => {
t.is(actual, expected, "Incorrect fix applied.");
});
});
test("deepFreeze", (t) => {
t.plan(6);
const obj = {
"prop": true,
"func": () => true,
"sub": {
"prop": [ 1 ],
"sub": {
"prop": "one"
}
}
};
t.is(helpers.deepFreeze(obj), obj, "Did not return object.");
[
() => {
obj.prop = false;
},
() => {
obj.func = () => false;
},
() => {
obj.sub.prop = [];
},
() => {
obj.sub.prop[0] = 0;
},
() => {
obj.sub.sub.prop = "zero";
}
].forEach((scenario) => {
t.throws(scenario, null, "Assigned to frozen object.");
});
});

View file

@ -6,44 +6,29 @@ const { existsSync } = require("fs");
// eslint-disable-next-line unicorn/import-style
const { join } = require("path");
const { promisify } = require("util");
const globby = require("globby");
const jsYaml = require("js-yaml");
const stripJsonComments = require("strip-json-comments");
const test = require("ava").default;
const markdownlint = require("../lib/markdownlint");
const markdownlintPromise = promisify(markdownlint);
const readConfigPromise = promisify(markdownlint.readConfig);
/**
* Parses JSONC text.
*
* @param {string} json JSON to parse.
* @returns {Object} Object representation.
*/
function jsoncParse(json) {
return JSON.parse(stripJsonComments(json));
}
/**
* Parses YAML text.
*
* @param {string} yaml YAML to parse.
* @returns {Object} Object representation.
*/
function yamlParse(yaml) {
return jsYaml.load(yaml);
}
/**
* Lints a test repository.
*
* @param {Object} t Test instance.
* @param {string[]} globPatterns Array of files to in/exclude.
* @param {string} configPath Path to config file.
* @param {RegExp[]} [ignoreRes] Array of RegExp violations to ignore.
* @returns {Promise} Test result.
*/
function lintTestRepo(t, globPatterns, configPath) {
async function lintTestRepo(t, globPatterns, configPath, ignoreRes) {
t.plan(1);
// eslint-disable-next-line node/no-unsupported-features/es-syntax
const { globby } = await import("globby");
// eslint-disable-next-line node/no-unsupported-features/es-syntax
const { "default": stripJsonComments } = await import("strip-json-comments");
const jsoncParse = (json) => JSON.parse(stripJsonComments(json));
const yamlParse = (yaml) => jsYaml.load(yaml);
return Promise.all([
globby(globPatterns),
// @ts-ignore
@ -57,7 +42,14 @@ function lintTestRepo(t, globPatterns, configPath) {
// eslint-disable-next-line no-console
console.log(`${t.title}: Linting ${files.length} files...`);
return markdownlintPromise(options).then((results) => {
const resultsString = results.toString();
let resultsString = results.toString();
for (const ignoreRe of (ignoreRes || [])) {
const lengthBefore = resultsString.length;
resultsString = resultsString.replace(ignoreRe, "");
if (resultsString.length === lengthBefore) {
t.fail(`Unnecessary ignore: ${ignoreRe}`);
}
}
if (resultsString.length > 0) {
// eslint-disable-next-line no-console
console.log(resultsString);
@ -67,13 +59,26 @@ function lintTestRepo(t, globPatterns, configPath) {
});
}
/**
* Excludes a list of globs.
*
* @param {string} rootDir Root directory for globs.
* @param {...string} globs Globs to exclude.
* @returns {string[]} Array of excluded globs.
*/
function excludeGlobs(rootDir, ...globs) {
return globs.map((glob) => "!" + join(rootDir, glob));
}
// Run markdownlint the same way the corresponding repositories do
test("https://github.com/eslint/eslint", (t) => {
const rootDir = "./test-repos/eslint-eslint";
const globPatterns = [ join(rootDir, "docs/**/*.md") ];
const configPath = join(rootDir, ".markdownlint.yml");
return lintTestRepo(t, globPatterns, configPath);
const ignoreRes =
[ /^[^:]+\/array-callback-return\.md: \d+: MD050\/.*$\r?\n?/gm ];
return lintTestRepo(t, globPatterns, configPath, ignoreRes);
});
test("https://github.com/mkdocs/mkdocs", (t) => {
@ -81,8 +86,13 @@ test("https://github.com/mkdocs/mkdocs", (t) => {
const globPatterns = [
join(rootDir, "README.md"),
join(rootDir, "CONTRIBUTING.md"),
join(rootDir, "docs/**/*.md"),
"!" + join(rootDir, "docs/CNAME")
join(rootDir, "docs"),
...excludeGlobs(
rootDir,
"docs/CNAME",
"docs/**/*.css",
"docs/**/*.png"
)
];
const configPath = join(rootDir, ".markdownlintrc");
return lintTestRepo(t, globPatterns, configPath);
@ -106,14 +116,16 @@ test("https://github.com/pi-hole/docs", (t) => {
const rootDir = "./test-repos/pi-hole-docs";
const globPatterns = [ join(rootDir, "**/*.md") ];
const configPath = join(rootDir, ".markdownlint.json");
return lintTestRepo(t, globPatterns, configPath);
const ignoreRes =
[ /^[^:]+\/(unbound|index|prerequisites)\.md: \d+: MD049\/.*$\r?\n?/gm ];
return lintTestRepo(t, globPatterns, configPath, ignoreRes);
});
test("https://github.com/webhintio/hint", (t) => {
const rootDir = "./test-repos/webhintio-hint";
const globPatterns = [
join(rootDir, "**/*.md"),
"!" + join(rootDir, "**/CHANGELOG.md")
...excludeGlobs(rootDir, "**/CHANGELOG.md")
];
const configPath = join(rootDir, ".markdownlintrc");
return lintTestRepo(t, globPatterns, configPath);
@ -132,12 +144,10 @@ const dotnetDocsDir = "./test-repos/dotnet-docs";
if (existsSync(dotnetDocsDir)) {
test("https://github.com/dotnet/docs", (t) => {
const rootDir = dotnetDocsDir;
const globPatterns = [
join(rootDir, "**/*.md"),
"!" + join(rootDir, "samples/**/*.md")
];
const globPatterns = [ join(rootDir, "**/*.md") ];
const configPath = join(rootDir, ".markdownlint.json");
return lintTestRepo(t, globPatterns, configPath);
const ignoreRes = [ /^[^:]+: \d+: (MD049|MD050)\/.*$\r?\n?/gm ];
return lintTestRepo(t, globPatterns, configPath, ignoreRes);
});
}
@ -147,6 +157,7 @@ if (existsSync(v8v8DevDir)) {
const rootDir = v8v8DevDir;
const globPatterns = [ join(rootDir, "src/**/*.md") ];
const configPath = join(rootDir, ".markdownlint.json");
return lintTestRepo(t, globPatterns, configPath);
const ignoreRes = [ /^[^:]+: \d+: MD049\/.*$\r?\n?/gm ];
return lintTestRepo(t, globPatterns, configPath, ignoreRes);
});
}

View file

@ -10,11 +10,11 @@ const pluginInline = require("markdown-it-for-inline");
const pluginSub = require("markdown-it-sub");
const pluginSup = require("markdown-it-sup");
const pluginTexMath = require("markdown-it-texmath");
const stripJsonComments = require("strip-json-comments");
const test = require("ava").default;
const tv4 = require("tv4");
const { homepage, version } = require("../package.json");
const markdownlint = require("../lib/markdownlint");
const constants = require("../lib/constants");
const rules = require("../lib/rules");
const customRules = require("./rules/rules.js");
const configSchema = require("../schema/markdownlint-config-schema.json");
@ -24,7 +24,7 @@ const pluginTexMathOptions = {
"renderToString": () => ""
}
};
const deprecatedRuleNames = new Set([ "MD002", "MD006" ]);
const deprecatedRuleNames = new Set(constants.deprecatedRuleNames);
const configSchemaStrict = {
...configSchema,
"additionalProperties": false
@ -83,11 +83,12 @@ test.cb("projectFilesNoInlineConfig", (t) => {
"doc/Prettier.md",
"helpers/README.md"
],
"noInlineConfig": true,
"config": {
"line-length": { "line_length": 150 },
"no-duplicate-heading": false
}
},
"customRules": [ require("markdownlint-rule-github-internal-links") ],
"noInlineConfig": true
};
markdownlint(options, function callback(err, actual) {
t.falsy(err);
@ -492,8 +493,10 @@ test.cb("styleAll", (t) => {
"MD042": [ 81 ],
"MD045": [ 85 ],
"MD046": [ 49, 73, 77 ],
"MD047": [ 88 ],
"MD048": [ 77 ]
"MD047": [ 96 ],
"MD048": [ 77 ],
"MD049": [ 90 ],
"MD050": [ 94 ]
}
};
// @ts-ignore
@ -535,8 +538,10 @@ test.cb("styleRelaxed", (t) => {
"MD042": [ 81 ],
"MD045": [ 85 ],
"MD046": [ 49, 73, 77 ],
"MD047": [ 88 ],
"MD048": [ 77 ]
"MD047": [ 96 ],
"MD048": [ 77 ],
"MD049": [ 90 ],
"MD050": [ 94 ]
}
};
// @ts-ignore
@ -836,8 +841,9 @@ test.cb("customFileSystemAsync", (t) => {
t.end();
});
});
test.cb("readme", (t) => {
t.plan(115);
t.plan(119);
const tagToRules = {};
rules.forEach(function forRule(rule) {
rule.tags.forEach(function forTag(tag) {
@ -913,7 +919,7 @@ test.cb("readme", (t) => {
});
test.cb("rules", (t) => {
t.plan(336);
t.plan(352);
fs.readFile("doc/Rules.md", "utf8",
(err, contents) => {
t.falsy(err);
@ -924,7 +930,6 @@ test.cb("rules", (t) => {
let ruleHasAliases = true;
let ruleUsesParams = null;
const tagAliasParameterRe = /, |: | /;
// eslint-disable-next-line func-style
const testTagsAliasesParams = (r) => {
// eslint-disable-next-line unicorn/prefer-default-parameters
r = r || "[NO RULE]";
@ -1064,9 +1069,10 @@ test("validateConfigSchemaAppliesToUnknownProperties", (t) => {
}
});
test("validateConfigExampleJson", (t) => {
test("validateConfigExampleJson", async(t) => {
t.plan(2);
// eslint-disable-next-line node/no-unsupported-features/es-syntax
const { "default": stripJsonComments } = await import("strip-json-comments");
// Validate JSONC
const fileJson = ".markdownlint.jsonc";
const dataJson = fs.readFileSync(
@ -1078,7 +1084,6 @@ test("validateConfigExampleJson", (t) => {
// @ts-ignore
tv4.validate(jsonObject, configSchemaStrict),
fileJson + "\n" + JSON.stringify(tv4.error, null, 2));
// Validate YAML
const fileYaml = ".markdownlint.yaml";
const dataYaml = fs.readFileSync(
@ -1090,483 +1095,8 @@ test("validateConfigExampleJson", (t) => {
"YAML example does not match JSON example.");
});
test.cb("configSingle", (t) => {
t.plan(2);
markdownlint.readConfig("./test/config/config-child.json",
function callback(err, actual) {
t.falsy(err);
const expected = require("./config/config-child.json");
t.deepEqual(actual, expected, "Config object not correct.");
t.end();
});
});
test.cb("configAbsolute", (t) => {
t.plan(2);
markdownlint.readConfig(path.join(__dirname, "config", "config-child.json"),
function callback(err, actual) {
t.falsy(err);
const expected = require("./config/config-child.json");
t.deepEqual(actual, expected, "Config object not correct.");
t.end();
});
});
test.cb("configMultiple", (t) => {
t.plan(2);
markdownlint.readConfig("./test/config/config-grandparent.json",
function callback(err, actual) {
t.falsy(err);
const expected = {
...require("./config/config-child.json"),
...require("./config/config-parent.json"),
...require("./config/config-grandparent.json")
};
delete expected.extends;
t.deepEqual(actual, expected, "Config object not correct.");
t.end();
});
});
test.cb("configMultipleWithRequireResolve", (t) => {
t.plan(2);
markdownlint.readConfig("./test/config/config-packageparent.json",
function callback(err, actual) {
t.falsy(err);
const expected = {
...require("./node_modules/pseudo-package/config-frompackage.json"),
...require("./config/config-packageparent.json")
};
delete expected.extends;
t.deepEqual(actual, expected, "Config object not correct.");
t.end();
});
});
test.cb("configCustomFileSystem", (t) => {
t.plan(5);
const file = path.resolve("/dir/file.json");
const extended = path.resolve("/dir/extended.json");
const fileContent = {
"extends": extended,
"default": true,
"MD001": false
};
const extendedContent = {
"MD001": true,
"MD002": true
};
const fsApi = {
"access": (p, m, cb) => {
t.is(p, extended);
return (cb || m)();
},
"readFile": (p, o, cb) => {
switch (p) {
case file:
t.is(p, file);
return cb(null, JSON.stringify(fileContent));
case extended:
t.is(p, extended);
return cb(null, JSON.stringify(extendedContent));
default:
return t.fail();
}
}
};
markdownlint.readConfig(
file,
null,
fsApi,
function callback(err, actual) {
t.falsy(err);
const expected = {
...extendedContent,
...fileContent
};
delete expected.extends;
t.deepEqual(actual, expected, "Config object not correct.");
t.end();
});
});
test.cb("configBadFile", (t) => {
t.plan(4);
markdownlint.readConfig("./test/config/config-badfile.json",
function callback(err, result) {
t.truthy(err, "Did not get an error for bad file.");
t.true(err instanceof Error, "Error not instance of Error.");
// @ts-ignore
t.is(err.code, "ENOENT", "Error code for bad file not ENOENT.");
t.true(!result, "Got result for bad file.");
t.end();
});
});
test.cb("configBadChildFile", (t) => {
t.plan(4);
markdownlint.readConfig("./test/config/config-badchildfile.json",
function callback(err, result) {
t.truthy(err, "Did not get an error for bad child file.");
t.true(err instanceof Error, "Error not instance of Error.");
// @ts-ignore
t.is(err.code, "ENOENT",
"Error code for bad child file not ENOENT.");
t.true(!result, "Got result for bad child file.");
t.end();
});
});
test.cb("configBadChildPackage", (t) => {
t.plan(4);
markdownlint.readConfig("./test/config/config-badchildpackage.json",
function callback(err, result) {
t.truthy(err, "Did not get an error for bad child package.");
t.true(err instanceof Error, "Error not instance of Error.");
// @ts-ignore
t.is(err.code, "ENOENT",
"Error code for bad child package not ENOENT.");
t.true(!result, "Got result for bad child package.");
t.end();
});
});
test.cb("configBadJson", (t) => {
t.plan(3);
markdownlint.readConfig("./test/config/config-badjson.json",
function callback(err, result) {
t.truthy(err, "Did not get an error for bad JSON.");
t.true(err instanceof Error, "Error not instance of Error.");
t.true(!result, "Got result for bad JSON.");
t.end();
});
});
test.cb("configBadChildJson", (t) => {
t.plan(3);
markdownlint.readConfig("./test/config/config-badchildjson.json",
function callback(err, result) {
t.truthy(err, "Did not get an error for bad child JSON.");
t.true(err instanceof Error, "Error not instance of Error.");
t.true(!result, "Got result for bad child JSON.");
t.end();
});
});
test.cb("configSingleYaml", (t) => {
t.plan(2);
markdownlint.readConfig(
"./test/config/config-child.yaml",
// @ts-ignore
[ require("js-yaml").load ],
function callback(err, actual) {
t.falsy(err);
const expected = require("./config/config-child.json");
t.deepEqual(actual, expected, "Config object not correct.");
t.end();
});
});
test.cb("configMultipleYaml", (t) => {
t.plan(2);
markdownlint.readConfig(
"./test/config/config-grandparent.yaml",
// @ts-ignore
[ require("js-yaml").load ],
function callback(err, actual) {
t.falsy(err);
const expected = {
...require("./config/config-child.json"),
...require("./config/config-parent.json"),
...require("./config/config-grandparent.json")
};
delete expected.extends;
t.deepEqual(actual, expected, "Config object not correct.");
t.end();
});
});
test.cb("configMultipleHybrid", (t) => {
t.plan(2);
markdownlint.readConfig(
"./test/config/config-grandparent-hybrid.yaml",
// @ts-ignore
[ JSON.parse, require("toml").parse, require("js-yaml").load ],
function callback(err, actual) {
t.falsy(err);
const expected = {
...require("./config/config-child.json"),
...require("./config/config-parent.json"),
...require("./config/config-grandparent.json")
};
delete expected.extends;
t.like(actual, expected, "Config object not correct.");
t.end();
});
});
test.cb("configBadHybrid", (t) => {
t.plan(4);
markdownlint.readConfig(
"./test/config/config-badcontent.txt",
// @ts-ignore
[ JSON.parse, require("toml").parse, require("js-yaml").load ],
function callback(err, result) {
t.truthy(err, "Did not get an error for bad child JSON.");
t.true(err instanceof Error, "Error not instance of Error.");
t.truthy(err.message.match(
// eslint-disable-next-line max-len
/^Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+;/
), "Error message unexpected.");
t.true(!result, "Got result for bad child JSON.");
t.end();
});
});
test("configSingleSync", (t) => {
t.plan(1);
const actual = markdownlint.readConfigSync("./test/config/config-child.json");
const expected = require("./config/config-child.json");
t.deepEqual(actual, expected, "Config object not correct.");
});
test("configAbsoluteSync", (t) => {
t.plan(1);
const actual = markdownlint.readConfigSync(
path.join(__dirname, "config", "config-child.json"));
const expected = require("./config/config-child.json");
t.deepEqual(actual, expected, "Config object not correct.");
});
test("configMultipleSync", (t) => {
t.plan(1);
const actual =
markdownlint.readConfigSync("./test/config/config-grandparent.json");
const expected = {
...require("./config/config-child.json"),
...require("./config/config-parent.json"),
...require("./config/config-grandparent.json")
};
delete expected.extends;
t.deepEqual(actual, expected, "Config object not correct.");
});
test("configBadFileSync", (t) => {
t.plan(1);
t.throws(
function badFileCall() {
markdownlint.readConfigSync("./test/config/config-badfile.json");
},
{
"message": /ENOENT/
},
"Did not get correct exception for bad file."
);
});
test("configBadChildFileSync", (t) => {
t.plan(1);
t.throws(
function badChildFileCall() {
markdownlint.readConfigSync("./test/config/config-badchildfile.json");
},
{
"message": /ENOENT/
},
"Did not get correct exception for bad child file."
);
});
test("configBadJsonSync", (t) => {
t.plan(1);
t.throws(
function badJsonCall() {
markdownlint.readConfigSync("./test/config/config-badjson.json");
},
{
"message":
/Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+/
},
"Did not get correct exception for bad JSON."
);
});
test("configBadChildJsonSync", (t) => {
t.plan(1);
t.throws(
function badChildJsonCall() {
markdownlint.readConfigSync("./test/config/config-badchildjson.json");
},
{
"message":
/Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+/
},
"Did not get correct exception for bad child JSON."
);
});
test("configSingleYamlSync", (t) => {
t.plan(1);
const actual = markdownlint.readConfigSync(
// @ts-ignore
"./test/config/config-child.yaml", [ require("js-yaml").load ]);
const expected = require("./config/config-child.json");
t.deepEqual(actual, expected, "Config object not correct.");
});
test("configMultipleYamlSync", (t) => {
t.plan(1);
const actual = markdownlint.readConfigSync(
// @ts-ignore
"./test/config/config-grandparent.yaml", [ require("js-yaml").load ]);
const expected = {
...require("./config/config-child.json"),
...require("./config/config-parent.json"),
...require("./config/config-grandparent.json")
};
delete expected.extends;
t.deepEqual(actual, expected, "Config object not correct.");
});
test("configMultipleHybridSync", (t) => {
t.plan(1);
const actual = markdownlint.readConfigSync(
"./test/config/config-grandparent-hybrid.yaml",
// @ts-ignore
[ JSON.parse, require("toml").parse, require("js-yaml").load ]);
const expected = {
...require("./config/config-child.json"),
...require("./config/config-parent.json"),
...require("./config/config-grandparent.json")
};
delete expected.extends;
t.like(actual, expected, "Config object not correct.");
});
test("configCustomFileSystemSync", (t) => {
t.plan(4);
const file = path.resolve("/dir/file.json");
const extended = path.resolve("/dir/extended.json");
const fileContent = {
"extends": extended,
"default": true,
"MD001": false
};
const extendedContent = {
"MD001": true,
"MD002": true
};
const fsApi = {
"accessSync": (p) => {
t.is(p, extended);
},
"readFileSync": (p) => {
switch (p) {
case file:
t.is(p, file);
return JSON.stringify(fileContent);
case extended:
t.is(p, extended);
return JSON.stringify(extendedContent);
default:
return t.fail();
}
}
};
const actual = markdownlint.readConfigSync(file, null, fsApi);
const expected = {
...extendedContent,
...fileContent
};
delete expected.extends;
t.deepEqual(actual, expected, "Config object not correct.");
});
test("configBadHybridSync", (t) => {
t.plan(1);
t.throws(
function badHybridCall() {
markdownlint.readConfigSync(
"./test/config/config-badcontent.txt",
// @ts-ignore
[ JSON.parse, require("toml").parse, require("js-yaml").load ]);
},
{
// eslint-disable-next-line max-len
"message": /^Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+;/
},
"Did not get correct exception for bad content."
);
});
test.cb("configSinglePromise", (t) => {
t.plan(1);
markdownlint.promises.readConfig("./test/config/config-child.json")
.then((actual) => {
const expected = require("./config/config-child.json");
t.deepEqual(actual, expected, "Config object not correct.");
t.end();
});
});
test.cb("configCustomFileSystemPromise", (t) => {
t.plan(4);
const file = path.resolve("/dir/file.json");
const extended = path.resolve("/dir/extended.json");
const fileContent = {
"extends": extended,
"default": true,
"MD001": false
};
const extendedContent = {
"MD001": true,
"MD002": true
};
const fsApi = {
"access": (p, m, cb) => {
t.is(p, extended);
return (cb || m)();
},
"readFile": (p, o, cb) => {
switch (p) {
case file:
t.is(p, file);
return cb(null, JSON.stringify(fileContent));
case extended:
t.is(p, extended);
return cb(null, JSON.stringify(extendedContent));
default:
return t.fail();
}
}
};
markdownlint.promises.readConfig(file, null, fsApi)
.then((actual) => {
const expected = {
...extendedContent,
...fileContent
};
delete expected.extends;
t.deepEqual(actual, expected, "Config object not correct.");
t.end();
});
});
test.cb("configBadFilePromise", (t) => {
t.plan(2);
markdownlint.promises.readConfig("./test/config/config-badfile.json")
.then(
null,
(error) => {
t.truthy(error, "Did not get an error for bad JSON.");
t.true(error instanceof Error, "Error not instance of Error.");
t.end();
}
);
});
test("allBuiltInRulesHaveValidUrl", (t) => {
t.plan(132);
t.plan(138);
rules.forEach(function forRule(rule) {
t.truthy(rule.information);
t.true(Object.getPrototypeOf(rule.information) === URL.prototype);
@ -1712,9 +1242,49 @@ test.cb("texmath test files with texmath plugin", (t) => {
});
});
test("token-map-spans", (t) => {
t.plan(38);
const options = {
"customRules": [
{
"names": [ "token-map-spans" ],
"description": "token-map-spans",
"tags": [ "tms" ],
"function": function tokenMapSpans(params) {
const tokenLines = [];
let lastLineNumber = -1;
const inlines = params.tokens.filter((c) => c.type === "inline");
for (const token of inlines) {
t.truthy(token.map);
for (let i = token.map[0]; i < token.map[1]; i++) {
if (tokenLines.includes(i)) {
t.true(
lastLineNumber === token.lineNumber,
`Line ${i + 1} is part of token maps from multiple lines.`
);
} else {
tokenLines.push(i);
}
lastLineNumber = token.lineNumber;
}
}
}
}
],
"files": [ "./test/token-map-spans.md" ]
};
markdownlint.sync(options);
});
test("getVersion", (t) => {
t.plan(1);
const actual = markdownlint.getVersion();
const expected = version;
t.is(actual, expected, "Version string not correct.");
});
test("constants", (t) => {
t.plan(2);
t.is(constants.homepage, homepage);
t.is(constants.version, version);
});

View file

@ -1,17 +1,19 @@
# Mixed Emphasis Markers
This paragraph *uses* both _kinds_ of emphasis marker.
This paragraph *uses* both _kinds_ of emphasis marker. {MD049}
This paragraph _uses_ both *kinds* of emphasis marker.
This paragraph _uses_ both *kinds* of emphasis marker. {MD049}
This paragraph *nests both _kinds_ of emphasis* marker.
This paragraph *nests both _kinds_ of emphasis* marker. {MD049}
This paragraph *nests both __kinds__ of emphasis* marker.
This paragraph **nests both __kinds__ of emphasis** marker.
This paragraph **nests both __kinds__ of emphasis** marker. {MD050}
This paragraph _nests both *kinds* of emphasis_ marker.
This paragraph _nests both *kinds* of emphasis_ marker. {MD049}
This paragraph _nests both **kinds** of emphasis_ marker.
This paragraph _nests both **kinds** of emphasis_ marker. {MD049} {MD050}
This paragraph __nests both **kinds** of emphasis__ marker.
This paragraph __nests both **kinds** of emphasis__ marker. {MD050}
<!-- markdownlint-disable-file MD037 -->

View file

@ -40,7 +40,7 @@ Quoted "Vue" and "vue-router"
Emphasized *Vue* and *vue-router*
Underscored _Vue_ and _vue-router_
Underscored _Vue_ and _vue-router_ {MD049}
Call it npm
But not Npm {MD044}

View file

@ -6,8 +6,6 @@ Quoted "Markdownlint" {MD044}
Emphasized *Markdownlint* {MD044}
Emphasized _Markdownlint_ {MD044}
JavaScript is a language
JavaScript is not Java
@ -52,7 +50,7 @@ HTML <u>javascript</u> {MD033} {MD044}
node.js is runtime {MD044}
```js
javascript is code {MD044} {MD046:54}
javascript is code {MD044} {MD046:52}
node.js is runtime {MD044}
```

View file

@ -2,7 +2,7 @@
|Pattern|Description|
|-------------|-----------------|
|`(?:\["'\](?<1>\[^"'\]*)["']|(?<1>\S+))`|...|
|`(?:\["'\](?<1>\[^"'\]*)["']|(?<1>\S+))`|{MD011}|
|Pattern|Description|
|-------------|-----------------|

View file

@ -4,7 +4,7 @@
const { filterTokens } = require("markdownlint-rule-helpers");
const eslint = require("eslint");
const cliEngine = new eslint.CLIEngine({});
const eslintInstance = new eslint.ESLint();
const linter = new eslint.Linter();
const languageJavaScript = /js|javascript/i;
@ -28,23 +28,27 @@ module.exports = {
"names": [ "lint-javascript" ],
"description": "Rule that lints JavaScript code",
"tags": [ "test", "lint", "javascript" ],
"asynchronous": true,
"function": (params, onError) => {
filterTokens(params, "fence", (fence) => {
if (languageJavaScript.test(fence.info)) {
let config = cliEngine.getConfigForFile(params.name);
config = cleanJsdocRulesFromEslintConfig(config);
const results = linter.verify(fence.content, config);
results.forEach((result) => {
const lineNumber = fence.lineNumber + result.line;
onError({
"lineNumber": lineNumber,
"detail": result.message,
"context": params.lines[lineNumber - 1]
return eslintInstance.calculateConfigForFile(params.name)
.then((config) => {
config = cleanJsdocRulesFromEslintConfig(config);
const results = linter.verify(fence.content, config);
results.forEach((result) => {
const lineNumber = fence.lineNumber + result.line;
onError({
"lineNumber": lineNumber,
"detail": result.message,
"context": params.lines[lineNumber - 1]
});
});
});
});
}
return Promise.resolve();
});
// Unused:
// Unsupported:
// filterTokens("code_block"), language unknown
// filterTokens("code_inline"), too brief
}

View file

@ -100,7 +100,7 @@ code
Mixed `code_span`
scenarios
are _also_ okay.
are _also_ okay. {MD049}
Mixed `code*span`
scenarios

View file

@ -1,5 +1,7 @@
# Heading
<!-- markdownlint-disable-file emphasis-style strong-style -->
Line with *Normal emphasis*
Line with **Normal strong**
@ -345,3 +347,13 @@ text [reference*link] star * star text
```yaml /* autogenerated */
# YAML...
```
new_value from *old_value* and *older_value*.
:ballot_box_with_check: _Emoji syntax_
some_snake_case_function() is _called_
_~/.ssh/id_rsa_ and _emphasis_
Partial *em*phasis of a *wo*rd.

View file

@ -50,3 +50,15 @@ Wrapped [ link with leading space
](https://example.com) {MD039}
Non-wrapped [ link with leading space](https://example.com) {MD039}
[][ref]
[link][ref]
[link ][ref] {MD039}
[ link][ref] {MD039}
[ link ][ref] {MD039}
[ref]: https://example.com

View file

@ -0,0 +1,6 @@
{
"default": true,
"MD050": {
"style": "asterisk"
}
}

View file

@ -0,0 +1,5 @@
# Strong style asterisk
This is **fine**
This is __not__ {MD050}

View file

@ -0,0 +1,6 @@
{
"default": true,
"MD050": {
"style": "underscore"
}
}

View file

@ -0,0 +1,5 @@
# Strong style underscore
This is __fine__
This is **not** {MD050}

View file

@ -0,0 +1,13 @@
# Table Content With Issues
| Content | Issue |
|------------------------------|---------|
| Text | N/A |
| (link)[https://example.com] | {MD011} |
| <hr> | {MD033} |
| https://example.com | {MD034} |
| * emphasis* | {MD037} |
| __strong __ | {MD037} |
| ` code` | {MD038} |
| [link ](https://example.com) | {MD039} |
| [link]() | {MD042} |

42
test/token-map-spans.md Normal file
View file

@ -0,0 +1,42 @@
# Token Map Spans
Text *emphasis* text __strong__ text `code` text [link](https://example.com).
Paragraph with *emphasis
spanning lines* and __strong
spanning lines__ and `code
spanning lines` and [link
spanning lines](https://example.com).
> Blockquote
> [link](https://example.com)
> > Nested
> > blockquote
> > [link](https://example.com)
Heading
-------
```lang
Fenced
code
```
Indented
code
1. List
2. List
- Sub-list
- Sub-list
3. List
| Table | Column 1 | Column 2 | Column 3 | Column 4 |
|-------|------------|------------|----------|----------------------------|
| Text | *emphasis* | __strong__ | `code` | [link](https://example.com) |
| Text | *emphasis* | __strong__ | `code` | [link](https://example.com) |
<!-- markdownlint-configure-file {
"code-block-style": false,
"heading-style": false
} -->