JavaScript IRC Parsers (v3)

Revision 3 of this benchmark created by Havvy on


Description

Benchmarking how long it takes for various IRC parsers to parse a line of IRC data.

Setup

var lines = [
      ":test!test@hello.this.is.my.test.net PRIVMSG #Node.js :Hello,   am looking for some help with express. Can you plz help me?????! !",
      ":prawn!salad@tasty.co LONGARGS #hello testing 1 2 3 test test test test hi abcdefghijklmnop :This was quite a good test!",
      "PING :return this, cretin!",
      "PING more :test coverage",
      ":service!nicks@services. 001 test :Welcome to the test IRC network test! Enjoy your stay."
    ];
    
    var mircColors = /\u0003\d?\d?,?\d?\d?/g;
    
    
    ircMessage = (function() {
      function ircMessage(line) {
        var nextspace, pair, position, rawTags, tag, _i, _len;
        this.tags = {};
        this.prefix = "";
        this.command = "";
        this.args = [];
        position = 0;
        nextspace = 0;
        if (line[0] === "@") {
          nextspace = line.indexOf(" ");
          if (nextspace === -1) {
            throw new Error("Expected prefix; malformed IRC message.");
          }
          rawTags = line.substring(1, nextspace).split(";");
          for (_i = 0, _len = rawTags.length; _i < _len; _i++) {
            tag = rawTags[_i];
            pair = tag.split("=");
            this.tags[pair[0]] = pair[1] || true;
          }
          position = nextspace + 1;
        }
        while (line.charAt(position) === " ") {
          position++;
        }
        if (line.charAt(position) === ":") {
          nextspace = line.indexOf(" ", position);
          if (nextspace === -1) {
            throw new Error("Expected command; malformed IRC message.");
          }
          this.prefix = line.substring(position + 1, nextspace);
          position = nextspace + 1;
          while (line.charAt(position) === " ") {
            position++;
          }
        }
        nextspace = line.indexOf(" ", position);
        if (nextspace === -1) {
          if (line.length > position) {
            this.command = line.substring(position).toUpperCase();
          } else {
            return;
          }
        }
        this.command = line.substring(position, nextspace).toUpperCase();
        position = nextspace + 1;
        while (line.charAt(position) === " ") {
          position++;
        }
        while (position < line.length) {
          nextspace = line.indexOf(" ", position);
          if (line.charAt(position) === ":") {
            this.args.push(line.substring(position + 1));
            break;
          }
          if (nextspace !== -1) {
            this.args.push(line.substring(position, nextspace));
            position = nextspace + 1;
            while (line.charAt(position) === " ") {
              position++;
            }
            continue;
          }
          if (nextspace === -1) {
            this.args.push(line.substring(position));
            break;
          }
        }
        Object.freeze(this);
        return;
      }
    
      ircMessage.prototype.toString = function() {
        var arg, string, tag, value, _i, _len, _ref, _ref1;
        string = "";
        if (Object.keys(this.tags).length !== 0) {
          string += "@";
          _ref = this.tags;
          for (tag in _ref) {
            value = _ref[tag];
            if (value !== null) {
              string += "" + tag + "=" + value + ";";
            } else {
              string += "" + tag + ";";
            }
          }
          string = string.slice(0, -1) + " ";
        }
        if (this.prefix.length !== 0) {
          string += ":" + this.prefix + " ";
        }
        if (this.command.length !== 0) {
          string += "" + this.command + " ";
        }
        if (this.args.length !== 0) {
          _ref1 = this.args;
          for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
            arg = _ref1[_i];
            if (arg.indexOf(" " !== -1)) {
              string += "" + arg + " ";
            } else {
              string += ":" + arg + " ";
            }
          }
        }
        string = string.slice(0, -1);
        return string;
      };
    
      ircMessage.prototype.prefixIsUserHostmask = function() {
        return this.prefix.indexOf("@") !== -1 && this.prefix.indexOf("!") !== -1;
      };
    
      ircMessage.prototype.prefixIsServerHostname = function() {
        return this.prefix.indexOf("@") === -1 && this.prefix.indexOf("!") === -1 && this.prefix.indexOf(".") !== -1;
      };
    
      ircMessage.prototype.parseHostmask = function() {
        var hostname, nickname, parsed, username, _ref;
        _ref = this.prefix.split(/[!@]/), nickname = _ref[0], username = _ref[1], hostname = _ref[2];
        parsed = {
          nickname: nickname,
          username: username,
          hostname: hostname
        };
        return parsed;
      };
    
      return ircMessage;
    
    })();
    
    var parse_regex = /^(?:(?:(?:@([^ ]+) )?):(?:([a-z0-9\x5B-\x60\x7B-\x7D\.\-*]+)|([a-z0-9\x5B-\x60\x7B-\x7D\.\-*]+)!([^\x00\r\n\ ]+?)@?([a-z0-9\.\-:\/_]+)?) )?(\S+)(?: (?!:)(.+?))?(?: :(.+))?$/i;
    
    function kiwiParse(buffer_line) {
      var msg,
        i, j,
        tags = [],
        tag,
        line = '';
    
      // Decode server encoding
      line = buffer_line;
      if (!line) return;
    
      // Parse the complete line, removing any carriage returns
      msg = parse_regex.exec(line.replace(/^\r+|\r+$/, ''));
    
      if (!msg) {
        // The line was not parsed correctly, must be malformed
        console.log("Malformed IRC line: " + line.replace(/^\r+|\r+$/, ''));
        return;
      }
    
      // Extract any tags (msg[1])
      if (msg[1]) {
        tags = msg[1].split(';');
    
        for (j = 0; j < tags.length; j++) {
          tag = tags[j].split('=');
          tags[j] = {
            tag: tag[0],
            value: tag[1]
          };
        }
      }
    
      msg = {
        tags: tags,
        prefix: msg[2],
        nick: msg[3],
        ident: msg[4],
        hostname: msg[5] || '',
        command: msg[6],
        params: msg[7] || '',
        trailing: (msg[8]) ? msg[8].trim() : ''
      };
    
      msg.params = msg.params.split(' ');
    }
    
    nodeIRC = function(line, stripColors) { // {{{
      var message = {};
      var match;
    
      if (stripColors) {
        line = line.replace(/[\x02\x1f\x16\x0f]|\x03\d{0,2}(?:,\d{0,2})?/g, "");
      }
    
      // Parse prefix
      if (match = line.match(/^:([^ ]+) +/)) {
        message.prefix = match[1];
        line = line.replace(/^:[^ ]+ +/, '');
        if (match = message.prefix.match(/^([_a-zA-Z0-9\[\]\\`^{}|-]*)(!([^@]+)@(.*))?$/)) {
          message.nick = match[1];
          message.user = match[3];
          message.host = match[4];
        } else {
          message.server = message.prefix;
        }
      }
    
      // Parse command
      match = line.match(/^([^ ]+) */);
      message.command = match[1];
      message.rawCommand = match[1];
      message.commandType = 'normal';
      line = line.replace(/^[^ ]+ +/, '');
    
      message.args = [];
      var middle, trailing;
    
      // Parse parameters
      if (line.search(/^:|\s+:/) != -1) {
        match = line.match(/(.*?)(?:^:|\s+:)(.*)/);
        middle = match[1].trimRight();
        trailing = match[2];
      } else {
        middle = line;
      }
    
      if (middle.length)
        message.args = middle.split(/ +/);
    
      if (typeof(trailing) != 'undefined' && trailing.length)
        message.args.push(trailing);
    
      return message;
    } // }}}
    
    var ircbParse = function(chunk) {
      var prefix = null,
        buffer = '',
        middle = [],
        command,
        trailing = null,
        matching,
        i;
    
      if (chunk[0] === ':') {
        matching = 'prefix';
        i = 1;
      } else {
        matching = 'command';
        i = 0;
      }
    
      for (; i < chunk.length; i++) {
        if (chunk[i] === ' ') {
          if (matching === 'prefix') {
            matching = 'command';
            prefix = buffer;
          } else if (matching === 'command') {
            matching = 'middle';
            command = buffer;
          } else if (matching === 'middle') {
            middle.push(buffer);
          }
          buffer = '';
        } else if (chunk[i] === ':' && matching === 'middle') {
          trailing = chunk.substring(i + 1, chunk.length);
          break;
        } else {
          buffer += chunk[i];
        }
      }
    
      if (matching === 'middle' && buffer) {
        middle.push(buffer);
      }
    
      return {
        prefix: prefix,
        command: command,
        middle: middle,
        trailing: trailing
      };
    };
    
    
    var tennu = function(message, receiver) {
      var ixa = -1,
        ixb = -1;
    
      if (tennu.hasSender(message)) {
        ixa = message.indexOf(" ");
    
        this.sender = message.slice(1, ixa);
      }
    
      ixb = message.indexOf(" ", ixa + 1);
    
      this.name = message.slice(ixa + 1, ixb).toLowerCase();
      this.args = tennu.parameterize(message.slice(ixb + 1));
      this.receiver = receiver;
    
      Object.freeze(this);
      Object.freeze(this.args);
    };
    
    tennu.prototype.equals = function(that) {
      for (var key in this) {
        if (this[key] !== that[key]) {
          return false;
        }
      }
    
      return true;
    };
    
    tennu.hasSender = function(message) {
      return message[0] === ":";
    };
    
    tennu.getSenderType = function(prefix) {
      var exclamation = /!/.test(prefix);
      var at = /@/.test(prefix);
      var dot = /\./.test(prefix);
    
      if (exclamation) {
        return "hostmask";
      }
    
      if (at && dot) {
        return "nick@host";
      }
    
      if (dot) {
        return "server";
      }
    
      return "nick";
    };
    
    tennu.parameterize = function(params) {
      var trailing, middle, trailingIx;
    
      trailingIx = params.indexOf(" :");
    
      if (params[0] === ":") {
        return [params.slice(1)];
      } else if (trailingIx === -1) {
        return params.split(" ");
      } else {
        trailing = params.slice(trailingIx + 2).trim();
        middle = params.slice(0, trailingIx).split(" ");
        middle.push(trailing); //push returns the length property
        return middle;
      }
    };

Test runner

Ready to run.

Testing in
TestOps/sec
irc-message
for (var i = 0; i < lines.length; i++) {
  new ircMessage(lines[i]);
}
ready
KiwiIRC
for (var i = 0; i < lines.length; i++) {
  kiwiParse(lines[i]);
}
ready
node-irc
for (var i = 0; i < lines.length; i++) {
  nodeIRC(lines[i]);
}
ready
ircb
for (var i = 0; i < lines.length; i++) {
  ircbParse(lines[i]);
}
ready
Tennu
for (var i = 0; i < lines.length; i++) {
  tennu(lines[i]);
}
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.

  • Revision 1: published by Fionn Kelleher on
  • Revision 2: published by Havvy on
  • Revision 3: published by Havvy on