Compare commits

...

5 Commits

Author SHA1 Message Date
theoludwig 1385c4bd80 fix: update markdown-it to v14.2.0 2026-06-09 21:53:13 +02:00
theoludwig 6e899aa899 docs: fix typo divider 2026-03-12 14:26:17 +01:00
Delta Umhöfer 36ed1ee788 feat: add config-option for fragment-index-divider (#20) 2026-02-25 15:04:32 +01:00
theoludwig f48932001a fix: update markdown-it to v14.1.1 2026-02-20 02:58:22 +01:00
theoludwig c744c4872a fix: ignore external https image links 2026-02-05 21:12:22 +01:00
14 changed files with 1268 additions and 807 deletions
+2 -2
View File
@@ -10,10 +10,10 @@ jobs:
lint:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2
- uses: "actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10" # v6.0.3
- name: "Setup Node.js"
uses: "actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238" # v6.2.0
uses: "actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e" # v6.4.0
with:
node-version: "lts/*"
cache: "npm"
+2 -2
View File
@@ -17,13 +17,13 @@ jobs:
pull-requests: "write"
id-token: "write"
steps:
- uses: "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2
- uses: "actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10" # v6.0.3
with:
fetch-depth: 0
persist-credentials: false
- name: "Setup Node.js"
uses: "actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238" # v6.2.0
uses: "actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e" # v6.4.0
with:
node-version: "lts/*"
cache: "npm"
+2 -2
View File
@@ -17,10 +17,10 @@ jobs:
- "macos-latest"
runs-on: "${{ matrix.runs-on }}"
steps:
- uses: "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2
- uses: "actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10" # v6.0.3
- name: "Setup Node.js"
uses: "actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238" # v6.2.0
uses: "actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e" # v6.4.0
with:
node-version: "lts/*"
cache: "npm"
+7 -1
View File
@@ -1,4 +1,10 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"extends": ["node_modules/eslint-config-conventions/.oxlintrc.json"]
"extends": ["node_modules/eslint-config-conventions/.oxlintrc.json"],
"env": {
"builtin": true,
"browser": true,
"node": true,
"shared-node-browser": true
}
}
+12
View File
@@ -52,6 +52,8 @@ awesome.md:3 relative-links Relative links should be valid ["./invalid.txt" shou
- 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).
- Headings defined multiple times in one file, will get enumerated as `<heading><divider><count>`.
The divider can be customized with [`fragment-count-divider`](#divider-for-fragment-index).
### Limitations
@@ -128,6 +130,16 @@ After this change, all absolute paths will be converted to relative paths, and w
For example, if you run markdownlint from a subdirectory (if `package.json` is located in a subdirectory), you should set `root_path` to `".."`.
### Divider for Fragment-Index
Headers with the same name in the same file, are appended with their index when converting them to the fragment.
Between the original fragment and the index a divider will be inserted.
The final fragment is `<original-fragment><divider><index>`.
This divider can be configured with `fragment-index-divider` to accomodate different markdown-engines.
The default-value is `-`.
## Usage
```sh
+1174 -786
View File
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -36,20 +36,20 @@
"release": "semantic-release"
},
"dependencies": {
"markdown-it": "14.1.0",
"markdown-it": "14.2.0",
"mime": "4.1.0"
},
"devDependencies": {
"@types/markdown-it": "14.1.2",
"@types/node": "25.2.1",
"eslint-config-conventions": "21.2.0",
"@types/node": "25.9.2",
"eslint-config-conventions": "21.4.0",
"markdownlint": "0.40.0",
"markdownlint-cli2": "0.20.0",
"oxfmt": "0.28.0",
"oxlint": "1.43.0",
"oxlint-tsgolint": "0.11.4",
"markdownlint-cli2": "0.22.1",
"oxfmt": "0.54.0",
"oxlint": "1.69.0",
"oxlint-tsgolint": "0.23.0",
"semantic-release": "25.0.3",
"typescript": "5.9.3"
"typescript": "6.0.3"
},
"engines": {
"node": ">=22.0.0"
+18 -3
View File
@@ -70,11 +70,24 @@ const relativeLinksRule = {
url = new URL(hrefSrc, pathToFileURL(params.name))
}
if (url.protocol !== "file:" && type !== "image") {
const detail = `"${hrefSrc}"`
if (
type === "image" &&
url.protocol !== "file:" &&
url.protocol !== "http:" &&
url.protocol !== "https:"
) {
onError({
lineNumber,
detail: `${detail} should be an image`,
})
continue
}
const detail = `"${hrefSrc}"`
if (url.protocol !== "file:") {
continue
}
if (!fs.existsSync(url)) {
onError({
@@ -131,12 +144,14 @@ const relativeLinksRule = {
/** @type {Map<string, number>} */
const fragments = new Map()
const fragmentCountDivider = params.config["fragment-index-divider"] ?? "-"
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}${fragmentCountDivider}${count}`
}
return fragment
})
+2 -2
View File
@@ -4,7 +4,7 @@ import { getHtmlAttributeRe } from "./markdownlint-rule-helpers/helpers.js"
export const markdownIt = new MarkdownIt({ html: true })
export const lineFragmentRe = /^#(?:L\d+(?:C\d+)?-L\d+(?:C\d+)?|L\d+)$/
export const lineFragmentRe = /^#(?:L\d+(?:C\d+)?-L\d+(?:C\d+)?|L\d+)$/u
/**
* Converts a Markdown heading into an HTML fragment according to the rules
@@ -125,7 +125,7 @@ export const getMarkdownIdOrAnchorNameFragments = (content) => {
* @example isValidIntegerString("1.0") // false
*/
export const isValidIntegerString = (value) => {
const regex = /^\d+$/
const regex = /^\d+$/u
return regex.test(value)
}
@@ -0,0 +1,13 @@
# Awesome
## Existing Heading
### Repeated Heading
Text
### Repeated Heading
Text
### Repeated Heading
@@ -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
View File
@@ -0,0 +1,3 @@
# Valid
![External Image](https://example.com/image.png)
+15 -1
View File
@@ -152,7 +152,7 @@ test("ensure the rule validates correctly", async (t) => {
fixturePath: "test/fixtures/invalid/invalid-image.md",
errors: [
'"../not-an-image.txt" should be an image',
'"mailto:not-an-image@pictures.com" should exist in the file system',
'"mailto:not-an-image@pictures.com" should be an image',
],
},
]
@@ -239,6 +239,20 @@ test("ensure the rule validates correctly", async (t) => {
},
},
},
{
name: "should be valid with multiple existing element id fragments",
fixturePath:
"test/fixtures/valid/existing-heading-fragment-divider/existing-heading-fragment.md",
config: {
"relative-links": {
"fragment-index-divider": "_",
},
},
},
{
name: "should ignore external image links",
fixturePath: "test/fixtures/valid/ignore-external-image.md",
},
]
for (const { name, fixturePath, config = defaultConfig } of testCases) {
+1
View File
@@ -4,6 +4,7 @@
"target": "ESNext",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"types": ["@types/node"],
"checkJs": true,
"allowJs": true,
"noEmit": true,