'use strict' const { pathToFileURL } = require('node:url') const fs = require('node:fs') const { filterTokens, addError, convertHeadingToHTMLFragment, getMarkdownHeadings } = require('./utils.js') const customRule = { names: ['relative-links'], description: 'Relative links should be valid', tags: ['links'], function: (params, onError) => { filterTokens(params, 'inline', (token) => { for (const child of token.children) { const { lineNumber, type, attrs } = child /** @type {string | null} */ let hrefSrc = null 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('/') if (isRelative) { const detail = `Link "${hrefSrc}"` if (!fs.existsSync(url)) { addError( onError, lineNumber, `${detail} should exist in the file system` ) continue } if (type === 'link_open' && url.hash !== '') { const fileContent = fs.readFileSync(url, { encoding: 'utf8' }) const headings = getMarkdownHeadings(fileContent) /** @type {Map} */ 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)) { addError( onError, lineNumber, `${detail} should have a valid fragment` ) } } } } } }) } } module.exports = customRule