defmodule GemtextToHTML do @moduledoc """ Render Gemtext as HTML. Phoenix function components are used to transform individual Gemtext elements to HTML. Simply implement the `GemtextToHTML.Components` behaviour and pass it to the `:components` option of `render/2`. ## Example gemtext = \"\"\" # Hello, world * this is a list * indeed pretty neat \"\"\" GemtextToHTML.render_to_string(gemtext, components: GemtextToHTML.DefaultComponents) # => "
pretty neat
" """ @default_opts %{ components: GemtextToHTML.DefaultComponents } @initial_state %{ prev_el: nil, lis: [] } @doc """ Returns a safe iolist of HTML rendered from a gemtext document. ## Options - `:components` - the `GemtextToHTML.Components` callback module used to render each element; defaults to `GemtextToHTML.DefaultComponents` Any additional options may be specified, which will be passed to the components as a map in the `@opts` assign. """ @spec render(String.t(), keyword) :: {:safe, iolist()} def render(gemtext, opts \\ []) do {:safe, render_to_iolist(gemtext, opts)} end @doc """ Returns an iolist of HTML rendered from a gemtext document. See `render/2` for options. """ @spec render_to_iolist(String.t(), keyword) :: iolist() def render_to_iolist(gemtext, opts \\ []) do opts = Map.merge(@default_opts, Map.new(opts)) gemtext |> Gemtext.parse() |> run(opts) end @doc """ Returns a string of HTML rendered from a gemtext document. See `render/2` for options. """ @spec render_to_string(String.t(), keyword) :: String.t() def render_to_string(gemtext, opts \\ []) do gemtext |> render_to_iolist(opts) |> :erlang.iolist_to_binary() end defp run(nodes, opts, state \\ @initial_state, rendered \\ []) defp run([], _opts, _state, rendered) do rendered |> Enum.reverse() |> Enum.map(fn rendered -> rendered |> Phoenix.HTML.html_escape() |> then(fn {:safe, iodata} -> iodata end) end) end # Render the list after the final sequential list item defp run([el | _] = rest, opts, %{prev_el: :li} = state, rendered) when elem(el, 0) != :li do rendered = [opts.components.list(%{items: Enum.reverse(state.lis), opts: opts}) | rendered] run(rest, opts, %{state | prev_el: nil, lis: []}, rendered) end # Render basic text elements for simple_element <- [:h1, :h2, :h3, :quote, :text, :pre] do defp run([{unquote(simple_element), text} | rest], opts, state, rendered) do rendered = [ opts.components.unquote(simple_element)(%{text: trim(text), opts: opts}) | rendered ] run_next(unquote(simple_element), rest, opts, state, rendered) end end # Render a link defp run([{:link, %{to: to}, text} | rest], opts, state, rendered) do rendered = [opts.components.link(%{text: trim(text), to: to, opts: opts}) | rendered] run_next(:link, rest, opts, state, rendered) end # Render preformatted text with a title defp run([{:pre, %{title: title}, text} | rest], opts, state, rendered) do rendered = [opts.components.pre(%{text: trim(text), title: title, opts: opts}) | rendered] run_next(:pre, rest, opts, state, rendered) end # Accumulate a list item (don't render anything yet) defp run([{:li, text} | rest], opts, state, rendered) do run_next(:li, rest, opts, %{state | lis: [text | state.lis]}, rendered) end # Ignore anything weird defp run([unexpected | rest], opts, state, rendered) do run_next(elem(unexpected, 0), rest, opts, state, rendered) end defp run_next(el, rest, opts, state, rendered) do run(rest, opts, %{state | prev_el: el}, rendered) end defp trim(text) when is_binary(text), do: String.trim(text) defp trim(value), do: value end