diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6a84750..18e48b1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -26,3 +26,4 @@ jobs: - run: "npm run lint:markdown" - run: "npm run lint:eslint" - run: "npm run lint:prettier" + - run: "npm run lint:javascript" diff --git a/.husky/pre-commit b/.husky/pre-commit index b4b9b05..9c8ffd6 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -2,4 +2,5 @@ . "$(dirname "$0")/_/husky.sh" npm run lint:staged +npm run lint:javascript npm run test diff --git a/README.md b/README.md index ca6dae5..e79d49a 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ With `awesome.md` content: Running [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2) with `markdownlint-rule-relative-links` will output: ```sh -awesome.md:3 relative-links Relative links should be valid [Link "./invalid.txt" should exist in the file system] +awesome.md:3 relative-links Relative links should be valid ["./invalid.txt" should exist in the file system] ``` ### Additional features diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..e997767 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "target": "ESNext", + "module": "CommonJS", + "moduleResolution": "Node", + "checkJs": true, + "allowJs": true, + "noEmit": true, + "rootDir": ".", + "baseUrl": ".", + "strict": true, + "allowUnusedLabels": false, + "allowUnreachableCode": false, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "forceConsistentCasingInFileNames": true + } +} diff --git a/package-lock.json b/package-lock.json index fe33d67..2971363 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "devDependencies": { "@commitlint/cli": "18.4.4", "@commitlint/config-conventional": "18.4.4", + "@types/markdown-it": "13.0.7", "@types/node": "20.10.8", "editorconfig-checker": "5.1.2", "eslint": "8.56.0", @@ -30,7 +31,8 @@ "markdownlint-cli2": "0.11.0", "pinst": "3.0.0", "prettier": "3.1.1", - "semantic-release": "22.0.12" + "semantic-release": "22.0.12", + "typescript": "5.3.3" }, "engines": { "node": ">=16.0.0", @@ -1294,6 +1296,28 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/linkify-it": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", + "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==", + "dev": true + }, + "node_modules/@types/markdown-it": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.7.tgz", + "integrity": "sha512-U/CBi2YUUcTHBt5tjO2r5QV/x0Po6nsYwQU4Y04fBS6vfoImaiZ6f8bi3CjTCxBPQSO1LMyUqkByzi8AidyxfA==", + "dev": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", + "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", + "dev": true + }, "node_modules/@types/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", @@ -10351,7 +10375,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 9ecdb4e..07ded7d 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "lint:markdown": "markdownlint-cli2", "lint:eslint": "eslint . --max-warnings 0 --report-unused-disable-directives --ignore-path .gitignore", "lint:prettier": "prettier . --check --ignore-path .gitignore", + "lint:javascript": "tsc --project jsconfig.json --noEmit", "lint:staged": "lint-staged", "test": "node --test ./test", "release": "semantic-release", @@ -48,6 +49,7 @@ "devDependencies": { "@commitlint/cli": "18.4.4", "@commitlint/config-conventional": "18.4.4", + "@types/markdown-it": "13.0.7", "@types/node": "20.10.8", "editorconfig-checker": "5.1.2", "eslint": "8.56.0", @@ -63,6 +65,7 @@ "markdownlint-cli2": "0.11.0", "pinst": "3.0.0", "prettier": "3.1.1", - "semantic-release": "22.0.12" + "semantic-release": "22.0.12", + "typescript": "5.3.3" } } diff --git a/src/index.js b/src/index.js index 9e8012d..1ba3513 100644 --- a/src/index.js +++ b/src/index.js @@ -5,22 +5,27 @@ const fs = require("node:fs") const { filterTokens, - addError, convertHeadingToHTMLFragment, getMarkdownHeadings, } = require("./utils.js") +/** @typedef {import('markdownlint').Rule} MarkdownLintRule */ + +/** + * @type {MarkdownLintRule} + */ const customRule = { names: ["relative-links"], description: "Relative links should be valid", tags: ["links"], function: (params, onError) => { filterTokens(params, "inline", (token) => { - for (const child of token.children) { - const { lineNumber, type, attrs } = child + const children = token.children ?? [] + for (const child of children) { + const { type, attrs, lineNumber } = child - /** @type {string | null} */ - let hrefSrc = null + /** @type {string | undefined} */ + let hrefSrc if (type === "link_open") { for (const attr of attrs) { @@ -45,14 +50,13 @@ const customRule = { const isRelative = url.protocol === "file:" && !hrefSrc.startsWith("/") if (isRelative) { - const detail = `Link "${hrefSrc}"` + const detail = `"${hrefSrc}"` if (!fs.existsSync(url)) { - addError( - onError, + onError({ lineNumber, - `${detail} should exist in the file system`, - ) + detail: `${detail} should exist in the file system`, + }) continue } @@ -74,11 +78,10 @@ const customRule = { }) if (!headingsHTMLFragments.includes(url.hash)) { - addError( - onError, + onError({ lineNumber, - `${detail} should have a valid fragment`, - ) + detail: `${detail} should have a valid fragment identifier`, + }) } } } diff --git a/src/utils.js b/src/utils.js index de73cd1..49baaec 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,11 +1,14 @@ const MarkdownIt = require("markdown-it") +/** @typedef {import('markdownlint').RuleParams} MarkdownLintRuleParams */ +/** @typedef {import('markdownlint').MarkdownItToken} MarkdownItToken */ + /** * Calls the provided function for each matching token. * - * @param {object} params RuleParams instance. + * @param {MarkdownLintRuleParams} params RuleParams instance. * @param {string} type Token type identifier. - * @param {Function} handler Callback function. + * @param {(token: MarkdownItToken) => void} handler Callback function. * @returns {void} */ const filterTokens = (params, type, handler) => { @@ -16,27 +19,6 @@ const filterTokens = (params, type, handler) => { } } -/** - * Adds a generic error object via the onError callback. - * - * @param {object} onError RuleOnError instance. - * @param {number} lineNumber Line number. - * @param {string} [detail] Error details. - * @param {string} [context] Error context. - * @param {number[]} [range] Column and length of error. - * @param {object} [fixInfo] RuleOnErrorFixInfo instance. - * @returns {void} - */ -const addError = (onError, lineNumber, detail, context, range, fixInfo) => { - onError({ - lineNumber, - detail, - context, - range, - fixInfo, - }) -} - /** * Converts a Markdown heading into an HTML fragment according to the rules * used by GitHub. @@ -98,8 +80,10 @@ const getMarkdownHeadings = (content) => { continue } + const children = token.children ?? [] + headings.push( - `${token.children + `${children .map((token) => { return token.content }) @@ -112,7 +96,6 @@ const getMarkdownHeadings = (content) => { module.exports = { filterTokens, - addError, convertHeadingToHTMLFragment, getMarkdownHeadings, } diff --git a/test/basic.test.js b/test/basic.test.js index aac73d4..80897ad 100644 --- a/test/basic.test.js +++ b/test/basic.test.js @@ -1,4 +1,4 @@ -const test = require("node:test") +const { test } = require("node:test") const assert = require("node:assert/strict") const { markdownlint } = require("markdownlint").promises @@ -14,33 +14,33 @@ test("ensure the rule validate correctly", async () => { }, customRules: [relativeLinks], }) - assert.equal(lintResults["test/fixtures/Valid.md"].length, 0) - assert.equal(lintResults["test/fixtures/Invalid.md"].length, 3) + assert.equal(lintResults["test/fixtures/Valid.md"]?.length, 0) + assert.equal(lintResults["test/fixtures/Invalid.md"]?.length, 3) assert.equal( - lintResults["test/fixtures/Invalid.md"][0]?.ruleDescription, + lintResults["test/fixtures/Invalid.md"]?.[0]?.ruleDescription, "Relative links should be valid", ) assert.equal( - lintResults["test/fixtures/Invalid.md"][0]?.errorDetail, - 'Link "./basic.test.js" should exist in the file system', + lintResults["test/fixtures/Invalid.md"]?.[0]?.errorDetail, + '"./basic.test.js" should exist in the file system', ) assert.equal( - lintResults["test/fixtures/Invalid.md"][1]?.ruleDescription, + lintResults["test/fixtures/Invalid.md"]?.[1]?.ruleDescription, "Relative links should be valid", ) assert.equal( - lintResults["test/fixtures/Invalid.md"][1]?.errorDetail, - 'Link "../image.png" should exist in the file system', + lintResults["test/fixtures/Invalid.md"]?.[1]?.errorDetail, + '"../image.png" should exist in the file system', ) assert.equal( - lintResults["test/fixtures/Invalid.md"][2]?.ruleDescription, + lintResults["test/fixtures/Invalid.md"]?.[2]?.ruleDescription, "Relative links should be valid", ) assert.equal( - lintResults["test/fixtures/Invalid.md"][2]?.errorDetail, - 'Link "./Valid.md#not-existing-heading" should have a valid fragment', + lintResults["test/fixtures/Invalid.md"]?.[2]?.errorDetail, + '"./Valid.md#not-existing-heading" should have a valid fragment identifier', ) }) diff --git a/test/utils.test.js b/test/utils.test.js index 71a4521..c87caa3 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -1,4 +1,4 @@ -const test = require("node:test") +const { test } = require("node:test") const assert = require("node:assert/strict") const {