diff --git a/.formatter.exs b/.formatter.exs index 2d7172e..6d31ace 100644 --- a/.formatter.exs +++ b/.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] ] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dda566a..084c283 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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' diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..736dee1 --- /dev/null +++ b/.github/workflows/release.yaml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index be7342e..a2d9b9e 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/README.md b/README.md index 520f586..51d92bc 100644 --- a/README.md +++ b/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`: + ```elixir def deps do [ - {:temple, "~> 0.9.0-rc.0"} + {:temple, "~> 0.12"} ] 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 diff --git a/guides/components.md b/guides/components.md index 921983f..17f49df 100644 --- a/guides/components.md +++ b/guides/components.md @@ -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 diff --git a/guides/converting-html.md b/guides/converting-html.md new file mode 100644 index 0000000..d6ccebe --- /dev/null +++ b/guides/converting-html.md @@ -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! diff --git a/guides/getting-started.md b/guides/getting-started.md index a5b6022..b35f128 100644 --- a/guides/getting-started.md +++ b/guides/getting-started.md @@ -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 diff --git a/guides/migrating/0.10-to-0.11.md b/guides/migrating/0.10-to-0.11.md new file mode 100644 index 0000000..62b272a --- /dev/null +++ b/guides/migrating/0.10-to-0.11.md @@ -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 +``` diff --git a/guides/migrating/0.8-to-0.9.md b/guides/migrating/0.8-to-0.9.md index a67b126..efa0b2f 100644 --- a/guides/migrating/0.8-to-0.9.md +++ b/guides/migrating/0.8-to-0.9.md @@ -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 + +# +# hello! +# + +# The ! version of the element would render it as "tight" +span! do + "hello!" +end + +# hello! +``` + +In 0.9, you would do: + +```elixir +span do + "hello!" +end + +# +# hello! +# + +span do: "hello!" + +# hello! +``` + +## 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. diff --git a/guides/your-first-template.md b/guides/your-first-template.md index e2bd592..69a4114 100644 --- a/guides/your-first-template.md +++ b/guides/your-first-template.md @@ -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 -
-

Profile

- -
-

- Name: Mitch -

-
- - +
+ Hello, world!
``` ## Elixir Expressions -### They Just Work - Any Elixir expression can be used anywhere inside of a Temple template. Here are a few examples. ```elixir diff --git a/lib/mix/tasks/temple.convert.ex b/lib/mix/tasks/temple.convert.ex new file mode 100644 index 0000000..4a0967a --- /dev/null +++ b/lib/mix/tasks/temple.convert.ex @@ -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 diff --git a/lib/temple.ex b/lib/temple.ex index 0b8cd1f..a796bbf 100644 --- a/lib/temple.ex +++ b/lib/temple.ex @@ -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 ``` """ - 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 diff --git a/lib/temple/ast.ex b/lib/temple/ast.ex index e7d6995..9c4fe29 100644 --- a/lib/temple/ast.ex +++ b/lib/temple/ast.ex @@ -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 diff --git a/lib/temple/parser/anonymous_functions.ex b/lib/temple/ast/anonymous_functions.ex similarity index 55% rename from lib/temple/parser/anonymous_functions.ex rename to lib/temple/ast/anonymous_functions.ex index f0a41db..e973e22 100644 --- a/lib/temple/parser/anonymous_functions.ex +++ b/lib/temple/ast/anonymous_functions.ex @@ -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 diff --git a/lib/temple/parser/components.ex b/lib/temple/ast/components.ex similarity index 50% rename from lib/temple/parser/components.ex rename to lib/temple/ast/components.ex index 81a0fc0..5e5b682 100644 --- a/lib/temple/parser/components.ex +++ b/lib/temple/ast/components.ex @@ -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} diff --git a/lib/temple/parser/default.ex b/lib/temple/ast/default.ex similarity index 54% rename from lib/temple/parser/default.ex rename to lib/temple/ast/default.ex index e48bf38..a605dea 100644 --- a/lib/temple/parser/default.ex +++ b/lib/temple/ast/default.ex @@ -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 diff --git a/lib/temple/parser/do_expressions.ex b/lib/temple/ast/do_expressions.ex similarity index 66% rename from lib/temple/parser/do_expressions.ex rename to lib/temple/ast/do_expressions.ex index a842cb6..ab1af30 100644 --- a/lib/temple/parser/do_expressions.ex +++ b/lib/temple/ast/do_expressions.ex @@ -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]) diff --git a/lib/temple/parser/element_list.ex b/lib/temple/ast/element_list.ex similarity index 57% rename from lib/temple/parser/element_list.ex rename to lib/temple/ast/element_list.ex index 55da997..0c583a2 100644 --- a/lib/temple/parser/element_list.ex +++ b/lib/temple/ast/element_list.ex @@ -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) diff --git a/lib/temple/parser/empty.ex b/lib/temple/ast/empty.ex similarity index 66% rename from lib/temple/parser/empty.ex rename to lib/temple/ast/empty.ex index a8f7bc2..a1ac2af 100644 --- a/lib/temple/parser/empty.ex +++ b/lib/temple/ast/empty.ex @@ -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 diff --git a/lib/temple/parser/match.ex b/lib/temple/ast/match.ex similarity index 62% rename from lib/temple/parser/match.ex rename to lib/temple/ast/match.ex index b1a794e..c180488 100644 --- a/lib/temple/parser/match.ex +++ b/lib/temple/ast/match.ex @@ -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 diff --git a/lib/temple/parser/nonvoid_elements_aliases.ex b/lib/temple/ast/nonvoid_elements_aliases.ex similarity index 61% rename from lib/temple/parser/nonvoid_elements_aliases.ex rename to lib/temple/ast/nonvoid_elements_aliases.ex index 9312ab6..42bdf38 100644 --- a/lib/temple/parser/nonvoid_elements_aliases.ex +++ b/lib/temple/ast/nonvoid_elements_aliases.ex @@ -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) ) diff --git a/lib/temple/parser/right_arrow.ex b/lib/temple/ast/right_arrow.ex similarity index 50% rename from lib/temple/parser/right_arrow.ex rename to lib/temple/ast/right_arrow.ex index 9af20d1..9f6785e 100644 --- a/lib/temple/parser/right_arrow.ex +++ b/lib/temple/ast/right_arrow.ex @@ -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 diff --git a/lib/temple/parser/slot.ex b/lib/temple/ast/slot.ex similarity index 74% rename from lib/temple/parser/slot.ex rename to lib/temple/ast/slot.ex index 426899a..458de65 100644 --- a/lib/temple/parser/slot.ex +++ b/lib/temple/ast/slot.ex @@ -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 diff --git a/lib/temple/ast/slottable.ex b/lib/temple/ast/slottable.ex new file mode 100644 index 0000000..7337096 --- /dev/null +++ b/lib/temple/ast/slottable.ex @@ -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 diff --git a/lib/temple/parser/temple_namespace_nonvoid.ex b/lib/temple/ast/temple_namespace_nonvoid.ex similarity index 70% rename from lib/temple/parser/temple_namespace_nonvoid.ex rename to lib/temple/ast/temple_namespace_nonvoid.ex index 696fab2..4f4e7e8 100644 --- a/lib/temple/parser/temple_namespace_nonvoid.ex +++ b/lib/temple/ast/temple_namespace_nonvoid.ex @@ -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 diff --git a/lib/temple/parser/temple_namespace_void.ex b/lib/temple/ast/temple_namespace_void.ex similarity index 67% rename from lib/temple/parser/temple_namespace_void.ex rename to lib/temple/ast/temple_namespace_void.ex index e86778a..4262c3d 100644 --- a/lib/temple/parser/temple_namespace_void.ex +++ b/lib/temple/ast/temple_namespace_void.ex @@ -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 diff --git a/lib/temple/parser/text.ex b/lib/temple/ast/text.ex similarity index 62% rename from lib/temple/parser/text.ex rename to lib/temple/ast/text.ex index cfc50da..bd2a14d 100644 --- a/lib/temple/parser/text.ex +++ b/lib/temple/ast/text.ex @@ -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 diff --git a/lib/temple/parser/utils.ex b/lib/temple/ast/utils.ex similarity index 77% rename from lib/temple/parser/utils.ex rename to lib/temple/ast/utils.ex index cae7523..dbb7c1b 100644 --- a/lib/temple/parser/utils.ex +++ b/lib/temple/ast/utils.ex @@ -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 diff --git a/lib/temple/parser/void_elements_aliases.ex b/lib/temple/ast/void_elements_aliases.ex similarity index 66% rename from lib/temple/parser/void_elements_aliases.ex rename to lib/temple/ast/void_elements_aliases.ex index 22ccc74..e3f954d 100644 --- a/lib/temple/parser/void_elements_aliases.ex +++ b/lib/temple/ast/void_elements_aliases.ex @@ -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 diff --git a/lib/temple/component.ex b/lib/temple/component.ex new file mode 100644 index 0000000..30410c9 --- /dev/null +++ b/lib/temple/component.ex @@ -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 diff --git a/lib/temple/converter.ex b/lib/temple/converter.ex new file mode 100644 index 0000000..fd99b00 --- /dev/null +++ b/lib/temple/converter.ex @@ -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 diff --git a/lib/temple/parser.ex b/lib/temple/parser.ex index 207d773..f72a246 100644 --- a/lib/temple/parser.ex +++ b/lib/temple/parser.ex @@ -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 [ diff --git a/lib/temple/parser/slottable.ex b/lib/temple/parser/slottable.ex deleted file mode 100644 index c047a1f..0000000 --- a/lib/temple/parser/slottable.ex +++ /dev/null @@ -1,5 +0,0 @@ -defmodule Temple.Parser.Slottable do - @moduledoc false - - defstruct content: nil, assigns: Macro.escape(%{}), name: nil -end diff --git a/lib/temple/renderer.ex b/lib/temple/renderer.ex index 36884ee..814c4fc 100644 --- a/lib/temple/renderer.ex +++ b/lib/temple/renderer.ex @@ -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} diff --git a/mix.exs b/mix.exs index 8160722..1acec04 100644 --- a/mix.exs +++ b/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 diff --git a/mix.lock b/mix.lock index fee66e2..8f5feaf 100644 --- a/mix.lock +++ b/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"}, } diff --git a/test/support/component.ex b/test/support/component.ex new file mode 100644 index 0000000..191affa --- /dev/null +++ b/test/support/component.ex @@ -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 diff --git a/test/support/components.ex b/test/support/components.ex new file mode 100644 index 0000000..adc210f --- /dev/null +++ b/test/support/components.ex @@ -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 diff --git a/test/support/helpers.ex b/test/support/helpers.ex new file mode 100644 index 0000000..420536e --- /dev/null +++ b/test/support/helpers.ex @@ -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 diff --git a/test/parser/anonymous_functions_test.exs b/test/temple/ast/anonymous_functions_test.exs similarity index 91% rename from test/parser/anonymous_functions_test.exs rename to test/temple/ast/anonymous_functions_test.exs index d100711..f335e60 100644 --- a/test/parser/anonymous_functions_test.exs +++ b/test/temple/ast/anonymous_functions_test.exs @@ -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 } ] diff --git a/test/parser/components_test.exs b/test/temple/ast/components_test.exs similarity index 63% rename from test/parser/components_test.exs rename to test/temple/ast/components_test.exs index 88a1754..09e4109 100644 --- a/test/parser/components_test.exs +++ b/test/temple/ast/components_test.exs @@ -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 + } + ] + } + ] } ] } diff --git a/test/parser/default_test.exs b/test/temple/ast/default_test.exs similarity index 88% rename from test/parser/default_test.exs rename to test/temple/ast/default_test.exs index 3f2aa99..3023dc2 100644 --- a/test/parser/default_test.exs +++ b/test/temple/ast/default_test.exs @@ -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 diff --git a/test/parser/do_expressions_test.exs b/test/temple/ast/do_expressions_test.exs similarity index 83% rename from test/parser/do_expressions_test.exs rename to test/temple/ast/do_expressions_test.exs index 7bde960..b4a5b30 100644 --- a/test/parser/do_expressions_test.exs +++ b/test/temple/ast/do_expressions_test.exs @@ -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 diff --git a/test/parser/empty_test.exs b/test/temple/ast/empty_test.exs similarity index 91% rename from test/parser/empty_test.exs rename to test/temple/ast/empty_test.exs index b727786..a9128e1 100644 --- a/test/parser/empty_test.exs +++ b/test/temple/ast/empty_test.exs @@ -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 diff --git a/test/parser/match_test.exs b/test/temple/ast/match_test.exs similarity index 92% rename from test/parser/match_test.exs rename to test/temple/ast/match_test.exs index 10bad4f..41320bf 100644 --- a/test/parser/match_test.exs +++ b/test/temple/ast/match_test.exs @@ -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 diff --git a/test/parser/nonvoid_elements_aliases_test.exs b/test/temple/ast/nonvoid_elements_aliases_test.exs similarity index 90% rename from test/parser/nonvoid_elements_aliases_test.exs rename to test/temple/ast/nonvoid_elements_aliases_test.exs index 7965d03..6105cbf 100644 --- a/test/parser/nonvoid_elements_aliases_test.exs +++ b/test/temple/ast/nonvoid_elements_aliases_test.exs @@ -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"} ] } } diff --git a/test/parser/right_arrow_test.exs b/test/temple/ast/right_arrow_test.exs similarity index 90% rename from test/parser/right_arrow_test.exs rename to test/temple/ast/right_arrow_test.exs index 2c4dc4d..a62427d 100644 --- a/test/parser/right_arrow_test.exs +++ b/test/temple/ast/right_arrow_test.exs @@ -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 } ] diff --git a/test/parser/slot_test.exs b/test/temple/ast/slot_test.exs similarity index 84% rename from test/parser/slot_test.exs rename to test/temple/ast/slot_test.exs index 9a88e4c..881c81d 100644 --- a/test/parser/slot_test.exs +++ b/test/temple/ast/slot_test.exs @@ -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 diff --git a/test/parser/temple_namespace_nonvoid_test.exs b/test/temple/ast/temple_namespace_nonvoid_test.exs similarity index 80% rename from test/parser/temple_namespace_nonvoid_test.exs rename to test/temple/ast/temple_namespace_nonvoid_test.exs index 98e8f5f..478a24a 100644 --- a/test/parser/temple_namespace_nonvoid_test.exs +++ b/test/temple/ast/temple_namespace_nonvoid_test.exs @@ -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 diff --git a/test/parser/temple_namespace_void_test.exs b/test/temple/ast/temple_namespace_void_test.exs similarity index 88% rename from test/parser/temple_namespace_void_test.exs rename to test/temple/ast/temple_namespace_void_test.exs index d848f4b..5824c63 100644 --- a/test/parser/temple_namespace_void_test.exs +++ b/test/temple/ast/temple_namespace_void_test.exs @@ -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 diff --git a/test/parser/text_test.exs b/test/temple/ast/text_test.exs similarity index 90% rename from test/parser/text_test.exs rename to test/temple/ast/text_test.exs index 39f207a..959e025 100644 --- a/test/parser/text_test.exs +++ b/test/temple/ast/text_test.exs @@ -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 diff --git a/test/parser/utils_test.exs b/test/temple/ast/utils_test.exs similarity index 61% rename from test/parser/utils_test.exs rename to test/temple/ast/utils_test.exs index 1d9c93e..4e0370e 100644 --- a/test/parser/utils_test.exs +++ b/test/temple/ast/utils_test.exs @@ -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 diff --git a/test/parser/void_elements_aliases_test.exs b/test/temple/ast/void_elements_aliases_test.exs similarity index 92% rename from test/parser/void_elements_aliases_test.exs rename to test/temple/ast/void_elements_aliases_test.exs index 68c4747..801d030 100644 --- a/test/parser/void_elements_aliases_test.exs +++ b/test/temple/ast/void_elements_aliases_test.exs @@ -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 diff --git a/test/temple/converter_test.exs b/test/temple/converter_test.exs new file mode 100644 index 0000000..7120c29 --- /dev/null +++ b/test/temple/converter_test.exs @@ -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 = """ +
+ + I'm some content! +
+ """ + + 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 = """ +
+ +
+ """ + + 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 = """ + + + + """ + + assert Converter.convert(html) === + """ + script do + "console.log(\\"ayy yoo\\");" + end + + style do + ".foo {" + "color: red;" + "}" + end + """ + |> String.trim() + end + end +end diff --git a/test/temple/renderer_test.exs b/test/temple/renderer_test.exs index 7e241f5..6b2867e 100644 --- a/test/temple/renderer_test.exs +++ b/test/temple/renderer_test.exs @@ -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|
hello worldbob
| - 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|
hello world
| - 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|
bob
| - 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|
hello world
| - 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 = """ - #
- # hello world - #
+ # html + expected = ~S|
hello world
| - # """ - - # 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|
hello world
| - assert expected == result + assert_html expected, result end test "handles multi line expression" do @@ -135,7 +133,7 @@ defmodule Temple.RendererTest do expected = ~S|
alicebobcarol
| - assert expected == result + assert_html expected, result end test "if expression" do @@ -156,7 +154,7 @@ defmodule Temple.RendererTest do # html expected = ~s|
#{val}
| - assert expected == result + assert_html expected, result end end @@ -183,7 +181,7 @@ defmodule Temple.RendererTest do # html expected = ~s|
#{val}
| - 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|
alicebobcarol
| - 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|
alicebobcarol
| - 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|
I am a basic component
| - 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|
I am above the sloti'm a slot
| - 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|
motchy boi is above the sloti'm a slot

motchy boi's in the footer!

| + ~S|
motchy boi is above the sloti'm a slot
i'm a slot attribute

motchy boi's in the footer!

| - assert expected == result + assert_html expected, result end end @@ -354,7 +326,7 @@ defmodule Temple.RendererTest do # html expected = ~S|
hello world
| - 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|| - 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|| - 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|| + + 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|
motchy boi is above the sloti'm a slot

motchy boi's in the footer!

motchy boi is the second footer!

| + + 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|
hi
| + + 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|
I am a basic foo with font-bold
| + + 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|
id is passed-into-slot and class is font-bold
| + + assert_html expected, result end end end diff --git a/test/temple_test.exs b/test/temple_test.exs index a8d0c29..75d9f6a 100644 --- a/test/temple_test.exs +++ b/test/temple_test.exs @@ -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|
mitch
| + expected = + ~S|
mitch
| 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