├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── eslint.config.mjs ├── lib ├── constructs │ ├── async-iterable.js │ ├── attribute.js │ ├── callback-function.js │ ├── callback-interface.js │ ├── constant.js │ ├── dictionary.js │ ├── enumeration.js │ ├── interface-mixin.js │ ├── interface.js │ ├── iterable.js │ ├── operation.js │ └── typedef.js ├── context.js ├── keywords.js ├── output │ └── utils.js ├── overloads.js ├── parameters.js ├── transformer.js ├── types.js └── utils.js ├── package-lock.json ├── package.json └── test ├── __snapshots__ └── test.js.snap ├── cases ├── AsyncCallbackFunction.webidl ├── AsyncCallbackInterface.webidl ├── AsyncIterablePairArgs.webidl ├── AsyncIterablePairNoArgs.webidl ├── AsyncIterableValueArgs.webidl ├── AsyncIterableValueNoArgs.webidl ├── AsyncIterableWithReturn.webidl ├── BufferSourceTypes.webidl ├── CEReactions.webidl ├── CallbackUsage.webidl ├── DOMImplementation.webidl ├── DOMRect.webidl ├── Dictionary.webidl ├── DictionaryConvert.webidl ├── Enum.webidl ├── EventListener.webidl ├── EventTarget.webidl ├── Global.webidl ├── HTMLCollection.webidl ├── HTMLConstructor.webidl ├── HTMLFormControlsCollection.webidl ├── LegacyLenientAttributes.webidl ├── LegacyNoInterfaceObject.webidl ├── LegacyUnforgeable.webidl ├── LegacyUnforgeableMap.webidl ├── MixedIn.webidl ├── NodeFilter.webidl ├── Overloads.webidl ├── PromiseTypes.webidl ├── Reflect.webidl ├── Replaceable.webidl ├── RequestDestination.webidl ├── SeqAndRec.webidl ├── Static.webidl ├── Storage.webidl ├── StringifierAttribute.webidl ├── StringifierDefaultOperation.webidl ├── StringifierNamedOperation.webidl ├── StringifierOperation.webidl ├── TypedefsAndUnions.webidl ├── URL.webidl ├── URLCallback.webidl ├── URLHandlerNonNull.webidl ├── URLList.webidl ├── URLSearchParams.webidl ├── URLSearchParamsCollection.webidl ├── URLSearchParamsCollection2.webidl ├── UnderscoredProperties.webidl ├── Unscopable.webidl ├── Variadic.webidl └── ZeroArgConstructor.webidl ├── implementations └── .gitkeep ├── output └── .gitkeep ├── reflector.js ├── test.js └── utils.test.js /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [18, 20, latest] 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm ci 21 | - run: npm run lint 22 | - run: npm test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # node specific 2 | node_modules/ 3 | npm-debug.log 4 | 5 | # project specific 6 | /test/output/* 7 | coverage/ 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © 2015–2019 Sebastian Mayr 5 | Copyright © 2016–2019 Tiancheng "Timothy" Gu 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript bindings generator for Web IDL 2 | 3 | webidl2js is a code generator that takes [Web IDL](http://heycam.github.io/webidl/) as input, and generates JavaScript files as output which implement the specified Web IDL semantics. These files implement a "wrapper class", which takes care of all Web IDL-specified behaviors such as type conversions and brand checking, before delegating to an "implementation class" that you provide. 4 | 5 | ## Example 6 | 7 | As a very minimal example, the Web IDL file 8 | 9 | ```webidl 10 | interface SomeInterface { 11 | unsigned long long add(unsigned long x, unsigned long y); 12 | }; 13 | ``` 14 | 15 | combined with the JavaScript implementation class file 16 | 17 | ```js 18 | exports.implementation = class SomeInterfaceImpl { 19 | add(x, y) { 20 | return x + y; 21 | } 22 | }; 23 | ``` 24 | 25 | will generate a JavaScript wrapper class file roughly like this: 26 | 27 | ```js 28 | const conversions = require("webidl-conversions"); 29 | const { implSymbol, ctorRegistrySymbol } = require("./utils.js"); 30 | 31 | const Impl = require("./SomeInterface-impl.js").implementation; 32 | 33 | class SomeInterface { 34 | constructor() { 35 | throw new TypeError("Illegal constructor"); 36 | } 37 | 38 | add(x, y) { 39 | if (!exports.is(this)) { 40 | throw new TypeError("Illegal invocation"); 41 | } 42 | if (arguments.length < 2) { 43 | throw new TypeError( 44 | "Failed to execute 'add' on 'SomeInterface': 2 arguments required, but only " + 45 | arguments.length + 46 | " present." 47 | ); 48 | } 49 | 50 | const args = []; 51 | args[0] = conversions["unsigned long"](arguments[0], { 52 | context: "Failed to execute 'add' on 'SomeInterface': parameter 1" 53 | }); 54 | args[1] = conversions["unsigned long"](arguments[1], { 55 | context: "Failed to execute 'add' on 'SomeInterface': parameter 2" 56 | }); 57 | 58 | return this[implSymbol].add(...args); 59 | } 60 | } 61 | 62 | Object.defineProperties(SomeInterface.prototype, { 63 | add: { enumerable: true }, 64 | [Symbol.toStringTag]: { value: "SomeInterface", configurable: true } 65 | }); 66 | 67 | exports.create = (globalObject, constructorArgs = [], privateData = {}) => { 68 | const ctor = globalObject[ctorRegistrySymbol].SomeInterface; 69 | const obj = Object.create(ctor.prototype); 70 | obj[implSymbol] = new Impl(constructorArgs, privateData); 71 | return obj; 72 | }; 73 | 74 | exports.is = obj => obj && obj[implSymbol] instanceof Impl; 75 | ``` 76 | 77 | 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: 78 | 79 | - webidl2js takes pains to fully implement Web IDL semantics. Here you can see some on display, such as: 80 | - Uncallable constructors, when no `constructor` operation is specified. 81 | - Brand-checking on operation invocation 82 | - Argument-length checking for non-optional arguments 83 | - Argument conversion according to the rules specified in Web IDL for given argument types 84 | - Enumerability of operations 85 | - @@toStringTag semantics to make `Object.prototype.toString.call()` behave correctly 86 | - After performing Web IDL-related processing, webidl2js delegates to the implementation class for the non-boilerplate parts of `add()`. This allows you to focus on writing the interesting parts of the implementation without worrying about types, brand-checking, parameter-processing, etc. 87 | - webidl2js attempts to generate informative error messages using what it knows. 88 | 89 | For more examples, you can check out the `test/` directory (with the generated output being in `test/__snapshots__`). Alternately, you can install [jsdom](https://www.npmjs.com/package/jsdom), [whatwg-url](https://www.npmjs.com/package/whatwg-url), or [domexception](https://www.npmjs.com/package/domexception) from npm and check out their source code. (Note that browsing them on GitHub will not work, as we do not check the generated files into Git, but instead generate them as part of publishing the package to npm.) 90 | 91 | ## Wrapper class file generation API 92 | 93 | A typical Node.js script that compiles IDL using webidl2js looks like the following: 94 | 95 | ```js 96 | "use strict"; 97 | const WebIDL2JS = require("webidl2js"); 98 | 99 | const transformer = new WebIDL2JS({ implSuffix: "-impl" }); 100 | 101 | transformer.addSource("idl", "impls"); 102 | transformer.generate("wrappers").catch(err => { 103 | console.error(err.stack); 104 | process.exit(1); 105 | }); 106 | ``` 107 | 108 | The main module's default export is a class which you can construct with a few options: 109 | 110 | - `implSuffix`: a suffix used, if any, to find files within the source directory based on the IDL file name 111 | - `suppressErrors`: set to true to suppress errors during generation 112 | - `processCEReactions` and `processHTMLConstructor`: see below 113 | 114 | The `addSource()` method can then be called multiple times to add directories containing `.webidl` IDL files and `.js` implementation class files. 115 | 116 | Finally, the `generate()` method will generate corresponding wrapper class files in the given directory. 117 | 118 | In this example, a file at `idl/SomeInterface.webidl` would generate a new wrapper class at `wrappers/SomeInterface.js`, which would refer to an implementation class at `impls/SomeInterface-impl.js`. (In practice, usually at least two of these directories are the same, making `implSuffix` a useful option.) 119 | 120 | The transformer will also generate a file named `utils.js` inside the wrapper class directory, which contains utilities used by all the wrapper class files. 121 | 122 | 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. 123 | 124 | ### `[CEReactions]` and `[HTMLConstructor]` 125 | 126 | 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. 127 | 128 | Both hooks have the signature `(code) => replacementCode`, where: 129 | 130 | - `code` (string) is the code generated by webidl2js normally, for calling into the impl class. 131 | 132 | - `replacementCode` (string) is the new code that will be output in place of `code` in the wrapper class. 133 | 134 | If either hook is omitted, then the code will not be replaced, i.e. the default is equivalent to `(code) => code`. 135 | 136 | Both hooks also have a utility method that is accessible via `this`: 137 | 138 | - `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. 139 | 140 | The following variables are available in the scope of the replacement code: 141 | 142 | - `globalObject` (object) is the global object associated with the interface 143 | 144 | - `interfaceName` (string) is the name of the interface 145 | 146 | An example of code that uses these hooks is as follows: 147 | 148 | ```js 149 | "use strict"; 150 | const WebIDL2JS = require("webidl2js"); 151 | 152 | const transformer = new WebIDL2JS({ 153 | implSuffix: "-impl", 154 | processCEReactions(code) { 155 | // Add `require("../ce-reactions")` to generated file. 156 | const ceReactions = this.addImport("../ce-reactions"); 157 | 158 | return ` 159 | ${ceReactions}.preSteps(globalObject); 160 | try { 161 | ${code} 162 | } finally { 163 | ${ceReactions}.postSteps(globalObject); 164 | } 165 | `; 166 | }, 167 | processHTMLConstructor(/* code */) { 168 | // Add `require("../HTMLConstructor").HTMLConstructor` to generated file. 169 | const htmlConstructor = this.addImport("../HTMLConstructor", "HTMLConstructor"); 170 | 171 | return ` 172 | return ${htmlConstructor}(globalObject, interfaceName); 173 | `; 174 | } 175 | }); 176 | 177 | transformer.addSource("idl", "impls"); 178 | transformer.generate("wrappers").catch(err => { 179 | console.error(err.stack); 180 | process.exit(1); 181 | }); 182 | ``` 183 | 184 | ### `[Reflect]` 185 | 186 | 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. 187 | 188 | The `processReflect` processor hook has the signature `(idl, implName) => ({ get, set })`, where: 189 | 190 | - `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. 191 | 192 | - `implName` (string) is a JavaScript expression that would evaluate to the implementation object at runtime. 193 | 194 | - `get` (string) is the code that will be output in the attribute getter, after any function prologue. 195 | 196 | - `set` (string) is the code that will be output in the attribute setter, after any function prologue. 197 | 198 | The hook also has a utility method that is accessible via `this`: 199 | 200 | - `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. 201 | 202 | The following variables are available in the scope of the replacement code: 203 | 204 | - `globalObject` (object) is the global object associated with the interface 205 | 206 | - `interfaceName` (string) is the name of the interface 207 | 208 | - (for setter only) `V` (any) is the converted input to the setter method. 209 | 210 | 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: 211 | 212 | ```webidl 213 | [Reflect] attribute boolean reflectedBoolean; 214 | [ReflectURL] attribute USVString reflectedURL; 215 | [Reflect=value, ReflectURL] attribute USVString reflectedValue; 216 | ``` 217 | 218 | 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. 219 | 220 | An example processor function that implements `boolean` IDL attribute reflection is as follows: 221 | 222 | ```js 223 | function processReflect(idl, implName) { 224 | // Assume the name of the reflected content attribute is the same as the IDL attribute, lowercased. 225 | const attrName = idl.name.toLowerCase(); 226 | 227 | if (idl.idlType.idlType === "boolean") { 228 | return { 229 | get: `return ${implName}.hasAttributeNS(null, "${attrName}");`, 230 | set: ` 231 | if (V) { 232 | ${implName}.setAttributeNS(null, "${attrName}", ""); 233 | } else { 234 | ${implName}.removeAttributeNS(null, "${attrName}"); 235 | } 236 | ` 237 | }; 238 | } 239 | throw new Error(`Not-yet-implemented IDL type for reflection: ${idl.idlType.idlType}`); 240 | } 241 | ``` 242 | 243 | ## Generated wrapper class file API 244 | 245 | 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. 246 | 247 | ### For interfaces 248 | 249 | #### `isImpl(value)` 250 | 251 | Returns a boolean indicating whether _value_ is an instance of the corresponding implementation class. 252 | 253 | This is especially useful inside implementation class files, where incoming wrappers will be _unwrapped_, so that you only ever see implementation class instances ("impls"). 254 | 255 | #### `is(value)` 256 | 257 | Returns a boolean indicating whether _value_ is an instance of the wrapper class. 258 | 259 | This is useful in other parts of your program that are not implementation class files, but instead receive wrapper classes from client code. 260 | 261 | #### `convert(globalObject, value, { context })` 262 | 263 | Performs the Web IDL conversion algorithm for this interface, converting _value_ into the correct representation of the interface type suitable for consumption by implementation classes: the corresponding impl. 264 | 265 | In practice, this means doing a type-check equivalent to `is(value)`, and if it passes, returns the corresponding impl. If the type-check fails, it throws an informative exception. _context_ can be used to describe the provided value in any resulting error message. 266 | 267 | #### `install(globalObject, globalNames)` 268 | 269 | This method creates a brand new wrapper constructor and prototype and attach it to the passed `globalObject`. It also registers the created constructor with the `globalObject`'s global constructor registry, which makes `create()`, `createImpl()`, and `setup()` work. (Thus, it is important to invoke `install()` before invoking those methods, as otherwise they will throw.) 270 | 271 | The second argument `globalNames` is an array containing the [global names](https://heycam.github.io/webidl/#dfn-global-name) of the interface that `globalObject` implements. This is used for the purposes of deciding which interfaces are [exposed](https://heycam.github.io/webidl/#dfn-exposed). For example, this array should be `["Window"]` for a [`Window`](https://html.spec.whatwg.org/multipage/window-object.html#window) global object. But for a [`DedicatedWorkerGlobalScope`](https://html.spec.whatwg.org/multipage/workers.html#dedicatedworkerglobalscope) global object, this array should be `["Worker", "DedicatedWorker"]`. Note that we do not yet implement [`[SecureContext]`](https://heycam.github.io/webidl/#SecureContext), so the "exposed" check is not fully implemented. 272 | 273 | #### `create(globalObject, constructorArgs, privateData)` 274 | 275 | Creates a new instance of the wrapper class and corresponding implementation class, passing in the `globalObject`, the `constructorArgs` array and `privateData` object to the implementation class constructor. Then returns the wrapper class. 276 | 277 | This is useful in other parts of your program that are not implementation class files, but instead want to interface with them from the outside. It's also mostly useful when creating instances of classes that do not have a `constructor`, i.e. are not constructible via their wrapper class constructor. 278 | 279 | #### `createImpl(globalObject, constructorArgs, privateData)` 280 | 281 | Creates a new instance of the wrapper class and corresponding implementation class, passing in the `globalObject`, the `constructorArgs` array and `privateData` object to the implementation class constructor. Then returns the implementation class. 282 | 283 | This is useful inside implementation class files, where it is easiest to only deal with impls, not wrappers. 284 | 285 | #### `new(globalObject, newTarget)` 286 | 287 | Creates a new instance of the wrapper class and corresponding implementation class, but without invoking the implementation class constructor logic. Then returns the implementation class. 288 | 289 | This corresponds to the [WebIDL "create a new object implementing the interface"](https://heycam.github.io/webidl/#new) and ["internally create a new object implementing the interface"](https://heycam.github.io/webidl/#internally-create-a-new-object-implementing-the-interface) algorithms, and is useful when implementing specifications that initialize objects in different ways than their constructors do. 290 | 291 | #### `setup(obj, globalObject, constructorArgs, privateData)` 292 | 293 | This function is mostly used internally, and almost never should be called by your code. The one exception is if you need to inherit from a wrapper class corresponding to an interface without a `constructor`, from a non-webidl2js-generated class. Then, you can call `SuperClass.setup(this, globalObject, [], privateData)` as a substitute for doing `super()` (which would throw). 294 | 295 | jsdom does this for `Window`, which is written in custom, non-webidl2js-generated code, but inherits from `EventTarget`, which is generated by webidl2js. 296 | 297 | ### For callback interfaces 298 | 299 | #### `convert(globalObject, value, { context })` 300 | 301 | Performs the Web IDL conversion algorithm for this callback interface, converting `value` into a function that performs [call a user object's operation](https://heycam.github.io/webidl/#call-a-user-objects-operation) when called, with _thisArg_ being the `this` value of the converted function. `globalObject` is used to ensure error cases result in `Error` or `Promise` objects from the correct realm. 302 | 303 | The resulting function has an `objectReference` property, which is the same object as `value` and can be used to perform identity checks, as `convert` returns a new function object every time. 304 | 305 | If any part of the conversion fails, `context` can be used to describe the provided value in any resulting error message. 306 | 307 | #### `install(globalObject, globalNames)` 308 | 309 | If this callback interface has constants, then this method creates a brand new legacy callback interface object and attaches it to the passed `globalObject`. Otherwise, this method is a no-op. 310 | 311 | The second argument `globalNames` is the same as for [the `install()` export for interfaces](#installglobalobject-globalnames). 312 | 313 | ### For dictionaries 314 | 315 | #### `convert(globalObject, value, { context })` 316 | 317 | Performs the Web IDL conversion algorithm for this dictionary, converting `value` into the correct representation of the dictionary type suitable for consumption by implementation classes: a `null`-[[Prototype]] object with its properties properly converted. `globalObject` is used to ensure error cases result in `Error` or `Promise` objects from the correct realm. 318 | 319 | If any part of the conversion fails, `context` can be used to describe the provided value in any resulting error message. 320 | 321 | ### Other requirements 322 | 323 | The generated wrapper files use modern JavaScript features such as `class` definitions and `Proxy`. They will not work on JavaScript runtimes that do not support the ECMAScript 2015 standard. 324 | 325 | ## Writing implementation class files 326 | 327 | webidl2js tries to ensure that your hand-authored implementation class files can be as straightforward as possible, leaving all the boilerplate in the generated wrapper file. 328 | 329 | The main export of implementation class is `implementation`, whose value is the implementation class. (See [#59](https://github.com/jsdom/webidl2js/issues/59) for potentially making this the default export instead.) The class will contain the following elements: 330 | 331 | ### The constructor 332 | 333 | A constructor for your implementation class, with signature `(globalObject, constructorArgs, privateData)` can serve several purposes: 334 | 335 | - Setting up initial state that will always be used, such as caches or default values 336 | - Keep a reference to the relevant `globalObject` for later consumption. 337 | - Processing constructor arguments `constructorArgs` passed to the wrapper class constructor, if the interface in question has a `constructor` operation. 338 | - Processing any private data `privateData` which is provided when other parts of your program use the generated `create()` or `createImpl()` exports of the wrapper class file. This is useful for constructing instances with specific state that cannot be constructed via the wrapper class constructor. 339 | 340 | If you don't need to do any of these things, the constructor is entirely optional. 341 | 342 | ### Methods implementing IDL operations 343 | 344 | IDL operations that you wish to implement need to have corresponding methods on the implementation class. 345 | 346 | The wrapper class will take care of type conversions for the arguments, so you can operate on the incoming data assuming it is converted to the appropriate Web IDL type. You can also assume that your `this` value is an implementation class; appropriate brand-checking was performed by the wrapper class. 347 | 348 | The wrapper class will even convert any implementation classes you return to the corresponding wrapper classes. This allows you to usually stay entirely within the realm of impls and never deal with wrappers yourself. 349 | 350 | However, note that apart from Web IDL container return values, this impl-back-to-wrapper conversion doesn't work in a "deep" fashion. That is, if you directly return an impl, or return an array or object containing impls from a `sequence<>`, `FrozenArray<>`, or `record<>`-returning operation, the conversion will work. But if you return some other container (such as your own `NodeList` implementation) containing impls, the impl classes will escape to your consumers. To fix this, you'll need to manually use the `wrapperFromImpl` util to ensure consumers only ever see wrappers. 351 | 352 | The same holds true in reverse: if you accept some other container as an argument, then webidl2js will not automatically be able to find all the wrappers it contains an convert them into impls; you will need to use `implFromWrapper` before processing them. 353 | 354 | Variadic operations are fully supported by webidl2js. 355 | 356 | #### Overloaded operations 357 | 358 | In the case of overloaded operations, webidl2js is not always as helpful as it could be. Due to the fact that JavaScript does not have a C++-like method overloading system, all overloads of a method are dispatched to the same implementation method, which means that the implementation class would need to do some secondary overload resolution to determine which overload is actually called. 359 | 360 | #### Static operations 361 | 362 | IDL static operations are declared as properties on the constructor, to mimic how the generated class works. This makes using `class` syntax especially idiomatic for defining an interface implementation, as you can just use `static` methods. 363 | 364 | ### Properties implementing IDL attributes 365 | 366 | IDL attributes that you wish to implement need to have corresponding properties on the implementation class. As with operations, the wrapper class will take care of type conversions for setter arguments, and convert any impls you return into wrappers. 367 | 368 | Note that for IDL attributes that are `readonly`, these properties do not need to be accessor properties. If you create a data property with the correct name, the wrapper class will still expose the property to consumers as a getter wrapping your implementation class's data property. This can sometimes be more convenient. 369 | 370 | #### Static attributes 371 | 372 | Just like static operations, static attributes are defined as properties on the constructor of the implementation class. And just like other attributes, the attribute can either be implemented as an accessor attribute or (if it is readonly) a data attribute. 373 | 374 | ### toString method implementing IDL stringifier 375 | 376 | Web IDL allows stringifiers to be either *aliased* to a specific attribute or named operation, or *standalone*. If the interface defines a standalone stringifier, the implementation class should define a string-returning `toString` method implementing the stringification behavior of that interface. The method is not needed if the stringifier is aliased: webidl2js will just call the attribute getter or named operation for stringification. 377 | 378 | ```webidl 379 | stringifier; // `toString()` is needed on the implementation class. 380 | stringifier attribute DOMString attr; // `toString()` is not needed. 381 | stringifier DOMString operation(); // `toString()` is not needed. 382 | ``` 383 | 384 | ### Indexed and named properties 385 | 386 | IDL indexed and named properties require multiple things on the implementation class to work properly. 387 | 388 | The first is the getters, (optional) setters, and (optional) deleters operations. Much like stringifiers, getters, setters, and deleters can either be standalone or aliased to a named operation (though not an attribute). If an operation is standalone, then the implementation class must implement the following symbol-named methods. The `utils` object below refers to the default export from the generated utilities file `utils.js`. 389 | 390 | - Getters: `utils.indexedGet`, `utils.namedGet` 391 | - Setters: `utils.indexedSetNew`, `utils.indexedSetExisting`, `utils.namedSetNew`, `utils.namedSetExisting` 392 | - Deleters: `utils.namedDelete` 393 | 394 | The second is the interface's supported property indices/names. By default, the implementation class should implement both a Boolean-returning `utils.supportsPropertyIndex`/`utils.supportsPropertyName` method that takes a single Number or String argument, respectively, and an iterable-returning `utils.supportedPropertyIndices`/`utils.supportedPropertyNames` for each variety of getters the interface exposes. 395 | 396 | If the getter function always returns a constant value for unsupported properties, webidl2js also offers a non-standard extended attribute `[WebIDL2JSValueAsUnsupported]` (documented below) that would simply call the getter function to check if a property index/name is supported, so that `supportsPropertyIndex`/`supportsPropertyName` would not need to be implemented separately. However, when using the extended attribute, be very sure that the value specified in the attribute is returned *if and only if* the property is unsupported. 397 | 398 | ### Iterables 399 | 400 | For synchronous value iterable declarations, there is no need to add implementation-class code: they will be automatically generated based on the indexed property getter and `length` property. 401 | 402 | For synchronous pair iterable declarations, the implementation class needs to implement the `[Symbol.iterator]()` property, returning an iterable of `[key, value]` pairs. These can be impls; the generated code will convert them into wrappers as necessary. 403 | 404 | ### Async iterables 405 | 406 | [Asynchronous iterable declarations](https://heycam.github.io/webidl/#idl-async-iterable) require the implementation class to implement the following symbol-named methods, corresponding to algorithms from the Web IDL specification. The `utils` object below refers to the default export from the generated utilities file `utils.js`. 407 | 408 | - `utils.asyncIteratorNext`: corresponds to the [get the next iteration result](https://heycam.github.io/webidl/#dfn-get-the-next-iteration-result) algorithm, and receives a single argument containing an instance of the generated async iterator. For pair asynchronous iterables, the return value must be a `[key, value]` pair array, or `utils.asyncIteratorEOI` to signal the end of the iteration. For value asynchronous iterables, the return value must be the value, or `utils.asyncIteratorEOI` to signal the end of the iteration. 409 | - `utils.asyncIteratorInit`: corresponds to the [asynchronous iterator initialization steps](https://heycam.github.io/webidl/#asynchronous-iterator-initialization-steps), and receives two arguments: the instance of the generated async iterator, and an array containing the post-conversion arguments. This method is optional. 410 | - `utils.asyncIteratorReturn`: corresponds to the [asynchronous iterator return](https://heycam.github.io/webidl/#asynchronous-iterator-return) algorithm, and receives two arguments: the instance of the generated async iterator, and the argument passed to the `return()` method. This method is optional. Note that if you include it, you need to annote the async iterable declaration with [`[WebIDL2JSHasReturnSteps]`](#webidl2jshasreturnsteps). 411 | 412 | ### Other, non-exposed data and functionality 413 | 414 | Your implementation class can contain other properties and methods in support of the wrapped properties and methods that the wrapper class calls into. These can be used to factor out common algorithms, or store private state, or keep caches, or anything of the sort. 415 | 416 | Because of the intermediary wrapper class, there is no need to be concerned about these properties and methods being exposed to consumers of the wrappers. As such, you can name them whatever you want. We often conventionally use a leading underscore prefix, so as to make it clearer that unprefixed class members are exposed and prefixed ones are not. But this is just a convention; no matter what name you use, they will not be visible to wrapper class users. 417 | 418 | ### Inheritance 419 | 420 | It is often useful for implementation classes to inherit from each other, if the corresponding IDL interfaces do. This gives a usually-appropriate implementation of all the inherited operations and attributes. 421 | 422 | However, it is not required! The wrapper classes will have a correct inheritance chain, regardless of the implementation class inheritance chain. Just make sure that, either via inheritance or manual implementation, you implement all of the expected operations and attributes. 423 | 424 | ### The init export 425 | 426 | In addition to the `implementation` export, for interfaces, your implementation class file can contain an `init` export. This would be a function taking as an argument an instance of the implementation class, and is called when any wrapper/implementation pairs are constructed (such as by the exports of the [generated wrapper module](https://github.com/jsdom/webidl2js#for-interfaces)). In particular, it is called even if they are constructed by [`new()`](newglobalobject), which does not invoke the implementation class constructor. 427 | 428 | ## The generated utilities file 429 | 430 | Along with the generated wrapper class files, webidl2js will also generate a utilities file, `utils.js`, in the same directory. (We may make the name less generic; see [#52](https://github.com/jsdom/webidl2js/issues/52) for this and other concerns.) This contains several functions for converting between wrapper and implementation classes. 431 | 432 | Using these functions should be rare, in ideal circumstances. Most of the time, you can operate entirely on wrapper classes from the outside, and entirely on implementation classes while writing implementation class code. But, exceptions exist, such as when needing to reach into the internals of a class you only have the wrapper of, or dealing with unusual containers inside your implementation classes (as explained above). 433 | 434 | ### `wrapperForImpl(impl)` 435 | 436 | Returns the corresponding wrapper class instance for a given implementation class instance, or `null` if the argument is not an implementation class instance. 437 | 438 | ### `tryWrapperForImpl(value)` 439 | 440 | Returns the corresponding wrapper class instance for a given implementation class instance, or returns the argument back if it is not a wrapper class instance. 441 | 442 | This is useful for scenarios when you are not sure whether your incoming value is an impl, wrapper, or some other value, and just want to ensure that you don't end up with an impl. An example is exposing values to consumers who don't know anything about webidl2js. 443 | 444 | ### `implForWrapper(wrapper)` 445 | 446 | Returns the corresponding impl class instance for a given wrapper class instance, or `null` if the argument is not a wrapper class instance. 447 | 448 | This can be useful when you are given a wrapper, but need to modify its inaccessible internals hidden inside the corresponding impl. 449 | 450 | ### `tryImplForWrapper(value)` 451 | 452 | Returns the corresponding impl class instance for a given wrapper class instance, or returns the argument back if it is not an implementation class instance. 453 | 454 | ## Web IDL features 455 | 456 | webidl2js is implementing an ever-growing subset of the Web IDL specification. So far we have implemented: 457 | 458 | - Interface types, including: 459 | - Inheritance 460 | - Attributes 461 | - Operations 462 | - Constants 463 | - Stringifiers 464 | - Named and indexed `getter`/`setter`/`deleter` declarations 465 | - `iterable<>` declarations 466 | - `async iterable<>` declarations 467 | - Class strings (with the semantics of [heycam/webidl#357](https://github.com/heycam/webidl/pull/357)) 468 | - Dictionary types 469 | - Enumeration types 470 | - Union types 471 | - Callback interfaces 472 | - Callback functions 473 | - Nullable types 474 | - `sequence<>` types 475 | - `record<>` types 476 | - `Promise<>` types 477 | - `FrozenArray<>` types 478 | - `typedef`s 479 | - Partial interfaces and dictionaries 480 | - Interface mixins 481 | - Basic types (via [webidl-conversions][]) 482 | - Overload resolution (although [tricky cases are not easy on the implementation class](#overloaded-operations)) 483 | - Variadic arguments 484 | - `[Clamp]` 485 | - `[EnforceRange]` 486 | - `[Exposed]` 487 | - `[LegacyLenientThis]` 488 | - `[LegacyLenientSetter]` 489 | - `[LegacyNoInterfaceObject]` 490 | - `[LegacyNullToEmptyString]` 491 | - `[LegacyOverrideBuiltins]` 492 | - `[LegacyTreatNonObjectAsNull]` 493 | - `[LegacyUnenumerableNamedProperties]` 494 | - `[LegacyUnforgeable]` 495 | - `[LegacyWindowAlias]` 496 | - `[PutForwards]` 497 | - `[Replaceable]` 498 | - `[SameObject]` (automatic caching) 499 | - `[Unscopable]` 500 | 501 | Supported Web IDL extensions defined in HTML: 502 | 503 | - `[CEReactions]` - behavior can be defined via the `processCEReactions` hook 504 | - `[HTMLConstructor]` - behavior can be defined via the `processHTMLConstructor` hook 505 | 506 | Notable missing features include: 507 | 508 | - Namespaces 509 | - `maplike<>` and `setlike<>` 510 | - `[AllowShared]` 511 | - `[Default]` (for `toJSON()` operations) 512 | - `[Global]`'s various consequences, including the named properties object and `[[SetPrototypeOf]]` 513 | - `[LegacyFactoryFunction]` 514 | - `[LegacyNamespace]` 515 | - `[LegacyTreatNonObjectAsNull]` 516 | - `[SecureContext]` 517 | 518 | ## Nonstandard extended attributes 519 | 520 | A couple of non-standard extended attributes are baked in to webidl2js: 521 | 522 | ### `[WebIDL2JSCallWithGlobal]` 523 | 524 | When the `[WebIDL2JSCallWithGlobal]` extended attribute is specified on static IDL operations, the generated interface code passes the [current global object](https://html.spec.whatwg.org/multipage/webappapis.html#current-global-object) as the first parameter to the implementation code. All other parameters follow `globalObject` and are unchanged. This could be used to implement factory functions that create objects in the current realm. 525 | 526 | ### `[WebIDL2JSHasReturnSteps]` 527 | 528 | This extended attribute can be applied to async iterable declarations. It declares that the implementation class will implement the `[idlUtils.asyncIteratorReturn]()` method. 529 | 530 | This is necessary because we need to figure out at code-generation time whether to generate a `return()` method on the async iterator prototype. At that point, only the Web IDL is available, not the implementation class properties. So, we need a signal in the Web IDL itself. 531 | 532 | ### `[WebIDL2JSValueAsUnsupported=value]` 533 | 534 | This extended attribute can be applied to named or indexed getters or setters. It says that whether the interface supports a given property name/index can be automatically derived by looking at the return value of its indexed getter/setter: whenever `value` is returned, the name/index is unsupported. Typically, `value` is either `_undefined` or `_null`. 535 | 536 | In practice, this means that the implementation class only needs to implement a single method (the named/indexed getter method), and doesn't need to implement the `[idlUtils.supportsPropertyName]()` or `[idlUtils.supportsPropertyIndex]()` method separately. 537 | 538 | [webidl-conversions]: https://github.com/jsdom/webidl-conversions 539 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import domenicConfig from "@domenic/eslint-config"; 2 | import globals from "globals"; 3 | 4 | export default [ 5 | { 6 | ignores: [ 7 | "test/output/", 8 | "test/__snapshots__/" 9 | ] 10 | }, 11 | { 12 | files: ["**/*.js"], 13 | languageOptions: { 14 | sourceType: "commonjs", 15 | globals: globals.node 16 | } 17 | }, 18 | { 19 | files: [ 20 | "test/test.js", 21 | "test/*.test.js" 22 | ], 23 | languageOptions: { 24 | globals: globals.jest 25 | } 26 | }, 27 | ...domenicConfig, 28 | { 29 | rules: { 30 | "max-len": ["error", { code: 120, ignoreTemplateLiterals: true }], 31 | "require-unicode-regexp": "off" 32 | } 33 | } 34 | ]; 35 | -------------------------------------------------------------------------------- /lib/constructs/async-iterable.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const utils = require("../utils"); 4 | const { generateAsyncIteratorArgConversions } = require("../parameters"); 5 | 6 | class AsyncIterable { 7 | constructor(ctx, I, idl) { 8 | this.ctx = ctx; 9 | this.interface = I; 10 | this.idl = idl; 11 | this.name = idl.type; 12 | } 13 | 14 | get isValue() { 15 | return this.idl.idlType.length === 1; 16 | } 17 | 18 | get isPair() { 19 | return this.idl.idlType.length === 2; 20 | } 21 | 22 | get isAsync() { 23 | return true; 24 | } 25 | 26 | get hasReturnSteps() { 27 | return Boolean(utils.getExtAttr(this.idl.extAttrs, "WebIDL2JSHasReturnSteps")); 28 | } 29 | 30 | generateFunction(key, kind, requires) { 31 | const conv = generateAsyncIteratorArgConversions( 32 | this.ctx, 33 | this.idl, 34 | this.interface, 35 | `Failed to execute '${key}' on '${this.interface.name}': ` 36 | ); 37 | requires.merge(conv.requires); 38 | 39 | this.interface.addMethod(this.interface.defaultWhence, key, [], ` 40 | if (!exports.is(this)) { 41 | throw new globalObject.TypeError("'${key}' called on an object that is not a valid instance of ${this.interface.name}."); 42 | } 43 | 44 | ${conv.body} 45 | 46 | const asyncIterator = exports.createDefaultAsyncIterator(globalObject, this, "${kind}"); 47 | if (this[implSymbol][utils.asyncIteratorInit]) { 48 | this[implSymbol][utils.asyncIteratorInit](asyncIterator, args); 49 | } 50 | return asyncIterator; 51 | `); 52 | } 53 | 54 | generate() { 55 | const whence = this.interface.defaultWhence; 56 | const requires = new utils.RequiresMap(this.ctx); 57 | 58 | // https://heycam.github.io/webidl/#define-the-asynchronous-iteration-methods 59 | 60 | if (this.isPair) { 61 | this.generateFunction("keys", "key", requires); 62 | this.generateFunction("values", "value", requires); 63 | this.generateFunction("entries", "key+value", requires); 64 | this.interface.addProperty(whence, Symbol.asyncIterator, `${this.interface.name}.prototype.entries`); 65 | } else { 66 | this.generateFunction("values", "value", requires); 67 | this.interface.addProperty(whence, Symbol.asyncIterator, `${this.interface.name}.prototype.values`); 68 | } 69 | 70 | return { requires }; 71 | } 72 | } 73 | 74 | module.exports = AsyncIterable; 75 | -------------------------------------------------------------------------------- /lib/constructs/attribute.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const conversions = require("webidl-conversions"); 4 | 5 | const utils = require("../utils"); 6 | const Types = require("../types"); 7 | 8 | class Attribute { 9 | constructor(ctx, I, idl) { 10 | this.ctx = ctx; 11 | this.interface = I; 12 | this.idl = idl; 13 | this.static = idl.special === "static"; 14 | } 15 | 16 | getWhence() { 17 | const { idl } = this; 18 | const isOnInstance = utils.isOnInstance(idl, this.interface.idl); 19 | 20 | if (utils.getExtAttr(idl.extAttrs, "LegacyUnforgeable")) { 21 | return "unforgeables"; 22 | } 23 | 24 | return isOnInstance ? "instance" : "prototype"; 25 | } 26 | 27 | generate() { 28 | const requires = new utils.RequiresMap(this.ctx); 29 | 30 | const whence = this.getWhence(); 31 | const configurable = whence !== "unforgeables"; 32 | const shouldReflect = 33 | this.idl.extAttrs.some(attr => attr.name.startsWith("Reflect")) && this.ctx.processReflect !== null; 34 | const sameObject = utils.getExtAttr(this.idl.extAttrs, "SameObject"); 35 | 36 | const async = this.idl.idlType.generic === "Promise"; 37 | const promiseHandlingBefore = async ? `try {` : ``; 38 | const promiseHandlingAfter = async ? `} catch (e) { return globalObject.Promise.reject(e); }` : ``; 39 | 40 | let brandCheck = ` 41 | if (!exports.is(esValue)) { 42 | throw new globalObject.TypeError("'$KEYWORD$ ${this.idl.name}' called on an object that is not a valid instance of ${this.interface.name}."); 43 | } 44 | `; 45 | let getterBody = `return utils.tryWrapperForImpl(esValue[implSymbol]["${this.idl.name}"]);`; 46 | let setterBody = `esValue[implSymbol]["${this.idl.name}"] = V;`; 47 | if (conversions[this.idl.idlType.idlType]) { 48 | getterBody = `return esValue[implSymbol]["${this.idl.name}"];`; 49 | } 50 | 51 | const addMethod = this.static ? 52 | this.interface.addStaticMethod.bind(this.interface) : 53 | this.interface.addMethod.bind(this.interface, whence); 54 | 55 | if (this.static) { 56 | brandCheck = ""; 57 | getterBody = `return Impl.implementation["${this.idl.name}"];`; 58 | setterBody = `Impl.implementation["${this.idl.name}"] = V;`; 59 | } else if (shouldReflect) { 60 | const processedOutput = this.ctx.invokeProcessReflect(this.idl, "esValue[implSymbol]", { requires }); 61 | getterBody = processedOutput.get; 62 | setterBody = processedOutput.set; 63 | } 64 | 65 | const replaceable = utils.getExtAttr(this.idl.extAttrs, "Replaceable"); 66 | const legacyLenientSetter = utils.getExtAttr(this.idl.extAttrs, "LegacyLenientSetter"); 67 | const legacyLenientThis = utils.getExtAttr(this.idl.extAttrs, "LegacyLenientThis"); 68 | 69 | if (legacyLenientThis) { 70 | brandCheck = ` 71 | if (!exports.is(esValue)) { 72 | return; 73 | } 74 | `; 75 | } 76 | 77 | if (sameObject) { 78 | getterBody = `return utils.getSameObject(this, "${this.idl.name}", () => { ${getterBody} });`; 79 | } 80 | 81 | if (utils.hasCEReactions(this.idl)) { 82 | const processorConfig = { requires }; 83 | 84 | getterBody = this.ctx.invokeProcessCEReactions(getterBody, processorConfig); 85 | setterBody = this.ctx.invokeProcessCEReactions(setterBody, processorConfig); 86 | } 87 | 88 | addMethod(this.idl.name, [], ` 89 | ${promiseHandlingBefore} 90 | const esValue = this !== null && this !== undefined ? this : globalObject; 91 | ${brandCheck.replace("$KEYWORD$", "get")} 92 | ${getterBody} 93 | ${promiseHandlingAfter} 94 | `, "get", { configurable }); 95 | 96 | brandCheck = brandCheck.replace("$KEYWORD$", "set"); 97 | 98 | if (!this.idl.readonly) { 99 | if (async) { 100 | throw new Error(`Illegal promise-typed attribute "${this.idl.name}" in interface "${this.interface.idl.name}"`); 101 | } 102 | 103 | let idlConversion; 104 | if (typeof this.idl.idlType.idlType === "string" && !this.idl.idlType.nullable && 105 | this.ctx.enumerations.has(this.idl.idlType.idlType)) { 106 | requires.addRelative(this.idl.idlType.idlType); 107 | idlConversion = ` 108 | V = \`\${V}\`; 109 | if (!${this.idl.idlType.idlType}.enumerationValues.has(V)) { 110 | return; 111 | } 112 | `; 113 | } else { 114 | const conv = Types.generateTypeConversion( 115 | this.ctx, 116 | "V", 117 | this.idl.idlType, 118 | this.idl.extAttrs, 119 | this.interface.name, 120 | `"Failed to set the '${this.idl.name}' property on '${this.interface.name}': The provided value"` 121 | ); 122 | requires.merge(conv.requires); 123 | idlConversion = conv.body; 124 | } 125 | 126 | addMethod(this.idl.name, ["V"], ` 127 | const esValue = this !== null && this !== undefined ? this : globalObject; 128 | ${brandCheck} 129 | ${idlConversion} 130 | ${setterBody} 131 | `, "set", { configurable }); 132 | } else { 133 | const putForwards = utils.getExtAttr(this.idl.extAttrs, "PutForwards"); 134 | 135 | setterBody = ""; 136 | if (replaceable) { 137 | if (legacyLenientThis) { 138 | brandCheck = ""; 139 | } 140 | 141 | setterBody = ` 142 | Object.defineProperty(esValue, "${this.idl.name}", { 143 | configurable: true, 144 | enumerable: true, 145 | value: V, 146 | writable: true 147 | }); 148 | `; 149 | } else if (putForwards) { 150 | setterBody = ` 151 | const Q = esValue["${this.idl.name}"]; 152 | if (!utils.isObject(Q)) { 153 | throw new globalObject.TypeError("Property '${this.idl.name}' is not an object"); 154 | } 155 | `; 156 | 157 | // WebIDL calls the `Set` abstract operation with a `Throw` value of `false`: 158 | setterBody += `Reflect.set(Q, "${putForwards.rhs.value}", V);`; 159 | } 160 | 161 | if (setterBody) { 162 | addMethod(this.idl.name, ["V"], ` 163 | const esValue = this !== null && this !== undefined ? this : globalObject; 164 | ${brandCheck} 165 | ${setterBody} 166 | `, "set", { configurable }); 167 | } else if (legacyLenientSetter) { 168 | const body = legacyLenientThis ? 169 | "" : 170 | ` 171 | const esValue = this !== null && this !== undefined ? this : globalObject; 172 | ${brandCheck} 173 | `; 174 | 175 | addMethod(this.idl.name, ["V"], body, "set", { configurable }); 176 | } 177 | } 178 | 179 | if (!this.static && this.idl.special === "stringifier") { 180 | addMethod("toString", [], ` 181 | const esValue = this; 182 | if (!exports.is(esValue)) { 183 | throw new globalObject.TypeError("'toString' called on an object that is not a valid instance of ${this.interface.name}."); 184 | } 185 | 186 | ${getterBody} 187 | `, "regular", { configurable, writable: configurable }); 188 | } 189 | 190 | return { requires }; 191 | } 192 | } 193 | 194 | module.exports = Attribute; 195 | -------------------------------------------------------------------------------- /lib/constructs/callback-function.js: -------------------------------------------------------------------------------- 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 | "" : 26 | ` 27 | if (typeof value !== "function") { 28 | throw new globalObject.TypeError(context + " is not a function"); 29 | } 30 | `; 31 | 32 | let returnIDL = ""; 33 | if (idl.idlType.idlType !== "undefined") { 34 | const conv = Types.generateTypeConversion(this.ctx, "callResult", idl.idlType, [], this.name, "context"); 35 | this.requires.merge(conv.requires); 36 | returnIDL = ` 37 | ${conv.body} 38 | return callResult; 39 | `; 40 | } 41 | 42 | // This is a simplification of https://heycam.github.io/webidl/#web-idl-arguments-list-converting that currently 43 | // fits our needs. 44 | let argsToES = ""; 45 | let inputArgs = ""; 46 | let applyArgs = "[]"; 47 | 48 | if (idl.arguments.length > 0) { 49 | if (idl.arguments.every(arg => !arg.optional && !arg.variadic)) { 50 | const argNames = idl.arguments.map(arg => arg.name); 51 | inputArgs = argNames.join(", "); 52 | applyArgs = `[${inputArgs}]`; 53 | 54 | for (const arg of idl.arguments) { 55 | const argName = arg.name; 56 | if (arg.idlType.union ? 57 | arg.idlType.idlType.some(type => !conversions[type.idlType]) : 58 | !conversions[arg.idlType.idlType]) { 59 | argsToES += ` 60 | ${argName} = utils.tryWrapperForImpl(${argName}); 61 | `; 62 | } 63 | } 64 | } else { 65 | const maxArgs = idl.arguments.some(arg => arg.variadic) ? Infinity : idl.arguments.length; 66 | let minArgs = 0; 67 | 68 | for (const arg of idl.arguments) { 69 | if (arg.optional || arg.variadic) { 70 | break; 71 | } 72 | 73 | minArgs++; 74 | } 75 | 76 | if (maxArgs > 0) { 77 | inputArgs = "...args"; 78 | applyArgs = "args"; 79 | 80 | const maxArgsLoop = Number.isFinite(maxArgs) ? 81 | `Math.min(args.length, ${maxArgs})` : 82 | "args.length"; 83 | 84 | argsToES += ` 85 | for (let i = 0; i < ${maxArgsLoop}; i++) { 86 | args[i] = utils.tryWrapperForImpl(args[i]); 87 | } 88 | `; 89 | 90 | if (minArgs > 0) { 91 | argsToES += ` 92 | if (args.length < ${minArgs}) { 93 | for (let i = args.length; i < ${minArgs}; i++) { 94 | args[i] = undefined; 95 | } 96 | } 97 | `; 98 | } 99 | 100 | if (Number.isFinite(maxArgs)) { 101 | argsToES += ` 102 | ${minArgs > 0 ? "else" : ""} if (args.length > ${maxArgs}) { 103 | args.length = ${maxArgs}; 104 | } 105 | `; 106 | } 107 | } 108 | } 109 | } 110 | 111 | this.str += ` 112 | exports.convert = (globalObject, value, { context = "The provided value" } = {}) => { 113 | ${assertCallable} 114 | function invokeTheCallbackFunction(${inputArgs}) { 115 | const thisArg = utils.tryWrapperForImpl(this); 116 | let callResult; 117 | `; 118 | 119 | if (isAsync) { 120 | this.str += ` 121 | try { 122 | `; 123 | } 124 | 125 | if (legacyTreatNonObjectAsNull) { 126 | this.str += ` 127 | if (typeof value === "function") { 128 | `; 129 | } 130 | 131 | this.str += ` 132 | ${argsToES} 133 | callResult = Reflect.apply(value, thisArg, ${applyArgs}); 134 | `; 135 | 136 | if (legacyTreatNonObjectAsNull) { 137 | this.str += "}"; 138 | } 139 | 140 | this.str += ` 141 | ${returnIDL} 142 | `; 143 | 144 | if (isAsync) { 145 | this.str += ` 146 | } catch (err) { 147 | return globalObject.Promise.reject(err); 148 | } 149 | `; 150 | } 151 | 152 | this.str += ` 153 | }; 154 | `; 155 | 156 | // `[TreatNonObjctAsNull]` and `isAsync` don't apply to 157 | // https://heycam.github.io/webidl/#construct-a-callback-function. 158 | this.str += ` 159 | invokeTheCallbackFunction.construct = (${inputArgs}) => { 160 | ${argsToES} 161 | let callResult = Reflect.construct(value, ${applyArgs}); 162 | ${returnIDL} 163 | }; 164 | `; 165 | 166 | // The wrapperSymbol ensures that if the callback function is used as a return value, that it exposes 167 | // the original callback back. I.e. it implements the conversion from IDL to JS value in 168 | // https://heycam.github.io/webidl/#es-callback-function. 169 | // 170 | // The objectReference is used to implement spec text such as that discussed in 171 | // https://github.com/whatwg/dom/issues/842. 172 | this.str += ` 173 | invokeTheCallbackFunction[utils.wrapperSymbol] = value; 174 | invokeTheCallbackFunction.objectReference = value; 175 | 176 | return invokeTheCallbackFunction; 177 | }; 178 | `; 179 | } 180 | 181 | generateRequires() { 182 | this.str = ` 183 | ${this.requires.generate()} 184 | 185 | ${this.str} 186 | `; 187 | } 188 | 189 | generate() { 190 | this.generateConversion(); 191 | 192 | this.generateRequires(); 193 | } 194 | 195 | toString() { 196 | this.str = ""; 197 | this.generate(); 198 | return this.str; 199 | } 200 | } 201 | 202 | CallbackFunction.prototype.type = "callback"; 203 | 204 | module.exports = CallbackFunction; 205 | -------------------------------------------------------------------------------- /lib/constructs/callback-interface.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const conversions = require("webidl-conversions"); 4 | 5 | const utils = require("../utils.js"); 6 | const Types = require("../types.js"); 7 | const Constant = require("./constant.js"); 8 | 9 | class CallbackInterface { 10 | constructor(ctx, idl) { 11 | this.ctx = ctx; 12 | this.idl = idl; 13 | this.name = idl.name; 14 | this.str = null; 15 | 16 | this.requires = new utils.RequiresMap(ctx); 17 | 18 | this.operation = null; 19 | this.constants = new Map(); 20 | 21 | this._analyzed = false; 22 | this._outputStaticProperties = new Map(); 23 | 24 | const exposed = utils.getExtAttr(this.idl.extAttrs, "Exposed"); 25 | if (this.idl.members.some(member => member.type === "const") && !exposed) { 26 | throw new Error(`Callback interface ${this.name} with defined constants lacks the [Exposed] extended attribute`); 27 | } 28 | 29 | if (exposed) { 30 | if (!exposed.rhs || (exposed.rhs.type !== "identifier" && exposed.rhs.type !== "identifier-list")) { 31 | throw new Error(`[Exposed] must take an identifier or an identifier list in callback interface ${this.name}`); 32 | } 33 | 34 | if (exposed.rhs.type === "identifier") { 35 | this.exposed = new Set([exposed.rhs.value]); 36 | } else { 37 | this.exposed = new Set(exposed.rhs.value.map(token => token.value)); 38 | } 39 | } else { 40 | this.exposed = new Set(); 41 | } 42 | } 43 | 44 | _analyzeMembers() { 45 | for (const member of this.idl.members) { 46 | switch (member.type) { 47 | case "operation": 48 | if (this.operation !== null) { 49 | throw new Error( 50 | `Callback interface ${this.name} has more than one operation` 51 | ); 52 | } 53 | this.operation = member; 54 | break; 55 | case "const": 56 | this.constants.set(member.name, new Constant(this.ctx, this, member)); 57 | break; 58 | default: 59 | throw new Error( 60 | `Illegal IDL member type "${member.type}" in callback interface ${this.name}` 61 | ); 62 | } 63 | } 64 | 65 | if (this.operation === null) { 66 | throw new Error(`Callback interface ${this.name} has no operation`); 67 | } 68 | } 69 | 70 | addAllProperties() { 71 | for (const member of this.constants.values()) { 72 | const data = member.generate(); 73 | this.requires.merge(data.requires); 74 | } 75 | } 76 | 77 | addStaticProperty(name, body, { configurable = true, enumerable = typeof name === "string", writable = true } = {}) { 78 | const descriptor = { configurable, enumerable, writable }; 79 | this._outputStaticProperties.set(name, { body, descriptor }); 80 | } 81 | 82 | // This is necessary due to usage in the `Constant` and other classes 83 | // It's empty because callback interfaces don't generate platform objects 84 | addProperty() {} 85 | 86 | generateConversion() { 87 | const { operation, name } = this; 88 | const opName = operation.name; 89 | const isAsync = operation.idlType.generic === "Promise"; 90 | 91 | const argNames = operation.arguments.map(arg => arg.name); 92 | if (operation.arguments.some(arg => arg.optional || arg.variadic)) { 93 | throw new Error("Internal error: optional/variadic arguments are not implemented for callback interfaces"); 94 | } 95 | 96 | this.str += ` 97 | exports.convert = (globalObject, value, { context = "The provided value" } = {}) => { 98 | if (!utils.isObject(value)) { 99 | throw new globalObject.TypeError(\`\${context} is not an object.\`); 100 | } 101 | 102 | function callTheUserObjectsOperation(${argNames.join(", ")}) { 103 | let thisArg = utils.tryWrapperForImpl(this); 104 | let O = value; 105 | let X = O; 106 | `; 107 | 108 | if (isAsync) { 109 | this.str += ` 110 | try { 111 | `; 112 | } 113 | 114 | this.str += ` 115 | if (typeof O !== "function") { 116 | X = O[${utils.stringifyPropertyName(opName)}]; 117 | if (typeof X !== "function") { 118 | throw new globalObject.TypeError(\`\${context} does not correctly implement ${name}.\`) 119 | } 120 | thisArg = O; 121 | } 122 | `; 123 | 124 | // We don't implement all of https://heycam.github.io/webidl/#web-idl-arguments-list-converting since the callers 125 | // are assumed to always pass the correct number of arguments and we don't support optional/variadic arguments. 126 | // See also: https://github.com/jsdom/webidl2js/issues/71 127 | for (const arg of operation.arguments) { 128 | const argName = arg.name; 129 | if (arg.idlType.union ? 130 | arg.idlType.idlType.some(type => !conversions[type]) : 131 | !conversions[arg.idlType.idlType]) { 132 | this.str += ` 133 | ${argName} = utils.tryWrapperForImpl(${argName}); 134 | `; 135 | } 136 | } 137 | 138 | this.str += ` 139 | let callResult = Reflect.apply(X, thisArg, [${argNames.join(", ")}]); 140 | `; 141 | 142 | if (operation.idlType.idlType !== "undefined") { 143 | const conv = Types.generateTypeConversion(this.ctx, "callResult", operation.idlType, [], name, "context"); 144 | this.requires.merge(conv.requires); 145 | this.str += ` 146 | ${conv.body} 147 | return callResult; 148 | `; 149 | } 150 | 151 | if (isAsync) { 152 | this.str += ` 153 | } catch (err) { 154 | return globalObject.Promise.reject(err); 155 | } 156 | `; 157 | } 158 | 159 | this.str += ` 160 | }; 161 | `; 162 | 163 | // The wrapperSymbol ensures that if the callback interface is used as a return value, e.g. in NodeIterator's filter 164 | // attribute, that it exposes the original callback back. I.e. it implements the conversion from IDL to JS value in 165 | // https://heycam.github.io/webidl/#es-callback-interface. 166 | // 167 | // The objectReference is used to implement spec text such as that discussed in 168 | // https://github.com/whatwg/dom/issues/842. 169 | this.str += ` 170 | callTheUserObjectsOperation[utils.wrapperSymbol] = value; 171 | callTheUserObjectsOperation.objectReference = value; 172 | 173 | return callTheUserObjectsOperation; 174 | }; 175 | `; 176 | } 177 | 178 | generateOffInstanceAfterClass() { 179 | const classProps = new Map(); 180 | 181 | for (const [name, { body, descriptor }] of this._outputStaticProperties) { 182 | const descriptorModifier = utils.getPropertyDescriptorModifier( 183 | utils.defaultDefinePropertyDescriptor, 184 | descriptor, 185 | "regular", 186 | body 187 | ); 188 | classProps.set(utils.stringifyPropertyKey(name), descriptorModifier); 189 | } 190 | 191 | if (classProps.size > 0) { 192 | const props = [...classProps].map(([name, body]) => `${name}: ${body}`); 193 | this.str += ` 194 | Object.defineProperties(${this.name}, { ${props.join(", ")} }); 195 | `; 196 | } 197 | } 198 | 199 | generateInstall() { 200 | if (this.constants.size > 0) { 201 | this.str += ` 202 | const exposed = new Set(${JSON.stringify([...this.exposed])}); 203 | `; 204 | } 205 | 206 | this.str += ` 207 | exports.install = (globalObject, globalNames) => { 208 | `; 209 | 210 | if (this.constants.size > 0) { 211 | const { name } = this; 212 | 213 | this.str += ` 214 | if (!globalNames.some(globalName => exposed.has(globalName))) { 215 | return; 216 | } 217 | 218 | const ctorRegistry = utils.initCtorRegistry(globalObject); 219 | const ${name} = () => { 220 | throw new globalObject.TypeError("Illegal invocation"); 221 | }; 222 | `; 223 | 224 | this.generateOffInstanceAfterClass(); 225 | 226 | this.str += ` 227 | Object.defineProperty(globalObject, ${JSON.stringify(name)}, { 228 | configurable: true, 229 | writable: true, 230 | value: ${name} 231 | }); 232 | `; 233 | } 234 | 235 | this.str += ` 236 | }; 237 | `; 238 | } 239 | 240 | generateRequires() { 241 | this.str = ` 242 | ${this.requires.generate()} 243 | 244 | ${this.str} 245 | `; 246 | } 247 | 248 | generate() { 249 | this.generateConversion(); 250 | this.generateInstall(); 251 | 252 | this.generateRequires(); 253 | } 254 | 255 | toString() { 256 | this.str = ""; 257 | if (!this._analyzed) { 258 | this._analyzed = true; 259 | this._analyzeMembers(); 260 | } 261 | this.addAllProperties(); 262 | this.generate(); 263 | return this.str; 264 | } 265 | } 266 | 267 | CallbackInterface.prototype.type = "callback interface"; 268 | 269 | module.exports = CallbackInterface; 270 | -------------------------------------------------------------------------------- /lib/constructs/constant.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const utils = require("../utils"); 4 | 5 | class Constant { 6 | constructor(ctx, I, idl) { 7 | this.ctx = ctx; 8 | this.interface = I; 9 | this.idl = idl; 10 | 11 | this.str = null; 12 | } 13 | 14 | generate() { 15 | this.interface.addStaticProperty(this.idl.name, utils.getDefault(this.idl.value), { 16 | configurable: false, 17 | writable: false 18 | }); 19 | this.interface.addProperty(this.interface.defaultWhence, this.idl.name, utils.getDefault(this.idl.value), { 20 | configurable: false, 21 | writable: false 22 | }); 23 | return { requires: new utils.RequiresMap(this.ctx) }; 24 | } 25 | } 26 | 27 | module.exports = Constant; 28 | -------------------------------------------------------------------------------- /lib/constructs/dictionary.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Types = require("../types"); 4 | const utils = require("../utils"); 5 | 6 | class Dictionary { 7 | constructor(ctx, idl) { 8 | this.ctx = ctx; 9 | this.idl = idl; 10 | this.name = idl.name; 11 | 12 | this.requires = new utils.RequiresMap(ctx); 13 | } 14 | 15 | _prepareFields() { 16 | const fields = []; 17 | const { members } = this.idl; 18 | members.forEach(member => { 19 | if (member.type !== "field") { 20 | throw new Error("webidl2js doesn't support non-field members in dictionaries"); 21 | } 22 | fields.push(member); 23 | }); 24 | 25 | fields.sort((a, b) => a.name < b.name ? -1 : 1); 26 | return fields; 27 | } 28 | 29 | _generateConversions() { 30 | let str = ""; 31 | 32 | for (const field of this._prepareFields()) { 33 | const typeConversion = field.idlType; 34 | const argAttrs = field.extAttrs; 35 | const conv = Types.generateTypeConversion( 36 | this.ctx, 37 | "value", 38 | typeConversion, 39 | argAttrs, 40 | this.name, 41 | `context + " has member '${field.name}' that"` 42 | ); 43 | this.requires.merge(conv.requires); 44 | 45 | str += ` 46 | { 47 | const key = "${field.name}"; 48 | let value = obj === undefined || obj === null ? undefined : obj[key]; 49 | if (value !== undefined) { 50 | ${conv.body} 51 | ret[key] = value; 52 | } 53 | `; 54 | 55 | if (field.required) { 56 | str += ` 57 | else { 58 | throw new globalObject.TypeError("${field.name} is required in '${this.name}'"); 59 | } 60 | `; 61 | } else if (field.default) { 62 | str += ` 63 | else { 64 | ret[key] = ${utils.getDefault(field.default)}; 65 | } 66 | `; 67 | } 68 | 69 | str += ` 70 | } 71 | `; 72 | } 73 | 74 | return str; 75 | } 76 | 77 | generate() { 78 | this.str += ` 79 | exports._convertInherit = (globalObject, obj, ret, { context = "The provided value" } = {}) => { 80 | `; 81 | 82 | if (this.idl.inheritance) { 83 | this.str += ` 84 | ${this.idl.inheritance}._convertInherit(globalObject, obj, ret, { context }); 85 | `; 86 | } 87 | 88 | this.str += ` 89 | ${this._generateConversions()} 90 | }; 91 | 92 | exports.convert = (globalObject, obj, { context = "The provided value" } = {}) => { 93 | if (obj !== undefined && typeof obj !== "object" && typeof obj !== "function") { 94 | throw new globalObject.TypeError(\`\${context} is not an object.\`); 95 | } 96 | 97 | const ret = Object.create(null); 98 | exports._convertInherit(globalObject, obj, ret, { context }); 99 | return ret; 100 | }; 101 | `; 102 | 103 | if (this.idl.inheritance) { 104 | this.requires.addRelative(this.idl.inheritance); 105 | } 106 | this.str = ` 107 | ${this.requires.generate()} 108 | 109 | ${this.str} 110 | `; 111 | 112 | return this.str; 113 | } 114 | 115 | toString() { 116 | this.str = ""; 117 | this.generate(); 118 | return this.str; 119 | } 120 | } 121 | 122 | Dictionary.prototype.type = "dictionary"; 123 | 124 | module.exports = Dictionary; 125 | -------------------------------------------------------------------------------- /lib/constructs/enumeration.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | class Enumeration { 4 | constructor(ctx, idl) { 5 | this.ctx = ctx; 6 | this.idl = idl; 7 | this.name = idl.name; 8 | this.str = null; 9 | } 10 | 11 | generate() { 12 | const values = new Set(this.idl.values.map(val => val.value)); 13 | if (values.size !== this.idl.values.length) { 14 | throw new Error(`Duplicates found in ${this.name}'s enumeration values`); 15 | } 16 | 17 | this.str += ` 18 | const enumerationValues = new Set(${JSON.stringify([...values])}); 19 | exports.enumerationValues = enumerationValues; 20 | 21 | exports.convert = (globalObject, value, { context = "The provided value" } = {}) => { 22 | const string = \`\${value}\`; 23 | if (!enumerationValues.has(string)) { 24 | throw new globalObject.TypeError(\`\${context} '\${string}' is not a valid enumeration value for ${this.name}\`); 25 | } 26 | return string; 27 | }; 28 | `; 29 | } 30 | 31 | toString() { 32 | this.str = ""; 33 | this.generate(); 34 | return this.str; 35 | } 36 | } 37 | 38 | module.exports = Enumeration; 39 | -------------------------------------------------------------------------------- /lib/constructs/interface-mixin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | class InterfaceMixin { 4 | constructor(ctx, idl) { 5 | this.ctx = ctx; 6 | this.idl = idl; 7 | this.name = idl.name; 8 | } 9 | } 10 | 11 | InterfaceMixin.prototype.type = "interface mixin"; 12 | 13 | module.exports = InterfaceMixin; 14 | -------------------------------------------------------------------------------- /lib/constructs/iterable.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const utils = require("../utils"); 4 | 5 | class Iterable { 6 | constructor(ctx, I, idl) { 7 | this.ctx = ctx; 8 | this.interface = I; 9 | this.idl = idl; 10 | this.name = idl.type; 11 | } 12 | 13 | get isValue() { 14 | return this.idl.idlType.length === 1; 15 | } 16 | 17 | get isPair() { 18 | return this.idl.idlType.length === 2; 19 | } 20 | 21 | get isAsync() { 22 | return false; 23 | } 24 | 25 | generateFunction(key, kind) { 26 | this.interface.addMethod(this.interface.defaultWhence, key, [], ` 27 | if (!exports.is(this)) { 28 | throw new globalObject.TypeError("'${key}' called on an object that is not a valid instance of ${this.interface.name}."); 29 | } 30 | return exports.createDefaultIterator(globalObject, this, "${kind}"); 31 | `); 32 | } 33 | 34 | generate() { 35 | const whence = this.interface.defaultWhence; 36 | const requires = new utils.RequiresMap(this.ctx); 37 | 38 | if (this.isPair) { 39 | this.generateFunction("keys", "key"); 40 | this.generateFunction("values", "value"); 41 | this.generateFunction("entries", "key+value"); 42 | this.interface.addProperty(whence, Symbol.iterator, `${this.interface.name}.prototype.entries`); 43 | this.interface.addMethod(whence, "forEach", ["callback"], ` 44 | if (!exports.is(this)) { 45 | throw new globalObject.TypeError("'forEach' called on an object that is not a valid instance of ${this.interface.name}."); 46 | } 47 | if (arguments.length < 1) { 48 | throw new globalObject.TypeError("Failed to execute 'forEach' on '${this.name}': 1 argument required, but only 0 present."); 49 | } 50 | callback = ${requires.addRelative("Function")}.convert(globalObject, callback, { 51 | context: "Failed to execute 'forEach' on '${this.name}': The callback provided as parameter 1" 52 | }); 53 | const thisArg = arguments[1]; 54 | let pairs = Array.from(this[implSymbol]); 55 | let i = 0; 56 | while (i < pairs.length) { 57 | const [key, value] = pairs[i].map(utils.tryWrapperForImpl); 58 | callback.call(thisArg, value, key, this); 59 | pairs = Array.from(this[implSymbol]); 60 | i++; 61 | } 62 | `); 63 | } else { 64 | this.interface.addProperty(whence, "keys", "globalObject.Array.prototype.keys"); 65 | this.interface.addProperty(whence, "values", "globalObject.Array.prototype.values"); 66 | this.interface.addProperty(whence, "entries", "globalObject.Array.prototype.entries"); 67 | this.interface.addProperty(whence, "forEach", "globalObject.Array.prototype.forEach"); 68 | // @@iterator is added in Interface class. 69 | } 70 | 71 | return { requires }; 72 | } 73 | } 74 | 75 | module.exports = Iterable; 76 | -------------------------------------------------------------------------------- /lib/constructs/operation.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const conversions = require("webidl-conversions"); 4 | 5 | const utils = require("../utils"); 6 | const Overloads = require("../overloads"); 7 | const Parameters = require("../parameters"); 8 | 9 | class Operation { 10 | constructor(ctx, I, idl) { 11 | this.ctx = ctx; 12 | this.interface = I; 13 | this.idls = [idl]; 14 | this.name = idl.name; 15 | this.static = idl.special === "static"; 16 | } 17 | 18 | getWhence() { 19 | const { idls } = this; 20 | const firstOverloadOnInstance = utils.isOnInstance(idls[0], this.interface.idl); 21 | const hasLegacyUnforgeable = Boolean(utils.getExtAttr(idls[0].extAttrs, "LegacyUnforgeable")); 22 | 23 | for (let i = 1; i < idls.length; i++) { 24 | if (Boolean(utils.getExtAttr(idls[i].extAttrs, "LegacyUnforgeable")) !== hasLegacyUnforgeable) { 25 | throw new Error( 26 | `[LegacyUnforgeable] is not applied uniformly to operation "${this.name}" on ${this.interface.name}` 27 | ); 28 | } 29 | } 30 | 31 | if (hasLegacyUnforgeable) { 32 | return "unforgeables"; 33 | } 34 | 35 | return firstOverloadOnInstance ? "instance" : "prototype"; 36 | } 37 | 38 | isAsync() { 39 | // As of the time of this writing, the current spec does not disallow such overloads, but the intention is to do so: 40 | // https://github.com/heycam/webidl/pull/776 41 | const firstAsync = this.idls[0].idlType.generic === "Promise"; 42 | for (const overload of this.idls.slice(1)) { 43 | const isAsync = overload.idlType.generic === "Promise"; 44 | if (isAsync !== firstAsync) { 45 | throw new Error( 46 | `Overloading between Promise and non-Promise return types is not allowed: operation ` + 47 | `"${this.name}" on ${this.interface.name}` 48 | ); 49 | } 50 | } 51 | return firstAsync; 52 | } 53 | 54 | hasCallWithGlobal() { 55 | const { idls } = this; 56 | const hasCallWithGlobal = Boolean(utils.getExtAttr(idls[0].extAttrs, "WebIDL2JSCallWithGlobal")); 57 | 58 | if (hasCallWithGlobal && !this.static) { 59 | throw new Error( 60 | `[WebIDL2JSCallWithGlobal] is only valid for static operations: "${this.name}" on ${this.interface.name}` 61 | ); 62 | } 63 | 64 | for (let i = 1; i < idls.length; i++) { 65 | if (hasCallWithGlobal !== Boolean(utils.getExtAttr(idls[i].extAttrs, "WebIDL2JSCallWithGlobal"))) { 66 | throw new Error( 67 | `[WebIDL2JSCallWithGlobal] is not applied uniformly to operation "${this.name}" on ${this.interface.name}` 68 | ); 69 | } 70 | } 71 | 72 | return hasCallWithGlobal; 73 | } 74 | 75 | fixUpArgsExtAttrs() { 76 | for (const idl of this.idls) { 77 | for (const arg of idl.arguments) { 78 | if (arg.extAttrs.length) { 79 | // Overwrite rather than push to workaround old webidl2 array-sharing bug 80 | // It's safe as the two cannot coexist. 81 | arg.idlType.extAttrs = [...arg.extAttrs]; 82 | } 83 | } 84 | } 85 | } 86 | 87 | generate() { 88 | const requires = new utils.RequiresMap(this.ctx); 89 | 90 | this.fixUpArgsExtAttrs(); 91 | let str = ""; 92 | 93 | if (!this.name) { 94 | throw new Error(`Internal error: this operation does not have a name (in interface ${this.interface.name})`); 95 | } 96 | 97 | const whence = this.getWhence(); 98 | const async = this.isAsync(); 99 | const promiseHandlingBefore = async ? `try {` : ``; 100 | const promiseHandlingAfter = async ? `} catch (e) { return globalObject.Promise.reject(e); }` : ``; 101 | const hasCallWithGlobal = this.hasCallWithGlobal(); 102 | 103 | const type = this.static ? "static operation" : "regular operation"; 104 | const overloads = Overloads.getEffectiveOverloads(type, this.name, 0, this.interface); 105 | let minOp = overloads[0]; 106 | for (let i = 1; i < overloads.length; ++i) { 107 | if (overloads[i].nameList.length < minOp.nameList.length) { 108 | minOp = overloads[i]; 109 | } 110 | } 111 | 112 | const argNames = minOp.nameList; 113 | 114 | if (!this.static) { 115 | str += ` 116 | const esValue = this !== null && this !== undefined ? this : globalObject; 117 | if (!exports.is(esValue)) { 118 | throw new globalObject.TypeError("'${this.name}' called on an object that is not a valid instance of ${this.interface.name}."); 119 | } 120 | `; 121 | } 122 | 123 | const callOn = this.static ? "Impl.implementation" : `esValue[implSymbol]`; 124 | // In case of stringifiers, use the named implementation function rather than hardcoded "toString". 125 | // All overloads will have the same name, so pick the first one. 126 | const implFunc = this.idls[0].name || this.name; 127 | 128 | const parameterConversions = Parameters.generateOverloadConversions( 129 | this.ctx, 130 | type, 131 | this.name, 132 | this.interface, 133 | `Failed to execute '${this.name}' on '${this.interface.name}': ` 134 | ); 135 | const args = []; 136 | requires.merge(parameterConversions.requires); 137 | str += parameterConversions.body; 138 | 139 | if (hasCallWithGlobal) { 140 | args.push("globalObject"); 141 | } 142 | 143 | if (parameterConversions.hasArgs) { 144 | args.push("...args"); 145 | } 146 | 147 | let invocation; 148 | if (overloads.every(overload => conversions[overload.operation.idlType.idlType])) { 149 | invocation = ` 150 | return ${callOn}.${implFunc}(${utils.formatArgs(args)}); 151 | `; 152 | } else { 153 | invocation = ` 154 | return utils.tryWrapperForImpl(${callOn}.${implFunc}(${utils.formatArgs(args)})); 155 | `; 156 | } 157 | 158 | if (utils.hasCEReactions(this.idls[0])) { 159 | invocation = this.ctx.invokeProcessCEReactions(invocation, { 160 | requires 161 | }); 162 | } 163 | str += invocation; 164 | 165 | str = promiseHandlingBefore + str + promiseHandlingAfter; 166 | 167 | if (this.static) { 168 | this.interface.addStaticMethod(this.name, argNames, str); 169 | } else { 170 | const forgeable = whence !== "unforgeables"; 171 | this.interface.addMethod( 172 | whence, 173 | this.name, 174 | argNames, 175 | str, 176 | "regular", 177 | { configurable: forgeable, writable: forgeable } 178 | ); 179 | } 180 | 181 | return { requires }; 182 | } 183 | } 184 | 185 | module.exports = Operation; 186 | -------------------------------------------------------------------------------- /lib/constructs/typedef.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Types = require("../types"); 4 | 5 | class Typedef { 6 | constructor(ctx, idl) { 7 | this.ctx = ctx; 8 | this.idlOrig = idl; 9 | this.idl = null; 10 | this.name = idl.name; 11 | this.resolved = false; 12 | } 13 | 14 | resolve(stack = []) { 15 | if (this.idl !== null) { 16 | return this.idl; 17 | } 18 | if (stack.includes(this.name)) { 19 | throw new Error(`Circular dependency in typedefs: ${stack.join(" -> ")} -> ${this.name}`); 20 | } 21 | stack.push(this.name); 22 | this.idl = Types.resolveType(this.ctx, this.idlOrig.idlType, stack); 23 | stack.pop(); 24 | this.idlOrig = null; 25 | return this.idl; 26 | } 27 | } 28 | 29 | module.exports = Typedef; 30 | -------------------------------------------------------------------------------- /lib/context.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const webidl = require("webidl2"); 3 | const CallbackFunction = require("./constructs/callback-function.js"); 4 | const Typedef = require("./constructs/typedef"); 5 | 6 | const builtinTypes = webidl.parse(` 7 | typedef (Int8Array or Int16Array or Int32Array or 8 | Uint8Array or Uint16Array or Uint32Array or Uint8ClampedArray or 9 | Float32Array or Float64Array or DataView) ArrayBufferView; 10 | typedef (ArrayBufferView or ArrayBuffer) BufferSource; 11 | typedef unsigned long long DOMTimeStamp; 12 | 13 | callback Function = any (any... arguments); 14 | callback VoidFunction = undefined (); 15 | `); 16 | 17 | function defaultProcessor(code) { 18 | return code; 19 | } 20 | 21 | class Context { 22 | constructor({ 23 | implSuffix = "", 24 | processCEReactions = defaultProcessor, 25 | processHTMLConstructor = defaultProcessor, 26 | processReflect = null, 27 | options = { suppressErrors: false } 28 | } = {}) { 29 | this.implSuffix = implSuffix; 30 | this.processCEReactions = processCEReactions; 31 | this.processHTMLConstructor = processHTMLConstructor; 32 | this.processReflect = processReflect; 33 | this.options = options; 34 | 35 | this.initialize(); 36 | } 37 | 38 | initialize() { 39 | this.typedefs = new Map(); 40 | this.interfaces = new Map(); 41 | this.interfaceMixins = new Map(); 42 | this.callbackInterfaces = new Map(); 43 | this.callbackFunctions = new Map(); 44 | this.dictionaries = new Map(); 45 | this.enumerations = new Map(); 46 | 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 | } 56 | } 57 | } 58 | 59 | typeOf(name) { 60 | if (this.typedefs.has(name)) { 61 | return "typedef"; 62 | } 63 | if (this.interfaces.has(name)) { 64 | return "interface"; 65 | } 66 | if (this.callbackInterfaces.has(name)) { 67 | return "callback interface"; 68 | } 69 | if (this.callbackFunctions.has(name)) { 70 | return "callback"; 71 | } 72 | if (this.dictionaries.has(name)) { 73 | return "dictionary"; 74 | } 75 | if (this.enumerations.has(name)) { 76 | return "enumeration"; 77 | } 78 | return undefined; 79 | } 80 | 81 | invokeProcessCEReactions(code, config) { 82 | return this._invokeProcessor(this.processCEReactions, config, code); 83 | } 84 | 85 | invokeProcessHTMLConstructor(code, config) { 86 | return this._invokeProcessor(this.processHTMLConstructor, config, code); 87 | } 88 | 89 | invokeProcessReflect(idl, implName, config) { 90 | return this._invokeProcessor(this.processReflect, config, idl, implName); 91 | } 92 | 93 | _invokeProcessor(processor, config, ...args) { 94 | const { requires } = config; 95 | 96 | if (!requires) { 97 | throw new TypeError("Internal error: missing requires object in context"); 98 | } 99 | 100 | const context = { 101 | addImport(source, imported) { 102 | return requires.add(source, imported); 103 | } 104 | }; 105 | 106 | return processor.apply(context, args); 107 | } 108 | } 109 | 110 | module.exports = Context; 111 | -------------------------------------------------------------------------------- /lib/keywords.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* eslint-disable array-element-newline */ 4 | module.exports = new Set([ 5 | "break", "case", "class", "catch", "const", "continue", "debugger", "default", "delete", 6 | "do", "else", "export", "extends", "finally", "for", "function", "if", "import", "in", "instanceof", 7 | "let", "new", "return", "super", "switch", "this", "throw", "try", "typeof", "var", "void", "while", "with", "yield", 8 | 9 | "enum", "await", 10 | 11 | "package", "protected", "static", "interface", "private", "public", 12 | 13 | "null", "true", "false" 14 | ]); 15 | -------------------------------------------------------------------------------- /lib/output/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Returns "Type(value) is Object" in ES terminology. 4 | function isObject(value) { 5 | return (typeof value === "object" && value !== null) || typeof value === "function"; 6 | } 7 | 8 | const call = Function.call.bind(Function.call); 9 | 10 | // Like `Object.assign`, but using `[[GetOwnProperty]]` and `[[DefineOwnProperty]]` 11 | // instead of `[[Get]]` and `[[Set]]` and only allowing objects 12 | function define(target, source) { 13 | for (const key of Reflect.ownKeys(source)) { 14 | const descriptor = Reflect.getOwnPropertyDescriptor(source, key); 15 | if (descriptor && !Reflect.defineProperty(target, key, descriptor)) { 16 | throw new TypeError(`Cannot redefine property: ${String(key)}`); 17 | } 18 | } 19 | } 20 | 21 | function newObjectInRealm(globalObject, object) { 22 | const ctorRegistry = initCtorRegistry(globalObject); 23 | return Object.defineProperties( 24 | Object.create(ctorRegistry["%Object.prototype%"]), 25 | Object.getOwnPropertyDescriptors(object) 26 | ); 27 | } 28 | 29 | const wrapperSymbol = Symbol("wrapper"); 30 | const implSymbol = Symbol("impl"); 31 | const sameObjectCaches = Symbol("SameObject caches"); 32 | const ctorRegistrySymbol = Symbol.for("[webidl2js] constructor registry"); 33 | 34 | const AsyncIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf(async function* () {}).prototype); 35 | 36 | function initCtorRegistry(globalObject) { 37 | if (Object.hasOwn(globalObject, ctorRegistrySymbol)) { 38 | return globalObject[ctorRegistrySymbol]; 39 | } 40 | 41 | const ctorRegistry = Object.create(null); 42 | 43 | // In addition to registering all the WebIDL2JS-generated types in the constructor registry, 44 | // we also register a few intrinsics that we make use of in generated code, since they are not 45 | // easy to grab from the globalObject variable. 46 | ctorRegistry["%Object.prototype%"] = globalObject.Object.prototype; 47 | ctorRegistry["%IteratorPrototype%"] = Object.getPrototypeOf( 48 | Object.getPrototypeOf(new globalObject.Array()[Symbol.iterator]()) 49 | ); 50 | 51 | try { 52 | ctorRegistry["%AsyncIteratorPrototype%"] = Object.getPrototypeOf( 53 | Object.getPrototypeOf( 54 | globalObject.eval("(async function* () {})").prototype 55 | ) 56 | ); 57 | } catch { 58 | ctorRegistry["%AsyncIteratorPrototype%"] = AsyncIteratorPrototype; 59 | } 60 | 61 | globalObject[ctorRegistrySymbol] = ctorRegistry; 62 | return ctorRegistry; 63 | } 64 | 65 | function getSameObject(wrapper, prop, creator) { 66 | if (!wrapper[sameObjectCaches]) { 67 | wrapper[sameObjectCaches] = Object.create(null); 68 | } 69 | 70 | if (prop in wrapper[sameObjectCaches]) { 71 | return wrapper[sameObjectCaches][prop]; 72 | } 73 | 74 | wrapper[sameObjectCaches][prop] = creator(); 75 | return wrapper[sameObjectCaches][prop]; 76 | } 77 | 78 | function wrapperForImpl(impl) { 79 | return impl ? impl[wrapperSymbol] : null; 80 | } 81 | 82 | function implForWrapper(wrapper) { 83 | return wrapper ? wrapper[implSymbol] : null; 84 | } 85 | 86 | function tryWrapperForImpl(impl) { 87 | const wrapper = wrapperForImpl(impl); 88 | return wrapper ? wrapper : impl; 89 | } 90 | 91 | function tryImplForWrapper(wrapper) { 92 | const impl = implForWrapper(wrapper); 93 | return impl ? impl : wrapper; 94 | } 95 | 96 | const iterInternalSymbol = Symbol("internal"); 97 | 98 | function isArrayIndexPropName(P) { 99 | if (typeof P !== "string") { 100 | return false; 101 | } 102 | const i = P >>> 0; 103 | if (i === 2 ** 32 - 1) { 104 | return false; 105 | } 106 | const s = `${i}`; 107 | if (P !== s) { 108 | return false; 109 | } 110 | return true; 111 | } 112 | 113 | const byteLengthGetter = 114 | Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, "byteLength").get; 115 | function isArrayBuffer(value) { 116 | try { 117 | byteLengthGetter.call(value); 118 | return true; 119 | } catch { 120 | return false; 121 | } 122 | } 123 | 124 | function iteratorResult([key, value], kind) { 125 | let result; 126 | switch (kind) { 127 | case "key": 128 | result = key; 129 | break; 130 | case "value": 131 | result = value; 132 | break; 133 | case "key+value": 134 | result = [key, value]; 135 | break; 136 | } 137 | return { value: result, done: false }; 138 | } 139 | 140 | function ordinarySetWithOwnDescriptor(target, property, value, receiver, ownDesc) { 141 | if (ownDesc === undefined) { 142 | const parent = Reflect.getPrototypeOf(target); 143 | if (parent !== null) { 144 | return Reflect.set(parent, property, value, receiver); 145 | } 146 | ownDesc = { writable: true, enumerable: true, configurable: true, value: undefined }; 147 | } 148 | if (isDataDescriptor(ownDesc)) { 149 | if (!ownDesc.writable) { 150 | return false; 151 | } 152 | if (!isObject(receiver)) { 153 | return false; 154 | } 155 | const existingDesc = Reflect.getOwnPropertyDescriptor(receiver, property); 156 | if (existingDesc !== undefined) { 157 | if (isAccessorDescriptor(existingDesc)) { 158 | return false; 159 | } 160 | if (existingDesc.writable === false) { 161 | return false; 162 | } 163 | const valueDesc = { value }; 164 | return Reflect.defineProperty(receiver, property, valueDesc); 165 | } 166 | 167 | return Reflect.defineProperty( 168 | receiver, 169 | property, 170 | { value, writable: true, enumerable: true, configurable: true } 171 | ); 172 | } 173 | 174 | const setter = ownDesc.set; 175 | if (setter === undefined) { 176 | return false; 177 | } 178 | call(setter, receiver, value); 179 | return true; 180 | } 181 | 182 | function isDataDescriptor(desc) { 183 | return Object.hasOwn(desc, "value") || Object.hasOwn(desc, "writable"); 184 | } 185 | 186 | function isAccessorDescriptor(desc) { 187 | return Object.hasOwn(desc, "get") || Object.hasOwn(desc, "set"); 188 | } 189 | 190 | const supportsPropertyIndex = Symbol("supports property index"); 191 | const supportedPropertyIndices = Symbol("supported property indices"); 192 | const supportsPropertyName = Symbol("supports property name"); 193 | const supportedPropertyNames = Symbol("supported property names"); 194 | const indexedGet = Symbol("indexed property get"); 195 | const indexedSetNew = Symbol("indexed property set new"); 196 | const indexedSetExisting = Symbol("indexed property set existing"); 197 | const namedGet = Symbol("named property get"); 198 | const namedSetNew = Symbol("named property set new"); 199 | const namedSetExisting = Symbol("named property set existing"); 200 | const namedDelete = Symbol("named property delete"); 201 | 202 | const asyncIteratorNext = Symbol("async iterator get the next iteration result"); 203 | const asyncIteratorReturn = Symbol("async iterator return steps"); 204 | const asyncIteratorInit = Symbol("async iterator initialization steps"); 205 | const asyncIteratorEOI = Symbol("async iterator end of iteration"); 206 | 207 | module.exports = exports = { 208 | isObject, 209 | define, 210 | newObjectInRealm, 211 | wrapperSymbol, 212 | implSymbol, 213 | getSameObject, 214 | ctorRegistrySymbol, 215 | initCtorRegistry, 216 | wrapperForImpl, 217 | implForWrapper, 218 | tryWrapperForImpl, 219 | tryImplForWrapper, 220 | iterInternalSymbol, 221 | isArrayBuffer, 222 | isArrayIndexPropName, 223 | supportsPropertyIndex, 224 | supportedPropertyIndices, 225 | supportsPropertyName, 226 | supportedPropertyNames, 227 | indexedGet, 228 | indexedSetNew, 229 | indexedSetExisting, 230 | namedGet, 231 | namedSetNew, 232 | namedSetExisting, 233 | namedDelete, 234 | asyncIteratorNext, 235 | asyncIteratorReturn, 236 | asyncIteratorInit, 237 | asyncIteratorEOI, 238 | iteratorResult, 239 | ordinarySetWithOwnDescriptor 240 | }; 241 | -------------------------------------------------------------------------------- /lib/overloads.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { areDistinguishable, sameType } = require("./types"); 4 | 5 | function getOperations(type, A, I) { 6 | switch (type) { 7 | case "regular operation": 8 | return I.operations.get(A).idls; 9 | case "static operation": 10 | return I.staticOperations.get(A).idls; 11 | case "constructor": { 12 | return I.constructorOperations; 13 | } 14 | } 15 | throw new RangeError(`${type}s are not yet supported`); 16 | } 17 | module.exports.getOperations = getOperations; 18 | 19 | module.exports.getEffectiveOverloads = function (type, A, N, I) { 20 | const S = []; 21 | const F = getOperations(type, A, I); 22 | let maxArgs = 0; 23 | for (const X of F) { 24 | if (X.arguments.length > maxArgs) { 25 | maxArgs = X.arguments.length; 26 | } 27 | } 28 | 29 | const max = Math.max(maxArgs, N); 30 | for (const X of F) { 31 | const n = X.arguments.length; 32 | const nameList = X.arguments.map(arg => arg.name); 33 | const typeList = X.arguments.map(arg => arg.idlType); 34 | const optionalityList = X.arguments.map(arg => { 35 | if (arg.optional) { 36 | return "optional"; 37 | } 38 | if (arg.variadic) { 39 | return "variadic"; 40 | } 41 | return "required"; 42 | }); 43 | 44 | S.push({ 45 | operation: X, 46 | nameList, 47 | typeList, 48 | optionalityList 49 | }); 50 | 51 | if (optionalityList[optionalityList.length - 1] === "variadic") { 52 | for (let i = n; i <= max - 1; i++) { 53 | const variadicNames = nameList.slice(0, n); 54 | const variadicTypes = typeList.slice(0, n); 55 | const variadicOptionalityValues = optionalityList.slice(0, n); 56 | for (let j = n; j <= i; j++) { 57 | variadicNames.push(nameList[n - 1]); 58 | variadicTypes.push(typeList[n - 1]); 59 | variadicOptionalityValues.push("variadic"); 60 | } 61 | S.push({ 62 | operation: X, 63 | nameList: variadicNames, 64 | typeList: variadicTypes, 65 | optionalityList: variadicOptionalityValues 66 | }); 67 | } 68 | } 69 | 70 | for (let i = n - 1; i >= 0; --i) { 71 | if (optionalityList[i] === "required") { 72 | break; 73 | } 74 | S.push({ 75 | operation: X, 76 | nameList: nameList.slice(0, i), 77 | typeList: typeList.slice(0, i), 78 | optionalityList: optionalityList.slice(0, i) 79 | }); 80 | } 81 | } 82 | 83 | return S; 84 | }; 85 | 86 | module.exports.distinguishingArgumentIndex = function (ctx, S) { 87 | for (let i = 0; i < S[0].typeList.length; i++) { 88 | let distinguishable = true; 89 | for (let j = 0; j < S.length - 1; j++) { 90 | for (let k = j + 1; k < S.length; k++) { 91 | if (!areDistinguishable(ctx, S[j].typeList[i], S[k].typeList[i])) { 92 | distinguishable = false; 93 | } 94 | } 95 | } 96 | if (distinguishable) { 97 | return i; 98 | } 99 | 100 | for (let j = 0; j < S.length - 1; j++) { 101 | for (let k = j + 1; k < S.length; k++) { 102 | if (!sameType(ctx, S[j].typeList[i], S[k].typeList[i])) { 103 | throw new Error(`Different but indistinguishable types at index ${i}`); 104 | } 105 | } 106 | } 107 | } 108 | 109 | return -1; 110 | }; 111 | -------------------------------------------------------------------------------- /lib/parameters.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Overloads = require("./overloads"); 4 | const Types = require("./types"); 5 | 6 | const utils = require("./utils"); 7 | 8 | function isOrIncludes(ctx, parent, predicate) { 9 | parent = Types.resolveType(ctx, parent); 10 | return predicate(parent) || (parent.union && parent.idlType.some(predicate)); 11 | } 12 | 13 | function generateVarConversion(ctx, overload, i, parent, errPrefix, targetIdx = i) { 14 | const requires = new utils.RequiresMap(ctx); 15 | const idlType = overload.typeList[i]; 16 | // Always (try to) force-convert dictionaries 17 | const isDefaultedDictionary = overload.operation.arguments[i].default && 18 | overload.operation.arguments[i].default.type === "dictionary"; 19 | if (isDefaultedDictionary && !ctx.dictionaries.has(idlType.idlType)) { 20 | throw new Error(`The parameter ${overload.operation.arguments[i].name} was defaulted to {}, but no dictionary ` + 21 | `named ${idlType.idlType} exists`); 22 | } 23 | const optional = overload.optionalityList[i] === "optional" && !isDefaultedDictionary; 24 | let str = `{ let curArg = arguments[${targetIdx}];`; 25 | if (optional) { 26 | str += ` 27 | if (curArg !== undefined) { 28 | `; 29 | } 30 | const msg = typeof targetIdx === "string" ? 31 | `"${errPrefix}parameter " + (${targetIdx} + 1)` : 32 | `"${errPrefix}parameter ${i + 1}"`; 33 | const conv = Types.generateTypeConversion(ctx, "curArg", idlType, [], parent.name, msg); 34 | requires.merge(conv.requires); 35 | str += conv.body; 36 | if (optional) { 37 | str += "}"; 38 | const defaultValue = overload.operation.arguments[i].default; 39 | if (defaultValue) { 40 | str += ` 41 | else { 42 | curArg = ${utils.getDefault(defaultValue)}; 43 | } 44 | `; 45 | } 46 | } 47 | str += "args.push(curArg);"; 48 | str += "}"; 49 | return { 50 | requires, 51 | body: str 52 | }; 53 | } 54 | 55 | module.exports.generateAsyncIteratorArgConversions = (ctx, idl, parent, errPrefix) => { 56 | const requires = new utils.RequiresMap(ctx); 57 | let str = "const args = [];"; 58 | 59 | for (let i = 0; i < idl.arguments.length; ++i) { 60 | const idlArg = idl.arguments[i]; 61 | if (!idlArg.optional) { 62 | throw new Error("All async iterable arguments must be optional"); 63 | } 64 | 65 | const isDefaultedDictionary = idlArg.default && idlArg.default.type === "dictionary"; 66 | if (isDefaultedDictionary && !ctx.dictionaries.has(idlArg.idlType.idlType)) { 67 | throw new Error(`The dictionary ${idlArg.idlType.idlType} was referenced in an argument list, but doesn't exist`); 68 | } 69 | 70 | const msg = `"${errPrefix}parameter ${i + 1}"`; 71 | const conv = Types.generateTypeConversion(ctx, `args[${i}]`, idlArg.idlType, [], parent.name, msg); 72 | requires.merge(conv.requires); 73 | 74 | if (isDefaultedDictionary) { 75 | str += `args[${i}] = arguments[${i}];${conv.body}`; 76 | } else { 77 | str += ` 78 | if (arguments[${i}] !== undefined) { 79 | args[${i}] = arguments[${i}];${conv.body} 80 | } 81 | `; 82 | if (idlArg.default) { 83 | str += ` 84 | else { 85 | args[${i}] = ${utils.getDefault(idlArg.default)}; 86 | } 87 | `; 88 | } else { 89 | str += ` 90 | else { 91 | args[${i}] = undefined; 92 | } 93 | `; 94 | } 95 | } 96 | } 97 | 98 | return { 99 | requires, 100 | body: str 101 | }; 102 | }; 103 | 104 | module.exports.generateOverloadConversions = function (ctx, typeOfOp, name, parent, errPrefix) { 105 | const requires = new utils.RequiresMap(ctx); 106 | const ops = Overloads.getOperations(typeOfOp, name, parent); 107 | const argLengths = Overloads.getEffectiveOverloads(typeOfOp, name, 0, parent).map(o => o.typeList.length); 108 | const maxArgs = Math.max(...argLengths); 109 | let str = ""; 110 | if (maxArgs > 0) { 111 | const minArgs = Math.min(...argLengths); 112 | if (minArgs > 0) { 113 | const plural = minArgs > 1 ? "s" : ""; 114 | str += ` 115 | if (arguments.length < ${minArgs}) { 116 | throw new globalObject.TypeError(\`${errPrefix}${minArgs} argument${plural} required, but only \${arguments.length} present.\`); 117 | } 118 | `; 119 | } 120 | str += "const args = [];"; 121 | const switchCases = []; 122 | // Special case: when the operation isn't overloaded, always try to convert to the maximum number of args. 123 | for (let numArgs = ops.length === 1 ? maxArgs : minArgs; numArgs <= maxArgs; numArgs++) { 124 | // for (let numArgs = minArgs; numArgs <= maxArgs; numArgs++) { 125 | const S = Overloads.getEffectiveOverloads(typeOfOp, name, numArgs, parent) 126 | .filter(o => o.typeList.length === numArgs); 127 | if (S.length === 0) { 128 | switchCases.push(` 129 | throw new globalObject.TypeError(\`${errPrefix}only \${arguments.length} arguments present.\`); 130 | `); 131 | continue; 132 | } 133 | 134 | let d = -1; 135 | if (S.length > 1) { 136 | d = Overloads.distinguishingArgumentIndex(ctx, S); 137 | } 138 | let caseSrc = ""; 139 | let i = 0; 140 | for (; i < d; i++) { 141 | const conv = generateVarConversion(ctx, S[0], i, parent, errPrefix); 142 | requires.merge(conv.requires); 143 | caseSrc += conv.body; 144 | } 145 | if (i === d) { 146 | caseSrc += "{"; 147 | caseSrc += `let curArg = arguments[${d}];`; 148 | const possibilities = []; 149 | 150 | const optionals = S.filter(o => o.optionalityList[d] === "optional"); 151 | if (optionals.length) { 152 | possibilities.push(` 153 | if (curArg === undefined) { 154 | ${continued(optionals[0], i)} 155 | } 156 | `); 157 | } 158 | 159 | const nullables = S.filter(o => { 160 | return isOrIncludes(ctx, o.typeList[d], t => t.nullable || ctx.dictionaries.has(t.idlType)); 161 | }); 162 | if (nullables.length) { 163 | possibilities.push(` 164 | if (curArg === null || curArg === undefined) { 165 | ${continued(nullables[0], i)} 166 | } 167 | `); 168 | } 169 | 170 | const interfaceTypes = new Map(); 171 | for (const o of S) { 172 | const type = Types.resolveType(ctx, o.typeList[d]); 173 | if (ctx.interfaces.has(type.idlType)) { 174 | interfaceTypes.set(type.idlType, o); 175 | } else if (type.union) { 176 | for (const child of type.idlType) { 177 | if (ctx.interfaces.has(child.idlType)) { 178 | interfaceTypes.set(child.idlType, o); 179 | } 180 | } 181 | } 182 | } 183 | for (const [iface, overload] of interfaceTypes) { 184 | let fn; 185 | // Avoid requiring the interface itself 186 | if (iface !== parent.name) { 187 | fn = `${iface}.is`; 188 | requires.addRelative(iface); 189 | } else { 190 | fn = "exports.is"; 191 | } 192 | possibilities.push(` 193 | if (${fn}(curArg)) { 194 | ${continued(overload, i)} 195 | } 196 | `); 197 | } 198 | 199 | const arrayBuffers = S.filter(o => isOrIncludes(ctx, o.typeList[d], t => t.idlType === "ArrayBuffer")); 200 | if (arrayBuffers.length) { 201 | possibilities.push(` 202 | if (utils.isArrayBuffer(curArg)) { 203 | ${continued(arrayBuffers[0], i)} 204 | } 205 | `); 206 | } 207 | 208 | const arrayBufferViews = new Map(); 209 | for (const o of S) { 210 | const type = Types.resolveType(ctx, o.typeList[d]); 211 | if (Types.arrayBufferViewTypes.has(type.idlType)) { 212 | arrayBufferViews.set(type.idlType, o); 213 | } else if (type.union) { 214 | for (const child of type.idlType) { 215 | if (Types.arrayBufferViewTypes.has(child.idlType)) { 216 | arrayBufferViews.set(child.idlType, o); 217 | } 218 | } 219 | } 220 | } 221 | if (arrayBufferViews.size) { 222 | // Special case for all ArrayBufferView types. 223 | if (arrayBufferViews.size === Types.arrayBufferViewTypes.size && 224 | new Set(arrayBufferViews.values()).size === 1) { 225 | possibilities.push(` 226 | if (ArrayBuffer.isView(curArg)) { 227 | ${continued(arrayBufferViews.get("Uint8Array"), i)} 228 | } 229 | `); 230 | } else { 231 | for (const [type, overload] of arrayBufferViews) { 232 | possibilities.push(` 233 | if (ArrayBuffer.isView(curArg) && curArg instanceof ${type}) { 234 | ${continued(overload, i)} 235 | } 236 | `); 237 | } 238 | } 239 | } 240 | 241 | const callables = S.filter(o => { 242 | return isOrIncludes(ctx, o.typeList[d], t => ["Function", "VoidFunction"].includes(t.idlType)); 243 | }); 244 | if (callables.length) { 245 | possibilities.push(` 246 | if (typeof curArg === "function") { 247 | ${continued(callables[0], i)} 248 | } 249 | `); 250 | } 251 | 252 | const iterables = S.filter(o => { 253 | return isOrIncludes(ctx, o.typeList[d], t => ["sequence", "FrozenArray"].includes(t.generic)); 254 | }); 255 | if (iterables.length) { 256 | possibilities.push(` 257 | if (utils.isObject(curArg) && typeof curArg[Symbol.iterator] === "function") { 258 | ${continued(iterables[0], i)} 259 | } 260 | `); 261 | } 262 | 263 | const objects = S.filter(o => isOrIncludes(ctx, o.typeList[d], t => t.idlType === "object")); 264 | if (objects.length) { 265 | possibilities.push(` 266 | if (utils.isObject(curArg)) { 267 | ${continued(objects[0], i)} 268 | } 269 | `); 270 | } 271 | 272 | const booleans = S.filter(o => isOrIncludes(ctx, o.typeList[d], t => t.idlType === "boolean")); 273 | if (booleans.length) { 274 | possibilities.push(` 275 | if (typeof curArg === "boolean") { 276 | ${continued(booleans[0], i)} 277 | } 278 | `); 279 | } 280 | 281 | const numerics = S.filter(o => isOrIncludes(ctx, o.typeList[d], t => Types.numericTypes.has(t.idlType))); 282 | if (numerics.length) { 283 | possibilities.push(` 284 | if (typeof curArg === "number") { 285 | ${continued(numerics[0], i)} 286 | } 287 | `); 288 | } 289 | 290 | const strings = S.filter(o => { 291 | return isOrIncludes(ctx, o.typeList[d], t => { 292 | return Types.stringTypes.has(t.idlType) || ctx.enumerations.has(t.idlType); 293 | }); 294 | }); 295 | const any = S.filter(o => isOrIncludes(ctx, o.typeList[d], t => t.idlType === "any")); 296 | if (strings.length) { 297 | possibilities.push(`{ ${continued(strings[0], i)} }`); 298 | } else if (numerics.length) { 299 | possibilities.push(`{ ${continued(numerics[0], i)} }`); 300 | } else if (booleans.length) { 301 | possibilities.push(`{ ${continued(booleans[0], i)} }`); 302 | } else if (any.length) { 303 | possibilities.push(`{ ${continued(any[0], i)} }`); 304 | } else { 305 | possibilities.push(`throw new globalObject.TypeError("${errPrefix}No such overload");`); 306 | } 307 | 308 | caseSrc += possibilities.join(" else "); 309 | 310 | caseSrc += "}"; 311 | } else { 312 | // Branch taken when S.length === 1. 313 | caseSrc += continued(S[0], i); 314 | } 315 | switchCases.push(caseSrc); 316 | 317 | function continued(overload, idx) { 318 | let continuedStr = ""; 319 | for (; idx < numArgs; idx++) { 320 | let targetIdx = idx; 321 | if (overload.optionalityList[idx] === "variadic" && numArgs === maxArgs && idx === numArgs - 1) { 322 | continuedStr += `for (let i = ${idx}; i < arguments.length; i++)`; 323 | targetIdx = "i"; 324 | } 325 | const conv = generateVarConversion(ctx, overload, idx, parent, errPrefix, targetIdx); 326 | requires.merge(conv.requires); 327 | continuedStr += conv.body; 328 | } 329 | return continuedStr; 330 | } 331 | } 332 | if (switchCases.length === 1) { 333 | str += switchCases[0]; 334 | } else { 335 | str += "switch (arguments.length) {"; 336 | let lastBody; 337 | for (let i = 0; i < switchCases.length - 1; i++) { 338 | if (lastBody !== undefined && switchCases[i] !== lastBody) { 339 | str += `${lastBody}break;`; 340 | } 341 | str += `case ${minArgs + i}:`; 342 | lastBody = switchCases[i]; 343 | } 344 | if (lastBody !== undefined && switchCases[switchCases.length - 1] !== lastBody) { 345 | str += `${lastBody}break;`; 346 | } 347 | str += "default:"; 348 | str += switchCases[switchCases.length - 1]; 349 | str += "}"; 350 | } 351 | } 352 | 353 | return { 354 | requires, 355 | body: str, 356 | hasArgs: maxArgs > 0 357 | }; 358 | }; 359 | -------------------------------------------------------------------------------- /lib/transformer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | 5 | const fs = require("fs").promises; 6 | const webidl = require("webidl2"); 7 | const prettier = require("prettier"); 8 | 9 | const Context = require("./context"); 10 | const Typedef = require("./constructs/typedef"); 11 | const Interface = require("./constructs/interface"); 12 | const InterfaceMixin = require("./constructs/interface-mixin"); 13 | const CallbackInterface = require("./constructs/callback-interface.js"); 14 | const CallbackFunction = require("./constructs/callback-function"); 15 | const Dictionary = require("./constructs/dictionary"); 16 | const Enumeration = require("./constructs/enumeration"); 17 | 18 | class Transformer { 19 | constructor(opts = {}) { 20 | this.ctx = new Context({ 21 | implSuffix: opts.implSuffix, 22 | processCEReactions: opts.processCEReactions, 23 | processHTMLConstructor: opts.processHTMLConstructor, 24 | processReflect: opts.processReflect, 25 | options: { 26 | suppressErrors: Boolean(opts.suppressErrors) 27 | } 28 | }); 29 | 30 | this.sources = []; // Absolute paths to the IDL and Impl directories. 31 | this.utilPath = null; 32 | } 33 | 34 | addSource(idl, impl) { 35 | if (typeof idl !== "string") { 36 | throw new TypeError("idl path has to be a string"); 37 | } 38 | if (typeof impl !== "string") { 39 | throw new TypeError("impl path has to be a string"); 40 | } 41 | this.sources.push({ idlPath: path.resolve(idl), impl: path.resolve(impl) }); 42 | return this; 43 | } 44 | 45 | async _collectSources() { 46 | const stats = await Promise.all(this.sources.map(src => fs.stat(src.idlPath))); 47 | const files = []; 48 | for (let i = 0; i < stats.length; ++i) { 49 | if (stats[i].isDirectory()) { 50 | const folderContents = await fs.readdir(this.sources[i].idlPath); 51 | for (const file of folderContents) { 52 | if (file.endsWith(".webidl")) { 53 | files.push({ 54 | idlPath: path.join(this.sources[i].idlPath, file), 55 | impl: this.sources[i].impl 56 | }); 57 | } 58 | } 59 | } else { 60 | files.push({ 61 | idlPath: this.sources[i].idlPath, 62 | impl: this.sources[i].impl 63 | }); 64 | } 65 | } 66 | return files; 67 | } 68 | 69 | async _readFiles(files) { 70 | const zipped = []; 71 | const fileContents = await Promise.all(files.map(f => fs.readFile(f.idlPath, { encoding: "utf-8" }))); 72 | for (let i = 0; i < files.length; ++i) { 73 | zipped.push({ 74 | idlContent: fileContents[i], 75 | impl: files[i].impl 76 | }); 77 | } 78 | return zipped; 79 | } 80 | 81 | _parse(outputDir, contents) { 82 | const parsed = contents.map(content => ({ 83 | idl: webidl.parse(content.idlContent), 84 | impl: content.impl 85 | })); 86 | 87 | this.ctx.initialize(); 88 | const { 89 | interfaces, 90 | interfaceMixins, 91 | callbackInterfaces, 92 | callbackFunctions, 93 | dictionaries, 94 | enumerations, 95 | typedefs 96 | } = this.ctx; 97 | 98 | // first we're gathering all full interfaces and ignore partial ones 99 | for (const file of parsed) { 100 | for (const instruction of file.idl) { 101 | let obj; 102 | switch (instruction.type) { 103 | case "interface": 104 | if (instruction.partial) { 105 | break; 106 | } 107 | 108 | obj = new Interface(this.ctx, instruction, { 109 | implDir: file.impl 110 | }); 111 | interfaces.set(obj.name, obj); 112 | break; 113 | case "interface mixin": 114 | if (instruction.partial) { 115 | break; 116 | } 117 | 118 | obj = new InterfaceMixin(this.ctx, instruction); 119 | interfaceMixins.set(obj.name, obj); 120 | break; 121 | case "callback interface": 122 | obj = new CallbackInterface(this.ctx, instruction); 123 | callbackInterfaces.set(obj.name, obj); 124 | break; 125 | case "callback": 126 | obj = new CallbackFunction(this.ctx, instruction); 127 | callbackFunctions.set(obj.name, obj); 128 | break; 129 | case "includes": 130 | break; // handled later 131 | case "dictionary": 132 | if (instruction.partial) { 133 | break; 134 | } 135 | 136 | obj = new Dictionary(this.ctx, instruction); 137 | dictionaries.set(obj.name, obj); 138 | break; 139 | case "enum": 140 | obj = new Enumeration(this.ctx, instruction); 141 | enumerations.set(obj.name, obj); 142 | break; 143 | case "typedef": 144 | obj = new Typedef(this.ctx, instruction); 145 | typedefs.set(obj.name, obj); 146 | break; 147 | default: 148 | if (!this.ctx.options.suppressErrors) { 149 | throw new Error(`Can't convert type '${instruction.type}'`); 150 | } 151 | } 152 | } 153 | } 154 | 155 | // second we add all partial members and handle includes 156 | for (const file of parsed) { 157 | for (const instruction of file.idl) { 158 | let oldMembers, extAttrs; 159 | switch (instruction.type) { 160 | case "interface": 161 | if (!instruction.partial) { 162 | break; 163 | } 164 | 165 | if (this.ctx.options.suppressErrors && !interfaces.has(instruction.name)) { 166 | break; 167 | } 168 | oldMembers = interfaces.get(instruction.name).idl.members; 169 | oldMembers.push(...instruction.members); 170 | extAttrs = interfaces.get(instruction.name).idl.extAttrs; 171 | extAttrs.push(...instruction.extAttrs); 172 | break; 173 | case "interface mixin": 174 | if (!instruction.partial) { 175 | break; 176 | } 177 | 178 | if (this.ctx.options.suppressErrors && !interfaceMixins.has(instruction.name)) { 179 | break; 180 | } 181 | oldMembers = interfaceMixins.get(instruction.name).idl.members; 182 | oldMembers.push(...instruction.members); 183 | extAttrs = interfaceMixins.get(instruction.name).idl.extAttrs; 184 | extAttrs.push(...instruction.extAttrs); 185 | break; 186 | case "dictionary": 187 | if (!instruction.partial) { 188 | break; 189 | } 190 | if (this.ctx.options.suppressErrors && !dictionaries.has(instruction.name)) { 191 | break; 192 | } 193 | oldMembers = dictionaries.get(instruction.name).idl.members; 194 | oldMembers.push(...instruction.members); 195 | extAttrs = dictionaries.get(instruction.name).idl.extAttrs; 196 | extAttrs.push(...instruction.extAttrs); 197 | break; 198 | case "includes": 199 | if (this.ctx.options.suppressErrors && !interfaces.has(instruction.target)) { 200 | break; 201 | } 202 | interfaces.get(instruction.target).includes(instruction.includes); 203 | break; 204 | } 205 | } 206 | } 207 | } 208 | 209 | async _writeFiles(outputDir) { 210 | const utilsText = await fs.readFile(path.resolve(__dirname, "output/utils.js")); 211 | await fs.writeFile(this.utilPath, utilsText); 212 | 213 | const { interfaces, callbackInterfaces, callbackFunctions, dictionaries, enumerations } = this.ctx; 214 | 215 | let relativeUtils = path.relative(outputDir, this.utilPath).replace(/\\/g, "/"); 216 | if (relativeUtils[0] !== ".") { 217 | relativeUtils = `./${relativeUtils}`; 218 | } 219 | 220 | for (const obj of interfaces.values()) { 221 | let source = obj.toString(); 222 | 223 | let implFile = path.relative(outputDir, path.resolve(obj.opts.implDir, obj.name + this.ctx.implSuffix)); 224 | implFile = implFile.replace(/\\/g, "/"); // fix windows file paths 225 | if (implFile[0] !== ".") { 226 | implFile = `./${implFile}`; 227 | } 228 | 229 | source = ` 230 | "use strict"; 231 | 232 | const conversions = require("webidl-conversions"); 233 | const utils = require("${relativeUtils}"); 234 | ${source} 235 | const Impl = require("${implFile}.js"); 236 | `; 237 | 238 | source = this._prettify(source); 239 | 240 | await fs.writeFile(path.join(outputDir, `${obj.name}.js`), source); 241 | } 242 | 243 | for (const obj of [...callbackInterfaces.values(), ...callbackFunctions.values(), ...dictionaries.values()]) { 244 | let source = obj.toString(); 245 | 246 | source = ` 247 | "use strict"; 248 | 249 | const conversions = require("webidl-conversions"); 250 | const utils = require("${relativeUtils}"); 251 | ${source} 252 | `; 253 | 254 | source = this._prettify(source); 255 | 256 | await fs.writeFile(path.join(outputDir, `${obj.name}.js`), source); 257 | } 258 | 259 | for (const obj of enumerations.values()) { 260 | const source = this._prettify(` 261 | "use strict"; 262 | 263 | ${obj.toString()} 264 | `); 265 | await fs.writeFile(path.join(outputDir, `${obj.name}.js`), source); 266 | } 267 | } 268 | 269 | _prettify(source) { 270 | return prettier.format(source, { 271 | printWidth: 120, 272 | trailingComma: "none", 273 | arrowParens: "avoid", 274 | parser: "babel" 275 | }); 276 | } 277 | 278 | async generate(outputDir) { 279 | if (!this.utilPath) { 280 | this.utilPath = path.join(outputDir, "utils.js"); 281 | } 282 | 283 | const sources = await this._collectSources(); 284 | const contents = await this._readFiles(sources); 285 | this._parse(outputDir, contents); 286 | await this._writeFiles(outputDir); 287 | } 288 | } 289 | 290 | module.exports = Transformer; 291 | -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const conversions = require("webidl-conversions"); 4 | 5 | const utils = require("./utils"); 6 | 7 | const typedArrayTypes = new Set([ 8 | "Int8Array", 9 | "Int16Array", 10 | "Int32Array", 11 | "Uint8Array", 12 | "Uint16Array", 13 | "Uint32Array", 14 | "Uint8ClampedArray", 15 | "Float32Array", 16 | "Float64Array" 17 | ]); 18 | const arrayBufferViewTypes = new Set([...typedArrayTypes, "DataView"]); 19 | const bufferSourceTypes = new Set([...arrayBufferViewTypes, "ArrayBuffer"]); 20 | const stringTypes = new Set(["DOMString", "ByteString", "USVString"]); 21 | const integerTypes = new Set([ 22 | "byte", 23 | "octet", 24 | "short", 25 | "unsigned short", 26 | "long", 27 | "unsigned long", 28 | "long long", 29 | "unsigned long long" 30 | ]); 31 | const numericTypes = new Set([...integerTypes, "float", "unrestricted float", "double", "unrestricted double"]); 32 | 33 | const resolvedMap = new WeakMap(); 34 | 35 | function mergeExtAttrs(a = [], b = []) { 36 | return [...a, ...b]; 37 | } 38 | 39 | // Types of types that generate an output file. 40 | const resolvedTypes = new Set(["callback", "callback interface", "dictionary", "enumeration", "interface"]); 41 | 42 | function resolveType(ctx, idlType, stack = []) { 43 | if (resolvedMap.has(idlType)) { 44 | return resolvedMap.get(idlType); 45 | } 46 | const original = idlType; 47 | idlType = deepClone(idlType); 48 | resolvedMap.set(original, idlType); 49 | 50 | if (idlType.union) { 51 | const types = []; 52 | for (let type of idlType.idlType) { 53 | type = resolveType(ctx, type, stack); 54 | idlType.nullable ||= type.nullable; 55 | // Only the outermost union is nullable 56 | type.nullable = false; 57 | if (type.union) { 58 | types.push(...type.idlType); 59 | } else { 60 | types.push(type); 61 | } 62 | } 63 | for (const type of types) { 64 | type.extAttrs = deepClone(mergeExtAttrs(type.extAttrs, idlType.extAttrs)); 65 | } 66 | idlType.idlType = types; 67 | return idlType; 68 | } else if (idlType.generic) { 69 | idlType.idlType = idlType.idlType.map(t => resolveType(ctx, t, stack)); 70 | return idlType; 71 | } else if (resolvedTypes.has(ctx.typeOf(idlType.idlType))) { 72 | // already resolved 73 | return idlType; 74 | } else if (ctx.typedefs.has(idlType.idlType)) { 75 | const out = deepClone(ctx.typedefs.get(idlType.idlType).resolve(stack)); 76 | resolvedMap.set(original, out); 77 | out.nullable ||= idlType.nullable; 78 | out.extAttrs = deepClone(mergeExtAttrs(out.extAttrs, idlType.extAttrs)); 79 | if (out.union) { 80 | for (const type of out.idlType) { 81 | type.extAttrs = deepClone(mergeExtAttrs(type.extAttrs, idlType.extAttrs)); 82 | } 83 | } 84 | return out; 85 | } else if (conversions[idlType.idlType]) { 86 | // already resolved 87 | return idlType; 88 | } 89 | // unknown 90 | return idlType; 91 | } 92 | 93 | function deepClone(obj) { 94 | return JSON.parse(JSON.stringify(obj)); 95 | } 96 | 97 | function generateTypeConversion( 98 | ctx, 99 | name, 100 | idlType, 101 | argAttrs = [], 102 | parentName = undefined, 103 | errPrefix = '"The provided value"' 104 | ) { 105 | const requires = new utils.RequiresMap(ctx); 106 | let str = ""; 107 | 108 | idlType = resolveType(ctx, idlType); 109 | const extAttrs = idlType.extAttrs !== undefined ? [...idlType.extAttrs, ...argAttrs] : argAttrs; 110 | 111 | if (idlType.nullable) { 112 | const callbackFunction = ctx.callbackFunctions.get(idlType.idlType); 113 | if (callbackFunction !== undefined && callbackFunction.legacyTreatNonObjectAsNull) { 114 | str += ` 115 | if (!utils.isObject(${name})) { 116 | ${name} = null; 117 | } else { 118 | `; 119 | } else { 120 | str += ` 121 | if (${name} === null || ${name} === undefined) { 122 | ${name} = null; 123 | } else { 124 | `; 125 | } 126 | } 127 | 128 | if (idlType.union) { 129 | // union type 130 | generateUnion(); 131 | } else if (idlType.generic === "sequence") { 132 | // sequence type 133 | generateSequence(); 134 | } else if (idlType.generic === "record") { 135 | // record type 136 | generateRecord(); 137 | } else if (idlType.generic === "Promise") { 138 | // Promise type 139 | generatePromise(); 140 | } else if (idlType.generic === "FrozenArray") { 141 | // frozen array type 142 | generateFrozenArray(); 143 | } else if (conversions[idlType.idlType]) { 144 | // string or number type compatible with webidl-conversions 145 | generateWebIDLConversions(`conversions["${idlType.idlType}"]`); 146 | } else if (resolvedTypes.has(ctx.typeOf(idlType.idlType))) { 147 | // callback functions, callback interfaces, dictionaries, enumerations, and interfaces 148 | let fn; 149 | // Avoid requiring the interface itself 150 | if (idlType.idlType !== parentName) { 151 | fn = `${idlType.idlType}.convert`; 152 | requires.addRelative(idlType.idlType); 153 | } else { 154 | fn = `exports.convert`; 155 | } 156 | generateWebIDL2JS(fn); 157 | } else { 158 | // unknown 159 | // Try to get the impl anyway. 160 | str += ` 161 | ${name} = utils.tryImplForWrapper(${name}); 162 | `; 163 | } 164 | 165 | if (idlType.nullable) { 166 | str += "}"; 167 | } 168 | 169 | return { 170 | requires, 171 | body: str 172 | }; 173 | 174 | function generateUnion() { 175 | const union = extractUnionInfo(ctx, idlType, errPrefix); 176 | const output = []; 177 | 178 | if (union.unknown) { 179 | // Oh well, what do we know... 180 | str += `${name} = utils.tryImplForWrapper(${name});`; 181 | return; 182 | } 183 | 184 | if (!idlType.nullable && union.dictionary) { 185 | const conv = generateTypeConversion(ctx, name, union.dictionary, [], parentName, errPrefix); 186 | requires.merge(conv.requires); 187 | output.push(` 188 | if (${name} === null || ${name} === undefined) { 189 | ${conv.body} 190 | } 191 | `); 192 | } 193 | 194 | if (union.object) { 195 | output.push(`if (utils.isObject(${name}) && ${name}[utils.implSymbol]) { 196 | ${name} = utils.implForWrapper(${name}); 197 | }`); 198 | } else if (union.interfaces.size > 0) { 199 | const exprs = [...union.interfaces].map(iface => { 200 | let fn; 201 | // Avoid requiring the interface itself 202 | if (iface !== parentName) { 203 | fn = `${iface}.is`; 204 | requires.addRelative(iface); 205 | } else { 206 | fn = "exports.is"; 207 | } 208 | return `${fn}(${name})`; 209 | }); 210 | output.push(` 211 | if (${exprs.join(" || ")}) { 212 | ${name} = utils.implForWrapper(${name}); 213 | } 214 | `); 215 | } 216 | 217 | // Do not convert buffer source types as the impl code can either "get a reference" or "get a copy" to the bytes. 218 | if (union.ArrayBuffer || union.object) { 219 | output.push(`if (utils.isArrayBuffer(${name})) {}`); 220 | } 221 | if (union.ArrayBufferViews.size > 0 || union.object) { 222 | let condition = `ArrayBuffer.isView(${name})`; 223 | // Skip specific type check if all ArrayBufferView member types are allowed. 224 | if (union.ArrayBufferViews.size !== arrayBufferViewTypes.size) { 225 | const exprs = [...union.ArrayBufferViews].map(a => `${name}.constructor.name === "${a}"`); 226 | condition += ` && (${exprs.join(" || ")})`; 227 | } 228 | output.push(`if (${condition}) {}`); 229 | } 230 | 231 | if (union.callbackFunction || union.object) { 232 | let code = `if (typeof ${name} === "function") {`; 233 | 234 | if (union.callbackFunction) { 235 | const conv = generateTypeConversion( 236 | ctx, 237 | name, 238 | union.callbackFunction, 239 | [], 240 | parentName, 241 | `${errPrefix} + " callback function"` 242 | ); 243 | requires.merge(conv.requires); 244 | code += conv.body; 245 | } else if (union.object) { 246 | // noop 247 | } 248 | 249 | code += "}"; 250 | 251 | output.push(code); 252 | } 253 | 254 | if (union.sequenceLike || union.dictionary || union.record || union.object || union.callbackInterface) { 255 | let code = `if (utils.isObject(${name})) {`; 256 | 257 | if (union.sequenceLike) { 258 | code += `if (${name}[Symbol.iterator] !== undefined) {`; 259 | const conv = generateTypeConversion( 260 | ctx, 261 | name, 262 | union.sequenceLike, 263 | [], 264 | parentName, 265 | `${errPrefix} + " sequence"` 266 | ); 267 | requires.merge(conv.requires); 268 | code += conv.body; 269 | code += `} else {`; 270 | } 271 | 272 | if (union.dictionary) { 273 | const conv = generateTypeConversion( 274 | ctx, 275 | name, 276 | union.dictionary, 277 | [], 278 | parentName, 279 | `${errPrefix} + " dictionary"` 280 | ); 281 | requires.merge(conv.requires); 282 | code += conv.body; 283 | } else if (union.record) { 284 | const conv = generateTypeConversion(ctx, name, union.record, [], parentName, `${errPrefix} + " record"`); 285 | requires.merge(conv.requires); 286 | code += conv.body; 287 | } else if (union.callbackInterface) { 288 | const conv = generateTypeConversion( 289 | ctx, 290 | name, 291 | union.callbackInterface, 292 | [], 293 | parentName, 294 | `${errPrefix} + " callback interface"` 295 | ); 296 | requires.merge(conv.requires); 297 | code += conv.body; 298 | } else if (union.object) { 299 | // noop 300 | } 301 | 302 | if (union.sequenceLike) { 303 | code += "}"; 304 | } 305 | 306 | code += "}"; 307 | 308 | output.push(code); 309 | } 310 | 311 | if (union.boolean) { 312 | output.push(` 313 | if (typeof ${name} === "boolean") { 314 | ${generateTypeConversion(ctx, name, union.boolean, [], parentName, errPrefix).body} 315 | } 316 | `); 317 | } 318 | 319 | if (union.numeric) { 320 | output.push(` 321 | if (typeof ${name} === "number") { 322 | ${generateTypeConversion(ctx, name, union.numeric, [], parentName, errPrefix).body} 323 | } 324 | `); 325 | } 326 | 327 | { 328 | let code = "{"; 329 | const type = union.string || union.numeric || union.boolean; 330 | if (type) { 331 | const conv = generateTypeConversion(ctx, name, type, [], parentName, errPrefix); 332 | code += conv.body; 333 | requires.merge(conv.requires); 334 | } else { 335 | code += `throw new globalObject.TypeError(${errPrefix} + " is not of any supported type.")`; 336 | } 337 | code += "}"; 338 | output.push(code); 339 | } 340 | 341 | str += output.join(" else "); 342 | } 343 | 344 | function generateSequence() { 345 | const conv = generateTypeConversion( 346 | ctx, 347 | "nextItem", 348 | idlType.idlType[0], 349 | [], 350 | parentName, 351 | `${errPrefix} + "'s element"` 352 | ); 353 | requires.merge(conv.requires); 354 | 355 | str += ` 356 | if (!utils.isObject(${name})) { 357 | throw new globalObject.TypeError(${errPrefix} + " is not an iterable object."); 358 | } else { 359 | const V = []; 360 | const tmp = ${name}; 361 | for (let nextItem of tmp) { 362 | ${conv.body} 363 | V.push(nextItem); 364 | } 365 | ${name} = V; 366 | } 367 | `; 368 | } 369 | 370 | function generateRecord() { 371 | const keyConv = generateTypeConversion( 372 | ctx, 373 | "typedKey", 374 | idlType.idlType[0], 375 | [], 376 | parentName, 377 | `${errPrefix} + "'s key"` 378 | ); 379 | requires.merge(keyConv.requires); 380 | const valConv = generateTypeConversion( 381 | ctx, 382 | "typedValue", 383 | idlType.idlType[1], 384 | [], 385 | parentName, 386 | `${errPrefix} + "'s value"` 387 | ); 388 | requires.merge(valConv.requires); 389 | 390 | str += ` 391 | if (!utils.isObject(${name})) { 392 | throw new globalObject.TypeError(${errPrefix} + " is not an object."); 393 | } else { 394 | const result = Object.create(null); 395 | for (const key of Reflect.ownKeys(${name})) { 396 | const desc = Object.getOwnPropertyDescriptor(${name}, key); 397 | if (desc && desc.enumerable) { 398 | let typedKey = key; 399 | ${keyConv.body} 400 | 401 | let typedValue = ${name}[key]; 402 | ${valConv.body} 403 | result[typedKey] = typedValue; 404 | } 405 | } 406 | ${name} = result; 407 | } 408 | `; 409 | } 410 | 411 | function generatePromise() { 412 | str += `${name} = new globalObject.Promise(resolve => resolve(${name}));`; 413 | } 414 | 415 | function generateFrozenArray() { 416 | generateSequence(); 417 | str += `${name} = Object.freeze(${name});`; 418 | } 419 | 420 | function generateWebIDLConversions(conversionFn) { 421 | const enforceRange = utils.getExtAttr(extAttrs, "EnforceRange"); 422 | const clamp = utils.getExtAttr(extAttrs, "Clamp"); 423 | const nullToEmptyString = utils.getExtAttr(extAttrs, "LegacyNullToEmptyString"); 424 | 425 | let optString = `context: ${errPrefix}, globals: globalObject,`; 426 | if (clamp) { 427 | optString += "clamp: true,"; 428 | } 429 | if (enforceRange) { 430 | optString += "enforceRange: true,"; 431 | } 432 | if (nullToEmptyString) { 433 | optString += "treatNullAsEmptyString: true,"; 434 | } 435 | if (idlType.array) { 436 | str += ` 437 | for (let i = 0; i < ${name}.length; ++i) { 438 | ${name}[i] = ${conversionFn}(${name}[i], { ${optString} }); 439 | } 440 | `; 441 | } else { 442 | str += ` 443 | ${name} = ${conversionFn}(${name}, { ${optString} }); 444 | `; 445 | } 446 | } 447 | 448 | function generateWebIDL2JS(conversionFn) { 449 | const optString = `context: ${errPrefix}`; 450 | 451 | if (idlType.array) { 452 | str += ` 453 | for (let i = 0; i < ${name}.length; ++i) { 454 | ${name}[i] = ${conversionFn}(globalObject, ${name}[i], { ${optString} }); 455 | } 456 | `; 457 | } else { 458 | str += ` 459 | ${name} = ${conversionFn}(globalObject, ${name}, { ${optString} }); 460 | `; 461 | } 462 | } 463 | } 464 | 465 | // Condense the member types of a union to a more consumable structured object. At the same time, check for the validity 466 | // of the union type (no forbidden types, no indistinguishable member types). Duplicated types are allowed for now 467 | // though. 468 | function extractUnionInfo(ctx, idlType, errPrefix) { 469 | const seen = { 470 | sequenceLike: null, 471 | record: null, 472 | get dictionaryLike() { 473 | return this.dictionary !== null || this.record !== null || this.callbackInterface !== null; 474 | }, 475 | ArrayBuffer: false, 476 | ArrayBufferViews: new Set(), 477 | get BufferSource() { 478 | return this.ArrayBuffer || this.ArrayBufferViews.size > 0; 479 | }, 480 | object: false, 481 | string: null, 482 | numeric: null, 483 | boolean: null, 484 | callbackFunction: null, 485 | dictionary: null, 486 | callbackInterface: null, 487 | interfaces: new Set(), 488 | get interfaceLike() { 489 | return this.interfaces.size > 0 || this.BufferSource; 490 | }, 491 | unknown: false 492 | }; 493 | for (const item of idlType.idlType) { 494 | if (item.generic === "sequence" || item.generic === "FrozenArray") { 495 | if (seen.sequenceLike) { 496 | error("There can only be one sequence-like type in a union type"); 497 | } 498 | seen.sequenceLike = item; 499 | } else if (item.generic === "record") { 500 | if (seen.object) { 501 | error("Dictionary-like types are not distinguishable with object type"); 502 | } 503 | if (seen.callbackFunction) { 504 | error("Dictionary-like types are not distinguishable with callback functions"); 505 | } 506 | if (seen.dictionaryLike) { 507 | error("There can only be one dictionary-like type in a union type"); 508 | } 509 | seen.record = item; 510 | } else if (item.generic === "Promise") { 511 | error("Promise types are not supported in union types"); 512 | } else if (item.generic) { 513 | error(`Unknown generic type ${item.generic}`); 514 | } else if (item.idlType === "any") { 515 | error("any type is not allowed in a union type"); 516 | } else if (item.idlType === "ArrayBuffer") { 517 | if (seen.object) { 518 | error("ArrayBuffer is not distinguishable with object type"); 519 | } 520 | seen.ArrayBuffer = true; 521 | } else if (arrayBufferViewTypes.has(item.idlType)) { 522 | if (seen.object) { 523 | error(`${item.idlType} is not distinguishable with object type`); 524 | } 525 | seen.ArrayBufferViews.add(item.idlType); 526 | } else if (stringTypes.has(item.idlType) || ctx.enumerations.has(item.idlType)) { 527 | if (seen.string) { 528 | error("There can only be one string type in a union type"); 529 | } 530 | seen.string = item; 531 | } else if (numericTypes.has(item.idlType)) { 532 | if (seen.numeric) { 533 | error("There can only be one numeric type in a union type"); 534 | } 535 | seen.numeric = item; 536 | } else if (item.idlType === "object") { 537 | if (seen.interfaceLike) { 538 | error("Object type is not distinguishable with interface-like types"); 539 | } 540 | if (seen.callbackFunction) { 541 | error("Object type is not distinguishable with callback functions"); 542 | } 543 | if (seen.dictionaryLike) { 544 | error("Object type is not distinguishable with dictionary-like types"); 545 | } 546 | if (seen.sequenceLike) { 547 | error("Object type is not distinguishable with sequence-like types"); 548 | } 549 | seen.object = true; 550 | } else if (item.idlType === "boolean") { 551 | seen.boolean = item; 552 | } else if (ctx.callbackFunctions.has(item.idlType)) { 553 | if (seen.object) { 554 | error("Callback functions are not distinguishable with object type"); 555 | } 556 | if (seen.dictionaryLike) { 557 | error("Callback functions are not distinguishable with dictionary-like types"); 558 | } 559 | seen.callbackFunction = item.idlType; 560 | } else if (ctx.dictionaries.has(item.idlType)) { 561 | if (seen.object) { 562 | error("Dictionary-like types are not distinguishable with object type"); 563 | } 564 | if (seen.callbackFunction) { 565 | error("Dictionary-like types are not distinguishable with callback functions"); 566 | } 567 | if (seen.dictionaryLike) { 568 | error("There can only be one dictionary-like type in a union type"); 569 | } 570 | seen.dictionary = item; 571 | } else if (ctx.callbackInterfaces.has(item.idlType)) { 572 | if (seen.object) { 573 | error("Dictionary-like types are not distinguishable with object type"); 574 | } 575 | if (seen.callbackFunction) { 576 | error("Dictionary-like types are not distinguishable with callback functions"); 577 | } 578 | if (seen.dictionaryLike) { 579 | error("There can only be one dictionary-like type in a union type"); 580 | } 581 | seen.callbackInterface = item.idlType; 582 | } else if (ctx.interfaces.has(item.idlType)) { 583 | if (seen.object) { 584 | error("Interface types are not distinguishable with object type"); 585 | } 586 | seen.interfaces.add(item.idlType); 587 | } else { 588 | seen.unknown = true; 589 | } 590 | } 591 | return seen; 592 | 593 | function error(msg) { 594 | throw new Error(`${msg}\n When compiling "${eval(errPrefix)}"`); // eslint-disable-line no-eval 595 | } 596 | } 597 | 598 | // https://heycam.github.io/webidl/#dfn-includes-a-nullable-type 599 | function includesNullableType(ctx, idlType) { 600 | idlType = resolveType(ctx, idlType); 601 | if (idlType.nullable) { 602 | return true; 603 | } 604 | if (!idlType.union) { 605 | return false; 606 | } 607 | for (const type of idlType.idlType) { 608 | if (type.nullable) { 609 | return true; 610 | } 611 | } 612 | return false; 613 | } 614 | 615 | function includesDictionaryType(ctx, idlType) { 616 | idlType = resolveType(ctx, idlType); 617 | if (typeof idlType.idlType === "string" && ctx.dictionaries.has(idlType.idlType)) { 618 | return true; 619 | } 620 | if (!idlType.union) { 621 | return false; 622 | } 623 | for (const type of idlType.idlType) { 624 | if (includesDictionaryType(ctx, type)) { 625 | return true; 626 | } 627 | } 628 | return false; 629 | } 630 | 631 | function sameType(ctx, type1, type2) { 632 | if (type1 === type2) { 633 | return true; 634 | } 635 | 636 | type1 = resolveType(ctx, type1); 637 | type2 = resolveType(ctx, type2); 638 | if (type1.generic !== type2.generic) { 639 | return false; 640 | } 641 | if (type1.union !== type2.union) { 642 | return false; 643 | } 644 | if (includesNullableType(ctx, type1) !== includesNullableType(ctx, type2)) { 645 | return false; 646 | } 647 | // TODO: check extended attributes 648 | if (typeof type1.idlType === "string" || typeof type2.idlType === "string") { 649 | return type1.idlType === type2.idlType; 650 | } 651 | if (type1.generic === "sequence" || type1.generic === "FrozenArray") { 652 | return sameType(ctx, type1.idlType, type2.idlType); 653 | } 654 | if (type1.generic === "record") { 655 | return sameType(ctx, type1.idlType[0], type2.idlType[0]) && 656 | sameType(ctx, type2.idlType[1], type2.idlType[1]); 657 | } 658 | 659 | if (!type1.union) { 660 | // This branch should never be taken. 661 | return false; 662 | } 663 | const extracted1 = extractUnionInfo(ctx, type1, `""`); 664 | const extracted2 = extractUnionInfo(ctx, type2, `""`); 665 | return sameType(ctx, extracted1.sequenceLike, extracted2.sequenceLike) && 666 | sameType(ctx, extracted1.record, extracted2.record) && 667 | extracted1.ArrayBuffer !== extracted2.ArrayBuffer && 668 | JSON.stringify([...extracted1.ArrayBufferViews].sort()) === 669 | JSON.stringify([...extracted2.ArrayBufferViews].sort()) && 670 | extracted1.object === extracted2.object && 671 | sameType(ctx, extracted1.string, extracted2.string) && 672 | sameType(ctx, extracted1.numeric, extracted2.numeric) && 673 | sameType(ctx, extracted1.boolean, extracted2.boolean) && 674 | extracted1.callback === extracted2.callback && 675 | sameType(ctx, extracted1.dictionary, extracted2.dictionary) && 676 | JSON.stringify([...extracted1.interfaces].sort()) === 677 | JSON.stringify([...extracted2.interfaces].sort()) && 678 | extracted1.callbackInterface === extracted2.callbackInterface && 679 | extracted1.unknown === extracted2.unknown; 680 | } 681 | 682 | function areDistinguishable(ctx, type1, type2) { 683 | const resolved1 = resolveType(ctx, type1); 684 | const resolved2 = resolveType(ctx, type2); 685 | 686 | const effectivelyNullable1 = includesNullableType(ctx, resolved1) || includesDictionaryType(ctx, resolved1); 687 | const effectivelyNullable2 = includesNullableType(ctx, resolved2) || includesDictionaryType(ctx, resolved2); 688 | if ((includesNullableType(ctx, resolved1) && effectivelyNullable2) || 689 | (effectivelyNullable1 && includesNullableType(ctx, resolved2))) { 690 | return false; 691 | } 692 | 693 | if (resolved1.union && resolved2.union) { 694 | for (const i of resolved1.idlType) { 695 | for (const j of resolved2.idlType) { 696 | if (!areDistinguishable(ctx, i, j)) { 697 | return false; 698 | } 699 | } 700 | } 701 | return true; 702 | } 703 | 704 | function inner(inner1, inner2) { 705 | if (inner1.union) { 706 | for (const i of inner1.idlType) { 707 | if (!areDistinguishable(ctx, i, inner2)) { 708 | return false; 709 | } 710 | } 711 | return true; 712 | } 713 | 714 | if (inner1.idlType === "boolean") { 715 | return inner2.idlType !== "boolean"; 716 | } 717 | 718 | if (numericTypes.has(inner1.idlType)) { 719 | return !numericTypes.has(inner2.idlType); 720 | } 721 | 722 | if (stringTypes.has(inner1.idlType) || ctx.enumerations.has(inner1.idlType)) { 723 | return !stringTypes.has(inner2.idlType) && !ctx.enumerations.has(inner2.idlType); 724 | } 725 | 726 | const isInterfaceLike1 = ctx.interfaces.has(inner1.idlType) || 727 | bufferSourceTypes.has(inner1.idlType); 728 | const isInterfaceLike2 = ctx.interfaces.has(inner2.idlType) || 729 | bufferSourceTypes.has(inner2.idlType); 730 | const isDictionaryLike1 = ctx.dictionaries.has(inner1.idlType) || 731 | ctx.callbackInterfaces.has(inner1.idlType) || 732 | inner1.generic === "record"; 733 | const isDictionaryLike2 = ctx.dictionaries.has(inner2.idlType) || 734 | ctx.callbackInterfaces.has(inner2.idlType) || 735 | inner2.generic === "record"; 736 | const isSequenceLike1 = inner1.generic === "sequence" || inner1.generic === "FrozenArray"; 737 | const isSequenceLike2 = inner2.generic === "sequence" || inner2.generic === "FrozenArray"; 738 | 739 | if (inner1.idlType === "object") { 740 | return inner2.idlType !== "object" && 741 | !isInterfaceLike2 && 742 | !isDictionaryLike2 && 743 | !isSequenceLike2; 744 | } 745 | 746 | if (inner1.idlType === "symbol") { 747 | return inner2.idlType !== "symbol"; 748 | } 749 | 750 | if (isInterfaceLike1) { 751 | return inner2.idlType !== "object" && 752 | (!isInterfaceLike2 || 753 | (!ctx.interfaces.has(inner2.idlType) || 754 | !new Set(ctx.interfaces.get(inner2.idlType).allInterfaces()).has(inner1.idlType))); 755 | } 756 | 757 | if (isDictionaryLike1) { 758 | return inner2.idlType !== "object" && !isDictionaryLike2; 759 | } 760 | 761 | if (isSequenceLike1) { 762 | return inner2.idlType !== "object" && !isSequenceLike2; 763 | } 764 | 765 | return true; 766 | } 767 | 768 | return inner(resolved1, resolved2) && inner(resolved2, resolved1); 769 | } 770 | 771 | module.exports = { 772 | arrayBufferViewTypes, 773 | stringTypes, 774 | numericTypes, 775 | 776 | generateTypeConversion, 777 | resolveType, 778 | includesNullableType, 779 | includesDictionaryType, 780 | areDistinguishable, 781 | sameType 782 | }; 783 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { extname } = require("path"); 3 | const keywords = require("./keywords.js"); 4 | 5 | function getDefault(dflt) { 6 | switch (dflt.type) { 7 | case "boolean": 8 | case "string": 9 | return JSON.stringify(dflt.value); 10 | case "number": 11 | return dflt.value; 12 | case "null": 13 | case "NaN": 14 | return dflt.type; 15 | case "Infinity": 16 | return `${dflt.negative ? "-" : ""}Infinity`; 17 | case "sequence": 18 | return "[]"; 19 | } 20 | throw new Error(`Unexpected default type: ${dflt.type}`); 21 | } 22 | 23 | function getExtAttr(attrs, name) { 24 | for (let i = 0; i < attrs.length; ++i) { 25 | if (attrs[i].name === name) { 26 | return attrs[i]; 27 | } 28 | } 29 | 30 | return null; 31 | } 32 | 33 | function isGlobal(idl) { 34 | return Boolean(getExtAttr(idl.extAttrs, "Global")); 35 | } 36 | 37 | function hasCEReactions(idl) { 38 | return Boolean(getExtAttr(idl.extAttrs, "CEReactions")); 39 | } 40 | 41 | function isOnInstance(memberIDL, interfaceIDL) { 42 | return memberIDL.special !== "static" && isGlobal(interfaceIDL); 43 | } 44 | 45 | function symbolName(symbol) { 46 | const desc = String(symbol).replace(/^Symbol\((.*)\)$/, "$1"); 47 | if (!desc.startsWith("Symbol.")) { 48 | throw new Error(`Internal error: Unsupported property name ${String(symbol)}`); 49 | } 50 | return desc; 51 | } 52 | 53 | function propertyName(name) { 54 | // All Web IDL identifiers are valid JavaScript PropertyNames, other than those with '-'. 55 | const isJSIdentifier = !name.includes("-"); 56 | if (isJSIdentifier) { 57 | return name; 58 | } 59 | return JSON.stringify(name); 60 | } 61 | 62 | function stringifyPropertyKey(prop) { 63 | return typeof prop === "symbol" ? `[${symbolName(prop)}]` : propertyName(prop); 64 | } 65 | 66 | function stringifyPropertyName(prop) { 67 | return typeof prop === "symbol" ? symbolName(prop) : JSON.stringify(propertyName(prop)); 68 | } 69 | 70 | // type can be "accessor" or "regular" 71 | function getPropertyDescriptorModifier(currentDesc, targetDesc, type, value = undefined) { 72 | const changes = []; 73 | if (value !== undefined) { 74 | changes.push(`value: ${value}`); 75 | } 76 | if (currentDesc.configurable !== targetDesc.configurable) { 77 | changes.push(`configurable: ${targetDesc.configurable}`); 78 | } 79 | if (currentDesc.enumerable !== targetDesc.enumerable) { 80 | changes.push(`enumerable: ${targetDesc.enumerable}`); 81 | } 82 | if (type !== "accessor" && currentDesc.writable !== targetDesc.writable) { 83 | changes.push(`writable: ${targetDesc.writable}`); 84 | } 85 | 86 | if (changes.length === 0) { 87 | return undefined; 88 | } 89 | return `{ ${changes.join(", ")} }`; 90 | } 91 | 92 | const defaultDefinePropertyDescriptor = { 93 | configurable: false, 94 | enumerable: false, 95 | writable: false 96 | }; 97 | 98 | function formatArgs(args) { 99 | return args 100 | .filter(name => name !== null && name !== undefined && name !== "") 101 | .map(name => name + (keywords.has(name) ? "_" : "")) 102 | .join(", "); 103 | } 104 | 105 | function toKey(type, func = "") { 106 | return String(func + type).replace(/[./-]+/g, " ").trim().replace(/ /g, "_"); 107 | } 108 | 109 | const PACKAGE_NAME_REGEX = /^(?:@([^/]+?)[/])?([^/]+?)$/u; 110 | 111 | class RequiresMap extends Map { 112 | constructor(ctx) { 113 | super(); 114 | this.ctx = ctx; 115 | } 116 | 117 | add(name, func = "") { 118 | const key = toKey(name, func); 119 | 120 | // If `name` is a package name or has a file extension, then use it as-is, 121 | // otherwise append the `.js` file extension: 122 | const importPath = PACKAGE_NAME_REGEX.test(name) || extname(name) ? name : `${name}.js`; 123 | let req = `require(${JSON.stringify(importPath)})`; 124 | 125 | if (func) { 126 | req += `.${func}`; 127 | } 128 | 129 | this.addRaw(key, req); 130 | return key; 131 | } 132 | 133 | addRelative(type, func = "") { 134 | const key = toKey(type, func); 135 | 136 | const path = type.startsWith(".") ? type : `./${type}`; 137 | let req = `require("${path}.js")`; 138 | 139 | if (func) { 140 | req += `.${func}`; 141 | } 142 | 143 | this.addRaw(key, req); 144 | return key; 145 | } 146 | 147 | addRaw(key, expr) { 148 | if (this.has(key) && this.get(key) !== expr) { 149 | throw new Error(`Internal error: Variable name clash: ${key}; was ${this.get(key)}, adding: ${expr}`); 150 | } 151 | super.set(key, expr); 152 | } 153 | 154 | merge(src) { 155 | if (!src || !(src instanceof RequiresMap)) { 156 | return; 157 | } 158 | for (const [key, val] of src) { 159 | this.addRaw(key, val); 160 | } 161 | } 162 | 163 | generate() { 164 | return [...this.keys()].map(key => `const ${key} = ${this.get(key)};`).join("\n"); 165 | } 166 | } 167 | 168 | module.exports = { 169 | getDefault, 170 | getExtAttr, 171 | isGlobal, 172 | hasCEReactions, 173 | isOnInstance, 174 | stringifyPropertyKey, 175 | stringifyPropertyName, 176 | getPropertyDescriptorModifier, 177 | defaultDefinePropertyDescriptor, 178 | formatArgs, 179 | RequiresMap 180 | }; 181 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webidl2js", 3 | "version": "18.0.2", 4 | "description": "Auto-generates class structures for WebIDL specifications", 5 | "main": "lib/transformer.js", 6 | "files": [ 7 | "lib/" 8 | ], 9 | "repository": "github:jsdom/webidl2js", 10 | "dependencies": { 11 | "prettier": "^2.8.8", 12 | "webidl-conversions": "^7.0.0", 13 | "webidl2": "^24.4.1" 14 | }, 15 | "devDependencies": { 16 | "@domenic/eslint-config": "^4.0.1", 17 | "eslint": "^9.25.0", 18 | "globals": "^16.0.0", 19 | "jest": "^29.7.0" 20 | }, 21 | "scripts": { 22 | "test": "jest", 23 | "update-snapshots": "jest --updateSnapshot", 24 | "lint": "eslint" 25 | }, 26 | "jest": { 27 | "testEnvironment": "node", 28 | "watchPathIgnorePatterns": [ 29 | "/test/output" 30 | ] 31 | }, 32 | "engines": { 33 | "node": ">=18" 34 | }, 35 | "author": "Sebastian Mayr ", 36 | "license": "MIT" 37 | } 38 | -------------------------------------------------------------------------------- /test/cases/AsyncCallbackFunction.webidl: -------------------------------------------------------------------------------- 1 | callback AsyncCallbackFunction = Promise (); 2 | -------------------------------------------------------------------------------- /test/cases/AsyncCallbackInterface.webidl: -------------------------------------------------------------------------------- 1 | callback interface AsyncCallbackInterface { 2 | Promise asyncMethod(); 3 | }; 4 | -------------------------------------------------------------------------------- /test/cases/AsyncIterablePairArgs.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface AsyncIterablePairArgs { 3 | async iterable(optional boolean url = false, optional DOMString string, optional Dictionary dict = {}); 4 | }; 5 | -------------------------------------------------------------------------------- /test/cases/AsyncIterablePairNoArgs.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface AsyncIterablePairNoArgs { 3 | async iterable(); 4 | }; 5 | -------------------------------------------------------------------------------- /test/cases/AsyncIterableValueArgs.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface AsyncIterableValueArgs { 3 | async iterable(optional URL url); 4 | }; 5 | -------------------------------------------------------------------------------- /test/cases/AsyncIterableValueNoArgs.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface AsyncIterableValueNoArgs { 3 | async iterable; 4 | }; 5 | -------------------------------------------------------------------------------- /test/cases/AsyncIterableWithReturn.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface AsyncIterableWithReturn { 3 | [WebIDL2JSHasReturnSteps] async iterable(optional Dictionary dict = {}); 4 | }; 5 | -------------------------------------------------------------------------------- /test/cases/BufferSourceTypes.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface BufferSourceTypes { 3 | undefined bs(BufferSource source); 4 | undefined ab(ArrayBuffer ab); 5 | undefined abv(ArrayBufferView abv); 6 | undefined u8a(Uint8Array u8); 7 | 8 | undefined abUnion((ArrayBuffer or DOMString) ab); 9 | undefined u8aUnion((Uint8Array or DOMString) ab); 10 | }; 11 | -------------------------------------------------------------------------------- /test/cases/CEReactions.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface CEReactions { 3 | [CEReactions] attribute DOMString attr; 4 | [CEReactions] undefined method(); 5 | 6 | getter DOMString (DOMString name); 7 | [CEReactions] setter undefined (DOMString name, DOMString value); 8 | [CEReactions] deleter undefined (DOMString name); 9 | 10 | [CEReactions] Promise promiseOperation(); 11 | [CEReactions] readonly attribute Promise promiseAttribute; 12 | }; 13 | -------------------------------------------------------------------------------- /test/cases/CallbackUsage.webidl: -------------------------------------------------------------------------------- 1 | dictionary CallbackUsage { 2 | Function function; 3 | VoidFunction voidFunction; 4 | URLCallback urlCallback; 5 | URLHandler urlHandler; 6 | URLHandlerNonNull urlHandlerNonNull; 7 | }; 8 | -------------------------------------------------------------------------------- /test/cases/DOMImplementation.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface DOMImplementation { 3 | [NewObject] DocumentType createDocumentType(DOMString qualifiedName, DOMString publicId, DOMString systemId); 4 | [NewObject] XMLDocument createDocument(DOMString? namespace, [LegacyNullToEmptyString] DOMString qualifiedName, optional DocumentType? doctype = null); 5 | [NewObject] Document createHTMLDocument(optional DOMString title); 6 | 7 | boolean hasFeature(); // useless; always returns true 8 | }; 9 | -------------------------------------------------------------------------------- /test/cases/DOMRect.webidl: -------------------------------------------------------------------------------- 1 | // Simplified from https://drafts.fxtf.org/geometry-1/#domrect 2 | 3 | [Exposed=(Window,Worker), 4 | Serializable, 5 | LegacyWindowAlias=SVGRect] 6 | interface DOMRect /* : DOMRectReadOnly */ { 7 | constructor(optional unrestricted double x = 0, optional unrestricted double y = 0, 8 | optional unrestricted double width = 0, optional unrestricted double height = 0); 9 | 10 | [NewObject, WebIDL2JSCallWithGlobal] static DOMRect fromRect(optional Dictionary other = {}); 11 | 12 | attribute unrestricted double x; 13 | attribute unrestricted double y; 14 | attribute unrestricted double width; 15 | attribute unrestricted double height; 16 | }; 17 | -------------------------------------------------------------------------------- /test/cases/Dictionary.webidl: -------------------------------------------------------------------------------- 1 | dictionary Dictionary { 2 | boolean boolWithDefault = false; 3 | required URL requiredInterface; 4 | sequence seq; 5 | DOMString vanillaString; 6 | }; 7 | -------------------------------------------------------------------------------- /test/cases/DictionaryConvert.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface DictionaryConvert { 3 | // Test force-conversion of dictionary types. 4 | DOMString op(optional DOMString arg1, optional Dictionary arg2 = {}); 5 | }; 6 | -------------------------------------------------------------------------------- /test/cases/Enum.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface Enum { 3 | undefined op(RequestDestination destination); 4 | attribute RequestDestination attr; 5 | }; 6 | -------------------------------------------------------------------------------- /test/cases/EventListener.webidl: -------------------------------------------------------------------------------- 1 | callback interface EventListener { 2 | undefined handleEvent(Event event); 3 | }; 4 | -------------------------------------------------------------------------------- /test/cases/EventTarget.webidl: -------------------------------------------------------------------------------- 1 | // Simplified from https://dom.spec.whatwg.org/#eventtarget 2 | 3 | [Exposed=(Window,Worker,AudioWorklet)] 4 | interface EventTarget { 5 | constructor(); 6 | 7 | undefined addEventListener(DOMString type, EventListener? callback); 8 | // undefined removeEventListener(DOMString type, EventListener? callback); 9 | // boolean dispatchEvent(Event event); 10 | }; 11 | -------------------------------------------------------------------------------- /test/cases/Global.webidl: -------------------------------------------------------------------------------- 1 | [Global=Global,Exposed=Global] 2 | interface Global { 3 | undefined op(); 4 | [LegacyUnforgeable] undefined unforgeableOp(); 5 | attribute DOMString attr; 6 | [LegacyUnforgeable] attribute DOMString unforgeableAttr; 7 | 8 | getter DOMString (unsigned long index); 9 | attribute unsigned long length; 10 | iterable; 11 | 12 | static undefined staticOp(); 13 | static attribute DOMString staticAttr; 14 | }; 15 | -------------------------------------------------------------------------------- /test/cases/HTMLCollection.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window, LegacyUnenumerableNamedProperties] 2 | interface HTMLCollection { 3 | readonly attribute unsigned long length; 4 | [WebIDL2JSValueAsUnsupported=_null] getter Element? item(unsigned long index); 5 | [WebIDL2JSValueAsUnsupported=_null] getter Element? namedItem(DOMString name); 6 | }; 7 | -------------------------------------------------------------------------------- /test/cases/HTMLConstructor.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window, 2 | HTMLConstructor] 3 | interface HTMLConstructor {}; 4 | -------------------------------------------------------------------------------- /test/cases/HTMLFormControlsCollection.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface HTMLFormControlsCollection : HTMLCollection { 3 | // inherits length and item() 4 | [WebIDL2JSValueAsUnsupported=_null] getter (RadioNodeList or Element)? namedItem(DOMString name); // shadows inherited namedItem() 5 | }; 6 | -------------------------------------------------------------------------------- /test/cases/LegacyLenientAttributes.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface LegacyLenientAttributes { 3 | [LegacyLenientSetter] readonly attribute DOMString lenientSetter; 4 | [LegacyLenientSetter, LegacyLenientThis] readonly attribute DOMString lenientThisSetter; 5 | 6 | [LegacyLenientThis] attribute DOMString lenientThis; 7 | [LegacyLenientThis] readonly attribute DOMString readonlyLenientThis; 8 | [LegacyLenientThis, Replaceable] readonly attribute DOMString replaceableLenientThis; 9 | }; 10 | -------------------------------------------------------------------------------- /test/cases/LegacyNoInterfaceObject.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window, LegacyNoInterfaceObject] 2 | interface LegacyNoInterfaceObject { 3 | attribute DOMString abc; 4 | DOMString def(); 5 | }; 6 | -------------------------------------------------------------------------------- /test/cases/LegacyUnforgeable.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface LegacyUnforgeable { 3 | [LegacyUnforgeable] stringifier attribute USVString href; 4 | [LegacyUnforgeable] readonly attribute USVString origin; 5 | [LegacyUnforgeable] attribute USVString protocol; 6 | [LegacyUnforgeable] undefined assign(USVString url); 7 | }; 8 | -------------------------------------------------------------------------------- /test/cases/LegacyUnforgeableMap.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface LegacyUnforgeableMap { 3 | [LegacyUnforgeable] readonly attribute DOMString a; 4 | getter DOMString (DOMString x); 5 | setter DOMString (DOMString x, DOMString y); 6 | }; 7 | -------------------------------------------------------------------------------- /test/cases/MixedIn.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface MixedIn { 3 | attribute DOMString mixedInAttr; 4 | DOMString mixedInOp(); 5 | const byte mixedInConst = 43; 6 | }; 7 | MixedIn includes InterfaceMixin; 8 | 9 | partial interface mixin InterfaceMixin { 10 | const byte ifaceMixinConst = 42; 11 | }; 12 | 13 | interface mixin InterfaceMixin { 14 | DOMString ifaceMixinOp(); 15 | attribute DOMString ifaceMixinAttr; 16 | }; 17 | -------------------------------------------------------------------------------- /test/cases/NodeFilter.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | callback interface NodeFilter { 3 | // Constants for acceptNode() 4 | const unsigned short FILTER_ACCEPT = 1; 5 | const unsigned short FILTER_REJECT = 2; 6 | const unsigned short FILTER_SKIP = 3; 7 | 8 | // Constants for whatToShow 9 | const unsigned long SHOW_ALL = 0xFFFFFFFF; 10 | const unsigned long SHOW_ELEMENT = 0x1; 11 | const unsigned long SHOW_ATTRIBUTE = 0x2; 12 | const unsigned long SHOW_TEXT = 0x4; 13 | const unsigned long SHOW_CDATA_SECTION = 0x8; 14 | const unsigned long SHOW_ENTITY_REFERENCE = 0x10; // historical 15 | const unsigned long SHOW_ENTITY = 0x20; // historical 16 | const unsigned long SHOW_PROCESSING_INSTRUCTION = 0x40; 17 | const unsigned long SHOW_COMMENT = 0x80; 18 | const unsigned long SHOW_DOCUMENT = 0x100; 19 | const unsigned long SHOW_DOCUMENT_TYPE = 0x200; 20 | const unsigned long SHOW_DOCUMENT_FRAGMENT = 0x400; 21 | const unsigned long SHOW_NOTATION = 0x800; // historical 22 | 23 | unsigned short acceptNode(Node node); 24 | }; 25 | -------------------------------------------------------------------------------- /test/cases/Overloads.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface Overloads { 3 | constructor(); 4 | constructor(DOMString arg1); 5 | constructor(URL arg1); 6 | 7 | DOMString compatible(DOMString arg1); 8 | byte compatible(DOMString arg1, DOMString arg2); 9 | URL compatible(DOMString arg1, DOMString arg2, optional long arg3 = 0); 10 | 11 | DOMString incompatible1(DOMString arg1); 12 | byte incompatible1(long arg1); 13 | 14 | DOMString incompatible2(DOMString arg1); 15 | byte incompatible2(DOMString arg1, DOMString arg2); 16 | 17 | DOMString incompatible3(DOMString arg1, optional URL arg2); 18 | byte incompatible3(DOMString arg1, DOMString arg2); 19 | byte incompatible3(DOMString arg1, BufferSource arg2); 20 | byte incompatible3(DOMString arg1, long arg2, BufferSource arg3, BufferSource arg4); 21 | }; 22 | -------------------------------------------------------------------------------- /test/cases/PromiseTypes.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface PromiseTypes { 3 | undefined voidPromiseConsumer(Promise p); 4 | undefined promiseConsumer(Promise p); 5 | 6 | Promise promiseOperation(); 7 | readonly attribute Promise promiseAttribute; 8 | 9 | [LegacyUnforgeable] Promise unforgeablePromiseOperation(); 10 | [LegacyUnforgeable] readonly attribute Promise unforgeablePromiseAttribute; 11 | 12 | static Promise staticPromiseOperation(); 13 | static readonly attribute Promise staticPromiseAttribute; 14 | }; 15 | -------------------------------------------------------------------------------- /test/cases/Reflect.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface Reflect { 3 | [Reflect] attribute boolean reflectedBoolean; 4 | [FooBar, Reflect] attribute DOMString reflectedDOMString; 5 | [Reflect, FooBar] attribute long reflectedLong; 6 | [Reflect] attribute unsigned long reflectedUnsignedLong; 7 | [FooBar, ReflectURL] attribute USVString reflectedUSVStringURL; 8 | 9 | [FooBar, Reflect=reflection] attribute DOMString reflectionTest; 10 | [Reflect=with_underscore, FooBar] attribute DOMString withUnderscore; 11 | }; 12 | -------------------------------------------------------------------------------- /test/cases/Replaceable.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface Replaceable { 3 | [Replaceable] readonly attribute DOMString replaceable; 4 | }; 5 | -------------------------------------------------------------------------------- /test/cases/RequestDestination.webidl: -------------------------------------------------------------------------------- 1 | enum RequestDestination { "", "audio", "document", "embed", "font", "image", "manifest", "object", "report", "script", "sharedworker", "style", "track", "video", "worker", "xslt" }; 2 | -------------------------------------------------------------------------------- /test/cases/SeqAndRec.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface SeqAndRec { 3 | constructor(); 4 | 5 | undefined recordConsumer(record rec); 6 | undefined recordConsumer2(record rec); 7 | undefined sequenceConsumer(sequence seq); 8 | undefined sequenceConsumer2(sequence seq); 9 | undefined frozenArrayConsumer(FrozenArray arr); 10 | }; 11 | -------------------------------------------------------------------------------- /test/cases/Static.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface Static { 3 | static attribute DOMString abc; 4 | attribute DOMString abc; 5 | static DOMString def(); 6 | DOMString def(); 7 | }; 8 | -------------------------------------------------------------------------------- /test/cases/Storage.webidl: -------------------------------------------------------------------------------- 1 | // https://html.spec.whatwg.org/multipage/webstorage.html#storage-2 2 | [Exposed=Window] 3 | interface Storage { 4 | readonly attribute unsigned long length; 5 | DOMString? key(unsigned long index); 6 | getter DOMString? getItem(DOMString key); 7 | setter undefined setItem(DOMString key, DOMString value); 8 | deleter undefined removeItem(DOMString key); 9 | undefined clear(); 10 | }; 11 | -------------------------------------------------------------------------------- /test/cases/StringifierAttribute.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface StringifierAttribute { 3 | stringifier readonly attribute DOMString attr; 4 | }; 5 | -------------------------------------------------------------------------------- /test/cases/StringifierDefaultOperation.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface StringifierDefaultOperation { 3 | stringifier; 4 | }; 5 | -------------------------------------------------------------------------------- /test/cases/StringifierNamedOperation.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface StringifierNamedOperation { 3 | stringifier DOMString operation(); 4 | }; 5 | -------------------------------------------------------------------------------- /test/cases/StringifierOperation.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface StringifierOperation { 3 | stringifier DOMString (); 4 | }; 5 | -------------------------------------------------------------------------------- /test/cases/TypedefsAndUnions.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface TypedefsAndUnions { 3 | undefined numOrStrConsumer(NumOrStr a); 4 | undefined numOrEnumConsumer((double or RequestDestination)? a); 5 | undefined numOrStrOrNullConsumer(NumOrStrOrNull a); 6 | undefined numOrStrOrURLOrNullConsumer(NumOrStrOrURLOrNull? a); 7 | undefined urlMapInnerConsumer(URLMapInner a); 8 | undefined urlMapConsumer(URLMap a); 9 | undefined bufferSourceOrURLConsumer((BufferSource or URL) b); 10 | undefined arrayBufferViewOrURLMapConsumer((ArrayBufferView or URLMap) b); 11 | undefined arrayBufferViewDupConsumer((ArrayBufferView or Uint8ClampedArray) b); 12 | 13 | attribute (ArrayBuffer or Uint8Array or Uint16Array) buf; 14 | attribute DOMTimeStamp time; 15 | }; 16 | 17 | typedef ([Clamp] double or DOMString) NumOrStr; 18 | typedef [EnforceRange] NumOrStr? NumOrStrOrNull; 19 | typedef (NumOrStrOrNull or URL)? NumOrStrOrURLOrNull; 20 | typedef record URLMapInner; 21 | typedef URLMapInner? URLMap; 22 | -------------------------------------------------------------------------------- /test/cases/URL.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=(Window,Worker), 2 | LegacyWindowAlias=webkitURL] 3 | interface URL { 4 | constructor(USVString url, optional USVString base); 5 | 6 | stringifier attribute USVString href; 7 | readonly attribute USVString origin; 8 | attribute USVString protocol; 9 | attribute USVString username; 10 | attribute USVString password; 11 | attribute USVString host; 12 | attribute USVString hostname; 13 | attribute USVString port; 14 | attribute USVString pathname; 15 | attribute USVString search; 16 | [SameObject] readonly attribute URLSearchParams searchParams; 17 | attribute USVString hash; 18 | 19 | USVString toJSON(); 20 | }; 21 | -------------------------------------------------------------------------------- /test/cases/URLCallback.webidl: -------------------------------------------------------------------------------- 1 | callback URLCallback = URL (URL? url, DOMString string); 2 | -------------------------------------------------------------------------------- /test/cases/URLHandlerNonNull.webidl: -------------------------------------------------------------------------------- 1 | // Adapted from https://html.spec.whatwg.org/multipage/webappapis.html#eventhandlernonnull. 2 | [LegacyTreatNonObjectAsNull] 3 | callback URLHandlerNonNull = any (URL url); 4 | typedef URLHandlerNonNull? URLHandler; 5 | -------------------------------------------------------------------------------- /test/cases/URLList.webidl: -------------------------------------------------------------------------------- 1 | // Adapted from NodeList 2 | // https://dom.spec.whatwg.org/#nodelist 3 | [Exposed=Window] 4 | interface URLList { 5 | getter URL? item(unsigned long index); 6 | readonly attribute unsigned long length; 7 | iterable; 8 | }; 9 | -------------------------------------------------------------------------------- /test/cases/URLSearchParams.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=(Window,Worker)] 2 | interface URLSearchParams { 3 | constructor(optional (sequence> or record or USVString) init = ""); 4 | 5 | undefined append(USVString name, USVString value); 6 | undefined delete(USVString name); 7 | USVString? get(USVString name); 8 | sequence getAll(USVString name); 9 | boolean has(USVString name); 10 | undefined set(USVString name, USVString value); 11 | 12 | undefined sort(); 13 | 14 | iterable; 15 | stringifier; 16 | }; 17 | -------------------------------------------------------------------------------- /test/cases/URLSearchParamsCollection.webidl: -------------------------------------------------------------------------------- 1 | // Adapted from HTMLCollection 2 | // https://dom.spec.whatwg.org/#htmlcollection 3 | [Exposed=Window, LegacyUnenumerableNamedProperties] 4 | interface URLSearchParamsCollection { 5 | readonly attribute unsigned long length; 6 | [WebIDL2JSValueAsUnsupported=_undefined] getter URLSearchParams? item(unsigned long index); 7 | [WebIDL2JSValueAsUnsupported=_null] getter URLSearchParams? namedItem(DOMString name); 8 | }; 9 | -------------------------------------------------------------------------------- /test/cases/URLSearchParamsCollection2.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface URLSearchParamsCollection2 : URLSearchParamsCollection { 3 | setter undefined (DOMString key, URL value); 4 | }; 5 | -------------------------------------------------------------------------------- /test/cases/UnderscoredProperties.webidl: -------------------------------------------------------------------------------- 1 | // https://heycam.github.io/webidl/#idl-names 2 | [Exposed=Window] 3 | interface _UnderscoredProperties { 4 | const byte _const = 42; 5 | attribute byte _attribute; 6 | static undefined _static(DOMString _void); 7 | undefined _operation(sequence _sequence); 8 | }; 9 | -------------------------------------------------------------------------------- /test/cases/Unscopable.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface Unscopable { 3 | [Unscopable] attribute boolean unscopableTest; 4 | }; 5 | Unscopable includes UnscopableMixin; 6 | 7 | interface mixin UnscopableMixin { 8 | [Unscopable] attribute boolean unscopableMixin; 9 | }; 10 | -------------------------------------------------------------------------------- /test/cases/Variadic.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface Variadic { 3 | undefined simple1(DOMString... strings); 4 | 5 | undefined simple2(DOMString first, URL... urls); 6 | 7 | // This is handled in a broken way, with no conversions. It is included here just so we can track any behavior changes 8 | // to the broken implementation over time. 9 | undefined overloaded1(DOMString... strings); 10 | undefined overloaded1(unsigned long... numbers); 11 | 12 | // This is handled in an extra-broken way, with a random conversion of the second argument to a DOMString, but no 13 | // subsequent arguments. Again, it is included here just so we can track changes over time. 14 | undefined overloaded2(DOMString first, DOMString... strings); 15 | undefined overloaded2(unsigned long first, DOMString... strings); 16 | }; 17 | -------------------------------------------------------------------------------- /test/cases/ZeroArgConstructor.webidl: -------------------------------------------------------------------------------- 1 | [Exposed=Window] 2 | interface ZeroArgConstructor { 3 | constructor(); 4 | }; 5 | -------------------------------------------------------------------------------- /test/implementations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsdom/webidl2js/d657f1c5942403b4dd1903b601ee0a26be7f39eb/test/implementations/.gitkeep -------------------------------------------------------------------------------- /test/output/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsdom/webidl2js/d657f1c5942403b4dd1903b601ee0a26be7f39eb/test/output/.gitkeep -------------------------------------------------------------------------------- /test/reflector.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports.boolean = { 4 | get(implObj, attrName) { 5 | return `return ${implObj}.hasAttributeNS(null, "${attrName}");`; 6 | }, 7 | set(implObj, attrName) { 8 | return ` 9 | if (V) { 10 | ${implObj}.setAttributeNS(null, "${attrName}", ""); 11 | } else { 12 | ${implObj}.removeAttributeNS(null, "${attrName}"); 13 | } 14 | `; 15 | } 16 | }; 17 | 18 | module.exports.DOMString = { 19 | get(implObj, attrName) { 20 | return ` 21 | const value = ${implObj}.getAttributeNS(null, "${attrName}"); 22 | return value === null ? "" : value; 23 | `; 24 | }, 25 | set(implObj, attrName) { 26 | return `${implObj}.setAttributeNS(null, "${attrName}", V);`; 27 | } 28 | }; 29 | 30 | module.exports.USVString = { 31 | get(implObj, attrName) { 32 | return ` 33 | const value = ${implObj}.getAttributeNS(null, "${attrName}"); 34 | return value === null ? "" : value; 35 | `; 36 | }, 37 | set(implObj, attrName) { 38 | return `${implObj}.setAttributeNS(null, "${attrName}", V);`; 39 | } 40 | }; 41 | 42 | module.exports.long = { 43 | get(implObj, attrName) { 44 | return ` 45 | const value = parseInt(${implObj}.getAttributeNS(null, "${attrName}")); 46 | return isNaN(value) || value < -2147483648 || value > 2147483647 ? 0 : value 47 | `; 48 | }, 49 | set(implObj, attrName) { 50 | return `${implObj}.setAttributeNS(null, "${attrName}", String(V));`; 51 | } 52 | }; 53 | 54 | module.exports["unsigned long"] = { 55 | get(implObj, attrName) { 56 | return ` 57 | const value = parseInt(${implObj}.getAttributeNS(null, "${attrName}")); 58 | return isNaN(value) || value < 0 || value > 2147483647 ? 0 : value 59 | `; 60 | }, 61 | set(implObj, attrName) { 62 | return `${implObj}.setAttributeNS(null, "${attrName}", String(V > 2147483647 ? 0 : V));`; 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const Transformer = require(".."); 6 | const reflector = require("./reflector"); 7 | 8 | const rootDir = path.resolve(__dirname, ".."); 9 | const casesDir = path.resolve(__dirname, "cases"); 10 | const implsDir = path.resolve(__dirname, "implementations"); 11 | const outputDir = path.resolve(__dirname, "output"); 12 | 13 | const idlFiles = fs.readdirSync(casesDir); 14 | 15 | describe("generation", () => { 16 | describe("built-in types", () => { 17 | beforeAll(() => { 18 | const transformer = new Transformer(); 19 | return transformer.generate(outputDir); 20 | }); 21 | 22 | test("Function", () => { 23 | const outputFile = path.resolve(outputDir, "Function.js"); 24 | const output = fs.readFileSync(outputFile, { encoding: "utf-8" }); 25 | 26 | expect(output).toMatchSnapshot(); 27 | }); 28 | 29 | test("VoidFunction", () => { 30 | const outputFile = path.resolve(outputDir, "VoidFunction.js"); 31 | const output = fs.readFileSync(outputFile, { encoding: "utf-8" }); 32 | 33 | expect(output).toMatchSnapshot(); 34 | }); 35 | }); 36 | 37 | describe("without processors", () => { 38 | beforeAll(() => { 39 | const transformer = new Transformer(); 40 | transformer.addSource(casesDir, implsDir); 41 | 42 | return transformer.generate(outputDir); 43 | }); 44 | 45 | for (const idlFile of idlFiles) { 46 | test(idlFile, () => { 47 | const outputFile = path.resolve(outputDir, `${path.basename(idlFile, ".webidl")}.js`); 48 | const output = fs.readFileSync(outputFile, { encoding: "utf-8" }); 49 | 50 | expect(output).toMatchSnapshot(); 51 | }); 52 | } 53 | }); 54 | 55 | describe("with processors", () => { 56 | beforeAll(() => { 57 | const transformer = new Transformer({ 58 | processCEReactions(code) { 59 | const ceReactions = this.addImport("../CEReactions"); 60 | 61 | return ` 62 | ${ceReactions}.preSteps(globalObject); 63 | try { 64 | ${code} 65 | } finally { 66 | ${ceReactions}.postSteps(globalObject); 67 | } 68 | `; 69 | }, 70 | processHTMLConstructor() { 71 | const htmlConstructor = this.addImport("../HTMLConstructor", "HTMLConstructor"); 72 | 73 | return ` 74 | return ${htmlConstructor}(globalObject, interfaceName); 75 | `; 76 | }, 77 | processReflect(idl, implObj) { 78 | const reflectAttr = idl.extAttrs.find(attr => attr.name === "Reflect"); 79 | const attrName = 80 | (reflectAttr && reflectAttr.rhs && reflectAttr.rhs.value.replace(/_/g, "-")) || idl.name.toLowerCase(); 81 | if (idl.idlType.idlType === "USVString") { 82 | const reflectURL = idl.extAttrs.find(attr => attr.name === "ReflectURL"); 83 | if (reflectURL) { 84 | const whatwgURL = this.addImport("whatwg-url"); 85 | return { 86 | get: ` 87 | const value = ${implObj}.getAttributeNS(null, "${attrName}"); 88 | if (value === null) { 89 | return ""; 90 | } 91 | const urlRecord = ${whatwgURL}.parseURL(value, { baseURL: "http://localhost:8080/" }); 92 | return urlRecord === null ? conversions.USVString(value) : ${whatwgURL}.serializeURL(urlRecord); 93 | `, 94 | set: ` 95 | ${implObj}.setAttributeNS(null, "${attrName}", V); 96 | ` 97 | }; 98 | } 99 | } 100 | const reflect = reflector[idl.idlType.idlType]; 101 | return { 102 | get: reflect.get(implObj, attrName), 103 | set: reflect.set(implObj, attrName) 104 | }; 105 | } 106 | }); 107 | transformer.addSource(casesDir, implsDir); 108 | 109 | return transformer.generate(outputDir); 110 | }); 111 | 112 | for (const idlFile of idlFiles) { 113 | test(idlFile, () => { 114 | const outputFile = path.resolve(outputDir, `${path.basename(idlFile, ".webidl")}.js`); 115 | const output = fs.readFileSync(outputFile, { encoding: "utf-8" }); 116 | 117 | expect(output).toMatchSnapshot(); 118 | }); 119 | } 120 | }); 121 | 122 | test("utils.js", () => { 123 | const input = fs.readFileSync(path.resolve(rootDir, "lib/output/utils.js"), { encoding: "utf-8" }); 124 | const output = fs.readFileSync(path.resolve(outputDir, "utils.js"), { encoding: "utf-8" }); 125 | expect(output).toBe(input); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const utils = require("../lib/output/utils"); 4 | 5 | describe("utils.js", () => { 6 | describe("isObject", () => { 7 | const primitives = [ 8 | 123, 9 | "string", 10 | Symbol.iterator, 11 | true, 12 | null, 13 | undefined 14 | ]; 15 | 16 | for (const primitive of primitives) { 17 | test(primitive === null ? "null" : typeof primitive, () => { 18 | expect(utils.isObject(primitive)).toBe(false); 19 | }); 20 | } 21 | 22 | test("bigint", () => { 23 | expect(utils.isObject(123n)).toBe(false); 24 | }); 25 | 26 | test("object", () => { 27 | expect(utils.isObject({})).toBe(true); 28 | }); 29 | 30 | test("function", () => { 31 | expect(utils.isObject(() => {})).toBe(true); 32 | }); 33 | }); 34 | 35 | describe("newObjectInRealm", () => { 36 | test("creates a new object in the given realm with the properties of the given object", () => { 37 | const realm = { Object: function Object() {}, Array }; 38 | const object = utils.newObjectInRealm(realm, { foo: 42 }); 39 | expect(object).toBeInstanceOf(realm.Object); 40 | expect(object).toEqual({ foo: 42 }); 41 | }); 42 | 43 | test("uses the captured intrinsic Object, not the current realm.Object", () => { 44 | const realm = { Object, Array }; 45 | utils.initCtorRegistry(realm); 46 | realm.Object = function Object() {}; 47 | const object = utils.newObjectInRealm(realm, {}); 48 | expect(object).toBeInstanceOf(Object); 49 | expect(object).not.toBeInstanceOf(realm.Object); 50 | }); 51 | }); 52 | }); 53 | --------------------------------------------------------------------------------