obsilink is a small, deterministic Python library for extracting and replacing links in Markdown text.
It supports:
- Obsidian wikilinks like
[[Note]],[[Note|Alias]], and![[Embedded]] - Markdown links like
[Docs](https://example.com)and embeds like - Plain URLs like
https://example.com,ftp://files.example.com, andmailto:user@example.com
The parser returns structured Link objects in encounter order and preserves duplicates.
Links can also be replaced in-place using structured (old, new) pairs.
- Deterministic parsing with stable ordering
- Two public functions:
extract_links(...)andreplace_links(...) - Accepts either raw
stror file-like input with.read() - Preserves duplicates instead of deduplicating
- Strict, typed API compatible with
mypystrict mode - Links are split into structured fields (
target,alias,heading,blockid) — heading (#) and block-id (^) fragments are extracted separately and never included intarget
uv add obsilinkpip install obsilinkgit clone https://github.com/chgroeling/obsilink.git
cd obsilink
uv syncIf the package is not yet published to your package index, use the source install method.
from obsilink import extract_links
text = """
See [[Project/Plan#Milestones|Roadmap]],
,
and https://example.com.
"""
links = extract_links(text)
for link in links:
print(link.type.value, link.target, link.alias)from obsilink import Link, LinkType, replace_links
text = "See [[OldNote]] for details."
old = Link(type=LinkType.WIKILINK, target="OldNote", alias=None, heading=None, blockid=None)
new = Link(type=LinkType.WIKILINK, target="NewNote", alias=None, heading=None, blockid=None)
result_text, applied = replace_links(text, [(old, new)])
print(result_text) # See [[NewNote]] for details.
print(applied) # [True]from obsilink import extract_links
text = """
[[Project/Plan#Milestones|Roadmap]]
![[Assets/Diagrams/System Overview#v2]]
[[People/Ada Lovelace#Biography^early-life|Ada]]
[[Project/Plan#Milestones|Roadmap]]
[[Malformed
"""
links = extract_links(text)
for link in links:
print(
link.type.value,
link.target,
link.alias,
link.heading,
link.blockid,
)
# Encounter order is preserved, duplicates are kept, and malformed
# wikilinks (like "[[Malformed") are ignored.Extracts links from:
str- text file-like object with
.read()returningstr
Returns:
list[Link]in encounter order
Raises:
TypeErrorfor unsupported source typesTypeErrorif.read()returns non-string data- Any exception raised by
.read()is propagated
Replaces links in text based on a list of (old_link, new_link) pairs.
Accepts:
source:stror text file-like object with.read()returningstrreplacements:list[tuple[Link, Link]]
Returns:
tuple[str, list[bool]]— the modified text and a boolean per pair indicating whether the replacement was applied
Replacements are applied sequentially; each pair replaces only the first occurrence.
Raises:
TypeErrorfor unsupported source typesTypeErrorif.read()returns non-string data- Any exception raised by
.read()is propagated
Link is a frozen dataclass with these fields:
type: LinkTypetarget: stralias: str | Noneheading: str | Noneblockid: str | None
Convenience properties:
link.is_url->Truefor URL targets (http,https,ftp,file,mailto)link.as_path->pathlib.Pathfor non-URL targets (raisesValueErrorfor URLs)
LinkType.WIKILINKLinkType.WIKILINK_EMBEDLinkType.MARKDOWN_LINKLinkType.MARKDOWN_EMBEDLinkType.PLAIN_URL
Each link is split into its constituent parts. The target field
contains only the note, path, or URL — heading (#) and block-id (^) fragments are
extracted into their own fields and are not included in target.
| Input | target |
alias |
heading |
blockid |
|---|---|---|---|---|
[[Note]] |
Note |
None |
None |
None |
[[Note|Alias]] |
Note |
Alias |
None |
None |
[[Folder/Note#Heading]] |
Folder/Note |
None |
Heading |
None |
[[Note^abc123]] |
Note |
None |
None |
abc123 |
[[Note#Section^block|Display]] |
Note |
Display |
Section |
block |
![[Embedded]] |
Embedded |
None |
None |
None |
For Markdown links, heading and blockid are always None. The target is the URL or path inside parentheses, and alias is the link text inside brackets.
| Input | target |
alias |
heading |
blockid |
|---|---|---|---|---|
[Docs](https://example.com) |
https://example.com |
Docs |
None |
None |
 |
assets/logo.png |
Logo |
None |
None |
[](relative/path.md) |
relative/path.md |
None |
None |
None |
This project uses uv, ruff, mypy, and pytest.
uv sync
uv run ruff format src/ tests/
uv run ruff check src/ tests/
uv run mypy src/
uv run pytestThis project is licensed under the MIT License. See LICENSE.