markdown-link 0.8.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
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/),
and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

## [0.8.0] - 2026-11-04

The scanner reports each `[[` it gave up on. Nothing is removed or changed.
`@osdlc/markdown-link` gets the same in its 0.8.0, and both suites read the
new cases.

### Added

- `scan_body_with(body, &ScanOptions) -> BodyScan`: the links
  `body_links_spanned_with` returns, and `abandoned`, each `[[` found outside
  code that is not a link, as an `AbandonedLink` (`reason`, `embed`,
  `byte_start`, `byte_end`). `AbandonReason` is `Bracket` (a `[` or `]`
  before `]]`), `Code` (the target is only code), `EmptyTarget` (`[[]]`,
  `[[ |alias]]`, or `[[#Heading]]` without `self_links`) or `Unclosed`.
  A `[[` that a link found after it starts inside is not reported:
  `[[[Note]]` is a literal `[` and a link. A run of `[` with no link after
  it, such as `[[[TODO`, is one entry: from the first `[[`, with the reason
  and end of the last.
  `body_links_spanned_with` is unchanged and returns `scan_body_with`'s links.
- `tests/fixtures/conformance.json`: a body case may list `abandoned`; one
  that leaves it out expects none.

### Fixed

- `percent_decode` takes an escape only when both characters after `%` are
  hex digits. `u8::from_str_radix` also takes a sign, so `%+5` decoded; it now
  stays as written. The conformance case `plus_sign_reads_as_a_hex_byte` is
  now `a_sign_is_not_a_hex_digit`, and `@osdlc/markdown-link`'s
  `percentDecode` matches.

## [0.7.0] - 2026-11-04

Options for tools that render or publish notes, a strict resolver, and helpers
that write a resolved link back out. The defaults keep the old behaviour,
except for block references, which now have a field of their own. There is no
0.6: this release joins `@osdlc/markdown-link` on 0.7.

### Added

- `ScanOptions` and `body_links_spanned_with(body, &ScanOptions)`: pick the
  code rules (`CodeRules::SkillExport`), skip links inside HTML comments
  (`skip_html_comments`), and read `[[#Heading]]` and `[[#^block]]` as a link
  to the note they are in, with an empty target (`self_links`).
  `ScanOptions::default()` is what `body_links_spanned` does.
- `parse_inner_self`: `parse_inner` that also reads a body with no target and
  a `#` fragment as a link to the note it is written in.
- `WikiLink::block` and `BodyLink::block`: a `#^id` fragment, without the `^`.
- `WikiLink::size` and `BodyLink::size`: an embed's `|300` or `|300x200`, as
  `Size { width, height }`. `Size::parse` and `is_size_spec` read one. The
  alias still holds it as written.
- `BodyLink::heading`.
- `LinkIndex::candidates(target, from)`: every path a link could mean,
  shortest first, the field `resolve_from` chooses from.
- `LinkIndex::resolve_strict(target, from)`: resolve without guessing. It
  takes a candidate only when it is the only one, its whole path is the
  target, or it sits at the target's path from the linking note's folder.
  Otherwise `ResolveError::Ambiguous` lists every candidate.
- `relative_link(from, to, fragment)`: the relative markdown link from one
  vault-relative file to another, each segment encoded as `encodeURIComponent`
  does and the fragment as `encodeURI` does with `#` escaped.
  `encode_uri_component`, `encode_uri_fragment` and `escape_label` are public
  too.

### Changed

- A `#^id` fragment is a block, not a heading. `parse_inner("Note#^abc", _)`
  gives `heading: None, block: Some("abc")`, where it gave
  `heading: Some("^abc")`. A heading path such as `Note#Parent#Child` is
  unchanged.
- `WikiLink` and `BodyLink` have new public fields, so code that builds one
  with a struct literal must set them.

## [0.5.5]https://github.com/osdlc/osdlc/compare/markdown-link/v0.5.4...markdown-link/v0.5.5 - 2026-11-01

### Other

- updated the following local packages: frontmatter-lossless

## [0.5.4]https://github.com/osdlc/osdlc/compare/markdown-link/v0.5.3...markdown-link/v0.5.4 - 2026-10-31

### Other

- add @osdlc/markdown-link, the TypeScript twin ([#567]https://github.com/osdlc/osdlc/pull/567)

## [0.5.3]https://github.com/osdlc/osdlc/compare/markdown-link/v0.5.2...markdown-link/v0.5.3 - 2026-10-30

### Changed

- Depend on slug-collate 0.2. slug-collate 0.1.2 was yanked, and this
  crate's earlier releases require it. Only `compare_paths` is used, and its
  order is unchanged, so behaviour is the same.

## [0.5.2]https://github.com/osdlc/osdlc/compare/markdown-link/v0.5.1...markdown-link/v0.5.2 - 2026-10-29

### Fixed

- *(markdown-link)* match whole folder names when preferring a folder ([#543]https://github.com/osdlc/osdlc/pull/543)

### Fixed

- `prefer_in_folder` (and so `LinkIndex::resolve_from`) matches whole folder names. A note in
  `notes/` no longer prefers a candidate in `notes2/`.

## [0.5.1]https://github.com/osdlc/osdlc/compare/markdown-link/v0.5.0...markdown-link/v0.5.1 - 2026-10-28

### Other

- updated the following local packages: yaml-splice

## [0.5.0] - Unreleased

First public release. `markdown-link` parses wiki-style and Markdown links — every `[[wikilink]]`,
`![[embed]]`, relative markdown link and frontmatter link in a note, with code spans excluded — and
resolves a target against a set of file paths by path suffix.

### Added

- `LinkIndex`: build a suffix index from vault paths, then `resolve`/`resolve_from` a target to a
  vault path (any path suffix reaches a file; a tie prefers the linking note's own folder, then the
  shorter full path, measured in characters).
- `body_links` and `body_links_spanned`: every wikilink, embed and relative markdown link in a note
  body, in order, the spanned form carrying byte offsets.
- `frontmatter_links` and `frontmatter_paths`: wikilinks and file-path values hiding in frontmatter,
  each tagged with its key; `may_hold_frontmatter_paths` is a cheap pre-test that skips the YAML
  parse.
- `parse_inner`, `target_range`, `parse_typed_ref`/`TypedRef`: parse one wikilink body, locate its
  target for in-place rewriting, or read a whole `[[type/slug|alias]]` value for shape.
- `display_text`: replace each link with what a reader sees (its alias, or its target), dropping an
  embed's `|300`/`|300x200` size suffix.
- `is_vault_link_url`, `md_link_target`, `strip_md_ext`, `percent_decode`, `normalize_target`,
  `suffixes`, `prefer_in_folder`, `join_relative`, `ATTACHMENT_EXTENSIONS`: the shared normalisation
  a second parser reuses so two scanners agree on what a target is.
- `code` module: `strip_code`/`strip_code_with` blank fenced and inline code to spaces (character
  count preserved) under `CodeRules::CommonMark` or `CodeRules::SkillExport`, with `Fences`, `Spans`
  and `FenceLine` for a streaming scan.

### Changed

Since 0.4.2, the last version in the history below:

- **Breaking:** `strip_to_display_text` is now `display_text`. An embed's trailing `|300` or
  `|300x200` is a size, not display text, so `![[img.png|300]]` shows `img.png`. An empty alias
  shows the target.
- **Breaking:** a body link carries an `alias`, the text after the first `|` outside code. Code
  that builds a `BodyLink` by struct literal must set it.

## History before the first public release

These versions were never published. They are rebuilt from the git history, one version per change.
A breaking entry breaks code written against the version before it.

- 0.4.2 (2026-10-15): added `frontmatter_paths`, for frontmatter values that name a file by path,
  and `may_hold_frontmatter_paths`, a cheap pre-test that skips the YAML parse.
- 0.4.1 (2026-10-11): trimming and markdown URL scanning run in linear time.
- 0.4.0 (2026-10-11): **Breaking:** `strip_code` follows CommonMark code rules by default, so tilde
  fences, fences longer than three and double-backtick spans are now code, and their links are no
  longer found. `body_links` returns links in the order they appear, not wikilinks first. The `code`
  module is public (`CodeRules`, `FenceLine`, `Fences`, `Spans`, `strip_code_with`). A code span no
  longer runs past its heading or table-row line.
- 0.3.6 (2026-10-06): added `target_range`. A backslash before the alias pipe is treated as an
  escape.
- 0.3.5 (2026-10-05): internal dependency changes; no API change.
- 0.3.4 (2026-10-05): code blanking moves into this crate; `strip_code` keeps its path.
- 0.3.3 (2026-09-29): added `join_relative`. `resolve_from` follows `./` and `../` targets from the
  linking note's folder.
- 0.3.2 (2026-09-18): added `body_links_spanned`, `BodyLink` and `BodyLinkKind`, a scan that
  reports each link's byte offsets.
- 0.3.1 (2026-09-11): added `wikilink_targets`.
- 0.3.0 (2026-08-22): **Breaking:** the shortest-path tie-break counts characters, not path
  segments, so some targets resolve to a different file. Added `resolve_from` and
  `prefer_in_folder`: a contested name prefers the linking note's own folder.
- 0.2.1 (2026-08-22): `tiff` and `heic` count as attachments.
- 0.2.0 (2026-08-22): **Breaking:** `is_md_link_url` is now `is_vault_link_url`, and is true for
  attachment links too. Added `ATTACHMENT_EXTENSIONS`. Markdown links to attachments are found as
  links.
- 0.1.4 (2026-08-22): `strip_code` shares one implementation; no API change.
- 0.1.3 (2026-08-22): added `suffixes`.
- 0.1.2 (2026-08-22): added `is_md_link_url`, `md_link_target`, `strip_md_ext` and
  `percent_decode`.
- 0.1.1 (2026-08-15): `LinkIndex::build` no longer slices bytes off a path.
- 0.1.0 (2026-08-07): first usable API: link parsing, body and frontmatter scans, display text, and
  `LinkIndex` with path-suffix resolution, tested with a fixture vault.