diff --git a/build.sbt b/build.sbt index dde6c8210..03591fcf9 100644 --- a/build.sbt +++ b/build.sbt @@ -9,7 +9,7 @@ resolvers += Resolver.sonatypeRepo("public") val NAME = "kaitai-struct-compiler" val VERSION = "0.9-SNAPSHOT" -val TARGET_LANGS = "C++/STL, C#, Java, JavaScript, Lua, Perl, PHP, Python, Ruby" +val TARGET_LANGS = "C++/STL, C#, Java, JavaScript, TypeScript, Lua, Perl, PHP, Python, Ruby" val UTF8 = Charset.forName("UTF-8") lazy val root = project.in(file(".")). diff --git a/shared/src/main/scala/io/kaitai/struct/Main.scala b/shared/src/main/scala/io/kaitai/struct/Main.scala index e96412ce4..8540744ca 100644 --- a/shared/src/main/scala/io/kaitai/struct/Main.scala +++ b/shared/src/main/scala/io/kaitai/struct/Main.scala @@ -1,7 +1,7 @@ package io.kaitai.struct import io.kaitai.struct.format.{ClassSpec, ClassSpecs, GenericStructClassSpec} -import io.kaitai.struct.languages.{GoCompiler, RustCompiler, NimCompiler} +import io.kaitai.struct.languages._ import io.kaitai.struct.languages.components.LanguageCompilerStatic import io.kaitai.struct.precompile._ @@ -87,6 +87,8 @@ object Main { new NimClassCompiler(specs, spec, config) case HtmlClassCompiler => new HtmlClassCompiler(specs, spec) + case TypeScriptCompiler => + new TypeScriptClassCompiler(specs, spec, config) case _ => new ClassCompiler(specs, spec, config, lang) } diff --git a/shared/src/main/scala/io/kaitai/struct/TypeScriptClassCompiler.scala b/shared/src/main/scala/io/kaitai/struct/TypeScriptClassCompiler.scala new file mode 100644 index 000000000..c712fee27 --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/TypeScriptClassCompiler.scala @@ -0,0 +1,70 @@ +package io.kaitai.struct + +import io.kaitai.struct.datatype.DataType.{KaitaiStreamType, UserTypeInstream, CalcUserType} +import io.kaitai.struct.datatype.{Endianness, FixedEndian, InheritedEndian} +import io.kaitai.struct.format._ +import io.kaitai.struct.languages.TypeScriptCompiler +import io.kaitai.struct.languages.components.ExtraAttrs + +class TypeScriptClassCompiler( + classSpecs: ClassSpecs, + override val topClass: ClassSpec, + config: RuntimeConfig +) extends ClassCompiler(classSpecs, topClass, config, TypeScriptCompiler) { + + override def compileClass(curClass: ClassSpec): Unit = { + provider.nowClass = curClass + + // Forward declarations for recursive types + curClass.types.foreach { case (typeName, _) => lang.classForwardDeclaration(List(typeName)) } + + // documentation + compileClassDoc(curClass) + + // class thing {... + lang.classHeader(curClass.name) + + // static = Object.freeze({...}) + compileEnums(curClass) + + // static = class {... + compileSubclasses(curClass) + + provider.nowClass = curClass + + + // public : ; + val allAttrs: List[MemberSpec] = + curClass.seq ++ + curClass.params ++ + List( + AttrSpec(List(), RootIdentifier, CalcUserType(topClassName, None)), + AttrSpec(List(), ParentIdentifier, curClass.parentType) + ) ++ + ExtraAttrs.forClassSpec(curClass, lang) + compileAttrReaders(allAttrs) + + // constructor() {...} + compileConstructor(curClass) + + compileEagerRead(curClass.seq, curClass.meta.endian) + + // private : ...; + // get () {...} + compileInstances(curClass) + + // } + lang.classFooter(curClass.name) + } + + override def compileSeq(seq: List[AttrSpec], defEndian: Option[FixedEndian]) = { + var wasUnaligned = false + seq.foreach { (attr) => + val nowUnaligned = isUnalignedBits(attr.dataType) + if (wasUnaligned && !nowUnaligned) + lang.alignToByte(lang.normalIO) + lang.attrParse(attr, attr.id, defEndian) + wasUnaligned = nowUnaligned + } + } +} diff --git a/shared/src/main/scala/io/kaitai/struct/languages/TypeScriptCompiler.scala b/shared/src/main/scala/io/kaitai/struct/languages/TypeScriptCompiler.scala new file mode 100644 index 000000000..3cb89af64 --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/languages/TypeScriptCompiler.scala @@ -0,0 +1,708 @@ +package io.kaitai.struct.languages + +import io.kaitai.struct.datatype.DataType._ +import io.kaitai.struct.datatype._ +import io.kaitai.struct.exprlang.Ast +import io.kaitai.struct.exprlang.Ast.expr +import io.kaitai.struct.format._ +import io.kaitai.struct.languages.components._ +import io.kaitai.struct.translators.TypeScriptTranslator +import io.kaitai.struct.{ClassTypeProvider, RuntimeConfig, Utils} + +class TypeScriptCompiler(val typeProvider: ClassTypeProvider, config: RuntimeConfig) + extends LanguageCompiler(typeProvider, config) + with ObjectOrientedLanguage + with UpperCamelCaseClasses + with SingleOutputFile + with UniversalDoc + with AllocateIOLocalVar + with EveryReadIsExpression + with SwitchIfOps + with FixedContentsUsingArrayByteLiteral { + import TypeScriptCompiler._ + + override val translator = new TypeScriptTranslator(typeProvider) + + override def indent: String = " " + override def outFileName(topClassName: String): String = s"${type2class(topClassName)}.ts" + + override def outImports(topClass: ClassSpec) = { + val impList = importList.toList + val quotedImpList = impList.map((x) => s"'$x'") + + // the following goes at the top of the file + "const func = function(KaitaiStream) {" + } + + override def fileHeader(topClassName: String): Unit = { + outHeader.puts(s"// $headerComment") + outHeader.puts + + outHeader.puts("interface IDebug {") + outHeader.inc + outHeader.puts("start?: number;") + outHeader.puts("ioOffset?: number;") + outHeader.puts("end?: number;") + outHeader.puts("arr?: IDebug[];") + outHeader.puts("enumName?: string;") + outHeader.puts("[key: string]: number | string | IDebug | IDebug[] | undefined;") + outHeader.dec + outHeader.puts("}") + + importList.add("kaitai-struct/KaitaiStream") + } + + override def fileFooter(name: String): Unit = { + out.puts(s"return ${type2class(name)};") + out.dec + out.puts("}") + out.puts + out.puts("export default func;") + out.puts + } + + override def opaqueClassDeclaration(classSpec: ClassSpec): Unit = { + + } + + override def classHeader(name: List[String]): Unit = { + val shortClassName = type2class(name.last) + + out.puts + + if (name.size > 1) { + out.puts(s"static $shortClassName = class {") + } else { + out.puts(s"class $shortClassName {") + } + + out.inc + } + + override def classFooter(name: List[String]): Unit = { + out.dec + out.puts("}") + out.puts + } + + override def classConstructorHeader(name: List[String], parentClassName: DataType, rootClassName: List[String], isHybrid: Boolean, params: List[ParamDefSpec]): Unit = { + if (config.readStoresPos) { + out.puts("public _debug!: IDebug;") + } + + typeProvider.nowClass.meta.endian match { + case Some(_: CalcEndian) | Some(InheritedEndian) => + out.puts(s"public _is_le!: boolean;") + case _ => + // no _is_le variable + } + + val endianSuffix = if (isHybrid) { + ", _is_le?: any" + } else { + "" + } + + val paramsList = Utils.join(params.map((p) => s"${paramName(p.id)}?: ${kaitaiType2NativeType(p.dataType)}"), ", ", ", ", "") + + // Parameter names + val pIo = paramName(IoIdentifier) + val pParent = paramName(ParentIdentifier) + val pRoot = paramName(RootIdentifier) + + out.puts(s"constructor(public _io: any, parent: any, root?: any$endianSuffix$paramsList) {") + out.inc + out.puts(s"this.$pParent = parent;") + out.puts(s"this.$pRoot = root || this;") + + if (isHybrid) + out.puts("this._is_le = _is_le;") + + params.foreach((p) => handleAssignmentSimple(p.id, paramName(p.id))) + + out.puts + + if (config.readStoresPos) { + out.puts("this._debug = {};") + } + + } + + override def classConstructorFooter: Unit = { + out.dec + out.puts("}") + out.puts + } + + override def runRead(): Unit = { + out.puts("this._read();") + } + + override def runReadCalc(): Unit = { + out.puts + out.puts(s"if (this._is_le === true) {") + out.inc + out.puts("this._readLE();") + out.dec + out.puts("} else if (this._is_le === false) {") + out.inc + out.puts("this._readBE();") + out.dec + out.puts("} else {") + out.inc + out.puts("throw new KaitaiStream.UndecidedEndiannessError();") + out.dec + out.puts("}") + } + + override def readHeader(endian: Option[FixedEndian], isEmpty: Boolean) = { + val suffix = endian match { + case Some(e) => Utils.upperUnderscoreCase(e.toSuffix) + case None => "" + } + out.puts(s"public _read$suffix() {") + out.inc + } + + override def readFooter() = { + out.dec + out.puts("}") + out.puts + } + + override def attributeDeclaration(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = {} + + override def attributeReader(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = { + out.puts(s"public ${privateMemberName(attrName).substring(5)}!: ${kaitaiType2NativeType(attrType.asNonOwning, false)};") + } + + override def universalDoc(doc: DocSpec): Unit = { + // JSDoc docstring style: http://usejsdoc.org/about-getting-started.html + out.puts + out.puts( "/**") + + doc.summary.foreach((summary) => out.putsLines(" * ", summary)) + + // http://usejsdoc.org/tags-see.html + doc.ref.foreach { + case TextRef(text) => + out.putsLines(" * ", s"@see $text") + case UrlRef(url, text) => + out.putsLines(" * ", s"@see {@link $url|$text}") + } + + out.puts( " */") + } + + override def attrParseHybrid(leProc: () => Unit, beProc: () => Unit): Unit = { + out.puts("if (this._is_le) {") + out.inc + leProc() + out.dec + out.puts("} else {") + out.inc + beProc() + out.dec + out.puts("}") + } + + override def attrFixedContentsParse(attrName: Identifier, contents: String): Unit = { + out.puts(s"${privateMemberName(attrName)} = " + + s"$normalIO.ensureFixedContents($contents);") + } + + override def attrProcess(proc: ProcessExpr, varSrc: Identifier, varDest: Identifier, rep: RepeatSpec): Unit = { + val srcExpr = getRawIdExpr(varSrc, rep) + + val expr = proc match { + case ProcessXor(xorValue) => + val procName = translator.detectType(xorValue) match { + case _: IntType => "processXorOne" + case _: BytesType => "processXorMany" + } + s"$kstreamName.$procName($srcExpr, ${expression(xorValue)})" + case ProcessZlib => + s"$kstreamName.processZlib($srcExpr)" + case ProcessRotate(isLeft, rotValue) => + val expr = if (isLeft) { + expression(rotValue) + } else { + s"8 - (${expression(rotValue)})" + } + s"$kstreamName.processRotateLeft($srcExpr, $expr, 1)" + case ProcessCustom(name, args) => + val nameInit = name.init + val pkgName = if (nameInit.isEmpty) "" else nameInit.mkString("-") + "/" + val procClass = type2class(name.last) + + importList.add(s"$pkgName$procClass") + + out.puts(s"let _process = new $procClass(${args.map(expression).mkString(", ")});") + s"_process.decode($srcExpr)" + } + handleAssignment(varDest, expr, rep, false) + } + + override def allocateIO(varName: Identifier, rep: RepeatSpec): String = { + val langName = idToStr(varName) + val memberCall = s"${privateMemberName(varName)}" + + val ioName = s"_io_$langName" + + val args = getRawIdExpr(varName, rep) + + out.puts(s"let $ioName = new $kstreamName($args);") + ioName + } + + def getRawIdExpr(varName: Identifier, rep: RepeatSpec): String = { + val memberName = privateMemberName(varName) + rep match { + case NoRepeat => memberName + case RepeatExpr(_) => s"$memberName[i]" + case _ => s"$memberName[$memberName.length - 1]" + } + } + + override def useIO(ioEx: expr): String = { + out.puts(s"let io = ${expression(ioEx)};") + "io" + } + + override def pushPos(io: String): Unit = + out.puts(s"let _pos = $io.pos;") + + override def seek(io: String, pos: Ast.expr): Unit = + out.puts(s"$io.seek(${expression(pos)});") + + override def popPos(io: String): Unit = + out.puts(s"$io.seek(_pos);") + + override def alignToByte(io: String): Unit = + out.puts(s"$io.alignToByte();") + + override def attrDebugStart(attrId: Identifier, attrType: DataType, io: Option[String], rep: RepeatSpec): Unit = { + if (!attrDebugNeeded(attrId)) + return + + val debugName = attrDebugName(attrId, rep, false) + + val ioProps = io match { + case None => "" + case Some(x) => s"start: $x.pos, ioOffset: $x.byteOffset" + } + + val enumNameProps = attrType match { + case t: EnumType => s"""enumName: \"${types2class(t.enumSpec.get.name)}\"""" + case _ => "" + } + + out.puts(s"$debugName = { $ioProps${if (ioProps != "" && enumNameProps != "") ", " else ""}$enumNameProps };") + } + + override def attrDebugEnd(attrId: Identifier, attrType: DataType, io: String, rep: RepeatSpec): Unit = { + if (!attrDebugNeeded(attrId)) + return + val debugName = attrDebugName(attrId, rep, true) + + out.puts(s"$debugName.end = $io.pos;") + } + + override def condIfHeader(expr: expr): Unit = { + out.puts(s"if (${expression(expr)}) {") + out.inc + } + + override def condIfFooter(expr: expr): Unit = { + out.dec + out.puts("}") + } + + override def condRepeatEosHeader(id: Identifier, io: String, dataType: DataType, needRaw: NeedRaw): Unit = { + if (needRaw.level >= 1) + out.puts(s"${privateMemberName(RawIdentifier(id))} = [];") + if (needRaw.level >= 2) + out.puts(s"${privateMemberName(RawIdentifier(RawIdentifier(id)))} = [];") + out.puts(s"${privateMemberName(id)} = [];") + if (config.readStoresPos) + out.puts(s"this._debug.${idToStr(id)}.arr = [];") + out.puts("var i = 0;") + out.puts(s"while (!$io.isEof()) {") + out.inc + } + + override def handleAssignmentRepeatEos(id: Identifier, expr: String): Unit = { + out.puts(s"${privateMemberName(id)}.push($expr);") + } + + override def condRepeatEosFooter: Unit = { + out.puts("i++;") + out.dec + out.puts("}") + } + + override def condRepeatExprHeader(id: Identifier, io: String, dataType: DataType, needRaw: NeedRaw, repeatExpr: expr): Unit = { + if (needRaw.level >= 1) + out.puts(s"${privateMemberName(RawIdentifier(id))} = new Array(${expression(repeatExpr)});") + if (needRaw.level >= 2) + out.puts(s"${privateMemberName(RawIdentifier(RawIdentifier(id)))} = new Array(${expression(repeatExpr)});") + out.puts(s"${privateMemberName(id)} = new Array(${expression(repeatExpr)});") + if (config.readStoresPos) + out.puts(s"this._debug.${idToStr(id)}.arr = new Array(${expression(repeatExpr)});") + out.puts(s"for (let i = 0; i < ${expression(repeatExpr)}; i++) {") + out.inc + } + + override def handleAssignmentRepeatExpr(id: Identifier, expr: String): Unit = { + out.puts(s"${privateMemberName(id)}[i] = $expr;") + } + + override def condRepeatExprFooter: Unit = { + out.dec + out.puts("}") + } + + override def condRepeatUntilHeader(id: Identifier, io: String, dataType: DataType, needRaw: NeedRaw, untilExpr: expr): Unit = { + if (needRaw.level >= 1) + out.puts(s"${privateMemberName(RawIdentifier(id))} = []") + if (needRaw.level >= 2) + out.puts(s"${privateMemberName(RawIdentifier(RawIdentifier(id)))} = []") + out.puts(s"${privateMemberName(id)} = []") + if (config.readStoresPos) + out.puts(s"this._debug.${idToStr(id)}.arr = [];") + out.puts("var i = 0;") + out.puts("do {") + out.inc + } + + override def handleAssignmentRepeatUntil(id: Identifier, expr: String, isRaw: Boolean): Unit = { + val tmpName = translator.doName(if (isRaw) Identifier.ITERATOR2 else Identifier.ITERATOR) + out.puts(s"var $tmpName = $expr;") + out.puts(s"${privateMemberName(id)}.push($tmpName);") + } + + override def condRepeatUntilFooter(id: Identifier, io: String, dataType: DataType, needRaw: NeedRaw, untilExpr: expr): Unit = { + typeProvider._currentIteratorType = Some(dataType) + out.puts("i++;") + out.dec + out.puts(s"} while (!(${expression(untilExpr)}));") + } + + override def handleAssignmentSimple(id: Identifier, expr: String): Unit = { + if (expr.startsWith("[")) + // ensures that byte arrays are treated appropriately + out.puts(s"${privateMemberName(id)} = new Uint8Array($expr);") + else { + out.puts(s"${privateMemberName(id)} = $expr;") + } + } + + override def handleAssignmentTempVar(dataType: DataType, id: String, expr: String): Unit = + out.puts(s"var $id = $expr;") + + override def parseExpr(dataType: DataType, assignType: DataType, io: String, defEndian: Option[FixedEndian]): String = { + dataType match { + case t: ReadableType => + s"$io.read${Utils.capitalize(t.apiCall(defEndian))}()" + case blt: BytesLimitType => + s"$io.readBytes(${expression(blt.size)})" + case _: BytesEosType => + s"$io.readBytesFull()" + case BytesTerminatedType(terminator, include, consume, eosError, _) => + s"$io.readBytesTerm($terminator, $include, $consume, $eosError)" + case BitsType1 => + s"$io.readBitsInt(1) != 0" + case BitsType(width: Int) => + s"$io.readBitsInt($width)" + case t: UserType => + val parent = t.forcedParent match { + case Some(USER_TYPE_NO_PARENT) => "null" + case Some(fp) => translator.translate(fp) + case None => "this" + } + val root = if (t.isOpaque) "null" else "this._root" + val addEndian = t.classSpec.get.meta.endian match { + case Some(InheritedEndian) => ", this._is_le" + case _ => "" + } + val addParams = Utils.join(t.args.map((a) => translator.translate(a)), ", ", ", ", "") + s"new ${types2class(t.classSpec.get.name)}($io, $parent, $root$addEndian$addParams)" + } + } + + override def bytesPadTermExpr(expr0: String, padRight: Option[Int], terminator: Option[Int], include: Boolean) = { + val expr1 = padRight match { + case Some(padByte) => s"$kstreamName.bytesStripRight($expr0, $padByte)" + case None => expr0 + } + val expr2 = terminator match { + case Some(term) => s"$kstreamName.bytesTerminate($expr1, $term, $include)" + case None => expr1 + } + expr2 + } + + override def userTypeDebugRead(id: String, dataType: DataType, assignType: DataType): Unit = { + val incThis = if (id.startsWith("_t_")) "" else "this." + out.puts(s"$id._read();") + } + + override def switchRequiresIfs(onType: DataType): Boolean = onType match { + case _: IntType | _: BooleanType | _: EnumType | _: StrType => false + case _ => true + } + + // + + override def switchStart(id: Identifier, on: Ast.expr): Unit = + out.puts(s"switch (${expression(on)} {") + + override def switchCaseFirstStart(condition: Ast.expr): Unit = + switchCaseStart(condition) + + override def switchCaseStart(condition: Ast.expr): Unit = { + out.puts(s"case ${expression(condition)}: {") + out.inc + } + + override def switchCaseEnd(): Unit = { + out.puts("break;") + out.dec + out.puts("}") + } + + override def switchElseStart(): Unit = { + out.puts("default: {") + out.inc + } + + override def switchEnd(): Unit = { + out.dec + out.puts("}") + } + + // + + // + + val NAME_SWITCH_ON = Ast.expr.Name(Ast.identifier(Identifier.SWITCH_ON)) + + override def switchIfStart(id: Identifier, on: Ast.expr, onType: DataType): Unit = { + out.puts("{") + out.inc + out.puts(s"let ${expression(NAME_SWITCH_ON)} = ${expression(on)}") + } + + private def switchCmpExpr(condition: Ast.expr): String = + expression( + Ast.expr.Compare( + NAME_SWITCH_ON, + Ast.cmpop.Eq, + condition + ) + ) + + override def switchIfCaseFirstStart(condition: Ast.expr): Unit = { + out.puts(s"if (${switchCmpExpr(condition)}) {") + out.inc + } + + override def switchIfCaseStart(condition: Ast.expr): Unit = { + out.puts(s"else if (${switchCmpExpr(condition)}) {") + out.inc + } + + override def switchIfCaseEnd(): Unit = { + out.dec + out.puts("}") + } + + override def switchIfElseStart(): Unit = { + out.puts("else {") + out.inc + } + + override def switchIfEnd(): Unit = { + out.dec + out.puts("}") + } + + // + + override def instanceHeader(className: List[String], instName: InstanceIdentifier, dataType: DataType, isNullable: Boolean): Unit = { + out.puts(s"private ${privateMemberName(instName).substring(5)}!: ${kaitaiType2NativeType(dataType, false)};") + out.puts(s"get ${publicMemberName(instName)}() {") + out.inc + } + + override def instanceClear(instName: InstanceIdentifier) = { + out.puts(s"${privateMemberName(instName)}") + } + + override def instanceFooter: Unit = { + out.dec + out.puts("}") + out.puts + } + + override def instanceCheckCacheAndReturn(instName: InstanceIdentifier, dataType: DataType): Unit = { + out.puts(s"if (${privateMemberName(instName)} !== undefined)") + out.inc + instanceReturn(instName, dataType) + out.dec + } + + override def instanceReturn(instName: InstanceIdentifier, attrType: DataType): Unit = { + out.puts(s"return ${privateMemberName(instName)};") + } + + override def enumDeclaration(curClass: List[String], enumName: String, enumColl: Seq[(Long, EnumValueSpec)]): Unit = { + out.puts(s"static ${type2class(enumName)} = Object.freeze({") + out.inc + + // Name to ID mapping + enumColl.foreach { case (id, label) => + out.puts(s"${enumValue(enumName, label.name)}: $id,") + } + out.puts + + // ID to name mapping + enumColl.foreach { case (id, label) => + val idStr = if (id < 0) { + "\"" + id.toString + "\"" + } else { + id.toString + } + out.puts(s"""$idStr: "${enumValue(enumName, label.name)}",""") + } + + out.dec + out.puts("});") + out.puts + } + + def enumValue(enumName: String, label: String) = Utils.upperUnderscoreCase(label) + + override def debugClassSequence(seq: List[AttrSpec]) = { + val seqStr = seq.map((attr) => "\"" + idToStr(attr.id) + "\"").mkString(", ") + out.puts(s"public SEQ_FIELDS = [$seqStr]") + } + + def idToStr(id: Identifier): String = { + id match { + case SpecialIdentifier(name) => name + case NamedIdentifier(name) => Utils.lowerCamelCase(name) + case NumberedIdentifier(idx) => s"_${NumberedIdentifier.TEMPLATE}$idx" + case InstanceIdentifier(name) => s"_m_${Utils.lowerCamelCase(name)}" + case RawIdentifier(innerId) => "_raw_" + idToStr(innerId) + } + } + + override def privateMemberName(id: Identifier): String = s"this.${idToStr(id)}" + + override def publicMemberName(id: Identifier): String = { + id match { + case NamedIdentifier(name) => Utils.lowerCamelCase(name) + case InstanceIdentifier(name) => Utils.lowerCamelCase(name) + } + } + + override def localTemporaryName(id: Identifier): String = s"_t_${idToStr(id)}" + + override def ksErrorName(err: KSError): String = TypeScriptCompiler.ksErrorName(err) + + override def attrValidateExpr( + attrId: Identifier, + attrType: DataType, + checkExpr: Ast.expr, + errName: String, + errArgs: List[Ast.expr] + ): Unit = { + val errArgsStr = errArgs.map(translator.translate).mkString(", ") + out.puts(s"if (!(${translator.translate(checkExpr)})) {") + out.inc + out.puts(s"throw new $errName($errArgsStr);") + out.dec + out.puts("}") + } + + private + def attrDebugNeeded(attrId: Identifier) = attrId match { + case _: NamedIdentifier | _: NumberedIdentifier | _: InstanceIdentifier => true + case _: RawIdentifier | _: SpecialIdentifier => false + } + + def attrDebugName(attrId: Identifier, rep: RepeatSpec, end: Boolean) = { + val arrIndexExpr = rep match { + case NoRepeat => "" + case _: RepeatExpr => ".arr[i]" + case RepeatEos | _: RepeatUntil => s".arr[${privateMemberName(attrId)}.length${if (end) " - 1" else ""}]" + } + + s"this._debug.${idToStr(attrId)}$arrIndexExpr" + } +} + +object TypeScriptCompiler extends LanguageCompilerStatic + with UpperCamelCaseClasses + with StreamStructNames + with ExceptionNames { + override def getCompiler( + tp: ClassTypeProvider, + config: RuntimeConfig + ): LanguageCompiler = new TypeScriptCompiler(tp, config) + + def kaitaiType2NativeType(attrType: DataType, absolute: Boolean = false): String = { + attrType match { + // for if/when BigInt support is added in js/ts runtime + // case IntMultiType(false, Width8, _) => "BigInt" + // case IntMultiType(true, Width8, _) => "number" + case _: NumericType => "number" + + case BitsType(_) => "number" + + case _: BooleanType => "boolean" + case CalcIntType => "number" + case CalcFloatType => "number" + + case _: StrType => "string" + case _: BytesType => "Uint8Array" + + case t: UserType => + s"typeof ${types2class(t.classSpec match { + case None => t.name + case Some(cs) => cs.name + })}.prototype" + + // for now enums are numbers by default + case t: EnumType => "number" //types2class(t.enumSpec.get.name) + + case at: ArrayType => + s"(${kaitaiType2NativeType(at.elType, absolute)})[]" + + case CalcArrayType(inType) => s"(${kaitaiType2NativeType(inType, absolute)})[]" + + case AnyType => "any" + + case KaitaiStreamType => s"$kstreamName*" + case KaitaiStructType | CalcKaitaiStructType => "any" + + // unfolds and removes duplicates by converting to set + case SwitchType(_, cases, _) => cases.map(kv => kaitaiType2NativeType(kv._2, false)).toSet.mkString(" | ") + } + } + + override def kstreamName: String = "KaitaiStream" + + // FIXME: probably KaitaiStruct will emerge some day in JavaScript runtime, but for now it is unused + override def kstructName: String = ??? + + def types2class(types: List[String]): String = types.map(type2class).mkString(".") + + override def ksErrorName(err: KSError): String = err match { + case EndOfStreamError => s"KaitaiStream.EOFError" + case _ => s"KaitaiStream.${err.name}" + } + + override def type2class(name: String): String = Utils.upperCamelCase(name) +} diff --git a/shared/src/main/scala/io/kaitai/struct/languages/components/LanguageCompilerStatic.scala b/shared/src/main/scala/io/kaitai/struct/languages/components/LanguageCompilerStatic.scala index 99f42503e..64f0a206f 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/components/LanguageCompilerStatic.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/components/LanguageCompilerStatic.scala @@ -23,7 +23,8 @@ object LanguageCompilerStatic { "php" -> PHPCompiler, "python" -> PythonCompiler, "ruby" -> RubyCompiler, - "rust" -> RustCompiler + "rust" -> RustCompiler, + "typescript" -> TypeScriptCompiler ) val CLASS_TO_NAME: Map[LanguageCompilerStatic, String] = NAME_TO_CLASS.map(_.swap) diff --git a/shared/src/main/scala/io/kaitai/struct/translators/TypeScriptTranslator.scala b/shared/src/main/scala/io/kaitai/struct/translators/TypeScriptTranslator.scala new file mode 100644 index 000000000..20f814f2a --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/translators/TypeScriptTranslator.scala @@ -0,0 +1,138 @@ +package io.kaitai.struct.translators + +import io.kaitai.struct.Utils +import io.kaitai.struct.datatype.DataType +import io.kaitai.struct.datatype.DataType._ +import io.kaitai.struct.exprlang.Ast +import io.kaitai.struct.exprlang.Ast.expr +import io.kaitai.struct.format.Identifier +import io.kaitai.struct.languages.TypeScriptCompiler + +class TypeScriptTranslator(provider: TypeProvider) extends BaseTranslator(provider) { + override def doByteArrayNonLiteral(elts: Seq[Ast.expr]): String = + s"new Uint8Array([${elts.map(translate).mkString(", ")}])" + + /** + * JavaScript rendition of common control character that would use hex form, + * not octal. "Octal" control character string literals might be accepted + * in non-strict JS mode, but in strict mode only hex or unicode are ok. + * Here we'll use hex, as they are shorter. + * + * @see https://github.com/kaitai-io/kaitai_struct/issues/279 + * @param code character code to represent + * @return string literal representation of given code + */ + override def strLiteralGenericCC(code: Char): String = + "\\x%02x".format(code.toInt) + + override def numericBinOp(left: Ast.expr, op: Ast.operator, right: Ast.expr) = { + (detectType(left), detectType(right), op) match { + case (_: IntType, _: IntType, Ast.operator.Div) => + s"Math.floor(${translate(left)} / ${translate(right)})" + case (_: IntType, _: IntType, Ast.operator.Mod) => + s"${TypeScriptCompiler.kstreamName}.mod(${translate(left)}, ${translate(right)})" + case (_: IntType, _: IntType, Ast.operator.RShift) => + s"(${translate(left)} >>> ${translate(right)})" + case _ => + super.numericBinOp(left, op, right) + } + } + + override def doLocalName(s: String) = { + s match { + case "_" => s + case Identifier.SWITCH_ON => "on" + case Identifier.INDEX => "i" + case _ => s"this.${doName(s)}" + } + } + + override def doName(s: String) = { + s match { + case "_root" | "_parent" | "_io" => s + case _ => Utils.lowerCamelCase(s) + } + } + + override def doEnumByLabel(enumType: List[String], label: String): String = + s"${TypeScriptCompiler.types2class(enumType)}.${Utils.upperUnderscoreCase(label)}" + override def doEnumById(enumTypeAbs: List[String], label: String): String = + // Just an integer, without any casts / resolutions - one would have to look up constants manually + label + + override def doBytesCompareOp(left: Ast.expr, op: Ast.cmpop, right: Ast.expr): String = + s"(${TypeScriptCompiler.kstreamName}.byteArrayCompare(${translate(left)}, ${translate(right)}) ${cmpOp(op)} 0)" + + override def arraySubscript(container: expr, idx: expr): String = + s"${translate(container)}[${translate(idx)}]" + override def doIfExp(condition: expr, ifTrue: expr, ifFalse: expr): String = + s"(${translate(condition)} ? ${translate(ifTrue)} : ${translate(ifFalse)})" + override def doCast(value: Ast.expr, typeName: DataType): String = + s"(${translate(value)} as ${TypeScriptCompiler.kaitaiType2NativeType(typeName, false)})" + + // Predefined methods of various types + override def strToInt(s: expr, base: expr): String = + s"Number.parseInt(${translate(s)}, ${translate(base)})" + + override def enumToInt(v: expr, et: EnumType): String = + translate(v) + + /** + * Converts a boolean (true or false) to integer (1 or 0, respectively) in + * JavaScript. There are quite a few methods to so, this one is generally + * accepted as one of the fastest (other top methods are +-0.3%), and it's + * pretty concise and readable. + * + * @see http://stackoverflow.com/questions/7820683/convert-boolean-result-into-number-integer + * @param v boolean expression to convert + * @return string rendition of conversion + */ + override def boolToInt(v: expr): String = + s"(${translate(v)} | 0)" + + /** + * Converts a float to an integer in JavaScript. There are many methods to + * do so, here we use the fastest one, but it requires ES6+. OTOH, it is + * relatively easy to add compatibility polyfill for non-supporting environments + * (see MDN page). + * + * @see http://stackoverflow.com/a/596503/487064 + * @see https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc + * @param v float expression to convert + * @return string rendition of conversion + */ + override def floatToInt(v: expr): String = + s"Math.trunc(${translate(v)})" + + override def intToStr(i: expr, base: expr): String = + s"(${translate(i)}).toString(${translate(base)})" + + override def bytesToStr(bytesExpr: String, encoding: Ast.expr): String = + s"${TypeScriptCompiler.kstreamName}.bytesToStr($bytesExpr, ${translate(encoding)})" + + override def strLength(s: expr): String = + s"${translate(s)}.length" + + // http://stackoverflow.com/a/36525647/2055163 + override def strReverse(s: expr): String = + s"Array.from(${translate(s)}).reverse().join('')" + + override def strSubstring(s: expr, from: expr, to: expr): String = + s"${translate(s)}.substring(${translate(from)}, ${translate(to)})" + + override def arrayFirst(a: expr): String = + s"${translate(a)}[0]" + override def arrayLast(a: expr): String = { + val v = translate(a) + s"$v[$v.length - 1]" + } + override def arraySize(a: expr): String = + s"${translate(a)}.length" + override def arrayMin(a: expr): String = + s"${TypeScriptCompiler.kstreamName}.arrayMin(${translate(a)})" + override def arrayMax(a: expr): String = + s"${TypeScriptCompiler.kstreamName}.arrayMax(${translate(a)})" + + override def kaitaiStreamEof(value: Ast.expr): String = + s"${translate(value)}.isEof()" +}