Write a CSS Parser

As you know, most of the websites are using CSS to customize the appearance of their pages so Dragonfly needs a way to parse the CSS in order to apply the styles to the DOM.

As usual, I want a parser written in Dart. It’s something essential to me because I also want to show that Dart is an efficient programming language and we can write efficient programs.

First, I checked the existing solutions. It’s not very complicated, there’s only one package to parse CSS and it’s made by the Dart team. I tried it and it worked! But the package doesn’t have documentation and the last update was 15 months ago. It returned me something that should be the CSS’s AST but I didn’t know what to do with it and, to be honest, I haven’t tried to find a solution too.

So as every software engineer, I said I can rewrite it from scratch. It shouldn’t be very hard and it probably would be better. How could I be so wrong!

My first very basic parser used petitparser. It’s a package that allows us to write parsers very quickly. For example, xml and puppeteer implement a parser made with petit parser. After a few hours of work, I had a parser that could return me all the CSS rules! It was working fast or at least, it wasn’t very slow.

Unfortunately, I wasn’t happy with this implementation because writing a parser is something new to me and the last time I had this exercise was during university where we had to write some compiler with lex and yacc and I also skipped most of the courses… So I lacked the experience and I had the feeling that what I did with petitparser was bad and not maintainable. I remember that if the CSS was wrong, the parser would throw an exception. It’s very bad! If we take the following code :

body {
	color: red
}

The color declaration misses the final semicolon but it’s still usable! The browser cannot refuse to parse the full style sheet because of one error. The UX would be horrible and the web wouldn’t be a very pleasant experience. Last thing, I wanted to know the exact position of a declaration in the style sheet. I saw that Firefox was doing it and I want that too.

Now the plan is simple. Write my own CSS parser from scratch! My parser is very inspired by the one made by the Dart team. You can find it here. It’s more complete and error-prone than my current solution. I also want to say that I’m nothing of a parser/compiler expert. It’s a new experience and that’s also why I’m writing it from scratch. My parser could contain errors.

Regular expressions?

Let’s take a basic style sheet :

body {
	color: red;
}

p {
	margin: 0;
}

At first sight, it’s very hard to analyze this code. How do we know that body is a selector, color a property and red its value? We could use RegEx!

RegExp(r"(\w)+\s*{([^}]*)}")

This regular expression could be used to detect a rule and capture its content. As you can see, it’s not readable. It’s 16 characters but I need several seconds to understand what it’s doing. The expression is incomplete. It wouldn’t recognize h1. The updated regex would be

RegExp(r"(\w|\d)+\s*{([^}]*)}")

Worse… now with the wild star selector, the . class selector, the # id selector..

RegExp(r"(\.|\#)?(\w|\d|\*)+\s*{([^}]*)}")

You understand the problem. We could split the expression into multiple smaller strings that we merge in the final expression to improve the readability. Maybe it just needs better organization but it’s not the only bad thing about regex in this case. They can’t handle nor detect errors.

bod%y { ... }

The previous regex is not able to detect this wrong rule and to report that it’s wrong. It wouldn’t recognize a media query :

@media screen and (min-width: 900px) {
  /* For the regex, the following rule is a normal rule. */
  article {
    padding: 1rem 3rem;
  }
}

Other reasons would be that they can have bad performance like this infamous incident from StackOverflow.

In short, regular expressions are not very maintainable, can have performance issues, can’t report parsing errors and may lack context.

The lexer/tokenizer

The tokenizer is a component that takes a text input and returns a list of tokens. A token is a piece of information that describes a part of the input. It can be a number, punctuation, commas, words, special keywords (like if or else in Dart) etc… It simplifies the parsing and the parsing can be done with linear time complexity.

For the following CSS:

body {
  margin: 0;
}

The tokens will be :

IDENTIFIER ; WHITESPACE ; LEFTBRACE ; WHITESPACE ; IDENTIFIER ; COLON ; WHITESPACE ; NUMBER ; SEMICOLON ; RIGHTBRACE

Or with the CSS structure :

IDENTIFIER WHITESPACE LEFTBRACE
   IDENTIFIER COLON WHITESPACE NUMBER SEMICOLON
RIGHTBRACE

It looks easier now to parse, right? So how to reach this result?

First, let’s define an enum TokenKind that defines all the possible kinds of tokens.

enum TokenKind {
	identifier,
	endOfFile,
	whitespace,
	comment,
	number,
	leftBrace,
	...
}

Then we need a Token class.

class Token {
	final TokenKind tokenKind;
	final int startPosition;
	final int endPosition; 
	
	Token(
		this.tokenKind,
		this.startPosition,
		this.endPosition,
	);
}

Each token must have a type of token. I also define startPosition and endPosition. They are cursors to store the token position in the style sheet. It makes the tokens lighter. An int in Dart is almost always 8 bytes and a string is between 2 and 4 bytes per character. So we have 16 bytes of int. I think it’s worth it because a lot of token content are very long like comments or some special properties.

It would probably be worth it to have int32 in Dart. We will never have 9 223 372 036 854 775 807 characters in a CSS file.

Whatever! Now we can create our Tokenizer class.

class Tokenizer {
	int _index = 0;
	int _startIndex = 0;	  
	
	final String css;
	
	Tokenizer({required this.css});
}

The class needs a css String. That’s the CSS we will tokenize. We also need _startIndex to know the offset of a new token and _index is a simple cursor.

Our public interface is :

List<Token> tokenize() {
	final tokens = <Token>[];

	Token currentToken = next();
	tokens.add(currentToken);

	while (currentToken.tokenKind != TokenKind.endOfFile) {
		currentToken = next();
		tokens.add(currentToken);
	} 

	return tokens;
}

It has a list of Token that will be returned at the end of the method execution. Then it calls the next() method. We will see it later in detail. We add this token to the list and we start a loop. This loop creates tokens until it produces an END OF FILE token. It’s the last token to be created. If the CSS is empty, we only have it.

Token next() {
	_startIndex = _index;
	var char = _getNextChar();
	
	switch (char) {
	  case TokenChar.endOfFile:
		return _closeToken(TokenKind.endOfFile);
	  default:
		return _closeToken(TokenKind.endOfFile);
	}
}

Token _closeToken(TokenKind kind) {
	return Token(kind, _startIndex, _index);
}

int _getNextChar() {
	if (_index < css.length) {
		return css.codeUnitAt(_index++);
	} else {
		return 0;
	}
}

This method is the one producing the tokens. Each call we call it, it returns a token. First, it saves the offset of the current cursor and then it requests the next character with _getNextChar(). It’s a very simple method that returns 0 if we have already read all the string otherwise, it returns the character at the index position and increments it! After writing this, maybe I should rename it _getCurrentChar()

We have the character so we pass it into a switch. TokenChar is a class with constants like this static const endOfFile = 0;. It contains all the special characters we expect. We could use if our grammar needs it.

If the character matches one of our expected characters, we call a special function to handle this token if it’s a multicharacter token or otherwise, we close the token. It creates the appropriate token with the start and end positions.

We could extend our switch like with more cases :

  case TokenChar.leftParenthesis:
	return _closeToken(TokenKind.leftParenthesis);
  case TokenChar.rightParenthesis:
	return _closeToken(TokenKind.rightParenthesis);
  case TokenChar.leftBracket:
	return _closeToken(TokenKind.leftBracket);
  case TokenChar.rightBracket:
	return _closeToken(TokenKind.rightBracket);
  case TokenChar.leftBrace:
	return _closeToken(TokenKind.leftBrace);
  case TokenChar.rightBrace:
	return _closeToken(TokenKind.rightBrace);
  case TokenChar.semicolon:
	return _closeToken(TokenKind.semicolon);
  case TokenChar.colon:
	return _closeToken(TokenKind.colon);
  case TokenChar.comma:
	return _closeToken(TokenKind.comma);

We can tokenize every single character but what about something like a comment? A comment is CSS has this format /* ... */. We need a slash then an asterisk to know that we are reading a comment.

switch(...) {
  case TokenChar.slash:
    if (_maybeEatChar(TokenChar.asterisk)) {
      return _consumeComment();
    }
    return _closeToken(TokenKind.slash);
}

bool _maybeEatChar(int char) {
  if (_index < css.length) {
    if (css.codeUnitAt(_index) == char) {
      _index++;
      return true;
    } else {
      return false;
    }
  } else {
    return false;
  }
}

I changed a bit the case for the slash. I want to know if the next character is an asterisk. The method _mayEatChar() checks if the next character is the one I expect and returns true. It only increments the index cursor when it matches. It’s very useful when we are expecting a bunch of special characters like --var or <!--.

That’s more or less enough for now. Later, we can add actual error tokens, handle special cases etc. This implementation is quite basic in comparison with the one from the csslib package.

The parser

Now that we have all the tokens, we need to analyze them. We need to build an AST (Abstract Syntax Tree). It’s a representation of our style sheet. We could regenerate our style sheet using the AST. It would allow us to minify or prettify it.

Some possible optimizations :

.card {
	color: red;
}

.title {
	color: red;
}

/* would be  */

.card, .title {
	color: red;
}

/* ------------- */

.card {
	color: #ffffff;
}

/* would be */

.card {
	color: #fff;
}

/* ------------- */

body {
	margin-top: 10px;
	margin-right: 10px;
	margin-bottom: 10px;
	margin-left: 10px;
}

/* would be */

body {
	margin: 10px;
}

This AST will be useful because later, I’ll be able to use it to generate my CSSOM.

Let’s start by writing a tree class.

abstract class TreeNode {
	final int start;
	final int end;

	TreeNode(this.start, this.end);
	
	dynamic getValue(String text) {}
}

Like for the token, the start and end are here to get the value from the CSS file. The getValue(...) will be useful later when we want to minify or create the CSSOM. The return type is dynamic because it could return a string, a number, a color, a URI etc…

Now, we can extend this class for each kind of node of our AST. We could have Stylesheet to represent all the CSS, Comment to represent a comment, Block to represent something like this { ... }.

Some of them will have children like StyleSheet or Block. Some will not need anything else like Comment and some like Rule could have more control over the accepted children.

Let’s start with StyleSheet and Comments. A style sheet can only be composed of comments.

class StyleSheet extends TreeNode {
  final List<TreeNode> children;

  StyleSheet(super.start, super.end, this.children);
}

class Comment extends TreeNode {
  Comment(super.start, super.end);
}

Nothing special to say. It’s straightforward. Now we can write our parser:

class CssParser {
  late Token _firstToken;
  late final Tokenizer _tokenizer;

  CssParser(String css) : _tokenizer = Tokenizer(css: css);

  StyleSheet parse() {
    final rules = <TreeNode>[];

    return StyleSheet(0, 0, rules);
  }
}

The CSSParser accepts a CSS string that it will directly pass to the tokenizer. It also has a _firstToken that has a similar role than the _index of the tokenizer. It’s a “pointer” to the current token to analyze.

You can also notice the public method parse(). It’s the main method of our class. It will parse each token and produce a bunch of TreeNode. I expect a StyleSheet as the return type.

Now let’s create a loop to consume the tokens.

StyleSheet parse() {
  final rules = <TreeNode>[];

  _firstToken = _tokenizer.next();

  while (_firstToken.tokenKind != TokenKind.endOfFile) {
    if (_firstToken.tokenKind == TokenKind.comment) {
      rules.add(_consumeComment());
    }

    _firstToken = _tokenizer.next();
  }

  return StyleSheet(0, 0, rules);
}

Comment _consumeComment() {
	return Comment(_firstToken.startPosition, _firstToken.endPosition);
}

We ask the tokenizer to return a token and we start a loop that only stops when we encounter an End Of File token (so at the very end). If the token is a comment token, we consume it. It returns a simple Comment node will the start and end of the string.

The parser has a solid base. It needs to handle more tokens but that’s more work.

To give you an example of what I expect at the end :

body {
	color: red;
}

Would generate the following AST :

StyleSheet
|
|-Rule
|--SelectorList
|----Selector
|------TypeSelector -> value = body
|--Block
|---Declaration -> property = color
|-----|-value = Identifier -> name = red

Conclusion

Writing a parser is hard. I’m lucky because CSS is a simple language and there’s no ambiguity so I didn’t suffer a lot writing this one. It’s a very funny experience and I learned a lot about parsing.

I was thinking about how to make this implementation faster. I think the tokenizer could be run inside another isolate. It would produce all the tokens and send them to the main isolate to be consumed by the parser. I don’t want to do early optimization and I would need benchmarks. Perhaps the time to set up the isolate is way too important. Maybe it could be done only with very big CSS like Bootstrap or Tailwind.

I was talking a lot about why this kind of parser is so great for errors and I didn’t show any examples. Very quickly, let’s take body $ { color: red;}. We would expect to have an IDENTIFIER, a WHITESPACE, a LEFT BRACE etc… In my example, $ is not expected. The parser would add an error in the parser errors attribute with the position of the token, what was expected etc… Then it will try to continue to parse again the following tokens to still load some CSS.

If you want to explore more code, I really recommend reading the repo of csslib. For now, my implementation is a bad copy of csslib. I think that with my explanation, you would navigate through the repo quickly.