first commit
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright OpenJS Foundation and other contributors, https://openjsf.org
|
||||
|
||||
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.
|
||||
+313
@@ -0,0 +1,313 @@
|
||||
# ESLint Markdown Language Plugin
|
||||
|
||||
[](https://www.npmjs.com/package/@eslint/markdown)
|
||||
[](https://www.npmjs.com/package/@eslint/markdown)
|
||||
[](https://github.com/eslint/markdown/actions)
|
||||
|
||||
Lint Markdown with ESLint, as well JS, JSX, TypeScript, and more inside Markdown.
|
||||
|
||||
<img
|
||||
src="screenshot.png"
|
||||
height="142"
|
||||
width="432"
|
||||
alt="A JS code snippet in a Markdown editor has red squiggly underlines. A tooltip explains the problem."
|
||||
/>
|
||||
|
||||
## Usage
|
||||
|
||||
### Installing
|
||||
|
||||
Install the plugin alongside ESLint v9.15.0 or greater. Type compatibility is guaranteed with ESLint v9.39.0 or greater.
|
||||
|
||||
For Node.js and compatible runtimes:
|
||||
|
||||
```sh
|
||||
npm install @eslint/markdown -D
|
||||
# or
|
||||
yarn add @eslint/markdown -D
|
||||
# or
|
||||
pnpm install @eslint/markdown -D
|
||||
# or
|
||||
bun add @eslint/markdown -D
|
||||
```
|
||||
|
||||
For Deno:
|
||||
|
||||
```sh
|
||||
deno add jsr:@eslint/markdown
|
||||
```
|
||||
|
||||
### Configurations
|
||||
|
||||
| **Configuration Name** | **Description** |
|
||||
| ---------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `recommended` | Lints all `.md` files with the recommended rules and assumes [CommonMark](https://commonmark.org/) format. |
|
||||
| `processor` | Enables extracting code blocks from all `.md` files so code blocks can be individually linted. |
|
||||
|
||||
In your `eslint.config.js` file, import `@eslint/markdown` and include the recommended config to enable Markdown parsing and linting:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import { defineConfig } from "eslint/config";
|
||||
import markdown from "@eslint/markdown";
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
files: ["**/*.md"],
|
||||
plugins: {
|
||||
markdown,
|
||||
},
|
||||
extends: ["markdown/recommended"],
|
||||
},
|
||||
// your other configs here
|
||||
]);
|
||||
```
|
||||
|
||||
You can also modify the recommended config by using `extends`:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import { defineConfig } from "eslint/config";
|
||||
import markdown from "@eslint/markdown";
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
plugins: {
|
||||
markdown,
|
||||
},
|
||||
extends: ["markdown/recommended"],
|
||||
rules: {
|
||||
"markdown/no-html": "error",
|
||||
},
|
||||
},
|
||||
|
||||
// your other configs here
|
||||
]);
|
||||
```
|
||||
|
||||
### Rules
|
||||
|
||||
<!-- NOTE: The following table is autogenerated. Do not manually edit. -->
|
||||
|
||||
<!-- Rule Table Start -->
|
||||
|
||||
| **Rule Name** | **Description** | **Recommended** |
|
||||
| :----------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------ | :-------------: |
|
||||
| [`fenced-code-language`](./docs/rules/fenced-code-language.md) | Require languages for fenced code blocks | yes |
|
||||
| [`fenced-code-meta`](./docs/rules/fenced-code-meta.md) | Require or disallow metadata for fenced code blocks | no |
|
||||
| [`heading-increment`](./docs/rules/heading-increment.md) | Enforce heading levels increment by one | yes |
|
||||
| [`no-bare-urls`](./docs/rules/no-bare-urls.md) | Disallow bare URLs | no |
|
||||
| [`no-duplicate-definitions`](./docs/rules/no-duplicate-definitions.md) | Disallow duplicate definitions | yes |
|
||||
| [`no-duplicate-headings`](./docs/rules/no-duplicate-headings.md) | Disallow duplicate headings in the same document | no |
|
||||
| [`no-empty-definitions`](./docs/rules/no-empty-definitions.md) | Disallow empty definitions | yes |
|
||||
| [`no-empty-images`](./docs/rules/no-empty-images.md) | Disallow empty images | yes |
|
||||
| [`no-empty-links`](./docs/rules/no-empty-links.md) | Disallow empty links | yes |
|
||||
| [`no-html`](./docs/rules/no-html.md) | Disallow HTML tags | no |
|
||||
| [`no-invalid-label-refs`](./docs/rules/no-invalid-label-refs.md) | Disallow invalid label references | yes |
|
||||
| [`no-missing-atx-heading-space`](./docs/rules/no-missing-atx-heading-space.md) | Disallow headings without a space after the hash characters | yes |
|
||||
| [`no-missing-label-refs`](./docs/rules/no-missing-label-refs.md) | Disallow missing label references | yes |
|
||||
| [`no-missing-link-fragments`](./docs/rules/no-missing-link-fragments.md) | Disallow link fragments that do not reference valid headings | yes |
|
||||
| [`no-multiple-h1`](./docs/rules/no-multiple-h1.md) | Disallow multiple H1 headings in the same document | yes |
|
||||
| [`no-reference-like-urls`](./docs/rules/no-reference-like-urls.md) | Disallow URLs that match defined reference identifiers | yes |
|
||||
| [`no-reversed-media-syntax`](./docs/rules/no-reversed-media-syntax.md) | Disallow reversed link and image syntax | yes |
|
||||
| [`no-space-in-emphasis`](./docs/rules/no-space-in-emphasis.md) | Disallow spaces around emphasis markers | yes |
|
||||
| [`no-unused-definitions`](./docs/rules/no-unused-definitions.md) | Disallow unused definitions | yes |
|
||||
| [`require-alt-text`](./docs/rules/require-alt-text.md) | Require alternative text for images | yes |
|
||||
| [`table-column-count`](./docs/rules/table-column-count.md) | Disallow data rows in a GitHub Flavored Markdown table from having more cells than the header row | yes |
|
||||
|
||||
<!-- Rule Table End -->
|
||||
|
||||
**Note:** This plugin does not provide formatting rules. We recommend using a source code formatter such as [Prettier](https://prettier.io) for that purpose.
|
||||
|
||||
In order to individually configure a rule in your `eslint.config.js` file, import `@eslint/markdown` and configure each rule with a prefix:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import { defineConfig } from "eslint/config";
|
||||
import markdown from "@eslint/markdown";
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
files: ["**/*.md"],
|
||||
plugins: {
|
||||
markdown,
|
||||
},
|
||||
language: "markdown/commonmark",
|
||||
rules: {
|
||||
"markdown/no-html": "error",
|
||||
},
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
You can individually disable rules in Markdown using HTML comments, such as:
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
```markdown
|
||||
<!-- eslint-disable-next-line markdown/no-html -- I want to allow HTML here -->
|
||||
<custom-element>Hello world!</custom-element>
|
||||
|
||||
<!-- eslint-disable markdown/no-html -- here too -->
|
||||
<another-element>Goodbye world!</another-element>
|
||||
<!-- eslint-enable markdown/no-html -- safe to re-enable now -->
|
||||
|
||||
[Object] <!-- eslint-disable-line markdown/no-missing-label-refs -- not meant to be a link ref -->
|
||||
```
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
### Languages
|
||||
|
||||
| **Language Name** | **Description** |
|
||||
| ----------------- | ----------------------------------------------------------------------------- |
|
||||
| `commonmark` | Parse using [CommonMark](https://commonmark.org) Markdown format |
|
||||
| `gfm` | Parse using [GitHub-Flavored Markdown](https://github.github.com/gfm/) format |
|
||||
|
||||
In order to individually configure a language in your `eslint.config.js` file, import `@eslint/markdown` and configure a `language`:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import { defineConfig } from "eslint/config";
|
||||
import markdown from "@eslint/markdown";
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
files: ["**/*.md"],
|
||||
plugins: {
|
||||
markdown,
|
||||
},
|
||||
language: "markdown/gfm",
|
||||
rules: {
|
||||
"markdown/no-html": "error",
|
||||
},
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
### Language Options
|
||||
|
||||
#### Enabling Front Matter in both `commonmark` and `gfm`
|
||||
|
||||
By default, Markdown parsers do not support [front matter](https://jekyllrb.com/docs/front-matter/). To enable front matter in both `commonmark` and `gfm`, you can use the `frontmatter` option in `languageOptions`.
|
||||
|
||||
> `@eslint/markdown` internally uses [`micromark-extension-frontmatter`](https://github.com/micromark/micromark-extension-frontmatter) and [`mdast-util-frontmatter`](https://github.com/syntax-tree/mdast-util-frontmatter) to parse front matter.
|
||||
|
||||
| **Option Value** | **Description** |
|
||||
| ---------------- | ---------------------------------------------------------- |
|
||||
| `false` | Disables front matter parsing in Markdown files. (Default) |
|
||||
| `"yaml"` | Enables YAML front matter parsing in Markdown files. |
|
||||
| `"toml"` | Enables TOML front matter parsing in Markdown files. |
|
||||
| `"json"` | Enables JSON front matter parsing in Markdown files. |
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import { defineConfig } from "eslint/config";
|
||||
import markdown from "@eslint/markdown";
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
files: ["**/*.md"],
|
||||
plugins: {
|
||||
markdown,
|
||||
},
|
||||
language: "markdown/gfm",
|
||||
languageOptions: {
|
||||
frontmatter: "yaml", // Or pass `"toml"` or `"json"` to enable TOML or JSON front matter parsing.
|
||||
},
|
||||
rules: {
|
||||
"markdown/no-html": "error",
|
||||
},
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
#### Enabling Math (LaTeX) in both `commonmark` and `gfm`
|
||||
|
||||
By default, Markdown parsers do not support [math](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/writing-mathematical-expressions) ([LaTeX](https://www.latex-project.org/)). To enable math in both `commonmark` and `gfm`, you can use the `math` option in `languageOptions`.
|
||||
|
||||
> `@eslint/markdown` internally uses [`micromark-extension-math`](https://github.com/micromark/micromark-extension-math) and [`mdast-util-math`](https://github.com/syntax-tree/mdast-util-math) to parse math.
|
||||
|
||||
| **Option Value** | **Description** |
|
||||
| ---------------- | -------------------------------------------------- |
|
||||
| `false` | Disables math parsing in Markdown files. (Default) |
|
||||
| `true` | Enables math parsing in Markdown files. |
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import { defineConfig } from "eslint/config";
|
||||
import markdown from "@eslint/markdown";
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
files: ["**/*.md"],
|
||||
plugins: {
|
||||
markdown,
|
||||
},
|
||||
language: "markdown/gfm",
|
||||
languageOptions: {
|
||||
math: true, // Or pass `false` to disable math parsing.
|
||||
},
|
||||
rules: {
|
||||
"markdown/no-html": "error",
|
||||
},
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
### Processors
|
||||
|
||||
| **Processor Name** | **Description** |
|
||||
| ------------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| [`markdown`](./docs/processors/markdown.md) | Extract fenced code blocks from the Markdown code so they can be linted separately. |
|
||||
|
||||
## Migration from `eslint-plugin-markdown`
|
||||
|
||||
See [Migration](./docs/migration.md#from-eslint-plugin-markdown).
|
||||
|
||||
## Editor Integrations
|
||||
|
||||
### VSCode
|
||||
|
||||
[`vscode-eslint`](https://github.com/microsoft/vscode-eslint) has built-in support for the Markdown processor.
|
||||
|
||||
## File Name Details
|
||||
|
||||
This processor will use file names from blocks if a `filename` meta is present.
|
||||
|
||||
For example, the following block will result in a parsed file name of `src/index.js`:
|
||||
|
||||
````md
|
||||
```js filename="src/index.js"
|
||||
export const value = "Hello, world!";
|
||||
```
|
||||
````
|
||||
|
||||
This can be useful for user configurations that include linting overrides for specific file paths. In this example, you could then target the specific code block in your configuration using `"file-name.md/*src/index.js"`.
|
||||
|
||||
## Contributing
|
||||
|
||||
```sh
|
||||
$ git clone https://github.com/eslint/markdown.git
|
||||
$ cd markdown
|
||||
$ npm install
|
||||
$ npm test
|
||||
```
|
||||
|
||||
This project follows the [ESLint contribution guidelines](https://eslint.org/docs/latest/contribute/).
|
||||
|
||||
<!-- NOTE: This section is autogenerated. Do not manually edit.-->
|
||||
<!--sponsorsstart-->
|
||||
|
||||
## Sponsors
|
||||
|
||||
The following companies, organizations, and individuals support ESLint's ongoing maintenance and development. [Become a Sponsor](https://eslint.org/donate)
|
||||
to get your logo on our READMEs and [website](https://eslint.org/sponsors).
|
||||
|
||||
<h3>Platinum Sponsors</h3>
|
||||
<p><a href="https://automattic.com"><img src="https://images.opencollective.com/automattic/d0ef3e1/logo.png" alt="Automattic" height="128"></a></p><h3>Gold Sponsors</h3>
|
||||
<p><a href="https://qlty.sh/"><img src="https://images.opencollective.com/qltysh/33d157d/logo.png" alt="Qlty Software" height="96"></a> <a href="https://shopify.engineering/"><img src="https://avatars.githubusercontent.com/u/8085" alt="Shopify" height="96"></a> <a href="https://www.coderabbit.ai/?utm_source=cr_org&utm_medium=github"><img src="https://avatars.githubusercontent.com/u/132028505" alt="CodeRabbit" height="96"></a></p><h3>Silver Sponsors</h3>
|
||||
<p><a href="https://vite.dev/"><img src="https://images.opencollective.com/vite/d472863/logo.png" alt="Vite" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/2d6c3b6/logo.png" alt="Liftoff" height="64"></a> <a href="https://stackblitz.com"><img src="https://avatars.githubusercontent.com/u/28635252" alt="StackBlitz" height="64"></a></p><h3>Bronze Sponsors</h3>
|
||||
<p><a href="https://cybozu.co.jp/"><img src="https://images.opencollective.com/cybozu/933e46d/logo.png" alt="Cybozu" height="32"></a> <a href="https://opensource.sap.com"><img src="https://avatars.githubusercontent.com/u/2531208" alt="SAP" height="32"></a> <a href="https://www.crawljobs.com/"><img src="https://images.opencollective.com/crawljobs-poland/fa43a17/logo.png" alt="CrawlJobs" height="32"></a> <a href="https://depot.dev"><img src="https://images.opencollective.com/depot/39125a1/logo.png" alt="Depot" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://www.gitbook.com"><img src="https://avatars.githubusercontent.com/u/7111340" alt="GitBook" height="32"></a> <a href="https://citadel.co.jp"><img src="https://avatars.githubusercontent.com/u/75781367" alt="Citadel AI" height="32"></a></p>
|
||||
<h3>Technology Sponsors</h3>
|
||||
Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.
|
||||
<p><a href="https://netlify.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/netlify-icon.svg" alt="Netlify" height="32"></a> <a href="https://algolia.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/algolia-icon.svg" alt="Algolia" height="32"></a> <a href="https://1password.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/1password-icon.svg" alt="1Password" height="32"></a></p>
|
||||
<!--sponsorsend-->
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
export default rules;
|
||||
declare const rules: {
|
||||
readonly "markdown/fenced-code-language": "error";
|
||||
readonly "markdown/heading-increment": "error";
|
||||
readonly "markdown/no-duplicate-definitions": "error";
|
||||
readonly "markdown/no-empty-definitions": "error";
|
||||
readonly "markdown/no-empty-images": "error";
|
||||
readonly "markdown/no-empty-links": "error";
|
||||
readonly "markdown/no-invalid-label-refs": "error";
|
||||
readonly "markdown/no-missing-atx-heading-space": "error";
|
||||
readonly "markdown/no-missing-label-refs": "error";
|
||||
readonly "markdown/no-missing-link-fragments": "error";
|
||||
readonly "markdown/no-multiple-h1": "error";
|
||||
readonly "markdown/no-reference-like-urls": "error";
|
||||
readonly "markdown/no-reversed-media-syntax": "error";
|
||||
readonly "markdown/no-space-in-emphasis": "error";
|
||||
readonly "markdown/no-unused-definitions": "error";
|
||||
readonly "markdown/require-alt-text": "error";
|
||||
readonly "markdown/table-column-count": "error";
|
||||
};
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
const rules = /** @type {const} */ ({
|
||||
"markdown/fenced-code-language": "error",
|
||||
"markdown/heading-increment": "error",
|
||||
"markdown/no-duplicate-definitions": "error",
|
||||
"markdown/no-empty-definitions": "error",
|
||||
"markdown/no-empty-images": "error",
|
||||
"markdown/no-empty-links": "error",
|
||||
"markdown/no-invalid-label-refs": "error",
|
||||
"markdown/no-missing-atx-heading-space": "error",
|
||||
"markdown/no-missing-label-refs": "error",
|
||||
"markdown/no-missing-link-fragments": "error",
|
||||
"markdown/no-multiple-h1": "error",
|
||||
"markdown/no-reference-like-urls": "error",
|
||||
"markdown/no-reversed-media-syntax": "error",
|
||||
"markdown/no-space-in-emphasis": "error",
|
||||
"markdown/no-unused-definitions": "error",
|
||||
"markdown/require-alt-text": "error",
|
||||
"markdown/table-column-count": "error"
|
||||
});
|
||||
export default rules;
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
declare const _default: {
|
||||
"fenced-code-language": {
|
||||
meta: typeof rule0.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"fenced-code-meta": {
|
||||
meta: typeof rule1.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"heading-increment": {
|
||||
meta: typeof rule2.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-bare-urls": {
|
||||
meta: typeof rule3.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-duplicate-definitions": {
|
||||
meta: typeof rule4.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-duplicate-headings": {
|
||||
meta: typeof rule5.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-empty-definitions": {
|
||||
meta: typeof rule6.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-empty-images": {
|
||||
meta: typeof rule7.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-empty-links": {
|
||||
meta: typeof rule8.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-html": {
|
||||
meta: typeof rule9.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-invalid-label-refs": {
|
||||
meta: typeof rule10.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-missing-atx-heading-space": {
|
||||
meta: typeof rule11.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-missing-label-refs": {
|
||||
meta: typeof rule12.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-missing-link-fragments": {
|
||||
meta: typeof rule13.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-multiple-h1": {
|
||||
meta: typeof rule14.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-reference-like-urls": {
|
||||
meta: typeof rule15.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-reversed-media-syntax": {
|
||||
meta: typeof rule16.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-space-in-emphasis": {
|
||||
meta: typeof rule17.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-unused-definitions": {
|
||||
meta: typeof rule18.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"require-alt-text": {
|
||||
meta: typeof rule19.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"table-column-count": {
|
||||
meta: typeof rule20.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
};
|
||||
export default _default;
|
||||
import rule0 from "../rules/fenced-code-language.js";
|
||||
import rule1 from "../rules/fenced-code-meta.js";
|
||||
import rule2 from "../rules/heading-increment.js";
|
||||
import rule3 from "../rules/no-bare-urls.js";
|
||||
import rule4 from "../rules/no-duplicate-definitions.js";
|
||||
import rule5 from "../rules/no-duplicate-headings.js";
|
||||
import rule6 from "../rules/no-empty-definitions.js";
|
||||
import rule7 from "../rules/no-empty-images.js";
|
||||
import rule8 from "../rules/no-empty-links.js";
|
||||
import rule9 from "../rules/no-html.js";
|
||||
import rule10 from "../rules/no-invalid-label-refs.js";
|
||||
import rule11 from "../rules/no-missing-atx-heading-space.js";
|
||||
import rule12 from "../rules/no-missing-label-refs.js";
|
||||
import rule13 from "../rules/no-missing-link-fragments.js";
|
||||
import rule14 from "../rules/no-multiple-h1.js";
|
||||
import rule15 from "../rules/no-reference-like-urls.js";
|
||||
import rule16 from "../rules/no-reversed-media-syntax.js";
|
||||
import rule17 from "../rules/no-space-in-emphasis.js";
|
||||
import rule18 from "../rules/no-unused-definitions.js";
|
||||
import rule19 from "../rules/require-alt-text.js";
|
||||
import rule20 from "../rules/table-column-count.js";
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import rule0 from "../rules/fenced-code-language.js";
|
||||
import rule1 from "../rules/fenced-code-meta.js";
|
||||
import rule2 from "../rules/heading-increment.js";
|
||||
import rule3 from "../rules/no-bare-urls.js";
|
||||
import rule4 from "../rules/no-duplicate-definitions.js";
|
||||
import rule5 from "../rules/no-duplicate-headings.js";
|
||||
import rule6 from "../rules/no-empty-definitions.js";
|
||||
import rule7 from "../rules/no-empty-images.js";
|
||||
import rule8 from "../rules/no-empty-links.js";
|
||||
import rule9 from "../rules/no-html.js";
|
||||
import rule10 from "../rules/no-invalid-label-refs.js";
|
||||
import rule11 from "../rules/no-missing-atx-heading-space.js";
|
||||
import rule12 from "../rules/no-missing-label-refs.js";
|
||||
import rule13 from "../rules/no-missing-link-fragments.js";
|
||||
import rule14 from "../rules/no-multiple-h1.js";
|
||||
import rule15 from "../rules/no-reference-like-urls.js";
|
||||
import rule16 from "../rules/no-reversed-media-syntax.js";
|
||||
import rule17 from "../rules/no-space-in-emphasis.js";
|
||||
import rule18 from "../rules/no-unused-definitions.js";
|
||||
import rule19 from "../rules/require-alt-text.js";
|
||||
import rule20 from "../rules/table-column-count.js";
|
||||
export default {
|
||||
"fenced-code-language": /** @type {{meta: typeof rule0.meta; create: (context: unknown) => any}} */ (rule0),
|
||||
"fenced-code-meta": /** @type {{meta: typeof rule1.meta; create: (context: unknown) => any}} */ (rule1),
|
||||
"heading-increment": /** @type {{meta: typeof rule2.meta; create: (context: unknown) => any}} */ (rule2),
|
||||
"no-bare-urls": /** @type {{meta: typeof rule3.meta; create: (context: unknown) => any}} */ (rule3),
|
||||
"no-duplicate-definitions": /** @type {{meta: typeof rule4.meta; create: (context: unknown) => any}} */ (rule4),
|
||||
"no-duplicate-headings": /** @type {{meta: typeof rule5.meta; create: (context: unknown) => any}} */ (rule5),
|
||||
"no-empty-definitions": /** @type {{meta: typeof rule6.meta; create: (context: unknown) => any}} */ (rule6),
|
||||
"no-empty-images": /** @type {{meta: typeof rule7.meta; create: (context: unknown) => any}} */ (rule7),
|
||||
"no-empty-links": /** @type {{meta: typeof rule8.meta; create: (context: unknown) => any}} */ (rule8),
|
||||
"no-html": /** @type {{meta: typeof rule9.meta; create: (context: unknown) => any}} */ (rule9),
|
||||
"no-invalid-label-refs": /** @type {{meta: typeof rule10.meta; create: (context: unknown) => any}} */ (rule10),
|
||||
"no-missing-atx-heading-space": /** @type {{meta: typeof rule11.meta; create: (context: unknown) => any}} */ (rule11),
|
||||
"no-missing-label-refs": /** @type {{meta: typeof rule12.meta; create: (context: unknown) => any}} */ (rule12),
|
||||
"no-missing-link-fragments": /** @type {{meta: typeof rule13.meta; create: (context: unknown) => any}} */ (rule13),
|
||||
"no-multiple-h1": /** @type {{meta: typeof rule14.meta; create: (context: unknown) => any}} */ (rule14),
|
||||
"no-reference-like-urls": /** @type {{meta: typeof rule15.meta; create: (context: unknown) => any}} */ (rule15),
|
||||
"no-reversed-media-syntax": /** @type {{meta: typeof rule16.meta; create: (context: unknown) => any}} */ (rule16),
|
||||
"no-space-in-emphasis": /** @type {{meta: typeof rule17.meta; create: (context: unknown) => any}} */ (rule17),
|
||||
"no-unused-definitions": /** @type {{meta: typeof rule18.meta; create: (context: unknown) => any}} */ (rule18),
|
||||
"require-alt-text": /** @type {{meta: typeof rule19.meta; create: (context: unknown) => any}} */ (rule19),
|
||||
"table-column-count": /** @type {{meta: typeof rule20.meta; create: (context: unknown) => any}} */ (rule20),
|
||||
};
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
export default plugin;
|
||||
export { MarkdownSourceCode };
|
||||
export * from "./language/markdown-language.js";
|
||||
export * from "./types.js";
|
||||
declare namespace plugin {
|
||||
export namespace meta {
|
||||
let name: string;
|
||||
let version: string;
|
||||
}
|
||||
export namespace processors {
|
||||
export { processor as markdown };
|
||||
}
|
||||
export namespace languages {
|
||||
let commonmark: MarkdownLanguage;
|
||||
let gfm: MarkdownLanguage;
|
||||
}
|
||||
export { rules };
|
||||
export let configs: {
|
||||
"recommended-legacy": {
|
||||
plugins: string[];
|
||||
overrides: ({
|
||||
files: string[];
|
||||
processor: string;
|
||||
parserOptions?: undefined;
|
||||
rules?: undefined;
|
||||
} | {
|
||||
files: string[];
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
impliedStrict: boolean;
|
||||
};
|
||||
};
|
||||
rules: {
|
||||
"eol-last": "off";
|
||||
"no-undef": "off";
|
||||
"no-unused-expressions": "off";
|
||||
"no-unused-vars": "off";
|
||||
"padded-blocks": "off";
|
||||
strict: "off";
|
||||
"unicode-bom": "off";
|
||||
};
|
||||
processor?: undefined;
|
||||
})[];
|
||||
};
|
||||
recommended: {
|
||||
name: string;
|
||||
files: string[];
|
||||
language: string;
|
||||
plugins: {};
|
||||
rules: {
|
||||
readonly "markdown/fenced-code-language": "error";
|
||||
readonly "markdown/heading-increment": "error";
|
||||
readonly "markdown/no-duplicate-definitions": "error";
|
||||
readonly "markdown/no-empty-definitions": "error";
|
||||
readonly "markdown/no-empty-images": "error";
|
||||
readonly "markdown/no-empty-links": "error";
|
||||
readonly "markdown/no-invalid-label-refs": "error";
|
||||
readonly "markdown/no-missing-atx-heading-space": "error";
|
||||
readonly "markdown/no-missing-label-refs": "error";
|
||||
readonly "markdown/no-missing-link-fragments": "error";
|
||||
readonly "markdown/no-multiple-h1": "error";
|
||||
readonly "markdown/no-reference-like-urls": "error";
|
||||
readonly "markdown/no-reversed-media-syntax": "error";
|
||||
readonly "markdown/no-space-in-emphasis": "error";
|
||||
readonly "markdown/no-unused-definitions": "error";
|
||||
readonly "markdown/require-alt-text": "error";
|
||||
readonly "markdown/table-column-count": "error";
|
||||
};
|
||||
}[];
|
||||
processor: ConfigObject[];
|
||||
};
|
||||
}
|
||||
import { MarkdownSourceCode } from "./language/markdown-source-code.js";
|
||||
import { processor } from "./processor.js";
|
||||
import { MarkdownLanguage } from "./language/markdown-language.js";
|
||||
import rules from "./build/rules.js";
|
||||
import type { ConfigObject } from "@eslint/core";
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* @fileoverview Markdown plugin.
|
||||
* @author Brandon Mills
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { processor } from "./processor.js";
|
||||
import { MarkdownLanguage } from "./language/markdown-language.js";
|
||||
import { MarkdownSourceCode } from "./language/markdown-source-code.js";
|
||||
import recommendedRules from "./build/recommended-config.js";
|
||||
import rules from "./build/rules.js";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { ConfigObject, RulesConfig } from "@eslint/core";
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
/** @satisfies {RulesConfig} */
|
||||
const processorRulesConfig = {
|
||||
// The Markdown parser automatically trims trailing
|
||||
// newlines from code blocks.
|
||||
"eol-last": "off",
|
||||
// In code snippets and examples, these rules are often
|
||||
// counterproductive to clarity and brevity.
|
||||
"no-undef": "off",
|
||||
"no-unused-expressions": "off",
|
||||
"no-unused-vars": "off",
|
||||
"padded-blocks": "off",
|
||||
// Adding a "use strict" directive at the top of every
|
||||
// code block is tedious and distracting. The config
|
||||
// opts into strict mode parsing without the directive.
|
||||
strict: "off",
|
||||
// The processor will not receive a Unicode Byte Order
|
||||
// Mark from the Markdown parser.
|
||||
"unicode-bom": "off",
|
||||
};
|
||||
let recommendedPlugins, processorPlugins;
|
||||
const plugin = {
|
||||
meta: {
|
||||
name: "@eslint/markdown",
|
||||
version: "8.0.3", // x-release-please-version
|
||||
},
|
||||
processors: {
|
||||
markdown: processor,
|
||||
},
|
||||
languages: {
|
||||
commonmark: new MarkdownLanguage({ mode: "commonmark" }),
|
||||
gfm: new MarkdownLanguage({ mode: "gfm" }),
|
||||
},
|
||||
rules,
|
||||
configs: {
|
||||
"recommended-legacy": {
|
||||
plugins: ["markdown"],
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.md"],
|
||||
processor: "markdown/markdown",
|
||||
},
|
||||
{
|
||||
files: ["**/*.md/**"],
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
// Adding a "use strict" directive at the top of
|
||||
// every code block is tedious and distracting, so
|
||||
// opt into strict mode parsing without the
|
||||
// directive.
|
||||
impliedStrict: true,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...processorRulesConfig,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
recommended: [
|
||||
{
|
||||
name: "markdown/recommended",
|
||||
files: ["**/*.md"],
|
||||
language: "markdown/commonmark",
|
||||
plugins: (recommendedPlugins = {}),
|
||||
rules: recommendedRules,
|
||||
},
|
||||
],
|
||||
processor: /** @type {ConfigObject[]} */ ([
|
||||
{
|
||||
name: "markdown/recommended/plugin",
|
||||
plugins: (processorPlugins = {}),
|
||||
},
|
||||
{
|
||||
name: "markdown/recommended/processor",
|
||||
files: ["**/*.md"],
|
||||
processor: "markdown/markdown",
|
||||
},
|
||||
{
|
||||
name: "markdown/recommended/code-blocks",
|
||||
files: ["**/*.md/**"],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
// Adding a "use strict" directive at the top of
|
||||
// every code block is tedious and distracting, so
|
||||
// opt into strict mode parsing without the
|
||||
// directive.
|
||||
impliedStrict: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...processorRulesConfig,
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
};
|
||||
Object.assign(recommendedPlugins, { markdown: plugin });
|
||||
Object.assign(processorPlugins, { markdown: plugin });
|
||||
export default plugin;
|
||||
export { MarkdownSourceCode };
|
||||
export * from "./language/markdown-language.js";
|
||||
export * from "./types.js";
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Markdown Language Object
|
||||
* @implements {Language}
|
||||
*/
|
||||
export class MarkdownLanguage implements Language {
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Object} options The options to use for this instance.
|
||||
* @param {ParserMode} [options.mode] The Markdown parser mode to use.
|
||||
*/
|
||||
constructor({ mode }?: {
|
||||
mode?: ParserMode;
|
||||
});
|
||||
/**
|
||||
* The type of file to read.
|
||||
* @type {"text"}
|
||||
*/
|
||||
fileType: "text";
|
||||
/**
|
||||
* The line number at which the parser starts counting.
|
||||
* @type {0|1}
|
||||
*/
|
||||
lineStart: 0 | 1;
|
||||
/**
|
||||
* The column number at which the parser starts counting.
|
||||
* @type {0|1}
|
||||
*/
|
||||
columnStart: 0 | 1;
|
||||
/**
|
||||
* The name of the key that holds the type of the node.
|
||||
* @type {string}
|
||||
*/
|
||||
nodeTypeKey: string;
|
||||
/**
|
||||
* Default language options. User-defined options are merged with this object.
|
||||
* @type {MarkdownLanguageOptions}
|
||||
*/
|
||||
defaultLanguageOptions: MarkdownLanguageOptions;
|
||||
/**
|
||||
* Validates the language options.
|
||||
* @param {MarkdownLanguageOptions} languageOptions The language options to validate.
|
||||
* @returns {void}
|
||||
* @throws {Error} When the language options are invalid.
|
||||
*/
|
||||
validateLanguageOptions(languageOptions: MarkdownLanguageOptions): void;
|
||||
/**
|
||||
* Parses the given file into an AST.
|
||||
* @param {File} file The virtual file to parse.
|
||||
* @param {MarkdownLanguageContext} context The options to use for parsing.
|
||||
* @returns {ParseResult<Root>} The result of parsing.
|
||||
*/
|
||||
parse(file: File, context: MarkdownLanguageContext): ParseResult<Root>;
|
||||
/**
|
||||
* Creates a new `MarkdownSourceCode` object from the given information.
|
||||
* @param {File} file The virtual file to create a `MarkdownSourceCode` object from.
|
||||
* @param {OkParseResult<Root>} parseResult The result returned from `parse()`.
|
||||
* @returns {MarkdownSourceCode} The new `MarkdownSourceCode` object.
|
||||
*/
|
||||
createSourceCode(file: File, parseResult: OkParseResult<Root>): MarkdownSourceCode;
|
||||
#private;
|
||||
}
|
||||
export type Extensions = Options["extensions"];
|
||||
export type MdastExtensions = Options["mdastExtensions"];
|
||||
export type ParserMode = "commonmark" | "gfm";
|
||||
import type { Language } from "@eslint/core";
|
||||
import type { MarkdownLanguageOptions } from "../types.js";
|
||||
import type { File } from "@eslint/core";
|
||||
import type { MarkdownLanguageContext } from "../types.js";
|
||||
import type { Root } from "mdast";
|
||||
import type { ParseResult } from "@eslint/core";
|
||||
import type { OkParseResult } from "@eslint/core";
|
||||
import { MarkdownSourceCode } from "./markdown-source-code.js";
|
||||
import type { Options } from "mdast-util-from-markdown";
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* @fileoverview The MarkdownLanguage class.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
/* eslint class-methods-use-this: 0 -- Required to complete interface. */
|
||||
//------------------------------------------------------------------------------
|
||||
// Imports
|
||||
//------------------------------------------------------------------------------
|
||||
import { MarkdownSourceCode } from "./markdown-source-code.js";
|
||||
import { fromMarkdown } from "mdast-util-from-markdown";
|
||||
import { frontmatterFromMarkdown } from "mdast-util-frontmatter";
|
||||
import { gfmFromMarkdown } from "mdast-util-gfm";
|
||||
import { mathFromMarkdown } from "mdast-util-math";
|
||||
import { frontmatter } from "micromark-extension-frontmatter";
|
||||
import { gfm } from "micromark-extension-gfm";
|
||||
import { math } from "micromark-extension-math";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Types
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { Language, File, ParseResult, OkParseResult } from "@eslint/core";
|
||||
* @import { Root } from "mdast";
|
||||
* @import { Options } from "mdast-util-from-markdown";
|
||||
* @import { MarkdownLanguageOptions, MarkdownLanguageContext } from "../types.js";
|
||||
* @typedef {Options['extensions']} Extensions
|
||||
* @typedef {Options['mdastExtensions']} MdastExtensions
|
||||
* @typedef {"commonmark"|"gfm"} ParserMode
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* Parser configuration for JSON frontmatter.
|
||||
* Example of supported frontmatter format:
|
||||
* ```markdown
|
||||
* ---
|
||||
* {
|
||||
* "title": "My Document",
|
||||
* "date": "2025-06-09"
|
||||
* }
|
||||
* ---
|
||||
* ```
|
||||
*/
|
||||
const jsonFrontmatterConfig = {
|
||||
type: "json",
|
||||
marker: "-",
|
||||
};
|
||||
/**
|
||||
* Create parser options based on `mode` and `languageOptions`.
|
||||
* @param {ParserMode} mode The markdown parser mode.
|
||||
* @param {MarkdownLanguageOptions} languageOptions Language options.
|
||||
* @returns {{extensions: Extensions, mdastExtensions: MdastExtensions}} Parser options for micromark and mdast.
|
||||
*/
|
||||
function createParserOptions(mode, languageOptions) {
|
||||
/** @type {Extensions} */
|
||||
const extensions = [];
|
||||
/** @type {MdastExtensions} */
|
||||
const mdastExtensions = [];
|
||||
// 1. `mode`: Add GFM extensions if mode is "gfm"
|
||||
if (mode === "gfm") {
|
||||
extensions.push(gfm());
|
||||
mdastExtensions.push(gfmFromMarkdown());
|
||||
}
|
||||
// 2. `languageOptions.frontmatter`: Handle frontmatter options
|
||||
const frontmatterOption = languageOptions?.frontmatter;
|
||||
// Skip frontmatter entirely if false
|
||||
if (frontmatterOption !== false) {
|
||||
if (frontmatterOption === "yaml") {
|
||||
extensions.push(frontmatter(["yaml"]));
|
||||
mdastExtensions.push(frontmatterFromMarkdown(["yaml"]));
|
||||
}
|
||||
else if (frontmatterOption === "toml") {
|
||||
extensions.push(frontmatter(["toml"]));
|
||||
mdastExtensions.push(frontmatterFromMarkdown(["toml"]));
|
||||
}
|
||||
else if (frontmatterOption === "json") {
|
||||
extensions.push(frontmatter(jsonFrontmatterConfig));
|
||||
mdastExtensions.push(frontmatterFromMarkdown(jsonFrontmatterConfig));
|
||||
}
|
||||
}
|
||||
// 3. `languageOptions.math`: Handle math option
|
||||
const mathOption = languageOptions?.math;
|
||||
// Skip math entirely if false
|
||||
if (mathOption === true) {
|
||||
extensions.push(math());
|
||||
mdastExtensions.push(mathFromMarkdown());
|
||||
}
|
||||
return {
|
||||
extensions,
|
||||
mdastExtensions,
|
||||
};
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* Markdown Language Object
|
||||
* @implements {Language}
|
||||
*/
|
||||
export class MarkdownLanguage {
|
||||
/**
|
||||
* The type of file to read.
|
||||
* @type {"text"}
|
||||
*/
|
||||
fileType = "text";
|
||||
/**
|
||||
* The line number at which the parser starts counting.
|
||||
* @type {0|1}
|
||||
*/
|
||||
lineStart = 1;
|
||||
/**
|
||||
* The column number at which the parser starts counting.
|
||||
* @type {0|1}
|
||||
*/
|
||||
columnStart = 1;
|
||||
/**
|
||||
* The name of the key that holds the type of the node.
|
||||
* @type {string}
|
||||
*/
|
||||
nodeTypeKey = "type";
|
||||
/**
|
||||
* Default language options. User-defined options are merged with this object.
|
||||
* @type {MarkdownLanguageOptions}
|
||||
*/
|
||||
defaultLanguageOptions = {
|
||||
frontmatter: false,
|
||||
math: false,
|
||||
};
|
||||
/**
|
||||
* The Markdown parser mode.
|
||||
* @type {ParserMode}
|
||||
*/
|
||||
#mode = "commonmark";
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Object} options The options to use for this instance.
|
||||
* @param {ParserMode} [options.mode] The Markdown parser mode to use.
|
||||
*/
|
||||
constructor({ mode } = {}) {
|
||||
if (mode) {
|
||||
this.#mode = mode;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Validates the language options.
|
||||
* @param {MarkdownLanguageOptions} languageOptions The language options to validate.
|
||||
* @returns {void}
|
||||
* @throws {Error} When the language options are invalid.
|
||||
*/
|
||||
validateLanguageOptions(languageOptions) {
|
||||
// `frontmatter` option validation
|
||||
const frontmatterOption = languageOptions?.frontmatter;
|
||||
const validFrontmatterOptions = new Set([
|
||||
false,
|
||||
"yaml",
|
||||
"toml",
|
||||
"json",
|
||||
]);
|
||||
if (frontmatterOption !== undefined &&
|
||||
!validFrontmatterOptions.has(frontmatterOption)) {
|
||||
throw new Error(`Invalid language option value \`${frontmatterOption}\` for frontmatter. Expected one of \`false\`, \`"yaml"\`, \`"toml"\`, or \`"json"\`.`);
|
||||
}
|
||||
// `math` option validation
|
||||
const mathOption = languageOptions?.math;
|
||||
if (mathOption !== undefined && typeof mathOption !== "boolean") {
|
||||
throw new Error(`Invalid language option value \`${mathOption}\` for math. Expected a boolean.`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Parses the given file into an AST.
|
||||
* @param {File} file The virtual file to parse.
|
||||
* @param {MarkdownLanguageContext} context The options to use for parsing.
|
||||
* @returns {ParseResult<Root>} The result of parsing.
|
||||
*/
|
||||
parse(file, context) {
|
||||
// Note: BOM already removed
|
||||
const text = /** @type {string} */ (file.body);
|
||||
/*
|
||||
* Check for parsing errors first. If there's a parsing error, nothing
|
||||
* else can happen. However, a parsing error does not throw an error
|
||||
* from this method - it's just considered a fatal error message, a
|
||||
* problem that ESLint identified just like any other.
|
||||
*/
|
||||
try {
|
||||
const options = createParserOptions(this.#mode, context?.languageOptions);
|
||||
const root = fromMarkdown(text, options);
|
||||
return {
|
||||
ok: true,
|
||||
ast: root,
|
||||
};
|
||||
}
|
||||
catch (ex) {
|
||||
return {
|
||||
ok: false,
|
||||
errors: [ex],
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Creates a new `MarkdownSourceCode` object from the given information.
|
||||
* @param {File} file The virtual file to create a `MarkdownSourceCode` object from.
|
||||
* @param {OkParseResult<Root>} parseResult The result returned from `parse()`.
|
||||
* @returns {MarkdownSourceCode} The new `MarkdownSourceCode` object.
|
||||
*/
|
||||
createSourceCode(file, parseResult) {
|
||||
return new MarkdownSourceCode({
|
||||
text: /** @type {string} */ (file.body),
|
||||
ast: parseResult.ast,
|
||||
});
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+96
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Represents an inline config comment in the source code.
|
||||
*/
|
||||
export class InlineConfigComment {
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Object} options The options for the instance.
|
||||
* @param {string} options.value The comment text.
|
||||
* @param {Position} options.position The position of the comment in the source code.
|
||||
*/
|
||||
constructor({ value, position }: {
|
||||
value: string;
|
||||
position: Position;
|
||||
});
|
||||
/**
|
||||
* The comment text.
|
||||
* @type {string}
|
||||
*/
|
||||
value: string;
|
||||
/**
|
||||
* The position of the comment in the source code.
|
||||
* @type {Position}
|
||||
*/
|
||||
position: Position;
|
||||
}
|
||||
/**
|
||||
* Markdown Source Code Object
|
||||
* @extends {TextSourceCodeBase<{LangOptions: MarkdownLanguageOptions, RootNode: Root, SyntaxElementWithLoc: Node, ConfigNode: { value: string; position: Position }}>}
|
||||
*/
|
||||
export class MarkdownSourceCode extends TextSourceCodeBase<{
|
||||
LangOptions: MarkdownLanguageOptions;
|
||||
RootNode: Root;
|
||||
SyntaxElementWithLoc: Node;
|
||||
ConfigNode: {
|
||||
value: string;
|
||||
position: Position;
|
||||
};
|
||||
}> {
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Object} options The options for the instance.
|
||||
* @param {string} options.text The source code text.
|
||||
* @param {Root} options.ast The root AST node.
|
||||
*/
|
||||
constructor({ text, ast }: {
|
||||
text: string;
|
||||
ast: Root;
|
||||
});
|
||||
/**
|
||||
* Returns the parent of the given node.
|
||||
* @param {Node} node The node to get the parent of.
|
||||
* @returns {Parent|undefined} The parent of the node.
|
||||
*/
|
||||
getParent(node: Node): Parent | undefined;
|
||||
/**
|
||||
* Returns an array of all inline configuration nodes found in the
|
||||
* source code.
|
||||
* @returns {Array<InlineConfigComment>} An array of all inline configuration nodes.
|
||||
*/
|
||||
getInlineConfigNodes(): Array<InlineConfigComment>;
|
||||
/**
|
||||
* Returns an all directive nodes that enable or disable rules along with any problems
|
||||
* encountered while parsing the directives.
|
||||
* @returns {{problems:Array<FileProblem>,directives:Array<Directive>}} Information
|
||||
* that ESLint needs to further process the directives.
|
||||
*/
|
||||
getDisableDirectives(): {
|
||||
problems: Array<FileProblem>;
|
||||
directives: Array<Directive>;
|
||||
};
|
||||
/**
|
||||
* Returns inline rule configurations along with any problems
|
||||
* encountered while parsing the configurations.
|
||||
* @returns {{problems:Array<FileProblem>,configs:Array<{config:{rules:RulesConfig},loc:Position}>}} Information
|
||||
* that ESLint needs to further process the rule configurations.
|
||||
*/
|
||||
applyInlineConfig(): {
|
||||
problems: Array<FileProblem>;
|
||||
configs: Array<{
|
||||
config: {
|
||||
rules: RulesConfig;
|
||||
};
|
||||
loc: Position;
|
||||
}>;
|
||||
};
|
||||
#private;
|
||||
}
|
||||
import type { Position } from "unist";
|
||||
import type { MarkdownLanguageOptions } from "../types.js";
|
||||
import type { Root } from "mdast";
|
||||
import type { Node } from "mdast";
|
||||
import { TextSourceCodeBase } from "@eslint/plugin-kit";
|
||||
import type { Parent } from "mdast";
|
||||
import type { FileProblem } from "@eslint/core";
|
||||
import { Directive } from "@eslint/plugin-kit";
|
||||
import type { RulesConfig } from "@eslint/core";
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* @fileoverview The MarkdownSourceCode class.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { VisitNodeStep, TextSourceCodeBase, ConfigCommentParser, Directive, } from "@eslint/plugin-kit";
|
||||
import { lineEndingPattern } from "../util.js";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Types
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { Position } from "unist";
|
||||
* @import { Parent, Root, Node, Html } from "mdast";
|
||||
* @import { TraversalStep, FileProblem, DirectiveType, RulesConfig } from "@eslint/core";
|
||||
* @import { MarkdownLanguageOptions } from "../types.js";
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
const commentParser = new ConfigCommentParser();
|
||||
const configCommentStart = /<!--\s*eslint(?:-enable|-disable(?:(?:-next)?-line)?)?(?:\s|-->)/u;
|
||||
const htmlComment = /<!--(.*?)-->/gsu;
|
||||
/**
|
||||
* Represents an inline config comment in the source code.
|
||||
*/
|
||||
export class InlineConfigComment {
|
||||
/**
|
||||
* The comment text.
|
||||
* @type {string}
|
||||
*/
|
||||
value;
|
||||
/**
|
||||
* The position of the comment in the source code.
|
||||
* @type {Position}
|
||||
*/
|
||||
position;
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Object} options The options for the instance.
|
||||
* @param {string} options.value The comment text.
|
||||
* @param {Position} options.position The position of the comment in the source code.
|
||||
*/
|
||||
constructor({ value, position }) {
|
||||
this.value = value.trim();
|
||||
this.position = position;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Extracts inline configuration comments from an HTML node.
|
||||
* @param {Html} node The HTML node to extract comments from.
|
||||
* @param {MarkdownSourceCode} sourceCode The Markdown source code object.
|
||||
* @returns {Array<InlineConfigComment>} The inline configuration comments found in the node.
|
||||
*/
|
||||
function extractInlineConfigCommentsFromHTML(node, sourceCode) {
|
||||
if (!configCommentStart.test(node.value)) {
|
||||
return [];
|
||||
}
|
||||
/** @type {Array<InlineConfigComment>} */
|
||||
const comments = [];
|
||||
/** @type {RegExpExecArray | null} */
|
||||
let match;
|
||||
while ((match = htmlComment.exec(node.value))) {
|
||||
if (configCommentStart.test(match[0])) {
|
||||
// calculate offset of the comment inside the node
|
||||
const startOffset = match.index + node.position.start.offset;
|
||||
const endOffset = startOffset + match[0].length;
|
||||
comments.push(new InlineConfigComment({
|
||||
value: match[1].trim(),
|
||||
position: {
|
||||
start: {
|
||||
...sourceCode.getLocFromIndex(startOffset),
|
||||
offset: startOffset,
|
||||
},
|
||||
end: {
|
||||
...sourceCode.getLocFromIndex(endOffset),
|
||||
offset: endOffset,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
return comments;
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* Markdown Source Code Object
|
||||
* @extends {TextSourceCodeBase<{LangOptions: MarkdownLanguageOptions, RootNode: Root, SyntaxElementWithLoc: Node, ConfigNode: { value: string; position: Position }}>}
|
||||
*/
|
||||
export class MarkdownSourceCode extends TextSourceCodeBase {
|
||||
/**
|
||||
* Cached traversal steps.
|
||||
* @type {Array<VisitNodeStep>|undefined}
|
||||
*/
|
||||
#steps;
|
||||
/**
|
||||
* Cache of parent nodes.
|
||||
* @type {WeakMap<Node, Parent|undefined>}
|
||||
*/
|
||||
#parents = new WeakMap();
|
||||
/**
|
||||
* Collection of HTML nodes. Used to find directive comments.
|
||||
* @type {Array<Html>}
|
||||
*/
|
||||
#htmlNodes = [];
|
||||
/**
|
||||
* Collection of inline configuration comments.
|
||||
* @type {Array<InlineConfigComment>}
|
||||
*/
|
||||
#inlineConfigComments;
|
||||
/**
|
||||
* The AST of the source code.
|
||||
* @type {Root}
|
||||
*/
|
||||
ast = undefined;
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Object} options The options for the instance.
|
||||
* @param {string} options.text The source code text.
|
||||
* @param {Root} options.ast The root AST node.
|
||||
*/
|
||||
constructor({ text, ast }) {
|
||||
super({ ast, text, lineEndingPattern });
|
||||
this.ast = ast;
|
||||
// need to traverse the source code to get the inline config nodes
|
||||
this.traverse();
|
||||
}
|
||||
/**
|
||||
* Returns the parent of the given node.
|
||||
* @param {Node} node The node to get the parent of.
|
||||
* @returns {Parent|undefined} The parent of the node.
|
||||
*/
|
||||
getParent(node) {
|
||||
return this.#parents.get(node);
|
||||
}
|
||||
/**
|
||||
* Returns an array of all inline configuration nodes found in the
|
||||
* source code.
|
||||
* @returns {Array<InlineConfigComment>} An array of all inline configuration nodes.
|
||||
*/
|
||||
getInlineConfigNodes() {
|
||||
if (!this.#inlineConfigComments) {
|
||||
this.#inlineConfigComments = this.#htmlNodes.flatMap(htmlNode => extractInlineConfigCommentsFromHTML(htmlNode, this));
|
||||
}
|
||||
return this.#inlineConfigComments;
|
||||
}
|
||||
/**
|
||||
* Returns an all directive nodes that enable or disable rules along with any problems
|
||||
* encountered while parsing the directives.
|
||||
* @returns {{problems:Array<FileProblem>,directives:Array<Directive>}} Information
|
||||
* that ESLint needs to further process the directives.
|
||||
*/
|
||||
getDisableDirectives() {
|
||||
/** @type {Array<FileProblem>} */
|
||||
const problems = [];
|
||||
/** @type {Array<Directive>} */
|
||||
const directives = [];
|
||||
this.getInlineConfigNodes().forEach(comment => {
|
||||
// Step 1: Parse the directive
|
||||
const { label, value, justification: justificationPart, } = commentParser.parseDirective(comment.value);
|
||||
// Step 2: Validate the directive does not span multiple lines
|
||||
if (label === "eslint-disable-line" &&
|
||||
comment.position.start.line !== comment.position.end.line) {
|
||||
const message = `${label} comment should not span multiple lines.`;
|
||||
problems.push({
|
||||
ruleId: null,
|
||||
message,
|
||||
loc: comment.position,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Step 3: Extract the directive value and create the Directive object
|
||||
switch (label) {
|
||||
case "eslint-disable":
|
||||
case "eslint-enable":
|
||||
case "eslint-disable-next-line":
|
||||
case "eslint-disable-line": {
|
||||
const directiveType = label.slice("eslint-".length);
|
||||
directives.push(new Directive({
|
||||
type: /** @type {DirectiveType} */ (directiveType),
|
||||
node: comment,
|
||||
value,
|
||||
justification: justificationPart,
|
||||
}));
|
||||
}
|
||||
// no default
|
||||
}
|
||||
});
|
||||
return { problems, directives };
|
||||
}
|
||||
/**
|
||||
* Returns inline rule configurations along with any problems
|
||||
* encountered while parsing the configurations.
|
||||
* @returns {{problems:Array<FileProblem>,configs:Array<{config:{rules:RulesConfig},loc:Position}>}} Information
|
||||
* that ESLint needs to further process the rule configurations.
|
||||
*/
|
||||
applyInlineConfig() {
|
||||
/** @type {Array<FileProblem>} */
|
||||
const problems = [];
|
||||
/** @type {Array<{config:{rules:RulesConfig},loc:Position}>} */
|
||||
const configs = [];
|
||||
this.getInlineConfigNodes().forEach(comment => {
|
||||
const { label, value } = commentParser.parseDirective(comment.value);
|
||||
if (label === "eslint") {
|
||||
const parseResult = commentParser.parseJSONLikeConfig(value);
|
||||
if (parseResult.ok) {
|
||||
configs.push({
|
||||
config: {
|
||||
rules: parseResult.config,
|
||||
},
|
||||
loc: comment.position,
|
||||
});
|
||||
}
|
||||
else {
|
||||
problems.push({
|
||||
ruleId: null,
|
||||
message:
|
||||
/** @type {{ok: false, error: { message: string }}} */ (parseResult).error.message,
|
||||
loc: comment.position,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return {
|
||||
configs,
|
||||
problems,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Traverse the source code and return the steps that were taken.
|
||||
* @returns {Iterable<TraversalStep>} The steps that were taken while traversing the source code.
|
||||
*/
|
||||
traverse() {
|
||||
// Because the AST doesn't mutate, we can cache the steps
|
||||
if (this.#steps) {
|
||||
return this.#steps.values();
|
||||
}
|
||||
/** @type {Array<VisitNodeStep>} */
|
||||
const steps = (this.#steps = []);
|
||||
/**
|
||||
* Recursively visits a node and its children.
|
||||
* @param {Node} node The node to visit.
|
||||
* @param {Parent} [parent] The parent of the node.
|
||||
* @returns {void}
|
||||
*/
|
||||
const visit = (node, parent) => {
|
||||
// first set the parent
|
||||
this.#parents.set(node, parent);
|
||||
// then add the step
|
||||
steps.push(new VisitNodeStep({
|
||||
target: node,
|
||||
phase: 1,
|
||||
args: [node, parent],
|
||||
}));
|
||||
// save HTML nodes
|
||||
if (node.type === "html") {
|
||||
this.#htmlNodes.push(/** @type {Html} */ (node));
|
||||
}
|
||||
// then visit the children
|
||||
if ("children" in node) {
|
||||
const parentNode = /** @type {Parent} */ (node);
|
||||
parentNode.children.forEach(child => {
|
||||
visit(child, parentNode);
|
||||
});
|
||||
}
|
||||
// then add the exit step
|
||||
steps.push(new VisitNodeStep({
|
||||
target: node,
|
||||
phase: 2,
|
||||
args: [node, parent],
|
||||
}));
|
||||
};
|
||||
visit(this.ast);
|
||||
return steps.values();
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
export namespace processor {
|
||||
export namespace meta {
|
||||
let name: string;
|
||||
let version: string;
|
||||
}
|
||||
export { preprocess };
|
||||
export { postprocess };
|
||||
export { SUPPORTS_AUTOFIX as supportsAutofix };
|
||||
}
|
||||
/**
|
||||
* Extracts lintable code blocks from Markdown text.
|
||||
* @param {string} sourceText The text of the file.
|
||||
* @param {string} filename The filename of the file.
|
||||
* @returns {Array<{ filename: string, text: string }>} Source code blocks to lint.
|
||||
*/
|
||||
declare function preprocess(sourceText: string, filename: string): Array<{
|
||||
filename: string;
|
||||
text: string;
|
||||
}>;
|
||||
/**
|
||||
* Transforms generated messages for output.
|
||||
* @param {Array<LintMessage[]>} messages An array containing one array of messages
|
||||
* for each code block returned from `preprocess`.
|
||||
* @param {string} filename The filename of the file
|
||||
* @returns {LintMessage[]} A flattened array of messages with mapped locations.
|
||||
*/
|
||||
declare function postprocess(messages: Array<LintMessage[]>, filename: string): LintMessage[];
|
||||
declare const SUPPORTS_AUTOFIX: true;
|
||||
import type { LintMessage } from "@eslint/core";
|
||||
export {};
|
||||
+394
@@ -0,0 +1,394 @@
|
||||
/**
|
||||
* @fileoverview Processes Markdown files for consumption by ESLint.
|
||||
* @author Brandon Mills
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { fromMarkdown } from "mdast-util-from-markdown";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { LintMessage, RuleTextEdit, SourceRange } from "@eslint/core";
|
||||
* @import { Node, Parent, Code, Html } from "mdast";
|
||||
* @import { Block, RangeMap } from "./types.js";
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
const UNSATISFIABLE_RULES = new Set([
|
||||
"eol-last", // The Markdown parser strips trailing newlines in code fences
|
||||
"unicode-bom", // Code blocks will begin in the middle of Markdown files
|
||||
]);
|
||||
const SUPPORTS_AUTOFIX = true;
|
||||
const BOM = "\uFEFF";
|
||||
/**
|
||||
* @type {Map<string, Block[]>}
|
||||
*/
|
||||
const blocksCache = new Map();
|
||||
/**
|
||||
* Performs a depth-first traversal of the Markdown AST.
|
||||
* @param {Node} node A Markdown AST node.
|
||||
* @param {{[key: string]: (() => void) | ((node: Code) => void) | ((node: Html) => void)}} callbacks A map of node types to callbacks.
|
||||
* @returns {void}
|
||||
*/
|
||||
function traverse(node, callbacks) {
|
||||
if (callbacks[node.type]) {
|
||||
/** @type {(node: Node) => void} */ (callbacks[node.type])(node);
|
||||
}
|
||||
else {
|
||||
/** @type {() => void} */ (callbacks["*"])();
|
||||
}
|
||||
const parent = /** @type {Parent} */ (node);
|
||||
if (typeof parent.children !== "undefined") {
|
||||
for (let i = 0; i < parent.children.length; i++) {
|
||||
traverse(parent.children[i], callbacks);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Extracts `eslint-*` or `global` comments from HTML comments if present.
|
||||
* @param {string} html The text content of an HTML AST node.
|
||||
* @returns {string} The comment's text without the opening and closing tags or
|
||||
* an empty string if the text is not an ESLint HTML comment.
|
||||
*/
|
||||
function getComment(html) {
|
||||
const commentStart = "<!--";
|
||||
const commentEnd = "-->";
|
||||
const regex = /^(?:eslint\b|global\s)/u;
|
||||
if (html.slice(0, commentStart.length) !== commentStart ||
|
||||
html.slice(-commentEnd.length) !== commentEnd) {
|
||||
return "";
|
||||
}
|
||||
const comment = html.slice(commentStart.length, -commentEnd.length);
|
||||
if (!regex.test(comment.trim())) {
|
||||
return "";
|
||||
}
|
||||
return comment;
|
||||
}
|
||||
// Before a code block, blockquote characters (`>`) are also considered
|
||||
// "whitespace".
|
||||
const leadingWhitespaceRegex = /^[>\s]*/u;
|
||||
/**
|
||||
* Gets the offset for the first column of the node's first line in the
|
||||
* original source text.
|
||||
* @param {Code} node A Markdown code block AST node.
|
||||
* @returns {number} The offset for the first column of the node's first line.
|
||||
*/
|
||||
function getBeginningOfLineOffset(node) {
|
||||
return node.position.start.offset - node.position.start.column + 1;
|
||||
}
|
||||
/**
|
||||
* Gets the leading text, typically whitespace with possible blockquote chars,
|
||||
* used to indent a code block.
|
||||
* @param {string} text The text of the file.
|
||||
* @param {Code} node A Markdown code block AST node.
|
||||
* @returns {string} The text from the start of the first line to the opening
|
||||
* fence of the code block.
|
||||
*/
|
||||
function getIndentText(text, node) {
|
||||
return leadingWhitespaceRegex.exec(text.slice(getBeginningOfLineOffset(node)))[0];
|
||||
}
|
||||
/**
|
||||
* When applying fixes, the postprocess step needs to know how to map fix ranges
|
||||
* from their location in the linted JS to the original offset in the Markdown.
|
||||
* Configuration comments and indentation trimming both complicate this process.
|
||||
*
|
||||
* Configuration comments appear in the linted JS but not in the Markdown code
|
||||
* block. Fixes to configuration comments would cause undefined behavior and
|
||||
* should be ignored during postprocessing. Fixes to actual code after
|
||||
* configuration comments need to be mapped back to the code block after
|
||||
* removing any offset due to configuration comments.
|
||||
*
|
||||
* Fenced code blocks can be indented by up to three spaces at the opening
|
||||
* fence. Inside of a list, for example, this indent can be in addition to the
|
||||
* indent already required for list item children. Leading whitespace inside
|
||||
* indented code blocks is trimmed up to the level of the opening fence and does
|
||||
* not appear in the linted code. Further, lines can have less leading
|
||||
* whitespace than the opening fence, so not all lines are guaranteed to have
|
||||
* the same column offset as the opening fence.
|
||||
*
|
||||
* The source code of a non-configuration-comment line in the linted JS is a
|
||||
* suffix of the corresponding line in the Markdown code block. There are no
|
||||
* differences within the line, so the mapping need only provide the offset
|
||||
* delta at the beginning of each line.
|
||||
* @param {string} text The text of the file.
|
||||
* @param {Code} node A Markdown code block AST node.
|
||||
* @param {string[]} comments List of configuration comment strings that will be
|
||||
* inserted at the beginning of the code block.
|
||||
* @returns {RangeMap[]} A list of offset-based adjustments, where lookups are
|
||||
* done based on the `js` key, which represents the range in the linted JS,
|
||||
* and the `md` key is the offset delta that, when added to the JS range,
|
||||
* returns the corresponding location in the original Markdown source.
|
||||
*/
|
||||
function getBlockRangeMap(text, node, comments) {
|
||||
/*
|
||||
* The parser sets the fenced code block's start offset to wherever content
|
||||
* should normally begin (typically the first column of the line, but more
|
||||
* inside a list item, for example). The code block's opening fence may be
|
||||
* further indented by up to three characters. If the code block has
|
||||
* additional indenting, the opening fence's first backtick may be up to
|
||||
* three whitespace characters after the start offset.
|
||||
*/
|
||||
const startOffset = getBeginningOfLineOffset(node);
|
||||
/*
|
||||
* Extract the Markdown source to determine the leading whitespace for each
|
||||
* line.
|
||||
*/
|
||||
const code = text.slice(startOffset, node.position.end.offset);
|
||||
const lines = code.split("\n");
|
||||
/*
|
||||
* The parser trims leading whitespace from each line of code within the
|
||||
* fenced code block up to the opening fence's first backtick. The first
|
||||
* backtick's column is the AST node's starting column plus any additional
|
||||
* indentation.
|
||||
*/
|
||||
const baseIndent = getIndentText(text, node).length;
|
||||
/*
|
||||
* Track the length of any inserted configuration comments at the beginning
|
||||
* of the linted JS and start the JS offset lookup keys at this index.
|
||||
*/
|
||||
const commentLength = comments.reduce((len, comment) => len + comment.length + 1, 0);
|
||||
/*
|
||||
* In case there are configuration comments, initialize the map so that the
|
||||
* first lookup index is always 0. If there are no configuration comments,
|
||||
* the lookup index will also be 0, and the lookup should always go to the
|
||||
* last range that matches, skipping this initialization entry.
|
||||
*/
|
||||
const rangeMap = [
|
||||
{
|
||||
indent: baseIndent,
|
||||
js: 0,
|
||||
md: 0,
|
||||
},
|
||||
];
|
||||
// Start the JS offset after any configuration comments.
|
||||
let jsOffset = commentLength;
|
||||
/*
|
||||
* Start the Markdown offset at the beginning of the block's first line of
|
||||
* actual code. The first line of the block is always the opening fence, so
|
||||
* the code begins on the second line.
|
||||
*/
|
||||
let mdOffset = startOffset + lines[0].length + 1;
|
||||
/*
|
||||
* For each line, determine how much leading whitespace was trimmed due to
|
||||
* indentation. Increase the JS lookup offset by the length of the line
|
||||
* post-trimming and the Markdown offset by the total line length.
|
||||
*/
|
||||
for (let i = 0; i + 1 < lines.length; i++) {
|
||||
const line = lines[i + 1];
|
||||
const leadingWhitespaceLength = leadingWhitespaceRegex.exec(line)[0].length;
|
||||
// The parser trims leading whitespace up to the level of the opening
|
||||
// fence, so keep any additional indentation beyond that.
|
||||
const trimLength = Math.min(baseIndent, leadingWhitespaceLength);
|
||||
rangeMap.push({
|
||||
indent: trimLength,
|
||||
js: jsOffset,
|
||||
// Advance `trimLength` character from the beginning of the Markdown
|
||||
// line to the beginning of the equivalent JS line, then compute the
|
||||
// delta.
|
||||
md: mdOffset + trimLength - jsOffset,
|
||||
});
|
||||
// Accumulate the current line in the offsets, and don't forget the
|
||||
// newline.
|
||||
mdOffset += line.length + 1;
|
||||
jsOffset += line.length - trimLength + 1;
|
||||
}
|
||||
return rangeMap;
|
||||
}
|
||||
const codeBlockFileNameRegex = /filename=(?<quote>["'])(?<filename>.*?)\1/u;
|
||||
/**
|
||||
* Parses the file name from a block meta, if available.
|
||||
* @param {Block} block A code block.
|
||||
* @returns {string | null | undefined} The filename, if parsed from block meta.
|
||||
*/
|
||||
function fileNameFromMeta(block) {
|
||||
return block.meta
|
||||
?.match(codeBlockFileNameRegex)
|
||||
?.groups.filename.replaceAll(/\s+/gu, "_");
|
||||
}
|
||||
const languageToFileExtension = {
|
||||
javascript: "js",
|
||||
ecmascript: "js",
|
||||
typescript: "ts",
|
||||
markdown: "md",
|
||||
};
|
||||
/**
|
||||
* Extracts lintable code blocks from Markdown text.
|
||||
* @param {string} sourceText The text of the file.
|
||||
* @param {string} filename The filename of the file.
|
||||
* @returns {Array<{ filename: string, text: string }>} Source code blocks to lint.
|
||||
*/
|
||||
function preprocess(sourceText, filename) {
|
||||
const text = sourceText.startsWith(BOM) ? sourceText.slice(1) : sourceText;
|
||||
const ast = fromMarkdown(text);
|
||||
/** @type {Block[]} */
|
||||
const blocks = [];
|
||||
blocksCache.set(filename, blocks);
|
||||
/**
|
||||
* During the depth-first traversal, keep track of any sequences of HTML
|
||||
* comment nodes containing `eslint-*` or `global` comments. If a code
|
||||
* block immediately follows such a sequence, insert the comments at the
|
||||
* top of the code block. Any non-ESLint comment or other node type breaks
|
||||
* and empties the sequence.
|
||||
* @type {string[]}
|
||||
*/
|
||||
let htmlComments = [];
|
||||
traverse(ast, {
|
||||
"*"() {
|
||||
htmlComments = [];
|
||||
},
|
||||
/**
|
||||
* Visit a code node.
|
||||
* @param {Code} node The visited node.
|
||||
* @returns {void}
|
||||
*/
|
||||
code(node) {
|
||||
if (node.lang) {
|
||||
/** @type {string[]} */
|
||||
const comments = [];
|
||||
for (const comment of htmlComments) {
|
||||
if (comment.trim() === "eslint-skip") {
|
||||
htmlComments = [];
|
||||
return;
|
||||
}
|
||||
comments.push(`/*${comment}*/`);
|
||||
}
|
||||
htmlComments = [];
|
||||
blocks.push({
|
||||
...node,
|
||||
baseIndentText: getIndentText(text, node),
|
||||
comments,
|
||||
rangeMap: getBlockRangeMap(text, node, comments),
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Visit an HTML node.
|
||||
* @param {Html} node The visited node.
|
||||
* @returns {void}
|
||||
*/
|
||||
html(node) {
|
||||
const comment = getComment(node.value);
|
||||
if (comment) {
|
||||
htmlComments.push(comment);
|
||||
}
|
||||
else {
|
||||
htmlComments = [];
|
||||
}
|
||||
},
|
||||
});
|
||||
return blocks.map((block, index) => {
|
||||
const [language] = block.lang.trim().split(" ");
|
||||
const fileExtension = Object.hasOwn(languageToFileExtension, language)
|
||||
? languageToFileExtension[
|
||||
/** @type {keyof typeof languageToFileExtension} */ (language)]
|
||||
: language;
|
||||
return {
|
||||
filename: fileNameFromMeta(block) ?? `${index}.${fileExtension}`,
|
||||
text: [...block.comments, block.value, ""].join("\n"),
|
||||
};
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Adjusts a fix in a code block.
|
||||
* @param {Block} block A code block.
|
||||
* @param {RuleTextEdit} fix A fix to adjust.
|
||||
* @returns {RuleTextEdit} The fix with adjusted ranges.
|
||||
*/
|
||||
function adjustFix(block, fix) {
|
||||
return {
|
||||
range: /** @type {SourceRange} */ (fix.range.map(range => {
|
||||
// Advance through the block's range map to find the last
|
||||
// matching range by finding the first range too far and
|
||||
// then going back one.
|
||||
let i = 1;
|
||||
while (i < block.rangeMap.length &&
|
||||
block.rangeMap[i].js <= range) {
|
||||
i++;
|
||||
}
|
||||
// Apply the mapping delta for this range.
|
||||
return range + block.rangeMap[i - 1].md;
|
||||
})),
|
||||
text: fix.text.replace(/\n/gu, `\n${block.baseIndentText}`),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Creates a map function that adjusts messages in a code block.
|
||||
* @param {Block} block A code block.
|
||||
* @returns {(message: LintMessage) => LintMessage | null} A function that adjusts messages in a code block.
|
||||
*/
|
||||
function adjustBlock(block) {
|
||||
const leadingCommentLines = block.comments.reduce((count, comment) => count + comment.split("\n").length, 0);
|
||||
const blockStart = block.position.start.line;
|
||||
/**
|
||||
* Adjusts ESLint messages to point to the correct location in the Markdown.
|
||||
* @param {LintMessage} message A message from ESLint.
|
||||
* @returns {LintMessage} The same message, but adjusted to the correct location.
|
||||
*/
|
||||
return function adjustMessage(message) {
|
||||
if (!Number.isInteger(message.line)) {
|
||||
return {
|
||||
...message,
|
||||
line: blockStart,
|
||||
column: block.position.start.column,
|
||||
};
|
||||
}
|
||||
const lineInCode = message.line - leadingCommentLines;
|
||||
if (lineInCode < 1 || lineInCode >= block.rangeMap.length) {
|
||||
return null;
|
||||
}
|
||||
/** @type {Pick<LintMessage, "line" | "column" | "endLine" | "suggestions">} */
|
||||
const out = {
|
||||
line: lineInCode + blockStart,
|
||||
column: message.column + block.rangeMap[lineInCode].indent,
|
||||
};
|
||||
if (Number.isInteger(message.endLine)) {
|
||||
out.endLine = message.endLine - leadingCommentLines + blockStart;
|
||||
}
|
||||
if (Array.isArray(message.suggestions)) {
|
||||
out.suggestions = message.suggestions.map(suggestion => ({
|
||||
...suggestion,
|
||||
fix: adjustFix(block, suggestion.fix),
|
||||
}));
|
||||
}
|
||||
const adjustedFix = {};
|
||||
if (message.fix) {
|
||||
adjustedFix.fix = adjustFix(block, message.fix);
|
||||
}
|
||||
return { ...message, ...out, ...adjustedFix };
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Excludes unsatisfiable rules from the list of messages.
|
||||
* @param {LintMessage} message A message from the linter.
|
||||
* @returns {boolean} True if the message should be included in output.
|
||||
*/
|
||||
function excludeUnsatisfiableRules(message) {
|
||||
return message && !UNSATISFIABLE_RULES.has(message.ruleId);
|
||||
}
|
||||
/**
|
||||
* Transforms generated messages for output.
|
||||
* @param {Array<LintMessage[]>} messages An array containing one array of messages
|
||||
* for each code block returned from `preprocess`.
|
||||
* @param {string} filename The filename of the file
|
||||
* @returns {LintMessage[]} A flattened array of messages with mapped locations.
|
||||
*/
|
||||
function postprocess(messages, filename) {
|
||||
const blocks = blocksCache.get(filename);
|
||||
blocksCache.delete(filename);
|
||||
return messages.flatMap((group, i) => {
|
||||
const adjust = adjustBlock(blocks[i]);
|
||||
return group.map(adjust).filter(excludeUnsatisfiableRules);
|
||||
});
|
||||
}
|
||||
export const processor = {
|
||||
meta: {
|
||||
name: "@eslint/markdown/markdown",
|
||||
version: "8.0.3", // x-release-please-version
|
||||
},
|
||||
preprocess,
|
||||
postprocess,
|
||||
supportsAutofix: SUPPORTS_AUTOFIX,
|
||||
};
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let recommended: boolean;
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let missingLanguage: string;
|
||||
let disallowedLanguage: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
required: {
|
||||
type: "array";
|
||||
items: {
|
||||
type: "string";
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
required: any[];
|
||||
}];
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../types.js").MarkdownLanguageOptions;
|
||||
Code: import("../index.js").MarkdownSourceCode;
|
||||
RuleOptions: FencedCodeLanguageOptions;
|
||||
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
|
||||
MessageIds: FencedCodeLanguageMessageIds;
|
||||
}>): {
|
||||
code(node: import("mdast").Code): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type FencedCodeLanguageMessageIds = "missingLanguage" | "disallowedLanguage";
|
||||
export type FencedCodeLanguageOptions = [{
|
||||
required?: string[];
|
||||
}];
|
||||
export type FencedCodeLanguageRuleDefinition = MarkdownRuleDefinition<{
|
||||
RuleOptions: FencedCodeLanguageOptions;
|
||||
MessageIds: FencedCodeLanguageMessageIds;
|
||||
}>;
|
||||
import type { MarkdownRuleDefinition } from "../types.js";
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce languages for fenced code.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { MarkdownRuleDefinition } from "../types.js";
|
||||
* @typedef {"missingLanguage" | "disallowedLanguage"} FencedCodeLanguageMessageIds
|
||||
* @typedef {[{ required?: string[] }]} FencedCodeLanguageOptions
|
||||
* @typedef {MarkdownRuleDefinition<{ RuleOptions: FencedCodeLanguageOptions, MessageIds: FencedCodeLanguageMessageIds }>} FencedCodeLanguageRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
const fencedCodeCharacters = new Set(["`", "~"]);
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {FencedCodeLanguageRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
recommended: true,
|
||||
description: "Require languages for fenced code blocks",
|
||||
url: "https://github.com/eslint/markdown/blob/main/docs/rules/fenced-code-language.md",
|
||||
},
|
||||
messages: {
|
||||
missingLanguage: "Missing code block language.",
|
||||
disallowedLanguage: 'Code block language "{{lang}}" is not allowed.',
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
required: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
required: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
create(context) {
|
||||
const required = new Set(context.options[0].required);
|
||||
const { sourceCode } = context;
|
||||
return {
|
||||
code(node) {
|
||||
if (!node.lang) {
|
||||
// only check fenced code blocks
|
||||
if (!fencedCodeCharacters.has(sourceCode.text[node.position.start.offset])) {
|
||||
return;
|
||||
}
|
||||
/** @type {number} */
|
||||
let openingCodeFenceEndOffset;
|
||||
for (openingCodeFenceEndOffset = node.position.start.offset; fencedCodeCharacters.has(sourceCode.text[openingCodeFenceEndOffset]); openingCodeFenceEndOffset++) {
|
||||
// Find the end offset of the opening code fence.
|
||||
}
|
||||
context.report({
|
||||
loc: {
|
||||
start: node.position.start,
|
||||
end: {
|
||||
line: node.position.start.line,
|
||||
column: node.position.start.column +
|
||||
openingCodeFenceEndOffset -
|
||||
node.position.start.offset,
|
||||
},
|
||||
},
|
||||
messageId: "missingLanguage",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (required.size && !required.has(node.lang)) {
|
||||
const langIndex = sourceCode
|
||||
.getText(node)
|
||||
.indexOf(node.lang);
|
||||
context.report({
|
||||
loc: {
|
||||
start: node.position.start,
|
||||
end: {
|
||||
line: node.position.start.line,
|
||||
column: node.position.start.column +
|
||||
langIndex +
|
||||
node.lang.length,
|
||||
},
|
||||
},
|
||||
messageId: "disallowedLanguage",
|
||||
data: {
|
||||
lang: node.lang,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let recommended: boolean;
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let missingMetadata: string;
|
||||
let disallowedMetadata: string;
|
||||
}
|
||||
let schema: {
|
||||
enum: string[];
|
||||
}[];
|
||||
let defaultOptions: ["always"];
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../types.js").MarkdownLanguageOptions;
|
||||
Code: import("../index.js").MarkdownSourceCode;
|
||||
RuleOptions: FencedCodeMetaOptions;
|
||||
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
|
||||
MessageIds: FencedCodeMetaMessageIds;
|
||||
}>): {
|
||||
code(node: import("mdast").Code): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type FencedCodeMetaMessageIds = "missingMetadata" | "disallowedMetadata";
|
||||
export type FencedCodeMetaOptions = ["always" | "never"];
|
||||
export type FencedCodeMetaRuleDefinition = MarkdownRuleDefinition<{
|
||||
RuleOptions: FencedCodeMetaOptions;
|
||||
MessageIds: FencedCodeMetaMessageIds;
|
||||
}>;
|
||||
import type { MarkdownRuleDefinition } from "../types.js";
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @fileoverview Rule to require or disallow metadata for fenced code blocks.
|
||||
* @author TKDev7
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { MarkdownRuleDefinition } from "../types.js";
|
||||
* @typedef {"missingMetadata" | "disallowedMetadata"} FencedCodeMetaMessageIds
|
||||
* @typedef {["always" | "never"]} FencedCodeMetaOptions
|
||||
* @typedef {MarkdownRuleDefinition<{ RuleOptions: FencedCodeMetaOptions, MessageIds: FencedCodeMetaMessageIds }>} FencedCodeMetaRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {FencedCodeMetaRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
recommended: false,
|
||||
description: "Require or disallow metadata for fenced code blocks",
|
||||
url: "https://github.com/eslint/markdown/blob/main/docs/rules/fenced-code-meta.md",
|
||||
},
|
||||
messages: {
|
||||
missingMetadata: "Missing code block metadata.",
|
||||
disallowedMetadata: "Code block metadata is not allowed.",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
enum: ["always", "never"],
|
||||
},
|
||||
],
|
||||
defaultOptions: ["always"],
|
||||
},
|
||||
create(context) {
|
||||
const [mode] = context.options;
|
||||
const { sourceCode } = context;
|
||||
return {
|
||||
code(node) {
|
||||
const lineText = sourceCode.lines[node.position.start.line - 1];
|
||||
const fenceLineText = lineText.slice(node.position.start.column - 1);
|
||||
if (mode === "always") {
|
||||
if (node.lang && !node.meta) {
|
||||
const langIndex = fenceLineText.indexOf(node.lang);
|
||||
context.report({
|
||||
loc: {
|
||||
start: node.position.start,
|
||||
end: {
|
||||
line: node.position.start.line,
|
||||
column: node.position.start.column +
|
||||
langIndex +
|
||||
node.lang.length,
|
||||
},
|
||||
},
|
||||
messageId: "missingMetadata",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (node.meta) {
|
||||
const metaIndex = fenceLineText.lastIndexOf(node.meta);
|
||||
context.report({
|
||||
loc: {
|
||||
start: {
|
||||
line: node.position.start.line,
|
||||
column: node.position.start.column + metaIndex,
|
||||
},
|
||||
end: {
|
||||
line: node.position.start.line,
|
||||
column: node.position.start.column +
|
||||
metaIndex +
|
||||
node.meta.trimEnd().length,
|
||||
},
|
||||
},
|
||||
messageId: "disallowedMetadata",
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let recommended: boolean;
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let skippedHeading: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
frontmatterTitle: {
|
||||
type: "string";
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
frontmatterTitle: string;
|
||||
}];
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../types.js").MarkdownLanguageOptions;
|
||||
Code: import("../index.js").MarkdownSourceCode;
|
||||
RuleOptions: HeadingIncrementOptions;
|
||||
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
|
||||
MessageIds: "skippedHeading";
|
||||
}>): {
|
||||
yaml(node: import("mdast").Yaml): void;
|
||||
toml(node: import("../types.js").Toml): void;
|
||||
json(node: import("../types.js").Json): void;
|
||||
heading(node: import("mdast").Heading): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type HeadingIncrementMessageIds = "skippedHeading";
|
||||
export type HeadingIncrementOptions = [{
|
||||
frontmatterTitle?: string;
|
||||
}];
|
||||
export type HeadingIncrementRuleDefinition = MarkdownRuleDefinition<{
|
||||
RuleOptions: HeadingIncrementOptions;
|
||||
MessageIds: HeadingIncrementMessageIds;
|
||||
}>;
|
||||
import type { MarkdownRuleDefinition } from "../types.js";
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce heading levels increment by one.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { frontmatterHasTitle } from "../util.js";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { MarkdownRuleDefinition } from "../types.js";
|
||||
* @typedef {"skippedHeading"} HeadingIncrementMessageIds
|
||||
* @typedef {[{ frontmatterTitle?: string }]} HeadingIncrementOptions
|
||||
* @typedef {MarkdownRuleDefinition<{ RuleOptions: HeadingIncrementOptions, MessageIds: HeadingIncrementMessageIds }>} HeadingIncrementRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {HeadingIncrementRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
recommended: true,
|
||||
description: "Enforce heading levels increment by one",
|
||||
url: "https://github.com/eslint/markdown/blob/main/docs/rules/heading-increment.md",
|
||||
},
|
||||
messages: {
|
||||
skippedHeading: "Heading level skipped from {{fromLevel}} to {{toLevel}}.",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
frontmatterTitle: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
frontmatterTitle: "^(?!\\s*['\"]title[:=]['\"])\\s*\\{?\\s*['\"]?title['\"]?\\s*[:=]",
|
||||
},
|
||||
],
|
||||
},
|
||||
create(context) {
|
||||
const [{ frontmatterTitle }] = context.options;
|
||||
const titlePattern = frontmatterTitle === "" ? null : new RegExp(frontmatterTitle, "iu");
|
||||
let lastHeadingDepth = 0;
|
||||
return {
|
||||
yaml(node) {
|
||||
if (frontmatterHasTitle(node.value, titlePattern)) {
|
||||
lastHeadingDepth = 1;
|
||||
}
|
||||
},
|
||||
toml(node) {
|
||||
if (frontmatterHasTitle(node.value, titlePattern)) {
|
||||
lastHeadingDepth = 1;
|
||||
}
|
||||
},
|
||||
json(node) {
|
||||
if (frontmatterHasTitle(node.value, titlePattern)) {
|
||||
lastHeadingDepth = 1;
|
||||
}
|
||||
},
|
||||
heading(node) {
|
||||
if (lastHeadingDepth > 0 && node.depth > lastHeadingDepth + 1) {
|
||||
context.report({
|
||||
loc: node.position,
|
||||
messageId: "skippedHeading",
|
||||
data: {
|
||||
fromLevel: lastHeadingDepth,
|
||||
toLevel: node.depth,
|
||||
},
|
||||
});
|
||||
}
|
||||
lastHeadingDepth = node.depth;
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
let fixable: "code";
|
||||
namespace messages {
|
||||
let bareUrl: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../types.js").MarkdownLanguageOptions;
|
||||
Code: import("../index.js").MarkdownSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
|
||||
MessageIds: "bareUrl";
|
||||
}>): {
|
||||
":matches(heading, paragraph, tableCell) html"(node: Html): void;
|
||||
":matches(heading, paragraph, tableCell) link"(node: Link): void;
|
||||
"heading:exit"(): void;
|
||||
"paragraph:exit"(): void;
|
||||
"tableCell:exit"(): void;
|
||||
"root:exit"(): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoBareUrlsMessageIds = "bareUrl";
|
||||
export type NoBareUrlsOptions = [];
|
||||
export type NoBareUrlsRuleDefinition = MarkdownRuleDefinition<{
|
||||
RuleOptions: NoBareUrlsOptions;
|
||||
MessageIds: NoBareUrlsMessageIds;
|
||||
}>;
|
||||
import type { Html } from "mdast";
|
||||
import type { Link } from "mdast";
|
||||
import type { MarkdownRuleDefinition } from "../types.js";
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* @fileoverview Rule to prevent bare URLs in Markdown.
|
||||
* @author xbinaryx
|
||||
*/
|
||||
/*
|
||||
* Here's a note on how the approach (algorithm) works:
|
||||
*
|
||||
* - When entering an `Html` node that is a child of a `Heading`, `Paragraph` or `TableCell`,
|
||||
* we check whether it is an opening or closing tag.
|
||||
* If we encounter an opening tag, we store the tag name and set `lastTagName`.
|
||||
* (`lastTagName` serves as a state to represent whether we're between opening and closing HTML tags.)
|
||||
* If we encounter a closing tag, we reset the stored tag name and `tempLinkNodes`.
|
||||
*
|
||||
* - When entering a `Link` node that is a child of a `Heading`, `Paragraph` or `TableCell`,
|
||||
* we check whether it is between opening and closing HTML tags.
|
||||
* If it's between opening and closing HTML tags, we add it to `tempLinkNodes`.
|
||||
* If it's not between opening and closing HTML tags, we add it to `linkNodes`.
|
||||
*
|
||||
* - When exiting a `Heading`, `Paragraph` or `TableCell`, we add all `tempLinkNodes` to `linkNodes`.
|
||||
* If there are any remaining `tempLinkNodes`, it means they are not between opening and closing HTML tags. (ex. `<br> ... <br>`)
|
||||
* If there are no remaining `tempLinkNodes`, it means they are between opening and closing HTML tags.
|
||||
*
|
||||
* - When exiting a `root` node, we report all `Link` nodes for bare URLs.
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { Link, Html } from "mdast";
|
||||
* @import { MarkdownRuleDefinition } from "../types.js";
|
||||
* @typedef {"bareUrl"} NoBareUrlsMessageIds
|
||||
* @typedef {[]} NoBareUrlsOptions
|
||||
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoBareUrlsOptions, MessageIds: NoBareUrlsMessageIds }>} NoBareUrlsRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
const htmlTagNamePattern = /^<(?<tagName>[^!>][^/\s>]*)/u;
|
||||
/**
|
||||
* Parses an HTML tag to extract its name and closing status
|
||||
* @param {string} tagText The HTML tag text to parse
|
||||
* @returns {{ name: string, isClosing: boolean } | null} Object containing tag name and closing status, or null if not a valid tag
|
||||
*/
|
||||
function parseHtmlTag(tagText) {
|
||||
const match = tagText.match(htmlTagNamePattern);
|
||||
if (match) {
|
||||
const tagName = match.groups.tagName.toLowerCase();
|
||||
const isClosing = tagName.startsWith("/");
|
||||
return {
|
||||
name: isClosing ? tagName.slice(1) : tagName,
|
||||
isClosing,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoBareUrlsRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Disallow bare URLs",
|
||||
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-bare-urls.md",
|
||||
},
|
||||
fixable: "code",
|
||||
messages: {
|
||||
bareUrl: "Unexpected bare URL. Use autolink (<URL>) or link ([text](URL)) instead.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
/**
|
||||
* This array is used to store all `Link` nodes for the final report.
|
||||
* @type {Array<Link>}
|
||||
*/
|
||||
const linkNodes = [];
|
||||
/**
|
||||
* This array is used to store `Link` nodes that are estimated to be between opening and closing HTML tags.
|
||||
* @type {Array<Link>}
|
||||
*/
|
||||
const tempLinkNodes = [];
|
||||
/** @type {string | null} */
|
||||
let lastTagName = null;
|
||||
/**
|
||||
* Resets `tempLinkNodes` and `lastTagName`
|
||||
* @returns {void}
|
||||
*/
|
||||
function reset() {
|
||||
tempLinkNodes.length = 0;
|
||||
lastTagName = null;
|
||||
}
|
||||
return {
|
||||
":matches(heading, paragraph, tableCell) html"(
|
||||
/** @type {Html} */ node) {
|
||||
const tagInfo = parseHtmlTag(node.value);
|
||||
if (!tagInfo) {
|
||||
return;
|
||||
}
|
||||
if (!tagInfo.isClosing && lastTagName === null) {
|
||||
lastTagName = tagInfo.name;
|
||||
}
|
||||
if (tagInfo.isClosing && lastTagName === tagInfo.name) {
|
||||
reset();
|
||||
}
|
||||
},
|
||||
":matches(heading, paragraph, tableCell) link"(
|
||||
/** @type {Link} */ node) {
|
||||
if (lastTagName !== null) {
|
||||
tempLinkNodes.push(node);
|
||||
}
|
||||
else {
|
||||
linkNodes.push(node);
|
||||
}
|
||||
},
|
||||
"heading:exit"() {
|
||||
linkNodes.push(...tempLinkNodes);
|
||||
reset();
|
||||
},
|
||||
"paragraph:exit"() {
|
||||
linkNodes.push(...tempLinkNodes);
|
||||
reset();
|
||||
},
|
||||
"tableCell:exit"() {
|
||||
linkNodes.push(...tempLinkNodes);
|
||||
reset();
|
||||
},
|
||||
"root:exit"() {
|
||||
for (const linkNode of linkNodes) {
|
||||
const text = sourceCode.getText(linkNode);
|
||||
const { url } = linkNode;
|
||||
if (url === text ||
|
||||
url === `http://${text}` ||
|
||||
url === `mailto:${text}`) {
|
||||
context.report({
|
||||
node: linkNode,
|
||||
messageId: "bareUrl",
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(linkNode, `<${text}>`);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
Generated
Vendored
+66
@@ -0,0 +1,66 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let recommended: boolean;
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let duplicateDefinition: string;
|
||||
let duplicateFootnoteDefinition: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
allowDefinitions: {
|
||||
type: "array";
|
||||
items: {
|
||||
type: "string";
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
allowFootnoteDefinitions: {
|
||||
type: "array";
|
||||
items: {
|
||||
type: "string";
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
checkFootnoteDefinitions: {
|
||||
type: "boolean";
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
allowDefinitions: string[];
|
||||
allowFootnoteDefinitions: any[];
|
||||
checkFootnoteDefinitions: true;
|
||||
}];
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../types.js").MarkdownLanguageOptions;
|
||||
Code: import("../index.js").MarkdownSourceCode;
|
||||
RuleOptions: NoDuplicateDefinitionsOptions;
|
||||
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
|
||||
MessageIds: NoDuplicateDefinitionsMessageIds;
|
||||
}>): {
|
||||
definition(node: Definition): void;
|
||||
footnoteDefinition(node: FootnoteDefinition): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoDuplicateDefinitionsMessageIds = "duplicateDefinition" | "duplicateFootnoteDefinition";
|
||||
export type NoDuplicateDefinitionsOptions = [{
|
||||
allowDefinitions?: string[];
|
||||
allowFootnoteDefinitions?: string[];
|
||||
checkFootnoteDefinitions?: boolean;
|
||||
}];
|
||||
export type NoDuplicateDefinitionsRuleDefinition = MarkdownRuleDefinition<{
|
||||
RuleOptions: NoDuplicateDefinitionsOptions;
|
||||
MessageIds: NoDuplicateDefinitionsMessageIds;
|
||||
}>;
|
||||
import type { Definition } from "mdast";
|
||||
import type { FootnoteDefinition } from "mdast";
|
||||
import type { MarkdownRuleDefinition } from "../types.js";
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* @fileoverview Rule to prevent duplicate definitions in Markdown.
|
||||
* @author 루밀LuMir(lumirlumir)
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { normalizeIdentifier } from "micromark-util-normalize-identifier";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { Definition, FootnoteDefinition } from "mdast";
|
||||
* @import { MarkdownRuleDefinition } from "../types.js";
|
||||
* @typedef {"duplicateDefinition" | "duplicateFootnoteDefinition"} NoDuplicateDefinitionsMessageIds
|
||||
* @typedef {[{ allowDefinitions?: string[], allowFootnoteDefinitions?: string[], checkFootnoteDefinitions?: boolean }]} NoDuplicateDefinitionsOptions
|
||||
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoDuplicateDefinitionsOptions, MessageIds: NoDuplicateDefinitionsMessageIds }>} NoDuplicateDefinitionsRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoDuplicateDefinitionsRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
recommended: true,
|
||||
description: "Disallow duplicate definitions",
|
||||
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-duplicate-definitions.md",
|
||||
},
|
||||
messages: {
|
||||
duplicateDefinition: "Unexpected duplicate definition `{{ identifier }}` (label: `{{ label }}`) found. First defined at line {{ firstLine }} (label: `{{ firstLabel }}`).",
|
||||
duplicateFootnoteDefinition: "Unexpected duplicate footnote definition `{{ identifier }}` (label: `{{ label }}`) found. First defined at line {{ firstLine }} (label: `{{ firstLabel }}`).",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowDefinitions: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
allowFootnoteDefinitions: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
checkFootnoteDefinitions: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
allowDefinitions: ["//"],
|
||||
allowFootnoteDefinitions: [],
|
||||
checkFootnoteDefinitions: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
create(context) {
|
||||
const allowDefinitions = new Set(context.options[0].allowDefinitions.map(identifier => normalizeIdentifier(identifier).toLowerCase()));
|
||||
const allowFootnoteDefinitions = new Set(context.options[0].allowFootnoteDefinitions.map(identifier => normalizeIdentifier(identifier).toLowerCase()));
|
||||
const [{ checkFootnoteDefinitions }] = context.options;
|
||||
/** @type {Map<string, Definition>} */
|
||||
const definitions = new Map();
|
||||
/** @type {Map<string, FootnoteDefinition>} */
|
||||
const footnoteDefinitions = new Map();
|
||||
return {
|
||||
definition(node) {
|
||||
if (allowDefinitions.has(node.identifier)) {
|
||||
return;
|
||||
}
|
||||
if (definitions.has(node.identifier)) {
|
||||
const firstDefinitionNode = definitions.get(node.identifier);
|
||||
context.report({
|
||||
node,
|
||||
messageId: "duplicateDefinition",
|
||||
data: {
|
||||
identifier: node.identifier,
|
||||
label: node.label.trim(),
|
||||
firstLine: firstDefinitionNode.position.start.line,
|
||||
firstLabel: firstDefinitionNode.label.trim(),
|
||||
},
|
||||
});
|
||||
}
|
||||
else {
|
||||
definitions.set(node.identifier, node);
|
||||
}
|
||||
},
|
||||
footnoteDefinition(node) {
|
||||
if (!checkFootnoteDefinitions ||
|
||||
allowFootnoteDefinitions.has(node.identifier)) {
|
||||
return;
|
||||
}
|
||||
if (footnoteDefinitions.has(node.identifier)) {
|
||||
const firstFootnoteDefinitionNode = footnoteDefinitions.get(node.identifier);
|
||||
context.report({
|
||||
node,
|
||||
messageId: "duplicateFootnoteDefinition",
|
||||
data: {
|
||||
identifier: node.identifier,
|
||||
label: node.label,
|
||||
firstLine: firstFootnoteDefinitionNode.position.start.line,
|
||||
firstLabel: firstFootnoteDefinitionNode.label,
|
||||
},
|
||||
});
|
||||
}
|
||||
else {
|
||||
footnoteDefinitions.set(node.identifier, node);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let duplicateHeading: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
checkSiblingsOnly: {
|
||||
type: "boolean";
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
checkSiblingsOnly: false;
|
||||
}];
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../types.js").MarkdownLanguageOptions;
|
||||
Code: import("../index.js").MarkdownSourceCode;
|
||||
RuleOptions: NoDuplicateHeadingsOptions;
|
||||
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
|
||||
MessageIds: "duplicateHeading";
|
||||
}>): {
|
||||
heading(node: import("mdast").Heading): void;
|
||||
"heading *"({ type, value }: any): void;
|
||||
"heading:exit"(node: import("mdast").Heading): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoDuplicateHeadingsMessageIds = "duplicateHeading";
|
||||
export type NoDuplicateHeadingsOptions = [{
|
||||
checkSiblingsOnly?: boolean;
|
||||
}];
|
||||
export type NoDuplicateHeadingsRuleDefinition = MarkdownRuleDefinition<{
|
||||
RuleOptions: NoDuplicateHeadingsOptions;
|
||||
MessageIds: NoDuplicateHeadingsMessageIds;
|
||||
}>;
|
||||
import type { MarkdownRuleDefinition } from "../types.js";
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @fileoverview Rule to prevent duplicate headings in Markdown.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { MarkdownRuleDefinition } from "../types.js";
|
||||
* @typedef {"duplicateHeading"} NoDuplicateHeadingsMessageIds
|
||||
* @typedef {[{ checkSiblingsOnly?: boolean }]} NoDuplicateHeadingsOptions
|
||||
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoDuplicateHeadingsOptions, MessageIds: NoDuplicateHeadingsMessageIds }>} NoDuplicateHeadingsRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoDuplicateHeadingsRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Disallow duplicate headings in the same document",
|
||||
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-duplicate-headings.md",
|
||||
},
|
||||
messages: {
|
||||
duplicateHeading: 'Duplicate heading "{{text}}" found.',
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
checkSiblingsOnly: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [{ checkSiblingsOnly: false }],
|
||||
},
|
||||
create(context) {
|
||||
const [{ checkSiblingsOnly }] = context.options;
|
||||
/** @type {Map<number, Set<string>>} */
|
||||
const headingsByLevel = checkSiblingsOnly
|
||||
? new Map([
|
||||
[1, new Set()],
|
||||
[2, new Set()],
|
||||
[3, new Set()],
|
||||
[4, new Set()],
|
||||
[5, new Set()],
|
||||
[6, new Set()],
|
||||
])
|
||||
: new Map([[1, new Set()]]);
|
||||
let lastLevel = 1;
|
||||
let currentLevelHeadings = headingsByLevel.get(lastLevel);
|
||||
/** @type {string} */
|
||||
let headingChildrenSequence;
|
||||
/** @type {string} */
|
||||
let headingText;
|
||||
return {
|
||||
heading(node) {
|
||||
headingChildrenSequence = "";
|
||||
headingText = "";
|
||||
if (checkSiblingsOnly) {
|
||||
const currentLevel = node.depth;
|
||||
if (currentLevel < lastLevel) {
|
||||
for (let level = lastLevel; level > currentLevel; level--) {
|
||||
headingsByLevel.get(level).clear();
|
||||
}
|
||||
}
|
||||
lastLevel = currentLevel;
|
||||
currentLevelHeadings = headingsByLevel.get(currentLevel);
|
||||
}
|
||||
},
|
||||
"heading *"({ type, value }) {
|
||||
if (value) {
|
||||
headingChildrenSequence += `[${type},${value}]`; // We use a custom sequence representation to keep track of heading children.
|
||||
if (type !== "html") {
|
||||
headingText += value;
|
||||
}
|
||||
}
|
||||
else {
|
||||
headingChildrenSequence += `[${type}]`;
|
||||
}
|
||||
},
|
||||
"heading:exit"(node) {
|
||||
if (currentLevelHeadings.has(headingChildrenSequence)) {
|
||||
context.report({
|
||||
loc: node.position,
|
||||
messageId: "duplicateHeading",
|
||||
data: {
|
||||
text: headingText,
|
||||
},
|
||||
});
|
||||
}
|
||||
else {
|
||||
currentLevelHeadings.add(headingChildrenSequence);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let recommended: boolean;
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let emptyDefinition: string;
|
||||
let emptyFootnoteDefinition: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
allowDefinitions: {
|
||||
type: "array";
|
||||
items: {
|
||||
type: "string";
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
allowFootnoteDefinitions: {
|
||||
type: "array";
|
||||
items: {
|
||||
type: "string";
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
checkFootnoteDefinitions: {
|
||||
type: "boolean";
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
allowDefinitions: string[];
|
||||
allowFootnoteDefinitions: any[];
|
||||
checkFootnoteDefinitions: true;
|
||||
}];
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../types.js").MarkdownLanguageOptions;
|
||||
Code: import("../index.js").MarkdownSourceCode;
|
||||
RuleOptions: NoEmptyDefinitionsOptions;
|
||||
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
|
||||
MessageIds: NoEmptyDefinitionsMessageIds;
|
||||
}>): {
|
||||
definition(node: import("mdast").Definition): void;
|
||||
footnoteDefinition(node: import("mdast").FootnoteDefinition): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoEmptyDefinitionsMessageIds = "emptyDefinition" | "emptyFootnoteDefinition";
|
||||
export type NoEmptyDefinitionsOptions = [{
|
||||
allowDefinitions?: string[];
|
||||
allowFootnoteDefinitions?: string[];
|
||||
checkFootnoteDefinitions?: boolean;
|
||||
}];
|
||||
export type NoEmptyDefinitionsRuleDefinition = MarkdownRuleDefinition<{
|
||||
RuleOptions: NoEmptyDefinitionsOptions;
|
||||
MessageIds: NoEmptyDefinitionsMessageIds;
|
||||
}>;
|
||||
import type { MarkdownRuleDefinition } from "../types.js";
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* @fileoverview Rule to prevent empty definitions in Markdown.
|
||||
* @author Pixel998
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { normalizeIdentifier } from "micromark-util-normalize-identifier";
|
||||
import { htmlCommentPattern } from "../util.js";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { MarkdownRuleDefinition } from "../types.js";
|
||||
* @typedef {"emptyDefinition" | "emptyFootnoteDefinition"} NoEmptyDefinitionsMessageIds
|
||||
* @typedef {[{ allowDefinitions?: string[], allowFootnoteDefinitions?: string[], checkFootnoteDefinitions?: boolean }]} NoEmptyDefinitionsOptions
|
||||
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoEmptyDefinitionsOptions, MessageIds: NoEmptyDefinitionsMessageIds }>} NoEmptyDefinitionsRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* Checks if a string contains only HTML comments.
|
||||
* @param {string} value The input string to check.
|
||||
* @returns {boolean} True if the string contains only HTML comments, false otherwise.
|
||||
*/
|
||||
function isOnlyComments(value) {
|
||||
const withoutComments = value.replace(htmlCommentPattern, "");
|
||||
return withoutComments.trim().length === 0;
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoEmptyDefinitionsRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
recommended: true,
|
||||
description: "Disallow empty definitions",
|
||||
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-empty-definitions.md",
|
||||
},
|
||||
messages: {
|
||||
emptyDefinition: "Unexpected empty definition `{{ identifier }}` (label: `{{ label }}`) found.",
|
||||
emptyFootnoteDefinition: "Unexpected empty footnote definition `{{ identifier }}` (label: `{{ label }}`) found.",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowDefinitions: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
allowFootnoteDefinitions: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
checkFootnoteDefinitions: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
allowDefinitions: ["//"],
|
||||
allowFootnoteDefinitions: [],
|
||||
checkFootnoteDefinitions: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
create(context) {
|
||||
const allowDefinitions = new Set(context.options[0].allowDefinitions.map(identifier => normalizeIdentifier(identifier).toLowerCase()));
|
||||
const allowFootnoteDefinitions = new Set(context.options[0].allowFootnoteDefinitions.map(identifier => normalizeIdentifier(identifier).toLowerCase()));
|
||||
const [{ checkFootnoteDefinitions }] = context.options;
|
||||
return {
|
||||
definition(node) {
|
||||
if ((!node.url || node.url === "#") &&
|
||||
!allowDefinitions.has(node.identifier)) {
|
||||
context.report({
|
||||
loc: node.position,
|
||||
messageId: "emptyDefinition",
|
||||
data: {
|
||||
identifier: node.identifier,
|
||||
label: node.label.trim(),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
footnoteDefinition(node) {
|
||||
if (checkFootnoteDefinitions &&
|
||||
!allowFootnoteDefinitions.has(node.identifier) &&
|
||||
(node.children.length === 0 ||
|
||||
node.children.every(child => child.type === "html" &&
|
||||
isOnlyComments(child.value)))) {
|
||||
context.report({
|
||||
loc: node.position,
|
||||
messageId: "emptyFootnoteDefinition",
|
||||
data: {
|
||||
identifier: node.identifier,
|
||||
label: node.label,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let recommended: boolean;
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let emptyImage: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../types.js").MarkdownLanguageOptions;
|
||||
Code: import("../index.js").MarkdownSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
|
||||
MessageIds: "emptyImage";
|
||||
}>): {
|
||||
image(node: import("mdast").Image): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoEmptyImagesMessageIds = "emptyImage";
|
||||
export type NoEmptyImagesOptions = [];
|
||||
export type NoEmptyImagesRuleDefinition = MarkdownRuleDefinition<{
|
||||
RuleOptions: NoEmptyImagesOptions;
|
||||
MessageIds: NoEmptyImagesMessageIds;
|
||||
}>;
|
||||
import type { MarkdownRuleDefinition } from "../types.js";
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @fileoverview Rule to prevent empty images in Markdown.
|
||||
* @author 루밀LuMir(lumirlumir)
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { MarkdownRuleDefinition } from "../types.js";
|
||||
* @typedef {"emptyImage"} NoEmptyImagesMessageIds
|
||||
* @typedef {[]} NoEmptyImagesOptions
|
||||
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoEmptyImagesOptions, MessageIds: NoEmptyImagesMessageIds }>} NoEmptyImagesRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoEmptyImagesRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
recommended: true,
|
||||
description: "Disallow empty images",
|
||||
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-empty-images.md",
|
||||
},
|
||||
messages: {
|
||||
emptyImage: "Unexpected empty image found.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
image(node) {
|
||||
if (!node.url || node.url === "#") {
|
||||
context.report({
|
||||
loc: node.position,
|
||||
messageId: "emptyImage",
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let recommended: boolean;
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let emptyLink: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../types.js").MarkdownLanguageOptions;
|
||||
Code: import("../index.js").MarkdownSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
|
||||
MessageIds: "emptyLink";
|
||||
}>): {
|
||||
link(node: import("mdast").Link): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoEmptyLinksMessageIds = "emptyLink";
|
||||
export type NoEmptyLinksOptions = [];
|
||||
export type NoEmptyLinksRuleDefinition = MarkdownRuleDefinition<{
|
||||
RuleOptions: NoEmptyLinksOptions;
|
||||
MessageIds: NoEmptyLinksMessageIds;
|
||||
}>;
|
||||
import type { MarkdownRuleDefinition } from "../types.js";
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @fileoverview Rule to prevent empty links in Markdown.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { MarkdownRuleDefinition } from "../types.js";
|
||||
* @typedef {"emptyLink"} NoEmptyLinksMessageIds
|
||||
* @typedef {[]} NoEmptyLinksOptions
|
||||
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoEmptyLinksOptions, MessageIds: NoEmptyLinksMessageIds }>} NoEmptyLinksRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoEmptyLinksRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
recommended: true,
|
||||
description: "Disallow empty links",
|
||||
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-empty-links.md",
|
||||
},
|
||||
messages: {
|
||||
emptyLink: "Unexpected empty link found.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
link(node) {
|
||||
if (!node.url || node.url === "#") {
|
||||
context.report({
|
||||
loc: node.position,
|
||||
messageId: "emptyLink",
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let disallowedElement: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
allowed: {
|
||||
type: "array";
|
||||
items: {
|
||||
type: "string";
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
allowedIgnoreCase: {
|
||||
type: "boolean";
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
allowed: any[];
|
||||
allowedIgnoreCase: false;
|
||||
}];
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../types.js").MarkdownLanguageOptions;
|
||||
Code: import("../index.js").MarkdownSourceCode;
|
||||
RuleOptions: NoHtmlOptions;
|
||||
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
|
||||
MessageIds: "disallowedElement";
|
||||
}>): {
|
||||
html(node: import("mdast").Html): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoHtmlMessageIds = "disallowedElement";
|
||||
export type NoHtmlOptions = [{
|
||||
allowed?: string[];
|
||||
allowedIgnoreCase?: boolean;
|
||||
}];
|
||||
export type NoHtmlRuleDefinition = MarkdownRuleDefinition<{
|
||||
RuleOptions: NoHtmlOptions;
|
||||
MessageIds: NoHtmlMessageIds;
|
||||
}>;
|
||||
import type { MarkdownRuleDefinition } from "../types.js";
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow HTML inside of content.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { lineEndingPattern, stripHtmlComments } from "../util.js";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { MarkdownRuleDefinition } from "../types.js";
|
||||
* @typedef {"disallowedElement"} NoHtmlMessageIds
|
||||
* @typedef {[{ allowed?: string[], allowedIgnoreCase?: boolean }]} NoHtmlOptions
|
||||
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoHtmlOptions, MessageIds: NoHtmlMessageIds }>} NoHtmlRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
const htmlTagPattern = /<(?<tagName>[a-z0-9]+(?:-[a-z0-9]+)*)(?:\s(?:[^>"']|"[^"]*"|'[^']*')*)?\/?>/giu;
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoHtmlRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Disallow HTML tags",
|
||||
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-html.md",
|
||||
},
|
||||
messages: {
|
||||
disallowedElement: 'HTML element "{{name}}" is not allowed.',
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowed: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
allowedIgnoreCase: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
allowed: [],
|
||||
allowedIgnoreCase: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
const [{ allowed, allowedIgnoreCase }] = context.options;
|
||||
/**
|
||||
* Normalize a tag name based on the `allowedIgnoreCase` option.
|
||||
* @param {string} tagName The tag name to normalize.
|
||||
* @returns {string} The normalized tag name.
|
||||
*/
|
||||
function normalizeTagName(tagName) {
|
||||
return allowedIgnoreCase ? tagName.toLowerCase() : tagName;
|
||||
}
|
||||
const allowedElements = new Set(allowed.map(normalizeTagName));
|
||||
return {
|
||||
html(node) {
|
||||
const text = stripHtmlComments(sourceCode.getText(node));
|
||||
/** @type {RegExpExecArray | null} */
|
||||
let match;
|
||||
while ((match = htmlTagPattern.exec(text)) !== null) {
|
||||
const fullMatch = match[0];
|
||||
const { tagName } = match.groups;
|
||||
const firstNewlineIndex = fullMatch.search(lineEndingPattern);
|
||||
const startOffset = // Adjust `htmlTagPattern` match index to the full source code.
|
||||
match.index + node.position.start.offset;
|
||||
const endOffset = startOffset +
|
||||
(firstNewlineIndex === -1
|
||||
? fullMatch.length
|
||||
: firstNewlineIndex);
|
||||
if (!allowedElements.has(normalizeTagName(tagName))) {
|
||||
context.report({
|
||||
loc: {
|
||||
start: sourceCode.getLocFromIndex(startOffset),
|
||||
end: sourceCode.getLocFromIndex(endOffset),
|
||||
},
|
||||
messageId: "disallowedElement",
|
||||
data: {
|
||||
name: tagName,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let recommended: boolean;
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let invalidLabelRef: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../types.js").MarkdownLanguageOptions;
|
||||
Code: MarkdownSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
|
||||
MessageIds: "invalidLabelRef";
|
||||
}>): {
|
||||
text(node: Text): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoInvalidLabelRefsMessageIds = "invalidLabelRef";
|
||||
export type NoInvalidLabelRefsOptions = [];
|
||||
export type NoInvalidLabelRefsRuleDefinition = MarkdownRuleDefinition<{
|
||||
RuleOptions: NoInvalidLabelRefsOptions;
|
||||
MessageIds: NoInvalidLabelRefsMessageIds;
|
||||
}>;
|
||||
import type { MarkdownSourceCode } from "../language/markdown-source-code.js";
|
||||
import type { Text } from "mdast";
|
||||
import type { MarkdownRuleDefinition } from "../types.js";
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @fileoverview Rule to prevent non-complaint link references.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { illegalShorthandTailPattern } from "../util.js";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { Position } from "unist";
|
||||
* @import { Text } from "mdast";
|
||||
* @import { MarkdownRuleDefinition } from "../types.js";
|
||||
* @import { MarkdownSourceCode } from "../language/markdown-source-code.js";
|
||||
* @typedef {"invalidLabelRef"} NoInvalidLabelRefsMessageIds
|
||||
* @typedef {[]} NoInvalidLabelRefsOptions
|
||||
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoInvalidLabelRefsOptions, MessageIds: NoInvalidLabelRefsMessageIds }>} NoInvalidLabelRefsRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
/** matches i.e., `[foo][bar]` */
|
||||
const labelPattern = /\]\[([^\]]+)\]/u;
|
||||
/**
|
||||
* Finds missing references in a node.
|
||||
* @param {Text} node The node to check.
|
||||
* @param {MarkdownSourceCode} sourceCode The Markdown source code object.
|
||||
* @returns {Array<{label:string,position:Position}>} The missing references.
|
||||
*/
|
||||
function findInvalidLabelReferences(node, sourceCode) {
|
||||
const nodeText = sourceCode.getText(node);
|
||||
const docText = sourceCode.text;
|
||||
const invalid = [];
|
||||
let startIndex = 0;
|
||||
/*
|
||||
* This loop works by searching the string inside the node for the next
|
||||
* label reference. If it finds one, it checks to see if there is any
|
||||
* white space between the [ and ]. If there is, it reports an error.
|
||||
* It then moves the start index to the end of the label reference and
|
||||
* continues searching the text until the end of the text is found.
|
||||
*/
|
||||
while (startIndex < nodeText.length) {
|
||||
const value = nodeText.slice(startIndex);
|
||||
const match = value.match(labelPattern);
|
||||
if (!match) {
|
||||
break;
|
||||
}
|
||||
if (!illegalShorthandTailPattern.test(match[0])) {
|
||||
startIndex += match.index + match[0].length;
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* Adjust `labelPattern` match index to the full source code.
|
||||
*/
|
||||
const startOffset = startIndex + match.index + node.position.start.offset;
|
||||
const endOffset = startOffset + match[0].length;
|
||||
/*
|
||||
* Search the entire document text to find the preceding open bracket.
|
||||
*/
|
||||
const lastOpenBracketIndex = docText.lastIndexOf("[", startOffset);
|
||||
if (lastOpenBracketIndex === -1) {
|
||||
startIndex += match.index + match[0].length;
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* Note: `label` can contain leading and trailing newlines, so we need to
|
||||
* take that into account when calculating the line and column offsets.
|
||||
*/
|
||||
const label = docText
|
||||
.slice(lastOpenBracketIndex, endOffset)
|
||||
.match(/!?\[([^\]]+)\]/u)[1];
|
||||
invalid.push({
|
||||
label: label.trim(),
|
||||
position: {
|
||||
start: sourceCode.getLocFromIndex(startOffset + 1),
|
||||
end: sourceCode.getLocFromIndex(endOffset),
|
||||
},
|
||||
});
|
||||
startIndex += match.index + match[0].length;
|
||||
}
|
||||
return invalid;
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoInvalidLabelRefsRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
recommended: true,
|
||||
description: "Disallow invalid label references",
|
||||
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-invalid-label-refs.md",
|
||||
},
|
||||
messages: {
|
||||
invalidLabelRef: "Label reference '{{label}}' is invalid due to white space between [ and ].",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
return {
|
||||
text(node) {
|
||||
const invalidReferences = findInvalidLabelReferences(node, sourceCode);
|
||||
for (const invalidReference of invalidReferences) {
|
||||
context.report({
|
||||
loc: invalidReference.position,
|
||||
messageId: "invalidLabelRef",
|
||||
data: {
|
||||
label: invalidReference.label,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
Generated
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let recommended: boolean;
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
let fixable: "whitespace";
|
||||
namespace messages {
|
||||
let missingSpace: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
checkClosedHeadings: {
|
||||
type: "boolean";
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
checkClosedHeadings: false;
|
||||
}];
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../types.js").MarkdownLanguageOptions;
|
||||
Code: import("../index.js").MarkdownSourceCode;
|
||||
RuleOptions: NoMissingAtxHeadingSpaceOptions;
|
||||
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
|
||||
MessageIds: "missingSpace";
|
||||
}>): {
|
||||
heading(node: import("mdast").Heading): void;
|
||||
paragraph(node: import("mdast").Paragraph): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoMissingAtxHeadingSpaceMessageIds = "missingSpace";
|
||||
export type NoMissingAtxHeadingSpaceOptions = [{
|
||||
checkClosedHeadings?: boolean;
|
||||
}];
|
||||
export type NoMissingAtxHeadingSpaceRuleDefinition = MarkdownRuleDefinition<{
|
||||
RuleOptions: NoMissingAtxHeadingSpaceOptions;
|
||||
MessageIds: NoMissingAtxHeadingSpaceMessageIds;
|
||||
}>;
|
||||
import type { MarkdownRuleDefinition } from "../types.js";
|
||||
Generated
Vendored
+101
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @fileoverview Rule to ensure there is a space after hash on ATX style headings in Markdown.
|
||||
* @author Sweta Tanwar (@SwetaTanwar)
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { MarkdownRuleDefinition } from "../types.js";
|
||||
* @typedef {"missingSpace"} NoMissingAtxHeadingSpaceMessageIds
|
||||
* @typedef {[{ checkClosedHeadings?: boolean }]} NoMissingAtxHeadingSpaceOptions
|
||||
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoMissingAtxHeadingSpaceOptions, MessageIds: NoMissingAtxHeadingSpaceMessageIds }>} NoMissingAtxHeadingSpaceRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
const leadingAtxHeadingHashPattern = /(?:^|(?<=[\r\n]))(?<hashes>#{1,6})(?:[^# \t]|$)/gu;
|
||||
const trailingAtxHeadingHashPattern = /(?<![ \t])(?<spaces>[ \t]*)(?<=(?<!\\)(?:\\{2})*)(?<hashes>#+)[ \t]*$/u;
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoMissingAtxHeadingSpaceRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
recommended: true,
|
||||
description: "Disallow headings without a space after the hash characters",
|
||||
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-missing-atx-heading-space.md",
|
||||
},
|
||||
fixable: "whitespace",
|
||||
messages: {
|
||||
missingSpace: "Missing space {{position}} hash(es) on ATX style heading.",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
checkClosedHeadings: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [{ checkClosedHeadings: false }],
|
||||
},
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
const [{ checkClosedHeadings }] = context.options;
|
||||
return {
|
||||
heading(node) {
|
||||
if (!checkClosedHeadings) {
|
||||
return;
|
||||
}
|
||||
const text = sourceCode.getText(node);
|
||||
const match = trailingAtxHeadingHashPattern.exec(text);
|
||||
if (match === null) {
|
||||
return;
|
||||
}
|
||||
const { spaces, hashes } = match.groups;
|
||||
if (spaces.length > 0) {
|
||||
return;
|
||||
}
|
||||
const startOffset = node.position.start.offset + match.index;
|
||||
const endOffset = startOffset + hashes.length;
|
||||
context.report({
|
||||
loc: {
|
||||
start: sourceCode.getLocFromIndex(startOffset - 1),
|
||||
end: sourceCode.getLocFromIndex(endOffset),
|
||||
},
|
||||
messageId: "missingSpace",
|
||||
data: { position: "before" },
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBeforeRange([startOffset, startOffset + 1], " ");
|
||||
},
|
||||
});
|
||||
},
|
||||
paragraph(node) {
|
||||
const text = sourceCode.getText(node);
|
||||
/** @type {RegExpExecArray | null} */
|
||||
let match;
|
||||
while ((match = leadingAtxHeadingHashPattern.exec(text)) !== null) {
|
||||
const { hashes } = match.groups;
|
||||
const startOffset = node.position.start.offset + match.index;
|
||||
const endOffset = startOffset + hashes.length;
|
||||
context.report({
|
||||
loc: {
|
||||
start: sourceCode.getLocFromIndex(startOffset),
|
||||
end: sourceCode.getLocFromIndex(endOffset + 1),
|
||||
},
|
||||
messageId: "missingSpace",
|
||||
data: { position: "after" },
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfterRange([endOffset - 1, endOffset], " ");
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let recommended: boolean;
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
allowLabels: {
|
||||
type: "array";
|
||||
items: {
|
||||
type: "string";
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
allowLabels: any[];
|
||||
}];
|
||||
namespace messages {
|
||||
let notFound: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../types.js").MarkdownLanguageOptions;
|
||||
Code: MarkdownSourceCode;
|
||||
RuleOptions: NoMissingLabelRefsOptions;
|
||||
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
|
||||
MessageIds: "notFound";
|
||||
}>): {
|
||||
"root:exit"(): void;
|
||||
text(node: Text): void;
|
||||
definition(node: import("mdast").Definition): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoMissingLabelRefsMessageIds = "notFound";
|
||||
export type NoMissingLabelRefsOptions = [{
|
||||
allowLabels?: string[];
|
||||
}];
|
||||
export type NoMissingLabelRefsRuleDefinition = MarkdownRuleDefinition<{
|
||||
RuleOptions: NoMissingLabelRefsOptions;
|
||||
MessageIds: NoMissingLabelRefsMessageIds;
|
||||
}>;
|
||||
import type { MarkdownSourceCode } from "../language/markdown-source-code.js";
|
||||
import type { Text } from "mdast";
|
||||
import type { MarkdownRuleDefinition } from "../types.js";
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* @fileoverview Rule to prevent missing label references in Markdown.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { illegalShorthandTailPattern } from "../util.js";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { Position } from "unist";
|
||||
* @import { Text } from "mdast";
|
||||
* @import { MarkdownRuleDefinition } from "../types.js";
|
||||
* @import { MarkdownSourceCode } from "../language/markdown-source-code.js";
|
||||
* @typedef {"notFound"} NoMissingLabelRefsMessageIds
|
||||
* @typedef {[{ allowLabels?: string[] }]} NoMissingLabelRefsOptions
|
||||
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoMissingLabelRefsOptions, MessageIds: NoMissingLabelRefsMessageIds }>} NoMissingLabelRefsRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* Finds missing references in a node.
|
||||
* @param {Text} node The node to check.
|
||||
* @param {MarkdownSourceCode} sourceCode The Markdown source code object.
|
||||
* @returns {Array<{label:string,position:Position}>} The missing references.
|
||||
*/
|
||||
function findMissingReferences(node, sourceCode) {
|
||||
/** @type {Array<{label:string,position:Position}>} */
|
||||
const missing = [];
|
||||
const nodeText = sourceCode.getText(node);
|
||||
/**
|
||||
* Matches substrings like `"[foo]"`, `"[]"`, `"[foo][bar]"`, `"[foo][]"`, `"[][bar]"`, or `"[][]"`.
|
||||
* `left` is the content between the first brackets. It can be empty.
|
||||
* `right` is the content between the second brackets. It can be empty, and it can be undefined.
|
||||
*/
|
||||
const labelPattern = /(?<=(?<!\\)(?:\\{2})*)\[(?<left>(?:\\.|[^[\]\\])*)\](?:\[(?<right>(?:\\.|[^\]\\])*)\])?/dgu;
|
||||
/** @type {RegExpExecArray | null} */
|
||||
let match;
|
||||
/*
|
||||
* This loop searches the text inside the node for sequences that
|
||||
* look like label references and reports an error for each one found.
|
||||
*/
|
||||
while ((match = labelPattern.exec(nodeText))) {
|
||||
// skip illegal shorthand tail -- handled by no-invalid-label-refs
|
||||
if (illegalShorthandTailPattern.test(match[0])) {
|
||||
continue;
|
||||
}
|
||||
const { left, right } = match.groups;
|
||||
// `[][]` or `[]`
|
||||
if (!left && !right) {
|
||||
continue;
|
||||
}
|
||||
let label, labelIndices;
|
||||
if (right) {
|
||||
label = right;
|
||||
labelIndices = match.indices.groups.right;
|
||||
}
|
||||
else {
|
||||
label = left;
|
||||
labelIndices = match.indices.groups.left;
|
||||
}
|
||||
const startOffset = labelIndices[0] + node.position.start.offset;
|
||||
const endOffset = labelIndices[1] + node.position.start.offset;
|
||||
missing.push({
|
||||
label: label.trim(),
|
||||
position: {
|
||||
start: sourceCode.getLocFromIndex(startOffset),
|
||||
end: sourceCode.getLocFromIndex(endOffset),
|
||||
},
|
||||
});
|
||||
}
|
||||
return missing;
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoMissingLabelRefsRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
recommended: true,
|
||||
description: "Disallow missing label references",
|
||||
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-missing-label-refs.md",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowLabels: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
allowLabels: [],
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
notFound: "Label reference '{{label}}' not found.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
const allowLabels = new Set(context.options[0].allowLabels);
|
||||
/** @type {Array<{label:string,position:Position}>} */
|
||||
let allMissingReferences = [];
|
||||
return {
|
||||
"root:exit"() {
|
||||
for (const missingReference of allMissingReferences) {
|
||||
context.report({
|
||||
loc: missingReference.position,
|
||||
messageId: "notFound",
|
||||
data: {
|
||||
label: missingReference.label,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
text(node) {
|
||||
const missingReferences = findMissingReferences(node, sourceCode);
|
||||
for (const missingReference of missingReferences) {
|
||||
if (!allowLabels.has(missingReference.label)) {
|
||||
allMissingReferences.push(missingReference);
|
||||
}
|
||||
}
|
||||
},
|
||||
definition(node) {
|
||||
/*
|
||||
* Sometimes a poorly-formatted link will end up a text node instead of a link node
|
||||
* even though the label definition exists. Here, we remove any missing references
|
||||
* that have a matching label definition.
|
||||
*/
|
||||
allMissingReferences = allMissingReferences.filter(missingReference => missingReference.label !== node.identifier);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
Generated
Vendored
+56
@@ -0,0 +1,56 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let recommended: boolean;
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
ignoreCase: {
|
||||
type: "boolean";
|
||||
};
|
||||
allowPattern: {
|
||||
type: "string";
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
namespace messages {
|
||||
let invalidFragment: string;
|
||||
}
|
||||
let defaultOptions: [{
|
||||
ignoreCase: true;
|
||||
allowPattern: string;
|
||||
}];
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../types.js").MarkdownLanguageOptions;
|
||||
Code: import("../index.js").MarkdownSourceCode;
|
||||
RuleOptions: NoMissingLinkFragmentsOptions;
|
||||
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
|
||||
MessageIds: "invalidFragment";
|
||||
}>): {
|
||||
heading(): void;
|
||||
"heading *:not(html)"({ value }: any): void;
|
||||
"heading:exit"(): void;
|
||||
html(node: import("mdast").Html): void;
|
||||
"definition, link"(node: Definition | Link): void;
|
||||
"root:exit"(): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoMissingLinkFragmentsMessageIds = "invalidFragment";
|
||||
export type NoMissingLinkFragmentsOptions = [{
|
||||
ignoreCase?: boolean;
|
||||
allowPattern?: string;
|
||||
}];
|
||||
export type NoMissingLinkFragmentsRuleDefinition = MarkdownRuleDefinition<{
|
||||
RuleOptions: NoMissingLinkFragmentsOptions;
|
||||
MessageIds: NoMissingLinkFragmentsMessageIds;
|
||||
}>;
|
||||
import type { Definition } from "mdast";
|
||||
import type { Link } from "mdast";
|
||||
import type { MarkdownRuleDefinition } from "../types.js";
|
||||
Generated
Vendored
+141
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* @fileoverview Rule to ensure link fragments (URLs that start with #) reference valid headings
|
||||
* @author Sweta Tanwar (@SwetaTanwar)
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import GithubSlugger from "github-slugger";
|
||||
import { stripHtmlComments } from "../util.js";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { Definition, Link } from "mdast";
|
||||
* @import { MarkdownRuleDefinition } from "../types.js";
|
||||
* @typedef {"invalidFragment"} NoMissingLinkFragmentsMessageIds
|
||||
* @typedef {[{ ignoreCase?: boolean; allowPattern?: string }]} NoMissingLinkFragmentsOptions
|
||||
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoMissingLinkFragmentsOptions, MessageIds: NoMissingLinkFragmentsMessageIds }>} NoMissingLinkFragmentsRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
const githubLineReferencePattern = /^L\d+(?:C\d+)?(?:-L\d+(?:C\d+)?)?$/u;
|
||||
const customHeadingIdPattern = /\{#(?<id>[^}\s]+)\}\s*$/u;
|
||||
const htmlHeadingPattern = /<h(?<depth>[1-6])[^>]*>(?<children>[\s\S]*?)<\/h\k<depth>\s*>/giu;
|
||||
const htmlIdNamePattern = /(?<!<)<[^>]+\s(?:id|name)\s*=\s*["']?(?<id>[^"'\s>]+)["']?/giu;
|
||||
const htmlTagPattern = /<\/?[a-z0-9]+(?:-[a-z0-9]+)*(?:\s(?:[^>"']|"[^"]*"|'[^']*')*)?(?:\/\s*)?>/giu;
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoMissingLinkFragmentsRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
recommended: true,
|
||||
description: "Disallow link fragments that do not reference valid headings",
|
||||
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-missing-link-fragments.md",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
ignoreCase: {
|
||||
type: "boolean",
|
||||
},
|
||||
allowPattern: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
invalidFragment: "Link fragment '#{{fragment}}' does not reference a heading or anchor in this document.",
|
||||
},
|
||||
defaultOptions: [
|
||||
{
|
||||
ignoreCase: true,
|
||||
allowPattern: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
create(context) {
|
||||
const [{ allowPattern, ignoreCase }] = context.options;
|
||||
const allowPatternOrNull = allowPattern
|
||||
? new RegExp(allowPattern, "u")
|
||||
: null;
|
||||
const fragmentIds = new Set(["top"]);
|
||||
const slugger = new GithubSlugger();
|
||||
/** @type {Array<Definition | Link>} */
|
||||
const relevantNodes = [];
|
||||
/** @type {string} */
|
||||
let headingText;
|
||||
return {
|
||||
heading() {
|
||||
headingText = "";
|
||||
},
|
||||
"heading *:not(html)"({ value }) {
|
||||
headingText += value ?? "";
|
||||
},
|
||||
"heading:exit"() {
|
||||
const customIdMatch = headingText.match(customHeadingIdPattern);
|
||||
const id = customIdMatch
|
||||
? customIdMatch.groups.id
|
||||
: headingText;
|
||||
fragmentIds.add(slugger.slug(id));
|
||||
},
|
||||
html(node) {
|
||||
// 1. Remove all comments
|
||||
const htmlTextWithoutComments = stripHtmlComments(node.value);
|
||||
// 2. Then look for IDs in the remaining text
|
||||
for (const match of htmlTextWithoutComments.matchAll(htmlIdNamePattern)) {
|
||||
const { id } = match.groups;
|
||||
fragmentIds.add(slugger.slug(id));
|
||||
}
|
||||
// 3. Finally, look for headings in the HTML
|
||||
for (const match of htmlTextWithoutComments.matchAll(htmlHeadingPattern)) {
|
||||
const { children } = match.groups;
|
||||
// Remove any HTML tags within the heading content to get plain text
|
||||
const id = children.replace(htmlTagPattern, "");
|
||||
fragmentIds.add(slugger.slug(id));
|
||||
}
|
||||
},
|
||||
"definition, link"(/** @type {Definition | Link} */ node) {
|
||||
const { url } = node;
|
||||
// If `url` is empty, `"#"`, or does not start with `"#"`, skip it.
|
||||
if (url === "" || url === "#" || !url.startsWith("#")) {
|
||||
return;
|
||||
}
|
||||
relevantNodes.push(node);
|
||||
},
|
||||
"root:exit"() {
|
||||
for (const node of relevantNodes) {
|
||||
const fragment = node.url.slice(1);
|
||||
let decodedFragment = fragment;
|
||||
// Decode URI component to handle encoded characters such as `%20`.
|
||||
try {
|
||||
decodedFragment = decodeURIComponent(fragment);
|
||||
}
|
||||
catch {
|
||||
// If decoding fails due to an invalid URI sequence, use the original fragment.
|
||||
}
|
||||
if (allowPatternOrNull?.test(decodedFragment) ||
|
||||
githubLineReferencePattern.test(decodedFragment)) {
|
||||
continue;
|
||||
}
|
||||
const normalizedFragment = ignoreCase
|
||||
? decodedFragment.toLowerCase()
|
||||
: decodedFragment;
|
||||
if (!fragmentIds.has(normalizedFragment)) {
|
||||
context.report({
|
||||
loc: node.position,
|
||||
messageId: "invalidFragment",
|
||||
data: { fragment },
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let recommended: boolean;
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let multipleH1: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
frontmatterTitle: {
|
||||
type: "string";
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
frontmatterTitle: string;
|
||||
}];
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../types.js").MarkdownLanguageOptions;
|
||||
Code: import("../index.js").MarkdownSourceCode;
|
||||
RuleOptions: NoMultipleH1Options;
|
||||
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
|
||||
MessageIds: "multipleH1";
|
||||
}>): {
|
||||
"yaml, toml, json"(node: Yaml | Toml | Json): void;
|
||||
html(node: import("mdast").Html): void;
|
||||
heading(node: import("mdast").Heading): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoMultipleH1MessageIds = "multipleH1";
|
||||
export type NoMultipleH1Options = [{
|
||||
frontmatterTitle?: string;
|
||||
}];
|
||||
export type NoMultipleH1RuleDefinition = MarkdownRuleDefinition<{
|
||||
RuleOptions: NoMultipleH1Options;
|
||||
MessageIds: NoMultipleH1MessageIds;
|
||||
}>;
|
||||
import type { Yaml } from "mdast";
|
||||
import type { Toml } from "../types.js";
|
||||
import type { Json } from "../types.js";
|
||||
import type { MarkdownRuleDefinition } from "../types.js";
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce at most one H1 heading in Markdown.
|
||||
* @author Pixel998
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { frontmatterHasTitle, stripHtmlComments } from "../util.js";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { Yaml } from "mdast";
|
||||
* @import { MarkdownRuleDefinition, Toml, Json } from "../types.js";
|
||||
* @typedef {"multipleH1"} NoMultipleH1MessageIds
|
||||
* @typedef {[{ frontmatterTitle?: string }]} NoMultipleH1Options
|
||||
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoMultipleH1Options, MessageIds: NoMultipleH1MessageIds }>} NoMultipleH1RuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
const h1TagPattern = /<h1[^>]*>[\s\S]*?<\/h1\s*>/giu;
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoMultipleH1RuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
recommended: true,
|
||||
description: "Disallow multiple H1 headings in the same document",
|
||||
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-multiple-h1.md",
|
||||
},
|
||||
messages: {
|
||||
multipleH1: "Unexpected additional H1 heading found.",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
frontmatterTitle: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
frontmatterTitle: "^(?!\\s*['\"]title[:=]['\"])\\s*\\{?\\s*['\"]?title['\"]?\\s*[:=]",
|
||||
},
|
||||
],
|
||||
},
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
const [{ frontmatterTitle }] = context.options;
|
||||
const titlePattern = frontmatterTitle === "" ? null : new RegExp(frontmatterTitle, "iu");
|
||||
let h1Count = 0;
|
||||
return {
|
||||
"yaml, toml, json"(/** @type {Yaml | Toml | Json} */ node) {
|
||||
if (frontmatterHasTitle(node.value, titlePattern)) {
|
||||
h1Count++;
|
||||
}
|
||||
},
|
||||
html(node) {
|
||||
const text = stripHtmlComments(node.value);
|
||||
/** @type {RegExpExecArray | null} */
|
||||
let match;
|
||||
while ((match = h1TagPattern.exec(text)) !== null) {
|
||||
h1Count++;
|
||||
if (h1Count > 1) {
|
||||
const startOffset = // Adjust `h1TagPattern` match index to the full source code.
|
||||
match.index + node.position.start.offset;
|
||||
const endOffset = startOffset + match[0].length;
|
||||
context.report({
|
||||
loc: {
|
||||
start: sourceCode.getLocFromIndex(startOffset),
|
||||
end: sourceCode.getLocFromIndex(endOffset),
|
||||
},
|
||||
messageId: "multipleH1",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
heading(node) {
|
||||
if (node.depth === 1) {
|
||||
h1Count++;
|
||||
if (h1Count > 1) {
|
||||
context.report({
|
||||
loc: node.position,
|
||||
messageId: "multipleH1",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let recommended: boolean;
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
let fixable: "code";
|
||||
namespace messages {
|
||||
let referenceLikeUrl: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../types.js").MarkdownLanguageOptions;
|
||||
Code: import("../index.js").MarkdownSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
|
||||
MessageIds: "referenceLikeUrl";
|
||||
}>): {
|
||||
definition(node: import("mdast").Definition): void;
|
||||
"image, link"(node: Image | Link): void;
|
||||
"root:exit"(): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoReferenceLikeUrlsMessageIds = "referenceLikeUrl";
|
||||
export type NoReferenceLikeUrlsOptions = [];
|
||||
export type NoReferenceLikeUrlsRuleDefinition = MarkdownRuleDefinition<{
|
||||
RuleOptions: NoReferenceLikeUrlsOptions;
|
||||
MessageIds: NoReferenceLikeUrlsMessageIds;
|
||||
}>;
|
||||
import type { Image } from "mdast";
|
||||
import type { Link } from "mdast";
|
||||
import type { MarkdownRuleDefinition } from "../types.js";
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce reference-style links when URL matches a defined identifier.
|
||||
* @author TKDev7
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { normalizeIdentifier } from "micromark-util-normalize-identifier";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { Image, Link } from "mdast";
|
||||
* @import { MarkdownRuleDefinition } from "../types.js";
|
||||
* @typedef {"referenceLikeUrl"} NoReferenceLikeUrlsMessageIds
|
||||
* @typedef {[]} NoReferenceLikeUrlsOptions
|
||||
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoReferenceLikeUrlsOptions, MessageIds: NoReferenceLikeUrlsMessageIds }>} NoReferenceLikeUrlsRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
/** Pattern to match both inline links: `[text](url)` and images: ``, with optional title */
|
||||
const linkOrImagePattern = /\[(?<label>(?:\\.|[^()\\]|\([\s\S]*\))*?)\]\((?<destination>[ \t]*\r?\n?(?<![ \t])[ \t]*(?:<[^>]*>|[^ \t()]+))(?:[ \t]*\r?\n?(?<![ \t])[ \t]*(?:"[^"]*"|'[^']*'|\([^)]*\)))?[ \t]*\r?\n?(?<![ \t])[ \t]*\)$/u;
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoReferenceLikeUrlsRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
recommended: true,
|
||||
description: "Disallow URLs that match defined reference identifiers",
|
||||
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-reference-like-urls.md",
|
||||
},
|
||||
fixable: "code",
|
||||
messages: {
|
||||
referenceLikeUrl: "Unexpected resource {{type}} ('{{prefix}}[text](url)') with URL that matches a definition identifier. Use '[text][id]' syntax instead.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
/** @type {Set<string>} */
|
||||
const definitionIdentifiers = new Set();
|
||||
/** @type {Array<Image | Link>} */
|
||||
const relevantNodes = [];
|
||||
return {
|
||||
definition(node) {
|
||||
definitionIdentifiers.add(node.identifier);
|
||||
},
|
||||
"image, link"(/** @type {Image | Link} */ node) {
|
||||
relevantNodes.push(node);
|
||||
},
|
||||
"root:exit"() {
|
||||
for (const node of relevantNodes) {
|
||||
const text = sourceCode.getText(node);
|
||||
const match = linkOrImagePattern.exec(text);
|
||||
if (match !== null) {
|
||||
const { label, destination } = match.groups;
|
||||
const { type, title } = node;
|
||||
const prefix = type === "image" ? "!" : "";
|
||||
const url = normalizeIdentifier(destination).toLowerCase();
|
||||
if (definitionIdentifiers.has(url)) {
|
||||
context.report({
|
||||
loc: node.position,
|
||||
messageId: "referenceLikeUrl",
|
||||
data: {
|
||||
type,
|
||||
prefix,
|
||||
},
|
||||
fix(fixer) {
|
||||
// The AST treats both missing and empty titles as null, so it's safe to auto-fix in both cases.
|
||||
if (title) {
|
||||
return null;
|
||||
}
|
||||
return fixer.replaceText(node, `${prefix}[${label}][${destination}]`);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
Generated
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let recommended: boolean;
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
let fixable: "code";
|
||||
namespace messages {
|
||||
let reversedSyntax: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../types.js").MarkdownLanguageOptions;
|
||||
Code: import("../index.js").MarkdownSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
|
||||
MessageIds: "reversedSyntax";
|
||||
}>): {
|
||||
"heading, paragraph, tableCell"(node: Heading | Paragraph | TableCell): void;
|
||||
":matches(heading, paragraph, tableCell) :matches(html, image, imageReference, inlineCode, linkReference, inlineMath)"(node: Html | Image | ImageReference | InlineCode | LinkReference | InlineMath): void;
|
||||
":matches(heading, paragraph, tableCell):exit"(): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoReversedMediaSyntaxMessageIds = "reversedSyntax";
|
||||
export type NoReversedMediaSyntaxOptions = [];
|
||||
export type NoReversedMediaSyntaxRuleDefinition = MarkdownRuleDefinition<{
|
||||
RuleOptions: NoReversedMediaSyntaxOptions;
|
||||
MessageIds: NoReversedMediaSyntaxMessageIds;
|
||||
}>;
|
||||
import type { Heading } from "mdast";
|
||||
import type { Paragraph } from "mdast";
|
||||
import type { TableCell } from "mdast";
|
||||
import type { Html } from "mdast";
|
||||
import type { Image } from "mdast";
|
||||
import type { ImageReference } from "mdast";
|
||||
import type { InlineCode } from "mdast";
|
||||
import type { LinkReference } from "mdast";
|
||||
import type { InlineMath } from "mdast-util-math";
|
||||
import type { MarkdownRuleDefinition } from "../types.js";
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* @fileoverview Rule to prevent reversed link and image syntax in Markdown.
|
||||
* @author xbinaryx
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { Heading, Paragraph, TableCell, Html, Image, ImageReference, InlineCode, LinkReference } from "mdast";
|
||||
* @import { InlineMath } from "mdast-util-math";
|
||||
* @import { MarkdownRuleDefinition } from "../types.js";
|
||||
* @typedef {"reversedSyntax"} NoReversedMediaSyntaxMessageIds
|
||||
* @typedef {[]} NoReversedMediaSyntaxOptions
|
||||
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoReversedMediaSyntaxOptions, MessageIds: NoReversedMediaSyntaxMessageIds }>} NoReversedMediaSyntaxRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
/** Matches reversed link/image syntax like `(text)[url]`, ignoring escaped characters like `\(text\)[url]`. */
|
||||
const reversedPattern = /(?<=(?<!\\)(?:\\{2})*)\((?<label>(?:\\.|[^()\\]|\([\s\S]*\))*)\)\[(?<url>(?:\\.|[^\]\\\r\n])*)\](?!\()/gu;
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoReversedMediaSyntaxRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
recommended: true,
|
||||
description: "Disallow reversed link and image syntax",
|
||||
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-reversed-media-syntax.md",
|
||||
},
|
||||
fixable: "code",
|
||||
messages: {
|
||||
reversedSyntax: "Unexpected reversed syntax found. Use [label](URL) syntax instead.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
/** @type {string[]} */
|
||||
let buffer;
|
||||
/** @type {number} */
|
||||
let nodeStartOffset;
|
||||
return {
|
||||
"heading, paragraph, tableCell"(
|
||||
/** @type {Heading | Paragraph | TableCell} */ node) {
|
||||
// Use UTF-16 code units so the buffer stays aligned with source offsets.
|
||||
buffer = sourceCode.getText(node).split("");
|
||||
// Store the start offset of the node for later calculations.
|
||||
nodeStartOffset = node.position.start.offset;
|
||||
},
|
||||
":matches(heading, paragraph, tableCell) :matches(html, image, imageReference, inlineCode, linkReference, inlineMath)"(
|
||||
/** @type {Html | Image | ImageReference | InlineCode | LinkReference | InlineMath} */ node) {
|
||||
const [startOffset, endOffset] = sourceCode.getRange(node);
|
||||
// Mask the content of `html`, `image`, `imageReference`, `inlineCode`, `linkReference`, and `inlineMath` nodes with whitespaces.
|
||||
for (let i = startOffset; i < endOffset; i++) {
|
||||
buffer[i - nodeStartOffset] = " ";
|
||||
}
|
||||
},
|
||||
":matches(heading, paragraph, tableCell):exit"() {
|
||||
const maskedText = buffer.join("");
|
||||
/** @type {RegExpExecArray | null} */
|
||||
let match;
|
||||
while ((match = reversedPattern.exec(maskedText)) !== null) {
|
||||
const { label, url } = match.groups;
|
||||
const startOffset = match.index + nodeStartOffset; // Adjust `reversedPattern` match index to the full source code.
|
||||
const endOffset = startOffset + match[0].length;
|
||||
const labelStartOffset = startOffset + 1; // Skip "("
|
||||
const labelEndOffset = labelStartOffset + label.length;
|
||||
const urlStartOffset = labelEndOffset + 2; // Skip ")["
|
||||
const urlEndOffset = urlStartOffset + url.length;
|
||||
context.report({
|
||||
loc: {
|
||||
start: sourceCode.getLocFromIndex(startOffset),
|
||||
end: sourceCode.getLocFromIndex(endOffset),
|
||||
},
|
||||
messageId: "reversedSyntax",
|
||||
fix(fixer) {
|
||||
return fixer.replaceTextRange([startOffset, endOffset], `[${sourceCode.text.slice(labelStartOffset, labelEndOffset)}](${sourceCode.text.slice(urlStartOffset, urlEndOffset)})`);
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let recommended: boolean;
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
let fixable: "whitespace";
|
||||
namespace messages {
|
||||
let spaceInEmphasis: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
checkStrikethrough: {
|
||||
type: "boolean";
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
checkStrikethrough: false;
|
||||
}];
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../types.js").MarkdownLanguageOptions;
|
||||
Code: import("../index.js").MarkdownSourceCode;
|
||||
RuleOptions: NoSpaceInEmphasisOptions;
|
||||
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
|
||||
MessageIds: "spaceInEmphasis";
|
||||
}>): {
|
||||
"heading, paragraph, tableCell"(node: Heading | Paragraph | TableCell): void;
|
||||
":matches(heading, paragraph, tableCell) > text"(node: Text): void;
|
||||
":matches(heading, paragraph, tableCell):exit"(node: Heading | Paragraph | TableCell): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoSpaceInEmphasisMessageIds = "spaceInEmphasis";
|
||||
export type NoSpaceInEmphasisOptions = [{
|
||||
checkStrikethrough?: boolean;
|
||||
}];
|
||||
export type NoSpaceInEmphasisRuleDefinition = MarkdownRuleDefinition<{
|
||||
RuleOptions: NoSpaceInEmphasisOptions;
|
||||
MessageIds: NoSpaceInEmphasisMessageIds;
|
||||
}>;
|
||||
import type { Heading } from "mdast";
|
||||
import type { Paragraph } from "mdast";
|
||||
import type { TableCell } from "mdast";
|
||||
import type { Text } from "mdast";
|
||||
import type { MarkdownRuleDefinition } from "../types.js";
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* @fileoverview Rule to prevent spaces around emphasis markers in Markdown.
|
||||
* @author Pixel998
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { SourceRange } from "@eslint/core";
|
||||
* @import { Heading, Paragraph, TableCell, Text } from "mdast";
|
||||
* @import { MarkdownRuleDefinition } from "../types.js";
|
||||
* @typedef {"spaceInEmphasis"} NoSpaceInEmphasisMessageIds
|
||||
* @typedef {[{ checkStrikethrough?: boolean }]} NoSpaceInEmphasisOptions
|
||||
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoSpaceInEmphasisOptions, MessageIds: NoSpaceInEmphasisMessageIds }>} NoSpaceInEmphasisRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
const whitespacePattern = /[ \t]/u;
|
||||
/**
|
||||
* Creates a marker pattern based on whether strikethrough should be included.
|
||||
* @param {boolean} checkStrikethrough Whether to include strikethrough markers.
|
||||
* @returns {RegExp} The marker pattern.
|
||||
*/
|
||||
function createMarkerPattern(checkStrikethrough) {
|
||||
return checkStrikethrough
|
||||
? /(?<=(?<!\\)(?:\\{2})*)(?:\*\*\*|\*\*|\*|___|__|_|~~|~)/gu
|
||||
: /(?<=(?<!\\)(?:\\{2})*)(?:\*\*\*|\*\*|\*|___|__|_)/gu;
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoSpaceInEmphasisRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
recommended: true,
|
||||
description: "Disallow spaces around emphasis markers",
|
||||
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-space-in-emphasis.md",
|
||||
},
|
||||
fixable: "whitespace",
|
||||
messages: {
|
||||
spaceInEmphasis: "Unexpected space around emphasis marker.",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
checkStrikethrough: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
checkStrikethrough: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
const [{ checkStrikethrough }] = context.options;
|
||||
const markerPattern = createMarkerPattern(checkStrikethrough);
|
||||
/** @type {string[]} */
|
||||
let buffer;
|
||||
/**
|
||||
* Reports a surrounding-space violation if present.
|
||||
* @param {number} checkIndex Character index to test for whitespace.
|
||||
* @param {number} highlightStartIndex Start index for highlighting.
|
||||
* @param {number} highlightEndIndex End index for highlighting.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportWhitespace(checkIndex, highlightStartIndex, highlightEndIndex) {
|
||||
if (whitespacePattern.test(sourceCode.text[checkIndex])) {
|
||||
context.report({
|
||||
loc: {
|
||||
start: sourceCode.getLocFromIndex(highlightStartIndex),
|
||||
end: sourceCode.getLocFromIndex(highlightEndIndex),
|
||||
},
|
||||
messageId: "spaceInEmphasis",
|
||||
fix(fixer) {
|
||||
return fixer.removeRange([checkIndex, checkIndex + 1]);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
"heading, paragraph, tableCell"(
|
||||
/** @type {Heading | Paragraph | TableCell} */ node) {
|
||||
const [startOffset, endOffset] = sourceCode.getRange(node);
|
||||
// Initialize `buffer` with a whitespace-masked character array.
|
||||
buffer = new Array(endOffset - startOffset).fill(" ");
|
||||
},
|
||||
":matches(heading, paragraph, tableCell) > text"(
|
||||
/** @type {Text} */ node) {
|
||||
const [startOffset, endOffset] = sourceCode.getRange(node);
|
||||
const parentNodeStartOffset = // Parent node can be `Heading`, `Paragraph`, or `TableCell`.
|
||||
sourceCode.getParent(node).position.start.offset;
|
||||
// Add the content of a `Text` node into the current buffer at the correct offsets.
|
||||
for (let i = startOffset; i < endOffset; i++) {
|
||||
buffer[i - parentNodeStartOffset] = sourceCode.text[i];
|
||||
}
|
||||
},
|
||||
":matches(heading, paragraph, tableCell):exit"(
|
||||
/** @type {Heading | Paragraph | TableCell} */ node) {
|
||||
const maskedText = buffer.join("");
|
||||
/** @type {Map<string, SourceRange[]>} */
|
||||
const markerGroups = new Map();
|
||||
/** @type {RegExpExecArray | null} */
|
||||
let match;
|
||||
while ((match = markerPattern.exec(maskedText)) !== null) {
|
||||
const marker = match[0];
|
||||
const startOffset = // Adjust `markerPattern` match index to the full source code.
|
||||
match.index + node.position.start.offset;
|
||||
const endOffset = startOffset + marker.length;
|
||||
if (!markerGroups.has(marker)) {
|
||||
markerGroups.set(marker, []);
|
||||
}
|
||||
markerGroups.get(marker).push([startOffset, endOffset]);
|
||||
}
|
||||
for (const group of markerGroups.values()) {
|
||||
for (let i = 0; i < group.length - 1; i += 2) {
|
||||
const startMarker = group[i];
|
||||
reportWhitespace(startMarker[1], startMarker[0], startMarker[1] + 2);
|
||||
const endMarker = group[i + 1];
|
||||
reportWhitespace(endMarker[0] - 1, endMarker[0] - 2, endMarker[1]);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let recommended: boolean;
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let unusedDefinition: string;
|
||||
let unusedFootnoteDefinition: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
allowDefinitions: {
|
||||
type: "array";
|
||||
items: {
|
||||
type: "string";
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
allowFootnoteDefinitions: {
|
||||
type: "array";
|
||||
items: {
|
||||
type: "string";
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
checkFootnoteDefinitions: {
|
||||
type: "boolean";
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
allowDefinitions: string[];
|
||||
allowFootnoteDefinitions: any[];
|
||||
checkFootnoteDefinitions: true;
|
||||
}];
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../types.js").MarkdownLanguageOptions;
|
||||
Code: import("../index.js").MarkdownSourceCode;
|
||||
RuleOptions: NoUnusedDefinitionsOptions;
|
||||
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
|
||||
MessageIds: NoUnusedDefinitionsMessageIds;
|
||||
}>): {
|
||||
imageReference(node: import("mdast").ImageReference): void;
|
||||
linkReference(node: import("mdast").LinkReference): void;
|
||||
footnoteReference(node: import("mdast").FootnoteReference): void;
|
||||
definition(node: Definition): void;
|
||||
footnoteDefinition(node: FootnoteDefinition): void;
|
||||
"root:exit"(): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoUnusedDefinitionsMessageIds = "unusedDefinition" | "unusedFootnoteDefinition";
|
||||
export type NoUnusedDefinitionsOptions = [{
|
||||
allowDefinitions?: string[];
|
||||
allowFootnoteDefinitions?: string[];
|
||||
checkFootnoteDefinitions?: boolean;
|
||||
}];
|
||||
export type NoUnusedDefinitionsRuleDefinition = MarkdownRuleDefinition<{
|
||||
RuleOptions: NoUnusedDefinitionsOptions;
|
||||
MessageIds: NoUnusedDefinitionsMessageIds;
|
||||
}>;
|
||||
import type { Definition } from "mdast";
|
||||
import type { FootnoteDefinition } from "mdast";
|
||||
import type { MarkdownRuleDefinition } from "../types.js";
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* @fileoverview Rule to prevent unused definitions in Markdown.
|
||||
* @author 루밀LuMir(lumirlumir)
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { normalizeIdentifier } from "micromark-util-normalize-identifier";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { Definition, FootnoteDefinition } from "mdast";
|
||||
* @import { MarkdownRuleDefinition } from "../types.js";
|
||||
* @typedef {"unusedDefinition" | "unusedFootnoteDefinition"} NoUnusedDefinitionsMessageIds
|
||||
* @typedef {[{ allowDefinitions?: string[], allowFootnoteDefinitions?: string[], checkFootnoteDefinitions?: boolean }]} NoUnusedDefinitionsOptions
|
||||
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoUnusedDefinitionsOptions, MessageIds: NoUnusedDefinitionsMessageIds }>} NoUnusedDefinitionsRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoUnusedDefinitionsRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
recommended: true,
|
||||
description: "Disallow unused definitions",
|
||||
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-unused-definitions.md",
|
||||
},
|
||||
messages: {
|
||||
unusedDefinition: "Unexpected unused definition `{{ identifier }}` (label: `{{ label }}`) found.",
|
||||
unusedFootnoteDefinition: "Unexpected unused footnote definition `{{ identifier }}` (label: `{{ label }}`) found.",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowDefinitions: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
allowFootnoteDefinitions: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
checkFootnoteDefinitions: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
allowDefinitions: ["//"],
|
||||
allowFootnoteDefinitions: [],
|
||||
checkFootnoteDefinitions: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
create(context) {
|
||||
const allowDefinitions = new Set(context.options[0].allowDefinitions.map(identifier => normalizeIdentifier(identifier).toLowerCase()));
|
||||
const allowFootnoteDefinitions = new Set(context.options[0].allowFootnoteDefinitions.map(identifier => normalizeIdentifier(identifier).toLowerCase()));
|
||||
const [{ checkFootnoteDefinitions }] = context.options;
|
||||
/** @type {Set<string>} Set to track used identifiers */
|
||||
const usedIdentifiers = new Set();
|
||||
/** @type {Set<string>} Set to track used footnote identifiers */
|
||||
const usedFootnoteIdentifiers = new Set();
|
||||
/** @type {Set<Definition>} */
|
||||
const definitions = new Set();
|
||||
/** @type {Set<FootnoteDefinition>} */
|
||||
const footnoteDefinitions = new Set();
|
||||
return {
|
||||
imageReference(node) {
|
||||
usedIdentifiers.add(node.identifier);
|
||||
},
|
||||
linkReference(node) {
|
||||
usedIdentifiers.add(node.identifier);
|
||||
},
|
||||
footnoteReference(node) {
|
||||
usedFootnoteIdentifiers.add(node.identifier);
|
||||
},
|
||||
definition(node) {
|
||||
if (allowDefinitions.has(node.identifier)) {
|
||||
return;
|
||||
}
|
||||
definitions.add(node);
|
||||
},
|
||||
footnoteDefinition(node) {
|
||||
if (!checkFootnoteDefinitions ||
|
||||
allowFootnoteDefinitions.has(node.identifier)) {
|
||||
return;
|
||||
}
|
||||
footnoteDefinitions.add(node);
|
||||
},
|
||||
"root:exit"() {
|
||||
for (const definition of definitions) {
|
||||
if (!usedIdentifiers.has(definition.identifier)) {
|
||||
context.report({
|
||||
node: definition,
|
||||
messageId: "unusedDefinition",
|
||||
data: {
|
||||
identifier: definition.identifier,
|
||||
label: definition.label.trim(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const footnoteDefinition of footnoteDefinitions) {
|
||||
if (!usedFootnoteIdentifiers.has(footnoteDefinition.identifier)) {
|
||||
context.report({
|
||||
node: footnoteDefinition,
|
||||
messageId: "unusedFootnoteDefinition",
|
||||
data: {
|
||||
identifier: footnoteDefinition.identifier,
|
||||
label: footnoteDefinition.label,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let recommended: boolean;
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let altTextRequired: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../types.js").MarkdownLanguageOptions;
|
||||
Code: import("../index.js").MarkdownSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
|
||||
MessageIds: "altTextRequired";
|
||||
}>): {
|
||||
"image, imageReference"(node: Image | ImageReference): void;
|
||||
html(node: import("mdast").Html): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type RequireAltTextMessageIds = "altTextRequired";
|
||||
export type RequireAltTextOptions = [];
|
||||
export type RequireAltTextRuleDefinition = MarkdownRuleDefinition<{
|
||||
RuleOptions: RequireAltTextOptions;
|
||||
MessageIds: RequireAltTextMessageIds;
|
||||
}>;
|
||||
import type { Image } from "mdast";
|
||||
import type { ImageReference } from "mdast";
|
||||
import type { MarkdownRuleDefinition } from "../types.js";
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* @fileoverview Rule to require alternative text for images in Markdown.
|
||||
* @author Pixel998
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { stripHtmlComments } from "../util.js";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { Image, ImageReference } from "mdast";
|
||||
* @import { MarkdownRuleDefinition } from "../types.js";
|
||||
* @typedef {"altTextRequired"} RequireAltTextMessageIds
|
||||
* @typedef {[]} RequireAltTextOptions
|
||||
* @typedef {MarkdownRuleDefinition<{ RuleOptions: RequireAltTextOptions, MessageIds: RequireAltTextMessageIds }>} RequireAltTextRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
const imgTagPattern = /<img(?:\s(?:[^>"']|"[^"]*"|'[^']*')*)?\/?>/giu;
|
||||
/**
|
||||
* Creates a regex to match HTML attributes
|
||||
* @param {string} name The attribute name to match
|
||||
* @returns {RegExp} Regular expression for matching the attribute
|
||||
*/
|
||||
function getHtmlAttributeRe(name) {
|
||||
return new RegExp(`\\s${name}(?:\\s*=\\s*['"]([^'"]*)['"])?`, "iu");
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {RequireAltTextRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
recommended: true,
|
||||
description: "Require alternative text for images",
|
||||
url: "https://github.com/eslint/markdown/blob/main/docs/rules/require-alt-text.md",
|
||||
},
|
||||
messages: {
|
||||
altTextRequired: "Alternative text for image is required.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
return {
|
||||
"image, imageReference"(
|
||||
/** @type {Image | ImageReference} */ node) {
|
||||
if (node.alt.trim().length === 0) {
|
||||
context.report({
|
||||
loc: node.position,
|
||||
messageId: "altTextRequired",
|
||||
});
|
||||
}
|
||||
},
|
||||
html(node) {
|
||||
const text = stripHtmlComments(sourceCode.getText(node));
|
||||
/** @type {RegExpExecArray | null} */
|
||||
let match;
|
||||
while ((match = imgTagPattern.exec(text)) !== null) {
|
||||
const imgTag = match[0];
|
||||
const ariaHiddenMatch = imgTag.match(getHtmlAttributeRe("aria-hidden"));
|
||||
if (ariaHiddenMatch &&
|
||||
ariaHiddenMatch[1] &&
|
||||
ariaHiddenMatch[1].toLowerCase() === "true") {
|
||||
continue;
|
||||
}
|
||||
const altMatch = imgTag.match(getHtmlAttributeRe("alt"));
|
||||
if (!altMatch ||
|
||||
(altMatch[1] &&
|
||||
altMatch[1].trim().length === 0 &&
|
||||
altMatch[1].length > 0)) {
|
||||
const startOffset = // Adjust `imgTagPattern` match indices to the full source code.
|
||||
match.index + node.position.start.offset;
|
||||
const endOffset = startOffset + imgTag.length;
|
||||
context.report({
|
||||
loc: {
|
||||
start: sourceCode.getLocFromIndex(startOffset),
|
||||
end: sourceCode.getLocFromIndex(endOffset),
|
||||
},
|
||||
messageId: "altTextRequired",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let recommended: boolean;
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let extraCells: string;
|
||||
let missingCells: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
checkMissingCells: {
|
||||
type: "boolean";
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
checkMissingCells: false;
|
||||
}];
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../types.js").MarkdownLanguageOptions;
|
||||
Code: import("../index.js").MarkdownSourceCode;
|
||||
RuleOptions: TableColumnCountOptions;
|
||||
Node: import("mdast").Node | import("../language/markdown-source-code.js").InlineConfigComment;
|
||||
MessageIds: TableColumnCountMessageIds;
|
||||
}>): {
|
||||
table(node: import("mdast").Table): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type TableColumnCountMessageIds = "extraCells" | "missingCells";
|
||||
export type TableColumnCountOptions = [{
|
||||
checkMissingCells?: boolean;
|
||||
}];
|
||||
export type TableColumnCountRuleDefinition = MarkdownRuleDefinition<{
|
||||
RuleOptions: TableColumnCountOptions;
|
||||
MessageIds: TableColumnCountMessageIds;
|
||||
}>;
|
||||
import type { MarkdownRuleDefinition } from "../types.js";
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow data rows in a GitHub Flavored Markdown table from having more cells than the header row
|
||||
* @author Sweta Tanwar (@SwetaTanwar)
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { MarkdownRuleDefinition } from "../types.js";
|
||||
* @typedef {"extraCells" | "missingCells"} TableColumnCountMessageIds
|
||||
* @typedef {[{ checkMissingCells?: boolean }]} TableColumnCountOptions
|
||||
* @typedef {MarkdownRuleDefinition<{ RuleOptions: TableColumnCountOptions, MessageIds: TableColumnCountMessageIds }>} TableColumnCountRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {TableColumnCountRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
recommended: true,
|
||||
description: "Disallow data rows in a GitHub Flavored Markdown table from having more cells than the header row",
|
||||
url: "https://github.com/eslint/markdown/blob/main/docs/rules/table-column-count.md",
|
||||
},
|
||||
messages: {
|
||||
extraCells: "Table column count mismatch (Expected: {{expectedCells}}, Actual: {{actualCells}}), extra data starting here will be ignored.",
|
||||
missingCells: "Table column count mismatch (Expected: {{expectedCells}}, Actual: {{actualCells}}), row might be missing data.",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
checkMissingCells: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [{ checkMissingCells: false }],
|
||||
},
|
||||
create(context) {
|
||||
const [{ checkMissingCells }] = context.options;
|
||||
return {
|
||||
table(node) {
|
||||
if (node.children.length < 1) {
|
||||
return;
|
||||
}
|
||||
const headerRow = node.children[0];
|
||||
const expectedCellsLength = headerRow.children.length;
|
||||
for (let i = 1; i < node.children.length; i++) {
|
||||
const currentRow = node.children[i];
|
||||
const actualCellsLength = currentRow.children.length;
|
||||
const lastActualCellNode = currentRow.children[actualCellsLength - 1];
|
||||
if (actualCellsLength > expectedCellsLength) {
|
||||
const firstExtraCellNode = currentRow.children[expectedCellsLength];
|
||||
context.report({
|
||||
loc: {
|
||||
start: firstExtraCellNode.position.start,
|
||||
end: lastActualCellNode.position.end,
|
||||
},
|
||||
messageId: "extraCells",
|
||||
data: {
|
||||
actualCells: actualCellsLength,
|
||||
expectedCells: expectedCellsLength,
|
||||
},
|
||||
});
|
||||
}
|
||||
else if (checkMissingCells &&
|
||||
actualCellsLength < expectedCellsLength) {
|
||||
context.report({
|
||||
loc: {
|
||||
start: {
|
||||
column: lastActualCellNode.position.end.column -
|
||||
1,
|
||||
line: lastActualCellNode.position.end.line,
|
||||
},
|
||||
end: currentRow.position.end,
|
||||
},
|
||||
messageId: "missingCells",
|
||||
data: {
|
||||
actualCells: actualCellsLength,
|
||||
expectedCells: expectedCellsLength,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import type { Node, Data, Literal, Parent, Blockquote, Break, Code, Definition, Emphasis, Heading, Html, Image, ImageReference, InlineCode, Link, LinkReference, List, ListItem, Paragraph, Root, Strong, Text, ThematicBreak, Delete, FootnoteDefinition, FootnoteReference, Table, TableCell, TableRow, Yaml } from "mdast";
|
||||
import type { InlineMath, Math } from "mdast-util-math";
|
||||
import type { LanguageContext, LanguageOptions, RuleVisitor } from "@eslint/core";
|
||||
import type { CustomRuleDefinitionType, CustomRuleTypeDefinitions, CustomRuleVisitorWithExit } from "@eslint/plugin-kit";
|
||||
import type { InlineConfigComment, MarkdownSourceCode } from "./language/markdown-source-code.js";
|
||||
export interface RangeMap {
|
||||
indent: number;
|
||||
js: number;
|
||||
md: number;
|
||||
}
|
||||
export interface BlockBase {
|
||||
baseIndentText: string;
|
||||
comments: string[];
|
||||
rangeMap: RangeMap[];
|
||||
}
|
||||
export type Block = Code & BlockBase;
|
||||
/**
|
||||
* Markdown TOML.
|
||||
*/
|
||||
export interface Toml extends Literal {
|
||||
/**
|
||||
* Node type of mdast TOML.
|
||||
*/
|
||||
type: "toml";
|
||||
/**
|
||||
* Data associated with the mdast TOML.
|
||||
*/
|
||||
data?: TomlData | undefined;
|
||||
}
|
||||
/**
|
||||
* Info associated with mdast TOML nodes by the ecosystem.
|
||||
*/
|
||||
export interface TomlData extends Data {
|
||||
}
|
||||
/**
|
||||
* Markdown JSON.
|
||||
*/
|
||||
export interface Json extends Literal {
|
||||
/**
|
||||
* Node type of mdast JSON.
|
||||
*/
|
||||
type: "json";
|
||||
/**
|
||||
* Data associated with the mdast JSON.
|
||||
*/
|
||||
data?: JsonData | undefined;
|
||||
}
|
||||
/**
|
||||
* Info associated with mdast JSON nodes by the ecosystem.
|
||||
*/
|
||||
export interface JsonData extends Data {
|
||||
}
|
||||
/**
|
||||
* Language options provided for Markdown files.
|
||||
*/
|
||||
export interface MarkdownLanguageOptions extends LanguageOptions {
|
||||
/**
|
||||
* The options for parsing frontmatter.
|
||||
*/
|
||||
frontmatter?: false | "yaml" | "toml" | "json";
|
||||
/**
|
||||
* The options for parsing math.
|
||||
*/
|
||||
math?: boolean;
|
||||
}
|
||||
/**
|
||||
* The context object that is passed to the Markdown language plugin methods.
|
||||
*/
|
||||
export type MarkdownLanguageContext = LanguageContext<MarkdownLanguageOptions>;
|
||||
/**
|
||||
* A Markdown syntax element, including nodes and comments.
|
||||
*/
|
||||
type MarkdownSyntaxElement = Node | InlineConfigComment;
|
||||
export interface MarkdownRuleVisitor extends RuleVisitor, CustomRuleVisitorWithExit<{
|
||||
root?(node: Root): void;
|
||||
} & {
|
||||
[NodeType in Blockquote | Break | Code | Definition | Emphasis | Heading | Html | Image | ImageReference | InlineCode | Link | LinkReference | List | ListItem | Paragraph | Strong | Text | ThematicBreak | Delete | FootnoteDefinition | FootnoteReference | Table | TableCell | TableRow | Yaml | Toml | Json | InlineMath | Math as NodeType["type"]]?: (node: NodeType, parent?: Parent) => void;
|
||||
}> {
|
||||
}
|
||||
export type MarkdownRuleDefinitionTypeOptions = CustomRuleTypeDefinitions;
|
||||
export type MarkdownRuleDefinition<Options extends Partial<MarkdownRuleDefinitionTypeOptions> = {}> = CustomRuleDefinitionType<{
|
||||
LangOptions: MarkdownLanguageOptions;
|
||||
Code: MarkdownSourceCode;
|
||||
Visitor: MarkdownRuleVisitor;
|
||||
Node: MarkdownSyntaxElement;
|
||||
}, Options>;
|
||||
export {};
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// Imports
|
||||
//------------------------------------------------------------------------------
|
||||
export {};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Checks if a frontmatter block contains a title matching the given pattern.
|
||||
* @param {string} value The frontmatter content.
|
||||
* @param {RegExp|null} pattern The pattern to match against.
|
||||
* @returns {boolean} Whether a title was found.
|
||||
*/
|
||||
export function frontmatterHasTitle(value: string, pattern: RegExp | null): boolean;
|
||||
/**
|
||||
* Replaces all HTML comments with whitespace.
|
||||
* This preserves offsets and locations of characters
|
||||
* outside HTML comments by keeping line breaks and replacing
|
||||
* other code units with a space character.
|
||||
* @param {string} value The string to remove HTML comments from.
|
||||
* @returns {string} The string with HTML comments removed.
|
||||
*/
|
||||
export function stripHtmlComments(value: string): string;
|
||||
/**
|
||||
* @fileoverview Utility Library
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
/**
|
||||
* Line ending pattern to match all line endings (CRLF, CR, LF). (CommonMark spec)
|
||||
* @see https://spec.commonmark.org/0.31.2/#line-ending
|
||||
*/
|
||||
export const lineEndingPattern: RegExp;
|
||||
/**
|
||||
* CommonMark does not allow any white space between the brackets in a reference link.
|
||||
* If that pattern is detected, then it's treated as text and not as a link. This pattern
|
||||
* is used to detect that situation.
|
||||
*/
|
||||
export const illegalShorthandTailPattern: RegExp;
|
||||
/**
|
||||
* Regular expression to match HTML comments, including multiline comments.
|
||||
*/
|
||||
export const htmlCommentPattern: RegExp;
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @fileoverview Utility Library
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Regex Patterns
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* Line ending pattern to match all line endings (CRLF, CR, LF). (CommonMark spec)
|
||||
* @see https://spec.commonmark.org/0.31.2/#line-ending
|
||||
*/
|
||||
export const lineEndingPattern = /\r\n|[\r\n]/u;
|
||||
/**
|
||||
* CommonMark does not allow any white space between the brackets in a reference link.
|
||||
* If that pattern is detected, then it's treated as text and not as a link. This pattern
|
||||
* is used to detect that situation.
|
||||
*/
|
||||
export const illegalShorthandTailPattern = /\]\[\s+\]$/u;
|
||||
/**
|
||||
* Regular expression to match HTML comments, including multiline comments.
|
||||
*/
|
||||
export const htmlCommentPattern = /<!--[\s\S]*?-->/gu;
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* Checks if a frontmatter block contains a title matching the given pattern.
|
||||
* @param {string} value The frontmatter content.
|
||||
* @param {RegExp|null} pattern The pattern to match against.
|
||||
* @returns {boolean} Whether a title was found.
|
||||
*/
|
||||
export function frontmatterHasTitle(value, pattern) {
|
||||
if (!pattern) {
|
||||
return false;
|
||||
}
|
||||
const lines = value.split(lineEndingPattern);
|
||||
for (const line of lines) {
|
||||
if (pattern.test(line)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Replaces all HTML comments with whitespace.
|
||||
* This preserves offsets and locations of characters
|
||||
* outside HTML comments by keeping line breaks and replacing
|
||||
* other code units with a space character.
|
||||
* @param {string} value The string to remove HTML comments from.
|
||||
* @returns {string} The string with HTML comments removed.
|
||||
*/
|
||||
export function stripHtmlComments(value) {
|
||||
return value.replace(htmlCommentPattern, match =>
|
||||
/* eslint-disable-next-line require-unicode-regexp
|
||||
-- we want to replace each code unit with a space
|
||||
*/
|
||||
match.replace(/[^\r\n]/g, " "));
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"name": "@eslint/markdown",
|
||||
"version": "8.0.3",
|
||||
"description": "The official ESLint language plugin for Markdown",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Brandon Mills",
|
||||
"url": "https://github.com/btmills"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": "eslint/markdown",
|
||||
"bugs": {
|
||||
"url": "https://github.com/eslint/markdown/issues"
|
||||
},
|
||||
"homepage": "https://github.com/eslint/markdown#readme",
|
||||
"keywords": [
|
||||
"eslint",
|
||||
"eslintplugin",
|
||||
"markdown",
|
||||
"lint",
|
||||
"linter"
|
||||
],
|
||||
"workspaces": [
|
||||
"examples/*"
|
||||
],
|
||||
"gitHooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"*.md": [
|
||||
"eslint --fix",
|
||||
"eslint --fix -c eslint.config-content.js"
|
||||
],
|
||||
"!(*.{js,md})": "prettier --write --ignore-unknown",
|
||||
"{src/rules/*.js,tools/update-rules-docs.js,README.md}": [
|
||||
"npm run build:update-rules-docs",
|
||||
"git add README.md"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint && eslint -c eslint.config-content.js",
|
||||
"lint:fix": "eslint --fix && eslint --fix -c eslint.config-content.js",
|
||||
"lint:types": "attw --pack --profile esm-only",
|
||||
"lint:unused": "knip",
|
||||
"fmt": "prettier --write .",
|
||||
"fmt:check": "prettier --check .",
|
||||
"build": "npm run build:rules && npm run build:types && npm run build:update-rules-docs",
|
||||
"build:rules": "node tools/build-rules.js",
|
||||
"build:types": "tsc",
|
||||
"build:update-rules-docs": "node tools/update-rules-docs.js",
|
||||
"prepare": "npm run build",
|
||||
"test": "mocha \"tests/**/*.test.js\" --timeout 30000",
|
||||
"test:coverage": "c8 npm test",
|
||||
"test:jsr": "npx -y jsr@latest publish --dry-run",
|
||||
"test:types": "tsc -p tests/types/tsconfig.json",
|
||||
"test:types:5.3": "npx -p typescript@5.3 -y -- tsc -p tests/types/tsconfig.legacy.json",
|
||||
"test:types:5.x": "npx -p typescript@5.x -y -- tsc -p tests/types/tsconfig.json",
|
||||
"test:types:7.x": "npx -p @typescript/native-preview@latest -y -- tsgo -p tests/types/tsconfig.json",
|
||||
"test:types:all": "npm run test:types && npm run test:types:5.3 && npm run test:types:5.x && npm run test:types:7.x"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arethetypeswrong/cli": "^0.18.3",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@eslint/json": "^2.0.0",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/unist": "^3.0.3",
|
||||
"c8": "^11.0.0",
|
||||
"dedent": "^1.7.1",
|
||||
"eslint": "^10.0.3",
|
||||
"eslint-v9": "npm:eslint@9.x",
|
||||
"eslint-config-eslint": "^14.0.0",
|
||||
"eslint-plugin-eslint-plugin": "^7.3.2",
|
||||
"globals": "^17.1.0",
|
||||
"knip": "^6.0.0",
|
||||
"lint-staged": "^16.0.0",
|
||||
"mocha": "^11.7.5",
|
||||
"prettier": "3.8.4",
|
||||
"semver": "^7.7.3",
|
||||
"typescript": "^6.0.3",
|
||||
"yorkie": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eslint/core": "^1.2.1",
|
||||
"@eslint/plugin-kit": "^0.7.2",
|
||||
"github-slugger": "^2.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.2",
|
||||
"mdast-util-frontmatter": "^2.0.1",
|
||||
"mdast-util-gfm": "^3.1.0",
|
||||
"mdast-util-math": "^3.0.0",
|
||||
"micromark-extension-frontmatter": "^2.0.0",
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"micromark-extension-math": "^3.1.0",
|
||||
"micromark-util-normalize-identifier": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user