Merge remote-tracking branch 'upstream/main'

This commit is contained in:
FloatingGhost 2023-08-06 18:29:06 +01:00
commit d1db62ef58
57 changed files with 1672 additions and 445 deletions

View file

@ -1,5 +1,6 @@
locals_without_parens = ~w[
temple c slot
temple = ~w[temple c slot]a
html = ~w[
html head title style script
noscript template
body section nav article aside h1 h2 h3 h4 h5 h6
@ -12,23 +13,114 @@ locals_without_parens = ~w[
map svg math
table caption colgroup tbody thead tfoot tr td th
form fieldset legend label button select datalist optgroup
option text_area output progress meter
option textarea output progress meter
details summary menuitem menu
meta link base
area br col embed hr img input keygen param source track wbr
]a
animate animateMotion animateTransform circle clipPath
color-profile defs desc discard ellipse feBlend
feColorMatrix feComponentTransfer feComposite feConvolveMatrix feDiffuseLighting feDisplacementMap feDistantLight feDropShadow
feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur feImage feMerge feMergeNode feMorphology feOffset
fePointLight feSpecularLighting feSpotLight feTile feTurbulence filter foreignObject g hatch hatchpath image line linearGradient
marker mask mesh meshgradient meshpatch meshrow metadata mpath path pattern polygon
polyline radialGradient rect set solidcolor stop svg switch symbol text
textPath tspan unknown use view
]a |> Enum.map(fn e -> {e, :*} end)
svg = ~w[
circle
ellipse
line
path
polygon
polyline
rect
stop
use
a
altGlyph
altGlyphDef
altGlyphItem
animate
animateColor
animateMotion
animateTransform
animation
audio
canvas
clipPath
cursor
defs
desc
discard
feBlend
feColorMatrix
feComponentTransfer
feComposite
feConvolveMatrix
feDiffuseLighting
feDisplacementMap
feDistantLight
feDropShadow
feFlood
feFuncA
feFuncB
feFuncG
feFuncR
feGaussianBlur
feImage
feMerge
feMergeNode
feMorphology
feOffset
fePointLight
feSpecularLighting
feSpotLight
feTile
feTurbulence
filter
font
foreignObject
g
glyph
glyphRef
handler
hatch
hatchpath
hkern
iframe
image
linearGradient
listener
marker
mask
mesh
meshgradient
meshpatch
meshrow
metadata
mpath
pattern
prefetch
radialGradient
script
set
solidColor
solidcolor
style
svg
switch
symbol
tbreak
text
textArea
textPath
title
tref
tspan
unknown
video
view
vkern
]a
locals_without_parens = Enum.map(temple ++ html ++ svg, &{&1, :*})
[
import_deps: [:typed_struct],
inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"],
locals_without_parens: locals_without_parens,
locals_without_parens: locals_without_parens ++ [assert_html: 2],
export: [locals_without_parens: locals_without_parens]
]

View file

@ -6,13 +6,13 @@ on:
jobs:
tests:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
name: Test (${{matrix.elixir}}/${{matrix.otp}})
strategy:
matrix:
otp: [23.x, 24.x]
elixir: [1.13.x]
otp: [23.x, 24.x, 25.x]
elixir: [1.13.x, 1.14.x]
steps:
- uses: actions/checkout@v2
@ -21,6 +21,7 @@ jobs:
otp-version: ${{matrix.otp}}
elixir-version: ${{matrix.elixir}}
- uses: actions/cache@v3
id: cache
with:
path: |
deps
@ -86,22 +87,23 @@ jobs:
formatter:
runs-on: ubuntu-latest
name: Formatter (1.13.x.x/24.x)
name: Formatter (1.14.x.x/25.x)
steps:
- uses: actions/checkout@v2
- uses: erlef/setup-beam@v1
with:
otp-version: 24.x
elixir-version: 1.13.x
otp-version: 25.x
elixir-version: 1.14.x
- uses: actions/cache@v3
id: cache
with:
path: |
deps
_build
key: ${{ runner.os }}-mix-24-1.13-${{ hashFiles('**/mix.lock') }}
key: ${{ runner.os }}-mix-23-1.14-${{ hashFiles('**/mix.lock') }}
restore-keys: |
${{ runner.os }}-mix-24-1.13-
${{ runner.os }}-mix-23-1.14-
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'

58
.github/workflows/release.yaml vendored Normal file
View file

@ -0,0 +1,58 @@
name: Release
on:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
jobs:
release:
name: release
runs-on: ubuntu-latest
strategy:
matrix:
otp: [25.3]
elixir: [1.14.x]
steps:
- uses: google-github-actions/release-please-action@v3
id: release
with:
release-type: elixir
package-name: temple
bump-minor-pre-major: true
extra-files: |
README.md
- uses: actions/checkout@v3
if: ${{ steps.release.outputs.release_created }}
- uses: erlef/setup-beam@v1
with:
otp-version: ${{matrix.otp}}
elixir-version: ${{matrix.elixir}}
if: ${{ steps.release.outputs.release_created }}
- uses: actions/cache@v3
id: cache
if: ${{ steps.release.outputs.release_created }}
with:
path: |
deps
_build
key: ${{ runner.os }}-mix-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }}
restore-keys: |
${{ runner.os }}-mix-${{ matrix.otp }}-${{ matrix.elixir }}-
- name: Install Dependencies
if: steps.release.outputs.release_created && steps.cache.outputs.cache-hit != 'true'
run: mix deps.get
- name: publish to hex
if: ${{ steps.release.outputs.release_created }}
env:
HEX_API_KEY: ${{secrets.HEX_API_KEY}}
run: |
mix hex.publish --yes

View file

@ -2,7 +2,50 @@
## Main
### 0.9.0-rc.0
## [0.12.0](https://github.com/mhanberg/temple/compare/v0.11.0...v0.12.0) (2023-06-13)
### ⚠ BREAKING CHANGES
* configure runtime attributes function ([#202](https://github.com/mhanberg/temple/issues/202))
### Features
* configure runtime attributes function ([#202](https://github.com/mhanberg/temple/issues/202)) ([dc57221](https://github.com/mhanberg/temple/commit/dc57221bc99e165530134559097b27b1dfe95dbe))
### Bug Fixes
* **docs:** typos ([7a50587](https://github.com/mhanberg/temple/commit/7a505875af6a1cee1536e516528f5be914df1f3f))
## v0.11.0
### Breaking Changes
- Rendering slots is now done by passing the assign with the slot name to the `slot` keyword instead of name as an atom. If this slot has multiple definitions, you can loop through them and render each one individually, or render them all at once. Please see the migration guide for more information.
- The `:default` slot has been renamed to `:inner_block`. This is to be easily compatible with HEEx/Surface. Please see the migration guide for more information.
- Capturing the data being passed into a slot is now defined using the `:let!` attribute. Please see the migration guide for more information.
### Enhancements
- Temple components are now compatible with HEEx/Surface components! Some small tweaks to the component implementation has made this possible. Please see the guides for more information.
- Multiple instances of the same slot name can now be declared and then rendered inside the component (similar to HEEx and Surface).
- You can now pass arbitrary data to slots, and it does not need to be a map or a keyword list. I don't think this is a breaking change, but please submit an issue if you notice it is.
- Slot attributes. You can now pass data into a slot from the definition site and use it at the call site (inside the component).
- Dynamic attributes/assigns. You can now pass dynamic attributes to the `:rest!` attribute in a tag, component, or slot.
### Fixes
- Attributes with runtime values that evaluate to true or false will be rendered correctly as boolean attributes.
### 0.10.0
### Enhancements
- mix temple.convert task to convert HTML into Temple syntax.
- Temple now works with SVG elements.
### 0.9.0
### Breaking Changes

View file

@ -1,23 +1,29 @@
# ![](temple-github-image.png)
![Temple](temple-github-image.png)
[![Actions Status](https://github.com/mhanberg/temple/workflows/CI/badge.svg)](https://github.com/mhanberg/temple/actions)
[![Hex.pm](https://img.shields.io/hexpm/v/temple.svg)](https://hex.pm/packages/temple)
> You are looking at the README for the main branch. The README for the latest stable release is located [here](https://github.com/mhanberg/temple/tree/v0.9.0).
> You are looking at the README for the main branch. The README for the latest stable release is located [here](https://github.com/mhanberg/temple/tree/v0.11.0).
Temple is an Elixir DSL for writing HTML.
# Temple
Temple is an Elixir DSL for writing HTML and SVG.
## Installation
Add `temple` to your list of dependencies in `mix.exs`:
<!-- x-release-please-start-version -->
```elixir
def deps do
[
{:temple, "~> 0.9.0-rc.0"}
{:temple, "~> 0.12"}
]
end
```
<!-- x-release-please-end -->
## Goals
Currently Temple has the following things on which it won't compromise.
@ -67,6 +73,8 @@ Temple components are simple to write and easy to use.
Unlike normal partials, Temple components have the concept of "slots", which are similar [Vue](https://v3.vuejs.org/guide/component-slots.html#named-slots). You can also refer to HEEx and Surface for examples of templates that have the "slot" concept.
Temple components are compatible with HEEx and Surface components and can be shared.
Please see the [guides](https://hexdocs.pm/temple/components.html) for more details.
```elixir
@ -77,15 +85,15 @@ defmodule MyAppWeb.Component do
temple do
section do
div do
slot :header
slot @header
end
div do
slot :default
slot @inner_block
end
div do
slot :footer
slot @footer
end
end
end
@ -140,6 +148,10 @@ To include Temple's formatter configuration, add `:temple` to your `.formatter.e
## Phoenix
When using Phoenix ~> 1.7, all you need to do is include `:temple` in your mix.exs.
If you plan on using the template structure that < 1.6 Phoenix applications use, you can use `:temple_phoenix` as described below.
To use with [Phoenix](https://github.com/phoenixframework/phoenix), please use the [temple_phoenix](https://github.com/mhanberg/temple_phoenix) package! This bundles up some useful helpers as well as the Phoenix Template engine.
## Related

View file

@ -26,7 +26,7 @@ end
To use a component, you will use the special `c` keyword. This is called a "keyword" because it is not a function or macro, but only exists inside of the `Temple.temple/1` block.
The first argument will be the function reference to your component function, followed by any assigns.
The first argument will be the function reference to your component function, followed by any assigns. You can pass dynamic assigns using the `:rest!` keyword the same way you would with a normal tag.
```elixir
defmodule MyApp.ConfirmDialog do
@ -54,7 +54,7 @@ Slots are defined and rendered using the `slot` keyword. This is similar to the
### Default Slot
The default slot can be rendered from within your component by passing the `slot` the atom `:default`. Let's redefine our button component using slots.
The default slot can be rendered from within your component by passing the `slot` the `@inner_block` assign. Let's redefine our button component using slots.
```elixir
defmodule MyApp.Components do
@ -63,7 +63,7 @@ defmodule MyApp.Components do
def button(assigns) do
temple do
button type: "button", class: "bg-blue-800 text-white rounded #{@class}" do
slot :default
slot @inner_block
end
end
end
@ -109,18 +109,18 @@ defmodule MyApp.Components do
div class: "card" do
header class: "card-header", style: "background-color: @f5f5f5" do
p class: "card-header-title" do
slot :header
slot @header
end
end
div class: "card-content" do
div class: "content" do
slot :default
slot @inner_block
end
end
footer class: "card-footer", style: "background-color: #f5f5f5" do
slot :footer
slot @footer
end
end
end
@ -145,8 +145,8 @@ def MyApp.CardExample do
"This example demonstrates how to create components with multiple, named slots"
slot :footer do
a href="#", class: "card-footer-item", do: "Footer Item 1"
a href="#", class: "card-footer-item", do: "Footer Item 2"
a href: "#", class: "card-footer-item", do: "Footer Item 1"
a href: "#", class: "card-footer-item", do: "Footer Item 2"
end
end
end
@ -154,11 +154,15 @@ def MyApp.CardExample do
end
```
## Passing Data Through Slots
## Passing data to and through Slots
Sometimes it is necessary to pass data from a component definition back to the call site.
Sometimes it is necessary to pass data _into_ a slot (hereby known as *slot attributes*) from the call site and _from_ a component definition (hereby known as *slot arguments*) back to the call site. Dynamic slot attributes can be passed using the `:rest!` attribute in the same way you can with tag attributes.
Let's look at what a `table` component could look like.
Let's look at what a `table` component could look like. Here we observe we access an attribute in the slot in the header with `col.label`.
This example is taken from the HEEx documentation to demonstrate how you can build the same thing with Temple.
Note: Slot attributes can only be accessed on an individual slot, so if you define a single slot definition, you still need to loop through it to access it, as they are stored as a list.
#### Definition
@ -166,30 +170,23 @@ Let's look at what a `table` component could look like.
defmodule MyApp.Components do
import Temple
def cols(items) do
items
|> List.first()
|> Map.keys()
|> Enum.sort()
end
def table(assigns) do
temple do
table do
thead do
tr do
for col <- cols(@entries) do
tr do: String.upcase(to_string(col))
for col <- @col do
th do: col.label # 👈 accessing a slot attribute
end
end
end
tbody do
for row <- @entries do
for row <- @rows do
tr do
for col <- cols(@entries) do
for col <- @col do
td do
slot :cell, %{value: row[cell]}
slot col, row
end
end
end
@ -203,7 +200,7 @@ end
#### Usage
When we render the slot, we can pattern match on the data passed through the slot. If this seems familiar, it's because this is the same syntax you use when writing your tests using `ExUnit.Case.test/3`.
When we render the slot, we can pattern match on the data passed through the slot via the `:let` attribute.
```elixir
def MyApp.TableExample do
@ -213,24 +210,16 @@ def MyApp.TableExample do
def render(assigns) do
temple do
section do
h2 do: "Inventory Levels"
h2 do: "Users"
c &table/1, entries: @item_inventories do
slot :cell, %{value: value} do
case value do
0 ->
span class: "font-bold" do
"Out of stock!"
end
c &table/1, rows: @users do
# 👇 defining the parameter for the slot argument
slot :col, let!: user, label: "Name" do # 👈 passing a slot attribute
user.name
end
level when is_number(level) ->
span do
"#{level} in stock"
end
_ ->
span do: value
end
slot :col, let!: user, label: "Address" do
user.address
end
end
end

25
guides/converting-html.md Normal file
View file

@ -0,0 +1,25 @@
# Converting HTML
If you want to use something like [TailwindUI](https://tailwindui.com) with Temple, you're going to have to convert a ton of vanilla HTML into Temple syntax.
Luckily, Temple provides a mix task for converting an HTML file into Temple syntax and writes it to stdout.
## Usage
First, we would want to create a temporary HTML file with the HTML we'd like to convert.
> #### Hint {: .tip}
>
> The following examples use the `pbpaste` and `pbcopy` utilities found on macOS. These are used to send your clipboard contents into stdout and put stdout into your clipboard.
```shell
$ pbpaste > temp.html
```
Then, we can convert that file and copy the output into our clipboard.
```shell
$ mix temple.convert temp.html | pbcopy
```
Now, you are free to paste the new temple syntax into your project!

View file

@ -42,13 +42,16 @@ Temple works out of the box without any configuration, but here are a couple of
### Engine
By default, Temple uses the built in `EEx.SmartEngine`. If you want to use a different engine, this is as easy as setting the `:engine` configuration option.
By default, Temple uses the built in `Phoenix.HTML.Engine`. If you want to use a different engine, this is as easy as setting the `:engine` configuration option.
You can also configure the function that is used for runtime attributes. By default, Temple uses `Phoenix.HTML.attributes_escape/1`.
```elixir
# config/config.exs
config :temple,
engine: Phoenix.HTML.Engine
engine: EEx.SmartEngine,
attributes: {Temple, :attributes}
```
### Aliases

View file

@ -0,0 +1,95 @@
# Migrating from 0.10 to 0.11
Most of the changes in this release are related to tweaking Temple's component model to align with HEEx & Surface.
## Rendering Slots
Slots are now available as assigns in the component and are rendered as such.
### Before
```elixir
def my_component(assign) do
temple do
span do
slot :a_slot
end
end
end
```
### After
```elixir
def my_component(assign) do
temple do
span do
slot @a_slot
end
end
end
```
## :default slot has been renamed to :inner_block
The main body of a component has been renamed from `:default` to `:inner_block`.
Note: The "after" example also includes the necessary change specified above.
### Before
```elixir
def my_component(assign) do
temple do
span do
slot :default
end
end
end
```
### After
```elixir
def my_component(assign) do
temple do
span do
slot @inner_block
end
end
end
```
## Passing data into slots
The syntax for capturing data being passed from the call site of a slot to the definition of a slot (or put another way, from the definition of a component to the call site of the component) has changed.
You now capture it as the value of the `:let!` attribute on the slot definition.
### Before
```elixir
def my_component(assign) do
temple do
c &my_component/1 do
slot :a_slot, %{some: value} do
"I'm using some #{value}"
end
end
end
end
```
### After
```elixir
def my_component(assign) do
temple do
c &my_component/1 do
slot :a_slot, let!: %{some: value} do
"I'm using some #{value}"
end
end
end
end
```

View file

@ -1,3 +1,108 @@
# Migrating from 0.8 to 0.9
TODO: explain it
First off, Temple now requires Elixir 1.13 or higher. This is because of some changes that were brought to the Elixir parser.
## Whitespace Control
To control whitespace in an element, Temple will now control this based on whether the `do` was used in the keyword list syntax or the do/end syntax.
In 0.8, you would do:
```elixir
span do
"hello!"
end
# <span>
# hello!
# </span>
# The ! version of the element would render it as "tight"
span! do
"hello!"
end
# <span>hello!</span>
```
In 0.9, you would do:
```elixir
span do
"hello!"
end
# <span>
# hello!
# </span>
span do: "hello!"
# <span>hello!</span>
```
## Components
Components are no longer module based. To render a component, you can pass a function reference to the `c` keyword. You also no longer need to define a component in a module, using the `Temple.Component` module and its `render` macro.
In 0.8, you would define a component like:
```elixir
defmodule MyAppWeb.Component.Card do
import Temple.Component
render do
div class: "border p-4 rounded" do
slot :default
end
end
end
```
And you would use the component like:
```elixir
div do
c MyAppWeb.Component.Card do
"Welcome to my app!"
end
end
```
In 0.9, you would define a component like:
```elixir
defmodule MyAppWeb.Components do
import Temple
def card(assigns) do
temple do
div class: "border p-4 rounded" do
slot :default
end
end
end
end
```
And you would use the component like:
```elixir
div do
c &MyAppWeb.Components.card/1 do
"Welcome to my app!"
end
end
```
We can observe here that in 0.9 the component is just any 1-arity function, so you can define them anywhere and you can have more than 1 in a single module.
### defcomp
Now that components are just functions, you no longer need this special macro to define a component in the middle of the module.
This can simply be converted to a function.
## Phoenix
All Phoenix related items have moved to the [temple_phoenix](https://github.com/mhanberg/temple_phoenix) package. Please see that library docs for more details.

View file

@ -121,54 +121,34 @@ end
## Attributes
Attributes are declared as a keyword list.
Temple leverages `Phoenix.HTML.attributes_escape/1` internally, so you can refer to it's documentation for all of the details.
- Keys with underscores are converted to the kebab syntax.
- Values can be Elixir expressions.
- Values that are compile time `true` will be emitted as a boolean attribute. `disabled` and `checked` are examples of boolean attributes.
- Values that are compile time `false` will not be emitted into the document at all.
- The class attribute has a special "object syntax" that allows you to specify classes as a keyword list, only emitting classes that evaluate to true into the final class.
### Dynamic Attributes
Let's look at an example.
To render dynamic attributes into a tag, you can pass them with the reserved attribute `:rest!`.
```elixir
assigns = %{highlight?: false, user_name: "Mitch"}
assigns = %{
data: [data_foo: "hi"]
}
temple do
div id: "hero" do
h2 class: "font-bold", do: "Profile"
section data_controller: "hero" do
p class: ["border": @highlight?] do
"Name: #{@user_name}"
end
video autoplay: true, src: "https://example.com/rick-rolled.mp4"
end
div id: "foo", rest!: @data do
"Hello, world!"
end
end
```
...will emit markup that looks like...
will render to
```html
<div id="hero">
<h2 class="font-bold">Profile</h2>
<section data-controller="hero">
<p class="">
Name: Mitch
</p>
</section>
<video autoplay src="https://example.com/rick-rolled.mp4"></video>
<div id="foo" data-foo="hi">
Hello, world!
</div>
```
## Elixir Expressions
### They Just Work
Any Elixir expression can be used anywhere inside of a Temple template. Here are a few examples.
```elixir

View file

@ -0,0 +1,34 @@
defmodule Mix.Tasks.Temple.Convert do
use Mix.Task
@shortdoc "A task to convert vanilla HTML into Temple syntax"
@moduledoc """
This task is useful for converting a ton of HTML into Temple syntax.
> #### Note about EEx and HEEx {: .tip}
>
> In the future, this should be able to convert EEx and HEEx as well, but that would involve invoking or forking their parsers. That is certainly doable, but is out of scope for what I needed right now. Contributions are welcome!
## Usage
```shell
$ mix temple.convert some_file.html
```
"""
@doc false
def run(argv) do
case argv do
[] ->
Mix.raise(
"You need to provide the path to an HTML file you would like to convert to Temple syntax"
)
[file] ->
file
|> File.read!()
|> Temple.Converter.convert()
|> IO.puts()
end
end
end

View file

@ -1,6 +1,4 @@
defmodule Temple do
@engine Application.compile_env(:temple, :engine, EEx.SmartEngine)
@moduledoc """
Temple syntax is available inside the `temple`, and is compiled into efficient Elixir code at compile time using the configured `EEx.Engine`.
@ -93,21 +91,45 @@ defmodule Temple do
<link href="/css/site.css">
```
"""
defmacro temple(block) do
opts = [engine: engine()]
quote do
require Temple.Renderer
Temple.Renderer.compile(unquote(opts), unquote(block))
Temple.Renderer.compile(unquote(block))
end
end
@doc false
def component(func, assigns) do
apply(func, [assigns])
end
defdelegate engine, to: Temple.Renderer
@doc false
def engine(), do: @engine
@doc """
Compiles runtime attributes.
To use this function, you set it in application config.
By default, Temple uses `{Phoenix.HTML, :attributes_escape}`. This is useful if you want to use `EEx.SmartEngine`.
```elixir
config :temple,
engine: EEx.SmartEngine,
attributes: {Temple, :attributes}
```
> #### Note {: .info}
>
> This function does not do any HTML escaping
> #### Note {: .info}
>
> This function is used by the compiler and shouldn't need to be used directly.
"""
def attributes(attributes) do
for {key, value} <- attributes, into: "" do
case value do
true -> ~s| #{key}|
false -> ""
value -> ~s| #{key}="#{value}"|
end
end
end
end

View file

@ -1,6 +1,19 @@
defmodule Temple.Ast do
@moduledoc false
@type t ::
Temple.Ast.Empty.t()
| Temple.Ast.Text.t()
| Temple.Ast.Components.t()
| Temple.Ast.Slot.t()
| Temple.Ast.NonvoidElementsAliases.t()
| Temple.Ast.VoidElementsAliases.t()
| Temple.Ast.AnonymousFunctions.t()
| Temple.Ast.RightArrow.t()
| Temple.Ast.DoExpressions.t()
| Temple.Ast.Match.t()
| Temple.Ast.Default.t()
def new(module, opts \\ []) do
struct(module, opts)
end

View file

@ -1,14 +1,17 @@
defmodule Temple.Parser.AnonymousFunctions do
defmodule Temple.Ast.AnonymousFunctions do
@moduledoc false
@behaviour Temple.Parser
defstruct elixir_ast: nil, children: []
use TypedStruct
alias Temple.Parser
typedstruct do
field :elixir_ast, Macro.t()
field :children, [map()]
end
@impl Parser
@impl true
def applicable?({_, _, args}) do
import Temple.Parser.Utils, only: [split_args: 1]
import Temple.Ast.Utils, only: [split_args: 1]
args
|> split_args()
@ -18,11 +21,11 @@ defmodule Temple.Parser.AnonymousFunctions do
def applicable?(_), do: false
@impl Parser
@impl true
def run({_name, _, args} = expression) do
{_do_and_else, args} = Temple.Parser.Utils.split_args(args)
{_do_and_else, args} = Temple.Ast.Utils.split_args(args)
{_args, func_arg, _args2} = Temple.Parser.Utils.split_on_fn(args, {[], nil, []})
{_args, func_arg, _args2} = Temple.Ast.Utils.split_on_fn(args, {[], nil, []})
{_func, _, [{_arrow, _, [[{_arg, _, _}], block]}]} = func_arg

View file

@ -1,41 +1,48 @@
defmodule Temple.Parser.Components do
defmodule Temple.Ast.Components do
@moduledoc false
@behaviour Temple.Parser
defstruct function: nil, assigns: [], children: [], slots: []
use TypedStruct
@impl Temple.Parser
typedstruct do
field :function, function()
field :arguments, map()
field :slots, [function()]
end
@impl true
def applicable?({:c, _, _}) do
true
end
def applicable?(_), do: false
@impl Temple.Parser
@impl true
def run({:c, _meta, [component_function | args]}) do
{do_and_else, args} =
args
|> Temple.Parser.Utils.split_args()
|> Temple.Ast.Utils.split_args()
{do_and_else, assigns} = Temple.Parser.Utils.consolidate_blocks(do_and_else, args)
{do_and_else, arguments} = Temple.Ast.Utils.consolidate_blocks(do_and_else, args)
{default_slot, {_, named_slots}} =
if children = do_and_else[:do] do
Macro.prewalk(
children,
{component_function, %{}},
{component_function, []},
fn
{:c, _, [name | _]} = node, {_, named_slots} ->
{node, {name, named_slots}}
{:slot, _, [name | args]} = node, {^component_function, named_slots} ->
{assigns, slot} = split_assigns_and_children(args, Macro.escape(%{}))
{arguments, slot} = split_assigns_and_children(args, nil)
if is_nil(slot) do
{node, {component_function, named_slots}}
else
{nil,
{component_function, Map.put(named_slots, name, %{assigns: assigns, slot: slot})}}
{parameter, attributes} = Keyword.pop(arguments || [], :let!)
new_slot = {name, %{parameter: parameter, slot: slot, attributes: attributes}}
{nil, {component_function, named_slots ++ [new_slot]}}
end
node, acc ->
@ -50,37 +57,45 @@ defmodule Temple.Parser.Components do
if default_slot == nil do
[]
else
Temple.Parser.parse(default_slot)
[
Temple.Ast.new(
Temple.Ast.Slottable,
name: :inner_block,
content: Temple.Parser.parse(default_slot)
)
]
end
slots =
for {name, %{slot: slot, assigns: assigns}} <- named_slots do
for {name, %{slot: slot, parameter: parameter, attributes: attributes}} <- named_slots do
Temple.Ast.new(
Temple.Parser.Slottable,
Temple.Ast.Slottable,
name: name,
content: Temple.Parser.parse(slot),
assigns: assigns
parameter: parameter,
attributes: attributes
)
end
slots = children ++ slots
Temple.Ast.new(__MODULE__,
function: component_function,
assigns: assigns,
slots: slots,
children: children
arguments: arguments,
slots: slots
)
end
defp split_assigns_and_children(args, empty) do
case args do
[assigns, [do: block]] ->
{assigns, block}
[arguments, [do: block]] ->
{arguments, block}
[[do: block]] ->
{empty, block}
[assigns] ->
{assigns, nil}
[arguments] ->
{arguments, nil}
_ ->
{empty, nil}

View file

@ -1,15 +1,17 @@
defmodule Temple.Parser.Default do
defmodule Temple.Ast.Default do
@moduledoc false
@behaviour Temple.Parser
defstruct elixir_ast: nil
use TypedStruct
alias Temple.Parser
typedstruct do
field :elixir_ast, Macro.t()
end
@impl Parser
@impl true
def applicable?(_ast), do: true
@impl Parser
@impl true
def run(ast) do
Temple.Ast.new(__MODULE__, elixir_ast: ast)
end

View file

@ -1,21 +1,24 @@
defmodule Temple.Parser.DoExpressions do
defmodule Temple.Ast.DoExpressions do
@moduledoc false
alias Temple.Parser
@behaviour Temple.Parser
@behaviour Parser
use TypedStruct
defstruct elixir_ast: nil, children: []
typedstruct do
field :elixir_ast, Macro.t()
field :children, [map()]
end
@impl Parser
@impl true
def applicable?({_, _, args}) when is_list(args) do
Enum.any?(args, fn arg -> match?([{:do, _} | _], arg) end)
end
def applicable?(_), do: false
@impl Parser
@impl true
def run({name, meta, args}) do
{do_and_else, args} = Temple.Parser.Utils.split_args(args)
{do_and_else, args} = Temple.Ast.Utils.split_args(args)
do_body = Temple.Parser.parse(do_and_else[:do])

View file

@ -1,14 +1,19 @@
defmodule Temple.Parser.ElementList do
defmodule Temple.Ast.ElementList do
@moduledoc false
@behaviour Temple.Parser
defstruct children: [], whitespace: :loose
use TypedStruct
@impl Temple.Parser
typedstruct do
field :children, list()
field :whitespace, :loose | :tight
end
@impl true
def applicable?(asts), do: is_list(asts)
@impl Temple.Parser
@impl true
def run(asts) do
children = Enum.flat_map(asts, &Temple.Parser.parse/1)

View file

@ -1,16 +1,18 @@
defmodule Temple.Parser.Empty do
defmodule Temple.Ast.Empty do
@moduledoc false
use TypedStruct
@behaviour Temple.Parser
defstruct []
typedstruct do
end
alias Temple.Parser
@impl Parser
@impl true
def applicable?(ast) when ast in [nil, []], do: true
def applicable?(_), do: false
@impl Parser
@impl true
def run(_ast) do
Temple.Ast.new(__MODULE__)
end

View file

@ -1,19 +1,21 @@
defmodule Temple.Parser.Match do
defmodule Temple.Ast.Match do
@moduledoc false
@behaviour Temple.Parser
defstruct elixir_ast: nil
use TypedStruct
alias Temple.Parser
typedstruct do
field :elixir_ast, Macro.t()
end
@impl Parser
@impl true
def applicable?({name, _, _}) do
name in [:=]
end
def applicable?(_), do: false
@impl Parser
@impl true
def run(macro) do
Temple.Ast.new(__MODULE__, elixir_ast: macro)
end

View file

@ -1,25 +1,32 @@
defmodule Temple.Parser.NonvoidElementsAliases do
defmodule Temple.Ast.NonvoidElementsAliases do
@moduledoc false
@behaviour Temple.Parser
defstruct name: nil, attrs: [], children: [], meta: %{}
use TypedStruct
typedstruct do
field :name, atom()
field :attrs, list()
field :children, list()
field :meta, map()
end
alias Temple.Parser
@impl Parser
@impl true
def applicable?({name, _, _}) do
name in Parser.nonvoid_elements_aliases()
end
def applicable?(_), do: false
@impl Parser
@impl true
def run({name, meta, args}) do
name = Parser.nonvoid_elements_lookup()[name]
{do_and_else, args} = Temple.Parser.Utils.split_args(args)
{do_and_else, args} = Temple.Ast.Utils.split_args(args)
{do_and_else, args} = Temple.Parser.Utils.consolidate_blocks(do_and_else, args)
{do_and_else, args} = Temple.Ast.Utils.consolidate_blocks(do_and_else, args)
children = Temple.Parser.parse(do_and_else[:do])
@ -28,7 +35,7 @@ defmodule Temple.Parser.NonvoidElementsAliases do
attrs: args,
meta: %{whitespace: whitespace(meta)},
children:
Temple.Ast.new(Temple.Parser.ElementList,
Temple.Ast.new(Temple.Ast.ElementList,
children: children,
whitespace: whitespace(meta)
)

View file

@ -1,18 +1,22 @@
defmodule Temple.Parser.RightArrow do
defmodule Temple.Ast.RightArrow do
@moduledoc false
alias Temple.Parser
@behaviour Parser
@behaviour Temple.Parser
defstruct elixir_ast: nil, children: []
use TypedStruct
@impl Parser
typedstruct do
field :elixir_ast, Macro.t()
field :children, [map()]
end
@impl true
def applicable?({:->, _, _}), do: true
def applicable?(_), do: false
@impl Parser
@impl true
def run({func, meta, [pattern, args]}) do
children = Parser.parse(args)
children = Temple.Parser.parse(args)
Temple.Ast.new(__MODULE__, elixir_ast: {func, meta, [pattern]}, children: children)
end

View file

@ -1,8 +1,12 @@
defmodule Temple.Parser.Slot do
defmodule Temple.Ast.Slot do
@moduledoc false
@behaviour Temple.Parser
use TypedStruct
defstruct name: nil, args: []
typedstruct do
field :name, atom()
field :args, list(), default: []
end
@impl true
def applicable?({:slot, _, _}) do

View file

@ -0,0 +1,12 @@
defmodule Temple.Ast.Slottable do
@moduledoc false
use TypedStruct
typedstruct do
field :content, [Temple.Ast.t()]
field :parameter, Macro.t()
field :name, atom()
field :attributes, Macro.t(), default: []
end
end

View file

@ -1,19 +1,19 @@
defmodule Temple.Parser.TempleNamespaceNonvoid do
defmodule Temple.Ast.TempleNamespaceNonvoid do
@moduledoc false
@behaviour Temple.Parser
alias Temple.Parser
@impl Parser
@impl true
def applicable?({{:., _, [{:__aliases__, _, [:Temple]}, name]}, _meta, _args}) do
name in Parser.nonvoid_elements_aliases()
end
def applicable?(_), do: false
@impl Parser
@impl true
def run({name, meta, args}) do
{:., _, [{:__aliases__, _, [:Temple]}, name]} = name
Temple.Parser.NonvoidElementsAliases.run({name, meta, args})
Temple.Ast.NonvoidElementsAliases.run({name, meta, args})
end
end

View file

@ -1,18 +1,18 @@
defmodule Temple.Parser.TempleNamespaceVoid do
defmodule Temple.Ast.TempleNamespaceVoid do
@moduledoc false
@behaviour Temple.Parser
@impl Temple.Parser
@impl true
def applicable?({{:., _, [{:__aliases__, _, [:Temple]}, name]}, _meta, _args}) do
name in Temple.Parser.void_elements_aliases()
end
def applicable?(_), do: false
@impl Temple.Parser
@impl true
def run({name, meta, args}) do
{:., _, [{:__aliases__, _, [:Temple]}, name]} = name
Temple.Parser.VoidElementsAliases.run({name, meta, args})
Temple.Ast.VoidElementsAliases.run({name, meta, args})
end
end

View file

@ -1,16 +1,18 @@
defmodule Temple.Parser.Text do
defmodule Temple.Ast.Text do
@moduledoc false
@behaviour Temple.Parser
defstruct text: nil
use TypedStruct
alias Temple.Parser
typedstruct do
field :text, String.t()
end
@impl Parser
@impl true
def applicable?(text) when is_binary(text), do: true
def applicable?(_), do: false
@impl Parser
@impl true
def run(text) do
Temple.Ast.new(__MODULE__, text: text)
end

View file

@ -1,6 +1,12 @@
defmodule Temple.Parser.Utils do
defmodule Temple.Ast.Utils do
@moduledoc false
@attributes Application.compile_env(
:temple,
:attributes,
{Phoenix.HTML, :attributes_escape}
)
def snake_to_kebab(stringable),
do: stringable |> to_string() |> String.replace_trailing("_", "") |> String.replace("_", "-")
@ -25,7 +31,7 @@ defmodule Temple.Parser.Utils do
[{:text, " " <> name <> "=\"" <> to_string(value) <> "\""} | acc]
else
true ->
nodes = Temple.Parser.Utils.build_attr(name, value)
nodes = Temple.Ast.Utils.build_attr(name, value)
Enum.reverse(nodes) ++ acc
end
end
@ -34,7 +40,7 @@ defmodule Temple.Parser.Utils do
[
{:expr,
quote do
unquote(List.first(attrs))
unquote(__MODULE__).__attributes__(unquote(List.first(attrs)))
end}
]
end
@ -48,8 +54,28 @@ defmodule Temple.Parser.Utils do
[]
end
def build_attr("rest!", values) when is_list(values) do
Enum.flat_map(values, fn {name, value} ->
build_attr(snake_to_kebab(name), value)
end)
end
def build_attr("rest!", {_, _, _} = value) do
expr =
quote do
unquote(__MODULE__).__attributes__(unquote(value))
end
[{:expr, expr}]
end
def build_attr(name, {_, _, _} = value) do
[{:text, ~s' #{name}="'}, {:expr, value}, {:text, ~s'"'}]
expr =
quote do
unquote(__MODULE__).__attributes__([{unquote(name), unquote(value)}])
end
[{:expr, expr}]
end
def build_attr("class", classes) when is_list(classes) do
@ -134,4 +160,10 @@ defmodule Temple.Parser.Utils do
ast
end
def __attributes__(attributes) do
{mod, func} = @attributes
apply(mod, func, [attributes])
end
end

View file

@ -1,20 +1,25 @@
defmodule Temple.Parser.VoidElementsAliases do
defmodule Temple.Ast.VoidElementsAliases do
@moduledoc false
@behaviour Temple.Parser
defstruct name: nil, attrs: []
use TypedStruct
@impl Temple.Parser
typedstruct do
field :name, atom()
field :attrs, list(), default: []
end
@impl true
def applicable?({name, _, _}) do
name in Temple.Parser.void_elements_aliases()
end
def applicable?(_), do: false
@impl Temple.Parser
@impl true
def run({name, _, args}) do
args =
case Temple.Parser.Utils.split_args(args) do
case Temple.Ast.Utils.split_args(args) do
{_, [args]} when is_list(args) ->
args

124
lib/temple/component.ex Normal file
View file

@ -0,0 +1,124 @@
defmodule Temple.Component do
@moduledoc """
Use this module to create your own component implementation.
This is only required if you are not using a component implementation from another framework,
like Phoenix LiveView.
At it's core, a component implmentation includes the following functions
- `component/2`
- `inner_block/2`
- `render_slot/2`
These functions are used by the template compiler, so you won't be calling them directly.
## Usage
Invoke the `__using__/1` macro to create your own module, and then import that module where you
need to define define or use components (usually everywhere).
We'll use an example that is similar to what Temple uses in its own test suite..
```elixir
defmodule MyAppWeb.Component do
use Temple.Component
defmacro __using__(_) do
quote do
import Temple
import unquote(__MODULE__)
end
end
end
```
Then you can `use` your module when you want to define or use a component.
```elixir
defmodule MyAppWeb.Components do
use MyAppWeb.Component
def basic_component(_assigns) do
temple do
div do
"I am a basic component"
end
end
end
end
```
"""
defmacro __using__(_) do
quote do
import Temple
@doc false
def component(func, assigns, _) do
apply(func, [assigns])
end
defmacro inner_block(_name, do: do_block) do
__inner_block__(do_block)
end
@doc false
def __inner_block__([{:->, meta, _} | _] = do_block) do
inner_fun = {:fn, meta, do_block}
quote do
fn arg ->
_ = var!(assigns)
unquote(inner_fun).(arg)
end
end
end
def __inner_block__(do_block) do
quote do
fn arg ->
_ = var!(assigns)
unquote(do_block)
end
end
end
defmacro render_slot(slot, arg) do
quote do
unquote(__MODULE__).__render_slot__(unquote(slot), unquote(arg))
end
end
@doc false
def __render_slot__([], _), do: nil
def __render_slot__([entry], argument) do
call_inner_block!(entry, argument)
end
def __render_slot__(entries, argument) when is_list(entries) do
assigns = %{}
_ = assigns
temple do
for entry <- entries do
call_inner_block!(entry, argument)
end
end
end
def __render_slot__(entry, argument) when is_map(entry) do
entry.inner_block.(argument)
end
defp call_inner_block!(entry, argument) do
if !entry.inner_block do
message = "attempted to render slot #{entry.__slot__} but the slot has no inner content"
raise RuntimeError, message
end
entry.inner_block.(argument)
end
end
end
end

103
lib/temple/converter.ex Normal file
View file

@ -0,0 +1,103 @@
defmodule Temple.Converter do
@moduledoc false
@boolean_attributes ~w[
allowfullscreen
async
autofocus
autoplay
checked
controls
default
defer
disabled
formnovalidate
ismap
itemscope
loop
multiple
muted
nomodule
novalidate
open
playsinline
readonly
required
reversed
selected
truespeed
]
def convert(html) do
html
|> Floki.parse_fragment!()
|> to_temple()
|> :erlang.iolist_to_binary()
|> Code.format_string!()
|> :erlang.iolist_to_binary()
end
def to_temple([]) do
[]
end
def to_temple([{tag, attrs, children} | rest]) do
[
to_string(tag),
" ",
to_temple_attrs(attrs),
" do\n",
to_temple(children),
"end\n"
] ++ to_temple(rest)
end
def to_temple([{:comment, comment} | rest]) do
[
comment
|> String.split("\n")
|> Enum.map_join("\n", fn line ->
if String.trim(line) != "" do
"# #{line}"
else
""
end
end),
"\n"
] ++ to_temple(rest)
end
def to_temple([text | rest]) when is_binary(text) do
[
text
|> String.split("\n")
|> Enum.map_join("\n", fn line ->
if String.trim(line) != "" do
escaped = String.replace(line, ~s|"|, ~s|\\"|)
~s|"#{String.trim(escaped)}"|
else
""
end
end),
"\n"
] ++ to_temple(rest)
end
defp to_temple_attrs([]) do
""
end
defp to_temple_attrs(attrs) do
Enum.map_join(attrs, ", ", fn
{attr, _value} when attr in @boolean_attributes ->
to_attr_name(attr) <> ": true"
{attr, value} ->
~s|#{to_attr_name(attr)}: "#{value}"|
end)
end
defp to_attr_name(name) do
String.replace(name, "-", "_")
end
end

View file

@ -1,32 +1,21 @@
defmodule Temple.Parser do
@moduledoc false
alias Temple.Parser.Empty
alias Temple.Parser.Text
alias Temple.Parser.TempleNamespaceNonvoid
alias Temple.Parser.TempleNamespaceVoid
alias Temple.Parser.Components
alias Temple.Parser.Slot
alias Temple.Parser.NonvoidElementsAliases
alias Temple.Parser.VoidElementsAliases
alias Temple.Parser.AnonymousFunctions
alias Temple.Parser.RightArrow
alias Temple.Parser.DoExpressions
alias Temple.Parser.Match
alias Temple.Parser.Default
alias Temple.Ast.AnonymousFunctions
alias Temple.Ast.Components
alias Temple.Ast.Default
alias Temple.Ast.DoExpressions
alias Temple.Ast.Empty
alias Temple.Ast.Match
alias Temple.Ast.NonvoidElementsAliases
alias Temple.Ast.RightArrow
alias Temple.Ast.Slot
alias Temple.Ast.TempleNamespaceNonvoid
alias Temple.Ast.TempleNamespaceVoid
alias Temple.Ast.Text
alias Temple.Ast.VoidElementsAliases
@type ast ::
%Empty{}
| %Text{}
| %Components{}
| %Slot{}
| %NonvoidElementsAliases{}
| %VoidElementsAliases{}
| %AnonymousFunctions{}
| %RightArrow{}
| %DoExpressions{}
| %Match{}
| %Default{}
@aliases Application.compile_env(:temple, :aliases, [])
@doc """
Should return true if the parser should apply for the given AST.
@ -38,9 +27,113 @@ defmodule Temple.Parser do
Should return Temple.AST.
"""
@callback run(ast :: Macro.t()) :: ast()
@callback run(ast :: Macro.t()) :: Temple.Ast.t()
@aliases Application.compile_env(:temple, :aliases, [])
@void_svg_lookup [
circle: "circle",
ellipse: "ellipse",
line: "line",
path: "path",
polygon: "polygon",
polyline: "polyline",
rect: "rect",
stop: "stop",
use: "use"
]
@void_svg_aliases Keyword.keys(@void_svg_lookup)
@nonvoid_svg_lookup [
a: "a",
altGlyph: "altGlyph",
altGlyphDef: "altGlyphDef",
altGlyphItem: "altGlyphItem",
animate: "animate",
animateColor: "animateColor",
animateMotion: "animateMotion",
animateTransform: "animateTransform",
animation: "animation",
audio: "audio",
canvas: "canvas",
clipPath: "clipPath",
cursor: "cursor",
defs: "defs",
desc: "desc",
discard: "discard",
feBlend: "feBlend",
feColorMatrix: "feColorMatrix",
feComponentTransfer: "feComponentTransfer",
feComposite: "feComposite",
feConvolveMatrix: "feConvolveMatrix",
feDiffuseLighting: "feDiffuseLighting",
feDisplacementMap: "feDisplacementMap",
feDistantLight: "feDistantLight",
feDropShadow: "feDropShadow",
feFlood: "feFlood",
feFuncA: "feFuncA",
feFuncB: "feFuncB",
feFuncG: "feFuncG",
feFuncR: "feFuncR",
feGaussianBlur: "feGaussianBlur",
feImage: "feImage",
feMerge: "feMerge",
feMergeNode: "feMergeNode",
feMorphology: "feMorphology",
feOffset: "feOffset",
fePointLight: "fePointLight",
feSpecularLighting: "feSpecularLighting",
feSpotLight: "feSpotLight",
feTile: "feTile",
feTurbulence: "feTurbulence",
filter: "filter",
font: "font",
foreignObject: "foreignObject",
g: "g",
glyph: "glyph",
glyphRef: "glyphRef",
handler: "handler",
hatch: "hatch",
hatchpath: "hatchpath",
hkern: "hkern",
iframe: "iframe",
image: "image",
linearGradient: "linearGradient",
listener: "listener",
marker: "marker",
mask: "mask",
mesh: "mesh",
meshgradient: "meshgradient",
meshpatch: "meshpatch",
meshrow: "meshrow",
metadata: "metadata",
mpath: "mpath",
pattern: "pattern",
prefetch: "prefetch",
radialGradient: "radialGradient",
script: "script",
set: "set",
solidColor: "solidColor",
solidcolor: "solidcolor",
style: "style",
svg: "svg",
switch: "switch",
symbol: "symbol",
tbreak: "tbreak",
text: "text",
textArea: "textArea",
textPath: "textPath",
title: "title",
tref: "tref",
tspan: "tspan",
unknown: "unknown",
video: "video",
view: "view",
vkern: "vkern"
]
@nonvoid_svg_aliases Keyword.keys(@nonvoid_svg_lookup)
# nonvoid tags
@nonvoid_elements ~w[
head title style script
@ -67,9 +160,9 @@ defmodule Temple.Parser do
{Keyword.get(@aliases, el, el), el}
end)
def nonvoid_elements, do: @nonvoid_elements
def nonvoid_elements_aliases, do: @nonvoid_elements_aliases
def nonvoid_elements_lookup, do: @nonvoid_elements_lookup
def nonvoid_elements, do: @nonvoid_elements ++ Keyword.values(@nonvoid_svg_aliases)
def nonvoid_elements_aliases, do: @nonvoid_elements_aliases ++ @nonvoid_svg_aliases
def nonvoid_elements_lookup, do: @nonvoid_elements_lookup ++ @nonvoid_svg_lookup
@void_elements ~w[
meta link base
@ -81,9 +174,9 @@ defmodule Temple.Parser do
{Keyword.get(@aliases, el, el), el}
end)
def void_elements, do: @void_elements
def void_elements_aliases, do: @void_elements_aliases
def void_elements_lookup, do: @void_elements_lookup
def void_elements, do: @void_elements ++ Keyword.values(@void_svg_aliases)
def void_elements_aliases, do: @void_elements_aliases ++ @void_svg_aliases
def void_elements_lookup, do: @void_elements_lookup ++ @void_svg_lookup
def parsers() do
[

View file

@ -1,5 +0,0 @@
defmodule Temple.Parser.Slottable do
@moduledoc false
defstruct content: nil, assigns: Macro.escape(%{}), name: nil
end

View file

@ -1,35 +1,38 @@
defmodule Temple.Renderer do
@moduledoc false
alias Temple.Parser.ElementList
alias Temple.Parser.Text
alias Temple.Parser.Components
alias Temple.Parser.Slot
alias Temple.Parser.NonvoidElementsAliases
alias Temple.Parser.VoidElementsAliases
alias Temple.Parser.AnonymousFunctions
alias Temple.Parser.RightArrow
alias Temple.Parser.DoExpressions
alias Temple.Parser.Match
alias Temple.Parser.Default
alias Temple.Parser.Empty
alias Temple.Ast.ElementList
alias Temple.Ast.Text
alias Temple.Ast.Components
alias Temple.Ast.Slot
alias Temple.Ast.Slottable
alias Temple.Ast.NonvoidElementsAliases
alias Temple.Ast.VoidElementsAliases
alias Temple.Ast.AnonymousFunctions
alias Temple.Ast.RightArrow
alias Temple.Ast.DoExpressions
alias Temple.Ast.Match
alias Temple.Ast.Default
alias Temple.Ast.Empty
alias Temple.Parser.Utils
alias Temple.Ast.Utils
@default_engine EEx.SmartEngine
@engine Application.compile_env(:temple, :engine, Phoenix.HTML.Engine)
@doc false
def engine(), do: @engine
defmacro compile(opts \\ [], do: block) do
defmacro compile(do: block) do
block
|> Temple.Parser.parse()
|> Temple.Renderer.render(opts)
|> Temple.Renderer.render(engine: @engine)
# |> Temple.Parser.Utils.inspect_ast()
# |> Temple.Ast.Utils.inspect_ast()
end
def render(asts, opts \\ [])
def render(asts, opts) when is_list(asts) and is_list(opts) do
engine = Keyword.get(opts, :engine, @default_engine)
engine = Keyword.get(opts, :engine, Phoenix.HTML.Engine)
state = %{
engine: engine,
@ -37,7 +40,7 @@ defmodule Temple.Renderer do
terminal_node: false
}
buffer = engine.init(%{})
buffer = engine.init([])
buffer =
for ast <- asts, reduce: buffer do
@ -73,25 +76,11 @@ defmodule Temple.Renderer do
def render(buffer, state, %Components{
function: function,
assigns: assigns,
children: children,
arguments: arguments,
slots: slots
}) do
child_quoted =
if Enum.any?(children) do
children_buffer = state.engine.handle_begin(buffer)
children_buffer =
for child <- children(children), reduce: children_buffer do
children_buffer ->
render(children_buffer, state, child)
end
state.engine.handle_end(children_buffer)
end
slot_quotes =
for slot <- slots do
Enum.group_by(slots, & &1.name, fn %Slottable{} = slot ->
slot_buffer = state.engine.handle_begin(buffer)
slot_buffer =
@ -102,29 +91,48 @@ defmodule Temple.Renderer do
ast = state.engine.handle_end(slot_buffer)
[quoted] =
inner_block =
quote do
{unquote(slot.name), unquote(slot.assigns)} ->
unquote(ast)
inner_block unquote(slot.name) do
unquote(slot.parameter || quote(do: _)) ->
unquote(ast)
end
end
quoted
end
{rest, attributes} = Keyword.pop(slot.attributes, :rest!, [])
{:fn, meta, clauses} =
quote do
fn
{:default, _} -> unquote(child_quoted)
slot =
{:%{}, [],
[
__slot__: slot.name,
inner_block: inner_block
] ++ attributes}
quote do
Map.merge(unquote(slot), Map.new(unquote(rest)))
end
end
end)
slot_func = {:fn, meta, slot_quotes ++ clauses}
{rest, arguments} = Keyword.pop(arguments, :rest!, [])
component_arguments =
{:%{}, [],
arguments
|> Map.new()
|> Map.merge(slot_quotes)
|> Enum.to_list()}
component_arguments =
quote do
Map.merge(unquote(component_arguments), Map.new(unquote(rest)))
end
expr =
quote do
component(
unquote(function),
Map.put(Map.new(unquote(assigns)), :__slots__, unquote(slot_func))
unquote(component_arguments),
{__MODULE__, __ENV__.function, __ENV__.file, __ENV__.line}
)
end
@ -134,7 +142,9 @@ defmodule Temple.Renderer do
def render(buffer, state, %Slot{} = ast) do
render_slot_func =
quote do
var!(assigns).__slots__.({unquote(ast.name), Map.new(unquote(ast.args))})
{rest, args} = Map.pop(Map.new(unquote(ast.args)), :rest!, [])
args = Map.merge(args, Map.new(rest))
render_slot(unquote(ast.name), args)
end
state.engine.handle_expr(buffer, "=", render_slot_func)
@ -242,7 +252,7 @@ defmodule Temple.Renderer do
{name, meta, args} = ast.elixir_ast
{args, {func, fmeta, [{arrow, arrowmeta, [first, _block]}]}, args2} =
Temple.Parser.Utils.split_on_fn(args, {[], nil, []})
Temple.Ast.Utils.split_on_fn(args, {[], nil, []})
full_ast =
{name, meta, args ++ [{func, fmeta, [{arrow, arrowmeta, [first, inner_quoted]}]}] ++ args2}

25
mix.exs
View file

@ -6,7 +6,7 @@ defmodule Temple.MixProject do
app: :temple,
name: "Temple",
description: "An HTML DSL for Elixir",
version: "0.9.0-rc.0",
version: "0.12.0",
package: package(),
elixirc_paths: elixirc_paths(Mix.env()),
elixir: "~> 1.13",
@ -18,7 +18,17 @@ defmodule Temple.MixProject do
end
# Specifies which paths to compile per environment.
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(:test) do
# hack to get the right compiler options used on the non-script files in
# test/support
Code.put_compiler_option(
:parser_options,
Keyword.put(Code.get_compiler_option(:parser_options), :token_metadata, true)
)
["lib", "test/support"]
end
defp elixirc_paths(_), do: ["lib"]
# Run "mix help compile.app" to learn about applications.
@ -36,7 +46,9 @@ defmodule Temple.MixProject do
"guides/getting-started.md",
"guides/your-first-template.md",
"guides/components.md",
"guides/migrating/0.8-to-0.9.md"
"guides/converting-html.md",
"guides/migrating/0.8-to-0.9.md",
"guides/migrating/0.10-to-0.11.md"
],
groups_for_extras: groups_for_extras()
]
@ -54,13 +66,16 @@ defmodule Temple.MixProject do
maintainers: ["Mitchell Hanberg"],
licenses: ["MIT"],
links: %{github: "https://github.com/mhanberg/temple"},
files: ~w(lib priv CHANGELOG.md LICENSE mix.exs README.md .formatter.exs)
files: ~w(lib CHANGELOG.md LICENSE mix.exs README.md .formatter.exs)
]
end
defp deps do
[
{:ex_doc, "~> 0.28.3", only: :dev, runtime: false}
{:floki, ">= 0.0.0"},
{:phoenix_html, "~> 3.2"},
{:typed_struct, "~> 0.3"},
{:ex_doc, "~> 0.30.0", only: :dev, runtime: false}
]
end
end

View file

@ -1,8 +1,12 @@
%{
"earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"},
"ex_doc": {:hex, :ex_doc, "0.28.6", "2bbd7a143d3014fc26de9056793e97600ae8978af2ced82c2575f130b7c0d7d7", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bca1441614654710ba37a0e173079273d619f9160cbcc8cd04e6bd59f1ad0e29"},
"ex_doc": {:hex, :ex_doc, "0.30.0", "ed94bf5183f559d2f825e4f866cc0eab277bbb17da76aff40f8e0f149656943e", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "6743fe46704fe27e2f2558faa61f00e5356528768807badb2092d38476d6dac2"},
"floki": {:hex, :floki, "0.34.0", "002d0cc194b48794d74711731db004fafeb328fe676976f160685262d43706a8", [:mix], [], "hexpm", "9c3a9f43f40dde00332a589bd9d389b90c1f518aef500364d00636acc5ebc99c"},
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
"phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
"typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"},
}

10
test/support/component.ex Normal file
View file

@ -0,0 +1,10 @@
defmodule Temple.Support.Component do
use Temple.Component
defmacro __using__(_) do
quote do
import Temple
import unquote(__MODULE__)
end
end
end

View file

@ -0,0 +1,54 @@
defmodule Temple.Support.Components do
use Temple.Support.Component
def basic_component(_assigns) do
temple do
div do
"I am a basic component"
end
end
end
def default_slot(assigns) do
temple do
div do
"I am above the slot"
slot @inner_block
end
end
end
def named_slot(assigns) do
temple do
div do
"#{@name} is above the slot"
slot @inner_block
end
footer do
for f <- @footer do
span do: f[:label]
slot f, %{name: @name}
end
end
end
end
def rest_component(assigns) do
temple do
div do
"I am a basic #{@id} with #{@class}"
end
end
end
def rest_slot(assigns) do
temple do
div do
for foo <- @foo do
slot foo, slot_id: foo.id, rest!: [slot_class: foo.class]
end
end
end
end
end

22
test/support/helpers.ex Normal file
View file

@ -0,0 +1,22 @@
defmodule Temple.Support.Helpers do
import ExUnit.Assertions
defmacro assert_html(expected, actual) do
quote location: :keep do
unquote(__MODULE__).__assert_html__(unquote_splicing([expected, actual]))
end
end
def __assert_html__(expected, actual) do
actual = actual |> Phoenix.HTML.Engine.encode_to_iodata!() |> IO.iodata_to_binary()
assert expected == actual,
"""
--- Expected ---
#{expected}----------------
--- Actual ---
#{actual}--------------
"""
end
end

View file

@ -1,7 +1,7 @@
defmodule Temple.Parser.AnonymousFunctionsTest do
defmodule Temple.Ast.AnonymousFunctionsTest do
use ExUnit.Case, async: true
alias Temple.Parser.AnonymousFunctions
alias Temple.Ast.AnonymousFunctions
describe "applicable?/1" do
test "returns true when the node contains an anonymous function as an argument to a function" do
@ -57,7 +57,7 @@ defmodule Temple.Parser.AnonymousFunctionsTest do
assert %AnonymousFunctions{
elixir_ast: _,
children: [
%Temple.Parser.Default{
%Temple.Ast.Default{
elixir_ast: ^expected_child
}
]

View file

@ -1,7 +1,7 @@
defmodule Temple.Parser.ComponentsTest do
use ExUnit.Case, async: false
alias Temple.Parser.Components
alias Temple.Parser.Slottable
defmodule Temple.Ast.ComponentsTest do
use ExUnit.Case, async: true
alias Temple.Ast.Components
alias Temple.Ast.Slottable
describe "applicable?/1" do
test "runs when using the `c` ast with a block" do
@ -57,8 +57,7 @@ defmodule Temple.Parser.ComponentsTest do
assert %Components{
function: ^func,
assigns: [],
children: _
arguments: []
} = ast
end
@ -72,8 +71,7 @@ defmodule Temple.Parser.ComponentsTest do
assert %Components{
function: ^func,
assigns: [foo: :bar],
children: _
arguments: [foo: :bar]
} = ast
end
@ -91,8 +89,7 @@ defmodule Temple.Parser.ComponentsTest do
assert %Components{
function: ^func,
assigns: [foo: :bar],
children: _
arguments: [foo: :bar]
} = ast
end
@ -106,8 +103,7 @@ defmodule Temple.Parser.ComponentsTest do
assert %Components{
function: ^func,
assigns: [foo: :bar],
children: []
arguments: [foo: :bar]
} = ast
end
@ -115,7 +111,7 @@ defmodule Temple.Parser.ComponentsTest do
raw_ast =
quote do
c unquote(func), foo: :bar do
slot :foo, %{form: form} do
slot :foo, let!: %{form: form} do
"in the slot"
end
end
@ -125,15 +121,40 @@ defmodule Temple.Parser.ComponentsTest do
assert %Components{
function: ^func,
assigns: [foo: :bar],
arguments: [foo: :bar],
slots: [
%Slottable{
name: :foo,
content: [%Temple.Parser.Text{}],
assigns: {:%{}, _, [form: _]}
content: [%Temple.Ast.Text{}],
parameter: {:%{}, _, [form: _]}
}
],
children: []
]
} = ast
end
test "slot attributes", %{func: func} do
raw_ast =
quote do
c unquote(func), foo: :bar do
slot :foo, let!: %{form: form}, label: the_label do
"in the slot"
end
end
end
ast = Components.run(raw_ast)
assert %Components{
function: ^func,
arguments: [foo: :bar],
slots: [
%Slottable{
name: :foo,
content: [%Temple.Ast.Text{}],
parameter: {:%{}, _, [form: _]},
attributes: [label: {:the_label, [], Temple.Ast.ComponentsTest}]
}
]
} = ast
end
@ -149,7 +170,7 @@ defmodule Temple.Parser.ComponentsTest do
c unquote(list), socials: @user.socials do
"hello"
slot :default, %{text: text, url: url} do
slot :foo, let!: %{text: text, url: url} do
a class: "text-blue-500 hover:underline", href: url do
text
end
@ -161,16 +182,32 @@ defmodule Temple.Parser.ComponentsTest do
ast = Components.run(raw_ast)
assert Kernel.==(ast.slots, [])
assert [
%Slottable{
name: :inner_block,
parameter: nil
}
] = ast.slots
assert %Components{
children: [
%Components{
children: [
slots: [
%Slottable{
content: [
%Components{
slots: [
%Slottable{
name: :default
content: [
%Components{
slots: [
%Slottable{
name: :inner_block
},
%Slottable{
name: :foo
}
]
}
]
}
]
}

View file

@ -1,7 +1,7 @@
defmodule Temple.Parser.DefaultTest do
defmodule Temple.Ast.DefaultTest do
use ExUnit.Case, async: true
alias Temple.Parser.Default
alias Temple.Ast.Default
describe "applicable?/1" do
test "returns true when the node is an elixir expression" do

View file

@ -1,7 +1,7 @@
defmodule Temple.Parser.DoExpressionsTest do
defmodule Temple.Ast.DoExpressionsTest do
use ExUnit.Case, async: true
alias Temple.Parser.DoExpressions
alias Temple.Ast.DoExpressions
describe "applicable?/1" do
test "returns true when the node contains a do expression" do
@ -30,7 +30,7 @@ defmodule Temple.Parser.DoExpressionsTest do
assert %DoExpressions{
elixir_ast: _,
children: [
[%Temple.Parser.Text{text: "bob"}],
[%Temple.Ast.Text{text: "bob"}],
nil
]
} = ast

View file

@ -1,7 +1,7 @@
defmodule Temple.Parser.EmptyTest do
defmodule Temple.Ast.EmptyTest do
use ExUnit.Case, async: true
alias Temple.Parser.Empty
alias Temple.Ast.Empty
describe "applicable?/1" do
test "returns true when the node is non-content" do

View file

@ -1,7 +1,7 @@
defmodule Temple.Parser.MatchTest do
defmodule Temple.Ast.MatchTest do
use ExUnit.Case, async: true
alias Temple.Parser.Match
alias Temple.Ast.Match
describe "applicable?/1" do
test "returns true when the node is an elixir match expression" do

View file

@ -1,8 +1,9 @@
defmodule Temple.Parser.NonvoidElementsAliasesTest do
defmodule Temple.Ast.NonvoidElementsAliasesTest do
use ExUnit.Case, async: true
alias Temple.Parser.NonvoidElementsAliases
alias Temple.Parser.ElementList
alias Temple.Ast.NonvoidElementsAliases
alias Temple.Ast.ElementList
alias Temple.Ast.Text
describe "applicable?/1" do
test "returns true when the node is a nonvoid element or alias" do
@ -74,7 +75,7 @@ defmodule Temple.Parser.NonvoidElementsAliasesTest do
name: "option",
children: %ElementList{
children: [
%Temple.Parser.Text{text: "foo"}
%Text{text: "foo"}
]
}
}

View file

@ -1,7 +1,7 @@
defmodule Temple.Parser.RightArrowTest do
defmodule Temple.Ast.RightArrowTest do
use ExUnit.Case, async: true
alias Temple.Parser.RightArrow
alias Temple.Ast.RightArrow
describe "applicable?/1" do
test "returns true when the node contains a right arrow" do
@ -52,7 +52,7 @@ defmodule Temple.Parser.RightArrowTest do
assert %RightArrow{
elixir_ast: {:->, [newlines: 1], [[:bing]]},
children: [
%Temple.Parser.Default{
%Temple.Ast.Default{
elixir_ast: ^bong
}
]

View file

@ -1,6 +1,6 @@
defmodule Temple.Parser.SlotTest do
use ExUnit.Case, async: false
alias Temple.Parser.Slot
defmodule Temple.Ast.SlotTest do
use ExUnit.Case, async: true
alias Temple.Ast.Slot
describe "applicable?/1" do
test "runs when using the `c` ast with a block" do

View file

@ -1,8 +1,10 @@
defmodule Temple.Parser.TempleNamespaceNonvoidTest do
defmodule Temple.Ast.TempleNamespaceNonvoidTest do
use ExUnit.Case, async: true
alias Temple.Parser.NonvoidElementsAliases
alias Temple.Parser.TempleNamespaceNonvoid
alias Temple.Ast.ElementList
alias Temple.Ast.NonvoidElementsAliases
alias Temple.Ast.TempleNamespaceNonvoid
alias Temple.Ast.Text
describe "applicable?/1" do
test "returns true when the node is a Temple aliased nonvoid element" do
@ -50,8 +52,8 @@ defmodule Temple.Parser.TempleNamespaceNonvoidTest do
assert %NonvoidElementsAliases{
name: "div",
attrs: [class: "foo", id: {:var, [], _}],
children: %Temple.Parser.ElementList{
children: [%Temple.Parser.Text{text: "foo"}],
children: %ElementList{
children: [%Text{text: "foo"}],
whitespace: :tight
}
} = ast

View file

@ -1,8 +1,8 @@
defmodule Temple.Parser.TempleNamespaceVoidTest do
defmodule Temple.Ast.TempleNamespaceVoidTest do
use ExUnit.Case, async: true
alias Temple.Parser.TempleNamespaceVoid
alias Temple.Parser.VoidElementsAliases
alias Temple.Ast.TempleNamespaceVoid
alias Temple.Ast.VoidElementsAliases
describe "applicable?/1" do
test "returns true when the node is a Temple aliased nonvoid element" do

View file

@ -1,7 +1,7 @@
defmodule Temple.Parser.TextTest do
defmodule Temple.Ast.TextTest do
use ExUnit.Case, async: true
alias Temple.Parser.Text
alias Temple.Ast.Text
describe "applicable?/1" do
test "returns true when the node is a string literal" do

View file

@ -1,7 +1,7 @@
defmodule Temple.Parser.UtilsTest do
defmodule Temple.Ast.UtilsTest do
use ExUnit.Case, async: true
alias Temple.Parser.Utils
alias Temple.Ast.Utils
describe "compile_attrs/1" do
test "returns a list of text nodes for static attributes" do
@ -19,20 +19,15 @@ defmodule Temple.Parser.UtilsTest do
test "returns a list of text and expr nodes for attributes with runtime values" do
class_ast = quote(do: @class)
id_ast = quote(do: @id)
attrs = [class: class_ast, id: id_ast, disabled: false, checked: true]
attrs = [class: class_ast, id: "foo"]
actual = Utils.compile_attrs(attrs)
assert [{:expr, actual}, {:text, ~s' id="foo"'}] = Utils.compile_attrs(attrs)
assert [
{:text, ~s' class="'},
{:expr, class_ast},
{:text, ~s'"'},
{:text, ~s' id="'},
{:expr, id_ast},
{:text, ~s'"'},
{:text, ~s' checked'}
] == actual
assert Macro.to_string(
quote do
Temple.Ast.Utils.__attributes__([{"class", unquote(class_ast)}])
end
) == Macro.to_string(actual)
end
test "returns a list of text and expr nodes for the class object syntax" do
@ -61,5 +56,27 @@ defmodule Temple.Parser.UtilsTest do
# the ast metadata is different, let's just compare stringified versions
assert Macro.to_string(result_expr) == Macro.to_string(expr)
end
test "the rest! attribute will mix in the values at runtime" do
rest_ast =
quote do
rest
end
attrs = [class: "text-red", rest!: rest_ast]
actual = Utils.compile_attrs(attrs)
assert [
{:text, ~s' class="text-red"'},
{:expr, rest_actual}
] = actual
assert Macro.to_string(
quote do
Temple.Ast.Utils.__attributes__(unquote(rest_ast))
end
) == Macro.to_string(rest_actual)
end
end
end

View file

@ -1,7 +1,7 @@
defmodule Temple.Parser.VoidElementsAliasesTest do
defmodule Temple.Ast.VoidElementsAliasesTest do
use ExUnit.Case, async: true
alias Temple.Parser.VoidElementsAliases
alias Temple.Ast.VoidElementsAliases
describe "applicable?/1" do
test "returns true when the node is a nonvoid element or alias" do

View file

@ -0,0 +1,79 @@
defmodule Temple.ConverterTest do
use ExUnit.Case, async: true
alias Temple.Converter
describe "convert/1" do
test "converts basic html" do
# html
html = """
<div class="container" disabled aria-label="alice">
<!-- this is a comment -->
I'm some content!
</div>
"""
assert Converter.convert(html) ===
"""
div class: "container", disabled: true, aria_label: "alice" do
# this is a comment
"I'm some content!"
end
"""
|> String.trim()
end
test "multiline html comments" do
# html
html = """
<div >
<!-- this is a comment
and this is some multi
stuff -->
</div>
"""
assert Converter.convert(html) ===
"""
div do
# this is a comment
# and this is some multi
# stuff
end
"""
|> String.trim()
end
test "script and style tag" do
# html
html = """
<script>
console.log("ayy yoo");
</script>
<style>
.foo {
color: red;
}
</style>
"""
assert Converter.convert(html) ===
"""
script do
"console.log(\\"ayy yoo\\");"
end
style do
".foo {"
"color: red;"
"}"
end
"""
|> String.trim()
end
end
end

View file

@ -1,11 +1,14 @@
defmodule Temple.RendererTest do
use ExUnit.Case, async: true
import Temple
use Temple.Support.Component
import Temple.Support.Components
require Temple.Renderer
alias Temple.Renderer
import Temple.Support.Helpers
describe "compile/1" do
test "produces renders a text node" do
result =
@ -13,7 +16,7 @@ defmodule Temple.RendererTest do
"hello world"
end
assert "hello world" == result
assert_html "hello world", result
end
test "produces renders a div" do
@ -29,7 +32,7 @@ defmodule Temple.RendererTest do
# html
expected = ~S|<div class="hello world">hello world<span id="name">bob</span></div>|
assert expected == result
assert_html expected, result
end
test "produces renders a void elements" do
@ -47,7 +50,7 @@ defmodule Temple.RendererTest do
expected =
~S|<div class="hello world">hello world<input type="button" value="Submit"><input type="button" value="Submit"></div>|
assert expected == result
assert_html expected, result
end
test "a match does not emit" do
@ -63,7 +66,7 @@ defmodule Temple.RendererTest do
# html
expected = ~S|<div class="hello world"><span id="name">bob</span></div>|
assert expected == result
assert_html expected, result
end
test "handles simple expression inside attributes" do
@ -79,29 +82,24 @@ defmodule Temple.RendererTest do
# html
expected = ~S|<div class="green">hello world</div>|
assert expected == result
assert_html expected, result
end
# test "handles simple expression are the entire attributes" do
# assigns = %{statement: "hello world", attributes: [class: "green"]}
test "handles simple expression are the entire attributes" do
assigns = %{statement: "hello world", attributes: [class: "green"]}
# result =
# Renderer.compile do
# div @attributes do
# @statement
# end
# end
result =
Renderer.compile do
div @attributes do
@statement
end
end
# # html
# expected = """
# <div class="green">
# hello world
# </div>
# html
expected = ~S|<div class="green">hello world</div>|
# """
# assert expected == result
# end
assert_html expected, result
end
test "handles simple expression with @ assign" do
assigns = %{statement: "hello world"}
@ -116,7 +114,7 @@ defmodule Temple.RendererTest do
# html
expected = ~S|<div>hello world</div>|
assert expected == result
assert_html expected, result
end
test "handles multi line expression" do
@ -135,7 +133,7 @@ defmodule Temple.RendererTest do
expected =
~S|<div><span class="name">alice</span><span class="name">bob</span><span class="name">carol</span></div>|
assert expected == result
assert_html expected, result
end
test "if expression" do
@ -156,7 +154,7 @@ defmodule Temple.RendererTest do
# html
expected = ~s|<div><span>#{val}</span></div>|
assert expected == result
assert_html expected, result
end
end
@ -183,7 +181,7 @@ defmodule Temple.RendererTest do
# html
expected = ~s|<div><span>#{val}</span></div>|
assert expected == result
assert_html expected, result
end
end
@ -209,7 +207,7 @@ defmodule Temple.RendererTest do
end
end
assert expected == result
assert_html expected, result
end
test "handles anonymous functions" do
@ -228,7 +226,7 @@ defmodule Temple.RendererTest do
expected =
~S|<div><span class="name">alice</span><span class="name">bob</span><span class="name">carol</span></div>|
assert expected == result
assert_html expected, result
end
def super_map(enumerable, func, _extra_args) do
@ -255,15 +253,7 @@ defmodule Temple.RendererTest do
expected =
~S|<div><span class="name">alice</span><span class="name">bob</span><span class="name">carol</span></div>|
assert expected == result
end
def basic_component(_assigns) do
temple do
div do
"I am a basic component"
end
end
assert_html expected, result
end
test "basic component" do
@ -277,19 +267,12 @@ defmodule Temple.RendererTest do
# html
expected = ~S|<div><div>I am a basic component</div></div>|
assert expected == result
end
def default_slot(assigns) do
temple do
div do
"I am above the slot"
slot :default
end
end
assert_html expected, result
end
test "component with default slot" do
assigns = %{}
result =
Renderer.compile do
div do
@ -302,30 +285,19 @@ defmodule Temple.RendererTest do
# html
expected = ~S|<div><div>I am above the slot<span>i'm a slot</span></div></div>|
assert expected == result
end
def named_slot(assigns) do
temple do
div do
"#{@name} is above the slot"
slot :default
end
footer do
slot :footer, %{name: @name}
end
end
assert_html expected, result
end
test "component with a named slot" do
assigns = %{label: "i'm a slot attribute"}
result =
Renderer.compile do
div do
c &named_slot/1, name: "motchy boi" do
span do: "i'm a slot"
slot :footer, %{name: name} do
slot :footer, let!: %{name: name}, label: @label, expr: 1 + 1 do
p do
"#{name}'s in the footer!"
end
@ -336,9 +308,9 @@ defmodule Temple.RendererTest do
# heex
expected =
~S|<div><div>motchy boi is above the slot<span>i'm a slot</span></div><footer><p>motchy boi's in the footer!</p></footer></div>|
~S|<div><div>motchy boi is above the slot<span>i'm a slot</span></div><footer><span>i&#39;m a slot attribute</span><p>motchy boi&#39;s in the footer!</p></footer></div>|
assert expected == result
assert_html expected, result
end
end
@ -354,7 +326,7 @@ defmodule Temple.RendererTest do
# html
expected = ~S|<div class="text-red">hello world</div>|
assert expected == result
assert_html expected, result
end
test "boolean attributes only emit correctly with truthy values" do
@ -366,7 +338,7 @@ defmodule Temple.RendererTest do
# html
expected = ~S|<input type="text" disabled placeholder="Enter some text...">|
assert expected == result
assert_html expected, result
end
test "boolean attributes don't emit with falsy values" do
@ -378,7 +350,117 @@ defmodule Temple.RendererTest do
# html
expected = ~S|<input type="text" placeholder="Enter some text...">|
assert expected == result
assert_html expected, result
end
test "runtime boolean attributes emit the right values" do
truthy = true
falsey = false
result =
Renderer.compile do
input type: "text", disabled: falsey, checked: truthy, placeholder: "Enter some text..."
end
# html
expected = ~S|<input type="text" checked placeholder="Enter some text...">|
assert_html expected, result
end
test "multiple slots" do
assigns = %{}
result =
Renderer.compile do
div do
c &named_slot/1, name: "motchy boi" do
span do: "i'm a slot"
slot :footer, let!: %{name: name} do
p do
"#{name}'s in the footer!"
end
end
slot :footer, let!: %{name: name} do
p do
"#{name} is the second footer!"
end
end
end
end
end
# heex
expected =
~S|<div><div>motchy boi is above the slot<span>i'm a slot</span></div><footer><span></span><p>motchy boi&#39;s in the footer!</p><span></span><p>motchy boi is the second footer!</p></footer></div>|
assert_html expected, result
end
test "rest! attribute can mix in dynamic attrs with the static attrs" do
assigns = %{
rest: [
class: "font-bold",
disabled: true
]
}
result =
Renderer.compile do
div id: "foo", rest!: @rest do
"hi"
end
end
# heex
expected = ~S|<div id="foo" class="font-bold" disabled>hi</div>|
assert_html expected, result
end
test "rest! attribute can mix in dynamic assigns to components" do
assigns = %{
rest: [
class: "font-bold"
]
}
result =
Renderer.compile do
c &rest_component/1, id: "foo", rest!: @rest
end
# heex
expected = ~S|<div>I am a basic foo with font-bold</div>|
assert_html expected, result
end
test "rest! attribute can mix in dynamic attributes to slots" do
assigns = %{
rest: [
class: "font-bold"
]
}
result =
Renderer.compile do
c &rest_slot/1 do
slot :foo,
id: "passed-into-slot",
rest!: @rest,
let!: %{slot_class: class, slot_id: id} do
"id is #{id} and class is #{class}"
end
end
end
# heex
expected = ~S|<div>id is passed-into-slot and class is font-bold</div>|
assert_html expected, result
end
end
end

View file

@ -4,21 +4,29 @@ defmodule TempleTest do
describe "temple/1" do
test "works" do
assigns = %{name: "mitch"}
assigns = %{name: "mitch", extra: [foo: "bar"]}
result =
temple do
div class: "hello" do
div class: "hi" do
div class: "hello", rest!: [id: "hi", name: @name] do
div class: "hi", rest!: @extra do
@name
end
end
end
|> Phoenix.HTML.safe_to_string()
# heex
expected = ~S|<div class="hello"><div class="hi">mitch</div></div>|
expected =
~S|<div class="hello" id="hi" name="mitch"><div class="hi" foo="bar">mitch</div></div>|
assert expected == result
end
end
describe "attributes/1" do
test "compiles runtime attributes" do
assert ~s| disabled class="foo"| == attributes(disabled: true, checked: false, class: "foo")
end
end
end