mirror of
				https://github.com/theoludwig/markdownlint-rule-relative-links.git
				synced 2025-09-09 19:39:29 +02:00 
			
		
		
		
	fix: cleaner code + better error messages
This commit is contained in:
		
							
								
								
									
										1
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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" | ||||
|   | ||||
| @@ -2,4 +2,5 @@ | ||||
| . "$(dirname "$0")/_/husky.sh" | ||||
|  | ||||
| npm run lint:staged | ||||
| npm run lint:javascript | ||||
| npm run test | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										24
									
								
								jsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								jsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										27
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										27
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -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" | ||||
|   | ||||
| @@ -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" | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										31
									
								
								src/index.js
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								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`, | ||||
|                 }) | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|   | ||||
							
								
								
									
										33
									
								
								src/utils.js
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								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, | ||||
| } | ||||
|   | ||||
| @@ -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', | ||||
|   ) | ||||
| }) | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| const test = require("node:test") | ||||
| const { test } = require("node:test") | ||||
| const assert = require("node:assert/strict") | ||||
|  | ||||
| const { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user