Function: markdown-ts-toc-generate
markdown-ts-toc-generate is an interactive and byte-compiled function
defined in markdown-ts-mode-x.el.gz.
Signature
(markdown-ts-toc-generate &optional INTERACTIVE BEG END)
Documentation
Generate tables of contents in the current buffer.
markdown-ts-mode uses Markdown HTML comment elements to identify table
of contents (aka toc) insertion boundaries and the parameters for each
table. Derive table content from the Markdown elements in the current
buffer which are usually headings but can also be list items, setext
headers, named code blocks.
Operate on the active region BEG to END, otherwise operate on the buffer, which may be narrowed.
Each time you run this command, existing tables of contents are cleared and refreshed with new content. So don't place toc elements around important text.
The buffer can have one or more tocs. By default, populate each toc with elements that appear in the buffer below the toc. Therefore, the easiest way to insert a complete table of contents, is to put the toc template near the top of your buffer.
A basic empty toc template looks like this:
Header text you want here.
<!-- markdown-ts-toc: -->
Contents inserted between these elements.
<!-- markdown-ts-toc-end: -->
Footer text you want here.
Use the command markdown-ts-toc-insert-template to insert a table of
contents template.
Use the command markdown-ts-toc-clear to clear table content, and the
command markdown-ts-toc-clear-and-remove to both clear and remove
table content and templates.
By default, a toc includes headers at all levels below the toc which are presented as list items with links.
To ignore a Markdown element that would otherwise be included in the toc, add a toc ignore element:
## Ignore Me <!-- markdown-ts-toc-ignore: -->
The starting toc element accepts parameters in the syntax of Emacs file variables.
For example, if you want to generate a table of contents limited to headings two levels deep:
<!-- markdown-ts-toc: -*- max-depth: 2; -*- -->
<!-- markdown-ts-toc-end: -->
Or use only headings two and three levels deep.
<!-- markdown-ts-toc: -*- min-depth: 2; max-depth: 3; -*- -->
<!-- markdown-ts-toc-end: -->
These are the supported parameters which are optional. They each list their defaults.
min-depth: 1 ; an integer
max-depth: nil ; an integer
candidates: (heading) ; a list
from: below ; a symbol
style: item ; a symbol
indent: 1 ; a number
no-link: nil ; a boolean
relative-depth: nil ; a boolean
ignore: nil ; a boolean
min-depth and max-depth both default to using all candidates at
every level. Use min-depth to ignore level 1 headings like "#
Heading", by using 2. Use max-depth to control how deep into the
heading hierarchy you need your toc to go. Use 3 to stop at level three
headings.
Headings dictate the level of their children even if headings are excluded from candidates. Other element types inherit the level of their preceding heading.
candidates is a list and can contain any of the following Markdown
element shortcut names. You can mix higher-level grouping symbols and
lower level symbols.
all (includes everything)
heading (includes the below)
h1 h2 h3 h4 h5 h6
setext (includes the below)
sh1 sh2
item (includes the below)
minus plus star
numbered-item (includes the below)
dot paren
code (named code blocks)
from controls the direction and scope from which candidates are
selected. Typically, a toc is placed at the beginning of a buffer and
from is below to capture entry candidates after the toc. If you set
from to above, candidates are selected from above the toc. Use
all to capture every entry candidate. This is useful to create a
complete toc at the end of your buffer.
The style parameter can be nil, item, number or number.. Under
nil, entries have no decorations. If item, entries are prefixed with
"-". If bullet, prefix with "*". If number, numeric prefixes
are generated, for example 1, 1.1, 2, 2.1, 2.1.1, 2.1.2. Use number.
to add a period to each entry's number.
indent is an integer between 0 and 10. 0 means nothing should be
indented. Otherwise, each entry is indented by the number of spaces of
its Markdown level multiplied by this value. Candidates that have no
native level such as list items inherit the base indentation level of
its preceding header. List item hierarchies and code blocks indent
under that base.
Set no-link to t if you do not want your entries to have header links.
By default, each entry derived from a heading is presented as a link to
its source heading. NOTE: The elements represented by setext, item,
numbered-item, code do not support links.
Use relative-depth to create a toc underneath a heading, limiting the
toc to heading depths underneath the preceding header and specify the
levels relative to that header. More concretely, if the preceding
header is a level 1 "#" header, min-depth 1 and max-depth 2 will
be interpreted to be 2 and 3. If you demote the header to level 2
"##" they will be interpreted as 3 and 4. This is useful to avoid
fussing with toc configurations under headers when you change their
levels.
For example:
<!-- markdown-ts-toc: -*- relative-depth: t; min-depth: 1; max-depth: 2; -*- -->
<!-- markdown-ts-toc-end: -->
Finally, if ignore is t, you can keep a toc element in place and skip
it.
INTERACTIVE will be non-nil if this command was invoked interactively.
See markdown-ts-toc-generate-warn-if-none to configure warnings about
tables not being processed when this function is called.
Key Bindings
Source Code
;; Defined in /usr/src/emacs/lisp/textmodes/markdown-ts-mode-x.el.gz
(defun markdown-ts-toc-generate (&optional interactive beg end)
"Generate tables of contents in the current buffer.
`markdown-ts-mode' uses Markdown HTML comment elements to identify table
of contents (aka toc) insertion boundaries and the parameters for each
table. Derive table content from the Markdown elements in the current
buffer which are usually headings but can also be list items, setext
headers, named code blocks.
Operate on the active region BEG to END, otherwise operate on the
buffer, which may be narrowed.
Each time you run this command, existing tables of contents are cleared
and refreshed with new content. So don't place toc elements around
important text.
The buffer can have one or more tocs. By default, populate each toc
with elements that appear in the buffer below the toc. Therefore, the
easiest way to insert a complete table of contents, is to put the toc
template near the top of your buffer.
A basic empty toc template looks like this:
Header text you want here.
<!-- markdown-ts-toc: -->
Contents inserted between these elements.
<!-- markdown-ts-toc-end: -->
Footer text you want here.
Use the command `markdown-ts-toc-insert-template' to insert a table of
contents template.
Use the command `markdown-ts-toc-clear' to clear table content, and the
command `markdown-ts-toc-clear-and-remove' to both clear and remove
table content and templates.
By default, a toc includes headers at all levels below the toc which are
presented as list items with links.
To ignore a Markdown element that would otherwise be included in the
toc, add a toc ignore element:
## Ignore Me <!-- markdown-ts-toc-ignore: -->
The starting toc element accepts parameters in the syntax of Emacs file
variables.
For example, if you want to generate a table of contents limited to
headings two levels deep:
<!-- markdown-ts-toc: -*- max-depth: 2; -*- -->
<!-- markdown-ts-toc-end: -->
Or use only headings two and three levels deep.
<!-- markdown-ts-toc: -*- min-depth: 2; max-depth: 3; -*- -->
<!-- markdown-ts-toc-end: -->
These are the supported parameters which are optional. They each list
their defaults.
min-depth: 1 ; an integer
max-depth: nil ; an integer
candidates: (heading) ; a list
from: below ; a symbol
style: item ; a symbol
indent: 1 ; a number
no-link: nil ; a boolean
relative-depth: nil ; a boolean
ignore: nil ; a boolean
`min-depth' and `max-depth' both default to using all candidates at
every level. Use `min-depth' to ignore level 1 headings like \"#
Heading\", by using 2. Use `max-depth' to control how deep into the
heading hierarchy you need your toc to go. Use 3 to stop at level three
headings.
Headings dictate the level of their children even if headings are
excluded from candidates. Other element types inherit the level of
their preceding heading.
`candidates' is a list and can contain any of the following Markdown
element shortcut names. You can mix higher-level grouping symbols and
lower level symbols.
all (includes everything)
heading (includes the below)
h1 h2 h3 h4 h5 h6
setext (includes the below)
sh1 sh2
item (includes the below)
minus plus star
numbered-item (includes the below)
dot paren
code (named code blocks)
`from' controls the direction and scope from which candidates are
selected. Typically, a toc is placed at the beginning of a buffer and
`from' is `below' to capture entry candidates after the toc. If you set
`from' to `above', candidates are selected from above the toc. Use
`all' to capture every entry candidate. This is useful to create a
complete toc at the end of your buffer.
The `style' parameter can be nil, `item', `number' or `number.'. Under
nil, entries have no decorations. If `item', entries are prefixed with
\"-\". If `bullet', prefix with \"*\". If `number', numeric prefixes
are generated, for example 1, 1.1, 2, 2.1, 2.1.1, 2.1.2. Use `number.'
to add a period to each entry's number.
`indent' is an integer between 0 and 10. 0 means nothing should be
indented. Otherwise, each entry is indented by the number of spaces of
its Markdown level multiplied by this value. Candidates that have no
native level such as list items inherit the base indentation level of
its preceding header. List item hierarchies and code blocks indent
under that base.
Set `no-link' to t if you do not want your entries to have header links.
By default, each entry derived from a heading is presented as a link to
its source heading. NOTE: The elements represented by `setext', `item',
`numbered-item', `code' do not support links.
Use `relative-depth' to create a toc underneath a heading, limiting the
toc to heading depths underneath the preceding header and specify the
levels relative to that header. More concretely, if the preceding
header is a level 1 \"#\" header, `min-depth' 1 and `max-depth' 2 will
be interpreted to be 2 and 3. If you demote the header to level 2
\"##\" they will be interpreted as 3 and 4. This is useful to avoid
fussing with toc configurations under headers when you change their
levels.
For example:
<!-- markdown-ts-toc: -*- relative-depth: t; min-depth: 1; max-depth: 2; -*- -->
<!-- markdown-ts-toc-end: -->
Finally, if `ignore' is t, you can keep a toc element in place and skip
it.
INTERACTIVE will be non-nil if this command was invoked interactively.
See `markdown-ts-toc-generate-warn-if-none' to configure warnings about
tables not being processed when this function is called."
(interactive "p\nR")
;; NOTE: If parameters or their defaults change, be sure to mirror
;; them in `markdown-ts-toc-insert-template'.
(markdown-ts--barf-if-not-mode 'markdown-ts-toc-generate)
(setq beg (or beg (point-min)))
(setq end (or end (point-max)))
(unless (eq beg end)
;; Convert markers to absolute positions.
(setq beg (if (markerp beg) (marker-position beg) beg))
(setq end (if (markerp end) (marker-position end) end))
;; NOTE: Operate on the widened buffer to avoid treesiter crash issues.
(without-restriction
(let ((tocs (markdown-ts--tocs beg end)))
(unless tocs
(if (and interactive
(eq 'insert-template markdown-ts-toc-generate-warn-if-none))
(progn
(markdown-ts-toc-insert-template)
;; If the user didn't quit the prompt, a template may have
;; been added, so rescan the buffer.
(setq tocs (markdown-ts--tocs)))
(when (eq 'no-tables markdown-ts-toc-generate-warn-if-none)
(lwarn 'markdown-ts-toc-generate
:warning
"No tables of contents found in %s."
(buffer-name (current-buffer))))))
(markdown-ts--tocs-sanity-check tocs)
;; Strategy is process one toc at a time from the top to the
;; bottom of the range BEG END. Subsequent toc and entry
;; candidate node positions change as we update each toc text so
;; we have to retrieve them fresh from the end of the most recent
;; toc.
(let ((tocs)
(num-tocs-processed 0)
(toc)
(toc-end)
(toc-size)
;; These are adjusted by the size of inserted text each pass.
(beg-pos beg)
(end-pos end)
(source-buffer (current-buffer)))
;; Find each "markdown-ts-toc" in succession starting at the end
;; of each previous one we find within the requested region.
(while (and (< beg-pos end-pos)
(setq tocs (markdown-ts--tocs beg-pos end-pos)))
(setq toc (car tocs))
(setq toc-size 0)
(when (equal "markdown-ts-toc" (car toc))
;; After sanity checking, we know the second element is a
;; toc-end.
(setq toc-end (nth 1 tocs))
(let* ((toc-node (alist-get 'node toc))
(toc-vars (alist-get 'vars toc))
(toc-ignore (alist-get 'ignore toc-vars)))
(unless toc-ignore
;; Build the toc one candidate at a time, tracking
;; indentation and creating links if needed.
(let* ((toc-end-node (alist-get 'node toc-end))
(reg-beg (treesit-node-end toc-node))
(reg-end (treesit-node-start toc-end-node))
(toc-handles (markdown-ts--toc-expand-candidate-handles
(or (cdr-safe (assq 'candidates toc-vars))
'heading)))
;; Now add headings to always retrieve and derive
;; levels even if they are excluded from the toc.
(seed-handles (copy-tree toc-handles))
(_ (dolist (elt (alist-get 'heading markdown-ts--toc-handles))
(cl-pushnew elt seed-handles)))
(toc-from (or (cdr-safe (assq 'from toc-vars)) 'below))
(toc-style (or (cdr-safe (assq 'style toc-vars)) 'item))
(toc-no-link (alist-get 'no-link toc-vars))
(toc-indent (min 10
(max 0 (or (cdr-safe (assq 'indent toc-vars)) 1))))
(toc-min-depth (max 1
(or (cdr-safe (assq 'min-depth toc-vars)) 1)))
(toc-max-depth (max toc-min-depth
(or (cdr-safe (assq 'max-depth toc-vars))
most-positive-fixnum)))
(toc-relative-depth (cdr-safe (assq 'relative-depth toc-vars)))
(toc-candidates (markdown-ts--toc-collect-candidates
toc-node toc-end-node seed-handles toc-from))
(toc-relative-heading-depth
(1+ (or (seq-position
'("atx_h1_marker" "atx_h2_marker" "atx_h3_marker"
"atx_h4_marker" "atx_h5_marker" "atx_h6_marker")
(treesit-node-type
(treesit-node-child
(treesit-search-forward
toc-node
"atx_heading" 'backward) 0 'named))
#'equal)
-1)))
(slug-to-pos (markdown-ts--heading-ids))
(pos-to-slug
(let ((ht (make-hash-table :size (hash-table-count slug-to-pos)
:test #'equal)))
(maphash (lambda (k v) (puthash v k ht)) slug-to-pos)
ht)))
(with-work-buffer
(insert "<!-- NOTE: markdown-ts-toc generated text section may be overwritten. -->\n")
(let ((handle) (handle-class)
(node)
(indent 0) (level 1) (level-prev 0)
;; Heading level takes precedence over items under
;; it. If there are no headings, it will remain 1.
(level-heading 1)
;; Adjust for non headings under headings.
(number-stack))
;; Candidates should be in buffer order.
(dolist (candidate toc-candidates)
(let ((slug-pos) (text) (link-target)
(level-adj 0))
(setq handle (car candidate)
handle-class (alist-get
handle markdown-ts--toc-handle-classes))
(setq node (cdr candidate))
(setq slug-pos nil)
;; Strip trailing newlines. Add them consistently below.
(setq text (string-trim-right
(markdown-ts--toc-text-normalize
handle node
(treesit-node-text node 'no-property))))
;; Candidate node type logic.
(pcase handle-class
('heading
(let ((x (seq-position '(h1 h2 h3 h4 h5 h6) handle)))
(setq slug-pos (treesit-node-start
(treesit-search-forward node "atx_heading" 'backward)))
(setq level (setq level-heading (1+ x)))
(setq level-adj 0)
(setq indent (1- level))
(setq link-target text)))
('setext
(let ((x (seq-position '(sh1 sh2) handle)))
(setq level (1+ x))
(setq level-adj 0)
(setq indent (1- level))
(setq link-target nil)))
((or 'item 'numbered-item)
(setq level level-heading)
(setq level-adj (markdown-ts--toc-list-item-depth node))
(setq indent (1- (+ level level-adj)))
(setq link-target nil))
('code
;; Inherit the ambient level.
(setq level-adj 1)
(setq indent (1- (+ level level-adj)))
(setq link-target nil)))
;; Filter desired handles and levels.
(when (and (memq handle toc-handles)
(>= level (+ toc-min-depth
(if toc-relative-depth
toc-relative-heading-depth 0)))
(<= level (+ toc-max-depth
(if toc-relative-depth
toc-relative-heading-depth 0))))
;; Create the entry with indent, item/number prefix, link.
;; Corner case: initial level higher than 1.
(when (and (> level 1)
(eq level-prev 0))
(setq level 1))
(when (and (not toc-no-link) link-target)
;; Sanitize the id to conform to CSS ident syntax.
(setq text
(format "[%s](#%s)"
text
(if-let* ((slug (gethash slug-pos pos-to-slug)))
slug
(funcall markdown-ts-toc-slug-function
link-target)))))
(insert (make-string (* (1- (max indent 1)) toc-indent) ?\s)
(pcase toc-style
('item "- ")
('bullet "* ")
((or 'number 'number.)
(let ((level (+ level level-adj)))
(cond ((or (not number-stack)
(> level (caar number-stack)))
(push `(,level . 0) number-stack))
((and number-stack
(< level (caar number-stack)))
(while (and number-stack
(< level (caar number-stack)))
(pop number-stack))
(unless number-stack
(push `(,level . 0) number-stack)))))
(setcdr (car number-stack) (1+ (cdar number-stack)))
(let ((s) (n (length number-stack)))
(seq-map-indexed
(lambda (elt idx)
(setq s
(concat
s
(format "%d" (cdr elt))
(if (eq (1+ idx) n) "" "."))))
(reverse number-stack))
(concat s
(when (eq toc-style 'number.) ".")
") ")))
(_ ""))
text
;; Add a hard line break. Backslash
;; seems less well supported by
;; renderers.
(make-string 2 ?\s)
"\n"))
(setq level-prev level))))
(setq toc-size (buffer-size))
(let ((temp-buffer (current-buffer)))
(with-current-buffer source-buffer
(replace-region-contents reg-beg reg-end temp-buffer)))))
(setq num-tocs-processed (1+ num-tocs-processed)))))
;; Start the next pass after this toc-end. Adjust for the
;; inserted text size.
(setq beg-pos (+ (alist-get 'end toc-end) toc-size))
(setq end-pos (min (point-max) (+ end-pos toc-size))))
(when (and (eq num-tocs-processed 0)
(eq 'no-active-tables markdown-ts-toc-generate-warn-if-none))
(lwarn 'markdown-ts-toc-generate
:warning
"No tables of contents generated in %s."
(buffer-name (current-buffer)))))))))