Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@ webidl2js is implementing an ever-growing subset of the Web IDL specification. S
- `FrozenArray<>` types
- Buffer source types
- `typedef`s
- Partial interfaces and dictionaries
- Namespaces
- Partial interfaces, namespaces, and dictionaries
- Interface mixins
- Basic types (via [webidl-conversions][])
- Overload resolution (although [tricky cases are not easy on the implementation class](#overloaded-operations))
Expand Down Expand Up @@ -508,7 +509,6 @@ Supported Web IDL extensions defined in HTML:

Notable missing features include:

- Namespaces
- `maplike<>` and `setlike<>`
- `async_sequence<>` types
- `ObservableArray<>` types
Expand Down
29 changes: 19 additions & 10 deletions lib/constructs/attribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Attribute {
this.interface = I;
this.idl = idl;
this.static = idl.special === "static";
this.namespace = I.type === "namespace";
}

getWhence() {
Expand All @@ -27,10 +28,11 @@ class Attribute {
generate() {
const requires = new utils.RequiresMap(this.ctx);

const whence = this.getWhence();
const whence = this.namespace ? null : this.getWhence();
const configurable = whence !== "unforgeables";
const shouldReflect =
this.idl.extAttrs.some(attr => attr.name.startsWith("Reflect")) && this.ctx.processReflect !== null;
!this.namespace && this.idl.extAttrs.some(attr => attr.name.startsWith("Reflect")) &&
this.ctx.processReflect !== null;
const sameObject = utils.getExtAttr(this.idl.extAttrs, "SameObject");

const async = this.idl.idlType.generic === "Promise";
Expand All @@ -48,13 +50,15 @@ class Attribute {
getterBody = `return esValue[implSymbol]["${this.idl.name}"];`;
}

const addMethod = this.static ?
const addMethod = this.static || this.namespace ?
this.interface.addStaticMethod.bind(this.interface) :
this.interface.addMethod.bind(this.interface, whence);

if (this.static) {
if (this.static || this.namespace) {
brandCheck = "";
getterBody = `return Impl.implementation["${this.idl.name}"];`;
getterBody = conversions[this.idl.idlType.idlType] ?
`return Impl.implementation["${this.idl.name}"];` :
`return utils.tryWrapperForImpl(Impl.implementation["${this.idl.name}"]);`;
setterBody = `Impl.implementation["${this.idl.name}"] = V;`;
} else if (shouldReflect) {
const processedOutput = this.ctx.invokeProcessReflect(this.idl, "esValue[implSymbol]", { requires });
Expand All @@ -75,7 +79,8 @@ class Attribute {
}

if (sameObject) {
getterBody = `return utils.getSameObject(this, "${this.idl.name}", () => { ${getterBody} });`;
const cacheKey = this.namespace ? "namespaceObject" : "this";
getterBody = `return utils.getSameObject(${cacheKey}, "${this.idl.name}", () => { ${getterBody} });`;
}

if (utils.hasCEReactions(this.idl)) {
Expand All @@ -85,9 +90,13 @@ class Attribute {
setterBody = this.ctx.invokeProcessCEReactions(setterBody, processorConfig);
}

const esValueSetup = this.static || this.namespace ?
"" :
"const esValue = this !== null && this !== undefined ? this : globalObject;";

addMethod(this.idl.name, [], `
${promiseHandlingBefore}
const esValue = this !== null && this !== undefined ? this : globalObject;
${esValueSetup}
${brandCheck.replace("$KEYWORD$", "get")}
${getterBody}
${promiseHandlingAfter}
Expand Down Expand Up @@ -124,7 +133,7 @@ class Attribute {
}

addMethod(this.idl.name, ["V"], `
const esValue = this !== null && this !== undefined ? this : globalObject;
${esValueSetup}
${brandCheck}
${idlConversion}
${setterBody}
Expand Down Expand Up @@ -160,7 +169,7 @@ class Attribute {

if (setterBody) {
addMethod(this.idl.name, ["V"], `
const esValue = this !== null && this !== undefined ? this : globalObject;
${esValueSetup}
${brandCheck}
${setterBody}
`, "set", { configurable });
Expand All @@ -176,7 +185,7 @@ class Attribute {
}
}

if (!this.static && this.idl.special === "stringifier") {
if (!this.static && !this.namespace && this.idl.special === "stringifier") {
addMethod("toString", [], `
const esValue = this;
if (!exports.is(esValue)) {
Expand Down
3 changes: 3 additions & 0 deletions lib/constructs/constant.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ class Constant {
configurable: false,
writable: false
});
if (this.interface.type === "namespace") {
return { requires: new utils.RequiresMap(this.ctx) };
}
this.interface.addProperty(this.interface.defaultWhence, this.idl.name, utils.getDefault(this.idl.value), {
configurable: false,
writable: false
Expand Down
244 changes: 244 additions & 0 deletions lib/constructs/namespace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
"use strict";

const utils = require("../utils");
const Attribute = require("./attribute");
const Constant = require("./constant");
const Operation = require("./operation");

const defaultObjectLiteralDescriptor = {
configurable: true,
enumerable: true,
writable: true
};

class Namespace {
constructor(ctx, idl, opts) {
this.ctx = ctx;
this.idl = idl;
this.name = idl.name;
this.opts = opts;

this.str = null;
this.requires = new utils.RequiresMap(ctx);

this.operations = new Map();
this.attributes = new Map();
this.constants = new Map();

this._analyzed = false;
this._outputStaticMethods = new Map();
this._outputStaticProperties = new Map();

const exposed = utils.getExtAttr(this.idl.extAttrs, "Exposed");
if (!exposed) {
throw new Error(`Namespace ${this.name} lacks [Exposed]`);
}

if (!exposed.rhs || (exposed.rhs.type !== "identifier" && exposed.rhs.type !== "identifier-list")) {
throw new Error(`[Exposed] must take an identifier or an identifier list in namespace ${this.name}`);
}

if (exposed.rhs.type === "identifier") {
this.exposed = new Set([exposed.rhs.value]);
} else {
this.exposed = new Set(exposed.rhs.value.map(token => token.value));
}
}

// type is either "regular", "get", or "set"
addStaticMethod(propName, args, body, type = "regular", {
configurable = true,
enumerable = typeof propName === "string",
writable = type === "regular" ? true : undefined
} = {}) {
if (type !== "regular") {
const existing = this._outputStaticMethods.get(propName);
if (existing !== undefined) {
if (type === "get") {
existing.body[0] = body;
} else {
existing.args = args;
existing.body[1] = body;
}
return;
}

const pair = new Array(2);
pair[type === "get" ? 0 : 1] = body;
body = pair;
type = "accessor";
}

const descriptor = { configurable, enumerable, writable };
this._outputStaticMethods.set(propName, { type, args, body, descriptor });
}

addStaticProperty(propName, str, {
configurable = true,
enumerable = typeof propName === "string",
writable = true
} = {}) {
const descriptor = { configurable, enumerable, writable };
this._outputStaticProperties.set(propName, { body: str, descriptor });
}

_analyzeMembers() {
for (const member of this.idl.members) {
switch (member.type) {
case "operation":
if (!member.name) {
if (!this.ctx.options.suppressErrors) {
throw new Error(`Unnamed operation in namespace ${this.name}`);
}
break;
}
if (!this.operations.has(member.name)) {
this.operations.set(member.name, new Operation(this.ctx, this, member));
} else {
this.operations.get(member.name).idls.push(member);
}
break;
case "attribute":
if (!member.readonly) {
if (!this.ctx.options.suppressErrors) {
throw new Error(`Namespace attribute "${member.name}" on ${this.name} must be readonly`);
}
break;
}
this.attributes.set(member.name, new Attribute(this.ctx, this, member));
break;
case "const":
this.constants.set(member.name, new Constant(this.ctx, this, member));
break;
default:
if (!this.ctx.options.suppressErrors) {
throw new Error(`Unknown IDL member type "${member.type}" in namespace ${this.name}`);
}
}
}
}

addAllMethodsProperties() {
this.addStaticProperty(Symbol.toStringTag, JSON.stringify(this.name), {
writable: false
});

for (const member of this.operations.values()) {
const data = member.generate();
this.requires.merge(data.requires);
}

for (const member of [...this.attributes.values(), ...this.constants.values()]) {
const data = member.generate();
this.requires.merge(data.requires);
}
}

generateNamespaceObject() {
const members = [];
const props = new Map();

function addOne(name, args, body) {
members.push(`
${name}(${utils.formatArgs(args)}) {${body}}
`);
}

for (const [name, { type, args, body, descriptor }] of this._outputStaticMethods) {
const propName = utils.stringifyPropertyKey(name);
if (type === "regular") {
addOne(propName, args, body);
} else {
if (body[0] !== undefined) {
addOne(`get ${propName}`, [], body[0]);
}
if (body[1] !== undefined) {
addOne(`set ${propName}`, args, body[1]);
}
}

const descriptorModifier = utils.getPropertyDescriptorModifier(defaultObjectLiteralDescriptor, descriptor, type);
if (descriptorModifier === undefined) {
continue;
}
props.set(propName, descriptorModifier);
}

for (const [name, { body, descriptor }] of this._outputStaticProperties) {
const descriptorModifier =
utils.getPropertyDescriptorModifier(utils.defaultDefinePropertyDescriptor, descriptor, "regular", body);
props.set(utils.stringifyPropertyKey(name), descriptorModifier);
}

const propStrs = [...props].map(([name, body]) => `${name}: ${body}`);

this.str += `
const namespaceObject = Object.create(globalObject.Object.prototype);
`;
if (members.length > 0) {
this.str += `
utils.define(namespaceObject, {
${members.join(", ")}
});
`;
}
if (propStrs.length > 0) {
this.str += `
Object.defineProperties(namespaceObject, {
${propStrs.join(", ")}
});
`;
}
}

generateInstall() {
this.str += `
const namespaceName = "${this.name}";
const exposed = new Set(${JSON.stringify([...this.exposed])});

exports.install = (globalObject, globalNames) => {
if (!globalNames.some(globalName => exposed.has(globalName))) {
return;
}
`;

this.generateNamespaceObject();

this.str += `
Object.defineProperty(globalObject, namespaceName, {
configurable: true,
writable: true,
value: namespaceObject
});
};
`;
}

generateRequires() {
this.str = `
${this.requires.generate()}

${this.str}
`;
}

generate() {
this.generateInstall();
this.generateRequires();
}

toString() {
this.str = "";
if (!this._analyzed) {
this._analyzed = true;
this._analyzeMembers();
}
this.addAllMethodsProperties();
this.generate();
return this.str;
}
}

Namespace.prototype.type = "namespace";

module.exports = Namespace;
Loading