DEV Community

Tjaco Oostdijk for Kabisa Software Artisans

Posted on • Originally published at kabisa.nl on

Rendering Markdown in Elixir

Recently, I acquired the domain realworldphoenix.com in order to continue my blogging adventure on a separate blog dedicated to writing exclusive content on Phoenix and Elixir (and some related subjects occassionally). I could of course take an off the shelf blogging platform or just use some kind of static site generator. But as I am writing about using Phoenix in the Real World, I figured why not just create a Phoenix application to host my blog. Then I can also do some nice things like make it a bit interactive or even let people sign-up and comment without using a third-party tool for that. We’ll see.

Writing content for a blog.

As most of you would probably agree, writing long-form content using html is a bit of a pain. So most people use markdown to write blogposts and I’m no exception. So let’s find out how we can write markdown, put it under version control (just because that is nice to have) and then make sure it gets rendered as html in our Phoenix app.

Earmark

The go-to way to convert markdown to html in Elixir land is the great library written by Dave Thomas, Earmark. It is used by ex_doc and is a nice and well maintained library. We could of course convert our markdown in our phoenix controller directly using Earmark and output that in our view, but we can do even better. We can make sure that our Phoenix app can actually render markdown files stored in our template directly to html, just like it render’s eex to html.

Phoenix Markdown Library

In fact there is even a library that handles this for us: Phoenix Markdown. Now I can just use this and be done with it (and as you are reading this… I am actually already using it ;) ), but my curious mind wants to know more! I’d like to know how this mechanism works so I have a better understanding of how rendering works in Phoenix. So, without further ado, let’s crack this baby open and take a look inside. Ooohhh what is that….?

It’s a Template Engine!

Ofcourse, Phoenix has this all figured out. The way that eex and exs are implemented is as Phoenix Template Engines. It is fairly straightforward to create your own Template Engine by implementing the Phoenix.Template.Engine behaviour. This is exactly what Boyd Multerer has created in his library phoenix_markdown. Does that name sound familiar? Oh, that’s because boyd was also the creator of the awesome Scenic Library. Great stuff!

How do we create a Template Engine?

So how does one go about and create a template engine? Well, the best way to find out is to see if we can create one for ourselves, right? So let’s create a template engine that scrambles all the text that gets rendered by the engine. We’ll use the Cambridge University Scrambled Text concept. So scramble all letters in words but keep the first and last letters intact. Fun fact, this so-called research was actually never done at Cambridge University! Somehow this internet meme got morphed into being a Cambridge research subject, but was actually never the case. Here’s a nice article from a Cambridge Professor about this.This however doesn’t hold us back from using the concept to build our awesome scramble engine! So let’s get cakrcnig!

Our scramble Engine

If you want to follow along you can create a fresh phoenix project and put the files in there to test this out yourself.We’ll define our engine in lib/engine/scrambled.ex. The Phoenix.Template.Engine behaviour requires us to implement compile/2. In there we can add our magic formula. :)

This is what I came up with for our use case:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

defmodule RealWorldPhoenix.Engines.Scrambled do
  @moduledoc false

  @behaviour Phoenix.Template.Engine

  def compile(path, _name) do
    path
    |> File.read!()
    |> scramble_words()
    |> Earmark.as_html!()
    |> EEx.compile_string(engine: Phoenix.HTML.Engine, file: path, line: 1)
  end

  defp scramble_words(content) do
    content
    |> String.split("\n", trim: true)
    |> Enum.map(&shuffle_words_in_sentence/1)
    |> Enum.join("\n")
  end

  defp shuffle_words_in_sentence(sentence) do
    sentence
    |> String.split(" ")
    |> Enum.map(&shuffle/1)
    |> Enum.join(" ")
  end

  defp shuffle(word) do
    if String.length(word) < 4 do
      word
    else
      scrambled =
        word
        |> String.split("", trim: true)
        |> Enum.slice(1..-2)
        |> Enum.shuffle()
        |> Enum.join("")

      String.first(word) <> scrambled <> String.last(word)
    end
  end
end

Enter fullscreen mode Exit fullscreen mode

And to use this in our Phoenix app, we need to configure the file extension we want to invoke scrambling for. You can see this is the same as I did in my app to configure phoenix_markdown. At compile time Phoenix will match any templates with the .scrambled extension and will run that source file through our Engine by calling our compile/2 function defined in our Engine module.


1
2
3
4
5

# config/config.exs

config :phoenix, :template_engines,
  md: PhoenixMarkdown.Engine,
  scrambled: RealWorldPhoenix.Engines.Scrambled

Enter fullscreen mode Exit fullscreen mode

So I am expecting this to scramble content written in markdown. I’m just considering paragraphs of text, just to not complicate things unnecessarily for this use case. That is why I am basically splitting sentences by splitting on the newline character and then processing each sentence and the containing words before joining them together again with newlines. Seems pretty straightforward. Let’s see if this works!

Smartypants… 🤔

Phoenix Markdown has an option to render server_tags, which basically means that you can invoke Elixir inside your markdown page using the standard tags you use in eex templates to invoke some Elixir code and evaluate it. I wanted to use this to render the following bit of text to illustrate my scrambled engine, but the first pass led to this error while compiling:


1
2
3
4
5
6
7
8
9

== Compilation error in file lib/real_world_phoenix_web/views/post_view.ex ==
** (SyntaxError) lib/real_world_phoenix_web/templates/post/blogs/2020-01-28/rendering_markdown.html.md:60: unexpected token: "“" (column 39, code point U+201C)
    (eex) lib/eex/compiler.ex:45: EEx.Compiler.generate_buffer/4
    (phoenix) lib/phoenix/template.ex:361: Phoenix.Template.compile/3
    (phoenix) lib/phoenix/template.ex:167: anonymous fn/4 in Phoenix.Template."MACRO- __before_compile__"/2
    (elixir) lib/enum.ex:1948: Enum."-reduce/3-lists^foldl/2-0-"/3
    (phoenix) expanding macro: Phoenix.Template. __before_compile__ /1
    lib/real_world_phoenix_web/views/post_view.ex:1: RealWorldPhoenixWeb.PostView (module)
    (elixir) lib/kernel/parallel_compiler.ex:229: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7

Enter fullscreen mode Exit fullscreen mode

What? 😕🤷

Ok, so what is happening here. It is seeing an unexpected token and it seems to be a left handed double quote. After banging my head against the wall for about a day, I decided to rtfm again and luckily Boyd has us covered. See here, and I’ll quote it below:

By default Earmark replaces some characters with prettier UTF-8 versions. For example, single and double quotes are replaced with left- and right-handed versions. This may break any server tag which contains a prettified character since EEx cannot interpret them as intended. To fix this, disable smartypants processing.

So it is actually a simple fix in our config:


1
2
3

config :phoenix_markdown, :earmark, %{
  smartypants: false
}

Enter fullscreen mode Exit fullscreen mode

Interactive blog…

The example below is rendered using LiveView. You can see this if you head over to my interactive blog.


1

<%= Phoenix.LiveView.Helpers.live_render(@conn, RealWorldPhoenixWeb.Live.Scrambled) %>

Enter fullscreen mode Exit fullscreen mode

And here is the LiveView component responsible for rendering the scrambled text:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

defmodule RealWorldPhoenixWeb.Live.Scrambled do
  use Phoenix.LiveView
  @moduledoc false

  def render(assigns) do
    ~L"""
      <h2 id="scramble">Let's Scramble!
        <button phx-click="scramble" class="button">
          <%= if @scrambled, do: "un-scramble", else: "scramble" %>
        </button>
      </h2>
      <%= get_scrambled_content(@scrambled) %>
    """
  end

  def mount(_session, socket) do
    {:ok, assign(socket, :scrambled, true)}
  end

  def handle_event("scramble", _value, socket) do
    {:noreply, assign(socket, :scrambled, !socket.assigns.scrambled)}
  end

  defp get_scrambled_content(true) do
    Phoenix.View.render(RealWorldPhoenixWeb.PostView, "blogs/2020-01-28/_scrambled.html", [])
  end

  defp get_scrambled_content(false) do
    Phoenix.View.render(RealWorldPhoenixWeb.PostView, "blogs/2020-01-28/_notscrambled.html", [])
  end
end

Enter fullscreen mode Exit fullscreen mode

Hold on buddy! This can’t work!

The above LiveView renders scrambled text and when the button is pressed it unscrambles it.

Ah I was wondering if you would notice. You smartypants out there probably were already wondering how in the world I get this scrambling functionality working. No, I’m not fooling you, it is running though our actual template engine. But indeed Phoenix pre-compiles the templates. That is also one of the reasons it is so fast.

So it is not running through our template when you press that button. I confess. I have two versions of the same file it switches between, one with the .scrambled extension and one without that are both precompiled by Phoenix when the app gets compiled. You will notice that the scrambling is also the same if you switch back and forth. That is because, once compiled, it does not recompile necessarily.

Conclusion

Although my Scrambled Engine might not be so useful, I think it does illustrate how easy it can be to create a Template Engine that does something small and works pretty quickly out of the box.

Be sure to check out the Core Example EEx Engine if you want to dive in some more. And be sure to read the awesome docs about templating in Phoenix!

I hope this was helpful and that you learned something new.

Until next time!

Top comments (0)