Style formatting for nested elements with earmark or earmark_parser


so I started to integrate markdown editing in my app and I ended up integrating earmark, which works fine for me.

Currently I have a utility function that I can call everywhere in my LiveViews in order to generate html from markdown.

Here how his looks ( I reuse styles, defined in my CoreComponents):

@doc """
  parse markdown to styled html

  def markdown(nil), do: nil

  def markdown(content) do
    case Earmark.Parser.as_ast(content) do
      {:ok, ast, _warnings} ->
        |> parse_ast()
        |> Earmark.Transform.transform()

      {:error, _ast, warnings} ->
        {:warning, _id, msg} = List.first(warnings)
        "Invalid markup: #{msg}"

  defp parse_ast(ast) do
    |> Earmark.Transform.map_ast(fn node -> parse_node(node) end, ignore_strings: true)
    |> List.flatten()

  defp parse_node({_tag, _attrs, _children_or_content, _meta} = node) do
    add = fn class_list ->
      fn node -> Earmark.AstTools.merge_atts_in_node(node, class: Enum.join(class_list, " ")) end

    processors = [
      {"h1", add.(PortalWeb.CoreComponents.typography(:h1))},
      {"h2", add.(PortalWeb.CoreComponents.typography(:h2))},
      {"h3", add.(PortalWeb.CoreComponents.typography(:h3))},
      {"h4", add.(PortalWeb.CoreComponents.typography(:h4))},
      {"p", add.(PortalWeb.CoreComponents.typography(:p))},
      {"ul", add.(PortalWeb.CoreComponents.typography(:ul))},
      {"ol", add.(PortalWeb.CoreComponents.typography(:ol))},
      {"li", add.(PortalWeb.CoreComponents.typography(:li))},
      {"a", add.(PortalWeb.CoreComponents.typography(:a))},
      {"strong", add.(PortalWeb.CoreComponents.typography(:strong))},
      {"em", add.(PortalWeb.CoreComponents.typography(:em))},
      {"u", add.(PortalWeb.CoreComponents.typography(:u))},
      {"img", add.(PortalWeb.CoreComponents.typography(:img))},
      {"blockquote", add.(PortalWeb.CoreComponents.typography(:blockquote))},
      {"hr", add.(PortalWeb.CoreComponents.typography(:hr))},
      {"code", add.(PortalWeb.CoreComponents.typography(:code))},
      {"pre", add.(PortalWeb.CoreComponents.typography(:pre))}

    postprocessor =
      Earmark.Options.make_options!(registered_processors: processors)
      |> Earmark.Transform.make_postprocessor()


No I have two questions:

  1. I ran into the thing, that earmark_parser was extracted from earmark. So if I only use earmark_parser - how do I generate html markup from the generated ast ? With floki? How is that done.
  2. Also I struggle with styling nested elements like lists in list. Has anyone a good example how that can be achieved? Currently I only can add classes to every
  3. element but cannot differntiate if this is a list of 1st or second level… (don’t want to do that via css but with tailwind classes…)

Any advice appreciated :slight_smile:

… also how is such a cool editor done like this one I am typing in…

It is back in Earmark so you should not have any problem. No it is not floki but homemade. Transform.transform

I am no frontend guy by any means, therefore I do not know what exactly is needed, but seems you are doing fine above, no?

As I am not maintaining Earmark I cannot change the code which might be a little bit too complicated but basically use

   something = Proxy.as_ast(markdown)
   modified = do_something_with(something)

Of course you can using the postprocessor as above if it is enough


Thank you very much for your reply!

well basically, when a generated list with a list within looks like that:

  <li class="list-level-1">
      <li class="list-level-2"></li>

But what I was not able to figure out, is, how can I be aware during Transformation (when preprocessors are added) if the list-item is of level 1 or 2…

… probably I can’t… ?

There is no built in way to do this easily as far as I remember, the idea is that you need to transform the ast yourself, here is a draft of how this should work

    defp add_level_att(tag, atts, inner, meta, level) do
        Keyword.put(atts, :class, "list-level-#{level}"),
        recursive_map(inner, level+1),

    defp recursive_map(ast, level)
    defp recursive_map(list, level) when is_list(list), do:, &recursive_map(&1, level))
    defp recursive_map(leaf, _) when is_binary(leaf), do: leaf
    defp recursive_map({"ol", atts, inner, meta}, level), do: add_level_att("ol", atts, inner, meta, level)
    defp recursive_map({"ul", atts, inner, meta}, level), do: add_level_att("ul", atts, inner, meta, level)
    defp recursive_map({tag, atts, inner, meta}, level), do: {tag, atts, recursive_map(inner, level), meta}

1 Like