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. ![]()
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
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
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.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance










