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.

https://github.com/dashbitco/nimble_publisher

Where Next?

Popular in Questions Top

aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a &gt; b) do {:ok, "a"} end if (a &lt; b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
dotdotdotPaul
Okay, I’m having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I’m sure I’...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

Other popular topics Top

malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
AstonJ
We’ve put together this wiki for Phoenix LiveView - please feel free to add any info you feel is worth including. What is Phoenix LiveV...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
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
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New

Latest on Elixir Forum

We're in Beta

About us Mission Statement