Skip to content

Commit

Permalink
Enable custom rules to use the micromark parser, export micromark hel…
Browse files Browse the repository at this point in the history
…pers for reuse.
  • Loading branch information
DavidAnson committed Oct 2, 2024
1 parent 264da24 commit 5cc40c5
Show file tree
Hide file tree
Showing 16 changed files with 4,110 additions and 114 deletions.
59 changes: 33 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,31 @@ npm install markdownlint --save-dev

## Overview

The [Markdown](https://en.wikipedia.org/wiki/Markdown) markup language
is designed to be easy to read, write, and understand. It succeeds -
and its flexibility is both a benefit and a drawback. Many styles are
possible, so formatting can be inconsistent. Some constructs don't
work well in all parsers and should be avoided. The
[CommonMark](https://commonmark.org/) specification standardizes
parsers - but not authors.

`markdownlint` is a
[static analysis](https://en.wikipedia.org/wiki/Static_program_analysis)
tool for [Node.js](https://nodejs.org/) with a library of rules
to enforce standards and consistency for Markdown files. It was
inspired by - and heavily influenced by - Mark Harrison's
[markdownlint](https://github.com/markdownlint/markdownlint) for
[Ruby](https://www.ruby-lang.org/). The initial rules, rule documentation,
and test cases came directly from that project.
The [Markdown][markdown] markup language is designed to be easy to read, write,
and understand. It succeeds - and its flexibility is both a benefit and a
drawback. Many styles are possible, so formatting can be inconsistent; some
constructs don't work well in all parsers and should be avoided.

`markdownlint` is a [static analysis][static-analysis] tool for
[Node.js][nodejs] with a library of rules to enforce standards and consistency
for Markdown files. It was inspired by - and heavily influenced by - Mark
Harrison's [markdownlint][markdownlint-ruby] for Ruby. The initial rules, rule
documentation, and test cases came from that project.

`markdownlint` uses the [`micromark`][micromark] parser and honors the
[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

Expand Down Expand Up @@ -565,7 +574,7 @@ Type: `Array` of `Array` of `Function` and plugin parameters

Specifies additional [`markdown-it` plugins][markdown-it-plugin] to use when
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

Expand Down Expand Up @@ -601,23 +610,20 @@ Specifies which version of the `result` object to return (see the "Usage"
section below for examples).

Passing a `resultVersion` of `0` corresponds to the original, simple format
where each error is identified by rule name and line number. *This is
deprecated.*
where each error is identified by rule name and line number. *Deprecated*

Passing a `resultVersion` of `1` corresponds to a detailed format where each
error includes information about the line number, rule name, alias, description,
as well as any additional detail or context that is available. *This is
deprecated.*
as well as any additional detail or context that is available. *Deprecated*

Passing a `resultVersion` of `2` corresponds to a detailed format where each
error includes information about the line number, rule names, description, as
well as any additional detail or context that is available. *This is
deprecated.*
well as any additional detail or context that is available. *Deprecated*

Passing a `resultVersion` of `3` corresponds to the detailed version `2` format
with additional information about how to fix automatically-fixable errors. In
this mode, all errors that occur on each line are reported (other versions
report only the first error for each rule). *This is the default.*
report only the first error for each rule). This is the default behavior.

##### options.strings

Expand Down Expand Up @@ -946,10 +952,11 @@ Generate normal and minified scripts with:
npm run build-demo
```

Then reference `markdown-it` and `markdownlint`:
Then reference `markdownlint` and `micromark` scripts:

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

Expand Down
2 changes: 1 addition & 1 deletion demo/markdownlint-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -1710,7 +1710,7 @@ function validateRuleList(ruleList, synchronous) {
!result &&
(rule.parser !== undefined) &&
(rule.parser !== "markdownit") &&
!((customIndex < 0) && (rule.parser === "micromark")) &&
(rule.parser !== "micromark") &&
(rule.parser !== "none")
) {
result = newError("parser", rule.parser);
Expand Down
70 changes: 53 additions & 17 deletions doc/CustomRules.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ built-in rules.
For simple requirements like disallowing certain characters or patterns,
the community-developed
[markdownlint-rule-search-replace][markdownlint-rule-search-replace]
plug-in can be used.
This plug-in allows anyone to create a set of simple text-replacement rules in
JSON without needing to write any code.
plug-in can be used. This plug-in allows anyone to create a set of simple
text-replacement rules without needing to write code.

[markdownlint-rule-search-replace]: https://www.npmjs.com/package/markdownlint-rule-search-replace

Expand All @@ -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
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
/** @type import("markdownlint").Rule */
module.exports = {
"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" ],
"names": [ "any-blockquote-markdown-it" ],
"description": "Rule that reports an error for any blockquote",
"information": new URL("https://example.com/rules/any-blockquote"),
"tags": [ "test" ],
"parser": "markdownit",
"function": function rule(params, onError) {
params.parsers.markdownit.tokens.filter(function filterToken(token) {
return token.type === "blockquote_open";
}).forEach(function forToken(blockquote) {
var lines = blockquote.map[1] - blockquote.map[0];
"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.substr(0, 7)
"context": blockquote.line
});
});
}
}
};
}
```

A rule is implemented as an `Object`:
Expand All @@ -62,9 +94,8 @@ A rule is implemented as an `Object`:
about the rule.
- `tags` is a required `Array` of `String` values that groups related rules for
easier customization.
- `parser` is a required `String` value `"markdownit" | "none"` that specifies
the parser data used via `params.parsers` (see below).
- Note: The value `"micromark"` is valid but is NOT currently supported.
- `parser` is a required `String` value `"markdownit" | "micromark" | "none"`
that specifies the parser data used via `params.parsers` (see below).
- `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
Expand All @@ -79,7 +110,10 @@ A rule is implemented as an `Object`:
- `tokens` is an `Array` of [`markdown-it` `Token`s][markdown-it-token]
with added `line` and `lineNumber` properties. (This property was
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
input file/string.
- `frontMatterLines` is an `Array` of `String` values corresponding to any
Expand Down Expand Up @@ -152,6 +186,8 @@ exception.
[markdown-it]: https://github.com/markdown-it/markdown-it
[markdown-it-token]: https://markdown-it.github.io/markdown-it/#Token
[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
[options-custom-rules]: ../README.md#optionscustomrules
[test-rules]: ../test/rules
Expand Down
1 change: 1 addition & 0 deletions helpers/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test.js
5 changes: 4 additions & 1 deletion helpers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
"version": "0.26.0",
"description": "A collection of markdownlint helper functions for custom rules",
"main": "./helpers.js",
"exports": "./helpers.js",
"exports": {
".": "./helpers.js",
"./micromark": "./micromark-helpers.cjs"
},
"author": "David Anson (https://dlaa.me/)",
"license": "MIT",
"homepage": "https://github.com/DavidAnson/markdownlint",
Expand Down
28 changes: 28 additions & 0 deletions helpers/test.cjs
Original file line number Diff line number Diff line change
@@ -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)
);
});
}
2 changes: 1 addition & 1 deletion lib/markdownlint.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ function validateRuleList(ruleList, synchronous) {
!result &&
(rule.parser !== undefined) &&
(rule.parser !== "markdownit") &&
!((customIndex < 0) && (rule.parser === "micromark")) &&
(rule.parser !== "micromark") &&
(rule.parser !== "none")
) {
result = newError("parser", rule.parser);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"lint-test-repos": "ava --timeout=10m test/markdownlint-test-repos-*.js",
"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",
"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-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",
Expand Down
Loading

0 comments on commit 5cc40c5

Please sign in to comment.