Regexp vs Parser

JavaScript performance comparison

Revision 7 of this test case created by Jamie Hill

Info

Comparing a parser written with regexp's with a proper parser.

Preparation code

 
<script>
Benchmark.prototype.setup = function() {
    var css = '';
    for (var i = 100; i > 0; i--) {
      css += "h1 {\n  color: red;\n  }\n\n  .some-class {\n  color: #ff0;\n  background: #f00;\n\n  .nested {\n  color: blue;\n\n  .another {\n    background: pink;\n  }\n  }\n  }\n\n"
    }
   
    var Rule,
        openRe = /((([\*\@\#\.\w\d])([\s\*\@\#\.\w\d\-\,\>\:\=\"\~\^\$\(\)\+]*))\{)\s*/,
        closeRe = /\s*\}/;
   
    Rule = (function() {
      function Rule(source, selector, start, declarationStart, end, declarationEnd) {
        this.source = source;
        this.selector = selector;
        this.start = start;
        this.declarationStart = declarationStart;
        this.end = end || null;
        this.declarationEnd = declarationEnd || null;
        this.nested = [];
      }
   
      return Rule;
    }());
   
    function regexParser(css) {
      var length = css.length, stack = [], context, nested, match, open, close, rule, rules;
     
      this.cssText = css;
      this.rules = rules = context = [];
   
      while (css) {
        open = css.indexOf('{');
        close = css.indexOf('}');
     
        if (open >= 0 && (close == -1 || open < close) && (match = css.match(openRe))) {
          css = css.slice(open + 1);
          rule = new Rule(this.cssText, match[2].trim(),
                          length - css.length - match[1].length, length - css.length);
          context.push(rule);
          stack.push(context);
          context = rule.nested;
        } else if (close >= 0 && (open == -1 || close < open) && (match = css.match(closeRe))) {
          css = css.slice(close + match.length);
          context = stack.pop();
          rule = context[context.length - 1];
          if (rule) {
            rule.end = length - css.length;
            rule.declarationEnd = rule.end - match.length;
          } else {
            console.log('Closing unopened rule');
          }
        } else {
          css = null;
        }
      }
   
      // Pop remaining
      while (stack.length) {
        context = stack.pop();
        rule = context[context.length - 1];
        rule.end = rule.declarationEnd = length;
        console.log("Unclosed rule: '" + rule.selector + "'");
      }
   
      return rules;
    }
   
    function realParserWithCase(css) {
      var stack = [], rules = [], context = rules, state = 'before-selector',
          buffer = 0, index = 0, rule, char, start, end;
   
      this.cssText = css;
      this.rules = rules;
   
      while (char = css.charAt(index)) {          
        switch(char) {
        case ' ': case '\t': case '\r': case '\n': case '\f':
          if (state === 'selector') { buffer++; }
          break;
        // TODO: This won't strip comments from names and values i.e.
        // color/*foo*/: #ff0 /*bar*/; due to the buffer count used here for
        // speed. Buffer index doesn't allow comments mid-selector either as
        // they will be included in selector.
        case '/':
                        if (css.charAt(index + 1) === '*') {
                                index += 2;
                                end = css.indexOf('*/', index);
                                if (end === -1) {
                                        console.log('Missing: */');
                                        index = css.length;
                                } else {
                                  console.log(css.slice(index - 2, end + 2));
                                        index = end + 2;
                                }
                        } else {
                                buffer++;
                        }
                        break;
        case '{':
          if (state === 'selector') {
            start = index - buffer;
            rule = new Rule(css, css.slice(start, index).trim(), start, index + 1);
            context.push(rule);
            stack.push(context);
            context = rule.nested;
            state = 'before-selector'
            buffer = 0;
          }
          break;
        case ';':
          if (state === 'selector') {
            state = 'before-selector';
            buffer = 0;
          }
          break;
        case '}':
          if (state === 'before-selector') {
            if (context = stack.pop()) {
              rule = context[context.length - 1];
              rule.end = index + 1;
              rule.declarationEnd = index;
            } else {
              console.log('Closing unopened rule');
            }
            state = "before-selector";
            buffer = 0;
          }
          break;
        default:
          if (state === 'before-selector') { state = 'selector'; }
          buffer++;
          break;
        }
        index++;
      }
   
      // Pop remaining
      while (stack.length) {
        context = stack.pop();
        rule = context[context.length - 1];
        rule.end = rule.declarationEnd = css.length;
        console.log('Unclosed rule: ' + rule.selector);
      }
   
      return rules;
    }
};

Benchmark.prototype.teardown = function() {
    Rule = null;
};
</script>

Test runner

Warning! For accurate results, please disable Firebug before running the tests. (Why?)

Java applet disabled.

Testing in unknown unknown
Test Ops/sec
Regexp Parser
regexParser(css);
pending…
Real Parser with "case"
realParserWithCase(css);
pending…

Compare results of other browsers

Revisions

You can edit these tests or add even more tests to this page by appending /edit to the URL. Here’s a list of current revisions for this page:

0 comments

Add a comment