24

How can each top-level heading of an org-mode buffer be exported to a separate file named after the value of the corresponding CUSTOM_ID + (sanitized) title?

Let's say a buffer contains:

* Title of Heading 1
  :PROPERTIES:
  :CUSTOM_ID: fibrillogenesis
  :END:
  Suspendisse potenti. Mauris ac felis vel velit tristique imperdiet.  

** Sub-Heading
   Nullam rutrum.

* Another Title for Heading 2
  :PROPERTIES:
  :CUSTOM_ID: mitochondrion
  :END:
  Mauris mollis tincidunt felis.  Sed bibendum.

The final result would be a directory containing two files, one for each of the two top-level headings, with the format chosen at export time (HTML, LaTeX, etc.), with the following file names and content:

  1. Filename of first exported heading: fibrillogenesis-title-of-heading-1.[ext]

    Exported content, corresponding to the original first top-level heading:

    * Title of Heading 1
      :PROPERTIES:
      :CUSTOM_ID: fibrillogenesis
      :END:
      Suspendisse potenti. Mauris ac felis vel velit tristique imperdiet.  
    
    ** Sub-Heading 
       Nullam rutrum.
    
  2. Filename of second exported heading: mitochondrion-another-title-for-heading-2.[ext]

    Exported content, corresponding to the original second top-level heading:

    * Another Title for Heading 2
    :PROPERTIES:
    :CUSTOM_ID: mitochondrion
    :END:
    Mauris mollis tincidunt felis.  Sed bibendum. 
    

I would be very grateful for any hint, direction, pseudocode, or (better) real code.

1 Answer 1

33

The following command lets you choose a back-end and then exports each top-level subtree to a separate file:

(defun org-export-all (backend)
  "Export all subtrees that are *not* tagged with :noexport: to
separate files.

Note that subtrees must have the :EXPORT_FILE_NAME: property set
to a unique value for this to work properly."
  (interactive "sEnter backend: ")
  (let ((fn (cond ((equal backend "html") 'org-html-export-to-html)
                  ((equal backend "latex") 'org-latex-export-to-latex)
                  ((equal backend "pdf") 'org-latex-export-to-pdf))))
    (save-excursion
      (set-mark (point-min))
      (goto-char (point-max))
      (org-map-entries (lambda () (funcall fn nil t)) "-noexport" 'region-start-level))))

This currently supports HTML (html), LaTeX (latex), and PDF (pdf) export. You can add support for more back-ends by adding more clauses to cond.

As the docstring says, for each subtree you need to set the :EXPORT_FILE_NAME: property to the file name you want it to be exported to. (See below for other options.)

Automatically generating export file name from heading text

If you don't want to add :EXPORT_FILE_NAME: properties to every top-level heading, you can modify org-export-all to generate the filename automatically from e.g. the heading text, temporarily setting :EXPORT_FILE_NAME: during export:

(defun org-export-all (backend)
  "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."
  (interactive "sEnter backend: ")
  (let ((fn (cond ((equal backend "html") 'org-html-export-to-html)
                  ((equal backend "latex") 'org-latex-export-to-latex)
                  ((equal backend "pdf") 'org-latex-export-to-pdf)))
        (modifiedp (buffer-modified-p)))
    (save-excursion
      (set-mark (point-min))
      (goto-char (point-max))
      (org-map-entries
       (lambda ()
         (let ((export-file (org-entry-get (point) "EXPORT_FILE_NAME")))
           (unless export-file
             (org-set-property
              "EXPORT_FILE_NAME"
              (replace-regexp-in-string " " "_" (nth 4 (org-heading-components)))))
           (funcall fn nil t)
           (unless export-file (org-delete-property "EXPORT_FILE_NAME"))
           (set-buffer-modified-p modifiedp)))
       "-noexport" 'region-start-level))))

This function generates the export filename by replacing spaces with "_" in the headline text. If you want to generate the filename some other way, change the replace-regexp-in-string sexp to whatever you like.

Generating :EXPORT_FILE_NAME: when setting :CUSTOM_ID:

With the following advice, org-set-property will automatically set an appropriate value for :EXPORT_FILE_NAME: when you set :CUSTOM_ID::

(defadvice org-set-property (after set-export-file-name
                                   (property value) activate compile)
  (when (equal org-last-set-property "CUSTOM_ID")
    (let ((export-file-name
           (concat (org-entry-get nil "CUSTOM_ID")
                   "-"
                   (replace-regexp-in-string " " "-" (downcase (org-get-heading t t))))))
      (org-entry-put nil "EXPORT_FILE_NAME" export-file-name))))

Note that this will not add a file extension to the value of :EXPORT_FILE_NAME: but that doesn't matter because when exporting to a specific back-end, org-mode will automatically choose the correct extension for the resulting files.


Additional information

Updating existing subtrees in bulk

If you have a large number of existing subtrees that you need to set the :EXPORT_FILE_NAME: property for, you can use a keyboard macro. Position point on the first subtree, then do the following:

  • F3

    ... to start recording.

  • C-c C-x p CUSTOM_ID RET RET

    ... to make Emacs set :EXPORT_FILE_NAME: based on :CUSTOM_ID:.

  • C-c C-f

    ... to move to the next top-level headline.

  • F4

    ... to stop recording.

To repeat the macro for the next subtree, press F4. To repeat the macro for all remaining subtrees, press M-0 F4 (that's a zero).

Saving macros for future sessions

By default, keyboard macros are not saved across sessions. To store the macro in your init-file for later use, do this:

  1. Name the macro:

    M-x name-last-kbd-macro RET org-set-export-file-name RET

  2. Find your init-file and move to a spot where you would like to insert the macro.

  3. Insert the macro:

    M-x insert-kbd-macro RET org-set-export-file-name RET

    Emacs will insert the following code at point:

    (fset 'org-set-export-file-name
       "\C-c\C-xpCUSTOM_ID\C-m\C-m\C-c\C-f")
    

    If you squint hard enough you can see that the second argument to fset contains the sequence of keys that you pressed when you recorded the macro :)

  4. (Optional) For best results, you might want to bind org-set-export-file-name to a key:

    (define-key org-mode-map (kbd "<f6>") 'org-set-export-file-name)
    
  5. Save.

11
  • 2
    Nice. Could you give me a hint on how to programmatically set :EXPORT_FILE_NAME: property to :CUSTOM_ID:+heading-title-lowercased for each heading?
    – gsl
    Commented Oct 16, 2014 at 18:20
  • 2
    @gsl You could advise org-set-property to automatically generate the :EXPORT_FILE_NAME: property when you set :CUSTOM_ID:.
    – itsjeyd
    Commented Oct 16, 2014 at 18:31
  • 2
    Thank you for the advice! I am not that fluent with elisp, but I shall try. I need to find out how to capture each heading's title, substitute white space with dash, put to lowercase, add that sanitised string to :CUSTOM_ID: and finally set an org property.
    – gsl
    Commented Oct 16, 2014 at 18:40
  • 2
    @gsl I added a defadvice to my answer that automatically sets :EXPORT_FILE_NAME: to <custom-id>-<heading> when you set :CUSTOM_ID:.
    – itsjeyd
    Commented Oct 16, 2014 at 19:22
  • 3
    Thank you so much, I have learned a lot with your code. If one would have already an org-mode file with CUSTOM_IDs set, how one could run the code to set "EXPORT_FILE_NAME"? There wouldn't be new insertions. I am guessing defadvice would not work? Is there a looping facility to traverse all top-level headings and apply the code to them?
    – gsl
    Commented Oct 17, 2014 at 6:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.