Current section
Files
Jump to
Current section
Files
lib/expug/compiler.ex
defmodule Expug.Compiler do
@moduledoc """
Compiles tokens into an AST.
## How it works
Nodes are maps with a `:type` key. They are then filled up using a function
with the same name as the type:
node = %{type: :document}
document({node, tokens})
This function returns another `{node, tokens}` tuple, where `node` is the
updated node, and `tokens` are the rest of the tokens to parse.
The functions (`document/1`) here can do 1 of these things:
- Spawn a child, say, `%{type: :element}`, then delegate to its function (eg, `element()`).
- Simply return a `{node, tokens}` - no transformation here.
The functions `indent()` and `statement()` are a little different. It can
give you an element, or a text node, or whatever.
## Also see
- `Expug.Tokenizer` is used to build the tokens used by this compiler.
- `Expug.Builder` uses the AST made by this compiler.
"""
require Logger
@doc """
Compiles tokens. Returns `{:ok, ast}` on success.
On failure, it returns `{:error, [type: type, position: {line, col}]}`.
"""
def compile(tokens, _opts \\ []) do
tokens = Enum.reverse(tokens)
node = %{type: :document}
try do
{node, _tokens} = document({node, tokens})
node = Expug.Transformer.transform(node)
node
catch
{:compile_error, type, [{pos, token, _} | _]} ->
# TODO: create an EOF token
throw %{ type: type, position: pos, token_type: token }
{:compile_error, type, []} ->
throw %{ type: type }
end
end
@doc """
A document.
"""
def document({node, [{_, :doctype, type} = t | tokens]}) do
node = Map.put(node, :doctype, %{
type: :doctype,
value: type,
token: t
})
indent({node, tokens}, [0])
end
def document({node, tokens}) do
indent({node, tokens}, [0]) # optional
end
@doc """
Indentation. Called with `depth` which is the current level its at.
"""
def indent({node, [{_, :indent, subdepth} | [_|_] = tokens]}, [d | _] = depths)
when subdepth > d do
if node[:children] == nil do
throw {:compile_error, :unexpected_indent, hd(tokens)}
end
# Found children, start a new subtree.
[child | rest] = Enum.reverse(node[:children] || [])
{child, tokens} = statement({child, tokens}, [ subdepth | depths ])
|> indent([ subdepth | depths ])
# Go back to our tree.
children = Enum.reverse([child | rest])
node = Map.put(node, :children, children)
{node, tokens}
|> indent(depths)
end
def indent({node, [{_, :indent, subdepth} | [_|_] = tokens]}, [d | _] = depths)
when subdepth == d do
{node, tokens}
|> statement(depths)
|> indent(depths)
end
def indent({node, [{_, :indent, subdepth} | [_|_]] = tokens}, [d | _])
when subdepth < d do
# throw {:compile_error, :ambiguous_indentation, token}
{node, tokens}
end
# End of file, no tokens left.
def indent({node, []}, _depth) do
{node, []}
end
def indent({_node, tokens}, _depth) do
throw {:compile_error, :unexpected_token, tokens}
end
@doc """
A statement after an `:indent`.
Can consume these:
:element_name
:element_class
:element_id
[:attribute_open [...] :attribute_close]
[:buffered_text | :unescaped_text | :raw_text | :block_text]
"""
def statement({node, [{_, :line_comment, _} | [{_, :subindent, _} | _] = tokens]}, _depths) do
# Pretend to be an element and capture stuff into it; discard it afterwards.
# This is wrong anyway; it should be tokenized differently.
subindent({node, tokens})
end
def statement({node, [{_, :line_comment, _} | tokens]}, _depths) do
{node, tokens}
end
def statement({node, [{_, :html_comment, value} = t | tokens]}, _depths) do
child = %{type: :html_comment, value: value, token: t}
{child, tokens} = append_subindent({child, tokens})
node = add_child(node, child)
{node, tokens}
end
def statement({node, [{_, :element_name, _} = t | _] = tokens}, depths) do
add_element(node, t, tokens, depths)
end
def statement({node, [{_, :element_class, _} = t | _] = tokens}, depths) do
add_element(node, t, tokens, depths)
end
def statement({node, [{_, :element_id, _} = t | _] = tokens}, depths) do
add_element(node, t, tokens, depths)
end
def statement({node, [{_, :raw_text, value} = t | tokens]}, _depth) do
child = %{type: :raw_text, value: value, token: t}
node = add_child(node, child)
{node, tokens}
end
def statement({node, [{_, :buffered_text, value} = t | tokens]}, _depth) do
child = %{type: :buffered_text, value: value, token: t}
{child, tokens} = append_subindent({child, tokens})
node = add_child(node, child)
{node, tokens}
end
def statement({node, [{_, :unescaped_text, value} = t | tokens]}, _depth) do
child = %{type: :unescaped_text, value: value, token: t}
{child, tokens} = append_subindent({child, tokens})
node = add_child(node, child)
{node, tokens}
end
def statement({node, [{_, :statement, value} = t | tokens]}, _depth) do
child = %{type: :statement, value: value, token: t}
{child, tokens} = append_subindent({child, tokens})
node = add_child(node, child)
{node, tokens}
end
def statement({_node, tokens}, _depths) do
throw {:compile_error, :unexpected_token, tokens}
end
@doc """
Consumes `:subindent` tokens and adds them to the `value` of `node`.
"""
def append_subindent({node, [{_, :subindent, value} | tokens]}) do
node = node
|> Map.update(:value, value, &(&1 <> "\n#{value}"))
{node, tokens}
|> append_subindent()
end
def append_subindent({node, tokens}) do
{node, tokens}
end
def add_element(node, t, tokens, depth) do
child = %{type: :element, name: "div", token: t}
{child, rest} = element({child, tokens}, node, depth)
node = add_child(node, child)
{node, rest}
end
@doc """
Parses an element.
Returns a `%{type: :element}` node.
"""
def element({node, tokens}, parent, depths) do
case tokens do
[{_, :element_name, value} | rest] ->
node = Map.put(node, :name, value)
element({node, rest}, parent, depths)
[{_, :element_id, value} | rest] ->
attr_list = add_attribute(node[:attributes] || %{}, "id", {:text, value})
node = Map.put(node, :attributes, attr_list)
element({node, rest}, parent, depths)
[{_, :element_class, value} | rest] ->
attr_list = add_attribute(node[:attributes] || %{}, "class", {:text, value})
node = Map.put(node, :attributes, attr_list)
element({node, rest}, parent, depths)
[{_, :raw_text, value} = t | rest] ->
# should be in children
child = %{type: :raw_text, value: value, token: t}
node = add_child(node, child)
element({node, rest}, parent, depths)
[{_, :buffered_text, value} = t | rest] ->
child = %{type: :buffered_text, value: value, token: t}
{child, rest} = append_subindent({child, rest})
node = add_child(node, child)
element({node, rest}, parent, depths)
[{_, :unescaped_text, value} = t | rest] ->
child = %{type: :unescaped_text, value: value, token: t}
{child, rest} = append_subindent({child, rest})
node = add_child(node, child)
element({node, rest}, parent, depths)
[{_, :block_text, _} | rest] ->
t = hd(rest)
{rest, lines} = subindent_capture(rest)
child = %{type: :block_text, value: Enum.join(lines, "\n"), token: t}
node = add_child(node, child)
element({node, rest}, parent, depths)
[{_, :attribute_open, _} | rest] ->
{attr_list, rest} = attribute({node[:attributes] || %{}, rest})
node = Map.put(node, :attributes, attr_list)
element({node, rest}, parent, depths)
tokens ->
{node, tokens}
end
end
@doc """
Returns a list of `[type: :attribute]` items.
"""
def attribute({attr_list, tokens}) do
case tokens do
[{_, :attribute_key, key}, {_, :attribute_value, value} | rest] ->
attr_list = add_attribute(attr_list, key, {:eval, value})
{attr_list, rest}
|> attribute()
[{_, :attribute_key, key} | rest] ->
attr_list = add_attribute(attr_list, key, {:eval, true})
{attr_list, rest}
|> attribute()
[{_, :attribute_close, _} | rest] ->
{attr_list, rest}
rest ->
{attr_list, rest}
end
end
def add_attribute(list, key, value) do
Map.update(list, key, [value], &(&1 ++ [value]))
end
@doc """
Adds a child to a Node.
iex> Expug.Compiler.add_child(%{}, %{type: :a})
%{children: [%{type: :a}]}
iex> src = %{children: [%{type: :a}]}
...> Expug.Compiler.add_child(src, %{type: :b})
%{children: [%{type: :a}, %{type: :b}]}
"""
def add_child(node, child) do
Map.update(node, :children, [child], &(&1 ++ [child]))
end
@doc """
Matches `:subindent` tokens and discards them. Used for line comments (`-#`).
"""
def subindent({node, [{_, :subindent, _} | rest]}) do
subindent({node, rest})
end
def subindent({node, rest}) do
{node, rest}
end
def subindent_capture(tokens, lines \\ [])
def subindent_capture([{_, :subindent, line} | rest], lines) do
lines = lines ++ [line]
subindent_capture(rest, lines)
end
def subindent_capture(rest, lines) do
{rest, lines}
end
end