Before you go, check out these stories!

0
Hackernoon logoWriting a Blog Engine in Phoenix and Elixir: Part 6, Markdown Support by@diamondgfx

Writing a Blog Engine in Phoenix and Elixir: Part 6, Markdown Support

Author profile picture

@diamondgfxBrandon Richey

Latest Update: 08/02/2016

Previous Post in thisย series

Part 5: Adding ExMachina

Current Versions

As of the time of writing this, the current versions of our applications are:

  • Elixir: v1.3.1
  • Phoenix: v1.2.0
  • Ecto: v2.0.2
  • ExMachina: v1.0.2
  • Earmark: 1.0.1

If you are reading this and these are not the latest, let me know and Iโ€™ll update this tutorial accordingly.

Where We Leftย Off

Weโ€™ve now added roles, users, and posts to our blogging platform, fixed up some bugs, upgraded some dependencies, and fixed our seeds. Our platform is really nice and stable and has a good set of base functionality. If you havenโ€™t been following along or missed a step along the way, you can continue with the โ€œ11092015โ€ branch of https://github.com/Diamond/pxblog.

Implementing Markdownย support

Given that weโ€™re tech people building a blog, one that we may or may not be using, we should probably allow people to write posts in something other than plaintext support. Markdown happens to be a language that works great for blogging and is really simple to support in Elixir, so letโ€™s implement Markdown support using a third party library called Earmark. We start off simply by adding a dependency to our application in the deps function in mix.exs:

{:earmark, "~> 1.0.1"}

And then weโ€™ll run mix do deps.get, compile to make sure we added it successfully to our application. If all goes well, we can move right on to adding support into our templates and views.

Updating ourย views

Weโ€™ll start by providing a convenient method to convert a post into Markdown. We donโ€™t want to have to write Earmark.to_html(data) all over the place, and if we ever decide to support any of Earmarkโ€™s other features and options, weโ€™d have to rewrite that code over and over. Instead, weโ€™ll implement a function in web/views/post_view.ex that will give us a single place to modify if we ever want to change that. Open that file up and add the following function:

def markdown(body) do
body
|> Earmark.to_html
|> raw
end

We have the input text, body, that weโ€™re piping into Earmarkโ€™s โ€œto_htmlโ€ function. Since weโ€™re expecting html out that we can place into our post, so weโ€™ll use raw here. I know what youโ€™re thinking: โ€œyikes, isnโ€™t raw terribly unsafe?โ€ The answer is yes, it absolutely is. Trouble is, we need to output in raw html from Earmark, so we need to get this data rendered. We also donโ€™t want to end up having to strip the same tags every single time we render this out, so letโ€™s go into web/models/post.ex and add some code that will strip it out:

def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:title, :body])
|> validate_required([:title, :body])
|> strip_unsafe_body(params)
end

defp strip_unsafe_body(model, %{"body" => nil}) do
model
end

defp strip_unsafe_body(model, %{"body" => body}) do
{:safe, clean_body} = Phoenix.HTML.html_escape(body)
model |> put_change(:body, clean_body)
end

defp strip_unsafe_body(model, _) do
model
end

We added a decent amount of code here, so letโ€™s discuss what weโ€™re doing. The first change we made is at the top, in our changeset function. We added a new call, strip_unsafe_body(model,ย โ€ฆ). This will pattern match into one of three possible functions. The first includes a body parameter that is nil; the second is the method that includes a body parameter which we have to strip it out, and the third is a final catch-all.

We use the put_change function to replace the body change with a stripped version, which calls out to Phoenix.HTMLโ€™s โ€œhtml_escapeโ€ function. This function takes in text, and then returns out a tuple of {:safe, cleaned_up_body} on success.

Writing this code with no tests makes me queasy, especially with what weโ€™re doing here, so letโ€™s test it!

Writing tests for our codeย stripper

The first thing weโ€™re going to need to do, since weโ€™re depending on these model actions to keep our code safe, is make sure theyโ€™re tested appropriately!

defmodule Pxblog.PostTest do
use Pxblog.ModelCase

alias Pxblog.Post

@valid_attrs %{body: "some content", title: "some content"}
@invalid_attrs %{}

test "changeset with valid attributes" do
changeset = Post.changeset(%Post{}, @valid_attrs)
assert changeset.valid?
end

test "changeset with invalid attributes" do
changeset = Post.changeset(%Post{}, @invalid_attrs)
refute changeset.valid?
end

test "when the body includes a script tag" do
changeset = Post.changeset(%Post{}, %{@valid_attrs | body: "Hello <script type='javascript'>alert('foo');</script>"})
refute String.match? get_change(changeset, :body), ~r{<script>}
end

test "when the body includes an iframe tag" do
changeset = Post.changeset(%Post{}, %{@valid_attrs | body: "Hello <iframe src='http://google.com'></iframe>"})
refute String.match? get_change(changeset, :body), ~r{<iframe>}
end

test "body includes no stripped tags" do
changeset = Post.changeset(%Post{}, @valid_attrs)
assert get_change(changeset, :body) == @valid_attrs[:body]
end
end

The first thing we have to modify is our @valid_attrs declaration at the top. When we receive data from the controller itโ€™s going to be in the form of string-based keys, not atoms, and it will cause our tests to fail unless we modify that.

Next, we import in Ecto.Changeset, but only the get_change/2 function, which we need to pull the translated value out of the changeset. Finally, we write four tests:

  1. When our body has a script tag in it
  2. When our body has an iframe tag in it
  3. When our body has none of the invalid tags in it

Re-run our test suite and we should be green. We have one more test to write and then some a few basic UI changes and then we are complete!

Writing our tests for the Markdownย helper

We should also write a quick test for our markdown helper function that we wrote in our Post View module. The good news is this should be a very simple test to write.

Create test/view/post_view_test.exs and fill it with the following:

defmodule Pxblog.PostViewTest do
use Pxblog.ConnCase, async: true

test "converts markdown to html" do
{:safe, result} = Pxblog.PostView.markdown("**bold me**")
assert String.contains? result, "<strong>bold me</strong>"
end

test "leaves text with no markdown alone" do
{:safe, result} = Pxblog.PostView.markdown("leave me alone")
assert String.contains? result, "leave me alone"
end
end

Run our tests, verify that all is green, and weโ€™re done with updating our tests for this new functionality!

Updating ourย UI

Right now, we have the ability to write up full posts but the UI doesnโ€™t really support us editing anything larger than a single line. Weโ€™ll modify the standard form to include a text area instead of a textbox and weโ€™ll also give it a shiny, better editor.

Open up web/templates/post/form.html.eex and add an id parameter and give it an id of โ€œbody-editorโ€. The finished line should look like this:

<%= textarea f, :body, class: "form-control", id: "body-editor" %>

Finally, add the following code to the bottom of your formโ€™s template (web/templates/post/form.html.eex):

<link rel="stylesheet" href="//cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">
<script src="//cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
<script>var simplemde = new SimpleMDE();</script>

Reload our UI and we should see a very clean interface for writing new posts! This is starting to look like a proper blogging platform and everything!

Finally, open up web/templates/post/show.html.eex and change the line that displays the postโ€™s body and change it to use the markdown(body) function we defined in the view.

<%= markdown(@post.body) %>

Conclusion

We now have a better means of editing/formatting our posts and the editor looks much, much nicer! Weโ€™ll probably also want to fix up the layout at some point, and there still isnโ€™t any way to add comments, but commenting actually would be a fun way to implement support for channels, so weโ€™ll hold off for now until weโ€™re ready to build that support in!

If you want to see the completed version of this code, just grab the โ€œadd_markdown_supportโ€ branch of our repo.

Next Post In Thisย Series

Writing A Blog Engine In Phoenix And Elixir: Part 7, Adding Comments Support

Check out my newย book!

Hey everyone! If you liked what you read here and want to learn more with me, check out my new book on Elixir and Phoenix web development:

Iโ€™m really excited to finally be bringing this project to the world! Itโ€™s written in the same style as my other tutorials where we will be building the scaffold of a full project from start to finish, even covering some of the trickier topics like file uploads, Twitter/Google OAuth logins, and APIs!

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.