defmodule Frenzy.Pipeline.RenderGeminiStage do require Logger alias Frenzy.Pipeline.Stage @behaviour Stage @impl Stage def apply(_opts, %{content: content, content_type: "text/gemini"} = item_params) do html = render_gemini(content) {:ok, %{item_params | content_type: "text/html", content: html}} end def apply(_opts, %{content_type: content_type} = item_params) do Logger.debug("Not rendering Gemini text for item, incorect content type: #{content_type}") {:ok, item_params} end @impl Stage def validate_opts(opts) do {:ok, opts} end @impl Stage def default_opts(), do: %{} def render_gemini(gemini_source) do gemini_source |> Gemini.parse() |> render_lines() |> Floki.raw_html() end @spec render_lines([Gemini.line()], [String.t()]) :: [String.t()] defp render_lines(lines, acc \\ []) defp render_lines([], acc) do Enum.reverse(acc) end defp render_lines([{:text, text} | rest], acc) do render_lines(rest, [{"p", [], [text]} | acc]) end defp render_lines([{:link, uri, text} | rest], acc) do uri_str = URI.to_string(uri) text = if is_nil(text), do: uri_str, else: text a = {"a", [{"href", uri_str}], [text]} p = {"p", [], [a]} render_lines(rest, [p | acc]) end defp render_lines([{:preformatted_start, _alt} | rest], acc) do {preformatted_lines, [:preformatted_end | rest]} = Enum.split_while(rest, fn {:preformatted, _} -> true _ -> false end) pre_text = preformatted_lines |> Enum.map(fn {:preformatted, text} -> text end) |> Enum.join("\n") pre = {"pre", [], pre_text} render_lines(rest, [pre | acc]) end defp render_lines([{:heading, text, level} | rest], acc) do tag = "h#{level}" heading = {tag, [], [text]} render_lines(rest, [heading | acc]) end defp render_lines([{:list_item, _text} | _rest] = lines, acc) do {list_items, rest} = Enum.split_while(lines, fn {:list_item, _} -> true _ -> false end) lis = Enum.map(list_items, fn {:list_item, text} -> {"li", [], [text]} end) ul = {"ul", [], lis} render_lines(rest, [ul | acc]) end defp render_lines([{:quoted, _text} | _rest] = lines, acc) do {quoted_lines, rest} = Enum.split_while(lines, fn {:quoted, _} -> true _ -> false end) ps = Enum.map(quoted_lines, fn {:quoted, text} -> {"p", [], [text]} end) blockquote = {"blockquote", [], ps} render_lines(rest, [blockquote | acc]) end end