mirror of
				https://github.com/theoludwig/markdownlint-rule-relative-links.git
				synced 2025-11-01 00:49:07 +01:00 
			
		
		
		
	Compare commits
	
		
			37 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						257d65e6f5
	
				 | 
					
					
						|||
| 
						
						
							
						
						bc89a30309
	
				 | 
					
					
						|||
| 
						 | 
					3ebc40c2ad | ||
| 
						 | 
					8a449ad181 | ||
| 
						
						
							
						
						9bb5ffe0ae
	
				 | 
					
					
						|||
| 
						
						
							
						
						876384344c
	
				 | 
					
					
						|||
| 
						
						
							
						
						70bdb7013e
	
				 | 
					
					
						|||
| 
						
						
							
						
						db57d67b0b
	
				 | 
					
					
						|||
| 
						
						
							
						
						aa24db4fac
	
				 | 
					
					
						|||
| 
						
						
							
						
						b4a04d2e8e
	
				 | 
					
					
						|||
| 
						
						
							
						
						54e45d3e5d
	
				 | 
					
					
						|||
| 
						
						
							
						
						dd70d6da3a
	
				 | 
					
					
						|||
| 
						
						
							
						
						85f465306f
	
				 | 
					
					
						|||
| 
						
						
							
						
						450cdb84f0
	
				 | 
					
					
						|||
| 
						
						
							
						
						2df95e97d8
	
				 | 
					
					
						|||
| 
						
						
							
						
						bf9403ad84
	
				 | 
					
					
						|||
| 
						
						
							
						
						9675c7a275
	
				 | 
					
					
						|||
| 
						
						
							
						
						5af131b840
	
				 | 
					
					
						|||
| 
						
						
							
						
						f332c833ca
	
				 | 
					
					
						|||
| 
						
						
							
						
						e20ee54b05
	
				 | 
					
					
						|||
| 
						
						
							
						
						5c39afbe20
	
				 | 
					
					
						|||
| 
						
						
							
						
						cc9a1cf6a2
	
				 | 
					
					
						|||
| 
						
						
							
						
						1095647d41
	
				 | 
					
					
						|||
| 146f904866 | |||
| 64396954e4 | |||
| 92f35daeaf | |||
| 7ef7cc3bb3 | |||
| 0479652ffe | |||
| 68f35ddc0b | |||
| 747203c23b | |||
| a5deae599a | |||
| 
						 | 
					24a0788d32 | ||
| 
						
						
							
						
						9d2cc818d5
	
				 | 
					
					
						|||
| 
						
						
							
						
						7465ffd8bc
	
				 | 
					
					
						|||
| 
						
						
							
						
						1ddcdc7b18
	
				 | 
					
					
						|||
| 
						
						
							
						
						fcd0340e57
	
				 | 
					
					
						|||
| 
						
						
							
						
						7bf3b93822
	
				 | 
					
					
						
@@ -1 +0,0 @@
 | 
			
		||||
{ "extends": ["@commitlint/config-conventional"] }
 | 
			
		||||
@@ -1,7 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": ["conventions", "prettier"],
 | 
			
		||||
  "plugins": ["prettier"],
 | 
			
		||||
  "rules": {
 | 
			
		||||
    "prettier/prettier": "error"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										23
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							@@ -4,16 +4,16 @@ on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: [develop]
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches: [master, develop]
 | 
			
		||||
    branches: [main, develop]
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  lint:
 | 
			
		||||
    runs-on: "ubuntu-latest"
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: "actions/checkout@v4.1.1"
 | 
			
		||||
      - uses: "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" # v5.0.0
 | 
			
		||||
 | 
			
		||||
      - name: "Setup Node.js"
 | 
			
		||||
        uses: "actions/setup-node@v4.0.1"
 | 
			
		||||
        uses: "actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903" # v6.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: "lts/*"
 | 
			
		||||
          cache: "npm"
 | 
			
		||||
@@ -21,8 +21,15 @@ jobs:
 | 
			
		||||
      - name: "Install dependencies"
 | 
			
		||||
        run: "npm clean-install"
 | 
			
		||||
 | 
			
		||||
      - run: 'npm run lint:commit -- --to "${{ github.sha }}"'
 | 
			
		||||
      - run: "npm run lint:editorconfig"
 | 
			
		||||
      - run: "npm run lint:markdown"
 | 
			
		||||
      - run: "npm run lint:eslint"
 | 
			
		||||
      - run: "npm run lint:prettier"
 | 
			
		||||
      - run: "node --run lint:editorconfig"
 | 
			
		||||
      - run: "node --run lint:markdown"
 | 
			
		||||
      - run: "node --run lint:eslint"
 | 
			
		||||
      - run: "node --run lint:prettier"
 | 
			
		||||
      - run: "node --run lint:typescript"
 | 
			
		||||
 | 
			
		||||
  commitlint:
 | 
			
		||||
    runs-on: "ubuntu-latest"
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" # v5.0.0
 | 
			
		||||
 | 
			
		||||
      - uses: "wagoid/commitlint-github-action@b948419dd99f3fd78a6548d48f94e3df7f6bf3ed" # v6.2.1
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@@ -2,7 +2,7 @@ name: "Release"
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: [master]
 | 
			
		||||
    branches: [main]
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  release:
 | 
			
		||||
@@ -13,13 +13,13 @@ jobs:
 | 
			
		||||
      pull-requests: "write"
 | 
			
		||||
      id-token: "write"
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: "actions/checkout@v4.1.1"
 | 
			
		||||
      - uses: "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          fetch-depth: 0
 | 
			
		||||
          persist-credentials: false
 | 
			
		||||
 | 
			
		||||
      - name: "Setup Node.js"
 | 
			
		||||
        uses: "actions/setup-node@v4.0.1"
 | 
			
		||||
        uses: "actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903" # v6.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: "lts/*"
 | 
			
		||||
          cache: "npm"
 | 
			
		||||
@@ -31,7 +31,7 @@ jobs:
 | 
			
		||||
        run: "npm audit signatures"
 | 
			
		||||
 | 
			
		||||
      - name: "Release"
 | 
			
		||||
        run: "npm run release"
 | 
			
		||||
        run: "node --run release"
 | 
			
		||||
        env:
 | 
			
		||||
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										17
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							@@ -4,16 +4,23 @@ on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: [develop]
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches: [master, develop]
 | 
			
		||||
    branches: [main, develop]
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  test:
 | 
			
		||||
    runs-on: "ubuntu-latest"
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        runs-on:
 | 
			
		||||
          - "ubuntu-latest"
 | 
			
		||||
          - "windows-latest"
 | 
			
		||||
          - "macos-latest"
 | 
			
		||||
    runs-on: "${{ matrix.runs-on }}"
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: "actions/checkout@v4.1.1"
 | 
			
		||||
      - uses: "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" # v5.0.0
 | 
			
		||||
 | 
			
		||||
      - name: "Setup Node.js"
 | 
			
		||||
        uses: "actions/setup-node@v4.0.1"
 | 
			
		||||
        uses: "actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903" # v6.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: "lts/*"
 | 
			
		||||
          cache: "npm"
 | 
			
		||||
@@ -22,4 +29,4 @@ jobs:
 | 
			
		||||
        run: "npm clean-install"
 | 
			
		||||
 | 
			
		||||
      - name: "Test"
 | 
			
		||||
        run: "npm run test"
 | 
			
		||||
        run: "node --run test"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -20,10 +20,6 @@ npm-debug.log*
 | 
			
		||||
 | 
			
		||||
# IDE - VSCode
 | 
			
		||||
.vscode/*
 | 
			
		||||
!.vscode/settings.json
 | 
			
		||||
!.vscode/tasks.json
 | 
			
		||||
!.vscode/launch.json
 | 
			
		||||
!.vscode/extensions.json
 | 
			
		||||
 | 
			
		||||
# misc
 | 
			
		||||
.DS_Store
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +0,0 @@
 | 
			
		||||
#!/bin/sh
 | 
			
		||||
. "$(dirname "$0")/_/husky.sh"
 | 
			
		||||
 | 
			
		||||
npm run lint:commit -- --edit
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
#!/bin/sh
 | 
			
		||||
. "$(dirname "$0")/_/husky.sh"
 | 
			
		||||
 | 
			
		||||
npm run lint:staged
 | 
			
		||||
npm run test
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "*": ["editorconfig-checker"],
 | 
			
		||||
  "*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix"],
 | 
			
		||||
  "*.{json,jsonc,yml,yaml}": ["prettier --write"],
 | 
			
		||||
  "*.{md,mdx}": ["prettier --write", "markdownlint-cli2 --fix"]
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "config": {
 | 
			
		||||
    "extends": "markdownlint/style/prettier",
 | 
			
		||||
    "relative-links": true,
 | 
			
		||||
    "default": true,
 | 
			
		||||
    "MD033": false
 | 
			
		||||
  },
 | 
			
		||||
  "globs": ["**/*.{md,mdx}"],
 | 
			
		||||
  "ignores": ["**/node_modules", "**/test/fixtures"],
 | 
			
		||||
  "customRules": ["./src/index.js"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								.markdownlint-cli2.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								.markdownlint-cli2.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
import relativeLinksRule from "./src/index.js"
 | 
			
		||||
 | 
			
		||||
const config = {
 | 
			
		||||
  config: {
 | 
			
		||||
    extends: "markdownlint/style/prettier",
 | 
			
		||||
    default: true,
 | 
			
		||||
    "relative-links": {
 | 
			
		||||
      root_path: ".",
 | 
			
		||||
    },
 | 
			
		||||
    "no-inline-html": false,
 | 
			
		||||
  },
 | 
			
		||||
  globs: ["**/*.md"],
 | 
			
		||||
  ignores: ["**/node_modules", "**/test/fixtures/**"],
 | 
			
		||||
  customRules: [relativeLinksRule],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default config
 | 
			
		||||
							
								
								
									
										4
									
								
								.npmrc
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								.npmrc
									
									
									
									
									
								
							@@ -1,2 +1,2 @@
 | 
			
		||||
save-exact=true
 | 
			
		||||
provenance=true
 | 
			
		||||
save-exact = true
 | 
			
		||||
provenance = true
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
  "branches": ["master"],
 | 
			
		||||
  "branches": ["main", { "name": "beta", "prerelease": true }],
 | 
			
		||||
  "plugins": [
 | 
			
		||||
    [
 | 
			
		||||
      "@semantic-release/commit-analyzer",
 | 
			
		||||
      {
 | 
			
		||||
        "preset": "conventionalcommits"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
      "@semantic-release/release-notes-generator",
 | 
			
		||||
      {
 | 
			
		||||
        "preset": "conventionalcommits"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "@semantic-release/commit-analyzer",
 | 
			
		||||
    "@semantic-release/release-notes-generator",
 | 
			
		||||
    "@semantic-release/npm",
 | 
			
		||||
    "@semantic-release/github"
 | 
			
		||||
  ]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							@@ -1,8 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "recommendations": [
 | 
			
		||||
    "editorconfig.editorconfig",
 | 
			
		||||
    "esbenp.prettier-vscode",
 | 
			
		||||
    "dbaeumer.vscode-eslint",
 | 
			
		||||
    "davidanson.vscode-markdownlint"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							@@ -1,8 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "editor.defaultFormatter": "esbenp.prettier-vscode",
 | 
			
		||||
  "prettier.configPath": ".prettierrc.json",
 | 
			
		||||
  "editor.formatOnSave": true,
 | 
			
		||||
  "editor.codeActionsOnSave": {
 | 
			
		||||
    "source.fixAll": "explicit"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,7 +4,7 @@ Thanks a lot for your interest in contributing to **markdownlint-rule-relative-l
 | 
			
		||||
 | 
			
		||||
## Code of Conduct
 | 
			
		||||
 | 
			
		||||
**markdownlint-rule-relative-links** adopted the [Contributor Covenant](https://www.contributor-covenant.org/) as its Code of Conduct, and we expect project participants to adhere to it. Please read [the full text](./CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated.
 | 
			
		||||
**markdownlint-rule-relative-links** adopted the [Contributor Covenant](https://www.contributor-covenant.org/) as its Code of Conduct, and we expect project participants to adhere to it. Please read [the full text](/CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated.
 | 
			
		||||
 | 
			
		||||
## Open Development
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
# MIT License
 | 
			
		||||
 | 
			
		||||
Copyright (c) Théo LUDWIG
 | 
			
		||||
Copyright (c) Théo LUDWIG <contact@theoludwig.fr>
 | 
			
		||||
 | 
			
		||||
Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
			
		||||
of this software and associated documentation files (the "Software"), to deal
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										66
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										66
									
								
								README.md
									
									
									
									
									
								
							@@ -10,7 +10,7 @@
 | 
			
		||||
  <a href="./CODE_OF_CONDUCT.md"><img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Contributor Covenant" /></a>
 | 
			
		||||
  <br />
 | 
			
		||||
  <a href="https://github.com/theoludwig/markdownlint-rule-relative-links/actions/workflows/lint.yml"><img src="https://github.com/theoludwig/markdownlint-rule-relative-links/actions/workflows/lint.yml/badge.svg?branch=develop" alt="Lint" /></a>
 | 
			
		||||
  <a href="https://github.com/theoludwig/markdownlint-rule-relative-linksactions/workflows/test.yml"><img src="https://github.com/theoludwig/markdownlint-rule-relative-links/actions/workflows/test.yml/badge.svg?branch=develop" alt="Test" /></a>
 | 
			
		||||
  <a href="https://github.com/theoludwig/markdownlint-rule-relative-links/actions/workflows/test.yml"><img src="https://github.com/theoludwig/markdownlint-rule-relative-links/actions/workflows/test.yml/badge.svg?branch=develop" alt="Test" /></a>
 | 
			
		||||
  <br />
 | 
			
		||||
  <a href="https://conventionalcommits.org"><img src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg" alt="Conventional Commits" /></a>
 | 
			
		||||
  <a href="https://github.com/semantic-release/semantic-release"><img src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg" alt="semantic-release" /></a>
 | 
			
		||||
@@ -43,9 +43,22 @@ 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
 | 
			
		||||
 | 
			
		||||
- Support images (e.g: ``).
 | 
			
		||||
- Support links fragments similar to the [built-in `markdownlint` rule - MD051](https://github.com/DavidAnson/markdownlint/blob/main/doc/md051.md) (e.g: `[Link](./awesome.md#heading)`).
 | 
			
		||||
- Ignore external links and absolute paths as it only checks relative links (e.g: `https://example.com/` or `/absolute/path.png`).
 | 
			
		||||
- If necessary, absolute paths can be validated too, with [`root_path` configuration option](#absolute-paths).
 | 
			
		||||
 | 
			
		||||
### Limitations
 | 
			
		||||
 | 
			
		||||
- Only images and links defined using markdown syntax are validated, html syntax is ignored (e.g: `<a href="./link.txt" />` or `<img src="./image.png" />`).
 | 
			
		||||
 | 
			
		||||
Contributions are welcome to improve the rule, and to alleviate these limitations. See [CONTRIBUTING.md](/CONTRIBUTING.md) for more information.
 | 
			
		||||
 | 
			
		||||
### Related links
 | 
			
		||||
 | 
			
		||||
- [DavidAnson/markdownlint#253](https://github.com/DavidAnson/markdownlint/issues/253)
 | 
			
		||||
@@ -54,7 +67,7 @@ awesome.md:3 relative-links Relative links should be valid [Link "./invalid.txt"
 | 
			
		||||
 | 
			
		||||
## Prerequisites
 | 
			
		||||
 | 
			
		||||
- [Node.js](https://nodejs.org/) >= 16.0.0
 | 
			
		||||
[Node.js](https://nodejs.org/) >= 22.0.0
 | 
			
		||||
 | 
			
		||||
## Installation
 | 
			
		||||
 | 
			
		||||
@@ -68,18 +81,22 @@ There are various ways [markdownlint](https://github.com/DavidAnson/markdownlint
 | 
			
		||||
 | 
			
		||||
We recommend configuring [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2) over [markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli) for compatibility with the [vscode-markdownlint](https://github.com/DavidAnson/vscode-markdownlint) extension.
 | 
			
		||||
 | 
			
		||||
`.markdownlint-cli2.jsonc`
 | 
			
		||||
`.markdownlint-cli2.mjs`
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
  "config": {
 | 
			
		||||
    "default": true,
 | 
			
		||||
    "relative-links": true
 | 
			
		||||
```js
 | 
			
		||||
import relativeLinksRule from "markdownlint-rule-relative-links"
 | 
			
		||||
 | 
			
		||||
const config = {
 | 
			
		||||
  config: {
 | 
			
		||||
    default: true,
 | 
			
		||||
    "relative-links": true,
 | 
			
		||||
  },
 | 
			
		||||
  "globs": ["**/*.{md,mdx}"],
 | 
			
		||||
  "ignores": ["**/node_modules"],
 | 
			
		||||
  "customRules": ["markdownlint-rule-relative-links"]
 | 
			
		||||
  globs: ["**/*.md"],
 | 
			
		||||
  ignores: ["**/node_modules"],
 | 
			
		||||
  customRules: [relativeLinksRule],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default config
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
`package.json`
 | 
			
		||||
@@ -92,18 +109,37 @@ We recommend configuring [markdownlint-cli2](https://github.com/DavidAnson/markd
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Absolute paths
 | 
			
		||||
 | 
			
		||||
GitHub (and, likely, other similar platforms) resolves absolute paths in Markdown links relative to the repository root.
 | 
			
		||||
 | 
			
		||||
To validate such links, add `root_path` option to the configuration:
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
  config: {
 | 
			
		||||
    default: true,
 | 
			
		||||
    "relative-links": {
 | 
			
		||||
      root_path: ".",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
After this change, all absolute paths will be converted to relative paths, and will be resolved relative to the specified directory.
 | 
			
		||||
 | 
			
		||||
For example, if you run markdownlint from a subdirectory (if `package.json` is located in a subdirectory), you should set `root_path` to `".."`.
 | 
			
		||||
 | 
			
		||||
## Usage
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
npm run lint:markdown
 | 
			
		||||
node --run lint:markdown
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 💡 Contributing
 | 
			
		||||
 | 
			
		||||
Anyone can help to improve the project, submit a Feature Request, a bug report or even correct a simple spelling mistake.
 | 
			
		||||
 | 
			
		||||
The steps to contribute can be found in the [CONTRIBUTING.md](./CONTRIBUTING.md) file.
 | 
			
		||||
The steps to contribute can be found in the [CONTRIBUTING.md](/CONTRIBUTING.md) file.
 | 
			
		||||
 | 
			
		||||
## 📄 License
 | 
			
		||||
 | 
			
		||||
[MIT](./LICENSE)
 | 
			
		||||
[MIT](/LICENSE)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								eslint.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								eslint.config.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
import typescriptESLint from "typescript-eslint"
 | 
			
		||||
import configConventions from "eslint-config-conventions"
 | 
			
		||||
 | 
			
		||||
export default typescriptESLint.config(...configConventions, {
 | 
			
		||||
  files: ["**/*.ts", "**/*.tsx"],
 | 
			
		||||
  languageOptions: {
 | 
			
		||||
    parser: typescriptESLint.parser,
 | 
			
		||||
    parserOptions: {
 | 
			
		||||
      projectService: true,
 | 
			
		||||
      tsconfigRootDir: import.meta.dirname,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										10345
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10345
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										54
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										54
									
								
								package.json
									
									
									
									
									
								
							@@ -18,6 +18,8 @@
 | 
			
		||||
    "markdownlint-rule"
 | 
			
		||||
  ],
 | 
			
		||||
  "main": "src/index.js",
 | 
			
		||||
  "types": "src/index.d.ts",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "files": [
 | 
			
		||||
    "src"
 | 
			
		||||
  ],
 | 
			
		||||
@@ -26,43 +28,35 @@
 | 
			
		||||
    "provenance": true
 | 
			
		||||
  },
 | 
			
		||||
  "engines": {
 | 
			
		||||
    "node": ">=16.0.0",
 | 
			
		||||
    "npm": ">=9.0.0"
 | 
			
		||||
    "node": ">=22.0.0"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "lint:commit": "commitlint",
 | 
			
		||||
    "lint:editorconfig": "editorconfig-checker",
 | 
			
		||||
    "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:staged": "lint-staged",
 | 
			
		||||
    "test": "node --test ./test",
 | 
			
		||||
    "release": "semantic-release",
 | 
			
		||||
    "postinstall": "husky install",
 | 
			
		||||
    "prepublishOnly": "pinst --disable",
 | 
			
		||||
    "postpublish": "pinst --enable"
 | 
			
		||||
    "lint:eslint": "eslint . --max-warnings 0",
 | 
			
		||||
    "lint:prettier": "prettier . --check",
 | 
			
		||||
    "lint:typescript": "tsc --noEmit",
 | 
			
		||||
    "test": "node --test",
 | 
			
		||||
    "release": "semantic-release"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "markdown-it": "14.0.0"
 | 
			
		||||
    "markdown-it": "14.1.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@commitlint/cli": "18.4.3",
 | 
			
		||||
    "@commitlint/config-conventional": "18.4.3",
 | 
			
		||||
    "@types/node": "20.10.5",
 | 
			
		||||
    "editorconfig-checker": "5.1.2",
 | 
			
		||||
    "eslint": "8.56.0",
 | 
			
		||||
    "eslint-config-conventions": "13.1.0",
 | 
			
		||||
    "eslint-config-prettier": "9.1.0",
 | 
			
		||||
    "eslint-plugin-import": "2.29.1",
 | 
			
		||||
    "eslint-plugin-prettier": "5.1.2",
 | 
			
		||||
    "eslint-plugin-promise": "6.1.1",
 | 
			
		||||
    "eslint-plugin-unicorn": "50.0.1",
 | 
			
		||||
    "husky": "8.0.3",
 | 
			
		||||
    "lint-staged": "15.2.0",
 | 
			
		||||
    "markdownlint": "0.32.1",
 | 
			
		||||
    "markdownlint-cli2": "0.11.0",
 | 
			
		||||
    "pinst": "3.0.0",
 | 
			
		||||
    "prettier": "3.1.1",
 | 
			
		||||
    "semantic-release": "22.0.12"
 | 
			
		||||
    "@types/markdown-it": "14.1.2",
 | 
			
		||||
    "@types/node": "24.9.2",
 | 
			
		||||
    "editorconfig-checker": "6.1.1",
 | 
			
		||||
    "eslint": "9.39.0",
 | 
			
		||||
    "eslint-config-conventions": "21.1.0",
 | 
			
		||||
    "eslint-plugin-promise": "7.2.1",
 | 
			
		||||
    "eslint-plugin-unicorn": "62.0.0",
 | 
			
		||||
    "eslint-plugin-import-x": "4.16.1",
 | 
			
		||||
    "globals": "16.4.0",
 | 
			
		||||
    "markdownlint": "0.38.0",
 | 
			
		||||
    "markdownlint-cli2": "0.18.1",
 | 
			
		||||
    "prettier": "3.6.2",
 | 
			
		||||
    "semantic-release": "25.0.1",
 | 
			
		||||
    "typescript-eslint": "8.46.2",
 | 
			
		||||
    "typescript": "5.9.3"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								src/index.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/index.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
import type MarkdownIt from "markdown-it"
 | 
			
		||||
import type { Rule } from "markdownlint"
 | 
			
		||||
 | 
			
		||||
declare const relativeLinksRule: Rule
 | 
			
		||||
export default relativeLinksRule
 | 
			
		||||
 | 
			
		||||
declare const markdownIt: MarkdownIt
 | 
			
		||||
export { markdownIt }
 | 
			
		||||
							
								
								
									
										192
									
								
								src/index.js
									
									
									
									
									
								
							
							
						
						
									
										192
									
								
								src/index.js
									
									
									
									
									
								
							@@ -1,26 +1,37 @@
 | 
			
		||||
"use strict"
 | 
			
		||||
import { pathToFileURL } from "node:url"
 | 
			
		||||
import fs from "node:fs"
 | 
			
		||||
 | 
			
		||||
const { pathToFileURL } = require("node:url")
 | 
			
		||||
const fs = require("node:fs")
 | 
			
		||||
 | 
			
		||||
const {
 | 
			
		||||
  filterTokens,
 | 
			
		||||
  addError,
 | 
			
		||||
import { filterTokens } from "./markdownlint-rule-helpers/helpers.js"
 | 
			
		||||
import {
 | 
			
		||||
  convertHeadingToHTMLFragment,
 | 
			
		||||
  getMarkdownHeadings,
 | 
			
		||||
} = require("./utils.js")
 | 
			
		||||
  getMarkdownIdOrAnchorNameFragments,
 | 
			
		||||
  isValidIntegerString,
 | 
			
		||||
  getNumberOfLines,
 | 
			
		||||
  getLineNumberStringFromFragment,
 | 
			
		||||
  lineFragmentRe,
 | 
			
		||||
} from "./utils.js"
 | 
			
		||||
 | 
			
		||||
const customRule = {
 | 
			
		||||
export { markdownIt } from "./utils.js"
 | 
			
		||||
 | 
			
		||||
/** @typedef {import('markdownlint').Rule} MarkdownLintRule */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @type {MarkdownLintRule}
 | 
			
		||||
 */
 | 
			
		||||
const relativeLinksRule = {
 | 
			
		||||
  names: ["relative-links"],
 | 
			
		||||
  description: "Relative links should be valid",
 | 
			
		||||
  tags: ["links"],
 | 
			
		||||
  parser: "markdownit",
 | 
			
		||||
  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) {
 | 
			
		||||
@@ -40,52 +51,133 @@ const customRule = {
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (hrefSrc != null) {
 | 
			
		||||
          const url = new URL(hrefSrc, pathToFileURL(params.name))
 | 
			
		||||
          const isRelative =
 | 
			
		||||
            url.protocol === "file:" && !hrefSrc.startsWith("/")
 | 
			
		||||
          if (isRelative) {
 | 
			
		||||
            const detail = `Link "${hrefSrc}"`
 | 
			
		||||
        if (hrefSrc == null || hrefSrc.startsWith("#")) {
 | 
			
		||||
          continue
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
            if (!fs.existsSync(url)) {
 | 
			
		||||
              addError(
 | 
			
		||||
                onError,
 | 
			
		||||
        let url
 | 
			
		||||
 | 
			
		||||
        if (hrefSrc.startsWith("/")) {
 | 
			
		||||
          const rootPath = params.config["root_path"]
 | 
			
		||||
 | 
			
		||||
          if (!rootPath) {
 | 
			
		||||
            continue
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          url = new URL(`.${hrefSrc}`, pathToFileURL(`${rootPath}/`))
 | 
			
		||||
        } else {
 | 
			
		||||
          url = new URL(hrefSrc, pathToFileURL(params.name))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (url.protocol !== "file:") {
 | 
			
		||||
          continue
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const detail = `"${hrefSrc}"`
 | 
			
		||||
 | 
			
		||||
        if (!fs.existsSync(url)) {
 | 
			
		||||
          onError({
 | 
			
		||||
            lineNumber,
 | 
			
		||||
            detail: `${detail} should exist in the file system`,
 | 
			
		||||
          })
 | 
			
		||||
          continue
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (url.hash.length <= 0) {
 | 
			
		||||
          if (hrefSrc.includes("#")) {
 | 
			
		||||
            if (type === "image") {
 | 
			
		||||
              onError({
 | 
			
		||||
                lineNumber,
 | 
			
		||||
                `${detail} should exist in the file system`,
 | 
			
		||||
              )
 | 
			
		||||
                detail: `${detail} should not have a fragment identifier as it is an image`,
 | 
			
		||||
              })
 | 
			
		||||
              continue
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (type === "link_open" && url.hash !== "") {
 | 
			
		||||
              const fileContent = fs.readFileSync(url, { encoding: "utf8" })
 | 
			
		||||
              const headings = getMarkdownHeadings(fileContent)
 | 
			
		||||
 | 
			
		||||
              /** @type {Map<string, number>} */
 | 
			
		||||
              const fragments = new Map()
 | 
			
		||||
 | 
			
		||||
              const headingsHTMLFragments = headings.map((heading) => {
 | 
			
		||||
                const fragment = convertHeadingToHTMLFragment(heading)
 | 
			
		||||
                const count = fragments.get(fragment) ?? 0
 | 
			
		||||
                fragments.set(fragment, count + 1)
 | 
			
		||||
                if (count !== 0) {
 | 
			
		||||
                  return `${fragment}-${count}`
 | 
			
		||||
                }
 | 
			
		||||
                return fragment
 | 
			
		||||
              })
 | 
			
		||||
 | 
			
		||||
              if (!headingsHTMLFragments.includes(url.hash)) {
 | 
			
		||||
                addError(
 | 
			
		||||
                  onError,
 | 
			
		||||
                  lineNumber,
 | 
			
		||||
                  `${detail} should have a valid fragment`,
 | 
			
		||||
                )
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            onError({
 | 
			
		||||
              lineNumber,
 | 
			
		||||
              detail: `${detail} should have a valid fragment identifier`,
 | 
			
		||||
            })
 | 
			
		||||
            continue
 | 
			
		||||
          }
 | 
			
		||||
          continue
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (type === "image") {
 | 
			
		||||
          onError({
 | 
			
		||||
            lineNumber,
 | 
			
		||||
            detail: `${detail} should not have a fragment identifier as it is an image`,
 | 
			
		||||
          })
 | 
			
		||||
          continue
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!url.pathname.endsWith(".md")) {
 | 
			
		||||
          continue
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const fileContent = fs.readFileSync(url, { encoding: "utf8" })
 | 
			
		||||
        const headings = getMarkdownHeadings(fileContent)
 | 
			
		||||
        const idOrAnchorNameHTMLFragments =
 | 
			
		||||
          getMarkdownIdOrAnchorNameFragments(fileContent)
 | 
			
		||||
 | 
			
		||||
        /** @type {Map<string, number>} */
 | 
			
		||||
        const fragments = new Map()
 | 
			
		||||
 | 
			
		||||
        const fragmentsHTML = headings.map((heading) => {
 | 
			
		||||
          const fragment = convertHeadingToHTMLFragment(heading)
 | 
			
		||||
          const count = fragments.get(fragment) ?? 0
 | 
			
		||||
          fragments.set(fragment, count + 1)
 | 
			
		||||
          if (count !== 0) {
 | 
			
		||||
            return `${fragment}-${count}`
 | 
			
		||||
          }
 | 
			
		||||
          return fragment
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        fragmentsHTML.push(...idOrAnchorNameHTMLFragments)
 | 
			
		||||
 | 
			
		||||
        if (!fragmentsHTML.includes(url.hash)) {
 | 
			
		||||
          if (url.hash.startsWith("#L")) {
 | 
			
		||||
            const lineNumberFragmentString = getLineNumberStringFromFragment(
 | 
			
		||||
              url.hash,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            const hasOnlyDigits = isValidIntegerString(lineNumberFragmentString)
 | 
			
		||||
            if (!hasOnlyDigits) {
 | 
			
		||||
              if (lineFragmentRe.test(url.hash)) {
 | 
			
		||||
                continue
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              onError({
 | 
			
		||||
                lineNumber,
 | 
			
		||||
                detail: `${detail} should have a valid fragment identifier`,
 | 
			
		||||
              })
 | 
			
		||||
              continue
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const lineNumberFragment = Number.parseInt(
 | 
			
		||||
              lineNumberFragmentString,
 | 
			
		||||
              10,
 | 
			
		||||
            )
 | 
			
		||||
            const numberOfLines = getNumberOfLines(fileContent)
 | 
			
		||||
            if (lineNumberFragment > numberOfLines) {
 | 
			
		||||
              onError({
 | 
			
		||||
                lineNumber,
 | 
			
		||||
                detail: `${detail} should have a valid fragment identifier, ${detail} should have at least ${lineNumberFragment} lines to be valid`,
 | 
			
		||||
              })
 | 
			
		||||
              continue
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            continue
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          onError({
 | 
			
		||||
            lineNumber,
 | 
			
		||||
            detail: `${detail} should have a valid fragment identifier`,
 | 
			
		||||
          })
 | 
			
		||||
          continue
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = customRule
 | 
			
		||||
export default relativeLinksRule
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										33
									
								
								src/markdownlint-rule-helpers/helpers.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/markdownlint-rule-helpers/helpers.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Dependency Vendoring of `markdownlint-rule-helpers`
 | 
			
		||||
 * @see https://www.npmjs.com/package/markdownlint-rule-helpers
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/** @typedef {import('markdownlint').RuleParams} MarkdownLintRuleParams */
 | 
			
		||||
/** @typedef {import('markdownlint').MarkdownItToken} MarkdownItToken */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Calls the provided function for each matching token.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {MarkdownLintRuleParams} params RuleParams instance.
 | 
			
		||||
 * @param {string} type Token type identifier.
 | 
			
		||||
 * @param {(token: MarkdownItToken) => void} handler Callback function.
 | 
			
		||||
 * @returns {void}
 | 
			
		||||
 */
 | 
			
		||||
export const filterTokens = (params, type, handler) => {
 | 
			
		||||
  for (const token of params.parsers.markdownit.tokens) {
 | 
			
		||||
    if (token.type === type) {
 | 
			
		||||
      handler(token)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Gets a Regular Expression for matching the specified HTML attribute.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {string} name HTML attribute name.
 | 
			
		||||
 * @returns {RegExp} Regular Expression for matching.
 | 
			
		||||
 */
 | 
			
		||||
export const getHtmlAttributeRe = (name) => {
 | 
			
		||||
  return new RegExp(`\\s${name}\\s*=\\s*['"]?([^'"\\s>]*)`, "iu")
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										125
									
								
								src/utils.js
									
									
									
									
									
								
							
							
						
						
									
										125
									
								
								src/utils.js
									
									
									
									
									
								
							@@ -1,41 +1,10 @@
 | 
			
		||||
const MarkdownIt = require("markdown-it")
 | 
			
		||||
import MarkdownIt from "markdown-it"
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Calls the provided function for each matching token.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {object} params RuleParams instance.
 | 
			
		||||
 * @param {string} type Token type identifier.
 | 
			
		||||
 * @param {Function} handler Callback function.
 | 
			
		||||
 * @returns {void}
 | 
			
		||||
 */
 | 
			
		||||
const filterTokens = (params, type, handler) => {
 | 
			
		||||
  for (const token of params.tokens) {
 | 
			
		||||
    if (token.type === type) {
 | 
			
		||||
      handler(token)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
import { getHtmlAttributeRe } from "./markdownlint-rule-helpers/helpers.js"
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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,
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
export const markdownIt = new MarkdownIt({ html: true })
 | 
			
		||||
 | 
			
		||||
export const lineFragmentRe = /^#(?:L\d+(?:C\d+)?-L\d+(?:C\d+)?|L\d+)$/
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Converts a Markdown heading into an HTML fragment according to the rules
 | 
			
		||||
@@ -45,7 +14,7 @@ const addError = (onError, lineNumber, detail, context, range, fixInfo) => {
 | 
			
		||||
 * @param {string} inlineText Inline token for heading.
 | 
			
		||||
 * @returns {string} Fragment string for heading.
 | 
			
		||||
 */
 | 
			
		||||
const convertHeadingToHTMLFragment = (inlineText) => {
 | 
			
		||||
export const convertHeadingToHTMLFragment = (inlineText) => {
 | 
			
		||||
  return (
 | 
			
		||||
    "#" +
 | 
			
		||||
    encodeURIComponent(
 | 
			
		||||
@@ -71,8 +40,7 @@ const ignoredTokens = new Set(["heading_open", "heading_close"])
 | 
			
		||||
 * @param {string} content
 | 
			
		||||
 * @returns {string[]}
 | 
			
		||||
 */
 | 
			
		||||
const getMarkdownHeadings = (content) => {
 | 
			
		||||
  const markdownIt = new MarkdownIt({ html: true })
 | 
			
		||||
export const getMarkdownHeadings = (content) => {
 | 
			
		||||
  const tokens = markdownIt.parse(content, {})
 | 
			
		||||
 | 
			
		||||
  /** @type {string[]} */
 | 
			
		||||
@@ -98,8 +66,10 @@ const getMarkdownHeadings = (content) => {
 | 
			
		||||
      continue
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const children = token.children ?? []
 | 
			
		||||
 | 
			
		||||
    headings.push(
 | 
			
		||||
      `${token.children
 | 
			
		||||
      `${children
 | 
			
		||||
        .map((token) => {
 | 
			
		||||
          return token.content
 | 
			
		||||
        })
 | 
			
		||||
@@ -110,9 +80,74 @@ const getMarkdownHeadings = (content) => {
 | 
			
		||||
  return headings
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  filterTokens,
 | 
			
		||||
  addError,
 | 
			
		||||
  convertHeadingToHTMLFragment,
 | 
			
		||||
  getMarkdownHeadings,
 | 
			
		||||
const nameHTMLAttributeRegex = getHtmlAttributeRe("name")
 | 
			
		||||
const idHTMLAttributeRegex = getHtmlAttributeRe("id")
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Gets the id or anchor name fragments from a Markdown string.
 | 
			
		||||
 * @param {string} content
 | 
			
		||||
 * @returns {string[]}
 | 
			
		||||
 */
 | 
			
		||||
export const getMarkdownIdOrAnchorNameFragments = (content) => {
 | 
			
		||||
  const tokens = markdownIt.parse(content, {})
 | 
			
		||||
 | 
			
		||||
  /** @type {string[]} */
 | 
			
		||||
  const result = []
 | 
			
		||||
 | 
			
		||||
  for (const token of tokens) {
 | 
			
		||||
    const regexMatch =
 | 
			
		||||
      idHTMLAttributeRegex.exec(token.content) ||
 | 
			
		||||
      nameHTMLAttributeRegex.exec(token.content)
 | 
			
		||||
    if (regexMatch == null) {
 | 
			
		||||
      continue
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const idOrName = regexMatch[1]
 | 
			
		||||
    if (idOrName == null || idOrName.length <= 0) {
 | 
			
		||||
      continue
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const htmlFragment = "#" + idOrName
 | 
			
		||||
    if (!result.includes(htmlFragment)) {
 | 
			
		||||
      result.push(htmlFragment)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return result
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Checks if a string is a valid integer.
 | 
			
		||||
 *
 | 
			
		||||
 * Using `Number.parseInt` combined with `Number.isNaN` will not be sufficient enough because `Number.parseInt("1abc", 10)` will return `1` (a valid number) instead of `NaN`.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {string} value
 | 
			
		||||
 * @returns {boolean}
 | 
			
		||||
 * @example isValidIntegerString("1") // true
 | 
			
		||||
 * @example isValidIntegerString("45") // true
 | 
			
		||||
 * @example isValidIntegerString("1abc") // false
 | 
			
		||||
 * @example isValidIntegerString("1.0") // false
 | 
			
		||||
 */
 | 
			
		||||
export const isValidIntegerString = (value) => {
 | 
			
		||||
  const regex = /^\d+$/
 | 
			
		||||
  return regex.test(value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Gets the number of lines in a string, based on the number of `\n` characters.
 | 
			
		||||
 * @param {string} content
 | 
			
		||||
 * @returns {number}
 | 
			
		||||
 */
 | 
			
		||||
export const getNumberOfLines = (content) => {
 | 
			
		||||
  return content.split("\n").length
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Gets the line number string from a fragment.
 | 
			
		||||
 * @param {string} fragment
 | 
			
		||||
 * @returns {string}
 | 
			
		||||
 * @example getLineNumberStringFromFragment("#L50") // 50
 | 
			
		||||
 */
 | 
			
		||||
export const getLineNumberStringFromFragment = (fragment) => {
 | 
			
		||||
  return fragment.slice(2)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,46 +0,0 @@
 | 
			
		||||
const test = require("node:test")
 | 
			
		||||
const assert = require("node:assert/strict")
 | 
			
		||||
 | 
			
		||||
const { markdownlint } = require("markdownlint").promises
 | 
			
		||||
 | 
			
		||||
const relativeLinks = require("../src/index.js")
 | 
			
		||||
 | 
			
		||||
test("ensure the rule validate correctly", async () => {
 | 
			
		||||
  const lintResults = await markdownlint({
 | 
			
		||||
    files: ["test/fixtures/Valid.md", "test/fixtures/Invalid.md"],
 | 
			
		||||
    config: {
 | 
			
		||||
      default: false,
 | 
			
		||||
      "relative-links": true,
 | 
			
		||||
    },
 | 
			
		||||
    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/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',
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  assert.equal(
 | 
			
		||||
    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',
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  assert.equal(
 | 
			
		||||
    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',
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										19
									
								
								test/fixtures/Invalid.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								test/fixtures/Invalid.md
									
									
									
									
										vendored
									
									
								
							@@ -1,19 +0,0 @@
 | 
			
		||||
# Invalid
 | 
			
		||||
 | 
			
		||||
[basic.js](./basic.test.js)
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
[Link fragment](./Valid.md#not-existing-heading)
 | 
			
		||||
 | 
			
		||||
## Existing Heading
 | 
			
		||||
 | 
			
		||||
### Repeated Heading
 | 
			
		||||
 | 
			
		||||
Text
 | 
			
		||||
 | 
			
		||||
### Repeated Heading
 | 
			
		||||
 | 
			
		||||
Text
 | 
			
		||||
 | 
			
		||||
### Repeated Heading
 | 
			
		||||
							
								
								
									
										21
									
								
								test/fixtures/Valid.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								test/fixtures/Valid.md
									
									
									
									
										vendored
									
									
								
							@@ -1,21 +0,0 @@
 | 
			
		||||
# Valid
 | 
			
		||||
 | 
			
		||||
[basic.js](../basic.test.js)
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
[External https link](https://example.com/)
 | 
			
		||||
 | 
			
		||||
[External https link 2](https:./external.https)
 | 
			
		||||
 | 
			
		||||
[External ftp link](ftp:./external.ftp)
 | 
			
		||||
 | 
			
		||||
[Link fragment](./Invalid.md#existing-heading)
 | 
			
		||||
 | 
			
		||||
[Link fragment Repeated 0](./Invalid.md#repeated-heading)
 | 
			
		||||
 | 
			
		||||
[Link fragment Repeated 1](./Invalid.md#repeated-heading-1)
 | 
			
		||||
 | 
			
		||||
[Link fragment Repeated 2](./Invalid.md#repeated-heading-2)
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/config-dependent/absolute-paths.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/config-dependent/absolute-paths.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Valid
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/invalid/empty-id-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/invalid/empty-id-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Awesome
 | 
			
		||||
 | 
			
		||||
<div id>Content</div>
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/invalid/empty-id-fragment/empty-id-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/invalid/empty-id-fragment/empty-id-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Invalid
 | 
			
		||||
 | 
			
		||||
[Link fragment](./awesome.md#)
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/invalid/ignore-empty-fragment-checking-for-image.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/invalid/ignore-empty-fragment-checking-for-image.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Invalid
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/invalid/ignore-fragment-checking-for-image.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/invalid/ignore-fragment-checking-for-image.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Invalid
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/invalid/ignore-name-fragment-if-not-an-anchor/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/invalid/ignore-name-fragment-if-not-an-anchor/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Awesome
 | 
			
		||||
 | 
			
		||||
<input name="name-should-be-ignored" id="id-after-name" />
 | 
			
		||||
@@ -0,0 +1,3 @@
 | 
			
		||||
# Invalid
 | 
			
		||||
 | 
			
		||||
[Invalid](./awesome.md#name-should-be-ignored)
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/invalid/ignore-not-an-id-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/invalid/ignore-not-an-id-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Awesome
 | 
			
		||||
 | 
			
		||||
<div data-id="not-an-id-should-be-ignored">Content</div>
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/invalid/ignore-not-an-id-fragment/ignore-not-an-id-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/invalid/ignore-not-an-id-fragment/ignore-not-an-id-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Invalid
 | 
			
		||||
 | 
			
		||||
[Invalid](./awesome.md#not-an-id-should-be-ignored)
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/invalid/invalid-heading-case-sensitive/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/invalid/invalid-heading-case-sensitive/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Awesome
 | 
			
		||||
 | 
			
		||||
## Existing Heading
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/invalid/invalid-heading-case-sensitive/invalid-heading-case-sensitive.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/invalid/invalid-heading-case-sensitive/invalid-heading-case-sensitive.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Valid
 | 
			
		||||
 | 
			
		||||
[Link fragment](./awesome.md#ExistIng-Heading)
 | 
			
		||||
							
								
								
									
										1
									
								
								test/fixtures/invalid/invalid-heading-with-L-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								test/fixtures/invalid/invalid-heading-with-L-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
# Awesome
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/invalid/invalid-heading-with-L-fragment/invalid-heading-with-L-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/invalid/invalid-heading-with-L-fragment/invalid-heading-with-L-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Invalid
 | 
			
		||||
 | 
			
		||||
[Link fragment line number 7](./awesome.md#L7abc)
 | 
			
		||||
							
								
								
									
										1
									
								
								test/fixtures/invalid/invalid-line-column-range-number-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								test/fixtures/invalid/invalid-line-column-range-number-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
# Awesome
 | 
			
		||||
@@ -0,0 +1,31 @@
 | 
			
		||||
# Invalid
 | 
			
		||||
 | 
			
		||||
[Invalid](./awesome.md#L12-not-a-line-link)
 | 
			
		||||
 | 
			
		||||
[Invalid](./awesome.md#l7)
 | 
			
		||||
 | 
			
		||||
[Invalid](./awesome.md#L)
 | 
			
		||||
 | 
			
		||||
[Invalid](./awesome.md#L7extra)
 | 
			
		||||
 | 
			
		||||
[Invalid](./awesome.md#L30C)
 | 
			
		||||
 | 
			
		||||
[Invalid](./awesome.md#L30Cextra)
 | 
			
		||||
 | 
			
		||||
[Invalid](./awesome.md#L30L12)
 | 
			
		||||
 | 
			
		||||
[Invalid](./awesome.md#L30C12)
 | 
			
		||||
 | 
			
		||||
[Invalid](./awesome.md#L30C11-)
 | 
			
		||||
 | 
			
		||||
[Invalid](./awesome.md#L30C11-L)
 | 
			
		||||
 | 
			
		||||
[Invalid](./awesome.md#L30C11-L31C)
 | 
			
		||||
 | 
			
		||||
[Invalid](./awesome.md#L30C11-C31)
 | 
			
		||||
 | 
			
		||||
[Invalid](./awesome.md#C30)
 | 
			
		||||
 | 
			
		||||
[Invalid](./awesome.md#C11-C31)
 | 
			
		||||
 | 
			
		||||
[Invalid](./awesome.md#C11-L4C31)
 | 
			
		||||
							
								
								
									
										1
									
								
								test/fixtures/invalid/invalid-line-number-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								test/fixtures/invalid/invalid-line-number-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
# Awesome
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/invalid/invalid-line-number-fragment/invalid-line-number-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/invalid/invalid-line-number-fragment/invalid-line-number-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Invalid
 | 
			
		||||
 | 
			
		||||
[Link fragment line number 7](./awesome.md#L7)
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/invalid/non-existing-anchor-name-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/invalid/non-existing-anchor-name-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Awesome
 | 
			
		||||
 | 
			
		||||
<a name="existing-heading-anchor">Link</a>
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/invalid/non-existing-anchor-name-fragment/non-existing-anchor-name-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/invalid/non-existing-anchor-name-fragment/non-existing-anchor-name-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Invalid
 | 
			
		||||
 | 
			
		||||
[Link fragment](./awesome.md#non-existing-anchor-name-fragment)
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/invalid/non-existing-element-id-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/invalid/non-existing-element-id-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Awesome
 | 
			
		||||
 | 
			
		||||
<div id="existing-element-id-fragment">Content</div>
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/invalid/non-existing-element-id-fragment/non-existing-element-id-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/invalid/non-existing-element-id-fragment/non-existing-element-id-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Invalid
 | 
			
		||||
 | 
			
		||||
[Link fragment](./awesome.md#non-existing-element-id-fragment)
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/invalid/non-existing-file.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/invalid/non-existing-file.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Invalid
 | 
			
		||||
 | 
			
		||||
[File](./index.test.js)
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/invalid/non-existing-heading-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/invalid/non-existing-heading-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Awesome
 | 
			
		||||
 | 
			
		||||
## Existing Heading
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/invalid/non-existing-heading-fragment/non-existing-heading-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/invalid/non-existing-heading-fragment/non-existing-heading-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Invalid
 | 
			
		||||
 | 
			
		||||
[Link fragment](./awesome.md#non-existing-heading)
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/invalid/non-existing-image.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/invalid/non-existing-image.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Invalid
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/valid/existing-anchor-name-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/valid/existing-anchor-name-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Awesome
 | 
			
		||||
 | 
			
		||||
<a name="existing-heading-anchor">Link</a>
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/valid/existing-anchor-name-fragment/existing-anchor-name-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/valid/existing-anchor-name-fragment/existing-anchor-name-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Invalid
 | 
			
		||||
 | 
			
		||||
[Link fragment](./awesome.md#existing-heading-anchor)
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/valid/existing-element-id-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/valid/existing-element-id-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Awesome
 | 
			
		||||
 | 
			
		||||
<div id="existing-element-id-fragment">Content</div>
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/valid/existing-element-id-fragment/existing-element-id-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/valid/existing-element-id-fragment/existing-element-id-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Invalid
 | 
			
		||||
 | 
			
		||||
[Link fragment](./awesome.md#existing-element-id-fragment)
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/valid/existing-file.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/valid/existing-file.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Valid
 | 
			
		||||
 | 
			
		||||
[File](../../index.test.js)
 | 
			
		||||
							
								
								
									
										13
									
								
								test/fixtures/valid/existing-heading-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								test/fixtures/valid/existing-heading-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
# Awesome
 | 
			
		||||
 | 
			
		||||
## Existing Heading
 | 
			
		||||
 | 
			
		||||
### Repeated Heading
 | 
			
		||||
 | 
			
		||||
Text
 | 
			
		||||
 | 
			
		||||
### Repeated Heading
 | 
			
		||||
 | 
			
		||||
Text
 | 
			
		||||
 | 
			
		||||
### Repeated Heading
 | 
			
		||||
							
								
								
									
										9
									
								
								test/fixtures/valid/existing-heading-fragment/existing-heading-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								test/fixtures/valid/existing-heading-fragment/existing-heading-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
# Valid
 | 
			
		||||
 | 
			
		||||
[Link fragment](./awesome.md#existing-heading)
 | 
			
		||||
 | 
			
		||||
[Link fragment Repeated 0](./awesome.md#repeated-heading)
 | 
			
		||||
 | 
			
		||||
[Link fragment Repeated 1](./awesome.md#repeated-heading-1)
 | 
			
		||||
 | 
			
		||||
[Link fragment Repeated 2](./awesome.md#repeated-heading-2)
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/valid/existing-heading-with-accents/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/valid/existing-heading-with-accents/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Awesome
 | 
			
		||||
 | 
			
		||||
## Développement
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/valid/existing-heading-with-accents/existing-heading-with-accents.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/valid/existing-heading-with-accents/existing-heading-with-accents.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Valid
 | 
			
		||||
 | 
			
		||||
[Link fragment](./awesome.md#développement)
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/valid/existing-image.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/valid/existing-image.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Valid
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/valid/ignore-absolute-paths.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/valid/ignore-absolute-paths.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Valid
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
							
								
								
									
										7
									
								
								test/fixtures/valid/ignore-external-links.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								test/fixtures/valid/ignore-external-links.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
# Valid
 | 
			
		||||
 | 
			
		||||
[External https link](https://example.com/)
 | 
			
		||||
 | 
			
		||||
[External https link 2](https:./external.https)
 | 
			
		||||
 | 
			
		||||
[External ftp link](ftp:./external.ftp)
 | 
			
		||||
							
								
								
									
										5
									
								
								test/fixtures/valid/ignore-fragment-checking-in-own-file.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								test/fixtures/valid/ignore-fragment-checking-in-own-file.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
# Valid
 | 
			
		||||
 | 
			
		||||
<div id="existing-element-id-fragment">Content</div>
 | 
			
		||||
 | 
			
		||||
[Link fragment](#non-existing-element-id-fragment)
 | 
			
		||||
							
								
								
									
										0
									
								
								test/fixtures/valid/only-parse-markdown-files-for-fragments/abc.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								test/fixtures/valid/only-parse-markdown-files-for-fragments/abc.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
								
								
									
										9
									
								
								test/fixtures/valid/only-parse-markdown-files-for-fragments/awesome.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								test/fixtures/valid/only-parse-markdown-files-for-fragments/awesome.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
<!doctype html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="UTF-8" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
    <title>Awesome</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body></body>
 | 
			
		||||
</html>
 | 
			
		||||
@@ -0,0 +1,7 @@
 | 
			
		||||
# Valid
 | 
			
		||||
 | 
			
		||||
[Link fragment HTML](./awesome.html#existing-heading)
 | 
			
		||||
 | 
			
		||||
[Link fragment TXT](./abc.txt#existing-heading)
 | 
			
		||||
 | 
			
		||||
[Link fragment Image](../../image.png#existing-heading)
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/valid/valid-heading-like-number-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/valid/valid-heading-like-number-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Awesome
 | 
			
		||||
 | 
			
		||||
## L7
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/valid/valid-heading-like-number-fragment/valid-heading-like-number-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/valid/valid-heading-like-number-fragment/valid-heading-like-number-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Valid
 | 
			
		||||
 | 
			
		||||
[Link fragment](./awesome.md#l7)
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/valid/valid-line-column-range-number-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/valid/valid-line-column-range-number-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Awesome
 | 
			
		||||
 | 
			
		||||
## L12 Not A Line Link
 | 
			
		||||
@@ -0,0 +1,11 @@
 | 
			
		||||
# Valid
 | 
			
		||||
 | 
			
		||||
[Valid](./awesome.md#l12-not-a-line-link)
 | 
			
		||||
 | 
			
		||||
[Valid](./awesome.md#L30-L31)
 | 
			
		||||
 | 
			
		||||
[Valid](./awesome.md#L3C24-L88)
 | 
			
		||||
 | 
			
		||||
[Valid](./awesome.md#L304-L314C98)
 | 
			
		||||
 | 
			
		||||
[Valid](./awesome.md#L200C4-L3244C2)
 | 
			
		||||
							
								
								
									
										9
									
								
								test/fixtures/valid/valid-line-number-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								test/fixtures/valid/valid-line-number-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
# Awesome
 | 
			
		||||
 | 
			
		||||
ABC
 | 
			
		||||
 | 
			
		||||
Line 5
 | 
			
		||||
 | 
			
		||||
Line 7 Text
 | 
			
		||||
 | 
			
		||||
## L7
 | 
			
		||||
							
								
								
									
										3
									
								
								test/fixtures/valid/valid-line-number-fragment/valid-line-number-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/valid/valid-line-number-fragment/valid-line-number-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# Valid
 | 
			
		||||
 | 
			
		||||
[Link fragment line number 7](./awesome.md#L7)
 | 
			
		||||
							
								
								
									
										281
									
								
								test/index.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										281
									
								
								test/index.test.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,281 @@
 | 
			
		||||
import { test } from "node:test"
 | 
			
		||||
import assert from "node:assert/strict"
 | 
			
		||||
 | 
			
		||||
import * as markdownlint from "markdownlint/promise"
 | 
			
		||||
 | 
			
		||||
import relativeLinksRule, { markdownIt } from "../src/index.js"
 | 
			
		||||
 | 
			
		||||
const defaultConfig = {
 | 
			
		||||
  "relative-links": true,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 *
 | 
			
		||||
 * @param {string} fixtureFile
 | 
			
		||||
 * @param {Object} config
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
const validateMarkdownLint = async (fixtureFile, config = defaultConfig) => {
 | 
			
		||||
  const lintResults = await markdownlint.lint({
 | 
			
		||||
    files: [fixtureFile],
 | 
			
		||||
    config: {
 | 
			
		||||
      default: false,
 | 
			
		||||
      ...config,
 | 
			
		||||
    },
 | 
			
		||||
    customRules: [relativeLinksRule],
 | 
			
		||||
    markdownItFactory: () => {
 | 
			
		||||
      return markdownIt
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
  return lintResults[fixtureFile]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
test("ensure the rule validates correctly", async (t) => {
 | 
			
		||||
  await t.test("should be invalid", async (t) => {
 | 
			
		||||
    const testCases = [
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be invalid with an empty id fragment",
 | 
			
		||||
        fixturePath:
 | 
			
		||||
          "test/fixtures/invalid/empty-id-fragment/empty-id-fragment.md",
 | 
			
		||||
        errors: ['"./awesome.md#" should have a valid fragment identifier'],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be invalid with a name fragment other than for an anchor",
 | 
			
		||||
        fixturePath:
 | 
			
		||||
          "test/fixtures/invalid/ignore-name-fragment-if-not-an-anchor/ignore-name-fragment-if-not-an-anchor.md",
 | 
			
		||||
        errors: [
 | 
			
		||||
          '"./awesome.md#name-should-be-ignored" should have a valid fragment identifier',
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be invalid with a non-existing id fragment (data-id !== id)",
 | 
			
		||||
        fixturePath:
 | 
			
		||||
          "test/fixtures/invalid/ignore-not-an-id-fragment/ignore-not-an-id-fragment.md",
 | 
			
		||||
        errors: [
 | 
			
		||||
          '"./awesome.md#not-an-id-should-be-ignored" should have a valid fragment identifier',
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be invalid with uppercase letters in fragment (case sensitive)",
 | 
			
		||||
        fixturePath:
 | 
			
		||||
          "test/fixtures/invalid/invalid-heading-case-sensitive/invalid-heading-case-sensitive.md",
 | 
			
		||||
        errors: [
 | 
			
		||||
          '"./awesome.md#ExistIng-Heading" should have a valid fragment identifier',
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be invalid with invalid heading with #L fragment",
 | 
			
		||||
        fixturePath:
 | 
			
		||||
          "test/fixtures/invalid/invalid-heading-with-L-fragment/invalid-heading-with-L-fragment.md",
 | 
			
		||||
        errors: [
 | 
			
		||||
          '"./awesome.md#L7abc" should have a valid fragment identifier',
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be invalid with a invalid line column range number fragment",
 | 
			
		||||
        fixturePath:
 | 
			
		||||
          "test/fixtures/invalid/invalid-line-column-range-number-fragment/invalid-line-column-range-number-fragment.md",
 | 
			
		||||
        errors: [
 | 
			
		||||
          '"./awesome.md#L12-not-a-line-link" should have a valid fragment identifier',
 | 
			
		||||
          '"./awesome.md#l7" should have a valid fragment identifier',
 | 
			
		||||
          '"./awesome.md#L" should have a valid fragment identifier',
 | 
			
		||||
          '"./awesome.md#L7extra" should have a valid fragment identifier',
 | 
			
		||||
          '"./awesome.md#L30C" should have a valid fragment identifier',
 | 
			
		||||
          '"./awesome.md#L30Cextra" should have a valid fragment identifier',
 | 
			
		||||
          '"./awesome.md#L30L12" should have a valid fragment identifier',
 | 
			
		||||
          '"./awesome.md#L30C12" should have a valid fragment identifier',
 | 
			
		||||
          '"./awesome.md#L30C11-" should have a valid fragment identifier',
 | 
			
		||||
          '"./awesome.md#L30C11-L" should have a valid fragment identifier',
 | 
			
		||||
          '"./awesome.md#L30C11-L31C" should have a valid fragment identifier',
 | 
			
		||||
          '"./awesome.md#L30C11-C31" should have a valid fragment identifier',
 | 
			
		||||
          '"./awesome.md#C30" should have a valid fragment identifier',
 | 
			
		||||
          '"./awesome.md#C11-C31" should have a valid fragment identifier',
 | 
			
		||||
          '"./awesome.md#C11-L4C31" should have a valid fragment identifier',
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be invalid with a invalid line number fragment",
 | 
			
		||||
        fixturePath:
 | 
			
		||||
          "test/fixtures/invalid/invalid-line-number-fragment/invalid-line-number-fragment.md",
 | 
			
		||||
        errors: [
 | 
			
		||||
          '"./awesome.md#L7" should have a valid fragment identifier, "./awesome.md#L7" should have at least 7 lines to be valid',
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be invalid with a non-existing anchor name fragment",
 | 
			
		||||
        fixturePath:
 | 
			
		||||
          "test/fixtures/invalid/non-existing-anchor-name-fragment/non-existing-anchor-name-fragment.md",
 | 
			
		||||
        errors: [
 | 
			
		||||
          '"./awesome.md#non-existing-anchor-name-fragment" should have a valid fragment identifier',
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be invalid with a non-existing element id fragment",
 | 
			
		||||
        fixturePath:
 | 
			
		||||
          "test/fixtures/invalid/non-existing-element-id-fragment/non-existing-element-id-fragment.md",
 | 
			
		||||
        errors: [
 | 
			
		||||
          '"./awesome.md#non-existing-element-id-fragment" should have a valid fragment identifier',
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be invalid with a non-existing heading fragment",
 | 
			
		||||
        fixturePath:
 | 
			
		||||
          "test/fixtures/invalid/non-existing-heading-fragment/non-existing-heading-fragment.md",
 | 
			
		||||
        errors: [
 | 
			
		||||
          '"./awesome.md#non-existing-heading" should have a valid fragment identifier',
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be invalid with a link to an image with a empty fragment",
 | 
			
		||||
        fixturePath:
 | 
			
		||||
          "test/fixtures/invalid/ignore-empty-fragment-checking-for-image.md",
 | 
			
		||||
        errors: [
 | 
			
		||||
          '"../image.png#" should not have a fragment identifier as it is an image',
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be invalid with a link to an image with a fragment",
 | 
			
		||||
        fixturePath:
 | 
			
		||||
          "test/fixtures/invalid/ignore-fragment-checking-for-image.md",
 | 
			
		||||
        errors: [
 | 
			
		||||
          '"../image.png#non-existing-fragment" should not have a fragment identifier as it is an image',
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be invalid with a non-existing file",
 | 
			
		||||
        fixturePath: "test/fixtures/invalid/non-existing-file.md",
 | 
			
		||||
        errors: ['"./index.test.js" should exist in the file system'],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be invalid with a non-existing image",
 | 
			
		||||
        fixturePath: "test/fixtures/invalid/non-existing-image.md",
 | 
			
		||||
        errors: ['"./image.png" should exist in the file system'],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be invalid with incorrect absolute paths",
 | 
			
		||||
        fixturePath: "test/fixtures/config-dependent/absolute-paths.md",
 | 
			
		||||
        errors: ['"/test/fixtures/image.png" should exist in the file system'],
 | 
			
		||||
        config: {
 | 
			
		||||
          "relative-links": {
 | 
			
		||||
            root_path: "test",
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    for (const {
 | 
			
		||||
      name,
 | 
			
		||||
      fixturePath,
 | 
			
		||||
      errors,
 | 
			
		||||
      config = defaultConfig,
 | 
			
		||||
    } of testCases) {
 | 
			
		||||
      await t.test(name, async () => {
 | 
			
		||||
        const lintResults =
 | 
			
		||||
          (await validateMarkdownLint(fixturePath, config)) ?? []
 | 
			
		||||
        const errorsDetails = lintResults.map((result) => {
 | 
			
		||||
          assert.deepEqual(result.ruleNames, relativeLinksRule.names)
 | 
			
		||||
          assert.deepEqual(
 | 
			
		||||
            result.ruleDescription,
 | 
			
		||||
            relativeLinksRule.description,
 | 
			
		||||
          )
 | 
			
		||||
          return result.errorDetail
 | 
			
		||||
        })
 | 
			
		||||
        assert.deepStrictEqual(
 | 
			
		||||
          errorsDetails,
 | 
			
		||||
          errors,
 | 
			
		||||
          `${fixturePath}: Expected errors`,
 | 
			
		||||
        )
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await t.test("should be valid", async (t) => {
 | 
			
		||||
    const testCases = [
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be valid with an existing anchor name fragment",
 | 
			
		||||
        fixturePath:
 | 
			
		||||
          "test/fixtures/valid/existing-anchor-name-fragment/existing-anchor-name-fragment.md",
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be valid with an existing element id fragment",
 | 
			
		||||
        fixturePath:
 | 
			
		||||
          "test/fixtures/valid/existing-element-id-fragment/existing-element-id-fragment.md",
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be valid with an existing heading fragment",
 | 
			
		||||
        fixturePath:
 | 
			
		||||
          "test/fixtures/valid/existing-heading-fragment/existing-heading-fragment.md",
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'should be valid with an existing heading fragment with accents (e.g: "é")',
 | 
			
		||||
        fixturePath:
 | 
			
		||||
          "test/fixtures/valid/existing-heading-with-accents/existing-heading-with-accents.md",
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should only parse markdown files for fragments checking",
 | 
			
		||||
        fixturePath:
 | 
			
		||||
          "test/fixtures/valid/only-parse-markdown-files-for-fragments/only-parse-markdown-files-for-fragments.md",
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should support lines and columns range numbers in link fragments",
 | 
			
		||||
        fixturePath:
 | 
			
		||||
          "test/fixtures/valid/valid-line-column-range-number-fragment/valid-line-column-range-number-fragment.md",
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'should be valid with valid heading "like" line number fragment',
 | 
			
		||||
        fixturePath:
 | 
			
		||||
          "test/fixtures/valid/valid-heading-like-number-fragment/valid-heading-like-number-fragment.md",
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be valid with valid line number fragment",
 | 
			
		||||
        fixturePath:
 | 
			
		||||
          "test/fixtures/valid/valid-line-number-fragment/valid-line-number-fragment.md",
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be valid with an existing file",
 | 
			
		||||
        fixturePath: "test/fixtures/valid/existing-file.md",
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be valid with an existing image",
 | 
			
		||||
        fixturePath: "test/fixtures/valid/existing-image.md",
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should ignore absolute paths if root_path is not set",
 | 
			
		||||
        fixturePath: "test/fixtures/valid/ignore-absolute-paths.md",
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should ignore external links",
 | 
			
		||||
        fixturePath: "test/fixtures/valid/ignore-external-links.md",
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should ignore checking fragment in own file",
 | 
			
		||||
        fixturePath:
 | 
			
		||||
          "test/fixtures/valid/ignore-fragment-checking-in-own-file.md",
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "should be valid with correct absolute paths if root_path is set",
 | 
			
		||||
        fixturePath: "test/fixtures/config-dependent/absolute-paths.md",
 | 
			
		||||
        config: {
 | 
			
		||||
          "relative-links": {
 | 
			
		||||
            root_path: ".",
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    for (const { name, fixturePath, config = defaultConfig } of testCases) {
 | 
			
		||||
      await t.test(name, async () => {
 | 
			
		||||
        const lintResults =
 | 
			
		||||
          (await validateMarkdownLint(fixturePath, config)) ?? []
 | 
			
		||||
        const errorsDetails = lintResults.map((result) => {
 | 
			
		||||
          return result.errorDetail
 | 
			
		||||
        })
 | 
			
		||||
        assert.equal(
 | 
			
		||||
          errorsDetails.length,
 | 
			
		||||
          0,
 | 
			
		||||
          `${fixturePath}: Expected no errors, got ${errorsDetails.join(", ")}`,
 | 
			
		||||
        )
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
@@ -1,10 +1,14 @@
 | 
			
		||||
const test = require("node:test")
 | 
			
		||||
const assert = require("node:assert/strict")
 | 
			
		||||
import { test } from "node:test"
 | 
			
		||||
import assert from "node:assert/strict"
 | 
			
		||||
 | 
			
		||||
const {
 | 
			
		||||
import {
 | 
			
		||||
  convertHeadingToHTMLFragment,
 | 
			
		||||
  getMarkdownHeadings,
 | 
			
		||||
} = require("../src/utils.js")
 | 
			
		||||
  getMarkdownIdOrAnchorNameFragments,
 | 
			
		||||
  isValidIntegerString,
 | 
			
		||||
  getNumberOfLines,
 | 
			
		||||
  getLineNumberStringFromFragment,
 | 
			
		||||
} from "../src/utils.js"
 | 
			
		||||
 | 
			
		||||
test("utils", async (t) => {
 | 
			
		||||
  await t.test("convertHeadingToHTMLFragment", async () => {
 | 
			
		||||
@@ -34,4 +38,42 @@ test("utils", async (t) => {
 | 
			
		||||
      ["Hello", "World", "Hello, world!"],
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await t.test("getMarkdownIdOrAnchorNameFragments", async () => {
 | 
			
		||||
    assert.deepStrictEqual(
 | 
			
		||||
      getMarkdownIdOrAnchorNameFragments(
 | 
			
		||||
        '<a name="anchorName" id="anchorId">Link</a>',
 | 
			
		||||
      ),
 | 
			
		||||
      ["#anchorId"],
 | 
			
		||||
    )
 | 
			
		||||
    assert.deepStrictEqual(
 | 
			
		||||
      getMarkdownIdOrAnchorNameFragments('<a name="anchorName">Link</a>'),
 | 
			
		||||
      ["#anchorName"],
 | 
			
		||||
    )
 | 
			
		||||
    assert.deepStrictEqual(
 | 
			
		||||
      getMarkdownIdOrAnchorNameFragments("<a>Link</a>"),
 | 
			
		||||
      [],
 | 
			
		||||
    )
 | 
			
		||||
    assert.deepStrictEqual(getMarkdownIdOrAnchorNameFragments("<a>"), [])
 | 
			
		||||
    assert.deepStrictEqual(getMarkdownIdOrAnchorNameFragments("<a id=>"), [])
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await t.test("isValidIntegerString", async () => {
 | 
			
		||||
    assert.strictEqual(isValidIntegerString("1"), true)
 | 
			
		||||
    assert.strictEqual(isValidIntegerString("45"), true)
 | 
			
		||||
    assert.strictEqual(isValidIntegerString("1abc"), false)
 | 
			
		||||
    assert.strictEqual(isValidIntegerString("1.0"), false)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await t.test("getNumberOfLines", async () => {
 | 
			
		||||
    assert.strictEqual(getNumberOfLines(""), 1)
 | 
			
		||||
    assert.strictEqual(getNumberOfLines("Hello"), 1)
 | 
			
		||||
    assert.strictEqual(getNumberOfLines("Hello\nWorld"), 2)
 | 
			
		||||
    assert.strictEqual(getNumberOfLines("Hello\nWorld\n"), 3)
 | 
			
		||||
    assert.strictEqual(getNumberOfLines("Hello\nWorld\n\n"), 4)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await t.test("getLineNumberStringFromFragment", async () => {
 | 
			
		||||
    assert.strictEqual(getLineNumberStringFromFragment("#L50"), "50")
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										25
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "lib": ["ESNext"],
 | 
			
		||||
    "target": "ESNext",
 | 
			
		||||
    "module": "NodeNext",
 | 
			
		||||
    "moduleResolution": "NodeNext",
 | 
			
		||||
    "checkJs": true,
 | 
			
		||||
    "allowJs": true,
 | 
			
		||||
    "noEmit": true,
 | 
			
		||||
    "rootDir": ".",
 | 
			
		||||
    "baseUrl": ".",
 | 
			
		||||
    "skipLibCheck": true,
 | 
			
		||||
    "strict": true,
 | 
			
		||||
    "allowUnusedLabels": false,
 | 
			
		||||
    "allowUnreachableCode": false,
 | 
			
		||||
    "noFallthroughCasesInSwitch": true,
 | 
			
		||||
    "noImplicitAny": true,
 | 
			
		||||
    "noImplicitOverride": true,
 | 
			
		||||
    "noImplicitReturns": true,
 | 
			
		||||
    "noUncheckedIndexedAccess": true,
 | 
			
		||||
    "noUnusedLocals": true,
 | 
			
		||||
    "noUnusedParameters": true,
 | 
			
		||||
    "forceConsistentCasingInFileNames": true
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user