blob: ef519c09df6d6da812fcbba57c7ca0f475af1ed5 [file] [log] [blame]
"use strict";
(() => {
// These regular expressions use the sticky flag so they will only match at
// the current location (ie. the offset of lastIndex).
const tokenRe = {
// This expression uses a lookahead assertion to catch false matches
// against integers early.
"float": /-?(?=[0-9]*\.|[0-9]+[eE])(([0-9]+\.[0-9]*|[0-9]*\.[0-9]+)([Ee][-+]?[0-9]+)?|[0-9]+[Ee][-+]?[0-9]+)/y,
"integer": /-?(0([Xx][0-9A-Fa-f]+|[0-7]*)|[1-9][0-9]*)/y,
"identifier": /_?[A-Za-z][0-9A-Z_a-z-]*/y,
"string": /"[^"]*"/y,
"whitespace": /[\t\n\r ]+/y,
"comment": /((\/(\/.*|\*([^*]|\*[^\/])*\*\/)[\t\n\r ]*)+)/y,
"other": /[^\t\n\r 0-9A-Za-z]/y
};
const stringTypes = [
"ByteString",
"DOMString",
"USVString"
];
const argumentNameKeywords = [
"attribute",
"callback",
"const",
"deleter",
"dictionary",
"enum",
"getter",
"includes",
"inherit",
"interface",
"iterable",
"maplike",
"namespace",
"partial",
"required",
"setlike",
"setter",
"static",
"stringifier",
"typedef",
"unrestricted"
];
const nonRegexTerminals = [
"FrozenArray",
"Infinity",
"NaN",
"Promise",
"boolean",
"byte",
"double",
"false",
"float",
"implements",
"legacyiterable",
"long",
"mixin",
"null",
"octet",
"optional",
"or",
"readonly",
"record",
"sequence",
"short",
"true",
"unsigned",
"void"
].concat(argumentNameKeywords, stringTypes);
const punctuations = [
"(",
")",
",",
"-Infinity",
"...",
":",
";",
"<",
"=",
">",
"?",
"[",
"]",
"{",
"}"
];
function tokenise(str) {
const tokens = [];
let lastIndex = 0;
let trivia = "";
while (lastIndex < str.length) {
const nextChar = str.charAt(lastIndex);
let result = -1;
if (/[\t\n\r ]/.test(nextChar)) {
result = attemptTokenMatch("whitespace", { noFlushTrivia: true });
} else if (nextChar === '/') {
result = attemptTokenMatch("comment", { noFlushTrivia: true });
}
if (result !== -1) {
trivia += tokens.pop().value;
} else if (/[-0-9.]/.test(nextChar)) {
result = attemptTokenMatch("float");
if (result === -1) {
result = attemptTokenMatch("integer");
}
} else if (/[A-Z_a-z]/.test(nextChar)) {
result = attemptTokenMatch("identifier");
const token = tokens[tokens.length - 1];
if (result !== -1 && nonRegexTerminals.includes(token.value)) {
token.type = token.value;
}
} else if (nextChar === '"') {
result = attemptTokenMatch("string");
}
for (const punctuation of punctuations) {
if (str.startsWith(punctuation, lastIndex)) {
tokens.push({ type: punctuation, value: punctuation, trivia });
trivia = "";
lastIndex += punctuation.length;
result = lastIndex;
break;
}
}
// other as the last try
if (result === -1) {
result = attemptTokenMatch("other");
}
if (result === -1) {
throw new Error("Token stream not progressing");
}
lastIndex = result;
}
return tokens;
function attemptTokenMatch(type, { noFlushTrivia } = {}) {
const re = tokenRe[type];
re.lastIndex = lastIndex;
const result = re.exec(str);
if (result) {
tokens.push({ type, value: result[0], trivia });
if (!noFlushTrivia) {
trivia = "";
}
return re.lastIndex;
}
return -1;
}
}
class WebIDLParseError {
constructor(str, line, input, tokens) {
this.message = str;
this.line = line;
this.input = input;
this.tokens = tokens;
}
toString() {
const escapedInput = JSON.stringify(this.input);
const tokens = JSON.stringify(this.tokens, null, 4);
return `${this.message}, line ${this.line} (tokens: ${escapedInput})\n${tokens}`;
}
}
function parse(tokens) {
let line = 1;
tokens = tokens.slice();
const names = new Map();
let current = null;
const FLOAT = "float";
const INT = "integer";
const ID = "identifier";
const STR = "string";
const OTHER = "other";
const EMPTY_OPERATION = Object.freeze({
type: "operation",
getter: false,
setter: false,
deleter: false,
static: false,
stringifier: false
});
const EMPTY_IDLTYPE = Object.freeze({
generic: null,
nullable: false,
union: false,
idlType: null,
extAttrs: []
});
function error(str) {
const maxTokens = 5;
const tok = tokens
.slice(consume_position, consume_position + maxTokens)
.map(t => t.trivia + t.value).join("");
// Count newlines preceding the actual erroneous token
if (tokens.length) {
line += count(tokens[consume_position].trivia, "\n");
}
let message;
if (current) {
message = `Got an error during or right after parsing \`${current.partial ? "partial " : ""}${current.type} ${current.name}\`: ${str}`
}
else {
// throwing before any valid definition
message = `Got an error before parsing any named definition: ${str}`;
}
throw new WebIDLParseError(message, line, tok, tokens.slice(0, maxTokens));
}
function sanitize_name(name, type) {
if (names.has(name)) {
error(`The name "${name}" of type "${names.get(name)}" is already seen`);
}
names.set(name, type);
return name;
}
let consume_position = 0;
function probe(type) {
return tokens.length > consume_position && tokens[consume_position].type === type;
}
function consume(...candidates) {
// TODO: use const when Servo updates its JS engine
for (let type of candidates) {
if (!probe(type)) continue;
const token = tokens[consume_position];
consume_position++;
line += count(token.trivia, "\n");
return token;
}
}
function unescape(identifier) {
return identifier.startsWith('_') ? identifier.slice(1) : identifier;
}
function unconsume(position) {
while (consume_position > position) {
consume_position--;
line -= count(tokens[consume_position].trivia, "\n");
}
}
function count(str, char) {
let total = 0;
for (let i = str.indexOf(char); i !== -1; i = str.indexOf(char, i + 1)) {
++total;
}
return total;
}
function integer_type() {
let ret = "";
if (consume("unsigned")) ret = "unsigned ";
if (consume("short")) return ret + "short";
if (consume("long")) {
ret += "long";
if (consume("long")) return ret + " long";
return ret;
}
if (ret) error("Failed to parse integer type");
}
function float_type() {
let ret = "";
if (consume("unrestricted")) ret = "unrestricted ";
if (consume("float")) return ret + "float";
if (consume("double")) return ret + "double";
if (ret) error("Failed to parse float type");
}
function primitive_type() {
const num_type = integer_type() || float_type();
if (num_type) return num_type;
if (consume("boolean")) return "boolean";
if (consume("byte")) return "byte";
if (consume("octet")) return "octet";
}
function const_value() {
if (consume("true")) return { type: "boolean", value: true };
if (consume("false")) return { type: "boolean", value: false };
if (consume("null")) return { type: "null" };
if (consume("Infinity")) return { type: "Infinity", negative: false };
if (consume("-Infinity")) return { type: "Infinity", negative: true };
if (consume("NaN")) return { type: "NaN" };
const ret = consume(FLOAT, INT);
if (ret) return { type: "number", value: ret.value };
}
function type_suffix(obj) {
obj.nullable = !!consume("?");
if (probe("?")) error("Can't nullable more than once");
}
function generic_type(typeName) {
const name = consume("FrozenArray", "Promise", "sequence", "record");
if (!name) {
return;
}
const ret = { generic: name.type };
consume("<") || error(`No opening bracket after ${name.type}`);
switch (name.type) {
case "Promise":
if (probe("[")) error("Promise type cannot have extended attribute");
ret.idlType = return_type(typeName);
break;
case "sequence":
case "FrozenArray":
ret.idlType = type_with_extended_attributes(typeName);
break;
case "record":
if (probe("[")) error("Record key cannot have extended attribute");
ret.idlType = [];
const keyType = consume(...stringTypes);
if (!keyType) error(`Record key must be a string type`);
ret.idlType.push(Object.assign({ type: typeName }, EMPTY_IDLTYPE, { idlType: keyType.value }));
consume(",") || error("Missing comma after record key type");
const valueType = type_with_extended_attributes(typeName) || error("Error parsing generic type record");
ret.idlType.push(valueType);
break;
}
if (!ret.idlType) error(`Error parsing generic type ${name.type}`);
consume(">") || error(`Missing closing bracket after ${name.type}`);
if (name.type === "Promise" && probe("?")) {
error("Promise type cannot be nullable");
}
type_suffix(ret);
return ret;
}
function single_type(typeName) {
const ret = Object.assign({ type: typeName || null }, EMPTY_IDLTYPE);
const generic = generic_type(typeName);
if (generic) {
return Object.assign(ret, generic);
}
const prim = primitive_type();
let name;
if (prim) {
ret.idlType = prim;
} else if (name = consume(ID, ...stringTypes)) {
ret.idlType = name.value;
if (probe("<")) error(`Unsupported generic type ${name.value}`);
} else {
return;
}
type_suffix(ret);
if (ret.nullable && ret.idlType === "any") error("Type any cannot be made nullable");
return ret;
}
function union_type(typeName) {
if (!consume("(")) return;
const ret = Object.assign({ type: typeName || null }, EMPTY_IDLTYPE, { union: true, idlType: [] });
do {
const typ = type_with_extended_attributes() || error("No type after open parenthesis or 'or' in union type");
ret.idlType.push(typ);
} while (consume("or"));
if (ret.idlType.length < 2) {
error("At least two types are expected in a union type but found less");
}
if (!consume(")")) error("Unterminated union type");
type_suffix(ret);
return ret;
}
function type(typeName) {
return single_type(typeName) || union_type(typeName);
}
function type_with_extended_attributes(typeName) {
const extAttrs = extended_attrs();
const ret = single_type(typeName) || union_type(typeName);
if (extAttrs.length && ret) ret.extAttrs = extAttrs;
return ret;
}
function argument() {
const start_position = consume_position;
const ret = { optional: false, variadic: false, default: null };
ret.extAttrs = extended_attrs();
const opt_token = consume("optional");
if (opt_token) {
ret.optional = true;
}
ret.idlType = type_with_extended_attributes("argument-type");
if (!ret.idlType) {
unconsume(start_position);
return;
}
if (!ret.optional && consume("...")) {
ret.variadic = true;
}
const name = consume(ID, ...argumentNameKeywords);
if (!name) {
unconsume(start_position);
return;
}
ret.name = unescape(name.value);
ret.escapedName = name.value;
if (ret.optional) {
ret.default = default_() || null;
}
return ret;
}
function argument_list() {
const ret = [];
const arg = argument();
if (!arg) return ret;
ret.push(arg);
while (true) {
if (!consume(",")) return ret;
const nxt = argument() || error("Trailing comma in arguments list");
ret.push(nxt);
}
}
function simple_extended_attr() {
const name = consume(ID);
if (!name) return;
const ret = {
name: name.value,
arguments: null,
type: "extended-attribute",
rhs: null
};
const eq = consume("=");
if (eq) {
ret.rhs = consume(ID, FLOAT, INT, STR);
if (ret.rhs) {
// No trivia exposure yet
ret.rhs.trivia = undefined;
}
}
if (consume("(")) {
if (eq && !ret.rhs) {
// [Exposed=(Window,Worker)]
ret.rhs = {
type: "identifier-list",
value: identifiers()
};
}
else {
// [NamedConstructor=Audio(DOMString src)] or [Constructor(DOMString str)]
ret.arguments = argument_list();
}
consume(")") || error("Unexpected token in extended attribute argument list");
}
if (eq && !ret.rhs) error("No right hand side to extended attribute assignment");
return ret;
}
// Note: we parse something simpler than the official syntax. It's all that ever
// seems to be used
function extended_attrs() {
const eas = [];
if (!consume("[")) return eas;
eas[0] = simple_extended_attr() || error("Extended attribute with not content");
while (consume(",")) {
eas.push(simple_extended_attr() || error("Trailing comma in extended attribute"));
}
consume("]") || error("No end of extended attribute");
return eas;
}
function default_() {
if (consume("=")) {
const def = const_value();
if (def) {
return def;
} else if (consume("[")) {
if (!consume("]")) error("Default sequence value must be empty");
return { type: "sequence", value: [] };
} else {
const str = consume(STR) || error("No value for default");
str.value = str.value.slice(1, -1);
// No trivia exposure yet
str.trivia = undefined;
return str;
}
}
}
function const_() {
if (!consume("const")) return;
const ret = { type: "const", nullable: false };
let typ = primitive_type();
if (!typ) {
typ = consume(ID) || error("No type for const");
typ = typ.value;
}
ret.idlType = Object.assign({ type: "const-type" }, EMPTY_IDLTYPE, { idlType: typ });
type_suffix(ret);
const name = consume(ID) || error("No name for const");
ret.name = name.value;
consume("=") || error("No value assignment for const");
const cnt = const_value();
if (cnt) ret.value = cnt;
else error("No value for const");
consume(";") || error("Unterminated const");
return ret;
}
function inheritance() {
if (consume(":")) {
const inh = consume(ID) || error("No type in inheritance");
return inh.value;
}
}
function operation_rest(ret) {
if (!ret) ret = {};
const name = consume(ID);
ret.name = name ? unescape(name.value) : null;
ret.escapedName = name ? name.value : null;
consume("(") || error("Invalid operation");
ret.arguments = argument_list();
consume(")") || error("Unterminated operation");
consume(";") || error("Unterminated operation");
return ret;
}
function callback() {
let ret;
if (!consume("callback")) return;
const tok = consume("interface");
if (tok) {
ret = interface_rest(false, "callback interface");
return ret;
}
const name = consume(ID) || error("No name for callback");
ret = current = { type: "callback", name: sanitize_name(name.value, "callback") };
consume("=") || error("No assignment in callback");
ret.idlType = return_type() || error("Missing return type");
consume("(") || error("No arguments in callback");
ret.arguments = argument_list();
consume(")") || error("Unterminated callback");
consume(";") || error("Unterminated callback");
return ret;
}
function attribute({ noInherit = false, readonly = false } = {}) {
const start_position = consume_position;
const ret = {
type: "attribute",
static: false,
stringifier: false,
inherit: false,
readonly: false
};
if (!noInherit && consume("inherit")) {
ret.inherit = true;
}
if (consume("readonly")) {
ret.readonly = true;
} else if (readonly && probe("attribute")) {
error("Attributes must be readonly in this context");
}
const rest = attribute_rest(ret);
if (!rest) {
unconsume(start_position);
}
return rest;
}
function attribute_rest(ret) {
if (!consume("attribute")) {
return;
}
ret.idlType = type_with_extended_attributes("attribute-type") || error("No type in attribute");
if (ret.idlType.generic === "sequence") error("Attributes cannot accept sequence types");
if (ret.idlType.generic === "record") error("Attributes cannot accept record types");
const name = consume(ID, "required") || error("No name in attribute");
ret.name = unescape(name.value);
ret.escapedName = name.value;
consume(";") || error("Unterminated attribute");
return ret;
}
function return_type(typeName) {
const typ = type(typeName || "return-type");
if (typ) {
return typ;
}
if (consume("void")) {
return Object.assign({ type: "return-type" }, EMPTY_IDLTYPE, { idlType: "void" });
}
}
function operation({ regular = false } = {}) {
const ret = Object.assign({}, EMPTY_OPERATION);
while (!regular) {
if (consume("getter")) ret.getter = true;
else if (consume("setter")) ret.setter = true;
else if (consume("deleter")) ret.deleter = true;
else break;
}
ret.idlType = return_type() || error("Missing return type");
operation_rest(ret);
return ret;
}
function static_member() {
if (!consume("static")) return;
const member = attribute({ noInherit: true }) ||
operation({ regular: true }) ||
error("No body in static member");
member.static = true;
return member;
}
function stringifier() {
if (!consume("stringifier")) return;
if (consume(";")) {
return Object.assign({}, EMPTY_OPERATION, { stringifier: true });
}
const member = attribute({ noInherit: true }) ||
operation({ regular: true }) ||
error("Unterminated stringifier");
member.stringifier = true;
return member;
}
function identifiers() {
const arr = [];
const id = consume(ID);
if (id) {
arr.push(id.value);
}
else error("Expected identifiers but not found");
while (true) {
if (consume(",")) {
const name = consume(ID) || error("Trailing comma in identifiers list");
arr.push(name.value);
} else break;
}
return arr;
}
function iterable_type() {
if (consume("iterable")) return "iterable";
else if (consume("legacyiterable")) return "legacyiterable";
else if (consume("maplike")) return "maplike";
else if (consume("setlike")) return "setlike";
else return;
}
function readonly_iterable_type() {
if (consume("maplike")) return "maplike";
else if (consume("setlike")) return "setlike";
else return;
}
function iterable() {
const start_position = consume_position;
const ret = { type: null, idlType: null, readonly: false };
if (consume("readonly")) {
ret.readonly = true;
}
const consumeItType = ret.readonly ? readonly_iterable_type : iterable_type;
const ittype = consumeItType();
if (!ittype) {
unconsume(start_position);
return;
}
const secondTypeRequired = ittype === "maplike";
const secondTypeAllowed = secondTypeRequired || ittype === "iterable";
ret.type = ittype;
if (ret.type !== 'maplike' && ret.type !== 'setlike')
delete ret.readonly;
if (consume("<")) {
ret.idlType = [type_with_extended_attributes()] || error(`Error parsing ${ittype} declaration`);
if (secondTypeAllowed) {
if (consume(",")) {
ret.idlType.push(type_with_extended_attributes());
}
else if (secondTypeRequired)
error(`Missing second type argument in ${ittype} declaration`);
}
if (!consume(">")) error(`Unterminated ${ittype} declaration`);
if (!consume(";")) error(`Missing semicolon after ${ittype} declaration`);
} else
error(`Error parsing ${ittype} declaration`);
return ret;
}
function interface_rest(isPartial, typeName = "interface") {
const name = consume(ID) || error("No name for interface");
const mems = [];
const ret = current = {
type: typeName,
name: isPartial ? name.value : sanitize_name(name.value, "interface"),
partial: isPartial,
members: mems
};
if (!isPartial) ret.inheritance = inheritance() || null;
consume("{") || error("Bodyless interface");
while (true) {
if (consume("}")) {
consume(";") || error("Missing semicolon after interface");
return ret;
}
const ea = extended_attrs();
const mem = const_() ||
static_member() ||
stringifier() ||
iterable() ||
attribute() ||
operation() ||
error("Unknown member");
mem.extAttrs = ea;
ret.members.push(mem);
}
}
function mixin_rest(isPartial) {
if (!consume("mixin")) return;
const name = consume(ID) || error("No name for interface mixin");
const mems = [];
const ret = current = {
type: "interface mixin",
name: isPartial ? name.value : sanitize_name(name.value, "interface mixin"),
partial: isPartial,
members: mems
};
consume("{") || error("Bodyless interface mixin");
while (true) {
if (consume("}")) {
consume(";") || error("Missing semicolon after interface mixin");
return ret;
}
const ea = extended_attrs();
const mem = const_() ||
stringifier() ||
attribute({ noInherit: true }) ||
operation({ regular: true }) ||
error("Unknown member");
mem.extAttrs = ea;
ret.members.push(mem);
}
}
function interface_(isPartial) {
if (!consume("interface")) return;
return mixin_rest(isPartial) ||
interface_rest(isPartial) ||
error("Interface has no proper body");
}
function namespace(isPartial) {
if (!consume("namespace")) return;
const name = consume(ID) || error("No name for namespace");
const mems = [];
const ret = current = {
type: "namespace",
name: isPartial ? name.value : sanitize_name(name.value, "namespace"),
partial: isPartial,
members: mems
};
consume("{") || error("Bodyless namespace");
while (true) {
if (consume("}")) {
consume(";") || error("Missing semicolon after namespace");
return ret;
}
const ea = extended_attrs();
const mem = attribute({ noInherit: true, readonly: true }) ||
operation({ regular: true }) ||
error("Unknown member");
mem.extAttrs = ea;
ret.members.push(mem);
}
}
function partial() {
if (!consume("partial")) return;
const thing = dictionary(true) ||
interface_(true) ||
namespace(true) ||
error("Partial doesn't apply to anything");
return thing;
}
function dictionary(isPartial) {
if (!consume("dictionary")) return;
const name = consume(ID) || error("No name for dictionary");
const mems = [];
const ret = current = {
type: "dictionary",
name: isPartial ? name.value : sanitize_name(name.value, "dictionary"),
partial: isPartial,
members: mems
};
if (!isPartial) ret.inheritance = inheritance() || null;
consume("{") || error("Bodyless dictionary");
while (true) {
if (consume("}")) {
consume(";") || error("Missing semicolon after dictionary");
return ret;
}
const ea = extended_attrs();
const required = consume("required");
const typ = type_with_extended_attributes("dictionary-type") || error("No type for dictionary member");
const name = consume(ID) || error("No name for dictionary member");
const dflt = default_() || null;
if (required && dflt) error("Required member must not have a default");
const member = {
type: "field",
name: unescape(name.value),
escapedName: name.value,
required: !!required,
idlType: typ,
extAttrs: ea,
default: dflt
};
ret.members.push(member);
consume(";") || error("Unterminated dictionary member");
}
}
function enum_() {
if (!consume("enum")) return;
const name = consume(ID) || error("No name for enum");
const vals = [];
const ret = current = {
type: "enum",
name: sanitize_name(name.value, "enum"),
values: vals
};
consume("{") || error("No curly for enum");
let value_expected = true;
while (true) {
if (consume("}")) {
if (!ret.values.length) error("No value in enum");
consume(";") || error("No semicolon after enum");
return ret;
}
else if (!value_expected) {
error("No comma between enum values");
}
const val = consume(STR) || error("Unexpected value in enum");
val.value = val.value.slice(1, -1);
// No trivia exposure yet
val.trivia = undefined;
ret.values.push(val);
value_expected = !!consume(",");
}
}
function typedef() {
if (!consume("typedef")) return;
const ret = {
type: "typedef"
};
ret.idlType = type_with_extended_attributes("typedef-type") || error("No type in typedef");
const name = consume(ID) || error("No name in typedef");
ret.name = sanitize_name(name.value, "typedef");
current = ret;
consume(";") || error("Unterminated typedef");
return ret;
}
function implements_() {
const start_position = consume_position;
const target = consume(ID);
if (!target) return;
if (consume("implements")) {
const ret = {
type: "implements",
target: target.value
};
const imp = consume(ID) || error("Incomplete implements statement");
ret.implements = imp.value;
consume(";") || error("No terminating ; for implements statement");
return ret;
} else {
// rollback
unconsume(start_position);
}
}
function includes() {
const start_position = consume_position;
const target = consume(ID);
if (!target) return;
if (consume("includes")) {
const ret = {
type: "includes",
target: target.value
};
const imp = consume(ID) || error("Incomplete includes statement");
ret.includes = imp.value;
consume(";") || error("No terminating ; for includes statement");
return ret;
} else {
// rollback
unconsume(start_position);
}
}
function definition() {
return callback() ||
interface_(false) ||
partial() ||
dictionary(false) ||
enum_() ||
typedef() ||
implements_() ||
includes() ||
namespace(false);
}
function definitions() {
if (!tokens.length) return [];
const defs = [];
while (true) {
const ea = extended_attrs();
const def = definition();
if (!def) {
if (ea.length) error("Stray extended attributes");
break;
}
def.extAttrs = ea;
defs.push(def);
}
return defs;
}
const res = definitions();
if (consume_position < tokens.length) error("Unrecognised tokens");
return res;
}
const obj = {
parse(str) {
const tokens = tokenise(str);
return parse(tokens);
}
};
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = obj;
} else if (typeof define === 'function' && define.amd) {
define([], () => obj);
} else {
(self || window).WebIDL2 = obj;
}
})();