md_toc

November 11, 2025 · View on GitHub

A Neovim plugin for generating and managing table of contents in markdown files. Designed to work seamlessly with Telekasten but works independently as well.

Features

  • Generate table of contents from markdown headings
  • Support for multiple markdown flavors:
    • GFM (GitHub Flavored Markdown)
    • GitLab
    • Redcarpet
    • Marked
  • Pleasant looking anchor links for internal file references
  • Optional Telescope integration for TOC navigation
  • Auto-update TOC on save (optional)
  • Works with both ATX (# Heading) and Setext heading styles
  • Respects code blocks (won't include headings from code)
  • Configurable indentation, list markers, and heading levels

Installation

Using Packer

use {
  'D3alWyth1T/md_toc',
  config = function()
    require('md_toc').setup({
      -- Optional configuration (these are the defaults)
      style = 'gfm',              -- gfm, gitlab, redcarpet, marked
      list_marker = '*',          -- Character for list items
      indent_size = 4,            -- Spaces per indentation level
      min_level = 1,              -- Minimum heading level to include
      max_level = 6,              -- Maximum heading level to include
      auto_update = false,        -- Auto-update TOC on save
      fence_text = 'md_toc',      -- Text to use in fence comments
      telescope_navigation = true, -- Use Telescope for navigation
      telekasten_integration = true, -- Enable Telekasten integration
    })
  end
}

Using lazy.nvim

{
  'D3alWyth1T/md_toc',
  ft = 'markdown',
  config = function()
    require('md_toc').setup()
  end
}

Using vim-plug

Plug 'D3alWyth1T/md_toc'

lua << EOF
require('md_toc').setup()
EOF

Usage

Commands

  • :MdTocGenerate - Generate a new table of contents at cursor position
  • :MdTocUpdate - Update existing table of contents
  • :MdTocRemove - Remove table of contents from buffer
  • :MdTocGoto - Jump to heading under cursor (when cursor is on TOC entry)
  • :MdTocNav - Open Telescope picker to navigate headings (requires Telescope)

Example Workflow

  1. Open a markdown file
  2. Position cursor where you want the TOC
  3. Run :MdTocGenerate

The plugin will generate a TOC like this:

<!-- md_toc GFM -->

* [md_toc](#md_toc)
    * [Features](#features)
    * [Installation](#installation)
        * [Using Packer](#using-packer)
        * [Using lazy.nvim](#using-lazynvim)
        * [Using vim-plug](#using-vim-plug)
    * [Usage](#usage)
        * [Commands](#commands)
        * [Example Workflow](#example-workflow)
        * [Auto-Update](#auto-update)
        * [Telescope Navigation](#telescope-navigation)
    * [Configuration](#configuration)
        * [Complete Configuration Example](#complete-configuration-example)
        * [Markdown Styles](#markdown-styles)
    * [Telekasten Integration](#telekasten-integration)
        * [Recommended Telekasten Configuration](#recommended-telekasten-configuration)
    * [Advanced Usage](#advanced-usage)
        * [Programmatic API](#programmatic-api)
        * [Custom Keybindings](#custom-keybindings)
    * [Comparison with vim-markdown-toc](#comparison-with-vim-markdown-toc)
    * [Requirements](#requirements)
    * [License](#license)
    * [Contributing](#contributing)
    * [Acknowledgments](#acknowledgments)

<!-- md_toc -->

Auto-Update

Enable auto-update to automatically refresh the TOC when you save:

require('md_toc').setup({
  auto_update = true,
})

Telescope Navigation

If Telescope is installed, use :MdTocNav to open a searchable list of all headings:

-- Optional: Add a keybinding
vim.keymap.set('n', '<leader>mt', '<cmd>MdTocNav<cr>', { desc = 'Navigate TOC' })

Configuration

Complete Configuration Example

require('md_toc').setup({
  -- Anchor link style
  style = 'gfm',  -- 'gfm', 'gitlab', 'redcarpet', 'marked'

  -- List formatting
  list_marker = '*',           -- Character for list items
  cycle_list_markers = false,  -- Use *, -, + for different levels
  indent_size = 4,             -- Spaces per indentation level

  -- Heading level filtering
  min_level = 1,  -- Include headings from level 1...
  max_level = 6,  -- ...through level 6

  -- Fence markers
  fence_text = 'md_toc',      -- Text in fence comments
  dont_insert_fence = false,   -- Set true to omit fence markers

  -- Auto-update behavior
  auto_update = false,  -- Auto-update on save

  -- Integration
  telekasten_integration = true,   -- Enable Telekasten integration
  telescope_navigation = true,     -- Enable Telescope navigation
})

Markdown Styles

Different markdown processors generate anchors differently:

GFM (GitHub Flavored Markdown)

  • Default style, works with GitHub, Obsidian, and most modern parsers
  • "Chapter One"#chapter-one
  • Preserves Unicode characters: "第三章"#第三章

GitLab

  • Similar to GFM with slightly different special character handling
  • Consolidates multiple hyphens
  • Strips leading/trailing underscores

Redcarpet

  • Ruby markdown processor
  • HTML entity encoding for some characters
  • "Q & A"#q-&amp;-a

Marked

  • JavaScript markdown processor
  • Minimal character transformation
  • Simpler anchor generation

Telekasten Integration

The plugin works great with Telekasten! It automatically detects when Telekasten is installed and can integrate with it.

-- In your Telekasten setup
require('telekasten').setup({
  home = vim.fn.expand("~/zettelkasten"),
  -- ... other Telekasten config
})

-- Setup md_toc
require('md_toc').setup({
  telekasten_integration = true,
})

-- Recommended keybindings for Telekasten workflow
vim.keymap.set('n', '<leader>zc', '<cmd>MdTocGenerate<cr>', { desc = 'Generate TOC' })
vim.keymap.set('n', '<leader>zu', '<cmd>MdTocUpdate<cr>', { desc = 'Update TOC' })
vim.keymap.set('n', '<leader>zh', '<cmd>MdTocGoto<cr>', { desc = 'Jump to heading' })

Advanced Usage

Programmatic API

You can also use the plugin programmatically in Lua:

local md_toc = require('md_toc')

-- Generate TOC
md_toc.generate()

-- Update TOC
md_toc.update()

-- Remove TOC
md_toc.remove()

-- Navigate to heading
md_toc.goto()

-- Access internal modules
local headings = md_toc.headings.extract_headings(0, config)
local anchor = md_toc.anchors.generate("My Heading", "gfm", {})

Custom Keybindings

Standalone usage (without Telekasten):

-- Basic TOC operations
vim.keymap.set('n', '<leader>tg', '<cmd>MdTocGenerate<cr>', { desc = 'Generate TOC' })
vim.keymap.set('n', '<leader>tu', '<cmd>MdTocUpdate<cr>', { desc = 'Update TOC' })
vim.keymap.set('n', '<leader>tr', '<cmd>MdTocRemove<cr>', { desc = 'Remove TOC' })
vim.keymap.set('n', '<leader>th', '<cmd>MdTocGoto<cr>', { desc = 'Jump to heading' })
vim.keymap.set('n', '<leader>tn', '<cmd>MdTocNav<cr>', { desc = 'Navigate TOC with Telescope' })

With Telekasten (recommended for Telekasten users):

-- Telekasten-style keybindings
vim.keymap.set('n', '<leader>nc', '<cmd>MdTocGenerate<cr>', { desc = 'Generate TOC' })
vim.keymap.set('n', '<leader>nu', '<cmd>MdTocUpdate<cr>', { desc = 'Update TOC' })
vim.keymap.set('n', '<leader>nh', '<cmd>MdTocGoto<cr>', { desc = 'Jump to heading' })

Advanced: Override gf to handle TOC links:

-- Make gf work with TOC entries (falls back to normal gf for other links)
vim.keymap.set('n', 'gf', function()
  local line = vim.api.nvim_get_current_line()
  if line:match('%[.-%]%(#.-%)') then
    vim.cmd('MdTocGoto')
  else
    vim.cmd('normal! gf')
  end
end, { buffer = true, desc = 'Follow link or TOC entry' })

Comparison with vim-markdown-toc

This plugin is inspired by vim-markdown-toc but offers:

  • Written in Lua for modern Neovim
  • Better integration with Neovim ecosystem (Telescope, Lua API)
  • Built-in Telekasten support
  • Simplified configuration using Lua tables
  • Active development for Neovim-specific features

Requirements

  • Neovim 0.7.0 or later
  • Optional: Telescope for enhanced navigation
  • Optional: Telekasten for note-taking integration

License

MIT License - see LICENSE file for details.

Contributing

Contributions welcome! Please feel free to submit issues or pull requests.

Acknowledgments