feat: use go tool

This commit is contained in:
2026-07-03 19:24:01 -04:00
parent 39548b4332
commit 4d0cab1fe0
1542 changed files with 252399 additions and 14 deletions
+520
View File
@@ -0,0 +1,520 @@
# Changelog
## 0.40.0
- Improve MD011/MD013/MD051/MD060
- Update dependencies
## 0.39.0
- Add MD060/table-column-style
- Improve MD001/MD007/MD009/MD010/MD029/MD033/MD037/MD059
- Add support for reporting violations as severity `warning`
- Deprecate `resultVersion` and `toString` (breaking change)
- Improve type definitions
- Improve demo web page
- Update dependencies
## 0.38.0
- Add MD059/descriptive-link-text
- Improve MD025/MD027/MD036/MD038/MD041/MD043/MD045/MD051/MD052
- `markdown-it` parser no longer a production dependency (breaking change)
- Add `markdownItFactory` option, remove `markdownItPlugins` option
- Remove support for end-of-life Node version 18
- Improve performance
- Update dependencies
## 0.37.4
- Stop using `module.createRequire`, export `resolveModule`
## 0.37.3
- Tweak `package.json` dependencies to work with `pnpm`
## 0.37.2
- Add subpath imports for overriding default bundler behavior
- Improve MD032
## 0.37.1
- Add support for "browser" condition (as used by webpack)
## 0.37.0
- Convert module to ECMAScript (breaking change)
- <https://nodejs.org/docs/latest/api/esm.html>
- <https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c>
- Convert module to named exports (breaking change)
## 0.36.1
- Fix behavior of MD054
## 0.36.0
- Improve MD051
- Move `applyFix` and `applyFixes` from helpers to core
- Make `micromark` parser available to custom rules
- Introduce `./micromark` helpers exports
- Update custom/rule documentation
- Improve performance
- Update dependencies
## 0.35.0
- Add MD058/blanks-around-tables
- Use `micromark` in MD001/MD003/MD009/MD010/MD013/MD014/MD019/MD021/MD023/
MD024/MD025/MD039/MD042/MD043
- Improve MD018/MD020/MD031/MD034/MD044
- `markdown-it` parser no longer invoked by default
- Add strict version of JSON schema
- Improve performance
- Update dependencies
## 0.34.0
- Use `micromark` in MD027/MD028/MD036/MD040/MD041/MD046/MD048
- Improve MD013/MD034/MD049/MD050/MD051
- Update custom rule requirements and documentation
- Improve various TypeScript declarations
- Update dependencies
## 0.33.0
- Add MD055/table-pipe-style, MD056/table-column-count
- Improve MD005/MD007/MD024/MD026/MD038
- Incorporate `micromark-extension-directive`
- Improve JSON schema, document validation
- Reduce size of browser script
- Update dependencies
## 0.32.1
- Fix behavior of MD054
## 0.32.0
- Remove deprecated MD002/MD006
- Remove rule aliases for "header"
- Add MD054/link-image-style
- Use `micromark` in MD005/MD007/MD030
- Improve MD022/MD026/MD034/MD037/MD038/MD045/MD051
- Improve JSON schema and related examples
- Provide type declaration for Configuration object
- Remove support for end-of-life Node version 16
- Update dependencies
## 0.31.1
- Improve MD032/MD034
- Update dependencies
## 0.31.0
- Improve MD032/MD037/MD043/MD044/MD051/MD052
- Improve performance
- Update dependencies
## 0.30.0
- Use `micromark` in MD022/MD026/MD032/MD037/MD045/MD051
- Incorporate `micromark-extension-math` for math syntax
- Allow custom rules to override information URL
- Update dependencies
## 0.29.0
- Update `micromark` parser dependencies for better performance
- Use `micromark` in MD049/MD050
- Improve MD034/MD037/MD044/MD049/MD050
- Support multiple parsers in demo page
- Remove support for end-of-life Node version 14
- Update dependencies
## 0.28.2
- Update dependencies for CVE-2023-2251
## 0.28.1
- Update dependencies
## 0.28.0
- Introduce `micromark` parser for better positional data (internal only)
- Use `micromark` in MD013/MD033/MD034/MD035/MD038/MD044/MD052/MD053
- Simplify file-based test cases
- Unify browser script for demo page
- Update dependencies
## 0.27.0
- Improve MD011/MD013/MD022/MD031/MD032/MD033/MD034/MD040/MD043/MD051/MD053
- Generate/separate documentation
- Improve documentation
- Update dependencies
## 0.26.2
- Improve MD037/MD051/MD053
## 0.26.1
- Improve MD051
## 0.26.0
- Add MD051/MD052/MD053 for validating link fragments & reference
links/images & link/image reference definitions (MD053 auto-fixable)
- Improve MD010/MD031/MD035/MD039/MD042/MD044/MD049/MD050
- Add `markdownlint-disable-line` inline comment
- Support `~` paths in `readConfig/Sync`
- Add `configParsers` option
- Remove support for end-of-life Node version 12
- Default `resultVersion` to 3
- Update browser script to use ES2015
- Simplify JSON schema
- Address remaining CodeQL issues
- Improve performance
- Update dependencies
## 0.25.1
- Update dependencies for CVE-2022-21670
## 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
## 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.23.1
- Work around lack of webpack support for dynamic calls to `require`(`.resolve`)
## 0.23.0
- Add comprehensive example `.markdownlint.jsonc`/`.markdownlint.yaml` files
- Add fix information for MD004/ul-style
- Improve MD018/MD019/MD020/MD021/MD037/MD041
- Improve HTML comment handling
- Update test runner and test suite
- Update dependencies
## 0.22.0
- Allow `extends` in config to reference installed packages by name
- Add `markdownlint-disable-next-line` inline comment
- Support JSON front matter
- Improve MD009/MD026/MD028/MD043
- Update dependencies (including `markdown-it` to v12)
## 0.21.1
- Improve MD011/MD031
- Export `getVersion` API
## 0.21.0
- Lint concurrently for better performance (async only)
- Add Promise-based APIs
- Update TypeScript declaration file
- Hide `toString` on `LintResults`
- Add ability to fix in browser demo
- Allow custom rules in `.markdownlint.json` schema
- Improve MD042/MD044
- Improve documentation
- Update dependencies
## 0.20.4
- Fix regression in MD037
- Improve MD034/MD044
- Improve documentation
## 0.20.3
- Fix regression in MD037
- Improve MD044
- Add automatic regression testing
## 0.20.2
- Fix regression in MD037
- Improve MD038
## 0.20.1
- Fix regression in MD037
## 0.20.0
- Add `markdownlint-configure-file` inline comment
- Reimplement MD037
- Improve MD005/MD007/MD013/MD018/MD029/MD031/MD034/MD038/MD039
- Improve HTML comment handling
- Update dependencies
## 0.19.0
- Remove support for end-of-life Node version 8
- Add fix information for MD005/list-indent
- Improve MD007/MD013/MD014
- Deprecate MD006/ul-start-left
- Add rationale for every rule
- Update test runner and code coverage
- Add more JSDoc comments
- Update dependencies
## 0.18.0
- Add MD048/code-fence-style
- Add fix information for MD007/ul-indent
- Add `markdownlint-disable-file`/`markdownlint-enable-file` inline comments
- Add type declaration file (.d.ts) for TypeScript dependents
- Update schema
- Improve MD006/MD007/MD009/MD013/MD030
- Update dependencies
## 0.17.2
- Improve MD020/MD033/MD044
## 0.17.1
- Fix handling of front matter by fix information
## 0.17.0
- Add `resultVersion` 3 to support fix information for default and custom rules
- Add fix information for 24 rules
- Update newline handling to match latest CommonMark specification
- Improve MD014/MD037/MD039
- Update dependencies
## 0.16.0
- Add custom rule sample for linting code
- Improve MD026/MD031/MD033/MD038
- Update dependencies
## 0.15.0
- Add `markdownlint-capture`/`markdownlint-restore` inline comments
- Improve MD009/MD013/MD026/MD033/MD036
- Update dependencies
## 0.14.2
- Improve MD047
- Add `handleRuleFailures` option
## 0.14.1
- Improve MD033
## 0.14.0
- Remove support for end-of-life Node version 6
- Introduce `markdownlint-rule-helpers`
- Add MD046/MD047
- Improve MD033/MD034/MD039
- Improve custom rule validation and in-browser demo
- Update dependencies
## 0.13.0
- Improve MD013/MD022/MD025/MD029/MD031/MD032/MD037/MD041
- Deprecate MD002
- Improve Pandoc YAML support
- Update dependencies
## 0.12.0
- Add `information` link for custom rules
- Add `markdownItPlugins` for extensibility
- Improve MD023/MD032/MD038
- Update dependencies
## 0.11.0
- Improve MD005/MD024/MD029/MD038
- Improve custom rule example
- Add `CONTRIBUTING.md`
- Update dependencies
## 0.10.0
- Add support for non-JSON configuration files
- Pass file/string name to custom rules
- Update dependencies
## 0.9.0
- Remove support for end-of-life Node versions 0.10/0.12/4
- Change "header" to "heading" per spec (non-breaking)
- Improve MD003/MD009/MD041
- Handle uncommon line-break characters
- Refactor for ES6
- Update dependencies
## 0.8.1
- Update item loop to be iterative
- Improve MD014
- Update dependencies
## 0.8.0
- Add support for using and authoring custom rules
- Improve MD004/MD007/MD013
- Add `engines` to `package.json`
- Refactor
- Update dependencies
## 0.7.0
- `resultVersion` defaults to 2 (breaking change)
- Add MD045
- Improve MD029
- Remove `trimLeft`/`trimRight`
- Split rules
- Refactor
- Update dependencies
## 0.6.4
- Improve MD029/MD042
- Update dependencies
## 0.6.3
- Improve highlighting for MD020
## 0.6.2
- Improve MD013/MD027/MD034/MD037/MD038/MD041/MD044
- Update dependencies
## 0.6.1
- Update `markdown-it` versioning
- Exclude demo/test from publishing
## 0.6.0
- `resultVersion` defaults to 1 (breaking change)
- Ignore HTML comments
- TOML front matter
- Fixes for MD044
- Update dependencies
## 0.5.0
- Add shareable configuration
- Add `noInlineConfig` option
- Add `README.md` links
- Fix MD030
- Improve MD009/MD041
- Update dependencies
## 0.4.1
- Fixes for MD038/front matter
- Improvements to MD044
- Update dependencies
## 0.4.0
- Add MD044
- Enhance MD013/MD032/MD041/MD042/MD043
- Fix for MD038
- Update dependencies
## 0.3.1
- Fix regressions in MD032/MD038
- Update dependencies
## 0.3.0
- More detailed error reporting with `resultVersion`
- Enhance MD010/MD012/MD036
- Fixes for MD027/MD029/MD030
- Include JSON schema dependencies
## 0.2.0
- Add MD042/MD043
- Enhance MD002/MD003/MD004/MD007/MD011/MD025/MD041
- Update dependencies
## 0.1.1
- Fix bug handling HTML in tables
- Reference `markdownlint-cli`
## 0.1.0
- Add aliases
- Exceptions for MD033
- Exclusions for MD013
- Update dependencies
## 0.0.8
- Support disabling/enabling rules inline
- Improve code fence
- Update dependencies
## 0.0.7
- Add MD041
- Improve MD003
- Ignore front matter
- Update dependencies
## 0.0.6
- Improve performance
- Simplify in-browser
- Update dependencies
## 0.0.5
- Add `strings` option to enable file-less scenarios
- Add in-browser demo
## 0.0.4
- Add tests MD033-MD040
- Update dependencies
## 0.0.3
- Add synchronous API
- Improve documentation and code
## 0.0.2
- Improve documentation, tests, and code
## 0.0.1
- Initial release
- Includes tests MD001-MD032
+92
View File
@@ -0,0 +1,92 @@
# Contributing
Interested in contributing? Great! Here are some suggestions to make it a good
experience:
Start by [opening an issue](https://github.com/DavidAnson/markdownlint/issues),
whether to identify a problem or outline a change. That issue should be used to
discuss the situation and agree on a plan of action before writing code or
sending a pull request. Maybe the problem isn't really a problem, or maybe there
are more things to consider. If so, it's best to realize that before spending
time and effort writing code that may not get used.
Match the coding style of the files you edit. Although everyone has their own
preferences and opinions, a pull request is not the right forum to debate them.
Do not add new [`dependencies` to `package.json`][dependencies]. The Markdown
parser [`micromark`][micromark] (and its extensions) is this project's only
dependency.
Package versions for `dependencies` and `devDependencies` should be specified
exactly (also known as "pinning"). The short explanation is that doing otherwise
eventually leads to inconsistent behavior and broken functionality. (See [Why I
pin dependency versions in Node.js packages][version-pinning] for a longer
explanation.)
If developing a new rule, start by creating a [custom rule][custom-rules] in its
own project. Once written, published, and tested in real world scenarios, open
an issue to consider adding it to this project. For rule ideas, see [issues
tagged with the `new rule` label][new-rule].
Add tests for all new/changed functionality. Test positive and negative
scenarios. 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][npm-scripts]. Tests should all pass on all platforms. The test runner is
[AVA][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. If `some-test.md` needs custom
configuration, a `some-test.json` is used to provide a custom `options.config`
for that scenario. Tests run by `markdownlint-test-scenarios.js` use [AVA's
snapshot feature][ava-snapshots]. To update snapshots (for example, after
modifying a test file), run `npm run update-snapshots` and include the updated
files with the pull request.
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 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. (See [Git
Tools - Rewriting History][rewriting-history] for details.)
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 (`.`). 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][closing-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][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, ChatGPT, LLMs (large language models), etc. that
incorporate code from other projects is not allowed.
Thank you!
[ava]: https://github.com/avajs/ava
[ava-snapshots]: https://github.com/avajs/ava/blob/main/docs/04-snapshot-testing.md
[closing-keywords]: https://help.github.com/articles/closing-issues-using-keywords/
[custom-rules]: doc/CustomRules.md
[dependencies]: https://docs.npmjs.com/cli/v11/configuring-npm/package-json#dependencies
[example-com]: https://en.wikipedia.org/wiki/Example.com
[micromark]: https://www.npmjs.com/package/micromark
[new-rule]: https://github.com/DavidAnson/markdownlint/labels/new%20rule
[npm-scripts]: https://docs.npmjs.com/misc/scripts
[rewriting-history]: https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History
[version-pinning]: https://dlaa.me/blog/post/versionpinning
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) David Anson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+1124
View File
File diff suppressed because it is too large Load Diff
+194
View File
@@ -0,0 +1,194 @@
# Custom Rules
In addition to its built-in rules, `markdownlint` lets you enhance the linting
experience by passing an array of custom rules using the [`options.customRules`
property][options-custom-rules]. Custom rules can do everything the built-in
rules can and are defined inline or imported from another package ([keyword
`markdownlint-rule` on npm][markdownlint-rule]). When defined by a file or
package, the export can be a single rule object (see below) or an array of them.
Custom rules can be disabled, enabled, and customized using the same syntax as
built-in rules.
## Implementing Simple 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 without needing to write code.
[markdownlint-rule-search-replace]: https://www.npmjs.com/package/markdownlint-rule-search-replace
## Authoring
Rules are defined by a name (or multiple names), a description, an optional link
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.
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-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": (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
});
}
}
}
```
A rule is implemented as an `Object`:
- `names` is a required `Array` of `String` values that identify the rule in
output messages and config.
- `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.
- `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
parameters:
- `params` is an `Object` with properties that describe the content being
analyzed:
- `name` is a `String` that identifies the input file/string.
- `parsers` is an `Object` with properties corresponding to the value of
`parser` in the rule definition (see above).
- `markdownit` is an `Object` that provides access to output from the
[`markdown-it`][markdown-it] parser.
- `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.)
- `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
front matter (not present in `lines`).
- `config` is an `Object` corresponding to the rule's entry in
`options.config` (if present).
- `version` is a `String` that corresponds to the version of `markdownlint`
- `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.
- `detail` is an optional `String` with information about what caused the
error.
- `context` is an optional `String` with relevant text surrounding the error
location.
- `information` is an optional (absolute) `URL` of a link to override the
same-named value provided by the rule definition. (Uncommon)
- `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 at least one of `deleteCount` and
`insertText` should be present; when applying a fix, the delete should be
performed before the insert):
- `lineNumber` is an optional `Number` specifying the 1-based line number
of the edit.
- `editColumn` is an optional `Number` specifying the 1-based column
number of the edit.
- `deleteCount` is an optional `Number` specifying the number of
characters to delete (the value `-1` is used to delete the line).
- `insertText` is an optional `String` specifying the text to insert. `\n`
is the platform-independent way to add a line break; line breaks should
be added at the beginning of a line instead of at the end.
The collection of helper functions shared by the built-in rules is available for
use by custom rules in the [markdownlint-rule-helpers package][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., `import { lint } from "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]
- [Complete example rule including npm configuration][extended-ascii]
- [Custom rules from the github/docs repository][github-docs]
- [Custom rules from the electron/lint-roller repository][electron]
- [Custom rules from the webhintio/hint repository][hint]
## References
- [CommonMark documentation and specification][commonmark]
- [`markdown-it` Markdown parser project page][markdown-it]
[commonmark]: https://commonmark.org/
[electron]: https://github.com/electron/lint-roller/tree/main/markdownlint-rules
[extended-ascii]: https://github.com/DavidAnson/markdownlint-rule-extended-ascii
[github-docs]: https://github.com/github/docs/tree/main/src/content-linter/lib/linting-rules
[hint]: https://github.com/webhintio/hint/blob/main/scripts/lint-markdown.js
[lib]: ../lib
[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.mts
[rule-helpers]: https://www.npmjs.com/package/markdownlint-rule-helpers
[options-custom-rules]: ../README.md#optionscustomrules
[test-rules]: ../test/rules
[tokens]: ../test/snapshots/markdownlint-test-custom-rules.mjs.md
+27
View File
@@ -0,0 +1,27 @@
# Using `markdownlint` with Prettier
[`Prettier`](https://prettier.io) is a popular code formatter.
For the most part, Prettier works seamlessly with `markdownlint`.
You can `extend` the [`prettier.json`](../style/prettier.json) style to disable
all `markdownlint` rules that overlap with Prettier.
Other scenarios are documented below.
## List item indentation
The default settings of `markdownlint` and `Prettier` are compatible and don't
result in any linting violations. If `Prettier` is used with `--tab-width` set
to `4` (vs. `2`), the following `markdownlint` configuration can be used:
```json
{
"list-marker-space": {
"ul_multi": 3,
"ul_single": 3
},
"ul-indent": {
"indent": 4
}
}
```
+20
View File
@@ -0,0 +1,20 @@
# Release Process
The `markdownlint` library has some related dependencies that are updated along
with it. To prevent possible regressions from having a widespread impact, these
releases are separated by a few days to provide an opportunity to find issues.
1. [`markdownlint`][markdownlint]
2. [`markdownlint-cli2`][markdownlint-cli2]
3. [`markdownlint-cli2-action`][markdownlint-cli2-action]
4. [`vscode-markdownlint`][vscode-markdownlint]
5. [`markdownlint-cli`][markdownlint-cli]
This sequence is not strict and may be adjusted based on the content of the
release and the scope of feature work in each dependency.
[markdownlint]: https://github.com/DavidAnson/markdownlint
[markdownlint-cli2]: https://github.com/DavidAnson/markdownlint-cli2
[markdownlint-cli2-action]: https://github.com/marketplace/actions/markdownlint-cli2-action
[vscode-markdownlint]: https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint
[markdownlint-cli]: https://github.com/igorshubovych/markdownlint-cli
+2814
View File
File diff suppressed because it is too large Load Diff
+51
View File
@@ -0,0 +1,51 @@
# `MD001` - Heading levels should only increment by one level at a time
Tags: `headings`
Aliases: `heading-increment`
Parameters:
- `front_matter_title`: RegExp for matching title in front matter (`string`,
default `^\s*title\s*[:=]`)
This rule is triggered when you skip heading levels in a Markdown document, for
example:
```markdown
# Heading 1
### Heading 3
We skipped out a 2nd level heading in this document
```
When using multiple heading levels, nested headings should increase by only one
level at a time:
```markdown
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
## Another Heading 2
### Another Heading 3
```
If [YAML](https://en.wikipedia.org/wiki/YAML) front matter is present and
contains a `title` property (commonly used with blog posts), this rule treats
that as a top level heading and will report a violation if the actual first
heading is not a level 2 heading. To use a different property name in the
front matter, specify the text of a regular expression via the
`front_matter_title` parameter. To disable the use of front matter by this
rule, specify `""` for `front_matter_title`. When front matter is not present,
the first heading can be any level.
Rationale: Headings represent the structure of a document and can be confusing
when skipped - especially for accessibility scenarios. More information:
<https://www.w3.org/WAI/tutorials/page-structure/headings/>.
+59
View File
@@ -0,0 +1,59 @@
# `MD003` - Heading style
Tags: `headings`
Aliases: `heading-style`
Parameters:
- `style`: Heading style (`string`, default `consistent`, values `atx` /
`atx_closed` / `consistent` / `setext` / `setext_with_atx` /
`setext_with_atx_closed`)
This rule is triggered when different heading styles are used in the same
document:
```markdown
# ATX style H1
## Closed ATX style H2 ##
Setext style H1
===============
```
To fix the issue, use consistent heading styles throughout the document:
```markdown
# ATX style H1
## ATX style H2
```
The `setext_with_atx` and `setext_with_atx_closed` settings allow ATX-style
headings of level 3 or more in documents with setext-style headings (which only
support level 1 and 2 headings):
```markdown
Setext style H1
===============
Setext style H2
---------------
### ATX style H3
```
Note: The configured heading style can be a specific style to require (`atx`,
`atx_closed`, `setext`, `setext_with_atx`, `setext_with_atx_closed`), or can
require that all heading styles match the first heading style via `consistent`.
Note: The placement of a horizontal rule directly below a line of text can
trigger this rule by turning that text into a level 2 setext-style heading:
```markdown
A line of text followed by a horizontal rule becomes a heading
---
```
Rationale: Consistent formatting makes it easier to understand a document.
+50
View File
@@ -0,0 +1,50 @@
# `MD004` - Unordered list style
Tags: `bullet`, `ul`
Aliases: `ul-style`
Parameters:
- `style`: List style (`string`, default `consistent`, values `asterisk` /
`consistent` / `dash` / `plus` / `sublist`)
Fixable: Some violations can be fixed by tooling
This rule is triggered when the symbols used in the document for unordered
list items do not match the configured unordered list style:
```markdown
* Item 1
+ Item 2
- Item 3
```
To fix this issue, use the configured style for list items throughout the
document:
```markdown
* Item 1
* Item 2
* Item 3
```
The configured list style can ensure all list styling is a specific symbol
(`asterisk`, `plus`, `dash`), ensure each sublist has a consistent symbol that
differs from its parent list (`sublist`), or ensure all list styles match the
first list style (`consistent`).
For example, the following is valid for the `sublist` style because the
outer-most indent uses asterisk, the middle indent uses plus, and the inner-most
indent uses dash:
```markdown
* Item 1
+ Item 2
- Item 3
+ Item 4
* Item 4
+ Item 5
```
Rationale: Consistent formatting makes it easier to understand a document.
+53
View File
@@ -0,0 +1,53 @@
# `MD005` - Inconsistent indentation for list items at the same level
Tags: `bullet`, `indentation`, `ul`
Aliases: `list-indent`
Fixable: Some violations can be fixed by tooling
This rule is triggered when list items are parsed as being at the same level,
but don't have the same indentation:
```markdown
* Item 1
* Nested Item 1
* Nested Item 2
* A misaligned item
```
Usually, this rule will be triggered because of a typo. Correct the indentation
for the list to fix it:
```markdown
* Item 1
* Nested Item 1
* Nested Item 2
* Nested Item 3
```
Sequentially-ordered list markers are usually left-aligned such that all items
have the same starting column:
```markdown
...
8. Item
9. Item
10. Item
11. Item
...
```
This rule also supports right-alignment of list markers such that all items have
the same ending column:
```markdown
...
8. Item
9. Item
10. Item
11. Item
...
```
Rationale: Violations of this rule can lead to improperly rendered content.
+52
View File
@@ -0,0 +1,52 @@
# `MD007` - Unordered list indentation
Tags: `bullet`, `indentation`, `ul`
Aliases: `ul-indent`
Parameters:
- `indent`: Spaces for indent (`integer`, default `2`)
- `start_indent`: Spaces for first level indent (when start_indented is set)
(`integer`, default `2`)
- `start_indented`: Whether to indent the first level of the list (`boolean`,
default `false`)
Fixable: Some violations can be fixed by tooling
This rule is triggered when list items are not indented by the configured
number of spaces (default: 2).
Example:
```markdown
* List item
* Nested list item indented by 3 spaces
```
Corrected Example:
```markdown
* List item
* Nested list item indented by 2 spaces
```
Note: This rule applies to a sublist only if its parent lists are all also
unordered (otherwise, extra indentation of ordered lists interferes with the
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 `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
used after the list marker. Indenting by 4 spaces is consistent with code blocks
and simpler for editors to implement. Additionally, this can be a compatibility
issue for other Markdown parsers, which require 4-space indents. More
information: [Markdown Style Guide][markdown-style-guide].
Note: See [Prettier.md](Prettier.md) for compatibility information.
[markdown-style-guide]: https://cirosantilli.com/markdown-style-guide#indentation-of-content-inside-lists
+50
View File
@@ -0,0 +1,50 @@
# `MD009` - Trailing spaces
Tags: `whitespace`
Aliases: `no-trailing-spaces`
Parameters:
- `br_spaces`: Spaces for line break (`integer`, default `2`)
- `code_blocks`: Include code blocks (`boolean`, default `false`)
- `list_item_empty_lines`: Allow spaces for empty lines in list items
(`boolean`, default `false`)
- `strict`: Include unnecessary breaks (`boolean`, default `false`)
Fixable: Some violations can be fixed by tooling
This rule is triggered on any lines that end with unexpected whitespace. To fix
this, remove the trailing space from the end of the line.
The `br_spaces` parameter allows an exception to this rule for a specific number
of trailing spaces, typically used to insert an explicit line break. The default
value allows 2 spaces to indicate a hard break (\<br> element). (You must set
`br_spaces` to a value >= 2 for this parameter to take effect. Setting
`br_spaces` to 1 behaves the same as 0, disallowing any trailing spaces.)
By default, trailing space is allowed in indented and fenced code blocks because
some programming languages require that. To report such instances, set the
`code_blocks` parameter to `true`.
By default, this rule will not trigger when the allowed number of spaces is
used, even when it doesn't create a hard break (for example, at the end of a
paragraph). To report such instances, set the `strict` parameter to `true`.
```markdown
Text text text
text[2 spaces]
```
Using spaces to indent blank lines inside a list item is usually not necessary,
but some parsers require it. Set the `list_item_empty_lines` parameter to `true`
to allow this (even when `strict` is `true`):
```markdown
- list item text
[2 spaces]
list item text
```
Rationale: Except when being used to create a line break, trailing whitespace
has no purpose and does not affect the rendering of content.
+62
View File
@@ -0,0 +1,62 @@
# `MD010` - Hard tabs
Tags: `hard_tab`, `whitespace`
Aliases: `no-hard-tabs`
Parameters:
- `code_blocks`: Include code blocks (`boolean`, default `true`)
- `ignore_code_languages`: Fenced code languages to ignore (`string[]`, default
`[]`)
- `spaces_per_tab`: Number of spaces for each hard tab (`integer`, default `1`)
Fixable: Some violations can be fixed by tooling
This rule is triggered by any lines that contain hard tab characters instead
of using spaces for indentation. To fix this, replace any hard tab characters
with spaces instead.
Example:
<!-- markdownlint-disable no-hard-tabs -->
```markdown
Some text
* hard tab character used to indent the list item
```
<!-- markdownlint-restore -->
Corrected example:
```markdown
Some text
* Spaces used to indent the list item instead
```
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).
When code blocks are scanned (e.g., by default or if `code_blocks` is `true`),
the `ignore_code_languages` parameter can be set to a list of languages that
should be ignored (i.e., hard tabs will be allowed, though not required). This
makes it easier for documents to include code for languages that require hard
tabs.
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.
More information:
- <https://agiletribe.wordpress.com/2011/10/27/18-dont-use-tab-characters/>
- <https://www.jwz.org/doc/tabs-vs-spaces.html>
- <https://adamspiers.org/computing/why_no_tabs.html>
+30
View File
@@ -0,0 +1,30 @@
# `MD011` - Reversed link syntax
Tags: `links`
Aliases: `no-reversed-links`
Fixable: Some violations can be fixed by tooling
This rule is triggered when text that appears to be a link is encountered, but
where the syntax appears to have been reversed (the `[]` and `()` are
reversed):
```markdown
(Incorrect link syntax)[https://www.example.com/]
```
To fix this, swap the `[]` and `()` around:
```markdown
[Correct link syntax](https://www.example.com/)
```
Note: [Markdown Extra](https://en.wikipedia.org/wiki/Markdown_Extra)-style
footnotes do not trigger this rule:
```markdown
For (example)[^1]
```
Rationale: Reversed links are not rendered as usable links.
+38
View File
@@ -0,0 +1,38 @@
# `MD012` - Multiple consecutive blank lines
Tags: `blank_lines`, `whitespace`
Aliases: `no-multiple-blanks`
Parameters:
- `maximum`: Consecutive blank lines (`integer`, default `1`)
Fixable: Some violations can be fixed by tooling
This rule is triggered when there are multiple consecutive blank lines in the
document:
```markdown
Some text here
Some more text here
```
To fix this, delete the offending lines:
```markdown
Some text here
Some more text here
```
Note: this rule will not be triggered if there are multiple consecutive blank
lines inside code blocks.
Note: The `maximum` parameter can be used to configure the maximum number of
consecutive blank lines.
Rationale: Except in a code block, blank lines serve no purpose and do not
affect the rendering of content.
+58
View File
@@ -0,0 +1,58 @@
# `MD013` - Line length
Tags: `line_length`
Aliases: `line-length`
Parameters:
- `code_block_line_length`: Number of characters for code blocks (`integer`,
default `80`)
- `code_blocks`: Include code blocks (`boolean`, default `true`)
- `heading_line_length`: Number of characters for headings (`integer`, default
`80`)
- `headings`: Include headings (`boolean`, default `true`)
- `line_length`: Number of characters (`integer`, default `80`)
- `stern`: Stern length checking (`boolean`, default `false`)
- `strict`: Strict length checking (`boolean`, default `false`)
- `tables`: Include tables (`boolean`, default `true`)
This rule is triggered when there are lines that are longer than the
configured `line_length` (default: 80 characters). To fix this, split the line
up into multiple lines. To set a different maximum length for headings, use
`heading_line_length`. To set a different maximum length for code blocks, use
`code_block_line_length`
This rule has an exception when there is no whitespace beyond the configured
line length. This allows you to include items such as long URLs without being
forced to break them in the middle. To disable this exception, set the `strict`
parameter to `true` and an issue will be reported when any line is too long. To
warn for lines that are too long and could be fixed but allow long lines
without spaces, set the `stern` parameter to `true`.
For example (assuming normal behavior):
```markdown
IF THIS LINE IS THE MAXIMUM LENGTH
This line is okay because there are-no-spaces-beyond-that-length
This line is a violation because there are spaces beyond that length
This-line-is-okay-because-there-are-no-spaces-anywhere-within
```
In `strict` mode, the last three lines above are all violations. In `stern`
mode, the middle two lines above are both violations, but the last is okay.
You have the option to exclude this rule for code blocks, tables, or headings.
To do so, set the `code_blocks`, `tables`, or `headings` parameter(s) to false.
Code blocks are included in this rule by default since it is often a
requirement for document readability, and tentatively compatible with code
rules. Still, some languages do not lend themselves to short lines.
Lines with link/image reference definitions and standalone lines (i.e., not part
of a paragraph) with only a link/image (possibly using (strong) emphasis) are
always exempted from this rule (even in `strict` mode) because there is often no
way to split such lines without breaking the URL.
Rationale: Extremely long lines can be difficult to work with in some editors.
More information: <https://cirosantilli.com/markdown-style-guide#line-wrapping>.
+54
View File
@@ -0,0 +1,54 @@
# `MD014` - Dollar signs used before commands without showing output
Tags: `code`
Aliases: `commands-show-output`
Fixable: Some violations can be fixed by tooling
This rule is triggered when there are code blocks showing shell commands to be
typed, and *all* of the shell commands are preceded by dollar signs ($):
<!-- markdownlint-disable commands-show-output -->
```markdown
$ ls
$ cat foo
$ less bar
```
<!-- markdownlint-restore -->
The dollar signs are unnecessary in this situation, and should not be
included:
```markdown
ls
cat foo
less bar
```
Showing output for commands preceded by dollar signs does not trigger this rule:
```markdown
$ ls
foo bar
$ cat foo
Hello world
$ cat bar
baz
```
Because some commands do not produce output, it is not a violation if *some*
commands do not have output:
```markdown
$ mkdir test
mkdir: created directory 'test'
$ ls test
```
Rationale: It is easier to copy/paste and less noisy if the dollar signs
are omitted when they are not needed. See
<https://cirosantilli.com/markdown-style-guide#dollar-signs-in-shell-code>
for more information.
+27
View File
@@ -0,0 +1,27 @@
# `MD018` - No space after hash on atx style heading
Tags: `atx`, `headings`, `spaces`
Aliases: `no-missing-space-atx`
Fixable: Some violations can be fixed by tooling
This rule is triggered when spaces are missing after the hash characters
in an atx style heading:
```markdown
#Heading 1
##Heading 2
```
To fix this, separate the heading text from the hash character by a single
space:
```markdown
# Heading 1
## Heading 2
```
Rationale: Violations of this rule can lead to improperly rendered content.
+28
View File
@@ -0,0 +1,28 @@
# `MD019` - Multiple spaces after hash on atx style heading
Tags: `atx`, `headings`, `spaces`
Aliases: `no-multiple-space-atx`
Fixable: Some violations can be fixed by tooling
This rule is triggered when more than one space is used to separate the
heading text from the hash characters in an atx style heading:
```markdown
# Heading 1
## Heading 2
```
To fix this, separate the heading text from the hash character by a single
space:
```markdown
# Heading 1
## Heading 2
```
Rationale: Extra space has no purpose and does not affect the rendering of
content.
+29
View File
@@ -0,0 +1,29 @@
# `MD020` - No space inside hashes on closed atx style heading
Tags: `atx_closed`, `headings`, `spaces`
Aliases: `no-missing-space-closed-atx`
Fixable: Some violations can be fixed by tooling
This rule is triggered when spaces are missing inside the hash characters
in a closed atx style heading:
```markdown
#Heading 1#
##Heading 2##
```
To fix this, separate the heading text from the hash character by a single
space:
```markdown
# Heading 1 #
## Heading 2 ##
```
Note: this rule will fire if either side of the heading is missing spaces.
Rationale: Violations of this rule can lead to improperly rendered content.
+31
View File
@@ -0,0 +1,31 @@
# `MD021` - Multiple spaces inside hashes on closed atx style heading
Tags: `atx_closed`, `headings`, `spaces`
Aliases: `no-multiple-space-closed-atx`
Fixable: Some violations can be fixed by tooling
This rule is triggered when more than one space is used to separate the
heading text from the hash characters in a closed atx style heading:
```markdown
# Heading 1 #
## Heading 2 ##
```
To fix this, separate the heading text from the hash character by a single
space:
```markdown
# Heading 1 #
## Heading 2 ##
```
Note: this rule will fire if either side of the heading contains multiple
spaces.
Rationale: Extra space has no purpose and does not affect the rendering of
content.
+52
View File
@@ -0,0 +1,52 @@
# `MD022` - Headings should be surrounded by blank lines
Tags: `blank_lines`, `headings`
Aliases: `blanks-around-headings`
Parameters:
- `lines_above`: Blank lines above heading (`integer|integer[]`, default `1`)
- `lines_below`: Blank lines below heading (`integer|integer[]`, default `1`)
Fixable: Some violations can be fixed by tooling
This rule is triggered when headings (any style) are either not preceded or not
followed by at least one blank line:
```markdown
# Heading 1
Some text
Some more text
## Heading 2
```
To fix this, ensure that all headings have a blank line both before and after
(except where the heading is at the beginning or end of the document):
```markdown
# Heading 1
Some text
Some more text
## Heading 2
```
The `lines_above` and `lines_below` parameters can be used to specify a
different number of blank lines (including `0`) above or below each heading.
If the value `-1` is used for either parameter, any number of blank lines is
allowed. To customize the number of lines above or below each heading level
individually, specify a `number[]` where values correspond to heading levels
1-6 (in order).
Notes: If `lines_above` or `lines_below` are configured to require more than one
blank line, [MD012/no-multiple-blanks](md012.md) should also be customized. This
rule checks for *at least* as many blank lines as specified; any extra blank
lines are ignored.
Rationale: Aside from aesthetic reasons, some parsers, including `kramdown`,
will not parse headings that don't have a blank line before, and will parse them
as regular text.
+33
View File
@@ -0,0 +1,33 @@
# `MD023` - Headings must start at the beginning of the line
Tags: `headings`, `spaces`
Aliases: `heading-start-left`
Fixable: Some violations can be fixed by tooling
This rule is triggered when a heading is indented by one or more spaces:
```markdown
Some text
# Indented heading
```
To fix this, ensure that all headings start at the beginning of the line:
```markdown
Some text
# Heading
```
Note that scenarios like block quotes "indent" the start of the line, so the
following is also correct:
```markdown
> # Heading in Block Quote
```
Rationale: Headings that don't start at the beginning of the line will not be
parsed as headings, and will instead appear as regular text.
+44
View File
@@ -0,0 +1,44 @@
# `MD024` - Multiple headings with the same content
Tags: `headings`
Aliases: `no-duplicate-heading`
Parameters:
- `siblings_only`: Only check sibling headings (`boolean`, default `false`)
This rule is triggered if there are multiple headings in the document that have
the same text:
```markdown
# Some text
## Some text
```
To fix this, ensure that the content of each heading is different:
```markdown
# Some text
## Some more text
```
If the parameter `siblings_only` is set to `true`, duplication is allowed for
headings with different parents (as is common in changelogs):
```markdown
# Change log
## 1.0.0
### Features
## 2.0.0
### Features
```
Rationale: Some Markdown parsers generate anchors for headings based on the
heading name; headings with the same content can cause problems with that.
+49
View File
@@ -0,0 +1,49 @@
# `MD025` - Multiple top-level headings in the same document
Tags: `headings`
Aliases: `single-h1`, `single-title`
Parameters:
- `front_matter_title`: RegExp for matching title in front matter (`string`,
default `^\s*title\s*[:=]`)
- `level`: Heading level (`integer`, default `1`)
This rule is triggered when a top-level heading is in use (the first line of
the file is an h1 heading), and more than one h1 heading is in use in the
document:
```markdown
# Top level heading
# Another top-level heading
```
To fix, structure your document so there is a single h1 heading that is
the title for the document. Subsequent headings must be
lower-level headings (h2, h3, etc.):
```markdown
# Title
## Heading
## Another heading
```
Note: The `level` parameter can be used to change the top-level (ex: to h2) in
cases where an h1 is added externally.
If [YAML](https://en.wikipedia.org/wiki/YAML) front matter is present and
contains a `title` property (commonly used with blog posts), this rule treats
that as a top level heading and will report a violation for any subsequent
top-level headings. To use a different property name in the front matter,
specify the text of a regular expression via the `front_matter_title` parameter.
To disable the use of front matter by this rule, specify `""` for
`front_matter_title`.
Rationale: A top-level heading is an h1 on the first line of the file, and
serves as the title for the document. If this convention is in use, then there
can not be more than one title for the document, and the entire document should
be contained within this heading.
+40
View File
@@ -0,0 +1,40 @@
# `MD026` - Trailing punctuation in heading
Tags: `headings`
Aliases: `no-trailing-punctuation`
Parameters:
- `punctuation`: Punctuation characters (`string`, default `.,;:!。,;:!`)
Fixable: Some violations can be fixed by tooling
This rule is triggered on any heading that has one of the specified normal or
full-width punctuation characters as the last character in the line:
```markdown
# This is a heading.
```
To fix this, remove the trailing punctuation:
```markdown
# This is a heading
```
Note: The `punctuation` parameter can be used to specify what characters count
as punctuation at the end of a heading. For example, you can change it to
`".,;:"` to allow headings that end with an exclamation point. `?` is
allowed by default because of how common it is in headings of FAQ-style
documents. Setting the `punctuation` parameter to `""` allows all characters -
and is equivalent to disabling the rule.
Note: The trailing semicolon of [HTML entity references][html-entity-references]
like `&copy;`, `&#169;`, and `&#x000A9;` is ignored by this rule.
Rationale: Headings are not meant to be full sentences. More information:
[Punctuation at the end of headers][end-punctuation].
[end-punctuation]: https://cirosantilli.com/markdown-style-guide#punctuation-at-the-end-of-headers
[html-entity-references]: https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references
+32
View File
@@ -0,0 +1,32 @@
# `MD027` - Multiple spaces after blockquote symbol
Tags: `blockquote`, `indentation`, `whitespace`
Aliases: `no-multiple-space-blockquote`
Parameters:
- `list_items`: Include list items (`boolean`, default `true`)
Fixable: Some violations can be fixed by tooling
This rule is triggered when blockquotes have more than one space after the
blockquote (`>`) symbol:
```markdown
> This is a blockquote with bad indentation
> there should only be one.
```
To fix, remove any extraneous space:
```markdown
> This is a blockquote with correct
> indentation.
```
Inferring intended list indentation within a blockquote can be challenging;
setting the `list_items` parameter to `false` disables this rule for ordered
and unordered list items.
Rationale: Consistent formatting makes it easier to understand a document.
+40
View File
@@ -0,0 +1,40 @@
# `MD028` - Blank line inside blockquote
Tags: `blockquote`, `whitespace`
Aliases: `no-blanks-blockquote`
This rule is triggered when two blockquote blocks are separated by nothing
except for a blank line:
```markdown
> This is a blockquote
> which is immediately followed by
> this blockquote. Unfortunately
> In some parsers, these are treated as the same blockquote.
```
To fix this, ensure that any blockquotes that are right next to each other
have some text in between:
```markdown
> This is a blockquote.
And Jimmy also said:
> This too is a blockquote.
```
Alternatively, if they are supposed to be the same quote, then add the
blockquote symbol at the beginning of the blank line:
```markdown
> This is a blockquote.
>
> This is the same blockquote.
```
Rationale: Some Markdown parsers will treat two blockquotes separated by one
or more blank lines as the same blockquote, while others will treat them as
separate blockquotes.
+100
View File
@@ -0,0 +1,100 @@
# `MD029` - Ordered list item prefix
Tags: `ol`
Aliases: `ol-prefix`
Parameters:
- `style`: List style (`string`, default `one_or_ordered`, values `one` /
`one_or_ordered` / `ordered` / `zero`)
Fixable: Some violations can be fixed by tooling
This rule is triggered for ordered lists that do not either start with '1.' or
do not have a prefix that increases in numerical order (depending on the
configured style). The less-common pattern of using '0.' as a first prefix or
for all prefixes is also supported.
Example valid list if the style is configured as 'one':
```markdown
1. Do this.
1. Do that.
1. Done.
```
Examples of valid lists if the style is configured as 'ordered':
```markdown
1. Do this.
2. Do that.
3. Done.
```
```markdown
0. Do this.
1. Do that.
2. Done.
```
All three examples are valid when the style is configured as 'one_or_ordered'.
Example valid list if the style is configured as 'zero':
```markdown
0. Do this.
0. Do that.
0. Done.
```
Example invalid list for all styles:
```markdown
1. Do this.
3. Done.
```
This rule supports 0-prefixing ordered list items for uniform indentation:
```markdown
...
08. Item
09. Item
10. Item
11. Item
...
```
Note: This rule will report violations for cases like the following where an
improperly-indented code block (or similar) appears between two list items and
"breaks" the list in two:
<!-- markdownlint-disable code-fence-style -->
~~~markdown
1. First list
```text
Code block
```
1. Second list
~~~
The fix is to indent the code block so it becomes part of the preceding list
item as intended:
~~~markdown
1. First list
```text
Code block
```
2. Still first list
~~~
<!-- markdownlint-restore -->
Rationale: Consistent formatting makes it easier to understand a document.
+82
View File
@@ -0,0 +1,82 @@
# `MD030` - Spaces after list markers
Tags: `ol`, `ul`, `whitespace`
Aliases: `list-marker-space`
Parameters:
- `ol_multi`: Spaces for multi-line ordered list items (`integer`, default `1`)
- `ol_single`: Spaces for single-line ordered list items (`integer`, default
`1`)
- `ul_multi`: Spaces for multi-line unordered list items (`integer`, default
`1`)
- `ul_single`: Spaces for single-line unordered list items (`integer`, default
`1`)
Fixable: Some violations can be fixed by tooling
This rule checks for the number of spaces between a list marker (e.g. '`-`',
'`*`', '`+`' or '`1.`') and the text of the list item.
The number of spaces checked for depends on the document style in use, but the
default is 1 space after any list marker:
```markdown
* Foo
* Bar
* Baz
1. Foo
1. Bar
1. Baz
1. Foo
* Bar
1. Baz
```
A document style may change the number of spaces after unordered list items
and ordered list items independently, as well as based on whether the content
of every item in the list consists of a single paragraph or multiple
paragraphs (including sub-lists and code blocks).
For example, the style guide at
<https://cirosantilli.com/markdown-style-guide#spaces-after-list-marker>
specifies that 1 space after the list marker should be used if every item in
the list fits within a single paragraph, but to use 2 or 3 spaces (for ordered
and unordered lists respectively) if there are multiple paragraphs of content
inside the list:
```markdown
* Foo
* Bar
* Baz
```
vs.
```markdown
* Foo
Second paragraph
* Bar
```
or
```markdown
1. Foo
Second paragraph
1. Bar
```
To fix this, ensure the correct number of spaces are used after the list marker
for your selected document style.
Rationale: Violations of this rule can lead to improperly rendered content.
Note: See [Prettier.md](Prettier.md) for compatibility information.
+50
View File
@@ -0,0 +1,50 @@
# `MD031` - Fenced code blocks should be surrounded by blank lines
Tags: `blank_lines`, `code`
Aliases: `blanks-around-fences`
Parameters:
- `list_items`: Include list items (`boolean`, default `true`)
Fixable: Some violations can be fixed by tooling
This rule is triggered when fenced code blocks are either not preceded or not
followed by a blank line:
````markdown
Some text
```
Code block
```
```
Another code block
```
Some more text
````
To fix this, ensure that all fenced code blocks have a blank line both before
and after (except where the block is at the beginning or end of the document):
````markdown
Some text
```
Code block
```
```
Another code block
```
Some more text
````
Set the `list_items` parameter to `false` to disable this rule for list items.
Disabling this behavior for lists can be useful if it is necessary to create a
[tight](https://spec.commonmark.org/0.29/#tight) list containing a code fence.
Rationale: Aside from aesthetic reasons, some parsers, including kramdown, will
not parse fenced code blocks that don't have blank lines before and after them.
+55
View File
@@ -0,0 +1,55 @@
# `MD032` - Lists should be surrounded by blank lines
Tags: `blank_lines`, `bullet`, `ol`, `ul`
Aliases: `blanks-around-lists`
Fixable: Some violations can be fixed by tooling
This rule is triggered when lists (of any kind) are either not preceded or not
followed by a blank line:
```markdown
Some text
* List item
* List item
1. List item
2. List item
***
```
In the first case above, text immediately precedes the unordered list. In the
second case above, a thematic break immediately follows the ordered list. To fix
violations of this rule, ensure that all lists have a blank line both before and
after (except when the list is at the very beginning or end of the document):
```markdown
Some text
* List item
* List item
1. List item
2. List item
***
```
Note that the following case is **not** a violation of this rule:
```markdown
1. List item
More item 1
2. List item
More item 2
```
Although it is not indented, the text "More item 2" is referred to as a
[lazy continuation line][lazy-continuation] and considered part of the second
list item.
Rationale: In addition to aesthetic reasons, some parsers, including kramdown,
will not parse lists that don't have blank lines before and after them.
[lazy-continuation]: https://spec.commonmark.org/0.30/#lazy-continuation-line
+33
View File
@@ -0,0 +1,33 @@
# `MD033` - Inline HTML
Tags: `html`
Aliases: `no-inline-html`
Parameters:
- `allowed_elements`: Allowed elements (`string[]`, default `[]`)
- `table_allowed_elements`: Allowed elements in tables (`string[]`, default
`[]`)
This rule is triggered whenever raw HTML is used in a Markdown document:
```markdown
<h1>Inline HTML heading</h1>
```
To fix this, use 'pure' Markdown instead of including raw HTML:
```markdown
# Markdown heading
```
To allow specific HTML elements anywhere in Markdown content, set the
`allowed_elements` parameter to a list of HTML element names. To allow a
specific set of HTML elements within Markdown tables, set the
`table_allowed_elements` parameter to a list of HTML element names. This can be
used to permit the use of `<br>`-style line breaks only within Markdown tables.
Rationale: Raw HTML is allowed in Markdown, but this rule is included for
those who want their documents to only include "pure" Markdown, or for those
who are rendering Markdown documents into something other than HTML.
+55
View File
@@ -0,0 +1,55 @@
# `MD034` - Bare URL used
Tags: `links`, `url`
Aliases: `no-bare-urls`
Fixable: Some violations can be fixed by tooling
This rule is triggered whenever a URL or email address appears without
surrounding angle brackets:
```markdown
For more info, visit https://www.example.com/ or email user@example.com.
```
To fix this, add angle brackets around the URL or email address:
```markdown
For more info, visit <https://www.example.com/> or email <user@example.com>.
```
If a URL or email address contains non-ASCII characters, it may be not be
handled as intended even when angle brackets are present. In such cases,
[percent-encoding](https://en.m.wikipedia.org/wiki/Percent-encoding) can be used
to comply with the required syntax for URL and email.
Note: To include a bare URL or email without it being converted into a link,
wrap it in a code span:
```markdown
Not a clickable link: `https://www.example.com`
```
Note: The following scenario does not trigger this rule because it could be a
shortcut link:
```markdown
[https://www.example.com]
```
Note: The following syntax triggers this rule because the nested link could be
a shortcut link (which takes precedence):
```markdown
[text [shortcut] text](https://example.com)
```
To avoid this, escape both inner brackets:
```markdown
[link \[text\] link](https://example.com)
```
Rationale: Without angle brackets, a bare URL or email isn't converted into a
link by some Markdown parsers.
+37
View File
@@ -0,0 +1,37 @@
# `MD035` - Horizontal rule style
Tags: `hr`
Aliases: `hr-style`
Parameters:
- `style`: Horizontal rule style (`string`, default `consistent`)
This rule is triggered when inconsistent styles of horizontal rules are used
in the document:
```markdown
---
- - -
***
* * *
****
```
To fix this, use the same horizontal rule everywhere:
```markdown
---
---
```
The configured style can ensure all horizontal rules use a specific string or it
can ensure all horizontal rules match the first horizontal rule (`consistent`).
Rationale: Consistent formatting makes it easier to understand a document.
+45
View File
@@ -0,0 +1,45 @@
# `MD036` - Emphasis used instead of a heading
Tags: `emphasis`, `headings`
Aliases: `no-emphasis-as-heading`
Parameters:
- `punctuation`: Punctuation characters (`string`, default `.,;:!?。,;:!?`)
This check looks for instances where emphasized (i.e. bold or italic) text is
used to separate sections, where a heading should be used instead:
```markdown
**My document**
Lorem ipsum dolor sit amet...
_Another section_
Consectetur adipiscing elit, sed do eiusmod.
```
To fix this, use Markdown headings instead of emphasized text to denote
sections:
```markdown
# My document
Lorem ipsum dolor sit amet...
## Another section
Consectetur adipiscing elit, sed do eiusmod.
```
Note: This rule looks for single-line paragraphs that consist entirely
of emphasized text. It won't fire on emphasis used within regular text,
multi-line emphasized paragraphs, or paragraphs ending in punctuation
(normal or full-width). Similarly to rule MD026, you can configure what
characters are recognized as punctuation.
Rationale: Using emphasis instead of a heading prevents tools from inferring
the structure of a document. More information:
<https://cirosantilli.com/markdown-style-guide#emphasis-vs-headers>.
+37
View File
@@ -0,0 +1,37 @@
# `MD037` - Spaces inside emphasis markers
Tags: `emphasis`, `whitespace`
Aliases: `no-space-in-emphasis`
Fixable: Some violations can be fixed by tooling
This rule is triggered when emphasis markers (bold, italic) are used, but they
have spaces between the markers and the text:
```markdown
Here is some ** bold ** text.
Here is some * italic * text.
Here is some more __ bold __ text.
Here is some more _ italic _ text.
```
To fix this, remove the spaces around the emphasis markers:
```markdown
Here is some **bold** text.
Here is some *italic* text.
Here is some more __bold__ text.
Here is some more _italic_ text.
```
Rationale: Emphasis is only parsed as such when the asterisks/underscores
aren't surrounded by spaces. This rule attempts to detect where
they were surrounded by spaces, but it appears that emphasized text was
intended by the author.
+52
View File
@@ -0,0 +1,52 @@
# `MD038` - Spaces inside code span elements
Tags: `code`, `whitespace`
Aliases: `no-space-in-code`
Fixable: Some violations can be fixed by tooling
This rule is triggered for code spans containing content with unnecessary space
next to the beginning or ending backticks:
```markdown
`some text `
` some text`
` some text `
```
To fix this, remove the extra space characters from the beginning and ending:
```markdown
`some text`
```
Note: A single leading *and* trailing space is allowed by the specification and
trimmed by the parser to support code spans that begin or end with a backtick:
```markdown
`` `backticks` ``
`` backtick` ``
```
Note: When single-space padding is present in the input, it will be preserved
(even if unnecessary):
```markdown
` code `
```
Note: Code spans containing only spaces are allowed by the specification and are
also preserved:
```markdown
` `
` `
```
Rationale: Violations of this rule are usually unintentional and can lead to
improperly-rendered content.
+21
View File
@@ -0,0 +1,21 @@
# `MD039` - Spaces inside link text
Tags: `links`, `whitespace`
Aliases: `no-space-in-links`
Fixable: Some violations can be fixed by tooling
This rule is triggered on links that have spaces surrounding the link text:
```markdown
[ a link ](https://www.example.com/)
```
To fix this, remove the spaces surrounding the link text:
```markdown
[a link](https://www.example.com/)
```
Rationale: Consistent formatting makes it easier to understand a document.
+52
View File
@@ -0,0 +1,52 @@
# `MD040` - Fenced code blocks should have a language specified
Tags: `code`, `language`
Aliases: `fenced-code-language`
Parameters:
- `allowed_languages`: List of languages (`string[]`, default `[]`)
- `language_only`: Require language only (`boolean`, default `false`)
This rule is triggered when fenced code blocks are used, but a language isn't
specified:
````markdown
```
#!/bin/bash
echo Hello world
```
````
To fix this, add a language specifier to the code block:
````markdown
```bash
#!/bin/bash
echo Hello world
```
````
To display a code block without syntax highlighting, use:
````markdown
```text
Plain text in a code block
```
````
You can configure the `allowed_languages` parameter to specify a list of
languages code blocks could use. Languages are case sensitive. The default value
is `[]` which means any language specifier is valid.
You can prevent extra data from being present in the info string of fenced code
blocks. To do so, set the `language_only` parameter to `true`.
<!-- markdownlint-disable-next-line no-space-in-code -->
Info strings with leading/trailing whitespace (ex: `js `) or other content (ex:
`ruby startline=3`) will trigger this rule.
Rationale: Specifying a language improves content rendering by using the
correct syntax highlighting for code. More information:
<https://cirosantilli.com/markdown-style-guide#option-code-fenced>.
+64
View File
@@ -0,0 +1,64 @@
# `MD041` - First line in a file should be a top-level heading
Tags: `headings`
Aliases: `first-line-h1`, `first-line-heading`
Parameters:
- `allow_preamble`: Allow content before first heading (`boolean`, default
`false`)
- `front_matter_title`: RegExp for matching title in front matter (`string`,
default `^\s*title\s*[:=]`)
- `level`: Heading level (`integer`, default `1`)
This rule is intended to ensure documents have a title and is triggered when
the first line in a document is not a top-level ([HTML][HTML] `h1`) heading:
```markdown
This is a document without a heading
```
To fix this, add a top-level heading to the beginning of the document:
```markdown
# Document Heading
This is a document with a top-level heading
```
Because it is common for projects on GitHub to use an image for the heading of
`README.md` and that pattern is not well-supported by Markdown, HTML headings
are also permitted by this rule. For example:
```markdown
<h1 align="center"><img src="https://placekitten.com/300/150"/></h1>
This is a document with a top-level HTML heading
```
In some cases, a document's title heading may be preceded by text like a table
of contents. This is not ideal for accessibility, but can be allowed by setting
the `allow_preamble` parameter to `true`.
```markdown
This is a document with preamble text
# Document Heading
```
If [YAML][YAML] front matter is present and contains a `title` property
(commonly used with blog posts), this rule will not report a violation. To use a
different property name in the front matter, specify the text of a [regular
expression][RegExp] via the `front_matter_title` parameter. To disable the use
of front matter by this rule, specify `""` for `front_matter_title`.
The `level` parameter can be used to change the top-level heading (ex: to `h2`)
in cases where an `h1` is added externally.
Rationale: The top-level heading often acts as the title of a document. More
information: <https://cirosantilli.com/markdown-style-guide#top-level-header>.
[HTML]: https://en.wikipedia.org/wiki/HTML
[RegExp]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions
[YAML]: https://en.wikipedia.org/wiki/YAML
+32
View File
@@ -0,0 +1,32 @@
# `MD042` - No empty links
Tags: `links`
Aliases: `no-empty-links`
This rule is triggered when an empty link is encountered:
```markdown
[an empty link]()
```
To fix the violation, provide a destination for the link:
```markdown
[a valid link](https://example.com/)
```
Empty fragments will trigger this rule:
```markdown
[an empty fragment](#)
```
But non-empty fragments will not:
```markdown
[a valid fragment](#fragment)
```
Rationale: Empty links do not lead anywhere and therefore don't function as
links.
+87
View File
@@ -0,0 +1,87 @@
# `MD043` - Required heading structure
Tags: `headings`
Aliases: `required-headings`
Parameters:
- `headings`: List of headings (`string[]`, default `[]`)
- `match_case`: Match case of headings (`boolean`, default `false`)
This rule is triggered when the headings in a file do not match the array of
headings passed to the rule. It can be used to enforce a standard heading
structure for a set of files.
To require exactly the following structure:
```markdown
# Heading
## Item
### Detail
```
Set the `headings` parameter to:
```json
[
"# Heading",
"## Item",
"### Detail"
]
```
To allow optional headings as with the following structure:
```markdown
# Heading
## Item
### Detail (optional)
## Foot
### Notes (optional)
```
Use the special value `"*"` meaning "zero or more unspecified headings" or the
special value `"+"` meaning "one or more unspecified headings" and set the
`headings` parameter to:
```json
[
"# Heading",
"## Item",
"*",
"## Foot",
"*"
]
```
To allow a single required heading to vary as with a project name:
```markdown
# Project Name
## Description
## Examples
```
Use the special value `"?"` meaning "exactly one unspecified heading":
```json
[
"?",
"## Description",
"## Examples"
]
```
When an error is detected, this rule outputs the line number of the first
problematic heading (otherwise, it outputs the last line number of the file).
Note that while the `headings` parameter uses the "## Text" ATX heading style
for simplicity, a file may use any supported heading style.
By default, the case of headings in the document is not required to match that
of `headings`. To require that case match exactly, set the `match_case`
parameter to `true`.
Rationale: Projects may wish to enforce a consistent document structure across
a set of similar content.
+45
View File
@@ -0,0 +1,45 @@
# `MD044` - Proper names should have the correct capitalization
Tags: `spelling`
Aliases: `proper-names`
Parameters:
- `code_blocks`: Include code blocks (`boolean`, default `true`)
- `html_elements`: Include HTML elements (`boolean`, default `true`)
- `names`: List of proper names (`string[]`, default `[]`)
Fixable: Some violations can be fixed by tooling
This rule is triggered when any of the strings in the `names` array do not have
the specified capitalization. It can be used to enforce a standard letter case
for the names of projects and products.
For example, the language "JavaScript" is usually written with both the 'J' and
'S' capitalized - though sometimes the 's' or 'j' appear in lower-case. To
enforce the proper capitalization, specify the desired letter case in the
`names` array:
```json
[
"JavaScript"
]
```
Sometimes a proper name is capitalized differently in certain contexts. In such
cases, add both forms to the `names` array:
```json
[
"GitHub",
"github.com"
]
```
Set the `code_blocks` parameter to `false` to disable this rule for code blocks
and spans. Set the `html_elements` parameter to `false` to disable this rule
for HTML elements and attributes (such as when using a proper name as part of
a path for `a`/`href` or `img`/`src`).
Rationale: Incorrect capitalization of proper names is usually a mistake.
+48
View File
@@ -0,0 +1,48 @@
# `MD045` - Images should have alternate text (alt text)
Tags: `accessibility`, `images`
Aliases: `no-alt-text`
This rule reports a violation when an image is missing alternate text (alt text)
information.
Alternate text is commonly specified inline as:
```markdown
![Alternate text](image.jpg)
```
Or with reference syntax as:
```markdown
![Alternate text][ref]
...
[ref]: image.jpg "Optional title"
```
Or with HTML as:
```html
<img src="image.jpg" alt="Alternate text" />
```
Note: If the [HTML `aria-hidden` attribute][aria-hidden] is used to hide the
image from assistive technology, this rule does not report a violation:
```html
<img src="image.jpg" aria-hidden="true" />
```
Guidance for writing alternate text is available from the [W3C][w3c],
[Wikipedia][wikipedia], and [other locations][phase2technology].
Rationale: Alternate text is important for accessibility and describes the
content of an image for people who may not be able to see it.
[aria-hidden]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-hidden
[phase2technology]: https://www.phase2technology.com/blog/no-more-excuses
[w3c]: https://www.w3.org/WAI/alt/
[wikipedia]: https://en.wikipedia.org/wiki/Alt_attribute
+40
View File
@@ -0,0 +1,40 @@
# `MD046` - Code block style
Tags: `code`
Aliases: `code-block-style`
Parameters:
- `style`: Block style (`string`, default `consistent`, values `consistent` /
`fenced` / `indented`)
This rule is triggered when unwanted or different code block styles are used in
the same document.
In the default configuration this rule reports a violation for the following
document:
<!-- markdownlint-disable code-block-style -->
Some text.
# Indented code
More text.
```ruby
# Fenced code
```
More text.
<!-- markdownlint-restore -->
To fix violations of this rule, use a consistent style (either indenting or code
fences).
The configured code block style can be specific (`fenced`, `indented`) or can
require all code blocks match the first code block (`consistent`).
Rationale: Consistent formatting makes it easier to understand a document.
+34
View File
@@ -0,0 +1,34 @@
# `MD047` - Files should end with a single newline character
Tags: `blank_lines`
Aliases: `single-trailing-newline`
Fixable: Some violations can be fixed by tooling
This rule is triggered when there is not a single newline character at the end
of a file.
An example that triggers the rule:
```markdown
# Heading
This file ends without a newline.[EOF]
```
To fix the violation, add a newline character to the end of the file:
```markdown
# Heading
This file ends with a newline.
[EOF]
```
Rationale: Some programs have trouble with files that do not end with a newline.
More information: [What's the point in adding a new line to the end of a
file?][stack-exchange]
[stack-exchange]: https://unix.stackexchange.com/questions/18743/whats-the-point-in-adding-a-new-line-to-the-end-of-a-file
+42
View File
@@ -0,0 +1,42 @@
# `MD048` - Code fence style
Tags: `code`
Aliases: `code-fence-style`
Parameters:
- `style`: Code fence style (`string`, default `consistent`, values `backtick`
/ `consistent` / `tilde`)
This rule is triggered when the symbols used in the document for fenced code
blocks do not match the configured code fence style:
````markdown
```ruby
# Fenced code
```
~~~ruby
# Fenced code
~~~
````
To fix this issue, use the configured code fence style throughout the
document:
````markdown
```ruby
# Fenced code
```
```ruby
# Fenced code
```
````
The configured code fence style can be a specific symbol to use (`backtick`,
`tilde`) or it can require all code fences match the first code fence
(`consistent`).
Rationale: Consistent formatting makes it easier to understand a document.
+36
View File
@@ -0,0 +1,36 @@
# `MD049` - Emphasis style
Tags: `emphasis`
Aliases: `emphasis-style`
Parameters:
- `style`: Emphasis style (`string`, default `consistent`, values `asterisk` /
`consistent` / `underscore`)
Fixable: Some 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 all emphasis matches the first emphasis
(`consistent`).
Note: Emphasis within a word is restricted to `asterisk` in order to avoid
unwanted emphasis for words containing internal underscores like_this_one.
Rationale: Consistent formatting makes it easier to understand a document.
+35
View File
@@ -0,0 +1,35 @@
# `MD050` - Strong style
Tags: `emphasis`
Aliases: `strong-style`
Parameters:
- `style`: Strong style (`string`, default `consistent`, values `asterisk` /
`consistent` / `underscore`)
Fixable: Some 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 all strong matches the first strong (`consistent`).
Note: Emphasis within a word is restricted to `asterisk` in order to avoid
unwanted emphasis for words containing internal underscores like__this__one.
Rationale: Consistent formatting makes it easier to understand a document.
+117
View File
@@ -0,0 +1,117 @@
# `MD051` - Link fragments should be valid
Tags: `links`
Aliases: `link-fragments`
Parameters:
- `ignore_case`: Ignore case of fragments (`boolean`, default `false`)
- `ignored_pattern`: Pattern for ignoring additional fragments (`string`,
default ``)
Fixable: Some violations can be fixed by tooling
This rule is triggered when a link fragment does not match any of the fragments
that are automatically generated for headings in a document:
```markdown
# Heading Name
[Link](#fragment)
```
To fix this issue, change the link fragment to reference an existing heading's
generated name (see below):
```markdown
# Heading Name
[Link](#heading-name)
```
For consistency, this rule requires fragments to exactly match the [GitHub
heading algorithm][github-heading-algorithm] which converts letters to
lowercase. Therefore, the following example is reported as a violation:
```markdown
# Heading Name
[Link](#Heading-Name)
```
To ignore case when comparing fragments with heading names, the `ignore_case`
parameter can be set to `true`. In this configuration, the previous example is
not reported as a violation.
Alternatively, some platforms allow the syntax `{#named-anchor}` to be used
within a heading to provide a specific name (consisting of only lower-case
letters, numbers, `-`, and `_`):
```markdown
# Heading Name {#custom-name}
[Link](#custom-name)
```
Alternatively, any HTML tag with an `id` attribute or an `a` tag with a `name`
attribute can be used to define a fragment:
```markdown
<a id="bookmark"></a>
[Link](#bookmark)
```
An `a` tag can be useful in scenarios where a heading is not appropriate or for
control over the text of the fragment identifier.
[HTML links to `#top` scroll to the top of a document][html-top-fragment]. This
rule allows that syntax (using lower-case for consistency):
```markdown
[Link](#top)
```
This rule also recognizes the custom fragment syntax used by GitHub to highlight
[specific content in a document][github-linking-to-content].
For example, this link to line 20:
```markdown
[Link](#L20)
```
And this link to content starting within line 19 running into line 21:
```markdown
[Link](#L19C5-L21C11)
```
Some Markdown generators dynamically create and insert headings when building
documents, for example by combining a fixed prefix like `figure-` and an
incrementing numeric counter. To ignore such generated fragments, set the
`ignored_pattern` [regular expression][RegEx] parameter to a pattern that
matches (e.g., `^figure-`).
Rationale: [GitHub section links][github-section-links] are created
automatically for every heading when Markdown content is displayed on GitHub.
This makes it easy to link directly to different sections within a document.
However, section links change if headings are renamed or removed. This rule
helps identify broken section links within a document.
Note: Section links are **not** part of the CommonMark specification; this rule
enforces the [GitHub heading algorithm][github-heading-algorithm]:
1. Convert text to lowercase
2. Remove punctuation characters
3. Convert spaces to dashes
4. Append an incrementing integer (as needed for uniqueness)
5. [URI-encode][encodeURIComponent] the result
[encodeURIComponent]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
[github-section-links]: https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#section-links
[github-heading-algorithm]: https://github.com/gjtorikian/html-pipeline/blob/f13a1534cb650ba17af400d1acd3a22c28004c09/lib/html/pipeline/toc_filter.rb
[github-linking-to-content]: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-a-permanent-link-to-a-code-snippet#linking-to-markdown
[html-top-fragment]: https://html.spec.whatwg.org/multipage/browsing-the-web.html#scrolling-to-a-fragment
[RegEx]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions
+52
View File
@@ -0,0 +1,52 @@
# `MD052` - Reference links and images should use a label that is defined
Tags: `images`, `links`
Aliases: `reference-links-images`
Parameters:
- `ignored_labels`: Ignored link labels (`string[]`, default `["x"]`)
- `shortcut_syntax`: Include shortcut syntax (`boolean`, default `false`)
Links and images in Markdown can provide the link destination or image source
at the time of use or can define it elsewhere and use a label for reference.
The reference format is convenient for keeping paragraph text clutter-free
and makes it easy to reuse the same URL in multiple places.
There are three kinds of reference links and images:
```markdown
Full: [text][label]
Collapsed: [label][]
Shortcut: [label]
Full: ![text][image]
Collapsed: ![image][]
Shortcut: ![image]
[label]: https://example.com/label
[image]: https://example.com/image
```
A link or image renders correctly when the corresponding label is defined, but
displays as text with brackets when the label is not present. By default, this
rule warns of undefined labels for "full" and "collapsed" reference syntax but
not for "shortcut" syntax because it is ambiguous.
The text `[example]` could be a shortcut link or the text "example" in brackets,
so "shortcut" syntax is ignored by default. To include "shortcut" syntax, set
the `include_shortcut` parameter to `true`. Note that doing so produces warnings
for *all* text in the document that *could* be a shortcut. If bracketed text is
intentional, brackets can be escaped with the `\` character: `\[example\]`.
If there are link labels that are deliberately unreferenced, they can be ignored
by setting the `ignored_labels` parameter to the list of strings to ignore. The
default value of this parameter ignores the checkbox syntax used by
[GitHub Flavored Markdown task list items][gfm-tasklist]:
```markdown
- [x] Checked task list item
```
[gfm-tasklist]: https://github.github.com/gfm/#task-list-items-extension-
+38
View File
@@ -0,0 +1,38 @@
# `MD053` - Link and image reference definitions should be needed
Tags: `images`, `links`
Aliases: `link-image-reference-definitions`
Parameters:
- `ignored_definitions`: Ignored definitions (`string[]`, default `["//"]`)
Fixable: Some violations can be fixed by tooling
Links and images in Markdown can provide the link destination or image source
at the time of use or can use a label to reference a definition elsewhere in
the document. The latter reference format is convenient for keeping paragraph
text clutter-free and makes it easy to reuse the same URL in multiple places.
Because link and image reference definitions are located separately from
where they are used, there are two scenarios where a definition can be
unnecessary:
1. If a label is not referenced by any link or image in a document, that
definition is unused and can be deleted.
2. If a label is defined multiple times in a document, the first definition is
used and the others can be deleted.
This rule considers a reference definition to be used if any link or image
reference has the corresponding label. The "full", "collapsed", and "shortcut"
formats are all supported.
If there are reference definitions that are deliberately unreferenced, they can
be ignored by setting the `ignored_definitions` parameter to the list of strings
to ignore. The default value of this parameter ignores the following convention
for adding non-HTML comments to Markdown:
```markdown
[//]: # (This behaves like a comment)
```
+100
View File
@@ -0,0 +1,100 @@
# `MD054` - Link and image style
Tags: `images`, `links`
Aliases: `link-image-style`
Parameters:
- `autolink`: Allow autolinks (`boolean`, default `true`)
- `collapsed`: Allow collapsed reference links and images (`boolean`, default
`true`)
- `full`: Allow full reference links and images (`boolean`, default `true`)
- `inline`: Allow inline links and images (`boolean`, default `true`)
- `shortcut`: Allow shortcut reference links and images (`boolean`, default
`true`)
- `url_inline`: Allow URLs as inline links (`boolean`, default `true`)
Fixable: Some violations can be fixed by tooling
Links and images in Markdown can provide the link destination or image source at
the time of use or can use a label to reference a definition elsewhere in the
document. The three reference formats are convenient for keeping paragraph text
clutter-free and make it easy to reuse the same URL in multiple places.
By default, this rule allows all link/image styles.
Setting the `autolink` parameter to `false` disables autolinks:
```markdown
<https://example.com>
```
Setting the `inline` parameter to `false` disables inline links and images:
```markdown
[link](https://example.com)
![image](https://example.com)
```
Setting the `full` parameter to `false` disables full reference links and
images:
```markdown
[link][url]
![image][url]
[url]: https://example.com
```
Setting the `collapsed` parameter to `false` disables collapsed reference links
and images:
```markdown
[url][]
![url][]
[url]: https://example.com
```
Setting the `shortcut` parameter to `false` disables shortcut reference links
and images:
```markdown
[url]
![url]
[url]: https://example.com
```
To fix violations of this rule, change the link or image to use an allowed
style. This rule can automatically fix violations when a link or image can be
converted to the `inline` style (preferred) or a link can be converted to the
`autolink` style (which does not support images and must be an absolute URL).
This rule does *not* fix scenarios that require converting a link or image to
the `full`, `collapsed`, or `shortcut` reference styles because that involves
naming the reference and determining where to insert it in the document.
Setting the `url_inline` parameter to `false` prevents the use of inline links
with the same absolute URL text/destination and no title because such links can
be converted to autolinks:
```markdown
[https://example.com](https://example.com)
```
To fix `url_inline` violations, use the simpler autolink syntax instead:
```markdown
<https://example.com>
```
Rationale: Consistent formatting makes it easier to understand a document.
Autolinks are concise, but appear as URLs which can be long and confusing.
Inline links and images can include descriptive text, but take up more space in
Markdown form. Reference links and images can be easier to read and manipulate
in Markdown form, but require a separate link reference definition.
+55
View File
@@ -0,0 +1,55 @@
# `MD055` - Table pipe style
Tags: `table`
Aliases: `table-pipe-style`
Parameters:
- `style`: Table pipe style (`string`, default `consistent`, values
`consistent` / `leading_and_trailing` / `leading_only` /
`no_leading_or_trailing` / `trailing_only`)
This rule is triggered when a [GitHub Flavored Markdown table][gfm-table-055]
is inconsistent about its use of leading and trailing pipe characters (`|`).
By default (`consistent` style), the header row of the first table in a document
is used to determine the style that is enforced for every table in the document.
A specific style can be used instead (`leading_and_trailing`, `leading_only`,
`no_leading_or_trailing`, `trailing_only`).
This table's header row has leading and trailing pipes, but its delimiter row is
missing the trailing pipe and its first row of cells is missing the leading
pipe:
```markdown
| Header | Header |
| ------ | ------
Cell | Cell |
```
To fix these issues, make sure there is a pipe character at the beginning and
end of every row:
```markdown
| Header | Header |
| ------ | ------ |
| Cell | Cell |
```
Note that text immediately following a table (i.e., not separated by an empty
line) is treated as part of the table (per the specification) and may also
trigger this rule:
```markdown
| Header | Header |
| ------ | ------ |
| Cell | Cell |
This text is part of the table
```
Rationale: Some parsers have difficulty with tables that are missing their
leading or trailing pipe characters. The use of leading/trailing pipes can also
help provide visual clarity.
[gfm-table-055]: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-tables
+37
View File
@@ -0,0 +1,37 @@
# `MD056` - Table column count
Tags: `table`
Aliases: `table-column-count`
This rule is triggered when a [GitHub Flavored Markdown table][gfm-table-056]
does not have the same number of cells in every row.
This table's second data row has too few cells and its third data row has too
many cells:
```markdown
| Header | Header |
| ------ | ------ |
| Cell | Cell |
| Cell |
| Cell | Cell | Cell |
```
To fix these issues, ensure every row has the same number of cells:
```markdown
| Header | Header |
| ------ | ------ |
| Cell | Cell |
| Cell | Cell |
| Cell | Cell |
```
Note that a table's header row and its delimiter row must have the same number
of cells or it will not be recognized as a table (per specification).
Rationale: Extra cells in a row are usually not shown, so their data is lost.
Missing cells in a row create holes in the table and suggest an omission.
[gfm-table-056]: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-tables
+48
View File
@@ -0,0 +1,48 @@
# `MD058` - Tables should be surrounded by blank lines
Tags: `table`
Aliases: `blanks-around-tables`
Fixable: Some violations can be fixed by tooling
This rule is triggered when tables are either not preceded or not followed by a
blank line:
```markdown
Some text
| Header | Header |
| ------ | ------ |
| Cell | Cell |
> Blockquote
```
To fix violations of this rule, ensure that all tables have a blank line both
before and after (except when the table is at the very beginning or end of the
document):
```markdown
Some text
| Header | Header |
| ------ | ------ |
| Cell | Cell |
> Blockquote
```
Note that text immediately following a table (i.e., not separated by an empty
line) is treated as part of the table (per the specification) and will not
trigger this rule:
```markdown
| Header | Header |
| ------ | ------ |
| Cell | Cell |
This text is part of the table and the next line is blank
Some text
```
Rationale: In addition to aesthetic reasons, some parsers will incorrectly parse
tables that don't have blank lines before and after them.
+33
View File
@@ -0,0 +1,33 @@
# `MD059` - Link text should be descriptive
Tags: `accessibility`, `links`
Aliases: `descriptive-link-text`
Parameters:
- `prohibited_texts`: Prohibited link texts (`string[]`, default `["click
here","here","link","more"]`)
This rule is triggered when a link has generic text like `[click here](...)` or
`[link](...)`.
Link text should be descriptive and communicate the purpose of the link (e.g.,
`[Download the budget document](...)` or `[CommonMark Specification](...)`).
This is especially important for screen readers which sometimes present links
without context.
By default, this rule prohibits a small number of common English words/phrases.
To customize that list of words/phrases, set the `prohibited_texts` parameter to
an `Array` of `string`s.
Note: For languages other than English, use the `prohibited_texts` parameter to
customize the list for that language. It is *not* a goal for this rule to have
translations for every language.
Note: This rule checks Markdown links; HTML links are ignored.
More information:
- <https://webaim.org/techniques/hypertext/>
- <https://www.w3.org/WAI/WCAG21/Understanding/link-purpose-link-only.html>
+118
View File
@@ -0,0 +1,118 @@
# `MD060` - Table column style
Tags: `table`
Aliases: `table-column-style`
Parameters:
- `aligned_delimiter`: Aligned delimiter columns (`boolean`, default `false`)
- `style`: Table column style (`string`, default `any`, values `aligned` /
`any` / `compact` / `tight`)
This rule is triggered when the column separator pipe characters (`|`) of a
[GitHub Flavored Markdown table][gfm-table-060] are used inconsistently.
This rule recognizes three table column styles based on popular use.
Style `aligned` ensures pipe characters are vertically aligned:
```markdown
| Character | Meaning |
| --------- | ------- |
| Y | Yes |
| N | No |
```
The `aligned` style ignores cell content, so the following is also valid:
```markdown
| Character | Meaning |
|-----------|---------|
| Y | Yes |
| N | No |
```
Style `compact` avoids extra padding with a single space around cell content:
```markdown
| Character | Meaning |
| --- | --- |
| Y | Yes |
| N | No |
```
Style `tight` uses no padding at all for cell content:
```markdown
|Character|Meaning|
|---|---|
|Y|Yes|
|N|No|
```
When this rule's `style` parameter is set to `aligned`, `compact`, or `tight`,
every table must match the corresponding pattern and any violations will be
reported. By default, or when the `any` style is used, each table is analyzed to
see if it satisfies any supported style. If so, no violations are reported. If
not, violations are be reported for whichever style would produce the *fewest*
issues (i.e., whichever style is the closest match).
Setting the `aligned_delimiter` parameter to `true` requires pipe characters in
the delimiter row to align with those in the header row. This can be used with
`compact` and `tight` tables to make the header text more obvious. (It's already
required for tables with style `aligned`.)
Style `compact` with `aligned_delimiter`:
```markdown
| Character | Meaning |
| --------- | ------- |
| Y | Yes |
| N | No |
```
Style `tight` with `aligned_delimiter`:
```markdown
|Character|Meaning|
|---------|-------|
|Y|Yes|
|N|No|
```
**Note**: This rule does not require leading/trailing pipe characters, so this
is also a valid table for style `compact`:
```markdown
Character | Meaning
--- | ---
Y | Yes
N | No
```
**Note**: Pipe alignment for the `aligned` style is based on visual appearance
and not character count. Because editors typically render [emoji][emoji] and
[CJK characters][cjk-characters] at *twice* the width of
[Latin characters][latin-script], this rule takes that into account for tables
using the `aligned` style. The following table is correctly formatted and will
appear aligned in most editors and monospaced fonts:
<!-- markdownlint-capture -->
<!-- markdownlint-disable extended-ascii -->
```markdown
| Response | Emoji |
| -------- | ----- |
| Yes | ✅ |
| No | ❎ |
```
<!-- markdownlint-restore -->
Rationale: Consistent formatting makes it easier to understand a document.
[cjk-characters]: https://en.wikipedia.org/wiki/CJK_characters
[emoji]: https://en.wikipedia.org/wiki/Emoji
[gfm-table-060]: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-tables
[latin-script]: https://en.wikipedia.org/wiki/Latin_script
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) David Anson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+29
View File
@@ -0,0 +1,29 @@
# markdownlint-rule-helpers
> A collection of `markdownlint` helper functions for custom rules
## Overview
The [Markdown][markdown] linter [`markdownlint`][markdownlint] offers a variety
of built-in validation [rules][rules] and supports the creation of [custom
rules][custom-rules]. The internal rules share various helper functions; this
package exposes those for reuse by custom rules.
## API
*Undocumented* - This package exports the internal functions as-is. The APIs
were not originally meant to be public, are not officially supported, and may
change from release to release. There are brief descriptive comments above each
function, but no [JSDoc][jsdoc] annotations. That said, some of what's here will
be useful to custom rule authors and may avoid duplicating code.
## Tests
*None* - The entire body of code is tested to 100% coverage by the core
`markdownlint` project, so there are no additional tests here.
[custom-rules]: https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/CustomRules.md
[jsdoc]: https://en.m.wikipedia.org/wiki/JSDoc
[markdown]: https://en.wikipedia.org/wiki/Markdown
[markdownlint]: https://github.com/DavidAnson/markdownlint
[rules]: https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/Rules.md
+696
View File
@@ -0,0 +1,696 @@
// @ts-check
"use strict";
const micromark = require("./micromark-helpers.cjs");
const { newLineRe, nextLinesRe } = require("./shared.cjs");
module.exports.newLineRe = newLineRe;
module.exports.nextLinesRe = nextLinesRe;
/** @typedef {import("../lib/exports.mjs").RuleOnError} RuleOnError */
/** @typedef {import("../lib/exports.mjs").RuleOnErrorFixInfo} RuleOnErrorFixInfo */
/** @typedef {import("../lib/exports.mjs").MicromarkToken} MicromarkToken */
// eslint-disable-next-line jsdoc/valid-types
/** @typedef {import("micromark-extension-gfm-footnote", { with: { "resolution-mode": "import" } })} */
// eslint-disable-next-line jsdoc/valid-types
/** @typedef {import("../lib/micromark-types.d.mts", { with: { "resolution-mode": "import" } })} */
// Regular expression for matching common front matter (YAML and TOML)
// @ts-ignore
module.exports.frontMatterRe =
/((^---[^\S\r\n\u2028\u2029]*$[\s\S]+?^---\s*)|(^\+\+\+[^\S\r\n\u2028\u2029]*$[\s\S]+?^(\+\+\+|\.\.\.)\s*)|(^\{[^\S\r\n\u2028\u2029]*$[\s\S]+?^\}\s*))(\r\n|\r|\n|$)/m;
// Regular expression for matching the start of inline disable/enable comments
const inlineCommentStartRe =
/(<!--\s*markdownlint-(disable|enable|capture|restore|disable-file|enable-file|disable-line|disable-next-line|configure-file))(?:\s|-->)/gi;
module.exports.inlineCommentStartRe = inlineCommentStartRe;
// Regular expression for identifying an HTML entity at the end of a line
module.exports.endOfLineHtmlEntityRe =
/&(?:#\d+|#[xX][\da-fA-F]+|[a-zA-Z]{2,31}|blk\d{2}|emsp1[34]|frac\d{2}|sup\d|there4);$/;
// Regular expression for identifying a GitHub emoji code at the end of a line
module.exports.endOfLineGemojiCodeRe =
/:(?:[abmovx]|[-+]1|100|1234|(?:1st|2nd|3rd)_place_medal|8ball|clock\d{1,4}|e-mail|non-potable_water|o2|t-rex|u5272|u5408|u55b6|u6307|u6708|u6709|u6e80|u7121|u7533|u7981|u7a7a|[a-z]{2,15}2?|[a-z]{1,14}(?:_[a-z\d]{1,16})+):$/;
// All punctuation characters (normal and full-width)
const allPunctuation = ".,;:!?。,;:!?";
module.exports.allPunctuation = allPunctuation;
// All punctuation characters without question mark (normal and full-width)
module.exports.allPunctuationNoQuestion = allPunctuation.replace(/[?]/gu, "");
/**
* Returns true iff the input is a Number.
*
* @param {Object} obj Object of unknown type.
* @returns {boolean} True iff obj is a Number.
*/
function isNumber(obj) {
return typeof obj === "number";
}
module.exports.isNumber = isNumber;
/**
* Returns true iff the input is a String.
*
* @param {Object} obj Object of unknown type.
* @returns {boolean} True iff obj is a String.
*/
function isString(obj) {
return typeof obj === "string";
}
module.exports.isString = isString;
/**
* Returns true iff the input String is empty.
*
* @param {string} str String of unknown length.
* @returns {boolean} True iff the input String is empty.
*/
function isEmptyString(str) {
return str.length === 0;
}
module.exports.isEmptyString = isEmptyString;
/**
* Returns true iff the input is an Object.
*
* @param {Object} obj Object of unknown type.
* @returns {boolean} True iff obj is an Object.
*/
function isObject(obj) {
return !!obj && (typeof obj === "object") && !Array.isArray(obj);
}
module.exports.isObject = isObject;
/**
* Returns true iff the input is a URL.
*
* @param {Object} obj Object of unknown type.
* @returns {boolean} True iff obj is a URL.
*/
function isUrl(obj) {
return !!obj && (Object.getPrototypeOf(obj) === URL.prototype);
}
module.exports.isUrl = isUrl;
/**
* Clones the input if it is an Array.
*
* @param {Object} arr Object of unknown type.
* @returns {Object} Clone of obj iff obj is an Array.
*/
function cloneIfArray(arr) {
return Array.isArray(arr) ? [ ...arr ] : arr;
}
module.exports.cloneIfArray = cloneIfArray;
/**
* Clones the input if it is a URL.
*
* @param {Object | undefined} url Object of unknown type.
* @returns {Object} Clone of obj iff obj is a URL.
*/
function cloneIfUrl(url) {
// @ts-ignore
return isUrl(url) ? new URL(url) : url;
}
module.exports.cloneIfUrl = cloneIfUrl;
/**
* Gets a Regular Expression for matching the specified HTML attribute.
*
* @param {string} name HTML attribute name.
* @returns {RegExp} Regular Expression for matching.
*/
module.exports.getHtmlAttributeRe = function getHtmlAttributeRe(name) {
return new RegExp(`\\s${name}\\s*=\\s*['"]?([^'"\\s>]*)`, "iu");
};
/**
* Returns true iff the input line is blank (contains nothing, whitespace, or
* comments (unclosed start/end comments allowed)).
*
* @param {string} line Input line.
* @returns {boolean} True iff line is blank.
*/
function isBlankLine(line) {
const startComment = "<!--";
const endComment = "-->";
const removeComments = (/** @type {string} */ s) => {
while (true) {
const start = s.indexOf(startComment);
const end = s.indexOf(endComment);
if ((end !== -1) && ((start === -1) || (end < start))) {
// Unmatched end comment is first
s = s.slice(end + endComment.length);
} else if ((start !== -1) && (end !== -1)) {
// Start comment is before end comment
s = s.slice(0, start) + s.slice(end + endComment.length);
} else if ((start !== -1) && (end === -1)) {
// Unmatched start comment is last
s = s.slice(0, start);
} else {
// No more comments to remove
return s;
}
}
};
return (
!line ||
!line.trim() ||
!removeComments(line).replace(/>/g, "").trim()
);
}
module.exports.isBlankLine = isBlankLine;
// Replaces the content of properly-formatted CommonMark comments with "."
// This preserves the line/column information for the rest of the document
// https://spec.commonmark.org/0.29/#html-blocks
// https://spec.commonmark.org/0.29/#html-comment
const htmlCommentBegin = "<!--";
const htmlCommentEnd = "-->";
const safeCommentCharacter = ".";
const startsWithPipeRe = /^ *\|/;
const notCrLfRe = /[^\r\n]/g;
const notSpaceCrLfRe = /[^ \r\n]/g;
const trailingSpaceRe = / +[\r\n]/g;
const replaceTrailingSpace = (/** @type {string} */ s) => s.replace(notCrLfRe, safeCommentCharacter);
module.exports.clearHtmlCommentText = function clearHtmlCommentText(/** @type {string} */ text) {
let i = 0;
while ((i = text.indexOf(htmlCommentBegin, i)) !== -1) {
const j = text.indexOf(htmlCommentEnd, i + 2);
if (j === -1) {
// Un-terminated comments are treated as text
break;
}
// If the comment has content...
if (j > i + htmlCommentBegin.length) {
const content = text.slice(i + htmlCommentBegin.length, j);
const lastLf = text.lastIndexOf("\n", i) + 1;
const preText = text.slice(lastLf, i);
const isBlock = preText.trim().length === 0;
const couldBeTable = startsWithPipeRe.test(preText);
const spansTableCells = couldBeTable && content.includes("\n");
const isValid =
isBlock ||
!(
spansTableCells ||
content.startsWith(">") ||
content.startsWith("->") ||
content.endsWith("-") ||
content.includes("--")
);
// If a valid block/inline comment...
if (isValid) {
const clearedContent = content
.replace(notSpaceCrLfRe, safeCommentCharacter)
.replace(trailingSpaceRe, replaceTrailingSpace);
text =
text.slice(0, i + htmlCommentBegin.length) +
clearedContent +
text.slice(j);
}
}
i = j + htmlCommentEnd.length;
}
return text;
};
// Escapes a string for use in a RegExp
module.exports.escapeForRegExp = function escapeForRegExp(/** @type {string} */ str) {
return str.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
};
/**
* Adds ellipsis to the left/right/middle of the specified text.
*
* @param {string} text Text to ellipsify.
* @param {boolean} [start] True iff the start of the text is important.
* @param {boolean} [end] True iff the end of the text is important.
* @returns {string} Ellipsified text.
*/
function ellipsify(text, start, end) {
if (text.length <= 30) {
// Nothing to do
} else if (start && end) {
text = text.slice(0, 15) + "..." + text.slice(-15);
} else if (end) {
text = "..." + text.slice(-30);
} else {
text = text.slice(0, 30) + "...";
}
return text;
}
module.exports.ellipsify = ellipsify;
/**
* Adds a generic error object via the onError callback.
*
* @param {RuleOnError} onError RuleOnError instance.
* @param {number} lineNumber Line number.
* @param {string} [detail] Error details.
* @param {string} [context] Error context.
* @param {number[]} [range] Column and length of error.
* @param {RuleOnErrorFixInfo} [fixInfo] RuleOnErrorFixInfo instance.
* @returns {void}
*/
function addError(onError, lineNumber, detail, context, range, fixInfo) {
onError({
lineNumber,
detail,
context,
range,
fixInfo
});
}
module.exports.addError = addError;
/**
* Adds an error object with details conditionally via the onError callback.
*
* @param {RuleOnError} onError RuleOnError instance.
* @param {number} lineNumber Line number.
* @param {Object} expected Expected value.
* @param {Object} actual Actual value.
* @param {string} [detail] Error details.
* @param {string} [context] Error context.
* @param {number[]} [range] Column and length of error.
* @param {RuleOnErrorFixInfo} [fixInfo] RuleOnErrorFixInfo instance.
* @returns {void}
*/
function addErrorDetailIf(
onError, lineNumber, expected, actual, detail, context, range, fixInfo) {
if (expected !== actual) {
addError(
onError,
lineNumber,
"Expected: " + expected + "; Actual: " + actual +
(detail ? "; " + detail : ""),
context,
range,
fixInfo);
}
}
module.exports.addErrorDetailIf = addErrorDetailIf;
/**
* Adds an error object with context via the onError callback.
*
* @param {RuleOnError} onError RuleOnError instance.
* @param {number} lineNumber Line number.
* @param {string} context Error context.
* @param {boolean} [start] True iff the start of the text is important.
* @param {boolean} [end] True iff the end of the text is important.
* @param {number[]} [range] Column and length of error.
* @param {RuleOnErrorFixInfo} [fixInfo] RuleOnErrorFixInfo instance.
* @returns {void}
*/
function addErrorContext(onError, lineNumber, context, start, end, range, fixInfo) {
// Normalize new line characters so Linux and Windows trim consistently
context = ellipsify(context.replace(newLineRe, "\n"), start, end);
addError(onError, lineNumber, undefined, context, range, fixInfo);
}
module.exports.addErrorContext = addErrorContext;
/**
* Defines a range within a file (start line/column to end line/column, subset of MicromarkToken).
*
* @typedef {Object} FileRange
* @property {number} startLine Start line (1-based).
* @property {number} startColumn Start column (1-based).
* @property {number} endLine End line (1-based).
* @property {number} endColumn End column (1-based).
*/
/**
* Returns whether line/column A is less than or equal to line/column B.
*
* @param {number} lineA Line A.
* @param {number} columnA Column A.
* @param {number} lineB Line B.
* @param {number} columnB Column B.
* @returns {boolean} True iff A is less than or equal to B.
*/
const positionLessThanOrEqual = (lineA, columnA, lineB, columnB) => (
(lineA < lineB) ||
((lineA === lineB) && (columnA <= columnB))
);
/**
* Returns whether two ranges (or MicromarkTokens) overlap anywhere.
*
* @param {FileRange|MicromarkToken} rangeA Range A.
* @param {FileRange|MicromarkToken} rangeB Range B.
* @returns {boolean} True iff the two ranges overlap.
*/
module.exports.hasOverlap = function hasOverlap(rangeA, rangeB) {
const lte = positionLessThanOrEqual(rangeA.startLine, rangeA.startColumn, rangeB.startLine, rangeB.startColumn);
const first = lte ? rangeA : rangeB;
const second = lte ? rangeB : rangeA;
return positionLessThanOrEqual(second.startLine, second.startColumn, first.endLine, first.endColumn);
};
// Determines if the front matter includes a title
module.exports.frontMatterHasTitle =
function frontMatterHasTitle(/** @type {readonly string[]} */ frontMatterLines, /** @type {string} */ frontMatterTitlePattern) {
const ignoreFrontMatter =
(frontMatterTitlePattern !== undefined) && !frontMatterTitlePattern;
const frontMatterTitleRe =
new RegExp(
String(frontMatterTitlePattern || "^\\s*\"?title\"?\\s*[:=]"),
"i"
);
return !ignoreFrontMatter &&
frontMatterLines.some((line) => frontMatterTitleRe.test(line));
};
/**
* Result object for getReferenceLinkImageData.
*
* @typedef {Object} GetReferenceLinkImageDataResult
* @property {Map<string, number[][]>} references References.
* @property {Map<string, number[][]>} shortcuts Shortcuts.
* @property {Map<string, [number, string]>} definitions Definitions.
* @property {[string, number][]} duplicateDefinitions Duplicate definitions.
* @property {number[]} definitionLineIndices Definition line indices.
*/
/**
* Returns an object with information about reference links and images.
*
* @param {MicromarkToken[]} tokens Micromark tokens.
* @returns {GetReferenceLinkImageDataResult} Reference link/image data.
*/
function getReferenceLinkImageData(tokens) {
const normalizeReference = (/** @type {string} */ s) => s.toLowerCase().trim().replace(/\s+/g, " ");
const getText = (/** @type {MicromarkToken} */ t) => t?.children.filter((c) => c.type !== "blockQuotePrefix").map((c) => c.text).join("");
/** @type {Map<string, number[][]>} */
const references = new Map();
/** @type {Map<string, number[][]>} */
const shortcuts = new Map();
const addReferenceToDictionary = (/** @type {MicromarkToken} */ token, /** @type {string} */ label, /** @type {boolean} */ isShortcut) => {
const referenceDatum = [
token.startLine - 1,
token.startColumn - 1,
token.text.length
];
const reference = normalizeReference(label);
const dictionary = isShortcut ? shortcuts : references;
const referenceData = dictionary.get(reference) || [];
referenceData.push(referenceDatum);
dictionary.set(reference, referenceData);
};
/** @type {Map<string, [number, string]>} */
const definitions = new Map();
/** @type {number[]} */
const definitionLineIndices = [];
/** @type {[string, number][]} */
const duplicateDefinitions = [];
const filteredTokens =
micromark.filterByTypes(
tokens,
[
// definitionLineIndices
"definition", "gfmFootnoteDefinition",
// definitions and definitionLineIndices
"definitionLabelString", "gfmFootnoteDefinitionLabelString",
// references and shortcuts
"gfmFootnoteCall", "image", "link",
// undefined link labels
"undefinedReferenceCollapsed", "undefinedReferenceFull", "undefinedReferenceShortcut"
]
);
for (const token of filteredTokens) {
let labelPrefix = "";
// eslint-disable-next-line default-case
switch (token.type) {
case "definition":
case "gfmFootnoteDefinition":
// definitionLineIndices
for (let i = token.startLine; i <= token.endLine; i++) {
definitionLineIndices.push(i - 1);
}
break;
case "gfmFootnoteDefinitionLabelString":
labelPrefix = "^";
case "definitionLabelString": // eslint-disable-line no-fallthrough
{
// definitions and definitionLineIndices
const reference = normalizeReference(`${labelPrefix}${token.text}`);
if (definitions.has(reference)) {
duplicateDefinitions.push([ reference, token.startLine - 1 ]);
} else {
const parent =
micromark.getParentOfType(token, [ "definition" ]);
const destinationString = parent &&
micromark.getDescendantsByType(parent, [ "definitionDestination", "definitionDestinationRaw", "definitionDestinationString" ])[0]?.text;
definitions.set(
reference,
[ token.startLine - 1, destinationString || "" ]
);
}
}
break;
case "gfmFootnoteCall":
case "image":
case "link":
{
// Identify if shortcut or full/collapsed
let isShortcut = (token.children.length === 1);
const isFullOrCollapsed = (token.children.length === 2) && !token.children.some((t) => t.type === "resource");
const [ labelText ] = micromark.getDescendantsByType(token, [ "label", "labelText" ]);
const [ referenceString ] = micromark.getDescendantsByType(token, [ "reference", "referenceString" ]);
let label = getText(labelText);
// Identify if footnote
if (!isShortcut && !isFullOrCollapsed) {
const [ footnoteCallMarker, footnoteCallString ] = token.children.filter(
(t) => [ "gfmFootnoteCallMarker", "gfmFootnoteCallString" ].includes(t.type)
);
if (footnoteCallMarker && footnoteCallString) {
label = `${footnoteCallMarker.text}${footnoteCallString.text}`;
isShortcut = true;
}
}
// Track link (handle shortcuts separately due to ambiguity in "text [text] text")
if (isShortcut || isFullOrCollapsed) {
addReferenceToDictionary(token, getText(referenceString) || label, isShortcut);
}
}
break;
case "undefinedReferenceCollapsed":
case "undefinedReferenceFull":
case "undefinedReferenceShortcut":
{
const undefinedReference = micromark.getDescendantsByType(token, [ "undefinedReference" ])[0];
const label = undefinedReference.children.map((t) => t.text).join("");
const isShortcut = (token.type === "undefinedReferenceShortcut");
addReferenceToDictionary(token, label, isShortcut);
}
break;
}
}
return {
references,
shortcuts,
definitions,
duplicateDefinitions,
definitionLineIndices
};
}
module.exports.getReferenceLinkImageData = getReferenceLinkImageData;
/**
* Gets the most common line ending, falling back to the platform default.
*
* @param {string} input Markdown content to analyze.
* @param {{EOL: string}} [os] Node.js "os" module.
* @returns {string} Preferred line ending.
*/
function getPreferredLineEnding(input, os) {
let cr = 0;
let lf = 0;
let crlf = 0;
const endings = input.match(newLineRe) || [];
for (const ending of endings) {
// eslint-disable-next-line default-case
switch (ending) {
case "\r":
cr++;
break;
case "\n":
lf++;
break;
case "\r\n":
crlf++;
break;
}
}
let preferredLineEnding = null;
if (!cr && !lf && !crlf) {
preferredLineEnding = (os && os.EOL) || "\n";
} else if ((lf >= crlf) && (lf >= cr)) {
preferredLineEnding = "\n";
} else if (crlf >= cr) {
preferredLineEnding = "\r\n";
} else {
preferredLineEnding = "\r";
}
return preferredLineEnding;
}
module.exports.getPreferredLineEnding = getPreferredLineEnding;
/**
* Expands a path with a tilde to an absolute path.
*
* @param {string} file Path that may begin with a tilde.
* @param {{homedir: () => string}} os Node.js "os" module.
* @returns {string} Absolute path (or original path).
*/
function expandTildePath(file, os) {
const homedir = os && os.homedir && os.homedir();
return homedir ? file.replace(/^~($|\/|\\)/, `${homedir}$1`) : file;
}
module.exports.expandTildePath = expandTildePath;
/** @typedef {import("../lib/markdownlint.mjs").LintError[]} LintErrors */
/** @typedef {import("../lib/markdownlint.mjs").LintResults} LintResults */
/**
* Converts lint errors from resultVersion 3 to 2.
*
* @param {LintErrors} errors Lint errors (v3).
* @returns {LintErrors} Lint errors (v2).
*/
function convertLintErrorsVersion3To2(errors) {
const noPrevious = {
"ruleNames": [],
"lineNumber": -1
};
return errors.filter((error, index, array) => {
// @ts-ignore
delete error.fixInfo;
// @ts-ignore
delete error.severity;
const previous = array[index - 1] || noPrevious;
return (
(error.ruleNames[0] !== previous.ruleNames[0]) ||
(error.lineNumber !== previous.lineNumber)
);
});
}
/**
* Converts lint errors from resultVersion 2 to 1.
*
* @param {LintErrors} errors Lint errors (v2).
* @returns {LintErrors} Lint errors (v1).
*/
function convertLintErrorsVersion2To1(errors) {
for (const error of errors) {
// @ts-ignore
error.ruleName = error.ruleNames[0];
// @ts-ignore
error.ruleAlias = error.ruleNames[1] || error.ruleName;
// @ts-ignore
delete error.ruleNames;
}
return errors;
}
/**
* Converts lint errors from resultVersion 2 to 0.
*
* @param {LintErrors} errors Lint errors (v2).
* @returns {LintErrors} Lint errors (v0).
*/
function convertLintErrorsVersion2To0(errors) {
/** @type {Object.<string, number[]>} */
const dictionary = {};
for (const error of errors) {
const ruleName = error.ruleNames[0];
const ruleLines = dictionary[ruleName] || [];
ruleLines.push(error.lineNumber);
dictionary[ruleName] = ruleLines;
}
// @ts-ignore
return dictionary;
}
/**
* Copies and transforms lint results from resultVersion 3 to ?.
*
* @param {LintResults} results Lint results (v3).
* @param {(errors: LintErrors) => LintErrors} transform Lint errors (v?).
* @returns {LintResults} Lint results (v?).
*/
function copyAndTransformResults(results, transform) {
/** @type {Object.<string, LintErrors>} */
const newResults = {};
Object.defineProperty(newResults, "toString", { "value": results.toString });
for (const key of Object.keys(results)) {
const arr = results[key].map((r) => ({ ...r }));
newResults[key] = transform(arr);
}
// @ts-ignore
return newResults;
}
/**
* Converts lint results from resultVersion 3 to 0.
*
* @param {LintResults} results Lint results (v3).
* @returns {LintResults} Lint results (v0).
*/
module.exports.convertToResultVersion0 = function convertToResultVersion0(results) {
return copyAndTransformResults(results, (r) => convertLintErrorsVersion2To0(convertLintErrorsVersion3To2(r)));
};
/**
* Converts lint results from resultVersion 3 to 1.
*
* @param {LintResults} results Lint results (v3).
* @returns {LintResults} Lint results (v1).
*/
module.exports.convertToResultVersion1 = function convertToResultVersion1(results) {
return copyAndTransformResults(results, (r) => convertLintErrorsVersion2To1(convertLintErrorsVersion3To2(r)));
};
/**
* Converts lint results from resultVersion 3 to 2.
*
* @param {LintResults} results Lint results (v3).
* @returns {LintResults} Lint results (v2).
*/
module.exports.convertToResultVersion2 = function convertToResultVersion2(results) {
return copyAndTransformResults(results, convertLintErrorsVersion3To2);
};
/**
* Formats lint results to an array of strings.
*
* @param {LintResults|undefined} lintResults Lint results.
* @returns {string[]} Lint error strings.
*/
module.exports.formatLintResults = function formatLintResults(lintResults) {
const results = [];
const entries = Object.entries(lintResults || {});
entries.sort((a, b) => a[0].localeCompare(b[0]));
for (const [ source, lintErrors ] of entries) {
for (const lintError of lintErrors) {
const { lineNumber, ruleNames, ruleDescription, errorDetail, errorContext, errorRange, severity } = lintError;
const rule = ruleNames.join("/");
const line = `:${lineNumber}`;
const rangeStart = (errorRange && errorRange[0]) || 0;
const column = rangeStart ? `:${rangeStart}` : "";
const description = ruleDescription;
const detail = (errorDetail ? ` [${errorDetail}]` : "");
const context = (errorContext ? ` [Context: "${errorContext}"]` : "");
results.push(`${source}${line}${column} ${severity} ${rule} ${description}${detail}${context}`);
}
}
return results;
};
+331
View File
@@ -0,0 +1,331 @@
// @ts-check
"use strict";
const { flatTokensSymbol, htmlFlowSymbol, newLineRe } = require("./shared.cjs");
// eslint-disable-next-line jsdoc/valid-types
/** @typedef {import("micromark-util-types", { with: { "resolution-mode": "import" } }).TokenType} TokenType */
/** @typedef {import("../lib/exports.mjs").MicromarkToken} Token */
// eslint-disable-next-line jsdoc/valid-types
/** @typedef {import("../lib/micromark-types.d.mts", { with: { "resolution-mode": "import" } })} */
/**
* Determines if a Micromark token is within an htmlFlow type.
*
* @param {Token} token Micromark token.
* @returns {boolean} True iff the token is within an htmlFlow type.
*/
function inHtmlFlow(token) {
// @ts-ignore
return Boolean(token[htmlFlowSymbol]);
}
/**
* Returns whether a token is an htmlFlow type containing an HTML comment.
*
* @param {Token} token Micromark token.
* @returns {boolean} True iff token is htmlFlow containing a comment.
*/
function isHtmlFlowComment(token) {
const { text, type } = token;
if (
(type === "htmlFlow") &&
text.startsWith("<!--") &&
text.endsWith("-->")
) {
const comment = text.slice(4, -3);
return (
!comment.startsWith(">") &&
!comment.startsWith("->") &&
!comment.endsWith("-")
// The following condition from the CommonMark specification is commented
// to avoid parsing HTML comments that include "--" because that is NOT a
// condition of the HTML specification.
// https://spec.commonmark.org/0.30/#raw-html
// https://html.spec.whatwg.org/multipage/syntax.html#comments
// && !comment.includes("--")
);
}
return false;
}
/**
* Adds a range of numbers to a set.
*
* @param {Set<number>} set Set of numbers.
* @param {number} start Starting number.
* @param {number} end Ending number.
* @returns {void}
*/
function addRangeToSet(set, start, end) {
for (let i = start; i <= end; i++) {
set.add(i);
}
}
/**
* @callback AllowedPredicate
* @param {Token} token Micromark token.
* @returns {boolean} True iff allowed.
*/
/**
* @callback TransformPredicate
* @param {Token} token Micromark token.
* @returns {Token[]} Child tokens.
*/
/**
* Filter a list of Micromark tokens by predicate.
*
* @param {Token[]} tokens Micromark tokens.
* @param {AllowedPredicate} allowed Allowed token predicate.
* @param {TransformPredicate} [transformChildren] Transform predicate.
* @returns {Token[]} Filtered tokens.
*/
function filterByPredicate(tokens, allowed, transformChildren) {
const result = [];
const queue = [
{
"array": tokens,
"index": 0
}
];
while (queue.length > 0) {
const current = queue[queue.length - 1];
const { array, index } = current;
if (index < array.length) {
const token = array[current.index++];
if (allowed(token)) {
result.push(token);
}
const { children } = token;
if (children.length > 0) {
const transformed =
transformChildren ? transformChildren(token) : children;
queue.push(
{
"array": transformed,
"index": 0
}
);
}
} else {
queue.pop();
}
}
return result;
}
/**
* Filter a list of Micromark tokens by type.
*
* @param {Token[]} tokens Micromark tokens.
* @param {TokenType[]} types Types to allow.
* @param {boolean} [htmlFlow] Whether to include htmlFlow content.
* @returns {Token[]} Filtered tokens.
*/
function filterByTypes(tokens, types, htmlFlow) {
const predicate = (/** @type {Token} */ token) => types.includes(token.type) && (htmlFlow || !inHtmlFlow(token));
/** @type {Token[]} */
const flatTokens =
// @ts-ignore
tokens[flatTokensSymbol];
if (flatTokens) {
return flatTokens.filter(predicate);
}
return filterByPredicate(tokens, predicate);
}
/**
* Gets the blockquote prefix text (if any) for the specified line number.
*
* @param {Token[]} tokens Micromark tokens.
* @param {number} lineNumber Line number to examine.
* @param {number} [count] Number of times to repeat.
* @returns {string} Blockquote prefix text.
*/
function getBlockQuotePrefixText(tokens, lineNumber, count = 1) {
return filterByTypes(tokens, [ "blockQuotePrefix", "linePrefix" ])
.filter((prefix) => prefix.startLine === lineNumber)
.map((prefix) => prefix.text)
.join("")
.trimEnd()
// eslint-disable-next-line unicorn/prefer-spread
.concat("\n")
.repeat(count);
};
/**
* Gets a list of nested Micromark token descendants by type path.
*
* @param {Token|Token[]} parent Micromark token parent or parents.
* @param {(TokenType|TokenType[])[]} typePath Micromark token type path.
* @returns {Token[]} Micromark token descendants.
*/
function getDescendantsByType(parent, typePath) {
let tokens = Array.isArray(parent) ? parent : [ parent ];
for (const type of typePath) {
const predicate = (/** @type {Token} */ token) => Array.isArray(type) ? type.includes(token.type) : (type === token.type);
tokens = tokens.flatMap((t) => t.children.filter(predicate));
}
return tokens;
}
/**
* Gets the heading level of a Micromark heading tokan.
*
* @param {Token} heading Micromark heading token.
* @returns {number} Heading level.
*/
function getHeadingLevel(heading) {
let level = 1;
const headingSequence = heading.children.find(
(child) => [ "atxHeadingSequence", "setextHeadingLine" ].includes(child.type)
);
// @ts-ignore
const { text } = headingSequence;
if (text[0] === "#") {
level = Math.min(text.length, 6);
} else if (text[0] === "-") {
level = 2;
}
return level;
}
/**
* Gets the heading style of a Micromark heading tokan.
*
* @param {Token} heading Micromark heading token.
* @returns {"atx" | "atx_closed" | "setext"} Heading style.
*/
function getHeadingStyle(heading) {
if (heading.type === "setextHeading") {
return "setext";
}
const atxHeadingSequenceLength = heading.children.filter(
(child) => child.type === "atxHeadingSequence"
).length;
if (atxHeadingSequenceLength === 1) {
return "atx";
}
return "atx_closed";
}
/**
* Gets the heading text of a Micromark heading token.
*
* @param {Token} heading Micromark heading token.
* @returns {string} Heading text.
*/
function getHeadingText(heading) {
return getDescendantsByType(heading, [ [ "atxHeadingText", "setextHeadingText" ] ])
.flatMap((descendant) => descendant.children.filter((child) => child.type !== "htmlText"))
.map((data) => data.text)
.join("")
.replace(newLineRe, " ");
}
/**
* HTML tag information.
*
* @typedef {Object} HtmlTagInfo
* @property {boolean} close True iff close tag.
* @property {string} name Tag name.
*/
/**
* Gets information about the tag in an HTML token.
*
* @param {Token} token Micromark token.
* @returns {HtmlTagInfo | null} HTML tag information.
*/
function getHtmlTagInfo(token) {
const htmlTagNameRe = /^<([^!>][^/\s>]*)/;
if (token.type === "htmlText") {
const match = htmlTagNameRe.exec(token.text);
if (match) {
const name = match[1];
const close = name.startsWith("/");
return {
close,
"name": close ? name.slice(1) : name
};
}
}
return null;
}
/**
* Gets the nearest parent of the specified type for a Micromark token.
*
* @param {Token} token Micromark token.
* @param {TokenType[]} types Types to allow.
* @returns {Token | null} Parent token.
*/
function getParentOfType(token, types) {
/** @type {Token | null} */
let current = token;
while ((current = current.parent) && !types.includes(current.type)) {
// Empty
}
return current;
}
const docfxTabSyntaxRe = /^#tab\//;
/**
* Returns whether the specified Micromark token looks like a Docfx tab.
*
* @param {Token | null} heading Micromark token.
* @returns {boolean} True iff the token looks like a Docfx tab.
*/
function isDocfxTab(heading) {
// See https://dotnet.github.io/docfx/docs/markdown.html?tabs=linux%2Cdotnet#tabs
if (heading?.type === "atxHeading") {
const headingTexts = getDescendantsByType(heading, [ "atxHeadingText" ]);
if ((headingTexts.length === 1) && (headingTexts[0].children.length === 1) && (headingTexts[0].children[0].type === "link")) {
const resourceDestinationStrings = filterByTypes(headingTexts[0].children[0].children, [ "resourceDestinationString" ]);
return (resourceDestinationStrings.length === 1) && docfxTabSyntaxRe.test(resourceDestinationStrings[0].text);
}
}
return false;
}
/**
* Set containing token types that do not contain content.
*
* @type {Set<TokenType>}
*/
const nonContentTokens = new Set([
"blockQuoteMarker",
"blockQuotePrefix",
"blockQuotePrefixWhitespace",
"gfmFootnoteDefinitionIndent",
"lineEnding",
"lineEndingBlank",
"linePrefix",
"listItemIndent",
"undefinedReference",
"undefinedReferenceCollapsed",
"undefinedReferenceFull",
"undefinedReferenceShortcut"
]);
module.exports = {
addRangeToSet,
filterByPredicate,
filterByTypes,
getBlockQuotePrefixText,
getDescendantsByType,
getHeadingLevel,
getHeadingStyle,
getHeadingText,
getHtmlTagInfo,
getParentOfType,
inHtmlFlow,
isDocfxTab,
isHtmlFlowComment,
nonContentTokens
};
+26
View File
@@ -0,0 +1,26 @@
{
"name": "markdownlint-rule-helpers",
"version": "0.30.0",
"description": "A collection of markdownlint helper functions for custom rules",
"main": "./helpers.cjs",
"exports": {
".": "./helpers.cjs",
"./micromark": "./micromark-helpers.cjs"
},
"author": "David Anson (https://dlaa.me/)",
"license": "MIT",
"homepage": "https://github.com/DavidAnson/markdownlint",
"repository": {
"type": "git",
"url": "git+https://github.com/DavidAnson/markdownlint.git"
},
"bugs": "https://github.com/DavidAnson/markdownlint/issues",
"funding": "https://github.com/sponsors/DavidAnson",
"engines": {
"node": ">=18"
},
"keywords": [
"markdownlint",
"markdownlint-rule"
]
}
+16
View File
@@ -0,0 +1,16 @@
// @ts-check
"use strict";
// Symbol for identifing the flat tokens array from micromark parse
module.exports.flatTokensSymbol = Symbol("flat-tokens");
// Symbol for identifying the htmlFlow token from micromark parse
module.exports.htmlFlowSymbol = Symbol("html-flow");
// Regular expression for matching common newline characters
// See NEWLINES_RE in markdown-it/lib/rules_core/normalize.js
module.exports.newLineRe = /\r\n?|\n/g;
// Regular expression for matching next lines
module.exports.nextLinesRe = /[\r\n][\s\S]*$/;
+80
View File
@@ -0,0 +1,80 @@
// @ts-check
import { getReferenceLinkImageData as helpersGetReferenceLinkImageData } from "../helpers/helpers.cjs";
import { filterByTypes } from "../helpers/micromark-helpers.cjs";
/** @typedef {import("markdownlint").RuleParams} RuleParams */
/** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */
/** @typedef {import("markdownlint").MicromarkTokenType} MicromarkTokenType */
/** @typedef {import("../helpers/helpers.cjs").GetReferenceLinkImageDataResult} GetReferenceLinkImageDataResult */
/** @type {Map<string, object>} */
const map = new Map();
/** @type {RuleParams | undefined} */
let params = undefined;
/**
* Initializes (resets) the cache.
*
* @param {RuleParams} [p] Rule parameters object.
* @returns {void}
*/
export function initialize(p) {
map.clear();
params = p;
}
/**
* Gets the cached Micromark token array (for testing).
*
* @returns {MicromarkToken[]} Micromark tokens.
*/
export function micromarkTokens() {
return params?.parsers.micromark.tokens || [];
}
/**
* Gets a cached object value - computes it and caches it.
*
* @param {string} name Cache object name.
* @param {() => Object} getValue Getter for object value.
* @returns {Object} Object value.
*/
function getCached(name, getValue) {
if (map.has(name)) {
// @ts-ignore
return map.get(name);
}
const value = getValue();
map.set(name, value);
return value;
}
/**
* Filters a list of Micromark tokens by type and caches the result.
*
* @param {MicromarkTokenType[]} types Types to allow.
* @param {boolean} [htmlFlow] Whether to include htmlFlow content.
* @returns {MicromarkToken[]} Filtered tokens.
*/
export function filterByTypesCached(types, htmlFlow) {
// @ts-ignore
return getCached(
// eslint-disable-next-line prefer-rest-params
JSON.stringify(arguments),
() => filterByTypes(micromarkTokens(), types, htmlFlow)
);
}
/**
* Gets a reference link and image data object.
*
* @returns {GetReferenceLinkImageDataResult} Reference link and image data object.
*/
export function getReferenceLinkImageData() {
// @ts-ignore
return getCached(
getReferenceLinkImageData.name,
() => helpersGetReferenceLinkImageData(micromarkTokens())
);
}
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
import type { ConfigurationStrict } from "./configuration-strict.d.ts";
export interface Configuration extends ConfigurationStrict {
/**
* Index signature for arbitrary custom rules.
*/
[k: string]: unknown;
}
+14
View File
@@ -0,0 +1,14 @@
// @ts-check
/** @type {string[]} */
export const deprecatedRuleNames = [];
export const fixableRuleNames = [
"MD004", "MD005", "MD007", "MD009", "MD010", "MD011",
"MD012", "MD014", "MD018", "MD019", "MD020", "MD021",
"MD022", "MD023", "MD026", "MD027", "MD029", "MD030",
"MD031", "MD032", "MD034", "MD037", "MD038", "MD039",
"MD044", "MD047", "MD049", "MD050", "MD051", "MD053",
"MD054", "MD058"
];
export const homepage = "https://github.com/DavidAnson/markdownlint";
export const version = "0.40.0";
+18
View File
@@ -0,0 +1,18 @@
// @ts-check
"use strict";
/* eslint-disable jsdoc/reject-any-type */
/**
* Calls require for markdownit.cjs. Used to synchronously defer loading because module.createRequire is buggy under webpack (https://github.com/webpack/webpack/issues/16724).
*
* @returns {any} Exported module content.
*/
function requireMarkdownItCjs() {
return require("./markdownit.cjs");
}
module.exports = {
requireMarkdownItCjs
};
+1
View File
@@ -0,0 +1 @@
export { lintAsync as lint, readConfigAsync as readConfig } from "./markdownlint.mjs";
+3
View File
@@ -0,0 +1,3 @@
// @ts-check
export { lintAsync as lint, readConfigAsync as readConfig } from "./markdownlint.mjs";
+1
View File
@@ -0,0 +1 @@
export { extendConfigPromise as extendConfig, lintPromise as lint, readConfigPromise as readConfig } from "./markdownlint.mjs";
+3
View File
@@ -0,0 +1,3 @@
// @ts-check
export { extendConfigPromise as extendConfig, lintPromise as lint, readConfigPromise as readConfig } from "./markdownlint.mjs";
+1
View File
@@ -0,0 +1 @@
export { lintSync as lint, readConfigSync as readConfig } from "./markdownlint.mjs";
+3
View File
@@ -0,0 +1,3 @@
// @ts-check
export { lintSync as lint, readConfigSync as readConfig } from "./markdownlint.mjs";
+29
View File
@@ -0,0 +1,29 @@
export { resolveModule } from "./resolve-module.cjs";
export type Configuration = import("./markdownlint.mjs").Configuration;
export type ConfigurationParser = import("./markdownlint.mjs").ConfigurationParser;
export type ConfigurationStrict = import("./markdownlint.mjs").ConfigurationStrict;
export type FixInfo = import("./markdownlint.mjs").FixInfo;
export type FixInfoNormalized = import("./markdownlint.mjs").FixInfoNormalized;
export type LintCallback = import("./markdownlint.mjs").LintCallback;
export type LintContentCallback = import("./markdownlint.mjs").LintContentCallback;
export type LintError = import("./markdownlint.mjs").LintError;
export type LintResults = import("./markdownlint.mjs").LintResults;
export type MarkdownItFactory = import("./markdownlint.mjs").MarkdownItFactory;
export type MarkdownItToken = import("./markdownlint.mjs").MarkdownItToken;
export type MarkdownParsers = import("./markdownlint.mjs").MarkdownParsers;
export type MicromarkToken = import("./markdownlint.mjs").MicromarkToken;
export type MicromarkTokenType = import("./markdownlint.mjs").MicromarkTokenType;
export type Options = import("./markdownlint.mjs").Options;
export type ParserMarkdownIt = import("./markdownlint.mjs").ParserMarkdownIt;
export type ParserMicromark = import("./markdownlint.mjs").ParserMicromark;
export type Plugin = import("./markdownlint.mjs").Plugin;
export type ReadConfigCallback = import("./markdownlint.mjs").ReadConfigCallback;
export type ResolveConfigExtendsCallback = import("./markdownlint.mjs").ResolveConfigExtendsCallback;
export type Rule = import("./markdownlint.mjs").Rule;
export type RuleConfiguration = import("./markdownlint.mjs").RuleConfiguration;
export type RuleFunction = import("./markdownlint.mjs").RuleFunction;
export type RuleOnError = import("./markdownlint.mjs").RuleOnError;
export type RuleOnErrorFixInfo = import("./markdownlint.mjs").RuleOnErrorFixInfo;
export type RuleOnErrorInfo = import("./markdownlint.mjs").RuleOnErrorInfo;
export type RuleParams = import("./markdownlint.mjs").RuleParams;
export { applyFix, applyFixes, getVersion } from "./markdownlint.mjs";
+32
View File
@@ -0,0 +1,32 @@
// @ts-check
export { applyFix, applyFixes, getVersion } from "./markdownlint.mjs";
export { resolveModule } from "./resolve-module.cjs";
/** @typedef {import("./markdownlint.mjs").Configuration} Configuration */
/** @typedef {import("./markdownlint.mjs").ConfigurationParser} ConfigurationParser */
/** @typedef {import("./markdownlint.mjs").ConfigurationStrict} ConfigurationStrict */
/** @typedef {import("./markdownlint.mjs").FixInfo} FixInfo */
/** @typedef {import("./markdownlint.mjs").FixInfoNormalized} FixInfoNormalized */
/** @typedef {import("./markdownlint.mjs").LintCallback} LintCallback */
/** @typedef {import("./markdownlint.mjs").LintContentCallback} LintContentCallback */
/** @typedef {import("./markdownlint.mjs").LintError} LintError */
/** @typedef {import("./markdownlint.mjs").LintResults} LintResults */
/** @typedef {import("./markdownlint.mjs").MarkdownItFactory} MarkdownItFactory */
/** @typedef {import("./markdownlint.mjs").MarkdownItToken} MarkdownItToken */
/** @typedef {import("./markdownlint.mjs").MarkdownParsers} MarkdownParsers */
/** @typedef {import("./markdownlint.mjs").MicromarkToken} MicromarkToken */
/** @typedef {import("./markdownlint.mjs").MicromarkTokenType} MicromarkTokenType */
/** @typedef {import("./markdownlint.mjs").Options} Options */
/** @typedef {import("./markdownlint.mjs").ParserMarkdownIt} ParserMarkdownIt */
/** @typedef {import("./markdownlint.mjs").ParserMicromark} ParserMicromark */
/** @typedef {import("./markdownlint.mjs").Plugin} Plugin */
/** @typedef {import("./markdownlint.mjs").ReadConfigCallback} ReadConfigCallback */
/** @typedef {import("./markdownlint.mjs").ResolveConfigExtendsCallback} ResolveConfigExtendsCallback */
/** @typedef {import("./markdownlint.mjs").Rule} Rule */
/** @typedef {import("./markdownlint.mjs").RuleConfiguration} RuleConfiguration */
/** @typedef {import("./markdownlint.mjs").RuleFunction} RuleFunction */
/** @typedef {import("./markdownlint.mjs").RuleOnError} RuleOnError */
/** @typedef {import("./markdownlint.mjs").RuleOnErrorFixInfo} RuleOnErrorFixInfo */
/** @typedef {import("./markdownlint.mjs").RuleOnErrorInfo} RuleOnErrorInfo */
/** @typedef {import("./markdownlint.mjs").RuleParams} RuleParams */
+170
View File
@@ -0,0 +1,170 @@
// @ts-check
"use strict";
const { newLineRe } = require("../helpers");
// @ts-expect-error https://github.com/microsoft/TypeScript/issues/52529
/** @typedef {import("markdownlint").MarkdownIt} MarkdownIt */
/** @typedef {import("markdownlint").MarkdownItToken} MarkdownItToken */
/** @typedef {import("markdownlint").Plugin} Plugin */
/**
* @callback InlineCodeSpanCallback
* @param {string} code Code content.
* @param {number} lineIndex Line index (0-based).
* @param {number} columnIndex Column index (0-based).
* @param {number} ticks Count of backticks.
* @returns {void}
*/
/**
* Calls the provided function for each inline code span's content.
*
* @param {string} input Markdown content.
* @param {InlineCodeSpanCallback} handler Callback function taking (code,
* lineIndex, columnIndex, ticks).
* @returns {void}
*/
function forEachInlineCodeSpan(input, handler) {
const backtickRe = /`+/g;
let match = null;
const backticksLengthAndIndex = [];
while ((match = backtickRe.exec(input)) !== null) {
backticksLengthAndIndex.push([ match[0].length, match.index ]);
}
const newLinesIndex = [];
while ((match = newLineRe.exec(input)) !== null) {
newLinesIndex.push(match.index);
}
let lineIndex = 0;
let lineStartIndex = 0;
let k = 0;
for (let i = 0; i < backticksLengthAndIndex.length - 1; i++) {
const [ startLength, startIndex ] = backticksLengthAndIndex[i];
if ((startIndex === 0) || (input[startIndex - 1] !== "\\")) {
for (let j = i + 1; j < backticksLengthAndIndex.length; j++) {
const [ endLength, endIndex ] = backticksLengthAndIndex[j];
if (startLength === endLength) {
for (; k < newLinesIndex.length; k++) {
const newLineIndex = newLinesIndex[k];
if (startIndex < newLineIndex) {
break;
}
lineIndex++;
lineStartIndex = newLineIndex + 1;
}
const columnIndex = startIndex - lineStartIndex + startLength;
handler(
input.slice(startIndex + startLength, endIndex),
lineIndex,
columnIndex,
startLength
);
i = j;
break;
}
}
}
}
}
/**
* Freeze all freeze-able members of a token and its children.
*
* @param {MarkdownItToken} token A markdown-it token.
* @returns {void}
*/
function freezeToken(token) {
if (token.attrs) {
for (const attr of token.attrs) {
Object.freeze(attr);
}
Object.freeze(token.attrs);
}
if (token.children) {
for (const child of token.children) {
freezeToken(child);
}
Object.freeze(token.children);
}
if (token.map) {
Object.freeze(token.map);
}
Object.freeze(token);
}
/**
* Annotate tokens with line/lineNumber and freeze them.
*
* @param {import("markdown-it").Token[]} tokens Array of markdown-it tokens.
* @param {string[]} lines Lines of Markdown content.
* @returns {void}
*/
function annotateAndFreezeTokens(tokens, lines) {
let trMap = null;
/** @type {MarkdownItToken[]} */
// @ts-ignore
const markdownItTokens = tokens;
for (const token of markdownItTokens) {
// Provide missing maps for table content
if (token.type === "tr_open") {
trMap = token.map;
} else if (token.type === "tr_close") {
trMap = null;
}
if (!token.map && trMap) {
token.map = [ ...trMap ];
}
// Update token metadata
if (token.map) {
token.line = lines[token.map[0]];
token.lineNumber = token.map[0] + 1;
// Trim bottom of token to exclude whitespace lines
while (token.map[1] && !((lines[token.map[1] - 1] || "").trim())) {
token.map[1]--;
}
}
// Annotate children with lineNumber
if (token.children) {
/** @type {number[]} */
const codeSpanExtraLines = [];
if (token.children.some((child) => child.type === "code_inline")) {
forEachInlineCodeSpan(token.content, (code) => {
codeSpanExtraLines.push(code.split(newLineRe).length - 1);
});
}
let lineNumber = token.lineNumber;
for (const child of token.children) {
child.lineNumber = lineNumber;
child.line = lines[lineNumber - 1];
if ((child.type === "softbreak") || (child.type === "hardbreak")) {
lineNumber++;
} else if (child.type === "code_inline") {
lineNumber += codeSpanExtraLines.shift() || 0;
}
}
}
freezeToken(token);
}
Object.freeze(tokens);
}
/**
* Gets an array of markdown-it tokens for the input.
*
* @param {MarkdownIt} markdownIt Instance of the markdown-it parser.
* @param {string} content Markdown content.
* @param {string[]} lines Lines of Markdown content.
* @returns {MarkdownItToken[]} Array of markdown-it tokens.
*/
function getMarkdownItTokens(markdownIt, content, lines) {
const tokens = markdownIt.parse(content, {});
annotateAndFreezeTokens(tokens, lines);
return tokens;
};
module.exports = {
forEachInlineCodeSpan,
getMarkdownItTokens
};
+600
View File
@@ -0,0 +1,600 @@
/**
* Lint specified Markdown files.
*
* @param {Options | null} options Configuration options.
* @param {LintCallback} callback Callback (err, result) function.
* @returns {void}
*/
export function lintAsync(options: Options | null, callback: LintCallback): void;
/**
* Lint specified Markdown files.
*
* @param {Options | null} options Configuration options.
* @returns {Promise<LintResults>} Results object.
*/
export function lintPromise(options: Options | null): Promise<LintResults>;
/**
* Lint specified Markdown files.
*
* @param {Options | null} options Configuration options.
* @returns {LintResults} Results object.
*/
export function lintSync(options: Options | null): LintResults;
/**
* Extend specified configuration object.
*
* @param {Configuration} config Configuration object.
* @param {string} file Configuration file name.
* @param {ConfigurationParser[] | undefined} parsers Parsing function(s).
* @param {FsLike} fs File system implementation.
* @returns {Promise<Configuration>} Configuration object.
*/
export function extendConfigPromise(config: Configuration, file: string, parsers: ConfigurationParser[] | undefined, fs: FsLike): Promise<Configuration>;
/**
* Read specified configuration file.
*
* @param {string} file Configuration file name.
* @param {ConfigurationParser[] | ReadConfigCallback} [parsers] Parsing function(s).
* @param {FsLike | ReadConfigCallback} [fs] File system implementation.
* @param {ReadConfigCallback} [callback] Callback (err, result) function.
* @returns {void}
*/
export function readConfigAsync(file: string, parsers?: ConfigurationParser[] | ReadConfigCallback, fs?: FsLike | ReadConfigCallback, callback?: ReadConfigCallback): void;
/**
* Read specified configuration file.
*
* @param {string} file Configuration file name.
* @param {ConfigurationParser[]} [parsers] Parsing function(s).
* @param {FsLike} [fs] File system implementation.
* @returns {Promise<Configuration>} Configuration object.
*/
export function readConfigPromise(file: string, parsers?: ConfigurationParser[], fs?: FsLike): Promise<Configuration>;
/**
* Read specified configuration file.
*
* @param {string} file Configuration file name.
* @param {ConfigurationParser[]} [parsers] Parsing function(s).
* @param {FsLike} [fs] File system implementation.
* @returns {Configuration} Configuration object.
*/
export function readConfigSync(file: string, parsers?: ConfigurationParser[], fs?: FsLike): Configuration;
/**
* Applies the specified fix to a Markdown content line.
*
* @param {string} line Line of Markdown content.
* @param {FixInfo} fixInfo FixInfo instance.
* @param {string} [lineEnding] Line ending to use.
* @returns {string | null} Fixed content or null if deleted.
*/
export function applyFix(line: string, fixInfo: FixInfo, lineEnding?: string): string | null;
/**
* Applies as many of the specified fixes as possible to Markdown content.
*
* @param {string} input Lines of Markdown content.
* @param {LintError[]} errors LintError instances.
* @returns {string} Fixed content.
*/
export function applyFixes(input: string, errors: LintError[]): string;
/**
* Gets the (semantic) version of the library.
*
* @returns {string} SemVer string.
*/
export function getVersion(): string;
/**
* Result object for removeFrontMatter.
*/
export type RemoveFrontMatterResult = {
/**
* Markdown content.
*/
content: string;
/**
* Front matter lines.
*/
frontMatterLines: string[];
};
/**
* Result object for getEffectiveConfig.
*/
export type GetEffectiveConfigResult = {
/**
* Effective configuration.
*/
effectiveConfig: Configuration;
/**
* Rules enabled.
*/
rulesEnabled: Map<string, boolean>;
/**
* Rules severity.
*/
rulesSeverity: Map<string, "error" | "warning">;
};
/**
* Result object for getEnabledRulesPerLineNumber.
*/
export type EnabledRulesPerLineNumberResult = {
/**
* Effective configuration.
*/
effectiveConfig: Configuration;
/**
* Enabled rules per line number.
*/
enabledRulesPerLineNumber: Map<string, boolean>[];
/**
* Enabled rule list.
*/
enabledRuleList: Rule[];
/**
* Rules severity.
*/
rulesSeverity: Map<string, "error" | "warning">;
};
/**
* Node fs instance (or compatible object).
*/
export type FsLike = {
/**
* access method.
*/
access: (path: string, callback: (err: Error) => void) => void;
/**
* accessSync method.
*/
accessSync: (path: string) => void;
/**
* readFile method.
*/
readFile: (path: string, encoding: string, callback: (err: Error, data: string) => void) => void;
/**
* readFileSync method.
*/
readFileSync: (path: string, encoding: string) => string;
};
/**
* Function to implement rule logic.
*/
export type RuleFunction = (params: RuleParams, onError: RuleOnError) => void;
/**
* Rule parameters.
*/
export type RuleParams = {
/**
* File/string name.
*/
name: string;
/**
* Markdown parser data.
*/
parsers: MarkdownParsers;
/**
* File/string lines.
*/
lines: readonly string[];
/**
* Front matter lines.
*/
frontMatterLines: readonly string[];
/**
* Rule configuration.
*/
config: RuleConfiguration;
/**
* Version of the markdownlint library.
*/
version: string;
};
/**
* Markdown parser data.
*/
export type MarkdownParsers = {
/**
* Markdown parser data from markdown-it (only present when Rule.parser is "markdownit").
*/
markdownit: ParserMarkdownIt;
/**
* Markdown parser data from micromark (only present when Rule.parser is "micromark").
*/
micromark: ParserMicromark;
};
/**
* Markdown parser data from markdown-it.
*/
export type ParserMarkdownIt = {
/**
* Token objects from markdown-it.
*/
tokens: MarkdownItToken[];
};
/**
* Markdown parser data from micromark.
*/
export type ParserMicromark = {
/**
* Token objects from micromark.
*/
tokens: MicromarkToken[];
};
/**
* markdown-it token.
*/
export type MarkdownItToken = {
/**
* HTML attributes.
*/
attrs: string[][];
/**
* Block-level token.
*/
block: boolean;
/**
* Child nodes.
*/
children: MarkdownItToken[];
/**
* Tag contents.
*/
content: string;
/**
* Ignore element.
*/
hidden: boolean;
/**
* Fence info.
*/
info: string;
/**
* Nesting level.
*/
level: number;
/**
* Beginning/ending line numbers.
*/
map: number[];
/**
* Markup text.
*/
markup: string;
/**
* Arbitrary data.
*/
meta: any;
/**
* Level change.
*/
nesting: number;
/**
* HTML tag name.
*/
tag: string;
/**
* Token type.
*/
type: string;
/**
* Line number (1-based).
*/
lineNumber: number;
/**
* Line content.
*/
line: string;
};
export type MicromarkTokenType = import("micromark-util-types").TokenType;
/**
* micromark token.
*/
export type MicromarkToken = {
/**
* Token type.
*/
type: MicromarkTokenType;
/**
* Start line (1-based).
*/
startLine: number;
/**
* Start column (1-based).
*/
startColumn: number;
/**
* End line (1-based).
*/
endLine: number;
/**
* End column (1-based).
*/
endColumn: number;
/**
* Token text.
*/
text: string;
/**
* Child tokens.
*/
children: MicromarkToken[];
/**
* Parent token.
*/
parent: MicromarkToken | null;
};
/**
* Error-reporting callback.
*/
export type RuleOnError = (onErrorInfo: RuleOnErrorInfo) => void;
/**
* Fix information for RuleOnError callback.
*/
export type RuleOnErrorInfo = {
/**
* Line number (1-based).
*/
lineNumber: number;
/**
* Detail about the error.
*/
detail?: string;
/**
* Context for the error.
*/
context?: string;
/**
* Link to more information.
*/
information?: URL;
/**
* Column number (1-based) and length.
*/
range?: number[];
/**
* Fix information.
*/
fixInfo?: RuleOnErrorFixInfo;
};
/**
* Fix information for RuleOnErrorInfo.
*/
export type RuleOnErrorFixInfo = {
/**
* Line number (1-based).
*/
lineNumber?: number;
/**
* Column of the fix (1-based).
*/
editColumn?: number;
/**
* Count of characters to delete.
*/
deleteCount?: number;
/**
* Text to insert (after deleting).
*/
insertText?: string;
};
/**
* Rule definition.
*/
export type Rule = {
/**
* Rule name(s).
*/
names: string[];
/**
* Rule description.
*/
description: string;
/**
* Link to more information.
*/
information?: URL;
/**
* Rule tag(s).
*/
tags: string[];
/**
* Parser used.
*/
parser: "markdownit" | "micromark" | "none";
/**
* True if asynchronous.
*/
asynchronous?: boolean;
/**
* Rule implementation.
*/
function: RuleFunction;
};
/**
* Method used by the markdown-it parser to parse input.
*/
export type MarkdownItParse = (src: string, env: any) => any[];
/**
* Instance of the markdown-it parser.
*/
export type MarkdownIt = {
/**
* Method to parse input.
*/
parse: MarkdownItParse;
};
/**
* Gets an instance of the markdown-it parser. Any plugins should already have been loaded.
*/
export type MarkdownItFactory = () => MarkdownIt | Promise<MarkdownIt>;
/**
* Configuration options.
*/
export type Options = {
/**
* Configuration object.
*/
config?: Configuration;
/**
* Configuration parsers.
*/
configParsers?: ConfigurationParser[];
/**
* Custom rules.
*/
customRules?: Rule[] | Rule;
/**
* Files to lint.
*/
files?: string[] | string;
/**
* Front matter pattern.
*/
frontMatter?: RegExp | null;
/**
* File system implementation.
*/
fs?: FsLike;
/**
* True to catch exceptions.
*/
handleRuleFailures?: boolean;
/**
* Function to create a markdown-it parser.
*/
markdownItFactory?: MarkdownItFactory;
/**
* True to ignore HTML directives.
*/
noInlineConfig?: boolean;
/**
* Strings to lint.
*/
strings?: {
[x: string]: string;
};
};
/**
* A markdown-it plugin.
*/
export type Plugin = any[];
/**
* Lint results.
*/
export type LintResults = {
[x: string]: LintError[];
};
/**
* Lint error.
*/
export type LintError = {
/**
* Line number (1-based).
*/
lineNumber: number;
/**
* Rule name(s).
*/
ruleNames: string[];
/**
* Rule description.
*/
ruleDescription: string;
/**
* Link to more information.
*/
ruleInformation: string | null;
/**
* Detail about the error.
*/
errorDetail: string | null;
/**
* Context for the error.
*/
errorContext: string | null;
/**
* Column number (1-based) and length.
*/
errorRange: number[] | null;
/**
* Fix information.
*/
fixInfo: FixInfo | null;
/**
* Severity of the error.
*/
severity: "error" | "warning";
};
/**
* Fix information.
*/
export type FixInfo = {
/**
* Line number (1-based).
*/
lineNumber?: number;
/**
* Column of the fix (1-based).
*/
editColumn?: number;
/**
* Count of characters to delete.
*/
deleteCount?: number;
/**
* Text to insert (after deleting).
*/
insertText?: string;
};
/**
* FixInfo with all optional properties present.
*/
export type FixInfoNormalized = {
/**
* Line number (1-based).
*/
lineNumber: number;
/**
* Column of the fix (1-based).
*/
editColumn: number;
/**
* Count of characters to delete.
*/
deleteCount: number;
/**
* Text to insert (after deleting).
*/
insertText: string;
};
/**
* Called with the result of linting a string or document.
*/
export type LintContentCallback = (error: Error | null, result?: LintError[]) => void;
/**
* Called with the result of the lint function.
*/
export type LintCallback = (error: Error | null, results?: LintResults) => void;
/**
* Configuration object for linting rules. For the JSON schema, see
* {@link ../schema/markdownlint-config-schema.json}.
*/
export type Configuration = import("./configuration.d.ts").Configuration;
/**
* Configuration object for linting rules strictly. For the JSON schema, see
* {@link ../schema/markdownlint-config-schema-strict.json}.
*/
export type ConfigurationStrict = import("./configuration-strict.d.ts").ConfigurationStrict;
/**
* Rule configuration.
*/
export type RuleConfiguration = boolean | any;
/**
* Parses a configuration string and returns a configuration object.
*/
export type ConfigurationParser = (text: string) => Configuration;
/**
* Called with the result of the readConfig function.
*/
export type ReadConfigCallback = (err: Error | null, config?: Configuration) => void;
/**
* Called with the result of the resolveConfigExtends function.
*/
export type ResolveConfigExtendsCallback = (err: Error | null, path?: string) => void;
+1660
View File
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
// @ts-check
import { addErrorDetailIf, frontMatterHasTitle } from "../helpers/helpers.cjs";
import { getHeadingLevel } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD001", "heading-increment" ],
"description": "Heading levels should only increment by one level at a time",
"tags": [ "headings" ],
"parser": "micromark",
"function": function MD001(params, onError) {
const hasTitle = frontMatterHasTitle(
params.frontMatterLines,
params.config.front_matter_title
);
let prevLevel = hasTitle ? 1 : Number.MAX_SAFE_INTEGER;
for (const heading of filterByTypesCached([ "atxHeading", "setextHeading" ])) {
const level = getHeadingLevel(heading);
if (level > prevLevel) {
addErrorDetailIf(
onError,
heading.startLine,
`h${prevLevel + 1}`,
`h${level}`
);
}
prevLevel = level;
}
}
};
+47
View File
@@ -0,0 +1,47 @@
// @ts-check
import { addErrorDetailIf } from "../helpers/helpers.cjs";
import { getHeadingLevel, getHeadingStyle } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD003", "heading-style" ],
"description": "Heading style",
"tags": [ "headings" ],
"parser": "micromark",
"function": function MD003(params, onError) {
let style = String(params.config.style || "consistent");
for (const heading of filterByTypesCached([ "atxHeading", "setextHeading" ])) {
const styleForToken = getHeadingStyle(heading);
if (style === "consistent") {
style = styleForToken;
}
if (styleForToken !== style) {
const h12 = getHeadingLevel(heading) <= 2;
const setextWithAtx =
(style === "setext_with_atx") &&
((h12 && (styleForToken === "setext")) ||
(!h12 && (styleForToken === "atx")));
const setextWithAtxClosed =
(style === "setext_with_atx_closed") &&
((h12 && (styleForToken === "setext")) ||
(!h12 && (styleForToken === "atx_closed")));
if (!setextWithAtx && !setextWithAtxClosed) {
let expected = style;
if (style === "setext_with_atx") {
expected = h12 ? "setext" : "atx";
} else if (style === "setext_with_atx_closed") {
expected = h12 ? "setext" : "atx_closed";
}
addErrorDetailIf(
onError,
heading.startLine,
expected,
styleForToken
);
}
}
}
}
};
+72
View File
@@ -0,0 +1,72 @@
// @ts-check
import { addErrorDetailIf } from "../helpers/helpers.cjs";
import { getDescendantsByType, getParentOfType } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
const markerToStyle = (/** @type {string} */ marker) => (marker === "-") ? "dash" : ((marker === "+") ? "plus" : "asterisk");
const styleToMarker = (/** @type {string} */ style) => (style === "dash") ? "-" : ((style === "plus") ? "+" : "*");
const differentItemStyle = (/** @type {string} */ style) => (style === "dash") ? "plus" : ((style === "plus") ? "asterisk" : "dash");
const validStyles = new Set([
"asterisk",
"consistent",
"dash",
"plus",
"sublist"
]);
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD004", "ul-style" ],
"description": "Unordered list style",
"tags": [ "bullet", "ul" ],
"parser": "micromark",
"function": function MD004(params, onError) {
const style = String(params.config.style || "consistent");
let expectedStyle = validStyles.has(style) ? style : "dash";
/** @type {("asterisk"|"dash"|"plus")[]} */
const nestingStyles = [];
for (const listUnordered of filterByTypesCached([ "listUnordered" ])) {
let nesting = 0;
if (style === "sublist") {
/** @type {import("markdownlint").MicromarkToken | null} */
let parent = listUnordered;
// @ts-ignore
while ((parent = getParentOfType(parent, [ "listOrdered", "listUnordered" ]))) {
nesting++;
}
}
const listItemMarkers = getDescendantsByType(listUnordered, [ "listItemPrefix", "listItemMarker" ]);
for (const listItemMarker of listItemMarkers) {
const itemStyle = markerToStyle(listItemMarker.text);
if (style === "sublist") {
if (!nestingStyles[nesting]) {
nestingStyles[nesting] =
(itemStyle === nestingStyles[nesting - 1]) ?
differentItemStyle(itemStyle) :
itemStyle;
}
expectedStyle = nestingStyles[nesting];
} else if (expectedStyle === "consistent") {
expectedStyle = itemStyle;
}
const column = listItemMarker.startColumn;
const length = listItemMarker.endColumn - listItemMarker.startColumn;
addErrorDetailIf(
onError,
listItemMarker.startLine,
expectedStyle,
itemStyle,
undefined,
undefined,
[ column, length ],
{
"editColumn": column,
"deleteCount": length,
"insertText": styleToMarker(expectedStyle)
}
);
}
}
}
};
+69
View File
@@ -0,0 +1,69 @@
// @ts-check
import { addError, addErrorDetailIf } from "../helpers/helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD005", "list-indent" ],
"description": "Inconsistent indentation for list items at the same level",
"tags": [ "bullet", "ul", "indentation" ],
"parser": "micromark",
"function": function MD005(params, onError) {
for (const list of filterByTypesCached([ "listOrdered", "listUnordered" ])) {
const expectedIndent = list.startColumn - 1;
let expectedEnd = 0;
let endMatching = false;
const listItemPrefixes =
list.children.filter((token) => (token.type === "listItemPrefix"));
for (const listItemPrefix of listItemPrefixes) {
const lineNumber = listItemPrefix.startLine;
const actualIndent = listItemPrefix.startColumn - 1;
const range = [ 1, listItemPrefix.endColumn - 1 ];
if (list.type === "listUnordered") {
addErrorDetailIf(
onError,
lineNumber,
expectedIndent,
actualIndent,
undefined,
undefined,
range
// No fixInfo; MD007 handles this scenario better
);
} else {
const markerLength = listItemPrefix.text.trim().length;
const actualEnd = listItemPrefix.startColumn + markerLength - 1;
expectedEnd = expectedEnd || actualEnd;
if ((expectedIndent !== actualIndent) || endMatching) {
if (expectedEnd === actualEnd) {
endMatching = true;
} else {
const detail = endMatching ?
`Expected: (${expectedEnd}); Actual: (${actualEnd})` :
`Expected: ${expectedIndent}; Actual: ${actualIndent}`;
const expected = endMatching ?
expectedEnd - markerLength :
expectedIndent;
const actual = endMatching ?
actualEnd - markerLength :
actualIndent;
addError(
onError,
lineNumber,
detail,
undefined,
range,
{
"editColumn": Math.min(actual, expected) + 1,
"deleteCount": Math.max(actual - expected, 0),
"insertText": "".padEnd(Math.max(expected - actual, 0))
}
);
}
}
}
}
}
}
};
+84
View File
@@ -0,0 +1,84 @@
// @ts-check
import { addErrorDetailIf } from "../helpers/helpers.cjs";
import { getParentOfType } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("micromark-util-types").TokenType[]} */
const unorderedListTypes =
[ "blockQuotePrefix", "listItemPrefix", "listUnordered" ];
/** @type {import("micromark-util-types").TokenType[]} */
const unorderedParentTypes =
[ "blockQuote", "listOrdered", "listUnordered" ];
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD007", "ul-indent" ],
"description": "Unordered list indentation",
"tags": [ "bullet", "ul", "indentation" ],
"parser": "micromark",
"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);
const unorderedListNesting = new Map();
let lastBlockQuotePrefix = null;
const tokens = filterByTypesCached(unorderedListTypes);
for (const token of tokens) {
const { endColumn, parent, startColumn, startLine, type } = token;
if (type === "blockQuotePrefix") {
lastBlockQuotePrefix = token;
} else if (type === "listUnordered") {
let nesting = 0;
/** @type {import("markdownlint").MicromarkToken | null} */
let current = token;
while (
// @ts-ignore
(current = getParentOfType(current, unorderedParentTypes))
) {
if (current.type === "listUnordered") {
nesting++;
// eslint-disable-next-line no-continue
continue;
} else if (current.type === "listOrdered") {
nesting = -1;
}
break;
}
if (nesting >= 0) {
unorderedListNesting.set(token, nesting);
}
} else {
// listItemPrefix
const nesting = unorderedListNesting.get(parent);
if (nesting !== undefined) {
// listItemPrefix for listUnordered
const baseIndent = (getParentOfType(token, [ "gfmFootnoteDefinition" ])) ? 4 : 0;
const expectedIndent =
baseIndent + (startIndented ? startIndent : 0) + (nesting * indent);
const blockQuoteAdjustment =
(lastBlockQuotePrefix?.endLine === startLine) ?
(lastBlockQuotePrefix.endColumn - 1) :
0;
const actualIndent = startColumn - 1 - blockQuoteAdjustment;
const range = [ 1, endColumn - 1 ];
const fixInfo = {
"editColumn": startColumn - actualIndent,
"deleteCount": Math.max(actualIndent - expectedIndent, 0),
"insertText": "".padEnd(Math.max(expectedIndent - actualIndent, 0))
};
addErrorDetailIf(
onError,
startLine,
expectedIndent,
actualIndent,
undefined,
undefined,
range,
fixInfo
);
}
}
}
}
};
+96
View File
@@ -0,0 +1,96 @@
// @ts-check
import { addError } from "../helpers/helpers.cjs";
import { addRangeToSet } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD009", "no-trailing-spaces" ],
"description": "Trailing spaces",
"tags": [ "whitespace" ],
"parser": "micromark",
"function": function MD009(params, onError) {
let brSpaces = params.config.br_spaces;
brSpaces = Number((brSpaces === undefined) ? 2 : brSpaces);
const codeBlocks = params.config.code_blocks;
const includeCode = (codeBlocks === undefined) ? false : !!codeBlocks;
const listItemEmptyLines = !!params.config.list_item_empty_lines;
const strict = !!params.config.strict;
const codeBlockLineNumbers = new Set();
if (!includeCode) {
for (const codeBlock of filterByTypesCached([ "codeFenced" ])) {
addRangeToSet(codeBlockLineNumbers, codeBlock.startLine + 1, codeBlock.endLine - 1);
}
for (const codeBlock of filterByTypesCached([ "codeIndented" ])) {
addRangeToSet(codeBlockLineNumbers, codeBlock.startLine, codeBlock.endLine);
}
}
const listItemLineNumbers = new Set();
if (listItemEmptyLines) {
for (const listBlock of filterByTypesCached([ "listOrdered", "listUnordered" ])) {
addRangeToSet(listItemLineNumbers, listBlock.startLine, listBlock.endLine);
let trailingIndent = true;
for (let i = listBlock.children.length - 1; i >= 0; i--) {
const child = listBlock.children[i];
switch (child.type) {
case "content":
trailingIndent = false;
break;
case "listItemIndent":
if (trailingIndent) {
listItemLineNumbers.delete(child.startLine);
}
break;
case "listItemPrefix":
trailingIndent = true;
break;
default:
break;
}
}
}
}
const paragraphLineNumbers = new Set();
const codeInlineLineNumbers = new Set();
if (strict) {
for (const paragraph of filterByTypesCached([ "paragraph" ])) {
addRangeToSet(paragraphLineNumbers, paragraph.startLine, paragraph.endLine - 1);
}
for (const codeText of filterByTypesCached([ "codeText" ])) {
addRangeToSet(codeInlineLineNumbers, codeText.startLine, codeText.endLine - 1);
}
}
const expected = (brSpaces < 2) ? 0 : brSpaces;
for (let lineIndex = 0; lineIndex < params.lines.length; lineIndex++) {
const line = params.lines[lineIndex];
const lineNumber = lineIndex + 1;
const trailingSpaces = line.length - line.trimEnd().length;
if (
trailingSpaces &&
!codeBlockLineNumbers.has(lineNumber) &&
!listItemLineNumbers.has(lineNumber) &&
(
(expected !== trailingSpaces) ||
(strict &&
(!paragraphLineNumbers.has(lineNumber) ||
codeInlineLineNumbers.has(lineNumber)))
)
) {
const column = line.length - trailingSpaces + 1;
addError(
onError,
lineNumber,
"Expected: " + (expected === 0 ? "" : "0 or ") +
expected + "; Actual: " + trailingSpaces,
undefined,
[ column, trailingSpaces ],
{
"editColumn": column,
"deleteCount": trailingSpaces
}
);
}
}
}
};
+78
View File
@@ -0,0 +1,78 @@
// @ts-check
import { addError, hasOverlap } from "../helpers/helpers.cjs";
import { getDescendantsByType } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
const tabRe = /\t+/g;
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD010", "no-hard-tabs" ],
"description": "Hard tabs",
"tags": [ "whitespace", "hard_tab" ],
"parser": "micromark",
"function": function MD010(params, onError) {
const codeBlocks = params.config.code_blocks;
const includeCode = (codeBlocks === undefined) ? true : !!codeBlocks;
const ignoreCodeLanguages = new Set(
(params.config.ignore_code_languages || [])
.map((/** @type {void} */ language) => String(language).toLowerCase())
);
const spacesPerTab = params.config.spaces_per_tab;
const spaceMultiplier = (spacesPerTab === undefined) ?
1 :
Math.max(0, Number(spacesPerTab));
/** @type {import("markdownlint").MicromarkTokenType[]} */
const exclusionTypes = [];
if (includeCode) {
if (ignoreCodeLanguages.size > 0) {
exclusionTypes.push("codeFenced");
}
} else {
exclusionTypes.push("codeFenced", "codeIndented", "codeText");
}
const codeTokens = filterByTypesCached(exclusionTypes).filter((token) => {
if ((token.type === "codeFenced") && (ignoreCodeLanguages.size > 0)) {
const fenceInfos = getDescendantsByType(token, [ "codeFencedFence", "codeFencedFenceInfo" ]);
return fenceInfos.every((fenceInfo) => ignoreCodeLanguages.has(fenceInfo.text.toLowerCase()));
}
return true;
});
const codeRanges = codeTokens.map((token) => {
const { type, startLine, startColumn, endLine, endColumn } = token;
const codeFenced = (type === "codeFenced");
return {
"startLine": startLine + (codeFenced ? 1 : 0),
"startColumn": codeFenced ? 0 : startColumn,
"endLine": endLine - (codeFenced ? 1 : 0),
"endColumn": codeFenced ? Number.MAX_SAFE_INTEGER : endColumn
};
});
for (let lineIndex = 0; lineIndex < params.lines.length; lineIndex++) {
const line = params.lines[lineIndex];
let match = null;
while ((match = tabRe.exec(line)) !== null) {
const lineNumber = lineIndex + 1;
const column = match.index + 1;
const length = match[0].length;
/** @type {import("../helpers/helpers.cjs").FileRange} */
const range = { "startLine": lineNumber, "startColumn": column, "endLine": lineNumber, "endColumn": column + length - 1 };
if (!codeRanges.some((codeRange) => hasOverlap(codeRange, range))) {
addError(
onError,
lineNumber,
"Column: " + column,
undefined,
[ column, length ],
{
"editColumn": column,
"deleteCount": length,
"insertText": "".padEnd(length * spaceMultiplier)
}
);
}
}
}
}
};
+56
View File
@@ -0,0 +1,56 @@
// @ts-check
import { addError, hasOverlap } from "../helpers/helpers.cjs";
import { addRangeToSet } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @typedef {import("micromark-extension-math")} */
const reversedLinkRe = /(^|[^\\])\(([^()]+)\)\[([^\]^][^\]]*)\](?!\()/g;
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD011", "no-reversed-links" ],
"description": "Reversed link syntax",
"tags": [ "links" ],
"parser": "micromark",
"function": function MD011(params, onError) {
const ignoreBlockLineNumbers = new Set();
for (const ignoreBlock of filterByTypesCached([ "codeFenced", "codeIndented", "mathFlow" ])) {
addRangeToSet(ignoreBlockLineNumbers, ignoreBlock.startLine, ignoreBlock.endLine);
}
const ignoreTexts = filterByTypesCached([ "codeText", "mathText" ]);
for (const [ lineIndex, line ] of params.lines.entries()) {
const lineNumber = lineIndex + 1;
if (!ignoreBlockLineNumbers.has(lineNumber)) {
let match = null;
while ((match = reversedLinkRe.exec(line)) !== null) {
const [ reversedLink, preChar, linkText, linkDestination ] = match;
if (
!linkText.endsWith("\\") &&
!linkDestination.endsWith("\\")
) {
const column = match.index + preChar.length + 1;
const length = match[0].length - preChar.length;
/** @type {import("../helpers/helpers.cjs").FileRange} */
const range = { "startLine": lineNumber, "startColumn": column, "endLine": lineNumber, "endColumn": column + length - 1 };
if (!ignoreTexts.some((ignoreText) => hasOverlap(ignoreText, range))) {
addError(
onError,
lineNumber,
reversedLink.slice(preChar.length),
undefined,
[ column, length ],
{
"editColumn": column,
"deleteCount": length,
"insertText": `[${linkText}](${linkDestination})`
}
);
}
}
}
}
}
}
};
+40
View File
@@ -0,0 +1,40 @@
// @ts-check
import { addErrorDetailIf } from "../helpers/helpers.cjs";
import { addRangeToSet } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD012", "no-multiple-blanks" ],
"description": "Multiple consecutive blank lines",
"tags": [ "whitespace", "blank_lines" ],
"parser": "micromark",
"function": function MD012(params, onError) {
const maximum = Number(params.config.maximum || 1);
const { lines } = params;
const codeBlockLineNumbers = new Set();
for (const codeBlock of filterByTypesCached([ "codeFenced", "codeIndented" ])) {
addRangeToSet(codeBlockLineNumbers, codeBlock.startLine, codeBlock.endLine);
}
let count = 0;
for (const [ lineIndex, line ] of lines.entries()) {
const inCode = codeBlockLineNumbers.has(lineIndex + 1);
count = (inCode || (line.trim().length > 0)) ? 0 : count + 1;
if (maximum < count) {
addErrorDetailIf(
onError,
lineIndex + 1,
maximum,
count,
undefined,
undefined,
undefined,
{
"deleteCount": -1
}
);
}
}
}
};
+91
View File
@@ -0,0 +1,91 @@
// @ts-check
import { addErrorDetailIf } from "../helpers/helpers.cjs";
import { filterByTypesCached, getReferenceLinkImageData } from "./cache.mjs";
import { addRangeToSet, getDescendantsByType } from "../helpers/micromark-helpers.cjs";
// Regular expression for a line that is not wrappable
const notWrappableRe = /^(?:[#>\s]*\s)?\S*$/;
/** @typedef {import("micromark-extension-gfm-autolink-literal")} */
/** @typedef {import("micromark-extension-gfm-table")} */
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD013", "line-length" ],
"description": "Line length",
"tags": [ "line_length" ],
"parser": "micromark",
"function": function MD013(params, onError) {
const lineLength = Number(params.config.line_length || 80);
const headingLineLength = Number(params.config.heading_line_length || lineLength);
const codeLineLength = Number(params.config.code_block_line_length || lineLength);
const strict = !!params.config.strict;
const stern = !!params.config.stern;
const codeBlocks = params.config.code_blocks;
const includeCodeBlocks = (codeBlocks === undefined) ? true : !!codeBlocks;
const tables = params.config.tables;
const includeTables = (tables === undefined) ? true : !!tables;
const headings = params.config.headings;
const includeHeadings = (headings === undefined) ? true : !!headings;
const headingLineNumbers = new Set();
for (const heading of filterByTypesCached([ "atxHeading", "setextHeading" ])) {
addRangeToSet(headingLineNumbers, heading.startLine, heading.endLine);
}
const codeBlockLineNumbers = new Set();
for (const codeBlock of filterByTypesCached([ "codeFenced", "codeIndented" ])) {
addRangeToSet(codeBlockLineNumbers, codeBlock.startLine, codeBlock.endLine);
}
const tableLineNumbers = new Set();
for (const table of filterByTypesCached([ "table" ])) {
addRangeToSet(tableLineNumbers, table.startLine, table.endLine);
}
const linkLineNumbers = new Set();
for (const link of filterByTypesCached([ "autolink", "image", "link", "literalAutolink" ])) {
addRangeToSet(linkLineNumbers, link.startLine, link.endLine);
}
const paragraphDataLineNumbers = new Set();
for (const paragraph of filterByTypesCached([ "paragraph" ])) {
for (const data of getDescendantsByType(paragraph, [ "data" ])) {
addRangeToSet(paragraphDataLineNumbers, data.startLine, data.endLine);
}
}
const linkOnlyLineNumbers = new Set();
for (const lineNumber of linkLineNumbers) {
if (!paragraphDataLineNumbers.has(lineNumber)) {
linkOnlyLineNumbers.add(lineNumber);
}
}
const definitionLineIndices = new Set(getReferenceLinkImageData().definitionLineIndices);
for (let lineIndex = 0; lineIndex < params.lines.length; lineIndex++) {
const line = params.lines[lineIndex];
const lineNumber = lineIndex + 1;
const isHeading = headingLineNumbers.has(lineNumber);
const inCode = codeBlockLineNumbers.has(lineNumber);
const inTable = tableLineNumbers.has(lineNumber);
const maxLength = inCode ? codeLineLength : (isHeading ? headingLineLength : lineLength);
// If not strict/stern, the last run of non-whitespace is allowed to go
// beyond the limit as long as it begins within the limit
const text = (strict || stern) ? line : line.replace(/\S*$/u, "#");
if ((maxLength > 0) &&
(includeCodeBlocks || !inCode) &&
(includeTables || !inTable) &&
(includeHeadings || !isHeading) &&
!definitionLineIndices.has(lineIndex) &&
(strict ||
(!(stern && notWrappableRe.test(line)) &&
!linkOnlyLineNumbers.has(lineNumber))) &&
(text.length > maxLength)) {
addErrorDetailIf(
onError,
lineNumber,
maxLength,
line.length,
undefined,
undefined,
[ maxLength + 1, line.length - maxLength ]
);
}
}
}
};
+47
View File
@@ -0,0 +1,47 @@
// @ts-check
import { addErrorContext } from "../helpers/helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
const dollarCommandRe = /^(\s*)(\$\s+)/;
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD014", "commands-show-output" ],
"description": "Dollar signs used before commands without showing output",
"tags": [ "code" ],
"parser": "micromark",
"function": function MD014(params, onError) {
for (const codeBlock of filterByTypesCached([ "codeFenced", "codeIndented" ])) {
const codeFlowValues = codeBlock.children.filter((child) => child.type === "codeFlowValue");
const dollarMatches = codeFlowValues
.map((codeFlowValue) => ({
"result": codeFlowValue.text.match(dollarCommandRe),
"startColumn": codeFlowValue.startColumn,
"startLine": codeFlowValue.startLine,
"text": codeFlowValue.text
}))
.filter((dollarMatch) => dollarMatch.result);
if (dollarMatches.length === codeFlowValues.length) {
for (const dollarMatch of dollarMatches) {
// @ts-ignore
const column = dollarMatch.startColumn + dollarMatch.result[1].length;
// @ts-ignore
const length = dollarMatch.result[2].length;
addErrorContext(
onError,
dollarMatch.startLine,
dollarMatch.text,
undefined,
undefined,
[ column, length ],
{
"editColumn": column,
"deleteCount": length
}
);
}
}
}
}
};
+43
View File
@@ -0,0 +1,43 @@
// @ts-check
import { addErrorContext } from "../helpers/helpers.cjs";
import { addRangeToSet } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD018", "no-missing-space-atx" ],
"description": "No space after hash on atx style heading",
"tags": [ "headings", "atx", "spaces" ],
"parser": "micromark",
"function": function MD018(params, onError) {
const { lines } = params;
const ignoreBlockLineNumbers = new Set();
for (const ignoreBlock of filterByTypesCached([ "codeFenced", "codeIndented", "htmlFlow" ])) {
addRangeToSet(ignoreBlockLineNumbers, ignoreBlock.startLine, ignoreBlock.endLine);
}
for (const [ lineIndex, line ] of lines.entries()) {
if (
!ignoreBlockLineNumbers.has(lineIndex + 1) &&
/^#+[^# \t]/.test(line) &&
!/#\s*$/.test(line) &&
!line.startsWith("#️⃣")
) {
// @ts-ignore
const hashCount = /^#+/.exec(line)[0].length;
addErrorContext(
onError,
lineIndex + 1,
line.trim(),
undefined,
undefined,
[ 1, hashCount + 1 ],
{
"editColumn": hashCount + 1,
"insertText": " "
}
);
}
}
}
};
+77
View File
@@ -0,0 +1,77 @@
// @ts-check
import { addErrorContext } from "../helpers/helpers.cjs";
import { getHeadingStyle } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/**
* Validate heading sequence and whitespace length at start or end.
*
* @param {import("markdownlint").RuleOnError} onError Error-reporting callback.
* @param {import("markdownlint").MicromarkToken} heading ATX heading token.
* @param {number} delta Direction to scan.
* @returns {void}
*/
function validateHeadingSpaces(onError, heading, delta) {
const { children, startLine, text } = heading;
let index = (delta > 0) ? 0 : (children.length - 1);
while (
children[index] &&
(children[index].type !== "atxHeadingSequence")
) {
index += delta;
}
const headingSequence = children[index];
const whitespace = children[index + delta];
if (
(headingSequence?.type === "atxHeadingSequence") &&
(whitespace?.type === "whitespace") &&
(whitespace.text.length > 1)
) {
const column = whitespace.startColumn + 1;
const length = whitespace.endColumn - column;
addErrorContext(
onError,
startLine,
text.trim(),
delta > 0,
delta < 0,
[ column, length ],
{
"editColumn": column,
"deleteCount": length
}
);
}
}
/** @type {import("markdownlint").Rule[]} */
export default [
{
"names": [ "MD019", "no-multiple-space-atx" ],
"description": "Multiple spaces after hash on atx style heading",
"tags": [ "headings", "atx", "spaces" ],
"parser": "micromark",
"function": function MD019(params, onError) {
const atxHeadings = filterByTypesCached([ "atxHeading" ])
.filter((heading) => getHeadingStyle(heading) === "atx");
for (const atxHeading of atxHeadings) {
validateHeadingSpaces(onError, atxHeading, 1);
}
}
},
{
"names": [ "MD021", "no-multiple-space-closed-atx" ],
"description": "Multiple spaces inside hashes on closed atx style heading",
"tags": [ "headings", "atx_closed", "spaces" ],
"parser": "micromark",
"function": function MD021(params, onError) {
const atxClosedHeadings = filterByTypesCached([ "atxHeading" ])
.filter((heading) => getHeadingStyle(heading) === "atx_closed");
for (const atxClosedHeading of atxClosedHeadings) {
validateHeadingSpaces(onError, atxClosedHeading, 1);
validateHeadingSpaces(onError, atxClosedHeading, -1);
}
}
}
];
+68
View File
@@ -0,0 +1,68 @@
// @ts-check
import { addErrorContext } from "../helpers/helpers.cjs";
import { addRangeToSet } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD020", "no-missing-space-closed-atx" ],
"description": "No space inside hashes on closed atx style heading",
"tags": [ "headings", "atx_closed", "spaces" ],
"parser": "micromark",
"function": function MD020(params, onError) {
const { lines } = params;
const ignoreBlockLineNumbers = new Set();
for (const ignoreBlock of filterByTypesCached([ "codeFenced", "codeIndented", "htmlFlow" ])) {
addRangeToSet(ignoreBlockLineNumbers, ignoreBlock.startLine, ignoreBlock.endLine);
}
for (const [ lineIndex, line ] of lines.entries()) {
if (!ignoreBlockLineNumbers.has(lineIndex + 1)) {
const match =
/^(#+)([ \t]*)([^# \t\\]|[^# \t][^#]*?[^# \t\\])([ \t]*)((?:\\#)?)(#+)(\s*)$/.exec(line);
if (match) {
const [
,
leftHash,
{ "length": leftSpaceLength },
content,
{ "length": rightSpaceLength },
rightEscape,
rightHash,
{ "length": trailSpaceLength }
] = match;
const leftHashLength = leftHash.length;
const rightHashLength = rightHash.length;
const left = !leftSpaceLength;
const right = !rightSpaceLength || !!rightEscape;
const rightEscapeReplacement = rightEscape ? `${rightEscape} ` : "";
if (left || right) {
const range = left ?
[
1,
leftHashLength + 1
] :
[
line.length - trailSpaceLength - rightHashLength,
rightHashLength + 1
];
addErrorContext(
onError,
lineIndex + 1,
line.trim(),
left,
right,
range,
{
"editColumn": 1,
"deleteCount": line.length,
"insertText":
`${leftHash} ${content} ${rightEscapeReplacement}${rightHash}`
}
);
}
}
}
}
}
};
+100
View File
@@ -0,0 +1,100 @@
// @ts-check
import { addErrorDetailIf, isBlankLine } from "../helpers/helpers.cjs";
import { getBlockQuotePrefixText, getHeadingLevel } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */
const defaultLines = 1;
// eslint-disable-next-line jsdoc/reject-any-type
const getLinesFunction = (/** @type {any} */ linesParam) => {
if (Array.isArray(linesParam)) {
const linesArray = new Array(6).fill(defaultLines);
for (const [ index, value ] of [ ...linesParam.entries() ].slice(0, 6)) {
linesArray[index] = value;
}
return (/** @type {MicromarkToken} */ heading) => linesArray[getHeadingLevel(heading) - 1];
}
// Coerce linesParam to a number
const lines = (linesParam === undefined) ? defaultLines : Number(linesParam);
return () => lines;
};
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD022", "blanks-around-headings" ],
"description": "Headings should be surrounded by blank lines",
"tags": [ "headings", "blank_lines" ],
"parser": "micromark",
"function": function MD022(params, onError) {
const getLinesAbove = getLinesFunction(params.config.lines_above);
const getLinesBelow = getLinesFunction(params.config.lines_below);
const { lines } = params;
const blockQuotePrefixes = filterByTypesCached([ "blockQuotePrefix", "linePrefix" ]);
for (const heading of filterByTypesCached([ "atxHeading", "setextHeading" ])) {
const { startLine, endLine } = heading;
const line = lines[startLine - 1].trim();
// Check lines above
const linesAbove = getLinesAbove(heading);
if (linesAbove >= 0) {
let actualAbove = 0;
for (
let i = 0;
(i < linesAbove) && isBlankLine(lines[startLine - 2 - i]);
i++
) {
actualAbove++;
}
addErrorDetailIf(
onError,
startLine,
linesAbove,
actualAbove,
"Above",
line,
undefined,
{
"insertText": getBlockQuotePrefixText(
blockQuotePrefixes,
startLine - 1,
linesAbove - actualAbove
)
}
);
}
// Check lines below
const linesBelow = getLinesBelow(heading);
if (linesBelow >= 0) {
let actualBelow = 0;
for (
let i = 0;
(i < linesBelow) && isBlankLine(lines[endLine + i]);
i++
) {
actualBelow++;
}
addErrorDetailIf(
onError,
startLine,
linesBelow,
actualBelow,
"Below",
line,
undefined,
{
"lineNumber": endLine + 1,
"insertText": getBlockQuotePrefixText(
blockQuotePrefixes,
endLine + 1,
linesBelow - actualBelow
)
}
);
}
}
}
};
+37
View File
@@ -0,0 +1,37 @@
// @ts-check
import { addErrorContext } from "../helpers/helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD023", "heading-start-left" ],
"description": "Headings must start at the beginning of the line",
"tags": [ "headings", "spaces" ],
"parser": "micromark",
"function": function MD023(params, onError) {
const headings = filterByTypesCached([ "atxHeading", "linePrefix", "setextHeading" ]);
for (let i = 0; i < headings.length - 1; i++) {
if (
(headings[i].type === "linePrefix") &&
(headings[i + 1].type !== "linePrefix") &&
(headings[i].startLine === headings[i + 1].startLine)
) {
const { endColumn, startColumn, startLine } = headings[i];
const length = endColumn - startColumn;
addErrorContext(
onError,
startLine,
params.lines[startLine - 1],
true,
false,
[ startColumn, length ],
{
"editColumn": startColumn,
"deleteCount": length
}
);
}
}
}
};
+45
View File
@@ -0,0 +1,45 @@
// @ts-check
import { addErrorContext } from "../helpers/helpers.cjs";
import { getHeadingLevel, getHeadingText } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD024", "no-duplicate-heading" ],
"description": "Multiple headings with the same content",
"tags": [ "headings" ],
"parser": "micromark",
"function": function MD024(params, onError) {
const siblingsOnly = !!params.config.siblings_only || false;
const knownContents = [ null, [] ];
let lastLevel = 1;
let knownContent = knownContents[lastLevel];
for (const heading of filterByTypesCached([ "atxHeading", "setextHeading" ])) {
const headingText = getHeadingText(heading);
if (siblingsOnly) {
const newLevel = getHeadingLevel(heading);
while (lastLevel < newLevel) {
lastLevel++;
knownContents[lastLevel] = [];
}
while (lastLevel > newLevel) {
knownContents[lastLevel] = [];
lastLevel--;
}
knownContent = knownContents[newLevel];
}
// @ts-ignore
if (knownContent.includes(headingText)) {
addErrorContext(
onError,
heading.startLine,
headingText.trim()
);
} else {
// @ts-ignore
knownContent.push(headingText);
}
}
}
};

Some files were not shown because too many files have changed in this diff Show More