diff --git a/.eslintrc.json b/.eslintrc.json
index 54dbab3..0059662 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,7 +1,11 @@
{
"extends": ["conventions", "prettier"],
- "plugins": ["prettier"],
+ "plugins": ["prettier", "import", "unicorn"],
+ "parserOptions": {
+ "project": "./tsconfig.json"
+ },
"rules": {
- "prettier/prettier": "error"
+ "prettier/prettier": "error",
+ "import/extensions": ["error", "always"]
}
}
diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc
index 3d46c8b..0b4e176 100644
--- a/.markdownlint-cli2.jsonc
+++ b/.markdownlint-cli2.jsonc
@@ -1,9 +1,9 @@
{
"config": {
"extends": "markdownlint/style/prettier",
- "relative-links": true,
"default": true,
- "MD033": false
+ "relative-links": true,
+ "no-inline-html": false
},
"globs": ["**/*.{md,mdx}"],
"ignores": ["**/node_modules", "**/test/fixtures/**"],
diff --git a/README.md b/README.md
index 0b76f4a..a0181d7 100644
--- a/README.md
+++ b/README.md
@@ -49,13 +49,12 @@ awesome.md:3 relative-links Relative links should be valid ["./invalid.txt" shou
### Additional features
- Support images (e.g: ``).
-- Support anchors (heading fragment links) (e.g: `[Link](./awesome.md#existing-heading)`).
+- 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`).
### Limitations
-- Only images and links defined using markdown syntax are validated, html syntax is not supported (e.g: `` or `
`).
-- Anchors checking is limited to headings, other elements are not supported (e.g: with a "id", `
`).
Contributions are welcome to improve the rule, and to alleviate these limitations. See [CONTRIBUTING.md](./CONTRIBUTING.md) for more information.
diff --git a/jsconfig.json b/jsconfig.json
index e997767..c82d356 100644
--- a/jsconfig.json
+++ b/jsconfig.json
@@ -9,6 +9,7 @@
"noEmit": true,
"rootDir": ".",
"baseUrl": ".",
+ "skipLibCheck": true,
"strict": true,
"allowUnusedLabels": false,
"allowUnreachableCode": false,
diff --git a/package-lock.json b/package-lock.json
index 2971363..5d152cd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,13 +16,13 @@
"@commitlint/cli": "18.4.4",
"@commitlint/config-conventional": "18.4.4",
"@types/markdown-it": "13.0.7",
- "@types/node": "20.10.8",
+ "@types/node": "20.11.0",
"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-prettier": "5.1.3",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-unicorn": "50.0.1",
"husky": "8.0.3",
@@ -557,13 +557,13 @@
}
},
"node_modules/@humanwhocodes/config-array": {
- "version": "0.11.13",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
- "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
+ "version": "0.11.14",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
+ "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
"dev": true,
"dependencies": {
- "@humanwhocodes/object-schema": "^2.0.1",
- "debug": "^4.1.1",
+ "@humanwhocodes/object-schema": "^2.0.2",
+ "debug": "^4.3.1",
"minimatch": "^3.0.5"
},
"engines": {
@@ -584,9 +584,9 @@
}
},
"node_modules/@humanwhocodes/object-schema": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
- "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
+ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
"dev": true
},
"node_modules/@nodelib/fs.scandir": {
@@ -1325,9 +1325,9 @@
"dev": true
},
"node_modules/@types/node": {
- "version": "20.10.8",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz",
- "integrity": "sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==",
+ "version": "20.11.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz",
+ "integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@@ -2422,9 +2422,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.4.625",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.625.tgz",
- "integrity": "sha512-DENMhh3MFgaPDoXWrVIqSPInQoLImywfCwrSmVl3cf9QHzoZSiutHwGaB/Ql3VkqcQV30rzgdM+BjKqBAJxo5Q==",
+ "version": "1.4.628",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.628.tgz",
+ "integrity": "sha512-2k7t5PHvLsufpP6Zwk0nof62yLOsCf032wZx7/q0mv8gwlXjhcxI3lz6f0jBr0GrnWKcm3burXzI3t5IrcdUxw==",
"dev": true
},
"node_modules/emoji-regex": {
@@ -2911,9 +2911,9 @@
}
},
"node_modules/eslint-plugin-prettier": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.2.tgz",
- "integrity": "sha512-dhlpWc9vOwohcWmClFcA+HjlvUpuyynYs0Rf+L/P6/0iQE6vlHW9l5bkfzN62/Stm9fbq8ku46qzde76T1xlSg==",
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz",
+ "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==",
"dev": true,
"dependencies": {
"prettier-linter-helpers": "^1.0.0",
@@ -5410,9 +5410,9 @@
}
},
"node_modules/npm": {
- "version": "10.2.5",
- "resolved": "https://registry.npmjs.org/npm/-/npm-10.2.5.tgz",
- "integrity": "sha512-lXdZ7titEN8CH5YJk9C/aYRU9JeDxQ4d8rwIIDsvH3SMjLjHTukB2CFstMiB30zXs4vCrPN2WH6cDq1yHBeJAw==",
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/npm/-/npm-10.3.0.tgz",
+ "integrity": "sha512-9u5GFc1UqI2DLlGI7QdjkpIaBs3UhTtY8KoCqYJK24gV/j/tByaI4BA4R7RkOc+ASqZMzFPKt4Pj2Z8JcGo//A==",
"bundleDependencies": [
"@isaacs/string-locale-compare",
"@npmcli/arborist",
@@ -5494,12 +5494,12 @@
"@npmcli/fs": "^3.1.0",
"@npmcli/map-workspaces": "^3.0.4",
"@npmcli/package-json": "^5.0.0",
- "@npmcli/promise-spawn": "^7.0.0",
- "@npmcli/run-script": "^7.0.2",
+ "@npmcli/promise-spawn": "^7.0.1",
+ "@npmcli/run-script": "^7.0.3",
"@sigstore/tuf": "^2.2.0",
"abbrev": "^2.0.0",
"archy": "~1.0.0",
- "cacache": "^18.0.1",
+ "cacache": "^18.0.2",
"chalk": "^5.3.0",
"ci-info": "^4.0.0",
"cli-columns": "^4.0.0",
@@ -5653,7 +5653,7 @@
}
},
"node_modules/npm/node_modules/@npmcli/arborist": {
- "version": "7.2.2",
+ "version": "7.3.0",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -5700,7 +5700,7 @@
}
},
"node_modules/npm/node_modules/@npmcli/config": {
- "version": "8.0.3",
+ "version": "8.1.0",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -5758,7 +5758,7 @@
}
},
"node_modules/npm/node_modules/@npmcli/git": {
- "version": "5.0.3",
+ "version": "5.0.4",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -5859,7 +5859,7 @@
}
},
"node_modules/npm/node_modules/@npmcli/promise-spawn": {
- "version": "7.0.0",
+ "version": "7.0.1",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -5883,7 +5883,7 @@
}
},
"node_modules/npm/node_modules/@npmcli/run-script": {
- "version": "7.0.2",
+ "version": "7.0.3",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -5987,18 +5987,6 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
- "node_modules/npm/node_modules/abort-controller": {
- "version": "3.0.0",
- "dev": true,
- "inBundle": true,
- "license": "MIT",
- "dependencies": {
- "event-target-shim": "^5.0.0"
- },
- "engines": {
- "node": ">=6.5"
- }
- },
"node_modules/npm/node_modules/agent-base": {
"version": "7.1.0",
"dev": true,
@@ -6061,14 +6049,10 @@
"license": "MIT"
},
"node_modules/npm/node_modules/are-we-there-yet": {
- "version": "4.0.1",
+ "version": "4.0.2",
"dev": true,
"inBundle": true,
"license": "ISC",
- "dependencies": {
- "delegates": "^1.0.0",
- "readable-stream": "^4.1.0"
- },
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
@@ -6079,26 +6063,6 @@
"inBundle": true,
"license": "MIT"
},
- "node_modules/npm/node_modules/base64-js": {
- "version": "1.5.1",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "inBundle": true,
- "license": "MIT"
- },
"node_modules/npm/node_modules/bin-links": {
"version": "4.0.3",
"dev": true,
@@ -6132,30 +6096,6 @@
"balanced-match": "^1.0.0"
}
},
- "node_modules/npm/node_modules/buffer": {
- "version": "6.0.3",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "inBundle": true,
- "license": "MIT",
- "dependencies": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.2.1"
- }
- },
"node_modules/npm/node_modules/builtins": {
"version": "5.0.1",
"dev": true,
@@ -6166,7 +6106,7 @@
}
},
"node_modules/npm/node_modules/cacache": {
- "version": "18.0.1",
+ "version": "18.0.2",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -6461,12 +6401,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/npm/node_modules/delegates": {
- "version": "1.0.0",
- "dev": true,
- "inBundle": true,
- "license": "MIT"
- },
"node_modules/npm/node_modules/diff": {
"version": "5.1.0",
"dev": true,
@@ -6513,24 +6447,6 @@
"inBundle": true,
"license": "MIT"
},
- "node_modules/npm/node_modules/event-target-shim": {
- "version": "5.0.1",
- "dev": true,
- "inBundle": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/npm/node_modules/events": {
- "version": "3.3.0",
- "dev": true,
- "inBundle": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.8.x"
- }
- },
"node_modules/npm/node_modules/exponential-backoff": {
"version": "3.1.1",
"dev": true,
@@ -6726,26 +6642,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/npm/node_modules/ieee754": {
- "version": "1.2.1",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "inBundle": true,
- "license": "BSD-3-Clause"
- },
"node_modules/npm/node_modules/ignore-walk": {
"version": "6.0.4",
"dev": true,
@@ -6937,7 +6833,7 @@
}
},
"node_modules/npm/node_modules/libnpmdiff": {
- "version": "6.0.4",
+ "version": "6.0.5",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -6957,7 +6853,7 @@
}
},
"node_modules/npm/node_modules/libnpmexec": {
- "version": "7.0.5",
+ "version": "7.0.6",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -6979,7 +6875,7 @@
}
},
"node_modules/npm/node_modules/libnpmfund": {
- "version": "5.0.2",
+ "version": "5.0.3",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -7017,7 +6913,7 @@
}
},
"node_modules/npm/node_modules/libnpmpack": {
- "version": "6.0.4",
+ "version": "6.0.5",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -7442,7 +7338,7 @@
}
},
"node_modules/npm/node_modules/npm-packlist": {
- "version": "8.0.1",
+ "version": "8.0.2",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -7610,7 +7506,7 @@
}
},
"node_modules/npm/node_modules/postcss-selector-parser": {
- "version": "6.0.13",
+ "version": "6.0.15",
"dev": true,
"inBundle": true,
"license": "MIT",
@@ -7631,15 +7527,6 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
- "node_modules/npm/node_modules/process": {
- "version": "0.11.10",
- "dev": true,
- "inBundle": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.6.0"
- }
- },
"node_modules/npm/node_modules/promise-all-reject-late": {
"version": "1.0.1",
"dev": true,
@@ -7746,22 +7633,6 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
- "node_modules/npm/node_modules/readable-stream": {
- "version": "4.4.2",
- "dev": true,
- "inBundle": true,
- "license": "MIT",
- "dependencies": {
- "abort-controller": "^3.0.0",
- "buffer": "^6.0.3",
- "events": "^3.3.0",
- "process": "^0.11.10",
- "string_decoder": "^1.3.0"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- }
- },
"node_modules/npm/node_modules/retry": {
"version": "0.12.0",
"dev": true,
@@ -7771,26 +7642,6 @@
"node": ">= 4"
}
},
- "node_modules/npm/node_modules/safe-buffer": {
- "version": "5.2.1",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "inBundle": true,
- "license": "MIT"
- },
"node_modules/npm/node_modules/safer-buffer": {
"version": "2.1.2",
"dev": true,
@@ -7961,15 +7812,6 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
- "node_modules/npm/node_modules/string_decoder": {
- "version": "1.3.0",
- "dev": true,
- "inBundle": true,
- "license": "MIT",
- "dependencies": {
- "safe-buffer": "~5.2.0"
- }
- },
"node_modules/npm/node_modules/string-width": {
"version": "4.2.3",
"dev": true,
@@ -9296,9 +9138,9 @@
"dev": true
},
"node_modules/safe-regex-test": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.1.tgz",
- "integrity": "sha512-Y5NejJTTliTyY4H7sipGqY+RX5P87i3F7c4Rcepy72nq+mNLhIsD0W4c7kEmduMDQCSqtPsXPlSTsFhh2LQv+g==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz",
+ "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.5",
diff --git a/package.json b/package.json
index d993b4f..4d11139 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,7 @@
"lint:prettier": "prettier . --check --ignore-path .gitignore",
"lint:javascript": "tsc --project jsconfig.json --noEmit",
"lint:staged": "lint-staged",
- "test": "node --test --experimental-test-coverage ./test",
+ "test": "node --test --experimental-test-coverage",
"release": "semantic-release",
"postinstall": "husky install",
"prepublishOnly": "pinst --disable",
@@ -50,13 +50,13 @@
"@commitlint/cli": "18.4.4",
"@commitlint/config-conventional": "18.4.4",
"@types/markdown-it": "13.0.7",
- "@types/node": "20.10.8",
+ "@types/node": "20.11.0",
"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-prettier": "5.1.3",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-unicorn": "50.0.1",
"husky": "8.0.3",
diff --git a/src/index.js b/src/index.js
index 1ba3513..0e7c9bd 100644
--- a/src/index.js
+++ b/src/index.js
@@ -3,10 +3,11 @@
const { pathToFileURL } = require("node:url")
const fs = require("node:fs")
+const { filterTokens } = require("./markdownlint-rule-helpers/helpers.js")
const {
- filterTokens,
convertHeadingToHTMLFragment,
getMarkdownHeadings,
+ getMarkdownIdOrAnchorNameFragments,
} = require("./utils.js")
/** @typedef {import('markdownlint').Rule} MarkdownLintRule */
@@ -45,46 +46,83 @@ const customRule = {
}
}
- if (hrefSrc != null) {
- const url = new URL(hrefSrc, pathToFileURL(params.name))
- const isRelative =
- url.protocol === "file:" && !hrefSrc.startsWith("/")
- if (isRelative) {
- const detail = `"${hrefSrc}"`
+ if (hrefSrc == null) {
+ continue
+ }
+
+ const url = new URL(hrefSrc, pathToFileURL(params.name))
+ const isRelative =
+ url.protocol === "file:" &&
+ !hrefSrc.startsWith("/") &&
+ !hrefSrc.startsWith("#")
+
+ if (!isRelative) {
+ continue
+ }
+
+ const detail = `"${hrefSrc}"`
- if (!fs.existsSync(url)) {
+ 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 !== "link_open") {
onError({
lineNumber,
- detail: `${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