runtime(helptoc): the helptoc package can be improved

Adds the following changes:
- New Maintainer: Pete Kenny
- New filetypes supported (asciidoc, html, tex, vim, xhtml)
- improved Markdown support
- Sanitised ToCs and popup presentation
- Configuration improvements and options
- Add helptoc.txt help file

closes: #17255

Signed-off-by: Peter Kenny <github.com@k1w1.cyou>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/.github/MAINTAINERS b/.github/MAINTAINERS
index 19a65c0..b5e2bc6 100644
--- a/.github/MAINTAINERS
+++ b/.github/MAINTAINERS
@@ -431,6 +431,7 @@
 runtime/lang/menu_ru_ru.utf-8.vim	@RestorerZ
 runtime/pack/dist/opt/cfilter/plugin/cfilter.vim	@yegappan
 runtime/pack/dist/opt/comment/	@habamax
+runtime/pack/dist/opt/helptoc/	@kennypete
 runtime/pack/dist/opt/matchit/		@chrisbra
 runtime/pack/dist/opt/nohlsearch/		@habamax
 runtime/plugin/manpager.vim		@Konfekt
diff --git a/Filelist b/Filelist
index a7a937c..17ec8df 100644
--- a/Filelist
+++ b/Filelist
@@ -808,6 +808,8 @@
 		runtime/pack/dist/opt/editorconfig/ftdetect/editorconfig.vim \
 		runtime/pack/dist/opt/editorconfig/plugin/editorconfig.vim \
 		runtime/pack/dist/opt/helptoc/autoload/helptoc.vim \
+		runtime/pack/dist/opt/helptoc/doc/helptoc.txt \
+		runtime/pack/dist/opt/helptoc/doc/tags \
 		runtime/pack/dist/opt/helptoc/plugin/helptoc.vim \
 		runtime/pack/dist/opt/hlyank/plugin/hlyank.vim \
 		runtime/pack/dist/opt/justify/plugin/justify.vim \
diff --git a/runtime/doc/helphelp.txt b/runtime/doc/helphelp.txt
index 647c35a..4e08d90 100644
--- a/runtime/doc/helphelp.txt
+++ b/runtime/doc/helphelp.txt
@@ -1,4 +1,4 @@
-*helphelp.txt*	For Vim version 9.1.  Last change: 2025 Apr 21
+*helphelp.txt*	For Vim version 9.1.  Last change: 2025 May 04
 
 
 		  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -287,9 +287,11 @@
 	<Home>     | select first entry
 	<End>      | select last entry
 
-The plugin can also provide a table of contents in man pages, markdown files,
-and terminal buffers.  In the latter, the entries will be the past executed
-shell commands.  To find those, the following pattern is used: >
+The plugin can also provide a table of contents in buffers of the following
+filetypes: asciidoc, html, man, markdown, tex, vim, and xhtml.  In addition
+it also provide a table of contents for a terminal buffer, which produces
+entries that are the past executed shell commands.  To find those, by default,
+the following pattern is used: >
 
 	^\w\+@\w\+:\f\+\$\s
 
@@ -303,6 +305,9 @@
 <Esc> instead of <CR>, you can then get more context for each remaining entry
 by pressing `J` or `K`.
 
+Refer |helptoc.vim| for more details about helptoc, particularly about using
+it with filetypes other than help, and configuring its options.
+
 ==============================================================================
 2. Translated help files				*help-translated*
 
diff --git a/runtime/pack/dist/opt/helptoc/autoload/helptoc.vim b/runtime/pack/dist/opt/helptoc/autoload/helptoc.vim
index c0d86a4..a22d4ba 100644
--- a/runtime/pack/dist/opt/helptoc/autoload/helptoc.vim
+++ b/runtime/pack/dist/opt/helptoc/autoload/helptoc.vim
@@ -1,24 +1,71 @@
 vim9script noclear
-
 # Config {{{1
+# g:helptoc {{{2
+# Create the g:helptoc dict (used to specify the shell_prompt and other
+# options) when it does not exist
+g:helptoc = exists('g:helptoc') ? g:helptoc : {}
 
-var SHELL_PROMPT: string = ''
+# Set the initial shell_prompt pattern matching a default bash prompt
+g:helptoc.shell_prompt = get(g:helptoc, 'shell_prompt', '^\w\+@\w\+:\f\+\$\s')
+
+# Track the prior prompt (used to reset b:toc if 'shell_prompt' changes)
+g:helptoc.prior_shell_prompt = g:helptoc.shell_prompt
 
 def UpdateUserSettings() #{{{2
-    var new_prompt: string = g:
-        ->get('helptoc', {})
-        ->get('shell_prompt', '^\w\+@\w\+:\f\+\$\s')
-    if new_prompt != SHELL_PROMPT
-        SHELL_PROMPT = new_prompt
+
+    if g:helptoc.shell_prompt != g:helptoc.prior_shell_prompt
         # invalidate cache: user config has changed
         unlet! b:toc
+        # reset the prior prompt to the new prompt
+        g:helptoc.prior_shell_prompt = g:helptoc.shell_prompt
     endif
+
+    # helptoc popup presentation options{{{
+    # Enable users to choose whether, in toc and help text popups, to have:
+    # - border (default [], which is a border, so is usually wanted)
+    # - borderchars (default single box drawing; use [] for Vim's defaults)
+    # - borderhighlight (default [], but a user may prefer something else)
+    # - close (default 'none'; mouse users may prefer 'button')
+    # - drag (default true, which is a popup_menu's default)
+    # - scrollbar (default false; for long tocs/HELP_TEXT true may be better)
+    # For example, in a Vim9 script .vimrc, these settings will produce tocs
+    # with borders that have the same highlight group as the inactive
+    # statusline, a scrollbar, and an 'X' close button:
+    # g:helptoc.popup_borderchars = get(g:helptoc, 'popup_borderchars', [' '])
+    # g:helptoc.popup_borderhighlight = get(g:helptoc,
+    #     'popup_borderhighlight', ['StatusLineNC'])
+    # g:helptoc.popup_close = get(g:helptoc, 'popup_close', 'button')
+    # g:helptoc.popup_scrollbar = get(g:helptoc, 'popup_scrollbar', true)
+    # }}}
+    g:helptoc.popup_border = get(g:helptoc, 'popup_border', [])
+    g:helptoc.popup_borderchars = get(g:helptoc, 'popup_borderchars',
+        ['─', '│', '─', '│', '┌', '┐', '┘', '└'])
+    g:helptoc.popup_borderhighlight = get(g:helptoc, 'popup_borderhighlight',
+        [])
+    g:helptoc.popup_drag = get(g:helptoc, 'popup_drag', true)
+    g:helptoc.popup_close = get(g:helptoc, 'popup_close', 'none')
+    g:helptoc.popup_scrollbar = get(g:helptoc, 'popup_scrollbar', false)
+    # For sanitized tocs, allow the user to specify the level indicator
+    g:helptoc.level_indicator = get(g:helptoc, 'level_indicator', '| ')
 enddef
 
 UpdateUserSettings()
 
-# Init {{{1
+# Syntax {{{1
 
+# Used by sanitized tocs (asciidoc, html, markdown, tex, vim, and xhtml)
+def SanitizedTocSyntax(): void
+    silent execute "syntax match helptocLevel _^\\(" ..
+        g:helptoc.level_indicator .. "\\)*_ contained"
+    silent execute "syntax region helptocText start=_^\\(" ..
+        g:helptoc.level_indicator .. "\\)*_ end=_$_ contains=helptocLevel"
+    highlight link helptocText Normal
+    highlight link helptocLevel NonText
+enddef
+
+# Init {{{1
+# Constants {{{2
+# HELP_TEXT {{{3
 const HELP_TEXT: list<string> =<< trim END
     normal commands in help window
     ──────────────────────────────
@@ -73,38 +120,108 @@
     more context for each remaining entry by pressing J or K
 END
 
+# UPTOINC_H {{{3
+const UPTOINC_H: string = '\v\c^%(%([<][^h][^>]*[>])|\s)*[<]h'
+
+# MATCH_ENTRY {{{3
 const MATCH_ENTRY: dict<dict<func: bool>> = {
+
     help: {},
 
-    man: {
-        1: (line: string, _): bool => line =~ '^\S',
-        2: (line: string, _): bool => line =~ '^\%( \{3\}\)\=\S',
-        3: (line: string, _): bool => line =~ '^\s\+\(\%(+\|-\)\S\+,\s\+\)*\%(+\|-\)\S\+',
+    # For asciidoc, these patterns should match:
+    # https://docs.asciidoctor.org/asciidoc/latest/sections/titles-and-levels/
+    asciidoc: {
+        1: (l: string, _): bool => l =~ '\v^%(\=|#)\s',
+        2: (l: string, _): bool => l =~ '\v^%(\={2}|#{2})\s',
+        3: (l: string, _): bool => l =~ '\v^%(\={3}|#{3})\s',
+        4: (l: string, _): bool => l =~ '\v^%(\={4}|#{4})\s',
+        5: (l: string, _): bool => l =~ '\v^%(\={5}|#{5})\s',
+        6: (l: string, _): bool => l =~ '\v^%(\={6}|#{6})\s',
     },
 
+    html: {
+        1: (l: string, _): bool => l =~ $"{UPTOINC_H}1",
+        2: (l: string, _): bool => l =~ $"{UPTOINC_H}2",
+        3: (l: string, _): bool => l =~ $"{UPTOINC_H}3",
+        4: (l: string, _): bool => l =~ $"{UPTOINC_H}4",
+        5: (l: string, _): bool => l =~ $"{UPTOINC_H}5",
+        6: (l: string, _): bool => l =~ $"{UPTOINC_H}6",
+    },
+
+    man: {
+        1: (l: string, _): bool => l =~ '^\S',
+        2: (l: string, _): bool => l =~ '\v^%( {3})=\S',
+        3: (l: string, _): bool => l =~ '\v^\s+%(%(\+|-)\S+,\s+)*(\+|-)\S+'
+    },
+
+    # For markdown, these patterns should match:
+    # https://spec.commonmark.org/0.31.2/#atx-headings and
+    # https://spec.commonmark.org/0.31.2/#setext-headings
     markdown: {
-        1: (line: string, nextline: string): bool =>
-           (line =~ '^#[^#]' || nextline =~ '^=\+$') && line =~ '\w',
-        2: (line: string, nextline: string): bool =>
-           (line =~ '^##[^#]' || nextline =~ '^-\+$') && line =~ '\w',
-        3: (line: string, _): bool => line =~ '^###[^#]',
-        4: (line: string, _): bool => line =~ '^####[^#]',
-        5: (line: string, _): bool => line =~ '^#####[^#]',
-        6: (line: string, _): bool => line =~ '^######[^#]',
+        1: (l: string, nextline: string): bool =>
+            (l =~ '\v^ {0,3}#%(\s|$)' || nextline =~ '\v^ {0,3}\=+$') &&
+            l =~ '\S',
+        2: (l: string, nextline: string): bool =>
+            (l =~ '\v^ {0,3}##%(\s|$)' || nextline =~ '\v^ {0,3}-+$') &&
+            l =~ '\S',
+        3: (l: string, _): bool => l =~ '\v {0,3}#{3}%(\s|$)',
+        4: (l: string, _): bool => l =~ '\v {0,3}#{4}%(\s|$)',
+        5: (l: string, _): bool => l =~ '\v {0,3}#{5}%(\s|$)',
+        6: (l: string, _): bool => l =~ '\v {0,3}#{6}%(\s|$)',
     },
 
     terminal: {
-        1: (line: string, _): bool => line =~ SHELL_PROMPT,
+        1: (l: string, _): bool => l =~ g:helptoc.shell_prompt
+    },
+
+    # For LaTeX, this should meet
+    # https://mirrors.rit.edu/CTAN/info/latex2e-help-texinfo/latex2e.pdf
+    #   including:
+    #   para 6.3:
+    #     \section{Heading}
+    #     \section[Alternative ToC Heading]{Heading}
+    #   para 25.1.2:
+    #     \section*{Not for the TOC heading}
+    #     \addcontentsline{toc}{section}{Alternative ToC Heading}
+    tex: {
+        1: (l: string, _): bool => l =~ '^[\\]\(\%(part\|chapter\)' ..
+            '\%([\u005B{]\)\|addcontentsline{toc}{\%(part\|chapter\)\)',
+        2: (l: string, _): bool => l =~ '^[\\]\%(section' ..
+            '\%([\u005B{]\)\|addcontentsline{toc}{section}\)',
+        3: (l: string, _): bool => l =~ '^[\\]\%(subsection' ..
+            '\%([\u005B{]\)\|addcontentsline{toc}{subsection}\)',
+        4: (l: string, _): bool => l =~ '^[\\]\%(subsubsection' ..
+            '\%([\u005B{]\)\|addcontentsline{toc}{subsubsection}\)',
+    },
+
+    vim: {
+        1: (l: string, _): bool => l =~ '\v\{{3}1',
+        2: (l: string, _): bool => l =~ '\v\{{3}2',
+        3: (l: string, _): bool => l =~ '\v\{{3}3',
+        4: (l: string, _): bool => l =~ '\v\{{3}4',
+        5: (l: string, _): bool => l =~ '\v\{{3}5',
+        6: (l: string, _): bool => l =~ '\v\{{3}6',
+    },
+
+    xhtml: {
+        1: (l: string, _): bool => l =~ $"{UPTOINC_H}1",
+        2: (l: string, _): bool => l =~ $"{UPTOINC_H}2",
+        3: (l: string, _): bool => l =~ $"{UPTOINC_H}3",
+        4: (l: string, _): bool => l =~ $"{UPTOINC_H}4",
+        5: (l: string, _): bool => l =~ $"{UPTOINC_H}5",
+        6: (l: string, _): bool => l =~ $"{UPTOINC_H}6",
     }
 }
 
+# HELP_RULERS {{{3
 const HELP_RULERS: dict<string> = {
     '=': '^=\{40,}$',
     '-': '^-\{40,}',
 }
 const HELP_RULER: string = HELP_RULERS->values()->join('\|')
 
-# the regex is copied from the help syntax plugin
+# HELP_TAG {{{3
+# The regex is copied from the help syntax plugin
 const HELP_TAG: string = '\*[#-)!+-~]\+\*\%(\s\|$\)\@='
 
 # Adapted from `$VIMRUNTIME/syntax/help.vim`.{{{
@@ -113,13 +230,15 @@
 #
 #     ^[-A-Z .][-A-Z0-9 .()_]*\ze\(\s\+\*\|$\)
 #
-# Allowing a  space or a hyphen  at the start  can give false positives,  and is
+# Allowing a space or a hyphen at the start can give false positives, and is
 # useless, so we don't allow them.
 #}}}
+
+# HELP_HEADLINE {{{3
 const HELP_HEADLINE: string = '^\C[A-Z.][-A-Z0-9 .()_]*\%(\s\+\*+\@!\|$\)'
 #                                                               ^--^
 # To prevent some false positives under `:help feature-list`.
-
+# Others {{{2
 var lvls: dict<number>
 def InitHelpLvls()
     lvls = {
@@ -133,7 +252,6 @@
     }
 enddef
 
-const AUGROUP: string = 'HelpToc'
 var fuzzy_entries: list<dict<any>>
 var help_winid: number
 var print_entry: bool
@@ -141,11 +259,11 @@
 
 # Interface {{{1
 export def Open() #{{{2
-    var type: string = GetType()
-    if !MATCH_ENTRY->has_key(type)
+    g:helptoc.type = GetType()
+    if !MATCH_ENTRY->has_key(g:helptoc.type)
         return
     endif
-    if type == 'terminal' && win_gettype() == 'popup'
+    if g:helptoc.type == 'terminal' && win_gettype() == 'popup'
         # trying to deal with a popup menu on top of a popup terminal seems
         # too tricky for now
         echomsg 'does not work in a popup window; only in a regular window'
@@ -158,7 +276,7 @@
     if exists('b:toc') && &filetype != 'man'
         if b:toc.changedtick != b:changedtick
         # in a terminal buffer, `b:changedtick` does not change
-        || type == 'terminal' && line('$') > b:toc.linecount
+        || g:helptoc.type == 'terminal' && line('$') > b:toc.linecount
             unlet! b:toc
         endif
     endif
@@ -187,66 +305,257 @@
             line: winpos[0],
             col: winpos[1] + width - 1,
             pos: 'topright',
-            scrollbar: false,
-            highlight: type == 'terminal' ? 'Terminal' : 'Normal',
-            border: [],
-            borderchars: ['─', '│', '─', '│', '┌', '┐', '┘', '└'],
+            highlight: g:helptoc.type == 'terminal' ? 'Terminal' : 'Normal',
             minheight: height,
             maxheight: height,
             minwidth: b:toc.width,
             maxwidth: b:toc.width,
             filter: Filter,
             callback: Callback,
+            border: g:helptoc.popup_border,
+            borderchars: g:helptoc.popup_borderchars,
+            borderhighlight: g:helptoc.popup_borderhighlight,
+            close: g:helptoc.popup_close,
+            drag: g:helptoc.popup_drag,
+            scrollbar: g:helptoc.popup_scrollbar,
         })
-    Win_execute(winid, [$'ownsyntax {&filetype}', '&l:conceallevel = 3'])
+    # Specify filetypes using sanitized toc syntax{{{
+    #   Those filetypes have a normalized toc structure.  The top level is
+    #   unprefixed and levels 2 to 6 are prefixed, by default, with a vertical
+    #   line and space for each level below 1:
+    #   Level 1
+    #   | Level 2
+    #   ...
+    #   | | | | | Level 6  }}}
+    final SanitizedTocSyntaxTypes: list<string> =
+        ['asciidoc', 'html', 'markdown', 'tex', 'vim', 'xhtml']
+    if index(SanitizedTocSyntaxTypes, g:helptoc.type) != -1
+        # Specified types' toc popups use a common syntax
+        Win_execute(winid, 'SanitizedTocSyntax()')
+    else
+        # Other types' toc popups use the same syntax as the buffer itself
+        Win_execute(winid, [$'ownsyntax {&filetype}', '&l:conceallevel = 3'])
+    endif
     # In a help file, we might reduce some noisy tags to a trailing asterisk.
     # Hide those.
-    if type == 'help'
+    if g:helptoc.type == 'help'
         matchadd('Conceal', '\*$', 0, -1, {window: winid})
     endif
     SelectNearestEntryFromCursor(winid)
 
-    # can't set  the title before  jumping to  the relevant line,  otherwise the
+    # Can't set the title before jumping to the relevant line, otherwise the
     # indicator in the title might be wrong
     SetTitle(winid)
 enddef
-#}}}1
+
 # Core {{{1
 def SetToc() #{{{2
-    var toc: dict<any> = {entries: []}
-    var type: string = GetType()
+    # Lambdas:
+    # CHARACTER_REFERENCES_TO_CHARACTERS {{{3
+    # These are used for AsciiDoc, Markdown, and [X]HTML, all of which allow
+    # for decimal, hexadecimal, and XML predefined entities.
+    #    Decimal character references: e.g., &#167; to §
+    #    Hexadecimal character references: e.g., &#xA7; to §
+    #    XML predefined entities to chars: e.g., &lt; to <
+    # All HTML5 named character references could be handled, though is that
+    # warranted for the few that may appear in a toc entry, especially when
+    # they are often mnemonic?  Future: A common Vim dict/enum could be useful?
+    const CHARACTER_REFERENCES_TO_CHARACTERS = (text: string): string =>
+        text->substitute('\v\&#0*([1-9]\d{0,6});',
+                '\=nr2char(str2nr(submatch(1), 10), 1)', 'g')
+            ->substitute('\c\v\&#x0*([1-9a-f][[:xdigit:]]{1,5});',
+                '\=nr2char(str2nr(submatch(1), 16), 1)', 'g')
+            ->substitute('\C&amp;', '\="\u0026"', 'g')
+            ->substitute('\C&apos;', "\u0027", 'g')
+            ->substitute('\C&gt;', "\u003E", 'g')
+            ->substitute('\C&lt;', "\u003C", 'g')
+            ->substitute('\C&quot;', "\u0022", 'g')
+
+    # SANITIZE_ASCIIDOC {{{3
+    # 1 - Substitute the = or # heading markup with the level indicator
+    # 2 - Substitute XML predefined, dec, and hex char refs in the entry
+    #     AsciiDoc recommends only using named char refs defined in XML:
+    #     https://docs.asciidoctor.org/asciidoc/latest/subs/replacements/
+    const SANITIZE_ASCIIDOC = (text: string): string =>
+        text->substitute('\v^(\={1,6}|#{1,6})\s+',
+            '\=repeat(g:helptoc.level_indicator, len(submatch(1)) - 1)', '')
+            ->CHARACTER_REFERENCES_TO_CHARACTERS()
+
+    # SANITIZE_HTML {{{3
+    #  1 - Remove any leading spaces or tabs
+    #  2 - Remove any <!--HTML comments-->
+    #  3 - Remove any <?processing_instructions?>
+    #  4 - Remove any leading tags (and any blanks) other than <h1 to <h6
+    #  5 - Remove any persisting leading blanks
+    #  6 - Handle empty XHTML headings, e.g., <h6 />
+    #  7 - Remove trailing content following the </h[1-6]>
+    #  8 - Remove the <h1
+    #  9 - Substitute the h2 to h6 heading tags with level indicator/level
+    # 10 - Remove intra-heading tags like <em>, </em>, <strong>, etc.
+    # 11 - Substitute XML predefined, dec and hex character references
+    const SANITIZE_HTML = (text: string): string =>
+        text->substitute('^\s*', '', '')
+            ->substitute('[<]!--.\{-}--[>]', '', 'g')
+            ->substitute('[<]?[^?]\+?[>]', '', 'g')
+            ->substitute('\v%([<][^Hh][^1-6]?[^>][>])*\s*', '', '')
+            ->substitute('^\s\+', '', '')
+            ->substitute('\v[<][Hh]([1-6])\s*[/][>].*',
+                '\=repeat(g:helptoc.level_indicator, ' ..
+                'str2nr(submatch(1)) - 1) ' ..
+                '.. "[Empty heading " .. submatch(1) .. "]"', '')
+            ->substitute('[<][/][Hh][1-6][>].*$', '', '')
+            ->substitute('[<][Hh]1[^>]*[>]', '', '')
+            ->substitute('\v[<][Hh]([2-6])[^>]*[>]',
+                '\=repeat(g:helptoc.level_indicator, ' ..
+                'str2nr(submatch(1)) - 1)', '')
+            ->substitute('[<][/]\?[[:alpha:]][^>]*[>]', '', 'g')
+            ->CHARACTER_REFERENCES_TO_CHARACTERS()
+
+    # SANITIZE_MARKDOWN #{{{3
+    # 1 - Hyperlink incl image, e.g. [![Vim The editor](xxx)](\uri), to Vim...
+    # 2 - Hyperlink [text](/uri) to text
+    # 3 - Substitute the # ATX heading markup with the level indicator/level
+    #     The omitted markup reflects CommonMark Spec:
+    #     https://spec.commonmark.org/0.31.2/#atx-headings
+    # 4 - Substitute decimal, hexadecimal, and XML predefined char refs
+    const SANITIZE_MARKDOWN = (text: string): string =>
+        text->substitute('\v[\u005B]![\u005B]([^\u005D]+)[\u005D]'
+                .. '[(][^)]+[)][\u005D][(][^)]+[)]', '\1', '')
+            ->substitute('\v[\u005B]([^\u005D]+)[\u005D][(][^)]+[)]',
+                '\1', '')
+            ->substitute('\v^ {0,3}(#{1,6})\s*',
+                '\=repeat(g:helptoc.level_indicator, len(submatch(1)) - 1)',
+                '')
+            ->CHARACTER_REFERENCES_TO_CHARACTERS()
+
+    # SANITIZE_TERMINAL {{{3
+    # Omit the prompt, which may be very long and otherwise just adds clutter
+    const SANITIZE_TERMINAL = (text: string): string =>
+        text->substitute('^' .. g:helptoc.shell_prompt, '', '')
+
+    # SANITIZE_TEX #{{{3
+    # 1 - Use any [toc-title] overrides to move its content into the
+    #     {heading} instead of the (non-ToC) heading's text
+    # 2 - Replace \part{ or \addcontentsline{toc}{part} with '[PART] '
+    # 3 - Omit \chapter{ or \addcontentsline{toc}{chapter}
+    # 4 - Omit \section{ or \addcontentsline{toc}{section}
+    # 5 - Omit \subsection{ or \addcontentsline{toc}{subsection}
+    # 6 - Omit \subsubsection{ or \addcontentsline{toc}{subsubsection}
+    # 7 - Omit the trailing }
+    # 8 - Unescape common escaped characters &%$_#{}~^\
+    const SANITIZE_TEX = (text: string): string =>
+        text->substitute('\v^[\\](part|chapter|%(sub){0,2}section)' ..
+                '[\u005B]([^\u005D]+).*', '\\\1{\2}', '')
+            ->substitute('^[\\]\(part\|addcontentsline{toc}{part}\){',
+                '[PART] ', '')
+            ->substitute('^[\\]\(chapter\|addcontentsline{toc}{chapter}\){',
+                '', '')
+            ->substitute('^[\\]\(section\|addcontentsline{toc}{section}\){',
+                '\=g:helptoc.level_indicator', '')
+            ->substitute('^[\\]\(subsection\|' ..
+                'addcontentsline{toc}{subsection}\){',
+                '\=repeat(g:helptoc.level_indicator, 2)', '')
+            ->substitute('^[\\]\(subsubsection\|' ..
+                'addcontentsline{toc}{subsubsection}\){',
+                '\=repeat(g:helptoc.level_indicator, 3)', '')
+            ->substitute('}[^}]*$', '', '')
+            ->substitute('\\\([&%$_#{}~\\^]\)', '\1', 'g')
+
+    # SANITIZE_VIM {{{3
+    # #1 - Omit leading Vim9 script # or vimscript " markers and blanks
+    # #2 - Omit numbered 3x { markers
+    const SANITIZE_VIM = (text: string): string =>
+        text->substitute('\v^[#[:blank:]"]*(.+)\ze[{]{3}([1-6])',
+                '\=submatch(2) == "1" ? submatch(1) : ' ..
+                'repeat(g:helptoc.level_indicator, str2nr(submatch(2)) - 1)' ..
+                ' .. submatch(1)', 'g')
+            ->substitute('[#[:blank:]"]*{\{3}[1-6]', '', '')
+    #}}}3
+
+    final toc: dict<any> = {entries: []}
     toc.changedtick = b:changedtick
     if !toc->has_key('width')
         toc.width = 0
     endif
     # We cache the toc in `b:toc` to get better performance.{{{
     #
-    # Without caching, when we  press `H`, `L`, `H`, `L`, ...  quickly for a few
+    # Without caching, when we press `H`, `L`, `H`, `L`, ... quickly for a few
     # seconds, there is some lag if we then try to move with `j` and `k`.
     # This can only be perceived in big man pages like with `:Man ffmpeg-all`.
     #}}}
     b:toc = toc
 
-    if type == 'help'
+    if g:helptoc.type == 'help'
         SetTocHelp()
         return
     endif
 
-    if type == 'terminal'
+    if g:helptoc.type == 'terminal'
         b:toc.linecount = line('$')
     endif
 
     var curline: string = getline(1)
     var nextline: string
     var lvl_and_test: list<list<any>> = MATCH_ENTRY
-        ->get(type, {})
+        ->get(g:helptoc.type, {})
         ->items()
-        ->sort((l: list<any>, ll: list<any>): number => l[0]->str2nr() - ll[0]->str2nr())
+        ->sort((l: list<any>, ll: list<any>): number =>
+            l[0]->str2nr() - ll[0]->str2nr())
 
+    var skip_next: bool = false
+
+    # Non-help headings processing
     for lnum: number in range(1, line('$'))
+        if skip_next
+            skip_next = false
+            curline = nextline
+            continue
+        endif
+
         nextline = getline(lnum + 1)
+
+        # Special handling for markdown filetype using setext headings
+        if g:helptoc.type == 'markdown'
+            # Check for setext formatted headings (= or - underlined)
+            if nextline =~ '^\s\{0,3}=\+$' && curline =~ '\S'
+                # Level 1 heading (one or more =, up to three spaces preceding)
+                b:toc.entries->add({
+                    lnum: lnum,
+                    lvl: 1,
+                    text: SANITIZE_MARKDOWN('# ' .. trim(curline)),
+                })
+                skip_next = true
+                curline = nextline
+                continue
+            elseif nextline =~ '^\s\{0,3}-\+$' && curline =~ '\S'
+                # Level 2 heading (one or more -, up to three spaces preceding)
+                b:toc.entries->add({
+                    lnum: lnum,
+                    lvl: 2,
+                    text: SANITIZE_MARKDOWN('## ' .. trim(curline)),
+                })
+                skip_next = true
+                curline = nextline
+                continue
+            endif
+        endif
+
+        # Regular processing for markdown ATX-style headings + other filetypes
         for [lvl: string, IsEntry: func: bool] in lvl_and_test
             if IsEntry(curline, nextline)
+                if g:helptoc.type == 'asciidoc'
+                    curline = curline->SANITIZE_ASCIIDOC()
+                elseif g:helptoc.type == 'html' || g:helptoc.type == 'xhtml'
+                    curline = curline->SANITIZE_HTML()
+                elseif g:helptoc.type == 'markdown'
+                    curline = curline->SANITIZE_MARKDOWN()
+                elseif g:helptoc.type == 'terminal'
+                    curline = curline->SANITIZE_TERMINAL()
+                elseif g:helptoc.type == 'tex'
+                    curline = curline->SANITIZE_TEX()
+                elseif g:helptoc.type == 'vim'
+                    curline = curline->SANITIZE_VIM()
+                endif
                 b:toc.entries->add({
                     lnum: lnum,
                     lvl: lvl->str2nr(),
@@ -281,9 +590,9 @@
 
         if main_ruler != '' && curline =~ main_ruler
             last_numbered_entry = 0
-            # The information gathered in `lvls`  might not be applicable to all
-            # the main sections of a help file.  Let's reset it whenever we find
-            # a ruler.
+            # The information gathered in `lvls` might not be applicable to
+            # all the main sections of a help file.  Let's reset it whenever
+            # we find a ruler.
             InitHelpLvls()
         endif
 
@@ -304,7 +613,7 @@
 
         # 1.
         if prevline =~ '^\d\+\.\s'
-        # let's assume that the  start of a main entry is  always followed by an
+        # Let's assume that the start of a main entry is always followed by an
         # empty line, or a line starting with a tag
         && (curline =~ '^>\=\s*$' || curline =~ $'^\s*{HELP_TAG}')
         # ignore a numbered line in a list
@@ -337,7 +646,8 @@
         if curline =~ HELP_HEADLINE
         && curline !~ '^CTRL-'
         &&  prevline->IsSpecialHelpLine()
-        && (nextline->IsSpecialHelpLine() || nextline =~ '^\s*(\|^\t\|^N[oO][tT][eE]:')
+        && (nextline ->IsSpecialHelpLine()
+            || nextline =~ '^\s*(\|^\t\|^N[oO][tT][eE]:')
             AddEntryInTocHelp('HEADLINE', lnum, curline)
         endif
 
@@ -411,7 +721,8 @@
         ->min()
     for entry: dict<any> in b:toc.entries
         entry.text = entry.text
-            ->substitute('^\s*', () => repeat(' ', (entry.lvl - min_lvl) * 3), '')
+            ->substitute('^\s*', () =>
+                repeat(' ', (entry.lvl - min_lvl) * 3), '')
     endfor
 enddef
 
@@ -455,29 +766,30 @@
 
     # Ignore noisy tags.{{{
     #
-    #     14. Linking groups              *:hi-link* *:highlight-link* *E412* *E413*
-    #                                     ^----------------------------------------^
-    #                                     ^\s*\d\+\.\%(\d\+\.\=\)*\s\+.\{-}\zs\*.*
+    #     14. Linking groups        *:hi-link* *:highlight-link* *E412* *E413*
+    #                               ^----------------------------------------^
+    #                               ^\s*\d\+\.\%(\d\+\.\=\)*\s\+.\{-}\zs\*.*
     # ---
     #
-    # We don't use conceal because then, `matchfuzzypos()` could match concealed
-    # characters, which would be confusing.
+    # We don't use conceal because then, `matchfuzzypos()` could match
+    # concealed characters, which would be confusing.
     #}}}
-    #     MAKING YOUR OWN SYNTAX FILES                            *mysyntaxfile*
-    #                                                             ^------------^
-    #                                                             ^\s*[A-Z].\{-}\*\zs.*
+    #     MAKING YOUR OWN SYNTAX FILES                  *mysyntaxfile*
+    #                                                   ^------------^
+    #                                                   ^\s*[A-Z].\{-}\*\zs.*
     #
     var after_HEADLINE: string = '^\s*[A-Z].\{-}\*\zs.*'
-    #     14. Linking groups              *:hi-link* *:highlight-link* *E412* *E413*
-    #                                     ^----------------------------------------^
-    #                                     ^\s*\d\+\.\%(\d\+\.\=\)*\s\+.\{-}\*\zs.*
+    #     14. Linking groups       *:hi-link* *:highlight-link* *E412* *E413*
+    #                              ^----------------------------------------^
+    #                              ^\s*\d\+\.\%(\d\+\.\=\)*\s\+.\{-}\*\zs.*
     var after_numbered: string = '^\s*\d\+\.\%(\d\+\.\=\)*\s\+.\{-}\*\zs.*'
-    #     01.3    Using the Vim tutor                             *tutor* *vimtutor*
-    #                                                             ^----------------^
+    #     01.3    Using the Vim tutor                      *tutor* *vimtutor*
+    #                                                      ^----------------^
     var after_numbered_tutor: string = '^\*\d\+\.\%(\d\+\.\=\)*.\{-}\t\*\zs.*'
-    var noisy_tags: string = $'{after_HEADLINE}\|{after_numbered}\|{after_numbered_tutor}'
+    var noisy_tags: string =
+        $'{after_HEADLINE}\|{after_numbered}\|{after_numbered_tutor}'
     text = text->substitute(noisy_tags, '', '')
-    # We  don't remove  the trailing  asterisk, because  the help  syntax plugin
+    # We don't remove the trailing asterisk, because the help syntax plugin
     # might need it to highlight some headlines.
 
     b:toc.entries->add({
@@ -498,7 +810,7 @@
 
 def Popup_settext(winid: number, entries: list<dict<any>>) #{{{2
     var text: list<any>
-    # When we  fuzzy search  the toc,  the dictionaries  in `entries`  contain a
+    # When we fuzzy search the toc, the dictionaries in `entries` contain a
     # `props` key, to highlight each matched character individually.
     # We don't want to process those dictionaries further.
     # The processing should already have been done by the caller.
@@ -544,7 +856,8 @@
     var lnum: number = line('.')
     var firstline: number = b:toc.entries
         ->copy()
-        ->filter((_, line: dict<any>): bool => line.lvl <= b:toc.curlvl && line.lnum <= lnum)
+        ->filter((_, line: dict<any>): bool =>
+            line.lvl <= b:toc.curlvl && line.lnum <= lnum)
         ->len()
     if firstline == 0
         return
@@ -599,8 +912,8 @@
         if key == 'J' || key == 'K'
             var lnum: number = GetBufLnum(winid)
             execute $'normal! 0{lnum}zt'
-            # install a match in the regular buffer to highlight the position of
-            # the entry in the latter
+            # Install a match in the regular buffer to highlight the position
+            # of the entry in the latter
             MatchDelete()
             selected_entry_match = matchaddpos('IncSearch', [lnum], 0, -1)
         endif
@@ -665,41 +978,44 @@
         return true
 
     elseif key == '/'
-        # This is probably what the user expect if they've started a first fuzzy
-        # search, press Escape, then start a new one.
+        # This is probably what the user expects if they've started a first
+        # fuzzy search, press Escape, then start a new one.
         DisplayNonFuzzyToc(winid)
 
         [{
-            group: AUGROUP,
+            group: 'HelpToc',
             event: 'CmdlineChanged',
             pattern: '@',
             cmd: $'FuzzySearch({winid})',
             replace: true,
         }, {
-            group: AUGROUP,
+            group: 'HelpToc',
             event: 'CmdlineLeave',
             pattern: '@',
             cmd: 'TearDown()',
             replace: true,
         }]->autocmd_add()
 
-        # Need to evaluate `winid` right now with an `eval`'ed and `execute()`'ed heredoc because:{{{
+        # Need to evaluate `winid` right now{{{
+        # with an `eval`'ed and `execute()`'ed heredoc because:
         #
-        #    - the mappings can only access the script-local namespace
-        #    - `winid` is in the function namespace; not in the script-local one
+        #  - the mappings can only access the script-local namespace
+        #  - `winid` is in the function namespace; not in the script-local one
         #}}}
         var input_mappings: list<string> =<< trim eval END
-            cnoremap <buffer><nowait> <Down> <ScriptCmd>Filter({winid}, 'j')<CR>
-            cnoremap <buffer><nowait> <Up> <ScriptCmd>Filter({winid}, 'k')<CR>
-            cnoremap <buffer><nowait> <C-N> <ScriptCmd>Filter({winid}, 'j')<CR>
-            cnoremap <buffer><nowait> <C-P> <ScriptCmd>Filter({winid}, 'k')<CR>
+          cnoremap <buffer><nowait> <Down> <ScriptCmd>Filter({winid}, 'j')<CR>
+          cnoremap <buffer><nowait> <Up> <ScriptCmd>Filter({winid}, 'k')<CR>
+          cnoremap <buffer><nowait> <C-N> <ScriptCmd>Filter({winid}, 'j')<CR>
+          cnoremap <buffer><nowait> <C-P> <ScriptCmd>Filter({winid}, 'k')<CR>
         END
         input_mappings->execute()
 
         var look_for: string
         try
             popup_setoptions(winid, {mapping: true})
-            look_for = input('look for: ', '', $'custom,{Complete->string()}') | redraw | echo ''
+            look_for = input('look for: ', '', $'custom,{Complete->string()}')
+                | redraw
+                | echo ''
         catch /Vim:Interrupt/
             TearDown()
         finally
@@ -718,9 +1034,9 @@
         return
     endif
 
-    # We  match against  *all* entries;  not  just the  currently visible  ones.
-    # Rationale: If we use a (fuzzy) search, we're probably lost.  We don't know
-    # where the info is.
+    # We match against *all* entries; not just the currently visible ones.
+    # Rationale: If we use a (fuzzy) search, we're probably lost.  We don't
+    # know where the info is.
     var matches: list<list<any>> = b:toc.entries
         ->copy()
         ->matchfuzzypos(look_for, {key: 'text'})
@@ -764,7 +1080,7 @@
 enddef
 
 def CollapseOrExpand(winid: number, key: string) #{{{2
-    # Must  be  saved  before  we  reset  the  popup  contents,  so  we  can
+    # Must be saved before we reset the popup contents, so we can
     # automatically select the least unexpected entry in the updated popup.
     var buf_lnum: number = GetBufLnum(winid)
 
@@ -798,11 +1114,11 @@
         endwhile
     endif
 
-    # update the popup contents
+    # Update the popup contents
     var toc_entries: list<dict<any>> = GetTocEntries()
     Popup_settext(winid, toc_entries)
 
-    # Try to  select the same entry;  if it's no longer  visible, select its
+    # Try to select the same entry;  if it's no longer visible, select its
     # direct parent.
     var toc_lnum: number = 0
     for entry: dict<any> in toc_entries
@@ -834,6 +1150,8 @@
     if choice == -1
         fuzzy_entries = null_list
         return
+    elseif choice == -2  # Button X is clicked (when close: 'button')
+        return
     endif
 
     var lnum: number = GetTocEntries()
@@ -851,36 +1169,38 @@
 enddef
 
 def ToggleHelp(menu_winid: number) #{{{2
+    # Show/hide HELP_TEXT in a second popup when '?' is typed{{{
+    # (when a helptoc popup is open).  A scrollbar on this popup makes sense
+    # because it is very long and, even if it's not used for scrolling, works
+    # well as an indicator of how far through the HELP_TEXT popup you are. }}}
     if help_winid == 0
         var height: number = [HELP_TEXT->len(), winheight(0) * 2 / 3]->min()
         var longest_line: number = HELP_TEXT
             ->copy()
             ->map((_, line: string) => line->strcharlen())
             ->max()
-        var width: number = [longest_line, winwidth(0) * 2 / 3]->min()
-        var pos: dict<number> = popup_getpos(menu_winid)
-        var [line: number, col: number] = [pos.line, pos.col]
-        --col
+        var width: number = [longest_line, winwidth(0) - 4]->min()
         var zindex: number = popup_getoptions(menu_winid).zindex
         ++zindex
         help_winid = HELP_TEXT->popup_create({
-            line: line,
-            col: col,
-            pos: 'topright',
+            pos: 'center',
             minheight: height,
             maxheight: height,
             minwidth: width,
             maxwidth: width,
-            border: [],
-            borderchars: ['─', '│', '─', '│', '┌', '┐', '┘', '└'],
             highlight: &buftype == 'terminal' ? 'Terminal' : 'Normal',
-            scrollbar: false,
             zindex: zindex,
+            border: g:helptoc.popup_border,
+            borderchars: g:helptoc.popup_borderchars,
+            borderhighlight: g:helptoc.popup_borderhighlight,
+            close: g:helptoc.popup_close,
+            scrollbar: true,
         })
 
         setwinvar(help_winid, '&cursorline', true)
         setwinvar(help_winid, '&linebreak', true)
-        matchadd('Special', '^<\S\+\|^\S\{,2}  \@=', 0, -1, {window: help_winid})
+        matchadd('Special', '^<\S\+\|^\S\{,2}  \@=', 0, -1,
+            {window: help_winid})
         matchadd('Number', '\d\+', 0, -1, {window: help_winid})
         for lnum: number in HELP_TEXT->len()->range()
             if HELP_TEXT[lnum] =~ '^─\+$'
@@ -898,23 +1218,22 @@
 enddef
 
 def Win_execute(winid: number, cmd: any) #{{{2
-# wrapper around `win_execute()`  to enforce a redraw, which  might be necessary
+# wrapper around `win_execute()` to enforce a redraw, which might be necessary
 # whenever we change the cursor position
     win_execute(winid, cmd)
     redraw
 enddef
 
 def TearDown() #{{{2
-    autocmd_delete([{group: AUGROUP}])
+    autocmd_delete([{group: 'HelpToc'}])
     cunmap <buffer> <Down>
     cunmap <buffer> <Up>
     cunmap <buffer> <C-N>
     cunmap <buffer> <C-P>
 enddef
-#}}}1
 # Util {{{1
 def GetType(): string #{{{2
-    return &buftype == 'terminal' ?  'terminal' : &filetype
+    return &buftype == 'terminal' ? 'terminal' : &filetype
 enddef
 
 def GetTocEntries(): list<dict<any>> #{{{2
@@ -944,10 +1263,12 @@
 def Complete(..._): string #{{{2
     return b:toc.entries
         ->copy()
-        ->map((_, entry: dict<any>) => entry.text->trim(' ~')->substitute('*', '', 'g'))
+        ->map((_, entry: dict<any>) =>
+            entry.text->trim(' ~')->substitute('*', '', 'g'))
         ->filter((_, text: string): bool => text =~ '^[-a-zA-Z0-9_() ]\+$')
         ->sort()
         ->uniq()
         ->join("\n")
-enddef
-
+enddef  #}}}2
+#}}}1
+# vim:et:ft=vim:fdm=marker:
diff --git a/runtime/pack/dist/opt/helptoc/doc/helptoc.txt b/runtime/pack/dist/opt/helptoc/doc/helptoc.txt
new file mode 100644
index 0000000..4bd87e6
--- /dev/null
+++ b/runtime/pack/dist/opt/helptoc/doc/helptoc.txt
@@ -0,0 +1,349 @@
+*helptoc.txt*   For Vim version 9.1.  Last change:  2025 May 04
+
+
+			  VIM REFERENCE MANUAL
+
+Interactive table of contents for help buffers and several other filetypes
+
+==============================================================================
+
+
+1. OVERVIEW						*HelpToc-overview*
+
+The helptoc.vim plugin provides one command, :HelpToc, which generates a
+hierarchical table of contents in a popup window, which is based on the
+structure of a Vim buffer.  It was designed initially for help buffers,
+but it also works with buffers of the following types:
+	- asciidoc
+	- html
+	- man
+	- markdown
+	- terminal
+	- tex
+	- vim		Note: only with numbered fold markers, e.g. {{{1
+	- xhtml
+
+1.1 The :HelpToc command				*HelpToc-:HelpToc*
+
+The :HelpToc command takes no arguments and it cannot be executed from an
+unsupported filetype.  Also, it cannot be used to generate a table of contents
+for an inactive buffer.
+
+For most buffers of the supported types, :HelpToc may be entered directly
+in Vim's |Command-line-mode|.  How to use it from Vim's |Terminal-Job| mode is
+explained in |HelpToc-terminal-buftype|.
+
+You may choose to map :HelpToc to keys making it easier to use.  These are
+examples of what could be used: >
+
+	nnoremap <Leader>ht <Cmd>HelpToc<CR>
+	tnoremap <C-t><C-t> <Cmd>HelpToc<CR>
+<
+
+2. DETAILS						*HelpToc-details*
+
+When the :HelpToc command is executed from an active buffer of a supported
+type, a popup window is produced.  The window contains a hierarchical and
+interactive table of contents.  The entries are based on the "headings" in
+the buffer.
+
+Jumping to an entry's position in the buffer can be achieved by pressing
+enter on the applicable entry.  Navigation, and other commands applicable to
+the popup window, such as expanding and contracting levels, fuzzy searching,
+and jumping to the previous or next entry (leaving the table of contents
+itself displayed, using J and K), is provided at |help-TOC|, so that is not
+reproduced in this help file.
+
+
+3. TYPES						*HelpToc-types*
+
+Some filetypes have more predictable structures than others.  For example,
+markdown and asciidoc make the identification of headings (aside from edge
+cases, such as when in quotes) straightforward.  Some filetypes do not have
+such obvious or reliable headings/levels (particularly help buffers).
+Further, in some instances, how to enter the :HelpToc command is not
+necessarily obvious, e.g., when in a Vim |terminal-window|.  So, the following
+headings address specific details regarding the in-scope types.
+
+3.1 asciidoc					*HelpToc-asciidoc-filetype*
+
+The heading levels in asciidoc are typically identified by lines starting
+with a "=" (up to six, one per level), one or more blank characters, and the
+heading text.  :HelpToc will generate a table of contents based on either
+that form of heading type or, because asciidoc also allows for ATX heading
+level syntax (i.e., using the "#" character), headings using that format too.
+
+As asciidoc is very structured, its table of contents entries are presented
+in a standardized form - see |HelpToc-standardized-toc|.  So, the initial
+"=" or "#" characters and the following space from the related heading are
+not included in the table of contents' entries.
+
+3.2 html					*HelpToc-html-filetype*
+
+HTML provides for six levels of headings, <h1> to <h6>, which may be either
+upper or lower case and preceded by all sorts of content like <!--comments-->.
+:HelpToc will produce a table of contents based on the six heading levels.
+
+As HTML is very structured, its table of contents entries are presented
+in a standardized form - see |HelpToc-standardized-toc|.  So, the <h1> to <h6>
+tags, any preceding content, and any trailing content after the heading text,
+is not included in the table of contents' entries.
+
+3.3 man pages					*HelpToc-man-filetype*
+
+Retrieving man pages is typically performed in the terminal.  To use :HelpToc
+to generate a table of contents, the |man.vim| filetype plugin is a
+prerequisite.  It is provided with Vim, and may be sourced with: >
+
+	:runtime ftplugin/man.vim
+<
+Once sourced, the |:Man| command will open the applicable man page in a new
+buffer (of "man" filetype).  For example: >
+
+	:Man pwd
+<
+Once in the man buffer, entering :HelpToc opens the table of contents popup,
+with level 1 containing section names like NAME, SYNOPSIS, etc.  Levels
+below 1 include subsections and options, with the level depending on the
+number of spaces preceding the content.
+
+The table of contents for a man buffer's popup window has the same syntax
+highlighting as man pages.  This reflects that its content is reproduced
+as-is, i.e., no preceding tags, level-indicating data, etc., need be omitted
+for optimal presentation.
+
+3.4 markdown					*HelpToc-markdown-filetype*
+
+The headings and levels in markdown typically are ATX formatted headings
+(lines starting with up to three spaces, one to six "#", then (optionally)
+one or blank characters and the heading text).  The alternate form,
+setext, uses up to three spaces, the heading 1 text, followed by a
+line of one or more "=".  The setext heading 2 is similar, but for one or
+more "-" instead of "=".  There is no heading 3+ in setext.  ATX and setext
+headings are supported.
+
+As markdown is very structured, its table of contents entries are presented
+in a standardized form - see |HelpToc-standardized-toc|.  So, they do not
+include any leading spaces, any initial "#" characters and the following blank
+character(s) in the table of contents' entries.
+
+3.5 terminal					*HelpToc-terminal-buftype*
+
+There are no explicit "headings" for a terminal buffer.  However, :HelpToc
+displays the history of executed shell commands.  Those may be specified
+by changing the pattern used to match the Vim terminal's prompt.
+See |HelpToc-configuration| for examples.
+
+To access the terminal's table of contents, from the Vim's |Terminal-Job| mode
+enter CTRL-W N to go to |Terminal-Normal| mode.  From there, enter :HelpToc to
+generate the table of contents.  If you use the terminal's table of contents
+a lot, an appropriate mapping may make it easier than using CTRL-W N - e.g.: >
+
+          tnoremap <C-t><C-t> <Cmd>HelpToc<CR>
+<
+As the terminal has only "level 1", the table of contents is presented in a
+standardized form - see |HelpToc-standardized-toc| - including only the history
+list of commands.  The prompt itself is also omitted since it adds no value
+repeating it for every entry.
+
+3.6 tex						*HelpToc-tex-filetype*
+
+In LaTeX, a document may be structured hierarchically using part, chapter,
+and sectioning commands.  Document structure levels are:
+	\part{}
+	\chapter{}
+	\section{}
+	\subsection{}
+	\subsubsection{}
+
+To keep things simpler, \part{} is supported, though treated as being at
+the same level as chapter.  Consequently, there are four levels displayed
+for a tex filetype's table of contents, regardless of the \documentclass{},
+i.e., part and chapter (at level 1), section (level 2), subsection (level 3),
+and subsubsection (level 4).
+
+Also supported are:
+	- The "*" used to produce unnumbered headings, which are not intended
+	  for reproduction in a table of contents: >
+		\section*{Unnumbered section heading not produced in the TOC}
+<	- Manual toc entries using \addcontentsline, for example: >
+		\addcontentsline{toc}{section}{entry in the TOC only!}
+<
+The table of contents for a tex filetype is in a standardized form -
+see |HelpToc-standardized-toc|.  Omitted are: the "\", the part, chapter,
+*section, or addcontentsline, and the left and right curly
+brackets preceding and following each heading's text.
+
+3.7 vim						*HelpToc-vim-filetype*
+
+Vimscript and Vim9 script do not have headings or levels inherently like
+markup languages.  However, Vim provides for |folds| defined by markers (|{{{|),
+which themselves may be succeeded by a number explicitly indicating the fold
+level.  This is the structure recognized and supported by helptoc.vim.
+So, for example, the following would produce three table of contents entries: >
+
+	vim9script
+	# Variables {{{1
+	var b: bool = true
+	var s: string = $"Fold markers are great?  {b}!"
+	# Functions {{{1
+	def MyFunction(): void #{{{2
+	  echo s
+	enddef
+	MyFunction()
+<
+The table of contents for that script would appear like this:
+	Variables
+	Functions
+	| MyFunction(): void
+
+Note: The numbered |{{{| marker structure is the only one supported by
+      helptoc.vim for the vim filetype.
+
+As the {{{1 to {{{6 markers make the "headings" explicit, the table of
+contents is in a standardized form - see |HelpToc-standardized-toc|.
+It does not include any leading comment markers (i.e., either # or ") and
+omits the markers themselves.
+
+3.8 xhtml					*HelpToc-xhtml-filetype*
+
+Although XHTML, being XML, is more strictly structured than HTML/HTML5,
+there is no practical difference in treatment required for the xhtml filetype
+because, at the heading level, the tags that matter are very similar.
+See |HelpToc-html-filetype| for how an xhtml filetype's table of contents is
+supported.
+
+
+4. STANDARDIZED TOC				*HelpToc-standardized-toc*
+
+The table of contents for a help buffer, terminal, or man page, make sense
+being presented in the form they appear, minus trailing content (such as tags).
+
+The table of contents for a markdown, asciidoc, [x]html, terminal, or tex
+buffer have superfluous content if the entire line was to be returned.
+For example:
+- Markdown has "#" characters before headings when using ATX heading notation.
+- Asciidoc will have either those or, more often, "=" characters before its
+  headings.
+- HTML, aside from the "<h" headings, may have additional tags, comments,
+  and whitespace before its headings.
+- The Vim terminal has the shell prompt, which adds nothing if repeated for
+  every heading (and may be very long).
+- LaTeX has "\" level indicators like "\section{" and a trailing "}".
+Consequently, standardising these filetypes' tables of contents, removing
+the "noise", and indicating the contents level of each entry, makes sense.
+
+HelpToc standardizes the markdown, asciidoc, [x]html, terminal and tex tables
+of contents by removing extraneous characters, markup indicators, and tags.
+It also applies simple, unobtrusive syntax highlighting to the text and level
+indicators.  By default, it will appear like the following example (though
+any level indicators will be less prominent, using |NonText| highlight group).
+
+	Level 1
+	| Level 2
+	| | Level 3
+	| | | Level 4
+	| | | | Level 5
+	| | | | | Level 6
+
+Note: The "| " level indicator may be changed - see |HelpToc-configuration|.
+
+
+5. CONFIGURATION				*HelpToc-configuration*
+
+All configuration is achieved utilizing the g:helptoc dictionary.  Any of the
+following may be adjusted to meet your needs or preferences:
+
+g:helptoc key	 what it controls
+-------------	 ----------------
+shell_prompt	 The terminal prompt, used for creating a table of contents
+		 for the terminal (history list).  The default is,
+		 '^\w\+@\w\+:\f\+\$\s', which should match many users' bash
+		 prompt.  To change it, either interactively or in your .vimrc,
+		 use (for example for a bare Bourne shell "$ " prompt): >
+			vim9 g:helptoc.shell_prompt = '^\$\s'
+
+<level_indicator  This key's value controls the level indicator used in
+		 standardized tables of contents.  The default is '| '
+		 (i.e., a vertical bar and a space), but may be changed to
+		 whatever you want.  For example, for a broken bar and space: >
+			vim9 g:helptoc.level_indicator = '¦ '
+<
+popup_border	 By default, the table of contents border will appear above,
+		 right, below, and left of the popup window.  If you prefer
+		 not to have the border on the right and left (for example
+		 only), you can achieve that with: >
+			vim9 g:helptoc.popup_border = [1, 0, 1, 0]
+<popup_borderchars
+		 The default border characters for the table of contents popup
+		 window is the list ['─', '│', '─', '│', '┌', '┐', '┘', '└'].
+		 There's nothing wrong with those box drawing characters,
+		 though, for example, if you wanted a border that only uses
+		 ASCII characters, you could make the border spaces only: >
+			vim9 g:helptoc.popup_borderchars = [' ']
+<popup_borderhighlight
+		 The default border highlight group is Normal.  You can change
+		 that, perhaps in combination with popup_borderchars, above,
+		 to create a very clearly prominent border.  For example, if
+		 the popup_borderchars are made [' '], like above, the border
+		 could be made a solid colour different to the background
+		 with: >
+			vim9 g:helptoc.popup_borderhighlight = ['Cursor']
+
+<			  Note: Choosing a highlight group that persists when
+				colorschemes change may be a good idea if you
+				do choose to customize this.
+
+popup_drag	By default, table of contents popup windows may be dragged
+		with a mouse.  If you want to prevent that from happening,
+		for whatever reason, you may deactivate it with: >
+			vim9 g:helptoc.popup_drag = false
+<
+popup_close	Table of contents popups have "none" as the default setting
+		for this option.  If you use a mouse, you may want either
+		to have the option to close popup windows by clicking on them
+		or to have a clickable "X" in the top right corner.  For the
+		former, use "click", and for the latter, use "button", e.g.: >
+			vim9 g:helptoc.popup_close = "button"
+<popup_scrollbar
+		No scrollbar is provided on helptoc popups by default.  If you
+		do want scrollbars (which may be useful as an indicator of how
+		far through the table of contents you are, not just for using
+		with a mouse) you may choose to have them with: >
+			vim9 g:helptoc.popup_scrollbar = true
+<
+NOTE: Information about the "popup_*" options, above, relate to popup options,
+which are explained at the 'second argument' part of |popup_create-arguments|.
+
+
+6. LIMITATIONS					*HelpToc-limitations*
+
+- The help filetype may have edge case formatting patterns.  Those may result
+  in some "headings" not being identified and/or may impact the heading levels
+  of entries in the table of contents itself.
+- Terminal window table of contents may not be active (insofar as jumping to
+  entries going to the Vim terminal's related command line).  For example, if
+  Vim's terminal is set to Windows PowerShell Core, the table of contents will
+  display successfully, though the entries go nowhere when Enter, J, or K are
+  entered on them.
+- The tex filetype may have variable sectioning commands depending on the
+  document class.  Consequently, some compromises are made, though they should
+  have minimal impact.  Specifically:
+  * In instances where \part{} and \chapter{} appear in the same buffer, they
+    will both present at the top level in the table of contents.  This should
+    be a minor matter because, in many instances, chapters will be in a
+    separate document using \include{}.
+  * An article or beamer \documentclass without a \part{} (or any document
+    with neither any \part{} nor any \chapter{} command) will have no content
+    at level 1.  Consequently, its table of contents entries will all appear
+    preceded by at least one "| " (by default) because its headings start at
+    level 2 (presuming \section{} is present).
+- The vim filetype is only supported where numbered fold markers are applied.
+  This is intentional (including not handling unnumbered markers, which, when
+  used in combination with numbered ones, may be used for folding comments).
+  helptoc.vim itself provides an exemplar of how to use numbered fold markers,
+  not only for folds, but to support generating a useful table of contents
+  using :HelpToc.
+
+==============================================================================
+vim:tw=78:ts=8:fo=tcq2:ft=help:
diff --git a/runtime/pack/dist/opt/helptoc/doc/tags b/runtime/pack/dist/opt/helptoc/doc/tags
new file mode 100644
index 0000000..cd62dec
--- /dev/null
+++ b/runtime/pack/dist/opt/helptoc/doc/tags
@@ -0,0 +1,16 @@
+HelpToc-:HelpToc	helptoc.txt	/*HelpToc-:HelpToc*
+HelpToc-asciidoc-filetype	helptoc.txt	/*HelpToc-asciidoc-filetype*
+HelpToc-configuration	helptoc.txt	/*HelpToc-configuration*
+HelpToc-details	helptoc.txt	/*HelpToc-details*
+HelpToc-html-filetype	helptoc.txt	/*HelpToc-html-filetype*
+HelpToc-limitations	helptoc.txt	/*HelpToc-limitations*
+HelpToc-man-filetype	helptoc.txt	/*HelpToc-man-filetype*
+HelpToc-markdown-filetype	helptoc.txt	/*HelpToc-markdown-filetype*
+HelpToc-overview	helptoc.txt	/*HelpToc-overview*
+HelpToc-standardized-toc	helptoc.txt	/*HelpToc-standardized-toc*
+HelpToc-terminal-buftype	helptoc.txt	/*HelpToc-terminal-buftype*
+HelpToc-tex-filetype	helptoc.txt	/*HelpToc-tex-filetype*
+HelpToc-types	helptoc.txt	/*HelpToc-types*
+HelpToc-vim-filetype	helptoc.txt	/*HelpToc-vim-filetype*
+HelpToc-xhtml-filetype	helptoc.txt	/*HelpToc-xhtml-filetype*
+helptoc.txt	helptoc.txt	/*helptoc.txt*