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 = ~w[temple c slot]a
temple c slot
html = ~w[
html head title style script html head title style script
noscript template noscript template
body section nav article aside h1 h2 h3 h4 h5 h6 body section nav article aside h1 h2 h3 h4 h5 h6
@ -12,23 +13,114 @@ locals_without_parens = ~w[
map svg math map svg math
table caption colgroup tbody thead tfoot tr td th table caption colgroup tbody thead tfoot tr td th
form fieldset legend label button select datalist optgroup form fieldset legend label button select datalist optgroup
option text_area output progress meter option textarea output progress meter
details summary menuitem menu details summary menuitem menu
meta link base meta link base
area br col embed hr img input keygen param source track wbr area br col embed hr img input keygen param source track wbr
]a
animate animateMotion animateTransform circle clipPath svg = ~w[
color-profile defs desc discard ellipse feBlend circle
feColorMatrix feComponentTransfer feComposite feConvolveMatrix feDiffuseLighting feDisplacementMap feDistantLight feDropShadow ellipse
feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur feImage feMerge feMergeNode feMorphology feOffset line
fePointLight feSpecularLighting feSpotLight feTile feTurbulence filter foreignObject g hatch hatchpath image line linearGradient path
marker mask mesh meshgradient meshpatch meshrow metadata mpath path pattern polygon polygon
polyline radialGradient rect set solidcolor stop svg switch symbol text polyline
textPath tspan unknown use view rect
]a |> Enum.map(fn e -> {e, :*} end) 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}"], 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] export: [locals_without_parens: locals_without_parens]
] ]

View File

@ -6,13 +6,13 @@ on:
jobs: jobs:
tests: tests:
runs-on: ubuntu-latest runs-on: ubuntu-20.04
name: Test (${{matrix.elixir}}/${{matrix.otp}}) name: Test (${{matrix.elixir}}/${{matrix.otp}})
strategy: strategy:
matrix: matrix:
otp: [23.x, 24.x] otp: [23.x, 24.x, 25.x]
elixir: [1.13.x] elixir: [1.13.x, 1.14.x]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -21,6 +21,7 @@ jobs:
otp-version: ${{matrix.otp}} otp-version: ${{matrix.otp}}
elixir-version: ${{matrix.elixir}} elixir-version: ${{matrix.elixir}}
- uses: actions/cache@v3 - uses: actions/cache@v3
id: cache
with: with:
path: | path: |
deps deps
@ -86,22 +87,23 @@ jobs:
formatter: formatter:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Formatter (1.13.x.x/24.x) name: Formatter (1.14.x.x/25.x)
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: erlef/setup-beam@v1 - uses: erlef/setup-beam@v1
with: with:
otp-version: 24.x otp-version: 25.x
elixir-version: 1.13.x elixir-version: 1.14.x
- uses: actions/cache@v3 - uses: actions/cache@v3
id: cache
with: with:
path: | path: |
deps deps
_build _build
key: ${{ runner.os }}-mix-24-1.13-${{ hashFiles('**/mix.lock') }} key: ${{ runner.os }}-mix-23-1.14-${{ hashFiles('**/mix.lock') }}
restore-keys: | restore-keys: |
${{ runner.os }}-mix-24-1.13- ${{ runner.os }}-mix-23-1.14-
- name: Install Dependencies - name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true' 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 ## 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 ### 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) [![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) [![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 ## Installation
Add `temple` to your list of dependencies in `mix.exs`: Add `temple` to your list of dependencies in `mix.exs`:
<!-- x-release-please-start-version -->
```elixir ```elixir
def deps do def deps do
[ [
{:temple, "~> 0.9.0-rc.0"} {:temple, "~> 0.12"}
] ]
end end
``` ```
<!-- x-release-please-end -->
## Goals ## Goals
Currently Temple has the following things on which it won't compromise. 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. 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. Please see the [guides](https://hexdocs.pm/temple/components.html) for more details.
```elixir ```elixir
@ -77,15 +85,15 @@ defmodule MyAppWeb.Component do
temple do temple do
section do section do
div do div do
slot :header slot @header
end end
div do div do
slot :default slot @inner_block
end end
div do div do
slot :footer slot @footer
end end
end end
end end
@ -140,6 +148,10 @@ To include Temple's formatter configuration, add `:temple` to your `.formatter.e
## Phoenix ## 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. 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 ## 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. 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 ```elixir
defmodule MyApp.ConfirmDialog do defmodule MyApp.ConfirmDialog do
@ -54,7 +54,7 @@ Slots are defined and rendered using the `slot` keyword. This is similar to the
### Default Slot ### 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 ```elixir
defmodule MyApp.Components do defmodule MyApp.Components do
@ -63,7 +63,7 @@ defmodule MyApp.Components do
def button(assigns) do def button(assigns) do
temple do temple do
button type: "button", class: "bg-blue-800 text-white rounded #{@class}" do button type: "button", class: "bg-blue-800 text-white rounded #{@class}" do
slot :default slot @inner_block
end end
end end
end end
@ -109,18 +109,18 @@ defmodule MyApp.Components do
div class: "card" do div class: "card" do
header class: "card-header", style: "background-color: @f5f5f5" do header class: "card-header", style: "background-color: @f5f5f5" do
p class: "card-header-title" do p class: "card-header-title" do
slot :header slot @header
end end
end end
div class: "card-content" do div class: "card-content" do
div class: "content" do div class: "content" do
slot :default slot @inner_block
end end
end end
footer class: "card-footer", style: "background-color: #f5f5f5" do footer class: "card-footer", style: "background-color: #f5f5f5" do
slot :footer slot @footer
end end
end end
end end
@ -145,8 +145,8 @@ def MyApp.CardExample do
"This example demonstrates how to create components with multiple, named slots" "This example demonstrates how to create components with multiple, named slots"
slot :footer do slot :footer do
a href="#", class: "card-footer-item", do: "Footer Item 1" 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 2"
end end
end end
end end
@ -154,11 +154,15 @@ def MyApp.CardExample do
end 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 #### Definition
@ -166,30 +170,23 @@ Let's look at what a `table` component could look like.
defmodule MyApp.Components do defmodule MyApp.Components do
import Temple import Temple
def cols(items) do
items
|> List.first()
|> Map.keys()
|> Enum.sort()
end
def table(assigns) do def table(assigns) do
temple do temple do
table do table do
thead do thead do
tr do tr do
for col <- cols(@entries) do for col <- @col do
tr do: String.upcase(to_string(col)) th do: col.label # 👈 accessing a slot attribute
end end
end end
end end
tbody do tbody do
for row <- @entries do for row <- @rows do
tr do tr do
for col <- cols(@entries) do for col <- @col do
td do td do
slot :cell, %{value: row[cell]} slot col, row
end end
end end
end end
@ -203,7 +200,7 @@ end
#### Usage #### 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 ```elixir
def MyApp.TableExample do def MyApp.TableExample do
@ -213,24 +210,16 @@ def MyApp.TableExample do
def render(assigns) do def render(assigns) do
temple do temple do
section do section do
h2 do: "Inventory Levels" h2 do: "Users"
c &table/1, entries: @item_inventories do c &table/1, rows: @users do
slot :cell, %{value: value} do # 👇 defining the parameter for the slot argument
case value do slot :col, let!: user, label: "Name" do # 👈 passing a slot attribute
0 -> user.name
span class: "font-bold" do end
"Out of stock!"
end
level when is_number(level) -> slot :col, let!: user, label: "Address" do
span do user.address
"#{level} in stock"
end
_ ->
span do: value
end
end end
end 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 ### 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 ```elixir
# config/config.exs # config/config.exs
config :temple, config :temple,
engine: Phoenix.HTML.Engine engine: EEx.SmartEngine,
attributes: {Temple, :attributes}
``` ```
### Aliases ### 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 # 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
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. ### Dynamic Attributes
- 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.
Let's look at an example. To render dynamic attributes into a tag, you can pass them with the reserved attribute `:rest!`.
```elixir ```elixir
assigns = %{highlight?: false, user_name: "Mitch"} assigns = %{
data: [data_foo: "hi"]
}
temple do temple do
div id: "hero" do div id: "foo", rest!: @data do
h2 class: "font-bold", do: "Profile" "Hello, world!"
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
end end
end end
``` ```
...will emit markup that looks like... will render to
```html ```html
<div id="hero"> <div id="foo" data-foo="hi">
<h2 class="font-bold">Profile</h2> Hello, world!
<section data-controller="hero">
<p class="">
Name: Mitch
</p>
</section>
<video autoplay src="https://example.com/rick-rolled.mp4"></video>
</div> </div>
``` ```
## Elixir Expressions ## Elixir Expressions
### They Just Work
Any Elixir expression can be used anywhere inside of a Temple template. Here are a few examples. Any Elixir expression can be used anywhere inside of a Temple template. Here are a few examples.
```elixir ```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 defmodule Temple do
@engine Application.compile_env(:temple, :engine, EEx.SmartEngine)
@moduledoc """ @moduledoc """
Temple syntax is available inside the `temple`, and is compiled into efficient Elixir code at compile time using the configured `EEx.Engine`. 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"> <link href="/css/site.css">
``` ```
""" """
defmacro temple(block) do defmacro temple(block) do
opts = [engine: engine()]
quote do quote do
require Temple.Renderer require Temple.Renderer
Temple.Renderer.compile(unquote(opts), unquote(block))
Temple.Renderer.compile(unquote(block))
end end
end end
@doc false @doc false
def component(func, assigns) do defdelegate engine, to: Temple.Renderer
apply(func, [assigns])
end
@doc false @doc """
def engine(), do: @engine 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 end

View File

@ -1,6 +1,19 @@
defmodule Temple.Ast do defmodule Temple.Ast do
@moduledoc false @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 def new(module, opts \\ []) do
struct(module, opts) struct(module, opts)
end end

View File

@ -1,14 +1,17 @@
defmodule Temple.Parser.AnonymousFunctions do defmodule Temple.Ast.AnonymousFunctions do
@moduledoc false @moduledoc false
@behaviour Temple.Parser @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 def applicable?({_, _, args}) do
import Temple.Parser.Utils, only: [split_args: 1] import Temple.Ast.Utils, only: [split_args: 1]
args args
|> split_args() |> split_args()
@ -18,11 +21,11 @@ defmodule Temple.Parser.AnonymousFunctions do
def applicable?(_), do: false def applicable?(_), do: false
@impl Parser @impl true
def run({_name, _, args} = expression) do 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 {_func, _, [{_arrow, _, [[{_arg, _, _}], block]}]} = func_arg

View File

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

View File

@ -1,15 +1,17 @@
defmodule Temple.Parser.Default do defmodule Temple.Ast.Default do
@moduledoc false @moduledoc false
@behaviour Temple.Parser @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 def applicable?(_ast), do: true
@impl Parser @impl true
def run(ast) do def run(ast) do
Temple.Ast.new(__MODULE__, elixir_ast: ast) Temple.Ast.new(__MODULE__, elixir_ast: ast)
end end

View File

@ -1,21 +1,24 @@
defmodule Temple.Parser.DoExpressions do defmodule Temple.Ast.DoExpressions do
@moduledoc false @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 def applicable?({_, _, args}) when is_list(args) do
Enum.any?(args, fn arg -> match?([{:do, _} | _], arg) end) Enum.any?(args, fn arg -> match?([{:do, _} | _], arg) end)
end end
def applicable?(_), do: false def applicable?(_), do: false
@impl Parser @impl true
def run({name, meta, args}) do 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]) 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 @moduledoc false
@behaviour Temple.Parser @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) def applicable?(asts), do: is_list(asts)
@impl Temple.Parser @impl true
def run(asts) do def run(asts) do
children = Enum.flat_map(asts, &Temple.Parser.parse/1) 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 @moduledoc false
use TypedStruct
@behaviour Temple.Parser @behaviour Temple.Parser
defstruct [] typedstruct do
end
alias Temple.Parser @impl true
@impl Parser
def applicable?(ast) when ast in [nil, []], do: true def applicable?(ast) when ast in [nil, []], do: true
def applicable?(_), do: false def applicable?(_), do: false
@impl Parser @impl true
def run(_ast) do def run(_ast) do
Temple.Ast.new(__MODULE__) Temple.Ast.new(__MODULE__)
end end

View File

@ -1,19 +1,21 @@
defmodule Temple.Parser.Match do defmodule Temple.Ast.Match do
@moduledoc false @moduledoc false
@behaviour Temple.Parser @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 def applicable?({name, _, _}) do
name in [:=] name in [:=]
end end
def applicable?(_), do: false def applicable?(_), do: false
@impl Parser @impl true
def run(macro) do def run(macro) do
Temple.Ast.new(__MODULE__, elixir_ast: macro) Temple.Ast.new(__MODULE__, elixir_ast: macro)
end end

View File

@ -1,25 +1,32 @@
defmodule Temple.Parser.NonvoidElementsAliases do defmodule Temple.Ast.NonvoidElementsAliases do
@moduledoc false @moduledoc false
@behaviour Temple.Parser @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 alias Temple.Parser
@impl Parser @impl true
def applicable?({name, _, _}) do def applicable?({name, _, _}) do
name in Parser.nonvoid_elements_aliases() name in Parser.nonvoid_elements_aliases()
end end
def applicable?(_), do: false def applicable?(_), do: false
@impl Parser @impl true
def run({name, meta, args}) do def run({name, meta, args}) do
name = Parser.nonvoid_elements_lookup()[name] 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]) children = Temple.Parser.parse(do_and_else[:do])
@ -28,7 +35,7 @@ defmodule Temple.Parser.NonvoidElementsAliases do
attrs: args, attrs: args,
meta: %{whitespace: whitespace(meta)}, meta: %{whitespace: whitespace(meta)},
children: children:
Temple.Ast.new(Temple.Parser.ElementList, Temple.Ast.new(Temple.Ast.ElementList,
children: children, children: children,
whitespace: whitespace(meta) whitespace: whitespace(meta)
) )

View File

@ -1,18 +1,22 @@
defmodule Temple.Parser.RightArrow do defmodule Temple.Ast.RightArrow do
@moduledoc false @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: true
def applicable?(_), do: false def applicable?(_), do: false
@impl Parser @impl true
def run({func, meta, [pattern, args]}) do 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) Temple.Ast.new(__MODULE__, elixir_ast: {func, meta, [pattern]}, children: children)
end end

View File

@ -1,8 +1,12 @@
defmodule Temple.Parser.Slot do defmodule Temple.Ast.Slot do
@moduledoc false @moduledoc false
@behaviour Temple.Parser @behaviour Temple.Parser
use TypedStruct
defstruct name: nil, args: [] typedstruct do
field :name, atom()
field :args, list(), default: []
end
@impl true @impl true
def applicable?({:slot, _, _}) do 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 @moduledoc false
@behaviour Temple.Parser @behaviour Temple.Parser
alias Temple.Parser alias Temple.Parser
@impl Parser @impl true
def applicable?({{:., _, [{:__aliases__, _, [:Temple]}, name]}, _meta, _args}) do def applicable?({{:., _, [{:__aliases__, _, [:Temple]}, name]}, _meta, _args}) do
name in Parser.nonvoid_elements_aliases() name in Parser.nonvoid_elements_aliases()
end end
def applicable?(_), do: false def applicable?(_), do: false
@impl Parser @impl true
def run({name, meta, args}) do def run({name, meta, args}) do
{:., _, [{:__aliases__, _, [:Temple]}, name]} = name {:., _, [{:__aliases__, _, [:Temple]}, name]} = name
Temple.Parser.NonvoidElementsAliases.run({name, meta, args}) Temple.Ast.NonvoidElementsAliases.run({name, meta, args})
end end
end end

View File

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

View File

@ -1,16 +1,18 @@
defmodule Temple.Parser.Text do defmodule Temple.Ast.Text do
@moduledoc false @moduledoc false
@behaviour Temple.Parser @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?(text) when is_binary(text), do: true
def applicable?(_), do: false def applicable?(_), do: false
@impl Parser @impl true
def run(text) do def run(text) do
Temple.Ast.new(__MODULE__, text: text) Temple.Ast.new(__MODULE__, text: text)
end end

View File

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

View File

@ -1,20 +1,25 @@
defmodule Temple.Parser.VoidElementsAliases do defmodule Temple.Ast.VoidElementsAliases do
@moduledoc false @moduledoc false
@behaviour Temple.Parser @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 def applicable?({name, _, _}) do
name in Temple.Parser.void_elements_aliases() name in Temple.Parser.void_elements_aliases()
end end
def applicable?(_), do: false def applicable?(_), do: false
@impl Temple.Parser @impl true
def run({name, _, args}) do def run({name, _, args}) do
args = args =
case Temple.Parser.Utils.split_args(args) do case Temple.Ast.Utils.split_args(args) do
{_, [args]} when is_list(args) -> {_, [args]} when is_list(args) ->
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 defmodule Temple.Parser do
@moduledoc false @moduledoc false
alias Temple.Parser.Empty alias Temple.Ast.AnonymousFunctions
alias Temple.Parser.Text alias Temple.Ast.Components
alias Temple.Parser.TempleNamespaceNonvoid alias Temple.Ast.Default
alias Temple.Parser.TempleNamespaceVoid alias Temple.Ast.DoExpressions
alias Temple.Parser.Components alias Temple.Ast.Empty
alias Temple.Parser.Slot alias Temple.Ast.Match
alias Temple.Parser.NonvoidElementsAliases alias Temple.Ast.NonvoidElementsAliases
alias Temple.Parser.VoidElementsAliases alias Temple.Ast.RightArrow
alias Temple.Parser.AnonymousFunctions alias Temple.Ast.Slot
alias Temple.Parser.RightArrow alias Temple.Ast.TempleNamespaceNonvoid
alias Temple.Parser.DoExpressions alias Temple.Ast.TempleNamespaceVoid
alias Temple.Parser.Match alias Temple.Ast.Text
alias Temple.Parser.Default alias Temple.Ast.VoidElementsAliases
@type ast :: @aliases Application.compile_env(:temple, :aliases, [])
%Empty{}
| %Text{}
| %Components{}
| %Slot{}
| %NonvoidElementsAliases{}
| %VoidElementsAliases{}
| %AnonymousFunctions{}
| %RightArrow{}
| %DoExpressions{}
| %Match{}
| %Default{}
@doc """ @doc """
Should return true if the parser should apply for the given AST. Should return true if the parser should apply for the given AST.
@ -38,9 +27,113 @@ defmodule Temple.Parser do
Should return Temple.AST. 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[ @nonvoid_elements ~w[
head title style script head title style script
@ -67,9 +160,9 @@ defmodule Temple.Parser do
{Keyword.get(@aliases, el, el), el} {Keyword.get(@aliases, el, el), el}
end) end)
def nonvoid_elements, do: @nonvoid_elements def nonvoid_elements, do: @nonvoid_elements ++ Keyword.values(@nonvoid_svg_aliases)
def nonvoid_elements_aliases, do: @nonvoid_elements_aliases def nonvoid_elements_aliases, do: @nonvoid_elements_aliases ++ @nonvoid_svg_aliases
def nonvoid_elements_lookup, do: @nonvoid_elements_lookup def nonvoid_elements_lookup, do: @nonvoid_elements_lookup ++ @nonvoid_svg_lookup
@void_elements ~w[ @void_elements ~w[
meta link base meta link base
@ -81,9 +174,9 @@ defmodule Temple.Parser do
{Keyword.get(@aliases, el, el), el} {Keyword.get(@aliases, el, el), el}
end) end)
def void_elements, do: @void_elements def void_elements, do: @void_elements ++ Keyword.values(@void_svg_aliases)
def void_elements_aliases, do: @void_elements_aliases def void_elements_aliases, do: @void_elements_aliases ++ @void_svg_aliases
def void_elements_lookup, do: @void_elements_lookup def void_elements_lookup, do: @void_elements_lookup ++ @void_svg_lookup
def parsers() do 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 defmodule Temple.Renderer do
@moduledoc false @moduledoc false
alias Temple.Parser.ElementList alias Temple.Ast.ElementList
alias Temple.Parser.Text alias Temple.Ast.Text
alias Temple.Parser.Components alias Temple.Ast.Components
alias Temple.Parser.Slot alias Temple.Ast.Slot
alias Temple.Parser.NonvoidElementsAliases alias Temple.Ast.Slottable
alias Temple.Parser.VoidElementsAliases alias Temple.Ast.NonvoidElementsAliases
alias Temple.Parser.AnonymousFunctions alias Temple.Ast.VoidElementsAliases
alias Temple.Parser.RightArrow alias Temple.Ast.AnonymousFunctions
alias Temple.Parser.DoExpressions alias Temple.Ast.RightArrow
alias Temple.Parser.Match alias Temple.Ast.DoExpressions
alias Temple.Parser.Default alias Temple.Ast.Match
alias Temple.Parser.Empty 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 block
|> Temple.Parser.parse() |> Temple.Parser.parse()
|> Temple.Renderer.render(opts) |> Temple.Renderer.render(engine: @engine)
# |> Temple.Parser.Utils.inspect_ast() # |> Temple.Ast.Utils.inspect_ast()
end end
def render(asts, opts \\ []) def render(asts, opts \\ [])
def render(asts, opts) when is_list(asts) and is_list(opts) do 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 = %{ state = %{
engine: engine, engine: engine,
@ -37,7 +40,7 @@ defmodule Temple.Renderer do
terminal_node: false terminal_node: false
} }
buffer = engine.init(%{}) buffer = engine.init([])
buffer = buffer =
for ast <- asts, reduce: buffer do for ast <- asts, reduce: buffer do
@ -73,25 +76,11 @@ defmodule Temple.Renderer do
def render(buffer, state, %Components{ def render(buffer, state, %Components{
function: function, function: function,
assigns: assigns, arguments: arguments,
children: children,
slots: slots slots: slots
}) do }) 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 = slot_quotes =
for slot <- slots do Enum.group_by(slots, & &1.name, fn %Slottable{} = slot ->
slot_buffer = state.engine.handle_begin(buffer) slot_buffer = state.engine.handle_begin(buffer)
slot_buffer = slot_buffer =
@ -102,29 +91,48 @@ defmodule Temple.Renderer do
ast = state.engine.handle_end(slot_buffer) ast = state.engine.handle_end(slot_buffer)
[quoted] = inner_block =
quote do quote do
{unquote(slot.name), unquote(slot.assigns)} -> inner_block unquote(slot.name) do
unquote(ast) unquote(slot.parameter || quote(do: _)) ->
unquote(ast)
end
end end
quoted {rest, attributes} = Keyword.pop(slot.attributes, :rest!, [])
end
{:fn, meta, clauses} = slot =
quote do {:%{}, [],
fn [
{:default, _} -> unquote(child_quoted) __slot__: slot.name,
inner_block: inner_block
] ++ attributes}
quote do
Map.merge(unquote(slot), Map.new(unquote(rest)))
end 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 = expr =
quote do quote do
component( component(
unquote(function), unquote(function),
Map.put(Map.new(unquote(assigns)), :__slots__, unquote(slot_func)) unquote(component_arguments),
{__MODULE__, __ENV__.function, __ENV__.file, __ENV__.line}
) )
end end
@ -134,7 +142,9 @@ defmodule Temple.Renderer do
def render(buffer, state, %Slot{} = ast) do def render(buffer, state, %Slot{} = ast) do
render_slot_func = render_slot_func =
quote do 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 end
state.engine.handle_expr(buffer, "=", render_slot_func) state.engine.handle_expr(buffer, "=", render_slot_func)
@ -242,7 +252,7 @@ defmodule Temple.Renderer do
{name, meta, args} = ast.elixir_ast {name, meta, args} = ast.elixir_ast
{args, {func, fmeta, [{arrow, arrowmeta, [first, _block]}]}, args2} = {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 = full_ast =
{name, meta, args ++ [{func, fmeta, [{arrow, arrowmeta, [first, inner_quoted]}]}] ++ args2} {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, app: :temple,
name: "Temple", name: "Temple",
description: "An HTML DSL for Elixir", description: "An HTML DSL for Elixir",
version: "0.9.0-rc.0", version: "0.12.0",
package: package(), package: package(),
elixirc_paths: elixirc_paths(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()),
elixir: "~> 1.13", elixir: "~> 1.13",
@ -18,7 +18,17 @@ defmodule Temple.MixProject do
end end
# Specifies which paths to compile per environment. # 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"] defp elixirc_paths(_), do: ["lib"]
# Run "mix help compile.app" to learn about applications. # Run "mix help compile.app" to learn about applications.
@ -36,7 +46,9 @@ defmodule Temple.MixProject do
"guides/getting-started.md", "guides/getting-started.md",
"guides/your-first-template.md", "guides/your-first-template.md",
"guides/components.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() groups_for_extras: groups_for_extras()
] ]
@ -54,13 +66,16 @@ defmodule Temple.MixProject do
maintainers: ["Mitchell Hanberg"], maintainers: ["Mitchell Hanberg"],
licenses: ["MIT"], licenses: ["MIT"],
links: %{github: "https://github.com/mhanberg/temple"}, 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 end
defp deps do 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
end end

View File

@ -1,8 +1,12 @@
%{ %{
"earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"}, "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": {: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_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"}, "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"}, "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 use ExUnit.Case, async: true
alias Temple.Parser.AnonymousFunctions alias Temple.Ast.AnonymousFunctions
describe "applicable?/1" do describe "applicable?/1" do
test "returns true when the node contains an anonymous function as an argument to a function" 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{ assert %AnonymousFunctions{
elixir_ast: _, elixir_ast: _,
children: [ children: [
%Temple.Parser.Default{ %Temple.Ast.Default{
elixir_ast: ^expected_child elixir_ast: ^expected_child
} }
] ]

View File

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

View File

@ -1,7 +1,7 @@
defmodule Temple.Parser.EmptyTest do defmodule Temple.Ast.EmptyTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
alias Temple.Parser.Empty alias Temple.Ast.Empty
describe "applicable?/1" do describe "applicable?/1" do
test "returns true when the node is non-content" 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 use ExUnit.Case, async: true
alias Temple.Parser.Match alias Temple.Ast.Match
describe "applicable?/1" do describe "applicable?/1" do
test "returns true when the node is an elixir match expression" 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 use ExUnit.Case, async: true
alias Temple.Parser.NonvoidElementsAliases alias Temple.Ast.NonvoidElementsAliases
alias Temple.Parser.ElementList alias Temple.Ast.ElementList
alias Temple.Ast.Text
describe "applicable?/1" do describe "applicable?/1" do
test "returns true when the node is a nonvoid element or alias" 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", name: "option",
children: %ElementList{ children: %ElementList{
children: [ 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 use ExUnit.Case, async: true
alias Temple.Parser.RightArrow alias Temple.Ast.RightArrow
describe "applicable?/1" do describe "applicable?/1" do
test "returns true when the node contains a right arrow" do test "returns true when the node contains a right arrow" do
@ -52,7 +52,7 @@ defmodule Temple.Parser.RightArrowTest do
assert %RightArrow{ assert %RightArrow{
elixir_ast: {:->, [newlines: 1], [[:bing]]}, elixir_ast: {:->, [newlines: 1], [[:bing]]},
children: [ children: [
%Temple.Parser.Default{ %Temple.Ast.Default{
elixir_ast: ^bong elixir_ast: ^bong
} }
] ]

View File

@ -1,6 +1,6 @@
defmodule Temple.Parser.SlotTest do defmodule Temple.Ast.SlotTest do
use ExUnit.Case, async: false use ExUnit.Case, async: true
alias Temple.Parser.Slot alias Temple.Ast.Slot
describe "applicable?/1" do describe "applicable?/1" do
test "runs when using the `c` ast with a block" 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 use ExUnit.Case, async: true
alias Temple.Parser.NonvoidElementsAliases alias Temple.Ast.ElementList
alias Temple.Parser.TempleNamespaceNonvoid alias Temple.Ast.NonvoidElementsAliases
alias Temple.Ast.TempleNamespaceNonvoid
alias Temple.Ast.Text
describe "applicable?/1" do describe "applicable?/1" do
test "returns true when the node is a Temple aliased nonvoid element" 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{ assert %NonvoidElementsAliases{
name: "div", name: "div",
attrs: [class: "foo", id: {:var, [], _}], attrs: [class: "foo", id: {:var, [], _}],
children: %Temple.Parser.ElementList{ children: %ElementList{
children: [%Temple.Parser.Text{text: "foo"}], children: [%Text{text: "foo"}],
whitespace: :tight whitespace: :tight
} }
} = ast } = ast

View File

@ -1,8 +1,8 @@
defmodule Temple.Parser.TempleNamespaceVoidTest do defmodule Temple.Ast.TempleNamespaceVoidTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
alias Temple.Parser.TempleNamespaceVoid alias Temple.Ast.TempleNamespaceVoid
alias Temple.Parser.VoidElementsAliases alias Temple.Ast.VoidElementsAliases
describe "applicable?/1" do describe "applicable?/1" do
test "returns true when the node is a Temple aliased nonvoid element" 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 use ExUnit.Case, async: true
alias Temple.Parser.Text alias Temple.Ast.Text
describe "applicable?/1" do describe "applicable?/1" do
test "returns true when the node is a string literal" 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 use ExUnit.Case, async: true
alias Temple.Parser.Utils alias Temple.Ast.Utils
describe "compile_attrs/1" do describe "compile_attrs/1" do
test "returns a list of text nodes for static attributes" 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 test "returns a list of text and expr nodes for attributes with runtime values" do
class_ast = quote(do: @class) class_ast = quote(do: @class)
id_ast = quote(do: @id) attrs = [class: class_ast, id: "foo"]
attrs = [class: class_ast, id: id_ast, disabled: false, checked: true]
actual = Utils.compile_attrs(attrs) assert [{:expr, actual}, {:text, ~s' id="foo"'}] = Utils.compile_attrs(attrs)
assert [ assert Macro.to_string(
{:text, ~s' class="'}, quote do
{:expr, class_ast}, Temple.Ast.Utils.__attributes__([{"class", unquote(class_ast)}])
{:text, ~s'"'}, end
{:text, ~s' id="'}, ) == Macro.to_string(actual)
{:expr, id_ast},
{:text, ~s'"'},
{:text, ~s' checked'}
] == actual
end end
test "returns a list of text and expr nodes for the class object syntax" do 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 # the ast metadata is different, let's just compare stringified versions
assert Macro.to_string(result_expr) == Macro.to_string(expr) assert Macro.to_string(result_expr) == Macro.to_string(expr)
end 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
end end

View File

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

View File

@ -4,21 +4,29 @@ defmodule TempleTest do
describe "temple/1" do describe "temple/1" do
test "works" do test "works" do
assigns = %{name: "mitch"} assigns = %{name: "mitch", extra: [foo: "bar"]}
result = result =
temple do temple do
div class: "hello" do div class: "hello", rest!: [id: "hi", name: @name] do
div class: "hi" do div class: "hi", rest!: @extra do
@name @name
end end
end end
end end
|> Phoenix.HTML.safe_to_string()
# heex # 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 assert expected == result
end end
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 end