Markdown to Org

Introduction

Recently, as I was creating my project Persp-topics I had a need to find readme files no matter what kind, and load them. It also happened that I was reading the Melpa README.md at the time. I could not get over how ugly markdown is, or how difficult markdown is to read, even in Emacs. I was yearning for Org as I waded through the details of making a Melpa package. What if all markdown files just loaded as Org files?

What if it just loads whatever is there and does what we want? I want Org, I don’t want to care if its Markdown or Org, just load it please.

A small package

This is just a small set of functions, but it makes sense to make it a package in order to share it. All of the code here is in my codeberg md-org repository. I’ve already done a pull request at Melpa.

The journey

I had no idea how to do it. Then Pandoc came to the rescue. I started by doing a shell-command to convert a file. It was the first pandoc example I found. It seemed like a good place to start.

The Pandoc command

Here is most of the pandoc command. I imagine we could make this work for more than markdown. I did not go down that rabbit hole.

(defun md-org-make-pandoc-cmd (wrap-preserve)
  "Create the pandoc command for markdown with WRAP-PRESERVE or not."
  (let ((wrap-preserve (if wrap-preserve "--wrap=preserve" "")))
    (format "pandoc -f markdown -t org %s" wrap-preserve)))

Converting a file

This was the first thing I did. The command to convert files was easy, the code was mostly UI and checks.

(defun md-org-file (md-file org-file &optional wrap-preserve)
  "Convert MD-FILE to ORG-FILE.
If WRAP-PRESERVE is t then do so.

This requires `pandoc' to be installed on your system."
  (interactive
   (list
    (read-file-name "Markdown file name:")
    (read-file-name "Org file name:")))
  
  (let* ((pandoc (executable-find "pandoc"))
         (arg-format (concat
                      (md-org-make-pandoc-cmd wrap-preserve)
                      " -o %s %s"))
         
         (is-md (f-file? md-file))
         (convert-it (and pandoc is-md)))

    (cond ((not is-md)
           (message
            "md-org: md->org, file does not exist: %s" md-file))
          ((not pandoc)
           (message
            "md-org: md->org, Cannot convert %s, pandoc not found."
            md-file)))

    (if convert-it
        (shell-command
         (format arg-format wrap-lines org-file md-file)))))

Shell command on Buffer

Then I thought I should just try to convert a buffer. It seems likely thats what I might need, and what could be easier ?. It looks easy now. I don’t how many ways I tried marking a region before it became this simple.

(defun md-org-shell-command-on-buffer (buffer command &optional output-buffer)
  "Run shell COMMAND on contents of BUFFER, direct output to OUTPUT-BUFFER."
  (interactive)
  (with-current-buffer buffer
    (shell-command-on-region (point-min) (point-max)
                             command
                             output-buffer)))

Convert a Buffer

Heres the actual buffer function, getting a name for the new buffer was most of the work.

(defun md-org-buffer (&optional buffer wrap-preserve)
  "Convert a markdown BUFFER to a new org buffer.
If WRAP-PRESERVE is set changes the pandoc command accordingly.
Defaults to converting the current buffer. Creates a new buffer with the
same name, but with an .org extension."
  (interactive)
  (let* ((buffer (or buffer (current-buffer)))
             (name (buffer-name buffer))
             (extension (file-name-extension name))
             (base-name (file-name-base name))
             (new-name (format "%s.org" base-name)))
    
    (if (equal extension "md")
        (progn
          (md-org-shell-command-on-buffer
           buffer
           (md-org-make-pandoc-cmd wrap-preserve) new-name)

          (pop-to-buffer new-name)
          (org-mode))
      (message "Md-Org: Can only convert markdown buffers."))))

Convert a Region

Somewhere along the way I found Charles Choi’s post on converting a region that was even simpler than converting a buffer. Here is my version. Charles is the reason that the wrap-preserve option is everywhere here. It seemed like a good idea to me too.

(defun md-org-region (start end &optional wrap-preserve output-buffer)
  "Convert region from markdown to org.
Using the region interface from START to END.
Adds WRAP-PRESERVE if set. Directs to OUTPUT-BUFFER if given."
  (interactive "r")
  (let* ((pandoc (md-org-make-pandoc-cmd  wrap-preserve))
             (output-buffer (or output-buffer t)))
    (shell-command-on-region start end pandoc output-buffer t)))

Convert on load

The next question was why have files at all?, So I made a find file function that just loads Markdown as Org.

Anything other than Markdown it just uses find-file like normal.

This is really nice. The Markdown just shows up as pretty org buffer.

(defun md-org-find-file (filename &optional wrap-preserve)
  "Find a markdown file with FILENAME, convert and load it as an org file.
If WRAP-PRESERVE is true then do that.
If called interactively prompts for a file name.
If file is not markdown the file is simply loaded.
If the file is markdown but pandoc is not available then
the file is loaded as markdown."
  
  (interactive
   (list
    (read-file-name "File name:")))

  (let ((pandoc (executable-find "pandoc"))
        (markdown (equal (file-name-extension filename) "md")))
    
    (if (not markdown)
        (find-file filename)
      
      (if (not pandoc)
          (progn
            (message "Md-Org: Unable to convert %s without pandoc." filename)
            (find-file filename))

        (let* ((buffer-name (format "%s.org" (file-name-base filename)) )
               (cmd-format (concat (md-org-make-pandoc-cmd wrap-preserve) " %s"))
               (pandoc (format cmd-format filename)))
          (with-current-buffer (get-buffer-create buffer-name )
            (pop-to-buffer buffer-name)
            (shell-command pandoc buffer-name)
            (org-mode)))))))

Just load the README already.

Persp-topics still had it’s needs, needing two Spawn templates for every template that wanted to load a README was too much. As a user, deciding which template to use without going to look was also a pain.

This function will find a README.org or README.md in the current directory and it will just load them, and it will convert the markdown if you ask. If there happens to be both an Org file and a Markdown file it will load the Org.

(defun md-org-find-readme (&optional convert wrap-preserve)
  "Try to find a readme file in the current directory.
if CONVERT and the file is markdown, and pandoc is installed,
convert to org and load that instead.
If WRAP-PRESERVE is t then do so.

This requires `pandoc' to be installed on your system."
  (interactive)
  (let* ((is-md (f-file? "README.md"))
         (is-org (f-file? "README.org"))
         (convert-it (and is-md convert (not is-org))))
    
    (if convert-it
        (md-org-find-file "README.md" wrap-preserve)
      (if is-org
          (find-file "README.org")
        (find-file "README.md")))))

Summary

This was a bit surprising to me. I just needed a solution for loading READMEs. The solution is so much more than that. It was a learning process and I kept the tools I made along the way. You can find this code in my md-org project on codeberg. I’ve already submitted it to Melpa, we will see how it goes.

Enjoy your Markdown in Org mode!

Zenie


© 2018-2024. All rights reserved.