defmodule Makeup.Lexers.HEExLexer do @moduledoc """ HEEx lexer """ import NimbleParsec alias Makeup.Lexer.Combinators, as: C import Makeup.Lexer.Groups # By default we'll use the HTMLLexer, but users can provide # a different HTML lexer alias Makeup.Lexers.{ ElixirLexer, HTMLLexer } alias Makeup.Lexers.EExLexer.Splicer @behaviour Makeup.Lexer ################################################################### # Step #1: tokenize the input (into a list of tokens) ################################################################### modified_inline_comment = string("#") |> concat( repeat( lookahead_not( choice([ string("\n"), string("%>"), string("}") ]) ) |> utf8_char([]) ) ) |> C.token(:comment_single) elixir_expr = choice([ modified_inline_comment, parsec({ElixirLexer, :root_element}) ]) # Make this more strict! # we're bound to get a number of false positives due to this. # Actually, I should try to find a syntax reference for HEEx text_outside_heex = times( lookahead_not( choice([ string("<%"), string("{") ]) ) |> utf8_char([]), min: 1 ) |> C.token(:text) |> map({__MODULE__, :__as_outside_text__, []}) def __as_outside_text__({ttype, meta, value}) do {ttype, Map.put(meta, :outside_text, true), value} end # The only thing that makes HEEx templates different from EEx templates heex_attrs = C.many_surrounded_by(elixir_expr, "{", "}", :punctuation) heex_comment = C.string_like("<%#", "%>", [utf8_char([])], :comment) heex_escape = C.many_surrounded_by(elixir_expr, "<%%", "%>", :punctuation) heex_show = C.many_surrounded_by(elixir_expr, "<%=", "%>", :punctuation) heex_pipe = C.many_surrounded_by(elixir_expr, "<%|", "%>", :punctuation) heex_slash = C.many_surrounded_by(elixir_expr, "<%/", "%>", :punctuation) heex_exec = C.many_surrounded_by(elixir_expr, "<%", "%>", :punctuation) root_element_combinator = choice([ # HEEx attributes heex_attrs, # EEx expressions heex_comment, heex_escape, heex_show, heex_pipe, heex_slash, heex_exec, # Text outside EEx text_outside_heex ]) # By default, don't inline the lexers. # Inlining them increases performance by ~20% # at the cost of doubling the compilation times... @inline false @doc false # If the token is already tagged as belonging to a language, we respect that # and don't change the language def __as_heex_language__({_ttype, %{language: _}, _value} = token) do token end def __as_heex_language__({ttype, meta, value}) do {ttype, Map.put(meta, :language, :heex), value} end @impl Makeup.Lexer defparsec( :root_element, root_element_combinator |> map({__MODULE__, :__as_heex_language__, []}), inline: @inline, export_combinator: true ) @impl Makeup.Lexer defparsec( :root, repeat(parsec(:root_element)), inline: @inline, export_combinator: true ) ################################################################### # Step #2: postprocess the list of tokens ################################################################### # Public API @impl Makeup.Lexer def postprocess(tokens, _opts \\ []), do: tokens ################################################################### # Step #3: highlight matching delimiters ################################################################### @impl Makeup.Lexer defgroupmatcher(:match_groups, heex_attrs: [ open: [ [{:punctuation, %{language: :heex}, "{"}] ], close: [ [{:punctuation, %{language: :heex}, ""}] ] ], heex_comment: [ open: [ [{:punctuation, %{language: :heex}, "<%#"}] ], close: [ [{:punctuation, %{language: :heex}, "%>"}] ] ], heex_escape: [ open: [ [{:punctuation, %{language: :heex}, "<%%"}] ], close: [ [{:punctuation, %{language: :heex}, "%>"}] ] ], heex_show: [ open: [ [{:punctuation, %{language: :heex}, "<%="}] ], close: [ [{:punctuation, %{language: :heex}, "%>"}] ] ], heex_pipe: [ open: [ [{:punctuation, %{language: :heex}, "<%|"}] ], close: [ [{:punctuation, %{language: :heex}, "%>"}] ] ], heex_slash: [ open: [ [{:punctuation, %{language: :heex}, "<%/"}] ], close: [ [{:punctuation, %{language: :heex}, "%>"}] ] ], heex_exec: [ open: [ [{:punctuation, %{language: :heex}, "<%"}] ], close: [ [{:punctuation, %{language: :heex}, "%>"}] ] ] ) # Finally, the public API for the lexer @impl Makeup.Lexer def lex(text, opts \\ []) do group_prefix = Keyword.get(opts, :group_prefix, random_prefix(10)) outer_lexer = Keyword.get(opts, :outer_lexer, HTMLLexer) # First pass - lex the HEEx part and ignore the outside HTML {:ok, tokens, "", _, _, _} = root(text) tokens = tokens |> postprocess([]) |> ElixirLexer.postprocess([]) new_group_prefix = group_prefix <> "-out" outer_opts = Keyword.put(opts, :group_prefix, new_group_prefix) # Second pass - Lex the outside HTML all_tokens = Splicer.lex_outside(tokens, outer_lexer, outer_opts) # Apply the finishing touches all_tokens |> match_groups(group_prefix) |> ElixirLexer.match_groups(group_prefix <> "-ex") end end