markdown-link 0.7.0

Parse and resolve Markdown and wiki-style links in notes: every `[[link]]`, `![[embed]]`, and relative Markdown link in a note body or its frontmatter, with code spans excluded, plus a LinkIndex that resolves a target against a set of file paths.
Documentation

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 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 is a command-line tool for link and backlink analysis. It ships no library target.
  • 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 edits YAML in place and supplies the frontmatter value type, and 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

cargo add markdown-link

Quick start

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.
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.

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.

Design decisions

Short notes on settled design choices are in decisions/.

TypeScript twin

@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. 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 repository, in packages/rust/markdown-link.

License

Licensed under either of Apache License, Version 2.0 or MIT License, at your option.