More, a CSS compiler

It’s hard to represent CSS in pictures, so I’m breaking this post up with puppies.

CSS is an… interesting technology.  As Wil Shipley put it, “CSS: If a horse were designed by a committee of camels.”  There’s just enough rough edges, weird decisions, and has-not-had-to-use-it-everyday blindness to make you want something better to work in.  While it’s still a draft (and I hope has no chance of becoming part of a standard), take a look at CSS Variables and despair that that was proposed.

I’m hardly the first (second, or probably even millionth) person to think this, so there are some alternatives to writing CSS out there.  At Stack Exchange we use LESS (specifically the dotLESS variant), Sass is also pretty popular just from straw polling developers though I’m not really familiar with it.

For kicks I started throwing my own CSS compiler together a few months ago that’s a little more radical than LESS (from which I took a great deal of inspiration).  I’m calling it More for now because: naming is hard, and I like the homage to LESS.  Although I am drawing explicit inspiration from LESS, More is not a LESS super-set.

The radical changes are

  • Order doesn’t matter
  • “Resets” are first class
  • Sprites are first class
  • Explicit copy selector syntax, in addition to mixins
More details below, as you might guess familiarity with LESS will make lots of this sound really familiar.  If you’d rather not read a 3,000+ word post, hop over to Github where the README takes a whack at summarizing the whole language.

Declaration Order Doesn’t Matter

In CSS subsequent declarations override preceding ones, if you declare .my-class twice the latter one’s rules win; likewise for rules within the same block.  This is pretty wrong-headed in my opinion, you should be stating your intentions explicitly not just tacking on new rules.  Combine this with how negative an impact bloated CSS can have on page load speeds (CSS and HTML are the bare minimum necessary for display, everything else can be deferred), I can’t help but classify this as a misfeature.

In More, the order of selector blocks in the output is explicitly not guaranteed to match that of the input.  Resets, detailed below, allow you to specify that certain blocks preceed all others and @media blocks necessarily follow all others but any other reliance on ordering is considered a bad practice.

Likewise, overriding rules in blocks is an explicit operation rather than a function of declaration order, the specifics are detailed below.

He wants your CSS structure to be explicit.

Resets

More natively supports the concept of a “reset”.  Blocks contained between @reset{…} will always be placed at the top of generated CSS file, and become available for inclusion via reset includes.

@reset{
  h1 { margin: 0; }
}
.class h1 { color: blue; }

becomes

h1 { margin: 0; }
.class h1 { color: blue; }

Blocks within a @reset block cannot use @reset includes or selector includes, but can use mixins.  Reset blocks are also not referenced by selector includes.

@reset includes let you say "reset this block to the default stylings", practically this means including any properties defined within a @reset{...} in the block with the @reset include.

@reset {
  h1 { margin: 0; }
}
.class h1 { color: blue; @reset(h1); }

becomes

h1 { margin: 0; }

.class h1 { color:blue; margin: 0; }

It is not necessary to specify the selector to reset to, an empty @reset() will reset to the selector of the block that contains it.  Note that for nested blocks this will be the inner most blocks selector.

@reset {
  :hover { color: red; }
  h1 { margin: 0; }
}
h1 {
  @reset(); 
  line-height: 15px; 
}
a {
  color: blue;
  &:hover {
    font-weight: bold;
    @reset();
  }
}

becomes

:hover { color: red; }
h1 { margin: 0; }
h1 { margin: 0; line-height: 15px; }
a { color: blue; }
a:hover { font-weight: bold; color: red; }

Properties included by a @reset() (with or without optional selector) will never override those defined otherwise in a block.

@reset { a { color: red; height: 10px; } }
a { @reset(); color: blue; } }

becomes

a { color: red; height: 10px; }
a { color: blue; height: 10px; }

The intent behind resets is to make it easier to define a style's "default element stylings" and subsequent reset to them easily.

You know what's cool? Not generating your sprite files independent of the CSS that uses them.

Sprites

If you include a non-trivial number of images on  a site, you really ought to be collapsing them into sprites.  There are plenty of tools for doing this out there, but tight integration with CSS generation (if you've already decided to compile your CSS) is an obvious win.

To that end, More lets you generate sprites with the following syntax:

@sprite('/img/sprite.png'){
  @up-mod = '/img/up.png';
  @down-mod = '/img/down.png';
}

This declaration creates the sprite.png from up.png and down.png, and adds the @up-mod and @down-mod mixins to the file.  Mixins created from sprites are no different than any other mixins.

For example:

.up-vote {
  @up-mod();
}

could compile to

.up-vote {
  background-image: url(img/sprite.png);
  background-position: 0px 0px;
  background-repeat: no-repeat;
  width: 20px;
  height: 20px;
}

For cases there are other properties that you want to include semantically "within" a sprite, these generated mixins take one mixin as an optional parameter.  The following would be valid, for example.

.down-vote {
  @down-mod(@myMixin);
}

Finally, there are command line options for running executable on generated sprite files.  It's absolutely worthwhile to squeeze the last few bytes out of a PNG file you'll serve a million times, but actually implementing such a compression algorithm is outside the scope of More.  While this can be fairly easily hacked together, as a built-in I hope it serves as a "you should be doing this" signal to developers.  I personally recommend PNGOUT for this purpose.

Include Selectors

Sometimes, you just want to copy CSS around.  This can be for brevity's sake, or perhaps because you want to define a set of selectors in terms of some other selector.   To do that with More, just include the selector in @() like so:

div.foo { color: #abc; }
div.bar {
  background-color: #def;
  @(div.foo);
}

This syntax works with more complicated selectors as well, including comma delimited ones, making it possible to include many distinct blocks in a single statement.  It is not an error if a selector does not match any block in the document.

Selector includes are one of the last things to resolve in a More document, so you are copying the final CSS around.  @(.my-class) will copy the final CSS in the .my-class block, but not any of the mixin invokations, nested blocks, or variable declarations that went into generating it.

For example:

.bar {
  foo: @c ?? #eee;
  &:hover {
    buzz: 123;
  }
}
.fizz {
  @c = #aaa;
  @(.bar);
}

Will generate:

.bar { foo: #eee; }
.bar:hover { buzz: 123; }
.fizz { foo: #eee; }

As @(.bar) copies only the properties of .bar in the final document.

Not stolen, inspired. *cough*

LESS Inspired Bits

That's it for the really radical new stuff, most of the rest of More is heavily inspired by (though rarely syntactically identical to) LESS.  Where the syntax has been changed, it has been changed for reasons of clarity (at least to me, clarity is inherently subjective).

More takes pains to "stand out" from normal CSS, as it is far more common to be reading CSS than writing it (as with all code).  As such, I believe it is of paramount importance that as little of a file need be read to understand what is happening in a single line of More.

This is the reason for heavy use of the @ character, as it distinguishes between "standard CSS" with its very rare at-rules and More extensions when scanning a document.  Likewise, the = character is used for assignment instead of :.

Variables

More allows you to define constants that can then be reused by name throughout a document, they can be declared globally as well as in blocks like mixins (which are detailed below).

@a = 1;
@b = #fff;

These are handy for defining or overriding common values.

Another example:

@x = 10px;
img {
  @y = @x*2;
  width: @y;
  height: @y;
}

It is an error for one variable to refer to another in the same scope before it is declared, but variables in inner scopes can referrer to those in containing scopes irrespective of declaration order.  Variables cannot be modified once declared, but inner variables can shadow outer ones.

Puppies, (debatably) not annoying.

Nested Blocks

One of the annoying parts of CSS is the repetition when declaring a heirarchy of selectors.  Selectors for #foo, #foo.bar, #foo.bar:hover, and so on must appear in full over and over again.

More lets you nest CSS blocks, again much like LESS.

#id {
  color: green;
  .class {
    background-color: red;
  }
  &:hover {
    border: 5px solid red;
  }
}

would compile to

#id {
  color: green;
}
#id .class {
  background-color: red;
}
#id:hover {
  border: 5px solid red;
}

Note the presense of LESS's & operator, which means "concat with parent".  Use it for when you don't want a space (descendent selector) between nested selectors; this is often the case with pseudo-class selectors.

Mixins

Mixins effectively lets you define functions which you can then include in CSS blocks.

@alert(){
  background-color: red;
  font-weight: bold;
}
.logout-alert{
  font-size: 14px;
  @alert();
}

Mixins can also take parameters.

@alert(@c) { color: @c; }

Parameters can have default values, or be optional altogether.   Default values are specified with an =<value>, optional parameters have a trailing ?.

@alert(@c=red, @size?) { color: @c; font-size: @size; }

Mixin's can be passed as parameters to mixins, as can selector inclusions.

@outer(@a, @b) { @a(); @b(green); }
@inner(@c) { color: @c; }
h1 { font-size: 12px; }
img { @outer(@(h1), @inner); }

Will compile to

h1  { font-size: 12px; }
img { font-size: 12px; color: green; }

As in LESS, the special variable @arguments is bound to all the parameters passed into a mixin.

@mx(@a, @b, @c) { font-family: @arguments; }
p { @mx("Times New Roman", Times, serif); }

Compiles to

p { font-family: "Times New Roman", Times, serif; }

Parameter-less mixins do not define @arguments, and it is an error to name any parameter or variable @arguments.

Puppy function: distract reader from how much text is in this post.

Functions

More implements the following functions:

  • blue(color)
  • darken(color, percentage)
  • desaturate(color, percentage)
  • fade(color, percentage)
  • fadein(color, percentage)
  • fadeout(color, percentage)
  • gray(color)
  • green(color)
  • hue(color)
  • lighten(color, percentage)
  • lightness(color)
  • mix(color, color, percentage/decimal)
  • nounit(any)
  • red(color)
  • round(number, digits?)
  • saturate(color, percentage)
  • saturation(color)
  • spin(color, number)

Most of these functions are held in common with LESS, to ease transition.  All functions must be preceded by @, for example:

@x = #123456;
p {
   color: rgb(@red(@x), 0, 0);
}

Note rgb(...), hsl(..), and other CSS constructs are not considered functions, but they will convert to typed values if used in variable declarations, mixin calls, or similar constructs.

In String Replacements

More strives to accept all existing valid CSS, but also attempts to parse the right hand side of properties for things like math and type information.  These two goals compete with each other, as there's a lot of rather odd CSS out there.

img {
  filter: progid:DXImageTransform.Microsoft.MotionBlur(strength=9, direction=90);
  font-size: 10px !ie7;
}

It's not reasonable, in my opinion, to expect style sheets with these hacks to be re-written just to take advantage of More, but it can't parse them either.

As a compromise, More will accept any of these rules but warn against their use.  Since a fancier parsing scheme isn't possible, a simple string replacement is done instead.

img {
  filter: progid:DXImageTransform.Microsoft.MotionBlur(strength=@strength, direction=90);
  font-size: @(@size * 2px) !ie7;
}

The above demonstrates the syntax.  Any variables placed in the value will be simply replaced, more complicated expressions need to be wrapped in @().

Although this feature is intended mostly as a compatibility hack, it is also available in quoted strings.

Other, Less Radical, Things

There are a couple of nice More features that aren't really LESS inspired, but would probably fit right in a future LESS version.  They're not nearly as radical additions to the idea of a CSS compiler.

Dressing up your puppies, optional.

Optional Values

Mixin invocations, parameters, variables, and effectively whole properties can be marked "optional".

.example {
  color: @c ?? #ccc;
  @foo()?;
}

This block would have a color property of #ccc if @c were not defined, making overrides quite simple (you either define @c or you don't).  Likewise, the foo mixin would only be included if it were defined (without the trailing ? it would be an error).

A trailing ? on a selector include will cause a warning, but not an error.  It doesn't make sense, as selector includes are always optional.

Entire properties can be optional as follows:

.another-example {
  height: (@a + @b * @c)?;
}

If any of a, b, or c are not defined then the entire height property would not be evaluated.  Without the grouping parenthesis and trailing ? this would result in an error.

Overrides

When using mixins, it is not uncommon for multiple copies of the same property to be defined.  This is especially likely if you're using mixins to override default values.  To alleviate this ambiguity, it is possible to specify that a mixin inclusion overrides rules in the containing block.

@foo(@c) { color: @c; }
.bar {
  color: blue;
  @foo(red)!;
}

This would create a .bar block with a single color property with the value of "red".  More will warn, but not error, when the same property is defined more than once in a single block.

You can also force a property to always override (regardless of trailing ! when included) by using !important.  More will not warn in these cases, as it's expressed intent.

You can also use a trailing ! on selector includes, to the same effect.

Puppies With Hats

Values With Units

Values can be typed, and will be correctly coersed when possible and result in an error otherwise.  This makes it easier to catch non-nonsensical statements.

@mx(@a, @b) { height: @a + @b; }
.foo{
  @mx(12px, 14); // Will evaluate to height: 26px;
  @mx(1in, 4cm); // Will evaluate to height: 6.54cm; (or equivalent)
  @mx(15, rgb(50,20,10)); // Will error
}

Similarly, any function which expects multiple colors can take colors of any form (hsl, rgb, rgba, hex triples, or hex sextuples).

When needed, units can be explicitly stripped from a value using the built in @nounit function.

Includes

You can include other More or CSS files using the @using directive.  Any included CSS files must be parse-able as More (which is a CSS super-set, although a strict one).  Any variables, mixins, or blocks defined in any includes can be referred to at any point in a More file, include order is unimportant.

@using can have a media query, this results in the copied blocks being placed inside of a @media block with the appropriate query specified.  More will error if the media query is unparseable.

At-rules, which are usually illegal within @media blocks, will be copied into the global context; this applies to mixins as well as @font-face, @keyframes, and so on.  This makes such at-rules available in the including file, allowing for flexibility in project structure.

More does allow normal CSS @imports.  They aren't really advisable for speed reasons.  @imports must appear at the start of a file, and More will warn about @import directives that had to be re-ordered as that is not strictly compliant CSS in the first place.

CSS3 Animations

More is aware of the CSS3 @keyframes directive, and gives you all the same mixin and variable tools to build them.

@mx(@offset) { top: @offset * 2px; left: @offset + 4px; }
@keyframes my-anim {
  from { @mx(5); }
  to { @mx(15); }
}

Compiles to

@keyframes my-anim {
  from { top: 10px; left: 9px;}
  to { top: 30px; left: 19px; }
}

It is an error to include, either via a mixin or directly, a nested block in an animation block.  More will also accept (and emit unchanged) the Mozilla and Webkit prefixed versions of @keyframes.

Variables can be declared within both @keyframes declarations and the inner animation blocks, the following would be legal More for example.

@keyframes with-vars {
  @a = 10;
  from { 
    @b = @a * 2;
    top: @b + 5px;
    left: @b + 3px;
  }
  to { top: @a; left: @a; }
}

It is an error to place @keyframes within @media blocks or mixins.  As with @media, @keyframes included via a @using directive will be pulled into the global scope.

More Is A CSS Superset

More strives to accept all existing CSS as valid More.  In-string replacements are a concession to this, as is @import support; both of which were discussed above.  @charset, @media, and @font-face are also supported in pursuit of this goal.

@charset may be declared in any file, even those referenced via @using.  It is an error for more than one character set to be referred to in a final file.  More will warn if an unrecognized character set is used in any @charset declaration.

@media declarations are validated, with More warning on unrecognized media types and erroring if no recognized media types are found in a declaration.  Blocks in @media statements can use mixins, but cannot declare them.  Selector includes first search within the @media block, and then search in the outer scope.  It is not possible for a selector include outside of a @media block to refer to a CSS block inside of one.

By way of example:

img { width: 10px; @(.avatar); }
p { font-color: grey; }
@media tv {
  p { font-color: black; }
  .avatar { border-width: 1px; @(p); }
  .wrapper { height:10px; @(img); }
}

Will compile to:

img { width: 10px; }
p { font-color: grey; }
@media tv {
  p { font-color: black; }
  .avatar { border-width: 1px; font-color: black; }
  .wrapper { height:10px; width: 10px; }
}

@font-face can be used as expected, however it is an error to include any nested blocks in a @font-face declaration.  More will warn if a @font-face is declared but not referred to elsewhere in the final CSS output; this is generally a sign of an error, but the possibility of another CSS file (or inline style) referring to the declared font remains.  More will error if no font-family or src rule is found in a @font-face block.

The pun is the highest form of humor.

Minification

More minifies it's output (based on a command line switch), so you don't need a separate CSS minifier.  It's not the best minifier out there, but it does an OK job.

In particular it removes unnecessary white space, quotation marks, and reduces color and size declarations; "green" becomes #080 and "140mm" become "14cm" and so on.

A contrived example:

img
{
  a: #aabbcc;
  b: rgb(100, 50, 30);
  c: #008000;
  d: 10mm;
  e: 1.00;
  f: 2.54cm;
}

Would become (leaving in white space for reading clarity):

img 
{
  a: #abc;
  b: #64321e;
  c: green;
  d: 1cm;
  e: 1;
  f: 1in;
}

This minification is controlled by a command line switch.

Cross Platform

For web development platform choice is really irrelevant and acknowledging that More runs just fine under Mono, and as such works on the Linux and Mac OS X command lines.

More Isn't As Polished As LESS

I always feel a need to call this out when I push a code artifact, but this isn't a Stack Exchange thing.  This is a "Kevin Montrose screwing around with code to relax" thing.  I've made a pretty solid effort to make it "production grade", lots of tests, converted some real CSS (some of which is public, some of which is unfortunately not available), but this is still a hobby project.  Nothing is going to make it as solid as years of real world usage, which LESS and Sass have and More doesn't.

Basically, if you're looking for something to simplify CSS right now and it cannot fail go with one of them.  But, if you like some of what you read and are looking to play around well...

Give it a Spin

You can also check out the code on Github.


4 Comments on “More, a CSS compiler”

  1. TheSisb says:

    I too despaired when I heard about the CSS variables proposition.
    I always code in plain old CSS, but I’ll give More a shot sometime because you seem cool.

  2. Looks very interesting and while I have not yet read the full post, I have enjoyed every puppy picture.

  3. emini_guy says:

    Puppies have stolen the show. But I may return to the article later. Bookmarked it.

  4. Pit says:

    It’s pleasure to learn CSS with these nice pictures :-)

Follow

Get every new post delivered to your Inbox.