summaryrefslogtreecommitdiff
path: root/lisp/org/ox-md.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/org/ox-md.el')
-rw-r--r--lisp/org/ox-md.el364
1 files changed, 248 insertions, 116 deletions
diff --git a/lisp/org/ox-md.el b/lisp/org/ox-md.el
index ab73f29dfa9..c8ea1fa045e 100644
--- a/lisp/org/ox-md.el
+++ b/lisp/org/ox-md.el
@@ -1,4 +1,4 @@
-;;; ox-md.el --- Markdown Back-End for Org Export Engine
+;;; ox-md.el --- Markdown Back-End for Org Export Engine -*- lexical-binding: t; -*-
;; Copyright (C) 2012-2017 Free Software Foundation, Inc.
@@ -28,9 +28,9 @@
;;; Code:
-(eval-when-compile (require 'cl))
+(require 'cl-lib)
(require 'ox-html)
-
+(require 'ox-publish)
;;; User-Configurable Variables
@@ -51,11 +51,29 @@ This variable can be set to either `atx' or `setext'."
(const :tag "Use \"Setext\" style" setext)))
+;;;; Footnotes
+
+(defcustom org-md-footnotes-section "%s%s"
+ "Format string for the footnotes section.
+The first %s placeholder will be replaced with the localized Footnotes section
+heading, the second with the contents of the Footnotes section."
+ :group 'org-export-md
+ :type 'string
+ :version "26.1"
+ :package-version '(Org . "9.0"))
+
+(defcustom org-md-footnote-format "<sup>%s</sup>"
+ "Format string for the footnote reference.
+The %s will be replaced by the footnote reference itself."
+ :group 'org-export-md
+ :type 'string
+ :version "26.1"
+ :package-version '(Org . "9.0"))
+
;;; Define Back-End
(org-export-define-derived-backend 'md 'html
- :export-block '("MD" "MARKDOWN")
:filters-alist '((:filter-parse-tree . org-md-separate-elements))
:menu-entry
'(?m "Export to Markdown"
@@ -68,62 +86,64 @@ This variable can be set to either `atx' or `setext'."
(org-open-file (org-md-export-to-markdown nil s v)))))))
:translate-alist '((bold . org-md-bold)
(code . org-md-verbatim)
- (comment . (lambda (&rest args) ""))
- (comment-block . (lambda (&rest args) ""))
(example-block . org-md-example-block)
+ (export-block . org-md-export-block)
(fixed-width . org-md-example-block)
- (footnote-definition . ignore)
- (footnote-reference . ignore)
(headline . org-md-headline)
(horizontal-rule . org-md-horizontal-rule)
(inline-src-block . org-md-verbatim)
(inner-template . org-md-inner-template)
(italic . org-md-italic)
(item . org-md-item)
+ (keyword . org-md-keyword)
(line-break . org-md-line-break)
(link . org-md-link)
+ (node-property . org-md-node-property)
(paragraph . org-md-paragraph)
(plain-list . org-md-plain-list)
(plain-text . org-md-plain-text)
+ (property-drawer . org-md-property-drawer)
(quote-block . org-md-quote-block)
- (quote-section . org-md-example-block)
(section . org-md-section)
(src-block . org-md-example-block)
(template . org-md-template)
- (verbatim . org-md-verbatim)))
-
+ (verbatim . org-md-verbatim))
+ :options-alist
+ '((:md-footnote-format nil nil org-md-footnote-format)
+ (:md-footnotes-section nil nil org-md-footnotes-section)
+ (:md-headline-style nil nil org-md-headline-style)))
;;; Filters
-(defun org-md-separate-elements (tree backend info)
+(defun org-md-separate-elements (tree _backend info)
"Fix blank lines between elements.
TREE is the parse tree being exported. BACKEND is the export
back-end used. INFO is a plist used as a communication channel.
-Enforce a blank line between elements. There are three
-exceptions to this rule:
+Enforce a blank line between elements. There are two exceptions
+to this rule:
1. Preserve blank lines between sibling items in a plain list,
- 2. Outside of plain lists, preserve blank lines between
- a paragraph and a plain list,
-
- 3. In an item, remove any blank line before the very first
- paragraph and the next sub-list.
+ 2. In an item, remove any blank line before the very first
+ paragraph and the next sub-list when the latter ends the
+ current item.
Assume BACKEND is `md'."
(org-element-map tree (remq 'item org-element-all-elements)
(lambda (e)
- (cond
- ((not (and (eq (org-element-type e) 'paragraph)
- (eq (org-element-type (org-export-get-next-element e info))
- 'plain-list)))
- (org-element-put-property e :post-blank 1))
- ((not (eq (org-element-type (org-element-property :parent e)) 'item)))
- (t (org-element-put-property
- e :post-blank (if (org-export-get-previous-element e info) 1 0))))))
+ (org-element-put-property
+ e :post-blank
+ (if (and (eq (org-element-type e) 'paragraph)
+ (eq (org-element-type (org-element-property :parent e)) 'item)
+ (org-export-first-sibling-p e info)
+ (let ((next (org-export-get-next-element e info)))
+ (and (eq (org-element-type next) 'plain-list)
+ (not (org-export-get-next-element next info)))))
+ 0
+ 1))))
;; Return updated tree.
tree)
@@ -133,7 +153,7 @@ Assume BACKEND is `md'."
;;;; Bold
-(defun org-md-bold (bold contents info)
+(defun org-md-bold (_bold contents _info)
"Transcode BOLD object into Markdown format.
CONTENTS is the text within bold markup. INFO is a plist used as
a communication channel."
@@ -142,22 +162,22 @@ a communication channel."
;;;; Code and Verbatim
-(defun org-md-verbatim (verbatim contents info)
+(defun org-md-verbatim (verbatim _contents _info)
"Transcode VERBATIM object into Markdown format.
CONTENTS is nil. INFO is a plist used as a communication
channel."
(let ((value (org-element-property :value verbatim)))
(format (cond ((not (string-match "`" value)) "`%s`")
- ((or (string-match "\\``" value)
- (string-match "`\\'" value))
+ ((or (string-prefix-p "`" value)
+ (string-suffix-p "`" value))
"`` %s ``")
(t "``%s``"))
value)))
-;;;; Example Block and Src Block
+;;;; Example Block, Src Block and export Block
-(defun org-md-example-block (example-block contents info)
+(defun org-md-example-block (example-block _contents info)
"Transcode EXAMPLE-BLOCK element into Markdown format.
CONTENTS is nil. INFO is a plist used as a communication
channel."
@@ -166,6 +186,14 @@ channel."
(org-remove-indentation
(org-export-format-code-default example-block info))))
+(defun org-md-export-block (export-block contents info)
+ "Transcode a EXPORT-BLOCK element from Org to Markdown.
+CONTENTS is nil. INFO is a plist holding contextual information."
+ (if (member (org-element-property :type export-block) '("MARKDOWN" "MD"))
+ (org-remove-indentation (org-element-property :value export-block))
+ ;; Also include HTML export blocks.
+ (org-export-with-backend 'html export-block contents info)))
+
;;;; Headline
@@ -189,45 +217,67 @@ a communication channel."
(and (plist-get info :with-priority)
(let ((char (org-element-property :priority headline)))
(and char (format "[#%c] " char)))))
- (anchor
- (when (plist-get info :with-toc)
- (org-html--anchor
- (or (org-element-property :CUSTOM_ID headline)
- (concat "sec-"
- (mapconcat 'number-to-string
- (org-export-get-headline-number
- headline info) "-"))))))
;; Headline text without tags.
- (heading (concat todo priority title)))
+ (heading (concat todo priority title))
+ (style (plist-get info :md-headline-style)))
(cond
;; Cannot create a headline. Fall-back to a list.
((or (org-export-low-level-p headline info)
- (not (memq org-md-headline-style '(atx setext)))
- (and (eq org-md-headline-style 'atx) (> level 6))
- (and (eq org-md-headline-style 'setext) (> level 2)))
+ (not (memq style '(atx setext)))
+ (and (eq style 'atx) (> level 6))
+ (and (eq style 'setext) (> level 2)))
(let ((bullet
(if (not (org-export-numbered-headline-p headline info)) "-"
(concat (number-to-string
(car (last (org-export-get-headline-number
headline info))))
"."))))
- (concat bullet (make-string (- 4 (length bullet)) ? ) heading tags
- "\n\n"
- (and contents
- (replace-regexp-in-string "^" " " contents)))))
- ;; Use "Setext" style.
- ((eq org-md-headline-style 'setext)
- (concat heading tags anchor "\n"
- (make-string (length heading) (if (= level 1) ?= ?-))
- "\n\n"
- contents))
- ;; Use "atx" style.
- (t (concat (make-string level ?#) " " heading tags anchor "\n\n" contents))))))
-
+ (concat bullet (make-string (- 4 (length bullet)) ?\s) heading tags "\n\n"
+ (and contents (replace-regexp-in-string "^" " " contents)))))
+ (t
+ (let ((anchor
+ (and (org-md--headline-referred-p headline info)
+ (format "<a id=\"%s\"></a>"
+ (or (org-element-property :CUSTOM_ID headline)
+ (org-export-get-reference headline info))))))
+ (concat (org-md--headline-title style level title anchor tags)
+ contents)))))))
+
+
+(defun org-md--headline-referred-p (headline info)
+ "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)))
+
+(defun org-md--headline-title (style level title &optional anchor tags)
+ "Generate a headline title in the preferred Markdown headline style.
+STYLE is the preferred style (`atx' or `setext'). LEVEL is the
+header level. TITLE is the headline title. ANCHOR is the HTML
+anchor tag for the section as a string. TAGS are the tags set on
+the section."
+ (let ((anchor-lines (and anchor (concat anchor "\n\n"))))
+ ;; Use "Setext" style
+ (if (and (eq style 'setext) (< level 3))
+ (let* ((underline-char (if (= level 1) ?= ?-))
+ (underline (concat (make-string (length title) underline-char)
+ "\n")))
+ (concat "\n" anchor-lines title tags "\n" underline "\n"))
+ ;; Use "Atx" style
+ (let ((level-mark (make-string level ?#)))
+ (concat "\n" anchor-lines level-mark " " title tags "\n\n")))))
;;;; Horizontal Rule
-(defun org-md-horizontal-rule (horizontal-rule contents info)
+(defun org-md-horizontal-rule (_horizontal-rule _contents _info)
"Transcode HORIZONTAL-RULE element into Markdown format.
CONTENTS is the horizontal rule contents. INFO is a plist used
as a communication channel."
@@ -236,7 +286,7 @@ as a communication channel."
;;;; Italic
-(defun org-md-italic (italic contents info)
+(defun org-md-italic (_italic contents _info)
"Transcode ITALIC object into Markdown format.
CONTENTS is the text within italic markup. INFO is a plist used
as a communication channel."
@@ -261,19 +311,31 @@ a communication channel."
"."))))
(concat bullet
(make-string (- 4 (length bullet)) ? )
- (case (org-element-property :checkbox item)
- (on "[X] ")
- (trans "[-] ")
- (off "[ ] "))
+ (pcase (org-element-property :checkbox item)
+ (`on "[X] ")
+ (`trans "[-] ")
+ (`off "[ ] "))
(let ((tag (org-element-property :tag item)))
(and tag (format "**%s:** "(org-export-data tag info))))
(and contents
(org-trim (replace-regexp-in-string "^" " " contents))))))
+
+;;;; Keyword
+
+(defun org-md-keyword (keyword contents info)
+ "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)))
+
+
;;;; Line Break
-(defun org-md-line-break (line-break contents info)
+(defun org-md-line-break (_line-break _contents _info)
"Transcode LINE-BREAK object into Markdown format.
CONTENTS is nil. INFO is a plist used as a communication
channel."
@@ -287,28 +349,50 @@ channel."
CONTENTS is the link's description. INFO is a plist used as
a communication channel."
(let ((link-org-files-as-md
- (function
- (lambda (raw-path)
- ;; Treat links to `file.org' as links to `file.md'.
- (if (string= ".org" (downcase (file-name-extension raw-path ".")))
- (concat (file-name-sans-extension raw-path) ".md")
- raw-path))))
+ (lambda (raw-path)
+ ;; Treat links to `file.org' as links to `file.md'.
+ (if (string= ".org" (downcase (file-name-extension raw-path ".")))
+ (concat (file-name-sans-extension raw-path) ".md")
+ raw-path)))
(type (org-element-property :type link)))
(cond
- ((member type '("custom-id" "id"))
- (let ((destination (org-export-resolve-id-link link info)))
- (if (stringp destination) ; External file.
- (let ((path (funcall link-org-files-as-md destination)))
- (if (not contents) (format "<%s>" path)
- (format "[%s](%s)" contents path)))
- (concat
- (and contents (concat contents " "))
- (format "(%s)"
- (format (org-export-translate "See section %s" :html info)
- (mapconcat 'number-to-string
- (org-export-get-headline-number
- destination info)
- ".")))))))
+ ;; Link type is handled by a special function.
+ ((org-export-custom-protocol-maybe link contents 'md))
+ ((member type '("custom-id" "id" "fuzzy"))
+ (let ((destination (if (string= type "fuzzy")
+ (org-export-resolve-fuzzy-link link info)
+ (org-export-resolve-id-link link info))))
+ (pcase (org-element-type destination)
+ (`plain-text ; External file.
+ (let ((path (funcall link-org-files-as-md destination)))
+ (if (not contents) (format "<%s>" path)
+ (format "[%s](%s)" contents path))))
+ (`headline
+ (format
+ "[%s](#%s)"
+ ;; Description.
+ (cond ((org-string-nw-p contents))
+ ((org-export-numbered-headline-p destination info)
+ (mapconcat #'number-to-string
+ (org-export-get-headline-number destination info)
+ "."))
+ (t (org-export-data (org-element-property :title destination)
+ info)))
+ ;; Reference.
+ (or (org-element-property :CUSTOM_ID destination)
+ (org-export-get-reference destination info))))
+ (_
+ (let ((description
+ (or (org-string-nw-p contents)
+ (let ((number (org-export-get-ordinal destination info)))
+ (cond
+ ((not number) nil)
+ ((atom number) (number-to-string number))
+ (t (mapconcat #'number-to-string number ".")))))))
+ (when description
+ (format "[%s](#%s)"
+ description
+ (org-export-get-reference destination info))))))))
((org-export-inline-image-p link org-html-inline-image-rules)
(let ((path (let ((raw-path (org-element-property :path link)))
(if (not (file-name-absolute-p raw-path)) raw-path
@@ -324,53 +408,46 @@ a communication channel."
(format (org-export-get-coderef-format ref contents)
(org-export-resolve-coderef ref info))))
((equal type "radio") contents)
- ((equal type "fuzzy")
- (let ((destination (org-export-resolve-fuzzy-link link info)))
- (if (org-string-nw-p contents) contents
- (when destination
- (let ((number (org-export-get-ordinal destination info)))
- (when number
- (if (atom number) (number-to-string number)
- (mapconcat 'number-to-string number "."))))))))
- ;; Link type is handled by a special function.
- ((let ((protocol (nth 2 (assoc type org-link-protocols))))
- (and (functionp protocol)
- (funcall protocol
- (org-link-unescape (org-element-property :path link))
- contents
- 'md))))
(t (let* ((raw-path (org-element-property :path link))
(path
(cond
((member type '("http" "https" "ftp"))
(concat type ":" raw-path))
((string= type "file")
- (let ((path (funcall link-org-files-as-md raw-path)))
- (if (not (file-name-absolute-p path)) path
- ;; If file path is absolute, prepend it
- ;; with "file:" component.
- (concat "file:" path))))
+ (org-export-file-uri (funcall link-org-files-as-md raw-path)))
(t raw-path))))
(if (not contents) (format "<%s>" path)
(format "[%s](%s)" contents path)))))))
+;;;; Node Property
+
+(defun org-md-node-property (node-property _contents _info)
+ "Transcode a NODE-PROPERTY element into Markdown syntax.
+CONTENTS is nil. INFO is a plist holding contextual
+information."
+ (format "%s:%s"
+ (org-element-property :key node-property)
+ (let ((value (org-element-property :value node-property)))
+ (if value (concat " " value) ""))))
+
+
;;;; Paragraph
-(defun org-md-paragraph (paragraph contents info)
+(defun org-md-paragraph (paragraph contents _info)
"Transcode PARAGRAPH element into Markdown format.
CONTENTS is the paragraph contents. INFO is a plist used as
a communication channel."
(let ((first-object (car (org-element-contents paragraph))))
;; If paragraph starts with a #, protect it.
- (if (and (stringp first-object) (string-match "\\`#" first-object))
- (replace-regexp-in-string "\\`#" "\\#" contents nil t)
+ (if (and (stringp first-object) (string-prefix-p "#" first-object))
+ (concat "\\" contents)
contents)))
;;;; Plain List
-(defun org-md-plain-list (plain-list contents info)
+(defun org-md-plain-list (_plain-list contents _info)
"Transcode PLAIN-LIST element into Markdown format.
CONTENTS is the plain-list contents. INFO is a plist used as
a communication channel."
@@ -403,9 +480,19 @@ contextual information."
text)
+;;;; Property Drawer
+
+(defun org-md-property-drawer (_property-drawer contents _info)
+ "Transcode a PROPERTY-DRAWER element into Markdown format.
+CONTENTS holds the contents of the drawer. INFO is a plist
+holding contextual information."
+ (and (org-string-nw-p contents)
+ (replace-regexp-in-string "^" " " contents)))
+
+
;;;; Quote Block
-(defun org-md-quote-block (quote-block contents info)
+(defun org-md-quote-block (_quote-block contents _info)
"Transcode QUOTE-BLOCK element into Markdown format.
CONTENTS is the quote-block contents. INFO is a plist used as
a communication channel."
@@ -416,7 +503,7 @@ a communication channel."
;;;; Section
-(defun org-md-section (section contents info)
+(defun org-md-section (_section contents _info)
"Transcode SECTION element into Markdown format.
CONTENTS is the section contents. INFO is a plist used as
a communication channel."
@@ -425,15 +512,50 @@ a communication channel."
;;;; Template
+(defun org-md--footnote-formatted (footnote info)
+ "Formats a single footnote entry FOOTNOTE.
+FOOTNOTE is a cons cell of the form (number . definition).
+INFO is a plist with contextual information."
+ (let* ((fn-num (car footnote))
+ (fn-text (cdr footnote))
+ (fn-format (plist-get info :md-footnote-format))
+ (fn-anchor (format "fn.%d" fn-num))
+ (fn-href (format " href=\"#fnr.%d\"" fn-num))
+ (fn-link-to-ref (org-html--anchor fn-anchor fn-num fn-href info)))
+ (concat (format fn-format fn-link-to-ref) " " fn-text "\n")))
+
+(defun org-md--footnote-section (info)
+ "Format the footnote section.
+INFO is a plist used as a communication channel."
+ (let* ((fn-alist (org-export-collect-footnote-definitions info))
+ (fn-alist (cl-loop for (n _type raw) in fn-alist collect
+ (cons n (org-trim (org-export-data raw info)))))
+ (headline-style (plist-get info :md-headline-style))
+ (section-title (org-html--translate "Footnotes" info)))
+ (when fn-alist
+ (format (plist-get info :md-footnotes-section)
+ (org-md--headline-title headline-style 1 section-title)
+ (mapconcat (lambda (fn) (org-md--footnote-formatted fn info))
+ fn-alist
+ "\n")))))
+
(defun org-md-inner-template (contents info)
"Return body of document after converting it to Markdown syntax.
CONTENTS is the transcoded contents string. INFO is a plist
holding export options."
;; Make sure CONTENTS is separated from table of contents and
;; footnotes with at least a blank line.
- (org-trim (org-html-inner-template (concat "\n" contents "\n") info)))
-
-(defun org-md-template (contents info)
+ (concat
+ ;; Table of contents.
+ (let ((depth (plist-get info :with-toc)))
+ (when depth (org-html-toc depth info)))
+ ;; Document contents.
+ contents
+ "\n"
+ ;; Footnotes section.
+ (org-md--footnote-section info)))
+
+(defun org-md-template (contents _info)
"Return complete document string after Markdown conversion.
CONTENTS is the transcoded contents string. INFO is a plist used
as a communication channel."
@@ -472,9 +594,9 @@ non-nil."
;;;###autoload
(defun org-md-convert-region-to-md ()
- "Assume the current region has org-mode syntax, and convert it to Markdown.
+ "Assume the current region has Org syntax, and convert it to Markdown.
This can be used in any buffer. For example, you can write an
-itemized list in org-mode syntax in a Markdown buffer and use
+itemized list in Org syntax in a Markdown buffer and use
this command to convert it."
(interactive)
(org-export-replace-region-by 'md))
@@ -505,6 +627,16 @@ Return output file's name."
(let ((outfile (org-export-output-file-name ".md" subtreep)))
(org-export-to-file 'md outfile async subtreep visible-only)))
+;;;###autoload
+(defun org-md-publish-to-md (plist filename pub-dir)
+ "Publish an org file to Markdown.
+
+FILENAME is the filename of the Org file to be published. PLIST
+is the property list for the given project. PUB-DIR is the
+publishing directory.
+
+Return output file name."
+ (org-publish-org-to 'md filename ".md" plist pub-dir))
(provide 'ox-md)