Enable custom rules to use the micromark parser, export micromark helpers for reuse.

This commit is contained in:
David Anson 2024-10-01 22:41:10 -07:00
parent 264da24dae
commit 5cc40c54b7
16 changed files with 4109 additions and 113 deletions

View file

@ -13,22 +13,31 @@ npm install markdownlint --save-dev
## Overview ## Overview
The [Markdown](https://en.wikipedia.org/wiki/Markdown) markup language The [Markdown][markdown] markup language is designed to be easy to read, write,
is designed to be easy to read, write, and understand. It succeeds - and understand. It succeeds - and its flexibility is both a benefit and a
and its flexibility is both a benefit and a drawback. Many styles are drawback. Many styles are possible, so formatting can be inconsistent; some
possible, so formatting can be inconsistent. Some constructs don't constructs don't work well in all parsers and should be avoided.
work well in all parsers and should be avoided. The
[CommonMark](https://commonmark.org/) specification standardizes
parsers - but not authors.
`markdownlint` is a `markdownlint` is a [static analysis][static-analysis] tool for
[static analysis](https://en.wikipedia.org/wiki/Static_program_analysis) [Node.js][nodejs] with a library of rules to enforce standards and consistency
tool for [Node.js](https://nodejs.org/) with a library of rules for Markdown files. It was inspired by - and heavily influenced by - Mark
to enforce standards and consistency for Markdown files. It was Harrison's [markdownlint][markdownlint-ruby] for Ruby. The initial rules, rule
inspired by - and heavily influenced by - Mark Harrison's documentation, and test cases came from that project.
[markdownlint](https://github.com/markdownlint/markdownlint) for
[Ruby](https://www.ruby-lang.org/). The initial rules, rule documentation, `markdownlint` uses the [`micromark`][micromark] parser and honors the
and test cases came directly from that project. [CommonMark][commonmark] specification for Markdown. It additionally supports
popular [GitHub Flavored Markdown (GFM)][gfm] syntax like autolinks and tables
as well as directives, footnotes, and math syntax - all implemented by
[`micromark` extensions][micromark-extensions].
[commonmark]: https://commonmark.org/
[gfm]: https://github.github.com/gfm/
[markdown]: https://en.wikipedia.org/wiki/Markdown
[markdownlint-ruby]: https://github.com/markdownlint/markdownlint
[micromark]: https://github.com/micromark/micromark
[micromark-extensions]: https://github.com/micromark/micromark?tab=readme-ov-file#list-of-extensions
[nodejs]: https://nodejs.org/
[static-analysis]: https://en.wikipedia.org/wiki/Static_program_analysis
### Related ### Related
@ -565,7 +574,7 @@ Type: `Array` of `Array` of `Function` and plugin parameters
Specifies additional [`markdown-it` plugins][markdown-it-plugin] to use when Specifies additional [`markdown-it` plugins][markdown-it-plugin] to use when
parsing input. Plugins can be used to support additional syntax and features for parsing input. Plugins can be used to support additional syntax and features for
advanced scenarios. advanced scenarios. *Deprecated.*
[markdown-it-plugin]: https://www.npmjs.com/search?q=keywords:markdown-it-plugin [markdown-it-plugin]: https://www.npmjs.com/search?q=keywords:markdown-it-plugin
@ -601,23 +610,20 @@ Specifies which version of the `result` object to return (see the "Usage"
section below for examples). section below for examples).
Passing a `resultVersion` of `0` corresponds to the original, simple format Passing a `resultVersion` of `0` corresponds to the original, simple format
where each error is identified by rule name and line number. *This is where each error is identified by rule name and line number. *Deprecated*
deprecated.*
Passing a `resultVersion` of `1` corresponds to a detailed format where each Passing a `resultVersion` of `1` corresponds to a detailed format where each
error includes information about the line number, rule name, alias, description, error includes information about the line number, rule name, alias, description,
as well as any additional detail or context that is available. *This is as well as any additional detail or context that is available. *Deprecated*
deprecated.*
Passing a `resultVersion` of `2` corresponds to a detailed format where each Passing a `resultVersion` of `2` corresponds to a detailed format where each
error includes information about the line number, rule names, description, as error includes information about the line number, rule names, description, as
well as any additional detail or context that is available. *This is well as any additional detail or context that is available. *Deprecated*
deprecated.*
Passing a `resultVersion` of `3` corresponds to the detailed version `2` format Passing a `resultVersion` of `3` corresponds to the detailed version `2` format
with additional information about how to fix automatically-fixable errors. In with additional information about how to fix automatically-fixable errors. In
this mode, all errors that occur on each line are reported (other versions this mode, all errors that occur on each line are reported (other versions
report only the first error for each rule). *This is the default.* report only the first error for each rule). This is the default behavior.
##### options.strings ##### options.strings
@ -946,10 +952,11 @@ Generate normal and minified scripts with:
npm run build-demo npm run build-demo
``` ```
Then reference `markdown-it` and `markdownlint`: Then reference `markdownlint` and `micromark` scripts:
```html ```html
<script src="demo/markdown-it.min.js"></script> <script src="demo/micromark-browser.js"></script>
<script src="demo/micromark-html-browser.js"></script>
<script src="demo/markdownlint-browser.min.js"></script> <script src="demo/markdownlint-browser.min.js"></script>
``` ```

View file

@ -1710,7 +1710,7 @@ function validateRuleList(ruleList, synchronous) {
!result && !result &&
(rule.parser !== undefined) && (rule.parser !== undefined) &&
(rule.parser !== "markdownit") && (rule.parser !== "markdownit") &&
!((customIndex < 0) && (rule.parser === "micromark")) && (rule.parser !== "micromark") &&
(rule.parser !== "none") (rule.parser !== "none")
) { ) {
result = newError("parser", rule.parser); result = newError("parser", rule.parser);

View file

@ -14,9 +14,8 @@ built-in rules.
For simple requirements like disallowing certain characters or patterns, For simple requirements like disallowing certain characters or patterns,
the community-developed the community-developed
[markdownlint-rule-search-replace][markdownlint-rule-search-replace] [markdownlint-rule-search-replace][markdownlint-rule-search-replace]
plug-in can be used. plug-in can be used. This plug-in allows anyone to create a set of simple
This plug-in allows anyone to create a set of simple text-replacement rules in text-replacement rules without needing to write code.
JSON without needing to write any code.
[markdownlint-rule-search-replace]: https://www.npmjs.com/package/markdownlint-rule-search-replace [markdownlint-rule-search-replace]: https://www.npmjs.com/package/markdownlint-rule-search-replace
@ -27,29 +26,62 @@ to more information, one or more tags, and a function that implements the rule's
behavior. That function is called once for each file/string input and is passed behavior. That function is called once for each file/string input and is passed
the parsed input and a function to log any violations. the parsed input and a function to log any violations.
A simple rule implementation looks like: Custom rules can (should) operate on a structured set of tokens based on the
[`micromark`][micromark] `parser` (this is preferred). Alternatively, custom
rules can operate on a structured set of tokens based on the
[`markdown-it`][markdown-it] `parser` (legacy support). Finally, custom rules
can operate directly on text with the `none` `parser`.
A simple rule implementation using the `micromark` parser to report a violation
for any use of blockquotes might look like:
```javascript ```javascript
/** @type import("markdownlint").Rule */ /** @type import("markdownlint").Rule */
module.exports = { module.exports = {
"names": [ "any-blockquote" ], "names": [ "any-blockquote-micromark" ],
"description": "Rule that reports an error for any blockquote",
"information": new URL("https://example.com/rules/any-blockquote"),
"tags": [ "test" ],
"parser": "micromark",
"function": (params, onError) => {
const blockquotes = params.parsers.micromark.tokens
.filter(((token) => token.type === "blockQuote"));
for (const blockquote of blockquotes) {
const lines = blockquote.endLine - blockquote.startLine + 1;
onError({
"lineNumber": blockquote.startLine,
"detail": "Blockquote spans " + lines + " line(s).",
"context": params.lines[blockquote.startLine - 1]
});
}
}
}
```
That same rule implemented using the `markdown-it` parser might look like:
```javascript
/** @type import("markdownlint").Rule */
module.exports = {
"names": [ "any-blockquote-markdown-it" ],
"description": "Rule that reports an error for any blockquote", "description": "Rule that reports an error for any blockquote",
"information": new URL("https://example.com/rules/any-blockquote"), "information": new URL("https://example.com/rules/any-blockquote"),
"tags": [ "test" ], "tags": [ "test" ],
"parser": "markdownit", "parser": "markdownit",
"function": function rule(params, onError) { "function": (params, onError) => {
params.parsers.markdownit.tokens.filter(function filterToken(token) { const blockquotes = params.parsers.markdownit.tokens
return token.type === "blockquote_open"; .filter(((token) => token.type === "blockquote_open"));
}).forEach(function forToken(blockquote) { for (const blockquote of blockquotes) {
var lines = blockquote.map[1] - blockquote.map[0]; const [ startIndex, endIndex ] = blockquote.map;
const lines = endIndex - startIndex;
onError({ onError({
"lineNumber": blockquote.lineNumber, "lineNumber": blockquote.lineNumber,
"detail": "Blockquote spans " + lines + " line(s).", "detail": "Blockquote spans " + lines + " line(s).",
"context": blockquote.line.substr(0, 7) "context": blockquote.line
}); });
}); }
} }
}; }
``` ```
A rule is implemented as an `Object`: A rule is implemented as an `Object`:
@ -62,9 +94,8 @@ A rule is implemented as an `Object`:
about the rule. about the rule.
- `tags` is a required `Array` of `String` values that groups related rules for - `tags` is a required `Array` of `String` values that groups related rules for
easier customization. easier customization.
- `parser` is a required `String` value `"markdownit" | "none"` that specifies - `parser` is a required `String` value `"markdownit" | "micromark" | "none"`
the parser data used via `params.parsers` (see below). that specifies the parser data used via `params.parsers` (see below).
- Note: The value `"micromark"` is valid but is NOT currently supported.
- `asynchronous` is an optional `Boolean` value that indicates whether the rule - `asynchronous` is an optional `Boolean` value that indicates whether the rule
returns a `Promise` and runs asynchronously. returns a `Promise` and runs asynchronously.
- `function` is a required `Function` that implements the rule and is passed two - `function` is a required `Function` that implements the rule and is passed two
@ -79,7 +110,10 @@ A rule is implemented as an `Object`:
- `tokens` is an `Array` of [`markdown-it` `Token`s][markdown-it-token] - `tokens` is an `Array` of [`markdown-it` `Token`s][markdown-it-token]
with added `line` and `lineNumber` properties. (This property was with added `line` and `lineNumber` properties. (This property was
previously on the `params` object.) previously on the `params` object.)
- Samples for `tokens` are available via [test snapshots][tokens]. - `micromark` is an `Object` that provides access to output from the
[`micromark`][micromark] parser.
- `tokens` is an `Array` of [`MicromarkToken`][micromark-token] objects.
- Samples for both `tokens` are available via [test snapshots][tokens].
- `lines` is an `Array` of `String` values corresponding to the lines of the - `lines` is an `Array` of `String` values corresponding to the lines of the
input file/string. input file/string.
- `frontMatterLines` is an `Array` of `String` values corresponding to any - `frontMatterLines` is an `Array` of `String` values corresponding to any
@ -152,6 +186,8 @@ exception.
[markdown-it]: https://github.com/markdown-it/markdown-it [markdown-it]: https://github.com/markdown-it/markdown-it
[markdown-it-token]: https://markdown-it.github.io/markdown-it/#Token [markdown-it-token]: https://markdown-it.github.io/markdown-it/#Token
[markdownlint-rule]: https://www.npmjs.com/search?q=keywords:markdownlint-rule [markdownlint-rule]: https://www.npmjs.com/search?q=keywords:markdownlint-rule
[micromark]: https://github.com/micromark/micromark
[micromark-token]: ../lib/markdownlint.d.ts
[rule-helpers]: https://www.npmjs.com/package/markdownlint-rule-helpers [rule-helpers]: https://www.npmjs.com/package/markdownlint-rule-helpers
[options-custom-rules]: ../README.md#optionscustomrules [options-custom-rules]: ../README.md#optionscustomrules
[test-rules]: ../test/rules [test-rules]: ../test/rules

1
helpers/.npmignore Normal file
View file

@ -0,0 +1 @@
test.js

View file

@ -3,7 +3,10 @@
"version": "0.26.0", "version": "0.26.0",
"description": "A collection of markdownlint helper functions for custom rules", "description": "A collection of markdownlint helper functions for custom rules",
"main": "./helpers.js", "main": "./helpers.js",
"exports": "./helpers.js", "exports": {
".": "./helpers.js",
"./micromark": "./micromark-helpers.cjs"
},
"author": "David Anson (https://dlaa.me/)", "author": "David Anson (https://dlaa.me/)",
"license": "MIT", "license": "MIT",
"homepage": "https://github.com/DavidAnson/markdownlint", "homepage": "https://github.com/DavidAnson/markdownlint",

28
helpers/test.cjs Normal file
View file

@ -0,0 +1,28 @@
// @ts-check
"use strict";
// eslint-disable-next-line n/no-extraneous-require
const test = require("ava").default;
const { "exports": packageExports, name } = require("../helpers/package.json");
const exportMappings = new Map([
[ ".", "../helpers/helpers.js" ],
[ "./micromark", "../helpers/micromark-helpers.cjs" ]
]);
test("exportMappings", (t) => {
t.deepEqual(
Object.keys(packageExports),
[ ...exportMappings.keys() ]
);
});
for (const [ exportName, exportPath ] of exportMappings) {
test(exportName, (t) => {
t.is(
require(exportName.replace(/^\./u, name)),
require(exportPath)
);
});
}

View file

@ -58,7 +58,7 @@ function validateRuleList(ruleList, synchronous) {
!result && !result &&
(rule.parser !== undefined) && (rule.parser !== undefined) &&
(rule.parser !== "markdownit") && (rule.parser !== "markdownit") &&
!((customIndex < 0) && (rule.parser === "micromark")) && (rule.parser !== "micromark") &&
(rule.parser !== "none") (rule.parser !== "none")
) { ) {
result = newError("parser", rule.parser); result = newError("parser", rule.parser);

View file

@ -53,7 +53,7 @@
"lint-test-repos": "ava --timeout=10m test/markdownlint-test-repos-*.js", "lint-test-repos": "ava --timeout=10m test/markdownlint-test-repos-*.js",
"serial-config-docs": "npm run build-config && npm run build-docs", "serial-config-docs": "npm run build-config && npm run build-docs",
"serial-declaration-demo": "npm run build-declaration && npm-run-all --continue-on-error --parallel build-demo test-declaration", "serial-declaration-demo": "npm run build-declaration && npm-run-all --continue-on-error --parallel build-demo test-declaration",
"test": "ava --timeout=30s test/markdownlint-test.js test/markdownlint-test-config.js test/markdownlint-test-custom-rules.js test/markdownlint-test-helpers.js test/markdownlint-test-micromark.mjs test/markdownlint-test-result-object.js test/markdownlint-test-scenarios.js", "test": "ava --timeout=30s test/markdownlint-test.js test/markdownlint-test-config.js test/markdownlint-test-custom-rules.js test/markdownlint-test-helpers.js test/markdownlint-test-micromark.mjs test/markdownlint-test-result-object.js test/markdownlint-test-scenarios.js helpers/test.cjs",
"test-cover": "c8 --100 npm test", "test-cover": "c8 --100 npm test",
"test-declaration": "cd example/typescript && tsc --module nodenext && tsc --module commonjs && node type-check.js", "test-declaration": "cd example/typescript && tsc --module nodenext && tsc --module commonjs && node type-check.js",
"test-extra": "ava --timeout=10m test/markdownlint-test-extra-parse.js test/markdownlint-test-extra-type.js", "test-extra": "ava --timeout=10m test/markdownlint-test-extra-parse.js test/markdownlint-test-extra-type.js",

View file

@ -7,6 +7,7 @@ const test = require("ava").default;
const markdownlint = require("../lib/markdownlint"); const markdownlint = require("../lib/markdownlint");
const customRules = require("./rules/rules.js"); const customRules = require("./rules/rules.js");
const { homepage, version } = require("../package.json"); const { homepage, version } = require("../package.json");
const { newLineRe } = require("../helpers/helpers.js");
test("customRulesV0", (t) => new Promise((resolve) => { test("customRulesV0", (t) => new Promise((resolve) => {
t.plan(4); t.plan(4);
@ -22,7 +23,8 @@ test("customRulesV0", (t) => new Promise((resolve) => {
t.falsy(err); t.falsy(err);
const expectedResult = {}; const expectedResult = {};
expectedResult[customRulesMd] = { expectedResult[customRulesMd] = {
"any-blockquote": [ 12 ], "any-blockquote-markdown-it": [ 12 ],
"any-blockquote-micromark": [ 12 ],
"every-n-lines": [ 2, 4, 6, 10, 12 ], "every-n-lines": [ 2, 4, 6, 10, 12 ],
"first-line": [ 1 ], "first-line": [ 1 ],
"letters-E-X": [ 3, 7 ] "letters-E-X": [ 3, 7 ]
@ -31,7 +33,9 @@ test("customRulesV0", (t) => new Promise((resolve) => {
// @ts-ignore // @ts-ignore
let actualMessage = actualResult.toString(); let actualMessage = actualResult.toString();
let expectedMessage = let expectedMessage =
"./test/custom-rules.md: 12: any-blockquote" + "./test/custom-rules.md: 12: any-blockquote-markdown-it" +
" Rule that reports an error for any blockquote\n" +
"./test/custom-rules.md: 12: any-blockquote-micromark" +
" Rule that reports an error for any blockquote\n" + " Rule that reports an error for any blockquote\n" +
"./test/custom-rules.md: 2: every-n-lines" + "./test/custom-rules.md: 2: every-n-lines" +
" Rule that reports an error every N lines\n" + " Rule that reports an error every N lines\n" +
@ -53,7 +57,9 @@ test("customRulesV0", (t) => new Promise((resolve) => {
// @ts-ignore // @ts-ignore
actualMessage = actualResult.toString(true); actualMessage = actualResult.toString(true);
expectedMessage = expectedMessage =
"./test/custom-rules.md: 12: any-blockquote" + "./test/custom-rules.md: 12: any-blockquote-markdown-it" +
" Rule that reports an error for any blockquote\n" +
"./test/custom-rules.md: 12: any-blockquote-micromark" +
" Rule that reports an error for any blockquote\n" + " Rule that reports an error for any blockquote\n" +
"./test/custom-rules.md: 2: every-n-lines" + "./test/custom-rules.md: 2: every-n-lines" +
" Rule that reports an error every N lines\n" + " Rule that reports an error every N lines\n" +
@ -91,13 +97,22 @@ test("customRulesV1", (t) => new Promise((resolve) => {
const expectedResult = {}; const expectedResult = {};
expectedResult[customRulesMd] = [ expectedResult[customRulesMd] = [
{ "lineNumber": 12, { "lineNumber": 12,
"ruleName": "any-blockquote", "ruleName": "any-blockquote-markdown-it",
"ruleAlias": "any-blockquote", "ruleAlias": "any-blockquote-markdown-it",
"ruleDescription": "Rule that reports an error for any blockquote", "ruleDescription": "Rule that reports an error for any blockquote",
"ruleInformation": "ruleInformation":
`${homepage}/blob/main/test/rules/any-blockquote.js`, `${homepage}/blob/main/test/rules/any-blockquote.js`,
"errorDetail": "Blockquote spans 1 line(s).", "errorDetail": "Blockquote spans 1 line(s).",
"errorContext": "> Block", "errorContext": "> Blockquote",
"errorRange": null },
{ "lineNumber": 12,
"ruleName": "any-blockquote-micromark",
"ruleAlias": "any-blockquote-micromark",
"ruleDescription": "Rule that reports an error for any blockquote",
"ruleInformation":
`${homepage}/blob/main/test/rules/any-blockquote.js`,
"errorDetail": "Blockquote spans 1 line(s).",
"errorContext": "> Blockquote",
"errorRange": null }, "errorRange": null },
{ "lineNumber": 2, { "lineNumber": 2,
"ruleName": "every-n-lines", "ruleName": "every-n-lines",
@ -170,9 +185,12 @@ test("customRulesV1", (t) => new Promise((resolve) => {
// @ts-ignore // @ts-ignore
const actualMessage = actualResult.toString(); const actualMessage = actualResult.toString();
const expectedMessage = const expectedMessage =
"./test/custom-rules.md: 12: any-blockquote/any-blockquote" + "./test/custom-rules.md: 12: any-blockquote-markdown-it/any-blockquote-markdown-it" +
" Rule that reports an error for any blockquote" + " Rule that reports an error for any blockquote" +
" [Blockquote spans 1 line(s).] [Context: \"> Block\"]\n" + " [Blockquote spans 1 line(s).] [Context: \"> Blockquote\"]\n" +
"./test/custom-rules.md: 12: any-blockquote-micromark/any-blockquote-micromark" +
" Rule that reports an error for any blockquote" +
" [Blockquote spans 1 line(s).] [Context: \"> Blockquote\"]\n" +
"./test/custom-rules.md: 2: every-n-lines/every-n-lines" + "./test/custom-rules.md: 2: every-n-lines/every-n-lines" +
" Rule that reports an error every N lines [Line number 2]\n" + " Rule that reports an error every N lines [Line number 2]\n" +
"./test/custom-rules.md: 4: every-n-lines/every-n-lines" + "./test/custom-rules.md: 4: every-n-lines/every-n-lines" +
@ -211,12 +229,20 @@ test("customRulesV2", (t) => new Promise((resolve) => {
const expectedResult = {}; const expectedResult = {};
expectedResult[customRulesMd] = [ expectedResult[customRulesMd] = [
{ "lineNumber": 12, { "lineNumber": 12,
"ruleNames": [ "any-blockquote" ], "ruleNames": [ "any-blockquote-markdown-it" ],
"ruleDescription": "Rule that reports an error for any blockquote", "ruleDescription": "Rule that reports an error for any blockquote",
"ruleInformation": "ruleInformation":
`${homepage}/blob/main/test/rules/any-blockquote.js`, `${homepage}/blob/main/test/rules/any-blockquote.js`,
"errorDetail": "Blockquote spans 1 line(s).", "errorDetail": "Blockquote spans 1 line(s).",
"errorContext": "> Block", "errorContext": "> Blockquote",
"errorRange": null },
{ "lineNumber": 12,
"ruleNames": [ "any-blockquote-micromark" ],
"ruleDescription": "Rule that reports an error for any blockquote",
"ruleInformation":
`${homepage}/blob/main/test/rules/any-blockquote.js`,
"errorDetail": "Blockquote spans 1 line(s).",
"errorContext": "> Blockquote",
"errorRange": null }, "errorRange": null },
{ "lineNumber": 2, { "lineNumber": 2,
"ruleNames": [ "every-n-lines" ], "ruleNames": [ "every-n-lines" ],
@ -281,9 +307,12 @@ test("customRulesV2", (t) => new Promise((resolve) => {
// @ts-ignore // @ts-ignore
const actualMessage = actualResult.toString(); const actualMessage = actualResult.toString();
const expectedMessage = const expectedMessage =
"./test/custom-rules.md: 12: any-blockquote" + "./test/custom-rules.md: 12: any-blockquote-markdown-it" +
" Rule that reports an error for any blockquote" + " Rule that reports an error for any blockquote" +
" [Blockquote spans 1 line(s).] [Context: \"> Block\"]\n" + " [Blockquote spans 1 line(s).] [Context: \"> Blockquote\"]\n" +
"./test/custom-rules.md: 12: any-blockquote-micromark" +
" Rule that reports an error for any blockquote" +
" [Blockquote spans 1 line(s).] [Context: \"> Blockquote\"]\n" +
"./test/custom-rules.md: 2: every-n-lines" + "./test/custom-rules.md: 2: every-n-lines" +
" Rule that reports an error every N lines [Line number 2]\n" + " Rule that reports an error every N lines [Line number 2]\n" +
"./test/custom-rules.md: 4: every-n-lines" + "./test/custom-rules.md: 4: every-n-lines" +
@ -328,7 +357,8 @@ test("customRulesConfig", (t) => new Promise((resolve) => {
t.falsy(err); t.falsy(err);
const expectedResult = {}; const expectedResult = {};
expectedResult[customRulesMd] = { expectedResult[customRulesMd] = {
"any-blockquote": [ 12 ], "any-blockquote-markdown-it": [ 12 ],
"any-blockquote-micromark": [ 12 ],
"every-n-lines": [ 3, 6, 12 ], "every-n-lines": [ 3, 6, 12 ],
"first-line": [ 1 ], "first-line": [ 1 ],
"letters-E-X": [ 7 ] "letters-E-X": [ 7 ]
@ -402,7 +432,7 @@ test("customRulesBadProperty", (t) => {
]) { ]) {
const { propertyName, propertyValues } = testCase; const { propertyName, propertyValues } = testCase;
for (const propertyValue of propertyValues) { for (const propertyValue of propertyValues) {
const badRule = { ...customRules.anyBlockquote }; const badRule = { ...customRules.firstLine };
badRule[propertyName] = propertyValue; badRule[propertyName] = propertyValue;
// eslint-disable-next-line jsdoc/valid-types // eslint-disable-next-line jsdoc/valid-types
/** @type import("../lib/markdownlint").Options */ /** @type import("../lib/markdownlint").Options */
@ -592,7 +622,7 @@ test("customRulesParserMarkdownIt", (t) => {
}); });
test("customRulesParserMicromark", (t) => { test("customRulesParserMicromark", (t) => {
t.plan(1); t.plan(5);
// eslint-disable-next-line jsdoc/valid-types // eslint-disable-next-line jsdoc/valid-types
/** @type import("../lib/markdownlint").Options */ /** @type import("../lib/markdownlint").Options */
const options = { const options = {
@ -603,12 +633,12 @@ test("customRulesParserMicromark", (t) => {
"tags": [ "tag" ], "tags": [ "tag" ],
"parser": "micromark", "parser": "micromark",
"function": "function":
() => { (params) => {
// t.false(Object.keys(params).includes("tokens")); t.false(Object.keys(params).includes("tokens"));
// t.is(Object.keys(params.parsers).length, 1); t.is(Object.keys(params.parsers).length, 1);
// t.truthy(params.parsers.micromark); t.truthy(params.parsers.micromark);
// t.is(Object.keys(params.parsers.micromark).length, 1); t.is(Object.keys(params.parsers.micromark).length, 1);
// t.truthy(params.parsers.micromark.tokens); t.truthy(params.parsers.micromark.tokens);
} }
} }
], ],
@ -616,10 +646,7 @@ test("customRulesParserMicromark", (t) => {
"string": "# Heading\n" "string": "# Heading\n"
} }
}; };
return markdownlint.promises.markdownlint(options).catch((error) => { return markdownlint.promises.markdownlint(options).then(() => null);
// parser "micromark" currently unsupported for custom rules
t.is(error.message, "Property 'parser' of custom rule at index 0 is incorrect: 'micromark'.");
});
}); });
test("customRulesMarkdownItParamsTokensSameObject", (t) => { test("customRulesMarkdownItParamsTokensSameObject", (t) => {
@ -664,10 +691,41 @@ test("customRulesMarkdownItTokensSnapshot", (t) => {
} }
} }
], ],
"files": "./test/every-markdown-syntax.md",
"noInlineConfig": true "noInlineConfig": true
}; };
return markdownlint.promises.markdownlint(options).then(() => null); return fs
.readFile("./test/every-markdown-syntax.md", "utf8")
.then((content) => {
options.strings = { "content": content.split(newLineRe).join("\n") };
return markdownlint.promises.markdownlint(options).then(() => null);
});
});
test("customRulesMicromarkTokensSnapshot", (t) => {
t.plan(1);
// eslint-disable-next-line jsdoc/valid-types
/** @type import("../lib/markdownlint").Options */
const options = {
"customRules": [
{
"names": [ "name" ],
"description": "description",
"tags": [ "tag" ],
"parser": "micromark",
"function":
(params) => {
t.snapshot(params.parsers.micromark.tokens, "Unexpected tokens");
}
}
],
"noInlineConfig": true
};
return fs
.readFile("./test/every-markdown-syntax.md", "utf8")
.then((content) => {
options.strings = { "content": content.split(newLineRe).join("\n") };
return markdownlint.promises.markdownlint(options).then(() => null);
});
}); });
test("customRulesDefinitionStatic", (t) => new Promise((resolve) => { test("customRulesDefinitionStatic", (t) => new Promise((resolve) => {

View file

@ -17,26 +17,9 @@ const testTokens = new Promise((resolve, reject) => {
testContent.then(parse).then(resolve, reject); testContent.then(parse).then(resolve, reject);
}); });
const cloneToken = (token) => {
for (const child of token.children) {
const expectedParent = (token.type ? token : null);
if (child.parent !== expectedParent) {
throw new Error("Unexpected parent.");
}
}
const clone = { ...token };
delete clone.parent;
clone.children = clone.children.map(cloneToken);
return clone;
};
const cloneTokens = (tokens) => (
cloneToken({ "children": tokens }).children
);
test("parse", async(t) => { test("parse", async(t) => {
t.plan(1); t.plan(1);
t.snapshot(cloneTokens(await testTokens), "Unexpected tokens"); t.snapshot(await testTokens, "Unexpected tokens");
}); });
test("getEvents/filterByPredicate", async(t) => { test("getEvents/filterByPredicate", async(t) => {

View file

@ -2,26 +2,54 @@
"use strict"; "use strict";
/** @type import("../../lib/markdownlint").Rule */ /** @type import("../../lib/markdownlint").Rule[] */
module.exports = { module.exports = [
"names": [ "any-blockquote" ],
"description": "Rule that reports an error for any blockquote", // micromark parser (preferred)
"information": new URL( {
"https://github.com/DavidAnson/markdownlint" + "names": [ "any-blockquote-micromark" ],
"/blob/main/test/rules/any-blockquote.js" "description": "Rule that reports an error for any blockquote",
), "information": new URL(
"tags": [ "test" ], "https://github.com/DavidAnson/markdownlint/blob/main/test/rules/any-blockquote.js"
"parser": "markdownit", ),
"function": (params, onError) => { "tags": [ "test" ],
const blockquotes = params.parsers.markdownit.tokens "parser": "micromark",
.filter(((token) => token.type === "blockquote_open")); "function": (params, onError) => {
for (const blockquote of blockquotes) { const blockquotes = params.parsers.micromark.tokens
const lines = blockquote.map[1] - blockquote.map[0]; .filter(((token) => token.type === "blockQuote"));
onError({ for (const blockquote of blockquotes) {
"lineNumber": blockquote.lineNumber, const lines = blockquote.endLine - blockquote.startLine + 1;
"detail": "Blockquote spans " + lines + " line(s).", onError({
"context": blockquote.line.substr(0, 7) "lineNumber": blockquote.startLine,
}); "detail": "Blockquote spans " + lines + " line(s).",
"context": params.lines[blockquote.startLine - 1]
});
}
}
},
// markdown-it parser (legacy)
{
"names": [ "any-blockquote-markdown-it" ],
"description": "Rule that reports an error for any blockquote",
"information": new URL(
"https://github.com/DavidAnson/markdownlint/blob/main/test/rules/any-blockquote.js"
),
"tags": [ "test" ],
"parser": "markdownit",
"function": (params, onError) => {
const blockquotes = params.parsers.markdownit.tokens
.filter(((token) => token.type === "blockquote_open"));
for (const blockquote of blockquotes) {
const [ startIndex, endIndex ] = blockquote.map;
const lines = endIndex - startIndex;
onError({
"lineNumber": blockquote.lineNumber,
"detail": "Blockquote spans " + lines + " line(s).",
"context": blockquote.line
});
}
} }
} }
};
];

View file

@ -21,7 +21,7 @@ const validateJson = require("./validate-json");
module.exports.validateJson = validateJson; module.exports.validateJson = validateJson;
module.exports.all = [ module.exports.all = [
anyBlockquote, ...anyBlockquote,
everyNLines, everyNLines,
firstLine, firstLine,
lettersEX, lettersEX,

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff