package cz.habarta.typescript.generator.emitter; import cz.habarta.typescript.generator.*; import cz.habarta.typescript.generator.compiler.EnumKind; import cz.habarta.typescript.generator.compiler.EnumMemberModel; import cz.habarta.typescript.generator.util.Utils; import java.io.*; import java.text.*; import java.util.*; public class Emitter implements EmitterExtension.Writer { private final Settings settings; private Writer writer; private boolean forceExportKeyword; private int indent; public Emitter(Settings settings) { this.settings = settings; } public void emit(TsModel model, Writer output, String outputName, boolean closeOutput, boolean forceExportKeyword, int initialIndentationLevel) { this.writer = output; this.forceExportKeyword = forceExportKeyword; this.indent = initialIndentationLevel; if (outputName != null) { System.out.println("Writing declarations to: " + outputName); } emitFileComment(); emitReferences(); emitImports(); emitModule(model); emitUmdNamespace(); if (closeOutput) { close(); } } private void emitFileComment() { if (!settings.noFileComment) { final String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); writeIndentedLine("// Generated using typescript-generator version " + TypeScriptGenerator.Version + " on " + timestamp + "."); } } private void emitReferences() { if (settings.referencedFiles != null && !settings.referencedFiles.isEmpty()) { writeNewLine(); for (String reference : settings.referencedFiles) { writeIndentedLine("/// <reference path=" + quote(reference, settings) + " />"); } } } private void emitImports() { if (settings.importDeclarations != null && !settings.importDeclarations.isEmpty()) { writeNewLine(); for (String importDeclaration : settings.importDeclarations) { writeIndentedLine(importDeclaration + ";"); } } } private void emitModule(TsModel model) { if (settings.outputKind == TypeScriptOutputKind.ambientModule) { writeNewLine(); writeIndentedLine("declare module " + quote(settings.module, settings) + " {"); indent++; emitNamespace(model); indent--; writeNewLine(); writeIndentedLine("}"); } else { emitNamespace(model); } } private void emitNamespace(TsModel model) { if (settings.namespace != null) { writeNewLine(); String prefix = ""; if (settings.outputFileType == TypeScriptFileType.declarationFile && settings.outputKind == TypeScriptOutputKind.global) { prefix = "declare "; } if (settings.outputKind == TypeScriptOutputKind.module) { prefix = "export "; } writeIndentedLine(prefix + "namespace " + settings.namespace + " {"); indent++; final boolean exportElements = settings.outputFileType == TypeScriptFileType.implementationFile; emitElements(model, exportElements, false); indent--; writeNewLine(); writeIndentedLine("}"); } else { final boolean exportElements = settings.outputKind == TypeScriptOutputKind.module; final boolean declareElements = settings.outputFileType == TypeScriptFileType.declarationFile && settings.outputKind == TypeScriptOutputKind.global; emitElements(model, exportElements, declareElements); } } private void emitElements(TsModel model, boolean exportKeyword, boolean declareKeyword) { exportKeyword = exportKeyword || forceExportKeyword; emitBeans(model, exportKeyword, declareKeyword); emitTypeAliases(model, exportKeyword, declareKeyword); emitNumberEnums(model, exportKeyword, declareKeyword); emitHelpers(model); for (EmitterExtension emitterExtension : settings.extensions) { writeNewLine(); writeNewLine(); writeIndentedLine(String.format("// Added by '%s' extension", emitterExtension.getClass().getSimpleName())); emitterExtension.emitElements(this, settings, exportKeyword, model); } } private void emitBeans(TsModel model, boolean exportKeyword, boolean declareKeyword) { for (TsBeanModel bean : model.getBeans()) { emitFullyQualifiedDeclaration(bean, exportKeyword, declareKeyword); } } private void emitTypeAliases(TsModel model, boolean exportKeyword, boolean declareKeyword) { for (TsAliasModel alias : model.getTypeAliases()) { emitFullyQualifiedDeclaration(alias, exportKeyword, declareKeyword); } } private void emitNumberEnums(TsModel model, boolean exportKeyword, boolean declareKeyword) { final ArrayList<TsEnumModel<?>> enums = settings.mapEnum == EnumMapping.asNumberBasedEnum && !settings.areDefaultStringEnumsOverriddenByExtension() ? new ArrayList<>(model.getEnums()) : new ArrayList<TsEnumModel<?>>(model.getEnums(EnumKind.NumberBased)); for (TsEnumModel<?> enumModel : enums) { emitFullyQualifiedDeclaration(enumModel, exportKeyword, declareKeyword); } } private void emitFullyQualifiedDeclaration(TsDeclarationModel declaration, boolean exportKeyword, boolean declareKeyword) { if (declaration.getName().getNamespace() != null) { writeNewLine(); final String prefix = declareKeyword ? "declare " : ""; writeIndentedLine(exportKeyword, prefix + "namespace " + declaration.getName().getNamespace() + " {"); indent++; emitDeclaration(declaration, true, false); indent--; writeNewLine(); writeIndentedLine("}"); } else { emitDeclaration(declaration, exportKeyword, declareKeyword); } } private void emitDeclaration(TsDeclarationModel declaration, boolean exportKeyword, boolean declareKeyword) { if (declaration instanceof TsBeanModel) { emitBean((TsBeanModel) declaration, exportKeyword); } else if (declaration instanceof TsAliasModel) { emitTypeAlias((TsAliasModel) declaration, exportKeyword); } else if (declaration instanceof TsEnumModel) { emitNumberEnum((TsEnumModel) declaration, exportKeyword, declareKeyword); } else { throw new RuntimeException("Unknown declaration type: " + declaration.getClass().getName()); } } private void emitBean(TsBeanModel bean, boolean exportKeyword) { writeNewLine(); emitComments(bean.getComments()); final String declarationType = bean.isClass() ? "class" : "interface"; final String typeParameters = bean.getTypeParameters().isEmpty() ? "" : "<" + Utils.join(bean.getTypeParameters(), ", ")+ ">"; final List<TsType> extendsList = bean.getExtendsList(); final List<TsType> implementsList = bean.getImplementsList(); final String extendsClause = extendsList.isEmpty() ? "" : " extends " + Utils.join(extendsList, ", "); final String implementsClause = implementsList.isEmpty() ? "" : " implements " + Utils.join(implementsList, ", "); writeIndentedLine(exportKeyword, declarationType + " " + bean.getName().getSimpleName() + typeParameters + extendsClause + implementsClause + " {"); indent++; for (TsPropertyModel property : bean.getProperties()) { emitProperty(property); } if (bean.getConstructor() != null) { emitCallable(bean.getConstructor()); } for (TsMethodModel method : bean.getMethods()) { emitCallable(method); } indent--; writeIndentedLine("}"); } private void emitProperty(TsPropertyModel property) { emitComments(property.getComments()); final TsType tsType = property.getTsType(); final String readonly = property.readonly ? "readonly " : ""; final String questionMark = settings.declarePropertiesAsOptional || (tsType instanceof TsType.OptionalType) ? "?" : ""; writeIndentedLine(readonly + quoteIfNeeded(property.getName(), settings) + questionMark + ": " + tsType.format(settings) + ";"); } public static String quoteIfNeeded(String name, Settings settings) { return isValidIdentifierName(name) ? name : quote(name, settings); } public static String quote(String value, Settings settings) { return settings.quotes + value + settings.quotes; } // https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#2.2.2 // http://www.ecma-international.org/ecma-262/6.0/index.html#sec-names-and-keywords public static boolean isValidIdentifierName(String name) { if (name == null || name.isEmpty()) { return false; } final char start = name.charAt(0); if (!Character.isUnicodeIdentifierStart(start) && start != '$' && start != '_') { return false; } for (char c : name.substring(1).toCharArray()) { if (!Character.isUnicodeIdentifierPart(c) && c != '$' && c != '_' && c != '\u200C' && c != '\u200D') { return false; } } return true; } private void emitCallable(TsCallableModel method) { writeNewLine(); emitComments(method.getComments()); final List<String> parameters = new ArrayList<>(); for (TsParameterModel parameter : method.getParameters()) { final String access = parameter.getAccessibilityModifier() != null ? parameter.getAccessibilityModifier().format() + " " : ""; final String questionMark = (parameter.getTsType() instanceof TsType.OptionalType) ? "?" : ""; final String type = parameter.getTsType() != null ? ": " + parameter.getTsType() : ""; parameters.add(access + parameter.getName() + questionMark + type); } final String type = method.getReturnType() != null ? ": " + method.getReturnType() : ""; final String signature = method.getName() + "(" + Utils.join(parameters, ", ") + ")" + type; if (method.getBody() != null) { writeIndentedLine(signature + " {"); indent++; emitStatements(method.getBody()); indent--; writeIndentedLine("}"); } else { writeIndentedLine(signature + ";"); } } private void emitStatements(List<TsStatement> statements) { for (TsStatement statement : statements) { if (statement instanceof TsReturnStatement) { final TsReturnStatement returnStatement = (TsReturnStatement) statement; if (returnStatement.getExpression() != null) { writeIndentedLine("return " + returnStatement.getExpression().format(settings) + ";"); } else { writeIndentedLine("return;"); } } } } private void emitTypeAlias(TsAliasModel alias, boolean exportKeyword) { writeNewLine(); emitComments(alias.getComments()); final String genericParameters = alias.getTypeParameters().isEmpty() ? "" : "<" + Utils.join(alias.getTypeParameters(), ", ") + ">"; writeIndentedLine(exportKeyword, "type " + alias.getName().getSimpleName() + genericParameters + " = " + alias.getDefinition().format(settings) + ";"); } private void emitNumberEnum(TsEnumModel<?> enumModel, boolean exportKeyword, boolean declareKeyword) { writeNewLine(); emitComments(enumModel.getComments()); writeIndentedLine(exportKeyword, (declareKeyword ? "declare " : "") + "const enum " + enumModel.getName().getSimpleName() + " {"); indent++; for (EnumMemberModel<?> member : enumModel.getMembers()) { emitComments(member.getComments()); final String initializer = enumModel.getKind() == EnumKind.NumberBased ? " = " + member.getEnumValue() : ""; writeIndentedLine(member.getPropertyName() + initializer + ","); } indent--; writeIndentedLine("}"); } private void emitHelpers(TsModel model) { for (TsHelper helper : model.getHelpers()) { writeNewLine(); writeTemplate(this, settings, helper.getLines(), null); } } private void emitUmdNamespace() { if (settings.umdNamespace != null) { writeNewLine(); writeIndentedLine("export as namespace " + settings.umdNamespace + ";"); } } private void emitComments(List<String> comments) { if (comments != null) { writeIndentedLine("/**"); for (String comment : comments) { writeIndentedLine(" * " + comment); } writeIndentedLine(" */"); } } public static void writeTemplate(EmitterExtension.Writer writer, Settings settings, List<String> template, Map<String, String> replacements) { for (String line : template) { if (replacements != null) { for (Map.Entry<String, String> entry : replacements.entrySet()) { line = line.replace(entry.getKey(), entry.getValue()); } } writer.writeIndentedLine(line .replace("\t", settings.indentString) .replace("\"", settings.quotes) ); } } private void writeIndentedLine(boolean exportKeyword, String line) { writeIndentedLine((exportKeyword ? "export " : "") + line); } @Override public void writeIndentedLine(String line) { try { if (!line.isEmpty()) { for (int i = 0; i < indent; i++) { writer.write(settings.indentString); } } writer.write(line); writeNewLine(); } catch (IOException e) { throw new RuntimeException(e); } } private void writeNewLine() { try { writer.write(settings.newline); writer.flush(); } catch (IOException e) { throw new RuntimeException(e); } } private void close() { try { writer.close(); } catch (IOException e) { throw new RuntimeException(e); } } }