Skip to content

Commit 58823fc

Browse files
TimothyGuExE-Bossdomenic
authored
Add proper reflection support through processor (#161)
It has long been proposed that reflection features be moved to the host-side, considering it is a part of HTML rather than Web IDL. With the recently added processor infrastructure, this is now possible. With this change, non-relative imports are now allowed in processors' addImport function. This is done by checking if the imported package fits the npm package name regular expression. Thanks to ExE Boss for this contribution. We also note that existing reflectors did not deal very well with the change from this to esValue, since 2a2a099. Since this commit moves the reflection code out of webidl2js, the bug now naturally disappears. Closes #15. Closes #93. Co-authored-by: ExE Boss <[email protected]> Co-authored-by: Domenic Denicola <[email protected]>
1 parent bba042c commit 58823fc

File tree

14 files changed

+332
-166
lines changed

14 files changed

+332
-166
lines changed

README.md

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,15 @@ By default webidl2js ignores HTML Standard-defined extended attributes [`[CEReac
128128

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

131-
- `code` is the code generated by webidl2js normally, for calling into the impl class.
131+
- `code` (string) is the code generated by webidl2js normally, for calling into the impl class.
132132

133-
- `replacementCode` is the new code that will be output in place of `code` in the wrapper class.
133+
- `replacementCode` (string) is the new code that will be output in place of `code` in the wrapper class.
134134

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

137-
Both hooks also have some utility methods that are accessible via `this`:
137+
Both hooks also have a utility method that is accessible via `this`:
138138

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.
139+
- `addImport(path, [importedIdentifier])` utility to require external modules from the generated interface. This method accepts 2 parameters: `path` the relative or absolute path from the generated interface file, and an optional `importedIdentifier` the identifier to import. This method returns the local identifier from the imported path.
140140

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

@@ -182,6 +182,65 @@ transformer.generate("wrappers").catch(err => {
182182
});
183183
```
184184

185+
### `[Reflect]`
186+
187+
Many HTML IDL attributes are defined using [reflecting a content attribute to an IDL attribute](https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#reflect). By default webidl2js doesn't do much with reflection, since it requires detailed knowledge of the host environment to implement correctly. However, we offer the `processReflect` processor hook to allow the host environment to automate the task of implementing reflected IDL attributes.
188+
189+
The `processReflect` processor hook has the signature `(idl, implName) => ({ get, set })`, where:
190+
191+
- `idl` is the [`attribute` AST node](https://github.com/w3c/webidl2.js/#attribute-member) as emitted by the [webidl2](https://github.com/w3c/webidl2.js) parser.
192+
193+
- `implName` (string) is a JavaScript expression that would evaluate to the implementation object at runtime.
194+
195+
- `get` (string) is the code that will be output in the attribute getter, after any function prologue.
196+
197+
- `set` (string) is the code that will be output in the attribute setter, after any function prologue.
198+
199+
The hook also has a utility method that is accessible via `this`:
200+
201+
- `addImport(path, [importedIdentifier])` utility to require external modules from the generated interface. This method accepts 2 parameters: `path` the relative or absolute path from the generated interface file, and an optional `importedIdentifier` the identifier to import. This method returns the local identifier from the imported path.
202+
203+
The following variables are available in the scope of the replacement code:
204+
205+
- `globalObject` (object) is the global object associated with the interface
206+
207+
- `interfaceName` (string) is the name of the interface
208+
209+
- (for setter only) `V` (any) is the converted input to the setter method.
210+
211+
To mark an attribute as reflected, an extended attribute whose name starts with `Reflect` should be added to the IDL attribute. This means that any of the following is treated as reflected by webidl2js:
212+
213+
```webidl
214+
[Reflect] attribute boolean reflectedBoolean;
215+
[ReflectURL] attribute USVString reflectedURL;
216+
[Reflect=value, ReflectURL] attribute USVString reflectedValue;
217+
```
218+
219+
webidl2js itself does not particularly care about the particular reflection-related extended attribute(s) being used, only that one exists. However, your processor hook can make use of the extended attributes for additional information on how the attribute is reflected.
220+
221+
An example processor function that implements `boolean` IDL attribute reflection is as follows:
222+
223+
```js
224+
function processReflect(idl, implName) {
225+
// Assume the name of the reflected content attribute is the same as the IDL attribute, lowercased.
226+
const attrName = idl.name.toLowerCase();
227+
228+
if (idl.idlType.idlType === "boolean") {
229+
return {
230+
get: `return ${implName}.hasAttributeNS(null, "${attrName}");`,
231+
set: `
232+
if (V) {
233+
${implName}.setAttributeNS(null, "${attrName}", "");
234+
} else {
235+
${implName}.removeAttributeNS(null, "${attrName}");
236+
}
237+
`
238+
};
239+
}
240+
throw new Error(`Not-yet-implemented IDL type for reflection: ${idl.idlType.idlType}`);
241+
}
242+
```
243+
185244
## Generated wrapper class file API
186245

187246
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.
@@ -414,17 +473,7 @@ Notable missing features include:
414473

415474
## Nonstandard extended attributes
416475

417-
A couple of non-standard extended attributes are baked in to webidl2js.
418-
419-
### `[Reflect]`
420-
421-
The `[Reflect]` extended attribute is used on IDL attributes to implement the rules for [reflecting a content attribute to an IDL attribute](https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#reflect). If `[Reflect]` is specified, the implementation class does not need to implement any getter or setter logic; webidl2js will take care of it.
422-
423-
By default the attribute passed to `this.getAttribute` and `this.setAttribute` will be the same as the name of the property being reflected. You can use the form `[Reflect=custom]` or `[Reflect=custom_with_dashes]` to change that to be `"custom"` or `"custom-with-dashes"`, respectively.
424-
425-
Note that only the basics of the reflect algorithm are implemented so far: `boolean`, `DOMString`, `long`, and `unsigned long`, with no parametrizations.
426-
427-
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.
476+
One non-standard extended attribute is baked in to webidl2js:
428477

429478
### `[WebIDL2JSValueAsUnsupported=value]`
430479

lib/constructs/attribute.js

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
const conversions = require("webidl-conversions");
44

55
const utils = require("../utils");
6-
const reflector = require("../reflector");
76
const Types = require("../types");
87

98
class Attribute {
@@ -18,7 +17,8 @@ class Attribute {
1817
const requires = new utils.RequiresMap(this.ctx);
1918

2019
const configurable = !utils.getExtAttr(this.idl.extAttrs, "Unforgeable");
21-
const shouldReflect = utils.getExtAttr(this.idl.extAttrs, "Reflect");
20+
const shouldReflect =
21+
this.idl.extAttrs.some(attr => attr.name.startsWith("Reflect")) && this.ctx.processReflect !== null;
2222
const sameObject = utils.getExtAttr(this.idl.extAttrs, "SameObject");
2323

2424
const onInstance = utils.isOnInstance(this.idl, this.interface.idl);
@@ -43,12 +43,9 @@ class Attribute {
4343
getterBody = `return Impl.implementation["${this.idl.name}"];`;
4444
setterBody = `Impl.implementation["${this.idl.name}"] = V;`;
4545
} else if (shouldReflect) {
46-
if (!reflector[this.idl.idlType.idlType]) {
47-
throw new Error("Unknown reflector type: " + this.idl.idlType.idlType);
48-
}
49-
const attrName = shouldReflect.rhs && shouldReflect.rhs.value.replace(/_/g, "-") || this.idl.name;
50-
getterBody = reflector[this.idl.idlType.idlType].get("esValue", attrName.toLowerCase());
51-
setterBody = reflector[this.idl.idlType.idlType].set("esValue", attrName.toLowerCase());
46+
const processedOutput = this.ctx.invokeProcessReflect(this.idl, "esValue[impl]", { requires });
47+
getterBody = processedOutput.get;
48+
setterBody = processedOutput.set;
5249
}
5350

5451
if (utils.getExtAttr(this.idl.extAttrs, "LenientThis")) {
@@ -76,7 +73,7 @@ class Attribute {
7673
let idlConversion;
7774
if (typeof this.idl.idlType.idlType === "string" && !this.idl.idlType.nullable &&
7875
this.ctx.enumerations.has(this.idl.idlType.idlType)) {
79-
requires.add(this.idl.idlType.idlType);
76+
requires.addRelative(this.idl.idlType.idlType);
8077
idlConversion = `
8178
V = \`\${V}\`;
8279
if (!${this.idl.idlType.idlType}.enumerationValues.has(V)) {

lib/constructs/dictionary.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ class Dictionary {
9595
`;
9696

9797
if (this.idl.inheritance) {
98-
this.requires.add(this.idl.inheritance);
98+
this.requires.addRelative(this.idl.inheritance);
9999
}
100100
this.str = `
101101
${this.requires.generate()}

lib/constructs/interface.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ class Interface {
496496
this.requires.addRaw("ctorRegistry", "utils.ctorRegistrySymbol");
497497

498498
if (this.idl.inheritance !== null) {
499-
this.requires.add(this.idl.inheritance);
499+
this.requires.addRelative(this.idl.inheritance);
500500
}
501501

502502
this.str = `

lib/context.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ class Context {
1919
implSuffix = "",
2020
processCEReactions = defaultProcessor,
2121
processHTMLConstructor = defaultProcessor,
22+
processReflect = null,
2223
options
2324
} = {}) {
2425
this.implSuffix = implSuffix;
2526
this.processCEReactions = processCEReactions;
2627
this.processHTMLConstructor = processHTMLConstructor;
28+
this.processReflect = processReflect;
2729
this.options = options;
2830

2931
this.initialize();
@@ -58,14 +60,18 @@ class Context {
5860
}
5961

6062
invokeProcessCEReactions(code, config) {
61-
return this._invokeProcessor(this.processCEReactions, code, config);
63+
return this._invokeProcessor(this.processCEReactions, config, code);
6264
}
6365

6466
invokeProcessHTMLConstructor(code, config) {
65-
return this._invokeProcessor(this.processHTMLConstructor, code, config);
67+
return this._invokeProcessor(this.processHTMLConstructor, config, code);
6668
}
6769

68-
_invokeProcessor(processor, code, config) {
70+
invokeProcessReflect(idl, implName, config) {
71+
return this._invokeProcessor(this.processReflect, config, idl, implName);
72+
}
73+
74+
_invokeProcessor(processor, config, ...args) {
6975
const { requires } = config;
7076

7177
if (!requires) {
@@ -78,7 +84,7 @@ class Context {
7884
}
7985
};
8086

81-
return processor.call(context, code);
87+
return processor.apply(context, args);
8288
}
8389
}
8490

lib/parameters.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ module.exports.generateOverloadConversions = function (ctx, typeOfOp, name, pare
131131
// Avoid requiring the interface itself
132132
if (iface !== parent.name) {
133133
fn = `${iface}.is`;
134-
requires.add(iface);
134+
requires.addRelative(iface);
135135
} else {
136136
fn = "exports.is";
137137
}

lib/reflector.js

Lines changed: 0 additions & 52 deletions
This file was deleted.

lib/transformer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class Transformer {
1919
implSuffix: opts.implSuffix,
2020
processCEReactions: opts.processCEReactions,
2121
processHTMLConstructor: opts.processHTMLConstructor,
22+
processReflect: opts.processReflect,
2223
options: {
2324
suppressErrors: Boolean(opts.suppressErrors)
2425
}

lib/types.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ function generateTypeConversion(ctx, name, idlType, argAttrs = [], parentName, e
122122
// Avoid requiring the interface itself
123123
if (idlType.idlType !== parentName) {
124124
fn = `${idlType.idlType}.convert`;
125-
requires.add(idlType.idlType);
125+
requires.addRelative(idlType.idlType);
126126
} else {
127127
fn = `exports.convert`;
128128
}
@@ -174,7 +174,7 @@ function generateTypeConversion(ctx, name, idlType, argAttrs = [], parentName, e
174174
// Avoid requiring the interface itself
175175
if (iface !== parentName) {
176176
fn = `${iface}.is`;
177-
requires.add(iface);
177+
requires.addRelative(iface);
178178
} else {
179179
fn = "exports.is";
180180
}

lib/utils.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"use strict";
2+
const { extname } = require("path");
23

34
function getDefault(dflt) {
45
switch (dflt.type) {
@@ -65,14 +66,36 @@ function stringifyPropertyName(prop) {
6566
return typeof prop === "symbol" ? symbolName(prop) : JSON.stringify(propertyName(prop));
6667
}
6768

69+
function toKey(type, func = "") {
70+
return String(func + type).replace(/[./-]+/g, " ").trim().replace(/ /g, "_");
71+
}
72+
73+
const PACKAGE_NAME_REGEX = /^(?:@([^/]+?)[/])?([^/]+?)$/u;
74+
6875
class RequiresMap extends Map {
6976
constructor(ctx) {
7077
super();
7178
this.ctx = ctx;
7279
}
7380

74-
add(type, func = "") {
75-
const key = (func + type).replace(/[./-]+/g, " ").trim().replace(/ /g, "_");
81+
add(name, func = "") {
82+
const key = toKey(name, func);
83+
84+
// If `name` is a package name or has a file extension, then use it as-is,
85+
// otherwise append the `.js` file extension:
86+
const importPath = PACKAGE_NAME_REGEX.test(name) || extname(name) ? name : `${name}.js`;
87+
let req = `require(${JSON.stringify(importPath)})`;
88+
89+
if (func) {
90+
req += `.${func}`;
91+
}
92+
93+
this.addRaw(key, req);
94+
return key;
95+
}
96+
97+
addRelative(type, func = "") {
98+
const key = toKey(type, func);
7699

77100
const path = type.startsWith(".") ? type : `./${type}`;
78101
let req = `require("${path}.js")`;

0 commit comments

Comments
 (0)