Skip to content

mneumann/baml

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

39 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Baml

A balanced, whitespace-insensitive markup language for HTML-templating inspired by Slim and Haml.

Advantages

  • Whitespace insensitive!!!
  • Compact markup.
  • Safe: Context-sensitive escaping and interpolation.
  • Editor friendly: Can simply jump to the end of a block.

Whitespace insensitive

How can Ruby programmers use whitespace sensitive markup??? :)

Editor friendly

Without special syntax support, to jump to the end of a tag or expression, position your cursor on the {-character and press % in vim. Code folding should work seamlessly as well (haven't tried).

Examples

This Baml code below:

section.container {
  h1 ${ post.title }
  h2 ${ post.subtitle }
  .content ${ post.content }
}

is equivalent to this in Haml:

%section.container
  %h1= post.title
  %h2= post.subtitle
  .content
    = post.content

or this is Slim:

section.container
  h1 = post.title
  h2 = post.subtitle
  .content = post.content

or this is Erb:

<section class="container">
  <h1><%= post.title %></h1>
  <h2><%= post.subtitle %></h2>
  <div class="content">
    <%= post.content %>
  </div>
</section>

More Complex Example

doctype html
html {
  head {
    title "Baml Examples";
    meta name="keywords" content="template language"
    meta name="author" content=${ author }
  }
  body {
    h1 "Markup examples"
    
    #content {
      p ~{
        This example shows you how a basic Baml
        templating file looks like.
      }
    }
    
    // `!` is a non-nesting code line
    ! yield(self)

    // while `%` introduces a block which has to be
    // terminated by `}` w/o leading `%`. This makes
    // it beautifully line up with in-place HTML.
    // The last character of a `%` line must be a
    // `{`
    
    % if items.len() > 0 {
      table {
        % for &item in items.iter() {
          tr {
              td.name =  ${ item.name }
              td.price = ${ item.price }
          }
        }
      }
    }
    % else {
      p "No items found. Please add some inventory. Thank you!"
    }
    
    div id="footer" {
      ${ render("footer") }
      "Copyright ${ year } #{ author }"
    }
  }
}

Further specs

Interpolation

Not interpolated attributes use a single quote:

div id='${ not interpolated }' 

And interpolated attribute values:

div id="item${ item.id }"

Of course this can be used in the same way for text:

div '${not interpolated}'
div "${ page.title }"

produces:

<div>${not interpolated}</div>
<div>The title</div>

Interpolation also appears within ~{}-strings. If you need to emit a not interpolated string, simply use a single quote, or use a @raw modifier.

Template parameter types

Statically typed languages need the types of the parameters to be specified. I am thinking about something like this for example for Rust:

// file: article.baml
: article_id: uint
: article_title: &str
: article_content: &HtmlMarkup

div#id="article${ article_id }" {
  h2 ${ article_title }
  div.content ${ article_content }
}

Note that all strings are sanitized by default. If you want to insert raw html code (e.g. article_content) you have to use a separate type like HtmlMarkup. Of course this can only be expanded in a section where HTML is accepted, not in an attribute!

The template above would generate the following Rust code:

mod article_template {
  struct Args {
    article_id: uint,
    article_title: &str,
    article_content: &HtmlMarkup,
  }
  fn render(renderer: &mut BamlRenderer, args: &Args) { 
    let article_id = args.article_id;
    let article_title = args.article_title;
    let article_content = args.article_content;
    // ...
  }
}

You can render this template from within another template as shown below:

// page.baml
html {
  body {
    ! article_template::render(renderer,
    !                          article_template::Args {
    !                              article_id: 1,
    !                              article_title: &"My first post",
    !                              article_content: &HtmlMarkup(~"<div>123</div>")
    !                          });
  }
}

Or by using the special render! macro:

// page.baml
html {
  body {
    ! render!(article_template { article_id: 1,
    !                            article_title: &"My first post",
    !                            article_content: &HtmlMarkup(~"<div>123</div>")
    !                          });
  }
}

FIXME: I am not sure if this works due to hygienic macros.

Of course it's more realistic that you have an Article struct:

struct Article {
  id: uint,
  title: &str,
  content: &HtmlMarkup
}

The article template then becomes simpler:

// file: article.baml
: article: &Article

div#id="article${ article.id }" {
  h2 ${ article.title }
  div.content ${ article.content }
}

And the page template would then look like:

// page.baml
: articles: &[&Article]

html {
  body {
    % for article in articles.iter() {
      ! render!(article_template { article: article });
    }
  }
}

Much, much nicer!

Spec

All whitespaces are ignored, i.e. "beginning of a line" means that it may be preceded by any number of whitespaces. Newlines are no whitespaces. In some parts they are significant.

  • Template parameter definition: :, whole line.
  • Comment: /, extends till end of line.
  • Html tag: [a-zA-Z][a-zA-Z0-9]*. Either at the beginning of a line, after a ; or a {.
  • Html tag attribute: [a-zA-Z][a-zA-Z0-9_-]*, followed by an optional = and string or expression.
  • Html id: The special syntax #myid is supported, where myid is either an identifier, or a string. Equivalent to id=myid. Can also stand on it's own without a html tag, in which case a div tag is used.
  • Html css class: The special syntax .css_class is supported. The same applies here as for a Html id.
  • Tag content: Either by nesting by using { after a tag definition, or in-place for text content by using a string (p "content of paragraph") or an expression (div ${content}).
  • Tag delimiter: If you want to have multiple tags on one line, use ; to separte them: p "hallo"; p "welt".
  • Nesting code: Use the % at the beginning of a line. Expands to the end of the line. The last character of the line should be a { as this keeps the balance with the closing } intact. But for some languages this could be relaxed. In Ruby for example % for i in 1..10 would not need a trailing {. Here, if there is a trailing {|parameters|, the closing } would generate a }, otherwise it would generate end.
  • Non-nesting code: Use ! at the beginning of a line. Expands till the end of the line. If you have problems with nesting code in some languages, you can always emulate it with the !.
  • Inline Html: Every line that starts with < gets copied verbatim, useful for including HTML.
  • Strings: Either "..." or '...'. The first variant can include expressions ("id${ article.id }"), the second cannot. A third variant is useful for multiline strings and looks like { ... }. It is also possible to specify a special terminator as in ###{ ....... }###. Everything between ~ and { determines the terminator which has to repeated after the closing }.
  • HTML comments: Use \, expands till the end of the line.

Open Questions

Multi-line strings

Multi-line strings look like this ~{ multiline } and also allow a terminator to be specified:

div ~{
  This is a paragraph of text
}

div ~###{
  This is a paragraph of text containing a }
  which does not end the string!
}###

div @raw ~{
  Interpolation is not ${ performed }
}

By default, interpolation of ${...} expressions within these strings is performed, but this can be changed with the @raw modifier.

Filters

This can be used to correctly escape Javascript code:

@javascript "if (this.value > 2)"

This will correctly escape the > and generate if (this.value &gt; 2).

Depending on the context this will also generate the surround <script> tags.

For multiline javascript:

@javascript ~{
  function some() {
    return "Hallo";
  }
}

There can be only one filter coming before an expression. Note that the filter might modify the interpolation method used within the following string:

:myvalue: &str
div onclick=@javascript "alert(${ myvalue })"
div id="${ myvalue }"

The first will interpolate myvalue as a Javascript string while the second will interpolate it as a HTML attribute (without the surrounding "...").

HTML attributes

HTML allows something like:

<div id="id" disabled />

I support this in Baml:

div id="id" disabled

But I might want to change it towards always using the attr = value notation, plus a special _ value which means "ignore":

div id="id" disabled=_

This doesn't look as nice, but is more regular. The question is how frequent those attributes are. This is required to allow expressions for attribute names:

div ${myattr}="abc"

which is currently not supported, as a string or expression is always treated as the (inline) body of a tag (and as such ends the tag).

Including multiple attributes into a tag is not (yet) supported. But I could think of something like:

div *${myattributes}

In Ruby, this would make it possible to inject either a single attribute or multiple attributes:

myattributes = {:id => "abc", :class => "css"}

But I think in the situation where you want to inject attributes into a tag, the whole tag should be rendered programmatically, because injecting attributes would disallow static merging of attributes:

div.article class="featured" // => <div class="article featured" />

While when we allo dynamic attribute keys, this is more complicated and has to be done at template runtime. Also I think the use case of dynamic attribute keys is too rare to be useful!

Macros

Including other baml files into the current:

@include('test.baml')

Note that here the string MUST NOT contain interpolation expressions, as interpolation is done at run-time, while @include is evaluated at compile time.

If we extend the @filter syntax for arguments like in @pygments(ruby) then we can also introduce macros in the language:

@macro(FOOTER, name, year) {
  div#footer {
    "Copyright (c) " @expand(year) " by " @expand(name)
  }
}

html {
  body {
    @expand(FOOTER, "Michael Neumann", "2014")
  }
}

We would need to make filters aware of tags, i.e. the HTML nodes { ... } would be passed to the macro filter. And also we'd have to allow filters to use arguments optionally. Also macros would need to be allowed to come at the beginning of a statement.

The macro definitions would be local to the current node, i.e. local macros can be defined, local to all sub nodes.

This would make for nice code blocks are reusable template code (independent of the language; Ruby or Rust). I now think we DEFINITIVLY need to include this in Baml!

Macros can also be defined for texts, i.e. exapand to any node or expression.

@macro(AUTHOR) "Michael Neumann"

@macro(PAGE_TITLE) "Title: ${ page.title }"

About

Balanced Markup Language

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published