neuone

neuone

Earmark - add my own class names to the default set of markdown tags

I’m looking to provide Markdown support for an app, but I want the default markdown to have custom class names and id placed inline. For example:

<h1 class="fw1 headline red">...</h1>

I’m using the library Earmark for the work.
From reading the docs it looks like I can achieve this goal. This is how I set it up, would like some feedback:

defmodule Portal.Blog.Post 

#..omit a lot of code

      defp parse_attr(:body, value) do
        value 
        |> Earmark.as_ast() 
        |> Portal.Blog.Parser.parsing() 
        |> Earmark.as_html!() 
      end

end

I have a module that will parse the body of a post
• Take the value from the body of a blog post
• Pass it to Earmark.as_ast, that returns a list where each item is an HTML node (See Floki)

{tag_name, attributes, children_nodes}
# example
{"p", [{"class", "headline"}], ["Floki"]}

The results from Earmark.as_ast is an {:ok, results, options} and we pass that
to my custom parser.

Portal.Blog.Parser.parsing() will take the list of results:
• map over each item
• pattern match using parse()
• In this example I’m only interested in p, h3 and h1 tags
• every other tag gets a pass through
• Then after each parse() its passed into Earmark.Transform.transform() which convert it into a string
• Then take the list of strings and concat into one

defmodule Portal.Blog.Parser do
    
    def parsing({:ok, results, _option}) do
      Enum.map(results, fn(item) -> 
         parse(item)
         |> Earmark.Transform.transform() 
      end)
      |> Enum.join("")
    end

    # This follows a similar structure to Floki library
    # link here
    # {tag_name, attributes, children_nodes}
    def parse({"p", _attributes, children_nodes }) do
        {"p", [{"class", "fw5 blue"}], children_nodes }
    end

    def parse({"h3", _attributes, children_nodes }) do
        {"h3", [{"class", "f1 fw1 lh-copy"}], children_nodes }
    end

    def parse({"h1", _attributes, children_nodes }) do
      {"h1", [{"class", "fw6"}], children_nodes }
    end

    def parse({"blockquote", _attributes, children_nodes }) do
      {"blockquote", [{"class", "bg-silver"}], children_nodes }
    end

    def parse(item) do
        item
    end

end

Back to my defmodule Portal.Blog.Post:
We then pass the results of Portal.Blog.Parser.parsing() to Earmark.as_html!()

|> Portal.Blog.Parser.parsing() 
|> Earmark.as_html!()

Everything works. :grinning:
And I think this is how to solve it.
My h1, h3 and p tags all have the correct class names.

The only issue I get is warnings like this:

<no file>:14: warning: Failed to find closing <pre>
<no file>:29: warning: Failed to find closing <pre>
<no file>:1: warning: Failed to find closing <p>
<no file>:15: warning: Failed to find closing <pre>
<no file>:1: warning: Failed to find closing <p>

Looking for feedback if this is how someone would approach this, or is there a better way of solving this.

Thanks

Most Liked

neuone

neuone

I have improved my Portal.Blog.Parser to be a bit clearer.

Problem
How do I change the default markdown so I can provide my own css styles inline. The reason for doing this is I have preference to write css styles inline.

Goal
• Custom css styles for the default Markdown.
• Still support HTML attributes that can be added to any block-level element, by using the Kramdown syntax.
<p> tags are wrapping the <img> tags with markdown. Would like the <p> tags removed. So the <img> tags are on its own. This is my preference. Nothing wrong with the default.

defmodule Portal.Blog.Parser do
    
    @moduledoc """
    This module will parse the body of a blog post and update the Markdown
    attributes with custom HTML and css attributes.

    This module is used within the Portal.Blog
    """
    def parsing({:ok, results, _option}) do
      Enum.map(results, fn(item) -> 
         parse(item)
         |> Earmark.Transform.transform() 
      end)
    end

    @doc """
    Customize your own css_styles by 
    providing your own tuple of css attributes
    """

    @css_style %{
      "img" =>  [{"class", "mw8 db"}],
      "p"   =>  [{"class", "mw7 lh-copy"}],
      "h1"  =>  [{"class", "mw7 lh-copy"}],
      "h2"  =>  [{"class", "mw7 lh-copy"}],
      "h3"  =>  [{"class", "mw7 lh-copy"}],
      "ul"  =>  [{"class", "mw7 mb4 lh-copy"}],
      "ol"  =>  [{"class", "mw7 lh-copy"}],
      "blockquote" => [{"class", "mw7 lh-copy"}]
      }


    @doc """
    The Markdown wraps all <img> tags with a <p> tag. This function will patttern match any
    <p> tags that might contain a nested <img> tag.

    Then it will take that <img> node and pass it through with some css style if it exists.

    If no img tag is found the <p> is passed through.
    """
    def parse({"p", attributes, children_nodes} = node) do
        first_child = List.first(children_nodes)
        case first_child do
          {"img", img_attr, img_child_nodes} -> parse({"img", img_attr ++ attributes, img_child_nodes})
          _no_img_tag -> {"p", merge_attributes(attributes, Map.get(@css_style, "p")), children_nodes }
        end
    end

    @doc """
    If the `tag` exists in the @css_style this function will merge the existing attributes with
    the new `css_style` attributes.

    If no `tag` exists in the @css_style the node will just pass through.
    """

    def parse({tag, attributes, children_nodes}) do
      {tag, merge_attributes(attributes, Map.get(@css_style, tag)), children_nodes }
    end

    @doc """
    If the css_style is nil just return the attributes
    """
    def merge_attributes(attributes, css_style) when is_nil(css_style) do
      attributes
    end

    @doc """  
    Will concat two list of tuples into one list. Then will merge
    any tuples that have a similar key value in the first index.

    Given the following params these are the expected results.

    # Params example 1
      attributes  = [{"class", "f1"}]
      css_style   = [{"class", "mw7"}]

    iex > merge_attributes(attributes, css_style)
    iex > [{"class", "f1 mw7"}]

    # Params example 2
      attributes    = [{"class", "f1"}, {"id", "headline"}]
      css_style     = [{"class", "mw7"}, {"id", "red"}]

    iex > merge_attributes(attributes, css_style)
    iex > [{"class", "f1 mw7"}, {"id", "headline red"}]

    """
    def merge_attributes(attributes, css_style) do
      attributes ++ css_style 
      |> merge_attributes
    end

    @doc """  
    With one list of tuples some of the items will have duplicate values in the first index.

    This function will merge the duplicates and return a list of tuples.

    # Params 
      attributes = [{"class", "f1"}, {"class", "mw7"}, {"id", "foo"}, {"title", "something"}, {"id", "header"}]

    iex > merge_attributes(attributes)
    iex > [{"class", "f1 mw7"}, {"id", "foo header"}, {"title", "something"}]
    """
    def merge_attributes(attributes) do
      group = 
        Enum.group_by(attributes, fn({key, _value}) -> key end)

      keys = 
        Map.keys(group)

      results = 
        Enum.map(keys, fn(key) -> 
          Enum.reduce(group[key], "", fn({_key, value}, acc) -> 
            acc <> " " <> value 
            |> String.trim
          end)
        end)

      Enum.zip(keys, results)
    end

end

Solution
This solution is working. Maybe the structure can be improved upon. Still learning.

neuone

neuone

I actually replaced my solution with a new system nimble_publisher.

In the README you have a “Learn More” section that has an excellent tutorial.

Where Next?

Popular in Questions Top

srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
sacepums
Hey guys. I'm new to elixir and im really stocked about it. But I ran into a bit of problem - I need to convert a date sting, for examp...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
gonzofish
I’m currently trying to understand how to join three tables using Ecto. All the examples I’ve seen use 2, so maybe I’m just missing somet...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
jc00ke
Expanding on this topic: https://forum.elixirforum.net/t/map-typespec-question/19217 Let’s say I have a map with required and optional k...
New

Other popular topics Top

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
AstonJ
by Lance Halvorsen Elixir and Phoenix are generating tremendous excitement as an unbeatable platform for building modern web application...
460 27162 124
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New

We're in Beta

About us Mission Statement