mirror of
				https://github.com/theoludwig/markdownlint-rule-relative-links.git
				synced 2025-11-01 00:49:07 +01: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