Created
March 22, 2018 14:06
-
-
Save haxiomic/7ac226c86dcd190e2e7bd82e8b794cf7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Incomplete parser for closure compiler externs | |
Converts closure externs to haxe classes and modules | |
Tested using https://github.com/nodebox/opentype.js/tree/master/externs | |
https://github.com/google/closure-compiler/wiki | |
*/ | |
class ClosureExternConverter { | |
static public function convert(externContent: String) { | |
// module = map of classes | |
var modules = new Map<String, Map<String, TypeDefinition>>(); | |
function getModule(path: Array<String>) { | |
var pathStr = path.join('.'); | |
var module = modules.get(pathStr); | |
if (module == null) { | |
throw 'Module "${pathStr}" has not been defined'; | |
} | |
return module; | |
} | |
// everything between /** */ | |
var e = EReg.escape; | |
var docPattern = new EReg('${ e('/**') }((.|\n)+?)(?=${ e('*/') })${ e('*/\n') }', 'm'); | |
var modulePattern = ~/^\s*(var|let|const)\s+(\w+)/; | |
var moduleFieldPattern = ~/^\s*([\w.]*)\b([a-z_]\w+)(\s*=\s*([^\n]*))?/; | |
var classPattern = ~/^\s*([\w.]*)\b([A-Z_]\w+)\s*=\s*([^\n]*)/; | |
var classFieldPattern = ~/^\s*([\w.]*)\b([A-Z_]\w+)\.prototype\.(\w+)(\s*=\s*([^\n]*))?/; | |
var constructorMetaPattern = ~/^@constructor\b/m; | |
var extendsMetaPattern = ~/^@extends\s+{?([^}\n]*)}?/m; | |
var str = externContent; | |
while (docPattern.match(str)) { | |
var doc = cleanDoc(docPattern.matched(1)); | |
str = docPattern.matchedRight(); | |
var nextLineEnd = str.indexOf('\n'); | |
var nextLine = str.substring(0, nextLineEnd); | |
// skip line | |
str = str.substr(nextLineEnd); | |
if (modulePattern.match(nextLine)) { | |
var modulePath = parseModulePath(modulePattern.matched(2)); | |
var moduleName = modulePath[modulePath.length - 1]; | |
// trace('Found module', modulePattern.matched(2), modulePath.join('.')); | |
modules.set(modulePath.join('.'), new Map()); | |
// create a module class for any static methods | |
var className = toClassName(moduleName); | |
var classPath = modulePath.concat([className]).join('.'); | |
var classDef = macro class $className {}; | |
classDef.meta = [{name: ':jsRequire', params: [{expr: EConst(CString(moduleName)), pos: null}], pos: null}]; | |
classDef.isExtern = true; | |
classDef.doc = doc; | |
classDef.pack = modulePath; | |
getModule(modulePath).set(classPath, classDef); | |
} else if (classPattern.match(nextLine)) { | |
var modulePath = parseModulePath(classPattern.matched(1)); | |
var className = classPattern.matched(2); | |
var expression = classPattern.matched(3); | |
var classDef = macro class $className {}; | |
if (constructorMetaPattern.match(doc)) { | |
var ctorDef = parseFunction(expression, doc); | |
ctorDef.name = 'new'; | |
classDef.fields.push(ctorDef); | |
} | |
if (extendsMetaPattern.match(doc)) { | |
var superPath = extendsMetaPattern.matched(1); | |
var parts = superPath.split('.'); | |
var superClass: TypePath = { | |
pack: parts.slice(0, parts.length - 1), | |
name: parts[parts.length - 1] | |
} | |
classDef.kind = TDClass(superClass); | |
} | |
classDef.isExtern = true; | |
classDef.doc = doc; | |
classDef.pack = modulePath; | |
var classPath = modulePath.concat([className]).join('.'); | |
getModule(modulePath).set(classPath, classDef); | |
} else if (classFieldPattern.match(nextLine)) { | |
var modulePath = parseModulePath(classFieldPattern.matched(1)); | |
var className = classFieldPattern.matched(2); | |
var fieldName = classFieldPattern.matched(3); | |
var expression = classFieldPattern.matched(5); | |
var classPath = modulePath.concat([className]).join('.'); | |
// trace('field', classPath, fieldName); | |
var classDef = getModule(modulePath).get(classPath); | |
if (classDef == null) throw 'Class $classPath not defined'; | |
// parse field | |
var fieldDef = parseField(fieldName, expression, doc); | |
classDef.fields.push(fieldDef); | |
} else if (moduleFieldPattern.match(nextLine)) { | |
var modulePath = parseModulePath(moduleFieldPattern.matched(1)); | |
var className = toClassName(modulePath[modulePath.length - 1]); | |
var classPath = modulePath.concat([className]).join('.'); | |
var fieldName = moduleFieldPattern.matched(2); | |
var expression = moduleFieldPattern.matched(3); | |
// get module class | |
var classDef = getModule(modulePath).get(classPath); | |
var fieldDef = parseField(fieldName, expression, doc); | |
fieldDef.access = [AStatic, APublic]; | |
classDef.fields.push(fieldDef); | |
} else if (StringTools.trim(nextLine) != '') { | |
trace('Unknown line format "$nextLine"'); | |
} | |
} | |
return modules; | |
} | |
static function parseType(typeStr: String): ComplexType { | |
typeStr = StringTools.trim(typeStr); | |
var builtIn = switch typeStr.toLowerCase() { | |
case 'string': macro :String; | |
case 'number': macro :Float; | |
case 'boolean': macro :Bool; | |
case 'array': macro :Array<Any>; | |
case 'object': macro :haxe.DynamicAccess<Any>; | |
case 'function': macro :Any; | |
// convert js type names into haxe type names | |
// this list could be fully completed by iterating all items in js and finding their @:native metadata | |
case 'canvasrenderingcontext2d': macro :js.html.CanvasRenderingContext2D; | |
case 'arraybuffer': macro :js.html.ArrayBuffer; | |
case 'svgpathelement': macro :js.html.svg.PathElement; | |
default: null; | |
} | |
if (builtIn != null) return builtIn; | |
var arrayPattern = ~/^(\[(.*)\]|(.*)\[\]|Array<(.*)>)$/; | |
if (arrayPattern.match(typeStr)) { | |
var innerTypeStr = | |
arrayPattern.matched(2) != null ? arrayPattern.matched(2) : | |
(arrayPattern.matched(3) != null ? arrayPattern.matched(3) : arrayPattern.matched(4)); | |
var innerType = parseType(innerTypeStr); | |
return macro :Array<$innerType>; | |
} | |
if (!~/^[\w.]+$/.match(typeStr)) { | |
throw 'Unhandled type syntax: "$typeStr"'; | |
} | |
return TPath({pack: [], name: typeStr}); | |
} | |
static function parseFunction(functionDeclExpression: String, doc: String): Field { | |
var functionDecl = ~/function\s*(\w+)?\s*\(([^)]*)\)/m; | |
var paramMetaPattern = ~/^@param\s+{([^}]*)}(\s+\[?(\w+))?/mg; | |
var returnMetaPattern = ~/^@return\s+{([^}]*)}/m; | |
if (!functionDecl.match(functionDeclExpression)) { | |
throw 'Unhandled function declaration'; | |
} | |
var funcName = functionDecl.matched(1); | |
var argNames = functionDecl.matched(2) | |
.split(',').map(s -> StringTools.trim(s)) | |
.filter(s -> s != ''); | |
var argTypes = new Map<String, {t: ComplexType, opt: Bool}>(); | |
var returnType = macro :Void; | |
// match function hints | |
paramMetaPattern.map(doc, s -> { | |
var typeStr = paramMetaPattern.matched(1).trim(); | |
var optional = false; | |
if (typeStr.charAt(typeStr.length - 1) == '=') { | |
typeStr = typeStr.substr(0, typeStr.length - 1); | |
optional = true; | |
} | |
var type = parseType(typeStr); | |
var name = paramMetaPattern.matched(3); | |
if (name == null) name = argNames[0]; | |
argTypes.set(name, {t: type, opt: optional}); | |
return s.matched(0); | |
}); | |
if (returnMetaPattern.match(doc)) { | |
returnType = parseType(returnMetaPattern.matched(1)); | |
} | |
return { | |
name: funcName, | |
kind: FFun({ | |
args: argNames.map(name -> { | |
name: name, | |
type: argTypes.exists(name) ? argTypes.get(name).t : macro :Any, | |
meta: null, | |
opt: argTypes.exists(name) ? argTypes.get(name).opt : false, | |
value: null, | |
}), | |
expr: null, | |
ret: returnType, | |
}), | |
pos: null | |
} | |
} | |
static function parseField(fieldName: String, expression: String, doc: String) { | |
var typeMetaPattern = ~/^@type\s+{([^}]*)}/m; | |
// default to var $fieldName:Any | |
var fieldDef = (macro class X { | |
var $fieldName: Any; | |
}).fields[0]; | |
if (typeMetaPattern.match(doc)) { | |
var type = parseType(typeMetaPattern.matched(1)); | |
fieldDef = (macro class X { | |
var $fieldName: $type; | |
}).fields[0]; | |
} else { | |
fieldDef = parseFunction(expression, doc); | |
fieldDef.name = fieldName; | |
} | |
fieldDef.doc = doc; | |
return fieldDef; | |
} | |
static function parseModulePath(str: String) { | |
return str.split('.').filter(s -> s != ''); | |
} | |
static function toClassName(str: String) { | |
return str.charAt(0).toUpperCase() + str.substr(1); | |
} | |
static function cleanDoc(doc: String) { | |
return doc.split('\n') | |
.map(l -> StringTools.trim(l)) | |
.map(l -> l.charAt(0) == '*' ? StringTools.trim(l.substr(1)) : l) | |
.join('\n'); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment