|
|
;;; ox-blackfriday.el --- Blackfriday Flavored Markdown Back-End for Org Export Engine -*- lexical-binding: t -*- |
|
|
|
|
|
;; Copyright (C) 2017 Free Software Foundation, Inc. |
|
|
|
|
|
;; Authors: Matt Price <moptop99@gmail.com> |
|
|
;; Kaushal Modi <kaushal.modi@gmail.com> |
|
|
;; Lars Tveito <larstvei@ifi.uio.no> |
|
|
;; URL: https://github.com/kaushalmodi/ox-hugo |
|
|
;; Package-Requires: ((emacs "24.5")) |
|
|
;; Keywords: org, markdown, blackfriday |
|
|
|
|
|
;; 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 <http://www.gnu.org/licenses/>. |
|
|
|
|
|
;;; Commentary: |
|
|
|
|
|
;; This library implements a Markdown back-end (Blackfriday flavor |
|
|
;; (https://github.com/russross/blackfriday)) for Org exporter, based |
|
|
;; on the `md' back-end. |
|
|
|
|
|
;; It is mostly copied from Lats Tveito's GitHub Flavored Markdown |
|
|
;; exporter (https://github.com/larstvei/ox-gfm). |
|
|
|
|
|
;;; Code: |
|
|
|
|
|
(require 'ox-md) |
|
|
(require 'ox-publish) |
|
|
|
|
|
(defvar width-cookies nil) |
|
|
(defvar width-cookies-table nil) |
|
|
|
|
|
(defconst blackfriday-table-left-border "") |
|
|
(defconst blackfriday-table-right-border " ") |
|
|
(defconst blackfriday-table-separator "| ") |
|
|
|
|
|
|
|
|
;;; User-Configurable Variables |
|
|
|
|
|
(defgroup org-export-blackfriday nil |
|
|
"Options for exporting Org mode files to Blackfriday Markdown." |
|
|
:tag "Org Export Blackfriday" |
|
|
:group 'org-export |
|
|
:version "25.2") |
|
|
|
|
|
|
|
|
;;; Define Back-End |
|
|
|
|
|
(org-export-define-derived-backend 'blackfriday 'md |
|
|
:filters-alist '((:filter-parse-tree . org-md-separate-elements)) |
|
|
:menu-entry |
|
|
'(?b "Export to Blackfriday Flavored Markdown" |
|
|
((?B "To temporary buffer" |
|
|
(lambda (a s v b) (org-blackfriday-export-as-markdown a s v))) |
|
|
(?b "To file" (lambda (a s v b) (org-blackfriday-export-to-markdown a s v))) |
|
|
(?o "To file and open" |
|
|
(lambda (a s v b) |
|
|
(if a (org-blackfriday-export-to-markdown t s v) |
|
|
(org-open-file (org-blackfriday-export-to-markdown nil s v))))))) |
|
|
:translate-alist '((inner-template . org-blackfriday-inner-template) |
|
|
(latex-fragment . org-blackfriday-latex-fragment) |
|
|
(paragraph . org-blackfriday-paragraph) |
|
|
(plain-list . org-blackfriday-plain-list) |
|
|
(strike-through . org-blackfriday-strike-through) |
|
|
(src-block . org-blackfriday-src-block) |
|
|
(example-block . org-blackfriday-example-block) |
|
|
(table-cell . org-blackfriday-table-cell) |
|
|
(table-row . org-blackfriday-table-row) |
|
|
(table . org-blackfriday-table))) |
|
|
|
|
|
|
|
|
|
|
|
;;; Transcode Functions |
|
|
|
|
|
;;;; Inner Template |
|
|
(defun org-blackfriday-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." |
|
|
(let* ((depth (plist-get info :with-toc)) |
|
|
(headlines (and depth (org-export-collect-headlines info depth))) |
|
|
(toc-tail (if headlines "\n\n" "")) |
|
|
(toc-string "")) |
|
|
|
|
|
(when headlines |
|
|
(dolist (headline headlines) |
|
|
(setq toc-string (concat toc-string |
|
|
(org-blackfriday-format-toc headline info) |
|
|
"\n")))) |
|
|
(org-trim (concat toc-string toc-tail contents "\n" (org-blackfriday-footnote-section info))))) |
|
|
|
|
|
;;;; Latex Fragment |
|
|
(defun org-blackfriday-latex-fragment (latex-fragment _contents info) |
|
|
"Transcode a LATEX-FRAGMENT object from Org to Blackfriday Markdown. |
|
|
INFO is a plist holding contextual information." |
|
|
(let ((latex-frag (org-element-property :value latex-fragment)) |
|
|
(processing-type (plist-get info :with-latex))) |
|
|
(cond |
|
|
((memq processing-type '(t mathjax)) |
|
|
(let* ((frag (org-html-format-latex latex-frag 'mathjax info)) |
|
|
;; https://gohugo.io/content-management/formats#solution |
|
|
(frag (replace-regexp-in-string "_" "\\\\_" frag)) ;_ -> \_ |
|
|
;; Need to escape the backslash in "\(", "\)", .. to |
|
|
;; make Blackfriday happy. So \( -> \\(, \) -> \\), |
|
|
;; \[ -> \\[ and \] -> \\]. |
|
|
(frag (replace-regexp-in-string "\\(\\\\[]()[]\\)" "\\\\\\1" frag))) |
|
|
frag)) |
|
|
((assq processing-type org-preview-latex-process-alist) |
|
|
(let ((formula-link |
|
|
(org-html-format-latex latex-frag processing-type info))) |
|
|
(when (and formula-link (string-match "file:\\([^]]*\\)" formula-link)) |
|
|
(org-html--format-image (match-string 1 formula-link) nil info)))) |
|
|
(t latex-frag)))) |
|
|
|
|
|
;;;; Paragraph |
|
|
(defun org-blackfriday-paragraph (paragraph contents info) |
|
|
"Transcode PARAGRAPH element into Blackfriday Markdown format. |
|
|
CONTENTS is the paragraph contents. INFO is a plist used as a |
|
|
communication channel." |
|
|
(unless (plist-get info :preserve-breaks) |
|
|
(setq contents (concat (mapconcat 'identity (split-string contents) " ") "\n"))) |
|
|
(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) |
|
|
contents))) |
|
|
|
|
|
;;;; Plain List |
|
|
(defun org-blackfriday-plain-list (_plain-list contents _info) |
|
|
"Transcode plain list in CONTENTS into Markdown format." |
|
|
;; Two consecutive lists in Markdown can be separated by a comment. |
|
|
(let* ((contents (format "%s\n<!--listend-->" contents)) |
|
|
;; Do this only for top level lists. So the lines with |
|
|
;; <!--listend--> comments with preceding whitespace will be |
|
|
;; deleted. |
|
|
(contents (replace-regexp-in-string "\n\\s-+<!--listend-->\n" "" contents))) |
|
|
contents)) |
|
|
|
|
|
;;;; Src Block |
|
|
(defun org-blackfriday-src-block (src-block _contents info) |
|
|
"Transcode SRC-BLOCK element into Blackfriday Markdown format. |
|
|
|
|
|
INFO is a plist used as a communication channel." |
|
|
(let* ((lang (org-element-property :language src-block)) |
|
|
(code (org-export-format-code-default src-block info)) |
|
|
(prefix (concat "```" lang "\n")) |
|
|
(suffix "```")) |
|
|
(concat prefix code suffix))) |
|
|
|
|
|
;;;; Example Block |
|
|
(defun org-blackfriday-example-block (example-block _contents info) |
|
|
"Transcode a EXAMPLE-BLOCK element into Blackfriday Markdown format. |
|
|
CONTENTS is nil. INFO is a plist holding contextual |
|
|
information." |
|
|
(format "```text\n%s```" |
|
|
(org-export-format-code-default example-block info))) |
|
|
|
|
|
;;;; Strike-Through |
|
|
(defun org-blackfriday-strike-through (_strike-through contents _info) |
|
|
"Transcode strike-through text from Org to Blackfriday Markdown. |
|
|
CONTENTS contains the text with strike-through markup." |
|
|
(format "~~%s~~" contents)) |
|
|
|
|
|
;;;; Table-Common |
|
|
(defun org-blackfriday-table-col-width (table column info) |
|
|
"Return width of TABLE at given COLUMN using INFO. |
|
|
|
|
|
INFO is a plist used as communication channel. |
|
|
Width of a column is determined either by inquerying `width-cookies' |
|
|
in the column, or by the maximum cell with in the column." |
|
|
(let ((cookie (when (hash-table-p width-cookies) |
|
|
(gethash column width-cookies)))) |
|
|
(if (and (eq table width-cookies-table) |
|
|
(not (eq nil cookie))) |
|
|
cookie |
|
|
(progn |
|
|
(unless (and (eq table width-cookies-table) |
|
|
(hash-table-p width-cookies)) |
|
|
(setq width-cookies (make-hash-table)) |
|
|
(setq width-cookies-table table)) |
|
|
(let ((max-width 0) |
|
|
(specialp (org-export-table-has-special-column-p table))) |
|
|
(org-element-map |
|
|
table |
|
|
'table-row |
|
|
(lambda (row) |
|
|
(setq max-width |
|
|
(max (length |
|
|
(org-export-data |
|
|
(org-element-contents |
|
|
(elt (if specialp |
|
|
(car (org-element-contents row)) |
|
|
(org-element-contents row)) |
|
|
column)) |
|
|
info)) |
|
|
max-width))) |
|
|
info) |
|
|
(puthash column max-width width-cookies)))))) |
|
|
|
|
|
(defun org-blackfriday-make-hline-builder (table info char) |
|
|
"Return a function to horizontal lines in TABLE. |
|
|
Draw the lines using INFO with given CHAR. |
|
|
|
|
|
INFO is a plist used as a communication channel." |
|
|
`(lambda (col) |
|
|
(let ((max-width (max 3 (+ 1 (org-blackfriday-table-col-width ,table col ,info))))) |
|
|
(when (< max-width 1) |
|
|
(setq max-width 1)) |
|
|
(make-string max-width ,char)))) |
|
|
|
|
|
;;;; Table-Cell |
|
|
(defun org-blackfriday-table-cell (table-cell contents info) |
|
|
"Transcode TABLE-CELL element from Org into Blackfriday. |
|
|
|
|
|
CONTENTS is content of the cell. INFO is a plist used as a |
|
|
communication channel." |
|
|
;; (message "[ox-bf-table-cell DBG] In contents: %s" contents) |
|
|
(let* ((table (org-export-get-parent-table table-cell)) |
|
|
(column (cdr (org-export-table-cell-address table-cell info))) |
|
|
(width (org-blackfriday-table-col-width table column info)) |
|
|
(left-border (if (org-export-table-cell-starts-colgroup-p table-cell info) "" " ")) |
|
|
(right-border (if (org-export-table-cell-ends-colgroup-p table-cell info) "" " |")) |
|
|
(data (or contents "")) |
|
|
(cell (concat left-border |
|
|
data |
|
|
(make-string (max 0 (- width (string-width data))) ?\s) |
|
|
right-border)) |
|
|
(cell-width (length cell))) |
|
|
;; Each cell needs to be at least 3 characters wide (4 chars, |
|
|
;; including the table border char "|"); otherwise the export |
|
|
;; is not rendered as a table |
|
|
(when (< cell-width 4) |
|
|
(setq cell (concat (make-string (- 4 cell-width) ? ) cell))) |
|
|
;; (message "[ox-bf-table-cell DBG] Cell:\n%s" cell) |
|
|
cell)) |
|
|
|
|
|
;;;; Table-Row |
|
|
(defvar org-blackfriday--hrule-inserted nil |
|
|
"State variable to keep track if the horizontal rule after |
|
|
first row is already inserted.") |
|
|
|
|
|
(defun org-blackfriday-table-row (table-row contents info) |
|
|
"Transcode TABLE-ROW element from Org into Blackfriday. |
|
|
|
|
|
CONTENTS is cell contents of TABLE-ROW. INFO is a plist used as a |
|
|
communication channel." |
|
|
(let* ((table (org-export-get-parent-table table-row)) |
|
|
(row-num (cl-position ;Begins with 0 |
|
|
table-row |
|
|
(org-element-map table 'table-row #'identity info))) |
|
|
(row contents)) ;If CONTENTS is `nil', row has to be returned as `nil' too |
|
|
;; Reset the state variable when the first row of the table is |
|
|
;; received. |
|
|
(when (eq 0 row-num) |
|
|
(setq org-blackfriday--hrule-inserted nil)) |
|
|
|
|
|
;; (message "[ox-bf-table-row DBG] Row # %0d In contents: %s,\ntable-row: %S" row-num contents table-row) |
|
|
(when row |
|
|
(progn |
|
|
(when (and (eq 'rule (org-element-property :type table-row)) |
|
|
;; In Blackfriday, rule is valid only at second row. |
|
|
(eq 1 row-num)) |
|
|
(let* ((table (org-export-get-parent-table table-row)) |
|
|
;; (headerp (org-export-table-row-starts-header-p table-row info)) |
|
|
(build-rule (org-blackfriday-make-hline-builder table info ?-)) |
|
|
(cols (cdr (org-export-table-dimensions table info)))) |
|
|
(setq row (concat blackfriday-table-left-border |
|
|
(mapconcat (lambda (col) |
|
|
(funcall build-rule col)) |
|
|
(number-sequence 0 (- cols 1)) |
|
|
blackfriday-table-separator) |
|
|
blackfriday-table-right-border)))) |
|
|
|
|
|
;; If the first table row is "abc | def", it needs to have a rule |
|
|
;; under it for Blackfriday to detect the whole object as a table. |
|
|
(when (and (stringp row) |
|
|
(null org-blackfriday--hrule-inserted)) |
|
|
(let ((rule (replace-regexp-in-string "[^|]" "-" row))) |
|
|
(setq row (concat row "\n" rule)) |
|
|
(setq org-blackfriday--hrule-inserted t))))) |
|
|
;; (message "[ox-bf-table-row DBG] Row:\n%s" row) |
|
|
row)) |
|
|
|
|
|
;;;; Table |
|
|
(defun org-blackfriday-table (table contents info) |
|
|
"Transcode TABLE element from Org into Blackfriday. |
|
|
|
|
|
CONTENTS is contents of the table. INFO is a plist holding |
|
|
contextual information." |
|
|
;; (message "[ox-bf-table DBG] In contents: %s" contents) |
|
|
(let* ((rows (org-element-map table 'table-row 'identity info)) |
|
|
(no-header (or (<= (length rows) 1))) |
|
|
(cols (cdr (org-export-table-dimensions table info))) |
|
|
(build-dummy-header |
|
|
(function |
|
|
(lambda () |
|
|
(let ((build-empty-cell (org-blackfriday-make-hline-builder table info ?\s)) |
|
|
(build-rule (org-blackfriday-make-hline-builder table info ?-)) |
|
|
(columns (number-sequence 0 (- cols 1)))) |
|
|
(concat blackfriday-table-left-border |
|
|
(mapconcat (lambda (col) |
|
|
(funcall build-empty-cell col)) |
|
|
columns |
|
|
blackfriday-table-separator) |
|
|
blackfriday-table-right-border "\n" blackfriday-table-left-border |
|
|
(mapconcat (lambda (col) |
|
|
(funcall build-rule col)) |
|
|
columns |
|
|
blackfriday-table-separator) |
|
|
blackfriday-table-right-border "\n"))))) |
|
|
(tbl (concat (when no-header |
|
|
(funcall build-dummy-header)) |
|
|
(replace-regexp-in-string "\n\n" "\n" contents)))) |
|
|
;; (message "[ox-bf-table DBG] Tbl:\n%s" tbl) |
|
|
tbl)) |
|
|
|
|
|
;;;; Table of contents |
|
|
(defun org-blackfriday-format-toc (headline info) |
|
|
"Return an appropriate table of contents entry for HEADLINE. |
|
|
|
|
|
INFO is a plist used as a communication channel." |
|
|
(let* ((title (org-export-data (org-export-get-alt-title headline info) info)) |
|
|
(level (1- (org-element-property :level headline))) |
|
|
(indent (concat (make-string (* level 2) ? ))) |
|
|
(anchor (or (org-element-property :CUSTOM_ID headline) |
|
|
(org-export-get-reference headline info)))) |
|
|
(concat indent "- [" title "]" "(#" anchor ")"))) |
|
|
|
|
|
;;;; Footnote section |
|
|
(defun org-blackfriday-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 raw) in fn-alist collect |
|
|
(cons n (org-trim (org-export-data raw info)))))) |
|
|
(when fn-alist |
|
|
(format |
|
|
"## %s\n%s" |
|
|
"Footnotes" |
|
|
(format |
|
|
"\n%s\n" |
|
|
(mapconcat |
|
|
(lambda (fn) |
|
|
(let ((n (car fn)) (def (cdr fn))) |
|
|
(format |
|
|
"%s %s\n" |
|
|
(format |
|
|
(plist-get info :html-footnote-format) |
|
|
(org-html--anchor |
|
|
(format "fn.%d" n) |
|
|
n |
|
|
(format " class=\"footnum\" href=\"#fnr.%d\"" n) |
|
|
info)) |
|
|
def))) |
|
|
fn-alist |
|
|
"\n")))))) |
|
|
|
|
|
|
|
|
;;; Interactive functions |
|
|
|
|
|
;;;###autoload |
|
|
(defun org-blackfriday-export-as-markdown (&optional async subtreep visible-only) |
|
|
"Export current buffer to a Github Flavored Markdown buffer. |
|
|
|
|
|
If narrowing is active in the current buffer, only export its |
|
|
narrowed part. |
|
|
|
|
|
If a region is active, export that region. |
|
|
|
|
|
A non-nil optional argument ASYNC means the process should happen |
|
|
asynchronously. The resulting buffer should be accessible |
|
|
through the `org-export-stack' interface. |
|
|
|
|
|
When optional argument SUBTREEP is non-nil, export the sub-tree |
|
|
at point, extracting information from the headline properties |
|
|
first. |
|
|
|
|
|
When optional argument VISIBLE-ONLY is non-nil, don't export |
|
|
contents of hidden elements. |
|
|
|
|
|
Export is done in a buffer named \"*Org BLACKFRIDAY Export*\", which will |
|
|
be displayed when `org-export-show-temporary-export-buffer' is |
|
|
non-nil." |
|
|
(interactive) |
|
|
(org-export-to-buffer 'blackfriday "*Org BLACKFRIDAY Export*" |
|
|
async subtreep visible-only nil nil (lambda () (text-mode)))) |
|
|
|
|
|
;;;###autoload |
|
|
(defun org-blackfriday-convert-region-to-md () |
|
|
"Convert text in the current region to Blackfriday Markdown. |
|
|
The text is assumed to be in Org mode format. |
|
|
|
|
|
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 |
|
|
this command to convert it." |
|
|
(interactive) |
|
|
(org-export-replace-region-by 'blackfriday)) |
|
|
|
|
|
;;;###autoload |
|
|
(defun org-blackfriday-export-to-markdown (&optional async subtreep visible-only) |
|
|
"Export current buffer to a Github Flavored Markdown file. |
|
|
|
|
|
If narrowing is active in the current buffer, only export its |
|
|
narrowed part. |
|
|
|
|
|
If a region is active, export that region. |
|
|
|
|
|
A non-nil optional argument ASYNC means the process should happen |
|
|
asynchronously. The resulting file should be accessible through |
|
|
the `org-export-stack' interface. |
|
|
|
|
|
When optional argument SUBTREEP is non-nil, export the sub-tree |
|
|
at point, extracting information from the headline properties |
|
|
first. |
|
|
|
|
|
When optional argument VISIBLE-ONLY is non-nil, don't export |
|
|
contents of hidden elements. |
|
|
|
|
|
Return output file's name." |
|
|
(interactive) |
|
|
(let ((outfile (org-export-output-file-name ".md" subtreep))) |
|
|
(org-export-to-file 'blackfriday outfile async subtreep visible-only))) |
|
|
|
|
|
;;;###autoload |
|
|
(defun org-blackfriday-publish-to-blackfriday (plist filename pub-dir) |
|
|
"Publish an Org file to Blackfriday Markdown file. |
|
|
|
|
|
PLIST is the property list for the given project. FILENAME is |
|
|
the filename of the Org file to be published. PUB-DIR is the |
|
|
publishing directory. |
|
|
|
|
|
Return output file name." |
|
|
(org-publish-org-to 'blackfriday filename ".md" plist pub-dir)) |
|
|
|
|
|
|
|
|
(provide 'ox-blackfriday) |
|
|
|
|
|
;;; ox-blackfriday.el ends here
|
|
|
|