
scalacss.internal.Macros.scala Maven / Gradle / Ivy
The newest version!
package scalacss.internal
import java.util.regex.Pattern
import scala.language.`3.0`
import scala.reflect.NameTransformer
import scala.quoted._
object Macros {
object Dsl {
trait Base {
extension (inline sc: StringContext) {
/** c"#fc6" provides a validates Color */
inline def c(inline args: Any*): Color =
${ ColorLiteral.impl('sc) }
}
}
inline def name(): String =
${ nameQuoted }
private def nameQuoted(using Quotes): Expr[String] = {
import quotes.reflect._
val owner = {
var o = Symbol.spliceOwner
while (o.flags.is(Flags.Synthetic))
o = o.owner
o
}
val isStyleSheet: Symbol => Boolean = {
val ss = TypeRepr.of[mutable.StyleSheet.Base]
sym =>
try {
sym.tree match {
case ClassDef(_, _, parents, _, _) =>
parents.exists {
case p: TypeTree => (p.tpe <:< ss)
case _ => false
}
case _ => false
}
} catch {
case _: Throwable => false
}
}
val classOwner = {
var o = owner.owner
while (!isStyleSheet(o) && !o.maybeOwner.isNoSymbol)
o = o.owner
o
}
def fixName(s: String): String =
s.replace("$", "")
val localName =
fixName(owner.name)
// `style()` instead of `val x = style()` results in ""
val finalName =
if (localName startsWith "<")
""
else {
// Try to extract a name, relative to the stylesheet class
val className = fixName(classOwner.fullName)
val fullName = fixName(owner.fullName)
if ((fullName.length > className.length + 1) && fullName.startsWith(className + ".")) {
val relName = fullName.substring(className.length + 1).replace('.', '-')
relName
} else
// Default to local name
localName
}
Inlined(None, Nil, Literal(StringConstant(finalName))).asExprOf[String]
}
import DslMacros._
trait Mixin {
protected def __macroStyle (name: String): MStyle
protected def __macroStyleF (name: String): MStyleF
protected def __macroKeyframes(name: String): MKeyframes
protected def __macroKeyframe : MStyle
protected def __macroFontFace : MFontFace
final protected inline def style : MStyle = __macroStyle(name())
final protected inline def styleF : MStyleF = __macroStyleF(name())
final protected inline def keyframes: MKeyframes = __macroKeyframes(name())
final protected inline def keyframe : MStyle = __macroKeyframe
final protected inline def fontFace : MFontFace = __macroFontFace
}
}
// ===================================================================================================================
type Color = ValueT[ValueT.Color]
object ColorLiteral {
private def cssFnRegex(f: String, as: String*) = {
val args = as mkString ","
Pattern compile s"^$f\\($args\\)$$"
}
private def int = "(-?\\d+)"
private def dbl = """(-?\d+(?:\.\d+)?|\.\d+)"""
private def pct = s"$dbl%"
private val ws = "\\s+".r
private val rgbI = cssFnRegex("rgb", int, int, int)
private val rgbP = cssFnRegex("rgb", pct, pct, pct)
private val rgba = cssFnRegex("rgba", int, int, int, dbl)
private val hsl = cssFnRegex("hsl", int, pct, pct)
private val hsla = cssFnRegex("hsla", int, pct, pct, dbl)
private def isHex: Char => Boolean =
c => (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
def impl(sc: Expr[StringContext])(using Quotes): Expr[Color] = {
import quotes.reflect._
import report.{throwError => fail}
val arg: Expr[String] =
sc match {
case '{ StringContext(${Varargs(parts)}*) } =>
parts.toList match {
case a :: Nil => a
case x => fail(s"Expected exactly 1 StringContext part, got: $x")
}
case _ =>
fail(s"Failed to extract StringContext parts")
}
val value: String =
arg.asTerm match {
case Literal(StringConstant(v)) => v
case Inlined(_, _, Literal(StringConstant(v))) => v
case x => fail("Don't know how to handle: " + x)
}
attempt(value) match {
case Right(c) =>
val l = Inlined(None, Nil, Literal(StringConstant(c))).asExprOf[String]
Inlined(None, Nil, '{ Color($l) }.asTerm).asExprOf[Color]
case Left(e) =>
report.throwError(e)
}
}
def runtime(text: String): Color =
attempt(text) match {
case Right(c) => Color(c)
case Left(e) => throw new IllegalArgumentException(e)
}
def attempt(text0: String): Either[String, String] = {
val text = ws.replaceAllIn(text0, "").toLowerCase
var errorResult: Left[String, String] =
null
def pass = true
def undecided = false
def fail(err: String) = {
errorResult = Left(err)
false
}
def validateHex: Boolean =
(text.charAt(0) == '#') && {
val v = text.drop(1)
v.length match {
case (3 | 6 | 4 | 8) if v.forall(isHex) => pass
case _ => fail("Hex notation must be #RGB, #RRGGBB, #RGBA, or #RRGGBBAA.")
}
}
type V = String => Unit
def validateInt(name: String, max: Int): V =
s => {
val i = s.toInt
if (i < 0 || i > max)
fail(s"Invalid $name value: $s")
}
def validateDbl(name: String, max: Double): V =
s => {
val d = s.toDouble
if (d < 0 || d > max)
fail(s"Invalid $name value: $s")
}
def validatePct(name: String): V =
validateDbl(name, 100)
def validateFn(p: Pattern, a: V, b: V, c: V, d: V = null): Boolean = {
val m = p.matcher(text)
if (m.matches) {
a(m group 1)
b(m group 2)
c(m group 3)
if (d ne null) d(m group 4)
pass
} else
undecided
}
def ri = validateInt("red", 255)
def gi = validateInt("green", 255)
def bi = validateInt("blue", 255)
def rp = validatePct("red")
def gp = validatePct("green")
def bp = validatePct("blue")
def h = validateInt("hue", 359)
def s = validatePct("saturation")
def l = validatePct("lightness")
def a = validateDbl("alpha", 1)
def validateRgbI = validateFn(rgbI, ri, gi, bi)
def validateRgbP = validateFn(rgbP, rp, gp, bp)
def validateRgba = validateFn(rgba, ri, gi, bi, a)
def validateHsl = validateFn(hsl, h, s, l)
def validateHsla = validateFn(hsla, h, s, l, a)
val validated =
validateHex || validateRgbI || validateRgbP || validateRgba || validateHsl || validateHsla
if (validated && (errorResult == null))
Right(text)
else
Option(errorResult).getOrElse(Left("Invalid colour: \"" + text0 + "\""))
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy