markdown.nvim · Editing · vimdoc

:help markdown.nvim

A self-contained Markdown toolkit for Neovim.

doc/markdown.nvim.txt — rendered from the plugin's own vimdoc

*markdown.nvim.txt*  A self-contained Markdown toolkit for Neovim.

CONTENTS *markdown.nvim-contents*

  1. Introduction .............. |markdown.nvim-intro|
  2. Requirements .............. |markdown.nvim-requirements|
  3. Installation .............. |markdown.nvim-installation|
  4. Setup ..................... |markdown.nvim-setup|
  5. Configuration ............. |markdown.nvim-config|
     5.1 General options ....... |markdown.nvim-config-general|
     5.2 blockquote_hl ......... |markdown.nvim-config-blockquote|
     5.3 fenced_fix ............ |markdown.nvim-config-fenced-fix|
     5.4 fenced_scope .......... |markdown.nvim-config-fenced-scope|
     5.5 toc ................... |markdown.nvim-config-toc|
     5.6 table .................. |markdown.nvim-config-table|
     5.7 features (gating) ..... |markdown.nvim-config-features|
     5.8 hover (link preview) .. |markdown.nvim-config-hover|
  6. Keymaps ................... |markdown.nvim-keymaps|
     6.1 Navigation ............ |markdown.nvim-keymaps-nav|
     6.2 Heading shift ......... |markdown.nvim-keymaps-shift|
     6.3 Folding ............... |markdown.nvim-keymaps-fold|
     6.4 TOC ................... |markdown.nvim-keymaps-toc|
     6.5 Cursor handler ........ |markdown.nvim-keymaps-handler|
     6.6 Bold / link wrap ...... |markdown.nvim-keymaps-bold|
     6.7 TableView ............. |markdown.nvim-keymaps-tableview|
     6.8 Deleting a link's file  |markdown.nvim-keymaps-delete|
  7. Commands .................. |markdown.nvim-commands|
     7.1 :Markdown ............. |:Markdown|
     7.2 links ................. |:Markdown-links|
     7.3 toc ................... |:Markdown-toc|
     7.4 refs .................. |:Markdown-refs|
     7.5 table ................. |:Markdown-table|
     7.6 render / preview / mdview / export |:Markdown-render|
     7.7 create ................ |:Markdown-create|
     7.8 headline_spacing ...... |:Markdown-headline-spacing|
     7.9 scope ................. |:Markdown-scope|
     7.10 list .................. |:Markdown-list|
     7.11 image ................. |:Markdown-image|
     7.12 gaps .................. |:Markdown-gaps|
     7.13 Buffer commands ....... |markdown.nvim-buf-commands|
     7.14 :MDTable* (wrapping) .. |markdown.nvim-mdtable|
  8. TableView ................. |markdown.nvim-tableview|
  9. Fold expression ........... |markdown.nvim-foldexpr|
 10. Lua API ................... |markdown.nvim-api|
 11. Architecture .............. |markdown.nvim-architecture|

1. INTRODUCTION *markdown.nvim-intro*

markdown.nvim is a self-contained Markdown toolkit for Neovim. All features
are FileType-scoped: keymaps and user commands are installed only for Markdown
buffers, and nothing is registered globally beyond one :Markdown command and
the :checkhealth handler.

Included subsystems:
  * Heading navigation and level shifting (count-aware)
  * Custom fold expression for ATX and Setext headings
  * Table of Contents generator (GFM slug + duplicate suffix)
  * Reference sync: auto-update #anchor links + TOC on heading rename
  * Visual bold (**) toggle
  * Link wrap (<leader>[) — wrap word/selection in []()
  * Headline spacing enforcer (blank-dash-blank between H2+ sections,
    including a closing separator after the final section)
  * Underline headings (:MarkdownNvimUnderlineHeadings): Setext-style =
    decoration below every ATX heading's text
  * Fenced-code and inline-code highlight override
  * Blockquote coloring via matchadd
  * Anchor / URL / image / file handler on double-click or ma
  * Floating Markdown table browser with HTML export/import (round-trip)
  * GFM table stack: formatter, auto-format table mode, tableize, cell motions
  * Link diagnostics: dead relative-file links + duplicate heading anchors
    via vim.diagnostic
  * Unified :Markdown command: links, toc, table, render, preview, mdview,
    create, headline_spacing, gaps
  * Width-limited table wrapping (:MDTable*): wraps over-wide cells onto
    GFM-valid continuation rows, with unwrap, lint, CSV roundtrip, and more

2. REQUIREMENTS *markdown.nvim-requirements*

  * Neovim >= 0.9
  * lib.nvim (https://github.com/StefanBartl/lib.nvim) — REQUIRED. Supplies
    the :Markdown/:TableView* command layer (lib.nvim.bindings.usercmd.composer) and
    core/table_mode.lua's buffer debouncing.

No other external tools required. Every other feature uses built-in Neovim
APIs.

3. INSTALLATION *markdown.nvim-installation*

lazy.nvim:
  {
    "StefanBartl/markdown.nvim",
    dependencies = { "StefanBartl/lib.nvim" },
    ft = { "markdown", "mdx", "md" },
    config = function()
      require("markdown").setup()
    end,
  }
Local development:
  {
    dir = "E:/repos/markdown.nvim",
    ft  = { "markdown", "mdx", "md" },
    config = function()
      require("markdown").setup()
    end,
  }

4. SETUP *markdown.nvim-setup*

Call setup() once during Neovim startup:

  require("markdown").setup({
    -- options (see |markdown.nvim-config|)
  })
setup() is idempotent — calling it twice has no effect.

On FileType events matching markdown / mdx / md, the plugin installs:
  * Buffer-local keymaps (see |markdown.nvim-keymaps|)
  * Buffer-local user commands (see |markdown.nvim-buf-commands|)
  * TableView keymaps and commands (see |markdown.nvim-tableview|)

Already-open Markdown buffers are also covered: the plugin applies its
keymaps and commands to every loaded buffer whose filetype matches.

5. CONFIGURATION *markdown.nvim-config*

All keys are optional. Unset keys use the defaults shown below.

For copy-paste-ready setup() snippets covering common customizations
(rather than the full reference below), see docs/templates/ in the repo —
GitHub-only, not duplicated into this helpfile.

5.1 General options *markdown.nvim-config-general*

  require("markdown").setup({
    features = {},  -- feature gating; see |markdown.nvim-config-features|
    progress_style         = "auto",  -- indicator for :Markdown links show|sanitize cwd
    map_double_asterisk    = true,
    map_wrap_link          = true,
    keep_inner_selection   = true,
    protect_h1             = false,
    nav = { fences = true },  -- <C-p>/<C-f> also stop on fence delimiters
    use_zf_override        = true,
    enable_autocmds        = true,
    enable_keymaps         = true,
    ft_only                = true,
    ensure_headline_spacing = true,
    underline_headings = { char = "=" },  -- :MarkdownNvimUnderlineHeadings
    keymaps = {},  -- per-binding disable/remap by id (see |markdown.nvim-keymaps|)
    tableview = { style = "markdown" },  -- default float style: "markdown" | "box"
    link_hl = { underline = false },
    refs = {
      mode        = "save",   -- "off" | "save" | "live"
      debounce_ms = 2000,
      update_toc  = true,
      orphans     = "report", -- "report" | "ignore"
      -- toc_header = "## Table of content", -- unset: falls back to toc.header
    },
    toc   = {
      header = "## Table of content", marker = "-",
      min_level = 2, max_level = 4,
      anchor_style = "gfm", anchor_separator = "-",
    },
    table = { header_align = "center", entry_align = "center" }, -- + col_overrides
    links = {
      picker = "hover_select", -- "hover_select"|"select"|"telescope"|"fzf"
      diagnostics = { mode = "off" }, -- "off" | "save"
    },
    open  = { external_extensions = { "png", "pdf", "mp4", ... } },
  })
progress_style
  Indicator style for the scope-wide operations that walk every *.md file
  under a directory — :Markdown links show cwd and `:Markdown links
  sanitize cwd`. Once that is more than 20 files they scan (and, for
  sanitize, rewrite) in chunks across event-loop ticks so the editor never
  freezes; a smaller tree stays synchronous. Provided by lib.nvim's
  lib.nvim.progress: "statusline" feeds its headless registry, the rest
  render directly. One of "auto" (default), "notify", "statusline",
  "fidget", "float", "kit".

map_double_asterisk
  When true, typing ** in visual mode toggles bold on the selection.
  Set to false if you use a surround plugin that provides this.

map_wrap_link
  When true, <leader>[ wraps the word under the cursor (normal mode) or
  the visual selection in a Markdown link. The inner text is auto-classified:
  a URL or file path goes into the (target) part, plain text into the
  [text] part. See |markdown.nvim-keymaps-bold|.

keep_inner_selection
  After wrapping text with **, restore the visual selection to the inner
  text only (without the asterisks). When false, selects the full wrapped
  region.

protect_h1
  When true, heading shift operations refuse to move H1 further up (i.e.,
  H1 cannot become plain text via <C-Left>). H2+ shifting is unaffected.

nav.fences
  When true (default), <C-p>/<C-f> and [[/]] stop on a fenced
  block's opening ``lang line and on its closing `` line as well as on
  headings — a code block is the other landmark of a markdown file, and it
  used to take a separate motion to reach. Set to false for the previous
  headings-only behaviour. The by-level hops {N}<leader><C-p> /
  {N}<leader><C-f> are headings-only either way, so heading-only
  navigation is always available. See |markdown.nvim-keymaps-nav|.

use_zf_override
  When true, zf is remapped to "toggle fold under cursor + center" in
  Markdown buffers, overriding the default Vim zf operator.

enable_autocmds
  When false, no FileType autocmds are registered — you must call
  require("markdown.setup.keymaps").apply(bufnr) manually.

enable_keymaps
  Currently unused at runtime; all keymaps are controlled by enable_autocmds.
  Reserved for finer-grained control in future versions.

ft_only
  Reserved. Currently all features are ft-only by design.

ensure_headline_spacing
  When true (default), refreshing the TOC (<leader>toc or
  :Markdown toc) also enforces [blank]---[blank] spacing between
  consecutive H2+ sections and adds a closing --- after the final section.
  Override per call with the --sep / --no-sep flags of :Markdown toc,
  or apply on demand with :Markdown headline_spacing (see
  |:Markdown-headline-spacing|).

check_heading_gaps
  When true (default), refreshing the TOC (<leader>toc or
  :Markdown toc) also checks for skipped heading levels (e.g. an H1
  followed directly by an H3, with no H2 in between) and, when any are
  found, notifies and asks whether to fix them immediately. Override per
  call with the --check-gaps / --no-check-gaps flags of :Markdown toc,
  or run the check on demand with :Markdown gaps (see |:Markdown-gaps|).

underline_headings
  char (default "=") is the underline character drawn below each ATX
  heading's text by :MarkdownNvimUnderlineHeadings (see
  |:MarkdownNvimUnderlineHeadings|).

links
  Options for :Markdown links show/check/sanitize (see
  |:Markdown-links|).
    links = {
      picker      = "hover_select", -- "hover_select"|"select"|"telescope"|"fzf"
      diagnostics = { mode = "off" }, -- "off" | "save"
      sanitize_on_save = true,       -- normalize link targets before a write
      repair_env_prefix = true,      -- './$VAR/x' -> '$VAR/x' (variable set)
      cursor = { enable = true, startinsert = true, path_cursor = "end" },
    }
  picker: "hover_select" uses lib.nvim's floating hover_select;
  "select" falls back to |vim.ui.select()|; "telescope" /"fzf" use
  nvim-telescope/telescope.nvim / ibhagwan/fzf-lua (soft dependencies — a
  requested backend whose plugin isn't installed falls back to
  vim.ui.select() with a warning).

  diagnostics.mode: dead relative-file links and duplicate heading titles
  are surfaced via |vim.diagnostic| (namespace "markdown_links"). The manual
  :Markdown links check command always works; mode = "save" also reruns
  it automatically on |BufWritePost|.

  sanitize_on_save (default true) runs :Markdown links sanitize on the
  current buffer before every write. Set to false to only ever sanitize
  manually.

  Env-rooted targets ($VAR/x, ${VAR}/x, %VAR%/x) never get a ./
  prefix. repair_env_prefix (default true) also strips the ./ an older
  version wrote in front of one (./$VAR/x -> $VAR/x), but only when the
  variable is set.

  cursor (after the link-wrap keymap): the cursor goes where the link still
  needs typing -- the empty title of [](url), else the path of [text]() --
  and into insert mode. startinsert = false only skips insert mode;
  enable = false leaves the cursor inside the link in normal mode.

open
  Controls how followed file targets open (see |markdown.nvim-keymaps-handler|
  and |:Markdown-links|).
    open = { external_extensions = { "png", "pdf", "mp4", ... } }
  Targets whose extension is in external_extensions are launched with the
  system application (image viewer, PDF reader, media player, …). Every other
  (text-like) target opens in the current window via :edit. The default list
  covers common image, video, audio, office, archive and binary formats;
  extend or replace it to taste.

image                                            *markdown.nvim-config-image*
  What mi does with an image target when an in-Neovim preview provider
  (images.nvim, snacks.nvim or image.nvim, all optional) is installed.
    image = { preview = "ask" }  -- "ask" | "preview" | "system"
  With no provider installed every value behaves like "system". See
  |markdown.nvim-image-preview| for the full behaviour.

link_hl                                        *markdown.nvim-config-link-hl*
  Inline-link highlight tweaks.
    link_hl = { underline = false }
  Neovim's markdown treesitter underlines link URLs/labels; a long URL then
  draws a full-width underline across the soft-wrapped screen line. With
  underline = false (default) that underline is stripped from the
  markdown_inline link groups only (other filetypes are untouched). Set to
  true to restore the built-in behaviour.

tableview                                   *markdown.nvim-config-tableview*
  Default float style for :Markdown table view toggle / <leader>tvt.
    tableview = { style = "markdown" }   -- "markdown" | "box"
  "markdown" is the aligned GFM table; "box" is a Unicode box-drawing
  "spreadsheet" grid. The explicit view markdown / view box actions (and
  <leader>tvx) always override this default. See |:Markdown-table|.

menu                                             *markdown.nvim-config-menu*
  Context-aware entries for nvzone/menu (soft, opt-in). markdown.nvim does not
  depend on a menu plugin; it provides entries and a host composes them.
    menu = { enable = true, fold = true, toc = true, refs = true }
  The fold entries (Fold/Unfold Heading, Fold below H2 toggle, Unfold All) appear only when
  the cursor is on a heading; TOC and refs entries are always available. Get the
  entries from require("markdown.integrations.menu").items() (inline
  list) or .submenu() (a single fly-out), and compose into your menu, e.g.
  require("menu").open(vim.list_extend(md.items(), require("menus.custom"))).

integrations                              *markdown.nvim-config-integrations*
  Which hosts may drive this plugin.
    integrations = { ui_menu = true }
  ui_menu = false keeps ui.nvim's right-click menu (ui.menu) from composing
  the Markdown fly-out; items()/submenu() still work for other hosts.

refs                                             *markdown.nvim-config-refs*
  Keep in-document [text](#anchor) links and the TOC in sync when headings
  are renamed (see |:Markdown-refs|).
    refs = {
      mode        = "save",   -- "off" | "save" | "live"
      debounce_ms = 2000,
      update_toc  = true,
      orphans     = "report", -- "report" | "ignore"
      -- toc_header = "## Table of content", -- unset: falls back to toc.header
    }
  mode governs only AUTOMATIC syncs; the manual :Markdown refs commands
  work regardless.
    "off"    No automatic sync (manual commands only).
    "save"   Reconcile on |BufWritePre| (default).
    "live"   Reconcile after edits, debounced by debounce_ms milliseconds
             (default 2000; 1500–3000 is a sane range) so it never runs on the
             hot path.
  update_toc refreshes an existing TOC block during a sync (it never
  force-creates one). orphans = "report" surfaces links whose #anchor
  matches no heading; "ignore" skips that report. toc_header is the TOC
  header line used to detect/refresh the block; unset by default, in which
  case it falls back to toc.header (see |markdown.nvim-config-toc|) so the
  two never drift apart unless you deliberately want refs to look for a
  DIFFERENT header than the one :Markdown toc itself generates.

5.2 blockquote_hl *markdown.nvim-config-blockquote*

  blockquote_hl = {
    marker_fg   = "#6A9955",  -- the > token; false = colorscheme-derived
    text_fg     = "#7EE787",  -- text after >; false = colorscheme-derived
    text_bg     = "dimm",     -- or a hex color or nil to disable
    text_bold   = true,
    text_italic = false,
    width       = "block",    -- "block" | "line" | "window" | <columns>
    -- link     = nil  or a highlight group name (overrides all other fields)
  },
Two separate highlight groups are created:
  MarkdownBlockquoteMarker   the > token
  MarkdownBlockquoteText     everything after > , plus padding up to
                               the width set by width (below)

Both are applied via a decoration provider (extmarks) at priority 110, which
wins over Tree-sitter's @punctuation.special.markdown (priority 100). Being
extmark-based (not matchadd()), the text region's background can extend
past the last character.

width sets how far the background reaches:
  "block"   (default) as wide as the widest line of the contiguous >
              block — one box per quote; a blank line starts a new block
  "line"    only behind each line's own text
  "window"  to the window edge (hl_eol, the former behavior)
  <number>    at least that many display columns (a longer line is not cut)

marker_fg / text_fg default to a fixed VS Code-style green, independent
of the active colorscheme — some themes' Comment / String groups (the
colorscheme-derived fallback below) are muted greys/blues that don't read as
"quoted". Set either field to a hex color to override, or to false to opt
back into colorscheme derivation: a markdown-specific highlight group first,
then Comment / String, then this same hex as the last-resort fallback —
re-derived on every |ColorScheme| event.

text_bg = "dimm" (the default) derives a background from marker_fg by
mixing 20% of its color toward black, filled as far as width says.

DON'T WANT THIS?                          *markdown.nvim-config-blockquote-off*
This VS Code-style coloring (green marker/text + dimmed whole-line
background) is ON by default. You must say so explicitly if you don't want
it — nothing about the plugin auto-detects "matches your colorscheme
already" or similar. The three ways to back out, from least to most drastic:
  1. text_bg = nil                        — keep the colors, drop the bg fill
  2. marker_fg = false, text_fg = false    — colorscheme-derived colors instead
  3. blockquote_hl = { link = "Normal" }   — fully flat, no special styling
Ready-to-paste snippets for all three (plus custom-color and link-group
examples) are in the repo under docs/templates/blockquote-hl.md.

5.3 fenced_fix *markdown.nvim-config-fenced-fix*

  fenced_fix = {
    inline_base_hl = { "DiagnosticWarn", "Special", "Constant", "String" },
    inline_style   = { italic = false, bold = false },
    delimiter_hl   = "Comment",
  },
inline_base_hl
  Ordered list of highlight groups tried in sequence. The first one that
  exists in the current colorscheme is used as the base color for inline
  backtick code spans.

inline_style
  Additional style flags (bold, italic, underline, undercurl) applied on
  top of the base color.

delimiter_hl
  Highlight group for the backtick delimiters (`  ``).

The fix also clears @markup.raw.block / @markup.fenced_code.block so
that injected language tokens use their own colors rather than a blanket
single-color overlay. Re-applied automatically on :colorscheme changes.

5.4 fenced_scope *markdown.nvim-config-fenced-scope*

  fenced_scope = {
    enable   = true,
    langs    = { "markdown", "md", "mdx", "ascii-markdown", "ascii-md" },
    provider = "auto",   -- "auto" | "color_my_ascii" | "builtin"
    operations = {
      toc = true, nav = true, jump = true, shift = true, fold = true,
    },
  },
Treat a markdown-family fenced block as its own document scope. When the
cursor is inside such a block, the heading-aware operations act on the
block's interior; when the cursor is outside, they act on the whole file but
skip every fenced block's interior. On by default.

enable     Master switch. When false, every operation reverts to its
             whole-buffer behavior (turning it off is a true no-op).
langs      Fence tags that count as a markdown sub-document.
provider   Fence-detection backend. "auto" uses color_my_ascii's fence API
             when installed, otherwise a small built-in scanner. color_my_ascii
             is a soft dependency; the feature works without it.
operations Per-operation opt-out:
               toc    Generate/insert the TOC inside the block; outside, the
                      outer TOC skips fenced interiors.
               nav    Heading navigation stays within the block; outside, it
                      jumps over fenced blocks.
               jump   Anchor jump resolves within the block.
               shift  Whole-"buffer" heading shift shifts only the block.
               fold   Scope-aware 'foldexpr': a # inside a NON-markdown fence
                      (e.g. a shell comment) no longer opens a fold.

Toggle at runtime with :Markdown scope (see |:Markdown-scope|).

Nesting: to nest a fenced block inside a ```markdown block, the outer fence
must be longer (CommonMark), e.g. open the outer block with ````markdown.

5.5 toc *markdown.nvim-config-toc*

  toc = {
    header    = "## Table of content",
    marker    = "-",             -- bullet prefix, e.g. "-" or "*"
    min_level = 2,
    max_level = 4,
    anchor_style     = "gfm",    -- "gfm" | "keep-case"
    anchor_separator = "-",
  },
Defaults for <leader>toc / :Markdown toc (see |markdown.nvim-keymaps-toc|
and |:Markdown-toc|).

header     TOC header line to insert/detect. Shared with refs.toc_header
             when the latter is left unset (see |markdown.nvim-config-refs|).
marker     Bullet prefix for every TOC entry.
min_level / max_level
             Default heading-level range included. :Markdown toc [level]
             (or max=N) still overrides max_level per call.
anchor_style
             "gfm" (default): lowercase, GitHub-style de-dup source slug.
             "keep-case": same shape, original case preserved.
anchor_separator
             Word separator in generated anchors (default "-"). Shared by
             core.toc and core.slug.heading_anchors(), so core.refs and
             core.link_diagnostics produce anchors that agree with the TOC.
             slug.gfm() itself always stays byte-for-byte the historical
             algorithm, regardless of this config.

:Markdown toc also accepts min=N, max=N, and marker=X as per-call
overrides (in addition to the legacy bare-number shorthand for max=N).

5.6 table *markdown.nvim-config-table*

  table = {
    header_align = "center",   -- "left" | "center" | "right"
    entry_align  = "center",
    -- col_overrides = { { col = 1, align = "left" }, { col = "Name", align = "left" } },
  },
Defaults for :Markdown table format (see |:Markdown-table|); explicit
command args (header=, cell=, skip=) always override these per call.
col_overrides[].col may be a 1-based column index or a header-cell name
(case-insensitive); it also accepts max/min for width-limited wrapping
(see below).

table.wrap / table.wrap_profiles configure the :MDTable* width-limited
wrapping command family; see |markdown.nvim-mdtable| for the full option
list, per-table directive syntax, and API hooks.

5.7 features (gating) *markdown.nvim-config-features*

Reduce the plugin to a subset without unsetting each option.

    require("markdown").setup({
      features = {
        -- disable = "all"                    -- turn every feature off
        -- disable = { "tableview", "refs" }  -- turn off just these
        -- enable  = { "table" }              -- re-enable (after disable)
        just_enable = { "table", "toc" },     -- ONLY these run
      },
    })
Precedence: just_enable (if set) wins — only the listed features stay on and
everything else is off. Otherwise the resolver starts all-on, applies
disable, then re-applies enable. Unknown names emit a warning.

Gateable feature names (also returned by `require("markdown.config")
.features()`):
    keymaps  fold  hl  link_hl  fenced_fix  fenced_scope  tableview  refs
    links  toc  table  render  preview  mdview  create  headline_spacing  scope
    list  image  underline_headings  table_wrap  hover

A disabled :Markdown subcommand drops out of completion and reports if
invoked; disabled keymaps and autocmds are never installed. The legacy
enable_keymaps / enable_autocmds flags still work alongside this gating.

5.8 hover (link preview) *markdown.nvim-config-hover*

Rest the cursor on a link and a small float previews what it points at: an
image, a PDF's first page, another file's section, a directory listing, an
in-page anchor, a URL -- or, when the target does not exist, that fact,
which is often the most useful answer of all.
    require("markdown").setup({
      hover = {
        enabled = true,
        trigger = { "CursorHold" },  -- add "mouse" to follow the pointer
        delay_ms = 250,
        max_lines = 20,
        max_width = 80,
        border = "rounded",
        inline_images = true,
        url = {
          fetch = false,
          timeout_ms = 2000,
        },
      },
    })

Options:

    enabled        (boolean)  Master switch. Default true.
    trigger        (string[]) "CursorHold" and/or "mouse".
                              Default { "CursorHold" }.
    delay_ms       (integer)  Debounce before the float opens. Default 250.
    max_lines      (integer)  Preview line cap, and the float's height cap.
                              Default 20.
    max_width      (integer)  Float width cap in display columns. Default 80.
    border         (string)   As |nvim_open_win()|. Default "rounded".
    inline_images  (boolean)  Draw images / PDF pages into the float.
                              Default true.
    url.fetch      (boolean)  Fetch <title>/<meta description> for http(s)
                              targets. Default FALSE -- see below.
    url.timeout_ms (integer)  Per-fetch timeout. Default 2000.

The "mouse" trigger additionally requires 'mousemoveevent', a global option
this plugin deliberately does not set for you. Without it, that trigger
simply never fires.

url.fetch is off by default on purpose: a hover that silently issues HTTP
requests would disclose every link you brush past to its host, and would
turn a link-heavy document into a request storm while scrolling. With it
off, URLs still preview -- parsed into host, path and decoded query,
entirely locally.

Drawing pictures into the float needs images.nvim specifically: it is the
only supported provider that can draw into a window it does not own. With
snacks.nvim or image.nvim you still get the metadata float, just without
the picture. PDF page rendering additionally needs pdfport.nvim, and runs
asynchronously -- move the cursor away and the render is discarded.

On demand, regardless of enabled:
    require("markdown").hover()       -- preview the link under the cursor
    require("markdown").hover_hide()  -- close it

    vim.keymap.set("n", "K", require("markdown").hover, { buffer = true })
Mapping K replaces |vim.lsp.buf.hover()| in that buffer; keep the mapping
buffer-local as shown.

Full write-up: docs/hover.md.

6. KEYMAPS *markdown.nvim-keymaps*

All keymaps are buffer-local and installed only for Markdown filetypes.
None of the keys are mapped globally.

Per-binding control (recommended). Every default key has a stable id (see the
editing list in docs/BINDINGS.md). Set keymaps[id] to disable or remap a
single binding; everything else keeps its default:

    require("markdown").setup({
      keymaps = {
        jump_anchor = false,            -- disable this binding
        toc         = "<leader>T",      -- remap to a new key (same mode)
        fold_toggle = { lhs = "<F2>" }, -- table form; may also override mode
      },
    })
enable_keymaps = false disables ALL default keys at once. The legacy flags
(map_double_asterisk, map_wrap_link, use_zf_override) still work too.

Free-form remapping: every action is also a plain function on
require("markdown").actions, bindable by hand (no <Plug> layer):

    local a = require("markdown").actions
    vim.keymap.set("n", "<C-n>", a.next_heading, { desc = "Next heading" })
    vim.keymap.set("n", "gO",    a.toc,          { desc = "Insert/refresh TOC" })
The full list of action names and ids lives in docs/BINDINGS.md. If which-key
is installed, the <leader>t prefix is labelled "Markdown" automatically.

6.1 Navigation *markdown.nvim-keymaps-nav*

Key Mode Action

  <C-p>              n/v/x  Jump to previous heading or fence delimiter
  [[                 n      Jump to previous heading or fence delimiter
  <C-f>              n/v/x  Jump to next heading or fence delimiter
  ]]                 n      Jump to next heading or fence delimiter
  {N}<leader><C-p>   n      Jump to previous heading of level N
  {N}<leader><C-f>   n      Jump to next heading of level N

With a count, the plain variants repeat N times (e.g., 3<C-f> skips three
landmarks forward). With the <leader> variants, the count sets the heading
level to match (e.g., 2<leader><C-f> finds the next ## heading).

The plain variants stop on a fenced block's opening ```lang line and on its
closing `` line as well as on headings; set nav = { fences = false }` for
headings only (see |markdown.nvim-config-general|). The <leader> variants
are headings-only regardless, which is what keeps heading-only navigation
available when fence stops are on.

6.2 Heading shift *markdown.nvim-keymaps-shift*

Key Mode Action

  <C-Right>   n      Increase heading level on current line
  <C-Left>    n      Decrease heading level on current line
  <C-Right>   v / x  Increase heading level on visual selection
  <C-Left>    v / x  Decrease heading level on visual selection
  <S-Right>   n      Increase all headings in buffer
  <S-Left>    n      Decrease all headings in buffer

Notes:
  * A {count} prefix shifts by that many levels (e.g. 2<C-Right> adds
    two #). Without a count, one level is used.
  * In normal mode (single line): plain text can be promoted to # .
  * In visual mode (multi-line): only existing headings are modified;
    plain text lines are skipped.
  * Lines inside fenced code blocks are never modified.
  * If protect_h1 = true, H1 headings cannot be shifted further.

6.3 Folding *markdown.nvim-keymaps-fold*

Key Mode Action

  zf                  n     Toggle fold under cursor + center (if use_zf_override)
  <localleader>f      n     Toggle fold under cursor + center
  zu                  n     Unfold all, center
  zi                  n     Fold previous heading then center
  zk                  n     Toggle outline: fold below H2 (keep H1+H2) / unfold

See also |markdown.nvim-foldexpr|.

6.4 TOC *markdown.nvim-keymaps-toc*

Key Mode Action

  {N}<leader>toc   n     Insert or refresh TOC; count N sets max heading level

The TOC header line and bullet marker come from config.toc (default
"## Table of content" / "-", see |markdown.nvim-config-toc|). Anchors use
GFM-compatible slugs by default (toc.anchor_style/anchor_separator opt
into other renderers' conventions). Duplicate headings receive numeric
suffixes (GitHub convention: slug, slug-1, slug-2, ...).

Placement rules:
  1. If a ## Table of content block already exists, it is replaced in-place.
  2. If a first-level heading (#) exists, the TOC is inserted before the
     first following ## heading.
  3. Otherwise the TOC is appended to the end of the file.

Spacing is normalized: exactly one blank line before the TOC header, one
blank line before the --- separator, and one blank line after it.

Every refresh also checks for skipped heading levels (see
check_heading_gaps, |markdown.nvim-config-general|); when gaps are found,
you are notified and asked whether to fix them right away.

6.5 Cursor handler *markdown.nvim-keymaps-handler*

Key Mode Action

  <2-LeftMouse>    n     Handle target under cursor
  <C-LeftMouse>    n     Handle target under cursor
  ma               n     Handle target under cursor
  mi               n     Open image under cursor
  mj               n     Jump to anchor under cursor

Dispatch priority:
  1. Markdown (#anchor) link inside a TOC block or list line.
  2. HTML anchor link (<a href="#id">, etc.).
  3. External file link with optional fragment (file.md#anchor).
  4. Image (![alt](path), <img src="...">).
  5. URL (https?://...).
  6. Local file ([text](path)).

Opening rules:
  * URLs open in the default browser (via |vim.ui.open()| where available,
    falling back to a system command).
  * Internal #anchor links jump in-buffer.
  * File targets whose extension is in open.external_extensions (images,
    PDFs, media, …) open with the system application; all other (text-like)
    files open in the current window via :edit. See
    |markdown.nvim-config-general| for the extension list.
  * PDFs additionally offer rendering into a Neovim buffer when
    pdfport.nvim is installed.
  * Images additionally offer an in-Neovim preview when images.nvim,
    snacks.nvim (Snacks.image) or image.nvim is installed — see
    |markdown.nvim-image-preview|. With none installed the system
    application is used directly, with no prompt.

                                            *markdown.nvim-image-preview*

Image preview (`mi`)

  All three providers are soft dependencies: nothing is required, and
  nothing changes for a setup that has none installed.

  image.preview selects the behaviour when one IS installed:
    require("markdown").setup({
      image = {
        preview = "ask",  -- "ask" | "preview" | "system"
      },
    })
  "ask"      Prompt for "System app" vs. "Preview in Neovim". Default.
  "preview"  Always render in a floating window, no prompt.
  "system"   Always hand off to the system viewer, no prompt. This is the
             behaviour from before the option existed.

  The float closes with q or <Esc>. A remote image (an http(s)://
  target) always goes to the system handler regardless of this setting —
  there is no local file for a provider to read. If a preview fails (a
  terminal with no image protocol, an unreadable file), the system viewer
  is used instead, so mi always shows the image somewhere.

  When several providers are installed, images.nvim is preferred: it is the
  only one of the three that draws on native Windows Neovim in WezTerm
  (snacks.nvim and image.nvim both speak only the Kitty graphics protocol,
  which Neovim's own output layer never gets drawn there — see images.nvim's
  README, "Why not snacks.image or image.nvim"). It renders via
  images.browse.draw_in_window(), the same primitive images.nvim's own
  :Image pickers preview uses. Without images.nvim, snacks.nvim is
  preferred over image.nvim: it renders through its own buffer hook, so it
  needs no explicit placement or teardown.

6.6 Bold / link wrap *markdown.nvim-keymaps-bold*

Key Mode Action

  **           v     Toggle **bold** on visual selection
  **           V     Toggle **bold** on every selected line
  <leader>[    n     Wrap word under cursor in a Markdown link
  <leader>[    v     Wrap visual selection in a Markdown link

Bold (**):
  If the selected text is already surrounded by **, the asterisks are
  removed. Otherwise ** is added on both sides. Whether the inner text
  or the full wrapped text is re-selected is controlled by
  keep_inner_selection (see |markdown.nvim-config-general|).
  Requires map_double_asterisk = true (default).

  Charwise (v) wraps exactly what is selected. Linewise (V) wraps the
  whole line, one span per selected line, since that is what selecting a
  line means. Indent, a blockquote's >, a list bullet or number, a
  task-list checkbox, an ATX heading's hashes and trailing hard-break
  spaces stay outside the wrap — **- item** is no longer a list item.
  A range whose every non-blank line is already bold unwraps instead.

Link (<leader>[):
  The inner text is auto-classified:
    * URL or file path -> [](target), cursor placed inside []
    * plain text       -> [text](),  cursor placed inside ()
    * empty / blank    -> [](),       cursor placed inside []
  A path separator, a scheme:// prefix, mailto:, or a name.ext
  shape marks the text as a target. Requires map_wrap_link = true
  (default).

6.7 TableView *markdown.nvim-keymaps-tableview*

Key Mode Action

  <leader>tvt   n     Toggle floating table preview (aligned Markdown style)
  <leader>tvx   n     Toggle floating table preview (box-drawing / spreadsheet)
  <leader>tvs   n     Open table selector (list of all tables in buffer)
  <leader>tvb   n     Export table at cursor as basic HTML, open in browser
  <leader>tvc   n     Close TableView floating window
  <leader>tvm   n     Toggle table mode (auto-format on edit)
  ]|            n     Jump to the next cell on the current table row
  [|            n     Jump to the previous cell on the current table row
  <leader>mtf   n     Format the table at the cursor -- identical to
                      :Markdown table format (id table_format)

Inside the floating preview itself (buffer-local to the popup, Normal mode
only — not active anywhere else):

Key Action

  <M-Right> / <M-l>  Widen the column under the cursor
  <M-Left>  / <M-h>  Narrow the column under the cursor (floors at its
                     natural content width — never truncates)
  <M-Up>    / <M-k>  Move the row under the cursor up (swap with the row
                     above)
  <M-Down>  / <M-j>  Move the row under the cursor down (swap with the row
                     below)
  :w                 Write the current row order back to the source

The h/j/k/l forms are a fallback: some terminals/multiplexers intercept
<M-Up>/<M-Down> (commonly for scrollback or pane navigation) before Neovim
sees them. Row-move never changes the row count; column-resize never
changes row order. A cursor on a border, separator, or (in a stacked
multi-table view) a "── Table i/N ──" label line is a no-op for all of them.
In a stacked view, each table's column widths and edits are tracked
independently.

:w writes the row order back to wherever the shown table(s) actually came
from: the source buffer (marks it modified, does not save it) if rendered
from a live buffer, or the file directly (writes to disk immediately) for
the %/cwd/path scopes reading files that aren't open as buffers. Column
widening is a reading aid only and is never written back — the written
table always uses natural, unpadded widths. A table with no known source is
skipped.

See |markdown.nvim-tableview| and |:Markdown-table| for details.

6.8 Deleting a link's file *markdown.nvim-keymaps-delete*

Key Mode Action

  DD    n     Delete the line, and the file its first link points at

On a line whose first link resolves to a file that exists, a confirmation
dialog names the resolved path and says how many other links point at the
same file (core/file_refs.lua scans every *.md under the cwd for that
count, with a ripgrep prefilter). Answering yes deletes the file and then
the line; answering no, or pressing <Esc>, leaves both alone.

On any other line it is plain dd, v:count included -- a key that stands
in for dd has to be at least dd. "Any other line" covers a line with no
link, a URL, a mailto:, an in-document #anchor, a target that resolves
to a directory, and a target that does not exist on disk (the last of those
also says so: a dead link is worth knowing about).

Two things worth knowing before binding it:

  * DD puts a mapping in front of the built-in D, which then waits
    'timeoutlen' for a second key. Only in Markdown buffers, but it is a
    real cost. keymaps.delete_link_file = false drops the binding;
    keymaps.delete_link_file = "<leader>dl" moves it.
  * The dialog is lib.nvim's ui.kit.confirm. Without lib.nvim there is
    no way to ask, so the key falls back to plain dd and warns -- it never
    deletes a file unasked.

The buffer is re-read against the remembered line both after the reference
scan and after the dialog is answered; if it moved in the meantime, nothing
is deleted.

7. COMMANDS *markdown.nvim-commands*


7.1 :Markdown *:Markdown*

:Markdown {subcommand} [args]

  Global command dispatcher for all Markdown utilities. It accepts a range,
  so visual selections are honoured by range-aware subcommands (e.g.
  create fs). Tab-completion is available at every level: subcommands,
  their actions, and their options.

  Subcommands:
    links              Show or generate Markdown links  |:Markdown-links|
    toc                Insert/refresh the TOC           |:Markdown-toc|
    refs               Sync #anchor links + TOC          |:Markdown-refs|
    table              view/format/new/mode/tableize/import |:Markdown-table|
    render             Toggle render-markdown.nvim       |:Markdown-render|
    preview            Start/stop mdview.nvim            |:Markdown-render|
    mdview             Open file via mdview.nvim         |:Markdown-render|
    export             Export to PDF via pdfport.nvim    |:Markdown-export|
    create             Create files for link targets     |:Markdown-create|
    headline_spacing   Enforce section separators   |:Markdown-headline-spacing|
    scope              Toggle fenced-block scope         |:Markdown-scope|
    list               List document items in a picker   |:Markdown-list|
    image              Delegate to images.nvim paste/screenshot |:Markdown-image|
    gaps               Check/fix skipped heading levels  |:Markdown-gaps|

7.2 :Markdown links *:Markdown-links*

:Markdown links show [%|cwd|<file>]

  Scan for links and open the chosen one through a picker. The source is the
  current buffer (%, the default), the current working directory (cwd),
  or a given file. Selecting an entry opens it:
    * URL      -> default browser
    * #anchor  -> in-buffer jump
    * file     -> system application or :edit (see
                  |markdown.nvim-config-general|, open).
  The picker backend is set by the links.picker option.

  When the scanned links include at least one image and both
  snacks.picker and images.nvim are installed, show routes through a
  snacks.picker picker instead, with a live image preview per image link
  (images.browse.draw_in_window()) — links.picker is ignored in that
  case, since none of its backends support a per-item live preview (the same
  constraint that applies to pickers.nvim). Without both deps, or with no image link in
  the results, show behaves exactly as above.

:Markdown links create [-r] [--noignore] [--root <path>] <path>

  Generate Markdown-style links for all files under <path> and copy them to
  the system clipboard (+ register).

  Arguments:
    <path>           Path to a file or directory.
    -r / --recursive Recurse into sub-directories.
    --noignore       Do not skip common directory names (.git, node_modules,
                     build, dist, …).
    --root <path>    Prepend this path to every generated link. Supports
                     $ENV_VAR expansion.

  Example:
    :Markdown links create -r --root $DOCS_ROOT ./docs
  A bare path with no subcommand (:Markdown links ./docs) is treated as
  create ./docs for backwards compatibility.

:Markdown links check

  Flag dead relative-file links and duplicate heading titles in the current
  buffer via |vim.diagnostic| (namespace "markdown_links"; reuses
  core.link_scan + core.slug). Cross-file path#anchor links only check
  that the file exists — validating an anchor inside another file is out of
  scope. See links.diagnostics (|markdown.nvim-config-general|) to also run
  this automatically on save.

:Markdown links sanitize [%|cwd|<file>]

  Normalize inline-link targets in the current buffer (%, the default),
  every *.md file under the working directory (cwd), or a given file:
    * Backslashes become forward slashes.
    * A bare relative path gets a ./ prefix.
  Examples: [t](doc.md) -> [t](./doc.md); [t](.\doc\file.md) ->
  [t](./doc/file.md).

  Left untouched: URLs (https://...), scheme targets (mailto:..., a
  Windows drive letter C:\...), #anchor-only links, absolute paths
  (/...), and ~-relative paths. Already-./- or ../-prefixed targets
  are kept (backslashes are still normalized).

  Runs automatically on the current buffer before every write, governed by
  the links.sanitize_on_save option (default true; see
  |markdown.nvim-config-general|).

7.3 :Markdown toc *:Markdown-toc*

:Markdown toc [level] [min=N] [max=N] [marker=X] [--sep | --no-sep]
              [--check-gaps | --no-check-gaps]

  Insert or refresh the Table of Contents (see |markdown.nvim-keymaps-toc|
  for placement and slug rules), using config.toc for anything not given
  here (see |markdown.nvim-config-toc|). A bare level is shorthand for
  max=N (legacy). min=/max=/marker= override config.toc for this
  call only; the anchor style/separator are config-only (no per-call
  override, so core.refs always agrees with the TOC just generated).

  By default the headline separators are applied afterwards, following the
  ensure_headline_spacing option. Force the behaviour per call:
    --sep      always apply separators
    --no-sep   never apply separators

  By default, skipped heading levels are also checked afterwards, following
  the check_heading_gaps option. Force the behaviour per call:
    --check-gaps      always check for gaps
    --no-check-gaps   skip the gap check

7.4 :Markdown refs *:Markdown-refs*

:Markdown refs [sync]

  Reconcile now: detect heading renames since the baseline, propagate each
  old-anchor -> new-anchor to every inline [text](#anchor) link and to an
  existing TOC block, then report orphaned anchor links. A bare `:Markdown
  refs is the same as :Markdown refs sync`.

:Markdown refs check

  Dry run. List every #anchor link with no matching heading in the |quickfix|
  list (open it with |:copen|). Changes nothing.

:Markdown refs live [on|off|toggle]

  Enable/disable debounced live tracking for the current buffer at runtime,
  independent of the refs.mode config default.

:Markdown refs baseline

  Re-snapshot the current heading anchors, resetting rename tracking. Useful
  after large restructuring when you do not want prior edits interpreted as
  renames.

  Heading identity is tracked with extmarks (which survive in-line edits) plus
  a positional-diff fallback (which catches whole-line replacements without
  inventing renames on structural add/delete). See |markdown.nvim-config-refs|
  for the automatic-trigger mode and the debounce.

7.5 :Markdown table *:Markdown-table*

:Markdown table view [toggle|markdown|box|select|close|browser|browsernice] [scope]

  Render a table (or every table) in a nicely formatted preview (see
  |markdown.nvim-tableview|). toggle uses the configured default style
  (tableview.style, default "markdown"); markdown / box force the
  aligned-Markdown or Unicode box-drawing "spreadsheet" style; browser /
  browsernice open it as basic / GitHub-styled HTML in the browser. select
  picks a table from a list; close closes the float (also q / <Esc>).

  toggle / markdown / box accept an optional scope:
    (none)   cursor ON a table    -> preview just that table
             cursor OFF any table -> every table in the current buffer
    %        every table in the current buffer, even with the cursor on one
    cwd      every table in every *.md file under the working directory
             (recursive)
    <path>   every table in that file, or — if <path> is a directory — every
             table in every *.md file under it (recursive)

  Multiple tables render stacked one after another, separated by a blank line
  and a label: ── Table i/N (line L) ── for same-buffer tables, or
  ── path:line (Table i/N) ── when a table came from a file on disk rather
  than the current buffer. Tab-completion on the scope argument offers %,
  cwd, and path completion. Same via the buffer-local commands directly:
  :TableViewToggle, :TableViewToggle %, :TableViewBox cwd,
  :TableViewToggle ./docs, …

  Inside the floating preview (Normal mode, buffer-local to the popup):
    <M-Right>/<M-l>  widen the column under the cursor
    <M-Left>/<M-h>   narrow it (floors at its natural content width)
    <M-Up>/<M-k>     move the row under the cursor up
    <M-Down>/<M-j>   move the row under the cursor down
    :w               write the current row order back to the source
  Column widening is never written back (:w uses natural, unpadded
  widths). A cursor on a border, separator, or multi-table label line is a
  no-op. See |markdown.nvim-keymaps-tableview|.

:Markdown table format [options]

  Run the self-contained GFM formatter on the table at the cursor (or in the
  given scope): pad columns to equal width and normalize separator rows.
  Defaults for alignment/column-overrides come from config.table (see
  |markdown.nvim-config-table|) when the command args don't set them.

:Markdown table new [cols] [rows]

  Insert an empty GFM table template with the given dimensions
  (defaults apply when omitted).

:Markdown table mode [on|off|toggle]

  Per-buffer table mode: a focused, dependency-free take on vim-table-mode.
  While on, the table under the cursor is re-aligned automatically after each
  edit (debounced, on InsertLeave / TextChanged), reusing the format
  alignment. Also mapped to <leader>tvm.

:Markdown table tableize [format]

  Convert delimited text into a GFM table. Operates on the command's range
  (:'<,'> or :N,M) or the current line. The separator is auto-detected
  (tab, comma, or runs of 2+ spaces) or named explicitly:

    auto              auto-detect (default)
    csv / comma       a single comma
    tsv / tab         a tab
    psv / pipe        a single |
    scsv / semicolon  a single ;
    colon             a single :
    space             a single space
    spaces            a run of 2+ whitespace
    ";" / ":" / …     any bare or quoted literal delimiter

  Single-character separators honor RFC-4180 double quoting: a field wrapped
  in "..." may contain the delimiter ("Smith, John",42 -> two cells), and
  "" inside is a literal quote. Consecutive separators produce empty cells, so
  leading separators map to leading empty columns. A literal space must be
  quoted: :Markdown table tableize " " (Neovim splits unquoted spaces). The
  result is alignment-formatted.

  Cell motions ]| / [| jump to the next / previous cell on the current row.
  The whole table surface lives under the table feature and stays available
  when only tableview is enabled (see |markdown.nvim-config-features|).

:Markdown table import [clipboard|PATH]

  Parse an HTML <table> into a GFM table — round-trips with the TableView
  "open in browser" export (|markdown.nvim-tableview|). The first row (<th>
  or <td>) becomes the header; tags inside cells are stripped and HTML
  entities (&amp;, &lt;, &gt;, &quot;, &apos;/&#39;, &nbsp;)
  unescaped.

  Source of the HTML:
    clipboard   the "+" register
    PATH        a file on disk
    (none)      the command's range if any (:'<,'>Markdown table import
                replaces the selected HTML in place), otherwise the whole
                current buffer (inserted below the cursor).

7.6 :Markdown render / preview / mdview / export *:Markdown-render*

:Markdown render [on|off|toggle]

  Enable/disable render-markdown.nvim for the current buffer. The plugin is
  an optional host dependency; the command warns gracefully when it is not
  installed.

:Markdown preview [start|stop|toggle]

  Start/stop mdview.nvim for the current buffer via :MDView start/`:MDView
  stop`. Once running, mdview.nvim follows buffer switches and drives scroll
  sync itself (browser.behavior, default "reuse"); no separate refresh step
  is needed here. An optional host dependency.

:Markdown mdview [path]

  Open path (default: the current buffer's file) directly in the browser
  via mdview.nvim's :MDView start, which starts a session or — if one is
  already running — pushes the file and re-opens the preview surface for it.
  mdview.nvim is an optional host dependency; the command warns gracefully
  when it is not installed or loaded (e.g. omitted as a dependency of
  markdown.nvim and not installed standalone). :checkhealth markdown
  reports whether it was detected.

                                                         *:Markdown-export*
:Markdown export [pdf] [path]

  Export the current buffer/file (or path) to PDF by delegating to
  pdfport.nvim's create() — pandoc plus a PDF engine, all owned by
  pdfport. pdf is the only (and default) sub. pdfport.nvim is an optional
  host; without it, or without an available producer, the command warns
  instead of erroring. Following a .pdf link from the cursor-action
  handler offers the same pdfport path (see |markdown.nvim-keymaps-handler|).

7.7 :Markdown create *:Markdown-create*

:Markdown create fs

  Walk the Markdown-link targets in the range (visual selection) or, without
  a range, the whole buffer, and create the corresponding filesystem entries.
  A trailing / denotes a directory; otherwise a file is created together
  with its parent directories. URLs, mailto: and #anchors are skipped and
  existing paths are left untouched. The result reports created / already
  existing / failed counts.

7.8 :Markdown headline_spacing *:Markdown-headline-spacing*

:Markdown headline_spacing

  Enforce [blank]---[blank] spacing between consecutive H2+ sections in the
  current buffer and add a closing --- after the final section. Idempotent.
  This is the on-demand form of the behaviour described under
  ensure_headline_spacing (see |markdown.nvim-config-general|).

7.9 :Markdown scope *:Markdown-scope*

:Markdown scope [on|off|toggle|status]

  Control the fenced-block scope feature at runtime, overriding the
  fenced_scope.enable config (see |markdown.nvim-config-fenced-scope|).
  With no argument, toggles. status reports the current state.

7.10 :Markdown list *:Markdown-list*

:Markdown list [headings] [%|cwd|<file>]

  List the document's items in a picker and jump to the chosen one. The only
  option so far is headings (also the default). The scope vocabulary is the
  same as |:Markdown-links|: the current buffer (%, the default), every
  *.md below the current working directory (cwd), or a given file.

  Entries are indented by heading level and labelled with their source
  location. Picking one opens the file if it is not the current buffer and
  moves the cursor to the heading; the jump is undoable with |CTRL-O|.

  Headings inside YAML frontmatter and inside fenced code blocks are skipped,
  so what is listed matches what |:Markdown-toc| would generate. Setext
  headings (underlined with === / ---) are not recognized, also matching
  the TOC generator.

  The picker backend is set by the list.picker option (same values and
  fallback behavior as links.picker, see |markdown.nvim-config-links|).

7.11 :Markdown image *:Markdown-image*

:Markdown image [paste|screenshot]

  Thin delegators to images.nvim (https://github.com/StefanBartl/images.nvim),
  an optional host, soft dependency — not a reimplementation. Default sub is
  paste.
    paste       Same as |:Image| paste: clipboard image -> file next to the
                document + inserted link.
    screenshot  Same as |:Image| screenshot: interactive screenshot, skipping
                the clipboard step.
  Without images.nvim installed, both report a warning instead of an error.
  Exists purely for discoverability from :Markdown <Tab> — markdown.nvim
  and images.nvim are scoped to the same buffers by default (markdown,
  vimwiki, norg, text), so the coupling is natural even though the actual
  logic lives entirely in images.nvim.

7.12 :Markdown gaps *:Markdown-gaps*

:Markdown gaps

  Check the current buffer for skipped heading levels (e.g. an H1 followed
  directly by an H3, with no H2 in between) and report them. When gaps are
  found, you are asked whether to fix them immediately (each offending
  heading is renumbered to close the gap). This is the on-demand form of the
  behaviour described under check_heading_gaps (see
  |markdown.nvim-config-general|); <leader>toc / :Markdown toc also run
  it automatically unless disabled.

7.13 Buffer-local commands *markdown.nvim-buf-commands*

These commands are available only in Markdown buffers (installed via FileType
autocmd):

                                          *:OpenWithSystemApplication*
:OpenWithSystemApplication

  Identical to pressing ma. Dispatches to the cursor-action handler:
  opens anchors, images, URLs or local files under the cursor.

                                    *:MarkdownNvimUnderlineHeadings*
:MarkdownNvimUnderlineHeadings

  Inserts (or corrects) a line of = below every ATX heading's text in the
  buffer, matching its length — a purely visual, Setext-style decoration.
  The ATX # marker is left untouched, and this applies at every heading
  level (not just H1/H2, unlike real Setext syntax). Idempotent: a
  correctly-sized underline already in place is left alone; fenced code
  interiors are skipped. Underline character is underline_headings.char
  (see |markdown.nvim-config-general|), default "=". Disable via
  features.disable = { "underline_headings" }
  (see |markdown.nvim-config-features|).

                                                    *:TableViewToggle*
:TableViewToggle

  Toggle the floating table preview for the table at the cursor.

                                                    *:TableViewSelect*
:TableViewSelect

  Open a floating selection list with all pipe-tables found in the buffer.
  Press <Enter> to preview a table; q or <Esc> to close.

                                                     *:TableViewClose*
:TableViewClose

  Close the persistent floating TableView window.

                                               *:TableViewOpenBrowser*
:TableViewOpenBrowser

  Export the table at the cursor as a basic HTML file and open it in the
  system default browser.

                                           *:TableViewOpenBrowserNice*
:TableViewOpenBrowserNice

  Export the table at the cursor as a styled HTML file (dark header, zebra
  rows, rounded card) and open it in the system default browser.

7.14 :MDTable* (width-limited wrapping) *markdown.nvim-mdtable*

A separate, opt-in command family (buffer-local, feature table_wrap, on by
default) -- not nested under |:Markdown-table|. table_fmt's formatter
aligns columns to their natural, unbounded width; these commands add a width
CAP: cells exceeding a column's planned width wrap onto GFM-valid
continuation rows of the same logical row (every physical row keeps the same
pipe count). Off by default -- nothing changes until one of these commands
runs, or table.wrap.enabled = true. Continuation rows carry a ↳ gutter
sign (virtual text only; the buffer text stays clean GFM).

:MDTableWrap
  Wrap the table at the cursor; falls back to every table in the buffer
  when the cursor isn't inside one.

:MDTableUnwrap
  Merge the table at the cursor's continuation rows back into one physical
  row each. Detected structurally (no in-buffer marker is written): a
  non-separator row with at most one non-empty cell, directly following
  another row of the same table, is treated as a continuation. One caveat:
  a genuine data row matching that same shape is indistinguishable and
  gets merged too.

:MDTableWrapVisual[!] (range)
  Wrap tables in the visual selection. ! unwraps first, for a clean
  recompute instead of re-wrapping an already-wrapped table in place.

:MDTableWrapVisible[!]
  Wrap tables intersecting the visible window range (line("w0") ..
  line("w$")). ! unwraps first.

:MDTableReflowHeader
  Reflow only the header + separator of the table at the cursor; body rows
  (and any existing continuation rows) are left untouched.

:MDTableFoldRow
:MDTableFoldAll
  Fold the continuation block under the cursor / every continuation block
  in the buffer. Extends the heading |markdown.nvim-foldexpr| rather than a
  separate manual-fold pass: continuation rows fold one level deeper than
  their heading section once either command has run in the buffer.

:MDTableProfile {compact|docs|wide}
  Load a named preset from table.wrap_profiles as this buffer's wrap
  opts, then re-wrap the table at the cursor.

:MDTableCol {inc|dec} [n]
  Widen/narrow the column under the cursor by n (default 1), taking the
  width from (or giving it to) the neighboring column so the row's total
  width is unchanged. (Vim command names can't contain +/-, hence
  inc/dec rather than the roadmap note's original Col+/Col-.)

:MDTableAlign {cycle|left|center|right}
  Cycle (or set) the alignment of the column under the cursor.

:MDTableFlavor {github|loose}
  github: strict GFM -- minimum 3-dash separator, spaced style. loose:
  no forced minimum. Re-wraps the table at the cursor.

:MDTableLint
  Flags unequal cell counts, missing separator lines, and empty header
  cells via |vim.diagnostic| (namespace markdown_table).

:MDTableFixMissingSeparator
  Inserts a separator line after every table block in the buffer missing
  one.

:MDTableDebug
  Prints the resolved column-width plan for the table at the cursor: avail
  width, pipes, padding, sum, and per-column width/natural/min/max/mode.

:MDTableToCSV [path]
  Exports the table at the cursor as RFC-4180 CSV, to path or the +
  register.

:MDTableFromCSV [path]
  Inserts a GFM table below the cursor, parsed from CSV (path or the +
  register).

Configuration (table.wrap, table.wrap_profiles; see
|markdown.nvim-config-table|): enabled, auto, min, max, pad,
join, soft_break_chars, continuation_marker, flavor, auto_resize,
resize_debounce_ms, selective_reflow. A per-table directive comment
immediately above a table overrides these for that one table:
  <!-- mdwrap: auto=false max=40 min=12 pad=1 join=br -->
  | Name | Description |
API hooks: `require("markdown.core.table_wrap").on("before_reflow"|
"after_reflow", function(bounds, opts) end) -- bounds` is
{ start_line, end_line } (1-indexed, inclusive); errors inside a hook are
caught and ignored.

8. TABLEVIEW *markdown.nvim-tableview*

The TableView subsystem parses GFM pipe-style tables from the current buffer
and displays them in a persistent floating window.

Table format supported:
  | Column A | Column B | Column C |
  |:---------|:--------:|---------:|
  | left     | center   | right    |
Alignment markers (:---, :---:, ---:) are respected in the rendered
output. Column widths are computed from screen-DISPLAY width
(vim.fn.strdisplaywidth), not byte length, so cells containing multi-byte
UTF-8 (umlauts, em dashes, curly quotes, arrows, …) still keep every | / │
divider lined up across rows.

require("markdown.tableview.renderer").validate_alignment(lines) checks
a rendered table's lines and reports (ok, err) — err names the first line
whose divider count or display column doesn't match the reference row. Use it
to verify a rendering programmatically (e.g. in a test) instead of eyeballing a
screenshot for drifted columns.

The floating window reuses the same buffer for successive renders. It is
closed by :TableViewClose, by pressing q or <Esc> in the selection
list, or when the buffer is wiped.

With the cursor on a table, :TableViewToggle (and :TableViewMarkdown /
:TableViewBox) preview just that one. Off any table, or with an explicit
scope (%, cwd, or a file/directory path), they preview every matching
table instead, stacked one after another and separated by a blank line plus a
label. See |:Markdown-table|.

Browser export creates a temporary .html file in vim.fn.tempname() and
opens it with the system application (xdg-open / open / cmd start).

9. FOLD EXPRESSION *markdown.nvim-foldexpr*

To enable fold support, add to your config (or use an ftplugin):

  vim.opt_local.foldmethod = "expr"
  vim.opt_local.foldexpr   = "v:lua.require('markdown').foldexpr(v:lnum)"
  vim.opt_local.foldenable  = true
The expression recognizes:
  * ATX headings (# , ## , …) — foldlevel equals the heading depth.
  * Setext H2 underlines (---) — treated as level 2.
  * All other lines return = (inherit from previous line).

Helper functions:

  require("markdown.core.fold").toggle_under_cursor()
    Toggle fold under cursor and center the view.

  require("markdown.core.fold").unfold_all_center()
    Open all folds (zR) and center the view.

  require("markdown.core.fold_levels").fold_h2_plus()
    Toggle an outline view: fold everything below H2 (H3+, foldlevel=2),
    keeping H1 and H2 open; running it again unfolds all.

  require("markdown.core.fold_levels").fold_levels({2,3,4})
    Fold the specified heading levels.

  require("markdown.core.fold_prev").fold_prev_heading_then_center()
    Move to the previous heading, close its fold, and center.

10. LUA API *markdown.nvim-api*

After calling setup(), the following functions are exported from the
top-level module:

                                               *markdown.foldexpr()*
require("markdown").foldexpr(lnum)

  Fold expression function. Pass it to foldexpr (see |markdown.nvim-foldexpr|).

                                          *markdown.goto_prev_heading()*
require("markdown").goto_prev_heading()

  Jump to the previous H2+ heading. Respects vim.v.count1.

                                          *markdown.goto_next_heading()*
require("markdown").goto_next_heading()

  Jump to the next H2+ heading. Respects vim.v.count1.

                                         *markdown.toggle_visual_bold()*
require("markdown").toggle_visual_bold()

  Toggle **bold** on the current visual selection.

                                               *markdown.update_toc()*
require("markdown").update_toc([header [, opts]])

  Insert or refresh the TOC in the current buffer.
  header     String: the TOC heading line (default "## Table of content").
  opts       Table with optional min_level and max_level (integers 1-6).

                                        *markdown.handle_cursor_action()*
require("markdown").handle_cursor_action()

  Dispatch the cursor-action handler (anchor / image / URL / file).

Sub-modules can be imported individually, e.g.:

  local headings = require("markdown.core.headings")
  headings.shift_range(srow, erow, delta)

  local toc = require("markdown.core.toc")
  toc.update_markdown_toc("## Contents", { max_level = 3 })

  local hs = require("markdown.core.headline_spacing")
  hs.apply_headl_separators(0, { notify = false })

  local gaps = require("markdown.core.heading_gaps")
  gaps.check(0, { silent_ok = true })

  local renderer = require("markdown.tableview.renderer")
  local ok, err = renderer.validate_alignment(rendered_lines)

  local diag = require("markdown.core.link_diagnostics")
  diag.check(0)   -- populate vim.diagnostic for buffer 0 (namespace "markdown_links")

  local tf = require("markdown.core.table_fmt")
  local rows = tf.parse_html_table(html_string)
  local gfm_lines = tf.rows_to_gfm(rows, {})

11. ARCHITECTURE *markdown.nvim-architecture*

  lua/markdown/
    init.lua                 setup() + public Lua facade
    config.lua               merged defaults
    util/
      notify.lua             vim.notify wrapper
      clipboard.lua          setreg("+") helper
      ignore.lua             default directory ignore list
      picker.lua             hover_select / vim.ui.select / telescope / fzf abstraction
    core/
      headings.lua           navigation + level shifting
      fold.lua               foldexpr, toggle, unfold
      fold_levels.lua        fold by heading level
      fold_prev.lua          fold previous heading
      toc.lua                TOC generator (GFM slugs, de-dup)
      heading_gaps.lua       detect/fix skipped heading levels
      wrap.lua               visual bold toggle
      wrap_link.lua          <leader>[ wrap word/selection in a link
      link_scan.lua          collect links from a line/buffer
      heading_scan.lua       collect ATX headings from lines/buffer/file
      link_diagnostics.lua   dead links / duplicate anchors (vim.diagnostic)
      link_sanitize.lua      normalize link-target paths (./, forward slashes)
      slug.lua               anchor slug algorithm (gfm/keep-case) + anchor map
      table_fmt.lua          GFM table formatter + HTML->GFM import (self-contained)
      headline_spacing/
        init.lua             blank-dash-blank enforcer (+ final closer)
    anchor/
      is_anchor_line.lua
      is_html_anchor_line.lua
      is_html_extern_anchor_line.lua
      is_inside_toc_block.lua
      jump.lua               jump to #anchor under cursor
    handler/
      init.lua               cursor-action dispatcher
      image.lua              open image (system viewer)
      url.lua                open URL (system browser)
      file.lua               open file (system viewer / :edit)
    fenced_fix/
      init.lua               fenced-code + inline-code HL override
    hl_options/
      init.lua               orchestrator (ColorScheme re-apply)
      hl_groups/
        blockquote.lua       decoration-provider blockquote coloring
    tableview/
      init.lua               setup (autocmds)
      autocmds.lua           FileType autocmd
      mappings.lua           buffer-local keymaps
      commands.lua           buffer-local user commands
      parser.lua             pipe-table parser
      renderer.lua           floating window renderer (single + stacked all-tables view)
      views/
        browser_basic.lua    basic HTML export
        browser_niceified.lua styled HTML export
        table_selector.lua   pick-a-table floating UI
    commands/
      init.lua               :Markdown dispatcher + nested completion
      links.lua              :Markdown links show|create|sanitize
      markdown_links.lua     directory-to-link generator (links create)
      toc.lua                :Markdown toc (TOC + separators)
      table.lua              :Markdown table view|format|new
      render.lua             :Markdown render (render-markdown.nvim)
      preview.lua            :Markdown preview (mdview.nvim)
      mdview.lua             :Markdown mdview (mdview.nvim)
      create.lua             :Markdown create fs
      image.lua              :Markdown image paste|screenshot (images.nvim)
    setup/
      keymaps.lua            buffer-local keymap installer
      autocmds.lua           FileType autocmd driver
      usercmds/
        init.lua             buffer-local user-command installer
  plugin/
    markdown.lua        guard (vim.g.loaded_markdown)