1
Fork 0
mirror of git://git.sv.gnu.org/emacs.git synced 2026-05-30 17:22:17 -07:00

Add read-only 'markdown-ts-view-mode' (bug#81023)

This new derived mode is intended for consumers that render
Markdown content for display rather than editing, such as Eglot
and Eldoc when showing documentation popups and buffers.  It
pre-sets the relevant customizations (markup hidden, inline
images on, hard-line-break markup hidden, native code-block
fontification, table and code-block context minor modes off),
makes the buffer read-only, and uses its own keymap derived from
'special-mode-map' so navigation keys behave like a viewer.

A pre-init hook lets callers normalize buffer content before the
grammar parses it; 'markdown-ts-add-final-newline' is the
default so that markup depending on a terminating newline parses
correctly.  'markdown-ts-buffer-string' returns the rendered
buffer string with overlay faces flattened into text properties,
which is useful for callers that capture the rendered output.

Along the way: 'list_marker_parenthesis' is now recognized as an
ordered list marker; the strikethrough query is simplified to a
single rule; thematic breaks span the window via an ':extend'
underline when the face supports it.

* lisp/textmodes/markdown-ts-mode.el (markdown-ts-view-mode):
New read-only derived mode.
(markdown-ts-view-mode-map): New keymap.
(markdown-ts-view-mode-pre-init-hook): New hook, defaulting to
'markdown-ts-add-final-newline'.
(markdown-ts-mode--initialize): New helper, factored out of
'markdown-ts-mode' so 'markdown-ts-view-mode' can reuse the
parser readiness and setup logic after overriding local
variables.
(markdown-ts-mode): Call 'markdown-ts-mode--initialize'.
(markdown-ts-add-final-newline): New function.
(markdown-ts-buffer-string): New function.
(markdown-ts-unordered-list-marker): New defcustom.
(markdown-ts-hard-line-break-backslash)
(markdown-ts-hard-line-break-space): Accept the symbol 'hide.
(markdown-ts--fontify-hard-line-break): Honor 'hide.
(markdown-ts--fontify-atx-heading)
(markdown-ts--fontify-setext-heading)
(markdown-ts--fontify-atx-delimiter)
(markdown-ts--fontify-unordered-list-marker)
(markdown-ts--list-item-depth): New functions supporting clean
rendering when markup is hidden.
(markdown-ts--fontify-thematic-break): Use ':extend' underline
span when the face supports it.
(markdown-ts--resolve-display-value): Accept non-cons values.
(markdown-ts--list-ordered-item-p): Also recognize
'list_marker_parenthesis'.
(markdown-ts--range-settings): Mark markdown-inline embed as
':local t'.
(markdown-ts--set-up): Create the markdown-inline parser only in
the inline setup branch; drop the redundant
'markdown-ts-hide-markup' make-local-variable.
(markdown-ts--treesit-settings): Route atx and setext headings
to their dedicated fontifiers; route unordered list markers
through 'markdown-ts--fontify-unordered-list-marker'; move
'strikethrough' to the simpler paragraph-inline query.
This commit is contained in:
Rahul Martim Juliato 2026-05-12 00:03:16 -03:00 committed by João Távora
parent b39c123490
commit 286833e401

View file

@ -315,6 +315,28 @@ Remote images are skipped by default for security."
:version "31.1"
:package-version "1.0")
(defcustom markdown-ts-unordered-list-marker '(("" . "- ")
("" . "- ")
("" . "- ")
("" . "- "))
"If markup is hidden, display these for an unordered list marker.
Each list item marker's depth in its list controls its selected string
starting at the first element and cycling through the others for deeper
items. The list will be cycle around back to the beginning if there are
insufficient strings to represent deep levels.
Note that the default strings have trailing spaces.
Value forms:
- (list (cons (PREFERRED . FALLBACK)) ...): where PREFERRED is used if
its first character passes `char-displayable-p', otherwise FALLBACK.
- nil: display the raw markup."
:type '(choice (repeat (cons (string :tag "Preferred (GUI)")
(string :tag "Fallback (TTY)")))
(const :tag "Display original markup" nil))
:version "31.1"
:package-version "1.0")
(defcustom markdown-ts-checked-checkbox '("" . "+")
"If markup is hidden, display this for a checked task list marker.
Value forms:
@ -367,10 +389,14 @@ Consulted only when `markdown-ts-unchecked-checkbox' is the symbol
(defcustom markdown-ts-thematic-break-character '(?─ . ?-)
"If markup is hidden, display this character for thematic breaks.
It is repeated to fill the window width.
It is repeated to fill the window width. This assumes a static window
width.
You may prefer an `:extend' attribute on the
`markdown-ts-thematic-break' which will span window width dynamically
using an underline, in which case this character is ignored.
The value is a cons (PREFERRED . FALLBACK): PREFERRED is used if it passes
`char-displayable-p', otherwise FALLBACK is used.
nil displays the raw markup."
Use nil to display the raw markup."
:type '(choice (cons (character :tag "Preferred (GUI)")
(character :tag "Fallback (TTY)"))
(const :tag "Display original markup" nil))
@ -384,7 +410,8 @@ The value is a cons (PREFERRED . FALLBACK): PREFERRED is used if it passes
nil keeps the raw markup."
:type '(choice (cons (character :tag "Preferred (GUI)")
(character :tag "Fallback (TTY)"))
(const :tag "Display original markup" nil))
(const :tag "Display original markup" nil)
(const :tag "Hide markup" hide))
:version "31.1"
:package-version "1.0")
@ -404,7 +431,8 @@ The value can be:
:type '(choice (character :tag "Display specified character (no repetition)")
(string :tag "Display specified string (no repetition)")
(function :tag "Function from count to display string")
(const :tag "Display original markup" nil))
(const :tag "Display original markup" nil)
(const :tag "Hide markup" hide))
:version "31.1"
:package-version "1.0")
@ -536,6 +564,18 @@ Set to nil to disable the lighter."
:version "31.1"
:package-version "1.0")
(defcustom markdown-ts-view-mode-pre-init-hook (list #'markdown-ts-add-final-newline)
"Hooks run before `markdown-ts-view-mode` initialization.
Functions on this list are intended to amend buffer content for
`markdown-ts-view-mode' and tree-sitter Markdown grammar compatibility.
For example, `markdown-ts-add-final-newline' ensures the grammar
correctly parses markup at the end of the buffer that depends on a final
newline."
:type '(hook)
:version "31.1"
:package-version "1.0")
;;; Faces:
(defgroup markdown-ts-faces nil
@ -763,7 +803,6 @@ shadow-colored block."
(ts typescript-ts-mode)
(yml yaml-ts-mode))
"Extra mappings from code block language tags to major modes.
Entries here are only needed when the language tag in a fenced code
block does NOT match the conventional mode name derivation, e.g. the
user writes \\=`\\=`\\=`ts instead of \\=`\\=`\\=`typescript, or
@ -796,7 +835,6 @@ conventional font-lock. `markdown-ts-mode' itself is one of them.")
(defun markdown-ts--fontify-delimiter (node override start end &rest _)
"Fontify delimiter NODE and optionally hide its markup.
NODE is the tree-sitter node representing the delimiter.
OVERRIDE, START, and END are passed through to
`treesit-fontify-with-override'."
@ -807,6 +845,26 @@ OVERRIDE, START, and END are passed through to
(put-text-property (treesit-node-start node) (treesit-node-end node)
'invisible 'markdown-ts--markup)))
(defun markdown-ts--fontify-atx-delimiter (node override start end &rest _)
"Fontify atx_heading delimiter NODE and optionally hide its markup.
NODE is the tree-sitter node representing the delimiter.
Leading whitespace between the delimiter and the heading text is hidden
along with the delimiter when hiding markup.
OVERRIDE, START, and END are passed through to
`treesit-fontify-with-override'."
(treesit-fontify-with-override
(treesit-node-start node) (treesit-node-end node)
'markdown-ts-delimiter override start end)
(when markdown-ts-hide-markup
(put-text-property (treesit-node-start node)
(save-excursion
(goto-char (treesit-node-end node))
(re-search-forward "[^[:blank:]]" (pos-eol) 'no-error)
(if (eq (point) (pos-eol))
(point)
(1- (point))))
'invisible 'markdown-ts--markup)))
(defvar url-mail-command) ; url/url-vars.el
(defun markdown-ts--make-link-button (beg end url)
@ -1021,57 +1079,89 @@ Pushes the mark before moving so `C-u C-SPC' returns. Signals
(recenter))
(user-error "No heading for fragment: #%s" id)))
(defun markdown-ts--fontify-heading (node _override _start _end &rest _)
"Apply the heading face across NODE.
(defun markdown-ts--fontify-atx-heading (node _override _start _end &rest _)
"Apply the heading face across an atx_heading NODE.
Layer the face on top of child sub-nodes (e.g. an inline link) so
their own faces are preserved. Strip any prior copy of the face
first so it does not accumulate when the heading is refontified or
its level/type changes during editing.
For ATX headings, also fontify any optional trailing closing-`#'
sequence as a delimiter. The tree-sitter grammar does not produce a
separate node for these; per CommonMark they are decorative and
must be preceded by a space or tab."
(let* ((type (treesit-node-type node))
(n-start (treesit-node-start node))
Do not fontify the header's trailing newline.
Elide trailing whitespace when hiding markup.
Fontify any optional trailing closing-`#' sequence as a delimiter. The
tree-sitter grammar does not produce a separate node for these; per
CommonMark they are decorative and must be preceded by a space or tab."
(let* ((n-start (treesit-node-start node))
(n-end (treesit-node-end node))
(face (cond
((equal type "setext_heading")
'markdown-ts-setext-heading)
(t
(let ((marker (treesit-node-child node 0)))
(intern (format "markdown-ts-heading-%d"
(length (treesit-node-text marker t)))))))))
(face (let ((marker (treesit-node-child node 0)))
(intern (format "markdown-ts-heading-%d"
(length (treesit-node-text marker t)))))))
(font-lock--remove-face-from-text-property n-start n-end 'face face)
(font-lock-append-text-property n-start n-end 'face face)
(when (string-prefix-p "atx_" type)
(save-excursion
(goto-char n-end)
(skip-chars-backward " \t\n" n-start)
(let ((line-end (point)))
(skip-chars-backward " \t" n-start)
(let ((trailing-end (point)))
(skip-chars-backward "#" n-start)
(let ((trailing-start (point)))
(when (and (< trailing-start trailing-end)
(> trailing-start n-start)
(memq (char-before trailing-start) '(?\s ?\t)))
(font-lock--remove-face-from-text-property
trailing-start trailing-end
'face 'markdown-ts-delimiter)
(font-lock-prepend-text-property
trailing-start trailing-end
'face 'markdown-ts-delimiter)
(when markdown-ts-hide-markup
;; Also hide the space(s) preceding the closer and any
;; trailing whitespace, so the heading looks clean.
(let ((hide-start (save-excursion
(goto-char trailing-start)
(skip-chars-backward " \t" n-start)
(point))))
(put-text-property hide-start line-end
'invisible
'markdown-ts--markup)))))))))))
(font-lock-append-text-property n-start (1- n-end) 'face face)
(save-excursion
(goto-char n-end)
(skip-chars-backward "[:space:]" n-start)
(let ((trailing-end (point)))
(skip-chars-backward "#" n-start)
(let ((trailing-start (point)))
(cond ((and (< trailing-start trailing-end)
(> trailing-start n-start)
(memq (char-before trailing-start) '(?\s ?\t)))
;; Identify the optional trailing closing-# sequence,
;; fontify it as a delimiter, and remove whitespace
;; between the heading text and the delimiter. The
;; grammar omits a node for this run despite CommonMark.
(font-lock--remove-face-from-text-property
trailing-start trailing-end
'face 'markdown-ts-delimiter)
(font-lock-prepend-text-property
trailing-start trailing-end
'face 'markdown-ts-delimiter)
(when markdown-ts-hide-markup
(let ((hide-start (save-excursion
(goto-char trailing-start)
(skip-chars-backward "[:space:]" n-start)
(point))))
(put-text-property hide-start (pos-eol)
'invisible 'markdown-ts--markup))))
(markdown-ts-hide-markup
;; Hide trailing whitespace in the nominal case.
(put-text-property trailing-end (pos-eol)
'invisible 'markdown-ts--markup))))))))
(defun markdown-ts--fontify-setext-heading (node _override _start _end &rest _)
"Apply the heading face across a setext NODE.
Layer the face on top of child sub-nodes (e.g. an inline link) so
their own faces are preserved. Strip any prior copy of the face
first so it does not accumulate when the heading is refontified or
its level/type changes during editing.
Apply the face to the setext heading_content separately from the
underline rather than treat them as a single range. This avoids putting
the face on the heading_content newline. If `markdown-ts-hide-markup'
is non-nil, hide the underline line entirely by setting its line-height
text property to 0.
Elide trailing whitespace when hiding markup."
(let* ((n-start (treesit-node-start node))
(n-end (treesit-node-end node))
(content (treesit-node-child node 0 'named))
(content-start (treesit-node-start content))
(content-end (treesit-node-end content))
(underline (treesit-node-child node 1 'named))
(underline-start (treesit-node-start underline))
(underline-end (treesit-node-end underline))
(face 'markdown-ts-setext-heading))
(font-lock--remove-face-from-text-property n-start n-end 'face face)
;; 1- content-end avoids the newline so it hides correctly.
(font-lock-append-text-property content-start (1- content-end) 'face face)
(font-lock-append-text-property underline-start underline-end 'face face)
(when markdown-ts-hide-markup
;; Hide heading_content trailing spaces.
(put-text-property (save-excursion
(goto-char content-end)
(skip-chars-backward "[:space:]" content-start)
(point))
content-end
'invisible 'markdown-ts--markup)
(put-text-property underline-start underline-end 'line-height 0))))
(defun markdown-ts--fontify-link-node (node override start end &rest _)
"Fontify link or image text NODE as a clickable button.
@ -1261,12 +1351,51 @@ OVERRIDE, START, and END are passed through to
(defun markdown-ts--resolve-display-value (val)
"Resolve VAL, a cons (PREFERRED . FALLBACK), to a displayable value.
Return PREFERRED if its first character passes `char-displayable-p',
otherwise return FALLBACK. Return nil if VAL is nil."
(when val
(let* ((preferred (car val))
(ch (if (characterp preferred) preferred (aref preferred 0))))
(if (char-displayable-p ch) (car val) (cdr val)))))
PREFERRED and FALLBACK can be a character or a string. Return PREFERRED
if it, or its first character, is `char-displayable-p', otherwise return
FALLBACK.
If VAL is not a cons or is nil, return VAL."
(if (consp val)
(let* ((preferred (car val))
(ch (if (characterp preferred)
preferred
(aref preferred 0))))
(if (char-displayable-p ch)
(car val)
(cdr val)))
val))
(defun markdown-ts--list-item-depth (node)
"Compute the depth of list NODE relative to its parents.
NODE can be a list, list_item, or one of the list_marker_'s.
If NODE is not in a list, return -1."
(let ((depth -1))
(while (and node
(not (equal (treesit-node-type node) "section")))
(when (equal (treesit-node-type node) "list")
(setq depth (1+ depth)))
(setq node (treesit-node-parent node)))
depth))
(defun markdown-ts--fontify-unordered-list-marker (node override start end &rest _)
"Fontify unordered list marker NODE, show a symbol when markup is hidden.
OVERRIDE, START, and END are passed through to
`treesit-fontify-with-override'."
(let* ((node-start (treesit-node-start node))
(node-end (treesit-node-end node))
(face 'markdown-ts-list-marker))
(treesit-fontify-with-override node-start node-end face
override start end)
(cond (markdown-ts-hide-markup
(let* ((depth (markdown-ts--list-item-depth node))
(value (if markdown-ts-unordered-list-marker
(nth (mod depth (length markdown-ts-unordered-list-marker))
markdown-ts-unordered-list-marker)
nil))
(display-spec (markdown-ts--resolve-display-value value)))
(put-text-property node-start node-end 'display display-spec)))
(t
(remove-text-properties node-start node-end '(display nil))))))
(defun markdown-ts--fontify-checkbox (node override start end &rest _)
"Fontify task list checkbox NODE, show a Unicode symbol when markup is hidden.
@ -1310,6 +1439,9 @@ A backslash break gets `markdown-ts-hard-line-break-backslash' (or its
backslash break is replaced by a single `markdown-ts-hard-line-break'
glyph; a trailing-spaces break replaces each space with the glyph, so the run
of pilcrows fills the line up to the newline.
If `markdown-ts-hard-line-break-backslash' or
`markdown-ts-hard-line-break-space' are the symbol `hide', hide the
markup entirely.
OVERRIDE, START, and END are passed through to
`treesit-fontify-with-override'."
(let* ((node-start (treesit-node-start node))
@ -1363,21 +1495,24 @@ OVERRIDE, START, and END are passed through to
((stringp spec) spec)
((functionp spec)
(funcall spec (- region-end region-start))))))
(when (and (stringp str)
(> (length str) 0)
(char-displayable-p (aref str 0)))
(put-text-property region-start (1+ region-start)
'display str)
;; For the trailing-spaces variant, hide the remaining
;; spaces in the run so the line doesn't end with leftover
;; whitespace after the substituted glyph. Each position
;; gets its own empty-string `display' so cursor placement
;; stays unambiguous.
(unless backslash
(let ((i (1+ region-start)))
(while (< i region-end)
(put-text-property i (1+ i) 'display "")
(setq i (1+ i))))))))))
(if (eq spec 'hide)
(put-text-property region-start region-end
'invisible 'markdown-ts--markup)
(when (and (stringp str)
(> (length str) 0)
(char-displayable-p (aref str 0)))
(put-text-property region-start (1+ region-start)
'display str)
;; For the trailing-spaces variant, hide the remaining
;; spaces in the run so the line doesn't end with leftover
;; whitespace after the substituted glyph. Each position
;; gets its own empty-string `display' so cursor placement
;; stays unambiguous.
(unless backslash
(let ((i (1+ region-start)))
(while (< i region-end)
(put-text-property i (1+ i) 'display "")
(setq i (1+ i)))))))))))
(defun markdown-ts--fontify-thematic-break (node override start end &rest _)
"Fontify thematic break NODE and show a line when markup is hidden.
@ -1388,22 +1523,27 @@ OVERRIDE, START, and END are passed through to
(treesit-fontify-with-override node-start node-end
'markdown-ts-thematic-break
override start end)
(let ((char (markdown-ts--resolve-display-value
markdown-ts-thematic-break-character)))
(if (and markdown-ts-hide-markup char (char-displayable-p char))
(let* ((col (save-excursion (goto-char node-start)
(current-column)))
;; Span if the face has non-nil :extend.
(span-length (if (face-attribute 'markdown-ts-thematic-break
:extend nil 'default)
(- (window-body-width) col)
12)))
(put-text-property node-start node-end
'display
(concat
(make-string span-length char)
"\n")))
(remove-text-properties node-start node-end '(display nil))))))
(if markdown-ts-hide-markup
(cond
((and (display-supports-face-attributes-p '(:extend t))
(face-attribute 'markdown-ts-thematic-break
:extend nil 'default))
(put-text-property node-start node-end
'display
(propertize "\n" 'face '(:extend t :underline t))))
(t
(when-let* ((char (markdown-ts--resolve-display-value
markdown-ts-thematic-break-character))
(_ (char-displayable-p char)))
(let* ((col (save-excursion (goto-char node-start)
(current-column)))
(span-length (max 12 (- (window-body-width) col))))
(put-text-property node-start node-end
'display
(concat
(make-string span-length char)
"\n"))))))
(remove-text-properties node-start node-end '(display nil)))))
(defun markdown-ts--fontify-code-block (node _override _start _end &rest _)
"Fontify code block content NODE with a background overlay.
@ -1637,18 +1777,18 @@ Skip matches already inside tree-sitter link or autolink nodes."
:language 'markdown
:feature 'heading
'(((atx_heading) @markdown-ts--fontify-heading)
((setext_heading) @markdown-ts--fontify-heading))
'(((atx_heading) @markdown-ts--fontify-atx-heading)
((setext_heading) @markdown-ts--fontify-setext-heading))
:language 'markdown
:feature 'heading
:override 'prepend
'((atx_h1_marker) @markdown-ts--fontify-delimiter
(atx_h2_marker) @markdown-ts--fontify-delimiter
(atx_h3_marker) @markdown-ts--fontify-delimiter
(atx_h4_marker) @markdown-ts--fontify-delimiter
(atx_h5_marker) @markdown-ts--fontify-delimiter
(atx_h6_marker) @markdown-ts--fontify-delimiter
'((atx_h1_marker) @markdown-ts--fontify-atx-delimiter
(atx_h2_marker) @markdown-ts--fontify-atx-delimiter
(atx_h3_marker) @markdown-ts--fontify-atx-delimiter
(atx_h4_marker) @markdown-ts--fontify-atx-delimiter
(atx_h5_marker) @markdown-ts--fontify-atx-delimiter
(atx_h6_marker) @markdown-ts--fontify-atx-delimiter
(setext_h1_underline) @markdown-ts--fontify-delimiter
(setext_h2_underline) @markdown-ts--fontify-delimiter)
@ -1657,9 +1797,9 @@ Skip matches already inside tree-sitter link or autolink nodes."
'(((thematic_break) @markdown-ts--fontify-thematic-break)
((html_block) @markdown-ts-html-block)
((indented_code_block) @markdown-ts-indented-code-block)
(list_item (list_marker_star) @markdown-ts-list-marker)
(list_item (list_marker_plus) @markdown-ts-list-marker)
(list_item (list_marker_minus) @markdown-ts-list-marker)
(list_item (list_marker_star) @markdown-ts--fontify-unordered-list-marker)
(list_item (list_marker_plus) @markdown-ts--fontify-unordered-list-marker)
(list_item (list_marker_minus) @markdown-ts--fontify-unordered-list-marker)
(list_item (list_marker_dot) @markdown-ts-list-marker)
(list_item (list_marker_parenthesis) @markdown-ts-list-marker)
(list_item (task_list_marker_unchecked) @markdown-ts--fontify-checkbox)
@ -1714,33 +1854,13 @@ Skip matches already inside tree-sitter link or autolink nodes."
'(((code_span) @markdown-ts-code-span)
((code_span_delimiter) @markdown-ts--fontify-delimiter))
:language 'markdown-inline
:override 'append
:feature 'paragraph-inline
;; Order matters: most specific to least specific.
'(;; ~ x ~ (strikethrough (emphasis_delimiter) ) : ( (emphasis_delimiter))
;; ^^^
;; inline text needs to be ignored
((strikethrough ((emphasis_delimiter)
:anchor
_ @foo
(emphasis_delimiter))
(:match "\\`[~[:print:]]\\'" @foo)
)
@default)
;; ~~x~~ (strikethrough (emphasis_delimiter) (strikethrough (emphasis_delimiter) (emphasis_delimiter)) (emphasis_delimiter))
((strikethrough (emphasis_delimiter) (strikethrough (emphasis_delimiter) (emphasis_delimiter)) (emphasis_delimiter))
@markdown-ts-strikethrough)
;; ~x~ (strikethrough (emphasis_delimiter) (emphasis_delimiter))
((strikethrough (emphasis_delimiter) (emphasis_delimiter))
@markdown-ts-strikethrough))
:language 'markdown-inline
:override 'append
:feature 'paragraph-inline
'(((link_destination) @markdown-ts--fontify-link-destination)
((emphasis) @markdown-ts-emphasis)
((strong_emphasis) @markdown-ts-bold)
((strikethrough) @markdown-ts-strikethrough)
(inline_link (link_text) @markdown-ts--fontify-link-node)
(full_reference_link (link_text) @markdown-ts--fontify-link-node)
(full_reference_link (link_label) @markdown-ts--fontify-link-node)
@ -2073,7 +2193,8 @@ indentation, which tree-sitter may include in the node."
(defun markdown-ts--list-ordered-item-p (item)
"Return non-nil if ITEM is an ordered (numbered) list item."
(let ((marker (treesit-node-child item 0)))
(equal (treesit-node-type marker) "list_marker_dot")))
(member (treesit-node-type marker)
'("list_marker_dot" "list_marker_parenthesis"))))
(defun markdown-ts--list-promote-or-demote (demote)
"Change nesting of the list item at point.
@ -4631,15 +4752,16 @@ If point is not at a table, do nothing."
"Return range settings for `markdown-ts-mode'."
(apply
#'treesit-range-rules
`(:embed markdown-inline
:host markdown
`( :embed markdown-inline
:host markdown
:local t
((inline) @markdown-inline)
,@(when markdown-ts-fontify-code-blocks-natively
'(:embed markdown-ts--code-block-ts-language
:host markdown
:local t
((fenced_code_block (info_string (language) @language)
(code_fence_content) @content)))))))
'( :embed markdown-ts--code-block-ts-language
:host markdown
:local t
((fenced_code_block (info_string (language) @language)
(code_fence_content) @content)))))))
(defun markdown-ts--remove-image-overlays ()
"Remove all inline image overlays from the current buffer."
@ -4924,6 +5046,27 @@ On a heading, call `outline-cycle'. Otherwise do nothing."
"M-RET" #'markdown-ts-insert-list-item
"TAB" #'markdown-ts-outline-cycle)
(defvar-keymap markdown-ts-view-mode-map
:doc "Keymap for `markdown-ts-view-mode'."
:parent special-mode-map
:menu nil
"g" #'ignore ; Override special-mode-map #'revert-buffer
"C-c C-n" #'outline-next-heading
"n" #'outline-next-heading
"C-c C-p" #'outline-previous-heading
"p" #'outline-previous-heading
"C-c C-u" #'outline-up-heading
"u" #'outline-up-heading
"C-c C-f" #'outline-forward-same-level
"f" #'outline-forward-same-level
"C-c C-b" #'outline-backward-same-level
"b" #'outline-backward-same-level
"C-c C-x C-m" #'markdown-ts-toggle-hide-markup
"C-c C-x C-v" #'markdown-ts-toggle-inline-images
"C-c C-v n" #'markdown-ts-move-to-next-code-block
"C-c C-v p" #'markdown-ts-move-to-previous-code-block
"TAB" #'markdown-ts-outline-cycle)
(defvar-keymap markdown-ts-code-block-in-context-mode-map
:doc "Keymap for `markdown-ts-code-block-in-context-mode'.
These override keys in `markdown-ts-mode-map' to support executing their
@ -5047,7 +5190,6 @@ NOTE: Call this function only when the treesit `markdown' and
(setq-local adaptive-fill-function #'markdown-ts--adaptive-fill)
;; Create and configure the parsers.
(treesit-parser-create 'markdown-inline)
(setq treesit-primary-parser
(treesit-parser-create 'markdown))
@ -5059,11 +5201,12 @@ NOTE: Call this function only when the treesit `markdown' and
(image-preview error)))
(cond (markdown-ts--set-up-inline
(setq-local treesit-range-settings
(treesit-range-rules
:embed 'markdown-inline
:host 'markdown
'((inline) @markdown-inline))))
(treesit-parser-create 'markdown-inline)
(setq-local treesit-range-settings
(treesit-range-rules
:embed 'markdown-inline
:host 'markdown
'((inline) @markdown-inline))))
(t
;; Range settings differ in the master buffer vs. inline above.
(setq-local treesit-range-settings (markdown-ts--range-settings))
@ -5092,7 +5235,6 @@ NOTE: Call this function only when the treesit `markdown' and
#'markdown-ts--outline-view-change nil t))
(progn
(make-local-variable 'markdown-ts-hide-markup)
(make-local-variable 'font-lock-extra-managed-props)
(dolist (prop '(invisible display button category action help-echo))
(add-to-list 'font-lock-extra-managed-props prop)))
@ -5209,10 +5351,8 @@ With a prefix argument, ARG, if needed, install parsers for `html',
(require 'toml-ts-mode)
(treesit-install-language-grammar 'toml))))
;;;###autoload
(define-derived-mode markdown-ts-mode text-mode "Markdown"
"Major mode for editing Markdown using tree-sitter grammar.
NOTE: See `markdown-ts--set-up-inline'."
(defun markdown-ts-mode--initialize ()
"Invoke this from major mode definitions after local variable set up."
(treesit-ensure-installed 'markdown)
(treesit-ensure-installed 'markdown-inline)
;; Bypass `treesit-max-buffer-size' so the mode activates in large
@ -5221,16 +5361,57 @@ NOTE: See `markdown-ts--set-up-inline'."
;; they are installed. Revisit if `treesit-parser-create' gains its
;; own buffer-size guard (see bug#80909).
(let ((treesit-max-buffer-size most-positive-fixnum))
(if (treesit-ready-p '(markdown markdown-inline) t)
(markdown-ts--set-up)
(warn "markdown-ts-mode cannot be set up; using fundamental-mode.
(cond ((treesit-ready-p '(markdown markdown-inline) t)
(markdown-ts--set-up))
(t
(warn "markdown-ts-mode cannot be set up; using fundamental-mode.
The tree-sitter parsers `markdown' and `markdown-inline' were not found.
Use the command `markdown-ts-mode-install-parsers' to install them.
With a prefix argument, it can also install optional parsers.")
(fundamental-mode))))
(fundamental-mode)))))
;;;###autoload
(define-derived-mode markdown-ts-mode text-mode "Markdown"
"Major mode for editing Markdown using tree-sitter grammar.
NOTE: See `markdown-ts--set-up-inline'."
(markdown-ts-mode--initialize))
(derived-mode-add-parents 'markdown-ts-mode '(markdown-mode))
;;; View mode:
;;;###autoload
(define-derived-mode markdown-ts-view-mode
nil ; Intentionally left blank.
"Markdown View"
"Major mode for read-only viewing Markdown using tree-sitter grammar."
;; NOTE: `markdown-ts-mode' is manually added as a parent to avoid
;; invoking its initialization before we set override variables.
(setq-local markdown-ts-menu-bar-show nil)
(setq-local markdown-ts-hide-markup t)
(setq-local markdown-ts-inline-images t)
(setq-local markdown-ts-hard-line-break-backslash 'hide)
(setq-local markdown-ts-hard-line-break-space 'hide)
(setq-local markdown-ts-fontify-code-blocks-natively t)
(setq-local markdown-ts-enable-code-block-context-mode nil)
(setq-local markdown-ts-enable-table-mode nil)
(run-hooks 'markdown-ts-view-mode-pre-init-hook)
(markdown-ts-mode--initialize)
(setq buffer-read-only t))
(derived-mode-add-parents 'markdown-ts-view-mode '(markdown-ts-mode special-mode))
;;; Mode utilities:
;;;###autoload
(defun markdown-ts-buffer-string ()
"Like `buffer-string', and convert overlay properties to text properties."
(let ((str (buffer-string)))
(dolist (ov (overlays-in (point-min) (point-max)) str)
(when-let* ((face (overlay-get ov 'face)))
(font-lock-append-text-property
(overlay-start ov) (overlay-end ov) 'face face str)))))
(defun markdown-ts--barf-if-not-mode (&optional context)
"Signal an error if the current buffer is not a `markdown-ts-mode' buffer.
Prefix the error message with CONTEXT."
@ -5238,6 +5419,18 @@ Prefix the error message with CONTEXT."
(user-error "%sis valid only in `markdown-ts-mode' buffers"
(if context (format "%s: " context) ""))))
(defun markdown-ts-add-final-newline ()
"Add a final newline to the current buffer, if necessary."
;; Inspired by files.el.
(let ((inhibit-read-only t))
(when (or (eq (buffer-size) 0)
(and (/= (char-after (1- (point-max))) ?\n)
(not (and (eq selective-display t)
(= (char-after (1- (point-max))) ?\r)))))
(save-excursion
(goto-char (point-max))
(insert ?\n)))))
(define-minor-mode markdown-ts-code-block-in-context-mode
"Minor mode enabled if point is within a fenced code block.
This enables the keymap `markdown-ts-code-block-in-context-mode-map'."