Markdown Nav
Navigate a long markdown document by its headers and its tags, from the Command Palette
Details
Installs
- Total 1
- Win 1
- Mac 0
- Linux 0
| Sep 17 | Sep 16 | Sep 15 | Sep 14 | Sep 13 | Sep 12 | Sep 11 | Sep 10 | Sep 9 | Sep 8 | Sep 7 | Sep 6 | Sep 5 | Sep 4 | Sep 3 | Sep 2 | Sep 1 | Aug 31 | Aug 30 | Aug 29 | Aug 28 | Aug 27 | Aug 26 | Aug 25 | Aug 24 | Aug 23 | Aug 22 | Aug 21 | Aug 20 | Aug 19 | Aug 18 | Aug 17 | Aug 16 | Aug 15 | Aug 14 | Aug 13 | Aug 12 | Aug 11 | Aug 10 | Aug 9 | Aug 8 | Aug 7 | Aug 6 | Aug 5 | Aug 4 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Windows | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| Mac | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| Linux | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Readme
- Source
- raw.githubusercontent.com
Markdown Nav
Navigate a long markdown document by its headers and its tags, from the Command Palette.
Built for big notes files — the kind that accumulate for years and get too long to scroll. Two commands:
| Command Palette | What it does |
|---|---|
Markdown Nav: Jump to Header |
Walk the header tree one level at a time to the section you want |
Markdown Nav: Find Tag |
Pick a tag, then pick one of its matches, then jump there |
No key bindings are set by default. Copy what you want out of Example.sublime-keymap, via
Preferences → Package Settings → Markdown Nav → Key Bindings.
Jump to Header
Rather than one flat list of every header in the file, this descends the outline a level at a time:
- The first panel shows the top level of the document. If the file has more than one
# H1, that's the H1s. If it has a single# H1(the usual document title), it starts a level in, at that H1's own sub-headers — so you don't click through a list of one. - Picking a header that has sub-headers opens the next level down, with
(jump here)as the first entry (which jumps to the header you just picked), followed by its children. - Picking a leaf header — one with nothing nested under it — jumps immediately, no extra panel.
Each row shows the header's depth (##), its text, its line number, and how many sub-headers it
has. Levels may be skipped: an ## H2 whose children are #### H4s works fine — each panel shows
the shallowest level actually present in that section. Headers inside fenced code blocks are
ignored. esc at any level cancels without moving the caret.
The panel reopens on your last pick. The top panel highlights the section containing it, and
each level down highlights the next step along that path, so re-running the command puts you right
back where you were. Sublime's quick panel can't be pre-filled with filter text, so this is done
with a pre-highlighted row — type to filter as usual, or press enter immediately to re-jump. The
memory is per view and keyed on header text (so edits above a header don't lose it), and lasts
only for the session.
The major header level
The first panel keys off the major level: the shallowest header level that appears more than
once. A file with several # H1s has H1 as its major level; a file with a single # H1 document
title falls through to its ## H2s, since a lone title isn't a section divider.
Find Tag
Two panels: pick a tag (listed most-recently-used first, with a match count), then pick a match (with its line number), then jump. Both are fuzzy-filterable like any Sublime quick panel.
Match labels are the tagged text — what the line says with the tag itself stripped out — so the list reads as content rather than as a column of repeated tag names.
How tags are defined
A tag has two orthogonal aspects: the form it's written in, and the scope it refers to. A tag's identity is its lowercased name.
Forms
Keyword-prefix form — heather: …
A line whose first text content (optionally after a markdown header prefix #{1,6}) is
<name>: followed by at least one space and then non-whitespace. The keyword is the tag.
The required space after the colon is what disqualifies URLs and similar:
| Matches | Does not match | Why not |
|---|---|---|
heather: lunch on Saturday |
mobilenotes://abc123 |
no space after : |
**heather:** lunch |
http://example.com |
no space after : |
**heather**: lunch |
mailto:foo@bar.com |
no space after : |
issue: bad bearings |
key:value |
no space after : |
## person: Heather |
Heather: lunch |
uppercase — see below |
Hashtag form — #heather
A #name token anywhere on the line. It must be at the start of the line or preceded by a
non-word, non-& character, so word#tag and the HTML entity — don't match.
| Matches | Does not match | Why not |
|---|---|---|
#heather |
#Heather |
uppercase — see below |
**#heather** |
#123 |
numeric |
## topic #heather |
— |
HTML entity |
we met #heather today |
word#tag |
no boundary before # |
Case. Tags are lowercase by default, in both forms. This is deliberate: it keeps tag identity
unambiguous and stops every capitalized word before a colon (Note: …, TODO: …) and every
sentence starting with a capital from becoming a tag. Change it with tag_name_pattern below if
you'd rather have uppercase count.
Bold. Tags in either form may be wrapped in **…**; the markers are stripped before the tag is
identified, and they don't affect scope.
Scopes
A tag's scope is section if either the line is a markdown header, or the line consists only of hashtag tokens. Otherwise it's single-line.
- Single-line scope — the match is that one line. Jumps to the start of the line.
- Section scope — the match is the enclosing section, anchored at the tag's own line if it's a header, otherwise at the nearest preceding header at any level, otherwise the top of the document. Jumps to the start of the anchor line.
| Example line | Form | Header? | Tags-only? | Scope |
|---|---|---|---|---|
heather: lunch on Saturday |
keyword | no | no | single-line |
## heather: lunch |
keyword | yes | — | section |
do #heather today |
hashtag | no | no | single-line |
#heather |
hashtag | no | yes | section |
#heather #family |
hashtag ×2 | no | yes | section |
## title #heather |
hashtag | yes | — | section |
Ignored
- Anything inside a fenced code block (
). - Numeric-only
#1,#123, and HTML entities&#nnn;. - Mid-token
word#tag. - A leading YAML
---frontmatter block. Itstags:key is metadata about the whole file, not an in-document tag, so it isn't reported as a tag namedtags. Turn this off withskip_frontmatter. An unterminated---is treated as a horizontal rule, not frontmatter.
Match text
The label shown for a match is the line with the tag stripped out:
| Raw line | Tag | Label |
|---|---|---|
heather: lunch on Saturday |
heather | lunch on Saturday |
**heather:** lunch on Saturday |
heather | lunch on Saturday |
* heather: lunch |
heather | lunch |
5. heather: lunch |
heather | lunch |
## person: Heather |
person | Heather |
we met #heather today |
heather | we met today |
* #heather alone item |
heather | alone item |
* item with #heather inline |
heather | * item with inline |
## title #heather |
heather | title |
The rules: strip the tag and its bold markers; if the tag was at the start of the line, strip the
leading whitespace, bullet marker (*, -, +, N., N)) and header marker too; if the line is
a header, strip the header marker regardless of where the tag was; if the tag was mid-line, remove
only the tag and keep the surrounding text as context; then collapse and trim whitespace.
When stripping leaves nothing, the line was tag-only — which means section scope — so the label
comes from the section anchor instead, or (top of doc) if there's no preceding header.
Settings
Preferences → Package Settings → Markdown Nav → Settings, or
Preferences: Markdown Nav Settings in the Command Palette.
{
"tag_name_pattern": "[a-z][a-z0-9_-]*",
"skip_frontmatter": true
}
*tag_name_pattern** — the character pattern for a tag name, spliced into both forms. To also
accept uppercase, use “[A-Za-z][A-Za-z0-9_-]”. To allow dots,“[a-z][a-z0-9._-]*”. It must not
contain a capture group — use(?:…) if you need grouping — because the tag name has to stay as
group 1 for scope and label extraction to work. A pattern that won't compile, or that adds a group,
is ignored with a note in the status bar and the default is used instead.
**skip_frontmatter** — whether to skip a leading YAML --- block when scanning for tags.
Everything else about tag handling is structural rather than a preference, and lives in code: which
of the two forms a line uses, whether the scope is a line or a section, where a section's anchor is,
and how a label is stripped. Those are interdependent — the patterns carry a contract that group 1
is the tag name — so exposing them as raw regex settings would let a bad override break scope and
labels silently instead of failing loudly. tag_name_pattern is the one part that varies
independently of the rest.
Requirements
Sublime Text 4, build 4107 or newer (it runs in the Python 3.8 plugin host).
Layout
mdnav.py # both commands and the parsing helpers
Markdown Nav.sublime-settings # tag_name_pattern, skip_frontmatter
Default.sublime-commands # Command Palette entries
Main.sublime-menu # Preferences -> Package Settings -> Markdown Nav
Example.sublime-keymap # suggested bindings; Sublime never loads this file
tests/test_tags.py # standalone, stubs `sublime`, no ST needed
Command names are mdnav_jump_to_header and mdnav_find_tag — the mdnav_ prefix predates the
rename from “mdnav” to “Markdown Nav” and is kept so existing key bindings keep working.
Tests
py tests/test_tags.py
License
MIT — see LICENSE.