UNPKG

webidl2js

Version:

Auto-generates class structures for WebIDL specifications

291 lines (253 loc) 8.91 kB
"use strict"; const path = require("path"); const fs = require("fs").promises; const webidl = require("webidl2"); const prettier = require("prettier"); const Context = require("./context"); const Typedef = require("./constructs/typedef"); const Interface = require("./constructs/interface"); const InterfaceMixin = require("./constructs/interface-mixin"); const CallbackInterface = require("./constructs/callback-interface.js"); const CallbackFunction = require("./constructs/callback-function"); const Dictionary = require("./constructs/dictionary"); const Enumeration = require("./constructs/enumeration"); class Transformer { constructor(opts = {}) { this.ctx = new Context({ implSuffix: opts.implSuffix, processCEReactions: opts.processCEReactions, processHTMLConstructor: opts.processHTMLConstructor, processReflect: opts.processReflect, options: { suppressErrors: Boolean(opts.suppressErrors) } }); this.sources = []; // Absolute paths to the IDL and Impl directories. this.utilPath = null; } addSource(idl, impl) { if (typeof idl !== "string") { throw new TypeError("idl path has to be a string"); } if (typeof impl !== "string") { throw new TypeError("impl path has to be a string"); } this.sources.push({ idlPath: path.resolve(idl), impl: path.resolve(impl) }); return this; } async _collectSources() { const stats = await Promise.all(this.sources.map(src => fs.stat(src.idlPath))); const files = []; for (let i = 0; i < stats.length; ++i) { if (stats[i].isDirectory()) { const folderContents = await fs.readdir(this.sources[i].idlPath); for (const file of folderContents) { if (file.endsWith(".webidl")) { files.push({ idlPath: path.join(this.sources[i].idlPath, file), impl: this.sources[i].impl }); } } } else { files.push({ idlPath: this.sources[i].idlPath, impl: this.sources[i].impl }); } } return files; } async _readFiles(files) { const zipped = []; const fileContents = await Promise.all(files.map(f => fs.readFile(f.idlPath, { encoding: "utf-8" }))); for (let i = 0; i < files.length; ++i) { zipped.push({ idlContent: fileContents[i], impl: files[i].impl }); } return zipped; } _parse(outputDir, contents) { const parsed = contents.map(content => ({ idl: webidl.parse(content.idlContent), impl: content.impl })); this.ctx.initialize(); const { interfaces, interfaceMixins, callbackInterfaces, callbackFunctions, dictionaries, enumerations, typedefs } = this.ctx; // first we're gathering all full interfaces and ignore partial ones for (const file of parsed) { for (const instruction of file.idl) { let obj; switch (instruction.type) { case "interface": if (instruction.partial) { break; } obj = new Interface(this.ctx, instruction, { implDir: file.impl }); interfaces.set(obj.name, obj); break; case "interface mixin": if (instruction.partial) { break; } obj = new InterfaceMixin(this.ctx, instruction); interfaceMixins.set(obj.name, obj); break; case "callback interface": obj = new CallbackInterface(this.ctx, instruction); callbackInterfaces.set(obj.name, obj); break; case "callback": obj = new CallbackFunction(this.ctx, instruction); callbackFunctions.set(obj.name, obj); break; case "includes": break; // handled later case "dictionary": if (instruction.partial) { break; } obj = new Dictionary(this.ctx, instruction); dictionaries.set(obj.name, obj); break; case "enum": obj = new Enumeration(this.ctx, instruction); enumerations.set(obj.name, obj); break; case "typedef": obj = new Typedef(this.ctx, instruction); typedefs.set(obj.name, obj); break; default: if (!this.ctx.options.suppressErrors) { throw new Error(`Can't convert type '${instruction.type}'`); } } } } // second we add all partial members and handle includes for (const file of parsed) { for (const instruction of file.idl) { let oldMembers, extAttrs; switch (instruction.type) { case "interface": if (!instruction.partial) { break; } if (this.ctx.options.suppressErrors && !interfaces.has(instruction.name)) { break; } oldMembers = interfaces.get(instruction.name).idl.members; oldMembers.push(...instruction.members); extAttrs = interfaces.get(instruction.name).idl.extAttrs; extAttrs.push(...instruction.extAttrs); break; case "interface mixin": if (!instruction.partial) { break; } if (this.ctx.options.suppressErrors && !interfaceMixins.has(instruction.name)) { break; } oldMembers = interfaceMixins.get(instruction.name).idl.members; oldMembers.push(...instruction.members); extAttrs = interfaceMixins.get(instruction.name).idl.extAttrs; extAttrs.push(...instruction.extAttrs); break; case "dictionary": if (!instruction.partial) { break; } if (this.ctx.options.suppressErrors && !dictionaries.has(instruction.name)) { break; } oldMembers = dictionaries.get(instruction.name).idl.members; oldMembers.push(...instruction.members); extAttrs = dictionaries.get(instruction.name).idl.extAttrs; extAttrs.push(...instruction.extAttrs); break; case "includes": if (this.ctx.options.suppressErrors && !interfaces.has(instruction.target)) { break; } interfaces.get(instruction.target).includes(instruction.includes); break; } } } } async _writeFiles(outputDir) { const utilsText = await fs.readFile(path.resolve(__dirname, "output/utils.js")); await fs.writeFile(this.utilPath, utilsText); const { interfaces, callbackInterfaces, callbackFunctions, dictionaries, enumerations } = this.ctx; let relativeUtils = path.relative(outputDir, this.utilPath).replace(/\\/g, "/"); if (relativeUtils[0] !== ".") { relativeUtils = `./${relativeUtils}`; } for (const obj of interfaces.values()) { let source = obj.toString(); let implFile = path.relative(outputDir, path.resolve(obj.opts.implDir, obj.name + this.ctx.implSuffix)); implFile = implFile.replace(/\\/g, "/"); // fix windows file paths if (implFile[0] !== ".") { implFile = `./${implFile}`; } source = ` "use strict"; const conversions = require("webidl-conversions"); const utils = require("${relativeUtils}"); ${source} const Impl = require("${implFile}.js"); `; source = this._prettify(source); await fs.writeFile(path.join(outputDir, `${obj.name}.js`), source); } for (const obj of [...callbackInterfaces.values(), ...callbackFunctions.values(), ...dictionaries.values()]) { let source = obj.toString(); source = ` "use strict"; const conversions = require("webidl-conversions"); const utils = require("${relativeUtils}"); ${source} `; source = this._prettify(source); await fs.writeFile(path.join(outputDir, `${obj.name}.js`), source); } for (const obj of enumerations.values()) { const source = this._prettify(` "use strict"; ${obj.toString()} `); await fs.writeFile(path.join(outputDir, `${obj.name}.js`), source); } } _prettify(source) { return prettier.format(source, { printWidth: 120, trailingComma: "none", arrowParens: "avoid", parser: "babel" }); } async generate(outputDir) { if (!this.utilPath) { this.utilPath = path.join(outputDir, "utils.js"); } const sources = await this._collectSources(); const contents = await this._readFiles(sources); this._parse(outputDir, contents); await this._writeFiles(outputDir); } } module.exports = Transformer;