GrooCSS lets you code your CSS in Groovy, using a natural Groovy DSL with optional code completion
1. Introduction
It was created by Adam L. Davis (@adamldavis) and inspired by the many other Groovy-based projects out there, like Gradle, Grails, Spock, Ratpack, and Grooscript.
1.1. Main features
-
DSL similar to CSS but with camel-case and some modifications to make it valid Groovy.
-
Keyframes, media, charset, and font-face support.
-
Automatically adds -webkit, -ms, -moz, -o extensions! (configurable)
-
Color support with rgb, rgba, hex, named colors, and several color changing methods (mix, tint, shade, saturate, etc.)
-
Minimization (compress)
-
Support for transforms directly (transformX, etc),
-
Math functions (sqrt, sin, cos, toRadians, etc.) and built-in Measurement math.
-
Unit methods (unit, getUnit, convert)
-
Ability to extend style-groups and add internal groups.
-
Pseudo-classes in DSL (nthChild, etc.)
-
Multiple ways to configure: Config.builder() or using withConfig
-
Close to CSS syntax using getAt, putAt, operator-overloading, underscore, methodMissing, and propertyMissing
-
Translator to convert from existing CSS.
-
Available pretty print (using Config)
-
Ability to create and reuse groups of styles using styles{} syntax.
-
Methods for getting an image’s width, height, or size.
-
Validates some values by default and can be configured with custom validators and/or processors.
-
Uses Groovy extension modules
1.2. How to Learn GrooCSS
-
Make sure you have the requirements installed.
-
See below for a simple example using a Groovy script.
-
Read about the Gradle Plugin.
-
Read the whole manual to learn about all the features.
-
Follow me on Twitter.
-
Star me on github.
1.3. Requirements
-
JDK 8+
-
Groovy 2.5+ (or 2.4+ if using 1.0-RC1-groovy2.4)
-
Gradle 5.x (or 4.8+ if using 1.0-RC1-groovy2.4)
1.4. Groovy Script
This example uses Groovy’s @Grab annotation to import GrooCSS. Create a file named "makeCss.groovy" with the following code:
@Grab('org.groocss:groocss:1.0-RC1')
import org.groocss.GrooCSS
// demo
def css = GrooCSS.withConfig { prettyPrint() }.process {
a { textDecoration 'none' }
body _.content {
fontSize 20.px
width 400.px
display 'flex'
}
}.writeTo(new File('main'))
Although not recommended, this code would create CSS from the given DSL. Invoke "groovy makeCss.groovy" at the command line to run it. It would output in "main.css" the following:
a {
text-decoration: none;
}
body .content {
font-size: 20px;
width: 400px;
display: flex;
}
1.5. Using Gradle without Plugin
I recommend you use GrooCSS with either the Gradle plugin or asset-pipeline, but it can be used by itself.
import org.groocss.GrooCSS
//
buildscript {
repositories { jcenter() }
dependencies { classpath 'org.groocss:groocss:1.0-RC1-groovy2.5' }
}
task css doLast {
def file = file('css/out.css')
GrooCSS.process {
// DSL goes here
}.writeTo(file)
}
Or using "convert" methods:
GrooCSS.convertFile('infile.groocss', 'outfile.css')
//or
GrooCSS.convert(new File('in'), new File('out'))
2. Gradle Plugin
There’s a GroocssTask available that extends Gradle’s CopyTask to give you finer-grained control of how to convert your files. Here’s an example using a task:
plugins {
id "org.groocss.groocss-gradle-plugin" version "1.0-RC1-groovy2.5"
}
def cssDir = "$parent.buildDir/../www/css"
task css(type: org.groocss.GroocssTask, dependsOn: convertCss) {
conf = new org.groocss.Config(compress: true, addOpera: false)
from 'index.groocss'
into "$cssDir/index.css.min"
}
To process multiple files from and into can be directories (it will assume groocss files end in .groovy or .groocss). For example, files ending in .css.groovy will be converted to files ending in .css. See this page for more about Config.
Using Gradle 4 or later you can use the "-t" command line option to continuously update your files. In other words, every time you change a .css.groovy file it will be automatically converted to a css file.
See learning-groovy for a working example (subproject2).
3. Measurements and math
Built into GrooCSS is the ability to do math with any compatiable Measurement types.
A Measurement is created by a number "." type notation (see below). Measurement Math
def myWidth = 100.pt + 1.in // converts to pt
def myDelay = 100.ms + 1.s // converts to ms
def mySize = myWidth / 2 // you can multiply/divide with any number
def doubleSize = myWidth * 2
4. Configuration
4.1. Config
There are several different ways to configure GrooCSS:
-
Using the groovy constructor:
new Config(compress: true)
-
Using the builder syntax:
Config.builder().compress(true).build()
-
Using the DSL:
GrooCSS.withConfig { noExts().compress().utf8() }.process {}
-
Properties
Of these options, the third is most recommended. With the DSL there are several chainable methods available to easily configure your CSS:
-
noExts()
- sets all extension flags to false (addOpera, etc.) -
onlyMs()
, onlyWebkit(), etc. - sets all extensions flags to false except one. -
utf8()
- sets the charset to UTF-8. -
compress()
- sets compress flag to true.
4.2. Properties
You can also use a Properties file to define Config. For example create a file named "groocss.properties" with the following content (true/TRUE/yes/t are all considered true):
addMoz=false
compress=true
variable.baseUrl=http://mywebsite.com/assets/
Anything that starts with "variable." will use the rest of the key ("baseUrl" above) as the name of variable and set the given value to it. See more about variables here.
It allows the property "processorClasses" to be a list of class-names separated by commas for Processors to use and "styleClasses" can be a comma separated list of styleClasses.
Or with the Gradle plugin you can use propertiesFile = file("groocss.properties") within the task definition. Compressing (Minimization)
To "compress" the output (no new-lines), just pass in a Config object:
GrooCSS.process(new Config(compress: true))
//OR
GrooCSS.convert(new Config(compress: true), infile, outfile)
//OR
groocss { compress = true } // using Gradle plugin
5. Colors and Images
5.1. Colors
Use the "c", "clr", "rgb" or "rgba" methods to create a color. For example:
def css = GrooCSS.process {
def sea = c('5512ab') //OR rgb(85, 18, 171)
_.sea {
color darken(sea)
background sea.brighter()
border "5px solid ${sea.alpha(0.5)}"
}
}
See the javadoc for all available methods.
You can also use named colors:
_.blue {
color darkBlue
background aliceBlue
}
6. Extending and Nesting
6.1. Extending
_.warn { color red }
_.error {
extend(_.warn) // extend '.warn' also works
background black
}
Produces:
.warn,.error {color: Red;}
.error {background: Black;}
6.2. Nesting
Nesting allows you to define modifications to a style (such as pseudo-classes additions to a selector) within the same block. For example:
a {
color '#000'
add ':hover', { color blue }
}
div {
add '> p', { color '#eee' }
}
Produces:
a { color: #000; }
a:hover { color: Blue; }
div > p { color: #eee; }
7. Pseudo-classes
7.1. Pseudo Elements
Pseudo elements are things like ::before, ::after, ::first-line, etc.
They can be added using the ** syntax. Eg:
input ** before { content '===' }
p ** firstLine { fontSize 2.em }
7.2. Pseudo Classes
CSS Pseudo-classes such as :hover, :active, :nth-child, etc.
input % hover { color blue }
li % nthChild('3n') { color blue }
Produces:
input:hover { color: Blue; }
li:nth-child(3n) { color: Blue; }
There are also shorthands like 'odd' and 'even':
li % odd { color blue }
Produces:
li:nth-child(odd) { color: Blue; }
8. Keyframes, Transitions, and Transforms DSL
8.1. Keyframes
GrooCSS includes support for @Keyframes, Transitions, and transforms (like translateX). Keyframes
You create keyframes in GrooCSS using the keyframes(selector, Closure) method (there’s also a "kf" alias).
def css = GrooCSS.process(new Config(addWebkit: false, addMoz: false, addOpera: false)) {
keyframes('bounce') {
40 % { translateY(-30.px) }
60 % { translateY(-15.px) }
frame([0,20,50,80,100]) {
translateY(0)
}
}
}
Produces:
@keyframes bounce {
40%{transform: translateY(-30px);}
60%{transform: translateY(-15px);}
0%, 20%, 50%, 80%, 100%{transform: translateY(0);}
}
8.2. Transitions
Transitions are supported like any normal style property OR with a special DSL which takes a Closure (or multiple Closures).
For example:
a%hover {
transition { all 1.s }
}
This would transition all properties that are changed for a:hover over 1 second.
If you want to supply the easing function, you can. Since command chains in Groovy require pairs to work (a method call and a value) you also need to provide the "delay" value (which can be zero). For example:
a%hover {
transition { all 1.s ease 0 }
}
You can also supply multiple closure to support multiple transition values, eg:
a%hover {
transition { color 1.s ease 0 } { background 2.s }
}
The correct CSS will be output.
9. Importing
The ability to import other groocss files is supported by importFile, importStream, and importString methods which take a parameter map and input. Any number of variables can be passed. For example:
def otherCss = new File('whatever/file.groovy') importFile otherCss.absoluteFile, linkColor: '#456789', backColor: 'black'
Within the imported groovy file, linkColor and background will be available with the given values.
For example, given that "file.groovy" has the following contents:
a { color linkColor backgroundColor backColor }
The resulting CSS would be the following:
a { color #456789; background-color: Black; }
9.1. Variants
importFile - Takes in a java.io.File.
importStream - Takes an InputStream and reads it.
importString - Takes a String expected to be a GrooCSS DSL input.
For example, to read a file named "other.css.groovy" on the classpath (under /groocss/) do the following:
importStream(getClass().getResourceAsStream("/groocss/other.css.groovy"), linkColor: '#456789', backColor: 'black')
9.2. Importing with Gradle
However, you might not always know what directory to look in for files. If you want to import files using the classpath method, you should follow some additional steps (for a multi-module Gradle build):
-
Create a "src/css/groovy" and "imports/css/groocss" and tell your IDE these are also source folders.
-
Create a Main.groovy under "src/css/groovy/groovycss"
-
Create a "build.gradle" file in the current directory.
-
Create a "settings.gradle" file in the current directory with the contents: include 'imports'
-
Create a "build.gradle" file in the "imports/" directory.
-
Create a Groocss file at "imports/css/groocss/elements/a.css.groovy".
9.2.1. Root Build
Put the following in /build.gradle:
plugins {
id 'groovy'
id 'application'
}
mainClassName = 'groovycss.Main'
repositories { jcenter() }
sourceSets {
main {
groovy.srcDirs += ["$projectDir/src/css/groovy"]
}
}
dependencies {
compile "org.groocss:groocss:1.0-RC1-groovy2.5"
runtime project(':imports')
}
task css(
dependsOn: ['compileGroovy', ':imports:build', 'run'],
description: "--> Converts GrooCSS to CSS files",
group: 'build'
) {
doFirst {
def outputDir = "${dir}/resources/css/"
file(outputDir).mkdirs()
}
}
9.2.2. Main
In "Main.groovy" put the following:
package groovycss
import groovy.transform.CompileStatic
import org.groocss.*
@CompileStatic
class Main {
static void main(String[] args) {
String dir = '.'
String outputDir = 'resources/css/'
def conf = new Config(prettyPrint: true, addOpera: false) // for dev
new File("${dir}/src/css/groocss")
.listFiles({ File f -> f.isFile() } as FileFilter)
.each { File f -> processFile(outputDir, f, conf) }
}
static void processFile(String outputDir, File f, Config conf) {
def outputFile = new File("${outputDir}${f.name.replace('.groovy', '')}")
GrooCSS.convert conf, f, outputFile
}
}
This will process all of your files under "src/css/groocss" and output the CSS in "resources/css/".
9.2.3. Imports Build
Within the "imports/build.gradle" file put the following:
plugins {
id 'java'
}
sourceSets {
main {
resources {srcDir 'css/groocss/'}
}
}
9.2.4. Importing From Classpath
Finally, create a file under "src/css/groocss" named "root.css.groovy" and put in the following:
'root'.groocss {
importStream(getClass().getResourceAsStream('/elements/a.css.groovy'))
}
This works since at runtime the files from the "imports" sub-project are included as dependencies.
10. Font-face
fontFace {
fontFamily 'myFirstFont'
fontWeight 'normal'
src 'url(sensational.woff)'
}
Resolves to:
@font-face {
font-family: myFirstFont;
font-weight: normal;
src:url(sensational.woff);
}
11. Media
media 'screen', {
body { width '100%' }
}
Produces:
@media screen {
body { width: 100%; }
}
12. Custom styles
body {
add style('-webkit-touch-callout', 'none')
add style('-webkit-textSize-adjust', 'none')
add style('-webkit-user-select', 'none')
}
Resolves to:
body {
-webkit-touch-callout: none
-webkit-textSize-adjust: none
-webkit-user-select: none
}
13. Detached Styles
You can also create Detached styles, using the styles method, which can be added conditionally to a concrete style group.
For example, see the following simple Closure definition which defines "color #123" if alpha is zero, otherwise it yields color rgba(0,0,0,alpha) which is black with the given opacity:
def mycolor = { alpha ->
styles {
if (alpha == 0) color '#123'
else color rgba(0,0,0,alpha)
}
}
This would allow you to call this Closure later on within your GrooCSS multiple times with different results each time. For example:
table { add mycolor(0) }
div { add mycolor(0.5) }
Would yield the following CSS:
table {
color: #123;
}
div {
color: rgba(0, 0, 0, 0.50)
}
However, you’re not limited to one parameter or one style. For example, you could have a more complicated scenario like the following:
def boxedStyles = { foreColor, timing ->
styles {
transition "all $timing ease"
color shade(foreColor, 0.1)
background tint(foreColor, 0.9)
boxShadow "10px 5px 5px ${shade(foreColor)}"
}
}
This would create styles which transition to a color, background, and box-shadow based on a given color. It would allow the following GrooCSS:
div.salmon %hover {
add boxedStyles(salmon, 1.s)
}
And it would yield the following CSS (with default Config):
div.salmon:hover {
transition: all 1s ease;
-webkit-transition: all 1s ease;
-moz-transition: all 1s ease;
-o-transition: all 1s ease;
color: #e17366;
background: #fef2f0;
box-shadow: 10px 5px 5px #7d4039;
-webkit-box-shadow: 10px 5px 5px #7d4039;
-moz-box-shadow: 10px 5px 5px #7d4039;
}
Remember, "boxedStyles" could be called multiple times, each time with different parameters. This allows code reuse so can greatly enhance your productivity.
14. Migrating from CSS in a Legacy Application
The first step is to make sure your CSS files are properly formatted.
-
Use a command line utility like cssbeautify ($ npm install cssbeautify-cli) or
-
Use your IDE to format each CSS file (using Ctrl+Alt+Shift+L in IDEA for example)
Once this is done, copy all your CSS files into one folder (in our example we’ll use a directory named "toconvert")
14.1. Translating
Then create a Groovy script for converting those files to GrooCSS using org.groocss.Translator:
@Grab('org.groocss:groocss:1.0-RC1-groovy2.5')
import org.groocss.*
File dir = new File("toconvert")
println dir.absolutePath
dir.listFiles().each { f ->
println f
Translator.convertFromCSS(f, new File(f.absolutePath + '.groovy'))
}
Keep track of the output from this process. You should follow up with every line that uses "raw" and see if you can manually convert them.
14.2. Building
Next, decide which way you want to build your GrooCSS into CSS. Which way depends on if you’re using Gralde, Maven, or something else, and how much control you want over the process.
If not using the plugin, you can create a script named "build.groovy" or something similar. This script will convert you groocss files to CSS as part of your build process. Here’s an example:
@Grab('org.groocss:groocss:1.0-RC1-groovy2.5')
import org.groocss.*
File groocssDir = new File("src/groovycss")
def cssDir = "web/resources/css"
new File(cssDir).mkdirs()
groocssDir.listFiles().each { f ->
try {
def config = new Config(addOpera: false, prettyPrint: false, compress: true)
GrooCSS.convert config, f, new File(cssDir, f.name[0..-8])
} catch (e) {
println "ERROR in $f.name"
e.printStackTrace()
}
}
This example expects all of your GroovyCSS files to be in the "src/groovycss" directory and end with ".css.groovy". Your CSS files will end up in "web/resources/css".
14.3. Verifying Conversion
Next you should make sure that all of your converted CSS files are identical to old CSS. To do this simply on a Linux command line, use the following command (assuming old CSS files are in web/styles/):
ls web/styles/ | xargs -I{} diff -wiB web/resources/css/{} web/styles/{} > css.diff
This goes through every converted CSS file and compares it to the original while ignoring case, whitespace, and blank lines. Look at the resulting "css.diff" file to see what changed. There will probably be a lot of differences but just make sure they don’t change the meaning of the CSS.
15. Processors
Processors allow you to write custom code (in any JVM language such as Java or Groovy) to process or validate your CSS before and during its generation. With Processors you can enforce rules, modify certain inputs, and generate CSS from given GrooCSS values. GrooCSS is built from the ground up to easily allow modification.
There are currently three phases:
-
PRE_VALIDATE
-
VALIDATE
-
POST_VALIDATE
During any phase if your Processor method returns a non-empty Optional value that will be considered a Validation failure and treated as an error. The given value will be printed out so that the user can then fix the problem.
In this way, your Processors could modify input in phase 1, validate it in phase 2 and do post-processing in phase 3.
You are only limited to what you can dream up, but here are some examples:
15.1. Conversion
class ConvertAllIntsToPixels implements Processor< Style > {
Optional process(Style style, Phase phase) {
if (phase == Phase.PRE_VALIDATE && style.value instanceof Integer) {
style.value = new Measurement(style.value, 'px')
}
return Optional.empty();
}
}
This Processor would convert any given int values into pixel measurement. So for example, "padding 2" would become "padding: 2px" in the CSS output.
Note that we put it in the PRE_VALIDATION phase. This would allow us to validate Measurements are not numbers in the VALIDATION phase.
15.2. Validation
import org.groocss.*
class PaddingValidator implements Processor< Style > {
Optional process(Style style, Phase phase) {
if (phase == Phase.VALIDATE &&
style.name == "padding" &&
!(style.value instanceof String &&
style.value.endsWith("px"))) {
return Optional.of("padding with value $style.value is not a px".toString())
}
return Optional.empty();
}
}
This PaddingValidator would validate that a padding value is always a String that ends with "px".
15.3. DefaultValidator and RequireMeasurements
GrooCSS uses the Processor mechanism internally as well. It comes with DefaultValidator which is automatically added to the set of Processors before processing. It validates that if a value is a Measurement, it’s of an allowed type. For example, "padding 2.deg" would cause an exception.
GrooCSS also comes with a built-in Processor that is not enabled by default, RequireMeasurements. It validates that every value that should be a Measurement is one. This means that "padding '2px'" would not be allowed, only "padding 2.px" for example.
RequireMeasurement extends AbstractValidator, which is an abstract class that implements Processor and calls an abstract method "validate" only during the VALIDATE phase. You can also extend AbstractValidator if you want.
15.4. Extensions
However, Processors need not be restricted to validating or simple conversions. Processors could also greatly expand from input.
For example, the following Processor, named "Expander", would expand "mp" to generate both "margin" and "padding".
import org.groocss.*
class Expander implements Processor< StyleGroup > {
Optional process(StyleGroup sg, Phase phase) {
if (phase == Phase.PRE_VALIDATE) {
def style = sg.styleList.find{ it.name == 'mp' }
if (style) {
style.name = "padding"
sg.add(new Style('margin', style.value))
}
}
return Optional.empty();
}
}
-Note that the generic type (StyleGroup in this class) determines what type will be processed by your Processor.
This allows the input to use those shortcuts in GrooCSS such as the following:
table {
add style('mp', 10.px)
}
Which will become the following CSS:
table {
margin: 10px;
padding: 10px;
}
This example demonstrates how Processors can modify and add to generated CSS.
16. Micronaut Example/Asset-pipeline
The GrooCSS codebase includes a working Micronaut example that uses Micronaut with asset-pipeline.
Taking a look at the build file, you’ll see the asset-pipeline plugin is included among the plugins:
plugins {
id "io.spring.dependency-management" version "1.0.6.RELEASE"
id "com.github.johnrengelman.shadow" version "4.0.2"
id "com.bertramlabs.asset-pipeline" version "3.0.6"
}
Second, you’ll need to add asset-pipeline-micronaut as a runtime dependency and the groocss-asset-pipeline as a "assets" level depenedency (a custom asset-pipeline configuration).
runtime 'com.bertramlabs.plugins:asset-pipeline-micronaut:3.0.7'
assets 'com.bertramlabs.plugins:groocss-asset-pipeline:3.0.7'
This will allow you to put GrooCSS files in "src/assets/lib" and have them automatically converted to CSS available to your application.
See the asset-pipeline docs for more.
Or checkout the Micronaut example code.
17. Static/Code Completion
GrooCSS has always made static code compilation one of its priorities. However, for greatest similarity to CSS, it was necessary to use dynamic method calls (using methodMissing behind the scenes) to allow for the .styleClass syntax (and more) to work.
To support fully static code (which enables code completion in your IDE), there are several ways to define CSS selectors without resorting to any dynamic method calls.
17.1. Set Up Your IDE
First you need to setup your IDE.
IDEA: Create a new Module of type Groovy.
Eclipse: Create a new Project of type Groovy (make sure you have a Groovy plugin installed).
Create a directory, "src/css/groocss" and tell your IDE this is a source folder.
Make sure that you have a groocss JAR as a dependency in your Module or project’s definition.
17.2. Root GrooCSS
To begin a fully static GrooCSS file, you can start with 'foo'.groocss { and end with }, where "foo" should be the name of the current file.
For example, create a file under "src/css/groocss" named "root.css.groovy" and put in the following:
'root'.groocss {
html { color black }
body { width 100%_ }
'div.main-div'.sg { fontSize 2.em }
'button1'.id { width 20.em }
_ ** selection { color '#ffeefe' }
$('.list li') { appearance 'none' }
}
The above would yield the following CSS:
html { color: Black; }
body { width: 100%; }
div.main-div { font-size: 2em; }
#button1 { width: 20em; }
::selection { color: #ffeefe; }
.list li { appearance: none; }
Note that you can mix and match many different operators and methods as long as you don’t call an undefined method. Building
Building is the same as already discussed. Importing
You can always use importFile(filename) or importFile(File) to import another file.
17.3. Allowed Static Code
17.3.1. Allowed
To keep all of your GrooCSS code statically typed, the following is allowed/static/non-dynamic code:
def myColor = c('#fe33ac')
table {
color myColor
}
input['class$="test"'] = { //becomes input[class$="test"]
background yellow
}
sg '#formId', {
minWidth 100.px // resolves to 100px
}
p + div {
border '1px solid black'
}
p | a { color red } // | => ,
p >> a { color blue } //>> => >
p * a { color blue } // * => *
p - a { color blue } // - => ~(tilde)
p ^ a { color blue } // ^ => (space)
18. Variables
Variables allow you make values available to your GrooCSS at compile time through Config (or through method parameters of convertWithoutBase).
You can also use a Properties file to define variables with the variable. prefix. Or you can directly pass variable values through the Config object. Config with Variables
There are three main methods on Config for setting variable values:
-
withVariable(String key, Object value)
Takes a variable name and value, adds it to variables Map. -
withVariables(String key, Object value, String key2, Object value2)
Takes a two variable names and values, adds them to variables Map. -
withVariables(Map<String, Object> variables)
Takes a map of variable name to value, adds them to variables Map.
Each of these methods returns the Config object so they can be chained. |
Groovy allows map parameters to be passed easily using the map-parameter syntax. Eg:
def config = new Config().withVariables(foreGround: '#123', backGround: '#abc')
This allows you to reference these variables within your GrooCSS files. Gradle with Variables
With the Gradle plugin, it might be useful to pass the buildDir to your GrooCSS files.
task css(type: org.groocss.GroocssTask, dependsOn: convertCss) {
conf = new org.groocss.Config().withVariables(buildDir: project.buildDir)
from 'groocss/'
into "css/"
}
Or when using the built-in task (convertCss):
groocss {
processors = []
variables = [buildDir: project.buildDir]
}
groocssfiles {
allfiles {
inFile = file('src/main/groovy/')
outFile = file("css/")
}
}
This can enable you to use importFile using the buildDir variable. Eg:
'main'.groocss {
importFile("$buildDir/../groocss/myfile1.css.groovy")
importFile("$buildDir/../groocss/myfile2.css.groovy")
}