Current section
Files
Jump to
Current section
Files
lib/eml.ex
defmodule Eml do
@moduledoc """
Eml makes markup a first class citizen in Elixir. It provides a
flexible and modular toolkit for generating, parsing and
manipulating markup. It's main focus is html, but other markup
languages could be implemented as well.
To start off:
This piece of code
```elixir
use Eml.Language.HTML
name = "Vincent"
age = 36
div class: "person" do
div do
span "name: "
span name
end
div do
span "age: "
span age
end
end |> Eml.render
```
produces
```html
<div class='person'>
<div>
<span>name: </span>
<span>Vincent</span>
</div>
<div>
<span>age: </span>
<span>36</span>
</div>
</div>
```
The functions and macro's in the `Eml` module cover most of
Eml's public API.
"""
alias Eml.Element
alias Eml.Template
alias Eml.Data
@default_lang Eml.Language.HTML
@type t :: String.t | Eml.Element.t | Eml.Parameter.t | Eml.Template.t | { :safe, String.t }
@type content :: [t]
@type enumerable :: Eml.Element.t | [Eml.Element.t]
@type transformable :: t | [t]
@type lang :: module
@type unpackr_result :: funpackr_result | [unpackr_result]
@type funpackr_result :: String.t | Eml.Parameter.t | Eml.Template.t | [String.t | Eml.Parameter.t | Eml.Template.t]
@doc false
def do_eml(quoted \\ nil, opts) do
(quoted || opts[:do])
|> handle_file(opts[:file])
|> handle_type(opts[:use] || @default_lang, opts[:type] || :eml)
|> handle_precompile(opts[:env] || [], opts[:file], opts[:precompile])
|> handle_assign(opts[:handle_assign])
end
defp handle_file(_, file) when is_binary(file) do
file
|> File.read!()
|> Code.string_to_quoted!(file: file, line: 1)
end
defp handle_file(ast, _), do: ast
defp handle_type(ast, lang, :template) do
quote do
use unquote(lang)
Eml.compile unquote(ast)
end
end
defp handle_type(ast, lang, :markup) do
quote do
use unquote(lang)
Eml.render unquote(ast)
end
end
defp handle_type(ast, lang, :eml) do
quote do
use unquote(lang)
unquote(ast)
end
end
defp handle_precompile(ast, env, file, true) do
if file do
env = [file: file, line: 1]
end
{ expr, _ } = Code.eval_quoted(ast, [] , env)
expr
end
defp handle_precompile(ast, _, _, _) do
ast
end
defp handle_assign(ast, true), do: Macro.prewalk(ast, &EEx.Engine.handle_assign/1)
defp handle_assign(ast, _), do: ast
@doc """
Define a function that produces eml.
This macro is provided both for convenience and
to be able to show intention of code.
This:
```elixir
defeml mydiv(content), do: div(content)
```
is effectively the same as:
```elixir
def mydiv(content) do
use Eml.Language.HTML
div(content)
end
```
"""
defmacro defeml(call, do_block) do
block = do_block[:do]
ast = do_eml(block, type: :eml)
quote do
def unquote(call) do
unquote(ast)
end
end
end
@doc """
Define a function that produces html.
This macro is provided both for convenience and
to be able to show intention of code.
This:
```elixir
defhtml mydiv(content), do: div(content)
```
is effectively the same as:
```elixir
def mydiv(content) do
use Eml.Language.HTML
div(content) |> Eml.render
end
```
"""
defmacro defhtml(call, do_block) do
block = do_block[:do]
ast = do_eml(block, type: :markup, use: Eml.Language.HTML)
quote do
def unquote(call) do
unquote(ast)
end
end
end
@doc """
Define a function that compiles eml to a template during compile time.
The function that this macro defines accepts optionally a bindings
object as argument for binding values to parameters. Note that because
the code in the do block is evaluated at compile time, it's not possible
to call other functions from the same module.
Instead of defining a do block, you can also provide a path to a file with
eml content.
When you ommit the name, this macro can also be used to precompile a
block or file inside any function.
### Example:
iex> File.write! "test.eml.exs", "div [id: "name"], :name"
iex> defmodule MyTemplates do
...> use Eml
...>
...> precompile test1 do
...> prefix = "fruit"
...> div do
...> span [class: "prefix"], prefix
...> span [class: "content"], :fruit
...> end
...> end
...>
...> precompile from_file, file: "test.eml.exs"
...>
...> defhtml test2 do
...> precompiled = precompile do
...> # Everything inside this block is evaluated at compile time
...> p [], :fruit
...> end
...>
...> # the rest of the function is evaluated at runtime
...> body do
...> bind precompiled, fruit: "Strawberry"
...> end
...> end
...> end
iex> File.rm! "test.eml.exs"
iex> MyTemplates.test1
#Template<[:fruit]>
iex> MyTemplates.test fruit: "lemon"
"<div><span class='prefix'>fruit</span><span class='content'>lemon</span></div>"
iex> MyTemplates.from_file name: "Vincent"
"<div id='name'>Vincent</div>"
iex> MyTemplated.test2
"<body><p>Strawberry</p></body>"
"""
defmacro precompile(name \\ nil, opts) do
ast = opts
|> Keyword.put(:type, :template)
|> Keyword.put(:precompile, true)
|> Keyword.put_new(:env, __CALLER__)
|> do_eml()
|> Macro.escape()
if is_nil(name) do
quote do
unquote(ast)
end
else
{ name, _, _ } = name
quote do
def unquote(name)(bindings \\ []) do
Eml.compile(unquote(ast), bindings)
end
end
end
end
@doc """
Selects content from arbritary eml.
It will traverse the complete eml tree, so all nodes are
evaluated. There is however currently no way to select templates
or parameters.
Nodes are matched depending on the provided options.
Those options can be:
* `:tag` - match element by tag (`atom`)
* `:id` - match element by id (`binary`)
* `:class` - match element by class (`binary`)
* `:pat` - match binary content by regular expression (`RegEx.t`)
* `:parent` - when set to true, selects the parent node
of the matched node (`boolean`)
When `:tag`, `:id`, or `:class` are combined, only elements are
selected that satisfy all conditions.
When the `:pat` options is used, `:tag`, `:id` and `:class` will
be ignored.
### Examples:
iex> e = div do
...> span [id: "inner1", class: "inner"], "hello "
...> span [id: "inner2", class: "inner"], "world"
...> end
#div<[#span<%{id: "inner1", class: "inner"} ["hello "]>,
#span<%{id: "inner2", class: "inner"} ["world"]>]>
iex> Eml.select(e, id: "inner1")
[#span<%{id: "inner1", class: "inner"} ["hello "]>]
iex> Eml.select(e, class: "inner")
[#span<%{id: "inner1", class: "inner"} ["hello "]>,
#span<%{id: "inner2", class: "inner"} ["world"]>]
iex> Eml.select(e, class: "inner", id: "test")
[]
iex> Eml.select(e, pat: ~r/h.*o/)
["hello "]
iex> Eml.select(e, pat: ~r/H.*o/, parent: true)
[#span<%{id: "inner1", class: "inner"} ["hello "]>]
"""
@spec select(enumerable) :: [t]
def select(eml, opts \\ [])
def select(content, opts) when is_list(content) do
Enum.flat_map(content, &select(&1, opts))
end
def select(%Template{}, _opts), do: []
def select(node, opts) do
tag = opts[:tag] || :any
id = opts[:id] || :any
class = opts[:class] || :any
pat = opts[:pat]
select_parent? = opts[:parent] || false
if select_parent? do
if pat do
pat_fun = fn node ->
element?(node) and
Enum.any?(node.content, fn el -> is_binary(el) and Regex.match?(pat, el) end)
end
Enum.filter(node, pat_fun)
else
idclass_fun = fn node ->
element?(node) and
Enum.any?(node.content, fn el -> Element.match?(el, tag, id, class) end)
end
Enum.filter(node, idclass_fun)
end
else
if pat do
pat_fun = fn
node when is_binary(node) -> Regex.match?(pat, node)
_ -> false
end
Enum.filter(node, pat_fun)
else
Enum.filter(node, &Element.match?(&1, tag, id, class))
end
end
end
@doc """
Adds content to matched elements.
It traverses and returns the complete eml tree.
Nodes are matched depending on the provided options.
Those options can be:
* `:tag` - match element by tag (`atom`)
* `:id` - match element by id (`binary`)
* `:class` - match element by class (`binary`)
* `:at` - add new content at begin or end of existing
content, default is `:end` (`:begin | :end`)
When `:tag`, `:id`, or `:class` are combined, only elements are
selected that satisfy all conditions.
### Examples:
iex> e = div do
...> span [id: "inner1", class: "inner"], "hello "
...> span [id: "inner2", class: "inner"], "world"
...> end
#div<[#span<%{id: "inner1", class: "inner"} ["hello "]>,
#span<%{id: "inner2", class: "inner"} ["world"]>]>
iex> Eml.add(e, "dear ", id: "inner1")
[#div<[#span<%{id: "inner1", class: "inner"} ["hello dear "]>,
#span<%{id: "inner2", class: "inner"} ["world"]>]>]
iex> Eml.add(e, "__", class: "inner", at: :begin)
[#div<[#span<%{id: "inner1", class: "inner"} ["__hello "]>,
#span<%{id: "inner2", class: "inner"} ["__world"]>]>]
iex> Eml.add(e, span("!"), tag: :div) |> Eml.render()
"<div><span id='inner1' class='inner'>hello </span><span id='inner2' class='inner'>world</span><span>!</span></div>"
"""
@spec add(transformable, Eml.Data.t, Keyword.t) :: transformable
def add(eml, data, opts \\ []) do
tag = opts[:tag] || :any
id = opts[:id] || :any
class = opts[:class] || :any
add_fun = fn node ->
if element?(node) and Element.match?(node, tag, id, class),
do: Element.add(node, data, opts),
else: node
end
transform(eml, add_fun)
end
@doc """
Updates matched nodes.
When nodes are matched, the provided function will be evaluated
with the matched node as argument.
When the provided function returns `nil`, the node will
be removed from the eml tree. Any other returned value will be
evaluated by `Eml.to_content/3` in order to guarantee valid eml.
Nodes are matched depending on the provided options.
Those options can be:
* `:tag` - match element by tag (`atom`)
* `:id` - match element by id (`binary`)
* `:class` - match element by class (`binary`)
* `:pat` - match binary content by regular expression (`RegEx.t`)
* `:parent` - when set to true, selects the parent node
of the matched node (`boolean`)
When `:tag`, `:id`, or `:class` are combined, only elements are
selected that satisfy all conditions.
When the `:pat` options is used, `:tag`, `:id` and `:class` will
be ignored.
### Examples:
iex> e = div do
...> span [id: "inner1", class: "inner"], "hello "
...> span [id: "inner2", class: "inner"], "world"
...> end
#div<[#span<%{id: "inner1", class: "inner"} ["hello "]>,
#span<%{id: "inner2", class: "inner"} ["world"]>]>
iex> Eml.update(e, fn m -> Element.id(m, "outer") end, tag: :div)
[#div<%{id: "outer"}
[#span<%{id: "inner1", class: "inner"} ["hello "]>,
#span<%{id: "inner2", class: "inner"} ["world"]>]>]
iex> Eml.update(e, fn m -> Element.id(m, "outer") end, id: "inner2", parent: true)
[#div<%{id: "outer"}
[#span<%{id: "inner1", class: "inner"} ["hello "]>,
#span<%{id: "inner2", class: "inner"} ["world"]>]>]
iex> Eml.update(e, fn s -> String.upcase(s) end, pat: ~r/.+/) |> Eml.render()
"<div><span id='inner1' class='inner'>HELLO </span><span id='inner2' class='inner'>WORLD</span></div>"
"""
@spec update(transformable, (t -> Eml.Data.t), Keyword.t) :: transformable
def update(eml, fun, opts \\ []) do
tag = opts[:tag] || :any
id = opts[:id] || :any
class = opts[:class] || :any
pat = opts[:pat]
update_parent? = opts[:parent] || false
update_fun =
if update_parent? do
if pat do
fn node ->
if element?(node) and
Enum.any?(node.content, fn el -> is_binary(el) and Regex.match?(pat, el) end),
do: fun.(node),
else: node
end
else
fn node ->
if element?(node) and
Enum.any?(node.content, fn el -> Element.match?(el, tag, id, class) end),
do: fun.(node),
else: node
end
end
else
if pat do
fn node ->
if is_binary(node) and Regex.match?(pat, node),
do: fun.(node),
else: node
end
else
fn node ->
if Element.match?(node, tag, id, class),
do: fun.(node),
else: node
end
end
end
transform(eml, update_fun)
end
@doc """
Removes matched nodes from the eml tree.
See `update/3` for a description of the provided options.
### Examples:
iex> e = div do
...> span [id: "inner1", class: "inner"], "hello "
...> span [id: "inner2", class: "inner"], "world"
...> end
#div<[#span<%{id: "inner1", class: "inner"} ["hello "]>,
#span<%{id: "inner2", class: "inner"} ["world"]>]>
iex> Eml.remove(e, tag: :div)
[]
iex> Eml.remove(e, id: "inner1")
[#div<[#span<%{id: "inner2", class: "inner"} ["world"]>]>]
iex> Eml.remove(e, pat: ~r/.+/)
[#div<[#span<%{id: "inner1", class: "inner"}>,
#span<%{id: "inner2", class: "inner"}>]>]
"""
@spec remove(transformable, Keyword.t) :: transformable
def remove(eml, opts \\ []) do
tag = opts[:tag] || :any
id = opts[:id] || :any
class = opts[:class] || :any
pat = opts[:pat]
remove_parent? = opts[:parent] || false
remove_fun =
if remove_parent? do
if pat do
fn node ->
if element?(node) and
Enum.any?(node.content, fn el -> is_binary(el) and Regex.match?(pat, el) end),
do: nil,
else: node
end
else
fn node ->
if element?(node) and
Enum.any?(node.content, fn el -> Element.match?(el, tag, id, class) end),
do: nil,
else: node
end
end
else
if pat do
fn node ->
if is_binary(node) and Regex.match?(pat, node),
do: nil,
else: node
end
else
fn node ->
if Element.match?(node, tag, id, class),
do: nil,
else: node
end
end
end
transform(eml, remove_fun)
end
@doc """
Returns true if there's at least one node matches
the provided options, returns false otherwise.
In other words, returns true when the same select query
would return a non-empty list.
See `select/3` for a description of the provided options.
### Examples:
iex> e = div do
...> span [id: "inner1", class: "inner"], "hello "
...> span [id: "inner2", class: "inner"], "world"
...> end
#div<[#span<%{id: "inner1", class: "inner"} ["hello "]>,
#span<%{id: "inner2", class: "inner"} ["world"]>]>
iex> Eml.member?(e, id: "inner1")
true
iex> Eml.member?(e, class: "inner", id: "test")
false
iex> Eml.member?(e, pat: ~r/h.*o/)
true
"""
@spec member?(enumerable, Keyword.t) :: boolean
def member?(eml, opts) do
case select(eml, opts) do
[] -> false
_ -> true
end
end
@doc """
Recursively transforms `eml` content.
This is the most low level operation provided by Eml for manipulating
eml nodes. For example, `update/3` and `remove/2` are implemented by
using this function.
It accepts any eml and traverses all nodes of the provided eml tree.
The provided transform function will be evaluated for every node `transform/3`
encounters. Parent nodes will be transformed before their children. Child nodes
of a parent will be evaluated before moving to the next sibling.
When the provided function returns `nil`, the node will
be removed from the eml tree. Any other returned value will be
evaluated by `Eml.to_content/3` in order to guarantee valid eml.
Note that because parent nodes are evaluated before their children,
no children will be evaluated if the parent is removed.
### Examples:
iex> e = div do
...> span [id: "inner1", class: "inner"], "hello "
...> span [id: "inner2", class: "inner"], "world"
...> end
#div<[#span<%{id: "inner1", class: "inner"} ["hello "]>,
#span<%{id: "inner2", class: "inner"} ["world"]>]>
iex> Eml.transform(e, fn x -> if Element.has?(x, tag: :span), do: "matched", else: x end)
[#div<["matched", "matched"]>]
iex> Eml.transform(e, fn x ->
...> IO.puts(inspect x)
...> x end)
#div<[#span<%{id: "inner1", class: "inner"} ["hello "]>, #span<%{id: "inner2", class: "inner"} ["world"]>]>
#span<%{id: "inner1", class: "inner"} ["hello "]>
"hello "
#span<%{id: "inner2", class: "inner"} ["world"]>
"world"
[#div<[#span<%{id: "inner1", class: "inner"} ["hello "]>,
#span<%{id: "inner2", class: "inner"} ["world"]>]>]
"""
@spec transform(transformable, (t -> Eml.Data.t)) :: transformable | nil
def transform(eml, fun)
def transform(eml, fun) when is_list(eml) do
for node <- eml, t = transform(node, fun), do: t
end
def transform(node, fun) do
node = fun.(node) |> Data.to_eml()
if element?(node) do
%Element{node| content: transform(node.content, fun)}
else
node
end
end
@doc """
Converts data to `eml` content by using the `Eml.Data` protocol.
It also concatenates binaries and flatten lists to ensure the result
is valid content.
### Example
iex> Eml.to_content(["1", 2, [3], " ", ["miles"]])
["123 miles"]
You can also use this function to add data to existing content:
iex> Eml.to_content(42, [" is the number"], :begin)
["42 is the number"]
iex> Eml.to_content(42, ["the number is "], :end)
["the number is 42"]
"""
@spec to_content(Eml.Data.t | [Eml.Data.t], content, atom) :: content
def to_content(data, acc \\ [], insert_at \\ :begin)
# No-ops
def to_content(nondata, acc, _)
when nondata in [nil, "", []], do: acc
# Handle lists
def to_content(data, acc, :end)
when is_list(data), do: add_nodes(data, :lists.reverse(acc), :end) |> :lists.reverse()
def to_content(data, acc, :begin)
when is_list(data), do: add_nodes(:lists.reverse(data), acc, :begin)
# Convert data to eml node
def to_content(data, acc, insert_at) do
Data.to_eml(data) |> add_node(acc, insert_at)
end
defp add_node(node, [], _), do: [node]
defp add_node(node, [h | t], :end) do
if is_binary(node) and is_binary(h) do
[h <> node | t]
else
[node, h | t]
end
end
defp add_node(node, [h | t], :begin) do
if is_binary(node) and is_binary(h) do
[node <> h | t]
else
[node, h | t]
end
end
defp add_nodes([h | t], acc, insert_at) do
acc = if is_list(h) and insert_at === :end do
add_nodes(h, acc, insert_at)
else
to_content(h, acc, insert_at)
end
add_nodes(t, acc, insert_at)
end
defp add_nodes([], acc, _), do: acc
@doc """
Parses data and converts it to eml
How the data is interpreted depends on the `lang` argument.
The default value is `Eml.Language.HTML', which means that
strings are parsed as html.
In case of error, raises an Eml.ParseError exception.
### Examples:
iex> Eml.parse("<body><h1 id='main-title'>The title</h1></body>")
[#body<[#h1<%{id: "main-title"} ["The title"]>]>]
"""
@spec parse(String.t, lang) :: content
def parse(data, lang \\ @default_lang)
def parse(data, lang) when is_binary(data) do
lang.parse(data)
end
def parse(data, _) do
raise Eml.ParseError, type: :unsupported_input, value: data
end
@doc """
Renders eml content to the specified language, which is
html by default.
When the provided eml contains a template, you can bind
its parameters by providing a Keyword list as the
second argument where the keys are the parameter id's.
The accepted options are:
* `:lang` - The language to render to, by default `Eml.Language.HTML`
* `:quote` - The type of quotes used for attribute values. Accepted values are `:single` (default) and `:double`.
* `:escape` - Escape `&`, `<` and `>` in attribute values and content to HTML entities.
Accepted values are `true` (default) and `false`.
In case of error, raises an Eml.CompileError exception.
### Examples:
iex> Eml.render(body(h1([id: "main-title"], "A title")))
"<body><h1 id='main-title'>A title</h1></body>"
iex> Eml.render(body(h1([id: "main-title"], "A title")), quote: :double)
"<body><h1 id=\"main-title\">A title</h1></body>"
iex> Eml.render(p "Tom & Jerry")
"<p>Tom & Jerry</p>"
"""
@spec render(t, Eml.Template.bindings, Keyword.t) :: String.t
def render(eml, bindings \\ [], opts \\ []) do
{ lang, opts } = Keyword.pop(opts, :lang, @default_lang)
opts = Keyword.put(opts, :bindings, bindings)
opts = Keyword.put(opts, :mode, :render)
lang.render(eml, opts)
end
@doc """
Same as `Eml.render/3` except that it returns a template
that might conatain unbound parameters.
In case of error, raises an Eml.CompileError exception.
### Examples:
iex> t = Eml.compile(body(h1([id: "main-title"], :the_title)))
#Template<[:the_title]>
iex> t.chunks
["<body><h1 id='main-title'>", #param:the_title, "</h1></body>"]
iex> Eml.render(t, the_title: "The Title")
"<body><h1 id='main-title'>The Title</h1></body>"
"""
@spec compile(t, Eml.Template.bindings, Keyword.t) :: Eml.Template.t
def compile(eml, bindings \\ [], opts \\ []) do
{ lang, opts } = Keyword.pop(opts, :lang, @default_lang)
opts = Keyword.put(opts, :bindings, bindings)
opts = Keyword.put(opts, :mode, :compile)
lang.render(eml, opts)
end
@doc """
Extracts a value from content (which is always a list) or an element
### Examples
iex> Eml.unpack ["42"]
"42"
iex> Eml.unpack 42
42
iex> Eml.unpack(div "hallo")
"hallo"
iex> Eml.unpack Eml.unpack(div(span("hallo")))
"hallo"
"""
@spec unpack(t) :: t
def unpack(%Element{content: [node]}), do: node
def unpack(%Element{content: content}), do: content
def unpack([node]), do: node
def unpack(eml), do: eml
@doc """
Extracts a value recursively from content or an element
### Examples
iex> Eml.unpackr div(span(42))
"42"
iex> Eml.unpackr div([span("Hallo"), span(" world")])
["Hallo", " world"]
"""
@spec unpackr(t) :: unpackr_result
def unpackr(%Element{content: [node]}), do: unpackr(node)
def unpackr(%Element{content: content}), do: unpack_content(content)
def unpackr([node]), do: unpackr(node)
def unpackr(content) when is_list(content), do: unpack_content(content)
def unpackr(node), do: node
defp unpack_content(content) do
for node <- content, do: unpackr(node)
end
@doc """
Extracts a value recursively from content or an element and flatten the results.
"""
@spec funpackr(t) :: funpackr_result
def funpackr(eml), do: unpackr(eml) |> :lists.flatten
@doc "Checks if a term is a `Eml.Element` struct."
@spec element?(term) :: boolean
def element?(%Element{}), do: true
def element?(_), do: false
@doc "Checks if a value is regarded as empty by Eml."
@spec empty?(term) :: boolean
def empty?(nil), do: true
def empty?([]), do: true
def empty?(%Element{content: []}), do: true
def empty?(_), do: false
@doc """
Returns the type of content.
The types are `:string`, `:element`, `:template`, `:parameter`, or `:undefined`.
"""
@spec type(t) :: :string | :element | :template | :parameter | :undefined
def type(node) when is_binary(node), do: :string
def type(%Element{}), do: :element
def type(%Template{}), do: :template
def type(%Eml.Parameter{}), do: :parameter
def type(_), do: :undefined
@doc false
def default_alias_and_imports do
quote do
alias Eml.Element
alias Eml.Template
import Eml.Template, only: [bind: 2]
end
end
# use Eml
@doc """
Import `defeml`, `defhtml` and `precompile` macro's.
Invoking it translates to:
```
import Eml, only: [defeml: 2, defhtml: 2, precompile: 1, precompile: 2]
```
"""
defmacro __using__(_) do
quote do
import Eml, only: [defeml: 2, defhtml: 2, precompile: 1, precompile: 2]
end
end
end