133 lines
3.3 KiB
JavaScript
Raw Normal View History

"use strict"
2023-01-02 15:23:16 +01:00
const { pathToFileURL } = require("node:url")
const fs = require("node:fs")
2023-01-02 15:23:47 +01:00
const {
filterTokens,
convertHeadingToHTMLFragment,
getMarkdownHeadings,
2024-01-12 00:43:45 +01:00
getMarkdownIdOrAnchorNameFragments,
} = require("./utils.js")
2023-01-02 15:23:47 +01:00
/** @typedef {import('markdownlint').Rule} MarkdownLintRule */
/**
* @type {MarkdownLintRule}
*/
2023-01-02 15:23:16 +01:00
const customRule = {
names: ["relative-links"],
description: "Relative links should be valid",
tags: ["links"],
2023-01-02 15:23:47 +01:00
function: (params, onError) => {
filterTokens(params, "inline", (token) => {
const children = token.children ?? []
for (const child of children) {
const { type, attrs, lineNumber } = child
2023-01-02 19:45:46 +01:00
/** @type {string | undefined} */
let hrefSrc
2023-01-02 19:45:46 +01:00
if (type === "link_open") {
2023-07-18 23:18:39 +02:00
for (const attr of attrs) {
if (attr[0] === "href") {
2023-01-02 19:45:46 +01:00
hrefSrc = attr[1]
2023-07-18 23:18:39 +02:00
break
2023-01-02 15:23:47 +01:00
}
2023-07-18 23:18:39 +02:00
}
2023-01-02 15:23:47 +01:00
}
2023-01-02 19:45:46 +01:00
if (type === "image") {
2023-07-18 23:18:39 +02:00
for (const attr of attrs) {
if (attr[0] === "src") {
2023-01-02 19:45:46 +01:00
hrefSrc = attr[1]
2023-07-18 23:18:39 +02:00
break
2023-01-02 19:45:46 +01:00
}
2023-07-18 23:18:39 +02:00
}
2023-01-02 19:45:46 +01:00
}
2024-01-12 00:43:45 +01:00
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}"`
2024-01-12 00:43:45 +01:00
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,
2024-01-12 00:43:45 +01:00
detail: `${detail} should not have a fragment identifier as it is an image`,
})
2023-07-18 23:18:39 +02:00
continue
}
2024-01-12 00:43:45 +01:00
onError({
lineNumber,
detail: `${detail} should have a valid fragment identifier`,
})
continue
}
continue
}
2024-01-12 00:43:45 +01:00
if (type !== "link_open") {
onError({
lineNumber,
detail: `${detail} should not have a fragment identifier as it is an image`,
})
continue
}
2024-01-12 00:43:45 +01:00
const fileContent = fs.readFileSync(url, { encoding: "utf8" })
const headings = getMarkdownHeadings(fileContent)
const idOrAnchorNameHTMLFragments =
getMarkdownIdOrAnchorNameFragments(fileContent)
2024-01-12 00:43:45 +01:00
/** @type {Map<string, number>} */
const fragments = new Map()
2024-01-11 13:42:34 +00:00
2024-01-12 00:43:45 +01:00
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}`
2023-01-02 19:45:46 +01:00
}
2024-01-12 00:43:45 +01:00
return fragment
})
fragmentsHTML.push(...idOrAnchorNameHTMLFragments)
if (!fragmentsHTML.includes(url.hash)) {
onError({
lineNumber,
detail: `${detail} should have a valid fragment identifier`,
})
continue
2023-01-02 19:45:46 +01:00
}
2023-07-18 23:18:39 +02:00
}
2023-01-02 15:23:47 +01:00
})
},
2023-01-02 15:23:16 +01:00
}
module.exports = customRule