0
# remark-validate-links
1
2
remark-validate-links is a unified remark plugin that validates local markdown links and images in Git repositories. It checks that links point to existing files and headings, working offline for fast and reliable link validation. The plugin integrates with the remark ecosystem and provides Git-aware validation for hosted services like GitHub, GitLab, and Bitbucket.
3
4
## Package Information
5
6
- **Package Name**: remark-validate-links
7
- **Package Type**: npm
8
- **Language**: JavaScript (with TypeScript definitions)
9
- **Installation**: `npm install remark-validate-links`
10
11
## Core Imports
12
13
```javascript
14
import remarkValidateLinks from "remark-validate-links";
15
```
16
17
For CommonJS:
18
19
```javascript
20
const remarkValidateLinks = require("remark-validate-links");
21
```
22
23
**Note:** This package exports only a default export function - no named exports are available. TypeScript types `Options` and `UrlConfig` are defined via JSDoc comments in the source code for TypeScript support, but cannot be imported at runtime.
24
25
## Basic Usage
26
27
```javascript
28
import remarkValidateLinks from "remark-validate-links";
29
import { remark } from "remark";
30
import { read } from "to-vfile";
31
32
// Basic usage with default options
33
const file = await remark()
34
.use(remarkValidateLinks)
35
.process(await read("example.md"));
36
37
// Usage with configuration options
38
const file = await remark()
39
.use(remarkValidateLinks, {
40
repository: "https://github.com/user/repo",
41
root: "./docs",
42
skipPathPatterns: [/\/temp\//, "ignore-me.md"]
43
})
44
.process(await read("example.md"));
45
```
46
47
## Architecture
48
49
remark-validate-links operates as a remark plugin that:
50
51
- **Link Collection**: Visits all link and image nodes in the markdown AST to collect references
52
- **Repository Detection**: Auto-detects Git repository information and remote URLs using git commands
53
- **Path Resolution**: Converts various link formats (relative, absolute, URLs) to local file paths
54
- **File Validation**: Checks existence of referenced files and directories (Node.js only)
55
- **Heading Validation**: Validates heading anchors within the same file (API) or across files (CLI)
56
- **Error Reporting**: Generates detailed warnings with suggestions for missing references
57
- **Platform Adaptation**: Provides full functionality in Node.js and limited functionality in browsers
58
59
## Capabilities
60
61
### Main Plugin Function
62
63
Creates a remark transformer that validates local links and images in markdown documents.
64
65
```javascript { .api }
66
/**
67
* Check that markdown links and images point to existing local files and headings in a Git repo.
68
*
69
* ⚠️ **Important**: The API in Node.js checks links to headings and files
70
* but does not check whether headings in other files exist.
71
* The API in browsers only checks links to headings in the same file.
72
* The CLI can check everything.
73
*
74
* @param {Options | null | undefined} [options] - Configuration (optional)
75
* @param {FileSet | null | undefined} [fileSet] - File set (optional)
76
* @returns {Function} Transform function
77
*/
78
function remarkValidateLinks(options, fileSet);
79
```
80
81
**Usage Examples:**
82
83
```javascript
84
import remarkValidateLinks from "remark-validate-links";
85
import { remark } from "remark";
86
87
// Basic usage
88
const processor = remark().use(remarkValidateLinks);
89
90
// With options
91
const processor = remark().use(remarkValidateLinks, {
92
repository: false, // disable repository detection
93
skipPathPatterns: [/node_modules/]
94
});
95
96
// For CLI usage with file set - enables cross-file validation
97
const fileSet = new FileSet();
98
const processor = remark().use(remarkValidateLinks, {}, fileSet);
99
// When fileSet is provided:
100
// - Plugin discovers referenced files and adds them for processing
101
// - Cross-file heading validation becomes available
102
// - Directory references resolve to readme files (.md, .markdown, .mdown, .mkdn)
103
```
104
105
### Link Validation Features
106
107
The plugin validates these types of links:
108
109
- **Same-file headings**: `#heading-id`
110
- **Relative file links**: `./path/to/file.md`, `../other.js`
111
- **Files with headings**: `./file.md#heading`
112
- **Absolute paths**: `/path/from/repo/root.md` (when repository is configured)
113
- **Hosted Git URLs**: Full URLs to GitHub/GitLab/Bitbucket files in the same repository
114
- **Line number links**: GitHub and GitLab style line links like `file.js#L123` (when `lines: true`)
115
- **Branch-aware URLs**: Handles branch prefixes in repository URLs (currently assumes main/master branch)
116
117
**Error Detection:**
118
119
- **Missing files**: Reports when linked files don't exist with file paths
120
- **Missing headings**: Reports when heading anchors don't exist within files
121
- **Typo suggestions**: Provides "did you mean" suggestions using edit distance algorithms
122
- **Context-aware errors**: Different error messages for headings vs files vs headings in other files
123
124
## Types
125
126
### Options Interface
127
128
Configuration object for the plugin.
129
130
```javascript { .api }
131
/**
132
* Configuration options for the plugin.
133
*/
134
interface Options {
135
/**
136
* URL to hosted Git (default: detected from Git remote);
137
* if you're not in a Git repository, you must pass `false`;
138
* if the repository resolves to something npm understands as a Git host such
139
* as GitHub, GitLab, or Bitbucket, full URLs to that host (say
140
* `https://github.com/remarkjs/remark-validate-links/readme.md#install`) are
141
* checked.
142
*/
143
repository?: string | false | null | undefined;
144
/**
145
* Path to Git root folder (default: local Git folder);
146
* if both `root` and `repository` are nullish, the Git root is detected;
147
* if `root` is not given but `repository` is, `file.cwd` is used.
148
*/
149
root?: string | null | undefined;
150
/**
151
* List of patterns for *paths* that should be skipped;
152
* each absolute local path + hash will be tested against each pattern and
153
* will be ignored if `new RegExp(pattern).test(value) === true`;
154
* example values are then `/Users/tilde/path/to/repo/readme.md#some-heading`.
155
*/
156
skipPathPatterns?: ReadonlyArray<RegExp | string> | null | undefined;
157
/**
158
* Config on how hosted Git works (default: detected from repo);
159
* `github.com`, `gitlab.com`, or `bitbucket.org` work automatically;
160
* otherwise, pass `urlConfig` manually.
161
*/
162
urlConfig?: UrlConfig | null | undefined;
163
}
164
```
165
166
### UrlConfig Interface
167
168
Configuration for hosted Git services like GitHub, GitLab, and Bitbucket.
169
170
```javascript { .api }
171
/**
172
* Hosted Git info configuration.
173
*
174
* For this repository (`remarkjs/remark-validate-links` on GitHub)
175
* `urlConfig` looks as follows:
176
*
177
* ```js
178
* {
179
* // Domain of URLs:
180
* hostname: 'github.com',
181
* // Path prefix before files:
182
* prefix: '/remarkjs/remark-validate-links/blob/',
183
* // Prefix of headings:
184
* headingPrefix: '#',
185
* // Hash to top of markdown documents:
186
* topAnchor: '#readme',
187
* // Whether lines in files can be linked:
188
* lines: true
189
* }
190
* ```
191
*
192
* If this project were hosted on Bitbucket, it would be:
193
*
194
* ```js
195
* {
196
* hostname: 'bitbucket.org',
197
* prefix: '/remarkjs/remark-validate-links/src/',
198
* headingPrefix: '#markdown-header-',
199
* lines: false
200
* }
201
* ```
202
*/
203
interface UrlConfig {
204
/** Prefix of headings (example: `'#'`, `'#markdown-header-'`) */
205
headingPrefix?: string | null | undefined;
206
/** Domain of URLs (example: `'github.com'`, `'bitbucket.org'`) */
207
hostname?: string | null | undefined;
208
/** Whether absolute paths (`/x/y/z.md`) resolve relative to a repo */
209
resolveAbsolutePathsInRepo?: boolean | null | undefined;
210
/** Whether lines in files can be linked */
211
lines?: boolean | null | undefined;
212
/** Path prefix before files (example: `'/remarkjs/remark-validate-links/blob/'`, `'/remarkjs/remark-validate-links/src/'`) */
213
prefix?: string | null | undefined;
214
/** Hash to top of readme (example: `#readme`) */
215
topAnchor?: string | null | undefined;
216
}
217
```
218
219
**Pre-configured services:**
220
221
- **GitHub**: `headingPrefix: '#'`, `lines: true`, `topAnchor: '#readme'`, `prefix: '/owner/repo/blob/'`, `resolveAbsolutePathsInRepo: true`
222
- **GitLab**: `headingPrefix: '#'`, `lines: true`, `topAnchor: '#readme'`, `prefix: '/owner/repo/blob/'`
223
- **Bitbucket**: `headingPrefix: '#markdown-header-'`, `lines: false`, `prefix: '/owner/repo/src/'`
224
225
**Automatic detection**: The plugin uses the `hosted-git-info` library to automatically detect the Git service type from the repository URL and apply appropriate configuration.
226
227
### Internal Types
228
229
Types used internally by the plugin for reference tracking and validation.
230
231
```javascript { .api }
232
/** Map of file paths to their available headings/anchors */
233
type Landmarks = Map<string, Map<string, boolean>>;
234
235
/** Reference to a file and optional heading */
236
interface Reference {
237
/** Absolute path to the referenced file */
238
filePath: string;
239
/** Hash/anchor portion of the reference */
240
hash?: string | undefined;
241
}
242
243
/** VFile from vfile package for file representation */
244
type VFile = import('vfile').VFile;
245
246
/** FileSet from unified-engine package for CLI batch processing */
247
type FileSet = import('unified-engine').FileSet;
248
249
/** Nodes type from mdast package */
250
type Nodes = import('mdast').Nodes;
251
252
/** Resource type from mdast package (link and image nodes) */
253
type Resource = import('mdast').Resource;
254
255
/** Link and image nodes from mdast AST */
256
type Resources = Extract<Nodes, Resource>;
257
258
/** Information about a reference including source context */
259
interface ReferenceInfo {
260
/** Source file containing the reference */
261
file: VFile;
262
/** The reference details */
263
reference: Reference;
264
/** AST nodes that contain this reference */
265
nodes: ReadonlyArray<Resources>;
266
}
267
268
/** Internal state passed between validation functions */
269
interface State {
270
/** Directory of the current file */
271
base: string;
272
/** Absolute path to the current file */
273
path: string;
274
/** Path to Git root directory */
275
root?: string | null | undefined;
276
/** Compiled skip patterns */
277
skipPathPatterns: ReadonlyArray<RegExp>;
278
/** URL configuration for the repository */
279
urlConfig: UrlConfig;
280
}
281
282
/** Plugin constants for error reporting and data storage */
283
interface Constants {
284
/** Rule ID for missing file errors */
285
fileRuleId: 'missing-file';
286
/** Rule ID for missing heading in other file errors */
287
headingInFileRuleId: 'missing-heading-in-file';
288
/** Rule ID for missing heading in current file errors */
289
headingRuleId: 'missing-heading';
290
/** Data key for storing landmarks in VFile data */
291
landmarkId: 'remarkValidateLinksLandmarks';
292
/** Data key for storing references in VFile data */
293
referenceId: 'remarkValidateLinksReferences';
294
/** Source identifier for error messages */
295
sourceId: 'remark-validate-links';
296
}
297
```
298
299
## Platform Differences
300
301
### Node.js Environment
302
303
Full functionality with platform-specific implementations:
304
305
- **File existence checking**: Uses Node.js `fs.access()` to verify file existence
306
- **Cross-file heading validation**: Available in CLI mode when using `FileSet`
307
- **Git repository detection**: Executes `git remote -v` and `git rev-parse --show-cdup` commands
308
- **Hosted Git URL validation**: Full support for GitHub, GitLab, and Bitbucket URL patterns
309
- **Directory handling**: Automatically resolves readme files in directories
310
- **Pattern matching**: Full RegExp support for `skipPathPatterns`
311
312
### Browser Environment
313
314
Limited functionality with browser-safe implementations:
315
316
- **Same-file heading validation only**: Cannot access filesystem or execute git commands
317
- **No file existence checking**: `checkFiles()` function is a no-op stub
318
- **No Git repository detection**: `findRepo()` function is a no-op stub
319
- **Manual configuration required**: Must provide `repository` and `root` options explicitly
320
- **Local anchor validation**: Can still validate `#heading` links within the same document
321
322
## Error Messages
323
324
The plugin generates detailed error messages with specific rule IDs:
325
326
- **`missing-file`**: Referenced file does not exist
327
- **`missing-heading`**: Heading anchor not found in current file
328
- **`missing-heading-in-file`**: Heading anchor not found in referenced file
329
330
Error messages include:
331
332
- **Contextual descriptions**: "Cannot find heading for `#heading-name`" or "Cannot find file `path/to/file.md`"
333
- **File context**: For cross-file references, shows both the missing anchor and target file
334
- **Smart suggestions**: Uses the `propose` library with edit distance algorithms (70% similarity threshold) to suggest corrections
335
- **Source attribution**: All errors include source URL `https://github.com/remarkjs/remark-validate-links#readme`
336
- **Position information**: Errors point to the exact location of problematic links in the source markdown
337
338
**Example error output:**
339
```
340
example.md:5:10-5:32: Cannot find heading for `#non-existent` in `readme.md`; did you mean `#installation`? [missing-heading-in-file](https://github.com/remarkjs/remark-validate-links#readme)
341
```
342
343
## Integration
344
345
### CLI Usage
346
347
```bash
348
# Check single file
349
npx remark example.md --use remark-validate-links --quiet
350
351
# Check multiple files
352
npx remark . --ext md --use remark-validate-links --quiet
353
```
354
355
### Programmatic Usage
356
357
```javascript
358
import remarkValidateLinks from "remark-validate-links";
359
import { remark } from "remark";
360
import { unified } from "unified";
361
import { reporter } from "vfile-reporter";
362
import { read } from "to-vfile";
363
364
// Process single file
365
const file = await remark()
366
.use(remarkValidateLinks)
367
.process(await read("document.md"));
368
369
console.log(reporter(file));
370
371
// Process multiple files with unified-engine
372
import { engine } from "unified-engine";
373
374
engine({
375
processor: remark().use(remarkValidateLinks),
376
files: ["*.md"],
377
extensions: ["md"],
378
pluginPrefix: "remark",
379
quiet: true
380
}, (error, code) => {
381
if (error) throw error;
382
process.exit(code);
383
});
384
```