├── project
├── build.properties
└── plugins.sbt
├── .gitignore
├── src
├── main
│ └── scala
│ │ └── dottytags
│ │ ├── predefs
│ │ ├── All.scala
│ │ ├── Tags.scala
│ │ ├── Svg.scala
│ │ └── Attrs.scala
│ │ ├── Conversions.scala
│ │ ├── Utils.scala
│ │ ├── Boilerplate.scala
│ │ ├── Types.scala
│ │ ├── StyleUnits.scala
│ │ ├── Splice.scala
│ │ ├── Tags.scala
│ │ ├── Modifiers.scala
│ │ └── Phaser.scala
└── test
│ └── scala
│ ├── TestUtils.scala
│ ├── TextTests.scala
│ ├── CorrectnessTests.scala
│ ├── PerfTest.scala
│ └── ExampleTests.scala
├── tasty
└── Playground.scala
├── LICENSE
└── README.md
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.5.4
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .bsp/**
2 | .metals/**
3 | .vscode/**
4 | .dotty**
5 | .idea**
6 | **target**
7 | **/metals.sbt
8 | **.log
--------------------------------------------------------------------------------
/src/main/scala/dottytags/predefs/All.scala:
--------------------------------------------------------------------------------
1 | package dottytags.predefs
2 |
3 | object all {
4 | export attrs.*
5 | export styles.*
6 | export tags.*
7 | }
8 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.6.0")
2 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.1.0")
3 | libraryDependencies += "org.scala-js" %% "scalajs-env-jsdom-nodejs" % "1.1.0"
4 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2")
5 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.7")
--------------------------------------------------------------------------------
/tasty/Playground.scala:
--------------------------------------------------------------------------------
1 | import dottytags.*
2 | import dottytags.predefs.all.*
3 |
4 | object Playground {
5 | def main(args: Array[String]): Unit = {
6 | println(html(cls := "foo", href := "bar", css("baz1") := "qux", "quux",
7 | System.currentTimeMillis.toString, css("baz2") := "qux", raw("a")
8 | ).toString)
9 | }
10 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020 Ciara O'Brien (ciaraobrienf@gmail.com)
2 | Inspired by, and some code adapted from, Scalatags, Copyright (c) 2018 Li Haoyi (haoyi.sg@gmail.com).
3 | Files incorporating significant direct adaptations from Scalatags bear a notice to that effect.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/test/scala/TestUtils.scala:
--------------------------------------------------------------------------------
1 | import minitest.api.{SourceLocation, AssertionException}
2 | import minitest.api.Asserts.*
3 | import Console.*
4 |
5 | /*
6 | * Flatten was adapted from Scalatags.
7 | */
8 | def flatten(s: String): String = s.replaceAll("(\\n|\\s)+<", "<").nn.trim.nn
9 |
10 | extension (s: String) def takePadding(n: Int, sep: String = ""): String =
11 | val take = s.take(n)
12 | if take.length == n then take else take + sep + " " + (" " * (n - take.length))
13 |
14 | extension (e: AssertionException) def elideStackTrace: AssertionException = {
15 | e.setStackTrace(Array.empty[java.lang.StackTraceElement | Null])
16 | e
17 | }
18 |
19 | def assertXMLEquiv(r: Any, e: String)(using pos: SourceLocation): Unit =
20 | val rf = flatten(r.toString); val ef = flatten(e)
21 | val matchLen = rf.zip(ef).segmentLength(_ == _, 0)
22 | val barLen = 100
23 | val before = Math.min(matchLen, barLen / 2)
24 | val after = Math.min(Math.max(rf.length, ef.length), barLen)
25 | val response =
26 | s"""$WHITE$BOLD|Mismatch$RESET at
27 | |$WHITE$BOLD|$RESET${rf.drop(matchLen - before).take(before)}$WHITE$BOLD${rf.lift(matchLen).getOrElse(s"$RED_B ")}$RESET${rf.drop(matchLen + 1).takePadding(barLen - before)}$WHITE$BOLD|
28 | |$WHITE$BOLD|$RESET$GREEN${"|" * before}$RED_B$WHITE$BOLD|$RESET$RED$RED_B${rf.drop(matchLen + 1).takePadding(barLen - before, RESET)}$BLACK_B$WHITE$BOLD|
29 | |$WHITE$BOLD|$RESET${ef.drop(matchLen - before).take(before)}$WHITE$BOLD${ef.lift(matchLen).getOrElse(s"$GREEN_B ")}$RESET${ef.drop(matchLen + 1).takePadding(barLen - before)}$WHITE$BOLD|""".stripMargin
30 | if rf.length != ef.length || matchLen != rf.length then throw AssertionException(response, pos).elideStackTrace
--------------------------------------------------------------------------------
/src/test/scala/TextTests.scala:
--------------------------------------------------------------------------------
1 | import dottytags.*
2 | import dottytags.predefs.all.*
3 | import dottytags.syntax.given
4 | import dottytags.units.*
5 | import scala.language.implicitConversions
6 | import minitest.*
7 |
8 |
9 | /*
10 | * Most of these were adapted from Scalatags' test suite so as to correctly test for compatibility.
11 | * (see LICENSE for copyright notice)
12 | */
13 |
14 | object TextTests extends SimpleTestSuite {
15 |
16 | test("Hello World") {
17 | assertXMLEquiv(div("omg"), "
omg
")
18 | }
19 |
20 | /**
21 | * Tests the usage of the pre-defined tags, as well as creating
22 | * the tags on the fly from Strings
23 | */
24 | test("Tag creation") {
25 | assertXMLEquiv(a().toString, "")
26 | assertXMLEquiv(html().toString, "")
27 | assertXMLEquiv(tag("this_is_an_unusual_tag").toString, "")
28 | assertXMLEquiv(tag("this-is-a-string-with-dashes", selfClosing = true).toString, "")
29 | }
30 |
31 | test("CSS Helpers"){
32 | assert(10.px == "10px")
33 | assert(10.0.px == "10.0px" || 10.0.px == "10px")
34 | assert(10.em == "10em")
35 | assert(10.pt == "10pt")
36 | }
37 |
38 | test("Sequences of Element Types can be Frags") {
39 | val frag1: Frag = Seq(
40 | h1("titless"),
41 | div("lol")
42 | )
43 | val frag2: Frag = Seq("i", "am", "cow")
44 | val frag3: Frag = Seq(frag1, frag2)
45 | val wrapped = div(frag1, frag2, frag3).toString
46 | assert(wrapped == "
titless
lol
iamcow
titless
lol
iamcow
")
47 | }
48 |
49 | test("Namespacing") {
50 | val sample = tag("abc:def")(attr("hello:world") := "moo")
51 | assert(sample.toString == """""")
52 | }
53 |
54 | }
--------------------------------------------------------------------------------
/src/main/scala/dottytags/Conversions.scala:
--------------------------------------------------------------------------------
1 | package dottytags
2 |
3 | import scala.collection.generic.IsIterable
4 | import scala.collection.IterableOnce.iterableOnceExtensionMethods
5 | import scala.language.implicitConversions
6 | import scala.quoted.*
7 |
8 | object syntax {
9 |
10 | // As far as I know this works alright, but it doesn't actually handle the potential for the expression's value
11 | // to be directly manipulated (cf. Phaser), it just tacks wrapper logic onto the expression automatically,
12 | // which is probably fine but still grinds my gears. However implementing Phaser logic in here would be a nightmare.
13 | trait InlineConversion[-A, +B] extends Conversion[A, B] {
14 | def apply(a: A): B = throw new Error("""Tried to call a macro conversion
15 | at runtime! This Conversion value is invalid at runtime, it must be applied
16 | and inlined by the compiler only, you shouldn't summon it, sorry""")
17 | }
18 |
19 | given numericToString[A : Numeric]: InlineConversion[A, String] with
20 | override inline def apply(a: A): String = a.toString
21 | given booleanToString: InlineConversion[Boolean, String] with
22 | override inline def apply(b: Boolean): String = if b then "true" else "false"
23 | given unitToString: InlineConversion[Unit, String] with
24 | override inline def apply(u: Unit): String = ""
25 |
26 | /** This reflexive implicit conversion is necessary for those below it to work properly */
27 | given contentToContent: Conversion[Content, Content] = (e: Content) => e
28 |
29 | given arrayToFrag [A](using c: Conversion[A, Content]): Conversion[Array [A], Frag] = (arr: Array [A]) => bind(arr.toIndexedSeq.map(c))
30 | given optionToFrag[A](using c: Conversion[A, Content]): Conversion[Option[A], Frag] = (opt: Option[A]) => bind(opt.toList.map(c))
31 | given seqToFrag [A](using c: Conversion[A, Content]): Conversion[Seq [A], Frag] = (seq: Seq [A]) => bind(seq.map(c))
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/scala/dottytags/Utils.scala:
--------------------------------------------------------------------------------
1 | package dottytags
2 |
3 | /*
4 | * Most of this file is adapted from Scalatags (see LICENSE for copyright notice):
5 | * https://github.com/lihaoyi/scalatags/blob/master/scalatags/src/scalatags/Escaping.scala
6 | */
7 |
8 | import scala.quoted.*
9 |
10 | private[dottytags] def error(error: String)(using Quotes): Nothing = {
11 | import quotes.reflect.*
12 | report.error(error)
13 | throw scala.quoted.runtime.StopMacroExpansion()
14 | }
15 |
16 | private[dottytags] def error(error: String, loc: Expr[Any])(using Quotes): Nothing = {
17 | import quotes.reflect.*
18 | report.error(error, loc)
19 | throw scala.quoted.runtime.StopMacroExpansion()
20 | }
21 |
22 | private val tagNameRegex = "^[a-z][:\\w0-9-]*$".r
23 | private [dottytags] def validateTagName(nameExpr: Expr[String])(using Quotes): String = {
24 | val name = Phaser.require(nameExpr, "tag name")
25 | if tagNameRegex.matches(name) then name else error(s"Not a valid XML tag name: $name.", nameExpr)
26 | }
27 |
28 | private val attrNameRegex = "^[a-zA-Z_:][-a-zA-Z0-9_:.]*$".r
29 | private [dottytags] def validateAttrName(nameExpr: Expr[String])(using Quotes): String = {
30 | val name = Phaser.require(nameExpr, "attribute name")
31 | if attrNameRegex.matches(name) then name else error(s"Not a valid XML attribute name: $name.", nameExpr)
32 | }
33 |
34 | private val styleNameRegex = "^-?[_a-zA-Z]+[_a-zA-Z0-9-]*$".r
35 | private [dottytags] def validateStyleName(nameExpr: Expr[String])(using Quotes): String = {
36 | val name = Phaser.require(nameExpr, "style name")
37 | if attrNameRegex.matches(name) then name else error(s"Not a valid CSS style name: $name.", nameExpr)
38 | }
39 |
40 | /**
41 | * Escapes a string for XML, apparently quite quickly, though it should usually not matter
42 | * since this function should mostly be called at compile-time rather than at runtime.
43 | * Lifted pretty much directly from scalatags' implementation.
44 | */
45 | def escapeString(text: String): String = {
46 | val s = java.io.StringWriter(text.length())
47 | // Implemented per XML spec:
48 | // http://www.w3.org/International/questions/qa-controls
49 | // Highly imperative code, ~2-3x faster than the previous implementation (2020-06-11)
50 | val charsArray = text.toCharArray.nn
51 | val len = charsArray.size.nn
52 | var pos = 0
53 | var i = 0
54 | while i < len do {
55 | val c = charsArray(i)
56 | c match {
57 | case '<' =>
58 | s.write(charsArray, pos, i - pos)
59 | s.write("<")
60 | pos = i + 1
61 | case '>' =>
62 | s.write(charsArray, pos, i - pos)
63 | s.write(">")
64 | pos = i + 1
65 | case '&' =>
66 | s.write(charsArray, pos, i - pos)
67 | s.write("&")
68 | pos = i + 1
69 | case '"' =>
70 | s.write(charsArray, pos, i - pos)
71 | s.write(""")
72 | pos = i + 1
73 | case '\n' =>
74 | case '\r' =>
75 | case '\t' =>
76 | case c if c < ' ' =>
77 | s.write(charsArray, pos, i - pos)
78 | pos = i + 1
79 | case _ =>
80 | }
81 | i = i + 1
82 | }
83 | // Apparently this isn't technically necessary if (len - pos) == 0 as
84 | // it doesn't cause any exception to occur in the JVM.
85 | // The problem is that it isn't documented anywhere so I left this if here
86 | // to make the error clear.
87 | if pos < len then s.write(charsArray, pos, len - pos)
88 | s.toString
89 | }
--------------------------------------------------------------------------------
/src/main/scala/dottytags/Boilerplate.scala:
--------------------------------------------------------------------------------
1 | package dottytags
2 |
3 | import scala.quoted.*
4 |
5 | // So many FromExprs and ToExprs!
6 |
7 | private[dottytags] given AttrClassFromExpr: FromExpr[AttrClass] with
8 | def unapply(x: Expr[AttrClass])(using Quotes): Option[AttrClass] = x match
9 | case '{ AttrClass(${Expr(name: String)}, ${Expr(raw: Boolean)})} => Some(AttrClass(name, raw))
10 | case _ => None
11 | private[dottytags] given AttrClassToExpr: ToExpr[AttrClass] with
12 | def apply(x: AttrClass)(using Quotes): Expr[AttrClass] = '{ AttrClass(${Expr(x.name)}, ${Expr(x.raw)}) }
13 |
14 |
15 | private[dottytags] given AttrFromExpr: FromExpr[Attr] with
16 | def unapply(x: Expr[Attr])(using Quotes): Option[Attr] = x match
17 | case '{ Attr(${Expr(name: String)}, ${Expr(value: String)}) } => Some(Attr(name, value))
18 | case _ => None
19 | private[dottytags] given AttrToExpr: ToExpr[Attr] with
20 | def apply(x: Attr)(using Quotes): Expr[Attr] = '{ Attr(${Expr(x.name)}, ${Expr(x.value)}) }
21 |
22 |
23 | private[dottytags] given StyleClassFromExpr: FromExpr[StyleClass] with
24 | def unapply(x: Expr[StyleClass])(using Quotes): Option[StyleClass] = x match
25 | case '{ StyleClass(${Expr(name: String)}, ${Expr(defaultUnits: String)})} => Some(StyleClass(name, defaultUnits))
26 | case _ => None
27 | private[dottytags] given StyleClassToExpr: ToExpr[StyleClass] with
28 | def apply(x: StyleClass)(using Quotes): Expr[StyleClass] = '{ StyleClass(${Expr(x.name)}, ${Expr(x.defaultUnits)}) }
29 |
30 |
31 | private[dottytags] given StyleFromExpr: FromExpr[Style] with
32 | def unapply(x: Expr[Style])(using Quotes): Option[Style] = x match
33 | case '{ Style(${Expr(name: String)}, ${Expr(value: String)}) } => Some(Style(name, value))
34 | case _ => None
35 | private[dottytags] given StyleToExpr: ToExpr[Style] with
36 | def apply(x: Style)(using Quotes): Expr[Style] = '{ Style(${Expr(x.name)}, ${Expr(x.value)}) }
37 |
38 |
39 | private[dottytags] given TagClassFromExpr: FromExpr[TagClass] with
40 | def unapply(x: Expr[TagClass])(using Quotes): Option[TagClass] = x match
41 | case '{ TagClass(${Expr(name: String)}, ${Expr(sc: Boolean)}) } => Some(TagClass(name, sc))
42 | case '{ new TagClass(${Expr(name: String)}, ${Expr(sc: Boolean)}) } => Some(TagClass(name, sc))
43 | case _ => None
44 | private[dottytags] given TagClassToExpr: ToExpr[TagClass] with
45 | def apply(x: TagClass)(using Quotes): Expr[TagClass] = '{ TagClass(${Expr(x.name)}, ${Expr(x.selfClosing)}) }
46 |
47 |
48 | private[dottytags] given TagFromExpr: FromExpr[Tag] with
49 | def unapply(x: Expr[Tag])(using Quotes): Option[Tag] = x match
50 | case '{ Tag(${Expr(str: String)}) } => Some(Tag(str))
51 | case '{ new Tag(${Expr(str: String)}) } => Some(Tag(str))
52 | case _ => None
53 | private[dottytags] given TagToExpr: ToExpr[Tag] with
54 | def apply(x: Tag)(using Quotes): Expr[Tag] = '{ Tag(${Expr(x.str)}) }
55 |
56 |
57 | private[dottytags] given FragFromExpr: FromExpr[Frag] with
58 | def unapply(x: Expr[Frag])(using Quotes): Option[Frag] = x match
59 | case '{ Frag(${Expr(str: String)}) } => Some(Frag(str))
60 | case '{ new Frag(${Expr(str: String)}) } => Some(Frag(str))
61 | case _ => None
62 | private[dottytags] given FragToExpr: ToExpr[Frag] with
63 | def apply(x: Frag)(using Quotes): Expr[Frag] = '{ Frag(${Expr(x.str)}) }
64 |
65 |
66 | private[dottytags] given RawFromExpr: FromExpr[Raw] with
67 | def unapply(x: Expr[Raw])(using Quotes): Option[Raw] = x match
68 | case '{ Raw(${Expr(str: String)}) } => Some(Raw(str))
69 | case '{ new Raw(${Expr(str: String)}) } => Some(Raw(str))
70 | case _ => None
71 | private[dottytags] given RawToExpr: ToExpr[Raw] with
72 | def apply(x: Raw)(using Quotes): Expr[Raw] = '{ Raw(${Expr(x.str)}) }
73 |
--------------------------------------------------------------------------------
/src/main/scala/dottytags/Types.scala:
--------------------------------------------------------------------------------
1 | package dottytags
2 |
3 | import scala.quoted.*
4 |
5 | /**
6 | * Anything a [[Tag]] can contain on its own, so attributes, inline CSS styles, child tags, text fragments, etc.
7 | */
8 | type Entity = Modifier | Content
9 | /**
10 | * Something a [[Tag]] places within its leading tag brackets rather than between its leading and following tags,
11 | * so attributes and inline CSS styles.
12 | */
13 | type Modifier = Attr | Style
14 | /**
15 | * Something a [[Tag]] places between its leading and following tags to hold it as part of its child contents,
16 | * so [[Tag]]s, text ([[String]] or [[Raw]]), and [[Frag]]s (which are just an invisible wrapper around some set of [[Content]]s).
17 | */
18 | type Content = Tag | Frag | Raw | String
19 |
20 | /**
21 | * A wrapper that combines some number of [[Content]] entities into one, which should hopefully be disenspliceable
22 | * for any [[Tag]] or even other [[Frag]] macro invocation that might enclose it.
23 | */
24 | final class Frag private[dottytags] (val str: String):
25 | override def toString: String = str
26 | /** [[Frag]] companion, private to hide [[Frag.apply]] */
27 | private[dottytags] object Frag:
28 | def apply (s: String): Frag = new Frag(s)
29 |
30 | /**
31 | * A macro that binds a varargs list of [[Content]]s together into one [[Frag]],
32 | * which is also a [[Content]], and which should produce a disenspliceable splice result.
33 | * @param parts The [[Content]]s to wrap up in the [[Frag]]. [[String]]s will be ampersand-escaped, or scheduled
34 | * to be so at runtime.
35 | * @return A [[Frag.apply]] invocation whose payload [[String]] expression should be disenspliceable i.e. be an invocation of [[spliceString]].
36 | */
37 | inline def frag(inline parts: Content*): Frag = ${ fragMacro('parts) }
38 | private def fragMacro(partsExpr: Expr[Seq[Content]])(using Quotes): Expr[Frag] = partsExpr match
39 | case Varargs(exprs) => '{ Frag(${ Splice.phaseSplice(exprs.map {
40 | case '{ $str: String } => PhunctionEscapeStr(Phased(str).maybeEval)
41 | case '{ Tag($str) } => Phased(str).maybeEval
42 | case '{ Raw($str) } => Phased(str).maybeEval
43 | case '{ Frag($str) } => Phased(str).maybeEval
44 | case '{ $tag: Tag } => Phased('{$tag.str}).maybeEval
45 | case '{ $raw: Raw } => Phased('{$raw.str}).maybeEval
46 | case '{ $frag: Frag } => Phased('{$frag.str}).maybeEval
47 | case e => error(s"Unrecognised frag component: ${e.show}", e)
48 | }*).expr }) }
49 | case _ => error(s"Input to \"frag\" must be varargs, use \"bind\" instead.", partsExpr)
50 |
51 | /**
52 | * A macro that binds a `Seq[Content]` up into a [[Frag]] with no varargs, and schedules its contents to be spliced together as one
53 | * span, since it is assumed that it won't happen until runtime anyway.
54 | */
55 | inline def bind(inline seq: Seq[Content]): Frag = ${ bindMacro('seq) }
56 | private def bindMacro(seqExpr: Expr[Seq[Content]])(using Quotes): Expr[Frag] = '{ Frag(bindContent($seqExpr)) }
57 | private def bindContent(seq: Seq[Content]): String = {
58 | val sb = new StringBuilder()
59 | var i = 0
60 | val len = seq.size
61 | while i < len do {
62 | sb.append(seq(i).toString)
63 | i = i + 1
64 | }
65 | sb.toString
66 | }
67 |
68 | /** A piece of text that should not be ampersand-escaped. */
69 | final class Raw private[dottytags] (val str: String):
70 | override def toString: String = str
71 | /** [[Raw]] companion, private to hide [[Raw.apply]] */
72 | private[dottytags] object Raw:
73 | def apply (s: String): Raw = new Raw(s)
74 |
75 | /** Binds its string argument, indicating that it should be incorporated without being ampersand-escaped. */
76 | inline def raw(inline str: String): Raw = ${ rawMacro('str) }
77 | private def rawMacro(expr: Expr[String])(using Quotes): Expr[Raw] = '{ Raw($expr) }
78 | /** Splices its string arguments together using [[Splice]] and indicates that the result need not be ampersand-escaped when incorporated. */
79 | inline def rawSplice(inline parts: String*): Raw = ${ rawSpliceMacro('parts) }
80 | private def rawSpliceMacro(partsExpr: Expr[Seq[String]])(using Quotes): Expr[Raw] = partsExpr match {
81 | case Varargs(exprs) => '{ Raw(${ Splice.phaseSplice(exprs.map(expr => Phased(expr).maybeEval)*).expr }) }
82 | case _ => error("\"parts\" must be varargs.", partsExpr)
83 | }
--------------------------------------------------------------------------------
/src/main/scala/dottytags/StyleUnits.scala:
--------------------------------------------------------------------------------
1 | package dottytags
2 |
3 | import scala.quoted.*
4 |
5 | /*
6 | * Documentation marked "MDN" is thanks to Mozilla Contributors
7 | * at https://developer.mozilla.org/en-US/docs/Web/API and available
8 | * under the Creative Commons Attribution-ShareAlike v2.5 or later.
9 | * http://creativecommons.org/licenses/by-sa/2.5/
10 | *
11 | * Everything else is under the MIT License, see here:
12 | * https://github.com/CiaraOBrien/dottytags/blob/main/LICENSE
13 | *
14 | * This whole file is, of course, adapted from Scalatags (see LICENSE for copyright notice):
15 | * https://github.com/lihaoyi/scalatags/blob/master/scalatags/src/scalatags/DataTypes.scala
16 | */
17 |
18 | object units {
19 |
20 | /**
21 | * Macro to simplify all of the unit declarations, they all just call it with a different string parameter.
22 | */
23 | private inline def unit[A : Numeric](inline n: A, inline unit: String) = ${ unitMacro('n, 'unit) }
24 | private def unitMacro[A : Type](value: Expr[A], unit: Expr[String])(using Quotes): Expr[String] = value match {
25 | case '{ $n: Double } => Splice.phaseSplice(PhunctionToString(Phased(n).maybeEval), Phased(unit).maybeEval).expr
26 | case '{ $n: Int } => Splice.phaseSplice(PhunctionToString(Phased(n).maybeEval), Phased(unit).maybeEval).expr
27 | case _ => '{$value.toString.+($unit)}
28 | }
29 |
30 |
31 | extension [A : Numeric](inline n: A) {
32 |
33 | /**
34 | * Relative to the viewing device. For screen display, typically one device
35 | * pixel (dot) of the display.
36 | *
37 | * For printers and very high resolution screens one CSS pixel implies
38 | * multiple device pixels, so that the number of pixel per inch stays around
39 | * 96.
40 | *
41 | * MDN
42 | */
43 | inline def px = unit(n, "px")
44 |
45 |
46 | /**
47 | * One point which is 1/72 of an inch.
48 | *
49 | * MDN
50 | */
51 | inline def pt = unit(n, "pt")
52 |
53 | /**
54 | * One millimeter.
55 | *
56 | * MDN
57 | */
58 | inline def mm = unit(n, "mm")
59 |
60 | /**
61 | * One centimeter 10 millimeters.
62 | *
63 | * MDN
64 | */
65 | inline def cm = unit(n, "cm")
66 |
67 | /**
68 | * One inch 2.54 centimeters.
69 | *
70 | * MDN
71 | */
72 | inline def in = unit(n, "in")
73 |
74 | /**
75 | * One pica which is 12 points.
76 | *
77 | * MDN
78 | */
79 | inline def pc = unit(n, "pc")
80 | /**
81 | * This unit represents the calculated font-size of the element. If used on
82 | * the font-size property itself, it represents the inherited font-size
83 | * of the element.
84 | *
85 | * MDN
86 | */
87 | inline def em = unit(n, "em")
88 |
89 | /**
90 | * This unit represents the width, or more precisely the advance measure, of
91 | * the glyph '0' zero, the Unicode character U+0030 in the element's font.
92 | *
93 | * MDN
94 | */
95 | inline def ch = unit(n, "ch")
96 |
97 | /**
98 | * This unit represents the x-height of the element's font. On fonts with the
99 | * 'x' letter, this is generally the height of lowercase letters in the font;
100 | * 1ex ≈ 0.5em in many fonts.
101 | *
102 | * MDN
103 | */
104 | inline def ex = unit(n, "ex")
105 |
106 | /**
107 | * This unit represents the font-size of the root element e.g. the font-size
108 | * of the html element. When used on the font-size on this root element,
109 | * it represents its initial value.
110 | *
111 | * MDN
112 | */
113 | inline def rem = unit(n, "rem")
114 |
115 | /**
116 | * An angle in degrees. One full circle is 360deg. E.g. 0deg, 90deg, 360deg.
117 | */
118 | inline def deg = unit(n, "deg")
119 |
120 | /**
121 | * An angle in gradians. One full circle is 400grad. E.g. 0grad, 100grad,
122 | * 400grad.
123 | *
124 | * MDN
125 | */
126 | inline def grad = unit(n, "grad")
127 |
128 | /**
129 | * An angle in radians. One full circle is 2π radians which approximates
130 | * to 6.2832rad. 1rad is 180/π degrees. E.g. 0rad, 1.0708rad, 6.2832rad.
131 | *
132 | * MDN
133 | */
134 | inline def rad = unit(n, "rad")
135 |
136 | /**
137 | * The number of turns the angle is. One full circle is 1turn. E.g. 0turn,
138 | * 0.25turn, 1turn.
139 | *
140 | * MDN
141 | */
142 | inline def turn = unit(n, "turn")
143 |
144 | /**
145 | * A percentage value
146 | */
147 | inline def pct = unit(n, "%")
148 | }
149 |
150 | }
151 |
--------------------------------------------------------------------------------
/src/test/scala/CorrectnessTests.scala:
--------------------------------------------------------------------------------
1 | import dottytags.*
2 | import dottytags.predefs.all.*
3 | import dottytags.syntax.given
4 | import scala.language.implicitConversions
5 | import minitest.*
6 |
7 | /*
8 | * Most of these were adapted from Scalatags' test suite so as to correctly test for compatibility,
9 | * and also for incompatibility, as not all of them pass. (see LICENSE for copyright notice)
10 | */
11 |
12 | object CorrectnessTests extends SimpleTestSuite {
13 |
14 |
15 | test("Basic Tree Building") {
16 | val x = script("")
17 | assertXMLEquiv(
18 | html(
19 | head(x, tag("string-tag")("Hi how are you")),
20 | body(div(p()))
21 | ).toString,
22 | """
23 | |
24 | |
25 | |
26 | | Hi how are you
27 | |
28 | |
29 | |
"""
51 | )}
52 |
53 | // These two are probably pretty doable, but attrs and styles in frags is gonna really hard
54 | test("Style after Style-Attribute Appends") { assertXMLEquiv (
55 | div(cls:="my-class", style:="background-color: red;", float.left, p("i am a cow")),
56 | """
"""
62 | )}
63 |
64 | // Implicit conversions save the day
65 | test("Integer Sequence") { assertXMLEquiv (
66 | div(h1("Hello ", "#", 1), bind(for(i <- 0 until 5) yield i)),
67 | """
Hello #1
01234
"""
68 | )}
69 |
70 | // Very much does not compile right now, I may have to rethink how I do literally everything in order to get shit like
71 | // this to even compile
72 | test("String array") {
73 | val strArr = Array("hello")
74 | assertXMLEquiv (
75 | div(Some("lol"), Some(1), None: Option[String], h1("Hello"), Array(1, 2, 3), strArr, ()),
76 | """
147 | |
148 | |
149 | """.stripMargin)}
150 |
151 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DottyTags
2 | [](https://index.scala-lang.org/ciaraobrien/dottytags/dottytags)
3 |
4 | Finally released on Maven Central!
5 | ```scala
6 | libraryDependencies += "io.github.ciaraobrien" %% "dottytags" % "1.1.0"
7 | ```
8 |
9 | An experimental reimplementation of [ScalaTags](https://com-lihaoyi.github.io/scalatags/) in (extremely meta) Scala 3. It is a more-or-less working clone of
10 | 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
11 | metaprogramming capabilities are leveraged to automatically reduce the tree as much as possible to simple serial concatenation of strings at compile-time.
12 | 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
13 | 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
14 | post-hoc mutation of the tree, however ordering is manipulated in the initial tree: duplicate attributes combine, styles combine and override, etc. in
15 | the same way as Scalatags. The primary differences are:
16 |
17 | * No chained `Tag` applications like `div(cls := "header", backgroundColor := "blue")(divContents)`. This would require either compromising on the flattening performance
18 | 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,
19 | especially since the point of the library is to do as much as possible at compile-time.
20 | * Sequences of elements generated by for-loops and the like must be explicitly deconstructed into a `Frag` with the `bind` macro (though there is an implicit conversion for
21 | this in `dottytags.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
22 | Scalatags even on loop-heavy code. Wrapping elements up with the `frag` macro incurs no such performance hit, and does not disrupt the system's ability to achieve the optimal
23 | splicing completeness, but it can only be used with true varargs, vararg ascription doesn't help. Use `frag` for when you want to group up some content in a lightweight fashion.
24 |
25 | ## Performance
26 | Based on my very unprofessional benchmark comparisons, DottyTags is between 2 and 6 times faster than ScalaTags when
27 | the comparison is roughly fair (complex HTML trees involving loops, external variables, etc., like those used in ScalaTags'
28 | own benchmarks), and significantly faster in less-fair comparisons involving mostly-static tree generation, in which DottyTags has an absurd advantage
29 | 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
30 | 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
31 | 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.
32 |
33 | ## Example
34 | As a quick example, this:
35 |
36 | ```scala
37 |
38 | println(html(cls := "foo", href := "bar", css("baz1") := "qux", "quux",
39 | System.currentTimeMillis.toString, css("baz2") := "qux", raw("a")
40 | ).toString)
41 | ```
42 | Boils down to something like:
43 | ```scala
44 | println(Tag.apply(
45 | dottytags.spliceString(
46 | Array[String](
47 | "quux",
48 | dottytags.escapeString(scala.Long.box(System.currentTimeMillis()).toString()),
49 | "a"
50 | )
51 | )
52 | ).toString)
53 | ```
54 | Which, when run, yields:
55 | ```html
56 | quux1608810396295a
57 | ```
58 | For comparison, ScalaTags' interpretation of the same code (by swapping out the imports, since the syntax is broadly compatible in most cases):
59 | ```scala
60 | scalatags.Text.all.html().asInstanceOf[scalatags.Text.Text$TypedTag].apply(
61 | scala.runtime.ScalaRunTime.wrapRefArray([
62 | scalatags.Text.all.cls().:=("foo",
63 | scalatags.Text.all.stringAttr()
64 | ),
65 | scalatags.Text.all.href().:=("bar",
66 | scalatags.Text.all.stringAttr()
67 | ),
68 | scalatags.Text.all.css("baz1").:=("qux",
69 | scalatags.Text.all.stringStyle()
70 | ),
71 | scalatags.Text.all.stringFrag("quux"),
72 | scalatags.Text.all.stringFrag(
73 | scala.Long.box(System.currentTimeMillis()).toString()
74 | ),
75 | scalatags.Text.all.css("baz2").:=("qux",
76 | scalatags.Text.all.stringStyle()
77 | ),
78 | scalatags.Text.all.raw("a") : scalatags.generic.Modifier
79 | ])
80 | ).render()
81 | ```
82 |
83 | ## Recent Developments
84 | I recently completely overhauled the entire library and rewrote it from scratch (which I decided to make 1.0.0), including the
85 | underlying metametaprogramming system used to make implementing the library less hellish, which I have traditionally called Phaser
86 | despite the fact that "Stage" is the correct term for compile/macro-time vs runtime, while "Phase" refers properly to parts of the
87 | 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
88 | and styles according to the order in which they are present in the tag body. The metametaprogramming facilities are much better-developed
89 | this time around, consisting of `Phaser.scala` and `Splice.scala`, both of which are amenable to use elsewhere, and I will probably
90 | break them back out into a revived Phaser library for more general usage.
91 |
--------------------------------------------------------------------------------
/src/main/scala/dottytags/predefs/Tags.scala:
--------------------------------------------------------------------------------
1 | package dottytags.predefs
2 |
3 | import dottytags.*
4 |
5 | /*
6 | * This whole file is, of course, adapted from Scalatags (see LICENSE for copyright notice):
7 | * https://github.com/lihaoyi/scalatags/blob/master/scalatags/src/scalatags/generic/Tags.scala
8 | * https://github.com/lihaoyi/scalatags/blob/master/scalatags/src/scalatags/generic/Tags2.scala
9 | */
10 |
11 | object tags {
12 | // Root Element
13 | inline def html = tag("html")
14 | // Document Metadata
15 | inline def head = tag("head")
16 | inline def base = tag("base", selfClosing = true)
17 | inline def link = tag("link", selfClosing = true)
18 | inline def meta = tag("meta", selfClosing = true)
19 | // Scripting
20 | inline def script = tag("script")
21 | // Sections
22 | inline def body = tag("body")
23 | inline def h1 = tag("h1")
24 | inline def h2 = tag("h2")
25 | inline def h3 = tag("h3")
26 | inline def h4 = tag("h4")
27 | inline def h5 = tag("h5")
28 | inline def h6 = tag("h6")
29 | inline def header = tag("header")
30 | inline def footer = tag("footer")
31 | // Grouping content
32 | inline def p = tag("p")
33 | inline def hr = tag("hr", selfClosing = true)
34 | inline def pre = tag("pre")
35 | inline def blockquote = tag("blockquote")
36 | inline def ol = tag("ol")
37 | inline def ul = tag("ul")
38 | inline def li = tag("li")
39 | inline def dl = tag("dl")
40 | inline def dt = tag("dt")
41 | inline def dd = tag("dd")
42 | inline def figure = tag("figure")
43 | inline def figcaption = tag("figcaption")
44 | inline def div = tag("div")
45 | // Text-level semantics
46 | inline def a = tag("a")
47 | inline def em = tag("em")
48 | inline def strong = tag("strong")
49 | inline def small = tag("small")
50 | inline def s = tag("s")
51 | inline def cite = tag("cite")
52 | inline def code = tag("code")
53 | inline def sub = tag("sub")
54 | inline def sup = tag("sup")
55 | inline def i = tag("i")
56 | inline def b = tag("b")
57 | inline def u = tag("u")
58 | inline def span = tag("span")
59 | inline def br = tag("br", selfClosing = true)
60 | inline def wbr = tag("wbr", selfClosing = true)
61 | // Edits
62 | inline def ins = tag("ins")
63 | inline def del = tag("del")
64 | // Embedded content
65 | inline def img = tag("img", selfClosing = true)
66 | inline def iframe = tag("iframe")
67 | inline def embed = tag("embed", selfClosing = true)
68 | inline def `object` = tag("object")
69 | inline def param = tag("param", selfClosing = true)
70 | inline def video = tag("video")
71 | inline def audio = tag("audio")
72 | inline def source = tag("source", selfClosing = true)
73 | inline def track = tag("track", selfClosing = true)
74 | inline def canvas = tag("canvas")
75 | inline def map = tag("map")
76 | inline def area = tag("area", selfClosing = true)
77 | // Tabular data
78 | inline def table = tag("table")
79 | inline def caption = tag("caption")
80 | inline def colgroup = tag("colgroup")
81 | inline def col = tag("col", selfClosing = true)
82 | inline def tbody = tag("tbody")
83 | inline def thead = tag("thead")
84 | inline def tfoot = tag("tfoot")
85 | inline def tr = tag("tr")
86 | inline def td = tag("td")
87 | inline def th = tag("th")
88 | // Forms
89 | inline def form = tag("form")
90 | inline def fieldset = tag("fieldset")
91 | inline def legend = tag("legend")
92 | inline def label = tag("label")
93 | inline def input = tag("input", selfClosing = true)
94 | inline def button = tag("button")
95 | inline def select = tag("select")
96 | inline def datalist = tag("datalist")
97 | inline def optgroup = tag("optgroup")
98 | inline def option = tag("option")
99 | inline def textarea = tag("textarea")
100 | }
101 |
102 | /**
103 | * Contains builtin tags which are used less frequently. These are not imported by
104 | * default to avoid namespace pollution.
105 | */
106 | object miscTags {
107 | // Document metadata
108 | inline def title = tag("title")
109 | inline def style = tag("style")
110 | // Scripting
111 | inline def noscript = tag("noscript")
112 | // Sections
113 | inline def section = tag("section")
114 | inline def nav = tag("nav")
115 | inline def article = tag("article")
116 | inline def aside = tag("aside")
117 | inline def address = tag("address")
118 | inline def main = tag("main")
119 | // Text level semantics
120 | inline def q = tag("q")
121 | inline def dfn = tag("dfn")
122 | inline def abbr = tag("abbr")
123 | inline def data = tag("data")
124 | inline def time = tag("time")
125 | inline def `var` = tag("var")
126 | inline def samp = tag("samp")
127 | inline def kbd = tag("kbd")
128 | inline def math = tag("math")
129 | inline def mark = tag("mark")
130 | inline def ruby = tag("ruby")
131 | inline def rt = tag("rt")
132 | inline def rp = tag("rp")
133 | inline def bdi = tag("bdi")
134 | inline def bdo = tag("bdo")
135 | // Forms
136 | inline def keygen = tag("keygen", selfClosing = true)
137 | inline def output = tag("output")
138 | inline def progress = tag("progress")
139 | inline def meter = tag("meter")
140 | // Interactive elements
141 | inline def details = tag("details")
142 | inline def summary = tag("summary")
143 | inline def command = tag("command", selfClosing = true)
144 | inline def menu = tag("menu")
145 | }
--------------------------------------------------------------------------------
/src/main/scala/dottytags/Splice.scala:
--------------------------------------------------------------------------------
1 | package dottytags
2 |
3 | import scala.collection.mutable.ListBuffer
4 | import scala.quoted.*
5 |
6 | /**
7 | * Handles the compile-time splicing-together of adjacent [[Static]] spans, separated by [[Phased]] spans which cannot
8 | * be spliced until runtime, at which point the whole list of remaining spans, including the [[Static]] ones,
9 | * should be spliced together in one reasonably efficient operation into one result value.
10 | */
11 | object Splice {
12 | /**
13 | * Splices some arbitrary number of [[Phaser]] spans of a particular [[Spliceable]] type together into one [[Phaser]],
14 | * which, if it is [[Phased]], can be disenspliced by an enclosing macro invocation to retrieve the original span [[Phaser]]s
15 | * that were present in this splicing cycle. Thus, an enclosing macro can get full access to every separate static and
16 | * phased span that resides underneath it, but if the splice is left free to runtime, it will splice properly (see [[spliceString]]).
17 | * @param spans The spans to be spliced together.
18 | * @tparam A The [[Spliceable]] type we're interested in splicing. For instance, [[String]].
19 | * @return A [[Phaser]]; if [[Phased]], it should be able to be disenspliced in future; if [[Static]], then there's
20 | * no need since it's just one span anyway.
21 | */
22 | def phaseSplice[A](spans: Phaser[A]*)(using q: Quotes, s: Spliceable[A], te: ToExpr[A], fe: FromExpr[A]): Phaser[A] = {
23 | // Attempt to extract prior splices from spans based on their expressions containing spliceString method calls
24 | val expanded = spans.flatMap(span => span match {
25 | case st @ Static(eval: Eval[A]) => Seq(st)
26 | case Phased(expr: Expr[A]) => s.disensplice(expr).map(e => Phased(e).maybeEval)
27 | })
28 | // This is all pretty unidiomatic sadly, might go back over it at some point
29 | // Iterate over the list of spans, condensing them into another list, if the next span in `expanded` is static,
30 | // and the span on top of `condensed` is static, then combine the two onto the top of `condensed` and move on. If
31 | // the next span in `expanded` is phased, or if there are no spans in `condensed` yet, then we have to put the next span on top of `condensed`.
32 | var condensed: List[Phaser[A]] = List()
33 | val iter = expanded.iterator
34 | while iter.hasNext do {
35 | iter.next match {
36 | case phase1 @ Static(eval1: Eval[A]) => condensed.headOption match
37 | case Some(Static(eval2: Eval[A])) => condensed = Static(s.spliceStatic(eval2, eval1)) :: condensed.tail
38 | case _ => condensed = phase1 :: condensed
39 | case phase1 @ Phased(expr1: Expr[A]) => condensed = phase1 :: condensed
40 | }
41 | }
42 | // If there is at least one phased span, then we have to ensplice the whole list,
43 | // if there are no phased spans then either there is exactly one static span, being the sum of all the other
44 | // formerly static spans (if there were no phased spans and some static spans), or there are no spans (if there
45 | // were no spans to begin with).
46 | if condensed.exists(!_.isStatic) then Phased(s.ensplice(condensed.map(_.expr).reverse))
47 | else condensed.headOption.getOrElse(Static(s.identity))
48 | }
49 |
50 | /**
51 | * Tries to identify expressions that contain [[spliceString]] invocations (which should therefore have been produced by
52 | * [[Spliceable.ensplice()]] in a previous macro expansion) and break them back up again into their component static and
53 | * phased spans, so they can be incorporated into the current splice.
54 | * @param span A phaser which, if it is phased, might be the remnant of an earlier splice cycle.
55 | * @return 1 or more spans, 1 if [[span]] was not a splice remnant, more if it was and we were able to disensplice it.
56 | */
57 | def phaseDisensplice[A](span: Phaser[A])(using q: Quotes, s: Spliceable[A], fe: FromExpr[A]): Seq[Phaser[A]] = span match {
58 | case st @ Static(eval: Eval[A]) => Seq(st)
59 | case Phased(expr: Expr[A]) => s.disensplice(expr).map(e => Phased(e).maybeEval)
60 | }
61 | }
62 |
63 | /**
64 | * Basically monoid stuff, plus an agreed-upon protocol for ensplicement and disensplicement (in the case of
65 | * [[StringSpliceable]] the splicing medium is [[spliceString]], so it is the form into which span lists are enspliced,
66 | * and it also offers a way to disensplice the exact span expressions that were enspliced by simply examining the varargs.
67 | * @tparam A The (monoidal) type that we're interested in splicing together
68 | */
69 | trait Spliceable[A] {
70 | def identity: A
71 | def spliceStatic(a1: A, a2: A): A
72 | def ensplice(spans: Seq[Expr[A]])(using Quotes): Expr[A]
73 | def disensplice(splice: Expr[A])(using Quotes): Seq[Expr[A]]
74 | }
75 |
76 | given StringSpliceable: Spliceable[String] with
77 | override def identity: String = ""
78 | override def spliceStatic(a1: String, a2: String): String = a1 + a2
79 | // Via ensplice and disensplice, spliceString acts not only as the ultimate executor of the splicing process,
80 | // using a StringBuilder, but it also acts to carry information about the splicing process along the way,
81 | // since it encodes all the spans it's meant to splice in its varargs, and this can be used to transmit finer-grained
82 | // splicing information to enclosing macro invocations, whereas normally they'd just be stuck with whatever string
83 | // expression came out of their sub-invocations (child macros).
84 | override def ensplice(spans: Seq[Expr[String]])(using Quotes): Expr[String] = '{ spliceString(${Varargs(spans)}: _*) }
85 | override def disensplice(splice: Expr[String])(using Quotes): Seq[Expr[String]] = splice match {
86 | case '{spliceString(${Varargs(spans)}: _*)} => spans
87 | case _ => Seq(splice)
88 | }
89 |
90 | /**
91 | * Does the dirty work at runtime of actually rolling all the spans into a [[StringBuilder]],
92 | * but also carries said spans as arguments in its list, so if macro code like [[Spliceable.disensplice]] examines an
93 | * invocation of [[spliceString]] it can extract all the spans, be they [[Static]] or [[Phased]].
94 | */
95 | def spliceString(as: String*): String = {
96 | val sb = new StringBuilder()
97 | var i = 0
98 | val len = as.size
99 | while i < len do {
100 | sb.append(as(i))
101 | i = i + 1
102 | }
103 | sb.toString
104 | }
105 |
106 |
--------------------------------------------------------------------------------
/src/test/scala/PerfTest.scala:
--------------------------------------------------------------------------------
1 | import minitest.*
2 |
3 | object PerfTestDotty extends SimpleTestSuite {
4 |
5 | import dottytags.*
6 | import dottytags.predefs.all.*
7 | import dottytags.syntax.given
8 | import scala.language.implicitConversions
9 |
10 | test("Dotty Perf Test") {
11 |
12 |
13 | val start = System.currentTimeMillis()
14 | var i: Long = 0
15 | val d = 10000
16 | var name = "DottyTags"
17 |
18 | while System.currentTimeMillis() - start < d do {
19 | i += 1
20 | calcStatic()
21 | }
22 |
23 | println(name.padTo(20, ' ') + i + " in " + d)
24 | println(name)
25 |
26 | }
27 |
28 | val titleString = "This is my title"
29 | val firstParaString = "This is my first paragraph"
30 | val contentpara = "contentpara"
31 | val first = "first"
32 |
33 | type AttrType = Attr
34 | type StyleType = Style
35 |
36 | inline def para(inline n: Int, inline s: String, inline m: AttrType, inline t: StyleType): Tag = p(
37 | title := ("this is paragraph " + n),
38 | s,
39 | m,
40 | t
41 | )
42 |
43 | def calcExpensive(): String = {
44 | html(
45 | head(
46 | script("console.log(1)")
47 | ),
48 | body(
49 | h1(color := "red", titleString),
50 | div(backgroundColor := "blue",
51 | para(0,
52 | firstParaString,
53 | cls := contentpara + " " + first,
54 | color := "white"
55 | ),
56 | a(href := "www.google.com",
57 | p("Goooogle")
58 | ),
59 | bind(for (i <- 0 until 5) yield {
60 | para(i,
61 | "Paragraph " + i,
62 | cls := contentpara,
63 | color := (if i % 2 == 0 then "red" else "green"),
64 | )
65 | })
66 | )
67 | )
68 | ).toString
69 | }
70 |
71 | def calcCheapish(): String = {
72 | {
73 | val title = "title"
74 | val numVisitors = 1023
75 |
76 | html(
77 | head(
78 | script("some script")
79 | ),
80 | body(
81 | h1("This is my ", title),
82 | div(
83 | p("This is my first paragraph"),
84 | p("you are the ", numVisitors.toString, "th visitor!")
85 | )
86 | )
87 | )
88 | }.toString
89 | }
90 |
91 | def calcStatic(): String = {
92 | html(
93 | head(
94 | script("some script")
95 | ),
96 | body(
97 | h1(backgroundColor := "blue", color := "red", "This is my title"),
98 | div(backgroundColor := "blue", color := "red",
99 | p(cls := "contentpara first",
100 | "This is my first paragraph"
101 | ),
102 | a(opacity := 0.9,
103 | p(cls := "contentpara", "Goooogle")
104 | )
105 | )
106 | )
107 | ).toString
108 | }
109 |
110 | }
111 |
112 | object PerfTestScala extends SimpleTestSuite {
113 |
114 | import scalatags.*
115 | import scalatags.Text.*
116 | import scalatags.Text.all.*
117 |
118 | test("Scala Perf Test") {
119 |
120 |
121 | val start = System.currentTimeMillis()
122 | var i: Long = 0
123 | val d = 10000
124 | var name = "ScalaTags"
125 |
126 | while System.currentTimeMillis() - start < d do {
127 | i += 1
128 | calcStatic()
129 | }
130 |
131 | println(name.padTo(20, ' ') + i + " in " + d)
132 | println(name)
133 |
134 | }
135 |
136 | val titleString = "This is my title"
137 | val firstParaString = "This is my first paragraph"
138 | val contentpara = "contentpara"
139 | val first = "first"
140 |
141 | type AttrType = Text.Modifier
142 | type StyleType = Text.Modifier
143 |
144 | def para(n: Int, s: String, m: AttrType, t: StyleType) = p(
145 | title := ("this is paragraph " + n),
146 | s,
147 | m,
148 | t
149 | )
150 |
151 | def calcExpensive(): String = {
152 | html(
153 | head(
154 | script("console.log(1)")
155 | ),
156 | body(
157 | h1(color := "red", titleString),
158 | div(backgroundColor := "blue",
159 | para(0,
160 | firstParaString,
161 | cls := contentpara + " " + first,
162 | color := "white"
163 | ),
164 | a(href := "www.google.com",
165 | p("Goooogle")
166 | ),
167 | for (i <- 0 until 5) yield {
168 | para(i,
169 | "Paragraph " + i,
170 | cls := contentpara,
171 | color := (if i % 2 == 0 then "red" else "green"),
172 | )
173 | }
174 | )
175 | )
176 | ).toString
177 | }
178 |
179 | def calcCheapish(): String = {
180 | {
181 | val title = "title"
182 | val numVisitors = 1023
183 |
184 | html(
185 | head(
186 | script("some script")
187 | ),
188 | body(
189 | h1("This is my ", title),
190 | div(
191 | p("This is my first paragraph"),
192 | p("you are the ", numVisitors.toString, "th visitor!")
193 | )
194 | )
195 | )
196 | }.toString
197 | }
198 |
199 | def calcStatic(): String = {
200 | html(
201 | head(
202 | script("some script")
203 | ),
204 | body(
205 | h1(backgroundColor := "blue", color := "red", "This is my title"),
206 | div(backgroundColor := "blue", color := "red",
207 | p(cls := "contentpara first",
208 | "This is my first paragraph"
209 | ),
210 | a(opacity := 0.9,
211 | p(cls := "contentpara", "Goooogle")
212 | )
213 | )
214 | )
215 | ).toString
216 | }
217 | }
--------------------------------------------------------------------------------
/src/main/scala/dottytags/Tags.scala:
--------------------------------------------------------------------------------
1 | package dottytags
2 |
3 | import scala.annotation.targetName
4 | import scala.collection.immutable.TreeSeqMap
5 | import scala.quoted.*
6 | import dottytags.given
7 |
8 | /**
9 | * The seed of an HTML/XML/whatever tag, which can be given child contents and attributes by [[apply]]ing it to varargs
10 | * of type [[Entity]]. Can be self-closing, in which case it will close like ` ` instead of ` ` if it has
11 | * no child [[Content]]s ([[Modifier]]s are still fair game).
12 | */
13 | final class TagClass private[dottytags] (val name: String, val selfClosing: Boolean):
14 | override def toString: String = if selfClosing then s"<$name />" else s"<$name>$name>"
15 | /** [[TagClass]] companion, private to hide [[TagClass.apply]] */
16 | private[dottytags] object TagClass:
17 | def apply (name: String, sc: Boolean): TagClass = new TagClass(name, sc)
18 |
19 | /**
20 | * Constructor macro for a [[TagClass]], which enables you to specify a name (must match `^[a-z][:\w0-9-]*$`),
21 | * and whether or not the tag should be self-closing like ` `.
22 | */
23 | inline def tag(inline name: String, inline selfClosing: Boolean = false): TagClass = ${ tagClassMacro('name, 'selfClosing) }
24 | private def tagClassMacro(nameExpr: Expr[String], selfClosingExpr: Expr[Boolean])(using Quotes): Expr[TagClass] =
25 | Expr(TagClass(validateTagName(nameExpr), Phaser.require(selfClosingExpr, "self-closing flag")))
26 |
27 | /**
28 | * An HTML/XML/SVG/whatever element, such as `LinkHere`,
29 | * where `a` is the name/[[TagClass]], `href="..."` is an [[Attr]], `color: red;` is an inline CSS [[Style]] (hence its
30 | * presence in the `style` attribute), and `Link` and `Here` are child [[Content]]s of the [[Tag]].
31 | * @param str the spliced string expression (see [[Splice]]) that holds the textual representation of this [[Tag]] in the output.
32 | */
33 | final class Tag private[dottytags] (val str: String):
34 | override def toString: String = str
35 | /** [[Tag]] companion, private to hide [[Tag.apply]] */
36 | private[dottytags] object Tag:
37 | def apply (s: String): Tag = new Tag(s)
38 |
39 | /**
40 | * The most important macro in this library, constructs a [[Tag]] from a [[TagClass]] and some varargs of [[Entity]] values,
41 | * including [[Attr]]s, [[Style]]s, [[Frag]]s, [[Raw]]s, other [[Tag]]s, and normal [[String]]s (which will be ampersand-escaped
42 | * on their way in). Order is respected fully for [[Content]]s, but [[Modifier]]s have some special behaviour that can cause
43 | * them to be applied out-of-order, such as [[Attr]]s of the same name combining into the first [[Attr]]'s value quotes.
44 | */
45 | extension (inline cls: TagClass) @targetName("tagApplyBody") inline def apply(inline entities: Entity*): Tag = ${ tagMacro('cls)('entities) }
46 | private def tagMacro(clsExpr: Expr[TagClass])(varargs: Expr[Seq[Entity]])(using Quotes): Expr[Tag] = {
47 | val cls = Phaser.require(clsExpr, "tag class")
48 | // The canonical ordered list of modifiers, including the `style` attribute if there are any inline styles.
49 | // TreeSeqMap is used because it's a Map that preserves insertion order, which is how attribute concatenation is supposed to work.
50 | // The values are Phasers because we need to have proper splicing and desplicing.
51 | var canonMods = TreeSeqMap[String, Phaser[String]]()
52 | // Go over the varargs jut for Modifiers (Attrs and Styles), we will go over again for Contents later.
53 | Varargs.unapply(varargs).foreach { _.foreach {
54 | case '{ Attr($nameExpr : String, $valueExpr: String) } => val name = Phaser.require(nameExpr, "attribute name")
55 | if name == "style" && canonMods.contains(name) then // An explicit "style" attribute overwrites any previous "style" attribute contents entirely.
56 | canonMods = canonMods.updated(name, Splice.phaseSplice(Static("=\""), Phased(valueExpr).maybeEval)) // The new splice starts with =".
57 | else if canonMods.contains(name) then // We have a matching attribute already, concatenate the two attrs' values
58 | canonMods = canonMods.updated(name, Splice.phaseSplice(canonMods.get(name).get, Static(" "), Phased(valueExpr).maybeEval) )
59 | else // The new attribute value splice starts with =", the closing " will be added to each attribute value at the end.
60 | canonMods = canonMods.updated(name, Splice.phaseSplice(Static("=\""), Phased(valueExpr).maybeEval))
61 | case '{ Style($nameExpr: String, $valueExpr: String) } => val name = Phaser.require(nameExpr, "style name")
62 | if canonMods.contains("style") then // If there is already a "style" attribute (either some previous inline styles, or an explicit "style" attr), update it
63 | canonMods = canonMods.updated("style", Splice.phaseSplice(canonMods.get("style").get, Static(" "), Static(name),
64 | Static(": "), Phased(valueExpr).maybeEval, Static(";")) )
65 | else // If there is no "style" attribute, make one.
66 | canonMods = canonMods.updated("style", Splice.phaseSplice(Static("=\""), Static(name),
67 | Static(": "), Phased(valueExpr).maybeEval, Static(";")))
68 | // These restrictions are necessary because fuck the very notion of parsing and editing the existing string value at runtime
69 | // to figure out where phased attrs and styles should go.
70 | case '{ $attr: Attr } => error("Attributes cannot be fully dynamic, only their values may vary at runtime.", attr)
71 | case '{ $style: Style } => error("Styles cannot be fully dynamic, only their values may vary at runtime.", style)
72 | case _ => () // Ignore Contents, we only care about Modifiers in this loop
73 | }}
74 | // Splice together all the modifiers, including a leading space, the attr name, =", values, and a closing ".
75 | val modifierSplice = Splice.phaseSplice(canonMods.map((name, splice) =>
76 | Splice.phaseSplice(Static(" "), Static(name), splice, Static("\""))).toSeq*)
77 | // Splice together all the tag contents, ignoring the modifiers, escaping string spans, be they static or phased.
78 | val contentsSplice = Varargs.unapply(varargs).map(exprs => Splice.phaseSplice(exprs.collect {
79 | case '{ $str: String } => PhunctionEscapeStr(Phased(str).maybeEval)
80 | case '{ Tag($str) } => Phased(str).maybeEval
81 | case '{ Raw($str) } => Phased(str).maybeEval
82 | case '{ Frag($str) } => Phased(str).maybeEval
83 | case '{ $tag: Tag } => Phased('{$tag.str}).maybeEval
84 | case '{ $raw: Raw } => Phased('{$raw.str}).maybeEval
85 | case '{ $frag: Frag } => Phased('{$frag.str}).maybeEval
86 | }*) // phaseSplice takes varargs
87 | ).getOrElse(Static("")) // Just have it empty if there are no varargs.
88 | // Construct the final Tag.apply invocation with a splice inside.
89 | contentsSplice match {
90 | case Static("") if cls.selfClosing => // If the content is empty and this is a self-closing tag, self-close! (add modifiers still though)
91 | '{ Tag(${ Splice.phaseSplice(Static("<"), Static(cls.name), modifierSplice, Static(" />")).expr }) }
92 | case contents => '{ Tag(${ Splice.phaseSplice(Static("<"), Static(cls.name), modifierSplice, Static(">"),
93 | contents, Static(""), Static(cls.name), Static(">")).expr }) }
94 | }
95 | }
--------------------------------------------------------------------------------
/src/main/scala/dottytags/Modifiers.scala:
--------------------------------------------------------------------------------
1 | package dottytags
2 |
3 | import scala.annotation.targetName
4 | import scala.quoted.*
5 |
6 | // === Attributes === //
7 |
8 | /**
9 | * The seed of an attribute, which can be given a value with one of the [[:=]] operators,
10 | * or [[empty]] for a flag value. Can be [[raw]], in which case it doesn't check the validity of the name as an
11 | * XML attribute name (compliance with the regex `^[a-zA-Z_:][-a-zA-Z0-9_:.]*$`).
12 | */
13 | final class AttrClass private (val name: String, val raw: Boolean):
14 | override def toString: String = name
15 | /** [[AttrClass]] companion, private to hide [[AttrClass.apply]] */
16 | private[dottytags] object AttrClass:
17 | def apply(name: String, raw: Boolean): AttrClass = new AttrClass(name, raw)
18 |
19 | /**
20 | * Constructor macro for an [[AttrClass]], which enables you to specify a name, and choose whether that name should be
21 | * validated as an XML attribute name (based on the regex `^[a-zA-Z_:][-a-zA-Z0-9_:.]*$`), or taken as raw input.
22 | */
23 | inline def attr(inline name: String, inline raw: Boolean = false): AttrClass = ${ attrClassMacro('name, 'raw) }
24 | private def attrClassMacro(nameExpr: Expr[String], rawExpr: Expr[Boolean])(using Quotes): Expr[AttrClass] =
25 | Phaser.require(rawExpr, "raw attribute flag") match
26 | case true => Expr(AttrClass(Phaser.require(nameExpr, "attribute name"), true))
27 | case false => Expr(AttrClass(validateAttrName(nameExpr), false))
28 |
29 | /**
30 | * Converts an [[AttrClass]] into a boolean flag [[Attr]], whose value is equal to its name.
31 | */
32 | extension (inline attrClass: AttrClass) inline def empty: Attr = attrClass := attrClass.name
33 |
34 | /**
35 | * An XML attribute, such as `class="cls"`, where `class` is the name/[[AttrClass]] and `cls`` is the value.
36 | * Can be added to a [[Tag]] as part of tree construction.
37 | * @param name The left-hand side of the equals sign. Usually follows a particular regex, but some [[AttrClass]]es are declared raw.
38 | * @param value The right-hand side of the equals sign, within the quotation marks.
39 | */
40 | final class Attr private (val name: String, val value: String):
41 | override def toString: String = name + "=\"" + value + "\""
42 | /** [[Attr]] companion, private to hide [[Attr.apply]] */
43 | private[dottytags] object Attr:
44 | def apply (name: String, value: String): Attr = new Attr(name, value)
45 |
46 | /**
47 | * Constructs an XML attribute from an [[AttrClass]] and an [[Int]].
48 | */
49 | extension (inline attrClass: AttrClass) @targetName("setAttrInt") inline def :=(inline value: Int): Attr = ${ setAttrIntMacro('attrClass, 'value) }
50 | private def setAttrIntMacro(attrClass: Expr[AttrClass], value: Expr[Int])(using Quotes): Expr[Attr] =
51 | setAttrNoEscMacro(attrClass, PhunctionToString(Phased(value).maybeEval).expr)
52 |
53 | /**
54 | * Constructs an XML attribute from an [[AttrClass]] and a [[String]], which will be HTML-escaped. The [[AttrClass]]
55 | * must be static (known at compile-time), the [[String]] may be static or dynamic.
56 | */
57 | extension (inline attrClass: AttrClass) @targetName("setAttr") inline def :=(inline value: String): Attr = ${ setAttrMacro('attrClass, 'value) }
58 | private def setAttrMacro(attrClassExpr: Expr[AttrClass], valueExpr: Expr[String])(using Quotes): Expr[Attr] =
59 | val attrClass = Phaser.require(attrClassExpr, "attribute definition")
60 | '{ Attr(${Expr(attrClass.name)}, ${PhunctionEscapeStr(Phased(valueExpr).maybeEval).expr}) }
61 |
62 | /**
63 | * Constructs an XML attribute from an [[AttrClass]] and a [[String]], which will not be HTML-escaped. The [[AttrClass]]
64 | * must be static (known at compile-time), the [[String]] may be static or dynamic.
65 | */
66 | private def setAttrNoEscMacro(attrClassExpr: Expr[AttrClass], valueExpr: Expr[String])(using Quotes): Expr[Attr] =
67 | val attrClass = Phaser.require(attrClassExpr, "attribute definition")
68 | '{ Attr(${Expr(attrClass.name)}, ${Phased(valueExpr).maybeEval.expr}) }
69 |
70 | // === Styles === //
71 |
72 | /**
73 | * The seed of an inline CSS style tag, which can be given a value with one of the [[:=]] operators.
74 | * @param name must be a valid CSS style name (approximately kebab case, `^-?[_a-zA-Z]+[_a-zA-Z0-9-]*$`)
75 | * @param defaultUnits if not empty, provides a unit suffix to any double or int values assigned to this [[StyleClass]].
76 | * Bypass this by passing the values as strings, or even by assigning your own units.
77 | */
78 | final class StyleClass private (val name: String, val defaultUnits: String):
79 | override def toString: String = name
80 | /** [[StyleClass]] companion, private to hide [[StyleClass.apply]] */
81 | private[dottytags] object StyleClass:
82 | def apply (name: String, defaultUnits: String): StyleClass = new StyleClass(name, defaultUnits)
83 |
84 | /**
85 | * Constructor macro for a [[StyleClass]], which enables you to specify a name, which must be validated as a CSS style name
86 | * (kebab-case), and which also allows you to specify a default unit suffix for any raw integer or double values that are
87 | * assigned to this [[StyleClass]] via [[:=]].
88 | */
89 | inline def css(inline name: String, inline defaultUnits: String = ""): StyleClass = ${ styleClassMacro('name, 'defaultUnits) }
90 | private def styleClassMacro(nameExpr: Expr[String], defaultUnitsExpr: Expr[String])(using Quotes): Expr[StyleClass] =
91 | Expr(StyleClass(validateStyleName(nameExpr), Phaser.require(defaultUnitsExpr, "default units")))
92 |
93 | /**
94 | * An inline CSS style tag, such as `float: left;`, which will be placed within an HTML inline `style` attribute
95 | * if added to a [[Tag]] as part of tree construction.
96 | * @param name The left-hand side of the colon. Must fulfill the regex `^-?[_a-zA-Z]+[_a-zA-Z0-9-]*$`.
97 | * @param value The right-hand side of the colon, which may have a unit suffix.
98 | */
99 | final class Style private (val name: String, val value: String):
100 | override def toString: String = name + ": " + value + ";"
101 | /** [[Style]] companion, private to hide [[Style.apply]] */
102 | private[dottytags] object Style:
103 | def apply (name: String, value: String): Style = new Style(name, value)
104 |
105 | /**
106 | * Constructs an inline CSS style tag from a [[StyleClass]] and a [[String]], which will be HTML-escaped. The [[StyleClass]]
107 | * must be static (known at compile-time), the [[String]] may be static or dynamic.
108 | */
109 | extension (inline styleClass: StyleClass) @targetName("setStyle") inline def :=(inline value: String): Style = ${ setStyleMacro('styleClass, 'value) }
110 | private def setStyleMacro(styleClassExpr: Expr[StyleClass], valueExpr: Expr[String])(using Quotes): Expr[Style] =
111 | val styleClass: StyleClass = Phaser.require(styleClassExpr, "style definition")
112 | '{ Style(${Expr(styleClass.name)}, ${PhunctionEscapeStr(Phased(valueExpr).maybeEval).expr}) }
113 |
114 | /**
115 | * Constructs an inline CSS style tag from a [[StyleClass]] and a [[Double]], which will be given a default unit suffix if
116 | * the [[StyleClass]] in question has one declared. The [[StyleClass]] must be static (known at compile-time),
117 | * the [[Double]] may be static or dynamic.
118 | */
119 | extension (inline styleClass: StyleClass) @targetName("setStyleUnitsDouble") inline def :=(inline value: Double): Style =
120 | ${ setStyleUnitsMacro('styleClass, 'value) }
121 | private def setStyleUnitsMacro(styleClassExpr: Expr[StyleClass], valueExpr: Expr[Double])(using Quotes): Expr[Style] =
122 | val styleClass: StyleClass = Phaser.require(styleClassExpr, "style definition")
123 | setStyleMacro(styleClassExpr, Splice.phaseSplice(PhunctionToString(Phased(valueExpr).maybeEval), Static(styleClass.defaultUnits)).expr)
124 |
125 | /**
126 | * Constructs an inline CSS style tag from a [[StyleClass]] and a [[Int]], which will be given a default unit suffix if
127 | * the [[StyleClass]] in question has one declared. The [[StyleClass]] must be static (known at compile-time),
128 | * the [[Int]] may be static or dynamic.
129 | */
130 | extension (inline styleClass: StyleClass) @targetName("setStyleUnitsInt") inline def :=(inline value: Int): Style =
131 | ${ setStyleUnitsIntMacro('styleClass, 'value) }
132 | private def setStyleUnitsIntMacro(styleClassExpr: Expr[StyleClass], valueExpr: Expr[Int])(using Quotes): Expr[Style] =
133 | val styleClass: StyleClass = Phaser.require(styleClassExpr, "style definition")
134 | setStyleMacro(styleClassExpr, Splice.phaseSplice(PhunctionToString(Phased(valueExpr).maybeEval), Static(styleClass.defaultUnits)).expr)
--------------------------------------------------------------------------------
/src/main/scala/dottytags/Phaser.scala:
--------------------------------------------------------------------------------
1 | package dottytags
2 |
3 | import scala.quoted.*
4 |
5 | /**
6 | * This whole system could be made much more ergonomic probably, but in months of working on it
7 | * and restarting several times, I have yet to find a particularly ergonomic setup, though
8 | * this is probably the best it's been. This whole "Phaser" concept is pretty much just Applicative,
9 | * but the Compiler God demands boilerplate, it's very difficult to get a "static" function `A => B` and the
10 | * corresponding "deferred" function `Expr[A] => Expr[B]` (essentially a function over expressions that sets up the
11 | * former function to be executed at runtime on the value of the expression in question) without having to type
12 | * both out and carry them around in a datatype, hence [[Phunction1]] and [[Phunction2]].
13 | */
14 |
15 | /**
16 | * Represents an [[Expr]] we encounter during macro expansion, and the possibility to discern its static value, or
17 | * to fail to discern that value (because it can only be known at runtime, for any number of reasons), and the possibility
18 | * to do the same calculations on it regardless, either performing the operations on the static value during compile-time,
19 | * or "scheduling" the same operations to be carried out in order at runtime.
20 | */
21 | sealed abstract class Phaser[+A] protected () {
22 | def isStatic: Boolean
23 | /** Option for if we have a value available (if this [[Phaser]] is [[Static]]) */
24 | def evalOpt: Option[Eval[A]]
25 | }
26 |
27 | /** An expression whose value we can determine at compile-time using [[FromExpr]]s, so we can do computations on it directly. */
28 | final case class Static[+A](val eval: Eval[A]) extends Phaser[A] {
29 | override def isStatic: Boolean = true
30 | override def evalOpt: Option[Eval[A]] = Some(eval)
31 | }
32 | /** Literally just so I don't have to worry about aligning `A` with `Expr[A]` in parallel lines of code, instead it just stays aligned automatically. */
33 | type Eval[A] = A
34 |
35 | /** An expression whose value we cannot determine right now, perhaps it's a variable reference or maybe it's just
36 | * shrouded by an unknown method call, either way we only know its type, but we can do things with it anyway by
37 | * wrapping its expression in method calls and stuff, so we can "schedule" computations to be done on it, once its value is known
38 | * at runtime. This enables us to do the same computations on both scrutible and inscrutible expressions, so we
39 | * can abstract over compile-time legibility, which is the whole point of this paradigm. */
40 | final case class Phased[+A](val expr: Expr[A]) extends Phaser[A] {
41 | override def isStatic: Boolean = false
42 | override def evalOpt: Option[Eval[A]] = None
43 | }
44 |
45 | /** [[Phaser]] companion, can be entirely public, doesn't need to hide anything while retaining visibility via macro method call injection */
46 | object Phaser {
47 | /** It's very natural to do Applicative type stuff with [[Phaser]], but you have to avoid the urge in this case because
48 | * generating a bunch of code to fuck around with tuples at runtime would be counterproductive. */
49 | def product[A : Type, B : Type](phaser1: Phaser[A], phaser2: Phaser[B])(using Quotes, ToExpr[A], ToExpr[B]): Phaser[(A, B)] = phaser1 match
50 | case Static(eval1: Eval[A]) => phaser2 match
51 | case Static(eval2: Eval[B]) => Static((eval1, eval2))
52 | case Phased(expr2: Expr[B]) => Phased('{(${Expr(eval1)}, $expr2)})
53 | case Phased(expr1: Expr[A]) => phaser2 match
54 | case Static(eval2: Eval[B]) => Phased('{($expr1, ${Expr(eval2)})})
55 | case Phased(expr2: Expr[B]) => Phased('{($expr1, $expr2)})
56 |
57 | /** If any of the (varargs) [[Phaser]]s are [[Phased]], defer them all */
58 | def alignAll[A](phasers: Phaser[A]*)(using Quotes, ToExpr[A]): Seq[Phaser[A]] = alignSeq(phasers)
59 | /** If any of the (seq of) [[Phaser]]s are [[Phased]], defer them all */
60 | def alignSeq[A](phasers: Seq[Phaser[A]])(using Quotes, ToExpr[A]): Seq[Phaser[A]] =
61 | if phasers.exists(!_.isStatic) then phasers.map(_.defer) else phasers
62 |
63 | /** If we cannot evaluate the given [[Expr]], error saying we expected a [[Static]] value for the given name. */
64 | def require[A](expr: Expr[A], name: String)(using Quotes, FromExpr[A]): Eval[A] = expr.value.getOrElse(error(s"Expected a static value for $name.", expr))
65 | }
66 |
67 | /** These have to be extensions because of variance issues */
68 | extension [A](p: Phaser[A])(using Quotes) {
69 |
70 | /** Convert a [[Static]] into a [[Phased]] using a [[ToExpr]], or just leave a [[Phased]] be. */
71 | def defer(using ToExpr[A]): Phased[A] = Phased(p.expr)
72 |
73 | /** Derive an [[Expr]] for this [[Phaser]], possibly using a [[ToExpr]], this is generally how you turn
74 | * your [[Phaser]]s back into mere expressions when it's time to let them back out into the wild outside this macro expansion context. */
75 | def expr(using ToExpr[A]): Expr[A] = p match
76 | case Static(eval: Eval[A]) => Expr(eval)
77 | case Phased(expr: Expr[A]) => expr
78 |
79 | /** Attempt to make this [[Phaser]] become a [[Static]] by reading it with a [[FromExpr]], but if not that's fine. */
80 | def maybeEval(using FromExpr[A]): Phaser[A] = p match
81 | case s @ Static(eval: Eval[A]) => s
82 | case p @ Phased(expr: Expr[A]) => expr.value.fold(p)(a => Static(a))
83 |
84 | /** Attempt to make this [[Phaser]] become a [[Static]] by reading it with a [[FromExpr]], and error if we can't. */
85 | def mustEval(name: String)(using FromExpr[A]): Static[A] = p match
86 | case s @ Static(eval: Eval[A]) => s
87 | case p @ Phased(expr: Expr[A]) => Static(expr.value.getOrElse(error(s"Expected a static value for $name.", expr)))
88 |
89 | def show: String = p match
90 | case Static(eval: Eval[A]) => s"Static(${eval.toString})"
91 | case Phased(expr: Expr[A]) => s"Phased(${expr.show})"
92 |
93 | }
94 |
95 | /**
96 | * Phunctions carry a static (non-Expr) and a dynamic (Expr) version of their underlying function.
97 | * Ideally this would be doable automatically, and it is theoretically possible based on my previous attempts,
98 | * but it's really finnicky and fragile. Probably would need some love on the compiler side of things, or at least
99 | * the TASTy reflection library.
100 | */
101 | trait Phunction1[-T, +R] {
102 | def apply(phased: Phaser[T])(using Quotes): Phaser[R] = phased match
103 | case Static(eval: Eval[T]) => Static(applyThis(eval))
104 | case Phased(expr: Expr[T]) => Phased(applyNext(expr))
105 | def applyThis(eval: Eval[T])(using Quotes): Eval[R]
106 | def applyNext(expr: Expr[T])(using Quotes): Expr[R]
107 | }
108 |
109 | /** [[Phunction1]] companion, no need to be hidden */
110 | object Phunction1 {
111 | def apply[T, R](evalFn: Function1[T, R], exprFn: Function1[Expr[T], Expr[R]]): Phunction1[T, R] = new Phunction1[T, R] {
112 | override def applyThis(eval: Eval[T])(using Quotes): Eval[R] = evalFn(eval)
113 | override def applyNext(expr: Expr[T])(using Quotes): Expr[R] = exprFn(expr)
114 | }
115 | }
116 |
117 | /** Like [[Phunction1]], but more! */
118 | trait Phunction2[-T1, -T2, +R] {
119 | def apply[TT1 <: T1, TT2 <: T2, RR >: R](phaser1: Phaser[TT1], phaser2: Phaser[TT2])(using Quotes, ToExpr[TT1], ToExpr[TT2]): Phaser[RR] = phaser1 match
120 | case Static(eval1: Eval[TT1]) => phaser2 match
121 | case Static(eval2: Eval[TT2]) => Static(applyThis(eval1, eval2))
122 | case Phased(expr2: Expr[TT2]) => Phased(applyNext(Expr(eval1), expr2))
123 | case Phased(expr1: Expr[TT1]) => phaser2 match
124 | case Static(eval2: Eval[TT2]) => Phased(applyNext(expr1, Expr(eval2)))
125 | case Phased(expr2: Expr[TT2]) => Phased(applyNext(expr1, expr2))
126 | def applyThis(eval1: Eval[T1], eval2: Eval[T2])(using Quotes): Eval[R]
127 | def applyNext(expr1: Expr[T1], expr2: Expr[T2])(using Quotes): Expr[R]
128 | }
129 |
130 | /** [[Phunction2]] companion, no need to be hidden */
131 | object Phunction2 {
132 | def apply[T1, T2, R](evalFn: Function2[T1, T2, R], exprFn: Function2[Expr[T1], Expr[T2], Expr[R]]): Phunction2[T1, T2, R] = new Phunction2[T1, T2, R] {
133 | override def applyThis(eval1: Eval[T1], eval2: Eval[T2])(using Quotes): Eval[R] = evalFn(eval1, eval2)
134 | override def applyNext(expr1: Expr[T1], expr2: Expr[T2])(using Quotes): Expr[R] = exprFn(expr1, expr2)
135 | }
136 | }
137 |
138 | // Various occasionally-useful Phunctions, they're too boilerplatey to just declare whenever you need them,
139 | // you pretty much have to stick them in their own corner or they get pretty yucky-looking
140 |
141 | /** Phunction to concatenate two strings */
142 | object PhunctionStrCat extends Phunction2[String, String, String] {
143 | override def applyThis(eval1: Eval[String], eval2: Eval[String])(using Quotes): Eval[String] = eval1 + eval2
144 | override def applyNext(expr1: Expr[String], expr2: Expr[String])(using Quotes): Expr[String] = '{$expr1 + $expr2}
145 | }
146 |
147 | /** Phunction to ampersand-escape a string */
148 | object PhunctionEscapeStr extends Phunction1[String, String] {
149 | override def applyThis(eval: Eval[String])(using Quotes): Eval[String] = escapeString(eval)
150 | override def applyNext(expr: Expr[String])(using Quotes): Expr[String] = '{ escapeString($expr) }
151 | }
152 |
153 | /** Phunction to call `toString` on anything */
154 | object PhunctionToString extends Phunction1[Any, String] {
155 | override def applyThis(eval: Eval[Any])(using Quotes): Eval[String] = eval.toString
156 | override def applyNext(expr: Expr[Any])(using Quotes): Expr[String] = '{ $expr.toString }
157 | }
--------------------------------------------------------------------------------
/src/test/scala/ExampleTests.scala:
--------------------------------------------------------------------------------
1 | import dottytags.*
2 | import dottytags.predefs.all.*
3 | import dottytags.syntax.given
4 | import dottytags.units.*
5 | import scala.language.implicitConversions
6 | import minitest.*
7 |
8 | /*
9 | * Most of these were adapted from Scalatags' test suite so as to correctly test for compatibility.
10 | * (see LICENSE for copyright notice)
11 | */
12 |
13 | object ExampleTests extends SimpleTestSuite {
14 |
15 | import dottytags.syntax.given
16 |
17 | test("Import Example") { assertXMLEquiv(
18 | div(
19 | p(color:="red", fontSize:=64.pt, "Big Red Text"),
20 | img(href:="www.imgur.com/picture.jpg")
21 | ),
22 | """
23 | |
369 |
370 |
371 | """
372 | )}
373 |
374 | }
--------------------------------------------------------------------------------
/src/main/scala/dottytags/predefs/Svg.scala:
--------------------------------------------------------------------------------
1 | package dottytags.predefs
2 |
3 | import scala.annotation.targetName
4 |
5 | import dottytags.*
6 |
7 | /*
8 | * Documentation marked "MDN" is thanks to Mozilla Contributors
9 | * at https://developer.mozilla.org/en-US/docs/Web/API and available
10 | * under the Creative Commons Attribution-ShareAlike v2.5 or later.
11 | * http://creativecommons.org/licenses/by-sa/2.5/
12 | *
13 | * Everything else is under the MIT License, see here:
14 | * https://github.com/CiaraOBrien/dottytags/blob/main/LICENSE
15 | *
16 | * This whole file is, of course, adapted from Scalatags (see LICENSE for copyright notice):
17 | * https://github.com/lihaoyi/scalatags/blob/master/scalatags/src/scalatags/generic/SvgTags.scala
18 | * https://github.com/lihaoyi/scalatags/blob/master/scalatags/src/scalatags/generic/SvgAttrs.scala
19 | */
20 |
21 | object svg {
22 |
23 | inline def altGlyph = tag("altGlyph")
24 | inline def altGlyphDef = tag("altGlyphDef")
25 | inline def altGlyphItem = tag("altGlyphItem")
26 | inline def animate = tag("animate")
27 | inline def animateMotion = tag("animateMotion")
28 | inline def animateTransform = tag("animateTransform")
29 | inline def circle = tag("circle")
30 | inline def clipPath = tag("clipPath")
31 | inline def `color-profile` = tag("color-profile")
32 | //inline def cursorTag = tag("cursor") Deprecated and unsupported in many browsers, use cursor attribute instead
33 | inline def defs = tag("defs")
34 | inline def desc = tag("desc")
35 | inline def ellipse = tag("ellipse")
36 | inline def feBlend = tag("feBlend")
37 | inline def feColorMatrix = tag("feColorMatrix")
38 | inline def feComponentTransfer = tag("feComponentTransfer")
39 | inline def feComposite = tag("feComposite")
40 | inline def feConvolveMatrix = tag("feConvolveMatrix")
41 | inline def feDiffuseLighting = tag("feDiffuseLighting")
42 | inline def feDisplacementMap = tag("feDisplacementMap")
43 | inline def feDistantLighting = tag("feDistantLighting")
44 | inline def feFlood = tag("feFlood")
45 | inline def feFuncA = tag("feFuncA")
46 | inline def feFuncB = tag("feFuncB")
47 | inline def feFuncG = tag("feFuncG")
48 | inline def feFuncR = tag("feFuncR")
49 | inline def feGaussianBlur = tag("feGaussianBlur")
50 | inline def feImage = tag("feImage")
51 | inline def feMerge = tag("feMerge")
52 | inline def feMergeNode = tag("feMergeNode")
53 | inline def feMorphology = tag("feMorphology")
54 | inline def feOffset = tag("feOffset")
55 | inline def fePointLight = tag("fePointLight")
56 | inline def feSpecularLighting = tag("feSpecularLighting")
57 | inline def feSpotlight = tag("feSpotlight")
58 | inline def feTile = tag("feTile")
59 | inline def feTurbulance = tag("feTurbulance")
60 | inline def filter = tag("filter")
61 | inline def font = tag("font")
62 | inline def `font-face` = tag("font-face")
63 | inline def `font-face-format` = tag("font-face-format")
64 | inline def `font-face-name` = tag("font-face-name")
65 | inline def `font-face-src` = tag("font-face-src")
66 | inline def `font-face-uri` = tag("font-face-uri")
67 | inline def foreignObject = tag("foreignObject")
68 | inline def g = tag("g")
69 | inline def glyph = tag("glyph")
70 | inline def glyphRef = tag("glyphRef")
71 | inline def hkern = tag("hkern")
72 | inline def image = tag("image")
73 | inline def line = tag("line")
74 | inline def linearGradient = tag("linearGradient")
75 | inline def marker = tag("marker")
76 | inline def mask = tag("mask")
77 | inline def metadata = tag("metadata")
78 | //inline def missingGlyph = tag("missing-glyph") Not supported in literally any browser apparently
79 | inline def mpath = tag("mpath")
80 | inline def path = tag("path")
81 | inline def pattern = tag("pattern")
82 | inline def polygon = tag("polygon")
83 | inline def polyline = tag("polyline")
84 | inline def radialGradient = tag("radialGradient")
85 | inline def rect = tag("rect")
86 | inline def set = tag("set")
87 | inline def stop = tag("stop")
88 | inline def svg = tag("svg")
89 | inline def switch = tag("switch")
90 | inline def symbol = tag("symbol")
91 | inline def text = tag("text")
92 | inline def textPath = tag("textPath")
93 | inline def tref = tag("tref")
94 | inline def tspan = tag("tspan")
95 | inline def use = tag("use")
96 | inline def view = tag("view")
97 | inline def vkern = tag("vkern")
98 |
99 | /**
100 | * This attribute defines the distance from the origin to the top of accent characters,
101 | * measured by a distance within the font coordinate system.
102 | * If the attribute is not specified, the effect is as if the attribute
103 | * were set to the value of the ascent attribute.
104 | *
105 | * Value
106 | *
107 | * MDN
108 | */
109 | inline def accentHeight = attr("accent-height")
110 |
111 | /**
112 | * This attribute controls whether or not the animation is cumulative.
113 | * It is frequently useful for repeated animations to build upon the previous results,
114 | * accumulating with each iteration. This attribute said to the animation if the value is added to
115 | * the previous animated attribute's value on each iteration.
116 | *
117 | * Value none | sum
118 | *
119 | * MDN
120 | */
121 | inline def accumulate = attr("accumulate")
122 |
123 | /**
124 | * This attribute controls whether or not the animation is additive.
125 | * It is frequently useful to define animation as an offset or delta
126 | * to an attribute's value, rather than as absolute values. This
127 | * attribute said to the animation if their values are added to the
128 | * original animated attribute's value.
129 | *
130 | * Value replace | sum
131 | *
132 | * MDN
133 | */
134 | inline def additive = attr("additive")
135 |
136 | /**
137 | * The alignment-baseline attribute specifies how an object is aligned
138 | * with respect to its parent. This property specifies which baseline
139 | * of this element is to be aligned with the corresponding baseline of
140 | * the parent. For example, this allows alphabetic baselines in Roman
141 | * text to stay aligned across font size changes. It defaults to the
142 | * baseline with the same name as the computed value of the
143 | * alignment-baseline property. As a presentation attribute, it also
144 | * can be used as a property directly inside a CSS stylesheet, see css
145 | * alignment-baseline for further information.
146 | *
147 | * Value: auto | baseline | before-edge | text-before-edge | middle | central | after-edge |
148 | * text-after-edge | ideographic | alphabetic | hanging | mathematical | inherit
149 | *
150 | * MDN
151 | */
152 | inline def alignmentBaseline = attr("alignment-baseline")
153 |
154 |
155 | /**
156 | * This attribute defines the maximum unaccented depth of the font
157 | * within the font coordinate system. If the attribute is not specified,
158 | * the effect is as if the attribute were set to the vert-origin-y value
159 | * for the corresponding font.
160 | *
161 | * Value
162 | *
163 | * MDN
164 | */
165 | inline def ascent = attr("ascent")
166 |
167 |
168 | /**
169 | * This attribute indicates the name of the attribute in the parent element
170 | * that is going to be changed during an animation.
171 | *
172 | * Value
173 | *
174 | * MDN
175 | */
176 | inline def attributeName = attr("attributeName")
177 |
178 |
179 | /**
180 | * This attribute specifies the namespace in which the target attribute
181 | * and its associated values are defined.
182 | *
183 | * Value CSS | XML | auto
184 | *
185 | * MDN
186 | */
187 | inline def attributeType = attr("attributeType")
188 |
189 |
190 | /**
191 | * The azimuth attribute represent the direction angle for the light
192 | * source on the XY plane (clockwise), in degrees from the x axis.
193 | * If the attribute is not specified, then the effect is as if a
194 | * value of 0 were specified.
195 | *
196 | * Value
197 | *
198 | * MDN
199 | */
200 | inline def azimuth = attr("azimuth")
201 |
202 |
203 | /**
204 | * The baseFrequency attribute represent The base frequencies parameter
205 | * for the noise function of the primitive. If two s
206 | * are provided, the first number represents a base frequency in the X
207 | * direction and the second value represents a base frequency in the Y direction.
208 | * If one number is provided, then that value is used for both X and Y.
209 | * Negative values are forbidden.
210 | * If the attribute is not specified, then the effect is as if a value
211 | * of 0 were specified.
212 | *
213 | * Value
214 | *
215 | * MDN
216 | */
217 | inline def baseFrequency = attr("baseFrequency")
218 |
219 |
220 | /**
221 | * The baseline-shift attribute allows repositioning of the dominant-baseline
222 | * relative to the dominant-baseline of the parent text content element.
223 | * The shifted object might be a sub- or superscript.
224 | * As a presentation attribute, it also can be used as a property directly
225 | * inside a CSS stylesheet, see css baseline-shift for further information.
226 | *
227 | * Value auto | baseline | sup | sub | | | inherit
228 | *
229 | * MDN
230 | */
231 | inline def baselineShift = attr("baseline-shift")
232 |
233 |
234 | /**
235 | * This attribute defines when an animation should begin.
236 | * The attribute value is a semicolon separated list of values. The interpretation
237 | * of a list of start times is detailed in the SMIL specification in "Evaluation
238 | * of begin and end time lists". Each individual value can be one of the following:
239 | * , , , , ,
240 | * or the keyword indefinite.
241 | *
242 | * Value
243 | *
244 | * MDN
245 | */
246 | inline def begin = attr("begin")
247 |
248 |
249 | /**
250 | * The bias attribute shifts the range of the filter. After applying the kernelMatrix
251 | * of the element to the input image to yield a number and applied
252 | * the divisor attribute, the bias attribute is added to each component. This allows
253 | * representation of values that would otherwise be clamped to 0 or 1.
254 | * If bias is not specified, then the effect is as if a value of 0 were specified.
255 | *
256 | * Value
257 | *
258 | * MDN
259 | */
260 | inline def bias = attr("bias")
261 |
262 |
263 | /**
264 | * This attribute specifies the interpolation mode for the animation. The default
265 | * mode is linear, however if the attribute does not support linear interpolation
266 | * (e.g. for strings), the calcMode attribute is ignored and discrete interpolation is used.
267 | *
268 | * Value discrete | linear | paced | spline
269 | *
270 | * MDN
271 | */
272 | inline def calcMode = attr("calcMode")
273 |
274 |
275 | /**
276 | * Assigns a class name or set of class names to an element. You may assign the same
277 | * class name or names to any number of elements. If you specify multiple class names,
278 | * they must be separated by whitespace characters.
279 | * The class name of an element has two key roles:
280 | * -As a style sheet selector, for use when an author wants to assign style
281 | * information to a set of elements.
282 | * -For general usage by the browser.
283 | * The class can be used to style SVG content using CSS.
284 | *
285 | * Value
286 | *
287 | * MDN
288 | */
289 | inline def `class` = attr("class")
290 |
291 |
292 | /**
293 | * The clip attribute has the same parameter values as defined for the css clip property.
294 | * Unitless values, which indicate current user coordinates, are permitted on the coordinate
295 | * values on the . The value of auto defines a clipping path along the bounds of
296 | * the viewport created by the given element.
297 | * As a presentation attribute, it also can be used as a property directly inside a
298 | * CSS stylesheet, see css clip for further information.
299 | *
300 | * Value auto | | inherit
301 | *
302 | * MDN
303 | */
304 | inline def clip = attr("clip")
305 |
306 |
307 | /**
308 | * The clip-path attribute bind the element is applied to with a given element
309 | * As a presentation attribute, it also can be used as a property directly inside a CSS stylesheet
310 | *
311 | * Value | none | inherit
312 | *
313 | * MDN
314 | */
315 | inline def clipPathBind = attr("clip-path")
316 |
317 | /**
318 | * The clipPathUnits attribute defines the coordinate system for the contents
319 | * of the element. the clipPathUnits attribute is not specified,
320 | * then the effect is as if a value of userSpaceOnUse were specified.
321 | * Note that values defined as a percentage inside the content of the
322 | * are not affected by this attribute. It means that even if you set the value of
323 | * maskContentUnits to objectBoundingBox, percentage values will be calculated as
324 | * if the value of the attribute were userSpaceOnUse.
325 | *
326 | * Value userSpaceOnUse | objectBoundingBox
327 | *
328 | * MDN
329 | */
330 | inline def clipPathUnits = attr("clipPathUnits")
331 |
332 | /**
333 | * The clip-rule attribute only applies to graphics elements that are contained within a
334 | * element. The clip-rule attribute basically works as the fill-rule attribute,
335 | * except that it applies to definitions.
336 | *
337 | * Value nonezero | evenodd | inherit
338 | *
339 | * MDN
340 | */
341 | inline def clipRule = attr("clip-rule")
342 |
343 | /**
344 | * The color attribute is used to provide a potential indirect value (currentColor)
345 | * for the fill, stroke, stop-color, flood-color and lighting-color attributes.
346 | * As a presentation attribute, it also can be used as a property directly inside a CSS
347 | * stylesheet, see css color for further information.
348 | *
349 | * Value | inherit
350 | *
351 | * MDN
352 | */
353 | inline def color = attr("color")
354 |
355 |
356 | /**
357 | * The color-interpolation attribute specifies the color space for gradient interpolations,
358 | * color animations and alpha compositing.When a child element is blended into a background,
359 | * the value of the color-interpolation attribute on the child determines the type of
360 | * blending, not the value of the color-interpolation on the parent. For gradients which
361 | * make use of the xlink:href attribute to reference another gradient, the gradient uses
362 | * the color-interpolation attribute value from the gradient element which is directly
363 | * referenced by the fill or stroke attribute. When animating colors, color interpolation
364 | * is performed according to the value of the color-interpolation attribute on the element
365 | * being animated.
366 | * As a presentation attribute, it also can be used as a property directly inside a CSS
367 | * stylesheet, see css color-interpolation for further information
368 | *
369 | * Value auto | sRGB | linearRGB | inherit
370 | *
371 | * MDN
372 | */
373 | inline def colorInterpolation = attr("color-interpolation")
374 |
375 |
376 | /**
377 | * The color-interpolation-filters attribute specifies the color space for imaging operations
378 | * performed via filter effects. Note that color-interpolation-filters has a different
379 | * initial value than color-interpolation. color-interpolation-filters has an initial
380 | * value of linearRGB, whereas color-interpolation has an initial value of sRGB. Thus,
381 | * in the default case, filter effects operations occur in the linearRGB color space,
382 | * whereas all other color interpolations occur by default in the sRGB color space.
383 | * As a presentation attribute, it also can be used as a property directly inside a
384 | * CSS stylesheet, see css color-interpolation-filters for further information
385 | *
386 | * Value auto | sRGB | linearRGB | inherit
387 | *
388 | * MDN
389 | */
390 | inline def colorInterpolationFilters = attr("color-interpolation-filters")
391 |
392 |
393 | /**
394 | * The color-profile attribute is used to define which color profile a raster image
395 | * included through the element should use. As a presentation attribute, it
396 | * also can be used as a property directly inside a CSS stylesheet, see css color-profile
397 | * for further information.
398 | *
399 | * Value auto | sRGB | | | inherit
400 | *
401 | * MDN
402 | */
403 | inline def colorProfile = attr("color-profile")
404 |
405 |
406 | /**
407 | * The color-rendering attribute provides a hint to the SVG user agent about how to
408 | * optimize its color interpolation and compositing operations. color-rendering
409 | * takes precedence over color-interpolation-filters. For example, assume color-rendering:
410 | * optimizeSpeed and color-interpolation-filters: linearRGB. In this case, the SVG user
411 | * agent should perform color operations in a way that optimizes performance, which might
412 | * mean sacrificing the color interpolation precision as specified by
413 | * color-interpolation-filters: linearRGB.
414 | * As a presentation attribute, it also can be used as a property directly inside
415 | * a CSS stylesheet, see css color-rendering for further information
416 | *
417 | * Value auto | optimizeSpeed | optimizeQuality | inherit
418 | *
419 | * MDN
420 | */
421 | inline def colorRendering = attr("color-rendering")
422 |
423 |
424 | /**
425 | * The contentScriptType attribute on the