Finally released on Maven Central!
libraryDependencies += "io.github.ciaraobrien" %% "dottytags" % "1.1.0"
An experimental reimplementation of ScalaTags in (extremely meta) Scala 3. It is a more-or-less working clone of
ScalaTags from the user's perspective, with most of the surface syntax being nearly identical, but the internals are radically different, as Scala 3's
metaprogramming capabilities are leveraged to automatically reduce the tree as much as possible to simple serial concatenation of strings at compile-time.
Therefore, the code that actually runs is, in many cases, basially an array of string literals interspersed with string expressions that are evaluated at runtime
and then appended with the literal spans in a single linear StringBuilder
loop. By the nature of the way this system works, it is not feasible to implement
post-hoc mutation of the tree, however ordering is manipulated in the initial tree: duplicate attributes combine, styles combine and override, etc. in
the same way as Scalatags. The primary differences are:
- No chained
Tag
applications likediv(cls := "header", backgroundColor := "blue")(divContents)
. This would require either compromising on the flattening performance or re-parsing the already-generated syntax tree at runtime in order to make changes, which is a highly unappealing idea. In my opinion this is of little use anyway, especially since the point of the library is to do as much as possible at compile-time. - Sequences of elements generated by for-loops and the like must be explicitly deconstructed into a
Frag
with thebind
macro (though there is an implicit conversion for this indottytags.syntax
). The way this has to be implemented is rather slow compared to the performance of the system in most other situations, though still faster than Scalatags even on loop-heavy code. Wrapping elements up with thefrag
macro incurs no such performance hit, and does not disrupt the system's ability to achieve the optimal splicing completeness, but it can only be used with true varargs, vararg ascription doesn't help. Usefrag
for when you want to group up some content in a lightweight fashion.
Based on my very unprofessional benchmark comparisons, DottyTags is between 2 and 6 times faster than ScalaTags when
the comparison is roughly fair (complex HTML trees involving loops, external variables, etc., like those used in ScalaTags'
own benchmarks), and significantly faster in less-fair comparisons involving mostly-static tree generation, in which DottyTags has an absurd advantage
since entirely-static trees get flattened into single string literals at compile-time, and trees with only a few dynamic elements pretty much boil down to
a small Array[String]
being appended to a StringBuilder
, where most of the elements of the array are string literals. This speed disparity carries over more or
less directly to Scala.JS, when comparing DottyTags to ScalaTags' text backend - Scalatags' JS DOM backend is far, far slower in my benchmarks for some reason.
As a quick example, this:
println(html(cls := "foo", href := "bar", css("baz1") := "qux", "quux",
System.currentTimeMillis.toString, css("baz2") := "qux", raw("a")
).toString)
Boils down to something like:
println(Tag.apply(
dottytags.spliceString(
Array[String](
"<html class=\"foo\" href=\"bar\" style=\"baz: qux;\">quux",
dottytags.escapeString(scala.Long.box(System.currentTimeMillis()).toString()),
"a</html>"
)
)
).toString)
Which, when run, yields:
<html class="foo" href="bar" style="baz1: qux; baz2: qux;">quux1608810396295a</html>
For comparison, ScalaTags' interpretation of the same code (by swapping out the imports, since the syntax is broadly compatible in most cases):
scalatags.Text.all.html().asInstanceOf[scalatags.Text.Text$TypedTag].apply(
scala.runtime.ScalaRunTime.wrapRefArray([
scalatags.Text.all.cls().:=("foo",
scalatags.Text.all.stringAttr()
),
scalatags.Text.all.href().:=("bar",
scalatags.Text.all.stringAttr()
),
scalatags.Text.all.css("baz1").:=("qux",
scalatags.Text.all.stringStyle()
),
scalatags.Text.all.stringFrag("quux"),
scalatags.Text.all.stringFrag(
scala.Long.box(System.currentTimeMillis()).toString()
),
scalatags.Text.all.css("baz2").:=("qux",
scalatags.Text.all.stringStyle()
),
scalatags.Text.all.raw("a") : scalatags.generic.Modifier
])
).render()
I recently completely overhauled the entire library and rewrote it from scratch (which I decided to make 1.0.0), including the
underlying metametaprogramming system used to make implementing the library less hellish, which I have traditionally called Phaser
despite the fact that "Stage" is the correct term for compile/macro-time vs runtime, while "Phase" refers properly to parts of the
compilation process. The internals of the library are far nicer than they used to be, and it can now do more, i.e. sort attributes
and styles according to the order in which they are present in the tag body. The metametaprogramming facilities are much better-developed
this time around, consisting of Phaser.scala
and Splice.scala
, both of which are amenable to use elsewhere, and I will probably
break them back out into a revived Phaser library for more general usage.