├── .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 | [![Build Status](https://api.travis-ci.org/zambal/eml.svg?branch=master)](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 |
36 |
37 | name: 38 | Vincent 39 |
40 |
41 | age: 42 | 36 43 |
44 |
45 | ``` 46 | 47 | ### Why? 48 | Most templating libraries are build around the idea of interpreting strings that 49 | can contain embeded code. This code is mostly used for implementing view logic 50 | in the template. You could say that these libraries are making code a first 51 | class citizen in template strings. As long as the view logic is simple this 52 | works pretty well, but with more complex views this can become quite messy. Eml 53 | takes this idea inside out and makes the markup that you normally would write as 54 | a string a first class citizen of the programming language, allowing you to 55 | compose and organize markup and view logic with all the power of Elixir. 56 | 57 | Please read on for a walkthrough that tries to cover most of Eml's features. 58 | 59 | 60 | ### Walkthrough 61 | 62 | - [Intro](#intro) 63 | - [Compiling](#compiling) 64 | - [Parsing](#parsing) 65 | - [Compiling and templates](#compiling-and-templates) 66 | - [Components and fragments](#components-and-fragments) 67 | - [Unpacking](#unpacking) 68 | - [Querying eml](#querying-eml) 69 | - [Transforming eml](#transforming-eml) 70 | - [Encoding data in Eml](#encoding-data-in-eml) 71 | - [Notes](#notes) 72 | 73 | #### Intro 74 | 75 | ```elixir 76 | iex> use Eml 77 | nil 78 | iex> use Eml.HTML 79 | nil 80 | ``` 81 | 82 | By invoking `use Eml`, some macro's are imported into the current scope and core 83 | API modules are aliased. `use Eml.HTML` imports all generated html element 84 | macros from `Eml.HTML` into the current scope. The element macros just translate 85 | to a call to `%Eml.Element{...}`, a struct that is the actual representation of 86 | elements, so they can even be used inside a match. 87 | ```elixir 88 | iex> div 42 89 | #div<42> 90 | ``` 91 | Here we created a `div` element with `42` as it contents. 92 | 93 | The element macro's in Eml try to be clever about the type of arguments that get 94 | passed. For example, if the first argument is a Keyword list, it will be 95 | interpreted as attributes, otherwise as content. 96 | ```elixir 97 | iex> div id: "some-id" 98 | #div<%{id: "some-id"}> 99 | 100 | iex> div "some content" 101 | #div<"some content"> 102 | 103 | iex> div do 104 | ...> "some content" 105 | ...> end 106 | #div<["some content"]> 107 | 108 | iex> div [id: "some-id"], "some content" 109 | #div<%{id: "some-id"} "some content"> 110 | 111 | iex> div id: "some-id" do 112 | ...> "some content" 113 | ...> end 114 | #div<%{id: "some-id"} ["some content"]> 115 | 116 | iex> %Element{tag: :div, attrs: %{id: "some-id"}, content: "some content"} 117 | #div<%{id: "some-id"} "some content"> 118 | ``` 119 | 120 | Note that attributes are stored internally as a map. 121 | 122 | 123 | #### Compiling 124 | 125 | Contents can be compiled to a string by calling `Eml.compile`. Eml automatically 126 | inserts a doctype declaration when the html element is the root. 127 | ```elixir 128 | iex> html(body(div(42))) |> Eml.compile 129 | "\n
42
\n" 130 | 131 | iex> "text & more" |> div |> body |> html |> Eml.compile 132 | "\n
text & more
" 133 | ``` 134 | As you can see, you can also use Elixir's pipe operator for creating markup. 135 | However, using do blocks, as can be seen in the introductory example, is more 136 | convenient most of the time. 137 | 138 | #### Parsing 139 | 140 | Eml's parser by default converts a string with html content into Eml content. 141 | ```elixir 142 | iex> Eml.parse "\n
42
" 143 | [#html<[#head<[#meta<%{charset: "UTF-8"}>]>, #body<[#div<"42">]>]>] 144 | 145 | iex> Eml.parse "

Title

blah & blah

" 146 | [#div<%{class: "content article"} 147 | [#h1<%{class: "title"} 148 | ["Title", #h1<[#p<%{class: "paragraph"} "blah & blah">]>]>]>] 149 | ``` 150 | 151 | The html parser is primarily written to parse html compiled by Eml, but it's 152 | flexible enough to parse most html you throw at it. Most notable missing 153 | features of the parser are attribute values without quotes and elements that are 154 | not properly closed. 155 | 156 | 157 | #### Compiling and templates 158 | 159 | Compiling and templates can be used in situations where most content is static 160 | and performance is critical, since its contents gets precompiled during compiletime. 161 | 162 | Eml uses the assigns extension from `EEx` for parameterizing templates. See 163 | the `EEx` docs for more info about them. The function that the template macro 164 | defines accepts optionally any Dict compatible dictionary as argument for 165 | binding values to assigns. 166 | 167 | ```elixir 168 | iex> defmodule MyTemplates1 do 169 | ...> use Eml 170 | ...> use Eml.HTML 171 | ...> 172 | ...> template example do 173 | ...> div id: "example" do 174 | ...> span @text 175 | ...> end 176 | ...> end 177 | ...> end 178 | iex> MyTemplates.example text: "Example text" 179 | {:safe, "
Example text
"} 180 | ``` 181 | 182 | Eml templates provides two ways of executing logic during runtime. By 183 | providing assigns handlers to the optional `funs` dictionary, or by calling 184 | external functions during runtime with the `&` operator. 185 | 186 | ```elixir 187 | iex> defmodule MyTemplates2 do 188 | ...> use Eml 189 | ...> use Eml.HTML 190 | ...> 191 | ...> template assigns_handler, 192 | ...> text: &String.upcase/1 do 193 | ...> div id: "example1" do 194 | ...> span @text 195 | ...> end 196 | ...> end 197 | ...> 198 | ...> template external_call do 199 | ...> body &assigns_handler(text: @example_text) 200 | ...> end 201 | ...> end 202 | iex> MyTemplates.assigns_handler text: "Example text" 203 | {:safe, "
EXAMPLE TEXT
"} 204 | iex> MyTemplates.exernal_call example_text: "Example text" 205 | {:safe, "
EXAMPLE TEXT
"} 206 | ``` 207 | 208 | Templates are composable, so they are allowed to call other templates. The 209 | only catch is that it's not possible to pass an assign to another template 210 | during precompilation. The reason for this is that the logic in a template is 211 | executed the moment the template is called, so if you would pass an assign 212 | during precompilation, the logic in a template would receive this assign 213 | instead of its result, which is only available during runtime. This all means 214 | that when you for example want to pass an assign to a nested template, the 215 | template should be prefixed with the `&` operator, or in other words, executed 216 | during runtime. 217 | 218 | ```elixir 219 | template templ1, 220 | num: &(&1 + &1) do 221 | div @num 222 | end 223 | 224 | template templ2 do 225 | h2 @title 226 | templ1(num: @number) # THIS GENERATES A COMPILE TIME ERROR 227 | &templ1(num: @number) # THIS IS OK 228 | end 229 | ``` 230 | 231 | Note that because the body of a template is evaluated at compile time, it's 232 | not possible to call other functions from the same module without using `&` 233 | operator. 234 | 235 | Instead of defining a do block, you can also provide a path to a file with the 236 | `:file` option. 237 | 238 | ```elixir 239 | iex> File.write! "test.eml.exs", "div @number" 240 | iex> defmodule MyTemplates3 do 241 | ...> use Eml 242 | ...> use Eml.HTML 243 | ...> 244 | ...> template from_file, file: "test.eml.exs" 245 | ...> end 246 | iex> File.rm! "test.eml.exs" 247 | iex> MyTemplates3.from_file number: 42 248 | {:safe, "
42
"} 249 | ``` 250 | 251 | #### Components and fragments 252 | 253 | Eml also provides `component/3` and `fragment/3` macros for defining 254 | template elements. They behave as normal elements, but they 255 | aditionally contain a template function that gets called with the 256 | element's attributes and content as arguments during compiling. 257 | 258 | ```elixir 259 | iex> use Eml 260 | iex> use Eml.HTML 261 | iex> defmodule ElTest do 262 | ...> 263 | ...> component my_list, 264 | ...> __CONTENT__: fn content -> 265 | ...> for item <- content do 266 | ...> li do 267 | ...> span "* " 268 | ...> span item 269 | ...> span " *" 270 | ...> end 271 | ...> end 272 | ...> end do 273 | ...> ul [class: @class], @__CONTENT__ 274 | ...> end 275 | ...> 276 | ...> end 277 | iex> import ElTest 278 | iex> el = my_list class: "some-class" do 279 | ...> "Item 1" 280 | ...> "Item 2" 281 | ...> end 282 | #my_list<%{class: "some-class"} ["Item 1", "Item 2"]> 283 | iex> Eml.compile(el) 284 | "" 285 | ``` 286 | 287 | Just like templates, its body gets precompiled and assigns, assign 288 | handlers and function calls prefixed with the `operator` are evaluated 289 | at runtime. All attributes of the element can be accessed as assigns 290 | and the element contents is accessable as the assign `@__CONTENT__`. 291 | 292 | The main difference between templates and components is their 293 | interface. You can use components like normal elements, even within a 294 | match. 295 | 296 | In addition to components, Eml also provides fragments. The difference 297 | between components and fragments is that fragments are without any 298 | logic, so assign handlers or the `&` capture operator are not allowed 299 | in fragment definitions. 300 | 301 | Fragments can be used for better composability and performance, 302 | because unlike templates and components, assigns are allowed as 303 | arguments during precompilation for fragments. This is possible 304 | because fragments don't contain any logic. 305 | 306 | 307 | #### Unpacking 308 | 309 | In order to easily access the contents of elements, Eml provides `unpack/1`. 310 | ```elixir 311 | iex> Eml.unpack div 42 312 | 42 313 | 314 | iex> Eml.unpack div span(42) 315 | 42 316 | ``` 317 | 318 | 319 | #### Querying eml 320 | 321 | `Eml.Element` implements the Elixir `Enumerable` protocol for traversing a tree of 322 | nodes. Let's start with creating something to query 323 | ```elixir 324 | iex> e = html do 325 | ...> head class: "head" do 326 | ...> meta charset: "UTF-8" 327 | ...> end 328 | ...> body do 329 | ...> article id: "main-content" do 330 | ...> section class: "article" do 331 | ...> h3 "Hello world" 332 | ...> end 333 | ...> section class: "article" do 334 | ...> "TODO" 335 | ...> end 336 | ...> end 337 | ...> end 338 | ...> end 339 | #html<[#head<%{class: "head"} [#meta<%{charset: "UTF-8"}>]>, 340 | #body<[#article<%{id: "main-content"} 341 | [#section<%{class: "article"} [#h3<"Hello world">]>, 342 | #section<%{class: "article"} ["TODO"]>]>]>]> 343 | ``` 344 | To get an idea how the tree is traversed, first just print all nodes 345 | ```elixir 346 | iex> Enum.each(e, fn x -> IO.puts(inspect x) end) 347 | #html<[#head<%{class: "head"} [#meta<%{charset: "UTF-8"}>]>, #body<[#article<%{id: "main-content"} [#section<%{class: "article"} [#h3<"Hello world">]>, #section<%{class: "article"} ["TODO"]>]>]>]> 348 | #head<%{class: "head"} [#meta<%{charset: "UTF-8"}>]> 349 | #meta<%{charset: "UTF-8"}> 350 | #body<[#article<%{id: "main-content"} [#section<%{class: "article"} [#h3<"Hello world">]>, #section<%{class: "article"} ["TODO"]>]>]> 351 | #article<%{id: "main-content"} [#section<%{class: "article"} [#h3<"Hello world">]>, #section<%{class: "article"} ["TODO"]>]> 352 | #section<%{class: "article"} [#h3<"Hello world">]> 353 | #h3<"Hello world"> 354 | "Hello world" 355 | #section<%{class: "article"} ["TODO"]> 356 | "TODO" 357 | :ok 358 | ``` 359 | 360 | As you can see every node of the tree is passed to `Enum`. Let's continue with 361 | some other examples. 362 | ```elixir 363 | iex> Enum.member?(e, "TODO") true 364 | 365 | iex> Enum.filter(e, &Eml.match?(&1, tag: :h3)) 366 | [#h3<"Hello world">] 367 | 368 | iex> Enum.filter(e, Eml.match?(&1, attrs: %{class: "article"})) 369 | [#section<%{class: "article"} [#h3<"Hello world">]>, 370 | #section<%{class: "article"} ["TODO"]>] 371 | ``` 372 | 373 | 374 | #### Transforming eml 375 | 376 | Eml also provides `Eml.transform/2`. `transform` mostly works like 377 | enumeration. The key difference is that `transform` returns a modified version 378 | of the eml tree that was passed as an argument, instead of collecting nodes in a 379 | list. `transform` passes any node it encounters to the provided transformation 380 | function. This transformer can return any data or `nil`, in which case the node 381 | is discarded, so it works a bit like a map and filter function in one pass. 382 | ```elixir 383 | iex> Eml.transform(e, fn 384 | ...> any(%{class: "article"}) = el -> %{el|content: "#"} 385 | ...> node -> node 386 | ...> end) 387 | #html<[#head<%{class: "head"} [#meta<%{charset: "UTF-8"}>]>, 388 | #body<[#article<%{id: "main-content"} 389 | [#section<%{class: "article"} "#">, #section<%{class: "article"} 390 | "#">]>]>]> 391 | 392 | iex> Eml.transform(e, fn 393 | ...> any(%{class: "article"}) = el -> %{el|content: "#"} 394 | ...> _ -> nil 395 | ...> end) 396 | nil 397 | ``` 398 | The last result may seem unexpected, but the result is `nil` because 399 | `Eml.transform` first evaluates a parent node, before continuing with its 400 | children. If the parent node gets removed, the children will be removed too and 401 | won't get evaluated. 402 | 403 | 404 | #### Encoding data in Eml 405 | 406 | In order to provide conversions from various data types, Eml provides the 407 | `Eml.Encoder` protocol. It is used internally by Eml's compiler. Eml provides a 408 | implementation for strings, numbers, tuples and atoms, but you can provide a 409 | protocol implementation for your own types by just implementing a `encode` 410 | function that converts your type to a valid `Eml.Compiler.node_primitive` type. 411 | 412 | Some examples using `Eml.encode` 413 | ```elixir 414 | 415 | iex> Eml.Encoder.encode 1 416 | "1" 417 | 418 | iex> Eml.Encoder.encode %{div: 42, span: 12} 419 | ** (Protocol.UndefinedError) protocol Eml.Encoder not implemented for %{div: 42, span: 12} 420 | 421 | iex> defimpl Eml.Encoder, for: Map 422 | ...> use Eml.HTML 423 | ...> def encode(data) do 424 | ...> for { k, v } <- data do 425 | ...> %Eml.Element{tag: k, content: v} 426 | ...> end 427 | ...> end 428 | ...> end 429 | iex> Eml.Encoder.encode %{div: 42, span: 12} 430 | [#div<42>, #span<12>] 431 | ``` 432 | 433 | ### Notes 434 | 435 | The first thing to note is that this is still a work in progress. 436 | While it should already be pretty stable and has quite a rich API, 437 | expect some raw edges here and there. 438 | 439 | #### Security 440 | Obviously, as Eml has full access to the Elixir environment, 441 | eml should only be written by developers that already have full access 442 | to the backend where Eml is used. Besides this, little thought has gone 443 | into other potential security issues. 444 | 445 | #### Validation 446 | Eml doesn't perform any validation on the produced output. 447 | You can add any attribute name to any element and Eml won't 448 | complain, as it has no knowledge of the type of markup that 449 | is to be generated. If you want to make sure that your eml code 450 | will be valid html, compile it to an html file and use this file with any 451 | existing html validator. In this sense Eml is the same as hand 452 | written html. 453 | 454 | #### HTML Parser 455 | The main purpose of the html parser is to parse back generated html 456 | from Eml. It's a custom parser written in about 500 LOC, 457 | so don't expect it to successfully parse every html in the wild. 458 | 459 | Most notably, it doesn't understand attribute values without quotes and arbitrary 460 | elements without proper closing, like `
`. 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 | "\nHello!!
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, "
  • * 1 *
  • * 2 *
"}] 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("", 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, "
lemon
orange
" } 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 = "

MY&TITLE

Paragraphs

1

2

3

" 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 | --------------------------------------------------------------------------------