import {Container} from './Container'; import {Value, IntValue, FloatValue, StringValue, DivertTargetValue, VariablePointerValue, ListValue} from './Value'; import {Glue} from './Glue'; import {ControlCommand} from './ControlCommand'; import {PushPopType} from './PushPop'; import {Divert} from './Divert'; import {ChoicePoint} from './ChoicePoint'; import {VariableReference} from './VariableReference'; import {VariableAssignment} from './VariableAssignment'; import {NativeFunctionCall} from './NativeFunctionCall'; import {Void} from './Void'; import {Tag} from './Tag'; import {Path} from './Path'; import {Choice} from './Choice'; import {ListDefinition} from './ListDefinition'; import {ListDefinitionsOrigin} from './ListDefinitionsOrigin'; import {InkListItem, InkList} from './InkList'; import {InkObject} from './Object'; import {JObject} from './JObject'; import {asOrNull, asNumberOrThrows} from './TypeAssertion'; import {throwNullException} from './NullException'; // tslint:disable no-conditional-assignment export class JsonSerialisation{ public static ListToJArray(serialisables: InkObject[]){ let jArray: any[] = []; for (let s of serialisables) { jArray.push(this.RuntimeObjectToJToken(s)); } return jArray; } public static JArrayToRuntimeObjList(jArray: any[], skipLast: boolean = false){ let count = jArray.length; if (skipLast) count--; let list: InkObject[] = []; for (let i = 0; i < count; i++){ let jTok = jArray[i]; let runtimeObj = this.JTokenToRuntimeObject(jTok); if (runtimeObj === null) { return throwNullException('runtimeObj'); } list.push(runtimeObj); } return list; } public static DictionaryRuntimeObjsToJObject(dictionary: Map){ let jsonObj: JObject = {}; for (let [key, value] of dictionary){ let runtimeObj = asOrNull(value, InkObject); if (runtimeObj != null) jsonObj[key] = this.RuntimeObjectToJToken(runtimeObj); } return jsonObj; } public static JObjectToDictionaryRuntimeObjs(jObject: JObject){ let dict: Map = new Map(); for (let key in jObject){ if (jObject.hasOwnProperty(key)) { let inkObject = this.JTokenToRuntimeObject(jObject[key]); if (inkObject === null) { return throwNullException('inkObject'); } dict.set(key, inkObject); } } return dict; } public static JObjectToIntDictionary(jObject: JObject){ let dict: Map = new Map(); for (let key in jObject){ if (jObject.hasOwnProperty(key)) { dict.set(key, parseInt(jObject[key])); } } return dict; } public static IntDictionaryToJObject(dict: Map){ let jObj: JObject = {}; for (let [key, value] of dict){ jObj[key] = asNumberOrThrows(value); } return jObj; } public static JTokenToRuntimeObject(token: any): InkObject | null { if (typeof token === 'number' && !isNaN(token)){ return Value.Create(token); } if (typeof token === 'string'){ let str = token.toString(); // String value let firstChar = str[0]; if (firstChar == '^') return new StringValue(str.substring(1)); else if(firstChar == '\n' && str.length == 1) return new StringValue('\n'); // Glue if (str == '<>') return new Glue(); // Control commands (would looking up in a hash set be faster?) for (let i = 0; i < JsonSerialisation._controlCommandNames.length; ++i) { let cmdName = JsonSerialisation._controlCommandNames[i]; if (str == cmdName) { return new ControlCommand(i); } } // Native functions if (str == 'L^') str = '^'; if( NativeFunctionCall.CallExistsWithName(str) ) return NativeFunctionCall.CallWithName(str); // Pop if (str == '->->') return ControlCommand.PopTunnel(); else if (str == '~ret') return ControlCommand.PopFunction(); // Void if (str == 'void') return new Void (); } if (typeof token === 'object' && !Array.isArray(token)){ let obj: JObject = token; let propValue; // Divert target value to path if (obj['^->']){ propValue = obj['^->']; return new DivertTargetValue(new Path(propValue.toString())); } // VariablePointerValue if (obj['^var']) { propValue = obj['^var']; let varPtr = new VariablePointerValue(propValue.toString()); if ('ci' in obj){ propValue = obj['ci']; varPtr.contextIndex = parseInt(propValue); } return varPtr; } // Divert let isDivert = false; let pushesToStack = false; let divPushType = PushPopType.Function; let external = false; if (propValue = obj['->']) { isDivert = true; } else if (propValue = obj['f()']) { isDivert = true; pushesToStack = true; divPushType = PushPopType.Function; } else if (propValue = obj['->t->']) { isDivert = true; pushesToStack = true; divPushType = PushPopType.Tunnel; } else if (propValue = obj['x()']) { isDivert = true; external = true; pushesToStack = false; divPushType = PushPopType.Function; } if (isDivert) { let divert = new Divert(); divert.pushesToStack = pushesToStack; divert.stackPushType = divPushType; divert.isExternal = external; let target = propValue.toString(); if (propValue = obj['var']) divert.variableDivertName = target; else divert.targetPathString = target; divert.isConditional = !!obj['c']; if (external) { if (propValue = obj['exArgs']) divert.externalArgs = parseInt(propValue); } return divert; } // Choice if (propValue = obj['*']) { let choice = new ChoicePoint(); choice.pathStringOnChoice = propValue.toString(); if (propValue = obj['flg']) choice.flags = parseInt(propValue); return choice; } // Variable reference if (propValue = obj['VAR?']) { return new VariableReference(propValue.toString()); } else if (propValue = obj['CNT?']) { let readCountVarRef = new VariableReference(); readCountVarRef.pathStringForCount = propValue.toString(); return readCountVarRef; } // Variable assignment let isVarAss = false; let isGlobalVar = false; if (propValue = obj['VAR=']) { isVarAss = true; isGlobalVar = true; } else if (propValue = obj['temp=']) { isVarAss = true; isGlobalVar = false; } if (isVarAss) { let varName = propValue.toString(); let isNewDecl = !obj['re']; let varAss = new VariableAssignment(varName, isNewDecl); varAss.isGlobal = isGlobalVar; return varAss; } if (obj['#'] !== undefined){ propValue = obj['#']; return new Tag(propValue.toString()); } // List value if (propValue = obj['list']) { // var listContent = (Dictionary)propValue; let listContent: JObject = propValue; let rawList = new InkList(); if (propValue = obj['origins']) { // var namesAsObjs = (List)propValue; let namesAsObjs = propValue as string[]; // rawList.SetInitialOriginNames(namesAsObjs.Cast().ToList()); rawList.SetInitialOriginNames(namesAsObjs); } for (let key in listContent){ if (listContent.hasOwnProperty(key)) { let nameToVal = listContent[key]; let item = new InkListItem(key); let val = parseInt(nameToVal); rawList.Add(item, val); } } return new ListValue(rawList); } if (obj['originalChoicePath'] != null) return this.JObjectToChoice(obj); } // Array is always a Runtime.Container if (Array.isArray(token)){ return this.JArrayToContainer(token); } if (token === null || token === undefined) return null; throw new Error('Failed to convert token to runtime object: ' + JSON.stringify(token)); } public static RuntimeObjectToJToken(obj: InkObject){ // var container = obj as Container; let container = asOrNull(obj, Container); if (container) { return this.ContainerToJArray(container); } // var divert = obj as Divert; let divert = asOrNull(obj, Divert); if (divert) { let divTypeKey = '->'; if (divert.isExternal) divTypeKey = 'x()'; else if (divert.pushesToStack) { if (divert.stackPushType == PushPopType.Function) divTypeKey = 'f()'; else if (divert.stackPushType == PushPopType.Tunnel) divTypeKey = '->t->'; } let targetStr; if (divert.hasVariableTarget) targetStr = divert.variableDivertName; else targetStr = divert.targetPathString; let jObj: JObject = {}; jObj[divTypeKey] = targetStr; if (divert.hasVariableTarget) jObj['var'] = true; if (divert.isConditional) jObj['c'] = true; if (divert.externalArgs > 0) jObj['exArgs'] = divert.externalArgs; return jObj; } // var choicePoint = obj as ChoicePoint; let choicePoint = asOrNull(obj, ChoicePoint); if (choicePoint) { let jObj: JObject = {}; jObj['*'] = choicePoint.pathStringOnChoice; jObj['flg'] = choicePoint.flags; return jObj; } // var intVal = obj as IntValue; let intVal = asOrNull(obj, IntValue); if (intVal) return intVal.value; // var floatVal = obj as FloatValue; let floatVal = asOrNull(obj, FloatValue); if (floatVal) return floatVal.value; // var strVal = obj as StringValue; let strVal = asOrNull(obj, StringValue); if (strVal) { if (strVal.isNewline) return '\n'; else return '^' + strVal.value; } // var listVal = obj as ListValue; let listVal = asOrNull(obj, ListValue); if (listVal) { return this.InkListToJObject(listVal); } // var divTargetVal = obj as DivertTargetValue; let divTargetVal = asOrNull(obj, DivertTargetValue); if (divTargetVal) { let divTargetJsonObj: JObject = {}; if (divTargetVal.value === null) { return throwNullException('divTargetVal.value'); } divTargetJsonObj['^->'] = divTargetVal.value.componentsString; return divTargetJsonObj; } // var varPtrVal = obj as VariablePointerValue; let varPtrVal = asOrNull(obj, VariablePointerValue); if (varPtrVal) { let varPtrJsonObj: JObject = {}; varPtrJsonObj['^var'] = varPtrVal.value; varPtrJsonObj['ci'] = varPtrVal.contextIndex; return varPtrJsonObj; } // var glue = obj as Runtime.Glue; let glue = asOrNull(obj, Glue); if (glue) return '<>'; // var controlCmd = obj as ControlCommand; let controlCmd = asOrNull(obj, ControlCommand); if (controlCmd) { return JsonSerialisation._controlCommandNames[controlCmd.commandType]; } // var nativeFunc = obj as Runtime.NativeFunctionCall; let nativeFunc = asOrNull(obj, NativeFunctionCall); if (nativeFunc) { let name = nativeFunc.name; if (name == '^') name = 'L^'; return name; } // Variable reference // var varRef = obj as VariableReference; let varRef = asOrNull(obj, VariableReference); if (varRef) { let jObj: JObject = {}; let readCountPath = varRef.pathStringForCount; if (readCountPath != null) { jObj['CNT?'] = readCountPath; } else { jObj['VAR?'] = varRef.name; } return jObj; } // Variable assignment // var varAss = obj as VariableAssignment; let varAss = asOrNull(obj, VariableAssignment); if (varAss) { let key = varAss.isGlobal ? 'VAR=' : 'temp='; let jObj: JObject = {}; jObj[key] = varAss.variableName; // Reassignment? if (!varAss.isNewDeclaration) jObj['re'] = true; return jObj; } // var voidObj = obj as Void; let voidObj = asOrNull(obj, Void); if (voidObj) return 'void'; // var tag = obj as Tag; let tag = asOrNull(obj, Tag); if (tag) { let jObj: JObject = {}; jObj['#'] = tag.text; return jObj; } // Used when serialising save state only // var choice = obj as Choice; let choice = asOrNull(obj, Choice); if (choice) return this.ChoiceToJObject(choice); throw new Error('Failed to convert runtime object to Json token: ' + obj); } public static ContainerToJArray(container: Container){ let jArray = this.ListToJArray(container.content); let namedOnlyContent = container.namedOnlyContent; let countFlags = container.countFlags; if (namedOnlyContent != null && namedOnlyContent.size > 0 || countFlags > 0 || container.name != null) { let terminatingObj; if (namedOnlyContent != null) { terminatingObj = this.DictionaryRuntimeObjsToJObject(namedOnlyContent); for (let key in terminatingObj){ if (terminatingObj.hasOwnProperty(key)) { // var subContainerJArray = namedContentObj.Value as JArray; let subContainerJArray = terminatingObj[key]; if (subContainerJArray != null) { // var attrJObj = subContainerJArray [subContainerJArray.Count - 1] as JObject; let attrJObj = subContainerJArray[subContainerJArray.length - 1] as JObject; if (attrJObj != null) { delete attrJObj['#n']; if (Object.keys(attrJObj).length == 0) subContainerJArray[subContainerJArray.length - 1] = null; } } } } } else terminatingObj = {}; if( countFlags > 0 ) terminatingObj['#f'] = countFlags; if( container.name != null ) terminatingObj['#n'] = container.name; jArray.push(terminatingObj); } // Add null terminator to indicate that there's no dictionary else { jArray.push(null); } return jArray; } public static JArrayToContainer(jArray: any[]){ let container = new Container(); container.content = this.JArrayToRuntimeObjList(jArray, true); let terminatingObj = jArray[jArray.length - 1] as JObject; if (terminatingObj != null) { let namedOnlyContent = new Map(); for (let key in terminatingObj){ if (key == '#f') { container.countFlags = parseInt(terminatingObj[key]); } else if (key == '#n') { container.name = terminatingObj[key].toString(); } else { let namedContentItem = this.JTokenToRuntimeObject(terminatingObj[key]); // var namedSubContainer = namedContentItem as Container; let namedSubContainer = asOrNull(namedContentItem, Container); if (namedSubContainer) namedSubContainer.name = key; namedOnlyContent.set(key, namedContentItem); } } container.namedOnlyContent = namedOnlyContent; } return container; } public static JObjectToChoice(jObj: JObject){ let choice = new Choice(); choice.text = jObj['text'].toString(); choice.index = parseInt(jObj['index']); choice.sourcePath = jObj['originalChoicePath'].toString(); choice.originalThreadIndex = parseInt(jObj['originalThreadIndex']); choice.pathStringOnChoice = jObj['targetPath'].toString(); return choice; } public static ChoiceToJObject(choice: Choice){ let jObj: JObject = {}; jObj['text'] = choice.text; jObj['index'] = choice.index; jObj['originalChoicePath'] = choice.sourcePath; jObj['originalThreadIndex'] = choice.originalThreadIndex; jObj['targetPath'] = choice.pathStringOnChoice; return jObj; } public static InkListToJObject(listVal: ListValue){ let rawList = listVal.value; if (rawList === null) { return throwNullException('rawList'); } let dict: JObject = {}; let content: JObject = {}; for (let [key, val] of rawList) { let item = InkListItem.fromSerializedKey(key); content[item.toString()] = val; } dict['list'] = content; if (rawList.Count == 0 && rawList.originNames != null && rawList.originNames.length > 0) { // dict["origins"] = rawList.originNames.Cast ().ToList (); dict['origins'] = rawList.originNames; } return dict; } public static ListDefinitionsToJToken(origin: ListDefinitionsOrigin){ let result: JObject = {}; for (let def of origin.lists) { let listDefJson: JObject = {}; for (let [key, val] of def.items) { let item = InkListItem.fromSerializedKey(key); if (item.itemName === null) { return throwNullException('item.itemName'); } listDefJson[item.itemName] = val; } result[def.name] = listDefJson; } return result; } public static JTokenToListDefinitions(obj: JObject){ // var defsObj = (Dictionary)obj; let defsObj = obj; let allDefs: ListDefinition[] = []; for (let key in defsObj){ if (defsObj.hasOwnProperty(key)) { let name = key.toString(); // var listDefJson = (Dictionary)kv.Value; let listDefJson: JObject = defsObj[key]; // Cast (string, object) to (string, int) for items let items: Map = new Map(); for (let nameValueKey in listDefJson){ if (defsObj.hasOwnProperty(key)) { let nameValue = listDefJson[nameValueKey]; items.set(nameValueKey, parseInt(nameValue)); } } let def = new ListDefinition(name, items); allDefs.push(def); } } return new ListDefinitionsOrigin(allDefs); } private static _controlCommandNames = (() => { let _controlCommandNames: string[] = []; _controlCommandNames[ControlCommand.CommandType.EvalStart] = 'ev'; _controlCommandNames[ControlCommand.CommandType.EvalOutput] = 'out'; _controlCommandNames[ControlCommand.CommandType.EvalEnd] = '/ev'; _controlCommandNames[ControlCommand.CommandType.Duplicate] = 'du'; _controlCommandNames[ControlCommand.CommandType.PopEvaluatedValue] = 'pop'; _controlCommandNames[ControlCommand.CommandType.PopFunction] = '~ret'; _controlCommandNames[ControlCommand.CommandType.PopTunnel] = '->->'; _controlCommandNames[ControlCommand.CommandType.BeginString] = 'str'; _controlCommandNames[ControlCommand.CommandType.EndString] = '/str'; _controlCommandNames[ControlCommand.CommandType.NoOp] = 'nop'; _controlCommandNames[ControlCommand.CommandType.ChoiceCount] = 'choiceCnt'; _controlCommandNames[ControlCommand.CommandType.Turns] = 'turn'; _controlCommandNames[ControlCommand.CommandType.TurnsSince] = 'turns'; _controlCommandNames[ControlCommand.CommandType.ReadCount] = 'readc'; _controlCommandNames[ControlCommand.CommandType.Random] = 'rnd'; _controlCommandNames[ControlCommand.CommandType.SeedRandom] = 'srnd'; _controlCommandNames[ControlCommand.CommandType.VisitIndex] = 'visit'; _controlCommandNames[ControlCommand.CommandType.SequenceShuffleIndex] = 'seq'; _controlCommandNames[ControlCommand.CommandType.StartThread] = 'thread'; _controlCommandNames[ControlCommand.CommandType.Done] = 'done'; _controlCommandNames[ControlCommand.CommandType.End] = 'end'; _controlCommandNames[ControlCommand.CommandType.ListFromInt] = 'listInt'; _controlCommandNames[ControlCommand.CommandType.ListRange] = 'range'; _controlCommandNames[ControlCommand.CommandType.ListRandom] = 'lrnd'; for (let i = 0; i < ControlCommand.CommandType.TOTAL_VALUES; ++i) { if (_controlCommandNames[i] == null) throw new Error('Control command not accounted for in serialisation'); } return _controlCommandNames; })(); }