├── .vscode ├── settings.json └── launch.json ├── .eslintrc.js ├── lib ├── index.js ├── configs.js ├── processors │ └── mdProcessor.js └── rules │ └── remark.js ├── package.json ├── tests └── lib │ └── rules │ └── remark.test.js ├── .gitignore └── README.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": ["markdown"] 3 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { node: true }, 4 | extends: ['plugin:tyrecheck/recommended'], 5 | rules: { 6 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 7 | 'no-console': [process.env.NODE_ENV === 'production' ? 'error' : 'off', { allow: ['warn', 'error'] }], 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview An ESLint plugin to lint and fix markdown files. 3 | * @author Leonid Buneev 4 | */ 5 | const path = require('path') 6 | const requireIndex = require('requireindex') 7 | const mdProcessor = require('./processors/mdProcessor') 8 | const configs = require('./configs') 9 | 10 | module.exports.rules = requireIndex(path.join(__dirname, '/rules')) 11 | module.exports.configs = configs 12 | module.exports.processors = { '.md': mdProcessor } 13 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "attach", 10 | "name": "Attach", 11 | "port": 9229, 12 | "skipFiles": [ 13 | "/**" 14 | ] 15 | }, 16 | { 17 | "type": "node", 18 | "request": "attach", 19 | "name": "Attach to test suite", 20 | "skipFiles": [ 21 | "/**" 22 | ], 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-md", 3 | "version": "1.0.19", 4 | "description": "An ESLint plugin that allows you to lint markdown code in your *.md files.", 5 | "keywords": [ 6 | "markdown", 7 | "md", 8 | "eslint-markdown", 9 | "eslint", 10 | "eslintplugin", 11 | "eslint-plugin" 12 | ], 13 | "author": "Leonid Buneev", 14 | "main": "lib/index.js", 15 | "repository": { 16 | "type" : "git", 17 | "url" : "https://github.com/leo-buneev/eslint-plugin-md" 18 | }, 19 | "scripts": { 20 | "lint": "cross-env NODE_ENV=production eslint . --ext js,md", 21 | "dev": "node --inspect ./node_modules/jest/bin/jest.js --runInBand --watch", 22 | "test": "jest" 23 | }, 24 | "dependencies": { 25 | "lodash": "^4.17.15", 26 | "markdown-eslint-parser": "^1.2.0", 27 | "prettier-linter-helpers": "^1.0.0", 28 | "remark": "^11.0.2", 29 | "remark-frontmatter": "^1.3.2", 30 | "remark-preset-lint-markdown-style-guide": "^2.1.3", 31 | "requireindex": "~1.1.0" 32 | }, 33 | "devDependencies": { 34 | "cross-env": "^7.0.2", 35 | "eslint-plugin-tyrecheck": "^2.10.2", 36 | "jest": "^25.1.0" 37 | }, 38 | "engines": { 39 | "node": ">=0.10.0" 40 | }, 41 | "license": "ISC" 42 | } 43 | -------------------------------------------------------------------------------- /lib/configs.js: -------------------------------------------------------------------------------- 1 | // We provide section "overrides" here - and if user's eslintrc doesn't specify "parser" at all, it will work. 2 | // However, usually users specify some kind of "parser". In this case, it will take precedense over our "overrides", 3 | // and end user will need to specify similar "overrides" explicitly in his eslintrc file. 4 | 5 | module.exports = { 6 | prettier: { 7 | plugins: ['md'], 8 | rules: { 9 | 'md/remark': [ 10 | 'error', 11 | { 12 | plugins: [ 13 | 'preset-lint-markdown-style-guide', 14 | 'frontmatter', 15 | // Disable rules handled by Prettier 16 | ['lint-maximum-line-length', false], 17 | ['lint-emphasis-marker', false], 18 | ['lint-list-item-indent', false], 19 | ['lint-list-item-spacing', false], 20 | ['lint-ordered-list-marker-value', false], 21 | ['lint-no-consecutive-blank-lines', false], 22 | ['lint-table-cell-padding', false], 23 | ['lint-link-title-style', false], 24 | ['lint-no-shortcut-reference-link', false], 25 | ], 26 | }, 27 | ], 28 | }, 29 | overrides: [ 30 | { 31 | files: ['*.md'], 32 | parser: 'markdown-eslint-parser', 33 | rules: { 34 | 'prettier/prettier': [ 35 | 'warn', 36 | { 37 | parser: 'markdown', 38 | }, 39 | ], 40 | }, 41 | }, 42 | ], 43 | }, 44 | 45 | recommended: { 46 | plugins: ['md'], 47 | rules: { 48 | 'md/remark': ['error', { plugins: ['preset-lint-markdown-style-guide', 'frontmatter'] }], 49 | }, 50 | overrides: [ 51 | { 52 | files: ['*.md'], 53 | parser: 'markdown-eslint-parser', 54 | }, 55 | ], 56 | }, 57 | } 58 | -------------------------------------------------------------------------------- /tests/lib/rules/remark.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Template literals (strings created with `backquote symbol`) should use loc`mystring` syntax for localization 3 | * @author Leonid Buneev 4 | */ 5 | 'use strict' 6 | 7 | // Requirements 8 | const rule = require('../../../lib/rules/remark') 9 | const RuleTester = require('eslint').RuleTester 10 | 11 | // Tests 12 | const ruleTester = new RuleTester({ 13 | parser: require.resolve('markdown-eslint-parser'), 14 | }) 15 | ruleTester.run('remark', rule, { 16 | valid: [ 17 | { 18 | options: [{ plugins: ['preset-lint-markdown-style-guide', ['lint-maximum-line-length', false]] }], 19 | code: `Line more than 80 symbols long. Line more than 80 symbols long. Line more than 80 symbols long.\n`, 20 | }, 21 | { 22 | options: [{ plugins: ['preset-lint-markdown-style-guide'] }], 23 | code: `*Hello world*\n`, 24 | }, 25 | { 26 | options: [{ plugins: ['preset-lint-markdown-style-guide'] }], 27 | code: `::: tip\nThis is a tip\n:::\n`, 28 | }, 29 | { 30 | options: [{ plugins: ['preset-lint-markdown-style-guide'] }], 31 | code: `| Tables | Are | Cool |\n 32 | | ------------- |:-------------:| -----:|\n 33 | | col 3 is | right-aligned | $1600 |\n`, 34 | }, 35 | { 36 | options: [{ plugins: ['preset-lint-markdown-style-guide', 'frontmatter'] }], 37 | code: `---\nhome: true\n---\n`, 38 | }, 39 | // Not supported 40 | // { 41 | // code: `[[toc]]\n`, 42 | // }, 43 | ], 44 | 45 | invalid: [ 46 | { 47 | options: [{ plugins: ['preset-lint-markdown-style-guide'] }], 48 | code: `_Hello world_\n`, 49 | errors: [ 50 | { 51 | message: 'Emphasis should use `*` as a marker (emphasis-marker)', 52 | }, 53 | ], 54 | }, 55 | ], 56 | }) 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and not Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | # Stores VSCode versions used for testing VSCode extensions 108 | .vscode-test 109 | 110 | # yarn v2 111 | 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .pnp.* -------------------------------------------------------------------------------- /lib/processors/mdProcessor.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | let mdOffsets = [] 4 | let nonMdBlocks = [] 5 | 6 | module.exports = { 7 | preprocess(text, filename) { 8 | let offset = 0 9 | const mdOffsetsInner = [] 10 | const nonMdBlocksInner = [] 11 | // const ignoreMdRangesInner = [] 12 | const mdWithoutJs = text.replace(/```(\w+)\n([\s\S]*?)\n```\n/gim, (substr, lang, match, position) => { 13 | const replacement = '\n'.repeat(match.split('\n').length + 2) 14 | const oldOffset = offset 15 | offset += substr.length - replacement.length 16 | // ignoreMdRangesInner.push({ start: position, end: position + replacement.length }) 17 | mdOffsetsInner.push({ startFrom: position - oldOffset, offset }) 18 | nonMdBlocksInner.push({ 19 | lang, 20 | text: match + '\n', 21 | charOffset: position + ('```' + lang + '\n').length, 22 | lineOffset: text.slice(0, position).split('\n').length, 23 | }) 24 | return replacement 25 | }) 26 | mdOffsets = mdOffsetsInner.reverse() 27 | nonMdBlocks = nonMdBlocksInner 28 | 29 | const partName = (filename && path.basename(filename)) || 'file.md' 30 | 31 | const jsBlocksResult = nonMdBlocksInner.map(({ text, lang }, i) => ({ 32 | text, 33 | // eslint internal code appends this filename to real filename 34 | filename: `/../../${partName}.${lang}`, 35 | })) 36 | // console.log('JSBR', jsBlocksResult) 37 | return [mdWithoutJs, ...jsBlocksResult] 38 | }, 39 | 40 | postprocess(messageGroups, x) { 41 | const result = [] 42 | // console.log('MG', messageGroups) 43 | for (let i = 0; i < messageGroups.length; i++) { 44 | const mg = messageGroups[i] 45 | if (i === 0) { 46 | for (const m of mg) { 47 | if (m.fix && m.fix.range) { 48 | m.fix.range = m.fix.range.map(pos => { 49 | const mdOffset = mdOffsets.find(o => o.startFrom < pos) 50 | if (!mdOffset) return pos 51 | return pos + mdOffset.offset 52 | }) 53 | } 54 | } 55 | } else { 56 | // Javascript 57 | const { lineOffset, charOffset } = nonMdBlocks[i - 1] 58 | for (const m of mg) { 59 | if (m.fix && m.fix.range) { 60 | m.fix.range = m.fix.range.map(pos => pos + charOffset) 61 | } 62 | m.line += lineOffset 63 | m.endLine = m.endLine && m.endLine + lineOffset 64 | } 65 | } 66 | result.push(...mg) 67 | } 68 | return result 69 | }, 70 | 71 | supportsAutofix: true, 72 | } 73 | -------------------------------------------------------------------------------- /lib/rules/remark.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Lint markdown (*.md) files using package "remark" 3 | * @author Leonid Buneev 4 | */ 5 | const _ = require('lodash') 6 | const remark = require('remark') 7 | const fs = require('fs') 8 | // const { showInvisibles, generateDifferences } = require('prettier-linter-helpers') 9 | 10 | // const { INSERT, DELETE, REPLACE } = generateDifferences 11 | 12 | // function reportInsert(context, offset, text) { 13 | // const pos = context.getSourceCode().getLocFromIndex(offset) 14 | // const range = [offset, offset] 15 | // context.report({ 16 | // message: 'Insert `{{ code }}`', 17 | // data: { code: showInvisibles(text) }, 18 | // loc: { start: pos, end: pos }, 19 | // fix(fixer) { 20 | // return fixer.insertTextAfterRange(range, text) 21 | // }, 22 | // }) 23 | // } 24 | 25 | // function reportDelete(context, offset, text) { 26 | // const start = context.getSourceCode().getLocFromIndex(offset) 27 | // const end = context.getSourceCode().getLocFromIndex(offset + text.length) 28 | // const range = [offset, offset + text.length] 29 | // context.report({ 30 | // message: 'Delete `{{ code }}`', 31 | // data: { code: showInvisibles(text) }, 32 | // loc: { start, end }, 33 | // fix(fixer) { 34 | // return fixer.removeRange(range) 35 | // }, 36 | // }) 37 | // } 38 | 39 | // function reportReplace(context, offset, deleteText, insertText) { 40 | // const start = context.getSourceCode().getLocFromIndex(offset) 41 | // const end = context.getSourceCode().getLocFromIndex(offset + deleteText.length) 42 | // const range = [offset, offset + deleteText.length] 43 | // context.report({ 44 | // message: 'Replace `{{ deleteCode }}` with `{{ insertCode }}`', 45 | // data: { 46 | // deleteCode: showInvisibles(deleteText), 47 | // insertCode: showInvisibles(insertText), 48 | // }, 49 | // loc: { start, end }, 50 | // fix(fixer) { 51 | // return fixer.replaceTextRange(range, insertText) 52 | // }, 53 | // }) 54 | // } 55 | 56 | module.exports = { 57 | meta: { 58 | docs: { 59 | description: 'Lint markdown (*.md) files using package "remark"', 60 | }, 61 | fixable: 'code', // or "code" or "whitespace" 62 | schema: [ 63 | // Remark options: 64 | { 65 | type: 'object', 66 | properties: {}, 67 | additionalProperties: true, 68 | }, 69 | { 70 | type: 'object', 71 | properties: { 72 | useRemarkrc: { type: 'boolean' }, 73 | }, 74 | additionalProperties: true, 75 | }, 76 | ], 77 | }, 78 | 79 | create: function(context) { 80 | return { 81 | Program(node) { 82 | if (!node.mdCode) return // Not processed by markdown-eslint-parser => not .md 83 | const useRemarkrc = !context.options[1] || context.options[1].useRemarkrc !== false 84 | let remarkOptionsFromFile = null 85 | 86 | if (useRemarkrc) { 87 | if (fs.existsSync('./.remarkrc')) { 88 | remarkOptionsFromFile = JSON.parse(fs.readFileSync('./.remarkrc', 'utf8')) 89 | } else if (fs.existsSync('./.remarkrc.js')) { 90 | remarkOptionsFromFile = require('./.remarkrc.js') 91 | } 92 | } 93 | const remarkOptions = _.merge({ plugins: [] }, remarkOptionsFromFile, context.options[0]) 94 | 95 | // const sourceCode = context.getSourceCode() 96 | // const filepath = context.getFilename() 97 | // const source = sourceCode.text 98 | const source = node.mdCode 99 | 100 | let remarkProcessor = remark() 101 | for (let plugin of remarkOptions.plugins) { 102 | if (!_.isArray(plugin)) plugin = [plugin] 103 | if (!plugin[0].startsWith('remark-')) plugin[0] = 'remark-' + plugin[0] 104 | plugin[0] = require(plugin[0]) 105 | remarkProcessor = remarkProcessor.use(...plugin) 106 | } 107 | // const vfile = remark() 108 | // .use({ 109 | // settings: { emphasis: '*', strong: '*' }, 110 | // }) 111 | const vfile = remarkProcessor.processSync(source) 112 | const messages = vfile.messages || [] 113 | 114 | // const fixedSource = String(vfile) 115 | 116 | for (const msg of messages) { 117 | msg.location.start.column -= 1 118 | msg.location.end.column -= 1 119 | context.report({ 120 | message: msg.message + ` (${msg.ruleId})`, 121 | loc: msg.location, 122 | }) 123 | } 124 | // if (source !== fixedSource) { 125 | // const differences = generateDifferences(source, fixedSource) 126 | // for (const difference of differences) { 127 | // switch (difference.operation) { 128 | // case INSERT: 129 | // reportInsert(context, difference.offset, difference.insertText) 130 | // break 131 | // case DELETE: 132 | // reportDelete(context, difference.offset, difference.deleteText) 133 | // break 134 | // case REPLACE: 135 | // reportReplace(context, difference.offset, difference.deleteText, difference.insertText) 136 | // break 137 | // } 138 | // } 139 | // } 140 | }, 141 | } 142 | }, 143 | } 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-md 2 | 3 | An ESLint plugin to lint and fix markdown files. It uses amazing [remark-lint](https://github.com/remarkjs/remark-lint) 4 | under the hood. Also enables using `prettier` to automatically format your markdown files right from `eslint`! 5 | 6 | ## Motivation 7 | 8 | ESLint is a tool for linting Javascript. There is another plugin called `eslint-plugin-markdown`, however it is still 9 | only linting Javascript parts inside `*.md` files, and leaves Markdown itself untouched. 10 | 11 | Wouldn't it be cool, if we could use ESLint to enforce style of Markdown itself? That is what this plugin is for! 12 | 13 | ## Installation 14 | 15 | You'll first need to install [ESLint](http://eslint.org): 16 | 17 | ```bash 18 | $ npm i eslint --save-dev 19 | # or 20 | $ yarn add -D eslint 21 | ``` 22 | 23 | Next, install `eslint-plugin-md`: 24 | 25 | ```bash 26 | $ npm install eslint-plugin-md --save-dev 27 | # or 28 | $ yarn add -D eslint-plugin-md 29 | ``` 30 | 31 | **Note:** If you installed ESLint globally (using the `npm -g` or `yarn global`) then you must also install 32 | `eslint-plugin-md` globally. 33 | 34 | ## Usage 35 | 36 | Add `plugin:md/recommended` config to `extends` section of your your `.eslintrc` configuration file 37 | 38 | ```js 39 | // .eslintrc.js 40 | module.exports = { 41 | extends: ['plugin:md/recommended'], 42 | overrides: [ 43 | { 44 | files: ['*.md'], 45 | parser: 'markdown-eslint-parser', 46 | }, 47 | ], 48 | } 49 | ``` 50 | 51 | And this is it! By default it will apply all rules from 52 | [Markdown style guide](https://github.com/remarkjs/remark-lint/tree/master/packages/remark-preset-lint-markdown-style-guide). 53 | 54 | **Note:** By default ESLint won't lint \*.md files. So: 55 | 56 | - If you use ESLint from CLI, use `--ext` parameter (e.g. `eslint . --ext js,md`) 57 | - If you use VSCode with ESLint plugin, add `"eslint.validate": ["markdown"]` in your VSCode preferences 58 | 59 | ## Usage with eslint-plugin-prettier 60 | 61 | [Prettier](https://prettier.io/) is an amazing code formatter that supports many languages, including markdown. It is 62 | common to use prettier as a rule in ESLint via amazing 63 | [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier). 64 | 65 | This plugin can play nicely together with `eslint-plugin-prettier` (meaning that first code will be formatted via 66 | prettier, and then remark rules will be applied). Typical eslint configuration will look like this: 67 | 68 | ```bash 69 | yarn add -D eslint eslint-plugin-prettier eslint-plugin-md 70 | ``` 71 | 72 | ```js 73 | // .eslintrc.js 74 | module.exports = { 75 | extends: ['plugin:prettier/recommended', 'plugin:md/recommended'], 76 | overrides: [ 77 | { 78 | files: ['*.md'], 79 | parser: 'markdown-eslint-parser', 80 | rules: { 81 | 'prettier/prettier': [ 82 | 'error', 83 | // Important to force prettier to use "markdown" parser - otherwise it wouldn't be able to parse *.md files. 84 | // You also can configure other options supported by prettier here - "prose-wrap" is 85 | // particularly useful for *.md files 86 | { parser: 'markdown' }, 87 | ], 88 | }, 89 | }, 90 | ], 91 | } 92 | ``` 93 | 94 | ## Modifying eslint setup for js code inside \*.md files 95 | 96 | By default, code inside fenced code block marked as js language (` ```js `) will be linted against your default eslint 97 | configuration for js files. However, that may be undesirable - usually you will want less strict rules for JS code in 98 | \*.md files. 99 | 100 | To modify setup, you can use "overrides" section in your eslintrc in this way: 101 | 102 | ```js 103 | // .eslintrc.js 104 | module.exports = { 105 | extends: ['plugin:md/recommended'], 106 | overrides: [ 107 | { 108 | files: ['*.md'], 109 | parser: 'markdown-eslint-parser', 110 | }, 111 | { 112 | files: ['*.md.js'], // Will match js code inside *.md files 113 | rules: { 114 | // Example - disable 2 core eslint rules 'no-unused-vars' and 'no-undef' 115 | 'no-unused-vars': 'off', 116 | 'no-undef': 'off', 117 | }, 118 | }, 119 | ], 120 | } 121 | ``` 122 | 123 | ## Supported Rules 124 | 125 | This plugin exposes only one eslint rule - `md/remark`. However, you can customize remark configuration however you 126 | wish. 127 | 128 | ```js 129 | // .eslintrc.js 130 | module.exports = { 131 | extends: ['plugin:md/recommended'], 132 | rules: { 133 | 'md/remark': [ 134 | 'error', 135 | { 136 | // This object corresponds to object you would export in .remarkrc file 137 | plugins: ['preset-lint-markdown-style-guide', 'frontmatter', ['lint-maximum-line-length', false]], 138 | }, 139 | ], 140 | }, 141 | overrides: [ 142 | { 143 | files: ['*.md'], 144 | parser: 'markdown-eslint-parser', 145 | }, 146 | ], 147 | } 148 | ``` 149 | 150 | "Plugin" in remark can mean many things, but for our purposes it can be either remark rule or remark preset. See list of 151 | available rules [here](https://github.com/remarkjs/remark-lint/blob/master/doc/rules.md), and list of available presets 152 | (=set of rules) [here](https://github.com/remarkjs/remark-lint#list-of-presets). 153 | 154 | Remark also supports [External rules](https://github.com/remarkjs/remark-lint#list-of-external-rules). You can use those 155 | as well, just make sure to install corresponding package first. For example, if you want to use 156 | [alphabetize-lists](https://github.com/vhf/remark-lint-alphabetize-lists) external rule: 157 | 158 | ```bash 159 | yarn add -D remark-lint-alphabetize-lists 160 | ``` 161 | 162 | ```js 163 | // .eslintrc.js 164 | module.exports = { 165 | extends: ['plugin:md/recommended'], 166 | rules: { 167 | 'md/remark': [ 168 | 'error', 169 | { 170 | // This object corresponds to object you would export in .remarkrc file 171 | plugins: ['preset-lint-markdown-style-guide', 'frontmatter', 'remark-lint-alphabetize-lists'], 172 | }, 173 | ], 174 | }, 175 | overrides: [ 176 | { 177 | files: ['*.md'], 178 | parser: 'markdown-eslint-parser', 179 | }, 180 | ], 181 | } 182 | ``` 183 | --------------------------------------------------------------------------------