# markdown-link
Parse and resolve Markdown and wiki-style links in notes, in Rust.
Notes link to each other in several forms. `[[Note]]`, `[[Note#Heading|alias]]`,
`![[image.png]]`, relative Markdown links and frontmatter values all point at
files by a name or a partial path. Resolving a name to the right file needs
clear rules. Getting those rules wrong gives broken link graphs and broken
renames.
markdown-link does two jobs:
- Find every link in a note: `[[wikilink]]`, `![[embed]]`, relative Markdown
links, and links in frontmatter values. Code spans and code blocks are
skipped.
- Resolve a link target against a set of file paths. Any path suffix reaches
a file, and when several files match, the one in the linking note's folder
wins, then the shorter path.
The resolver follows documented link-resolution conventions. A fixture vault
and a shared case file, which other language ports can run, check the
behaviour.
## Who it is for
Programs that read or change a folder of notes: link graph builders, search
indexers, static site generators, linters, and tools that rewrite links when
a file is renamed.
## When not to use it
- You only need to render wikilinks to HTML as part of an mdBook build. Use
`mdbook-wikilink`.
- You need a full Markdown parser or an editor model. markdown-link only finds
and resolves links. Pair it with a Markdown parser if you need more.
- You need note-app features beyond links, such as tags, properties queries,
canvas or plugins.
## How it compares
- [`trama`](https://crates.io/crates/trama) is a library. It extracts
wikilinks and builds a file index that resolves a link by file stem,
ignoring case. It has no path-suffix matching, no tie-break by the linking
note's folder, and no frontmatter links.
- [`nexis`](https://crates.io/crates/nexis) is a command-line tool for link
and backlink analysis. It ships no library target.
- [`mdbook-wikilink`](https://crates.io/crates/mdbook-wikilink) is an mdBook
preprocessor. It turns wikilinks into HTML links for a book. It does not
expose a link index or a scanner.
- The same author publishes related libraries, and markdown-link uses two of
them: [`yaml-splice`](https://crates.io/crates/yaml-splice) edits YAML in
place and supplies the frontmatter value type, and
[`slug-collate`](https://crates.io/crates/slug-collate) supplies the
case- and accent-insensitive path order that breaks ties.
`frontmatter-lossless` splits a note into frontmatter and body; it is only
used by markdown-link's own tests.
markdown-link combines the link scanner, the path-suffix resolution rules and
a fixture-vault test suite in one library.
## Install
```sh
cargo add markdown-link
```
## Quick start
```rust
use markdown_link::{LinkIndex, LinkKind, body_links};
let vault = vec![
"Projects/Plan.md".to_string(),
"Daily/2026-10-18.md".to_string(),
"Assets/diagram.png".to_string(),
];
let index = LinkIndex::build(&vault);
let note = "See [[Plan]] and ![[diagram.png]]. Not a link: `[[Plan]]`.";
for link in body_links(note) {
let found = index.resolve_from(&link.target, "Daily/2026-10-18.md");
println!("{:?} {} -> {:?}", link.kind, link.target, found);
}
assert_eq!(index.resolve("Plan"), Some("Projects/Plan.md"));
assert_eq!(body_links(note)[1].kind, LinkKind::Embed);
```
### Options for a stricter reader
The defaults above are the long-standing behaviour. A tool that renders or
publishes notes can ask for more:
- `body_links_spanned_with(body, &ScanOptions { .. })` picks the code rules
(`CodeRules::SkillExport` for skill renderers), skips links inside HTML
comments, and reads `[[#Heading]]` as a link to the note it is in.
- Each parsed link carries its `heading`, its `block` (`#^id`) and an embed's
`size` (`|300` or `|300x200`).
- `LinkIndex::resolve_strict` refuses a name several files answer to,
returning `ResolveError::Ambiguous` with every candidate, where
`resolve_from` picks one. `LinkIndex::candidates` lists them.
- `relative_link(from, to, fragment)`, `encode_uri_component`,
`encode_uri_fragment` and `escape_label` write a resolved link back out as
a relative markdown link.
```rust
use markdown_link::code::CodeRules;
use markdown_link::{LinkIndex, ResolveError, ScanOptions, body_links_spanned_with, relative_link};
let options = ScanOptions {
code_rules: CodeRules::SkillExport,
skip_html_comments: true,
self_links: true,
};
let links = body_links_spanned_with("<!-- [[Draft]] --> [[Plan#^step2]]", &options);
assert_eq!(links.len(), 1);
assert_eq!(links[0].block.as_deref(), Some("step2"));
let index = LinkIndex::build(&["a/Plan.md".to_string(), "b/Plan.md".to_string()]);
assert!(matches!(index.resolve_strict("Plan", "x.md"), Err(ResolveError::Ambiguous(_))));
assert_eq!(relative_link("a/x.md", "a/Plan.md", Some("Step 2")), "Plan.md#Step%202");
```
The public items are documented on [docs.rs](https://docs.rs/markdown-link).
## Supported versions
- Rust: 1.89 or newer. This is the only place the number is stated in
prose. The source of truth is the workspace's `rust-version`, which the
crate inherits with `rust-version.workspace = true`.
## Platforms
Any platform with `std`. The crate has no platform-specific code and no
`unsafe` code.
## Stability
This is a 0.x crate. Breaking changes are possible between minor versions and
are listed in the [changelog](CHANGELOG.md).
## Design decisions
Short notes on settled design choices are in [decisions/](decisions/).
## TypeScript twin
[`@osdlc/markdown-link`](https://www.npmjs.com/package/@osdlc/markdown-link)
is the TypeScript twin of the crate's parsing half: the code lexer, the link
scanner (with `ScanOptions`), display text and typed references. It does
not resolve or write links yet: `LinkIndex`, `relative_link` and the URI
encoders are Rust only. It
sits beside the crate, in
[`packages/ts/markdown-link`](https://github.com/osdlc/osdlc/tree/main/packages/ts/markdown-link).
Its tests read this crate's `tests/fixtures/conformance.json`, so the two
agree on every case in it. Its offsets count UTF-16 code units, where the
crate's count bytes.
## Source
The crate lives in the [osdlc/osdlc](https://github.com/osdlc/osdlc)
repository, in `packages/rust/markdown-link`.
## License
Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or
[MIT License](LICENSE-MIT), at your option.