├── .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 |
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 | [](https://www.npmjs.com/package/eslint-plugin-markdown)
4 | [](https://www.npmjs.com/package/eslint-plugin-markdown)
5 | [](https://github.com/eslint/eslint-plugin-markdown/actions)
6 |
7 | Lint JS, JSX, TypeScript, and more inside Markdown.
8 |
9 |
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 |
--------------------------------------------------------------------------------