├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── lib
├── eml.ex
└── eml
│ ├── compiler.ex
│ ├── element.ex
│ ├── element
│ └── generator.ex
│ ├── encoder.ex
│ ├── errors.ex
│ ├── html.ex
│ ├── html
│ ├── compiler.ex
│ └── parser.ex
│ ├── parser.ex
│ └── query.ex
├── mix.exs
└── test
├── eml_test.exs
└── test_helper.exs
/.gitignore:
--------------------------------------------------------------------------------
1 | /_build
2 | /deps
3 | erl_crash.dump
4 | *.ez
5 | *~
6 | *#
7 | .#*
8 | /doc/
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: elixir
2 | elixir:
3 | - 1.5.2
4 | otp_release:
5 | - 20.1.2
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## v0.9.0-dev
4 | * Enhancements
5 | * Replaced quoted expressions with assign handlers and runtime function calls
6 | * `use Eml` can now be used to set compile options and elements to import
7 | * Added `Eml.collect/3`
8 | * Added `Eml.Query`, a small dsl for transforming and collecting Eml nodes
9 |
10 | * Bug fixed
11 | * Fixed typo in Eml.Element.apply_template/1
12 | * Suppress new underscore variable compile warning
13 |
14 | * Backwards incompatible changes
15 | * Quoted expressions are now invalid. Use assign handlers or runtime function calls instead.
16 |
17 | ## v0.8.0-dev
18 | * Enhancements
19 | * Much richer templates by using quoted expressions as replacement for parameters
20 | * Removed all generic functionality from the html parser and renderer,
21 | which makes it easier to implement other parsers and renderers
22 | * Added `{ :safe, String.t }` as a new content type which you can use when you need to add content to an element that should not get escaped
23 | * Added `transform` option to `Eml.render/3`
24 | * Added `casing` option to `Eml.Element.Generator.__using__` to control the casing of tags for generated elements
25 | * Added the `&` capture operator as shortcut for quoted expressions
26 | * Introduced components and fragments
27 | * Added `Eml.match?/2` macro
28 | * Added `Eml.Element.put_template/3`, `Eml.Element.remove_template/1` and `Eml.apply_template/1` functions
29 | * Added `any` element as a catch all tag macro to be used in a match
30 | * Added `Eml.escape/1` and `Eml.unescape/1` functions that recursively escape or unescape content.
31 | * `Eml.Compiler.compile` returns results by default as `{ :safe, result }` so that
32 | they can be easily added to other elements, witout getting escaped
33 |
34 | * Bug fixes
35 | * Using element macro's in a match had different confusing behaviour
36 |
37 | * Backwards incompatible changes
38 | * Removed `Eml.Template` and `Eml.Parameter` in favor of quoted expressions
39 | * Replaced `Eml.precompile` with `Eml.template` and `Eml.template_fn`
40 | * Changed names of render options: :lang => :renderer, :quote => :quotes
41 | * Importing all HTML element macro's is now done via `use Eml.HTML` instead of `use Eml.Language.HTML`
42 | * Renamed `Eml.Data` protocol to `Eml.Encoder` and removed `Eml.to_content`
43 | * Data conversion is now only done during compiling and not when adding data to elements.
44 | * Removed query functions
45 | * Removed transform functions
46 | * Removed `defeml` and `defhtml` macros
47 | * Removed `Eml.unpackr/1` and `Eml.funpackr/1`. `Eml.unpack` now always unpacks recursively
48 | * Removed `Eml.element?/1`, `Eml.empty?/1` and `Eml.type/1` functions.
49 | * Removed all previous helper functions from `Eml.Element`
50 | * Removed `Eml.compile/2` in favor of `Eml.Compiler.compile/2`
51 | * Parser doesn't automatically converts entities anymore. Use `Eml.unescape/1` instead.
52 |
53 | ## v0.7.1
54 | * Enhancements
55 | * Added unit tests that test escaping and enity parsing
56 | * Documentation additions and corrections
57 |
58 | * Bug fixes
59 | * Single and double quotes in attributes now should get properly escaped
60 |
61 | * Backwards incompatible changes
62 |
63 | ## v0.7.0
64 | * Enhancements
65 | * It's now easy to provide conversions for custom data types by using the new `Eml.Data` protocol
66 | * Better separation of concerns by removing all data conversions from parsing
67 |
68 | * Bug fixes
69 | * Some type fixes in the Eml.Language behaviour
70 |
71 | * Backwards incompatible changes
72 | * Renamed `Eml.Language.Html` to `Eml.Language.HTML` in order to be compliant with Elixir's naming conventions
73 | * The undocumented `Eml.parse/4` function is now replaced by `Eml.to_content/3`
74 | * The `Eml.Parsable` protocol is replaced by `Eml.Data`, which is now strictly used for converting various
75 | data types into valid Eml nodes.
76 | * `Eml.parse/2` now always returns a list again, because the type
77 | conversions are now done by `Eml.to_content/3` and consequently you can't force
78 | `Eml.parse/2` to return a list anymore, which would make it dangerous to
79 | use when parsing html partials where you don't know the nummer of nodes.
80 | * `Eml.parse/2`, `Eml.render/3` and `Eml.compile/3` now always raise an exception on error.
81 | Removed `Eml.parse!/2`, `Eml.render!/3` and `Eml.compile!/3`. Reason is that it was hard
82 | to guarantee that those functions never raised an error and it simplifies Eml's API
83 | * Removed `Eml.render_to_eex/3`, `Eml.render_to_eex!/3`, `Eml.compile_to_eex/3` and `Eml.compile_to_eex!/3`,
84 | as they didn't provide much usefulness
85 |
86 | ## v0.6.0
87 |
88 | * Enhancements
89 | * Introduced `use Eml.Language.Html` as prefered way of defining markup
90 | * Restructured README.md and added new content about precompiling
91 | * It's now possible to pass content as the first argument of an element macro, ie. `div "Hello world!"`
92 | * Added `Eml.compile_to_eex/3` and `Eml.render_to_eex/3`
93 |
94 | * Bug fixes
95 | * Documentation corrections.
96 | * Removed duplicate code in `Eml.defhtml/2`
97 | * Type specification fixes
98 | * `Eml.parse/2` in some cases returned weird results when the input is a list
99 | * Template bindings were not correctly parsed
100 |
101 | * Backwards incompatible changes
102 | * Removed `Eml.eml/2` in favor of `use Eml.Language.Html`
103 | * `Eml.parse/2` now returns results in the form of `{ :ok, res }`, in order to be consistent with render and compile functions
104 | * Unless the input of `Eml.parse/2` is a list, if the result is a single element, `Eml.parse/2` now just returns the single element
105 | instead of always wrapping the result in a list
106 |
107 |
108 | ## v0.5.0
109 |
110 | * Enhancements
111 | * Added documentation for all public modules
112 | * Added some meta data to mix.exs for ex_doc
113 | * Added `Eml.compile!/3`
114 |
115 | * Bug fixes
116 | * Lots of documentation corrections
117 | * Some type fixes
118 |
119 | * Backwards incompatible changes
120 | * Renamed all read function to parse
121 | * Renamed all write functions to render
122 | * Removed `Eml.write_file` and `Eml.write_file!`
123 | * Removed `Eml.read_file` and `Eml.read_file!`
124 | * Parameters with the same name just reuse the same bounded value, instead of popping a list of bounded values
125 | * Renamed the module `Eml.Markup` to `Eml.Element` and the module `Eml.Language.Html.Markup` to `Eml.Language.Html.Elements`
126 | * `Eml.compile` now returns { :ok, template } on success instead of just the template in order to be consistent with Eml's
127 | render and parse functions
128 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2013-2015 Vincent Siliakus
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/zambal/eml)
2 |
3 | # Eml
4 |
5 | ## Markup for developers
6 |
7 | ### What is it?
8 | Eml makes markup a first class citizen in Elixir. It provides a flexible and
9 | modular toolkit for generating, parsing and manipulating markup. It's main focus
10 | is html, but other markup languages could be implemented as well.
11 |
12 | To start off:
13 |
14 | This piece of code
15 | ```elixir
16 | use Eml.HTML
17 |
18 | name = "Vincent"
19 | age = 36
20 |
21 | div class: "person" do
22 | div do
23 | span "name: "
24 | span name
25 | end
26 | div do
27 | span "age: "
28 | span age
29 | end
30 | end |> Eml.compile
31 | ```
32 |
33 | produces
34 | ```html
35 |
`. An element should always be written
461 | as `
`, or `
`. However, explicit exceptions are made for void
462 | elements that are expected to never have any child elements.
463 |
464 | The bottom line is that whenever the parser fails to parse back generated
465 | html from Eml, it is a bug and please report it. Whenever it fails to
466 | parse some external html, I'm still interested to hear about it, but I
467 | can't guarantee I can or will fix it.
468 |
--------------------------------------------------------------------------------
/lib/eml.ex:
--------------------------------------------------------------------------------
1 | defmodule Eml do
2 | @moduledoc """
3 | Eml makes markup a first class citizen in Elixir. It provides a
4 | flexible and modular toolkit for generating, parsing and
5 | manipulating markup. It's main focus is html, but other markup
6 | languages could be implemented as well.
7 |
8 | To start off:
9 |
10 | This piece of code
11 | ```elixir
12 | use Eml.HTML
13 |
14 | name = "Vincent"
15 | age = 36
16 |
17 | div class: "person" do
18 | div do
19 | span "name: "
20 | span name
21 | end
22 | div do
23 | span "age: "
24 | span age
25 | end
26 | end |> Eml.compile
27 | ```
28 |
29 | produces
30 | ```html
31 |
32 |
33 | name:
34 | Vincent
35 |
36 |
37 | age:
38 | 36
39 |
40 |
41 | ```
42 |
43 | The functions and macro's in the `Eml` module cover most of
44 | Eml's public API.
45 | """
46 |
47 | alias Eml.Element
48 |
49 | @default_elements Eml.HTML
50 | @default_parser Eml.HTML.Parser
51 |
52 | @type t :: Eml.Encoder.t | [Eml.Encoder.t] | [t]
53 | @type node_primitive :: String.t | { :safe, String.t } | Macro.t | Eml.Element.t
54 |
55 | @doc """
56 | Define a template function that compiles eml to a string during compile time.
57 |
58 | Eml uses the assigns extension from `EEx` for parameterizing templates. See
59 | the `EEx` docs for more info about them. The function that the template macro
60 | defines accepts optionally any Dict compatible dictionary as argument for
61 | binding values to assigns.
62 |
63 | ### Example:
64 |
65 | iex> defmodule MyTemplates1 do
66 | ...> use Eml
67 | ...> use Eml.HTML
68 | ...>
69 | ...> template example do
70 | ...> div id: "example" do
71 | ...> span @text
72 | ...> end
73 | ...> end
74 | ...> end
75 | iex> MyTemplates.example text: "Example text"
76 | {:safe, "
Example text
"}
77 |
78 |
79 | Eml templates provides two ways of executing logic during runtime. By
80 | providing assigns handlers to the optional `funs` dictionary, or by calling
81 | external functions during runtime with the `&` operator.
82 |
83 | ### Example:
84 |
85 | iex> defmodule MyTemplates2 do
86 | ...> use Eml
87 | ...> use Eml.HTML
88 | ...>
89 | ...> template assigns_handler,
90 | ...> text: &String.upcase/1 do
91 | ...> div id: "example1" do
92 | ...> span @text
93 | ...> end
94 | ...> end
95 | ...>
96 | ...> template external_call do
97 | ...> body &assigns_handler(text: @example_text)
98 | ...> end
99 | ...> end
100 | iex> MyTemplates.assigns_handler text: "Example text"
101 | {:safe, "
EXAMPLE TEXT
"}
102 | iex> MyTemplates.exernal_call example_text: "Example text"
103 | {:safe, "
EXAMPLE TEXT
"}
104 |
105 |
106 | Templates are composable, so they are allowed to call other templates. The
107 | only catch is that it's not possible to pass an assign to another template
108 | during precompilation. The reason for this is that the logic in a template is
109 | executed the moment the template is called, so if you would pass an assign
110 | during precompilation, the logic in a template would receive this assign
111 | instead of its result, which is only available during runtime. This all means
112 | that when you for example want to pass an assign to a nested template, the
113 | template should be prefixed with the `&` operator, or in other words, executed
114 | during runtime.
115 |
116 | ### Example
117 |
118 | iex> defmodule T1 do
119 | ...> template templ1,
120 | ...> num: &(&1 + &1) do
121 | ...> div @num
122 | ...> end
123 | ...> end
124 |
125 | iex> template templ2 do
126 | ...> h2 @title
127 | ...> templ1(num: @number) # THIS GENERATES A COMPILE TIME ERROR
128 | ...> &templ1(num: @number) # THIS IS OK
129 | ...> end
130 |
131 | Note that because the body of a template is evaluated at compiletime, it's
132 | not possible to call other functions from the same module without using `&`
133 | operator.
134 |
135 | Instead of defining a do block, you can also provide a path to a file with the
136 | `:file` option.
137 |
138 | ### Example:
139 |
140 | iex> File.write! "test.eml.exs", "div @number"
141 | iex> defmodule MyTemplates3 do
142 | ...> use Eml
143 | ...> use Eml.HTML
144 | ...>
145 | ...> template from_file, file: "test.eml.exs"
146 | ...> end
147 | iex> File.rm! "test.eml.exs"
148 | iex> MyTemplates3.from_file number: 42
149 | {:safe, "
42
"}
150 |
151 | """
152 | defmacro template(name, funs \\ [], do_block) do
153 | do_template(name, funs, do_block, __CALLER__, false)
154 | end
155 |
156 | @doc """
157 | Define a private template.
158 |
159 | Same as `template/3` except that it defines a private function.
160 | """
161 | defmacro templatep(name, funs \\ [], do_block) do
162 | do_template(name, funs, do_block, __CALLER__, true)
163 | end
164 |
165 | defp do_template(tag, funs, do_block, caller, private) do
166 | { tag, _, _ } = tag
167 | def_call = if private, do: :defp, else: :def
168 | template = Eml.Compiler.precompile(caller, do_block)
169 | quote do
170 | unquote(def_call)(unquote(tag)(var!(assigns))) do
171 | _ = var!(assigns)
172 | var!(funs) = unquote(funs)
173 | _ = var!(funs)
174 | unquote(template)
175 | end
176 | end
177 | end
178 |
179 | @doc """
180 | Define a template as an anonymous function.
181 |
182 | Same as `template/3`, except that it defines an anonymous function.
183 |
184 | ### Example
185 | iex> t = template_fn names: fn names ->
186 | ...> for n <- names, do: li n
187 | ...> end do
188 | ...> ul @names
189 | ...> end
190 | iex> t.(names: ~w(john james jesse))
191 | {:safe, "
"}
192 |
193 | """
194 | defmacro template_fn(funs \\ [], do_block) do
195 | template = Eml.Compiler.precompile(__CALLER__, do_block)
196 | quote do
197 | fn var!(assigns) ->
198 | _ = var!(assigns)
199 | var!(funs) = unquote(funs)
200 | _ = var!(funs)
201 | unquote(template)
202 | end
203 | end
204 | end
205 |
206 | @doc """
207 | Define a component element
208 |
209 | Components in Eml are a special kind of element that inherit functionality
210 | from templates. Like templates, everything within the do block gets
211 | precompiled, except assigns and function calls prefixed with the `&`
212 | operator. Defined attributes on a component can be accessed as assigns, just
213 | like with templates. Content can be accessed via the the special assign
214 | `__CONTENT__`. However, since the type of a component is `Eml.Element.t`,
215 | they can be queried and transformed, just like normal Eml elements.
216 |
217 | See `template/3` for more info about composability, assigns, runtime logic and
218 | accepted options.
219 |
220 | ### Example
221 |
222 | iex> use Eml
223 | iex> use Eml.HTML
224 | iex> defmodule ElTest do
225 | ...>
226 | ...> component my_list,
227 | ...> __CONTENT__: fn content ->
228 | ...> for item <- content do
229 | ...> li do
230 | ...> span "* "
231 | ...> span item
232 | ...> span " *"
233 | ...> end
234 | ...> end
235 | ...> end do
236 | ...> ul [class: @class], @__CONTENT__
237 | ...> end
238 | ...>
239 | ...> end
240 | iex> import ElTest
241 | iex> el = my_list class: "some-class" do
242 | ...> "Item 1"
243 | ...> "Item 2"
244 | ...> end
245 | #my_list<%{class: "some-class"} ["Item 1", "Item 2"]>
246 | iex> Eml.compile(el)
247 | "
"
248 | """
249 | defmacro component(tag, funs \\ [], do_block) do
250 | do_template_element(tag, funs, do_block, __CALLER__, false)
251 | end
252 |
253 | @doc """
254 | Define a fragment element
255 |
256 | Fragments in Eml are a special kind of element that inherit functionality from
257 | templates. Like templates, everything within the do block gets precompiled,
258 | except assigns. Defined attributes on a component can be accessed as assigns,
259 | just like with templates. Content can be accessed via the the special assign
260 | `__CONTENT__`. However, since the type of a fragment is `Eml.Element.t`, they
261 | can be queried and transformed, just like normal Eml elements.
262 |
263 | The difference between components and fragments is that fragments are without
264 | any logic, so assign handlers or the `&` operator are not allowed in a
265 | fragment definition.
266 |
267 | The reason for their existence is easier composability and performance,
268 | because unlike templates and components, it is allowed to pass assigns to
269 | fragments during precompilation. This is possible because fragments don't
270 | contain any logic.
271 |
272 | ### Example
273 |
274 | iex> use Eml
275 | nil
276 | iex> use Eml.HTML
277 | nil
278 | iex> defmodule ElTest do
279 | ...>
280 | ...> fragment basic_page do
281 | ...> html do
282 | ...> head do
283 | ...> meta charset: "UTF-8"
284 | ...> title @title
285 | ...> end
286 | ...> body do
287 | ...> @__CONTENT__
288 | ...> end
289 | ...> end
290 | ...> end
291 | ...>
292 | ...> end
293 | {:module, ElTest, ...}
294 | iex> import ElTest
295 | nil
296 | iex> page = basic_page title: "Hello!" do
297 | ...> div "Hello World"
298 | ...> end
299 | #basic_page<%{title: "Hello!!"} [#div<"Hello World">]>
300 | iex> Eml.compile page
301 | "\n
Hello!!Hello World
"
302 | """
303 | defmacro fragment(tag, do_block) do
304 | do_template_element(tag, nil, do_block, __CALLER__, true)
305 | end
306 |
307 | defp do_template_element(tag, funs, do_block, caller, fragment?) do
308 | { tag, _, _ } = tag
309 | template = Eml.Compiler.precompile(caller, Keyword.merge(do_block, fragment: fragment?))
310 | template_tag = (Atom.to_string(tag) <> "__template") |> String.to_atom()
311 | template_type = if fragment?, do: :fragment, else: :component
312 | funs = unless fragment? do
313 | quote do
314 | var!(funs) = unquote(funs)
315 | _ = var!(funs)
316 | end
317 | end
318 | quote do
319 | @doc false
320 | def unquote(template_tag)(var!(assigns)) do
321 | _ = var!(assigns)
322 | unquote(funs)
323 | unquote(template)
324 | end
325 | defmacro unquote(tag)(content_or_attrs, maybe_content \\ nil) do
326 | tag = unquote(tag)
327 | template_tag = unquote(template_tag)
328 | template_type = unquote(template_type)
329 | in_match = Macro.Env.in_match?(__CALLER__)
330 | { attrs, content } = Eml.Element.Generator.extract_content(content_or_attrs, maybe_content, in_match)
331 | if in_match do
332 | quote do
333 | %Eml.Element{tag: unquote(tag), attrs: unquote(attrs), content: unquote(content)}
334 | end
335 | else
336 | quote do
337 | %Eml.Element{tag: unquote(tag),
338 | attrs: Enum.into(unquote(attrs), %{}),
339 | content: List.wrap(unquote(content)),
340 | template: &unquote(__MODULE__).unquote(template_tag)/1,
341 | type: unquote(template_type)}
342 | end
343 | end
344 | end
345 | end
346 | end
347 |
348 | @doc """
349 | Parses data and converts it to eml
350 |
351 | How the data is interpreted depends on the `parser` argument.
352 | The default value is `Eml.HTML.Parser', which means that
353 | strings are parsed as html.
354 |
355 | In case of error, raises an Eml.ParseError exception.
356 |
357 | ### Examples:
358 |
359 | iex> Eml.parse("
The title
")
360 | [#body<[#h1<%{id: "main-title"} "The title">]>]
361 | """
362 | @spec parse(String.t, Keyword.t) :: [t]
363 | def parse(data, opts \\ [])
364 |
365 | def parse(data, opts) when is_binary(data) do
366 | parser = opts[:parser] || @default_parser
367 | parser.parse(data, opts)
368 | end
369 | def parse(data, _) do
370 | raise Eml.ParseError, message: "Bad argument: #{inspect data}"
371 | end
372 |
373 | @doc """
374 | Compiles eml content with the specified markup compiler, which is html by default.
375 |
376 | The accepted options are:
377 |
378 | * `:compiler` - The compiler to use, by default `Eml.HTML.Compiler`
379 | * `:quotes` - The type of quotes used for attribute values. Accepted values are `:single` (default) and `:double`.
380 | * `:transform` - A function that receives every node just before it get's compiled. Same as using `transform/2`,
381 | but more efficient, since it's getting called during the compile pass.
382 | * `:escape` - Automatically escape strings, default is `true`.
383 |
384 | In case of error, raises an Eml.CompileError exception.
385 |
386 | ### Examples:
387 |
388 | iex> Eml.compile(body(h1([id: "main-title"], "A title")))
389 | "
A title
"
390 |
391 | iex> Eml.compile(body(h1([id: "main-title"], "A title")), quotes: :double)
392 | "
A title
"
393 |
394 | iex> Eml.compile(p "Tom & Jerry")
395 | "
Tom & Jerry
"
396 |
397 | """
398 | @spec compile(t, Dict.t) :: String.t
399 | def compile(content, opts \\ [])
400 | def compile({ :safe, string }, _opts) do
401 | string
402 | end
403 | def compile(content, opts) do
404 | case Eml.Compiler.compile(content, opts) do
405 | { :safe, string } ->
406 | string
407 | _ ->
408 | raise Eml.CompileError, message: "Bad argument: #{inspect content}"
409 | end
410 | end
411 |
412 | @doc """
413 | Recursively transforms `eml` content.
414 |
415 | It traverses all nodes of the provided eml tree. The provided transform
416 | function will be evaluated for every node `transform/3` encounters. Parent
417 | nodes will be transformed before their children. Child nodes of a parent will
418 | be evaluated before moving to the next sibling.
419 |
420 | When the provided function returns `nil`, the node will be removed from the
421 | eml tree.
422 |
423 | Note that because parent nodes are evaluated before their children, no
424 | children will be evaluated if the parent is removed.
425 |
426 | ### Examples:
427 |
428 | iex> e = div do
429 | ...> span [id: "inner1", class: "inner"], "hello "
430 | ...> span [id: "inner2", class: "inner"], "world"
431 | ...> end
432 | #div<[#span<%{id: "inner1", class: "inner"} "hello ">,
433 | #span<%{id: "inner2", class: "inner"} "world">]>
434 |
435 | iex> Eml.transform(e, fn
436 | ...> span(_) -> "matched"
437 | ...> node -> node
438 | ...> end)
439 | #div<["matched", "matched"]>
440 |
441 | iex> transform(e, fn node ->
442 | ...> IO.inspect(node)
443 | ...> node
444 | ...> end)
445 | #div<[#span<%{class: "inner", id: "inner1"} "hello ">,
446 | #span<%{class: "inner", id: "inner2"} "world">]>
447 | #span<%{class: "inner", id: "inner1"} "hello ">
448 | "hello "
449 | #span<%{class: "inner", id: "inner2"} "world">
450 | "world"
451 | #div<[#span<%{class: "inner", id: "inner1"} "hello ">,
452 | #span<%{class: "inner", id: "inner2"} "world">]>
453 | """
454 | @spec transform(t, (t -> t)) :: t | nil
455 | def transform(nil, _fun) do
456 | nil
457 | end
458 | def transform(eml, fun) when is_list(eml) do
459 | for node <- eml, t = transform(node, fun), do: t
460 | end
461 | def transform(node, fun) do
462 | case fun.(node) do
463 | %Element{content: content} = node ->
464 | %Element{node| content: transform(content, fun)}
465 | node ->
466 | node
467 | end
468 | end
469 |
470 | @doc """
471 | Recursively reduces a tree of nodes
472 |
473 | ### Example
474 |
475 | iex> tree = div do
476 | ...> span 1
477 | ...> span 2
478 | ...> end
479 | iex> list = [tree, tree]
480 |
481 | iex> Eml.collect(list, [], fn node, acc ->
482 | ...> if is_integer(node) do
483 | ...> [node | acc]
484 | ...> else
485 | ...> acc
486 | ...> end
487 | ...> end)
488 | [2, 1, 2, 1]
489 | """
490 | @spec collect(t, term, (t, term -> term)) :: term
491 | def collect(eml, acc \\ %{}, fun)
492 | def collect(eml, acc, fun) when is_list(eml) do
493 | Enum.reduce(eml, acc, &collect(&1, &2, fun))
494 | end
495 | def collect(%Element{} = eml, acc, fun) do
496 | Enum.reduce(eml, acc, fun)
497 | end
498 | def collect(eml, acc, fun) do
499 | fun.(eml, acc)
500 | end
501 |
502 |
503 | @doc """
504 | Match on element tag, attributes, or content
505 |
506 | Implemented as a macro.
507 | ### Examples:
508 |
509 | iex> use Eml
510 | iex> use Eml.HTML
511 | iex> node = section [id: "my-section"], [div([id: "some_id"], "Some content"), div([id: "another_id"], "Other content")]
512 | iex> Eml.match?(node, attrs: %{id: "my-section"})
513 | true
514 | iex> Eml.match?(node, tag: :div)
515 | false
516 | iex> Enum.filter(node, &Eml.match?(&1, tag: :div))
517 | [#div<%{id: "some_id"} "Some content">, #div<%{id: "another_id"}
518 | "Other content">]
519 | iex> Eml.transform(node, fn node ->
520 | ...> if Eml.match?(node, content: "Other content") do
521 | ...> put_in(node.content, "New content")
522 | ...> else
523 | ...> node
524 | ...> end
525 | ...> end)
526 | #section<%{id: "my-section"}
527 | [#div<%{id: "some_id"} "Some content">, #div<%{id: "another_id"}
528 | "New content">]>
529 | """
530 | defmacro match?(node, opts \\ []) do
531 | tag = opts[:tag] || quote do: _
532 | attrs = opts[:attrs] || quote do: _
533 | content = opts[:content] || quote do: _
534 | quote do
535 | case unquote(node) do
536 | %Eml.Element{tag: unquote(tag), attrs: unquote(attrs), content: unquote(content)} ->
537 | true
538 | _ ->
539 | false
540 | end
541 | end
542 | end
543 |
544 | @doc """
545 | Extracts a value recursively from content
546 |
547 | ### Examples
548 |
549 | iex> Eml.unpack [42]
550 | bm 42
551 |
552 | iex> Eml.unpack 42
553 | 42
554 |
555 | iex> Eml.unpack(div "hallo")
556 | "hallo"
557 |
558 | iex> Eml.unpack Eml.unpack(div(span("hallo")))
559 | "hallo"
560 |
561 | iex> Eml.unpack div(span(42))
562 | 42
563 |
564 | iex> Eml.unpack div([span("Hallo"), span(" world")])
565 | ["Hallo", " world"]
566 |
567 | """
568 | @spec unpack(t) :: t
569 | def unpack(%Element{content: content}) do
570 | unpack(content)
571 | end
572 | def unpack([node]) do
573 | unpack(node)
574 | end
575 | def unpack(content) when is_list(content) do
576 | for node <- content, do: unpack(node)
577 | end
578 | def unpack({ :safe, node }) do
579 | node
580 | end
581 | def unpack(node) do
582 | node
583 | end
584 |
585 | @doc """
586 | Escape content
587 |
588 | ### Examples
589 |
590 | iex> escape "Tom & Jerry"
591 | "Tom & Jerry"
592 | iex> escape div span("Tom & Jerry")
593 | #div<[#span<["Tom & Jerry"]>]>
594 | """
595 | @spec escape(t) :: t
596 | defdelegate escape(eml), to: Eml.Compiler
597 |
598 | @doc """
599 | Unescape content
600 |
601 | ### Examples
602 |
603 | iex> unescape "Tom & Jerry"
604 | "Tom & Jerry"
605 | iex> unescape div span("Tom & Jerry")
606 | #div<[#span<["Tom & Jerry"]>]>
607 | """
608 | @spec unescape(t) :: t
609 | defdelegate unescape(eml), to: Eml.Parser
610 |
611 | # use Eml
612 | @doc """
613 | Import macro's from this module and alias `Eml.Element`.
614 |
615 | Accepts the following options:
616 |
617 | * `:compile` - Set dcompile options as a Keyword list for all templates,
618 | components and fragments that are defined in the module where `use Eml` is
619 | invoked. See `Eml.compile/2` for all available options.
620 | * `:elements` - Which elements to import in the current scope. Accepts a
621 | module, or list of modules and defaults to `Eml.HTML`. When you don't want
622 | to import any elements, set to `nil` or `false`.
623 | """
624 | defmacro __using__(opts) do
625 | use_elements = if mods = Keyword.get(opts, :elements, @default_elements) do
626 | for mod <- List.wrap(mods) do
627 | quote do: use unquote(mod)
628 | end
629 | end
630 | compile_opts = Keyword.get(opts, :compile, [])
631 | if mod = __CALLER__.module do
632 | Module.put_attribute(mod, :eml_compile, compile_opts)
633 | end
634 | quote do
635 | unquote(use_elements)
636 | import Eml, only: [
637 | template: 2, template: 3,
638 | templatep: 2, templatep: 3,
639 | template_fn: 1, template_fn: 2,
640 | component: 2, component: 3,
641 | fragment: 2
642 | ]
643 | end
644 | end
645 | end
646 |
--------------------------------------------------------------------------------
/lib/eml/compiler.ex:
--------------------------------------------------------------------------------
1 | defmodule Eml.Compiler do
2 | @moduledoc """
3 | Various helper functions for implementing an Eml compiler.
4 | """
5 |
6 | @type chunk :: String.t | { :safe, String.t } | Macro.t
7 |
8 | # Options helper
9 |
10 | @default_opts [escape: true,
11 | transform: nil,
12 | fragment: false,
13 | compiler: Eml.HTML.Compiler]
14 |
15 | defp new_opts(opts), do: Keyword.merge(@default_opts, opts)
16 |
17 | # API
18 |
19 | @doc """
20 | Compiles eml to a string, or a quoted expression when the input contains
21 | contains quoted expressions too.
22 |
23 | Accepts the same options as `Eml.render/3`
24 |
25 | In case of error, raises an Eml.CompileError exception.
26 |
27 | ### Examples:
28 |
29 | iex> Eml.Compiler.compile(body(h1(id: "main-title")))
30 | {:safe, "
"
31 |
32 | """
33 |
34 | @spec compile(Eml.t, Keyword.t) :: { :safe, String.t } | Macro.t
35 | def compile(eml, opts \\ []) do
36 | opts = new_opts(opts)
37 | opts = Keyword.merge(opts[:compiler].opts(), opts)
38 | compile_node(eml, opts, []) |> to_result(opts)
39 | end
40 |
41 | @spec precompile(Macro.Env.t, Keyword.t) :: { :safe, String.t } | Macro.t
42 | def precompile(env \\ %Macro.Env{}, opts) do
43 | mod = env.module
44 | mod_opts = if mod && Module.open?(mod),
45 | do: Module.get_attribute(mod, :eml_compile) |> Macro.escape(),
46 | else: []
47 | opts = Keyword.merge(mod_opts, opts)
48 | { file, opts } = Keyword.pop(opts, :file)
49 | { block, opts } = Keyword.pop(opts, :do)
50 | ast = if file do
51 | string = File.read!(file)
52 | Code.string_to_quoted!(string, file: file, line: 1)
53 | else
54 | block
55 | end |> prewalk(opts[:fragment])
56 | { expr, _ } = Code.eval_quoted(ast, [], env)
57 | { opts, _ } = Code.eval_quoted(opts, [], env)
58 | compile(expr, opts)
59 | end
60 |
61 | # Content parsing
62 |
63 | @spec compile_node(Eml.t, map, [chunk]) :: [chunk]
64 | def compile_node(list, opts, chunks) when is_list(list) do
65 | Enum.reduce(list, chunks, fn node, chunks ->
66 | compile_node(node, opts, chunks)
67 | end)
68 | end
69 |
70 | def compile_node(node, opts, chunks) do
71 | node = node
72 | |> maybe_transform(opts)
73 | |> Eml.Encoder.encode()
74 | case opts[:compiler].compile_node(node, opts, chunks) do
75 | :unhandled ->
76 | default_compile_node(node, opts, chunks)
77 | s ->
78 | s
79 | end
80 | end
81 |
82 | @spec default_compile_node(Eml.node_primitive, map, [chunk]) :: [chunk]
83 | defp default_compile_node(node, opts, chunks) when is_binary(node) do
84 | add_chunk(maybe_escape(node, opts), chunks)
85 | end
86 |
87 | defp default_compile_node({ :safe, node }, _opts, chunks) when is_binary(node) do
88 | add_chunk(node, chunks)
89 | end
90 |
91 | defp default_compile_node(node, _opts, chunks) when is_tuple(node) do
92 | add_chunk(node, chunks)
93 | end
94 |
95 | defp default_compile_node(%Eml.Element{template: fun} = node, opts, chunks) when is_function(fun) do
96 | node |> Eml.Element.apply_template() |> compile_node(opts, chunks)
97 | end
98 |
99 | defp default_compile_node(nil, _opts, chunks) do
100 | chunks
101 | end
102 |
103 | defp default_compile_node(node, _, _) do
104 | raise Eml.CompileError, message: "Bad node primitive: #{inspect node}"
105 | end
106 |
107 | # Attributes parsing
108 |
109 | @spec compile_attrs(Eml.Element.attrs, map, [chunk]) :: [chunk]
110 | def compile_attrs(attrs, opts, chunks) when is_map(attrs) do
111 | Enum.reduce(attrs, chunks, fn
112 | { _, nil }, chunks -> chunks
113 | { k, v }, chunks -> compile_attr(k, v, opts, chunks)
114 | end)
115 | end
116 |
117 | @spec compile_attr(atom, Eml.t, map, [chunk]) :: [chunk]
118 | def compile_attr(field, value, opts, chunks) do
119 | opts[:compiler].compile_attr(field, value, opts, chunks)
120 | end
121 |
122 | @spec compile_attr_value(Eml.t, map, [chunk]) :: [chunk]
123 | def compile_attr_value(list, opts, chunks) when is_list(list) do
124 | Enum.reduce(list, chunks, fn value, chunks ->
125 | compile_attr_value(value, opts, chunks)
126 | end)
127 | end
128 |
129 | def compile_attr_value(value, opts, chunks) do
130 | value = Eml.Encoder.encode(value)
131 | case opts[:compiler].compile_attr_value(value, opts, chunks) do
132 | :unhandled ->
133 | default_compile_node(value, opts, chunks)
134 | s ->
135 | s
136 | end
137 | end
138 |
139 | # Text escaping
140 |
141 | entity_map = %{"&" => "&",
142 | "<" => "<",
143 | ">" => ">",
144 | "\"" => """,
145 | "'" => "'",
146 | "…" => "…"}
147 |
148 | def escape(eml) do
149 | Eml.transform(eml, fn
150 | node when is_binary(node) ->
151 | escape(node, "")
152 | node ->
153 | node
154 | end)
155 | end
156 |
157 | for {char, entity} <- entity_map do
158 | defp escape(unquote(char) <> rest, acc) do
159 | escape(rest, acc <> unquote(entity))
160 | end
161 | end
162 | defp escape(<
>, acc) do
163 | escape(rest, acc <> <>)
164 | end
165 | defp escape("", acc) do
166 | acc
167 | end
168 |
169 | # Create final result.
170 |
171 | defp to_result([{ :safe, string }], _opts) do
172 | { :safe, string }
173 | end
174 | defp to_result(chunks, opts) do
175 | template = :lists.reverse(chunks)
176 | if opts[:fragment] do
177 | template
178 | else
179 | quote do
180 | Eml.Compiler.concat(unquote(template), unquote(Macro.escape(opts)))
181 | end
182 | end
183 | end
184 |
185 | def maybe_transform(node, opts) do
186 | fun = opts[:transform]
187 | if is_function(fun), do: fun.(node), else: node
188 | end
189 |
190 | def maybe_escape(node, opts) do
191 | if opts[:escape], do: escape(node, ""), else: node
192 | end
193 |
194 | def add_chunk(chunk, [{:safe, safe_chunk} | rest]) when is_binary(chunk) do
195 | [{:safe, safe_chunk <> chunk } | rest]
196 | end
197 | def add_chunk(chunk, chunks) when is_binary(chunk) do
198 | [{ :safe, chunk } | chunks]
199 | end
200 | def add_chunk(chunk, chunks) do
201 | [chunk | chunks]
202 | end
203 |
204 | def concat(buffer, opts) do
205 | try do
206 | { :safe, concat(buffer, "", opts) }
207 | catch
208 | :throw, :illegal_quoted ->
209 | reraise Eml.CompileError,
210 | [message: "It's only possible to pass assigns to templates or components when using &"],
211 | __STACKTRACE__
212 | end
213 | end
214 |
215 | defp concat({ :safe, chunk }, acc, _opts) do
216 | acc <> chunk
217 | end
218 | defp concat(chunk, acc, opts) when is_binary(chunk) do
219 | acc <> maybe_escape(chunk, opts)
220 | end
221 | defp concat([chunk | rest], acc, opts) do
222 | concat(rest, concat(chunk, acc, opts), opts)
223 | end
224 | defp concat([], acc, _opts) do
225 | acc
226 | end
227 | defp concat(nil, acc, _opts) do
228 | acc
229 | end
230 | defp concat(node, acc, opts) do
231 | case Eml.Compiler.compile(node, opts) do
232 | { :safe, chunk } ->
233 | acc <> chunk
234 | _ ->
235 | throw :illegal_quoted
236 | end
237 | end
238 |
239 | def prewalk(quoted, fragment?) do
240 | handler = if fragment?,
241 | do: &handle_fragment/1,
242 | else: &handle_template/1
243 | Macro.prewalk(quoted, handler)
244 | end
245 |
246 | defp handle_fragment({ :@, meta, [{ name, _, atom }] }) when is_atom(name) and is_atom(atom) do
247 | line = meta[:line] || 0
248 | Macro.escape(quote line: line do
249 | Access.get(var!(assigns), unquote(name))
250 | end)
251 | end
252 | defp handle_fragment({ :&, _meta, [{ _fun, _, args }] } = ast) do
253 | case Macro.prewalk(args, false, &handle_capture_args/2) do
254 | { _, true } ->
255 | ast
256 | { _, false } ->
257 | raise Eml.CompileError,
258 | message: "It's not possible to use & inside fragments"
259 | end
260 | end
261 | defp handle_fragment(arg) do
262 | arg
263 | end
264 |
265 | defp handle_template({ :&, meta, [{ fun, _, args }] }) do
266 | case Macro.prewalk(args, false, &handle_capture_args/2) do
267 | { _, true } ->
268 | raise Eml.CompileError,
269 | message: "It's not possible to use & for captures inside templates or components"
270 | { new_args, false } ->
271 | line = Keyword.get(meta, :line, 0)
272 | Macro.escape(quote line: line do
273 | unquote(fun)(unquote_splicing(List.wrap(new_args)))
274 | end)
275 | end
276 | end
277 | defp handle_template({ :@, meta, [{ name, _, atom }]}) when is_atom(name) and is_atom(atom) do
278 | line = Keyword.get(meta, :line, 0)
279 | Macro.escape(quote line: line do
280 | Eml.Compiler.get_assign(unquote(name), var!(assigns), var!(funs))
281 | end)
282 | end
283 | defp handle_template(ast) do
284 | ast
285 | end
286 |
287 | defp handle_capture_args({ :@, meta, [{ name, _, atom }]}, regular_capure?) when is_atom(name) and is_atom(atom) do
288 | line = Keyword.get(meta, :line, 0)
289 | ast = quote line: line do
290 | Eml.Compiler.get_assign(unquote(name), var!(assigns), var!(funs))
291 | end
292 | { ast, regular_capure? }
293 | end
294 | defp handle_capture_args({ :&, _meta, [num]} = ast, _regular_capure?) when is_integer(num) do
295 | { ast, true }
296 | end
297 | defp handle_capture_args({ :/, _meta, _args} = ast, _regular_capure?) do
298 | { ast, true }
299 | end
300 | defp handle_capture_args(ast, regular_capure?) do
301 | { ast, regular_capure? }
302 | end
303 |
304 | @doc false
305 | def get_assign(key, assigns, funs) do
306 | x = if is_map(assigns), do: Map.get(assigns, key), else: Keyword.get(assigns, key)
307 | case Keyword.get(funs, key) do
308 | nil -> x
309 | fun -> fun.(x)
310 | end
311 | end
312 | end
313 |
--------------------------------------------------------------------------------
/lib/eml/element.ex:
--------------------------------------------------------------------------------
1 | defmodule Eml.Element do
2 | @moduledoc """
3 | `Eml.Element` defines the struct that represents an element in Eml.
4 |
5 | In practice, you will mostly use the element macro's instead of
6 | directly creating `Eml.Element` structs, but the functions in this
7 | module can be valuable when querying, manipulating or transforming
8 | `eml`.
9 | """
10 | alias __MODULE__, as: El
11 |
12 | defstruct tag: :div, attrs: %{}, content: nil, template: nil, type: :primitive
13 |
14 | @type attr_name :: atom
15 | @type attr_value :: Eml.t
16 | @type attrs :: %{ attr_name => attr_value }
17 | @type template_fn :: ((Dict.t) -> { :safe, String.t } | Macro.t)
18 | @type element_type :: :primitive | :fragment | :component
19 |
20 | @type t :: %El{tag: atom, content: Eml.t, attrs: attrs, template: template_fn, type: element_type}
21 |
22 | @doc """
23 | Assign a template function to an element
24 |
25 | Setting the element type is purely informative and has no effect on
26 | compilation.
27 | """
28 | @spec put_template(t, template_fn, element_type) :: t
29 | def put_template(%El{} = el, fun, type \\ :fragment) do
30 | %El{el| template: fun, type: type}
31 | end
32 |
33 | @doc """
34 | Removes a template function from an element
35 | """
36 | @spec remove_template(t) :: t
37 | def remove_template(%El{} = el) do
38 | %El{el| template: nil, type: :primitive}
39 | end
40 |
41 | @doc """
42 | Calls the template function of an element with its attributes and
43 | content as argument.
44 |
45 | Raises an `Eml.CompileError` when no template function is present.
46 |
47 | ### Example
48 |
49 | iex> use Eml
50 | nil
51 | iex> use Eml.HTML
52 | nil
53 | iex> defmodule ElTest do
54 | ...>
55 | ...> fragment my_list do
56 | ...> ul class: @class do
57 | ...> quote do
58 | ...> for item <- @__CONTENT__ do
59 | ...> li do
60 | ...> end
61 | ...> end
62 | ...> end
63 | ...> end
64 | ...> end
65 | ...>
66 | ...> end
67 | {:module, ElTest, ...}
68 | iex> import ElTest
69 | nil
70 | iex> el = my_list class: "some-class" do
71 | ...> span 1
72 | ...> span 2
73 | ...> end
74 | #my_list<%{class: "some-class"} [#span<[1]>, #span<[2]>]>
75 | iex> Eml.Element.apply_template(el)
76 | [{:safe, ""}]
77 | """
78 | @spec apply_template(t) :: { :safe, String.t } | Macro.t
79 | def apply_template(%El{attrs: attrs, content: content, template: fun}) when is_function(fun) do
80 | assigns = Map.put(attrs, :__CONTENT__, content)
81 | fun.(assigns)
82 | end
83 | def apply_template(badarg) do
84 | raise Eml.CompileError, message: "Bad template element: #{inspect badarg}"
85 | end
86 | end
87 |
88 | # Enumerable protocol implementation
89 |
90 | defimpl Enumerable, for: Eml.Element do
91 | def count(_el), do: { :error, __MODULE__ }
92 | def member?(_el, _), do: { :error, __MODULE__ }
93 | def slice(_el), do: { :error, __MODULE__ }
94 |
95 | def reduce(el, acc, fun) do
96 | case reduce_content([el], acc, fun) do
97 | { :cont, acc } -> { :done, acc }
98 | { :suspend, acc } -> { :suspended, acc }
99 | { :halt, acc } -> { :halted, acc }
100 | end
101 | end
102 |
103 | defp reduce_content(_, { :halt, acc }, _fun) do
104 | { :halt, acc }
105 | end
106 | defp reduce_content(content, { :suspend, acc }, fun) do
107 | { :suspend, acc, &reduce_content(content, &1, fun) }
108 | end
109 | defp reduce_content([%Eml.Element{content: content} = el | rest], { :cont, acc }, fun) do
110 | reduce_content(rest, reduce_content(content, fun.(el, acc), fun), fun)
111 | end
112 | defp reduce_content([node | rest], { :cont, acc }, fun) do
113 | reduce_content(rest, fun.(node, acc), fun)
114 | end
115 | defp reduce_content(nil, acc, _fun) do
116 | acc
117 | end
118 | defp reduce_content([], acc, _fun) do
119 | acc
120 | end
121 | defp reduce_content(node, { :cont, acc }, fun) do
122 | fun.(node, acc)
123 | end
124 | end
125 |
126 | # Inspect protocol implementation
127 |
128 | defimpl Inspect, for: Eml.Element do
129 | import Inspect.Algebra
130 |
131 | def inspect(%Eml.Element{tag: tag, attrs: attrs, content: content}, opts) do
132 | opts = if is_list(opts), do: Keyword.put(opts, :hide_content_type, true), else: opts
133 | tag = Atom.to_string(tag)
134 | attrs = if attrs == %{}, do: "", else: to_doc(attrs, opts)
135 | content = if content in [nil, "", []], do: "", else: to_doc(content, opts)
136 | fields = case { attrs, content } do
137 | { "", "" } -> ""
138 | { "", _ } -> content
139 | { _, "" } -> attrs
140 | { _, _ } -> glue(attrs, " ", content)
141 | end
142 | concat ["#", tag, "<", fields, ">"]
143 | end
144 | end
145 |
--------------------------------------------------------------------------------
/lib/eml/element/generator.ex:
--------------------------------------------------------------------------------
1 | defmodule Eml.Element.Generator do
2 | @moduledoc """
3 | This module defines some macro's and helper functions
4 | for generating Eml element macro's.
5 |
6 | ### Example
7 |
8 | iex> defmodule MyElements do
9 | ...> use Eml.Element.Generator, tags: [:custom1, :custom2]
10 | ...> end
11 | iex> import MyElements
12 | iex> custom1 [id: 42], "content in a custom element"
13 | #custom1<%{id: "42"} ["content in a custom element"]>
14 |
15 | You can also optionally control the casing of the generated elements
16 | with the `:casing` option. Accepted values are: `:snake` (default),
17 | `:snake_upcase`, `:pascal`, `:camel`, `:lisp` and `:lisp_upcase`.
18 |
19 | ### Example
20 |
21 | iex> defmodule MyElements2 do
22 | ...> use Eml.Element.Generator, casing: :pascal, tags: [:some_long_element, :another_long_element]
23 | ...> end
24 | iex> import MyElements
25 | iex> some_long_element [id: 42], "content in a custom element"
26 | #SomeLongElement<%{id: "42"} ["content in a custom element"]>
27 | """
28 |
29 | defmacro __using__(opts) do
30 | tags = opts[:tags] || []
31 | casing = opts[:casing] || :snake
32 | catch_all? = opts[:generate_catch_all]
33 | quote do
34 | defmacro __using__(_) do
35 | mod = __MODULE__
36 | ambiguous_imports = Eml.Element.Generator.find_ambiguous_imports(unquote(tags))
37 | quote do
38 | import Kernel, except: unquote(ambiguous_imports)
39 | import unquote(mod)
40 | end
41 | end
42 | Enum.each(unquote(tags), fn tag ->
43 | Eml.Element.Generator.def_element(tag, unquote(casing))
44 | end)
45 | if unquote(catch_all?) do
46 | Eml.Element.Generator.def_catch_all()
47 | end
48 | end
49 | end
50 |
51 | @doc false
52 | defmacro def_element(tag, casing) do
53 | quote bind_quoted: [tag: tag, casing: casing] do
54 | defmacro unquote(tag)(content_or_attrs \\ nil, maybe_content \\ nil) do
55 | tag = unquote(tag) |> Eml.Element.Generator.do_casing(unquote(casing))
56 | in_match = Macro.Env.in_match?(__CALLER__)
57 | { attrs, content } = Eml.Element.Generator.extract_content(content_or_attrs, maybe_content, in_match)
58 | if in_match do
59 | quote do
60 | %Eml.Element{tag: unquote(tag), attrs: unquote(attrs), content: unquote(content)}
61 | end
62 | else
63 | quote do
64 | %Eml.Element{tag: unquote(tag), attrs: Enum.into(unquote(attrs), %{}), content: unquote(content)}
65 | end
66 | end
67 | end
68 | end
69 | end
70 |
71 | defmacro def_catch_all do
72 | quote do
73 | defmacro any(content_or_attrs, maybe_content \\ nil) do
74 | { attrs, content } = Eml.Element.Generator.extract_content(content_or_attrs, maybe_content, true)
75 | quote do
76 | %Eml.Element{tag: _, attrs: unquote(attrs), content: unquote(content)}
77 | end
78 | end
79 | end
80 | end
81 |
82 | @doc false
83 | def do_casing(tag, :snake) do
84 | tag
85 | end
86 | def do_casing(tag, :snake_upcase) do
87 | tag
88 | |> Atom.to_string()
89 | |> String.upcase()
90 | |> String.to_atom()
91 | end
92 | def do_casing(tag, :pascal) do
93 | tag
94 | |> split()
95 | |> Enum.map(&String.capitalize/1)
96 | |> join()
97 | end
98 | def do_casing(tag, :camel) do
99 | [first | rest] = split(tag)
100 | rest = Enum.map(rest, &String.capitalize/1)
101 | join([first | rest])
102 | end
103 | def do_casing(tag, :lisp) do
104 | tag
105 | |> split()
106 | |> join("-")
107 | end
108 | def do_casing(tag, :lisp_upcase) do
109 | tag
110 | |> split()
111 | |> Enum.map(&String.upcase/1)
112 | |> join("-")
113 | end
114 |
115 | defp split(tag) do
116 | tag
117 | |> Atom.to_string()
118 | |> String.split("_")
119 | end
120 |
121 | defp join(tokens, joiner \\ "") do
122 | tokens
123 | |> Enum.join(joiner)
124 | |> String.to_atom()
125 | end
126 |
127 | @doc false
128 | def find_ambiguous_imports(tags) do
129 | default_imports = Kernel.__info__(:functions) ++ Kernel.__info__(:macros)
130 | for { name, arity } <- default_imports, arity in 1..2 and name in tags do
131 | { name, arity }
132 | end
133 | end
134 |
135 | @doc false
136 | def extract_content(content_or_attrs, maybe_content, in_match) do
137 | init = fn
138 | nil, nil, true ->
139 | { (quote do: _), quote do: _ }
140 | nil, nil, false ->
141 | { (quote do: %{}), nil }
142 | nil, content, true ->
143 | { (quote do: _), content }
144 | nil, content, false ->
145 | { (quote do: %{}), content }
146 | attrs, nil, true ->
147 | { attrs, quote do: _ }
148 | attrs, nil, false ->
149 | { attrs, nil }
150 | attrs, content, _ ->
151 | { attrs, content }
152 | end
153 | case { content_or_attrs, maybe_content } do
154 | { [{ :do, {:__block__, _, content}}], _ } -> init.(nil, content, in_match)
155 | { [{ :do, content}], _ } -> init.(nil, List.wrap(content), in_match)
156 | { attrs, [{ :do, {:__block__, _, content}}] } -> init.(attrs, content, in_match)
157 | { attrs, [{ :do, content}] } -> init.(attrs, List.wrap(content), in_match)
158 | { [{ _, _ } | _] = attrs, nil } -> init.(attrs, nil, in_match)
159 | { attrs, nil } when in_match -> init.(attrs, nil, in_match)
160 | { content, nil } when not in_match -> init.(nil, content, in_match)
161 | { attrs, content } -> init.(attrs, content, in_match)
162 | end
163 | end
164 | end
165 |
--------------------------------------------------------------------------------
/lib/eml/encoder.ex:
--------------------------------------------------------------------------------
1 | defprotocol Eml.Encoder do
2 | @moduledoc """
3 | The Eml Encoder protocol.
4 |
5 | This protocol is used by Eml's compiler to convert different Elixir
6 | data types to it's `Eml.Compiler.chunk` type.
7 |
8 | Chunks can be of the type `String.t`, `{ :safe, String.t }`,
9 | `Eml.Element.t`, or `Macro.t`, so any implementation of the
10 | `Eml.Encoder` protocol needs to return one of these types.
11 |
12 | Eml implements the following types by default:
13 |
14 | `Integer`, `Float`, `Atom`, `Tuple`, `BitString` and `Eml.Element`
15 |
16 | You can easily implement a protocol implementation for a custom
17 | type, by defining an `encode` function that receives the custom type
18 | and outputs to `Eml.Compiler.chunk`.
19 |
20 | ### Example
21 |
22 | iex> defmodule Customer do
23 | ...> defstruct [:name, :email, :phone]
24 | ...> end
25 | iex> defimpl Eml.Encoder, for: Customer do
26 | ...> def encode(%Customer{name: name, email: email, phone: phone}) do
27 | ...> use Eml.HTML
28 | ...>
29 | ...> div [class: "customer"] do
30 | ...> div [span("name: "), span(name)]
31 | ...> div [span("email: "), span(email)]
32 | ...> div [span("phone: "), span(phone)]
33 | ...> end
34 | ...> end
35 | ...> end
36 | iex> c = %Customer{name: "Fred", email: "freddy@mail.com", phone: "+31 6 5678 1234"}
37 | %Customer{email: "freddy@mail.com", name: "Fred", phone: "+31 6 5678 1234"}
38 | iex> Eml.Encoder.encode c
39 | #div<%{class: "customer"}
40 | [#div<[#span<"name: ">, #span<"Fred">]>,
41 | #div<[#span<"email: ">, #span<"freddy@mail.com">]>,
42 | #div<[#span<"phone: ">, #span<"+31 6 5678 1234">]>]>
43 | iex> Eml.render c
44 | "name: Fred
email: freddy@mail.com
phone: +31 6 5678 1234
"
45 |
46 | """
47 | @spec encode(Eml.Encoder.t) :: Eml.node_primitive
48 | def encode(data)
49 | end
50 |
51 | defimpl Eml.Encoder, for: Integer do
52 | def encode(data), do: Integer.to_string(data)
53 | end
54 |
55 | defimpl Eml.Encoder, for: Float do
56 | def encode(data), do: Float.to_string(data)
57 | end
58 |
59 | defimpl Eml.Encoder, for: Atom do
60 | def encode(nil), do: nil
61 | def encode(data), do: Atom.to_string(data)
62 | end
63 |
64 | defimpl Eml.Encoder, for: Tuple do
65 | def encode({ :safe, data }) do
66 | if is_binary(data) do
67 | { :safe, data }
68 | else
69 | raise Protocol.UndefinedError, protocol: Eml.Encoder, value: { :safe, data }
70 | end
71 | end
72 | def encode(data) do
73 | if Macro.validate(data) == :ok do
74 | data
75 | else
76 | raise Protocol.UndefinedError, protocol: Eml.Encoder, value: data
77 | end
78 | end
79 | end
80 |
81 | defimpl Eml.Encoder, for: [BitString, Eml.Element] do
82 | def encode(data), do: data
83 | end
84 |
--------------------------------------------------------------------------------
/lib/eml/errors.ex:
--------------------------------------------------------------------------------
1 | defmodule Eml.CompileError do
2 | defexception message: "compile error"
3 | end
4 |
5 | defmodule Eml.ParseError do
6 | defexception message: "parse error"
7 | end
8 |
9 | defmodule Eml.QueryError do
10 | defexception message: "query error"
11 | end
12 |
--------------------------------------------------------------------------------
/lib/eml/html.ex:
--------------------------------------------------------------------------------
1 | defmodule Eml.HTML do
2 | @moduledoc """
3 | This is the container module of all the generated HTML element macro's.
4 |
5 | To import all these macro's into current scope, invoke `use Eml.HTML`
6 | instead of `import Eml.HTML`, because it also handles ambiguous named elements.
7 | """
8 |
9 | use Eml.Element.Generator,
10 | generate_catch_all: true,
11 | tags: [:html, :head, :title, :base, :link, :meta, :style,
12 | :script, :noscript, :body, :div, :span, :article,
13 | :section, :nav, :aside, :h1, :h2, :h3, :h4, :h5, :h6,
14 | :header, :footer, :address, :p, :hr, :pre, :blockquote,
15 | :ol, :ul, :li, :dl, :dt, :dd, :figure, :figcaption, :main,
16 | :a, :em, :strong, :small, :s, :cite, :q, :dfn, :abbr, :data,
17 | :time, :code, :var, :samp, :kbd, :sub, :sup, :i, :b, :u, :mark,
18 | :ruby, :rt, :rp, :bdi, :bdo, :br, :wbr, :ins, :del, :img, :iframe,
19 | :embed, :object, :param, :video, :audio, :source, :track, :canvas, :map,
20 | :area, :svg, :math, :table, :caption, :colgroup, :col, :tbody, :thead, :tfoot,
21 | :tr, :td, :th, :form, :fieldset, :legend, :label, :input, :button, :select,
22 | :datalist, :optgroup, :option, :textarea, :keygen, :output, :progress,
23 | :meter, :details, :summary, :menuitem, :menu]
24 |
25 | end
26 |
--------------------------------------------------------------------------------
/lib/eml/html/compiler.ex:
--------------------------------------------------------------------------------
1 | defmodule Eml.HTML.Compiler do
2 | @moduledoc false
3 |
4 | alias Eml.Compiler
5 | alias Eml.Element
6 | import Eml.Compiler, only: [add_chunk: 2]
7 |
8 | def opts do
9 | [quotes: :single]
10 | end
11 |
12 | # Eml parsing
13 |
14 | def compile_node(%Element{tag: tag, attrs: attrs, content: content, template: nil}, opts, chunks) do
15 | chunks = chunks |> maybe_doctype(tag) |> start_tag_open(tag)
16 | chunks = Compiler.compile_attrs(attrs, opts, chunks)
17 | if is_void_element?(tag) do
18 | void_tag_close(chunks)
19 | else
20 | chunks = start_tag_close(chunks)
21 | chunks = Compiler.compile_node(content, opts, chunks)
22 | end_tag(chunks, tag)
23 | end
24 | end
25 |
26 | def compile_node(_, _, _) do
27 | :unhandled
28 | end
29 |
30 | def compile_attr(field, value, opts, chunks) do
31 | quotes_char = quotes_char(opts[:quotes])
32 | field = attr_field(field)
33 | chunks = add_chunk(" #{field}=#{quotes_char}", chunks)
34 | chunks = Compiler.compile_attr_value(value, opts, chunks)
35 | add_chunk("#{quotes_char}", chunks)
36 | end
37 |
38 | defp attr_field(field) do
39 | field = Atom.to_string(field)
40 | if String.starts_with?(field, "_"),
41 | do: "data-" <> String.trim_leading(field, "_"),
42 | else: field
43 | end
44 |
45 | def compile_attr_value(_, _, _) do
46 | :unhandled
47 | end
48 |
49 | # Element generators
50 |
51 | defp start_tag_open(chunks, tag), do: add_chunk("<#{tag}", chunks)
52 | defp start_tag_close(chunks), do: add_chunk(">", chunks)
53 | defp void_tag_close(chunks), do: add_chunk("/>", chunks)
54 | defp end_tag(chunks, tag), do: add_chunk("#{tag}>", chunks)
55 |
56 | defp maybe_doctype(chunks, :html), do: add_chunk("\n", chunks)
57 | defp maybe_doctype(chunks, _), do: chunks
58 |
59 | # Element helpers
60 |
61 | defp is_void_element?(tag) do
62 | tag in [:area, :base, :br, :col, :embed, :hr, :img, :input, :keygen, :link, :meta, :param, :source, :track, :wbr]
63 | end
64 |
65 | # Attribute element helpers
66 |
67 | defp quotes_char(:single), do: "'"
68 | defp quotes_char(:double), do: "\""
69 | end
70 |
--------------------------------------------------------------------------------
/lib/eml/html/parser.ex:
--------------------------------------------------------------------------------
1 | defmodule Eml.HTML.Parser do
2 | @moduledoc false
3 |
4 | # API
5 |
6 | @spec parse(binary, Keyword.t) :: [Eml.t]
7 | def parse(html, opts \\ []) do
8 | res = tokenize(html, { :blank, [] }, [], :blank, opts) |> parse_content()
9 | case res do
10 | { content, [] } ->
11 | content
12 | { content, rest }->
13 | raise Eml.ParseError, message: "Unparsable content, parsed: #{inspect content}, rest: #{inspect rest}"
14 | end
15 | end
16 |
17 | # Tokenize
18 |
19 | # Skip comments
20 | defp tokenize("" <> rest, buf, acc, :comment, opts) do
25 | { state, _ } = buf
26 | tokenize(rest, buf, acc, state, opts)
27 | end
28 | defp tokenize(<<_>> <> rest, buf, acc, :comment, opts) do
29 | tokenize(rest, buf, acc, :comment, opts)
30 | end
31 |
32 | # Skip doctype
33 | defp tokenize(" rest, buf, acc, :blank, opts) do
34 | tokenize(rest, buf, acc, :doctype, opts)
35 | end
36 | defp tokenize(" rest, buf, acc, :blank, opts) do
37 | tokenize(rest, buf, acc, :doctype, opts)
38 | end
39 | defp tokenize(">" <> rest, buf, acc, :doctype, opts) do
40 | tokenize(rest, buf, acc, :blank, opts)
41 | end
42 | defp tokenize(<<_>> <> rest, buf, acc, :doctype, opts) do
43 | tokenize(rest, buf, acc, :doctype, opts)
44 | end
45 |
46 | # CDATA
47 | defp tokenize(" rest, buf, acc, state, opts)
48 | when state in [:content, :blank, :start_close, :end_close, :close] do
49 | next(rest, buf, "", acc, :cdata, opts)
50 | end
51 | defp tokenize("]]>" <> rest, buf, acc, :cdata, opts) do
52 | next(rest, buf, "", acc, :content, opts)
53 | end
54 | defp tokenize(<> <> rest, buf, acc, :cdata, opts) do
55 | consume(char, rest, buf, acc, :cdata, opts)
56 | end
57 | # Makes it possible for elements to treat its contents as if cdata
58 | defp tokenize(chars, buf, acc, { :cdata, end_tag } = state, opts) do
59 | end_token = "" <> end_tag <> ">"
60 | n = byte_size(end_token)
61 | case chars do
62 | <<^end_token::binary-size(n), rest::binary>> ->
63 | acc = change(buf, acc, :cdata)
64 | acc = change({ :open, "<" }, acc)
65 | acc = change({ :slash, "/" }, acc)
66 | acc = change({ :end_tag, end_tag }, acc)
67 | tokenize(rest, { :end_close, ">" }, acc, :end_close, opts)
68 | <> <> rest ->
69 | consume(char, rest, buf, acc, state, opts)
70 | "" ->
71 | :lists.reverse([buf | acc])
72 | end
73 | end
74 |
75 | # Attribute quotes
76 | defp tokenize("'" <> rest, buf, acc, :attr_sep, opts) do
77 | next(rest, buf, "'", acc, :attr_single_open, opts)
78 | end
79 | defp tokenize("\"" <> rest, buf, acc, :attr_sep, opts) do
80 | next(rest, buf, "\"", acc, :attr_double_open, opts)
81 | end
82 | defp tokenize(<> <> rest, buf, acc, :attr_value, opts) when char in [?\", ?\'] do
83 | case { char, previous_state(acc, [:attr_value]) } do
84 | t when t in [{ ?\', :attr_single_open }, { ?\", :attr_double_open }] ->
85 | next(rest, buf, char, acc, :attr_close, opts)
86 | _else ->
87 | consume(char, rest, buf, acc, :attr_value, opts)
88 | end
89 | end
90 | defp tokenize(<> <> rest, buf, acc, state, opts)
91 | when { char, state } in [{ ?\', :attr_single_open }, { ?\", :attr_double_open }] do
92 | next(rest, buf, char, acc, :attr_close, opts)
93 | end
94 |
95 | # Attributes values accept any character
96 | defp tokenize(<> <> rest, buf, acc, state, opts)
97 | when state in [:attr_single_open, :attr_double_open] do
98 | next(rest, buf, char, acc, :attr_value, opts)
99 | end
100 | defp tokenize(<> <> rest, buf, acc, :attr_value, opts) do
101 | consume(char, rest, buf, acc, :attr_value, opts)
102 | end
103 |
104 | # Attribute field/value seperator
105 | defp tokenize("=" <> rest, buf, acc, :attr_field, opts) do
106 | next(rest, buf, "=", acc, :attr_sep, opts)
107 | end
108 |
109 | # Allow boolean attributes, ie. attributes with only a field name
110 | defp tokenize(<> <> rest, buf, acc, :attr_field, opts)
111 | when char in [?\>, ?\s, ?\n, ?\r, ?\t] do
112 | next(<>, buf, "\"", acc, :attr_close, opts)
113 | end
114 |
115 | # Whitespace handling
116 | defp tokenize(<> <> rest, buf, acc, state, opts)
117 | when char in [?\s, ?\n, ?\r, ?\t] do
118 | case state do
119 | :start_tag ->
120 | next(rest, buf, "", acc, :start_tag_close, opts)
121 | s when s in [:close, :start_close, :end_close] ->
122 | if char in [?\n, ?\r] do
123 | next(rest, buf, "", acc, :content, opts)
124 | else
125 | next(rest, buf, char, acc, :content, opts)
126 | end
127 | :content ->
128 | consume(char, rest, buf, acc, state, opts)
129 | _ ->
130 | tokenize(rest, buf, acc, state, opts)
131 | end
132 | end
133 |
134 | # Open tag
135 | defp tokenize("<" <> rest, buf, acc, state, opts) do
136 | case state do
137 | s when s in [:blank, :start_close, :end_close, :close, :content] ->
138 | next(rest, buf, "<", acc, :open, opts)
139 | _ ->
140 | error("<", rest, buf, acc, state)
141 | end
142 | end
143 |
144 | # Close tag
145 | defp tokenize(">" <> rest, buf, acc, state, opts) do
146 | case state do
147 | s when s in [:attr_close, :start_tag] ->
148 | # The html tokenizer doesn't support elements without proper closing.
149 | # However, it does makes exceptions for tags specified in is_void_element?/1
150 | # and assume they never have children.
151 | tag = get_last_tag(acc, buf)
152 | if is_void_element?(tag) do
153 | next(rest, buf, ">", acc, :close, opts)
154 | else
155 | # check if the content of the element should be interpreted as cdata
156 | case element_type([buf | acc], List.wrap(opts[:treat_as_cdata])) do
157 | :content ->
158 | next(rest, buf, ">", acc, :start_close, opts)
159 | { :cdata, tag } ->
160 | acc = change(buf, acc)
161 | next(rest, { :start_close, ">" }, "", acc, { :cdata, tag }, opts)
162 | end
163 | end
164 | :slash ->
165 | next(rest, buf, ">", acc, :close, opts)
166 | :end_tag ->
167 | next(rest, buf, ">", acc, :end_close, opts)
168 | _ ->
169 | def_tokenize(">" <> rest, buf, acc, state, opts)
170 | end
171 | end
172 |
173 | # Slash
174 | defp tokenize("/" <> rest, buf, acc, state, opts)
175 | when state in [:open, :attr_field, :attr_close, :start_tag, :start_tag_close] do
176 | next(rest, buf, "/", acc, :slash, opts)
177 | end
178 |
179 | defp tokenize("", buf, acc, _, _opts) do
180 | :lists.reverse([buf | acc])
181 | end
182 |
183 | # Default parsing
184 | defp tokenize(chars, buf, acc, state, opts), do: def_tokenize(chars, buf, acc, state, opts)
185 |
186 | # Either start or consume content or tag.
187 | defp def_tokenize(<> <> rest, buf, acc, state, opts) do
188 | case state do
189 | s when s in [:start_tag, :end_tag, :attr_field, :content] ->
190 | consume(char, rest, buf, acc, state, opts)
191 | s when s in [:blank, :start_close, :end_close, :close] ->
192 | next(rest, buf, char, acc, :content, opts)
193 | s when s in [:attr_close, :start_tag_close] ->
194 | next(rest, buf, char, acc, :attr_field, opts)
195 | :open ->
196 | next(rest, buf, char, acc, :start_tag, opts)
197 | :slash ->
198 | next(rest, buf, char, acc, :end_tag, opts)
199 | _ ->
200 | error(char, rest, buf, acc, state)
201 | end
202 | end
203 |
204 | # Stops tokenizing and dumps all info in a tuple
205 | defp error(char, rest, buf, acc, state) do
206 | char = if is_integer(char), do: <>, else: char
207 | state = [state: state,
208 | char: char,
209 | buf: buf,
210 | last_token: List.first(acc),
211 | next_char: String.first(rest)]
212 | raise Eml.ParseError, message: "Illegal token, parse state is: #{inspect state}"
213 | end
214 |
215 | # Consumes character and put it in the buffer
216 | defp consume(char, rest, { type, buf }, acc, state, opts) do
217 | char = if is_integer(char), do: <>, else: char
218 | tokenize(rest, { type, buf <> char }, acc, state, opts)
219 | end
220 |
221 | # Add the old buffer to the accumulator and start a new buffer
222 | defp next(rest, old_buf, new_buf, acc, new_state, opts) do
223 | acc = change(old_buf, acc)
224 | new_buf = if is_integer(new_buf), do: <>, else: new_buf
225 | tokenize(rest, { new_state, new_buf }, acc, new_state, opts)
226 | end
227 |
228 | # Add buffer to the accumulator if its content is not empty.
229 | defp change({ type, buf }, acc, type_modifier \\ nil) do
230 | type = if is_nil(type_modifier), do: type, else: type_modifier
231 | token = { type, buf }
232 | if empty?(token) do
233 | acc
234 | else
235 | [token | acc]
236 | end
237 | end
238 |
239 | # Checks for empty content
240 | defp empty?({ :blank, _ }), do: true
241 | defp empty?({ :content, content }) do
242 | String.trim(content) === ""
243 | end
244 | defp empty?(_), do: false
245 |
246 | # Checks if last tokenized tag is a tag that should always close.
247 | defp get_last_tag(tokens, { type, buf }) do
248 | get_last_tag([{ type, buf } | tokens])
249 | end
250 |
251 | defp get_last_tag([{ :start_tag, tag } | _]), do: tag
252 | defp get_last_tag([_ | ts]), do: get_last_tag(ts)
253 | defp get_last_tag([]), do: nil
254 |
255 | defp is_void_element?(tag) do
256 | tag in ["area", "base", "br", "col", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"]
257 | end
258 |
259 | defp previous_state([{ state, _ } | rest], skip_states) do
260 | if state in skip_states do
261 | previous_state(rest, skip_states)
262 | else
263 | state
264 | end
265 | end
266 | defp previous_state([], _), do: :blank
267 |
268 | # CDATA element helper
269 |
270 | @cdata_elements ["script", "style"]
271 |
272 | defp element_type(acc, extra_cdata_elements) do
273 | cdata_elements = @cdata_elements ++ extra_cdata_elements
274 | case get_last_tag(acc) do
275 | nil ->
276 | :content
277 | tag ->
278 | if tag in cdata_elements do
279 | { :cdata, tag }
280 | else
281 | :content
282 | end
283 | end
284 | end
285 |
286 | # Parse the genrated tokens
287 |
288 | defp parse_content(tokens) do
289 | parse_content(tokens, [])
290 | end
291 |
292 | defp parse_content([{ type, token } | ts], acc) do
293 | case preparse(type, token) do
294 | :skip ->
295 | parse_content(ts, acc)
296 | { :tag, tag } ->
297 | { element, tokens } = parse_element(ts, [tag: tag, attrs: [], content: []])
298 | parse_content(tokens, [element | acc])
299 | { :content, content } ->
300 | parse_content(ts, [content | acc])
301 | { :cdata, content } ->
302 | # tag cdata in order to skip whitespace trimming
303 | parse_content(ts, [{ :cdata, content } | acc])
304 | :end_el ->
305 | { :lists.reverse(acc), ts }
306 | end
307 | end
308 | defp parse_content([], acc) do
309 | { :lists.reverse(acc), [] }
310 | end
311 |
312 | defp parse_element([{ type, token } | ts], acc) do
313 | case preparse(type, token) do
314 | :skip ->
315 | parse_element(ts, acc)
316 | { :attr_field, field } ->
317 | attrs = [{ field, "" } | acc[:attrs]]
318 | parse_element(ts, Keyword.put(acc, :attrs, attrs))
319 | { :attr_value, value } ->
320 | [{ field, current } | rest] = acc[:attrs]
321 | attrs = if is_binary(current) && is_binary(value) do
322 | [{ field, current <> value } | rest]
323 | else
324 | [{ field, List.wrap(current) ++ [value] } | rest]
325 | end
326 | parse_element(ts, Keyword.put(acc, :attrs, attrs))
327 | :start_content ->
328 | { content, tokens } = parse_content(ts, [])
329 | { make_element(Keyword.put(acc, :content, content)), tokens }
330 | :end_el ->
331 | { make_element(acc), ts }
332 | end
333 | end
334 | defp parse_element([], acc) do
335 | { make_element(acc), [] }
336 | end
337 |
338 | defp make_element(acc) do
339 | attrs = acc[:attrs]
340 | %Eml.Element{tag: acc[:tag], attrs: Enum.into(attrs, %{}), content: finalize_content(acc[:content], acc[:tag])}
341 | end
342 |
343 | defp preparse(:blank, _), do: :skip
344 | defp preparse(:open, _), do: :skip
345 | defp preparse(:slash, _), do: :skip
346 | defp preparse(:attr_single_open, _), do: :skip
347 | defp preparse(:attr_double_open, _), do: :skip
348 | defp preparse(:attr_close, _), do: :skip
349 | defp preparse(:attr_sep, _), do: :skip
350 | defp preparse(:end_tag, _), do: :skip
351 | defp preparse(:start_tag_close, _), do: :skip
352 |
353 | defp preparse(:attr_field, token) do
354 | { :attr_field, String.to_atom(token) }
355 | end
356 |
357 | defp preparse(:attr_value, token), do: { :attr_value, token }
358 | defp preparse(:start_tag, token), do: { :tag, String.to_atom(token) }
359 | defp preparse(:start_close, _), do: :start_content
360 | defp preparse(:content, token), do: { :content, token }
361 | defp preparse(:end_close, _), do: :end_el
362 | defp preparse(:close, _), do: :end_el
363 |
364 | defp preparse(:cdata, token), do: { :cdata, token }
365 |
366 | defp finalize_content(content, tag)
367 | when tag in [:textarea, :pre] do
368 | case content do
369 | [content] when is_binary(content) ->
370 | content
371 | [] ->
372 | nil
373 | content ->
374 | content
375 | end
376 | end
377 | defp finalize_content(content, _) do
378 | case content do
379 | [content] when is_binary(content) ->
380 | trim_whitespace(content, :only)
381 | [] ->
382 | nil
383 | [first | rest] ->
384 | first = trim_whitespace(first, :first)
385 | [first | trim_whitespace_loop(rest, [])]
386 | end
387 | end
388 |
389 | defp trim_whitespace_loop([last], acc) do
390 | last = trim_whitespace(last, :last)
391 | :lists.reverse([last | acc])
392 | end
393 | defp trim_whitespace_loop([h | t], acc) do
394 | trim_whitespace_loop(t, [trim_whitespace(h, :other) | acc])
395 | end
396 | defp trim_whitespace_loop([], acc) do
397 | acc
398 | end
399 |
400 | defp trim_whitespace(content, position) do
401 | trim_whitespace(content, "", false, position)
402 | end
403 |
404 | defp trim_whitespace(<> <> rest, acc, in_whitespace?, pos) do
405 | if char in [?\s, ?\n, ?\r, ?\t] do
406 | if in_whitespace? do
407 | trim_whitespace(rest, acc, true, pos)
408 | else
409 | trim_whitespace(rest, acc <> " ", true, pos)
410 | end
411 | else
412 | trim_whitespace(rest, acc <> <>, false, pos)
413 | end
414 | end
415 | defp trim_whitespace("", acc, _, pos) do
416 | case pos do
417 | :first -> String.trim_leading(acc)
418 | :last -> String.trim_trailing(acc)
419 | :only -> String.trim(acc)
420 | :other -> acc
421 | end
422 | end
423 | defp trim_whitespace({ :cdata, noop }, _, _, _), do: noop
424 | defp trim_whitespace(noop, _, _, _), do: noop
425 | end
426 |
--------------------------------------------------------------------------------
/lib/eml/parser.ex:
--------------------------------------------------------------------------------
1 | defmodule Eml.Parser do
2 | @moduledoc """
3 | Various helper functions for implementing an Eml parser.
4 | """
5 |
6 | # Entity helpers
7 |
8 | entity_map = %{"&" => "&",
9 | "<" => "<",
10 | ">" => ">",
11 | """ => "\"",
12 | "…" => "…"}
13 | entity_map = for n <- 32..126, into: entity_map do
14 | { "#{n};", <> }
15 | end
16 |
17 | def unescape(eml) do
18 | Eml.transform(eml, fn
19 | node when is_binary(node) ->
20 | unescape(node, "")
21 | node ->
22 | node
23 | end)
24 | end
25 |
26 | for {entity, char} <- entity_map do
27 | defp unescape(unquote(entity) <> rest, acc) do
28 | unescape(rest, acc <> unquote(char))
29 | end
30 | end
31 | defp unescape(<>, acc) do
32 | unescape(rest, acc <> <>)
33 | end
34 | defp unescape("", acc) do
35 | acc
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/eml/query.ex:
--------------------------------------------------------------------------------
1 | defmodule Eml.Query.Helper do
2 | @moduledoc false
3 | alias Eml.Element
4 |
5 | def do_transform(_node, :node, value) do
6 | value
7 | end
8 | def do_transform(%Element{attrs: attrs} = node, { :attrs, key }, value) do
9 | %Element{node|attrs: Map.put(attrs, key, value)}
10 | end
11 | def do_transform(%Element{} = node, key, value) do
12 | Map.put(node, key, value)
13 | end
14 | def do_transform(node, key, _value) do
15 | raise Eml.QueryError, message: "can only set key `#{inspect key}` on an element node, got: #{inspect node}"
16 | end
17 |
18 | def do_add(%Element{content: content} = node, expr, op) do
19 | content = if op == :insert do
20 | List.wrap(expr) ++ List.wrap(content)
21 | else
22 | List.wrap(content) ++ List.wrap(expr)
23 | end
24 | %Element{node|content: content}
25 | end
26 | def do_add(node, _expr, op) do
27 | raise Eml.QueryError,
28 | message: "can only #{op} with an element node, got: #{inspect node}"
29 | end
30 |
31 | def do_collect(op, acc, key, expr) do
32 | case op do
33 | :put -> Map.put(acc, key, expr)
34 | :insert -> Map.update(acc, key, List.wrap(expr), &(List.wrap(expr) ++ List.wrap(&1)))
35 | :append -> Map.update(acc, key, List.wrap(expr), &(List.wrap(&1) ++ List.wrap(expr)))
36 | end
37 | end
38 | end
39 |
40 | defmodule Eml.Query do
41 | @moduledoc """
42 | Provides a DSL for retrieving and manipulating eml.
43 |
44 | Queries can be used on its own, but they are best used together with
45 | `Eml.transform/2`, `Eml.collect/3` and the `Enumerable` protocol. Using them
46 | often results in simpler and easier to read code. One of the main conveniences
47 | of queries is that you have easy access to all fields of an element. For
48 | example, all queries support a `where` expression. Inside a `where` expression
49 | you can automatically use the variables `node`, `tag`, `attrs`,
50 | `attrs.{field}`, `content` and `type`. Lets illustrate this with an example of
51 | `node_match?/2`, the simplest query Eml provides:
52 |
53 | iex> use Eml
54 | iex> import Eml.Query
55 | iex> node = div [class: "test"], "Hello world"
56 | #div<%{class: "test"} "Hello world">
57 |
58 | iex> node_match? node, where: attrs.class == "test"
59 | true
60 |
61 | iex> node_match? node, where: tag in [:div, :span]
62 | true
63 |
64 | iex> node_match? node, where: tag == :span or is_binary(node)
65 | false
66 |
67 | Queries can be divided in two groups: queries that help collecting data from
68 | eml nodes and queries that help transform eml nodes.
69 |
70 | ## Transform queries
71 |
72 | * `put`: puts new data into a node
73 | * `update`: updates data in a node
74 | * `drop`: removes a node
75 | * `insert`: insert new content into a node
76 | * `append`: appends new content to a node
77 |
78 | ### Transfrom examples
79 |
80 | iex> use Eml
81 | iex> import Eml.Query
82 | iex> node = div(42)
83 | #div<42>
84 |
85 | iex> put node, "Hello World", in: content, where: content == 42
86 | #div<"Hello World">
87 |
88 | iex> insert node, 21, where: tag == :div
89 | #div<[21, 42]>
90 |
91 | iex> node = html do
92 | ...> head do
93 | ...> meta charset: "UTF-8"
94 | ...> end
95 | ...> body do
96 | ...> div class: "person" do
97 | ...> div [class: "person-name"], "mike"
98 | ...> div [class: "person-age"], 23
99 | ...> end
100 | ...> div class: "person" do
101 | ...> div [class: "person-name"], "john"
102 | ...> div [class: "person-age"], 42
103 | ...> end
104 | ...> end
105 | ...> end
106 |
107 | iex> Eml.transform(node, fn n ->
108 | ...> n
109 | ...> |> drop(where: tag == :head)
110 | ...> |> update(content, with: &(&1 + 1), where: attrs.class == "person-age")
111 | ...> |> update(content, with: &String.capitalize/1, where: attrs.class == "person-name")
112 | ...> |> insert(h1("Persons"), where: tag == :body)
113 | ...> |> append(div([class: "person-status"], "friend"), where: attrs.class == "person")
114 | ...> end)
115 | #html<[#body<[#h1<"Persons">, #div<%{class: "person"}
116 | [#div<%{class: "person-name"} "Mike">, #div<%{class: "person-age"} 24>,
117 | #div<%{class: "person-status"} "friend">]>, #div<%{class: "person"}
118 | [#div<%{class: "person-name"} "John">, #div<%{class: "person-age"} 43>,
119 | #div<%{class: "person-status"} "friend">]>]>]>
120 |
121 |
122 | ## Collect queries
123 |
124 | * `put`: get data from a node and put it in a map
125 | * `insert` get data from a node and insert it in a map
126 | * `append` get data from a node and append it to a map
127 |
128 | ### Collect examples
129 |
130 | iex> use Eml
131 | iex> import Eml.Query
132 |
133 | iex> node = html do
134 | ...> head do
135 | ...> meta charset: "UTF-8"
136 | ...> end
137 | ...> body do
138 | ...> div class: "person" do
139 | ...> div [class: "person-name"], "mike"
140 | ...> div [class: "person-age"], 23
141 | ...> end
142 | ...> div class: "person" do
143 | ...> div [class: "person-name"], "john"
144 | ...> div [class: "person-age"], 42
145 | ...> end
146 | ...> end
147 | ...> end
148 |
149 | iex> Eml.collect(node, fn n, acc ->
150 | ...> acc
151 | ...> |> append(n, content, in: :names, where: attrs.class == "person-name")
152 | ...> |> append(n, content, in: :ages, where: attrs.class == "person-age")
153 | ...> end)
154 | %{ages: [23, 42], names: ["mike", "john"]}
155 |
156 | iex> collect_person = fn person_node ->
157 | ...> Eml.collect(person_node, fn n, acc ->
158 | ...> acc
159 | ...> |> put(n, content, in: :name, where: attrs.class == "person-name")
160 | ...> |> put(n, content, in: :age, where: attrs.class == "person-age")
161 | ...> end)
162 | ...> end
163 |
164 | iex> Eml.collect(node, fn n, acc ->
165 | ...> append(acc, n, content, in: :persons, with: collect_person, where: tag == :div and attrs.class == "person")
166 | ...> end)
167 | %{persons: [%{age: 23, name: "mike"}, %{age: 42, name: "john"}]}
168 |
169 | ## Chaining queries with `pipe`
170 |
171 | `Eml.Query` also provide the `pipe/3` macro that makes chains of queries more
172 | readable. The first collect example could be rewritten with the `pipe` macro
173 | like this:
174 |
175 | iex> Eml.collect(node, fn n, acc ->
176 | ...> pipe acc, inject: n do
177 | ...> append content, in: :names, where: attrs.class == "person-name"
178 | ...> append content, in: :ages, where: attrs.class == "person-age"
179 | ...> end
180 | ...> end)
181 | %{ages: [23, 42], names: ["mike", "john"]}
182 | """
183 |
184 | @doc """
185 | Puts new data into a node
186 |
187 | iex> node = div(42)
188 | #div<42>
189 |
190 | iex> put node, "Hello World", in: content, where: content == 42
191 | #div<"Hello World">
192 |
193 | iex> put node, "article", in: attrs.class, where: content == 42
194 | #div<%{class: "article"} 42>
195 | """
196 | defmacro put(node, expr, opts) do
197 | build_transform(node, prepare_where(opts), fetch!(opts, :in), expr)
198 | end
199 |
200 | @doc """
201 | Updates data in a node
202 |
203 | iex> node = div do
204 | ...> span 21
205 | ...> span 101
206 | ...> end
207 | #div<[#span<21>, #span<101>]>
208 |
209 | iex> Eml.transform node, fn n ->
210 | ...> update n, content, with: &(&1 * 2), where: tag == :span
211 | ...> end
212 | #div<[#span<42>, #span<202>]>
213 | """
214 | defmacro update(node, var, opts) do
215 | validate_var(var)
216 | expr = Macro.prewalk(var, &handle_attrs/1)
217 | expr = quote do: unquote(fetch!(opts, :with)).(unquote(expr))
218 | build_transform(node, prepare_where(opts), var, expr)
219 | end
220 |
221 | @doc """
222 | Removes a node in a tree
223 |
224 | iex> node = div do
225 | ...> span [class: "remove-me"], 21
226 | ...> span 101
227 | ...> end
228 | #div<[#span<%{class: "remove-me"} 21>, #span<101>]>
229 |
230 | iex> Eml.transform node, fn n ->
231 | ...> drop n, where: attrs.class == "remove-me"
232 | ...> end
233 | #div<[#span<101>]>
234 | """
235 | defmacro drop(node, opts) do
236 | quote do
237 | unquote(inject_vars(node))
238 | if unquote(prepare_where(opts)), do: nil, else: var!(node)
239 | end
240 | end
241 |
242 | @doc """
243 | Inserts content into an element node
244 |
245 | iex> node = div do
246 | ...> span 42
247 | ...> end
248 | #div<[#span<42>]>
249 |
250 | iex> Eml.transform(node, fn n ->
251 | ...> insert n, 21, where: is_integer(content)
252 | ...> end
253 | #div<[#span<[21, 42]>]>
254 | """
255 | defmacro insert(node, expr, opts) do
256 | build_add(node, expr, opts, :insert)
257 | end
258 |
259 | @doc """
260 | Appends content into an element node
261 |
262 | iex> node = div do
263 | ...> span 42
264 | ...> end
265 | #div<[#span<42>]>
266 |
267 | iex> Eml.transform(node, fn n ->
268 | ...> append n, 21, where: is_integer(content)
269 | ...> end
270 | #div<[#span<[42, 21]>]>
271 | """
272 | defmacro append(node, expr, opts) do
273 | build_add(node, expr, opts, :append)
274 | end
275 |
276 | @doc """
277 | Collects data from a node and puts it in a map
278 |
279 | Optionally accepts a `:with` function that allows processing matched
280 | data before it's being stored in the map.
281 |
282 | iex> node = ul do
283 | ...> li "Hello World"
284 | ...> li 42
285 | ...> end
286 | #ul<[#li<"Hello World">, #li<42>]>
287 |
288 | iex> Eml.collect(node, fn n, acc ->
289 | ...> pipe acc, inject: n do
290 | ...> put content, in: :number, where: is_integer(content)
291 | ...> put content, in: :text, with: &String.upcase/1, where: is_binary(content)
292 | ...> end
293 | ...> end)
294 | %{number: 42, text: "HELLO WORLD"}
295 | """
296 | defmacro put(acc, node, var, opts) do
297 | build_collect(acc, node, var, opts, :put)
298 | end
299 |
300 | @doc """
301 | Collects data from a node and inserts at a given key in a map
302 |
303 | See `Eml.Query.put/4` for more info.
304 |
305 | iex> node = ul do
306 | ...> li "Hello World"
307 | ...> li 42
308 | ...> end
309 | #ul<[#li<"Hello World">, #li<42>]>
310 |
311 | iex> Eml.collect(node, fn n, acc ->
312 | ...> pipe acc, inject: n do
313 | ...> insert content, in: :list, where: is_integer(content)
314 | ...> insert content, in: :list, where: is_binary(content)
315 | ...> end
316 | ...> end)
317 | %{list: [42, "Hello World"]}
318 | """
319 | defmacro insert(acc, node, var, opts) do
320 | build_collect(acc, node, var, opts, :insert)
321 | end
322 |
323 | @doc """
324 | Collects data from a node and appends at a given key in a map
325 |
326 | See `Eml.Query.put/4` for more info.
327 |
328 | iex> node = ul do
329 | ...> li "Hello World"
330 | ...> li 42
331 | ...> end
332 | #ul<[#li<"Hello World">, #li<42>]>
333 |
334 | iex> Eml.collect(node, fn n, acc ->
335 | ...> pipe acc, inject: n do
336 | ...> append content, in: :list, where: is_integer(content)
337 | ...> append content, in: :list, where: is_binary(content)
338 | ...> end
339 | ...> end)
340 | %{list: ["Hello World", 42]}
341 | """
342 | defmacro append(acc, node, var, opts) do
343 | build_collect(acc, node, var, opts, :append)
344 | end
345 |
346 | @doc """
347 | Allows convenient chaing of queries
348 |
349 | See `Eml.Query.put/4` for an example.
350 | """
351 | defmacro pipe(x, opts \\ [], do: block) do
352 | { :__block__, _, calls } = block
353 | calls = if arg = Keyword.get(opts, :inject) do
354 | insert_arg(calls, arg)
355 | else
356 | calls
357 | end
358 | pipeline = build_pipeline(x, calls)
359 | quote do
360 | unquote(pipeline)
361 | end
362 | end
363 |
364 | @doc """
365 | Returns true if the node matches the `where` expression
366 |
367 | iex> node = div [class: "match-me"], 101
368 | #div<%{class: "match-me"} 101>
369 |
370 | iex> match_node? node, where: attrs.class == "match-me"
371 | true
372 | """
373 | defmacro match_node?(node, opts) do
374 | quote do
375 | unquote(inject_vars(node))
376 | unquote(prepare_where(opts))
377 | end
378 | end
379 |
380 | defp build_transform(node, where, var, expr) do
381 | key = get_key(var)
382 | quote do
383 | unquote(inject_vars(node))
384 | if unquote(where) do
385 | Eml.Query.Helper.do_transform(var!(node), unquote(key), unquote(expr))
386 | else
387 | var!(node)
388 | end
389 | end
390 | end
391 |
392 | defp build_add(node, expr, opts, op) do
393 | quote do
394 | unquote(inject_vars(node))
395 | if unquote(prepare_where(opts)) do
396 | Eml.Query.Helper.do_add(var!(node), unquote(expr), unquote(op))
397 | else
398 | var!(node)
399 | end
400 | end
401 | end
402 |
403 | defp build_collect(acc, node, var, opts, op) do
404 | validate_var(var)
405 | key = fetch!(opts, :in)
406 | expr = Macro.prewalk(var, &handle_attrs/1)
407 | expr = if fun = Keyword.get(opts, :with) do
408 | quote do: unquote(fun).(unquote(expr))
409 | else
410 | expr
411 | end
412 | quote do
413 | acc = unquote(acc)
414 | unquote(inject_vars(node))
415 | if unquote(prepare_where(opts)) do
416 | Eml.Query.Helper.do_collect(unquote(op), acc, unquote(key), unquote(expr))
417 | else
418 | acc
419 | end
420 | end
421 | end
422 |
423 | defp inject_vars(node) do
424 | quote do
425 | var!(node) = unquote(node)
426 | { var!(tag), var!(attrs), var!(content), var!(type) } =
427 | case var!(node) do
428 | %Eml.Element{tag: tag, attrs: attrs, content: content, type: type} ->
429 | { tag, attrs, content, type }
430 | _ ->
431 | { nil, %{}, nil, nil }
432 | end
433 | _ = var!(node); _ = var!(tag); _ = var!(attrs)
434 | _ = var!(content); _ = var!(type)
435 | end
436 | end
437 |
438 | defp get_key({ key, _, _ }) when key in ~w(node tag attrs content type)a do
439 | key
440 | end
441 | defp get_key({ { :., _, [{ :attrs, _, _ }, key] }, _, _ }) do
442 | { :attrs, key }
443 | end
444 | defp get_key(expr) do
445 | raise Eml.QueryError,
446 | message: "only `node`, `tag`, `attrs`, `attrs.{field}, `content` and `type` are valid, got: #{Macro.to_string(expr)}"
447 | end
448 |
449 | defp validate_var(expr) do
450 | get_key(expr)
451 | :ok
452 | end
453 |
454 | defp fetch!(keywords, key) do
455 | case Keyword.get(keywords, key, :__undefined) do
456 | :__undefined ->
457 | raise Eml.QueryError, message: "Missing `#{inspect key}` option"
458 | value ->
459 | value
460 | end
461 | end
462 |
463 | defp prepare_where(opts) do
464 | Macro.prewalk(fetch!(opts, :where), &handle_attrs/1)
465 | end
466 |
467 | defp build_pipeline(expr, calls) do
468 | Enum.reduce(calls, expr, fn call, acc ->
469 | Macro.pipe(acc, call, 0)
470 | end)
471 | end
472 |
473 | defp handle_attrs({ { :., _, [{ :attrs, _, _ }, key] }, meta, _ }) do
474 | line = Keyword.get(meta, :line, 0)
475 | quote line: line do
476 | Map.get(var!(attrs), unquote(key))
477 | end
478 | end
479 | defp handle_attrs(expr) do
480 | expr
481 | end
482 |
483 | defp insert_arg(calls, arg) do
484 | Enum.map(calls, fn
485 | { name, meta, args } when is_atom(name) and (is_list(args) or is_atom(args)) ->
486 | args = if is_atom(args), do: [], else: args
487 | { name, meta, [arg | args] }
488 | _ ->
489 | raise Eml.QueryError, message: "invalid pipeline"
490 | end)
491 | end
492 | end
493 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Eml.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [ app: :eml,
6 | version: "0.9.0-dev",
7 | name: "Eml",
8 | source_url: "https://github.com/zambal/eml",
9 | homepage_url: "https://github.com/zambal/eml",
10 | deps: deps(),
11 | description: description(),
12 | package: package() ]
13 | end
14 |
15 | def application do
16 | []
17 | end
18 |
19 | def deps do
20 | [ { :ex_doc, "~> 0.9", only: :docs },
21 | { :earmark, "~> 0.1", only: :docs } ]
22 | end
23 |
24 | defp description do
25 | """
26 | Eml makes markup a first class citizen in Elixir. It provides a
27 | flexible and modular toolkit for generating, parsing and
28 | manipulating markup. It's main focus is html, but other markup
29 | languages could be implemented as well.
30 | """
31 | end
32 |
33 | defp package do
34 | [ files: ["lib", "priv", "mix.exs", "README*", "readme*", "LICENSE*", "license*", "CHANGELOG*"],
35 | contributors: ["Vincent Siliakus"],
36 | licenses: ["Apache 2.0"],
37 | links: %{
38 | "GitHub" => "https://github.com/zambal/eml",
39 | "Walkthrough" => "https://github.com/zambal/eml/blob/master/README.md",
40 | "Documentation" => "https://hexdocs.pm/eml/"
41 | } ]
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/test/eml_test.exs:
--------------------------------------------------------------------------------
1 | defmodule EmlTest.Fragment do
2 | use Eml
3 |
4 | fragment my_fragment do
5 | div class: @class do
6 | h1 @title
7 | @__CONTENT__
8 | end
9 | end
10 | end
11 |
12 | defmodule EmlTest.Template do
13 | use Eml
14 |
15 | import EmlTest.Fragment, warn: false
16 |
17 | template my_template,
18 | title: &String.upcase/1,
19 | paragraphs: &(for par <- &1, do: p par) do
20 | my_fragment class: @class, title: @title do
21 | h3 "Paragraphs"
22 | @paragraphs
23 | end
24 | end
25 | end
26 |
27 | defmodule EmlTest do
28 | use ExUnit.Case
29 | use Eml
30 |
31 | alias Eml.Element, as: M
32 |
33 | defp doc() do
34 | %M{tag: :html, content: [
35 | %M{tag: :head, attrs: %{class: "test"}, content: [
36 | %M{tag: :title, attrs: %{class: "title"}, content: "Eml is HTML for developers"}
37 | ]},
38 | %M{tag: :body, attrs: %{class: "test main"}, content: [
39 | %M{tag: :h1, attrs: %{class: "title"}, content: "Eml is HTML for developers"},
40 | %M{tag: :article, attrs: %{class: "content", "data-custom": "some custom attribute"},
41 | content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam suscipit non neque pharetra dignissim."},
42 | %M{tag: :div, attrs: %{id: "main-side-bar", class: "content side-bar"},
43 | content: [%M{tag: :span, attrs: %{class: "test"}, content: "Some notes..."}]}
44 | ]}
45 | ]}
46 | end
47 |
48 | test "Element macro" do
49 | doc = html do
50 | head class: "test" do
51 | title [class: "title"], "Eml is HTML for developers"
52 | end
53 | body class: "test main" do
54 | h1 [class: "title"], "Eml is HTML for developers"
55 | article [class: "content", "data-custom": "some custom attribute"],
56 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam suscipit non neque pharetra dignissim."
57 | div id: "main-side-bar", class: "content side-bar" do
58 | span [class: "test"], "Some notes..."
59 | end
60 | end
61 | end
62 |
63 | assert doc() == doc
64 | end
65 |
66 | test "Element macro arguments" do
67 | assert %M{tag: :div, attrs: %{}, content: nil} == div()
68 | assert %M{tag: :div, attrs: %{}, content: "content"} == div "content"
69 | assert %M{tag: :div, attrs: %{class: "some-class"}, content: nil} == div class: "some-class"
70 | assert %M{tag: :div, attrs: %{class: "some-class"}, content: "content"} == div [class: "some-class"], "content"
71 | assert %M{tag: :div, attrs: %{class: "some-class"}, content: ["content"]} == div [class: "some-class"], do: "content"
72 | assert %M{tag: :div, attrs: %{class: "some-class"}, content: ["content"]} == div [class: "some-class"], do: ["content"]
73 | assert %M{tag: :div, attrs: %{class: "some-class"}, content: ["content"]} == (div class: "some-class" do
74 | "content"
75 | end)
76 | end
77 |
78 | test "Element macro as match pattern" do
79 | span(%{id: id}, _) = %M{tag: :span, attrs: %{id: "test"}, content: []}
80 | assert "test" == id
81 | end
82 |
83 | test "Enumerate content" do
84 | assert true == Enum.member?(doc(), "Some notes...")
85 |
86 | e = [h1([class: "title"], "Eml is HTML for developers")]
87 | assert e == Enum.filter(doc(), fn
88 | %M{tag: :h1} -> true
89 | _ -> false
90 | end)
91 | end
92 |
93 | test "Compile => Parse => Compare" do
94 | # Parsing always return results in a list
95 | expected = [doc()]
96 | compiled = Eml.compile(doc())
97 | assert expected == Eml.parse(compiled)
98 | end
99 |
100 | test "Eml.Encoder protocol and encode" do
101 | assert "true" == Eml.Encoder.encode true
102 | assert_raise Protocol.UndefinedError, fn -> Eml.Encoder.encode({}) end
103 | end
104 |
105 | test "Unpack" do
106 | e = div 42
107 | assert 42 == Eml.unpack e
108 | assert "42" == Eml.unpack ["42"]
109 | assert "42" == Eml.unpack "42"
110 | assert "42" == Eml.unpack %M{tag: :div, attrs: %{}, content: ["42"]}
111 |
112 | e = [div(1), div(2)]
113 | assert [1, 2] == Eml.unpack e
114 | end
115 |
116 | test "Multi unpack" do
117 | multi = html do
118 | body do
119 | div [ span(1), span(2) ]
120 | div span(3)
121 | end
122 | end
123 | assert [[1, 2], 3] == Eml.unpack(multi)
124 | end
125 |
126 | test "Compiling" do
127 | e = quote do
128 | div id: @myid do
129 | div @fruit1
130 | div @fruit2
131 | end
132 | end
133 |
134 | { chunks, _ } = Eml.Compiler.precompile(fragment: true, do: e)
135 | |> Code.eval_quoted(assigns: [myid: "fruit", fruit1: "lemon", fruit2: "orange"])
136 | compiled = Eml.Compiler.concat(chunks, [])
137 |
138 | expected = { :safe, "" }
139 |
140 | assert expected == compiled
141 | end
142 |
143 | test "Templates" do
144 | e = quote do
145 | for _ <- 1..4 do
146 | div [], @fruit
147 | end
148 | end
149 | { chunks, _ } = Eml.Compiler.precompile(fragment: true, do: e)
150 | |> Code.eval_quoted(assigns: [fruit: "lemon"])
151 | compiled = Eml.Compiler.concat(chunks, [])
152 |
153 | expected = { :safe, "lemon
lemon
lemon
lemon
" }
154 |
155 | assert expected == compiled
156 | end
157 |
158 | test "fragment elements" do
159 | import EmlTest.Template
160 |
161 | el = my_template class: "some'class", title: "My&Title", paragraphs: [1, 2, 3]
162 |
163 | expected = ""
164 |
165 | assert Eml.compile(el) == expected
166 | end
167 |
168 | test "Quoted content in eml" do
169 | fruit = quote do: section @fruit
170 | qfruit = Eml.Compiler.precompile(fragment: true, do: fruit)
171 | aside = aside qfruit
172 | qaside = Eml.Compiler.compile(aside)
173 |
174 | expected = aside do
175 | section "lemon"
176 | end
177 |
178 | { chunks, _ } = Code.eval_quoted(qaside, assigns: [fruit: "lemon"])
179 | { :safe, string } = Eml.Compiler.concat(chunks, [])
180 |
181 | assert Eml.compile(expected) == string
182 | end
183 |
184 | test "Assigns and attribute compiling" do
185 | e = quote do
186 | div id: @id_assign,
187 | class: [@class1, " class2 ", @class3],
188 | _custom: @custom
189 | end
190 |
191 | { chunks, _ } = Eml.Compiler.precompile(fragment: true, do: e)
192 | |> Code.eval_quoted(assigns: [id_assign: "assigned",
193 | class1: "class1",
194 | class3: "class3",
195 | custom: 1])
196 | compiled = Eml.Compiler.concat(chunks, [])
197 |
198 | expected = { :safe, "" }
199 |
200 | assert expected == compiled
201 | end
202 |
203 | test "Content escaping" do
204 | expected = "Tom & Jerry"
205 | assert expected == Eml.escape "Tom & Jerry"
206 |
207 | expected = "Tom > Jerry"
208 | assert expected == Eml.escape "Tom > Jerry"
209 |
210 | expected = "Tom < Jerry"
211 | assert expected == Eml.escape "Tom < Jerry"
212 |
213 | expected = "hello "world""
214 | assert expected == Eml.escape "hello \"world\""
215 |
216 | expected = "hello 'world'"
217 | assert expected == Eml.escape "hello 'world'"
218 |
219 | expected = div span("Tom & Jerry")
220 | assert expected == Eml.escape div span("Tom & Jerry")
221 |
222 | expected = "Tom & Jerry
"
223 | assert expected == Eml.compile div span("Tom & Jerry")
224 |
225 | expected = "Tom & Jerry
"
226 | assert expected == Eml.compile div span({ :safe, "Tom & Jerry" })
227 | end
228 |
229 | test "Content unescaping" do
230 | expected = "Tom & Jerry"
231 | assert expected == Eml.unescape "Tom & Jerry"
232 |
233 | expected = "Tom > Jerry"
234 | assert expected == Eml.unescape "Tom > Jerry"
235 |
236 | expected = "Tom < Jerry"
237 | assert expected == Eml.unescape "Tom < Jerry"
238 |
239 | expected = "hello \"world\""
240 | assert expected == Eml.unescape "hello "world""
241 |
242 | expected = "hello 'world'"
243 | assert expected == Eml.unescape "hello 'world'"
244 |
245 | expected = div span("Tom & Jerry")
246 | assert expected == Eml.unescape div span("Tom & Jerry")
247 | end
248 |
249 | test "Precompile transform" do
250 | e = div do
251 | span "hallo "
252 | span "world"
253 | end
254 | expected = "HALLO WORLD
"
255 |
256 | assert expected == Eml.compile(e, transform: &(if is_binary(&1), do: String.upcase(&1), else: &1))
257 | end
258 |
259 | test "Element casing" do
260 | name = :some_long_element_name
261 |
262 | assert name == Eml.Element.Generator.do_casing(name, :snake)
263 | assert :"SOME_LONG_ELEMENT_NAME" == Eml.Element.Generator.do_casing(name, :snake_upcase)
264 | assert :"SomeLongElementName" == Eml.Element.Generator.do_casing(name, :pascal)
265 | assert :"someLongElementName" == Eml.Element.Generator.do_casing(name, :camel)
266 | assert :"some-long-element-name" == Eml.Element.Generator.do_casing(name, :lisp)
267 | assert :"SOME-LONG-ELEMENT-NAME" == Eml.Element.Generator.do_casing(name, :lisp_upcase)
268 | end
269 |
270 | test "Eml transform queries" do
271 | import Eml.Query
272 |
273 | e = div do
274 | span [class: "upcase-me"], "hallo "
275 | span ["world", "?"]
276 | end
277 |
278 | transformed = Eml.transform(e, fn node ->
279 | pipe node do
280 | update content, with: &String.upcase/1, where: attrs.class == "upcase-me"
281 | append "!", where: is_list(content) and "world" in content
282 | drop where: node == "?"
283 | end
284 | end)
285 |
286 | expected = div do
287 | span [class: "upcase-me"], "HALLO "
288 | span ["world", "!"]
289 | end
290 |
291 | assert transformed == expected
292 | end
293 |
294 | test "Eml collect queries" do
295 | import Eml.Query
296 |
297 | collected = Eml.collect(doc(), fn node, acc ->
298 | pipe acc, inject: node do
299 | put content, in: :title, where: attrs.class == "title" and tag == :title
300 | insert attrs, in: :content_attrs, where: not is_nil(attrs.class) and String.contains?(attrs.class, "content")
301 | put tag, in: :main_tag, with: &Atom.to_string/1, where: not is_nil(attrs.class) and String.contains?(attrs.class, "main")
302 | append tag, in: :tags_with_id, where: not is_nil(attrs.id)
303 | end
304 | end)
305 |
306 | expected = %{
307 | title: "Eml is HTML for developers",
308 | content_attrs: [
309 | %{id: "main-side-bar", class: "content side-bar"},
310 | %{class: "content", "data-custom": "some custom attribute"}
311 | ],
312 | main_tag: "body",
313 | tags_with_id: [:div]
314 | }
315 |
316 | assert collected == expected
317 | end
318 | end
319 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start
2 |
--------------------------------------------------------------------------------