diff --git a/.formatter.exs b/.formatter.exs index ac394c7..2d7172e 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -25,7 +25,7 @@ locals_without_parens = ~w[ 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.flat_map(fn e -> [{e, :*}, {:"#{e}!", :*}] end) +]a |> Enum.map(fn e -> {e, :*} end) [ inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"], diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9d758e..dda566a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: otp: [23.x, 24.x] - elixir: [1.10.x, 1.12.x] + elixir: [1.13.x] steps: - uses: actions/checkout@v2 @@ -20,14 +20,14 @@ jobs: with: otp-version: ${{matrix.otp}} elixir-version: ${{matrix.elixir}} - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: | deps _build - key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + key: ${{ runner.os }}-mix-${{matrix.otp}}-${{matrix.elixir}}-${{ hashFiles('**/mix.lock') }} restore-keys: | - ${{ runner.os }}-mix- + ${{ runner.os }}-mix-${{matrix.otp}}-${{matrix.elixir}}- - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' @@ -36,77 +36,72 @@ jobs: - name: Run Tests run: mix test - integration_tests: - runs-on: ubuntu-latest - name: Integration Test (${{matrix.elixir}}/${{matrix.otp}}) - defaults: - run: - working-directory: "./integration_test/temple_demo" + # integration_tests: + # runs-on: ubuntu-latest + # name: Integration Test (${{matrix.elixir}}/${{matrix.otp}}) + # defaults: + # run: + # working-directory: "./integration_test/temple_demo" - strategy: - matrix: - otp: [23.x] - elixir: [1.9.x, 1.11.x] + # services: + # db: + # image: postgres:12 + # env: + # POSTGRES_USER: postgres + # POSTGRES_PASSWORD: postgres + # POSTGRES_DB: temple_demo_test + # ports: ['5432:5432'] + # options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 - services: - db: - image: postgres:12 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: temple_demo_test - ports: ['5432:5432'] - options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + # steps: + # - uses: actions/checkout@v2 + # - uses: erlef/setup-beam@v1 + # with: + # otp-version: 24.x + # elixir-version: 1.13.x - steps: - - uses: actions/checkout@v2 - - uses: erlef/setup-beam@v1 - with: - otp-version: ${{matrix.otp}} - elixir-version: ${{matrix.elixir}} + # - uses: actions/cache@v3 + # with: + # path: | + # deps + # _build + # key: ${{ runner.os }}-mix-24-1.13-${{ hashFiles('**/mix.lock') }} + # restore-keys: | + # ${{ runner.os }}-mix-24-1.13- - - uses: actions/cache@v2 - with: - path: | - deps - _build - key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} - restore-keys: | - ${{ runner.os }}-mix- + # - name: Install Dependencies + # if: steps.cache.outputs.cache-hit != 'true' + # run: mix deps.get - - name: Install Dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: mix deps.get + # - name: Run Tests + # run: mix test || mix test --failed || mix test --failed + # env: + # MIX_ENV: test - - name: Run Tests - run: mix test || mix test --failed || mix test --failed - env: - MIX_ENV: test - - - uses: actions/upload-artifact@v2 - if: failure() - with: - name: screenshots - path: screenshots/ + # - uses: actions/upload-artifact@v2 + # if: failure() + # with: + # name: screenshots + # path: screenshots/ formatter: runs-on: ubuntu-latest - name: Formatter (1.11.x.x/23.x) + name: Formatter (1.13.x.x/24.x) steps: - uses: actions/checkout@v2 - uses: erlef/setup-beam@v1 with: - otp-version: 23.x - elixir-version: 1.11.x - - uses: actions/cache@v2 + otp-version: 24.x + elixir-version: 1.13.x + - uses: actions/cache@v3 with: path: | deps _build - key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + key: ${{ runner.os }}-mix-24-1.13-${{ hashFiles('**/mix.lock') }} restore-keys: | - ${{ runner.os }}-mix- + ${{ runner.os }}-mix-24-1.13- - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' diff --git a/.tool-versions b/.tool-versions index 84bf208..1b09c5a 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -elixir 1.11.3 -erlang 23.2.6 +elixir ref:v1.13.4 +erlang 25.0-rc2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2beda2d..be7342e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ ## Main +### 0.9.0-rc.0 + +### Breaking Changes + +- Requires Elixir 1.13+ +- Whitespace control is now controlled by whether you use `do/end` or `:do` syntax. The `:do` syntax will render "tight" markup. +- Components are no longer module based. Any function can now be a component. Now to render a component, you pass a function reference `c &my_component/1`. + - Temple.Component has been removed, which removes the `render/1` macro for defining a component. Now all you need to do is define a function and have it take an `assigns` parameter and call the `temple/1` macro that is imported from `Temple`. + - The `defcomp` macro has been removed, since now all you need is a function. +- All Phoenix related things and dependencies have been removed. If you are going to use Temple with Phoenix, now use the [temple_phoenix](https://github.com/mhanberg/temple_phoenix) package instead. +- Config options have changed. Now all you can configure are the aliases (unchanged from before) and now you can configure the EEx.Engine to use. By default it uses `EEx.SmartEngine`. + +Please see the guides for more in depth migration information. + ## 0.8.0 ### Enhancements diff --git a/README.md b/README.md index 0b23d73..6431435 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,11 @@ -# ![](temple.png) +# ![](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.8.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.9.0). -Temple is a DSL for writing HTML and EEx using Elixir. - -You're probably here because you want to use Temple to write Phoenix templates, which is why Temple includes a [Phoenix template engine](#phoenix-templates). +Temple is an Elixir DSL for writing HTML. ## Installation @@ -16,8 +14,7 @@ Add `temple` to your list of dependencies in `mix.exs`: ```elixir def deps do [ - {:temple, "~> 0.8.0"}, - {:phoenix_live_view, "~> 0.16"} # if you are using Phoenix LiveView + {:temple, "~> 0.9.0-rc.0"} ] end ``` @@ -26,16 +23,16 @@ end Currently Temple has the following things on which it won't compromise. - Will only work with valid Elixir syntax. -- Should always work with normal EEx, as well as Phoenix and Phoenix LiveView. +- Should work in all web environments such as Plug, Aino, Phoenix, and Phoenix LiveView. ## Usage -Using Temple is as simple as using the DSL inside of an `temple/1` block. This returns an EEx string at compile time. +Using Temple is as simple as using the DSL inside of an `temple/1` block. The runtime result of the macro is your HTML. -See the [documentation](https://hexdocs.pm/temple/Temple.html) for more details. +See the [guides](https://hexdocs.pm/temple/your-first-template.html) for more details. ```elixir -use Temple +import Temple temple do h2 do: "todos" @@ -66,40 +63,43 @@ end ### Components -Temple components provide an ergonomic API for creating flexible and reusable views. Unlike normal partials, Temple components can take slots, which are similar [Vue](https://v3.vuejs.org/guide/component-slots.html#named-slots). +Temple components are simple to write and easy to use. -For example, if I were to define a `Card` component, I would create the following module. +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. + +Please see the [guides](https://hexdocs.pm/temple/components.html) for more details. ```elixir -defmodule MyAppWeb.Components.Card do - import Temple.Component +defmodule MyAppWeb.Component do + import Temple - render do - section do - div do - slot :header - end + def card(assigns) do + temple do + section do + div do + slot :header + end - div do - slot :default - end + div do + slot :default + end - div do - slot :footer + div do + slot :footer + end end end end end ``` -And we could use the component like so +Using components is as simple as passing a reference to your component function to the `c` keyword. ```elixir -# lib/my_app_web/views/page_view.ex -alias MyAppWeb.Components.Card +import MyAppWeb.Component # lib/my_app_web/templates/page/index.html.exs -c Card do +c &card/1 do slot :header do @user.full_name end @@ -117,87 +117,17 @@ c Card do end ``` -### Phoenix templates +### Engine -To use temple as a Phoenix Template engine, you'll need to configure the right file extensions with the right Temple engine. +By default, Temple will use the `EEx.SmartEngine` that is built into the Elixir standard library. If you are a web framework that uses it's own template engine (such as [Aino](https://github.com/oestrich/aino) and Phoenix/LiveView, you can configure Temple to it! ```elixir -# config.exs -config :phoenix, :template_engines, - exs: Temple.Engine - # or for LiveView support - # this will work for files named like `index.html.lexs` - # you can enable Elixir syntax highlighting in your editor - lexs: Temple.LiveViewEngine +# config/config.exs -# If you're going to be using live_view, make sure to set the `:mode` to `:live_view`. -# This is necessary for Temple to emit markup that is compatible. -config :temple, :mode, :live_view # defaults to normal - -# config/dev.exs -config :your_app, YourAppWeb.Endpoint, - live_reload: [ - patterns: [ - ~r"lib/myapp_web/(live|views)/.*(ex|exs|lexs)$", - ~r"lib/myapp_web/templates/.*(eex|exs|lexs)$" - ] - ] +config :temple, + engine: Aino.View.Engine # or Phoenix.HTML.Engine or Phoenix.LiveView.Engine ``` -```elixir -# app.html.exs - -"" -html lang: "en" do - head do - meta charset: "utf-8" - meta http_equiv: "X-UA-Compatible", content: "IE=edge" - meta name: "viewport", content: "width=device-width, initial-scale=1.0" - title do: "YourApp · Phoenix Framework" - - _link rel: "stylesheet", href: Routes.static_path(@conn, "/css/app.css") - end - - body do - header do - section class: "container" do - nav role: "navigation" do - ul do - li do - a href: "https://hexdocs.pm/phoenix/overview.html"), do: "Get Started" - end - end - end - - a href: "http://phoenixframework.org/", class: "phx-logo" do - img src: Routes.static_path(@conn, "/images/phoenix.png"), - alt: "Phoenix Framework Logo" - end - end - end - - main role: "main", class: "container" do - p class: "alert alert-info", role: "alert", do: get_flash(@conn, :info) - p class: "alert alert-danger", role: "alert", do: get_flash(@conn, :error) - - render @view_module, @view_template, assigns - end - - script type: "text/javascript", src: Routes.static_path(@conn, "/js/app.js") - end -end -``` - -### Tasks - -#### temple.gen.layout - -Generates the app layout. - -#### temple.gen.html - -Generates the templates for a resource. - ### Formatter To include Temple's formatter configuration, add `:temple` to your `.formatter.exs`. @@ -209,7 +139,13 @@ To include Temple's formatter configuration, add `:temple` to your `.formatter.e ] ``` +## Phoenix + +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 - [Introducing Temple: An elegant HTML library for Elixir and Phoenix](https://www.mitchellhanberg.com/introducing-temple-an-elegant-html-library-for-elixir-and-phoenix/) - [Temple, AST, and Protocols](https://www.mitchellhanberg.com/temple-ast-and-protocols/) +- [Thinking Elixir Episode 92: Temple with Mitchell Hanberg](https://podcast.thinkingelixir.com/92) +- [How EEx Turns Your Template Into HTML](https://www.mitchellhanberg.com/how-eex-turns-your-template-into-html/) diff --git a/config/config.exs b/config/config.exs index 8233fe9..d1186fe 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,3 +1,3 @@ -use Mix.Config +import Config -import_config "#{Mix.env()}.exs" +import_config "#{config_env()}.exs" diff --git a/config/dev.exs b/config/dev.exs index d2d855e..becde76 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1 +1 @@ -use Mix.Config +import Config diff --git a/config/test.exs b/config/test.exs index 50b5a38..a4b79a0 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,8 +1,4 @@ -use Mix.Config - -# this is to make the warning go away, -# Temple does not use a json_library -config :phoenix, json_library: Temple +import Config config :temple, aliases: [ diff --git a/guides/components.md b/guides/components.md new file mode 100644 index 0000000..921983f --- /dev/null +++ b/guides/components.md @@ -0,0 +1,240 @@ +# Components + +Temple has the concept of components, which allow you an expressive and composable way to break up your templates into re-usable chunks. + +A component is any arity-1 function that take an argument called `assigns` and returns the result of the `Temple.temple/1` macro. + +## Definition + +Here is an example of a simple Temple component. You can observe that it seems very similar to a regular Temple template, and that is because it is a regular template! + +```elixir +defmodule MyApp.Components do + import Temple + + def button(assigns) do + temple do + button type: "button", class: "bg-blue-800 text-white rounded #{@class}" do + @text + end + end + end +end +``` + +## Usage + +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. + +```elixir +defmodule MyApp.ConfirmDialog do + import Temple + import MyApp.Components + + def render(assigns) do + temple do + dialog open: true do + p do: "Are you sure?" + form method: "dialog" do + c &button/1, class: "border border-white", text: "Yes" + end + end + end + end +end +``` + +## Slots + +Temple components can take "slots" as well. This is the method for providing dynamic content from the call site into the component. + +Slots are defined and rendered using the `slot` keyword. This is similar to the `c` keyword, in that it is not defined using a function or macro. + +### 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. + +```elixir +defmodule MyApp.Components do + import Temple + + def button(assigns) do + temple do + button type: "button", class: "bg-blue-800 text-white rounded #{@class}" do + slot :default + end + end + end +end +``` + +You can pass content through the "default" slot of your component simply by passing a `do/end` block to your component at the call site. This is a special case for the default slot. + +```elixir +defmodule MyApp.ConfirmDialog do + import Temple + import MyApp.Components + + def render(assigns) do + temple do + dialog open: true do + p do: "Are you sure?" + form method: "dialog" do + c &button/1, class: "border border-white" do + "Yes" + end + end + end + end + end +end +``` + +### Named Slots + +You can also define a "named" slot, which allows you to pass more than one set of dynamic content to your component. + +We'll use a "card" example to illustrate this. This example is adapted from the [Surface documentation](https://surface-ui.org/slots) on slots. + +#### Definition + +```elixir +defmodule MyApp.Components do + import Temple + + def card(assigns) do + temple do + div class: "card" do + header class: "card-header", style: "background-color: @f5f5f5" do + p class: "card-header-title" do + slot :header + end + end + + div class: "card-content" do + div class: "content" do + slot :default + end + end + + footer class: "card-footer", style: "background-color: #f5f5f5" do + slot :footer + end + end + end + end +end +``` + +#### Usage + +```elixir +def MyApp.CardExample do + import Temple + import MyApp.Components + + def render(assigns) do + temple do + c &card/1 do + slot :header do + "A simple card component" + end + + "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" + end + end + end + end +end +``` + +## Passing Data Through Slots + +Sometimes it is necessary to pass data from a component definition back to the call site. + +Let's look at what a `table` component could look like. + +#### Definition + +```elixir +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)) + end + end + end + + tbody do + for row <- @entries do + tr do + for col <- cols(@entries) do + td do + slot :cell, %{value: row[cell]} + end + end + end + end + end + end + end + end +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`. + +```elixir +def MyApp.TableExample do + import Temple + import MyApp.Componens + + def render(assigns) do + temple do + section do + h2 do: "Inventory Levels" + + 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 + + level when is_number(level) -> + span do + "#{level} in stock" + end + + _ -> + span do: value + end + end + end + end + end + end +end +``` diff --git a/guides/getting-started.md b/guides/getting-started.md new file mode 100644 index 0000000..a5b6022 --- /dev/null +++ b/guides/getting-started.md @@ -0,0 +1,68 @@ +# Getting Started + +## Install + +Welcome! + +Temple is a HTML DSL for Elixir, let's get started! + + +First, make sure you are using Elixir `V1.13` or higher. + +Add `:temple` to your deps and run `mix deps.get` + +```elixir +{:temple, "~> 0.9.0-rc.0"} +``` + +Now you must prepend the Temple compiler to your projects `:compilers` configuration in `mix.exs`. There is a chance that your project doesn't set this option at all, but don't worry, it's really easy to add! + +```elixir +defmodule MyApp.MixProject do + use Mix.Project + + def project do + [ + # ... + compilers: [:temple] ++ Mix.compilers(), + # ... + ] + end + +# ... + +end +``` + +All done, Now let's start building our app! + +## Configuration + +Temple works out of the box without any configuration, but here are a couple of conifg options that you could need to use. + +### 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. + +```elixir +# config/config.exs + +config :temple, + engine: Phoenix.HTML.Engine +``` + +### Aliases + +Temple code will reserve some local function calls for HTML tags. If you have a local function that you would like to use instead, you can create an alias for any tag. + +Common aliases for Phoenix projects look like this: + +```elixir +config :temple, + aliases: [ + label: :label_tag, + link: :link_tag, + select: :select_tag, + textarea: :textarea_tag + ] +``` diff --git a/guides/migrating/0.8-to-0.9.md b/guides/migrating/0.8-to-0.9.md new file mode 100644 index 0000000..a67b126 --- /dev/null +++ b/guides/migrating/0.8-to-0.9.md @@ -0,0 +1,3 @@ +# Migrating from 0.8 to 0.9 + +TODO: explain it diff --git a/guides/your-first-template.md b/guides/your-first-template.md new file mode 100644 index 0000000..e2bd592 --- /dev/null +++ b/guides/your-first-template.md @@ -0,0 +1,236 @@ +# Your First Template + +A Temple template is written inside of the `Temple.temple/1` macro. Code inside there will be compiled into efficient Elixir code by the configured EEx engine. + +Local functions that have a corresponding HTML5 tag are reserved and will be used when generated your markup. Let's take a look at a basic form written with Temple. + +```elixir +defmodule MyApp.FormExample do + import Temple + + def form_page() do + assigns = %{title: "My Site | Sign Up", logged_in: false} + + temple do + "" + + html do + head do + meta charset: "utf-8" + meta http_equiv: "X-UA-Compatible", content: "IE=edge" + meta name: "viewport", content: "width=device-width, initial-scale=1.0" + link rel: "stylesheet", href: "/css/app.css" + + title do: @title + end + + body do + if @logged_in do + header class: "header" do + ul do + li do + a href: "/", do: "Home" + end + li do + a href: "/logout", do: "Logout" + end + end + end + end + + form action: "", method: "get", class: "form-example" do + div class: "form-example" do + label for: "name", do: "Enter your name:" + input type: "text", name: "name", id: "name", required: true + end + div class: "form-example" do + label for: "email", do: "Enter your email:" + input type: "email", name: "email", id: "email", required: true + end + div class: "form-example" do + input type: "submit", value: "Subscribe!" + end + end + end + end + end + end +end +``` + +This example showcases an entire HTML page made with Temple! Let's dive a little deeper everything we're seeing here. + +Through out this guide, you will see code that includes features that are explained later on. Feel free to skip ahead to read on, or just keep reading. It will all make sense eventually! + +## Text Nodes + +The text node is a basic building block of any HTML document. In Temple, text nodes are represented by Elixir string literals. + +The very first line of the previous example is our doc type, emitted into the final document with `""`. This is a text node and will be emitted into the document as-is. + +Note: String _literals_ are emitted into text nodes. If you are using string interpolation with the `#{some_expression}` syntax, that is treated as an expression and will be evaluated in whichever way the configured engine evaluates expression. By default, the `EEx.SmartEngine` doesn't do any escaping of expressions, so that could still be emitted as-is, or even as HTML to be interpreted by your web browser. + +## Void Tags + +Void tags are HTML5 tags that do not have children, meaning they are "self closing". + +We can observe these in the previous example as the `` tag. You'll note that the tag does not have a `:do` key or a `do` block. + +## Non-void Tags + +Non-void tags are HTML5 tags that _do_ have children. You are probably most familiar with these type of tags, as they include the famous `
` and ``. + +These tags can enclose their children nodes with either a `do/end` block or the inline `:do` keyword. + +### Whitespace + +Nonvoid tags that use the `do/end` syntax will be emitted _with_ internal whitespace. + +```elixir +temple do + div class: "foo" do + # children + end +end +``` + +...will emit markup that looks like... + +```html +
+ +
+``` + +Note: The Elixir comment _will not_ be rendered into an HTML comment. This is just used in the example. (This does sound like a good feature though...) + +Nonvoid tags that use the `:do` keyword syntax will be emitted _without_ internal whitespace. This allows you to correctly use the `:empty` CSS psuedo-selector in your stylesheet. + + +```elixir +temple do + p class: "alert alert-info", do: "Your account was recently updated!" +end +``` + +...will emit markup that looks like... + +```html +

Your account was recently updated!

+``` + +## Attributes + +Attributes are declared as a keyword list. + +- 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. + +Let's look at an example. + +```elixir +assigns = %{highlight?: false, user_name: "Mitch"} + +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 + end +end +``` + +...will emit markup that looks like... + +```html +
+

Profile

+ +
+

+ Name: Mitch +

+
+ + +
+``` + +## Elixir Expressions + +### They Just Work + +Any Elixir expression can be used anywhere inside of a Temple template. Here are a few examples. + +```elixir +temple do + h2 do: "Members" + + ul do + for member <- @members do + li do: member + end + end +end +``` + +### Match Expressions + +Match expression are handled slightly differently. Generally if you are assigning an expression to a variable (a match), you are going to use that binding later and do _not_ want to emit it into the document. + +So, match expressions are _not_ emitted into the document. They are functionally equivalent to the `<% .. %.` syntax of `EEx`. The expression is evaluated, but not included in the rendered document. + +Typically you should not be writing this type of expression inside of your template, but if you wanted to declare an alias, you would need to write the following to not emit the alias into the document. + +```elixir +temple do + _ = alias My.Deep.Module + + div do + Module.func() + end +end +``` + +## Assigns + +Since Temple uses the `EEx.SmartEngine` by default, you are able to use the assigns feature. + +The assigns feature allows you to ergonomically access the members of a `assigns` variable by the `@` macro. + +The assign variable just needs to exist within the scope of the template (the same as a normal `EEx` template that uses `EEx.SmartEngine`), it can be a function parameter or created inside the function. + +```elixir +def card(assigns) do + temple do + div class: "card" do + section class: "card-header" do + @name + end + + section class: "card-body" do + @bio + end + + if Enum.any?(@socials) do + section class: "card-footer" do + for social <- @socials do + a href: social.link do + social.name + end + end + end + end + end + end +end +``` diff --git a/integration_test/temple_demo/.formatter.exs b/integration_test/temple_demo/.formatter.exs deleted file mode 100644 index 09a57c5..0000000 --- a/integration_test/temple_demo/.formatter.exs +++ /dev/null @@ -1,5 +0,0 @@ -[ - import_deps: [:ecto, :phoenix, :temple], - inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"], - subdirectories: ["priv/*/migrations"] -] diff --git a/integration_test/temple_demo/.tool-versions b/integration_test/temple_demo/.tool-versions deleted file mode 100644 index 8d7a08d..0000000 --- a/integration_test/temple_demo/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -elixir 1.9.4 diff --git a/integration_test/temple_demo/README.md b/integration_test/temple_demo/README.md deleted file mode 100644 index 0f76960..0000000 --- a/integration_test/temple_demo/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# TempleDemo - -To start your Phoenix server: - - * Setup the project with `mix setup` - * Start Phoenix endpoint with `mix phx.server` - -Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. - -Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). - -## Learn more - - * Official website: https://www.phoenixframework.org/ - * Guides: https://hexdocs.pm/phoenix/overview.html - * Docs: https://hexdocs.pm/phoenix - * Forum: https://elixirforum.com/c/phoenix-forum - * Source: https://github.com/phoenixframework/phoenix diff --git a/integration_test/temple_demo/config/config.exs b/integration_test/temple_demo/config/config.exs deleted file mode 100644 index 9dc97c5..0000000 --- a/integration_test/temple_demo/config/config.exs +++ /dev/null @@ -1,43 +0,0 @@ -# This file is responsible for configuring your application -# and its dependencies with the aid of the Mix.Config module. -# -# This configuration file is loaded before any dependency and -# is restricted to this project. - -# General application configuration -use Mix.Config - -config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase - -config :temple_demo, - ecto_repos: [TempleDemo.Repo] - -config :phoenix, :template_engines, exs: Temple.Engine - -# Configures the endpoint -config :temple_demo, TempleDemoWeb.Endpoint, - url: [host: "localhost"], - secret_key_base: "ww1nKdikInNFHHUfSdCE1wiTcOmQq/KLvOxG7CY1TlKLDTmLW5yheCCYpfoxmZAW", - render_errors: [view: TempleDemoWeb.ErrorView, accepts: ~w(html json), layout: false], - pubsub_server: TempleDemo.PubSub, - live_view: [signing_salt: "KCU/YIG0"] - -# Configures Elixir's Logger -config :logger, :console, - format: "$time $metadata[$level] $message\n", - metadata: [:request_id] - -# Use Jason for JSON parsing in Phoenix -config :phoenix, :json_library, Jason - -config :temple, - mode: :normal, - aliases: [ - label: :_label, - link: :_link, - textarea: :_textarea - ] - -# Import environment specific config. This must remain at the bottom -# of this file so it overrides the configuration defined above. -import_config "#{Mix.env()}.exs" diff --git a/integration_test/temple_demo/config/dev.exs b/integration_test/temple_demo/config/dev.exs deleted file mode 100644 index abbf5d3..0000000 --- a/integration_test/temple_demo/config/dev.exs +++ /dev/null @@ -1,68 +0,0 @@ -use Mix.Config - -# Configure your database -config :temple_demo, TempleDemo.Repo, - username: "postgres", - password: "postgres", - database: "temple_demo_dev", - hostname: "localhost", - show_sensitive_data_on_connection_error: true, - pool_size: 10 - -# For development, we disable any cache and enable -# debugging and code reloading. -# -# The watchers configuration can be used to run external -# watchers to your application. For example, we use it -# with webpack to recompile .js and .css sources. -config :temple_demo, TempleDemoWeb.Endpoint, - http: [port: 4000], - debug_errors: true, - code_reloader: true, - check_origin: false, - watchers: [] - -# ## SSL Support -# -# In order to use HTTPS in development, a self-signed -# certificate can be generated by running the following -# Mix task: -# -# mix phx.gen.cert -# -# Note that this task requires Erlang/OTP 20 or later. -# Run `mix help phx.gen.cert` for more information. -# -# The `http:` config above can be replaced with: -# -# https: [ -# port: 4001, -# cipher_suite: :strong, -# keyfile: "priv/cert/selfsigned_key.pem", -# certfile: "priv/cert/selfsigned.pem" -# ], -# -# If desired, both `http:` and `https:` keys can be -# configured to run both http and https servers on -# different ports. - -# Watch static and templates for browser reloading. -config :temple_demo, TempleDemoWeb.Endpoint, - live_reload: [ - patterns: [ - ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", - ~r"priv/gettext/.*(po)$", - ~r"lib/temple_demo_web/(live|views|components)/.*(ex)$", - ~r"lib/temple_demo_web/templates/.*(eex|exs)$" - ] - ] - -# Do not include metadata nor timestamps in development logs -config :logger, :console, format: "[$level] $message\n" - -# Set a higher stacktrace during development. Avoid configuring such -# in production as building large stacktraces may be expensive. -config :phoenix, :stacktrace_depth, 20 - -# Initialize plugs at runtime for faster development compilation -config :phoenix, :plug_init_mode, :runtime diff --git a/integration_test/temple_demo/config/prod.exs b/integration_test/temple_demo/config/prod.exs deleted file mode 100644 index de2626c..0000000 --- a/integration_test/temple_demo/config/prod.exs +++ /dev/null @@ -1,55 +0,0 @@ -use Mix.Config - -# For production, don't forget to configure the url host -# to something meaningful, Phoenix uses this information -# when generating URLs. -# -# Note we also include the path to a cache manifest -# containing the digested version of static files. This -# manifest is generated by the `mix phx.digest` task, -# which you should run after static files are built and -# before starting your production server. -config :temple_demo, TempleDemoWeb.Endpoint, - url: [host: "example.com", port: 80], - cache_static_manifest: "priv/static/cache_manifest.json" - -# Do not print debug messages in production -config :logger, level: :info - -# ## SSL Support -# -# To get SSL working, you will need to add the `https` key -# to the previous section and set your `:url` port to 443: -# -# config :temple_demo, TempleDemoWeb.Endpoint, -# ... -# url: [host: "example.com", port: 443], -# https: [ -# port: 443, -# cipher_suite: :strong, -# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), -# certfile: System.get_env("SOME_APP_SSL_CERT_PATH"), -# transport_options: [socket_opts: [:inet6]] -# ] -# -# The `cipher_suite` is set to `:strong` to support only the -# latest and more secure SSL ciphers. This means old browsers -# and clients may not be supported. You can set it to -# `:compatible` for wider support. -# -# `:keyfile` and `:certfile` expect an absolute path to the key -# and cert in disk or a relative path inside priv, for example -# "priv/ssl/server.key". For all supported SSL configuration -# options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 -# -# We also recommend setting `force_ssl` in your endpoint, ensuring -# no data is ever sent via http, always redirecting to https: -# -# config :temple_demo, TempleDemoWeb.Endpoint, -# force_ssl: [hsts: true] -# -# Check `Plug.SSL` for all available options in `force_ssl`. - -# Finally import the config/prod.secret.exs which loads secrets -# and configuration from environment variables. -import_config "prod.secret.exs" diff --git a/integration_test/temple_demo/config/prod.secret.exs b/integration_test/temple_demo/config/prod.secret.exs deleted file mode 100644 index 06dc174..0000000 --- a/integration_test/temple_demo/config/prod.secret.exs +++ /dev/null @@ -1,41 +0,0 @@ -# In this file, we load production configuration and secrets -# from environment variables. You can also hardcode secrets, -# although such is generally not recommended and you have to -# remember to add this file to your .gitignore. -use Mix.Config - -database_url = - System.get_env("DATABASE_URL") || - raise """ - environment variable DATABASE_URL is missing. - For example: ecto://USER:PASS@HOST/DATABASE - """ - -config :temple_demo, TempleDemo.Repo, - # ssl: true, - url: database_url, - pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10") - -secret_key_base = - System.get_env("SECRET_KEY_BASE") || - raise """ - environment variable SECRET_KEY_BASE is missing. - You can generate one by calling: mix phx.gen.secret - """ - -config :temple_demo, TempleDemoWeb.Endpoint, - http: [ - port: String.to_integer(System.get_env("PORT") || "4000"), - transport_options: [socket_opts: [:inet6]] - ], - secret_key_base: secret_key_base - -# ## Using releases (Elixir v1.9+) -# -# If you are doing OTP releases, you need to instruct Phoenix -# to start each relevant endpoint: -# -# config :temple_demo, TempleDemoWeb.Endpoint, server: true -# -# Then you can assemble a release by calling `mix release`. -# See `mix help release` for more information. diff --git a/integration_test/temple_demo/config/test.exs b/integration_test/temple_demo/config/test.exs deleted file mode 100644 index e07ba5e..0000000 --- a/integration_test/temple_demo/config/test.exs +++ /dev/null @@ -1,33 +0,0 @@ -use Mix.Config - -# Configure your database -# -# The MIX_TEST_PARTITION environment variable can be used -# to provide built-in test partitioning in CI environment. -# Run `mix help test` for more information. -config :temple_demo, TempleDemo.Repo, - username: "postgres", - password: "postgres", - database: "temple_demo_test#{System.get_env("MIX_TEST_PARTITION")}", - hostname: "localhost", - pool: Ecto.Adapters.SQL.Sandbox - -# We don't run a server during test. If one is required, -# you can enable the server option below. -config :temple_demo, TempleDemoWeb.Endpoint, - http: [port: 4002], - server: true - -config :temple_demo, :sql_sandbox, true - -config :wallaby, - chromedriver: [ - # headless: false, - binary: System.get_env("CHROME_BROWSER") - ], - base_url: "http://localhost:4002", - otp_app: :temple_demo, - screenshot_on_failure: true - -# Print only warnings and errors during test -config :logger, level: :warn diff --git a/integration_test/temple_demo/lib/temple_demo.ex b/integration_test/temple_demo/lib/temple_demo.ex deleted file mode 100644 index 6552333..0000000 --- a/integration_test/temple_demo/lib/temple_demo.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule TempleDemo do - @moduledoc """ - TempleDemo keeps the contexts that define your domain - and business logic. - - Contexts are also responsible for managing your data, regardless - if it comes from the database, an external API or others. - """ -end diff --git a/integration_test/temple_demo/lib/temple_demo/application.ex b/integration_test/temple_demo/lib/temple_demo/application.ex deleted file mode 100644 index 75194f7..0000000 --- a/integration_test/temple_demo/lib/temple_demo/application.ex +++ /dev/null @@ -1,34 +0,0 @@ -defmodule TempleDemo.Application do - # See https://hexdocs.pm/elixir/Application.html - # for more information on OTP Applications - @moduledoc false - - use Application - - def start(_type, _args) do - children = [ - # Start the Ecto repository - TempleDemo.Repo, - # Start the Telemetry supervisor - TempleDemoWeb.Telemetry, - # Start the PubSub system - {Phoenix.PubSub, name: TempleDemo.PubSub}, - # Start the Endpoint (http/https) - TempleDemoWeb.Endpoint - # Start a worker by calling: TempleDemo.Worker.start_link(arg) - # {TempleDemo.Worker, arg} - ] - - # See https://hexdocs.pm/elixir/Supervisor.html - # for other strategies and supported options - opts = [strategy: :one_for_one, name: TempleDemo.Supervisor] - Supervisor.start_link(children, opts) - end - - # Tell Phoenix to update the endpoint configuration - # whenever the application is updated. - def config_change(changed, _new, removed) do - TempleDemoWeb.Endpoint.config_change(changed, removed) - :ok - end -end diff --git a/integration_test/temple_demo/lib/temple_demo/blog.ex b/integration_test/temple_demo/lib/temple_demo/blog.ex deleted file mode 100644 index dfc94b6..0000000 --- a/integration_test/temple_demo/lib/temple_demo/blog.ex +++ /dev/null @@ -1,104 +0,0 @@ -defmodule TempleDemo.Blog do - @moduledoc """ - The Blog context. - """ - - import Ecto.Query, warn: false - alias TempleDemo.Repo - - alias TempleDemo.Blog.Post - - @doc """ - Returns the list of posts. - - ## Examples - - iex> list_posts() - [%Post{}, ...] - - """ - def list_posts do - Repo.all(Post) - end - - @doc """ - Gets a single post. - - Raises `Ecto.NoResultsError` if the Post does not exist. - - ## Examples - - iex> get_post!(123) - %Post{} - - iex> get_post!(456) - ** (Ecto.NoResultsError) - - """ - def get_post!(id), do: Repo.get!(Post, id) - - @doc """ - Creates a post. - - ## Examples - - iex> create_post(%{field: value}) - {:ok, %Post{}} - - iex> create_post(%{field: bad_value}) - {:error, %Ecto.Changeset{}} - - """ - def create_post(attrs \\ %{}) do - %Post{} - |> Post.changeset(attrs) - |> Repo.insert() - end - - @doc """ - Updates a post. - - ## Examples - - iex> update_post(post, %{field: new_value}) - {:ok, %Post{}} - - iex> update_post(post, %{field: bad_value}) - {:error, %Ecto.Changeset{}} - - """ - def update_post(%Post{} = post, attrs) do - post - |> Post.changeset(attrs) - |> Repo.update() - end - - @doc """ - Deletes a post. - - ## Examples - - iex> delete_post(post) - {:ok, %Post{}} - - iex> delete_post(post) - {:error, %Ecto.Changeset{}} - - """ - def delete_post(%Post{} = post) do - Repo.delete(post) - end - - @doc """ - Returns an `%Ecto.Changeset{}` for tracking post changes. - - ## Examples - - iex> change_post(post) - %Ecto.Changeset{data: %Post{}} - - """ - def change_post(%Post{} = post, attrs \\ %{}) do - Post.changeset(post, attrs) - end -end diff --git a/integration_test/temple_demo/lib/temple_demo/blog/post.ex b/integration_test/temple_demo/lib/temple_demo/blog/post.ex deleted file mode 100644 index 6e85e82..0000000 --- a/integration_test/temple_demo/lib/temple_demo/blog/post.ex +++ /dev/null @@ -1,20 +0,0 @@ -defmodule TempleDemo.Blog.Post do - use Ecto.Schema - import Ecto.Changeset - - schema "posts" do - field :author, :string - field :body, :string - field :published_at, :naive_datetime - field :title, :string - - timestamps() - end - - @doc false - def changeset(post, attrs) do - post - |> cast(attrs, [:title, :body, :published_at, :author]) - |> validate_required([:title, :body, :published_at, :author]) - end -end diff --git a/integration_test/temple_demo/lib/temple_demo/repo.ex b/integration_test/temple_demo/lib/temple_demo/repo.ex deleted file mode 100644 index fe372f1..0000000 --- a/integration_test/temple_demo/lib/temple_demo/repo.ex +++ /dev/null @@ -1,5 +0,0 @@ -defmodule TempleDemo.Repo do - use Ecto.Repo, - otp_app: :temple_demo, - adapter: Ecto.Adapters.Postgres -end diff --git a/integration_test/temple_demo/lib/temple_demo_web.ex b/integration_test/temple_demo/lib/temple_demo_web.ex deleted file mode 100644 index 0a66735..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web.ex +++ /dev/null @@ -1,85 +0,0 @@ -defmodule TempleDemoWeb do - @moduledoc """ - The entrypoint for defining your web interface, such - as controllers, views, channels and so on. - - This can be used in your application as: - - use TempleDemoWeb, :controller - use TempleDemoWeb, :view - - The definitions below will be executed for every view, - controller, etc, so keep them short and clean, focused - on imports, uses and aliases. - - Do NOT define functions inside the quoted expressions - below. Instead, define any helper function in modules - and import those modules here. - """ - - def controller do - quote do - use Phoenix.Controller, namespace: TempleDemoWeb - - import Plug.Conn - import TempleDemoWeb.Gettext - alias TempleDemoWeb.Router.Helpers, as: Routes - end - end - - def view do - quote do - use Phoenix.View, - root: "lib/temple_demo_web/templates", - namespace: TempleDemoWeb - - # Import convenience functions from controllers - import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1] - - import Temple.Component - alias TempleDemoWeb.Component.Outer - alias TempleDemoWeb.Component.Flash - alias TempleDemoWeb.Component.Form - - # Include shared imports and aliases for views - unquote(view_helpers()) - end - end - - def router do - quote do - use Phoenix.Router - - import Plug.Conn - import Phoenix.Controller - end - end - - def channel do - quote do - use Phoenix.Channel - import TempleDemoWeb.Gettext - end - end - - defp view_helpers do - quote do - # Use all HTML functionality (forms, tags, etc) - use Phoenix.HTML - - # Import basic rendering functionality (render, render_layout, etc) - import Phoenix.View - - import TempleDemoWeb.ErrorHelpers - import TempleDemoWeb.Gettext - alias TempleDemoWeb.Router.Helpers, as: Routes - end - end - - @doc """ - When used, dispatch to the appropriate controller/view/etc. - """ - defmacro __using__(which) when is_atom(which) do - apply(__MODULE__, which, []) - end -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/channels/user_socket.ex b/integration_test/temple_demo/lib/temple_demo_web/channels/user_socket.ex deleted file mode 100644 index 8e9d8ff..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/channels/user_socket.ex +++ /dev/null @@ -1,35 +0,0 @@ -defmodule TempleDemoWeb.UserSocket do - use Phoenix.Socket - - ## Channels - # channel "room:*", TempleDemoWeb.RoomChannel - - # Socket params are passed from the client and can - # be used to verify and authenticate a user. After - # verification, you can put default assigns into - # the socket that will be set for all channels, ie - # - # {:ok, assign(socket, :user_id, verified_user_id)} - # - # To deny connection, return `:error`. - # - # See `Phoenix.Token` documentation for examples in - # performing token verification on connect. - @impl true - def connect(_params, socket, _connect_info) do - {:ok, socket} - end - - # Socket id's are topics that allow you to identify all sockets for a given user: - # - # def id(socket), do: "user_socket:#{socket.assigns.user_id}" - # - # Would allow you to broadcast a "disconnect" event and terminate - # all active sockets and channels for a given user: - # - # TempleDemoWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) - # - # Returning `nil` makes this socket anonymous. - @impl true - def id(_socket), do: nil -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/components/flash.ex b/integration_test/temple_demo/lib/temple_demo_web/components/flash.ex deleted file mode 100644 index 2603ec1..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/components/flash.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule TempleDemoWeb.Component.Flash do - import Temple.Component - - render do - div class: "alert alert-#{@type}", style: "border: solid 5px pink" do - slot :default - end - end -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/components/form.ex b/integration_test/temple_demo/lib/temple_demo_web/components/form.ex deleted file mode 100644 index 0532241..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/components/form.ex +++ /dev/null @@ -1,13 +0,0 @@ -defmodule TempleDemoWeb.Component.Form do - import Temple.Component - - render do - f = Phoenix.HTML.Form.form_for(@changeset, @action) - - f - - slot :f, f: f - - "" - end -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/components/inner.ex b/integration_test/temple_demo/lib/temple_demo_web/components/inner.ex deleted file mode 100644 index c6ea49a..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/components/inner.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule TempleDemoWeb.Component.Inner do - import Temple.Component - - render do - div id: "inner", outer_id: @outer_id do - slot :default - end - end -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/components/outer.ex b/integration_test/temple_demo/lib/temple_demo_web/components/outer.ex deleted file mode 100644 index fb28c22..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/components/outer.ex +++ /dev/null @@ -1,10 +0,0 @@ -defmodule TempleDemoWeb.Component.Outer do - import Temple.Component - alias TempleDemoWeb.Component.Inner - - render do - c Inner, outer_id: "from-outer" do - slot :default - end - end -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/controllers/page_controller.ex b/integration_test/temple_demo/lib/temple_demo_web/controllers/page_controller.ex deleted file mode 100644 index fae9dbd..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/controllers/page_controller.ex +++ /dev/null @@ -1,7 +0,0 @@ -defmodule TempleDemoWeb.PageController do - use TempleDemoWeb, :controller - - def index(conn, params) do - render(conn, "index.html", text: params["text"]) - end -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/controllers/post_controller.ex b/integration_test/temple_demo/lib/temple_demo_web/controllers/post_controller.ex deleted file mode 100644 index c93a80d..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/controllers/post_controller.ex +++ /dev/null @@ -1,63 +0,0 @@ -defmodule TempleDemoWeb.PostController do - use TempleDemoWeb, :controller - - alias TempleDemo.Blog - alias TempleDemo.Blog.Post - - def index(conn, _params) do - posts = Blog.list_posts() - render(conn, "index.html", posts: posts) - end - - def new(conn, _params) do - changeset = Blog.change_post(%Post{}) - - render(conn, "new.html", changeset: changeset) - end - - def create(conn, %{"post" => post_params}) do - case Blog.create_post(post_params) do - {:ok, post} -> - conn - |> put_flash(:info, "Post created successfully.") - |> redirect(to: Routes.post_path(conn, :show, post)) - - {:error, %Ecto.Changeset{} = changeset} -> - render(conn, "new.html", changeset: changeset) - end - end - - def show(conn, %{"id" => id}) do - post = Blog.get_post!(id) - render(conn, "show.html", post: post) - end - - def edit(conn, %{"id" => id}) do - post = Blog.get_post!(id) - changeset = Blog.change_post(post) - render(conn, "edit.html", post: post, changeset: changeset) - end - - def update(conn, %{"id" => id, "post" => post_params}) do - post = Blog.get_post!(id) - - case Blog.update_post(post, post_params) do - {:ok, post} -> - conn - |> put_flash(:info, "Post updated successfully.") - |> redirect(to: Routes.post_path(conn, :show, post)) - - {:error, %Ecto.Changeset{} = changeset} -> - render(conn, "edit.html", post: post, changeset: changeset) - end - end - - def delete(conn, %{"id" => id}) do - post = Blog.get_post!(id) - {:ok, _post} = Blog.delete_post(post) - - conn - |> put_flash(:info, "Post deleted successfully.") - |> redirect(to: Routes.post_path(conn, :index)) - end -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/endpoint.ex b/integration_test/temple_demo/lib/temple_demo_web/endpoint.ex deleted file mode 100644 index a888b97..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/endpoint.ex +++ /dev/null @@ -1,58 +0,0 @@ -defmodule TempleDemoWeb.Endpoint do - use Phoenix.Endpoint, otp_app: :temple_demo - - if Application.get_env(:temple_demo, :sql_sandbox) do - plug Phoenix.Ecto.SQL.Sandbox - end - - # The session will be stored in the cookie and signed, - # this means its contents can be read but not tampered with. - # Set :encryption_salt if you would also like to encrypt it. - @session_options [ - store: :cookie, - key: "_temple_demo_key", - signing_salt: "p72rbvlQ" - ] - - socket "/socket", TempleDemoWeb.UserSocket, - websocket: true, - longpoll: false - - socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] - - # Serve at "/" the static files from "priv/static" directory. - # - # You should set gzip to true if you are running phx.digest - # when deploying your static files in production. - plug Plug.Static, - at: "/", - from: :temple_demo, - gzip: false, - only: ~w(css fonts images js favicon.ico robots.txt) - - # Code reloading can be explicitly enabled under the - # :code_reloader configuration of your endpoint. - if code_reloading? do - socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket - plug Phoenix.LiveReloader - plug Phoenix.CodeReloader - plug Phoenix.Ecto.CheckRepoStatus, otp_app: :temple_demo - end - - plug Phoenix.LiveDashboard.RequestLogger, - param_key: "request_logger", - cookie_key: "request_logger" - - plug Plug.RequestId - plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] - - plug Plug.Parsers, - parsers: [:urlencoded, :multipart, :json], - pass: ["*/*"], - json_decoder: Phoenix.json_library() - - plug Plug.MethodOverride - plug Plug.Head - plug Plug.Session, @session_options - plug TempleDemoWeb.Router -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/gettext.ex b/integration_test/temple_demo/lib/temple_demo_web/gettext.ex deleted file mode 100644 index a0aca6f..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/gettext.ex +++ /dev/null @@ -1,24 +0,0 @@ -defmodule TempleDemoWeb.Gettext do - @moduledoc """ - A module providing Internationalization with a gettext-based API. - - By using [Gettext](https://hexdocs.pm/gettext), - your module gains a set of macros for translations, for example: - - import TempleDemoWeb.Gettext - - # Simple translation - gettext("Here is the string to translate") - - # Plural translation - ngettext("Here is the string to translate", - "Here are the strings to translate", - 3) - - # Domain-based translation - dgettext("errors", "Here is the error message to translate") - - See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. - """ - use Gettext, otp_app: :temple_demo -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/router.ex b/integration_test/temple_demo/lib/temple_demo_web/router.ex deleted file mode 100644 index 3f3027b..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/router.ex +++ /dev/null @@ -1,43 +0,0 @@ -defmodule TempleDemoWeb.Router do - use TempleDemoWeb, :router - - pipeline :browser do - plug :accepts, ["html"] - plug :fetch_session - plug :fetch_flash - plug :protect_from_forgery - plug :put_secure_browser_headers - end - - pipeline :api do - plug :accepts, ["json"] - end - - scope "/", TempleDemoWeb do - pipe_through :browser - - get "/", PageController, :index - resources "/posts", PostController - end - - # Other scopes may use custom stacks. - # scope "/api", TempleDemoWeb do - # pipe_through :api - # end - - # Enables LiveDashboard only for development - # - # If you want to use the LiveDashboard in production, you should put - # it behind authentication and allow only admins to access it. - # If your application does not have an admins-only section yet, - # you can use Plug.BasicAuth to set up some basic authentication - # as long as you are also using SSL (which you should anyway). - if Mix.env() in [:dev, :test] do - import Phoenix.LiveDashboard.Router - - scope "/" do - pipe_through :browser - live_dashboard "/dashboard", metrics: TempleDemoWeb.Telemetry - end - end -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/telemetry.ex b/integration_test/temple_demo/lib/temple_demo_web/telemetry.ex deleted file mode 100644 index ed592c4..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/telemetry.ex +++ /dev/null @@ -1,53 +0,0 @@ -defmodule TempleDemoWeb.Telemetry do - use Supervisor - import Telemetry.Metrics - - def start_link(arg) do - Supervisor.start_link(__MODULE__, arg, name: __MODULE__) - end - - @impl true - def init(_arg) do - children = [ - {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} - # Add reporters as children of your supervision tree. - # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} - ] - - Supervisor.init(children, strategy: :one_for_one) - end - - def metrics do - [ - # Phoenix Metrics - summary("phoenix.endpoint.stop.duration", - unit: {:native, :millisecond} - ), - summary("phoenix.router_dispatch.stop.duration", - tags: [:route], - unit: {:native, :millisecond} - ), - - # Database Metrics - summary("temple_demo.repo.query.total_time", unit: {:native, :millisecond}), - summary("temple_demo.repo.query.decode_time", unit: {:native, :millisecond}), - summary("temple_demo.repo.query.query_time", unit: {:native, :millisecond}), - summary("temple_demo.repo.query.queue_time", unit: {:native, :millisecond}), - summary("temple_demo.repo.query.idle_time", unit: {:native, :millisecond}), - - # VM Metrics - summary("vm.memory.total", unit: {:byte, :kilobyte}), - summary("vm.total_run_queue_lengths.total"), - summary("vm.total_run_queue_lengths.cpu"), - summary("vm.total_run_queue_lengths.io") - ] - end - - defp periodic_measurements do - [ - # A module, function and arguments to be invoked periodically. - # This function must call :telemetry.execute/3 and a metric must be added above. - # {TempleDemoWeb, :count_users, []} - ] - end -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/templates/layout/app.html.exs b/integration_test/temple_demo/lib/temple_demo_web/templates/layout/app.html.exs deleted file mode 100644 index 36ac5ac..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/templates/layout/app.html.exs +++ /dev/null @@ -1,44 +0,0 @@ -html lang: "en" do - head do - meta charset: "utf-8" - meta http_equiv: "X-UA-Compatible", content: "IE=edge" - meta name: "viewport", content: "width=device-width, initial-scale=1.0" - title do: "TempleDemo · Phoenix Framework" - - _link(rel: "stylesheet", href: Routes.static_path(@conn, "/css/app.css")) - end - - body do - header do - section class: "container" do - nav role: "navigation" do - ul do - li do - a href: "https://hexdocs.pm/phoenix/overview.html" do - "Get Started" - end - end - end - end - - a href: "http://phoenixframework.org/", class: "phx-logo" do - img src: Routes.static_path(@conn, "/images/phoenix.png"), - alt: "Phoenix Framework Logo" - end - end - end - - main role: "main", class: "container" do - for {type, message} <- get_flash(@conn) do - p class: "alert alert-#{type}", role: "alert" do - message - end - end - - @inner_content - end - - script type: "text/javascript", src: Routes.static_path(@conn, "/js/phoenix_html.js") - script type: "text/javascript", src: Routes.static_path(@conn, "/js/app.js") - end -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/templates/page/index.html.exs b/integration_test/temple_demo/lib/temple_demo_web/templates/page/index.html.exs deleted file mode 100644 index 0657fb4..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/templates/page/index.html.exs +++ /dev/null @@ -1,77 +0,0 @@ -section class: "phx-hero" do - h1 do - gettext("Welcome to %{name}!", name: "Phoenix") - end - - c Outer, outer_id: "hello" do - "inner content of outer" - end - - case @text do - "staging" -> - p do - "Peace-of-mind from prototype to staging" - end - - _ -> - p do - "Peace-of-mind from prototype to production" - end - end -end - -section class: "row" do - article class: "column" do - h2 do: "Resources" - - ul do - li do - a href: "https://hexdocs.pm/phoenix/overview.html" do - "Guides & Docs" - end - end - - li do - a href: "https://github.com/phoenixframework/phoenix" do - "Source" - end - end - - li do - a href: "https://github.com/phoenixframework/phoenix/blob/v1.5/CHANGELOG.md" do - "v1.5 Changelog" - end - end - end - end - - article class: "column" do - h2 do: "Help" - - ul do - li do - a href: "https://elixirforum.com/c/phoenix-forum" do - "Forum" - end - end - - li do - a href: "https://webchat.freenode.net/?channels=elixir-lang" do - "#elixir-lang on Freenode IRC" - end - end - - li do - a href: "https://twitter.com/elixirphoenix" do - "Twitter @elixirphoenix" - end - end - - li do - a href: "https://elixir-slackin.herokuapp.com/" do - "Elixir on Slack" - end - end - end - end -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/templates/post/edit.html.exs b/integration_test/temple_demo/lib/temple_demo_web/templates/post/edit.html.exs deleted file mode 100644 index 16e842f..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/templates/post/edit.html.exs +++ /dev/null @@ -1,7 +0,0 @@ -h1 do: "Edit Post" - -render("form.html", Map.put(assigns, :action, Routes.post_path(@conn, :update, @post))) - -span do - link "Back", to: Routes.post_path(@conn, :index) -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/templates/post/form.html.exs b/integration_test/temple_demo/lib/temple_demo_web/templates/post/form.html.exs deleted file mode 100644 index aaad6e6..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/templates/post/form.html.exs +++ /dev/null @@ -1,31 +0,0 @@ -c Form, changeset: @changeset, action: @action do - slot :f, %{f: f} do - if @changeset.action do - c Flash, type: :info do - p do: "Oops, something went wrong! Please check the errors below." - end - end - - label f, :title - text_input f, :title - error_tag(f, :title) - - label f, :body - textarea f, :body - error_tag(f, :body) - - label f, :published_at - datetime_select f, :published_at - error_tag(f, :published_at) - - label f, :author - text_input f, :author - error_tag(f, :author) - - input type: "text", disabled: true, id: "disabled-input" - - div do - submit "Save" - end - end -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/templates/post/index.html.exs b/integration_test/temple_demo/lib/temple_demo_web/templates/post/index.html.exs deleted file mode 100644 index 40d5281..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/templates/post/index.html.exs +++ /dev/null @@ -1,36 +0,0 @@ -h1 do: "Listing Posts" - -table do - c Headers do - th do: "Title" - th do: "Body" - th do: "Published at" - th do: "Author" - th do: "BOB" - end - - tbody do - for post <- @posts do - tr do - td do: post.title - td do: post.body - td do: post.published_at - td do: post.author - - td do - link "Show", to: Routes.post_path(@conn, :show, post) - link "Edit", to: Routes.post_path(@conn, :edit, post) - - link "Delete", - to: Routes.post_path(@conn, :delete, post), - method: :delete, - data: [confirm: "Are you sure?"] - end - end - end - end -end - -span do - link "New Post", to: Routes.post_path(@conn, :new) -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/templates/post/new.html.exs b/integration_test/temple_demo/lib/temple_demo_web/templates/post/new.html.exs deleted file mode 100644 index ce06483..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/templates/post/new.html.exs +++ /dev/null @@ -1,7 +0,0 @@ -h1 do: "New Post" - -render("form.html", Map.put(assigns, :action, Routes.post_path(@conn, :create))) - -span do - link "Back", to: Routes.post_path(@conn, :index) -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/templates/post/show.html.exs b/integration_test/temple_demo/lib/temple_demo_web/templates/post/show.html.exs deleted file mode 100644 index 200882f..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/templates/post/show.html.exs +++ /dev/null @@ -1,28 +0,0 @@ -h1 do: "Show Post" - -ul do - li do: [strong(do: "Title"), @post.title] - - li do - strong do: "Body" - Phoenix.HTML.Format.text_to_html(@post.body, attributes: [class: "whitespace-pre"]) - end - - li do - strong do: "Published at" - @post.published_at - end - - li do - strong do: "Author" - @post.author - end - - span do - link "Edit", to: Routes.post_path(@conn, :edit, @post) - end - - span do - link "Back", to: Routes.post_path(@conn, :index) - end -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/views/error_helpers.ex b/integration_test/temple_demo/lib/temple_demo_web/views/error_helpers.ex deleted file mode 100644 index dc48246..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/views/error_helpers.ex +++ /dev/null @@ -1,47 +0,0 @@ -defmodule TempleDemoWeb.ErrorHelpers do - @moduledoc """ - Conveniences for translating and building error messages. - """ - - use Phoenix.HTML - - @doc """ - Generates tag for inlined form input errors. - """ - def error_tag(form, field) do - Enum.map(Keyword.get_values(form.errors, field), fn error -> - content_tag(:span, translate_error(error), - class: "invalid-feedback", - phx_feedback_for: input_id(form, field) - ) - end) - end - - @doc """ - Translates an error message using gettext. - """ - def translate_error({msg, opts}) do - # When using gettext, we typically pass the strings we want - # to translate as a static argument: - # - # # Translate "is invalid" in the "errors" domain - # dgettext("errors", "is invalid") - # - # # Translate the number of files with plural rules - # dngettext("errors", "1 file", "%{count} files", count) - # - # Because the error messages we show in our forms and APIs - # are defined inside Ecto, we need to translate them dynamically. - # This requires us to call the Gettext module passing our gettext - # backend as first argument. - # - # Note we use the "errors" domain, which means translations - # should be written to the errors.po file. The :count option is - # set by Ecto and indicates we should also apply plural rules. - if count = opts[:count] do - Gettext.dngettext(TempleDemoWeb.Gettext, "errors", msg, msg, count, opts) - else - Gettext.dgettext(TempleDemoWeb.Gettext, "errors", msg, opts) - end - end -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/views/error_view.ex b/integration_test/temple_demo/lib/temple_demo_web/views/error_view.ex deleted file mode 100644 index 31bd467..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/views/error_view.ex +++ /dev/null @@ -1,16 +0,0 @@ -defmodule TempleDemoWeb.ErrorView do - use TempleDemoWeb, :view - - # If you want to customize a particular status code - # for a certain format, you may uncomment below. - # def render("500.html", _assigns) do - # "Internal Server Error" - # end - - # By default, Phoenix returns the status message from - # the template name. For example, "404.html" becomes - # "Not Found". - def template_not_found(template, _assigns) do - Phoenix.Controller.status_message_from_template(template) - end -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/views/layout_view.ex b/integration_test/temple_demo/lib/temple_demo_web/views/layout_view.ex deleted file mode 100644 index 63b7f02..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/views/layout_view.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule TempleDemoWeb.LayoutView do - use TempleDemoWeb, :view -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/views/page_view.ex b/integration_test/temple_demo/lib/temple_demo_web/views/page_view.ex deleted file mode 100644 index 4194599..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/views/page_view.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule TempleDemoWeb.PageView do - use TempleDemoWeb, :view -end diff --git a/integration_test/temple_demo/lib/temple_demo_web/views/post_view.ex b/integration_test/temple_demo/lib/temple_demo_web/views/post_view.ex deleted file mode 100644 index 3696e25..0000000 --- a/integration_test/temple_demo/lib/temple_demo_web/views/post_view.ex +++ /dev/null @@ -1,13 +0,0 @@ -defmodule TempleDemoWeb.PostView do - use TempleDemoWeb, :view - - def thing(), do: "foobar" - - defcomp Headers do - thead id: PostView.thing() do - tr do - slot :default - end - end - end -end diff --git a/integration_test/temple_demo/mix.exs b/integration_test/temple_demo/mix.exs deleted file mode 100644 index 751176d..0000000 --- a/integration_test/temple_demo/mix.exs +++ /dev/null @@ -1,68 +0,0 @@ -defmodule TempleDemo.MixProject do - use Mix.Project - - def project do - [ - app: :temple_demo, - version: "0.1.0", - elixir: "~> 1.7", - elixirc_paths: elixirc_paths(Mix.env()), - compilers: [:phoenix, :gettext] ++ Mix.compilers(), - start_permanent: Mix.env() == :prod, - aliases: aliases(), - deps: deps() - ] - end - - # Configuration for the OTP application. - # - # Type `mix help compile.app` for more information. - def application do - [ - mod: {TempleDemo.Application, []}, - extra_applications: [:logger, :runtime_tools] - ] - end - - # Specifies which paths to compile per environment. - defp elixirc_paths(:test), do: ["lib", "test/support"] - defp elixirc_paths(_), do: ["lib"] - - # Specifies your project dependencies. - # - # Type `mix help deps` for examples and options. - defp deps do - [ - {:phoenix, "~> 1.5.1"}, - {:phoenix_ecto, "~> 4.1"}, - {:ecto_sql, "~> 3.4"}, - {:postgrex, ">= 0.0.0"}, - {:phoenix_html, "~> 2.11"}, - {:phoenix_live_reload, "~> 1.2", only: :dev}, - {:phoenix_live_dashboard, "~> 0.2.0"}, - {:telemetry_metrics, "~> 0.4"}, - {:telemetry_poller, "~> 0.4"}, - {:gettext, "~> 0.11"}, - {:jason, "~> 1.0"}, - {:plug_cowboy, "~> 2.0"}, - {:wallaby, "~> 0.28.0", only: :test}, - {:tzdata, "~> 1.0.3"}, - {:temple, path: "../../"} - ] - end - - # Aliases are shortcuts or tasks specific to the current project. - # For example, to install project dependencies and perform other setup tasks, run: - # - # $ mix setup - # - # See the documentation for `Mix` for more info on aliases. - defp aliases do - [ - setup: ["deps.get", "ecto.setup"], - "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], - "ecto.reset": ["ecto.drop", "ecto.setup"], - test: ["ecto.create --quiet", "ecto.migrate", "test"] - ] - end -end diff --git a/integration_test/temple_demo/mix.lock b/integration_test/temple_demo/mix.lock deleted file mode 100644 index 16d581d..0000000 --- a/integration_test/temple_demo/mix.lock +++ /dev/null @@ -1,44 +0,0 @@ -%{ - "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"}, - "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, - "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"}, - "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"}, - "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"}, - "db_connection": {:hex, :db_connection, "2.3.1", "4c9f3ed1ef37471cbdd2762d6655be11e38193904d9c5c1c9389f1b891a3088e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "abaab61780dde30301d840417890bd9f74131041afd02174cf4e10635b3a63f5"}, - "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, - "ecto": {:hex, :ecto, "3.5.5", "48219a991bb86daba6e38a1e64f8cea540cded58950ff38fbc8163e062281a07", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "98dd0e5e1de7f45beca6130d13116eae675db59adfa055fb79612406acf6f6f1"}, - "ecto_sql": {:hex, :ecto_sql, "3.5.3", "1964df0305538364b97cc4661a2bd2b6c89d803e66e5655e4e55ff1571943efd", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d2f53592432ce17d3978feb8f43e8dc0705e288b0890caf06d449785f018061c"}, - "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "gettext": {:hex, :gettext, "0.18.0", "406d6b9e0e3278162c2ae1de0a60270452c553536772167e2d701f028116f870", [:mix], [], "hexpm", "c3f850be6367ebe1a08616c2158affe4a23231c70391050bf359d5f92f66a571"}, - "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, - "httpoison": {:hex, :httpoison, "1.7.0", "abba7d086233c2d8574726227b6c2c4f6e53c4deae7fe5f6de531162ce9929a0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "975cc87c845a103d3d1ea1ccfd68a2700c211a434d8428b10c323dc95dc5b980"}, - "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"}, - "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, - "phoenix": {:hex, :phoenix, "1.5.8", "71cfa7a9bb9a37af4df98939790642f210e35f696b935ca6d9d9c55a884621a4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "35ded0a32f4836168c7ab6c33b88822eccd201bcd9492125a9bea4c54332d955"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.2.1", "13f124cf0a3ce0f1948cf24654c7b9f2347169ff75c1123f44674afee6af3b03", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "478a1bae899cac0a6e02be1deec7e2944b7754c04e7d4107fc5a517f877743c0"}, - "phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"}, - "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.2.4", "3080e8a89bab3ec08d4dd9a6858dfa24af9334464aae78c83e58a2db37c6f983", [:mix], [{:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.12.0 or ~> 0.13.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1c89595ef60f1b76ac07705e73f001823af451491792a4b0d5b2b2a3789b0a00"}, - "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.2", "38d94c30df5e2ef11000697a4fbe2b38d0fbf79239d492ff1be87bbc33bc3a84", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "a3dec3d28ddb5476c96a7c8a38ea8437923408bc88da43e5c45d97037b396280"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "0.13.0", "dec006b3da4ab164283d5bebe960724eb4d19cd0ed553e05fb99b260233e200f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.4.17 or ~> 1.5.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm", "bd6f13b666fa9bfeca88b013db20414c693d5a5e6d19b1fc2602c282d626ed8e"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, - "phoenix_view": {:hex, :phoenix_view, "1.0.0", "fea71ecaaed71178b26dd65c401607de5ec22e2e9ef141389c721b3f3d4d8011", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "82be3e2516f5633220246e2e58181282c71640dab7afc04f70ad94253025db0c"}, - "plug": {:hex, :plug, "1.11.1", "f2992bac66fdae679453c9e86134a4201f6f43a687d8ff1cd1b2862d53c80259", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "23524e4fefbb587c11f0833b3910bfb414bf2e2534d61928e920f54e3a1b881f"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.5.0", "51c998f788c4e68fc9f947a5eba8c215fbb1d63a520f7604134cab0270ea6513", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5b2c8925a5e2587446f33810a58c01e66b3c345652eeec809b76ba007acde71a"}, - "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, - "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, - "postgrex": {:hex, :postgrex, "0.15.7", "724410acd48abac529d0faa6c2a379fb8ae2088e31247687b16cacc0e0883372", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "88310c010ff047cecd73d5ceca1d99205e4b1ab1b9abfdab7e00f5c9d20ef8f9"}, - "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, - "telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"}, - "telemetry_metrics": {:hex, :telemetry_metrics, "0.5.0", "1b796e74add83abf844e808564275dfb342bcc930b04c7577ab780e262b0d998", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31225e6ce7a37a421a0a96ec55244386aec1c190b22578bd245188a4a33298fd"}, - "telemetry_poller": {:hex, :telemetry_poller, "0.5.0", "4770888ef85599ead39c7f51d6b4b62306e602d96c69b2625d54dea3d9a5204b", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69e4e8e65b0ae077c9e14cd5f42c7cc486de0e07ac6e3409e6f0e52699a7872c"}, - "tesla": {:hex, :tesla, "1.3.3", "26ae98627af5c406584aa6755ab5fc96315d70d69a24dd7f8369cfcb75094a45", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2648f1c276102f9250299e0b7b57f3071c67827349d9173f34c281756a1b124c"}, - "tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"}, - "wallaby": {:hex, :wallaby, "0.28.0", "2ff217c0f245cadb3e5d91748ebcf0102873ceb9ef8a3507717c8bdd73915668", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.1.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "e58112650d0b51e81714a626eab7d486d7a77342c9bbc2ba262b6653f9b22558"}, - "web_driver_client": {:hex, :web_driver_client, "0.1.0", "19466a989c76b7ec803c796cec0fec4611a64f445fd5120ce50c9e3817e09c2c", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "c9c031ca915e8fc75b5e24ac93503244f3cc406dd7f53047087a45aa62d60e9e"}, -} diff --git a/integration_test/temple_demo/priv/gettext/en/LC_MESSAGES/errors.po b/integration_test/temple_demo/priv/gettext/en/LC_MESSAGES/errors.po deleted file mode 100644 index a589998..0000000 --- a/integration_test/temple_demo/priv/gettext/en/LC_MESSAGES/errors.po +++ /dev/null @@ -1,97 +0,0 @@ -## `msgid`s in this file come from POT (.pot) files. -## -## Do not add, change, or remove `msgid`s manually here as -## they're tied to the ones in the corresponding POT file -## (with the same domain). -## -## Use `mix gettext.extract --merge` or `mix gettext.merge` -## to merge POT files into PO files. -msgid "" -msgstr "" -"Language: en\n" - -## From Ecto.Changeset.cast/4 -msgid "can't be blank" -msgstr "" - -## From Ecto.Changeset.unique_constraint/3 -msgid "has already been taken" -msgstr "" - -## From Ecto.Changeset.put_change/3 -msgid "is invalid" -msgstr "" - -## From Ecto.Changeset.validate_acceptance/3 -msgid "must be accepted" -msgstr "" - -## From Ecto.Changeset.validate_format/3 -msgid "has invalid format" -msgstr "" - -## From Ecto.Changeset.validate_subset/3 -msgid "has an invalid entry" -msgstr "" - -## From Ecto.Changeset.validate_exclusion/3 -msgid "is reserved" -msgstr "" - -## From Ecto.Changeset.validate_confirmation/3 -msgid "does not match confirmation" -msgstr "" - -## From Ecto.Changeset.no_assoc_constraint/3 -msgid "is still associated with this entry" -msgstr "" - -msgid "are still associated with this entry" -msgstr "" - -## From Ecto.Changeset.validate_length/3 -msgid "should be %{count} character(s)" -msgid_plural "should be %{count} character(s)" -msgstr[0] "" -msgstr[1] "" - -msgid "should have %{count} item(s)" -msgid_plural "should have %{count} item(s)" -msgstr[0] "" -msgstr[1] "" - -msgid "should be at least %{count} character(s)" -msgid_plural "should be at least %{count} character(s)" -msgstr[0] "" -msgstr[1] "" - -msgid "should have at least %{count} item(s)" -msgid_plural "should have at least %{count} item(s)" -msgstr[0] "" -msgstr[1] "" - -msgid "should be at most %{count} character(s)" -msgid_plural "should be at most %{count} character(s)" -msgstr[0] "" -msgstr[1] "" - -msgid "should have at most %{count} item(s)" -msgid_plural "should have at most %{count} item(s)" -msgstr[0] "" -msgstr[1] "" - -## From Ecto.Changeset.validate_number/3 -msgid "must be less than %{number}" -msgstr "" - -msgid "must be greater than %{number}" -msgstr "" - -msgid "must be less than or equal to %{number}" -msgstr "" - -msgid "must be greater than or equal to %{number}" -msgstr "" - -msgid "must be equal to %{number}" -msgstr "" diff --git a/integration_test/temple_demo/priv/gettext/errors.pot b/integration_test/temple_demo/priv/gettext/errors.pot deleted file mode 100644 index 39a220b..0000000 --- a/integration_test/temple_demo/priv/gettext/errors.pot +++ /dev/null @@ -1,95 +0,0 @@ -## This is a PO Template file. -## -## `msgid`s here are often extracted from source code. -## Add new translations manually only if they're dynamic -## translations that can't be statically extracted. -## -## Run `mix gettext.extract` to bring this file up to -## date. Leave `msgstr`s empty as changing them here has no -## effect: edit them in PO (`.po`) files instead. - -## From Ecto.Changeset.cast/4 -msgid "can't be blank" -msgstr "" - -## From Ecto.Changeset.unique_constraint/3 -msgid "has already been taken" -msgstr "" - -## From Ecto.Changeset.put_change/3 -msgid "is invalid" -msgstr "" - -## From Ecto.Changeset.validate_acceptance/3 -msgid "must be accepted" -msgstr "" - -## From Ecto.Changeset.validate_format/3 -msgid "has invalid format" -msgstr "" - -## From Ecto.Changeset.validate_subset/3 -msgid "has an invalid entry" -msgstr "" - -## From Ecto.Changeset.validate_exclusion/3 -msgid "is reserved" -msgstr "" - -## From Ecto.Changeset.validate_confirmation/3 -msgid "does not match confirmation" -msgstr "" - -## From Ecto.Changeset.no_assoc_constraint/3 -msgid "is still associated with this entry" -msgstr "" - -msgid "are still associated with this entry" -msgstr "" - -## From Ecto.Changeset.validate_length/3 -msgid "should be %{count} character(s)" -msgid_plural "should be %{count} character(s)" -msgstr[0] "" -msgstr[1] "" - -msgid "should have %{count} item(s)" -msgid_plural "should have %{count} item(s)" -msgstr[0] "" -msgstr[1] "" - -msgid "should be at least %{count} character(s)" -msgid_plural "should be at least %{count} character(s)" -msgstr[0] "" -msgstr[1] "" - -msgid "should have at least %{count} item(s)" -msgid_plural "should have at least %{count} item(s)" -msgstr[0] "" -msgstr[1] "" - -msgid "should be at most %{count} character(s)" -msgid_plural "should be at most %{count} character(s)" -msgstr[0] "" -msgstr[1] "" - -msgid "should have at most %{count} item(s)" -msgid_plural "should have at most %{count} item(s)" -msgstr[0] "" -msgstr[1] "" - -## From Ecto.Changeset.validate_number/3 -msgid "must be less than %{number}" -msgstr "" - -msgid "must be greater than %{number}" -msgstr "" - -msgid "must be less than or equal to %{number}" -msgstr "" - -msgid "must be greater than or equal to %{number}" -msgstr "" - -msgid "must be equal to %{number}" -msgstr "" diff --git a/integration_test/temple_demo/priv/repo/migrations/.formatter.exs b/integration_test/temple_demo/priv/repo/migrations/.formatter.exs deleted file mode 100644 index 49f9151..0000000 --- a/integration_test/temple_demo/priv/repo/migrations/.formatter.exs +++ /dev/null @@ -1,4 +0,0 @@ -[ - import_deps: [:ecto_sql], - inputs: ["*.exs"] -] diff --git a/integration_test/temple_demo/priv/repo/migrations/20200522023854_create_posts.exs b/integration_test/temple_demo/priv/repo/migrations/20200522023854_create_posts.exs deleted file mode 100644 index 18ac3da..0000000 --- a/integration_test/temple_demo/priv/repo/migrations/20200522023854_create_posts.exs +++ /dev/null @@ -1,14 +0,0 @@ -defmodule TempleDemo.Repo.Migrations.CreatePosts do - use Ecto.Migration - - def change do - create table(:posts) do - add :title, :string - add :body, :text - add :published_at, :naive_datetime - add :author, :string - - timestamps() - end - end -end diff --git a/integration_test/temple_demo/priv/repo/seeds.exs b/integration_test/temple_demo/priv/repo/seeds.exs deleted file mode 100644 index 04ba8d9..0000000 --- a/integration_test/temple_demo/priv/repo/seeds.exs +++ /dev/null @@ -1,11 +0,0 @@ -# Script for populating the database. You can run it as: -# -# mix run priv/repo/seeds.exs -# -# Inside the script, you can read and write to any of your -# repositories directly: -# -# TempleDemo.Repo.insert!(%TempleDemo.SomeSchema{}) -# -# We recommend using the bang functions (`insert!`, `update!` -# and so on) as they will fail if something goes wrong. diff --git a/integration_test/temple_demo/priv/static/css/app.css b/integration_test/temple_demo/priv/static/css/app.css deleted file mode 100644 index 416c782..0000000 --- a/integration_test/temple_demo/priv/static/css/app.css +++ /dev/null @@ -1,40 +0,0 @@ -/* This file is for your main application css. */ -@import "./phoenix.css"; - -.invalid-feedback { - color: red; -} - -.alert:empty { - display: none; -} - -.alert { - padding: 15px; - margin-bottom: 20px; - border: 1px solid transparent; - border-radius: 4px; -} - -.alert-info { - color: #31708f; - background-color: #d9edf7; - border-color: #bce8f1; -} -.alert-warning { - color: #8a6d3b; - background-color: #fcf8e3; - border-color: #faebcc; -} -.alert-error { - color: #a94442; - background-color: #f2dede; - border-color: #ebccd1; -} -.alert p { - margin-bottom: 0; -} - -strong { - font-weight: 700; -} diff --git a/integration_test/temple_demo/priv/static/css/phoenix.css b/integration_test/temple_demo/priv/static/css/phoenix.css deleted file mode 100644 index 3767b31..0000000 --- a/integration_test/temple_demo/priv/static/css/phoenix.css +++ /dev/null @@ -1,101 +0,0 @@ -/* Includes some default style for the starter application. - * This can be safely deleted to start fresh. - */ - -/* Milligram v1.3.0 https://milligram.github.io - * Copyright (c) 2017 CJ Patoilo Licensed under the MIT license - */ - -*,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#000000;font-family:'Helvetica', 'Arial', sans-serif;font-size:1.6em;font-weight:300;line-height:1.6}blockquote{border-left:0.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin-bottom:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#0069d9;border:0.1rem solid #0069d9;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3.0rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#0069d9;border-color:#0069d9}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#0069d9}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#0069d9}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#0069d9}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#0069d9}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;margin:0 .2rem;padding:.2rem .5rem;white-space:nowrap}pre{background:#f4f5f6;border-left:0.3rem solid #0069d9;overflow-y:hidden}pre>code{border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:0.1rem solid #f4f5f6;margin:3.0rem 0}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;box-sizing:inherit;height:3.8rem;padding:.6rem 1.0rem;width:100%}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,textarea:focus,select:focus{border-color:#0069d9;outline:0}select{background:url('data:image/svg+xml;utf8,') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,')}textarea{min-height:6.5rem}label,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.row-no-padding{padding:0}.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-items:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretch{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1 1 auto;margin-left:0;max-width:100%;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .column .column-bottom{align-self:flex-end}.row .column .column-center{-ms-grid-row-align:center;align-self:center}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1.0rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1.0rem}}a{color:#0069d9;text-decoration:none}a:focus,a:hover{color:#606c76}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{font-size:90%;margin:1.5rem 0 1.5rem 3.0rem}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}table{border-spacing:0;width:100%}td,th{border-bottom:0.1rem solid #e1e1e1;padding:1.2rem 1.5rem;text-align:left}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right} - -/* General style */ -h1{font-size: 3.6rem; line-height: 1.25} -h2{font-size: 2.8rem; line-height: 1.3} -h3{font-size: 2.2rem; letter-spacing: -.08rem; line-height: 1.35} -h4{font-size: 1.8rem; letter-spacing: -.05rem; line-height: 1.5} -h5{font-size: 1.6rem; letter-spacing: 0; line-height: 1.4} -h6{font-size: 1.4rem; letter-spacing: 0; line-height: 1.2} -pre{padding: 1em;} - -.container{ - margin: 0 auto; - max-width: 80.0rem; - padding: 0 2.0rem; - position: relative; - width: 100% -} -select { - width: auto; -} - -/* Phoenix promo and logo */ -.phx-hero { - text-align: center; - border-bottom: 1px solid #e3e3e3; - background: #eee; - border-radius: 6px; - padding: 3em 3em 1em; - margin-bottom: 3rem; - font-weight: 200; - font-size: 120%; -} -.phx-hero input { - background: #ffffff; -} -.phx-logo { - min-width: 300px; - margin: 1rem; - display: block; -} -.phx-logo img { - width: auto; - display: block; -} - -/* Headers */ -header { - width: 100%; - background: #fdfdfd; - border-bottom: 1px solid #eaeaea; - margin-bottom: 2rem; -} -header section { - align-items: center; - display: flex; - flex-direction: column; - justify-content: space-between; -} -header section :first-child { - order: 2; -} -header section :last-child { - order: 1; -} -header nav ul, -header nav li { - margin: 0; - padding: 0; - display: block; - text-align: right; - white-space: nowrap; -} -header nav ul { - margin: 1rem; - margin-top: 0; -} -header nav a { - display: block; -} - -@media (min-width: 40.0rem) { /* Small devices (landscape phones, 576px and up) */ - header section { - flex-direction: row; - } - header nav ul { - margin: 1rem; - } - .phx-logo { - flex-basis: 527px; - margin: 2rem 1rem; - } -} diff --git a/integration_test/temple_demo/priv/static/favicon.ico b/integration_test/temple_demo/priv/static/favicon.ico deleted file mode 100644 index 73de524..0000000 Binary files a/integration_test/temple_demo/priv/static/favicon.ico and /dev/null differ diff --git a/integration_test/temple_demo/priv/static/images/phoenix.png b/integration_test/temple_demo/priv/static/images/phoenix.png deleted file mode 100644 index 9c81075..0000000 Binary files a/integration_test/temple_demo/priv/static/images/phoenix.png and /dev/null differ diff --git a/integration_test/temple_demo/priv/static/js/app.js b/integration_test/temple_demo/priv/static/js/app.js deleted file mode 100644 index c0b39de..0000000 --- a/integration_test/temple_demo/priv/static/js/app.js +++ /dev/null @@ -1,3 +0,0 @@ -// for phoenix_html support, including form and button helpers -// copy the following scripts into your javascript bundle: -// * deps/phoenix_html/priv/static/phoenix_html.js diff --git a/integration_test/temple_demo/priv/static/js/phoenix.js b/integration_test/temple_demo/priv/static/js/phoenix.js deleted file mode 100644 index 692f637..0000000 --- a/integration_test/temple_demo/priv/static/js/phoenix.js +++ /dev/null @@ -1 +0,0 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Phoenix=t():e.Phoenix=t()}(this,(function(){return function(e){var t={};function n(i){if(t[i])return t[i].exports;var o=t[i]={i:i,l:!1,exports:{}};return e[i].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,i){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(i,o,function(t){return e[t]}.bind(null,o));return i},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){(function(t){e.exports=t.Phoenix=n(2)}).call(this,n(1))},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t,n){"use strict";function i(e){return function(e){if(Array.isArray(e))return a(e)}(e)||function(e){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(e))return Array.from(e)}(e)||s(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function r(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){if("undefined"==typeof Symbol||!(Symbol.iterator in Object(e)))return;var n=[],i=!0,o=!1,r=void 0;try{for(var s,a=e[Symbol.iterator]();!(i=(s=a.next()).done)&&(n.push(s.value),!t||n.length!==t);i=!0);}catch(e){o=!0,r=e}finally{try{i||null==a.return||a.return()}finally{if(o)throw r}}return n}(e,t)||s(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function s(e,t){if(e){if("string"==typeof e)return a(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(n):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?a(e,t):void 0}}function a(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,i=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:this.timeout;if(this.joinedOnce)throw new Error("tried to join multiple times. 'join' can only be called a single time per channel instance");return this.timeout=e,this.joinedOnce=!0,this.rejoin(),this.joinPush}},{key:"onClose",value:function(e){this.on(R,e)}},{key:"onError",value:function(e){return this.on(S,(function(t){return e(t)}))}},{key:"on",value:function(e,t){var n=this.bindingRef++;return this.bindings.push({event:e,ref:n,callback:t}),n}},{key:"off",value:function(e,t){this.bindings=this.bindings.filter((function(n){return!(n.event===e&&(void 0===t||t===n.ref))}))}},{key:"canPush",value:function(){return this.socket.isConnected()&&this.isJoined()}},{key:"push",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:this.timeout;if(!this.joinedOnce)throw new Error("tried to push '".concat(e,"' to '").concat(this.topic,"' before joining. Use channel.join() before pushing events"));var i=new _(this,e,(function(){return t}),n);return this.canPush()?i.send():(i.startTimeout(),this.pushBuffer.push(i)),i}},{key:"leave",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.timeout;this.rejoinTimer.reset(),this.joinPush.cancelTimeout(),this.state=C;var n=function(){e.socket.hasLogger()&&e.socket.log("channel","leave ".concat(e.topic)),e.trigger(R,"leave")},i=new _(this,E,L({}),t);return i.receive("ok",(function(){return n()})).receive("timeout",(function(){return n()})),i.send(),this.canPush()||i.trigger("ok",{}),i}},{key:"onMessage",value:function(e,t,n){return t}},{key:"isLifecycleEvent",value:function(e){return x.indexOf(e)>=0}},{key:"isMember",value:function(e,t,n,i){return this.topic===e&&(!i||i===this.joinRef()||!this.isLifecycleEvent(t)||(this.socket.hasLogger()&&this.socket.log("channel","dropping outdated message",{topic:e,event:t,payload:n,joinRef:i}),!1))}},{key:"joinRef",value:function(){return this.joinPush.ref}},{key:"rejoin",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.timeout;this.isLeaving()||(this.socket.leaveOpenTopic(this.topic),this.state=j,this.joinPush.resend(e))}},{key:"trigger",value:function(e,t,n,i){var o=this.onMessage(e,t,n,i);if(t&&!o)throw new Error("channel onMessage callbacks must return the payload, modified or unmodified");for(var r=this.bindings.filter((function(t){return t.event===e})),s=0;s1&&void 0!==arguments[1]?arguments[1]:{};c(this,e),this.stateChangeCallbacks={open:[],close:[],error:[],message:[]},this.channels=[],this.sendBuffer=[],this.ref=0,this.timeout=i.timeout||1e4,this.transport=i.transport||d.WebSocket||H,this.defaultEncoder=M.encode,this.defaultDecoder=M.decode,this.closeWasClean=!1,this.unloaded=!1,this.binaryType=i.binaryType||"arraybuffer",this.transport!==H?(this.encode=i.encode||this.defaultEncoder,this.decode=i.decode||this.defaultDecoder):(this.encode=this.defaultEncoder,this.decode=this.defaultDecoder),f&&f.addEventListener&&f.addEventListener("unload",(function(e){n.conn&&(n.unloaded=!0,n.abnormalClose("unloaded"))})),this.heartbeatIntervalMs=i.heartbeatIntervalMs||3e4,this.rejoinAfterMs=function(e){return i.rejoinAfterMs?i.rejoinAfterMs(e):[1e3,2e3,5e3][e-1]||1e4},this.reconnectAfterMs=function(e){return n.unloaded?100:i.reconnectAfterMs?i.reconnectAfterMs(e):[10,50,100,150,200,250,500,1e3,2e3][e-1]||5e3},this.logger=i.logger||null,this.longpollerTimeout=i.longpollerTimeout||2e4,this.params=L(i.params||{}),this.endPoint="".concat(t,"/").concat(P),this.vsn=i.vsn||"2.0.0",this.heartbeatTimer=null,this.pendingHeartbeatRef=null,this.reconnectTimer=new I((function(){n.teardown((function(){return n.connect()}))}),this.reconnectAfterMs)}return h(e,[{key:"protocol",value:function(){return location.protocol.match(/^https/)?"wss":"ws"}},{key:"endPointURL",value:function(){var e=D.appendParams(D.appendParams(this.endPoint,this.params()),{vsn:this.vsn});return"/"!==e.charAt(0)?e:"/"===e.charAt(1)?"".concat(this.protocol(),":").concat(e):"".concat(this.protocol(),"://").concat(location.host).concat(e)}},{key:"disconnect",value:function(e,t,n){this.closeWasClean=!0,this.reconnectTimer.reset(),this.teardown(e,t,n)}},{key:"connect",value:function(e){var t=this;e&&(console&&console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor"),this.params=L(e)),this.conn||(this.closeWasClean=!1,this.conn=new this.transport(this.endPointURL()),this.conn.binaryType=this.binaryType,this.conn.timeout=this.longpollerTimeout,this.conn.onopen=function(){return t.onConnOpen()},this.conn.onerror=function(e){return t.onConnError(e)},this.conn.onmessage=function(e){return t.onConnMessage(e)},this.conn.onclose=function(e){return t.onConnClose(e)})}},{key:"log",value:function(e,t,n){this.logger(e,t,n)}},{key:"hasLogger",value:function(){return null!==this.logger}},{key:"onOpen",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.open.push([t,e]),t}},{key:"onClose",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.close.push([t,e]),t}},{key:"onError",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.error.push([t,e]),t}},{key:"onMessage",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.message.push([t,e]),t}},{key:"onConnOpen",value:function(){this.hasLogger()&&this.log("transport","connected to ".concat(this.endPointURL())),this.unloaded=!1,this.closeWasClean=!1,this.flushSendBuffer(),this.reconnectTimer.reset(),this.resetHeartbeat(),this.stateChangeCallbacks.open.forEach((function(e){return(0,r(e,2)[1])()}))}},{key:"resetHeartbeat",value:function(){var e=this;this.conn&&this.conn.skipHeartbeat||(this.pendingHeartbeatRef=null,clearInterval(this.heartbeatTimer),this.heartbeatTimer=setInterval((function(){return e.sendHeartbeat()}),this.heartbeatIntervalMs))}},{key:"teardown",value:function(e,t,n){var i=this;if(!this.conn)return e&&e();this.waitForBufferDone((function(){i.conn&&(t?i.conn.close(t,n||""):i.conn.close()),i.waitForSocketClosed((function(){i.conn&&(i.conn.onclose=function(){},i.conn=null),e&&e()}))}))}},{key:"waitForBufferDone",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1;5===n||!this.conn||this.conn.bufferedAmount&&0===this.conn.bufferedAmount?e():setTimeout((function(){t.waitForBufferDone(e,n+1)}),150*n)}},{key:"waitForSocketClosed",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1;5!==n&&this.conn&&this.conn.readyState!==m?setTimeout((function(){t.waitForSocketClosed(e,n+1)}),150*n):e()}},{key:"onConnClose",value:function(e){this.hasLogger()&&this.log("transport","close",e),this.triggerChanError(),clearInterval(this.heartbeatTimer),this.closeWasClean||this.reconnectTimer.scheduleTimeout(),this.stateChangeCallbacks.close.forEach((function(t){return(0,r(t,2)[1])(e)}))}},{key:"onConnError",value:function(e){this.hasLogger()&&this.log("transport",e),this.triggerChanError(),this.stateChangeCallbacks.error.forEach((function(t){return(0,r(t,2)[1])(e)}))}},{key:"triggerChanError",value:function(){this.channels.forEach((function(e){e.isErrored()||e.isLeaving()||e.isClosed()||e.trigger(S)}))}},{key:"connectionState",value:function(){switch(this.conn&&this.conn.readyState){case p:return"connecting";case v:return"open";case y:return"closing";default:return"closed"}}},{key:"isConnected",value:function(){return"open"===this.connectionState()}},{key:"remove",value:function(e){this.off(e.stateChangeRefs),this.channels=this.channels.filter((function(t){return t.joinRef()!==e.joinRef()}))}},{key:"off",value:function(e){for(var t in this.stateChangeCallbacks)this.stateChangeCallbacks[t]=this.stateChangeCallbacks[t].filter((function(t){var n=r(t,1)[0];return-1===e.indexOf(n)}))}},{key:"channel",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=new A(e,t,this);return this.channels.push(n),n}},{key:"push",value:function(e){var t=this;if(this.hasLogger()){var n=e.topic,i=e.event,o=e.payload,r=e.ref,s=e.join_ref;this.log("push","".concat(n," ").concat(i," (").concat(s,", ").concat(r,")"),o)}this.isConnected()?this.encode(e,(function(e){return t.conn.send(e)})):this.sendBuffer.push((function(){return t.encode(e,(function(e){return t.conn.send(e)}))}))}},{key:"makeRef",value:function(){var e=this.ref+1;return e===this.ref?this.ref=0:this.ref=e,this.ref.toString()}},{key:"sendHeartbeat",value:function(){if(this.isConnected()){if(this.pendingHeartbeatRef)return this.pendingHeartbeatRef=null,this.hasLogger()&&this.log("transport","heartbeat timeout. Attempting to re-establish connection"),void this.abnormalClose("heartbeat timeout");this.pendingHeartbeatRef=this.makeRef(),this.push({topic:"phoenix",event:"heartbeat",payload:{},ref:this.pendingHeartbeatRef})}}},{key:"abnormalClose",value:function(e){this.closeWasClean=!1,this.conn.close(1e3,e)}},{key:"flushSendBuffer",value:function(){this.isConnected()&&this.sendBuffer.length>0&&(this.sendBuffer.forEach((function(e){return e()})),this.sendBuffer=[])}},{key:"onConnMessage",value:function(e){var t=this;this.decode(e.data,(function(e){var n=e.topic,i=e.event,o=e.payload,s=e.ref,a=e.join_ref;s&&s===t.pendingHeartbeatRef&&(t.pendingHeartbeatRef=null),t.hasLogger()&&t.log("receive","".concat(o.status||""," ").concat(n," ").concat(i," ").concat(s&&"("+s+")"||""),o);for(var c=0;c1&&void 0!==arguments[1]?arguments[1]:{};c(this,e);var o=i.events||{state:"presence_state",diff:"presence_diff"};this.state={},this.pendingDiffs=[],this.channel=t,this.joinRef=null,this.caller={onJoin:function(){},onLeave:function(){},onSync:function(){}},this.channel.on(o.state,(function(t){var i=n.caller,o=i.onJoin,r=i.onLeave,s=i.onSync;n.joinRef=n.channel.joinRef(),n.state=e.syncState(n.state,t,o,r),n.pendingDiffs.forEach((function(t){n.state=e.syncDiff(n.state,t,o,r)})),n.pendingDiffs=[],s()})),this.channel.on(o.diff,(function(t){var i=n.caller,o=i.onJoin,r=i.onLeave,s=i.onSync;n.inPendingSyncState()?n.pendingDiffs.push(t):(n.state=e.syncDiff(n.state,t,o,r),s())}))}return h(e,[{key:"onJoin",value:function(e){this.caller.onJoin=e}},{key:"onLeave",value:function(e){this.caller.onLeave=e}},{key:"onSync",value:function(e){this.caller.onSync=e}},{key:"list",value:function(t){return e.list(this.state,t)}},{key:"inPendingSyncState",value:function(){return!this.joinRef||this.joinRef!==this.channel.joinRef()}}],[{key:"syncState",value:function(e,t,n,i){var o=this,r=this.clone(e),s={},a={};return this.map(r,(function(e,n){t[e]||(a[e]=n)})),this.map(t,(function(e,t){var n=r[e];if(n){var i=t.metas.map((function(e){return e.phx_ref})),c=n.metas.map((function(e){return e.phx_ref})),u=t.metas.filter((function(e){return c.indexOf(e.phx_ref)<0})),h=n.metas.filter((function(e){return i.indexOf(e.phx_ref)<0}));u.length>0&&(s[e]=t,s[e].metas=u),h.length>0&&(a[e]=o.clone(n),a[e].metas=h)}else s[e]=t})),this.syncDiff(r,{joins:s,leaves:a},n,i)}},{key:"syncDiff",value:function(e,t,n,o){var r=t.joins,s=t.leaves,a=this.clone(e);return n||(n=function(){}),o||(o=function(){}),this.map(r,(function(e,t){var o=a[e];if(a[e]=t,o){var r,s=a[e].metas.map((function(e){return e.phx_ref})),c=o.metas.filter((function(e){return s.indexOf(e.phx_ref)<0}));(r=a[e].metas).unshift.apply(r,i(c))}n(e,o,t)})),this.map(s,(function(e,t){var n=a[e];if(n){var i=t.metas.map((function(e){return e.phx_ref}));n.metas=n.metas.filter((function(e){return i.indexOf(e.phx_ref)<0})),o(e,n,t),0===n.metas.length&&delete a[e]}})),a}},{key:"list",value:function(e,t){return t||(t=function(e,t){return t}),this.map(e,(function(e,n){return t(e,n)}))}},{key:"map",value:function(e,t){return Object.getOwnPropertyNames(e).map((function(n){return t(n,e[n])}))}},{key:"clone",value:function(e){return JSON.parse(JSON.stringify(e))}}]),e}(),I=function(){function e(t,n){c(this,e),this.callback=t,this.timerCalc=n,this.timer=null,this.tries=0}return h(e,[{key:"reset",value:function(){this.tries=0,clearTimeout(this.timer)}},{key:"scheduleTimeout",value:function(){var e=this;clearTimeout(this.timer),this.timer=setTimeout((function(){e.tries=e.tries+1,e.callback()}),this.timerCalc(this.tries+1))}}]),e}()}])})); \ No newline at end of file diff --git a/integration_test/temple_demo/priv/static/js/phoenix_html.js b/integration_test/temple_demo/priv/static/js/phoenix_html.js deleted file mode 100644 index e1ae852..0000000 --- a/integration_test/temple_demo/priv/static/js/phoenix_html.js +++ /dev/null @@ -1,76 +0,0 @@ -"use strict"; - -(function() { - var PolyfillEvent = eventConstructor(); - - function eventConstructor() { - if (typeof window.CustomEvent === "function") return window.CustomEvent; - // IE<=9 Support - function CustomEvent(event, params) { - params = params || {bubbles: false, cancelable: false, detail: undefined}; - var evt = document.createEvent('CustomEvent'); - evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); - return evt; - } - CustomEvent.prototype = window.Event.prototype; - return CustomEvent; - } - - function buildHiddenInput(name, value) { - var input = document.createElement("input"); - input.type = "hidden"; - input.name = name; - input.value = value; - return input; - } - - function handleClick(element) { - var to = element.getAttribute("data-to"), - method = buildHiddenInput("_method", element.getAttribute("data-method")), - csrf = buildHiddenInput("_csrf_token", element.getAttribute("data-csrf")), - form = document.createElement("form"), - target = element.getAttribute("target"); - - form.method = (element.getAttribute("data-method") === "get") ? "get" : "post"; - form.action = to; - form.style.display = "hidden"; - - if (target) form.target = target; - - form.appendChild(csrf); - form.appendChild(method); - document.body.appendChild(form); - form.submit(); - } - - window.addEventListener("click", function(e) { - var element = e.target; - - while (element && element.getAttribute) { - var phoenixLinkEvent = new PolyfillEvent('phoenix.link.click', { - "bubbles": true, "cancelable": true - }); - - if (!element.dispatchEvent(phoenixLinkEvent)) { - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } - - if (element.getAttribute("data-method")) { - handleClick(element); - e.preventDefault(); - return false; - } else { - element = element.parentNode; - } - } - }, false); - - window.addEventListener('phoenix.link.click', function (e) { - var message = e.target.getAttribute("data-confirm"); - if(message && !window.confirm(message)) { - e.preventDefault(); - } - }, false); -})(); diff --git a/integration_test/temple_demo/priv/static/robots.txt b/integration_test/temple_demo/priv/static/robots.txt deleted file mode 100644 index 3c9c7c0..0000000 --- a/integration_test/temple_demo/priv/static/robots.txt +++ /dev/null @@ -1,5 +0,0 @@ -# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file -# -# To ban all spiders from the entire site uncomment the next two lines: -# User-agent: * -# Disallow: / diff --git a/integration_test/temple_demo/test/support/channel_case.ex b/integration_test/temple_demo/test/support/channel_case.ex deleted file mode 100644 index 1cbae75..0000000 --- a/integration_test/temple_demo/test/support/channel_case.ex +++ /dev/null @@ -1,40 +0,0 @@ -defmodule TempleDemoWeb.ChannelCase do - @moduledoc """ - This module defines the test case to be used by - channel tests. - - Such tests rely on `Phoenix.ChannelTest` and also - import other functionality to make it easier - to build common data structures and query the data layer. - - Finally, if the test case interacts with the database, - we enable the SQL sandbox, so changes done to the database - are reverted at the end of every test. If you are using - PostgreSQL, you can even run database tests asynchronously - by setting `use TempleDemoWeb.ChannelCase, async: true`, although - this option is not recommended for other databases. - """ - - use ExUnit.CaseTemplate - - using do - quote do - # Import conveniences for testing with channels - import Phoenix.ChannelTest - import TempleDemoWeb.ChannelCase - - # The default endpoint for testing - @endpoint TempleDemoWeb.Endpoint - end - end - - setup tags do - :ok = Ecto.Adapters.SQL.Sandbox.checkout(TempleDemo.Repo) - - unless tags[:async] do - Ecto.Adapters.SQL.Sandbox.mode(TempleDemo.Repo, {:shared, self()}) - end - - :ok - end -end diff --git a/integration_test/temple_demo/test/support/conn_case.ex b/integration_test/temple_demo/test/support/conn_case.ex deleted file mode 100644 index 8ce6ed3..0000000 --- a/integration_test/temple_demo/test/support/conn_case.ex +++ /dev/null @@ -1,43 +0,0 @@ -defmodule TempleDemoWeb.ConnCase do - @moduledoc """ - This module defines the test case to be used by - tests that require setting up a connection. - - Such tests rely on `Phoenix.ConnTest` and also - import other functionality to make it easier - to build common data structures and query the data layer. - - Finally, if the test case interacts with the database, - we enable the SQL sandbox, so changes done to the database - are reverted at the end of every test. If you are using - PostgreSQL, you can even run database tests asynchronously - by setting `use TempleDemoWeb.ConnCase, async: true`, although - this option is not recommended for other databases. - """ - - use ExUnit.CaseTemplate - - using do - quote do - # Import conveniences for testing with connections - import Plug.Conn - import Phoenix.ConnTest - import TempleDemoWeb.ConnCase - - alias TempleDemoWeb.Router.Helpers, as: Routes - - # The default endpoint for testing - @endpoint TempleDemoWeb.Endpoint - end - end - - setup tags do - :ok = Ecto.Adapters.SQL.Sandbox.checkout(TempleDemo.Repo) - - unless tags[:async] do - Ecto.Adapters.SQL.Sandbox.mode(TempleDemo.Repo, {:shared, self()}) - end - - {:ok, conn: Phoenix.ConnTest.build_conn()} - end -end diff --git a/integration_test/temple_demo/test/support/data_case.ex b/integration_test/temple_demo/test/support/data_case.ex deleted file mode 100644 index 7dfbe0e..0000000 --- a/integration_test/temple_demo/test/support/data_case.ex +++ /dev/null @@ -1,55 +0,0 @@ -defmodule TempleDemo.DataCase do - @moduledoc """ - This module defines the setup for tests requiring - access to the application's data layer. - - You may define functions here to be used as helpers in - your tests. - - Finally, if the test case interacts with the database, - we enable the SQL sandbox, so changes done to the database - are reverted at the end of every test. If you are using - PostgreSQL, you can even run database tests asynchronously - by setting `use TempleDemo.DataCase, async: true`, although - this option is not recommended for other databases. - """ - - use ExUnit.CaseTemplate - - using do - quote do - alias TempleDemo.Repo - - import Ecto - import Ecto.Changeset - import Ecto.Query - import TempleDemo.DataCase - end - end - - setup tags do - :ok = Ecto.Adapters.SQL.Sandbox.checkout(TempleDemo.Repo) - - unless tags[:async] do - Ecto.Adapters.SQL.Sandbox.mode(TempleDemo.Repo, {:shared, self()}) - end - - :ok - end - - @doc """ - A helper that transforms changeset errors into a map of messages. - - assert {:error, changeset} = Accounts.create_user(%{password: "short"}) - assert "password is too short" in errors_on(changeset).password - assert %{password: ["password is too short"]} = errors_on(changeset) - - """ - def errors_on(changeset) do - Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> - Regex.replace(~r"%{(\w+)}", message, fn _, key -> - opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() - end) - end) - end -end diff --git a/integration_test/temple_demo/test/temple_demo/blog_test.exs b/integration_test/temple_demo/test/temple_demo/blog_test.exs deleted file mode 100644 index 8a6c7f3..0000000 --- a/integration_test/temple_demo/test/temple_demo/blog_test.exs +++ /dev/null @@ -1,80 +0,0 @@ -defmodule TempleDemo.BlogTest do - use TempleDemo.DataCase - - alias TempleDemo.Blog - - describe "posts" do - alias TempleDemo.Blog.Post - - @valid_attrs %{ - author: "some author", - body: "some body", - published_at: ~N[2010-04-17 14:00:00], - title: "some title" - } - @update_attrs %{ - author: "some updated author", - body: "some updated body", - published_at: ~N[2011-05-18 15:01:01], - title: "some updated title" - } - @invalid_attrs %{author: nil, body: nil, published_at: nil, title: nil} - - def post_fixture(attrs \\ %{}) do - {:ok, post} = - attrs - |> Enum.into(@valid_attrs) - |> Blog.create_post() - - post - end - - test "list_posts/0 returns all posts" do - post = post_fixture() - assert Blog.list_posts() == [post] - end - - test "get_post!/1 returns the post with given id" do - post = post_fixture() - assert Blog.get_post!(post.id) == post - end - - test "create_post/1 with valid data creates a post" do - assert {:ok, %Post{} = post} = Blog.create_post(@valid_attrs) - assert post.author == "some author" - assert post.body == "some body" - assert post.published_at == ~N[2010-04-17 14:00:00] - assert post.title == "some title" - end - - test "create_post/1 with invalid data returns error changeset" do - assert {:error, %Ecto.Changeset{}} = Blog.create_post(@invalid_attrs) - end - - test "update_post/2 with valid data updates the post" do - post = post_fixture() - assert {:ok, %Post{} = post} = Blog.update_post(post, @update_attrs) - assert post.author == "some updated author" - assert post.body == "some updated body" - assert post.published_at == ~N[2011-05-18 15:01:01] - assert post.title == "some updated title" - end - - test "update_post/2 with invalid data returns error changeset" do - post = post_fixture() - assert {:error, %Ecto.Changeset{}} = Blog.update_post(post, @invalid_attrs) - assert post == Blog.get_post!(post.id) - end - - test "delete_post/1 deletes the post" do - post = post_fixture() - assert {:ok, %Post{}} = Blog.delete_post(post) - assert_raise Ecto.NoResultsError, fn -> Blog.get_post!(post.id) end - end - - test "change_post/1 returns a post changeset" do - post = post_fixture() - assert %Ecto.Changeset{} = Blog.change_post(post) - end - end -end diff --git a/integration_test/temple_demo/test/temple_demo_web/controllers/page_controller_test.exs b/integration_test/temple_demo/test/temple_demo_web/controllers/page_controller_test.exs deleted file mode 100644 index b6c60a9..0000000 --- a/integration_test/temple_demo/test/temple_demo_web/controllers/page_controller_test.exs +++ /dev/null @@ -1,8 +0,0 @@ -defmodule TempleDemoWeb.PageControllerTest do - use TempleDemoWeb.ConnCase - - test "GET /", %{conn: conn} do - conn = get(conn, "/") - assert html_response(conn, 200) =~ "Welcome to Phoenix!" - end -end diff --git a/integration_test/temple_demo/test/temple_demo_web/controllers/post_controller_test.exs b/integration_test/temple_demo/test/temple_demo_web/controllers/post_controller_test.exs deleted file mode 100644 index f226242..0000000 --- a/integration_test/temple_demo/test/temple_demo_web/controllers/post_controller_test.exs +++ /dev/null @@ -1,99 +0,0 @@ -defmodule TempleDemoWeb.PostControllerTest do - use TempleDemoWeb.ConnCase - - alias TempleDemo.Blog - - @create_attrs %{ - author: "some author", - body: "some body", - published_at: ~N[2010-04-17 14:00:00], - title: "some title" - } - @update_attrs %{ - author: "some updated author", - body: "some updated body", - published_at: ~N[2011-05-18 15:01:01], - title: "some updated title" - } - @invalid_attrs %{author: nil, body: nil, published_at: nil, title: nil} - - def fixture(:post) do - {:ok, post} = Blog.create_post(@create_attrs) - post - end - - describe "index" do - test "lists all posts", %{conn: conn} do - conn = get(conn, Routes.post_path(conn, :index)) - assert html_response(conn, 200) =~ "Listing Posts" - end - end - - describe "new post" do - test "renders form", %{conn: conn} do - conn = get(conn, Routes.post_path(conn, :new)) - assert html_response(conn, 200) =~ "New Post" - end - end - - describe "create post" do - test "redirects to show when data is valid", %{conn: conn} do - conn = post(conn, Routes.post_path(conn, :create), post: @create_attrs) - - assert %{id: id} = redirected_params(conn) - assert redirected_to(conn) == Routes.post_path(conn, :show, id) - - conn = get(conn, Routes.post_path(conn, :show, id)) - assert html_response(conn, 200) =~ "Show Post" - end - - test "renders errors when data is invalid", %{conn: conn} do - conn = post(conn, Routes.post_path(conn, :create), post: @invalid_attrs) - assert html_response(conn, 200) =~ "New Post" - end - end - - describe "edit post" do - setup [:create_post] - - test "renders form for editing chosen post", %{conn: conn, post: post} do - conn = get(conn, Routes.post_path(conn, :edit, post)) - assert html_response(conn, 200) =~ "Edit Post" - end - end - - describe "update post" do - setup [:create_post] - - test "redirects when data is valid", %{conn: conn, post: post} do - conn = put(conn, Routes.post_path(conn, :update, post), post: @update_attrs) - assert redirected_to(conn) == Routes.post_path(conn, :show, post) - - conn = get(conn, Routes.post_path(conn, :show, post)) - assert html_response(conn, 200) =~ "some updated author" - end - - test "renders errors when data is invalid", %{conn: conn, post: post} do - conn = put(conn, Routes.post_path(conn, :update, post), post: @invalid_attrs) - assert html_response(conn, 200) =~ "Edit Post" - end - end - - describe "delete post" do - setup [:create_post] - - test "deletes chosen post", %{conn: conn, post: post} do - conn = delete(conn, Routes.post_path(conn, :delete, post)) - assert redirected_to(conn) == Routes.post_path(conn, :index) - - assert_error_sent 404, fn -> - get(conn, Routes.post_path(conn, :show, post)) - end - end - end - - defp create_post(_) do - post = fixture(:post) - {:ok, post: post} - end -end diff --git a/integration_test/temple_demo/test/temple_demo_web/features/temple_feature_test.exs b/integration_test/temple_demo/test/temple_demo_web/features/temple_feature_test.exs deleted file mode 100644 index 2a42384..0000000 --- a/integration_test/temple_demo/test/temple_demo_web/features/temple_feature_test.exs +++ /dev/null @@ -1,44 +0,0 @@ -defmodule TempleDemoWeb.TempleFeatureTest do - use ExUnit.Case, async: false - use Wallaby.Feature - alias TempleDemoWeb.Router.Helpers, as: Routes - @endpoint TempleDemoWeb.Endpoint - - feature "renders the homepage", %{session: session} do - session - |> visit("/") - |> assert_text("Welcome to Phoenix!") - |> assert_text("inner content of outer") - end - - feature "case statements work", %{session: session} do - session - |> visit("/?text=staging") - |> assert_text("Welcome to Phoenix!") - |> assert_text("Peace-of-mind from prototype to staging") - |> visit("/?text=foobar") - |> assert_text("Welcome to Phoenix!") - |> assert_text("Peace-of-mind from prototype to production") - end - - feature "can create a new post", %{session: session} do - session - |> visit(Routes.post_path(@endpoint, :index)) - |> click(Query.link("New Post")) - |> fill_in(Query.text_field("Title"), with: "Temple is awesome!") - |> fill_in(Query.text_field("Body"), with: "In this post I will show you how to use Temple") - |> find(Query.select("post_published_at_year"), fn s -> - s |> click(Query.option("2020")) - end) - |> find(Query.select("post_published_at_month"), fn s -> - s |> click(Query.option("May")) - end) - |> find(Query.select("post_published_at_day"), fn s -> - s |> click(Query.option("21")) - end) - |> fill_in(Query.text_field("Author"), with: "Mitchelob Ultra") - |> assert_has(Query.css("#disabled-input[disabled]")) - |> click(Query.button("Save")) - |> assert_text("Post created successfully.") - end -end diff --git a/integration_test/temple_demo/test/temple_demo_web/views/error_view_test.exs b/integration_test/temple_demo/test/temple_demo_web/views/error_view_test.exs deleted file mode 100644 index bda7ef1..0000000 --- a/integration_test/temple_demo/test/temple_demo_web/views/error_view_test.exs +++ /dev/null @@ -1,14 +0,0 @@ -defmodule TempleDemoWeb.ErrorViewTest do - use TempleDemoWeb.ConnCase, async: true - - # Bring render/3 and render_to_string/3 for testing custom views - import Phoenix.View - - test "renders 404.html" do - assert render_to_string(TempleDemoWeb.ErrorView, "404.html", []) == "Not Found" - end - - test "renders 500.html" do - assert render_to_string(TempleDemoWeb.ErrorView, "500.html", []) == "Internal Server Error" - end -end diff --git a/integration_test/temple_demo/test/temple_demo_web/views/layout_view_test.exs b/integration_test/temple_demo/test/temple_demo_web/views/layout_view_test.exs deleted file mode 100644 index d84d669..0000000 --- a/integration_test/temple_demo/test/temple_demo_web/views/layout_view_test.exs +++ /dev/null @@ -1,8 +0,0 @@ -defmodule TempleDemoWeb.LayoutViewTest do - use TempleDemoWeb.ConnCase, async: true - - # When testing helpers, you may want to import Phoenix.HTML and - # use functions such as safe_to_string() to convert the helper - # result into an HTML string. - # import Phoenix.HTML -end diff --git a/integration_test/temple_demo/test/temple_demo_web/views/page_view_test.exs b/integration_test/temple_demo/test/temple_demo_web/views/page_view_test.exs deleted file mode 100644 index d6e0210..0000000 --- a/integration_test/temple_demo/test/temple_demo_web/views/page_view_test.exs +++ /dev/null @@ -1,3 +0,0 @@ -defmodule TempleDemoWeb.PageViewTest do - use TempleDemoWeb.ConnCase, async: true -end diff --git a/integration_test/temple_demo/test/test_helper.exs b/integration_test/temple_demo/test/test_helper.exs deleted file mode 100644 index ab9695a..0000000 --- a/integration_test/temple_demo/test/test_helper.exs +++ /dev/null @@ -1,3 +0,0 @@ -Ecto.Adapters.SQL.Sandbox.mode(TempleDemo.Repo, :manual) - -ExUnit.start() diff --git a/integration_test/temple_plug_demo/.formatter.exs b/integration_test/temple_plug_demo/.formatter.exs new file mode 100644 index 0000000..6ab28bf --- /dev/null +++ b/integration_test/temple_plug_demo/.formatter.exs @@ -0,0 +1,5 @@ +# Used by "mix format" +[ + import_deps: [:plug, :temple], + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/integration_test/temple_demo/.gitignore b/integration_test/temple_plug_demo/.gitignore similarity index 78% rename from integration_test/temple_demo/.gitignore rename to integration_test/temple_plug_demo/.gitignore index 50c2c65..1317118 100644 --- a/integration_test/temple_demo/.gitignore +++ b/integration_test/temple_plug_demo/.gitignore @@ -7,7 +7,7 @@ # The directory Mix downloads your dependencies sources to. /deps/ -# Where 3rd-party dependencies like ExDoc output generated docs. +# Where third-party dependencies like ExDoc output generated docs. /doc/ # Ignore .fetch files in case you like to edit your project deps locally. @@ -20,7 +20,7 @@ erl_crash.dump *.ez # Ignore package tarball (built via "mix hex.build"). -temple_demo-*.tar - -/screenshots/ +temple_plug_demo-*.tar +# Temporary files, for example, from tests. +/tmp/ diff --git a/integration_test/temple_plug_demo/README.md b/integration_test/temple_plug_demo/README.md new file mode 100644 index 0000000..4a4cb93 --- /dev/null +++ b/integration_test/temple_plug_demo/README.md @@ -0,0 +1,21 @@ +# TemplePlugDemo + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `temple_plug_demo` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:temple_plug_demo, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at . + diff --git a/integration_test/temple_plug_demo/assets/css/app.css b/integration_test/temple_plug_demo/assets/css/app.css new file mode 100644 index 0000000..1f8afc3 --- /dev/null +++ b/integration_test/temple_plug_demo/assets/css/app.css @@ -0,0 +1,5 @@ +@import "tailwindcss/base"; +@import "tailwindcss/components"; +@import "tailwindcss/utilities"; + + diff --git a/integration_test/temple_plug_demo/assets/tailwind.config.js b/integration_test/temple_plug_demo/assets/tailwind.config.js new file mode 100644 index 0000000..a2874cf --- /dev/null +++ b/integration_test/temple_plug_demo/assets/tailwind.config.js @@ -0,0 +1,13 @@ +// See the Tailwind configuration guide for advanced usage +// https://tailwindcss.com/docs/configuration +module.exports = { + content: [ + '../lib/**/*.*ex' + ], + theme: { + extend: {}, + }, + plugins: [ + require('@tailwindcss/forms') + ] +} diff --git a/integration_test/temple_plug_demo/config/config.exs b/integration_test/temple_plug_demo/config/config.exs new file mode 100644 index 0000000..cabf62c --- /dev/null +++ b/integration_test/temple_plug_demo/config/config.exs @@ -0,0 +1,15 @@ +import Config + +config :tailwind, + version: "3.0.24", + default: [ + args: ~w( + --config=tailwind.config.js + --input=css/app.css + --output=../priv/static/assets/app.css + ), + cd: Path.expand("../assets", __DIR__) + ] + +config :temple_plug_demo, :watchers, + tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]} diff --git a/integration_test/temple_plug_demo/lib/temple_plug_demo.ex b/integration_test/temple_plug_demo/lib/temple_plug_demo.ex new file mode 100644 index 0000000..21a76a6 --- /dev/null +++ b/integration_test/temple_plug_demo/lib/temple_plug_demo.ex @@ -0,0 +1,18 @@ +defmodule TemplePlugDemo do + @moduledoc """ + Documentation for `TemplePlugDemo`. + """ + + @doc """ + Hello world. + + ## Examples + + iex> TemplePlugDemo.hello() + :world + + """ + def hello do + :world + end +end diff --git a/integration_test/temple_plug_demo/lib/temple_plug_demo/application.ex b/integration_test/temple_plug_demo/lib/temple_plug_demo/application.ex new file mode 100644 index 0000000..cd0e3cb --- /dev/null +++ b/integration_test/temple_plug_demo/lib/temple_plug_demo/application.ex @@ -0,0 +1,25 @@ +defmodule TemplePlugDemo.Application do + # See https://hexdocs.pm/elixir/Application.html + # for more information on OTP Applications + @moduledoc false + + use Application + + @impl true + + def start(_type, _args) do + children = + [ + {Bandit, plug: TemplePlugDemo.Router, scheme: :http, options: [port: 4001]} + ] ++ watcher_children() + + opts = [strategy: :one_for_one, name: MyApp.Supervisor] + Supervisor.start_link(children, opts) + end + + def watcher_children() do + for args <- Application.get_env(:temple_plug_demo, :watchers, []) do + {TemplePlugDemo.Watcher, args} + end + end +end diff --git a/integration_test/temple_plug_demo/lib/temple_plug_demo/router.ex b/integration_test/temple_plug_demo/lib/temple_plug_demo/router.ex new file mode 100644 index 0000000..5e68cf7 --- /dev/null +++ b/integration_test/temple_plug_demo/lib/temple_plug_demo/router.ex @@ -0,0 +1,47 @@ +defmodule TemplePlugDemo.Router do + use Plug.Router + + import Temple + + plug Plug.Static, from: {:temple_plug_demo, "priv/static"}, at: "/static" + + plug :match + plug :dispatch + + get "/" do + assigns = %{title: "Motch App"} + + response = + temple do + "" + + html do + head do + title do: @title + + link rel: "stylesheet", href: "/static/assets/app.css" + end + + body class: "font-sans container mx-auto" do + span do + "loose" + end + + span do: "tight" + + div do + "world" + end + end + end + end + + conn + |> put_resp_content_type("text/html") + |> send_resp(200, response) + end + + match _ do + send_resp(conn, 404, "oops") + end +end diff --git a/integration_test/temple_plug_demo/lib/temple_plug_demo/watcher.ex b/integration_test/temple_plug_demo/lib/temple_plug_demo/watcher.ex new file mode 100644 index 0000000..1f35bcf --- /dev/null +++ b/integration_test/temple_plug_demo/lib/temple_plug_demo/watcher.ex @@ -0,0 +1,62 @@ +# module mostly taken from `Phoenix.Endpoint.Watcher` + +defmodule TemplePlugDemo.Watcher do + @moduledoc false + require Logger + + def child_spec(args) do + %{ + id: make_ref(), + start: {__MODULE__, :start_link, [args]}, + restart: :transient + } + end + + def start_link({cmd, args}) do + Task.start_link(__MODULE__, :watch, [to_string(cmd), args]) + end + + def watch(_cmd, {mod, fun, args}) do + try do + apply(mod, fun, args) + catch + kind, reason -> + # The function returned a non-zero exit code. + # Sleep for a couple seconds before exiting to + # ensure this doesn't hit the supervisor's + # max_restarts/max_seconds limit. + Process.sleep(2000) + :erlang.raise(kind, reason, __STACKTRACE__) + end + end + + def watch(cmd, args) when is_list(args) do + {args, opts} = Enum.split_while(args, &is_binary(&1)) + opts = Keyword.merge([into: IO.stream(:stdio, :line), stderr_to_stdout: true], opts) + + try do + System.cmd(cmd, args, opts) + catch + :error, :enoent -> + relative = Path.relative_to_cwd(cmd) + + Logger.error( + "Could not start watcher #{inspect(relative)} from #{inspect(cd(opts))}, executable does not exist" + ) + + exit(:shutdown) + else + {_, 0} -> + :ok + + {_, _} -> + # System.cmd returned a non-zero exit code + # sleep for a couple seconds before exiting to ensure this doesn't + # hit the supervisor's max_restarts / max_seconds limit + Process.sleep(2000) + exit(:watcher_command_error) + end + end + + defp cd(opts), do: opts[:cd] || File.cwd!() +end diff --git a/integration_test/temple_plug_demo/mix.exs b/integration_test/temple_plug_demo/mix.exs new file mode 100644 index 0000000..be84fb2 --- /dev/null +++ b/integration_test/temple_plug_demo/mix.exs @@ -0,0 +1,33 @@ +defmodule TemplePlugDemo.MixProject do + use Mix.Project + + def project do + [ + app: :temple_plug_demo, + version: "0.1.0", + elixir: "~> 1.13", + compilers: [:temple] ++ Mix.compilers(), + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger], + mod: {TemplePlugDemo.Application, []} + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:bandit, "~> 0.4.9"}, + {:tailwind, "~> 0.1", runtime: Mix.env() == :dev}, + {:temple, path: "../../"} + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + ] + end +end diff --git a/integration_test/temple_plug_demo/mix.lock b/integration_test/temple_plug_demo/mix.lock new file mode 100644 index 0000000..ffd8a31 --- /dev/null +++ b/integration_test/temple_plug_demo/mix.lock @@ -0,0 +1,11 @@ +%{ + "bandit": {:hex, :bandit, "0.4.9", "8045b78a5087a51144a1a20bcbba879c989e659946e4ce27822726235b1d0d43", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.12", [hex: :plug, repo: "hexpm", optional: false]}, {:thousand_island, "~> 0.5.7", [hex: :thousand_island, repo: "hexpm", optional: false]}], "hexpm", "b04cba7e5530cc094d971c3664b0084c06e1f6d74518f4ddc92d0d00ebe6ca59"}, + "castore": {:hex, :castore, "0.1.16", "2675f717adc700475345c5512c381ef9273eb5df26bdd3f8c13e2636cf4cc175", [:mix], [], "hexpm", "28ed2c43d83b5c25d35c51bc0abf229ac51359c170cba76171a462ced2e4b651"}, + "hpax": {:hex, :hpax, "0.1.1", "2396c313683ada39e98c20a75a82911592b47e5c24391363343bde74f82396ca", [:mix], [], "hexpm", "0ae7d5a0b04a8a60caf7a39fcf3ec476f35cc2cc16c05abea730d3ce6ac6c826"}, + "mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"}, + "plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"}, + "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, + "tailwind": {:hex, :tailwind, "0.1.5", "5561bed6c114434415077972f6d291e7d43b258ef0ee756bda1ead7293811f61", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "3be21a0ddec7fc29b323ee72bed7516078a2787f7b142e455698a2209296e2a5"}, + "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, + "thousand_island": {:hex, :thousand_island, "0.5.7", "5066fb800287a9eb2699365746f51853ddc5ed0d4d39ea4c8b906f40b14a2499", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5d79e910a9d295e75ee4022df3a68173588dcc9a7e4dfcce4876325c8a9f51a7"}, +} diff --git a/integration_test/temple_plug_demo/priv/static/assets/app.css b/integration_test/temple_plug_demo/priv/static/assets/app.css new file mode 100644 index 0000000..71d3fba --- /dev/null +++ b/integration_test/temple_plug_demo/priv/static/assets/app.css @@ -0,0 +1,684 @@ +/* +! tailwindcss v3.0.24 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +*/ + +html { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input:-ms-input-placeholder, textarea:-ms-input-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* +Ensure the default browser behavior of the `hidden` attribute. +*/ + +[hidden] { + display: none; +} + +[type='text'],[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: #fff; + border-color: #6b7280; + border-width: 1px; + border-radius: 0px; + padding-top: 0.5rem; + padding-right: 0.75rem; + padding-bottom: 0.5rem; + padding-left: 0.75rem; + font-size: 1rem; + line-height: 1.5rem; + --tw-shadow: 0 0 #0000; +} + +[type='text']:focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: #2563eb; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + border-color: #2563eb; +} + +input::-moz-placeholder, textarea::-moz-placeholder { + color: #6b7280; + opacity: 1; +} + +input:-ms-input-placeholder, textarea:-ms-input-placeholder { + color: #6b7280; + opacity: 1; +} + +input::placeholder,textarea::placeholder { + color: #6b7280; + opacity: 1; +} + +::-webkit-datetime-edit-fields-wrapper { + padding: 0; +} + +::-webkit-date-and-time-value { + min-height: 1.5em; +} + +select { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); + background-position: right 0.5rem center; + background-repeat: no-repeat; + background-size: 1.5em 1.5em; + padding-right: 2.5rem; + -webkit-print-color-adjust: exact; + color-adjust: exact; +} + +[multiple] { + background-image: initial; + background-position: initial; + background-repeat: unset; + background-size: initial; + padding-right: 0.75rem; + -webkit-print-color-adjust: unset; + color-adjust: unset; +} + +[type='checkbox'],[type='radio'] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + padding: 0; + -webkit-print-color-adjust: exact; + color-adjust: exact; + display: inline-block; + vertical-align: middle; + background-origin: border-box; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + flex-shrink: 0; + height: 1rem; + width: 1rem; + color: #2563eb; + background-color: #fff; + border-color: #6b7280; + border-width: 1px; + --tw-shadow: 0 0 #0000; +} + +[type='checkbox'] { + border-radius: 0px; +} + +[type='radio'] { + border-radius: 100%; +} + +[type='checkbox']:focus,[type='radio']:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); + --tw-ring-offset-width: 2px; + --tw-ring-offset-color: #fff; + --tw-ring-color: #2563eb; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); +} + +[type='checkbox']:checked,[type='radio']:checked { + border-color: transparent; + background-color: currentColor; + background-size: 100% 100%; + background-position: center; + background-repeat: no-repeat; +} + +[type='checkbox']:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); +} + +[type='radio']:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); +} + +[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus { + border-color: transparent; + background-color: currentColor; +} + +[type='checkbox']:indeterminate { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e"); + border-color: transparent; + background-color: currentColor; + background-size: 100% 100%; + background-position: center; + background-repeat: no-repeat; +} + +[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus { + border-color: transparent; + background-color: currentColor; +} + +[type='file'] { + background: unset; + border-color: inherit; + border-width: 0; + border-radius: 0; + padding: 0; + font-size: unset; + line-height: inherit; +} + +[type='file']:focus { + outline: 1px auto -webkit-focus-ring-color; +} + +*, ::before, ::after { + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.container { + width: 100%; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + +.relative { + position: relative; +} + +.mx-auto { + margin-left: auto; + margin-right: auto; +} + +.font-sans { + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +} + + diff --git a/integration_test/temple_plug_demo/test/temple_plug_demo_test.exs b/integration_test/temple_plug_demo/test/temple_plug_demo_test.exs new file mode 100644 index 0000000..49d8073 --- /dev/null +++ b/integration_test/temple_plug_demo/test/temple_plug_demo_test.exs @@ -0,0 +1,8 @@ +defmodule TemplePlugDemoTest do + use ExUnit.Case + doctest TemplePlugDemo + + test "greets the world" do + assert TemplePlugDemo.hello() == :world + end +end diff --git a/integration_test/temple_plug_demo/test/test_helper.exs b/integration_test/temple_plug_demo/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/integration_test/temple_plug_demo/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/lib/mix/tasks/compile.temple.ex b/lib/mix/tasks/compile.temple.ex new file mode 100644 index 0000000..fe83a85 --- /dev/null +++ b/lib/mix/tasks/compile.temple.ex @@ -0,0 +1,15 @@ +defmodule Mix.Tasks.Compile.Temple do + use Mix.Task.Compiler + + @recursive true + + @impl Mix.Task.Compiler + def run(_) do + Code.put_compiler_option( + :parser_options, + Keyword.put(Code.get_compiler_option(:parser_options), :token_metadata, true) + ) + + {:ok, []} + end +end diff --git a/lib/mix/tasks/temple.gen.html.ex b/lib/mix/tasks/temple.gen.html.ex deleted file mode 100644 index 330de46..0000000 --- a/lib/mix/tasks/temple.gen.html.ex +++ /dev/null @@ -1,263 +0,0 @@ -if Code.ensure_loaded?(Mix.Phoenix) do - defmodule Mix.Tasks.Temple.Gen.Html do - @shortdoc "Generates controller, views, and context for an HTML resource in Temple" - - @moduledoc """ - Generates controller, views, and context for an HTML resource in Temple. - - mix temple.gen.html Accounts User users name:string age:integer - - The first argument is the context module followed by the schema module - and its plural name (used as the schema table name). - - The context is an Elixir module that serves as an API boundary for - the given resource. A context often holds many related resources. - Therefore, if the context already exists, it will be augmented with - functions for the given resource. - - > Note: A resource may also be split - > over distinct contexts (such as `Accounts.User` and `Payments.User`). - - The schema is responsible for mapping the database fields into an - Elixir struct. - - Overall, this generator will add the following files to `lib/`: - - * a context module in `lib/app/accounts.ex` for the accounts API - * a schema in `lib/app/accounts/user.ex`, with an `users` table - * a view in `lib/app_web/views/user_view.ex` - * a controller in `lib/app_web/controllers/user_controller.ex` - * default CRUD templates in `lib/app_web/templates/user` - - A migration file for the repository and test files for the context and - controller features will also be generated. - - The location of the web files (controllers, views, templates, etc) in an - umbrella application will vary based on the `:context_app` config located - in your applications `:generators` configuration. When set, the Phoenix - generators will generate web files directly in your lib and test folders - since the application is assumed to be isolated to web specific functionality. - If `:context_app` is not set, the generators will place web related lib - and test files in a `web/` directory since the application is assumed - to be handling both web and domain specific functionality. - Example configuration: - - config :my_app_web, :generators, context_app: :my_app - - Alternatively, the `--context-app` option may be supplied to the generator: - - mix phx.gen.html Sales User users --context-app warehouse - - ## Web namespace - - By default, the controller and view will be namespaced by the schema name. - You can customize the web module namespace by passing the `--web` flag with a - module name, for example: - - mix phx.gen.html.temple Sales User users --web Sales - - Which would generate a `lib/app_web/controllers/sales/user_controller.ex` and - `lib/app_web/views/sales/user_view.ex`. - - ## Generating without a schema or context file - - In some cases, you may wish to bootstrap HTML templates, controllers, and - controller tests, but leave internal implementation of the context or schema - to yourself. You can use the `--no-context` and `--no-schema` flags for - file generation control. - - ## table - - By default, the table name for the migration and schema will be - the plural name provided for the resource. To customize this value, - a `--table` option may be provided. For example: - - mix phx.gen.html.temple Accounts User users --table cms_users - - ## binary_id - - Generated migration can use `binary_id` for schema's primary key - and its references with option `--binary-id`. - - ## Default options - - This generator uses default options provided in the `:generators` - configuration of your application. These are the defaults: - - config :your_app, :generators, - migration: true, - binary_id: false, - sample_binary_id: "11111111-1111-1111-1111-111111111111" - - You can override those options per invocation by providing corresponding - switches, e.g. `--no-binary-id` to use normal ids despite the default - configuration or `--migration` to force generation of the migration. - - Read the documentation for `phx.gen.schema` for more information on - attributes. - """ - use Mix.Task - - alias Mix.Phoenix.{Context, Schema} - alias Mix.Tasks.Phx.Gen - - @doc false - def run(args) do - if Mix.Project.umbrella?() do - Mix.raise("mix temple.gen.html can only be run inside an application directory") - end - - {context, schema} = Gen.Context.build(args) - Gen.Context.prompt_for_code_injection(context) - - binding = [context: context, schema: schema, inputs: inputs(schema)] - paths = [".", :temple] - - prompt_for_conflicts(context) - - context - |> copy_new_files(paths, binding) - |> print_shell_instructions() - end - - defp prompt_for_conflicts(context) do - context - |> files_to_be_generated() - |> Kernel.++(context_files(context)) - |> Mix.Phoenix.prompt_for_conflicts() - end - - defp context_files(%Context{generate?: true} = context) do - Gen.Context.files_to_be_generated(context) - end - - defp context_files(%Context{generate?: false}) do - [] - end - - @doc false - def files_to_be_generated(%Context{schema: schema, context_app: context_app}) do - web_prefix = Mix.Phoenix.web_path(context_app) - test_prefix = Mix.Phoenix.web_test_path(context_app) - web_path = to_string(schema.web_path) - - [ - {:eex, "controller.ex", - Path.join([web_prefix, "controllers", web_path, "#{schema.singular}_controller.ex"])}, - {:eex, "edit.html.exs", - Path.join([web_prefix, "templates", web_path, schema.singular, "edit.html.exs"])}, - {:eex, "form.html.exs", - Path.join([web_prefix, "templates", web_path, schema.singular, "form.html.exs"])}, - {:eex, "index.html.exs", - Path.join([web_prefix, "templates", web_path, schema.singular, "index.html.exs"])}, - {:eex, "new.html.exs", - Path.join([web_prefix, "templates", web_path, schema.singular, "new.html.exs"])}, - {:eex, "show.html.exs", - Path.join([web_prefix, "templates", web_path, schema.singular, "show.html.exs"])}, - {:eex, "view.ex", - Path.join([web_prefix, "views", web_path, "#{schema.singular}_view.ex"])}, - {:eex, "controller_test.exs", - Path.join([ - test_prefix, - "controllers", - web_path, - "#{schema.singular}_controller_test.exs" - ])} - ] - end - - @doc false - def copy_new_files(%Context{} = context, paths, binding) do - files = files_to_be_generated(context) - - Mix.Phoenix.copy_from(paths, "priv/templates/temple.gen.html", binding, files) - - if context.generate?, - do: Gen.Context.copy_new_files(context, Mix.Phoenix.generator_paths(), binding) - - context - end - - @doc false - def print_shell_instructions(%Context{schema: schema, context_app: ctx_app} = context) do - if schema.web_namespace do - Mix.shell().info(""" - - Add the resource to your #{schema.web_namespace} :browser scope in #{ - Mix.Phoenix.web_path(ctx_app) - }/router.ex: - - scope "/#{schema.web_path}", #{ - inspect(Module.concat(context.web_module, schema.web_namespace)) - }, as: :#{schema.web_path} do - pipe_through :browser - ... - resources "/#{schema.plural}", #{inspect(schema.alias)}Controller - end - """) - else - Mix.shell().info(""" - - Add the resource to your browser scope in #{Mix.Phoenix.web_path(ctx_app)}/router.ex: - - resources "/#{schema.plural}", #{inspect(schema.alias)}Controller - """) - end - - if context.generate?, do: Gen.Context.print_shell_instructions(context) - end - - def inputs(%Schema{} = schema) do - Enum.map(schema.attrs, fn - {_, {:references, _}} -> - {nil, nil, nil} - - {key, :integer} -> - {label(key), ~s(number_input f, #{inspect(key)}), error(key)} - - {key, :float} -> - {label(key), ~s(number_input f, #{inspect(key)}, step: "any"), error(key)} - - {key, :decimal} -> - {label(key), ~s(number_input f, #{inspect(key)}, step: "any"), error(key)} - - {key, :boolean} -> - {label(key), ~s(checkbox f, #{inspect(key)}), error(key)} - - {key, :text} -> - {label(key), ~s(textarea f, #{inspect(key)}), error(key)} - - {key, :date} -> - {label(key), ~s(date_select f, #{inspect(key)}), error(key)} - - {key, :time} -> - {label(key), ~s(time_select f, #{inspect(key)}), error(key)} - - {key, :utc_datetime} -> - {label(key), ~s(datetime_select f, #{inspect(key)}), error(key)} - - {key, :naive_datetime} -> - {label(key), ~s(datetime_select f, #{inspect(key)}), error(key)} - - {key, {:array, :integer}} -> - {label(key), ~s(multiple_select f, #{inspect(key)}, ["1": 1, "2": 2]), error(key)} - - {key, {:array, _}} -> - {label(key), - ~s(multiple_select f, #{inspect(key)}, ["Option 1": "option1", "Option 2": "option2"]), - error(key)} - - {key, _} -> - {label(key), ~s(text_input f, #{inspect(key)}), error(key)} - end) - end - - defp label(key) do - ~s(label f, #{inspect(key)}) - end - - defp error(field) do - ~s{error_tag(f, #{inspect(field)})} - end - end -end diff --git a/lib/mix/tasks/temple.gen.layout.ex b/lib/mix/tasks/temple.gen.layout.ex deleted file mode 100644 index 4c4a0a6..0000000 --- a/lib/mix/tasks/temple.gen.layout.ex +++ /dev/null @@ -1,31 +0,0 @@ -if Code.ensure_loaded?(Mix.Phoenix) do - defmodule Mix.Tasks.Temple.Gen.Layout do - use Mix.Task - - @shortdoc "Generates a default Phoenix layout file in Temple" - - @moduledoc """ - Generates a Phoenix layout file in Temple. - mix temple.gen.layout - """ - def run(_args) do - context_app = Mix.Phoenix.context_app() - web_prefix = Mix.Phoenix.web_path(context_app) - binding = [application_module: Mix.Phoenix.base()] - - Mix.Phoenix.copy_from(temple_paths(), "priv/templates/temple.gen.layout", binding, [ - {:eex, "app.html.eex", "#{web_prefix}/templates/layout/app.html.exs"} - ]) - - instructions = """ - A new #{web_prefix}/templates/layout/app.html.exs file was generated. - """ - - Mix.shell().info(instructions) - end - - defp temple_paths do - [".", :temple] - end - end -end diff --git a/lib/mix/tasks/temple.gen.live.ex b/lib/mix/tasks/temple.gen.live.ex deleted file mode 100644 index c68cb46..0000000 --- a/lib/mix/tasks/temple.gen.live.ex +++ /dev/null @@ -1,254 +0,0 @@ -if Code.ensure_loaded?(Mix.Phoenix) do - defmodule Mix.Tasks.Temple.Gen.Live do - @shortdoc "Generates LiveView, templates, and context for a resource" - - @moduledoc """ - Generates LiveView, templates, and context for a resource. - - mix temple.gen.live Accounts User users name:string age:integer - - The first argument is the context module followed by the schema module - and its plural name (used as the schema table name). - - The context is an Elixir module that serves as an API boundary for - the given resource. A context often holds many related resources. - Therefore, if the context already exists, it will be augmented with - functions for the given resource. - - When this command is run for the first time, a `ModalComponent` and - `LiveHelpers` module will be created, along with the resource level - LiveViews and components, including an `IndexLive`, `ShowLive`, `FormComponent` - for the new resource. - - > Note: A resource may also be split - > over distinct contexts (such as `Accounts.User` and `Payments.User`). - - The schema is responsible for mapping the database fields into an - Elixir struct. It is followed by an optional list of attributes, - with their respective names and types. See `mix phx.gen.schema` - for more information on attributes. - - Overall, this generator will add the following files to `lib/`: - - * a context module in `lib/app/accounts.ex` for the accounts API - * a schema in `lib/app/accounts/user.ex`, with an `users` table - * a view in `lib/app_web/views/user_view.ex` - * a LiveView in `lib/app_web/live/user_live/show_live.ex` - * a LiveView in `lib/app_web/live/user_live/index_live.ex` - * a LiveComponent in `lib/app_web/live/user_live/form_component.ex` - * a LiveComponent in `lib/app_web/live/modal_component.ex` - * a helpers modules in `lib/app_web/live/live_helpers.ex` - - ## The context app - - A migration file for the repository and test files for the context and - controller features will also be generated. - - The location of the web files (LiveView's, views, templates, etc) in an - umbrella application will vary based on the `:context_app` config located - in your applications `:generators` configuration. When set, the Phoenix - generators will generate web files directly in your lib and test folders - since the application is assumed to be isolated to web specific functionality. - If `:context_app` is not set, the generators will place web related lib - and test files in a `web/` directory since the application is assumed - to be handling both web and domain specific functionality. - Example configuration: - - config :my_app_web, :generators, context_app: :my_app - - Alternatively, the `--context-app` option may be supplied to the generator: - - mix temple.gen.live Sales User users --context-app warehouse - - ## Web namespace - - By default, the controller and view will be namespaced by the schema name. - You can customize the web module namespace by passing the `--web` flag with a - module name, for example: - - mix temple.gen.live Sales User users --web Sales - - Which would generate a LiveViews inside `lib/app_web/live/sales/user_live/` and a - view at `lib/app_web/views/sales/user_view.ex`. - - ## Customising the context, schema, tables and migrations - - In some cases, you may wish to bootstrap HTML templates, LiveViews, - and tests, but leave internal implementation of the context or schema - to yourself. You can use the `--no-context` and `--no-schema` flags - for file generation control. - - You can also change the table name or configure the migrations to - use binary ids for primary keys, see `mix phx.gen.schema` for more - information. - """ - use Mix.Task - - alias Mix.Phoenix.{Context} - alias Mix.Tasks.Phx.Gen - - @doc false - def run(args) do - if Mix.Project.umbrella?() do - Mix.raise("mix temple.gen.live can only be run inside an application directory") - end - - {context, schema} = Gen.Context.build(args) - Gen.Context.prompt_for_code_injection(context) - - binding = [ - context: context, - schema: schema, - inputs: Mix.Tasks.Temple.Gen.Html.inputs(schema) - ] - - paths = [".", :temple] - - prompt_for_conflicts(context) - - context - |> copy_new_files(binding, paths) - |> maybe_inject_helpers() - |> print_shell_instructions() - end - - defp prompt_for_conflicts(context) do - context - |> files_to_be_generated() - |> Kernel.++(context_files(context)) - |> Mix.Phoenix.prompt_for_conflicts() - end - - defp context_files(%Context{generate?: true} = context) do - Gen.Context.files_to_be_generated(context) - end - - defp context_files(%Context{generate?: false}) do - [] - end - - defp files_to_be_generated(%Context{schema: schema, context_app: context_app}) do - web_prefix = Mix.Phoenix.web_path(context_app) - test_prefix = Mix.Phoenix.web_test_path(context_app) - web_path = to_string(schema.web_path) - live_subdir = "#{schema.singular}_live" - - [ - {:eex, "show.ex", Path.join([web_prefix, "live", web_path, live_subdir, "show.ex"])}, - {:eex, "index.ex", Path.join([web_prefix, "live", web_path, live_subdir, "index.ex"])}, - {:eex, "form_component.ex", - Path.join([web_prefix, "live", web_path, live_subdir, "form_component.ex"])}, - {:eex, "form_component.html.lexs", - Path.join([web_prefix, "live", web_path, live_subdir, "form_component.html.lexs"])}, - {:eex, "index.html.lexs", - Path.join([web_prefix, "live", web_path, live_subdir, "index.html.lexs"])}, - {:eex, "show.html.lexs", - Path.join([web_prefix, "live", web_path, live_subdir, "show.html.lexs"])}, - {:eex, "live_test.exs", - Path.join([test_prefix, "live", web_path, "#{schema.singular}_live_test.exs"])}, - {:new_eex, "modal_component.ex", Path.join([web_prefix, "live", "modal_component.ex"])}, - {:new_eex, "live_helpers.ex", Path.join([web_prefix, "live", "live_helpers.ex"])} - ] - end - - defp copy_new_files(%Context{} = context, binding, paths) do - files = files_to_be_generated(context) - - Mix.Phoenix.copy_from( - paths, - "priv/templates/temple.gen.live", - binding, - files - ) - - if context.generate?, - do: Gen.Context.copy_new_files(context, Mix.Phoenix.generator_paths(), binding) - - context - end - - defp maybe_inject_helpers(%Context{context_app: ctx_app} = context) do - web_prefix = Mix.Phoenix.web_path(ctx_app) - [lib_prefix, web_dir] = Path.split(web_prefix) - file_path = Path.join(lib_prefix, "#{web_dir}.ex") - file = File.read!(file_path) - inject = "import #{inspect(context.web_module)}.LiveHelpers" - - if String.contains?(file, inject) do - :ok - else - do_inject_helpers(context, file, file_path, inject) - end - - context - end - - defp do_inject_helpers(context, file, file_path, inject) do - Mix.shell().info([:green, "* injecting ", :reset, Path.relative_to_cwd(file_path)]) - - new_file = - String.replace( - file, - "import Phoenix.LiveView.Helpers", - "import Phoenix.LiveView.Helpers\n #{inject}" - ) - - if file != new_file do - File.write!(file_path, new_file) - else - Mix.shell().info(""" - - Could not find Phoenix.LiveView.Helpers imported in #{file_path}. - - This typically happens because your application was not generated - with the --live flag: - - mix temple.new my_app --live - - Please make sure LiveView is installed and that #{inspect(context.web_module)} - defines both `live_view/0` and `live_component/0` functions, - and that both functions import #{inspect(context.web_module)}.LiveHelpers. - """) - end - end - - @doc false - def print_shell_instructions(%Context{schema: schema, context_app: ctx_app} = context) do - prefix = Module.concat(context.web_module, schema.web_namespace) - web_path = Mix.Phoenix.web_path(ctx_app) - - if schema.web_namespace do - Mix.shell().info(""" - - Add the live routes to your #{schema.web_namespace} :browser scope in #{web_path}/router.ex: - - scope "/#{schema.web_path}", #{inspect(prefix)}, as: :#{schema.web_path} do - pipe_through :browser - ... - - #{for line <- live_route_instructions(schema), do: " #{line}"} - end - """) - else - Mix.shell().info(""" - - Add the live routes to your browser scope in #{Mix.Phoenix.web_path(ctx_app)}/router.ex: - - #{for line <- live_route_instructions(schema), do: " #{line}"} - """) - end - - if context.generate?, do: Gen.Context.print_shell_instructions(context) - end - - defp live_route_instructions(schema) do - [ - ~s|live "/#{schema.plural}", #{inspect(schema.alias)}Live.Index, :index\n|, - ~s|live "/#{schema.plural}/new", #{inspect(schema.alias)}Live.Index, :new\n|, - ~s|live "/#{schema.plural}/:id/edit", #{inspect(schema.alias)}Live.Index, :edit\n\n|, - ~s|live "/#{schema.plural}/:id", #{inspect(schema.alias)}Live.Show, :show\n|, - ~s|live "/#{schema.plural}/:id/show/edit", #{inspect(schema.alias)}Live.Show, :edit| - ] - end - end -end diff --git a/lib/temple.ex b/lib/temple.ex index d6b3399..0b8cd1f 100644 --- a/lib/temple.ex +++ b/lib/temple.ex @@ -1,120 +1,67 @@ defmodule Temple do - alias Temple.Parser + @engine Application.compile_env(:temple, :engine, EEx.SmartEngine) @moduledoc """ - Temple syntax is available inside the `temple`, and is compiled into EEx at build time. + Temple syntax is available inside the `temple`, and is compiled into efficient Elixir code at compile time using the configured `EEx.Engine`. + + You should checkout the [guides](https://hexdocs.pm/temple/your-first-template.html) for a more in depth explanation. ## Usage ```elixir - temple do - # You can define attributes by passing a keyword list to the element, the values can be literals or variables. - class = "text-blue" - id = "jumbotron" + defmodule MyApp.HomePage do + import Temple - div class: class, id: id do - # Text nodes can be emitted as string literals or variables. - "Bob" + def render() do + assigns = %{title: "My Site | Sign Up", logged_in: false} - id - end + temple do + "" - # Attributes that result in boolean values will be emitted as a boolean attribute. Examples of boolean attributes are `disabled` and `checked`. + html do + head do + meta charset: "utf-8" + meta http_equiv: "X-UA-Compatible", content: "IE=edge" + meta name: "viewport", content: "width=device-width, initial-scale=1.0" + link rel: "stylesheet", href: "/css/app.css" - input type: "text", disabled: true - # + title do: @title + end - input type: "text", disabled: false - # + body do + header class: "header" do + ul do + li do + a href: "/", do: "Home" + end + li do + if @logged_in do + a href: "/logout", do: "Logout" + else + a href: "/login", do: "Login" + end + end + end + end - # The class attribute also can take a keyword list of classes to conditionally render, based on the boolean result of the value. - - div class: ["text-red-500": false, "text-green-500": true] do - "Alert!" - end - - #
Alert!
- - # if and unless expressions can be used to conditionally render content - if 5 > 0 do - p do - "Greater than 0!" + main do + "Hi! Welcome to my website." + end + end + end end end - - unless 5 > 0 do - p do - "Less than 0!" - end - end - - # You can loop over items using for comprehensions - for x <- 0..5 do - div do - x - end - end - - # You can use multiline anonymous functions, like if you're building a form in Phoenix - form_for @changeset, Routes.user_path(@conn, :create), fn f -> - "Name: " - text_input f, :name - end - - # You can explicitly emit a tag by prefixing with the Temple module - Temple.div do - "Foo" - end - - # You can also pass children as a do key instead of a block - div do: "Alice", class: "text-yellow" end ``` - ## Whitespace Control - - By default, Temple will emit internal whitespace into tags, something like this. - - ```elixir - span do - "Hello, world!" - end - ``` - - ```html - - Hello, world! - - ``` - - If you need to create a "tight" tag, you can call the "bang" version of the desired tag. - - ```elixir - span! do - "Hello, world!" - end - ``` - - ```html - Hello, world! - ``` - ## Configuration - ### Mode + ### Engine - There are two "modes", `:normal` (the default) and `:live_view`. - - In `:live_view` mode, Temple emits markup that uses functions provided by Phoenix LiveView in order to be fully "diff trackable". These LiveView functions have not been released yet, so if you are going to combine Temple with LiveView, you need to use the latest unreleased default branch from GitHub. - - You should use `:live_view` mode even if you only have a single LiveView. + By default Temple wil use the `EEx.SmartEngine`, but you can configure it to use any other engine. Examples could be `Phoenix.HTML.Engine` or `Phoenix.LiveView.Engine`. ```elixir - config :temple, :mode, :normal # default - - # or - - config :temple, :mode, :live_view + config :temple, engine: Phoenix.HTML.Engine ``` ### Aliases @@ -123,16 +70,16 @@ defmodule Temple do ```elixir config :temple, :aliases, - label: :_label, - link: :_link, - select: :_select + label: :label_tag, + link: :link_tag, + select: :select_tag temple do - _label do + label_tag do "Email" end - _link href: "/css/site.css" + link_tag href: "/css/site.css" end ``` @@ -147,83 +94,20 @@ defmodule Temple do ``` """ - defmacro __using__(_) do - quote location: :keep do - import Temple - require Temple.Component - end - end - - @doc """ - Context for temple markup. - - Returns an EEx string that can be passed into an EEx template engine. - - ## Usage - - ```elixir - import Temple - - temple do - div class: @class do - "Hello, world!" - end - end - - #
- # Hello, world! - #
- ``` - """ - defmacro temple([do: block] = _block) do - markup = - block - |> Parser.parse() - |> Enum.map(fn parsed -> Temple.Generator.to_eex(parsed, 0) end) - |> Enum.intersperse("\n") - |> :erlang.iolist_to_binary() - - quote location: :keep do - unquote(markup) - end - end - defmacro temple(block) do - quote location: :keep do - unquote(block) - |> Parser.parse() - |> Enum.map(fn parsed -> Temple.Generator.to_eex(parsed, 0) end) - |> Enum.intersperse("\n") - |> :erlang.iolist_to_binary() + opts = [engine: engine()] + + quote do + require Temple.Renderer + Temple.Renderer.compile(unquote(opts), unquote(block)) end end - @doc """ - Compiles temple markup into a quoted expression using the given EEx Engine. - - Returns the same output that Phoenix templates output into the `render/1` function of their view modules. - - ## Usage - - ```elixir - require Temple - - Temple.compile Phoenix.HTML.Engine do - div class: @class do - "Hello, world!" - end + @doc false + def component(func, assigns) do + apply(func, [assigns]) end - ``` - """ - defmacro compile(engine, [do: block] = _block) do - markup = - block - |> Parser.parse() - |> Enum.map(fn parsed -> Temple.Generator.to_eex(parsed, 0) end) - |> Enum.intersperse("\n") - |> :erlang.iolist_to_binary() - - EEx.compile_string(markup, engine: engine, line: __CALLER__.line, file: __CALLER__.file) - end + @doc false + def engine(), do: @engine end diff --git a/lib/temple/component.ex b/lib/temple/component.ex deleted file mode 100644 index 6cf0a9e..0000000 --- a/lib/temple/component.ex +++ /dev/null @@ -1,274 +0,0 @@ -defmodule Temple.Component do - @moduledoc """ - API for defining components. - - Component modules are basically normal Phoenix View modules. The contents of the `render` macro are compiled into a `render/2` function. This means that you can define functions in your component module and use them in your component markup. - - Since component modules are view modules, the assigns you pass to the component are accessible via the `@` macro and the `assigns` variable. - - You must `require Temple.Component` in your views that use components, as the `c` and `slot` generate markup that uses macros provided by Temple. - - ## Components - - ```elixir - defmodule MyAppWeb.Components.Flash do - import Temple.Component - - def border_class(:info), do: "border-blue-500" - def border_class(:warning), do: "border-yellow-500" - def border_class(:error), do: "border-red-500" - def border_class(:success), do: "border-green-500" - - render do - div class: "border rounded p-2 #\{assigns[:class]} #\{border_class(@message_type)}" do - slot :default - end - end - end - ``` - - Components are used by calling the `c` keyword, followed by the component module and any assigns you need to pass to the template. - - `c` is a _**compile time keyword**_, not a function or a macro, so you won't see it in the generated documention. - - ``` - c MyAppWeb.Components.Flash, class: "font-bold", message_type: :info do - ul do - for info <- infos do - li class: "p-4" do - info.message - end - end - end - end - ``` - - Since components are just modules, if you alias your module, you can use them more ergonomically. - - ``` - # lib/my_app_web/views/page_view.ex - alias MyAppWeb.Components.Flex - - # lib/my_app_web/templates/page/index.html.exs - c Flex, class: "justify-between items center" do - for item <- items do - div class: "p-4" do - item.name - end - end - end - ``` - - ## Slots - - Components can use slots, which are named placeholders for markup that can be passed to the component by the caller. - - Slots are invoked by using the `slot` keyword, followed by the name of the slot and any assigns you'd like to pass into the slot. - - `slot` is a _**compile time keyword**_, not a function or a macro, so you won't see it in the generated documention. - - ```elixir - defmodule Flex do - import Temple.Component - - render do - div class: "flex #\{@class}" do - slot :default - end - end - end - ``` - - You can also use "named slots", which allow for data to be passed back into them. This is very useful - when a component needs to pass data from the inside of the component back to the caller, like when rendering a form in LiveView. - - ```elixir - defmodule Form do - import Temple.Component - - render do - form = form_for(@changeset, @action, assigns) - - form - - slot :f, form: form - - "" - end - end - ``` - - By default, the body of a component fills the `:default` slot. - - Named slots can be defined by invoking the `slot` keyword with the name of the slot and a do block. - - You can also pattern match on any assigns that are being passed into the slot as if you were defining an anonymous function. - - `slot` is a _**compile time keyword**_, not a function or a macro, so you won't see it in the generated documention. - - ```elixir - # lib/my_app_web/templates/post/new.html.lexs - - c Form, changeset: @changeset, - action: @action, - class: "form-control", - phx_submit: :save, - phx_change: :validate do - slot :f, %{form: f} do - label f do - "Widget Name" - text_input f, :name, class: "text-input" - end - - submit "Save!" - end - end - ``` - """ - - @doc false - defmacro __component__(module, assigns \\ [], block \\ []) do - {inner_block, assigns} = - case {block, assigns} do - {[do: do_block], _} -> {rewrite_do(do_block), assigns} - {_, [do: do_block]} -> {rewrite_do(do_block), []} - {_, _} -> {nil, assigns} - end - - if is_nil(inner_block) do - quote do - Phoenix.View.render(unquote(module), :self, unquote(assigns)) - end - else - quote do - Phoenix.View.render( - unquote(module), - :self, - Map.put(Map.new(unquote(assigns)), :inner_block, unquote(inner_block)) - ) - end - end - end - - @doc false - defmacro __render_block__(inner_block, argument \\ []) do - quote do - unquote(inner_block).(unquote(argument)) - end - end - - defp rewrite_do([{:->, meta, _} | _] = do_block) do - {:fn, meta, do_block} - end - - defp rewrite_do(do_block) do - quote do - fn _ -> - unquote(do_block) - end - end - end - - @doc """ - Defines a component template. - - ## Usage - - ```elixir - defmodule MyAppWeb.Components.Flash do - import Temple.Component - - def border_class(:info), do: "border-blue-500" - def border_class(:warning), do: "border-yellow-500" - def border_class(:error), do: "border-red-500" - def border_class(:success), do: "border-green-500" - - render do - div class: "border rounded p-2 #\{assigns[:class]} #\{border_class(@message_type)}" do - slot :default - end - end - end - ``` - - """ - defmacro render(block) do - quote do - def render(var!(assigns)) do - require Temple - - _ = var!(assigns) - - Temple.compile(unquote(Temple.Component.__engine__()), unquote(block)) - end - - def render(:self, var!(assigns)) do - require Temple - - _ = var!(assigns) - - Temple.compile(unquote(Temple.Component.__engine__()), unquote(block)) - end - end - end - - @doc """ - Defines a component module. - - This macro makes it easy to define components without creating a separate file. It literally inlines a component module. - - Since it defines a module inside of the current module, local function calls from the outer module won't be available. For convenience, the outer module is aliased for you, so you can call remote functions with a shorter module name. - - ## Usage - - ```elixir - def MyAppWeb.SomeView do - use MyAppWeb.SomeView, :view - import Temple.Component, only: [defcomp: 2] - - # define a function in outer module - def foobar(), do: "foobar" - - # define a component - defcomp Button do - button id: SomeView.foobar(), # `MyAppWeb.SomeView` is aliased for you. - class: "text-sm px-3 py-2 rounded #\{assigns[:extra_classes]}", - type: "submit" do - slot :default - end - end - end - - # use the component in a SomeView template. Or else, you must alias `MyAppWeb.SomeView.Button` - c Button, extra_classes: "border-2 border-red-500" do - "Submit!" - end - ``` - """ - defmacro defcomp(module, [do: block] = _block) do - quote location: :keep do - defmodule unquote(module) do - import Temple.Component - alias unquote(__CALLER__.module) - - render do - unquote(block) - end - end - end - end - - @doc false - def __engine__() do - cond do - Code.ensure_loaded?(Phoenix.LiveView.Engine) -> - Phoenix.LiveView.Engine - - Code.ensure_loaded?(Phoenix.HTML.Engine) -> - Phoenix.HTML.Engine - - true -> - nil - end - end -end diff --git a/lib/temple/config.ex b/lib/temple/config.ex deleted file mode 100644 index b34aa53..0000000 --- a/lib/temple/config.ex +++ /dev/null @@ -1,21 +0,0 @@ -defmodule Temple.Config do - @moduledoc false - - def mode do - case Application.get_env(:temple, :mode, :normal) do - :normal -> - %{ - component_function: "Temple.Component.__component__", - render_block_function: "Temple.Component.__render_block__", - renderer: fn module -> Macro.to_string(module) end - } - - :live_view -> - %{ - component_function: "component", - render_block_function: "render_block", - renderer: fn module -> "&" <> Macro.to_string(module) <> ".render/1" end - } - end - end -end diff --git a/lib/temple/engine.ex b/lib/temple/engine.ex deleted file mode 100644 index 3f8dafe..0000000 --- a/lib/temple/engine.ex +++ /dev/null @@ -1,44 +0,0 @@ -defmodule Temple.Engine do - @behaviour Phoenix.Template.Engine - - @moduledoc """ - The Temple HTML engine makes it possible to use Temple with Phoenix controllers. - - To get started, you will configure Phoenix to use this module for `.exs` files. - - ```elixir - # config.exs - config :phoenix, :template_engines, - # this will work for files named like `index.html.exs` - exs: Temple.Engine - - # config/dev.exs - config :your_app, YourAppWeb.Endpoint, - live_reload: [ - patterns: [ - ~r"lib/myapp_web/(live|views)/.*(ex|exs|lexs)$", - ~r"lib/myapp_web/templates/.*(eex|exs|lexs)$" - ] - ] - - # my_app/ - # lib/ - # my_app/ - # my_app_web/ - # templates/ - # posts/ - # show.html.exs - ``` - - Now you can get started by writing `exs` files in the templates directory and they will be compiled as you would expect. - """ - - def compile(path, _name) do - require Temple - - template = path |> File.read!() |> Code.string_to_quoted!(file: path) - - Temple.temple(template) - |> EEx.compile_string(engine: Phoenix.HTML.Engine, file: path, line: 1) - end -end diff --git a/lib/temple/generator.ex b/lib/temple/generator.ex deleted file mode 100644 index c0f7d46..0000000 --- a/lib/temple/generator.ex +++ /dev/null @@ -1,5 +0,0 @@ -defprotocol Temple.Generator do - @moduledoc false - - def to_eex(ast, indent \\ 0) -end diff --git a/lib/temple/live_view_engine.ex b/lib/temple/live_view_engine.ex deleted file mode 100644 index 97e78f2..0000000 --- a/lib/temple/live_view_engine.ex +++ /dev/null @@ -1,46 +0,0 @@ -defmodule Temple.LiveViewEngine do - @behaviour Phoenix.Template.Engine - - @moduledoc """ - The Temple LiveView engine makes it possible to use Temple with Phoenix LiveView. - - To get started, you will configure Phoenix to use this module for `.lexs` files. - - ```elixir - # config.exs - config :phoenix, :template_engines, - # this will work for files named like `index.html.lexs` - # you can enable Elixir syntax highlighting in your editor for this extension - lexs: Temple.LiveViewEngine - - # config/dev.exs - config :your_app, YourAppWeb.Endpoint, - live_reload: [ - patterns: [ - ~r"lib/myapp_web/(live|views)/.*(ex|exs|lexs)$", - ~r"lib/myapp_web/templates/.*(eex|exs|lexs)$" - ] - ] - - # my_app/ - # lib/ - # my_app/ - # my_app_web/ - # live/ - # posts_live/ - # show.ex - # show.html.lexs - ``` - - Now you can get started by writing `lexs` files co-located with your live views and they will be compiled as you would expect. - """ - - def compile(path, _name) do - require Temple - - template = path |> File.read!() |> Code.string_to_quoted!(file: path) - - Temple.temple(template) - |> EEx.compile_string(engine: Phoenix.LiveView.Engine, file: path, line: 1) - end -end diff --git a/lib/temple/parser.ex b/lib/temple/parser.ex index 1ddbf05..7ef078a 100644 --- a/lib/temple/parser.ex +++ b/lib/temple/parser.ex @@ -36,7 +36,7 @@ defmodule Temple.Parser do @doc """ Processes the given AST, adding the markup to the given buffer. - Should return `:ok` if the parsing pass is over, or `{:component_applied, ast}` if the pass should be restarted. + Should return Temple.AST. """ @callback run(ast :: Macro.t()) :: ast() @@ -58,7 +58,7 @@ defmodule Temple.Parser do option textarea output progress meter details summary menuitem menu html - ]a |> Enum.flat_map(fn el -> [el, :"#{el}!"] end) + ]a @nonvoid_elements_aliases Enum.map(@nonvoid_elements, fn el -> Keyword.get(@aliases, el, el) @@ -125,7 +125,7 @@ defmodule Temple.Parser do {_, false} <- {DoExpressions, DoExpressions.applicable?(ast)}, {_, false} <- {Match, Match.applicable?(ast)}, {_, false} <- {Default, Default.applicable?(ast)} do - raise "No parsers applicable!!" + raise "No parsers applicable!" else {parser, true} -> ast diff --git a/lib/temple/parser/anonymous_functions.ex b/lib/temple/parser/anonymous_functions.ex index dd11ff7..f0a41db 100644 --- a/lib/temple/parser/anonymous_functions.ex +++ b/lib/temple/parser/anonymous_functions.ex @@ -30,39 +30,4 @@ defmodule Temple.Parser.AnonymousFunctions do Temple.Ast.new(__MODULE__, elixir_ast: expression, children: children) end - - defimpl Temple.Generator do - def to_eex(%{elixir_ast: {name, _, args}, children: children}, indent \\ 0) do - {_do_and_else, args} = Temple.Parser.Utils.split_args(args) - - {args, {func, _, [{arrow, _, [[{arg, _, _}], _block]}]}, args2} = - Temple.Parser.Utils.split_on_fn(args, {[], nil, []}) - - [ - "#{Parser.Utils.indent(indent)}<%= ", - to_string(name), - " ", - Enum.map(args, &Macro.to_string(&1)) |> Enum.join(", "), - ", ", - to_string(func), - " ", - to_string(arg), - " ", - to_string(arrow), - " %>", - "\n", - for(child <- children, do: Temple.Generator.to_eex(child, indent + 1)), - if Enum.any?(args2) do - [ - "#{Parser.Utils.indent(indent)}<% end, ", - Enum.map(args2, fn arg -> Macro.to_string(arg) end) - |> Enum.join(", "), - " %>" - ] - else - ["#{Parser.Utils.indent(indent)}<% end %>", "\n"] - end - ] - end - end end diff --git a/lib/temple/parser/components.ex b/lib/temple/parser/components.ex index b9913bb..81a0fc0 100644 --- a/lib/temple/parser/components.ex +++ b/lib/temple/parser/components.ex @@ -2,9 +2,7 @@ defmodule Temple.Parser.Components do @moduledoc false @behaviour Temple.Parser - alias Temple.Parser - - defstruct module: nil, assigns: [], children: [], slots: [] + defstruct function: nil, assigns: [], children: [], slots: [] @impl Temple.Parser def applicable?({:c, _, _}) do @@ -14,7 +12,7 @@ defmodule Temple.Parser.Components do def applicable?(_), do: false @impl Temple.Parser - def run({:c, _meta, [component_module | args]}) do + def run({:c, _meta, [component_function | args]}) do {do_and_else, args} = args |> Temple.Parser.Utils.split_args() @@ -25,19 +23,19 @@ defmodule Temple.Parser.Components do if children = do_and_else[:do] do Macro.prewalk( children, - {component_module, %{}}, + {component_function, %{}}, fn {:c, _, [name | _]} = node, {_, named_slots} -> {node, {name, named_slots}} - {:slot, _, [name | args]} = node, {^component_module, named_slots} -> + {:slot, _, [name | args]} = node, {^component_function, named_slots} -> {assigns, slot} = split_assigns_and_children(args, Macro.escape(%{})) if is_nil(slot) do - {node, {component_module, named_slots}} + {node, {component_function, named_slots}} else {nil, - {component_module, Map.put(named_slots, name, %{assigns: assigns, slot: slot})}} + {component_function, Map.put(named_slots, name, %{assigns: assigns, slot: slot})}} end node, acc -> @@ -66,7 +64,7 @@ defmodule Temple.Parser.Components do end Temple.Ast.new(__MODULE__, - module: Macro.expand_once(component_module, __ENV__), + function: component_function, assigns: assigns, slots: slots, children: children @@ -88,43 +86,4 @@ defmodule Temple.Parser.Components do {empty, nil} end end - - defimpl Temple.Generator do - def to_eex(%{module: module, assigns: assigns, children: children, slots: slots}, indent \\ 0) do - component_function = Temple.Config.mode().component_function - renderer = Temple.Config.mode().renderer.(module) - - [ - "#{Parser.Utils.indent(indent)}<%= #{component_function} ", - renderer, - ", ", - Macro.to_string(assigns), - if not Enum.empty?(children ++ slots) do - [ - " do %>\n", - if not Enum.empty?(children) do - [ - "#{Parser.Utils.indent(indent + 1)}<% {:default, _} -> %>\n", - for(child <- children, do: Temple.Generator.to_eex(child, indent + 2)) - ] - else - "" - end, - for slot <- slots do - [ - "#{Parser.Utils.indent(indent + 1)}<% {:", - to_string(slot.name), - ", ", - "#{Macro.to_string(slot.assigns)}} -> %>\n", - for(child <- slot.content, do: Temple.Generator.to_eex(child, indent + 2)) - ] - end, - "\n#{Parser.Utils.indent(indent)}<% end %>" - ] - else - " %>" - end - ] - end - end end diff --git a/lib/temple/parser/default.ex b/lib/temple/parser/default.ex index dc6e30a..e48bf38 100644 --- a/lib/temple/parser/default.ex +++ b/lib/temple/parser/default.ex @@ -13,10 +13,4 @@ defmodule Temple.Parser.Default do def run(ast) do Temple.Ast.new(__MODULE__, elixir_ast: ast) end - - defimpl Temple.Generator do - def to_eex(%{elixir_ast: expression}, indent \\ 0) do - ["#{Parser.Utils.indent(indent)}<%= ", Macro.to_string(expression), " %>"] - end - end end diff --git a/lib/temple/parser/do_expressions.ex b/lib/temple/parser/do_expressions.ex index 915f79d..a842cb6 100644 --- a/lib/temple/parser/do_expressions.ex +++ b/lib/temple/parser/do_expressions.ex @@ -28,26 +28,4 @@ defmodule Temple.Parser.DoExpressions do Temple.Ast.new(__MODULE__, elixir_ast: {name, meta, args}, children: [do_body, else_body]) end - - defimpl Temple.Generator do - def to_eex(%{elixir_ast: expression, children: [do_body, else_body]}, indent \\ 0) do - [ - "#{Parser.Utils.indent(indent)}<%= ", - Macro.to_string(expression), - " do %>", - "\n", - for(child <- do_body, do: Temple.Generator.to_eex(child, indent + 1)) - |> Enum.intersperse("\n"), - if(else_body != nil, - do: [ - "#{Parser.Utils.indent(indent)}\n<% else %>\n", - for(child <- else_body, do: Temple.Generator.to_eex(child, indent + 1)) - |> Enum.intersperse("\n") - ], - else: "" - ), - "\n#{Parser.Utils.indent(indent)}<% end %>" - ] - end - end end diff --git a/lib/temple/parser/element_list.ex b/lib/temple/parser/element_list.ex index 1b1260b..55da997 100644 --- a/lib/temple/parser/element_list.ex +++ b/lib/temple/parser/element_list.ex @@ -14,20 +14,4 @@ defmodule Temple.Parser.ElementList do Temple.Ast.new(__MODULE__, children: children) end - - defimpl Temple.Generator do - def to_eex(%{children: children, whitespace: whitespace}, indent \\ 0) do - child_indent = if whitespace == :loose, do: indent + 1, else: 0 - self_indent = if whitespace == :loose, do: indent, else: 0 - whitespace = if whitespace == :tight, do: [], else: ["\n"] - - [ - whitespace, - for(child <- children, do: Temple.Generator.to_eex(child, child_indent)) - |> Enum.intersperse("\n"), - whitespace, - Temple.Parser.Utils.indent(self_indent) - ] - end - end end diff --git a/lib/temple/parser/empty.ex b/lib/temple/parser/empty.ex index 9a76156..a8f7bc2 100644 --- a/lib/temple/parser/empty.ex +++ b/lib/temple/parser/empty.ex @@ -14,10 +14,4 @@ defmodule Temple.Parser.Empty do def run(_ast) do Temple.Ast.new(__MODULE__) end - - defimpl Temple.Generator do - def to_eex(_, _ \\ 0) do - [] - end - end end diff --git a/lib/temple/parser/match.ex b/lib/temple/parser/match.ex index 15a65f4..b1a794e 100644 --- a/lib/temple/parser/match.ex +++ b/lib/temple/parser/match.ex @@ -17,10 +17,4 @@ defmodule Temple.Parser.Match do def run(macro) do Temple.Ast.new(__MODULE__, elixir_ast: macro) end - - defimpl Temple.Generator do - def to_eex(%{elixir_ast: elixir_ast}, indent \\ 0) do - ["#{Parser.Utils.indent(indent)}<% ", Macro.to_string(elixir_ast), " %>"] - end - end end diff --git a/lib/temple/parser/nonvoid_elements_aliases.ex b/lib/temple/parser/nonvoid_elements_aliases.ex index 28bca16..faee2b6 100644 --- a/lib/temple/parser/nonvoid_elements_aliases.ex +++ b/lib/temple/parser/nonvoid_elements_aliases.ex @@ -14,7 +14,7 @@ defmodule Temple.Parser.NonvoidElementsAliases do def applicable?(_), do: false @impl Parser - def run({name, _, args}) do + def run({name, meta, args}) do name = Parser.nonvoid_elements_lookup()[name] {do_and_else, args} = Temple.Parser.Utils.split_args(args) @@ -26,34 +26,20 @@ defmodule Temple.Parser.NonvoidElementsAliases do Temple.Ast.new(__MODULE__, name: to_string(name) |> String.replace_suffix("!", ""), attrs: args, + meta: %{whitespace: whitespace(meta)}, children: Temple.Ast.new(Temple.Parser.ElementList, children: children, - whitespace: whitespace(to_string(name)) + whitespace: whitespace(meta) ) ) end - defp whitespace(name) do - if String.ends_with?(name, "!") do - :tight - else + defp whitespace(meta) do + if Keyword.has_key?(meta, :end) do :loose - end - end - - defimpl Temple.Generator do - def to_eex(%{name: name, attrs: attrs, children: children}, indent \\ 0) do - [ - "#{Parser.Utils.indent(indent)}<", - name, - Temple.Parser.Utils.compile_attrs(attrs), - ">", - Temple.Generator.to_eex(children, indent), - "" - ] + else + :tight end end end diff --git a/lib/temple/parser/right_arrow.ex b/lib/temple/parser/right_arrow.ex index 3be2015..9af20d1 100644 --- a/lib/temple/parser/right_arrow.ex +++ b/lib/temple/parser/right_arrow.ex @@ -11,20 +11,9 @@ defmodule Temple.Parser.RightArrow do def applicable?(_), do: false @impl Parser - def run({_, _, [[pattern], args]}) do + def run({func, meta, [pattern, args]}) do children = Parser.parse(args) - Temple.Ast.new(__MODULE__, elixir_ast: pattern, children: children) - end - - defimpl Temple.Generator do - def to_eex(%{elixir_ast: elixir_ast, children: children}, indent \\ 0) do - [ - "#{Parser.Utils.indent(indent)}<% ", - Macro.to_string(elixir_ast), - " -> %>\n", - for(child <- children, do: Temple.Generator.to_eex(child, indent + 1)) - ] - end + Temple.Ast.new(__MODULE__, elixir_ast: {func, meta, [pattern]}, children: children) end end diff --git a/lib/temple/parser/slot.ex b/lib/temple/parser/slot.ex index 287992d..426899a 100644 --- a/lib/temple/parser/slot.ex +++ b/lib/temple/parser/slot.ex @@ -1,7 +1,6 @@ defmodule Temple.Parser.Slot do @moduledoc false @behaviour Temple.Parser - alias Temple.Parser.Utils defstruct name: nil, args: [] @@ -25,18 +24,4 @@ defmodule Temple.Parser.Slot do Temple.Ast.new(__MODULE__, name: slot_name, args: args) end - - defimpl Temple.Generator do - def to_eex(%{name: name, args: args}, indent \\ 0) do - render_block_function = Temple.Config.mode().render_block_function - - [ - "#{Utils.indent(indent)}<%= #{render_block_function}(@inner_block, {:", - to_string(name), - ", ", - Macro.to_string(quote(do: Enum.into(unquote(args), %{}))), - "}) %>\n" - ] - end - end end diff --git a/lib/temple/parser/text.ex b/lib/temple/parser/text.ex index 3642d5c..cfc50da 100644 --- a/lib/temple/parser/text.ex +++ b/lib/temple/parser/text.ex @@ -14,10 +14,4 @@ defmodule Temple.Parser.Text do def run(text) do Temple.Ast.new(__MODULE__, text: text) end - - defimpl Temple.Generator do - def to_eex(%{text: text}, indent \\ 0) do - [Parser.Utils.indent(indent), text] - end - end end diff --git a/lib/temple/parser/utils.ex b/lib/temple/parser/utils.ex index bbe05e3..cae7523 100644 --- a/lib/temple/parser/utils.ex +++ b/lib/temple/parser/utils.ex @@ -7,7 +7,7 @@ defmodule Temple.Parser.Utils do def kebab_to_snake(stringable), do: stringable |> to_string() |> String.replace("-", "_") - def compile_attrs([]), do: "" + def compile_attrs([]), do: [] def compile_attrs([attrs]) when is_list(attrs) do compile_attrs(attrs) @@ -15,49 +15,54 @@ defmodule Temple.Parser.Utils do def compile_attrs(attrs) when is_list(attrs) do if Keyword.keyword?(attrs) do - for {name, value} <- attrs, into: "" do - name = snake_to_kebab(name) + for {name, value} <- attrs, reduce: [] do + acc -> + name = snake_to_kebab(name) - with false <- not is_binary(value) && Macro.quoted_literal?(value), - false <- match?({_, _, _}, value), - false <- is_list(value) do - " " <> name <> "=\"" <> to_string(value) <> "\"" - else - true -> - ~s|<%= {:safe, Temple.Parser.Utils.build_attr("#{name}", #{Macro.to_string(value)})} %>| - end + with false <- not is_binary(value) && Macro.quoted_literal?(value), + false <- match?({_, _, _}, value), + false <- is_list(value) do + [{:text, " " <> name <> "=\"" <> to_string(value) <> "\""} | acc] + else + true -> + nodes = Temple.Parser.Utils.build_attr(name, value) + Enum.reverse(nodes) ++ acc + end end + |> Enum.reverse() else - "<%= Temple.Parser.Utils.runtime_attrs(" <> - (attrs |> List.first() |> Macro.to_string()) <> ") %>" + [ + {:expr, + quote do + unquote(List.first(attrs)) + end} + ] end end - def runtime_attrs(attrs) do - {:safe, - for {name, value} <- attrs, name not in [:inner_block, :inner_content], into: "" do - name = snake_to_kebab(name) - - build_attr(name, value) - end} - end - def build_attr(name, true) do - " " <> name + [{:text, " " <> name}] end def build_attr(_name, false) do - "" + [] + end + + def build_attr(name, {_, _, _} = value) do + [{:text, ~s' #{name}="'}, {:expr, value}, {:text, ~s'"'}] end def build_attr("class", classes) when is_list(classes) do - value = String.trim_leading(for {class, true} <- classes, into: "", do: " #{class}") + value = + quote do + String.trim_leading(for {class, true} <- unquote(classes), into: "", do: " #{class}") + end - " class" <> "=\"" <> value <> "\"" + [{:text, ~s' class="'}, {:expr, value}, {:text, ~s'"'}] end def build_attr(name, value) do - " " <> name <> "=\"" <> to_string(value) <> "\"" + [{:text, ~s' #{name}="' <> to_string(value) <> ~s'"'}] end def split_args(not_what_i_want) when is_nil(not_what_i_want) or is_atom(not_what_i_want), @@ -114,7 +119,19 @@ defmodule Temple.Parser.Utils do Keyword.pop(args, :compact, false) end + def indent(nil) do + "" + end + def indent(level) do String.duplicate(" ", level * 2) end + + def inspect_ast(ast) do + ast + |> Macro.to_string() + |> IO.puts() + + ast + end end diff --git a/lib/temple/parser/void_elements_aliases.ex b/lib/temple/parser/void_elements_aliases.ex index 71ae88d..22ccc74 100644 --- a/lib/temple/parser/void_elements_aliases.ex +++ b/lib/temple/parser/void_elements_aliases.ex @@ -2,8 +2,6 @@ defmodule Temple.Parser.VoidElementsAliases do @moduledoc false @behaviour Temple.Parser - alias Temple.Parser.Utils - defstruct name: nil, attrs: [] @impl Temple.Parser @@ -28,15 +26,4 @@ defmodule Temple.Parser.VoidElementsAliases do Temple.Ast.new(__MODULE__, name: name, attrs: args) end - - defimpl Temple.Generator do - def to_eex(%{name: name, attrs: attrs}, indent \\ 0) do - [ - "#{Utils.indent(indent)}<", - to_string(name), - Temple.Parser.Utils.compile_attrs(attrs), - ">" - ] - end - end end diff --git a/lib/temple/renderer.ex b/lib/temple/renderer.ex new file mode 100644 index 0000000..4538dc0 --- /dev/null +++ b/lib/temple/renderer.ex @@ -0,0 +1,361 @@ +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.Parser.Utils + + @default_engine EEx.SmartEngine + + defmacro compile(opts \\ [], do: block) do + block + |> Temple.Parser.parse() + |> Temple.Renderer.render(opts) + + # |> Temple.Parser.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) + + state = %{ + engine: engine, + indentation: 0, + terminal_node: false + } + + buffer = engine.init(%{}) + + buffer = + for ast <- asts, reduce: buffer do + buffer -> + render(buffer, state, ast) + end + + if function_exported?(engine, :handle_body, 2) do + engine.handle_body(buffer, root: length(asts) == 1) + else + engine.handle_body(buffer) + end + end + + def render(buffer, state, %Text{text: text}) do + t = Utils.indent(state.indentation) <> text <> new_line(state) + + unless t == "" do + state.engine.handle_text( + buffer, + [], + t + ) + end + end + + def render(buffer, state, asts) when is_list(asts) do + for ast <- asts, reduce: buffer do + buffer -> + render(buffer, state, ast) + end + end + + def render(buffer, state, %Components{ + function: function, + assigns: assigns, + children: children, + 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 + slot_buffer = state.engine.handle_begin(buffer) + + slot_buffer = + for child <- children(slot.content), reduce: slot_buffer do + slot_buffer -> + render(slot_buffer, state, child) + end + + ast = state.engine.handle_end(slot_buffer) + + [quoted] = + quote do + {unquote(slot.name), unquote(slot.assigns)} -> + unquote(ast) + end + + quoted + end + + {:fn, meta, clauses} = + quote do + fn + {:default, _} -> unquote(child_quoted) + end + end + + slot_func = {:fn, meta, slot_quotes ++ clauses} + + expr = + quote do + component( + unquote(function), + Map.put(Map.new(unquote(assigns)), :__slots__, unquote(slot_func)) + ) + end + + state.engine.handle_expr(buffer, "=", expr) + end + + def render(buffer, state, %Slot{} = ast) do + render_slot_func = + quote do + var!(assigns).__slots__.({unquote(ast.name), Map.new(unquote(ast.args))}) + end + + state.engine.handle_expr(buffer, "=", render_slot_func) + end + + def render(buffer, state, %ElementList{} = ast) do + render(buffer, state, ast.children) + end + + def render(buffer, state, %NonvoidElementsAliases{} = ast) do + current_indent = Utils.indent(state.indentation) + + inside_new_lines = if ast.meta.whitespace == :tight, do: "", else: "\n" + new_indent = if ast.meta.whitespace == :tight, do: nil, else: state.indentation + 1 + + buffer = + state.engine.handle_text( + buffer, + [], + "#{current_indent}<#{ast.name}" + ) + + buffer = + for node <- Utils.compile_attrs(ast.attrs), reduce: buffer do + buffer -> + case node do + {:text, text} -> + state.engine.handle_text(buffer, [], text) + + {:expr, expr} -> + state.engine.handle_expr(buffer, "=", expr) + end + end + + buffer = + state.engine.handle_text( + buffer, + [], + ">#{inside_new_lines}" + ) + + buffer = + if Enum.any?(children(ast.children)) do + for {child, index} <- Enum.with_index(children(ast.children), 1), reduce: buffer do + buffer -> + render( + buffer, + %{ + state + | indentation: new_indent, + terminal_node: index == length(children(ast.children)) + }, + child + ) + end + else + buffer + end + + state.engine.handle_text( + buffer, + [], + "#{inside_new_lines}#{Utils.indent(if(ast.meta.whitespace == :loose, do: state.indentation, else: nil))}#{new_line(state)}\n" + ) + end + + def render(buffer, state, %VoidElementsAliases{} = ast) do + current_indent = Utils.indent(state.indentation) + + buffer = + state.engine.handle_text( + buffer, + [], + "#{current_indent}<#{ast.name}" + ) + + buffer = + for node <- Utils.compile_attrs(ast.attrs), reduce: buffer do + buffer -> + case node do + {:text, text} -> + state.engine.handle_text(buffer, [], text) + + {:expr, expr} -> + state.engine.handle_expr(buffer, "=", expr) + end + end + + state.engine.handle_text(buffer, [], ">\n") + end + + def render(buffer, state, %AnonymousFunctions{} = ast) do + new_buffer = state.engine.handle_begin(buffer) + + new_buffer = + for child <- children(ast.children), child != nil, reduce: new_buffer do + new_buffer -> + render(new_buffer, state, child) + end + + new_buffer = state.engine.handle_text(new_buffer, [], "\n") + + inner_quoted = state.engine.handle_end(new_buffer) + + {name, meta, args} = ast.elixir_ast + + {args, {func, fmeta, [{arrow, arrowmeta, [first, _block]}]}, args2} = + Temple.Parser.Utils.split_on_fn(args, {[], nil, []}) + + full_ast = + {name, meta, args ++ [{func, fmeta, [{arrow, arrowmeta, [first, inner_quoted]}]}] ++ args2} + + state.engine.handle_expr(buffer, "=", full_ast) + end + + def render(buffer, state, %RightArrow{elixir_ast: elixir_ast} = ast) do + new_buffer = state.engine.handle_begin(buffer) + + new_buffer = + for child <- children(ast.children), child != nil, reduce: new_buffer do + new_buffer -> + render(new_buffer, state, child) + end + + inner_quoted = state.engine.handle_end(new_buffer) + + {func, meta, [first]} = elixir_ast + + full_ast = {func, meta, [first | [inner_quoted]]} + + state.engine.handle_expr(buffer, "", full_ast) + end + + def render(buffer, state, %DoExpressions{} = ast) do + {func, meta, args} = ast.elixir_ast + new_buffer = state.engine.handle_begin(buffer) + + [do_block, else_block] = ast.children + + do_inner_quoted = + case do_block do + [%RightArrow{} | _] = bodies -> + for b <- bodies do + new_buffer = state.engine.handle_begin(buffer) + + new_buffer = render(new_buffer, state, b) + + {:__block__, _, [quoted | _]} = state.engine.handle_end(new_buffer) + quoted + end + + block -> + for child <- children(block), child != nil, reduce: new_buffer do + new_buffer -> + render(new_buffer, state, child) + end + |> state.engine.handle_end() + end + + else_inner_quoted = + if else_block do + case else_block do + [%RightArrow{} | _] = bodies -> + for b <- bodies do + new_buffer = state.engine.handle_begin(buffer) + + new_buffer = render(new_buffer, state, b) + + {:__block__, _, [quoted | _]} = state.engine.handle_end(new_buffer) + quoted + end + + block -> + for child <- children(block), child != nil, reduce: new_buffer do + new_buffer -> + render(new_buffer, state, child) + end + |> state.engine.handle_end() + end + end + + new_args = + then([do: do_inner_quoted], fn args -> + if else_inner_quoted do + Keyword.put(args, :else, else_inner_quoted) |> Enum.reverse() + else + args + end + end) + + full_ast = {func, meta, args ++ [new_args]} + + state.engine.handle_expr(buffer, "=", full_ast) + end + + def render(buffer, state, %Match{elixir_ast: elixir_ast}) do + state.engine.handle_expr(buffer, "", elixir_ast) + end + + def render(buffer, state, %Default{elixir_ast: elixir_ast}) do + buffer = + if state.indentation && state.indentation > 0 do + state.engine.handle_text(buffer, [], Utils.indent(state.indentation)) + else + buffer + end + + buffer = state.engine.handle_expr(buffer, "=", elixir_ast) + + if not state.terminal_node do + state.engine.handle_text(buffer, [], "\n") + else + buffer + end + end + + def render(buffer, _state, %Empty{}), do: buffer + + defp children(%ElementList{children: children}), do: children + defp children(list) when is_list(list), do: list + + def new_line(%{terminal_node: false}), do: "\n" + def new_line(%{terminal_node: true}), do: "" +end diff --git a/mix.exs b/mix.exs index 0d8e293..90312aa 100644 --- a/mix.exs +++ b/mix.exs @@ -6,17 +6,14 @@ defmodule Temple.MixProject do app: :temple, name: "Temple", description: "An HTML DSL for Elixir and Phoenix", - version: "0.8.0", + version: "0.9.0-rc.0", package: package(), elixirc_paths: elixirc_paths(Mix.env()), - elixir: "~> 1.9", + elixir: "~> 1.13", start_permanent: Mix.env() == :prod, deps: deps(), source_url: "https://github.com/mhanberg/temple", - docs: [ - main: "Temple", - extras: ["README.md"] - ] + docs: docs() ] end @@ -31,6 +28,27 @@ defmodule Temple.MixProject do ] end + defp docs() do + [ + main: "Temple", + extras: [ + "README.md", + "guides/getting-started.md", + "guides/your-first-template.md", + "guides/components.md", + "guides/migrating/0.8-to-0.9.md" + ], + groups_for_extras: groups_for_extras() + ] + end + + defp groups_for_extras do + [ + Guides: ~r/guides\/[^\/]+\.md/, + Migrating: ~r/guides\/migrating\/.?/ + ] + end + defp package do [ maintainers: ["Mitchell Hanberg"], @@ -43,8 +61,8 @@ defmodule Temple.MixProject do defp deps do [ {:ex_doc, "~> 0.28.3", only: :dev, runtime: false}, - {:phoenix_view, "~> 1.0"}, - {:phoenix_live_view, ">= 0.0.0", only: :test} + {:phoenix_view, "~> 1.1.2", optional: true}, + {:phoenix, "~> 1.6", optional: true} ] end end diff --git a/mix.lock b/mix.lock index 6fbc162..5c3a4e5 100644 --- a/mix.lock +++ b/mix.lock @@ -6,12 +6,10 @@ "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, "mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, - "phoenix": {:hex, :phoenix, "1.6.2", "6cbd5c8ed7a797f25a919a37fafbc2fb1634c9cdb12a4448d7a5d0b26926f005", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7bbee475acae0c3abc229b7f189e210ea788e63bd168e585f60c299a4b2f9133"}, - "phoenix_html": {:hex, :phoenix_html, "3.1.0", "0b499df05aad27160d697a9362f0e89fa0e24d3c7a9065c2bd9d38b4d1416c09", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0c0a98a2cefa63433657983a2a594c7dee5927e4391e0f1bfd3a151d1def33fc"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.5", "63f52a6f9f6983f04e424586ff897c016ecc5e4f8d1e2c22c2887af1c57215d8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c5586e6a3d4df71b8214c769d4f5eb8ece2b4001711a7ca0f97323c36958b0e3"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, - "phoenix_view": {:hex, :phoenix_view, "1.0.0", "fea71ecaaed71178b26dd65c401607de5ec22e2e9ef141389c721b3f3d4d8011", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "82be3e2516f5633220246e2e58181282c71640dab7afc04f70ad94253025db0c"}, - "plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"}, + "phoenix": {:hex, :phoenix, "1.6.7", "f1de32418bbbcd471f4fe74d3860ee9c8e8c6c36a0ec173be8ff468a5d72ac90", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b354a4f11d9a2f3a380fb731042dae064f22d7aed8c7e7c024a2459f12994aad"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, + "phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"}, + "plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"}, "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, - "telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"}, + "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, } diff --git a/temple-github-image.png b/temple-github-image.png new file mode 100644 index 0000000..fcb01a6 Binary files /dev/null and b/temple-github-image.png differ diff --git a/test/component_test.exs b/test/component_test.exs deleted file mode 100644 index 9750f31..0000000 --- a/test/component_test.exs +++ /dev/null @@ -1,160 +0,0 @@ -defmodule Temple.ComponentTest do - use ExUnit.Case, async: true - use Temple - use Temple.Support.Utils - - test "renders components using Phoenix.View.render_layout" do - result = - temple do - div class: "font-bold" do - "Hello, world" - end - - c Temple.Components.Component do - aside class: "foobar" do - "I'm a component!" - end - end - end - - assert evaluate_template(result) == - ~s""" -
- Hello, world -
-
- - - -
- - """ - end - - test "function components can accept local assigns" do - result = - temple do - div class: "font-bold" do - "Hello, world" - end - - c Temple.Components.Component2, class: "bg-red" do - "I'm a component!" - end - end - - assert evaluate_template(result) == - ~s""" -
- Hello, world -
-
- - I'm a component! - -
- """ - end - - test "function components can use other components" do - result = - temple do - c Temple.Components.Outer do - "outer!" - end - - c Temple.Components.Inner, outer_id: "set by root inner" do - "inner!" - end - end - - assert evaluate_template(result) == ~s""" -
- - - outer! - - -
- - -
- - inner! - -
- - """ - end - - test "components can use functions from their modules" do - result = - temple do - c Temple.Components.WithFuncs, foo: :bar do - "doo doo" - end - end - - assert evaluate_template(result) == ~s""" -
- - doo doo - -
- - """ - end - - test "components can be void elements" do - result = - temple do - c Temple.Components.VoidComponent, foo: :bar - end - - assert evaluate_template(result) == ~s""" -
- bar -
- """ - end - - test "components can have named slots" do - assigns = %{name: "bob"} - - result = - temple do - c Temple.Components.WithSlot do - slot :header, %{value: val} do - div do - "the value is #{val}" - end - end - - button class: "btn", phx_click: :toggle do - @name - end - end - end - - assert evaluate_template(result, assigns) == - ~s""" -
- -
- the value is Header -
- -
- - - -
-
- - """ - end -end diff --git a/test/parser/anonymous_functions_test.exs b/test/parser/anonymous_functions_test.exs index 6564304..d100711 100644 --- a/test/parser/anonymous_functions_test.exs +++ b/test/parser/anonymous_functions_test.exs @@ -64,24 +64,4 @@ defmodule Temple.Parser.AnonymousFunctionsTest do } = ast end end - - describe "Temple.Generator.to_eex/1" do - test "emits eex" do - raw_ast = - quote do - form_for(changeset, Routes.foo_path(conn, :create), fn form -> - Does.something!(form) - end) - end - - result = - raw_ast - |> AnonymousFunctions.run() - |> struct(children: []) - |> Temple.Generator.to_eex() - - assert result |> :erlang.iolist_to_binary() == - ~s|<%= form_for changeset, Routes.foo_path(conn, :create), fn form -> %>\n<% end %>\n| - end - end end diff --git a/test/parser/components_test.exs b/test/parser/components_test.exs index 224f87e..88a1754 100644 --- a/test/parser/components_test.exs +++ b/test/parser/components_test.exs @@ -2,13 +2,12 @@ defmodule Temple.Parser.ComponentsTest do use ExUnit.Case, async: false alias Temple.Parser.Components alias Temple.Parser.Slottable - use Temple.Support.Utils describe "applicable?/1" do test "runs when using the `c` ast with a block" do ast = quote do - c SomeModule, foo: :bar do + c &SomeModule.render/1, foo: :bar do div do "hello" end @@ -21,7 +20,9 @@ defmodule Temple.Parser.ComponentsTest do test "runs when using the `c` ast with an inline block" do ast = quote do - c SomeModule, foo: :bar, do: "hello" + c &SomeModule.render/1, foo: :bar do + "hello" + end end assert Components.applicable?(ast) @@ -30,7 +31,7 @@ defmodule Temple.Parser.ComponentsTest do test "runs when using the `c` ast without a block" do ast = quote do - c(SomeModule, foo: :bar) + c &SomeModule.render/1, foo: :bar end assert Components.applicable?(ast) @@ -38,10 +39,14 @@ defmodule Temple.Parser.ComponentsTest do end describe "run/2" do - test "adds a node to the buffer" do + setup do + [func: quote(do: &SomeModule.render/1)] + end + + test "adds a node to the buffer", %{func: func} do raw_ast = quote do - c SomeModule do + c unquote(func) do aside class: "foobar" do "I'm a component!" end @@ -51,31 +56,31 @@ defmodule Temple.Parser.ComponentsTest do ast = Components.run(raw_ast) assert %Components{ - module: SomeModule, + function: ^func, assigns: [], children: _ } = ast end - test "runs when using the `c` ast with an inline block" do + test "runs when using the `c` ast with an inline block", %{func: func} do ast = quote do - c SomeModule, foo: :bar, do: "hello" + c unquote(func), foo: :bar, do: "hello" end ast = Components.run(ast) assert %Components{ - module: SomeModule, + function: ^func, assigns: [foo: :bar], children: _ } = ast end - test "adds a node to the buffer that takes args" do + test "adds a node to the buffer that takes args", %{func: func} do raw_ast = quote do - c SomeModule, foo: :bar do + c unquote(func), foo: :bar do aside class: "foobar" do "I'm a component!" end @@ -85,31 +90,31 @@ defmodule Temple.Parser.ComponentsTest do ast = Components.run(raw_ast) assert %Components{ - module: SomeModule, + function: ^func, assigns: [foo: :bar], children: _ } = ast end - test "adds a node to the buffer that without a block" do + test "adds a node to the buffer that without a block", %{func: func} do raw_ast = quote do - c SomeModule, foo: :bar + c unquote(func), foo: :bar end ast = Components.run(raw_ast) assert %Components{ - module: SomeModule, + function: ^func, assigns: [foo: :bar], children: [] } = ast end - test "gathers all slots" do + test "gathers all slots", %{func: func} do raw_ast = quote do - c SomeModule, foo: :bar do + c unquote(func), foo: :bar do slot :foo, %{form: form} do "in the slot" end @@ -119,7 +124,7 @@ defmodule Temple.Parser.ComponentsTest do ast = Components.run(raw_ast) assert %Components{ - module: SomeModule, + function: ^func, assigns: [foo: :bar], slots: [ %Slottable{ @@ -133,11 +138,15 @@ defmodule Temple.Parser.ComponentsTest do end test "slots should only be assigned to the component root" do + card = quote do: &Card.render/1 + footer = quote do: &Card.Footer.render/1 + list = quote do: &LinkList.render/1 + raw_ast = quote do - c Card do - c Card.Footer do - c LinkList, socials: @user.socials do + c unquote(card) do + c unquote(footer) do + c unquote(list), socials: @user.socials do "hello" slot :default, %{text: text, url: url} do @@ -171,107 +180,4 @@ defmodule Temple.Parser.ComponentsTest do } = ast end end - - describe "Temple.Generator.to_eex/1" do - test "emits eex for non void component" do - raw_ast = - quote do - c SomeModule, foo: :bar do - "I'm a component!" - end - end - - result = - raw_ast - |> Components.run() - |> Temple.Generator.to_eex() - - assert result |> :erlang.iolist_to_binary() == - ~s""" - <%= Temple.Component.__component__ SomeModule, [foo: :bar] do %> - <% {:default, _} -> %> - I'm a component! - <% end %> - """ - end - - test "emits eex for void component with slots" do - raw_ast = - quote do - c SomeModule, foo: :bar do - slot :foo, %{form: form} do - div do - "in the slot" - end - end - end - end - - result = - raw_ast - |> Components.run() - |> Temple.Generator.to_eex() - - assert result |> :erlang.iolist_to_binary() == - ~s""" - <%= Temple.Component.__component__ SomeModule, [foo: :bar] do %> - <% {:foo, %{form: form}} -> %> -
- in the slot -
- <% end %> - """ - end - - test "emits eex for nonvoid component with slots" do - raw_ast = - quote do - c SomeModule, foo: :bar do - slot :foo, %{form: form} do - div do - "in the slot" - end - end - - div do - "inner content" - end - end - end - - result = - raw_ast - |> Components.run() - |> Temple.Generator.to_eex() - - assert result |> :erlang.iolist_to_binary() == - ~s""" - <%= Temple.Component.__component__ SomeModule, [foo: :bar] do %> - <% {:default, _} -> %> -
- inner content -
- <% {:foo, %{form: form}} -> %> -
- in the slot -
- <% end %> - """ - end - - test "emits eex for void component" do - raw_ast = - quote do - c SomeModule, foo: :bar - end - - result = - raw_ast - |> Components.run() - |> Temple.Generator.to_eex() - - assert result |> :erlang.iolist_to_binary() == - ~s|<%= Temple.Component.__component__ SomeModule, [foo: :bar] %>| - end - end end diff --git a/test/parser/default_test.exs b/test/parser/default_test.exs index 7c2ffd8..3f2aa99 100644 --- a/test/parser/default_test.exs +++ b/test/parser/default_test.exs @@ -2,7 +2,6 @@ defmodule Temple.Parser.DefaultTest do use ExUnit.Case, async: true alias Temple.Parser.Default - alias Temple.Support.Utils describe "applicable?/1" do test "returns true when the node is an elixir expression" do @@ -27,20 +26,4 @@ defmodule Temple.Parser.DefaultTest do assert %Default{elixir_ast: expression} == ast end end - - describe "to_eex/1" do - test "emits eex" do - result = - quote do - Foo.bar!(baz) - end - |> Default.run() - |> Temple.Generator.to_eex() - |> Utils.iolist_to_binary() - - assert result == ~s""" - <%= Foo.bar!(baz) %> - """ - end - end end diff --git a/test/parser/do_expressions_test.exs b/test/parser/do_expressions_test.exs index 6592fc5..7bde960 100644 --- a/test/parser/do_expressions_test.exs +++ b/test/parser/do_expressions_test.exs @@ -2,7 +2,6 @@ defmodule Temple.Parser.DoExpressionsTest do use ExUnit.Case, async: true alias Temple.Parser.DoExpressions - alias Temple.Support.Utils describe "applicable?/1" do test "returns true when the node contains a do expression" do @@ -37,72 +36,4 @@ defmodule Temple.Parser.DoExpressionsTest do } = ast end end - - describe "to_eex/1" do - test "emits eex" do - result = - quote do - for big <- boys do - "bob" - end - end - |> DoExpressions.run() - |> Temple.Generator.to_eex() - |> Utils.iolist_to_binary() - - assert result == - ~s""" - <%= for(big <- boys) do %> - bob - <% end %> - """ - end - - test "emits eex for that includes in else clause" do - result = - quote do - if foo? do - "bob" - - "bobby" - else - "carol" - end - end - |> DoExpressions.run() - |> Temple.Generator.to_eex() - |> Utils.iolist_to_binary() - - assert result == - ~s""" - <%= if(foo?) do %> - bob - bobby - <% else %> - carol - <% end %> - """ - end - - test "emits eex for a case expression" do - result = - quote do - case foo? do - :bing -> - :bong - end - end - |> DoExpressions.run() - |> Temple.Generator.to_eex() - |> Utils.iolist_to_binary() - - assert result == - ~s""" - <%= case(foo?) do %> - <% :bing -> %> - <%= :bong %> - <% end %> - """ - end - end end diff --git a/test/parser/empty_test.exs b/test/parser/empty_test.exs index c6f492c..b727786 100644 --- a/test/parser/empty_test.exs +++ b/test/parser/empty_test.exs @@ -30,20 +30,4 @@ defmodule Temple.Parser.EmptyTest do end end end - - describe "Temple.Generator.to_eex/1" do - test "emits eex for non void component" do - raw_ast = - quote do - nil - end - - result = - raw_ast - |> Empty.run() - |> Temple.Generator.to_eex() - - assert result |> :erlang.iolist_to_binary() == "" - end - end end diff --git a/test/parser/match_test.exs b/test/parser/match_test.exs index a7cb392..10bad4f 100644 --- a/test/parser/match_test.exs +++ b/test/parser/match_test.exs @@ -36,37 +36,4 @@ defmodule Temple.Parser.MatchTest do assert %Match{elixir_ast: expression} == ast end end - - describe "Temple.Generator.to_eex/1" do - test "emits eex" do - raw_ast = - quote do - yolo = :synergy - end - - result = - raw_ast - |> Match.run() - |> Temple.Generator.to_eex() - - assert result |> :erlang.iolist_to_binary() == ~s|<% yolo = :synergy %>| - end - - test "emits eex big boy" do - raw_ast = - quote do - yolo = - if true do - :synergy - end - end - - result = - raw_ast - |> Match.run() - |> Temple.Generator.to_eex() - - assert result |> :erlang.iolist_to_binary() == ~s|<% yolo = if(true) do\n :synergy\nend %>| - end - end end diff --git a/test/parser/nonvoid_elements_aliases_test.exs b/test/parser/nonvoid_elements_aliases_test.exs index 22411a2..7965d03 100644 --- a/test/parser/nonvoid_elements_aliases_test.exs +++ b/test/parser/nonvoid_elements_aliases_test.exs @@ -3,7 +3,6 @@ defmodule Temple.Parser.NonvoidElementsAliasesTest do alias Temple.Parser.NonvoidElementsAliases alias Temple.Parser.ElementList - alias Temple.Support.Utils describe "applicable?/1" do test "returns true when the node is a nonvoid element or alias" do @@ -87,59 +86,4 @@ defmodule Temple.Parser.NonvoidElementsAliasesTest do } = ast end end - - describe "to_eex/1" do - test "emits eex" do - result = - quote do - div class: "foo", id: var do - select__ do - option do - "foo" - end - end - end - end - |> NonvoidElementsAliases.run() - |> Temple.Generator.to_eex() - |> Utils.iolist_to_binary() - - assert result == - ~s""" -
> - -
- """ - end - - test "produce 'tight' markup" do - result = - quote do - div class: "foo", id: var do - select__ do - option! do - "foo" - end - end - end - end - |> NonvoidElementsAliases.run() - |> Temple.Generator.to_eex() - |> :erlang.iolist_to_binary() - |> Kernel.<>("\n") - - assert result == - ~s""" -
> - -
- """ - end - end end diff --git a/test/parser/right_arrow_test.exs b/test/parser/right_arrow_test.exs index 7ee66ef..2c4dc4d 100644 --- a/test/parser/right_arrow_test.exs +++ b/test/parser/right_arrow_test.exs @@ -2,7 +2,6 @@ defmodule Temple.Parser.RightArrowTest do use ExUnit.Case, async: true alias Temple.Parser.RightArrow - alias Temple.Support.Utils describe "applicable?/1" do test "returns true when the node contains a right arrow" do @@ -51,7 +50,7 @@ defmodule Temple.Parser.RightArrowTest do ast = RightArrow.run(raw_ast) assert %RightArrow{ - elixir_ast: :bing, + elixir_ast: {:->, [newlines: 1], [[:bing]]}, children: [ %Temple.Parser.Default{ elixir_ast: ^bong @@ -60,24 +59,4 @@ defmodule Temple.Parser.RightArrowTest do } = ast end end - - describe "to_eex/1" do - test "emits eex" do - result = - quote do - :bing -> - :bong - end - |> List.first() - |> RightArrow.run() - |> Temple.Generator.to_eex() - |> Utils.iolist_to_binary() - - assert result == - ~s""" - <% :bing -> %> - <%= :bong %> - """ - end - end end diff --git a/test/parser/slot_test.exs b/test/parser/slot_test.exs index 78b015c..9a88e4c 100644 --- a/test/parser/slot_test.exs +++ b/test/parser/slot_test.exs @@ -28,23 +28,4 @@ defmodule Temple.Parser.SlotTest do } == ast end end - - describe "Temple.Generator.to_eex/1" do - test "emits eex for a slot" do - raw_ast = - quote do - slot :header, value: Form.form_for(changeset, action) - end - - result = - raw_ast - |> Slot.run() - |> Temple.Generator.to_eex() - - assert result |> :erlang.iolist_to_binary() == - ~s""" - <%= Temple.Component.__render_block__(@inner_block, {:header, Enum.into([value: Form.form_for(changeset, action)], %{})}) %> - """ - end - end end diff --git a/test/parser/temple_namespace_nonvoid_test.exs b/test/parser/temple_namespace_nonvoid_test.exs index 81df4ab..9d72d5e 100644 --- a/test/parser/temple_namespace_nonvoid_test.exs +++ b/test/parser/temple_namespace_nonvoid_test.exs @@ -3,7 +3,6 @@ defmodule Temple.Parser.TempleNamespaceNonvoidTest do alias Temple.Parser.NonvoidElementsAliases alias Temple.Parser.TempleNamespaceNonvoid - alias Temple.Support.Utils describe "applicable?/1" do test "returns true when the node is a Temple aliased nonvoid element" do @@ -58,25 +57,4 @@ defmodule Temple.Parser.TempleNamespaceNonvoidTest do } = ast end end - - describe "to_eex/1" do - test "emits eex" do - result = - quote do - Temple.div class: "foo", id: var do - "foo" - end - end - |> TempleNamespaceNonvoid.run() - |> Temple.Generator.to_eex() - |> Utils.iolist_to_binary() - - assert result == - ~s""" -
> - foo -
- """ - end - end end diff --git a/test/parser/temple_namespace_void_test.exs b/test/parser/temple_namespace_void_test.exs index 19e8d2d..d848f4b 100644 --- a/test/parser/temple_namespace_void_test.exs +++ b/test/parser/temple_namespace_void_test.exs @@ -3,7 +3,6 @@ defmodule Temple.Parser.TempleNamespaceVoidTest do alias Temple.Parser.TempleNamespaceVoid alias Temple.Parser.VoidElementsAliases - alias Temple.Support.Utils describe "applicable?/1" do test "returns true when the node is a Temple aliased nonvoid element" do @@ -50,18 +49,4 @@ defmodule Temple.Parser.TempleNamespaceVoidTest do } = ast end end - - describe "to_eex/1" do - test "emits eex" do - result = - quote do - Temple.meta(content: "foo") - end - |> TempleNamespaceVoid.run() - |> Temple.Generator.to_eex() - |> Utils.iolist_to_binary() - - assert result == ~s|\n| - end - end end diff --git a/test/parser/text_test.exs b/test/parser/text_test.exs index 432df1c..39f207a 100644 --- a/test/parser/text_test.exs +++ b/test/parser/text_test.exs @@ -2,7 +2,6 @@ defmodule Temple.Parser.TextTest do use ExUnit.Case, async: true alias Temple.Parser.Text - alias Temple.Support.Utils describe "applicable?/1" do test "returns true when the node is a string literal" do @@ -29,16 +28,4 @@ defmodule Temple.Parser.TextTest do assert %Text{text: text} == ast end end - - describe "Temple.Generator.to_eex/1" do - test "emits eex" do - result = - "string literal" - |> Text.run() - |> Temple.Generator.to_eex() - |> Utils.iolist_to_binary() - - assert result == ~s|string literal\n| - end - end end diff --git a/test/parser/utils_test.exs b/test/parser/utils_test.exs index 7030e9b..1d9c93e 100644 --- a/test/parser/utils_test.exs +++ b/test/parser/utils_test.exs @@ -3,30 +3,63 @@ defmodule Temple.Parser.UtilsTest do alias Temple.Parser.Utils - describe "runtime_attrs/1" do - test "compiles keyword lists and maps into html attributes" do - attrs_map = %{ - class: "text-red", - id: "form1", - disabled: false, - inner_block: %{} - } + describe "compile_attrs/1" do + test "returns a list of text nodes for static attributes" do + attrs = [class: "text-red", id: "error", phx_submit: :save, data_number: 99] - attrs_kw = [ - class: "text-red", - id: "form1", - disabled: true, - inner_block: %{} - ] + actual = Utils.compile_attrs(attrs) - assert {:safe, ~s| class="text-red" id="form1"|} == Utils.runtime_attrs(attrs_map) - assert {:safe, ~s| class="text-red" id="form1" disabled|} == Utils.runtime_attrs(attrs_kw) + assert [ + {:text, ~s' class="text-red"'}, + {:text, ~s' id="error"'}, + {:text, ~s' phx-submit="save"'}, + {:text, ~s' data-number="99"'} + ] == actual end - test "class accepts a keyword list which conditionally emits classes" do - attrs = [class: ["text-red": false, "text-blue": true], id: "form1"] + 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] - assert {:safe, ~s| class="text-blue" id="form1"|} == Utils.runtime_attrs(attrs) + actual = 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 + end + + test "returns a list of text and expr nodes for the class object syntax" do + class_ast = quote(do: @class) + + list = + quote do + ["text-red": unquote(class_ast)] + end + + expr = + quote do + String.trim_leading(for {class, true} <- unquote(list), into: "", do: " #{class}") + end + + attrs = [class: ["text-red": class_ast]] + + actual = Utils.compile_attrs(attrs) + + assert [ + {:text, ~s' class="'}, + {:expr, result_expr}, + {:text, ~s'"'} + ] = actual + + # the ast metadata is different, let's just compare stringified versions + assert Macro.to_string(result_expr) == Macro.to_string(expr) end end end diff --git a/test/parser/void_elements_aliases_test.exs b/test/parser/void_elements_aliases_test.exs index a2a898a..68c4747 100644 --- a/test/parser/void_elements_aliases_test.exs +++ b/test/parser/void_elements_aliases_test.exs @@ -2,7 +2,6 @@ defmodule Temple.Parser.VoidElementsAliasesTest do use ExUnit.Case, async: true alias Temple.Parser.VoidElementsAliases - alias Temple.Support.Utils describe "applicable?/1" do test "returns true when the node is a nonvoid element or alias" do @@ -55,18 +54,4 @@ defmodule Temple.Parser.VoidElementsAliasesTest do } = ast end end - - describe "to_eex/1" do - test "emits eex" do - result = - quote do - meta content: "foo" - end - |> VoidElementsAliases.run() - |> Temple.Generator.to_eex() - |> Utils.iolist_to_binary() - - assert result == ~s|\n| - end - end end diff --git a/test/support/components/component.ex b/test/support/components/component.ex deleted file mode 100644 index 6a8666d..0000000 --- a/test/support/components/component.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Temple.Components.Component do - import Temple.Component - - render do - div do - slot :default - end - end -end diff --git a/test/support/components/component2.ex b/test/support/components/component2.ex deleted file mode 100644 index d719937..0000000 --- a/test/support/components/component2.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Temple.Components.Component2 do - import Temple.Component - - render do - div class: @class do - slot :default - end - end -end diff --git a/test/support/components/has_temple.ex b/test/support/components/has_temple.ex deleted file mode 100644 index a404017..0000000 --- a/test/support/components/has_temple.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Temple.Components.HasTemple do - import Temple.Component - - render do - div class: @temple[:class] do - slot :default - end - end -end diff --git a/test/support/components/inner.ex b/test/support/components/inner.ex deleted file mode 100644 index 841e9fb..0000000 --- a/test/support/components/inner.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Temple.Components.Inner do - import Temple.Component - - render do - div id: "inner", outer_id: @outer_id do - slot :default - end - end -end diff --git a/test/support/components/outer.ex b/test/support/components/outer.ex deleted file mode 100644 index 0b1691f..0000000 --- a/test/support/components/outer.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Temple.Components.Outer do - import Temple.Component - - render do - c Temple.Components.Inner, outer_id: "from-outer" do - slot :default, %{} - end - end -end diff --git a/test/support/components/section.ex b/test/support/components/section.ex deleted file mode 100644 index 3772901..0000000 --- a/test/support/components/section.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Temple.Components.Section do - import Temple.Component - - render do - section class: "foo!" do - slot :default - end - end -end diff --git a/test/support/components/void_component.ex b/test/support/components/void_component.ex deleted file mode 100644 index 3338caa..0000000 --- a/test/support/components/void_component.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Temple.Components.VoidComponent do - import Temple.Component - - render do - div class: "void!!" do - "bar" - end - end -end diff --git a/test/support/components/with_funcs.ex b/test/support/components/with_funcs.ex deleted file mode 100644 index c2a3052..0000000 --- a/test/support/components/with_funcs.ex +++ /dev/null @@ -1,17 +0,0 @@ -defmodule Temple.Components.WithFuncs do - import Temple.Component - - def get_class(:bar) do - "barbarbar" - end - - def get_class(_) do - "foofoofoo" - end - - render do - div class: get_class(@foo) do - slot :default - end - end -end diff --git a/test/support/components/with_slot.ex b/test/support/components/with_slot.ex deleted file mode 100644 index f1fc64e..0000000 --- a/test/support/components/with_slot.ex +++ /dev/null @@ -1,13 +0,0 @@ -defmodule Temple.Components.WithSlot do - import Temple.Component - - render do - div do - slot :header, value: "Header" - - div class: "wrapped" do - slot :default - end - end - end -end diff --git a/test/support/utils.ex b/test/support/utils.ex deleted file mode 100644 index e47adf0..0000000 --- a/test/support/utils.ex +++ /dev/null @@ -1,52 +0,0 @@ -defmodule Temple.Support.Utils do - defmacro __using__(_) do - quote do - import Kernel, except: [==: 2, =~: 2] - import unquote(__MODULE__) - end - end - - def a == b when is_binary(a) and is_binary(b) do - a = String.replace(a, "\n", "") - b = String.replace(b, "\n", "") - - Kernel.==(a, b) - end - - def a =~ b when is_binary(a) and is_binary(b) do - a = String.replace(a, "\n", "") - b = String.replace(b, "\n", "") - - Kernel.=~(a, b) - end - - def env do - require Temple.Component - - __ENV__ - end - - def evaluate_template(template, assigns \\ %{}) do - template - |> EEx.compile_string(engine: Phoenix.HTML.Engine) - |> Code.eval_quoted([assigns: assigns], env()) - |> elem(0) - |> Phoenix.HTML.safe_to_string() - end - - @doc """ - Converts an iolist to a binary and appends a new line. - """ - def iolist_to_binary(iolist) do - iolist - |> :erlang.iolist_to_binary() - |> append_new_line() - end - - @doc """ - Appends a new line to a string. - """ - def append_new_line(string) do - string <> "\n" - end -end diff --git a/test/temple/renderer_test.exs b/test/temple/renderer_test.exs new file mode 100644 index 0000000..86dfb97 --- /dev/null +++ b/test/temple/renderer_test.exs @@ -0,0 +1,505 @@ +defmodule Temple.RendererTest do + use ExUnit.Case, async: true + + import Temple + + require Temple.Renderer + alias Temple.Renderer + + describe "compile/1" do + test "produces renders a text node" do + result = + Renderer.compile do + "hello world" + end + + assert "hello world\n" == result + end + + test "produces renders a div" do + result = + Renderer.compile do + div class: "hello world" do + "hello world" + + span id: "name", do: "bob" + end + end + + # html + expected = """ +
+ hello world + bob + +
+ + """ + + assert expected == result + end + + test "produces renders a void elements" do + result = + Renderer.compile do + div class: "hello world" do + "hello world" + + input type: "button", value: "Submit" + input type: "button", value: "Submit" + end + end + + # html + expected = """ +
+ hello world + + + +
+ + """ + + assert expected == result + end + + test "a match does not emit" do + result = + Renderer.compile do + div class: "hello world" do + _ = "hello world" + + span id: "name", do: "bob" + end + end + + # html + expected = """ +
+ bob + +
+ + """ + + assert expected == result + end + + test "handles simple expression inside attributes" do + assigns = %{statement: "hello world", color: "green"} + + result = + Renderer.compile do + div class: @color do + @statement + end + end + + # html + expected = """ +
+ hello world +
+ + """ + + assert expected == result + end + + # 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 + + # # html + # expected = """ + #
+ # hello world + #
+ + # """ + + # assert expected == result + # end + + test "handles simple expression with @ assign" do + assigns = %{statement: "hello world"} + + result = + Renderer.compile do + div do + @statement + end + end + + # html + expected = """ +
+ hello world +
+ + """ + + assert expected == result + end + + test "handles multi line expression" do + assigns = %{names: ["alice", "bob", "carol"]} + + result = + Renderer.compile do + div do + for name <- @names do + span class: "name", do: name + end + end + end + + # html + expected = """ +
+ alice + bob + carol + +
+ + """ + + assert expected == result + end + + test "if expression" do + for val <- [true, false] do + assigns = %{value: val} + + result = + Renderer.compile do + div do + if @value do + span do: "true" + else + span do: "false" + end + end + end + + # html + expected = """ +
+ #{val} + +
+ + """ + + assert expected == result + end + end + + test "with expression" do + for val <- [true, false, "bobby"] do + assigns = %{value: val} + + result = + Renderer.compile do + div do + with false <- @value, + true <- "motch" not in ["lame", "not funny"] do + span do: "false" + else + true -> + span do: true + + _ -> + span do: "bobby" + end + end + end + + # html + expected = """ +
+ #{val} + +
+ + """ + + assert expected == result + end + end + + test "handles case expression" do + assigns = %{name: "alice"} + + # html + expected = """ +
+ alice is the best + +
+ + """ + + result = + Renderer.compile do + div do + case @name do + "bob" -> + span do: "bob is cool" + + "alice" -> + span id: "correct answer", do: "alice is the best" + + _ -> + span do: "everyone is lame" + end + end + end + + assert expected == result + end + + test "handles anonymous functions" do + assigns = %{names: ["alice", "bob", "carol"]} + + result = + Renderer.compile do + div do + Enum.map(@names, fn name -> + span class: "name", do: name + end) + end + end + + # html + expected = """ +
+ alice + + bob + + carol + + +
+ + """ + + assert expected == result + end + + def super_map(enumerable, func, _extra_args) do + Enum.map(enumerable, func) + end + + test "handles anonymous functions with subsequent args" do + assigns = %{names: ["alice", "bob", "carol"]} + + result = + Renderer.compile do + div do + super_map( + @names, + fn name -> + span class: "name", do: name + end, + "hello world" + ) + end + end + + # html + expected = """ +
+ alice + + bob + + carol + + +
+ + """ + + assert expected == result + end + + def basic_component(_assigns) do + temple do + div do + "I am a basic component" + end + end + end + + test "basic component" do + result = + Renderer.compile do + div do + c &basic_component/1 + end + end + + # html + expected = """ +
+
+ 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 + end + + test "component with default slot" do + result = + Renderer.compile do + div do + c &default_slot/1 do + span do: "i'm a slot" + end + end + end + + # html + expected = """ +
+
+ I am above the slot + i'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 + end + + test "component with a named slot" do + 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 + p do + "#{name}'s in the footer!" + end + end + end + end + end + + # heex + expected = """ +
+
+ motchy boi is above the slot + i'm a slot + +
+ +
+

+ motchy boi's in the footer! +

+ +
+ + +
+ + """ + + assert expected == result + end + end + + describe "special attribute stuff" do + test "class object syntax" do + result = + Renderer.compile do + div class: ["hello world": false, "text-red": true] do + "hello world" + end + end + + # html + expected = """ +
+ hello world +
+ + """ + + assert expected == result + end + + test "boolean attributes only emit correctly with truthy values" do + result = + Renderer.compile do + input type: "text", disabled: true, placeholder: "Enter some text..." + end + + # html + expected = """ + + """ + + assert expected == result + end + + test "boolean attributes don't emit with falsy values" do + result = + Renderer.compile do + input type: "text", disabled: false, placeholder: "Enter some text..." + end + + # html + expected = """ + + """ + + assert expected == result + end + end +end diff --git a/test/temple_test.exs b/test/temple_test.exs index fecbd14..132994c 100644 --- a/test/temple_test.exs +++ b/test/temple_test.exs @@ -1,512 +1,32 @@ defmodule TempleTest do use ExUnit.Case, async: true - use Temple - use Temple.Support.Utils + import Temple - test "renders an attribute on a div passed as a variable" do - result = - temple do - div class: "hello" do - div class: "hi" - end - end + describe "temple/1" do + test "works" do + assigns = %{name: "mitch"} - assert result == ~s""" -
-
-
-
- """ - end - - test "renders void element" do - result = - temple do - input name: "password" - end - - assert result == ~s{} - end - - test "renders a text node from the text keyword with siblings" do - result = - temple do - div class: "hello" do - "hi" - "foo" - end - end - - assert result == ~s""" -
- hi - foo -
- """ - end - - test "renders a variable text node as eex" do - result = - temple do - div class: "hello" do - foo - end - end - - assert result == ~s""" -
- <%= foo %> -
- """ - end - - test "renders an assign text node as eex" do - result = - temple do - div class: "hello" do - @foo - end - end - - assert result == ~s""" -
- <%= @foo %> -
- """ - end - - test "renders a match expression" do - result = - temple do - x = 420 - - div do - "blaze it" - end - end - - assert result == ~s""" - <% x = 420 %> -
- blaze it -
- """ - end - - test "renders a non-match expression" do - result = - temple do - IO.inspect(:foo) - - div do - "bar" - end - end - - assert result == ~s""" - <%= IO.inspect(:foo) %> -
- bar -
- """ - end - - test "renders an expression in attr as eex" do - result = - temple do - div class: foo <> " bar" - end - - assert result == - ~s| " bar")} %>>| - end - - test "renders an attribute on a div passed as a variable as eex" do - result = - temple do - div class: Enum.map([:one, :two], fn x -> x end) do - div class: "hi" - end - end - - assert result == - ~s""" - x end))} %>> -
-
- - """ - end - - test "renders a for comprehension as eex" do - result = - temple do - for x <- 1..5 do - div class: "hi" - end - end - - assert result == ~s""" - <%= for(x <- 1..5) do %> -
-
- <% end %> - """ - end - - test "renders an if expression as eex" do - result = - temple do - if true == false do - div class: "hi" - end - end - - assert result == ~s""" - <%= if(true == false) do %> -
-
- <% end %> - """ - end - - test "renders an if/else expression as eex" do - result = - temple do - if true == false do - div class: "hi" - else - div class: "haha" - end - end - - assert result == - ~s""" - <%= if(true == false) do %> -
-
- <% else %> -
-
- <% end %> - """ - end - - test "renders an unless expression as eex" do - result = - temple do - unless true == false do - div class: "hi" - end - end - - assert result == ~s""" - <%= unless(true == false) do %> -
-
- <% end %> - """ - end - - test "renders a case expression as eex" do - result = - temple do - case @foo do - :baz -> - some_component(form: @form) - end - end - - expected = ~S""" - <%= case(@foo) do %> - <% :baz -> %> - <%= some_component(form: @form) %> - <% end %> - """ - - assert result == expected - end - - test "renders multiline anonymous function with 1 arg before the function" do - result = - temple do - form_for(Routes.user_path(@conn, :create), fn f -> - "Name: " - text_input(f, :name) - end) - end - - assert result == - ~s""" - <%= form_for Routes.user_path(@conn, :create), fn f -> %> - Name: - <%= text_input(f, :name) %> - <% end %> - """ - end - - test "renders multiline anonymous functions with 2 args before the function" do - result = - temple do - form_for(@changeset, Routes.user_path(@conn, :create), fn f -> - "Name: " - text_input(f, :name) - end) - end - - assert result == - ~s""" - <%= form_for @changeset, Routes.user_path(@conn, :create), fn f -> %> - Name: - <%= text_input(f, :name) %> - <% end %> - """ - end - - test "renders multiline anonymous functions with complex nested children" do - result = - temple do - form_for(@changeset, Routes.user_path(@conn, :create), fn f -> - div do - "Name: " - text_input(f, :name) - end - end) - end - - assert result == - ~s""" - <%= form_for @changeset, Routes.user_path(@conn, :create), fn f -> %> -
- Name: - <%= text_input(f, :name) %> -
- <% end %> - """ - end - - test "renders multiline anonymous function with 3 arg before the function" do - result = - temple do - form_for(@changeset, Routes.user_path(@conn, :create), [foo: :bar], fn f -> - "Name: " - text_input(f, :name) - end) - end - - assert result == - ~s""" - <%= form_for @changeset, Routes.user_path(@conn, :create), [foo: :bar], fn f -> %> - Name: - <%= text_input(f, :name) %> - <% end %> - """ - end - - test "renders multiline anonymous function with 1 arg before the function and 1 arg after" do - result = - temple do - form_for( - @changeset, - fn f -> - "Name: " - text_input(f, :name) - end, - foo: :bar - ) - end - - assert result == - ~s""" - <%= form_for @changeset, fn f -> %> - Name: - <%= text_input(f, :name) %> - <% end, [foo: :bar] %> - """ - end - - test "tags prefixed with Temple. should be interpreted as temple tags" do - result = - temple do - div do - Temple.span do - "bob" + result = + temple do + div class: "hello" do + div class: "hi" do + @name + end end end - end - assert result == ~s""" -
- - bob - -
- """ - end + # heex + expected = """ +
+
+ mitch +
- test "can pass do as an arg instead of a block" do - result = - temple do - div class: "font-bold" do - "Hello, world" - end +
- div class: "font-bold", do: "Hello, world" - div do: "Hello, world" - end + """ - assert result == - ~s""" -
- Hello, world -
-
- Hello, world -
-
- Hello, world -
- """ - end - - test "for with 2 generators" do - result = - temple do - for x <- 1..5, y <- 6..10 do - div do: x - div do: y - end - end - - assert result == - ~s""" - <%= for(x <- 1..5, y <- 6..10) do %> -
- <%= x %> -
-
- <%= y %> -
- <% end %> - """ - end - - test "can pass an expression as assigns" do - result = - temple do - fieldset if true == false, do: [disabled: true], else: [] do - input type: "text" - end - end - - assert result == - ~s""" - > - - - """ - end - - test "can pass a variable as assigns" do - result = - temple do - fieldset foo_bar do - input type: "text" - end - end - - assert result == - ~s""" - > - - - """ - end - - test "can pass a function as assigns" do - result = - temple do - fieldset Foo.foo_bar() do - input type: "text" - end - end - - assert result == - ~s""" - > - - - """ - end - - test "hr tag works" do - assigns = %{foo: [class: "foofoo"]} - - result = - temple do - div do: "foo" - hr() - div do: "foo" - hr @foo - div do: "bar" - hr class: "foofoo" - div do: "bar" - end - - assert evaluate_template(result, assigns) == - ~s""" -
- foo -
-
-
- foo -
-
-
- bar -
-
-
- bar -
- """ - end - - test "boolean attributes" do - assigns = %{is_true: true, is_false: false} - - result = - temple do - input type: "text", disabled: true - input type: "text", disabled: false - - input type: "text", disabled: @is_true - input type: "text", disabled: @is_false - end - - assert evaluate_template(result, assigns) == - ~s{\n\n\n} - end - - test "class attribute can be pass a keyword list of class/boolean pairs" do - assigns = %{is_true: true, is_false: false} - - result = - temple do - div class: ["text-blue": @is_false, "text-red": true] do - "foo" - end - end - - assert evaluate_template(result, assigns) == ~s""" -
- foo -
- """ + assert expected == result + end end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 869559e..46f47bb 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1,6 @@ -ExUnit.start() +Code.put_compiler_option( + :parser_options, + Keyword.put(Code.get_compiler_option(:parser_options), :token_metadata, true) +) + +ExUnit.start(exclude: [skip: true]) diff --git a/test/whitespace_test.exs b/test/whitespace_test.exs deleted file mode 100644 index c663026..0000000 --- a/test/whitespace_test.exs +++ /dev/null @@ -1,46 +0,0 @@ -defmodule Temple.WhitespaceTest do - use ExUnit.Case, async: true - - import Temple - - alias Temple.Support.Utils - - test "only emits a single new line" do - result = - temple do - div class: "hello" do - span id: "foo" do - "Howdy, " - end - - div class: "hi" do - "Jim Bob" - end - - c WhoaNelly, foo: "bar" do - slot :silver do - "esketit" - end - end - end - end - |> Utils.append_new_line() - - expected = ~s""" -
- - Howdy, - -
- Jim Bob -
- <%= Temple.Component.__component__ WhoaNelly, [foo: "bar"] do %> - <% {:silver, %{}} -> %> - esketit - <% end %> -
- """ - - assert result == expected - end -end