Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
d1db62ef58
57 changed files with 1672 additions and 445 deletions
118
.formatter.exs
118
.formatter.exs
|
@ -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]
|
||||
]
|
||||
|
|
18
.github/workflows/ci.yml
vendored
18
.github/workflows/ci.yml
vendored
|
@ -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
58
.github/workflows/release.yaml
vendored
Normal 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
|
45
CHANGELOG.md
45
CHANGELOG.md
|
@ -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
|
||||
|
||||
|
|
26
README.md
26
README.md
|
@ -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
|
||||
|
|
|
@ -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
25
guides/converting-html.md
Normal 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!
|
|
@ -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
|
||||
|
|
95
guides/migrating/0.10-to-0.11.md
Normal file
95
guides/migrating/0.10-to-0.11.md
Normal 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
|
||||
```
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
34
lib/mix/tasks/temple.convert.ex
Normal file
34
lib/mix/tasks/temple.convert.ex
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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}
|
|
@ -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
|
|
@ -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])
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
)
|
|
@ -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
|
|
@ -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
|
12
lib/temple/ast/slottable.ex
Normal file
12
lib/temple/ast/slottable.ex
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
124
lib/temple/component.ex
Normal 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
103
lib/temple/converter.ex
Normal 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
|
|
@ -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
|
||||
[
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
defmodule Temple.Parser.Slottable do
|
||||
@moduledoc false
|
||||
|
||||
defstruct content: nil, assigns: Macro.escape(%{}), name: nil
|
||||
end
|
|
@ -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
25
mix.exs
|
@ -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
|
||||
|
|
6
mix.lock
6
mix.lock
|
@ -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
10
test/support/component.ex
Normal 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
|
54
test/support/components.ex
Normal file
54
test/support/components.ex
Normal 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
22
test/support/helpers.ex
Normal 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
|
|
@ -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
|
||||
}
|
||||
]
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
79
test/temple/converter_test.exs
Normal file
79
test/temple/converter_test.exs
Normal 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
|
|
@ -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'm a slot attribute</span><p>motchy boi'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'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
|
||||
|
|
|
@ -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
|
||||
|
|
Reference in a new issue