Introducing HSML
HSML
(shorthand for Haskellβs Simple Markup Language) is simple markup language with syntax similar to XML
and HTML
. It allows you to embed Haskell expressions and declarations in your templates.
There are two incarnations of HSML
-
Classic
HSML
(or justHSML
): this is the full blown templating system. These templates can contain expressions, declarations and template arguments. They are translated into record type and its instance ofIsTemplate
type class. -
Simplified
HSML
: this is a cut-down version of the full blown system, the only feature that is lacking are template arguments. SimplifiedHSML
templates are translated into expression of the typeText.Blaze.Markup
.
Valid simplified HSML
is valid HSML
. Valid HSML
with its arguments removed is also valid simplified HSML
.
You can find HSML
on hackage and follow its development on github.
Syntaxπ
Valid HSML
document starts with declarations of the templateβs arguments, this is the only syntactical difference from simplified HSML
.
After that follows a list of chunks, where chunk is either text, raw text, element node, element leaf or haskell, where haskell can be either Haskell expression or Haskell declaration.
What follows is a brief description with examples for every HSML
construct.
Textπ
The most basic chunk is text. Text can contain any characters, except for <
and {
which have to be escaped using \
(in fact, you can use \
to escape any character). Text is also automatically HTML
escaped.
Syntaxπ
? all characters except '<' and '{' which have to be escaped ?
Exampleπ
This is text that ends with less-than sign and opening curly bracket \<\{
Raw Textπ
Raw text is similar to text, but is rendered as is, that means without HTML
escaping. Raw text can contain any characters, but can not contain |}
as a substring (you can circumvent this by escaping).
Syntaxπ
"{r|" ? all characters, the sequence can not contain "|}" substring ? "|}"
Exampleπ
{r|This text is raw|}, but this is not. Do you want "|}" inside raw text?
No problem, do it like this: {r|foo |\} bar|}
Element nodeπ
Element node is an element that can contain other chunks. In HTML
it could be for example <div>...</div>
. Element nodes can have attributes. Attributes, attribute names and attribute values can also be expressions.
These additional requirements have to be met:
-
The attribute expressions have to be of type
Text.Blaze.Attribute
. -
The attribute name expressions have to be of type
String
. -
The attribute value expressions have to be of a type that is an instance of
Text.Blaze.ToValue
or of typeText.Blaze.AttributeValue
, depending on the template options.
Syntaxπ
"<" element_name { attribute } ">" { chunk } "</" element_name ">"
Exampleπ
<ul class={h|userID|}>
<li>Name: {h|userName|}</li>
<li>Age: {h|userAge|}</li>
</ul>
<div {h|name|}="static_value" static_name={h|value|}>
This div has attribute with dynamic name and an attribute with dynamic value.
</div>
<div {h|name|}={h|value|} {h|attribute|}>
This div has attribute with dynamic name and value.
</div>
<div {h|attribute|}>
This div has fully dynamic attribute. This allows for optional attributes.
</div>
Element leafπ
Element leaf is an element that can not contain any other chunks. In HTML
it could be for example <br/>
. Element leafs can also have attributes.
Syntaxπ
"<" element_name { attribute } "/>"
Exampleπ
Some things are just broken, <br/>
just like this line.
Haskellπ
The Haskell chunks in your templates can be either expressions or declarations.
Declarations are scoped, this means that if you have a declaration inside an element node, it can be used only inside that node (that also includes any nested chunks). Top level declarations (those that are not inside any element node) are visible in the whole template.
Expressions have to be of a type that is an instance of Text.Blaze.ToMarkup
or of type Text.Blaze.Markup
, depending on the template options.
Syntaxπ
"{h|" expression | declaration "|}"
Exampleπ
{h| omnipresent = "omnipresent" |}
<div>
{h| local = "first local" |}
<p>{h|omnipresent|}</p>
<p>{h|local|}</p>
</div>
<div class={h|omnipresent|}>
{h| local = "second local" |}
<p>{h|omnipresent|}</p>
<p>{h|local|}</p>
</div>
Argumentπ
Arguments get translated into fields of the record type, you can optionally specify the type of the field. If you decide to not specify the type, the type will become a parameter of the record type.
Syntaxπ
"{a|" <argument name> [ :: <argument type> ] "|}"
Exampleπ
{a| name :: String |}
{a| age :: Int |}
{a| mystery |}
And the mystery was: {h|mystery|}
Usageπ
If you have come this far, you might be interested in trying out HSML
. The Template.HSML
module exports all that you need and I recommend you to read the documentation or ask me to improve it if you happen to find it lacking.
Often the most efficient way to learn is by example, so here is one:
Main.hsπ
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
------------------------------------------------------------------------------
import Control.Monad
------------------------------------------------------------------------------
import Data.Monoid ((<>))
------------------------------------------------------------------------------
import qualified Text.Blaze.Html5 as B
------------------------------------------------------------------------------
import Template.HSML
------------------------------------------------------------------------------
data User = User
{ userID :: Int
, userName :: String
, userAge :: Int
}
$(hsmlFileWith (defaultOptions "Default") "default_layout.hsml")
homeTemplate :: [User] -> B.Markup
homeTemplate users = renderTemplate Default
{ defaultTitle = "Home page"
, defaultSectionMiddle = middle
, defaultSectionFooter = [m| <p>Generated by HSML</p> |]
}
where
middle = [m|
<ul class="users">
{h| forM_ users wrap |}
</ul> |]
wrap u = [m|<li> {h| userTemplate u |} </li>|]
userTemplate :: User -> B.Markup
userTemplate User{..} = [m|
<ul class={h| "user-" <> show userID |}>
<li>Name: {h|userName|}</li>
<li>Age: {h|userAge|}</li>
</ul> |]
default_layout.hsmlπ
{a| title :: String |}
{a| sectionMiddle :: B.Markup |}
{a| sectionFooter :: B.Markup |}
{h| B.docType |}
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>{h|title|}</title>
</head>
<body>
<div class="section middle">
{h|sectionMiddle|}
</div>
<footer>
{h|sectionFooter|}
</footer>
</body>
</html>
And the prettified result of renderMarkup $ homeTemplate [User 1 "Jon Doe" 16, User 2 "Jane Roe" 17]
is this:
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Home page</title>
</head>
<body>
<div class="section middle">
<ul class="users">
<li>
<ul class="user-1">
<li>Name: Jon Doe</li>
<li>Age: 16</li>
</ul>
</li>
<li>
<ul class="user-2">
<li>Name: Jane Roe</li>
<li>Age: 17</li>
</ul>
</li>
</ul>
</div>
<footer>
<p>Generated by HSML</p>
</footer>
</body>
</html>
Thoughtsπ
Currently, nesting HSML
within expressions or declarations within HSML
(and of course deeper nesting) is not really user-friendly. Thatβs because neither the embedded Haskell expressions nor declarations can contain Quasi Quotes (as haskell-src-meta
does not support that at the moment β there is a pending ticket for that). But I think that it should be possible to work around that by transforming QuasiQuote
into SpliceExp
β I will definitely try it out.
Epilogueπ
I hope you liked the article and HSML
. There are still few things that remain to be done, like better test coverage, measure the impact on run-time performance, consider using attoparsec
instead of parsec
, etc.
If you have any comments or ideas for improvement, I will gladly hear them out.