NOTE: Since this episode was released (over 2 years ago, and before Elixir 1.0 was out), quite a bit has changed. There are no longer records of this type in the language - see Episodes 054 and 055 on Maps to see what replaced records in the language. Also, Phoenix has changed substantially. The broad strokes of this episode are still appropriate, but you won't be able to follow it step by step these days. Sorry about that!

Something I always wanted was a quick way to serve up a set of markdown files from a given directory. There are a lot of ways to solve this, but it makes for a good project for us to cover over a few episodes. Let's get started.

Project

Start a new project with mix new markdown_server and cd into it.

ExDoc already has a basic markdown renderer built into it, so for now we're going to use that to do the bulk of the work for us. We'll go ahead and add it as a dependency in mix.exs:

  defp deps do
    [
      { :ex_doc, github: "elixir-lang/ex_doc" },
    ]
  end

And fetch it with mix deps.get. Next, we'll start writing some tests. Open up test/markdown_server/renderer_test.exs and let's talk about it.

mkdir test/markdown_server
vim test/markdown_server/renderer_test.exs
defmodule MarkdownServer.RendererTest do
  use ExUnit.Case
end

Alright, so the first thing we're going to want to do is define what a 'rendered' document looks like. I expect this to be a record...

(at the top of the test file)

defrecord MarkdownServer.RenderedDocument

...with a body field, and we'll also try to extract a title out of the record. If we can't extract a title, we'd like it to default to "Untitled Document".

defrecord MarkdownServer.RenderedDocument, body: nil, title: "Untitled Document"

Alright, so now let's think about the API we want for this Renderer module. Ultimately, I'd like it to be able to take a file path to render, but let's start out just rendering a string. We'll write a test:

  test "renders markdown documents from strings" do
    rendered_document = MarkdownServer.Renderer.render_string("This doc has no title.")
    expected_body = "<p>This doc has no title.</p>\n"
    expected_title = "Untitled Document"
    assert MarkdownServer.RenderedDocument[title: expected_title, body: expected_body] == rendered_document
  end

Now of course that will fail, because we've not built the module. Let's stub that function for now:

(at the top of the test)

defmodule MarkdownServer.Renderer do
  def render_string(string) do
    MarkdownServer.RenderedDocument.new
  end
end

Alright, so basically all that's in our way at present is actually rendering the body with markdown. This can be handled with ExDoc. It brought in a Markdown module with it, which has a to_html/1 function. Let's use it.

  def render_string(string) do
    body = string |> Markdown.to_html
    MarkdownServer.RenderedDocument[body: body]
  end

Alright, so run the tests and that should make it pass. Next, we'd like to write a test for extracting a title from the documents. I like to do this by taking the first <h1> tag out of a document. We'll write a new test:

  test "extracts titles from markdown documents" do
    rendered_document = MarkdownServer.Renderer.render_string("# This is a title\n\nThis doc has a title.")
    expected_title = "This is a title"
    assert expected_title == rendered_document.title
  end

Run the tests, and they will of course fail. Even though we know that the center cannot hold, we're going to use a RegEx to parse this html to extract the title.

I like to break out regexes into named things, so we're going to do this by defining a couple of private functions. One is just a name for a RegEx that will match the title with a named capture:

  defp title_matcher do
    %r/<h1>(?<title>.*)<\/h1>/g
  end

I've not shown you named captures with RegExes in Elixir yet, so this ends up pretty fun. Next, we'll write a function to use this RegEx and extract the title:

  defp title_for(body) do
    case Regex.named_captures(title_matcher, body) do
      [title: title] -> title
      nil -> "Untitled Document"
    end
  end

Finally, let's use this when parsing the document to extract the title:

  def render_string(string) do
    body = string |> Markdown.to_html
    MarkdownServer.RenderedDocument[body: body, title: title_for(body)]
  end

Run the tests, and they pass. With this, we have a record to store our concept of a rendered document, and we have a module that can parse a string and return a RenderedDocument record.

Before we move on, let's extract the Renderer into his own file. We'll pull it out of the test file and write it to lib/markdown_server/renderer.ex. Run the tests once more just to make sure everything's still kosher.

Alright, so our goal is to serve these up via a webserver. Might as well pull one in. I've been enjoying trying out different libraries, so we're going to have a look at a new webserver from Chris McCord called phoenix. Open up mix.exs and add the dependency:

  defp deps do
    [
      { :ex_doc, github: "elixir-lang/ex_doc" },
      { :phoenix, github: "chrismccord/phoenix" }
    ]
  end

Fetch it with mix deps.get. Alright, so I looked over the Phoenix documentation, and we need to start out by building a Router module. Open up lib/markdown_server/router.ex and let's get a basic router in place with a single route defined:

defmodule MarkdownServer.Router do
  use Phoenix.Router, port: 4000

  get "pages/:page", MarkdownServer.PagesController, :show, as: :page
end

This is pretty straightforward, but there's no PagesController yet. Let's go ahead and create it in this file:

defmodule MarkdownServer.PagesController do
  use Phoenix.Controller
  alias MarkdownServer.Renderer

  def show(conn) do
    document = Renderer.render_string("this is a doc")
    html(conn, html_for(document))
  end

  defp html_for(rendered_document) do
    """
    <html>
      <head>
        <title>#{rendered_document.title}</title>
      </head>
      <body>
        #{rendered_document.body}
      </body>
    </html>
    """
  end
end

Now let's start the webserver up. Fire up an iex with iex -S mix.

MarkdownServer.Router.start

Fire up a browser and visit http://localhost:4000/pages/foo and you should see our fake rendered doc.

Summary

In today's episode, we built a module to render a markdown file and explored the new Phoenix webserver. In the next episode, we'll allow passing in the source directory for the markdown files and we'll look at doing some more interesting things like styling it up a bit and building an index page. See you soon!

Resources