Skip to content

Commit 1029610

Browse files
committed
Merge branch 'master' into refactor/rename-imported-symbols
2 parents 108f608 + 58823fc commit 1029610

File tree

16 files changed

+476
-255
lines changed

16 files changed

+476
-255
lines changed

README.md

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class SomeInterface {
5656
context: "Failed to execute 'add' on 'SomeInterface': parameter 2"
5757
});
5858

59-
return this[impl].add(...args);
59+
return this[implSymbol].add(...args);
6060
}
6161
}
6262

@@ -68,11 +68,11 @@ Object.defineProperties(SomeInterface.prototype, {
6868
exports.create = (globalObject, constructorArgs = [], privateData = {}) => {
6969
const ctor = globalObject[ctorRegistry].SomeInterface;
7070
const obj = Object.create(ctor.prototype);
71-
obj[impl] = new Impl(constructorArgs, privateData);
71+
obj[implSymbol] = new Impl(constructorArgs, privateData);
7272
return obj;
7373
};
7474

75-
exports.is = obj => obj && obj[impl] instanceof Impl;
75+
exports.is = obj => obj && obj[implSymbol] instanceof Impl;
7676
```
7777

7878
The above is a simplification of the actual generated code, but should give you some idea of what's going on. We bring your attention to a few points:
@@ -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[implSymbol]", { 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
@@ -495,7 +495,7 @@ class Interface {
495495
this.requires.addRaw(`{ implSymbol, ctorRegistrySymbol }`, "utils");
496496

497497
if (this.idl.inheritance !== null) {
498-
this.requires.add(this.idl.inheritance);
498+
this.requires.addRelative(this.idl.inheritance);
499499
}
500500

501501
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: 3 additions & 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
}
@@ -256,6 +257,8 @@ class Transformer {
256257
_prettify(source) {
257258
return prettier.format(source, {
258259
printWidth: 120,
260+
trailingComma: "none",
261+
arrowParens: "avoid",
259262
parser: "babel"
260263
});
261264
}

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)