├── .npmrc ├── examples ├── react │ ├── .npmrc │ ├── package.json │ ├── README.md │ └── .eslintrc.js └── typescript │ ├── .npmrc │ ├── package.json │ ├── .eslintrc.js │ └── README.md ├── tests ├── .eslintrc.yml ├── fixtures │ ├── recommended.json │ ├── eslintrc.json │ └── long.md ├── examples │ └── all.js └── lib │ ├── processor.js │ └── plugin.js ├── .eslintignore ├── screenshot.png ├── index.js ├── .gitignore ├── CONTRIBUTING.md ├── .editorconfig ├── .github └── workflows │ ├── add-to-triage.yml │ ├── ci.yml │ └── release-please.yml ├── npm-prepare.js ├── LICENSE ├── .eslintrc.js ├── package.json ├── lib ├── index.js └── processor.js ├── README.md └── CHANGELOG.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock = false 2 | -------------------------------------------------------------------------------- /examples/react/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock = false 2 | -------------------------------------------------------------------------------- /tests/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | -------------------------------------------------------------------------------- /examples/typescript/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock = false 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | tests/fixtures 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu/eslint-plugin-markdown/main/screenshot.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Exports the processor. 3 | * @author Brandon Mills 4 | */ 5 | 6 | "use strict"; 7 | 8 | module.exports = require("./lib"); 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | coverage/ 3 | node_modules/ 4 | npm-debug.log 5 | .eslint-release-info.json 6 | .nyc_output 7 | yarn.lock 8 | package-lock.json 9 | pnpm-lock.yaml 10 | -------------------------------------------------------------------------------- /tests/fixtures/recommended.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["eslint:recommended", "plugin:markdown/recommended"], 4 | "rules": { 5 | "no-console": "error" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": "eslint ." 5 | }, 6 | "devDependencies": { 7 | "eslint": "^7.5.0", 8 | "eslint-plugin-markdown": "file:../..", 9 | "eslint-plugin-react": "^7.20.3" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Code 2 | 3 | Please sign the ESLint [Contributor License Agreement](https://cla.js.foundation/eslint/eslint-plugin-markdown) 4 | 5 | ## Full Documentation 6 | 7 | Our full contribution guidelines can be found at: 8 | http://eslint.org/docs/developer-guide/contributing/ 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [package.json] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /examples/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": "eslint ." 5 | }, 6 | "devDependencies": { 7 | "@typescript-eslint/eslint-plugin": "^3.6.1", 8 | "@typescript-eslint/parser": "^3.6.1", 9 | "eslint": "^7.5.0", 10 | "eslint-plugin-markdown": "file:../..", 11 | "typescript": "^3.9.7" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/add-to-triage.yml: -------------------------------------------------------------------------------- 1 | name: Add to Triage 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | add-to-project: 10 | name: Add issue to project 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/add-to-project@v0.4.0 14 | with: 15 | project-url: https://github.com/orgs/eslint/projects/3 16 | github-token: ${{ secrets.PROJECT_BOT_TOKEN }} 17 | labeled: "triage:no" 18 | label-operator: NOT 19 | -------------------------------------------------------------------------------- /tests/fixtures/eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true 5 | }, 6 | "plugins": ["markdown"], 7 | "overrides": [ 8 | { 9 | "files": ["*.md", "*.mkdn", "*.mdown", "*.markdown", "*.custom"], 10 | "processor": "markdown/markdown" 11 | } 12 | ], 13 | "rules": { 14 | "eol-last": "error", 15 | "no-console": "error", 16 | "no-undef": "error", 17 | "quotes": "error", 18 | "spaced-comment": "error" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/typescript/.eslintrc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | root: true, 5 | extends: [ 6 | "eslint:recommended", 7 | "plugin:markdown/recommended", 8 | ], 9 | overrides: [ 10 | { 11 | files: [".eslintrc.js"], 12 | env: { 13 | node: true 14 | } 15 | }, 16 | { 17 | files: ["*.ts"], 18 | parser: "@typescript-eslint/parser", 19 | extends: ["plugin:@typescript-eslint/recommended"] 20 | }, 21 | ] 22 | }; 23 | -------------------------------------------------------------------------------- /examples/react/README.md: -------------------------------------------------------------------------------- 1 | # React Example 2 | 3 | ```jsx 4 | function App({ name }) { 5 | return ( 6 |
7 |

Hello, {name}!

8 |
9 | ); 10 | } 11 | ``` 12 | 13 | ```sh 14 | $ git clone https://github.com/eslint/eslint-plugin-markdown.git 15 | $ cd eslint-plugin-markdown/examples/react 16 | $ npm install 17 | $ npm test 18 | 19 | eslint-plugin-markdown/examples/react/README.md 20 | 4:10 error 'App' is defined but never used no-unused-vars 21 | 4:16 error 'name' is missing in props validation react/prop-types 22 | 23 | ✖ 2 problems (2 errors, 0 warnings) 24 | ``` 25 | -------------------------------------------------------------------------------- /tests/fixtures/long.md: -------------------------------------------------------------------------------- 1 | # Test 2 | 3 | ```txt 4 | Don't lint me! 5 | ``` 6 | 7 | This is some code: 8 | 9 | ```js 10 | console.log(42); 11 | ``` 12 | 13 | ```js 14 | // Comment 15 | function foo() { 16 | console.log("Hello"); 17 | } 18 | ``` 19 | 20 | 21 | 22 | 23 | ```js 24 | console.log(process.version); 25 | ``` 26 | 27 | How about some JSX? 28 | 29 | 35 | 36 | 37 | ```js 38 | console.log("Error!"); 39 | ``` 40 | 41 | I may be a code block, but don't lint me! 42 | 43 | 44 | 45 | ```js 46 | !@#$%^&*() 47 | ``` 48 | 49 | The end. 50 | -------------------------------------------------------------------------------- /npm-prepare.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Install examples' dependencies as part of local npm install for 3 | * development and CI. 4 | * @author btmills 5 | */ 6 | 7 | "use strict"; 8 | 9 | if (!process.env.NO_RECURSIVE_PREPARE) { 10 | const childProcess = require("child_process"); 11 | const fs = require("fs"); 12 | const path = require("path"); 13 | 14 | const examplesDir = path.resolve(__dirname, "examples"); 15 | const examples = fs.readdirSync(examplesDir) 16 | .filter(exampleDir => fs.statSync(path.join(examplesDir, exampleDir)).isDirectory()) 17 | .filter(exampleDir => fs.existsSync(path.join(examplesDir, exampleDir, "package.json"))); 18 | 19 | for (const example of examples) { 20 | childProcess.execSync("npm install", { 21 | cwd: path.resolve(examplesDir, example), 22 | env: { 23 | ...process.env, 24 | NO_RECURSIVE_PREPARE: "true" 25 | } 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/typescript/README.md: -------------------------------------------------------------------------------- 1 | # TypeScript Example 2 | 3 | The `@typescript-eslint` parser and the `recommended` config's rules will work in `ts` code blocks. However, [type-aware rules](https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/TYPED_LINTING.md) will not work because the code blocks are not part of a compilable `tsconfig.json` project. 4 | 5 | ```ts 6 | function hello(name: String) { 7 | console.log(`Hello, ${name}!`); 8 | } 9 | 10 | hello(42 as any); 11 | ``` 12 | 13 | ```sh 14 | $ git clone https://github.com/eslint/eslint-plugin-markdown.git 15 | $ cd eslint-plugin-markdown/examples/typescript 16 | $ npm install 17 | $ npm test 18 | 19 | eslint-plugin-markdown/examples/typescript/README.md 20 | 6:22 error Don’t use `String` as a type. Use string instead @typescript-eslint/ban-types 21 | 10:13 warning Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 22 | 23 | ✖ 2 problems (1 error, 1 warning) 24 | 1 error and 0 warnings potentially fixable with the `--fix` option. 25 | ``` 26 | -------------------------------------------------------------------------------- /examples/react/.eslintrc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | root: true, 5 | extends: [ 6 | "eslint:recommended", 7 | "plugin:markdown/recommended", 8 | "plugin:react/recommended", 9 | ], 10 | settings: { 11 | react: { 12 | version: "16.8.0" 13 | } 14 | }, 15 | parserOptions: { 16 | ecmaFeatures: { 17 | jsx: true 18 | }, 19 | ecmaVersion: 2015, 20 | sourceType: "module" 21 | }, 22 | env: { 23 | browser: true, 24 | es6: true 25 | }, 26 | overrides: [ 27 | { 28 | files: [".eslintrc.js"], 29 | env: { 30 | node: true 31 | } 32 | }, 33 | { 34 | files: ["**/*.md/*.jsx"], 35 | globals: { 36 | // For code examples, `import React from "react";` at the top 37 | // of every code block is distracting, so pre-define the 38 | // `React` global. 39 | React: false 40 | }, 41 | } 42 | ] 43 | }; 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright JS Foundation and other contributors, https://js.foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const PACKAGE_NAME = require("./package").name; 6 | const SYMLINK_LOCATION = path.join(__dirname, "node_modules", PACKAGE_NAME); 7 | 8 | // Symlink node_modules/eslint-plugin-markdown to this directory so that ESLint 9 | // resolves this plugin name correctly. 10 | if (!fs.existsSync(SYMLINK_LOCATION)) { 11 | fs.symlinkSync(__dirname, SYMLINK_LOCATION); 12 | } 13 | 14 | module.exports = { 15 | root: true, 16 | 17 | parserOptions: { 18 | ecmaVersion: 2018 19 | }, 20 | 21 | plugins: [ 22 | PACKAGE_NAME 23 | ], 24 | 25 | env: { 26 | node: true 27 | }, 28 | 29 | extends: "eslint", 30 | 31 | ignorePatterns: ["examples"], 32 | 33 | overrides: [ 34 | { 35 | files: ["**/*.md"], 36 | processor: "markdown/markdown" 37 | }, 38 | { 39 | files: ["**/*.md/*.js"], 40 | parserOptions: { 41 | ecmaFeatures: { 42 | impliedStrict: true 43 | } 44 | }, 45 | rules: { 46 | "lines-around-comment": "off" 47 | } 48 | } 49 | ] 50 | }; 51 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | lint: 11 | name: Lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | - name: Install Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 16 20 | - name: Install Packages 21 | run: npm install 22 | env: 23 | CI: true 24 | - name: Lint 25 | run: npm run lint 26 | 27 | test: 28 | name: Test 29 | strategy: 30 | matrix: 31 | os: [ubuntu-latest] 32 | eslint: [6, 7, 8] 33 | node: [12.22.0, 14, 16, 17, 18, 19, 20, 21] 34 | include: 35 | - os: windows-latest 36 | eslint: 7 37 | node: 16 38 | - os: macOS-latest 39 | eslint: 7 40 | node: 16 41 | runs-on: ${{ matrix.os }} 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@v3 45 | - name: Install Node.js ${{ matrix.node }} 46 | uses: actions/setup-node@v3 47 | with: 48 | node-version: ${{ matrix.node }} 49 | - name: Install Packages 50 | run: npm install 51 | env: 52 | CI: true 53 | - name: Install ESLint@${{ matrix.eslint }} 54 | run: npm install eslint@${{ matrix.eslint }} 55 | - name: Test 56 | run: npm run test 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-markdown", 3 | "version": "3.0.1", 4 | "description": "An ESLint plugin to lint JavaScript in Markdown code fences.", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Brandon Mills", 8 | "url": "https://github.com/btmills" 9 | }, 10 | "repository": "eslint/eslint-plugin-markdown", 11 | "bugs": { 12 | "url": "https://github.com/eslint/eslint-plugin-markdown/issues" 13 | }, 14 | "homepage": "https://github.com/eslint/eslint-plugin-markdown#readme", 15 | "keywords": [ 16 | "eslint", 17 | "eslintplugin", 18 | "markdown", 19 | "lint", 20 | "linter" 21 | ], 22 | "scripts": { 23 | "lint": "eslint --ext js,md .", 24 | "prepare": "node ./npm-prepare.js", 25 | "release:generate:latest": "eslint-generate-release", 26 | "release:generate:alpha": "eslint-generate-prerelease alpha", 27 | "release:generate:beta": "eslint-generate-prerelease beta", 28 | "release:generate:rc": "eslint-generate-prerelease rc", 29 | "release:publish": "eslint-publish-release", 30 | "test": "nyc _mocha -- -c tests/{examples,lib}/**/*.js" 31 | }, 32 | "main": "index.js", 33 | "files": [ 34 | "index.js", 35 | "lib/index.js", 36 | "lib/processor.js" 37 | ], 38 | "devDependencies": { 39 | "chai": "^4.2.0", 40 | "eslint": "^7.32.0", 41 | "eslint-config-eslint": "^7.0.0", 42 | "eslint-plugin-jsdoc": "^37.0.3", 43 | "eslint-plugin-node": "^11.1.0", 44 | "eslint-release": "^3.1.2", 45 | "mocha": "^6.2.2", 46 | "nyc": "^14.1.1" 47 | }, 48 | "dependencies": { 49 | "mdast-util-from-markdown": "^0.8.5" 50 | }, 51 | "peerDependencies": { 52 | "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" 53 | }, 54 | "engines": { 55 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/examples/all.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const assert = require("chai").assert; 4 | const fs = require("fs"); 5 | const path = require("path"); 6 | const semver = require("semver"); 7 | 8 | const examplesDir = path.resolve(__dirname, "../../examples/"); 9 | const examples = fs.readdirSync(examplesDir) 10 | .filter(exampleDir => fs.statSync(path.join(examplesDir, exampleDir)).isDirectory()) 11 | .filter(exampleDir => fs.existsSync(path.join(examplesDir, exampleDir, "package.json"))); 12 | 13 | for (const example of examples) { 14 | const cwd = path.join(examplesDir, example); 15 | 16 | // The plugin officially supports ESLint as early as v6, but the examples 17 | // use ESLint v7, which has a higher minimum Node.js version than does v6. 18 | // Only exercise the example if the running Node.js version satisfies the 19 | // minimum version constraint. In CI, this will skip these tests in Node.js 20 | // v8 and run them on all other Node.js versions. 21 | const eslintPackageJsonPath = require.resolve("eslint/package.json", { 22 | paths: [cwd] 23 | }); 24 | const eslintPackageJson = require(eslintPackageJsonPath); 25 | if (semver.satisfies(process.version, eslintPackageJson.engines.node)) { 26 | describe("examples", function () { 27 | describe(example, () => { 28 | it("reports errors on code blocks in .md files", async () => { 29 | const { ESLint } = require( 30 | require.resolve("eslint", { paths: [cwd] }) 31 | ); 32 | const eslint = new ESLint({ cwd }); 33 | 34 | const results = await eslint.lintFiles(["."]); 35 | const readme = results.find(result => 36 | path.basename(result.filePath) == "README.md" 37 | ); 38 | assert.isNotNull(readme); 39 | assert.isAbove(readme.messages.length, 0); 40 | }); 41 | }); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Enables the processor for Markdown file extensions. 3 | * @author Brandon Mills 4 | */ 5 | 6 | "use strict"; 7 | 8 | const processor = require("./processor"); 9 | 10 | module.exports = { 11 | configs: { 12 | recommended: { 13 | plugins: ["markdown"], 14 | overrides: [ 15 | { 16 | files: ["*.md"], 17 | processor: "markdown/markdown" 18 | }, 19 | { 20 | files: ["**/*.md/**"], 21 | parserOptions: { 22 | ecmaFeatures: { 23 | 24 | // Adding a "use strict" directive at the top of 25 | // every code block is tedious and distracting, so 26 | // opt into strict mode parsing without the 27 | // directive. 28 | impliedStrict: true 29 | } 30 | }, 31 | rules: { 32 | 33 | // The Markdown parser automatically trims trailing 34 | // newlines from code blocks. 35 | "eol-last": "off", 36 | 37 | // In code snippets and examples, these rules are often 38 | // counterproductive to clarity and brevity. 39 | "no-undef": "off", 40 | "no-unused-expressions": "off", 41 | "no-unused-vars": "off", 42 | "padded-blocks": "off", 43 | 44 | // Adding a "use strict" directive at the top of every 45 | // code block is tedious and distracting. The config 46 | // opts into strict mode parsing without the directive. 47 | strict: "off", 48 | 49 | // The processor will not receive a Unicode Byte Order 50 | // Mark from the Markdown parser. 51 | "unicode-bom": "off" 52 | } 53 | } 54 | ] 55 | } 56 | }, 57 | processors: { 58 | markdown: processor 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | name: release-please 6 | jobs: 7 | release-please: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: write 11 | pull-requests: write 12 | id-token: write 13 | steps: 14 | - uses: google-github-actions/release-please-action@v3 15 | id: release 16 | with: 17 | release-type: node 18 | package-name: 'eslint-plugin-markdown' 19 | pull-request-title-pattern: 'chore: release ${version}' 20 | changelog-types: > 21 | [ 22 | { "type": "feat", "section": "Features", "hidden": false }, 23 | { "type": "fix", "section": "Bug Fixes", "hidden": false }, 24 | { "type": "docs", "section": "Documentation", "hidden": false }, 25 | { "type": "build", "section": "Build Related", "hidden": false }, 26 | { "type": "chore", "section": "Chores", "hidden": false }, 27 | { "type": "perf", "section": "Chores", "hidden": false }, 28 | { "type": "ci", "section": "Chores", "hidden": false }, 29 | { "type": "refactor", "section": "Chores", "hidden": false }, 30 | { "type": "test", "section": "Chores", "hidden": false } 31 | ] 32 | - uses: actions/checkout@v3 33 | if: ${{ steps.release.outputs.release_created }} 34 | - uses: actions/setup-node@v3 35 | with: 36 | node-version: lts/* 37 | registry-url: https://registry.npmjs.org 38 | if: ${{ steps.release.outputs.release_created }} 39 | - run: npm publish --provenance 40 | env: 41 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | if: ${{ steps.release.outputs.release_created }} 43 | - run: 'npx @humanwhocodes/tweet "eslint-plugin-markdown ${{ steps.release.outputs.tag_name }} has been released: ${{ steps.release.outputs.html_url }}"' 44 | if: ${{ steps.release.outputs.release_created }} 45 | env: 46 | TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} 47 | TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} 48 | TWITTER_ACCESS_TOKEN_KEY: ${{ secrets.TWITTER_ACCESS_TOKEN_KEY }} 49 | TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} 50 | - run: 'npx @humanwhocodes/toot "eslint-plugin-markdown ${{ steps.release.outputs.tag_name }} has been released: ${{ steps.release.outputs.html_url }}"' 51 | if: ${{ steps.release.outputs.release_created }} 52 | env: 53 | MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} 54 | MASTODON_HOST: ${{ secrets.MASTODON_HOST }} 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-markdown 2 | 3 | [![npm Version](https://img.shields.io/npm/v/eslint-plugin-markdown.svg)](https://www.npmjs.com/package/eslint-plugin-markdown) 4 | [![Downloads](https://img.shields.io/npm/dm/eslint-plugin-markdown.svg)](https://www.npmjs.com/package/eslint-plugin-markdown) 5 | [![Build Status](https://github.com/eslint/eslint-plugin-markdown/workflows/CI/badge.svg)](https://github.com/eslint/eslint-plugin-markdown/actions) 6 | 7 | Lint JS, JSX, TypeScript, and more inside Markdown. 8 | 9 | A JS code snippet in a Markdown editor has red squiggly underlines. A tooltip explains the problem. 15 | 16 | ## Usage 17 | 18 | ### Installing 19 | 20 | Install the plugin alongside ESLint v6 or greater: 21 | 22 | ```sh 23 | npm install --save-dev eslint eslint-plugin-markdown 24 | ``` 25 | 26 | ### Configuring 27 | 28 | Extending the `plugin:markdown/recommended` config will enable the Markdown processor on all `.md` files: 29 | 30 | ```js 31 | // .eslintrc.js 32 | module.exports = { 33 | extends: "plugin:markdown/recommended" 34 | }; 35 | ``` 36 | 37 | #### Advanced Configuration 38 | 39 | Add the plugin to your `.eslintrc` and use the `processor` option in an `overrides` entry to enable the plugin's `markdown/markdown` processor on Markdown files. 40 | Each fenced code block inside a Markdown document has a virtual filename appended to the Markdown file's path. 41 | The virtual filename's extension will match the fenced code block's syntax tag, so for example, ```js code blocks in README.md would match README.md/*.js. 42 | [`overrides` glob patterns](https://eslint.org/docs/user-guide/configuring#configuration-based-on-glob-patterns) for these virtual filenames can customize configuration for code blocks without affecting regular code. 43 | For more information on configuring processors, refer to the [ESLint documentation](https://eslint.org/docs/user-guide/configuring#specifying-processor). 44 | 45 | ```js 46 | // .eslintrc.js 47 | module.exports = { 48 | // 1. Add the plugin. 49 | plugins: ["markdown"], 50 | overrides: [ 51 | { 52 | // 2. Enable the Markdown processor for all .md files. 53 | files: ["**/*.md"], 54 | processor: "markdown/markdown" 55 | }, 56 | { 57 | // 3. Optionally, customize the configuration ESLint uses for ```js 58 | // fenced code blocks inside .md files. 59 | files: ["**/*.md/*.js"], 60 | // ... 61 | rules: { 62 | // ... 63 | } 64 | } 65 | ] 66 | }; 67 | ``` 68 | 69 | #### Frequently-Disabled Rules 70 | 71 | Some rules that catch mistakes in regular code are less helpful in documentation. 72 | For example, `no-undef` would flag variables that are declared outside of a code snippet because they aren't relevant to the example. 73 | The `plugin:markdown/recommended` config disables these rules in Markdown files: 74 | 75 | - [`no-undef`](https://eslint.org/docs/rules/no-undef) 76 | - [`no-unused-expressions`](https://eslint.org/docs/rules/no-unused-expressions) 77 | - [`no-unused-vars`](https://eslint.org/docs/rules/no-unused-vars) 78 | - [`padded-blocks`](https://eslint.org/docs/rules/padded-blocks) 79 | 80 | Use [`overrides` glob patterns](https://eslint.org/docs/user-guide/configuring#configuration-based-on-glob-patterns) to disable more rules just for Markdown code blocks: 81 | 82 | ```js 83 | module.exports = { 84 | // ... 85 | overrides: [ 86 | // ... 87 | { 88 | // 1. Target ```js code blocks in .md files. 89 | files: ["**/*.md/*.js"], 90 | rules: { 91 | // 2. Disable other rules. 92 | "no-console": "off", 93 | "import/no-unresolved": "off" 94 | } 95 | } 96 | ] 97 | }; 98 | ``` 99 | 100 | #### Strict Mode 101 | 102 | `"use strict"` directives in every code block would be annoying. 103 | The `plugin:markdown/recommended` config enables the [`impliedStrict` parser option](https://eslint.org/docs/user-guide/configuring#specifying-parser-options) and disables the [`strict` rule](https://eslint.org/docs/rules/strict) in Markdown files. 104 | This opts into strict mode parsing without repeated `"use strict"` directives. 105 | 106 | #### Unsatisfiable Rules 107 | 108 | Markdown code blocks are not real files, so ESLint's file-format rules do not apply. 109 | The `plugin:markdown/recommended` config disables these rules in Markdown files: 110 | 111 | - [`eol-last`](https://eslint.org/docs/rules/eol-last): The Markdown parser trims trailing newlines from code blocks. 112 | - [`unicode-bom`](https://eslint.org/docs/rules/unicode-bom): Markdown code blocks do not have Unicode Byte Order Marks. 113 | 114 | #### Migrating from `eslint-plugin-markdown` v1 115 | 116 | `eslint-plugin-markdown` v1 used an older version of ESLint's processor API. 117 | The Markdown processor automatically ran on `.md`, `.mkdn`, `.mdown`, and `.markdown` files, and it only extracted fenced code blocks marked with `js`, `javascript`, `jsx`, or `node` syntax. 118 | Configuration specifically for fenced code blocks went inside an `overrides` entry with a `files` pattern matching the containing Markdown document's filename that applied to all fenced code blocks inside the file. 119 | 120 | ```js 121 | // .eslintrc.js for eslint-plugin-markdown v1 122 | module.exports = { 123 | plugins: ["markdown"], 124 | overrides: [ 125 | { 126 | files: ["**/*.md"], 127 | // In v1, configuration for fenced code blocks went inside an 128 | // `overrides` entry with a .md pattern, for example: 129 | parserOptions: { 130 | ecmaFeatures: { 131 | impliedStrict: true 132 | } 133 | }, 134 | rules: { 135 | "no-console": "off" 136 | } 137 | } 138 | ] 139 | }; 140 | ``` 141 | 142 | [RFC3](https://github.com/eslint/rfcs/blob/master/designs/2018-processors-improvements/README.md) designed a new processor API to remove these limitations, and the new API was [implemented](https://github.com/eslint/eslint/pull/11552) as part of ESLint v6. 143 | `eslint-plugin-markdown` v2 uses this new API. 144 | 145 | ```bash 146 | $ npm install --save-dev eslint@latest eslint-plugin-markdown@latest 147 | ``` 148 | 149 | All of the Markdown file extensions that were previously hard-coded are now fully configurable in `.eslintrc.js`. 150 | Use the new `processor` option to apply the `markdown/markdown` processor on any Markdown documents matching a `files` pattern. 151 | Each fenced code block inside a Markdown document has a virtual filename appended to the Markdown file's path. 152 | The virtual filename's extension will match the fenced code block's syntax tag, so for example, ```js code blocks in README.md would match README.md/*.js. 153 | 154 | ```js 155 | // eslintrc.js for eslint-plugin-markdown v2 156 | module.exports = { 157 | plugins: ["markdown"], 158 | overrides: [ 159 | { 160 | // In v2, explicitly apply eslint-plugin-markdown's `markdown` 161 | // processor on any Markdown files you want to lint. 162 | files: ["**/*.md"], 163 | processor: "markdown/markdown" 164 | }, 165 | { 166 | // In v2, configuration for fenced code blocks is separate from the 167 | // containing Markdown file. Each code block has a virtual filename 168 | // appended to the Markdown file's path. 169 | files: ["**/*.md/*.js"], 170 | // Configuration for fenced code blocks goes with the override for 171 | // the code block's virtual filename, for example: 172 | parserOptions: { 173 | ecmaFeatures: { 174 | impliedStrict: true 175 | } 176 | }, 177 | rules: { 178 | "no-console": "off" 179 | } 180 | } 181 | ] 182 | }; 183 | ``` 184 | 185 | If you need to precisely mimic the behavior of v1 with the hard-coded Markdown extensions and fenced code block syntaxes, you can use those as glob patterns in `overrides[].files`: 186 | 187 | ```js 188 | // eslintrc.js for v2 mimicking v1 behavior 189 | module.exports = { 190 | plugins: ["markdown"], 191 | overrides: [ 192 | { 193 | files: ["**/*.{md,mkdn,mdown,markdown}"], 194 | processor: "markdown/markdown" 195 | }, 196 | { 197 | files: ["**/*.{md,mkdn,mdown,markdown}/*.{js,javascript,jsx,node}"] 198 | // ... 199 | } 200 | ] 201 | }; 202 | ``` 203 | 204 | ### Running 205 | 206 | #### ESLint v7 207 | 208 | You can run ESLint as usual and do not need to use the `--ext` option. 209 | ESLint v7 [automatically lints file extensions specified in `overrides[].files` patterns in config files](https://github.com/eslint/rfcs/blob/0253e3a95511c65d622eaa387eb73f824249b467/designs/2019-additional-lint-targets/README.md). 210 | 211 | #### ESLint v6 212 | 213 | Use the [`--ext` option](https://eslint.org/docs/user-guide/command-line-interface#ext) to include `.js` and `.md` extensions in ESLint's file search: 214 | 215 | ```sh 216 | eslint --ext js,md . 217 | ``` 218 | 219 | ### Autofixing 220 | 221 | With this plugin, [ESLint's `--fix` option](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some issues in your Markdown fenced code blocks. 222 | To enable this, pass the `--fix` flag when you run ESLint: 223 | 224 | ```bash 225 | eslint --fix . 226 | ``` 227 | 228 | ## What Gets Linted? 229 | 230 | With this plugin, ESLint will lint [fenced code blocks](https://help.github.com/articles/github-flavored-markdown/#fenced-code-blocks) in your Markdown documents: 231 | 232 | ````markdown 233 | ```js 234 | // This gets linted 235 | var answer = 6 * 7; 236 | console.log(answer); 237 | ``` 238 | 239 | Here is some regular Markdown text that will be ignored. 240 | 241 | ```js 242 | // This also gets linted 243 | 244 | /* eslint quotes: [2, "double"] */ 245 | 246 | function hello() { 247 | console.log("Hello, world!"); 248 | } 249 | hello(); 250 | ``` 251 | 252 | ```jsx 253 | // This can be linted too if you add `.jsx` files to `overrides` in ESLint v7 254 | // or pass `--ext jsx` in ESLint v6. 255 | var div =
; 256 | ``` 257 | ```` 258 | 259 | Blocks that don't specify a syntax are ignored: 260 | 261 | ````markdown 262 | ``` 263 | This is plain text and doesn't get linted. 264 | ``` 265 | ```` 266 | 267 | Unless a fenced code block's syntax appears as a file extension in `overrides[].files` in ESLint v7, it will be ignored. 268 | If using ESLint v6, you must also include the extension with the `--ext` option. 269 | 270 | ````markdown 271 | ```python 272 | print("This doesn't get linted either.") 273 | ``` 274 | ```` 275 | 276 | ## Configuration Comments 277 | 278 | The processor will convert HTML comments immediately preceding a code block into JavaScript block comments and insert them at the beginning of the source code that it passes to ESLint. 279 | This permits configuring ESLint via configuration comments while keeping the configuration comments themselves hidden when the markdown is rendered. 280 | Comment bodies are passed through unmodified, so the plugin supports any [configuration comments](http://eslint.org/docs/user-guide/configuring) supported by ESLint itself. 281 | 282 | This example enables the `browser` environment, disables the `no-alert` rule, and configures the `quotes` rule to prefer single quotes: 283 | 284 | ````markdown 285 | 286 | 287 | 288 | 289 | ```js 290 | alert('Hello, world!'); 291 | ``` 292 | ```` 293 | 294 | Each code block in a file is linted separately, so configuration comments apply only to the code block that immediately follows. 295 | 296 | ````markdown 297 | Assuming `no-alert` is enabled in `.eslintrc`, the first code block will have no error from `no-alert`: 298 | 299 | 300 | 301 | 302 | ```js 303 | alert("Hello, world!"); 304 | ``` 305 | 306 | But the next code block will have an error from `no-alert`: 307 | 308 | 309 | 310 | ```js 311 | alert("Hello, world!"); 312 | ``` 313 | ```` 314 | 315 | ### Skipping Blocks 316 | 317 | Sometimes it can be useful to have code blocks marked with `js` even though they don't contain valid JavaScript syntax, such as commented JSON blobs that need `js` syntax highlighting. 318 | Standard `eslint-disable` comments only silence rule reporting, but ESLint still reports any syntax errors it finds. 319 | In cases where a code block should not even be parsed, insert a non-standard `` comment before the block, and this plugin will hide the following block from ESLint. 320 | Neither rule nor syntax errors will be reported. 321 | 322 | ````markdown 323 | There are comments in this JSON, so we use `js` syntax for better 324 | highlighting. Skip the block to prevent warnings about invalid syntax. 325 | 326 | 327 | 328 | ```js 329 | { 330 | // This code block is hidden from ESLint. 331 | "hello": "world" 332 | } 333 | ``` 334 | 335 | ```js 336 | console.log("This code block is linted normally."); 337 | ``` 338 | ```` 339 | 340 | ## Editor Integrations 341 | 342 | ### VSCode 343 | 344 | [`vscode-eslint`](https://github.com/microsoft/vscode-eslint) has built-in support for the Markdown processor. 345 | 346 | ### Atom 347 | 348 | The [`linter-eslint`](https://atom.io/packages/linter-eslint) package allows for linting within the [Atom IDE](https://atom.io/). 349 | 350 | In order to see `eslint-plugin-markdown` work its magic within Markdown code blocks in your Atom editor, you can go to `linter-eslint`'s settings and within "List of scopes to run ESLint on...", add the cursor scope "source.gfm". 351 | 352 | However, this reports a problem when viewing Markdown which does not have configuration, so you may wish to use the cursor scope "source.embedded.js", but note that `eslint-plugin-markdown` configuration comments and skip directives won't work in this context. 353 | 354 | ## Contributing 355 | 356 | ```sh 357 | $ git clone https://github.com/eslint/eslint-plugin-markdown.git 358 | $ cd eslint-plugin-markdown 359 | $ npm install 360 | $ npm test 361 | ``` 362 | 363 | This project follows the [ESLint contribution guidelines](http://eslint.org/docs/developer-guide/contributing/). 364 | -------------------------------------------------------------------------------- /lib/processor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Processes Markdown files for consumption by ESLint. 3 | * @author Brandon Mills 4 | */ 5 | 6 | /** 7 | * @typedef {import('eslint/lib/shared/types').LintMessage} Message 8 | * 9 | * @typedef {Object} ASTNode 10 | * @property {string} type 11 | * @property {string} [lang] 12 | * 13 | * @typedef {Object} RangeMap 14 | * @property {number} indent Number of code block indent characters trimmed from 15 | * the beginning of the line during extraction. 16 | * @property {number} js Offset from the start of the code block's range in the 17 | * extracted JS. 18 | * @property {number} md Offset from the start of the code block's range in the 19 | * original Markdown. 20 | * 21 | * @typedef {Object} BlockBase 22 | * @property {string} baseIndentText 23 | * @property {string[]} comments 24 | * @property {RangeMap[]} rangeMap 25 | * 26 | * @typedef {ASTNode & BlockBase} Block 27 | */ 28 | 29 | "use strict"; 30 | 31 | const parse = require("mdast-util-from-markdown"); 32 | 33 | const UNSATISFIABLE_RULES = [ 34 | "eol-last", // The Markdown parser strips trailing newlines in code fences 35 | "unicode-bom" // Code blocks will begin in the middle of Markdown files 36 | ]; 37 | const SUPPORTS_AUTOFIX = true; 38 | 39 | /** 40 | * @type {Map} 41 | */ 42 | const blocksCache = new Map(); 43 | 44 | /** 45 | * Performs a depth-first traversal of the Markdown AST. 46 | * @param {ASTNode} node A Markdown AST node. 47 | * @param {{[key: string]: (node: ASTNode) => void}} callbacks A map of node types to callbacks. 48 | * @returns {void} 49 | */ 50 | function traverse(node, callbacks) { 51 | if (callbacks[node.type]) { 52 | callbacks[node.type](node); 53 | } else { 54 | callbacks["*"](); 55 | } 56 | 57 | if (typeof node.children !== "undefined") { 58 | for (let i = 0; i < node.children.length; i++) { 59 | traverse(node.children[i], callbacks); 60 | } 61 | } 62 | } 63 | 64 | /** 65 | * Extracts `eslint-*` or `global` comments from HTML comments if present. 66 | * @param {string} html The text content of an HTML AST node. 67 | * @returns {string} The comment's text without the opening and closing tags or 68 | * an empty string if the text is not an ESLint HTML comment. 69 | */ 70 | function getComment(html) { 71 | const commentStart = ""; 73 | const regex = /^(eslint\b|global\s)/u; 74 | 75 | if ( 76 | html.slice(0, commentStart.length) !== commentStart || 77 | html.slice(-commentEnd.length) !== commentEnd 78 | ) { 79 | return ""; 80 | } 81 | 82 | const comment = html.slice(commentStart.length, -commentEnd.length); 83 | 84 | if (!regex.test(comment.trim())) { 85 | return ""; 86 | } 87 | 88 | return comment; 89 | } 90 | 91 | // Before a code block, blockquote characters (`>`) are also considered 92 | // "whitespace". 93 | const leadingWhitespaceRegex = /^[>\s]*/u; 94 | 95 | /** 96 | * Gets the offset for the first column of the node's first line in the 97 | * original source text. 98 | * @param {ASTNode} node A Markdown code block AST node. 99 | * @returns {number} The offset for the first column of the node's first line. 100 | */ 101 | function getBeginningOfLineOffset(node) { 102 | return node.position.start.offset - node.position.start.column + 1; 103 | } 104 | 105 | /** 106 | * Gets the leading text, typically whitespace with possible blockquote chars, 107 | * used to indent a code block. 108 | * @param {string} text The text of the file. 109 | * @param {ASTNode} node A Markdown code block AST node. 110 | * @returns {string} The text from the start of the first line to the opening 111 | * fence of the code block. 112 | */ 113 | function getIndentText(text, node) { 114 | return leadingWhitespaceRegex.exec( 115 | text.slice(getBeginningOfLineOffset(node)) 116 | )[0]; 117 | } 118 | 119 | /** 120 | * When applying fixes, the postprocess step needs to know how to map fix ranges 121 | * from their location in the linted JS to the original offset in the Markdown. 122 | * Configuration comments and indentation trimming both complicate this process. 123 | * 124 | * Configuration comments appear in the linted JS but not in the Markdown code 125 | * block. Fixes to configuration comments would cause undefined behavior and 126 | * should be ignored during postprocessing. Fixes to actual code after 127 | * configuration comments need to be mapped back to the code block after 128 | * removing any offset due to configuration comments. 129 | * 130 | * Fenced code blocks can be indented by up to three spaces at the opening 131 | * fence. Inside of a list, for example, this indent can be in addition to the 132 | * indent already required for list item children. Leading whitespace inside 133 | * indented code blocks is trimmed up to the level of the opening fence and does 134 | * not appear in the linted code. Further, lines can have less leading 135 | * whitespace than the opening fence, so not all lines are guaranteed to have 136 | * the same column offset as the opening fence. 137 | * 138 | * The source code of a non-configuration-comment line in the linted JS is a 139 | * suffix of the corresponding line in the Markdown code block. There are no 140 | * differences within the line, so the mapping need only provide the offset 141 | * delta at the beginning of each line. 142 | * @param {string} text The text of the file. 143 | * @param {ASTNode} node A Markdown code block AST node. 144 | * @param {string[]} comments List of configuration comment strings that will be 145 | * inserted at the beginning of the code block. 146 | * @returns {RangeMap[]} A list of offset-based adjustments, where lookups are 147 | * done based on the `js` key, which represents the range in the linted JS, 148 | * and the `md` key is the offset delta that, when added to the JS range, 149 | * returns the corresponding location in the original Markdown source. 150 | */ 151 | function getBlockRangeMap(text, node, comments) { 152 | 153 | /* 154 | * The parser sets the fenced code block's start offset to wherever content 155 | * should normally begin (typically the first column of the line, but more 156 | * inside a list item, for example). The code block's opening fence may be 157 | * further indented by up to three characters. If the code block has 158 | * additional indenting, the opening fence's first backtick may be up to 159 | * three whitespace characters after the start offset. 160 | */ 161 | const startOffset = getBeginningOfLineOffset(node); 162 | 163 | /* 164 | * Extract the Markdown source to determine the leading whitespace for each 165 | * line. 166 | */ 167 | const code = text.slice(startOffset, node.position.end.offset); 168 | const lines = code.split("\n"); 169 | 170 | /* 171 | * The parser trims leading whitespace from each line of code within the 172 | * fenced code block up to the opening fence's first backtick. The first 173 | * backtick's column is the AST node's starting column plus any additional 174 | * indentation. 175 | */ 176 | const baseIndent = getIndentText(text, node).length; 177 | 178 | /* 179 | * Track the length of any inserted configuration comments at the beginning 180 | * of the linted JS and start the JS offset lookup keys at this index. 181 | */ 182 | const commentLength = comments.reduce((len, comment) => len + comment.length + 1, 0); 183 | 184 | /* 185 | * In case there are configuration comments, initialize the map so that the 186 | * first lookup index is always 0. If there are no configuration comments, 187 | * the lookup index will also be 0, and the lookup should always go to the 188 | * last range that matches, skipping this initialization entry. 189 | */ 190 | const rangeMap = [{ 191 | indent: baseIndent, 192 | js: 0, 193 | md: 0 194 | }]; 195 | 196 | // Start the JS offset after any configuration comments. 197 | let jsOffset = commentLength; 198 | 199 | /* 200 | * Start the Markdown offset at the beginning of the block's first line of 201 | * actual code. The first line of the block is always the opening fence, so 202 | * the code begins on the second line. 203 | */ 204 | let mdOffset = startOffset + lines[0].length + 1; 205 | 206 | /* 207 | * For each line, determine how much leading whitespace was trimmed due to 208 | * indentation. Increase the JS lookup offset by the length of the line 209 | * post-trimming and the Markdown offset by the total line length. 210 | */ 211 | for (let i = 0; i + 1 < lines.length; i++) { 212 | const line = lines[i + 1]; 213 | const leadingWhitespaceLength = leadingWhitespaceRegex.exec(line)[0].length; 214 | 215 | // The parser trims leading whitespace up to the level of the opening 216 | // fence, so keep any additional indentation beyond that. 217 | const trimLength = Math.min(baseIndent, leadingWhitespaceLength); 218 | 219 | rangeMap.push({ 220 | indent: trimLength, 221 | js: jsOffset, 222 | 223 | // Advance `trimLength` character from the beginning of the Markdown 224 | // line to the beginning of the equivalent JS line, then compute the 225 | // delta. 226 | md: mdOffset + trimLength - jsOffset 227 | }); 228 | 229 | // Accumulate the current line in the offsets, and don't forget the 230 | // newline. 231 | mdOffset += line.length + 1; 232 | jsOffset += line.length - trimLength + 1; 233 | } 234 | 235 | return rangeMap; 236 | } 237 | 238 | /** 239 | * Extracts lintable code blocks from Markdown text. 240 | * @param {string} text The text of the file. 241 | * @param {string} filename The filename of the file 242 | * @returns {Array<{ filename: string, text: string }>} Source code blocks to lint. 243 | */ 244 | function preprocess(text, filename) { 245 | const ast = parse(text); 246 | const blocks = []; 247 | 248 | blocksCache.set(filename, blocks); 249 | 250 | /** 251 | * During the depth-first traversal, keep track of any sequences of HTML 252 | * comment nodes containing `eslint-*` or `global` comments. If a code 253 | * block immediately follows such a sequence, insert the comments at the 254 | * top of the code block. Any non-ESLint comment or other node type breaks 255 | * and empties the sequence. 256 | * @type {string[]} 257 | */ 258 | let htmlComments = []; 259 | 260 | traverse(ast, { 261 | "*"() { 262 | htmlComments = []; 263 | }, 264 | code(node) { 265 | if (node.lang) { 266 | const comments = []; 267 | 268 | for (const comment of htmlComments) { 269 | if (comment.trim() === "eslint-skip") { 270 | htmlComments = []; 271 | return; 272 | } 273 | 274 | comments.push(`/*${comment}*/`); 275 | } 276 | 277 | htmlComments = []; 278 | 279 | blocks.push({ 280 | ...node, 281 | baseIndentText: getIndentText(text, node), 282 | comments, 283 | rangeMap: getBlockRangeMap(text, node, comments) 284 | }); 285 | } 286 | }, 287 | html(node) { 288 | const comment = getComment(node.value); 289 | 290 | if (comment) { 291 | htmlComments.push(comment); 292 | } else { 293 | htmlComments = []; 294 | } 295 | } 296 | }); 297 | 298 | return blocks.map((block, index) => ({ 299 | filename: `${index}.${block.lang.trim().split(" ")[0]}`, 300 | text: [ 301 | ...block.comments, 302 | block.value, 303 | "" 304 | ].join("\n") 305 | })); 306 | } 307 | 308 | /** 309 | * Creates a map function that adjusts messages in a code block. 310 | * @param {Block} block A code block. 311 | * @returns {(message: Message) => Message} A function that adjusts messages in a code block. 312 | */ 313 | function adjustBlock(block) { 314 | const leadingCommentLines = block.comments.reduce((count, comment) => count + comment.split("\n").length, 0); 315 | 316 | const blockStart = block.position.start.line; 317 | 318 | /** 319 | * Adjusts ESLint messages to point to the correct location in the Markdown. 320 | * @param {Message} message A message from ESLint. 321 | * @returns {Message} The same message, but adjusted to the correct location. 322 | */ 323 | return function adjustMessage(message) { 324 | if (!Number.isInteger(message.line)) { 325 | return { 326 | ...message, 327 | line: blockStart, 328 | column: block.position.start.column 329 | }; 330 | } 331 | 332 | const lineInCode = message.line - leadingCommentLines; 333 | 334 | if (lineInCode < 1) { 335 | return null; 336 | } 337 | 338 | const out = { 339 | line: lineInCode + blockStart, 340 | column: message.column + block.rangeMap[lineInCode].indent 341 | }; 342 | 343 | if (Number.isInteger(message.endLine)) { 344 | out.endLine = message.endLine - leadingCommentLines + blockStart; 345 | } 346 | 347 | const adjustedFix = {}; 348 | 349 | if (message.fix) { 350 | adjustedFix.fix = { 351 | range: message.fix.range.map(range => { 352 | 353 | // Advance through the block's range map to find the last 354 | // matching range by finding the first range too far and 355 | // then going back one. 356 | let i = 1; 357 | 358 | while (i < block.rangeMap.length && block.rangeMap[i].js <= range) { 359 | i++; 360 | } 361 | 362 | // Apply the mapping delta for this range. 363 | return range + block.rangeMap[i - 1].md; 364 | }), 365 | text: message.fix.text.replace(/\n/gu, `\n${block.baseIndentText}`) 366 | }; 367 | } 368 | 369 | return { ...message, ...out, ...adjustedFix }; 370 | }; 371 | } 372 | 373 | /** 374 | * Excludes unsatisfiable rules from the list of messages. 375 | * @param {Message} message A message from the linter. 376 | * @returns {boolean} True if the message should be included in output. 377 | */ 378 | function excludeUnsatisfiableRules(message) { 379 | return message && UNSATISFIABLE_RULES.indexOf(message.ruleId) < 0; 380 | } 381 | 382 | /** 383 | * Transforms generated messages for output. 384 | * @param {Array} messages An array containing one array of messages 385 | * for each code block returned from `preprocess`. 386 | * @param {string} filename The filename of the file 387 | * @returns {Message[]} A flattened array of messages with mapped locations. 388 | */ 389 | function postprocess(messages, filename) { 390 | const blocks = blocksCache.get(filename); 391 | 392 | blocksCache.delete(filename); 393 | 394 | return [].concat(...messages.map((group, i) => { 395 | const adjust = adjustBlock(blocks[i]); 396 | 397 | return group.map(adjust).filter(excludeUnsatisfiableRules); 398 | })); 399 | } 400 | 401 | module.exports = { 402 | preprocess, 403 | postprocess, 404 | supportsAutofix: SUPPORTS_AUTOFIX 405 | }; 406 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [3.0.1](https://github.com/eslint/eslint-plugin-markdown/compare/v3.0.0...v3.0.1) (2023-07-15) 4 | 5 | 6 | ### Chores 7 | 8 | * add Node v19 ([#212](https://github.com/eslint/eslint-plugin-markdown/issues/212)) ([81ff967](https://github.com/eslint/eslint-plugin-markdown/commit/81ff967a325608e44b7bb467f8359ab620528dac)) 9 | * add triage action ([#213](https://github.com/eslint/eslint-plugin-markdown/issues/213)) ([ef7dcdc](https://github.com/eslint/eslint-plugin-markdown/commit/ef7dcdccfb94ac7e657a6c66998ce8831f3e58fd)) 10 | * generate provenance statements when release ([#222](https://github.com/eslint/eslint-plugin-markdown/issues/222)) ([30ae649](https://github.com/eslint/eslint-plugin-markdown/commit/30ae6492e48328672c10da3a7a5bead850b03b52)) 11 | * run tests on Node.js v20 ([#215](https://github.com/eslint/eslint-plugin-markdown/issues/215)) ([f5ce090](https://github.com/eslint/eslint-plugin-markdown/commit/f5ce090010659cd55e38348083585ab116d9b19a)) 12 | * set up release-please ([#219](https://github.com/eslint/eslint-plugin-markdown/issues/219)) ([311c626](https://github.com/eslint/eslint-plugin-markdown/commit/311c626ac9f9ec05aa8bc6915e995f0a0c408891)) 13 | 14 | v3.0.0 - July 16, 2022 15 | 16 | * [`558ae3c`](https://github.com/eslint/eslint-plugin-markdown/commit/558ae3ca2b2b35f7a389aa37389d322dc3d3630c) chore: add node v18 (#205) (Amaresh S M) 17 | * [`071fa66`](https://github.com/eslint/eslint-plugin-markdown/commit/071fa661875e4bd88a91dcd39eee9276bf3f2b0a) feat!: drop node v8 and v10 (#203) (Amaresh S M) 18 | * [`f186730`](https://github.com/eslint/eslint-plugin-markdown/commit/f186730bd7420e251da6469f07f3d873d9259abd) ci: update github actions (#207) (Deepshika S) 19 | * [`6570c82`](https://github.com/eslint/eslint-plugin-markdown/commit/6570c829155a2ca802195c1efd3623e62ca18f4e) ci: Work around npm behavior changes to fix CI on main (#206) (Brandon Mills) 20 | * [`87c2b53`](https://github.com/eslint/eslint-plugin-markdown/commit/87c2b536fd80b15e134766d92b90048ae45cbe1f) docs: update badges (#202) (Milos Djermanovic) 21 | * [`2fd5b89`](https://github.com/eslint/eslint-plugin-markdown/commit/2fd5b89a589a5b25677983c0228bd2a27e60ba00) chore: add tests for ESLint 8 (#195) (Michaël De Boey) 22 | * [`8db0978`](https://github.com/eslint/eslint-plugin-markdown/commit/8db097895222a8b0ac0e85b68728829dc508701f) chore: Check for package.json in examples (#200) (Brandon Mills) 23 | * [`b695396`](https://github.com/eslint/eslint-plugin-markdown/commit/b69539679a2bd4f9dc1b0b52bae68fba85749187) test: test with `ESLint` instead of `CLIEngine` when available (#198) (Michaël De Boey) 24 | * [`e1ddcc5`](https://github.com/eslint/eslint-plugin-markdown/commit/e1ddcc5a274bbb4902b8ac8029928a3b2aff5a51) ci: use node `v16` (#199) (Nitin Kumar) 25 | * [`8f590fc`](https://github.com/eslint/eslint-plugin-markdown/commit/8f590fc3bc61df4e72a06da3e6bf3264df9eea54) chore: update `devDependencies` (#197) (Michaël De Boey) 26 | * [`3667566`](https://github.com/eslint/eslint-plugin-markdown/commit/36675663b83bf02bcde5b7bd8895f7fd4d0b7451) chore: test all supported ESLint versions (#196) (Michaël De Boey) 27 | * [`ecae4fe`](https://github.com/eslint/eslint-plugin-markdown/commit/ecae4fe7ee53afeefbb8a2627b6bde38a4e5d297) Chore: ignore pnpm-lock.yaml (#193) (Nitin Kumar) 28 | * [`ffdb245`](https://github.com/eslint/eslint-plugin-markdown/commit/ffdb24573dff0728342e21c44013fa78882352d9) Chore: use `actions/setup-node@v2` in CI (#192) (Nitin Kumar) 29 | 30 | v2.2.1 - September 11, 2021 31 | 32 | * [`3a40160`](https://github.com/eslint/eslint-plugin-markdown/commit/3a401606cb2ac4dae6b95720799ed1c611af32d0) Fix: `message.line` could be `undefined` (#191) (JounQin) 33 | 34 | v2.2.0 - May 26, 2021 35 | 36 | * [`32203f6`](https://github.com/eslint/eslint-plugin-markdown/commit/32203f6ec86ec5e220d18099863d94408f334665) Update: Replace Markdown parser (fixes #125, fixes #186) (#188) (Brandon Mills) 37 | 38 | v2.1.0 - April 25, 2021 39 | 40 | * [`f1e153b`](https://github.com/eslint/eslint-plugin-markdown/commit/f1e153b8b634af7121e87b505c3c882536f4e3a5) Update: Upgrade remark-parse to v7 (fixes #77, fixes #78) (#175) (Brandon Mills) 41 | 42 | v2.0.1 - April 5, 2021 43 | 44 | * [`d23d5f7`](https://github.com/eslint/eslint-plugin-markdown/commit/d23d5f739943d136669aac945ef25528f31cd7db) Fix: use blocksCache instead of single blocks instance (fixes #181) (#183) (JounQin) 45 | * [`a09a645`](https://github.com/eslint/eslint-plugin-markdown/commit/a09a6452c1031b029efb17fe606cc5f56cfa0d23) Chore: add yarn.lock and package-lock.json into .gitignore (#184) (JounQin) 46 | * [`1280ac1`](https://github.com/eslint/eslint-plugin-markdown/commit/1280ac1f4998e8ab2030742fe510cc02d200aea2) Docs: improve jsdoc, better for typings (#182) (JounQin) 47 | * [`79be776`](https://github.com/eslint/eslint-plugin-markdown/commit/79be776331cf2bb4db2f265ee6cf7260e90e3d5e) Fix: More reliable comment attachment (fixes #76) (#177) (Brandon Mills) 48 | 49 | v2.0.0 - February 14, 2021 50 | 51 | * [`53dc0e5`](https://github.com/eslint/eslint-plugin-markdown/commit/53dc0e56a86144a8b93b3e220116252058fa3144) Docs: Remove prerelease README notes (#173) (Brandon Mills) 52 | * [`140adf4`](https://github.com/eslint/eslint-plugin-markdown/commit/140adf42a9e103c5fdce5338b737fa0a7c47d38c) 2.0.0-rc.2 (ESLint Jenkins) 53 | * [`15d7aa6`](https://github.com/eslint/eslint-plugin-markdown/commit/15d7aa6cd830f769078b6eb6cf89ef3e6e04548f) Build: changelog update for 2.0.0-rc.2 (ESLint Jenkins) 54 | * [`f6a3fad`](https://github.com/eslint/eslint-plugin-markdown/commit/f6a3fada43aaeb613aaf9168dfd06a53b9db0ab4) Fix: overrides pattern for virtual filenames in recommended config (#169) (Milos Djermanovic) 55 | * [`390d508`](https://github.com/eslint/eslint-plugin-markdown/commit/390d508607aa6a5b1668633799d8e6b34a853d26) 2.0.0-rc.1 (ESLint Jenkins) 56 | * [`e05d6eb`](https://github.com/eslint/eslint-plugin-markdown/commit/e05d6ebdbcd87d0ac57ff037fcfe82cd2b0cca37) Build: changelog update for 2.0.0-rc.1 (ESLint Jenkins) 57 | * [`1dd7089`](https://github.com/eslint/eslint-plugin-markdown/commit/1dd70890b92827a5fbd3a86a62c3f2bc30389340) Fix: npm prepare script on Windows (refs #166) (#168) (Brandon Mills) 58 | * [`23ac2b9`](https://github.com/eslint/eslint-plugin-markdown/commit/23ac2b95b1c2666baf422c24f5b73607d315a700) Fix: Ignore words in info string after syntax (fixes #166) (#167) (Brandon Mills) 59 | * [`8f729d3`](https://github.com/eslint/eslint-plugin-markdown/commit/8f729d3f286820da8099aaf2708d54aa9edcc000) Chore: Switch to main for primary branch (fixes #161) (#165) (Brandon Mills) 60 | * [`d30c50f`](https://github.com/eslint/eslint-plugin-markdown/commit/d30c50f46237af2fdef0a8a21fb547ed8e6c4d80) Chore: Automatically install example dependencies (#164) (Brandon Mills) 61 | * [`2749b4d`](https://github.com/eslint/eslint-plugin-markdown/commit/2749b4deb8a8f8015721ecb5eb49bec8de2042c4) 2.0.0-rc.0 (ESLint Jenkins) 62 | * [`922a00e`](https://github.com/eslint/eslint-plugin-markdown/commit/922a00e286f548f2810ebe5fb534418ae9ba83a3) Build: changelog update for 2.0.0-rc.0 (ESLint Jenkins) 63 | * [`d94c22f`](https://github.com/eslint/eslint-plugin-markdown/commit/d94c22fa908c5ea93f5ca083438af3b108f440c2) Build: Install example test dependencies in Jenkins (#160) (Brandon Mills) 64 | * [`7f26cb9`](https://github.com/eslint/eslint-plugin-markdown/commit/7f26cb9a9d1b3c169f532200d12aee80d41bb3e7) Docs: Reference recommended config disabled rules (#159) (Brandon Mills) 65 | * [`bf7648f`](https://github.com/eslint/eslint-plugin-markdown/commit/bf7648f0ebdb5ac967059ee83708b46f389aa4a9) Docs: Add TypeScript example (#155) (Brandon Mills) 66 | * [`d80be9e`](https://github.com/eslint/eslint-plugin-markdown/commit/d80be9e0b668c0bf3b2176f0f82b5852d4559b59) New: Add rules to recommended config (#157) (Nikolay Stoynov) 67 | * [`fc4d7aa`](https://github.com/eslint/eslint-plugin-markdown/commit/fc4d7aa0612a3fdeeb26fbaf261e94547393ab48) Chore: run CI in Node 14.x (#158) (Kai Cataldo) 68 | * [`f2d4923`](https://github.com/eslint/eslint-plugin-markdown/commit/f2d4923d3b974a201077574fd6e6e7535152db96) Docs: Add React example (#152) (Brandon Mills) 69 | * [`eb66833`](https://github.com/eslint/eslint-plugin-markdown/commit/eb6683351f72735f52dad5018d4fa0c1b3f0f2a1) New: Add recommended config (fixes #151) (#153) (Brandon Mills) 70 | * [`0311640`](https://github.com/eslint/eslint-plugin-markdown/commit/03116401ae7be0c86b5a48d22aacd94df387a5df) Fix: Don't require message end locations (fixes #112) (#154) (Brandon Mills) 71 | * [`2bc9352`](https://github.com/eslint/eslint-plugin-markdown/commit/2bc93523e006b482a4c57a251c221e7b8711b66b) 2.0.0-alpha.0 (ESLint Jenkins) 72 | * [`c0ba401`](https://github.com/eslint/eslint-plugin-markdown/commit/c0ba401315323890ce072507a83ab9b3207aeff7) Build: changelog update for 2.0.0-alpha.0 (ESLint Jenkins) 73 | * [`51e48c6`](https://github.com/eslint/eslint-plugin-markdown/commit/51e48c68535964b1fe0f5c949d721baca4e6a1d6) Docs: Revamp documentation for v2 (#149) (Brandon Mills) 74 | * [`b221391`](https://github.com/eslint/eslint-plugin-markdown/commit/b2213919e3973ebb3788295143c17e14f5fc3f3b) Docs: Dogfood plugin by linting readme (#145) (Brandon Mills) 75 | * [`7423610`](https://github.com/eslint/eslint-plugin-markdown/commit/74236108b5c996b5f73046c2112270c7458cbae9) Docs: Explain use of --ext option in ESLint v7 (#146) (Brandon Mills) 76 | * [`0d4dbe8`](https://github.com/eslint/eslint-plugin-markdown/commit/0d4dbe8a50852516e14f656007c60e9e7a180b0a) Breaking: Implement new processor API (fixes #138) (#144) (Brandon Mills) 77 | * [`7eeafb8`](https://github.com/eslint/eslint-plugin-markdown/commit/7eeafb83e446f76bc4581381cd68dacc484b2249) Chore: Update ESLint config and plugins (#143) (Brandon Mills) 78 | * [`f483343`](https://github.com/eslint/eslint-plugin-markdown/commit/f4833438fa2c06941f05e994eb1084321ce4cfb3) Breaking: Require ESLint v6 (#142) (Brandon Mills) 79 | * [`9aa1fdc`](https://github.com/eslint/eslint-plugin-markdown/commit/9aa1fdca62733543d2c26a755ad14dbc02926f27) Chore: Use ES2018 object spread syntax (#141) (Brandon Mills) 80 | * [`f584cc6`](https://github.com/eslint/eslint-plugin-markdown/commit/f584cc6f08f0eeac0e657ae45cbf561764fab696) Build: Remove Travis (#140) (Brandon Mills) 81 | * [`35f9a11`](https://github.com/eslint/eslint-plugin-markdown/commit/35f9a11b407078774eef37295ba7ddb95c56f419) Breaking: Drop support for Node.js v6 (refs #138) (#137) (Brandon Mills) 82 | * [`6f02ef5`](https://github.com/eslint/eslint-plugin-markdown/commit/6f02ef53abc08b5e35b56361f2bd7cbc7ea8e993) Chore: Add npm version and build status badges (#139) (Brandon Mills) 83 | 84 | v2.0.0-rc.2 - January 30, 2021 85 | 86 | * [`f6a3fad`](https://github.com/eslint/eslint-plugin-markdown/commit/f6a3fada43aaeb613aaf9168dfd06a53b9db0ab4) Fix: overrides pattern for virtual filenames in recommended config (#169) (Milos Djermanovic) 87 | 88 | v2.0.0-rc.1 - December 20, 2020 89 | 90 | * [`1dd7089`](https://github.com/eslint/eslint-plugin-markdown/commit/1dd70890b92827a5fbd3a86a62c3f2bc30389340) Fix: npm prepare script on Windows (refs #166) (#168) (Brandon Mills) 91 | * [`23ac2b9`](https://github.com/eslint/eslint-plugin-markdown/commit/23ac2b95b1c2666baf422c24f5b73607d315a700) Fix: Ignore words in info string after syntax (fixes #166) (#167) (Brandon Mills) 92 | * [`8f729d3`](https://github.com/eslint/eslint-plugin-markdown/commit/8f729d3f286820da8099aaf2708d54aa9edcc000) Chore: Switch to main for primary branch (fixes #161) (#165) (Brandon Mills) 93 | * [`d30c50f`](https://github.com/eslint/eslint-plugin-markdown/commit/d30c50f46237af2fdef0a8a21fb547ed8e6c4d80) Chore: Automatically install example dependencies (#164) (Brandon Mills) 94 | 95 | v2.0.0-rc.0 - August 19, 2020 96 | 97 | * [`d94c22f`](https://github.com/eslint/eslint-plugin-markdown/commit/d94c22fa908c5ea93f5ca083438af3b108f440c2) Build: Install example test dependencies in Jenkins (#160) (Brandon Mills) 98 | * [`7f26cb9`](https://github.com/eslint/eslint-plugin-markdown/commit/7f26cb9a9d1b3c169f532200d12aee80d41bb3e7) Docs: Reference recommended config disabled rules (#159) (Brandon Mills) 99 | * [`bf7648f`](https://github.com/eslint/eslint-plugin-markdown/commit/bf7648f0ebdb5ac967059ee83708b46f389aa4a9) Docs: Add TypeScript example (#155) (Brandon Mills) 100 | * [`d80be9e`](https://github.com/eslint/eslint-plugin-markdown/commit/d80be9e0b668c0bf3b2176f0f82b5852d4559b59) New: Add rules to recommended config (#157) (Nikolay Stoynov) 101 | * [`fc4d7aa`](https://github.com/eslint/eslint-plugin-markdown/commit/fc4d7aa0612a3fdeeb26fbaf261e94547393ab48) Chore: run CI in Node 14.x (#158) (Kai Cataldo) 102 | * [`f2d4923`](https://github.com/eslint/eslint-plugin-markdown/commit/f2d4923d3b974a201077574fd6e6e7535152db96) Docs: Add React example (#152) (Brandon Mills) 103 | * [`eb66833`](https://github.com/eslint/eslint-plugin-markdown/commit/eb6683351f72735f52dad5018d4fa0c1b3f0f2a1) New: Add recommended config (fixes #151) (#153) (Brandon Mills) 104 | * [`0311640`](https://github.com/eslint/eslint-plugin-markdown/commit/03116401ae7be0c86b5a48d22aacd94df387a5df) Fix: Don't require message end locations (fixes #112) (#154) (Brandon Mills) 105 | 106 | v2.0.0-alpha.0 - April 12, 2020 107 | 108 | * [`51e48c6`](https://github.com/eslint/eslint-plugin-markdown/commit/51e48c68535964b1fe0f5c949d721baca4e6a1d6) Docs: Revamp documentation for v2 (#149) (Brandon Mills) 109 | * [`b221391`](https://github.com/eslint/eslint-plugin-markdown/commit/b2213919e3973ebb3788295143c17e14f5fc3f3b) Docs: Dogfood plugin by linting readme (#145) (Brandon Mills) 110 | * [`7423610`](https://github.com/eslint/eslint-plugin-markdown/commit/74236108b5c996b5f73046c2112270c7458cbae9) Docs: Explain use of --ext option in ESLint v7 (#146) (Brandon Mills) 111 | * [`0d4dbe8`](https://github.com/eslint/eslint-plugin-markdown/commit/0d4dbe8a50852516e14f656007c60e9e7a180b0a) Breaking: Implement new processor API (fixes #138) (#144) (Brandon Mills) 112 | * [`7eeafb8`](https://github.com/eslint/eslint-plugin-markdown/commit/7eeafb83e446f76bc4581381cd68dacc484b2249) Chore: Update ESLint config and plugins (#143) (Brandon Mills) 113 | * [`f483343`](https://github.com/eslint/eslint-plugin-markdown/commit/f4833438fa2c06941f05e994eb1084321ce4cfb3) Breaking: Require ESLint v6 (#142) (Brandon Mills) 114 | * [`9aa1fdc`](https://github.com/eslint/eslint-plugin-markdown/commit/9aa1fdca62733543d2c26a755ad14dbc02926f27) Chore: Use ES2018 object spread syntax (#141) (Brandon Mills) 115 | * [`f584cc6`](https://github.com/eslint/eslint-plugin-markdown/commit/f584cc6f08f0eeac0e657ae45cbf561764fab696) Build: Remove Travis (#140) (Brandon Mills) 116 | * [`35f9a11`](https://github.com/eslint/eslint-plugin-markdown/commit/35f9a11b407078774eef37295ba7ddb95c56f419) Breaking: Drop support for Node.js v6 (refs #138) (#137) (Brandon Mills) 117 | * [`6f02ef5`](https://github.com/eslint/eslint-plugin-markdown/commit/6f02ef53abc08b5e35b56361f2bd7cbc7ea8e993) Chore: Add npm version and build status badges (#139) (Brandon Mills) 118 | 119 | v1.0.2 - February 24, 2020 120 | 121 | * [`52e0984`](https://github.com/eslint/eslint-plugin-markdown/commit/52e098483fdf958a8dce96ab66c52b4337d522fe) Upgrade: Update devDeps and change istanbul -> nyc (#130) (Brett Zamir) 122 | * [`d52988f`](https://github.com/eslint/eslint-plugin-markdown/commit/d52988f5efcacb16862c79c1857e9b912cf3ffb0) Chore: Remove call to lint absent `Makefile.js` (#129) (Brett Zamir) 123 | * [`5640ea6`](https://github.com/eslint/eslint-plugin-markdown/commit/5640ea65730abab5c9c97d77b5708f3499ec62f3) Fix: Apply base indent to multiple line breaks (fixes #127) (#128) (Brett Zamir) 124 | 125 | v1.0.1 - October 21, 2019 126 | 127 | * [`fb0b5a3`](https://github.com/eslint/eslint-plugin-markdown/commit/fb0b5a3fc36ad362556cafc49929f49e3b4bc6b0) Fix: Indent multiline fixes (fixes #120) (#124) (Brandon Mills) 128 | * [`07c9017`](https://github.com/eslint/eslint-plugin-markdown/commit/07c9017551d3a3382126882cf08bc162afcab734) Chore: Use GitHub Actions (#123) (Brandon Mills) 129 | * [`b5bf014`](https://github.com/eslint/eslint-plugin-markdown/commit/b5bf01465252a5d5ae3e1849b99b7d37bcd5a030) Chore: Add Node 12 to Travis (#122) (Brandon Mills) 130 | * [`dc90961`](https://github.com/eslint/eslint-plugin-markdown/commit/dc909618aa8f39e84279f5bdeb4a888d56d919b1) Fix: Support autofix at the very start of blocks (fixes #117) (#119) (Simon Lydell) 131 | * [`2de2490`](https://github.com/eslint/eslint-plugin-markdown/commit/2de2490f6d9dd5073bd9662d7ec6d61ceb13a811) Docs: Syntax highlight Markdown (#116) (Brett Zamir) 132 | * [`fdacf0c`](https://github.com/eslint/eslint-plugin-markdown/commit/fdacf0c29e4c9267816df0918f1b372fbd8eef32) Chore: Upgrade to eslint-config-eslint@5.0.1 (#110) (Brandon Mills) 133 | 134 | v1.0.0 - January 2, 2019 135 | 136 | * [`2a8482e`](https://github.com/eslint/eslint-plugin-markdown/commit/2a8482e8e39da2ab4a1d8aeb7459f26a8377905d) Fix: `overrides` general docs and Atom linter-eslint tips (fixes #109) (#111) (Brett Zamir) 137 | 138 | v1.0.0-rc.1 - November 5, 2018 139 | 140 | * a2f4492 Fix: Allowing eslint-plugin-prettier to work (fixes #101) (#107) (simlu) 141 | 142 | v1.0.0-rc.0 - October 27, 2018 143 | 144 | * 8fe9a0e New: Enable autofix with --fix (fixes #58) (#97) (Bohdan Khodakivskyi) 145 | * a5d0cce Fix: Ignore anything after space in code fence's language (fixes #98) (#99) (Francisco Ryan Tolmasky I) 146 | * 6fd340d Upgrade: eslint-release@1.0.0 (#100) (Teddy Katz) 147 | * dff8e9c Fix: Emit correct endLine numbers (#88) (Paul Murray) 148 | * 83f00d0 Docs: Suggest disabling strict in .md files (fixes #94) (#95) (Brandon Mills) 149 | * 3b4ff95 Build: Test against Node v10 (#96) (Brandon Mills) 150 | * 6777977 Breaking: required node version 6+ (#89) (薛定谔的猫) 151 | * 5582fce Docs: Updating CLA link (#93) (Pablo Nevares) 152 | * 24070e6 Build: Upgrade to eslint-release@0.11.1 (#92) (Brandon Mills) 153 | * 6cfd1f0 Docs: Add unicode-bom to list of unsatisfiable rules (#91) (Brandon Mills) 154 | 155 | v1.0.0-beta.8 - April 8, 2018 156 | 157 | * a1544c2 Chore: Add .npmrc to disable creating package-lock.json (#90) (Brandon Mills) 158 | * 47ad3f9 Chore: Replace global comment integration test with unit test (refs #81) (#85) (Brandon Mills) 159 | * e34acc6 Fix: Add unicode-bom to unsatisfiable rules (refs #75) (#84) (Brandon Mills) 160 | * 7c19f8b Fix: Support globals (fixes #79) (#81) (Anders D. Johnson) 161 | 162 | v1.0.0-beta.7 - July 2, 2017 163 | 164 | * f8ba18a New: Custom eslint-skip HTML comment skips blocks (fixes #69) (#73) (Brandon Mills) 165 | * 249904f Chore: Add test for code fences without blank lines (#72) (Brandon Mills) 166 | * 3abc569 Chore: Un-disable strict and eol-last in repository (#71) (Brandon Mills) 167 | * 132ea5b Chore: Add test ensuring config comments do not fall through (#70) (Brandon Mills) 168 | 169 | v1.0.0-beta.6 - April 29, 2017 170 | 171 | * c5e9d67 Build: Explicitly specify package.json files (#67) (Brandon Mills) 172 | 173 | v1.0.0-beta.5 - April 29, 2017 174 | 175 | * 7bd0f6e Build: Install eslint-release (#66) (Brandon Mills) 176 | * 48122eb Build: Dogfood plugin without npm link (#65) (Brandon Mills) 177 | * cc7deea Chore: Increase code coverage (#64) (Brandon Mills) 178 | * 29f2f05 Build: Use eslint-release (#63) (Brandon Mills) 179 | * d2f2962 Upgrade: remark (#62) (Titus) 180 | -------------------------------------------------------------------------------- /tests/lib/processor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for the Markdown processor. 3 | * @author Brandon Mills 4 | */ 5 | 6 | "use strict"; 7 | 8 | const assert = require("chai").assert; 9 | const processor = require("../../lib/processor"); 10 | 11 | describe("processor", () => { 12 | 13 | describe("preprocess", () => { 14 | 15 | it("should not crash", () => { 16 | processor.preprocess("Hello, world!"); 17 | }); 18 | 19 | it("should not crash on an empty string", () => { 20 | processor.preprocess(""); 21 | }); 22 | 23 | it("should return an array", () => { 24 | assert.isArray(processor.preprocess("Hello, world!")); 25 | }); 26 | 27 | it("should ignore normal text", () => { 28 | const blocks = processor.preprocess("Hello, world!"); 29 | 30 | assert.strictEqual(blocks.length, 0); 31 | }); 32 | 33 | it("should ignore inline code", () => { 34 | const blocks = processor.preprocess("Hello, `{{name}}!"); 35 | 36 | assert.strictEqual(blocks.length, 0); 37 | }); 38 | 39 | it("should ignore space-indented code blocks", () => { 40 | const code = [ 41 | "Hello, world!", 42 | " ", 43 | " var answer = 6 * 7;", 44 | " ", 45 | "Goodbye" 46 | ].join("\n"); 47 | const blocks = processor.preprocess(code); 48 | 49 | assert.strictEqual(blocks.length, 0); 50 | }); 51 | 52 | it("should ignore 4-space-indented code fences", () => { 53 | const code = [ 54 | "Hello, world!", 55 | " ```js", 56 | " var answer = 6 * 7;", 57 | " ```", 58 | "Goodbye" 59 | ].join("\n"); 60 | const blocks = processor.preprocess(code); 61 | 62 | assert.strictEqual(blocks.length, 0); 63 | }); 64 | 65 | it("should ignore 4-space-indented fence ends", () => { 66 | const code = [ 67 | "Hello, world!", 68 | "```js", 69 | "var answer = 6 * 7;", 70 | " ```", 71 | "Goodbye" 72 | ].join("\n"); 73 | const blocks = processor.preprocess(code); 74 | 75 | assert.strictEqual(blocks.length, 1); 76 | assert.strictEqual(blocks[0].filename, "0.js"); 77 | assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\n ```\nGoodbye\n"); 78 | }); 79 | 80 | it("should ignore tab-indented code blocks", () => { 81 | const code = [ 82 | "Hello, world!", 83 | "\t", 84 | "\tvar answer = 6 * 7;", 85 | "\t", 86 | "Goodbye" 87 | ].join("\n"); 88 | const blocks = processor.preprocess(code); 89 | 90 | assert.strictEqual(blocks.length, 0); 91 | }); 92 | 93 | it("should terminate blocks at EOF", () => { 94 | const code = [ 95 | "Hello, world!", 96 | "```js", 97 | "var answer = 6 * 7;" 98 | ].join("\n"); 99 | const blocks = processor.preprocess(code); 100 | 101 | assert.strictEqual(blocks.length, 1); 102 | assert.strictEqual(blocks[0].filename, "0.js"); 103 | assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\n"); 104 | }); 105 | 106 | it("should allow backticks or tildes", () => { 107 | const code = [ 108 | "```js", 109 | "backticks", 110 | "```", 111 | "~~~javascript", 112 | "tildes", 113 | "~~~" 114 | ].join("\n"); 115 | const blocks = processor.preprocess(code); 116 | 117 | assert.strictEqual(blocks.length, 2); 118 | assert.strictEqual(blocks[0].filename, "0.js"); 119 | assert.strictEqual(blocks[0].text, "backticks\n"); 120 | assert.strictEqual(blocks[1].filename, "1.javascript"); 121 | assert.strictEqual(blocks[1].text, "tildes\n"); 122 | }); 123 | 124 | it("should allow more than three fence characters", () => { 125 | const code = [ 126 | "````js", 127 | "four", 128 | "````" 129 | ].join("\n"); 130 | const blocks = processor.preprocess(code); 131 | 132 | assert.strictEqual(blocks.length, 1); 133 | assert.strictEqual(blocks[0].filename, "0.js"); 134 | assert.strictEqual(blocks[0].text, "four\n"); 135 | }); 136 | 137 | it("should require end fences at least as long as the starting fence", () => { 138 | const code = [ 139 | "````js", 140 | "four", 141 | "```", 142 | "````", 143 | "`````js", 144 | "five", 145 | "`````", 146 | "``````js", 147 | "six", 148 | "```````" 149 | ].join("\n"); 150 | const blocks = processor.preprocess(code); 151 | 152 | assert.strictEqual(blocks.length, 3); 153 | assert.strictEqual(blocks[0].filename, "0.js"); 154 | assert.strictEqual(blocks[0].text, "four\n```\n"); 155 | assert.strictEqual(blocks[1].filename, "1.js"); 156 | assert.strictEqual(blocks[1].text, "five\n"); 157 | assert.strictEqual(blocks[2].filename, "2.js"); 158 | assert.strictEqual(blocks[2].text, "six\n"); 159 | }); 160 | 161 | it("should not allow other content on ending fence line", () => { 162 | const code = [ 163 | "```js", 164 | "test();", 165 | "``` end", 166 | "```" 167 | ].join("\n"); 168 | const blocks = processor.preprocess(code); 169 | 170 | assert.strictEqual(blocks.length, 1); 171 | assert.strictEqual(blocks[0].filename, "0.js"); 172 | assert.strictEqual(blocks[0].text, "test();\n``` end\n"); 173 | }); 174 | 175 | it("should allow empty blocks", () => { 176 | const code = [ 177 | "```js", 178 | "", 179 | "````" 180 | ].join("\n"); 181 | const blocks = processor.preprocess(code); 182 | 183 | assert.strictEqual(blocks.length, 1); 184 | assert.strictEqual(blocks[0].filename, "0.js"); 185 | assert.strictEqual(blocks[0].text, "\n"); 186 | }); 187 | 188 | it("should allow whitespace-only blocks", () => { 189 | const code = [ 190 | " ```js", 191 | "", 192 | " ", 193 | " ", 194 | " ", 195 | " ", 196 | "```" 197 | ].join("\n"); 198 | const blocks = processor.preprocess(code); 199 | 200 | assert.strictEqual(blocks.length, 1); 201 | assert.strictEqual(blocks[0].filename, "0.js"); 202 | assert.strictEqual(blocks[0].text, "\n\n\n \n \n"); 203 | }); 204 | 205 | it("should preserve leading and trailing empty lines", () => { 206 | const code = [ 207 | "```js", 208 | "", 209 | "console.log(42);", 210 | "", 211 | "```" 212 | ].join("\n"); 213 | const blocks = processor.preprocess(code); 214 | 215 | assert.strictEqual(blocks[0].filename, "0.js"); 216 | assert.strictEqual(blocks[0].text, "\nconsole.log(42);\n\n"); 217 | }); 218 | 219 | it("should ignore code fences with unspecified info string", () => { 220 | const code = [ 221 | "```", 222 | "var answer = 6 * 7;", 223 | "```" 224 | ].join("\n"); 225 | const blocks = processor.preprocess(code); 226 | 227 | assert.strictEqual(blocks.length, 0); 228 | }); 229 | 230 | it("should find code fences with js info string", () => { 231 | const code = [ 232 | "```js", 233 | "var answer = 6 * 7;", 234 | "```" 235 | ].join("\n"); 236 | const blocks = processor.preprocess(code); 237 | 238 | assert.strictEqual(blocks.length, 1); 239 | assert.strictEqual(blocks[0].filename, "0.js"); 240 | }); 241 | 242 | it("should find code fences with javascript info string", () => { 243 | const code = [ 244 | "```javascript", 245 | "var answer = 6 * 7;", 246 | "```" 247 | ].join("\n"); 248 | const blocks = processor.preprocess(code); 249 | 250 | assert.strictEqual(blocks.length, 1); 251 | assert.strictEqual(blocks[0].filename, "0.javascript"); 252 | }); 253 | 254 | it("should find code fences with node info string", () => { 255 | const code = [ 256 | "```node", 257 | "var answer = 6 * 7;", 258 | "```" 259 | ].join("\n"); 260 | const blocks = processor.preprocess(code); 261 | 262 | assert.strictEqual(blocks.length, 1); 263 | assert.strictEqual(blocks[0].filename, "0.node"); 264 | }); 265 | 266 | it("should find code fences with jsx info string", () => { 267 | const code = [ 268 | "```jsx", 269 | "var answer = 6 * 7;", 270 | "```" 271 | ].join("\n"); 272 | const blocks = processor.preprocess(code); 273 | 274 | assert.strictEqual(blocks.length, 1); 275 | assert.strictEqual(blocks[0].filename, "0.jsx"); 276 | }); 277 | 278 | it("should find code fences ignoring info string case", () => { 279 | const code = [ 280 | "```JavaScript", 281 | "var answer = 6 * 7;", 282 | "```" 283 | ].join("\n"); 284 | const blocks = processor.preprocess(code); 285 | 286 | assert.strictEqual(blocks.length, 1); 287 | assert.strictEqual(blocks[0].filename, "0.JavaScript"); 288 | }); 289 | 290 | it("should ignore anything after the first word of the info string", () => { 291 | const code = [ 292 | "```js more words are ignored", 293 | "var answer = 6 * 7;", 294 | "```" 295 | ].join("\n"); 296 | const blocks = processor.preprocess(code); 297 | 298 | assert.strictEqual(blocks.length, 1); 299 | assert.strictEqual(blocks[0].filename, "0.js"); 300 | }); 301 | 302 | it("should ignore leading whitespace in the info string", () => { 303 | const code = [ 304 | "``` js ignores leading whitespace", 305 | "var answer = 6 * 7;", 306 | "```" 307 | ].join("\n"); 308 | const blocks = processor.preprocess(code); 309 | 310 | assert.strictEqual(blocks.length, 1); 311 | assert.strictEqual(blocks[0].filename, "0.js"); 312 | }); 313 | 314 | it("should ignore trailing whitespace in the info string", () => { 315 | const code = [ 316 | "```js ", 317 | "var answer = 6 * 7;", 318 | "```" 319 | ].join("\n"); 320 | const blocks = processor.preprocess(code); 321 | 322 | assert.strictEqual(blocks.length, 1); 323 | assert.strictEqual(blocks[0].filename, "0.js"); 324 | }); 325 | 326 | it("should find code fences not surrounded by blank lines", () => { 327 | const code = [ 328 | "", 329 | "```js", 330 | "var answer = 6 * 7;", 331 | "```", 332 | "Paragraph text", 333 | "```js", 334 | "var answer = 6 * 7;", 335 | "```" 336 | ].join("\n"); 337 | const blocks = processor.preprocess(code); 338 | 339 | assert.strictEqual(blocks.length, 2); 340 | assert.strictEqual(blocks[0].filename, "0.js"); 341 | assert.strictEqual(blocks[1].filename, "1.js"); 342 | }); 343 | 344 | it("should return the source code in the block", () => { 345 | const code = [ 346 | "```js", 347 | "var answer = 6 * 7;", 348 | "```" 349 | ].join("\n"); 350 | const blocks = processor.preprocess(code); 351 | 352 | assert.strictEqual(blocks[0].filename, "0.js"); 353 | assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\n"); 354 | }); 355 | 356 | it("should allow multi-line source code", () => { 357 | const code = [ 358 | "```js", 359 | "var answer = 6 * 7;", 360 | "console.log(answer);", 361 | "```" 362 | ].join("\n"); 363 | const blocks = processor.preprocess(code); 364 | 365 | assert.strictEqual(blocks[0].filename, "0.js"); 366 | assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\nconsole.log(answer);\n"); 367 | }); 368 | 369 | it("should preserve original line endings", () => { 370 | const code = [ 371 | "```js", 372 | "var answer = 6 * 7;", 373 | "console.log(answer);", 374 | "```" 375 | ].join("\r\n"); 376 | const blocks = processor.preprocess(code); 377 | 378 | assert.strictEqual(blocks[0].filename, "0.js"); 379 | assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\r\nconsole.log(answer);\n"); 380 | }); 381 | 382 | it("should unindent space-indented code fences", () => { 383 | const code = [ 384 | " ```js", 385 | " var answer = 6 * 7;", 386 | " console.log(answer);", 387 | " // Fin.", 388 | "```" 389 | ].join("\n"); 390 | const blocks = processor.preprocess(code); 391 | 392 | assert.strictEqual(blocks[0].filename, "0.js"); 393 | assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\n console.log(answer);\n// Fin.\n"); 394 | }); 395 | 396 | it("should find multiple code fences", () => { 397 | const code = [ 398 | "Hello, world!", 399 | "", 400 | "```js", 401 | "var answer = 6 * 7;", 402 | "```", 403 | "", 404 | "```javascript", 405 | "console.log(answer);", 406 | "```", 407 | "", 408 | "Goodbye" 409 | ].join("\n"); 410 | const blocks = processor.preprocess(code); 411 | 412 | assert.strictEqual(blocks.length, 2); 413 | assert.strictEqual(blocks[0].filename, "0.js"); 414 | assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\n"); 415 | assert.strictEqual(blocks[1].filename, "1.javascript"); 416 | assert.strictEqual(blocks[1].text, "console.log(answer);\n"); 417 | }); 418 | 419 | it("should insert leading configuration comments", () => { 420 | const code = [ 421 | "", 422 | "", 428 | "", 429 | "```js", 430 | "alert('Hello, world!');", 431 | "```" 432 | ].join("\n"); 433 | const blocks = processor.preprocess(code); 434 | 435 | assert.strictEqual(blocks.length, 1); 436 | assert.strictEqual(blocks[0].filename, "0.js"); 437 | assert.strictEqual(blocks[0].text, [ 438 | "/* eslint-env browser */", 439 | "/*", 440 | " eslint quotes: [", 441 | " \"error\",", 442 | " \"single\"", 443 | " ]", 444 | "*/", 445 | "alert('Hello, world!');", 446 | "" 447 | ].join("\n")); 448 | }); 449 | 450 | it("should insert global comments", () => { 451 | const code = [ 452 | "", 453 | "", 454 | "", 455 | "```js", 456 | "alert(foo, bar, baz);", 457 | "```" 458 | ].join("\n"); 459 | const blocks = processor.preprocess(code); 460 | 461 | assert.strictEqual(blocks.length, 1); 462 | assert.strictEqual(blocks[0].filename, "0.js"); 463 | assert.strictEqual(blocks[0].text, [ 464 | "/* global foo */", 465 | "/* global bar:false, baz:true */", 466 | "alert(foo, bar, baz);", 467 | "" 468 | ].join("\n")); 469 | }); 470 | 471 | // https://github.com/eslint/eslint-plugin-markdown/issues/76 472 | it("should insert comments inside list items", () => { 473 | const code = [ 474 | "* List item followed by a blank line", 475 | "", 476 | "", 477 | "```js", 478 | "console.log(\"Blank line\");", 479 | "```", 480 | "", 481 | "* List item without a blank line", 482 | "", 483 | "```js", 484 | "console.log(\"No blank line\");", 485 | "```" 486 | ].join("\n"); 487 | const blocks = processor.preprocess(code); 488 | 489 | assert.strictEqual(blocks.length, 2); 490 | assert.strictEqual(blocks[0].text, [ 491 | "/* eslint-disable no-console */", 492 | "console.log(\"Blank line\");", 493 | "" 494 | ].join("\n")); 495 | assert.strictEqual(blocks[1].text, [ 496 | "/* eslint-disable no-console */", 497 | "console.log(\"No blank line\");", 498 | "" 499 | ].join("\n")); 500 | }); 501 | 502 | it("should ignore non-eslint comments", () => { 503 | const code = [ 504 | "", 505 | "", 506 | "", 507 | "```js", 508 | "alert('Hello, world!');", 509 | "```" 510 | ].join("\n"); 511 | const blocks = processor.preprocess(code); 512 | 513 | assert.strictEqual(blocks.length, 1); 514 | assert.strictEqual(blocks[0].filename, "0.js"); 515 | assert.strictEqual(blocks[0].text, [ 516 | "alert('Hello, world!');", 517 | "" 518 | ].join("\n")); 519 | }); 520 | 521 | it("should ignore non-comment html", () => { 522 | const code = [ 523 | "", 524 | "

For example:

", 525 | "", 526 | "```js", 527 | "alert('Hello, world!');", 528 | "```" 529 | ].join("\n"); 530 | const blocks = processor.preprocess(code); 531 | 532 | assert.strictEqual(blocks.length, 1); 533 | assert.strictEqual(blocks[0].filename, "0.js"); 534 | assert.strictEqual(blocks[0].text, [ 535 | "alert('Hello, world!');", 536 | "" 537 | ].join("\n")); 538 | }); 539 | 540 | describe("eslint-skip", () => { 541 | 542 | it("should skip the next block", () => { 543 | const code = [ 544 | "", 545 | "", 546 | "```js", 547 | "alert('Hello, world!');", 548 | "```" 549 | ].join("\n"); 550 | const blocks = processor.preprocess(code); 551 | 552 | assert.strictEqual(blocks.length, 0); 553 | }); 554 | 555 | it("should skip only one block", () => { 556 | const code = [ 557 | "", 558 | "", 559 | "```js", 560 | "alert('Hello, world!');", 561 | "```", 562 | "", 563 | "```js", 564 | "var answer = 6 * 7;", 565 | "```" 566 | ].join("\n"); 567 | const blocks = processor.preprocess(code); 568 | 569 | assert.strictEqual(blocks.length, 1); 570 | assert.strictEqual(blocks[0].filename, "0.js"); 571 | assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\n"); 572 | }); 573 | 574 | it("should still work surrounded by other comments", () => { 575 | const code = [ 576 | "", 577 | "", 578 | "", 579 | "", 580 | "```js", 581 | "alert('Hello, world!');", 582 | "```", 583 | "", 584 | "```js", 585 | "var answer = 6 * 7;", 586 | "```" 587 | ].join("\n"); 588 | const blocks = processor.preprocess(code); 589 | 590 | assert.strictEqual(blocks.length, 1); 591 | assert.strictEqual(blocks[0].filename, "0.js"); 592 | assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\n"); 593 | }); 594 | 595 | }); 596 | 597 | }); 598 | 599 | describe("postprocess", () => { 600 | const code = [ 601 | "Hello, world!", 602 | "", 603 | "```js", 604 | "var answer = 6 * 7;", 605 | "if (answer === 42) {", 606 | " console.log(answer);", 607 | "}", 608 | "```", 609 | "", 610 | "Let's make a list.", 611 | "", 612 | "1. First item", 613 | "", 614 | " ```JavaScript", 615 | " var arr = [", 616 | " 1,", 617 | " 2", 618 | " ];", 619 | " ```", 620 | "", 621 | "1. Second item", 622 | "", 623 | " ```JS", 624 | " function boolean(arg) {", 625 | " \treturn", 626 | " \t!!arg;", 627 | "};", 628 | " ```" 629 | ].join("\n"); 630 | const messages = [ 631 | [ 632 | { line: 1, endLine: 1, column: 1, message: "Use the global form of \"use strict\".", ruleId: "strict" }, 633 | { line: 3, endLine: 3, column: 5, message: "Unexpected console statement.", ruleId: "no-console" } 634 | ], [ 635 | { line: 3, endLine: 3, column: 6, message: "Missing trailing comma.", ruleId: "comma-dangle", fix: { range: [24, 24], text: "," } } 636 | ], [ 637 | { line: 3, endLine: 6, column: 2, message: "Unreachable code after return.", ruleId: "no-unreachable" }, 638 | { line: 4, endLine: 4, column: 2, message: "Unnecessary semicolon.", ruleId: "no-extra-semi", fix: { range: [38, 39], text: "" } } 639 | ] 640 | ]; 641 | 642 | beforeEach(() => { 643 | processor.preprocess(code); 644 | }); 645 | 646 | it("should allow for no messages", () => { 647 | const result = processor.postprocess([[], [], []]); 648 | 649 | assert.strictEqual(result.length, 0); 650 | }); 651 | 652 | it("should flatten messages", () => { 653 | const result = processor.postprocess(messages); 654 | 655 | assert.strictEqual(result.length, 5); 656 | assert.strictEqual(result[0].message, "Use the global form of \"use strict\"."); 657 | assert.strictEqual(result[1].message, "Unexpected console statement."); 658 | assert.strictEqual(result[2].message, "Missing trailing comma."); 659 | assert.strictEqual(result[3].message, "Unreachable code after return."); 660 | assert.strictEqual(result[4].message, "Unnecessary semicolon."); 661 | }); 662 | 663 | it("should translate line numbers", () => { 664 | const result = processor.postprocess(messages); 665 | 666 | assert.strictEqual(result[0].line, 4); 667 | assert.strictEqual(result[1].line, 6); 668 | assert.strictEqual(result[2].line, 17); 669 | assert.strictEqual(result[3].line, 26); 670 | assert.strictEqual(result[4].line, 27); 671 | }); 672 | 673 | it("should translate endLine numbers", () => { 674 | const result = processor.postprocess(messages); 675 | 676 | assert.strictEqual(result[0].endLine, 4); 677 | assert.strictEqual(result[1].endLine, 6); 678 | assert.strictEqual(result[2].endLine, 17); 679 | assert.strictEqual(result[3].endLine, 29); 680 | assert.strictEqual(result[4].endLine, 27); 681 | }); 682 | 683 | it("should translate column numbers", () => { 684 | const result = processor.postprocess(messages); 685 | 686 | assert.strictEqual(result[0].column, 1); 687 | assert.strictEqual(result[1].column, 5); 688 | }); 689 | 690 | it("should translate indented column numbers", () => { 691 | const result = processor.postprocess(messages); 692 | 693 | assert.strictEqual(result[2].column, 9); 694 | assert.strictEqual(result[3].column, 4); 695 | assert.strictEqual(result[4].column, 2); 696 | }); 697 | 698 | it("should adjust fix range properties", () => { 699 | const result = processor.postprocess(messages); 700 | 701 | assert(result[2].fix.range, [185, 185]); 702 | assert(result[4].fix.range, [264, 265]); 703 | }); 704 | 705 | describe("should exclude messages from unsatisfiable rules", () => { 706 | 707 | it("eol-last", () => { 708 | const result = processor.postprocess([ 709 | [ 710 | { line: 4, column: 3, message: "Newline required at end of file but not found.", ruleId: "eol-last" } 711 | ] 712 | ]); 713 | 714 | assert.strictEqual(result.length, 0); 715 | }); 716 | 717 | it("unicode-bom", () => { 718 | const result = processor.postprocess([ 719 | [ 720 | { line: 1, column: 1, message: "Expected Unicode BOM (Byte Order Mark).", ruleId: "unicode-bom" } 721 | ] 722 | ]); 723 | 724 | assert.strictEqual(result.length, 0); 725 | }); 726 | 727 | }); 728 | 729 | it("should attach messages without `line` to opening code fence", () => { 730 | const message = { message: "Parsing error: \"parserOptions.project\" has been set for @typescript-eslint/parser.", ruleId: null }; 731 | const result = processor.postprocess([[message], [message], [message]]); 732 | 733 | assert.strictEqual(result.length, 3); 734 | assert.deepStrictEqual(result[0], { 735 | ...message, 736 | line: 3, 737 | column: 1 738 | }); 739 | assert.deepStrictEqual(result[1], { 740 | ...message, 741 | line: 14, 742 | column: 4 743 | }); 744 | assert.deepStrictEqual(result[2], { 745 | ...message, 746 | line: 23, 747 | column: 3 748 | }); 749 | }); 750 | 751 | }); 752 | 753 | describe("supportsAutofix", () => { 754 | it("should equal true", () => { 755 | assert.strictEqual(processor.supportsAutofix, true); 756 | }); 757 | }); 758 | 759 | }); 760 | -------------------------------------------------------------------------------- /tests/lib/plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for the preprocessor plugin. 3 | * @author Brandon Mills 4 | */ 5 | 6 | "use strict"; 7 | 8 | const assert = require("chai").assert; 9 | const execSync = require("child_process").execSync; 10 | const { CLIEngine, ESLint } = require("eslint"); 11 | const path = require("path"); 12 | const plugin = require("../.."); 13 | 14 | /** 15 | * @typedef {import('eslint/lib/cli-engine/cli-engine').CLIEngineOptions} CLIEngineOptions 16 | */ 17 | 18 | /** 19 | * Helper function which creates CLIEngine instance with enabled/disabled autofix feature. 20 | * @param {string} fixtureConfigName ESLint JSON config fixture filename. 21 | * @param {CLIEngineOptions} [options={}] Whether to enable autofix feature. 22 | * @returns {ESLint} ESLint instance to execute in tests. 23 | */ 24 | function initESLint(fixtureConfigName, options = {}) { 25 | if (ESLint) { // ESLint v7+ 26 | return new ESLint({ 27 | cwd: path.resolve(__dirname, "../fixtures/"), 28 | ignore: false, 29 | useEslintrc: false, 30 | overrideConfigFile: path.resolve(__dirname, "../fixtures/", fixtureConfigName), 31 | plugins: { markdown: plugin }, 32 | ...options 33 | }); 34 | } 35 | 36 | const cli = new CLIEngine({ 37 | cwd: path.resolve(__dirname, "../fixtures/"), 38 | ignore: false, 39 | useEslintrc: false, 40 | configFile: path.resolve(__dirname, "../fixtures/", fixtureConfigName), 41 | ...options 42 | }); 43 | 44 | cli.addPlugin("markdown", plugin); 45 | return { 46 | async calculateConfigForFile(filename) { 47 | return cli.getConfigForFile(filename); 48 | }, 49 | async lintFiles(files) { 50 | return cli.executeOnFiles(files).results; 51 | }, 52 | async lintText(text, { filePath }) { 53 | return cli.executeOnText(text, filePath).results; 54 | } 55 | }; 56 | } 57 | 58 | describe("recommended config", () => { 59 | let eslint; 60 | const shortText = [ 61 | "```js", 62 | "var unusedVar = console.log(undef);", 63 | "'unused expression';", 64 | "```" 65 | ].join("\n"); 66 | 67 | before(function() { 68 | try { 69 | 70 | // The tests for the recommended config will have ESLint import 71 | // the plugin, so we need to make sure it's resolvable and link it 72 | // if not. 73 | // eslint-disable-next-line node/no-extraneous-require 74 | require.resolve("eslint-plugin-markdown"); 75 | } catch (error) { 76 | if (error.code === "MODULE_NOT_FOUND") { 77 | 78 | // The npm link step can take longer than Mocha's default 2s 79 | // timeout, so give it more time. Mocha's API for customizing 80 | // hook-level timeouts uses `this`, so disable the rule. 81 | // https://mochajs.org/#hook-level 82 | // eslint-disable-next-line no-invalid-this 83 | this.timeout(30000); 84 | 85 | execSync("npm link && npm link eslint-plugin-markdown --legacy-peer-deps"); 86 | } else { 87 | throw error; 88 | } 89 | } 90 | 91 | eslint = initESLint("recommended.json"); 92 | }); 93 | 94 | it("should include the plugin", async () => { 95 | const config = await eslint.calculateConfigForFile("test.md"); 96 | 97 | assert.include(config.plugins, "markdown"); 98 | }); 99 | 100 | it("applies convenience configuration", async () => { 101 | const config = await eslint.calculateConfigForFile("subdir/test.md/0.js"); 102 | 103 | assert.deepStrictEqual(config.parserOptions, { 104 | ecmaFeatures: { 105 | impliedStrict: true 106 | } 107 | }); 108 | assert.deepStrictEqual(config.rules["eol-last"], ["off"]); 109 | assert.deepStrictEqual(config.rules["no-undef"], ["off"]); 110 | assert.deepStrictEqual(config.rules["no-unused-expressions"], ["off"]); 111 | assert.deepStrictEqual(config.rules["no-unused-vars"], ["off"]); 112 | assert.deepStrictEqual(config.rules["padded-blocks"], ["off"]); 113 | assert.deepStrictEqual(config.rules.strict, ["off"]); 114 | assert.deepStrictEqual(config.rules["unicode-bom"], ["off"]); 115 | }); 116 | 117 | it("overrides configure processor to parse .md file code blocks", async () => { 118 | const results = await eslint.lintText(shortText, { filePath: "test.md" }); 119 | 120 | assert.strictEqual(results.length, 1); 121 | assert.strictEqual(results[0].messages.length, 1); 122 | assert.strictEqual(results[0].messages[0].ruleId, "no-console"); 123 | }); 124 | 125 | }); 126 | 127 | describe("plugin", () => { 128 | let eslint; 129 | const shortText = [ 130 | "```js", 131 | "console.log(42);", 132 | "```" 133 | ].join("\n"); 134 | 135 | before(() => { 136 | eslint = initESLint("eslintrc.json"); 137 | }); 138 | 139 | it("should run on .md files", async () => { 140 | const results = await eslint.lintText(shortText, { filePath: "test.md" }); 141 | 142 | assert.strictEqual(results.length, 1); 143 | assert.strictEqual(results[0].messages.length, 1); 144 | assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); 145 | assert.strictEqual(results[0].messages[0].line, 2); 146 | }); 147 | 148 | it("should emit correct line numbers", async () => { 149 | const code = [ 150 | "# Hello, world!", 151 | "", 152 | "", 153 | "```js", 154 | "var bar = baz", 155 | "", 156 | "", 157 | "var foo = blah", 158 | "```" 159 | ].join("\n"); 160 | const results = await eslint.lintText(code, { filePath: "test.md" }); 161 | 162 | assert.strictEqual(results[0].messages[0].message, "'baz' is not defined."); 163 | assert.strictEqual(results[0].messages[0].line, 5); 164 | assert.strictEqual(results[0].messages[0].endLine, 5); 165 | assert.strictEqual(results[0].messages[1].message, "'blah' is not defined."); 166 | assert.strictEqual(results[0].messages[1].line, 8); 167 | assert.strictEqual(results[0].messages[1].endLine, 8); 168 | }); 169 | 170 | // https://github.com/eslint/eslint-plugin-markdown/issues/77 171 | it("should emit correct line numbers with leading blank line", async () => { 172 | const code = [ 173 | "### Heading", 174 | "", 175 | "```js", 176 | "", 177 | "console.log('a')", 178 | "```" 179 | ].join("\n"); 180 | const results = await eslint.lintText(code, { filePath: "test.md" }); 181 | 182 | assert.strictEqual(results[0].messages[0].line, 5); 183 | }); 184 | 185 | it("doesn't add end locations to messages without them", async () => { 186 | const code = [ 187 | "```js", 188 | "!@#$%^&*()", 189 | "```" 190 | ].join("\n"); 191 | const results = await eslint.lintText(code, { filePath: "test.md" }); 192 | 193 | assert.strictEqual(results.length, 1); 194 | assert.strictEqual(results[0].messages.length, 1); 195 | assert.notProperty(results[0].messages[0], "endLine"); 196 | assert.notProperty(results[0].messages[0], "endColumn"); 197 | }); 198 | 199 | it("should emit correct line numbers with leading comments", async () => { 200 | const code = [ 201 | "# Hello, world!", 202 | "", 203 | "", 204 | "", 205 | "", 206 | "```js", 207 | "var bar = baz", 208 | "", 209 | "var str = 'single quotes'", 210 | "", 211 | "var foo = blah", 212 | "```" 213 | ].join("\n"); 214 | const results = await eslint.lintText(code, { filePath: "test.md" }); 215 | 216 | assert.strictEqual(results[0].messages[0].message, "'baz' is not defined."); 217 | assert.strictEqual(results[0].messages[0].line, 7); 218 | assert.strictEqual(results[0].messages[0].endLine, 7); 219 | assert.strictEqual(results[0].messages[1].message, "'blah' is not defined."); 220 | assert.strictEqual(results[0].messages[1].line, 11); 221 | assert.strictEqual(results[0].messages[1].endLine, 11); 222 | }); 223 | 224 | it("should run on .mkdn files", async () => { 225 | const results = await eslint.lintText(shortText, { filePath: "test.mkdn" }); 226 | 227 | assert.strictEqual(results.length, 1); 228 | assert.strictEqual(results[0].messages.length, 1); 229 | assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); 230 | assert.strictEqual(results[0].messages[0].line, 2); 231 | }); 232 | 233 | it("should run on .mdown files", async () => { 234 | const results = await eslint.lintText(shortText, { filePath: "test.mdown" }); 235 | 236 | assert.strictEqual(results.length, 1); 237 | assert.strictEqual(results[0].messages.length, 1); 238 | assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); 239 | assert.strictEqual(results[0].messages[0].line, 2); 240 | }); 241 | 242 | it("should run on .markdown files", async () => { 243 | const results = await eslint.lintText(shortText, { filePath: "test.markdown" }); 244 | 245 | assert.strictEqual(results.length, 1); 246 | assert.strictEqual(results[0].messages.length, 1); 247 | assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); 248 | assert.strictEqual(results[0].messages[0].line, 2); 249 | }); 250 | 251 | it("should run on files with any custom extension", async () => { 252 | const results = await eslint.lintText(shortText, { filePath: "test.custom" }); 253 | 254 | assert.strictEqual(results.length, 1); 255 | assert.strictEqual(results[0].messages.length, 1); 256 | assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); 257 | assert.strictEqual(results[0].messages[0].line, 2); 258 | }); 259 | 260 | it("should extract blocks and remap messages", async () => { 261 | const results = await eslint.lintFiles([path.resolve(__dirname, "../fixtures/long.md")]); 262 | 263 | assert.strictEqual(results.length, 1); 264 | assert.strictEqual(results[0].messages.length, 5); 265 | assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); 266 | assert.strictEqual(results[0].messages[0].line, 10); 267 | assert.strictEqual(results[0].messages[0].column, 1); 268 | assert.strictEqual(results[0].messages[1].message, "Unexpected console statement."); 269 | assert.strictEqual(results[0].messages[1].line, 16); 270 | assert.strictEqual(results[0].messages[1].column, 5); 271 | assert.strictEqual(results[0].messages[2].message, "Unexpected console statement."); 272 | assert.strictEqual(results[0].messages[2].line, 24); 273 | assert.strictEqual(results[0].messages[2].column, 1); 274 | assert.strictEqual(results[0].messages[3].message, "Strings must use singlequote."); 275 | assert.strictEqual(results[0].messages[3].line, 38); 276 | assert.strictEqual(results[0].messages[3].column, 13); 277 | assert.strictEqual(results[0].messages[4].message, "Parsing error: Unexpected character '@'"); 278 | assert.strictEqual(results[0].messages[4].line, 46); 279 | assert.strictEqual(results[0].messages[4].column, 2); 280 | }); 281 | 282 | // https://github.com/eslint/eslint-plugin-markdown/issues/181 283 | it("should work when called on nested code blocks in the same file", async () => { 284 | 285 | /* 286 | * As of this writing, the nested code block, though it uses the same 287 | * Markdown processor, must use a different extension or ESLint will not 288 | * re-apply the processor on the nested code block. To work around that, 289 | * a file named `test.md` contains a nested `markdown` code block in 290 | * this test. 291 | * 292 | * https://github.com/eslint/eslint/pull/14227/files#r602802758 293 | */ 294 | const code = [ 295 | "", 296 | "", 297 | "````markdown", 298 | "", 299 | "", 300 | "This test only repros if the MD files have a different number of lines before code blocks.", 301 | "", 302 | "```js", 303 | "// test.md/0_0.markdown/0_0.js", 304 | "console.log('single quotes')", 305 | "```", 306 | "````" 307 | ].join("\n"); 308 | const recursiveCli = initESLint("eslintrc.json", { 309 | extensions: [".js", ".markdown", ".md"] 310 | }); 311 | const results = await recursiveCli.lintText(code, { filePath: "test.md" }); 312 | 313 | assert.strictEqual(results.length, 1); 314 | assert.strictEqual(results[0].messages.length, 2); 315 | assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); 316 | assert.strictEqual(results[0].messages[0].line, 10); 317 | assert.strictEqual(results[0].messages[1].message, "Strings must use doublequote."); 318 | assert.strictEqual(results[0].messages[1].line, 10); 319 | }); 320 | 321 | describe("configuration comments", () => { 322 | it("apply only to the code block immediately following", async () => { 323 | const code = [ 324 | "", 325 | "", 326 | "", 327 | "```js", 328 | "var single = 'single';", 329 | "console.log(single);", 330 | "var double = \"double\";", 331 | "console.log(double);", 332 | "```", 333 | "", 334 | "```js", 335 | "var single = 'single';", 336 | "console.log(single);", 337 | "var double = \"double\";", 338 | "console.log(double);", 339 | "```" 340 | ].join("\n"); 341 | const results = await eslint.lintText(code, { filePath: "test.md" }); 342 | 343 | assert.strictEqual(results.length, 1); 344 | assert.strictEqual(results[0].messages.length, 4); 345 | assert.strictEqual(results[0].messages[0].message, "Strings must use singlequote."); 346 | assert.strictEqual(results[0].messages[0].line, 7); 347 | assert.strictEqual(results[0].messages[1].message, "Strings must use doublequote."); 348 | assert.strictEqual(results[0].messages[1].line, 12); 349 | assert.strictEqual(results[0].messages[2].message, "Unexpected console statement."); 350 | assert.strictEqual(results[0].messages[2].line, 13); 351 | assert.strictEqual(results[0].messages[3].message, "Unexpected console statement."); 352 | assert.strictEqual(results[0].messages[3].line, 15); 353 | }); 354 | 355 | // https://github.com/eslint/eslint-plugin-markdown/issues/78 356 | it("preserves leading empty lines", async () => { 357 | const code = [ 358 | "", 359 | "", 360 | "```js", 361 | "", 362 | "\"use strict\";", 363 | "```" 364 | ].join("\n"); 365 | const results = await eslint.lintText(code, { filePath: "test.md" }); 366 | 367 | assert.strictEqual(results.length, 1); 368 | assert.strictEqual(results[0].messages.length, 1); 369 | assert.strictEqual(results[0].messages[0].message, "Unexpected newline before \"use strict\" directive."); 370 | assert.strictEqual(results[0].messages[0].line, 5); 371 | }); 372 | }); 373 | 374 | describe("should fix code", () => { 375 | before(() => { 376 | eslint = initESLint("eslintrc.json", { fix: true }); 377 | }); 378 | 379 | it("in the simplest case", async () => { 380 | const input = [ 381 | "This is Markdown.", 382 | "", 383 | "```js", 384 | "console.log('Hello, world!')", 385 | "```" 386 | ].join("\n"); 387 | const expected = [ 388 | "This is Markdown.", 389 | "", 390 | "```js", 391 | "console.log(\"Hello, world!\")", 392 | "```" 393 | ].join("\n"); 394 | const results = await eslint.lintText(input, { filePath: "test.md" }); 395 | const actual = results[0].output; 396 | 397 | assert.strictEqual(actual, expected); 398 | }); 399 | 400 | it("across multiple lines", async () => { 401 | const input = [ 402 | "This is Markdown.", 403 | "", 404 | "```js", 405 | "console.log('Hello, world!')", 406 | "console.log('Hello, world!')", 407 | "```" 408 | ].join("\n"); 409 | const expected = [ 410 | "This is Markdown.", 411 | "", 412 | "```js", 413 | "console.log(\"Hello, world!\")", 414 | "console.log(\"Hello, world!\")", 415 | "```" 416 | ].join("\n"); 417 | const results = await eslint.lintText(input, { filePath: "test.md" }); 418 | const actual = results[0].output; 419 | 420 | assert.strictEqual(actual, expected); 421 | }); 422 | 423 | it("across multiple blocks", async () => { 424 | const input = [ 425 | "This is Markdown.", 426 | "", 427 | "```js", 428 | "console.log('Hello, world!')", 429 | "```", 430 | "", 431 | "```js", 432 | "console.log('Hello, world!')", 433 | "```" 434 | ].join("\n"); 435 | const expected = [ 436 | "This is Markdown.", 437 | "", 438 | "```js", 439 | "console.log(\"Hello, world!\")", 440 | "```", 441 | "", 442 | "```js", 443 | "console.log(\"Hello, world!\")", 444 | "```" 445 | ].join("\n"); 446 | const results = await eslint.lintText(input, { filePath: "test.md" }); 447 | const actual = results[0].output; 448 | 449 | assert.strictEqual(actual, expected); 450 | }); 451 | 452 | it("with lines indented by spaces", async () => { 453 | const input = [ 454 | "This is Markdown.", 455 | "", 456 | "```js", 457 | "function test() {", 458 | " console.log('Hello, world!')", 459 | "}", 460 | "```" 461 | ].join("\n"); 462 | const expected = [ 463 | "This is Markdown.", 464 | "", 465 | "```js", 466 | "function test() {", 467 | " console.log(\"Hello, world!\")", 468 | "}", 469 | "```" 470 | ].join("\n"); 471 | const results = await eslint.lintText(input, { filePath: "test.md" }); 472 | const actual = results[0].output; 473 | 474 | assert.strictEqual(actual, expected); 475 | }); 476 | 477 | it("with lines indented by tabs", async () => { 478 | const input = [ 479 | "This is Markdown.", 480 | "", 481 | "```js", 482 | "function test() {", 483 | "\tconsole.log('Hello, world!')", 484 | "}", 485 | "```" 486 | ].join("\n"); 487 | const expected = [ 488 | "This is Markdown.", 489 | "", 490 | "```js", 491 | "function test() {", 492 | "\tconsole.log(\"Hello, world!\")", 493 | "}", 494 | "```" 495 | ].join("\n"); 496 | const results = await eslint.lintText(input, { filePath: "test.md" }); 497 | const actual = results[0].output; 498 | 499 | assert.strictEqual(actual, expected); 500 | }); 501 | 502 | it("at the very start of a block", async () => { 503 | const input = [ 504 | "This is Markdown.", 505 | "", 506 | "```js", 507 | "'use strict'", 508 | "```" 509 | ].join("\n"); 510 | const expected = [ 511 | "This is Markdown.", 512 | "", 513 | "```js", 514 | "\"use strict\"", 515 | "```" 516 | ].join("\n"); 517 | const results = await eslint.lintText(input, { filePath: "test.md" }); 518 | const actual = results[0].output; 519 | 520 | assert.strictEqual(actual, expected); 521 | }); 522 | 523 | it("in blocks with extra backticks", async () => { 524 | const input = [ 525 | "This is Markdown.", 526 | "", 527 | "````js", 528 | "console.log('Hello, world!')", 529 | "````" 530 | ].join("\n"); 531 | const expected = [ 532 | "This is Markdown.", 533 | "", 534 | "````js", 535 | "console.log(\"Hello, world!\")", 536 | "````" 537 | ].join("\n"); 538 | const results = await eslint.lintText(input, { filePath: "test.md" }); 539 | const actual = results[0].output; 540 | 541 | assert.strictEqual(actual, expected); 542 | }); 543 | 544 | it("with configuration comments", async () => { 545 | const input = [ 546 | "", 547 | "", 548 | "```js", 549 | "console.log('Hello, world!')", 550 | "```" 551 | ].join("\n"); 552 | const expected = [ 553 | "", 554 | "", 555 | "```js", 556 | "console.log(\"Hello, world!\");", 557 | "```" 558 | ].join("\n"); 559 | const results = await eslint.lintText(input, { filePath: "test.md" }); 560 | const actual = results[0].output; 561 | 562 | assert.strictEqual(actual, expected); 563 | }); 564 | 565 | it("inside a list single line", async () => { 566 | const input = [ 567 | "- Inside a list", 568 | "", 569 | " ```js", 570 | " console.log('Hello, world!')", 571 | " ```" 572 | ].join("\n"); 573 | const expected = [ 574 | "- Inside a list", 575 | "", 576 | " ```js", 577 | " console.log(\"Hello, world!\")", 578 | " ```" 579 | ].join("\n"); 580 | const results = await eslint.lintText(input, { filePath: "test.md" }); 581 | const actual = results[0].output; 582 | 583 | assert.strictEqual(actual, expected); 584 | }); 585 | 586 | it("inside a list multi line", async () => { 587 | const input = [ 588 | "- Inside a list", 589 | "", 590 | " ```js", 591 | " console.log('Hello, world!')", 592 | " console.log('Hello, world!')", 593 | " ", 594 | " var obj = {", 595 | " hello: 'value'", 596 | " }", 597 | " ```" 598 | ].join("\n"); 599 | const expected = [ 600 | "- Inside a list", 601 | "", 602 | " ```js", 603 | " console.log(\"Hello, world!\")", 604 | " console.log(\"Hello, world!\")", 605 | " ", 606 | " var obj = {", 607 | " hello: \"value\"", 608 | " }", 609 | " ```" 610 | ].join("\n"); 611 | const results = await eslint.lintText(input, { filePath: "test.md" }); 612 | const actual = results[0].output; 613 | 614 | assert.strictEqual(actual, expected); 615 | }); 616 | 617 | it("with multiline autofix and CRLF", async () => { 618 | const input = [ 619 | "This is Markdown.", 620 | "", 621 | "```js", 622 | "console.log('Hello, \\", 623 | "world!')", 624 | "console.log('Hello, \\", 625 | "world!')", 626 | "```" 627 | ].join("\r\n"); 628 | const expected = [ 629 | "This is Markdown.", 630 | "", 631 | "```js", 632 | "console.log(\"Hello, \\", 633 | "world!\")", 634 | "console.log(\"Hello, \\", 635 | "world!\")", 636 | "```" 637 | ].join("\r\n"); 638 | const results = await eslint.lintText(input, { filePath: "test.md" }); 639 | const actual = results[0].output; 640 | 641 | assert.strictEqual(actual, expected); 642 | }); 643 | 644 | // https://spec.commonmark.org/0.28/#fenced-code-blocks 645 | describe("when indented", () => { 646 | it("by one space", async () => { 647 | const input = [ 648 | "This is Markdown.", 649 | "", 650 | " ```js", 651 | " console.log('Hello, world!')", 652 | " console.log('Hello, world!')", 653 | " ```" 654 | ].join("\n"); 655 | const expected = [ 656 | "This is Markdown.", 657 | "", 658 | " ```js", 659 | " console.log(\"Hello, world!\")", 660 | " console.log(\"Hello, world!\")", 661 | " ```" 662 | ].join("\n"); 663 | const results = await eslint.lintText(input, { filePath: "test.md" }); 664 | const actual = results[0].output; 665 | 666 | assert.strictEqual(actual, expected); 667 | }); 668 | 669 | it("by two spaces", async () => { 670 | const input = [ 671 | "This is Markdown.", 672 | "", 673 | " ```js", 674 | " console.log('Hello, world!')", 675 | " console.log('Hello, world!')", 676 | " ```" 677 | ].join("\n"); 678 | const expected = [ 679 | "This is Markdown.", 680 | "", 681 | " ```js", 682 | " console.log(\"Hello, world!\")", 683 | " console.log(\"Hello, world!\")", 684 | " ```" 685 | ].join("\n"); 686 | const results = await eslint.lintText(input, { filePath: "test.md" }); 687 | const actual = results[0].output; 688 | 689 | assert.strictEqual(actual, expected); 690 | }); 691 | 692 | it("by three spaces", async () => { 693 | const input = [ 694 | "This is Markdown.", 695 | "", 696 | " ```js", 697 | " console.log('Hello, world!')", 698 | " console.log('Hello, world!')", 699 | " ```" 700 | ].join("\n"); 701 | const expected = [ 702 | "This is Markdown.", 703 | "", 704 | " ```js", 705 | " console.log(\"Hello, world!\")", 706 | " console.log(\"Hello, world!\")", 707 | " ```" 708 | ].join("\n"); 709 | const results = await eslint.lintText(input, { filePath: "test.md" }); 710 | const actual = results[0].output; 711 | 712 | assert.strictEqual(actual, expected); 713 | }); 714 | 715 | it("and the closing fence is differently indented", async () => { 716 | const input = [ 717 | "This is Markdown.", 718 | "", 719 | " ```js", 720 | " console.log('Hello, world!')", 721 | " console.log('Hello, world!')", 722 | " ```" 723 | ].join("\n"); 724 | const expected = [ 725 | "This is Markdown.", 726 | "", 727 | " ```js", 728 | " console.log(\"Hello, world!\")", 729 | " console.log(\"Hello, world!\")", 730 | " ```" 731 | ].join("\n"); 732 | const results = await eslint.lintText(input, { filePath: "test.md" }); 733 | const actual = results[0].output; 734 | 735 | assert.strictEqual(actual, expected); 736 | }); 737 | 738 | it("underindented", async () => { 739 | const input = [ 740 | "This is Markdown.", 741 | "", 742 | " ```js", 743 | " console.log('Hello, world!')", 744 | " console.log('Hello, world!')", 745 | " console.log('Hello, world!')", 746 | " ```" 747 | ].join("\n"); 748 | const expected = [ 749 | "This is Markdown.", 750 | "", 751 | " ```js", 752 | " console.log(\"Hello, world!\")", 753 | " console.log(\"Hello, world!\")", 754 | " console.log(\"Hello, world!\")", 755 | " ```" 756 | ].join("\n"); 757 | const results = await eslint.lintText(input, { filePath: "test.md" }); 758 | const actual = results[0].output; 759 | 760 | assert.strictEqual(actual, expected); 761 | }); 762 | 763 | it("multiline autofix", async () => { 764 | const input = [ 765 | "This is Markdown.", 766 | "", 767 | " ```js", 768 | " console.log('Hello, \\", 769 | " world!')", 770 | " console.log('Hello, \\", 771 | " world!')", 772 | " ```" 773 | ].join("\n"); 774 | const expected = [ 775 | "This is Markdown.", 776 | "", 777 | " ```js", 778 | " console.log(\"Hello, \\", 779 | " world!\")", 780 | " console.log(\"Hello, \\", 781 | " world!\")", 782 | " ```" 783 | ].join("\n"); 784 | const results = await eslint.lintText(input, { filePath: "test.md" }); 785 | const actual = results[0].output; 786 | 787 | assert.strictEqual(actual, expected); 788 | }); 789 | 790 | it("underindented multiline autofix", async () => { 791 | const input = [ 792 | " ```js", 793 | " console.log('Hello, world!')", 794 | " console.log('Hello, \\", 795 | " world!')", 796 | " console.log('Hello, world!')", 797 | " ```" 798 | ].join("\n"); 799 | 800 | // The Markdown parser doesn't have any concept of a "negative" 801 | // indent left of the opening code fence, so autofixes move 802 | // lines that were previously underindented to the same level 803 | // as the opening code fence. 804 | const expected = [ 805 | " ```js", 806 | " console.log(\"Hello, world!\")", 807 | " console.log(\"Hello, \\", 808 | " world!\")", 809 | " console.log(\"Hello, world!\")", 810 | " ```" 811 | ].join("\n"); 812 | const results = await eslint.lintText(input, { filePath: "test.md" }); 813 | const actual = results[0].output; 814 | 815 | assert.strictEqual(actual, expected); 816 | }); 817 | 818 | it("multiline autofix in blockquote", async () => { 819 | const input = [ 820 | "This is Markdown.", 821 | "", 822 | "> ```js", 823 | "> console.log('Hello, \\", 824 | "> world!')", 825 | "> console.log('Hello, \\", 826 | "> world!')", 827 | "> ```" 828 | ].join("\n"); 829 | const expected = [ 830 | "This is Markdown.", 831 | "", 832 | "> ```js", 833 | "> console.log(\"Hello, \\", 834 | "> world!\")", 835 | "> console.log(\"Hello, \\", 836 | "> world!\")", 837 | "> ```" 838 | ].join("\n"); 839 | const results = await eslint.lintText(input, { filePath: "test.md" }); 840 | const actual = results[0].output; 841 | 842 | assert.strictEqual(actual, expected); 843 | }); 844 | 845 | it("multiline autofix in nested blockquote", async () => { 846 | const input = [ 847 | "This is Markdown.", 848 | "", 849 | "> This is a nested blockquote.", 850 | ">", 851 | "> > ```js", 852 | "> > console.log('Hello, \\", 853 | "> > new\\", 854 | "> > world!')", 855 | "> > console.log('Hello, \\", 856 | "> > world!')", 857 | "> > ```" 858 | ].join("\n"); 859 | 860 | // The Markdown parser doesn't have any concept of a "negative" 861 | // indent left of the opening code fence, so autofixes move 862 | // lines that were previously underindented to the same level 863 | // as the opening code fence. 864 | const expected = [ 865 | "This is Markdown.", 866 | "", 867 | "> This is a nested blockquote.", 868 | ">", 869 | "> > ```js", 870 | "> > console.log(\"Hello, \\", 871 | "> > new\\", 872 | "> > world!\")", 873 | "> > console.log(\"Hello, \\", 874 | "> > world!\")", 875 | "> > ```" 876 | ].join("\n"); 877 | const results = await eslint.lintText(input, { filePath: "test.md" }); 878 | const actual = results[0].output; 879 | 880 | assert.strictEqual(actual, expected); 881 | }); 882 | 883 | it("by one space with comments", async () => { 884 | const input = [ 885 | "This is Markdown.", 886 | "", 887 | "", 888 | "", 889 | "", 890 | " ```js", 891 | " console.log('Hello, world!')", 892 | " console.log('Hello, world!')", 893 | " ```" 894 | ].join("\n"); 895 | const expected = [ 896 | "This is Markdown.", 897 | "", 898 | "", 899 | "", 900 | "", 901 | " ```js", 902 | " console.log(\"Hello, world!\");", 903 | " console.log(\"Hello, world!\");", 904 | " ```" 905 | ].join("\n"); 906 | const results = await eslint.lintText(input, { filePath: "test.md" }); 907 | const actual = results[0].output; 908 | 909 | assert.strictEqual(actual, expected); 910 | }); 911 | 912 | it("unevenly by two spaces with comments", async () => { 913 | const input = [ 914 | "This is Markdown.", 915 | "", 916 | "", 917 | "", 918 | "", 919 | " ```js", 920 | " console.log('Hello, world!')", 921 | " console.log('Hello, world!')", 922 | " console.log('Hello, world!')", 923 | " ```" 924 | ].join("\n"); 925 | const expected = [ 926 | "This is Markdown.", 927 | "", 928 | "", 929 | "", 930 | "", 931 | " ```js", 932 | " console.log(\"Hello, world!\");", 933 | " console.log(\"Hello, world!\");", 934 | " console.log(\"Hello, world!\");", 935 | " ```" 936 | ].join("\n"); 937 | const results = await eslint.lintText(input, { filePath: "test.md" }); 938 | const actual = results[0].output; 939 | 940 | assert.strictEqual(actual, expected); 941 | }); 942 | 943 | describe("inside a list", () => { 944 | it("normally", async () => { 945 | const input = [ 946 | "- This is a Markdown list.", 947 | "", 948 | " ```js", 949 | " console.log('Hello, world!')", 950 | " console.log('Hello, world!')", 951 | " ```" 952 | ].join("\n"); 953 | const expected = [ 954 | "- This is a Markdown list.", 955 | "", 956 | " ```js", 957 | " console.log(\"Hello, world!\")", 958 | " console.log(\"Hello, world!\")", 959 | " ```" 960 | ].join("\n"); 961 | const results = await eslint.lintText(input, { filePath: "test.md" }); 962 | const actual = results[0].output; 963 | 964 | assert.strictEqual(actual, expected); 965 | }); 966 | 967 | it("by one space", async () => { 968 | const input = [ 969 | "- This is a Markdown list.", 970 | "", 971 | " ```js", 972 | " console.log('Hello, world!')", 973 | " console.log('Hello, world!')", 974 | " ```" 975 | ].join("\n"); 976 | const expected = [ 977 | "- This is a Markdown list.", 978 | "", 979 | " ```js", 980 | " console.log(\"Hello, world!\")", 981 | " console.log(\"Hello, world!\")", 982 | " ```" 983 | ].join("\n"); 984 | const results = await eslint.lintText(input, { filePath: "test.md" }); 985 | const actual = results[0].output; 986 | 987 | assert.strictEqual(actual, expected); 988 | }); 989 | }); 990 | }); 991 | 992 | it("with multiple rules", async () => { 993 | const input = [ 994 | "## Hello!", 995 | "", 996 | "", 997 | "", 998 | "```js", 999 | "var obj = {", 1000 | " some: 'value'", 1001 | "}", 1002 | "", 1003 | "console.log('opop');", 1004 | "", 1005 | "function hello() {", 1006 | " return false", 1007 | "};", 1008 | "```" 1009 | ].join("\n"); 1010 | const expected = [ 1011 | "## Hello!", 1012 | "", 1013 | "", 1014 | "", 1015 | "```js", 1016 | "var obj = {", 1017 | " some: \"value\"", 1018 | "};", 1019 | "", 1020 | "console.log(\"opop\");", 1021 | "", 1022 | "function hello() {", 1023 | " return false;", 1024 | "};", 1025 | "```" 1026 | ].join("\n"); 1027 | const results = await eslint.lintText(input, { filePath: "test.md" }); 1028 | const actual = results[0].output; 1029 | 1030 | assert.strictEqual(actual, expected); 1031 | }); 1032 | 1033 | }); 1034 | 1035 | }); 1036 | --------------------------------------------------------------------------------