From 61babde967669f47adf734a66bcb427810061ff2 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 23 Jul 2024 16:00:03 -0400 Subject: [PATCH 1/8] feat!: Rename to @eslint/markdown --- .github/workflows/release-please.yml | 4 ++-- LICENSE | 2 +- README.md | 26 +++++++++++++------------- package.json | 19 ++++++++----------- 4 files changed, 24 insertions(+), 27 deletions(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index b857d80d..3eba292a 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -27,14 +27,14 @@ jobs: env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} if: ${{ steps.release.outputs.release_created }} - - run: 'npx @humanwhocodes/tweet "eslint-plugin-markdown v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }} has been released: ${{ steps.release.outputs.html_url }}"' + - run: 'npx @humanwhocodes/tweet "eslint/markdown v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }} has been released: ${{ steps.release.outputs.html_url }}"' if: ${{ steps.release.outputs.release_created }} env: TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} TWITTER_ACCESS_TOKEN_KEY: ${{ secrets.TWITTER_ACCESS_TOKEN_KEY }} TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} - - run: 'npx @humanwhocodes/toot "eslint-plugin-markdown v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }} has been released: ${{ steps.release.outputs.html_url }}"' + - run: 'npx @humanwhocodes/toot "eslint/markdown v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }} has been released: ${{ steps.release.outputs.html_url }}"' if: ${{ steps.release.outputs.release_created }} env: MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} diff --git a/LICENSE b/LICENSE index cf5a5995..e71a3d8f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright JS Foundation and other contributors, https://js.foundation +Copyright OpenJS Foundation and other contributors, https://openjsf.org Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 43c1a44c..7a8c21d9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# eslint-plugin-markdown +# ESLint Markdown Language Plugins -[![npm Version](https://img.shields.io/npm/v/eslint-plugin-markdown.svg)](https://www.npmjs.com/package/eslint-plugin-markdown) -[![Downloads](https://img.shields.io/npm/dm/eslint-plugin-markdown.svg)](https://www.npmjs.com/package/eslint-plugin-markdown) -[![Build Status](https://github.com/eslint/eslint-plugin-markdown/workflows/CI/badge.svg)](https://github.com/eslint/eslint-plugin-markdown/actions) +[![npm Version](https://img.shields.io/npm/v/@eslint/markdown.svg)](https://www.npmjs.com/package/@eslint/markdown) +[![Downloads](https://img.shields.io/npm/dm/@eslint/markdown.svg)](https://www.npmjs.com/package/@eslint/markdown) +[![Build Status](https://github.com/eslint/markdown/workflows/CI/badge.svg)](https://github.com/eslint/markdown/actions) Lint JS, JSX, TypeScript, and more inside Markdown. @@ -20,16 +20,16 @@ Lint JS, JSX, TypeScript, and more inside Markdown. Install the plugin alongside ESLint v8 or greater: ```sh -npm install --save-dev eslint eslint-plugin-markdown +npm install --save-dev eslint @eslint/markdown ``` ### Configuring -In your `eslint.config.js` file, import `eslint-plugin-markdown` and include the recommended config to enable the Markdown processor on all `.md` files: +In your `eslint.config.js` file, import `@eslint/markdown` and include the recommended config to enable the Markdown processor on all `.md` files: ```js // eslint.config.js -import markdown from "eslint-plugin-markdown"; +import markdown from "@eslint/markdown"; export default [ ...markdown.configs.recommended @@ -59,7 +59,7 @@ Here's an example: ```js // eslint.config.js -import markdown from "eslint-plugin-markdown"; +import markdown from "@eslint/markdown"; export default [ { @@ -102,7 +102,7 @@ Use glob patterns to disable more rules just for Markdown code blocks: ```js // / eslint.config.js -import markdown from "eslint-plugin-markdown"; +import markdown from "@eslint/markdown"; export default [ { @@ -271,15 +271,15 @@ console.log("This code block is linted normally."); The [`linter-eslint`](https://atom.io/packages/linter-eslint) package allows for linting within the [Atom IDE](https://atom.io/). -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". +In order to see `@eslint/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". -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. +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/markdown` configuration comments and skip directives won't work in this context. ## Contributing ```sh -$ git clone https://github.com/eslint/eslint-plugin-markdown.git -$ cd eslint-plugin-markdown +$ git clone https://github.com/eslint/markdown.git +$ cd markdown $ npm install $ npm test ``` diff --git a/package.json b/package.json index 655f84a4..e2479d74 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "eslint-plugin-markdown", + "name": "@eslint/markdown", "version": "5.1.0", - "description": "An ESLint plugin to lint JavaScript in Markdown code fences.", + "description": "The official ESLint language plugin for Markdown", "license": "MIT", "author": { "name": "Brandon Mills", @@ -17,11 +17,14 @@ "files": [ "src" ], - "repository": "eslint/eslint-plugin-markdown", + "publishConfig": { + "access": "public" + }, + "repository": "eslint/markdown", "bugs": { - "url": "https://github.com/eslint/eslint-plugin-markdown/issues" + "url": "https://github.com/eslint/markdown/issues" }, - "homepage": "https://github.com/eslint/eslint-plugin-markdown#readme", + "homepage": "https://github.com/eslint/markdown#readme", "keywords": [ "eslint", "eslintplugin", @@ -32,11 +35,6 @@ "scripts": { "lint": "eslint .", "prepare": "node ./npm-prepare.cjs", - "release:generate:latest": "eslint-generate-release", - "release:generate:alpha": "eslint-generate-prerelease alpha", - "release:generate:beta": "eslint-generate-prerelease beta", - "release:generate:rc": "eslint-generate-prerelease rc", - "release:publish": "eslint-publish-release", "test": "c8 mocha \"tests/**/*.test.js\" --timeout 30000" }, "devDependencies": { @@ -46,7 +44,6 @@ "chai": "^5.1.1", "eslint": "^9.4.0", "eslint-config-eslint": "^11.0.0", - "eslint-release": "^3.1.2", "globals": "^15.1.0", "mocha": "^10.6.0" }, From 933f9a60b72359333b7d76977e1e619e0ad03e72 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 23 Jul 2024 16:07:15 -0400 Subject: [PATCH 2/8] Update meta.name properties --- src/index.js | 8 +++++++- src/processor.js | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 5c375274..086d6030 100644 --- a/src/index.js +++ b/src/index.js @@ -3,8 +3,14 @@ * @author Brandon Mills */ +//----------------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------------- + import { processor } from "./processor.js"; + + const rulesConfig = { // The Markdown parser automatically trims trailing @@ -30,7 +36,7 @@ const rulesConfig = { const plugin = { meta: { - name: "eslint-plugin-markdown", + name: "@eslint/markdown", version: "5.1.0" // x-release-please-version }, processors: { diff --git a/src/processor.js b/src/processor.js index 43fa6618..7712d373 100644 --- a/src/processor.js +++ b/src/processor.js @@ -409,7 +409,7 @@ function postprocess(messages, filename) { export const processor = { meta: { - name: "eslint-plugin-markdown/markdown", + name: "@eslint/markdown/markdown", version: "5.1.0" // x-release-please-version }, preprocess, From 4cdf611b4b5d7ad613168a6385915cfecc628605 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 24 Jul 2024 11:39:03 -0400 Subject: [PATCH 3/8] Update src/index.js Co-authored-by: Milos Djermanovic --- src/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 086d6030..7de920c2 100644 --- a/src/index.js +++ b/src/index.js @@ -9,7 +9,9 @@ import { processor } from "./processor.js"; - +//----------------------------------------------------------------------------- +// Exports +//----------------------------------------------------------------------------- const rulesConfig = { From 7d1cef637d6a3048314689eee26cef9b0aa7bb7a Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 24 Jul 2024 11:41:15 -0400 Subject: [PATCH 4/8] Fix typo --- README.md | 2 +- dist/esm/index.d.ts | 113 ++++++++++ dist/esm/index.js | 530 ++++++++++++++++++++++++++++++++++++++++++++ dist/esm/types.d.ts | 15 ++ dist/esm/types.ts | 19 ++ 5 files changed, 678 insertions(+), 1 deletion(-) create mode 100644 dist/esm/index.d.ts create mode 100644 dist/esm/index.js create mode 100644 dist/esm/types.d.ts create mode 100644 dist/esm/types.ts diff --git a/README.md b/README.md index 7a8c21d9..83049eb4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ESLint Markdown Language Plugins +# ESLint Markdown Language Plugin [![npm Version](https://img.shields.io/npm/v/@eslint/markdown.svg)](https://www.npmjs.com/package/@eslint/markdown) [![Downloads](https://img.shields.io/npm/dm/@eslint/markdown.svg)](https://www.npmjs.com/package/@eslint/markdown) diff --git a/dist/esm/index.d.ts b/dist/esm/index.d.ts new file mode 100644 index 00000000..cf694eb2 --- /dev/null +++ b/dist/esm/index.d.ts @@ -0,0 +1,113 @@ +export { plugin as default }; +export type Block = import("./types.js").Block; +export type RangeMap = import("./types.js").RangeMap; +export type Node = import("mdast").Node; +export type ParentNode = import("mdast").Parent; +export type CodeNode = import("mdast").Code; +export type HtmlNode = import("mdast").Html; +export type Message = import("eslint").Linter.LintMessage; +export type Range = import("eslint").AST.Range; +declare namespace plugin { + namespace configs { + let recommended: ({ + name: string; + plugins: { + markdown: { + meta: { + name: string; + version: string; + }; + processors: { + markdown: { + meta: { + name: string; + version: string; + }; + preprocess: typeof preprocess; + postprocess: typeof postprocess; + supportsAutofix: boolean; + }; + }; + configs: { + "recommended-legacy": { + plugins: string[]; + overrides: ({ + files: string[]; + processor: string; + parserOptions?: undefined; + rules?: undefined; + } | { + files: string[]; + parserOptions: { + ecmaFeatures: { + impliedStrict: boolean; + }; + }; + rules: { + "eol-last": string; + "no-undef": string; + "no-unused-expressions": string; + "no-unused-vars": string; + "padded-blocks": string; + strict: string; + "unicode-bom": string; + }; + processor?: undefined; + })[]; + }; + }; + }; + }; + files?: undefined; + processor?: undefined; + languageOptions?: undefined; + rules?: undefined; + } | { + name: string; + files: string[]; + processor: string; + plugins?: undefined; + languageOptions?: undefined; + rules?: undefined; + } | { + name: string; + files: string[]; + languageOptions: { + parserOptions: { + ecmaFeatures: { + impliedStrict: boolean; + }; + }; + }; + rules: { + "eol-last": string; + "no-undef": string; + "no-unused-expressions": string; + "no-unused-vars": string; + "padded-blocks": string; + strict: string; + "unicode-bom": string; + }; + plugins?: undefined; + processor?: undefined; + })[]; + } +} +/** + * Extracts lintable code blocks from Markdown text. + * @param {string} text The text of the file. + * @param {string} filename The filename of the file + * @returns {Array<{ filename: string, text: string }>} Source code blocks to lint. + */ +declare function preprocess(text: string, filename: string): Array<{ + filename: string; + text: string; +}>; +/** + * Transforms generated messages for output. + * @param {Array} messages An array containing one array of messages + * for each code block returned from `preprocess`. + * @param {string} filename The filename of the file + * @returns {Message[]} A flattened array of messages with mapped locations. + */ +declare function postprocess(messages: Array, filename: string): Message[]; diff --git a/dist/esm/index.js b/dist/esm/index.js new file mode 100644 index 00000000..c7a852be --- /dev/null +++ b/dist/esm/index.js @@ -0,0 +1,530 @@ +// @ts-self-types="./index.d.ts" +import { fromMarkdown } from 'mdast-util-from-markdown'; + +/** + * @fileoverview Processes Markdown files for consumption by ESLint. + * @author Brandon Mills + */ + + +//----------------------------------------------------------------------------- +// Type Definitions +//----------------------------------------------------------------------------- + +/** @typedef {import("./types.js").Block} Block */ +/** @typedef {import("./types.js").RangeMap} RangeMap */ +/** @typedef {import("mdast").Node} Node */ +/** @typedef {import("mdast").Parent} ParentNode */ +/** @typedef {import("mdast").Code} CodeNode */ +/** @typedef {import("mdast").Html} HtmlNode */ +/** @typedef {import("eslint").Linter.LintMessage} Message */ +/** @typedef {import("eslint").AST.Range} Range */ + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +const UNSATISFIABLE_RULES = new Set([ + "eol-last", // The Markdown parser strips trailing newlines in code fences + "unicode-bom" // Code blocks will begin in the middle of Markdown files +]); +const SUPPORTS_AUTOFIX = true; + +/** + * @type {Map} + */ +const blocksCache = new Map(); + +/** + * Performs a depth-first traversal of the Markdown AST. + * @param {Node} node A Markdown AST node. + * @param {{[key: string]: (node?: Node) => void}} callbacks A map of node types to callbacks. + * @returns {void} + */ +function traverse(node, callbacks) { + if (callbacks[node.type]) { + callbacks[node.type](node); + } else { + callbacks["*"](); + } + + const parent = /** @type {ParentNode} */ (node); + + if (typeof parent.children !== "undefined") { + for (let i = 0; i < parent.children.length; i++) { + traverse(parent.children[i], callbacks); + } + } +} + +/** + * Extracts `eslint-*` or `global` comments from HTML comments if present. + * @param {string} html The text content of an HTML AST node. + * @returns {string} The comment's text without the opening and closing tags or + * an empty string if the text is not an ESLint HTML comment. + */ +function getComment(html) { + const commentStart = ""; + const regex = /^(eslint\b|global\s)/u; + + if ( + html.slice(0, commentStart.length) !== commentStart || + html.slice(-commentEnd.length) !== commentEnd + ) { + return ""; + } + + const comment = html.slice(commentStart.length, -commentEnd.length); + + if (!regex.test(comment.trim())) { + return ""; + } + + return comment; +} + +// Before a code block, blockquote characters (`>`) are also considered +// "whitespace". +const leadingWhitespaceRegex = /^[>\s]*/u; + +/** + * Gets the offset for the first column of the node's first line in the + * original source text. + * @param {Node} node A Markdown code block AST node. + * @returns {number} The offset for the first column of the node's first line. + */ +function getBeginningOfLineOffset(node) { + return node.position.start.offset - node.position.start.column + 1; +} + +/** + * Gets the leading text, typically whitespace with possible blockquote chars, + * used to indent a code block. + * @param {string} text The text of the file. + * @param {Node} node A Markdown code block AST node. + * @returns {string} The text from the start of the first line to the opening + * fence of the code block. + */ +function getIndentText(text, node) { + return leadingWhitespaceRegex.exec( + text.slice(getBeginningOfLineOffset(node)) + )[0]; +} + +/** + * When applying fixes, the postprocess step needs to know how to map fix ranges + * from their location in the linted JS to the original offset in the Markdown. + * Configuration comments and indentation trimming both complicate this process. + * + * Configuration comments appear in the linted JS but not in the Markdown code + * block. Fixes to configuration comments would cause undefined behavior and + * should be ignored during postprocessing. Fixes to actual code after + * configuration comments need to be mapped back to the code block after + * removing any offset due to configuration comments. + * + * Fenced code blocks can be indented by up to three spaces at the opening + * fence. Inside of a list, for example, this indent can be in addition to the + * indent already required for list item children. Leading whitespace inside + * indented code blocks is trimmed up to the level of the opening fence and does + * not appear in the linted code. Further, lines can have less leading + * whitespace than the opening fence, so not all lines are guaranteed to have + * the same column offset as the opening fence. + * + * The source code of a non-configuration-comment line in the linted JS is a + * suffix of the corresponding line in the Markdown code block. There are no + * differences within the line, so the mapping need only provide the offset + * delta at the beginning of each line. + * @param {string} text The text of the file. + * @param {Node} node A Markdown code block AST node. + * @param {string[]} comments List of configuration comment strings that will be + * inserted at the beginning of the code block. + * @returns {RangeMap[]} A list of offset-based adjustments, where lookups are + * done based on the `js` key, which represents the range in the linted JS, + * and the `md` key is the offset delta that, when added to the JS range, + * returns the corresponding location in the original Markdown source. + */ +function getBlockRangeMap(text, node, comments) { + + /* + * The parser sets the fenced code block's start offset to wherever content + * should normally begin (typically the first column of the line, but more + * inside a list item, for example). The code block's opening fence may be + * further indented by up to three characters. If the code block has + * additional indenting, the opening fence's first backtick may be up to + * three whitespace characters after the start offset. + */ + const startOffset = getBeginningOfLineOffset(node); + + /* + * Extract the Markdown source to determine the leading whitespace for each + * line. + */ + const code = text.slice(startOffset, node.position.end.offset); + const lines = code.split("\n"); + + /* + * The parser trims leading whitespace from each line of code within the + * fenced code block up to the opening fence's first backtick. The first + * backtick's column is the AST node's starting column plus any additional + * indentation. + */ + const baseIndent = getIndentText(text, node).length; + + /* + * Track the length of any inserted configuration comments at the beginning + * of the linted JS and start the JS offset lookup keys at this index. + */ + const commentLength = comments.reduce((len, comment) => len + comment.length + 1, 0); + + /* + * In case there are configuration comments, initialize the map so that the + * first lookup index is always 0. If there are no configuration comments, + * the lookup index will also be 0, and the lookup should always go to the + * last range that matches, skipping this initialization entry. + */ + const rangeMap = [{ + indent: baseIndent, + js: 0, + md: 0 + }]; + + // Start the JS offset after any configuration comments. + let jsOffset = commentLength; + + /* + * Start the Markdown offset at the beginning of the block's first line of + * actual code. The first line of the block is always the opening fence, so + * the code begins on the second line. + */ + let mdOffset = startOffset + lines[0].length + 1; + + /* + * For each line, determine how much leading whitespace was trimmed due to + * indentation. Increase the JS lookup offset by the length of the line + * post-trimming and the Markdown offset by the total line length. + */ + for (let i = 0; i + 1 < lines.length; i++) { + const line = lines[i + 1]; + const leadingWhitespaceLength = leadingWhitespaceRegex.exec(line)[0].length; + + // The parser trims leading whitespace up to the level of the opening + // fence, so keep any additional indentation beyond that. + const trimLength = Math.min(baseIndent, leadingWhitespaceLength); + + rangeMap.push({ + indent: trimLength, + js: jsOffset, + + // Advance `trimLength` character from the beginning of the Markdown + // line to the beginning of the equivalent JS line, then compute the + // delta. + md: mdOffset + trimLength - jsOffset + }); + + // Accumulate the current line in the offsets, and don't forget the + // newline. + mdOffset += line.length + 1; + jsOffset += line.length - trimLength + 1; + } + + return rangeMap; +} + +const languageToFileExtension = { + javascript: "js", + ecmascript: "js", + typescript: "ts", + markdown: "md" +}; + +/** + * Extracts lintable code blocks from Markdown text. + * @param {string} text The text of the file. + * @param {string} filename The filename of the file + * @returns {Array<{ filename: string, text: string }>} Source code blocks to lint. + */ +function preprocess(text, filename) { + const ast = fromMarkdown(text); + const blocks = []; + + blocksCache.set(filename, blocks); + + /** + * During the depth-first traversal, keep track of any sequences of HTML + * comment nodes containing `eslint-*` or `global` comments. If a code + * block immediately follows such a sequence, insert the comments at the + * top of the code block. Any non-ESLint comment or other node type breaks + * and empties the sequence. + * @type {string[]} + */ + let htmlComments = []; + + traverse(ast, { + "*"() { + htmlComments = []; + }, + + /** + * Visit a code node. + * @param {CodeNode} node The visited node. + * @returns {void} + */ + code(node) { + if (node.lang) { + const comments = []; + + for (const comment of htmlComments) { + if (comment.trim() === "eslint-skip") { + htmlComments = []; + return; + } + + comments.push(`/*${comment}*/`); + } + + htmlComments = []; + + blocks.push({ + ...node, + baseIndentText: getIndentText(text, node), + comments, + rangeMap: getBlockRangeMap(text, node, comments) + }); + } + }, + + /** + * Visit an HTML node. + * @param {HtmlNode} node The visited node. + * @returns {void} + */ + html(node) { + const comment = getComment(node.value); + + if (comment) { + htmlComments.push(comment); + } else { + htmlComments = []; + } + } + }); + + return blocks.map((block, index) => { + const [language] = block.lang.trim().split(" "); + const fileExtension = Object.hasOwn(languageToFileExtension, language) ? languageToFileExtension[language] : language; + + return { + filename: `${index}.${fileExtension}`, + text: [ + ...block.comments, + block.value, + "" + ].join("\n") + }; + }); +} + +/** + * Creates a map function that adjusts messages in a code block. + * @param {Block} block A code block. + * @returns {(message: Message) => Message} A function that adjusts messages in a code block. + */ +function adjustBlock(block) { + const leadingCommentLines = block.comments.reduce((count, comment) => count + comment.split("\n").length, 0); + + const blockStart = block.position.start.line; + + /** + * Adjusts ESLint messages to point to the correct location in the Markdown. + * @param {Message} message A message from ESLint. + * @returns {Message} The same message, but adjusted to the correct location. + */ + return function adjustMessage(message) { + if (!Number.isInteger(message.line)) { + return { + ...message, + line: blockStart, + column: block.position.start.column + }; + } + + const lineInCode = message.line - leadingCommentLines; + + if (lineInCode < 1 || lineInCode >= block.rangeMap.length) { + return null; + } + + const out = { + line: lineInCode + blockStart, + column: message.column + block.rangeMap[lineInCode].indent + }; + + if (Number.isInteger(message.endLine)) { + out.endLine = message.endLine - leadingCommentLines + blockStart; + } + + const adjustedFix = {}; + + if (message.fix) { + adjustedFix.fix = { + range: /** @typedef {[number,number]} */ (message.fix.range.map(range => { + + // Advance through the block's range map to find the last + // matching range by finding the first range too far and + // then going back one. + let i = 1; + + while (i < block.rangeMap.length && block.rangeMap[i].js <= range) { + i++; + } + + // Apply the mapping delta for this range. + return range + block.rangeMap[i - 1].md; + })), + text: message.fix.text.replace(/\n/gu, `\n${block.baseIndentText}`) + }; + } + + return { ...message, ...out, ...adjustedFix }; + }; +} + +/** + * Excludes unsatisfiable rules from the list of messages. + * @param {Message} message A message from the linter. + * @returns {boolean} True if the message should be included in output. + */ +function excludeUnsatisfiableRules(message) { + return message && !UNSATISFIABLE_RULES.has(message.ruleId); +} + +/** + * Transforms generated messages for output. + * @param {Array} messages An array containing one array of messages + * for each code block returned from `preprocess`. + * @param {string} filename The filename of the file + * @returns {Message[]} A flattened array of messages with mapped locations. + */ +function postprocess(messages, filename) { + const blocks = blocksCache.get(filename); + + blocksCache.delete(filename); + + return messages.flatMap((group, i) => { + const adjust = adjustBlock(blocks[i]); + + return group.map(adjust).filter(excludeUnsatisfiableRules); + }); +} + +const processor = { + meta: { + name: "eslint-plugin-markdown/markdown", + version: "5.1.0" // x-release-please-version + }, + preprocess, + postprocess, + supportsAutofix: SUPPORTS_AUTOFIX +}; + +/** + * @fileoverview Enables the processor for Markdown file extensions. + * @author Brandon Mills + */ + + +const rulesConfig = { + + // The Markdown parser automatically trims trailing + // newlines from code blocks. + "eol-last": "off", + + // In code snippets and examples, these rules are often + // counterproductive to clarity and brevity. + "no-undef": "off", + "no-unused-expressions": "off", + "no-unused-vars": "off", + "padded-blocks": "off", + + // Adding a "use strict" directive at the top of every + // code block is tedious and distracting. The config + // opts into strict mode parsing without the directive. + strict: "off", + + // The processor will not receive a Unicode Byte Order + // Mark from the Markdown parser. + "unicode-bom": "off" +}; + +const plugin = { + meta: { + name: "eslint-plugin-markdown", + version: "5.1.0" // x-release-please-version + }, + processors: { + markdown: processor + }, + configs: { + "recommended-legacy": { + plugins: ["markdown"], + overrides: [ + { + files: ["*.md"], + processor: "markdown/markdown" + }, + { + files: ["**/*.md/**"], + parserOptions: { + ecmaFeatures: { + + // Adding a "use strict" directive at the top of + // every code block is tedious and distracting, so + // opt into strict mode parsing without the + // directive. + impliedStrict: true + } + }, + rules: { + ...rulesConfig + } + } + ] + } + } +}; + +plugin.configs.recommended = [ + { + name: "markdown/recommended/plugin", + plugins: { + markdown: plugin + } + }, + { + name: "markdown/recommended/processor", + files: ["**/*.md"], + processor: "markdown/markdown" + }, + { + name: "markdown/recommended/code-blocks", + files: ["**/*.md/**"], + languageOptions: { + parserOptions: { + ecmaFeatures: { + + // Adding a "use strict" directive at the top of + // every code block is tedious and distracting, so + // opt into strict mode parsing without the + // directive. + impliedStrict: true + } + } + }, + rules: { + ...rulesConfig + } + } +]; + +export { plugin as default }; diff --git a/dist/esm/types.d.ts b/dist/esm/types.d.ts new file mode 100644 index 00000000..76f5231c --- /dev/null +++ b/dist/esm/types.d.ts @@ -0,0 +1,15 @@ +import type { Node } from "mdast"; +import type { Linter } from "eslint"; +export interface RangeMap { + indent: number; + js: number; + md: number; +} +export interface BlockBase { + baseIndentText: string; + comments: string[]; + rangeMap: RangeMap[]; +} +export interface Block extends Node, BlockBase { +} +export type Message = Linter.LintMessage; diff --git a/dist/esm/types.ts b/dist/esm/types.ts new file mode 100644 index 00000000..02638045 --- /dev/null +++ b/dist/esm/types.ts @@ -0,0 +1,19 @@ +import type { Node } from "mdast"; +import type { Linter } from "eslint"; + + +export interface RangeMap { + indent: number; + js: number; + md: number; +} + +export interface BlockBase { + baseIndentText: string; + comments: string[]; + rangeMap: RangeMap[]; +} + +export interface Block extends Node, BlockBase {} + +export type Message = Linter.LintMessage; From d0dbc4a0765ddb3478eca4319e72b9d792547dff Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 24 Jul 2024 11:43:47 -0400 Subject: [PATCH 5/8] Update more references --- examples/react/README.md | 6 +++--- examples/typescript/README.md | 6 +++--- tests/plugin.test.js | 14 +++++++------- tests/processor.test.js | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/examples/react/README.md b/examples/react/README.md index dec0abfa..42adcd16 100644 --- a/examples/react/README.md +++ b/examples/react/README.md @@ -11,13 +11,13 @@ function App({ name }) { ``` ```sh -$ git clone https://github.com/eslint/eslint-plugin-markdown.git -$ cd eslint-plugin-markdown +$ git clone https://github.com/eslint/markdown.git +$ cd markdown $ npm install $ cd examples/react $ npm test -eslint-plugin-markdown/examples/react/README.md +markdown/examples/react/README.md 4:16 error 'name' is missing in props validation react/prop-types ✖ 1 problem (1 error, 0 warnings) diff --git a/examples/typescript/README.md b/examples/typescript/README.md index 7d2fc53c..55673a5c 100644 --- a/examples/typescript/README.md +++ b/examples/typescript/README.md @@ -11,13 +11,13 @@ hello(42 as any); ``` ```sh -$ git clone https://github.com/eslint/eslint-plugin-markdown.git -$ cd eslint-plugin-markdown +$ git clone https://github.com/eslint/markdown.git +$ cd markdown $ npm install $ cd examples/typescript $ npm test -eslint-plugin-markdown/examples/typescript/README.md +markdown/examples/typescript/README.md 6:22 error Prefer using the primitive `string` as a type name, rather than the upper-cased `String` @typescript-eslint/no-wrapper-object-types 10:13 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any diff --git a/tests/plugin.test.js b/tests/plugin.test.js index 94edb62c..15c2b288 100644 --- a/tests/plugin.test.js +++ b/tests/plugin.test.js @@ -69,7 +69,7 @@ function initFlatESLint(fixtureConfigName, options = {}) { describe("meta", () => { it("should export meta property", () => { - assert.deepStrictEqual(plugin.meta, { name: "eslint-plugin-markdown", version: pkg.version }); + assert.deepStrictEqual(plugin.meta, { name: "@eslint/markdown", version: pkg.version }); }); }); @@ -165,7 +165,7 @@ describe("LegacyESLint", () => { assert.strictEqual(results[0].messages[1].endLine, 8); }); - // https://github.com/eslint/eslint-plugin-markdown/issues/77 + // https://github.com/eslint/markdown/issues/77 it("should emit correct line numbers with leading blank line", async () => { const code = [ "### Heading", @@ -277,7 +277,7 @@ describe("LegacyESLint", () => { assert.strictEqual(results[0].messages[4].column, 2); }); - // https://github.com/eslint/eslint-plugin-markdown/issues/181 + // https://github.com/eslint/markdown/issues/181 it("should work when called on nested code blocks in the same file", async () => { /* @@ -350,7 +350,7 @@ describe("LegacyESLint", () => { assert.strictEqual(results[0].messages[3].line, 15); }); - // https://github.com/eslint/eslint-plugin-markdown/issues/78 + // https://github.com/eslint/markdown/issues/78 it("preserves leading empty lines", async () => { const code = [ "", @@ -1127,7 +1127,7 @@ describe("FlatESLint", () => { assert.strictEqual(results[0].messages[1].endLine, 8); }); - // https://github.com/eslint/eslint-plugin-markdown/issues/77 + // https://github.com/eslint/markdown/issues/77 it("should emit correct line numbers with leading blank line", async () => { const code = [ "### Heading", @@ -1239,7 +1239,7 @@ describe("FlatESLint", () => { assert.strictEqual(results[0].messages[4].column, 2); }); - // https://github.com/eslint/eslint-plugin-markdown/issues/181 + // https://github.com/eslint/markdown/issues/181 it("should work when called on nested code blocks in the same file", async () => { /* @@ -1312,7 +1312,7 @@ describe("FlatESLint", () => { assert.strictEqual(results[0].messages[3].line, 15); }); - // https://github.com/eslint/eslint-plugin-markdown/issues/78 + // https://github.com/eslint/markdown/issues/78 it("preserves leading empty lines", async () => { const code = [ "", diff --git a/tests/processor.test.js b/tests/processor.test.js index bb00eb33..d7d9dc65 100644 --- a/tests/processor.test.js +++ b/tests/processor.test.js @@ -30,7 +30,7 @@ describe("processor", () => { describe("meta", () => { it("should have meta property", () => { - assert.deepStrictEqual(processor.meta, { name: "eslint-plugin-markdown/markdown", version: pkg.version }); + assert.deepStrictEqual(processor.meta, { name: "@eslint/markdown/markdown", version: pkg.version }); }); }); @@ -504,7 +504,7 @@ describe("processor", () => { ].join("\n")); }); - // https://github.com/eslint/eslint-plugin-markdown/issues/76 + // https://github.com/eslint/markdown/issues/76 it("should insert comments inside list items", () => { const code = [ "* List item followed by a blank line", From 494de7420fc749cfeb846d076777a564d18adfaa Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 24 Jul 2024 11:44:27 -0400 Subject: [PATCH 6/8] Update CLA URL --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a82eb281..86c14ae1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing Code -Please sign the ESLint [Contributor License Agreement](https://cla.js.foundation/eslint/eslint-plugin-markdown) +Please sign the ESLint [Contributor License Agreement](https://eslint.org/cla) ## Full Documentation From b5eb6c6eebc11e9a3d63245af3447de80b46fe9d Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 25 Jul 2024 10:58:02 -0400 Subject: [PATCH 7/8] Fix lint errors --- eslint.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eslint.config.js b/eslint.config.js index dae8360f..fc7ea9ae 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -15,7 +15,8 @@ export default [ ignores: [ "**/examples", "**/coverage", - "**/tests/fixtures" + "**/tests/fixtures", + "dist" ] }, { From c7978dfe5f555b1fa4dcc3094df11e1cf01c89cd Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 25 Jul 2024 16:13:22 -0400 Subject: [PATCH 8/8] Remove dist files --- dist/esm/index.d.ts | 113 ---------- dist/esm/index.js | 530 -------------------------------------------- dist/esm/types.d.ts | 15 -- dist/esm/types.ts | 19 -- 4 files changed, 677 deletions(-) delete mode 100644 dist/esm/index.d.ts delete mode 100644 dist/esm/index.js delete mode 100644 dist/esm/types.d.ts delete mode 100644 dist/esm/types.ts diff --git a/dist/esm/index.d.ts b/dist/esm/index.d.ts deleted file mode 100644 index cf694eb2..00000000 --- a/dist/esm/index.d.ts +++ /dev/null @@ -1,113 +0,0 @@ -export { plugin as default }; -export type Block = import("./types.js").Block; -export type RangeMap = import("./types.js").RangeMap; -export type Node = import("mdast").Node; -export type ParentNode = import("mdast").Parent; -export type CodeNode = import("mdast").Code; -export type HtmlNode = import("mdast").Html; -export type Message = import("eslint").Linter.LintMessage; -export type Range = import("eslint").AST.Range; -declare namespace plugin { - namespace configs { - let recommended: ({ - name: string; - plugins: { - markdown: { - meta: { - name: string; - version: string; - }; - processors: { - markdown: { - meta: { - name: string; - version: string; - }; - preprocess: typeof preprocess; - postprocess: typeof postprocess; - supportsAutofix: boolean; - }; - }; - configs: { - "recommended-legacy": { - plugins: string[]; - overrides: ({ - files: string[]; - processor: string; - parserOptions?: undefined; - rules?: undefined; - } | { - files: string[]; - parserOptions: { - ecmaFeatures: { - impliedStrict: boolean; - }; - }; - rules: { - "eol-last": string; - "no-undef": string; - "no-unused-expressions": string; - "no-unused-vars": string; - "padded-blocks": string; - strict: string; - "unicode-bom": string; - }; - processor?: undefined; - })[]; - }; - }; - }; - }; - files?: undefined; - processor?: undefined; - languageOptions?: undefined; - rules?: undefined; - } | { - name: string; - files: string[]; - processor: string; - plugins?: undefined; - languageOptions?: undefined; - rules?: undefined; - } | { - name: string; - files: string[]; - languageOptions: { - parserOptions: { - ecmaFeatures: { - impliedStrict: boolean; - }; - }; - }; - rules: { - "eol-last": string; - "no-undef": string; - "no-unused-expressions": string; - "no-unused-vars": string; - "padded-blocks": string; - strict: string; - "unicode-bom": string; - }; - plugins?: undefined; - processor?: undefined; - })[]; - } -} -/** - * Extracts lintable code blocks from Markdown text. - * @param {string} text The text of the file. - * @param {string} filename The filename of the file - * @returns {Array<{ filename: string, text: string }>} Source code blocks to lint. - */ -declare function preprocess(text: string, filename: string): Array<{ - filename: string; - text: string; -}>; -/** - * Transforms generated messages for output. - * @param {Array} messages An array containing one array of messages - * for each code block returned from `preprocess`. - * @param {string} filename The filename of the file - * @returns {Message[]} A flattened array of messages with mapped locations. - */ -declare function postprocess(messages: Array, filename: string): Message[]; diff --git a/dist/esm/index.js b/dist/esm/index.js deleted file mode 100644 index c7a852be..00000000 --- a/dist/esm/index.js +++ /dev/null @@ -1,530 +0,0 @@ -// @ts-self-types="./index.d.ts" -import { fromMarkdown } from 'mdast-util-from-markdown'; - -/** - * @fileoverview Processes Markdown files for consumption by ESLint. - * @author Brandon Mills - */ - - -//----------------------------------------------------------------------------- -// Type Definitions -//----------------------------------------------------------------------------- - -/** @typedef {import("./types.js").Block} Block */ -/** @typedef {import("./types.js").RangeMap} RangeMap */ -/** @typedef {import("mdast").Node} Node */ -/** @typedef {import("mdast").Parent} ParentNode */ -/** @typedef {import("mdast").Code} CodeNode */ -/** @typedef {import("mdast").Html} HtmlNode */ -/** @typedef {import("eslint").Linter.LintMessage} Message */ -/** @typedef {import("eslint").AST.Range} Range */ - -//----------------------------------------------------------------------------- -// Helpers -//----------------------------------------------------------------------------- - -const UNSATISFIABLE_RULES = new Set([ - "eol-last", // The Markdown parser strips trailing newlines in code fences - "unicode-bom" // Code blocks will begin in the middle of Markdown files -]); -const SUPPORTS_AUTOFIX = true; - -/** - * @type {Map} - */ -const blocksCache = new Map(); - -/** - * Performs a depth-first traversal of the Markdown AST. - * @param {Node} node A Markdown AST node. - * @param {{[key: string]: (node?: Node) => void}} callbacks A map of node types to callbacks. - * @returns {void} - */ -function traverse(node, callbacks) { - if (callbacks[node.type]) { - callbacks[node.type](node); - } else { - callbacks["*"](); - } - - const parent = /** @type {ParentNode} */ (node); - - if (typeof parent.children !== "undefined") { - for (let i = 0; i < parent.children.length; i++) { - traverse(parent.children[i], callbacks); - } - } -} - -/** - * Extracts `eslint-*` or `global` comments from HTML comments if present. - * @param {string} html The text content of an HTML AST node. - * @returns {string} The comment's text without the opening and closing tags or - * an empty string if the text is not an ESLint HTML comment. - */ -function getComment(html) { - const commentStart = ""; - const regex = /^(eslint\b|global\s)/u; - - if ( - html.slice(0, commentStart.length) !== commentStart || - html.slice(-commentEnd.length) !== commentEnd - ) { - return ""; - } - - const comment = html.slice(commentStart.length, -commentEnd.length); - - if (!regex.test(comment.trim())) { - return ""; - } - - return comment; -} - -// Before a code block, blockquote characters (`>`) are also considered -// "whitespace". -const leadingWhitespaceRegex = /^[>\s]*/u; - -/** - * Gets the offset for the first column of the node's first line in the - * original source text. - * @param {Node} node A Markdown code block AST node. - * @returns {number} The offset for the first column of the node's first line. - */ -function getBeginningOfLineOffset(node) { - return node.position.start.offset - node.position.start.column + 1; -} - -/** - * Gets the leading text, typically whitespace with possible blockquote chars, - * used to indent a code block. - * @param {string} text The text of the file. - * @param {Node} node A Markdown code block AST node. - * @returns {string} The text from the start of the first line to the opening - * fence of the code block. - */ -function getIndentText(text, node) { - return leadingWhitespaceRegex.exec( - text.slice(getBeginningOfLineOffset(node)) - )[0]; -} - -/** - * When applying fixes, the postprocess step needs to know how to map fix ranges - * from their location in the linted JS to the original offset in the Markdown. - * Configuration comments and indentation trimming both complicate this process. - * - * Configuration comments appear in the linted JS but not in the Markdown code - * block. Fixes to configuration comments would cause undefined behavior and - * should be ignored during postprocessing. Fixes to actual code after - * configuration comments need to be mapped back to the code block after - * removing any offset due to configuration comments. - * - * Fenced code blocks can be indented by up to three spaces at the opening - * fence. Inside of a list, for example, this indent can be in addition to the - * indent already required for list item children. Leading whitespace inside - * indented code blocks is trimmed up to the level of the opening fence and does - * not appear in the linted code. Further, lines can have less leading - * whitespace than the opening fence, so not all lines are guaranteed to have - * the same column offset as the opening fence. - * - * The source code of a non-configuration-comment line in the linted JS is a - * suffix of the corresponding line in the Markdown code block. There are no - * differences within the line, so the mapping need only provide the offset - * delta at the beginning of each line. - * @param {string} text The text of the file. - * @param {Node} node A Markdown code block AST node. - * @param {string[]} comments List of configuration comment strings that will be - * inserted at the beginning of the code block. - * @returns {RangeMap[]} A list of offset-based adjustments, where lookups are - * done based on the `js` key, which represents the range in the linted JS, - * and the `md` key is the offset delta that, when added to the JS range, - * returns the corresponding location in the original Markdown source. - */ -function getBlockRangeMap(text, node, comments) { - - /* - * The parser sets the fenced code block's start offset to wherever content - * should normally begin (typically the first column of the line, but more - * inside a list item, for example). The code block's opening fence may be - * further indented by up to three characters. If the code block has - * additional indenting, the opening fence's first backtick may be up to - * three whitespace characters after the start offset. - */ - const startOffset = getBeginningOfLineOffset(node); - - /* - * Extract the Markdown source to determine the leading whitespace for each - * line. - */ - const code = text.slice(startOffset, node.position.end.offset); - const lines = code.split("\n"); - - /* - * The parser trims leading whitespace from each line of code within the - * fenced code block up to the opening fence's first backtick. The first - * backtick's column is the AST node's starting column plus any additional - * indentation. - */ - const baseIndent = getIndentText(text, node).length; - - /* - * Track the length of any inserted configuration comments at the beginning - * of the linted JS and start the JS offset lookup keys at this index. - */ - const commentLength = comments.reduce((len, comment) => len + comment.length + 1, 0); - - /* - * In case there are configuration comments, initialize the map so that the - * first lookup index is always 0. If there are no configuration comments, - * the lookup index will also be 0, and the lookup should always go to the - * last range that matches, skipping this initialization entry. - */ - const rangeMap = [{ - indent: baseIndent, - js: 0, - md: 0 - }]; - - // Start the JS offset after any configuration comments. - let jsOffset = commentLength; - - /* - * Start the Markdown offset at the beginning of the block's first line of - * actual code. The first line of the block is always the opening fence, so - * the code begins on the second line. - */ - let mdOffset = startOffset + lines[0].length + 1; - - /* - * For each line, determine how much leading whitespace was trimmed due to - * indentation. Increase the JS lookup offset by the length of the line - * post-trimming and the Markdown offset by the total line length. - */ - for (let i = 0; i + 1 < lines.length; i++) { - const line = lines[i + 1]; - const leadingWhitespaceLength = leadingWhitespaceRegex.exec(line)[0].length; - - // The parser trims leading whitespace up to the level of the opening - // fence, so keep any additional indentation beyond that. - const trimLength = Math.min(baseIndent, leadingWhitespaceLength); - - rangeMap.push({ - indent: trimLength, - js: jsOffset, - - // Advance `trimLength` character from the beginning of the Markdown - // line to the beginning of the equivalent JS line, then compute the - // delta. - md: mdOffset + trimLength - jsOffset - }); - - // Accumulate the current line in the offsets, and don't forget the - // newline. - mdOffset += line.length + 1; - jsOffset += line.length - trimLength + 1; - } - - return rangeMap; -} - -const languageToFileExtension = { - javascript: "js", - ecmascript: "js", - typescript: "ts", - markdown: "md" -}; - -/** - * Extracts lintable code blocks from Markdown text. - * @param {string} text The text of the file. - * @param {string} filename The filename of the file - * @returns {Array<{ filename: string, text: string }>} Source code blocks to lint. - */ -function preprocess(text, filename) { - const ast = fromMarkdown(text); - const blocks = []; - - blocksCache.set(filename, blocks); - - /** - * During the depth-first traversal, keep track of any sequences of HTML - * comment nodes containing `eslint-*` or `global` comments. If a code - * block immediately follows such a sequence, insert the comments at the - * top of the code block. Any non-ESLint comment or other node type breaks - * and empties the sequence. - * @type {string[]} - */ - let htmlComments = []; - - traverse(ast, { - "*"() { - htmlComments = []; - }, - - /** - * Visit a code node. - * @param {CodeNode} node The visited node. - * @returns {void} - */ - code(node) { - if (node.lang) { - const comments = []; - - for (const comment of htmlComments) { - if (comment.trim() === "eslint-skip") { - htmlComments = []; - return; - } - - comments.push(`/*${comment}*/`); - } - - htmlComments = []; - - blocks.push({ - ...node, - baseIndentText: getIndentText(text, node), - comments, - rangeMap: getBlockRangeMap(text, node, comments) - }); - } - }, - - /** - * Visit an HTML node. - * @param {HtmlNode} node The visited node. - * @returns {void} - */ - html(node) { - const comment = getComment(node.value); - - if (comment) { - htmlComments.push(comment); - } else { - htmlComments = []; - } - } - }); - - return blocks.map((block, index) => { - const [language] = block.lang.trim().split(" "); - const fileExtension = Object.hasOwn(languageToFileExtension, language) ? languageToFileExtension[language] : language; - - return { - filename: `${index}.${fileExtension}`, - text: [ - ...block.comments, - block.value, - "" - ].join("\n") - }; - }); -} - -/** - * Creates a map function that adjusts messages in a code block. - * @param {Block} block A code block. - * @returns {(message: Message) => Message} A function that adjusts messages in a code block. - */ -function adjustBlock(block) { - const leadingCommentLines = block.comments.reduce((count, comment) => count + comment.split("\n").length, 0); - - const blockStart = block.position.start.line; - - /** - * Adjusts ESLint messages to point to the correct location in the Markdown. - * @param {Message} message A message from ESLint. - * @returns {Message} The same message, but adjusted to the correct location. - */ - return function adjustMessage(message) { - if (!Number.isInteger(message.line)) { - return { - ...message, - line: blockStart, - column: block.position.start.column - }; - } - - const lineInCode = message.line - leadingCommentLines; - - if (lineInCode < 1 || lineInCode >= block.rangeMap.length) { - return null; - } - - const out = { - line: lineInCode + blockStart, - column: message.column + block.rangeMap[lineInCode].indent - }; - - if (Number.isInteger(message.endLine)) { - out.endLine = message.endLine - leadingCommentLines + blockStart; - } - - const adjustedFix = {}; - - if (message.fix) { - adjustedFix.fix = { - range: /** @typedef {[number,number]} */ (message.fix.range.map(range => { - - // Advance through the block's range map to find the last - // matching range by finding the first range too far and - // then going back one. - let i = 1; - - while (i < block.rangeMap.length && block.rangeMap[i].js <= range) { - i++; - } - - // Apply the mapping delta for this range. - return range + block.rangeMap[i - 1].md; - })), - text: message.fix.text.replace(/\n/gu, `\n${block.baseIndentText}`) - }; - } - - return { ...message, ...out, ...adjustedFix }; - }; -} - -/** - * Excludes unsatisfiable rules from the list of messages. - * @param {Message} message A message from the linter. - * @returns {boolean} True if the message should be included in output. - */ -function excludeUnsatisfiableRules(message) { - return message && !UNSATISFIABLE_RULES.has(message.ruleId); -} - -/** - * Transforms generated messages for output. - * @param {Array} messages An array containing one array of messages - * for each code block returned from `preprocess`. - * @param {string} filename The filename of the file - * @returns {Message[]} A flattened array of messages with mapped locations. - */ -function postprocess(messages, filename) { - const blocks = blocksCache.get(filename); - - blocksCache.delete(filename); - - return messages.flatMap((group, i) => { - const adjust = adjustBlock(blocks[i]); - - return group.map(adjust).filter(excludeUnsatisfiableRules); - }); -} - -const processor = { - meta: { - name: "eslint-plugin-markdown/markdown", - version: "5.1.0" // x-release-please-version - }, - preprocess, - postprocess, - supportsAutofix: SUPPORTS_AUTOFIX -}; - -/** - * @fileoverview Enables the processor for Markdown file extensions. - * @author Brandon Mills - */ - - -const rulesConfig = { - - // The Markdown parser automatically trims trailing - // newlines from code blocks. - "eol-last": "off", - - // In code snippets and examples, these rules are often - // counterproductive to clarity and brevity. - "no-undef": "off", - "no-unused-expressions": "off", - "no-unused-vars": "off", - "padded-blocks": "off", - - // Adding a "use strict" directive at the top of every - // code block is tedious and distracting. The config - // opts into strict mode parsing without the directive. - strict: "off", - - // The processor will not receive a Unicode Byte Order - // Mark from the Markdown parser. - "unicode-bom": "off" -}; - -const plugin = { - meta: { - name: "eslint-plugin-markdown", - version: "5.1.0" // x-release-please-version - }, - processors: { - markdown: processor - }, - configs: { - "recommended-legacy": { - plugins: ["markdown"], - overrides: [ - { - files: ["*.md"], - processor: "markdown/markdown" - }, - { - files: ["**/*.md/**"], - parserOptions: { - ecmaFeatures: { - - // Adding a "use strict" directive at the top of - // every code block is tedious and distracting, so - // opt into strict mode parsing without the - // directive. - impliedStrict: true - } - }, - rules: { - ...rulesConfig - } - } - ] - } - } -}; - -plugin.configs.recommended = [ - { - name: "markdown/recommended/plugin", - plugins: { - markdown: plugin - } - }, - { - name: "markdown/recommended/processor", - files: ["**/*.md"], - processor: "markdown/markdown" - }, - { - name: "markdown/recommended/code-blocks", - files: ["**/*.md/**"], - languageOptions: { - parserOptions: { - ecmaFeatures: { - - // Adding a "use strict" directive at the top of - // every code block is tedious and distracting, so - // opt into strict mode parsing without the - // directive. - impliedStrict: true - } - } - }, - rules: { - ...rulesConfig - } - } -]; - -export { plugin as default }; diff --git a/dist/esm/types.d.ts b/dist/esm/types.d.ts deleted file mode 100644 index 76f5231c..00000000 --- a/dist/esm/types.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Node } from "mdast"; -import type { Linter } from "eslint"; -export interface RangeMap { - indent: number; - js: number; - md: number; -} -export interface BlockBase { - baseIndentText: string; - comments: string[]; - rangeMap: RangeMap[]; -} -export interface Block extends Node, BlockBase { -} -export type Message = Linter.LintMessage; diff --git a/dist/esm/types.ts b/dist/esm/types.ts deleted file mode 100644 index 02638045..00000000 --- a/dist/esm/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Node } from "mdast"; -import type { Linter } from "eslint"; - - -export interface RangeMap { - indent: number; - js: number; - md: number; -} - -export interface BlockBase { - baseIndentText: string; - comments: string[]; - rangeMap: RangeMap[]; -} - -export interface Block extends Node, BlockBase {} - -export type Message = Linter.LintMessage;