README
Run the code inside the code block.
Run M-x org-export-head or (org-export-head directory backend)
The MENU and FOOTNOTES have to be :noexport: It cannot contain a property drawer at the moment.
Code
;; based on http://pragmaticemacs.com/emacs/export-org-mode-headlines-to-separate-files/
;; export headlines to separate files
;; http://emacs.stackexchange.com/questions/2259/how-to-export-top-level-headings-of-org-mode-buffer-to-separate-files
(defun org-export-head--run-on-temp-copy-buffer (function-to-run &rest args)
"Runs function on a temp buffer with the contents of the original buffer"
(save-excursion
(let ((temp-buffer (generate-new-buffer "tmp")))
(copy-to-buffer temp-buffer (point-min) (point-max))
(with-current-buffer temp-buffer
(org-mode)
(outline-show-all)
(apply function-to-run args))
(kill-buffer temp-buffer))))
(defun org-export-head (&optional directory-name backend reexport)
"Updates the hashes and reexport all changed headings if reexport is nil.
Reexports all headings if reexport is non-nil"
(interactive)
(let ((directory-name (or directory-name (read-directory-name "Directory:")))
(backend (or backend "html")))
(make-directory directory-name t)
(org-export-head--run-on-temp-copy-buffer #'org-export-head--modify-buffer-ast directory-name backend reexport)
(org-export-head--update-hashes)))
(defun org-export-head-reexport (&optional directory-name backend)
"Reexports all the headings"
(interactive)
(org-export-head directory-name backend t))
(defun org-export-head--modify-buffer-ast (directory-path backend reexport)
"Export all subtrees that are *not* tagged with :noexport: to
separate files.
Subtrees that do not have the :EXPORT_FILE_NAME: property set
are exported to a filename derived from the headline text."
;; Delete content that has already been exported and set it to noreexport
(org-export-head--update-hashes)
(if (not reexport)
(org-export-head--delete-noreexport))
;;Get headlines, and generate macros (previous post, etc)
(let* ((headlines-hash-list (org-export-head--get-headlines))
(headlines-hash (car headlines-hash-list))
(headlines-list (cdr headlines-hash-list))
;;Insert extra things in the headlines-hash to be used for fixing the macros
;;To define new headline-level macros, add extra functions here
(headlines-hash (org-export-head--insert-next-previous-headline headlines-hash headlines-list))
;;Now we get global macros such as the index and the reversed index
(global-macros (org-export-head--generate-index-alist headlines-list headlines-hash))
;;Now we get the templates. At the moment it is only the header
(header (org-export-head--get-content-subtree-match "header")))
;;For each not noexport/noreexport headline apply the template, i.e. copy contents
(org-export-head--run-on-each-heading
#'(lambda ()
(org-export-head--insert-on-header header))
"-noexport-noreexport")
;;After applying the template we replace the macros on all places
(org-export-head--run-on-each-heading
#'(lambda ()
(let* ((headline-name (org-export-head--headline))
(headline-alist (gethash headline-name headlines-hash nil))
(macro-alist (append headline-alist global-macros))) ;;in reverse order so that headline properties can overshadow these
(org-export-head--replace-headline-macros macro-alist)))
"-noexport-noreexport")
;;Get the parser tree and the headlines that will become files
(let* ((ast (org-element-parse-buffer)))
;;Fix links -- order is important. First external than fuzzy links
(org-element-map ast 'link
(lambda (link)
(let* ((link (or (org-export-head--fix-file-external-link-ast directory-path link) link))
(link (or (org-export-head--fix-local-link-ast headlines-hash link) link))))))
;;Convert the buffer to contain the new AST,
;;this is needed because the exporter expects the content to be in a buffer
(erase-buffer)
(insert (org-element-interpret-data ast))
(outline-show-all)
;;Finally export all the headers
(org-export-head-export-headers directory-path backend))))
;;Not everything can be done using the AST, sadly.
;;Org element has no support for adding custom properties to headlines
;;Nor does it have a nice interface to grab the contents without the property drawer
;;Ideally everything would be done using the AST and org-element, since it is
;;Less prone to writting bugs when using it.
;;So right now it is only used for fixing links
;;START OF NON AST (non org-element) SESSION
(defun org-export-head--run-on-each-heading(fn match &rest args)
"Puts the point on each heading and runs the function. Needed for exporting all headings
from http://pragmaticemacs.com/emacs/export-org-mode-headlines-to-separate-files/"
(save-excursion
(goto-char (point-min))
(goto-char (re-search-forward "^*"))
(set-mark (line-beginning-position))
(goto-char (point-max))
(org-map-entries
(lambda ()
(apply fn args))
match 'region-start-level)
(deactivate-mark)))
(defun org-export-head-export-headers (directory-name backend)
"Exports each heading to directory-name using backend"
(if (equal backend "html")
(org-export-head--run-on-each-heading
#'(lambda ()
(org-set-property
"EXPORT_FILE_NAME"
(concat directory-name (org-export-head--escaped-headline)))
(deactivate-mark)
(org-html-export-to-html nil t)
(set-buffer-modified-p t)) "-noexport-noreexport"))
(if (equal backend "pdf")
(org-export-head--run-on-each-heading
#'(lambda ()
(org-set-property
"EXPORT_FILE_NAME"
(concat directory-name (org-export-head--escaped-headline)))
(deactivate-mark)
(org-latex-export-to-pdf nil t)
(set-buffer-modified-p t)) "-noexport-noreexport")))
(defun org-export-head--goto-header(&optional no-new-line)
"Puts point after property-block if it exists, in an empty line
by creating a new line, unless no-new-line is non nil and returns point"
(interactive)
(org-back-to-heading t)
(let* ((beg-end (org-get-property-block))
(end (cdr beg-end)))
(goto-char (or end (point))))
(goto-char (point-at-bol 2)) ;;Advance one line
(if (not no-new-line)
(progn
(newline)
(goto-char (point-at-bol 0)))) ;;Go back one line
(point))
(defun org-export-head--get-content-subtree-at-point()
"Gets the content of the subtree at point"
(save-excursion
(deactivate-mark t)
(let ((start (org-export-head--goto-header t))
(end (org-end-of-subtree t)))
(buffer-substring start end))))
;;; HASH code
;;Idea from https://emacs.stackexchange.com/a/39376/20165
(defun org-export-head--update-hashes()
"Updates the hashes of all the headings"
(org-export-head--run-on-each-heading
#'(lambda()
(let ((new-hash (format "%s" (org-export-head-get-hash-value-content)))
(old-hash (org-entry-get-with-inheritance "HASH"))
(older-hash (org-entry-get-with-inheritance "PREVIOUS-HASH")))
(if (not old-hash)
(progn
(org-set-property "CREATION-DATE" (format-time-string "%Y-%m-%d"))))
;;If there was a change made
(if (not (equal new-hash old-hash))
(progn
(org-set-property "MODIFICATION-DATE" (format-time-string "%Y-%m-%d"))
(org-set-property "HASH" new-hash)))
;;Setting property is expensive
(if (not (equal old-hash older-hash))
(org-set-property "PREVIOUS-HASH" (or old-hash "")))))
"-noexport"))
(defun org-export-head-get-hash-value-content()
"Gets the hash of the subtree at point"
(org-export-head-hash-function (org-export-head--get-content-subtree-at-point)))
(defun org-export-head-hash-function(text)
"Function to calculate the hash of text.
Can be changed to something such as (length text) to run even faster.
Shouldn't rally affect the time to export unless your file contains over 100 thousand lines of text"
(md5 text))
;;;END HASH CODE
(defun org-export-head--delete-noreexport()
"Faster export by deleting things that won't be exported so we don't process them and their links"
(org-export-head--run-on-each-heading
#'(lambda()
(let ((old-hash (org-entry-get-with-inheritance "PREVIOUS-HASH"))
(new-hash (org-entry-get-with-inheritance "HASH")))
;;If there was a change made
(if (equal new-hash old-hash)
(progn
(org-toggle-tag "noreexport" 'on)
;;faster export by deleting noexport things before processing
(org-export-head--erase-content-subtree)))))
"-noexport-reexport"))
(defun org-export-head--erase-content-subtree()
(save-excursion
(let ((start (org-export-head--goto-header t))
(end (org-end-of-subtree)))
(delete-region start end))))
(defun org-export-head--get-headlines ()
"Returns a tuple that contains a hashtable of headline name to Alist of headline properties
As well as a list of the headline names"
(flet ((make-hash ()
(make-hash-table :test 'equal))
(add-to-hash (hashtable)
(puthash (org-export-head--headline) (org-entry-properties) hashtable)))
(let ((headlines-hash (make-hash))
(headlines-list ()))
(org-export-head--run-on-each-heading
#'(lambda()
(add-to-hash headlines-hash)
(setq headlines-list (cons (org-export-head--headline) headlines-list)))
"-noexport")
(cons headlines-hash headlines-list))))
(defun org-export-head--headline ()
"Gets the headline title if point is at the headline"
(nth 4 (org-heading-components)))
(defun org-export-head--escaped-headline ()
(org-export-head--escape (org-export-head--headline)))
(defun org-export-head--replace-headline-macros(macro-alist)
"Replace macros of the type ###TEXT### They can contain information such as date
or previous and next post.
Any headline property can be used as a macro of this type."
(save-excursion
(org-back-to-heading)
;;End of subtree might change because of macro expansion, so it is recalculated.
(while (re-search-forward "\\#\\#\\#\\([-A-Za-z_]+\\)\\#\\#\\#" (save-excursion (org-end-of-subtree)) t)
(unless (org-in-src-block-p)
(let* ((macro (match-string-no-properties 1))
(macro-subs (cdr (assoc macro macro-alist))))
(if macro-subs
(replace-match macro-subs t t)
(replace-match "")))))))
(defun org-export-head--get-content-subtree-match(match)
"Get content of the subtree that matches \"match\"
Where match is a tag or -tag or combination of them."
(save-excursion
(let ((content ""))
(org-export-head--run-on-each-heading
#'(lambda()
(setq content (concat content (org-export-head--get-content-subtree-at-point))))
match)
content)))
(defun org-export-head--insert-on-header (text)
"Insert text on the header of the subtree, but after the property box"
(save-excursion
(org-export-head--goto-header)
(insert text)))
(defun org-export-head--generate-index-alist (headlines-list headlines-hash)
"Geneates an org list with the index of the website and inserts it in an alist"
(let ((index "")
(reverse-index "")
(index-with-dates ""))
(dolist (headline-name headlines-list)
(let* ((headline-alist (gethash headline-name headlines-hash nil))
(creation-date (cdr (assoc "CREATION-DATE" headline-alist)))
(modification-date (cdr (assoc "MODIFICATION-DATE" headline-alist))))
(setq reverse-index (concat index "- [["headline-name"]["headline-name"]]\n"))
(setq index (concat "- [["headline-name"]["headline-name"]]\n" index))
(setq index-with-dates (concat "- [["headline-name"]["headline-name"]]"
"@@html:<span class=\"page-date\">@@"
" (" creation-date", updated " modification-date ")"
"@@html:</span>@@" "\n"
index-with-dates))))
(list (cons "INDEX" index) (cons "REVERSE-INDEX" reverse-index) (cons "INDEX-WITH-DATES" index-with-dates))))
;;END OF NON AST (non org-element) SESSION
(defun org-export-head--fix-local-link-ast (headlines link)
"Fixes fuzzy links to headlines, so the they point to new files"
(flet ((get-hash (element set)
(gethash element set nil)))
(when (string= (org-element-property :type link) "fuzzy")
(let* ((path (org-element-property :path link))
(new-path (get-hash path headlines)))
(when new-path
(let ((link-copy (org-element-copy link)))
(apply #'org-element-adopt-elements link-copy (org-element-contents link))
(org-element-put-property link-copy :type "file")
(org-element-put-property link-copy :path (concat (org-export-head--escape path) ".org"))
(org-element-set-element link link-copy)))))))
(defun org-export-head--fix-file-external-link-ast (directory-path link)
"Creates hard links to the external files in the output directory"
(when (string= (org-element-property :type link) "file")
(let* ((path (org-element-property :path link))
(link-copy (org-element-copy link))
;;Removes ../ from the releative path of the file to force it to be moved to a subfolder
;;of the current dir. This causes some file conflits in edge cases
;;e.g: ../images and ../../images will map to the same place. This should be rare in normal usage
(new-relative-path
(concat "./" (file-name-extension path) "/" (file-name-nondirectory path)))
(new-hard-link-path (concat directory-path new-relative-path))
(new-hard-link-directory (file-name-directory new-hard-link-path)))
;;Fix the AST
(apply #'org-element-adopt-elements link-copy (org-element-contents link))
(org-element-put-property link-copy :path new-relative-path)
(org-element-set-element link link-copy)
;;Create hard link folder
(make-directory new-hard-link-directory t)
;;Create hard link, not replacing if it already exists, catching error if file does not exist
(condition-case nil
(add-name-to-file path new-hard-link-path nil)
(error nil)))))
(defun org-export-head--insert-next-previous-headline(headlines-hash headlines-list)
"Decides what is the next and the previous post and create macro"
(let* ((temp-list (cons nil headlines-list))
(len (length headlines-list)))
(dotimes (i len)
(let* ((previous (nth 0 temp-list))
(headline-name (nth 1 temp-list))
(next (nth 2 temp-list))
(headline (gethash headline-name headlines-hash nil))
(new-properties
(list (cons "PREVIOUS" previous)
(cons "NEXT" next)))
(headline (append headline new-properties))) ;; In reverse order, to allow headline properties to shadow this.
(puthash headline-name headline headlines-hash))
(setq temp-list (cdr temp-list))))
headlines-hash)
(defun org-export-head--headline-to-file(headline-name)
"Generate the file name of the headline"
(concat (org-export-head--escape headline-name) ".org"))
(defun org-export-head--escape(text)
(when text
(let* ((text (replace-regexp-in-string " " "_" text))
(text (replace-regexp-in-string "/" "-" text))
(text (replace-regexp-in-string "[\\?.,!]" "" text)))
text)))
Includes
Creates a hard link to org.css in the export directory. ./org.css
Menu
@@html: <h1>@@ {{{title}}} @@comment: This is the title of the headline @@ @@html: </h1>@@
Index
This is my index! It uses a macro
Index is here!
###INDEX-WITH-DATES###
Foo
subheader 0
This is an example page that will be generated.
subheader1
Subheader 2
Subheader 2.1
Subheader 2.1.1
Subheader 3
[[test]] ;Show that links in code are not affected!Bar
Here we show that footnotes work [fn:1].
They are pretty great! [fn:2]
Foo Bar
Nothing to see here (org-export-head) ###previous###
Footnotes
[fn:2] My second foot note!
[fn:1] How does this work