lib/hologram/template/embedded_expression_parser.ex

defmodule Hologram.Template.EmbeddedExpressionParser do
  alias Hologram.Compiler.{Context, Transformer}
  alias Hologram.Compiler.Parser, as: CompilerParser
  alias Hologram.Template.{TokenHTMLEncoder, Tokenizer}
  alias Hologram.Template.VDOM.{Expression, TextNode}

  @doc """
  Splits a string which may contain embedded expressions into a list of expression nodes and text nodes.
  """
  def parse(str, %Context{} = context) do
    acc = %{
      nodes: [],
      num_open_braces: 0,
      prev_tokens: [],
      token_buffer: []
    }

    Tokenizer.tokenize(str)
    |> assemble_nodes(:text, acc, context)
    |> Map.get(:nodes)
  end

  # status is one of: :text, :expression
  defp assemble_nodes(tokens, status, acc, context)

  defp assemble_nodes([], :text, acc, _) do
    maybe_add_text_node(acc)
  end

  # DEFER: implement this case (raise an error)
  # defp assemble_nodes([], :expression, acc, _)

  defp assemble_nodes([{:symbol, :"{"} = token | rest], :text, acc, context) do
    acc =
      acc
      |> maybe_add_text_node()
      |> increment_num_open_braces()
      |> add_prev_token(token)

    assemble_nodes(rest, :expression, acc, context)
  end

  defp assemble_nodes([{:symbol, :"{"} = token | rest], :expression, acc, context) do
    acc
    |> increment_num_open_braces()
    |> assemble_node_part(token, rest, :expression, context)
  end

  defp assemble_nodes(
         [{:symbol, :"}"} = token | rest],
         :expression,
         %{num_open_braces: 1} = acc,
         context
       ) do
    acc =
      acc
      |> maybe_add_expression_part(context)
      |> decrement_num_open_braces()
      |> add_prev_token(token)

    assemble_nodes(rest, :text, acc, context)
  end

  defp assemble_nodes([{:symbol, :"}"} = token | rest], :expression, acc, context) do
    acc
    |> decrement_num_open_braces()
    |> assemble_node_part(token, rest, :expression, context)
  end

  defp assemble_nodes([token | rest], :text, acc, context) do
    assemble_node_part(acc, token, rest, :text, context)
  end

  defp assemble_nodes([token | rest], :expression, acc, context) do
    assemble_node_part(acc, token, rest, :expression, context)
  end

  defp add_prev_token(acc, token) do
    %{acc | prev_tokens: acc.prev_tokens ++ [token]}
  end

  defp assemble_node_part(acc, token, rest, type, context) do
    acc = acc |> buffer_token(token) |> add_prev_token(token)
    assemble_nodes(rest, type, acc, context)
  end

  defp buffer_token(acc, token) do
    %{acc | token_buffer: acc.token_buffer ++ [token]}
  end

  defp decrement_num_open_braces(acc) do
    %{acc | num_open_braces: acc.num_open_braces - 1}
  end

  defp flush_token_buffer(acc) do
    tokens = acc.token_buffer
    acc = %{acc | token_buffer: []}
    {tokens, acc}
  end

  defp get_ir(code, context) do
    CompilerParser.parse!("{#{code}}")
    |> Transformer.transform(context)
  end

  defp increment_num_open_braces(acc) do
    %{acc | num_open_braces: acc.num_open_braces + 1}
  end

  defp maybe_add_expression_part(acc, context) do
    {tokens, acc} = flush_token_buffer(acc)

    if Enum.any?(tokens) do
      code = TokenHTMLEncoder.encode(tokens)
      node = %Expression{ir: get_ir(code, context)}
      %{acc | nodes: acc.nodes ++ [node]}
    else
      acc
    end
  end

  defp maybe_add_text_node(acc) do
    {tokens, acc} = flush_token_buffer(acc)

    if Enum.any?(tokens) do
      node = %TextNode{content: TokenHTMLEncoder.encode(tokens)}
      %{acc | nodes: acc.nodes ++ [node]}
    else
      acc
    end
  end
end