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

vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
pgiesin
This should be a simple problem but I just can’t seem to figure it out. I have a standalone Elixir app that won’t find the database. Dep...
New
Werner
Hi, I’m using Ubuntu 18.04 and after updating to OTP-24.0 yesterday i have this warning when I run “mix local.hex”: 14:57:30.512 [warn] ...
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
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New

Other popular topics Top

JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1140 51847 244
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
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
dokuzbir
Hello, I am trying to convert my lists to string without losing brackets.For start i have 3 map. They look like these buyer = %{ id: ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 42633 214
New
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
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
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
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

We're in Beta

About us Mission Statement