├── .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 | ![Image](#) 25 | ``` 26 | 27 | Exmaples of correct code: 28 | 29 | ```markdown 30 | 31 | 32 | ![](https://eslint.org/image.png) 33 | 34 | ![ESLint Logo](https://eslint.org/image.png) 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 | ![](sunset.png) 23 | 24 | ![ ](sunset.png) 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 | ![A beautiful sunset](sunset.png) 41 | 42 | ![Company logo][logo] 43 | 44 | [logo]: logo.png 45 | 46 | 47 | 48 | A beautiful sunset 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 |
7 |

Hello, {name}!

8 |
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: ["![foo](bar)", "![foo](#bar)", "![foo](http://bar.com/image.png)"], 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: "![foo](#)", 66 | errors: [ 67 | { 68 | messageId: "emptyImage", 69 | line: 1, 70 | column: 1, 71 | endLine: 1, 72 | endColumn: 10, 73 | }, 74 | ], 75 | }, 76 | { 77 | code: "![foo]( )", 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 | "![Alternative text](image.jpg)", 29 | '![Alternative text](image.jpg "Title")', 30 | dedent` 31 | ![Alternative text][notitle] 32 | 33 | [notitle]: image.jpg`, 34 | dedent` 35 | ![Alternative text][title] 36 | 37 | [title]: image.jpg "Title"`, 38 | "[![Alternative text](image.jpg)](image.jpg)", 39 | 'Descriptive text', 40 | '', 41 | '', 42 | "", 43 | 'Descriptive text', 44 | '', 45 | '', 46 | '

Descriptive text

', 47 | '', 48 | ], 49 | invalid: [ 50 | { 51 | code: "![ ](image.jpg)", 52 | errors: [ 53 | { 54 | messageId: "altTextRequired", 55 | line: 1, 56 | column: 1, 57 | endLine: 1, 58 | endColumn: 16, 59 | }, 60 | ], 61 | }, 62 | { 63 | code: "![](image.jpg)", 64 | errors: [ 65 | { 66 | messageId: "altTextRequired", 67 | line: 1, 68 | column: 1, 69 | endLine: 1, 70 | endColumn: 15, 71 | }, 72 | ], 73 | }, 74 | { 75 | code: '![](image.jpg "Title")', 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 ![](image.jpg) 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)](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 | --------------------------------------------------------------------------------