diff --git a/README.md b/README.md index 4e08f923..8484ee3e 100644 --- a/README.md +++ b/README.md @@ -45,15 +45,19 @@ export default [ ### Rules -| **Rule Name** | **Description** | -|---------------|-----------------| -| [`fenced-code-language`](./docs/rules/fenced-code-language.md) | Enforce fenced code blocks to specify a language. | -| [`heading-increment`](./docs/rules/heading-increment.md) | Enforce heading levels increment by one. | -| [`no-duplicate-headings`](./docs/rules/no-duplicate-headings.md) | Disallow duplicate headings in the same document. | -| [`no-empty-links`](./docs/rules/no-empty-links.md) | Disallow empty links. | -| [`no-html`](./docs/rules/no-html.md) | Enforce fenced code blocks to specify a language. | -| [`no-invalid-label-refs`](./docs/rules/no-invalid-label-refs.md) | Disallow invalid label references. | -| [`no-missing-label-refs`](./docs/rules/no-missing-label-refs.md) | Disallow missing label references. | + + + +| **Rule Name** | **Description** | **Recommended** | +| :- | :- | :-: | +| [`fenced-code-language`](./docs/rules/fenced-code-language.md) | Require languages for fenced code blocks. | yes | +| [`heading-increment`](./docs/rules/heading-increment.md) | Enforce heading levels increment by one. | yes | +| [`no-duplicate-headings`](./docs/rules/no-duplicate-headings.md) | Disallow duplicate headings in the same document. | no | +| [`no-empty-links`](./docs/rules/no-empty-links.md) | Disallow empty links. | yes | +| [`no-html`](./docs/rules/no-html.md) | Disallow HTML tags. | no | +| [`no-invalid-label-refs`](./docs/rules/no-invalid-label-refs.md) | Disallow invalid label references. | yes | +| [`no-missing-label-refs`](./docs/rules/no-missing-label-refs.md) | Disallow missing label references. | yes | + **Note:** This plugin does not provide formatting rules. We recommend using a source code formatter such as [Prettier](https://prettier.io) for that purpose. diff --git a/package.json b/package.json index 4051c951..42f0eee5 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "eslint --fix", "prettier --write" ], - "!(*.{js,md})": "prettier --write --ignore-unknown" + "!(*.{js,md})": "prettier --write --ignore-unknown", + "{src/rules/*.js,tools/update-rules-docs.js,README.md}": "npm run build:update-rules-docs" }, "scripts": { "lint": "eslint . && eslint -c eslint.config-content.js .", @@ -50,7 +51,8 @@ "fmt:check": "prettier --check .", "build:dedupe-types": "node tools/dedupe-types.js dist/esm/index.js", "build:rules": "node tools/build-rules.js", - "build": "npm run build:rules && rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json", + "build:update-rules-docs": "node tools/update-rules-docs.js", + "build": "npm run build:rules && rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json && npm run build:update-rules-docs", "prepare": "node ./npm-prepare.cjs && npm run build", "test": "c8 mocha \"tests/**/*.test.js\" --timeout 30000", "test:jsr": "npx jsr@latest publish --dry-run" diff --git a/tools/update-rules-docs.js b/tools/update-rules-docs.js new file mode 100644 index 00000000..07a3be93 --- /dev/null +++ b/tools/update-rules-docs.js @@ -0,0 +1,102 @@ +/** + * @fileoverview Updates the rules table in README.md with rule names, + * descriptions, and whether the rules are recommended or not. + * + * Usage: + * node tools/update-rules-docs.js + * + * @author Francesco Trotta + */ + +//----------------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------------- + +import { fromMarkdown } from "mdast-util-from-markdown"; +import fs from "node:fs/promises"; +import path from "node:path"; + +//----------------------------------------------------------------------------- +// Type Definitions +//----------------------------------------------------------------------------- + +/** @typedef {import("eslint").AST.Range} Range */ + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +const docsFileURL = new URL("../README.md", import.meta.url); +const rulesDirURL = new URL("../src/rules/", import.meta.url); + +/** + * Formats a table row from a rule filename. + * @param {string} ruleFilename The filename of the rule module without directory. + * @returns {Promise} The formatted markdown text of the table row. + */ +async function formatTableRowFromFilename(ruleFilename) { + const ruleURL = new URL(ruleFilename, rulesDirURL); + const { default: rule } = await import(ruleURL); + const ruleName = path.parse(ruleFilename).name; + const { description, recommended } = rule.meta.docs; + const ruleLink = `[\`${ruleName}\`](./docs/rules/${ruleName}.md)`; + const recommendedText = recommended ? "yes" : "no"; + + return `| ${ruleLink} | ${description} | ${recommendedText} |`; +} + +/** + * Generates the markdown text for the rules table. + * @returns {Promise} The formatted markdown text of the rules table. + */ +async function createRulesTableText() { + const filenames = await fs.readdir(rulesDirURL); + const ruleFilenames = filenames.filter( + filename => path.extname(filename) === ".js", + ); + const text = [ + "| **Rule Name** | **Description** | **Recommended** |", + "| :- | :- | :-: |", + ...(await Promise.all(ruleFilenames.map(formatTableRowFromFilename))), + ].join("\n"); + + return text; +} + +/** + * Returns start and end offset of the rules table as indicated by "Rule Table Start" and + * "Rule Table End" HTML comments in the markdown text. + * @param {string} text The markdown text. + * @returns {Range | null} The offset range of the rules table, or `null`. + */ +function getRulesTableRange(text) { + const tree = fromMarkdown(text); + const htmlNodes = tree.children.filter(({ type }) => type === "html"); + const startComment = htmlNodes.find( + ({ value }) => value === "", + ); + const endComment = htmlNodes.find( + ({ value }) => value === "", + ); + + return startComment && endComment + ? [startComment.position.end.offset, endComment.position.start.offset] + : null; +} + +//----------------------------------------------------------------------------- +// Main +//----------------------------------------------------------------------------- + +let docsText = await fs.readFile(docsFileURL, "utf-8"); +const rulesTableRange = getRulesTableRange(docsText); + +if (!rulesTableRange) { + throw Error("Rule Table Start/End comments not found, unable to update."); +} + +const tableText = await createRulesTableText(); + +docsText = `${docsText.slice(0, rulesTableRange[0])}\n${tableText}\n${docsText.slice(rulesTableRange[1])}`; + +await fs.writeFile(docsFileURL, docsText);