Initial commit

This commit is contained in:
Mitchell Hanberg 2019-04-14 21:44:39 -04:00
commit 115f148864
11 changed files with 490 additions and 0 deletions

12
.formatter.exs Normal file
View file

@ -0,0 +1,12 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
export: [
locals_without_parens: [
htm: 1,
partial: :*,
flex: :*
div: :*
]
]
]

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
dsl-*.tar

21
README.md Normal file
View file

@ -0,0 +1,21 @@
# Dsl
**TODO: Add description**
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `dsl` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:dsl, "~> 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 [https://hexdocs.pm/dsl](https://hexdocs.pm/dsl).

30
config/config.exs Normal file
View file

@ -0,0 +1,30 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config
# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for
# third-party users, it should be done in your "mix.exs" file.
# You can configure your application as:
#
# config :dsl, key: :value
#
# and access this configuration in your application as:
#
# Application.get_env(:dsl, :key)
#
# You can also configure a third-party app:
#
# config :logger, level: :info
#
# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
# import_config "#{Mix.env()}.exs"

12
lib/dsl.ex Normal file
View file

@ -0,0 +1,12 @@
defmodule Dsl do
@moduledoc """
Documentation for Dsl.
"""
def __using__(_) do
quote do
import Kernel, except: [div: 2]
import Dsl.Html
end
end
end

164
lib/dsl/html.ex Normal file
View file

@ -0,0 +1,164 @@
defmodule Dsl.Html do
alias Phoenix.HTML
@nonvoid_elements ~w[
head title style script
noscript template
body section nav article aside h1 h2 h3 h4 h5 h6
header footer address main
p pre blockquote ol ul li dl dt dd figure figcaption div
a em strong small s cite q dfn abbr data time code var samp kbd
sub sup i b u mark ruby rt rp bdi bdo span
ins del
iframe object video audio canvas
map svg math
table caption colgroup tbody thead tfoot tr td th
form fieldset legend label button select datalist optgroup
option textarea output progress meter
details summary menuitem menu
]a
@void_elements ~w[
meta link base
area br col embed hr img input keygen param source track wbr
]a
defmacro htm(opts) do
quote do
htm(unquote(Keyword.get(opts, :safe, false)), unquote(opts[:do]))
end
end
defmacro htm(safe?, block) do
quote do
import Kernel, except: [div: 2]
{:ok, var!(buff, Dsl.Html)} = start_buffer([])
unquote(block)
markup = get_buffer(var!(buff, Dsl.Html))
:ok = stop_buffer(var!(buff, Dsl.Html))
if unquote(safe?) do
markup |> Enum.reverse() |> Enum.join("") |> HTML.html_escape()
else
markup |> Enum.reverse() |> Enum.join("")
end
end
end
for el <- @nonvoid_elements do
defmacro unquote(el)(attrs \\ [])
defmacro unquote(el)(attrs) do
el = unquote(el)
{inner, attrs} = Keyword.pop(attrs, :do, nil)
quote do
unquote(el)(unquote(attrs), unquote(inner))
end
end
defmacro unquote(el)(attrs, inner) do
el = unquote(el)
quote do
put_buffer(var!(buff, Dsl.Html), "<#{unquote(el)}#{unquote(compile_attrs(attrs))}>")
unquote(inner)
put_buffer(var!(buff, Dsl.Html), "</#{unquote(el)}>")
end
end
end
for el <- @void_elements do
defmacro unquote(el)(attrs \\ [])
defmacro unquote(el)(attrs) do
el = unquote(el)
quote do
put_buffer(
var!(buff, Dsl.Html),
"<#{unquote(el)}#{unquote(compile_attrs(attrs))}>"
)
end
end
end
defmacro text(text) do
quote do
put_buffer(var!(buff, Dsl.Html), to_string(unquote(text)))
end
end
defmacro partial(text), do: quote(do: text(unquote(text)))
defmacro deftag(name, do: block) do
quote do
defmacro unquote(name)(attrs \\ [])
defmacro unquote(name)(attrs) do
outer = unquote(Macro.escape(block))
name = unquote(name)
{inner, attrs} = Keyword.pop(attrs, :do, nil)
inner =
case inner do
{_, _, inner} ->
inner
nil ->
nil
end
quote do
unquote(name)(unquote(attrs), unquote(inner), unquote(outer))
end
end
defmacro unquote(name)(attrs, inner) do
outer = unquote(Macro.escape(block))
name = unquote(name)
quote do
unquote(name)(unquote(attrs), unquote(inner), unquote(outer))
end
end
defmacro unquote(name)(attrs, inner, outer) do
outer = unquote(Macro.escape(block))
{tag, meta, [old_attrs]} = outer
attrs = [
{:do, inner}
| Keyword.merge(old_attrs, attrs, fn _, two, three -> two <> " " <> three end)
]
outer = {tag, meta, [attrs]}
quote do
unquote(outer)
end
end
end
end
defp compile_attrs([]), do: ""
defp compile_attrs(attrs) do
for {name, value} <- attrs, into: "" do
name = name |> Atom.to_string() |> String.replace("_", "-")
" " <> name <> "=\"" <> to_string(value) <> "\""
end
end
def start_buffer(initial_buffer), do: Agent.start(fn -> initial_buffer end)
def put_buffer(buff, content), do: Agent.update(buff, &[content | &1])
def get_buffer(buff), do: Agent.get(buff, & &1)
def stop_buffer(buff), do: Agent.stop(buff)
end

27
mix.exs Normal file
View file

@ -0,0 +1,27 @@
defmodule Dsl.MixProject do
use Mix.Project
def project do
[
app: :dsl,
version: "0.1.0",
elixir: "~> 1.8",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:phoenix_html, "~> 2.13"}
]
end
end

6
mix.lock Normal file
View file

@ -0,0 +1,6 @@
%{
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"},
"phoenix_html": {:hex, :phoenix_html, "2.13.2", "f5d27c9b10ce881a60177d2b5227314fc60881e6b66b41dfe3349db6ed06cf57", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"plug": {:hex, :plug, "1.8.0", "9d2685cb007fe5e28ed9ac27af2815bc262b7817a00929ac10f56f169f43b977", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"},
"plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"},
}

190
test/dsl/html_test.exs Normal file
View file

@ -0,0 +1,190 @@
defmodule Dsl.HtmlTest do
use ExUnit.Case, async: true
import Dsl.Html
describe "non-void elements" do
test "renders two divs" do
result =
htm do
div()
div()
end
assert result == "<div></div><div></div>"
end
test "renders two els in the right order" do
result =
htm do
div()
span()
end
assert result == "<div></div><span></span>"
end
test "renders two divs that are rendered by a loop" do
result =
htm do
for _ <- 1..2 do
div()
end
end
assert result == "<div></div><div></div>"
end
test "renders two spans" do
result =
htm do
span()
span()
end
assert result == "<span></span><span></span>"
end
test "renders a div within a div" do
result =
htm do
div do
div()
end
end
assert result == "<div><div></div></div>"
end
test "renders an attribute on a div" do
result =
htm do
div class: "hello" do
div(class: "hi")
end
end
assert result == ~s{<div class="hello"><div class="hi"></div></div>}
end
test "renders multiple attributes on a div without block" do
result =
htm do
div(class: "hello", id: "12")
end
assert result == ~s{<div class="hello" id="12"></div>}
end
end
describe "void elements" do
test "renders an input" do
result =
htm do
input()
end
assert result == ~s{<input>}
end
test "renders an input with an attribute" do
result =
htm do
input(type: "number")
end
assert result == ~s{<input type="number">}
end
end
describe "escaping" do
test "marks as safe" do
{safe?, result} =
htm safe: true do
div()
end
assert safe? == :safe
assert IO.iodata_to_binary(result) == ~s{&lt;div&gt;&lt;/div&gt;}
end
end
describe "data attributes" do
test "can have one data attributes" do
result =
htm do
div(data_controller: "stimulus-controller")
end
assert result == ~s{<div data-controller="stimulus-controller"></div>}
end
test "can have multiple data attributes" do
result =
htm do
div(data_controller: "stimulus-controller", data_target: "stimulus-target")
end
assert result ==
~s{<div data-controller="stimulus-controller" data-target="stimulus-target"></div>}
end
end
defmodule CustomTag do
deftag :flex do
div(class: "flex")
end
end
describe "custom tags" do
test "defines a basic tag that acts as partial" do
import CustomTag
result =
htm do
flex()
end
assert result == ~s{<div class="flex"></div>}
end
test "defines a tag that takes children" do
import CustomTag
result =
htm do
flex do
div()
div()
end
end
assert result == ~s{<div class="flex"><div></div><div></div></div>}
end
test "defines a tag that has attributes" do
import CustomTag
result =
htm do
flex class: "justify-between", id: "king"
end
assert result =~ ~s{class="flex justify-between"}
assert result =~ ~s{id="king"}
end
test "defines a tag that has attributes AND children" do
import CustomTag
result =
htm do
flex class: "justify-between" do
div()
div()
end
end
assert result == ~s{<div class="flex justify-between"><div></div><div></div></div>}
end
end
end

3
test/dsl_test.exs Normal file
View file

@ -0,0 +1,3 @@
defmodule DslTest do
use ExUnit.Case
end

1
test/test_helper.exs Normal file
View file

@ -0,0 +1 @@
ExUnit.start()