├── .editorconfig
├── .github
├── ISSUE_TEMPLATE
│ ├── bug-report.yml
│ ├── change.yml
│ ├── config.yml
│ ├── docs.yml
│ ├── new-rule.yml
│ └── rule-change.yml
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── add-to-triage.yml
│ ├── bun-test.yml
│ ├── ci.yml
│ ├── release-please.yml
│ └── update-readme.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .release-please-manifest.json
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── docs
├── processors
│ └── markdown.md
└── rules
│ ├── fenced-code-language.md
│ ├── heading-increment.md
│ ├── no-duplicate-definitions.md
│ ├── no-duplicate-headings.md
│ ├── no-empty-definitions.md
│ ├── no-empty-images.md
│ ├── no-empty-links.md
│ ├── no-html.md
│ ├── no-invalid-label-refs.md
│ ├── no-missing-atx-heading-space.md
│ ├── no-missing-label-refs.md
│ ├── no-multiple-h1.md
│ ├── require-alt-text.md
│ └── table-column-count.md
├── eslint.config-content.js
├── eslint.config.js
├── examples
├── react
│ ├── .npmrc
│ ├── README.md
│ ├── eslint.config.mjs
│ └── package.json
└── typescript
│ ├── .npmrc
│ ├── README.md
│ ├── eslint.config.mjs
│ └── package.json
├── jsr.json
├── npm-prepare.cjs
├── package.json
├── prettier.config.js
├── release-please-config.json
├── rfcs
└── configure-file-name-from-block-meta.md
├── rollup.config.js
├── screenshot.png
├── src
├── index.js
├── language
│ ├── markdown-language.js
│ └── markdown-source-code.js
├── processor.js
├── rules
│ ├── fenced-code-language.js
│ ├── heading-increment.js
│ ├── no-duplicate-definitions.js
│ ├── no-duplicate-headings.js
│ ├── no-empty-definitions.js
│ ├── no-empty-images.js
│ ├── no-empty-links.js
│ ├── no-html.js
│ ├── no-invalid-label-refs.js
│ ├── no-missing-atx-heading-space.js
│ ├── no-missing-label-refs.js
│ ├── no-multiple-h1.js
│ ├── require-alt-text.js
│ └── table-column-count.js
├── types.ts
└── util.js
├── tests
├── examples
│ └── all.test.js
├── fixtures
│ ├── eslint.config.js
│ ├── eslintrc.json
│ ├── filename.md
│ ├── long.md
│ ├── recommended.js
│ └── recommended.json
├── language
│ ├── markdown-language.test.js
│ └── markdown-source-code.test.js
├── plugin.test.js
├── processor.test.js
├── rules
│ ├── fenced-code-language.test.js
│ ├── heading-increment.test.js
│ ├── no-duplicate-definitions.test.js
│ ├── no-duplicate-headings.test.js
│ ├── no-empty-definitions.test.js
│ ├── no-empty-images.test.js
│ ├── no-empty-links.test.js
│ ├── no-html.test.js
│ ├── no-invalid-label-refs.test.js
│ ├── no-missing-atx-heading-space.test.js
│ ├── no-missing-label-refs.test.js
│ ├── no-multiple-h1.test.js
│ ├── require-alt-text.test.js
│ └── table-column-count.test.js
└── types
│ ├── tsconfig.json
│ └── types.test.ts
├── tools
├── build-rules.js
├── commit-readme.sh
├── dedupe-types.js
├── update-readme.js
└── update-rules-docs.js
├── tsconfig.esm.json
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 4
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [package.json]
12 | indent_style = space
13 | indent_size = 2
14 |
15 | [*.md]
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.yml:
--------------------------------------------------------------------------------
1 | name: "\U0001F41E Report a problem"
2 | description: "Report something that isn't working the way you expected."
3 | title: "Bug: (fill in)"
4 | labels:
5 | - bug
6 | - "repro:needed"
7 | body:
8 | - type: markdown
9 | attributes:
10 | value: By opening an issue, you agree to abide by the [OpenJS Foundation Code of Conduct](https://eslint.org/conduct).
11 | - type: textarea
12 | attributes:
13 | label: Environment
14 | description: |
15 | Please tell us about how you're running ESLint (Run `npx eslint --env-info`.)
16 | value: |
17 | ESLint version:
18 | @eslint/markdown version:
19 | Node version:
20 | npm version:
21 | Operating System:
22 | validations:
23 | required: true
24 | - type: dropdown
25 | attributes:
26 | label: Which language are you using?
27 | description: |
28 | Just tell us which language mode you're using.
29 | options:
30 | - commonmark
31 | - gfm
32 | validations:
33 | required: true
34 | - type: textarea
35 | attributes:
36 | label: What did you do?
37 | description: |
38 | Please include a *minimal* reproduction case.
39 | value: |
40 |
41 | Configuration
42 |
43 | ```
44 |
45 | ```
46 |
47 |
48 | ```js
49 |
50 | ```
51 | validations:
52 | required: true
53 | - type: textarea
54 | attributes:
55 | label: What did you expect to happen?
56 | validations:
57 | required: true
58 | - type: textarea
59 | attributes:
60 | label: What actually happened?
61 | description: |
62 | Please copy-paste the actual ESLint output.
63 | validations:
64 | required: true
65 | - type: input
66 | attributes:
67 | label: Link to Minimal Reproducible Example
68 | description: "Link to a [StackBlitz](https://stackblitz.com) or GitHub repo with a minimal reproduction of the problem. **A minimal reproduction is required** so that others can help debug your issue. If a report is vague (e.g. just a generic error message) and has no reproduction, it may be auto-closed."
69 | placeholder: "https://stackblitz.com/abcd1234"
70 | validations:
71 | required: true
72 | - type: checkboxes
73 | attributes:
74 | label: Participation
75 | options:
76 | - label: I am willing to submit a pull request for this issue.
77 | required: false
78 | - type: textarea
79 | attributes:
80 | label: Additional comments
81 | description: Is there anything else that's important for the team to know?
82 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/change.yml:
--------------------------------------------------------------------------------
1 | name: "\U0001F680 Request a change (not rule-related)"
2 | description: "Request a change that is not a bug fix, rule change, or new rule"
3 | title: "Change Request: (fill in)"
4 | labels:
5 | - enhancement
6 | - core
7 | body:
8 | - type: markdown
9 | attributes:
10 | value: By opening an issue, you agree to abide by the [OpenJS Foundation Code of Conduct](https://eslint.org/conduct).
11 | - type: textarea
12 | attributes:
13 | label: Environment
14 | description: |
15 | Please tell us about how you're running ESLint (Run `npx eslint --env-info`.)
16 | value: |
17 | ESLint version:
18 | @eslint/markdown version:
19 | Node version:
20 | npm version:
21 | Operating System:
22 | validations:
23 | required: true
24 | - type: textarea
25 | attributes:
26 | label: What problem do you want to solve?
27 | description: |
28 | Please explain your use case in as much detail as possible.
29 | placeholder: |
30 | The Markdown plugin currently...
31 | validations:
32 | required: true
33 | - type: textarea
34 | attributes:
35 | label: What do you think is the correct solution?
36 | description: |
37 | Please explain how you'd like to change the Markdown plugin to address the problem.
38 | placeholder: |
39 | I'd like the Markdown plugin to...
40 | validations:
41 | required: true
42 | - type: checkboxes
43 | attributes:
44 | label: Participation
45 | options:
46 | - label: I am willing to submit a pull request for this change.
47 | required: false
48 | - type: textarea
49 | attributes:
50 | label: Additional comments
51 | description: Is there anything else that's important for the team to know?
52 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: 🐛 Report a Parsing Error
4 | url: https://github.com/syntax-tree/mdast-util-from-markdown/issues/new/choose
5 | about: File an issue with the parser that this plugin uses
6 | - name: 🗣 Ask a Question, Discuss
7 | url: https://github.com/eslint/markdown/discussions
8 | about: Get help using this plugin
9 | - name: Discord Server
10 | url: https://eslint.org/chat
11 | about: Talk with the team
12 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/docs.yml:
--------------------------------------------------------------------------------
1 | name: "\U0001F4DD Docs"
2 | description: "Request an improvement to documentation"
3 | title: "Docs: (fill in)"
4 | labels:
5 | - documentation
6 | body:
7 | - type: markdown
8 | attributes:
9 | value: By opening an issue, you agree to abide by the [OpenJS Foundation Code of Conduct](https://eslint.org/conduct).
10 | - type: textarea
11 | attributes:
12 | label: Docs page(s)
13 | description: |
14 | What page(s) are you suggesting be changed or created?
15 | placeholder: |
16 | e.g. https://eslint.org/docs/latest/use/getting-started
17 | validations:
18 | required: true
19 | - type: textarea
20 | attributes:
21 | label: What documentation issue do you want to solve?
22 | description: |
23 | Please explain your issue in as much detail as possible.
24 | placeholder: |
25 | The docs currently...
26 | validations:
27 | required: true
28 | - type: textarea
29 | attributes:
30 | label: What do you think is the correct solution?
31 | description: |
32 | Please explain how you'd like to change the docs to address the problem.
33 | placeholder: |
34 | I'd like the docs to...
35 | validations:
36 | required: true
37 | - type: checkboxes
38 | attributes:
39 | label: Participation
40 | options:
41 | - label: I am willing to submit a pull request for this change.
42 | required: false
43 | - type: textarea
44 | attributes:
45 | label: Additional comments
46 | description: Is there anything else that's important for the team to know?
47 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/new-rule.yml:
--------------------------------------------------------------------------------
1 | name: "\U0001F680 Propose a new rule"
2 | description: "Propose a new rule to be added to the plugin"
3 | title: "New Rule: (fill in)"
4 | labels:
5 | - rule
6 | - feature
7 | body:
8 | - type: markdown
9 | attributes:
10 | value: By opening an issue, you agree to abide by the [OpenJS Foundation Code of Conduct](https://eslint.org/conduct).
11 | - type: input
12 | attributes:
13 | label: Rule details
14 | description: What should the new rule do?
15 | validations:
16 | required: true
17 | - type: dropdown
18 | attributes:
19 | label: What type of rule is this?
20 | options:
21 | - Warns about a potential problem
22 | - Suggests an alternate way of doing something
23 | validations:
24 | required: true
25 | - type: textarea
26 | attributes:
27 | label: Example code
28 | description: Please provide some example code that this rule will warn about.
29 | render: markdown
30 | validations:
31 | required: true
32 | - type: textarea
33 | attributes:
34 | label: Prior Art
35 | description: If this rule already exists in another Markdown linter, please mention that here and provide link(s) to documentation.
36 | render: markdown
37 | - type: checkboxes
38 | attributes:
39 | label: Participation
40 | options:
41 | - label: I am willing to submit a pull request to implement this rule.
42 | required: false
43 | - type: textarea
44 | attributes:
45 | label: Additional comments
46 | description: Is there anything else that's important for the team to know?
47 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/rule-change.yml:
--------------------------------------------------------------------------------
1 | name: "\U0001F4DD Request a rule change"
2 | description: "Request a change to an existing rule"
3 | title: "Rule Change: (fill in)"
4 | labels:
5 | - enhancement
6 | - rule
7 | body:
8 | - type: markdown
9 | attributes:
10 | value: By opening an issue, you agree to abide by the [OpenJS Foundation Code of Conduct](https://eslint.org/conduct).
11 | - type: input
12 | attributes:
13 | label: What rule do you want to change?
14 | validations:
15 | required: true
16 | - type: dropdown
17 | attributes:
18 | label: What change do you want to make?
19 | options:
20 | - Generate more warnings
21 | - Generate fewer warnings
22 | - Implement autofix
23 | - Implement suggestions
24 | validations:
25 | required: true
26 | - type: dropdown
27 | attributes:
28 | label: How do you think the change should be implemented?
29 | options:
30 | - A new option
31 | - A new default behavior
32 | - Other
33 | validations:
34 | required: true
35 | - type: textarea
36 | attributes:
37 | label: Example code
38 | description: Please provide some example code that this change will affect.
39 | render: markdown
40 | validations:
41 | required: true
42 | - type: textarea
43 | attributes:
44 | label: What does the rule currently do for this code?
45 | validations:
46 | required: true
47 | - type: textarea
48 | attributes:
49 | label: What will the rule do after it's changed?
50 | validations:
51 | required: true
52 | - type: checkboxes
53 | attributes:
54 | label: Participation
55 | options:
56 | - label: I am willing to submit a pull request to implement this change.
57 | required: false
58 | - type: textarea
59 | attributes:
60 | label: Additional comments
61 | description: Is there anything else that's important for the team to know?
62 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | #### Prerequisites checklist
8 |
9 |
10 | - [ ] I have read the [contributing guidelines](https://github.com/eslint/eslint/blob/HEAD/CONTRIBUTING.md).
11 |
12 |
20 |
21 |
24 |
25 | #### What is the purpose of this pull request?
26 |
27 | #### What changes did you make? (Give an overview)
28 |
29 | #### Related Issues
30 |
31 |
32 |
33 | #### Is there anything you'd like reviewers to focus on?
34 |
35 |
36 |
--------------------------------------------------------------------------------
/.github/workflows/add-to-triage.yml:
--------------------------------------------------------------------------------
1 | name: Add to Triage
2 |
3 | on:
4 | issues:
5 | types:
6 | - opened
7 |
8 | jobs:
9 | add-to-project:
10 | name: Add issue to project
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/add-to-project@v0.4.0
14 | with:
15 | project-url: https://github.com/orgs/eslint/projects/3
16 | github-token: ${{ secrets.PROJECT_BOT_TOKEN }}
17 | labeled: "triage:no"
18 | label-operator: NOT
19 |
--------------------------------------------------------------------------------
/.github/workflows/bun-test.yml:
--------------------------------------------------------------------------------
1 | name: Bun CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | build:
13 | runs-on: ${{ matrix.os }}
14 |
15 | strategy:
16 | matrix:
17 | os: [windows-latest, macOS-latest, ubuntu-latest]
18 | bun: [latest]
19 |
20 | steps:
21 | - uses: actions/checkout@v4
22 | - name: Use Bun ${{ matrix.bun }} ${{ matrix.os }}
23 | uses: oven-sh/setup-bun@v2
24 | with:
25 | bun-version: ${{ matrix.bun }}
26 | - name: bun install, build, and test
27 | run: |
28 | bun install
29 | bun run --bun test
30 | env:
31 | CI: true
32 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | lint:
11 | name: Lint
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v4
16 | - name: Install Node.js
17 | uses: actions/setup-node@v4
18 | with:
19 | node-version: lts/*
20 | - name: Install Packages
21 | run: npm install
22 | env:
23 | CI: true
24 | - name: Lint
25 | run: npm run lint
26 |
27 | format:
28 | name: File Format
29 | runs-on: ubuntu-latest
30 | steps:
31 | - name: Checkout
32 | uses: actions/checkout@v4
33 | - name: Install Node.js
34 | uses: actions/setup-node@v4
35 | with:
36 | node-version: lts/*
37 | - name: Install Packages
38 | run: npm install
39 | env:
40 | CI: true
41 | - name: Prettier Check
42 | run: npm run fmt:check
43 |
44 | test:
45 | name: Test
46 | strategy:
47 | matrix:
48 | os: [ubuntu-latest]
49 | eslint: [9]
50 | node: [24.x, 22.x, 20.x, 18.x, "18.18.0"]
51 | include:
52 | - os: windows-latest
53 | eslint: 9
54 | node: 20
55 | - os: macOS-latest
56 | eslint: 9
57 | node: 20
58 | runs-on: ${{ matrix.os }}
59 | steps:
60 | - name: Checkout
61 | uses: actions/checkout@v4
62 | - name: Install Node.js ${{ matrix.node }}
63 | uses: actions/setup-node@v4
64 | with:
65 | node-version: ${{ matrix.node }}
66 | - name: Install Packages
67 | run: npm install
68 | env:
69 | CI: true
70 | - name: Install ESLint@${{ matrix.eslint }}
71 | run: npm install eslint@${{ matrix.eslint }}
72 | - name: Test
73 | run: npm run test
74 | test_types:
75 | name: Test Types
76 | runs-on: ubuntu-latest
77 | steps:
78 | - uses: actions/checkout@v4
79 | - name: Setup Node.js
80 | uses: actions/setup-node@v4
81 | with:
82 | node-version: "lts/*"
83 | - name: Install dependencies
84 | run: npm install
85 | - name: Build
86 | run: npm run build
87 | - name: Check Types
88 | run: npm run test:types
89 | jsr_test:
90 | name: Verify JSR Publish
91 | runs-on: ubuntu-latest
92 | steps:
93 | - uses: actions/checkout@v4
94 | - uses: actions/setup-node@v4
95 | with:
96 | node-version: "lts/*"
97 | - name: Install Packages
98 | run: npm install
99 | - name: Run --dry-run
100 | run: |
101 | npm run build
102 | npm run test:jsr
103 |
--------------------------------------------------------------------------------
/.github/workflows/release-please.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - main
5 | name: release-please
6 | jobs:
7 | release-please:
8 | runs-on: ubuntu-latest
9 | permissions:
10 | contents: write
11 | pull-requests: write
12 | id-token: write
13 | models: read
14 | steps:
15 | - uses: googleapis/release-please-action@v4
16 | id: release
17 | - uses: actions/checkout@v4
18 | if: ${{ steps.release.outputs.release_created }}
19 | - uses: actions/setup-node@v4
20 | with:
21 | node-version: lts/*
22 | registry-url: https://registry.npmjs.org
23 | if: ${{ steps.release.outputs.release_created }}
24 | - name: Publish to npm
25 | run: |
26 | npm install
27 | npm run build --if-present
28 | npm publish --provenance
29 | env:
30 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
31 | if: ${{ steps.release.outputs.release_created }}
32 | - name: Publish to JSR
33 | run: |
34 | npm run build --if-present
35 | npx jsr publish
36 | if: ${{ steps.release.outputs.release_created }}
37 |
38 | # Generates the social media post
39 | - run: npx @humanwhocodes/social-changelog --org ${{ github.repository_owner }} --repo ${{ github.event.repository.name }} --tag ${{ steps.release.outputs.tag_name }} > social-post.txt
40 | if: ${{ steps.release.outputs.release_created }}
41 | env:
42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43 |
44 | - name: Post release announcement
45 | run: npx @humanwhocodes/crosspost -t -b -m --discord-webhook --file social-post.txt
46 | if: ${{ steps.release.outputs.release_created }}
47 | env:
48 | TWITTER_API_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }}
49 | TWITTER_API_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }}
50 | TWITTER_ACCESS_TOKEN_KEY: ${{ secrets.TWITTER_ACCESS_TOKEN_KEY }}
51 | TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
52 | MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }}
53 | MASTODON_HOST: ${{ secrets.MASTODON_HOST }}
54 | BLUESKY_IDENTIFIER: ${{ vars.BLUESKY_IDENTIFIER }}
55 | BLUESKY_PASSWORD: ${{ secrets.BLUESKY_PASSWORD }}
56 | BLUESKY_HOST: ${{ vars.BLUESKY_HOST }}
57 | DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
58 |
--------------------------------------------------------------------------------
/.github/workflows/update-readme.yml:
--------------------------------------------------------------------------------
1 | name: Data Fetch
2 |
3 | on:
4 | schedule:
5 | - cron: "0 8 * * *" # Every day at 1am PDT
6 | workflow_dispatch:
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Check out repo
13 | uses: actions/checkout@v4
14 | with:
15 | token: ${{ secrets.WORKFLOW_PUSH_BOT_TOKEN }}
16 |
17 | - name: Set up Node.js
18 | uses: actions/setup-node@v4
19 |
20 | - name: Install npm packages
21 | run: npm install
22 |
23 | - name: Update README with latest sponsor data
24 | run: npm run build:readme
25 |
26 | - name: Setup Git
27 | run: |
28 | git config user.name "GitHub Actions Bot"
29 | git config user.email ""
30 |
31 | - name: Save updated files
32 | run: |
33 | chmod +x ./tools/commit-readme.sh
34 | ./tools/commit-readme.sh
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vscode
3 | *.code-workspace
4 | coverage/
5 | node_modules/
6 | npm-debug.log
7 | .eslint-release-info.json
8 | .nyc_output
9 | yarn.lock
10 | package-lock.json
11 | pnpm-lock.yaml
12 | dist
13 | src/build
14 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock = false
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | CHANGELOG.md
3 | jsr.json
4 | **/*.md
5 |
--------------------------------------------------------------------------------
/.release-please-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | ".": "6.4.0"
3 | }
4 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Code
2 |
3 | Please sign the ESLint [Contributor License Agreement](https://eslint.org/cla)
4 |
5 | ## Full Documentation
6 |
7 | Our full contribution guidelines can be found at:
8 | http://eslint.org/docs/developer-guide/contributing/
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright OpenJS Foundation and other contributors, https://openjsf.org
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs/processors/markdown.md:
--------------------------------------------------------------------------------
1 | # Using the Markdown processor
2 |
3 | With this processor, ESLint will lint [fenced code blocks](https://www.markdownguide.org/extended-syntax/#fenced-code-blocks) in your Markdown documents. This processor uses [CommonMark](https://commonmark.org) format to evaluate the Markdown code, but this shouldn't matter because all Markdown dialects use the same format for code blocks. Here are some examples:
4 |
5 | ````markdown
6 | ```js
7 | // This gets linted
8 | var answer = 6 * 7;
9 | console.log(answer);
10 | ```
11 |
12 | Here is some regular Markdown text that will be ignored.
13 |
14 | ```js
15 | // This also gets linted
16 |
17 | /* eslint quotes: [2, "double"] */
18 |
19 | function hello() {
20 | console.log("Hello, world!");
21 | }
22 | hello();
23 | ```
24 |
25 | ```jsx
26 | // This can be linted too if you add `.jsx` files to file patterns in the `eslint.config.js`.
27 | var div =
;
28 | ```
29 | ````
30 |
31 | Blocks that don't specify a syntax are ignored:
32 |
33 | ````markdown
34 | ```
35 | This is plain text and doesn't get linted.
36 | ```
37 | ````
38 |
39 | Unless a fenced code block's syntax appears as a file extension in file patterns in your config file, it will be ignored.
40 |
41 | **Important:** You cannot combine this processor and Markdown-specific linting rules. You can either lint the code blocks or lint the Markdown, but not both. This is an ESLint limitation.
42 |
43 | ## Basic Configuration
44 |
45 | To enable the Markdown processor, use the `processor` configuration, which contains all of the configuration for setting up the plugin and processor to work on `.md` files:
46 |
47 | ```js
48 | // eslint.config.js
49 | import markdown from "@eslint/markdown";
50 |
51 | export default [
52 | ...markdown.configs.processor
53 |
54 | // your other configs here
55 | ];
56 | ```
57 |
58 | ## Advanced Configuration
59 |
60 | You can manually include the Markdown processor by setting the `processor` option in your configuration file for all `.md` files.
61 |
62 | Each fenced code block inside a Markdown document has a virtual filename appended to the Markdown file's path.
63 |
64 | The virtual filename's extension will match the fenced code block's syntax tag, except for the following:
65 |
66 | * `javascript` and `ecmascript` are mapped to `js`
67 | * `typescript` is mapped to `ts`
68 | * `markdown` is mapped to `md`
69 |
70 | For example, ```` ```js ```` code blocks in `README.md` would match `README.md/*.js` and ```` ```typescript ```` in `CONTRIBUTING.md` would match `CONTRIBUTING.md/*.ts`.
71 |
72 | You can use glob patterns for these virtual filenames to customize configuration for code blocks without affecting regular code.
73 | For more information on configuring processors, refer to the [ESLint documentation](https://eslint.org/docs/latest/use/configure/plugins#specify-a-processor).
74 |
75 | Here's an example:
76 |
77 | ```js
78 | // eslint.config.js
79 | import markdown from "@eslint/markdown";
80 |
81 | export default [
82 | {
83 | // 1. Add the plugin
84 | plugins: {
85 | markdown
86 | }
87 | },
88 | {
89 | // 2. Enable the Markdown processor for all .md files.
90 | files: ["**/*.md"],
91 | processor: "markdown/markdown"
92 | },
93 | {
94 | // 3. Optionally, customize the configuration ESLint uses for ```js
95 | // fenced code blocks inside .md files.
96 | files: ["**/*.md/*.js"],
97 | // ...
98 | rules: {
99 | // ...
100 | }
101 | }
102 |
103 | // your other configs here
104 | ];
105 | ```
106 |
107 | ## Frequently-Disabled Rules
108 |
109 | Some rules that catch mistakes in regular code are less helpful in documentation.
110 | For example, `no-undef` would flag variables that are declared outside of a code snippet because they aren't relevant to the example.
111 | The `markdown.configs.processor` config disables these rules in Markdown files:
112 |
113 | - [`no-undef`](https://eslint.org/docs/rules/no-undef)
114 | - [`no-unused-expressions`](https://eslint.org/docs/rules/no-unused-expressions)
115 | - [`no-unused-vars`](https://eslint.org/docs/rules/no-unused-vars)
116 | - [`padded-blocks`](https://eslint.org/docs/rules/padded-blocks)
117 |
118 | Use glob patterns to disable more rules just for Markdown code blocks:
119 |
120 | ```js
121 | // / eslint.config.js
122 | import markdown from "@eslint/markdown";
123 |
124 | export default [
125 | {
126 | plugins: {
127 | markdown
128 | }
129 | },
130 | {
131 | files: ["**/*.md"],
132 | processor: "markdown/markdown"
133 | },
134 | {
135 | // 1. Target ```js code blocks in .md files.
136 | files: ["**/*.md/*.js"],
137 | rules: {
138 | // 2. Disable other rules.
139 | "no-console": "off",
140 | "import/no-unresolved": "off"
141 | }
142 | }
143 |
144 | // your other configs here
145 | ];
146 | ```
147 |
148 | ## Additional Notes
149 |
150 | Here are some other things to keep in mind when linting code blocks.
151 |
152 | ### Strict Mode
153 |
154 | `"use strict"` directives in every code block would be annoying.
155 | The `markdown.configs.processor` config enables the [`impliedStrict` parser option](https://eslint.org/docs/latest/use/configure/parser#configure-parser-options) and disables the [`strict` rule](https://eslint.org/docs/rules/strict) in Markdown files.
156 | This opts into strict mode parsing without repeated `"use strict"` directives.
157 |
158 | ### Unsatisfiable Rules
159 |
160 | Markdown code blocks are not real files, so ESLint's file-format rules do not apply.
161 | The `markdown.configs.processor` config disables these rules in Markdown files:
162 |
163 | - [`eol-last`](https://eslint.org/docs/rules/eol-last): The Markdown parser trims trailing newlines from code blocks.
164 | - [`unicode-bom`](https://eslint.org/docs/rules/unicode-bom): Markdown code blocks do not have Unicode Byte Order Marks.
165 |
166 | ### Autofixing
167 |
168 | With this plugin, [ESLint's `--fix` option](https://eslint.org/docs/latest/use/command-line-interface#fix-problems) can automatically fix some issues in your Markdown fenced code blocks.
169 | To enable this, pass the `--fix` flag when you run ESLint:
170 |
171 | ```bash
172 | eslint --fix .
173 | ```
174 |
175 | ## Configuration Comments
176 |
177 | The processor will convert HTML comments immediately preceding a code block into JavaScript block comments and insert them at the beginning of the source code that it passes to ESLint.
178 | This permits configuring ESLint via configuration comments while keeping the configuration comments themselves hidden when the markdown is rendered.
179 | Comment bodies are passed through unmodified, so the plugin supports any [configuration comments](http://eslint.org/docs/user-guide/configuring) supported by ESLint itself.
180 |
181 | This example enables the `alert` global variable, disables the `no-alert` rule, and configures the `quotes` rule to prefer single quotes:
182 |
183 | ````markdown
184 |
185 |
186 |
187 |
188 | ```js
189 | alert('Hello, world!');
190 | ```
191 | ````
192 |
193 | Each code block in a file is linted separately, so configuration comments apply only to the code block that immediately follows.
194 |
195 | ````markdown
196 | Assuming `no-alert` is enabled in `eslint.config.js`, the first code block will have no error from `no-alert`:
197 |
198 |
199 |
200 |
201 | ```js
202 | alert("Hello, world!");
203 | ```
204 |
205 | But the next code block will have an error from `no-alert`:
206 |
207 |
208 |
209 | ```js
210 | alert("Hello, world!");
211 | ```
212 | ````
213 |
214 | ### Skipping Blocks
215 |
216 | Sometimes it can be useful to have code blocks marked with `js` even though they don't contain valid JavaScript syntax, such as commented JSON blobs that need `js` syntax highlighting.
217 | Standard `eslint-disable` comments only silence rule reporting, but ESLint still reports any syntax errors it finds.
218 | In cases where a code block should not even be parsed, insert a non-standard `` comment before the block, and this plugin will hide the following block from ESLint.
219 | Neither rule nor syntax errors will be reported.
220 |
221 | ````markdown
222 | There are comments in this JSON, so we use `js` syntax for better
223 | highlighting. Skip the block to prevent warnings about invalid syntax.
224 |
225 |
226 |
227 | ```js
228 | {
229 | // This code block is hidden from ESLint.
230 | "hello": "world"
231 | }
232 | ```
233 |
234 | ```js
235 | console.log("This code block is linted normally.");
236 | ```
237 | ````
238 |
--------------------------------------------------------------------------------
/docs/rules/fenced-code-language.md:
--------------------------------------------------------------------------------
1 | # fenced-code-language
2 |
3 | Require languages for fenced code blocks.
4 |
5 | ## Background
6 |
7 | One of the ways that Markdown allows you to embed syntax-highlighted blocks of other languages is using fenced code blocks, such as:
8 |
9 | ````markdown
10 | ```js
11 | const message = "Hello, world!";
12 | console.log(message);
13 | ```
14 | ````
15 |
16 | The language name is expected, but not required, after the initial three backticks. In general, it's a good idea to provide a language because that allows editors and converters to properly syntax highlight the embedded code. Even if you're just embedding plain text, it's preferable to use `text` as the language to indicate your intention.
17 |
18 | ## Rule Details
19 |
20 | This rule warns when it finds code blocks without a language specified.
21 |
22 | Examples of **incorrect** code for this rule:
23 |
24 | ````markdown
25 |
26 |
27 | ```
28 | const message = "Hello, world!";
29 | console.log(message);
30 | ```
31 | ````
32 |
33 | ## Options
34 |
35 | The following options are available on this rule:
36 |
37 | * `required: Array` - when specified, fenced code blocks must use one of the languages specified in this array.
38 |
39 | Examples of **incorrect** code when configured as `"fenced-code-language": ["error", { required: ["js"] }]`:
40 |
41 | ````markdown
42 |
43 |
44 | ```javascript
45 | const message = "Hello, world!";
46 | console.log(message);
47 | ```
48 | ````
49 |
50 | ## When Not to Use It
51 |
52 | If you don't mind omitting the language for fenced code blocks, you can safely disable this rule.
53 |
54 | ## Prior Art
55 |
56 | * [MD040 - Fenced code blocks should have a language specified](https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md#md040---fenced-code-blocks-should-have-a-language-specified)
57 | * [MD040 fenced-code-language](https://github.com/DavidAnson/markdownlint/blob/main/doc/md040.md)
58 |
--------------------------------------------------------------------------------
/docs/rules/heading-increment.md:
--------------------------------------------------------------------------------
1 | # heading-increment
2 |
3 | Enforce heading levels increment by one.
4 |
5 | ## Background
6 |
7 | It can be difficult to keep track of the correct heading levels in a long document. Most of the time, you want to increment heading levels by one, so inside of a heading level 1 you'll have one or more heading level 2s. If you've skipped from, for example, heading level 1 to heading level 3, that is most likely an error.
8 |
9 | ## Rule Details
10 |
11 | This rule warns when it finds a heading that is more than one level higher than the preceding heading.
12 |
13 | Examples of **incorrect** code for this rule:
14 |
15 | ```markdown
16 |
17 |
18 | # Hello world!
19 |
20 | ### Hello world!
21 |
22 | Goodbye World!
23 | --------------
24 |
25 | #EEE Goodbye World!
26 | ```
27 |
28 | ## When Not to Use It
29 |
30 | If you aren't concerned with enforcing heading levels increment by one, you can safely disable this rule.
31 |
32 | ## Prior Art
33 |
34 | * [MD001 - Header levels should only increment by one level at a time](https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md#md001---header-levels-should-only-increment-by-one-level-at-a-time)
35 | * [MD001 - heading-increment](https://github.com/DavidAnson/markdownlint/blob/main/doc/md001.md)
36 |
--------------------------------------------------------------------------------
/docs/rules/no-duplicate-definitions.md:
--------------------------------------------------------------------------------
1 | # no-duplicate-definitions
2 |
3 | Disallow duplicate definitions.
4 |
5 | ## Background
6 |
7 | In Markdown, it's possible to define the same definition identifier multiple times. However, this is usually a mistake, as it can lead to unintended or incorrect link, image, and footnote references.
8 |
9 | Please note that this rule does not report definition-style comments. For example:
10 |
11 | ```markdown
12 | [//]: # (This is a comment 1)
13 | [//]: <> (This is a comment 2)
14 | ```
15 |
16 | ## Rule Details
17 |
18 | > [!IMPORTANT]
19 | >
20 | > The `FootnoteDefinition` node is detected only when using `language` mode [`markdown/gfm`](/README.md#languages).
21 |
22 | This rule warns when `Definition` and `FootnoteDefinition` type identifiers are defined multiple times. Please note that this rule is **case-insensitive**, meaning `earth` and `Earth` are treated as the same identifier.
23 |
24 | Examples of **incorrect** code:
25 |
26 | ```markdown
27 |
28 |
29 |
30 | [mercury]: https://example.com/mercury/
31 | [mercury]: https://example.com/venus/
32 |
33 | [earth]: https://example.com/earth/
34 | [Earth]: https://example.com/mars/
35 |
36 |
37 |
38 | [^mercury]: Hello, Mercury!
39 | [^mercury]: Hello, Venus!
40 |
41 | [^earth]: Hello, Earth!
42 | [^Earth]: Hello, Mars!
43 | ```
44 |
45 | Examples of **correct** code:
46 |
47 | ```markdown
48 |
49 |
50 |
51 | [mercury]: https://example.com/mercury/
52 | [venus]: https://example.com/venus/
53 |
54 |
55 |
56 | [^mercury]: Hello, Mercury!
57 | [^venus]: Hello, Venus!
58 |
59 |
60 |
61 | [//]: # (This is a comment 1)
62 | [//]: <> (This is a comment 2)
63 | ```
64 |
65 | ## Options
66 |
67 | The following options are available on this rule:
68 |
69 | - `allowDefinitions: Array` - when specified, duplicate definitions are allowed if they match one of the identifiers in this array. This is useful for ignoring definitions that are intentionally duplicated. (default: `["//"]`)
70 |
71 | Examples of **correct** code when configured as `"no-duplicate-definitions: ["error", { allowDefinitions: ["mercury"] }]`:
72 |
73 | ```markdown
74 |
75 | [mercury]: https://example.com/mercury/
76 | [mercury]: https://example.com/venus/
77 | ```
78 |
79 | - `allowFootnoteDefinitions: Array` - when specified, duplicate footnote definitions are allowed if they match one of the identifiers in this array. This is useful for ignoring footnote definitions that are intentionally duplicated. (default: `[]`)
80 |
81 | Examples of **correct** code when configured as `"no-duplicate-definitions: ["error", { allowFootnoteDefinitions: ["mercury"] }]`:
82 |
83 | ```markdown
84 |
85 | [^mercury]: Hello, Mercury!
86 | [^mercury]: Hello, Venus!
87 | ```
88 |
89 | ## When Not to Use It
90 |
91 | If you are using a different style of definition comments, or not concerned with duplicate definitions, you can safely disable this rule.
92 |
--------------------------------------------------------------------------------
/docs/rules/no-duplicate-headings.md:
--------------------------------------------------------------------------------
1 | # no-duplicate-headings
2 |
3 | Disallow duplicate headings in the same document.
4 |
5 | ## Background
6 |
7 | Headings in Markdown documents are often used in a variety ways:
8 |
9 | 1. To generate in-document links
10 | 1. To generate a table of contents
11 |
12 | When generating in-document links, unique headings are necessary to ensure you can navigate to a specific heading. Generated tables of contents then use those links, and when there are duplicate headings, you can only link to the first instance.
13 |
14 | ## Rule Details
15 |
16 | This rule warns when it finds more than one heading with the same text, even if the headings are of different levels.
17 |
18 | Examples of **incorrect** code for this rule:
19 |
20 | ```markdown
21 |
22 |
23 | # Hello world!
24 |
25 | ## Hello world!
26 |
27 | Goodbye World!
28 | --------------
29 |
30 | # Goodbye World!
31 | ```
32 |
33 | ## When Not to Use It
34 |
35 | If you aren't concerned with autolinking heading or autogenerating a table of contents, you can safely disable this rule.
36 |
37 | ## Prior Art
38 |
39 | * [MD024 - Multiple headers with the same content](https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md#md024---multiple-headers-with-the-same-content)
40 | * [MD024 - no-duplicate-heading](https://github.com/DavidAnson/markdownlint/blob/main/doc/md024.md)
41 |
--------------------------------------------------------------------------------
/docs/rules/no-empty-definitions.md:
--------------------------------------------------------------------------------
1 | # no-empty-definitions
2 |
3 | Disallow empty definitions.
4 |
5 | ## Background
6 |
7 | Markdown allows you to specify a label as a placeholder for a URL in both links and images using square brackets, such as:
8 |
9 | ```markdown
10 | [ESLint][eslint]
11 |
12 | [eslint]: https://eslint.org
13 | ```
14 |
15 | If the definition's URL is empty or only contains an empty fragment (`#`), then it's not providing any useful information and could be a mistake.
16 |
17 | ## Rule Details
18 |
19 | This rule warns when it finds definitions where the URL is either not specified or contains only an empty fragment (`#`).
20 |
21 | Examples of **incorrect** code for this rule:
22 |
23 | ```markdown
24 |
25 |
26 | [earth]: <>
27 | [moon]: #
28 | ```
29 |
30 | Examples of correct code:
31 |
32 | ```markdown
33 |
34 |
35 | [earth]: https://example.com/earth/
36 | [moon]: #section
37 | ```
38 |
39 | ## When Not to Use It
40 |
41 | If you aren't concerned with empty definitions, you can safely disable this rule.
42 |
43 | ## Prior Art
44 |
45 | * [remark-lint-no-empty-url](https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-no-empty-url)
--------------------------------------------------------------------------------
/docs/rules/no-empty-images.md:
--------------------------------------------------------------------------------
1 | # no-empty-images
2 |
3 | Disallow empty images.
4 |
5 | ## Background
6 |
7 | In Markdown, it’s not always easy to spot when you’ve forgotten to provide a destination for an image. This is especially common when you’re writing something in Markdown and intend to insert an image, leaving the destination to fill in later, only to forget to go back and complete it. This often results in broken image links.
8 |
9 | ## Rule Details
10 |
11 | This rule warns when it finds images that either don't have a URL specified or have only an empty fragment (`"#"`).
12 |
13 | Examples of **incorrect** code for this rule:
14 |
15 | ```markdown
16 |
17 |
18 | ![]()
19 |
20 | ![ESLint Logo]()
21 |
22 | 
23 |
24 | 
25 | ```
26 |
27 | Exmaples of correct code:
28 |
29 | ```markdown
30 |
31 |
32 | 
33 |
34 | 
35 | ```
36 |
37 | ## When Not to Use It
38 |
39 | If you aren't concerned with empty images, you can safely disable this rule.
40 |
41 | ## Prior Art
42 |
43 | * [remark-lint-no-empty-url](https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-no-empty-url)
44 |
--------------------------------------------------------------------------------
/docs/rules/no-empty-links.md:
--------------------------------------------------------------------------------
1 | # no-empty-links
2 |
3 | Disallow empty links.
4 |
5 | ## Background
6 |
7 | Markdown syntax can make it difficult to easily see that you've forgotten to give a link a destination. This is especially true when writing prose in Markdown, in which case you may intend to create a link but leave the destination for later...and then forget to go back and add it.
8 |
9 | ## Rule Details
10 |
11 | This rule warns when it finds links that either don't have a URL specified or have only an empty fragment (`"#"`).
12 |
13 | Examples of **incorrect** code for this rule:
14 |
15 | ```markdown
16 |
17 |
18 | [ESLint]()
19 |
20 | [Skip to Content](#)
21 | ```
22 |
23 | ## When Not to Use It
24 |
25 | If you aren't concerned with empty links, you can safely disable this rule.
26 |
27 | ## Prior Art
28 |
29 | * [MD042 - no-empty-links](https://github.com/DavidAnson/markdownlint/blob/main/doc/md042.md)
30 |
--------------------------------------------------------------------------------
/docs/rules/no-html.md:
--------------------------------------------------------------------------------
1 | # no-html
2 |
3 | Disallow HTML tags.
4 |
5 | ## Background
6 |
7 | By default, Markdown allows you to use HTML tags mixed in with Markdown syntax. In some cases, you may want to restrict the use of HTML to ensure that the output is predictable when converting the Markdown to HTML.
8 |
9 | ## Rule Details
10 |
11 | This rule warns when it finds HTML tags inside Markdown content.
12 |
13 | Examples of **incorrect** code for this rule:
14 |
15 | ```markdown
16 |
17 |
18 | # Heading 1
19 |
20 | Hello world!
21 | ```
22 |
23 | ## Options
24 |
25 | The following options are available on this rule:
26 |
27 | * `allowed: Array` - when specified, HTML tags are allowed only if they match one of the tags in this array.
28 |
29 | Examples of **incorrect** code when configured as `"no-html": ["error", { allowed: ["b"] }]`:
30 |
31 | ```markdown
32 |
33 |
34 | # Heading 1
35 |
36 | Hello world!
37 | ```
38 |
39 | Examples of **correct** code when configured as `"no-html": ["error", { allowed: ["b"] }]`:
40 |
41 | ```markdown
42 |
43 |
44 | # Heading 1
45 |
46 | Hello world!
47 | ```
48 |
49 | ## When Not to Use It
50 |
51 | If you aren't concerned with HTML tags, you can safely disable this rule.
52 |
53 | ## Prior Art
54 |
55 | * [MD033 - Inline HTML](https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md#md033---inline-html)
56 | * [MD033 - no-inline-html](https://github.com/DavidAnson/markdownlint/blob/main/doc/md033.md)
57 |
--------------------------------------------------------------------------------
/docs/rules/no-invalid-label-refs.md:
--------------------------------------------------------------------------------
1 | # no-invalid-label-refs
2 |
3 | Disallow invalid label references.
4 |
5 | ## Background
6 |
7 | CommonMark allows you to specify a label as a placeholder for a URL in both links and images using square brackets, such as:
8 |
9 | ```markdown
10 | [ESLint][eslint]
11 | [eslint][]
12 | [eslint]
13 |
14 | [eslint]: https://eslint.org
15 | ```
16 |
17 | The shorthand form, `[label][]` does not allow any white space between the brackets, and when found, doesn't treat this as a link reference.
18 |
19 | Confusingly, GitHub still treats this as a label reference and will render it as if there is no white space between the brackets. Relying on this behavior could result in errors when using CommonMark-compliant renderers.
20 |
21 | ## Rule Details
22 |
23 | This rule warns when it finds text that looks like it's a shorthand label reference and there's white space between the brackets.
24 |
25 | Examples of **incorrect** code for this rule:
26 |
27 | ```markdown
28 |
29 |
30 | [eslint][ ]
31 |
32 | [eslint][
33 |
34 | ]
35 | ```
36 |
37 | ## When Not to Use It
38 |
39 | If you publish your Markdown exclusively on GitHub, then you can safely disable this rule.
40 |
--------------------------------------------------------------------------------
/docs/rules/no-missing-atx-heading-space.md:
--------------------------------------------------------------------------------
1 | # no-missing-atx-heading-space
2 |
3 | This rule warns when spaces are missing after the hash characters in an ATX style heading.
4 |
5 | ## Rule Details
6 |
7 | In Markdown, headings can be created using ATX style (using hash (`#`) characters at the beginning of the line) or Setext style (using underlining with equals (`=`) or hyphens (`-`)).
8 |
9 | For ATX style headings, a space should be used after the hash characters to improve readability and ensure proper rendering across various Markdown parsers.
10 |
11 | This rule is automatically fixable by the `--fix` command line option.
12 |
13 | Examples of **incorrect** code for this rule:
14 |
15 | ```md
16 |
17 |
18 | #Heading 1
19 | ##Heading 2
20 | ###Heading 3
21 | ```
22 |
23 | Examples of **correct** code for this rule:
24 |
25 | ```md
26 |
27 |
28 | # Heading 1
29 | ## Heading 2
30 | ### Heading 3
31 |
32 | #Some Heading
33 |
34 | [#123](link.com)
35 |
36 | ![#alttext][link.png]
37 |
38 | This is a paragraph with a #hashtag, not a heading.
39 | ```
40 |
41 | ## When Not To Use It
42 |
43 | You might want to turn this rule off if you're working with a Markdown variant that doesn't require spaces after hash characters in headings.
44 |
45 | ## Prior Art
46 |
47 | - [MD018 - No space after hash on atx style heading](https://github.com/DavidAnson/markdownlint/blob/main/doc/md018.md)
48 |
49 | ## Further Reading
50 |
51 | - [Markdown Syntax: Headings](https://daringfireball.net/projects/markdown/syntax#header)
52 | - [CommonMark Spec: ATX Headings](https://spec.commonmark.org/0.30/#atx-headings)
--------------------------------------------------------------------------------
/docs/rules/no-missing-label-refs.md:
--------------------------------------------------------------------------------
1 | # no-missing-label-refs
2 |
3 | Disallow missing label references.
4 |
5 | ## Background
6 |
7 | Markdown allows you to specify a label as a placeholder for a URL in both links and images using square brackets, such as:
8 |
9 | ```markdown
10 | [ESLint][eslint]
11 |
12 | [eslint]: https://eslint.org
13 | ```
14 |
15 | If the label is never defined, then Markdown doesn't render a link and instead renders plain text.
16 |
17 | ## Rule Details
18 |
19 | This rule warns when it finds text that looks like it's a label but the label reference doesn't exist.
20 |
21 | Examples of **incorrect** code for this rule:
22 |
23 | ```markdown
24 |
25 |
26 | [ESLint][eslint]
27 |
28 | [eslint][]
29 |
30 | [eslint]
31 | ```
32 |
33 | ## When Not to Use It
34 |
35 | If you aren't concerned with missing label references, you can safely disable this rule.
36 |
37 | ## Prior Art
38 |
39 | * [MD052 - reference-links-images](https://github.com/DavidAnson/markdownlint/blob/main/doc/md052.md)
40 |
--------------------------------------------------------------------------------
/docs/rules/no-multiple-h1.md:
--------------------------------------------------------------------------------
1 | # no-multiple-h1
2 |
3 | Disallow multiple H1 headings in the same document.
4 |
5 | ## Background
6 |
7 | An H1 heading is meant to define the main heading of a page, providing important structural information for both users and assistive technologies. Using more than one H1 heading per page can cause confusion for screen readers, dilute SEO signals, and break the logical content hierarchy. While modern search engines are more forgiving, best practice is to use a single H1 heading to ensure clarity and accessibility.
8 |
9 | ## Rule Details
10 |
11 | This rule warns when it finds more than one H1 heading in a Markdown document. It checks for:
12 |
13 | - ATX-style headings (`# Heading`)
14 | - Setext-style headings (`Heading\n=========`)
15 | - Front matter title fields (YAML and TOML)
16 | - HTML h1 tags (`Heading `)
17 |
18 | Examples of **incorrect** code for this rule:
19 |
20 | ```markdown
21 |
22 |
23 | # Heading 1
24 |
25 | # Another H1 heading
26 | ```
27 |
28 | ```markdown
29 |
30 |
31 | # Heading 1
32 |
33 | Another H1 heading
34 | ==================
35 | ```
36 |
37 | ```markdown
38 |
39 |
40 | First Heading
41 |
42 | Second Heading
43 | ```
44 |
45 | ## Options
46 |
47 | The following options are available on this rule:
48 |
49 | * `frontmatterTitle: string` - A regex pattern to match title fields in front matter. The default pattern matches both YAML (`title:`) and TOML (`title =`) formats. Set to an empty string to disable front matter title checking.
50 |
51 | Examples of **incorrect** code for this rule:
52 |
53 | ```markdown
54 |
55 |
56 | ---
57 | title: My Title
58 | ---
59 |
60 | # Heading 1
61 | ```
62 |
63 | Examples of **incorrect** code when configured as `"no-multiple-h1": ["error", { "frontmatterTitle": "\\s*heading\\s*[:=]" }]`:
64 |
65 | ```markdown
66 |
67 |
68 | ---
69 | heading: My Title
70 | ---
71 |
72 | # Heading 1
73 | ```
74 |
75 | Examples of **correct** code when configured as `"no-multiple-h1": ["error", { "frontmatterTitle": "" }]`:
76 |
77 | ```markdown
78 |
79 |
80 | ---
81 | title: My Title
82 | ---
83 |
84 | # Heading 1
85 | ```
86 |
87 | ## When Not to Use It
88 |
89 | If you have a specific use case that requires multiple H1 headings in a single Markdown document, you can safely disable this rule. However, this is not recommended.
90 |
91 | ## Prior Art
92 |
93 | * [MD025 - Multiple top-level headings in the same document](https://github.com/DavidAnson/markdownlint/blob/main/doc/md025.md)
--------------------------------------------------------------------------------
/docs/rules/require-alt-text.md:
--------------------------------------------------------------------------------
1 | # require-alt-text
2 |
3 | Require alternative text for images.
4 |
5 | ## Background
6 |
7 | Providing alternative text for images is essential for accessibility. Without alternative text, users who rely on screen readers or other assistive technologies won't be able to understand the content of images. This rule helps catch cases where alternative text is missing or contains only whitespace.
8 |
9 | ## Rule Details
10 |
11 | This rule warns when it finds images that either don't have alternative text or have only whitespace as alternative text.
12 |
13 | This rule does not warn when:
14 | - An HTML image has an empty alt attribute (`alt=""`)
15 | - An HTML image has the `aria-hidden="true"` attribute
16 |
17 | Examples of **incorrect** code:
18 |
19 | ```markdown
20 |
21 |
22 | 
23 |
24 | 
25 |
26 | ![][ref]
27 |
28 | [ref]: sunset.png
29 |
30 |
31 |
32 |
33 | ```
34 |
35 | Examples of **correct** code:
36 |
37 | ```markdown
38 |
39 |
40 | 
41 |
42 | ![Company logo][logo]
43 |
44 | [logo]: logo.png
45 |
46 |
47 |
48 |
49 |
50 |
51 | ```
52 |
53 | ## When Not to Use It
54 |
55 | If you aren't concerned with image accessibility or if your images are purely decorative and don't convey meaningful content, you can safely disable this rule.
56 |
57 | ## Prior Art
58 |
59 | * [MD045 - Images should have alternate text (alt text)](https://github.com/DavidAnson/markdownlint/blob/main/doc/md045.md)
--------------------------------------------------------------------------------
/docs/rules/table-column-count.md:
--------------------------------------------------------------------------------
1 | # table-column-count
2 |
3 | Disallow data rows in a GitHub Flavored Markdown table from having more cells than the header row.
4 |
5 | ## Background
6 |
7 | In GitHub Flavored Markdown [tables](https://github.github.com/gfm/#tables-extension-), rows should maintain a consistent number of cells. While variations are sometimes tolerated, data rows having *more* cells than the header can lead to lost data or rendering issues. This rule prevents data rows from exceeding the header's column count.
8 |
9 | ## Rule Details
10 |
11 | > [!IMPORTANT]
12 | >
13 | > This rule relies on the `table` AST node, typically available when using a GFM-compatible parser (e.g., `language: "markdown/gfm"`).
14 |
15 | This rule is triggered if a data row in a GFM table contains more cells than the header row. It does not flag rows with fewer cells than the header.
16 |
17 | Examples of **incorrect** code for this rule:
18 |
19 | ```markdown
20 |
21 |
22 | | Head1 | Head2 |
23 | | ----- | ----- |
24 | | R1C1 | R1C2 | R2C3 |
25 |
26 | | A |
27 | | - |
28 | | 1 | 2 |
29 | ```
30 |
31 | Examples of **correct** code for this rule:
32 |
33 | ```markdown
34 |
35 |
36 |
37 | | Header | Header |
38 | | ------ | ------ |
39 | | Cell | Cell |
40 | | Cell | Cell |
41 |
42 |
43 |
44 | | Header | Header | Header |
45 | | ------ | ------ | ------ |
46 | | Cell | Cell | |
47 |
48 |
49 |
50 | | Col A | Col B | Col C |
51 | | ----- | ----- | ----- |
52 | | 1 | | 3 |
53 | | 4 | 5 |
54 |
55 |
56 | | Single Header |
57 | | ------------- |
58 | | Single Cell |
59 | ```
60 |
61 | ## When Not To Use It
62 |
63 | If you intentionally create Markdown tables where data rows are expected to contain more cells than the header, and you have a specific (perhaps non-standard) processing or rendering pipeline that handles this scenario correctly, you might choose to disable this rule. However, adhering to this rule is recommended for typical GFM rendering and data consistency.
64 |
65 | ## Prior Art
66 |
67 | * [MD056 - table-column-count](https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md#md056---table-column-count)
68 |
--------------------------------------------------------------------------------
/eslint.config-content.js:
--------------------------------------------------------------------------------
1 | import markdown from "./src/index.js";
2 |
3 | export default [
4 | {
5 | name: "markdown/content/ignores",
6 | ignores: ["**/*.js", "**/.cjs", "**/.mjs"],
7 | },
8 | ...markdown.configs.recommended,
9 | ];
10 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | //-----------------------------------------------------------------------------
2 | // Imports
3 | //-----------------------------------------------------------------------------
4 |
5 | import globals from "globals";
6 | import eslintConfigESLint from "eslint-config-eslint";
7 | import eslintPlugin from "eslint-plugin-eslint-plugin";
8 | import markdown from "./src/index.js";
9 | import { defineConfig } from "eslint/config";
10 |
11 | //-----------------------------------------------------------------------------
12 | // Helpers
13 | //-----------------------------------------------------------------------------
14 |
15 | const eslintPluginRulesRecommendedConfig =
16 | eslintPlugin.configs["flat/rules-recommended"];
17 | const eslintPluginTestsRecommendedConfig =
18 | eslintPlugin.configs["flat/tests-recommended"];
19 |
20 | //-----------------------------------------------------------------------------
21 | // Configuration
22 | //-----------------------------------------------------------------------------
23 |
24 | export default defineConfig([
25 | eslintConfigESLint,
26 | {
27 | name: "markdown/js",
28 | files: ["**/*.js"],
29 | rules: {
30 | "no-undefined": "off",
31 | },
32 | },
33 | {
34 | name: "markdown/plugins",
35 | plugins: {
36 | markdown,
37 | },
38 | },
39 | {
40 | name: "markdown/ignores",
41 | ignores: [
42 | "**/examples",
43 | "**/coverage",
44 | "**/tests/fixtures",
45 | "dist",
46 | "src/build/",
47 | ],
48 | },
49 | {
50 | name: "markdown/tools",
51 | files: ["tools/**/*.js"],
52 | rules: {
53 | "no-console": "off",
54 | },
55 | },
56 | {
57 | name: "markdown/tests",
58 | files: ["tests/**/*.js"],
59 | languageOptions: {
60 | globals: {
61 | ...globals.mocha,
62 | },
63 | },
64 | rules: {
65 | "no-underscore-dangle": "off",
66 | },
67 | },
68 | {
69 | name: "markdown/code-blocks",
70 | files: ["**/*.md"],
71 | processor: "markdown/markdown",
72 | },
73 | {
74 | name: "markdown/code-blocks/js",
75 | files: ["**/*.md/*.js"],
76 | languageOptions: {
77 | sourceType: "module",
78 | parserOptions: {
79 | ecmaFeatures: {
80 | impliedStrict: true,
81 | },
82 | },
83 | },
84 | rules: {
85 | "lines-around-comment": "off",
86 | "n/no-missing-import": "off",
87 | "no-var": "off",
88 | "padding-line-between-statements": "off",
89 | "no-console": "off",
90 | "no-alert": "off",
91 | "@eslint-community/eslint-comments/require-description": "off",
92 | "jsdoc/require-jsdoc": "off",
93 | },
94 | },
95 | {
96 | files: ["src/rules/*.js"],
97 | extends: [eslintPluginRulesRecommendedConfig],
98 | rules: {
99 | "eslint-plugin/require-meta-schema": "off", // `schema` defaults to []
100 | "eslint-plugin/prefer-placeholders": "error",
101 | "eslint-plugin/prefer-replace-text": "error",
102 | "eslint-plugin/report-message-format": ["error", "[^a-z].*\\.$"],
103 | "eslint-plugin/require-meta-docs-description": [
104 | "error",
105 | { pattern: "^(Enforce|Require|Disallow) .+[^. ]$" },
106 | ],
107 | "eslint-plugin/require-meta-docs-url": [
108 | "error",
109 | {
110 | pattern:
111 | "https://github.com/eslint/markdown/blob/main/docs/rules/{{name}}.md",
112 | },
113 | ],
114 | },
115 | },
116 | {
117 | files: ["tests/rules/*.test.js"],
118 | extends: [eslintPluginTestsRecommendedConfig],
119 | rules: {
120 | "eslint-plugin/test-case-property-ordering": [
121 | "error",
122 | [
123 | "name",
124 | "filename",
125 | "code",
126 | "output",
127 | "language",
128 | "options",
129 | "languageOptions",
130 | "errors",
131 | ],
132 | ],
133 | "eslint-plugin/test-case-shorthand-strings": "error",
134 | },
135 | },
136 | ]);
137 |
--------------------------------------------------------------------------------
/examples/react/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock = false
2 |
--------------------------------------------------------------------------------
/examples/react/README.md:
--------------------------------------------------------------------------------
1 | # React Example
2 |
3 | ```jsx
4 | function App({ name }) {
5 | return (
6 |
9 | );
10 | }
11 | ```
12 |
13 | ```sh
14 | $ git clone https://github.com/eslint/markdown.git
15 | $ cd markdown
16 | $ npm install
17 | $ cd examples/react
18 | $ npm test
19 |
20 | markdown/examples/react/README.md
21 | 4:16 error 'name' is missing in props validation react/prop-types
22 |
23 | ✖ 1 problem (1 error, 0 warnings)
24 | ```
25 |
--------------------------------------------------------------------------------
/examples/react/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import js from "@eslint/js";
2 | import markdown from "../../src/index.js";
3 | import globals from "globals";
4 | import reactPlugin from "eslint-plugin-react";
5 |
6 | export default [
7 | js.configs.recommended,
8 | ...markdown.configs.processor,
9 | reactPlugin.configs.flat.recommended,
10 | {
11 | settings: {
12 | react: {
13 | version: "16.8.0",
14 | },
15 | },
16 | languageOptions: {
17 | parserOptions: {
18 | ecmaFeatures: {
19 | jsx: true,
20 | },
21 | },
22 | ecmaVersion: 2015,
23 | sourceType: "module",
24 | globals: globals.browser,
25 | },
26 | },
27 | {
28 | files: ["**/*.md/*.jsx"],
29 | languageOptions: {
30 | globals: {
31 | React: false,
32 | },
33 | },
34 | },
35 | ];
36 |
--------------------------------------------------------------------------------
/examples/react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "test": "eslint ."
5 | },
6 | "devDependencies": {
7 | "@eslint/js": "^9.7.0",
8 | "eslint": "^9.7.0",
9 | "eslint-plugin-react": "^7.35.0",
10 | "globals": "^13.24.0"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/examples/typescript/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock = false
2 |
--------------------------------------------------------------------------------
/examples/typescript/README.md:
--------------------------------------------------------------------------------
1 | # TypeScript Example
2 |
3 | The `@typescript-eslint` parser and the `recommended` config's rules will work in `ts` code blocks. However, [type-aware rules](https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/TYPED_LINTING.md) will not work because the code blocks are not part of a compilable `tsconfig.json` project.
4 |
5 | ```ts
6 | function hello(name: String) {
7 | console.log(`Hello, ${name}!`);
8 | }
9 |
10 | hello(42 as any);
11 | ```
12 |
13 | ```sh
14 | $ git clone https://github.com/eslint/markdown.git
15 | $ cd markdown
16 | $ npm install
17 | $ cd examples/typescript
18 | $ npm test
19 |
20 | markdown/examples/typescript/README.md
21 | 6:22 error Prefer using the primitive `string` as a type name, rather than the upper-cased `String` @typescript-eslint/no-wrapper-object-types
22 | 10:13 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
23 |
24 | ✖ 2 problems (2 errors, 0 warnings)
25 | 1 error and 0 warnings potentially fixable with the `--fix` option.
26 | ```
27 |
--------------------------------------------------------------------------------
/examples/typescript/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import js from "@eslint/js";
2 | import markdown from "../../src/index.js";
3 | import tseslint from "typescript-eslint";
4 |
5 | export default tseslint.config(
6 | js.configs.recommended,
7 | ...markdown.configs.processor,
8 | ...tseslint.configs.recommended.map(config => ({
9 | ...config,
10 | files: ["**/*.ts"],
11 | })),
12 | );
13 |
--------------------------------------------------------------------------------
/examples/typescript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "test": "eslint ."
5 | },
6 | "devDependencies": {
7 | "@eslint/js": "^9.7.0",
8 | "eslint": "^9.15.0",
9 | "typescript": "^5.3.3",
10 | "typescript-eslint": "^8.15.0"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/jsr.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@eslint/markdown",
3 | "version": "6.4.0",
4 | "exports": "./dist/esm/index.js",
5 | "publish": {
6 | "include": [
7 | "dist/esm/index.js",
8 | "dist/esm/index.d.ts",
9 | "dist/esm/types.ts",
10 | "dist/esm/types.d.ts",
11 | "README.md",
12 | "jsr.json",
13 | "LICENSE"
14 | ]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/npm-prepare.cjs:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Install examples' dependencies as part of local npm install for
3 | * development and CI.
4 | * @author btmills
5 | */
6 |
7 | "use strict";
8 |
9 | if (!process.env.NO_RECURSIVE_PREPARE) {
10 | const childProcess = require("node:child_process");
11 | const fs = require("node:fs");
12 | const path = require("node:path");
13 |
14 | const examplesDir = path.resolve(__dirname, "examples");
15 | const examples = fs
16 | .readdirSync(examplesDir)
17 | .filter(exampleDir =>
18 | fs.statSync(path.join(examplesDir, exampleDir)).isDirectory(),
19 | )
20 | .filter(exampleDir =>
21 | fs.existsSync(path.join(examplesDir, exampleDir, "package.json")),
22 | );
23 |
24 | for (const example of examples) {
25 | childProcess.execSync("npm install", {
26 | cwd: path.resolve(examplesDir, example),
27 | env: {
28 | ...process.env,
29 | NO_RECURSIVE_PREPARE: "true",
30 | },
31 | });
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@eslint/markdown",
3 | "version": "6.4.0",
4 | "description": "The official ESLint language plugin for Markdown",
5 | "license": "MIT",
6 | "author": {
7 | "name": "Brandon Mills",
8 | "url": "https://github.com/btmills"
9 | },
10 | "type": "module",
11 | "main": "dist/esm/index.js",
12 | "types": "dist/esm/index.d.ts",
13 | "exports": {
14 | ".": {
15 | "types": "./dist/esm/index.d.ts",
16 | "default": "./dist/esm/index.js"
17 | },
18 | "./types": {
19 | "types": "./dist/esm/types.d.ts"
20 | }
21 | },
22 | "files": [
23 | "dist"
24 | ],
25 | "publishConfig": {
26 | "access": "public"
27 | },
28 | "repository": "eslint/markdown",
29 | "bugs": {
30 | "url": "https://github.com/eslint/markdown/issues"
31 | },
32 | "homepage": "https://github.com/eslint/markdown#readme",
33 | "keywords": [
34 | "eslint",
35 | "eslintplugin",
36 | "markdown",
37 | "lint",
38 | "linter"
39 | ],
40 | "gitHooks": {
41 | "pre-commit": "lint-staged"
42 | },
43 | "lint-staged": {
44 | "*.js": [
45 | "eslint --fix",
46 | "prettier --write"
47 | ],
48 | "*.md": [
49 | "eslint --fix",
50 | "eslint --fix -c eslint.config-content.js"
51 | ],
52 | "!(*.{js,md})": "prettier --write --ignore-unknown",
53 | "{src/rules/*.js,tools/update-rules-docs.js,README.md}": "npm run build:update-rules-docs"
54 | },
55 | "scripts": {
56 | "lint": "eslint . && eslint -c eslint.config-content.js .",
57 | "lint:fix": "eslint --fix . && eslint --fix -c eslint.config-content.js .",
58 | "fmt": "prettier --write .",
59 | "fmt:check": "prettier --check .",
60 | "build:dedupe-types": "node tools/dedupe-types.js dist/esm/index.js",
61 | "build:rules": "node tools/build-rules.js",
62 | "build:update-rules-docs": "node tools/update-rules-docs.js",
63 | "build": "npm run build:rules && rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json && npm run build:update-rules-docs",
64 | "build:readme": "node tools/update-readme.js",
65 | "prepare": "node ./npm-prepare.cjs && npm run build",
66 | "test": "c8 mocha \"tests/**/*.test.js\" --timeout 30000",
67 | "test:jsr": "npx jsr@latest publish --dry-run",
68 | "test:types": "tsc -p tests/types/tsconfig.json"
69 | },
70 | "devDependencies": {
71 | "@eslint/js": "^9.4.0",
72 | "@types/eslint": "^9.6.0",
73 | "c8": "^10.1.2",
74 | "dedent": "^1.5.3",
75 | "eslint": "^9.25.1",
76 | "eslint-config-eslint": "^11.0.0",
77 | "eslint-plugin-eslint-plugin": "^6.3.2",
78 | "globals": "^15.1.0",
79 | "got": "^14.4.2",
80 | "lint-staged": "^15.2.9",
81 | "mocha": "^10.6.0",
82 | "prettier": "^3.3.3",
83 | "rollup": "^4.19.0",
84 | "rollup-plugin-copy": "^3.5.0",
85 | "rollup-plugin-delete": "^3.0.1",
86 | "typescript": "^5.5.4",
87 | "yorkie": "^2.0.0"
88 | },
89 | "dependencies": {
90 | "@eslint/core": "^0.14.0",
91 | "@eslint/plugin-kit": "^0.3.1",
92 | "mdast-util-from-markdown": "^2.0.2",
93 | "mdast-util-frontmatter": "^2.0.1",
94 | "mdast-util-gfm": "^3.0.0",
95 | "micromark-extension-frontmatter": "^2.0.0",
96 | "micromark-extension-gfm": "^3.0.0"
97 | },
98 | "engines": {
99 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | useTabs: true,
3 | tabWidth: 4,
4 | arrowParens: "avoid",
5 |
6 | overrides: [
7 | {
8 | files: ["*.json"],
9 | options: {
10 | tabWidth: 2,
11 | useTabs: false,
12 | },
13 | },
14 | ],
15 | };
16 |
--------------------------------------------------------------------------------
/release-please-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": {
3 | ".": {
4 | "release-type": "node",
5 | "pull-request-title-pattern": "chore: release ${version} 🚀",
6 | "include-component-in-tag": false,
7 | "extra-files": [
8 | "src/index.js",
9 | "src/processor.js",
10 | {
11 | "type": "json",
12 | "path": "jsr.json",
13 | "jsonpath": "$.version"
14 | }
15 | ]
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/rfcs/configure-file-name-from-block-meta.md:
--------------------------------------------------------------------------------
1 | # RFC: Configuring File Name from Block Meta
2 |
3 | ## Summary
4 |
5 | Allowing codeblocks to change the `filename` used in `eslint-plugin-markdown`'s processor using codeblock `meta` text.
6 |
7 | ## Motivation
8 |
9 | Some projects use ESLint `overrides` to run different lint rules on files based on their file name.
10 | There's no way to respect file name based `overrides` in parsed Markdown codeblocks right now using `eslint-plugin-markdown`'s parsing.
11 | This RFC would allow codeblocks to specify a custom file name so that `overrides` can be used more idiomatically.
12 |
13 | ### Real-World Example
14 |
15 | In [`create-typescript-app`](https://github.com/JoshuaKGoldberg/create-typescript-app), `*.json` files _except_ `package.json` files have [`jsonc/sort-keys`](https://ota-meshi.github.io/eslint-plugin-jsonc/rules/sort-keys.html) enabled using [an ESLint config file `overrides`](https://github.com/JoshuaKGoldberg/create-typescript-app/blob/76a75186fd89fc3f66e4c1254c717c28d70afe0d/.eslintrc.cjs#L94).
16 | However, a codeblock in a file named `docs/FAQs.md` intended to be a `package.json` is currently given a name like `create-typescript-app/docs/FAQs.md/0_0.jsonc`.
17 | Short of hardcoding overrides or disabling the lint rule in the file, there is no way to have the `overrides` apply to the markdown codeblock.
18 |
19 | ## Detailed Design
20 |
21 | This RFC proposes that codeblocks be allowed to specify a file path in `meta` (the \`\`\` opening fence) with `filename="..."`.
22 | Doing so would replace the `filename` provided by `eslint-plugin-markdown`'s `preprocess` method.
23 |
24 | ````md
25 | ```json filename="package.json"
26 | {}
27 | ```
28 | ````
29 |
30 | Parsing would be handled by a regular expression similar to the [Docusaurus parser](https://github.com/facebook/docusaurus/blob/7650829e913ec4bb1263d855719779f6b97066b6/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts#L12).
31 |
32 | Parsed language is ignored when the codeblock provides a custom title.
33 | Some languages have many file extensions, such as TypeScript's `.cts`, `.ts`, `.tsx`, etc.
34 | Some extensions may map to multiple languages, such as Perl and Prolog both using `.pl`.
35 |
36 | Roughly:
37 |
38 | ```diff
39 | - filename: `${index}.${block.lang.trim().split(" ")[0]}`,
40 | + filename: titleFromMeta(block) ?? `${index}.${block.lang.trim().split(" ")[0]}`,
41 | ```
42 |
43 | ### Name Uniqueness
44 |
45 | Codeblocks must have unique file paths for ESLint processing.
46 | [ESLint internally prepends a unique number to codeblock filenames](https://github.com/eslint/eslint/blob/5ff6c1dd09f32b56c05ab97f328741fc8ffb1f64/lib/services/processor-service.js#L83), in the format of \`${i}_${block.filename}\`
.
47 | For example, given three codeblocks with the same name, the names in order would become:
48 |
49 | - `example`: `0_example`, `1_example`, `2_example`
50 | - `example.js`: `0_example.js`, `1_example.js`, `2_example.js`
51 |
52 | Alternately, if multiple codeblocks require the same _file_ name, developers can give different _directory paths_:
53 |
54 | ````md
55 | ```json filename="example-1/package.json"
56 | {}
57 | ```
58 |
59 | ```json filename="example-2/package.json"
60 | {}
61 | ```
62 | ````
63 |
64 | ### Parsing Meta
65 |
66 | There is no unified standard in the ecosystem for parsing codeblock metadata in Markdown.
67 | The syntax has roughly converged on the syntax looking like \`\`\`lang key="value"
, and to a lesser extent, using `filename` or `title` as the prop name.
68 |
69 | - [Docusaurus's `codeBlockTitleRegex`](https://github.com/facebook/docusaurus/blob/7650829e913ec4bb1263d855719779f6b97066b6/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts#L12): only supports a [code title prop](https://mdxjs.com/guides/syntax-highlighting/#syntax-highlighting-with-the-meta-field) like \`\`\`jsx title="/src/components/HelloCodeTitle.js"
.
70 | - [Expressive Code's `title`](https://expressive-code.com/key-features/code-component/#title): used by [Astro](https://astro.build), with the syntax \`\`\`js title="my-test-file.js"
.
71 | - Gatsby plugins such as [`gatsby-remark-prismjs`](https://www.gatsbyjs.com/plugins/gatsby-remark-prismjs) rely on a syntax like \`\`\`javascript{numberLines: true}`
.
72 | - Separately, [`gatsby-remark-code-titles`](https://www.gatsbyjs.com/plugins/gatsby-remark-code-titles) allows a syntax like \`\`\`js:title=example-file.js
.
73 | - [`rehype-mdx-code-props`](https://github.com/remcohaszing/rehype-mdx-code-props): generally handles parsing of the raw `meta` text from the rehype AST.
74 | It specifies syntax like \`\`\`js copy filename="awesome.js"
, with a suggestion of `filename` for just the file's name.
75 | - [`remark-mdx-code-meta`](https://github.com/remcohaszing/remark-mdx-code-meta) is [referenced in the mdxjs.com `meta` docs](https://mdxjs.com/guides/syntax-highlighting/#syntax-highlighting-with-the-meta-field), but was deprecated with a notice to use `rehype-mdx-code-props` instead.
76 | It also specified syntax like \`\`\`js copy filename="awesome.js" onUsage={props.beAwesome} {...props}
.
77 | - [`remark-fenced-props`](https://github.com/shawnbot/remark-fenced-props): A proof-of-concept that augments Remark's codeblock parsing with arbitrary MDX props, written to support [mdx-js/mdx/issues/702](https://github.com/mdx-js/mdx/issues/702).
78 | It only specifies syntax like \`\`\`jsx live style={{border: '1px solid red'}}
.
79 |
80 | This RFC chooses `filename` over alternatives such as `title`.
81 | As noted in [docusaurus/discussions#10033 Choice of filename vs. title for codeblocks](https://github.com/facebook/docusaurus/discussions/10033), `filename` implies a source code file name, whereas `title` implies the visual display.
82 |
83 | ## Related Discussions
84 |
85 | See #226 for the original issue.
86 | This RFC is intended to contain a superset of all information discussed there.
87 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import copy from "rollup-plugin-copy";
2 | import del from "rollup-plugin-delete";
3 |
4 | export default {
5 | input: "src/index.js",
6 | output: [
7 | {
8 | file: "dist/esm/index.js",
9 | format: "esm",
10 | banner: '// @ts-self-types="./index.d.ts"',
11 | },
12 | ],
13 | plugins: [
14 | del({ targets: "dist/*" }),
15 | copy({
16 | targets: [{ src: "src/types.ts", dest: "dist/esm" }],
17 | }),
18 | ],
19 | };
20 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eslint/markdown/c68513942c0735fc7fbae7edbc95a36093a30ee0/screenshot.png
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Enables the processor for Markdown file extensions.
3 | * @author Brandon Mills
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Imports
8 | //-----------------------------------------------------------------------------
9 |
10 | import { processor } from "./processor.js";
11 | import { MarkdownLanguage } from "./language/markdown-language.js";
12 | import { MarkdownSourceCode } from "./language/markdown-source-code.js";
13 | import recommendedRules from "./build/recommended-config.js";
14 | import rules from "./build/rules.js";
15 |
16 | //-----------------------------------------------------------------------------
17 | // Type Definitions
18 | //-----------------------------------------------------------------------------
19 |
20 | /** @typedef {import("eslint").Linter.RulesRecord} RulesRecord*/
21 | /** @typedef {import("eslint").Linter.Config} Config*/
22 | /** @typedef {import("eslint").ESLint.Plugin} Plugin */
23 | /**
24 | * @typedef {import("./types.ts").MarkdownRuleDefinition} MarkdownRuleDefinition
25 | * @template {Partial} [Options={}]
26 | */
27 | /** @typedef {MarkdownRuleDefinition} RuleModule */
28 | /** @typedef {import("./types.ts").MarkdownRuleVisitor} MarkdownRuleVisitor */
29 | /** @typedef {import("@eslint/core").Language} Language */
30 |
31 | //-----------------------------------------------------------------------------
32 | // Exports
33 | //-----------------------------------------------------------------------------
34 |
35 | /** @type {RulesRecord} */
36 | const processorRulesConfig = {
37 | // The Markdown parser automatically trims trailing
38 | // newlines from code blocks.
39 | "eol-last": "off",
40 |
41 | // In code snippets and examples, these rules are often
42 | // counterproductive to clarity and brevity.
43 | "no-undef": "off",
44 | "no-unused-expressions": "off",
45 | "no-unused-vars": "off",
46 | "padded-blocks": "off",
47 |
48 | // Adding a "use strict" directive at the top of every
49 | // code block is tedious and distracting. The config
50 | // opts into strict mode parsing without the directive.
51 | strict: "off",
52 |
53 | // The processor will not receive a Unicode Byte Order
54 | // Mark from the Markdown parser.
55 | "unicode-bom": "off",
56 | };
57 |
58 | let recommendedPlugins, processorPlugins;
59 |
60 | const plugin = {
61 | meta: {
62 | name: "@eslint/markdown",
63 | version: "6.4.0", // x-release-please-version
64 | },
65 | processors: {
66 | markdown: processor,
67 | },
68 | languages: {
69 | commonmark: new MarkdownLanguage({ mode: "commonmark" }),
70 | gfm: new MarkdownLanguage({ mode: "gfm" }),
71 | },
72 | rules,
73 | configs: {
74 | "recommended-legacy": {
75 | plugins: ["markdown"],
76 | overrides: [
77 | {
78 | files: ["*.md"],
79 | processor: "markdown/markdown",
80 | },
81 | {
82 | files: ["**/*.md/**"],
83 | parserOptions: {
84 | ecmaFeatures: {
85 | // Adding a "use strict" directive at the top of
86 | // every code block is tedious and distracting, so
87 | // opt into strict mode parsing without the
88 | // directive.
89 | impliedStrict: true,
90 | },
91 | },
92 | rules: {
93 | ...processorRulesConfig,
94 | },
95 | },
96 | ],
97 | },
98 | recommended: [
99 | {
100 | name: "markdown/recommended",
101 | files: ["**/*.md"],
102 | language: "markdown/commonmark",
103 | plugins: (recommendedPlugins = {}),
104 | rules: recommendedRules,
105 | },
106 | ],
107 | processor: [
108 | {
109 | name: "markdown/recommended/plugin",
110 | plugins: (processorPlugins = {}),
111 | },
112 | {
113 | name: "markdown/recommended/processor",
114 | files: ["**/*.md"],
115 | processor: "markdown/markdown",
116 | },
117 | {
118 | name: "markdown/recommended/code-blocks",
119 | files: ["**/*.md/**"],
120 | languageOptions: {
121 | parserOptions: {
122 | ecmaFeatures: {
123 | // Adding a "use strict" directive at the top of
124 | // every code block is tedious and distracting, so
125 | // opt into strict mode parsing without the
126 | // directive.
127 | impliedStrict: true,
128 | },
129 | },
130 | },
131 | rules: {
132 | ...processorRulesConfig,
133 | },
134 | },
135 | ],
136 | },
137 | };
138 |
139 | // @ts-expect-error
140 | recommendedPlugins.markdown = processorPlugins.markdown = plugin;
141 |
142 | export default plugin;
143 | export { MarkdownSourceCode };
144 |
--------------------------------------------------------------------------------
/src/language/markdown-language.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Functions to fix up rules to provide missing methods on the `context` object.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | /* eslint class-methods-use-this: 0 -- Required to complete interface. */
7 |
8 | //------------------------------------------------------------------------------
9 | // Imports
10 | //------------------------------------------------------------------------------
11 |
12 | import { MarkdownSourceCode } from "./markdown-source-code.js";
13 | import { fromMarkdown } from "mdast-util-from-markdown";
14 | import { frontmatterFromMarkdown } from "mdast-util-frontmatter";
15 | import { gfmFromMarkdown } from "mdast-util-gfm";
16 | import { frontmatter } from "micromark-extension-frontmatter";
17 | import { gfm } from "micromark-extension-gfm";
18 |
19 | //-----------------------------------------------------------------------------
20 | // Types
21 | //-----------------------------------------------------------------------------
22 |
23 | /** @typedef {import("mdast").Root} RootNode */
24 | /** @typedef {import("mdast-util-from-markdown").Options['extensions']} Extensions */
25 | /** @typedef {import("mdast-util-from-markdown").Options['mdastExtensions']} MdastExtensions */
26 | /** @typedef {import("@eslint/core").Language} Language */
27 | /** @typedef {import("@eslint/core").File} File */
28 | /** @typedef {import("@eslint/core").ParseResult} ParseResult */
29 | /** @typedef {import("@eslint/core").OkParseResult} OkParseResult */
30 | /** @typedef {import("../types.ts").MarkdownLanguageOptions} MarkdownLanguageOptions */
31 | /** @typedef {import("../types.ts").MarkdownLanguageContext} MarkdownLanguageContext */
32 | /** @typedef {"commonmark"|"gfm"} ParserMode */
33 |
34 | //-----------------------------------------------------------------------------
35 | // Helpers
36 | //-----------------------------------------------------------------------------
37 |
38 | /**
39 | * Create parser options based on `mode` and `languageOptions`.
40 | * @param {ParserMode} mode The markdown parser mode.
41 | * @param {MarkdownLanguageOptions} languageOptions Language options.
42 | * @returns {{extensions: Extensions, mdastExtensions: MdastExtensions}} Parser options for micromark and mdast
43 | */
44 | function createParserOptions(mode, languageOptions) {
45 | /** @type {Extensions} */
46 | const extensions = [];
47 | /** @type {MdastExtensions} */
48 | const mdastExtensions = [];
49 |
50 | // 1. `mode`: Add GFM extensions if mode is "gfm"
51 | if (mode === "gfm") {
52 | extensions.push(gfm());
53 | mdastExtensions.push(gfmFromMarkdown());
54 | }
55 |
56 | // 2. `languageOptions.frontmatter`: Handle frontmatter options
57 | const frontmatterOption = languageOptions?.frontmatter;
58 |
59 | // Skip frontmatter entirely if false
60 | if (frontmatterOption !== false) {
61 | if (frontmatterOption === "yaml") {
62 | extensions.push(frontmatter(["yaml"]));
63 | mdastExtensions.push(frontmatterFromMarkdown(["yaml"]));
64 | } else if (frontmatterOption === "toml") {
65 | extensions.push(frontmatter(["toml"]));
66 | mdastExtensions.push(frontmatterFromMarkdown(["toml"]));
67 | }
68 | }
69 |
70 | return {
71 | extensions,
72 | mdastExtensions,
73 | };
74 | }
75 |
76 | //-----------------------------------------------------------------------------
77 | // Exports
78 | //-----------------------------------------------------------------------------
79 |
80 | /**
81 | * Markdown Language Object
82 | * @implements {Language}
83 | */
84 | export class MarkdownLanguage {
85 | /**
86 | * The type of file to read.
87 | * @type {"text"}
88 | */
89 | fileType = "text";
90 |
91 | /**
92 | * The line number at which the parser starts counting.
93 | * @type {0|1}
94 | */
95 | lineStart = 1;
96 |
97 | /**
98 | * The column number at which the parser starts counting.
99 | * @type {0|1}
100 | */
101 | columnStart = 1;
102 |
103 | /**
104 | * The name of the key that holds the type of the node.
105 | * @type {string}
106 | */
107 | nodeTypeKey = "type";
108 |
109 | /**
110 | * Default language options. User-defined options are merged with this object.
111 | * @type {MarkdownLanguageOptions}
112 | */
113 | defaultLanguageOptions = {
114 | frontmatter: false,
115 | };
116 |
117 | /**
118 | * The Markdown parser mode.
119 | * @type {ParserMode}
120 | */
121 | #mode = "commonmark";
122 |
123 | /**
124 | * Creates a new instance.
125 | * @param {Object} options The options to use for this instance.
126 | * @param {ParserMode} [options.mode] The Markdown parser mode to use.
127 | */
128 | constructor({ mode } = {}) {
129 | if (mode) {
130 | this.#mode = mode;
131 | }
132 | }
133 |
134 | /**
135 | * Validates the language options.
136 | * @param {MarkdownLanguageOptions} languageOptions The language options to validate.
137 | * @returns {void}
138 | * @throws {Error} When the language options are invalid.
139 | */
140 | validateLanguageOptions(languageOptions) {
141 | const frontmatterOption = languageOptions?.frontmatter;
142 | const validFrontmatterOptions = new Set([false, "yaml", "toml"]);
143 |
144 | if (
145 | frontmatterOption !== undefined &&
146 | !validFrontmatterOptions.has(frontmatterOption)
147 | ) {
148 | throw new Error(
149 | `Invalid language option value \`${frontmatterOption}\` for frontmatter.`,
150 | );
151 | }
152 | }
153 |
154 | /**
155 | * Parses the given file into an AST.
156 | * @param {File} file The virtual file to parse.
157 | * @param {MarkdownLanguageContext} context The options to use for parsing.
158 | * @returns {ParseResult} The result of parsing.
159 | */
160 | parse(file, context) {
161 | // Note: BOM already removed
162 | const text = /** @type {string} */ (file.body);
163 |
164 | /*
165 | * Check for parsing errors first. If there's a parsing error, nothing
166 | * else can happen. However, a parsing error does not throw an error
167 | * from this method - it's just considered a fatal error message, a
168 | * problem that ESLint identified just like any other.
169 | */
170 | try {
171 | const options = createParserOptions(
172 | this.#mode,
173 | context?.languageOptions,
174 | );
175 | const root = fromMarkdown(text, options);
176 |
177 | return {
178 | ok: true,
179 | ast: root,
180 | };
181 | } catch (ex) {
182 | return {
183 | ok: false,
184 | errors: [ex],
185 | };
186 | }
187 | }
188 |
189 | /**
190 | * Creates a new `MarkdownSourceCode` object from the given information.
191 | * @param {File} file The virtual file to create a `MarkdownSourceCode` object from.
192 | * @param {OkParseResult} parseResult The result returned from `parse()`.
193 | * @returns {MarkdownSourceCode} The new `MarkdownSourceCode` object.
194 | */
195 | createSourceCode(file, parseResult) {
196 | return new MarkdownSourceCode({
197 | text: /** @type {string} */ (file.body),
198 | ast: parseResult.ast,
199 | });
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/src/rules/fenced-code-language.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Rule to enforce languages for fenced code.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Type Definitions
8 | //-----------------------------------------------------------------------------
9 |
10 | /**
11 | * @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: [{ required?: string[]; }]; }>}
12 | * FencedCodeLanguageRuleDefinition
13 | */
14 |
15 | //-----------------------------------------------------------------------------
16 | // Helpers
17 | //-----------------------------------------------------------------------------
18 |
19 | const fencedCodeCharacters = new Set(["`", "~"]);
20 |
21 | //-----------------------------------------------------------------------------
22 | // Rule Definition
23 | //-----------------------------------------------------------------------------
24 |
25 | /** @type {FencedCodeLanguageRuleDefinition} */
26 | export default {
27 | meta: {
28 | type: "problem",
29 |
30 | docs: {
31 | recommended: true,
32 | description: "Require languages for fenced code blocks",
33 | url: "https://github.com/eslint/markdown/blob/main/docs/rules/fenced-code-language.md",
34 | },
35 |
36 | messages: {
37 | missingLanguage: "Missing code block language.",
38 | disallowedLanguage:
39 | 'Code block language "{{lang}}" is not allowed.',
40 | },
41 |
42 | schema: [
43 | {
44 | type: "object",
45 | properties: {
46 | required: {
47 | type: "array",
48 | items: {
49 | type: "string",
50 | },
51 | uniqueItems: true,
52 | },
53 | },
54 | additionalProperties: false,
55 | },
56 | ],
57 | },
58 |
59 | create(context) {
60 | const required = new Set(context.options[0]?.required);
61 | const { sourceCode } = context;
62 |
63 | return {
64 | code(node) {
65 | if (!node.lang) {
66 | // only check fenced code blocks
67 | if (
68 | !fencedCodeCharacters.has(
69 | sourceCode.text[node.position.start.offset],
70 | )
71 | ) {
72 | return;
73 | }
74 |
75 | context.report({
76 | loc: node.position,
77 | messageId: "missingLanguage",
78 | });
79 |
80 | return;
81 | }
82 |
83 | if (required.size && !required.has(node.lang)) {
84 | context.report({
85 | loc: node.position,
86 | messageId: "disallowedLanguage",
87 | data: {
88 | lang: node.lang,
89 | },
90 | });
91 | }
92 | },
93 | };
94 | },
95 | };
96 |
--------------------------------------------------------------------------------
/src/rules/heading-increment.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Rule to enforce heading levels increment by one.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Type Definitions
8 | //-----------------------------------------------------------------------------
9 |
10 | /**
11 | * @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: []; }>}
12 | * HeadingIncrementRuleDefinition
13 | */
14 |
15 | //-----------------------------------------------------------------------------
16 | // Rule Definition
17 | //-----------------------------------------------------------------------------
18 |
19 | /** @type {HeadingIncrementRuleDefinition} */
20 | export default {
21 | meta: {
22 | type: "problem",
23 |
24 | docs: {
25 | recommended: true,
26 | description: "Enforce heading levels increment by one",
27 | url: "https://github.com/eslint/markdown/blob/main/docs/rules/heading-increment.md",
28 | },
29 |
30 | messages: {
31 | skippedHeading:
32 | "Heading level skipped from {{fromLevel}} to {{toLevel}}.",
33 | },
34 | },
35 |
36 | create(context) {
37 | let lastHeadingDepth = 0;
38 |
39 | return {
40 | heading(node) {
41 | if (lastHeadingDepth > 0 && node.depth > lastHeadingDepth + 1) {
42 | context.report({
43 | loc: node.position,
44 | messageId: "skippedHeading",
45 | data: {
46 | fromLevel: lastHeadingDepth.toString(),
47 | toLevel: node.depth.toString(),
48 | },
49 | });
50 | }
51 |
52 | lastHeadingDepth = node.depth;
53 | },
54 | };
55 | },
56 | };
57 |
--------------------------------------------------------------------------------
/src/rules/no-duplicate-definitions.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Rule to prevent duplicate definitions in Markdown.
3 | * @author 루밀LuMir(lumirlumir)
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Type Definitions
8 | //-----------------------------------------------------------------------------
9 |
10 | /**
11 | * @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: [{ allowDefinitions: string[], allowFootnoteDefinitions: string[]; }]; }>}
12 | * NoDuplicateDefinitionsRuleDefinition
13 | */
14 |
15 | //-----------------------------------------------------------------------------
16 | // Rule Definition
17 | //-----------------------------------------------------------------------------
18 |
19 | /** @type {NoDuplicateDefinitionsRuleDefinition} */
20 | export default {
21 | meta: {
22 | type: "problem",
23 |
24 | docs: {
25 | recommended: true,
26 | description: "Disallow duplicate definitions",
27 | url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-duplicate-definitions.md",
28 | },
29 |
30 | messages: {
31 | duplicateDefinition: "Unexpected duplicate definition found.",
32 | duplicateFootnoteDefinition:
33 | "Unexpected duplicate footnote definition found.",
34 | },
35 |
36 | schema: [
37 | {
38 | type: "object",
39 | properties: {
40 | allowDefinitions: {
41 | type: "array",
42 | items: {
43 | type: "string",
44 | },
45 | uniqueItems: true,
46 | },
47 | allowFootnoteDefinitions: {
48 | type: "array",
49 | items: {
50 | type: "string",
51 | },
52 | uniqueItems: true,
53 | },
54 | },
55 | additionalProperties: false,
56 | },
57 | ],
58 |
59 | defaultOptions: [
60 | {
61 | allowDefinitions: ["//"],
62 | allowFootnoteDefinitions: [],
63 | },
64 | ],
65 | },
66 |
67 | create(context) {
68 | const allowDefinitions = new Set(context.options[0]?.allowDefinitions);
69 | const allowFootnoteDefinitions = new Set(
70 | context.options[0]?.allowFootnoteDefinitions,
71 | );
72 |
73 | const definitions = new Set();
74 | const footnoteDefinitions = new Set();
75 |
76 | return {
77 | definition(node) {
78 | if (allowDefinitions.has(node.identifier)) {
79 | return;
80 | }
81 |
82 | if (definitions.has(node.identifier)) {
83 | context.report({
84 | node,
85 | messageId: "duplicateDefinition",
86 | });
87 | } else {
88 | definitions.add(node.identifier);
89 | }
90 | },
91 |
92 | footnoteDefinition(node) {
93 | if (allowFootnoteDefinitions.has(node.identifier)) {
94 | return;
95 | }
96 |
97 | if (footnoteDefinitions.has(node.identifier)) {
98 | context.report({
99 | node,
100 | messageId: "duplicateFootnoteDefinition",
101 | });
102 | } else {
103 | footnoteDefinitions.add(node.identifier);
104 | }
105 | },
106 | };
107 | },
108 | };
109 |
--------------------------------------------------------------------------------
/src/rules/no-duplicate-headings.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Rule to prevent duplicate headings in Markdown.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Type Definitions
8 | //-----------------------------------------------------------------------------
9 |
10 | /**
11 | * @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: []; }>}
12 | * NoDuplicateHeadingsRuleDefinition
13 | */
14 |
15 | //-----------------------------------------------------------------------------
16 | // Rule Definition
17 | //-----------------------------------------------------------------------------
18 |
19 | /** @type {NoDuplicateHeadingsRuleDefinition} */
20 | export default {
21 | meta: {
22 | type: "problem",
23 |
24 | docs: {
25 | description: "Disallow duplicate headings in the same document",
26 | url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-duplicate-headings.md",
27 | },
28 |
29 | messages: {
30 | duplicateHeading: 'Duplicate heading "{{text}}" found.',
31 | },
32 | },
33 |
34 | create(context) {
35 | const headings = new Set();
36 | const { sourceCode } = context;
37 |
38 | return {
39 | heading(node) {
40 | /*
41 | * There are two types of headings in markdown:
42 | * - ATX headings, which start with one or more # characters
43 | * - Setext headings, which are underlined with = or -
44 | * Setext headings are identified by being on two lines instead of one,
45 | * with the second line containing only = or - characters. In order to
46 | * get the correct heading text, we need to determine which type of
47 | * heading we're dealing with.
48 | */
49 | const isSetext =
50 | node.position.start.line !== node.position.end.line;
51 |
52 | const text = isSetext
53 | ? // get only the text from the first line
54 | sourceCode.lines[node.position.start.line - 1].trim()
55 | : // get the text without the leading # characters
56 | sourceCode
57 | .getText(node)
58 | .slice(node.depth + 1)
59 | .trim();
60 |
61 | if (headings.has(text)) {
62 | context.report({
63 | loc: node.position,
64 | messageId: "duplicateHeading",
65 | data: {
66 | text,
67 | },
68 | });
69 | }
70 |
71 | headings.add(text);
72 | },
73 | };
74 | },
75 | };
76 |
--------------------------------------------------------------------------------
/src/rules/no-empty-definitions.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Rule to prevent empty definitions in Markdown.
3 | * @author Pixel998
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Type Definitions
8 | //-----------------------------------------------------------------------------
9 |
10 | /**
11 | * @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: []; }>}
12 | * NoEmptyDefinitionsRuleDefinition
13 | */
14 |
15 | //-----------------------------------------------------------------------------
16 | // Rule Definition
17 | //-----------------------------------------------------------------------------
18 |
19 | /** @type {NoEmptyDefinitionsRuleDefinition} */
20 | export default {
21 | meta: {
22 | type: "problem",
23 |
24 | docs: {
25 | recommended: true,
26 | description: "Disallow empty definitions",
27 | url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-empty-definitions.md",
28 | },
29 |
30 | messages: {
31 | emptyDefinition: "Unexpected empty definition found.",
32 | },
33 | },
34 |
35 | create(context) {
36 | return {
37 | definition(node) {
38 | if (!node.url || node.url === "#") {
39 | context.report({
40 | loc: node.position,
41 | messageId: "emptyDefinition",
42 | });
43 | }
44 | },
45 | };
46 | },
47 | };
48 |
--------------------------------------------------------------------------------
/src/rules/no-empty-images.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Rule to prevent empty images in Markdown.
3 | * @author 루밀LuMir(lumirlumir)
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Type Definitions
8 | //-----------------------------------------------------------------------------
9 |
10 | /**
11 | * @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: []; }>}
12 | * NoEmptyImagesRuleDefinition
13 | */
14 |
15 | //-----------------------------------------------------------------------------
16 | // Rule Definition
17 | //-----------------------------------------------------------------------------
18 |
19 | /** @type {NoEmptyImagesRuleDefinition} */
20 | export default {
21 | meta: {
22 | type: "problem",
23 |
24 | docs: {
25 | recommended: true,
26 | description: "Disallow empty images",
27 | url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-empty-images.md",
28 | },
29 |
30 | messages: {
31 | emptyImage: "Unexpected empty image found.",
32 | },
33 | },
34 |
35 | create(context) {
36 | return {
37 | image(node) {
38 | if (!node.url || node.url === "#") {
39 | context.report({
40 | loc: node.position,
41 | messageId: "emptyImage",
42 | });
43 | }
44 | },
45 | };
46 | },
47 | };
48 |
--------------------------------------------------------------------------------
/src/rules/no-empty-links.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Rule to prevent empty links in Markdown.
3 | * @author Nicholas C. Zakas
4 | */
5 | //-----------------------------------------------------------------------------
6 | // Type Definitions
7 | //-----------------------------------------------------------------------------
8 |
9 | /**
10 | * @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: []; }>}
11 | * NoEmptyLinksRuleDefinition
12 | */
13 |
14 | //-----------------------------------------------------------------------------
15 | // Rule Definition
16 | //-----------------------------------------------------------------------------
17 |
18 | /** @type {NoEmptyLinksRuleDefinition} */
19 | export default {
20 | meta: {
21 | type: "problem",
22 |
23 | docs: {
24 | recommended: true,
25 | description: "Disallow empty links",
26 | url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-empty-links.md",
27 | },
28 |
29 | messages: {
30 | emptyLink: "Unexpected empty link found.",
31 | },
32 | },
33 |
34 | create(context) {
35 | return {
36 | link(node) {
37 | if (!node.url || node.url === "#") {
38 | context.report({
39 | loc: node.position,
40 | messageId: "emptyLink",
41 | });
42 | }
43 | },
44 | };
45 | },
46 | };
47 |
--------------------------------------------------------------------------------
/src/rules/no-html.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Rule to disallow HTML inside of content.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Imports
8 | //-----------------------------------------------------------------------------
9 |
10 | import { findOffsets } from "../util.js";
11 |
12 | //-----------------------------------------------------------------------------
13 | // Type Definitions
14 | //-----------------------------------------------------------------------------
15 |
16 | /**
17 | * @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: [{ allowed?: string[]; }]; }>}
18 | * NoHtmlRuleDefinition
19 | */
20 |
21 | //-----------------------------------------------------------------------------
22 | // Helpers
23 | //-----------------------------------------------------------------------------
24 |
25 | const htmlTagPattern = /<([a-z0-9]+(?:-[a-z0-9]+)*)/giu;
26 |
27 | //-----------------------------------------------------------------------------
28 | // Rule Definition
29 | //-----------------------------------------------------------------------------
30 |
31 | /** @type {NoHtmlRuleDefinition} */
32 | export default {
33 | meta: {
34 | type: "problem",
35 |
36 | docs: {
37 | description: "Disallow HTML tags",
38 | url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-html.md",
39 | },
40 |
41 | messages: {
42 | disallowedElement: 'HTML element "{{name}}" is not allowed.',
43 | },
44 |
45 | schema: [
46 | {
47 | type: "object",
48 | properties: {
49 | allowed: {
50 | type: "array",
51 | items: {
52 | type: "string",
53 | },
54 | uniqueItems: true,
55 | },
56 | },
57 | additionalProperties: false,
58 | },
59 | ],
60 | },
61 |
62 | create(context) {
63 | const allowed = new Set(context.options[0]?.allowed);
64 |
65 | return {
66 | html(node) {
67 | let match;
68 |
69 | while ((match = htmlTagPattern.exec(node.value)) !== null) {
70 | const tagName = match[1];
71 | const { lineOffset, columnOffset } = findOffsets(
72 | node.value,
73 | match.index,
74 | );
75 | const start = {
76 | line: node.position.start.line + lineOffset,
77 | column: node.position.start.column + columnOffset,
78 | };
79 | const end = {
80 | line: start.line,
81 | column: start.column + match[0].length + 1,
82 | };
83 |
84 | if (allowed.size === 0 || !allowed.has(tagName)) {
85 | context.report({
86 | loc: { start, end },
87 | messageId: "disallowedElement",
88 | data: {
89 | name: tagName,
90 | },
91 | });
92 | }
93 | }
94 | },
95 | };
96 | },
97 | };
98 |
--------------------------------------------------------------------------------
/src/rules/no-invalid-label-refs.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Rule to prevent non-complaint link references.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Imports
8 | //-----------------------------------------------------------------------------
9 |
10 | import { findOffsets, illegalShorthandTailPattern } from "../util.js";
11 |
12 | //-----------------------------------------------------------------------------
13 | // Type Definitions
14 | //-----------------------------------------------------------------------------
15 |
16 | /** @typedef {import("unist").Position} Position */
17 | /** @typedef {import("mdast").Text} TextNode */
18 | /** @typedef {Parameters[0]['sourceCode']} sourceCode */
19 | /**
20 | * @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: []; }>}
21 | * NoInvalidLabelRuleDefinition
22 | */
23 |
24 | //-----------------------------------------------------------------------------
25 | // Helpers
26 | //-----------------------------------------------------------------------------
27 |
28 | // matches i.e., [foo][bar]
29 | const labelPattern = /\]\[([^\]]+)\]/u;
30 |
31 | /**
32 | * Finds missing references in a node.
33 | * @param {TextNode} node The node to check.
34 | * @param {sourceCode} sourceCode The Markdown source code object.
35 | * @returns {Array<{label:string,position:Position}>} The missing references.
36 | */
37 | function findInvalidLabelReferences(node, sourceCode) {
38 | const nodeText = sourceCode.getText(node);
39 | const docText = sourceCode.text;
40 | const invalid = [];
41 | let startIndex = 0;
42 | const offset = node.position.start.offset;
43 | const nodeStartLine = node.position.start.line;
44 | const nodeStartColumn = node.position.start.column;
45 |
46 | /*
47 | * This loop works by searching the string inside the node for the next
48 | * label reference. If it finds one, it checks to see if there is any
49 | * white space between the [ and ]. If there is, it reports an error.
50 | * It then moves the start index to the end of the label reference and
51 | * continues searching the text until the end of the text is found.
52 | */
53 | while (startIndex < nodeText.length) {
54 | const value = nodeText.slice(startIndex);
55 | const match = value.match(labelPattern);
56 |
57 | if (!match) {
58 | break;
59 | }
60 |
61 | if (!illegalShorthandTailPattern.test(match[0])) {
62 | startIndex += match.index + match[0].length;
63 | continue;
64 | }
65 |
66 | /*
67 | * Calculate the match index relative to just the node and
68 | * to the entire document text.
69 | */
70 | const nodeMatchIndex = startIndex + match.index;
71 | const docMatchIndex = offset + nodeMatchIndex;
72 |
73 | /*
74 | * Search the entire document text to find the preceding open bracket.
75 | */
76 | const lastOpenBracketIndex = docText.lastIndexOf("[", docMatchIndex);
77 |
78 | if (lastOpenBracketIndex === -1) {
79 | startIndex += match.index + match[0].length;
80 | continue;
81 | }
82 |
83 | /*
84 | * Note: `label` can contain leading and trailing newlines, so we need to
85 | * take that into account when calculating the line and column offsets.
86 | */
87 | const label = docText
88 | .slice(lastOpenBracketIndex, docMatchIndex + match[0].length)
89 | .match(/!?\[([^\]]+)\]/u)[1];
90 |
91 | // find location of [ in the document text
92 | const { lineOffset: startLineOffset, columnOffset: startColumnOffset } =
93 | findOffsets(nodeText, nodeMatchIndex + 1);
94 |
95 | // find location of [ in the document text
96 | const { lineOffset: endLineOffset, columnOffset: endColumnOffset } =
97 | findOffsets(nodeText, nodeMatchIndex + match[0].length);
98 |
99 | const startLine = nodeStartLine + startLineOffset;
100 | const startColumn = nodeStartColumn + startColumnOffset;
101 | const endLine = nodeStartLine + endLineOffset;
102 | const endColumn =
103 | (endLine === startLine ? nodeStartColumn : 0) + endColumnOffset;
104 |
105 | invalid.push({
106 | label: label.trim(),
107 | position: {
108 | start: {
109 | line: startLine,
110 | column: startColumn,
111 | },
112 | end: {
113 | line: endLine,
114 | column: endColumn,
115 | },
116 | },
117 | });
118 |
119 | startIndex += match.index + match[0].length;
120 | }
121 |
122 | return invalid;
123 | }
124 |
125 | //-----------------------------------------------------------------------------
126 | // Rule Definition
127 | //-----------------------------------------------------------------------------
128 |
129 | /** @type {NoInvalidLabelRuleDefinition} */
130 | export default {
131 | meta: {
132 | type: "problem",
133 |
134 | docs: {
135 | recommended: true,
136 | description: "Disallow invalid label references",
137 | url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-invalid-label-refs.md",
138 | },
139 |
140 | messages: {
141 | invalidLabelRef:
142 | "Label reference '{{label}}' is invalid due to white space between [ and ].",
143 | },
144 | },
145 |
146 | create(context) {
147 | const { sourceCode } = context;
148 |
149 | return {
150 | text(node) {
151 | const invalidReferences = findInvalidLabelReferences(
152 | node,
153 | sourceCode,
154 | );
155 |
156 | for (const invalidReference of invalidReferences) {
157 | context.report({
158 | loc: invalidReference.position,
159 | messageId: "invalidLabelRef",
160 | data: {
161 | label: invalidReference.label,
162 | },
163 | });
164 | }
165 | },
166 | };
167 | },
168 | };
169 |
--------------------------------------------------------------------------------
/src/rules/no-missing-atx-heading-space.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Rule to ensure there is a space after hash on ATX style headings in Markdown.
3 | * @author Sweta Tanwar (@SwetaTanwar)
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Type Definitions
8 | //-----------------------------------------------------------------------------
9 |
10 | /**
11 | * @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: []; }>}
12 | * NoMissingAtxHeadingSpaceRuleDefinition
13 | */
14 |
15 | //-----------------------------------------------------------------------------
16 | // Helpers
17 | //-----------------------------------------------------------------------------
18 |
19 | const headingPattern = /^(#{1,6})(?:[^#\s]|$)/u;
20 | const newLinePattern = /\r?\n/u;
21 |
22 | //-----------------------------------------------------------------------------
23 | // Rule Definition
24 | //-----------------------------------------------------------------------------
25 |
26 | /** @type {NoMissingAtxHeadingSpaceRuleDefinition} */
27 | export default {
28 | meta: {
29 | type: "problem",
30 |
31 | docs: {
32 | recommended: true,
33 | description:
34 | "Disallow headings without a space after the hash characters",
35 | url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-missing-atx-heading-space.md",
36 | },
37 |
38 | fixable: "whitespace",
39 |
40 | messages: {
41 | missingSpace: "Missing space after hash(es) on ATX style heading.",
42 | },
43 | },
44 |
45 | create(context) {
46 | return {
47 | paragraph(node) {
48 | if (node.children && node.children.length > 0) {
49 | const firstTextChild = node.children.find(
50 | child => child.type === "text",
51 | );
52 | if (!firstTextChild) {
53 | return;
54 | }
55 |
56 | const text = context.sourceCode.getText(firstTextChild);
57 | const lines = text.split(newLinePattern);
58 |
59 | lines.forEach((line, idx) => {
60 | const lineNum =
61 | firstTextChild.position.start.line + idx;
62 |
63 | const match = headingPattern.exec(line);
64 | if (!match) {
65 | return;
66 | }
67 |
68 | const hashes = match[1];
69 |
70 | const startColumn =
71 | firstTextChild.position.start.column;
72 |
73 | context.report({
74 | loc: {
75 | start: { line: lineNum, column: startColumn },
76 | end: {
77 | line: lineNum,
78 | column: startColumn + hashes.length + 1,
79 | },
80 | },
81 | messageId: "missingSpace",
82 | fix(fixer) {
83 | const offset =
84 | firstTextChild.position.start.offset +
85 | lines.slice(0, idx).join("\n").length +
86 | (idx > 0 ? 1 : 0);
87 |
88 | return fixer.insertTextAfterRange(
89 | [
90 | offset + hashes.length - 1,
91 | offset + hashes.length,
92 | ],
93 | " ",
94 | );
95 | },
96 | });
97 | });
98 | }
99 | },
100 | };
101 | },
102 | };
103 |
--------------------------------------------------------------------------------
/src/rules/no-missing-label-refs.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Rule to prevent missing label references in Markdown.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Imports
8 | //-----------------------------------------------------------------------------
9 |
10 | import { findOffsets, illegalShorthandTailPattern } from "../util.js";
11 |
12 | //-----------------------------------------------------------------------------
13 | // Type Definitions
14 | //-----------------------------------------------------------------------------
15 |
16 | /** @typedef {import("unist").Position} Position */
17 | /** @typedef {import("mdast").Text} TextNode */
18 | /**
19 | * @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: []; }>}
20 | * NoMissingLabelRuleDefinition
21 | */
22 |
23 | //-----------------------------------------------------------------------------
24 | // Helpers
25 | //-----------------------------------------------------------------------------
26 |
27 | /**
28 | * Finds missing references in a node.
29 | * @param {TextNode} node The node to check.
30 | * @param {string} nodeText The text of the node.
31 | * @returns {Array<{label:string,position:Position}>} The missing references.
32 | */
33 | function findMissingReferences(node, nodeText) {
34 | const missing = [];
35 | const nodeStartLine = node.position.start.line;
36 | const nodeStartColumn = node.position.start.column;
37 |
38 | /*
39 | * Matches substrings like "[foo]", "[]", "[foo][bar]", "[foo][]", "[][bar]", or "[][]".
40 | * `left` is the content between the first brackets. It can be empty.
41 | * `right` is the content between the second brackets. It can be empty, and it can be undefined.
42 | */
43 | const labelPattern =
44 | /(?(?:\\.|[^\]])*)(?(?:\\.|[^\]])*)(? 0
87 | ? startColumnOffset + 1
88 | : nodeStartColumn + startColumnOffset,
89 | },
90 | end: {
91 | line: nodeStartLine + endLineOffset,
92 | column:
93 | endLineOffset > 0
94 | ? endColumnOffset + 1
95 | : nodeStartColumn + endColumnOffset,
96 | },
97 | },
98 | });
99 | }
100 |
101 | return missing;
102 | }
103 |
104 | //-----------------------------------------------------------------------------
105 | // Rule Definition
106 | //-----------------------------------------------------------------------------
107 |
108 | /** @type {NoMissingLabelRuleDefinition} */
109 | export default {
110 | meta: {
111 | type: "problem",
112 |
113 | docs: {
114 | recommended: true,
115 | description: "Disallow missing label references",
116 | url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-missing-label-refs.md",
117 | },
118 |
119 | messages: {
120 | notFound: "Label reference '{{label}}' not found.",
121 | },
122 | },
123 |
124 | create(context) {
125 | const { sourceCode } = context;
126 | let allMissingReferences = [];
127 |
128 | return {
129 | "root:exit"() {
130 | for (const missingReference of allMissingReferences) {
131 | context.report({
132 | loc: missingReference.position,
133 | messageId: "notFound",
134 | data: {
135 | label: missingReference.label,
136 | },
137 | });
138 | }
139 | },
140 |
141 | text(node) {
142 | allMissingReferences.push(
143 | ...findMissingReferences(node, sourceCode.getText(node)),
144 | );
145 | },
146 |
147 | definition(node) {
148 | /*
149 | * Sometimes a poorly-formatted link will end up a text node instead of a link node
150 | * even though the label definition exists. Here, we remove any missing references
151 | * that have a matching label definition.
152 | */
153 | allMissingReferences = allMissingReferences.filter(
154 | missingReference =>
155 | missingReference.label !== node.identifier,
156 | );
157 | },
158 | };
159 | },
160 | };
161 |
--------------------------------------------------------------------------------
/src/rules/no-multiple-h1.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Rule to enforce at most one H1 heading in Markdown.
3 | * @author Pixel998
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Imports
8 | //-----------------------------------------------------------------------------
9 |
10 | import { findOffsets } from "../util.js";
11 |
12 | //-----------------------------------------------------------------------------
13 | // Type Definitions
14 | //-----------------------------------------------------------------------------
15 |
16 | /**
17 | * @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: [{ frontmatterTitle?: string; }]; }>}
18 | * NoMultipleH1RuleDefinition
19 | */
20 |
21 | //-----------------------------------------------------------------------------
22 | // Helpers
23 | //-----------------------------------------------------------------------------
24 |
25 | const h1TagPattern = /(?]*>[\s\S]*?<\/h1>/giu;
26 |
27 | /**
28 | * Checks if a frontmatter block contains a title matching the given pattern
29 | * @param {string} value The frontmatter content
30 | * @param {RegExp|null} pattern The pattern to match against
31 | * @returns {boolean} Whether a title was found
32 | */
33 | function frontmatterHasTitle(value, pattern) {
34 | if (!pattern) {
35 | return false;
36 | }
37 | const lines = value.split("\n");
38 | for (const line of lines) {
39 | if (pattern.test(line)) {
40 | return true;
41 | }
42 | }
43 | return false;
44 | }
45 |
46 | //-----------------------------------------------------------------------------
47 | // Rule Definition
48 | //-----------------------------------------------------------------------------
49 |
50 | /** @type {NoMultipleH1RuleDefinition} */
51 | export default {
52 | meta: {
53 | type: "problem",
54 |
55 | docs: {
56 | recommended: true,
57 | description: "Disallow multiple H1 headings in the same document",
58 | url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-multiple-h1.md",
59 | },
60 |
61 | messages: {
62 | multipleH1: "Unexpected additional H1 heading found.",
63 | },
64 |
65 | schema: [
66 | {
67 | type: "object",
68 | properties: {
69 | frontmatterTitle: {
70 | type: "string",
71 | },
72 | },
73 | additionalProperties: false,
74 | },
75 | ],
76 |
77 | defaultOptions: [
78 | { frontmatterTitle: "^\\s*['\"]?title['\"]?\\s*[:=]" },
79 | ],
80 | },
81 |
82 | create(context) {
83 | const [{ frontmatterTitle }] = context.options;
84 | const titlePattern =
85 | frontmatterTitle === "" ? null : new RegExp(frontmatterTitle, "iu");
86 | let h1Count = 0;
87 |
88 | return {
89 | yaml(node) {
90 | if (frontmatterHasTitle(node.value, titlePattern)) {
91 | h1Count++;
92 | }
93 | },
94 |
95 | toml(node) {
96 | if (frontmatterHasTitle(node.value, titlePattern)) {
97 | h1Count++;
98 | }
99 | },
100 |
101 | html(node) {
102 | let match;
103 | while ((match = h1TagPattern.exec(node.value)) !== null) {
104 | h1Count++;
105 | if (h1Count > 1) {
106 | const {
107 | lineOffset: startLineOffset,
108 | columnOffset: startColumnOffset,
109 | } = findOffsets(node.value, match.index);
110 |
111 | const {
112 | lineOffset: endLineOffset,
113 | columnOffset: endColumnOffset,
114 | } = findOffsets(
115 | node.value,
116 | match.index + match[0].length,
117 | );
118 |
119 | const nodeStartLine = node.position.start.line;
120 | const nodeStartColumn = node.position.start.column;
121 | const startLine = nodeStartLine + startLineOffset;
122 | const endLine = nodeStartLine + endLineOffset;
123 | const startColumn =
124 | (startLine === nodeStartLine
125 | ? nodeStartColumn
126 | : 1) + startColumnOffset;
127 | const endColumn =
128 | (endLine === nodeStartLine ? nodeStartColumn : 1) +
129 | endColumnOffset;
130 |
131 | context.report({
132 | loc: {
133 | start: {
134 | line: startLine,
135 | column: startColumn,
136 | },
137 | end: {
138 | line: endLine,
139 | column: endColumn,
140 | },
141 | },
142 | messageId: "multipleH1",
143 | });
144 | }
145 | }
146 | },
147 |
148 | heading(node) {
149 | if (node.depth === 1) {
150 | h1Count++;
151 | if (h1Count > 1) {
152 | context.report({
153 | loc: node.position,
154 | messageId: "multipleH1",
155 | });
156 | }
157 | }
158 | },
159 | };
160 | },
161 | };
162 |
--------------------------------------------------------------------------------
/src/rules/require-alt-text.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Rule to require alternative text for images in Markdown.
3 | * @author Pixel998
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Imports
8 | //-----------------------------------------------------------------------------
9 |
10 | import { findOffsets } from "../util.js";
11 |
12 | //-----------------------------------------------------------------------------
13 | // Type Definitions
14 | //-----------------------------------------------------------------------------
15 |
16 | /**
17 | * @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: []; }>}
18 | * RequireAltTextRuleDefinition
19 | */
20 |
21 | //-----------------------------------------------------------------------------
22 | // Helpers
23 | //-----------------------------------------------------------------------------
24 |
25 | const imgTagPattern = /(?]*>/giu;
26 |
27 | /**
28 | * Creates a regex to match HTML attributes
29 | * @param {string} name The attribute name to match
30 | * @returns {RegExp} Regular expression for matching the attribute
31 | */
32 | function getHtmlAttributeRe(name) {
33 | return new RegExp(`\\s${name}(?:\\s*=\\s*['"]([^'"]*)['"])?`, "iu");
34 | }
35 |
36 | //-----------------------------------------------------------------------------
37 | // Rule Definition
38 | //-----------------------------------------------------------------------------
39 |
40 | /** @type {RequireAltTextRuleDefinition} */
41 | export default {
42 | meta: {
43 | type: "problem",
44 |
45 | docs: {
46 | recommended: true,
47 | description: "Require alternative text for images",
48 | url: "https://github.com/eslint/markdown/blob/main/docs/rules/require-alt-text.md",
49 | },
50 |
51 | messages: {
52 | altTextRequired: "Alternative text for image is required.",
53 | },
54 | },
55 |
56 | create(context) {
57 | return {
58 | image(node) {
59 | if (node.alt.trim().length === 0) {
60 | context.report({
61 | loc: node.position,
62 | messageId: "altTextRequired",
63 | });
64 | }
65 | },
66 |
67 | imageReference(node) {
68 | if (node.alt.trim().length === 0) {
69 | context.report({
70 | loc: node.position,
71 | messageId: "altTextRequired",
72 | });
73 | }
74 | },
75 |
76 | html(node) {
77 | let match;
78 |
79 | while ((match = imgTagPattern.exec(node.value)) !== null) {
80 | const imgTag = match[0];
81 | const ariaHiddenMatch = imgTag.match(
82 | getHtmlAttributeRe("aria-hidden"),
83 | );
84 | if (
85 | ariaHiddenMatch &&
86 | ariaHiddenMatch[1].toLowerCase() === "true"
87 | ) {
88 | continue;
89 | }
90 |
91 | const altMatch = imgTag.match(getHtmlAttributeRe("alt"));
92 | if (
93 | !altMatch ||
94 | (altMatch[1] &&
95 | altMatch[1].trim().length === 0 &&
96 | altMatch[1].length > 0)
97 | ) {
98 | const {
99 | lineOffset: startLineOffset,
100 | columnOffset: startColumnOffset,
101 | } = findOffsets(node.value, match.index);
102 |
103 | const {
104 | lineOffset: endLineOffset,
105 | columnOffset: endColumnOffset,
106 | } = findOffsets(
107 | node.value,
108 | match.index + imgTag.length,
109 | );
110 |
111 | const nodeStartLine = node.position.start.line;
112 | const nodeStartColumn = node.position.start.column;
113 | const startLine = nodeStartLine + startLineOffset;
114 | const endLine = nodeStartLine + endLineOffset;
115 | const startColumn =
116 | (startLine === nodeStartLine
117 | ? nodeStartColumn
118 | : 1) + startColumnOffset;
119 | const endColumn =
120 | (endLine === nodeStartLine ? nodeStartColumn : 1) +
121 | endColumnOffset;
122 |
123 | context.report({
124 | loc: {
125 | start: {
126 | line: startLine,
127 | column: startColumn,
128 | },
129 | end: {
130 | line: endLine,
131 | column: endColumn,
132 | },
133 | },
134 | messageId: "altTextRequired",
135 | });
136 | }
137 | }
138 | },
139 | };
140 | },
141 | };
142 |
--------------------------------------------------------------------------------
/src/rules/table-column-count.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Rule to disallow data rows in a GitHub Flavored Markdown table from having more cells than the header row
3 | * @author Sweta Tanwar (@SwetaTanwar)
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Type Definitions
8 | //-----------------------------------------------------------------------------
9 |
10 | /**
11 | * @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: []; }>} TableColumnCountRuleDefinition
12 | */
13 |
14 | //-----------------------------------------------------------------------------
15 | // Rule Definition
16 | //-----------------------------------------------------------------------------
17 |
18 | /** @type {TableColumnCountRuleDefinition} */
19 | export default {
20 | meta: {
21 | type: "problem",
22 |
23 | docs: {
24 | recommended: true,
25 | description:
26 | "Disallow data rows in a GitHub Flavored Markdown table from having more cells than the header row",
27 | url: "https://github.com/eslint/markdown/blob/main/docs/rules/table-column-count.md",
28 | },
29 |
30 | messages: {
31 | inconsistentColumnCount:
32 | "Table column count mismatch (Expected: {{expectedCells}}, Actual: {{actualCells}}), extra data starting here will be ignored.",
33 | },
34 | },
35 |
36 | create(context) {
37 | return {
38 | table(node) {
39 | if (node.children.length < 1) {
40 | return;
41 | }
42 |
43 | const headerRow = node.children[0];
44 | const expectedCellsLength = headerRow.children.length;
45 |
46 | for (let i = 1; i < node.children.length; i++) {
47 | const currentRow = node.children[i];
48 | const actualCellsLength = currentRow.children.length;
49 |
50 | if (actualCellsLength > expectedCellsLength) {
51 | const firstExtraCellNode =
52 | currentRow.children[expectedCellsLength];
53 |
54 | const lastActualCellNode =
55 | currentRow.children[actualCellsLength - 1];
56 |
57 | context.report({
58 | loc: {
59 | start: firstExtraCellNode.position.start,
60 | end: lastActualCellNode.position.end,
61 | },
62 | messageId: "inconsistentColumnCount",
63 | data: {
64 | actualCells: String(actualCellsLength),
65 | expectedCells: String(expectedCellsLength),
66 | },
67 | });
68 | }
69 | }
70 | },
71 | };
72 | },
73 | };
74 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | // Imports
3 | //------------------------------------------------------------------------------
4 |
5 | import type {
6 | // Nodes (abstract)
7 | Node,
8 | Data,
9 | Literal,
10 | Parent,
11 | // Nodes
12 | Blockquote,
13 | Break,
14 | Code,
15 | Definition,
16 | Emphasis,
17 | Heading,
18 | Html,
19 | Image,
20 | ImageReference,
21 | InlineCode,
22 | Link,
23 | LinkReference,
24 | List,
25 | ListItem,
26 | Paragraph,
27 | Root,
28 | Strong,
29 | Text,
30 | ThematicBreak,
31 | // Extensions (GFM)
32 | Delete,
33 | FootnoteDefinition,
34 | FootnoteReference,
35 | Table,
36 | TableCell,
37 | TableRow,
38 | // Extensions (front matter)
39 | Yaml,
40 | } from "mdast";
41 | import type { Linter } from "eslint";
42 | import type {
43 | LanguageOptions,
44 | LanguageContext,
45 | RuleDefinition,
46 | RuleVisitor,
47 | } from "@eslint/core";
48 | import type { MarkdownSourceCode } from "./index.js";
49 |
50 | //------------------------------------------------------------------------------
51 | // Helpers
52 | //------------------------------------------------------------------------------
53 |
54 | /** Adds matching `:exit` selectors for all properties of a `RuleVisitor`. */
55 | type WithExit = {
56 | [Key in keyof RuleVisitorType as
57 | | Key
58 | | `${Key & string}:exit`]: RuleVisitorType[Key];
59 | };
60 |
61 | //------------------------------------------------------------------------------
62 | // Exports
63 | //------------------------------------------------------------------------------
64 |
65 | export interface RangeMap {
66 | indent: number;
67 | js: number;
68 | md: number;
69 | }
70 |
71 | export interface BlockBase {
72 | baseIndentText: string;
73 | comments: string[];
74 | rangeMap: RangeMap[];
75 | }
76 |
77 | export interface Block extends Node, BlockBase {
78 | meta: string | null;
79 | }
80 |
81 | export type Message = Linter.LintMessage;
82 |
83 | export type RuleType = "problem" | "suggestion" | "layout";
84 |
85 | /**
86 | * Markdown TOML.
87 | */
88 | export interface Toml extends Literal {
89 | /**
90 | * Node type of mdast TOML.
91 | */
92 | type: "toml";
93 | /**
94 | * Data associated with the mdast TOML.
95 | */
96 | data?: TomlData | undefined;
97 | }
98 |
99 | /**
100 | * Info associated with mdast TOML nodes by the ecosystem.
101 | */
102 | export interface TomlData extends Data {}
103 |
104 | /**
105 | * Language options provided for Markdown files.
106 | */
107 | export interface MarkdownLanguageOptions extends LanguageOptions {
108 | /**
109 | * The options for parsing frontmatter.
110 | */
111 | frontmatter?: false | "yaml" | "toml";
112 | }
113 |
114 | /**
115 | * The context object that is passed to the Markdown language plugin methods.
116 | */
117 | export type MarkdownLanguageContext = LanguageContext;
118 |
119 | export interface MarkdownRuleVisitor
120 | extends RuleVisitor,
121 | WithExit<
122 | {
123 | root?(node: Root): void;
124 | } & {
125 | [NodeType in
126 | | Blockquote // Nodes
127 | | Break
128 | | Code
129 | | Definition
130 | | Emphasis
131 | | Heading
132 | | Html
133 | | Image
134 | | ImageReference
135 | | InlineCode
136 | | Link
137 | | LinkReference
138 | | List
139 | | ListItem
140 | | Paragraph
141 | | Strong
142 | | Text
143 | | ThematicBreak
144 | | Delete // Extensions (GFM)
145 | | FootnoteDefinition
146 | | FootnoteReference
147 | | Table
148 | | TableCell
149 | | TableRow
150 | | Yaml // Extensions (front matter)
151 | | Toml as NodeType["type"]]?: (
152 | node: NodeType,
153 | parent?: Parent,
154 | ) => void;
155 | }
156 | > {}
157 |
158 | export type MarkdownRuleDefinitionTypeOptions = {
159 | RuleOptions: unknown[];
160 | MessageIds: string;
161 | ExtRuleDocs: Record;
162 | };
163 |
164 | export type MarkdownRuleDefinition<
165 | Options extends Partial = {},
166 | > = RuleDefinition<
167 | // Language specific type options (non-configurable)
168 | {
169 | LangOptions: MarkdownLanguageOptions;
170 | Code: MarkdownSourceCode;
171 | Visitor: MarkdownRuleVisitor;
172 | Node: Node;
173 | } & Required<
174 | // Rule specific type options (custom)
175 | Options &
176 | // Rule specific type options (defaults)
177 | Omit
178 | >
179 | >;
180 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Utility Library
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | /*
7 | * CommonMark does not allow any white space between the brackets in a reference link.
8 | * If that pattern is detected, then it's treated as text and not as a link. This pattern
9 | * is used to detect that situation.
10 | */
11 | export const illegalShorthandTailPattern = /\]\[\s+\]$/u;
12 |
13 | /**
14 | * Finds the line and column offsets for a given offset in a string.
15 | * @param {string} text The text to search.
16 | * @param {number} offset The offset to find.
17 | * @returns {{lineOffset:number,columnOffset:number}} The location of the offset.
18 | * Note that `columnOffset` should be used as an offset to the column number
19 | * of the given text in the source code only when `lineOffset` is 0.
20 | * Otherwise, it should be used as a 0-based column number in the source code.
21 | */
22 | export function findOffsets(text, offset) {
23 | let lineOffset = 0;
24 | let columnOffset = 0;
25 |
26 | for (let i = 0; i < offset; i++) {
27 | if (text[i] === "\n") {
28 | lineOffset++;
29 | columnOffset = 0;
30 | } else {
31 | columnOffset++;
32 | }
33 | }
34 |
35 | return {
36 | lineOffset,
37 | columnOffset,
38 | };
39 | }
40 |
--------------------------------------------------------------------------------
/tests/examples/all.test.js:
--------------------------------------------------------------------------------
1 | import assert from "node:assert";
2 | import fs from "node:fs";
3 | import path from "node:path";
4 | import semver from "semver";
5 | import { createRequire } from "node:module";
6 | import { fileURLToPath } from "node:url";
7 |
8 | const require = createRequire(import.meta.url);
9 | const __filename = fileURLToPath(import.meta.url);
10 | const __dirname = path.dirname(__filename);
11 | const examplesDir = path.resolve(__dirname, "../../examples/");
12 | const examples = fs
13 | .readdirSync(examplesDir)
14 | .filter(exampleDir =>
15 | fs.statSync(path.join(examplesDir, exampleDir)).isDirectory(),
16 | )
17 | .filter(exampleDir =>
18 | fs.existsSync(path.join(examplesDir, exampleDir, "package.json")),
19 | );
20 |
21 | for (const example of examples) {
22 | const cwd = path.join(examplesDir, example);
23 |
24 | // In case when this plugin supports multiple major versions of ESLint,
25 | // CI matrix may include Node.js versions that are not supported by
26 | // the version of ESLint that is used in examples.
27 | // Only exercise the example if the running Node.js version satisfies the
28 | // minimum version constraint.
29 | const eslintPackageJsonPath = require.resolve("eslint/package.json", {
30 | paths: [cwd],
31 | });
32 | const eslintPackageJson = require(eslintPackageJsonPath);
33 |
34 | if (semver.satisfies(process.version, eslintPackageJson.engines.node)) {
35 | describe("examples", () => {
36 | describe(example, () => {
37 | it("reports errors on code blocks in .md files", async () => {
38 | const { FlatESLint } = require(
39 | require.resolve("eslint/use-at-your-own-risk", {
40 | paths: [cwd],
41 | }),
42 | );
43 | const eslint = new FlatESLint({ cwd });
44 | const results = await eslint.lintFiles(["README.md"]);
45 | const readme = results.find(
46 | result => path.basename(result.filePath) == "README.md",
47 | );
48 |
49 | assert.notStrictEqual(readme, null);
50 | assert.ok(readme.messages.length > 0);
51 | });
52 | });
53 | });
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/tests/fixtures/eslint.config.js:
--------------------------------------------------------------------------------
1 | import markdown from "../../src/index.js";
2 | import globals from "globals";
3 |
4 | export default [
5 | {
6 | plugins: {
7 | markdown,
8 | },
9 | languageOptions: {
10 | globals: globals.browser,
11 | },
12 | rules: {
13 | "eol-last": "error",
14 | "no-console": "error",
15 | "no-undef": "error",
16 | quotes: "error",
17 | "spaced-comment": "error",
18 | },
19 | },
20 | {
21 | files: ["*.md", "*.mkdn", "*.mdown", "*.markdown", "*.custom"],
22 | processor: "markdown/markdown",
23 | },
24 | ];
25 |
--------------------------------------------------------------------------------
/tests/fixtures/eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "env": {
4 | "browser": true
5 | },
6 | "plugins": ["markdown"],
7 | "overrides": [
8 | {
9 | "files": ["*.md", "*.mkdn", "*.mdown", "*.markdown", "*.custom"],
10 | "processor": "markdown/markdown"
11 | }
12 | ],
13 | "rules": {
14 | "eol-last": "error",
15 | "no-console": "error",
16 | "no-undef": "error",
17 | "quotes": "error",
18 | "spaced-comment": "error"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/fixtures/filename.md:
--------------------------------------------------------------------------------
1 | # Test
2 |
3 | ```js filename="a/b/C D E.js"
4 | console.log("...");
5 | ```
6 |
--------------------------------------------------------------------------------
/tests/fixtures/long.md:
--------------------------------------------------------------------------------
1 | # Test
2 |
3 | ```txt
4 | Don't lint me!
5 | ```
6 |
7 | This is some code:
8 |
9 | ```js
10 | console.log(42);
11 | ```
12 |
13 | ```js
14 | // Comment
15 | function foo() {
16 | console.log("Hello");
17 | }
18 | ```
19 |
20 |
21 |
22 |
23 | ```js
24 | console.log(process.version);
25 | ```
26 |
27 | How about some JSX?
28 |
29 |
35 |
36 |
37 | ```js
38 | console.log("Error!");
39 | ```
40 |
41 | I may be a code block, but don't lint me!
42 |
43 |
44 |
45 | ```js
46 | !@#$%^&*()
47 | ```
48 |
49 | The end.
50 |
--------------------------------------------------------------------------------
/tests/fixtures/recommended.js:
--------------------------------------------------------------------------------
1 | import markdown from "../../src/index.js";
2 | import js from "@eslint/js";
3 |
4 | export default [
5 | js.configs.recommended,
6 | ...markdown.configs.processor,
7 | {
8 | rules: {
9 | "no-console": "error",
10 | },
11 | },
12 | ];
13 |
--------------------------------------------------------------------------------
/tests/fixtures/recommended.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": ["eslint:recommended", "plugin:markdown/recommended-legacy"],
4 | "rules": {
5 | "no-console": "error"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/tests/language/markdown-language.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for MarkdownLanguage
3 | * @author 루밀LuMir(lumirlumir)
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Imports
8 | //-----------------------------------------------------------------------------
9 |
10 | import { MarkdownLanguage } from "../../src/language/markdown-language.js";
11 | import assert from "node:assert";
12 |
13 | //-----------------------------------------------------------------------------
14 | // Tests
15 | //-----------------------------------------------------------------------------
16 |
17 | describe("MarkdownLanguage", () => {
18 | describe("validateLanguageOptions()", () => {
19 | it("should throw an error if `frontmatter` is not `false`, `'yaml'`, or `'toml'`", () => {
20 | const language = new MarkdownLanguage();
21 |
22 | assert.throws(() => {
23 | language.validateLanguageOptions({ frontmatter: "invalid" });
24 | }, /Invalid language option value/u);
25 | assert.throws(() => {
26 | language.validateLanguageOptions({ frontmatter: 123 });
27 | }, /Invalid language option value/u);
28 | });
29 |
30 | it("should not throw an error when `frontmatter` is not provided", () => {
31 | const language = new MarkdownLanguage();
32 |
33 | assert.doesNotThrow(() => {
34 | language.validateLanguageOptions();
35 | });
36 | assert.doesNotThrow(() => {
37 | language.validateLanguageOptions({});
38 | });
39 | });
40 |
41 | it("should not throw an error when `frontmatter` is not provided and other keys are present", () => {
42 | const language = new MarkdownLanguage();
43 | assert.doesNotThrow(() => {
44 | language.validateLanguageOptions({ foo: "bar" });
45 | });
46 | });
47 |
48 | it("should not throw an error when `frontmatter` has a correct value in commonmark mode", () => {
49 | const language = new MarkdownLanguage({ mode: "commonmark" });
50 |
51 | assert.doesNotThrow(() => {
52 | language.validateLanguageOptions({ frontmatter: false });
53 | });
54 | assert.doesNotThrow(() => {
55 | language.validateLanguageOptions({ frontmatter: "yaml" });
56 | });
57 | assert.doesNotThrow(() => {
58 | language.validateLanguageOptions({ frontmatter: "toml" });
59 | });
60 | });
61 |
62 | it("should not throw an error when `frontmatter` has a correct value in gfm mode", () => {
63 | const language = new MarkdownLanguage({ mode: "gfm" });
64 |
65 | assert.doesNotThrow(() => {
66 | language.validateLanguageOptions({ frontmatter: false });
67 | });
68 | assert.doesNotThrow(() => {
69 | language.validateLanguageOptions({ frontmatter: "yaml" });
70 | });
71 | assert.doesNotThrow(() => {
72 | language.validateLanguageOptions({ frontmatter: "toml" });
73 | });
74 | });
75 | });
76 |
77 | describe("parse()", () => {
78 | it("should parse markdown", () => {
79 | const language = new MarkdownLanguage();
80 | const result = language.parse({
81 | body: "# Hello, World!\n\nHello, World!",
82 | path: "test.md",
83 | });
84 |
85 | assert.strictEqual(result.ok, true);
86 | assert.strictEqual(result.ast.type, "root");
87 | assert.strictEqual(result.ast.children[0].type, "heading");
88 | assert.strictEqual(result.ast.children[1].type, "paragraph");
89 | });
90 |
91 | it("should not parse gfm features in commonmark mode", () => {
92 | const language = new MarkdownLanguage({ mode: "commonmark" });
93 | const result = language.parse({
94 | body: "| Column 1 | Column 2 |\n| -------- | -------- |\n| Cell 1 | Cell 2 |",
95 | path: "test.md",
96 | });
97 |
98 | assert.strictEqual(result.ok, true);
99 | // The table should not be parsed and should be recognized as plain text.
100 | assert.strictEqual(result.ast.children[0].type, "paragraph");
101 | });
102 |
103 | it("should parse gfm features in gfm mode", () => {
104 | const language = new MarkdownLanguage({ mode: "gfm" });
105 | const result = language.parse({
106 | body: "| Column 1 | Column 2 |\n| -------- | -------- |\n| Cell 1 | Cell 2 |",
107 | path: "test.md",
108 | });
109 |
110 | assert.strictEqual(result.ok, true);
111 | // The table should be parsed correctly.
112 | assert.strictEqual(result.ast.children[0].type, "table");
113 | });
114 |
115 | it("should not parse frontmatter by default", () => {
116 | const language = new MarkdownLanguage();
117 | const result = language.parse({
118 | body: "---\ntitle: Hello\n---\n\n# Hello, World!\n\nHello, World!",
119 | path: "test.md",
120 | });
121 |
122 | assert.strictEqual(result.ok, true);
123 | assert.strictEqual(result.ast.type, "root");
124 | assert.strictEqual(result.ast.children[0].type, "thematicBreak");
125 | assert.strictEqual(result.ast.children[1].type, "heading");
126 | assert.strictEqual(result.ast.children[2].type, "heading");
127 | assert.strictEqual(result.ast.children[3].type, "paragraph");
128 | });
129 |
130 | it("should parse YAML frontmatter in commonmark mode when `frontmatter: 'yaml'` is set", () => {
131 | const language = new MarkdownLanguage({ mode: "commonmark" });
132 | const result = language.parse(
133 | {
134 | body: "---\ntitle: Hello\n---\n\n# Hello, World!\n\nHello, World!",
135 | path: "test.md",
136 | },
137 | {
138 | languageOptions: {
139 | frontmatter: "yaml",
140 | },
141 | },
142 | );
143 |
144 | assert.strictEqual(result.ok, true);
145 | assert.strictEqual(result.ast.type, "root");
146 | assert.strictEqual(result.ast.children[0].type, "yaml");
147 | assert.strictEqual(result.ast.children[0].value, "title: Hello");
148 | assert.strictEqual(result.ast.children[1].type, "heading");
149 | assert.strictEqual(result.ast.children[2].type, "paragraph");
150 | });
151 |
152 | it("should parse YAML frontmatter in gfm mode when `frontmatter: 'yaml'` is set", () => {
153 | const language = new MarkdownLanguage({ mode: "gfm" });
154 | const result = language.parse(
155 | {
156 | body: "---\ntitle: Hello\n---\n\n# Hello, World!\n\nHello, World!",
157 | path: "test.md",
158 | },
159 | {
160 | languageOptions: {
161 | frontmatter: "yaml",
162 | },
163 | },
164 | );
165 |
166 | assert.strictEqual(result.ok, true);
167 | assert.strictEqual(result.ast.type, "root");
168 | assert.strictEqual(result.ast.children[0].type, "yaml");
169 | assert.strictEqual(result.ast.children[0].value, "title: Hello");
170 | assert.strictEqual(result.ast.children[1].type, "heading");
171 | assert.strictEqual(result.ast.children[2].type, "paragraph");
172 | });
173 |
174 | it("should parse TOML frontmatter in commonmark mode when `frontmatter: 'toml'` is set", () => {
175 | const language = new MarkdownLanguage({ mode: "commonmark" });
176 | const result = language.parse(
177 | {
178 | body: "+++\ntitle = 'Hello'\n+++\n\n# Hello, World!\n\nHello, World!",
179 | path: "test.md",
180 | },
181 | {
182 | languageOptions: {
183 | frontmatter: "toml",
184 | },
185 | },
186 | );
187 |
188 | assert.strictEqual(result.ok, true);
189 | assert.strictEqual(result.ast.type, "root");
190 | assert.strictEqual(result.ast.children[0].type, "toml");
191 | assert.strictEqual(result.ast.children[0].value, "title = 'Hello'");
192 | assert.strictEqual(result.ast.children[1].type, "heading");
193 | assert.strictEqual(result.ast.children[2].type, "paragraph");
194 | });
195 |
196 | it("should parse TOML frontmatter in gfm mode when `frontmatter: 'toml'` is set", () => {
197 | const language = new MarkdownLanguage({ mode: "gfm" });
198 | const result = language.parse(
199 | {
200 | body: "+++\ntitle = 'Hello'\n+++\n\n# Hello, World!\n\nHello, World!",
201 | path: "test.md",
202 | },
203 | {
204 | languageOptions: {
205 | frontmatter: "toml",
206 | },
207 | },
208 | );
209 |
210 | assert.strictEqual(result.ok, true);
211 | assert.strictEqual(result.ast.type, "root");
212 | assert.strictEqual(result.ast.children[0].type, "toml");
213 | assert.strictEqual(result.ast.children[0].value, "title = 'Hello'");
214 | assert.strictEqual(result.ast.children[1].type, "heading");
215 | assert.strictEqual(result.ast.children[2].type, "paragraph");
216 | });
217 | });
218 |
219 | describe("createSourceCode()", () => {
220 | it("should create a MarkdownSourceCode instance for commonmark", () => {
221 | const language = new MarkdownLanguage({ mode: "commonmark" });
222 | const file = {
223 | body: "| Column 1 | Column 2 |\n| -------- | -------- |\n| Cell 1 | Cell 2 |",
224 | path: "test.md",
225 | };
226 | const parseResult = language.parse(file);
227 | const sourceCode = language.createSourceCode(file, parseResult);
228 |
229 | assert.strictEqual(
230 | sourceCode.constructor.name,
231 | "MarkdownSourceCode",
232 | );
233 | assert.strictEqual(sourceCode.ast.type, "root");
234 | assert.strictEqual(sourceCode.ast.children[0].type, "paragraph");
235 | assert.strictEqual(
236 | sourceCode.text,
237 | "| Column 1 | Column 2 |\n| -------- | -------- |\n| Cell 1 | Cell 2 |",
238 | );
239 | });
240 |
241 | it("should create a MarkdownSourceCode instance for gfm", () => {
242 | const language = new MarkdownLanguage({ mode: "gfm" });
243 | const file = {
244 | body: "| Column 1 | Column 2 |\n| -------- | -------- |\n| Cell 1 | Cell 2 |",
245 | path: "test.md",
246 | };
247 | const parseResult = language.parse(file);
248 | const sourceCode = language.createSourceCode(file, parseResult);
249 |
250 | assert.strictEqual(
251 | sourceCode.constructor.name,
252 | "MarkdownSourceCode",
253 | );
254 | assert.strictEqual(sourceCode.ast.type, "root");
255 | assert.strictEqual(sourceCode.ast.children[0].type, "table");
256 | assert.strictEqual(
257 | sourceCode.text,
258 | "| Column 1 | Column 2 |\n| -------- | -------- |\n| Cell 1 | Cell 2 |",
259 | );
260 | });
261 | });
262 | });
263 |
--------------------------------------------------------------------------------
/tests/rules/fenced-code-language.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for fenced-code-language rule.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Imports
8 | //------------------------------------------------------------------------------
9 |
10 | import rule from "../../src/rules/fenced-code-language.js";
11 | import markdown from "../../src/index.js";
12 | import { RuleTester } from "eslint";
13 |
14 | //------------------------------------------------------------------------------
15 | // Tests
16 | //------------------------------------------------------------------------------
17 |
18 | const ruleTester = new RuleTester({
19 | plugins: {
20 | markdown,
21 | },
22 | language: "markdown/commonmark",
23 | });
24 |
25 | ruleTester.run("fenced-code-language", rule, {
26 | valid: [
27 | // backtick code block
28 | `\`\`\`js
29 | console.log("Hello, world!");
30 | \`\`\``,
31 | `\`\`\`javascript
32 | console.log("Hello, world!");
33 | \`\`\``,
34 |
35 | // tilde code block
36 | `~~~js
37 | console.log("Hello, world!");
38 | ~~~`,
39 | `~~~javascript
40 | console.log("Hello, world!");
41 | ~~~`,
42 |
43 | // indented code block
44 | `
45 | console.log("Hello, world!");
46 | `,
47 | {
48 | code: `\`\`\`js
49 | console.log("Hello, world!");
50 | \`\`\``,
51 | options: [{ required: ["js"] }],
52 | },
53 | ],
54 | invalid: [
55 | {
56 | code: `\`\`\`
57 | console.log("Hello, world!");
58 | \`\`\``,
59 | errors: [
60 | {
61 | messageId: "missingLanguage",
62 | line: 1,
63 | column: 1,
64 | endLine: 3,
65 | endColumn: 20,
66 | },
67 | ],
68 | },
69 | {
70 | code: `~~~
71 | console.log("Hello, world!");
72 | ~~~`,
73 | errors: [
74 | {
75 | messageId: "missingLanguage",
76 | line: 1,
77 | column: 1,
78 | endLine: 3,
79 | endColumn: 20,
80 | },
81 | ],
82 | },
83 | {
84 | code: `\`\`\`javascript
85 | console.log("Hello, world!");
86 | \`\`\``,
87 | options: [{ required: ["js"] }],
88 | errors: [
89 | {
90 | messageId: "disallowedLanguage",
91 | line: 1,
92 | column: 1,
93 | endLine: 3,
94 | endColumn: 20,
95 | },
96 | ],
97 | },
98 | ],
99 | });
100 |
--------------------------------------------------------------------------------
/tests/rules/heading-increment.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for heading-increment rule.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Imports
8 | //------------------------------------------------------------------------------
9 |
10 | import rule from "../../src/rules/heading-increment.js";
11 | import markdown from "../../src/index.js";
12 | import { RuleTester } from "eslint";
13 | import dedent from "dedent";
14 |
15 | //------------------------------------------------------------------------------
16 | // Tests
17 | //------------------------------------------------------------------------------
18 |
19 | const ruleTester = new RuleTester({
20 | plugins: {
21 | markdown,
22 | },
23 | language: "markdown/commonmark",
24 | });
25 |
26 | ruleTester.run("heading-increment", rule, {
27 | valid: [
28 | "# Heading 1",
29 | "## Heading 2",
30 | dedent`# Heading 1
31 |
32 | ## Heading 2`,
33 | dedent`# Heading 1
34 |
35 | # Heading 2`,
36 | ],
37 | invalid: [
38 | {
39 | code: dedent`
40 | # Heading 1
41 |
42 | ### Heading 3
43 | `,
44 | errors: [
45 | {
46 | messageId: "skippedHeading",
47 | line: 3,
48 | column: 1,
49 | endLine: 3,
50 | endColumn: 14,
51 | data: {
52 | fromLevel: 1,
53 | toLevel: 3,
54 | },
55 | },
56 | ],
57 | },
58 | {
59 | code: dedent`
60 | ## Heading 2
61 |
62 | ##### Heading 5
63 | `,
64 | errors: [
65 | {
66 | messageId: "skippedHeading",
67 | line: 3,
68 | column: 1,
69 | endLine: 3,
70 | endColumn: 16,
71 | data: {
72 | fromLevel: 2,
73 | toLevel: 5,
74 | },
75 | },
76 | ],
77 | },
78 | {
79 | code: dedent`
80 | Heading 1
81 | =========
82 |
83 | ### Heading 3
84 | `,
85 | errors: [
86 | {
87 | messageId: "skippedHeading",
88 | line: 4,
89 | column: 1,
90 | endLine: 4,
91 | endColumn: 14,
92 | data: {
93 | fromLevel: 1,
94 | toLevel: 3,
95 | },
96 | },
97 | ],
98 | },
99 | ],
100 | });
101 |
--------------------------------------------------------------------------------
/tests/rules/no-duplicate-definitions.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for no-duplicate-definitions rule.
3 | * @author 루밀LuMir(lumirlumir)
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Imports
8 | //------------------------------------------------------------------------------
9 |
10 | import rule from "../../src/rules/no-duplicate-definitions.js";
11 | import markdown from "../../src/index.js";
12 | import { RuleTester } from "eslint";
13 |
14 | //------------------------------------------------------------------------------
15 | // Tests
16 | //------------------------------------------------------------------------------
17 |
18 | const ruleTester = new RuleTester({
19 | plugins: {
20 | markdown,
21 | },
22 | language: "markdown/gfm",
23 | });
24 |
25 | ruleTester.run("no-duplicate-definitions", rule, {
26 | valid: [
27 | `
28 | [mercury]: https://example.com/mercury/
29 | `,
30 |
31 | `
32 | [mercury]: https://example.com/mercury/
33 | [venus]: https://example.com/venus/
34 | `,
35 |
36 | `
37 | [^mercury]: Hello, Mercury!
38 | `,
39 |
40 | `
41 | [^mercury]: Hello, Mercury!
42 | [^venus]: Hello, Venus!
43 | `,
44 |
45 | `
46 | [alpha]: bravo
47 |
48 | [^alpha]: bravo
49 | `,
50 |
51 | `
52 | [//]: # (This is a comment 1)
53 | [//]: <> (This is a comment 2)
54 | `,
55 |
56 | {
57 | code: `
58 | [mercury]: https://example.com/mercury/
59 | [mercury]: https://example.com/venus/
60 | `,
61 | options: [
62 | {
63 | allowDefinitions: ["mercury"],
64 | },
65 | ],
66 | },
67 |
68 | {
69 | code: `
70 | [^mercury]: Hello, Mercury!
71 | [^mercury]: Hello, Venus!
72 | `,
73 | options: [
74 | {
75 | allowFootnoteDefinitions: ["mercury"],
76 | },
77 | ],
78 | },
79 | ],
80 |
81 | invalid: [
82 | {
83 | code: `
84 | [mercury]: https://example.com/mercury/
85 | [mercury]: https://example.com/venus/
86 | `,
87 | errors: [
88 | {
89 | messageId: "duplicateDefinition",
90 | line: 3,
91 | column: 1,
92 | endLine: 3,
93 | endColumn: 38,
94 | },
95 | ],
96 | },
97 |
98 | {
99 | code: `
100 | [mercury]: https://example.com/mercury/
101 | [mercury]: https://example.com/venus/
102 | [mercury]: https://example.com/earth/
103 | [mercury]: https://example.com/mars/
104 | `,
105 | errors: [
106 | {
107 | messageId: "duplicateDefinition",
108 | line: 3,
109 | column: 1,
110 | endLine: 3,
111 | endColumn: 38,
112 | },
113 | {
114 | messageId: "duplicateDefinition",
115 | line: 4,
116 | column: 1,
117 | endLine: 4,
118 | endColumn: 38,
119 | },
120 | {
121 | messageId: "duplicateDefinition",
122 | line: 5,
123 | column: 1,
124 | endLine: 5,
125 | endColumn: 37,
126 | },
127 | ],
128 | },
129 |
130 | {
131 | code: `
132 | [mercury]: https://example.com/mercury/
133 | [Mercury]: https://example.com/venus/
134 | `, // case insensitive
135 | errors: [
136 | {
137 | messageId: "duplicateDefinition",
138 | line: 3,
139 | column: 1,
140 | endLine: 3,
141 | endColumn: 38,
142 | },
143 | ],
144 | },
145 |
146 | {
147 | code: `
148 | [mercury]: https://example.com/mercury/
149 | [mercury ]: https://example.com/venus/
150 | `,
151 | errors: [
152 | {
153 | messageId: "duplicateDefinition",
154 | line: 3,
155 | column: 1,
156 | endLine: 3,
157 | endColumn: 42,
158 | },
159 | ],
160 | },
161 |
162 | {
163 | code: `
164 | [mercury]: https://example.com/mercury/
165 | [mercury]: https://example.com/venus/
166 | `,
167 | options: [
168 | {
169 | allowDefinitions: ["venus"],
170 | allowFootnoteDefinitions: ["mercury"],
171 | },
172 | ],
173 |
174 | errors: [
175 | {
176 | messageId: "duplicateDefinition",
177 | line: 3,
178 | column: 1,
179 | endLine: 3,
180 | endColumn: 38,
181 | },
182 | ],
183 | },
184 |
185 | {
186 | code: `
187 | [^mercury]: Hello, Mercury!
188 | [^mercury]: Hello, Venus!
189 | `,
190 | errors: [
191 | {
192 | messageId: "duplicateFootnoteDefinition",
193 | line: 3,
194 | column: 1,
195 | endLine: 3,
196 | endColumn: 26,
197 | },
198 | ],
199 | },
200 |
201 | {
202 | code: `
203 | [^mercury]: Hello, Mercury!
204 | [^mercury]: Hello, Venus!
205 | [^mercury]: Hello, Earth!
206 | [^mercury]: Hello, Mars!
207 | `,
208 | errors: [
209 | {
210 | messageId: "duplicateFootnoteDefinition",
211 | line: 3,
212 | column: 1,
213 | endLine: 3,
214 | endColumn: 26,
215 | },
216 | {
217 | messageId: "duplicateFootnoteDefinition",
218 | line: 4,
219 | column: 1,
220 | endLine: 4,
221 | endColumn: 26,
222 | },
223 | {
224 | messageId: "duplicateFootnoteDefinition",
225 | line: 5,
226 | column: 1,
227 | endLine: 5,
228 | endColumn: 25,
229 | },
230 | ],
231 | },
232 |
233 | {
234 | code: `
235 | [^mercury]: Hello, Mercury!
236 | [^Mercury]: Hello, Venus!
237 | `, // case insensitive
238 | errors: [
239 | {
240 | messageId: "duplicateFootnoteDefinition",
241 | line: 3,
242 | column: 1,
243 | endLine: 3,
244 | endColumn: 26,
245 | },
246 | ],
247 | },
248 |
249 | {
250 | code: `
251 | [^mercury]: Hello, Mercury!
252 | [^mercury]: Hello, Venus!
253 | `,
254 | options: [
255 | {
256 | allowDefinitions: ["mercury"],
257 | allowFootnoteDefinitions: ["venus"],
258 | },
259 | ],
260 |
261 | errors: [
262 | {
263 | messageId: "duplicateFootnoteDefinition",
264 | line: 3,
265 | column: 1,
266 | endLine: 3,
267 | endColumn: 26,
268 | },
269 | ],
270 | },
271 |
272 | {
273 | code: `
274 | [mercury]: https://example.com/mercury/
275 | [earth]: https://example.com/earth/
276 | [mars]: https://example.com/mars/
277 |
278 | [//]: # (comment about mars)
279 |
280 | [jupiter]: https://example.com/jupiter/
281 |
282 | [//]: # (comment about jupiter)
283 |
284 | [mercury]: https://example.com/venus/
285 | `,
286 | errors: [
287 | {
288 | messageId: "duplicateDefinition",
289 | line: 12,
290 | column: 1,
291 | endLine: 12,
292 | endColumn: 38,
293 | },
294 | ],
295 | },
296 | ],
297 | });
298 |
--------------------------------------------------------------------------------
/tests/rules/no-duplicate-headings.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for no-duplicate-heading rule.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Imports
8 | //------------------------------------------------------------------------------
9 |
10 | import rule from "../../src/rules/no-duplicate-headings.js";
11 | import markdown from "../../src/index.js";
12 | import { RuleTester } from "eslint";
13 |
14 | //------------------------------------------------------------------------------
15 | // Tests
16 | //------------------------------------------------------------------------------
17 |
18 | const ruleTester = new RuleTester({
19 | plugins: {
20 | markdown,
21 | },
22 | language: "markdown/commonmark",
23 | });
24 |
25 | ruleTester.run("no-duplicate-headings", rule, {
26 | valid: [
27 | `# Heading 1
28 |
29 | ## Heading 2`,
30 | ],
31 | invalid: [
32 | {
33 | code: `
34 | # Heading 1
35 |
36 | # Heading 1
37 | `,
38 | errors: [
39 | {
40 | messageId: "duplicateHeading",
41 | line: 4,
42 | column: 1,
43 | endLine: 4,
44 | endColumn: 12,
45 | },
46 | ],
47 | },
48 | {
49 | code: `
50 | # Heading 1
51 |
52 | ## Heading 1
53 | `,
54 | errors: [
55 | {
56 | messageId: "duplicateHeading",
57 | line: 4,
58 | column: 1,
59 | endLine: 4,
60 | endColumn: 13,
61 | },
62 | ],
63 | },
64 | {
65 | code: `
66 | # Heading 1
67 |
68 | Heading 1
69 | ---------
70 | `,
71 | errors: [
72 | {
73 | messageId: "duplicateHeading",
74 | line: 4,
75 | column: 1,
76 | endLine: 5,
77 | endColumn: 10,
78 | },
79 | ],
80 | },
81 | {
82 | code: `
83 | # Heading 1
84 |
85 | Heading 1
86 | =========
87 | `,
88 | errors: [
89 | {
90 | messageId: "duplicateHeading",
91 | line: 4,
92 | column: 1,
93 | endLine: 5,
94 | endColumn: 10,
95 | },
96 | ],
97 | },
98 | ],
99 | });
100 |
--------------------------------------------------------------------------------
/tests/rules/no-empty-definitions.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for no-empty-definitions rule.
3 | * @author Pixel998
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Imports
8 | //------------------------------------------------------------------------------
9 |
10 | import rule from "../../src/rules/no-empty-definitions.js";
11 | import markdown from "../../src/index.js";
12 | import { RuleTester } from "eslint";
13 | import dedent from "dedent";
14 |
15 | //------------------------------------------------------------------------------
16 | // Tests
17 | //------------------------------------------------------------------------------
18 |
19 | const ruleTester = new RuleTester({
20 | plugins: {
21 | markdown,
22 | },
23 | language: "markdown/commonmark",
24 | });
25 |
26 | ruleTester.run("no-empty-definitions", rule, {
27 | valid: [
28 | "[foo]: bar",
29 | "[foo]: #bar",
30 | "[foo]: http://bar.com",
31 | "[foo]: ",
32 | ],
33 | invalid: [
34 | {
35 | code: "[foo]: #",
36 | errors: [
37 | {
38 | messageId: "emptyDefinition",
39 | line: 1,
40 | column: 1,
41 | endLine: 1,
42 | endColumn: 9,
43 | },
44 | ],
45 | },
46 | {
47 | code: "[foo]: <>",
48 | errors: [
49 | {
50 | messageId: "emptyDefinition",
51 | line: 1,
52 | column: 1,
53 | endLine: 1,
54 | endColumn: 10,
55 | },
56 | ],
57 | },
58 | {
59 | code: dedent`
60 | [foo]: #
61 | [bar]: <>
62 | `,
63 | errors: [
64 | {
65 | messageId: "emptyDefinition",
66 | line: 1,
67 | column: 1,
68 | endLine: 1,
69 | endColumn: 9,
70 | },
71 | {
72 | messageId: "emptyDefinition",
73 | line: 2,
74 | column: 1,
75 | endLine: 2,
76 | endColumn: 10,
77 | },
78 | ],
79 | },
80 | ],
81 | });
82 |
--------------------------------------------------------------------------------
/tests/rules/no-empty-images.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for no-empty-images rule.
3 | * @author 루밀LuMir(lumirlumir)
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Imports
8 | //------------------------------------------------------------------------------
9 |
10 | import rule from "../../src/rules/no-empty-images.js";
11 | import markdown from "../../src/index.js";
12 | import { RuleTester } from "eslint";
13 |
14 | //------------------------------------------------------------------------------
15 | // Tests
16 | //------------------------------------------------------------------------------
17 |
18 | const ruleTester = new RuleTester({
19 | plugins: {
20 | markdown,
21 | },
22 | language: "markdown/commonmark",
23 | });
24 |
25 | ruleTester.run("no-empty-images", rule, {
26 | valid: ["", "", ""],
27 | invalid: [
28 | {
29 | code: "![]()",
30 | errors: [
31 | {
32 | messageId: "emptyImage",
33 | line: 1,
34 | column: 1,
35 | endLine: 1,
36 | endColumn: 6,
37 | },
38 | ],
39 | },
40 | {
41 | code: "",
42 | errors: [
43 | {
44 | messageId: "emptyImage",
45 | line: 1,
46 | column: 1,
47 | endLine: 1,
48 | endColumn: 7,
49 | },
50 | ],
51 | },
52 | {
53 | code: "![foo]()",
54 | errors: [
55 | {
56 | messageId: "emptyImage",
57 | line: 1,
58 | column: 1,
59 | endLine: 1,
60 | endColumn: 9,
61 | },
62 | ],
63 | },
64 | {
65 | code: "",
66 | errors: [
67 | {
68 | messageId: "emptyImage",
69 | line: 1,
70 | column: 1,
71 | endLine: 1,
72 | endColumn: 10,
73 | },
74 | ],
75 | },
76 | {
77 | code: "",
78 | errors: [
79 | {
80 | messageId: "emptyImage",
81 | line: 1,
82 | column: 1,
83 | endLine: 1,
84 | endColumn: 10,
85 | },
86 | ],
87 | },
88 | ],
89 | });
90 |
--------------------------------------------------------------------------------
/tests/rules/no-empty-links.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for no-empty-links rule.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Imports
8 | //------------------------------------------------------------------------------
9 |
10 | import rule from "../../src/rules/no-empty-links.js";
11 | import markdown from "../../src/index.js";
12 | import { RuleTester } from "eslint";
13 |
14 | //------------------------------------------------------------------------------
15 | // Tests
16 | //------------------------------------------------------------------------------
17 |
18 | const ruleTester = new RuleTester({
19 | plugins: {
20 | markdown,
21 | },
22 | language: "markdown/commonmark",
23 | });
24 |
25 | ruleTester.run("no-empty-links", rule, {
26 | valid: ["[foo](bar)", "[foo](#bar)", "[foo](http://bar.com)"],
27 | invalid: [
28 | {
29 | code: "[foo]()",
30 | errors: [
31 | {
32 | messageId: "emptyLink",
33 | line: 1,
34 | column: 1,
35 | endLine: 1,
36 | endColumn: 8,
37 | },
38 | ],
39 | },
40 | {
41 | code: "[foo](#)",
42 | errors: [
43 | {
44 | messageId: "emptyLink",
45 | line: 1,
46 | column: 1,
47 | endLine: 1,
48 | endColumn: 9,
49 | },
50 | ],
51 | },
52 | {
53 | code: "[foo]( )",
54 | errors: [
55 | {
56 | messageId: "emptyLink",
57 | line: 1,
58 | column: 1,
59 | endLine: 1,
60 | endColumn: 9,
61 | },
62 | ],
63 | },
64 | ],
65 | });
66 |
--------------------------------------------------------------------------------
/tests/rules/no-html.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for no-html rule.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Imports
8 | //------------------------------------------------------------------------------
9 |
10 | import rule from "../../src/rules/no-html.js";
11 | import markdown from "../../src/index.js";
12 | import { RuleTester } from "eslint";
13 | import dedent from "dedent";
14 |
15 | //------------------------------------------------------------------------------
16 | // Tests
17 | //------------------------------------------------------------------------------
18 |
19 | const ruleTester = new RuleTester({
20 | plugins: {
21 | markdown,
22 | },
23 | language: "markdown/commonmark",
24 | });
25 |
26 | ruleTester.run("no-html", rule, {
27 | valid: [
28 | "Hello world!",
29 | " 1 < 5",
30 | "",
31 | dedent`\`\`\`html
32 | Hello world!
33 | \`\`\``,
34 | {
35 | code: "Hello world! ",
36 | options: [{ allowed: ["b"] }],
37 | },
38 | {
39 | code: "Hello world! ",
40 | options: [{ allowed: ["custom-element"] }],
41 | },
42 | ],
43 | invalid: [
44 | {
45 | code: "Hello world! ",
46 | errors: [
47 | {
48 | messageId: "disallowedElement",
49 | line: 1,
50 | column: 1,
51 | endLine: 1,
52 | endColumn: 4,
53 | data: {
54 | name: "b",
55 | },
56 | },
57 | ],
58 | },
59 | {
60 | code: "Hello world! ",
61 | options: [{ allowed: ["em"] }],
62 | errors: [
63 | {
64 | messageId: "disallowedElement",
65 | line: 1,
66 | column: 1,
67 | endLine: 1,
68 | endColumn: 4,
69 | data: {
70 | name: "b",
71 | },
72 | },
73 | ],
74 | },
75 | {
76 | code: "Hello world! Goodbye world! ",
77 | options: [{ allowed: ["em"] }],
78 | errors: [
79 | {
80 | messageId: "disallowedElement",
81 | line: 1,
82 | column: 1,
83 | endLine: 1,
84 | endColumn: 4,
85 | data: {
86 | name: "b",
87 | },
88 | },
89 | {
90 | messageId: "disallowedElement",
91 | line: 1,
92 | column: 20,
93 | endLine: 1,
94 | endColumn: 23,
95 | data: {
96 | name: "i",
97 | },
98 | },
99 | ],
100 | },
101 | {
102 | code: "Hello world! ",
103 | options: [{ allowed: ["em"] }],
104 | errors: [
105 | {
106 | messageId: "disallowedElement",
107 | line: 1,
108 | column: 12,
109 | endLine: 1,
110 | endColumn: 15,
111 | data: {
112 | name: "b",
113 | },
114 | },
115 | ],
116 | },
117 | {
118 | code: "Hello world! ",
119 | options: [{ allowed: ["em"] }],
120 | errors: [
121 | {
122 | messageId: "disallowedElement",
123 | line: 1,
124 | column: 1,
125 | endLine: 1,
126 | endColumn: 17,
127 | data: {
128 | name: "custom-element",
129 | },
130 | },
131 | ],
132 | },
133 | ],
134 | });
135 |
--------------------------------------------------------------------------------
/tests/rules/no-invalid-label-refs.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for no-invalid-label-refs rule.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Imports
8 | //------------------------------------------------------------------------------
9 |
10 | import rule from "../../src/rules/no-invalid-label-refs.js";
11 | import markdown from "../../src/index.js";
12 | import { RuleTester } from "eslint";
13 |
14 | //------------------------------------------------------------------------------
15 | // Tests
16 | //------------------------------------------------------------------------------
17 |
18 | const ruleTester = new RuleTester({
19 | plugins: {
20 | markdown,
21 | },
22 | language: "markdown/commonmark",
23 | });
24 |
25 | ruleTester.run("no-invalid-label-refs", rule, {
26 | valid: [
27 | "[*foo*]",
28 | "[foo]\n\n[foo]: http://bar.com",
29 | "[foo][ foo ]\n\n[foo]: http://bar.com",
30 | "![foo][foo]\n\n[foo]: http://bar.com/image.jpg",
31 | "[foo][]\n\n[foo]: http://bar.com/image.jpg",
32 | "![foo][]\n\n[foo]: http://bar.com/image.jpg",
33 | "[ foo ][]\n\n[foo]: http://bar.com/image.jpg",
34 | ],
35 | invalid: [
36 | {
37 | code: "[foo][ ]\n\n[foo]: http://bar.com/image.jpg",
38 | errors: [
39 | {
40 | messageId: "invalidLabelRef",
41 | data: { label: "foo" },
42 | line: 1,
43 | column: 6,
44 | endLine: 1,
45 | endColumn: 9,
46 | },
47 | ],
48 | },
49 | {
50 | code: "![foo][ ]\n\n[foo]: http://bar.com/image.jpg",
51 | errors: [
52 | {
53 | messageId: "invalidLabelRef",
54 | data: { label: "foo" },
55 | line: 1,
56 | column: 7,
57 | endLine: 1,
58 | endColumn: 10,
59 | },
60 | ],
61 | },
62 | {
63 | code: "[\nfoo\n][\n]\n\n[foo]: http://bar.com/image.jpg",
64 | errors: [
65 | {
66 | messageId: "invalidLabelRef",
67 | data: { label: "foo" },
68 | line: 3,
69 | column: 2,
70 | endLine: 4,
71 | endColumn: 1,
72 | },
73 | ],
74 | },
75 | {
76 | code: "[foo][ ]\n[bar][ ]\n\n[foo]: http://foo.com\n[bar]: http://bar.com",
77 | errors: [
78 | {
79 | messageId: "invalidLabelRef",
80 | data: { label: "foo" },
81 | line: 1,
82 | column: 6,
83 | endLine: 1,
84 | endColumn: 9,
85 | },
86 | {
87 | messageId: "invalidLabelRef",
88 | data: { label: "bar" },
89 | line: 2,
90 | column: 6,
91 | endLine: 2,
92 | endColumn: 9,
93 | },
94 | ],
95 | },
96 | {
97 | code: "[foo][ ]\n![bar][ ]\n\n[foo]: http://foo.com\n[bar]: http://bar.com",
98 | errors: [
99 | {
100 | messageId: "invalidLabelRef",
101 | data: { label: "foo" },
102 | line: 1,
103 | column: 6,
104 | endLine: 1,
105 | endColumn: 9,
106 | },
107 | {
108 | messageId: "invalidLabelRef",
109 | data: { label: "bar" },
110 | line: 2,
111 | column: 7,
112 | endLine: 2,
113 | endColumn: 10,
114 | },
115 | ],
116 | },
117 | {
118 | code: "- - - [foo][ ]\n\n[foo]: http://foo.com",
119 | errors: [
120 | {
121 | messageId: "invalidLabelRef",
122 | data: { label: "foo" },
123 | line: 1,
124 | column: 12,
125 | endLine: 1,
126 | endColumn: 15,
127 | },
128 | ],
129 | },
130 | {
131 | code: "eslint [eslint][ ]",
132 | errors: [
133 | {
134 | messageId: "invalidLabelRef",
135 | data: { label: "eslint" },
136 | line: 1,
137 | column: 16,
138 | endLine: 1,
139 | endColumn: 19,
140 | },
141 | ],
142 | },
143 | {
144 | code: "\\\\eslint [eslint][ ]",
145 | errors: [
146 | {
147 | messageId: "invalidLabelRef",
148 | data: { label: "eslint" },
149 | line: 1,
150 | column: 18,
151 | endLine: 1,
152 | endColumn: 21,
153 | },
154 | ],
155 | },
156 | {
157 | code: "es\\\\lint\\\\ [eslint][ ]",
158 | errors: [
159 | {
160 | messageId: "invalidLabelRef",
161 | data: { label: "eslint" },
162 | line: 1,
163 | column: 20,
164 | endLine: 1,
165 | endColumn: 23,
166 | },
167 | ],
168 | },
169 | ],
170 | });
171 |
--------------------------------------------------------------------------------
/tests/rules/no-missing-label-refs.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for no-missing-label-refs rule.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Imports
8 | //------------------------------------------------------------------------------
9 |
10 | import rule from "../../src/rules/no-missing-label-refs.js";
11 | import markdown from "../../src/index.js";
12 | import { RuleTester } from "eslint";
13 |
14 | //------------------------------------------------------------------------------
15 | // Tests
16 | //------------------------------------------------------------------------------
17 |
18 | const ruleTester = new RuleTester({
19 | plugins: {
20 | markdown,
21 | },
22 | language: "markdown/commonmark",
23 | });
24 |
25 | ruleTester.run("no-missing-label-refs", rule, {
26 | valid: [
27 | "[*foo*]",
28 | "[foo]\n\n[foo]: http://bar.com",
29 | "[foo][foo]\n\n[foo]: http://bar.com",
30 | "[foo][foo]\n\n[ foo ]: http://bar.com",
31 | "[foo][ foo ]\n\n[ foo ]: http://bar.com",
32 | "![foo][foo]\n\n[foo]: http://bar.com/image.jpg",
33 | "[foo][]\n\n[foo]: http://bar.com/image.jpg",
34 | "![foo][]\n\n[foo]: http://bar.com/image.jpg",
35 | "[ foo ][]\n\n[foo]: http://bar.com/image.jpg",
36 | "[foo][ ]\n\n[foo]: http://bar.com/image.jpg",
37 | "[\nfoo\n][\n]\n\n[foo]: http://bar.com/image.jpg",
38 | "[]",
39 | "][]",
40 | "[][]",
41 | "[] []",
42 | "[foo",
43 | "foo]",
44 | "foo][bar]\n\n[bar]: http://bar.com",
45 | "foo][bar][baz]\n\n[baz]: http://baz.com",
46 | "[][foo]\n\n[foo]: http://foo.com",
47 | "\\[\\]",
48 | "[\\]",
49 | "\\[]",
50 | "\\[escaped\\]",
51 | "\\[escaped]",
52 | "[escaped\\]",
53 | "\\[escaped\\]\\[escaped\\]",
54 | "\\[escaped\\]\\[escaped]",
55 | "[escaped\\]\\[escaped\\]",
56 | "\\[escaped]\\[escaped]",
57 | "[escaped\\][escaped\\]",
58 | ],
59 | invalid: [
60 | {
61 | code: "[foo][bar]",
62 | errors: [
63 | {
64 | messageId: "notFound",
65 | data: { label: "bar" },
66 | line: 1,
67 | column: 7,
68 | endLine: 1,
69 | endColumn: 10,
70 | },
71 | ],
72 | },
73 | {
74 | code: "![foo][bar]",
75 | errors: [
76 | {
77 | messageId: "notFound",
78 | data: { label: "bar" },
79 | line: 1,
80 | column: 8,
81 | endLine: 1,
82 | endColumn: 11,
83 | },
84 | ],
85 | },
86 | {
87 | code: "[foo][]",
88 | errors: [
89 | {
90 | messageId: "notFound",
91 | data: { label: "foo" },
92 | line: 1,
93 | column: 2,
94 | endLine: 1,
95 | endColumn: 5,
96 | },
97 | ],
98 | },
99 | {
100 | code: "![foo][]",
101 | errors: [
102 | {
103 | messageId: "notFound",
104 | data: { label: "foo" },
105 | line: 1,
106 | column: 3,
107 | endLine: 1,
108 | endColumn: 6,
109 | },
110 | ],
111 | },
112 | {
113 | code: "[foo]",
114 | errors: [
115 | {
116 | messageId: "notFound",
117 | data: { label: "foo" },
118 | line: 1,
119 | column: 2,
120 | endLine: 1,
121 | endColumn: 5,
122 | },
123 | ],
124 | },
125 | {
126 | code: "![foo]",
127 | errors: [
128 | {
129 | messageId: "notFound",
130 | data: { label: "foo" },
131 | line: 1,
132 | column: 3,
133 | endLine: 1,
134 | endColumn: 6,
135 | },
136 | ],
137 | },
138 | {
139 | code: "[foo]\n[bar]",
140 | errors: [
141 | {
142 | messageId: "notFound",
143 | data: { label: "foo" },
144 | line: 1,
145 | column: 2,
146 | endLine: 1,
147 | endColumn: 5,
148 | },
149 | {
150 | messageId: "notFound",
151 | data: { label: "bar" },
152 | line: 2,
153 | column: 2,
154 | endLine: 2,
155 | endColumn: 5,
156 | },
157 | ],
158 | },
159 | {
160 | code: "- - - [foo]",
161 | errors: [
162 | {
163 | messageId: "notFound",
164 | data: { label: "foo" },
165 | line: 1,
166 | column: 8,
167 | endLine: 1,
168 | endColumn: 11,
169 | },
170 | ],
171 | },
172 | {
173 | code: "foo][bar]\n\n[baz]: http://baz.com",
174 | errors: [
175 | {
176 | messageId: "notFound",
177 | data: { label: "bar" },
178 | line: 1,
179 | column: 6,
180 | endLine: 1,
181 | endColumn: 9,
182 | },
183 | ],
184 | },
185 | {
186 | code: "foo][bar][baz]\n\n[bar]: http://bar.com",
187 | errors: [
188 | {
189 | messageId: "notFound",
190 | data: { label: "baz" },
191 | line: 1,
192 | column: 11,
193 | endLine: 1,
194 | endColumn: 14,
195 | },
196 | ],
197 | },
198 | {
199 | code: "[foo]\n[foo][bar]",
200 | errors: [
201 | {
202 | messageId: "notFound",
203 | data: { label: "foo" },
204 | line: 1,
205 | column: 2,
206 | endLine: 1,
207 | endColumn: 5,
208 | },
209 | {
210 | messageId: "notFound",
211 | data: { label: "bar" },
212 | line: 2,
213 | column: 7,
214 | endLine: 2,
215 | endColumn: 10,
216 | },
217 | ],
218 | },
219 | {
220 | code: "[Foo][foo]\n[Bar][]",
221 | errors: [
222 | {
223 | messageId: "notFound",
224 | data: { label: "foo" },
225 | line: 1,
226 | column: 7,
227 | endLine: 1,
228 | endColumn: 10,
229 | },
230 | {
231 | messageId: "notFound",
232 | data: { label: "Bar" },
233 | line: 2,
234 | column: 2,
235 | endLine: 2,
236 | endColumn: 5,
237 | },
238 | ],
239 | },
240 | {
241 | code: "[Foo][]\n[Bar][]",
242 | errors: [
243 | {
244 | messageId: "notFound",
245 | data: { label: "Foo" },
246 | line: 1,
247 | column: 2,
248 | endLine: 1,
249 | endColumn: 5,
250 | },
251 | {
252 | messageId: "notFound",
253 | data: { label: "Bar" },
254 | line: 2,
255 | column: 2,
256 | endLine: 2,
257 | endColumn: 5,
258 | },
259 | ],
260 | },
261 | {
262 | code: "[Foo][foo]\n[Bar][bar]\n[Hoge][]",
263 | errors: [
264 | {
265 | messageId: "notFound",
266 | data: { label: "foo" },
267 | line: 1,
268 | column: 7,
269 | endLine: 1,
270 | endColumn: 10,
271 | },
272 | {
273 | messageId: "notFound",
274 | data: { label: "bar" },
275 | line: 2,
276 | column: 7,
277 | endLine: 2,
278 | endColumn: 10,
279 | },
280 | {
281 | messageId: "notFound",
282 | data: { label: "Hoge" },
283 | line: 3,
284 | column: 2,
285 | endLine: 3,
286 | endColumn: 6,
287 | },
288 | ],
289 | },
290 | {
291 | code: "[Foo][]\n[Bar][bar]\n[Hoge][hoge]",
292 | errors: [
293 | {
294 | messageId: "notFound",
295 | data: { label: "Foo" },
296 | line: 1,
297 | column: 2,
298 | endLine: 1,
299 | endColumn: 5,
300 | },
301 | {
302 | messageId: "notFound",
303 | data: { label: "bar" },
304 | line: 2,
305 | column: 7,
306 | endLine: 2,
307 | endColumn: 10,
308 | },
309 | {
310 | messageId: "notFound",
311 | data: { label: "hoge" },
312 | line: 3,
313 | column: 8,
314 | endLine: 3,
315 | endColumn: 12,
316 | },
317 | ],
318 | },
319 | {
320 | code: "[][foo]",
321 | errors: [
322 | {
323 | messageId: "notFound",
324 | data: { label: "foo" },
325 | line: 1,
326 | column: 4,
327 | endLine: 1,
328 | endColumn: 7,
329 | },
330 | ],
331 | },
332 | {
333 | code: " foo\n [bar]",
334 | errors: [
335 | {
336 | messageId: "notFound",
337 | data: { label: "bar" },
338 | line: 2,
339 | column: 4,
340 | endLine: 2,
341 | endColumn: 7,
342 | },
343 | ],
344 | },
345 | {
346 | code: "\\[[foo]\\]",
347 | errors: [
348 | {
349 | messageId: "notFound",
350 | data: { label: "foo" },
351 | line: 1,
352 | column: 4,
353 | endLine: 1,
354 | endColumn: 7,
355 | },
356 | ],
357 | },
358 | {
359 | code: "[\\[foo\\]]",
360 | errors: [
361 | {
362 | messageId: "notFound",
363 | data: { label: "\\[foo\\]" },
364 | line: 1,
365 | column: 2,
366 | endLine: 1,
367 | endColumn: 9,
368 | },
369 | ],
370 | },
371 | {
372 | code: "[\\[\\[foo\\]\\]]",
373 | errors: [
374 | {
375 | messageId: "notFound",
376 | data: { label: "\\[\\[foo\\]\\]" },
377 | line: 1,
378 | column: 2,
379 | endLine: 1,
380 | endColumn: 13,
381 | },
382 | ],
383 | },
384 | ],
385 | });
386 |
--------------------------------------------------------------------------------
/tests/rules/require-alt-text.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for require-alt-text rule.
3 | * @author Pixel998
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Imports
8 | //------------------------------------------------------------------------------
9 |
10 | import rule from "../../src/rules/require-alt-text.js";
11 | import markdown from "../../src/index.js";
12 | import { RuleTester } from "eslint";
13 | import dedent from "dedent";
14 |
15 | //------------------------------------------------------------------------------
16 | // Tests
17 | //------------------------------------------------------------------------------
18 |
19 | const ruleTester = new RuleTester({
20 | plugins: {
21 | markdown,
22 | },
23 | language: "markdown/commonmark",
24 | });
25 |
26 | ruleTester.run("require-alt-text", rule, {
27 | valid: [
28 | "",
29 | '',
30 | dedent`
31 | ![Alternative text][notitle]
32 |
33 | [notitle]: image.jpg`,
34 | dedent`
35 | ![Alternative text][title]
36 |
37 | [title]: image.jpg "Title"`,
38 | "[](image.jpg)",
39 | ' ',
40 | ' ',
41 | ' ',
42 | " ",
43 | ' ',
44 | ' ',
45 | ' ',
46 | '
',
47 | '',
48 | ],
49 | invalid: [
50 | {
51 | code: "",
52 | errors: [
53 | {
54 | messageId: "altTextRequired",
55 | line: 1,
56 | column: 1,
57 | endLine: 1,
58 | endColumn: 16,
59 | },
60 | ],
61 | },
62 | {
63 | code: "",
64 | errors: [
65 | {
66 | messageId: "altTextRequired",
67 | line: 1,
68 | column: 1,
69 | endLine: 1,
70 | endColumn: 15,
71 | },
72 | ],
73 | },
74 | {
75 | code: '',
76 | errors: [
77 | {
78 | messageId: "altTextRequired",
79 | line: 1,
80 | column: 1,
81 | endLine: 1,
82 | endColumn: 23,
83 | },
84 | ],
85 | },
86 | {
87 | code: "Image without an alternative text  in a sentence.",
88 | errors: [
89 | {
90 | messageId: "altTextRequired",
91 | line: 1,
92 | column: 35,
93 | endLine: 1,
94 | endColumn: 49,
95 | },
96 | ],
97 | },
98 | {
99 | code: dedent`
100 | ![][notitle]
101 |
102 | [notitle]: image.jpg`,
103 | errors: [
104 | {
105 | messageId: "altTextRequired",
106 | line: 1,
107 | column: 1,
108 | endLine: 1,
109 | endColumn: 13,
110 | },
111 | ],
112 | },
113 | {
114 | code: dedent`
115 | ![][title]
116 |
117 | [title]: image.jpg "Title"`,
118 | errors: [
119 | {
120 | messageId: "altTextRequired",
121 | line: 1,
122 | column: 1,
123 | endLine: 1,
124 | endColumn: 11,
125 | },
126 | ],
127 | },
128 | {
129 | code: "[](image.jpg)",
130 | errors: [
131 | {
132 | messageId: "altTextRequired",
133 | line: 1,
134 | column: 2,
135 | endLine: 1,
136 | endColumn: 16,
137 | },
138 | ],
139 | },
140 | {
141 | code: ' ',
142 | errors: [
143 | {
144 | messageId: "altTextRequired",
145 | line: 1,
146 | column: 1,
147 | endLine: 1,
148 | endColumn: 24,
149 | },
150 | ],
151 | },
152 | {
153 | code: ' ',
154 | errors: [
155 | {
156 | messageId: "altTextRequired",
157 | line: 1,
158 | column: 1,
159 | endLine: 1,
160 | endColumn: 24,
161 | },
162 | ],
163 | },
164 | {
165 | code: ' ',
166 | errors: [
167 | {
168 | messageId: "altTextRequired",
169 | line: 1,
170 | column: 1,
171 | endLine: 1,
172 | endColumn: 32,
173 | },
174 | ],
175 | },
176 | {
177 | code: dedent`
178 |
179 |
180 |
181 | `,
182 | errors: [
183 | {
184 | messageId: "altTextRequired",
185 | line: 2,
186 | column: 1,
187 | endLine: 2,
188 | endColumn: 24,
189 | },
190 | ],
191 | },
192 | {
193 | code: dedent`
194 |
195 |
196 |
197 |
198 | `,
199 | errors: [
200 | {
201 | messageId: "altTextRequired",
202 | line: 2,
203 | column: 1,
204 | endLine: 2,
205 | endColumn: 24,
206 | },
207 | {
208 | messageId: "altTextRequired",
209 | line: 3,
210 | column: 1,
211 | endLine: 3,
212 | endColumn: 25,
213 | },
214 | ],
215 | },
216 | {
217 | code: ' ',
218 | errors: [
219 | {
220 | messageId: "altTextRequired",
221 | line: 1,
222 | column: 1,
223 | endLine: 1,
224 | endColumn: 43,
225 | },
226 | ],
227 | },
228 | {
229 | code: dedent`
230 |
232 | `,
233 | errors: [
234 | {
235 | messageId: "altTextRequired",
236 | line: 1,
237 | column: 1,
238 | endLine: 2,
239 | endColumn: 19,
240 | },
241 | ],
242 | },
243 | ],
244 | });
245 |
--------------------------------------------------------------------------------
/tests/rules/table-column-count.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for table-column-count rule.
3 | * @author Sweta Tanwar (@SwetaTanwar)
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Imports
8 | //------------------------------------------------------------------------------
9 |
10 | import rule from "../../src/rules/table-column-count.js";
11 | import markdown from "../../src/index.js";
12 | import { RuleTester } from "eslint";
13 | import dedent from "dedent";
14 |
15 | //------------------------------------------------------------------------------
16 | // Tests
17 | //------------------------------------------------------------------------------
18 |
19 | const ruleTester = new RuleTester({
20 | plugins: {
21 | markdown,
22 | },
23 | language: "markdown/gfm",
24 | });
25 |
26 | ruleTester.run("table-column-count", rule, {
27 | valid: [
28 | dedent`
29 | | Header | Header |
30 | | ------ | ------ |
31 | | Cell | Cell |
32 | | Cell | Cell |
33 | `,
34 | dedent`
35 | | Header | Header | Header |
36 | | ------ | ------ | ------ |
37 | | Cell | Cell |
38 | | Cell | |
39 | `,
40 | dedent`
41 | | A | B |
42 | |---|---|
43 | | | |
44 | | C | |
45 | `,
46 | `Just some text. | not a table |`,
47 | dedent`
48 | | Header | Header |
49 | | ------ | ------ | ----- |
50 | | Cell | Cell |
51 | `,
52 | dedent`
53 | | Header | Header |
54 | | ------ | ------ |
55 | `,
56 | dedent`
57 | Some text before.
58 |
59 | | H1 | H2 |
60 | |----|----|
61 | | D1 | D2 |
62 |
63 | Some text after.
64 | `,
65 | dedent`
66 | | Valid | Table |
67 | | ----- | ----- |
68 | | Row | Here |
69 | `,
70 | dedent`
71 | | abc | defghi |
72 | :-: | -----------:
73 | bar | baz
74 | `,
75 | dedent`
76 | | f|oo |
77 | | ------ |
78 | | b \`|\` az |
79 | | b **|** im |
80 | `,
81 | dedent`
82 | | abc | def |
83 | | --- | --- |
84 | | bar | baz |
85 | > bar
86 | `,
87 | dedent`
88 | | abc | def |
89 | | --- | --- |
90 | `,
91 | ],
92 |
93 | invalid: [
94 | {
95 | code: dedent`
96 | | Head1 | Head2 |
97 | | ----- | ----- |
98 | | R1C1 | R1C2 | R2C3 |
99 | `,
100 | errors: [
101 | {
102 | messageId: "inconsistentColumnCount",
103 | data: { actualCells: "3", expectedCells: "2" },
104 | line: 3,
105 | column: 17,
106 | endLine: 3,
107 | endColumn: 26,
108 | },
109 | ],
110 | },
111 | {
112 | code: dedent`
113 | | Head1 | Head2 |
114 | | ----- | ----- |
115 | | R1C1 | R1C2 | R2C3 | R3C4 |
116 | `,
117 | errors: [
118 | {
119 | messageId: "inconsistentColumnCount",
120 | data: { actualCells: "4", expectedCells: "2" },
121 | line: 3,
122 | column: 17,
123 | endLine: 3,
124 | endColumn: 33,
125 | },
126 | ],
127 | },
128 | {
129 | code: dedent`
130 | | A |
131 | | - |
132 | | 1 | 2 |
133 | `,
134 | errors: [
135 | {
136 | messageId: "inconsistentColumnCount",
137 | data: { actualCells: "2", expectedCells: "1" },
138 | line: 3,
139 | column: 5,
140 | endLine: 3,
141 | endColumn: 10,
142 | },
143 | ],
144 | },
145 | {
146 | code: dedent`
147 | Some introductory text.
148 |
149 | | Header1 | Header2 |
150 | | ------- | ------- |
151 | | Data1 | Data2 | Data3 |
152 | | D4 | D5 |
153 |
154 | Some concluding text.
155 | `,
156 | errors: [
157 | {
158 | messageId: "inconsistentColumnCount",
159 | data: { actualCells: "3", expectedCells: "2" },
160 | line: 5,
161 | column: 21,
162 | endLine: 5,
163 | endColumn: 30,
164 | },
165 | ],
166 | },
167 | {
168 | code: dedent`
169 | | abc | defghi |
170 | :-: | -----------:
171 | bar | baz
172 | bar | baz | bad
173 | `,
174 | errors: [
175 | {
176 | messageId: "inconsistentColumnCount",
177 | data: { actualCells: "3", expectedCells: "2" },
178 | line: 4,
179 | column: 11,
180 | endLine: 4,
181 | endColumn: 16,
182 | },
183 | ],
184 | },
185 | {
186 | code: dedent`
187 | | abc | def |
188 | | --- | --- |
189 | | bar | baz | Extra |
190 | > This is a blockquote after
191 | `,
192 | errors: [
193 | {
194 | messageId: "inconsistentColumnCount",
195 | data: { actualCells: "3", expectedCells: "2" },
196 | line: 3,
197 | column: 13,
198 | endLine: 3,
199 | endColumn: 22,
200 | },
201 | ],
202 | },
203 | {
204 | code: dedent`
205 | | abc | def |
206 | | --- | --- |
207 | | bar | baz | Extra1 |
208 | | bar | baz | Extra2 |
209 | `,
210 | errors: [
211 | {
212 | messageId: "inconsistentColumnCount",
213 | data: { actualCells: "3", expectedCells: "2" },
214 | line: 3,
215 | column: 13,
216 | endLine: 3,
217 | endColumn: 23,
218 | },
219 | {
220 | messageId: "inconsistentColumnCount",
221 | data: { actualCells: "3", expectedCells: "2" },
222 | line: 4,
223 | column: 13,
224 | endLine: 4,
225 | endColumn: 23,
226 | },
227 | ],
228 | },
229 | {
230 | code: dedent`
231 | | abc | def |
232 | | --- | --- |
233 | | bar | baz | Extra1 |
234 | | bar | baz |
235 | | bar | baz | Extra2 |
236 | `,
237 | errors: [
238 | {
239 | messageId: "inconsistentColumnCount",
240 | data: { actualCells: "3", expectedCells: "2" },
241 | line: 3,
242 | column: 13,
243 | endLine: 3,
244 | endColumn: 23,
245 | },
246 | {
247 | messageId: "inconsistentColumnCount",
248 | data: { actualCells: "3", expectedCells: "2" },
249 | line: 5,
250 | column: 13,
251 | endLine: 5,
252 | endColumn: 23,
253 | },
254 | ],
255 | },
256 | ],
257 | });
258 |
--------------------------------------------------------------------------------
/tests/types/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "emitDeclarationOnly": false,
5 | "noEmit": true,
6 | "rootDir": "../..",
7 | "strict": true
8 | },
9 | "files": [],
10 | "include": [".", "../../dist"]
11 | }
12 |
--------------------------------------------------------------------------------
/tests/types/types.test.ts:
--------------------------------------------------------------------------------
1 | import markdown, {
2 | MarkdownSourceCode,
3 | MarkdownNode,
4 | MarkdownRuleDefinition,
5 | MarkdownRuleVisitor,
6 | SourceLocation,
7 | SourceRange,
8 | type RuleModule,
9 | } from "@eslint/markdown";
10 | import { Toml } from "@eslint/markdown/types";
11 | import { ESLint, Linter } from "eslint";
12 | import type {
13 | // Nodes (abstract)
14 | Node,
15 | Parent,
16 | // Nodes
17 | Blockquote,
18 | Break,
19 | Code,
20 | Definition,
21 | Emphasis,
22 | Heading,
23 | Html,
24 | Image,
25 | ImageReference,
26 | InlineCode,
27 | Link,
28 | LinkReference,
29 | List,
30 | ListItem,
31 | Paragraph,
32 | Root,
33 | Strong,
34 | Text,
35 | ThematicBreak,
36 | // Extensions (GFM)
37 | Delete,
38 | FootnoteDefinition,
39 | FootnoteReference,
40 | Table,
41 | TableCell,
42 | TableRow,
43 | // Extensions (front matter)
44 | Yaml,
45 | } from "mdast";
46 |
47 | markdown satisfies ESLint.Plugin;
48 | markdown.meta.name satisfies string;
49 | markdown.meta.version satisfies string;
50 |
51 | // Check that the processor is defined:
52 | markdown.processors.markdown satisfies object;
53 |
54 | // Check that these languages are defined:
55 | markdown.languages.commonmark satisfies object;
56 | markdown.languages.gfm satisfies object;
57 |
58 | markdown.configs["recommended-legacy"] satisfies Linter.LegacyConfig;
59 | markdown.configs.recommended satisfies Linter.Config[];
60 | markdown.configs.processor satisfies Linter.Config[];
61 |
62 | // Check that `plugins` in the recommended config is empty:
63 | const [{ plugins: recommendedPlugins }] = markdown.configs.recommended;
64 | typeof recommendedPlugins satisfies {};
65 | ({}) satisfies typeof recommendedPlugins;
66 |
67 | // Check that `plugins` in the processor config is empty:
68 | const [{ plugins: processorPlugins }] = markdown.configs.processor;
69 | typeof processorPlugins satisfies {};
70 | ({}) satisfies typeof processorPlugins;
71 |
72 | {
73 | type RecommendedRuleName =
74 | keyof (typeof markdown.configs.recommended)[0]["rules"];
75 | type RuleName = `markdown/${keyof typeof markdown.rules}`;
76 | type AssertAllNamesIn = never;
77 |
78 | // Check that all recommended rule names match the names of existing rules in this plugin.
79 | null as AssertAllNamesIn;
80 | }
81 |
82 | (): RuleModule => ({
83 | create({ sourceCode }): MarkdownRuleVisitor {
84 | sourceCode satisfies MarkdownSourceCode;
85 | sourceCode.ast satisfies Root;
86 | sourceCode.lines satisfies string[];
87 | sourceCode.text satisfies string;
88 |
89 | function testVisitor(
90 | node: NodeType,
91 | parent?: Parent | undefined,
92 | ) {
93 | sourceCode.getLoc(node) satisfies SourceLocation;
94 | sourceCode.getRange(node) satisfies SourceRange;
95 | sourceCode.getParent(node) satisfies Node | undefined;
96 | sourceCode.getAncestors(node) satisfies Node[];
97 | sourceCode.getText(node) satisfies string;
98 | }
99 |
100 | return {
101 | // Nodes
102 | blockquote: (...args) => testVisitor(...args),
103 | "blockquote:exit": (...args) => testVisitor(...args),
104 | break: (...args) => testVisitor(...args),
105 | "break:exit": (...args) => testVisitor(...args),
106 | code: (...args) => testVisitor(...args),
107 | "code:exit": (...args) => testVisitor(...args),
108 | definition: (...args) => testVisitor(...args),
109 | "definition:exit": (...args) => testVisitor(...args),
110 | emphasis: (...args) => testVisitor(...args),
111 | "emphasis:exit": (...args) => testVisitor(...args),
112 | heading: (...args) => testVisitor(...args),
113 | "heading:exit": (...args) => testVisitor(...args),
114 | html: (...args) => testVisitor(...args),
115 | "html:exit": (...args) => testVisitor(...args),
116 | image: (...args) => testVisitor(...args),
117 | "image:exit": (...args) => testVisitor(...args),
118 | imageReference: (...args) => testVisitor(...args),
119 | "imageReference:exit": (...args) =>
120 | testVisitor(...args),
121 | inlineCode: (...args) => testVisitor(...args),
122 | "inlineCode:exit": (...args) => testVisitor(...args),
123 | link: (...args) => testVisitor (...args),
124 | "link:exit": (...args) => testVisitor (...args),
125 | linkReference: (...args) => testVisitor(...args),
126 | "linkReference:exit": (...args) =>
127 | testVisitor(...args),
128 | list: (...args) => testVisitor(...args),
129 | "list:exit": (...args) => testVisitor(...args),
130 | listItem: (...args) => testVisitor(...args),
131 | "listItem:exit": (...args) => testVisitor(...args),
132 | paragraph: (...args) => testVisitor(...args),
133 | "paragraph:exit": (...args) => testVisitor(...args),
134 | root: (...args) => testVisitor(...args),
135 | "root:exit": (...arg) => testVisitor(...arg),
136 | strong: (...args) => testVisitor(...args),
137 | "strong:exit": (...args) => testVisitor(...args),
138 | text: (...args) => testVisitor(...args),
139 | "text:exit": (...args) => testVisitor(...args),
140 | thematicBreak: (...args) => testVisitor(...args),
141 | "thematicBreak:exit": (...args) =>
142 | testVisitor(...args),
143 |
144 | // Extensions (GFM)
145 | delete: (...args) => testVisitor(...args),
146 | "delete:exit": (...args) => testVisitor(...args),
147 | footnoteDefinition: (...args) =>
148 | testVisitor(...args),
149 | "footnoteDefinition:exit": (...args) =>
150 | testVisitor(...args),
151 | footnoteReference: (...args) =>
152 | testVisitor(...args),
153 | "footnoteReference:exit": (...args) =>
154 | testVisitor(...args),
155 | table: (...args) => testVisitor(...args),
156 | "table:exit": (...args) => testVisitor(...args),
157 | tableCell: (...args) => testVisitor(...args),
158 | "tableCell:exit": (...args) => testVisitor(...args),
159 | tableRow: (...args) => testVisitor(...args),
160 | "tableRow:exit": (...args) => testVisitor(...args),
161 |
162 | // Extensions (front matter)
163 | yaml: (...args) => testVisitor(...args),
164 | "yaml:exit": (...args) => testVisitor(...args),
165 | toml: (...args) => testVisitor(...args),
166 | "toml:exit": (...args) => testVisitor(...args),
167 |
168 | // Unknown selectors allowed
169 | "heading[depth=1]"(node: MarkdownNode, parent?: ParentNode) {},
170 | "randomSelector:exit"(node: MarkdownNode, parent?: ParentNode) {},
171 | };
172 | },
173 | });
174 |
175 | // All options optional - MarkdownRuleDefinition, MarkdownRuleDefinition<{}> and RuleModule
176 | // should be the same type.
177 | (
178 | rule1: MarkdownRuleDefinition,
179 | rule2: MarkdownRuleDefinition<{}>,
180 | rule3: RuleModule,
181 | ) => {
182 | rule1 satisfies typeof rule2 satisfies typeof rule3;
183 | rule2 satisfies typeof rule1 satisfies typeof rule3;
184 | rule3 satisfies typeof rule1 satisfies typeof rule2;
185 | };
186 |
187 | // Type restrictions should be enforced
188 | (): MarkdownRuleDefinition<{
189 | RuleOptions: [string, number];
190 | MessageIds: "foo" | "bar";
191 | ExtRuleDocs: { foo: string; bar: number };
192 | }> => ({
193 | meta: {
194 | messages: {
195 | foo: "FOO",
196 |
197 | // @ts-expect-error Wrong type for message ID
198 | bar: 42,
199 | },
200 | docs: {
201 | foo: "FOO",
202 |
203 | // @ts-expect-error Wrong type for declared property
204 | bar: "BAR",
205 |
206 | // @ts-expect-error Wrong type for predefined property
207 | description: 42,
208 | },
209 | },
210 | create({ options }) {
211 | // Types for rule options
212 | options[0] satisfies string;
213 | options[1] satisfies number;
214 |
215 | return {};
216 | },
217 | });
218 |
219 | // Undeclared properties should produce an error
220 | (): MarkdownRuleDefinition<{
221 | MessageIds: "foo" | "bar";
222 | ExtRuleDocs: { foo: number; bar: string };
223 | }> => ({
224 | meta: {
225 | messages: {
226 | foo: "FOO",
227 |
228 | // Declared message ID is not required
229 | // bar: "BAR",
230 |
231 | // @ts-expect-error Undeclared message ID is not allowed
232 | baz: "BAZ",
233 | },
234 | docs: {
235 | foo: 42,
236 |
237 | // Declared property is not required
238 | // bar: "BAR",
239 |
240 | // @ts-expect-error Undeclared property key is not allowed
241 | baz: "BAZ",
242 |
243 | // Predefined property is allowed
244 | description: "Lorem ipsum",
245 | },
246 | },
247 | create() {
248 | return {};
249 | },
250 | });
251 |
--------------------------------------------------------------------------------
/tools/build-rules.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Generates the recommended configuration and import file for rules.
3 | *
4 | * Usage:
5 | * node tools/build-rules.js
6 | *
7 | * @author Nicholas C. Zakas
8 | */
9 |
10 | //-----------------------------------------------------------------------------
11 | // Imports
12 | //-----------------------------------------------------------------------------
13 |
14 | import fs from "node:fs";
15 | import path from "node:path";
16 | import { fileURLToPath, pathToFileURL } from "node:url";
17 |
18 | //-----------------------------------------------------------------------------
19 | // Main
20 | //-----------------------------------------------------------------------------
21 |
22 | const thisDir = path.dirname(fileURLToPath(import.meta.url));
23 | const rulesPath = path.resolve(thisDir, "../src/rules");
24 | const rules = fs.readdirSync(rulesPath);
25 | const recommended = [];
26 |
27 | for (const ruleId of rules) {
28 | const rulePath = path.resolve(rulesPath, ruleId);
29 | const rule = await import(pathToFileURL(rulePath));
30 |
31 | if (rule.default.meta.docs.recommended) {
32 | recommended.push(ruleId);
33 | }
34 | }
35 |
36 | const output = `const rules = /** @type {const} */ ({
37 | ${recommended.map(id => `"markdown/${id.slice(0, -3)}": "error"`).join(",\n ")}
38 | });
39 |
40 | export default rules;
41 | `;
42 |
43 | fs.mkdirSync(path.resolve(thisDir, "../src/build"), { recursive: true });
44 | fs.writeFileSync(
45 | path.resolve(thisDir, "../src/build/recommended-config.js"),
46 | output,
47 | );
48 |
49 | console.log("Recommended rules generated successfully.");
50 |
51 | const rulesOutput = `
52 | ${rules.map((id, index) => `import rule${index} from "../rules/${id}";`).join("\n")}
53 |
54 | export default {
55 | ${rules.map((id, index) => `"${id.slice(0, -3)}": rule${index},`).join("\n ")}
56 | };
57 | `.trim();
58 |
59 | fs.writeFileSync(path.resolve(thisDir, "../src/build/rules.js"), rulesOutput);
60 |
61 | console.log("Rules import file generated successfully.");
62 |
--------------------------------------------------------------------------------
/tools/commit-readme.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #------------------------------------------------------------------------------
4 | # Commits the data files if any have changed
5 | #------------------------------------------------------------------------------
6 |
7 | if [ -z "$(git status --porcelain)" ]; then
8 | echo "Data did not change."
9 | else
10 | echo "Data changed!"
11 |
12 | # commit the result
13 | git add README.md
14 | git commit -m "docs: Update README sponsors"
15 |
16 | # push back to source control
17 | git push origin HEAD
18 | fi
19 |
--------------------------------------------------------------------------------
/tools/dedupe-types.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Strips typedef aliases from the rolled-up file. This
3 | * is necessary because the TypeScript compiler throws an error when
4 | * it encounters a duplicate typedef.
5 | *
6 | * Usage:
7 | * node scripts/strip-typedefs.js filename1.js filename2.js ...
8 | *
9 | * @author Nicholas C. Zakas
10 | */
11 |
12 | //-----------------------------------------------------------------------------
13 | // Imports
14 | //-----------------------------------------------------------------------------
15 |
16 | import fs from "node:fs";
17 |
18 | //-----------------------------------------------------------------------------
19 | // Main
20 | //-----------------------------------------------------------------------------
21 |
22 | // read files from the command line
23 | const files = process.argv.slice(2);
24 |
25 | files.forEach(filePath => {
26 | const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/gu);
27 | const typedefs = new Set();
28 |
29 | const remainingLines = lines.filter(line => {
30 | if (!line.startsWith("/** @typedef {import")) {
31 | return true;
32 | }
33 |
34 | if (typedefs.has(line)) {
35 | return false;
36 | }
37 |
38 | typedefs.add(line);
39 | return true;
40 | });
41 |
42 | // replace references to ../types.ts with ./types.ts
43 | const text = remainingLines
44 | .join("\n")
45 | .replace(/\.\.\/types\.ts/gu, "./types.ts");
46 |
47 | fs.writeFileSync(filePath, text, "utf8");
48 | });
49 |
--------------------------------------------------------------------------------
/tools/update-readme.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Script to update the README with sponsors details in all packages.
3 | *
4 | * node tools/update-readme.js
5 | *
6 | * @author Milos Djermanovic
7 | */
8 |
9 | //-----------------------------------------------------------------------------
10 | // Requirements
11 | //-----------------------------------------------------------------------------
12 |
13 | import { readFileSync, writeFileSync } from "node:fs";
14 | import got from "got";
15 |
16 | //-----------------------------------------------------------------------------
17 | // Data
18 | //-----------------------------------------------------------------------------
19 |
20 | const SPONSORS_URL =
21 | "https://raw.githubusercontent.com/eslint/eslint.org/main/includes/sponsors.md";
22 |
23 | const README_FILE_PATH = "./README.md";
24 |
25 | //-----------------------------------------------------------------------------
26 | // Helpers
27 | //-----------------------------------------------------------------------------
28 |
29 | /**
30 | * Fetches the latest sponsors from the website.
31 | * @returns {Promise}} Prerendered sponsors markdown.
32 | */
33 | async function fetchSponsorsMarkdown() {
34 | return got(SPONSORS_URL).text();
35 | }
36 |
37 | //-----------------------------------------------------------------------------
38 | // Main
39 | //-----------------------------------------------------------------------------
40 |
41 | const allSponsors = await fetchSponsorsMarkdown();
42 |
43 | // read readme file
44 | const readme = readFileSync(README_FILE_PATH, "utf8");
45 |
46 | let newReadme = readme.replace(
47 | /[\w\W]*?/u,
48 | `\n\n${allSponsors}\n`,
49 | );
50 |
51 | // replace multiple consecutive blank lines with just one blank line
52 | newReadme = newReadme.replace(/(?<=^|\n)\n{2,}/gu, "\n");
53 |
54 | // output to the files
55 | writeFileSync(README_FILE_PATH, newReadme, "utf8");
56 |
--------------------------------------------------------------------------------
/tools/update-rules-docs.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Updates the rules table in README.md with rule names,
3 | * descriptions, and whether the rules are recommended or not.
4 | *
5 | * Usage:
6 | * node tools/update-rules-docs.js
7 | *
8 | * @author Francesco Trotta
9 | */
10 |
11 | //-----------------------------------------------------------------------------
12 | // Imports
13 | //-----------------------------------------------------------------------------
14 |
15 | import { fromMarkdown } from "mdast-util-from-markdown";
16 | import fs from "node:fs/promises";
17 | import path from "node:path";
18 |
19 | //-----------------------------------------------------------------------------
20 | // Type Definitions
21 | //-----------------------------------------------------------------------------
22 |
23 | /** @typedef {import("eslint").AST.Range} Range */
24 |
25 | //-----------------------------------------------------------------------------
26 | // Helpers
27 | //-----------------------------------------------------------------------------
28 |
29 | const docsFileURL = new URL("../README.md", import.meta.url);
30 | const rulesDirURL = new URL("../src/rules/", import.meta.url);
31 |
32 | /**
33 | * Formats a table row from a rule filename.
34 | * @param {string} ruleFilename The filename of the rule module without directory.
35 | * @returns {Promise} The formatted markdown text of the table row.
36 | */
37 | async function formatTableRowFromFilename(ruleFilename) {
38 | const ruleURL = new URL(ruleFilename, rulesDirURL);
39 | const { default: rule } = await import(ruleURL);
40 | const ruleName = path.parse(ruleFilename).name;
41 | const { description, recommended } = rule.meta.docs;
42 | const ruleLink = `[\`${ruleName}\`](./docs/rules/${ruleName}.md)`;
43 | const recommendedText = recommended ? "yes" : "no";
44 |
45 | return `| ${ruleLink} | ${description} | ${recommendedText} |`;
46 | }
47 |
48 | /**
49 | * Generates the markdown text for the rules table.
50 | * @returns {Promise} The formatted markdown text of the rules table.
51 | */
52 | async function createRulesTableText() {
53 | const filenames = await fs.readdir(rulesDirURL);
54 | const ruleFilenames = filenames.filter(
55 | filename => path.extname(filename) === ".js",
56 | );
57 | const text = [
58 | "| **Rule Name** | **Description** | **Recommended** |",
59 | "| :- | :- | :-: |",
60 | ...(await Promise.all(ruleFilenames.map(formatTableRowFromFilename))),
61 | ].join("\n");
62 |
63 | return text;
64 | }
65 |
66 | /**
67 | * Returns start and end offset of the rules table as indicated by "Rule Table Start" and
68 | * "Rule Table End" HTML comments in the markdown text.
69 | * @param {string} text The markdown text.
70 | * @returns {Range | null} The offset range of the rules table, or `null`.
71 | */
72 | function getRulesTableRange(text) {
73 | const tree = fromMarkdown(text);
74 | const htmlNodes = tree.children.filter(({ type }) => type === "html");
75 | const startComment = htmlNodes.find(
76 | ({ value }) => value === "",
77 | );
78 | const endComment = htmlNodes.find(
79 | ({ value }) => value === "",
80 | );
81 |
82 | return startComment && endComment
83 | ? [startComment.position.end.offset, endComment.position.start.offset]
84 | : null;
85 | }
86 |
87 | //-----------------------------------------------------------------------------
88 | // Main
89 | //-----------------------------------------------------------------------------
90 |
91 | let docsText = await fs.readFile(docsFileURL, "utf-8");
92 | const rulesTableRange = getRulesTableRange(docsText);
93 |
94 | if (!rulesTableRange) {
95 | throw Error("Rule Table Start/End comments not found, unable to update.");
96 | }
97 |
98 | const tableText = await createRulesTableText();
99 |
100 | docsText = `${docsText.slice(0, rulesTableRange[0])}\n${tableText}\n${docsText.slice(rulesTableRange[1])}`;
101 |
102 | await fs.writeFile(docsFileURL, docsText);
103 |
--------------------------------------------------------------------------------
/tsconfig.esm.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "files": ["dist/esm/index.js"]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": ["src/index.js"],
3 | "compilerOptions": {
4 | "declaration": true,
5 | "emitDeclarationOnly": true,
6 | "allowImportingTsExtensions": true,
7 | "allowJs": true,
8 | "checkJs": true,
9 | "outDir": "dist/esm",
10 | "target": "ES2022",
11 | "moduleResolution": "NodeNext",
12 | "module": "NodeNext"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------