mirror of
git://git.sv.gnu.org/emacs.git
synced 2026-06-14 04:21:24 +00:00
Replace the "c" character-prompt interactive spec with 'read-multiple-choice', which presents named options instead of requiring users to decode the prompt string and type a single character. * lisp/textmodes/markdown-ts-mode.el (markdown-ts-table-align-column): Use 'read-multiple-choice'. Adjust ALIGN docstring punctuation. * lisp/textmodes/markdown-ts-mode-x.el (markdown-ts-toc-insert-template): Use 'read-multiple-choice'.
1140 lines
50 KiB
EmacsLisp
1140 lines
50 KiB
EmacsLisp
;;; markdown-ts-mode-x.el --- markdown-ts-mode extras -*- lexical-binding: t; -*-
|
|
|
|
;; Copyright (C) 2026 Free Software Foundation, Inc.
|
|
|
|
;; Author : Rahul Martim Juliato <rahul.juliato@gmail.com>
|
|
;; : Stéphane Marks <shipmints@gmail.com>
|
|
;; Maintainer : Rahul Martim Juliato <rahul.juliato@gmail.com>
|
|
;; Created : April 2026
|
|
;; Version : 1.0
|
|
;; Package-Requires : ((emacs "31.1"))
|
|
;; Keywords : markdown md text edit languages tree-sitter
|
|
|
|
;; This file is part of GNU Emacs.
|
|
|
|
;; GNU Emacs is free software: you can redistribute it and/or modify
|
|
;; it under the terms of the GNU General Public License as published by
|
|
;; the Free Software Foundation, either version 3 of the License, or
|
|
;; (at your option) any later version.
|
|
|
|
;; GNU Emacs is distributed in the hope that it will be useful,
|
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
;; GNU General Public License for more details.
|
|
|
|
;; You should have received a copy of the GNU General Public License
|
|
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
;;; Commentary:
|
|
|
|
;;; Code:
|
|
|
|
(require 'markdown-ts-mode)
|
|
|
|
;;; Convenience:
|
|
|
|
(defconst markdown-ts-commonmark-spec-url
|
|
"https://spec.commonmark.org/current/")
|
|
|
|
(defconst markdown-ts-gfm-spec-url
|
|
"https://github.github.com/gfm/")
|
|
|
|
(defun markdown-ts-browse-commonmark-spec ()
|
|
"Browse the CommonMark Markdown spec in a web browser."
|
|
(interactive)
|
|
(browse-url markdown-ts-commonmark-spec-url))
|
|
|
|
(defun markdown-ts-browse-gfm-spec ()
|
|
"Browse the GitHub Flavored Markdown spec in a web browser."
|
|
(interactive)
|
|
(browse-url markdown-ts-gfm-spec-url))
|
|
|
|
;;; Export and preview:
|
|
|
|
(defgroup markdown-ts-convert nil
|
|
"Convert `markdown-ts-mode' buffers or files."
|
|
:prefix "markdown-ts-convert-"
|
|
:group 'text
|
|
:group 'editing
|
|
:version "31.1")
|
|
|
|
(defcustom markdown-ts-default-converter nil
|
|
"Default Markdown format and converter.
|
|
If nil, you will be prompted for a format and a converter.
|
|
See `markdown-ts-converters'."
|
|
:type '(choice (const :tag "Prompt for format and converter" nil)
|
|
(const :tag "PDF via Pandoc" (pdf . pandoc))
|
|
(const :tag "HTML via Pandoc" (html . pandoc))
|
|
(const :tag "HTML via cmark" (html . cmark))
|
|
(const :tag "HTML via cmark-gfm (GitHub bells and whistles)" (html . cmark-gfm))
|
|
(const :tag "HTML via markdown" (html . markdown))
|
|
(const :tag "HTML via markdown.pl" (html . markdown.pl))
|
|
(cons :tag "Format and Converter"
|
|
(symbol :tag "Format")
|
|
(symbol :tag "Converter")))
|
|
:version "31.1"
|
|
:package-version "1.0")
|
|
|
|
(defcustom markdown-ts-convert-display-function #'eww-open-file
|
|
"Function to display converted Markdown the output file.
|
|
The function is called with one argument, the file name."
|
|
:type '(choice (const :tag "Display using `eww'" eww-open-file)
|
|
(const :tag "Display in a browser" browse-url-of-file)
|
|
(const :tag "Display output file in a buffer" find-file-other-window)
|
|
(function :tag "Other function"))
|
|
:version "31.1"
|
|
:package-version "1.0")
|
|
|
|
(defconst markdown-ts--convert-error-buffer
|
|
"*markdown-ts-convert errors*")
|
|
|
|
(defconst markdown-ts--convert-output-buffer
|
|
"*markdown-ts-convert output*")
|
|
|
|
(treesit-declare-unavailable-functions)
|
|
|
|
(defvar markdown-ts-converters
|
|
(list
|
|
(cons 'html
|
|
(list
|
|
(cons 'pandoc
|
|
(list :command "pandoc"
|
|
:input '(file stdin)
|
|
:output '(file)
|
|
:arguments-function
|
|
(lambda (input-file output-file)
|
|
(append (list "-f" "gfm"
|
|
"-t" "html5"
|
|
"--standalone"
|
|
"-o" output-file)
|
|
(when input-file
|
|
(list input-file))))))
|
|
(cons 'cmark
|
|
(list :command "cmark"
|
|
:input '(file stdin)
|
|
:output '(stdout)
|
|
:arguments-function
|
|
(lambda (input-file _output-file)
|
|
(append (list "-t" "html")
|
|
(when input-file
|
|
(list input-file))))))
|
|
(cons 'cmark-gfm
|
|
(list :command "cmark-gfm"
|
|
:input '(file stdin)
|
|
:output '(stdout)
|
|
:arguments-function
|
|
(lambda (input-file _output-file)
|
|
(append (list "-t" "html"
|
|
;; Use GitHub-style <pre lang> for code blocks
|
|
"--github-pre-lang"
|
|
;; Include remainder of code block info
|
|
;; string in a separate attribute.
|
|
"--full-info-string"
|
|
;; Use style attributes to align table
|
|
;; cells instead of align attributes.
|
|
"--table-prefer-style-attributes"
|
|
"--extension" "footnotes"
|
|
"--extension" "table"
|
|
"--extension" "strikethrough"
|
|
"--extension" "autolink"
|
|
"--extension" "tagfilter"
|
|
"--extension" "tasklist")
|
|
(when input-file
|
|
(list input-file))))))
|
|
;; 'markdown.pl is the same as 'markdown with another common file
|
|
;; name.
|
|
(cons 'markdown.pl
|
|
(list :command "markdown.pl"
|
|
:input '(file stdin)
|
|
:output '(stdout)
|
|
:arguments-function
|
|
(lambda (input-file _output-file)
|
|
(append (list "--html4tags")
|
|
(when input-file
|
|
(list "--" input-file))))))
|
|
;; 'markdown is the same as 'markdown.pl with another common file
|
|
;; name.
|
|
(cons 'markdown
|
|
(list :command "markdown"
|
|
:input '(file stdin)
|
|
:output '(stdout)
|
|
:arguments-function
|
|
(lambda (input-file _output-file)
|
|
(append (list "--html4tags")
|
|
(when input-file
|
|
(list "--" input-file))))))))
|
|
(cons 'pdf
|
|
(list
|
|
(cons 'pandoc
|
|
(list :command "pandoc"
|
|
:input '(file stdin)
|
|
:output '(file)
|
|
:arguments-function
|
|
(lambda (input-file output-file)
|
|
(append (list "-f" "gfm"
|
|
"-t" "pdf"
|
|
"-o" output-file)
|
|
(when input-file
|
|
(list input-file)))))))))
|
|
"An alist of format/converter configurations.
|
|
It is a two level alist with the first level being format, for example
|
|
`html' or `pdf', and the second being the converter name, for example
|
|
`pandoc'. A converter configuration entry is a plist of the form:
|
|
|
|
\\='(:command \"command name\"
|
|
:input (file stdin) ; accepts file name or stdin
|
|
:output (stdout) ; produces stdout only
|
|
:arguments-function ; function; e.g.,
|
|
(lambda (input-file _output-file)
|
|
(append (list \"-t\" \"html\")
|
|
(when input-file
|
|
(list input-file)))))
|
|
|
|
`arguments-function' is passed two arguments, INPUT-FILE and
|
|
OUTPUT-FILE. If INPUT-FILE is nil, assume stdin, or it is a
|
|
fully-qualified file name. If OUTPUT-FILE is nil, assume stdout, or it
|
|
is a fully-qualified file name. It should return a list of
|
|
arguments suitable for `call-process'.")
|
|
|
|
;;;###autoload
|
|
(defun markdown-ts-convert-file (input-file
|
|
&optional
|
|
format
|
|
output-file
|
|
display overwrite quiet)
|
|
"Convert Markdown INPUT-FILE to FORMAT.
|
|
If optional DISPLAY is non-nil, show the output file in a buffer, if
|
|
possible, using the function `markdown-ts-convert-display-function'.
|
|
|
|
With a prefix argument, DISPLAY will be non-nil.
|
|
|
|
Optional FORMAT is a format/converter pair specified as a cons
|
|
\\='(format . converter) for example \\='(html . pandoc). See
|
|
`markdown-ts-converters'. If nil, prompt for the format and converter.
|
|
|
|
If optional OVERWRITE is non-nil, silently overwrite OUTPUT-FILE if it
|
|
exists.
|
|
|
|
If QUIET is non-nil, inhibit showing conversion warnings or errors.
|
|
|
|
The external executable specified by a converter must be installed and
|
|
found; see the variable `exec-path'."
|
|
(interactive "fConvert Markdown file: ")
|
|
(markdown-ts-convert input-file output-file
|
|
format
|
|
(or display current-prefix-arg)
|
|
overwrite
|
|
quiet))
|
|
|
|
;;;###autoload
|
|
(defun markdown-ts-convert (&optional
|
|
input-file output-file
|
|
format display overwrite quiet)
|
|
"Convert a `markdown-ts-mode' buffer or file to FORMAT.
|
|
Convert INPUT-FILE to OUTPUT-FILE.
|
|
|
|
If INPUT-FILE is nil, use the current buffer.
|
|
|
|
If OUTPUT-FILE is nil, prompt for an output file.
|
|
|
|
Optional FORMAT is a format/converter pair specified as a cons
|
|
\\='(format . converter) for example \\='(html . pandoc). See
|
|
`markdown-ts-converters'. If nil, prompt for the format and converter.
|
|
|
|
If optional DISPLAY is non-nil, show the output file in a buffer, if
|
|
possible, using the function `markdown-ts-convert-display-function'.
|
|
|
|
With a prefix argument, DISPLAY will be non-nil.
|
|
|
|
If optional OVERWRITE is non-nil, silently overwrite OUTPUT-FILE if it
|
|
exists.
|
|
|
|
If QUIET is non-nil, inhibit showing conversion warnings or errors.
|
|
|
|
The external executable specified by a converter must be installed and
|
|
found; see the variable `exec-path'."
|
|
(interactive)
|
|
(when (and input-file
|
|
(equal buffer-file-name (expand-file-name input-file))
|
|
(buffer-modified-p))
|
|
(when (yes-or-no-p "Save modified buffer before converting? ")
|
|
(save-buffer)))
|
|
(unless input-file
|
|
(markdown-ts--barf-if-not-mode 'markdown-ts-convert))
|
|
|
|
(setq display (or display current-prefix-arg))
|
|
|
|
(let* ((converter-format
|
|
(or (car format)
|
|
(car markdown-ts-default-converter)
|
|
(intern
|
|
(completing-read
|
|
"Select an output format: "
|
|
(mapcar (lambda (elt) (symbol-name (car elt)))
|
|
markdown-ts-converters)
|
|
nil
|
|
t
|
|
nil
|
|
'markdown-ts-converter-format))))
|
|
(converter
|
|
(or (cdr format)
|
|
(cdr markdown-ts-default-converter)
|
|
(intern
|
|
(completing-read
|
|
"Select a converter: "
|
|
(mapcar (lambda (elt) (symbol-name (car elt)))
|
|
(alist-get converter-format markdown-ts-converters))
|
|
nil
|
|
t
|
|
nil
|
|
'markdown-ts-converter))))
|
|
(config (alist-get converter
|
|
(alist-get converter-format
|
|
markdown-ts-converters)))
|
|
(command (plist-get config :command))
|
|
(command-input (plist-get config :input))
|
|
(command-output (plist-get config :output))
|
|
(stdin-only (equal command-input '(stdin)))
|
|
(stdout-only (equal command-output '(stdout))))
|
|
(unless output-file
|
|
(setq output-file
|
|
(read-file-name "Output file name: "
|
|
nil
|
|
nil
|
|
nil
|
|
(concat (file-name-sans-extension
|
|
(file-relative-name
|
|
(or input-file buffer-file-name)))
|
|
"."
|
|
(symbol-name converter-format))))
|
|
(unless output-file
|
|
(user-error "Output file name is required")))
|
|
(when output-file
|
|
(when (and (not overwrite)
|
|
(file-exists-p output-file)
|
|
(not (yes-or-no-p (format "%s exists; overwrite?" output-file))))
|
|
(user-error "Not overwriting existing file: %s" output-file))
|
|
(setq output-file (expand-file-name output-file)))
|
|
|
|
|
|
(let ((command-args (funcall (plist-get config :arguments-function)
|
|
(when input-file (expand-file-name input-file))
|
|
output-file)))
|
|
(unless (executable-find command)
|
|
(user-error "`%s' executable not found" command))
|
|
(let ((error-buffer (get-buffer-create
|
|
markdown-ts--convert-error-buffer))
|
|
(error-temp-file (make-temp-file "markdown-ts-convert-stderr"
|
|
nil ".log"))
|
|
(stdout-buffer (when stdout-only
|
|
(get-buffer-create
|
|
markdown-ts--convert-output-buffer))))
|
|
(unwind-protect
|
|
(let (exit-status)
|
|
(with-current-buffer error-buffer
|
|
(special-mode)
|
|
(setq buffer-read-only nil)
|
|
(widen)
|
|
(erase-buffer))
|
|
(when stdout-buffer
|
|
(with-current-buffer stdout-buffer
|
|
(special-mode)
|
|
(setq buffer-read-only nil)
|
|
(widen)
|
|
(erase-buffer)))
|
|
(cond
|
|
((and input-file (not stdin-only))
|
|
(setq exit-status
|
|
(apply #'call-process
|
|
command
|
|
nil
|
|
(list stdout-buffer error-temp-file)
|
|
nil
|
|
command-args)))
|
|
((and input-file stdin-only)
|
|
(with-temp-buffer
|
|
(insert-file-contents input-file)
|
|
(setq exit-status
|
|
(apply #'call-process-region
|
|
nil
|
|
nil
|
|
command
|
|
nil
|
|
(list stdout-buffer error-temp-file)
|
|
nil
|
|
command-args))))
|
|
(t
|
|
(without-restriction
|
|
(setq exit-status
|
|
(apply #'call-process-region
|
|
nil
|
|
nil
|
|
command
|
|
nil
|
|
(list stdout-buffer error-temp-file)
|
|
nil
|
|
command-args)))))
|
|
(let ((error-size (file-attribute-size (file-attributes error-temp-file))))
|
|
(when (and error-size (> error-size 0))
|
|
(with-current-buffer error-buffer
|
|
(insert-file-contents error-temp-file)
|
|
(goto-char (point-min)))))
|
|
(cond ((eq 0 exit-status) ; Integer or string.
|
|
(when stdout-buffer
|
|
(with-current-buffer stdout-buffer
|
|
(write-region (point-min) (point-max) output-file)))
|
|
(when display
|
|
(funcall markdown-ts-convert-display-function
|
|
output-file))
|
|
(when (and (not quiet)
|
|
(not (zerop (buffer-size error-buffer)))
|
|
(yes-or-no-p
|
|
(format-message
|
|
"Conversion file `%s' successfully created with warnings; display them?"
|
|
output-file)))
|
|
(display-buffer error-buffer))
|
|
(message "Conversion file successfully created `%s'%s"
|
|
output-file
|
|
(if (zerop (buffer-size error-buffer))
|
|
""
|
|
(format " with warnings; see buffer `%s'" error-buffer))))
|
|
(t
|
|
(if quiet
|
|
(error
|
|
"Converter aborted with exit code %S; see buffer `%s' for errors"
|
|
exit-status
|
|
markdown-ts--convert-error-buffer)
|
|
(when (yes-or-no-p (format
|
|
"Converter aborted with exit code %S; display errors?"
|
|
exit-status))
|
|
(display-buffer error-buffer))))))
|
|
(delete-file error-temp-file))))))
|
|
|
|
;;; Table of contents:
|
|
|
|
(defgroup markdown-ts-toc nil
|
|
"Table of contents for `markdown-ts-mode' buffers."
|
|
:prefix "markdown-ts-toc-"
|
|
:group 'text
|
|
:group 'editing
|
|
:version "31.1")
|
|
|
|
(defcustom markdown-ts-toc-generate-warn-if-none 'insert-template
|
|
"Warn if no tables were processed when calling `markdown-ts-toc-generate'.
|
|
If nil, do nothing. If the symbol `insert-template' and
|
|
`markdown-ts-toc-generate' was called interactively, offer to insert a
|
|
template if there are none. If `no-tables', warn when there are no
|
|
tables in the buffer. `no-active-tables', is the same as `no-tables'
|
|
with the addition of a warning if there are tables, but none are active."
|
|
:type '(choice (const :tag "No warnings" nil)
|
|
(const :tag "Insert a table template (if interactive)" insert-template)
|
|
(const :tag "Warn if no tables were processed" no-tables)
|
|
(const :tag "Warn if no active tables were processed" no-active-tables))
|
|
:version "31.1"
|
|
:package-version "1.0")
|
|
|
|
(defcustom markdown-ts-toc-update-before-save-mode-lighter " [toc on save]"
|
|
"Minor-mode \"lighter\" for `markdown-ts-toc-update-before-save-mode'.
|
|
Note the single space prefix."
|
|
:type '(choice (const :tag "Default" " [toc on save]")
|
|
(const :tag "None" nil)
|
|
(string :tag "Other string"))
|
|
:version "31.1"
|
|
:package-version "1.0")
|
|
|
|
(defcustom markdown-ts-toc-slug-function #'markdown-ts--slug-github
|
|
"Function to generate Markdown heading \"slugs\" in tables of contents."
|
|
:type '(choice (const :tag "GitHub slug" markdown-ts--slug-github)
|
|
(const :tag "Pandoc slug" markdown-ts--slug-pandoc)
|
|
(function :tag "Other function"))
|
|
:version "31.1"
|
|
:package-version "1.0")
|
|
|
|
(defvar markdown-ts--toc-handles
|
|
'((heading . (h1 h2 h3 h4 h5 h6))
|
|
(setext . (sh1 sh2))
|
|
(item . (minus plus star))
|
|
(numbered-item . (dot paren))
|
|
(code . (code)))
|
|
"Map high level node handles to their base constituents.
|
|
The special symbol `all' means all handles.")
|
|
|
|
(defvar markdown-ts--toc-handle-classes
|
|
(let ((x))
|
|
(dolist (elt markdown-ts--toc-handles)
|
|
(dolist (val (cdr elt))
|
|
(push `(,val . ,(car elt)) x)))
|
|
x)
|
|
"Inverse of `markdown-ts--toc-handles'.")
|
|
|
|
(defun markdown-ts--toc-expand-candidate-handles (handles)
|
|
"Expand candidate HANDLES and keep only base handles.
|
|
See `markdown-ts--toc-handles'."
|
|
(let ((expanded (make-hash-table))
|
|
(res))
|
|
(unless (listp handles)
|
|
(setq handles (list handles)))
|
|
(when (equal handles '(all))
|
|
(setq handles (mapcar #'car markdown-ts--toc-handles)))
|
|
(dolist (elt handles)
|
|
(setq elt (alist-get elt markdown-ts--toc-handles elt))
|
|
(cond ((consp elt)
|
|
(dolist (k elt) (puthash k t expanded)))
|
|
(t (puthash elt t expanded))))
|
|
(maphash (lambda (k _v) (push k res)) expanded)
|
|
res))
|
|
|
|
(defun markdown-ts--tocs (&optional beg end)
|
|
"Scan the current buffer for toc entries from BEG to END.
|
|
If BEG is nil, use `point-min'. If END is nil, use `point-max'.
|
|
|
|
Return an alist of the form:
|
|
\\='((keyword (node beg end vars)))
|
|
|
|
Vars are expanded via `hack-local-variables-prop-line'.
|
|
|
|
Return the list in the order of appearance in the buffer."
|
|
;; NOTE: This scans all matching nodes below BEG with no maximum count
|
|
;; even when we want only a single next matching item. Perhaps
|
|
;; `treesit-query-capture' could be improved to accept a max-count
|
|
;; argument.
|
|
;;
|
|
;; Match "markdown-ts-toc:", "markdown-ts-toc-abc123:" but not
|
|
;; "markdown-ts-tocxxx:".
|
|
(let ((res))
|
|
(dolist (node
|
|
(treesit-query-capture
|
|
'markdown
|
|
'((((html_block) @markdown-ts-toc)
|
|
(:match "<!--[[:blank:]]*markdown-ts-toc\\(-[[:alnum:]]+\\)*:" @markdown-ts-toc)))
|
|
(or beg (point-min))
|
|
(or end (point-max))
|
|
'node-only))
|
|
(let* ((s (treesit-node-text node 'no-property))
|
|
(match (string-match "\\(markdown-ts-toc\\(-[[:alnum:]]+\\)*\\):.*" s))
|
|
(keyword (when match (match-string 1 s))))
|
|
(let ((vars))
|
|
(with-work-buffer
|
|
(insert s)
|
|
(setq vars (hack-local-variables-prop-line)))
|
|
(push
|
|
`(,keyword
|
|
(node . ,node)
|
|
(beg . ,(treesit-node-start node))
|
|
(end . ,(treesit-node-end node))
|
|
(vars . ,vars))
|
|
res))))
|
|
(nreverse res)))
|
|
|
|
(defun markdown-ts--tocs-sanity-check (tocs)
|
|
"Sanity check TOCS for paired markdown-ts-toc: markdown-ts-toc-end: nodes.
|
|
Signal an error if TOCS are insane.
|
|
See `markdown-ts--tocs'."
|
|
(seq-map-indexed
|
|
(lambda (elt idx)
|
|
(when (equal "markdown-ts-toc" (car elt))
|
|
(unless (equal "markdown-ts-toc-end" (car (nth (1+ idx) tocs)))
|
|
(user-error
|
|
"\"markdown-ts-toc\" at position %d must be followed by a matching \"markdown-ts-toc-end\""
|
|
(alist-get 'beg elt)))))
|
|
tocs))
|
|
|
|
(defun markdown-ts--toc-collect-candidates (toc-start-node
|
|
toc-end-node handles from)
|
|
"Return an alist of toc entry candidates from the current buffer.
|
|
HANDLES is a list of base handles; see `markdown-ts--toc-handles'.
|
|
|
|
TOC-START-NODE is the \"markdown-ts-toc-start\" node.
|
|
TOC-END-NODE is the \"markdown-ts-toc-end\" node.
|
|
|
|
If FROM is the symbol `above', source candidates from the beginning of
|
|
the buffer until the toc start node. If `below', source candidates from
|
|
toc start node until the end of the buffer. If `all', use the whole
|
|
buffer.
|
|
|
|
Remove ignored candidates containing \"<!-- markdown-ts-ignore: -->\".
|
|
|
|
Sort the list by the starting character positions of nodes.
|
|
|
|
The alist is of the form:
|
|
\\='((handle . node))"
|
|
(let (candidates beg end)
|
|
(pcase from
|
|
('above
|
|
(setq beg (point-min))
|
|
(setq end (treesit-node-start toc-start-node)))
|
|
('below
|
|
(setq beg (treesit-node-end toc-end-node))
|
|
(setq end (point-max)))
|
|
('all
|
|
(setq beg (point-min))
|
|
(setq end (point-max))))
|
|
|
|
;; It looks like we cannot get them in one fell swoop.
|
|
(dolist (elt handles)
|
|
(setq
|
|
candidates
|
|
(append
|
|
candidates
|
|
(pcase elt
|
|
('h1 (treesit-query-capture
|
|
'markdown '((atx_heading (atx_h1_marker) (inline) @h1)) beg end))
|
|
('h2 (treesit-query-capture
|
|
'markdown '((atx_heading (atx_h2_marker) (inline) @h2)) beg end))
|
|
('h3 (treesit-query-capture
|
|
'markdown '((atx_heading (atx_h3_marker) (inline) @h3)) beg end))
|
|
('h4 (treesit-query-capture
|
|
'markdown '((atx_heading (atx_h4_marker) (inline) @h4)) beg end))
|
|
('h5 (treesit-query-capture
|
|
'markdown '((atx_heading (atx_h5_marker) (inline) @h5)) beg end))
|
|
('h6 (treesit-query-capture
|
|
'markdown '((atx_heading (atx_h6_marker) (inline) @h6)) beg end))
|
|
('sh1 (treesit-query-capture
|
|
'markdown '((setext_heading heading_content: (_) @sh1 (setext_h1_underline))) beg end))
|
|
('sh2 (treesit-query-capture
|
|
'markdown '((setext_heading heading_content: (_) @sh2 (setext_h2_underline))) beg end))
|
|
('minus (treesit-query-capture
|
|
'markdown '((list_item (list_marker_minus) (paragraph (inline) @minus))) beg end))
|
|
('plus (treesit-query-capture
|
|
'markdown '((list_item (list_marker_plus) (paragraph (inline) @plus))) beg end))
|
|
('star (treesit-query-capture
|
|
'markdown '((list_item (list_marker_star) (paragraph (inline) @star))) beg end))
|
|
('dot (treesit-query-capture
|
|
'markdown '((list_item (list_marker_dot) (paragraph (inline) @dot))) beg end))
|
|
('paren (treesit-query-capture
|
|
'markdown '((list_item (list_marker_parenthesis) (paragraph (inline) @paren))) beg end))
|
|
('code (treesit-query-capture
|
|
'markdown '((fenced_code_block (info_string (language)) @code)) beg end))))))
|
|
(sort
|
|
(seq-remove
|
|
(lambda (elt)
|
|
(string-match-p "<!--[[:blank:]]*markdown-ts-ignore:[[:blank:]]*-->"
|
|
(treesit-node-text (cdr elt) t)))
|
|
candidates)
|
|
:key (lambda (elt)
|
|
(treesit-node-start (cdr elt)))
|
|
:lessp #'<
|
|
:in-place t)))
|
|
|
|
(defun markdown-ts--toc-list-item-depth (node)
|
|
"Return NODE's item depth relative to its predecessors.
|
|
If NODE is not part of a list, return nil."
|
|
(let ((depth nil) (n (treesit-node-parent node)))
|
|
(while n
|
|
(when (equal (treesit-node-type n) "list")
|
|
(setq depth (1+ (or depth 0))))
|
|
(setq n (treesit-node-parent n)))
|
|
depth))
|
|
|
|
(defun markdown-ts--toc-atx_header-normalize (text)
|
|
"Remove trailing # from TEXT unless they are backslash escaped.
|
|
First remove HTML comments."
|
|
(if (string-match-p "[[:blank:]]*\\\\+#+[[:blank:]]*\n*\\'" text)
|
|
text
|
|
(replace-regexp-in-string "[[:blank:]]*#+[[:blank:]]*\n*\\'" "" text)))
|
|
|
|
(defvar markdown-ts--toc-text-normalizers
|
|
`((heading . markdown-ts--toc-atx_header-normalize)
|
|
(h1 . markdown-ts--toc-atx_header-normalize)
|
|
(h2 . markdown-ts--toc-atx_header-normalize)
|
|
(h3 . markdown-ts--toc-atx_header-normalize)
|
|
(h4 . markdown-ts--toc-atx_header-normalize)
|
|
(h5 . markdown-ts--toc-atx_header-normalize)
|
|
(h6 . markdown-ts--toc-atx_header-normalize))
|
|
"Map node type handles to functions that normalize its text.
|
|
See `markdown-ts--toc-text-normalize'.
|
|
Each function accepts one argument, the string TEXT, and returns a string.")
|
|
|
|
(defun markdown-ts--toc-text-normalize (handle node &optional text)
|
|
"Normalize NODE's TEXT according to its HANDLE.
|
|
TEXT should have no text properties. If TEXT is nil, use the NODE's
|
|
text. This is not always correct so it's best to pass TEXT if you know
|
|
it in the calling context.
|
|
|
|
If no normalizer is found, return TEXT.
|
|
|
|
One example normalization is atx_heading text that ends in trailing hash
|
|
signs which are intended to be elided so \"Heading Text ##\" is
|
|
transformed into \"Heading Text\"."
|
|
(setq text (or text (treesit-node-text node 'no-property)))
|
|
(if-let* ((fixer (alist-get handle markdown-ts--toc-text-normalizers)))
|
|
(funcall fixer text)
|
|
text))
|
|
|
|
;;;###autoload
|
|
(define-minor-mode markdown-ts-toc-update-before-save-mode
|
|
"If enabled, update `markdown-ts-mode' tables of contents before saving."
|
|
:init-value nil
|
|
:global nil
|
|
:lighter markdown-ts-toc-update-before-save-mode-lighter
|
|
(if (derived-mode-p 'markdown-ts-mode)
|
|
(if markdown-ts-toc-update-before-save-mode
|
|
(add-hook 'before-save-hook #'markdown-ts-toc-generate nil 'local)
|
|
(remove-hook 'before-save-hook #'markdown-ts-toc-generate 'local))
|
|
(setq markdown-ts-toc-update-before-save-mode nil)
|
|
(lwarn 'markdown-ts-toc-update-before-save-mode
|
|
:warning
|
|
"Minor mode valid only in `markdown-ts-mode' buffers.")))
|
|
|
|
;;;###autoload
|
|
(defun markdown-ts-toc-clear-and-remove (&optional beg end)
|
|
"Remove `markdown-ts-mode' table of contents bodies and templates.
|
|
Operate on the active region BEG to END, otherwise operate on the
|
|
buffer, which may be narrowed."
|
|
(interactive "R")
|
|
(markdown-ts--barf-if-not-mode 'markdown-ts-toc-clear-and-remove)
|
|
(markdown-ts-toc-clear beg end 'remove))
|
|
|
|
;;;###autoload
|
|
(defun markdown-ts-toc-clear (&optional beg end remove)
|
|
"Clear `markdown-ts-mode' table of contents bodies.
|
|
Operate on the active region BEG to END, otherwise operate on the
|
|
buffer, which may be narrowed.
|
|
If optional REMOVE is non-nil, remove tables including their templates."
|
|
(interactive "R")
|
|
(markdown-ts--barf-if-not-mode 'markdown-ts-toc-clear)
|
|
(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)
|
|
(beg-pos beg)
|
|
(end-pos end))
|
|
;; We scan each pass because their positions change.
|
|
(while (and (< beg-pos end-pos)
|
|
(setq tocs (markdown-ts--tocs beg-pos end-pos)))
|
|
(markdown-ts--tocs-sanity-check tocs)
|
|
;; After sanity checking, we know the second element is a
|
|
;; toc-end.
|
|
(let* ((toc (nth 0 tocs))
|
|
(toc-end (nth 1 tocs))
|
|
(reg-beg (if remove
|
|
(alist-get 'beg toc)
|
|
(alist-get 'end toc)))
|
|
(reg-end (if remove
|
|
(alist-get 'end toc-end)
|
|
(alist-get 'beg toc-end)))
|
|
(reg-size (- reg-end reg-beg)))
|
|
(delete-region reg-beg reg-end)
|
|
;; Start the next pass after this toc-end. Adjust for the
|
|
;; deleted region.
|
|
(setq beg-pos (- (alist-get 'end toc-end)
|
|
reg-size))
|
|
(setq end-pos (min (point-max) (- end-pos reg-size)))))))))
|
|
|
|
;;;###autoload
|
|
(defun markdown-ts-toc-insert-template (&optional char)
|
|
"Insert a `markdown-ts-mode` table of contents template at point.
|
|
|
|
CHAR is the template type \"b\" for basic, \"c\" for complete. If CHAR
|
|
is nil and the command is run interactively, prompt for a template.
|
|
|
|
The basic template uses all defaults and is likely the best choice for
|
|
most uses. The complete template illustrates all parameters set to
|
|
their defaults and is useful as a starting point to customize a table."
|
|
(interactive
|
|
(list (car (read-multiple-choice
|
|
"Table of contents template"
|
|
'((?b "basic")
|
|
(?c "complete"))))))
|
|
(pcase char
|
|
(?b
|
|
(insert "<!-- markdown-ts-toc: -->\n"
|
|
"<!-- markdown-ts-toc-end: -->\n"))
|
|
(?c
|
|
(insert "<!-- markdown-ts-toc: -*- "
|
|
"min-depth: 1; "
|
|
"max-depth: nil; "
|
|
"candidates: (heading); "
|
|
"from: below; "
|
|
"style: item; "
|
|
"indent: 1; "
|
|
"no-link: nil; "
|
|
"relative-depth: nil; "
|
|
"ignore: nil; "
|
|
"-*- -->\n"
|
|
"<!-- markdown-ts-toc-end: -->\n"))
|
|
(_ (user-error "No such template type: %c" char))))
|
|
|
|
;;;###autoload
|
|
(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)))))))))
|
|
|
|
(provide 'markdown-ts-mode-x)
|
|
;;; markdown-ts-mode-x.el ends here
|