Node Knockout: A chance to play & experiment

This weekend, I competed in Node Knockout, a 48-hour coding competition designed around Node.js. For my idea, I chose to write a visualization of an AST querying language called cssauron. You can see the entry here.

Overview

cssauron is a tool which maps the ideas of querying for DOM elements through CSS selectors (eg how jQuery operates), and mapping that to source code. cssauron itself is AST agnostic and there are plugins for a few options that map the concept of selectors to the relevant AST nodes. The implementation I used was cssauron-falafel which binds with the falafel library for querying javascript source.

One of the problems with this setup is as you're learning it's hard to get visual feedback on what might match your css selectors. If I, for instance, am searching for variable > id, that is "the identifier that is part of a variable declaration" aka the variable name, it's difficult to know which one of those things match. To fix this, I built what amounts to a Regular Expression tester for these selectors.

You may ask, if it's not clear how the selectors work, why would you bother? There are a few neat things that you can do when you can pin-point nodes in an AST. One option here is to report on whether they're well formed or not. This is the idea behind jsl, which is a modular linter that you can customize. Another option is to transform these nodes, which is what is what the rewrite-js project aims for. These are powerful tools, so increasing the ease of adoption seems like a worthy goal.

The Node

The node.js code operates by asking cssauron-falafel for a list of nodes that match a selector. From this, we get the offset into the source string. For this project, I chose to modify the string in-place, overwriting old values. This presents an issue because if you add things to your source string, all of your offsets will be off.

To solve this issue, I originally started by keeping a counter of offsets. When I added a few characters, I bumped up the offset counter. This was problematic because it didn't differentiate between where in the source string I had inserted things. Take this code for instance:

multiRangeInsert(
  'var x = 1;', 
  [
    {
      start: 4, startString: '<b>', 
      end: 5, endString: '</b>'
    },
    {
      start: 8, startString: '<b>', 
      end: 9, endString: '</b>'
    },
  ]);

This bit of code should insert bold tags around the range 4-5 and 8-9 which will bold the x and 1 sections. It might be fun to think about how you would solve that. What happens if the order is changed to 8-9 then 4-5? What about if the one range is 1-10 and another is 2-3? I'm going to describe my solution next, so if you want to play, don't read any further.

The soultion I settled on here was keeping track of the insert points and how many characters were added as a list of tuples. That looks something like:

// each element looks like: [0, 14] which is the position inserted,
// and number of characters
var inserted = [];

var offsetForPos = function (pos) {
  var amount = 0;
  
  _.each(inserted, function (seen) {
    if (seen[0] < pos) {
      amount += seen[1];
    }
  });

  return amount;
};

What we do is calculate the offset of where the string needs to be inserted by looking back at how many things had been inserted previous to our insertion point. That is, given tuples who's first value is the position they were inserted, and the second value is how many characters were inserted, we loop through the list and find any first values which are less than our current range. For any we find, we add their second value to our "Which position do I insert this string?" number.

The Frontend

The frontend for the app is a fairly standard backbone site. A coworker of mine has been teaching me the ways of well-formed CSS based around Snook's SMACSS and I've actually found that it makes a fair amount of sense. It's been nice to apply those pricinciples without having thousands of lines of css to steer you away from it.

On the javascript side, I was planning to use Backbone, as it's what I'm most familiar with but another coworker convinced me that I should try to not use it, as it's probably a bit heavyweight. The resulting application was around 150 lines of javascript using underscore and jQuery. The state is managed using arrays of vanilla javascript objects. I did find myself missing the event structure of backbone models (e.g. the observer pattern), but it's overall a reasonable way to write small applications.

Conclusion

I had a lot of fun writing this little project. It'll help users of this small javascript library, it let me play around with some concepts that I've been toying around with at work. In my mind, this is the best part about these little code weekends. Free experimentation and low expectations allow you to play with things different than your day job. Who could ask for more than that?