AbsurdJS

JavaScript based CSS/HTML preprocessor


Compile
Click me or press Ctrl+Enter

»Client-side usage

Download the client-side version:
absurd.js
absurd.min.js

After that simply include the file into your page.

<script src="absurd.min.js"></script>

Initialize Absurd and use the API.

var api = Absurd();
api.add({
	body: {
		marginTop: "20px",
		p: {
			color: "#000"
		}
	}
});

Compile and get the CSS.

api.compile(function(err, css) {
	// use the compiled css
});

»Client-side tests

Just download the files from the repository and open /client-side/tests in your browser.

/client-side/tests

»Server-side usage

»Installation

npm install -g absurd

»Compilation

»Inline compilation

Use this approach if you want to integrate AbsurdJS into your NodeJS app.

var Absurd = require("absurd");
Absurd(function(api) {
    // use the Absurd's api here
}).compile(function(err, css) {
    // do something with the css
});

»Inline compilation to a file

Use this approach if you want to integrate AbsurdJS into your NodeJS app and save the result to a file.

var output = "./css/styles.css";
Absurd("./css/styles.js").compileFile(output, function(err, css) {
    // ...
});

»Compiling through command line interface

// Outputs the css in the console.
absurd -s [source file] 

// Outputs the css to a file.
absurd -s [source file] -o [output file]

// Outputs the css to a file and watching 
// specific directory. If some of the files 
// there is changed a compilation is fired.
absurd -s [source file] -o [output file] -w [directory]

// Minify the CSS
absurd -s [source file] -m true

// Preprocessing html
absurd -s [source file] -t html

// keep the camel case of the properties
absurd -s [source file] -k true
Here are two examples uses all the options:
# as css parser
node ./index.js -s ./tests/data/css/index.js -o ./tests/data/result/res.css -w ./tests/data/**/*.js -m true

# as html parser 
node ./index.js -s ./tests/data/html.json -o ./tests/data/result/code.html -w ./tests/data/**/*.json -m true -t html

Dependencies for your package.json:

"dependencies": {
    "grunt": "~0.4.1",
    "grunt-contrib-watch": "*",
    "grunt-absurd": "*"
}

Simple Gruntfile.js:

module.exports = function(grunt) {

    grunt.initConfig({
        absurd: {
            task: {
                src: __dirname + "/css/absurd/index.js",
                dest: 'css/styles.css'
            }
        },
        watch: {
            css: {
                files: ['css/absurd/**/*.js'],
                tasks: ['absurd']
            }
        }
    });

    grunt.loadNpmTasks('grunt-contrib-watch');
    grunt.loadNpmTasks('grunt-absurd');

    // grunt.registerTask('default', ['concat', 'less']);
    grunt.registerTask('default', ['absurd', 'watch']);

}

»Input formats

»JavaScript

Just create a .js file like for example /css/styles.js. The file should contain the usual nodejs module code.

module.exports = function(api) {
    // use the Absurd's api here
}

»JSON

The library supports importing of pure JSON. Of course this approach doesn't give you an access to the API, but it is still useful if you want to skip the typical Nodejs module definition.

// styles.json
{
    "body": {
        "margin": "0",
        "padding": "0",
        "fontSize": "1em",
        "p": {
            "line-height": "30px"
        }
    }
}

// app.js
Absurd("styles.json").compile(function(err, css) {
    // ...
});

»CSS

Absurd accepts normal CSS. The good news is that you can still use all the other features.

// styles.js
api.plugin("size", function(api, type) {
    switch(type) {
        case "large": return { fontSize: "30px" }; break;
        case "medium": return { fontSize: "24px" }; break;
        case "small": return { fontSize: "12px" }; break;
        default: return { fontSize: "16px" };
    }
});
api.import(__dirname + "/css/main.css");

// main.css
body {
    margin-top: 10px;
    size: small;
}

And the result is:

body {
    margin-top: 10px;
    font-size: 12px;
}

»YAML

The library supports import of YAML files

// styles.yaml
---
body:
  margin: 0
  padding: 0
  fontSize: 1em
  p:
    line-height: 30px

// app.js
Absurd("styles.yaml").compile(function(err, css) {
    // ...
});

»Syntax

In the context of JavaScript the closest thing to pure CSS syntax is JSON format. So, every valid JSON is actually valid Absurd syntax. As you can see above, there are two ways to send styles to the module. In both cases you have an access to the Absurd's API.

Absurd(function(api) {
    api.add({});
});

or in an external js file

module.exports = function(api) {
    api.add({});
}

The add method accepts valid JSON. This could be something like

{
    body: {
        marginTop: '20px',
        padding: 0,
        fontWeight: 'bold',
        section: {
            paddingTop: '20px'
        }
    }
}

Every object defines a selector. Every property of that object could be a property and its value or another object. For example the JSON above is compiled to:

body {
    margin-top: 20px;
    padding: 0;
    font-weight: bold;
}
body section {
    padding-top: 20px;
}

Have in mind that you don't need to quote the CSS properties. AbsurdJS converts every uppercase letter to a dash followed by the lowercase version of the letter. I.e.:

WebkitTransform -> -webkit-transform

You may send even an array of style like that

{
    '.header nav': [
        { fontSize: '10px' },
        { fontSize: '20px' }
    ]
}

And that's compiled to

.header nav {
    font-size: 20px;
}

»Pseudo classes

It's similar like the example above:

a: {
    textDecoration: 'none',
    color: '#000',
    ':hover': {
        textDecoration: 'underline',
        color: '#999'
    },
    ':before': {
        content: '"> "'
    }
}

Is compiled to:

a {
    text-decoration: none;
    color: #000;
}
a:hover {
    text-decoration: underline;
    color: #999;
}
a:before {
    content: "> ";
}

Using ampersand (&) operator

It has the same meaning like in LESS.

a: {
    color: '#000',
    '&.sky': {
        color: "blue"
    }
}

Is compiled to:

a {
  color: #000;
}
a.sky {
  color: blue;
}

»Importing

Of course you can't put everything in a single file. That's why there is .import method:

module.exports = function(api) {
    api.import(__dirname + '/config/colors.js');
    api.import(__dirname + '/config/sizes.js');
    api.import([
        __dirname + '/layout/grid.json'
    ]);
    api.import([
        __dirname + '/forms/login-form.css',
        __dirname + '/forms/feedback-form.css'
    ]);
}

Pay attention to the type of the files. The first two are actually JavaScript files, because they need an access to the API (to define mixins, plugins etc ...). The second import statement adds the actual styling. Of course it is not necessary to use this approach, but writing pure JSON sometimes may be a better option.


»Using variables

There is no need to use something special. Because that's pure javascript you may write:

var brandColor = "#00F";
Absurd(function(api) {
    api.add({
        '.header nav': {
            color: brandColor
        }
    })
})

However, if you want to share variables between the files you may use the API's method storage. Let's say that you have two files /css/A.js and /css/B.js and you want to share values between them.

// A.js
module.exports = function(api) {
    api.storage("brandColor", "#00F");
}

// B.js
module.exports = function(api) {
    api.add({
        body: {
            color: api.storage("brandColor")
        }
    })
}

»Mixins

In the context of Absurd mixins are actually normal javascript functions. The ability to put things inside the Absurd's storage gives you also the power to share mixins between the files. For example:

// A.js
module.exports = function(api) {
    api.storage("button", function(color, thickness) {
        return {
            color: color,
            display: "inline-block",
            padding: "10px 20px",
            border: "solid " + thickness + "px " + color,
            fontSize: "10px"
        }
    });
}

// B.js
module.exports = function(api) {
    api.add({
        '.header-button': [
            api.storage("button")("#AAA", 10),
            {
                color: '#F00',
                fontSize: '13px'
            }
        ]
    });
}

What you do is to send array of two objects to the selector '.header-button'. The above code is compiled to:

.header-button {
    color: #F00;
    display: inline-block;
    padding: 10px 20px;
    border: solid 10px #AAA;
    font-size: 13px;
}

Notice that the second object overwrites few styles passed via the mixin.


Plugins (i.e. define your own CSS properties)

The plugins are similar like mixins, but act as property-value pair. There is an API method for registering plugins.

api.plugin('my-custom-gradient', function(api, colors) {
    return {
        background: 'linear-gradient(to bottom, ' + colors.join(", ") + ')'
    };
});
api.plugin('brand-font-size', function(api, type) {
    switch(type) {
        case "small": return { fontSize: '12px'}; break;
        case "medium": return { fontSize: '22px'}; break;
        case "big": return { fontSize: '32px'}; break;
        default: return { fontSize: '12px'}; break;
    }
});

The code creates two plugins, which respond on my-custom-gradient and brand-font-size property. So, the following styles

api.add({
    body: {
        margin: '20px',
        fontSize: '14px',
        'my-custom-gradient': ['#F00', '#00F'],
        p: {
            'brand-font-size': 'big'    
        }                   
    }
});

are compiled to:

body {
    margin: 20px;
    font-size: 14px;
    background: linear-gradient(to bottom, #F00, #00F);
}
body p {
    font-size: 32px;
}

Have in mind, that the plugin should always return an object.


»Media queries

The following code

api.add({
    body: {
        lineHeight: '20px',
        '@media all (max-width: 950px)': {
            lineHeight: '40px',
            color: '#BADA55'
        },
        '@media all (min-width: 550px)': {
            lineHeight: '32px'  
        },
        p: {
            margin: '10px',
            padding: '4px',
            '@media all (max-width: 950px)': {
                padding: '12px',
                'brand-color': ''
            }
        }
    }
});

is compiled to

body {
    line-height: 20px;
}
body p {
    margin: 10px;
    padding: 4px;
}
@media all (max-width: 950px) {
    body {
        line-height: 40px;
        color: #BADA55;
    }
    body p {
        color: #9fA;
        padding: 12px;
    }
}
@media all (min-width: 550px) {
    body {
        line-height: 32px;
    }
}

»Sending raw data to the final compiled CSS

AbsurdJS gives you ability to directly send content to the final CSS. I.e. something which is skipped by the compiler and it is directly concatenated with the processed data.

api
.add({
    body: {
        marginTop: "20px",
        p: {
            fontSize: "5px"
        }
    }
})
.raw('/* ' + comment + ' */')
.add({
    a: {
        paddingTop: "20px"
    }
});

The code above is compiled to

body {
  margin-top: 20px;
}
body p {
  font-size: 5px;
}
/* AbsurdJS is awesome! */
a {
  padding-top: 20px;
}

»Extending

You may create your own CSS properties by using the plugins feature.

api.plugin("hoverEffect", function(api, color) {
	return {
		":hover": {
			color: color,
            background: api.lighten(color, 60)
		}
	}
})
api.add({
	a: {
		color: "#000",
		hoverEffect: "#999"
	}
});

You could create your own API methods.

api.register("buttonize", function(selector) {
	var styles = {};
	styles[selector] = {
		cursor: "pointer",
		display: "block",
		background: "#aaa",
		":hover": {
			background: "#ddd"
		}
	};
	api.add(styles);
});
api.buttonize(".header a");
The code above produces:
.header a {
  cursor: pointer;
  display: block;
  background: #aaa;
}
.header a:hover {
  background: #ddd;
}

Every API method could be overwritten. You are able to run your own function instead of the original one. If your method returns true then the default behaviour of the method is suppressed and only your implementation works.

api.hook("add", function(rules) {	
	if(rules.grid) {
		var styles = {};
		styles[rules.grid.parent] = parentStyles = {
			":after": {
				display: "table",
				content: "",
				clear: "both"
			}
		};
		parentStyles[rules.grid.child] = {
			width: (100 / rules.grid.columns) + "%",
			float: "left"
		};
		api.add(styles);
		return true;
	}
});
api.add({
	grid: {
		parent: ".header .menu",
		child: ".link",
		columns: 4
	}
});

And the result is:

.header .menu:after {
  display: table;
  content: "";
  clear: both;
}
.header .menu .link {
  width: 25%;
  float: left;
}

»Metamorphosis / HTML

AbsurdJS was made really flexible. It is so flexible that you can use it as HTML preprocessor. I.e. you may write JavaScript, JSON or YAML and receive HTML at the end.

api.morph("html");
api.add({
	html: {
	    head: {
	    	title: "title"
	    },
	    body: {}
	}
}).compile(function(err, html) {
	// use the html here
});
The result of the above compilation is:
<html>
	<head>
		<title>
			title
		</title>
	</head>
	<body/>
</html>

There are three special properties that you may set:

_ (underscore) is interpreted as node value.
body: {
	_: "page text"
}
Will be compiled to:
<body>page text</body>
_attrs accepts an object, which is used to define node attributes.
body: {
	_attrs: { class: "black" }
}
The result is:
<body class="black"/>
_tpl is used when you have a template defined. During the usage of add method you may pass a template name. For example:
api.add({
	title: "AbsurdJS preprocessor"
}, "title");

api.add({
	html: {
		head: {
			_tpl: "title"
		}
	}
})
The above code is compiled to:
<html>
	<head>
		<title>
			AbsurdJS preprocessor
		</title>
	</head>
</html>
_include accepts an object or array of objects. It acts as templates. Just compiles the included objects.
var paragraph = { p: "Paragraph text." };
var footer = { footer: "Footer text." };
var body = {
	_include: [
		{ h1: "Title here" },
		paragraph, 
		footer
	]
}
api.morph("html");
api.add({
	html: {
		_include: body
	}
});
The result is:
<html>
	<h1>
		Title here
	</h1>
	<p>
		Paragraph text.
	</p>
	<footer>
		Footer text.
	</footer>
</html>

If you want to speed up your writing you will be glad to see that Absurd supports Emmet like syntax.

api.morph("html");
api.add({
	html: {
		head: {
			title: "AbsurdJS is awesome"
		},
		body: {
			'.content.left#wrapper': {
				'a[href="http://krasimir.github.io/absurd/"]': "AbsurdJS documentation",
				'p.text[title="description" data-type="selectable"]': "CSS preprocessor"
			}
		}
	}
});
The code produces:
<html>
	<head>
		<title>
			AbsurdJS is awesome
		</title>
	</head>
	<body>
		<div class="content left" id="wrapper">
			<a href="http://krasimir.github.io/absurd/">
				AbsurdJS documentation
			</a>
			<p class="text" title="description" data-type="selectable">
				CSS preprocessor
			</p>
		</div>
	</body>
</html>

AbsurdJS could act as a template engine. Use <% and %> as place holders. The data is passed along with the callback in the .compile method.

api.morph("html").add({
  body: {
    p: "Hello, my name is <% this.data.name %>!",
      small: "I'm <% this.data.profile.age %> years old",
      ul: [        
        '<% for(var skill in this.data.skills) { %>',
        { li: {
          'a[href="#"]': 'I do <% this.data.skills[skill] %>'
        }},
        '<% } %>'
      ]
    }
}).compile(function(err, html) {
	/* html:
	<body>
		<p>Hello, my name is John!</p>
		<small>I\'m 29 years old</small>
		<ul>
			<li><a href="#">I do javascript</a></li>
			<li><a href="#">I do html</a></li>
			<li><a href="#">I do css</a></li>
		</ul>
	</body>
	*/
}, {
    data: {
	    name: "John",
	    profile: { age: 29 },
        skills: ['javascript', 'html', 'css']
    }
});

»Metamorphosis / Component

AbsurdJS process CSS and HTML. So, why not doing both at the same time.

var page = {
	css: {
		".page": {
			marginTop: "20px",
			h2: {
				fontSize: "40px"
			}
		}
	},
	html: {
		'section.page': {
			h2: "That's a page"
		}
	}
}
api
.morph("component")
.add(page)
.compile(function(err, css, html) {
	// ...
});
The result of the above compilation is:
---- CSS ----
.page {
  margin-top: 20px;
}
.page h2 {
  font-size: 40px;
}

---- HTML ----
<section class="page">
	<h2>
		That's a page
	</h2>
</section>

You can even nest components.

var title = {
    css: {
        ".title": { fontSize: "30px" }
    },
    html: {
        "h1.title": "Title here"
    }
}
var header = function() {
    return {
        css: {
            header: { marginTop: "20px" }
        },
        html: {
            header: {
                span: "Header text"
            }
        }
    }
}
var page = {
	css: {
		".page": {
			marginTop: "20px",
			h2: {
				fontSize: "40px"
			}
		}
	},
	html: {
        _include: [header, title],
		'section.page': {
			h2: "That's a page"
		}
	}
}
api.morph("component").add(page);
The result is:
---- CSS ----
.page, header {
  margin-top: 20px;
}
.page h2 {
  font-size: 40px;
}
.title {
  font-size: 30px;
}

---- HTML ----
<header>
	<span>
		Header text
	</span>
</header>
<h1 class="title">
	Title here
</h1>
<section class="page">
	<h2>
		That's a page
	</h2>
</section>

»API

».add

Parameters:
[object]

module.exports = function(api) {
    api.add({
        body: { 
            padding: 0 
        }
    });
}

».import

Parameters:
[string] | [[string, string, ...]]

module.exports = function(A) {
    A.import(__dirname + '/config/sizes.js');
    A.import([
        __dirname + '/config/colors.js',
        __dirname + '/config/sizes.js'
    ]);
}

AbsurdJS supports importing of JavaScript, JSON and CSS files.


».storage

Parameters:
[key], [value]

Setting value:

module.exports = function(api) {
    api.storage("mixin", function(px) { 
        return {
            fontSize: px + 'px'
        }
    });
}

Getting value:

module.exports = function(api) {
    api.add({
        body: [
            api.storage("mixin")(18)
        ]
    });
}  

».plugin

Parameters:
[name of property], [function]

api.plugin('my-custom-gradient', function(api, colors) {
    return {
        background: 'linear-gradient(to bottom, ' + colors.join(", ") + ')'
    };
});

The function of the plugin accepts two arguments. The first one is a reference to the API and second one is the value of the CSS property.


».darken

Parameters:
[color], [percents]

module.exports = function(api) {
    api.add({
        body: {
            color: api.darken('#BADA55', 25) // darken 25%
        }
    });
}

».lighten

Parameters:
[color], [percents]

module.exports = function(api) {
    api.add({
        body: {
            color: api.lighten('#BADA55', 25) // lighten 25%
        }
    });
}

».raw

Parameters:
[string]

module.exports = function(api) {
    api.raw('/* comment here */');
}

».rawImport

Parameters:
string or array of strings

module.exports = function(api) {
    api.rawImport(['bootstrap-responsive.css', 'bootstrap.css']);
}

».compile

Parameters:
[function], [options]

api.compile(function(err, css) {
	// use the compiled css
}, {});
		

The options parameter is optional. It's default value is:

{
	combineSelectors: true,
	minify: false,
	processor: [internal CSS preprocessor],
	keepCamelCase: false
}
		

».compileFile

Parameters:
[path], [function], [options]

api.compileFile("css/styles.css", function(err, css) {
    // use the compiled css
}, {});
		

Check .compile method for more information regarding options parameter.


».hook

Parameters:
[method], [function]

api.hook("add", function(rules, stylesheet) {
	// write your logic here
	return true;
});
		

If your hook handler returns true the default method behaviour is skipped.


».register

Parameters:
[method], [function]

api.register("toJSON", function(callback) {
	// your custom code here
})
		

».morph

Parameters:
[type]

api.morph("html");
api.morph("component");

»Testing

The tests are placed in /tests directory. Install jasmine-node and run:

jasmine ./tests

Fork me on GitHub