10 Commits

Author SHA1 Message Date
146f904866 chore: small tweaks 2024-01-12 01:30:52 +01:00
64396954e4 chore: dependency vendoring of markdownlint-rule-helpers 2024-01-12 01:30:52 +01:00
92f35daeaf docs: explain limitations/features 2024-01-12 01:30:52 +01:00
7ef7cc3bb3 refactor: early conditions first 2024-01-12 01:30:52 +01:00
0479652ffe test: add cases for fragments checking 2024-01-12 01:30:52 +01:00
68f35ddc0b fix: empty id fragment should be invalid 2024-01-12 01:30:52 +01:00
747203c23b fix: fragments checking should work in other elements than only anchor (e.g: <div>) 2024-01-12 01:30:52 +01:00
a5deae599a fix: ignore checking fragment in own file
We don't need to check if fragments are valid in own file.
It's already checked by the rule MD051 of markdownlint (else we have the error duplicated).
Ref: https://github.com/DavidAnson/markdownlint/blob/main/doc/md051.md
2024-01-12 01:30:52 +01:00
24a0788d32 feat: html anchor support 2024-01-12 01:30:52 +01:00
9d2cc818d5 docs: fix broken GitHub Action link 2024-01-10 21:09:11 +01:00
31 changed files with 404 additions and 345 deletions

View File

@ -1,7 +1,11 @@
{ {
"extends": ["conventions", "prettier"], "extends": ["conventions", "prettier"],
"plugins": ["prettier"], "plugins": ["prettier", "import", "unicorn"],
"parserOptions": {
"project": "./tsconfig.json"
},
"rules": { "rules": {
"prettier/prettier": "error" "prettier/prettier": "error",
"import/extensions": ["error", "always"]
} }
} }

View File

@ -1,9 +1,9 @@
{ {
"config": { "config": {
"extends": "markdownlint/style/prettier", "extends": "markdownlint/style/prettier",
"relative-links": true,
"default": true, "default": true,
"MD033": false "relative-links": true,
"no-inline-html": false
}, },
"globs": ["**/*.{md,mdx}"], "globs": ["**/*.{md,mdx}"],
"ignores": ["**/node_modules", "**/test/fixtures/**"], "ignores": ["**/node_modules", "**/test/fixtures/**"],

View File

@ -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> <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 /> <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-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 /> <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://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> <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>
@ -49,13 +49,12 @@ awesome.md:3 relative-links Relative links should be valid ["./invalid.txt" shou
### Additional features ### Additional features
- Support images (e.g: `![Image](./image.png)`). - Support images (e.g: `![Image](./image.png)`).
- 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`). - Ignore external links and absolute paths as it only checks relative links (e.g: `https://example.com/` or `/absolute/path.png`).
### Limitations ### Limitations
- Only images and links defined using markdown syntax are supported, html syntax is not supported (e.g: `<a href="./link.txt" />` or `<img src="./image.png" />`). - 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" />`).
- Anchors checking is limited to headings, other elements are not supported (e.g: with a "id", `<div id="anchor" />`).
Contributions are welcome to improve the rule, and to alleviate these limitations. See [CONTRIBUTING.md](./CONTRIBUTING.md) for more information. Contributions are welcome to improve the rule, and to alleviate these limitations. See [CONTRIBUTING.md](./CONTRIBUTING.md) for more information.

View File

@ -9,6 +9,7 @@
"noEmit": true, "noEmit": true,
"rootDir": ".", "rootDir": ".",
"baseUrl": ".", "baseUrl": ".",
"skipLibCheck": true,
"strict": true, "strict": true,
"allowUnusedLabels": false, "allowUnusedLabels": false,
"allowUnreachableCode": false, "allowUnreachableCode": false,

240
package-lock.json generated
View File

@ -16,13 +16,13 @@
"@commitlint/cli": "18.4.4", "@commitlint/cli": "18.4.4",
"@commitlint/config-conventional": "18.4.4", "@commitlint/config-conventional": "18.4.4",
"@types/markdown-it": "13.0.7", "@types/markdown-it": "13.0.7",
"@types/node": "20.10.8", "@types/node": "20.11.0",
"editorconfig-checker": "5.1.2", "editorconfig-checker": "5.1.2",
"eslint": "8.56.0", "eslint": "8.56.0",
"eslint-config-conventions": "13.1.0", "eslint-config-conventions": "13.1.0",
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.29.1", "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-promise": "6.1.1",
"eslint-plugin-unicorn": "50.0.1", "eslint-plugin-unicorn": "50.0.1",
"husky": "8.0.3", "husky": "8.0.3",
@ -557,13 +557,13 @@
} }
}, },
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.11.13", "version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
"integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@humanwhocodes/object-schema": "^2.0.1", "@humanwhocodes/object-schema": "^2.0.2",
"debug": "^4.1.1", "debug": "^4.3.1",
"minimatch": "^3.0.5" "minimatch": "^3.0.5"
}, },
"engines": { "engines": {
@ -584,9 +584,9 @@
} }
}, },
"node_modules/@humanwhocodes/object-schema": { "node_modules/@humanwhocodes/object-schema": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
"integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
"dev": true "dev": true
}, },
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
@ -1325,9 +1325,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.10.8", "version": "20.11.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz",
"integrity": "sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==", "integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
@ -2422,9 +2422,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.4.625", "version": "1.4.628",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.625.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.628.tgz",
"integrity": "sha512-DENMhh3MFgaPDoXWrVIqSPInQoLImywfCwrSmVl3cf9QHzoZSiutHwGaB/Ql3VkqcQV30rzgdM+BjKqBAJxo5Q==", "integrity": "sha512-2k7t5PHvLsufpP6Zwk0nof62yLOsCf032wZx7/q0mv8gwlXjhcxI3lz6f0jBr0GrnWKcm3burXzI3t5IrcdUxw==",
"dev": true "dev": true
}, },
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
@ -2911,9 +2911,9 @@
} }
}, },
"node_modules/eslint-plugin-prettier": { "node_modules/eslint-plugin-prettier": {
"version": "5.1.2", "version": "5.1.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.2.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz",
"integrity": "sha512-dhlpWc9vOwohcWmClFcA+HjlvUpuyynYs0Rf+L/P6/0iQE6vlHW9l5bkfzN62/Stm9fbq8ku46qzde76T1xlSg==", "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"prettier-linter-helpers": "^1.0.0", "prettier-linter-helpers": "^1.0.0",
@ -5410,9 +5410,9 @@
} }
}, },
"node_modules/npm": { "node_modules/npm": {
"version": "10.2.5", "version": "10.3.0",
"resolved": "https://registry.npmjs.org/npm/-/npm-10.2.5.tgz", "resolved": "https://registry.npmjs.org/npm/-/npm-10.3.0.tgz",
"integrity": "sha512-lXdZ7titEN8CH5YJk9C/aYRU9JeDxQ4d8rwIIDsvH3SMjLjHTukB2CFstMiB30zXs4vCrPN2WH6cDq1yHBeJAw==", "integrity": "sha512-9u5GFc1UqI2DLlGI7QdjkpIaBs3UhTtY8KoCqYJK24gV/j/tByaI4BA4R7RkOc+ASqZMzFPKt4Pj2Z8JcGo//A==",
"bundleDependencies": [ "bundleDependencies": [
"@isaacs/string-locale-compare", "@isaacs/string-locale-compare",
"@npmcli/arborist", "@npmcli/arborist",
@ -5494,12 +5494,12 @@
"@npmcli/fs": "^3.1.0", "@npmcli/fs": "^3.1.0",
"@npmcli/map-workspaces": "^3.0.4", "@npmcli/map-workspaces": "^3.0.4",
"@npmcli/package-json": "^5.0.0", "@npmcli/package-json": "^5.0.0",
"@npmcli/promise-spawn": "^7.0.0", "@npmcli/promise-spawn": "^7.0.1",
"@npmcli/run-script": "^7.0.2", "@npmcli/run-script": "^7.0.3",
"@sigstore/tuf": "^2.2.0", "@sigstore/tuf": "^2.2.0",
"abbrev": "^2.0.0", "abbrev": "^2.0.0",
"archy": "~1.0.0", "archy": "~1.0.0",
"cacache": "^18.0.1", "cacache": "^18.0.2",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"ci-info": "^4.0.0", "ci-info": "^4.0.0",
"cli-columns": "^4.0.0", "cli-columns": "^4.0.0",
@ -5653,7 +5653,7 @@
} }
}, },
"node_modules/npm/node_modules/@npmcli/arborist": { "node_modules/npm/node_modules/@npmcli/arborist": {
"version": "7.2.2", "version": "7.3.0",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "ISC", "license": "ISC",
@ -5700,7 +5700,7 @@
} }
}, },
"node_modules/npm/node_modules/@npmcli/config": { "node_modules/npm/node_modules/@npmcli/config": {
"version": "8.0.3", "version": "8.1.0",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "ISC", "license": "ISC",
@ -5758,7 +5758,7 @@
} }
}, },
"node_modules/npm/node_modules/@npmcli/git": { "node_modules/npm/node_modules/@npmcli/git": {
"version": "5.0.3", "version": "5.0.4",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "ISC", "license": "ISC",
@ -5859,7 +5859,7 @@
} }
}, },
"node_modules/npm/node_modules/@npmcli/promise-spawn": { "node_modules/npm/node_modules/@npmcli/promise-spawn": {
"version": "7.0.0", "version": "7.0.1",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "ISC", "license": "ISC",
@ -5883,7 +5883,7 @@
} }
}, },
"node_modules/npm/node_modules/@npmcli/run-script": { "node_modules/npm/node_modules/@npmcli/run-script": {
"version": "7.0.2", "version": "7.0.3",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "ISC", "license": "ISC",
@ -5987,18 +5987,6 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0" "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": { "node_modules/npm/node_modules/agent-base": {
"version": "7.1.0", "version": "7.1.0",
"dev": true, "dev": true,
@ -6061,14 +6049,10 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/npm/node_modules/are-we-there-yet": { "node_modules/npm/node_modules/are-we-there-yet": {
"version": "4.0.1", "version": "4.0.2",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "ISC", "license": "ISC",
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^4.1.0"
},
"engines": { "engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0" "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
} }
@ -6079,26 +6063,6 @@
"inBundle": true, "inBundle": true,
"license": "MIT" "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": { "node_modules/npm/node_modules/bin-links": {
"version": "4.0.3", "version": "4.0.3",
"dev": true, "dev": true,
@ -6132,30 +6096,6 @@
"balanced-match": "^1.0.0" "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": { "node_modules/npm/node_modules/builtins": {
"version": "5.0.1", "version": "5.0.1",
"dev": true, "dev": true,
@ -6166,7 +6106,7 @@
} }
}, },
"node_modules/npm/node_modules/cacache": { "node_modules/npm/node_modules/cacache": {
"version": "18.0.1", "version": "18.0.2",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "ISC", "license": "ISC",
@ -6461,12 +6401,6 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/npm/node_modules/diff": {
"version": "5.1.0", "version": "5.1.0",
"dev": true, "dev": true,
@ -6513,24 +6447,6 @@
"inBundle": true, "inBundle": true,
"license": "MIT" "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": { "node_modules/npm/node_modules/exponential-backoff": {
"version": "3.1.1", "version": "3.1.1",
"dev": true, "dev": true,
@ -6726,26 +6642,6 @@
"node": ">=0.10.0" "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": { "node_modules/npm/node_modules/ignore-walk": {
"version": "6.0.4", "version": "6.0.4",
"dev": true, "dev": true,
@ -6937,7 +6833,7 @@
} }
}, },
"node_modules/npm/node_modules/libnpmdiff": { "node_modules/npm/node_modules/libnpmdiff": {
"version": "6.0.4", "version": "6.0.5",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "ISC", "license": "ISC",
@ -6957,7 +6853,7 @@
} }
}, },
"node_modules/npm/node_modules/libnpmexec": { "node_modules/npm/node_modules/libnpmexec": {
"version": "7.0.5", "version": "7.0.6",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "ISC", "license": "ISC",
@ -6979,7 +6875,7 @@
} }
}, },
"node_modules/npm/node_modules/libnpmfund": { "node_modules/npm/node_modules/libnpmfund": {
"version": "5.0.2", "version": "5.0.3",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "ISC", "license": "ISC",
@ -7017,7 +6913,7 @@
} }
}, },
"node_modules/npm/node_modules/libnpmpack": { "node_modules/npm/node_modules/libnpmpack": {
"version": "6.0.4", "version": "6.0.5",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "ISC", "license": "ISC",
@ -7442,7 +7338,7 @@
} }
}, },
"node_modules/npm/node_modules/npm-packlist": { "node_modules/npm/node_modules/npm-packlist": {
"version": "8.0.1", "version": "8.0.2",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "ISC", "license": "ISC",
@ -7610,7 +7506,7 @@
} }
}, },
"node_modules/npm/node_modules/postcss-selector-parser": { "node_modules/npm/node_modules/postcss-selector-parser": {
"version": "6.0.13", "version": "6.0.15",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
@ -7631,15 +7527,6 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0" "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": { "node_modules/npm/node_modules/promise-all-reject-late": {
"version": "1.0.1", "version": "1.0.1",
"dev": true, "dev": true,
@ -7746,22 +7633,6 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0" "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": { "node_modules/npm/node_modules/retry": {
"version": "0.12.0", "version": "0.12.0",
"dev": true, "dev": true,
@ -7771,26 +7642,6 @@
"node": ">= 4" "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": { "node_modules/npm/node_modules/safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
"dev": true, "dev": true,
@ -7961,15 +7812,6 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0" "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": { "node_modules/npm/node_modules/string-width": {
"version": "4.2.3", "version": "4.2.3",
"dev": true, "dev": true,
@ -9296,9 +9138,9 @@
"dev": true "dev": true
}, },
"node_modules/safe-regex-test": { "node_modules/safe-regex-test": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.1.tgz", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz",
"integrity": "sha512-Y5NejJTTliTyY4H7sipGqY+RX5P87i3F7c4Rcepy72nq+mNLhIsD0W4c7kEmduMDQCSqtPsXPlSTsFhh2LQv+g==", "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"call-bind": "^1.0.5", "call-bind": "^1.0.5",

View File

@ -37,7 +37,7 @@
"lint:prettier": "prettier . --check --ignore-path .gitignore", "lint:prettier": "prettier . --check --ignore-path .gitignore",
"lint:javascript": "tsc --project jsconfig.json --noEmit", "lint:javascript": "tsc --project jsconfig.json --noEmit",
"lint:staged": "lint-staged", "lint:staged": "lint-staged",
"test": "node --test --experimental-test-coverage ./test", "test": "node --test --experimental-test-coverage",
"release": "semantic-release", "release": "semantic-release",
"postinstall": "husky install", "postinstall": "husky install",
"prepublishOnly": "pinst --disable", "prepublishOnly": "pinst --disable",
@ -50,13 +50,13 @@
"@commitlint/cli": "18.4.4", "@commitlint/cli": "18.4.4",
"@commitlint/config-conventional": "18.4.4", "@commitlint/config-conventional": "18.4.4",
"@types/markdown-it": "13.0.7", "@types/markdown-it": "13.0.7",
"@types/node": "20.10.8", "@types/node": "20.11.0",
"editorconfig-checker": "5.1.2", "editorconfig-checker": "5.1.2",
"eslint": "8.56.0", "eslint": "8.56.0",
"eslint-config-conventions": "13.1.0", "eslint-config-conventions": "13.1.0",
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.29.1", "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-promise": "6.1.1",
"eslint-plugin-unicorn": "50.0.1", "eslint-plugin-unicorn": "50.0.1",
"husky": "8.0.3", "husky": "8.0.3",

View File

@ -3,10 +3,11 @@
const { pathToFileURL } = require("node:url") const { pathToFileURL } = require("node:url")
const fs = require("node:fs") const fs = require("node:fs")
const { filterTokens } = require("./markdownlint-rule-helpers/helpers.js")
const { const {
filterTokens,
convertHeadingToHTMLFragment, convertHeadingToHTMLFragment,
getMarkdownHeadings, getMarkdownHeadings,
getMarkdownIdOrAnchorNameFragments,
} = require("./utils.js") } = require("./utils.js")
/** @typedef {import('markdownlint').Rule} MarkdownLintRule */ /** @typedef {import('markdownlint').Rule} MarkdownLintRule */
@ -45,11 +46,20 @@ const customRule = {
} }
} }
if (hrefSrc != null) { if (hrefSrc == null) {
continue
}
const url = new URL(hrefSrc, pathToFileURL(params.name)) const url = new URL(hrefSrc, pathToFileURL(params.name))
const isRelative = const isRelative =
url.protocol === "file:" && !hrefSrc.startsWith("/") url.protocol === "file:" &&
if (isRelative) { !hrefSrc.startsWith("/") &&
!hrefSrc.startsWith("#")
if (!isRelative) {
continue
}
const detail = `"${hrefSrc}"` const detail = `"${hrefSrc}"`
if (!fs.existsSync(url)) { if (!fs.existsSync(url)) {
@ -60,14 +70,42 @@ const customRule = {
continue continue
} }
if (type === "link_open" && url.hash !== "") { if (url.hash.length <= 0) {
if (hrefSrc.includes("#")) {
if (type !== "link_open") {
onError({
lineNumber,
detail: `${detail} should not have a fragment identifier as it is an image`,
})
continue
}
onError({
lineNumber,
detail: `${detail} should have a valid fragment identifier`,
})
continue
}
continue
}
if (type !== "link_open") {
onError({
lineNumber,
detail: `${detail} should not have a fragment identifier as it is an image`,
})
continue
}
const fileContent = fs.readFileSync(url, { encoding: "utf8" }) const fileContent = fs.readFileSync(url, { encoding: "utf8" })
const headings = getMarkdownHeadings(fileContent) const headings = getMarkdownHeadings(fileContent)
const idOrAnchorNameHTMLFragments =
getMarkdownIdOrAnchorNameFragments(fileContent)
/** @type {Map<string, number>} */ /** @type {Map<string, number>} */
const fragments = new Map() const fragments = new Map()
const headingsHTMLFragments = headings.map((heading) => { const fragmentsHTML = headings.map((heading) => {
const fragment = convertHeadingToHTMLFragment(heading) const fragment = convertHeadingToHTMLFragment(heading)
const count = fragments.get(fragment) ?? 0 const count = fragments.get(fragment) ?? 0
fragments.set(fragment, count + 1) fragments.set(fragment, count + 1)
@ -77,14 +115,14 @@ const customRule = {
return fragment return fragment
}) })
if (!headingsHTMLFragments.includes(url.hash)) { fragmentsHTML.push(...idOrAnchorNameHTMLFragments)
if (!fragmentsHTML.includes(url.hash)) {
onError({ onError({
lineNumber, lineNumber,
detail: `${detail} should have a valid fragment identifier`, detail: `${detail} should have a valid fragment identifier`,
}) })
} continue
}
}
} }
} }
}) })

View File

@ -0,0 +1,38 @@
/**
* 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}
*/
const filterTokens = (params, type, handler) => {
for (const token of params.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.
*/
const getHtmlAttributeRe = (name) => {
return new RegExp(`\\s${name}\\s*=\\s*['"]?([^'"\\s>]*)`, "iu")
}
module.exports = {
filterTokens,
getHtmlAttributeRe,
}

View File

@ -1,23 +1,6 @@
const MarkdownIt = require("markdown-it") const MarkdownIt = require("markdown-it")
/** @typedef {import('markdownlint').RuleParams} MarkdownLintRuleParams */ const { getHtmlAttributeRe } = require("./markdownlint-rule-helpers/helpers.js")
/** @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}
*/
const filterTokens = (params, type, handler) => {
for (const token of params.tokens) {
if (token.type === type) {
handler(token)
}
}
}
/** /**
* Converts a Markdown heading into an HTML fragment according to the rules * Converts a Markdown heading into an HTML fragment according to the rules
@ -94,8 +77,45 @@ const getMarkdownHeadings = (content) => {
return headings return headings
} }
const nameHTMLAttributeRegex = getHtmlAttributeRe("name")
const idHTMLAttributeRegex = getHtmlAttributeRe("id")
/**
* Gets the id or anchor name fragments from a Markdown string.
* @param {string} content
* @returns {string[]}
*/
const getMarkdownIdOrAnchorNameFragments = (content) => {
const markdownIt = new MarkdownIt({ html: true })
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
}
module.exports = { module.exports = {
filterTokens,
convertHeadingToHTMLFragment, convertHeadingToHTMLFragment,
getMarkdownHeadings, getMarkdownHeadings,
getMarkdownIdOrAnchorNameFragments,
} }

View File

Before

Width:  |  Height:  |  Size: 95 B

After

Width:  |  Height:  |  Size: 95 B

View File

@ -0,0 +1,3 @@
# Awesome
<div id>Content</div>

View File

@ -0,0 +1,3 @@
# Invalid
[Link fragment](./awesome.md#)

View File

@ -0,0 +1,3 @@
# Invalid
![Image](../image.png#)

View File

@ -0,0 +1,3 @@
# Invalid
![Image](../image.png#non-existing-fragment)

View File

@ -0,0 +1,3 @@
# Awesome
<input name="name-should-be-ignored" id="id-after-name" />

View File

@ -0,0 +1,3 @@
# Invalid
[Invalid](./awesome.md#name-should-be-ignored)

View File

@ -0,0 +1,3 @@
# Awesome
<div data-id="not-an-id-should-be-ignored">Content</div>

View File

@ -0,0 +1,3 @@
# Invalid
[Invalid](./awesome.md#not-an-id-should-be-ignored)

View File

@ -0,0 +1,3 @@
# Awesome
<a name="existing-heading-anchor">Link</a>

View File

@ -0,0 +1,3 @@
# Invalid
[Link fragment](./awesome.md#non-existing-anchor-name-fragment)

View File

@ -0,0 +1,3 @@
# Awesome
<div id="existing-element-id-fragment">Content</div>

View File

@ -0,0 +1,3 @@
# Invalid
[Link fragment](./awesome.md#non-existing-element-id-fragment)

View File

@ -1,3 +1,3 @@
# Valid # Awesome
## Existing Heading ## Existing Heading

View File

@ -0,0 +1,3 @@
# Awesome
<a name="existing-heading-anchor">Link</a>

View File

@ -0,0 +1,3 @@
# Invalid
[Link fragment](./awesome.md#existing-heading-anchor)

View File

@ -0,0 +1,3 @@
# Awesome
<div id="existing-element-id-fragment">Content</div>

View File

@ -0,0 +1,3 @@
# Invalid
[Link fragment](./awesome.md#existing-element-id-fragment)

View File

@ -1,3 +1,3 @@
# Valid # Valid
![Image](./image.png) ![Image](../image.png)

View File

@ -0,0 +1,5 @@
# Valid
<div id="existing-element-id-fragment">Content</div>
[Link fragment](#non-existing-element-id-fragment)

View File

@ -23,90 +23,134 @@ const validateMarkdownLint = async (fixtureFile) => {
} }
test("ensure the rule validates correctly", async (t) => { test("ensure the rule validates correctly", async (t) => {
await t.test("should be valid", async (t) => {
await t.test("with an existing heading fragment", async () => {
const lintResults = await validateMarkdownLint(
"test/fixtures/valid/existing-heading-fragment/existing-heading-fragment.md",
)
assert.equal(lintResults?.length, 0)
})
await t.test("with an existing file", async () => {
const lintResults = await validateMarkdownLint(
"test/fixtures/valid/existing-file.md",
)
assert.equal(lintResults?.length, 0)
})
await t.test("with an existing image", async () => {
const lintResults = await validateMarkdownLint(
"test/fixtures/valid/existing-image.md",
)
assert.equal(lintResults?.length, 0)
})
await t.test("should ignore absolute paths", async () => {
const lintResults = await validateMarkdownLint(
"test/fixtures/valid/ignore-absolute-paths.md",
)
assert.equal(lintResults?.length, 0)
})
await t.test("should ignore external links", async () => {
const lintResults = await validateMarkdownLint(
"test/fixtures/valid/ignore-external-links.md",
)
assert.equal(lintResults?.length, 0)
})
})
await t.test("should be invalid", async (t) => { await t.test("should be invalid", async (t) => {
await t.test("with a non-existing heading fragment", async () => { const testCases = [
const lintResults = await validateMarkdownLint( {
name: "with an empty id fragment",
fixturePath:
"test/fixtures/invalid/empty-id-fragment/empty-id-fragment.md",
error: '"./awesome.md#" should have a valid fragment identifier',
},
{
name: "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",
error:
'"./awesome.md#name-should-be-ignored" should have a valid fragment identifier',
},
{
name: "with a non-existing id fragment (data-id !== id)",
fixturePath:
"test/fixtures/invalid/ignore-not-an-id-fragment/ignore-not-an-id-fragment.md",
error:
'"./awesome.md#not-an-id-should-be-ignored" should have a valid fragment identifier',
},
{
name: "with a non-existing anchor name fragment",
fixturePath:
"test/fixtures/invalid/non-existing-anchor-name-fragment/non-existing-anchor-name-fragment.md",
error:
'"./awesome.md#non-existing-anchor-name-fragment" should have a valid fragment identifier',
},
{
name: "with a non-existing element id fragment",
fixturePath:
"test/fixtures/invalid/non-existing-element-id-fragment/non-existing-element-id-fragment.md",
error:
'"./awesome.md#non-existing-element-id-fragment" should have a valid fragment identifier',
},
{
name: "with a non-existing heading fragment",
fixturePath:
"test/fixtures/invalid/non-existing-heading-fragment/non-existing-heading-fragment.md", "test/fixtures/invalid/non-existing-heading-fragment/non-existing-heading-fragment.md",
) error:
assert.equal(lintResults?.length, 1)
assert.deepEqual(lintResults?.[0]?.ruleNames, relativeLinksRule.names)
assert.equal(
lintResults?.[0]?.ruleDescription,
relativeLinksRule.description,
)
assert.equal(
lintResults?.[0]?.errorDetail,
'"./awesome.md#non-existing-heading" should have a valid fragment identifier', '"./awesome.md#non-existing-heading" should have a valid fragment identifier',
) },
}) {
name: "with a link to an image with a empty fragment",
fixturePath:
"test/fixtures/invalid/ignore-empty-fragment-checking-for-image.md",
error:
'"../image.png#" should not have a fragment identifier as it is an image',
},
{
name: "with a link to an image with a fragment",
fixturePath:
"test/fixtures/invalid/ignore-fragment-checking-for-image.md",
error:
'"../image.png#non-existing-fragment" should not have a fragment identifier as it is an image',
},
{
name: "with a non-existing file",
fixturePath: "test/fixtures/invalid/non-existing-file.md",
error: '"./index.test.js" should exist in the file system',
},
{
name: "with a non-existing image",
fixturePath: "test/fixtures/invalid/non-existing-image.md",
error: '"./image.png" should exist in the file system',
},
]
await t.test("with a non-existing file", async () => { for (const { name, fixturePath, error } of testCases) {
const lintResults = await validateMarkdownLint( await t.test(name, async () => {
"test/fixtures/invalid/non-existing-file.md", const lintResults = await validateMarkdownLint(fixturePath)
)
assert.equal(lintResults?.length, 1) assert.equal(lintResults?.length, 1)
assert.deepEqual(lintResults?.[0]?.ruleNames, relativeLinksRule.names) assert.deepEqual(lintResults?.[0]?.ruleNames, relativeLinksRule.names)
assert.equal( assert.equal(
lintResults?.[0]?.ruleDescription, lintResults?.[0]?.ruleDescription,
relativeLinksRule.description, relativeLinksRule.description,
) )
assert.equal( assert.equal(lintResults?.[0]?.errorDetail, error)
lintResults?.[0]?.errorDetail, })
'"./index.test.js" should exist in the file system', }
)
}) })
await t.test("with a non-existing image", async () => { await t.test("should be valid", async (t) => {
const lintResults = await validateMarkdownLint( const testCases = [
"test/fixtures/invalid/non-existing-image.md", {
) name: "with an existing anchor name fragment",
assert.equal(lintResults?.length, 1) fixturePath:
assert.deepEqual(lintResults?.[0]?.ruleNames, relativeLinksRule.names) "test/fixtures/valid/existing-anchor-name-fragment/existing-anchor-name-fragment.md",
assert.equal( },
lintResults?.[0]?.ruleDescription, {
relativeLinksRule.description, name: "with an existing element id fragment",
) fixturePath:
assert.equal( "test/fixtures/valid/existing-element-id-fragment/existing-element-id-fragment.md",
lintResults?.[0]?.errorDetail, },
'"./image.png" should exist in the file system', {
) name: "with an existing heading fragment",
fixturePath:
"test/fixtures/valid/existing-heading-fragment/existing-heading-fragment.md",
},
{
name: "with an existing file",
fixturePath: "test/fixtures/valid/existing-file.md",
},
{
name: "with an existing image",
fixturePath: "test/fixtures/valid/existing-image.md",
},
{
name: "should ignore absolute paths",
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",
},
]
for (const { name, fixturePath } of testCases) {
await t.test(name, async () => {
const lintResults = await validateMarkdownLint(fixturePath)
assert.equal(lintResults?.length, 0)
}) })
}
}) })
}) })

View File

@ -4,6 +4,7 @@ const assert = require("node:assert/strict")
const { const {
convertHeadingToHTMLFragment, convertHeadingToHTMLFragment,
getMarkdownHeadings, getMarkdownHeadings,
getMarkdownIdOrAnchorNameFragments,
} = require("../src/utils.js") } = require("../src/utils.js")
test("utils", async (t) => { test("utils", async (t) => {
@ -34,4 +35,23 @@ test("utils", async (t) => {
["Hello", "World", "Hello, world!"], ["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=>"), [])
})
}) })