In January through April of 2015, I attempted to fill in a gap which I thought was missing in the area of digital note-taking. I have recipes I want to remember, general to-do lists, packing lists for camping and hunting which could come in handy in the future, a wish list, work notes, journals and more. Although there are several note-taking applications out there, I wanted something a little different than what they all offered. Here is my list of requirements:

  • a way that I can edit files in my favorite text editor locally without an internet connection
  • these notes will be taken in markdown syntax so they can be beautified into html
  • these notes can be organized in an arbitrarily complex structure of directories
  • at a time of my choosing, when connected to the internet, I can push the changes to the cloud
  • these pushed notes can be accessed in a read-only fashion through a browser
  • the web app would allow a viewer to easily navigate the directory structure previously mentioned in order to find notes
  • I'd prefer that these notes remain private once pushed to the cloud

I posted in reddit asking for suggestions that would meet those requirements. Some decent answers were given, but nothing I was very excited about using. This is when I decided to create something for myself using the rust language. Writing this program would be a useful exercise in familiarizing myself with the language, and, practically, it would be very useful in day-to-day note taking.

Design

I decided to use markdown as the note syntax (as mentioned above) and handlebars as the templating engine for building the read-only site. Also, for my case, I'm using git for version control and distributing the content of the site. To keep everything straight, I'll try to refer to pieces of this application like so:

  • rust-notes - a rust project that compiles to a runnable binary which converts content into a content website - github
  • content - a collection of styles, handlebars templates, notes, images, and directories which is used as input for rust-notes - github example
  • content website - the output of rust-notes which contains css, javascript, and html which an be served statically by a web server (I use apache) - example output

rust-notes

This is the main part of this project and the part written in rust. It loads all the handlebars templates from content, then recursively navigates through all files in the notes/ directory in content. I think I've made a decent mechanism for converting each file encountered in notes/ to a web page. I've made a FileType trait which can be implemented for any type of file and knows how to convert the input to some web asset. These get created by FileTypeFactorys. I'll mention these further down. The FileType trait can be seen here:

pub trait FileType {
    fn get_url(&self, context: &::AppContext) -> String;
    fn convert(&self, context: &::AppContext);
    fn get_type_str(&self) -> &'static str;
}

When implementing these different FileTypes, the constructor usually takes the actual file in question, so it's within the context of that FileType. get_url returns the url which will be used for this file in question once it's converted for the content website. convert does the physical conversion (creates html in a separate directory usually), and get_type_str simply returns a string so this FileType can be differentiated from others without knowing what actual type is being used.

Another trait I've created is FileTypeFactory.

trait FileTypeFactory {
    fn try_create(&self, path: &Path) -> Option<Box<FileType>>;
    fn initialize(&self, app_context: &mut ::AppContext) -> Result<(), &'static str>;
}

Each file type I'm handling (markdown, directories, and unknown) must also implement one of these. initialize allows all handlebars templates to be pre-loaded and other initialization code. try_create is the factory part of the trait. It will return Some(Box<FileType>) if the file existing at the input path is of the correct type, or None if it isn't. Using the try_create method on all factories lets me avoid making a large clunky-looking if/elseif statement, since I farm out the logic to these factories. The code which receives a path and outputs a Box looks like this. self.factories is a list of Box<FileTypeFactory>.

pub fn create_file_type<P: AsRef<Path>>(&self, path: P) -> Box<FileType> {
    for factory in self.factories.iter() {
        let result_maybe = factory.try_create(path.as_ref());
        if result_maybe.is_some() {
            return result_maybe.unwrap();
        }
    }
    self.unknown_factory.try_create(path.as_ref()).unwrap()
}

The last line of this function will use the unknown file type since none of the others worked.

Handled File Types

At this point, rust-notes can convert markdown files (I started with calling these types 'notes' so you may see that sprinkled throughout code), directories, and unknown files (a catch-all).

directory

Each time a directory is encountered, a corresponding directory and index.html file within that directory is created in the content website. These index.html files use the template file located at layouts/dir.hbs in content. Mine looks like this at the moment:

<div class="container">
    <div class="row">

        {{#each children}}
            <div class="col-xs-6 col-sm-3 col-lg-2 dir-item">
                <a href="{{url}}">
                    <span class="icon-{{file_type}} icon"></span>
                    <span>{{name}}</span>
                </a>
            </div>
        {{/each}}

    </div>
</div>

This list called children you see in the above template is sorted with directories at first and then alphabetically. The file_type property simply provides some javascript code information for getting the correct icon.

markdown

Markdown files are handled similarly to directories in that they also use a handlebars template. Only one item gets made in the content website and that is <markdown-file-name>.html. The template used to create this html file is located at layouts/note.hbs and mine looks like this:

<div class="container">

    {{{content}}}

</div>

Quite simple! The non-trivial part of this conversion is using a rust markdown library (I'm using this) to generate html.

unknown

Every other type of file (images, videos, text files, etc.) are handled in this way: copying. The file simply gets copied from content over to the content website.

Extensibility

In the end, I think this program turned out decently extensible. In order to add a different file type handler, you'd need to implement FileType, FileTypeFactory and add the factory to the list of factories. Eventually I'd like to write a handler for images. Perhaps there are other handlers which would be nice to have for this website-generating program.

Output

I'm not much of a CSS or design wizard, but I did my best in styling the content website so it doesn't scar your eyes.

directory

directory

markdown file

markdown