The Core Markdown Link Pattern
Markdown inline links look like [text](url). A simple two-group regex handles the vast majority of real-world content:
/\[([^\]]+)\]\(([^)]+)\)/g
// Group 1: link text (anything inside [...] except ])
// Group 2: URL (anything inside (...) except ))
// Input:
// "See [docs](https://example.com) and [source](https://github.com/x/y)."
// Matches 2 links. For each match:
// match[1] = "docs" or "source"
// match[2] = "https://example.com" or "https://github.com/x/y"Skipping Images
Markdown images use the same syntax with a leading !: . The basic pattern matches them too, which is usually not what you want. Add a negative lookbehind to skip them:
/(?<!!)\[([^\]]+)\]\(([^)]+)\)/g
// Input: "See [docs](a.html) and  and [more](b.html)."
// Matches: 2 links, image is skipped
// match[1]: "docs", "more"
// match[2]: "a.html", "b.html"
// Fallback for engines without lookbehind (older Safari):
[...str.matchAll(/(^|[^!])\[([^\]]+)\]\(([^)]+)\)/g)]
// group 1 = char before [, 2 = text, 3 = urlCapturing Optional Titles
Markdown allows an optional title after the URL: [text](url "title"). Add a third capture group to extract it:
/\[([^\]]+)\]\(([^\s)]+)(?:\s+"([^"]*)")?\)/g
// Group 1: text
// Group 2: URL (no whitespace, no ")")
// Group 3: optional title (undefined if absent)
// Input: '[docs](https://example.com "Documentation") and [plain](url)'
// Match 1: text="docs", url="https://example.com", title="Documentation"
// Match 2: text="plain", url="url", title=undefinedReference-Style Links
Markdown also supports a two-part reference form: [text][id] paired with [id]: url elsewhere in the document.
// Match the usage site [text][id] (id may be empty = shortcut)
/\[([^\]]+)\]\[([^\]]*)\]/g
// Match the definition [id]: url (usually at line start)
/^\s*\[([^\]]+)\]:\s*(\S+)(?:\s+"([^"]*)")?\s*$/gm
// To fully resolve references you need to:
// 1. First scan all definitions and build a map { id -> url }
// 2. Then scan usages and substitute via the map
// Regex alone can't do step 2 (requires state)Language-Specific Usage
JavaScript
const MD_LINK = /(?<!!)\[([^\]]+)\]\(([^)]+)\)/g;
const text = "Visit [docs](https://a.com) and [blog](https://b.com).";
for (const m of text.matchAll(MD_LINK)) {
console.log({ text: m[1], url: m[2] });
}
// { text: "docs", url: "https://a.com" }
// { text: "blog", url: "https://b.com" }
// Convert Markdown links to HTML <a> tags
text.replace(MD_LINK, (_, label, url) => `<a href="${url}">${label}</a>`);
// 'Visit <a href="https://a.com">docs</a> and <a href="https://b.com">blog</a>.'Python
import re
MD_LINK = re.compile(r'(?<!!)\[([^\]]+)\]\(([^)]+)\)')
text = "Visit [docs](https://a.com) and [blog](https://b.com)."
for m in MD_LINK.finditer(text):
print({'text': m.group(1), 'url': m.group(2)})
# {'text': 'docs', 'url': 'https://a.com'}
# {'text': 'blog', 'url': 'https://b.com'}
# Extract just the URLs
urls = MD_LINK.findall(text)
# [('docs', 'https://a.com'), ('blog', 'https://b.com')]
just_urls = [u for (_, u) in urls]PHP
$text = "Visit [docs](https://a.com) and [blog](https://b.com).";
preg_match_all('/(?<!!)\[([^\]]+)\]\(([^)]+)\)/', $text, $m);
// $m[1] => ["docs", "blog"]
// $m[2] => ["https://a.com", "https://b.com"]
// Pair text + URL
$pairs = array_map(null, $m[1], $m[2]);Common Pitfalls
URLs with closing parentheses
Wikipedia URLs often contain parentheses: https://en.wikipedia.org/wiki/Regex_(programming). The basic pattern breaks at the first closing paren. There's no clean regex fix because parentheses aren't escaped in Markdown URLs. Workaround: match balanced pairs with a quantifier, or escape the offending URLs with angle brackets: [docs](<url(with)parens>).
Nested brackets in link text
CommonMark allows nested balanced brackets in link text: [see [1]](ref). The pattern [^\]]+ stops at the first ] so it fails here. For most document extraction tasks this is rare enough to ignore. For strict CommonMark compliance, give up and use a parser.
Escaped brackets in text
\[not a link\](text) is literal text in Markdown. The regex matches it as a link. For high-fidelity extraction, pre-filter with a check that counts preceding backslashes.
Multiline link text
By default . does not match newlines. The pattern [^\]]+ does (because it's a negated class, not .), so multi-line link text is handled correctly. Verify in the live tester before shipping.
Mixing patterns in a replace callback
When you convert [text](url) to HTML, remember that the replacement callback receives the full match as the first argument and the captures as subsequent arguments. Using the match itself in the replacement is a common mistake:
// Wrong — the full match is "[text](url)", not just the text
str.replace(regex, (match, text, url) => `<a href="${url}">${match}</a>`);
// Right
str.replace(regex, (_, text, url) => `<a href="${url}">${text}</a>`);Markdown Link Cheatsheet
| Goal | Pattern | Captures |
|---|---|---|
| Basic inline link | /\[([^\]]+)\]\(([^)]+)\)/g | 1=text, 2=url |
| Skip images | /(?<!!)\[([^\]]+)\]\(([^)]+)\)/g | 1=text, 2=url |
| With title | /\[([^\]]+)\]\(([^\s)]+)(?:\s+"([^"]*)")?\)/g | 1=text, 2=url, 3=title |
| Images only | /!\[([^\]]*)\]\(([^)]+)\)/g | 1=alt, 2=src |
| Reference usage | /\[([^\]]+)\]\[([^\]]*)\]/g | 1=text, 2=id |
Testing Your Markdown Link Regex
Use the live Regex Tester above with this canonical test block:
See [the docs](https://example.com) for details.
Also check out [source code](https://github.com/user/repo "GitHub repo").
Here is an image:  that we skip.
Reference-style: [intro][1]
[1]: https://example.com/intro
Patterns to try:
/\[([^\]]+)\]\(([^)]+)\)/g — 3 matches (incl. image)
/(?<!!)\[([^\]]+)\]\(([^)]+)\)/g — 2 matches (links only)
/!\[([^\]]*)\]\(([^)]+)\)/g — 1 match (image only)
/\[([^\]]+)\]\[([^\]]*)\]/g — 1 match (reference use)When to Give Up and Use a Parser
Regex is excellent for extracting Markdown links from trusted content — your own blog posts, your own docs, your own commit messages. For user-submitted content that must render faithfully (comments, wiki pages, forum posts), use a real CommonMark parser: markdown-it or remark for JavaScript, markdown-it-py or commonmark for Python, Parsedown for PHP. Parsers correctly handle balanced brackets, escapes, backticks that disable Markdown, HTML embedding, and the dozen other edge cases regex cannot see.
Performance Notes
Negated character classes like [^\]] and [^)] are the fastest building blocks in regex — they never backtrack. The patterns on this page are O(n) in the input length. Running them over a megabyte of Markdown takes a few milliseconds. The lookbehind for image skipping adds negligible overhead because it inspects a single character.