lib/notionex/renderer/html_renderer.ex

defmodule Notionex.Renderer.HTMLRenderer do
  @behaviour Notionex.Renderer

  alias Notionex.Object.{Block, List}

  @impl true
  def render_block(%List{object: "list", type: "block", results: results}) do
    results
    |> Enum.reduce([], fn block, acc ->
      updated_block = update_numbered_list_item_number(block, Enum.at(acc, 0))
      [updated_block | acc]
    end)
    |> Enum.reverse()
    |> Enum.map(fn b -> render_block(b) end)
    |> Enum.join("<br />")
  end

  def render_block(%Block{object: "block", type: "paragraph", paragraph: paragraph}) do
    paragraph
    |> render_rich_text()
    |> then(&"<p>#{&1}</p>")
  end

  def render_block(%Block{object: "block", type: "heading_1", heading_1: heading_1}) do
    heading_1
    |> render_rich_text()
    |> then(&"<h1>#{&1}</h1>")
  end

  def render_block(%Block{object: "block", type: "heading_2", heading_2: heading_2}) do
    heading_2
    |> render_rich_text()
    |> then(&"<h3>#{&1}</h3>")
  end

  def render_block(%Block{object: "block", type: "heading_3", heading_3: heading_3}) do
    heading_3
    |> render_rich_text()
    |> then(&"<h5>#{&1}</h5>")
  end

  def render_block(%Block{
        object: "block",
        type: "numbered_list_item",
        numbered_list_item: numbered_list_item,
        numbered_list_item_number: numbered_list_item_number
      }) do
    numbered_list_item
    |> render_rich_text()
    # TODO: Wrap within <li> and let HTML generate the numbers
    |> then(&"#{numbered_list_item_number}. #{&1}")
    |> then(&"<ol>#{&1}</ol>")
  end

  def render_block(%Block{
        object: "block",
        type: "bulleted_list_item",
        bulleted_list_item: bullet_list_item
      }) do
    bullet_list_item
    |> render_rich_text()
    |> then(&"<ul><li>#{&1}</li></ul>")
  end

  def render_block(%Block{object: "block", type: "code", code: code}) do
    code
    |> render_rich_text()
    |> then(&"<pre><code>#{&1}</code></pre>")
  end

  # TODO: Handle caption
  def render_block(%Block{object: "block", type: "video", video: video}) do
    case Map.get(video, "type") do
      "external" ->
        video
        |> get_in(["external", "url"])
        |> then(&"<iframe src=\"#{&1}\" frameborder=\"0\" allowfullscreen></iframe>")

      _ ->
        raise "Block type video, not implemented type: #{Map.get(video, "type")}"
    end
  end

  # TODO: Handle caption
  def render_block(%Block{object: "block", type: "image", image: image}) do
    case Map.get(image, "type") do
      "external" ->
        image
        |> get_in(["external", "url"])
        |> then(&"<img src=\"#{&1}\" />")

      "file" ->
        image
        |> get_in(["file", "url"])
        |> then(&"<img src=\"#{&1}\" />")

      _ ->
        raise "Block type image, not implemented type: #{Map.get(image, "type")}"
    end
  end

  # TODO: Handle caption
  def render_block(%Block{object: "block", type: "bookmark", bookmark: bookmark}) do
    bookmark
    |> Map.get("url")
    |> then(&"<a href=\"#{&1}\">#{&1}</a>")
  end

  # Not sure what to do with this
  def render_block(%Block{object: "block", type: "column_list"}) do
    ""
  end

  def render_block(%Block{object: "block", type: "quote", quote: blockquote}) do
    blockquote
    |> render_rich_text()
    |> then(&"<blockquote>#{&1}</blockquote>")
  end

  def render_block(%Block{object: "block", type: "embed", embed: embed}) do
    "<iframe width=\"560px\" height=\"315px\" src=\"#{Map.get(embed, "url")}\" frameborder=\"0\" allowfullscreen></iframe>"
  end

  def render_block(%Block{object: "block", type: type}) do
    raise "Block type not implemented: #{type}"
  end

  @impl true
  def render_rich_text(%{"rich_text" => rich_text}) do
    render_rich_text(rich_text)
  end

  def render_rich_text(rich_text_list) when is_list(rich_text_list) do
    rich_text_list
    |> Enum.map(&render_rich_text/1)
    |> Enum.join("")
  end

  def render_rich_text(%{"type" => "text"} = rich_text) do
    rich_text
    |> apply_annotations()
  end

  def render_rich_text(_, _), do: ""

  def apply_annotations(%{"type" => "text", "annotations" => annotations} = rich_text) do
    content = get_in(rich_text, ["text", "content"])

    annotations
    |> Enum.reduce(content, fn
      {"bold", true}, acc -> "<strong>#{acc}</strong>"
      {"italic", true}, acc -> "<em>#{acc}</em>"
      {"strikethrough", true}, acc -> "<del>#{acc}</del>"
      {"underline", true}, acc -> "<u>#{acc}</u>"
      {"code", true}, acc -> "<code>#{acc}</code>"
      {"color", "default"}, acc -> acc
      _, acc -> acc
    end)
  end

  ## Helpers

  # Current and prev blocks are numbered_list_items
  def update_numbered_list_item_number(
        %Block{type: "numbered_list_item"} = block,
        %Block{
          type: "numbered_list_item",
          numbered_list_item_number: numbered_list_item_number
        } = _prev_block
      ) do
    block
    |> Map.put(:numbered_list_item_number, numbered_list_item_number + 1)
  end

  # Current block is numbered_list_item, prev block is not
  def update_numbered_list_item_number(
        %Block{type: "numbered_list_item"} = block,
        _prev_block
      ) do
    block
    |> Map.put(:numbered_list_item_number, 1)
  end

  def update_numbered_list_item_number(block, _prev_block), do: block
end