diff --git a/README.org b/README.org index 2991929c..c4297f1f 100644 --- a/README.org +++ b/README.org @@ -4,11 +4,15 @@ This package will be an org exporter backend that exports =org-mode= to =markdow What this package is planned to do -- -- Write blog posts in =org-mode=. +- Write blog posts and other content in =org-mode=. - For *current subtree* - Export the org-mode meta-data and content into a separate markdown file. - Do that with each save. + +At present this project consists of the following files: + + This package might evolve into a multi-.el project. 1. An =ox-hugo.el= that just deals with exporting org to md. @@ -19,21 +23,25 @@ This package might evolve into a multi-.el project. - Set separate faces for titles based on /draft/ state and /futureness/. - Option to use template =config.toml= and some default hugo theme. So all a new user would need to do is to (i) have the =hugo= binary in =PATH= (ii) define their =hugo= blog dir in the =defcustom= (iii) =M-x hugo=. -* TODO [0/9] -- [ ] Have =ox-hugo= be a backend derived from =ox-gfm= (=ox-gfm= needed at least for table support). -- [ ] Parse org tags to set the hugo post tags/categories in fm. -- [ ] Parse org heading to set the hugo post title in fm. -- [ ] Use title to auto-generate file name string. -- [ ] Set post date to be the same as the export date *unless* =:PUBLISHDATE:= property exists. -- [ ] Ability to set/toggle =:DRAFT: true= in property drawer. Of course that should translate to hugo post fm. +* TODO [1/8] +- [X] Have =ox-hugo= be a backend derived from =ox-blackfriday= (=ox-blackfriday= needed at least for table support). +- [ ] fix table horizontal rule generator, which currently adds an additional syntax-breaking space in each cell +- [ ] Parse org heading to set the hugo post title in fm +- [-] Clean up and formalize metadata fields + - [ ] Parse org tags to set the hugo post tags/categories in fm. + - [X] Use title to auto-generate file name string. + - [ ] Set post date to be the same as the export date *unless* =:PUBLISHDATE:= property exists. + - [ ] Ability to set/toggle =:DRAFT: true= in property drawer. Of course that should translate to hugo post fm. - [ ] Function to re-export the whole org file to subtree-specific markdown files - [ ] Use =org-capture= to generate new posts in a pre-defined "blog posts org file". That step should also auto-insert the meta-data needed for hugo frontmatter as needed -- like the post's initial /draft/ state. -- [ ] Different faces for the post heading based on its /draft/ state and /futureness/ (if /publishdate/ is newer than today). +- [ ] Different faces for the post heading based on its /draft/ state and /futureness/ (if /publishdate/ is newer than today). (this seems like a `hugo-minor-mode`. Seems cool to do, but maybe a separate project? - [ ] Call =hugo= after each save. * References -Currently the =ox-hugo.el= just contains slightly re-factored code snippets from the below 2 sources: +Currently the =ox-hugo-helper.el= just contains slightly re-factored code snippets from the below 2 sources: - http://www.holgerschurig.de/en/emacs-blog-from-org-to-hugo/ - http://whyarethingsthewaytheyare.com/setting-up-the-blog/ -Recently discovered https://github.com/helloyi/ox-hugo. +=ox-blackfriday.el= is a derived backend that exports to the blackfriday markdown syntax, which Hugo uses as a basis. Stolen from `ox-gfm.el` + +=ox-hugo-helper.el= is a lightly-rewritten version of @helloyi's exporter: https://github.com/helloyi/ox-hugo. diff --git a/ox-blackfriday.el b/ox-blackfriday.el new file mode 100644 index 00000000..9dc26ceb --- /dev/null +++ b/ox-blackfriday.el @@ -0,0 +1,370 @@ +;;; ox-blackfriday.el --- Blackfriday Flavored Markdown Back-End for Org Export Engine + +;; Copyright (C) 2014 Lars Tveito + +;; Author: Lars Tveito, Matt Price +;; Keywords: org, wp, markdown, blackfriday +;; Package-Version: 20170304.1504 + +;; This file is not 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 . + +;;; Commentary: + +;; This library implements a Markdown back-end (blackfriday flavor) for Org +;; exporter, based on the `md' back-end. It is mostly copied from Lats Tveito's gfm +;; exporter (https://github.com/larstvei/ox-gfm). + +;;; Code: + +(require 'ox-md) +(require 'ox-publish) + +;;; User-Configurable Variables + +(defgroup org-export-blackfriday nil + "Options specific to Markdown export back-end." + :tag "Org Github Flavored Markdown" + :group 'org-export + :version "24.4" + :package-version '(Org . "8.0")) + + +;;; 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) + (paragraph . org-blackfriday-paragraph) + (strike-through . org-blackfriday-strike-through) + (src-block . org-blackfriday-src-block) + (table-cell . org-blackfriday-table-cell) + (table-row . org-blackfriday-table-row) + (table . org-blackfriday-table))) + + + +;;; Transcode Functions + +;;;; Paragraph + +(defun org-blackfriday-paragraph (paragraph contents info) + "Transcode PARAGRAPH element into Github Flavoured 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))) + + +;;;; Src Block + +(defun org-blackfriday-src-block (src-block contents info) + "Transcode SRC-BLOCK element into Github Flavored Markdown +format. CONTENTS is nil. 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))) + + +;;;; Strike-Through + +(defun org-blackfriday-strike-through (strike-through contents info) + "Transcode STRIKE-THROUGH from Org to Markdown (BLACKFRIDAY). +CONTENTS is the text with strike-through markup. INFO is a plist +holding contextual information." + (format "~~%s~~" contents)) + + +;;;; Table-Common + +(defvar width-cookies nil) +(defvar width-cookies-table nil) + +(defconst blackfriday-table-left-border "") +(defconst blackfriday-table-right-border " ") +(defconst blackfriday-table-separator "| ") + +(defun org-blackfriday-table-col-width (table column info) + "Return width of TABLE at given COLUMN. 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 build horizontal line in TABLE 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." + (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 ""))) + (setq contents + (concat data + (make-string (max 0 (- width (string-width data))) + ?\s))) + (concat left-border contents right-border))) + + +;;;; Table-Row + +(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))) + (when (and (eq 'rule (org-element-property :type table-row)) + ;; In BLACKFRIDAY, rule is valid only at second row. + (eq 1 (cl-position + table-row + (org-element-map table 'table-row 'identity info)))) + (let* ((table (org-export-get-parent-table table-row)) + (header-p (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 contents + (concat blackfriday-table-left-border + (mapconcat (lambda (col) (funcall build-rule col)) + (number-sequence 0 (- cols 1)) + blackfriday-table-separator) + blackfriday-table-right-border)))) + contents)) + + + +;;;; Table + +(defun org-blackfriday-table (table contents info) + "Transcode TABLE element into Github Flavored Markdown table. +CONTENTS is the contents of the table. INFO is a plist holding +contextual information." + (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")))))) + (concat (when no-header (funcall build-dummy-header)) + (replace-regexp-in-string "\n\n" "\n" contents)))) + + +;;;; Table of contents + +(defun org-blackfriday-format-toc (headline) + "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 type 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")))))) + + +;;;; 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-string (or (mapconcat 'org-blackfriday-format-toc headlines "\n") "")) + (toc-tail (if headlines "\n\n" ""))) + (org-trim (concat toc-string toc-tail contents "\n" (org-blackfriday-footnote-section info))))) + + + + + +;;; Interactive function + +;;;###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 () + "Assume the current region has org-mode syntax, and convert it +to Github Flavored 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 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 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 'blackfriday filename ".md" plist pub-dir)) + +(provide 'ox-blackfriday) + +;;; ox-blackfriday.el ends here diff --git a/ox-hugo-helper.el b/ox-hugo-helper.el new file mode 100644 index 00000000..d810aac5 --- /dev/null +++ b/ox-hugo-helper.el @@ -0,0 +1,155 @@ +;;; ox-hugo-helper.el --- Helper functions to write hugo blog posts in org-mode -*- lexical-binding: t -*- + +;;; Commentary: +;; This file is a colletion of legacy files pilfered from the web. +;; preserved here as resources for a more systematic approach. + + +;; http://www.holgerschurig.de/en/emacs-blog-from-org-to-hugo/ +(defvar hugo-content-dir (concat (getenv "HOME") "/sandbox/org/ox-hugo/content/") + "Path to Hugo's content directory") + +(defun hugo-ensure-property (property) + "Make sure that a property exists. If not, it will be created. + +Returns the property name if the property has been created, otherwise nil." + (unless (org-entry-get nil property) + (org-entry-put nil property "") + property)) + +(defun hugo-ensure-properties () + "This ensures that several properties exists. + +If not, these properties will be created in an empty form. In this case, the +drawer will also be opened and the cursor will be positioned at the first +element that needs to be filled. + +Returns list of properties that still must be filled in" + (require 'dash) + (let ((current-time (format-time-string + (org-time-stamp-format :long :inactive) + (org-current-time))) + first) + (save-excursion + (unless (org-entry-get nil "TITLE") + (org-entry-put nil "TITLE" (nth 4 (org-heading-components)))) + (setq first (--first it (mapcar #'hugo-ensure-property + '("HUGO_TAGS" "HUGO_TOPICS" "HUGO_FILE")))) + (unless (org-entry-get nil "HUGO_DATE") + (org-entry-put nil "HUGO_DATE" current-time))) + (when first + (goto-char (org-entry-beginning-position)) + ;; The following opens the drawer + (forward-line 1) + (beginning-of-line 1) + (when (looking-at org-drawer-regexp) + (org-flag-drawer nil)) + ;; And now move to the drawer property + (search-forward (concat ":" first ":")) + (end-of-line)) + first)) + +(defun hugo () + (interactive) + (unless (hugo-ensure-properties) + (let* ((title (concat "title = \"" + (org-entry-get nil "TITLE") + "\"\n")) + (date (concat "date = \"" + (format-time-string + "%Y-%m-%d" + (apply #'encode-time + (org-parse-time-string + (org-entry-get nil "HUGO_DATE")))) + "\"\n")) + (topics (concat "topics = [ \"" + (mapconcat #'identity + (split-string (org-entry-get nil "HUGO_TOPICS") + "\\( *, *\\)" :omit-nulls) + "\", \"") + "\" ]\n")) + (tags (concat "tags = [ \"" + (mapconcat #'identity + (split-string (org-entry-get nil "HUGO_TAGS") + "\\( *, *\\)" :omit-nulls) + "\", \"") + "\" ]\n")) + (fm (concat "+++\n" + title + date + tags + topics + "+++\n\n")) + (file (org-entry-get nil "HUGO_FILE")) + (coding-system-for-write buffer-file-coding-system) + backend + blog) + ;; Load ox-gfm.el if available and use it as backend + (if (require 'ox-gfm nil :noerror) + (setq backend 'gfm) + (progn + (require 'ox-md) + (setq backend 'md))) + (setq blog (org-export-as backend :subtreep)) + ;; Normalize save file path + (unless (string-match "^[/~]" file) + (setq file (concat hugo-content-dir file)) + (unless (string-match "\\.md$" file) + (setq file (concat file ".md"))) + ;; Save markdown + (with-temp-buffer + (insert fm) + (insert blog) + (write-file file) + (message "Exported to %s" file)))))) + +;; http://whyarethingsthewaytheyare.com/setting-up-the-blog/ +(defun diego/org-hugo-export () + "Export current subheading to markdown using pandoc." + (interactive) + (save-excursion + (unless (eq (org-current-level) 1) + (outline-up-heading 10)) + (let* ((org-pandoc-format 'markdown) + (org-pandoc-options-for-markdown '((standalone . t) + (atx-headers . t) + (columns . 79))) + (properties (org-entry-properties)) + (filename (cdr (assoc "EXPORT_FILE_NAME" properties))) + (title (concat "\"" (cdr (assoc "ITEM" properties)) "\"")) + (slug (concat "\"" (cdr (assoc "SLUG" properties)) "\"")) + (date (concat "\"" (cdr (assoc "DATE" properties)) "\"")) + (categories + (concat "[\"" (mapconcat + 'identity + (remove "" + (split-string + (cdr (assoc "TAGS" properties)) ":")) + "\",\"") "\"]"))) + (org-export-to-file + 'pandoc + (org-export-output-file-name + (concat (make-temp-name ".tmp") ".org") t) + nil t nil nil nil + (lambda (f) + (org-pandoc-run-to-buffer-or-file f 'markdown t nil))) + (sleep-for 0.5) + (with-temp-file filename + (insert-file-contents filename) + (goto-char (point-min)) + (re-search-forward "---\\(.\\|\n\\)+?---\n\n") + (replace-match "") + (goto-char (point-min)) + (insert + (format + "---\ntitle: %s\nslug: %s\ndate: %s\ncategories: %s\n---\n\n" + title slug date categories)) + (dolist (reps '(("^#" . "##") + ("\n``` {\\.\\(.+?\\)}" . "```\\1"))) + (goto-char (point-min)) + (while (re-search-forward (car reps) nil t) + (replace-match (cdr reps)))))))) + +(provide 'ox-hugo-helper) + +;;; ox-hugo.el ends here diff --git a/ox-hugo.el b/ox-hugo.el index 7835013f..5327d84a 100644 --- a/ox-hugo.el +++ b/ox-hugo.el @@ -1,152 +1,272 @@ -;;; ox-hugo.el --- Write hugo blog posts in org-mode -*- lexical-binding: t -*- +;;; ox-hugo.el --- Hugo Markdown Back-End for Org Export Engine + +;; Copyright (C) 2016 Helloyi He + +;; Author: Helloyi He +;; Keywords: org, hugo, markdown, gitpage + +;; This file is not 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 . ;;; Commentary: -;; - -;; http://www.holgerschurig.de/en/emacs-blog-from-org-to-hugo/ -(defvar hugo-content-dir (concat (getenv "HOME") "/sandbox/org/ox-hugo/content/") - "Path to Hugo's content directory") - -(defun hugo-ensure-property (property) - "Make sure that a property exists. If not, it will be created. - -Returns the property name if the property has been created, otherwise nil." - (unless (org-entry-get nil property) - (org-entry-put nil property "") - property)) - -(defun hugo-ensure-properties () - "This ensures that several properties exists. - -If not, these properties will be created in an empty form. In this case, the -drawer will also be opened and the cursor will be positioned at the first -element that needs to be filled. - -Returns list of properties that still must be filled in" - (require 'dash) - (let ((current-time (format-time-string - (org-time-stamp-format :long :inactive) - (org-current-time))) - first) - (save-excursion - (unless (org-entry-get nil "TITLE") - (org-entry-put nil "TITLE" (nth 4 (org-heading-components)))) - (setq first (--first it (mapcar #'hugo-ensure-property - '("HUGO_TAGS" "HUGO_TOPICS" "HUGO_FILE")))) - (unless (org-entry-get nil "HUGO_DATE") - (org-entry-put nil "HUGO_DATE" current-time))) - (when first - (goto-char (org-entry-beginning-position)) - ;; The following opens the drawer - (forward-line 1) - (beginning-of-line 1) - (when (looking-at org-drawer-regexp) - (org-flag-drawer nil)) - ;; And now move to the drawer property - (search-forward (concat ":" first ":")) - (end-of-line)) - first)) - -(defun hugo () + +;; This library implements a Markdown back-end (hugo flavor) for Org +;; exporter, based on the `md' back-end. + +;;; Code: + +(require 'ox-blackfriday) + + +;;; User-Configurable Variables + +(defgroup org-export-hugo nil + "Options specific to Markdown export back-end." + :tag "Org Hugo Flavored Markdown" + :group 'org-export + :version "24.4" + :package-version '(Org . "8.0")) + +(defcustom org-hugo-metadata-format "toml" + "Format used to metadata. +This variable can be set to either `toml' or `yaml'." + :group 'org-export-hugo + :type 'string) + + +;;; Define Back-End + +(org-export-define-derived-backend 'hugo 'blackfriday + ;;:export-block '("HMD" "HUGO FLAVORED MARKDOWN") + :menu-entry + '(?H "Export to Hugo Flavored Markdown" + ((?M "To temporary buffer" + (lambda (a s v b) (org-hugo-export-as-md a s v))) + (?m "To file" (lambda (a s v b) (org-hugo-export-to-md a s v))) + (?o "To file and open" + (lambda (a s v b) + (if a (org-hugo-export-to-md t s v) + (org-open-file (org-hugo-export-to-md nil s v))))))) + :translate-alist '((src-block . org-hugo-src-block) + ;;(inner-template . org-hugo-inner-template) + (table . org-hugo-table)) + :filters-alist '((:filter-body . org-hugo-body-filter)) + + :options-alist '((:hugo-metadata-format "HUGO_METADATA_FORMAT" nil org-hugo-metadata-format) + (:hugo-tags "HUGO_TAGS" nil nil) + (:hugo-categories "HUGO_CATEGORIES" nil nil) + (:hugo-description "HUGO_DESCRIPTION" nil nil) + (:hugo-slug "HUGO_SLUG" nil nil) + (:hugo-url "HUGO_URL" nil nil))) + + +;;; Transcode Functions + +;;;; Src Block + +(defun org-hugo-src-block (src-block contents info) + "Transcode SRC-BLOCK element into Hugo Flavored Markdown +format. CONTENTS is nil. INFO is a plist used as a communication +channel." + (let* ((lang (org-element-property :language src-block)) + (code (org-element-property :value src-block)) + (shortcode (concat "{{< highlight " lang " >}}\n")) + (close-shortcode "{{< /highlight >}}\n")) + (concat shortcode code close-shortcode))) + + + + +;;;; Hugo metadata + +(defun org-hugo-metadata (info) + "..." + (let* ((mt-format (org-export-data (plist-get info :hugo-metadata-format) info)) + (title (org-hugo--get-metadata-title info)) + (date (org-hugo--get-metadata-date info)) + + (description (org-hugo--get-string-metadata info :hugo-description)) + (tags (org-hugo--get-list-metadata info :hugo-tags)) + (categories (org-hugo--get-list-metadata info :hugo-categories)) + (slug (org-hugo--get-string-metadata info :hugo-slug)) + (url (org-hugo--get-string-metadata info :hugo-url)) + + (data (list "title" title "date" date)) + (data (if description (plist-put data "description" description) data)) + (data (if tags (plist-put data "tags" tags) data)) + (data (if categories (plist-put data "categories" categories) data)) + (data (if slug (plist-put data "slug" slug) data)) + (data (if url (plist-put data "url" url) data))) + + (message "%s" data) + (cond ((string= mt-format "toml") (org-hugo--encode-metadata-to-toml data)) + ((string= mt-format "yaml") (org-hugo--encode-metadata-to-yaml data)) + ""))) + +(defun org-hugo--get-metadata-title (info) + "Get title of hugo. +If title is nil, set it with current buffer name" + (let ((title (org-hugo--get-string-metadata info :title))) + (if title title + (org-hugo-string--wrap-quotes + (file-name-sans-extension + (file-name-nondirectory (buffer-file-name))))))) + +(defun org-hugo--get-metadata-date (info) + "Get date of hugo. +If date is nil, set it with current time" + (let ((date (org-export-get-date info "%Y-%m-%d %T %z"))) + (if date (org-hugo-string--wrap-quotes date) + (org-hugo-string--wrap-quotes (format-time-string "%Y-%m-%d %T %z" (current-time)))))) + +(defun org-hugo--get-list-metadata (info key) + "Get hugo metadata of list type. +INFO is a plist holding export options. +KEY is a key of hugo metadata." + (let ((value (org-export-data (plist-get info key) info)) + (key (substring (symbol-name key) 1))) + (cond ((string-empty-p value) nil) + (t (mapcar 'org-hugo-string--wrap-quotes (split-string value)))))) + +(defun org-hugo--get-string-metadata (info key) + "Get hugo metadata of string type. +INFO is a plist holding export options. +KEY is a key of hugo metadata." + (let ((value (org-export-data (plist-get info key) info)) + (key (substring (symbol-name key) 1))) + (cond ((string-empty-p value) nil) + (t (org-hugo-string--wrap-quotes value))))) + +(defun org-hugo-string--wrap-quotes (str) + "Wrap double quotes to string." + (cond ((string-empty-p str) "") + ((and (string= (substring str 0 1) "\"") + (string= (substring str -1) "\"")) str) + (t (concat "\"" str "\"")))) + +(defun org-hugo--encode-metadata-to-toml (data) + "Encode hugo metadata to toml format." + (setq metadata "+++\n") + (cl-loop for (key value) on data by 'cddr do + (setq metadata + (concat metadata + key " = " + (cond ((or (string= key "tags") (string= key "categories")) + (concat "[" (mapconcat 'identity value ", ") "]")) + (value)) + "\n"))) + (concat metadata "+++\n")) + +(defun org-hugo--encode-metadata-to-yaml (data) + "Encode hugo metadata to yaml format." + (setq metadata "---\n") + (cl-loop for (key value) on data by 'cddr do + (setq metadata + (concat metadata key ": " + (cond ((string= key "tags") + (concat "[" (mapconcat 'identity value ", ") "]")) + ((string= key "categories") + (concat "\n - " (mapconcat 'identity value "\n - "))) + (value)) + "\n"))) + (concat metadata "---\n")) + +;;;; Template + +(defun org-hugo-body-filter (body backend info) + "Add frontmatter to body of document. +BODY is the result of the export. BACKEND is always going to be hugo. INFO is a plist +holding export options." + (format "%s\n%s" (org-hugo-metadata info) body)) + + +;;; Interactive function + +;;;###autoload +(defun org-hugo-export-as-md (&optional async subtreep visible-only) + "Export current buffer to a Hugo 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 Hugo Export*\", which will +be displayed when `org-export-show-temporary-export-buffer' is +non-nil." (interactive) - (unless (hugo-ensure-properties) - (let* ((title (concat "title = \"" - (org-entry-get nil "TITLE") - "\"\n")) - (date (concat "date = \"" - (format-time-string - "%Y-%m-%d" - (apply #'encode-time - (org-parse-time-string - (org-entry-get nil "HUGO_DATE")))) - "\"\n")) - (topics (concat "topics = [ \"" - (mapconcat #'identity - (split-string (org-entry-get nil "HUGO_TOPICS") - "\\( *, *\\)" :omit-nulls) - "\", \"") - "\" ]\n")) - (tags (concat "tags = [ \"" - (mapconcat #'identity - (split-string (org-entry-get nil "HUGO_TAGS") - "\\( *, *\\)" :omit-nulls) - "\", \"") - "\" ]\n")) - (fm (concat "+++\n" - title - date - tags - topics - "+++\n\n")) - (file (org-entry-get nil "HUGO_FILE")) - (coding-system-for-write buffer-file-coding-system) - backend - blog) - ;; Load ox-gfm.el if available and use it as backend - (if (require 'ox-gfm nil :noerror) - (setq backend 'gfm) - (progn - (require 'ox-md) - (setq backend 'md))) - (setq blog (org-export-as backend :subtreep)) - ;; Normalize save file path - (unless (string-match "^[/~]" file) - (setq file (concat hugo-content-dir file)) - (unless (string-match "\\.md$" file) - (setq file (concat file ".md"))) - ;; Save markdown - (with-temp-buffer - (insert fm) - (insert blog) - (write-file file) - (message "Exported to %s" file)))))) - -;; http://whyarethingsthewaytheyare.com/setting-up-the-blog/ -(defun diego/org-hugo-export () - "Export current subheading to markdown using pandoc." + (org-export-to-buffer 'hugo "*Org Hugo Export*" + async subtreep visible-only nil nil (lambda () (text-mode)))) + + +;;;###autoload +(defun org-hugo-convert-region-to-md () + "Assume the current region has org-mode syntax, and convert it +to Hugo Flavored 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 this command to convert it." + (interactive) + (org-export-replace-region-by 'hugo)) + + +;;;###autoload +(defun org-hugo-export-to-md (&optional async subtreep visible-only) + "Export current buffer to a Hugo 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) - (save-excursion - (unless (eq (org-current-level) 1) - (outline-up-heading 10)) - (let* ((org-pandoc-format 'markdown) - (org-pandoc-options-for-markdown '((standalone . t) - (atx-headers . t) - (columns . 79))) - (properties (org-entry-properties)) - (filename (cdr (assoc "EXPORT_FILE_NAME" properties))) - (title (concat "\"" (cdr (assoc "ITEM" properties)) "\"")) - (slug (concat "\"" (cdr (assoc "SLUG" properties)) "\"")) - (date (concat "\"" (cdr (assoc "DATE" properties)) "\"")) - (categories - (concat "[\"" (mapconcat - 'identity - (remove "" - (split-string - (cdr (assoc "TAGS" properties)) ":")) - "\",\"") "\"]"))) - (org-export-to-file - 'pandoc - (org-export-output-file-name - (concat (make-temp-name ".tmp") ".org") t) - nil t nil nil nil - (lambda (f) - (org-pandoc-run-to-buffer-or-file f 'markdown t nil))) - (sleep-for 0.5) - (with-temp-file filename - (insert-file-contents filename) - (goto-char (point-min)) - (re-search-forward "---\\(.\\|\n\\)+?---\n\n") - (replace-match "") - (goto-char (point-min)) - (insert - (format - "---\ntitle: %s\nslug: %s\ndate: %s\ncategories: %s\n---\n\n" - title slug date categories)) - (dolist (reps '(("^#" . "##") - ("\n``` {\\.\\(.+?\\)}" . "```\\1"))) - (goto-char (point-min)) - (while (re-search-forward (car reps) nil t) - (replace-match (cdr reps)))))))) + (let ((outfile (org-export-output-file-name ".md" subtreep))) + (org-export-to-file 'hugo outfile async subtreep visible-only))) + +;;;###autoload +(defun org-hugo-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 'hugo filename ".md" plist pub-dir)) (provide 'ox-hugo)