├── 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 | |
30 | |

31 | |
32 | | 33 | | 34 | """.stripMargin 35 | ) 36 | } 37 | 38 | test("CSS Style Chaining") { assertXMLEquiv ( 39 | div(float.left, color:="red"), 40 | """
""" 41 | )} 42 | 43 | test("Attribute Chaining") { assertXMLEquiv ( 44 | div(`class`:="thing lol", id:="cow"), 45 | """
""" 46 | )} 47 | 48 | test("Mixing Attributes, Styles, and Children") { assertXMLEquiv ( 49 | div(id:="cow", float.left, p("i am a cow")), 50 | """

i am a cow

""" 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 | """

i am a cow

""" 57 | )} 58 | 59 | test("Style-Attribute Overwrites Previous Styles") { assertXMLEquiv ( 60 | div(cls:="my-class", float.left, style:="background-color: red;", p("i am a cow")), 61 | """

i am a cow

""" 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 | """
lol1

Hello

123hello
""" 77 | ) 78 | } 79 | 80 | // This is a 'hole' in the syntax that I'm not even interested in plugging, why would you want to do this? 81 | /*test("Tag apply chaining") { assertXMLEquiv ( 82 | a(tabindex := 1, onclick := "lol")(href := "boo", alt := "g"), 83 | """""" 84 | )*/ 85 | 86 | test("Automatic Pixel Suffixes") { 87 | assertXMLEquiv ( 88 | div(width:=100, zIndex:=100, height:="100px"), 89 | """
""" 90 | ) 91 | } 92 | 93 | test("Raw Attributes") { assertXMLEquiv ( 94 | button( 95 | attr("[class.active]", raw = true) := "isActive", 96 | attr("(click)", raw = true) := "myEvent()" 97 | ), 98 | """""" 99 | )} 100 | 101 | test("Invalid names fail to compile") { 102 | assertDoesNotCompile("""div(attr("[(ngModel)]") := "myModel"""") 103 | assertDoesNotCompile("""val s: String = "hi"; div(attr(s) := "myModel"""") 104 | } 105 | 106 | test("Repeating Attributes Causes them to be Combined") { 107 | assertXMLEquiv( 108 | input(cls := "a", cls := "b").toString, 109 | """""" 110 | ) 111 | /*assertXMLEquiv( 112 | input(cls := "a")(cls := "b").render, 113 | """""" 114 | )*/ 115 | } 116 | 117 | test("Big Final Test") { assertXMLEquiv ( 118 | html( 119 | head( 120 | script("some script") 121 | ), 122 | body( 123 | h1(backgroundColor:="blue", color:="red", "This is my title"), 124 | div(backgroundColor:="blue", color:="red", 125 | p(cls := "contentpara first", 126 | "This is my first paragraph" 127 | ), 128 | a(opacity:=0.9, 129 | p(cls := "contentpara", "Goooogle") 130 | ) 131 | ) 132 | ) 133 | ).toString, 134 | """ 135 | | 136 | | 137 | | 138 | | 139 | | 140 | |

This is my title

141 | |
142 | |

This is my first paragraph

143 | | 144 | |

Goooogle

145 | |
146 | |
147 | | 148 | | 149 | """.stripMargin)} 150 | 151 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DottyTags 2 | [![dottytags Scala version support](https://index.scala-lang.org/ciaraobrien/dottytags/dottytags/latest-by-scala-version.svg)](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>" 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("")).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 | |
24 | |

Big Red Text

25 | | 26 | |
27 | """.stripMargin 28 | ) 29 | } 30 | 31 | test("Splash Example") { assertXMLEquiv ( 32 | html( 33 | head( 34 | script(src:="..."), 35 | script( 36 | "alert('Hello World')" 37 | ) 38 | ), 39 | body( 40 | div( 41 | h1(id:="title", "This is a title"), 42 | p("This is a big paragraph of text") 43 | ) 44 | ) 45 | ) 46 | , 47 | """ 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 |

This is a title

56 |

This is a big paragraph of text

57 |
58 | 59 | 60 | """ 61 | )} 62 | 63 | test("Hello World") { assertXMLEquiv ( 64 | html( 65 | head( 66 | script("some script") 67 | ), 68 | body( 69 | h1("This is my title"), 70 | div( 71 | p("This is my first paragraph"), 72 | p("This is my second paragraph") 73 | ) 74 | ) 75 | ) 76 | , 77 | """ 78 | 79 | 80 | 81 | 82 | 83 |

This is my title

84 |
85 |

This is my first paragraph

86 |

This is my second paragraph

87 |
88 | 89 | 90 | """ 91 | )} 92 | 93 | test("Variables") { assertXMLEquiv ( 94 | { 95 | val title = "title" 96 | val numVisitors = 1023 97 | 98 | html( 99 | head( 100 | script("some script") 101 | ), 102 | body( 103 | h1("This is my ", title), 104 | div( 105 | p("This is my first paragraph"), 106 | p("you are the ", numVisitors.toString, "th visitor!") 107 | ) 108 | ) 109 | ) 110 | } 111 | , 112 | """ 113 | 114 | 115 | 116 | 117 | 118 |

This is my title

119 |
120 |

This is my first paragraph

121 |

you are the 1023th visitor!

122 |
123 | 124 | 125 | """ 126 | )} 127 | 128 | test("Control Flow") { assertXMLEquiv ( 129 | { 130 | val numVisitors = 1023 131 | val posts = Seq( 132 | ("alice", "i like pie"), 133 | ("bob", "pie is evil i hate you"), 134 | ("charlie", "i like pie and pie is evil, i hat myself") 135 | ) 136 | 137 | html( 138 | head( 139 | script("some script") 140 | ), 141 | body( 142 | h1("This is my title"), 143 | div("posts"), 144 | bind(for ((name, text) <- posts) yield div( 145 | h2("Post by ", name), 146 | p(text) 147 | )), 148 | if numVisitors > 100 then p("No more posts!") 149 | else p("Please post below...") 150 | ) 151 | ) 152 | } 153 | , 154 | """ 155 | 156 | 157 | 158 | 159 | 160 |

This is my title

161 |
posts
162 |
163 |

Post by alice

164 |

i like pie

165 |
166 |
167 |

Post by bob

168 |

pie is evil i hate you

169 |
170 |
171 |

Post by charlie

172 |

i like pie and pie is evil, i hat myself

173 |
174 |

No more posts!

175 | 176 | 177 | """ 178 | )} 179 | 180 | test("Functions") { assertXMLEquiv ( { 181 | inline def imgBox(inline source: String, inline text: String) = div( 182 | img(src:=source), 183 | div( 184 | p(text) 185 | ) 186 | ) 187 | 188 | html( 189 | head( 190 | script("some script") 191 | ), 192 | body( 193 | h1("This is my title"), 194 | imgBox("www.mysite.com/imageOne.png", "This is the first image displayed on the site"), 195 | div(`class`:="content", 196 | p("blah blah blah i am text"), 197 | imgBox("www.mysite.com/imageTwo.png", "This image is very interesting") 198 | ) 199 | ) 200 | ) 201 | } 202 | , 203 | """ 204 | 205 | 206 | 207 | 208 | 209 |

This is my title

210 |
211 | 212 |
213 |

This is the first image displayed on the site

214 |
215 |
216 |
217 |

blah blah blah i am text

218 |
219 | 220 |
221 |

This image is very interesting

222 |
223 |
224 |
225 | 226 | 227 | """ 228 | )} 229 | 230 | test("Attributes") { assertXMLEquiv ( 231 | html( 232 | head( 233 | script("some script") 234 | ), 235 | body( 236 | h1("This is my title"), 237 | div( 238 | p(attr("onclick") := "... do some js", 239 | "This is my first paragraph" 240 | ), 241 | a(attr("href") := "www.google.com", 242 | p("Goooogle") 243 | ), 244 | p(attr("hidden").empty, 245 | "I am hidden" 246 | ) 247 | ) 248 | ) 249 | ) 250 | , 251 | """ 252 | 253 | 254 | 255 | 256 | 257 |

This is my title

258 |
259 |

This is my first paragraph

260 | 261 |

Goooogle

262 |
263 | 264 |
265 | 266 | 267 | """ 268 | )} 269 | 270 | test("Custom Classes and CSS") { assertXMLEquiv ( 271 | html( 272 | head( 273 | script("some script") 274 | ), 275 | body( 276 | h1(style:="background-color: blue; color: red;", "This is my title"), 277 | div(style:="background-color: blue; color: red;", 278 | p(`class`:="contentpara first", 279 | "This is my first paragraph" 280 | ), 281 | a(style:="opacity: 0.9;", 282 | p(cls := "contentpara", "Goooogle") 283 | ) 284 | ) 285 | ) 286 | ) 287 | , 288 | """ 289 | 290 | 291 | 292 | 293 | 294 |

This is my title

295 |
296 |

This is my first paragraph

297 | 298 |

Goooogle

299 |
300 |
301 | 302 | 303 | """ 304 | )} 305 | 306 | test("Non-String Attribute And Styles") { assertXMLEquiv ( 307 | div( 308 | p(float.left, 309 | "This is my first paragraph" 310 | ), 311 | 312 | a(tabindex:=10, 313 | p("Goooogle") 314 | ), 315 | 316 | input(attr("disabled"):=true) 317 | ) 318 | , 319 | """ 320 |
321 |

This is my first paragraph

322 | 323 |

Goooogle

324 |
325 | 326 |
327 | """ 328 | )} 329 | 330 | test("Boolean Attributes") { assertXMLEquiv ( 331 | div(input(readonly)), """
""" 332 | ) 333 | } 334 | 335 | test("Layouts") { assertXMLEquiv ( { 336 | def page(scripts: Seq[Tag], content: Seq[Tag]): String = 337 | html( 338 | head(scripts), 339 | body( 340 | h1("This is my title"), 341 | div(cls := "content", content) 342 | ) 343 | ).toString 344 | 345 | page( 346 | Seq( 347 | script("some script") 348 | ), 349 | Seq( 350 | p("This is the first ", b("image"), " displayed on the ", a("site")), 351 | img(src:="www.myImage.com/image.jpg"), 352 | p("blah blah blah i am text") 353 | ) 354 | ) 355 | } 356 | , 357 | """ 358 | 359 | 360 | 361 | 362 | 363 |

This is my title

364 |
365 |

This is the first image displayed on the site

366 | 367 |

blah blah blah i am text

368 |
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 element specifies the default scripting 426 | * language for the given document fragment. 427 | * This attribute sets the default scripting language used to process the value strings 428 | * in event attributes. This language must be used for all instances of script that do not 429 | * specify their own scripting language. The value content-type specifies a media type, 430 | * per MIME Part Two: Media Types [RFC2046]. The default value is application/ecmascript 431 | * 432 | * Value 433 | * 434 | * MDN 435 | */ 436 | inline def contentScriptType = attr("contentScriptType") 437 | 438 | 439 | /** 440 | * This attribute specifies the style sheet language for the given document fragment. 441 | * The contentStyleType is specified on the element. By default, if it's not defined, 442 | * the value is text/css 443 | * 444 | * Value 445 | * 446 | * MDN 447 | */ 448 | inline def contentStyleType = attr("contentStyleType") 449 | 450 | 451 | /** 452 | * The cursor attribute specifies the mouse cursor displayed when the mouse pointer 453 | * is over an element.This attribute behave exactly like the css cursor property except 454 | * that if the browser suport the element, it should allow to use it with the 455 | * notation. As a presentation attribute, it also can be used as a property 456 | * directly inside a CSS stylesheet, see css cursor for further information. 457 | * 458 | * Value auto | crosshair | default | pointer | move | e-resize | 459 | * ne-resize | nw-resize | n-resize | se-resize | sw-resize | s-resize | w-resize| text | 460 | * wait | help | inherit 461 | * 462 | * MDN 463 | */ 464 | inline def cursor = attr("cursor") 465 | 466 | 467 | /** 468 | * For the and the element, this attribute define the x-axis coordinate 469 | * of the center of the element. If the attribute is not specified, the effect is as if a 470 | * value of "0" were specified.For the element, this attribute define 471 | * the x-axis coordinate of the largest (i.e., outermost) circle for the radial gradient. 472 | * The gradient will be drawn such that the 100% gradient stop is mapped to the perimeter 473 | * of this largest (i.e., outermost) circle. If the attribute is not specified, the effect 474 | * is as if a value of 50% were specified 475 | * 476 | * Value 477 | * 478 | * MDN 479 | */ 480 | inline def cx = attr("cx") 481 | 482 | /** 483 | * For the and the element, this attribute define the y-axis coordinate 484 | * of the center of the element. If the attribute is not specified, the effect is as if a 485 | * value of "0" were specified.For the element, this attribute define 486 | * the x-axis coordinate of the largest (i.e., outermost) circle for the radial gradient. 487 | * The gradient will be drawn such that the 100% gradient stop is mapped to the perimeter 488 | * of this largest (i.e., outermost) circle. If the attribute is not specified, the effect 489 | * is as if a value of 50% were specified 490 | * 491 | * Value 492 | * 493 | * MDN 494 | */ 495 | inline def cy = attr("cy") 496 | 497 | 498 | /** 499 | * 500 | * 501 | * MDN 502 | */ 503 | inline def d = attr("d") 504 | 505 | 506 | /** 507 | * 508 | * 509 | * MDN 510 | */ 511 | inline def diffuseConstant = attr("diffuseConstant") 512 | 513 | 514 | /** 515 | * 516 | * 517 | * MDN 518 | */ 519 | inline def direction = attr("direction") 520 | 521 | 522 | /** 523 | * 524 | * 525 | * MDN 526 | */ 527 | inline def display = attr("display") 528 | 529 | 530 | /** 531 | * 532 | * 533 | * MDN 534 | */ 535 | inline def divisor = attr("divisor") 536 | 537 | 538 | /** 539 | * 540 | * 541 | * MDN 542 | */ 543 | inline def dominantBaseline = attr("dominant-baseline") 544 | 545 | 546 | /** 547 | * 548 | * 549 | * MDN 550 | */ 551 | inline def dur = attr("dur") 552 | 553 | 554 | /** 555 | * 556 | * 557 | * MDN 558 | */ 559 | inline def dx = attr("dx") 560 | 561 | 562 | /** 563 | * 564 | * 565 | * MDN 566 | */ 567 | inline def dy = attr("dy") 568 | 569 | 570 | /** 571 | * 572 | * 573 | * MDN 574 | */ 575 | inline def edgeMode = attr("edgeMode") 576 | 577 | 578 | /** 579 | * 580 | * 581 | * MDN 582 | */ 583 | inline def elevation = attr("elevation") 584 | 585 | 586 | /** 587 | * 588 | * 589 | * MDN 590 | */ 591 | inline def end = attr("end") 592 | 593 | 594 | /** 595 | * 596 | * 597 | * MDN 598 | */ 599 | inline def externalResourcesRequired = attr("externalResourcesRequired") 600 | 601 | 602 | /** 603 | * 604 | * 605 | * MDN 606 | */ 607 | inline def fill = attr("fill") 608 | 609 | 610 | /** 611 | * 612 | * 613 | * MDN 614 | */ 615 | inline def fillOpacity = attr("fill-opacity") 616 | 617 | 618 | /** 619 | * 620 | * 621 | * MDN 622 | */ 623 | inline def fillRule = attr("fill-rule") 624 | 625 | 626 | /** 627 | * The filter attribute specifies the filter effects defined by the `` element that shall be applied to its element. 628 | * 629 | * MDN 630 | */ 631 | inline def filterBind = attr("filter") 632 | 633 | 634 | /** 635 | * 636 | * 637 | * MDN 638 | */ 639 | inline def filterRes = attr("filterRes") 640 | 641 | 642 | /** 643 | * 644 | * 645 | * MDN 646 | */ 647 | inline def filterUnits = attr("filterUnits") 648 | 649 | 650 | /** 651 | * 652 | * 653 | * MDN 654 | */ 655 | inline def floodColor = attr("flood-color") 656 | 657 | 658 | /** 659 | * 660 | * 661 | * MDN 662 | */ 663 | inline def floodOpacity = attr("flood-opacity") 664 | 665 | 666 | /** 667 | * 668 | * 669 | * MDN 670 | */ 671 | inline def fontFamily = attr("font-family") 672 | 673 | 674 | /** 675 | * 676 | * 677 | * MDN 678 | */ 679 | inline def fontSize = attr("font-size") 680 | 681 | 682 | /** 683 | * 684 | * 685 | * MDN 686 | */ 687 | inline def fontSizeAdjust = attr("font-size-adjust") 688 | 689 | 690 | /** 691 | * 692 | * 693 | * MDN 694 | */ 695 | inline def fontStretch = attr("font-stretch") 696 | 697 | 698 | /** 699 | * 700 | * 701 | * MDN 702 | */ 703 | inline def fontVariant = attr("font-variant") 704 | 705 | 706 | /** 707 | * 708 | * 709 | * MDN 710 | */ 711 | inline def fontWeight = attr("font-weight") 712 | 713 | 714 | /** 715 | * 716 | * 717 | * MDN 718 | */ 719 | inline def from = attr("from") 720 | 721 | 722 | /** 723 | * 724 | * 725 | * MDN 726 | */ 727 | inline def fx = attr("fx") 728 | 729 | 730 | /** 731 | * 732 | * 733 | * MDN 734 | */ 735 | inline def fy = attr("fy") 736 | 737 | 738 | /** 739 | * 740 | * 741 | * MDN 742 | */ 743 | inline def gradientTransform = attr("gradientTransform") 744 | 745 | 746 | /** 747 | * 748 | * 749 | * MDN 750 | */ 751 | inline def gradientUnits = attr("gradientUnits") 752 | 753 | 754 | /** 755 | * 756 | * 757 | * MDN 758 | */ 759 | inline def height = attr("height") 760 | 761 | 762 | /** 763 | * 764 | * 765 | * MDN 766 | */ 767 | inline def imageRendering = attr("imageRendering") 768 | 769 | inline def id = attr("id") 770 | 771 | /** 772 | * 773 | * 774 | * MDN 775 | */ 776 | inline def in = attr("in") 777 | 778 | 779 | 780 | /** 781 | * 782 | * 783 | * MDN 784 | */ 785 | inline def in2 = attr("in2") 786 | 787 | 788 | 789 | /** 790 | * 791 | * 792 | * MDN 793 | */ 794 | inline def k1 = attr("k1") 795 | 796 | 797 | /** 798 | * 799 | * 800 | * MDN 801 | */ 802 | inline def k2 = attr("k2") 803 | 804 | 805 | /** 806 | * 807 | * 808 | * MDN 809 | */ 810 | inline def k3 = attr("k3") 811 | 812 | 813 | /** 814 | * 815 | * 816 | * MDN 817 | */ 818 | inline def k4 = attr("k4") 819 | 820 | 821 | 822 | /** 823 | * 824 | * 825 | * MDN 826 | */ 827 | inline def kernelMatrix = attr("kernelMatrix") 828 | 829 | 830 | 831 | /** 832 | * 833 | * 834 | * MDN 835 | */ 836 | inline def kernelUnitLength = attr("kernelUnitLength") 837 | 838 | 839 | /** 840 | * 841 | * 842 | * MDN 843 | */ 844 | inline def kerning = attr("kerning") 845 | 846 | 847 | /** 848 | * 849 | * 850 | * MDN 851 | */ 852 | inline def keySplines = attr("keySplines") 853 | 854 | 855 | 856 | /** 857 | * 858 | * 859 | * MDN 860 | */ 861 | inline def keyTimes = attr("keyTimes") 862 | 863 | 864 | 865 | 866 | /** 867 | * 868 | * 869 | * MDN 870 | */ 871 | inline def letterSpacing = attr("letter-spacing") 872 | 873 | 874 | 875 | /** 876 | * 877 | * 878 | * MDN 879 | */ 880 | inline def lightingColor = attr("lighting-color") 881 | 882 | 883 | 884 | /** 885 | * 886 | * 887 | * MDN 888 | */ 889 | inline def limitingConeAngle = attr("limitingConeAngle") 890 | 891 | 892 | 893 | /** 894 | * 895 | * 896 | * MDN 897 | */ 898 | inline def local = attr("local") 899 | 900 | 901 | 902 | /** 903 | * 904 | * 905 | * MDN 906 | */ 907 | inline def markerEnd = attr("marker-end") 908 | 909 | 910 | /** 911 | * 912 | * 913 | * MDN 914 | */ 915 | inline def markerMid = attr("marker-mid") 916 | 917 | 918 | /** 919 | * 920 | * 921 | * MDN 922 | */ 923 | inline def markerStart = attr("marker-start") 924 | 925 | 926 | /** 927 | * 928 | * 929 | * MDN 930 | */ 931 | inline def markerHeight = attr("markerHeight") 932 | 933 | 934 | /** 935 | * 936 | * 937 | * MDN 938 | */ 939 | inline def markerUnits = attr("markerUnits") 940 | 941 | 942 | /** 943 | * 944 | * 945 | * MDN 946 | */ 947 | inline def markerWidth = attr("markerWidth") 948 | 949 | 950 | /** 951 | * 952 | * 953 | * MDN 954 | */ 955 | inline def maskContentUnits = attr("maskContentUnits") 956 | 957 | 958 | /** 959 | * 960 | * 961 | * MDN 962 | */ 963 | inline def maskUnits = attr("maskUnits") 964 | 965 | 966 | /** 967 | * 968 | * 969 | * MDN 970 | */ 971 | inline def maskBind = attr("mask") 972 | 973 | /** 974 | * 975 | * 976 | * MDN 977 | */ 978 | inline def max = attr("max") 979 | 980 | 981 | 982 | /** 983 | * 984 | * 985 | * MDN 986 | */ 987 | inline def min = attr("min") 988 | 989 | 990 | /** 991 | * 992 | * 993 | * MDN 994 | */ 995 | inline def mode = attr("mode") 996 | 997 | 998 | /** 999 | * 1000 | * 1001 | * MDN 1002 | */ 1003 | inline def numOctaves = attr("numOctaves") 1004 | 1005 | 1006 | inline def offset = attr("offset") 1007 | 1008 | /** 1009 | * The ‘orient’ attribute indicates how the marker is rotated when it is placed at its position on the markable element. 1010 | * 1011 | * W3C 1012 | */ 1013 | inline def orient = attr("orient") 1014 | 1015 | /** 1016 | * 1017 | * 1018 | * MDN 1019 | */ 1020 | inline def opacity = attr("opacity") 1021 | 1022 | 1023 | 1024 | /** 1025 | * 1026 | * 1027 | * MDN 1028 | */ 1029 | inline def operator = attr("operator") 1030 | 1031 | 1032 | /** 1033 | * 1034 | * 1035 | * MDN 1036 | */ 1037 | inline def order = attr("order") 1038 | 1039 | 1040 | /** 1041 | * 1042 | * 1043 | * MDN 1044 | */ 1045 | inline def overflow = attr("overflow") 1046 | 1047 | 1048 | 1049 | /** 1050 | * 1051 | * 1052 | * MDN 1053 | */ 1054 | inline def paintOrder = attr("paint-order") 1055 | 1056 | 1057 | 1058 | /** 1059 | * 1060 | * 1061 | * MDN 1062 | */ 1063 | inline def pathLength = attr("pathLength") 1064 | 1065 | 1066 | 1067 | /** 1068 | * 1069 | * 1070 | * MDN 1071 | */ 1072 | inline def patternContentUnits = attr("patternContentUnits") 1073 | 1074 | 1075 | /** 1076 | * 1077 | * 1078 | * MDN 1079 | */ 1080 | inline def patternTransform = attr("patternTransform") 1081 | 1082 | 1083 | 1084 | /** 1085 | * 1086 | * 1087 | * MDN 1088 | */ 1089 | inline def patternUnits = attr("patternUnits") 1090 | 1091 | 1092 | 1093 | /** 1094 | * 1095 | * 1096 | * MDN 1097 | */ 1098 | inline def pointerEvents = attr("pointer-events") 1099 | 1100 | 1101 | /** 1102 | * 1103 | * 1104 | * MDN 1105 | */ 1106 | inline def points = attr("points") 1107 | 1108 | 1109 | /** 1110 | * 1111 | * 1112 | * MDN 1113 | */ 1114 | inline def pointsAtX = attr("pointsAtX") 1115 | 1116 | 1117 | /** 1118 | * 1119 | * 1120 | * MDN 1121 | */ 1122 | inline def pointsAtY = attr("pointsAtY") 1123 | 1124 | 1125 | /** 1126 | * 1127 | * 1128 | * MDN 1129 | */ 1130 | inline def pointsAtZ = attr("pointsAtZ") 1131 | 1132 | 1133 | /** 1134 | * 1135 | * 1136 | * MDN 1137 | */ 1138 | inline def preserveAlpha = attr("preserveAlpha") 1139 | 1140 | 1141 | 1142 | /** 1143 | * 1144 | * 1145 | * MDN 1146 | */ 1147 | inline def preserveAspectRatio = attr("preserveAspectRatio") 1148 | 1149 | 1150 | 1151 | /** 1152 | * 1153 | * 1154 | * MDN 1155 | */ 1156 | inline def primitiveUnits = attr("primitiveUnits") 1157 | 1158 | 1159 | /** 1160 | * 1161 | * 1162 | * MDN 1163 | */ 1164 | inline def r = attr("r") 1165 | 1166 | 1167 | 1168 | /** 1169 | * 1170 | * 1171 | * MDN 1172 | */ 1173 | inline def radius = attr("radius") 1174 | 1175 | 1176 | /** 1177 | * The ‘refX’ attribute defines the reference point of the marker which is to be placed exactly at 1178 | * the marker's position on the markable element. It is interpreted as being in the coordinate system of 1179 | * the marker contents, after application of the ‘viewBox’ and ‘preserveAspectRatio’ attributes. 1180 | * 1181 | * W3C 1182 | */ 1183 | inline def refX = attr("refX") 1184 | 1185 | /** 1186 | * The ‘refY’ attribute defines the reference point of the marker which is to be placed exactly at 1187 | * the marker's position on the markable element. It is interpreted as being in the coordinate system of 1188 | * the marker contents, after application of the ‘viewBox’ and ‘preserveAspectRatio’ attributes. 1189 | * 1190 | * W3C 1191 | */ 1192 | inline def refY = attr("refY") 1193 | 1194 | /** 1195 | * 1196 | * 1197 | * MDN 1198 | */ 1199 | inline def repeatCount = attr("repeatCount") 1200 | 1201 | 1202 | /** 1203 | * 1204 | * 1205 | * MDN 1206 | */ 1207 | inline def repeatDur = attr("repeatDur") 1208 | 1209 | 1210 | 1211 | /** 1212 | * 1213 | * 1214 | * MDN 1215 | */ 1216 | inline def requiredFeatures = attr("requiredFeatures") 1217 | 1218 | 1219 | 1220 | /** 1221 | * 1222 | * 1223 | * MDN 1224 | */ 1225 | inline def restart = attr("restart") 1226 | 1227 | 1228 | 1229 | /** 1230 | * 1231 | * 1232 | * MDN 1233 | */ 1234 | inline def result = attr("result") 1235 | 1236 | 1237 | 1238 | /** 1239 | * 1240 | * 1241 | * MDN 1242 | */ 1243 | inline def rx = attr("rx") 1244 | 1245 | 1246 | 1247 | /** 1248 | * 1249 | * 1250 | * MDN 1251 | */ 1252 | inline def ry = attr("ry") 1253 | 1254 | 1255 | 1256 | /** 1257 | * 1258 | * 1259 | * MDN 1260 | */ 1261 | inline def scale = attr("scale") 1262 | 1263 | 1264 | 1265 | /** 1266 | * 1267 | * 1268 | * MDN 1269 | */ 1270 | inline def seed = attr("seed") 1271 | 1272 | 1273 | 1274 | /** 1275 | * 1276 | * 1277 | * MDN 1278 | */ 1279 | inline def shapeRendering = attr("shape-rendering") 1280 | 1281 | 1282 | 1283 | /** 1284 | * 1285 | * 1286 | * MDN 1287 | */ 1288 | inline def specularConstant = attr("specularConstant") 1289 | 1290 | 1291 | 1292 | /** 1293 | * 1294 | * 1295 | * MDN 1296 | */ 1297 | inline def specularExponent = attr("specularExponent") 1298 | 1299 | 1300 | 1301 | /** 1302 | * 1303 | * 1304 | * MDN 1305 | */ 1306 | inline def spreadMethod = attr("spreadMethod") 1307 | 1308 | 1309 | 1310 | /** 1311 | * 1312 | * 1313 | * MDN 1314 | */ 1315 | inline def stdDeviation = attr("stdDeviation") 1316 | 1317 | 1318 | 1319 | /** 1320 | * 1321 | * 1322 | * MDN 1323 | */ 1324 | inline def stitchTiles = attr("stitchTiles") 1325 | 1326 | 1327 | 1328 | /** 1329 | * 1330 | * 1331 | * MDN 1332 | */ 1333 | inline def stopColor = attr("stop-color") 1334 | 1335 | 1336 | 1337 | /** 1338 | * 1339 | * 1340 | * MDN 1341 | */ 1342 | inline def stopOpacity = attr("stop-opacity") 1343 | 1344 | 1345 | 1346 | /** 1347 | * 1348 | * 1349 | * MDN 1350 | */ 1351 | inline def stroke = attr("stroke") 1352 | 1353 | 1354 | /** 1355 | * 1356 | * 1357 | * MDN 1358 | */ 1359 | inline def strokeDasharray= attr("stroke-dasharray") 1360 | 1361 | 1362 | /** 1363 | * 1364 | * 1365 | * MDN 1366 | */ 1367 | inline def strokeDashoffset = attr("stroke-dashoffset") 1368 | 1369 | 1370 | /** 1371 | * 1372 | * 1373 | * MDN 1374 | */ 1375 | inline def strokeLinecap = attr("stroke-linecap") 1376 | 1377 | 1378 | /** 1379 | * 1380 | * 1381 | * MDN 1382 | */ 1383 | inline def strokeLinejoin = attr("stroke-linejoin") 1384 | 1385 | 1386 | /** 1387 | * 1388 | * 1389 | * MDN 1390 | */ 1391 | inline def strokeMiterlimit = attr("stroke-miterlimit") 1392 | 1393 | 1394 | /** 1395 | * 1396 | * 1397 | * MDN 1398 | */ 1399 | inline def strokeOpacity = attr("stroke-opacity") 1400 | 1401 | 1402 | /** 1403 | * 1404 | * 1405 | * MDN 1406 | */ 1407 | inline def strokeWidth = attr("stroke-width") 1408 | 1409 | 1410 | /** 1411 | * 1412 | * 1413 | * MDN 1414 | */ 1415 | inline def style = attr("style") 1416 | 1417 | 1418 | 1419 | /** 1420 | * 1421 | * 1422 | * MDN 1423 | */ 1424 | inline def surfaceScale = attr("surfaceScale") 1425 | 1426 | 1427 | /** 1428 | * 1429 | * 1430 | * MDN 1431 | */ 1432 | inline def targetX = attr("targetX") 1433 | 1434 | 1435 | /** 1436 | * 1437 | * 1438 | * MDN 1439 | */ 1440 | inline def targetY = attr("targetY") 1441 | 1442 | 1443 | /** 1444 | * 1445 | * 1446 | * MDN 1447 | */ 1448 | inline def textAnchor = attr("text-anchor") 1449 | 1450 | 1451 | /** 1452 | * 1453 | * 1454 | * MDN 1455 | */ 1456 | inline def textDecoration = attr("text-decoration") 1457 | 1458 | 1459 | /** 1460 | * 1461 | * 1462 | * MDN 1463 | */ 1464 | inline def textRendering = attr("text-rendering") 1465 | 1466 | 1467 | /** 1468 | * 1469 | * 1470 | * MDN 1471 | */ 1472 | inline def to = attr("to") 1473 | 1474 | 1475 | /* 1476 | * 1477 | * 1478 | * MDN 1479 | */ 1480 | inline def transform = attr("transform") 1481 | 1482 | 1483 | /* 1484 | * 1485 | * 1486 | * MDN 1487 | */ 1488 | inline def `type`= attr("type") 1489 | 1490 | 1491 | /* 1492 | * 1493 | * 1494 | * MDN 1495 | */ 1496 | inline def values = attr("values") 1497 | 1498 | 1499 | /** 1500 | * 1501 | * 1502 | * MDN 1503 | */ 1504 | inline def viewBox = attr("viewBox") 1505 | 1506 | 1507 | /* 1508 | * 1509 | * 1510 | * MDN 1511 | */ 1512 | inline def visibility = attr("visibility") 1513 | 1514 | 1515 | /* 1516 | * 1517 | * 1518 | * MDN 1519 | */ 1520 | inline def width = attr("width") 1521 | 1522 | 1523 | /* 1524 | * 1525 | * 1526 | * MDN 1527 | */ 1528 | inline def wordSpacing = attr("word-spacing") 1529 | 1530 | /* 1531 | * 1532 | * 1533 | * MDN 1534 | */ 1535 | inline def writingMode = attr("writing-mode") 1536 | 1537 | 1538 | /* 1539 | * 1540 | * 1541 | * MDN 1542 | */ 1543 | inline def x = attr("x") 1544 | 1545 | 1546 | /* 1547 | * 1548 | * 1549 | * MDN 1550 | */ 1551 | inline def x1 = attr("x1") 1552 | 1553 | 1554 | /* 1555 | * 1556 | * 1557 | * MDN 1558 | */ 1559 | inline def x2 = attr("x2") 1560 | 1561 | 1562 | /* 1563 | * 1564 | * 1565 | * MDN 1566 | */ 1567 | inline def xChannelSelector = attr("xChannelSelector") 1568 | 1569 | 1570 | /* 1571 | * 1572 | * 1573 | * MDN 1574 | */ 1575 | inline def xLinkHref= attr("xlink:href") 1576 | 1577 | 1578 | /* 1579 | * 1580 | * 1581 | * MDN 1582 | */ 1583 | inline def xLink = attr("xlink:role") 1584 | 1585 | 1586 | /* 1587 | * 1588 | * 1589 | * MDN 1590 | */ 1591 | inline def xLinkTitle = attr("xlink:title") 1592 | 1593 | 1594 | /* 1595 | * 1596 | * 1597 | * MDN 1598 | */ 1599 | inline def xmlSpace = attr("xml:space") 1600 | 1601 | 1602 | /** 1603 | * 1604 | * 1605 | * MDN 1606 | */ 1607 | inline def xmlns = attr("xmlns") 1608 | 1609 | 1610 | /** 1611 | * 1612 | * 1613 | * MDN 1614 | */ 1615 | inline def xmlnsXlink = attr("xmlns:xlink") 1616 | 1617 | 1618 | /* 1619 | * 1620 | * 1621 | * MDN 1622 | */ 1623 | inline def y = attr("y") 1624 | 1625 | 1626 | /* 1627 | * 1628 | * 1629 | * MDN 1630 | */ 1631 | inline def y1 = attr("y1") 1632 | 1633 | 1634 | /* 1635 | * 1636 | * 1637 | * MDN 1638 | */ 1639 | inline def y2 = attr("y2") 1640 | 1641 | 1642 | /* 1643 | * 1644 | * 1645 | * MDN 1646 | */ 1647 | inline def yChannelSelector = attr("yChannelSelector") 1648 | 1649 | 1650 | /* 1651 | * 1652 | * 1653 | * MDN 1654 | */ 1655 | inline def z = attr("z") 1656 | 1657 | } -------------------------------------------------------------------------------- /src/main/scala/dottytags/predefs/Attrs.scala: -------------------------------------------------------------------------------- 1 | package dottytags.predefs 2 | 3 | import dottytags._ 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/generic/Attrs.scala 16 | */ 17 | 18 | /** 19 | * A trait for global attributes that are applicable to any HTML5 element. All traits that define Attrs should 20 | * derive from this trait since all groupings of attributes should include these global ones. 21 | */ 22 | object globalAttrs { 23 | 24 | /** 25 | * Specifies a shortcut key to activate/focus an element 26 | */ 27 | inline def accesskey = attr("accesskey") 28 | /** 29 | * This attribute is a space-separated list of the classes of the element. 30 | * Classes allows CSS and Javascript to select and access specific elements 31 | * via the class selectors or functions like the DOM method 32 | * document.getElementsByClassName. You can use cls as an alias for this 33 | * attribute so you don't have to backtick-escape this attribute. 34 | * 35 | * MDN 36 | */ 37 | inline def `class` = attr("class") 38 | /** 39 | * Shorthand for the `class` attribute 40 | */ 41 | inline def cls = `class` 42 | inline def contenteditable = attr("contenteditable") // Specifies whether the content of an element is editable or not 43 | inline def contextmenu = attr("contextmenu") // Specifies a context menu for an element. The context menu appears when a user right-clicks on the element 44 | /** 45 | * This class of attributes, called custom data attributes, allows proprietary 46 | * information to be exchanged between the HTML and its DOM representation that 47 | * may be used by scripts. All such custom data are available via the HTMLElement 48 | * interface of the element the attribute is set on. The HTMLElement.dataset 49 | * property gives access to them. 50 | * 51 | * The * may be replaced by any name following the production rule of xml names 52 | * with the following restrictions: 53 | * 54 | * the name must not start with xml, whatever case is used for these letters; 55 | * the name must not contain any semicolon (U+003A); 56 | * the name must not contain capital A to Z letters. 57 | * 58 | * Note that the HTMLElement.dataset attribute is a StringMap and the name of the 59 | * custom data attribute data-test-value will be accessible via 60 | * HTMLElement.dataset.testValue as any dash (U+002D) is replaced by the capitalization 61 | * of the next letter (camelcase). 62 | * 63 | * MDN 64 | */ 65 | inline def data(suffix: String) = attr("data-" + suffix) 66 | /** 67 | * Specifies the text direction for the content in an element. The valid values are: 68 | * 69 | * - `ltr` Default. Left-to-right text direction 70 | * 71 | * - `rtl` Right-to-left text direction 72 | * 73 | * - `auto` Let the browser figure out the text direction, based on the content, 74 | * (only recommended if the text direction is unknown) 75 | */ 76 | inline def dir = attr("dir") 77 | /** 78 | * A Boolean attribute that specifies whether an element is draggable or not 79 | */ 80 | inline def draggable = attr("draggable") := "draggable" 81 | /** 82 | * Specifies whether the dragged data is copied, moved, or linked, when dropped 83 | */ 84 | inline def dropzone = attr("dropzone") 85 | /** 86 | * Specifies that an element is not yet, or is no longer, relevant and 87 | * consequently hidden from view of the user. 88 | */ 89 | inline def hidden = attr("hidden") := "hidden" 90 | /** 91 | * This attribute defines a unique identifier (ID) which must be unique in 92 | * the whole document. Its purpose is to identify the element when linking 93 | * (using a fragment identifier), scripting, or styling (with CSS). 94 | * 95 | * MDN 96 | */ 97 | inline def id = attr("id") 98 | /** 99 | * This attribute participates in defining the language of the element, the 100 | * language that non-editable elements are written in or the language that 101 | * editable elements should be written in. The tag contains one single entry 102 | * value in the format defines in the Tags for Identifying Languages (BCP47) 103 | * IETF document. If the tag content is the empty string the language is set 104 | * to unknown; if the tag content is not valid, regarding to BCP47, it is set 105 | * to invalid. 106 | * 107 | * MDN 108 | */ 109 | inline def lang = attr("lang") 110 | /** 111 | * This enumerated attribute defines whether the element may be checked for 112 | * spelling errors. 113 | * 114 | * MDN 115 | */ 116 | inline def spellcheck = attr("spellcheck") := "spellcheck" 117 | /** 118 | * This attribute contains CSS styling declarations to be applied to the 119 | * element. Note that it is recommended for styles to be defined in a separate 120 | * file or files. This attribute and the style element have mainly the 121 | * purpose of allowing for quick styling, for example for testing purposes. 122 | * 123 | * MDN 124 | */ 125 | inline def style = attr("style") 126 | /** 127 | * This integer attribute indicates if the element can take input focus (is 128 | * focusable), if it should participate to sequential keyboard navigation, and 129 | * if so, at what position. It can takes several values: 130 | * 131 | * - a negative value means that the element should be focusable, but should 132 | * not be reachable via sequential keyboard navigation; 133 | * - 0 means that the element should be focusable and reachable via sequential 134 | * keyboard navigation, but its relative order is defined by the platform 135 | * convention; 136 | * - a positive value which means should be focusable and reachable via 137 | * sequential keyboard navigation; its relative order is defined by the value 138 | * of the attribute: the sequential follow the increasing number of the 139 | * tabindex. If several elements share the same tabindex, their relative order 140 | * follows their relative position in the document). 141 | * 142 | * An element with a 0 value, an invalid value, or no tabindex value should be placed after elements with a positive tabindex in the sequential keyboard navigation order. 143 | */ 144 | inline def tabindex = attr("tabindex") 145 | /** 146 | * This attribute contains a text representing advisory information related to 147 | * the element it belongs too. Such information can typically, but not 148 | * necessarily, be presented to the user as a tooltip. 149 | * 150 | * MDN 151 | */ 152 | inline def title = attr("title") 153 | /** 154 | * Specifies whether the content of an element should be translated or not 155 | */ 156 | inline def translate = attr("translate") := "translate" 157 | } 158 | 159 | object sharedEventAttrs { 160 | /** 161 | * Script to be run when an error occurs when the file is being loaded 162 | */ 163 | inline def onerror = attr("onerror") 164 | } 165 | 166 | /** 167 | * Clipboard Events 168 | */ 169 | object clipboardEventAttrs { 170 | /** 171 | * Fires when the user copies the content of an element 172 | */ 173 | inline def oncopy = attr("oncopy") 174 | /** 175 | * Fires when the user cuts the content of an element 176 | */ 177 | inline def oncut = attr("oncut") 178 | /** 179 | * Fires when the user pastes some content in an element 180 | */ 181 | inline def onpaste = attr("onpaste") 182 | } 183 | 184 | /** 185 | * Media Events - triggered by media like videos, images and audio. These apply to 186 | * all HTML elements, but they are most common in media elements, like , , , and