Importing notes from Bear

Bear, my note-taking app of choice, stores its notes in a SQLite database at ~/Library/Group Containers/9K33E3U3T4.net.shinyfrog.bear/Application Data/database.sqlite. I use the following query to get the content out:

SELECT
  *,
  datetime(zmodificationdate, 'unixepoch', '31 years')
    AS zmodificationdate2
FROM
  zsfnote
WHERE
  ztrashed == 0 AND zarchived == 0

The zmodificationdate2 derived column takes care of the way timestamps are stored in Bear: these are Core Data timestamps, which are the number of seconds elapsed since January 1st, 2001.

Once I have the query, I can load the notes from the database:

notes = db[query].map do |row|
  {
    id: row[:ZUNIQUEIDENTIFIER],
    title: row[:ZTITLE],
    text: row[:ZTEXT],
    modification_date: row[:zmodificationdate2]
  }
end

I don’t use the note ID directly. The IDs of Bear notes are quite long (longer than UUIDs). I hash the UUIDs, then Avoid naughty words in hex digest strings by changing the set of characters, and finally take the first 15 characters, dash-separated in groups of 5 characters, as the new ID, e.g. 7fmkz-pkwxt-kcxvt, which I use in note URLs.

Tags and metadata

The Nanoc importer reads the note tags from the second line, below the header. This particular note has the #public tag:

# Importing notes from Bear
\#public

[[Bear]], my note-taking app of choice, …

To do: My importer doesn’t properly handle # escapes.

Only public notes, i.e. notes with the #public tag, are published on my web site.

The top of the note can have specific metadata, too. At the moment, only slug is supported, which is used to give the note a clean URL on my web site:

---
slug: importing-notes-from-bear
---

This yields a note at /notes/importing-notes-from-bear/.

Importing images

Images live in ~/Library/Group Containers/9K33E3U3T4.net.shinyfrog.bear/Application Data/Local Files/Note Images/. I don’t copy all images all as-is though, but rather copy an image whenever it occurs in a published note.

Where an imagine appears in a note, the note content has [image:<PATH>], with <PATH> being the path to the image relative to the Note Images/ directory mentioned above. For example: To do: This is not correct as of Bear 2.0 anymore.

[image:C6BEE843-E18A-4C98-82F0-EFA6499274E2-2641-0000089C7C19B1C6/Screen Shot 2022-10-27 at 21.03.30.png]

With that relative path known, I copy the image into my site’s content/notes/images/ directory.

A Nanoc filter then replaces those [image:<PATH>] markup directives with HTML img elements. The filter looks like this:

    content.gsub(/\[image:([^\]]+)\]/) do |match|
      partial_path = Regexp.last_match[1]
      relative_path = '/notes/images/' + partial_path

      %[<figure><img src="#{relative_path}"></figure>]
    end

Adding image metadata

An image can be followed immediately by a code block with YAML metadata:

alt: "A kitten"
wide: true

The importer treats this as metadata for the preceding image. The alt key specifies the alt text, and wide is a boolean that determines whether or not the image should also span the sidebar.

References