(function() {
  var tokenise = function(str) {
    var tokens = [],
      re = {
        "float": /^-?(([0-9]+\.[0-9]*|[0-9]*\.[0-9]+)([Ee][-+]?[0-9]+)?|[0-9]+[Ee][-+]?[0-9]+)/,
        "integer": /^-?(0([Xx][0-9A-Fa-f]+|[0-7]*)|[1-9][0-9]*)/,
        "identifier": /^[A-Z_a-z][0-9A-Z_a-z-]*/,
        "string": /^"[^"]*"/,
        "whitespace": /^(?:[\t\n\r ]+|[\t\n\r ]*((\/\/.*|\/\*(.|\n|\r)*?\*\/)[\t\n\r ]*))+/,
        "other": /^[^\t\n\r 0-9A-Z_a-z]/
      },
      types = ["float", "integer", "identifier", "string", "whitespace", "other"];
    while (str.length > 0) {
      var matched = false;
      for (var i = 0, n = types.length; i < n; i++) {
        var type = types[i];
        str = str.replace(re[type], function(tok) {
          tokens.push({ type: type, value: tok });
          matched = true;
          return "";
        });
        if (matched) break;
      }
      if (matched) continue;
      throw new Error("Token stream not progressing");
    }
    return tokens;
  };

  function WebIDLParseError(str, line, input, tokens) {
    this.message = str;
    this.line = line;
    this.input = input;
    this.tokens = tokens;
  };

  WebIDLParseError.prototype.toString = function() {
    return this.message + ", line " + this.line + " (tokens: '" + this.input + "')\n" +
      JSON.stringify(this.tokens, null, 4);
  };

  var parse = function(tokens, opt) {
    var line = 1;
    tokens = tokens.slice();

    var FLOAT = "float",
      INT = "integer",
      ID = "identifier",
      STR = "string",
      OTHER = "other";

    var error = function(str) {
      var tok = "";
      var numTokens = 0;
      var maxTokens = 5;
      while (numTokens < maxTokens && tokens.length > numTokens) {
        tok += tokens[numTokens].value;
        numTokens++;
      }
      throw new WebIDLParseError(str, line, tok, tokens.slice(0, 5));
    };

    var last_token = null;

    var consume = function(type, value) {
      if (!tokens.length || tokens[0].type !== type) return;
      if (typeof value === "undefined" || tokens[0].value === value) {
        last_token = tokens.shift();
        if (type === ID) last_token.value = last_token.value.replace(/^_/, "");
        return last_token;
      }
    };

    var ws = function() {
      if (!tokens.length) return;
      if (tokens[0].type === "whitespace") {
        var t = tokens.shift();
        t.value.replace(/\n/g, function(m) { line++;
          return m; });
        return t;
      }
    };

    var all_ws = function(store, pea) { // pea == post extended attribute, tpea = same for types
      var t = { type: "whitespace", value: "" };
      while (true) {
        var w = ws();
        if (!w) break;
        t.value += w.value;
      }
      if (t.value.length > 0) {
        if (store) {
          var w = t.value,
            re = {
              "ws": /^([\t\n\r ]+)/,
              "line-comment": /^\/\/(.*)\n?/m,
              "multiline-comment": /^\/\*((?:.|\n|\r)*?)\*\//
            },
            wsTypes = [];
          for (var k in re) wsTypes.push(k);
          while (w.length) {
            var matched = false;
            for (var i = 0, n = wsTypes.length; i < n; i++) {
              var type = wsTypes[i];
              w = w.replace(re[type], function(tok, m1) {
                store.push({ type: type + (pea ? ("-" + pea) : ""), value: m1 });
                matched = true;
                return "";
              });
              if (matched) break;
            }
            if (matched) continue;
            throw new Error("Surprising white space construct."); // this shouldn't happen
          }
        }
        return t;
      }
    };

    var integer_type = function() {
      var ret = "";
      all_ws();
      if (consume(ID, "unsigned")) ret = "unsigned ";
      all_ws();
      if (consume(ID, "short")) return ret + "short";
      if (consume(ID, "long")) {
        ret += "long";
        all_ws();
        if (consume(ID, "long")) return ret + " long";
        return ret;
      }
      if (ret) error("Failed to parse integer type");
    };

    var float_type = function() {
      var ret = "";
      all_ws();
      if (consume(ID, "unrestricted")) ret = "unrestricted ";
      all_ws();
      if (consume(ID, "float")) return ret + "float";
      if (consume(ID, "double")) return ret + "double";
      if (ret) error("Failed to parse float type");
    };

    var primitive_type = function() {
      var num_type = integer_type() || float_type();
      if (num_type) return num_type;
      all_ws();
      if (consume(ID, "boolean")) return "boolean";
      if (consume(ID, "byte")) return "byte";
      if (consume(ID, "octet")) return "octet";
    };

    var const_value = function() {
      if (consume(ID, "true")) return { type: "boolean", value: true };
      if (consume(ID, "false")) return { type: "boolean", value: false };
      if (consume(ID, "null")) return { type: "null" };
      if (consume(ID, "Infinity")) return { type: "Infinity", negative: false };
      if (consume(ID, "NaN")) return { type: "NaN" };
      var ret = consume(FLOAT) || consume(INT);
      if (ret) return { type: "number", value: 1 * ret.value };
      var tok = consume(OTHER, "-");
      if (tok) {
        if (consume(ID, "Infinity")) return { type: "Infinity", negative: true };
        else tokens.unshift(tok);
      }
    };

    var type_suffix = function(obj) {
      while (true) {
        all_ws();
        if (consume(OTHER, "?")) {
          if (obj.nullable) error("Can't nullable more than once");
          obj.nullable = true;
        } else if (consume(OTHER, "[")) {
          all_ws();
          consume(OTHER, "]") || error("Unterminated array type");
          if (!obj.array) {
            obj.array = 1;
            obj.nullableArray = [obj.nullable];
          } else {
            obj.array++;
            obj.nullableArray.push(obj.nullable);
          }
          obj.nullable = false;
        } else return;
      }
    };

    var single_type = function() {
      var prim = primitive_type(),
        ret = { sequence: false, generic: null, nullable: false, array: false, union: false },
        name, value;
      if (prim) {
        ret.idlType = prim;
      } else if (name = consume(ID)) {
        value = name.value;
        all_ws();
        // Generic types
        if (consume(OTHER, "<")) {
          // backwards compat
          if (value === "sequence") {
            ret.sequence = true;
          }
          ret.generic = value;
          var types = [];
          do {
            all_ws();
            types.push(type() || error("Error parsing generic type " + value));
            all_ws();
          }
          while (consume(OTHER, ","));
          if (value === "sequence") {
            if (types.length !== 1) error("A sequence must have exactly one subtype");
          } else if (value === "record") {
            if (types.length !== 2) error("A record must have exactly two subtypes");
            if (!/^(DOMString|USVString|ByteString)$/.test(types[0].idlType)) {
              error("Record key must be DOMString, USVString, or ByteString");
            }
          }
          ret.idlType = types.length === 1 ? types[0] : types;
          all_ws();
          if (!consume(OTHER, ">")) error("Unterminated generic type " + value);
          type_suffix(ret);
          return ret;
        } else {
          ret.idlType = value;
        }
      } else {
        return;
      }
      type_suffix(ret);
      if (ret.nullable && !ret.array && ret.idlType === "any") error("Type any cannot be made nullable");
      return ret;
    };

    var union_type = function() {
      all_ws();
      if (!consume(OTHER, "(")) return;
      var ret = { sequence: false, generic: null, nullable: false, array: false, union: true, idlType: [] };
      var fst = type_with_extended_attributes() || error("Union type with no content");
      ret.idlType.push(fst);
      while (true) {
        all_ws();
        if (!consume(ID, "or")) break;
        var typ = type_with_extended_attributes() || error("No type after 'or' in union type");
        ret.idlType.push(typ);
      }
      if (!consume(OTHER, ")")) error("Unterminated union type");
      type_suffix(ret);
      return ret;
    };

    var type = function() {
      return single_type() || union_type();
    };

    var type_with_extended_attributes = function() {
      var extAttrs = extended_attrs();
      var ret = single_type() || union_type();
      if (extAttrs.length && ret) ret.extAttrs = extAttrs;
      return ret;
    };

    var argument = function(store) {
      var ret = { optional: false, variadic: false };
      ret.extAttrs = extended_attrs(store);
      all_ws(store, "pea");
      var opt_token = consume(ID, "optional");
      if (opt_token) {
        ret.optional = true;
        all_ws();
      }
      ret.idlType = type_with_extended_attributes();
      if (!ret.idlType) {
        if (opt_token) tokens.unshift(opt_token);
        return;
      }
      var type_token = last_token;
      if (!ret.optional) {
        all_ws();
        if (tokens.length >= 3 &&
          tokens[0].type === "other" && tokens[0].value === "." &&
          tokens[1].type === "other" && tokens[1].value === "." &&
          tokens[2].type === "other" && tokens[2].value === "."
        ) {
          tokens.shift();
          tokens.shift();
          tokens.shift();
          ret.variadic = true;
        }
      }
      all_ws();
      var name = consume(ID);
      if (!name) {
        if (opt_token) tokens.unshift(opt_token);
        tokens.unshift(type_token);
        return;
      }
      ret.name = name.value;
      if (ret.optional) {
        all_ws();
        var dflt = default_();
        if (typeof dflt !== "undefined") {
          ret["default"] = dflt;
        }
      }
      return ret;
    };

    var argument_list = function(store) {
      var ret = [],
        arg = argument(store ? ret : null);
      if (!arg) return;
      ret.push(arg);
      while (true) {
        all_ws(store ? ret : null);
        if (!consume(OTHER, ",")) return ret;
        var nxt = argument(store ? ret : null) || error("Trailing comma in arguments list");
        ret.push(nxt);
      }
    };

    var simple_extended_attr = function(store) {
      all_ws();
      var name = consume(ID);
      if (!name) return;
      var ret = {
        name: name.value,
        "arguments": null
      };
      all_ws();
      var eq = consume(OTHER, "=");
      if (eq) {
        var rhs;
        all_ws();
        if (rhs = consume(ID)) {
          ret.rhs = rhs;
        } else if (rhs = consume(FLOAT)) {
          ret.rhs = rhs;
        } else if (rhs = consume(INT)) {
          ret.rhs = rhs;
        } else if (rhs = consume(STR)) {
          ret.rhs = rhs;
        } else if (consume(OTHER, "(")) {
          // [Exposed=(Window,Worker)]
          rhs = [];
          var id = consume(ID);
          if (id) {
            rhs = [id.value];
          }
          identifiers(rhs);
          consume(OTHER, ")") || error("Unexpected token in extended attribute argument list or type pair");
          ret.rhs = {
            type: "identifier-list",
            value: rhs
          };
        }
        if (!ret.rhs) return error("No right hand side to extended attribute assignment");
      }
      all_ws();
      if (consume(OTHER, "(")) {
        var args, pair;
        // [Constructor(DOMString str)]
        if (args = argument_list(store)) {
          ret["arguments"] = args;
        }
        // [Constructor()]
        else {
          ret["arguments"] = [];
        }
        all_ws();
        consume(OTHER, ")") || error("Unexpected token in extended attribute argument list");
      }
      return ret;
    };

    // Note: we parse something simpler than the official syntax. It's all that ever
    // seems to be used
    var extended_attrs = function(store) {
      var eas = [];
      all_ws(store);
      if (!consume(OTHER, "[")) return eas;
      eas[0] = simple_extended_attr(store) || error("Extended attribute with not content");
      all_ws();
      while (consume(OTHER, ",")) {
        if (eas.length) {
          eas.push(simple_extended_attr(store));
        } else {
          eas.push(simple_extended_attr(store) || error("Trailing comma in extended attribute"));
        }
      }
      consume(OTHER, "]") || error("No end of extended attribute");
      return eas;
    };

    var default_ = function() {
      all_ws();
      if (consume(OTHER, "=")) {
        all_ws();
        var def = const_value();
        if (def) {
          return def;
        } else if (consume(OTHER, "[")) {
          if (!consume(OTHER, "]")) error("Default sequence value must be empty");
          return { type: "sequence", value: [] };
        } else {
          var str = consume(STR) || error("No value for default");
          str.value = str.value.replace(/^"/, "").replace(/"$/, "");
          return str;
        }
      }
    };

    var const_ = function(store) {
      all_ws(store, "pea");
      if (!consume(ID, "const")) return;
      var ret = { type: "const", nullable: false };
      all_ws();
      var typ = primitive_type();
      if (!typ) {
        typ = consume(ID) || error("No type for const");
        typ = typ.value;
      }
      ret.idlType = typ;
      all_ws();
      if (consume(OTHER, "?")) {
        ret.nullable = true;
        all_ws();
      }
      var name = consume(ID) || error("No name for const");
      ret.name = name.value;
      all_ws();
      consume(OTHER, "=") || error("No value assignment for const");
      all_ws();
      var cnt = const_value();
      if (cnt) ret.value = cnt;
      else error("No value for const");
      all_ws();
      consume(OTHER, ";") || error("Unterminated const");
      return ret;
    };

    var inheritance = function() {
      all_ws();
      if (consume(OTHER, ":")) {
        all_ws();
        var inh = consume(ID) || error("No type in inheritance");
        return inh.value;
      }
    };

    var operation_rest = function(ret, store) {
      all_ws();
      if (!ret) ret = {};
      var name = consume(ID);
      ret.name = name ? name.value : null;
      all_ws();
      consume(OTHER, "(") || error("Invalid operation");
      ret["arguments"] = argument_list(store) || [];
      all_ws();
      consume(OTHER, ")") || error("Unterminated operation");
      all_ws();
      consume(OTHER, ";") || error("Unterminated operation");
      return ret;
    };

    var callback = function(store) {
      all_ws(store, "pea");
      var ret;
      if (!consume(ID, "callback")) return;
      all_ws();
      var tok = consume(ID, "interface");
      if (tok) {
        tokens.unshift(tok);
        ret = interface_();
        ret.type = "callback interface";
        return ret;
      }
      var name = consume(ID) || error("No name for callback");
      ret = { type: "callback", name: name.value };
      all_ws();
      consume(OTHER, "=") || error("No assignment in callback");
      all_ws();
      ret.idlType = return_type();
      all_ws();
      consume(OTHER, "(") || error("No arguments in callback");
      ret["arguments"] = argument_list(store) || [];
      all_ws();
      consume(OTHER, ")") || error("Unterminated callback");
      all_ws();
      consume(OTHER, ";") || error("Unterminated callback");
      return ret;
    };

    var attribute = function(store) {
      all_ws(store, "pea");
      var grabbed = [],
        ret = {
          type: "attribute",
          "static": false,
          stringifier: false,
          inherit: false,
          readonly: false
        };
      if (consume(ID, "static")) {
        ret["static"] = true;
        grabbed.push(last_token);
      } else if (consume(ID, "stringifier")) {
        ret.stringifier = true;
        grabbed.push(last_token);
      }
      var w = all_ws();
      if (w) grabbed.push(w);
      if (consume(ID, "inherit")) {
        if (ret["static"] || ret.stringifier) error("Cannot have a static or stringifier inherit");
        ret.inherit = true;
        grabbed.push(last_token);
        var w = all_ws();
        if (w) grabbed.push(w);
      }
      if (consume(ID, "readonly")) {
        ret.readonly = true;
        grabbed.push(last_token);
        var w = all_ws();
        if (w) grabbed.push(w);
      }
      var rest = attribute_rest(ret);
      if (!rest) {
        tokens = grabbed.concat(tokens);
      }
      return rest;
    };

    var attribute_rest = function(ret) {
      if (!consume(ID, "attribute")) {
        return;
      }
      all_ws();
      ret.idlType = type_with_extended_attributes() || error("No type in attribute");
      if (ret.idlType.sequence) error("Attributes cannot accept sequence types");
      if (ret.idlType.generic === "record") error("Attributes cannot accept record types");
      all_ws();
      var name = consume(ID) || error("No name in attribute");
      ret.name = name.value;
      all_ws();
      consume(OTHER, ";") || error("Unterminated attribute");
      return ret;
    };

    var return_type = function() {
      var typ = type();
      if (!typ) {
        if (consume(ID, "void")) {
          return "void";
        } else error("No return type");
      }
      return typ;
    };

    var operation = function(store) {
      all_ws(store, "pea");
      var ret = {
        type: "operation",
        getter: false,
        setter: false,
        creator: false,
        deleter: false,
        legacycaller: false,
        "static": false,
        stringifier: false
      };
      while (true) {
        all_ws();
        if (consume(ID, "getter")) ret.getter = true;
        else if (consume(ID, "setter")) ret.setter = true;
        else if (consume(ID, "creator")) ret.creator = true;
        else if (consume(ID, "deleter")) ret.deleter = true;
        else if (consume(ID, "legacycaller")) ret.legacycaller = true;
        else break;
      }
      if (ret.getter || ret.setter || ret.creator || ret.deleter || ret.legacycaller) {
        all_ws();
        ret.idlType = return_type();
        operation_rest(ret, store);
        return ret;
      }
      if (consume(ID, "static")) {
        ret["static"] = true;
        ret.idlType = return_type();
        operation_rest(ret, store);
        return ret;
      } else if (consume(ID, "stringifier")) {
        ret.stringifier = true; -
        all_ws();
        if (consume(OTHER, ";")) return ret;
        ret.idlType = return_type();
        operation_rest(ret, store);
        return ret;
      }
      ret.idlType = return_type();
      all_ws();
      if (consume(ID, "iterator")) {
        all_ws();
        ret.type = "iterator";
        if (consume(ID, "object")) {
          ret.iteratorObject = "object";
        } else if (consume(OTHER, "=")) {
          all_ws();
          var name = consume(ID) || error("No right hand side in iterator");
          ret.iteratorObject = name.value;
        }
        all_ws();
        consume(OTHER, ";") || error("Unterminated iterator");
        return ret;
      } else {
        operation_rest(ret, store);
        return ret;
      }
    };

    var identifiers = function(arr) {
      while (true) {
        all_ws();
        if (consume(OTHER, ",")) {
          all_ws();
          var name = consume(ID) || error("Trailing comma in identifiers list");
          arr.push(name.value);
        } else break;
      }
    };

    var serialiser = function(store) {
      all_ws(store, "pea");
      if (!consume(ID, "serializer")) return;
      var ret = { type: "serializer" };
      all_ws();
      if (consume(OTHER, "=")) {
        all_ws();
        if (consume(OTHER, "{")) {
          ret.patternMap = true;
          all_ws();
          var id = consume(ID);
          if (id && id.value === "getter") {
            ret.names = ["getter"];
          } else if (id && id.value === "inherit") {
            ret.names = ["inherit"];
            identifiers(ret.names);
          } else if (id) {
            ret.names = [id.value];
            identifiers(ret.names);
          } else {
            ret.names = [];
          }
          all_ws();
          consume(OTHER, "}") || error("Unterminated serializer pattern map");
        } else if (consume(OTHER, "[")) {
          ret.patternList = true;
          all_ws();
          var id = consume(ID);
          if (id && id.value === "getter") {
            ret.names = ["getter"];
          } else if (id) {
            ret.names = [id.value];
            identifiers(ret.names);
          } else {
            ret.names = [];
          }
          all_ws();
          consume(OTHER, "]") || error("Unterminated serializer pattern list");
        } else {
          var name = consume(ID) || error("Invalid serializer");
          ret.name = name.value;
        }
        all_ws();
        consume(OTHER, ";") || error("Unterminated serializer");
        return ret;
      } else if (consume(OTHER, ";")) {
        // noop, just parsing
      } else {
        ret.idlType = return_type();
        all_ws();
        ret.operation = operation_rest(null, store);
      }
      return ret;
    };

    var iterable_type = function() {
      if (consume(ID, "iterable")) return "iterable";
      else if (consume(ID, "legacyiterable")) return "legacyiterable";
      else if (consume(ID, "maplike")) return "maplike";
      else if (consume(ID, "setlike")) return "setlike";
      else return;
    };

    var readonly_iterable_type = function() {
      if (consume(ID, "maplike")) return "maplike";
      else if (consume(ID, "setlike")) return "setlike";
      else return;
    };

    var iterable = function(store) {
      all_ws(store, "pea");
      var grabbed = [],
        ret = { type: null, idlType: null, readonly: false };
      if (consume(ID, "readonly")) {
        ret.readonly = true;
        grabbed.push(last_token);
        var w = all_ws();
        if (w) grabbed.push(w);
      }
      var consumeItType = ret.readonly ? readonly_iterable_type : iterable_type;

      var ittype = consumeItType();
      if (!ittype) {
        tokens = grabbed.concat(tokens);
        return;
      }

      var secondTypeRequired = ittype === "maplike";
      var secondTypeAllowed = secondTypeRequired || ittype === "iterable";
      ret.type = ittype;
      if (ret.type !== 'maplike' && ret.type !== 'setlike')
        delete ret.readonly;
      all_ws();
      if (consume(OTHER, "<")) {
        ret.idlType = type_with_extended_attributes() || error("Error parsing " + ittype + " declaration");
        all_ws();
        if (secondTypeAllowed) {
          var type2 = null;
          if (consume(OTHER, ",")) {
            all_ws();
            type2 = type_with_extended_attributes();
            all_ws();
          }
          if (type2)
            ret.idlType = [ret.idlType, type2];
          else if (secondTypeRequired)
            error("Missing second type argument in " + ittype + " declaration");
        }
        if (!consume(OTHER, ">")) error("Unterminated " + ittype + " declaration");
        all_ws();
        if (!consume(OTHER, ";")) error("Missing semicolon after " + ittype + " declaration");
      } else
        error("Error parsing " + ittype + " declaration");

      return ret;
    };

    var interface_ = function(isPartial, store) {
      all_ws(isPartial ? null : store, "pea");
      if (!consume(ID, "interface")) return;
      all_ws();
      var name = consume(ID) || error("No name for interface");
      var mems = [],
        ret = {
          type: "interface",
          name: name.value,
          partial: false,
          members: mems
        };
      if (!isPartial) ret.inheritance = inheritance() || null;
      all_ws();
      consume(OTHER, "{") || error("Bodyless interface");
      while (true) {
        all_ws(store ? mems : null);
        if (consume(OTHER, "}")) {
          all_ws();
          consume(OTHER, ";") || error("Missing semicolon after interface");
          return ret;
        }
        var ea = extended_attrs(store ? mems : null);
        all_ws();
        var cnt = const_(store ? mems : null);
        if (cnt) {
          cnt.extAttrs = ea;
          ret.members.push(cnt);
          continue;
        }
        var mem = (opt.allowNestedTypedefs && typedef(store ? mems : null)) ||
          iterable(store ? mems : null) ||
          serialiser(store ? mems : null) ||
          attribute(store ? mems : null) ||
          operation(store ? mems : null) ||
          error("Unknown member");
        mem.extAttrs = ea;
        ret.members.push(mem);
      }
    };

    var namespace = function(isPartial, store) {
      all_ws(isPartial ? null : store, "pea");
      if (!consume(ID, "namespace")) return;
      all_ws();
      var name = consume(ID) || error("No name for namespace");
      var mems = [],
        ret = {
          type: "namespace",
          name: name.value,
          partial: isPartial,
          members: mems
        };
      all_ws();
      consume(OTHER, "{") || error("Bodyless namespace");
      while (true) {
        all_ws(store ? mems : null);
        if (consume(OTHER, "}")) {
          all_ws();
          consume(OTHER, ";") || error("Missing semicolon after namespace");
          return ret;
        }
        var ea = extended_attrs(store ? mems : null);
        all_ws();
        var mem = noninherited_attribute(store ? mems : null) ||
          nonspecial_operation(store ? mems : null) ||
          error("Unknown member");
        mem.extAttrs = ea;
        ret.members.push(mem);
      }
    }

    var noninherited_attribute = function(store) {
      var w = all_ws(store, "pea"), 
        grabbed = [],
        ret = {
          type: "attribute",
          "static": false,
          stringifier: false,
          inherit: false,
          readonly: false
        };
      if (w) grabbed.push(w);
      if (consume(ID, "readonly")) {
        ret.readonly = true;
        grabbed.push(last_token);
        var w = all_ws();
        if (w) grabbed.push(w);
      }
      var rest = attribute_rest(ret);
      if (!rest) {
        tokens = grabbed.concat(tokens);
      }
      return rest;
    }
    
    var nonspecial_operation = function(store) {
      all_ws(store, "pea");
      var ret = {
        type: "operation",
        getter: false,
        setter: false,
        creator: false,
        deleter: false,
        legacycaller: false,
        "static": false,
        stringifier: false
      };
      ret.idlType = return_type();
      return operation_rest(ret, store);
    }

    var partial = function(store) {
      all_ws(store, "pea");
      if (!consume(ID, "partial")) return;
      var thing = dictionary(true, store) ||
        interface_(true, store) ||
        namespace(true, store) ||
        error("Partial doesn't apply to anything");
      thing.partial = true;
      return thing;
    };

    var dictionary = function(isPartial, store) {
      all_ws(isPartial ? null : store, "pea");
      if (!consume(ID, "dictionary")) return;
      all_ws();
      var name = consume(ID) || error("No name for dictionary");
      var mems = [],
        ret = {
          type: "dictionary",
          name: name.value,
          partial: false,
          members: mems
        };
      if (!isPartial) ret.inheritance = inheritance() || null;
      all_ws();
      consume(OTHER, "{") || error("Bodyless dictionary");
      while (true) {
        all_ws(store ? mems : null);
        if (consume(OTHER, "}")) {
          all_ws();
          consume(OTHER, ";") || error("Missing semicolon after dictionary");
          return ret;
        }
        var ea = extended_attrs(store ? mems : null);
        all_ws(store ? mems : null, "pea");
        var required = consume(ID, "required");
        var typ = type_with_extended_attributes() || error("No type for dictionary member");
        all_ws();
        var name = consume(ID) || error("No name for dictionary member");
        var dflt = default_();
        if (required && dflt) error("Required member must not have a default");
        var member = {
          type: "field",
          name: name.value,
          required: !!required,
          idlType: typ,
          extAttrs: ea
        };
        if (typeof dflt !== "undefined") {
          member["default"] = dflt;
        }
        ret.members.push(member);
        all_ws();
        consume(OTHER, ";") || error("Unterminated dictionary member");
      }
    };

    var exception = function(store) {
      all_ws(store, "pea");
      if (!consume(ID, "exception")) return;
      all_ws();
      var name = consume(ID) || error("No name for exception");
      var mems = [],
        ret = {
          type: "exception",
          name: name.value,
          members: mems
        };
      ret.inheritance = inheritance() || null;
      all_ws();
      consume(OTHER, "{") || error("Bodyless exception");
      while (true) {
        all_ws(store ? mems : null);
        if (consume(OTHER, "}")) {
          all_ws();
          consume(OTHER, ";") || error("Missing semicolon after exception");
          return ret;
        }
        var ea = extended_attrs(store ? mems : null);
        all_ws(store ? mems : null, "pea");
        var cnt = const_();
        if (cnt) {
          cnt.extAttrs = ea;
          ret.members.push(cnt);
        } else {
          var typ = type();
          all_ws();
          var name = consume(ID);
          all_ws();
          if (!typ || !name || !consume(OTHER, ";")) error("Unknown member in exception body");
          ret.members.push({
            type: "field",
            name: name.value,
            idlType: typ,
            extAttrs: ea
          });
        }
      }
    };

    var enum_ = function(store) {
      all_ws(store, "pea");
      if (!consume(ID, "enum")) return;
      all_ws();
      var name = consume(ID) || error("No name for enum");
      var vals = [],
        ret = {
          type: "enum",
          name: name.value,
          values: vals
        };
      all_ws();
      consume(OTHER, "{") || error("No curly for enum");
      var saw_comma = false;
      while (true) {
        all_ws(store ? vals : null);
        if (consume(OTHER, "}")) {
          all_ws();
          consume(OTHER, ";") || error("No semicolon after enum");
          return ret;
        }
        var val = consume(STR) || error("Unexpected value in enum");
        ret.values.push(val.value.replace(/"/g, ""));
        all_ws(store ? vals : null);
        if (consume(OTHER, ",")) {
          if (store) vals.push({ type: "," });
          all_ws(store ? vals : null);
          saw_comma = true;
        } else {
          saw_comma = false;
        }
      }
    };

    var typedef = function(store) {
      all_ws(store, "pea");
      if (!consume(ID, "typedef")) return;
      var ret = {
        type: "typedef"
      };
      all_ws();
      ret.idlType = type_with_extended_attributes() || error("No type in typedef");
      all_ws();
      var name = consume(ID) || error("No name in typedef");
      ret.name = name.value;
      all_ws();
      consume(OTHER, ";") || error("Unterminated typedef");
      return ret;
    };

    var implements_ = function(store) {
      all_ws(store, "pea");
      var target = consume(ID);
      if (!target) return;
      var w = all_ws();
      if (consume(ID, "implements")) {
        var ret = {
          type: "implements",
          target: target.value
        };
        all_ws();
        var imp = consume(ID) || error("Incomplete implements statement");
        ret["implements"] = imp.value;
        all_ws();
        consume(OTHER, ";") || error("No terminating ; for implements statement");
        return ret;
      } else {
        // rollback
        tokens.unshift(w);
        tokens.unshift(target);
      }
    };

    var definition = function(store) {
      return callback(store) ||
        interface_(false, store) ||
        partial(store) ||
        dictionary(false, store) ||
        exception(store) ||
        enum_(store) ||
        typedef(store) ||
        implements_(store) ||
        namespace(false, store);
    };

    var definitions = function(store) {
      if (!tokens.length) return [];
      var defs = [];
      while (true) {
        var ea = extended_attrs(store ? defs : null),
          def = definition(store ? defs : null);
        if (!def) {
          if (ea.length) error("Stray extended attributes");
          break;
        }
        def.extAttrs = ea;
        defs.push(def);
      }
      return defs;
    };
    var res = definitions(opt.ws);
    if (tokens.length) error("Unrecognised tokens");
    return res;
  };

  var obj = {
    parse: function(str, opt) {
      if (!opt) opt = {};
      var tokens = tokenise(str);
      return parse(tokens, opt);
    }
  };

  if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
    module.exports = obj;
  } else if (typeof define === 'function' && define.amd) {
    define([], function() {
      return obj;
    });
  } else {
    (self || window).WebIDL2 = obj;
  }
}());
