This repository has been archived on 2023-08-07. You can view files and clone it, but cannot push or open issues or pull requests.
temple/lib/temple.ex

424 lines
11 KiB
Elixir
Raw Normal View History

2019-07-02 02:48:51 +00:00
defmodule Temple do
2020-06-16 19:28:21 +00:00
alias Temple.Buffer
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
@moduledoc """
> Warning: Docs are WIP
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
Temple syntax is available inside the `temple` and `live_temple` macros, and is compiled into EEx at build time.
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
### Usage
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
```elixir
temple do
# You can define attributes by passing a keyword list to the element, the values can be literals or variables.
class = "text-blue"
id = "jumbotron"
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
div class: class, id: id do
# Text nodes can be emitted as string literals or variables.
"Bob"
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
id
end
# if and unless expressions can be used to conditionally render content
if 5 > 0 do
p do
"Greater than 0!"
2019-05-11 16:49:34 +00:00
end
end
2020-06-16 19:28:21 +00:00
unless 5 > 0 do
p do
"Less than 0!"
end
end
# You can loop over items using for comprehensions
for x <- 0..5 do
div do
x
end
end
# You can use multiline anonymous functions, like if you're building a form in Phoenix
form_for @changeset, Routes.user_path(@conn, :create), fn f ->
"Name: "
text_input f, :name
end
# You can explicitly call a tag by prefixing with the Temple module
Temple.div do
"Foo"
end
# You can also pass children as a do key instead of a block
div do: "Alice", class: "text-yellow"
end
2019-05-11 16:49:34 +00:00
```
2020-06-16 19:28:21 +00:00
### Reserved keywords
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
You can pass a keyword list to an element as element attributes, but there are several reserved keywords.
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
#### Compact
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
Passing `compact: true` will not rendering new lines from within the element. This is useful if you are trying to use the `:empty` psuedo selector.
```elixir
temple do
p compact: true do
"Foo"
end
p do
"Bar"
2019-05-11 16:49:34 +00:00
end
end
2020-06-16 19:28:21 +00:00
```
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
would evaluate to
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
```html
<p>Foo</p>
<p>
Bar
</p>
2019-05-11 16:49:34 +00:00
```
2020-06-16 19:28:21 +00:00
### Configuration
#### Aliases
You can add an alias for an element if there is a namespace collision with a function. If you are using `Phoenix.HTML`, there will be namespace collisions with the `<link>` and `<label>` elements.
```elixir
config :temple, :aliases,
label: :_label,
link: :_link
2019-07-09 02:29:41 +00:00
temple do
2020-06-16 19:28:21 +00:00
_label do
"Email"
2019-05-11 16:49:34 +00:00
end
2020-06-16 19:28:21 +00:00
_link href: "/css/site.css"
2019-05-11 16:49:34 +00:00
end
2020-06-16 19:28:21 +00:00
```
This will result in:
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
```html
<label>
Email
</label>
<link href="/css/site.css">
2019-05-11 16:49:34 +00:00
```
"""
2020-06-16 19:28:21 +00:00
defmacro __using__(_) do
quote location: :keep do
2020-06-16 19:28:21 +00:00
import Temple
2019-05-11 16:49:34 +00:00
end
end
2020-06-16 19:28:21 +00:00
defmodule Private do
@moduledoc false
@aliases Application.get_env(:temple, :aliases, [])
@nonvoid_elements ~w[
head title style script
noscript template
body section nav article aside h1 h2 h3 h4 h5 h6
header footer address main
p pre blockquote ol ul li dl dt dd figure figcaption div
a em strong small s cite q dfn abbr data time code var samp kbd
sub sup i b u mark ruby rt rp bdi bdo span
ins del
iframe object video audio canvas
map
table caption colgroup tbody thead tfoot tr td th
form fieldset legend label button select datalist optgroup
option textarea output progress meter
details summary menuitem menu
html
]a
@nonvoid_elements_aliases Enum.map(@nonvoid_elements, fn el ->
Keyword.get(@aliases, el, el)
end)
@nonvoid_elements_lookup Enum.map(@nonvoid_elements, fn el ->
{Keyword.get(@aliases, el, el), el}
end)
@void_elements ~w[
meta link base
area br col embed hr img input keygen param source track wbr
]a
@void_elements_aliases Enum.map(@void_elements, fn el -> Keyword.get(@aliases, el, el) end)
@void_elements_lookup Enum.map(@void_elements, fn el ->
{Keyword.get(@aliases, el, el), el}
end)
def snake_to_kebab(stringable),
do:
stringable |> to_string() |> String.replace_trailing("_", "") |> String.replace("_", "-")
def kebab_to_snake(stringable),
do: stringable |> to_string() |> String.replace("-", "_")
def compile_attrs([]), do: ""
def compile_attrs([attrs]) when is_list(attrs) do
compile_attrs(attrs)
end
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
def compile_attrs(attrs) do
for {name, value} <- attrs, into: "" do
name = snake_to_kebab(name)
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
case value do
{_, _, _} = macro ->
" " <> name <> "=\"<%= " <> Macro.to_string(macro) <> " %>\""
value ->
" " <> name <> "=\"" <> to_string(value) <> "\""
end
2019-05-11 16:49:34 +00:00
end
2020-06-16 19:28:21 +00:00
end
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
def split_args(nil), do: {[], []}
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
def split_args(args) do
{do_and_else, args} =
args
|> Enum.split_with(fn
arg when is_list(arg) ->
(Keyword.keys(arg) -- [:do, :else]) |> Enum.count() == 0
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
_ ->
false
end)
{List.flatten(do_and_else), args}
2019-05-11 16:49:34 +00:00
end
2020-06-16 19:28:21 +00:00
def split_on_fn([{:fn, _, _} = func | rest], {args, _, args2}) do
split_on_fn(rest, {args, func, args2})
2019-05-11 16:49:34 +00:00
end
2020-06-16 19:28:21 +00:00
def split_on_fn([arg | rest], {args, nil, args2}) do
split_on_fn(rest, {[arg | args], nil, args2})
end
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
def split_on_fn([arg | rest], {args, func, args2}) do
split_on_fn(rest, {args, func, [arg | args2]})
end
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
def split_on_fn([], {args, func, args2}) do
{Enum.reverse(args), func, Enum.reverse(args2)}
end
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
def pop_compact?([]), do: {false, []}
def pop_compact?([args]) when is_list(args), do: pop_compact?(args)
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
def pop_compact?(args) do
Keyword.pop(args, :compact, false)
end
def traverse(buffer, {:__block__, _meta, block}) do
traverse(buffer, block)
end
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
def traverse(buffer, {name, meta, args} = macro) do
{do_and_else, args} =
args
|> split_args()
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
includes_fn? = args |> Enum.any?(fn x -> match?({:fn, _, _}, x) end)
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
case name do
{:., _, [{:__aliases__, _, [:Temple]}, name]} when name in @nonvoid_elements_aliases ->
{do_and_else, args} =
case args do
[args] ->
{do_value, args} = Keyword.pop(args, :do)
2019-06-01 04:02:49 +00:00
2020-06-16 19:28:21 +00:00
do_and_else = Keyword.put_new(do_and_else, :do, do_value)
{do_and_else, args}
_ ->
{do_and_else, args}
end
name = @nonvoid_elements_lookup[name]
{compact?, args} = pop_compact?(args)
Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
unless compact?, do: Buffer.put(buffer, "\n")
traverse(buffer, do_and_else[:do])
if compact?, do: Buffer.remove_new_line(buffer)
Buffer.put(buffer, "</#{name}>")
Buffer.put(buffer, "\n")
{:., _, [{:__aliases__, _, [:Temple]}, name]} when name in @void_elements_aliases ->
name = @void_elements_lookup[name]
Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
Buffer.put(buffer, "\n")
name when name in @nonvoid_elements_aliases ->
{do_and_else, args} =
case args do
[args] ->
{do_value, args} = Keyword.pop(args, :do)
do_and_else = Keyword.put_new(do_and_else, :do, do_value)
{do_and_else, args}
_ ->
{do_and_else, args}
end
name = @nonvoid_elements_lookup[name]
{compact?, args} = pop_compact?(args)
Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
unless compact?, do: Buffer.put(buffer, "\n")
traverse(buffer, do_and_else[:do])
if compact?, do: Buffer.remove_new_line(buffer)
Buffer.put(buffer, "</#{name}>")
Buffer.put(buffer, "\n")
name when name in @void_elements_aliases ->
name = @void_elements_lookup[name]
Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
Buffer.put(buffer, "\n")
name when includes_fn? ->
{args, func_arg, args2} = split_on_fn(args, {[], nil, []})
{func, _, [{arrow, _, [[{arg, _, _}], block]}]} = func_arg
Buffer.put(
buffer,
"<%= " <>
to_string(name) <>
" " <>
(Enum.map(args, &Macro.to_string(&1)) |> Enum.join(", ")) <>
", " <>
to_string(func) <> " " <> to_string(arg) <> " " <> to_string(arrow) <> " %>"
)
Buffer.put(buffer, "\n")
traverse(buffer, block)
if Enum.any?(args2) do
Buffer.put(
buffer,
"<% end, " <>
(Enum.map(args2, fn arg -> Macro.to_string(arg) end)
|> Enum.join(", ")) <> " %>"
)
Buffer.put(buffer, "\n")
else
Buffer.put(buffer, "<% end %>")
Buffer.put(buffer, "\n")
end
name when name in [:for, :if, :unless] ->
Buffer.put(buffer, "<%= " <> Macro.to_string({name, meta, args}) <> " do %>")
Buffer.put(buffer, "\n")
traverse(buffer, do_and_else[:do])
if Keyword.has_key?(do_and_else, :else) do
Buffer.put(buffer, "<% else %>")
Buffer.put(buffer, "\n")
traverse(buffer, do_and_else[:else])
end
Buffer.put(buffer, "<% end %>")
Buffer.put(buffer, "\n")
name when name in [:=] ->
Buffer.put(buffer, "<% " <> Macro.to_string(macro) <> " %>")
Buffer.put(buffer, "\n")
traverse(buffer, do_and_else[:do])
_ ->
Buffer.put(buffer, "<%= " <> Macro.to_string(macro) <> " %>")
Buffer.put(buffer, "\n")
traverse(buffer, do_and_else[:do])
end
end
def traverse(buffer, [first | rest]) do
traverse(buffer, first)
traverse(buffer, rest)
end
def traverse(buffer, text) when is_binary(text) do
Buffer.put(buffer, text)
Buffer.put(buffer, "\n")
end
def traverse(_buffer, arg) when arg in [nil, []] do
nil
2019-05-11 16:49:34 +00:00
end
end
2020-06-16 19:28:21 +00:00
defmacro temple([do: block] = _block) do
{:ok, buffer} = Buffer.start_link()
buffer
|> Temple.Private.traverse(block)
markup = Buffer.get(buffer)
Buffer.stop(buffer)
quote location: :keep do
unquote(markup)
2019-05-11 16:49:34 +00:00
end
end
2020-06-16 19:28:21 +00:00
defmacro temple(block) do
quote location: :keep do
2020-06-16 19:28:21 +00:00
import Temple
2020-06-16 19:28:21 +00:00
{:ok, buffer} = Buffer.start_link()
2020-06-16 19:28:21 +00:00
buffer
|> Temple.Private.traverse(unquote(block))
2020-06-16 19:28:21 +00:00
markup = Buffer.get(buffer)
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
Buffer.stop(buffer)
2020-06-16 19:28:21 +00:00
markup
end
end
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
defmacro live_temple([do: block] = _block) do
{:ok, buffer} = Buffer.start_link()
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
buffer
|> Temple.Private.traverse(block)
2019-05-11 16:49:34 +00:00
2020-06-16 19:28:21 +00:00
markup = Buffer.get(buffer)
Buffer.stop(buffer)
EEx.compile_string(markup, engine: Phoenix.LiveView.Engine)
2019-05-11 16:49:34 +00:00
end
2019-04-15 01:44:39 +00:00
end