Skip to content
Find file
Fetching contributors…
Cannot retrieve contributors at this time
executable file 532 lines (470 sloc) 17 KB
#!/usr/bin/env node
/*
* idl2domjs: convert WebIDL interfaces into JavaScript source for dom.js
*
* Reads WebIDL from the file specified on the command line and writes
* JavaScript code to standard output. The output code is idiosyncratic
* to the dom.js project. In particular, the code it generates uses functions
* defined in ../src/idl.js
*
* This program makes no attempt to handle every nuance of WebIDL.
* I'll add features as I need them to translate particular DOM interfaces.
*
* This script uses darobin's JavaScript WebIDL parser:
* https://github.com/darobin/webidl.js as a submodule. To set up the
* submodules, use "git submodule init" and "git submodule update".
*
* This program treats the [Callback] attribute like the [NoInterfaceObject]
* attribute and does not create an interface for it. So for DOM Core,
* there is no EventListener. Also, [Callback] interfaces
* need a different kind of conversion than other interfaces: they're not
* unwrapped, we just have to check that they are functions or objects.
*
* XXX Still need to handle exceptions
*
* XXX: I can probably automatically generate some simple tests from the
* IDL file as well. For each interface I, output a testI() function that
* takes an instance of the interface and verifies simple things like
* instanceof, that it has all of the attributes, operations and constants.
* For attributes, it could read the attribute and test that the value
* was the correct type. So testDocument() would call testElement() on the
* value of the documentElement property? And testElement() would chain to
* testNode(), since that is the superclass?
*
* Not sure how valuable these tests would be if they're generated by
* the same code that generates the classes....
*/
var infile = process.argv[2];
var out = console.log.bind(console);
if (!infile) {
console.error("No input file specified");
process.exit(1);
}
var sourceidl = require("fs").readFileSync(infile,"utf-8");
var webidl = require("../deps/webidl.js/node/WebIDLParser.js")
var parsedidl;
try {
parsedidl = webidl.Parser.parse(sourceidl);
}
catch(e) {
console.log(e.name, ":", e.line, ":", e.column, ": ", e.message);
process.exit();
}
out("//");
out("// DO NOT EDIT.");
out("// This file was generated by idl2domjs from %s", infile);
out("//");
out();
var callbackTypes = {}; // A set of names of [Callback] interfaces
var arrayTypes = {}; // A set of array types we have to create
var dictionaryTypes = {} // A set of dictionary types
// First, loop through the interfaces, looking for callbacks and dictionaries
for(var i = 0; i < parsedidl.length; i++) {
var item = parsedidl[i];
if (item.type === "interface") {
var attrs = getExtAttrs(item);
if ("Callback" in attrs) {
callbackTypes[item.name] = true;
}
}
else if (item.type === "dictionary") {
dictionaryTypes[item.name] = item;
}
}
// Loop again, looking for, and fixing up implements declarations
for(var i = 0; i < parsedidl.length; i++) {
var item = parsedidl[i];
if (item.type === "implements") {
var targetName = item.target;
var sourceName = item.implements;
var target = null, source = null;
parsedidl.forEach(function(x) {
if (x.type !== "interface") return;
if (x.name === targetName) target = x;
if (x.name === sourceName) source = x;
});
if (target === null || source === null) {
console.warn("Skipping %s implements %s: \n\t" +
"source and/or target interface is unknown.",
item.target, item["implements"]);
}
else {
var copy = JSON.parse(JSON.stringify(source.members));
copy.forEach(function(x) { target.members.push(x); });
}
}
}
// Now loop through the interfaces, outputing definitions for them all
for(var i = 0; i < parsedidl.length; i++) {
var item = parsedidl[i];
if (item.type === "interface") {
outputInterface(item);
}
else if (item.type === "dictionary") {
outputDictionary(item);
}
else if (item.type === "implements") {
// Do nothing: we already handled implements declarations
}
else {
console.warn("Skipping %s %s", item.type, item.name);
}
}
// Now that we've output code for each of the IDL interfaces, we need
// to output classes for any array types that the IDL used.
// XXX: probably for sequences, too, if any APIs use them
for(var type in arrayTypes) {
out();
out('defineLazyProperty(idl, "%s", function() {', type);
out(' return new IDLInterface({');
out(' name: "%s",', type);
out(' proxyFactory: %sProxy,', type);
out(' });')
out('});')
}
var current_interface; // For error messages
function outputInterface(idl) {
// Don't output anything for Callback and NoInterfaceObject interfaces
var attrs = getExtAttrs(idl);
if ("Callback" in attrs || "NoInterfaceObject" in attrs) return;
current_interface = idl;
out();
out("//");
out("// Interface %s", idl.name);
out("//");
out();
var members = idl.members;
// Are there any constants in this interface?
var constants = members.some(function(m) { return m.type === "const" })
var prefix = ' '; // 12 spaces for pretty-printing
// If there are constants, define them "globally" (within the closure)
// so that they can be shared by the idl class and the implementation
// class and can be used without prefixes.
// XXX: this will break if dom core or html5 defines two constants
// with the same name in different interfaces.
if (constants) {
out("// Constants defined by %s", idl.name);
members.forEach(function(m) {
if (m.type === "const") {
// XXX what if the constant value needs to be quoted?
// I suppose that technically I should check the type of
// the constant.
out("const %s = %s;", m.name, m.value);
}
});
out();
}
out('defineLazyProperty(global, "' + idl.name + '", function() {');
out(' return idl.' + idl.name + '.publicInterface;');
out('}, true);');
out();
out('defineLazyProperty(idl, "' + idl.name + '", function() {');
out(' return new IDLInterface({');
out(' name: "' + idl.name + '",');
if (idl.inheritance) {
out(' superclass: idl.' + idl.inheritance[0] + ',');
}
// See if this interface has a constructor
// XXX: eventually I'll have to handle multiple constructor and
// named constructors as well. For now, just enough for event constructors.
if ("Constructor" in attrs) {
var c = attrs.Constructor;
out(' constructor: function %s%s{',
idl.name,
arglist(c," "));
out(' return wrap(new impl.%s(%s));',
idl.name,
invokeargs(c, prefix + " "));
out(' },');
}
// If the interface needs a proxy (because it has an indexed getter, e.g.)
// then specify that
if (needsProxy(idl)) {
out(' proxyFactory: %sProxy,', idl.name);
}
// If the interface defines constants
if (constants) {
out(' constants: {');
members.forEach(function(m) {
if (m.type === "const") {
// Output name twice because we declared the name
// as a const above.
out("%s%s: %s,", prefix, m.name, m.name);
}
});
out(' },');
}
out(' members: {');
members.forEach(function(m) {
// Check for overloaded members
if (m.name !== "") { // getters, setters, etc., have no name
var n = members.filter(function(x) {
return x.name === m.name;
}).length;
if (n > 1) {
console.warn("Skipping overloaded member %s.%s",
idl.name, m.name);
return;
}
}
if (m.type === "attribute")
outputAttribute(m, prefix);
else if (m.type === "operation") {
outputMethod(m, prefix);
}
});
out(' },');
out(' });');
out('});');
}
/*
* WebIDL dictionary types don't have any public representation, and
* the JS representation of dictionary instances are ordinary objects.
* So to output a dictionary we don't need to export anything to the global
* object. But we do need to create a conversion function that converts
* an object passed in to a DOM API (like the Event()) constructor to a
* type-checked legal object that we can pass to the implementation of that
* API
*/
function outputDictionary(d) {
current_interface = d;
out();
out("//");
out("// Dictionary %s", d.name);
out("//");
out();
// Looks like a constructor, but we're using it as a conversion func
out("function %s(o) {", d.name);
out(" var rv = O.create(%s.prototype);", d.name);
outputDictionaryMemberConversions(d);
out(" return rv;");
out("}");
out("function Optional%s(o) {", d.name);
out(" return (o === undefined) ? undefined : %s(o);", d.name);
out("}");
if (d.inheritance)
out("%s.prototype = O.create(%s.prototype);",
d.name, d.inheritance);
else
out("%s.prototype = {};", d.name);
for(var i = 0; i < d.members.length; i++) {
if (d.members[i].defaultValue) {
out("%s.prototype.%s = %s;",
d.name,
d.members[i].name,
d.members[i].defaultValue);
}
}
}
function outputDictionaryMemberConversions(d) {
if (d.inheritance) {
var s = dictionaryTypes[d.inheritance];
if (!s) {
console.warn("Unknown dictionary superclass %s", d.inheritance);
}
else {
outputDictionaryMemberConversions(s);
}
}
for(var i = 0; i < d.members.length; i++) {
var m = d.members[i];
out(" if ('%s' in o) rv['%s'] = %s;",
m.name,
m.name,
convert(m, "o['" + m.name + "']"));
}
}
function needsProxy(idl) {
// If any of the members of this interface are getters, then
// we need a proxy.
return idl.members.some(function(m) {
return (m.type === "operation") &&
(m.getter || m.setter || m.deleter || m.creator || m.caller);
});
}
function outputMethod(m, prefix) {
// Don't output special operation methods if they are omittable or if
// they don't have a name.
if (m.getter || m.setter || m.deleter || m.creator || m.caller) {
if (m.name === "" || m.omittable) return;
}
// Except for stringifiers: if a stringifier doesn't have a name,
// then name it toString.
// XXX What to do if a stringifier does have a name?
if (m.stringifier) {
if (m.name === "")
m.name = "toString";
else {
console.warn("Skipping named stringifier %s in %s",
m.name, current_interface.name);
return;
}
}
out("%s%s: function %s%s{", // intentionally missing spaces
prefix, m.name, m.name, arglist(m, prefix));
var numargs = m.arguments.length;
var variadic = numargs !== 0 && m.arguments[numargs-1].variadic;
var invocation;
if (variadic) {
var fixedargs = "";
for(var i = 0; i < m.arguments.length-1; i++) {
if (i > 0) fixedargs += ", ";
fixedargs += convert(m.arguments[i], m.arguments[i].name);
}
out("%s var context = unwrap(this);", prefix);
out("%s var args = [" + fixedargs + "];", prefix);
var lastarg = m.arguments[numargs-1];
out("%s for(var i = " +
(numargs-1) +
"; i < arguments.length; i++) {", prefix);
out("%s push(args, " +
convert(lastarg, "arguments[i]") + ");", prefix);
out("%s }", prefix);
invocation = "apply(context." + m.name + ", context, args)";
}
else {
invocation = "unwrap(this)." + m.name + "(" +
invokeargs(m, prefix + " ") + ")";
}
if (m.idlType === "void") { // method has no return value
out("%s %s;", prefix, invocation);
}
else if (isInterfaceType(m.idlType)) {
out("%s return wrap(%s);", prefix, invocation);
if (m.idlType.array) {
// If this is an array type, remember that we've used it
// so that we can output an IDL interface for it later.
arrayTypes[m.idlType.idlType + "Array"] = true;
}
}
else {
out("%s return %s;", prefix, invocation);
}
out("%s},", prefix);
out();
}
// Return a comma-separated arg list for method m
function arglist(m, prefix) {
if (m.arguments.length == 0) {
return "() ";
}
else if (m.arguments.length == 1) {
return "(" +
m.arguments[0].name +
(m.arguments[0].variadic ? " /*...*/":"") +
") ";
}
else {
var indent = "\n " + prefix;
var args = m.arguments.map(function(a) {
return a.name + (a.variadic ? " /*...*/" : "");
});
var list = args.join("," + indent)
return "(" + indent + list + ")\n" + prefix;
}
}
// Convert the args and return a list suitable for an invocation
function invokeargs(m, prefix) {
var numargs = m.arguments.length;
var rv = m.arguments.
map(function(a) { return convert(a, a.name)}).
join(",\n" + prefix);
if (numargs > 1) rv = "\n" + prefix + rv;
return rv;
}
function outputAttribute(a, prefix) {
out("%sget %s() {", prefix, a.name);
if (isInterfaceType(a.idlType)) {
out("%s return wrap(unwrap(this).%s);", prefix, a.name);
if (a.idlType.array) {
// If this is an array type, remember that we've used it
// so that we can output an IDL interface for it later.
arrayTypes[a.idlType.idlType + "Array"] = true;
}
}
else {
out("%s return unwrap(this).%s;", prefix, a.name);
}
out("%s},", prefix);
if (!a.readonly) {
// XXX output the setter method here
out("%sset %s(newval) {", prefix, a.name);
out("%s unwrap(this).%s = %s;",
prefix, a.name, convert(a, "newval"));
out("%s},", prefix);
}
out();
}
// XXX: interfaces with more than one Constructor attribute will have
// to do something different here. Currently we'll only see the last one.
function getExtAttrs(a) {
var rv = {};
if (a.extAttrs) a.extAttrs.forEach(function(o) { rv[o.name] = o; });
return rv;
}
// Return a string that invokes a conversion function on v, like Boolean(v)
// A is an attribute or a method argument or a dictionary member
function convert(a, v) {
// a is an attribute record or an argument record
var t = a.idlType || a.type; // idlType for attributes, type for arguments
if (t.sequence || t.array) {
console.warn("Don't know now to convert sequence or array of %s in %s",
t.idlType, current_interface.name)
return v;
}
var c;
if (callbackTypes[t.idlType])
c = "toCallback";
else if (isDictionaryType(t))
c = t.idlType;
else if (isInterfaceType(t))
c = "unwrap";
else if (t.idlType === "DOMString")
c = "String";
else if (t.idlType === "DOMTimeStamp")
c = "Number"; // XXX: really unsigned long long
else if (t.idlType === "unsigned long")
c = "toULong";
else if (t.idlType === "long")
c = "toLong";
else if (t.idlType === "unsigned short")
c = "toUShort";
else if (t.idlType === "double")
c = "toDouble";
else if (t.idlType === "boolean")
c = "Boolean";
else if (t.idlType === "object")
c = "Object";
else if (t.idlType === "any")
c = "";
else
throw new Error("Need a converter function for: " + t.idlType);
if (t.nullable) c += "OrNull";
if (a.optional) c = "Optional" + c;
// XXX:
// WebIDL may be making this the default and using
// TreatNullAs=String for the current default behavior.
var attrs = getExtAttrs(a);
if (t.idlType === "DOMString" &&
attrs.TreatNullAs &&
attrs.TreatNullAs.value === "EmptyString")
c += "OrEmpty";
if (c)
return c + "(" + v + ")";
else
return v;
}
function isDictionaryType(t) {
return t.idlType && dictionaryTypes[t.idlType];
}
function isInterfaceType(t) {
// WebIDL primitive types are all lower case or DOMString
// We'll just assume that anything else is an interface type
// Unless we know it is a callback type, like Function
if (t === "void") // Annoyingly, void is just a string
return false;
return (t.idlType !== "DOMString" &&
t.idlType !== "DOMTimeStamp" &&
!(t.idlType in callbackTypes) &&
t.idlType !== t.idlType.toLowerCase())
}
Something went wrong with that request. Please try again.