ScalaTags is a small, fast XML/HTML construction library for Scala that takes fragments in plain Scala code that look like this:
html(
head(
script(src:="..."),
script(
"alert('Hello World')"
)
),
body(
div(
h1(id:="title", "This is a title"),
p("This is a big paragraph of text")
)
)
)
And turns them into HTML like this:
<html>
<head>
<script src="..."></script>
<script>alert('Hello World')</script>
</head>
<body>
<div>
<h1 id="title">This is a title</h1>
<p>This is a big paragraph of text</p>
</div>
</body>
</html>
- Mailing List
- Getting Started
- ScalaJS
- Why Scalatags
- Basic Examples
- DOM Backend
- Performance
- Internals
- Prior Work
- License
- Scaladoc
ScalaTags is hosted on Maven Central; to get started, simply add the following to your build.sbt
:
libraryDependencies += "com.scalatags" %% "scalatags" % "0.4.2"
And you're good to go! Open up a sbt console
and you can start working through the Examples, which should just work when copied and pasted into the console.
To use Scalatags with a ScalaJS project, add the following to the built.sbt
of your ScalaJS project:
libraryDependencies += "com.scalatags" %%% "scalatags" % "0.4.2"
And you should be good to go generating HTML fragments in the browser! Scalatags has no dependencies, and so all the examples should work right off the bat whether run in Chrome, Firefox or Rhino. Scalatags 0.3.9 is currently only compatibly with ScalaJS 0.5.3+.
The core functionality of Scalatags is a tiny amount of code, and yet it provides all the functionality of large frameworks like Python's Jinja2 or C#'s Razor, and out-performs the competition by a large margin. It does this by leveraging the functionality of the Scala language to do almost everything, meaning you don't need to learn a second template pseudo-language just to stitch your HTML fragments together
Since ScalaTags is pure Scala, any editor that understands Scala will understand scalatags.Text. Not only do you get syntax highlighting, you also get code completion:
and Error Highlighting:
and in-editor documentation:
And all the other good things (jump to definition, extract method, etc.) you're used to in a statically typed language. No more digging around in templates which mess up the highlighting of your HTML editor, or waiting months for the correct plugin to materialize.
Take a look at the prior work section for a more detailed analysis of Scalatags in comparison to other popular libraries.
This is a bunch of simple examples to get you started using Scalatags. These examples are all in the unit tests. For more examples, take a look at the demo page, backed by the code in example/
import scalatags.Text.all._
val frag = html(
head(
script("some script")
),
body(
h1("This is my title"),
div(
p("This is my first paragraph"),
p("This is my second paragraph")
)
)
)
The core of Scalatags is a way to generate (X)HTML fragments using plain Scala. This is done by values such as head
, script
, and so on, which are automatically imported into your program when you import scalatags.Text.all._
. See below if you want more fine-grained control over what's imported.
This example code creates a Scalatags fragment. We could do many things with a fragment: store it, return it, etc. since it's just a normal Scala value. Eventually, though, you will want to convert it into HTML. To do this, simply use:
frag.toString
Which will give you a String containing the HTML representation:
<html><head><script>some script</script></head><body><h1>This is my title</h1><div><p>This is my first paragraph</p><p>This is my second paragraph</p></div></body></html>
This representation omits unnecesary whitespace. To improve readability we could pretty-print it using some XML processing:
val prettier = new scala.xml.PrettyPrinter(80, 4)
println(prettier.format(scala.xml.XML.loadString(frag.toString)))
Executing the above prints out:
<html>
<head>
<script>some script</script>
</head>
<body>
<h1>This is my title</h1>
<div>
<p>This is my first paragraph</p>
<p>This is my second paragraph</p>
</div>
</body>
</html>
If you want to add a doctype to your HTML fragment, simply add it as a string:
"<!DOCTYPE html>" + html(
...
)
The following examples will simply show the initial Scalatag fragment and the final pretty-printed HTML, skipping the intermediate steps.
html(
head(
script("some script")
),
body(
h1("This is my title"),
div(
p(onclick:="... do some js")(
"This is my first paragraph"
),
a(href:="www.google.com")(
p("Goooogle")
)
)
)
)
In Scalatags, each attribute has an associated value which can be used to set it. This example shows how you set the onclick
and href
attributes with the :=
operator. Attributes are all instances of the Attr class.
The common HTML attributes all have static values to use in your fragments, and the list can be seen here. This keeps things concise and statically checked. However, inevitably you'll want to set some attribute which isn't in the initial list defined by scalatags.Text. This can be done with the .attr
method that Scalatags adds to Strings:
html(
head(
script("some script")
),
body(
h1("This is my title"),
div(
p("onclick".attr:="... do some js")(
"This is my first paragraph"
),
a("href".attr:="www.google.com")(
p("Goooogle")
)
)
)
)
If you wish to, you can also take the result of the .attr
call and assign it to a variable for you to use later in an identical way.
Both the above examples print the same thing:
<html>
<head>
<script>some script</script>
</head>
<body>
<h1>This is my title</h1>
<div>
<p onclick="... do some js">
This is my first paragraph
</p>
<a href="www.google.com">
<p>Goooogle</p>
</a>
</div>
</body>
</html>
html(
head(
script("some script")
),
body(
h1(backgroundColor:="blue", color:="red")("This is my title"),
div(backgroundColor:="blue", color:="red")(
p(cls := "contentpara first")(
"This is my first paragraph"
),
a(opacity:=0.9)(
p(cls := "contentpara")("Goooogle")
)
)
)
)
In HTML, style
attributes are often thought of not as normal attributes (which contain strings), but as lists of key-value pairs. Furthermore, there is a large but finite number of styles, and a style cannot simply be any arbitrary string. The above example shows how CSS classes and inline-styles are typically set.
Note that in this case, backgroundColor
, color
and opacity
are all statically typed identifiers. These are all instances of Style) are defined by Scalatags.
Of course, style
is in the end just an attribute, so you can treat itas such and bind them directly without any fuss:
html(
head(
script("some script")
),
body(
h1(style:="background-color: blue; color: red;")("This is my title"),
div(style:="background-color: blue; color: red;")(
p(`class`:="contentpara first")(
"This is my first paragraph"
),
a(style:="opacity: 0.9;")(
p(cls := "contentpara")("Goooogle")
)
)
)
)
Both the above examples print the same thing:
<html>
<head>
<script>some script</script>
</head>
<body>
<h1 style="background-color: blue; color: red;">This is my title</h1>
<div style="background-color: blue; color: red;">
<p class="contentpara first">This is my first paragraph</p>
<a style="opacity: 0.9;">
<p class="contentpara">Goooogle</p>
</a>
</div>
</body>
</html>
A list of the attributes and styles provided by ScalaTags can be found in Attrs and Styles. This of course won't include any which you define yourself.
div(
p(float.left)(
"This is my first paragraph"
),
a(tabindex:=10)(
p("Goooogle")
),
input(disabled:=true)
)
Not all attributes and styles take strings; some, like float
, have an enumeration of valid values, and can be referenced by float.left
, float.right
, etc.. Others, like tabindex
or disabled
, take Ints and Booleans respectively. These are used directly as shown in the example above.
Even for styles or attributes which take values other than strings (e.g. tabindex
), the :=
operator can still be used to force assignment:
div(
p(float:="left")(
"This is my first paragraph"
),
a(tabindex:="10")(
p("Goooogle")
),
input(disabled:="true")
)
Both of the above examples print the same thing:
<div>
<p style="float: left;">This is my first paragraph</p>
<a tabindex="10">
<p>Goooogle</p>
</a>
<input disabled="true" />
</div>
Passing in numbers to the styles which require a px
at the end results in the px
being appended automatically:
div(width:=100, zIndex:=100, padding:=100)
<div style="width: 100px; z-index: 100; padding: 100px;"></div>
Note how this doesn't affect other styles (e.g. z-index
) which don't need this suffix. Generally, just pass in whatever you want and it'll do the right thing.
import scalatags.Text.{attrs => a, styles => s, _}
import scalatags.Text.tags._
div(
p(s.color:="red")("Red Text"),
img(a.href:="www.imgur.com/picture.jpg")
)
Apart from using import scalatags.Text.all._
, it is possible to perform the imports manually, renaming whatever you feel like renaming. The example above provides an example which imports all HTML tags, but imports Attrs and Styles aliased rather than dumping their contents into your global namespace. This helps avoid polluting your namespace with lots of common names (e.g. value
, id
, etc.) that you may not use.
The main objects which you can import things from are:
- tags: common HTML tags
- tags2: less common HTML tags
- attrs: common HTML attributes
- styles: common CSS styles
- styles2: less common CSS styles
- svgTags: SVG tags
- svgAttrs: attributes only associated with SVG elements
- DataConverters: convenient extensions (e.g.
10.px
) to create the CSS datatypes
You can pick and choose exactly which bits you want to import, or you can use one of the provided aggregates:
- all: this imports the contents of Tags, Attrs, Styles and DataConverters
- short: this imports the contents of Tags and DataConverters, but aliases Attrs and Styles as
*
Thus, you can choose exactly what you want to import, and how:
import scalatags.Text.{attrs => attr, styles => css, _}
import scalatags.Text.tags._
div(
p(css.color:="red")("Red Text"),
img(attr.href:="www.imgur.com/picture.jpg")
)
Or you can rely on a aggregator like all (which the rest of the examples use) or short. short imports Attrs and Styles as *
, making them quick to access without cluttering the global namespace:
import scalatags.Text.short._
div(
p(*.color:="red")("Red Text"),
img(*.href:="www.imgur.com/picture.jpg")
)
Both these examples print:
<div>
<p style="color: red;">Red Text</p>
<img href="www.imgur.com/picture.jpg" />
</div>
This custom import object also provides you a place to define your own custom tags that will be imported throughout your project e.g. a js(s: String)
tag as shorthand for script(src:=s)
. You can even override the default definitions, e.g. making script
be a shorthand for script(type:="javascript")
so that you can never forget to use your own custom version.
The lesser used tags or styles are generally not imported wholesale by default, but you can always import the ones you need:
import Styles2.pageBreakBefore
import Tags2.address
import scalatags.JsDom.svgTags._
import scalatags.JsDom.svgAttrs._
div(
p(pageBreakBefore.always, "a long paragraph which should not be broken"),
address("500 Memorial Drive, Cambridge MA"),
svg(stroke:="blue")
)
This prints:
<div>
<p style="page-break-before: always;">
a long paragraph which should not be broken
</p>
<address>500 Memorial Drive, Cambridge MA</address>
<svg stroke="blue" />
</div>
object CustomBundle extends Text.Cap with text.Tags with text.Tags2 with Text.Aggregate{
object st extends Text.Cap with Text.Styles with Text.Styles2
object at extends Text.Cap with Text.Attrs
}
import CustomBundle._
html(
head(
script("some script")
),
body(
h1(st.backgroundColor:="blue", st.color:="red")("This is my title"),
div(st.backgroundColor:="blue", st.color:="red")(
p(at.cls := "contentpara first")(
"This is my first paragraph"
),
a(st.opacity:=0.9)(
p(at.cls := "contentpara")("Goooogle")
)
)
)
)
In addition to importing things piecemeal from various pre-defined namespaces, Scalatags allows you to build a custom bundle which can be used to provide a single-import syntax for whatever import convention you're using. For example, the above snippet sets up a custom bundle which dumps Tags
and Tags2
in the local namespace, assigns Styles
and Styles2
to the st
object, Attrs
to the at
object, and ignores Svg-related styles and tags entirely. This lets you enforce a particular convention without having to duplicate the same import-renamings in multiple files in your application.
The above snippet prints the following:
<html>
<head>
<script>some script</script>
</head>
<body>
<h1 style="background-color: blue; color: red;">This is my title</h1>
<div style="background-color: blue; color: red;">
<p class="contentpara first">This is my first paragraph</p>
<a style="opacity: 0.9;">
<p class="contentpara">Goooogle</p>
</a>
</div>
</body>
</html>
Despite its usefulness in enforcing a particular import convention, custom bundles are completely interoperable with each other or with the default all
and short
bundles, and the above snippet could equally be written as:
import CustomBundle.{st, at}
import Text.all._
CustomBundle.html(
head(
script("some script")
),
CustomBundle.body(
h1(backgroundColor:="blue", st.color:="red")("This is my title"),
div(st.backgroundColor:="blue", color:="red")(
p(cls := "contentpara first")(
"This is my first paragraph"
),
CustomBundle.a(st.opacity:=0.9)(
p(at.cls := "contentpara")("Goooogle")
)
)
)
)
Mixing both things from the all
bundle as well as st
and at
from our own CustomBundle
. That's not to say you should do this, but if for some reason if e.g. you're using different conventions for different source files, you can be sure that they'll work together just fine.
val title = "title"
val numVisitors = 1023
html(
head(
script("some script")
),
body(
h1("This is my ", title),
div(
p("This is my first paragraph"),
p("you are the ", numVisitors.toString, "th visitor!")
)
)
)
Variables can be inserted into the templates as Strings, simply by adding them to an element's children. This prints:
<html>
<head>
<script>some script</script>
</head>
<body>
<h1>This is my title</h1>
<div>
<p>This is my first paragraph</p>
<p>you are the 1023th visitor!</p>
</div>
</body>
</html>
val numVisitors = 1023
val posts = Seq(
("alice", "i like pie"),
("bob", "pie is evil i hate you"),
("charlie", "i like pie and pie is evil, i hat myself")
)
html(
head(
script("some script")
),
body(
h1("This is my title"),
div("posts"),
for ((name, text) <- posts) yield div(
h2("Post by ", name),
p(text)
),
if(numVisitors > 100) p("No more posts!")
else p("Please post below...")
)
)
Like most other XML templating languages, ScalaTags contains control flow statements like if
and for
. Unlike other templating languages which have their own crufty little programming language embedded inside them for control flow, you probably already know how to use ScalaTags' control flow syntax. It's just Scala after all.
This prints out:
<html>
<head>
<script>some script</script>
</head>
<body>
<h1>This is my title</h1>
<div>posts</div>
<div>
<h2>Post by alice</h2>
<p>i like pie</p>
</div>
<div>
<h2>Post by bob</h2>
<p>pie is evil i hate you</p>
</div>
<div>
<h2>Post by charlie</h2>
<p>i like pie and pie is evil, i hat myself</p>
</div>
<p>No more posts!</p>
</body>
</html>
def imgBox(source: String, text: String) = div(
img(src:=source),
div(
p(text)
)
)
html(
head(
script("some script")
),
body(
h1("This is my title"),
imgBox("www.mysite.com/imageOne.png", "This is the first image displayed on the site"),
div(`class`:="content")(
p("blah blah blah i am text"),
imgBox("www.mysite.com/imageTwo.png", "This image is very interesting")
)
)
)
Many other templating systems define incredibly roundabout ways of creating re-usable parts of the template. In ScalaTags, we don't need to re-invent the wheel, because Scala has these amazing things called functions.
The above example prints:
<html>
<head>
<script>some script</script>
</head>
<body>
<h1>This is my title</h1>
<div>
<img src="www.mysite.com/imageOne.png" />
<div>
<p>This is the first image displayed on the site</p>
</div>
</div>
<div class="content">
<p>blah blah blah i am text</p>
<div>
<img src="www.mysite.com/imageTwo.png" />
<div>
<p>This image is very interesting</p>
</div>
</div>
</div>
</body>
</html>
val evilInput1 = "\"><script>alert('hello!')</script>"
val evilInput2 = "<script>alert('hello!')</script>"
html(
head(
script("some script")
),
body(
h1(
title:=evilInput1,
"This is my title"
),
evilInput2
)
)
By default, any text that's put into the Scalatags templates, whether as a attribute value or a text node, is properly escaped when it is rendered. Thus, when you run the following snippet, you get this:
<html>
<head>
<script>some script</script>
</head>
<body>
<h1 title=""><script>alert('hello!')</script>">
This is my title
</h1>
<script>alert('hello!')</script>
</body>
</html>
As you can see, the contents of the variables evilInput1
and evilInput2
have been HTML-escaped, so you do not have to worry about un-escaped user input messing up your DOM or causing XSS injections. Furthermore, the names of the tags (e.g. "html") and attributes (e.g. "href") are themselves validated: passing in an invalid name to either of those (e.g. a tag or attribute name with a space inside) will throw an IllegalArgumentException
).
If you really want, for whatever reason, to put unsanitized input into your HTML, simply surround the string with a raw
tag:
val evilInput = "<script>alert('hello!')</script>"
html(
head(
script("some script")
),
body(
h1("This is my title"),
raw(evilInput)
)
)
prints
<html>
<head>
<script>some script</script>
</head>
<body>
<h1>This is my title</h1>
<script>alert('hello!')</script>
</body>
</html>
As you can see, the <script>
tags in evilInput
have been passed through to the resultant HTML string unchanged. Although this makes it easy to open up XSS holes (as shown above!), if you know what you're doing, go ahead.
There isn't any way to put unescaped text inside tag names, attribute names, or attribute values.
def page(scripts: Seq[Frag], content: Seq[Frag]) =
html(
head(scripts),
body(
h1("This is my title"),
div(cls:="content")(content)
)
)
page(
Seq(
script("some script")
),
Seq(
p("This is the first ", b("image"), " displayed on the ", a("site")),
img(src:="www.myImage.com/image.jpg"),
p("blah blah blah i am text")
)
)
Again, this is something that many other templating languages have their own special implementations of. In ScalaTags, this can be done simply by just using functions! The above snippet gives you:
<html>
<head>
<script>some script</script>
</head>
<body>
<h1>This is my title</h1>
<div class="content">
<p>This is the first <b>image</b> displayed on the <a>site</a></p>
<img src="www.myImage.com/image.jpg" />
<p>blah blah blah i am text</p>
</div>
</body>
</html>
class Parent{
def render = html(
headFrag,
bodyFrag
)
def headFrag = head(
script("some script")
)
def bodyFrag = body(
h1("This is my title"),
div(
p("This is my first paragraph"),
p("This is my second paragraph")
)
)
}
object Child extends Parent{
override def headFrag = head(
script("some other script")
)
}
Child.render
Most of the time, functions are sufficient to keep things DRY, but if for some reason you want to use inheritance to structure your code, you probably already know how to do so. Again, unlike other frameworks that have implemented complex inheritance systems themselves, Scalatags is just Scala, and it behaves as you'd expect.
<html>
<head>
<script>some other script</script>
</head>
<body>
<h1>This is my title</h1>
<div>
<p>This is my first paragraph</p>
<p>This is my second paragraph</p>
</div>
</body>
</html>
Data attributes work by separating the sections of the attribute with .
instead of -
:
div(
id:="electriccars",
data.columns:="3",
data.index.number:="12314",
data.parent:="cars",
"..."
)
<div
id="electriccars"
data-columns="3"
data-index-number="12314"
data-parent="cars">
...
</div>
Aria attributes work too:
div(
div(id:="ch1Panel", role:="tabpanel", aria.labelledby:="ch1Tab")(
"Chapter 1 content goes here"
),
div(id:="ch2Panel", role:="tabpanel", aria.labelledby:="ch2Tab")(
"Chapter 2 content goes here"
),
div(id:="quizPanel", role:="tabpanel", aria.labelledby:="quizTab")(
"Quiz content goes here"
)
)
<div>
<div id="ch1Panel" role="tabpanel" aria-labelledby="ch1Tab">
Chapter 1 content goes here
</div>
<div id="ch2Panel" role="tabpanel" aria-labelledby="ch2Tab">
Chapter 2 content goes here
</div>
<div id="quizPanel" role="tabpanel" aria-labelledby="quizTab">
Quiz content goes here
</div>
</div>
Although Scalatags was originally a HTML-String generation library, it now ships with an additional backend that runs only on ScalaJS, available by replacing
import scalatags.Text._
with
import scalatags.JsDom._
This gives you a version of Scalatags that renders directly to dom.Element
s, rather than spitting out strings. With the DOM backend, toString
still works and generates a HTML string, but is implemented by constructing a tree of dom.Element
s and calling .outerHTML
on them, and the resultant string may have subtle differences (e.g. order of attributes, or whitespace between styles) from the Text backend.
The DOM backend provides an additional method .render
on all Scalatags fragments, which converts the fragment into a DOM tree:
val elem = div.render
println(elem.children.length) // 0
elem.appendChild(p("omg", "wtf", "bbq").render)
println(elem.children.length) // 1
val pElem = elem.children(0).asInstanceOf[dom.HTMLParagraphElement]
println(pElem.childNodes.length) // 3
println(pElem.textContent) // "omgwtfbbq"
As you can see, you can manipulate DOM elements directly, calling standard DOM APIs like .children
, .appendChild
, etc.
In addition to the default ability to splice String
s, Boolean
s and Int
s into the Scalatags fragment, the DOM backend allows you to bind arbitrary js.Any
-convertible objects, e.g. the function literal shown below:
var count = 0
val elem = div(
onclick := {() => count += 1},
tabindex := 1
).render
assert(count == 0)
elem.onclick(null)
assert(count == 1)
As you can see, the function literal is kept intact rather than being serialized (as you would expect in the Text backend). When called via the attribute of the element, you can see that it functions intact and properly increments the counter.
Having direct access to the DOM allows a great deal of flexibility that you do not have when working with strings. For example, the example below defines an input-label pair which clears the label when you focus on the input, a common pattern:
val labelElem = label("Default").render
val inputElem = input(
`type`:="text",
onfocus := { () => labelElem.textContent = ""}
).render
val box = div(
inputElem,
labelElem
).render
println(labelElem.textContent) // "Default"
inputElem.onfocus(null)
println(labelElem.textContent) // ""
This allows a very convenient direct-binding of behaviors without having to trawl through the DOM with jquery selectors or creating lots of IDs and worrying about collisions.
The DOM backend currently requires you to separately add a dependency on scala-js-dom for it to function.
Template Engine | Renders |
---|---|
Scalatags | 7436041 |
scala-xml | 3794707 |
Twirl | 1902274 |
Scalate-Mustache | 500975 |
Scalate-Jade | 396224 |
These numbers are the number of times each template engine is able to render (to a String) a simple, dynamic HTML fragment in 60 seconds.
The fragment (shown below) is designed to exercise a bunch of different functionality in each template engine: functions/partials, loops, value-interpolation, etc.. The templates were structured identically despite the different languages used by the various engines. All templates were loaded and rendered once before the benchmarking begun, to allow for any file-operations/pre-compilation to happen.
The numbers speak for themselves; Scalatags is almost twice as fast as splicing/serializing scala-xml
literals, almost four times as fast as Twirl, and 10-15 times as fast as the various Scalate alternatives. This is likely due to overhead from the somewhat bloated data structures used by scala-xml
(which Twirl also uses) and the heavy-use of dictionaries used to implement the custom scoping in the Scalate templates. Although this is a microbenchmark, and probably does not perfectly match real-world usage patterns, the conclusion is pretty clear: Scalatags is fast!
This is the Scalatags fragment that was rendered:
def para(n: Int) = p(
cls:="contentpara",
title:=s"this is paragraph $n"
)
val titleString = "This is my title"
val firstParaString = "This is my first paragraph"
html(
head(
script("console.log(1)")
),
body(
h1(color:="red")(titleString),
div(backgroundColor:="blue")(
para(0)(
cls := "contentpara first",
firstParaString
),
a(href:="www.google.com")(
p("Goooogle")
),
for(i <- 0 until 5) yield para(i)(
s"Paragraph $i",
color:=(if (i % 2 == 0) "red" else "green")
)
)
)
).toString()
The rest of the code involved in this micro-benchmark can be found in PerfTests.scala
The primary data structure Scalatags uses are the TypedTag
:
trait TypedTag[Builder, +Output, FragT] extends Frag[Builder, FragT]{
def tag: String
/**
* The modifiers that are applied to a TypedTag are kept in this linked-Seq
* (which are actually WrappedArrays) data-structure in order for maximum
* performance.
*/
def modifiers: List[Seq[Modifier[Builder]]]
/**
* Add the given modifications (e.g. additional children, or new attributes)
* to the [[TypedTag]].
*/
def apply(xs: Modifier[Builder]*): Self
/**
* Collapses this scalatags tag tree and returns an [[Output]]
*/
def render: Output
}
The Modifier
:
trait Modifier[Builder] {
/**
* Applies this modifier to the specified [[Builder]], such that when
* rendering is complete the effect of adding this modifier can be seen.
*/
def applyTo(t: Builder): Unit
}
And the Frag
:
trait Frag[Builder, FragT] extends Modifier[Builder]{
def render: FragT
}
A TypedTag
is basically a tag-name together with a loose bag of Modifier
s, and is itself a Modifier
so it can be nested within other TypedTag
s. A Modifier
is a tag, a sequence of tags, an attribute binding, a style binding, or anything else that can be used to modify how a tag will be rendered. Lastly, a Frag
represents the smallest standalone atom, which includes tags, loose strings, numbers, and other things.
In the text backend you have the alias
type Tag = TypedTag[String]
Since everything renders to a string. In the DOM backend you have:
type HtmlTag = JsDom.TypedTag[dom.HTMLElement]
type SvgTag = JsDom.TypedTag[dom.SVGElement]
type Tag = JsDom.TypedTag[dom.Element]
These aliases help you keep your code short by letting you refer to the most common versions of TypedTag
via concise names.
Each Scalatags backend has its own refinements, e.g. Text.TypedTag
, Text.Frag
and Text.Modifier
have the Builder
type-parameter fixed as text.Builder
, and the Output
type-parameter fixed as String
. Their JsDom.*
counterparts have Builder
fixed as dom.Element
, and Output
fixed to various subclasses of dom.Element
. The various other classes/traits (e.g. Attr
, AttrPair
, StylePair`, etc.) are similarly abstract with concrete versions in each backend.
The current selection of Modifier
(or implicitly convertable) types include
TypedTag
s andString
s: appends itself to the parent'schildren
list.AttrPair
s: sets a key in the parent'sattrs
list.StylePair
s: appends the inlinestyle: value;
to the parent'sstyle
attribute.StringFrag
s: append the string as a child.RawFrag
s: append the string to the parent as unescaped HTML.
The bulk of Scalatag's ~5000 lines of code is static bindings (and inline documentation!) for the myriad of CSS rules and HTML tags and attributes that exist. The core of Scalatags lives in Core.scala, with most of the implicit extensions and conversions living in package.scala.
Scalatags has pretty odd internals in order to support code re-use between the Text and Dom packages. Essentially, each Scalatags package is an instance of
trait Bundle[Builder, Output <: FragT, FragT]{
Which is parametrized on the Builder
used to generate the output, the type generated by rendering a Frag
, as well as the final Output
type generated by rendering a TypedTag
. The Text package is defined as
object Text extends Bundle[text.Builder, String, String] {
Since it uses a custom text.Builder
object for maximum performance and spits out a String
, while the Dom package is defined as as
object JsDom extends generic.Bundle[dom.Element, dom.Element, dom.Node]
Since it uses dom.Element
s both as the intermediate builder as well as the final result.
This layout allows Scalatags to specify formally which types are common between the two backends (i.e. those in generic.Bundle
) and which can vary. For example, both backends have a concept of TypedTag
s, Frag
s and Modifier
s. On the other hand, The Text backend has TypedTag[String]
aliased as Tag
since it will always be String
, while the Dom backend has it left as
TypedTag[+Output <: dom.Element]
With helper types bound as:
type HtmlTag = TypedTag[dom.HTMLElement]
type SvgTag = TypedTag[dom.SVGElement]
type Tag = TypedTag[dom.Element]
Since it is likely that working with the Dom backend you will want to distinguish these.
A mix of typeclasses and implicit conversions is used to control what you can put into a Scalatags fragment. For example, the typeclasses
trait AttrValue[Builder, T]{
def apply(t: Builder, a: Attr, v: T)
}
trait StyleValue[Builder, T]{
def apply(t: Builder, s: Style, v: T)
}
Allow you to specify what (and how) types can be used as attributes and styles respectively, while implicit conversions to Modifier
or Frag
are used to allow you to use arbitrary types as children. The use of implicit conversions in this case is to allow it to work with variable length argument lists (i.e. (mods: *Modifier)
), which is difficult to do with typeclasses.
Due to this design, and the parametrization of the bundles described earlier, it is possible to define behavior for a particular type only where it makes sense. e.g. there is a pair of typeclass instances
implicit object bindJsAny extends generic.AttrValue[dom.Element, js.Any]
implicit def bindJsAnyLike[T <% js.Any] = new generic.AttrValue[dom.Element, T]
Which allows you to bind anything convertible to a js.Any
into the JsDom fragments, since they can just be assigned directly to the attributes of the dom.Element
objects. Doing the same thing for Text fragments doesn't make sense, and would correctly fail to compile.
You can easier add other typeclass instances to handle binding e.g. Future
s (which will add a child or set an attr/style on completion), or reactive variables (which would constantly update the child/attr/style every time it changes).
If you wish to, it is possible to write code that is generic against the Scalatags backend used, and can be compiled and run on both Text and JsDom backends at the same time! This is done by adding an explicit dependency on generic.Bundle[Builder, Output, FragT]
, e.g. how it is done in the unit tests:
class ExampleTests[Builder, Output, FragT](bundle: Bundle[Builder, Output, FragT]) extends TestSuite{
import bundle._
...
}
Inside this scope, you are limited to only using the common functionality defined in generic.Bundle
, and can't use any Text or JsDom specific APIs. However, in exchange you can write code that works in either backend, by instantiating it with the respective bundle:
object ExampleTests extends generic.ExampleTests(scalatags.Text)
object ExampleTests extends generic.ExampleTests(scalatags.JsDom)
This is currently used to shared the bulk of unit tests between the Text and JsDom backends, and could be useful in other scenarios where you may want to swap between them (e.g. using Text on the server, and JsDom on the client where it's available) while sharing as much code as possible.
For a concrete, self-contained example of this, look at the demo page.
Scalatags was made after experience with a broad range of HTML generation systems. This experience (with both the pros and cons of existing systems) informed the design of scalatags.Text.
Jinja2 is the templating engine that comes bundled with Flask, a similar (but somewhat weaker) system comes bundled with Django, and another system in a similar vein is Ruby on Rail's ERB rendering engine. This spread more-or-less represents the old-school way of rendering HTML, in that they:
- Are effectively string-based
- Use special syntax for both interpolating variables as well as for basic control flow logic
- Have a ruby/python-like (but not quite!) language for logic within the template
- Are generally with one template per file.
They also showcase many of the weaknesses of this style of templating system:
- The fact that it's string based means it's vulnerable to XSS injections, or plain-old malformed HTML output.
- The one-template-per-file rule discourages you from building your page from small re-usable fragments, because who wants to keep track of hundreds of individual files. People are reluctant to make a file with 3-5 lines in it, which is understandable but unfortunate because factoring templates into re-usable 3-5 line snippets is a good way of staying sane.
- The API is complex and novel: Jinja2 for example contains logic around file-loading & caching, as well as custom Jinja2-specific ways of doing loops, conditionals, functions, comments, inheritance, scoping, imports and other things. All these are things you would have to learn.
- The syntax is completely new; finding an editor that properly supports all the quirks and semantics (or even simply highlighting things properly) is hard. You could hack together something quick and extremely fragile, or you could wait ages for a solid plugin to materialize.
- Abstraction is clunky: inbuilt tags are used via
<div />
, while user-defined components are called e.g. by{{ component() }}
. You're left with a choice between not using much abstraction and mainly sticking to inbuilt tags, or creating components and accepting the fact that your templates will basically be totally composed of{{ curly braces }}
. Neither choice is satisfying. - Have no sort of static checking at all; you're just passing dicts around, and waiting for silly mistakes to blow up at run-time so you can fix them.
Razor (the ASP.NET MVC template engine) and the Play framework's template engine go in a new direction. Their templates generally:
- Are statically-compiled
- Re-use large chunks of the host language
Both templating systems generally use @
to delimit "code"; e.g. @for(...){...}
declares a for-loop. Nice things are:
- The API is far simpler: all the custom control-flow/logic/syntax basically collapses into the simple statement "do it the way C#/Scala does it".
- Static checking in templates is nice.
However, they still have their downsides:
- Abstractions are less clunky to use than in old-school templates, e.g.
@component()
rather than{{ component() }}
, but still not ideal. - The one-template-per-file rule is still there, making abstractions clunky to define.
- They still require their own special build-system-integration, run-time compilation, caching, and other things, which a templating language really shouldn't need. Scalate, which has a runtime dependency on the entire Scala compiler, suffers from the same problem.
- The syntax still poses a problem for editors; both HTML editors and C#/Scala editors won't want to work with these templates, so you still end up with sub-par support or waiting for plugins.
- You still end up with weird edge cases due to the fact that you're squashing together two completely unrelated syntaxes.
XHP and Pyxl are HTML generation systems used at Facebook and Dropbox. In short, they allow you to:
- Embed sections of your HTML as literals within your PHP and Python code
- Reference them as objects (e.g. calling methods and modifying fields)
- Provide a way to interpolate values and combine them.
The Pyxl homepage provides this example:
image_name = "bolton.png"
image = <img src="/static/images/{image_name}" />
text = "Michael Bolton"
block = <div>{image}{text}</div>
element_list = [image, text]
block2 = <div>{element_list}</div>
Which shows how you can generate HTML directly in your python code, using your python variables. These libraries are basically the same thing, and have some nice properties:
- The one-template-per-file rule is gone! This encourages you to make more, smaller fragments and then compose them, which is great.
- They're no longer string-based, so you won't have problems with XSS or malformed output.
- The API is really simple; maybe a dozen different things you need to remember, and the rest is "just use Python/PHP". E.g. they don't need to load templates from the filesystem any more (with all the associated discovery/loading/caching logic) since it's all just in your code.
- The syntax is completely familiar too; apart from maybe one new rule (using
{...}
to interpolate values) the rest of your templates and logic are bog-standard HTML/Python/PHP. - Abstraction is almost seamless; both systems allow you to define custom components and have them called via
<component arg="..." />
But they're not quite there:
- Although the syntax is familiar to you, it's not familiar to your editor, and probably will mess up your syntax highlighting (and other tooling) in your Python/PHP files.
- Defining custom components (e.g. in Pyxl) is much more verbose/tedious than it needs to be. Most of the time, all you want is a function; very rarely do you want a fragment that is long lived and has mutable state, which is where classes/objects are really necessary.
- Even using custom components gets tedious; at some scale, everything you pass into a component will be a structured Python value rather than a string, and you end up with code like
<component arg1="{value1}" arg2="{value2}" arg3="{value3}" arg4="{value4}" />
. It's nice to have inbuilt/custom components behave uniformly, but you wonder what the XML syntax is really buying you other than forcing you to only use keyword-arguments and to wrap arguments in"{...}"
. - It's still XML! People spend a lot of time trying to get XML out of their templates; it seems odd to spend just as much time trying to put it into your source code.
And that's why I created Scalatags:
- Structured and immune to malformed output/XSS.
- No more one-template-per-file rule; make small templates to your heart's content!
- Dead-simple API, which re-uses 100% of what you know about Scala's language and libraries.
- 100% Scala syntax; no more bumping into weird edge cases with the parser.
- Syntax highlighting, error-highlighting, jump-to-definition, in-editor-documentation, and all the other nice IDE features out of the box. No more waiting for plugins!
- Inbuilt and custom tags are uniformly just function calls (e.g.
div("hello world")
) - Custom components are trivial to create (
def component(x: Int, y: Int) = ...
) and trivial to use (component(value1, value2)
) because they're just functions. - No dependencies! No need for SBT integration, filesystem loading, runtime compilation, caching, and all that jazz. You create a Scala object, call
.toString()
, and that's it!
On top of fixing all the old problems, Scalatags targets some new ones:
- Typesafe-ish access to HTML tags, attributes and CSS classes and styles. No more weird bugs due to typos like
flaot: left
or<dvi>
. - Cross compiles to run on both JVM and Javascript via ScalaJS, which is a property few other engines (e.g. Mustache) have.
- Blazing fast
Scalatags is still a work in progress, but I think I've hit most of the pain points I was feeling with the old systems, and hope to continually improve it over time. Pull requests are welcome!
- Added Javascript source maps that point to the Github sources.
- Added basic
Namespace
support, to allow for proper rendering of SVG elements in theJsDom
backend - Replaced unmaintained/untested
SvgStyles
with a semi-testedSvgAttrs
- Added
example/
folder
- Removed unused type param from
Frag
- Changed
bindNode
fromModifier[dom.Element]
toFrag[dom.Element, dom.Node]
- Better error messages for missing implicits
- Added attribuets
method
,role
,content
,httpEquiv
,onkeyup
,onkeypress
- Added implicit conversion from
Seq[Frag]
toFrag
- Added
Frag
alias inBundle
, to match all the other aliases per-bundle
- Added
implicits
object, for people who want to import all the implicit conversions/typeclasses without importing tags or other things
- Added unit test and readme-section to document making custom-bundles, to allow customizable single-import syntax to bring all the things you want into scope.
- Deleted dead code in
Platform
- Enforce lack of cycles between internal files
- Removed
StyleValue[T: Numeric]
andAttrValue[T: Numeric]
in favor ofByte
/Short
/Int
/etc. versions, to work around bug in type-inference in 2.10
- Fixed a bug in
Text
backend that blew up when you had more than 8 attributes
- Added a separate type parameter for
Frag.render
, distinct from the type ofTypedTag.render
, to more accurately type the API and allow the usage ofFrag
s on their own.
The MIT License (MIT)
Copyright (c) 2013, Li Haoyi
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.