Export Day One (macOS) Journals to Emacs org-mode Diary Entries

I noticed that I still had the journaling app Day One (macOS) on my computer. I haven’t touched it in ages. So I figured it’d be time to export to plain text and then shove all the goodness from 2004 and onward into my Emacs org-mode diary.org.

Turns out there’s no Day One to org-mode exporter.

Yet!

Using olivetti-mode, modus-operandi theme, and hiding the leading org asterisks and auto-indenting the text, that's what it looks like for me

The best StackOverflow topic results in “just regex \tDate:\t and make an org outline”. It’s a good first step. But might just as well add some more cleverness to the conversion to get the year and month-of-year outline items for free, too!

Since my Emacs Lisp is so bad, it’s a Ruby script, of course.

So the result converts a Day One plain text export (usually called Journal.txt) into org-mode journal format. It features

  • Markdown image conversion from ![](thepath.jpg) to org inline image links, [[thepath.jpg]].
  • Extraction of images from the front of an entry; moves it right after the property drawer.
  • Creation of nested year/month/day date sections (see example below).

The output:

* 2020
** 2020-03 March
*** 2020-03-14 Saturday
**** Here's a diary entry imported from Day One.
:PROPERTIES:
:CREATED: [2020-03-14 Sat 14:33]
:END_PROPERTIES:
**** And another one from the same day.
:PROPERTIES:
:CREATED: [2020-03-14 Sat 19:12]
:END_PROPERTIES:
** 2020-06 June
*** 2020-06-22 Monday
**** Year, month, and day sections are created automatically.
:PROPERTIES:
:CREATED: [2020-06-22 Mon 08:06]
:END_PROPERTIES:

The conversion is pretty simple, but I added a bit of edge-case handling. A lot of my entries were a prominent image at the top plus text below, and with Day One putting all images inline in the plain text export, this meant that the first line started with a Markdown image reference followed by some text. Not pretty. The converter splits these.

Source Code

See the public Gist for a comfortable download.

#!/usr/bin/env ruby

require "date"

input, output, *rest = ARGV
if input.nil?
  STDERR.puts "Usage: #{__FILE__} DAYONE_INPUT_PATH ORG_OUTPUT_PATH"
  STDERR.puts "Missing input file path"
  exit 1
elsif output.nil?
  STDERR.puts "Usage: #{__FILE__} DAYONE_INPUT_PATH ORG_OUTPUT_PATH"
  STDERR.puts "Missing output file path"
  exit 1
end

File.open(output, "w") do |out|
  # Cached values to make sub-headings
  year = nil
  month = nil
  day = nil
  props = {}
  File.readlines(input).each do |line|
    if /\A\t(?<key>\w+):\t(?<value>.*+)$/ =~ line
      # Collect metadata in `props` dictionary
      case key
      when "Date"
        date = DateTime.parse(value)
        props["Created"] = date.strftime("[%Y-%m-%d %a %H:%M]")
        # Convert date lines to new entries in org

        # Output:  "* 2020"
        if year != date.year
          out.puts "* #{date.year}"
          year = date.year
          month = nil
        end

        # Output:  "** 2020-03 March"
        if month != date.month
          out.puts "** #{date.strftime("%Y-%m %B")}"
          month = date.month
        end

        # Output:  "*** 2020-03-12 Thursday"
        this_day = date.strftime("%Y-%m-%d %A")
        if day != this_day
          out.puts "*** #{this_day}"
        end
      else
        props[key] = value
      end
    elsif !props.empty?
      # Produce entry title and metadata
      if line.strip.empty?
        # Skip empty line separator after metadata
      else
        # Add entry heading, handling leading entry images
        cached_image = nil
        if /\A!\[]\((?<path>.+)\)(?<rest>.*)$/ =~ line
          cached_image = "[[./#{path}]]"
          out.puts "**** #{rest}"
        else
          out.puts "**** #{line}"
        end

        # Append property drawer
        out.puts ":PROPERTIES:"
        out.puts(props.map { |key, value| ":" + key.upcase.to_s + ":  " + value.to_s }
                      .join("\n"))
        out.puts ":END_PROPERTIES:"
        props = {}

        if !cached_image.nil?
          out.puts ""
          out.puts cached_image
        end
      end
    else
      line = line.gsub(/!\[\]\((.+)\)/) { "[[./#{$1}]]" }
      out.puts line
    end
  end
end