2023-01-02 15:23:16 +01:00
|
|
|
'use strict'
|
|
|
|
|
2023-01-02 15:23:47 +01:00
|
|
|
const { pathToFileURL } = require('node:url')
|
|
|
|
const fs = require('node:fs')
|
|
|
|
|
2023-06-24 11:42:09 +02:00
|
|
|
const {
|
|
|
|
filterTokens,
|
|
|
|
addError,
|
|
|
|
convertHeadingToHTMLFragment,
|
|
|
|
getMarkdownHeadings
|
|
|
|
} = require('./utils.js')
|
2023-01-02 15:23:47 +01:00
|
|
|
|
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) => {
|
2023-07-18 23:18:39 +02:00
|
|
|
for (const child of token.children) {
|
2023-01-02 15:23:47 +01:00
|
|
|
const { lineNumber, type, attrs } = child
|
2023-01-02 19:45:46 +01:00
|
|
|
|
|
|
|
/** @type {string | null} */
|
|
|
|
let hrefSrc = null
|
|
|
|
|
2023-01-02 15:23:47 +01:00
|
|
|
if (type === 'link_open') {
|
2023-07-18 23:18:39 +02:00
|
|
|
for (const attr of attrs) {
|
2023-01-02 15:23:47 +01: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
|
|
|
|
|
|
|
if (type === 'image') {
|
2023-07-18 23:18:39 +02:00
|
|
|
for (const attr of attrs) {
|
2023-01-02 19:45:46 +01:00
|
|
|
if (attr[0] === 'src') {
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
if (hrefSrc != null) {
|
|
|
|
const url = new URL(hrefSrc, pathToFileURL(params.name))
|
2023-04-02 21:10:47 +02:00
|
|
|
const isRelative =
|
|
|
|
url.protocol === 'file:' && !hrefSrc.startsWith('/')
|
2023-06-24 11:42:09 +02:00
|
|
|
if (isRelative) {
|
2023-06-27 13:15:03 +02:00
|
|
|
const detail = `Link "${hrefSrc}"`
|
2023-06-24 11:42:09 +02:00
|
|
|
|
|
|
|
if (!fs.existsSync(url)) {
|
2023-06-27 13:15:03 +02:00
|
|
|
addError(
|
|
|
|
onError,
|
|
|
|
lineNumber,
|
|
|
|
`${detail} should exist in the file system`
|
|
|
|
)
|
2023-07-18 23:18:39 +02:00
|
|
|
continue
|
2023-06-24 11:42:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (type === 'link_open' && url.hash !== '') {
|
|
|
|
const fileContent = fs.readFileSync(url, { encoding: 'utf8' })
|
|
|
|
const headings = getMarkdownHeadings(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
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!headingsHTMLFragments.includes(url.hash)) {
|
2023-06-27 13:15:03 +02:00
|
|
|
addError(
|
|
|
|
onError,
|
|
|
|
lineNumber,
|
|
|
|
`${detail} should have a valid fragment`
|
|
|
|
)
|
2023-06-24 11:42:09 +02:00
|
|
|
}
|
|
|
|
}
|
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
|