Skip to content

Commit

Permalink
Add [CEReactions] and [HTMLConstructor] processors
Browse files Browse the repository at this point in the history
These will be used for jsdom to implement custom elements (jsdom/jsdom#2548).
  • Loading branch information
pmdartus authored and domenic committed Jan 20, 2020
1 parent 7c8037f commit fda810e
Show file tree
Hide file tree
Showing 11 changed files with 762 additions and 79 deletions.
72 changes: 69 additions & 3 deletions README.md
Expand Up @@ -108,8 +108,9 @@ transformer.generate("wrappers").catch(err => {

The main module's default export is a class which you can construct with a few options:

- `implSuffix`: a suffix used, if any, to find files within the source directory based on the IDL file name.
- `suppressErrors`: set to true to suppress errors during generation.
- `implSuffix`: a suffix used, if any, to find files within the source directory based on the IDL file name
- `suppressErrors`: set to true to suppress errors during generation
- `processCEReactions` and `processHTMLConstructor`: see below

The `addSource()` method can then be called multiple times to add directories containing `.webidl` IDL files and `.js` implementation class files.

Expand All @@ -121,6 +122,66 @@ The transformer will also generate a file named `utils.js` inside the wrapper cl

Note that webidl2js works best when there is a single transformer instance that knows about as many files as possible. This allows it to resolve type references, e.g. when one operation has an argument type referring to some other interface.

### `[CEReactions]` and `[HTMLConstructor]`

By default webidl2js ignores HTML Standard-defined extended attributes [`[CEReactions]`](https://html.spec.whatwg.org/multipage/custom-elements.html#cereactions) and [`[HTMLConstructor]`](https://html.spec.whatwg.org/multipage/dom.html#htmlconstructor), since they require detailed knowledge of the host environment to implement correctly. The `processCEReactions` and `processHTMLConstructor` hooks provide a way to customize the generation of the wrapper class files when these extended attributes are present.

Both hooks have the signature `(code) => replacementCode`, where:

- `code` is the code generated by webidl2js normally, for calling into the impl class.

- `replacementCode` is the new code that will be output in place of `code` in the wrapper class.

If either hook is omitted, then the code will not be replaced, i.e. the default is equivalent to `(code) => code`.

Both hooks also have some utility methods that are accessible via `this`:

- `addImport(relPath, [importedIdentifier])` utility to require external modules from the generated interface. This method accepts 2 parameters: `relPath` the relative path from the generated interface file, and an optional `importedIdentifier` the identifier to import. This method returns the local identifier from the imported path.

The following variables are available in the scope of the replacement code:

- `globalObject` (object) is the global object associated with the interface

- `interfaceName` (string) is the name of the interface

An example of code that uses these hooks is as follows:

```js
"use strict";
const WebIDL2JS = require("webidl2js");

const transformer = new WebIDL2JS({
implSuffix: "-impl",
processCEReactions(code) {
// Add `require("../ce-reactions")` to generated file.
const ceReactions = this.addImport("../ce-reactions");

return `
${ceReactions}.preSteps(globalObject);
try {
${code}
} finally {
${ceReactions}.postSteps(globalObject);
}
`;
},
processHTMLConstructor(/* code */) {
// Add `require("../HTMLConstructor").HTMLConstructor` to generated file.
const htmlConstructor = this.addImport("../HTMLConstructor", "HTMLConstructor");

return `
return ${htmlConstructor}(globalObject, interfaceName);
`;
}
});

transformer.addSource("idl", "impls");
transformer.generate("wrappers").catch(err => {
console.error(err.stack);
process.exit(1);
});
```

## Generated wrapper class file API

The example above showed a simplified generated wrapper file with only three exports: `create`, `is`, and `interface`. In reality the generated wrapper file will contain more functionality, documented here. This functionality is different between generated wrapper files for interfaces and for dictionaries.
Expand Down Expand Up @@ -338,6 +399,11 @@ webidl2js is implementing an ever-growing subset of the Web IDL specification. S
- `[Unforgeable]`
- `[Unscopable]`

Supported Web IDL extensions defined in HTML:

- `[CEReactions]` - behavior can be defined via the `processCEReactions` hook
- `[HTMLConstructor]` - behavior can be defined via the `processHTMLConstructor` hook

Notable missing features include:

- Namespaces
Expand Down Expand Up @@ -366,7 +432,7 @@ By default the attribute passed to `this.getAttribute` and `this.setAttribute` w

Note that only the basics of the reflect algorithm are implemented so far: `boolean`, `DOMString`, `long`, and `unsigned long`, with no parametrizations.

In the future we may move this extended attribute out into some sort of plugin, since it is more related to HTML than to Web IDL.
In the future we may change this extended attribute to be handled by the caller, similar to `[CEReactions]` and `[HTMLConstructor]`, since it is more related to HTML than to Web IDL.

### `[WebIDL2JSValueAsUnsupported=value]`

Expand Down
7 changes: 7 additions & 0 deletions lib/constructs/attribute.js
Expand Up @@ -63,6 +63,13 @@ class Attribute {
getterBody = `return utils.getSameObject(this, "${this.idl.name}", () => { ${getterBody} });`;
}

if (utils.hasCEReactions(this.idl)) {
const processorConfig = { requires };

getterBody = this.ctx.invokeProcessCEReactions(getterBody, processorConfig);
setterBody = this.ctx.invokeProcessCEReactions(setterBody, processorConfig);
}

addMethod(this.idl.name, [], `
${brandCheck}
${getterBody}
Expand Down
57 changes: 45 additions & 12 deletions lib/constructs/interface.js
Expand Up @@ -66,6 +66,7 @@ class Interface {
this.ctx = ctx;
this.idl = idl;
this.name = idl.name;

for (const member of this.idl.members) {
member.definingInterface = this.name;
}
Expand Down Expand Up @@ -638,14 +639,15 @@ class Interface {
`"Failed to set the " + index + " property on '${this.name}': The provided value"`);
this.requires.merge(conv.requires);

let str = `
const prolog = `
const index = ${P} >>> 0;
let indexedValue = ${V};
${conv.body}
`;

let invocation;
if (!this.indexedSetter.name) {
str += `
invocation = `
const creating = !(${supportsPropertyIndex(O, "index")});
if (creating) {
${O}[impl][utils.indexedSetNew](index, indexedValue);
Expand All @@ -654,12 +656,18 @@ class Interface {
}
`;
} else {
str += `
invocation = `
${O}[impl].${this.indexedSetter.name}(index, indexedValue);
`;
}

return str;
if (utils.hasCEReactions(this.indexedSetter)) {
invocation = this.ctx.invokeProcessCEReactions(invocation, {
requires: this.requires
});
}

return prolog + invocation;
};

// "invoke a named property setter"
Expand All @@ -670,13 +678,15 @@ class Interface {
`"Failed to set the '" + ${P} + "' property on '${this.name}': The provided value"`);
this.requires.merge(conv.requires);

let str = `
const prolog = `
let namedValue = ${V};
${conv.body}
`;


let invocation;
if (!this.namedSetter.name) {
str += `
invocation = `
const creating = !(${supportsPropertyName(O, P)});
if (creating) {
${O}[impl][utils.namedSetNew](${P}, namedValue);
Expand All @@ -685,12 +695,18 @@ class Interface {
}
`;
} else {
str += `
invocation = `
${O}[impl].${this.namedSetter.name}(${P}, namedValue);
`;
}

return str;
if (utils.hasCEReactions(this.namedSetter)) {
invocation = this.ctx.invokeProcessCEReactions(invocation, {
requires: this.requires
});
}

return prolog + invocation;
};

this.str += `
Expand Down Expand Up @@ -1065,17 +1081,27 @@ class Interface {
return false;
`;
} else {
let invocation;
const func = this.namedDeleter.name ? `.${this.namedDeleter.name}` : "[utils.namedDelete]";

if (this.namedDeleter.idlType.idlType === "bool") {
this.str += `
invocation = `
return target[impl]${func}(P);
`;
} else {
this.str += `
invocation = `
target[impl]${func}(P);
return true;
`;
}

if (utils.hasCEReactions(this.namedDeleter)) {
invocation = this.ctx.invokeProcessCEReactions(invocation, {
requires: this.requires
});
}

this.str += invocation;
}
this.str += `
}
Expand Down Expand Up @@ -1192,6 +1218,12 @@ class Interface {
`;
}

if (utils.getExtAttr(this.idl.extAttrs, "HTMLConstructor")) {
body = this.ctx.invokeProcessHTMLConstructor(body, {
requires: this.requires
});
}

this.addMethod("prototype", "constructor", argNames, body, "regular", { enumerable: false });
}

Expand Down Expand Up @@ -1419,6 +1451,7 @@ class Interface {

this.str += `
exports.install = function install(globalObject) {
const interfaceName = "${name}";
`;

if (idl.inheritance) {
Expand All @@ -1441,9 +1474,9 @@ class Interface {
if (globalObject[ctorRegistry] === undefined) {
globalObject[ctorRegistry] = Object.create(null);
}
globalObject[ctorRegistry]["${name}"] = ${name};
globalObject[ctorRegistry][interfaceName] = ${name};
Object.defineProperty(globalObject, "${name}", {
Object.defineProperty(globalObject, interfaceName, {
configurable: true,
writable: true,
value: ${name}
Expand Down
13 changes: 11 additions & 2 deletions lib/constructs/operation.js
Expand Up @@ -39,6 +39,7 @@ class Operation {

generate() {
const requires = new utils.RequiresMap(this.ctx);

this.fixUpArgsExtAttrs();
let str = "";

Expand Down Expand Up @@ -78,16 +79,24 @@ class Operation {
requires.merge(parameterConversions.requires);
str += parameterConversions.body;

let invocation;
if (overloads.every(overload => conversions[overload.operation.idlType.idlType])) {
str += `
invocation = `
return ${callOn}.${implFunc}(${argsSpread});
`;
} else {
str += `
invocation = `
return utils.tryWrapperForImpl(${callOn}.${implFunc}(${argsSpread}));
`;
}

if (utils.hasCEReactions(this.idls[0])) {
invocation = this.ctx.invokeProcessCEReactions(invocation, {
requires
});
}
str += invocation;

if (this.static) {
this.interface.addStaticMethod(this.name, argNames, str);
} else {
Expand Down
38 changes: 37 additions & 1 deletion lib/context.js
Expand Up @@ -10,10 +10,22 @@ const builtinTypedefs = webidl.parse(`
typedef unsigned long long DOMTimeStamp;
`);

function defaultProcessor(_idl, code) {
return code;
}

class Context {
constructor({ implSuffix = "", options } = {}) {
constructor({
implSuffix = "",
processCEReactions = defaultProcessor,
processHTMLConstructor = defaultProcessor,
options
} = {}) {
this.implSuffix = implSuffix;
this.processCEReactions = processCEReactions;
this.processHTMLConstructor = processHTMLConstructor;
this.options = options;

this.initialize();
}

Expand Down Expand Up @@ -44,6 +56,30 @@ class Context {
}
return undefined;
}

invokeProcessCEReactions(code, config) {
return this._invokeProcessor(this.processCEReactions, code, config);
}

invokeProcessHTMLConstructor(code, config) {
return this._invokeProcessor(this.processHTMLConstructor, code, config);
}

_invokeProcessor(processor, code, config) {
const { requires } = config;

if (!requires) {
throw new TypeError("Internal error: missing requires object in context");
}

const context = {
addImport(source, imported) {
return requires.add(source, imported);
}
};

return processor.call(context, code);
}
}

module.exports = Context;
2 changes: 2 additions & 0 deletions lib/transformer.js
Expand Up @@ -17,6 +17,8 @@ class Transformer {
constructor(opts = {}) {
this.ctx = new Context({
implSuffix: opts.implSuffix,
processCEReactions: opts.processCEReactions,
processHTMLConstructor: opts.processHTMLConstructor,
options: {
suppressErrors: Boolean(opts.suppressErrors)
}
Expand Down

0 comments on commit fda810e

Please sign in to comment.