mirror of
https://github.com/servo/servo.git
synced 2025-07-12 01:43:43 +01:00
970 lines
29 KiB
JavaScript
970 lines
29 KiB
JavaScript
"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;
|
|
}
|
|
})();
|