Getting CSS Code in Order: Yandex Experience

Web Development

Hey there,

I’m currently working at the front-end of a huge project – Yandex search results. As any other web project, we have a huge amount of CSS code and a considerable team interacting with it.

When a lot of people write and edit CSS code with the help of various tools, it can become really complex and inconsistent. For instance, someone prefers writing vendor prefixes in one order, the other one prefers writing it in a different way. Some of them will use brackets round url, others won’t. When fixing some bug, someone could write, for example, position: relative at the beginning of the properties block without noticing that there’s already position: absolute somewhere between color and box-shadow. And he will keep wondering, why it’s not working.

But despite the fact that we write the code in different ways, there’s a perfect order in our repository: CSS code is absolutely consistent and looks great. The whole of it.

In this article you will read about the ways to achieve it.

First Steps

If we want CSS code to be written according to the code style, the first thing coming to mind, is some big boss to say: “Write it this way and strict to that” and then detect mismatches during the code review. Sounds great, but not in practice.

  1. It’s hard for a person to memorize the strictly recommended order of all the CSS properties and never make mistakes;
  2. Code review becomes a set of updates according to the code style, while we would like it to contain discussions about logic and architecture;
  3. All bugs should be fixed manually. All mismatches should be specified in some document.

Anyway, it does not look good, takes plenty of time and frustrates developers.

To satisfy developers, we had to look for another solution. And we’ve found it!

A Bit about Robots

There used to be an old robot — CSScomb, that solved the most important part of our task. It sorted properties in a specified order. But it also had some drawbacks:

  1. It could not do anything except the sort, though we wanted it to remove brackets when needed, sort vendor prefixes in ascending order;
  2. It was written in PHP and regular expressions and did not provide any abilities to be extended.

Though PHP is more popular than C++ in the sphere of JS programmers, it’s not the perfect solution.

Therefore, a member of our team has decided to create a tool without these drawbacks. A tool that could execute plenty of useful things, would be written in JS and could be easily extended. Despite the fact that Yandex employees take an active part in development, CSScomb.js is an open source project that has gone far beyond its limits.

Please, welcome, CSScomb.js – a new version of the old CSScomb that can perform plenty of cool things.

CSSComb

How does It Look Like?

Let’s take a look at the current development process by an example. Suppose we want to commit the following code for purely educational purposes:

.block {
-webkit-box-shadow: 0 0 0 1px rgba(0, 0, 0, .1);
-moz-box-shadow: 0 0 0 1px rgba(0, 0, 0, .1);
box-shadow: 0 0 0 1px rgba(0, 0, 0, .1);
z-index: 2;
position: absolute;
height: 2px;
width: 2px;
color: #FFFFFF;
}

It does not really look good, and is not even formed according to the code style. Thus, when trying to commit, we will see the following:

% git commit block.css -m "Add My awesome css"                                                                                   
Code style errors found.
! block.css

We will not be able to commit such CSS file, since there’s a pre-commit hook in git repository that checks all changes of CSS files with the help of CSScomb. It operates in linter mode. The hook is quite smart and checks only the files we are going to commit, not all of them.

At seeing this message, we do not get upset, but ask CSScomb.js to fix everything:

% csscomb block.css Well, it’s a different story now:

.block
{
    position: absolute;
    z-index: 2;
    width: 2px;
    height: 2px;
    color: #fff;
    -webkit-box-shadow: 0 0 0 1px rgba(0, 0, 0, .1);
       -moz-box-shadow: 0 0 0 1px rgba(0, 0, 0, .1);
            box-shadow: 0 0 0 1px rgba(0, 0, 0, .1);
}

It has done quite many of small fixes in the code. Transferred a bracket to another line, switched the color to the lower case, sorted vendor prefixes in ascending order and sorted properties in the correct order. Now this code will pass the automatic check successfully and will move on to further review of its functional qualities.

Thus, a developer does not have to think about the ways of writing CSS properly according to the code style. He just writes it the way he likes. All other things happen automatically. That’s exactly what we aimed at.

Structure

Such system consists of two parts: CSScomb itself and the pre-commit hook that uses it. I’ll tell you about the hook in the next part. Now, let’s review the CSSComb.js structure in details.

There are two things at the heart of CSScomb.js: a plug-in system and gonzales-pe CSS parser. The latter is a really cool thing that can operate not only with pure CSS, but also with preprocessors like Sass or LESS.

Let’s begin with the parser. It is applied on CSS code and builds AST on the basis of it. For example, for

.block
{
     position: absolute
}

AST will look like the following:

[ "stylesheet",
  [ "ruleset",
    [ "selector",
      [ "simpleselector",
        [ "class", [ "ident", "block" ] ],
        [ "s", "\n" ]
      ]
    ],
    [ "block",
      [ "s", "\n    " ],
      [ "declaration",
        [ "property", [ "ident", "position" ] ],
        [ "propertyDelim" ],
        [ "value", [ "s", " " ], [ "ident", "absolute" ] ]
      ],
      [ "s", "\n" ]
    ]
  ],
  [ "s", "\n" ]
]

This format is very lengthy and is not really comfortable for reading, but it’s good for the automatic processing that is executed by plugins. They are in charge of all further CSS conversions and operate not with the CSS code directly, but with the AST that is much simpler to handle. A separate config file has all the information as for the plugins to be used and their settings.

In the mentioned above example we did not use a semicolon after absolute. The plugin, which is responsible for detecting and fixing the semicolons, noticed and fixed it via AST modification (not to duplicate a big part of the code, I shortened the tree a bit)

...
    [ "block",
      [ "s", "\n    " ],
      [ "declaration",
        [ "property", [ "ident", "position" ] ],
        [ "propertyDelim" ],
        [ "value", [ "s", " " ], [ "ident", "absolute" ] ]
      ],
      >>>[ "declDelim" ],<<<
      [ "s", "\n" ]
...

Once all the plugins have done their job, AST converts back to CSS with the help of gonzales-pe, and the missed semicolon turns out to be in a proper place:

.block
{
     position: absolute;
}

How do We Start Using It?

The First Step

Add CSScomb.js to the project dependencies. If npm is used, add it to devDependencies in package.json:

{
  ...
  "devDependencies": {
    ...
    "csscomb": "2.0.4",
    ...
  }
  ...
}

The Second Step

To make plugins normalize the code to the form we need, place a config file in the project root. Here you will find the details of each plugin options.

CSScomb.js can do it automatically to some extent, as it can generate the config by using some existing CSS as the basis.

csscomb -d example.css > .csscomb.json We can configure all plugins in a similar way. The only exception is properties sort. We will have to either write it ourselves, or copy from somewhere.

The Last Step

We should make CSScomb.js to be launched (in a linter mode) before the commit on equal terms with jscs/jshint-groups or other tools that are usually run before the commit. In case of git and linux/BSD use, it can look something like this:

#!/usr/bin/env bash
PATCH_FILE="working-tree.patch" 
NPM_BIN="./node_modules/.bin"
function cleanup {
    exit_code=$?
    if [ -f "$PATCH_FILE" ]; then
        patch -p0 < "$PATCH_FILE"
        rm "$PATCH_FILE"
    fi
    exit $exit_code
}
#Cleaning up before exiting the script
trap cleanup EXIT SIGINT SIGHUP
# Create a patch file
git diff > "$PATCH_FILE" --no-prefix
#Flush the unstaged changes
git checkout -- .
# get a list of files (containing changes) that we want to commit 
git_cached_files=$(git diff --cached --name-only --diff-filter=ACMR | xargs echo)
if [ "$git_cached_files" ]; then
    #Set CSScomb.js
    $NPM_BIN/csscomb -v -l $git_cached_files || exit 1
fi

This hook has an interesting peculiarity. Why not to run CSSComb.js in the correction mode at once and then commit automatically? In most cases it does work. But problems will turn up, when there are several corrections in a file, but we want to commit one of them (git add -p) only. In this case, if we find a mistake in the file version to be committed, the following unpleasant situations will take place:

  1. We can run CSScomb.js for the file version to be committed and sometimes get conflicts when applying the patch;
  2. Or, you can run CSScomb.js on the current file. But there can be anything in the updates we do not want to commit for now, including the wrong code.

It’s annoying when a file completely changes itself. Therefore, we think that developers specifically should start the CSScomb.js.

Done!

That’s it. Now, CSScomb.js will not allow to commit bad CSS, which allows to normalize the code to the necessary format with the help of one command only.

That’s how a simple idea and not a simple tool help to keep the code in order. They also save developers’ time and nerves.

We can use it in many projects. If you’re missing some basic functionality or plugins, you can always add issue, or contribute the necessary things by yourself.

Comments

  1. Wow I knew some CSS linters and such but csscomb looks pretty nice, I integrated it into my workflow with no problem at all, I was using Gulp.Nice post and thanks for sharing such a nice tool :)
  2. How do you like gulp? We are currently using Grunt, but there were some talks about gulp around, so just curious.
  3. I tried Grunt, I didn’t really use it much as to make a fair comparison but personally Gulp feels much easier to use, Grunt is just a bunch of options which can be intimidating and quite hard to understand at first. Gulp on the other hand is lightweight, and it’s just Javascript, it feels like I can get more done with fewer lines, even though that might not be true. It also feels more powerful as you can write conditionals to run one part of the build process or the other in plain Javascript. That’s just my opinion though, remember I haven’t used Grunt much, only for a few personal frontend projects.
  4. I use CSSComb as part of my workflow. I try and stick to the order as much as possible and allow csscomb to reformat when I miss it.I also have 2 tasks. One that runs csscomb everytime my sass is compiled. This makes sure my,CSS output us always clean.The other task reorganizes the properties in my .scss files. This I run manually whenever required. It’s annoying to work on a file and have it rearrange properties when you hit savePersonally gulp worked faster and better than grunt. Esp when running only for files that changed. Would love your opinion on thatAlso, is this project replacing the old one or is it just a better fork? Great work..thanks for the write up :)
  5. Excuse typos an on the run and typing from mobile
  6. CSSComb.js is basically a new version of CSSComb, which has much more features. It can also «fix» your CSS, not only sort. In addition, it was written with Node.js, which means that engineers have much more chances to use it in their projects (comparing to an old, PHP-based, CSSComb)
1,128

Ropes — Fast Strings

Most of us work with strings one way or another. There’s no way to avoid them — when writing code, you’re doomed to concatinate strings every day, split them into parts and access certain characters by index. We are used to the fact that strings are fixed-length arrays of characters, which leads to certain limitations when working with them. For instance, we cannot quickly concatenate two strings. To do this, we will at first need to allocate the required amount of memory, and then copy there the data from the concatenated strings.