├── README └── src ├── AST.hx ├── Printer.hx ├── Main.hx └── Parser.hx /README: -------------------------------------------------------------------------------- 1 | Converts JSDoc annotated JavaScript files to Haxe externs. This is not a general purpose converter. It was built to convert the Phaser library specifically, but it may be general enough to convert other libraries as well. Really the only requirement is that every JS file corresponds to a class and the folder structure maps directly to package structure. Built for Object Oriented structuring. 2 | 3 | Requires DOX: https://github.com/visionmedia/dox 4 | 5 | Compile and run: 6 | 7 | neko js2hx INDIR OUTDIR 8 | 9 | Example: 10 | 11 | neko js2hx path/to/phaser path/to/out -------------------------------------------------------------------------------- /src/AST.hx: -------------------------------------------------------------------------------- 1 | package ; 2 | 3 | /** 4 | * AST definitions. 5 | * 6 | * @author Sam MacPherson 7 | */ 8 | 9 | typedef DClass = { 10 | name:String, 11 | pkg:String, 12 | native:String, 13 | ext:Null, 14 | fields:Array 15 | } 16 | 17 | typedef DField = { 18 | name:String, 19 | stat:Bool, 20 | kind:DFieldKind, 21 | doc:Null 22 | } 23 | 24 | enum DFieldKind { 25 | FFun(params:Array<{ opt:Bool, name:String, type:Array, ?value:String, ?varArg:Bool }>, ret:String); 26 | FVar(type:String, ?get:String, ?set:String); 27 | } 28 | 29 | class DClassTools { 30 | 31 | public static function getFullName (cls:DClass):String { 32 | if (cls.pkg == "") return cls.name; 33 | else return '${cls.pkg}.${cls.name}'; 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /src/Printer.hx: -------------------------------------------------------------------------------- 1 | package ; 2 | 3 | import AST; 4 | import sys.io.File; 5 | import sys.io.FileOutput; 6 | 7 | using StringTools; 8 | 9 | /** 10 | * Print Haxe file from AST. 11 | * 12 | * @author Sam MacPherson 13 | */ 14 | class Printer { 15 | 16 | var output:FileOutput; 17 | var funcs:Array>; 18 | 19 | public function new (outFile:String) { 20 | output = File.write(outFile, false); 21 | } 22 | 23 | function w (ln:String, ?t:Int = 0):Void { 24 | output.writeString(StringTools.lpad("", "\t", t) + ln + "\n"); 25 | } 26 | 27 | function expandFunc (params:Array<{ opt:Bool, name:String, type:Array, ?value:String, ?varArg:Bool }>, ?currParam:Int = 0):Void { 28 | if (currParam == 0) funcs.push(new Array()); 29 | 30 | if (currParam == params.length) return; 31 | 32 | var param = params[currParam]; 33 | 34 | var len = funcs.length; 35 | for (i in 0 ... (param.type.length - 1)) { 36 | for (o in 0 ... len) { 37 | var arr = new Array(); 38 | for (p in 0 ... funcs[o].length) { 39 | arr.push(funcs[o][p]); 40 | } 41 | funcs.push(arr); 42 | } 43 | } 44 | for (i in 0 ... param.type.length) { 45 | for (o in 0 ... len) { 46 | funcs[i * len + o].push(param.type[i]); 47 | } 48 | } 49 | expandFunc(params, currParam + 1); 50 | } 51 | 52 | public function write (cls:DClass):Void { 53 | w('package ${cls.pkg};'); 54 | w(''); 55 | w('@:native("${cls.native}")'); 56 | var clsDef = 'extern class ${cls.name}'; 57 | if (cls.ext != null) clsDef += ' extends ${cls.ext}'; 58 | w('$clsDef {'); 59 | for (i in cls.fields) { 60 | w('', 1); 61 | if (i.doc != null) { 62 | var doc = i.doc.replace("\n", "\n\t * "); 63 | w('/**', 1); 64 | w(' * $doc', 1); 65 | w(' */', 1); 66 | } 67 | switch (i.kind) { 68 | case FFun(params, ret): 69 | funcs = new Array>(); 70 | expandFunc(params); 71 | for (f in 0 ... funcs.length) { 72 | var lastIndex = f + 1 == funcs.length; 73 | var str = ''; 74 | if (lastIndex) { 75 | if (i.stat) str += 'static '; 76 | str += 'function ${i.name} ('; 77 | } 78 | else str += '@:overload(function ('; 79 | var first = true; 80 | for (o in 0 ... funcs[f].length) { 81 | var p = params[o]; 82 | if (p.varArg == true && o == funcs[f].length - 1) { 83 | for (va in 0 ... 5) { 84 | if (!first) str += ', '; 85 | str += '?${p.name}${va}:${funcs[f][o]}'; 86 | first = false; 87 | } 88 | } else { 89 | if (!first) str += ', '; 90 | if (p.opt) str += '?'; 91 | str += '${p.name}:${funcs[f][o]}'; 92 | if (p.value != null) str += ' = ${p.value}'; 93 | } 94 | first = false; 95 | } 96 | if (i.name != "new" || !lastIndex) str += '):${ret}'; 97 | else str += ')'; 98 | if (lastIndex) str += ';'; 99 | else str += ' {})'; 100 | w(str, 1); 101 | } 102 | case FVar(t, g, s): 103 | var str = ''; 104 | if (i.stat) str += 'static '; 105 | str += 'var ${i.name}'; 106 | if (g != null || s != null) { 107 | if (g == null) g = 'default'; 108 | if (s == null) g = 'default'; 109 | 110 | str += '($g, $s)'; 111 | } 112 | str += ':$t;'; 113 | w(str, 1); 114 | } 115 | } 116 | w('', 1); 117 | w('}'); 118 | 119 | output.close(); 120 | } 121 | 122 | } -------------------------------------------------------------------------------- /src/Main.hx: -------------------------------------------------------------------------------- 1 | package ; 2 | 3 | import AST; 4 | import neko.Lib; 5 | import sys.FileSystem; 6 | 7 | class Main { 8 | 9 | var classes:Array; 10 | var nativeTypes:Map; 11 | var hxTypes:Map; 12 | var filesParsed:Int; 13 | 14 | public function new (inDir:String, outDir:String) { 15 | classes = new Array(); 16 | nativeTypes = new Map(); 17 | hxTypes = new Map(); 18 | 19 | Lib.println("Parsing AST..."); 20 | filesParsed = 0; 21 | parseDirectory(inDir); 22 | Lib.println("Resolving types..."); 23 | resolveTypes(); 24 | Lib.println("Checking for redefinitions in sub-classes..."); 25 | removeSubClassRedefinitions(); 26 | Lib.println("Writing output..."); 27 | FileSystem.createDirectory(outDir); 28 | for (i in classes) { 29 | writeClass(outDir, i); 30 | } 31 | Lib.println("Done!"); 32 | } 33 | 34 | function parseDirectory (dir:String):Void { 35 | for (i in FileSystem.readDirectory(dir)) { 36 | var name = '$dir/$i'; 37 | if (FileSystem.exists(name)) { 38 | if (FileSystem.isDirectory(name)) { 39 | parseDirectory(name); 40 | } else { 41 | var ext = name.substr(name.lastIndexOf(".")); 42 | if (ext == ".js") { 43 | var cls = new Parser(name.substr(0, name.lastIndexOf("."))).run(); 44 | if (cls != null) { 45 | classes.push(cls); 46 | nativeTypes.set(cls.native, cls); 47 | hxTypes.set(DClassTools.getFullName(cls), cls); 48 | 49 | filesParsed++; 50 | Lib.println('Parsed $filesParsed files'); 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | function resolveTypes ():Void { 59 | for (i in classes) { 60 | if (i.ext != null) { 61 | var origExt = i.ext; 62 | i.ext = resolveType(i.ext); 63 | if (i.ext == "Dynamic") { 64 | //Could not find -- try prefixing native namespace 65 | i.ext = resolveType(i.native.substr(0, i.native.lastIndexOf(".")) + "." + origExt); 66 | if (i.ext == "Dynamic") { 67 | //If still not found then set to null 68 | Lib.println('Warning: Could not resolve extends type: $origExt in ${DClassTools.getFullName(i)}'); 69 | i.ext = null; 70 | } 71 | } 72 | } 73 | 74 | for (o in i.fields) { 75 | switch (o.kind) { 76 | case FFun(params, ret): 77 | for (p in params) { 78 | for (t in 0 ... p.type.length) { 79 | p.type[t] = resolveType(p.type[t]); 80 | } 81 | } 82 | o.kind = FFun(params, resolveType(ret)); 83 | case FVar(t, g, s): 84 | o.kind = FVar(resolveType(t), g, s); 85 | } 86 | } 87 | } 88 | } 89 | 90 | function removeSubClassRedefinitions ():Void { 91 | for (i in classes) { 92 | if (i.ext != null) { 93 | var sup = hxTypes.get(i.ext); 94 | if (sup != null) { 95 | var index = 0; 96 | while (index < i.fields.length) { 97 | if (hasField(sup, i.fields[index].name)) { 98 | i.fields.splice(index, 1); 99 | } else { 100 | index++; 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | function hasField (cls:DClass, name:String):Bool { 109 | for (i in cls.fields) { 110 | if (i.name == name) return true; 111 | } 112 | 113 | if (cls.ext != null) { 114 | var sup = hxTypes.get(cls.ext); 115 | if (sup != null) { 116 | return hasField(sup, name); 117 | } 118 | } 119 | 120 | return false; 121 | } 122 | 123 | function resolveType (t:String):String { 124 | if (t == "Void" || t == "Float" || t == "Int" || t == "String" || t == "Dynamic" || t == "Bool" || t == "Array") return t; 125 | 126 | var cls = nativeTypes.get(t); 127 | if (cls != null) { 128 | return DClassTools.getFullName(cls); 129 | } else { 130 | if (Type.resolveClass(t) != null) { 131 | return t; 132 | } else { 133 | return "Dynamic"; 134 | } 135 | } 136 | } 137 | 138 | function writeClass (dir:String, cls:DClass):Void { 139 | var path = dir; 140 | for (i in cls.pkg.split(".")) { 141 | path += '/$i'; 142 | FileSystem.createDirectory(path); 143 | } 144 | 145 | new Printer('$path/${cls.name}.hx').write(cls); 146 | } 147 | 148 | static function main () { 149 | new Main(Sys.args()[0], Sys.args()[1]); 150 | } 151 | 152 | } -------------------------------------------------------------------------------- /src/Parser.hx: -------------------------------------------------------------------------------- 1 | package ; 2 | 3 | import AST; 4 | import haxe.io.Eof; 5 | import haxe.Json; 6 | import sys.FileSystem; 7 | import sys.io.File; 8 | import sys.io.FileOutput; 9 | import sys.io.Process; 10 | 11 | using Lambda; 12 | using StringTools; 13 | 14 | /** 15 | * Parses a JavaScript file in AST. 16 | * 17 | * @author Sam MacPherson 18 | */ 19 | class Parser { 20 | 21 | static var RESERVED = ["override", "private", "public", "dynamic", "inline", "static"]; 22 | 23 | var file:String; 24 | 25 | public function new (file:String) { 26 | this.file = file; 27 | } 28 | 29 | public function run ():Null { 30 | var fields = new Array(); 31 | var native = null; 32 | var ext = null; 33 | for (i in cast(jsToJson(file), Array)) { 34 | var field = parseField(i); 35 | if (field != null && !fields.exists(function (e) { return e.name == field.name; } )) { 36 | fields.push(field); 37 | } 38 | 39 | for (o in cast(i.tags, Array)) { 40 | var isClass = false; 41 | switch (o.type) { 42 | case "class": 43 | isClass = true; 44 | case "extends": 45 | ext = o.string; 46 | default: 47 | } 48 | if (!isClass && i.description != null) { 49 | isClass = cast(i.description.full, String).indexOf("@class") != -1; 50 | } 51 | if (isClass) { 52 | if (i.ctx != null && i.ctx.type == "method" && i.ctx.receiver != null && i.ctx.name != null) { 53 | //Preferentially use context for native location 54 | native = i.ctx.receiver + "." + i.ctx.name; 55 | } else { 56 | native = o.string; 57 | } 58 | } 59 | } 60 | if (native == null && i.ctx != null) { 61 | switch (i.ctx.type) { 62 | case "declaration": 63 | native = i.ctx.name; 64 | break; 65 | default: 66 | } 67 | } 68 | if (native == null && i.description != null) { 69 | var fullStr:String = i.description.full; 70 | var start = fullStr.indexOf("@class"); 71 | if (start != -1) { 72 | fullStr = fullStr.substr(start + "@class".length).trim(); 73 | var regex = ~/^([A-Za-z0-9\.]+)/; 74 | regex.match(fullStr); 75 | native = regex.matched(1); 76 | } 77 | } 78 | } 79 | if (native == null) return null; 80 | var pkg = file.split("/"); 81 | return { name:pkg[pkg.length - 1], pkg:pkg.length > 1 ? pkg.slice(0, pkg.length - 1).join(".") : "", native:native, ext:ext, fields:fields }; 82 | } 83 | 84 | function parseField (field:Dynamic):Null { 85 | var name = null; 86 | var fun = false; 87 | var params = new Array(); 88 | var ret = "Void"; 89 | var type = "Dynamic"; 90 | var argIndex = 0; 91 | var stat = false; 92 | var cls = false; 93 | var get = null; 94 | var set = null; 95 | var doc = null; 96 | var forceOpt = false; 97 | for (i in cast(field.tags, Array)) { 98 | switch (i.type) { 99 | case "constructor": 100 | name = "new"; 101 | fun = true; 102 | case "param": 103 | var param:Dynamic = parseName(i.name, 'a${argIndex++}', forceOpt); 104 | var types = new Array(); 105 | for (o in cast(i.types, Array)) { 106 | var type = cast(o, String); 107 | if (type.indexOf("...") == 0) { 108 | param.varArg = true; 109 | type = type.substr(3); 110 | } 111 | types.push(getHaxeType(type)); 112 | } 113 | param.type = types; 114 | params.push(param); 115 | 116 | if (param.opt) forceOpt = true; 117 | case "returns", "return": 118 | if (i.types != null) { 119 | if (i.types.length == 1) ret = getHaxeType(i.types[0]); 120 | else ret = "Dynamic"; 121 | } else { 122 | var str:String = i.string; 123 | ret = getHaxeType(str.substr(str.indexOf("{") + 1, str.indexOf("}") - str.indexOf("{") - 1)); 124 | } 125 | case "method": 126 | var str:String = i.string; 127 | var hashIndex = str.lastIndexOf("#"); 128 | if (hashIndex == -1) hashIndex = str.lastIndexOf("."); 129 | name = hashIndex != -1 ? str.substr(hashIndex + 1) : (i.name != null ? i.name.substr(i.name.lastIndexOf(".") + 1) : null); 130 | fun = true; 131 | case "static": 132 | stat = true; 133 | case "class": 134 | cls = true; 135 | case "property": 136 | var str:String = i.string; 137 | var data = str.split(" "); 138 | if (data[0].charAt(0) == "{") { 139 | type = getHaxeType(data[0].substr(1, data[0].length - 2)); 140 | name = parseName(data[1], 'a${argIndex++}').name; 141 | } else { 142 | name = parseName(data[0], 'a${argIndex++}').name; 143 | } 144 | case "readonly": 145 | set = "null"; 146 | case "type": 147 | if (i.types.length == 1) type = getHaxeType(i.types[0]); 148 | else type = "Dynamic"; 149 | default: 150 | } 151 | } 152 | if (cls && !fun) return null; 153 | 154 | if (field.ctx != null && name == null) { 155 | if (field.ctx.type == "property") { 156 | name = field.ctx.name; 157 | fun = false; 158 | 159 | if (field.description != null) { 160 | var str:String = field.description.full; 161 | if (str != null) { 162 | type = getHaxeType(str.substr(str.indexOf("{") + 1, str.indexOf("}") - str.indexOf("{") - 1)); 163 | } 164 | } 165 | } 166 | } 167 | 168 | if (field.description != null) { 169 | doc = parseDocs(field.description.full); 170 | 171 | if (name == null && cast(field.description.full, String).indexOf("@const") != -1) { 172 | var code = cast(field.code, String).split("="); 173 | var n = code[0].trim(); 174 | name = n.substr(n.lastIndexOf(".") + 1); 175 | stat = true; 176 | } 177 | } 178 | 179 | if (name != null && !isReserved(name)) { 180 | if (fun) { 181 | return { name:name, stat:stat, kind:FFun(params, ret), doc:doc }; 182 | } else { 183 | return { name:name, stat:stat, kind:FVar(type, get, set), doc:doc }; 184 | } 185 | } else { 186 | return null; 187 | } 188 | } 189 | 190 | function getHaxeType (type:String):String { 191 | return switch (type.toLowerCase()) { 192 | case "number": "Float"; 193 | case "integer": "Int"; 194 | case "string": "String"; 195 | case "object": "Dynamic"; 196 | case "boolean": "Bool"; 197 | case "array": "Array"; 198 | case "*": "Dynamic"; 199 | default: type; 200 | } 201 | } 202 | 203 | function parseName (name:String, alt:String, ?forceOpt:Bool = false): { name:String, opt:Bool, ?value:String } { 204 | var regex1 = ~/\[([A-Za-z][A-Za-z0-9]*)=([^\]]+)\]/; 205 | var regex2 = ~/\[([A-Za-z][A-Za-z0-9]*)\]/; 206 | var regex3 = ~/([A-Za-z][A-Za-z0-9]*)/; 207 | 208 | var n = null; 209 | var opt = false; 210 | var value = null; 211 | 212 | if (name != null) { 213 | if (regex1.match(name)) { 214 | n = regex1.matched(1); 215 | opt = true; 216 | value = regex1.matched(2); 217 | 218 | //Only store contants 219 | if ((value.charCodeAt(0) < '0'.code || value.charCodeAt(0) > '9'.code) && value.charAt(0) != "'" && value.charAt(0) != '"' && value != "true" && value != "false") { 220 | value = null; 221 | } 222 | } else if (regex2.match(name)) { 223 | n = regex2.matched(1); 224 | opt = true; 225 | } else if (regex3.match(name)) { 226 | n = regex3.matched(1); 227 | } else { 228 | n = alt; 229 | } 230 | } else { 231 | n = alt; 232 | } 233 | 234 | if (isReserved(n)) n = alt; 235 | 236 | if (forceOpt) opt = true; 237 | 238 | return { name:n, opt:opt, value:value }; 239 | } 240 | 241 | function parseDocs (str:String):String { 242 | str = str.replace("

", "") 243 | .replace("", "") 244 | .replace("", "") 245 | .replace("
", "\n") 246 | .replace("

", "\n\n") 247 | .replace("\n\n\n\n", "\n\n"); 248 | if (str.endsWith("\n\n")) str = str.substr(0, str.length - 2); 249 | 250 | var propIndex = str.indexOf("@property"); 251 | var dashIndex = str.indexOf("-"); 252 | 253 | if (propIndex != -1 && dashIndex != -1) { 254 | return str.substr(0, propIndex) + str.substr(dashIndex + 2); 255 | } else { 256 | return str; 257 | } 258 | } 259 | 260 | function jsToJson (file:String):Dynamic { 261 | Sys.command("dox", ["<", file + ".js", ">", file + ".json"]); 262 | var json = Json.parse(File.getContent(file + ".json")); 263 | FileSystem.deleteFile(file + ".json"); 264 | return json; 265 | } 266 | 267 | static function isReserved (name:String):Bool { 268 | return RESERVED.has(name); 269 | } 270 | 271 | } --------------------------------------------------------------------------------