diff --git a/.github/workflows/add-to-triage.yml b/.github/workflows/add-to-triage.yml index 0042cad8..fe65a960 100644 --- a/.github/workflows/add-to-triage.yml +++ b/.github/workflows/add-to-triage.yml @@ -1,18 +1,18 @@ -name: Add to Triage - -on: - issues: - types: - - opened - -jobs: - add-to-project: - name: Add issue to project - runs-on: ubuntu-latest - steps: - - uses: actions/add-to-project@v0.4.0 - with: - project-url: https://github.com/orgs/eslint/projects/3 - github-token: ${{ secrets.PROJECT_BOT_TOKEN }} - labeled: "triage:no" - label-operator: NOT +name: Add to Triage + +on: + issues: + types: + - opened + +jobs: + add-to-project: + name: Add issue to project + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@v0.4.0 + with: + project-url: https://github.com/orgs/eslint/projects/3 + github-token: ${{ secrets.PROJECT_BOT_TOKEN }} + labeled: "triage:no" + label-operator: NOT diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2545cdf9..2e615d14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,56 +1,73 @@ name: CI on: - push: - branches: [main] - pull_request: - branches: [main] + push: + branches: [main] + pull_request: + branches: [main] jobs: - lint: - name: Lint - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: lts/* - - name: Install Packages - run: npm install - env: - CI: true - - name: Lint - run: npm run lint + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install Packages + run: npm install + env: + CI: true + - name: Lint + run: npm run lint - test: - name: Test - strategy: - matrix: - os: [ubuntu-latest] - eslint: [9] - node: [22.x, 21.x, 20.x, 18.x, "18.18.0"] - include: - - os: windows-latest - eslint: 9 - node: 20 - - os: macOS-latest - eslint: 9 - node: 20 - runs-on: ${{ matrix.os }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Install Node.js ${{ matrix.node }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node }} - - name: Install Packages - run: npm install - env: - CI: true - - name: Install ESLint@${{ matrix.eslint }} - run: npm install eslint@${{ matrix.eslint }} - - name: Test - run: npm run test + format: + name: File Format + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install Packages + run: npm install + env: + CI: true + - name: Prettier Check + run: npm run fmt:check + + test: + name: Test + strategy: + matrix: + os: [ubuntu-latest] + eslint: [9] + node: [22.x, 21.x, 20.x, 18.x, "18.18.0"] + include: + - os: windows-latest + eslint: 9 + node: 20 + - os: macOS-latest + eslint: 9 + node: 20 + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install Node.js ${{ matrix.node }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - name: Install Packages + run: npm install + env: + CI: true + - name: Install ESLint@${{ matrix.eslint }} + run: npm install eslint@${{ matrix.eslint }} + - name: Test + run: npm run test diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 3eba292a..d2758744 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -1,41 +1,41 @@ on: - push: - branches: - - main + push: + branches: + - main name: release-please jobs: - release-please: - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - id-token: write - steps: - - uses: googleapis/release-please-action@v4 - id: release - - uses: actions/checkout@v4 - if: ${{ steps.release.outputs.release_created }} - - uses: actions/setup-node@v4 - with: - node-version: lts/* - registry-url: https://registry.npmjs.org - if: ${{ steps.release.outputs.release_created }} - - run: | - npm install - npm run build --if-present - npm publish --provenance - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - if: ${{ steps.release.outputs.release_created }} - - run: 'npx @humanwhocodes/tweet "eslint/markdown v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }} has been released: ${{ steps.release.outputs.html_url }}"' - if: ${{ steps.release.outputs.release_created }} - env: - TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} - TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} - TWITTER_ACCESS_TOKEN_KEY: ${{ secrets.TWITTER_ACCESS_TOKEN_KEY }} - TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} - - run: 'npx @humanwhocodes/toot "eslint/markdown v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }} has been released: ${{ steps.release.outputs.html_url }}"' - if: ${{ steps.release.outputs.release_created }} - env: - MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} - MASTODON_HOST: ${{ secrets.MASTODON_HOST }} + release-please: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + id-token: write + steps: + - uses: googleapis/release-please-action@v4 + id: release + - uses: actions/checkout@v4 + if: ${{ steps.release.outputs.release_created }} + - uses: actions/setup-node@v4 + with: + node-version: lts/* + registry-url: https://registry.npmjs.org + if: ${{ steps.release.outputs.release_created }} + - run: | + npm install + npm run build --if-present + npm publish --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + if: ${{ steps.release.outputs.release_created }} + - run: 'npx @humanwhocodes/tweet "eslint/markdown v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }} has been released: ${{ steps.release.outputs.html_url }}"' + if: ${{ steps.release.outputs.release_created }} + env: + TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} + TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} + TWITTER_ACCESS_TOKEN_KEY: ${{ secrets.TWITTER_ACCESS_TOKEN_KEY }} + TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} + - run: 'npx @humanwhocodes/toot "eslint/markdown v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }} has been released: ${{ steps.release.outputs.html_url }}"' + if: ${{ steps.release.outputs.release_created }} + env: + MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} + MASTODON_HOST: ${{ secrets.MASTODON_HOST }} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..10e4e6f2 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +dist +CHANGELOG.md +jsr.json diff --git a/README.md b/README.md index 4e08f923..1be92ebc 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,10 @@ npm install --save-dev eslint @eslint/markdown ### Configurations -| **Configuration Name** | **Description** | -|---------------|-----------------| -| `recommended` | Lints all `.md` files with the recommended rules and assumes [CommonMark](https://commonmark.org/) format. | -| `processor` | Enables extracting code blocks from all `.md` files so code blocks can be individually linted. | +| **Configuration Name** | **Description** | +| ---------------------- | ---------------------------------------------------------------------------------------------------------- | +| `recommended` | Lints all `.md` files with the recommended rules and assumes [CommonMark](https://commonmark.org/) format. | +| `processor` | Enables extracting code blocks from all `.md` files so code blocks can be individually linted. | In your `eslint.config.js` file, import `@eslint/markdown` and include the recommended config to enable the Markdown processor on all `.md` files: @@ -37,23 +37,23 @@ In your `eslint.config.js` file, import `@eslint/markdown` and include the recom import markdown from "@eslint/markdown"; export default [ - ...markdown.configs.recommended + ...markdown.configs.recommended, - // your other configs here + // your other configs here ]; ``` ### Rules -| **Rule Name** | **Description** | -|---------------|-----------------| -| [`fenced-code-language`](./docs/rules/fenced-code-language.md) | Enforce fenced code blocks to specify a language. | -| [`heading-increment`](./docs/rules/heading-increment.md) | Enforce heading levels increment by one. | +| **Rule Name** | **Description** | +| ---------------------------------------------------------------- | ------------------------------------------------- | +| [`fenced-code-language`](./docs/rules/fenced-code-language.md) | Enforce fenced code blocks to specify a language. | +| [`heading-increment`](./docs/rules/heading-increment.md) | Enforce heading levels increment by one. | | [`no-duplicate-headings`](./docs/rules/no-duplicate-headings.md) | Disallow duplicate headings in the same document. | -| [`no-empty-links`](./docs/rules/no-empty-links.md) | Disallow empty links. | -| [`no-html`](./docs/rules/no-html.md) | Enforce fenced code blocks to specify a language. | -| [`no-invalid-label-refs`](./docs/rules/no-invalid-label-refs.md) | Disallow invalid label references. | -| [`no-missing-label-refs`](./docs/rules/no-missing-label-refs.md) | Disallow missing label references. | +| [`no-empty-links`](./docs/rules/no-empty-links.md) | Disallow empty links. | +| [`no-html`](./docs/rules/no-html.md) | Enforce fenced code blocks to specify a language. | +| [`no-invalid-label-refs`](./docs/rules/no-invalid-label-refs.md) | Disallow invalid label references. | +| [`no-missing-label-refs`](./docs/rules/no-missing-label-refs.md) | Disallow missing label references. | **Note:** This plugin does not provide formatting rules. We recommend using a source code formatter such as [Prettier](https://prettier.io) for that purpose. @@ -64,25 +64,24 @@ In order to individually configure a rule in your `eslint.config.js` file, impor import markdown from "@eslint/markdown"; export default [ - { - files: ["**/*.md"], - plugins: { - markdown - }, - rules: { - "markdown/no-html": "error" - } - } + { + files: ["**/*.md"], + plugins: { + markdown, + }, + rules: { + "markdown/no-html": "error", + }, + }, ]; ``` ### Languages -| **Language Name** | **Description** | -|---------------|-----------------| -| `commonmark` | Parse using [CommonMark](https://commonmark.org) Markdown format | -| `gfm` | Parse using [GitHub-Flavored Markdown](https://github.github.com/gfm/) format | - +| **Language Name** | **Description** | +| ----------------- | ----------------------------------------------------------------------------- | +| `commonmark` | Parse using [CommonMark](https://commonmark.org) Markdown format | +| `gfm` | Parse using [GitHub-Flavored Markdown](https://github.github.com/gfm/) format | In order to individually configure a language in your `eslint.config.js` file, import `@eslint/markdown` and configure a `language`: @@ -91,24 +90,24 @@ In order to individually configure a language in your `eslint.config.js` file, i import markdown from "@eslint/markdown"; export default [ - { - files: ["**/*.md"], - plugins: { - markdown - }, - language: "markdown/gfm", - rules: { - "markdown/no-html": "error" - } - } + { + files: ["**/*.md"], + plugins: { + markdown, + }, + language: "markdown/gfm", + rules: { + "markdown/no-html": "error", + }, + }, ]; ``` ### Processors -| **Processor Name** | **Description** | -|---------------|-----------------| -| [`markdown`](./docs/processors/markdown.md) | Extract fenced code blocks from the Markdown code so they can be linted separately. | +| **Processor Name** | **Description** | +| ------------------------------------------- | ----------------------------------------------------------------------------------- | +| [`markdown`](./docs/processors/markdown.md) | Extract fenced code blocks from the Markdown code so they can be linted separately. | ## Editor Integrations diff --git a/docs/processors/markdown.md b/docs/processors/markdown.md index 3f97eb9b..e8f94a1e 100644 --- a/docs/processors/markdown.md +++ b/docs/processors/markdown.md @@ -17,7 +17,7 @@ Here is some regular Markdown text that will be ignored. /* eslint quotes: [2, "double"] */ function hello() { - console.log("Hello, world!"); + console.log("Hello, world!"); } hello(); ``` @@ -49,9 +49,9 @@ To enable the Markdown processor, use the `processor` configuration, which conta import markdown from "@eslint/markdown"; export default [ - ...markdown.configs.processor + ...markdown.configs.processor, - // your other configs here + // your other configs here ]; ``` @@ -61,48 +61,48 @@ You can manually include the Markdown processor by setting the `processor` optio Each fenced code block inside a Markdown document has a virtual filename appended to the Markdown file's path. -The virtual filename's extension will match the fenced code block's syntax tag, except for the following: +The virtual filename's extension will match the fenced code block's syntax tag, except for the following: -* `javascript` and `ecmascript` are mapped to `js` -* `typescript` is mapped to `ts` -* `markdown` is mapped to `md` +- `javascript` and `ecmascript` are mapped to `js` +- `typescript` is mapped to `ts` +- `markdown` is mapped to `md` -For example, ```` ```js ```` code blocks in `README.md` would match `README.md/*.js` and ```` ```typescript ```` in `CONTRIBUTING.md` would match `CONTRIBUTING.md/*.ts`. +For example, ` ```js ` code blocks in `README.md` would match `README.md/*.js` and ` ```typescript ` in `CONTRIBUTING.md` would match `CONTRIBUTING.md/*.ts`. You can use glob patterns for these virtual filenames to customize configuration for code blocks without affecting regular code. For more information on configuring processors, refer to the [ESLint documentation](https://eslint.org/docs/latest/use/configure/plugins#specify-a-processor). Here's an example: -```js +````js // eslint.config.js import markdown from "@eslint/markdown"; export default [ - { - // 1. Add the plugin - plugins: { - markdown - } - }, - { - // 2. Enable the Markdown processor for all .md files. - files: ["**/*.md"], - processor: "markdown/markdown" - }, - { - // 3. Optionally, customize the configuration ESLint uses for ```js - // fenced code blocks inside .md files. - files: ["**/*.md/*.js"], - // ... - rules: { - // ... - } - } - - // your other configs here + { + // 1. Add the plugin + plugins: { + markdown, + }, + }, + { + // 2. Enable the Markdown processor for all .md files. + files: ["**/*.md"], + processor: "markdown/markdown", + }, + { + // 3. Optionally, customize the configuration ESLint uses for ```js + // fenced code blocks inside .md files. + files: ["**/*.md/*.js"], + // ... + rules: { + // ... + }, + }, + + // your other configs here ]; -``` +```` ## Frequently-Disabled Rules @@ -110,40 +110,40 @@ Some rules that catch mistakes in regular code are less helpful in documentation For example, `no-undef` would flag variables that are declared outside of a code snippet because they aren't relevant to the example. The `markdown.configs.processor` config disables these rules in Markdown files: -- [`no-undef`](https://eslint.org/docs/rules/no-undef) -- [`no-unused-expressions`](https://eslint.org/docs/rules/no-unused-expressions) -- [`no-unused-vars`](https://eslint.org/docs/rules/no-unused-vars) -- [`padded-blocks`](https://eslint.org/docs/rules/padded-blocks) +- [`no-undef`](https://eslint.org/docs/rules/no-undef) +- [`no-unused-expressions`](https://eslint.org/docs/rules/no-unused-expressions) +- [`no-unused-vars`](https://eslint.org/docs/rules/no-unused-vars) +- [`padded-blocks`](https://eslint.org/docs/rules/padded-blocks) Use glob patterns to disable more rules just for Markdown code blocks: -```js +````js // / eslint.config.js import markdown from "@eslint/markdown"; export default [ - { - plugins: { - markdown - } - }, - { - files: ["**/*.md"], - processor: "markdown/markdown" - }, - { - // 1. Target ```js code blocks in .md files. - files: ["**/*.md/*.js"], - rules: { - // 2. Disable other rules. - "no-console": "off", - "import/no-unresolved": "off" - } - } - - // your other configs here + { + plugins: { + markdown, + }, + }, + { + files: ["**/*.md"], + processor: "markdown/markdown", + }, + { + // 1. Target ```js code blocks in .md files. + files: ["**/*.md/*.js"], + rules: { + // 2. Disable other rules. + "no-console": "off", + "import/no-unresolved": "off", + }, + }, + + // your other configs here ]; -``` +```` ## Additional Notes @@ -160,8 +160,8 @@ This opts into strict mode parsing without repeated `"use strict"` directives. Markdown code blocks are not real files, so ESLint's file-format rules do not apply. The `markdown.configs.processor` config disables these rules in Markdown files: -- [`eol-last`](https://eslint.org/docs/rules/eol-last): The Markdown parser trims trailing newlines from code blocks. -- [`unicode-bom`](https://eslint.org/docs/rules/unicode-bom): Markdown code blocks do not have Unicode Byte Order Marks. +- [`eol-last`](https://eslint.org/docs/rules/eol-last): The Markdown parser trims trailing newlines from code blocks. +- [`unicode-bom`](https://eslint.org/docs/rules/unicode-bom): Markdown code blocks do not have Unicode Byte Order Marks. ### Autofixing @@ -186,7 +186,7 @@ This example enables the `alert` global variable, disables the `no-alert` rule, ```js -alert('Hello, world!'); +alert("Hello, world!"); ``` ```` diff --git a/docs/rules/fenced-code-language.md b/docs/rules/fenced-code-language.md index c8ef46e1..78ab46ed 100644 --- a/docs/rules/fenced-code-language.md +++ b/docs/rules/fenced-code-language.md @@ -32,7 +32,7 @@ console.log(message); The following options are available on this rule: -* `required: Array` - when specified, fenced code blocks must use one of the languages specified in this array. +- `required: Array` - when specified, fenced code blocks must use one of the languages specified in this array. Examples of incorrect code when configured as `"fenced-code-language: ["error", { required: ["js"]}]`: @@ -49,5 +49,5 @@ If you don't mind omitting the language for fenced code blocks, you can safely d ## Prior Art -* [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) -* [MD040 fenced-code-language](https://github.com/DavidAnson/markdownlint/blob/main/doc/md040.md) +- [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) +- [MD040 fenced-code-language](https://github.com/DavidAnson/markdownlint/blob/main/doc/md040.md) diff --git a/docs/rules/heading-increment.md b/docs/rules/heading-increment.md index a41ef970..b56e1441 100644 --- a/docs/rules/heading-increment.md +++ b/docs/rules/heading-increment.md @@ -17,8 +17,7 @@ Examples of incorrect code: ### Hello world! -Goodbye World! --------------- +## Goodbye World! #EEE Goodbye World! ``` @@ -29,5 +28,5 @@ If you aren't concerned with enforcing heading levels increment by one, you can ## Prior Art -* [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) -* [MD001 - heading-increment](https://github.com/DavidAnson/markdownlint/blob/main/doc/md001.md) +- [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) +- [MD001 - heading-increment](https://github.com/DavidAnson/markdownlint/blob/main/doc/md001.md) diff --git a/docs/rules/no-duplicate-headings.md b/docs/rules/no-duplicate-headings.md index 6d0853eb..67a6ac70 100644 --- a/docs/rules/no-duplicate-headings.md +++ b/docs/rules/no-duplicate-headings.md @@ -22,8 +22,7 @@ Examples of incorrect code: ## Hello world! -Goodbye World! --------------- +## Goodbye World! # Goodbye World! ``` @@ -34,5 +33,5 @@ If you aren't concerned with autolinking heading or autogenerating a table of co ## Prior Art -* [MD024 - Multiple headers with the same content](https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md#md024---multiple-headers-with-the-same-content) -* [MD024 - no-duplicate-heading](https://github.com/DavidAnson/markdownlint/blob/main/doc/md024.md) +- [MD024 - Multiple headers with the same content](https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md#md024---multiple-headers-with-the-same-content) +- [MD024 - no-duplicate-heading](https://github.com/DavidAnson/markdownlint/blob/main/doc/md024.md) diff --git a/docs/rules/no-empty-links.md b/docs/rules/no-empty-links.md index 2736727e..d4b3aef8 100644 --- a/docs/rules/no-empty-links.md +++ b/docs/rules/no-empty-links.md @@ -24,4 +24,4 @@ If you aren't concerned with empty links, you can safely disable this rule. ## Prior Art -* [MD042 - no-empty-links](https://github.com/DavidAnson/markdownlint/blob/main/doc/md042.md) +- [MD042 - no-empty-links](https://github.com/DavidAnson/markdownlint/blob/main/doc/md042.md) diff --git a/docs/rules/no-html.md b/docs/rules/no-html.md index 76b4599c..a233e166 100644 --- a/docs/rules/no-html.md +++ b/docs/rules/no-html.md @@ -22,7 +22,7 @@ Hello world! The following options are available on this rule: -* `allowed: Array` - when specified, HTML tags are allowed only if they match one of the tags in this array.. +- `allowed: Array` - when specified, HTML tags are allowed only if they match one of the tags in this array.. Examples of incorrect code when configured as `"no-html: ["error", { allowed: ["b"]}]`: @@ -46,5 +46,5 @@ If you aren't concerned with empty links, you can safely disable this rule. ## Prior Art -* [MD033 - Inline HTML](https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md#md033---inline-html) -* [MD033 - no-inline-html](https://github.com/DavidAnson/markdownlint/blob/main/doc/md033.md) +- [MD033 - Inline HTML](https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md#md033---inline-html) +- [MD033 - no-inline-html](https://github.com/DavidAnson/markdownlint/blob/main/doc/md033.md) diff --git a/docs/rules/no-invalid-label-refs.md b/docs/rules/no-invalid-label-refs.md index 454530e7..8464b02b 100644 --- a/docs/rules/no-invalid-label-refs.md +++ b/docs/rules/no-invalid-label-refs.md @@ -14,7 +14,7 @@ CommonMark allows you to specify a label as a placeholder for a URL in both link [eslint]: https://eslint.org ``` -The shorthand form, `[label][]` does not allow any white space between the brackets, and when found, doesn't treat this as a link reference. +The shorthand form, `[label][]` does not allow any white space between the brackets, and when found, doesn't treat this as a link reference. 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. @@ -27,7 +27,7 @@ Examples of incorrect code: ```markdown [eslint][ ] -[eslint][ +[eslint] ] ``` diff --git a/docs/rules/no-missing-label-refs.md b/docs/rules/no-missing-label-refs.md index 716f2ac7..7a722ed2 100644 --- a/docs/rules/no-missing-label-refs.md +++ b/docs/rules/no-missing-label-refs.md @@ -34,4 +34,4 @@ If you aren't concerned with missing label references, you can safely disable th ## Prior Art -* [MD052 - reference-links-images](https://github.com/DavidAnson/markdownlint/blob/main/doc/md052.md) +- [MD052 - reference-links-images](https://github.com/DavidAnson/markdownlint/blob/main/doc/md052.md) diff --git a/eslint.config-content.js b/eslint.config-content.js index ed5d1420..67555d9f 100644 --- a/eslint.config-content.js +++ b/eslint.config-content.js @@ -1,9 +1,9 @@ import markdown from "./src/index.js"; export default [ - { - name: "markdown/content/ignores", - ignores: ["**/*.js", "**/.cjs", "**/.mjs"] - }, - ...markdown.configs.recommended + { + name: "markdown/content/ignores", + ignores: ["**/*.js", "**/.cjs", "**/.mjs"], + }, + ...markdown.configs.recommended, ]; diff --git a/eslint.config.js b/eslint.config.js index 021e4ecd..0679c9a8 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,71 +1,69 @@ import globals from "globals"; import eslintConfigESLint from "eslint-config-eslint"; -import eslintConfigESLintFormatting from "eslint-config-eslint/formatting"; import markdown from "./src/index.js"; export default [ - ...eslintConfigESLint, - eslintConfigESLintFormatting, - { - name: "markdown/plugins", - plugins: { - markdown - } - }, - { - name: "markdown/ignores", - ignores: [ - "**/examples", - "**/coverage", - "**/tests/fixtures", - "dist", - "src/build/" - ] - }, - { - name: "markdown/tools", - files: ["tools/**/*.js"], - rules: { - "no-console": "off" - } - }, - { - name: "markdown/tests", - files: ["tests/**/*.js"], - languageOptions: { - globals: { - ...globals.mocha - } - }, - rules: { - "no-underscore-dangle": "off" - } - }, - { - name: "markdown/code-blocks", - files: ["**/*.md"], - processor: "markdown/markdown" - }, - { - name: "markdown/code-blocks/js", - files: ["**/*.md/*.js"], - languageOptions: { - sourceType: "module", - parserOptions: { - ecmaFeatures: { - impliedStrict: true - } - } - }, - rules: { - "lines-around-comment": "off", - "n/no-missing-import": "off", - "no-var": "off", - "padding-line-between-statements": "off", - "no-console": "off", - "no-alert": "off", - "@eslint-community/eslint-comments/require-description": "off", - "jsdoc/require-jsdoc": "off" - } - } + ...eslintConfigESLint, + { + name: "markdown/plugins", + plugins: { + markdown, + }, + }, + { + name: "markdown/ignores", + ignores: [ + "**/examples", + "**/coverage", + "**/tests/fixtures", + "dist", + "src/build/", + ], + }, + { + name: "markdown/tools", + files: ["tools/**/*.js"], + rules: { + "no-console": "off", + }, + }, + { + name: "markdown/tests", + files: ["tests/**/*.js"], + languageOptions: { + globals: { + ...globals.mocha, + }, + }, + rules: { + "no-underscore-dangle": "off", + }, + }, + { + name: "markdown/code-blocks", + files: ["**/*.md"], + processor: "markdown/markdown", + }, + { + name: "markdown/code-blocks/js", + files: ["**/*.md/*.js"], + languageOptions: { + sourceType: "module", + parserOptions: { + ecmaFeatures: { + impliedStrict: true, + }, + }, + }, + rules: { + "lines-around-comment": "off", + "n/no-missing-import": "off", + "no-var": "off", + "padding-line-between-statements": "off", + "no-console": "off", + "no-alert": "off", + "@eslint-community/eslint-comments/require-description": "off", + "jsdoc/require-jsdoc": "off", + }, + }, ]; diff --git a/examples/react/README.md b/examples/react/README.md index 42adcd16..509a14ef 100644 --- a/examples/react/README.md +++ b/examples/react/README.md @@ -2,11 +2,11 @@ ```jsx function App({ name }) { - return ( -
-

Hello, {name}!

-
- ); + return ( +
+

Hello, {name}!

+
+ ); } ``` diff --git a/examples/react/eslint.config.mjs b/examples/react/eslint.config.mjs index 297f8c77..5e49ab77 100644 --- a/examples/react/eslint.config.mjs +++ b/examples/react/eslint.config.mjs @@ -1,36 +1,35 @@ - import js from "@eslint/js"; import markdown from "../../src/index.js"; import globals from "globals"; import reactPlugin from "eslint-plugin-react"; export default [ - js.configs.recommended, - ...markdown.configs.processor, - reactPlugin.configs.flat.recommended, - { - settings: { - react: { - version: "16.8.0" - } - }, - languageOptions: { - parserOptions: { - ecmaFeatures: { - jsx: true - } - }, - ecmaVersion: 2015, - sourceType: "module", - globals: globals.browser - } - }, - { - files: ["**/*.md/*.jsx"], - languageOptions: { - globals: { - React: false - } - } - } + js.configs.recommended, + ...markdown.configs.processor, + reactPlugin.configs.flat.recommended, + { + settings: { + react: { + version: "16.8.0", + }, + }, + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + ecmaVersion: 2015, + sourceType: "module", + globals: globals.browser, + }, + }, + { + files: ["**/*.md/*.jsx"], + languageOptions: { + globals: { + React: false, + }, + }, + }, ]; diff --git a/examples/typescript/README.md b/examples/typescript/README.md index 55673a5c..a4e4c2c1 100644 --- a/examples/typescript/README.md +++ b/examples/typescript/README.md @@ -4,7 +4,7 @@ The `@typescript-eslint` parser and the `recommended` config's rules will work i ```ts function hello(name: String) { - console.log(`Hello, ${name}!`); + console.log(`Hello, ${name}!`); } hello(42 as any); diff --git a/examples/typescript/eslint.config.mjs b/examples/typescript/eslint.config.mjs index c2760dff..cdd969e2 100644 --- a/examples/typescript/eslint.config.mjs +++ b/examples/typescript/eslint.config.mjs @@ -3,10 +3,10 @@ import markdown from "../../src/index.js"; import tseslint from "typescript-eslint"; export default tseslint.config( - js.configs.recommended, - ...markdown.configs.processor, - ...tseslint.configs.recommended.map(config => ({ - ...config, - files: ["**/*.ts"] - })) + js.configs.recommended, + ...markdown.configs.processor, + ...tseslint.configs.recommended.map(config => ({ + ...config, + files: ["**/*.ts"], + })), ); diff --git a/npm-prepare.cjs b/npm-prepare.cjs index 93849a97..a104c2e0 100644 --- a/npm-prepare.cjs +++ b/npm-prepare.cjs @@ -7,22 +7,27 @@ "use strict"; if (!process.env.NO_RECURSIVE_PREPARE) { - const childProcess = require("node:child_process"); - const fs = require("node:fs"); - const path = require("node:path"); + const childProcess = require("node:child_process"); + const fs = require("node:fs"); + const path = require("node:path"); - const examplesDir = path.resolve(__dirname, "examples"); - const examples = fs.readdirSync(examplesDir) - .filter(exampleDir => fs.statSync(path.join(examplesDir, exampleDir)).isDirectory()) - .filter(exampleDir => fs.existsSync(path.join(examplesDir, exampleDir, "package.json"))); + const examplesDir = path.resolve(__dirname, "examples"); + const examples = fs + .readdirSync(examplesDir) + .filter(exampleDir => + fs.statSync(path.join(examplesDir, exampleDir)).isDirectory(), + ) + .filter(exampleDir => + fs.existsSync(path.join(examplesDir, exampleDir, "package.json")), + ); - for (const example of examples) { - childProcess.execSync("npm install", { - cwd: path.resolve(examplesDir, example), - env: { - ...process.env, - NO_RECURSIVE_PREPARE: "true" - } - }); - } + for (const example of examples) { + childProcess.execSync("npm install", { + cwd: path.resolve(examplesDir, example), + env: { + ...process.env, + NO_RECURSIVE_PREPARE: "true", + }, + }); + } } diff --git a/package.json b/package.json index d1c5edba..2451df5e 100644 --- a/package.json +++ b/package.json @@ -33,9 +33,21 @@ "lint", "linter" ], + "gitHooks": { + "pre-commit": "lint-staged" + }, + "lint-staged": { + "*.js": [ + "eslint --fix", + "prettier --write" + ], + "!(*.js)": "prettier --write --ignore-unknown" + }, "scripts": { "lint": "eslint . && eslint -c eslint.config-content.js .", "lint:fix": "eslint --fix . && eslint --fix -c eslint.config-content.js .", + "fmt": "prettier --write .", + "fmt:check": "prettier --check .", "build:dedupe-types": "node tools/dedupe-types.js dist/esm/index.js", "build:rules": "node tools/build-rules.js", "build": "npm run build:rules && rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json", @@ -52,10 +64,13 @@ "eslint": "^9.8.0", "eslint-config-eslint": "^11.0.0", "globals": "^15.1.0", + "lint-staged": "^15.2.9", "mocha": "^10.6.0", + "prettier": "^3.3.3", "rollup": "^4.19.0", "rollup-plugin-copy": "^3.5.0", - "typescript": "^5.5.4" + "typescript": "^5.5.4", + "yorkie": "^2.0.0" }, "dependencies": { "mdast-util-from-markdown": "^2.0.1", diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 00000000..c334317f --- /dev/null +++ b/prettier.config.js @@ -0,0 +1,15 @@ +export default { + useTabs: true, + tabWidth: 4, + arrowParens: "avoid", + + overrides: [ + { + files: ["*.json"], + options: { + tabWidth: 2, + useTabs: false, + }, + }, + ], +}; diff --git a/release-please-config.json b/release-please-config.json index d551e111..49024064 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -4,10 +4,7 @@ "release-type": "node", "pull-request-title-pattern": "chore: release ${version} 🚀", "include-component-in-tag": false, - "extra-files": [ - "src/index.js", - "src/processor.js" - ] + "extra-files": ["src/index.js", "src/processor.js"] } } } diff --git a/rollup.config.js b/rollup.config.js index 5dfdd45c..dc4aeb94 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,19 +1,17 @@ import copy from "rollup-plugin-copy"; export default { - input: "src/index.js", - output: [ - { - file: "dist/esm/index.js", - format: "esm", - banner: '// @ts-self-types="./index.d.ts"' - } - ], - plugins: [ - copy({ - targets: [ - { src: "src/types.ts", dest: "dist/esm" } - ] - }) - ] + input: "src/index.js", + output: [ + { + file: "dist/esm/index.js", + format: "esm", + banner: '// @ts-self-types="./index.d.ts"', + }, + ], + plugins: [ + copy({ + targets: [{ src: "src/types.ts", dest: "dist/esm" }], + }), + ], }; diff --git a/src/index.js b/src/index.js index 6d188828..87c9856f 100644 --- a/src/index.js +++ b/src/index.js @@ -28,116 +28,112 @@ import rules from "./build/rules.js"; /** @type {RulesRecord} */ const processorRulesConfig = { - - // The Markdown parser automatically trims trailing - // newlines from code blocks. - "eol-last": "off", - - // In code snippets and examples, these rules are often - // counterproductive to clarity and brevity. - "no-undef": "off", - "no-unused-expressions": "off", - "no-unused-vars": "off", - "padded-blocks": "off", - - // Adding a "use strict" directive at the top of every - // code block is tedious and distracting. The config - // opts into strict mode parsing without the directive. - strict: "off", - - // The processor will not receive a Unicode Byte Order - // Mark from the Markdown parser. - "unicode-bom": "off" + // The Markdown parser automatically trims trailing + // newlines from code blocks. + "eol-last": "off", + + // In code snippets and examples, these rules are often + // counterproductive to clarity and brevity. + "no-undef": "off", + "no-unused-expressions": "off", + "no-unused-vars": "off", + "padded-blocks": "off", + + // Adding a "use strict" directive at the top of every + // code block is tedious and distracting. The config + // opts into strict mode parsing without the directive. + strict: "off", + + // The processor will not receive a Unicode Byte Order + // Mark from the Markdown parser. + "unicode-bom": "off", }; /** @type {Plugin & { languages: Record}} */ const plugin = { - meta: { - name: "@eslint/markdown", - version: "6.0.0" // x-release-please-version - }, - processors: { - markdown: processor - }, - languages: { - commonmark: new MarkdownLanguage({ mode: "commonmark" }), - gfm: new MarkdownLanguage({ mode: "gfm" }) - }, - rules, - configs: { - "recommended-legacy": { - plugins: ["markdown"], - overrides: [ - { - files: ["*.md"], - processor: "markdown/markdown" - }, - { - files: ["**/*.md/**"], - parserOptions: { - ecmaFeatures: { - - // Adding a "use strict" directive at the top of - // every code block is tedious and distracting, so - // opt into strict mode parsing without the - // directive. - impliedStrict: true - } - }, - rules: { - ...processorRulesConfig - } - } - ] - } - } + meta: { + name: "@eslint/markdown", + version: "6.0.0", // x-release-please-version + }, + processors: { + markdown: processor, + }, + languages: { + commonmark: new MarkdownLanguage({ mode: "commonmark" }), + gfm: new MarkdownLanguage({ mode: "gfm" }), + }, + rules, + configs: { + "recommended-legacy": { + plugins: ["markdown"], + overrides: [ + { + files: ["*.md"], + processor: "markdown/markdown", + }, + { + files: ["**/*.md/**"], + parserOptions: { + ecmaFeatures: { + // Adding a "use strict" directive at the top of + // every code block is tedious and distracting, so + // opt into strict mode parsing without the + // directive. + impliedStrict: true, + }, + }, + rules: { + ...processorRulesConfig, + }, + }, + ], + }, + }, }; plugin.configs.recommended = [ - - /** @type {Config & {language:string}} */ - ({ - name: "markdown/recommended", - files: ["**/*.md"], - language: "markdown/commonmark", - plugins: { - markdown: plugin - }, - rules: /** @type {RulesRecord} */ (recommendedRules) - }) + /** @type {Config & {language:string}} */ + ({ + name: "markdown/recommended", + files: ["**/*.md"], + language: "markdown/commonmark", + plugins: { + markdown: plugin, + }, + rules: /** @type {RulesRecord} */ (recommendedRules), + }), ]; plugin.configs.processor = [ - { - name: "markdown/recommended/plugin", - plugins: { - markdown: plugin - } - }, - { - name: "markdown/recommended/processor", - files: ["**/*.md"], - processor: "markdown/markdown" - }, - { - name: "markdown/recommended/code-blocks", - files: ["**/*.md/**"], - languageOptions: { - parserOptions: { - ecmaFeatures: { - - // Adding a "use strict" directive at the top of - // every code block is tedious and distracting, so - // opt into strict mode parsing without the - // directive. - impliedStrict: true - } - } - }, - rules: { - ...processorRulesConfig - } - } + { + name: "markdown/recommended/plugin", + plugins: { + markdown: plugin, + }, + }, + { + name: "markdown/recommended/processor", + files: ["**/*.md"], + processor: "markdown/markdown", + }, + { + name: "markdown/recommended/code-blocks", + files: ["**/*.md/**"], + languageOptions: { + parserOptions: { + ecmaFeatures: { + // Adding a "use strict" directive at the top of + // every code block is tedious and distracting, so + // opt into strict mode parsing without the + // directive. + impliedStrict: true, + }, + }, + }, + rules: { + ...processorRulesConfig, + }, + }, ]; export default plugin; diff --git a/src/language/markdown-language.js b/src/language/markdown-language.js index ad00f97b..d02539ce 100644 --- a/src/language/markdown-language.js +++ b/src/language/markdown-language.js @@ -35,111 +35,106 @@ import { gfm } from "micromark-extension-gfm"; * @implements {Language} */ export class MarkdownLanguage { - - /** - * The type of file to read. - * @type {"text"} - */ - fileType = "text"; - - /** - * The line number at which the parser starts counting. - * @type {0|1} - */ - lineStart = 1; - - /** - * The column number at which the parser starts counting. - * @type {0|1} - */ - columnStart = 1; - - /** - * The name of the key that holds the type of the node. - * @type {string} - */ - nodeTypeKey = "type"; - - - /** - * The Markdown parser mode. - * @type {ParserMode} - */ - #mode = "commonmark"; - - /** - * Creates a new instance. - * @param {Object} options The options to use for this instance. - * @param {ParserMode} [options.mode] The Markdown parser mode to use. - */ - constructor({ mode } = {}) { - if (mode) { - this.#mode = mode; - } - } - - /* eslint-disable no-unused-vars -- Required to complete interface. */ - /** - * Validates the language options. - * @param {Object} languageOptions The language options to validate. - * @returns {void} - * @throws {Error} When the language options are invalid. - */ - validateLanguageOptions(languageOptions) { - - // no-op - } - /* eslint-enable no-unused-vars -- Required to complete interface. */ - - /** - * Parses the given file into an AST. - * @param {File} file The virtual file to parse. - * @returns {ParseResult} The result of parsing. - */ - parse(file) { - - // Note: BOM already removed - const text = /** @type {string} */ (file.body); - - /* - * Check for parsing errors first. If there's a parsing error, nothing - * else can happen. However, a parsing error does not throw an error - * from this method - it's just considered a fatal error message, a - * problem that ESLint identified just like any other. - */ - try { - const options = this.#mode === "gfm" ? { - extensions: [gfm()], - mdastExtensions: [gfmFromMarkdown()] - } : { extensions: [] }; - const root = fromMarkdown(text, options); - - return { - ok: true, - ast: root - }; - - } catch (ex) { - - return { - ok: false, - errors: [ - ex - ] - }; - } - } - - /** - * Creates a new `JSONSourceCode` object from the given information. - * @param {File} file The virtual file to create a `JSONSourceCode` object from. - * @param {OkParseResult} parseResult The result returned from `parse()`. - * @returns {MarkdownSourceCode} The new `JSONSourceCode` object. - */ - createSourceCode(file, parseResult) { - return new MarkdownSourceCode({ - text: /** @type {string} */ (file.body), - ast: parseResult.ast - }); - } + /** + * The type of file to read. + * @type {"text"} + */ + fileType = "text"; + + /** + * The line number at which the parser starts counting. + * @type {0|1} + */ + lineStart = 1; + + /** + * The column number at which the parser starts counting. + * @type {0|1} + */ + columnStart = 1; + + /** + * The name of the key that holds the type of the node. + * @type {string} + */ + nodeTypeKey = "type"; + + /** + * The Markdown parser mode. + * @type {ParserMode} + */ + #mode = "commonmark"; + + /** + * Creates a new instance. + * @param {Object} options The options to use for this instance. + * @param {ParserMode} [options.mode] The Markdown parser mode to use. + */ + constructor({ mode } = {}) { + if (mode) { + this.#mode = mode; + } + } + + /* eslint-disable no-unused-vars -- Required to complete interface. */ + /** + * Validates the language options. + * @param {Object} languageOptions The language options to validate. + * @returns {void} + * @throws {Error} When the language options are invalid. + */ + validateLanguageOptions(languageOptions) { + // no-op + } + /* eslint-enable no-unused-vars -- Required to complete interface. */ + + /** + * Parses the given file into an AST. + * @param {File} file The virtual file to parse. + * @returns {ParseResult} The result of parsing. + */ + parse(file) { + // Note: BOM already removed + const text = /** @type {string} */ (file.body); + + /* + * Check for parsing errors first. If there's a parsing error, nothing + * else can happen. However, a parsing error does not throw an error + * from this method - it's just considered a fatal error message, a + * problem that ESLint identified just like any other. + */ + try { + const options = + this.#mode === "gfm" + ? { + extensions: [gfm()], + mdastExtensions: [gfmFromMarkdown()], + } + : { extensions: [] }; + const root = fromMarkdown(text, options); + + return { + ok: true, + ast: root, + }; + } catch (ex) { + return { + ok: false, + errors: [ex], + }; + } + } + + /** + * Creates a new `JSONSourceCode` object from the given information. + * @param {File} file The virtual file to create a `JSONSourceCode` object from. + * @param {OkParseResult} parseResult The result returned from `parse()`. + * @returns {MarkdownSourceCode} The new `JSONSourceCode` object. + */ + createSourceCode(file, parseResult) { + return new MarkdownSourceCode({ + text: /** @type {string} */ (file.body), + ast: parseResult.ast, + }); + } } diff --git a/src/language/markdown-source-code.js b/src/language/markdown-source-code.js index c85dc931..a76060fc 100644 --- a/src/language/markdown-source-code.js +++ b/src/language/markdown-source-code.js @@ -7,7 +7,6 @@ // Imports //----------------------------------------------------------------------------- - //----------------------------------------------------------------------------- // Types //----------------------------------------------------------------------------- @@ -32,52 +31,51 @@ * @implements {VisitTraversalStep} */ class MarkdownTraversalStep { - - /** - * The type of the step. - * @type {"visit"} - * @readonly - */ - type = "visit"; - - /** - * The kind of the step. Represents the same data as the `type` property - * but it's a number for performance. - * @type {1} - * @readonly - */ - kind = 1; - - /** - * The target of the step. - * @type {MarkdownNode} - */ - target; - - /** - * The phase of the step. - * @type {1|2} - */ - phase; - - /** - * The arguments of the step. - * @type {Array} - */ - args; - - /** - * Creates a new instance. - * @param {Object} options The options for the step. - * @param {MarkdownNode} options.target The target of the step. - * @param {1|2} options.phase The phase of the step. - * @param {Array} options.args The arguments of the step. - */ - constructor({ target, phase, args }) { - this.target = target; - this.phase = phase; - this.args = args; - } + /** + * The type of the step. + * @type {"visit"} + * @readonly + */ + type = "visit"; + + /** + * The kind of the step. Represents the same data as the `type` property + * but it's a number for performance. + * @type {1} + * @readonly + */ + kind = 1; + + /** + * The target of the step. + * @type {MarkdownNode} + */ + target; + + /** + * The phase of the step. + * @type {1|2} + */ + phase; + + /** + * The arguments of the step. + * @type {Array} + */ + args; + + /** + * Creates a new instance. + * @param {Object} options The options for the step. + * @param {MarkdownNode} options.target The target of the step. + * @param {1|2} options.phase The phase of the step. + * @param {Array} options.args The arguments of the step. + */ + constructor({ target, phase, args }) { + this.target = target; + this.phase = phase; + this.args = args; + } } //----------------------------------------------------------------------------- @@ -89,192 +87,191 @@ class MarkdownTraversalStep { * @implements {TextSourceCode} */ export class MarkdownSourceCode { - - /** - * Cached traversal steps. - * @type {Array|undefined} - */ - #steps; - - /** - * Cache of parent nodes. - * @type {WeakMap} - */ - #parents = new WeakMap(); - - /** - * The lines of text in the source code. - * @type {Array} - */ - #lines; - - /** - * Cache of ranges. - * @type {WeakMap} - */ - #ranges = new WeakMap(); - - /** - * The AST of the source code. - * @type {RootNode} - */ - ast; - - /** - * The text of the source code. - * @type {string} - */ - text; - - /** - * Creates a new instance. - * @param {Object} options The options for the instance. - * @param {string} options.text The source code text. - * @param {RootNode} options.ast The root AST node. - */ - constructor({ text, ast }) { - this.ast = ast; - this.text = text; - } - - /* eslint-disable class-methods-use-this -- Required to complete interface. */ - /** - * Gets the location of the node. - * @param {MarkdownNode} node The node to get the location of. - * @returns {SourceLocation} The location of the node. - */ - getLoc(node) { - return node.position; - } - - /** - * Gets the range of the node. - * @param {MarkdownNode} node The node to get the range of. - * @returns {SourceRange} The range of the node. - */ - getRange(node) { - - if (!this.#ranges.has(node)) { - this.#ranges.set(node, [node.position.start.offset, node.position.end.offset]); - } - - return this.#ranges.get(node); - } - - /* eslint-enable class-methods-use-this -- Required to complete interface. */ - - /** - * Returns the parent of the given node. - * @param {MarkdownNode} node The node to get the parent of. - * @returns {MarkdownNode|undefined} The parent of the node. - */ - getParent(node) { - return this.#parents.get(node); - } - - /** - * Gets all the ancestors of a given node - * @param {MarkdownNode} node The node - * @returns {Array} All the ancestor nodes in the AST, not including the provided node, starting - * from the root node at index 0 and going inwards to the parent node. - * @throws {TypeError} When `node` is missing. - */ - getAncestors(node) { - if (!node) { - throw new TypeError("Missing required argument: node."); - } - - const ancestorsStartingAtParent = []; - - for ( - let ancestor = this.#parents.get(node); - ancestor; - ancestor = this.#parents.get(ancestor) - ) { - ancestorsStartingAtParent.push(ancestor); - } - - return ancestorsStartingAtParent.reverse(); - } - - /** - * Gets the source code for the given node. - * @param {MarkdownNode} [node] The AST node to get the text for. - * @param {number} [beforeCount] The number of characters before the node to retrieve. - * @param {number} [afterCount] The number of characters after the node to retrieve. - * @returns {string} The text representing the AST node. - * @public - */ - getText(node, beforeCount = 0, afterCount = 0) { - if (node) { - const range = this.getRange(node); - - return this.text.slice( - Math.max(range[0] - beforeCount, 0), - range[1] + afterCount - ); - } - return this.text; - } - - /** - * Gets the entire source text split into an array of lines. - * @returns {Array} The source text as an array of lines. - * @public - */ - get lines() { - if (!this.#lines) { - this.#lines = this.text.split(/\r?\n/gu); - } - return this.#lines; - } - - /** - * Traverse the source code and return the steps that were taken. - * @returns {Iterable} The steps that were taken while traversing the source code. - */ - traverse() { - - // Because the AST doesn't mutate, we can cache the steps - if (this.#steps) { - return this.#steps.values(); - } - - const steps = (this.#steps = []); - - const visit = (node, parent) => { - - // first set the parent - this.#parents.set(node, parent); - - // then add the step - steps.push( - new MarkdownTraversalStep({ - target: node, - phase: 1, - args: [node, parent] - }) - ); - - // then visit the children - if (node.children) { - node.children.forEach(child => { - visit(child, node); - }); - } - - // then add the exit step - steps.push( - new MarkdownTraversalStep({ - target: node, - phase: 2, - args: [node, parent] - }) - ); - }; - - visit(this.ast); - - return steps.values(); - } + /** + * Cached traversal steps. + * @type {Array|undefined} + */ + #steps; + + /** + * Cache of parent nodes. + * @type {WeakMap} + */ + #parents = new WeakMap(); + + /** + * The lines of text in the source code. + * @type {Array} + */ + #lines; + + /** + * Cache of ranges. + * @type {WeakMap} + */ + #ranges = new WeakMap(); + + /** + * The AST of the source code. + * @type {RootNode} + */ + ast; + + /** + * The text of the source code. + * @type {string} + */ + text; + + /** + * Creates a new instance. + * @param {Object} options The options for the instance. + * @param {string} options.text The source code text. + * @param {RootNode} options.ast The root AST node. + */ + constructor({ text, ast }) { + this.ast = ast; + this.text = text; + } + + /* eslint-disable class-methods-use-this -- Required to complete interface. */ + /** + * Gets the location of the node. + * @param {MarkdownNode} node The node to get the location of. + * @returns {SourceLocation} The location of the node. + */ + getLoc(node) { + return node.position; + } + + /** + * Gets the range of the node. + * @param {MarkdownNode} node The node to get the range of. + * @returns {SourceRange} The range of the node. + */ + getRange(node) { + if (!this.#ranges.has(node)) { + this.#ranges.set(node, [ + node.position.start.offset, + node.position.end.offset, + ]); + } + + return this.#ranges.get(node); + } + + /* eslint-enable class-methods-use-this -- Required to complete interface. */ + + /** + * Returns the parent of the given node. + * @param {MarkdownNode} node The node to get the parent of. + * @returns {MarkdownNode|undefined} The parent of the node. + */ + getParent(node) { + return this.#parents.get(node); + } + + /** + * Gets all the ancestors of a given node + * @param {MarkdownNode} node The node + * @returns {Array} All the ancestor nodes in the AST, not including the provided node, starting + * from the root node at index 0 and going inwards to the parent node. + * @throws {TypeError} When `node` is missing. + */ + getAncestors(node) { + if (!node) { + throw new TypeError("Missing required argument: node."); + } + + const ancestorsStartingAtParent = []; + + for ( + let ancestor = this.#parents.get(node); + ancestor; + ancestor = this.#parents.get(ancestor) + ) { + ancestorsStartingAtParent.push(ancestor); + } + + return ancestorsStartingAtParent.reverse(); + } + + /** + * Gets the source code for the given node. + * @param {MarkdownNode} [node] The AST node to get the text for. + * @param {number} [beforeCount] The number of characters before the node to retrieve. + * @param {number} [afterCount] The number of characters after the node to retrieve. + * @returns {string} The text representing the AST node. + * @public + */ + getText(node, beforeCount = 0, afterCount = 0) { + if (node) { + const range = this.getRange(node); + + return this.text.slice( + Math.max(range[0] - beforeCount, 0), + range[1] + afterCount, + ); + } + return this.text; + } + + /** + * Gets the entire source text split into an array of lines. + * @returns {Array} The source text as an array of lines. + * @public + */ + get lines() { + if (!this.#lines) { + this.#lines = this.text.split(/\r?\n/gu); + } + return this.#lines; + } + + /** + * Traverse the source code and return the steps that were taken. + * @returns {Iterable} The steps that were taken while traversing the source code. + */ + traverse() { + // Because the AST doesn't mutate, we can cache the steps + if (this.#steps) { + return this.#steps.values(); + } + + const steps = (this.#steps = []); + + const visit = (node, parent) => { + // first set the parent + this.#parents.set(node, parent); + + // then add the step + steps.push( + new MarkdownTraversalStep({ + target: node, + phase: 1, + args: [node, parent], + }), + ); + + // then visit the children + if (node.children) { + node.children.forEach(child => { + visit(child, node); + }); + } + + // then add the exit step + steps.push( + new MarkdownTraversalStep({ + target: node, + phase: 2, + args: [node, parent], + }), + ); + }; + + visit(this.ast); + + return steps.values(); + } } diff --git a/src/processor.js b/src/processor.js index f55920a3..faf78cac 100644 --- a/src/processor.js +++ b/src/processor.js @@ -28,8 +28,8 @@ import { fromMarkdown } from "mdast-util-from-markdown"; //----------------------------------------------------------------------------- const UNSATISFIABLE_RULES = new Set([ - "eol-last", // The Markdown parser strips trailing newlines in code fences - "unicode-bom" // Code blocks will begin in the middle of Markdown files + "eol-last", // The Markdown parser strips trailing newlines in code fences + "unicode-bom", // Code blocks will begin in the middle of Markdown files ]); const SUPPORTS_AUTOFIX = true; @@ -45,19 +45,19 @@ const blocksCache = new Map(); * @returns {void} */ function traverse(node, callbacks) { - if (callbacks[node.type]) { - callbacks[node.type](node); - } else { - callbacks["*"](); - } - - const parent = /** @type {ParentNode} */ (node); - - if (typeof parent.children !== "undefined") { - for (let i = 0; i < parent.children.length; i++) { - traverse(parent.children[i], callbacks); - } - } + if (callbacks[node.type]) { + callbacks[node.type](node); + } else { + callbacks["*"](); + } + + const parent = /** @type {ParentNode} */ (node); + + if (typeof parent.children !== "undefined") { + for (let i = 0; i < parent.children.length; i++) { + traverse(parent.children[i], callbacks); + } + } } /** @@ -67,24 +67,24 @@ function traverse(node, callbacks) { * an empty string if the text is not an ESLint HTML comment. */ function getComment(html) { - const commentStart = ""; - const regex = /^(eslint\b|global\s)/u; + const commentStart = ""; + const regex = /^(eslint\b|global\s)/u; - if ( - html.slice(0, commentStart.length) !== commentStart || - html.slice(-commentEnd.length) !== commentEnd - ) { - return ""; - } + if ( + html.slice(0, commentStart.length) !== commentStart || + html.slice(-commentEnd.length) !== commentEnd + ) { + return ""; + } - const comment = html.slice(commentStart.length, -commentEnd.length); + const comment = html.slice(commentStart.length, -commentEnd.length); - if (!regex.test(comment.trim())) { - return ""; - } + if (!regex.test(comment.trim())) { + return ""; + } - return comment; + return comment; } // Before a code block, blockquote characters (`>`) are also considered @@ -98,7 +98,7 @@ const leadingWhitespaceRegex = /^[>\s]*/u; * @returns {number} The offset for the first column of the node's first line. */ function getBeginningOfLineOffset(node) { - return node.position.start.offset - node.position.start.column + 1; + return node.position.start.offset - node.position.start.column + 1; } /** @@ -110,9 +110,9 @@ function getBeginningOfLineOffset(node) { * fence of the code block. */ function getIndentText(text, node) { - return leadingWhitespaceRegex.exec( - text.slice(getBeginningOfLineOffset(node)) - )[0]; + return leadingWhitespaceRegex.exec( + text.slice(getBeginningOfLineOffset(node)), + )[0]; } /** @@ -148,97 +148,102 @@ function getIndentText(text, node) { * returns the corresponding location in the original Markdown source. */ function getBlockRangeMap(text, node, comments) { - - /* - * The parser sets the fenced code block's start offset to wherever content - * should normally begin (typically the first column of the line, but more - * inside a list item, for example). The code block's opening fence may be - * further indented by up to three characters. If the code block has - * additional indenting, the opening fence's first backtick may be up to - * three whitespace characters after the start offset. - */ - const startOffset = getBeginningOfLineOffset(node); - - /* - * Extract the Markdown source to determine the leading whitespace for each - * line. - */ - const code = text.slice(startOffset, node.position.end.offset); - const lines = code.split("\n"); - - /* - * The parser trims leading whitespace from each line of code within the - * fenced code block up to the opening fence's first backtick. The first - * backtick's column is the AST node's starting column plus any additional - * indentation. - */ - const baseIndent = getIndentText(text, node).length; - - /* - * Track the length of any inserted configuration comments at the beginning - * of the linted JS and start the JS offset lookup keys at this index. - */ - const commentLength = comments.reduce((len, comment) => len + comment.length + 1, 0); - - /* - * In case there are configuration comments, initialize the map so that the - * first lookup index is always 0. If there are no configuration comments, - * the lookup index will also be 0, and the lookup should always go to the - * last range that matches, skipping this initialization entry. - */ - const rangeMap = [{ - indent: baseIndent, - js: 0, - md: 0 - }]; - - // Start the JS offset after any configuration comments. - let jsOffset = commentLength; - - /* - * Start the Markdown offset at the beginning of the block's first line of - * actual code. The first line of the block is always the opening fence, so - * the code begins on the second line. - */ - let mdOffset = startOffset + lines[0].length + 1; - - /* - * For each line, determine how much leading whitespace was trimmed due to - * indentation. Increase the JS lookup offset by the length of the line - * post-trimming and the Markdown offset by the total line length. - */ - for (let i = 0; i + 1 < lines.length; i++) { - const line = lines[i + 1]; - const leadingWhitespaceLength = leadingWhitespaceRegex.exec(line)[0].length; - - // The parser trims leading whitespace up to the level of the opening - // fence, so keep any additional indentation beyond that. - const trimLength = Math.min(baseIndent, leadingWhitespaceLength); - - rangeMap.push({ - indent: trimLength, - js: jsOffset, - - // Advance `trimLength` character from the beginning of the Markdown - // line to the beginning of the equivalent JS line, then compute the - // delta. - md: mdOffset + trimLength - jsOffset - }); - - // Accumulate the current line in the offsets, and don't forget the - // newline. - mdOffset += line.length + 1; - jsOffset += line.length - trimLength + 1; - } - - return rangeMap; + /* + * The parser sets the fenced code block's start offset to wherever content + * should normally begin (typically the first column of the line, but more + * inside a list item, for example). The code block's opening fence may be + * further indented by up to three characters. If the code block has + * additional indenting, the opening fence's first backtick may be up to + * three whitespace characters after the start offset. + */ + const startOffset = getBeginningOfLineOffset(node); + + /* + * Extract the Markdown source to determine the leading whitespace for each + * line. + */ + const code = text.slice(startOffset, node.position.end.offset); + const lines = code.split("\n"); + + /* + * The parser trims leading whitespace from each line of code within the + * fenced code block up to the opening fence's first backtick. The first + * backtick's column is the AST node's starting column plus any additional + * indentation. + */ + const baseIndent = getIndentText(text, node).length; + + /* + * Track the length of any inserted configuration comments at the beginning + * of the linted JS and start the JS offset lookup keys at this index. + */ + const commentLength = comments.reduce( + (len, comment) => len + comment.length + 1, + 0, + ); + + /* + * In case there are configuration comments, initialize the map so that the + * first lookup index is always 0. If there are no configuration comments, + * the lookup index will also be 0, and the lookup should always go to the + * last range that matches, skipping this initialization entry. + */ + const rangeMap = [ + { + indent: baseIndent, + js: 0, + md: 0, + }, + ]; + + // Start the JS offset after any configuration comments. + let jsOffset = commentLength; + + /* + * Start the Markdown offset at the beginning of the block's first line of + * actual code. The first line of the block is always the opening fence, so + * the code begins on the second line. + */ + let mdOffset = startOffset + lines[0].length + 1; + + /* + * For each line, determine how much leading whitespace was trimmed due to + * indentation. Increase the JS lookup offset by the length of the line + * post-trimming and the Markdown offset by the total line length. + */ + for (let i = 0; i + 1 < lines.length; i++) { + const line = lines[i + 1]; + const leadingWhitespaceLength = + leadingWhitespaceRegex.exec(line)[0].length; + + // The parser trims leading whitespace up to the level of the opening + // fence, so keep any additional indentation beyond that. + const trimLength = Math.min(baseIndent, leadingWhitespaceLength); + + rangeMap.push({ + indent: trimLength, + js: jsOffset, + + // Advance `trimLength` character from the beginning of the Markdown + // line to the beginning of the equivalent JS line, then compute the + // delta. + md: mdOffset + trimLength - jsOffset, + }); + + // Accumulate the current line in the offsets, and don't forget the + // newline. + mdOffset += line.length + 1; + jsOffset += line.length - trimLength + 1; + } + + return rangeMap; } const languageToFileExtension = { - javascript: "js", - ecmascript: "js", - typescript: "ts", - markdown: "md" + javascript: "js", + ecmascript: "js", + typescript: "ts", + markdown: "md", }; /** @@ -248,84 +253,82 @@ const languageToFileExtension = { * @returns {Array<{ filename: string, text: string }>} Source code blocks to lint. */ function preprocess(text, filename) { - const ast = fromMarkdown(text); - const blocks = []; - - blocksCache.set(filename, blocks); - - /** - * During the depth-first traversal, keep track of any sequences of HTML - * comment nodes containing `eslint-*` or `global` comments. If a code - * block immediately follows such a sequence, insert the comments at the - * top of the code block. Any non-ESLint comment or other node type breaks - * and empties the sequence. - * @type {string[]} - */ - let htmlComments = []; - - traverse(ast, { - "*"() { - htmlComments = []; - }, - - /** - * Visit a code node. - * @param {CodeNode} node The visited node. - * @returns {void} - */ - code(node) { - if (node.lang) { - const comments = []; - - for (const comment of htmlComments) { - if (comment.trim() === "eslint-skip") { - htmlComments = []; - return; - } - - comments.push(`/*${comment}*/`); - } - - htmlComments = []; - - blocks.push({ - ...node, - baseIndentText: getIndentText(text, node), - comments, - rangeMap: getBlockRangeMap(text, node, comments) - }); - } - }, - - /** - * Visit an HTML node. - * @param {HtmlNode} node The visited node. - * @returns {void} - */ - html(node) { - const comment = getComment(node.value); - - if (comment) { - htmlComments.push(comment); - } else { - htmlComments = []; - } - } - }); - - return blocks.map((block, index) => { - const [language] = block.lang.trim().split(" "); - const fileExtension = Object.hasOwn(languageToFileExtension, language) ? languageToFileExtension[language] : language; - - return { - filename: `${index}.${fileExtension}`, - text: [ - ...block.comments, - block.value, - "" - ].join("\n") - }; - }); + const ast = fromMarkdown(text); + const blocks = []; + + blocksCache.set(filename, blocks); + + /** + * During the depth-first traversal, keep track of any sequences of HTML + * comment nodes containing `eslint-*` or `global` comments. If a code + * block immediately follows such a sequence, insert the comments at the + * top of the code block. Any non-ESLint comment or other node type breaks + * and empties the sequence. + * @type {string[]} + */ + let htmlComments = []; + + traverse(ast, { + "*"() { + htmlComments = []; + }, + + /** + * Visit a code node. + * @param {CodeNode} node The visited node. + * @returns {void} + */ + code(node) { + if (node.lang) { + const comments = []; + + for (const comment of htmlComments) { + if (comment.trim() === "eslint-skip") { + htmlComments = []; + return; + } + + comments.push(`/*${comment}*/`); + } + + htmlComments = []; + + blocks.push({ + ...node, + baseIndentText: getIndentText(text, node), + comments, + rangeMap: getBlockRangeMap(text, node, comments), + }); + } + }, + + /** + * Visit an HTML node. + * @param {HtmlNode} node The visited node. + * @returns {void} + */ + html(node) { + const comment = getComment(node.value); + + if (comment) { + htmlComments.push(comment); + } else { + htmlComments = []; + } + }, + }); + + return blocks.map((block, index) => { + const [language] = block.lang.trim().split(" "); + const fileExtension = Object.hasOwn(languageToFileExtension, language) + ? languageToFileExtension[language] + : language; + + return { + filename: `${index}.${fileExtension}`, + text: [...block.comments, block.value, ""].join("\n"), + }; + }); } /** @@ -334,63 +337,73 @@ function preprocess(text, filename) { * @returns {(message: Message) => Message} A function that adjusts messages in a code block. */ function adjustBlock(block) { - const leadingCommentLines = block.comments.reduce((count, comment) => count + comment.split("\n").length, 0); - - const blockStart = block.position.start.line; - - /** - * Adjusts ESLint messages to point to the correct location in the Markdown. - * @param {Message} message A message from ESLint. - * @returns {Message} The same message, but adjusted to the correct location. - */ - return function adjustMessage(message) { - if (!Number.isInteger(message.line)) { - return { - ...message, - line: blockStart, - column: block.position.start.column - }; - } - - const lineInCode = message.line - leadingCommentLines; - - if (lineInCode < 1 || lineInCode >= block.rangeMap.length) { - return null; - } - - const out = { - line: lineInCode + blockStart, - column: message.column + block.rangeMap[lineInCode].indent - }; - - if (Number.isInteger(message.endLine)) { - out.endLine = message.endLine - leadingCommentLines + blockStart; - } - - const adjustedFix = {}; - - if (message.fix) { - adjustedFix.fix = { - range: /** @type {Range} */ (message.fix.range.map(range => { - - // Advance through the block's range map to find the last - // matching range by finding the first range too far and - // then going back one. - let i = 1; - - while (i < block.rangeMap.length && block.rangeMap[i].js <= range) { - i++; - } - - // Apply the mapping delta for this range. - return range + block.rangeMap[i - 1].md; - })), - text: message.fix.text.replace(/\n/gu, `\n${block.baseIndentText}`) - }; - } - - return { ...message, ...out, ...adjustedFix }; - }; + const leadingCommentLines = block.comments.reduce( + (count, comment) => count + comment.split("\n").length, + 0, + ); + + const blockStart = block.position.start.line; + + /** + * Adjusts ESLint messages to point to the correct location in the Markdown. + * @param {Message} message A message from ESLint. + * @returns {Message} The same message, but adjusted to the correct location. + */ + return function adjustMessage(message) { + if (!Number.isInteger(message.line)) { + return { + ...message, + line: blockStart, + column: block.position.start.column, + }; + } + + const lineInCode = message.line - leadingCommentLines; + + if (lineInCode < 1 || lineInCode >= block.rangeMap.length) { + return null; + } + + const out = { + line: lineInCode + blockStart, + column: message.column + block.rangeMap[lineInCode].indent, + }; + + if (Number.isInteger(message.endLine)) { + out.endLine = message.endLine - leadingCommentLines + blockStart; + } + + const adjustedFix = {}; + + if (message.fix) { + adjustedFix.fix = { + range: /** @type {Range} */ ( + message.fix.range.map(range => { + // Advance through the block's range map to find the last + // matching range by finding the first range too far and + // then going back one. + let i = 1; + + while ( + i < block.rangeMap.length && + block.rangeMap[i].js <= range + ) { + i++; + } + + // Apply the mapping delta for this range. + return range + block.rangeMap[i - 1].md; + }) + ), + text: message.fix.text.replace( + /\n/gu, + `\n${block.baseIndentText}`, + ), + }; + } + + return { ...message, ...out, ...adjustedFix }; + }; } /** @@ -399,7 +412,7 @@ function adjustBlock(block) { * @returns {boolean} True if the message should be included in output. */ function excludeUnsatisfiableRules(message) { - return message && !UNSATISFIABLE_RULES.has(message.ruleId); + return message && !UNSATISFIABLE_RULES.has(message.ruleId); } /** @@ -410,23 +423,23 @@ function excludeUnsatisfiableRules(message) { * @returns {Message[]} A flattened array of messages with mapped locations. */ function postprocess(messages, filename) { - const blocks = blocksCache.get(filename); + const blocks = blocksCache.get(filename); - blocksCache.delete(filename); + blocksCache.delete(filename); - return messages.flatMap((group, i) => { - const adjust = adjustBlock(blocks[i]); + return messages.flatMap((group, i) => { + const adjust = adjustBlock(blocks[i]); - return group.map(adjust).filter(excludeUnsatisfiableRules); - }); + return group.map(adjust).filter(excludeUnsatisfiableRules); + }); } export const processor = { - meta: { - name: "@eslint/markdown/markdown", - version: "6.0.0" // x-release-please-version - }, - preprocess, - postprocess, - supportsAutofix: SUPPORTS_AUTOFIX + meta: { + name: "@eslint/markdown/markdown", + version: "6.0.0", // x-release-please-version + }, + preprocess, + postprocess, + supportsAutofix: SUPPORTS_AUTOFIX, }; diff --git a/src/rules/fenced-code-language.js b/src/rules/fenced-code-language.js index ccc37d39..113b263f 100644 --- a/src/rules/fenced-code-language.js +++ b/src/rules/fenced-code-language.js @@ -15,71 +15,67 @@ /** @type {RuleModule} */ export default { - meta: { - type: "problem", - - docs: { - recommended: true, - description: "Require languages for fenced code blocks." - }, - - messages: { - missingLanguage: "Missing code block language.", - disallowedLanguage: 'Code block language "{{lang}}" is not allowed.' - }, - - schema: [ - { - type: "object", - properties: { - required: { - type: "array", - items: { - type: "string" - }, - uniqueItems: true - } - }, - additionalProperties: false - } - ] - }, - - create(context) { - - const required = new Set(context.options[0]?.required); - const { sourceCode } = context; - - return { - - code(node) { - - if (!node.lang) { - - // only check fenced code blocks - if (sourceCode.text[node.position.start.offset] !== "`") { - return; - } - - context.report({ - loc: node.position, - messageId: "missingLanguage" - }); - - return; - } - - if (required.size && !required.has(node.lang)) { - context.report({ - loc: node.position, - messageId: "disallowedLanguage", - data: { - lang: node.lang - } - }); - } - - } - }; - } + meta: { + type: "problem", + + docs: { + recommended: true, + description: "Require languages for fenced code blocks.", + }, + + messages: { + missingLanguage: "Missing code block language.", + disallowedLanguage: + 'Code block language "{{lang}}" is not allowed.', + }, + + schema: [ + { + type: "object", + properties: { + required: { + type: "array", + items: { + type: "string", + }, + uniqueItems: true, + }, + }, + additionalProperties: false, + }, + ], + }, + + create(context) { + const required = new Set(context.options[0]?.required); + const { sourceCode } = context; + + return { + code(node) { + if (!node.lang) { + // only check fenced code blocks + if (sourceCode.text[node.position.start.offset] !== "`") { + return; + } + + context.report({ + loc: node.position, + messageId: "missingLanguage", + }); + + return; + } + + if (required.size && !required.has(node.lang)) { + context.report({ + loc: node.position, + messageId: "disallowedLanguage", + data: { + lang: node.lang, + }, + }); + } + }, + }; + }, }; diff --git a/src/rules/heading-increment.js b/src/rules/heading-increment.js index 15f213f4..e1382ab3 100644 --- a/src/rules/heading-increment.js +++ b/src/rules/heading-increment.js @@ -15,38 +15,38 @@ /** @type {RuleModule} */ export default { - meta: { - type: "problem", - - docs: { - recommended: true, - description: "Enforce heading levels increment by one." - }, - - messages: { - skippedHeading: "Heading level skipped from {{fromLevel}} to {{toLevel}}." - } - }, - - create(context) { - let lastHeadingDepth = 0; - - return { - heading(node) { - - if (lastHeadingDepth > 0 && node.depth > lastHeadingDepth + 1) { - context.report({ - loc: node.position, - messageId: "skippedHeading", - data: { - fromLevel: lastHeadingDepth.toString(), - toLevel: node.depth - } - }); - } - - lastHeadingDepth = node.depth; - } - }; - } + meta: { + type: "problem", + + docs: { + recommended: true, + description: "Enforce heading levels increment by one.", + }, + + messages: { + skippedHeading: + "Heading level skipped from {{fromLevel}} to {{toLevel}}.", + }, + }, + + create(context) { + let lastHeadingDepth = 0; + + return { + heading(node) { + if (lastHeadingDepth > 0 && node.depth > lastHeadingDepth + 1) { + context.report({ + loc: node.position, + messageId: "skippedHeading", + data: { + fromLevel: lastHeadingDepth.toString(), + toLevel: node.depth, + }, + }); + } + + lastHeadingDepth = node.depth; + }, + }; + }, }; diff --git a/src/rules/no-duplicate-headings.js b/src/rules/no-duplicate-headings.js index c0230a4e..ead80413 100644 --- a/src/rules/no-duplicate-headings.js +++ b/src/rules/no-duplicate-headings.js @@ -15,56 +15,57 @@ /** @type {RuleModule} */ export default { - meta: { - type: "problem", + meta: { + type: "problem", - docs: { - description: "Disallow duplicate headings in the same document." - }, + docs: { + description: "Disallow duplicate headings in the same document.", + }, - messages: { - duplicateHeading: 'Duplicate heading "{{text}}" found.' - } - }, + messages: { + duplicateHeading: 'Duplicate heading "{{text}}" found.', + }, + }, - create(context) { - const headings = new Set(); - const { sourceCode } = context; + create(context) { + const headings = new Set(); + const { sourceCode } = context; - return { - heading(node) { + return { + heading(node) { + /* + * There are two types of headings in markdown: + * - ATX headings, which start with one or more # characters + * - Setext headings, which are underlined with = or - + * Setext headings are identified by being on two lines instead of one, + * with the second line containing only = or - characters. In order to + * get the correct heading text, we need to determine which type of + * heading we're dealing with. + */ + const isSetext = + node.position.start.line !== node.position.end.line; - /* - * There are two types of headings in markdown: - * - ATX headings, which start with one or more # characters - * - Setext headings, which are underlined with = or - - * Setext headings are identified by being on two lines instead of one, - * with the second line containing only = or - characters. In order to - * get the correct heading text, we need to determine which type of - * heading we're dealing with. - */ - const isSetext = node.position.start.line !== node.position.end.line; + const text = isSetext + ? // get only the text from the first line + sourceCode.lines[node.position.start.line - 1].trim() + : // get the text without the leading # characters + sourceCode + .getText(node) + .slice(node.depth + 1) + .trim(); - const text = isSetext + if (headings.has(text)) { + context.report({ + loc: node.position, + messageId: "duplicateHeading", + data: { + text, + }, + }); + } - // get only the text from the first line - ? sourceCode.lines[node.position.start.line - 1].trim() - - // get the text without the leading # characters - : sourceCode.getText(node).slice(node.depth + 1).trim(); - - if (headings.has(text)) { - context.report({ - loc: node.position, - messageId: "duplicateHeading", - data: { - text - } - }); - } - - headings.add(text); - } - }; - } + headings.add(text); + }, + }; + }, }; diff --git a/src/rules/no-empty-links.js b/src/rules/no-empty-links.js index 2730ad74..26aeeb04 100644 --- a/src/rules/no-empty-links.js +++ b/src/rules/no-empty-links.js @@ -14,32 +14,29 @@ /** @type {RuleModule} */ export default { - meta: { - type: "problem", - - docs: { - recommended: true, - description: "Disallow empty links." - }, - - messages: { - emptyLink: "Unexpected empty link found." - } - }, - - create(context) { - - return { - link(node) { - - if (!node.url || node.url === "#") { - context.report({ - loc: node.position, - messageId: "emptyLink" - }); - } - - } - }; - } + meta: { + type: "problem", + + docs: { + recommended: true, + description: "Disallow empty links.", + }, + + messages: { + emptyLink: "Unexpected empty link found.", + }, + }, + + create(context) { + return { + link(node) { + if (!node.url || node.url === "#") { + context.report({ + loc: node.position, + messageId: "emptyLink", + }); + } + }, + }; + }, }; diff --git a/src/rules/no-html.js b/src/rules/no-html.js index f6f0c333..9a858da8 100644 --- a/src/rules/no-html.js +++ b/src/rules/no-html.js @@ -15,64 +15,63 @@ /** @type {RuleModule} */ export default { - meta: { - type: "problem", + meta: { + type: "problem", - docs: { - description: "Disallow HTML tags." - }, + docs: { + description: "Disallow HTML tags.", + }, - messages: { - disallowedElement: 'HTML element "{{name}}" is not allowed.' - }, + messages: { + disallowedElement: 'HTML element "{{name}}" is not allowed.', + }, - schema: [ - { - type: "object", - properties: { - allowed: { - type: "array", - items: { - type: "string" - }, - uniqueItems: true - } - }, - additionalProperties: false - } - ] - }, + schema: [ + { + type: "object", + properties: { + allowed: { + type: "array", + items: { + type: "string", + }, + uniqueItems: true, + }, + }, + additionalProperties: false, + }, + ], + }, - create(context) { + create(context) { + const allowed = new Set(context.options[0]?.allowed); - const allowed = new Set(context.options[0]?.allowed); + return { + html(node) { + // don't care about closing tags + if (node.value.startsWith("} The missing references. */ function findInvalidLabelReferences(node, docText) { - - const invalid = []; - let startIndex = 0; - const offset = node.position.start.offset; - const nodeStartLine = node.position.start.line; - const nodeStartColumn = node.position.start.column; - - /* - * This loop works by searching the string inside the node for the next - * label reference. If it finds one, it checks to see if there is any - * white space between the [ and ]. If there is, it reports an error. - * It then moves the start index to the end of the label reference and - * continues searching the text until the end of the text is found. - */ - while (startIndex < node.value.length) { - - const value = node.value.slice(startIndex); - const match = value.match(labelPattern); - - if (!match) { - break; - } - - if (!illegalShorthandTailPattern.test(match[0])) { - startIndex += match.index + match[0].length; - continue; - } - - /* - * Calculate the match index relative to just the node and - * to the entire document text. - */ - const nodeMatchIndex = startIndex + match.index; - const docMatchIndex = offset + nodeMatchIndex; - - /* - * Search the entire document text to find the preceding open bracket. - */ - const lastOpenBracketIndex = docText.lastIndexOf("[", docMatchIndex); - - if (lastOpenBracketIndex === -1) { - startIndex += match.index + match[0].length; - continue; - } - - /* - * Note: `label` can contain leading and trailing newlines, so we need to - * take that into account when calculating the line and column offsets. - */ - const label = docText.slice(lastOpenBracketIndex, docMatchIndex + match[0].length).match(/!?\[([^\]]+)\]/u)[1]; - - // find location of [ in the document text - const { - lineOffset: startLineOffset, - columnOffset: startColumnOffset - } = findOffsets(node.value, nodeMatchIndex + 1); - - // find location of [ in the document text - const { - lineOffset: endLineOffset, - columnOffset: endColumnOffset - } = findOffsets(node.value, nodeMatchIndex + match[0].length); - - const startLine = nodeStartLine + startLineOffset; - const startColumn = nodeStartColumn + startColumnOffset; - const endLine = nodeStartLine + endLineOffset; - const endColumn = (endLine === startLine ? nodeStartColumn : 0) + endColumnOffset; - - invalid.push({ - label: label.trim(), - position: { - start: { - line: startLine, - column: startColumn - }, - end: { - line: endLine, - column: endColumn - } - } - }); - - startIndex += match.index + match[0].length; - } - - return invalid; - + const invalid = []; + let startIndex = 0; + const offset = node.position.start.offset; + const nodeStartLine = node.position.start.line; + const nodeStartColumn = node.position.start.column; + + /* + * This loop works by searching the string inside the node for the next + * label reference. If it finds one, it checks to see if there is any + * white space between the [ and ]. If there is, it reports an error. + * It then moves the start index to the end of the label reference and + * continues searching the text until the end of the text is found. + */ + while (startIndex < node.value.length) { + const value = node.value.slice(startIndex); + const match = value.match(labelPattern); + + if (!match) { + break; + } + + if (!illegalShorthandTailPattern.test(match[0])) { + startIndex += match.index + match[0].length; + continue; + } + + /* + * Calculate the match index relative to just the node and + * to the entire document text. + */ + const nodeMatchIndex = startIndex + match.index; + const docMatchIndex = offset + nodeMatchIndex; + + /* + * Search the entire document text to find the preceding open bracket. + */ + const lastOpenBracketIndex = docText.lastIndexOf("[", docMatchIndex); + + if (lastOpenBracketIndex === -1) { + startIndex += match.index + match[0].length; + continue; + } + + /* + * Note: `label` can contain leading and trailing newlines, so we need to + * take that into account when calculating the line and column offsets. + */ + const label = docText + .slice(lastOpenBracketIndex, docMatchIndex + match[0].length) + .match(/!?\[([^\]]+)\]/u)[1]; + + // find location of [ in the document text + const { lineOffset: startLineOffset, columnOffset: startColumnOffset } = + findOffsets(node.value, nodeMatchIndex + 1); + + // find location of [ in the document text + const { lineOffset: endLineOffset, columnOffset: endColumnOffset } = + findOffsets(node.value, nodeMatchIndex + match[0].length); + + const startLine = nodeStartLine + startLineOffset; + const startColumn = nodeStartColumn + startColumnOffset; + const endLine = nodeStartLine + endLineOffset; + const endColumn = + (endLine === startLine ? nodeStartColumn : 0) + endColumnOffset; + + invalid.push({ + label: label.trim(), + position: { + start: { + line: startLine, + column: startColumn, + }, + end: { + line: endLine, + column: endColumn, + }, + }, + }); + + startIndex += match.index + match[0].length; + } + + return invalid; } //----------------------------------------------------------------------------- @@ -127,39 +122,40 @@ function findInvalidLabelReferences(node, docText) { /** @type {RuleModule} */ export default { - meta: { - type: "problem", - - docs: { - recommended: true, - description: "Disallow invalid label references." - }, - - messages: { - invalidLabelRef: "Label reference '{{label}}' is invalid due to white space between [ and ]." - } - }, - - create(context) { - - const { sourceCode } = context; - - return { - - text(node) { - const invalidReferences = findInvalidLabelReferences(node, sourceCode.text); - - for (const invalidReference of invalidReferences) { - context.report({ - loc: invalidReference.position, - messageId: "invalidLabelRef", - data: { - label: invalidReference.label - } - }); - } - } - - }; - } + meta: { + type: "problem", + + docs: { + recommended: true, + description: "Disallow invalid label references.", + }, + + messages: { + invalidLabelRef: + "Label reference '{{label}}' is invalid due to white space between [ and ].", + }, + }, + + create(context) { + const { sourceCode } = context; + + return { + text(node) { + const invalidReferences = findInvalidLabelReferences( + node, + sourceCode.text, + ); + + for (const invalidReference of invalidReferences) { + context.report({ + loc: invalidReference.position, + messageId: "invalidLabelRef", + data: { + label: invalidReference.label, + }, + }); + } + }, + }; + }, }; diff --git a/src/rules/no-missing-label-refs.js b/src/rules/no-missing-label-refs.js index d899ca0a..fbc66053 100644 --- a/src/rules/no-missing-label-refs.js +++ b/src/rules/no-missing-label-refs.js @@ -22,15 +22,14 @@ import { findOffsets, illegalShorthandTailPattern } from "../util.js"; //----------------------------------------------------------------------------- const labelPatterns = [ + // [foo][bar] + /\]\[([^\]]+)\]/u, - // [foo][bar] - /\]\[([^\]]+)\]/u, + // [foo][] + /(\]\[\])/u, - // [foo][] - /(\]\[\])/u, - - // [foo] - /\[([^\]]+)\]/u + // [foo] + /\[([^\]]+)\]/u, ]; const shorthandTailPattern = /\]\[\]$/u; @@ -42,94 +41,89 @@ const shorthandTailPattern = /\]\[\]$/u; * @returns {Array<{label:string,position:Position}>} The missing references. */ function findMissingReferences(node, docText) { - - const missing = []; - let startIndex = 0; - const offset = node.position.start.offset; - const nodeStartLine = node.position.start.line; - const nodeStartColumn = node.position.start.column; - - /* - * This loop works by searching the string inside the node for the next - * label reference. If there is, it reports an error. - * It then moves the start index to the end of the label reference and - * continues searching the text until the end of the text is found. - */ - while (startIndex < node.value.length) { - - const value = node.value.slice(startIndex); - - const match = labelPatterns.reduce((previous, pattern) => { - if (previous) { - return previous; - } - - return value.match(pattern); - }, null); - - // check for array instead of null to appease TypeScript - if (!Array.isArray(match)) { - break; - } - - // skip illegal shorthand tail -- handled by no-invalid-label-refs - if (illegalShorthandTailPattern.test(match[0])) { - startIndex += match.index + match[0].length; - continue; - } - - - // Calculate the match index relative to just the node. - let columnStart = startIndex + match.index; - let label = match[1]; - - // need to look backward to get the label - if (shorthandTailPattern.test(match[0])) { - - // adding 1 to the index just in case we're in a ![] and need to skip the !. - const startFrom = offset + startIndex + 1; - const lastOpenBracket = docText.lastIndexOf("[", startFrom); - - if (lastOpenBracket === -1) { - startIndex += match.index + match[0].length; - continue; - } - - label = docText.slice(lastOpenBracket, match.index + match[0].length).match(/!?\[([^\]]+)\]/u)?.[1]; - columnStart -= label.length; - } else if (match[0].startsWith("]")) { - columnStart += 2; - } else { - columnStart += 1; - } - - const { - lineOffset: startLineOffset, - columnOffset: startColumnOffset - } = findOffsets(node.value, columnStart); - - const startLine = nodeStartLine + startLineOffset; - const startColumn = nodeStartColumn + startColumnOffset; - - missing.push({ - label: label.trim(), - position: { - start: { - line: startLine, - column: startColumn - }, - end: { - line: startLine, - column: startColumn + label.length - } - } - }); - - startIndex += match.index + match[0].length; - } - - return missing; - + const missing = []; + let startIndex = 0; + const offset = node.position.start.offset; + const nodeStartLine = node.position.start.line; + const nodeStartColumn = node.position.start.column; + + /* + * This loop works by searching the string inside the node for the next + * label reference. If there is, it reports an error. + * It then moves the start index to the end of the label reference and + * continues searching the text until the end of the text is found. + */ + while (startIndex < node.value.length) { + const value = node.value.slice(startIndex); + + const match = labelPatterns.reduce((previous, pattern) => { + if (previous) { + return previous; + } + + return value.match(pattern); + }, null); + + // check for array instead of null to appease TypeScript + if (!Array.isArray(match)) { + break; + } + + // skip illegal shorthand tail -- handled by no-invalid-label-refs + if (illegalShorthandTailPattern.test(match[0])) { + startIndex += match.index + match[0].length; + continue; + } + + // Calculate the match index relative to just the node. + let columnStart = startIndex + match.index; + let label = match[1]; + + // need to look backward to get the label + if (shorthandTailPattern.test(match[0])) { + // adding 1 to the index just in case we're in a ![] and need to skip the !. + const startFrom = offset + startIndex + 1; + const lastOpenBracket = docText.lastIndexOf("[", startFrom); + + if (lastOpenBracket === -1) { + startIndex += match.index + match[0].length; + continue; + } + + label = docText + .slice(lastOpenBracket, match.index + match[0].length) + .match(/!?\[([^\]]+)\]/u)?.[1]; + columnStart -= label.length; + } else if (match[0].startsWith("]")) { + columnStart += 2; + } else { + columnStart += 1; + } + + const { lineOffset: startLineOffset, columnOffset: startColumnOffset } = + findOffsets(node.value, columnStart); + + const startLine = nodeStartLine + startLineOffset; + const startColumn = nodeStartColumn + startColumnOffset; + + missing.push({ + label: label.trim(), + position: { + start: { + line: startLine, + column: startColumn, + }, + end: { + line: startLine, + column: startColumn + label.length, + }, + }, + }); + + startIndex += match.index + match[0].length; + } + + return missing; } //----------------------------------------------------------------------------- @@ -138,55 +132,53 @@ function findMissingReferences(node, docText) { /** @type {RuleModule} */ export default { - meta: { - type: "problem", - - docs: { - recommended: true, - description: "Disallow missing label references." - }, - - messages: { - notFound: "Label reference '{{label}}' not found." - } - }, - - create(context) { - - const { sourceCode } = context; - let allMissingReferences = []; - - return { - - "root:exit"() { - - for (const missingReference of allMissingReferences) { - context.report({ - loc: missingReference.position, - messageId: "notFound", - data: { - label: missingReference.label - } - }); - } - - }, - - text(node) { - allMissingReferences.push(...findMissingReferences(node, sourceCode.text)); - }, - - definition(node) { - - /* - * Sometimes a poorly-formatted link will end up a text node instead of a link node - * even though the label definition exists. Here, we remove any missing references - * that have a matching label definition. - */ - allMissingReferences = allMissingReferences.filter( - missingReference => missingReference.label !== node.identifier - ); - } - }; - } + meta: { + type: "problem", + + docs: { + recommended: true, + description: "Disallow missing label references.", + }, + + messages: { + notFound: "Label reference '{{label}}' not found.", + }, + }, + + create(context) { + const { sourceCode } = context; + let allMissingReferences = []; + + return { + "root:exit"() { + for (const missingReference of allMissingReferences) { + context.report({ + loc: missingReference.position, + messageId: "notFound", + data: { + label: missingReference.label, + }, + }); + } + }, + + text(node) { + allMissingReferences.push( + ...findMissingReferences(node, sourceCode.text), + ); + }, + + definition(node) { + /* + * Sometimes a poorly-formatted link will end up a text node instead of a link node + * even though the label definition exists. Here, we remove any missing references + * that have a matching label definition. + */ + allMissingReferences = allMissingReferences.filter( + missingReference => + missingReference.label !== node.identifier, + ); + }, + }; + }, }; diff --git a/src/types.ts b/src/types.ts index 757e1371..7e22b199 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,17 +1,16 @@ import type { Node } from "mdast"; import type { Linter } from "eslint"; - export interface RangeMap { - indent: number; - js: number; - md: number; + indent: number; + js: number; + md: number; } export interface BlockBase { - baseIndentText: string; - comments: string[]; - rangeMap: RangeMap[]; + baseIndentText: string; + comments: string[]; + rangeMap: RangeMap[]; } export interface Block extends Node, BlockBase {} diff --git a/src/util.js b/src/util.js index 0d9007f0..3648d964 100644 --- a/src/util.js +++ b/src/util.js @@ -3,7 +3,6 @@ * @author Nicholas C. Zakas */ - /* * CommonMark does not allow any white space between the brackets in a reference link. * If that pattern is detected, then it's treated as text and not as a link. This pattern @@ -18,21 +17,20 @@ export const illegalShorthandTailPattern = /\]\[\s+\]$/u; * @returns {{lineOffset:number,columnOffset:number}} The location of the offset. */ export function findOffsets(text, offset) { + let lineOffset = 0; + let columnOffset = 0; - let lineOffset = 0; - let columnOffset = 0; - - for (let i = 0; i < offset; i++) { - if (text[i] === "\n") { - lineOffset++; - columnOffset = 0; - } else { - columnOffset++; - } - } + for (let i = 0; i < offset; i++) { + if (text[i] === "\n") { + lineOffset++; + columnOffset = 0; + } else { + columnOffset++; + } + } - return { - lineOffset, - columnOffset - }; + return { + lineOffset, + columnOffset, + }; } diff --git a/tests/examples/all.test.js b/tests/examples/all.test.js index 4506a135..5f00b539 100644 --- a/tests/examples/all.test.js +++ b/tests/examples/all.test.js @@ -9,39 +9,47 @@ const require = createRequire(import.meta.url); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const examplesDir = path.resolve(__dirname, "../../examples/"); -const examples = fs.readdirSync(examplesDir) - .filter(exampleDir => fs.statSync(path.join(examplesDir, exampleDir)).isDirectory()) - .filter(exampleDir => fs.existsSync(path.join(examplesDir, exampleDir, "package.json"))); +const examples = fs + .readdirSync(examplesDir) + .filter(exampleDir => + fs.statSync(path.join(examplesDir, exampleDir)).isDirectory(), + ) + .filter(exampleDir => + fs.existsSync(path.join(examplesDir, exampleDir, "package.json")), + ); for (const example of examples) { - const cwd = path.join(examplesDir, example); + const cwd = path.join(examplesDir, example); - // In case when this plugin supports multiple major versions of ESLint, - // CI matrix may include Node.js versions that are not supported by - // the version of ESLint that is used in examples. - // Only exercise the example if the running Node.js version satisfies the - // minimum version constraint. - const eslintPackageJsonPath = require.resolve("eslint/package.json", { - paths: [cwd] - }); - const eslintPackageJson = require(eslintPackageJsonPath); + // In case when this plugin supports multiple major versions of ESLint, + // CI matrix may include Node.js versions that are not supported by + // the version of ESLint that is used in examples. + // Only exercise the example if the running Node.js version satisfies the + // minimum version constraint. + const eslintPackageJsonPath = require.resolve("eslint/package.json", { + paths: [cwd], + }); + const eslintPackageJson = require(eslintPackageJsonPath); - if (semver.satisfies(process.version, eslintPackageJson.engines.node)) { - describe("examples", () => { - describe(example, () => { - it("reports errors on code blocks in .md files", async () => { - const { FlatESLint } = require( - require.resolve("eslint/use-at-your-own-risk", { paths: [cwd] }) - ); - const eslint = new FlatESLint({ cwd }); - const results = await eslint.lintFiles(["README.md"]); - const readme = results.find(result => - path.basename(result.filePath) == "README.md"); + if (semver.satisfies(process.version, eslintPackageJson.engines.node)) { + describe("examples", () => { + describe(example, () => { + it("reports errors on code blocks in .md files", async () => { + const { FlatESLint } = require( + require.resolve("eslint/use-at-your-own-risk", { + paths: [cwd], + }), + ); + const eslint = new FlatESLint({ cwd }); + const results = await eslint.lintFiles(["README.md"]); + const readme = results.find( + result => path.basename(result.filePath) == "README.md", + ); - assert.isNotNull(readme); - assert.isAbove(readme.messages.length, 0); - }); - }); - }); - } + assert.isNotNull(readme); + assert.isAbove(readme.messages.length, 0); + }); + }); + }); + } } diff --git a/tests/fixtures/eslint.config.js b/tests/fixtures/eslint.config.js index d8c4ecb7..cbe96978 100644 --- a/tests/fixtures/eslint.config.js +++ b/tests/fixtures/eslint.config.js @@ -2,23 +2,23 @@ import markdown from "../../src/index.js"; import globals from "globals"; export default [ - { - plugins: { - markdown - }, - languageOptions: { - globals: globals.browser - }, - rules: { - "eol-last": "error", - "no-console": "error", - "no-undef": "error", - "quotes": "error", - "spaced-comment": "error" - } - }, - { - "files": ["*.md", "*.mkdn", "*.mdown", "*.markdown", "*.custom"], - "processor": "markdown/markdown" - } + { + plugins: { + markdown, + }, + languageOptions: { + globals: globals.browser, + }, + rules: { + "eol-last": "error", + "no-console": "error", + "no-undef": "error", + quotes: "error", + "spaced-comment": "error", + }, + }, + { + files: ["*.md", "*.mkdn", "*.mdown", "*.markdown", "*.custom"], + processor: "markdown/markdown", + }, ]; diff --git a/tests/fixtures/eslintrc.json b/tests/fixtures/eslintrc.json index 3e64f12d..2697af09 100644 --- a/tests/fixtures/eslintrc.json +++ b/tests/fixtures/eslintrc.json @@ -1,20 +1,20 @@ { - "root": true, - "env": { - "browser": true - }, - "plugins": ["markdown"], - "overrides": [ - { - "files": ["*.md", "*.mkdn", "*.mdown", "*.markdown", "*.custom"], - "processor": "markdown/markdown" - } - ], - "rules": { - "eol-last": "error", - "no-console": "error", - "no-undef": "error", - "quotes": "error", - "spaced-comment": "error" + "root": true, + "env": { + "browser": true + }, + "plugins": ["markdown"], + "overrides": [ + { + "files": ["*.md", "*.mkdn", "*.mdown", "*.markdown", "*.custom"], + "processor": "markdown/markdown" } + ], + "rules": { + "eol-last": "error", + "no-console": "error", + "no-undef": "error", + "quotes": "error", + "spaced-comment": "error" + } } diff --git a/tests/fixtures/long.md b/tests/fixtures/long.md index 78f2940a..de6eca3e 100644 --- a/tests/fixtures/long.md +++ b/tests/fixtures/long.md @@ -13,7 +13,7 @@ console.log(42); ```js // Comment function foo() { - console.log("Hello"); + console.log("Hello"); } ``` diff --git a/tests/fixtures/recommended.js b/tests/fixtures/recommended.js index fa8c7329..4bd33ac9 100644 --- a/tests/fixtures/recommended.js +++ b/tests/fixtures/recommended.js @@ -2,12 +2,11 @@ import markdown from "../../src/index.js"; import js from "@eslint/js"; export default [ - js.configs.recommended, - ...markdown.configs.processor, - { - "rules": { - "no-console": "error" - } - } - + js.configs.recommended, + ...markdown.configs.processor, + { + rules: { + "no-console": "error", + }, + }, ]; diff --git a/tests/fixtures/recommended.json b/tests/fixtures/recommended.json index a21edeb9..b4393472 100644 --- a/tests/fixtures/recommended.json +++ b/tests/fixtures/recommended.json @@ -1,7 +1,7 @@ { - "root": true, - "extends": ["eslint:recommended", "plugin:markdown/recommended-legacy"], - "rules": { - "no-console": "error" - } + "root": true, + "extends": ["eslint:recommended", "plugin:markdown/recommended-legacy"], + "rules": { + "no-console": "error" + } } diff --git a/tests/language/markdown-source-code.test.js b/tests/language/markdown-source-code.test.js index 96dc2234..00881330 100644 --- a/tests/language/markdown-source-code.test.js +++ b/tests/language/markdown-source-code.test.js @@ -34,80 +34,87 @@ const ast = fromMarkdown(markdownText); //------------------------------------------------------------------------------ describe("MarkdownSourceCode", () => { - - let sourceCode; - - beforeEach(() => { - sourceCode = new MarkdownSourceCode({ text: markdownText, ast }); - }); - - describe("getText()", () => { - it("should return the text of the Markdown source code", () => { - assert.strictEqual(sourceCode.getText(), markdownText); - }); - - it("should return just the text of the first paragraph", () => { - assert.strictEqual(sourceCode.getText(ast.children[1]), "This is a paragraph."); - }); - - it("should return the text of the code block plus the ## of the following heading", () => { - assert.strictEqual(sourceCode.getText(ast.children[2], 0, 4), "```js\nconsole.log(\"Hello, world!\");\n```\n\n##"); - }); - }); - - describe("getLoc()", () => { - - it("should return the location of a node", () => { - assert.deepStrictEqual(sourceCode.getLoc(ast.children[0]), ast.children[0].position); - }); - - }); - - describe("getRange()", () => { - - it("should return the range of a node", () => { - assert.deepStrictEqual(sourceCode.getRange(ast.children[0]), [ast.children[0].position.start.offset, ast.children[0].position.end.offset]); - }); - - }); - - describe("traverse()", () => { - - it("should traverse the AST", () => { - - const steps = sourceCode.traverse(); - const stepsArray = Array.from(steps).map(step => [step.phase, step.target.type, step.target.value]); - - assert.deepStrictEqual(stepsArray, [ - [1, "root", void 0], - [1, "heading", void 0], - [1, "text", "Hello, world!"], - [2, "text", "Hello, world!"], - [2, "heading", void 0], - [1, "paragraph", void 0], - [1, "text", "This is a paragraph."], - [2, "text", "This is a paragraph."], - [2, "paragraph", void 0], - [1, "code", "console.log(\"Hello, world!\");"], - [2, "code", "console.log(\"Hello, world!\");"], - [1, "heading", void 0], - [1, "text", "This is a heading level 2"], - [2, "text", "This is a heading level 2"], - [2, "heading", void 0], - [1, "paragraph", void 0], - [1, "text", "This is "], - [2, "text", "This is "], - [1, "emphasis", void 0], - [1, "text", "another"], - [2, "text", "another"], - [2, "emphasis", void 0], - [1, "text", " paragraph."], - [2, "text", " paragraph."], - [2, "paragraph", void 0], - [2, "root", void 0] - ]); - }); - - }); - + let sourceCode; + + beforeEach(() => { + sourceCode = new MarkdownSourceCode({ text: markdownText, ast }); + }); + + describe("getText()", () => { + it("should return the text of the Markdown source code", () => { + assert.strictEqual(sourceCode.getText(), markdownText); + }); + + it("should return just the text of the first paragraph", () => { + assert.strictEqual( + sourceCode.getText(ast.children[1]), + "This is a paragraph.", + ); + }); + + it("should return the text of the code block plus the ## of the following heading", () => { + assert.strictEqual( + sourceCode.getText(ast.children[2], 0, 4), + '```js\nconsole.log("Hello, world!");\n```\n\n##', + ); + }); + }); + + describe("getLoc()", () => { + it("should return the location of a node", () => { + assert.deepStrictEqual( + sourceCode.getLoc(ast.children[0]), + ast.children[0].position, + ); + }); + }); + + describe("getRange()", () => { + it("should return the range of a node", () => { + assert.deepStrictEqual(sourceCode.getRange(ast.children[0]), [ + ast.children[0].position.start.offset, + ast.children[0].position.end.offset, + ]); + }); + }); + + describe("traverse()", () => { + it("should traverse the AST", () => { + const steps = sourceCode.traverse(); + const stepsArray = Array.from(steps).map(step => [ + step.phase, + step.target.type, + step.target.value, + ]); + + assert.deepStrictEqual(stepsArray, [ + [1, "root", void 0], + [1, "heading", void 0], + [1, "text", "Hello, world!"], + [2, "text", "Hello, world!"], + [2, "heading", void 0], + [1, "paragraph", void 0], + [1, "text", "This is a paragraph."], + [2, "text", "This is a paragraph."], + [2, "paragraph", void 0], + [1, "code", 'console.log("Hello, world!");'], + [2, "code", 'console.log("Hello, world!");'], + [1, "heading", void 0], + [1, "text", "This is a heading level 2"], + [2, "text", "This is a heading level 2"], + [2, "heading", void 0], + [1, "paragraph", void 0], + [1, "text", "This is "], + [2, "text", "This is "], + [1, "emphasis", void 0], + [1, "text", "another"], + [2, "text", "another"], + [2, "emphasis", void 0], + [1, "text", " paragraph."], + [2, "text", " paragraph."], + [2, "paragraph", void 0], + [2, "root", void 0], + ]); + }); + }); }); diff --git a/tests/plugin.test.js b/tests/plugin.test.js index 15c2b288..4848a52b 100644 --- a/tests/plugin.test.js +++ b/tests/plugin.test.js @@ -24,7 +24,9 @@ const __dirname = path.dirname(__filename); // Data //----------------------------------------------------------------------------- -const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../package.json"), "utf8")); +const pkg = JSON.parse( + fs.readFileSync(path.resolve(__dirname, "../package.json"), "utf8"), +); //----------------------------------------------------------------------------- // Helpers @@ -37,14 +39,18 @@ const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../package.json" * @returns {LegacyESLint} ESLint instance to execute in tests. */ function initLegacyESLint(fixtureConfigName, options = {}) { - return new LegacyESLint({ - cwd: path.resolve(__dirname, "./fixtures/"), - ignore: false, - useEslintrc: false, - overrideConfigFile: path.resolve(__dirname, "./fixtures/", fixtureConfigName), - plugins: { markdown: plugin }, - ...options - }); + return new LegacyESLint({ + cwd: path.resolve(__dirname, "./fixtures/"), + ignore: false, + useEslintrc: false, + overrideConfigFile: path.resolve( + __dirname, + "./fixtures/", + fixtureConfigName, + ), + plugins: { markdown: plugin }, + ...options, + }); } /** @@ -54,1945 +60,2207 @@ function initLegacyESLint(fixtureConfigName, options = {}) { * @returns {ESLint} ESLint instance to execute in tests. */ function initFlatESLint(fixtureConfigName, options = {}) { - return new ESLint({ - cwd: path.resolve(__dirname, "./fixtures/"), - ignore: false, - overrideConfigFile: path.resolve(__dirname, "./fixtures/", fixtureConfigName), - ...options - }); + return new ESLint({ + cwd: path.resolve(__dirname, "./fixtures/"), + ignore: false, + overrideConfigFile: path.resolve( + __dirname, + "./fixtures/", + fixtureConfigName, + ), + ...options, + }); } - //----------------------------------------------------------------------------- // Tests //----------------------------------------------------------------------------- describe("meta", () => { - it("should export meta property", () => { - assert.deepStrictEqual(plugin.meta, { name: "@eslint/markdown", version: pkg.version }); - }); + it("should export meta property", () => { + assert.deepStrictEqual(plugin.meta, { + name: "@eslint/markdown", + version: pkg.version, + }); + }); }); describe("LegacyESLint", () => { - - - describe("recommended config", () => { - let eslint; - const shortText = [ - "```js", - "var unusedVar = console.log(undef);", - "'unused expression';", - "```" - ].join("\n"); - - before(() => { - eslint = initLegacyESLint("recommended.json"); - }); - - it("should include the plugin", async () => { - const config = await eslint.calculateConfigForFile("test.md"); - - assert.include(config.plugins, "markdown"); - }); - - it("applies convenience configuration", async () => { - const config = await eslint.calculateConfigForFile("subdir/test.md/0.js"); - - assert.deepStrictEqual(config.parserOptions, { - ecmaFeatures: { - impliedStrict: true - } - }); - assert.deepStrictEqual(config.rules["eol-last"], ["off"]); - assert.deepStrictEqual(config.rules["no-undef"], ["off"]); - assert.deepStrictEqual(config.rules["no-unused-expressions"], ["off"]); - assert.deepStrictEqual(config.rules["no-unused-vars"], ["off"]); - assert.deepStrictEqual(config.rules["padded-blocks"], ["off"]); - assert.deepStrictEqual(config.rules.strict, ["off"]); - assert.deepStrictEqual(config.rules["unicode-bom"], ["off"]); - }); - - it("overrides configure processor to parse .md file code blocks", async () => { - const results = await eslint.lintText(shortText, { filePath: "test.md" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].ruleId, "no-console"); - }); - - }); - - describe("plugin", () => { - let eslint; - const shortText = [ - "```js", - "console.log(42);", - "```" - ].join("\n"); - - before(() => { - eslint = initLegacyESLint("eslintrc.json"); - }); - - it("should run on .md files", async () => { - const results = await eslint.lintText(shortText, { filePath: "test.md" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[0].line, 2); - }); - - it("should emit correct line numbers", async () => { - const code = [ - "# Hello, world!", - "", - "", - "```js", - "var bar = baz", - "", - "", - "var foo = blah", - "```" - ].join("\n"); - const results = await eslint.lintText(code, { filePath: "test.md" }); - - assert.strictEqual(results[0].messages[0].message, "'baz' is not defined."); - assert.strictEqual(results[0].messages[0].line, 5); - assert.strictEqual(results[0].messages[0].endLine, 5); - assert.strictEqual(results[0].messages[1].message, "'blah' is not defined."); - assert.strictEqual(results[0].messages[1].line, 8); - assert.strictEqual(results[0].messages[1].endLine, 8); - }); - - // https://github.com/eslint/markdown/issues/77 - it("should emit correct line numbers with leading blank line", async () => { - const code = [ - "### Heading", - "", - "```js", - "", - "console.log('a')", - "```" - ].join("\n"); - const results = await eslint.lintText(code, { filePath: "test.md" }); - - assert.strictEqual(results[0].messages[0].line, 5); - }); - - it("doesn't add end locations to messages without them", async () => { - const code = [ - "```js", - "!@#$%^&*()", - "```" - ].join("\n"); - const results = await eslint.lintText(code, { filePath: "test.md" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.notProperty(results[0].messages[0], "endLine"); - assert.notProperty(results[0].messages[0], "endColumn"); - }); - - it("should emit correct line numbers with leading comments", async () => { - const code = [ - "# Hello, world!", - "", - "", - "", - "", - "```js", - "var bar = baz", - "", - "var str = 'single quotes'", - "", - "var foo = blah", - "```" - ].join("\n"); - const results = await eslint.lintText(code, { filePath: "test.md" }); - - assert.strictEqual(results[0].messages[0].message, "'baz' is not defined."); - assert.strictEqual(results[0].messages[0].line, 7); - assert.strictEqual(results[0].messages[0].endLine, 7); - assert.strictEqual(results[0].messages[1].message, "'blah' is not defined."); - assert.strictEqual(results[0].messages[1].line, 11); - assert.strictEqual(results[0].messages[1].endLine, 11); - }); - - it("should run on .mkdn files", async () => { - const results = await eslint.lintText(shortText, { filePath: "test.mkdn" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[0].line, 2); - }); - - it("should run on .mdown files", async () => { - const results = await eslint.lintText(shortText, { filePath: "test.mdown" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[0].line, 2); - }); - - it("should run on .markdown files", async () => { - const results = await eslint.lintText(shortText, { filePath: "test.markdown" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[0].line, 2); - }); - - it("should run on files with any custom extension", async () => { - const results = await eslint.lintText(shortText, { filePath: "test.custom" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[0].line, 2); - }); - - it("should extract blocks and remap messages", async () => { - const results = await eslint.lintFiles([path.resolve(__dirname, "./fixtures/long.md")]); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 5); - assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[0].line, 10); - assert.strictEqual(results[0].messages[0].column, 1); - assert.strictEqual(results[0].messages[1].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[1].line, 16); - assert.strictEqual(results[0].messages[1].column, 5); - assert.strictEqual(results[0].messages[2].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[2].line, 24); - assert.strictEqual(results[0].messages[2].column, 1); - assert.strictEqual(results[0].messages[3].message, "Strings must use singlequote."); - assert.strictEqual(results[0].messages[3].line, 38); - assert.strictEqual(results[0].messages[3].column, 13); - assert.strictEqual(results[0].messages[4].message, "Parsing error: Unexpected character '@'"); - assert.strictEqual(results[0].messages[4].line, 46); - assert.strictEqual(results[0].messages[4].column, 2); - }); - - // https://github.com/eslint/markdown/issues/181 - it("should work when called on nested code blocks in the same file", async () => { - - /* - * As of this writing, the nested code block, though it uses the same - * Markdown processor, must use a different extension or ESLint will not - * re-apply the processor on the nested code block. To work around that, - * a file named `test.md` contains a nested `markdown` code block in - * this test. - * - * https://github.com/eslint/eslint/pull/14227/files#r602802758 - */ - const code = [ - "", - "", - "````markdown", - "", - "", - "This test only repros if the MD files have a different number of lines before code blocks.", - "", - "```js", - "// test.md/0_0.markdown/0_0.js", - "console.log('single quotes')", - "```", - "````" - ].join("\n"); - const recursiveCli = initLegacyESLint("eslintrc.json", { - extensions: [".js", ".markdown", ".md"] - }); - const results = await recursiveCli.lintText(code, { filePath: "test.md" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 2); - assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[0].line, 10); - assert.strictEqual(results[0].messages[1].message, "Strings must use doublequote."); - assert.strictEqual(results[0].messages[1].line, 10); - }); - - describe("configuration comments", () => { - it("apply only to the code block immediately following", async () => { - const code = [ - "", - "", - "", - "```js", - "var single = 'single';", - "console.log(single);", - "var double = \"double\";", - "console.log(double);", - "```", - "", - "```js", - "var single = 'single';", - "console.log(single);", - "var double = \"double\";", - "console.log(double);", - "```" - ].join("\n"); - const results = await eslint.lintText(code, { filePath: "test.md" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 4); - assert.strictEqual(results[0].messages[0].message, "Strings must use singlequote."); - assert.strictEqual(results[0].messages[0].line, 7); - assert.strictEqual(results[0].messages[1].message, "Strings must use doublequote."); - assert.strictEqual(results[0].messages[1].line, 12); - assert.strictEqual(results[0].messages[2].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[2].line, 13); - assert.strictEqual(results[0].messages[3].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[3].line, 15); - }); - - // https://github.com/eslint/markdown/issues/78 - it("preserves leading empty lines", async () => { - const code = [ - "", - "", - "```js", - "", - "\"use strict\";", - "```" - ].join("\n"); - const results = await eslint.lintText(code, { filePath: "test.md" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].message, "Unexpected newline before \"use strict\" directive."); - assert.strictEqual(results[0].messages[0].line, 5); - }); - }); - - describe("should fix code", () => { - before(() => { - eslint = initLegacyESLint("eslintrc.json", { fix: true }); - }); - - it("in the simplest case", async () => { - const input = [ - "This is Markdown.", - "", - "```js", - "console.log('Hello, world!')", - "```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "```js", - "console.log(\"Hello, world!\")", - "```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("across multiple lines", async () => { - const input = [ - "This is Markdown.", - "", - "```js", - "console.log('Hello, world!')", - "console.log('Hello, world!')", - "```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "```js", - "console.log(\"Hello, world!\")", - "console.log(\"Hello, world!\")", - "```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("across multiple blocks", async () => { - const input = [ - "This is Markdown.", - "", - "```js", - "console.log('Hello, world!')", - "```", - "", - "```js", - "console.log('Hello, world!')", - "```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "```js", - "console.log(\"Hello, world!\")", - "```", - "", - "```js", - "console.log(\"Hello, world!\")", - "```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("with lines indented by spaces", async () => { - const input = [ - "This is Markdown.", - "", - "```js", - "function test() {", - " console.log('Hello, world!')", - "}", - "```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "```js", - "function test() {", - " console.log(\"Hello, world!\")", - "}", - "```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("with lines indented by tabs", async () => { - const input = [ - "This is Markdown.", - "", - "```js", - "function test() {", - "\tconsole.log('Hello, world!')", - "}", - "```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "```js", - "function test() {", - "\tconsole.log(\"Hello, world!\")", - "}", - "```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("at the very start of a block", async () => { - const input = [ - "This is Markdown.", - "", - "```js", - "'use strict'", - "```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "```js", - "\"use strict\"", - "```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("in blocks with extra backticks", async () => { - const input = [ - "This is Markdown.", - "", - "````js", - "console.log('Hello, world!')", - "````" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "````js", - "console.log(\"Hello, world!\")", - "````" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("with configuration comments", async () => { - const input = [ - "", - "", - "```js", - "console.log('Hello, world!')", - "```" - ].join("\n"); - const expected = [ - "", - "", - "```js", - "console.log(\"Hello, world!\");", - "```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("inside a list single line", async () => { - const input = [ - "- Inside a list", - "", - " ```js", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - const expected = [ - "- Inside a list", - "", - " ```js", - " console.log(\"Hello, world!\")", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("inside a list multi line", async () => { - const input = [ - "- Inside a list", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ", - " var obj = {", - " hello: 'value'", - " }", - " ```" - ].join("\n"); - const expected = [ - "- Inside a list", - "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ", - " var obj = {", - " hello: \"value\"", - " }", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("with multiline autofix and CRLF", async () => { - const input = [ - "This is Markdown.", - "", - "```js", - "console.log('Hello, \\", - "world!')", - "console.log('Hello, \\", - "world!')", - "```" - ].join("\r\n"); - const expected = [ - "This is Markdown.", - "", - "```js", - "console.log(\"Hello, \\", - "world!\")", - "console.log(\"Hello, \\", - "world!\")", - "```" - ].join("\r\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - // https://spec.commonmark.org/0.28/#fenced-code-blocks - describe("when indented", () => { - it("by one space", async () => { - const input = [ - "This is Markdown.", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("by two spaces", async () => { - const input = [ - "This is Markdown.", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("by three spaces", async () => { - const input = [ - "This is Markdown.", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("and the closing fence is differently indented", async () => { - const input = [ - "This is Markdown.", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("underindented", async () => { - const input = [ - "This is Markdown.", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("multiline autofix", async () => { - const input = [ - "This is Markdown.", - "", - " ```js", - " console.log('Hello, \\", - " world!')", - " console.log('Hello, \\", - " world!')", - " ```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - " ```js", - " console.log(\"Hello, \\", - " world!\")", - " console.log(\"Hello, \\", - " world!\")", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("underindented multiline autofix", async () => { - const input = [ - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, \\", - " world!')", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - - // The Markdown parser doesn't have any concept of a "negative" - // indent left of the opening code fence, so autofixes move - // lines that were previously underindented to the same level - // as the opening code fence. - const expected = [ - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, \\", - " world!\")", - " console.log(\"Hello, world!\")", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("multiline autofix in blockquote", async () => { - const input = [ - "This is Markdown.", - "", - "> ```js", - "> console.log('Hello, \\", - "> world!')", - "> console.log('Hello, \\", - "> world!')", - "> ```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "> ```js", - "> console.log(\"Hello, \\", - "> world!\")", - "> console.log(\"Hello, \\", - "> world!\")", - "> ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("multiline autofix in nested blockquote", async () => { - const input = [ - "This is Markdown.", - "", - "> This is a nested blockquote.", - ">", - "> > ```js", - "> > console.log('Hello, \\", - "> > new\\", - "> > world!')", - "> > console.log('Hello, \\", - "> > world!')", - "> > ```" - ].join("\n"); - - // The Markdown parser doesn't have any concept of a "negative" - // indent left of the opening code fence, so autofixes move - // lines that were previously underindented to the same level - // as the opening code fence. - const expected = [ - "This is Markdown.", - "", - "> This is a nested blockquote.", - ">", - "> > ```js", - "> > console.log(\"Hello, \\", - "> > new\\", - "> > world!\")", - "> > console.log(\"Hello, \\", - "> > world!\")", - "> > ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("by one space with comments", async () => { - const input = [ - "This is Markdown.", - "", - "", - "", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "", - "", - "", - " ```js", - " console.log(\"Hello, world!\");", - " console.log(\"Hello, world!\");", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("unevenly by two spaces with comments", async () => { - const input = [ - "This is Markdown.", - "", - "", - "", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "", - "", - "", - " ```js", - " console.log(\"Hello, world!\");", - " console.log(\"Hello, world!\");", - " console.log(\"Hello, world!\");", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - describe("inside a list", () => { - it("normally", async () => { - const input = [ - "- This is a Markdown list.", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - const expected = [ - "- This is a Markdown list.", - "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("by one space", async () => { - const input = [ - "- This is a Markdown list.", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - const expected = [ - "- This is a Markdown list.", - "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - }); - }); - - it("with multiple rules", async () => { - const input = [ - "## Hello!", - "", - "", - "", - "```js", - "var obj = {", - " some: 'value'", - "}", - "", - "console.log('opop');", - "", - "function hello() {", - " return false", - "};", - "```" - ].join("\n"); - const expected = [ - "## Hello!", - "", - "", - "", - "```js", - "var obj = {", - " some: \"value\"", - "};", - "", - "console.log(\"opop\");", - "", - "function hello() {", - " return false;", - "};", - "```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - }); - - }); - + describe("recommended config", () => { + let eslint; + const shortText = [ + "```js", + "var unusedVar = console.log(undef);", + "'unused expression';", + "```", + ].join("\n"); + + before(() => { + eslint = initLegacyESLint("recommended.json"); + }); + + it("should include the plugin", async () => { + const config = await eslint.calculateConfigForFile("test.md"); + + assert.include(config.plugins, "markdown"); + }); + + it("applies convenience configuration", async () => { + const config = await eslint.calculateConfigForFile( + "subdir/test.md/0.js", + ); + + assert.deepStrictEqual(config.parserOptions, { + ecmaFeatures: { + impliedStrict: true, + }, + }); + assert.deepStrictEqual(config.rules["eol-last"], ["off"]); + assert.deepStrictEqual(config.rules["no-undef"], ["off"]); + assert.deepStrictEqual(config.rules["no-unused-expressions"], [ + "off", + ]); + assert.deepStrictEqual(config.rules["no-unused-vars"], ["off"]); + assert.deepStrictEqual(config.rules["padded-blocks"], ["off"]); + assert.deepStrictEqual(config.rules.strict, ["off"]); + assert.deepStrictEqual(config.rules["unicode-bom"], ["off"]); + }); + + it("overrides configure processor to parse .md file code blocks", async () => { + const results = await eslint.lintText(shortText, { + filePath: "test.md", + }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "no-console"); + }); + }); + + describe("plugin", () => { + let eslint; + const shortText = ["```js", "console.log(42);", "```"].join("\n"); + + before(() => { + eslint = initLegacyESLint("eslintrc.json"); + }); + + it("should run on .md files", async () => { + const results = await eslint.lintText(shortText, { + filePath: "test.md", + }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual( + results[0].messages[0].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[0].line, 2); + }); + + it("should emit correct line numbers", async () => { + const code = [ + "# Hello, world!", + "", + "", + "```js", + "var bar = baz", + "", + "", + "var foo = blah", + "```", + ].join("\n"); + const results = await eslint.lintText(code, { + filePath: "test.md", + }); + + assert.strictEqual( + results[0].messages[0].message, + "'baz' is not defined.", + ); + assert.strictEqual(results[0].messages[0].line, 5); + assert.strictEqual(results[0].messages[0].endLine, 5); + assert.strictEqual( + results[0].messages[1].message, + "'blah' is not defined.", + ); + assert.strictEqual(results[0].messages[1].line, 8); + assert.strictEqual(results[0].messages[1].endLine, 8); + }); + + // https://github.com/eslint/markdown/issues/77 + it("should emit correct line numbers with leading blank line", async () => { + const code = [ + "### Heading", + "", + "```js", + "", + "console.log('a')", + "```", + ].join("\n"); + const results = await eslint.lintText(code, { + filePath: "test.md", + }); + + assert.strictEqual(results[0].messages[0].line, 5); + }); + + it("doesn't add end locations to messages without them", async () => { + const code = ["```js", "!@#$%^&*()", "```"].join("\n"); + const results = await eslint.lintText(code, { + filePath: "test.md", + }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.notProperty(results[0].messages[0], "endLine"); + assert.notProperty(results[0].messages[0], "endColumn"); + }); + + it("should emit correct line numbers with leading comments", async () => { + const code = [ + "# Hello, world!", + "", + "", + "", + "", + "```js", + "var bar = baz", + "", + "var str = 'single quotes'", + "", + "var foo = blah", + "```", + ].join("\n"); + const results = await eslint.lintText(code, { + filePath: "test.md", + }); + + assert.strictEqual( + results[0].messages[0].message, + "'baz' is not defined.", + ); + assert.strictEqual(results[0].messages[0].line, 7); + assert.strictEqual(results[0].messages[0].endLine, 7); + assert.strictEqual( + results[0].messages[1].message, + "'blah' is not defined.", + ); + assert.strictEqual(results[0].messages[1].line, 11); + assert.strictEqual(results[0].messages[1].endLine, 11); + }); + + it("should run on .mkdn files", async () => { + const results = await eslint.lintText(shortText, { + filePath: "test.mkdn", + }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual( + results[0].messages[0].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[0].line, 2); + }); + + it("should run on .mdown files", async () => { + const results = await eslint.lintText(shortText, { + filePath: "test.mdown", + }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual( + results[0].messages[0].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[0].line, 2); + }); + + it("should run on .markdown files", async () => { + const results = await eslint.lintText(shortText, { + filePath: "test.markdown", + }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual( + results[0].messages[0].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[0].line, 2); + }); + + it("should run on files with any custom extension", async () => { + const results = await eslint.lintText(shortText, { + filePath: "test.custom", + }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual( + results[0].messages[0].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[0].line, 2); + }); + + it("should extract blocks and remap messages", async () => { + const results = await eslint.lintFiles([ + path.resolve(__dirname, "./fixtures/long.md"), + ]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 5); + assert.strictEqual( + results[0].messages[0].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[0].line, 10); + assert.strictEqual(results[0].messages[0].column, 1); + assert.strictEqual( + results[0].messages[1].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[1].line, 16); + assert.strictEqual(results[0].messages[1].column, 5); + assert.strictEqual( + results[0].messages[2].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[2].line, 24); + assert.strictEqual(results[0].messages[2].column, 1); + assert.strictEqual( + results[0].messages[3].message, + "Strings must use singlequote.", + ); + assert.strictEqual(results[0].messages[3].line, 38); + assert.strictEqual(results[0].messages[3].column, 13); + assert.strictEqual( + results[0].messages[4].message, + "Parsing error: Unexpected character '@'", + ); + assert.strictEqual(results[0].messages[4].line, 46); + assert.strictEqual(results[0].messages[4].column, 2); + }); + + // https://github.com/eslint/markdown/issues/181 + it("should work when called on nested code blocks in the same file", async () => { + /* + * As of this writing, the nested code block, though it uses the same + * Markdown processor, must use a different extension or ESLint will not + * re-apply the processor on the nested code block. To work around that, + * a file named `test.md` contains a nested `markdown` code block in + * this test. + * + * https://github.com/eslint/eslint/pull/14227/files#r602802758 + */ + const code = [ + "", + "", + "````markdown", + "", + "", + "This test only repros if the MD files have a different number of lines before code blocks.", + "", + "```js", + "// test.md/0_0.markdown/0_0.js", + "console.log('single quotes')", + "```", + "````", + ].join("\n"); + const recursiveCli = initLegacyESLint("eslintrc.json", { + extensions: [".js", ".markdown", ".md"], + }); + const results = await recursiveCli.lintText(code, { + filePath: "test.md", + }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual( + results[0].messages[0].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[0].line, 10); + assert.strictEqual( + results[0].messages[1].message, + "Strings must use doublequote.", + ); + assert.strictEqual(results[0].messages[1].line, 10); + }); + + describe("configuration comments", () => { + it("apply only to the code block immediately following", async () => { + const code = [ + '', + "", + "", + "```js", + "var single = 'single';", + "console.log(single);", + 'var double = "double";', + "console.log(double);", + "```", + "", + "```js", + "var single = 'single';", + "console.log(single);", + 'var double = "double";', + "console.log(double);", + "```", + ].join("\n"); + const results = await eslint.lintText(code, { + filePath: "test.md", + }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 4); + assert.strictEqual( + results[0].messages[0].message, + "Strings must use singlequote.", + ); + assert.strictEqual(results[0].messages[0].line, 7); + assert.strictEqual( + results[0].messages[1].message, + "Strings must use doublequote.", + ); + assert.strictEqual(results[0].messages[1].line, 12); + assert.strictEqual( + results[0].messages[2].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[2].line, 13); + assert.strictEqual( + results[0].messages[3].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[3].line, 15); + }); + + // https://github.com/eslint/markdown/issues/78 + it("preserves leading empty lines", async () => { + const code = [ + "", + "", + "```js", + "", + '"use strict";', + "```", + ].join("\n"); + const results = await eslint.lintText(code, { + filePath: "test.md", + }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual( + results[0].messages[0].message, + 'Unexpected newline before "use strict" directive.', + ); + assert.strictEqual(results[0].messages[0].line, 5); + }); + }); + + describe("should fix code", () => { + before(() => { + eslint = initLegacyESLint("eslintrc.json", { fix: true }); + }); + + it("in the simplest case", async () => { + const input = [ + "This is Markdown.", + "", + "```js", + "console.log('Hello, world!')", + "```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "```js", + 'console.log("Hello, world!")', + "```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("across multiple lines", async () => { + const input = [ + "This is Markdown.", + "", + "```js", + "console.log('Hello, world!')", + "console.log('Hello, world!')", + "```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "```js", + 'console.log("Hello, world!")', + 'console.log("Hello, world!")', + "```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("across multiple blocks", async () => { + const input = [ + "This is Markdown.", + "", + "```js", + "console.log('Hello, world!')", + "```", + "", + "```js", + "console.log('Hello, world!')", + "```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "```js", + 'console.log("Hello, world!")', + "```", + "", + "```js", + 'console.log("Hello, world!")', + "```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("with lines indented by spaces", async () => { + const input = [ + "This is Markdown.", + "", + "```js", + "function test() {", + " console.log('Hello, world!')", + "}", + "```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "```js", + "function test() {", + ' console.log("Hello, world!")', + "}", + "```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("with lines indented by tabs", async () => { + const input = [ + "This is Markdown.", + "", + "```js", + "function test() {", + "\tconsole.log('Hello, world!')", + "}", + "```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "```js", + "function test() {", + '\tconsole.log("Hello, world!")', + "}", + "```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("at the very start of a block", async () => { + const input = [ + "This is Markdown.", + "", + "```js", + "'use strict'", + "```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "```js", + '"use strict"', + "```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("in blocks with extra backticks", async () => { + const input = [ + "This is Markdown.", + "", + "````js", + "console.log('Hello, world!')", + "````", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "````js", + 'console.log("Hello, world!")', + "````", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("with configuration comments", async () => { + const input = [ + "", + "", + "```js", + "console.log('Hello, world!')", + "```", + ].join("\n"); + const expected = [ + "", + "", + "```js", + 'console.log("Hello, world!");', + "```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("inside a list single line", async () => { + const input = [ + "- Inside a list", + "", + " ```js", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + const expected = [ + "- Inside a list", + "", + " ```js", + ' console.log("Hello, world!")', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("inside a list multi line", async () => { + const input = [ + "- Inside a list", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ", + " var obj = {", + " hello: 'value'", + " }", + " ```", + ].join("\n"); + const expected = [ + "- Inside a list", + "", + " ```js", + ' console.log("Hello, world!")', + ' console.log("Hello, world!")', + " ", + " var obj = {", + ' hello: "value"', + " }", + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("with multiline autofix and CRLF", async () => { + const input = [ + "This is Markdown.", + "", + "```js", + "console.log('Hello, \\", + "world!')", + "console.log('Hello, \\", + "world!')", + "```", + ].join("\r\n"); + const expected = [ + "This is Markdown.", + "", + "```js", + 'console.log("Hello, \\', + 'world!")', + 'console.log("Hello, \\', + 'world!")', + "```", + ].join("\r\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + // https://spec.commonmark.org/0.28/#fenced-code-blocks + describe("when indented", () => { + it("by one space", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + ' console.log("Hello, world!")', + ' console.log("Hello, world!")', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("by two spaces", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + ' console.log("Hello, world!")', + ' console.log("Hello, world!")', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("by three spaces", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + ' console.log("Hello, world!")', + ' console.log("Hello, world!")', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("and the closing fence is differently indented", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + ' console.log("Hello, world!")', + ' console.log("Hello, world!")', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("underindented", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + ' console.log("Hello, world!")', + ' console.log("Hello, world!")', + ' console.log("Hello, world!")', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("multiline autofix", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, \\", + " world!')", + " console.log('Hello, \\", + " world!')", + " ```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + ' console.log("Hello, \\', + ' world!")', + ' console.log("Hello, \\', + ' world!")', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("underindented multiline autofix", async () => { + const input = [ + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, \\", + " world!')", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + + // The Markdown parser doesn't have any concept of a "negative" + // indent left of the opening code fence, so autofixes move + // lines that were previously underindented to the same level + // as the opening code fence. + const expected = [ + " ```js", + ' console.log("Hello, world!")', + ' console.log("Hello, \\', + ' world!")', + ' console.log("Hello, world!")', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("multiline autofix in blockquote", async () => { + const input = [ + "This is Markdown.", + "", + "> ```js", + "> console.log('Hello, \\", + "> world!')", + "> console.log('Hello, \\", + "> world!')", + "> ```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "> ```js", + '> console.log("Hello, \\', + '> world!")', + '> console.log("Hello, \\', + '> world!")', + "> ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("multiline autofix in nested blockquote", async () => { + const input = [ + "This is Markdown.", + "", + "> This is a nested blockquote.", + ">", + "> > ```js", + "> > console.log('Hello, \\", + "> > new\\", + "> > world!')", + "> > console.log('Hello, \\", + "> > world!')", + "> > ```", + ].join("\n"); + + // The Markdown parser doesn't have any concept of a "negative" + // indent left of the opening code fence, so autofixes move + // lines that were previously underindented to the same level + // as the opening code fence. + const expected = [ + "This is Markdown.", + "", + "> This is a nested blockquote.", + ">", + "> > ```js", + '> > console.log("Hello, \\', + "> > new\\", + '> > world!")', + '> > console.log("Hello, \\', + '> > world!")', + "> > ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("by one space with comments", async () => { + const input = [ + "This is Markdown.", + "", + "", + "", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "", + "", + "", + " ```js", + ' console.log("Hello, world!");', + ' console.log("Hello, world!");', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("unevenly by two spaces with comments", async () => { + const input = [ + "This is Markdown.", + "", + "", + "", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "", + "", + "", + " ```js", + ' console.log("Hello, world!");', + ' console.log("Hello, world!");', + ' console.log("Hello, world!");', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + describe("inside a list", () => { + it("normally", async () => { + const input = [ + "- This is a Markdown list.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + const expected = [ + "- This is a Markdown list.", + "", + " ```js", + ' console.log("Hello, world!")', + ' console.log("Hello, world!")', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("by one space", async () => { + const input = [ + "- This is a Markdown list.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + const expected = [ + "- This is a Markdown list.", + "", + " ```js", + ' console.log("Hello, world!")', + ' console.log("Hello, world!")', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + }); + }); + + it("with multiple rules", async () => { + const input = [ + "## Hello!", + "", + "", + "", + "```js", + "var obj = {", + " some: 'value'", + "}", + "", + "console.log('opop');", + "", + "function hello() {", + " return false", + "};", + "```", + ].join("\n"); + const expected = [ + "## Hello!", + "", + "", + "", + "```js", + "var obj = {", + ' some: "value"', + "};", + "", + 'console.log("opop");', + "", + "function hello() {", + " return false;", + "};", + "```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + }); + }); }); - describe("FlatESLint", () => { - - - describe("recommended config", () => { - let eslint; - const shortText = [ - "```js", - "var unusedVar = console.log(undef);", - "'unused expression';", - "```" - ].join("\n"); - - before(() => { - eslint = initFlatESLint("recommended.js"); - }); - - it("should include the plugin", async () => { - const config = await eslint.calculateConfigForFile("test.md"); - - assert.isDefined(config.plugins.markdown); - }); - - it("applies convenience configuration", async () => { - const config = await eslint.calculateConfigForFile("subdir/test.md/0.js"); - - assert.deepStrictEqual(config.languageOptions.parserOptions, { - ecmaFeatures: { - impliedStrict: true - } - }); - assert.deepStrictEqual(config.rules["eol-last"], [0]); - assert.deepStrictEqual(config.rules["no-undef"], [0]); - assert.deepStrictEqual(config.rules["no-unused-expressions"], [0]); - assert.deepStrictEqual(config.rules["no-unused-vars"], [0]); - assert.deepStrictEqual(config.rules["padded-blocks"], [0]); - assert.deepStrictEqual(config.rules.strict, [0]); - assert.deepStrictEqual(config.rules["unicode-bom"], [0]); - }); - - it("overrides configure processor to parse .md file code blocks", async () => { - const results = await eslint.lintText(shortText, { filePath: "test.md" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].ruleId, "no-console"); - }); - - }); - - describe("plugin", () => { - let eslint; - const shortText = [ - "```js", - "console.log(42);", - "```" - ].join("\n"); - - before(() => { - eslint = initFlatESLint("eslint.config.js"); - }); - - it("should run on .md files", async () => { - const results = await eslint.lintText(shortText, { filePath: "test.md" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[0].line, 2); - }); - - it("should emit correct line numbers", async () => { - const code = [ - "# Hello, world!", - "", - "", - "```js", - "var bar = baz", - "", - "", - "var foo = blah", - "```" - ].join("\n"); - const results = await eslint.lintText(code, { filePath: "test.md" }); - - assert.strictEqual(results[0].messages[0].message, "'baz' is not defined."); - assert.strictEqual(results[0].messages[0].line, 5); - assert.strictEqual(results[0].messages[0].endLine, 5); - assert.strictEqual(results[0].messages[1].message, "'blah' is not defined."); - assert.strictEqual(results[0].messages[1].line, 8); - assert.strictEqual(results[0].messages[1].endLine, 8); - }); - - // https://github.com/eslint/markdown/issues/77 - it("should emit correct line numbers with leading blank line", async () => { - const code = [ - "### Heading", - "", - "```js", - "", - "console.log('a')", - "```" - ].join("\n"); - const results = await eslint.lintText(code, { filePath: "test.md" }); - - assert.strictEqual(results[0].messages[0].line, 5); - }); - - it("doesn't add end locations to messages without them", async () => { - const code = [ - "```js", - "!@#$%^&*()", - "```" - ].join("\n"); - const results = await eslint.lintText(code, { filePath: "test.md" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.notProperty(results[0].messages[0], "endLine"); - assert.notProperty(results[0].messages[0], "endColumn"); - }); - - it("should emit correct line numbers with leading comments", async () => { - const code = [ - "# Hello, world!", - "", - "", - "", - "", - "```js", - "var bar = baz", - "", - "var str = 'single quotes'", - "", - "var foo = blah", - "```" - ].join("\n"); - const results = await eslint.lintText(code, { filePath: "test.md" }); - - assert.strictEqual(results[0].messages[0].message, "'baz' is not defined."); - assert.strictEqual(results[0].messages[0].line, 7); - assert.strictEqual(results[0].messages[0].endLine, 7); - assert.strictEqual(results[0].messages[1].message, "'blah' is not defined."); - assert.strictEqual(results[0].messages[1].line, 11); - assert.strictEqual(results[0].messages[1].endLine, 11); - }); - - it("should run on .mkdn files", async () => { - const results = await eslint.lintText(shortText, { filePath: "test.mkdn" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[0].line, 2); - }); - - it("should run on .mdown files", async () => { - const results = await eslint.lintText(shortText, { filePath: "test.mdown" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[0].line, 2); - }); - - it("should run on .markdown files", async () => { - const results = await eslint.lintText(shortText, { filePath: "test.markdown" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[0].line, 2); - }); - - it("should run on files with any custom extension", async () => { - const results = await eslint.lintText(shortText, { filePath: "test.custom" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[0].line, 2); - }); - - it("should extract blocks and remap messages", async () => { - const results = await eslint.lintFiles([path.resolve(__dirname, "./fixtures/long.md")]); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 5); - assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[0].line, 10); - assert.strictEqual(results[0].messages[0].column, 1); - assert.strictEqual(results[0].messages[1].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[1].line, 16); - assert.strictEqual(results[0].messages[1].column, 5); - assert.strictEqual(results[0].messages[2].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[2].line, 24); - assert.strictEqual(results[0].messages[2].column, 1); - assert.strictEqual(results[0].messages[3].message, "Strings must use singlequote."); - assert.strictEqual(results[0].messages[3].line, 38); - assert.strictEqual(results[0].messages[3].column, 13); - assert.strictEqual(results[0].messages[4].message, "Parsing error: Unexpected character '@'"); - assert.strictEqual(results[0].messages[4].line, 46); - assert.strictEqual(results[0].messages[4].column, 2); - }); - - // https://github.com/eslint/markdown/issues/181 - it("should work when called on nested code blocks in the same file", async () => { - - /* - * As of this writing, the nested code block, though it uses the same - * Markdown processor, must use a different extension or ESLint will not - * re-apply the processor on the nested code block. To work around that, - * a file named `test.md` contains a nested `markdown` code block in - * this test. - * - * https://github.com/eslint/eslint/pull/14227/files#r602802758 - */ - const code = [ - "", - "", - "````markdown", - "", - "", - "This test only repros if the MD files have a different number of lines before code blocks.", - "", - "```js", - "// test.md/0_0.markdown/0_0.js", - "console.log('single quotes')", - "```", - "````" - ].join("\n"); - const recursiveCli = initLegacyESLint("eslintrc.json", { - extensions: [".js", ".markdown", ".md"] - }); - const results = await recursiveCli.lintText(code, { filePath: "test.md" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 2); - assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[0].line, 10); - assert.strictEqual(results[0].messages[1].message, "Strings must use doublequote."); - assert.strictEqual(results[0].messages[1].line, 10); - }); - - describe("configuration comments", () => { - it("apply only to the code block immediately following", async () => { - const code = [ - "", - "", - "", - "```js", - "var single = 'single';", - "console.log(single);", - "var double = \"double\";", - "console.log(double);", - "```", - "", - "```js", - "var single = 'single';", - "console.log(single);", - "var double = \"double\";", - "console.log(double);", - "```" - ].join("\n"); - const results = await eslint.lintText(code, { filePath: "test.md" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 4); - assert.strictEqual(results[0].messages[0].message, "Strings must use singlequote."); - assert.strictEqual(results[0].messages[0].line, 7); - assert.strictEqual(results[0].messages[1].message, "Strings must use doublequote."); - assert.strictEqual(results[0].messages[1].line, 12); - assert.strictEqual(results[0].messages[2].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[2].line, 13); - assert.strictEqual(results[0].messages[3].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[3].line, 15); - }); - - // https://github.com/eslint/markdown/issues/78 - it("preserves leading empty lines", async () => { - const code = [ - "", - "", - "```js", - "", - "\"use strict\";", - "```" - ].join("\n"); - const results = await eslint.lintText(code, { filePath: "test.md" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].message, "Unexpected newline before \"use strict\" directive."); - assert.strictEqual(results[0].messages[0].line, 5); - }); - }); - - describe("should fix code", () => { - before(() => { - eslint = initLegacyESLint("eslintrc.json", { fix: true }); - }); - - it("in the simplest case", async () => { - const input = [ - "This is Markdown.", - "", - "```js", - "console.log('Hello, world!')", - "```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "```js", - "console.log(\"Hello, world!\")", - "```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("across multiple lines", async () => { - const input = [ - "This is Markdown.", - "", - "```js", - "console.log('Hello, world!')", - "console.log('Hello, world!')", - "```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "```js", - "console.log(\"Hello, world!\")", - "console.log(\"Hello, world!\")", - "```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("across multiple blocks", async () => { - const input = [ - "This is Markdown.", - "", - "```js", - "console.log('Hello, world!')", - "```", - "", - "```js", - "console.log('Hello, world!')", - "```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "```js", - "console.log(\"Hello, world!\")", - "```", - "", - "```js", - "console.log(\"Hello, world!\")", - "```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("with lines indented by spaces", async () => { - const input = [ - "This is Markdown.", - "", - "```js", - "function test() {", - " console.log('Hello, world!')", - "}", - "```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "```js", - "function test() {", - " console.log(\"Hello, world!\")", - "}", - "```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("with lines indented by tabs", async () => { - const input = [ - "This is Markdown.", - "", - "```js", - "function test() {", - "\tconsole.log('Hello, world!')", - "}", - "```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "```js", - "function test() {", - "\tconsole.log(\"Hello, world!\")", - "}", - "```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("at the very start of a block", async () => { - const input = [ - "This is Markdown.", - "", - "```js", - "'use strict'", - "```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "```js", - "\"use strict\"", - "```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("in blocks with extra backticks", async () => { - const input = [ - "This is Markdown.", - "", - "````js", - "console.log('Hello, world!')", - "````" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "````js", - "console.log(\"Hello, world!\")", - "````" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("with configuration comments", async () => { - const input = [ - "", - "", - "```js", - "console.log('Hello, world!')", - "```" - ].join("\n"); - const expected = [ - "", - "", - "```js", - "console.log(\"Hello, world!\");", - "```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("inside a list single line", async () => { - const input = [ - "- Inside a list", - "", - " ```js", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - const expected = [ - "- Inside a list", - "", - " ```js", - " console.log(\"Hello, world!\")", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("inside a list multi line", async () => { - const input = [ - "- Inside a list", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ", - " var obj = {", - " hello: 'value'", - " }", - " ```" - ].join("\n"); - const expected = [ - "- Inside a list", - "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ", - " var obj = {", - " hello: \"value\"", - " }", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("with multiline autofix and CRLF", async () => { - const input = [ - "This is Markdown.", - "", - "```js", - "console.log('Hello, \\", - "world!')", - "console.log('Hello, \\", - "world!')", - "```" - ].join("\r\n"); - const expected = [ - "This is Markdown.", - "", - "```js", - "console.log(\"Hello, \\", - "world!\")", - "console.log(\"Hello, \\", - "world!\")", - "```" - ].join("\r\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - // https://spec.commonmark.org/0.28/#fenced-code-blocks - describe("when indented", () => { - it("by one space", async () => { - const input = [ - "This is Markdown.", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("by two spaces", async () => { - const input = [ - "This is Markdown.", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("by three spaces", async () => { - const input = [ - "This is Markdown.", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("and the closing fence is differently indented", async () => { - const input = [ - "This is Markdown.", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("underindented", async () => { - const input = [ - "This is Markdown.", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("multiline autofix", async () => { - const input = [ - "This is Markdown.", - "", - " ```js", - " console.log('Hello, \\", - " world!')", - " console.log('Hello, \\", - " world!')", - " ```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - " ```js", - " console.log(\"Hello, \\", - " world!\")", - " console.log(\"Hello, \\", - " world!\")", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("underindented multiline autofix", async () => { - const input = [ - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, \\", - " world!')", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - - // The Markdown parser doesn't have any concept of a "negative" - // indent left of the opening code fence, so autofixes move - // lines that were previously underindented to the same level - // as the opening code fence. - const expected = [ - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, \\", - " world!\")", - " console.log(\"Hello, world!\")", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("multiline autofix in blockquote", async () => { - const input = [ - "This is Markdown.", - "", - "> ```js", - "> console.log('Hello, \\", - "> world!')", - "> console.log('Hello, \\", - "> world!')", - "> ```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "> ```js", - "> console.log(\"Hello, \\", - "> world!\")", - "> console.log(\"Hello, \\", - "> world!\")", - "> ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("multiline autofix in nested blockquote", async () => { - const input = [ - "This is Markdown.", - "", - "> This is a nested blockquote.", - ">", - "> > ```js", - "> > console.log('Hello, \\", - "> > new\\", - "> > world!')", - "> > console.log('Hello, \\", - "> > world!')", - "> > ```" - ].join("\n"); - - // The Markdown parser doesn't have any concept of a "negative" - // indent left of the opening code fence, so autofixes move - // lines that were previously underindented to the same level - // as the opening code fence. - const expected = [ - "This is Markdown.", - "", - "> This is a nested blockquote.", - ">", - "> > ```js", - "> > console.log(\"Hello, \\", - "> > new\\", - "> > world!\")", - "> > console.log(\"Hello, \\", - "> > world!\")", - "> > ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("by one space with comments", async () => { - const input = [ - "This is Markdown.", - "", - "", - "", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "", - "", - "", - " ```js", - " console.log(\"Hello, world!\");", - " console.log(\"Hello, world!\");", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("unevenly by two spaces with comments", async () => { - const input = [ - "This is Markdown.", - "", - "", - "", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "", - "", - "", - " ```js", - " console.log(\"Hello, world!\");", - " console.log(\"Hello, world!\");", - " console.log(\"Hello, world!\");", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - describe("inside a list", () => { - it("normally", async () => { - const input = [ - "- This is a Markdown list.", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - const expected = [ - "- This is a Markdown list.", - "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("by one space", async () => { - const input = [ - "- This is a Markdown list.", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - const expected = [ - "- This is a Markdown list.", - "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - }); - }); - - it("with multiple rules", async () => { - const input = [ - "## Hello!", - "", - "", - "", - "```js", - "var obj = {", - " some: 'value'", - "}", - "", - "console.log('opop');", - "", - "function hello() {", - " return false", - "};", - "```" - ].join("\n"); - const expected = [ - "## Hello!", - "", - "", - "", - "```js", - "var obj = {", - " some: \"value\"", - "};", - "", - "console.log(\"opop\");", - "", - "function hello() {", - " return false;", - "};", - "```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - }); - - }); - - + describe("recommended config", () => { + let eslint; + const shortText = [ + "```js", + "var unusedVar = console.log(undef);", + "'unused expression';", + "```", + ].join("\n"); + + before(() => { + eslint = initFlatESLint("recommended.js"); + }); + + it("should include the plugin", async () => { + const config = await eslint.calculateConfigForFile("test.md"); + + assert.isDefined(config.plugins.markdown); + }); + + it("applies convenience configuration", async () => { + const config = await eslint.calculateConfigForFile( + "subdir/test.md/0.js", + ); + + assert.deepStrictEqual(config.languageOptions.parserOptions, { + ecmaFeatures: { + impliedStrict: true, + }, + }); + assert.deepStrictEqual(config.rules["eol-last"], [0]); + assert.deepStrictEqual(config.rules["no-undef"], [0]); + assert.deepStrictEqual(config.rules["no-unused-expressions"], [0]); + assert.deepStrictEqual(config.rules["no-unused-vars"], [0]); + assert.deepStrictEqual(config.rules["padded-blocks"], [0]); + assert.deepStrictEqual(config.rules.strict, [0]); + assert.deepStrictEqual(config.rules["unicode-bom"], [0]); + }); + + it("overrides configure processor to parse .md file code blocks", async () => { + const results = await eslint.lintText(shortText, { + filePath: "test.md", + }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "no-console"); + }); + }); + + describe("plugin", () => { + let eslint; + const shortText = ["```js", "console.log(42);", "```"].join("\n"); + + before(() => { + eslint = initFlatESLint("eslint.config.js"); + }); + + it("should run on .md files", async () => { + const results = await eslint.lintText(shortText, { + filePath: "test.md", + }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual( + results[0].messages[0].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[0].line, 2); + }); + + it("should emit correct line numbers", async () => { + const code = [ + "# Hello, world!", + "", + "", + "```js", + "var bar = baz", + "", + "", + "var foo = blah", + "```", + ].join("\n"); + const results = await eslint.lintText(code, { + filePath: "test.md", + }); + + assert.strictEqual( + results[0].messages[0].message, + "'baz' is not defined.", + ); + assert.strictEqual(results[0].messages[0].line, 5); + assert.strictEqual(results[0].messages[0].endLine, 5); + assert.strictEqual( + results[0].messages[1].message, + "'blah' is not defined.", + ); + assert.strictEqual(results[0].messages[1].line, 8); + assert.strictEqual(results[0].messages[1].endLine, 8); + }); + + // https://github.com/eslint/markdown/issues/77 + it("should emit correct line numbers with leading blank line", async () => { + const code = [ + "### Heading", + "", + "```js", + "", + "console.log('a')", + "```", + ].join("\n"); + const results = await eslint.lintText(code, { + filePath: "test.md", + }); + + assert.strictEqual(results[0].messages[0].line, 5); + }); + + it("doesn't add end locations to messages without them", async () => { + const code = ["```js", "!@#$%^&*()", "```"].join("\n"); + const results = await eslint.lintText(code, { + filePath: "test.md", + }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.notProperty(results[0].messages[0], "endLine"); + assert.notProperty(results[0].messages[0], "endColumn"); + }); + + it("should emit correct line numbers with leading comments", async () => { + const code = [ + "# Hello, world!", + "", + "", + "", + "", + "```js", + "var bar = baz", + "", + "var str = 'single quotes'", + "", + "var foo = blah", + "```", + ].join("\n"); + const results = await eslint.lintText(code, { + filePath: "test.md", + }); + + assert.strictEqual( + results[0].messages[0].message, + "'baz' is not defined.", + ); + assert.strictEqual(results[0].messages[0].line, 7); + assert.strictEqual(results[0].messages[0].endLine, 7); + assert.strictEqual( + results[0].messages[1].message, + "'blah' is not defined.", + ); + assert.strictEqual(results[0].messages[1].line, 11); + assert.strictEqual(results[0].messages[1].endLine, 11); + }); + + it("should run on .mkdn files", async () => { + const results = await eslint.lintText(shortText, { + filePath: "test.mkdn", + }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual( + results[0].messages[0].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[0].line, 2); + }); + + it("should run on .mdown files", async () => { + const results = await eslint.lintText(shortText, { + filePath: "test.mdown", + }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual( + results[0].messages[0].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[0].line, 2); + }); + + it("should run on .markdown files", async () => { + const results = await eslint.lintText(shortText, { + filePath: "test.markdown", + }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual( + results[0].messages[0].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[0].line, 2); + }); + + it("should run on files with any custom extension", async () => { + const results = await eslint.lintText(shortText, { + filePath: "test.custom", + }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual( + results[0].messages[0].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[0].line, 2); + }); + + it("should extract blocks and remap messages", async () => { + const results = await eslint.lintFiles([ + path.resolve(__dirname, "./fixtures/long.md"), + ]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 5); + assert.strictEqual( + results[0].messages[0].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[0].line, 10); + assert.strictEqual(results[0].messages[0].column, 1); + assert.strictEqual( + results[0].messages[1].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[1].line, 16); + assert.strictEqual(results[0].messages[1].column, 5); + assert.strictEqual( + results[0].messages[2].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[2].line, 24); + assert.strictEqual(results[0].messages[2].column, 1); + assert.strictEqual( + results[0].messages[3].message, + "Strings must use singlequote.", + ); + assert.strictEqual(results[0].messages[3].line, 38); + assert.strictEqual(results[0].messages[3].column, 13); + assert.strictEqual( + results[0].messages[4].message, + "Parsing error: Unexpected character '@'", + ); + assert.strictEqual(results[0].messages[4].line, 46); + assert.strictEqual(results[0].messages[4].column, 2); + }); + + // https://github.com/eslint/markdown/issues/181 + it("should work when called on nested code blocks in the same file", async () => { + /* + * As of this writing, the nested code block, though it uses the same + * Markdown processor, must use a different extension or ESLint will not + * re-apply the processor on the nested code block. To work around that, + * a file named `test.md` contains a nested `markdown` code block in + * this test. + * + * https://github.com/eslint/eslint/pull/14227/files#r602802758 + */ + const code = [ + "", + "", + "````markdown", + "", + "", + "This test only repros if the MD files have a different number of lines before code blocks.", + "", + "```js", + "// test.md/0_0.markdown/0_0.js", + "console.log('single quotes')", + "```", + "````", + ].join("\n"); + const recursiveCli = initLegacyESLint("eslintrc.json", { + extensions: [".js", ".markdown", ".md"], + }); + const results = await recursiveCli.lintText(code, { + filePath: "test.md", + }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual( + results[0].messages[0].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[0].line, 10); + assert.strictEqual( + results[0].messages[1].message, + "Strings must use doublequote.", + ); + assert.strictEqual(results[0].messages[1].line, 10); + }); + + describe("configuration comments", () => { + it("apply only to the code block immediately following", async () => { + const code = [ + '', + "", + "", + "```js", + "var single = 'single';", + "console.log(single);", + 'var double = "double";', + "console.log(double);", + "```", + "", + "```js", + "var single = 'single';", + "console.log(single);", + 'var double = "double";', + "console.log(double);", + "```", + ].join("\n"); + const results = await eslint.lintText(code, { + filePath: "test.md", + }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 4); + assert.strictEqual( + results[0].messages[0].message, + "Strings must use singlequote.", + ); + assert.strictEqual(results[0].messages[0].line, 7); + assert.strictEqual( + results[0].messages[1].message, + "Strings must use doublequote.", + ); + assert.strictEqual(results[0].messages[1].line, 12); + assert.strictEqual( + results[0].messages[2].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[2].line, 13); + assert.strictEqual( + results[0].messages[3].message, + "Unexpected console statement.", + ); + assert.strictEqual(results[0].messages[3].line, 15); + }); + + // https://github.com/eslint/markdown/issues/78 + it("preserves leading empty lines", async () => { + const code = [ + "", + "", + "```js", + "", + '"use strict";', + "```", + ].join("\n"); + const results = await eslint.lintText(code, { + filePath: "test.md", + }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual( + results[0].messages[0].message, + 'Unexpected newline before "use strict" directive.', + ); + assert.strictEqual(results[0].messages[0].line, 5); + }); + }); + + describe("should fix code", () => { + before(() => { + eslint = initLegacyESLint("eslintrc.json", { fix: true }); + }); + + it("in the simplest case", async () => { + const input = [ + "This is Markdown.", + "", + "```js", + "console.log('Hello, world!')", + "```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "```js", + 'console.log("Hello, world!")', + "```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("across multiple lines", async () => { + const input = [ + "This is Markdown.", + "", + "```js", + "console.log('Hello, world!')", + "console.log('Hello, world!')", + "```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "```js", + 'console.log("Hello, world!")', + 'console.log("Hello, world!")', + "```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("across multiple blocks", async () => { + const input = [ + "This is Markdown.", + "", + "```js", + "console.log('Hello, world!')", + "```", + "", + "```js", + "console.log('Hello, world!')", + "```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "```js", + 'console.log("Hello, world!")', + "```", + "", + "```js", + 'console.log("Hello, world!")', + "```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("with lines indented by spaces", async () => { + const input = [ + "This is Markdown.", + "", + "```js", + "function test() {", + " console.log('Hello, world!')", + "}", + "```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "```js", + "function test() {", + ' console.log("Hello, world!")', + "}", + "```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("with lines indented by tabs", async () => { + const input = [ + "This is Markdown.", + "", + "```js", + "function test() {", + "\tconsole.log('Hello, world!')", + "}", + "```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "```js", + "function test() {", + '\tconsole.log("Hello, world!")', + "}", + "```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("at the very start of a block", async () => { + const input = [ + "This is Markdown.", + "", + "```js", + "'use strict'", + "```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "```js", + '"use strict"', + "```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("in blocks with extra backticks", async () => { + const input = [ + "This is Markdown.", + "", + "````js", + "console.log('Hello, world!')", + "````", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "````js", + 'console.log("Hello, world!")', + "````", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("with configuration comments", async () => { + const input = [ + "", + "", + "```js", + "console.log('Hello, world!')", + "```", + ].join("\n"); + const expected = [ + "", + "", + "```js", + 'console.log("Hello, world!");', + "```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("inside a list single line", async () => { + const input = [ + "- Inside a list", + "", + " ```js", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + const expected = [ + "- Inside a list", + "", + " ```js", + ' console.log("Hello, world!")', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("inside a list multi line", async () => { + const input = [ + "- Inside a list", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ", + " var obj = {", + " hello: 'value'", + " }", + " ```", + ].join("\n"); + const expected = [ + "- Inside a list", + "", + " ```js", + ' console.log("Hello, world!")', + ' console.log("Hello, world!")', + " ", + " var obj = {", + ' hello: "value"', + " }", + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("with multiline autofix and CRLF", async () => { + const input = [ + "This is Markdown.", + "", + "```js", + "console.log('Hello, \\", + "world!')", + "console.log('Hello, \\", + "world!')", + "```", + ].join("\r\n"); + const expected = [ + "This is Markdown.", + "", + "```js", + 'console.log("Hello, \\', + 'world!")', + 'console.log("Hello, \\', + 'world!")', + "```", + ].join("\r\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + // https://spec.commonmark.org/0.28/#fenced-code-blocks + describe("when indented", () => { + it("by one space", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + ' console.log("Hello, world!")', + ' console.log("Hello, world!")', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("by two spaces", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + ' console.log("Hello, world!")', + ' console.log("Hello, world!")', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("by three spaces", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + ' console.log("Hello, world!")', + ' console.log("Hello, world!")', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("and the closing fence is differently indented", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + ' console.log("Hello, world!")', + ' console.log("Hello, world!")', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("underindented", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + ' console.log("Hello, world!")', + ' console.log("Hello, world!")', + ' console.log("Hello, world!")', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("multiline autofix", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, \\", + " world!')", + " console.log('Hello, \\", + " world!')", + " ```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + ' console.log("Hello, \\', + ' world!")', + ' console.log("Hello, \\', + ' world!")', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("underindented multiline autofix", async () => { + const input = [ + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, \\", + " world!')", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + + // The Markdown parser doesn't have any concept of a "negative" + // indent left of the opening code fence, so autofixes move + // lines that were previously underindented to the same level + // as the opening code fence. + const expected = [ + " ```js", + ' console.log("Hello, world!")', + ' console.log("Hello, \\', + ' world!")', + ' console.log("Hello, world!")', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("multiline autofix in blockquote", async () => { + const input = [ + "This is Markdown.", + "", + "> ```js", + "> console.log('Hello, \\", + "> world!')", + "> console.log('Hello, \\", + "> world!')", + "> ```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "> ```js", + '> console.log("Hello, \\', + '> world!")', + '> console.log("Hello, \\', + '> world!")', + "> ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("multiline autofix in nested blockquote", async () => { + const input = [ + "This is Markdown.", + "", + "> This is a nested blockquote.", + ">", + "> > ```js", + "> > console.log('Hello, \\", + "> > new\\", + "> > world!')", + "> > console.log('Hello, \\", + "> > world!')", + "> > ```", + ].join("\n"); + + // The Markdown parser doesn't have any concept of a "negative" + // indent left of the opening code fence, so autofixes move + // lines that were previously underindented to the same level + // as the opening code fence. + const expected = [ + "This is Markdown.", + "", + "> This is a nested blockquote.", + ">", + "> > ```js", + '> > console.log("Hello, \\', + "> > new\\", + '> > world!")', + '> > console.log("Hello, \\', + '> > world!")', + "> > ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("by one space with comments", async () => { + const input = [ + "This is Markdown.", + "", + "", + "", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "", + "", + "", + " ```js", + ' console.log("Hello, world!");', + ' console.log("Hello, world!");', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("unevenly by two spaces with comments", async () => { + const input = [ + "This is Markdown.", + "", + "", + "", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "", + "", + "", + " ```js", + ' console.log("Hello, world!");', + ' console.log("Hello, world!");', + ' console.log("Hello, world!");', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + describe("inside a list", () => { + it("normally", async () => { + const input = [ + "- This is a Markdown list.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + const expected = [ + "- This is a Markdown list.", + "", + " ```js", + ' console.log("Hello, world!")', + ' console.log("Hello, world!")', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("by one space", async () => { + const input = [ + "- This is a Markdown list.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```", + ].join("\n"); + const expected = [ + "- This is a Markdown list.", + "", + " ```js", + ' console.log("Hello, world!")', + ' console.log("Hello, world!")', + " ```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + }); + }); + + it("with multiple rules", async () => { + const input = [ + "## Hello!", + "", + "", + "", + "```js", + "var obj = {", + " some: 'value'", + "}", + "", + "console.log('opop');", + "", + "function hello() {", + " return false", + "};", + "```", + ].join("\n"); + const expected = [ + "## Hello!", + "", + "", + "", + "```js", + "var obj = {", + ' some: "value"', + "};", + "", + 'console.log("opop");', + "", + "function hello() {", + " return false;", + "};", + "```", + ].join("\n"); + const results = await eslint.lintText(input, { + filePath: "test.md", + }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + }); + }); }); diff --git a/tests/processor.test.js b/tests/processor.test.js index d7d9dc65..92fc09cd 100644 --- a/tests/processor.test.js +++ b/tests/processor.test.js @@ -20,788 +20,815 @@ const __dirname = path.dirname(__filename); // Data //----------------------------------------------------------------------------- -const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../package.json"), "utf8")); +const pkg = JSON.parse( + fs.readFileSync(path.resolve(__dirname, "../package.json"), "utf8"), +); //----------------------------------------------------------------------------- // Tests //----------------------------------------------------------------------------- describe("processor", () => { - - describe("meta", () => { - it("should have meta property", () => { - assert.deepStrictEqual(processor.meta, { name: "@eslint/markdown/markdown", version: pkg.version }); - }); - }); - - describe("preprocess", () => { - - it("should not crash", () => { - processor.preprocess("Hello, world!"); - }); - - it("should not crash on an empty string", () => { - processor.preprocess(""); - }); - - it("should return an array", () => { - assert.isArray(processor.preprocess("Hello, world!")); - }); - - it("should ignore normal text", () => { - const blocks = processor.preprocess("Hello, world!"); - - assert.strictEqual(blocks.length, 0); - }); - - it("should ignore inline code", () => { - const blocks = processor.preprocess("Hello, `{{name}}!"); - - assert.strictEqual(blocks.length, 0); - }); - - it("should ignore space-indented code blocks", () => { - const code = [ - "Hello, world!", - " ", - " var answer = 6 * 7;", - " ", - "Goodbye" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 0); - }); - - it("should ignore 4-space-indented code fences", () => { - const code = [ - "Hello, world!", - " ```js", - " var answer = 6 * 7;", - " ```", - "Goodbye" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 0); - }); - - it("should ignore 4-space-indented fence ends", () => { - const code = [ - "Hello, world!", - "```js", - "var answer = 6 * 7;", - " ```", - "Goodbye" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 1); - assert.strictEqual(blocks[0].filename, "0.js"); - assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\n ```\nGoodbye\n"); - }); - - it("should ignore tab-indented code blocks", () => { - const code = [ - "Hello, world!", - "\t", - "\tvar answer = 6 * 7;", - "\t", - "Goodbye" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 0); - }); - - it("should terminate blocks at EOF", () => { - const code = [ - "Hello, world!", - "```js", - "var answer = 6 * 7;" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 1); - assert.strictEqual(blocks[0].filename, "0.js"); - assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\n"); - }); - - it("should allow backticks or tildes", () => { - const code = [ - "```js", - "backticks", - "```", - "~~~js", - "tildes", - "~~~" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 2); - assert.strictEqual(blocks[0].filename, "0.js"); - assert.strictEqual(blocks[0].text, "backticks\n"); - assert.strictEqual(blocks[1].filename, "1.js"); - assert.strictEqual(blocks[1].text, "tildes\n"); - }); - - it("should allow more than three fence characters", () => { - const code = [ - "````js", - "four", - "````" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 1); - assert.strictEqual(blocks[0].filename, "0.js"); - assert.strictEqual(blocks[0].text, "four\n"); - }); - - it("should require end fences at least as long as the starting fence", () => { - const code = [ - "````js", - "four", - "```", - "````", - "`````js", - "five", - "`````", - "``````js", - "six", - "```````" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 3); - assert.strictEqual(blocks[0].filename, "0.js"); - assert.strictEqual(blocks[0].text, "four\n```\n"); - assert.strictEqual(blocks[1].filename, "1.js"); - assert.strictEqual(blocks[1].text, "five\n"); - assert.strictEqual(blocks[2].filename, "2.js"); - assert.strictEqual(blocks[2].text, "six\n"); - }); - - it("should not allow other content on ending fence line", () => { - const code = [ - "```js", - "test();", - "``` end", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 1); - assert.strictEqual(blocks[0].filename, "0.js"); - assert.strictEqual(blocks[0].text, "test();\n``` end\n"); - }); - - it("should allow empty blocks", () => { - const code = [ - "```js", - "", - "````" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 1); - assert.strictEqual(blocks[0].filename, "0.js"); - assert.strictEqual(blocks[0].text, "\n"); - }); - - it("should allow whitespace-only blocks", () => { - const code = [ - " ```js", - "", - " ", - " ", - " ", - " ", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 1); - assert.strictEqual(blocks[0].filename, "0.js"); - assert.strictEqual(blocks[0].text, "\n\n\n \n \n"); - }); - - it("should preserve leading and trailing empty lines", () => { - const code = [ - "```js", - "", - "console.log(42);", - "", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks[0].filename, "0.js"); - assert.strictEqual(blocks[0].text, "\nconsole.log(42);\n\n"); - }); - - it("should ignore code fences with unspecified info string", () => { - const code = [ - "```", - "var answer = 6 * 7;", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 0); - }); - - it("should find code fences with js info string", () => { - const code = [ - "```js", - "var answer = 6 * 7;", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 1); - assert.strictEqual(blocks[0].filename, "0.js"); - }); - - it("should find code fences with javascript info string", () => { - const code = [ - "```javascript", - "var answer = 6 * 7;", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 1); - assert.strictEqual(blocks[0].filename, "0.js"); - }); - - it("should find code fences with node info string", () => { - const code = [ - "```node", - "var answer = 6 * 7;", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 1); - assert.strictEqual(blocks[0].filename, "0.node"); - }); - - it("should find code fences with jsx info string", () => { - const code = [ - "```jsx", - "var answer = 6 * 7;", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 1); - assert.strictEqual(blocks[0].filename, "0.jsx"); - }); - - it("should find code fences ignoring info string case", () => { - const code = [ - "```JavaScript", - "var answer = 6 * 7;", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 1); - assert.strictEqual(blocks[0].filename, "0.JavaScript"); - }); - - it("should ignore anything after the first word of the info string", () => { - const code = [ - "```js more words are ignored", - "var answer = 6 * 7;", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 1); - assert.strictEqual(blocks[0].filename, "0.js"); - }); - - it("should ignore leading whitespace in the info string", () => { - const code = [ - "``` js ignores leading whitespace", - "var answer = 6 * 7;", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 1); - assert.strictEqual(blocks[0].filename, "0.js"); - }); - - it("should ignore trailing whitespace in the info string", () => { - const code = [ - "```js ", - "var answer = 6 * 7;", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 1); - assert.strictEqual(blocks[0].filename, "0.js"); - }); - - it("should translate the language to its file extension with leading whitespace and trailing characters", () => { - const code = [ - "``` javascript CUSTOM", - "var answer = 6 * 7;", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 1); - assert.strictEqual(blocks[0].filename, "0.js"); - }); - - it("should find code fences not surrounded by blank lines", () => { - const code = [ - "", - "```js", - "var answer = 6 * 7;", - "```", - "Paragraph text", - "```js", - "var answer = 6 * 7;", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 2); - assert.strictEqual(blocks[0].filename, "0.js"); - assert.strictEqual(blocks[1].filename, "1.js"); - }); - - it("should return the source code in the block", () => { - const code = [ - "```js", - "var answer = 6 * 7;", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks[0].filename, "0.js"); - assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\n"); - }); - - it("should allow multi-line source code", () => { - const code = [ - "```js", - "var answer = 6 * 7;", - "console.log(answer);", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks[0].filename, "0.js"); - assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\nconsole.log(answer);\n"); - }); - - it("should preserve original line endings", () => { - const code = [ - "```js", - "var answer = 6 * 7;", - "console.log(answer);", - "```" - ].join("\r\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks[0].filename, "0.js"); - assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\r\nconsole.log(answer);\n"); - }); - - it("should unindent space-indented code fences", () => { - const code = [ - " ```js", - " var answer = 6 * 7;", - " console.log(answer);", - " // Fin.", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks[0].filename, "0.js"); - assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\n console.log(answer);\n// Fin.\n"); - }); - - it("should find multiple code fences", () => { - const code = [ - "Hello, world!", - "", - "```js", - "var answer = 6 * 7;", - "```", - "", - "```js", - "console.log(answer);", - "```", - "", - "Goodbye" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 2); - assert.strictEqual(blocks[0].filename, "0.js"); - assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\n"); - assert.strictEqual(blocks[1].filename, "1.js"); - assert.strictEqual(blocks[1].text, "console.log(answer);\n"); - }); - - it("should insert leading configuration comments", () => { - const code = [ - "", - "", - "", - "```js", - "alert('Hello, world!');", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 1); - assert.strictEqual(blocks[0].filename, "0.js"); - assert.strictEqual(blocks[0].text, [ - "/* eslint-env browser */", - "/*", - " eslint quotes: [", - " \"error\",", - " \"single\"", - " ]", - "*/", - "alert('Hello, world!');", - "" - ].join("\n")); - }); - - it("should insert global comments", () => { - const code = [ - "", - "", - "", - "```js", - "alert(foo, bar, baz);", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 1); - assert.strictEqual(blocks[0].filename, "0.js"); - assert.strictEqual(blocks[0].text, [ - "/* global foo */", - "/* global bar:false, baz:true */", - "alert(foo, bar, baz);", - "" - ].join("\n")); - }); - - // https://github.com/eslint/markdown/issues/76 - it("should insert comments inside list items", () => { - const code = [ - "* List item followed by a blank line", - "", - "", - "```js", - "console.log(\"Blank line\");", - "```", - "", - "* List item without a blank line", - "", - "```js", - "console.log(\"No blank line\");", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 2); - assert.strictEqual(blocks[0].text, [ - "/* eslint-disable no-console */", - "console.log(\"Blank line\");", - "" - ].join("\n")); - assert.strictEqual(blocks[1].text, [ - "/* eslint-disable no-console */", - "console.log(\"No blank line\");", - "" - ].join("\n")); - }); - - it("should ignore non-eslint comments", () => { - const code = [ - "", - "", - "", - "```js", - "alert('Hello, world!');", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 1); - assert.strictEqual(blocks[0].filename, "0.js"); - assert.strictEqual(blocks[0].text, [ - "alert('Hello, world!');", - "" - ].join("\n")); - }); - - it("should ignore non-comment html", () => { - const code = [ - "", - "

For example:

", - "", - "```js", - "alert('Hello, world!');", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 1); - assert.strictEqual(blocks[0].filename, "0.js"); - assert.strictEqual(blocks[0].text, [ - "alert('Hello, world!');", - "" - ].join("\n")); - }); - - describe("eslint-skip", () => { - - it("should skip the next block", () => { - const code = [ - "", - "", - "```js", - "alert('Hello, world!');", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 0); - }); - - it("should skip only one block", () => { - const code = [ - "", - "", - "```js", - "alert('Hello, world!');", - "```", - "", - "```js", - "var answer = 6 * 7;", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 1); - assert.strictEqual(blocks[0].filename, "0.js"); - assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\n"); - }); - - it("should still work surrounded by other comments", () => { - const code = [ - "", - "", - "", - "", - "```js", - "alert('Hello, world!');", - "```", - "", - "```js", - "var answer = 6 * 7;", - "```" - ].join("\n"); - const blocks = processor.preprocess(code); - - assert.strictEqual(blocks.length, 1); - assert.strictEqual(blocks[0].filename, "0.js"); - assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\n"); - }); - - }); - - }); - - describe("postprocess", () => { - const code = [ - "Hello, world!", - "", - "```js", - "var answer = 6 * 7;", - "if (answer === 42) {", - " console.log(answer);", - "}", - "```", - "", - "Let's make a list.", - "", - "1. First item", - "", - " ```JavaScript", - " var arr = [", - " 1,", - " 2", - " ];", - " ```", - "", - "1. Second item", - "", - " ```JS", - " function boolean(arg) {", - " \treturn", - " \t!!arg;", - "};", - " ```" - ].join("\n"); - const messages = [ - [ - { line: 1, endLine: 1, column: 1, message: "Use the global form of \"use strict\".", ruleId: "strict" }, - { line: 3, endLine: 3, column: 5, message: "Unexpected console statement.", ruleId: "no-console" } - ], [ - { line: 3, endLine: 3, column: 6, message: "Missing trailing comma.", ruleId: "comma-dangle", fix: { range: [24, 24], text: "," } } - ], [ - { line: 3, endLine: 6, column: 2, message: "Unreachable code after return.", ruleId: "no-unreachable" }, - { line: 4, endLine: 4, column: 2, message: "Unnecessary semicolon.", ruleId: "no-extra-semi", fix: { range: [38, 39], text: "" } } - ] - ]; - - beforeEach(() => { - processor.preprocess(code); - }); - - it("should allow for no messages", () => { - const result = processor.postprocess([[], [], []]); - - assert.strictEqual(result.length, 0); - }); - - it("should flatten messages", () => { - const result = processor.postprocess(messages); - - assert.strictEqual(result.length, 5); - assert.strictEqual(result[0].message, "Use the global form of \"use strict\"."); - assert.strictEqual(result[1].message, "Unexpected console statement."); - assert.strictEqual(result[2].message, "Missing trailing comma."); - assert.strictEqual(result[3].message, "Unreachable code after return."); - assert.strictEqual(result[4].message, "Unnecessary semicolon."); - }); - - it("should translate line numbers", () => { - const result = processor.postprocess(messages); - - assert.strictEqual(result[0].line, 4); - assert.strictEqual(result[1].line, 6); - assert.strictEqual(result[2].line, 17); - assert.strictEqual(result[3].line, 26); - assert.strictEqual(result[4].line, 27); - }); - - it("should translate endLine numbers", () => { - const result = processor.postprocess(messages); - - assert.strictEqual(result[0].endLine, 4); - assert.strictEqual(result[1].endLine, 6); - assert.strictEqual(result[2].endLine, 17); - assert.strictEqual(result[3].endLine, 29); - assert.strictEqual(result[4].endLine, 27); - }); - - it("should translate column numbers", () => { - const result = processor.postprocess(messages); - - assert.strictEqual(result[0].column, 1); - assert.strictEqual(result[1].column, 5); - }); - - it("should translate indented column numbers", () => { - const result = processor.postprocess(messages); - - assert.strictEqual(result[2].column, 9); - assert.strictEqual(result[3].column, 4); - assert.strictEqual(result[4].column, 2); - }); - - it("should adjust fix range properties", () => { - const result = processor.postprocess(messages); - - assert(result[2].fix.range, [185, 185]); - assert(result[4].fix.range, [264, 265]); - }); - - describe("should exclude messages from unsatisfiable rules", () => { - - it("eol-last", () => { - const result = processor.postprocess([ - [ - { line: 4, column: 3, message: "Newline required at end of file but not found.", ruleId: "eol-last" } - ] - ]); - - assert.strictEqual(result.length, 0); - }); - - it("unicode-bom", () => { - const result = processor.postprocess([ - [ - { line: 1, column: 1, message: "Expected Unicode BOM (Byte Order Mark).", ruleId: "unicode-bom" } - ] - ]); - - assert.strictEqual(result.length, 0); - }); - - }); - - it("should attach messages without `line` to opening code fence", () => { - const message = { message: "Parsing error: \"parserOptions.project\" has been set for @typescript-eslint/parser.", ruleId: null }; - const result = processor.postprocess([[message], [message], [message]]); - - assert.strictEqual(result.length, 3); - assert.deepStrictEqual(result[0], { - ...message, - line: 3, - column: 1 - }); - assert.deepStrictEqual(result[1], { - ...message, - line: 14, - column: 4 - }); - assert.deepStrictEqual(result[2], { - ...message, - line: 23, - column: 3 - }); - }); - - it("should ignore messages after the code block", () => { - const empty = [ - "```javascript", - "```" - ].join("\n"); - - processor.preprocess(empty, "empty.md"); - const message = { message: "Empty file", ruleId: null, line: 2 }; - const result = processor.postprocess([[message]], "empty.md"); - - assert.deepStrictEqual(result, []); - }); - }); - - describe("supportsAutofix", () => { - it("should equal true", () => { - assert.strictEqual(processor.supportsAutofix, true); - }); - }); - + describe("meta", () => { + it("should have meta property", () => { + assert.deepStrictEqual(processor.meta, { + name: "@eslint/markdown/markdown", + version: pkg.version, + }); + }); + }); + + describe("preprocess", () => { + it("should not crash", () => { + processor.preprocess("Hello, world!"); + }); + + it("should not crash on an empty string", () => { + processor.preprocess(""); + }); + + it("should return an array", () => { + assert.isArray(processor.preprocess("Hello, world!")); + }); + + it("should ignore normal text", () => { + const blocks = processor.preprocess("Hello, world!"); + + assert.strictEqual(blocks.length, 0); + }); + + it("should ignore inline code", () => { + const blocks = processor.preprocess("Hello, `{{name}}!"); + + assert.strictEqual(blocks.length, 0); + }); + + it("should ignore space-indented code blocks", () => { + const code = [ + "Hello, world!", + " ", + " var answer = 6 * 7;", + " ", + "Goodbye", + ].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 0); + }); + + it("should ignore 4-space-indented code fences", () => { + const code = [ + "Hello, world!", + " ```js", + " var answer = 6 * 7;", + " ```", + "Goodbye", + ].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 0); + }); + + it("should ignore 4-space-indented fence ends", () => { + const code = [ + "Hello, world!", + "```js", + "var answer = 6 * 7;", + " ```", + "Goodbye", + ].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 1); + assert.strictEqual(blocks[0].filename, "0.js"); + assert.strictEqual( + blocks[0].text, + "var answer = 6 * 7;\n ```\nGoodbye\n", + ); + }); + + it("should ignore tab-indented code blocks", () => { + const code = [ + "Hello, world!", + "\t", + "\tvar answer = 6 * 7;", + "\t", + "Goodbye", + ].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 0); + }); + + it("should terminate blocks at EOF", () => { + const code = ["Hello, world!", "```js", "var answer = 6 * 7;"].join( + "\n", + ); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 1); + assert.strictEqual(blocks[0].filename, "0.js"); + assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\n"); + }); + + it("should allow backticks or tildes", () => { + const code = [ + "```js", + "backticks", + "```", + "~~~js", + "tildes", + "~~~", + ].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 2); + assert.strictEqual(blocks[0].filename, "0.js"); + assert.strictEqual(blocks[0].text, "backticks\n"); + assert.strictEqual(blocks[1].filename, "1.js"); + assert.strictEqual(blocks[1].text, "tildes\n"); + }); + + it("should allow more than three fence characters", () => { + const code = ["````js", "four", "````"].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 1); + assert.strictEqual(blocks[0].filename, "0.js"); + assert.strictEqual(blocks[0].text, "four\n"); + }); + + it("should require end fences at least as long as the starting fence", () => { + const code = [ + "````js", + "four", + "```", + "````", + "`````js", + "five", + "`````", + "``````js", + "six", + "```````", + ].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 3); + assert.strictEqual(blocks[0].filename, "0.js"); + assert.strictEqual(blocks[0].text, "four\n```\n"); + assert.strictEqual(blocks[1].filename, "1.js"); + assert.strictEqual(blocks[1].text, "five\n"); + assert.strictEqual(blocks[2].filename, "2.js"); + assert.strictEqual(blocks[2].text, "six\n"); + }); + + it("should not allow other content on ending fence line", () => { + const code = ["```js", "test();", "``` end", "```"].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 1); + assert.strictEqual(blocks[0].filename, "0.js"); + assert.strictEqual(blocks[0].text, "test();\n``` end\n"); + }); + + it("should allow empty blocks", () => { + const code = ["```js", "", "````"].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 1); + assert.strictEqual(blocks[0].filename, "0.js"); + assert.strictEqual(blocks[0].text, "\n"); + }); + + it("should allow whitespace-only blocks", () => { + const code = [" ```js", "", " ", " ", " ", " ", "```"].join( + "\n", + ); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 1); + assert.strictEqual(blocks[0].filename, "0.js"); + assert.strictEqual(blocks[0].text, "\n\n\n \n \n"); + }); + + it("should preserve leading and trailing empty lines", () => { + const code = ["```js", "", "console.log(42);", "", "```"].join( + "\n", + ); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks[0].filename, "0.js"); + assert.strictEqual(blocks[0].text, "\nconsole.log(42);\n\n"); + }); + + it("should ignore code fences with unspecified info string", () => { + const code = ["```", "var answer = 6 * 7;", "```"].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 0); + }); + + it("should find code fences with js info string", () => { + const code = ["```js", "var answer = 6 * 7;", "```"].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 1); + assert.strictEqual(blocks[0].filename, "0.js"); + }); + + it("should find code fences with javascript info string", () => { + const code = ["```javascript", "var answer = 6 * 7;", "```"].join( + "\n", + ); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 1); + assert.strictEqual(blocks[0].filename, "0.js"); + }); + + it("should find code fences with node info string", () => { + const code = ["```node", "var answer = 6 * 7;", "```"].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 1); + assert.strictEqual(blocks[0].filename, "0.node"); + }); + + it("should find code fences with jsx info string", () => { + const code = ["```jsx", "var answer = 6 * 7;", "```"].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 1); + assert.strictEqual(blocks[0].filename, "0.jsx"); + }); + + it("should find code fences ignoring info string case", () => { + const code = ["```JavaScript", "var answer = 6 * 7;", "```"].join( + "\n", + ); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 1); + assert.strictEqual(blocks[0].filename, "0.JavaScript"); + }); + + it("should ignore anything after the first word of the info string", () => { + const code = [ + "```js more words are ignored", + "var answer = 6 * 7;", + "```", + ].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 1); + assert.strictEqual(blocks[0].filename, "0.js"); + }); + + it("should ignore leading whitespace in the info string", () => { + const code = [ + "``` js ignores leading whitespace", + "var answer = 6 * 7;", + "```", + ].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 1); + assert.strictEqual(blocks[0].filename, "0.js"); + }); + + it("should ignore trailing whitespace in the info string", () => { + const code = ["```js ", "var answer = 6 * 7;", "```"].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 1); + assert.strictEqual(blocks[0].filename, "0.js"); + }); + + it("should translate the language to its file extension with leading whitespace and trailing characters", () => { + const code = [ + "``` javascript CUSTOM", + "var answer = 6 * 7;", + "```", + ].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 1); + assert.strictEqual(blocks[0].filename, "0.js"); + }); + + it("should find code fences not surrounded by blank lines", () => { + const code = [ + "", + "```js", + "var answer = 6 * 7;", + "```", + "Paragraph text", + "```js", + "var answer = 6 * 7;", + "```", + ].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 2); + assert.strictEqual(blocks[0].filename, "0.js"); + assert.strictEqual(blocks[1].filename, "1.js"); + }); + + it("should return the source code in the block", () => { + const code = ["```js", "var answer = 6 * 7;", "```"].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks[0].filename, "0.js"); + assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\n"); + }); + + it("should allow multi-line source code", () => { + const code = [ + "```js", + "var answer = 6 * 7;", + "console.log(answer);", + "```", + ].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks[0].filename, "0.js"); + assert.strictEqual( + blocks[0].text, + "var answer = 6 * 7;\nconsole.log(answer);\n", + ); + }); + + it("should preserve original line endings", () => { + const code = [ + "```js", + "var answer = 6 * 7;", + "console.log(answer);", + "```", + ].join("\r\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks[0].filename, "0.js"); + assert.strictEqual( + blocks[0].text, + "var answer = 6 * 7;\r\nconsole.log(answer);\n", + ); + }); + + it("should unindent space-indented code fences", () => { + const code = [ + " ```js", + " var answer = 6 * 7;", + " console.log(answer);", + " // Fin.", + "```", + ].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks[0].filename, "0.js"); + assert.strictEqual( + blocks[0].text, + "var answer = 6 * 7;\n console.log(answer);\n// Fin.\n", + ); + }); + + it("should find multiple code fences", () => { + const code = [ + "Hello, world!", + "", + "```js", + "var answer = 6 * 7;", + "```", + "", + "```js", + "console.log(answer);", + "```", + "", + "Goodbye", + ].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 2); + assert.strictEqual(blocks[0].filename, "0.js"); + assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\n"); + assert.strictEqual(blocks[1].filename, "1.js"); + assert.strictEqual(blocks[1].text, "console.log(answer);\n"); + }); + + it("should insert leading configuration comments", () => { + const code = [ + "", + "", + "", + "```js", + "alert('Hello, world!');", + "```", + ].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 1); + assert.strictEqual(blocks[0].filename, "0.js"); + assert.strictEqual( + blocks[0].text, + [ + "/* eslint-env browser */", + "/*", + " eslint quotes: [", + ' "error",', + ' "single"', + " ]", + "*/", + "alert('Hello, world!');", + "", + ].join("\n"), + ); + }); + + it("should insert global comments", () => { + const code = [ + "", + "", + "", + "```js", + "alert(foo, bar, baz);", + "```", + ].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 1); + assert.strictEqual(blocks[0].filename, "0.js"); + assert.strictEqual( + blocks[0].text, + [ + "/* global foo */", + "/* global bar:false, baz:true */", + "alert(foo, bar, baz);", + "", + ].join("\n"), + ); + }); + + // https://github.com/eslint/markdown/issues/76 + it("should insert comments inside list items", () => { + const code = [ + "* List item followed by a blank line", + "", + "", + "```js", + 'console.log("Blank line");', + "```", + "", + "* List item without a blank line", + "", + "```js", + 'console.log("No blank line");', + "```", + ].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 2); + assert.strictEqual( + blocks[0].text, + [ + "/* eslint-disable no-console */", + 'console.log("Blank line");', + "", + ].join("\n"), + ); + assert.strictEqual( + blocks[1].text, + [ + "/* eslint-disable no-console */", + 'console.log("No blank line");', + "", + ].join("\n"), + ); + }); + + it("should ignore non-eslint comments", () => { + const code = [ + "", + "", + "", + "```js", + "alert('Hello, world!');", + "```", + ].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 1); + assert.strictEqual(blocks[0].filename, "0.js"); + assert.strictEqual( + blocks[0].text, + ["alert('Hello, world!');", ""].join("\n"), + ); + }); + + it("should ignore non-comment html", () => { + const code = [ + "", + "

For example:

", + "", + "```js", + "alert('Hello, world!');", + "```", + ].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 1); + assert.strictEqual(blocks[0].filename, "0.js"); + assert.strictEqual( + blocks[0].text, + ["alert('Hello, world!');", ""].join("\n"), + ); + }); + + describe("eslint-skip", () => { + it("should skip the next block", () => { + const code = [ + "", + "", + "```js", + "alert('Hello, world!');", + "```", + ].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 0); + }); + + it("should skip only one block", () => { + const code = [ + "", + "", + "```js", + "alert('Hello, world!');", + "```", + "", + "```js", + "var answer = 6 * 7;", + "```", + ].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 1); + assert.strictEqual(blocks[0].filename, "0.js"); + assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\n"); + }); + + it("should still work surrounded by other comments", () => { + const code = [ + "", + "", + "", + "", + "```js", + "alert('Hello, world!');", + "```", + "", + "```js", + "var answer = 6 * 7;", + "```", + ].join("\n"); + const blocks = processor.preprocess(code); + + assert.strictEqual(blocks.length, 1); + assert.strictEqual(blocks[0].filename, "0.js"); + assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\n"); + }); + }); + }); + + describe("postprocess", () => { + const code = [ + "Hello, world!", + "", + "```js", + "var answer = 6 * 7;", + "if (answer === 42) {", + " console.log(answer);", + "}", + "```", + "", + "Let's make a list.", + "", + "1. First item", + "", + " ```JavaScript", + " var arr = [", + " 1,", + " 2", + " ];", + " ```", + "", + "1. Second item", + "", + " ```JS", + " function boolean(arg) {", + " \treturn", + " \t!!arg;", + "};", + " ```", + ].join("\n"); + const messages = [ + [ + { + line: 1, + endLine: 1, + column: 1, + message: 'Use the global form of "use strict".', + ruleId: "strict", + }, + { + line: 3, + endLine: 3, + column: 5, + message: "Unexpected console statement.", + ruleId: "no-console", + }, + ], + [ + { + line: 3, + endLine: 3, + column: 6, + message: "Missing trailing comma.", + ruleId: "comma-dangle", + fix: { range: [24, 24], text: "," }, + }, + ], + [ + { + line: 3, + endLine: 6, + column: 2, + message: "Unreachable code after return.", + ruleId: "no-unreachable", + }, + { + line: 4, + endLine: 4, + column: 2, + message: "Unnecessary semicolon.", + ruleId: "no-extra-semi", + fix: { range: [38, 39], text: "" }, + }, + ], + ]; + + beforeEach(() => { + processor.preprocess(code); + }); + + it("should allow for no messages", () => { + const result = processor.postprocess([[], [], []]); + + assert.strictEqual(result.length, 0); + }); + + it("should flatten messages", () => { + const result = processor.postprocess(messages); + + assert.strictEqual(result.length, 5); + assert.strictEqual( + result[0].message, + 'Use the global form of "use strict".', + ); + assert.strictEqual( + result[1].message, + "Unexpected console statement.", + ); + assert.strictEqual(result[2].message, "Missing trailing comma."); + assert.strictEqual( + result[3].message, + "Unreachable code after return.", + ); + assert.strictEqual(result[4].message, "Unnecessary semicolon."); + }); + + it("should translate line numbers", () => { + const result = processor.postprocess(messages); + + assert.strictEqual(result[0].line, 4); + assert.strictEqual(result[1].line, 6); + assert.strictEqual(result[2].line, 17); + assert.strictEqual(result[3].line, 26); + assert.strictEqual(result[4].line, 27); + }); + + it("should translate endLine numbers", () => { + const result = processor.postprocess(messages); + + assert.strictEqual(result[0].endLine, 4); + assert.strictEqual(result[1].endLine, 6); + assert.strictEqual(result[2].endLine, 17); + assert.strictEqual(result[3].endLine, 29); + assert.strictEqual(result[4].endLine, 27); + }); + + it("should translate column numbers", () => { + const result = processor.postprocess(messages); + + assert.strictEqual(result[0].column, 1); + assert.strictEqual(result[1].column, 5); + }); + + it("should translate indented column numbers", () => { + const result = processor.postprocess(messages); + + assert.strictEqual(result[2].column, 9); + assert.strictEqual(result[3].column, 4); + assert.strictEqual(result[4].column, 2); + }); + + it("should adjust fix range properties", () => { + const result = processor.postprocess(messages); + + assert(result[2].fix.range, [185, 185]); + assert(result[4].fix.range, [264, 265]); + }); + + describe("should exclude messages from unsatisfiable rules", () => { + it("eol-last", () => { + const result = processor.postprocess([ + [ + { + line: 4, + column: 3, + message: + "Newline required at end of file but not found.", + ruleId: "eol-last", + }, + ], + ]); + + assert.strictEqual(result.length, 0); + }); + + it("unicode-bom", () => { + const result = processor.postprocess([ + [ + { + line: 1, + column: 1, + message: "Expected Unicode BOM (Byte Order Mark).", + ruleId: "unicode-bom", + }, + ], + ]); + + assert.strictEqual(result.length, 0); + }); + }); + + it("should attach messages without `line` to opening code fence", () => { + const message = { + message: + 'Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser.', + ruleId: null, + }; + const result = processor.postprocess([ + [message], + [message], + [message], + ]); + + assert.strictEqual(result.length, 3); + assert.deepStrictEqual(result[0], { + ...message, + line: 3, + column: 1, + }); + assert.deepStrictEqual(result[1], { + ...message, + line: 14, + column: 4, + }); + assert.deepStrictEqual(result[2], { + ...message, + line: 23, + column: 3, + }); + }); + + it("should ignore messages after the code block", () => { + const empty = ["```javascript", "```"].join("\n"); + + processor.preprocess(empty, "empty.md"); + const message = { message: "Empty file", ruleId: null, line: 2 }; + const result = processor.postprocess([[message]], "empty.md"); + + assert.deepStrictEqual(result, []); + }); + }); + + describe("supportsAutofix", () => { + it("should equal true", () => { + assert.strictEqual(processor.supportsAutofix, true); + }); + }); }); diff --git a/tests/rules/fenced-code-language.test.js b/tests/rules/fenced-code-language.test.js index 184674bb..985b3094 100644 --- a/tests/rules/fenced-code-language.test.js +++ b/tests/rules/fenced-code-language.test.js @@ -16,64 +16,61 @@ import { RuleTester } from "eslint"; //------------------------------------------------------------------------------ const ruleTester = new RuleTester({ - plugins: { - markdown - }, - language: "markdown/commonmark" + plugins: { + markdown, + }, + language: "markdown/commonmark", }); ruleTester.run("fenced-code-language", rule, { - valid: [ - `\`\`\`js + valid: [ + `\`\`\`js console.log("Hello, world!"); \`\`\``, - `\`\`\`javascript + `\`\`\`javascript console.log("Hello, world!"); \`\`\``, - // indented code block - ` + // indented code block + ` console.log("Hello, world!"); `, - { - code: - `\`\`\`js + { + code: `\`\`\`js console.log("Hello, world!"); \`\`\``, - options: [{ required: ["js"] }] - } - ], - invalid: [ - { - code: - `\`\`\` + options: [{ required: ["js"] }], + }, + ], + invalid: [ + { + code: `\`\`\` console.log("Hello, world!"); \`\`\``, - errors: [ - { - messageId: "missingLanguage", - line: 1, - column: 1, - endLine: 3, - endColumn: 20 - } - ] - }, - { - code: - `\`\`\`javascript + errors: [ + { + messageId: "missingLanguage", + line: 1, + column: 1, + endLine: 3, + endColumn: 20, + }, + ], + }, + { + code: `\`\`\`javascript console.log("Hello, world!"); \`\`\``, - options: [{ required: ["js"] }], - errors: [ - { - messageId: "disallowedLanguage", - line: 1, - column: 1, - endLine: 3, - endColumn: 20 - } - ] - } - ] + options: [{ required: ["js"] }], + errors: [ + { + messageId: "disallowedLanguage", + line: 1, + column: 1, + endLine: 3, + endColumn: 20, + }, + ], + }, + ], }); diff --git a/tests/rules/heading-increment.test.js b/tests/rules/heading-increment.test.js index e35a0c61..8c4c7b0b 100644 --- a/tests/rules/heading-increment.test.js +++ b/tests/rules/heading-increment.test.js @@ -17,84 +17,84 @@ import dedent from "dedent"; //------------------------------------------------------------------------------ const ruleTester = new RuleTester({ - plugins: { - markdown - }, - language: "markdown/commonmark" + plugins: { + markdown, + }, + language: "markdown/commonmark", }); ruleTester.run("heading-increment", rule, { - valid: [ - "# Heading 1", - "## Heading 2", - dedent`# Heading 1 + valid: [ + "# Heading 1", + "## Heading 2", + dedent`# Heading 1 ## Heading 2`, - dedent`# Heading 1 + dedent`# Heading 1 - # Heading 2` - ], - invalid: [ - { - code: dedent` + # Heading 2`, + ], + invalid: [ + { + code: dedent` # Heading 1 ### Heading 3 `, - errors: [ - { - messageId: "skippedHeading", - line: 3, - column: 1, - endLine: 3, - endColumn: 14, - data: { - fromLevel: 1, - toLevel: 3 - } - } - ] - }, - { - code: dedent` + errors: [ + { + messageId: "skippedHeading", + line: 3, + column: 1, + endLine: 3, + endColumn: 14, + data: { + fromLevel: 1, + toLevel: 3, + }, + }, + ], + }, + { + code: dedent` ## Heading 2 ##### Heading 5 `, - errors: [ - { - messageId: "skippedHeading", - line: 3, - column: 1, - endLine: 3, - endColumn: 16, - data: { - fromLevel: 2, - toLevel: 5 - } - } - ] - }, - { - code: dedent` + errors: [ + { + messageId: "skippedHeading", + line: 3, + column: 1, + endLine: 3, + endColumn: 16, + data: { + fromLevel: 2, + toLevel: 5, + }, + }, + ], + }, + { + code: dedent` Heading 1 ========= ### Heading 3 `, - errors: [ - { - messageId: "skippedHeading", - line: 4, - column: 1, - endLine: 4, - endColumn: 14, - data: { - fromLevel: 1, - toLevel: 3 - } - } - ] - } - ] + errors: [ + { + messageId: "skippedHeading", + line: 4, + column: 1, + endLine: 4, + endColumn: 14, + data: { + fromLevel: 1, + toLevel: 3, + }, + }, + ], + }, + ], }); diff --git a/tests/rules/no-duplicate-headings.test.js b/tests/rules/no-duplicate-headings.test.js index 60e328f2..a3473717 100644 --- a/tests/rules/no-duplicate-headings.test.js +++ b/tests/rules/no-duplicate-headings.test.js @@ -16,84 +16,84 @@ import { RuleTester } from "eslint"; //------------------------------------------------------------------------------ const ruleTester = new RuleTester({ - plugins: { - markdown - }, - language: "markdown/commonmark" + plugins: { + markdown, + }, + language: "markdown/commonmark", }); ruleTester.run("no-duplicate-headings", rule, { - valid: [ - `# Heading 1 + valid: [ + `# Heading 1 - ## Heading 2` - ], - invalid: [ - { - code: ` + ## Heading 2`, + ], + invalid: [ + { + code: ` # Heading 1 # Heading 1 `, - errors: [ - { - messageId: "duplicateHeading", - line: 4, - column: 1, - endLine: 4, - endColumn: 12 - } - ] - }, - { - code: ` + errors: [ + { + messageId: "duplicateHeading", + line: 4, + column: 1, + endLine: 4, + endColumn: 12, + }, + ], + }, + { + code: ` # Heading 1 ## Heading 1 `, - errors: [ - { - messageId: "duplicateHeading", - line: 4, - column: 1, - endLine: 4, - endColumn: 13 - } - ] - }, - { - code: ` + errors: [ + { + messageId: "duplicateHeading", + line: 4, + column: 1, + endLine: 4, + endColumn: 13, + }, + ], + }, + { + code: ` # Heading 1 Heading 1 --------- `, - errors: [ - { - messageId: "duplicateHeading", - line: 4, - column: 1, - endLine: 5, - endColumn: 10 - } - ] - }, - { - code: ` + errors: [ + { + messageId: "duplicateHeading", + line: 4, + column: 1, + endLine: 5, + endColumn: 10, + }, + ], + }, + { + code: ` # Heading 1 Heading 1 ========= `, - errors: [ - { - messageId: "duplicateHeading", - line: 4, - column: 1, - endLine: 5, - endColumn: 10 - } - ] - } - ] + errors: [ + { + messageId: "duplicateHeading", + line: 4, + column: 1, + endLine: 5, + endColumn: 10, + }, + ], + }, + ], }); diff --git a/tests/rules/no-empty-links.test.js b/tests/rules/no-empty-links.test.js index 9ea02558..75e950da 100644 --- a/tests/rules/no-empty-links.test.js +++ b/tests/rules/no-empty-links.test.js @@ -16,54 +16,50 @@ import { RuleTester } from "eslint"; //------------------------------------------------------------------------------ const ruleTester = new RuleTester({ - plugins: { - markdown - }, - language: "markdown/commonmark" + plugins: { + markdown, + }, + language: "markdown/commonmark", }); ruleTester.run("no-empty-links", rule, { - valid: [ - "[foo](bar)", - "[foo](#bar)", - "[foo](http://bar.com)" - ], - invalid: [ - { - code: "[foo]()", - errors: [ - { - messageId: "emptyLink", - line: 1, - column: 1, - endLine: 1, - endColumn: 8 - } - ] - }, - { - code: "[foo](#)", - errors: [ - { - messageId: "emptyLink", - line: 1, - column: 1, - endLine: 1, - endColumn: 9 - } - ] - }, - { - code: "[foo]( )", - errors: [ - { - messageId: "emptyLink", - line: 1, - column: 1, - endLine: 1, - endColumn: 9 - } - ] - } - ] + valid: ["[foo](bar)", "[foo](#bar)", "[foo](http://bar.com)"], + invalid: [ + { + code: "[foo]()", + errors: [ + { + messageId: "emptyLink", + line: 1, + column: 1, + endLine: 1, + endColumn: 8, + }, + ], + }, + { + code: "[foo](#)", + errors: [ + { + messageId: "emptyLink", + line: 1, + column: 1, + endLine: 1, + endColumn: 9, + }, + ], + }, + { + code: "[foo]( )", + errors: [ + { + messageId: "emptyLink", + line: 1, + column: 1, + endLine: 1, + endColumn: 9, + }, + ], + }, + ], }); diff --git a/tests/rules/no-html.test.js b/tests/rules/no-html.test.js index b3269004..4ed85a9b 100644 --- a/tests/rules/no-html.test.js +++ b/tests/rules/no-html.test.js @@ -17,76 +17,76 @@ import dedent from "dedent"; //------------------------------------------------------------------------------ const ruleTester = new RuleTester({ - plugins: { - markdown - }, - language: "markdown/commonmark" + plugins: { + markdown, + }, + language: "markdown/commonmark", }); ruleTester.run("no-html", rule, { - valid: [ - "Hello world!", - " 1 < 5", - "", - dedent`\`\`\`html + valid: [ + "Hello world!", + " 1 < 5", + "", + dedent`\`\`\`html Hello world! \`\`\``, - { - code: "Hello world!", - options: [{ allowed: ["b"] }] - }, - { - code: "Hello world!", - options: [{ allowed: ["custom-element"] }] - } - ], - invalid: [ - { - code: "Hello world!", - errors: [ - { - messageId: "disallowedElement", - line: 1, - column: 1, - endLine: 1, - endColumn: 4, - data: { - name: "b" - } - } - ] - }, - { - code: "Hello world!", - options: [{ allowed: ["em"] }], - errors: [ - { - messageId: "disallowedElement", - line: 1, - column: 1, - endLine: 1, - endColumn: 4, - data: { - name: "b" - } - } - ] - }, - { - code: "Hello world!", - options: [{ allowed: ["em"] }], - errors: [ - { - messageId: "disallowedElement", - line: 1, - column: 1, - endLine: 1, - endColumn: 17, - data: { - name: "custom-element" - } - } - ] - } - ] + { + code: "Hello world!", + options: [{ allowed: ["b"] }], + }, + { + code: "Hello world!", + options: [{ allowed: ["custom-element"] }], + }, + ], + invalid: [ + { + code: "Hello world!", + errors: [ + { + messageId: "disallowedElement", + line: 1, + column: 1, + endLine: 1, + endColumn: 4, + data: { + name: "b", + }, + }, + ], + }, + { + code: "Hello world!", + options: [{ allowed: ["em"] }], + errors: [ + { + messageId: "disallowedElement", + line: 1, + column: 1, + endLine: 1, + endColumn: 4, + data: { + name: "b", + }, + }, + ], + }, + { + code: "Hello world!", + options: [{ allowed: ["em"] }], + errors: [ + { + messageId: "disallowedElement", + line: 1, + column: 1, + endLine: 1, + endColumn: 17, + data: { + name: "custom-element", + }, + }, + ], + }, + ], }); diff --git a/tests/rules/no-invalid-label-refs.test.js b/tests/rules/no-invalid-label-refs.test.js index ea3e289a..c259501c 100644 --- a/tests/rules/no-invalid-label-refs.test.js +++ b/tests/rules/no-invalid-label-refs.test.js @@ -16,116 +16,116 @@ import { RuleTester } from "eslint"; //------------------------------------------------------------------------------ const ruleTester = new RuleTester({ - plugins: { - markdown - }, - language: "markdown/commonmark" + plugins: { + markdown, + }, + language: "markdown/commonmark", }); ruleTester.run("no-invalid-label-refs", rule, { - valid: [ - "[*foo*]", - "[foo]\n\n[foo]: http://bar.com", - "[foo][ foo ]\n\n[foo]: http://bar.com", - "![foo][foo]\n\n[foo]: http://bar.com/image.jpg", - "[foo][]\n\n[foo]: http://bar.com/image.jpg", - "![foo][]\n\n[foo]: http://bar.com/image.jpg", - "[ foo ][]\n\n[foo]: http://bar.com/image.jpg" - ], - invalid: [ - { - code: "[foo][ ]\n\n[foo]: http://bar.com/image.jpg", - errors: [ - { - messageId: "invalidLabelRef", - data: { label: "foo" }, - line: 1, - column: 6, - endLine: 1, - endColumn: 9 - } - ] - }, - { - code: "![foo][ ]\n\n[foo]: http://bar.com/image.jpg", - errors: [ - { - messageId: "invalidLabelRef", - data: { label: "foo" }, - line: 1, - column: 7, - endLine: 1, - endColumn: 10 - } - ] - }, - { - code: "[\nfoo\n][\n]\n\n[foo]: http://bar.com/image.jpg", - errors: [ - { - messageId: "invalidLabelRef", - data: { label: "foo" }, - line: 3, - column: 2, - endLine: 4, - endColumn: 1 - } - ] - }, - { - code: "[foo][ ]\n[bar][ ]\n\n[foo]: http://foo.com\n[bar]: http://bar.com", - errors: [ - { - messageId: "invalidLabelRef", - data: { label: "foo" }, - line: 1, - column: 6, - endLine: 1, - endColumn: 9 - }, - { - messageId: "invalidLabelRef", - data: { label: "bar" }, - line: 2, - column: 6, - endLine: 2, - endColumn: 9 - } - ] - }, - { - code: "[foo][ ]\n![bar][ ]\n\n[foo]: http://foo.com\n[bar]: http://bar.com", - errors: [ - { - messageId: "invalidLabelRef", - data: { label: "foo" }, - line: 1, - column: 6, - endLine: 1, - endColumn: 9 - }, - { - messageId: "invalidLabelRef", - data: { label: "bar" }, - line: 2, - column: 7, - endLine: 2, - endColumn: 10 - } - ] - }, - { - code: "- - - [foo][ ]\n\n[foo]: http://foo.com", - errors: [ - { - messageId: "invalidLabelRef", - data: { label: "foo" }, - line: 1, - column: 12, - endLine: 1, - endColumn: 15 - } - ] - } - ] + valid: [ + "[*foo*]", + "[foo]\n\n[foo]: http://bar.com", + "[foo][ foo ]\n\n[foo]: http://bar.com", + "![foo][foo]\n\n[foo]: http://bar.com/image.jpg", + "[foo][]\n\n[foo]: http://bar.com/image.jpg", + "![foo][]\n\n[foo]: http://bar.com/image.jpg", + "[ foo ][]\n\n[foo]: http://bar.com/image.jpg", + ], + invalid: [ + { + code: "[foo][ ]\n\n[foo]: http://bar.com/image.jpg", + errors: [ + { + messageId: "invalidLabelRef", + data: { label: "foo" }, + line: 1, + column: 6, + endLine: 1, + endColumn: 9, + }, + ], + }, + { + code: "![foo][ ]\n\n[foo]: http://bar.com/image.jpg", + errors: [ + { + messageId: "invalidLabelRef", + data: { label: "foo" }, + line: 1, + column: 7, + endLine: 1, + endColumn: 10, + }, + ], + }, + { + code: "[\nfoo\n][\n]\n\n[foo]: http://bar.com/image.jpg", + errors: [ + { + messageId: "invalidLabelRef", + data: { label: "foo" }, + line: 3, + column: 2, + endLine: 4, + endColumn: 1, + }, + ], + }, + { + code: "[foo][ ]\n[bar][ ]\n\n[foo]: http://foo.com\n[bar]: http://bar.com", + errors: [ + { + messageId: "invalidLabelRef", + data: { label: "foo" }, + line: 1, + column: 6, + endLine: 1, + endColumn: 9, + }, + { + messageId: "invalidLabelRef", + data: { label: "bar" }, + line: 2, + column: 6, + endLine: 2, + endColumn: 9, + }, + ], + }, + { + code: "[foo][ ]\n![bar][ ]\n\n[foo]: http://foo.com\n[bar]: http://bar.com", + errors: [ + { + messageId: "invalidLabelRef", + data: { label: "foo" }, + line: 1, + column: 6, + endLine: 1, + endColumn: 9, + }, + { + messageId: "invalidLabelRef", + data: { label: "bar" }, + line: 2, + column: 7, + endLine: 2, + endColumn: 10, + }, + ], + }, + { + code: "- - - [foo][ ]\n\n[foo]: http://foo.com", + errors: [ + { + messageId: "invalidLabelRef", + data: { label: "foo" }, + line: 1, + column: 12, + endLine: 1, + endColumn: 15, + }, + ], + }, + ], }); diff --git a/tests/rules/no-missing-label-refs.test.js b/tests/rules/no-missing-label-refs.test.js index f6328f1f..5c221aae 100644 --- a/tests/rules/no-missing-label-refs.test.js +++ b/tests/rules/no-missing-label-refs.test.js @@ -16,138 +16,138 @@ import { RuleTester } from "eslint"; //------------------------------------------------------------------------------ const ruleTester = new RuleTester({ - plugins: { - markdown - }, - language: "markdown/commonmark" + plugins: { + markdown, + }, + language: "markdown/commonmark", }); ruleTester.run("no-missing-label-refs", rule, { - valid: [ - "[*foo*]", - "[foo]\n\n[foo]: http://bar.com", - "[foo][foo]\n\n[foo]: http://bar.com", - "[foo][foo]\n\n[ foo ]: http://bar.com", - "[foo][ foo ]\n\n[ foo ]: http://bar.com", - "![foo][foo]\n\n[foo]: http://bar.com/image.jpg", - "[foo][]\n\n[foo]: http://bar.com/image.jpg", - "![foo][]\n\n[foo]: http://bar.com/image.jpg", - "[ foo ][]\n\n[foo]: http://bar.com/image.jpg", - "[foo][ ]\n\n[foo]: http://bar.com/image.jpg", - "[\nfoo\n][\n]\n\n[foo]: http://bar.com/image.jpg" - ], - invalid: [ - { - code: "[foo][bar]", - errors: [ - { - messageId: "notFound", - data: { label: "bar" }, - line: 1, - column: 7, - endLine: 1, - endColumn: 10 - } - ] - }, - { - code: "![foo][bar]", - errors: [ - { - messageId: "notFound", - data: { label: "bar" }, - line: 1, - column: 8, - endLine: 1, - endColumn: 11 - } - ] - }, - { - code: "[foo][]", - errors: [ - { - messageId: "notFound", - data: { label: "foo" }, - line: 1, - column: 2, - endLine: 1, - endColumn: 5 - } - ] - }, - { - code: "![foo][]", - errors: [ - { - messageId: "notFound", - data: { label: "foo" }, - line: 1, - column: 3, - endLine: 1, - endColumn: 6 - } - ] - }, - { - code: "[foo]", - errors: [ - { - messageId: "notFound", - data: { label: "foo" }, - line: 1, - column: 2, - endLine: 1, - endColumn: 5 - } - ] - }, - { - code: "![foo]", - errors: [ - { - messageId: "notFound", - data: { label: "foo" }, - line: 1, - column: 3, - endLine: 1, - endColumn: 6 - } - ] - }, - { - code: "[foo]\n[bar]", - errors: [ - { - messageId: "notFound", - data: { label: "foo" }, - line: 1, - column: 2, - endLine: 1, - endColumn: 5 - }, - { - messageId: "notFound", - data: { label: "bar" }, - line: 2, - column: 2, - endLine: 2, - endColumn: 5 - } - ] - }, - { - code: "- - - [foo]", - errors: [ - { - messageId: "notFound", - data: { label: "foo" }, - line: 1, - column: 8, - endLine: 1, - endColumn: 11 - } - ] - } - ] + valid: [ + "[*foo*]", + "[foo]\n\n[foo]: http://bar.com", + "[foo][foo]\n\n[foo]: http://bar.com", + "[foo][foo]\n\n[ foo ]: http://bar.com", + "[foo][ foo ]\n\n[ foo ]: http://bar.com", + "![foo][foo]\n\n[foo]: http://bar.com/image.jpg", + "[foo][]\n\n[foo]: http://bar.com/image.jpg", + "![foo][]\n\n[foo]: http://bar.com/image.jpg", + "[ foo ][]\n\n[foo]: http://bar.com/image.jpg", + "[foo][ ]\n\n[foo]: http://bar.com/image.jpg", + "[\nfoo\n][\n]\n\n[foo]: http://bar.com/image.jpg", + ], + invalid: [ + { + code: "[foo][bar]", + errors: [ + { + messageId: "notFound", + data: { label: "bar" }, + line: 1, + column: 7, + endLine: 1, + endColumn: 10, + }, + ], + }, + { + code: "![foo][bar]", + errors: [ + { + messageId: "notFound", + data: { label: "bar" }, + line: 1, + column: 8, + endLine: 1, + endColumn: 11, + }, + ], + }, + { + code: "[foo][]", + errors: [ + { + messageId: "notFound", + data: { label: "foo" }, + line: 1, + column: 2, + endLine: 1, + endColumn: 5, + }, + ], + }, + { + code: "![foo][]", + errors: [ + { + messageId: "notFound", + data: { label: "foo" }, + line: 1, + column: 3, + endLine: 1, + endColumn: 6, + }, + ], + }, + { + code: "[foo]", + errors: [ + { + messageId: "notFound", + data: { label: "foo" }, + line: 1, + column: 2, + endLine: 1, + endColumn: 5, + }, + ], + }, + { + code: "![foo]", + errors: [ + { + messageId: "notFound", + data: { label: "foo" }, + line: 1, + column: 3, + endLine: 1, + endColumn: 6, + }, + ], + }, + { + code: "[foo]\n[bar]", + errors: [ + { + messageId: "notFound", + data: { label: "foo" }, + line: 1, + column: 2, + endLine: 1, + endColumn: 5, + }, + { + messageId: "notFound", + data: { label: "bar" }, + line: 2, + column: 2, + endLine: 2, + endColumn: 5, + }, + ], + }, + { + code: "- - - [foo]", + errors: [ + { + messageId: "notFound", + data: { label: "foo" }, + line: 1, + column: 8, + endLine: 1, + endColumn: 11, + }, + ], + }, + ], }); diff --git a/tools/build-rules.js b/tools/build-rules.js index 82464ba5..e67fe592 100644 --- a/tools/build-rules.js +++ b/tools/build-rules.js @@ -25,12 +25,12 @@ const rules = fs.readdirSync(rulesPath); const recommended = []; for (const ruleId of rules) { - const rulePath = path.resolve(rulesPath, ruleId); - const rule = await import(pathToFileURL(rulePath)); + const rulePath = path.resolve(rulesPath, ruleId); + const rule = await import(pathToFileURL(rulePath)); - if (rule.default.meta.docs.recommended) { - recommended.push(ruleId); - } + if (rule.default.meta.docs.recommended) { + recommended.push(ruleId); + } } const output = `export default { @@ -39,7 +39,10 @@ const output = `export default { `; fs.mkdirSync(path.resolve(thisDir, "../src/build"), { recursive: true }); -fs.writeFileSync(path.resolve(thisDir, "../src/build/recommended-config.js"), output); +fs.writeFileSync( + path.resolve(thisDir, "../src/build/recommended-config.js"), + output, +); console.log("Recommended rules generated successfully."); diff --git a/tools/dedupe-types.js b/tools/dedupe-types.js index e6bba030..ce8b16d1 100644 --- a/tools/dedupe-types.js +++ b/tools/dedupe-types.js @@ -23,21 +23,21 @@ import fs from "node:fs"; const files = process.argv.slice(2); files.forEach(filePath => { - const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/gu); - const typedefs = new Set(); + const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/gu); + const typedefs = new Set(); - const remainingLines = lines.filter(line => { - if (!line.startsWith("/** @typedef {import")) { - return true; - } + const remainingLines = lines.filter(line => { + if (!line.startsWith("/** @typedef {import")) { + return true; + } - if (typedefs.has(line)) { - return false; - } + if (typedefs.has(line)) { + return false; + } - typedefs.add(line); - return true; - }); + typedefs.add(line); + return true; + }); - fs.writeFileSync(filePath, remainingLines.join("\n"), "utf8"); + fs.writeFileSync(filePath, remainingLines.join("\n"), "utf8"); });