defmodule ContentfulRenderer do require Logger @doc """ Renders a Contentful node, in a tree of `Map`s and `List`s, to HTML. It accepts custom renderers for each node type, passed in as `Keyword`s in the `options` parameter. To override the `heading-1` renderer, pass in: ``` heading_1_node_renderer: fn node, options -> "

\#{ContentfulRenderer.render_content(node, options)\}

" end ``` By default it renders `embedded-entry-inline` and `embedded-entry-block` as blank, because these usually require local knowledge about the content model to render usefully. When this happens, `Logger` will warn. ## Examples ``` iex> %{ ...> :content => [ ...> %{ ...> :content => [ ...> %{ ...> :data => %{}, ...> :marks => [], ...> :nodeType => "text", ...> :value => "Paragraph 1" ...> } ...> ], ...> :data => %{}, ...> :nodeType => "paragraph" ...> }, ...> %{ ...> :content => [ ...> %{ ...> :data => %{}, ...> :marks => [], ...> :nodeType => "text", ...> :value => "Paragraph 2" ...> } ...> ], ...> :data => %{}, ...> :nodeType => "paragraph" ...> } ...> ], ...> :data => %{}, ...> :nodeType => "document" ...> } ...> |> ContentfulRenderer.render() "

Paragraph 1

Paragraph 2

" ``` """ def render(node, options \\ []) def render(content, options) when is_list(content) do content |> Enum.map(&render(&1, options)) |> Enum.join("") end def render(%{nodeType: nodeType} = node, options) do renderer = case nodeType do "document" -> &render_content/2 "paragraph" -> Keyword.get(options, :paragraph_node_renderer, &default_paragraph_node_renderer/2) "text" -> Keyword.get(options, :text_node_renderer, &default_text_node_renderer/2) "heading-1" -> Keyword.get(options, :heading_1_node_renderer, &default_heading_1_node_renderer/2) "heading-2" -> Keyword.get(options, :heading_2_node_renderer, &default_heading_2_node_renderer/2) "heading-3" -> Keyword.get(options, :heading_3_node_renderer, &default_heading_3_node_renderer/2) "heading-4" -> Keyword.get(options, :heading_4_node_renderer, &default_heading_4_node_renderer/2) "heading-5" -> Keyword.get(options, :heading_5_node_renderer, &default_heading_5_node_renderer/2) "heading-6" -> Keyword.get(options, :heading_6_node_renderer, &default_heading_6_node_renderer/2) "unordered-list" -> Keyword.get( options, :unordered_list_node_renderer, &default_unordered_list_node_renderer/2 ) "ordered-list" -> Keyword.get( options, :ordered_list_node_renderer, &default_ordered_list_node_renderer/2 ) "list-item" -> Keyword.get( options, :list_item_node_renderer, &default_list_item_node_renderer/2 ) "hyperlink" -> Keyword.get(options, :hyperlink_node_renderer, &default_hyperlink_node_renderer/2) "embedded-entry-inline" -> Keyword.get( options, :embedded_entry_inline_node_renderer, &default_embedded_entry_inline_node_renderer/2 ) "embedded-entry-block" -> Keyword.get( options, :embedded_entry_block_node_renderer, &default_embedded_entry_block_node_renderer/2 ) end renderer.(node, options) end @doc """ Renders the content inside a node. Useful if you've overriden a renderer, but you don't want to replement how the renderer walks into the content. """ def render_content(node, options \\ []) do node |> Map.get(:content, []) |> render(options) end defp default_paragraph_node_renderer(node, options) do "

#{render_content(node, options)}

" end defp default_heading_1_node_renderer(node, options) do "

#{render_content(node, options)}

" end defp default_heading_2_node_renderer(node, options) do "

#{render_content(node, options)}

" end defp default_heading_3_node_renderer(node, options) do "

#{render_content(node, options)}

" end defp default_heading_4_node_renderer(node, options) do "

#{render_content(node, options)}

" end defp default_heading_5_node_renderer(node, options) do "
#{render_content(node, options)}
" end defp default_heading_6_node_renderer(node, options) do "
#{render_content(node, options)}
" end defp default_unordered_list_node_renderer(node, options) do "" end defp default_ordered_list_node_renderer(node, options) do "
    #{render_content(node, options)}
" end defp default_list_item_node_renderer(node, options) do "
  • #{render_content(node, options)}
  • " end defp default_hyperlink_node_renderer(node, options) do uri = node[:data][:uri] "#{render_content(node, options)}" end defp default_embedded_entry_inline_node_renderer(_node, _options) do Logger.warn("Using null renderer for embedded-entry-inline node") "" end defp default_embedded_entry_block_node_renderer(_node, _options) do Logger.warn("Using null renderer for embedded-entry-block node") "" end defp default_text_node_renderer(node, options) do text = Map.fetch!(node, :value) |> Phoenix.HTML.html_escape() |> Phoenix.HTML.safe_to_string() node |> Map.get(:marks, []) |> render_marks(text, options) end defp default_bold_mark_renderer(text, _options) do "#{text}" end defp default_underline_mark_renderer(text, _options) do "#{text}" end defp default_code_mark_renderer(text, _options) do "#{text}" end defp default_italic_mark_renderer(text, _options) do "#{text}" end defp render_marks(marks, text, options) do marks |> Enum.reduce( text, fn mark, text_acc -> type = Map.fetch!(mark, :type) case type do "bold" -> default_bold_mark_renderer(text_acc, options) "underline" -> default_underline_mark_renderer(text_acc, options) "code" -> default_code_mark_renderer(text_acc, options) "italic" -> default_italic_mark_renderer(text_acc, options) end end ) end end