diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index aa0c7dcd6..df9919134 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -165,6 +165,10 @@ Where clue > 0 Added a nullary function that returns a list of files as a possible argument for the scope of the clock table. *** Export +**** Implement vernacular table of contents in Markdown exporter +Global table of contents are generated using vanilla Markdown syntax +instead of HTML. Also #+TOC keyword, including local table of +contents, are now supported. **** Add Slovanian translations **** Implement ~org-export-insert-image-links~ This new function is meant to be used in back-ends supporting images diff --git a/lisp/ox-md.el b/lisp/ox-md.el index c8ea1fa04..e2b3d29e1 100644 --- a/lisp/ox-md.el +++ b/lisp/ox-md.el @@ -248,15 +248,42 @@ a communication channel." "Non-nil when HEADLINE is being referred to. INFO is a plist used as a communication channel. Links and table of contents can refer to headlines." - (or (plist-get info :with-toc) - (org-element-map (plist-get info :parse-tree) 'link - (lambda (link) - (eq headline - (pcase (org-element-property :type link) - ((or "custom-id" "id") (org-export-resolve-id-link link info)) - ("fuzzy" (org-export-resolve-fuzzy-link link info)) - (_ nil)))) - info t))) + (unless (org-element-property :footnote-section-p headline) + (or + ;; Global table of contents includes HEADLINE. + (and (plist-get info :with-toc) + (memq headline + (org-export-collect-headlines info (plist-get info :with-toc)))) + ;; A local table of contents includes HEADLINE. + (cl-some + (lambda (h) + (let ((section (car (org-element-contents h)))) + (and + (eq 'section (org-element-type section)) + (org-element-map section 'keyword + (lambda (keyword) + (when (equal "TOC" (org-element-property :key keyword)) + (let ((case-fold-search t) + (value (org-element-property :value keyword))) + (and (string-match-p "\\" value) + (let ((n (and + (string-match "\\<[0-9]+\\>" value) + (string-to-number (match-string 0 value)))) + (local? (string-match-p "\\" value))) + (memq headline + (org-export-collect-headlines + info n (and local? keyword)))))))) + info t)))) + (org-element-lineage headline)) + ;; A link refers internally to HEADLINE. + (org-element-map (plist-get info :parse-tree) 'link + (lambda (link) + (eq headline + (pcase (org-element-property :type link) + ((or "custom-id" "id") (org-export-resolve-id-link link info)) + ("fuzzy" (org-export-resolve-fuzzy-link link info)) + (_ nil)))) + info t)))) (defun org-md--headline-title (style level title &optional anchor tags) "Generate a headline title in the preferred Markdown headline style. @@ -328,9 +355,19 @@ a communication channel." "Transcode a KEYWORD element into Markdown format. CONTENTS is nil. INFO is a plist used as a communication channel." - (if (member (org-element-property :key keyword) '("MARKDOWN" "MD")) - (org-element-property :value keyword) - (org-export-with-backend 'html keyword contents info))) + (pcase (org-element-property :key keyword) + ((or "MARKDOWN" "MD") (org-element-property :value keyword)) + ("TOC" + (let ((case-fold-search t) + (value (org-element-property :value keyword))) + (cond + ((string-match-p "\\" value) + (let ((depth (and (string-match "\\<[0-9]+\\>" value) + (string-to-number (match-string 0 value)))) + (local? (string-match-p "\\" value))) + (org-remove-indentation + (org-md--build-toc info depth keyword local?))))))) + (_ (org-export-with-backend 'html keyword contents info)))) ;;;; Line Break @@ -512,6 +549,61 @@ a communication channel." ;;;; Template +(defun org-md--build-toc (info &optional n keyword local) + "Return a table of contents. + +INFO is a plist used as a communication channel. + +Optional argument N, when non-nil, is an integer specifying the +depth of the table. + +Optional argument KEYWORD specifies the TOC keyword, if any, from +which the table of contents generation has been initiated. + +When optional argument LOCAL is non-nil, build a table of +contents according to the current headline." + (concat + (unless local + (let ((style (plist-get info :md-headline-style)) + (title (org-html--translate "Table of Contents" info))) + (org-md--headline-title style 1 title nil))) + (mapconcat + (lambda (headline) + (let* ((indentation + (make-string + (* 4 (1- (org-export-get-relative-level headline info))) + ?\s)) + (number (format "%d." + (org-last + (org-export-get-headline-number headline info)))) + (bullet (concat number (make-string (- 4 (length number)) ?\s))) + (title + (format "[%s](#%s)" + (org-export-data-with-backend + (org-export-get-alt-title headline info) + ;; Create an anonymous back-end that will + ;; ignore any footnote-reference, link, + ;; radio-target and target in table of + ;; contents. + (org-export-create-backend + :parent 'md + :transcoders '((footnote-reference . ignore) + (link . (lambda (object c i) c)) + (radio-target . (lambda (object c i) c)) + (target . ignore))) + info) + (or (org-element-property :CUSTOM_ID headline) + (org-export-get-reference headline info)))) + (tags (and (plist-get info :with-tags) + (not (eq 'not-in-toc (plist-get info :with-tags))) + (let ((tags (org-export-get-tags headline info))) + (and tags + (format ":%s:" + (mapconcat #'identity tags ":"))))))) + (concat indentation bullet title tags))) + (org-export-collect-headlines info n (and local keyword)) "\n") + "\n")) + (defun org-md--footnote-formatted (footnote info) "Formats a single footnote entry FOOTNOTE. FOOTNOTE is a cons cell of the form (number . definition). @@ -548,7 +640,8 @@ holding export options." (concat ;; Table of contents. (let ((depth (plist-get info :with-toc))) - (when depth (org-html-toc depth info))) + (when depth + (concat (org-md--build-toc info (and (wholenump depth) depth)) "\n"))) ;; Document contents. contents "\n"