Skip to content

Commit fda810e

Browse files
pmdartusdomenic
authored andcommitted
Add [CEReactions] and [HTMLConstructor] processors
These will be used for jsdom to implement custom elements (jsdom/jsdom#2548).
1 parent 7c8037f commit fda810e

File tree

11 files changed

+762
-79
lines changed

11 files changed

+762
-79
lines changed

README.md

+69-3
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,9 @@ transformer.generate("wrappers").catch(err => {
108108

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

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

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

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

122123
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.
123124

125+
### `[CEReactions]` and `[HTMLConstructor]`
126+
127+
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.
128+
129+
Both hooks have the signature `(code) => replacementCode`, where:
130+
131+
- `code` is the code generated by webidl2js normally, for calling into the impl class.
132+
133+
- `replacementCode` is the new code that will be output in place of `code` in the wrapper class.
134+
135+
If either hook is omitted, then the code will not be replaced, i.e. the default is equivalent to `(code) => code`.
136+
137+
Both hooks also have some utility methods that are accessible via `this`:
138+
139+
- `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.
140+
141+
The following variables are available in the scope of the replacement code:
142+
143+
- `globalObject` (object) is the global object associated with the interface
144+
145+
- `interfaceName` (string) is the name of the interface
146+
147+
An example of code that uses these hooks is as follows:
148+
149+
```js
150+
"use strict";
151+
const WebIDL2JS = require("webidl2js");
152+
153+
const transformer = new WebIDL2JS({
154+
implSuffix: "-impl",
155+
processCEReactions(code) {
156+
// Add `require("../ce-reactions")` to generated file.
157+
const ceReactions = this.addImport("../ce-reactions");
158+
159+
return `
160+
${ceReactions}.preSteps(globalObject);
161+
try {
162+
${code}
163+
} finally {
164+
${ceReactions}.postSteps(globalObject);
165+
}
166+
`;
167+
},
168+
processHTMLConstructor(/* code */) {
169+
// Add `require("../HTMLConstructor").HTMLConstructor` to generated file.
170+
const htmlConstructor = this.addImport("../HTMLConstructor", "HTMLConstructor");
171+
172+
return `
173+
return ${htmlConstructor}(globalObject, interfaceName);
174+
`;
175+
}
176+
});
177+
178+
transformer.addSource("idl", "impls");
179+
transformer.generate("wrappers").catch(err => {
180+
console.error(err.stack);
181+
process.exit(1);
182+
});
183+
```
184+
124185
## Generated wrapper class file API
125186

126187
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.
@@ -338,6 +399,11 @@ webidl2js is implementing an ever-growing subset of the Web IDL specification. S
338399
- `[Unforgeable]`
339400
- `[Unscopable]`
340401

402+
Supported Web IDL extensions defined in HTML:
403+
404+
- `[CEReactions]` - behavior can be defined via the `processCEReactions` hook
405+
- `[HTMLConstructor]` - behavior can be defined via the `processHTMLConstructor` hook
406+
341407
Notable missing features include:
342408

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

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

369-
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.
435+
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.
370436

371437
### `[WebIDL2JSValueAsUnsupported=value]`
372438

lib/constructs/attribute.js

+7
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ class Attribute {
6363
getterBody = `return utils.getSameObject(this, "${this.idl.name}", () => { ${getterBody} });`;
6464
}
6565

66+
if (utils.hasCEReactions(this.idl)) {
67+
const processorConfig = { requires };
68+
69+
getterBody = this.ctx.invokeProcessCEReactions(getterBody, processorConfig);
70+
setterBody = this.ctx.invokeProcessCEReactions(setterBody, processorConfig);
71+
}
72+
6673
addMethod(this.idl.name, [], `
6774
${brandCheck}
6875
${getterBody}

lib/constructs/interface.js

+45-12
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class Interface {
6666
this.ctx = ctx;
6767
this.idl = idl;
6868
this.name = idl.name;
69+
6970
for (const member of this.idl.members) {
7071
member.definingInterface = this.name;
7172
}
@@ -638,14 +639,15 @@ class Interface {
638639
`"Failed to set the " + index + " property on '${this.name}': The provided value"`);
639640
this.requires.merge(conv.requires);
640641

641-
let str = `
642+
const prolog = `
642643
const index = ${P} >>> 0;
643644
let indexedValue = ${V};
644645
${conv.body}
645646
`;
646647

648+
let invocation;
647649
if (!this.indexedSetter.name) {
648-
str += `
650+
invocation = `
649651
const creating = !(${supportsPropertyIndex(O, "index")});
650652
if (creating) {
651653
${O}[impl][utils.indexedSetNew](index, indexedValue);
@@ -654,12 +656,18 @@ class Interface {
654656
}
655657
`;
656658
} else {
657-
str += `
659+
invocation = `
658660
${O}[impl].${this.indexedSetter.name}(index, indexedValue);
659661
`;
660662
}
661663

662-
return str;
664+
if (utils.hasCEReactions(this.indexedSetter)) {
665+
invocation = this.ctx.invokeProcessCEReactions(invocation, {
666+
requires: this.requires
667+
});
668+
}
669+
670+
return prolog + invocation;
663671
};
664672

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

673-
let str = `
681+
const prolog = `
674682
let namedValue = ${V};
675683
${conv.body}
676684
`;
677685

686+
687+
let invocation;
678688
if (!this.namedSetter.name) {
679-
str += `
689+
invocation = `
680690
const creating = !(${supportsPropertyName(O, P)});
681691
if (creating) {
682692
${O}[impl][utils.namedSetNew](${P}, namedValue);
@@ -685,12 +695,18 @@ class Interface {
685695
}
686696
`;
687697
} else {
688-
str += `
698+
invocation = `
689699
${O}[impl].${this.namedSetter.name}(${P}, namedValue);
690700
`;
691701
}
692702

693-
return str;
703+
if (utils.hasCEReactions(this.namedSetter)) {
704+
invocation = this.ctx.invokeProcessCEReactions(invocation, {
705+
requires: this.requires
706+
});
707+
}
708+
709+
return prolog + invocation;
694710
};
695711

696712
this.str += `
@@ -1065,17 +1081,27 @@ class Interface {
10651081
return false;
10661082
`;
10671083
} else {
1084+
let invocation;
10681085
const func = this.namedDeleter.name ? `.${this.namedDeleter.name}` : "[utils.namedDelete]";
1086+
10691087
if (this.namedDeleter.idlType.idlType === "bool") {
1070-
this.str += `
1088+
invocation = `
10711089
return target[impl]${func}(P);
10721090
`;
10731091
} else {
1074-
this.str += `
1092+
invocation = `
10751093
target[impl]${func}(P);
10761094
return true;
10771095
`;
10781096
}
1097+
1098+
if (utils.hasCEReactions(this.namedDeleter)) {
1099+
invocation = this.ctx.invokeProcessCEReactions(invocation, {
1100+
requires: this.requires
1101+
});
1102+
}
1103+
1104+
this.str += invocation;
10791105
}
10801106
this.str += `
10811107
}
@@ -1192,6 +1218,12 @@ class Interface {
11921218
`;
11931219
}
11941220

1221+
if (utils.getExtAttr(this.idl.extAttrs, "HTMLConstructor")) {
1222+
body = this.ctx.invokeProcessHTMLConstructor(body, {
1223+
requires: this.requires
1224+
});
1225+
}
1226+
11951227
this.addMethod("prototype", "constructor", argNames, body, "regular", { enumerable: false });
11961228
}
11971229

@@ -1419,6 +1451,7 @@ class Interface {
14191451

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

14241457
if (idl.inheritance) {
@@ -1441,9 +1474,9 @@ class Interface {
14411474
if (globalObject[ctorRegistry] === undefined) {
14421475
globalObject[ctorRegistry] = Object.create(null);
14431476
}
1444-
globalObject[ctorRegistry]["${name}"] = ${name};
1477+
globalObject[ctorRegistry][interfaceName] = ${name};
14451478
1446-
Object.defineProperty(globalObject, "${name}", {
1479+
Object.defineProperty(globalObject, interfaceName, {
14471480
configurable: true,
14481481
writable: true,
14491482
value: ${name}

lib/constructs/operation.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class Operation {
3939

4040
generate() {
4141
const requires = new utils.RequiresMap(this.ctx);
42+
4243
this.fixUpArgsExtAttrs();
4344
let str = "";
4445

@@ -78,16 +79,24 @@ class Operation {
7879
requires.merge(parameterConversions.requires);
7980
str += parameterConversions.body;
8081

82+
let invocation;
8183
if (overloads.every(overload => conversions[overload.operation.idlType.idlType])) {
82-
str += `
84+
invocation = `
8385
return ${callOn}.${implFunc}(${argsSpread});
8486
`;
8587
} else {
86-
str += `
88+
invocation = `
8789
return utils.tryWrapperForImpl(${callOn}.${implFunc}(${argsSpread}));
8890
`;
8991
}
9092

93+
if (utils.hasCEReactions(this.idls[0])) {
94+
invocation = this.ctx.invokeProcessCEReactions(invocation, {
95+
requires
96+
});
97+
}
98+
str += invocation;
99+
91100
if (this.static) {
92101
this.interface.addStaticMethod(this.name, argNames, str);
93102
} else {

lib/context.js

+37-1
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,22 @@ const builtinTypedefs = webidl.parse(`
1010
typedef unsigned long long DOMTimeStamp;
1111
`);
1212

13+
function defaultProcessor(_idl, code) {
14+
return code;
15+
}
16+
1317
class Context {
14-
constructor({ implSuffix = "", options } = {}) {
18+
constructor({
19+
implSuffix = "",
20+
processCEReactions = defaultProcessor,
21+
processHTMLConstructor = defaultProcessor,
22+
options
23+
} = {}) {
1524
this.implSuffix = implSuffix;
25+
this.processCEReactions = processCEReactions;
26+
this.processHTMLConstructor = processHTMLConstructor;
1627
this.options = options;
28+
1729
this.initialize();
1830
}
1931

@@ -44,6 +56,30 @@ class Context {
4456
}
4557
return undefined;
4658
}
59+
60+
invokeProcessCEReactions(code, config) {
61+
return this._invokeProcessor(this.processCEReactions, code, config);
62+
}
63+
64+
invokeProcessHTMLConstructor(code, config) {
65+
return this._invokeProcessor(this.processHTMLConstructor, code, config);
66+
}
67+
68+
_invokeProcessor(processor, code, config) {
69+
const { requires } = config;
70+
71+
if (!requires) {
72+
throw new TypeError("Internal error: missing requires object in context");
73+
}
74+
75+
const context = {
76+
addImport(source, imported) {
77+
return requires.add(source, imported);
78+
}
79+
};
80+
81+
return processor.call(context, code);
82+
}
4783
}
4884

4985
module.exports = Context;

lib/transformer.js

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class Transformer {
1717
constructor(opts = {}) {
1818
this.ctx = new Context({
1919
implSuffix: opts.implSuffix,
20+
processCEReactions: opts.processCEReactions,
21+
processHTMLConstructor: opts.processHTMLConstructor,
2022
options: {
2123
suppressErrors: Boolean(opts.suppressErrors)
2224
}

0 commit comments

Comments
 (0)