[erlang-questions] [ANN] Brim - HTML templating library

Per Melin per.melin@REDACTED
Thu Mar 15 11:40:22 CET 2012


Brim is inspired by Enlive for Clojure, meaning that the HTML templates are completely free of the embedded code, annotations, special attributes, etc that we usually see. It also uses CSS selectors to reference the parts of the HTML we want to transform with Erlang.

https://github.com/permelin/brim

It is version 0.0.1 and while usable it's too early to be very useful yet. I'm announcing it anyway because I really want input on the API, which I am struggling with.

An example (since there is no documentation): A web page that shows a table with the applications running on our Erlang node. Here is the template, filled with some dummy text so that we can test what it will look like in a browser.

<table id="apps">
  <caption>node name will go here</caption>
  <tbody>
    <tr>
      <td>application</td>
      <td>yada yada yada</td>
      <td>version</td>
    </tr>
  </tbody>
</table>


Then the code that will transform the above HTML and fill it with data.

-module(running_apps).

-export([html/0]).    

html() ->
    % Read the template
    Doc0 = brim:document("priv/apps.html"),

    % Set the caption to the output from node()
    Doc1 = brim:content(Doc0, "#apps > caption", node()),

    % Fill the table tbody -- see rows/1 below
    Doc2 = brim:content(Doc1, "#apps > tbody", rows(Doc1)),

    % And to really show off the CSS selector support we'll add the
    % class "pointless" on all <tr> whose id does not start with "ker"
    Doc3 = brim:add_class(Doc2, "tr:not([id^=ker])", "pointless"),

    % Print to screen -- use brim:render/1 instead to get io list
    brim:print(Doc3).

rows(Doc) ->
    % Create one clone of the <tr> for each item returned from
    % application:which_application() and transform them as specified
    % by the fun.
    brim:map(
        Doc, "#apps tr", application:which_applications(),
        fun(TR0, {App, Description, Version}) ->
            TR1 = brim:content(TR0, "td:first-child", App),
            TR2 = brim:content(TR1, "td:nth-child(2)", Description),
            TR3 = brim:content(TR2, "td:last-child", Version),
            % And finally set the id of each <tr> to the name of the
            % corresponding application, for no good reason
            brim:id(TR3, "tr", atom_to_list(App))
        end).


And here is the output (formatted for readability):

<table id="apps">
  <caption>nonode@REDACTED</caption>
  <tbody>
    <tr id="inets" class="pointless">
      <td>inets</td>
      <td>INETS  CXC 138 49</td>
      <td>5.7.1</td>
    </tr>
    <tr id="stdlib" class="pointless">
      <td>stdlib</td>
      <td>ERTS  CXC 138 10</td>
      <td>1.17.5</td>
    </tr>
    <tr id="kernel">
      <td>kernel</td>
      <td>ERTS  CXC 138 10</td>
      <td>2.14.5</td>
    </tr>
  </tbody>
</table>


As you can see, there is a lot of explicit state threading (Doc0, Doc1, Doc2, Doc3, TR0, TR1, TR2, TR3) which I fear can get annoying when dealing with larger templates. I'm trying out different ways to minimize that (and verbosity in general). The most obvious (to me) is to move the state argument to last position to facilitate chaining.

html() ->
    Doc = brim:document("priv/apps.html"),
    brim:print(
      brim:add_class("tr:not([id^=ker])", "pointless",
        brim:content("#apps > caption", node(),
          brim:content("#apps > tbody", app_rows(Doc), Doc)))).

Another alternative below. But I think it has too many limitations to be feasible.

html() ->
    Doc = brim:document("priv/apps.html"),
    brim:do(Doc,
        [brim:content("#apps > caption", node()),
         brim:content("#apps > tbody", app_rows(Doc)),
         brim:add_class("tr:not([id^=ker])", "pointless"),
         brim:print()]).

As I said, all input is appreciated.




More information about the erlang-questions mailing list