mirror of
https://github.com/theoludwig/markdownlint-rule-relative-links.git
synced 2025-02-07 15:36:52 +01:00
115 lines
3.1 KiB
JavaScript
115 lines
3.1 KiB
JavaScript
"use strict"
|
|
|
|
const { pathToFileURL } = require("node:url")
|
|
const fs = require("node:fs")
|
|
|
|
const {
|
|
filterTokens,
|
|
convertHeadingToHTMLFragment,
|
|
getMarkdownHeadings,
|
|
getMarkdownAnchorHTMLFragments,
|
|
} = require("./utils.js")
|
|
|
|
/** @typedef {import('markdownlint').Rule} MarkdownLintRule */
|
|
|
|
/**
|
|
* @type {MarkdownLintRule}
|
|
*/
|
|
const customRule = {
|
|
names: ["relative-links"],
|
|
description: "Relative links should be valid",
|
|
tags: ["links"],
|
|
function: (params, onError) => {
|
|
filterTokens(params, "inline", (token) => {
|
|
const children = token.children ?? []
|
|
for (const child of children) {
|
|
const { type, attrs, lineNumber } = child
|
|
|
|
/** @type {string | undefined} */
|
|
let hrefSrc
|
|
|
|
if (type === "link_open") {
|
|
for (const attr of attrs) {
|
|
if (attr[0] === "href") {
|
|
hrefSrc = attr[1]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if (type === "image") {
|
|
for (const attr of attrs) {
|
|
if (attr[0] === "src") {
|
|
hrefSrc = attr[1]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hrefSrc != null) {
|
|
const url = new URL(hrefSrc, pathToFileURL(params.name))
|
|
const isRelative =
|
|
url.protocol === "file:" &&
|
|
!hrefSrc.startsWith("/") &&
|
|
!hrefSrc.startsWith("#")
|
|
if (isRelative) {
|
|
const detail = `"${hrefSrc}"`
|
|
|
|
if (!fs.existsSync(url)) {
|
|
onError({
|
|
lineNumber,
|
|
detail: `${detail} should exist in the file system`,
|
|
})
|
|
continue
|
|
}
|
|
|
|
if (type !== "link_open") {
|
|
continue
|
|
}
|
|
|
|
if (url.hash.length <= 0) {
|
|
if (hrefSrc.includes("#")) {
|
|
onError({
|
|
lineNumber,
|
|
detail: `${detail} should have a valid fragment identifier`,
|
|
})
|
|
}
|
|
}
|
|
|
|
if (url.hash.length > 0) {
|
|
const fileContent = fs.readFileSync(url, { encoding: "utf8" })
|
|
const headings = getMarkdownHeadings(fileContent)
|
|
const anchorHTMLFragments =
|
|
getMarkdownAnchorHTMLFragments(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
|
|
})
|
|
|
|
headingsHTMLFragments.push(...anchorHTMLFragments)
|
|
|
|
if (!headingsHTMLFragments.includes(url.hash)) {
|
|
onError({
|
|
lineNumber,
|
|
detail: `${detail} should have a valid fragment identifier`,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
},
|
|
}
|
|
|
|
module.exports = customRule
|