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