Skip to content

Commit ced50d4

Browse files
committed
Merge branch 'master' into feat/ext-attr/legacy-factory-function
2 parents e051a5c + b61f4d8 commit ced50d4

13 files changed

+855
-109
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@ webidl2js is implementing an ever-growing subset of the Web IDL specification. S
464464
- Enumeration types
465465
- Union types
466466
- Callback interfaces
467-
- Callback function types, somewhat
467+
- Callback functions
468468
- Nullable types
469469
- `sequence<>` types
470470
- `record<>` types
@@ -485,6 +485,7 @@ webidl2js is implementing an ever-growing subset of the Web IDL specification. S
485485
- `[LegacyNoInterfaceObject]`
486486
- `[LegacyNullToEmptyString]`
487487
- `[LegacyOverrideBuiltins]`
488+
- `[LegacyTreatNonObjectAsNull]`
488489
- `[LegacyUnenumerableNamedProperties]`
489490
- `[LegacyUnforgeable]`
490491
- `[LegacyWindowAlias]`

lib/constructs/callback-function.js

+206
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
"use strict";
2+
3+
const conversions = require("webidl-conversions");
4+
5+
const utils = require("../utils.js");
6+
const Types = require("../types.js");
7+
8+
class CallbackFunction {
9+
constructor(ctx, idl) {
10+
this.ctx = ctx;
11+
this.idl = idl;
12+
this.name = idl.name;
13+
this.str = null;
14+
15+
this.requires = new utils.RequiresMap(ctx);
16+
17+
this.legacyTreatNonObjectAsNull = Boolean(utils.getExtAttr(idl.extAttrs, "LegacyTreatNonObjectAsNull"));
18+
}
19+
20+
generateConversion() {
21+
const { idl, legacyTreatNonObjectAsNull } = this;
22+
const isAsync = idl.idlType.generic === "Promise";
23+
24+
const assertCallable = legacyTreatNonObjectAsNull ? "" : `
25+
if (typeof value !== "function") {
26+
throw new TypeError(context + " is not a function");
27+
}
28+
`;
29+
30+
let returnIDL = "";
31+
if (idl.idlType.idlType !== "void") {
32+
const conv = Types.generateTypeConversion(this.ctx, "callResult", idl.idlType, [], this.name, "context");
33+
this.requires.merge(conv.requires);
34+
returnIDL = `
35+
${conv.body}
36+
return callResult;
37+
`;
38+
}
39+
40+
// This is a simplification of https://heycam.github.io/webidl/#web-idl-arguments-list-converting that currently
41+
// fits our needs.
42+
let argsToES = "";
43+
let inputArgs = "";
44+
let applyArgs = "[]";
45+
46+
if (idl.arguments.length > 0) {
47+
if (idl.arguments.every(arg => !arg.optional && !arg.variadic)) {
48+
const argNames = idl.arguments.map(arg => arg.name);
49+
inputArgs = argNames.join(", ");
50+
applyArgs = `[${inputArgs}]`;
51+
52+
for (const arg of idl.arguments) {
53+
const argName = arg.name;
54+
if (arg.idlType.union ?
55+
arg.idlType.idlType.some(type => !conversions[type.idlType]) :
56+
!conversions[arg.idlType.idlType]) {
57+
argsToES += `
58+
${argName} = utils.tryWrapperForImpl(${argName});
59+
`;
60+
}
61+
}
62+
} else {
63+
const maxArgs = idl.arguments.some(arg => arg.variadic) ? Infinity : idl.arguments.length;
64+
let minArgs = 0;
65+
66+
for (const arg of idl.arguments) {
67+
if (arg.optional || arg.variadic) {
68+
break;
69+
}
70+
71+
minArgs++;
72+
}
73+
74+
if (maxArgs > 0) {
75+
inputArgs = "...args";
76+
applyArgs = "args";
77+
78+
const maxArgsLoop = Number.isFinite(maxArgs) ?
79+
`Math.min(args.length, ${maxArgs})` :
80+
"args.length";
81+
82+
argsToES += `
83+
for (let i = 0; i < ${maxArgsLoop}; i++) {
84+
args[i] = utils.tryWrapperForImpl(args[i]);
85+
}
86+
`;
87+
88+
if (minArgs > 0) {
89+
argsToES += `
90+
if (args.length < ${minArgs}) {
91+
for (let i = args.length; i < ${minArgs}; i++) {
92+
args[i] = undefined;
93+
}
94+
}
95+
`;
96+
}
97+
98+
if (Number.isFinite(maxArgs)) {
99+
argsToES += `
100+
${minArgs > 0 ? "else" : ""} if (args.length > ${maxArgs}) {
101+
args.length = ${maxArgs};
102+
}
103+
`;
104+
}
105+
}
106+
}
107+
}
108+
109+
this.str += `
110+
exports.convert = (value, { context = "The provided value" } = {}) => {
111+
${assertCallable}
112+
function invokeTheCallbackFunction(${inputArgs}) {
113+
if (new.target !== undefined) {
114+
throw new Error("Internal error: invokeTheCallbackFunction is not a constructor");
115+
}
116+
117+
const thisArg = utils.tryWrapperForImpl(this);
118+
let callResult;
119+
`;
120+
121+
if (isAsync) {
122+
this.str += `
123+
try {
124+
`;
125+
}
126+
127+
if (legacyTreatNonObjectAsNull) {
128+
this.str += `
129+
if (typeof value === "function") {
130+
`;
131+
}
132+
133+
this.str += `
134+
${argsToES}
135+
callResult = Reflect.apply(value, thisArg, ${applyArgs});
136+
`;
137+
138+
if (legacyTreatNonObjectAsNull) {
139+
this.str += "}";
140+
}
141+
142+
this.str += `
143+
${returnIDL}
144+
`;
145+
146+
if (isAsync) {
147+
this.str += `
148+
} catch (err) {
149+
return Promise.reject(err);
150+
}
151+
`;
152+
}
153+
154+
this.str += `
155+
};
156+
`;
157+
158+
// `[TreatNonObjctAsNull]` and `isAsync` don't apply to
159+
// https://heycam.github.io/webidl/#construct-a-callback-function.
160+
this.str += `
161+
invokeTheCallbackFunction.construct = (${inputArgs}) => {
162+
${argsToES}
163+
let callResult = Reflect.construct(value, ${applyArgs});
164+
${returnIDL}
165+
};
166+
`;
167+
168+
// The wrapperSymbol ensures that if the callback function is used as a return value, that it exposes
169+
// the original callback back. I.e. it implements the conversion from IDL to JS value in
170+
// https://heycam.github.io/webidl/#es-callback-function.
171+
//
172+
// The objectReference is used to implement spec text such as that discussed in
173+
// https://github.com/whatwg/dom/issues/842.
174+
this.str += `
175+
invokeTheCallbackFunction[utils.wrapperSymbol] = value;
176+
invokeTheCallbackFunction.objectReference = value;
177+
178+
return invokeTheCallbackFunction;
179+
};
180+
`;
181+
}
182+
183+
generateRequires() {
184+
this.str = `
185+
${this.requires.generate()}
186+
187+
${this.str}
188+
`;
189+
}
190+
191+
generate() {
192+
this.generateConversion();
193+
194+
this.generateRequires();
195+
}
196+
197+
toString() {
198+
this.str = "";
199+
this.generate();
200+
return this.str;
201+
}
202+
}
203+
204+
CallbackFunction.prototype.type = "callback";
205+
206+
module.exports = CallbackFunction;

lib/constructs/iterable.js

+6-7
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ class Iterable {
2929

3030
generate() {
3131
const whence = this.interface.defaultWhence;
32+
const requires = new utils.RequiresMap(this.ctx);
33+
3234
if (this.isPair) {
3335
this.generateFunction("keys", "key");
3436
this.generateFunction("values", "value");
@@ -42,10 +44,9 @@ class Iterable {
4244
throw new TypeError("Failed to execute 'forEach' on '${this.name}': 1 argument required, " +
4345
"but only 0 present.");
4446
}
45-
if (typeof callback !== "function") {
46-
throw new TypeError("Failed to execute 'forEach' on '${this.name}': The callback provided " +
47-
"as parameter 1 is not a function.");
48-
}
47+
callback = ${requires.addRelative("Function")}.convert(callback, {
48+
context: "Failed to execute 'forEach' on '${this.name}': The callback provided as parameter 1"
49+
});
4950
const thisArg = arguments[1];
5051
let pairs = Array.from(this[implSymbol]);
5152
let i = 0;
@@ -64,9 +65,7 @@ class Iterable {
6465
// @@iterator is added in Interface class.
6566
}
6667

67-
return {
68-
requires: new utils.RequiresMap(this.ctx)
69-
};
68+
return { requires };
7069
}
7170
}
7271

lib/context.js

+19-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
"use strict";
22
const webidl = require("webidl2");
3+
const CallbackFunction = require("./constructs/callback-function.js");
34
const Typedef = require("./constructs/typedef");
45

5-
const builtinTypedefs = webidl.parse(`
6+
const builtinTypes = webidl.parse(`
67
typedef (Int8Array or Int16Array or Int32Array or
78
Uint8Array or Uint16Array or Uint32Array or Uint8ClampedArray or
89
Float32Array or Float64Array or DataView) ArrayBufferView;
910
typedef (ArrayBufferView or ArrayBuffer) BufferSource;
1011
typedef unsigned long long DOMTimeStamp;
12+
13+
callback Function = any (any... arguments);
14+
callback VoidFunction = void ();
1115
`);
1216

1317
function defaultProcessor(code) {
@@ -20,7 +24,7 @@ class Context {
2024
processCEReactions = defaultProcessor,
2125
processHTMLConstructor = defaultProcessor,
2226
processReflect = null,
23-
options
27+
options = { suppressErrors: false }
2428
} = {}) {
2529
this.implSuffix = implSuffix;
2630
this.processCEReactions = processCEReactions;
@@ -36,11 +40,19 @@ class Context {
3640
this.interfaces = new Map();
3741
this.interfaceMixins = new Map();
3842
this.callbackInterfaces = new Map();
43+
this.callbackFunctions = new Map();
3944
this.dictionaries = new Map();
4045
this.enumerations = new Map();
4146

42-
for (const typedef of builtinTypedefs) {
43-
this.typedefs.set(typedef.name, new Typedef(this, typedef));
47+
for (const idl of builtinTypes) {
48+
switch (idl.type) {
49+
case "typedef":
50+
this.typedefs.set(idl.name, new Typedef(this, idl));
51+
break;
52+
case "callback":
53+
this.callbackFunctions.set(idl.name, new CallbackFunction(this, idl));
54+
break;
55+
}
4456
}
4557
}
4658

@@ -54,6 +66,9 @@ class Context {
5466
if (this.callbackInterfaces.has(name)) {
5567
return "callback interface";
5668
}
69+
if (this.callbackFunctions.has(name)) {
70+
return "callback";
71+
}
5772
if (this.dictionaries.has(name)) {
5873
return "dictionary";
5974
}

lib/transformer.js

+16-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const Typedef = require("./constructs/typedef");
1111
const Interface = require("./constructs/interface");
1212
const InterfaceMixin = require("./constructs/interface-mixin");
1313
const CallbackInterface = require("./constructs/callback-interface.js");
14+
const CallbackFunction = require("./constructs/callback-function");
1415
const Dictionary = require("./constructs/dictionary");
1516
const Enumeration = require("./constructs/enumeration");
1617

@@ -84,7 +85,15 @@ class Transformer {
8485
}));
8586

8687
this.ctx.initialize();
87-
const { interfaces, interfaceMixins, callbackInterfaces, dictionaries, enumerations, typedefs } = this.ctx;
88+
const {
89+
interfaces,
90+
interfaceMixins,
91+
callbackInterfaces,
92+
callbackFunctions,
93+
dictionaries,
94+
enumerations,
95+
typedefs
96+
} = this.ctx;
8897

8998
// first we're gathering all full interfaces and ignore partial ones
9099
for (const file of parsed) {
@@ -113,6 +122,10 @@ class Transformer {
113122
obj = new CallbackInterface(this.ctx, instruction);
114123
callbackInterfaces.set(obj.name, obj);
115124
break;
125+
case "callback":
126+
obj = new CallbackFunction(this.ctx, instruction);
127+
callbackFunctions.set(obj.name, obj);
128+
break;
116129
case "includes":
117130
break; // handled later
118131
case "dictionary":
@@ -198,7 +211,7 @@ class Transformer {
198211
const utilsText = await fs.readFile(path.resolve(__dirname, "output/utils.js"));
199212
await fs.writeFile(this.utilPath, utilsText);
200213

201-
const { interfaces, callbackInterfaces, dictionaries, enumerations } = this.ctx;
214+
const { interfaces, callbackInterfaces, callbackFunctions, dictionaries, enumerations } = this.ctx;
202215

203216
let relativeUtils = path.relative(outputDir, this.utilPath).replace(/\\/g, "/");
204217
if (relativeUtils[0] !== ".") {
@@ -228,7 +241,7 @@ class Transformer {
228241
await fs.writeFile(path.join(outputDir, obj.name + ".js"), source);
229242
}
230243

231-
for (const obj of [...callbackInterfaces.values(), ...dictionaries.values()]) {
244+
for (const obj of [...callbackInterfaces.values(), ...callbackFunctions.values(), ...dictionaries.values()]) {
232245
let source = obj.toString();
233246

234247
source = `

0 commit comments

Comments
 (0)