From 78bd5ae1398699ccced672ab6d4ee2b97e1f1481 Mon Sep 17 00:00:00 2001 From: Unknown Date: Tue, 4 Jun 2019 16:56:56 -0400 Subject: [PATCH 1/3] Added Typescript as a Compiler Target The output of the new "TypeScript" target is typescript that uses a class based format instead of a prototype-based one. TypeScript allows for autocompletion, making it significantly easier to work with the output of complex formats. Certain shortcuts were also taken for the time being, namely using a type of 'any' instead of the 'type 1 | type 2' for switch types --- .../main/scala/io/kaitai/struct/Main.scala | 4 +- .../struct/TypeScriptClassCompiler.scala | 76 ++ .../struct/languages/TypeScriptCompiler.scala | 722 ++++++++++++++++++ .../components/LanguageCompilerStatic.scala | 3 +- .../translators/TypeScriptTranslator.scala | 135 ++++ 5 files changed, 938 insertions(+), 2 deletions(-) create mode 100644 shared/src/main/scala/io/kaitai/struct/TypeScriptClassCompiler.scala create mode 100644 shared/src/main/scala/io/kaitai/struct/languages/TypeScriptCompiler.scala create mode 100644 shared/src/main/scala/io/kaitai/struct/translators/TypeScriptTranslator.scala diff --git a/shared/src/main/scala/io/kaitai/struct/Main.scala b/shared/src/main/scala/io/kaitai/struct/Main.scala index 15f00b34f..d0ed94f6a 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} +import io.kaitai.struct.languages.{GoCompiler, RustCompiler, TypeScriptCompiler} import io.kaitai.struct.languages.components.LanguageCompilerStatic import io.kaitai.struct.precompile._ @@ -65,6 +65,8 @@ object Main { new ConstructClassCompiler(specs, spec) 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..ba263df37 --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/TypeScriptClassCompiler.scala @@ -0,0 +1,76 @@ +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 compileSeqProc(seq: List[AttrSpec], defEndian: Option[FixedEndian]) = { + lang.readHeader(defEndian, seq.isEmpty) + compileSeq(seq, defEndian) + lang.readFooter() + } + + 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..70c2b1766 --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/languages/TypeScriptCompiler.scala @@ -0,0 +1,722 @@ +package io.kaitai.struct.languages + +import io.kaitai.struct.datatype.DataType._ +import io.kaitai.struct.datatype.{Endianness, DataType, FixedEndian, InheritedEndian} +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(typeProvider: ClassTypeProvider, config: RuntimeConfig) + extends LanguageCompiler(typeProvider, config) + with ObjectOrientedLanguage + with UpperCamelCaseClasses + with SingleOutputFile + with UniversalDoc + with AllocateIOLocalVar + with EveryReadIsExpression + 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'") + val defineArgs = quotedImpList.mkString(", ") + val moduleArgs = quotedImpList.map((x) => s"require($x)").mkString(", ") + val argClasses = impList.map((x) => x.split('/').last) + val rootArgs = argClasses.map((x) => s"root.$x").mkString(", ") + + // 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;") + } + + val endianSuffix = if (isHybrid) { + ", _is_le: any" + } else { + "" + } + + // val paramsList = Utils.join(params.map((p) => paramName(p.id)), ", public ", ", public ", "") + + // 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 ) {") + 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) => e.toSuffix.toUpperCase + 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 = { + def arr = attrType match { + case ArrayType(_) => "[]" + case _ => "" + } + + val typeof = isUserOrEnumType(attrType) match { + case "user" => "typeof " + case _ => "" + } + + val prototype = isUserOrEnumType(attrType) match { + case "user" => ".prototype" + case _ => "" + } + + out.puts(s"public ${privateMemberName(attrName)}!: $typeof${kaitaiType2NativeType(attrType.asNonOwning, false)}$prototype$arr;") + } + + 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 match { + case TextRef(text) => + out.putsLines(" * ", s"@see $text") + case UrlRef(url, text) => + out.putsLines(" * ", s"@see {@link $url|$text}") + case NoRef => + } + + out.puts( " */") + } + + // override def attrParse(attr: AttrLikeSpec, id: Identifier, defEndian: Option[Endianness]): Unit = { + // out.puts(s"this.meta[${privateMemberName(id)}] = '$attr'") + // } + + 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"this.${privateMemberName(attrName)} = " + + s"$normalIO.ensureFixedContents($contents);") + } + + override def attrProcess(proc: ProcessExpr, varSrc: Identifier, varDest: Identifier): Unit = { + val srcName = privateMemberName(varSrc) + val destName = privateMemberName(varDest) + + proc match { + case ProcessXor(xorValue) => + val procName = translator.detectType(xorValue) match { + case _: IntType => "processXorOne" + case _: BytesType => "processXorMany" + } + out.puts(s"$destName = $kstreamName.$procName($srcName, ${expression(xorValue)});") + case ProcessZlib => + out.puts(s"$destName = $kstreamName.processZlib($srcName);") + case ProcessRotate(isLeft, rotValue) => + val expr = if (isLeft) { + expression(rotValue) + } else { + s"8 - (${expression(rotValue)})" + } + out.puts(s"$destName = $kstreamName.processRotateLeft($srcName, $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(", ")});") + out.puts(s"$destName = _process.decode($srcName);") + } + } + + override def allocateIO(varName: Identifier, rep: RepeatSpec): String = { + val langName = s"this.${idToStr(varName)}" + val memberCall = s"this.${privateMemberName(varName)}" + + val ioName = s"_io_$langName" + + val args = rep match { + case RepeatEos | RepeatUntil(_) => s"$memberCall[$memberCall.length - 1]" + case RepeatExpr(_) => s"$memberCall[i]" + case NoRepeat => memberCall + } + + out.puts(s"let $ioName = new $kstreamName($args);") + ioName + } + + 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 = this.$io.pos;") + + override def seek(io: String, pos: Ast.expr): Unit = + out.puts(s"this.$io.seek(${expression(pos)});") + + override def popPos(io: String): Unit = + out.puts(s"this.$io.seek(_pos);") + + override def alignToByte(io: String): Unit = + out.puts(s"this.$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: this.$x.pos, ioOffset: this.$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 = this.$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: Boolean): Unit = { + if (needRaw) + out.puts(s"this.${privateMemberName(RawIdentifier(id))} = [];") + out.puts(s"this.${privateMemberName(id)} = [];") + if (config.readStoresPos) + out.puts(s"this._debug.${idToStr(id)}.arr = [];") + out.puts("let i = 0;") + out.puts(s"while (!this.$io.isEof()) {") + out.inc + } + + override def handleAssignmentRepeatEos(id: Identifier, expr: String): Unit = { + out.puts(s"this.${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: Boolean, repeatExpr: expr): Unit = { + if (needRaw) + out.puts(s"this.${privateMemberName(RawIdentifier(id))} = new Array(${expression(repeatExpr)});") + out.puts(s"this.${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"this.${privateMemberName(id)}[i] = $expr;") + } + + override def condRepeatExprFooter: Unit = { + out.dec + out.puts("}") + } + + override def condRepeatUntilHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, untilExpr: expr): Unit = { + if (needRaw) + out.puts(s"this.${privateMemberName(RawIdentifier(id))} = []") + out.puts(s"this.${privateMemberName(id)} = []") + if (config.readStoresPos) + out.puts(s"this._debug.${idToStr(id)}.arr = [];") + out.puts("let 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"let $tmpName = $expr;") + out.puts(s"this.${privateMemberName(id)}.push($tmpName);") + } + + override def condRepeatUntilFooter(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, 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 = { + out.puts(s"this.${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"this.$io.read${Utils.capitalize(t.apiCall(defEndian))}()" + case blt: BytesLimitType => + s"this.$io.readBytes(${expression(blt.size)})" + case _: BytesEosType => + s"this.$io.readBytesFull()" + case BytesTerminatedType(terminator, include, consume, eosError, _) => + s"this.$io.readBytesTerm($terminator, $include, $consume, $eosError)" + case BitsType1 => + s"this.$io.readBitsInt(1) != 0" + case BitsType(width: Int) => + s"this.$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)}(this.$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): Unit = { + val incThis = if (id.startsWith("_t_")) "" else "this." + out.puts(s"$incThis$id._read();") + } + + /** + * Designates switch mode. If false, we're doing real switch-case for this + * attribute. If true, we're doing if-based emulation. + */ + var switchIfs = false + + val NAME_SWITCH_ON = Ast.expr.Name(Ast.identifier(Identifier.SWITCH_ON)) + + override def switchStart(id: Identifier, on: Ast.expr): Unit = { + val onType = translator.detectType(on) + typeProvider._currentSwitchType = Some(onType) + + // Determine switching mode for this construct based on type + switchIfs = onType match { + case _: IntType | _: BooleanType | _: EnumType | _: StrType => false + case _ => true + } + + if (switchIfs) { + out.puts("{") + out.inc + out.puts(s"let ${expression(NAME_SWITCH_ON)} = ${expression(on)};") + } else { + out.puts(s"switch (${expression(on)}) {") + } + } + + def switchCmpExpr(condition: Ast.expr): String = + expression( + Ast.expr.Compare( + NAME_SWITCH_ON, + Ast.cmpop.Eq, + condition + ) + ) + + override def switchCaseFirstStart(condition: Ast.expr): Unit = { + if (switchIfs) { + out.puts(s"if (${switchCmpExpr(condition)}) {") + out.inc + } else { + switchCaseStart(condition) + } + } + + override def switchCaseStart(condition: Ast.expr): Unit = { + if (switchIfs) { + out.puts(s"else if (${switchCmpExpr(condition)}) {") + out.inc + } else { + out.puts(s"case ${expression(condition)}:") + out.inc + } + } + + override def switchCaseEnd(): Unit = { + if (switchIfs) { + out.dec + out.puts("}") + } else { + out.puts("break;") + out.dec + } + } + + override def switchElseStart(): Unit = { + if (switchIfs) { + out.puts("else {") + out.inc + } else { + out.puts("default:") + out.inc + } + } + + override def switchEnd(): Unit = { + if (switchIfs) + out.dec + out.puts("}") + } + + override def instanceHeader(className: List[String], instName: InstanceIdentifier, dataType: DataType, isNullable: Boolean): Unit = { + def arr = dataType match { + case ArrayType(_) => "[]" + case _ => "" + } + + val typeof = isUserOrEnumType(dataType) match { + case "user" => "typeof " + case _ => "" + } + + val prototype = if (isUserOrEnumType(dataType) == "user") { + ".prototype" + } else { + "" + } + + out.puts(s"private ${privateMemberName(instName)}!: $typeof${kaitaiType2NativeType(dataType, false)}$prototype$arr;") + out.puts(s"get ${publicMemberName(instName)}() {") + out.inc + } + + override def instanceClear(instName: InstanceIdentifier) = { + out.puts(s"this.${privateMemberName(instName)}") + } + + // override def instanceSetCalculated(instName: InstanceIdentifier) = { + // out.puts(s"this.${privateMemberName(instName)} = // instanceSetCalculated") + // } + + override def instanceCalculate(instName: Identifier, dataType: DataType, value: Ast.expr) = { + out.puts(s"this.${privateMemberName(instName)} = $value"); + } + + override def instanceFooter: Unit = { + out.dec + out.puts("}") + out.puts + } + + override def instanceCheckCacheAndReturn(instName: InstanceIdentifier, dataType: DataType): Unit = { + out.puts(s"if (this.${privateMemberName(instName)} !== undefined)") + out.inc + instanceReturn(instName, dataType) + out.dec + } + + override def instanceReturn(instName: InstanceIdentifier, attrType: DataType): Unit = { + out.puts(s"return this.${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) = label.toUpperCase + + 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"${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)}" + + 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[this.${privateMemberName(attrId)}.length${if (end) " - 1" else ""}]" + } + + s"this._debug.${idToStr(attrId)}$arrIndexExpr" + } +} + +object TypeScriptCompiler extends LanguageCompilerStatic + with UpperCamelCaseClasses + with StreamStructNames { + override def getCompiler( + tp: ClassTypeProvider, + config: RuntimeConfig + ): LanguageCompiler = new TypeScriptCompiler(tp, config) + + def kaitaiType2NativeType(attrType: DataType, absolute: Boolean = false): String = { + attrType match { + case Int1Type(false) => "number" + case IntMultiType(false, Width2, _) => "number" + case IntMultiType(false, Width4, _) => "number" + case IntMultiType(false, Width8, _) => "number" + + case Int1Type(true) => "number" + case IntMultiType(true, Width2, _) => "number" + case IntMultiType(true, Width4, _) => "number" + case IntMultiType(true, Width8, _) => "number" + + case FloatMultiType(Width4, _) => "number" + case FloatMultiType(Width8, _) => "number" + + case BitsType(_) => "number" + + case _: BooleanType => "boolean" + case CalcIntType => "number" + case CalcFloatType => "number" + + case _: StrType => "string" + case _: BytesType => "Uint8Array" + + case t: UserType => + types2class(t.classSpec match { + case None => t.name + case Some(cs) => cs.name + }) + + // for now enums are numbers by default + case t: EnumType => "number" //types2class(t.enumSpec.get.name) + + case ArrayType(inType) => + s"${kaitaiType2NativeType(inType, absolute)}" + + case CalcArrayType(inType) => s"${kaitaiType2NativeType(inType, absolute)}" + + case AnyType => "any" + + case KaitaiStreamType => s"$kstreamName*" + case KaitaiStructType | CalcKaitaiStructType => "any" + + case st: SwitchType => kaitaiType2NativeType(st.combinedType, absolute) + } + } + + 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 type2class(name: String): String = Utils.upperCamelCase(name) + + def isUserOrEnumType(inType: DataType): String = inType match { + case _: UserType => "user" + case _: EnumType => "enum" + case ArrayType(innerType) => isUserOrEnumType(innerType) + case _ => "other" + } +} 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 bbb497c96..475ead6ef 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 @@ -22,7 +22,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..85358eb20 --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/translators/TypeScriptTranslator.scala @@ -0,0 +1,135 @@ +package io.kaitai.struct.translators + +import io.kaitai.struct.Utils +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)}.${label.toUpperCase}" + 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 doSubscript(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)})" + + // 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()" +} \ No newline at end of file From e788e36ff3d3038b8e964df5cb2340d353a80305 Mon Sep 17 00:00:00 2001 From: Dominick Reba Date: Mon, 30 Mar 2020 10:51:28 -0400 Subject: [PATCH 2/3] cleaned code, squashed a whole bunch of bugs, and ensured that typescript works with all the tests that javascript can and more --- .../struct/TypeScriptClassCompiler.scala | 6 - .../struct/languages/TypeScriptCompiler.scala | 215 ++++++++---------- .../translators/TypeScriptTranslator.scala | 5 +- 3 files changed, 105 insertions(+), 121 deletions(-) diff --git a/shared/src/main/scala/io/kaitai/struct/TypeScriptClassCompiler.scala b/shared/src/main/scala/io/kaitai/struct/TypeScriptClassCompiler.scala index ba263df37..c712fee27 100644 --- a/shared/src/main/scala/io/kaitai/struct/TypeScriptClassCompiler.scala +++ b/shared/src/main/scala/io/kaitai/struct/TypeScriptClassCompiler.scala @@ -57,12 +57,6 @@ class TypeScriptClassCompiler( lang.classFooter(curClass.name) } - override def compileSeqProc(seq: List[AttrSpec], defEndian: Option[FixedEndian]) = { - lang.readHeader(defEndian, seq.isEmpty) - compileSeq(seq, defEndian) - lang.readFooter() - } - override def compileSeq(seq: List[AttrSpec], defEndian: Option[FixedEndian]) = { var wasUnaligned = false seq.foreach { (attr) => diff --git a/shared/src/main/scala/io/kaitai/struct/languages/TypeScriptCompiler.scala b/shared/src/main/scala/io/kaitai/struct/languages/TypeScriptCompiler.scala index 70c2b1766..ff5e8e3cf 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/TypeScriptCompiler.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/TypeScriptCompiler.scala @@ -1,7 +1,7 @@ package io.kaitai.struct.languages import io.kaitai.struct.datatype.DataType._ -import io.kaitai.struct.datatype.{Endianness, DataType, FixedEndian, InheritedEndian} +import io.kaitai.struct.datatype.{CalcEndian, Endianness, DataType, FixedEndian, InheritedEndian} import io.kaitai.struct.exprlang.Ast import io.kaitai.struct.exprlang.Ast.expr import io.kaitai.struct.format._ @@ -28,10 +28,6 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def outImports(topClass: ClassSpec) = { val impList = importList.toList val quotedImpList = impList.map((x) => s"'$x'") - val defineArgs = quotedImpList.mkString(", ") - val moduleArgs = quotedImpList.map((x) => s"require($x)").mkString(", ") - val argClasses = impList.map((x) => x.split('/').last) - val rootArgs = argClasses.map((x) => s"root.$x").mkString(", ") // the following goes at the top of the file "const func = function(KaitaiStream) {" @@ -93,20 +89,27 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) 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" + ", _is_le?: any" } else { "" } - // val paramsList = Utils.join(params.map((p) => paramName(p.id)), ", public ", ", public ", "") + 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 ) {") + 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;") @@ -169,22 +172,7 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def attributeDeclaration(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = {} override def attributeReader(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = { - def arr = attrType match { - case ArrayType(_) => "[]" - case _ => "" - } - - val typeof = isUserOrEnumType(attrType) match { - case "user" => "typeof " - case _ => "" - } - - val prototype = isUserOrEnumType(attrType) match { - case "user" => ".prototype" - case _ => "" - } - - out.puts(s"public ${privateMemberName(attrName)}!: $typeof${kaitaiType2NativeType(attrType.asNonOwning, false)}$prototype$arr;") + out.puts(s"public ${privateMemberName(attrName).substring(5)}!: ${kaitaiType2NativeType(attrType.asNonOwning, false)};") } override def universalDoc(doc: DocSpec): Unit = { @@ -223,7 +211,7 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) } override def attrFixedContentsParse(attrName: Identifier, contents: String): Unit = { - out.puts(s"this.${privateMemberName(attrName)} = " + + out.puts(s"${privateMemberName(attrName)} = " + s"$normalIO.ensureFixedContents($contents);") } @@ -260,8 +248,8 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) } override def allocateIO(varName: Identifier, rep: RepeatSpec): String = { - val langName = s"this.${idToStr(varName)}" - val memberCall = s"this.${privateMemberName(varName)}" + val langName = idToStr(varName) + val memberCall = s"${privateMemberName(varName)}" val ioName = s"_io_$langName" @@ -281,16 +269,16 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) } override def pushPos(io: String): Unit = - out.puts(s"let _pos = this.$io.pos;") + out.puts(s"let _pos = $io.pos;") override def seek(io: String, pos: Ast.expr): Unit = - out.puts(s"this.$io.seek(${expression(pos)});") + out.puts(s"$io.seek(${expression(pos)});") override def popPos(io: String): Unit = - out.puts(s"this.$io.seek(_pos);") + out.puts(s"$io.seek(_pos);") override def alignToByte(io: String): Unit = - out.puts(s"this.$io.alignToByte();") + out.puts(s"$io.alignToByte();") override def attrDebugStart(attrId: Identifier, attrType: DataType, io: Option[String], rep: RepeatSpec): Unit = { if (!attrDebugNeeded(attrId)) @@ -300,7 +288,7 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) val ioProps = io match { case None => "" - case Some(x) => s"start: this.$x.pos, ioOffset: this.$x._byteOffset" + case Some(x) => s"start: $x.pos, ioOffset: $x._byteOffset" } val enumNameProps = attrType match { @@ -316,7 +304,7 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) return val debugName = attrDebugName(attrId, rep, true) - out.puts(s"$debugName.end = this.$io.pos;") + out.puts(s"$debugName.end = $io.pos;") } override def condIfHeader(expr: expr): Unit = { @@ -331,17 +319,17 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def condRepeatEosHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean): Unit = { if (needRaw) - out.puts(s"this.${privateMemberName(RawIdentifier(id))} = [];") - out.puts(s"this.${privateMemberName(id)} = [];") + out.puts(s"${privateMemberName(RawIdentifier(id))} = [];") + out.puts(s"${privateMemberName(id)} = [];") if (config.readStoresPos) out.puts(s"this._debug.${idToStr(id)}.arr = [];") - out.puts("let i = 0;") - out.puts(s"while (!this.$io.isEof()) {") + out.puts("var i = 0;") + out.puts(s"while (!$io.isEof()) {") out.inc } override def handleAssignmentRepeatEos(id: Identifier, expr: String): Unit = { - out.puts(s"this.${privateMemberName(id)}.push($expr);") + out.puts(s"${privateMemberName(id)}.push($expr);") } override def condRepeatEosFooter: Unit = { @@ -352,8 +340,8 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def condRepeatExprHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, repeatExpr: expr): Unit = { if (needRaw) - out.puts(s"this.${privateMemberName(RawIdentifier(id))} = new Array(${expression(repeatExpr)});") - out.puts(s"this.${privateMemberName(id)} = new Array(${expression(repeatExpr)});") + out.puts(s"${privateMemberName(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++) {") @@ -361,7 +349,7 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) } override def handleAssignmentRepeatExpr(id: Identifier, expr: String): Unit = { - out.puts(s"this.${privateMemberName(id)}[i] = $expr;") + out.puts(s"${privateMemberName(id)}[i] = $expr;") } override def condRepeatExprFooter: Unit = { @@ -371,19 +359,19 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def condRepeatUntilHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, untilExpr: expr): Unit = { if (needRaw) - out.puts(s"this.${privateMemberName(RawIdentifier(id))} = []") - out.puts(s"this.${privateMemberName(id)} = []") + out.puts(s"${privateMemberName(RawIdentifier(id))} = []") + out.puts(s"${privateMemberName(id)} = []") if (config.readStoresPos) out.puts(s"this._debug.${idToStr(id)}.arr = [];") - out.puts("let i = 0;") + 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"let $tmpName = $expr;") - out.puts(s"this.${privateMemberName(id)}.push($tmpName);") + out.puts(s"var $tmpName = $expr;") + out.puts(s"${privateMemberName(id)}.push($tmpName);") } override def condRepeatUntilFooter(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, untilExpr: expr): Unit = { @@ -394,7 +382,12 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) } override def handleAssignmentSimple(id: Identifier, expr: String): Unit = { - out.puts(s"this.${privateMemberName(id)} = $expr;") + 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 = @@ -403,17 +396,17 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def parseExpr(dataType: DataType, assignType: DataType, io: String, defEndian: Option[FixedEndian]): String = { dataType match { case t: ReadableType => - s"this.$io.read${Utils.capitalize(t.apiCall(defEndian))}()" + s"$io.read${Utils.capitalize(t.apiCall(defEndian))}()" case blt: BytesLimitType => - s"this.$io.readBytes(${expression(blt.size)})" + s"$io.readBytes(${expression(blt.size)})" case _: BytesEosType => - s"this.$io.readBytesFull()" + s"$io.readBytesFull()" case BytesTerminatedType(terminator, include, consume, eosError, _) => - s"this.$io.readBytesTerm($terminator, $include, $consume, $eosError)" + s"$io.readBytesTerm($terminator, $include, $consume, $eosError)" case BitsType1 => - s"this.$io.readBitsInt(1) != 0" + s"$io.readBitsInt(1) != 0" case BitsType(width: Int) => - s"this.$io.readBitsInt($width)" + s"$io.readBitsInt($width)" case t: UserType => val parent = t.forcedParent match { case Some(USER_TYPE_NO_PARENT) => "null" @@ -426,7 +419,7 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) case _ => "" } val addParams = Utils.join(t.args.map((a) => translator.translate(a)), ", ", ", ", "") - s"new ${types2class(t.classSpec.get.name)}(this.$io, $parent, $root$addEndian$addParams)" + s"new ${types2class(t.classSpec.get.name)}($io, $parent, $root$addEndian$addParams)" } } @@ -444,7 +437,7 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def userTypeDebugRead(id: String): Unit = { val incThis = if (id.startsWith("_t_")) "" else "this." - out.puts(s"$incThis$id._read();") + out.puts(s"$id._read();") } /** @@ -470,7 +463,10 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.inc out.puts(s"let ${expression(NAME_SWITCH_ON)} = ${expression(on)};") } else { - out.puts(s"switch (${expression(on)}) {") + // makes the typescript compiler happy when expression(on) evaluates to a number literal + out.puts(s"var __temp__ = ${expression(on)};") + out.puts("switch (__temp__) {") + out.inc } } @@ -497,7 +493,7 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts(s"else if (${switchCmpExpr(condition)}) {") out.inc } else { - out.puts(s"case ${expression(condition)}:") + out.puts(s"case ${expression(condition)}: {") out.inc } } @@ -509,7 +505,9 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) } else { out.puts("break;") out.dec + out.puts("}") } + } override def switchElseStart(): Unit = { @@ -517,49 +515,52 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts("else {") out.inc } else { - out.puts("default:") + out.puts("default: {") out.inc } } override def switchEnd(): Unit = { - if (switchIfs) - out.dec + out.dec out.puts("}") } override def instanceHeader(className: List[String], instName: InstanceIdentifier, dataType: DataType, isNullable: Boolean): Unit = { - def arr = dataType match { - case ArrayType(_) => "[]" - case _ => "" - } - - val typeof = isUserOrEnumType(dataType) match { - case "user" => "typeof " - case _ => "" - } - - val prototype = if (isUserOrEnumType(dataType) == "user") { - ".prototype" - } else { - "" - } - - out.puts(s"private ${privateMemberName(instName)}!: $typeof${kaitaiType2NativeType(dataType, false)}$prototype$arr;") + // val typeOut = dataType match { + // case SwitchType(_, cases, _) => { + // cases.map(kv => { + // val b = StringBuilder.newBuilder + // if (kv._2.isInstanceOf[UserType]) { + // b.append("typeof ") + // b.append(kaitaiType2NativeType(kv._2, false)) + // b.append(".prototype") + // } else { + // b.append(kaitaiType2NativeType(kv._2, false)) + // } + // b.toString() + // }).mkString(" | ") + // } + // case otherType => { + // val b = StringBuilder.newBuilder + // if (otherType.isInstanceOf[UserType]) { + // b.append("typeof ") + // b.append(kaitaiType2NativeType(kv._2, false)) + // b.append(".prototype") + // } else { + // b.append(kaitaiType2NativeType(kv._2, false)) + // } + // b.toString() + // } + // } + + out.puts(s"private ${privateMemberName(instName).substring(5)}!: ${kaitaiType2NativeType(dataType, false)};") + // out.puts(s"private ${privateMemberName(instName).substring(5)}!: $typeof${kaitaiType2NativeType(dataType, false)}$prototype$arr;") out.puts(s"get ${publicMemberName(instName)}() {") out.inc } override def instanceClear(instName: InstanceIdentifier) = { - out.puts(s"this.${privateMemberName(instName)}") - } - - // override def instanceSetCalculated(instName: InstanceIdentifier) = { - // out.puts(s"this.${privateMemberName(instName)} = // instanceSetCalculated") - // } - - override def instanceCalculate(instName: Identifier, dataType: DataType, value: Ast.expr) = { - out.puts(s"this.${privateMemberName(instName)} = $value"); + out.puts(s"${privateMemberName(instName)}") } override def instanceFooter: Unit = { @@ -569,14 +570,14 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) } override def instanceCheckCacheAndReturn(instName: InstanceIdentifier, dataType: DataType): Unit = { - out.puts(s"if (this.${privateMemberName(instName)} !== undefined)") + 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 this.${privateMemberName(instName)};") + out.puts(s"return ${privateMemberName(instName)};") } override def enumDeclaration(curClass: List[String], enumName: String, enumColl: Seq[(Long, EnumValueSpec)]): Unit = { @@ -621,7 +622,7 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) } } - override def privateMemberName(id: Identifier): String = s"${idToStr(id)}" + override def privateMemberName(id: Identifier): String = s"this.${idToStr(id)}" override def publicMemberName(id: Identifier): String = { id match { @@ -642,7 +643,7 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) val arrIndexExpr = rep match { case NoRepeat => "" case _: RepeatExpr => ".arr[i]" - case RepeatEos | _: RepeatUntil => s".arr[this.${privateMemberName(attrId)}.length${if (end) " - 1" else ""}]" + case RepeatEos | _: RepeatUntil => s".arr[${privateMemberName(attrId)}.length${if (end) " - 1" else ""}]" } s"this._debug.${idToStr(attrId)}$arrIndexExpr" @@ -659,18 +660,10 @@ object TypeScriptCompiler extends LanguageCompilerStatic def kaitaiType2NativeType(attrType: DataType, absolute: Boolean = false): String = { attrType match { - case Int1Type(false) => "number" - case IntMultiType(false, Width2, _) => "number" - case IntMultiType(false, Width4, _) => "number" - case IntMultiType(false, Width8, _) => "number" - - case Int1Type(true) => "number" - case IntMultiType(true, Width2, _) => "number" - case IntMultiType(true, Width4, _) => "number" - case IntMultiType(true, Width8, _) => "number" - - case FloatMultiType(Width4, _) => "number" - case FloatMultiType(Width8, _) => "number" + // 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" @@ -682,25 +675,26 @@ object TypeScriptCompiler extends LanguageCompilerStatic case _: BytesType => "Uint8Array" case t: UserType => - types2class(t.classSpec match { + 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 ArrayType(inType) => - s"${kaitaiType2NativeType(inType, absolute)}" + s"(${kaitaiType2NativeType(inType, absolute)})[]" - case CalcArrayType(inType) => s"${kaitaiType2NativeType(inType, absolute)}" + case CalcArrayType(inType) => s"(${kaitaiType2NativeType(inType, absolute)})[]" case AnyType => "any" case KaitaiStreamType => s"$kstreamName*" case KaitaiStructType | CalcKaitaiStructType => "any" - case st: SwitchType => kaitaiType2NativeType(st.combinedType, absolute) + // unfolds and removes duplicates by converting to set + case SwitchType(_, cases, _) => cases.map(kv => kaitaiType2NativeType(kv._2, false)).toSet.mkString(" | ") } } @@ -712,11 +706,4 @@ object TypeScriptCompiler extends LanguageCompilerStatic def types2class(types: List[String]): String = types.map(type2class).mkString(".") override def type2class(name: String): String = Utils.upperCamelCase(name) - - def isUserOrEnumType(inType: DataType): String = inType match { - case _: UserType => "user" - case _: EnumType => "enum" - case ArrayType(innerType) => isUserOrEnumType(innerType) - case _ => "other" - } } diff --git a/shared/src/main/scala/io/kaitai/struct/translators/TypeScriptTranslator.scala b/shared/src/main/scala/io/kaitai/struct/translators/TypeScriptTranslator.scala index 85358eb20..778f45248 100644 --- a/shared/src/main/scala/io/kaitai/struct/translators/TypeScriptTranslator.scala +++ b/shared/src/main/scala/io/kaitai/struct/translators/TypeScriptTranslator.scala @@ -1,6 +1,7 @@ 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 @@ -66,6 +67,8 @@ class TypeScriptTranslator(provider: TypeProvider) extends BaseTranslator(provid 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 = @@ -132,4 +135,4 @@ class TypeScriptTranslator(provider: TypeProvider) extends BaseTranslator(provid override def kaitaiStreamEof(value: Ast.expr): String = s"${translate(value)}.isEof()" -} \ No newline at end of file +} From 9d3752076822e7e692d94c6ecae01c2c53bfce32 Mon Sep 17 00:00:00 2001 From: fudgepop01 Date: Sat, 4 Apr 2020 15:58:56 -0400 Subject: [PATCH 3/3] updated typescript code to match js standandards --- build.sbt | 2 +- .../struct/languages/TypeScriptCompiler.scala | 239 +++++++++--------- .../translators/TypeScriptTranslator.scala | 6 +- 3 files changed, 123 insertions(+), 124 deletions(-) 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/languages/TypeScriptCompiler.scala b/shared/src/main/scala/io/kaitai/struct/languages/TypeScriptCompiler.scala index ff5e8e3cf..3cb89af64 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/TypeScriptCompiler.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/TypeScriptCompiler.scala @@ -1,7 +1,7 @@ package io.kaitai.struct.languages import io.kaitai.struct.datatype.DataType._ -import io.kaitai.struct.datatype.{CalcEndian, Endianness, DataType, FixedEndian, InheritedEndian} +import io.kaitai.struct.datatype._ import io.kaitai.struct.exprlang.Ast import io.kaitai.struct.exprlang.Ast.expr import io.kaitai.struct.format._ @@ -9,7 +9,7 @@ import io.kaitai.struct.languages.components._ import io.kaitai.struct.translators.TypeScriptTranslator import io.kaitai.struct.{ClassTypeProvider, RuntimeConfig, Utils} -class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) +class TypeScriptCompiler(val typeProvider: ClassTypeProvider, config: RuntimeConfig) extends LanguageCompiler(typeProvider, config) with ObjectOrientedLanguage with UpperCamelCaseClasses @@ -17,6 +17,7 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) with UniversalDoc with AllocateIOLocalVar with EveryReadIsExpression + with SwitchIfOps with FixedContentsUsingArrayByteLiteral { import TypeScriptCompiler._ @@ -156,7 +157,7 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def readHeader(endian: Option[FixedEndian], isEmpty: Boolean) = { val suffix = endian match { - case Some(e) => e.toSuffix.toUpperCase + case Some(e) => Utils.upperUnderscoreCase(e.toSuffix) case None => "" } out.puts(s"public _read$suffix() {") @@ -183,21 +184,16 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) doc.summary.foreach((summary) => out.putsLines(" * ", summary)) // http://usejsdoc.org/tags-see.html - doc.ref match { + doc.ref.foreach { case TextRef(text) => out.putsLines(" * ", s"@see $text") case UrlRef(url, text) => out.putsLines(" * ", s"@see {@link $url|$text}") - case NoRef => } out.puts( " */") } - // override def attrParse(attr: AttrLikeSpec, id: Identifier, defEndian: Option[Endianness]): Unit = { - // out.puts(s"this.meta[${privateMemberName(id)}] = '$attr'") - // } - override def attrParseHybrid(leProc: () => Unit, beProc: () => Unit): Unit = { out.puts("if (this._is_le) {") out.inc @@ -215,26 +211,25 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) s"$normalIO.ensureFixedContents($contents);") } - override def attrProcess(proc: ProcessExpr, varSrc: Identifier, varDest: Identifier): Unit = { - val srcName = privateMemberName(varSrc) - val destName = privateMemberName(varDest) + override def attrProcess(proc: ProcessExpr, varSrc: Identifier, varDest: Identifier, rep: RepeatSpec): Unit = { + val srcExpr = getRawIdExpr(varSrc, rep) - proc match { + val expr = proc match { case ProcessXor(xorValue) => val procName = translator.detectType(xorValue) match { case _: IntType => "processXorOne" case _: BytesType => "processXorMany" } - out.puts(s"$destName = $kstreamName.$procName($srcName, ${expression(xorValue)});") + s"$kstreamName.$procName($srcExpr, ${expression(xorValue)})" case ProcessZlib => - out.puts(s"$destName = $kstreamName.processZlib($srcName);") + s"$kstreamName.processZlib($srcExpr)" case ProcessRotate(isLeft, rotValue) => val expr = if (isLeft) { expression(rotValue) } else { s"8 - (${expression(rotValue)})" } - out.puts(s"$destName = $kstreamName.processRotateLeft($srcName, $expr, 1);") + s"$kstreamName.processRotateLeft($srcExpr, $expr, 1)" case ProcessCustom(name, args) => val nameInit = name.init val pkgName = if (nameInit.isEmpty) "" else nameInit.mkString("-") + "/" @@ -243,8 +238,9 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) importList.add(s"$pkgName$procClass") out.puts(s"let _process = new $procClass(${args.map(expression).mkString(", ")});") - out.puts(s"$destName = _process.decode($srcName);") + s"_process.decode($srcExpr)" } + handleAssignment(varDest, expr, rep, false) } override def allocateIO(varName: Identifier, rep: RepeatSpec): String = { @@ -253,16 +249,21 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) val ioName = s"_io_$langName" - val args = rep match { - case RepeatEos | RepeatUntil(_) => s"$memberCall[$memberCall.length - 1]" - case RepeatExpr(_) => s"$memberCall[i]" - case NoRepeat => memberCall - } + 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" @@ -288,7 +289,7 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) val ioProps = io match { case None => "" - case Some(x) => s"start: $x.pos, ioOffset: $x._byteOffset" + case Some(x) => s"start: $x.pos, ioOffset: $x.byteOffset" } val enumNameProps = attrType match { @@ -317,9 +318,11 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts("}") } - override def condRepeatEosHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean): Unit = { - if (needRaw) + 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 = [];") @@ -338,9 +341,11 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts("}") } - override def condRepeatExprHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, repeatExpr: expr): Unit = { - if (needRaw) + 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)});") @@ -357,9 +362,11 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts("}") } - override def condRepeatUntilHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, untilExpr: expr): Unit = { - if (needRaw) + 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 = [];") @@ -374,7 +381,7 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts(s"${privateMemberName(id)}.push($tmpName);") } - override def condRepeatUntilFooter(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, untilExpr: expr): Unit = { + override def condRepeatUntilFooter(id: Identifier, io: String, dataType: DataType, needRaw: NeedRaw, untilExpr: expr): Unit = { typeProvider._currentIteratorType = Some(dataType) out.puts("i++;") out.dec @@ -435,42 +442,58 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) expr2 } - override def userTypeDebugRead(id: String): Unit = { + override def userTypeDebugRead(id: String, dataType: DataType, assignType: DataType): Unit = { val incThis = if (id.startsWith("_t_")) "" else "this." out.puts(s"$id._read();") } - /** - * Designates switch mode. If false, we're doing real switch-case for this - * attribute. If true, we're doing if-based emulation. - */ - var switchIfs = false + override def switchRequiresIfs(onType: DataType): Boolean = onType match { + case _: IntType | _: BooleanType | _: EnumType | _: StrType => false + case _ => true + } - val NAME_SWITCH_ON = Ast.expr.Name(Ast.identifier(Identifier.SWITCH_ON)) + // - override def switchStart(id: Identifier, on: Ast.expr): Unit = { - val onType = translator.detectType(on) - typeProvider._currentSwitchType = Some(onType) + override def switchStart(id: Identifier, on: Ast.expr): Unit = + out.puts(s"switch (${expression(on)} {") - // Determine switching mode for this construct based on type - switchIfs = onType match { - case _: IntType | _: BooleanType | _: EnumType | _: StrType => false - case _ => true - } + override def switchCaseFirstStart(condition: Ast.expr): Unit = + switchCaseStart(condition) - if (switchIfs) { - out.puts("{") + override def switchCaseStart(condition: Ast.expr): Unit = { + out.puts(s"case ${expression(condition)}: {") out.inc - out.puts(s"let ${expression(NAME_SWITCH_ON)} = ${expression(on)};") - } else { - // makes the typescript compiler happy when expression(on) evaluates to a number literal - out.puts(s"var __temp__ = ${expression(on)};") - out.puts("switch (__temp__) {") + } + + override def switchCaseEnd(): Unit = { + out.puts("break;") + out.dec + out.puts("}") + } + + override def switchElseStart(): Unit = { + out.puts("default: {") out.inc - } } - def switchCmpExpr(condition: Ast.expr): String = + 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, @@ -479,82 +502,35 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) ) ) - override def switchCaseFirstStart(condition: Ast.expr): Unit = { - if (switchIfs) { - out.puts(s"if (${switchCmpExpr(condition)}) {") - out.inc - } else { - switchCaseStart(condition) - } + override def switchIfCaseFirstStart(condition: Ast.expr): Unit = { + out.puts(s"if (${switchCmpExpr(condition)}) {") + out.inc } - override def switchCaseStart(condition: Ast.expr): Unit = { - if (switchIfs) { - out.puts(s"else if (${switchCmpExpr(condition)}) {") - out.inc - } else { - out.puts(s"case ${expression(condition)}: {") - out.inc - } + override def switchIfCaseStart(condition: Ast.expr): Unit = { + out.puts(s"else if (${switchCmpExpr(condition)}) {") + out.inc } - override def switchCaseEnd(): Unit = { - if (switchIfs) { - out.dec - out.puts("}") - } else { - out.puts("break;") - out.dec - out.puts("}") - } - + override def switchIfCaseEnd(): Unit = { + out.dec + out.puts("}") } - override def switchElseStart(): Unit = { - if (switchIfs) { - out.puts("else {") - out.inc - } else { - out.puts("default: {") - out.inc - } + override def switchIfElseStart(): Unit = { + out.puts("else {") + out.inc } - override def switchEnd(): Unit = { + override def switchIfEnd(): Unit = { out.dec out.puts("}") } - override def instanceHeader(className: List[String], instName: InstanceIdentifier, dataType: DataType, isNullable: Boolean): Unit = { - // val typeOut = dataType match { - // case SwitchType(_, cases, _) => { - // cases.map(kv => { - // val b = StringBuilder.newBuilder - // if (kv._2.isInstanceOf[UserType]) { - // b.append("typeof ") - // b.append(kaitaiType2NativeType(kv._2, false)) - // b.append(".prototype") - // } else { - // b.append(kaitaiType2NativeType(kv._2, false)) - // } - // b.toString() - // }).mkString(" | ") - // } - // case otherType => { - // val b = StringBuilder.newBuilder - // if (otherType.isInstanceOf[UserType]) { - // b.append("typeof ") - // b.append(kaitaiType2NativeType(kv._2, false)) - // b.append(".prototype") - // } else { - // b.append(kaitaiType2NativeType(kv._2, false)) - // } - // b.toString() - // } - // } + // + 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"private ${privateMemberName(instName).substring(5)}!: $typeof${kaitaiType2NativeType(dataType, false)}$prototype$arr;") out.puts(s"get ${publicMemberName(instName)}() {") out.inc } @@ -605,7 +581,7 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts } - def enumValue(enumName: String, label: String) = label.toUpperCase + def enumValue(enumName: String, label: String) = Utils.upperUnderscoreCase(label) override def debugClassSequence(seq: List[AttrSpec]) = { val seqStr = seq.map((attr) => "\"" + idToStr(attr.id) + "\"").mkString(", ") @@ -633,6 +609,23 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) 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 @@ -652,7 +645,8 @@ class TypeScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) object TypeScriptCompiler extends LanguageCompilerStatic with UpperCamelCaseClasses - with StreamStructNames { + with StreamStructNames + with ExceptionNames { override def getCompiler( tp: ClassTypeProvider, config: RuntimeConfig @@ -683,8 +677,8 @@ object TypeScriptCompiler extends LanguageCompilerStatic // for now enums are numbers by default case t: EnumType => "number" //types2class(t.enumSpec.get.name) - case ArrayType(inType) => - s"(${kaitaiType2NativeType(inType, absolute)})[]" + case at: ArrayType => + s"(${kaitaiType2NativeType(at.elType, absolute)})[]" case CalcArrayType(inType) => s"(${kaitaiType2NativeType(inType, absolute)})[]" @@ -705,5 +699,10 @@ object TypeScriptCompiler extends LanguageCompilerStatic 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/translators/TypeScriptTranslator.scala b/shared/src/main/scala/io/kaitai/struct/translators/TypeScriptTranslator.scala index 778f45248..20f814f2a 100644 --- a/shared/src/main/scala/io/kaitai/struct/translators/TypeScriptTranslator.scala +++ b/shared/src/main/scala/io/kaitai/struct/translators/TypeScriptTranslator.scala @@ -55,7 +55,7 @@ class TypeScriptTranslator(provider: TypeProvider) extends BaseTranslator(provid } override def doEnumByLabel(enumType: List[String], label: String): String = - s"${TypeScriptCompiler.types2class(enumType)}.${label.toUpperCase}" + 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 @@ -63,11 +63,11 @@ class TypeScriptTranslator(provider: TypeProvider) extends BaseTranslator(provid 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 doSubscript(container: expr, idx: expr): String = + 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 = + override def doCast(value: Ast.expr, typeName: DataType): String = s"(${translate(value)} as ${TypeScriptCompiler.kaitaiType2NativeType(typeName, false)})" // Predefined methods of various types