remark-validate-links
Advanced tools
Comparing version 10.0.4 to 13.0.0
@@ -1,3 +0,6 @@ | ||
'use strict' | ||
/** | ||
* @typedef {import('./lib/index.js').Options} Options | ||
* @typedef {import('./lib/index.js').UrlConfig} UrlConfig | ||
*/ | ||
module.exports = require('./lib') | ||
export {default} from './lib/index.js' |
@@ -1,8 +0,8 @@ | ||
'use strict' | ||
exports.sourceId = 'remark-validate-links' | ||
exports.headingRuleId = 'missing-heading' | ||
exports.headingInFileRuleId = 'missing-heading-in-file' | ||
exports.fileRuleId = 'missing-file' | ||
exports.landmarkId = 'remarkValidateLinksLandmarks' | ||
exports.referenceId = 'remarkValidateLinksReferences' | ||
export const constants = /** @type {const} */ ({ | ||
fileRuleId: 'missing-file', | ||
headingInFileRuleId: 'missing-heading-in-file', | ||
headingRuleId: 'missing-heading', | ||
landmarkId: 'remarkValidateLinksLandmarks', | ||
referenceId: 'remarkValidateLinksReferences', | ||
sourceId: 'remark-validate-links' | ||
}) |
780
lib/index.js
@@ -1,14 +0,185 @@ | ||
'use strict' | ||
/// <reference types="mdast-util-to-hast" /> | ||
var check = require('./check') | ||
var find = require('./find') | ||
var constants = require('./constants') | ||
/** | ||
* @typedef {import('hosted-git-info').Hosts} Hosts | ||
* @typedef {import('mdast').Nodes} Nodes | ||
* @typedef {import('mdast').Resource} Resource | ||
* @typedef {import('mdast').Root} Root | ||
* @typedef {import('unified-engine').FileSet} FileSet | ||
*/ | ||
module.exports = validateLinks | ||
/** | ||
* @typedef {Map<string, Map<string, boolean>>} Landmarks | ||
* Landmarks. | ||
* | ||
* @typedef Options | ||
* Configuration. | ||
* @property {string | false | null | undefined} [repository] | ||
* URL to hosted Git (default: detected from Git remote); | ||
* if you’re not in a Git repository, you must pass `false`; | ||
* if the repository resolves to something npm understands as a Git host such | ||
* as GitHub, GitLab, or Bitbucket, full URLs to that host (say, | ||
* `https://github.com/remarkjs/remark-validate-links/readme.md#install`) are | ||
* checked. | ||
* @property {string | null | undefined} [root] | ||
* Path to Git root folder (default: local Git folder); | ||
* if both `root` and `repository` are nullish, the Git root is detected; | ||
* if `root` is not given but `repository` is, `file.cwd` is used. | ||
* @property {Readonly<UrlConfig> | null | undefined} [urlConfig] | ||
* Config on how hosted Git works (default: detected from repo); | ||
* `github.com`, `gitlab.com`, or `bitbucket.org` work automatically; | ||
* otherwise, pass `urlConfig` manually. | ||
* | ||
* @callback Propose | ||
* @param {string} value | ||
* @param {ReadonlyArray<string>} dictionary | ||
* @param {Readonly<ProposeOptions> | null | undefined} [options] | ||
* @returns {string | undefined} | ||
* | ||
* @typedef ProposeOptions | ||
* Configuration for `propose`. | ||
* @property {number| null | undefined} [threshold] | ||
* Threshold. | ||
* | ||
* @typedef {Extract<Nodes, Resource>} Resources | ||
* Resources. | ||
* | ||
* @typedef Reference | ||
* Reference to something. | ||
* @property {string} filePath | ||
* Path to file. | ||
* @property {string | undefined} hash | ||
* Hash. | ||
* | ||
* @typedef ReferenceInfo | ||
* Info on a reference. | ||
* @property {VFile} file | ||
* File. | ||
* @property {Readonly<Reference>} reference | ||
* Reference. | ||
* @property {ReadonlyArray<Readonly<Resources>>} nodes | ||
* Nodes that reference it. | ||
* | ||
* @typedef State | ||
* Info passed around. | ||
* @property {string} base | ||
* Folder of file. | ||
* @property {string} path | ||
* Path to file. | ||
* @property {string | null | undefined} root | ||
* Path to Git folder. | ||
* @property {Readonly<UrlConfig>} urlConfig | ||
* Configuration. | ||
* | ||
* @typedef UrlConfig | ||
* Hosted Git info. | ||
* | ||
* ###### Notes | ||
* | ||
* For this repository (`remarkjs/remark-validate-links` on GitHub) | ||
* `urlConfig` looks as follows: | ||
* | ||
* ```js | ||
* { | ||
* // Domain of URLs: | ||
* hostname: 'github.com', | ||
* // Path prefix before files: | ||
* prefix: '/remarkjs/remark-validate-links/blob/', | ||
* // Prefix of headings: | ||
* headingPrefix: '#', | ||
* // Hash to top of markdown documents: | ||
* topAnchor: '#readme', | ||
* // Whether lines in files can be linked: | ||
* lines: true | ||
* } | ||
* ``` | ||
* | ||
* If this project were hosted on Bitbucket, it would be: | ||
* | ||
* ```js | ||
* { | ||
* hostname: 'bitbucket.org', | ||
* prefix: '/remarkjs/remark-validate-links/src/', | ||
* headingPrefix: '#markdown-header-', | ||
* lines: false | ||
* } | ||
* ``` | ||
* | ||
* @property {string | null | undefined} [headingPrefix] | ||
* Prefix of headings (example: `'#'`, `'#markdown-header-'`). | ||
* @property {string | null | undefined} [hostname] | ||
* Domain of URLs (example: `'github.com'`, `'bitbucket.org'`). | ||
* @property {boolean | null | undefined} [lines] | ||
* Whether lines in files can be linked. | ||
* @property {string | null | undefined} [prefix] | ||
* Path prefix before files (example: | ||
* `'/remarkjs/remark-validate-links/blob/'`, | ||
* `'/remarkjs/remark-validate-links/src/'`). | ||
* @property {string | null | undefined} [topAnchor] | ||
* Hash to top of readme (example: `#readme`). | ||
*/ | ||
import fs from 'node:fs/promises' | ||
import path from 'node:path' | ||
import GithubSlugger from 'github-slugger' | ||
import hostedGitInfo from 'hosted-git-info' | ||
import {toString} from 'mdast-util-to-string' | ||
// @ts-expect-error: untyped. | ||
import propose_ from 'propose' | ||
import {visit} from 'unist-util-visit' | ||
import {VFile} from 'vfile' | ||
import {constants} from './constants.js' | ||
import {checkFiles} from '#check-files' | ||
import {findRepo} from '#find-repo' | ||
const propose = /** @type {Propose} */ (propose_) | ||
cliCompleter.pluginId = constants.sourceId | ||
function validateLinks(options, fileSet) { | ||
var settings = options || {} | ||
/** @type {Readonly<Partial<Record<Hosts, string>>>} */ | ||
const viewPaths = {github: 'blob', gitlab: 'blob', bitbucket: 'src'} | ||
/** @type {Readonly<Partial<Record<Hosts, string>>>} */ | ||
const headingPrefixes = { | ||
bitbucket: '#markdown-header-', | ||
github: '#', | ||
gitlab: '#' | ||
} | ||
/** @type {Readonly<Partial<Record<Hosts, string>>>} */ | ||
const topAnchors = {github: '#readme', gitlab: '#readme'} | ||
/** @type {Readonly<Partial<Record<Hosts, boolean>>>} */ | ||
const lineLinks = {github: true, gitlab: true} | ||
const slugger = new GithubSlugger() | ||
const slash = '/' | ||
const numberSign = '#' | ||
const questionMark = '?' | ||
const https = 'https:' | ||
const http = 'http:' | ||
const slashes = '//' | ||
const lineExpression = /^#l\d/i | ||
// List from: https://github.com/github/markup#markups | ||
const readmeExtensions = new Set(['.markdown', '.mdown', '.mkdn', '.md']) | ||
const readmeBasename = /^readme$/i | ||
/** | ||
* Check that markdown links and images point to existing local files and | ||
* headings in a Git repo. | ||
* | ||
* > ⚠️ **Important**: The API in Node.js checks links to headings and files | ||
* > but does not check whether headings in other files exist. | ||
* > The API in browsers only checks links to headings in the same file. | ||
* > The CLI can check everything. | ||
* | ||
* @param {Readonly<Options> | null | undefined} [options] | ||
* Configuration (optional). | ||
* @param {FileSet | null | undefined} [fileSet] | ||
* File set (optional). | ||
* @returns | ||
* Transform. | ||
*/ | ||
export default function remarkValidateLinks(options, fileSet) { | ||
const settings = options || {} | ||
// Attach a `completer`. | ||
@@ -19,33 +190,586 @@ if (fileSet) { | ||
return transformer | ||
/** | ||
* Transform. | ||
* | ||
* @param {Root} tree | ||
* Tree. | ||
* @param {VFile} file | ||
* File. | ||
* @returns {Promise<void>} | ||
* Nothing. | ||
* | ||
* Note: `void` needed because `unified` doesn’t seem to accept `undefined`. | ||
*/ | ||
return async function (tree, file) { | ||
/* c8 ignore next -- this yields `undefined` in browsers. */ | ||
const [repo, root] = (await findRepo(file, settings)) || [] | ||
let urlConfig = settings.urlConfig | ||
// Find references and landmarks. | ||
function transformer(tree, file, next) { | ||
find.run( | ||
Object.assign({}, settings, {tree: tree, file: file, fileSet: fileSet}), | ||
done | ||
) | ||
if (!urlConfig) { | ||
/** @type {UrlConfig} */ | ||
const config = { | ||
headingPrefix: '#', | ||
hostname: undefined, | ||
lines: false, | ||
prefix: '', | ||
topAnchor: undefined | ||
} | ||
function done(error) { | ||
if (error) { | ||
next(error) | ||
} else if (fileSet) { | ||
next() | ||
} else { | ||
checkAll([file], next) | ||
if (repo) { | ||
const info = hostedGitInfo.fromUrl(repo) | ||
if (info && info.type !== 'gist') { | ||
if (info.type in viewPaths) { | ||
config.prefix = '/' + info.path() + '/' + viewPaths[info.type] + '/' | ||
} | ||
if (info.type in headingPrefixes) { | ||
config.headingPrefix = headingPrefixes[info.type] | ||
} | ||
if (info.type in lineLinks) { | ||
config.lines = lineLinks[info.type] | ||
} | ||
if (info.type in topAnchors) { | ||
config.topAnchor = topAnchors[info.type] | ||
} | ||
config.hostname = info.domain | ||
} | ||
} | ||
urlConfig = config | ||
} | ||
const absolute = file.path ? path.resolve(file.cwd, file.path) : '' | ||
const space = file.data | ||
/** @type {Map<string, Map<string, Array<Resources>>>} */ | ||
const references = new Map() | ||
/** @type {Landmarks} */ | ||
const landmarks = new Map() | ||
/** @type {State} */ | ||
const state = { | ||
base: absolute ? path.dirname(absolute) : file.cwd, | ||
path: absolute, | ||
root, | ||
urlConfig | ||
} | ||
/** @type {Set<string>} */ | ||
const statted = new Set() | ||
/** @type {Set<string>} */ | ||
const added = new Set() | ||
/** @type {Array<Promise<void>>} */ | ||
const promises = [] | ||
space[constants.referenceId] = references | ||
space[constants.landmarkId] = landmarks | ||
addLandmarks(absolute, '') | ||
slugger.reset() | ||
visit(tree, function (node) { | ||
const data = node.data || {} | ||
const props = data.hProperties || {} | ||
// @ts-expect-error: accept a `data.id`, which is not standard mdast, but | ||
// is here for historical reasons. | ||
const dataId = /** @type {unknown} */ (data.id) | ||
let id = String(props.name || props.id || dataId || '') | ||
if (!id && node.type === 'heading') { | ||
id = slugger.slug( | ||
toString(node, {includeHtml: false, includeImageAlt: false}) | ||
) | ||
} | ||
if (id) { | ||
addLandmarks(absolute, id) | ||
} | ||
if ('url' in node && node.url) { | ||
const info = urlToPath(node.url, state, node.type) | ||
if (info) { | ||
const fp = info.filePath | ||
const hash = info.hash | ||
addReference(fp, '', node) | ||
if (hash) { | ||
if (fileSet || fp === absolute) { | ||
addReference(fp, hash, node) | ||
} | ||
if (fileSet && fp && !statted.has(fp)) { | ||
promises.push(addFile(fp)) | ||
} | ||
} | ||
} | ||
} | ||
}) | ||
await Promise.all(promises) | ||
if (!fileSet) { | ||
await checkAll([file]) | ||
} | ||
/** | ||
* @param {string} filePath | ||
* Absolute path to file. | ||
* @param {string} hash | ||
* Hash. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function addLandmarks(filePath, hash) { | ||
addLandmark(filePath, hash) | ||
// Note: this may add marks too many anchors as defined. | ||
// For example, if there is both a `readme.md` and a `readme.markdown` in a | ||
// folder, both their landmarks will be defined for their parent folder. | ||
// To solve this, we could check whichever sorts first, and ignore the | ||
// others. | ||
// This is an unlikely scenario though, and adds a lot of complexity, so | ||
// we’re ignoring it. | ||
if (readme(filePath)) { | ||
addLandmark(path.dirname(filePath), hash) | ||
} | ||
} | ||
/** | ||
* @param {string} filePath | ||
* Absolute path to file. | ||
* @param {string} hash | ||
* Hash. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function addLandmark(filePath, hash) { | ||
let marks = landmarks.get(filePath) | ||
if (!marks) { | ||
marks = new Map() | ||
landmarks.set(filePath, marks) | ||
} | ||
marks.set(hash, true) | ||
} | ||
/** | ||
* @param {string} filePath | ||
* Absolute path to file. | ||
* @param {string} hash | ||
* Hash. | ||
* @param {Resources} node | ||
* Node. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function addReference(filePath, hash, node) { | ||
let refs = references.get(filePath) | ||
if (!refs) { | ||
refs = new Map() | ||
references.set(filePath, refs) | ||
} | ||
let hashes = refs.get(hash) | ||
if (!hashes) { | ||
hashes = [] | ||
refs.set(hash, hashes) | ||
} | ||
hashes.push(node) | ||
} | ||
/** | ||
* @param {string} filePath | ||
* Absolute path to file. | ||
* @returns {Promise<undefined>} | ||
* Nothing. | ||
*/ | ||
async function addFile(filePath) { | ||
statted.add(filePath) | ||
try { | ||
const stats = await fs.stat(filePath) | ||
if (stats.isDirectory()) { | ||
/** @type {Array<string>} */ | ||
let entries = [] | ||
try { | ||
entries = await fs.readdir(filePath) | ||
/* c8 ignore next -- seems to never happen after a stat. */ | ||
} catch {} | ||
const files = entries.sort() | ||
let index = -1 | ||
/** @type {string | undefined} */ | ||
let file | ||
while (++index < files.length) { | ||
const entry = entries[index] | ||
if (readme(entry)) { | ||
file = entry | ||
break | ||
} | ||
} | ||
// To do: test for no readme in directory. | ||
// Else, there’s no readme that we can parse, so add the directory. | ||
if (file) { | ||
filePath = path.join(filePath, file) | ||
statted.add(filePath) | ||
} | ||
} | ||
} catch {} | ||
if (fileSet && !added.has(filePath)) { | ||
added.add(filePath) | ||
fileSet.add( | ||
new VFile({cwd: file.cwd, path: path.relative(file.cwd, filePath)}) | ||
) | ||
} | ||
} | ||
} | ||
} | ||
// Completer for the CLI (multiple files, supports parsing more files). | ||
function cliCompleter(set, next) { | ||
checkAll(set.valueOf(), next) | ||
/** | ||
* Completer for the CLI (multiple files, supports parsing more files). | ||
* | ||
* @param {FileSet} set | ||
* @returns {Promise<undefined>} | ||
*/ | ||
async function cliCompleter(set) { | ||
await checkAll(set.valueOf()) | ||
} | ||
function checkAll(files, next) { | ||
// Check all references and landmarks. | ||
check.run({files: files}, function (error) { | ||
next(error) | ||
/** | ||
* Completer for the CLI (multiple files, supports parsing more files). | ||
* | ||
* @param {ReadonlyArray<VFile>} files | ||
* Files. | ||
* @returns {Promise<undefined>} | ||
* Nothing. | ||
*/ | ||
async function checkAll(files) { | ||
// Merge landmarks. | ||
/** @type {Landmarks} */ | ||
const landmarks = new Map() | ||
let index = -1 | ||
while (++index < files.length) { | ||
const file = files[index] | ||
const fileLandmarks = /** @type {Landmarks | undefined} */ ( | ||
file.data[constants.landmarkId] | ||
) | ||
if (fileLandmarks) { | ||
for (const [filePath, marks] of fileLandmarks) { | ||
landmarks.set(filePath, new Map(marks)) | ||
} | ||
} | ||
} | ||
// Merge references. | ||
/** @type {Map<string, Map<string, Array<ReferenceInfo>>>} */ | ||
const references = new Map() | ||
index = -1 | ||
while (++index < files.length) { | ||
const file = files[index] | ||
const fileReferences = | ||
/** @type {Map<string, Map<string, Array<Resources>>> | undefined} */ ( | ||
file.data[constants.referenceId] | ||
) | ||
if (!fileReferences) { | ||
continue | ||
} | ||
for (const [reference, internal] of fileReferences) { | ||
let all = references.get(reference) | ||
if (!all) { | ||
all = new Map() | ||
references.set(reference, all) | ||
} | ||
for (const [hash, nodes] of internal) { | ||
let list = all.get(hash) | ||
if (!list) { | ||
list = [] | ||
all.set(hash, list) | ||
} | ||
list.push({ | ||
file, | ||
nodes, | ||
reference: {filePath: reference, hash} | ||
}) | ||
} | ||
} | ||
} | ||
// Access files to see whether they exist. | ||
await checkFiles(landmarks, [...references.keys()]) | ||
/** @type {Array<ReferenceInfo>} */ | ||
const missing = [] | ||
for (const [key, refs] of references) { | ||
const lands = landmarks.get(key) | ||
for (const [hash, infos] of refs) { | ||
/* c8 ignore next -- `else` can only happen in browser. */ | ||
const exists = lands ? lands.get(hash) : false | ||
if (!exists) { | ||
missing.push(...infos) | ||
} | ||
} | ||
} | ||
index = -1 | ||
while (++index < missing.length) { | ||
warn(landmarks, missing[index]) | ||
} | ||
} | ||
/** | ||
* @param {Landmarks} landmarks | ||
* Landmarks. | ||
* @param {ReferenceInfo} reference | ||
* Reference. | ||
*/ | ||
function warn(landmarks, reference) { | ||
const absolute = reference.file.path | ||
? path.resolve(reference.file.cwd, reference.file.path) | ||
: '' | ||
const base = absolute ? path.dirname(absolute) : null | ||
const filePath = base | ||
? path.relative(base, reference.reference.filePath) | ||
: reference.reference.filePath | ||
const hash = reference.reference.hash | ||
/** @type {Array<string>} */ | ||
const dictionary = [] | ||
/** @type {string} */ | ||
let reason | ||
/** @type {string} */ | ||
let ruleId | ||
if (hash) { | ||
reason = 'Cannot find heading for `#' + hash + '`' | ||
ruleId = constants.headingRuleId | ||
if (base && path.join(base, filePath) !== absolute) { | ||
reason += ' in `' + filePath + '`' | ||
ruleId = constants.headingInFileRuleId | ||
} | ||
} else { | ||
reason = 'Cannot find file `' + filePath + '`' | ||
ruleId = constants.fileRuleId | ||
} | ||
const origin = [constants.sourceId, ruleId].join(':') | ||
for (const [landmark, marks] of landmarks) { | ||
// Only suggest if file exists. | ||
if (!marks || !marks.get('')) { | ||
continue | ||
} | ||
const relativeLandmark = base ? path.relative(base, landmark) : landmark | ||
if (!hash) { | ||
dictionary.push(relativeLandmark) | ||
continue | ||
} | ||
if (relativeLandmark !== filePath) { | ||
continue | ||
} | ||
for (const [subhash] of marks) { | ||
if (subhash !== '') { | ||
dictionary.push(subhash) | ||
} | ||
} | ||
} | ||
const suggestion = propose(hash ? hash : filePath, dictionary, { | ||
threshold: 0.7 | ||
}) | ||
if (suggestion) { | ||
reason += '; did you mean `' + suggestion + '`' | ||
} | ||
let index = -1 | ||
while (++index < reference.nodes.length) { | ||
const node = reference.nodes[index] | ||
const message = reference.file.message(reason, { | ||
place: node.position, | ||
source: origin, | ||
ruleId | ||
}) | ||
message.url = 'https://github.com/remarkjs/remark-validate-links#readme' | ||
} | ||
} | ||
/** | ||
* @param {string} value | ||
* URL. | ||
* @param {State} state | ||
* State. | ||
* @param {Resources['type']} type | ||
* Type of node (`'link'` or `'image'`). | ||
* @returns {Reference | undefined} | ||
* Reference. | ||
*/ | ||
// eslint-disable-next-line complexity | ||
function urlToPath(value, state, type) { | ||
// Absolute paths: `/wooorm/test/blob/main/directory/example.md`. | ||
if (value.charAt(0) === slash) { | ||
if (!state.urlConfig.hostname) { | ||
return | ||
} | ||
// Create a URL. | ||
value = https + slashes + state.urlConfig.hostname + value | ||
} | ||
/** @type {URL | undefined} */ | ||
let url | ||
try { | ||
url = new URL(value) | ||
} catch {} | ||
// URLs: `https://github.com/wooorm/test/blob/main/directory/example.md`. | ||
if (url && state.root) { | ||
// Exit if we don’t have hosted Git info or this is not a URL to the repo. | ||
if ( | ||
!state.urlConfig.prefix || | ||
!state.urlConfig.hostname || | ||
(url.protocol !== https && url.protocol !== http) || | ||
url.hostname !== state.urlConfig.hostname || | ||
url.pathname.slice(0, state.urlConfig.prefix.length) !== | ||
state.urlConfig.prefix | ||
) { | ||
return | ||
} | ||
value = url.pathname.slice(state.urlConfig.prefix.length) | ||
// Things get interesting here: branches: `foo/bar/baz` could be `baz` on | ||
// the `foo/bar` branch, or, `baz` in the `bar` directory on the `foo` | ||
// branch. | ||
// Currently, we’re ignoring this and just not supporting branches. | ||
value = value.split(slash).slice(1).join(slash) | ||
return normalize( | ||
path.resolve(state.root, value + (type === 'image' ? '' : url.hash)), | ||
state | ||
) | ||
} | ||
// Remove the search: `?foo=bar`. | ||
// But don’t remove stuff if it’s in the hash: `readme.md#heading?`. | ||
let numberSignIndex = value.indexOf(numberSign) | ||
const questionMarkIndex = value.indexOf(questionMark) | ||
if ( | ||
questionMarkIndex !== -1 && | ||
(numberSignIndex === -1 || numberSignIndex > questionMarkIndex) | ||
) { | ||
value = | ||
value.slice(0, questionMarkIndex) + | ||
(numberSignIndex === -1 ? '' : value.slice(numberSignIndex)) | ||
numberSignIndex = value.indexOf(numberSign) | ||
} | ||
// Ignore "headings" in image links: `image.png#metadata` | ||
if (numberSignIndex !== -1 && type === 'image') { | ||
value = value.slice(0, numberSignIndex) | ||
} | ||
// Local: `#heading`. | ||
if (value.charAt(0) === numberSign) { | ||
value = state.path ? state.path + value : value | ||
} | ||
// Anything else, such as `readme.md`. | ||
else { | ||
value = state.path ? path.resolve(state.base, value) : '' | ||
} | ||
return normalize(value, state) | ||
} | ||
/** | ||
* @param {string} url | ||
* URL. | ||
* @param {State} state | ||
* State. | ||
* @returns {Reference} | ||
* Reference. | ||
*/ | ||
function normalize(url, state) { | ||
const numberSignIndex = url.indexOf(numberSign) | ||
const lines = state.urlConfig.lines | ||
const prefix = state.urlConfig.headingPrefix | ||
const topAnchor = state.urlConfig.topAnchor | ||
/** @type {string} */ | ||
let filePath | ||
/** @type {string | undefined} */ | ||
let hash | ||
if (numberSignIndex === -1) { | ||
filePath = url | ||
} else { | ||
filePath = url.slice(0, numberSignIndex) | ||
hash = url.slice(numberSignIndex).toLowerCase() | ||
// Ignore the hash if it references the top anchor of the environment | ||
if (topAnchor && hash === topAnchor) { | ||
hash = undefined | ||
} | ||
// Ignore the hash if it references lines in a file or doesn’t start | ||
// with a heading prefix. | ||
else if ( | ||
prefix && | ||
((lines && lineExpression.test(hash)) || | ||
hash.slice(0, prefix.length) !== prefix) | ||
) { | ||
hash = undefined | ||
} | ||
// Use the hash if it starts with a heading prefix. | ||
else if (prefix) { | ||
hash = hash.slice(prefix.length) | ||
} | ||
} | ||
return {filePath: decodeURIComponent(filePath), hash} | ||
} | ||
/** | ||
* @param {string} filePath | ||
* Absolute path to file. | ||
* @returns {boolean} | ||
* Whether `filePath` is a readme. | ||
*/ | ||
function readme(filePath) { | ||
const ext = path.extname(filePath) | ||
return ( | ||
readmeExtensions.has(ext) && | ||
readmeBasename.test(path.basename(filePath, ext)) | ||
) | ||
} |
135
package.json
{ | ||
"name": "remark-validate-links", | ||
"version": "10.0.4", | ||
"version": "13.0.0", | ||
"description": "remark plugin to validate links to headings and files", | ||
"license": "MIT", | ||
"keywords": [ | ||
"unified", | ||
"file", | ||
"heading", | ||
"link", | ||
"markdown", | ||
"mdast", | ||
"plugin", | ||
"reference", | ||
"remark", | ||
"remark-plugin", | ||
"plugin", | ||
"mdast", | ||
"markdown", | ||
"validate", | ||
"link", | ||
"reference", | ||
"file", | ||
"heading" | ||
"unified", | ||
"validate" | ||
], | ||
@@ -35,69 +35,92 @@ "repository": "remarkjs/remark-validate-links", | ||
], | ||
"browser": { | ||
"./lib/check/check-files.js": "./lib/check/check-files.browser.js", | ||
"./lib/find/find-repo.js": "./lib/find/find-repo.browser.js" | ||
"sideEffects": false, | ||
"type": "module", | ||
"exports": "./index.js", | ||
"imports": { | ||
"#check-files": { | ||
"node": "./lib/check-files.node.js", | ||
"default": "./lib/check-files.default.js" | ||
}, | ||
"#find-repo": { | ||
"node": "./lib/find-repo.node.js", | ||
"default": "./lib/find-repo.default.js" | ||
} | ||
}, | ||
"files": [ | ||
"lib/", | ||
"index.d.ts", | ||
"index.js" | ||
], | ||
"dependencies": { | ||
"github-slugger": "^1.0.0", | ||
"hosted-git-info": "^3.0.0", | ||
"mdast-util-to-string": "^1.0.0", | ||
"@types/mdast": "^4.0.0", | ||
"github-slugger": "^2.0.0", | ||
"hosted-git-info": "^7.0.0", | ||
"mdast-util-to-hast": "^13.0.0", | ||
"mdast-util-to-string": "^4.0.0", | ||
"propose": "0.0.5", | ||
"to-vfile": "^6.0.0", | ||
"trough": "^1.0.0", | ||
"unist-util-visit": "^2.0.0" | ||
"trough": "^2.0.0", | ||
"unified-engine": "^11.0.0", | ||
"unist-util-visit": "^5.0.0", | ||
"vfile": "^6.0.0" | ||
}, | ||
"devDependencies": { | ||
"nyc": "^15.0.0", | ||
"prettier": "^2.0.0", | ||
"remark": "^13.0.0", | ||
"remark-cli": "^9.0.0", | ||
"remark-preset-wooorm": "^8.0.0", | ||
"rimraf": "^3.0.0", | ||
"strip-ansi": "^6.0.0", | ||
"tape": "^5.0.0", | ||
"vfile-sort": "^2.0.0", | ||
"xo": "^0.38.0" | ||
"@types/hosted-git-info": "^3.0.0", | ||
"@types/node": "^20.0.0", | ||
"c8": "^8.0.0", | ||
"prettier": "^3.0.0", | ||
"remark": "^15.0.0", | ||
"remark-cli": "^12.0.0", | ||
"remark-preset-wooorm": "^9.0.0", | ||
"strip-ansi": "^7.0.0", | ||
"to-vfile": "^8.0.0", | ||
"type-coverage": "^2.0.0", | ||
"typescript": "^5.0.0", | ||
"vfile-sort": "^4.0.0", | ||
"xo": "^0.56.0" | ||
}, | ||
"scripts": { | ||
"format": "remark . -qfo --ignore-pattern test/ && prettier . -w --loglevel warn && xo --fix", | ||
"test-api": "node test", | ||
"test-coverage": "nyc --reporter lcov tape test/index.js", | ||
"test": "npm run format && npm run test-coverage" | ||
"build": "tsc --build --clean && tsc --build && type-coverage", | ||
"#": "remark . --frail --output --quiet", | ||
"format": "prettier . --log-level warn --write && xo --fix", | ||
"prepack": "npm run build && npm run format", | ||
"test": "npm run build && npm run format && npm run test-coverage", | ||
"test-api": "node --conditions development test/index.js", | ||
"test-coverage": "c8 --100 --reporter lcov npm run test-api" | ||
}, | ||
"nyc": { | ||
"check-coverage": true, | ||
"lines": 100, | ||
"functions": 100, | ||
"branches": 100 | ||
}, | ||
"prettier": { | ||
"tabWidth": 2, | ||
"useTabs": false, | ||
"bracketSpacing": false, | ||
"singleQuote": true, | ||
"bracketSpacing": false, | ||
"semi": false, | ||
"trailingComma": "none" | ||
"tabWidth": 2, | ||
"trailingComma": "none", | ||
"useTabs": false | ||
}, | ||
"remarkConfig": { | ||
"plugins": [ | ||
"remark-preset-wooorm" | ||
] | ||
}, | ||
"typeCoverage": { | ||
"atLeast": 100, | ||
"detail": true, | ||
"ignoreCatch": true, | ||
"strict": true | ||
}, | ||
"xo": { | ||
"overrides": [ | ||
{ | ||
"files": [ | ||
"test/**/*.js" | ||
], | ||
"rules": { | ||
"no-await-in-loop": "off" | ||
} | ||
} | ||
], | ||
"prettier": true, | ||
"esnext": false, | ||
"rules": { | ||
"complexity": "off", | ||
"guard-for-in": "off", | ||
"unicorn/no-array-callback-reference": "off", | ||
"unicorn/no-array-for-each": "off", | ||
"unicorn/prefer-number-properties": "off", | ||
"unicorn/prefer-optional-catch-binding": "off", | ||
"unicorn/prefer-includes": "off" | ||
"unicorn/prefer-logical-operator-over-ternary": "off", | ||
"unicorn/prefer-string-replace-all": "off" | ||
} | ||
}, | ||
"remarkConfig": { | ||
"plugins": [ | ||
"preset-wooorm" | ||
] | ||
} | ||
} |
400
readme.md
@@ -6,3 +6,2 @@ # remark-validate-links | ||
[![Downloads][downloads-badge]][downloads] | ||
[![Size][size-badge]][size] | ||
[![Sponsors][sponsors-badge]][collective] | ||
@@ -12,33 +11,30 @@ [![Backers][backers-badge]][collective] | ||
[**remark**][remark] plugin to validate that Markdown links and images reference | ||
existing local files and headings. | ||
**[remark][]** plugin to check that markdown links and images point to existing | ||
local files and headings in a Git repo. | ||
For example, this document does not have a heading named `Hello`. | ||
So if we’d link to it (`[welcome](#hello)`), we’d get a warning. | ||
Links to headings in other markdown documents (`examples/foo.md#hello`) and | ||
links to files (`license` or `index.js`) are also checked. | ||
In addition, when there’s a link to a heading in another document | ||
(`examples/foo.md#hello`), if that file exists but the heading does not, or if | ||
that file does not exist, we’d also get a warning. | ||
This is specifically for Git repos. | ||
Like this one. | ||
Not for say a website. | ||
Linking to other files, such as `license` or `index.js` (when they exist) is | ||
fine. | ||
This plugin does not check external URLs (see | ||
[`remark-lint-no-dead-urls`][no-dead-urls]) or undefined references | ||
(see [`remark-lint-no-undefined-references`][no-undef-refs]). | ||
## Note! | ||
This plugin is ready for the new parser in remark | ||
([`remarkjs/remark#536`](https://github.com/remarkjs/remark/pull/536)). | ||
No change is needed: it works exactly the same now as it did before! | ||
## Contents | ||
* [What is this?](#what-is-this) | ||
* [When should I use this?](#when-should-i-use-this) | ||
* [Install](#install) | ||
* [Use](#use) | ||
* [CLI](#cli) | ||
* [API](#api) | ||
* [Configuration](#configuration) | ||
* [API](#api) | ||
* [`unified().use(remarkValidateLinks[, options])`](#unifieduseremarkvalidatelinks-options) | ||
* [`Options`](#options) | ||
* [`UrlConfig`](#urlconfig) | ||
* [Examples](#examples) | ||
* [Example: CLI](#example-cli) | ||
* [Example: CLI in npm scripts](#example-cli-in-npm-scripts) | ||
* [Integration](#integration) | ||
* [Types](#types) | ||
* [Compatibility](#compatibility) | ||
* [Security](#security) | ||
@@ -49,57 +45,47 @@ * [Related](#related) | ||
## Install | ||
## What is this? | ||
[npm][]: | ||
This package is a [unified][] ([remark][]) plugin to check local links in a Git | ||
repo. | ||
```sh | ||
npm install remark-validate-links | ||
``` | ||
## When should I use this? | ||
## Use | ||
This project is useful if you have a Git repo, such as this one, with docs in | ||
markdown and links to headings and other files, and want to check whether | ||
they’re correct. | ||
Compared to other links checkers, this project can work offline (making it fast | ||
en prone to fewer false positives), and is specifically made for local links in | ||
Git repos. | ||
This plugin does not check external URLs (see | ||
[`remark-lint-no-dead-urls`][remark-lint-no-dead-urls]) or undefined references | ||
(see | ||
[`remark-lint-no-undefined-references`][remark-lint-no-undefined-references]). | ||
### CLI | ||
## Install | ||
Use `remark-validate-links` together with [**remark**][remark]: | ||
This package is [ESM only][esm]. | ||
In Node.js (version 16+), install with [npm][]: | ||
```bash | ||
npm install --global remark-cli remark-validate-links | ||
```sh | ||
npm install remark-validate-links | ||
``` | ||
Let’s say `readme.md` is this document, and `example.md` looks as follows: | ||
In Deno with [`esm.sh`][esmsh]: | ||
```markdown | ||
# Hello | ||
Read more [whoops, this does not exist](#world). | ||
This doesn’t exist either [whoops!](readme.md#foo). | ||
But this does exist: [license](license). | ||
So does this: [README](readme.md#installation). | ||
```js | ||
import remarkValidateLinks from 'https://esm.sh/remark-validate-links@13' | ||
``` | ||
Now, running `remark -u validate-links .` yields: | ||
In browsers with [`esm.sh`][esmsh]: | ||
```text | ||
example.md | ||
3:11-3:48 warning Link to unknown heading: `world` missing-heading remark-validate-links | ||
5:27-5:51 warning Link to unknown heading in `readme.md`: `foo` missing-heading-in-file remark-validate-links | ||
readme.md: no issues found | ||
⚠ 2 warnings | ||
```html | ||
<script type="module"> | ||
import remarkValidateLinks from 'https://esm.sh/remark-validate-links@13?bundle' | ||
</script> | ||
``` | ||
> Note: passing a file over stdin(4) may not work as expected, because it is not | ||
> known where the file originates from. | ||
## Use | ||
### API | ||
Say we have the following file `example.md` in this project: | ||
> Note: The API checks links to headings and files. | ||
> It does not check headings in other files. | ||
> In a browser, only local links to headings are checked. | ||
Say we have the following file, `example.md`: | ||
```markdown | ||
@@ -111,7 +97,7 @@ # Alpha | ||
This [exists](#alpha). | ||
This [one does not](#does-not). | ||
This [one does not](#apha). | ||
# Bravo | ||
Headings in `readme.md` are [checked](readme.md#nosuchheading). | ||
Headings in `readme.md` are [checked](readme.md#no-such-heading). | ||
And [missing files are reported](missing-example.js). | ||
@@ -127,24 +113,24 @@ | ||
And our script, `example.js`, looks as follows: | ||
…and a module `example.js`: | ||
```js | ||
var vfile = require('to-vfile') | ||
var report = require('vfile-reporter') | ||
var remark = require('remark') | ||
var links = require('remark-validate-links') | ||
import {remark} from 'remark' | ||
import remarkValidateLinks from 'remark-validate-links' | ||
import {read} from 'to-vfile' | ||
import {reporter} from 'vfile-reporter' | ||
remark() | ||
.use(links) | ||
.process(vfile.readSync('example.md'), function (err, file) { | ||
console.error(report(err || file)) | ||
}) | ||
const file = await remark() | ||
.use(remarkValidateLinks) | ||
.process(await read('example.md')) | ||
console.log(reporter(file)) | ||
``` | ||
Now, running `node example` yields: | ||
…then running `node example.js` yields: | ||
```markdown | ||
example.md | ||
6:6-6:31 warning Link to unknown heading: `does-not` missing-heading remark-validate-links | ||
11:5-11:53 warning Link to unknown file: `missing-example.js` missing-file remark-validate-links | ||
16:1-16:20 warning Link to unknown heading: `charlie` missing-heading remark-validate-links | ||
6:6-6:27 warning Cannot find heading for `#apha`; did you mean `alpha` missing-heading remark-validate-links:missing-heading | ||
11:5-11:53 warning Cannot find file `missing-example.js` missing-file remark-validate-links:missing-file | ||
16:1-16:20 warning Cannot find heading for `#charlie` missing-heading remark-validate-links:missing-heading | ||
@@ -154,52 +140,77 @@ ⚠ 3 warnings | ||
(Note that `readme.md#nosuchheading` is not warned about, because the API | ||
does not check headings in other Markdown files). | ||
> 👉 **Note**: `readme.md#no-such-heading` is not warned about on the API, as it | ||
> does not check headings in other markdown files. | ||
> The remark CLI is able to do that. | ||
## Configuration | ||
## API | ||
Typically, you don’t need to configure `remark-validate-links`, as it detects | ||
local Git repositories. | ||
If one is detected that references a known Git host (GitHub, GitLab, | ||
or Bitbucket), some extra links can be checked. | ||
If one is detected that does not reference a known Git host, local links still | ||
work as expected. | ||
If you’re not in a Git repository, you must pass `repository: false` explicitly. | ||
This package exports no identifiers. | ||
The default export is [`remarkValidateLinks`][api-remark-validate-links]. | ||
You can pass a `repository` (`string?`, `false`). | ||
If `repository` is nullish, the Git origin remote is detected. | ||
If the repository resolves to something [npm understands][package-repository] | ||
as a Git host such as GitHub, GitLab, or Bitbucket, full URLs to that host | ||
(say, `https://github.com/remarkjs/remark-validate-links/readme.md#install`) | ||
can also be checked. | ||
### `unified().use(remarkValidateLinks[, options])` | ||
```sh | ||
remark --use 'validate-links=repository:"foo/bar"' example.md | ||
``` | ||
Check that markdown links and images point to existing local files and headings | ||
in a Git repo. | ||
For this to work, a `root` (`string?`) is also used, referencing the local Git | ||
root directory (the place where `.git` is). | ||
If both `root` and `repository` are nullish, the Git root is detected. | ||
If `root` is not given but `repository` is, [`file.cwd`][cwd] is used. | ||
> ⚠️ **Important**: The API in Node.js checks links to headings and files but | ||
> does not check whether headings in other files exist. | ||
> The API in browsers only checks links to headings in the same file. | ||
> The CLI can check everything. | ||
You can define this repository in [configuration files][cli] too. | ||
An example `.remarkrc` file could look as follows: | ||
###### Parameters | ||
```json | ||
{ | ||
"plugins": [ | ||
[ | ||
"validate-links", | ||
{ | ||
"repository": "foo/bar" | ||
} | ||
] | ||
] | ||
} | ||
``` | ||
* `options` ([`Options`][api-options], optional) | ||
— configuration | ||
If you’re self-hosting a Git server, you can provide URL information directly, | ||
as `urlConfig` (`Object`). | ||
###### Returns | ||
For this repository, `urlConfig` looks as follows: | ||
Transform ([`Transformer`][unified-transformer]). | ||
### `Options` | ||
Configuration (TypeScript type). | ||
###### Fields | ||
* `repository` (`string` or `false`, default: detected from Git remote) | ||
— URL to hosted Git; | ||
if you’re not in a Git repository, you must pass `false`; | ||
if the repository resolves to something npm understands as a Git host such | ||
as GitHub, GitLab, or Bitbucket, full URLs to that host (say, | ||
`https://github.com/remarkjs/remark-validate-links/readme.md#install`) are | ||
checked | ||
* `root` (`string`, default: local Git folder) | ||
— path to Git root folder; | ||
if both `root` and `repository` are nullish, the Git root is detected; | ||
if `root` is not given but `repository` is, `file.cwd` is used | ||
* `urlConfig` ([`UrlConfig`][api-url-config], default: detected from repo) | ||
— config on how hosted Git works; | ||
`github.com`, `gitlab.com`, or `bitbucket.org` work automatically; | ||
otherwise, pass `urlConfig` manually | ||
### `UrlConfig` | ||
Hosted Git info (TypeScript type). | ||
###### Fields | ||
* `headingPrefix` (`string`, optional, example: `'#'`, `'#markdown-header-'`) | ||
— prefix of headings | ||
* `hostname` (`string`, optional, example: `'github.com'`, | ||
`'bitbucket.org'`) | ||
— domain of URLs | ||
* `lines` (`boolean`, default: `false`) | ||
— whether lines in files can be linked | ||
* `path` (`string`, optional, example: | ||
`'/remarkjs/remark-validate-links/blob/'`, | ||
`'/remarkjs/remark-validate-links/src/'`) | ||
— path prefix before files | ||
* `topAnchor` (`string`, optional, example: `#readme`) | ||
— hash to top of readme | ||
###### Notes | ||
For this repository (`remarkjs/remark-validate-links` on GitHub) `urlConfig` | ||
looks as follows: | ||
```js | ||
@@ -213,2 +224,4 @@ { | ||
headingPrefix: '#', | ||
// Hash to top of markdown documents: | ||
topAnchor: '#readme', | ||
// Whether lines in files can be linked: | ||
@@ -230,2 +243,84 @@ lines: true | ||
## Examples | ||
### Example: CLI | ||
It’s recommended to use `remark-validate-links` on the CLI with | ||
[`remark-cli`][remark-cli]. | ||
Install both with [npm][]: | ||
```sh | ||
npm install remark-cli remark-validate-links --save-dev | ||
``` | ||
Let’s say we have a `readme.md` (this current document) and an `example.md` | ||
with the following text: | ||
```markdown | ||
# Hello | ||
Read more [whoops, this does not exist](#world). | ||
This doesn’t exist either [whoops!](readme.md#foo). | ||
But this does exist: [license](license). | ||
So does this: [readme](readme.md#install). | ||
``` | ||
Now, running `./node_modules/.bin/remark --use remark-validate-links .` yields: | ||
<!-- To do: regenerate. --> | ||
```txt | ||
example.md | ||
3:11-3:48 warning Link to unknown heading: `world` missing-heading remark-validate-links | ||
5:27-5:51 warning Link to unknown heading in `readme.md`: `foo` missing-heading-in-file remark-validate-links | ||
readme.md: no issues found | ||
⚠ 2 warnings | ||
``` | ||
### Example: CLI in npm scripts | ||
You can use `remark-validate-links` and [`remark-cli`][remark-cli] in an npm | ||
script to check and format markdown in your project. | ||
Install both with [npm][]: | ||
```sh | ||
npm install remark-cli remark-validate-links --save-dev | ||
``` | ||
Then, add a format script and configuration to `package.json`: | ||
```js | ||
{ | ||
// … | ||
"scripts": { | ||
// … | ||
"format": "remark . --quiet --frail --output", | ||
// … | ||
}, | ||
"remarkConfig": { | ||
"plugins": [ | ||
"remark-validate-links" | ||
] | ||
}, | ||
// … | ||
} | ||
``` | ||
> 💡 **Tip**: Add other tools such as prettier or ESLint to check and format | ||
> other files. | ||
> | ||
> 💡 **Tip**: Run `./node_modules/.bin/remark --help` for help with | ||
> `remark-cli`. | ||
Now you check and format markdown in your project with: | ||
```sh | ||
npm run format | ||
``` | ||
## Integration | ||
@@ -236,9 +331,30 @@ | ||
* `node.data.hProperties.name` — Used by [`remark-html`][remark-html] | ||
* `node.data.hProperties.name` — Used by | ||
[`mdast-util-to-hast`][mdast-util-to-hast] | ||
to create a `name` attribute, which anchors can link to | ||
* `node.data.hProperties.id` — Used by [`remark-html`][remark-html] | ||
* `node.data.hProperties.id` — Used by | ||
[`mdast-util-to-hast`][mdast-util-to-hast] | ||
to create an `id` attribute, which anchors can link to | ||
* `node.data.id` — Used, in the future, by other tools to signal | ||
* `node.data.id` — Used potentially in the future by other tools to signal | ||
unique identifiers on nodes | ||
## Types | ||
This package is fully typed with [TypeScript][]. | ||
It exports the additional types [`Options`][api-options] and | ||
[`UrlConfig`][api-url-config]. | ||
## Compatibility | ||
Projects maintained by the unified collective are compatible with maintained | ||
versions of Node.js. | ||
When we cut a new major release, we drop support for unmaintained versions of | ||
Node. | ||
This means we try to keep the current release line, | ||
`remark-validate-links@^13`, compatible with Node.js 16. | ||
This plugin works with `unified` version 6+, `remark` version 7+, and | ||
`remark-cli` version 8+. | ||
## Security | ||
@@ -251,8 +367,10 @@ | ||
The tree is not modified, so there are no openings for | ||
[cross-site scripting (XSS)][xss] attacks. | ||
[cross-site scripting (XSS)][wiki-xss] attacks. | ||
## Related | ||
* [`remark-lint`][remark-lint] — Markdown code style linter | ||
* [`remark-lint-no-dead-urls`][no-dead-urls] — Ensure external links are alive | ||
* [`remark-lint`][remark-lint] | ||
— markdown code style linter | ||
* [`remark-lint-no-dead-urls`][remark-lint-no-dead-urls] | ||
— check that external links are alive | ||
@@ -287,6 +405,2 @@ ## Contribute | ||
[size-badge]: https://img.shields.io/bundlephobia/minzip/remark-validate-links.svg | ||
[size]: https://bundlephobia.com/result?p=remark-validate-links | ||
[sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg | ||
@@ -304,9 +418,13 @@ | ||
[esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c | ||
[esmsh]: https://esm.sh | ||
[health]: https://github.com/remarkjs/.github | ||
[contributing]: https://github.com/remarkjs/.github/blob/HEAD/contributing.md | ||
[contributing]: https://github.com/remarkjs/.github/blob/main/contributing.md | ||
[support]: https://github.com/remarkjs/.github/blob/HEAD/support.md | ||
[support]: https://github.com/remarkjs/.github/blob/main/support.md | ||
[coc]: https://github.com/remarkjs/.github/blob/HEAD/code-of-conduct.md | ||
[coc]: https://github.com/remarkjs/.github/blob/main/code-of-conduct.md | ||
@@ -317,18 +435,26 @@ [license]: license | ||
[mdast-util-to-hast]: https://github.com/syntax-tree/mdast-util-to-hast#notes | ||
[remark]: https://github.com/remarkjs/remark | ||
[cli]: https://github.com/remarkjs/remark/tree/HEAD/packages/remark-cli#readme | ||
[remark-cli]: https://github.com/remarkjs/remark/tree/main/packages/remark-cli#readme | ||
[remark-lint]: https://github.com/remarkjs/remark-lint | ||
[remark-html]: https://github.com/remarkjs/remark-html | ||
[remark-lint-no-dead-urls]: https://github.com/remarkjs/remark-lint-no-dead-urls | ||
[no-dead-urls]: https://github.com/davidtheclark/remark-lint-no-dead-urls | ||
[remark-lint-no-undefined-references]: https://github.com/remarkjs/remark-lint/tree/master/packages/remark-lint-no-undefined-references | ||
[no-undef-refs]: https://github.com/remarkjs/remark-lint/tree/master/packages/remark-lint-no-undefined-references | ||
[typescript]: https://www.typescriptlang.org | ||
[package-repository]: https://docs.npmjs.com/files/package.json#repository | ||
[unified]: https://github.com/unifiedjs/unified | ||
[cwd]: https://github.com/vfile/vfile#vfilecwd | ||
[unified-transformer]: https://github.com/unifiedjs/unified#transformer | ||
[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting | ||
[wiki-xss]: https://en.wikipedia.org/wiki/Cross-site_scripting | ||
[api-options]: #options | ||
[api-remark-validate-links]: #unifieduseremarkvalidatelinks-options | ||
[api-url-config]: #urlconfig |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
46484
990
448
1
Yes
10
13
+ Added@types/mdast@^4.0.0
+ Addedmdast-util-to-hast@^13.0.0
+ Addedunified-engine@^11.0.0
+ Addedvfile@^6.0.0
+ Added@babel/code-frame@7.25.9(transitive)
+ Added@babel/helper-validator-identifier@7.25.9(transitive)
+ Added@babel/highlight@7.25.9(transitive)
+ Added@isaacs/cliui@8.0.2(transitive)
+ Added@npmcli/config@8.3.4(transitive)
+ Added@npmcli/git@5.0.8(transitive)
+ Added@npmcli/map-workspaces@3.0.6(transitive)
+ Added@npmcli/name-from-folder@2.0.0(transitive)
+ Added@npmcli/package-json@5.2.1(transitive)
+ Added@npmcli/promise-spawn@7.0.2(transitive)
+ Added@pkgjs/parseargs@0.11.0(transitive)
+ Added@types/concat-stream@2.0.3(transitive)
+ Added@types/debug@4.1.12(transitive)
+ Added@types/hast@3.0.4(transitive)
+ Added@types/is-empty@1.2.3(transitive)
+ Added@types/mdast@4.0.4(transitive)
+ Added@types/ms@0.7.34(transitive)
+ Added@types/node@20.16.15(transitive)
+ Added@types/supports-color@8.1.3(transitive)
+ Added@types/unist@3.0.3(transitive)
+ Added@ungap/structured-clone@1.2.0(transitive)
+ Addedabbrev@2.0.0(transitive)
+ Addedansi-regex@5.0.16.1.0(transitive)
+ Addedansi-styles@3.2.14.3.06.2.1(transitive)
+ Addedbalanced-match@1.0.2(transitive)
+ Addedbrace-expansion@2.0.1(transitive)
+ Addedbuffer-from@1.1.2(transitive)
+ Addedchalk@2.4.2(transitive)
+ Addedci-info@4.0.0(transitive)
+ Addedcolor-convert@1.9.32.0.1(transitive)
+ Addedcolor-name@1.1.31.1.4(transitive)
+ Addedconcat-stream@2.0.0(transitive)
+ Addedcross-spawn@7.0.3(transitive)
+ Addeddebug@4.3.7(transitive)
+ Addeddequal@2.0.3(transitive)
+ Addeddevlop@1.1.0(transitive)
+ Addedeastasianwidth@0.2.0(transitive)
+ Addedemoji-regex@10.4.08.0.09.2.2(transitive)
+ Addederr-code@2.0.3(transitive)
+ Addederror-ex@1.3.2(transitive)
+ Addedescape-string-regexp@1.0.5(transitive)
+ Addedextend@3.0.2(transitive)
+ Addedforeground-child@3.3.0(transitive)
+ Addedgithub-slugger@2.0.0(transitive)
+ Addedglob@10.4.5(transitive)
+ Addedhas-flag@3.0.0(transitive)
+ Addedhosted-git-info@7.0.2(transitive)
+ Addedignore@5.3.2(transitive)
+ Addedimport-meta-resolve@4.1.0(transitive)
+ Addedinherits@2.0.4(transitive)
+ Addedini@4.1.3(transitive)
+ Addedis-arrayish@0.2.1(transitive)
+ Addedis-empty@1.2.0(transitive)
+ Addedis-fullwidth-code-point@3.0.0(transitive)
+ Addedis-plain-obj@4.1.0(transitive)
+ Addedisexe@2.0.03.1.1(transitive)
+ Addedjackspeak@3.4.3(transitive)
+ Addedjs-tokens@4.0.0(transitive)
+ Addedjson-parse-even-better-errors@3.0.2(transitive)
+ Addedlines-and-columns@2.0.4(transitive)
+ Addedload-plugin@6.0.3(transitive)
+ Addedlru-cache@10.4.3(transitive)
+ Addedmdast-util-to-hast@13.2.0(transitive)
+ Addedmdast-util-to-string@4.0.0(transitive)
+ Addedmicromark-util-character@2.1.0(transitive)
+ Addedmicromark-util-encode@2.0.0(transitive)
+ Addedmicromark-util-sanitize-uri@2.0.0(transitive)
+ Addedmicromark-util-symbol@2.0.0(transitive)
+ Addedmicromark-util-types@2.0.0(transitive)
+ Addedminimatch@9.0.5(transitive)
+ Addedminipass@7.1.2(transitive)
+ Addedms@2.1.3(transitive)
+ Addednopt@7.2.1(transitive)
+ Addednormalize-package-data@6.0.2(transitive)
+ Addednpm-install-checks@6.3.0(transitive)
+ Addednpm-normalize-package-bin@3.0.1(transitive)
+ Addednpm-package-arg@11.0.3(transitive)
+ Addednpm-pick-manifest@9.1.0(transitive)
+ Addedpackage-json-from-dist@1.0.1(transitive)
+ Addedparse-json@7.1.1(transitive)
+ Addedpath-key@3.1.1(transitive)
+ Addedpath-scurry@1.11.1(transitive)
+ Addedpicocolors@1.1.1(transitive)
+ Addedproc-log@4.2.0(transitive)
+ Addedpromise-inflight@1.0.1(transitive)
+ Addedpromise-retry@2.0.1(transitive)
+ Addedread-package-json-fast@3.0.2(transitive)
+ Addedreadable-stream@3.6.2(transitive)
+ Addedretry@0.12.0(transitive)
+ Addedsafe-buffer@5.2.1(transitive)
+ Addedsemver@7.6.3(transitive)
+ Addedshebang-command@2.0.0(transitive)
+ Addedshebang-regex@3.0.0(transitive)
+ Addedsignal-exit@4.1.0(transitive)
+ Addedspdx-correct@3.2.0(transitive)
+ Addedspdx-exceptions@2.5.0(transitive)
+ Addedspdx-expression-parse@3.0.1(transitive)
+ Addedspdx-license-ids@3.0.20(transitive)
+ Addedstring-width@4.2.35.1.26.1.0(transitive)
+ Addedstring_decoder@1.3.0(transitive)
+ Addedstrip-ansi@6.0.17.1.0(transitive)
+ Addedsupports-color@5.5.09.4.0(transitive)
+ Addedtrim-lines@3.0.1(transitive)
+ Addedtrough@2.2.0(transitive)
+ Addedtype-fest@3.13.1(transitive)
+ Addedtypedarray@0.0.6(transitive)
+ Addedundici-types@6.19.8(transitive)
+ Addedunified-engine@11.2.1(transitive)
+ Addedunist-util-inspect@8.1.0(transitive)
+ Addedunist-util-is@6.0.0(transitive)
+ Addedunist-util-position@5.0.0(transitive)
+ Addedunist-util-stringify-position@4.0.0(transitive)
+ Addedunist-util-visit@5.0.0(transitive)
+ Addedunist-util-visit-parents@6.0.1(transitive)
+ Addedutil-deprecate@1.0.2(transitive)
+ Addedvalidate-npm-package-license@3.0.4(transitive)
+ Addedvalidate-npm-package-name@5.0.1(transitive)
+ Addedvfile@6.0.3(transitive)
+ Addedvfile-message@4.0.2(transitive)
+ Addedvfile-reporter@8.1.1(transitive)
+ Addedvfile-sort@4.0.0(transitive)
+ Addedvfile-statistics@3.0.0(transitive)
+ Addedwalk-up-path@3.0.1(transitive)
+ Addedwhich@2.0.24.0.0(transitive)
+ Addedwrap-ansi@7.0.08.1.0(transitive)
+ Addedyaml@2.6.0(transitive)
- Removedto-vfile@^6.0.0
- Removed@types/unist@2.0.11(transitive)
- Removedgithub-slugger@1.5.0(transitive)
- Removedhosted-git-info@3.0.8(transitive)
- Removedis-buffer@2.0.5(transitive)
- Removedlru-cache@6.0.0(transitive)
- Removedmdast-util-to-string@1.1.0(transitive)
- Removedto-vfile@6.1.0(transitive)
- Removedtrough@1.0.5(transitive)
- Removedunist-util-is@4.1.0(transitive)
- Removedunist-util-stringify-position@2.0.3(transitive)
- Removedunist-util-visit@2.0.3(transitive)
- Removedunist-util-visit-parents@3.1.1(transitive)
- Removedvfile@4.2.1(transitive)
- Removedvfile-message@2.0.4(transitive)
- Removedyallist@4.0.0(transitive)
Updatedgithub-slugger@^2.0.0
Updatedhosted-git-info@^7.0.0
Updatedmdast-util-to-string@^4.0.0
Updatedtrough@^2.0.0
Updatedunist-util-visit@^5.0.0